@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -2
  4. package/SKILL.md +123 -0
  5. package/VERSION +1 -0
  6. package/docs/README.md +26 -0
  7. package/docs/add-ons.md +33 -0
  8. package/docs/artifacts.md +69 -0
  9. package/docs/commands.md +315 -0
  10. package/docs/dataset.md +20 -0
  11. package/docs/decomposed-runs.md +268 -0
  12. package/docs/development.md +95 -0
  13. package/docs/forge-ui.md +92 -0
  14. package/docs/fractal.md +181 -0
  15. package/docs/install.md +160 -0
  16. package/docs/memory.md +156 -0
  17. package/docs/models.md +118 -0
  18. package/docs/reviews.md +231 -0
  19. package/docs/safety.md +55 -0
  20. package/docs/troubleshooting.md +54 -0
  21. package/forge +79 -0
  22. package/package.json +27 -4
  23. package/references/dataset.md +329 -0
  24. package/references/decompose.md +667 -0
  25. package/references/fractal-dashboard.md +21 -0
  26. package/references/fractal.md +307 -0
  27. package/references/harnesses.md +365 -0
  28. package/references/jev.md +865 -0
  29. package/references/memory.md +228 -0
  30. package/references/planning.md +42 -0
  31. package/references/ripwire.md +47 -0
  32. package/references/runtime.md +64 -0
  33. package/references/solo.md +113 -0
  34. package/references/ui.md +112 -0
  35. package/registry.tsv +81 -0
  36. package/scripts/forge-artifact.sh +78 -0
  37. package/scripts/forge-contract.py +430 -0
  38. package/scripts/forge-dataset.py +6 -0
  39. package/scripts/forge-dispatch.sh +671 -0
  40. package/scripts/forge-fail-first.py +195 -0
  41. package/scripts/forge-fractal-dispatch.sh +4 -0
  42. package/scripts/forge-fractal-options.sh +54 -0
  43. package/scripts/forge-fractal.py +6 -0
  44. package/scripts/forge-guard.py +266 -0
  45. package/scripts/forge-install-fractal.sh +4 -0
  46. package/scripts/forge-install-ripwire.sh +52 -0
  47. package/scripts/forge-install.sh +135 -0
  48. package/scripts/forge-jev-options.sh +259 -0
  49. package/scripts/forge-jev.py +6 -0
  50. package/scripts/forge-lib-plans.sh +785 -0
  51. package/scripts/forge-lib-review.sh +494 -0
  52. package/scripts/forge-lib-task.sh +302 -0
  53. package/scripts/forge-lib-verify.sh +30 -0
  54. package/scripts/forge-memory.sh +575 -0
  55. package/scripts/forge-metrics.sh +39 -0
  56. package/scripts/forge-parallel-options.sh +76 -0
  57. package/scripts/forge-parallel.sh +1717 -0
  58. package/scripts/forge-plan-check.py +118 -0
  59. package/scripts/forge-prompt.py +45 -0
  60. package/scripts/forge-ripwire.py +136 -0
  61. package/scripts/forge-route.py +344 -0
  62. package/scripts/forge-runs.py +121 -0
  63. package/scripts/forge-runtime.py +586 -0
  64. package/scripts/forge-schedule.py +312 -0
  65. package/scripts/forge-shadow.py +305 -0
  66. package/scripts/forge-solo.sh +347 -0
  67. package/scripts/forge-ui-options.sh +42 -0
  68. package/scripts/forge-ui.py +435 -0
  69. package/scripts/forge-update.sh +71 -0
  70. package/scripts/forge-verify-coverage.py +956 -0
  71. package/scripts/forge-version.py +240 -0
  72. package/scripts/forge_dataset/__init__.py +439 -0
  73. package/scripts/forge_dataset/capture.py +330 -0
  74. package/scripts/forge_dataset/checks.py +109 -0
  75. package/scripts/forge_dataset/cli.py +233 -0
  76. package/scripts/forge_dataset/export.py +1009 -0
  77. package/scripts/forge_dataset/harvest.py +716 -0
  78. package/scripts/forge_dataset/importer.py +412 -0
  79. package/scripts/forge_dataset/outcomes.py +275 -0
  80. package/scripts/forge_dataset/routing.py +278 -0
  81. package/scripts/forge_fractal/__init__.py +84 -0
  82. package/scripts/forge_fractal/bridge.py +46 -0
  83. package/scripts/forge_fractal/cli.py +202 -0
  84. package/scripts/forge_fractal/dashboard.css +138 -0
  85. package/scripts/forge_fractal/dashboard.html +41 -0
  86. package/scripts/forge_fractal/dashboard.js +687 -0
  87. package/scripts/forge_fractal/decomposition.py +262 -0
  88. package/scripts/forge_fractal/execution.py +659 -0
  89. package/scripts/forge_fractal/inspection.py +261 -0
  90. package/scripts/forge_fractal/install.py +158 -0
  91. package/scripts/forge_fractal/invocation.py +94 -0
  92. package/scripts/forge_fractal/pipeline.py +124 -0
  93. package/scripts/forge_fractal/selection.py +179 -0
  94. package/scripts/forge_jev/__init__.py +376 -0
  95. package/scripts/forge_jev/backtest.py +322 -0
  96. package/scripts/forge_jev/cli.py +980 -0
  97. package/scripts/forge_jev/client.py +418 -0
  98. package/scripts/forge_jev/corpus.py +156 -0
  99. package/scripts/forge_jev/coupling.py +244 -0
  100. package/scripts/forge_jev/features.py +24 -0
  101. package/scripts/forge_jev/gates.py +100 -0
  102. package/scripts/forge_jev/memory.py +163 -0
  103. package/scripts/forge_jev/outcome.py +49 -0
  104. package/scripts/forge_jev/predict.py +204 -0
  105. package/scripts/forge_jev/questions.py +661 -0
  106. package/scripts/forge_jev/review.py +73 -0
  107. package/scripts/forge_jev/routing.py +271 -0
  108. package/scripts/forge_jev/subset.py +123 -0
  109. package/scripts/forge_jev/verify.py +453 -0
  110. package/scripts/forge_registry.py +140 -0
  111. package/ui/README.md +347 -0
  112. package/ui/THIRD_PARTY_NOTICES.md +177 -0
  113. package/ui/docs/api.md +970 -0
  114. package/ui/forge_ui/__init__.py +20 -0
  115. package/ui/forge_ui/__main__.py +135 -0
  116. package/ui/forge_ui/diffs.py +454 -0
  117. package/ui/forge_ui/executions.py +436 -0
  118. package/ui/forge_ui/export.py +214 -0
  119. package/ui/forge_ui/findings.py +132 -0
  120. package/ui/forge_ui/fractal.py +582 -0
  121. package/ui/forge_ui/models.py +216 -0
  122. package/ui/forge_ui/reader.py +1424 -0
  123. package/ui/forge_ui/server.py +312 -0
  124. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  125. package/ui/forge_ui/static/app.js +1079 -0
  126. package/ui/forge_ui/static/cloud3d.js +647 -0
  127. package/ui/forge_ui/static/cloudmodel.js +375 -0
  128. package/ui/forge_ui/static/cloudui.js +397 -0
  129. package/ui/forge_ui/static/convo.js +295 -0
  130. package/ui/forge_ui/static/datatable.js +232 -0
  131. package/ui/forge_ui/static/diffview.js +235 -0
  132. package/ui/forge_ui/static/favicon.svg +1 -0
  133. package/ui/forge_ui/static/files.js +776 -0
  134. package/ui/forge_ui/static/findings3d.js +134 -0
  135. package/ui/forge_ui/static/gantt.js +97 -0
  136. package/ui/forge_ui/static/graph.js +266 -0
  137. package/ui/forge_ui/static/icons.js +94 -0
  138. package/ui/forge_ui/static/index.html +67 -0
  139. package/ui/forge_ui/static/iso.js +816 -0
  140. package/ui/forge_ui/static/leaderboard.js +311 -0
  141. package/ui/forge_ui/static/map3d.js +45 -0
  142. package/ui/forge_ui/static/modelstats.js +249 -0
  143. package/ui/forge_ui/static/notify.js +179 -0
  144. package/ui/forge_ui/static/panel.js +562 -0
  145. package/ui/forge_ui/static/runmodel.js +667 -0
  146. package/ui/forge_ui/static/scene3d.js +1471 -0
  147. package/ui/forge_ui/static/search.js +120 -0
  148. package/ui/forge_ui/static/shell.js +137 -0
  149. package/ui/forge_ui/static/stagectl.js +150 -0
  150. package/ui/forge_ui/static/style.css +1073 -0
  151. package/ui/forge_ui/static/theme.js +25 -0
  152. package/ui/forge_ui/transcripts.py +608 -0
