Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "agentille",
"displayName": "agentille",
"version": "3.8.0",
"version": "3.8.1",
"description": "One command for Claude Code: /agt plans, builds and reviews with the right model per step, steps aside on small tasks, and ends on one short card. Zero setup.",
"author": {
"name": "Systown AI Lab",
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to agentille are documented here.

## [3.8.1] — 2026-10-08

### Fixed

- A swarm lane past eight cells keeps the working agent: it keeps every
working and failed cell and fills the rest with the newest finished ones.
It used to keep the oldest, so a live agent could fall into `+n`.
- No swarm line outside an `/agt` run: plain subagents of a session no
longer build a session-long line.
- A workflow agent inside one tool call longer than ten minutes is no
longer marked done.

## [3.8.0] — 2026-10-08

### Added
Expand Down
11 changes: 8 additions & 3 deletions hooks/board.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,12 @@ export function phaseOf(role) {
// One cell per agent of the run, done ones included, so a run of twelve reads in one line where
// the tree only has room for a few rows. A working cell pulses (◆/◇, ▣/□) on the ticker, offset
// per cell; a finished one is a dim ✓, a failed or killed one ✗. Pane workers come from their
// routes (one per spawn), working until the route ends. Undefined below three agents: the tree
// already shows that many.
// routes (one per spawn), working until the route ends. Undefined below three agents (the tree
// already shows that many) and outside an /agt run (`adhoc` is every plain subagent of the session).
// A lane past LANE_CAP keeps every working and failed cell and fills the rest with the newest
// finished ones, in start order: the live agent is never the one cut.
export function swarm({ agents, routes = [], run, tick = 0 }) {
if (run === 'adhoc') return undefined
const cells = []
for (const a of agents.values()) {
if (a.run !== run) continue
Expand All @@ -248,7 +251,9 @@ export function swarm({ agents, routes = [], run, tick = 0 }) {
for (const phase of PHASES) {
const mine = cells.filter((c) => c.phase === phase)
if (!mine.length) continue
const shown = mine.slice(0, LANE_CAP).map((c, i) => {
const keep = new Set(mine.filter((c) => c.working || c.broken).slice(-LANE_CAP))
for (let i = mine.length - 1; i >= 0 && keep.size < LANE_CAP; i--) keep.add(mine[i])
const shown = mine.filter((c) => keep.has(c)).map((c, i) => {
if (c.broken) return { glyph: '✗', color: 'error', dim: false }
if (!c.working) return { glyph: DONE_GLYPH, color: hex(DONE_COLOR), dim: true }
const on = (tick + i) % 4 < 2
Expand Down
11 changes: 11 additions & 0 deletions hooks/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,17 @@ export function register(on) {
a.tool = toolLabel(e.tool, e)
a.heard = Date.now()
}
// A workflow agent inside one long call (a full test suite) is alive: tree.js quietWorkflows
// skips it until the call returns, and the return counts as heard.
if (a?.workflow) {
a.calls = (a.calls ?? 0) + 1
try {
return await next(e)
} finally {
a.calls -= 1
a.heard = Date.now()
}
}
} else if (wireName) {
pub.tool = toolLabel(e.tool, e)
void publish($).catch(() => {})
Expand Down
2 changes: 1 addition & 1 deletion hooks/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function applyStatus(live, list, now) {
export function quietWorkflows(live, now) {
let changed = false
for (const a of live.values()) {
if (!a.workflow || a.state !== 'working' || now - (a.heard ?? a.start) < WORKFLOW_SILENT_MS) continue
if (!a.workflow || a.state !== 'working' || a.calls > 0 || now - (a.heard ?? a.start) < WORKFLOW_SILENT_MS) continue
finish(a, now)
changed = true
}
Expand Down
17 changes: 17 additions & 0 deletions tests/mod/board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,23 @@ describe('swarm', () => {
expect(s.total).toBe(11)
})

test('a long lane keeps the live agent and the newest finished ones', async () => {
const m = new Map()
for (let i = 0; i < 9; i++) { const a = sub('f' + i, 'executor', 'sonnet', i); finish(a, 100, 1); m.set(a.id, a) }
m.set('live', sub('live', 'executor', 'sonnet', 50))
const lane = swarm({ agents: m, run: 'r1', tick: 0 })!.lanes[0]
expect(lane.cells.length).toBe(8)
expect(lane.more).toBe(2)
expect(['◆', '◇']).toContain(lane.cells[7].glyph) // the newest, working, drawn last
expect(lane.cells.filter((c) => c.glyph === '✓').length).toBe(7)
})

test('no swarm line outside an /agt run', async () => {
const m = agents()
for (const a of m.values()) a.run = 'adhoc'
expect(swarm({ agents: m, run: 'adhoc' })).toBe(undefined)
})

test('phaseOf sorts every routed role', async () => {
expect(['planner', 'plan-reviewer', 'ui-prototyper'].map(phaseOf)).toEqual(['plan', 'plan', 'plan'])
expect(['executor', 'adversary'].map(phaseOf)).toEqual(['build', 'build'])
Expand Down
7 changes: 7 additions & 0 deletions tests/mod/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ describe('tree: forks and workflow agents', () => {
expect(quietWorkflows(live, WORKFLOW_SILENT_MS)).toBe(true)
expect([...live.values()].map((a) => a.state)).toEqual(['done', 'working', 'working'])
})

test('a workflow agent inside a long tool call is not timed out', async () => {
const live = new Map([['w1', agent('w1', { workflow: 'wf_1', heard: 0, calls: 1 })]])
expect(quietWorkflows(live, WORKFLOW_SILENT_MS * 3)).toBe(false)
live.get('w1').calls = 0
expect(quietWorkflows(live, WORKFLOW_SILENT_MS * 3)).toBe(true)
})
})

describe('tree: nest', () => {
Expand Down
Loading