@julioborges/gantry 1.0.1 → 1.0.4
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/.agents/skills/gantry/SKILL.md +32 -1
- package/.agents/skills/gantry/capabilities/antigravity.json +15 -0
- package/.agents/skills/gantry/hooks/antigravity.hooks.json +26 -0
- package/.agents/skills/gantry/reference/plan-workflow.md +52 -14
- package/.agents/skills/gantry/reference/round-workflow.md +277 -11
- package/.agents/skills/gantry/scripts/budget.py +49 -7
- package/.agents/skills/gantry/scripts/caveman.py +243 -0
- package/.agents/skills/gantry/scripts/common.py +2 -0
- package/.agents/skills/gantry/scripts/discovery.py +262 -0
- package/.agents/skills/gantry/scripts/execution.py +797 -0
- package/.agents/skills/gantry/scripts/frontier.py +1 -1
- package/.agents/skills/gantry/scripts/guard.py +91 -25
- package/.agents/skills/gantry/scripts/runlog.py +38 -1
- package/.agents/skills/gantry/scripts/setup.py +39 -0
- package/.agents/skills/gantry-setup/SKILL.md +16 -3
- package/README.md +2 -0
- package/assets/gantry.png +0 -0
- package/package.json +23 -6
|
@@ -28,6 +28,34 @@ Before a Run, resolve these values once and pass them as `args` to every referen
|
|
|
28
28
|
`<repoRoot>/.gantry/config.json` when present. A missing repository policy is valid.
|
|
29
29
|
4. `paths` comes from `common.resolve_workflow_paths(repoRoot, scopeSlug)`. Prompts receive paths, not
|
|
30
30
|
repository-specific literals.
|
|
31
|
+
5. `caveman` comes from resolving repository preference `policy["caveman"]` and host availability
|
|
32
|
+
via `caveman.resolve_activation(policy, harness=hostHarness, root=repoRoot, warned=args.cavemanWarned)`.
|
|
33
|
+
When active, the coordinating agent, planning roles (Requirement Critic, research, Planner, Plan Critic)
|
|
34
|
+
and round roles (Implementer, Reviewer, Critic, optional Learner) across all initial calls, retries,
|
|
35
|
+
review fixes and correction passes receive concise phrasing instructions for conversational messages
|
|
36
|
+
and summaries along with supported external-skill access, while all Specs, draft Issues, code,
|
|
37
|
+
documentation, lesson candidates, PR descriptions, Result Contracts, exact commands, exact errors,
|
|
38
|
+
acceptance criteria and verification evidence retain full detail. If enabled in policy but unavailable
|
|
39
|
+
in the host environment, emit at most one actionable warning per Run with installation guidance and
|
|
40
|
+
continue with normal behavior without claiming live token savings; a disabled preference does not load
|
|
41
|
+
the skill. The manual harness-neutral path carries the same instructions.
|
|
42
|
+
6. `roles` comes from resolving repository defaults under `policy["execution"]["roles"]` overlaid by
|
|
43
|
+
optional Run and Issue overrides via `execution.resolve_roles()`. Preflight validates that every selected
|
|
44
|
+
combination is supported, executable, meets minimum installed harness version requirements, and authenticated
|
|
45
|
+
(via `execution.preflight_validate()`), refusing to start implementation when validation fails. Derived roles
|
|
46
|
+
(`requirement-critic`, `plan-critic`, `learner` inheriting from `critic`, and `research` from `plan`) adopt
|
|
47
|
+
parent defaults unless explicitly set. Bounded native harness invocations execute roles across Codex CLI,
|
|
48
|
+
Claude Code, OpenCode, and Antigravity (`agy --print`) while the Host Harness retains coordination. External
|
|
49
|
+
results pass `result.py` contract validation, missing or invalid results trigger Protocol Failure handling,
|
|
50
|
+
unsupported installed versions are rejected, Critic verification fails visibly on permission or tool limitations,
|
|
51
|
+
selection identity is honestly recorded in the Run log, and native or configured automatic model fallback
|
|
52
|
+
is detected and rejected. Runtime execution failures preserve work and pause the affected Issue separately
|
|
53
|
+
from Critic refutations and protocol failures; independent Issues integrate serially, while the next round
|
|
54
|
+
waits for explicit recovery without automatic fallback or retry. Explicit Issue-role replacements are
|
|
55
|
+
validated and recorded via `role.changed`, leave saved repository defaults and running agents unchanged, and
|
|
56
|
+
preserve spent correction budgets. Run log selection (`role.selected`) and change (`role.changed`) events
|
|
57
|
+
record requested and effective selection evidence without command outputs or credentials. Model strength
|
|
58
|
+
guidance between roles is advisory and does not block valid cross-family selections.
|
|
31
59
|
|
|
32
60
|
Run the standard-library workflow scripts as `python3 <skillDir>/scripts/<script>.py`. `common.py` provides
|
|
33
61
|
the shared Markdown parser and policy resolution; its `--json` path prints the resolved portable runtime.
|
|
@@ -123,7 +151,9 @@ Every script provides `--help`, and data-producing paths support `--json`.
|
|
|
123
151
|
and Spec axes, one review fix pass, then a fresh adversarial Critic. The Critic alone can establish a
|
|
124
152
|
complete delivery. Its refutation consumes at most the correction budget.
|
|
125
153
|
- A multi-Issue round uses one isolated worktree and branch per implementer. Integrate accepted branches
|
|
126
|
-
serially, run gates after every merge, and stop on a failed integration gate.
|
|
154
|
+
serially, run gates after every merge, and stop on a failed integration gate. When an Issue execution fails,
|
|
155
|
+
pause that Issue, preserve its worktree and branch, allow independent accepted Issues to integrate, and block
|
|
156
|
+
the next round until explicit validated recovery. Create and identify each
|
|
127
157
|
Issue branch through the `git.issueBranch` policy template (default
|
|
128
158
|
`{prefix}{spec}-{number:02d}`), rendered by `common.issue_branch(policy, issue)`.
|
|
129
159
|
- Only after Critic acceptance, green gates and a clean worktree may the orchestrator run
|
|
@@ -163,4 +193,5 @@ command's stdin, not appended to the command line. Every recorded Run event goes
|
|
|
163
193
|
critic and mandatory operator stop.
|
|
164
194
|
- `reference/round-workflow.md` — TDD implementation, two-axis review, adversarial Critic and serial
|
|
165
195
|
integration contract.
|
|
196
|
+
- `docs/role-execution.md` — cross-harness execution, model discovery, role overrides, and failure recovery.
|
|
166
197
|
- `templates/` — default Spec, PRD and Issue structures.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tier": "compatible",
|
|
3
|
+
"hooks": true,
|
|
4
|
+
"structured_output": true,
|
|
5
|
+
"worktree_isolation": true,
|
|
6
|
+
"per_role_model": true,
|
|
7
|
+
"parallel_round": true,
|
|
8
|
+
"skills_path": ".agents/skills",
|
|
9
|
+
"hook_events": ["PreToolUse", "PostToolUse"],
|
|
10
|
+
"payload_fields": ["tool_name", "tool_input", "session_id"],
|
|
11
|
+
"models": {
|
|
12
|
+
"gemini-3.8-flash-medium": {"contextWindow": 1000000},
|
|
13
|
+
"gemini-3.1-pro-high": {"contextWindow": 1000000}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "python3 \".agents/skills/gantry/scripts/guard.py\" PreToolUse --json"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "*",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "python3 \".agents/skills/gantry/scripts/guard.py\" PostToolUse --json"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -82,6 +82,16 @@ const targetText = t.kind === 'goal' ? `the goal "${t.goal}" (new slug: ${t.slug
|
|
|
82
82
|
: t.kind === 'issue' ? `the Issue at ${t.issuePath} (Spec ${t.specPath})`
|
|
83
83
|
: `the Spec at ${t.specPath} (slug ${t.slug})`
|
|
84
84
|
|
|
85
|
+
const cavemanState = A.caveman || { active: false, preference: false, scope: 'none' }
|
|
86
|
+
const cavemanActive = Boolean(cavemanState && cavemanState.active)
|
|
87
|
+
if (cavemanState && cavemanState.warning) {
|
|
88
|
+
if (typeof log === 'function') log(cavemanState.warning)
|
|
89
|
+
cavemanState.warned = true
|
|
90
|
+
}
|
|
91
|
+
const cavemanInstruction = cavemanActive
|
|
92
|
+
? 'Caveman lite is active: use concise phrasing for conversational messages and summaries. Specs, draft Issues, persisted role results, exact errors, commands, and acceptance criteria retain full detail.'
|
|
93
|
+
: ''
|
|
94
|
+
|
|
85
95
|
async function validateSpecBeforePlanning() {
|
|
86
96
|
if (t.kind === 'goal') return null
|
|
87
97
|
const specPath = t.specPath || paths.specPath
|
|
@@ -138,17 +148,38 @@ async function validRoleResult(role, result) {
|
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
async function requestRole(role, prompt, options) {
|
|
151
|
+
const selection = (options && options.selection) || (A.roles && A.roles[role])
|
|
152
|
+
const hostHarness = A.hostHarness || 'claude-code'
|
|
153
|
+
if (selection && selection.harness && selection.harness !== hostHarness) {
|
|
154
|
+
const cwd = (options && options.cwd) || A.repoRoot
|
|
155
|
+
const selectionJson = JSON.stringify(selection)
|
|
156
|
+
const agentPrompt = cavemanActive ? `${prompt}\n\n${cavemanInstruction}` : prompt
|
|
157
|
+
const cmd = `python3 "${scripts}/execution.py" dispatch --role "${role}" --cwd ${shellQuote(cwd)} --selection '${selectionJson.replaceAll("'", "'\\''")}'`
|
|
158
|
+
const res = await runCommand(cmd, { cwd: A.repoRoot, input: agentPrompt })
|
|
159
|
+
if (res && res.exitCode === 0) {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(res.stdout)
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
141
168
|
const native = A.structuredOutput === true
|
|
142
|
-
const
|
|
169
|
+
const agentPrompt = cavemanActive ? `${prompt}\n\n${cavemanInstruction}` : prompt
|
|
170
|
+
const agentOptions = {
|
|
143
171
|
...options,
|
|
172
|
+
...(cavemanActive ? { skills: (options.skills || []).concat(cavemanState.skill_path || 'caveman'), caveman: true } : {}),
|
|
144
173
|
...(native ? { schema: await roleSchema(role) } : {}),
|
|
145
|
-
}
|
|
174
|
+
}
|
|
175
|
+
const result = await agent(agentPrompt, agentOptions)
|
|
146
176
|
if (await validRoleResult(role, result)) return result
|
|
147
|
-
const
|
|
148
|
-
|
|
177
|
+
const retryPrompt = `${agentPrompt}\nYour prior result was invalid. Return the complete ${role} result contract.`
|
|
178
|
+
const retryOptions = {
|
|
179
|
+
...agentOptions,
|
|
149
180
|
label: `${options.label}:retry`,
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
}
|
|
182
|
+
const retry = await agent(retryPrompt, retryOptions)
|
|
152
183
|
return (await validRoleResult(role, retry)) ? retry : null
|
|
153
184
|
}
|
|
154
185
|
|
|
@@ -282,14 +313,21 @@ and budgets as structured output.`
|
|
|
282
313
|
}
|
|
283
314
|
|
|
284
315
|
phase('Research')
|
|
316
|
+
const researchPrompt = (text) => (cavemanActive ? `${text}\n\n${cavemanInstruction}` : text)
|
|
317
|
+
const researchOptions = (opts) => ({
|
|
318
|
+
...opts,
|
|
319
|
+
phase: 'Research',
|
|
320
|
+
model: A.models.plan,
|
|
321
|
+
...(cavemanActive ? { skills: (opts.skills || []).concat(cavemanState.skill_path || 'caveman'), caveman: true } : {}),
|
|
322
|
+
})
|
|
285
323
|
const research = await parallel([
|
|
286
|
-
() => agent(`Survey ${A.repoRoot}: conventions, current code/tests, ${paths.context}, and ${paths.adrs}.
|
|
287
|
-
Return facts and paths for the Planner
|
|
288
|
-
() => agent(`Read ${t.specPath || paths.specPath}, ${paths.decisions}, and the relevant repository documents.
|
|
289
|
-
Return owned and consumed contracts, settled decisions, and the testing seam
|
|
290
|
-
() => agent(`Read ${paths.exemplarIssue} and ${paths.issueTracker}. Run
|
|
291
|
-
\`python3 ${scripts}/frontier.py --scope frontier --json\`. Return the exact Issue format and frontier facts
|
|
292
|
-
{ label: 'research:format'
|
|
324
|
+
() => agent(researchPrompt(`Survey ${A.repoRoot}: conventions, current code/tests, ${paths.context}, and ${paths.adrs}.
|
|
325
|
+
Return facts and paths for the Planner.`), researchOptions({ label: 'research:codebase' })),
|
|
326
|
+
() => agent(researchPrompt(`Read ${t.specPath || paths.specPath}, ${paths.decisions}, and the relevant repository documents.
|
|
327
|
+
Return owned and consumed contracts, settled decisions, and the testing seam.`), researchOptions({ label: 'research:spec' })),
|
|
328
|
+
() => agent(researchPrompt(`Read ${paths.exemplarIssue} and ${paths.issueTracker}. Run
|
|
329
|
+
\`python3 ${scripts}/frontier.py --scope frontier --json\`. Return the exact Issue format and frontier facts.`),
|
|
330
|
+
researchOptions({ label: 'research:format' })),
|
|
293
331
|
])
|
|
294
332
|
|
|
295
333
|
phase('Plan')
|
|
@@ -379,5 +417,5 @@ async function approvePlan() {
|
|
|
379
417
|
}
|
|
380
418
|
|
|
381
419
|
const approved = await approvePlan()
|
|
382
|
-
return { target: t, structuralValidation, requirementReview, plan, critique, awaitingOperatorApproval: !approved, approved }
|
|
420
|
+
return { target: t, structuralValidation, requirementReview, plan, critique, caveman: cavemanState, awaitingOperatorApproval: !approved, approved }
|
|
383
421
|
```
|
|
@@ -166,6 +166,16 @@ const budget = Number.isInteger(requestedBudget) && requestedBudget >= 0
|
|
|
166
166
|
const runLogEnabled = Boolean(A.runId && A.unitId)
|
|
167
167
|
const stateRootFlag = A.stateRoot ? ` --state-root '${String(A.stateRoot).replaceAll("'", "'\\''")}'` : ''
|
|
168
168
|
|
|
169
|
+
const cavemanState = A.caveman || { active: false, preference: false, scope: 'none' }
|
|
170
|
+
const cavemanActive = Boolean(cavemanState && cavemanState.active)
|
|
171
|
+
if (cavemanState && cavemanState.warning) {
|
|
172
|
+
if (typeof log === 'function') log(cavemanState.warning)
|
|
173
|
+
cavemanState.warned = true
|
|
174
|
+
}
|
|
175
|
+
const cavemanInstruction = cavemanActive
|
|
176
|
+
? 'Caveman lite is active: use concise phrasing for conversational messages and summaries. Code, documentation, lesson candidates, PR descriptions, Result Contracts, exact commands, exact errors, acceptance criteria, and verification evidence retain full detail.'
|
|
177
|
+
: ''
|
|
178
|
+
|
|
169
179
|
function canonical(value) {
|
|
170
180
|
if (Array.isArray(value)) return value.map(canonical)
|
|
171
181
|
if (value && typeof value === 'object') {
|
|
@@ -302,6 +312,104 @@ async function roleSchema(role) {
|
|
|
302
312
|
return JSON.parse(result.stdout)
|
|
303
313
|
}
|
|
304
314
|
|
|
315
|
+
function resolveRoleForIssue(issueRef, role, options) {
|
|
316
|
+
if (options && options.selection) return options.selection
|
|
317
|
+
if (issueRef && A.issueRoleReplacements && A.issueRoleReplacements[issueRef] && A.issueRoleReplacements[issueRef][role]) {
|
|
318
|
+
return A.issueRoleReplacements[issueRef][role]
|
|
319
|
+
}
|
|
320
|
+
if (issueRef && A.issueRoles && A.issueRoles[issueRef] && A.issueRoles[issueRef][role]) {
|
|
321
|
+
return A.issueRoles[issueRef][role]
|
|
322
|
+
}
|
|
323
|
+
const roleKey = role === 'implementer' ? 'implement' : role === 'reviewer' ? 'review' : role
|
|
324
|
+
if (A.roles && (A.roles[role] || A.roles[roleKey])) {
|
|
325
|
+
return A.roles[role] || A.roles[roleKey]
|
|
326
|
+
}
|
|
327
|
+
if (A.models && (A.models[role] || A.models[roleKey])) {
|
|
328
|
+
return { harness: A.hostHarness || 'claude-code', model: A.models[role] || A.models[roleKey] }
|
|
329
|
+
}
|
|
330
|
+
return null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function logRoleSelected(issueRef, role, selection) {
|
|
334
|
+
const isSelectionConfigured = Boolean(A.roles || A.issueRoles || A.issueRoleReplacements || A.recordRoleSelection)
|
|
335
|
+
if (!runLogEnabled || !selection || !issueRef || !isSelectionConfigured) return
|
|
336
|
+
await appendRunEvent('role.selected', issueRef, undefined, {
|
|
337
|
+
role,
|
|
338
|
+
issue: issueRef,
|
|
339
|
+
requested: selection,
|
|
340
|
+
effective: selection,
|
|
341
|
+
evidence: { source: 'role_selection' },
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Check for unresolved execution failures
|
|
346
|
+
let unresolvedFailures = Array.isArray(A.unresolvedFailures) ? [...A.unresolvedFailures] : []
|
|
347
|
+
if (unresolvedFailures.length === 0 && runLogEnabled && !isFirstRound) {
|
|
348
|
+
try {
|
|
349
|
+
const unresCheck = await runCommand(
|
|
350
|
+
`python3 "${scripts}/execution.py" unresolved-failures '${A.unitId}' '${A.runId}'${stateRootFlag} --json`,
|
|
351
|
+
{ cwd: A.repoRoot },
|
|
352
|
+
)
|
|
353
|
+
if (unresCheck && unresCheck.exitCode === 0) {
|
|
354
|
+
unresolvedFailures = JSON.parse(unresCheck.stdout)
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (unresolvedFailures.length > 0) {
|
|
360
|
+
const remainingUnresolved = []
|
|
361
|
+
for (const failure of unresolvedFailures) {
|
|
362
|
+
const replacement = resolveRoleForIssue(failure.issue, failure.role, null)
|
|
363
|
+
const hasReplacement = Boolean(
|
|
364
|
+
replacement &&
|
|
365
|
+
((A.issueRoles && A.issueRoles[failure.issue] && A.issueRoles[failure.issue][failure.role]) ||
|
|
366
|
+
(A.issueRoleReplacements && A.issueRoleReplacements[failure.issue] && A.issueRoleReplacements[failure.issue][failure.role]))
|
|
367
|
+
)
|
|
368
|
+
if (hasReplacement) {
|
|
369
|
+
const repJson = JSON.stringify(replacement)
|
|
370
|
+
const valCmd = `python3 "${scripts}/execution.py" validate-replacement --issue "${failure.issue}" --role "${failure.role}" --selection '${repJson.replaceAll("'", "'\\''")}' --cwd ${shellQuote(A.repoRoot)} --json`
|
|
371
|
+
const valRes = await runCommand(valCmd, { cwd: A.repoRoot })
|
|
372
|
+
let valid = false
|
|
373
|
+
if (valRes && valRes.exitCode === 0) {
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(valRes.stdout)
|
|
376
|
+
valid = parsed.valid === true
|
|
377
|
+
} catch (e) {}
|
|
378
|
+
}
|
|
379
|
+
if (!valid) {
|
|
380
|
+
return {
|
|
381
|
+
round: A.round,
|
|
382
|
+
blocked: true,
|
|
383
|
+
nextRoundBlocked: true,
|
|
384
|
+
reason: 'invalid_role_replacement',
|
|
385
|
+
error: `Invalid replacement selection for ${failure.issue} role ${failure.role}`,
|
|
386
|
+
results: [],
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await appendRunEvent('role.changed', failure.issue, undefined, {
|
|
390
|
+
role: failure.role,
|
|
391
|
+
issue: failure.issue,
|
|
392
|
+
previous: failure.selection || { harness: 'unknown' },
|
|
393
|
+
requested: replacement,
|
|
394
|
+
effective: replacement,
|
|
395
|
+
evidence: { source: 'explicit_recovery' },
|
|
396
|
+
})
|
|
397
|
+
} else {
|
|
398
|
+
remainingUnresolved.push(failure)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (remainingUnresolved.length > 0) {
|
|
402
|
+
return {
|
|
403
|
+
round: A.round,
|
|
404
|
+
blocked: true,
|
|
405
|
+
nextRoundBlocked: true,
|
|
406
|
+
reason: 'unresolved_execution_failure',
|
|
407
|
+
unresolvedFailures: remainingUnresolved,
|
|
408
|
+
results: [],
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
305
413
|
async function validRoleResult(role, result) {
|
|
306
414
|
if (!result) return false
|
|
307
415
|
if (A.structuredOutput === true) return true
|
|
@@ -312,18 +420,62 @@ async function validRoleResult(role, result) {
|
|
|
312
420
|
return Boolean(validation && validation.exitCode === 0)
|
|
313
421
|
}
|
|
314
422
|
async function requestRole(role, prompt, options) {
|
|
423
|
+
const issueRef = options && options.issueRef
|
|
424
|
+
const selection = resolveRoleForIssue(issueRef, role, options)
|
|
425
|
+
const hostHarness = A.hostHarness || 'claude-code'
|
|
426
|
+
if (options && options.executionUnavailable) {
|
|
427
|
+
return { executionUnavailable: true, error: options.error || 'Role execution unavailable' }
|
|
428
|
+
}
|
|
429
|
+
if (selection && selection.harness && selection.harness !== hostHarness) {
|
|
430
|
+
const cwd = (options && options.cwd) || A.repoRoot
|
|
431
|
+
const selectionJson = JSON.stringify(selection)
|
|
432
|
+
const agentPrompt = cavemanActive ? `${prompt}\n\n${cavemanInstruction}` : prompt
|
|
433
|
+
const cmd = `python3 "${scripts}/execution.py" dispatch --role "${role}" --cwd ${shellQuote(cwd)} --selection '${selectionJson.replaceAll("'", "'\\''")}'`
|
|
434
|
+
const res = await runCommand(cmd, { cwd: A.repoRoot, input: agentPrompt })
|
|
435
|
+
if (res && res.exitCode === 0) {
|
|
436
|
+
try {
|
|
437
|
+
return JSON.parse(res.stdout)
|
|
438
|
+
} catch (e) {
|
|
439
|
+
return null
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (res && res.exitCode === 2) {
|
|
443
|
+
return null
|
|
444
|
+
}
|
|
445
|
+
return { executionUnavailable: true, error: (res && res.stderr) || 'Harness execution failed' }
|
|
446
|
+
}
|
|
315
447
|
const native = A.structuredOutput === true
|
|
316
448
|
const schema = native ? await roleSchema(role) : null
|
|
317
|
-
const
|
|
449
|
+
const agentPrompt = cavemanActive ? `${prompt}\n\n${cavemanInstruction}` : prompt
|
|
450
|
+
const agentOptions = {
|
|
318
451
|
...options,
|
|
452
|
+
...(cavemanActive ? { skills: (options.skills || []).concat(cavemanState.skill_path || 'caveman'), caveman: true } : {}),
|
|
319
453
|
...(schema ? { schema } : {}),
|
|
320
|
-
}
|
|
454
|
+
}
|
|
455
|
+
let result
|
|
456
|
+
try {
|
|
457
|
+
result = await agent(agentPrompt, agentOptions)
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return { executionUnavailable: true, error: String(err && err.message ? err.message : err) }
|
|
460
|
+
}
|
|
461
|
+
if (result && result.executionUnavailable) {
|
|
462
|
+
return { executionUnavailable: true, error: result.error || 'Execution unavailable' }
|
|
463
|
+
}
|
|
321
464
|
if (await validRoleResult(role, result)) return result
|
|
322
|
-
const
|
|
323
|
-
|
|
465
|
+
const retryPrompt = `${agentPrompt}\nYour prior result was invalid. Return the complete ${role} result contract.`
|
|
466
|
+
const retryOptions = {
|
|
467
|
+
...agentOptions,
|
|
324
468
|
label: `${options.label}:retry`,
|
|
325
|
-
|
|
326
|
-
|
|
469
|
+
}
|
|
470
|
+
let retry
|
|
471
|
+
try {
|
|
472
|
+
retry = await agent(retryPrompt, retryOptions)
|
|
473
|
+
} catch (err) {
|
|
474
|
+
return { executionUnavailable: true, error: String(err && err.message ? err.message : err) }
|
|
475
|
+
}
|
|
476
|
+
if (retry && retry.executionUnavailable) {
|
|
477
|
+
return { executionUnavailable: true, error: retry.error || 'Execution unavailable' }
|
|
478
|
+
}
|
|
327
479
|
return (await validRoleResult(role, retry)) ? retry : null
|
|
328
480
|
}
|
|
329
481
|
|
|
@@ -500,13 +652,20 @@ refutations, gateResult, gateFailures and decisionsForOperator as structured out
|
|
|
500
652
|
async function implement(issue, feedback, previous) {
|
|
501
653
|
const assigned = await implementationLocation(issue, previous)
|
|
502
654
|
if (!assigned) return null
|
|
655
|
+
const selection = resolveRoleForIssue(issue.ref, 'implementer', null)
|
|
656
|
+
await logRoleSelected(issue.ref, 'implementer', selection)
|
|
503
657
|
const options = {
|
|
504
658
|
label: `implement:${issue.ref}`, phase: 'Implement', model: A.models.implement, cwd: assigned.worktree,
|
|
659
|
+
issueRef: issue.ref, selection,
|
|
660
|
+
...(A.issueExecutionUnavailable && A.issueExecutionUnavailable[issue.ref] === 'implementer' ? { executionUnavailable: true } : {}),
|
|
505
661
|
}
|
|
506
662
|
await markRunIn(assigned.worktree)
|
|
507
663
|
await appendRunEvent('phase.started', issue.ref, 'Implement', { worktree: assigned.worktree })
|
|
508
664
|
await appendRunEvent('subagent.started', issue.ref, 'Implement', { role: 'implementer' })
|
|
509
665
|
const result = await requestRole('implementer', implementPrompt(issue, feedback, assigned), options)
|
|
666
|
+
if (result && result.executionUnavailable) {
|
|
667
|
+
return { executionUnavailable: true, role: 'implementer', error: result.error, worktree: assigned.worktree, branch: assigned.branch }
|
|
668
|
+
}
|
|
510
669
|
await appendRunEvent('subagent.stopped', issue.ref, 'Implement', { role: 'implementer', result })
|
|
511
670
|
await appendRunEvent('phase.finished', issue.ref, 'Implement', { worktree: assigned.worktree })
|
|
512
671
|
return result && result.worktree === assigned.worktree && result.branch === assigned.branch ? result : null
|
|
@@ -517,11 +676,19 @@ const results = await pipeline(
|
|
|
517
676
|
issue => implement(issue, null, null),
|
|
518
677
|
async (impl, issue) => {
|
|
519
678
|
if (!impl) return null
|
|
679
|
+
if (impl.executionUnavailable) return impl
|
|
680
|
+
const selection = resolveRoleForIssue(issue.ref, 'reviewer', null)
|
|
681
|
+
await logRoleSelected(issue.ref, 'reviewer', selection)
|
|
520
682
|
await appendRunEvent('phase.started', issue.ref, 'Review', { worktree: location(impl) })
|
|
521
683
|
await appendRunEvent('subagent.started', issue.ref, 'Review', { role: 'reviewer' })
|
|
522
684
|
const review = await requestRole('reviewer', reviewPrompt(issue, impl), {
|
|
523
685
|
label: `review:${issue.ref}`, phase: 'Review', model: A.models.review, cwd: location(impl),
|
|
686
|
+
issueRef: issue.ref, selection,
|
|
687
|
+
...(A.issueExecutionUnavailable && A.issueExecutionUnavailable[issue.ref] === 'reviewer' ? { executionUnavailable: true } : {}),
|
|
524
688
|
})
|
|
689
|
+
if (review && review.executionUnavailable) {
|
|
690
|
+
return { executionUnavailable: true, role: 'reviewer', error: review.error, impl }
|
|
691
|
+
}
|
|
525
692
|
await appendRunEvent('subagent.stopped', issue.ref, 'Review', { role: 'reviewer', result: review })
|
|
526
693
|
await appendRunEvent('phase.finished', issue.ref, 'Review', {})
|
|
527
694
|
if (review) {
|
|
@@ -536,10 +703,42 @@ const results = await pipeline(
|
|
|
536
703
|
if (!reviewed) {
|
|
537
704
|
return { impl: null, implementerFailed: true, failedImpl: impl, review, reviewFix: true }
|
|
538
705
|
}
|
|
706
|
+
if (reviewed.executionUnavailable) return reviewed
|
|
539
707
|
return { impl: reviewed, review, reviewFix: Boolean(review && review.blocking.length) }
|
|
540
708
|
},
|
|
541
709
|
async (state, issue) => {
|
|
542
710
|
if (!state) return { ref: issue.ref, outcome: 'implementer_failed' }
|
|
711
|
+
if (state.executionUnavailable) {
|
|
712
|
+
const impl = state.impl
|
|
713
|
+
const worktree = (impl && impl.worktree) || state.worktree || issueWorktreePath(issue)
|
|
714
|
+
const branch = (impl && impl.branch) || state.branch || await configuredIssueBranch(issue)
|
|
715
|
+
const corrections = state.corrections || 0
|
|
716
|
+
await appendRunEvent('issue.paused', issue.ref, state.role === 'reviewer' ? 'Review' : state.role === 'implementer' ? 'Implement' : 'Critic', {
|
|
717
|
+
role: state.role || 'critic',
|
|
718
|
+
reason: 'execution_unavailable',
|
|
719
|
+
worktree,
|
|
720
|
+
error: state.error || 'Role execution unavailable',
|
|
721
|
+
})
|
|
722
|
+
return {
|
|
723
|
+
ref: issue.ref,
|
|
724
|
+
issuePath: issue.path,
|
|
725
|
+
outcome: 'paused',
|
|
726
|
+
status: 'paused',
|
|
727
|
+
reason: 'execution_unavailable',
|
|
728
|
+
executionUnavailable: true,
|
|
729
|
+
role: state.role || 'critic',
|
|
730
|
+
error: state.error || 'Role execution unavailable',
|
|
731
|
+
worktree,
|
|
732
|
+
branch,
|
|
733
|
+
commits: (impl && impl.commits) || [],
|
|
734
|
+
corrections,
|
|
735
|
+
reviewFix: state.reviewFix,
|
|
736
|
+
review: state.review,
|
|
737
|
+
verdict: null,
|
|
738
|
+
decisions: (impl && impl.decisions) || [],
|
|
739
|
+
blockers: (impl && impl.blockers) || [],
|
|
740
|
+
}
|
|
741
|
+
}
|
|
543
742
|
if (state.implementerFailed) {
|
|
544
743
|
return {
|
|
545
744
|
ref: issue.ref, outcome: 'implementer_failed',
|
|
@@ -557,11 +756,42 @@ const results = await pipeline(
|
|
|
557
756
|
let corrections = prior && Number.isInteger(prior.correctionsSpent) ? prior.correctionsSpent : 0
|
|
558
757
|
let accepted = false
|
|
559
758
|
for (let attempt = 1; ; attempt += 1) {
|
|
759
|
+
const selection = resolveRoleForIssue(issue.ref, 'critic', null)
|
|
760
|
+
await logRoleSelected(issue.ref, 'critic', selection)
|
|
560
761
|
await appendRunEvent('phase.started', issue.ref, 'Critic', { attempt, worktree: location(impl) })
|
|
561
762
|
await appendRunEvent('subagent.started', issue.ref, 'Critic', { role: 'critic', attempt })
|
|
562
763
|
verdict = await requestRole('critic', criticPrompt(issue, impl, state.review, attempt), {
|
|
563
764
|
label: `critic:${issue.ref}#${attempt}`, phase: 'Critic', model: A.models.critic, cwd: location(impl),
|
|
765
|
+
issueRef: issue.ref, selection,
|
|
766
|
+
...(A.issueExecutionUnavailable && A.issueExecutionUnavailable[issue.ref] === 'critic' ? { executionUnavailable: true } : {}),
|
|
564
767
|
})
|
|
768
|
+
if (verdict && verdict.executionUnavailable) {
|
|
769
|
+
await appendRunEvent('issue.paused', issue.ref, 'Critic', {
|
|
770
|
+
role: 'critic',
|
|
771
|
+
reason: 'execution_unavailable',
|
|
772
|
+
worktree: location(impl),
|
|
773
|
+
error: verdict.error || 'Critic execution unavailable',
|
|
774
|
+
})
|
|
775
|
+
return {
|
|
776
|
+
ref: issue.ref,
|
|
777
|
+
issuePath: issue.path,
|
|
778
|
+
outcome: 'paused',
|
|
779
|
+
status: 'paused',
|
|
780
|
+
reason: 'execution_unavailable',
|
|
781
|
+
executionUnavailable: true,
|
|
782
|
+
role: 'critic',
|
|
783
|
+
error: verdict.error || 'Critic execution unavailable',
|
|
784
|
+
worktree: impl.worktree,
|
|
785
|
+
branch: impl.branch,
|
|
786
|
+
commits: impl.commits,
|
|
787
|
+
corrections,
|
|
788
|
+
reviewFix: state.reviewFix,
|
|
789
|
+
review: state.review,
|
|
790
|
+
verdict: null,
|
|
791
|
+
decisions: (impl.decisions) || [],
|
|
792
|
+
blockers: (impl.blockers) || [],
|
|
793
|
+
}
|
|
794
|
+
}
|
|
565
795
|
await appendRunEvent('subagent.stopped', issue.ref, 'Critic', { role: 'critic', attempt, result: projectCriticResult(verdict) })
|
|
566
796
|
await appendRunEvent('phase.finished', issue.ref, 'Critic', { attempt })
|
|
567
797
|
if (!verdict) {
|
|
@@ -589,6 +819,33 @@ const results = await pipeline(
|
|
|
589
819
|
blockers: impl.blockers || [],
|
|
590
820
|
}
|
|
591
821
|
}
|
|
822
|
+
if (corrected.executionUnavailable) {
|
|
823
|
+
await appendRunEvent('issue.paused', issue.ref, 'Implement', {
|
|
824
|
+
role: 'implementer',
|
|
825
|
+
reason: 'execution_unavailable',
|
|
826
|
+
worktree: location(corrected),
|
|
827
|
+
error: corrected.error || 'Implementer execution unavailable',
|
|
828
|
+
})
|
|
829
|
+
return {
|
|
830
|
+
ref: issue.ref,
|
|
831
|
+
issuePath: issue.path,
|
|
832
|
+
outcome: 'paused',
|
|
833
|
+
status: 'paused',
|
|
834
|
+
reason: 'execution_unavailable',
|
|
835
|
+
executionUnavailable: true,
|
|
836
|
+
role: 'implementer',
|
|
837
|
+
error: corrected.error || 'Implementer execution unavailable',
|
|
838
|
+
worktree: corrected.worktree || impl.worktree,
|
|
839
|
+
branch: corrected.branch || impl.branch,
|
|
840
|
+
commits: impl.commits,
|
|
841
|
+
corrections,
|
|
842
|
+
reviewFix: state.reviewFix,
|
|
843
|
+
review: state.review,
|
|
844
|
+
verdict,
|
|
845
|
+
decisions: [...(impl.decisions || []), ...(verdict.decisionsForOperator || [])],
|
|
846
|
+
blockers: impl.blockers || [],
|
|
847
|
+
}
|
|
848
|
+
}
|
|
592
849
|
corrections += 1
|
|
593
850
|
impl = corrected
|
|
594
851
|
}
|
|
@@ -693,15 +950,16 @@ if (integrationStopped) {
|
|
|
693
950
|
round: A.round, reason: cancelReason && cancelReason.reason,
|
|
694
951
|
})
|
|
695
952
|
}
|
|
696
|
-
const
|
|
697
|
-
|
|
953
|
+
const hasPaused = deliveries.some(d => d.outcome === 'paused')
|
|
954
|
+
const candidates = (A.isLastRound && !hasPaused) ? await learn() : []
|
|
955
|
+
if (!integrationStopped && A.isLastRound && !hasPaused) {
|
|
698
956
|
await appendRunEvent('run.finished', undefined, undefined, { round: A.round })
|
|
699
957
|
}
|
|
700
|
-
if (integrationStopped || A.isLastRound) {
|
|
958
|
+
if (integrationStopped || (A.isLastRound && !hasPaused)) {
|
|
701
959
|
await unmarkRun()
|
|
702
960
|
}
|
|
703
961
|
let prOffer = null
|
|
704
|
-
if (!integrationStopped && A.isLastRound) {
|
|
962
|
+
if (!integrationStopped && A.isLastRound && !hasPaused) {
|
|
705
963
|
const completed = deliveries.filter(d => d.outcome === 'done')
|
|
706
964
|
if (completed.length > 0) {
|
|
707
965
|
const ghCheck = await runCommand('gh --version', { cwd: A.repoRoot })
|
|
@@ -747,7 +1005,15 @@ if (!integrationStopped && A.isLastRound) {
|
|
|
747
1005
|
}
|
|
748
1006
|
}
|
|
749
1007
|
}
|
|
750
|
-
return {
|
|
1008
|
+
return {
|
|
1009
|
+
round: A.round,
|
|
1010
|
+
date: A.date,
|
|
1011
|
+
results: deliveries,
|
|
1012
|
+
caveman: cavemanState,
|
|
1013
|
+
nextRoundBlocked: hasPaused || integrationStopped,
|
|
1014
|
+
...(hasPaused ? { pausedIssues: deliveries.filter(d => d.outcome === 'paused') } : {}),
|
|
1015
|
+
...(A.isLastRound && !hasPaused ? { candidates, prOffer } : {}),
|
|
1016
|
+
}
|
|
751
1017
|
```
|
|
752
1018
|
|
|
753
1019
|
The Workflow returns `done` only after serial integration (when isolated), a passing post-integration
|
|
@@ -76,15 +76,56 @@ def declared_models(capabilities_dir: Path) -> dict[str, tuple[str, int]]:
|
|
|
76
76
|
return models
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def
|
|
79
|
+
def resolve_model_metadata(
|
|
80
|
+
model: str,
|
|
81
|
+
capabilities_dir: Path,
|
|
82
|
+
catalog_path: Path | None = None,
|
|
83
|
+
) -> tuple[str, int]:
|
|
84
|
+
"""Resolve model harness and context window from capabilities or discovered catalog."""
|
|
85
|
+
models = declared_models(capabilities_dir)
|
|
86
|
+
if model in models:
|
|
87
|
+
return models[model]
|
|
88
|
+
|
|
89
|
+
if catalog_path and catalog_path.is_file():
|
|
90
|
+
try:
|
|
91
|
+
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
|
|
92
|
+
except json.JSONDecodeError as exc:
|
|
93
|
+
raise ConfigurationError(f"invalid catalog JSON in {catalog_path}: {exc}") from exc
|
|
94
|
+
|
|
95
|
+
raw_models = catalog.get("models", {})
|
|
96
|
+
details = None
|
|
97
|
+
harness = catalog.get("harness", "discovered")
|
|
98
|
+
if isinstance(raw_models, dict) and model in raw_models:
|
|
99
|
+
details = raw_models[model]
|
|
100
|
+
elif isinstance(raw_models, list):
|
|
101
|
+
for m in raw_models:
|
|
102
|
+
if isinstance(m, dict) and m.get("id") == model:
|
|
103
|
+
details = m
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if details is not None:
|
|
107
|
+
window = details.get("contextWindow")
|
|
108
|
+
if not isinstance(window, int) or isinstance(window, bool) or window <= 0:
|
|
109
|
+
raise ConfigurationError(f"missing verified context metadata for model {model!r}")
|
|
110
|
+
harness = details.get("harness", harness)
|
|
111
|
+
return (harness, window)
|
|
112
|
+
|
|
113
|
+
raise ConfigurationError(f"unknown model ID: {model!r}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def estimate(
|
|
117
|
+
issue_path: Path,
|
|
118
|
+
model: str,
|
|
119
|
+
root: Path,
|
|
120
|
+
capabilities_dir: Path,
|
|
121
|
+
catalog_path: Path | None = None,
|
|
122
|
+
) -> dict:
|
|
80
123
|
"""Build the deterministic initial-package estimate for one Issue."""
|
|
81
124
|
issue = parse_issue(issue_path)
|
|
82
125
|
spec_path = artifact_path(root, "specs", issue.spec).resolve()
|
|
83
126
|
if not spec_path.is_file():
|
|
84
127
|
raise ConfigurationError(f"parent Spec does not exist: {spec_path.relative_to(root)}")
|
|
85
|
-
|
|
86
|
-
if model not in models:
|
|
87
|
-
raise ConfigurationError(f"unknown model ID: {model!r}")
|
|
128
|
+
harness, window = resolve_model_metadata(model, capabilities_dir, catalog_path)
|
|
88
129
|
policy = resolve_policy(root)
|
|
89
130
|
share = policy.get("budget", {}).get("contextShare")
|
|
90
131
|
if not isinstance(share, (int, float)) or isinstance(share, bool) or not 0 < share <= 1:
|
|
@@ -101,7 +142,6 @@ def estimate(issue_path: Path, model: str, root: Path, capabilities_dir: Path) -
|
|
|
101
142
|
for path in ordered
|
|
102
143
|
]
|
|
103
144
|
byte_count = sum(entry["bytes"] for entry in files)
|
|
104
|
-
harness, window = models[model]
|
|
105
145
|
estimated_tokens = byte_count / 4
|
|
106
146
|
budget_tokens = window * share
|
|
107
147
|
over_budget = estimated_tokens > budget_tokens
|
|
@@ -138,7 +178,8 @@ def emit(payload: dict, as_json: bool) -> None:
|
|
|
138
178
|
def main() -> int:
|
|
139
179
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
140
180
|
parser.add_argument("issue", help="Issue path")
|
|
141
|
-
parser.add_argument("--model", required=True, help="exact model ID declared by a capability")
|
|
181
|
+
parser.add_argument("--model", required=True, help="exact model ID declared by a capability or catalog")
|
|
182
|
+
parser.add_argument("--catalog", help="path to discovered model catalog JSON")
|
|
142
183
|
parser.add_argument("--cwd", default=".", help="repository or worktree to inspect")
|
|
143
184
|
parser.add_argument("--json", action="store_true", help="emit machine-readable output")
|
|
144
185
|
args = parser.parse_args()
|
|
@@ -149,8 +190,9 @@ def main() -> int:
|
|
|
149
190
|
emit({"error": f"configuration error: Issue does not exist: {issue_path}"}, args.json)
|
|
150
191
|
return 1
|
|
151
192
|
capabilities = Path(__file__).resolve().parents[1] / "capabilities"
|
|
193
|
+
catalog_path = Path(args.catalog).resolve() if args.catalog else None
|
|
152
194
|
try:
|
|
153
|
-
payload = estimate(issue_path, args.model, root, capabilities)
|
|
195
|
+
payload = estimate(issue_path, args.model, root, capabilities, catalog_path=catalog_path)
|
|
154
196
|
except (ConfigurationError, ValueError) as exc:
|
|
155
197
|
emit({"error": f"configuration error: {exc}"}, args.json)
|
|
156
198
|
return 1
|