@giamme/forge 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Mandatory, durable planning decisions for opt-in automatic task trees."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
from . import artifact, write_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
WORK_PROTOCOL = '''
|
|
14
|
+
Forge has already made this node's decomposition decision. Implement the remaining
|
|
15
|
+
scoped work, integrate completed child results, and run the requested verification.
|
|
16
|
+
Do not spawn children or change control metadata. End your final response with:
|
|
17
|
+
FORGE_FRACTAL: {"done":true}
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
PLAN_PROTOCOL = '''
|
|
21
|
+
FORGE_DECOMPOSITION_DECISION
|
|
22
|
+
This is a planning-only invocation. Inspect the repository without changing files,
|
|
23
|
+
running implementation, or using Fractal control commands. The approved task contract
|
|
24
|
+
and ownership are authoritative; decide only how to execute this node within them.
|
|
25
|
+
Prefer splitting nontrivial work into useful, independently verifiable children.
|
|
26
|
+
Keep genuinely atomic or tightly coupled work together and explain why.
|
|
27
|
+
Child goals must contain complete instructions and verification requirements.
|
|
28
|
+
Use globally unique child IDs, declared paths within the node's ownership, and sibling
|
|
29
|
+
dependencies for ordering. Existing completed work must be preserved, not delegated again.
|
|
30
|
+
Your children receive this same planning stage automatically. Do not force needless depth.
|
|
31
|
+
Return exactly one final-line JSON marker, with a nonempty reason:
|
|
32
|
+
FORGE_FRACTAL: {"decision":"atomic","reason":"why this work should remain together"}
|
|
33
|
+
or:
|
|
34
|
+
FORGE_FRACTAL: {"decision":"split","reason":"why this split is useful","children":[
|
|
35
|
+
{"id":"unique-name","goal":"complete instructions","difficulty":"low","paths":["relative/path"],"deps":[]},
|
|
36
|
+
{"id":"another-name","goal":"complete instructions","difficulty":"medium","paths":["another/path"],"deps":[]}]}
|
|
37
|
+
The split marker must be on ONE line and contain at least two children. Do not return done.
|
|
38
|
+
'''
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_decision(text: str) -> dict:
|
|
42
|
+
markers = [line for line in text.splitlines() if line.startswith('FORGE_FRACTAL: ')]
|
|
43
|
+
if len(markers) != 1 or text.strip().splitlines()[-1] != markers[0]:
|
|
44
|
+
raise ValueError('Expected exactly one final FORGE_FRACTAL decision marker')
|
|
45
|
+
value = json.loads(markers[0].split(': ', 1)[1])
|
|
46
|
+
if not isinstance(value, dict) or value.get('decision') not in ('split', 'atomic'):
|
|
47
|
+
raise ValueError('Planner must decide split or atomic; done is not a planning decision')
|
|
48
|
+
if not isinstance(value.get('reason'), str) or not value['reason'].strip():
|
|
49
|
+
raise ValueError('Decomposition decision requires a nonempty reason')
|
|
50
|
+
allowed = {'decision', 'reason', 'children'} if value['decision'] == 'split' else {'decision', 'reason'}
|
|
51
|
+
if set(value) != allowed:
|
|
52
|
+
raise ValueError('Unexpected or missing decomposition decision fields')
|
|
53
|
+
if value['decision'] == 'split':
|
|
54
|
+
children = value['children']
|
|
55
|
+
if not isinstance(children, list) or len(children) < 2:
|
|
56
|
+
raise ValueError('A split requires at least two children')
|
|
57
|
+
for child in children:
|
|
58
|
+
_validate_child_shape(child)
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validate_child_shape(child: dict) -> None:
|
|
63
|
+
required = {'id', 'goal', 'difficulty', 'paths'}
|
|
64
|
+
if not isinstance(child, dict) or not required <= child.keys() or child.keys() - required - {'deps'}:
|
|
65
|
+
raise ValueError('Invalid child fields')
|
|
66
|
+
if any(not isinstance(child[key], str) or not child[key].strip() for key in ('id', 'goal', 'difficulty')):
|
|
67
|
+
raise ValueError('Child id, goal and difficulty must be nonempty strings')
|
|
68
|
+
for key in ('paths', 'deps'):
|
|
69
|
+
values = child.get(key, [])
|
|
70
|
+
if not isinstance(values, list) or any(not isinstance(item, str) for item in values):
|
|
71
|
+
raise ValueError('Child paths and deps must be lists of strings')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def manifests(node: dict) -> list[dict]:
|
|
75
|
+
return [*node.get('decomposition_history', []), node.get('decomposition', {})]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def reserved_nodes(nodes: dict) -> dict:
|
|
79
|
+
"""Count accepted but not yet materialized children against lifetime bounds."""
|
|
80
|
+
result = dict(nodes)
|
|
81
|
+
for node in nodes.values():
|
|
82
|
+
for decision in manifests(node):
|
|
83
|
+
for child in decision.get('admission', []):
|
|
84
|
+
result.setdefault(child['name'], dict(id=child['name'], parent=node['id'], status='reserved'))
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def capacity(tree, node: dict) -> dict:
|
|
89
|
+
nodes = reserved_nodes(tree.nodes())
|
|
90
|
+
unsettled = sum(n['parent'] == node['id'] and n['status'] != 'completed' for n in nodes.values())
|
|
91
|
+
return dict(depth=len(node['ancestors']), max_depth=tree.limits['depth'],
|
|
92
|
+
children=tree.limits['children'] - unsettled, nodes=tree.limits['nodes'] - len(nodes))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def bound_reason(bounds: dict) -> str:
|
|
96
|
+
if bounds['depth'] >= bounds['max_depth']:
|
|
97
|
+
return 'depth limit reached'
|
|
98
|
+
if bounds['children'] < 2:
|
|
99
|
+
return 'fewer than two direct child slots available'
|
|
100
|
+
if bounds['nodes'] < 2:
|
|
101
|
+
return 'fewer than two lifetime node slots available'
|
|
102
|
+
return ''
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def implementation_prompt(tree, node: dict) -> str:
|
|
106
|
+
from .execution import PROTOCOL
|
|
107
|
+
automatic = tree.config.get('decomposition', {}).get('enabled', False)
|
|
108
|
+
prompt = node['goal'] + (WORK_PROTOCOL if automatic else PROTOCOL)
|
|
109
|
+
prompt += '\nOwned paths: ' + json.dumps(node['paths'])
|
|
110
|
+
if automatic:
|
|
111
|
+
prompt += '\nApproved task contract:\n' + tree.request['prompt']
|
|
112
|
+
children = [n for n in tree.nodes().values() if n['parent'] == node['id']]
|
|
113
|
+
if children:
|
|
114
|
+
prompt += '\nCompleted child results (already imported):\n' + json.dumps(children)
|
|
115
|
+
if node.get('last'):
|
|
116
|
+
prompt += '\nPrevious work is preserved. Prior response:\n' + node['last']
|
|
117
|
+
return prompt
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def retry_decision(node: dict, prompt: str) -> None:
|
|
121
|
+
decision = node.get('decomposition')
|
|
122
|
+
changed_goal = node['id'] == 'implementation' and node['goal'] != prompt
|
|
123
|
+
if decision and (changed_goal or decision['phase'] != 'accepted'):
|
|
124
|
+
node.setdefault('decomposition_history', []).append(node.pop('decomposition'))
|
|
125
|
+
node.pop('error', None)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def snapshot_fingerprint(workspace: Path) -> str:
|
|
129
|
+
"""Include ignored/product files as well as HEAD and staged content."""
|
|
130
|
+
digest = hashlib.sha256(artifact('forge_fingerprint', workspace).encode())
|
|
131
|
+
for root, dirs, files in os.walk(workspace, followlinks=False):
|
|
132
|
+
if Path(root) == workspace:
|
|
133
|
+
dirs[:] = [name for name in dirs if name != '.git']
|
|
134
|
+
for name in sorted(dirs + files):
|
|
135
|
+
path = Path(root) / name
|
|
136
|
+
digest.update(str(path.relative_to(workspace)).encode())
|
|
137
|
+
digest.update(str(path.lstat().st_mode).encode())
|
|
138
|
+
if path.is_symlink():
|
|
139
|
+
digest.update(os.readlink(path).encode())
|
|
140
|
+
elif path.is_file():
|
|
141
|
+
with path.open('rb') as stream:
|
|
142
|
+
for block in iter(lambda: stream.read(65536), b''):
|
|
143
|
+
digest.update(block)
|
|
144
|
+
dirs.sort()
|
|
145
|
+
return digest.hexdigest()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DecisionStage:
|
|
149
|
+
def __init__(self, tree) -> None:
|
|
150
|
+
self.tree = tree
|
|
151
|
+
|
|
152
|
+
def save(self, node: dict, decision: dict) -> None:
|
|
153
|
+
from .execution import event
|
|
154
|
+
self.tree.update(node, decomposition=decision)
|
|
155
|
+
event(self.tree.task, 'decomposition_decision', node=node['id'],
|
|
156
|
+
phase=decision['phase'], decision=decision.get('decision'), reason=decision.get('reason'))
|
|
157
|
+
|
|
158
|
+
def recover_admission(self, node: dict) -> None:
|
|
159
|
+
# A manifest is the durable reservation and fixes routing before any spawn.
|
|
160
|
+
with self.tree.guard:
|
|
161
|
+
for decision in manifests(node):
|
|
162
|
+
for child in decision.get('admission', []):
|
|
163
|
+
if child['name'] not in self.tree.nodes():
|
|
164
|
+
self.tree.create_node(parent=node, **child)
|
|
165
|
+
self.tree.update(node)
|
|
166
|
+
|
|
167
|
+
def ensure(self, node: dict, workspace: Path) -> None:
|
|
168
|
+
from .execution import Halt
|
|
169
|
+
self.recover_admission(node)
|
|
170
|
+
if node.get('awaiting_children'):
|
|
171
|
+
self.tree.children(node)
|
|
172
|
+
self.tree.update(node, awaiting_children=False)
|
|
173
|
+
decision = node.get('decomposition')
|
|
174
|
+
if decision and decision['phase'] == 'accepted':
|
|
175
|
+
return
|
|
176
|
+
if decision and decision['phase'] == 'failed':
|
|
177
|
+
raise Halt(decision['error'])
|
|
178
|
+
if decision is None:
|
|
179
|
+
decision = dict(phase='pending', attempts=0,
|
|
180
|
+
goal_sha256=hashlib.sha256(node['goal'].encode()).hexdigest(),
|
|
181
|
+
planner=self.tree.request['planner'])
|
|
182
|
+
self.save(node, decision)
|
|
183
|
+
while decision['attempts'] < 2:
|
|
184
|
+
self.tree.pause_gate(node)
|
|
185
|
+
bounds = capacity(self.tree, node)
|
|
186
|
+
if reason := bound_reason(bounds):
|
|
187
|
+
self.save(node, dict(decision, phase='accepted', decision='bounded', reason=reason, bounds=bounds))
|
|
188
|
+
return
|
|
189
|
+
decision.update(phase='planning', attempts=decision['attempts'] + 1, bounds=bounds)
|
|
190
|
+
self.save(node, decision)
|
|
191
|
+
try:
|
|
192
|
+
text = self.invoke(node, workspace, decision)
|
|
193
|
+
proposal = parse_decision(text)
|
|
194
|
+
self.accept(node, decision, proposal)
|
|
195
|
+
except Halt as error:
|
|
196
|
+
if str(error) == 'paused':
|
|
197
|
+
decision.update(phase='pending', attempts=decision['attempts'] - 1)
|
|
198
|
+
self.save(node, decision)
|
|
199
|
+
continue
|
|
200
|
+
if str(error) not in ('stopped', 'interrupted', 'timeout'):
|
|
201
|
+
self.save(node, dict(decision, phase='failed', error=str(error)))
|
|
202
|
+
raise
|
|
203
|
+
except (ValueError, KeyError, TypeError) as error:
|
|
204
|
+
decision.update(phase='pending', feedback=str(error))
|
|
205
|
+
self.save(node, decision)
|
|
206
|
+
continue
|
|
207
|
+
self.recover_admission(node)
|
|
208
|
+
if node.get('awaiting_children'):
|
|
209
|
+
self.tree.children(node)
|
|
210
|
+
self.tree.update(node, awaiting_children=False)
|
|
211
|
+
return
|
|
212
|
+
self.save(node, dict(decision, phase='failed', error='planning_exhausted'))
|
|
213
|
+
raise Halt('planning_exhausted')
|
|
214
|
+
|
|
215
|
+
def invoke(self, node: dict, workspace: Path, decision: dict) -> str:
|
|
216
|
+
from .execution import Halt
|
|
217
|
+
from .invocation import run_step
|
|
218
|
+
directory = self.tree.task / 'nodes' / node['id'] / 'planning' / uuid.uuid4().hex
|
|
219
|
+
snapshot = directory / 'product'
|
|
220
|
+
base = artifact('forge_tree', workspace)
|
|
221
|
+
source_before = artifact('forge_fingerprint', workspace)
|
|
222
|
+
artifact('forge_review_snapshot', workspace, base, base, snapshot)
|
|
223
|
+
before = snapshot_fingerprint(snapshot)
|
|
224
|
+
children = [n for n in self.tree.nodes().values() if n['parent'] == node['id']]
|
|
225
|
+
context = dict(node=node['id'], goal=node['goal'], owned_paths=node['paths'],
|
|
226
|
+
bounds=decision['bounds'], routing=self.tree.config['routing'],
|
|
227
|
+
children=children, feedback=decision.get('feedback'),
|
|
228
|
+
existing_ids=list(reserved_nodes(self.tree.nodes())))
|
|
229
|
+
prompt = ('Approved task contract:\n' + self.tree.request['prompt'] + '\n' + PLAN_PROTOCOL
|
|
230
|
+
+ '\nNode planning context:\n' + json.dumps(context))
|
|
231
|
+
write_json(directory / 'request.json', context)
|
|
232
|
+
try:
|
|
233
|
+
rc, text = run_step(self.tree, node, snapshot, prompt, phase='planning')
|
|
234
|
+
finally:
|
|
235
|
+
try:
|
|
236
|
+
unchanged = snapshot_fingerprint(snapshot) == before
|
|
237
|
+
except (OSError, RuntimeError):
|
|
238
|
+
unchanged = False
|
|
239
|
+
if not unchanged or artifact('forge_fingerprint', workspace) != source_before:
|
|
240
|
+
raise Halt('planning_mutation')
|
|
241
|
+
self.tree.update(node, planning_last=text)
|
|
242
|
+
if rc:
|
|
243
|
+
raise Halt('timeout' if rc == 7 else 'planning_failed')
|
|
244
|
+
return text
|
|
245
|
+
|
|
246
|
+
def accept(self, node: dict, decision: dict, proposal: dict) -> None:
|
|
247
|
+
with self.tree.guard:
|
|
248
|
+
bounds = capacity(self.tree, node)
|
|
249
|
+
reason = bound_reason(bounds)
|
|
250
|
+
needed = len(proposal.get('children', []))
|
|
251
|
+
offered = decision.get('bounds', bounds)
|
|
252
|
+
if min(bounds['children'], bounds['nodes']) < needed <= min(offered['children'], offered['nodes']):
|
|
253
|
+
reason = 'child capacity consumed by another admission during planning'
|
|
254
|
+
if proposal['decision'] == 'split' and reason:
|
|
255
|
+
proposal = dict(decision='bounded', reason=reason)
|
|
256
|
+
admission = []
|
|
257
|
+
if proposal['decision'] == 'split':
|
|
258
|
+
admission = self.tree.validate_children(node, proposal['children'])
|
|
259
|
+
accepted = dict(decision, **proposal, phase='accepted', bounds=bounds, admission=admission)
|
|
260
|
+
# Decision and awaiting state share one atomic node.json write.
|
|
261
|
+
node['awaiting_children'] = bool(admission)
|
|
262
|
+
self.save(node, accepted)
|