@giamme/forge 0.0.0-stage → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -0
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ # forge-install.sh — make /forge reachable from codex, claude, openclaude and opencode.
3
+ #
4
+ # This skill directory stays the single source of truth.
5
+ #
6
+ # claude / openclaude / codex symlink into their skills dir — edits are live
7
+ # opencode nothing to do; it scans ~/.claude/ for skills itself
8
+ # antigravity plugin install, which COPIES — see below
9
+ #
10
+ # Codex reads ~/.codex/skills, not ~/.codex/prompts: a prompts entry gives you a
11
+ # slash command, but not the skill, and it is not where any other skill here lives.
12
+ #
13
+ # forge-install.sh [--dry-run]
14
+ set -uo pipefail
15
+
16
+ SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ RESULT=0
18
+ DRY=0
19
+ # `forge install --help` must never install; nor may a mistyped --dry-run.
20
+ case "${1:-}" in
21
+ '') ;;
22
+ --dry-run) DRY=1 ;;
23
+ -h|--help) echo 'usage: forge install [--dry-run]'; exit 0 ;;
24
+ *) echo "forge: unknown install option: $1" >&2; exit 2 ;;
25
+ esac
26
+
27
+ say() { printf '%s\n' "$*"; }
28
+ do_() { if [ "$DRY" = 1 ]; then say " would: $*"; else "$@"; fi; }
29
+
30
+ link_dir() { # link_dir <target-parent> <label>
31
+ local parent="$1" label="$2" dest="$1/forge"
32
+ if [ ! -d "$parent" ]; then say "$label: skipped ($parent does not exist)"; return; fi
33
+ if [ "$dest" = "$SKILL_DIR" ]; then say "$label: is the source directory"; return; fi
34
+ if [ -L "$dest" ] && [ "$(readlink "$dest")" = "$SKILL_DIR" ]; then
35
+ say "$label: already linked"; return
36
+ fi
37
+ if [ -e "$dest" ] && [ ! -L "$dest" ]; then
38
+ # Never clobber a real directory the user may have edited in place.
39
+ say "$label: SKIPPED — $dest exists and is not a symlink; move it aside first"; return 1
40
+ fi
41
+ do_ ln -sfn "$SKILL_DIR" "$dest" && say "$label: linked -> $dest"
42
+ }
43
+
44
+ # opencode needs no install at all: it scans ~/.claude/ for skills by default
45
+ # (disabled only by OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1), so it picks this
46
+ # directory up directly. Verify with `opencode debug skill`.
47
+ report_opencode() {
48
+ if ! command -v opencode >/dev/null 2>&1; then
49
+ say "opencode : skipped (not on PATH)"; return
50
+ fi
51
+ say "opencode : auto-detected from ~/.claude/skills (no install needed)"
52
+ }
53
+
54
+ # Antigravity loads skills only as part of a plugin — a loose directory under
55
+ # ~/.gemini/skills is ignored (agy cannot see skills placed there at all). So forge
56
+ # is wrapped in a minimal plugin whose skills/ entry points back at this directory.
57
+ #
58
+ # Note that `agy plugin install` COPIES the files into ~/.gemini/config/plugins
59
+ # rather than referencing them. Antigravity is therefore the one harness that does
60
+ # not track edits to this directory automatically: re-run this installer after
61
+ # changing the skill, or agy keeps running the version it copied.
62
+ install_antigravity() {
63
+ local label="antigravity" wrapper="$HOME/.gemini/forge-plugin"
64
+ if ! command -v agy >/dev/null 2>&1; then
65
+ say "$label: skipped (agy not on PATH)"; return
66
+ fi
67
+ if [ "$DRY" = 1 ]; then
68
+ say " would: write plugin wrapper $wrapper"
69
+ say " would: agy plugin install $wrapper"
70
+ say "$label: plugin -> $wrapper"; return
71
+ fi
72
+ mkdir -p "$wrapper/skills" || return 1
73
+ cat > "$wrapper/plugin.json" <<EOF
74
+ {
75
+ "name": "forge",
76
+ "description": "Dispatch a coding task to a dwarf model to implement, then an independent qa model to review its actual diff, across codex/claude/openclaude/opencode/antigravity.",
77
+ "version": "1.0.0",
78
+ "author": { "name": "forge" }
79
+ }
80
+ EOF
81
+ [ "$?" = 0 ] || return 1
82
+ ln -sfn "$SKILL_DIR" "$wrapper/skills/forge" || return 1
83
+ if agy plugin install "$wrapper"; then
84
+ say "$label: plugin installed (copied — re-run this script after editing the skill)"
85
+ else
86
+ say "$label: FAILED — try: agy plugin install $wrapper"
87
+ return 1
88
+ fi
89
+ }
90
+
91
+ say "forge source: $SKILL_DIR"
92
+ [ "$DRY" = 1 ] && say "(dry run)"
93
+
94
+ link_dir "$HOME/.claude/skills" "claude " || RESULT=1
95
+ link_dir "$HOME/.openclaude/skills" "openclaude" || RESULT=1
96
+ link_dir "$HOME/.codex/skills" "codex " || RESULT=1
97
+ install_antigravity || RESULT=1
98
+ report_opencode
99
+
100
+ say
101
+ say "Verify with: bash $SKILL_DIR/scripts/forge-dispatch.sh doctor"
102
+
103
+ # Optional context engine; failure never prevents Forge setup.
104
+ if [ "$DRY" = 1 ]; then
105
+ /bin/bash "$SKILL_DIR/scripts/forge-install-ripwire.sh" --dry-run
106
+ else
107
+ /bin/bash "$SKILL_DIR/scripts/forge-install-ripwire.sh" || true
108
+ fi
109
+
110
+ # Optional runtime; installing never activates it for a run.
111
+ if [ "$DRY" = 1 ]; then
112
+ /bin/bash "$SKILL_DIR/scripts/forge-install-fractal.sh" --dry-run
113
+ elif [ -t 0 ]; then
114
+ printf 'Install optional Fractal execution support? [y/N] '
115
+ read -r fractal_answer
116
+ if [ "$fractal_answer" = y ] || [ "$fractal_answer" = Y ]; then
117
+ /bin/bash "$SKILL_DIR/scripts/forge-install-fractal.sh" --with-prerequisites || true
118
+ fi
119
+ fi
120
+ say "Fractal CLI: $SKILL_DIR/forge fractal --help (repository-local; existing forge commands are preserved)"
121
+
122
+ # Optional advisory judgments; sends task/diff/test/memory excerpts to an external
123
+ # API. Off unless configured and enabled. Failure never prevents Forge setup.
124
+ if [ "$DRY" = 1 ]; then
125
+ say " would: offer optional Jev setup (sends data to an external API; declined by default)"
126
+ elif [ -t 0 ]; then
127
+ printf 'Set up optional Jev advisory judgments? Sends task/diff excerpts to an external API. [y/N] '
128
+ read -r jev_answer
129
+ if [ "$jev_answer" = y ] || [ "$jev_answer" = Y ]; then
130
+ python3 "$SKILL_DIR/scripts/forge-jev.py" setup || true
131
+ fi
132
+ fi
133
+ say "Jev CLI: $SKILL_DIR/forge jev --help (optional; disabled until configured)"
134
+
135
+ exit "$RESULT"
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env bash
2
+ # Source in runners (forge-solo.sh / forge-parallel.sh, a later phase). Arrays and
3
+ # syntax remain compatible with macOS Bash 3.2: no associative arrays, no
4
+ # ${var,,}, and possibly-empty arrays are always expanded with the
5
+ # ${arr[@]+"${arr[@]}"} idiom.
6
+ #
7
+ # Precedence the Python side (scripts/forge_jev/) implements — keep these two in
8
+ # sync when either changes:
9
+ # 1. FORGE_JEV=off wins over everything: everything off, full stop.
10
+ # 2. Any FORGE_JEV_<CAP>=on acts as an allowlist — only those capabilities run
11
+ # (so `--jev-tests` alone works without `--jev`).
12
+ # 3. Otherwise FORGE_JEV=on (from --jev) or the stored config's `enabled` flag
13
+ # turns it on, and a capability explicitly off is subtracted.
14
+ # 4. Default: off.
15
+ #
16
+ # Phase 0 wires only the flags, env vars and freeze/restore files below. Nothing
17
+ # dispatches differently yet.
18
+
19
+ # What the environment said before any flag was parsed. Captured once, and exported so
20
+ # a second source of this file (there are several per run) sees the original answer
21
+ # rather than whatever forge_jev_export has since written into FORGE_JEV.
22
+ #
23
+ # This exists because wiring the flags up made a documented guarantee reachable for the
24
+ # first time: FORGE_JEV=off is a hard kill switch that wins over everything, and until
25
+ # forge_jev_export was actually called by a runner, it won by default -- nothing was
26
+ # there to override it. The first real run with --jev clobbered it.
27
+ if [ -z "${FORGE_JEV_ENV_CAPTURED:-}" ]; then
28
+ FORGE_JEV_ENV_CAPTURED=1
29
+ FORGE_JEV_ENV_KILL=0
30
+ [ "${FORGE_JEV:-}" = off ] && FORGE_JEV_ENV_KILL=1
31
+ FORGE_JEV_ENV_ROUTING="${FORGE_JEV_ROUTING:-}"
32
+ FORGE_JEV_ENV_TESTS="${FORGE_JEV_TESTS:-}"
33
+ FORGE_JEV_ENV_GATES="${FORGE_JEV_GATES:-}"
34
+ FORGE_JEV_ENV_MEMORY="${FORGE_JEV_MEMORY:-}"
35
+ export FORGE_JEV_ENV_CAPTURED FORGE_JEV_ENV_KILL FORGE_JEV_ENV_ROUTING \
36
+ FORGE_JEV_ENV_TESTS FORGE_JEV_ENV_GATES FORGE_JEV_ENV_MEMORY
37
+ fi
38
+
39
+ JEV_CHOICE="" # "" | on | off
40
+ JEV_ACT=0 # --jev-act
41
+ JEV_SHADOW=0 # --jev-shadow
42
+ JEV_CAPS=() # entries like "tests=on" / "routing=off"
43
+
44
+ forge_jev_help() {
45
+ cat <<'HELP'
46
+ Optional Jev (TypeSafe System One) advisory judgments (solo or parallel run):
47
+ --jev | --no-jev Explicit choice; unattended defaults off
48
+ --jev-act Kept for old commands: only marks judgments as acting
49
+ --jev-shadow Record judgments only; changes nothing
50
+ --jev-<cap> | --no-jev-<cap> Per-capability on/off: routing tests gates memory
51
+ FORGE_JEV=off is a hard kill switch and wins over every other setting.
52
+ Setup, status, doctor and enable/disable live under ./forge jev --help.
53
+ HELP
54
+ }
55
+
56
+ # forge_jev_flag "$@" — parse ONE option from the front of the argument list.
57
+ # Echoes nothing. Returns 0 and sets JEV_SHIFT to the number of argv items
58
+ # consumed (always 1 for our flags) when the option is ours; returns 1 and sets
59
+ # JEV_SHIFT=0 when the first argument is not one of ours; returns 2 on a usage
60
+ # error (message on stderr), also with JEV_SHIFT unset/irrelevant.
61
+ forge_jev_flag() {
62
+ local opt="$1"
63
+ JEV_SHIFT=0
64
+ case "$opt" in
65
+ --jev)
66
+ [ -z "$JEV_CHOICE" ] || [ "$JEV_CHOICE" = on ] || { echo '--jev and --no-jev are mutually exclusive' >&2; return 2; }
67
+ JEV_CHOICE=on; JEV_SHIFT=1; return 0
68
+ ;;
69
+ --no-jev)
70
+ [ -z "$JEV_CHOICE" ] || [ "$JEV_CHOICE" = off ] || { echo '--jev and --no-jev are mutually exclusive' >&2; return 2; }
71
+ JEV_CHOICE=off; JEV_SHIFT=1; return 0
72
+ ;;
73
+ --jev-act)
74
+ JEV_ACT=1; JEV_SHIFT=1; return 0
75
+ ;;
76
+ --jev-shadow)
77
+ JEV_SHADOW=1; JEV_SHIFT=1; return 0
78
+ ;;
79
+ --jev-routing|--jev-tests|--jev-gates|--jev-memory)
80
+ JEV_CAPS+=("${opt#--jev-}=on"); JEV_SHIFT=1; return 0
81
+ ;;
82
+ --no-jev-routing|--no-jev-tests|--no-jev-gates|--no-jev-memory)
83
+ JEV_CAPS+=("${opt#--no-jev-}=off"); JEV_SHIFT=1; return 0
84
+ ;;
85
+ *)
86
+ return 1
87
+ ;;
88
+ esac
89
+ }
90
+
91
+ # forge_jev_export — translate the parsed state into the environment the
92
+ # Python side reads. Call after all flags are parsed (and after forge_jev_restore,
93
+ # if resuming, since restore re-parses the frozen selection through this path).
94
+ forge_jev_export() {
95
+ if [ "${FORGE_JEV_ENV_KILL:-0}" = 1 ]; then
96
+ # Rule 1 of the precedence above: it wins over everything, and "everything" has to
97
+ # include this function. A flag must not be able to switch Jev back on.
98
+ export FORGE_JEV=off
99
+ elif [ "$JEV_CHOICE" = off ]; then
100
+ export FORGE_JEV=off
101
+ elif [ "$JEV_CHOICE" = on ]; then
102
+ export FORGE_JEV=on
103
+ else
104
+ unset FORGE_JEV 2>/dev/null || true
105
+ fi
106
+ # Restore what the environment said, rather than clearing it: a per-capability
107
+ # variable set by the caller is rule 2's allowlist, and wiping it here would have
108
+ # made `FORGE_JEV_TESTS=on forge ...` silently do nothing. A flag for the same
109
+ # capability still wins, because it is the more specific instruction.
110
+ local cap_env
111
+ for cap_env in ROUTING TESTS GATES MEMORY; do
112
+ eval "local inherited=\"\${FORGE_JEV_ENV_$cap_env:-}\""
113
+ if [ -n "$inherited" ]; then
114
+ eval "export FORGE_JEV_$cap_env=\"\$inherited\""
115
+ else
116
+ unset "FORGE_JEV_$cap_env" 2>/dev/null || true
117
+ fi
118
+ done
119
+ local entry cap val
120
+ for entry in ${JEV_CAPS[@]+"${JEV_CAPS[@]}"}; do
121
+ cap="${entry%%=*}"; val="${entry#*=}"
122
+ case "$cap" in
123
+ routing) export FORGE_JEV_ROUTING="$val" ;;
124
+ tests) export FORGE_JEV_TESTS="$val" ;;
125
+ gates) export FORGE_JEV_GATES="$val" ;;
126
+ memory) export FORGE_JEV_MEMORY="$val" ;;
127
+ esac
128
+ done
129
+ if [ "$JEV_ACT" = 1 ]; then export FORGE_JEV_ACT=on; else unset FORGE_JEV_ACT 2>/dev/null || true; fi
130
+ if [ "$JEV_SHADOW" = 1 ]; then export FORGE_JEV_SHADOW=on; else unset FORGE_JEV_SHADOW 2>/dev/null || true; fi
131
+ }
132
+
133
+ # forge_jev_record <run-dir> — freeze the resolved selection into
134
+ # <run-dir>/jev-selection.json. A resumed or retried run must make the same
135
+ # decisions as the original run, the same reason fractal-selection.json is
136
+ # frozen for Fractal: settings must not silently drift between the first
137
+ # attempt and a later resume/retry of the same run directory.
138
+ forge_jev_record() {
139
+ local run="$1" caps_json="{}" entry cap val first=1
140
+ for entry in ${JEV_CAPS[@]+"${JEV_CAPS[@]}"}; do
141
+ cap="${entry%%=*}"; val="${entry#*=}"
142
+ if [ "$first" = 1 ]; then caps_json="{\"$cap\": \"$val\""; first=0
143
+ else caps_json="$caps_json, \"$cap\": \"$val\""; fi
144
+ done
145
+ [ "$first" = 1 ] || caps_json="$caps_json}"
146
+ python3 -c '
147
+ import json, sys
148
+ run, choice, act, shadow, caps_json = sys.argv[1:6]
149
+ data = {
150
+ "choice": choice if choice else None,
151
+ "act": act == "1",
152
+ "shadow": shadow == "1",
153
+ "capabilities": json.loads(caps_json),
154
+ }
155
+ with open(run + "/jev-selection.json", "w") as fh:
156
+ json.dump(data, fh, indent=2, sort_keys=True)
157
+ fh.write("\n")
158
+ ' "$run" "$JEV_CHOICE" "$JEV_ACT" "$JEV_SHADOW" "$caps_json"
159
+ }
160
+
161
+ # forge_jev_restore <run-dir> — re-export a previously frozen selection, for
162
+ # resume/retry of an existing run directory. Reads jev-selection.json (written
163
+ # by forge_jev_record) back into JEV_CHOICE/JEV_ACT/JEV_SHADOW/JEV_CAPS and
164
+ # calls forge_jev_export so the environment matches the original run exactly.
165
+ forge_jev_restore() {
166
+ # Separate statements on purpose: bash expands every argument of a single `local`
167
+ # before assigning any of them, so `selection="$run/..."` on this line would see an
168
+ # empty run and silently resolve to /jev-selection.json.
169
+ local run="$1" line selection
170
+ selection="$run/jev-selection.json"
171
+ [ -f "$selection" ] || return 0
172
+ JEV_CAPS=()
173
+ local parsed
174
+ parsed="$(python3 -c '
175
+ import json, sys
176
+ with open(sys.argv[1]) as fh:
177
+ data = json.load(fh)
178
+ choice = data.get("choice") or ""
179
+ print(choice)
180
+ print("1" if data.get("act") else "0")
181
+ print("1" if data.get("shadow") else "0")
182
+ for cap, val in sorted((data.get("capabilities") or {}).items()):
183
+ print(cap + "=" + val)
184
+ ' "$selection")" || return $?
185
+ local i=0
186
+ while IFS= read -r line; do
187
+ case "$i" in
188
+ 0) JEV_CHOICE="$line" ;;
189
+ 1) JEV_ACT="$line" ;;
190
+ 2) JEV_SHADOW="$line" ;;
191
+ *) [ -z "$line" ] || JEV_CAPS+=("$line") ;;
192
+ esac
193
+ i=$((i + 1))
194
+ done <<EOF
195
+ $parsed
196
+ EOF
197
+ forge_jev_export
198
+ }
199
+
200
+ # forge_jev_settle <plan-dir> — what `run` and `retry` do about Jev.
201
+ #
202
+ # `plan` is the command that makes the choice and freezes it. A later run or retry of
203
+ # the same plan must make the same decisions, for the reason fractal-selection.json is
204
+ # frozen: a resumed run that quietly changed its mind about whether a judgment may act
205
+ # is a run nobody can reason about afterwards.
206
+ #
207
+ # So a frozen selection wins over a flag passed here, and saying so out loud beats
208
+ # ignoring the flag in silence. When nothing was frozen — a plan dir written before
209
+ # selections existed — the flags parsed on this command apply and are then frozen, so
210
+ # the next retry of it is reproducible too.
211
+ forge_jev_settle() {
212
+ local plan="$1"
213
+ if [ -f "$plan/jev-selection.json" ]; then
214
+ if [ -n "$JEV_CHOICE" ] || [ "$JEV_ACT" = 1 ] || [ "$JEV_SHADOW" = 1 ] \
215
+ || [ "${#JEV_CAPS[@]}" -gt 0 ]; then
216
+ printf 'forge: jev: using the selection frozen at plan time; ignoring the flags given here\n' >&2
217
+ fi
218
+ forge_jev_restore "$plan"
219
+ else
220
+ forge_jev_export
221
+ forge_jev_record "$plan"
222
+ fi
223
+ }
224
+
225
+ # forge_jev_active <capability> — succeeds (0) when Jev is enabled for that
226
+ # capability AND a key is configured; fails (non-zero) otherwise, including when
227
+ # the Python package is missing, the config is unreadable, or anything else goes
228
+ # wrong. Silent (no stdout/stderr) and never blocks: it only reads local config
229
+ # through `forge-jev.py status --json`, never the network.
230
+ #
231
+ # The status JSON is fetched at most once per shell (cached in
232
+ # FORGE_JEV_STATUS_JSON/FORGE_JEV_STATUS_FETCHED) so a caller checking several
233
+ # capabilities, or the same capability more than once in one verify_result call,
234
+ # pays for one subprocess instead of one per check.
235
+ FORGE_JEV_STATUS_JSON=""
236
+ FORGE_JEV_STATUS_FETCHED=0
237
+ forge_jev_active() {
238
+ local cap="$1"
239
+ if [ "$FORGE_JEV_STATUS_FETCHED" != 1 ]; then
240
+ FORGE_JEV_STATUS_FETCHED=1
241
+ FORGE_JEV_STATUS_JSON="$(python3 "$SKILL_DIR/scripts/forge-jev.py" status --json 2>/dev/null)" \
242
+ || FORGE_JEV_STATUS_JSON=""
243
+ fi
244
+ [ -n "$FORGE_JEV_STATUS_JSON" ] || return 1
245
+ printf '%s' "$FORGE_JEV_STATUS_JSON" | python3 -c '
246
+ import json, sys
247
+ cap = sys.argv[1]
248
+ try:
249
+ data = json.load(sys.stdin)
250
+ except ValueError:
251
+ sys.exit(1)
252
+ # capability_ready: some site of this capability has credentials for the provider it
253
+ # uses (a Cloudflare-only setup has no TypeSafe key). Older status output lacks it.
254
+ ready = data.get("capability_ready")
255
+ if isinstance(ready, dict):
256
+ sys.exit(0 if ready.get(cap) else 1)
257
+ sys.exit(0 if data.get("key_present") and data.get("capabilities", {}).get(cap) else 1)
258
+ ' "$cap" 2>/dev/null
259
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ """Repository-local entry point for Forge's optional Jev integration."""
3
+ from forge_jev.cli import main
4
+
5
+ if __name__ == '__main__':
6
+ raise SystemExit(main())