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.
Files changed (96) hide show
  1. package/README.md +544 -28
  2. package/commands/handoff.md +11 -0
  3. package/commands/handon.md +10 -0
  4. package/commands/review-pr.md +9 -0
  5. package/commands-claude/watch-review.md +9 -0
  6. package/index.js +12 -0
  7. package/package.json +29 -14
  8. package/payload/codex-agents/leo-executor.toml +36 -0
  9. package/payload/codex-agents/leo-runner.toml +28 -0
  10. package/rules/preferences.md +97 -0
  11. package/scripts/check.py +244 -0
  12. package/scripts/ghreview.py +17 -3
  13. package/scripts/handoff.py +183 -0
  14. package/scripts/leo-install.py +509 -0
  15. package/scripts/measure_context.py +113 -0
  16. package/scripts/publish-npm.py +138 -0
  17. package/scripts/watch_review.py +169 -0
  18. package/skills/doctor/SKILL.md +73 -115
  19. package/skills/doctor/agents/openai.yaml +5 -0
  20. package/skills/handoff/SKILL.md +99 -0
  21. package/skills/handoff/agents/openai.yaml +5 -0
  22. package/skills/handon/SKILL.md +61 -0
  23. package/skills/install/SKILL.md +79 -0
  24. package/skills/install/agents/openai.yaml +5 -0
  25. package/skills/review-pr/SKILL.md +59 -318
  26. package/skills/review-pr/reference/lenses.md +67 -0
  27. package/skills/review-pr/reference/procedure.md +348 -0
  28. package/skills-claude/attach-pr/SKILL.md +178 -0
  29. package/skills-claude/watch-review/SKILL.md +91 -0
  30. package/adapters/cursor/agents/executor.md +0 -17
  31. package/adapters/cursor/agents/expert.md +0 -70
  32. package/adapters/cursor/agents/explore.md +0 -16
  33. package/adapters/cursor/agents/implementer.md +0 -18
  34. package/adapters/cursor/agents/investigator.md +0 -18
  35. package/adapters/cursor/agents/planner.md +0 -28
  36. package/adapters/cursor/agents/review-lens.md +0 -22
  37. package/adapters/cursor/agents/reviewer.md +0 -34
  38. package/adapters/opencode/agents.json +0 -105
  39. package/adapters/opencode/plugin.js +0 -576
  40. package/config/MCP_PINS.md +0 -17
  41. package/config/models.json +0 -676
  42. package/hooks/bash-guard.py +0 -583
  43. package/hooks/cursor-guard.py +0 -84
  44. package/hooks/hooks-cursor.json +0 -11
  45. package/hooks/hooks.json +0 -20
  46. package/hooks/session-start.py +0 -148
  47. package/roles/executor.md +0 -15
  48. package/roles/expert.md +0 -67
  49. package/roles/explore.md +0 -13
  50. package/roles/implementer.md +0 -16
  51. package/roles/investigator.md +0 -15
  52. package/roles/planner.md +0 -25
  53. package/roles/review-lens.md +0 -20
  54. package/roles/reviewer.md +0 -31
  55. package/scripts/doctor.py +0 -520
  56. package/scripts/jsonc_bridge.cjs +0 -23
  57. package/scripts/memory.py +0 -744
  58. package/scripts/render_adapters.py +0 -508
  59. package/scripts/setup.py +0 -1753
  60. package/settings.json +0 -7
  61. package/skills/.gitkeep +0 -0
  62. package/skills/brainstorming/SKILL.md +0 -111
  63. package/skills/debugging/SKILL.md +0 -100
  64. package/skills/delegation/SKILL.md +0 -143
  65. package/skills/executing-plans/SKILL.md +0 -117
  66. package/skills/finishing-a-branch/SKILL.md +0 -125
  67. package/skills/freshness/SKILL.md +0 -131
  68. package/skills/memory/SKILL.md +0 -154
  69. package/skills/resolve-ticket/SKILL.md +0 -275
  70. package/skills/setup/SKILL.md +0 -199
  71. package/skills/setup/agents/openai.yaml +0 -5
  72. package/skills/test-first/SKILL.md +0 -92
  73. package/skills/using-leo/SKILL.md +0 -101
  74. package/skills/using-leo/references/claude-mapping.md +0 -33
  75. package/skills/using-leo/references/codex-mapping.md +0 -33
  76. package/skills/using-leo/references/cursor-mapping.md +0 -35
  77. package/skills/using-leo/references/hermes-mapping.md +0 -37
  78. package/skills/using-leo/references/opencode-mapping.md +0 -39
  79. package/skills/verification/SKILL.md +0 -110
  80. package/skills/visual-verification/SKILL.md +0 -115
  81. package/skills/watch-review/SKILL.md +0 -128
  82. package/skills/watch-review/agents/openai.yaml +0 -5
  83. package/skills/worktrees/SKILL.md +0 -131
  84. package/skills/writing-plans/SKILL.md +0 -97
  85. package/skills/writing-skills/SKILL.md +0 -141
  86. package/vendor/jsonc-parser-3.3.1/LICENSE.md +0 -21
  87. package/vendor/jsonc-parser-3.3.1/README.md +0 -26
  88. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +0 -201
  89. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +0 -275
  90. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +0 -682
  91. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +0 -456
  92. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +0 -42
  93. package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +0 -351
  94. package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +0 -194
  95. package/vendor/jsonc-parser-3.3.1/package.json +0 -37
  96. 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
- }