@giamme/forge 0.0.0-stage → 0.9.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 +205 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SKILL.md +123 -0
- package/VERSION +1 -0
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +79 -0
- package/package.json +27 -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 +365 -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 +671 -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-runs.py +121 -0
- package/scripts/forge-runtime.py +586 -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 +240 -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 +84 -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 +347 -0
- package/ui/THIRD_PARTY_NOTICES.md +177 -0
- package/ui/docs/api.md +970 -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 +214 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +582 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1424 -0
- package/ui/forge_ui/server.py +312 -0
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +1079 -0
- package/ui/forge_ui/static/cloud3d.js +647 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +397 -0
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +235 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +67 -0
- package/ui/forge_ui/static/iso.js +816 -0
- package/ui/forge_ui/static/leaderboard.js +311 -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 +562 -0
- package/ui/forge_ui/static/runmodel.js +667 -0
- package/ui/forge_ui/static/scene3d.js +1471 -0
- package/ui/forge_ui/static/search.js +120 -0
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +1073 -0
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# forge-memory.sh — project memory that outlives a single forge run.
|
|
3
|
+
#
|
|
4
|
+
# Every forge dispatch is a clean slate, so without this the tenth run in a repo
|
|
5
|
+
# rediscovers what the first one learned. Two files, deliberately split:
|
|
6
|
+
#
|
|
7
|
+
# .forge/memory.md curated, hard-capped, injected into every prompt
|
|
8
|
+
# .forge/ledger.tsv append-only, one row per dispatch, NEVER injected
|
|
9
|
+
#
|
|
10
|
+
# That split is what makes "remembers everything" and "small" compatible. The
|
|
11
|
+
# ledger is unbounded but costs no context; only memory.md is paid for, on every
|
|
12
|
+
# dispatch of every future run, which is why it has a hard cap.
|
|
13
|
+
#
|
|
14
|
+
# Writing is automatic, so the judgment has to be free and the bookkeeping has to
|
|
15
|
+
# be deterministic: the model that just did the work emits its learning in-band
|
|
16
|
+
# (a FORGE_LEARNING line in its final message, alongside FORGE_VERDICT), and this
|
|
17
|
+
# script does dedup, recurrence counting, staleness and capping in bash. No model
|
|
18
|
+
# ever decides what to prune.
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# forge-memory.sh inject <repo> <planner|dwarf|qa>
|
|
22
|
+
# forge-memory.sh note <planner|dwarf|qa>
|
|
23
|
+
# forge-memory.sh record <repo> --last <file> --role <r> [--run-id X] [--task T]
|
|
24
|
+
# [--model M] [--verdict V] [--duration S]
|
|
25
|
+
# forge-memory.sh show <repo>
|
|
26
|
+
# forge-memory.sh prune <repo>
|
|
27
|
+
# forge-memory.sh spend <repo> [--run <run-id>]
|
|
28
|
+
#
|
|
29
|
+
# Exit codes: 0 ok | 2 usage error
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
# Where to log the Jev calls this script makes. Empty means nowhere, which is what it
|
|
33
|
+
# meant everywhere until a real run showed 51 logged calls and an unknown number of
|
|
34
|
+
# unlogged memory ones sitting beside them.
|
|
35
|
+
JEV_RUN_DIR=""
|
|
36
|
+
|
|
37
|
+
die() { printf 'forge: %s\n' "$1" >&2; exit "${2:-2}"; }
|
|
38
|
+
|
|
39
|
+
MAX_LINES="${FORGE_MEMORY_MAX_LINES:-40}"
|
|
40
|
+
MAX_BYTES="${FORGE_MEMORY_MAX_BYTES:-4096}"
|
|
41
|
+
TAB="$(printf '\t')"
|
|
42
|
+
|
|
43
|
+
# Derived here rather than inherited: forge-parallel.sh sets SKILL_DIR but does not
|
|
44
|
+
# export it, and this script is also run directly. Getting it wrong would leave the Jev
|
|
45
|
+
# curation below silently disabled instead of visibly broken.
|
|
46
|
+
SKILL_DIR="${SKILL_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
47
|
+
|
|
48
|
+
# Writing files into someone's repo automatically needs an escape hatch that does
|
|
49
|
+
# not require editing the skill.
|
|
50
|
+
memory_off() { [ "${FORGE_MEMORY:-on}" = "off" ]; }
|
|
51
|
+
|
|
52
|
+
# --- run dataset ----------------------------------------------------------------
|
|
53
|
+
# What curation decided, for the run dataset (scripts/forge_dataset): the final ledger
|
|
54
|
+
# rows say what was kept, but not that Jev refiled a category, merged a paraphrase or
|
|
55
|
+
# muted a fact, nor which facts a rebuild dropped and why. Fail-open and silent.
|
|
56
|
+
MEMORY_DROPS="" # when set, rebuild appends "<reason>\t<category>\t<text>" rows here
|
|
57
|
+
memory_dataset_on() {
|
|
58
|
+
case "${FORGE_DATASET:-on}" in off|OFF|Off|0|no|false) return 1 ;; esac
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
memory_dataset_event() { # memory_dataset_event <name> <repo> <run-dir> [--set k=v | --file label=path]...
|
|
62
|
+
local name="$1" repo="$2" dir="$3"; shift 3
|
|
63
|
+
memory_dataset_on || return 0
|
|
64
|
+
python3 "$SKILL_DIR/scripts/forge-dataset.py" event "$name" --repo "$repo" ${dir:+--dir "$dir"} "$@" \
|
|
65
|
+
>/dev/null 2>&1 || true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dir_for() { echo "$1/.forge"; }
|
|
69
|
+
memory_for() { echo "$1/.forge/memory.md"; }
|
|
70
|
+
ledger_for() { echo "$1/.forge/ledger.tsv"; }
|
|
71
|
+
|
|
72
|
+
# Normalized dedup key: lowercase, every run of non-alphanumerics collapsed to a
|
|
73
|
+
# single space. Exact match after that — genuinely different phrasings will NOT
|
|
74
|
+
# merge. That is mitigated by a feedback loop rather than by clever matching:
|
|
75
|
+
# memory is in the prompt, so a reviewer that has seen "2x: route added without
|
|
76
|
+
# updating openapi.yaml" tends to re-emit that phrasing instead of inventing one.
|
|
77
|
+
norm_key() {
|
|
78
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
|
|
79
|
+
| sed 's/[^a-z0-9]\{1,\}/ /g; s/^ //; s/ $//'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# A trailing [path] is what makes an entry's staleness detectable later.
|
|
83
|
+
anchor_of() {
|
|
84
|
+
printf '%s' "$1" | sed -n 's/.*\[\([^][]\{1,\}\)\][[:space:]]*$/\1/p'
|
|
85
|
+
}
|
|
86
|
+
strip_anchor() {
|
|
87
|
+
printf '%s' "$1" | sed 's/[[:space:]]*\[[^][]\{1,\}\][[:space:]]*$//'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Tabs are the ledger's field separator and newlines are its record separator, so
|
|
91
|
+
# neither may survive into a field.
|
|
92
|
+
sanitize() { printf '%s' "$1" | tr '\t\n' ' ' | sed 's/ \{1,\}/ /g; s/^ //; s/ $//'; }
|
|
93
|
+
|
|
94
|
+
# One discursive model should not be able to spend the whole byte budget on a
|
|
95
|
+
# single entry. Truncate the prose but keep the [anchor], since that is what lets
|
|
96
|
+
# the entry be recognised as stale later.
|
|
97
|
+
MAX_FACT_CHARS="${FORGE_MEMORY_MAX_FACT_CHARS:-180}"
|
|
98
|
+
clamp_fact() {
|
|
99
|
+
local text="$1" anchor body
|
|
100
|
+
[ "${#text}" -le "$MAX_FACT_CHARS" ] && { printf '%s' "$text"; return; }
|
|
101
|
+
anchor="$(anchor_of "$text")"
|
|
102
|
+
body="$(strip_anchor "$text")"
|
|
103
|
+
local room="$MAX_FACT_CHARS"
|
|
104
|
+
[ -n "$anchor" ] && room=$((MAX_FACT_CHARS - ${#anchor} - 3))
|
|
105
|
+
[ "$room" -lt 40 ] && room=40
|
|
106
|
+
body="$(printf '%s' "$body" | cut -c1-"$room" | sed 's/[^ ]*$//; s/ $//')"
|
|
107
|
+
if [ -n "$anchor" ]; then printf '%s… [%s]' "$body" "$anchor"; else printf '%s…' "$body"; fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
section_title() {
|
|
111
|
+
case "$1" in
|
|
112
|
+
verify) echo "## Verify" ;;
|
|
113
|
+
trap) echo "## Known traps" ;;
|
|
114
|
+
finding) echo "## Recurring QA findings" ;;
|
|
115
|
+
esac
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# --- rebuild ------------------------------------------------------------------
|
|
119
|
+
# memory.md is regenerated from the whole ledger rather than mutated in place.
|
|
120
|
+
# Rebuilding is idempotent and order-independent, so a crashed or concurrent run
|
|
121
|
+
# cannot leave memory half-updated — it can only leave the ledger short a row.
|
|
122
|
+
rebuild() { # rebuild <repo> -> prints "kept<TAB>pruned_stale<TAB>pruned_cap"
|
|
123
|
+
local repo="$1" led mem tmp cand jev_loaded=0
|
|
124
|
+
if [ "${FORGE_MEMORY_DEEP_PRUNE:-0}" = 1 ] && [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
|
|
125
|
+
source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
|
|
126
|
+
fi
|
|
127
|
+
led="$(ledger_for "$repo")"; mem="$(memory_for "$repo")"
|
|
128
|
+
[ -f "$led" ] || return 0
|
|
129
|
+
tmp="$mem.tmp.$$"; cand="$mem.cand.$$"
|
|
130
|
+
|
|
131
|
+
# Group by (category, key). count = number of DISTINCT runs the key appeared in,
|
|
132
|
+
# which is what makes a recurrence threshold meaningful: one run emitting the
|
|
133
|
+
# same learning twice is still one occurrence.
|
|
134
|
+
awk -F'\t' '
|
|
135
|
+
/^#/ || NF < 9 { next }
|
|
136
|
+
# Column 11 is the injectable flag. Rows written before it existed have NF == 10
|
|
137
|
+
# and are treated as injectable, so an existing ledger keeps behaving as it did.
|
|
138
|
+
NF >= 11 && $11 == "0" { next }
|
|
139
|
+
{
|
|
140
|
+
ts=$1; run=$2; cat=$7; key=$8; txt=$9
|
|
141
|
+
k = cat "\034" key
|
|
142
|
+
if (!((k "\034" run) in seen_run)) { seen_run[k "\034" run]=1; cnt[k]++ }
|
|
143
|
+
if (ts >= lastts[k]) { lastts[k]=ts; last[k]=txt }
|
|
144
|
+
catof[k]=cat
|
|
145
|
+
}
|
|
146
|
+
END { for (k in cnt) printf "%s\t%d\t%s\t%s\n", catof[k], cnt[k], lastts[k], last[k] }
|
|
147
|
+
' "$led" > "$cand" 2>/dev/null
|
|
148
|
+
|
|
149
|
+
local kept=0 stale=0 capped=0
|
|
150
|
+
: > "$tmp"
|
|
151
|
+
local sec cat count ts text anchor line
|
|
152
|
+
for sec in verify trap finding; do
|
|
153
|
+
local body="$mem.sec.$$"; : > "$body"
|
|
154
|
+
# Highest count first, then most recent — the ordering a human scans for a
|
|
155
|
+
# systemic problem, and deterministic so tests can assert on it.
|
|
156
|
+
while IFS=$'\t' read -r cat count ts text; do
|
|
157
|
+
[ "$cat" = "$sec" ] || continue
|
|
158
|
+
# A finding seen once is an incident, not a pattern. Promoting it would fill
|
|
159
|
+
# memory with one-offs, which is exactly how this becomes a junk drawer.
|
|
160
|
+
if [ "$sec" = finding ] && [ "$count" -lt 2 ]; then continue; fi
|
|
161
|
+
anchor="$(anchor_of "$text")"
|
|
162
|
+
# An entry anchored to a path that no longer exists is stale by construction:
|
|
163
|
+
# the code it describes is gone, so the fact is at best useless and at worst
|
|
164
|
+
# actively misleading. Drop it rather than wait for the cap to evict it.
|
|
165
|
+
if [ -n "$anchor" ] && [ ! -e "$repo/$anchor" ]; then
|
|
166
|
+
[ -z "$MEMORY_DROPS" ] || printf 'anchor_missing\t%s\t%s\n' "$cat" "$text" >> "$MEMORY_DROPS"
|
|
167
|
+
stale=$((stale+1)); continue
|
|
168
|
+
fi
|
|
169
|
+
# The anchor still exists, but the fact about it may have quietly stopped being
|
|
170
|
+
# true. Only on an explicit `prune`: rebuild runs after EVERY dispatch, and a
|
|
171
|
+
# request per entry per dispatch would be the most expensive thing in a run.
|
|
172
|
+
# An unreachable or undecided answer keeps the entry -- dropping a fact on a
|
|
173
|
+
# failed request would erode memory over time with nothing recording why.
|
|
174
|
+
if [ "${FORGE_MEMORY_DEEP_PRUNE:-0}" = 1 ] && [ -n "$anchor" ] \
|
|
175
|
+
&& [ "$jev_loaded" = 1 ] && forge_jev_active memory; then
|
|
176
|
+
if python3 "$SKILL_DIR/scripts/forge-jev.py" memory-stale \
|
|
177
|
+
--text "$text" --anchor "$repo/$anchor" \
|
|
178
|
+
${JEV_RUN_DIR:+--run-dir "$JEV_RUN_DIR"} >/dev/null 2>&1; then
|
|
179
|
+
[ -z "$MEMORY_DROPS" ] || printf 'jev_stale\t%s\t%s\n' "$cat" "$text" >> "$MEMORY_DROPS"
|
|
180
|
+
stale=$((stale+1)); continue
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
if [ "$sec" = finding ]; then
|
|
184
|
+
printf '%s\t- %sx: %s\n' "$count" "$count" "$text" >> "$body"
|
|
185
|
+
else
|
|
186
|
+
printf '%s\t- %s\n' "$count" "$text" >> "$body"
|
|
187
|
+
fi
|
|
188
|
+
done < <(sort -t"$TAB" -k2,2nr -k3,3r "$cand")
|
|
189
|
+
|
|
190
|
+
if [ -s "$body" ]; then
|
|
191
|
+
{ section_title "$sec"; cut -f2- "$body"; echo; } >> "$tmp"
|
|
192
|
+
kept=$((kept + $(wc -l < "$body" | tr -d ' ')))
|
|
193
|
+
fi
|
|
194
|
+
rm -f "$body"
|
|
195
|
+
done
|
|
196
|
+
rm -f "$cand"
|
|
197
|
+
|
|
198
|
+
if [ ! -s "$tmp" ]; then rm -f "$tmp" "$mem"; printf '0\t%s\t0\n' "$stale"; return 0; fi
|
|
199
|
+
|
|
200
|
+
local header="$mem.hdr.$$"
|
|
201
|
+
{
|
|
202
|
+
echo "# forge project memory"
|
|
203
|
+
echo "<!-- Maintained by forge from what its own runs observed. Do not edit this file"
|
|
204
|
+
echo " inside a dwarf run: it is excluded from every diff and your edit would be"
|
|
205
|
+
echo " overwritten. Facts only, one per line. -->"
|
|
206
|
+
echo
|
|
207
|
+
cat "$tmp"
|
|
208
|
+
} > "$header"
|
|
209
|
+
|
|
210
|
+
# Cap enforcement. Everything above already dropped stale entries and unpromoted
|
|
211
|
+
# findings, so what is left is real; trimming happens from the bottom, which the
|
|
212
|
+
# sort above has arranged to be the lowest-count, least-recent facts.
|
|
213
|
+
while [ "$(wc -l < "$header" | tr -d ' ')" -gt "$MAX_LINES" ] || \
|
|
214
|
+
[ "$(wc -c < "$header" | tr -d ' ')" -gt "$MAX_BYTES" ]; do
|
|
215
|
+
local last_fact
|
|
216
|
+
last_fact="$(grep -n '^- ' "$header" | tail -1 | cut -d: -f1)"
|
|
217
|
+
[ -n "$last_fact" ] || break
|
|
218
|
+
[ -z "$MEMORY_DROPS" ] || printf 'cap\t-\t%s\n' "$(sed -n "${last_fact}p" "$header")" >> "$MEMORY_DROPS"
|
|
219
|
+
sed "${last_fact}d" "$header" > "$header.x" && mv "$header.x" "$header"
|
|
220
|
+
capped=$((capped+1)); kept=$((kept-1))
|
|
221
|
+
done
|
|
222
|
+
# The cap can eat a section's last fact, leaving a heading over nothing.
|
|
223
|
+
drop_empty_sections "$header"
|
|
224
|
+
|
|
225
|
+
mkdir -p "$(dir_for "$repo")"
|
|
226
|
+
mv "$header" "$mem"
|
|
227
|
+
rm -f "$tmp"
|
|
228
|
+
printf '%s\t%s\t%s\n' "$kept" "$stale" "$capped"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Remove a "## Section" line that has no "- " fact under it, which the cap can
|
|
232
|
+
# produce by trimming a section's last entry.
|
|
233
|
+
drop_empty_sections() {
|
|
234
|
+
local f="$1" out="$1.de.$$"
|
|
235
|
+
awk '
|
|
236
|
+
{ l[NR] = $0 }
|
|
237
|
+
END {
|
|
238
|
+
for (i = 1; i <= NR; i++) {
|
|
239
|
+
if (l[i] ~ /^## /) {
|
|
240
|
+
keep = 0
|
|
241
|
+
for (j = i + 1; j <= NR && l[j] !~ /^## /; j++)
|
|
242
|
+
if (l[j] ~ /^- /) { keep = 1; break }
|
|
243
|
+
if (!keep) continue
|
|
244
|
+
}
|
|
245
|
+
print l[i]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
' "$f" > "$out"
|
|
249
|
+
# Collapse any run of blank lines to a single one.
|
|
250
|
+
awk '{ if ($0 == "") blank++; else blank = 0; if (blank < 2) print }' "$out" > "$f"
|
|
251
|
+
rm -f "$out"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# --- inject -------------------------------------------------------------------
|
|
255
|
+
# Slices differ by role because every injected line is paid for on every dispatch:
|
|
256
|
+
# QA is not building anything, so the build command is dead weight in its prompt.
|
|
257
|
+
do_inject() {
|
|
258
|
+
local repo="${1:?inject needs a repo}" role="${2:?inject needs a role}"
|
|
259
|
+
shift 2 2>/dev/null || true
|
|
260
|
+
local TASKFILE=""
|
|
261
|
+
while [ $# -gt 0 ]; do
|
|
262
|
+
case "$1" in
|
|
263
|
+
--task) TASKFILE="${2:-}"; shift 2 ;;
|
|
264
|
+
--run-dir) JEV_RUN_DIR="${2:-}"; shift 2 ;;
|
|
265
|
+
*) shift ;;
|
|
266
|
+
esac
|
|
267
|
+
done
|
|
268
|
+
memory_off && return 0
|
|
269
|
+
local mem; mem="$(memory_for "$repo")"
|
|
270
|
+
[ -s "$mem" ] || return 0
|
|
271
|
+
|
|
272
|
+
local want
|
|
273
|
+
case "$role" in
|
|
274
|
+
planner|dwarf) want="Verify|Known traps|Recurring QA findings" ;;
|
|
275
|
+
qa) want="Known traps|Recurring QA findings" ;;
|
|
276
|
+
*) die "inject: unknown role '$role' (planner|dwarf|qa)" ;;
|
|
277
|
+
esac
|
|
278
|
+
|
|
279
|
+
local body; body="$(awk -v want="$want" '
|
|
280
|
+
/^## / { keep = 0; t=$0; sub(/^## /,"",t)
|
|
281
|
+
n=split(want,w,"|"); for (i=1;i<=n;i++) if (w[i]==t) keep=1
|
|
282
|
+
if (keep) { print ""; print $0 } ; next }
|
|
283
|
+
/^# |^<!--|^ / { next }
|
|
284
|
+
keep && NF { print }
|
|
285
|
+
' "$mem")"
|
|
286
|
+
[ -n "$(printf '%s' "$body" | tr -d '[:space:]')" ] || return 0
|
|
287
|
+
|
|
288
|
+
# Narrow to the facts that bear on THIS task. The role slice and the 40-line/4KB cap
|
|
289
|
+
# above remain the outer bound; this only removes lines, never adds or reorders them,
|
|
290
|
+
# and any failure leaves `body` exactly as it was. Every line here is paid for on
|
|
291
|
+
# every dispatch of every run, which is what makes narrowing worth a request -- and
|
|
292
|
+
# also why the fallback has to be the full slice rather than an empty one.
|
|
293
|
+
if [ -n "$TASKFILE" ] && [ -r "$TASKFILE" ]; then
|
|
294
|
+
local jev_loaded=0
|
|
295
|
+
if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
|
|
296
|
+
source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
|
|
297
|
+
fi
|
|
298
|
+
if [ "$jev_loaded" = 1 ] && forge_jev_active memory; then
|
|
299
|
+
local facts sliced
|
|
300
|
+
facts="$(printf '%s\n' "$body" | grep '^- ' || true)"
|
|
301
|
+
if [ -n "$facts" ]; then
|
|
302
|
+
local ftmp="${TMPDIR:-/tmp}/forge-mem-$$.txt"
|
|
303
|
+
printf '%s\n' "$facts" > "$ftmp"
|
|
304
|
+
local ktmp="${TMPDIR:-/tmp}/forge-mem-keep-$$.txt"
|
|
305
|
+
if python3 "$SKILL_DIR/scripts/forge-jev.py" memory-slice \
|
|
306
|
+
--task "$TASKFILE" --lines "$ftmp" \
|
|
307
|
+
${JEV_RUN_DIR:+--run-dir "$JEV_RUN_DIR"} > "$ktmp" 2>/dev/null && [ -s "$ktmp" ]; then
|
|
308
|
+
# Two files, not -v: awk's -v processes escapes and cannot carry a literal
|
|
309
|
+
# newline, so passing the kept lines that way silently truncated the set at the
|
|
310
|
+
# first one. Read them as a first file instead.
|
|
311
|
+
sliced="$(printf '%s\n' "$body" | awk '
|
|
312
|
+
NR == FNR { want[$0] = 1; next }
|
|
313
|
+
/^## / { header = $0; printed = 0; next }
|
|
314
|
+
/^- / { if ($0 in want) { if (!printed) { print ""; print header; printed = 1 } print $0 } next }
|
|
315
|
+
{ print }
|
|
316
|
+
' "$ktmp" -)"
|
|
317
|
+
# Only adopt a narrowed body that actually kept something. An awk that fails,
|
|
318
|
+
# or a slice that matched nothing, must leave the full slice in place -- an
|
|
319
|
+
# empty memory section is a worse outcome than an unnarrowed one.
|
|
320
|
+
if [ -n "$(printf '%s' "$sliced" | grep -c '^- ' | tr -d ' ')" ] \
|
|
321
|
+
&& printf '%s' "$sliced" | grep -q '^- '; then
|
|
322
|
+
body="$sliced"
|
|
323
|
+
fi
|
|
324
|
+
fi
|
|
325
|
+
rm -f "$ftmp" "$ktmp" 2>/dev/null || true
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
echo "## What forge learned in this repo before"
|
|
331
|
+
if [ "$role" = qa ]; then
|
|
332
|
+
echo "From earlier forge runs on this project. Use it to notice a repeat bug and to"
|
|
333
|
+
echo "avoid reporting a known-generated file as a defect — it is context, not a checklist."
|
|
334
|
+
else
|
|
335
|
+
echo "From earlier forge runs on this project. Treat it as known-good context so you do"
|
|
336
|
+
echo "not rediscover it; if something here is now wrong, say so in your final message."
|
|
337
|
+
fi
|
|
338
|
+
printf '%s\n' "$body"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# --- note ---------------------------------------------------------------------
|
|
342
|
+
# The instruction that makes automatic memory possible at all. It asks for the
|
|
343
|
+
# learning in the same breath as the work, from the model that just did it — so
|
|
344
|
+
# extraction costs no extra dispatch and no separate model has to guess what
|
|
345
|
+
# mattered. Deliberately asks for nothing when nothing generalizes: a role that
|
|
346
|
+
# invents a learning to be helpful is precisely how this fills up with noise.
|
|
347
|
+
do_note() {
|
|
348
|
+
local role="${1:?note needs a role}"
|
|
349
|
+
memory_off && return 0
|
|
350
|
+
echo "## Before you finish"
|
|
351
|
+
echo "If — and only if — you learned something about THIS repository that would still be"
|
|
352
|
+
if [ "$role" = qa ]; then
|
|
353
|
+
# The reviewer's verdict line must stay the very last line of the reply, so a learning
|
|
354
|
+
# goes just above it; asking for "the very end" produced replies the parser rejected.
|
|
355
|
+
echo "true and useful on an unrelated task next month, add a line for it just above your"
|
|
356
|
+
echo "final FORGE_VERDICT line (the verdict stays the last line of your reply):"
|
|
357
|
+
else
|
|
358
|
+
echo "true and useful on an unrelated task next month, add a line for it at the very end:"
|
|
359
|
+
fi
|
|
360
|
+
echo
|
|
361
|
+
echo " FORGE_LEARNING: verify | <the command that actually builds or tests this project>"
|
|
362
|
+
echo " FORGE_LEARNING: trap | <a landmine: a flaky test, a generated file, a sharp edge>"
|
|
363
|
+
if [ "$role" = qa ]; then
|
|
364
|
+
echo " FORGE_LEARNING: finding | <a mistake pattern likely to recur, not this one bug>"
|
|
365
|
+
fi
|
|
366
|
+
echo
|
|
367
|
+
echo "End the text with [path/that/anchors/it] where a path makes sense; forge drops an"
|
|
368
|
+
echo "entry whose path stops existing, which is how this file stays true as code moves."
|
|
369
|
+
echo "One short line each, phrased as a fact. Reuse the exact wording of anything already"
|
|
370
|
+
echo "listed above rather than rephrasing it, so repeats are recognised as repeats."
|
|
371
|
+
echo "Most runs teach nothing durable. Emitting no line is the normal, correct outcome."
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# --- record -------------------------------------------------------------------
|
|
375
|
+
do_record() {
|
|
376
|
+
local repo="${1:?record needs a repo}"; shift
|
|
377
|
+
local LAST="" ROLE="" RUN_ID="-" TASK="-" MODEL="-" VERDICT="-" DURATION="-"
|
|
378
|
+
while [ $# -gt 0 ]; do
|
|
379
|
+
case "$1" in
|
|
380
|
+
--last) LAST="${2:?--last needs a value}"; shift 2 ;;
|
|
381
|
+
--role) ROLE="${2:?--role needs a value}"; shift 2 ;;
|
|
382
|
+
--run-id) RUN_ID="${2:?--run-id needs a value}"; shift 2 ;;
|
|
383
|
+
--task) TASK="${2:?--task needs a value}"; shift 2 ;;
|
|
384
|
+
--run-dir) JEV_RUN_DIR="${2:?--run-dir needs a value}"; shift 2 ;;
|
|
385
|
+
--model) MODEL="${2:?--model needs a value}"; shift 2 ;;
|
|
386
|
+
--verdict) VERDICT="${2:?--verdict needs a value}"; shift 2 ;;
|
|
387
|
+
--duration) DURATION="${2:?--duration needs a value}"; shift 2 ;;
|
|
388
|
+
*) die "record: unknown option '$1'" ;;
|
|
389
|
+
esac
|
|
390
|
+
done
|
|
391
|
+
memory_off && return 0
|
|
392
|
+
[ -n "$ROLE" ] || die "record needs --role"
|
|
393
|
+
[ -d "$repo" ] || die "record: '$repo' is not a directory"
|
|
394
|
+
|
|
395
|
+
local led ts n=0
|
|
396
|
+
led="$(ledger_for "$repo")"
|
|
397
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
398
|
+
mkdir -p "$(dir_for "$repo")"
|
|
399
|
+
if [ ! -f "$led" ]; then
|
|
400
|
+
printf '# ts\trun_id\ttask\trole\tmodel\tverdict\tcategory\tkey\ttext\tduration_s\tinjectable\n' > "$led"
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
local jev_loaded=0 cur=""
|
|
404
|
+
if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
|
|
405
|
+
source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
|
|
406
|
+
fi
|
|
407
|
+
# Scratch files for the dataset event, outside the repo: .forge/ is the user's tree.
|
|
408
|
+
if memory_dataset_on; then
|
|
409
|
+
cur="$(mktemp "${TMPDIR:-/tmp}/forge-memory-cur.XXXXXX" 2>/dev/null)" || cur=""
|
|
410
|
+
MEMORY_DROPS="$(mktemp "${TMPDIR:-/tmp}/forge-memory-drops.XXXXXX" 2>/dev/null)" || MEMORY_DROPS=""
|
|
411
|
+
fi
|
|
412
|
+
if [ -n "$LAST" ] && [ -r "$LAST" ]; then
|
|
413
|
+
local line cat text key inject curated curated_rc jcat jinject jmatch jkey ecat
|
|
414
|
+
while IFS= read -r line; do
|
|
415
|
+
cat="$(printf '%s' "$line" | sed -n 's/^[[:space:]]*FORGE_LEARNING:[[:space:]]*\([a-zA-Z]\{1,\}\)[[:space:]]*|.*/\1/p' | tr '[:upper:]' '[:lower:]')"
|
|
416
|
+
text="$(printf '%s' "$line" | sed -n 's/^[[:space:]]*FORGE_LEARNING:[[:space:]]*[a-zA-Z]\{1,\}[[:space:]]*|[[:space:]]*\(.*\)/\1/p')"
|
|
417
|
+
case "$cat" in verify|trap|finding) ;; *) continue ;; esac
|
|
418
|
+
text="$(clamp_fact "$(sanitize "$text")")"
|
|
419
|
+
[ -n "$text" ] || continue
|
|
420
|
+
key="$(norm_key "$(strip_anchor "$text")")"
|
|
421
|
+
[ -n "$key" ] || continue
|
|
422
|
+
# Jev may refile the category, merge a paraphrase onto an existing key, and mark
|
|
423
|
+
# a one-off as not worth injecting. Every one of those is a suggestion: any
|
|
424
|
+
# failure, any missing key, any disabled capability leaves the three values below
|
|
425
|
+
# exactly as the model emitted them. The row is written either way -- the ledger
|
|
426
|
+
# is append-only and never injected, so this gates injection, never recording.
|
|
427
|
+
inject=1; ecat="$cat"; jcat=""; jinject=""; jmatch=""
|
|
428
|
+
if [ "$jev_loaded" = 1 ] && forge_jev_active memory; then
|
|
429
|
+
curated="$(python3 "$SKILL_DIR/scripts/forge-jev.py" memory-curate \
|
|
430
|
+
--repo "$repo" --category "$cat" --text "$text" \
|
|
431
|
+
${JEV_RUN_DIR:+--run-dir "$JEV_RUN_DIR"} 2>/dev/null)"
|
|
432
|
+
curated_rc=$?
|
|
433
|
+
if [ "$curated_rc" -eq 0 ] && [ -n "$curated" ]; then
|
|
434
|
+
jcat="$(printf '%s' "$curated" | cut -f1)"
|
|
435
|
+
jinject="$(printf '%s' "$curated" | cut -f2)"
|
|
436
|
+
jmatch="$(printf '%s' "$curated" | cut -f3)"
|
|
437
|
+
case "$jcat" in verify|trap|finding) cat="$jcat" ;; esac
|
|
438
|
+
[ "$jinject" = 0 ] && inject=0
|
|
439
|
+
if [ -n "$jmatch" ]; then
|
|
440
|
+
# Re-derive the key with THIS file's norm_key rather than having Python
|
|
441
|
+
# return one: two implementations of the same normalisation would drift,
|
|
442
|
+
# and a key that disagrees by one character silently stops deduplicating.
|
|
443
|
+
# The leading "Nx: " is rebuild's recurrence marker, not part of the fact.
|
|
444
|
+
jmatch="$(printf '%s' "$jmatch" | sed 's/^[0-9]\{1,\}x: //')"
|
|
445
|
+
jkey="$(norm_key "$(strip_anchor "$jmatch")")"
|
|
446
|
+
[ -n "$jkey" ] && key="$jkey"
|
|
447
|
+
fi
|
|
448
|
+
fi
|
|
449
|
+
fi
|
|
450
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
451
|
+
"$ts" "$(sanitize "$RUN_ID")" "$(sanitize "$TASK")" "$(sanitize "$ROLE")" \
|
|
452
|
+
"$(sanitize "$MODEL")" "$(sanitize "$VERDICT")" "$cat" "$key" "$text" \
|
|
453
|
+
"$(sanitize "$DURATION")" "$inject" >> "$led"
|
|
454
|
+
[ -z "$cur" ] || printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$ecat" "$cat" "$key" "$inject" \
|
|
455
|
+
"$jcat" "$jinject" "$jmatch" "$text" >> "$cur"
|
|
456
|
+
n=$((n+1))
|
|
457
|
+
done < <(grep -o 'FORGE_LEARNING:.*' "$LAST" 2>/dev/null)
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
# A dispatch that emitted no learning is still worth a ledger row: it is how the
|
|
461
|
+
# ledger can later answer "which model has worked on this repo, and how did it do".
|
|
462
|
+
if [ "$n" -eq 0 ]; then
|
|
463
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
464
|
+
"$ts" "$(sanitize "$RUN_ID")" "$(sanitize "$TASK")" "$(sanitize "$ROLE")" \
|
|
465
|
+
"$(sanitize "$MODEL")" "$(sanitize "$VERDICT")" "-" "-" "-" \
|
|
466
|
+
"$(sanitize "$DURATION")" >> "$led"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
local out kept stale capped
|
|
470
|
+
out="$(rebuild "$repo")"
|
|
471
|
+
kept="$(printf '%s' "$out" | cut -f1)"; stale="$(printf '%s' "$out" | cut -f2)"
|
|
472
|
+
capped="$(printf '%s' "$out" | cut -f3)"
|
|
473
|
+
memory_dataset_event memory_record "$repo" "$JEV_RUN_DIR" --set role="$ROLE" --set run_id="$RUN_ID" \
|
|
474
|
+
--set task="$TASK" --set model="$MODEL" --set verdict="$VERDICT" --set recorded="$n" \
|
|
475
|
+
--set kept="${kept:-0}" --set stale="${stale:-0}" --set capped="${capped:-0}" \
|
|
476
|
+
--set max_lines="$MAX_LINES" --set max_bytes="$MAX_BYTES" \
|
|
477
|
+
${cur:+--file curation="$cur"} ${MEMORY_DROPS:+--file drops="$MEMORY_DROPS"} --file memory="$(memory_for "$repo")"
|
|
478
|
+
rm -f ${cur:+"$cur"} ${MEMORY_DROPS:+"$MEMORY_DROPS"}
|
|
479
|
+
printf 'memory: %s learning(s) recorded, %s fact(s) live' "$n" "${kept:-0}"
|
|
480
|
+
[ "${stale:-0}" -gt 0 ] 2>/dev/null && printf ', %s stale dropped' "$stale"
|
|
481
|
+
[ "${capped:-0}" -gt 0 ] 2>/dev/null && printf ', %s trimmed at cap' "$capped"
|
|
482
|
+
printf '\n'
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# --- prune --------------------------------------------------------------------
|
|
486
|
+
do_prune() { # do_prune <repo>
|
|
487
|
+
local repo="$1" out
|
|
488
|
+
if memory_dataset_on; then
|
|
489
|
+
MEMORY_DROPS="$(mktemp "${TMPDIR:-/tmp}/forge-memory-drops.XXXXXX" 2>/dev/null)" || MEMORY_DROPS=""
|
|
490
|
+
fi
|
|
491
|
+
out="$(FORGE_MEMORY_DEEP_PRUNE=1 rebuild "$repo")"
|
|
492
|
+
memory_dataset_event memory_prune "$repo" "" --set kept="$(printf '%s' "$out" | cut -f1)" \
|
|
493
|
+
--set stale="$(printf '%s' "$out" | cut -f2)" --set capped="$(printf '%s' "$out" | cut -f3)" \
|
|
494
|
+
${MEMORY_DROPS:+--file drops="$MEMORY_DROPS"} --file memory="$(memory_for "$repo")"
|
|
495
|
+
rm -f ${MEMORY_DROPS:+"$MEMORY_DROPS"}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# --- spend --------------------------------------------------------------------
|
|
499
|
+
# The ledger already carries one row per dispatch, and it is never injected into a
|
|
500
|
+
# prompt, so accounting from it costs nothing at all. Time and dispatch counts
|
|
501
|
+
# only: token accounting is exposed differently (or not at all) by the five CLIs,
|
|
502
|
+
# and a number forge cannot actually measure would be worse than no number.
|
|
503
|
+
do_spend() {
|
|
504
|
+
local repo="${1:?spend needs a repo}"; shift
|
|
505
|
+
local ONLY_RUN=""
|
|
506
|
+
while [ $# -gt 0 ]; do
|
|
507
|
+
case "$1" in
|
|
508
|
+
--run) ONLY_RUN="${2:?--run needs a value}"; shift 2 ;;
|
|
509
|
+
*) die "spend: unknown option '$1'" ;;
|
|
510
|
+
esac
|
|
511
|
+
done
|
|
512
|
+
local led; led="$(ledger_for "$repo")"
|
|
513
|
+
[ -s "$led" ] || { echo "no ledger in $repo/.forge — nothing dispatched here yet"; return 0; }
|
|
514
|
+
|
|
515
|
+
awk -F'\t' -v only="$ONLY_RUN" '
|
|
516
|
+
function hms(x, h,m,s) {
|
|
517
|
+
if (x == "") return " -"
|
|
518
|
+
h=int(x/3600); m=int((x%3600)/60); s=x%60
|
|
519
|
+
if (h>0) return sprintf("%dh%02dm", h, m)
|
|
520
|
+
if (m>0) return sprintf("%dm%02ds", m, s)
|
|
521
|
+
return sprintf("%ds", s)
|
|
522
|
+
}
|
|
523
|
+
/^#/ || NF < 9 { next }
|
|
524
|
+
{
|
|
525
|
+
ts=$1; run=$2; task=$3; role=$4; model=$5
|
|
526
|
+
dur = (NF >= 10 ? $10 : "-")
|
|
527
|
+
if (only != "" && run != only) next
|
|
528
|
+
# One dispatch emitting two learnings writes two rows. Every row from one
|
|
529
|
+
# dispatch shares its timestamp, so this counts dispatches, not lines.
|
|
530
|
+
k = ts "\034" run "\034" task "\034" role
|
|
531
|
+
if (!(k in seen)) {
|
|
532
|
+
seen[k] = 1; n[model]++; rn[role]++; total++; runs[run] = 1
|
|
533
|
+
}
|
|
534
|
+
# Count the dispatch once, but take its duration from whichever of its rows
|
|
535
|
+
# carries one — a dispatch that straddles the schema change has both kinds.
|
|
536
|
+
if (dur ~ /^[0-9]+$/ && !(k in durof)) {
|
|
537
|
+
durof[k] = dur
|
|
538
|
+
t[model] += dur; timed[model]++
|
|
539
|
+
rt[role] += dur
|
|
540
|
+
tt += dur; ttn++
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
END {
|
|
544
|
+
if (total == 0) { print "no dispatches recorded" ; exit }
|
|
545
|
+
nruns = 0; for (r in runs) nruns++
|
|
546
|
+
printf "%d dispatch(es) across %d run(s), %s of model time\n", total, nruns, hms(tt)
|
|
547
|
+
if (ttn < total)
|
|
548
|
+
printf "(%d dispatch(es) predate duration recording and are counted but not timed)\n", total - ttn
|
|
549
|
+
print "Durations sum concurrent work, so they exceed the wall-clock of a parallel run."
|
|
550
|
+
print ""
|
|
551
|
+
printf " %-26s %6s %9s %9s\n", "by model", "disp", "total", "mean"
|
|
552
|
+
for (m in n)
|
|
553
|
+
printf " %-26s %6d %9s %9s\n", m, n[m], hms(t[m]), \
|
|
554
|
+
(timed[m] > 0 ? hms(int(t[m]/timed[m])) : " -")
|
|
555
|
+
print ""
|
|
556
|
+
printf " %-26s %6s %9s\n", "by role", "disp", "total"
|
|
557
|
+
for (r in rn)
|
|
558
|
+
printf " %-26s %6d %9s\n", r, rn[r], hms(rt[r])
|
|
559
|
+
}
|
|
560
|
+
' "$led"
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# --- main ---------------------------------------------------------------------
|
|
564
|
+
[ $# -ge 1 ] || die "usage: forge-memory.sh <inject|note|record|show|prune|spend> [repo] [...]"
|
|
565
|
+
CMD="$1"; shift
|
|
566
|
+
case "$CMD" in
|
|
567
|
+
inject) do_inject "$@" ;;
|
|
568
|
+
note) do_note "$@" ;;
|
|
569
|
+
record) do_record "$@" ;;
|
|
570
|
+
show) mem="$(memory_for "${1:?show needs a repo}")"; [ -s "$mem" ] && cat "$mem" ;;
|
|
571
|
+
spend) do_spend "$@" ;;
|
|
572
|
+
prune) do_prune "${1:?prune needs a repo}" >/dev/null
|
|
573
|
+
do_inject "${1}" dwarf ;;
|
|
574
|
+
*) die "unknown subcommand '$CMD'" ;;
|
|
575
|
+
esac
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Each runner owns its metric state in a process/subshell, including failure exits.
|
|
3
|
+
forge_metric_begin() {
|
|
4
|
+
FORGE_METRIC_ROOT="$1"; FORGE_METRIC_ROLE="$2"
|
|
5
|
+
FORGE_METRIC="$(python3 "$SKILL_DIR/scripts/forge-runtime.py" begin "$1" "$2")" || return 3
|
|
6
|
+
# Dispatches started from here name this execution as their parent in the run dataset.
|
|
7
|
+
export FORGE_DATASET_PARENT="${FORGE_METRIC##*/}"
|
|
8
|
+
# The hook, when the caller defines it, records status before finish and Fractal's _outcome.
|
|
9
|
+
trap 'metric_rc=$?; ! declare -F forge_metric_on_exit >/dev/null || forge_metric_on_exit "$metric_rc"; python3 "$SKILL_DIR/scripts/forge-runtime.py" finish "$FORGE_METRIC" "$FORGE_METRIC_ROOT" "$FORGE_METRIC_ROLE" pipeline "$metric_rc"; if [ -n "${FORGE_FRACTAL_RUN:-}" ]; then python3 "$SKILL_DIR/scripts/forge-fractal.py" _outcome "$FORGE_METRIC_ROOT" "$metric_rc"; fi; exit "$metric_rc"' EXIT
|
|
10
|
+
}
|
|
11
|
+
forge_metric_phase() {
|
|
12
|
+
python3 "$SKILL_DIR/scripts/forge-runtime.py" phase "$FORGE_METRIC" "$1"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# --- run dataset (scripts/forge_dataset) --------------------------------------------
|
|
16
|
+
# Facts that are gone before any EXIT trap could read them: an aborted merge, a combine
|
|
17
|
+
# that rolled back, a branch that moved. Fail-open and silent, like every capture.
|
|
18
|
+
forge_dataset_off() {
|
|
19
|
+
case "${FORGE_DATASET:-on}" in off|OFF|Off|0|no|false) return 0 ;; esac
|
|
20
|
+
return 1
|
|
21
|
+
}
|
|
22
|
+
forge_dataset_event() { # forge_dataset_event <name> --dir <dir> [--task id] [--set k=v]... [--file label=path]...
|
|
23
|
+
forge_dataset_off && return 0
|
|
24
|
+
python3 "$SKILL_DIR/scripts/forge-dataset.py" event "$@" >/dev/null 2>&1 || true
|
|
25
|
+
}
|
|
26
|
+
forge_dataset_invocation() { # forge_dataset_invocation <command> <rc> <started> [argv...]
|
|
27
|
+
forge_dataset_off && return 0
|
|
28
|
+
local command="$1" rc="$2" started="$3"; shift 3
|
|
29
|
+
# Options before the command name: everything after it is the subcommand's own argv.
|
|
30
|
+
python3 "$SKILL_DIR/scripts/forge-dataset.py" invocation --dir "${1:-}" --rc "$rc" \
|
|
31
|
+
--started "$started" --pid "$$" "$command" -- "$@" >/dev/null 2>&1 || true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# --- update notice (scripts/forge-version.py) ----------------------------------------
|
|
35
|
+
# One line on stderr when a newer forge release exists, checked at most once a day. Silent
|
|
36
|
+
# offline, in CI and with FORGE_UPDATE_CHECK=off; it never changes a run's exit code.
|
|
37
|
+
forge_update_notice() {
|
|
38
|
+
python3 "$SKILL_DIR/scripts/forge-version.py" notice --source "$SKILL_DIR" </dev/null >&2 2>/dev/null || true
|
|
39
|
+
}
|