nightralph 0.0.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 (101) hide show
  1. package/.claude/settings.local.json +5 -0
  2. package/AGENTS.md +60 -0
  3. package/CLAUDE.md +1 -0
  4. package/PROMPT.md +95 -0
  5. package/README.md +138 -0
  6. package/WIP.md +5 -0
  7. package/dist/docs-templates/domain.md +36 -0
  8. package/dist/docs-templates/issue-tracker-github.md +228 -0
  9. package/dist/docs-templates/issue-tracker.md +30 -0
  10. package/dist/docs-templates/triage-labels.md +15 -0
  11. package/dist/index.js +154 -0
  12. package/dist/index.js.map +7 -0
  13. package/dist/meta.json +207 -0
  14. package/dist/orchestrator.js +273 -0
  15. package/dist/orchestrator.js.map +7 -0
  16. package/dist/resolve.js +71 -0
  17. package/dist/resolve.js.map +7 -0
  18. package/dist/setup.js +151 -0
  19. package/dist/setup.js.map +7 -0
  20. package/dist/skills/domain-modeling/ADR-FORMAT.md +47 -0
  21. package/dist/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  22. package/dist/skills/domain-modeling/SKILL.md +74 -0
  23. package/dist/skills/domain-modeling/agents/openai.yaml +3 -0
  24. package/dist/skills/grill/SKILL.md +7 -0
  25. package/dist/skills/grill/agents/openai.yaml +5 -0
  26. package/dist/skills/grilling/SKILL.md +28 -0
  27. package/dist/skills/grilling/agents/openai.yaml +3 -0
  28. package/dist/skills/tdd/SKILL.md +38 -0
  29. package/dist/skills/tdd/agents/openai.yaml +3 -0
  30. package/dist/skills/tdd/mocking.md +59 -0
  31. package/dist/skills/tdd/tests.md +77 -0
  32. package/dist/skills/to-spec/SKILL.md +75 -0
  33. package/dist/skills/to-spec/agents/openai.yaml +5 -0
  34. package/dist/skills/to-tickets/SKILL.md +105 -0
  35. package/dist/skills/to-tickets/agents/openai.yaml +5 -0
  36. package/dist/skills/upstream.json +5 -0
  37. package/dist/src/index.d.ts +4 -0
  38. package/dist/src/index.d.ts.map +1 -0
  39. package/dist/src/orchestrator.d.ts +31 -0
  40. package/dist/src/orchestrator.d.ts.map +1 -0
  41. package/dist/src/resolve.d.ts +10 -0
  42. package/dist/src/resolve.d.ts.map +1 -0
  43. package/dist/src/setup.d.ts +13 -0
  44. package/dist/src/setup.d.ts.map +1 -0
  45. package/dist/test/integration.test.js +848 -0
  46. package/dist/test/integration.test.js.map +7 -0
  47. package/dist/test/orchestrator.test.js +749 -0
  48. package/dist/test/orchestrator.test.js.map +7 -0
  49. package/dist/test/setup.test.js +574 -0
  50. package/dist/test/setup.test.js.map +7 -0
  51. package/docs/README.md +12 -0
  52. package/docs/agents/domain.md +36 -0
  53. package/docs/agents/issue-tracker.md +30 -0
  54. package/docs/agents/triage-labels.md +15 -0
  55. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +144 -0
  56. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +292 -0
  57. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +212 -0
  58. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +203 -0
  59. package/docs/skills.md +58 -0
  60. package/docs/test-plans/2026-08-27-issue-tracker-choice.md +95 -0
  61. package/eslint.config.js +142 -0
  62. package/lib.es5.d.ts +19 -0
  63. package/package.json +36 -0
  64. package/progress.log +190 -0
  65. package/ralph_claude.sh +263 -0
  66. package/scripts/clone-skills.sh +52 -0
  67. package/specs/prd.json +172 -0
  68. package/src/docs-templates/domain.md +36 -0
  69. package/src/docs-templates/issue-tracker-github.md +228 -0
  70. package/src/docs-templates/issue-tracker.md +30 -0
  71. package/src/docs-templates/triage-labels.md +15 -0
  72. package/src/index.ts +184 -0
  73. package/src/orchestrator.ts +324 -0
  74. package/src/resolve.ts +73 -0
  75. package/src/setup.ts +157 -0
  76. package/src/skills/domain-modeling/ADR-FORMAT.md +47 -0
  77. package/src/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  78. package/src/skills/domain-modeling/SKILL.md +74 -0
  79. package/src/skills/domain-modeling/agents/openai.yaml +3 -0
  80. package/src/skills/grill/SKILL.md +7 -0
  81. package/src/skills/grill/agents/openai.yaml +5 -0
  82. package/src/skills/grilling/SKILL.md +28 -0
  83. package/src/skills/grilling/agents/openai.yaml +3 -0
  84. package/src/skills/tdd/SKILL.md +38 -0
  85. package/src/skills/tdd/agents/openai.yaml +3 -0
  86. package/src/skills/tdd/mocking.md +59 -0
  87. package/src/skills/tdd/tests.md +77 -0
  88. package/src/skills/to-spec/SKILL.md +75 -0
  89. package/src/skills/to-spec/agents/openai.yaml +5 -0
  90. package/src/skills/to-tickets/SKILL.md +105 -0
  91. package/src/skills/to-tickets/agents/openai.yaml +5 -0
  92. package/src/skills/upstream.json +5 -0
  93. package/tasks/prd-integration-test-coverage.md +243 -0
  94. package/test/integration.test.ts +636 -0
  95. package/test/mock-agent-echo-args.js +4 -0
  96. package/test/mock-agent-fail.js +3 -0
  97. package/test/mock-agent-pass.js +3 -0
  98. package/test/orchestrator.test.ts +609 -0
  99. package/test/setup.test.ts +417 -0
  100. package/tsconfig.build.json +7 -0
  101. package/tsconfig.json +31 -0
