summaryrefslogtreecommitdiff
path: root/.claude/workflows/fanout-dev.js
blob: e98a86f1dd9fd4e3fefcf0603730ddfb0ccd579f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
export const meta = {
  name: 'fanout-dev',
  description: 'Implement one described change across many ports/file-sets: one port-dev worker per item, independent builder verification, optional review',
  whenToUse: 'Applying a fix or pattern across multiple TinyUSB ports (e.g. the same DCD bug in several drivers)',
  phases: [
    { title: 'Implement', detail: 'port-dev per item (opus xhigh)' },
    { title: 'Verify', detail: 'builder single-example check' },
    { title: 'Review', detail: 'optional driver-reviewer pass' },
  ],
}

// args: { task: string, items: string[], board?: string | Record<string,string>, review?: boolean, worktree?: boolean }
if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !args.task || !Array.isArray(args.items) || args.items.length === 0) {
  throw new Error('args must be { task: string, items: string[], board?, review?, worktree? }')
}
const boardFor = (item) =>
  typeof args.board === 'string' ? args.board : (args.board && args.board[item]) || null
const short = (s) => s.replace(/\/+$/, '').split('/').slice(-2).join('/')
if (args.worktree) log('worktree mode: independent builder verification and review skipped (workers verify inside their own worktrees)')

const DEV = {
  type: 'object', additionalProperties: false,
  required: ['item', 'diffstat', 'buildOk', 'board', 'notes'],
  properties: {
    item: { type: 'string' }, diffstat: { type: 'string' }, buildOk: { type: 'boolean' },
    board: { type: 'string' }, notes: { type: 'string' },
  },
}
const BUILD = {
  type: 'object', additionalProperties: false,
  required: ['board', 'pass', 'builtCount', 'failures'],
  properties: {
    board: { type: 'string' }, pass: { type: 'boolean' }, builtCount: { type: 'integer' },
    failures: {
      type: 'array',
      items: {
        type: 'object', additionalProperties: false,
        required: ['example', 'class', 'firstError'],
        properties: { example: { type: 'string' }, class: { type: 'string' }, firstError: { type: 'string' } },
      },
    },
  },
}
const FINDINGS = {
  type: 'object', additionalProperties: false,
  required: ['scope', 'dimension', 'findings'],
  properties: {
    scope: { type: 'string' }, dimension: { type: 'string' },
    findings: {
      type: 'array',
      items: {
        type: 'object', additionalProperties: false,
        required: ['file', 'line', 'snippet', 'why', 'severity', 'confidence'],
        properties: {
          file: { type: 'string' }, line: { type: 'integer' }, snippet: { type: 'string' },
          why: { type: 'string' }, severity: { type: 'string' }, confidence: { type: 'string' },
        },
      },
    },
  },
}

const results = await pipeline(
  args.items,

  item => agent(
    `${args.task}\n\nAssigned scope: ${item} — touch nothing outside it.` +
    (boardFor(item)
      ? ` Verify with board ${boardFor(item)}.`
      : ' Pick a verification board from hw/bsp whose family uses this scope.'),
    {
      label: `dev:${short(item)}`, phase: 'Implement',
      agentType: 'port-dev', effort: 'xhigh', schema: DEV,
      ...(args.worktree ? { isolation: 'worktree' } : {}),
    },
  ),

  (dev, item) => {
    if (!dev) return null
    // worktree mode: edits live in the worker's own worktree; an independent
    // verifier in the shared tree cannot see them — trust dev.buildOk.
    if (args.worktree) return dev
    return agent(
      `Build the single example device/cdc_msc for board ${dev.board}. Use a unique build dir (mktemp -d) to avoid collisions with parallel builds.`,
      { label: `verify:${short(item)}`, phase: 'Verify', agentType: 'builder', schema: BUILD },
    ).then(b => {
      // verifyBuild: true/false = real builder verdict; null = builder died
      if (!b) log(`verify:${short(item)}: builder agent died — independent verification unknown`)
      return { ...dev, verifyBuild: b ? b.pass : null }
    })
  },

  (r, item) => {
    if (!r || !args.review || args.worktree) return r
    return agent(
      `Review the uncommitted change in ${item} (inspect with: git diff -- ${item}) against this task:\n${args.task}\n` +
      'Dimension: does the diff correctly and completely implement the task with no unintended side effects? Coverage-first findings.',
      { label: `review:${short(item)}`, phase: 'Review', agentType: 'driver-reviewer', effort: 'xhigh', schema: FINDINGS },
    ).then(f => {
      // review: array = findings; null = reviewer died; absent = not requested
      if (!f) log(`review:${short(item)}: reviewer agent died`)
      return { ...r, review: f ? f.findings : null }
    })
  },
)

const done = results.filter(Boolean)
const dropped = args.items.length - done.length
if (dropped > 0) log(`${dropped} item(s) dropped (worker died)`)
log(`${done.length}/${args.items.length} items completed; ${done.filter(r => r.buildOk && r.verifyBuild !== false).length} build-clean`)
return done