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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../test/orchestrator.test.ts", "../../src/orchestrator.ts"],
4
+ "sourcesContent": ["import { test } from '@substrate-system/tapzero'\nimport {\n mkdtempSync, mkdirSync, writeFileSync,\n readFileSync, existsSync, readdirSync,\n} from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport {\n parseTicketFilename,\n parseStatus,\n parseBlockers,\n scanTickets,\n findReadyTickets,\n markDone,\n renderPrompt,\n runWaves,\n type Ticket,\n} from '../src/orchestrator.js'\n\n// -- helpers --\n\nfunction tmp ():string {\n return mkdtempSync(\n join(tmpdir(), 'nightralph-orch-test-')\n )\n}\n\nfunction makeTicketBody (opts:{\n status:string\n blockers?:number[]\n}):string {\n const lines = [\n '# Test ticket',\n '',\n `**Status:** ${opts.status}`,\n ]\n if (opts.blockers && opts.blockers.length > 0) {\n lines.push(\n `**Blocked by:** ${opts.blockers.join(', ')}`\n )\n }\n return lines.join('\\n')\n}\n\nfunction makeTicket (overrides:Partial<Ticket>&{\n num:number;\n filename:string;\n}):Ticket {\n return {\n slug:overrides.filename.replace(\n /^\\d+-(.+)\\.md$/, '$1'\n ),\n filepath:`/fake/${overrides.filename}`,\n body:makeTicketBody({\n status:overrides.status ?? 'ready-for-agent',\n }),\n status:overrides.status ?? 'ready-for-agent',\n blockers:overrides.blockers ?? [],\n ...overrides,\n }\n}\n\n// -- parseTicketFilename --\n\ntest('parseTicketFilename parses valid names', t => {\n const r = parseTicketFilename('01-add-auth.md')\n t.ok(r, 'should return a result')\n t.equal(r!.num, 1, 'num')\n t.equal(r!.slug, 'add-auth', 'slug')\n})\n\ntest('parseTicketFilename with multi-digit number', t => {\n const r = parseTicketFilename('123-foo-bar.md')\n t.ok(r, 'should return a result')\n t.equal(r!.num, 123, 'num')\n t.equal(r!.slug, 'foo-bar', 'slug')\n})\n\ntest('parseTicketFilename rejects non-matching names', t => {\n t.equal(\n parseTicketFilename('readme.md'),\n null,\n 'no leading number'\n )\n t.equal(\n parseTicketFilename('01-thing.txt'),\n null,\n 'wrong extension'\n )\n t.equal(\n parseTicketFilename('not-a-ticket'),\n null,\n 'no extension'\n )\n})\n\n// -- parseStatus --\n\ntest('parseStatus extracts status', t => {\n t.equal(\n parseStatus('**Status:** ready-for-agent'),\n 'ready-for-agent',\n 'basic'\n )\n t.equal(\n parseStatus('**Status:** Done '),\n 'done',\n 'trims and lowercases'\n )\n t.equal(\n parseStatus('no status here'),\n '',\n 'missing status returns empty'\n )\n})\n\ntest('parseStatus works in multi-line body', t => {\n const body = [\n '# Ticket',\n '',\n '**Status:** ready-for-agent',\n '**Blocked by:** 1, 2',\n ].join('\\n')\n t.equal(parseStatus(body), 'ready-for-agent')\n})\n\n// -- parseBlockers --\n\ntest('parseBlockers extracts blocker numbers', t => {\n t.deepEqual(\n parseBlockers('**Blocked by:** 1, 2, 3'),\n [1, 2, 3],\n 'comma-separated'\n )\n t.deepEqual(\n parseBlockers('**Blocked by:** #4 and #5'),\n [4, 5],\n 'hash-prefixed'\n )\n})\n\ntest('parseBlockers returns empty for none', t => {\n t.deepEqual(\n parseBlockers('**Blocked by:** none'),\n [],\n 'explicit none'\n )\n t.deepEqual(\n parseBlockers('**Blocked by:** None'),\n [],\n 'case-insensitive none'\n )\n t.deepEqual(\n parseBlockers('no blockers field at all'),\n [],\n 'missing field'\n )\n})\n\n// -- scanTickets --\n\ntest('scanTickets reads and parses ticket files', t => {\n const dir = tmp()\n writeFileSync(\n join(dir, '01-first.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[],\n })\n )\n writeFileSync(\n join(dir, '02-second.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[1],\n })\n )\n writeFileSync(\n join(dir, 'ignore-me.txt'),\n 'not a ticket'\n )\n\n const tickets = scanTickets(dir)\n t.equal(tickets.length, 2, 'finds 2 tickets')\n t.equal(tickets[0].num, 1, 'first ticket num')\n t.equal(tickets[0].slug, 'first', 'first ticket slug')\n t.equal(\n tickets[0].status,\n 'ready-for-agent',\n 'first ticket status'\n )\n t.deepEqual(\n tickets[0].blockers,\n [],\n 'first has no blockers'\n )\n t.equal(tickets[1].num, 2, 'second ticket num')\n t.deepEqual(\n tickets[1].blockers,\n [1],\n 'second blocked by 1'\n )\n})\n\ntest('scanTickets returns sorted by number', t => {\n const dir = tmp()\n writeFileSync(\n join(dir, '10-z.md'),\n makeTicketBody({ status:'ready-for-agent' })\n )\n writeFileSync(\n join(dir, '02-a.md'),\n makeTicketBody({ status:'ready-for-agent' })\n )\n writeFileSync(\n join(dir, '03-b.md'),\n makeTicketBody({ status:'ready-for-agent' })\n )\n\n const tickets = scanTickets(dir)\n t.deepEqual(\n tickets.map(t => t.num),\n [2, 3, 10],\n 'numeric sort'\n )\n})\n\n// -- findReadyTickets --\n\ntest('findReadyTickets returns unblocked ready tickets', t => {\n const tickets:Ticket[] = [\n makeTicket({\n num:1,\n filename:'01-a.md',\n status:'ready-for-agent',\n blockers:[],\n }),\n makeTicket({\n num:2,\n filename:'02-b.md',\n status:'ready-for-agent',\n blockers:[1],\n }),\n makeTicket({\n num:3,\n filename:'03-c.md',\n status:'done',\n blockers:[],\n }),\n ]\n\n const ready = findReadyTickets(tickets)\n t.equal(ready.length, 1, 'only one is ready')\n t.equal(ready[0].num, 1, 'ticket 1 is ready')\n})\n\ntest('findReadyTickets unblocks after deps done', t => {\n const tickets:Ticket[] = [\n makeTicket({\n num:1,\n filename:'01-a.md',\n status:'done',\n blockers:[],\n }),\n makeTicket({\n num:2,\n filename:'02-b.md',\n status:'ready-for-agent',\n blockers:[1],\n }),\n makeTicket({\n num:3,\n filename:'03-c.md',\n status:'ready-for-agent',\n blockers:[1, 2],\n }),\n ]\n\n const ready = findReadyTickets(tickets)\n t.equal(ready.length, 1, 'only ticket 2 is ready')\n t.equal(\n ready[0].num,\n 2,\n 'ticket 2 unblocked because 1 is done'\n )\n})\n\ntest('findReadyTickets returns nothing when all blocked', t => {\n const tickets:Ticket[] = [\n makeTicket({\n num:1,\n filename:'01-a.md',\n status:'ready-for-agent',\n blockers:[2],\n }),\n makeTicket({\n num:2,\n filename:'02-b.md',\n status:'ready-for-agent',\n blockers:[1],\n }),\n ]\n\n const ready = findReadyTickets(tickets)\n t.equal(ready.length, 0, 'circular dep deadlock')\n})\n\n// -- markDone --\n\ntest('markDone updates file and ticket object', t => {\n const dir = tmp()\n const filepath = join(dir, '01-test.md')\n const body = makeTicketBody({\n status:'ready-for-agent',\n })\n writeFileSync(filepath, body)\n\n const ticket:Ticket = {\n num:1,\n slug:'test',\n filename:'01-test.md',\n filepath,\n body,\n status:'ready-for-agent',\n blockers:[],\n }\n\n markDone(ticket)\n\n t.equal(ticket.status, 'done', 'in-memory status')\n const onDisk = readFileSync(filepath, 'utf8')\n t.ok(\n onDisk.includes('**Status:** done'),\n 'file updated on disk'\n )\n t.ok(\n !onDisk.includes('ready-for-agent'),\n 'old status removed'\n )\n})\n\n// -- renderPrompt --\n\ntest('renderPrompt includes spec and ticket', t => {\n const ticket = makeTicket({\n num:1,\n filename:'01-thing.md',\n })\n const prompt = renderPrompt('spec body here', ticket)\n t.ok(\n prompt.includes('spec body here'),\n 'contains spec'\n )\n t.ok(\n prompt.includes('01-thing.md'),\n 'contains ticket filename'\n )\n t.ok(\n prompt.includes('AGENT INSTRUCTIONS'),\n 'contains instructions'\n )\n})\n\n// -- runWaves (integration) --\n\nconst MOCK_PASS = join(\n process.cwd(), 'test', 'mock-agent-pass.js'\n)\nconst MOCK_FAIL = join(\n process.cwd(), 'test', 'mock-agent-fail.js'\n)\n\ntest('runWaves completes unblocked tickets', async t => {\n const base = tmp()\n const issuesDir = join(base, 'issues')\n const logsDir = join(base, 'logs')\n mkdirSync(issuesDir)\n\n writeFileSync(\n join(issuesDir, '01-first.md'),\n makeTicketBody({\n status:'ready-for-agent',\n })\n )\n writeFileSync(\n join(issuesDir, '02-second.md'),\n makeTicketBody({\n status:'ready-for-agent',\n })\n )\n\n const tickets = scanTickets(issuesDir)\n\n await runWaves(\n tickets, 'test spec',\n MOCK_PASS, null, 10, logsDir,\n )\n\n t.equal(\n tickets[0].status, 'done',\n 'ticket 1 done'\n )\n t.equal(\n tickets[1].status, 'done',\n 'ticket 2 done'\n )\n\n const onDisk = readFileSync(\n join(issuesDir, '01-first.md'), 'utf8'\n )\n t.ok(\n onDisk.includes('**Status:** done'),\n 'ticket 1 persisted to disk'\n )\n})\n\ntest('runWaves respects dependency order', async t => {\n const base = tmp()\n const issuesDir = join(base, 'issues')\n const logsDir = join(base, 'logs')\n mkdirSync(issuesDir)\n\n writeFileSync(\n join(issuesDir, '01-base.md'),\n makeTicketBody({\n status:'ready-for-agent',\n })\n )\n writeFileSync(\n join(issuesDir, '02-depends-on-1.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[1],\n })\n )\n writeFileSync(\n join(issuesDir, '03-depends-on-2.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[2],\n })\n )\n\n const tickets = scanTickets(issuesDir)\n\n await runWaves(\n tickets, 'test spec',\n MOCK_PASS, null, 10, logsDir,\n )\n\n for (const ticket of tickets) {\n t.equal(\n ticket.status,\n 'done',\n `${ticket.filename} completed`\n )\n }\n})\n\ntest('runWaves stops dependents when agent fails', async t => {\n const base = tmp()\n const issuesDir = join(base, 'issues')\n const logsDir = join(base, 'logs')\n mkdirSync(issuesDir)\n\n writeFileSync(\n join(issuesDir, '01-will-fail.md'),\n makeTicketBody({\n status:'ready-for-agent',\n })\n )\n writeFileSync(\n join(issuesDir, '02-blocked.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[1],\n })\n )\n\n const tickets = scanTickets(issuesDir)\n\n await runWaves(\n tickets, 'test spec',\n MOCK_FAIL, null, 10, logsDir,\n )\n\n t.equal(\n tickets[0].status,\n 'ready-for-agent',\n 'failed ticket keeps status'\n )\n t.equal(\n tickets[1].status,\n 'ready-for-agent',\n 'dependent never ran'\n )\n})\n\ntest('runWaves writes log files', async t => {\n const base = tmp()\n const issuesDir = join(base, 'issues')\n const logsDir = join(base, 'logs')\n mkdirSync(issuesDir)\n\n writeFileSync(\n join(issuesDir, '01-task.md'),\n makeTicketBody({\n status:'ready-for-agent',\n })\n )\n\n const tickets = scanTickets(issuesDir)\n\n await runWaves(\n tickets, 'test spec',\n MOCK_PASS, null, 10, logsDir,\n )\n\n t.ok(\n existsSync(join(logsDir, '01-task.log')),\n 'log file created'\n )\n})\n\ntest('runWaves skips already-done tickets', async t => {\n const base = tmp()\n const issuesDir = join(base, 'issues')\n const logsDir = join(base, 'logs')\n mkdirSync(issuesDir)\n\n writeFileSync(\n join(issuesDir, '01-done.md'),\n makeTicketBody({ status:'done' })\n )\n writeFileSync(\n join(issuesDir, '02-ready.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[1],\n })\n )\n\n const tickets = scanTickets(issuesDir)\n\n await runWaves(\n tickets, 'test spec',\n MOCK_PASS, null, 10, logsDir,\n )\n\n t.equal(\n tickets[0].status,\n 'done',\n 'pre-done ticket unchanged'\n )\n t.equal(\n tickets[1].status,\n 'done',\n 'dependent ran and completed'\n )\n t.ok(\n !existsSync(join(logsDir, '01-done.log')),\n 'no log for skipped ticket'\n )\n})\n\ntest('runWaves handles parallel independent tickets', async t => {\n const base = tmp()\n const issuesDir = join(base, 'issues')\n const logsDir = join(base, 'logs')\n mkdirSync(issuesDir)\n\n writeFileSync(\n join(issuesDir, '01-a.md'),\n makeTicketBody({ status:'ready-for-agent' })\n )\n writeFileSync(\n join(issuesDir, '02-b.md'),\n makeTicketBody({ status:'ready-for-agent' })\n )\n writeFileSync(\n join(issuesDir, '03-c.md'),\n makeTicketBody({ status:'ready-for-agent' })\n )\n writeFileSync(\n join(issuesDir, '04-final.md'),\n makeTicketBody({\n status:'ready-for-agent',\n blockers:[1, 2, 3],\n })\n )\n\n const tickets = scanTickets(issuesDir)\n\n await runWaves(\n tickets, 'test spec',\n MOCK_PASS, null, 10, logsDir,\n )\n\n for (const ticket of tickets) {\n t.equal(\n ticket.status,\n 'done',\n `${ticket.filename} completed`\n )\n }\n\n const logs = readdirSync(logsDir)\n t.equal(logs.length, 4, 'all four logs written')\n})\n", "import { spawn } from 'node:child_process'\nimport {\n createWriteStream,\n mkdirSync,\n readFileSync,\n writeFileSync,\n readdirSync,\n} from 'node:fs'\nimport { join } from 'node:path'\n\nexport type Ticket = {\n num:number\n slug:string\n filename:string\n filepath:string\n body:string\n status:string\n blockers:number[]\n}\n\nexport function formatErrorMessage (error:unknown):string {\n return error instanceof Error ?\n error.message :\n String(error)\n}\n\nexport const TICKET_RE = /^(\\d+)-(.+)\\.md$/\n\nexport function parseTicketFilename (\n filename:string\n):{ num:number; slug:string }|null {\n const m = filename.match(TICKET_RE)\n if (!m) return null\n return { num:Number(m[1]), slug:m[2] }\n}\n\nexport function parseStatus (body:string):string {\n const m = body.match(/\\*\\*Status:\\*\\*\\s*(.+)/i)\n return m ? m[1].trim().toLowerCase() : ''\n}\n\nexport function parseBlockers (body:string):number[] {\n const m = body.match(/\\*\\*Blocked by:\\*\\*\\s*(.+)/i)\n if (!m) return []\n const text = m[1]\n if (/none/i.test(text)) return []\n const nums:number[] = []\n const re = /(\\d+)/g\n let match\n while ((match = re.exec(text))) {\n nums.push(Number(match[1]))\n }\n return nums\n}\n\nexport function scanTickets (dir:string):Ticket[] {\n const files = readdirSync(dir)\n .filter(f => TICKET_RE.test(f))\n .sort((a, b) => a.localeCompare(\n b,\n undefined,\n { numeric:true, sensitivity:'base' }\n ))\n\n return files.map(filename => {\n const parsed = parseTicketFilename(filename)!\n const filepath = join(dir, filename)\n const body = readFileSync(filepath, 'utf8')\n return {\n num:parsed.num,\n slug:parsed.slug,\n filename,\n filepath,\n body,\n status:parseStatus(body),\n blockers:parseBlockers(body),\n }\n })\n}\n\nexport function findReadyTickets (\n tickets:ReadonlyArray<Ticket>\n):Ticket[] {\n const doneNums = new Set(\n tickets\n .filter(t => t.status !== 'ready-for-agent')\n .map(t => t.num)\n )\n return tickets.filter(t => {\n if (t.status !== 'ready-for-agent') return false\n return t.blockers.every(b => doneNums.has(b))\n })\n}\n\nexport function markDone (ticket:Ticket):void {\n const updated = ticket.body.replace(\n /\\*\\*Status:\\*\\*\\s*.+/i,\n '**Status:** done'\n )\n writeFileSync(ticket.filepath, updated)\n ticket.status = 'done'\n ticket.body = updated\n}\n\nexport function renderPrompt (\n specBody:string,\n ticket:Ticket\n):string {\n return [\n '---- SPEC CONTEXT ----',\n specBody,\n '',\n '---- TICKET ----',\n ticket.filename,\n ticket.body,\n '',\n '---- AGENT INSTRUCTIONS ----',\n 'You are an autonomous coding agent. ' +\n 'Implement the ticket.',\n '',\n ].join('\\n')\n}\n\nexport function spawnAgent (opts:{\n prompt:string\n agentCmd:string\n model:string|null\n timeoutSecs:number\n logPath:string\n}):Promise<number> {\n const args:string[] = []\n if (opts.model) {\n args.push('--model', opts.model)\n }\n\n const logStream = createWriteStream(\n opts.logPath,\n { flags:'w' }\n )\n const proc = spawn(opts.agentCmd, args, {\n stdio:['pipe', 'pipe', 'pipe'],\n })\n\n proc.stdout.pipe(logStream)\n proc.stderr.pipe(logStream)\n\n const timeout = setTimeout(() => {\n if (!proc.killed) {\n console.warn(\n 'Agent did not exit in ' +\n `${opts.timeoutSecs}s; killing.`\n )\n proc.kill('SIGKILL')\n }\n }, opts.timeoutSecs * 1000)\n\n proc.stdin.write(opts.prompt)\n proc.stdin.end()\n\n return new Promise<number>(resolve => {\n proc.on('close', code => {\n clearTimeout(timeout)\n logStream.end()\n resolve(code ?? 1)\n })\n proc.on('error', err => {\n clearTimeout(timeout)\n logStream.end()\n console.error(\n 'Agent process error:',\n formatErrorMessage(err)\n )\n resolve(1)\n })\n })\n}\n\nexport function dryRun (\n tickets:Ticket[],\n specBody:string\n):void {\n console.log('\\nTickets:')\n for (const t of tickets) {\n const blockerStr = t.blockers.length > 0 ?\n `blocked by [${t.blockers.join(', ')}]` :\n 'no blockers'\n console.log(\n ` ${t.filename} ` +\n `(${t.status}) ${blockerStr}`\n )\n }\n\n console.log('\\nSimulated wave order:')\n const sim = new Map(\n tickets.map(t => [t.num, t.status])\n )\n let wave = 1\n while (true) {\n const doneNums = new Set(\n [...sim.entries()]\n .filter(([, s]) => s !== 'ready-for-agent')\n .map(([n]) => n)\n )\n const ready = tickets.filter(t => {\n if (sim.get(t.num) !== 'ready-for-agent') {\n return false\n }\n return t.blockers.every(\n b => doneNums.has(b)\n )\n })\n if (ready.length === 0) break\n console.log(\n ` Wave ${wave}: ` +\n ready.map(t => t.filename).join(', ')\n )\n for (const t of ready) {\n sim.set(t.num, 'done')\n }\n wave++\n }\n\n const stuck = tickets.filter(\n t => sim.get(t.num) === 'ready-for-agent'\n )\n if (stuck.length > 0) {\n console.log(\n '\\nDeadlocked tickets: ' +\n stuck.map(t => t.filename).join(', ')\n )\n }\n\n const firstReady = findReadyTickets(tickets)\n if (firstReady.length > 0) {\n console.log(\n '\\nPrompt for first ready ticket ' +\n `(${firstReady[0].filename}):\\n`\n )\n console.log(\n renderPrompt(specBody, firstReady[0])\n )\n }\n}\n\nexport async function runWaves (\n tickets:Ticket[],\n specBody:string,\n agentCmd:string,\n model:string|null,\n timeoutSecs:number,\n logsDir:string,\n):Promise<void> {\n mkdirSync(logsDir, { recursive:true })\n\n let totalCompleted = 0\n let waveNum = 0\n\n while (true) {\n const ready = findReadyTickets(tickets)\n if (ready.length === 0) break\n\n waveNum++\n console.log(\n `\\nWave ${waveNum}: ` +\n ready.map(t => t.filename).join(', ')\n )\n\n const results = await Promise.allSettled(\n ready.map(async t => {\n const prompt = renderPrompt(specBody, t)\n const logPath = join(\n logsDir,\n t.filename.replace(/\\.md$/, '.log')\n )\n console.log(` Starting ${t.filename}`)\n const code = await spawnAgent({\n prompt,\n agentCmd,\n model,\n timeoutSecs,\n logPath,\n })\n return { ticket:t, exitCode:code }\n })\n )\n\n let waveCompleted = 0\n for (const r of results) {\n if (r.status === 'rejected') continue\n const { ticket, exitCode } = r.value\n if (exitCode === 0) {\n markDone(ticket)\n waveCompleted++\n totalCompleted++\n console.log(\n ` ${ticket.filename}: done`\n )\n } else {\n console.log(\n ` ${ticket.filename}: ` +\n `agent exited ${exitCode}`\n )\n }\n }\n\n if (waveCompleted === 0) {\n console.log(\n '\\nNo tickets completed this wave. ' +\n 'Stopping.'\n )\n break\n }\n }\n\n const remaining = tickets.filter(\n t => t.status === 'ready-for-agent'\n )\n console.log(\n `\\nFinished: ${totalCompleted} completed` +\n (remaining.length > 0 ?\n `, ${remaining.length} remaining` :\n '')\n )\n}\n"],
5
+ "mappings": ";AAAA,SAAS,YAAY;AACrB;AAAA,EACI;AAAA,EAAa,aAAAA;AAAA,EAAW,iBAAAC;AAAA,EACxB,gBAAAC;AAAA,EAAc;AAAA,EAAY,eAAAC;AAAA,OACvB;AACP,SAAS,QAAAC,aAAY;AACrB,SAAS,cAAc;;;ACNvB,SAAS,aAAa;AACtB;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP,SAAS,YAAY;AAYd,SAAS,mBAAoB,OAAsB;AACtD,SAAO,iBAAiB,QACpB,MAAM,UACN,OAAO,KAAK;AACpB;AAEO,IAAM,YAAY;AAElB,SAAS,oBACZ,UAC+B;AAC/B,QAAM,IAAI,SAAS,MAAM,SAAS;AAClC,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,EAAE,KAAI,OAAO,EAAE,CAAC,CAAC,GAAG,MAAK,EAAE,CAAC,EAAE;AACzC;AAEO,SAAS,YAAa,MAAoB;AAC7C,QAAM,IAAI,KAAK,MAAM,yBAAyB;AAC9C,SAAO,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,IAAI;AAC3C;AAEO,SAAS,cAAe,MAAsB;AACjD,QAAM,IAAI,KAAK,MAAM,6BAA6B;AAClD,MAAI,CAAC,EAAG,QAAO,CAAC;AAChB,QAAM,OAAO,EAAE,CAAC;AAChB,MAAI,QAAQ,KAAK,IAAI,EAAG,QAAO,CAAC;AAChC,QAAM,OAAgB,CAAC;AACvB,QAAM,KAAK;AACX,MAAI;AACJ,SAAQ,QAAQ,GAAG,KAAK,IAAI,GAAI;AAC5B,SAAK,KAAK,OAAO,MAAM,CAAC,CAAC,CAAC;AAAA,EAC9B;AACA,SAAO;AACX;AAEO,SAAS,YAAa,KAAqB;AAC9C,QAAM,QAAQ,YAAY,GAAG,EACxB,OAAO,OAAK,UAAU,KAAK,CAAC,CAAC,EAC7B,KAAK,CAAC,GAAG,MAAM,EAAE;AAAA,IACd;AAAA,IACA;AAAA,IACA,EAAE,SAAQ,MAAM,aAAY,OAAO;AAAA,EACvC,CAAC;AAEL,SAAO,MAAM,IAAI,cAAY;AACzB,UAAM,SAAS,oBAAoB,QAAQ;AAC3C,UAAM,WAAW,KAAK,KAAK,QAAQ;AACnC,UAAM,OAAO,aAAa,UAAU,MAAM;AAC1C,WAAO;AAAA,MACH,KAAI,OAAO;AAAA,MACX,MAAK,OAAO;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAO,YAAY,IAAI;AAAA,MACvB,UAAS,cAAc,IAAI;AAAA,IAC/B;AAAA,EACJ,CAAC;AACL;AAEO,SAAS,iBACZ,SACO;AACP,QAAM,WAAW,IAAI;AAAA,IACjB,QACK,OAAO,OAAK,EAAE,WAAW,iBAAiB,EAC1C,IAAI,OAAK,EAAE,GAAG;AAAA,EACvB;AACA,SAAO,QAAQ,OAAO,OAAK;AACvB,QAAI,EAAE,WAAW,kBAAmB,QAAO;AAC3C,WAAO,EAAE,SAAS,MAAM,OAAK,SAAS,IAAI,CAAC,CAAC;AAAA,EAChD,CAAC;AACL;AAEO,SAAS,SAAU,QAAoB;AAC1C,QAAM,UAAU,OAAO,KAAK;AAAA,IACxB;AAAA,IACA;AAAA,EACJ;AACA,gBAAc,OAAO,UAAU,OAAO;AACtC,SAAO,SAAS;AAChB,SAAO,OAAO;AAClB;AAEO,SAAS,aACZ,UACA,QACK;AACL,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IAEA;AAAA,EACJ,EAAE,KAAK,IAAI;AACf;AAEO,SAAS,WAAY,MAMT;AACf,QAAM,OAAgB,CAAC;AACvB,MAAI,KAAK,OAAO;AACZ,SAAK,KAAK,WAAW,KAAK,KAAK;AAAA,EACnC;AAEA,QAAM,YAAY;AAAA,IACd,KAAK;AAAA,IACL,EAAE,OAAM,IAAI;AAAA,EAChB;AACA,QAAM,OAAO,MAAM,KAAK,UAAU,MAAM;AAAA,IACpC,OAAM,CAAC,QAAQ,QAAQ,MAAM;AAAA,EACjC,CAAC;AAED,OAAK,OAAO,KAAK,SAAS;AAC1B,OAAK,OAAO,KAAK,SAAS;AAE1B,QAAM,UAAU,WAAW,MAAM;AAC7B,QAAI,CAAC,KAAK,QAAQ;AACd,cAAQ;AAAA,QACJ,yBACG,KAAK,WAAW;AAAA,MACvB;AACA,WAAK,KAAK,SAAS;AAAA,IACvB;AAAA,EACJ,GAAG,KAAK,cAAc,GAAI;AAE1B,OAAK,MAAM,MAAM,KAAK,MAAM;AAC5B,OAAK,MAAM,IAAI;AAEf,SAAO,IAAI,QAAgB,aAAW;AAClC,SAAK,GAAG,SAAS,UAAQ;AACrB,mBAAa,OAAO;AACpB,gBAAU,IAAI;AACd,cAAQ,QAAQ,CAAC;AAAA,IACrB,CAAC;AACD,SAAK,GAAG,SAAS,SAAO;AACpB,mBAAa,OAAO;AACpB,gBAAU,IAAI;AACd,cAAQ;AAAA,QACJ;AAAA,QACA,mBAAmB,GAAG;AAAA,MAC1B;AACA,cAAQ,CAAC;AAAA,IACb,CAAC;AAAA,EACL,CAAC;AACL;AAqEA,eAAsB,SAClB,SACA,UACA,UACA,OACA,aACA,SACY;AACZ,YAAU,SAAS,EAAE,WAAU,KAAK,CAAC;AAErC,MAAI,iBAAiB;AACrB,MAAI,UAAU;AAEd,SAAO,MAAM;AACT,UAAM,QAAQ,iBAAiB,OAAO;AACtC,QAAI,MAAM,WAAW,EAAG;AAExB;AACA,YAAQ;AAAA,MACJ;AAAA,OAAU,OAAO,OACjB,MAAM,IAAI,OAAK,EAAE,QAAQ,EAAE,KAAK,IAAI;AAAA,IACxC;AAEA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC1B,MAAM,IAAI,OAAM,MAAK;AACjB,cAAM,SAAS,aAAa,UAAU,CAAC;AACvC,cAAM,UAAU;AAAA,UACZ;AAAA,UACA,EAAE,SAAS,QAAQ,SAAS,MAAM;AAAA,QACtC;AACA,gBAAQ,IAAI,cAAc,EAAE,QAAQ,EAAE;AACtC,cAAM,OAAO,MAAM,WAAW;AAAA,UAC1B;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACJ,CAAC;AACD,eAAO,EAAE,QAAO,GAAG,UAAS,KAAK;AAAA,MACrC,CAAC;AAAA,IACL;AAEA,QAAI,gBAAgB;AACpB,eAAW,KAAK,SAAS;AACrB,UAAI,EAAE,WAAW,WAAY;AAC7B,YAAM,EAAE,QAAQ,SAAS,IAAI,EAAE;AAC/B,UAAI,aAAa,GAAG;AAChB,iBAAS,MAAM;AACf;AACA;AACA,gBAAQ;AAAA,UACJ,KAAK,OAAO,QAAQ;AAAA,QACxB;AAAA,MACJ,OAAO;AACH,gBAAQ;AAAA,UACJ,KAAK,OAAO,QAAQ,kBACJ,QAAQ;AAAA,QAC5B;AAAA,MACJ;AAAA,IACJ;AAEA,QAAI,kBAAkB,GAAG;AACrB,cAAQ;AAAA,QACJ;AAAA,MAEJ;AACA;AAAA,IACJ;AAAA,EACJ;AAEA,QAAM,YAAY,QAAQ;AAAA,IACtB,OAAK,EAAE,WAAW;AAAA,EACtB;AACA,UAAQ;AAAA,IACJ;AAAA,YAAe,cAAc,gBAC5B,UAAU,SAAS,IAChB,KAAK,UAAU,MAAM,eACrB;AAAA,EACR;AACJ;;;AD9SA,SAAS,MAAc;AACnB,SAAO;AAAA,IACHC,MAAK,OAAO,GAAG,uBAAuB;AAAA,EAC1C;AACJ;AAEA,SAAS,eAAgB,MAGf;AACN,QAAM,QAAQ;AAAA,IACV;AAAA,IACA;AAAA,IACA,eAAe,KAAK,MAAM;AAAA,EAC9B;AACA,MAAI,KAAK,YAAY,KAAK,SAAS,SAAS,GAAG;AAC3C,UAAM;AAAA,MACF,mBAAmB,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,IAC/C;AAAA,EACJ;AACA,SAAO,MAAM,KAAK,IAAI;AAC1B;AAEA,SAAS,WAAY,WAGX;AACN,SAAO;AAAA,IACH,MAAK,UAAU,SAAS;AAAA,MACpB;AAAA,MAAkB;AAAA,IACtB;AAAA,IACA,UAAS,SAAS,UAAU,QAAQ;AAAA,IACpC,MAAK,eAAe;AAAA,MAChB,QAAO,UAAU,UAAU;AAAA,IAC/B,CAAC;AAAA,IACD,QAAO,UAAU,UAAU;AAAA,IAC3B,UAAS,UAAU,YAAY,CAAC;AAAA,IAChC,GAAG;AAAA,EACP;AACJ;AAIA,KAAK,0CAA0C,OAAK;AAChD,QAAM,IAAI,oBAAoB,gBAAgB;AAC9C,IAAE,GAAG,GAAG,wBAAwB;AAChC,IAAE,MAAM,EAAG,KAAK,GAAG,KAAK;AACxB,IAAE,MAAM,EAAG,MAAM,YAAY,MAAM;AACvC,CAAC;AAED,KAAK,+CAA+C,OAAK;AACrD,QAAM,IAAI,oBAAoB,gBAAgB;AAC9C,IAAE,GAAG,GAAG,wBAAwB;AAChC,IAAE,MAAM,EAAG,KAAK,KAAK,KAAK;AAC1B,IAAE,MAAM,EAAG,MAAM,WAAW,MAAM;AACtC,CAAC;AAED,KAAK,kDAAkD,OAAK;AACxD,IAAE;AAAA,IACE,oBAAoB,WAAW;AAAA,IAC/B;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,oBAAoB,cAAc;AAAA,IAClC;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,oBAAoB,cAAc;AAAA,IAClC;AAAA,IACA;AAAA,EACJ;AACJ,CAAC;AAID,KAAK,+BAA+B,OAAK;AACrC,IAAE;AAAA,IACE,YAAY,6BAA6B;AAAA,IACzC;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,YAAY,sBAAsB;AAAA,IAClC;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,YAAY,gBAAgB;AAAA,IAC5B;AAAA,IACA;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,wCAAwC,OAAK;AAC9C,QAAM,OAAO;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ,EAAE,KAAK,IAAI;AACX,IAAE,MAAM,YAAY,IAAI,GAAG,iBAAiB;AAChD,CAAC;AAID,KAAK,0CAA0C,OAAK;AAChD,IAAE;AAAA,IACE,cAAc,yBAAyB;AAAA,IACvC,CAAC,GAAG,GAAG,CAAC;AAAA,IACR;AAAA,EACJ;AACA,IAAE;AAAA,IACE,cAAc,2BAA2B;AAAA,IACzC,CAAC,GAAG,CAAC;AAAA,IACL;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,wCAAwC,OAAK;AAC9C,IAAE;AAAA,IACE,cAAc,sBAAsB;AAAA,IACpC,CAAC;AAAA,IACD;AAAA,EACJ;AACA,IAAE;AAAA,IACE,cAAc,sBAAsB;AAAA,IACpC,CAAC;AAAA,IACD;AAAA,EACJ;AACA,IAAE;AAAA,IACE,cAAc,0BAA0B;AAAA,IACxC,CAAC;AAAA,IACD;AAAA,EACJ;AACJ,CAAC;AAID,KAAK,6CAA6C,OAAK;AACnD,QAAM,MAAM,IAAI;AAChB,EAAAC;AAAA,IACID,MAAK,KAAK,aAAa;AAAA,IACvB,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC;AAAA,IACd,CAAC;AAAA,EACL;AACA,EAAAC;AAAA,IACID,MAAK,KAAK,cAAc;AAAA,IACxB,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,EACL;AACA,EAAAC;AAAA,IACID,MAAK,KAAK,eAAe;AAAA,IACzB;AAAA,EACJ;AAEA,QAAM,UAAU,YAAY,GAAG;AAC/B,IAAE,MAAM,QAAQ,QAAQ,GAAG,iBAAiB;AAC5C,IAAE,MAAM,QAAQ,CAAC,EAAE,KAAK,GAAG,kBAAkB;AAC7C,IAAE,MAAM,QAAQ,CAAC,EAAE,MAAM,SAAS,mBAAmB;AACrD,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX,CAAC;AAAA,IACD;AAAA,EACJ;AACA,IAAE,MAAM,QAAQ,CAAC,EAAE,KAAK,GAAG,mBAAmB;AAC9C,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX,CAAC,CAAC;AAAA,IACF;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,wCAAwC,OAAK;AAC9C,QAAM,MAAM,IAAI;AAChB,EAAAC;AAAA,IACID,MAAK,KAAK,SAAS;AAAA,IACnB,eAAe,EAAE,QAAO,kBAAkB,CAAC;AAAA,EAC/C;AACA,EAAAC;AAAA,IACID,MAAK,KAAK,SAAS;AAAA,IACnB,eAAe,EAAE,QAAO,kBAAkB,CAAC;AAAA,EAC/C;AACA,EAAAC;AAAA,IACID,MAAK,KAAK,SAAS;AAAA,IACnB,eAAe,EAAE,QAAO,kBAAkB,CAAC;AAAA,EAC/C;AAEA,QAAM,UAAU,YAAY,GAAG;AAC/B,IAAE;AAAA,IACE,QAAQ,IAAI,CAAAE,OAAKA,GAAE,GAAG;AAAA,IACtB,CAAC,GAAG,GAAG,EAAE;AAAA,IACT;AAAA,EACJ;AACJ,CAAC;AAID,KAAK,oDAAoD,OAAK;AAC1D,QAAM,UAAmB;AAAA,IACrB,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC;AAAA,IACd,CAAC;AAAA,IACD,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,IACD,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC;AAAA,IACd,CAAC;AAAA,EACL;AAEA,QAAM,QAAQ,iBAAiB,OAAO;AACtC,IAAE,MAAM,MAAM,QAAQ,GAAG,mBAAmB;AAC5C,IAAE,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,mBAAmB;AAChD,CAAC;AAED,KAAK,6CAA6C,OAAK;AACnD,QAAM,UAAmB;AAAA,IACrB,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC;AAAA,IACd,CAAC;AAAA,IACD,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,IACD,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC,GAAG,CAAC;AAAA,IAClB,CAAC;AAAA,EACL;AAEA,QAAM,QAAQ,iBAAiB,OAAO;AACtC,IAAE,MAAM,MAAM,QAAQ,GAAG,wBAAwB;AACjD,IAAE;AAAA,IACE,MAAM,CAAC,EAAE;AAAA,IACT;AAAA,IACA;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,qDAAqD,OAAK;AAC3D,QAAM,UAAmB;AAAA,IACrB,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,IACD,WAAW;AAAA,MACP,KAAI;AAAA,MACJ,UAAS;AAAA,MACT,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,EACL;AAEA,QAAM,QAAQ,iBAAiB,OAAO;AACtC,IAAE,MAAM,MAAM,QAAQ,GAAG,uBAAuB;AACpD,CAAC;AAID,KAAK,2CAA2C,OAAK;AACjD,QAAM,MAAM,IAAI;AAChB,QAAM,WAAWF,MAAK,KAAK,YAAY;AACvC,QAAM,OAAO,eAAe;AAAA,IACxB,QAAO;AAAA,EACX,CAAC;AACD,EAAAC,eAAc,UAAU,IAAI;AAE5B,QAAM,SAAgB;AAAA,IAClB,KAAI;AAAA,IACJ,MAAK;AAAA,IACL,UAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA,QAAO;AAAA,IACP,UAAS,CAAC;AAAA,EACd;AAEA,WAAS,MAAM;AAEf,IAAE,MAAM,OAAO,QAAQ,QAAQ,kBAAkB;AACjD,QAAM,SAASE,cAAa,UAAU,MAAM;AAC5C,IAAE;AAAA,IACE,OAAO,SAAS,kBAAkB;AAAA,IAClC;AAAA,EACJ;AACA,IAAE;AAAA,IACE,CAAC,OAAO,SAAS,iBAAiB;AAAA,IAClC;AAAA,EACJ;AACJ,CAAC;AAID,KAAK,yCAAyC,OAAK;AAC/C,QAAM,SAAS,WAAW;AAAA,IACtB,KAAI;AAAA,IACJ,UAAS;AAAA,EACb,CAAC;AACD,QAAM,SAAS,aAAa,kBAAkB,MAAM;AACpD,IAAE;AAAA,IACE,OAAO,SAAS,gBAAgB;AAAA,IAChC;AAAA,EACJ;AACA,IAAE;AAAA,IACE,OAAO,SAAS,aAAa;AAAA,IAC7B;AAAA,EACJ;AACA,IAAE;AAAA,IACE,OAAO,SAAS,oBAAoB;AAAA,IACpC;AAAA,EACJ;AACJ,CAAC;AAID,IAAM,YAAYH;AAAA,EACd,QAAQ,IAAI;AAAA,EAAG;AAAA,EAAQ;AAC3B;AACA,IAAM,YAAYA;AAAA,EACd,QAAQ,IAAI;AAAA,EAAG;AAAA,EAAQ;AAC3B;AAEA,KAAK,wCAAwC,OAAM,MAAK;AACpD,QAAM,OAAO,IAAI;AACjB,QAAM,YAAYA,MAAK,MAAM,QAAQ;AACrC,QAAM,UAAUA,MAAK,MAAM,MAAM;AACjC,EAAAI,WAAU,SAAS;AAEnB,EAAAH;AAAA,IACID,MAAK,WAAW,aAAa;AAAA,IAC7B,eAAe;AAAA,MACX,QAAO;AAAA,IACX,CAAC;AAAA,EACL;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,cAAc;AAAA,IAC9B,eAAe;AAAA,MACX,QAAO;AAAA,IACX,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,YAAY,SAAS;AAErC,QAAM;AAAA,IACF;AAAA,IAAS;AAAA,IACT;AAAA,IAAW;AAAA,IAAM;AAAA,IAAI;AAAA,EACzB;AAEA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IAAQ;AAAA,IACnB;AAAA,EACJ;AACA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IAAQ;AAAA,IACnB;AAAA,EACJ;AAEA,QAAM,SAASG;AAAA,IACXH,MAAK,WAAW,aAAa;AAAA,IAAG;AAAA,EACpC;AACA,IAAE;AAAA,IACE,OAAO,SAAS,kBAAkB;AAAA,IAClC;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,sCAAsC,OAAM,MAAK;AAClD,QAAM,OAAO,IAAI;AACjB,QAAM,YAAYA,MAAK,MAAM,QAAQ;AACrC,QAAM,UAAUA,MAAK,MAAM,MAAM;AACjC,EAAAI,WAAU,SAAS;AAEnB,EAAAH;AAAA,IACID,MAAK,WAAW,YAAY;AAAA,IAC5B,eAAe;AAAA,MACX,QAAO;AAAA,IACX,CAAC;AAAA,EACL;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,oBAAoB;AAAA,IACpC,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,EACL;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,oBAAoB;AAAA,IACpC,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,YAAY,SAAS;AAErC,QAAM;AAAA,IACF;AAAA,IAAS;AAAA,IACT;AAAA,IAAW;AAAA,IAAM;AAAA,IAAI;AAAA,EACzB;AAEA,aAAW,UAAU,SAAS;AAC1B,MAAE;AAAA,MACE,OAAO;AAAA,MACP;AAAA,MACA,GAAG,OAAO,QAAQ;AAAA,IACtB;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,8CAA8C,OAAM,MAAK;AAC1D,QAAM,OAAO,IAAI;AACjB,QAAM,YAAYA,MAAK,MAAM,QAAQ;AACrC,QAAM,UAAUA,MAAK,MAAM,MAAM;AACjC,EAAAI,WAAU,SAAS;AAEnB,EAAAH;AAAA,IACID,MAAK,WAAW,iBAAiB;AAAA,IACjC,eAAe;AAAA,MACX,QAAO;AAAA,IACX,CAAC;AAAA,EACL;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,eAAe;AAAA,IAC/B,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,YAAY,SAAS;AAErC,QAAM;AAAA,IACF;AAAA,IAAS;AAAA,IACT;AAAA,IAAW;AAAA,IAAM;AAAA,IAAI;AAAA,EACzB;AAEA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX;AAAA,IACA;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,6BAA6B,OAAM,MAAK;AACzC,QAAM,OAAO,IAAI;AACjB,QAAM,YAAYA,MAAK,MAAM,QAAQ;AACrC,QAAM,UAAUA,MAAK,MAAM,MAAM;AACjC,EAAAI,WAAU,SAAS;AAEnB,EAAAH;AAAA,IACID,MAAK,WAAW,YAAY;AAAA,IAC5B,eAAe;AAAA,MACX,QAAO;AAAA,IACX,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,YAAY,SAAS;AAErC,QAAM;AAAA,IACF;AAAA,IAAS;AAAA,IACT;AAAA,IAAW;AAAA,IAAM;AAAA,IAAI;AAAA,EACzB;AAEA,IAAE;AAAA,IACE,WAAWA,MAAK,SAAS,aAAa,CAAC;AAAA,IACvC;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,uCAAuC,OAAM,MAAK;AACnD,QAAM,OAAO,IAAI;AACjB,QAAM,YAAYA,MAAK,MAAM,QAAQ;AACrC,QAAM,UAAUA,MAAK,MAAM,MAAM;AACjC,EAAAI,WAAU,SAAS;AAEnB,EAAAH;AAAA,IACID,MAAK,WAAW,YAAY;AAAA,IAC5B,eAAe,EAAE,QAAO,OAAO,CAAC;AAAA,EACpC;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,aAAa;AAAA,IAC7B,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC,CAAC;AAAA,IACf,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,YAAY,SAAS;AAErC,QAAM;AAAA,IACF;AAAA,IAAS;AAAA,IACT;AAAA,IAAW;AAAA,IAAM;AAAA,IAAI;AAAA,EACzB;AAEA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,QAAQ,CAAC,EAAE;AAAA,IACX;AAAA,IACA;AAAA,EACJ;AACA,IAAE;AAAA,IACE,CAAC,WAAWA,MAAK,SAAS,aAAa,CAAC;AAAA,IACxC;AAAA,EACJ;AACJ,CAAC;AAED,KAAK,iDAAiD,OAAM,MAAK;AAC7D,QAAM,OAAO,IAAI;AACjB,QAAM,YAAYA,MAAK,MAAM,QAAQ;AACrC,QAAM,UAAUA,MAAK,MAAM,MAAM;AACjC,EAAAI,WAAU,SAAS;AAEnB,EAAAH;AAAA,IACID,MAAK,WAAW,SAAS;AAAA,IACzB,eAAe,EAAE,QAAO,kBAAkB,CAAC;AAAA,EAC/C;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,SAAS;AAAA,IACzB,eAAe,EAAE,QAAO,kBAAkB,CAAC;AAAA,EAC/C;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,SAAS;AAAA,IACzB,eAAe,EAAE,QAAO,kBAAkB,CAAC;AAAA,EAC/C;AACA,EAAAC;AAAA,IACID,MAAK,WAAW,aAAa;AAAA,IAC7B,eAAe;AAAA,MACX,QAAO;AAAA,MACP,UAAS,CAAC,GAAG,GAAG,CAAC;AAAA,IACrB,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,YAAY,SAAS;AAErC,QAAM;AAAA,IACF;AAAA,IAAS;AAAA,IACT;AAAA,IAAW;AAAA,IAAM;AAAA,IAAI;AAAA,EACzB;AAEA,aAAW,UAAU,SAAS;AAC1B,MAAE;AAAA,MACE,OAAO;AAAA,MACP;AAAA,MACA,GAAG,OAAO,QAAQ;AAAA,IACtB;AAAA,EACJ;AAEA,QAAM,OAAOK,aAAY,OAAO;AAChC,IAAE,MAAM,KAAK,QAAQ,GAAG,uBAAuB;AACnD,CAAC;",
6
+ "names": ["mkdirSync", "writeFileSync", "readFileSync", "readdirSync", "join", "join", "writeFileSync", "t", "readFileSync", "mkdirSync", "readdirSync"]
7
+ }
@@ -0,0 +1,574 @@
1
+ // test/setup.test.ts
2
+ import { test } from "@substrate-system/tapzero";
3
+ import {
4
+ mkdtempSync,
5
+ mkdirSync as mkdirSync2,
6
+ writeFileSync as writeFileSync2,
7
+ existsSync as existsSync2,
8
+ lstatSync,
9
+ readlinkSync,
10
+ readFileSync as readFileSync2
11
+ } from "node:fs";
12
+ import { join as join2 } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ // src/setup.ts
16
+ import {
17
+ cpSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ readdirSync,
21
+ readFileSync,
22
+ symlinkSync,
23
+ writeFileSync
24
+ } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { execSync } from "node:child_process";
27
+ var SKILL_DIRS = [
28
+ "grill",
29
+ "grilling",
30
+ "domain-modeling",
31
+ "to-spec",
32
+ "to-tickets",
33
+ "tdd"
34
+ ];
35
+ var DOC_TEMPLATES = [
36
+ "triage-labels.md",
37
+ "domain.md"
38
+ ];
39
+ function checkGhAuth() {
40
+ try {
41
+ execSync("gh auth status", { stdio: "ignore" });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ var TRACKER_TEMPLATES = {
48
+ local: "issue-tracker.md",
49
+ github: "issue-tracker-github.md"
50
+ };
51
+ function setup(opts) {
52
+ const {
53
+ bundledSkillsDir,
54
+ docsTemplatesDir,
55
+ projectDir,
56
+ claude,
57
+ tracker
58
+ } = opts;
59
+ const agentsSkillsDir = join(
60
+ projectDir,
61
+ ".agents",
62
+ "skills"
63
+ );
64
+ const upstream = JSON.parse(
65
+ readFileSync(
66
+ join(bundledSkillsDir, "upstream.json"),
67
+ "utf8"
68
+ )
69
+ );
70
+ console.log("Installing skills to .agents/skills/\n");
71
+ for (const skill of SKILL_DIRS) {
72
+ const src = join(bundledSkillsDir, skill);
73
+ const dest = join(agentsSkillsDir, skill);
74
+ mkdirSync(dest, { recursive: true });
75
+ cpSync(src, dest, { recursive: true });
76
+ const files = readdirSync(src);
77
+ console.log(
78
+ ` ${skill}/ (${files.join(", ")})`
79
+ );
80
+ }
81
+ if (claude) {
82
+ const claudeSkillsDir = join(
83
+ projectDir,
84
+ ".claude",
85
+ "skills"
86
+ );
87
+ mkdirSync(claudeSkillsDir, { recursive: true });
88
+ for (const skill of SKILL_DIRS) {
89
+ const linkPath = join(claudeSkillsDir, skill);
90
+ const target = join(
91
+ "..",
92
+ "..",
93
+ ".agents",
94
+ "skills",
95
+ skill
96
+ );
97
+ if (existsSync(linkPath)) continue;
98
+ symlinkSync(target, linkPath);
99
+ }
100
+ console.log(
101
+ "\nCreated symlinks in .claude/skills/"
102
+ );
103
+ }
104
+ const docsAgentsDir = join(
105
+ projectDir,
106
+ "docs",
107
+ "agents"
108
+ );
109
+ mkdirSync(docsAgentsDir, { recursive: true });
110
+ console.log("");
111
+ for (const tmpl of DOC_TEMPLATES) {
112
+ const dest = join(docsAgentsDir, tmpl);
113
+ if (existsSync(dest)) {
114
+ console.log(
115
+ ` docs/agents/${tmpl} (already exists)`
116
+ );
117
+ continue;
118
+ }
119
+ const src = join(docsTemplatesDir, tmpl);
120
+ writeFileSync(dest, readFileSync(src, "utf8"));
121
+ console.log(` docs/agents/${tmpl} (created)`);
122
+ }
123
+ const trackerSrc = join(
124
+ docsTemplatesDir,
125
+ TRACKER_TEMPLATES[tracker]
126
+ );
127
+ const trackerDest = join(docsAgentsDir, "issue-tracker.md");
128
+ writeFileSync(trackerDest, readFileSync(trackerSrc, "utf8"));
129
+ console.log(
130
+ ` docs/agents/issue-tracker.md (${tracker})`
131
+ );
132
+ const trackerLine = tracker === "github" ? "Issues are tracked via GitHub Issues using the `gh` CLI." : "Issues are tracked as local markdown files under `.scratch/`.";
133
+ const agentsMd = [
134
+ "## Agent skills",
135
+ "",
136
+ "### Issue tracker",
137
+ "",
138
+ `${trackerLine} See \`docs/agents/issue-tracker.md\`.`,
139
+ "",
140
+ "### Triage labels",
141
+ "",
142
+ "Default label vocabulary. See `docs/agents/triage-labels.md`.",
143
+ "",
144
+ "### Domain docs",
145
+ "",
146
+ "Single-context layout. See `docs/agents/domain.md`.",
147
+ ""
148
+ ].join("\n");
149
+ writeFileSync(join(projectDir, "AGENTS.md"), agentsMd);
150
+ console.log("\n AGENTS.md (updated)");
151
+ console.log(
152
+ `
153
+ Source: ${upstream.repo} @ ${upstream.commit}`
154
+ );
155
+ }
156
+
157
+ // test/setup.test.ts
158
+ function makeTempProject() {
159
+ const base = mkdtempSync(
160
+ join2(tmpdir(), "nightralph-test-")
161
+ );
162
+ const projectDir = join2(base, "project");
163
+ mkdirSync2(projectDir, { recursive: true });
164
+ const bundledSkillsDir = join2(base, "bundled");
165
+ mkdirSync2(bundledSkillsDir, { recursive: true });
166
+ writeFileSync2(
167
+ join2(bundledSkillsDir, "upstream.json"),
168
+ JSON.stringify({
169
+ repo: "test/repo",
170
+ commit: "abc123"
171
+ })
172
+ );
173
+ for (const skill of SKILL_DIRS) {
174
+ const dir = join2(bundledSkillsDir, skill);
175
+ mkdirSync2(dir, { recursive: true });
176
+ writeFileSync2(
177
+ join2(dir, "SKILL.md"),
178
+ `# ${skill}`
179
+ );
180
+ }
181
+ const docsTemplatesDir = join2(base, "docs-templates");
182
+ mkdirSync2(docsTemplatesDir, { recursive: true });
183
+ for (const tmpl of DOC_TEMPLATES) {
184
+ writeFileSync2(
185
+ join2(docsTemplatesDir, tmpl),
186
+ `# ${tmpl}`
187
+ );
188
+ }
189
+ writeFileSync2(
190
+ join2(docsTemplatesDir, "issue-tracker.md"),
191
+ "# issue-tracker.md"
192
+ );
193
+ writeFileSync2(
194
+ join2(docsTemplatesDir, "issue-tracker-github.md"),
195
+ "# issue-tracker-github.md"
196
+ );
197
+ return { projectDir, bundledSkillsDir, docsTemplatesDir };
198
+ }
199
+ test("setup copies skills to .agents/skills", (t) => {
200
+ const {
201
+ projectDir,
202
+ bundledSkillsDir,
203
+ docsTemplatesDir
204
+ } = makeTempProject();
205
+ setup({
206
+ bundledSkillsDir,
207
+ docsTemplatesDir,
208
+ projectDir,
209
+ tracker: "local"
210
+ });
211
+ for (const skill of SKILL_DIRS) {
212
+ const dest = join2(
213
+ projectDir,
214
+ ".agents",
215
+ "skills",
216
+ skill
217
+ );
218
+ t.ok(
219
+ existsSync2(dest),
220
+ `${skill}/ should exist in .agents/skills`
221
+ );
222
+ t.ok(
223
+ existsSync2(join2(dest, "SKILL.md")),
224
+ `${skill}/SKILL.md should be copied`
225
+ );
226
+ }
227
+ });
228
+ test("setup does NOT create .claude/skills by default", (t) => {
229
+ const {
230
+ projectDir,
231
+ bundledSkillsDir,
232
+ docsTemplatesDir
233
+ } = makeTempProject();
234
+ setup({
235
+ bundledSkillsDir,
236
+ docsTemplatesDir,
237
+ projectDir,
238
+ tracker: "local"
239
+ });
240
+ const claudeSkills = join2(
241
+ projectDir,
242
+ ".claude",
243
+ "skills"
244
+ );
245
+ t.ok(
246
+ !existsSync2(claudeSkills),
247
+ ".claude/skills should not exist without --claude"
248
+ );
249
+ });
250
+ test("setup --claude creates symlinks in .claude/skills", (t) => {
251
+ const {
252
+ projectDir,
253
+ bundledSkillsDir,
254
+ docsTemplatesDir
255
+ } = makeTempProject();
256
+ setup({
257
+ bundledSkillsDir,
258
+ docsTemplatesDir,
259
+ projectDir,
260
+ claude: true,
261
+ tracker: "local"
262
+ });
263
+ for (const skill of SKILL_DIRS) {
264
+ const agentsDest = join2(
265
+ projectDir,
266
+ ".agents",
267
+ "skills",
268
+ skill
269
+ );
270
+ t.ok(
271
+ existsSync2(agentsDest),
272
+ `${skill}/ should exist in .agents/skills`
273
+ );
274
+ const claudeDest = join2(
275
+ projectDir,
276
+ ".claude",
277
+ "skills",
278
+ skill
279
+ );
280
+ t.ok(
281
+ existsSync2(claudeDest),
282
+ `${skill}/ should exist in .claude/skills`
283
+ );
284
+ if (!existsSync2(claudeDest)) {
285
+ t.fail(`symlink missing for ${skill}`);
286
+ continue;
287
+ }
288
+ const stat = lstatSync(claudeDest);
289
+ t.ok(
290
+ stat.isSymbolicLink(),
291
+ `.claude/skills/${skill} should be a symlink`
292
+ );
293
+ const target = readlinkSync(claudeDest);
294
+ const expected = join2(
295
+ "..",
296
+ "..",
297
+ ".agents",
298
+ "skills",
299
+ skill
300
+ );
301
+ t.equal(
302
+ target,
303
+ expected,
304
+ `symlink should point to ${expected}`
305
+ );
306
+ }
307
+ });
308
+ test("setup creates docs/agents/ templates", (t) => {
309
+ const {
310
+ projectDir,
311
+ bundledSkillsDir,
312
+ docsTemplatesDir
313
+ } = makeTempProject();
314
+ setup({
315
+ bundledSkillsDir,
316
+ docsTemplatesDir,
317
+ projectDir,
318
+ tracker: "local"
319
+ });
320
+ for (const tmpl of DOC_TEMPLATES) {
321
+ const dest = join2(
322
+ projectDir,
323
+ "docs",
324
+ "agents",
325
+ tmpl
326
+ );
327
+ t.ok(
328
+ existsSync2(dest),
329
+ `docs/agents/${tmpl} should be created`
330
+ );
331
+ }
332
+ });
333
+ test("setup does not overwrite non-tracker docs", (t) => {
334
+ const {
335
+ projectDir,
336
+ bundledSkillsDir,
337
+ docsTemplatesDir
338
+ } = makeTempProject();
339
+ const triagePath = join2(
340
+ projectDir,
341
+ "docs",
342
+ "agents",
343
+ "triage-labels.md"
344
+ );
345
+ mkdirSync2(join2(projectDir, "docs", "agents"), {
346
+ recursive: true
347
+ });
348
+ writeFileSync2(triagePath, "custom triage content");
349
+ setup({
350
+ bundledSkillsDir,
351
+ docsTemplatesDir,
352
+ projectDir,
353
+ tracker: "local"
354
+ });
355
+ const content = readFileSync2(triagePath, "utf8");
356
+ t.equal(
357
+ content,
358
+ "custom triage content",
359
+ "should preserve existing triage-labels.md"
360
+ );
361
+ });
362
+ test("setup is idempotent", (t) => {
363
+ const {
364
+ projectDir,
365
+ bundledSkillsDir,
366
+ docsTemplatesDir
367
+ } = makeTempProject();
368
+ const opts = {
369
+ bundledSkillsDir,
370
+ docsTemplatesDir,
371
+ projectDir,
372
+ claude: true,
373
+ tracker: "local"
374
+ };
375
+ setup(opts);
376
+ setup(opts);
377
+ for (const skill of SKILL_DIRS) {
378
+ const agentsDest = join2(
379
+ projectDir,
380
+ ".agents",
381
+ "skills",
382
+ skill
383
+ );
384
+ t.ok(
385
+ existsSync2(agentsDest),
386
+ `${skill}/ should still exist after second run`
387
+ );
388
+ const claudeDest = join2(
389
+ projectDir,
390
+ ".claude",
391
+ "skills",
392
+ skill
393
+ );
394
+ if (!existsSync2(claudeDest)) {
395
+ t.fail(`symlink missing for ${skill}`);
396
+ continue;
397
+ }
398
+ const stat = lstatSync(claudeDest);
399
+ t.ok(
400
+ stat.isSymbolicLink(),
401
+ `${skill} symlink should still be valid`
402
+ );
403
+ }
404
+ for (const tmpl of DOC_TEMPLATES) {
405
+ const dest = join2(
406
+ projectDir,
407
+ "docs",
408
+ "agents",
409
+ tmpl
410
+ );
411
+ t.ok(
412
+ existsSync2(dest),
413
+ `docs/agents/${tmpl} should still exist`
414
+ );
415
+ }
416
+ });
417
+ test("checkGhAuth exists and is callable", (t) => {
418
+ t.equal(
419
+ typeof checkGhAuth,
420
+ "function",
421
+ "checkGhAuth should be a function"
422
+ );
423
+ });
424
+ test("setup with tracker:local copies local markdown template", (t) => {
425
+ const {
426
+ projectDir,
427
+ bundledSkillsDir,
428
+ docsTemplatesDir
429
+ } = makeTempProject();
430
+ setup({
431
+ bundledSkillsDir,
432
+ docsTemplatesDir,
433
+ projectDir,
434
+ tracker: "local"
435
+ });
436
+ const trackerPath = join2(
437
+ projectDir,
438
+ "docs",
439
+ "agents",
440
+ "issue-tracker.md"
441
+ );
442
+ t.ok(
443
+ existsSync2(trackerPath),
444
+ "docs/agents/issue-tracker.md should exist"
445
+ );
446
+ const content = readFileSync2(trackerPath, "utf8");
447
+ t.ok(
448
+ content.includes("# issue-tracker.md"),
449
+ "should copy local markdown template"
450
+ );
451
+ });
452
+ test("setup with tracker:github copies github template", (t) => {
453
+ const {
454
+ projectDir,
455
+ bundledSkillsDir,
456
+ docsTemplatesDir
457
+ } = makeTempProject();
458
+ setup({
459
+ bundledSkillsDir,
460
+ docsTemplatesDir,
461
+ projectDir,
462
+ tracker: "github"
463
+ });
464
+ const trackerPath = join2(
465
+ projectDir,
466
+ "docs",
467
+ "agents",
468
+ "issue-tracker.md"
469
+ );
470
+ t.ok(
471
+ existsSync2(trackerPath),
472
+ "docs/agents/issue-tracker.md should exist"
473
+ );
474
+ const content = readFileSync2(trackerPath, "utf8");
475
+ t.ok(
476
+ content.includes("# issue-tracker-github.md"),
477
+ "should copy github template"
478
+ );
479
+ });
480
+ test("setup creates AGENTS.md with local markdown text", (t) => {
481
+ const {
482
+ projectDir,
483
+ bundledSkillsDir,
484
+ docsTemplatesDir
485
+ } = makeTempProject();
486
+ setup({
487
+ bundledSkillsDir,
488
+ docsTemplatesDir,
489
+ projectDir,
490
+ tracker: "local"
491
+ });
492
+ const agentsMdPath = join2(projectDir, "AGENTS.md");
493
+ t.ok(
494
+ existsSync2(agentsMdPath),
495
+ "AGENTS.md should exist"
496
+ );
497
+ const content = readFileSync2(agentsMdPath, "utf8");
498
+ t.ok(
499
+ content.includes("local markdown files"),
500
+ "should mention local markdown"
501
+ );
502
+ });
503
+ test("setup creates AGENTS.md with GitHub Issues text", (t) => {
504
+ const {
505
+ projectDir,
506
+ bundledSkillsDir,
507
+ docsTemplatesDir
508
+ } = makeTempProject();
509
+ setup({
510
+ bundledSkillsDir,
511
+ docsTemplatesDir,
512
+ projectDir,
513
+ tracker: "github"
514
+ });
515
+ const agentsMdPath = join2(projectDir, "AGENTS.md");
516
+ t.ok(
517
+ existsSync2(agentsMdPath),
518
+ "AGENTS.md should exist"
519
+ );
520
+ const content = readFileSync2(agentsMdPath, "utf8");
521
+ t.ok(
522
+ content.includes("GitHub Issues"),
523
+ "should mention GitHub Issues"
524
+ );
525
+ });
526
+ test(
527
+ "tracker template overwrites when switching trackers",
528
+ (t) => {
529
+ const {
530
+ projectDir,
531
+ bundledSkillsDir,
532
+ docsTemplatesDir
533
+ } = makeTempProject();
534
+ setup({
535
+ bundledSkillsDir,
536
+ docsTemplatesDir,
537
+ projectDir,
538
+ tracker: "local"
539
+ });
540
+ const trackerPath = join2(
541
+ projectDir,
542
+ "docs",
543
+ "agents",
544
+ "issue-tracker.md"
545
+ );
546
+ const localContent = readFileSync2(
547
+ trackerPath,
548
+ "utf8"
549
+ );
550
+ t.ok(
551
+ localContent.includes("# issue-tracker.md"),
552
+ "should have local template initially"
553
+ );
554
+ setup({
555
+ bundledSkillsDir,
556
+ docsTemplatesDir,
557
+ projectDir,
558
+ tracker: "github"
559
+ });
560
+ const githubContent = readFileSync2(
561
+ trackerPath,
562
+ "utf8"
563
+ );
564
+ t.ok(
565
+ githubContent.includes("# issue-tracker-github.md"),
566
+ "should overwrite with GitHub template"
567
+ );
568
+ t.ok(
569
+ !githubContent.includes("# issue-tracker.md"),
570
+ "should not contain local template anymore"
571
+ );
572
+ }
573
+ );
574
+ //# sourceMappingURL=setup.test.js.map