leos-agent 7.0.0 → 10.2.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/README.md +544 -28
- package/commands/handoff.md +11 -0
- package/commands/handon.md +10 -0
- package/commands/review-pr.md +9 -0
- package/commands-claude/watch-review.md +9 -0
- package/index.js +12 -0
- package/package.json +29 -14
- package/payload/codex-agents/leo-executor.toml +36 -0
- package/payload/codex-agents/leo-runner.toml +28 -0
- package/rules/preferences.md +97 -0
- package/scripts/check.py +244 -0
- package/scripts/ghreview.py +17 -3
- package/scripts/handoff.py +183 -0
- package/scripts/leo-install.py +509 -0
- package/scripts/measure_context.py +113 -0
- package/scripts/publish-npm.py +138 -0
- package/scripts/watch_review.py +169 -0
- package/skills/doctor/SKILL.md +73 -115
- package/skills/doctor/agents/openai.yaml +5 -0
- package/skills/handoff/SKILL.md +99 -0
- package/skills/handoff/agents/openai.yaml +5 -0
- package/skills/handon/SKILL.md +61 -0
- package/skills/install/SKILL.md +79 -0
- package/skills/install/agents/openai.yaml +5 -0
- package/skills/review-pr/SKILL.md +59 -318
- package/skills/review-pr/reference/lenses.md +67 -0
- package/skills/review-pr/reference/procedure.md +348 -0
- package/skills-claude/attach-pr/SKILL.md +178 -0
- package/skills-claude/watch-review/SKILL.md +91 -0
- package/adapters/cursor/agents/executor.md +0 -17
- package/adapters/cursor/agents/expert.md +0 -70
- package/adapters/cursor/agents/explore.md +0 -16
- package/adapters/cursor/agents/implementer.md +0 -18
- package/adapters/cursor/agents/investigator.md +0 -18
- package/adapters/cursor/agents/planner.md +0 -28
- package/adapters/cursor/agents/review-lens.md +0 -22
- package/adapters/cursor/agents/reviewer.md +0 -34
- package/adapters/opencode/agents.json +0 -105
- package/adapters/opencode/plugin.js +0 -576
- package/config/MCP_PINS.md +0 -17
- package/config/models.json +0 -676
- package/hooks/bash-guard.py +0 -583
- package/hooks/cursor-guard.py +0 -84
- package/hooks/hooks-cursor.json +0 -11
- package/hooks/hooks.json +0 -20
- package/hooks/session-start.py +0 -148
- package/roles/executor.md +0 -15
- package/roles/expert.md +0 -67
- package/roles/explore.md +0 -13
- package/roles/implementer.md +0 -16
- package/roles/investigator.md +0 -15
- package/roles/planner.md +0 -25
- package/roles/review-lens.md +0 -20
- package/roles/reviewer.md +0 -31
- package/scripts/doctor.py +0 -520
- package/scripts/jsonc_bridge.cjs +0 -23
- package/scripts/memory.py +0 -744
- package/scripts/render_adapters.py +0 -508
- package/scripts/setup.py +0 -1753
- package/settings.json +0 -7
- package/skills/.gitkeep +0 -0
- package/skills/brainstorming/SKILL.md +0 -111
- package/skills/debugging/SKILL.md +0 -100
- package/skills/delegation/SKILL.md +0 -143
- package/skills/executing-plans/SKILL.md +0 -117
- package/skills/finishing-a-branch/SKILL.md +0 -125
- package/skills/freshness/SKILL.md +0 -131
- package/skills/memory/SKILL.md +0 -154
- package/skills/resolve-ticket/SKILL.md +0 -275
- package/skills/setup/SKILL.md +0 -199
- package/skills/setup/agents/openai.yaml +0 -5
- package/skills/test-first/SKILL.md +0 -92
- package/skills/using-leo/SKILL.md +0 -101
- package/skills/using-leo/references/claude-mapping.md +0 -33
- package/skills/using-leo/references/codex-mapping.md +0 -33
- package/skills/using-leo/references/cursor-mapping.md +0 -35
- package/skills/using-leo/references/hermes-mapping.md +0 -37
- package/skills/using-leo/references/opencode-mapping.md +0 -39
- package/skills/verification/SKILL.md +0 -110
- package/skills/visual-verification/SKILL.md +0 -115
- package/skills/watch-review/SKILL.md +0 -128
- package/skills/watch-review/agents/openai.yaml +0 -5
- package/skills/worktrees/SKILL.md +0 -131
- package/skills/writing-plans/SKILL.md +0 -97
- package/skills/writing-skills/SKILL.md +0 -141
- package/vendor/jsonc-parser-3.3.1/LICENSE.md +0 -21
- package/vendor/jsonc-parser-3.3.1/README.md +0 -26
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +0 -201
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +0 -275
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +0 -682
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +0 -456
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +0 -42
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +0 -351
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +0 -194
- package/vendor/jsonc-parser-3.3.1/package.json +0 -37
- package/workflows/cost-tiered-fix.js +0 -287
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
name: 'cost-tiered-fix',
|
|
3
|
-
description: 'Fix a batch of independent tasks with tiered models: Opus plans and verifies, Haiku/Sonnet execute, low-confidence items escalate to Opus',
|
|
4
|
-
whenToUse: 'A list of independent, well-scoped fixes (many tickets, many files) — NOT one large stateful change, which belongs in a normal session with subagents',
|
|
5
|
-
phases: [
|
|
6
|
-
{ title: 'Plan', detail: 'decompose the goal into tiered work items', model: 'opus' },
|
|
7
|
-
{ title: 'Execute', detail: 'cheap executors, one isolated worktree per item' },
|
|
8
|
-
{ title: 'Verify', detail: 'Opus reviews each branch diff', model: 'opus' },
|
|
9
|
-
],
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Invoke with either:
|
|
13
|
-
// args: { goal: "...", runId?: "..." } -> Opus plans the decomposition
|
|
14
|
-
// args: { tasks: ["...", { task, tier }], runId?: "..." } -> skip planning, run your list
|
|
15
|
-
// runId (e.g. a ticket id or date string) namespaces branch names across runs;
|
|
16
|
-
// Date.now()/Math.random() are unavailable in workflow scripts, so it must come
|
|
17
|
-
// from the caller. Without it, executors resolve collisions by numeric suffix.
|
|
18
|
-
// Each work item ends up as a committed branch plus an Opus verdict.
|
|
19
|
-
// Merging approved branches is left to the main session.
|
|
20
|
-
//
|
|
21
|
-
// args.tiers optionally remaps the three rungs, e.g.
|
|
22
|
-
// { tiers: { cheap: 'haiku', normal: 'sonnet', judge: 'opus' } }
|
|
23
|
-
// Workflow scripts have no filesystem access, so the canonical matrix in
|
|
24
|
-
// config/models.json cannot be read here — the caller passes it through when
|
|
25
|
-
// this machine's mapping differs from the defaults below.
|
|
26
|
-
|
|
27
|
-
if (!args || (!args.goal && !Array.isArray(args.tasks))) {
|
|
28
|
-
throw new Error('cost-tiered-fix needs args: { goal: "..." } or { tasks: [...] }')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/
|
|
32
|
-
if (args.runId !== undefined && (typeof args.runId !== 'string' || !RUN_ID_RE.test(args.runId))) {
|
|
33
|
-
throw new Error('cost-tiered-fix: runId must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$')
|
|
34
|
-
}
|
|
35
|
-
const BRANCH_PREFIX = args.runId ? `leos/fix-${args.runId}` : 'leos/fix'
|
|
36
|
-
|
|
37
|
-
// Bare model aliases only — this is the documented subagent `model:` shape.
|
|
38
|
-
// The 1m-extended-context suffix (square-bracket /model syntax) is a
|
|
39
|
-
// /model-command / SKILL-frontmatter thing, not a valid subagent model
|
|
40
|
-
// value; passing it here reaches the model selector verbatim and kills the
|
|
41
|
-
// spawn (see the outage this repo just had). Do not add that suffix back.
|
|
42
|
-
const TIERS = {
|
|
43
|
-
cheap: 'haiku',
|
|
44
|
-
normal: 'sonnet',
|
|
45
|
-
judge: 'opus',
|
|
46
|
-
...(args.tiers || {}),
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const PLAN_SCHEMA = {
|
|
50
|
-
type: 'object',
|
|
51
|
-
properties: {
|
|
52
|
-
items: {
|
|
53
|
-
type: 'array',
|
|
54
|
-
items: {
|
|
55
|
-
type: 'object',
|
|
56
|
-
properties: {
|
|
57
|
-
task: { type: 'string', description: 'self-contained instruction: exact file paths, expected behavior, how to check it' },
|
|
58
|
-
tier: { type: 'string', enum: [TIERS.cheap, TIERS.normal], description: `${TIERS.cheap} for mechanical work, ${TIERS.normal} for normal implementation` },
|
|
59
|
-
},
|
|
60
|
-
required: ['task', 'tier'],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
required: ['items'],
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const EXEC_SCHEMA = {
|
|
68
|
-
type: 'object',
|
|
69
|
-
properties: {
|
|
70
|
-
branch: { type: 'string', description: 'the branch actually created and committed to; omit if no branch was created' },
|
|
71
|
-
summary: { type: 'string' },
|
|
72
|
-
checks: { type: 'string', description: 'what was run to verify, and the result' },
|
|
73
|
-
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
74
|
-
},
|
|
75
|
-
required: ['summary', 'confidence'],
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const VERDICT_SCHEMA = {
|
|
79
|
-
type: 'object',
|
|
80
|
-
properties: {
|
|
81
|
-
approved: { type: 'boolean' },
|
|
82
|
-
issues: { type: 'array', items: { type: 'string' } },
|
|
83
|
-
},
|
|
84
|
-
required: ['approved', 'issues'],
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isGeneratedBranch(branch, expected) {
|
|
88
|
-
return typeof branch === 'string' && (branch === expected || new RegExp(`^${expected.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-[1-9]\\d*$`).test(branch))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function boundExecutorOutput(untrusted, expectedBranch) {
|
|
92
|
-
// Agent output is untrusted: keep only a bounded, typed report and never
|
|
93
|
-
// let it point verification at an arbitrary refs/heads/* branch.
|
|
94
|
-
if (!untrusted || typeof untrusted !== 'object') return null
|
|
95
|
-
const result = {
|
|
96
|
-
summary: typeof untrusted.summary === 'string' ? untrusted.summary.slice(0, 2000) : 'untrusted executor output omitted summary',
|
|
97
|
-
checks: typeof untrusted.checks === 'string' ? untrusted.checks.slice(0, 2000) : undefined,
|
|
98
|
-
confidence: ['high', 'medium', 'low'].includes(untrusted.confidence) ? untrusted.confidence : 'low',
|
|
99
|
-
}
|
|
100
|
-
if (untrusted.branch !== undefined) {
|
|
101
|
-
if (!isGeneratedBranch(untrusted.branch, expectedBranch)) {
|
|
102
|
-
result.summary = `${result.summary}\n[untrusted executor output: rejected unexpected branch]`.slice(0, 2000)
|
|
103
|
-
result.confidence = 'low'
|
|
104
|
-
} else {
|
|
105
|
-
result.branch = untrusted.branch
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return result
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function execPrompt(task, branch) {
|
|
112
|
-
return [
|
|
113
|
-
'You are one executor in a fan-out. Work ONLY on this task; touch nothing else.',
|
|
114
|
-
`Task: ${task}`,
|
|
115
|
-
`You are in an isolated git worktree. Create and switch to branch ${branch} (if that name already exists, use the lowest free numeric suffix, e.g. ${branch}-2), implement the task, run the narrowest relevant check (tests/build for the touched files), and commit.`,
|
|
116
|
-
'Report the branch name you actually used. If the task is ambiguous or you cannot make the check pass, commit only what is safe and report confidence: low with the blocker in summary. If you created no branch, omit the branch field entirely — never invent one.',
|
|
117
|
-
].join('\n')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Next tier up the escalation ladder. The judge tier is the ceiling: it has
|
|
121
|
-
// nowhere left to escalate to, so it maps to itself.
|
|
122
|
-
function nextTier(tier) {
|
|
123
|
-
if (tier === TIERS.cheap) return TIERS.normal
|
|
124
|
-
return TIERS.judge
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Escalation must buy more than a model swap: the normal rung gets a wider
|
|
128
|
-
// reasoning budget than the cheap rung it is replacing, or a cheap->normal
|
|
129
|
-
// escalation would spend more money for the same effort that already failed.
|
|
130
|
-
function effortFor(tier) {
|
|
131
|
-
return tier === TIERS.judge ? 'high' : tier === TIERS.normal ? 'medium' : 'low'
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Caller-supplied args.tasks bypasses the planning agent (and PLAN_SCHEMA's
|
|
135
|
-
// validation with it), so entries need their own gate here: a bad `tier`
|
|
136
|
-
// would flow straight into `model:` below, and a non-object/non-string entry
|
|
137
|
-
// (e.g. null) would silently produce `task: undefined` fed to execPrompt.
|
|
138
|
-
const ALLOWED_TIERS = new Set([TIERS.cheap, TIERS.normal])
|
|
139
|
-
function validateTask(t, i) {
|
|
140
|
-
if (typeof t === 'string') return { task: t, tier: TIERS.normal }
|
|
141
|
-
if (t && typeof t === 'object' && typeof t.task === 'string') {
|
|
142
|
-
if (t.tier !== undefined && !ALLOWED_TIERS.has(t.tier)) {
|
|
143
|
-
throw new Error(`cost-tiered-fix: args.tasks[${i}].tier must be one of ${[...ALLOWED_TIERS].join(', ')}, got ${JSON.stringify(t.tier)}`)
|
|
144
|
-
}
|
|
145
|
-
return { tier: TIERS.normal, ...t }
|
|
146
|
-
}
|
|
147
|
-
throw new Error(`cost-tiered-fix: args.tasks[${i}] must be a string or an object with a string "task", got ${JSON.stringify(t)}`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
phase('Plan')
|
|
151
|
-
let items
|
|
152
|
-
if (Array.isArray(args.tasks)) {
|
|
153
|
-
items = args.tasks.map(validateTask)
|
|
154
|
-
log(`Using ${items.length} caller-provided tasks (planning skipped)`)
|
|
155
|
-
} else {
|
|
156
|
-
const plan = await agent(
|
|
157
|
-
`Decompose this goal into independent, well-scoped work items that can each be done in an isolated worktree without touching the same files. For each item write a self-contained instruction (exact file paths, expected behavior, how to check it) and pick a tier: ${TIERS.cheap} for mechanical work, ${TIERS.normal} for normal implementation. At most 10 items — if the goal needs more, return the 10 highest-value and say so in the last item.\n\nGoal: ` + args.goal,
|
|
158
|
-
{ label: 'plan', phase: 'Plan', model: TIERS.judge, effort: 'high', schema: PLAN_SCHEMA },
|
|
159
|
-
)
|
|
160
|
-
if (!plan || !Array.isArray(plan.items) || plan.items.length === 0) {
|
|
161
|
-
log('Planning agent failed or returned no items — aborting cleanly')
|
|
162
|
-
return { approved: [], rejected: [], note: 'planning agent died or produced no work items; nothing was run' }
|
|
163
|
-
}
|
|
164
|
-
items = plan.items
|
|
165
|
-
log(`Planned ${items.length} work items`)
|
|
166
|
-
}
|
|
167
|
-
if (items.length > 10) {
|
|
168
|
-
log(`Capping fan-out: running the first 10 of ${items.length} items`)
|
|
169
|
-
items = items.slice(0, 10)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// pipeline(): no barrier between stages — item 0 can be verifying while item 3
|
|
173
|
-
// is still executing. Wall-clock is the slowest single item, not the sum.
|
|
174
|
-
const results = await pipeline(
|
|
175
|
-
items,
|
|
176
|
-
|
|
177
|
-
// Stage 1 — execute cheap (haiku/sonnet, effort low: the cost levers)
|
|
178
|
-
(item, _orig, i) =>
|
|
179
|
-
agent(execPrompt(item.task, `${BRANCH_PREFIX}-${i}`), {
|
|
180
|
-
label: `exec-${i}:${item.tier}`,
|
|
181
|
-
phase: 'Execute',
|
|
182
|
-
model: item.tier,
|
|
183
|
-
effort: 'low',
|
|
184
|
-
isolation: 'worktree',
|
|
185
|
-
schema: EXEC_SCHEMA,
|
|
186
|
-
}).then(result => boundExecutorOutput(result, `${BRANCH_PREFIX}-${i}`)),
|
|
187
|
-
|
|
188
|
-
// Stage 2 — escalation ladder:
|
|
189
|
-
// - confident result (non-null, confidence !== 'low') -> return as-is, no escalation.
|
|
190
|
-
// - null result -> ONE retry at the same tier (haiku retries at sonnet, since
|
|
191
|
-
// haiku already failed cheap); if that retry is also null/low, ONE escalation to
|
|
192
|
-
// the next tier up.
|
|
193
|
-
// - low-confidence result -> ONE escalation exactly one rung up.
|
|
194
|
-
// Stop at the first confident attempt. Every superseded attempt's branch is
|
|
195
|
-
// collected into supersededBranches so the tail can flag it as an orphan.
|
|
196
|
-
async (run, item, i) => {
|
|
197
|
-
if (run && run.confidence !== 'low') return run
|
|
198
|
-
|
|
199
|
-
const supersededBranches = []
|
|
200
|
-
|
|
201
|
-
async function attempt(tier, suffix, priorSummary) {
|
|
202
|
-
const branch = `${BRANCH_PREFIX}-${i}-${suffix}`
|
|
203
|
-
return agent(
|
|
204
|
-
execPrompt(item.task, branch) +
|
|
205
|
-
`\n\nA cheaper model already attempted this and reported: "${priorSummary}". Start from the task itself on a fresh branch off the same base as mainline — do NOT build on the earlier attempt's branch.`,
|
|
206
|
-
{ label: `escalate-${i}-${suffix}`, phase: 'Execute', model: tier, effort: effortFor(tier), isolation: 'worktree', schema: EXEC_SCHEMA },
|
|
207
|
-
).then(result => boundExecutorOutput(result, branch))
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let result
|
|
211
|
-
let finalTier
|
|
212
|
-
if (!run) {
|
|
213
|
-
const retryTier = item.tier === TIERS.cheap ? TIERS.normal : item.tier
|
|
214
|
-
finalTier = retryTier
|
|
215
|
-
log(`Item ${i} produced no result — retrying at ${retryTier}`)
|
|
216
|
-
result = await attempt(retryTier, 'r2', 'no result (agent failed)')
|
|
217
|
-
if (!result || result.confidence === 'low') {
|
|
218
|
-
if (result && result.branch) supersededBranches.push(result.branch)
|
|
219
|
-
const escTier = nextTier(retryTier)
|
|
220
|
-
finalTier = escTier
|
|
221
|
-
log(`Item ${i} still ${result ? 'low confidence' : 'no result'} at ${retryTier} — escalating to ${escTier}`)
|
|
222
|
-
result = await attempt(escTier, 'r3', result ? result.summary : 'no result on retry')
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
if (run.branch) supersededBranches.push(run.branch)
|
|
226
|
-
const escTier = nextTier(item.tier)
|
|
227
|
-
finalTier = escTier
|
|
228
|
-
log(`Item ${i} low confidence — escalating to ${escTier}`)
|
|
229
|
-
result = await attempt(escTier, 'r2', run.summary)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// "escalated" means the work actually moved up a rung. A same-tier retry
|
|
233
|
-
// (a null result at a tier that is already the ceiling) is not one.
|
|
234
|
-
const escalated = finalTier !== item.tier
|
|
235
|
-
|
|
236
|
-
if (!result) {
|
|
237
|
-
// Every attempt failed, but earlier attempts may already have created
|
|
238
|
-
// branches. Returning null here would drop supersededBranches and leave
|
|
239
|
-
// those branches out of the orphan report — invisible litter in the repo.
|
|
240
|
-
return { summary: 'every attempt failed; no usable result', confidence: 'low', supersededBranches, escalated }
|
|
241
|
-
}
|
|
242
|
-
return { ...result, supersededBranches, escalated }
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
// Stage 3 — Opus verifies the actual diff, not the executor's self-report.
|
|
246
|
-
// Stage 2 always returns an object (never null — see its final `return`s
|
|
247
|
-
// above), so `run` here is never null; no null-guard needed.
|
|
248
|
-
async (run, item, i) => {
|
|
249
|
-
if (!run.branch) {
|
|
250
|
-
return { task: item.task, ...run, verdict: { approved: false, issues: ['executor reported no branch — nothing to review'] } }
|
|
251
|
-
}
|
|
252
|
-
// agentType pulls in the canonical reviewer rubric (roles/reviewer.md)
|
|
253
|
-
// instead of the weaker inline restatement this used to carry.
|
|
254
|
-
const verdict = await agent(
|
|
255
|
-
[
|
|
256
|
-
`Review branch ${run.branch} against this task: "${item.task}".`,
|
|
257
|
-
`Diff scope: git diff $(git merge-base HEAD refs/heads/${run.branch}) refs/heads/${run.branch}`,
|
|
258
|
-
`First check the branch is reviewable: git rev-parse --verify refs/heads/${run.branch} and git diff --stat $(git merge-base HEAD refs/heads/${run.branch}) refs/heads/${run.branch}. If the branch is missing or the diff is empty, return approved: false with issue "no reviewable diff".`,
|
|
259
|
-
`Executor self-report (do not trust it, verify it): ${run.summary} — checks: ${run.checks || 'none reported'}`,
|
|
260
|
-
].join('\n'),
|
|
261
|
-
{ label: `verify-${i}`, phase: 'Verify', agentType: 'leo:reviewer', model: TIERS.judge, effort: 'medium', schema: VERDICT_SCHEMA },
|
|
262
|
-
)
|
|
263
|
-
return { task: item.task, ...run, verdict }
|
|
264
|
-
},
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
const done = results.filter(Boolean)
|
|
268
|
-
const approved = done.filter(r => r.verdict && r.verdict.approved)
|
|
269
|
-
const rejected = done.filter(r => !r.verdict || !r.verdict.approved)
|
|
270
|
-
log(`${approved.length} approved, ${rejected.length} rejected, ${items.length - done.length} failed to run`)
|
|
271
|
-
|
|
272
|
-
// Orphan tracking: only superseded retries (an earlier attempt's branch that
|
|
273
|
-
// got superseded by a later, kept attempt on the SAME item) are safe to
|
|
274
|
-
// delete — that work is duplicated by the branch that replaced it. A
|
|
275
|
-
// rejected branch is different: it may be the only copy of that item's
|
|
276
|
-
// work, just judged not good enough yet, so it is reported separately and
|
|
277
|
-
// never described as safe to delete — deleting it on the note's say-so
|
|
278
|
-
// would destroy the only copy.
|
|
279
|
-
const orphans = [...new Set(done.flatMap(r => r.supersededBranches || []).filter(Boolean))]
|
|
280
|
-
const kept = approved.map(r => r.branch).filter(Boolean)
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
approved: approved.map(r => ({ task: r.task, branch: r.branch, escalated: !!r.escalated })),
|
|
284
|
-
rejected: rejected.map(r => ({ task: r.task, branch: r.branch || null, issues: r.verdict ? r.verdict.issues : ['agent failed, no verdict'] })),
|
|
285
|
-
orphans,
|
|
286
|
-
note: `Approved (merge these from the main session): ${kept.join(', ') || 'none'}. Orphaned (superseded retries — safe to delete): ${orphans.join(', ') || 'none'}. Rejected branches hold work that failed review but may still be worth salvaging — do NOT delete them without reviewing first: ${rejected.map(r => r.branch).filter(Boolean).join(', ') || 'none'}. To clean up an orphan: \`git worktree list\` to find its path, then \`git worktree remove <path>\` (prune does not remove live worktrees).`,
|
|
287
|
-
}
|