@@ -0,0 +1,609 @@
1
+ import { test } from '@substrate-system/tapzero'
2
+ import {
3
+ mkdtempSync, mkdirSync, writeFileSync,
4
+ readFileSync, existsSync, readdirSync,
5
+ } from 'node:fs'
6
+ import { join } from 'node:path'
7
+ import { tmpdir } from 'node:os'
8
+ import {
9
+ parseTicketFilename,
10
+ parseStatus,
11
+ parseBlockers,
12
+ scanTickets,
13
+ findReadyTickets,
14
+ markDone,
15
+ renderPrompt,
16
+ runWaves,
17
+ type Ticket,
18
+ } from '../src/orchestrator.js'
19
+
20
+ // -- helpers --
21
+
22
+ function tmp ():string {
23
+ return mkdtempSync(
24
+ join(tmpdir(), 'nightralph-orch-test-')
25
+ )
26
+ }
27
+
28
+ function makeTicketBody (opts:{
29
+ status:string
30
+ blockers?:number[]
31
+ }):string {
32
+ const lines = [
33
+ '# Test ticket',
34
+ '',
35
+ `**Status:** ${opts.status}`,
36
+ ]
37
+ if (opts.blockers && opts.blockers.length > 0) {
38
+ lines.push(
39
+ `**Blocked by:** ${opts.blockers.join(', ')}`
40
+ )
41
+ }
42
+ return lines.join('\n')
43
+ }
44
+
45
+ function makeTicket (overrides:Partial<Ticket>&{
46
+ num:number;
47
+ filename:string;
48
+ }):Ticket {
49
+ return {
50
+ slug:overrides.filename.replace(
51
+ /^\d+-(.+)\.md$/, '$1'
52
+ ),
53
+ filepath:`/fake/${overrides.filename}`,
54
+ body:makeTicketBody({
55
+ status:overrides.status ?? 'ready-for-agent',
56
+ }),
57
+ status:overrides.status ?? 'ready-for-agent',
58
+ blockers:overrides.blockers ?? [],
59
+ ...overrides,
60
+ }
61
+ }
62
+
63
+ // -- parseTicketFilename --
64
+
65
+ test('parseTicketFilename parses valid names', t => {
66
+ const r = parseTicketFilename('01-add-auth.md')
67
+ t.ok(r, 'should return a result')
68
+ t.equal(r!.num, 1, 'num')
69
+ t.equal(r!.slug, 'add-auth', 'slug')
70
+ })
71
+
72
+ test('parseTicketFilename with multi-digit number', t => {
73
+ const r = parseTicketFilename('123-foo-bar.md')
74
+ t.ok(r, 'should return a result')
75
+ t.equal(r!.num, 123, 'num')
76
+ t.equal(r!.slug, 'foo-bar', 'slug')
77
+ })
78
+
79
+ test('parseTicketFilename rejects non-matching names', t => {
80
+ t.equal(
81
+ parseTicketFilename('readme.md'),
82
+ null,
83
+ 'no leading number'
84
+ )
85
+ t.equal(
86
+ parseTicketFilename('01-thing.txt'),
87
+ null,
88
+ 'wrong extension'
89
+ )
90
+ t.equal(
91
+ parseTicketFilename('not-a-ticket'),
92
+ null,
93
+ 'no extension'
94
+ )
95
+ })
96
+
97
+ // -- parseStatus --
98
+
99
+ test('parseStatus extracts status', t => {
100
+ t.equal(
101
+ parseStatus('**Status:** ready-for-agent'),
102
+ 'ready-for-agent',
103
+ 'basic'
104
+ )
105
+ t.equal(
106
+ parseStatus('**Status:** Done '),
107
+ 'done',
108
+ 'trims and lowercases'
109
+ )
110
+ t.equal(
111
+ parseStatus('no status here'),
112
+ '',
113
+ 'missing status returns empty'
114
+ )
115
+ })
116
+
117
+ test('parseStatus works in multi-line body', t => {
118
+ const body = [
119
+ '# Ticket',
120
+ '',
121
+ '**Status:** ready-for-agent',
122
+ '**Blocked by:** 1, 2',
123
+ ].join('\n')
124
+ t.equal(parseStatus(body), 'ready-for-agent')
125
+ })
126
+
127
+ // -- parseBlockers --
128
+
129
+ test('parseBlockers extracts blocker numbers', t => {
130
+ t.deepEqual(
131
+ parseBlockers('**Blocked by:** 1, 2, 3'),
132
+ [1, 2, 3],
133
+ 'comma-separated'
134
+ )
135
+ t.deepEqual(
136
+ parseBlockers('**Blocked by:** #4 and #5'),
137
+ [4, 5],
138
+ 'hash-prefixed'
139
+ )
140
+ })
141
+
142
+ test('parseBlockers returns empty for none', t => {
143
+ t.deepEqual(
144
+ parseBlockers('**Blocked by:** none'),
145
+ [],
146
+ 'explicit none'
147
+ )
148
+ t.deepEqual(
149
+ parseBlockers('**Blocked by:** None'),
150
+ [],
151
+ 'case-insensitive none'
152
+ )
153
+ t.deepEqual(
154
+ parseBlockers('no blockers field at all'),
155
+ [],
156
+ 'missing field'
157
+ )
158
+ })
159
+
160
+ // -- scanTickets --
161
+
162
+ test('scanTickets reads and parses ticket files', t => {
163
+ const dir = tmp()
164
+ writeFileSync(
165
+ join(dir, '01-first.md'),
166
+ makeTicketBody({
167
+ status:'ready-for-agent',
168
+ blockers:[],
169
+ })
170
+ )
171
+ writeFileSync(
172
+ join(dir, '02-second.md'),
173
+ makeTicketBody({
174
+ status:'ready-for-agent',
175
+ blockers:[1],
176
+ })
177
+ )
178
+ writeFileSync(
179
+ join(dir, 'ignore-me.txt'),
180
+ 'not a ticket'
181
+ )
182
+
183
+ const tickets = scanTickets(dir)
184
+ t.equal(tickets.length, 2, 'finds 2 tickets')
185
+ t.equal(tickets[0].num, 1, 'first ticket num')
186
+ t.equal(tickets[0].slug, 'first', 'first ticket slug')
187
+ t.equal(
188
+ tickets[0].status,
189
+ 'ready-for-agent',
190
+ 'first ticket status'
191
+ )
192
+ t.deepEqual(
193
+ tickets[0].blockers,
194
+ [],
195
+ 'first has no blockers'
196
+ )
197
+ t.equal(tickets[1].num, 2, 'second ticket num')
198
+ t.deepEqual(
199
+ tickets[1].blockers,
200
+ [1],
201
+ 'second blocked by 1'
202
+ )
203
+ })
204
+
205
+ test('scanTickets returns sorted by number', t => {
206
+ const dir = tmp()
207
+ writeFileSync(
208
+ join(dir, '10-z.md'),
209
+ makeTicketBody({ status:'ready-for-agent' })
210
+ )
211
+ writeFileSync(
212
+ join(dir, '02-a.md'),
213
+ makeTicketBody({ status:'ready-for-agent' })
214
+ )
215
+ writeFileSync(
216
+ join(dir, '03-b.md'),
217
+ makeTicketBody({ status:'ready-for-agent' })
218
+ )
219
+
220
+ const tickets = scanTickets(dir)
221
+ t.deepEqual(
222
+ tickets.map(t => t.num),
223
+ [2, 3, 10],
224
+ 'numeric sort'
225
+ )
226
+ })
227
+
228
+ // -- findReadyTickets --
229
+
230
+ test('findReadyTickets returns unblocked ready tickets', t => {
231
+ const tickets:Ticket[] = [
232
+ makeTicket({
233
+ num:1,
234
+ filename:'01-a.md',
235
+ status:'ready-for-agent',
236
+ blockers:[],
237
+ }),
238
+ makeTicket({
239
+ num:2,
240
+ filename:'02-b.md',
241
+ status:'ready-for-agent',
242
+ blockers:[1],
243
+ }),
244
+ makeTicket({
245
+ num:3,
246
+ filename:'03-c.md',
247
+ status:'done',
248
+ blockers:[],
249
+ }),
250
+ ]
251
+
252
+ const ready = findReadyTickets(tickets)
253
+ t.equal(ready.length, 1, 'only one is ready')
254
+ t.equal(ready[0].num, 1, 'ticket 1 is ready')
255
+ })
256
+
257
+ test('findReadyTickets unblocks after deps done', t => {
258
+ const tickets:Ticket[] = [
259
+ makeTicket({
260
+ num:1,
261
+ filename:'01-a.md',
262
+ status:'done',
263
+ blockers:[],
264
+ }),
265
+ makeTicket({
266
+ num:2,
267
+ filename:'02-b.md',
268
+ status:'ready-for-agent',
269
+ blockers:[1],
270
+ }),
271
+ makeTicket({
272
+ num:3,
273
+ filename:'03-c.md',
274
+ status:'ready-for-agent',
275
+ blockers:[1, 2],
276
+ }),
277
+ ]
278
+
279
+ const ready = findReadyTickets(tickets)
280
+ t.equal(ready.length, 1, 'only ticket 2 is ready')
281
+ t.equal(
282
+ ready[0].num,
283
+ 2,
284
+ 'ticket 2 unblocked because 1 is done'
285
+ )
286
+ })
287
+
288
+ test('findReadyTickets returns nothing when all blocked', t => {
289
+ const tickets:Ticket[] = [
290
+ makeTicket({
291
+ num:1,
292
+ filename:'01-a.md',
293
+ status:'ready-for-agent',
294
+ blockers:[2],
295
+ }),
296
+ makeTicket({
297
+ num:2,
298
+ filename:'02-b.md',
299
+ status:'ready-for-agent',
300
+ blockers:[1],
301
+ }),
302
+ ]
303
+
304
+ const ready = findReadyTickets(tickets)
305
+ t.equal(ready.length, 0, 'circular dep deadlock')
306
+ })
307
+
308
+ // -- markDone --
309
+
310
+ test('markDone updates file and ticket object', t => {
311
+ const dir = tmp()
312
+ const filepath = join(dir, '01-test.md')
313
+ const body = makeTicketBody({
314
+ status:'ready-for-agent',
315
+ })
316
+ writeFileSync(filepath, body)
317
+
318
+ const ticket:Ticket = {
319
+ num:1,
320
+ slug:'test',
321
+ filename:'01-test.md',
322
+ filepath,
323
+ body,
324
+ status:'ready-for-agent',
325
+ blockers:[],
326
+ }
327
+
328
+ markDone(ticket)
329
+
330
+ t.equal(ticket.status, 'done', 'in-memory status')
331
+ const onDisk = readFileSync(filepath, 'utf8')
332
+ t.ok(
333
+ onDisk.includes('**Status:** done'),
334
+ 'file updated on disk'
335
+ )
336
+ t.ok(
337
+ !onDisk.includes('ready-for-agent'),
338
+ 'old status removed'
339
+ )
340
+ })
341
+
342
+ // -- renderPrompt --
343
+
344
+ test('renderPrompt includes spec and ticket', t => {
345
+ const ticket = makeTicket({
346
+ num:1,
347
+ filename:'01-thing.md',
348
+ })
349
+ const prompt = renderPrompt('spec body here', ticket)
350
+ t.ok(
351
+ prompt.includes('spec body here'),
352
+ 'contains spec'
353
+ )
354
+ t.ok(
355
+ prompt.includes('01-thing.md'),
356
+ 'contains ticket filename'
357
+ )
358
+ t.ok(
359
+ prompt.includes('AGENT INSTRUCTIONS'),
360
+ 'contains instructions'
361
+ )
362
+ })
363
+
364
+ // -- runWaves (integration) --
365
+
366
+ const MOCK_PASS = join(
367
+ process.cwd(), 'test', 'mock-agent-pass.js'
368
+ )
369
+ const MOCK_FAIL = join(
370
+ process.cwd(), 'test', 'mock-agent-fail.js'
371
+ )
372
+
373
+ test('runWaves completes unblocked tickets', async t => {
374
+ const base = tmp()
375
+ const issuesDir = join(base, 'issues')
376
+ const logsDir = join(base, 'logs')
377
+ mkdirSync(issuesDir)
378
+
379
+ writeFileSync(
380
+ join(issuesDir, '01-first.md'),
381
+ makeTicketBody({
382
+ status:'ready-for-agent',
383
+ })
384
+ )
385
+ writeFileSync(
386
+ join(issuesDir, '02-second.md'),
387
+ makeTicketBody({
388
+ status:'ready-for-agent',
389
+ })
390
+ )
391
+
392
+ const tickets = scanTickets(issuesDir)
393
+
394
+ await runWaves(
395
+ tickets, 'test spec',
396
+ MOCK_PASS, null, 10, logsDir,
397
+ )
398
+
399
+ t.equal(
400
+ tickets[0].status, 'done',
401
+ 'ticket 1 done'
402
+ )
403
+ t.equal(
404
+ tickets[1].status, 'done',
405
+ 'ticket 2 done'
406
+ )
407
+
408
+ const onDisk = readFileSync(
409
+ join(issuesDir, '01-first.md'), 'utf8'
410
+ )
411
+ t.ok(
412
+ onDisk.includes('**Status:** done'),
413
+ 'ticket 1 persisted to disk'
414
+ )
415
+ })
416
+
417
+ test('runWaves respects dependency order', async t => {
418
+ const base = tmp()
419
+ const issuesDir = join(base, 'issues')
420
+ const logsDir = join(base, 'logs')
421
+ mkdirSync(issuesDir)
422
+
423
+ writeFileSync(
424
+ join(issuesDir, '01-base.md'),
425
+ makeTicketBody({
426
+ status:'ready-for-agent',
427
+ })
428
+ )
429
+ writeFileSync(
430
+ join(issuesDir, '02-depends-on-1.md'),
431
+ makeTicketBody({
432
+ status:'ready-for-agent',
433
+ blockers:[1],
434
+ })
435
+ )
436
+ writeFileSync(
437
+ join(issuesDir, '03-depends-on-2.md'),
438
+ makeTicketBody({
439
+ status:'ready-for-agent',
440
+ blockers:[2],
441
+ })
442
+ )
443
+
444
+ const tickets = scanTickets(issuesDir)
445
+
446
+ await runWaves(
447
+ tickets, 'test spec',
448
+ MOCK_PASS, null, 10, logsDir,
449
+ )
450
+
451
+ for (const ticket of tickets) {
452
+ t.equal(
453
+ ticket.status,
454
+ 'done',
455
+ `${ticket.filename} completed`
456
+ )
457
+ }
458
+ })
459
+
460
+ test('runWaves stops dependents when agent fails', async t => {
461
+ const base = tmp()
462
+ const issuesDir = join(base, 'issues')
463
+ const logsDir = join(base, 'logs')
464
+ mkdirSync(issuesDir)
465
+
466
+ writeFileSync(
467
+ join(issuesDir, '01-will-fail.md'),
468
+ makeTicketBody({
469
+ status:'ready-for-agent',
470
+ })
471
+ )
472
+ writeFileSync(
473
+ join(issuesDir, '02-blocked.md'),
474
+ makeTicketBody({
475
+ status:'ready-for-agent',
476
+ blockers:[1],
477
+ })
478
+ )
479
+
480
+ const tickets = scanTickets(issuesDir)
481
+
482
+ await runWaves(
483
+ tickets, 'test spec',
484
+ MOCK_FAIL, null, 10, logsDir,
485
+ )
486
+
487
+ t.equal(
488
+ tickets[0].status,
489
+ 'ready-for-agent',
490
+ 'failed ticket keeps status'
491
+ )
492
+ t.equal(
493
+ tickets[1].status,
494
+ 'ready-for-agent',
495
+ 'dependent never ran'
496
+ )
497
+ })
498
+
499
+ test('runWaves writes log files', async t => {
500
+ const base = tmp()
501
+ const issuesDir = join(base, 'issues')
502
+ const logsDir = join(base, 'logs')
503
+ mkdirSync(issuesDir)
504
+
505
+ writeFileSync(
506
+ join(issuesDir, '01-task.md'),
507
+ makeTicketBody({
508
+ status:'ready-for-agent',
509
+ })
510
+ )
511
+
512
+ const tickets = scanTickets(issuesDir)
513
+
514
+ await runWaves(
515
+ tickets, 'test spec',
516
+ MOCK_PASS, null, 10, logsDir,
517
+ )
518
+
519
+ t.ok(
520
+ existsSync(join(logsDir, '01-task.log')),
521
+ 'log file created'
522
+ )
523
+ })
524
+
525
+ test('runWaves skips already-done tickets', async t => {
526
+ const base = tmp()
527
+ const issuesDir = join(base, 'issues')
528
+ const logsDir = join(base, 'logs')
529
+ mkdirSync(issuesDir)
530
+
531
+ writeFileSync(
532
+ join(issuesDir, '01-done.md'),
533
+ makeTicketBody({ status:'done' })
534
+ )
535
+ writeFileSync(
536
+ join(issuesDir, '02-ready.md'),
537
+ makeTicketBody({
538
+ status:'ready-for-agent',
539
+ blockers:[1],
540
+ })
541
+ )
542
+
543
+ const tickets = scanTickets(issuesDir)
544
+
545
+ await runWaves(
546
+ tickets, 'test spec',
547
+ MOCK_PASS, null, 10, logsDir,
548
+ )
549
+
550
+ t.equal(
551
+ tickets[0].status,
552
+ 'done',
553
+ 'pre-done ticket unchanged'
554
+ )
555
+ t.equal(
556
+ tickets[1].status,
557
+ 'done',
558
+ 'dependent ran and completed'
559
+ )
560
+ t.ok(
561
+ !existsSync(join(logsDir, '01-done.log')),
562
+ 'no log for skipped ticket'
563
+ )
564
+ })
565
+
566
+ test('runWaves handles parallel independent tickets', async t => {
567
+ const base = tmp()
568
+ const issuesDir = join(base, 'issues')
569
+ const logsDir = join(base, 'logs')
570
+ mkdirSync(issuesDir)
571
+
572
+ writeFileSync(
573
+ join(issuesDir, '01-a.md'),
574
+ makeTicketBody({ status:'ready-for-agent' })
575
+ )
576
+ writeFileSync(
577
+ join(issuesDir, '02-b.md'),
578
+ makeTicketBody({ status:'ready-for-agent' })
579
+ )
580
+ writeFileSync(
581
+ join(issuesDir, '03-c.md'),
582
+ makeTicketBody({ status:'ready-for-agent' })
583
+ )
584
+ writeFileSync(
585
+ join(issuesDir, '04-final.md'),
586
+ makeTicketBody({
587
+ status:'ready-for-agent',
588
+ blockers:[1, 2, 3],
589
+ })
590
+ )
591
+
592
+ const tickets = scanTickets(issuesDir)
593
+
594
+ await runWaves(
595
+ tickets, 'test spec',
596
+ MOCK_PASS, null, 10, logsDir,
597
+ )
598
+
599
+ for (const ticket of tickets) {
600
+ t.equal(
601
+ ticket.status,
602
+ 'done',
603
+ `${ticket.filename} completed`
604
+ )
605
+ }
606
+
607
+ const logs = readdirSync(logsDir)
608
+ t.equal(logs.length, 4, 'all four logs written')
609
+ })