@@ -0,0 +1,179 @@
1
+ """Per-run selection and frozen routing through the existing dispatcher."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import contextlib
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ from . import DEFAULTS, SCRIPTS, command, managed_run, read_json, state_root, write_json
11
+ from .install import doctor, install
12
+
13
+
14
+ def resolve(spec: str, role: str, yolo: bool = False, dry: bool = False) -> dict:
15
+ import tempfile
16
+ with tempfile.TemporaryDirectory(prefix='forge-fractal-resolve-') as tmp:
17
+ root = Path(tmp)
18
+ prompt = root / 'input'
19
+ prompt.write_text('Offline resolution')
20
+ argv = ['/bin/bash', SCRIPTS / 'forge-dispatch.sh']
21
+ if dry:
22
+ argv += [role, spec, '--run-dir', root, '--prompt-file', prompt, '--dry-run']
23
+ else:
24
+ argv += ['doctor', '--spec', spec, '--role', role]
25
+ if yolo:
26
+ argv += ['--yolo']
27
+ output = command(argv, timeout=60).decode()
28
+ # Dry resolution prints canonical routing without invoking a provider.
29
+ fields = dict(line.split('=', 1) for line in output.splitlines() if '=' in line)
30
+ canonical = ':'.join(fields.get(k, '') for k in ('model', 'effort', 'harness'))
31
+ return dict(spec=spec, role=role, resolution=output, canonical=canonical)
32
+
33
+
34
+ def pools(args) -> dict:
35
+ result = {}
36
+ inherited = Path(args.run_dir) / 'fractal-routing.json'
37
+ if inherited.exists():
38
+ result.update(read_json(inherited))
39
+ for tier in ('low', 'medium', 'high', 'any'):
40
+ value = getattr(args, 'dwarf_' + tier, '')
41
+ if value:
42
+ result[tier] = [s.strip() for s in value.split(',') if s.strip()]
43
+ return result
44
+
45
+
46
+ def ensure_runtime(allow_ordinary: bool = True) -> bool:
47
+ status = doctor()
48
+ if status['ready']:
49
+ return True
50
+ print(json.dumps(status, indent=2), file=sys.stderr)
51
+ if sys.stdin.isatty():
52
+ from argparse import Namespace
53
+ print('Install Fractal and missing prerequisites? [y/N] ', end='', file=sys.stderr, flush=True)
54
+ if input().lower() == 'y':
55
+ with contextlib.redirect_stdout(sys.stderr):
56
+ install(Namespace(yes=True, with_prerequisites=True, dry_run=False))
57
+ elif allow_ordinary:
58
+ print('Use ordinary execution instead? [y/N] ', end='', file=sys.stderr, flush=True)
59
+ if input().lower() == 'y':
60
+ return False
61
+ if not doctor()['ready']:
62
+ raise ValueError('Fractal selected but unavailable. Run forge fractal install --yes --with-prerequisites, then retry')
63
+ return True
64
+
65
+
66
+ def decomposition_settings(args, roots: set[str], previous: dict | None = None) -> dict:
67
+ """Freeze planner-role resolutions independently of the dwarf routing pools."""
68
+ enabled, explicit = args.fractal_auto_decompose, args.fractal_planner
69
+ if previous is not None:
70
+ saved = previous.get('decomposition', {})
71
+ if enabled and not saved.get('enabled'):
72
+ raise ValueError('Automatic decomposition is frozen; create a new run directory')
73
+ if explicit and (not saved.get('enabled') or explicit != saved.get('planner')):
74
+ raise ValueError('Fractal planner is frozen; create a new run directory')
75
+ return saved
76
+ if explicit and not enabled:
77
+ raise ValueError('--fractal-planner requires --fractal-auto-decompose')
78
+ if not enabled:
79
+ return {}
80
+ inherited = Path(args.run_dir) / 'planner'
81
+ planner = explicit or (inherited.read_text().strip() if inherited.is_file() else '')
82
+ specs = {planner} if planner else roots
83
+ if not specs:
84
+ raise ValueError('Automatic decomposition requires a task root or explicit planner model')
85
+ return dict(enabled=True, version=1, planner=planner,
86
+ resolved=[resolve(s, 'planner', dry=args.dry_run) for s in sorted(specs)])
87
+
88
+
89
+ def decomposition_choice(args) -> str:
90
+ if not args.fractal_auto_decompose:
91
+ return args.choice
92
+ if args.choice == 'off':
93
+ raise ValueError('--fractal-auto-decompose and --no-fractal are mutually exclusive')
94
+ return 'on'
95
+
96
+
97
+ def select(args) -> str:
98
+ if args.max_cost is not None:
99
+ raise ValueError('Unsupported capability: dollar caps cannot be enforced by the Forge bridge')
100
+ source = Path(args.run_dir).resolve()
101
+ remembered = source / 'fractal-selection.json'
102
+ previous = read_json(remembered) if remembered.exists() else None
103
+ choice = decomposition_choice(args)
104
+ if previous:
105
+ if choice and (choice == 'on') != previous['enabled']:
106
+ raise ValueError('This run already selected a backend; create a new run directory to change it')
107
+ choice = 'on' if previous['enabled'] else 'off'
108
+ if not choice:
109
+ choice = 'off'
110
+ if sys.stdin.isatty() and not args.dry_run:
111
+ print('Use Fractal for this run? [y/N] ', end='', file=sys.stderr, flush=True)
112
+ choice = 'on' if input().lower() == 'y' else 'off'
113
+ if choice == 'off':
114
+ decomposition_settings(args, set())
115
+ if not args.dry_run:
116
+ write_json(remembered, dict(enabled=False))
117
+ return 'off'
118
+ limits = {key: getattr(args, key, None) if getattr(args, key, None) is not None else value
119
+ for key, value in DEFAULTS.items()}
120
+ if any(type(v) is not int or v < (0 if k == 'depth' else 1) for k, v in limits.items()):
121
+ raise ValueError('Fractal limits must be positive integers (depth may be zero)')
122
+ routing = pools(args)
123
+ if args.mode == 'solo' and args.dwarf:
124
+ routing.setdefault('any', [args.dwarf])
125
+ eligible = set(s for pool in routing.values() for s in pool)
126
+ roots = {args.dwarf} if args.dwarf else set()
127
+ if args.dwarf:
128
+ eligible.add(args.dwarf)
129
+ tasks = source / 'tasks.tsv'
130
+ qa_specs = {args.qa} if args.qa else set()
131
+ if tasks.exists():
132
+ for line in tasks.read_text().splitlines():
133
+ if not line or line.startswith('#'):
134
+ continue
135
+ fields = line.split('\t')
136
+ eligible.add(fields[4])
137
+ roots.add(fields[4])
138
+ qa_specs.add(fields[5])
139
+ run_id = 'run-' + hashlib.sha256(str(source).encode()).hexdigest()[:20]
140
+ if previous:
141
+ run = managed_run(previous['run'])
142
+ config = read_json(run / 'run.json')
143
+ decomposition_settings(args, roots, config)
144
+ if config['repository'] != str(Path(args.repo).resolve()):
145
+ raise ValueError('Run repository changed')
146
+ if args.dwarf and args.dwarf not in config['eligible']:
147
+ raise ValueError('Model is outside this run\'s frozen pool')
148
+ if args.qa and args.qa not in config['qa_specs']:
149
+ raise ValueError('QA model changed for an existing run')
150
+ for key in DEFAULTS:
151
+ if getattr(args, key, None) is not None and limits[key] != config['limits'][key]:
152
+ raise ValueError('Limits are frozen for this run; use a new run directory to change ' + key)
153
+ for tier, pool in routing.items():
154
+ if pool != config['routing'].get(tier):
155
+ raise ValueError('Routing pools are frozen for this run')
156
+ if args.dry_run:
157
+ print(json.dumps(config, indent=2), file=sys.stderr)
158
+ return 'dry-fractal'
159
+ ensure_runtime(allow_ordinary=False)
160
+ return str(run)
161
+ config = dict(id=run_id, backend='fractal', mode=args.mode, repository=str(Path(args.repo).resolve()),
162
+ runner=str(source), routing=routing, limits=limits, yolo_dwarf=args.yolo_dwarf,
163
+ eligible=sorted(eligible), qa_specs=sorted(qa_specs), version='1.2.0',
164
+ acceptance='pending', workspace=str(state_root() / 'runs' / run_id / 'tasks'),
165
+ resolved=[resolve(s, 'dwarf', args.yolo_dwarf, dry=args.dry_run) for s in sorted(eligible)])
166
+ config['decomposition'] = decomposition_settings(args, roots)
167
+ for spec in sorted(qa_specs):
168
+ resolve(spec, 'qa', args.yolo_qa, dry=args.dry_run)
169
+ if args.dry_run:
170
+ print(json.dumps(config, indent=2), file=sys.stderr)
171
+ return 'dry-fractal'
172
+ if not ensure_runtime(allow_ordinary=not args.fractal_auto_decompose):
173
+ write_json(remembered, dict(enabled=False))
174
+ return 'off'
175
+ run = state_root() / 'runs' / run_id
176
+ run.mkdir(parents=True, exist_ok=True, mode=0o700)
177
+ write_json(run / 'run.json', config)
178
+ write_json(remembered, dict(enabled=True, run=run_id))
179
+ return str(run)
@@ -0,0 +1,376 @@
1
+ """Opt-in TypeSafe/Jev adapter. Importing this package never makes a network call."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.util
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import uuid
9
+
10
+ SCRIPTS = Path(__file__).resolve().parents[1]
11
+
12
+ ENDPOINT = 'https://api.typesafe.ai/v1/systemone'
13
+ MODEL = 'jev-latest'
14
+ CAPABILITIES = ('routing', 'tests', 'gates', 'memory')
15
+ DEFAULT_THRESHOLDS = dict(tests_act=0.70, flake_act=0.90,
16
+ gate_warn=0.60,
17
+ # Measured twice. The first 15 prompts were hand-written and
18
+ # scored with only a task TITLE in state, because prompt.md did
19
+ # not reach the model then -- so 0.30 was fitted to data
20
+ # production never produced.
21
+ #
22
+ # Re-measured with the real state, 3 runs over 8 prompts: ones
23
+ # a reviewer could not judge correctness from ("make the error
24
+ # messages better", "harden the server -- fix what you find")
25
+ # scored 0.06-0.27, and ones stating checkable behaviour scored
26
+ # 0.85-0.96. Eight real task prompts landed 0.85-0.96 too. A
27
+ # 0.58 gap, so this sits in the middle of it: 0.28 clear on
28
+ # both sides, against a measured repeat-noise of 0.07.
29
+ # 0.30 also classified every case correctly, but sat 0.03 above
30
+ # the worst true positive -- correct by luck, not by margin.
31
+ prompt_warn=0.55,
32
+ # The rubric asked whether verifying a task needs "work
33
+ # declared in a different task" while the state contained no
34
+ # other task, so the model could only guess whether one
35
+ # existed. That, not the wording, is why the classes used to
36
+ # overlap. The plan is now in state (routing._siblings) and the
37
+ # rubric points at it by name.
38
+ #
39
+ # Re-measured on the 8 real jevscope tasks, ground truth taken
40
+ # from the import graph and wave position rather than from the
41
+ # titles: `server` (wave 1, imports discover/events/stats and
42
+ # serves page's index.html, none declared) 0.26-0.34; `page`
43
+ # (consumes server's API -- both defects that shipped were
44
+ # page<->events shape mismatches) 0.43-0.53; `readme`
45
+ # (documents the wave-2 CLI, and shipped wrong) 0.60-0.65.
46
+ # Genuinely self-contained tasks scored 0.65-0.96.
47
+ #
48
+ # At the old 0.38 this gate caught NOTHING on a real plan, the
49
+ # same way coupling never fired: every fragment scored above
50
+ # it. 0.55 catches server (0.21 clear) and page (0.02 clear --
51
+ # marginal, counted as recall but not relied on), misses readme
52
+ # entirely, and keeps 0.10 to the nearest true negative, which
53
+ # is the only margin here that beats the 0.07 noise floor.
54
+ # Precision is what an advisory gate must hold: a gate that
55
+ # cries wolf is one people switch off.
56
+ #
57
+ # `readme` is not separable from a real task by this rubric --
58
+ # it sits at 0.65 alongside `cli` at 0.65 -- so silence above
59
+ # 0.55 still means "no evidence", not "verifiable".
60
+ #
61
+ # Equal to prompt_warn by coincidence, not by kinship. They
62
+ # were measured separately against different rubrics and must
63
+ # stay separate keys; collapsing them into one constant is the
64
+ # exact mistake this file has now recorded six times.
65
+ verifiable_warn=0.55,
66
+ # Coupling borrowed gate_warn (0.60) and therefore never fired
67
+ # once, on any plan. Measured over 11 same-wave pairs from a
68
+ # real 8-task plan, 5 runs each, with ground truth taken from
69
+ # the run itself -- two of those pairs produced a defect that
70
+ # actually shipped. Coupling probabilities live at 0.09-0.27.
71
+ # 0.60 was not a strict threshold, it was unreachable.
72
+ #
73
+ # At 0.20: 4 of 5 genuinely coupled pairs warn, 0 of 6
74
+ # independent ones do. Provisional -- one plan, one repo, and
75
+ # margins of about 0.02 either side -- but a gate that fires on
76
+ # the right pairs 4 times in 5 beats one that cannot fire.
77
+ coupling_warn=0.20,
78
+ # The ordering question coupling could not ask: an earlier-wave
79
+ # task written against what a LATER task builds. jevscope's
80
+ # `readme` ran in wave 1 documenting the wave-2 CLI, passed its
81
+ # own review because the thing it was wrong about did not exist
82
+ # yet, and shipped describing an interface nobody wrote.
83
+ #
84
+ # Measured over that plan's 7 cross-wave pairs, 5 runs each:
85
+ # readme->cli 0.88-0.90, every independent pair 0.05-0.15. A
86
+ # 0.73-wide gap, the cleanest any rubric here has produced, so
87
+ # this sits in the middle with 0.35 clear below and 0.38 above
88
+ # against a 0.07 noise floor.
89
+ #
90
+ # Provisional in one specific way: the gap is enormous but it
91
+ # rests on a single positive example. A second plan could move
92
+ # the positive class; it is unlikely to move it 0.38.
93
+ forward_warn=0.50,
94
+ # Two rubrics, and they were measured separately because a
95
+ # shared name is not a shared scale. The measurement then said
96
+ # the scales ARE close -- a correct judgment scores 0.91+ on
97
+ # both, a wrong one 0.72 or less on both -- so what separates
98
+ # these two numbers is not the scale but the cost of being
99
+ # wrong, and each sits at a different point in the same gap.
100
+ #
101
+ # A wrong merge loses a fact permanently, so it takes the top
102
+ # of the gap: 0.13 clear of the worst false merge, 0.06 under
103
+ # the weakest true one.
104
+ memory_merge=0.85,
105
+ # A wrong refile is recoverable -- the fact is still there,
106
+ # under the wrong heading -- and refusing a correction is the
107
+ # commoner harm, so it takes the bottom of the same gap.
108
+ memory_recategorize=0.80,
109
+ # Deliberately far below gate_warn: dropping a fact is
110
+ # irreversible, so only a confident "this went false" acts.
111
+ stale_drop=0.25)
112
+ DEFAULTS = dict(enabled=False, key='', model=MODEL, endpoint=ENDPOINT, deadline_s=5.0,
113
+ capabilities={c: True for c in CAPABILITIES},
114
+ thresholds=dict(DEFAULT_THRESHOLDS),
115
+ # A second provider, per site. Empty means every site asks TypeSafe exactly
116
+ # as before; see "Cloudflare Clef" in references/jev.md.
117
+ providers={}, sites={}, model_thresholds={})
118
+
119
+ # Cloudflare Workers AI's hosted Clef: Jev's request shape, a different envelope and
120
+ # limits. Context is state AND questions together; a text state that does not fit is
121
+ # cut silently by the service, so the client refuses rather than send it.
122
+ PROVIDERS = ('typesafe', 'cloudflare')
123
+ CLOUDFLARE_URL = 'https://api.cloudflare.com/client/v4/accounts/{account}/ai/run/@cf/cloudflare/{model}'
124
+ CLOUDFLARE_MODEL = 'clef'
125
+ CLOUDFLARE_CONTEXT = {'clef': 65536, 'clef-flash': 24576}
126
+ CLOUDFLARE_MAX_QUESTIONS = 64
127
+ # Beside jev.json (honoring XDG_CONFIG_HOME), so a test's private config home never reads
128
+ # the developer's real token.
129
+ CLOUDFLARE_TOKEN_FILE = 'cloudflare.env'
130
+
131
+ # Which capability each site belongs to, so a site on a provider with no credentials is
132
+ # not ready even when its capability is on. clef-* sites exist only on Cloudflare: they
133
+ # ask rubrics TypeSafe was never measured on, and record silently.
134
+ SITE_CAPABILITY = {
135
+ 'jev-score-plan': 'routing',
136
+ 'clef-first-pass': 'routing', 'clef-retry-pass': 'routing',
137
+ 'jev-drift': 'gates', 'jev-coupling': 'gates', 'jev-forward': 'gates',
138
+ 'jev-review-triage': 'gates',
139
+ 'jev-memory-curate': 'memory', 'jev-memory-stale': 'memory', 'jev-memory-slice': 'memory',
140
+ 'jev-test-subset': 'tests', 'jev-verify-discover': 'tests', 'jev-verify-triage': 'tests',
141
+ }
142
+ CLEF_ONLY_SITES = ('clef-first-pass', 'clef-retry-pass')
143
+
144
+ # Loaded via importlib rather than a package-relative import: forge-runtime.py lives one
145
+ # directory up and is not itself part of a package (same trick as forge_fractal).
146
+ _runtime_spec = importlib.util.spec_from_file_location('forge_runtime', SCRIPTS / 'forge-runtime.py')
147
+ _runtime = importlib.util.module_from_spec(_runtime_spec)
148
+ _runtime_spec.loader.exec_module(_runtime)
149
+
150
+
151
+ def config_path() -> Path:
152
+ # An empty XDG_CONFIG_HOME counts as unset (XDG spec): taken literally it put jev.json
153
+ # below the current directory.
154
+ return Path(os.environ.get('XDG_CONFIG_HOME') or str(Path.home() / '.config')).expanduser() / 'forge/jev.json'
155
+
156
+
157
+ def load_config() -> dict:
158
+ data = dict(DEFAULTS)
159
+ data['capabilities'] = dict(DEFAULTS['capabilities'])
160
+ data['thresholds'] = dict(DEFAULTS['thresholds'])
161
+ try:
162
+ stored = json.loads(config_path().read_text())
163
+ except (OSError, ValueError):
164
+ return data
165
+ # Valid JSON that is not an object (a list, a bare string, null) would make update()
166
+ # raise, and a config file is not worth failing a run over. Treat it as absent.
167
+ if not isinstance(stored, dict):
168
+ return data
169
+ data.update(stored)
170
+ for section in ('capabilities', 'thresholds', 'providers', 'sites', 'model_thresholds'):
171
+ value = stored.get(section)
172
+ data[section] = dict(DEFAULTS[section], **value) if isinstance(value, dict) else dict(DEFAULTS[section])
173
+ return data
174
+
175
+
176
+ def save_config(data: dict) -> None:
177
+ path = config_path()
178
+ path.parent.mkdir(parents=True, exist_ok=True)
179
+ temporary = path.with_name(path.name + '.' + uuid.uuid4().hex)
180
+ temporary.write_text(json.dumps(data, indent=2) + '\n')
181
+ temporary.chmod(0o600)
182
+ temporary.replace(path)
183
+ path.chmod(0o600)
184
+
185
+
186
+ def api_key(config: dict | None = None) -> str | None:
187
+ env = os.environ.get('TYPESAFE_API_KEY')
188
+ if env:
189
+ return env
190
+ key = (config if config is not None else load_config()).get('key')
191
+ return key or None
192
+
193
+
194
+ def redact(text: str, *keys: str) -> str:
195
+ for key in keys:
196
+ if key:
197
+ text = text.replace(key, '***')
198
+ for var in ('TYPESAFE_API_KEY', 'CLOUDFLARE_API_TOKEN'):
199
+ env = os.environ.get(var)
200
+ if env:
201
+ text = text.replace(env, '***')
202
+ return text
203
+
204
+
205
+ def _env_file(path: str) -> dict:
206
+ """KEY=VALUE lines of a private env file; {} when it is missing or unreadable."""
207
+ out = {}
208
+ try:
209
+ lines = Path(path).expanduser().read_text().splitlines()
210
+ except (OSError, UnicodeDecodeError):
211
+ return out
212
+ for line in lines:
213
+ line = line.strip()
214
+ if not line or line.startswith('#') or '=' not in line:
215
+ continue
216
+ name, _, value = line.partition('=')
217
+ name = name.strip()
218
+ if name.startswith('export '):
219
+ name = name[len('export '):].strip()
220
+ out[name] = value.strip().strip('"').strip("'")
221
+ return out
222
+
223
+
224
+ def cloudflare(config: dict | None = None) -> dict | None:
225
+ """The Cloudflare provider's account, token and model, or None when not configured.
226
+
227
+ Configured means a `providers.cloudflare` entry exists, or the token is in the
228
+ environment. The token comes from CLOUDFLARE_API_TOKEN, else the entry's `token`,
229
+ else its `token_file` (default cloudflare.env beside jev.json) -- never from argv.
230
+ """
231
+ config = config if config is not None else load_config()
232
+ entry = (config.get('providers') or {}).get('cloudflare')
233
+ if not isinstance(entry, dict):
234
+ entry = None
235
+ if entry is None and not os.environ.get('CLOUDFLARE_API_TOKEN'):
236
+ return None
237
+ entry = entry or {}
238
+ from_file = _env_file(entry.get('token_file') or str(config_path().parent / CLOUDFLARE_TOKEN_FILE))
239
+ token = os.environ.get('CLOUDFLARE_API_TOKEN') or entry.get('token') or from_file.get('CLOUDFLARE_API_TOKEN')
240
+ account = (os.environ.get('CLOUDFLARE_ACCOUNT_ID') or entry.get('account_id')
241
+ or from_file.get('CLOUDFLARE_ACCOUNT_ID'))
242
+ model = entry.get('model') or CLOUDFLARE_MODEL
243
+ # `url` is a template with {account} and {model}: an AI Gateway route, or a local test server.
244
+ return dict(token=token or None, account_id=account or None,
245
+ model=model if model in CLOUDFLARE_CONTEXT else CLOUDFLARE_MODEL,
246
+ url=entry.get('url') or CLOUDFLARE_URL)
247
+
248
+
249
+ def secrets(config: dict | None = None) -> list:
250
+ """Every credential forge might send, for redaction."""
251
+ config = config if config is not None else load_config()
252
+ out = [api_key(config)]
253
+ cf = cloudflare(config)
254
+ if cf:
255
+ out.append(cf['token'])
256
+ return [s for s in out if s]
257
+
258
+
259
+ def provider_for(site: str, config: dict | None = None) -> str:
260
+ """Which provider answers `site`: the site map, unless an env override says otherwise."""
261
+ if site in CLEF_ONLY_SITES:
262
+ return 'cloudflare'
263
+ if os.environ.get('FORGE_CLEF') == 'off':
264
+ return 'typesafe'
265
+ forced = os.environ.get('FORGE_JEV_PROVIDER')
266
+ if forced in PROVIDERS:
267
+ return forced
268
+ config = config if config is not None else load_config()
269
+ chosen = (config.get('sites') or {}).get(site)
270
+ return chosen if chosen in PROVIDERS else 'typesafe'
271
+
272
+
273
+ def provider_ready(name: str, config: dict | None = None) -> bool:
274
+ config = config if config is not None else load_config()
275
+ if name == 'cloudflare':
276
+ cf = cloudflare(config)
277
+ return bool(cf and cf['token'] and cf['account_id'])
278
+ return api_key(config) is not None
279
+
280
+
281
+ def site_ready(site: str, config: dict | None = None) -> bool:
282
+ """The site's capability is on and its provider has credentials. Offline: reads config only."""
283
+ config = config if config is not None else load_config()
284
+ capability = SITE_CAPABILITY.get(site)
285
+ if not enabled(capability, config=config):
286
+ return False
287
+ if site in CLEF_ONLY_SITES:
288
+ entry = (config.get('providers') or {}).get('cloudflare')
289
+ if (os.environ.get('FORGE_CLEF') == 'off' or os.environ.get('FORGE_JEV_OUTCOMES') == 'off'
290
+ or (isinstance(entry, dict) and entry.get('outcomes') is False)):
291
+ return False
292
+ return provider_ready(provider_for(site, config), config)
293
+
294
+
295
+ def capability_ready(capability: str, config: dict | None = None) -> bool:
296
+ """Some site of this capability can be asked: what the shell's forge_jev_active tests."""
297
+ config = config if config is not None else load_config()
298
+ return any(site_ready(site, config) for site, cap in SITE_CAPABILITY.items()
299
+ if cap == capability and site not in CLEF_ONLY_SITES)
300
+
301
+
302
+ def _capability_overrides() -> dict[str, str]:
303
+ names = dict(routing='FORGE_JEV_ROUTING', tests='FORGE_JEV_TESTS', gates='FORGE_JEV_GATES', memory='FORGE_JEV_MEMORY')
304
+ return {cap: os.environ[var] for cap, var in names.items() if os.environ.get(var) in ('on', 'off')}
305
+
306
+
307
+ def enabled(capability: str | None = None, *, config: dict | None = None) -> bool:
308
+ if os.environ.get('FORGE_JEV') == 'off':
309
+ return False
310
+ overrides = _capability_overrides()
311
+ # An explicit --jev-<cap> is an allowlist, so naming one capability does not
312
+ # silently enable the rest.
313
+ if 'on' in overrides.values():
314
+ return capability is None or overrides.get(capability) == 'on'
315
+ if overrides.get(capability) == 'off':
316
+ return False
317
+ stored = config if config is not None else load_config()
318
+ if os.environ.get('FORGE_JEV') != 'on' and not stored['enabled']:
319
+ return False
320
+ return capability is None or stored['capabilities'].get(capability, True)
321
+
322
+
323
+ def acting() -> bool:
324
+ return os.environ.get('FORGE_JEV_ACT') == 'on'
325
+
326
+
327
+ def shadow() -> bool:
328
+ return os.environ.get('FORGE_JEV_SHADOW') == 'on'
329
+
330
+
331
+ def threshold(name: str, *, config: dict | None = None, model: str | None = None) -> float:
332
+ """A rubric's cutoff. `model` is the model that answered: a threshold measured on one
333
+ model's probabilities means nothing on another's, so a per-model value wins."""
334
+ config = config or load_config()
335
+ if model:
336
+ per_model = dict(DEFAULT_MODEL_THRESHOLDS.get(model, {}),
337
+ **((config.get('model_thresholds') or {}).get(model) or {}))
338
+ if name in per_model:
339
+ return per_model[name]
340
+ if name not in config['thresholds'] and name in THRESHOLD_FALLBACK:
341
+ return threshold(THRESHOLD_FALLBACK[name], config=config)
342
+ return config['thresholds'].get(name, DEFAULT_THRESHOLDS.get(name))
343
+
344
+
345
+ # Rubric thresholds introduced after a shared one: until measured separately they read the
346
+ # old shared value, so a config that never named them behaves exactly as before.
347
+ THRESHOLD_FALLBACK = dict(drift_warn='gate_warn')
348
+ # Thresholds whose only reader was removed on 2026-10-17 (routing's --jev-act tier rewrite
349
+ # and `forge jev calibrate`). A config written before then still holds them: they are
350
+ # ignored, and `doctor` names them as safe to delete rather than as unknown.
351
+ RETIRED_THRESHOLDS = frozenset(('routing_act', 'calibrate_floor'))
352
+ # Per-model cutoffs, only where a replay justified one. Clef has none yet:
353
+ #
354
+ # Replayed 2026-10-16 on the 38 live jev-drift requests whose task outcome could be joined
355
+ # (4560 decisions, 16 positives, 17 runs; older half of runs to calibrate, newer half to
356
+ # evaluate). Clef ranks drift at least as well as Jev on the held-out runs (AUC 0.985 vs
357
+ # 0.972, paired run-bootstrap difference [0.000, +0.028]) and at the shared 0.60 cutoff its
358
+ # warnings are no less precise (1 of 1 against 1 of 2). The calibration half could not fit
359
+ # a cutoff of its own: neither model warned precisely there (Jev flagged nothing at 0.60).
360
+ # So drift_warn keeps reading gate_warn for Clef too; the evaluation half hints 0.40 (3 of
361
+ # 4 right), but choosing it there would be fitting on the data that judges it.
362
+ DEFAULT_MODEL_THRESHOLDS: dict = {}
363
+
364
+ # The sites a replay showed Clef non-inferior on, which `forge jev setup --provider
365
+ # cloudflare` maps by default. jev-memory-slice tied (AUC 0.951 vs 0.953) on too few held-out
366
+ # runs (5) to clear the rule; jev-review-triage has no live label for its correctness half.
367
+ CLOUDFLARE_DEFAULT_SITES = ('jev-drift',)
368
+
369
+
370
+ def write_json(path: Path, value: object) -> None:
371
+ path.parent.mkdir(parents=True, exist_ok=True)
372
+ _runtime.write_json(path, value)
373
+
374
+
375
+ def read_json(path: Path) -> dict:
376
+ return json.loads(path.read_text())