@specmarket/cli 0.0.4 → 0.0.6
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.
- package/README.md +1 -1
- package/dist/{chunk-MS2DYACY.js → chunk-OTXWWFAO.js} +42 -3
- package/dist/chunk-OTXWWFAO.js.map +1 -0
- package/dist/{config-R5KWZSJP.js → config-5JMI3YAR.js} +2 -2
- package/dist/index.js +1945 -252
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/comment.test.ts +211 -0
- package/src/commands/comment.ts +176 -0
- package/src/commands/fork.test.ts +163 -0
- package/src/commands/info.test.ts +192 -0
- package/src/commands/info.ts +66 -2
- package/src/commands/init.test.ts +245 -0
- package/src/commands/init.ts +359 -25
- package/src/commands/issues.test.ts +382 -0
- package/src/commands/issues.ts +436 -0
- package/src/commands/login.test.ts +99 -0
- package/src/commands/login.ts +2 -6
- package/src/commands/logout.test.ts +54 -0
- package/src/commands/publish.test.ts +159 -0
- package/src/commands/publish.ts +1 -0
- package/src/commands/report.test.ts +181 -0
- package/src/commands/run.test.ts +419 -0
- package/src/commands/run.ts +71 -3
- package/src/commands/search.test.ts +147 -0
- package/src/commands/validate.test.ts +206 -2
- package/src/commands/validate.ts +315 -192
- package/src/commands/whoami.test.ts +106 -0
- package/src/index.ts +6 -0
- package/src/lib/convex-client.ts +6 -2
- package/src/lib/format-detection.test.ts +223 -0
- package/src/lib/format-detection.ts +172 -0
- package/src/lib/meta-instructions.test.ts +340 -0
- package/src/lib/meta-instructions.ts +562 -0
- package/src/lib/ralph-loop.test.ts +404 -0
- package/src/lib/ralph-loop.ts +501 -95
- package/src/lib/telemetry.ts +7 -1
- package/dist/chunk-MS2DYACY.js.map +0 -1
- /package/dist/{config-R5KWZSJP.js.map → config-5JMI3YAR.js.map} +0 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { readFile, readdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { fileExists, directoryExists } from './format-detection.js';
|
|
5
|
+
import { SIDECAR_FILENAME } from '@specmarket/shared';
|
|
6
|
+
|
|
7
|
+
/** Filename for generated meta-instructions in the run directory */
|
|
8
|
+
export const META_INSTRUCTION_FILENAME = '.specmarket-runner.md';
|
|
9
|
+
|
|
10
|
+
interface SidecarData {
|
|
11
|
+
display_name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
spec_format?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Read display_name and description from specmarket.yaml if present */
|
|
17
|
+
async function readSidecarData(dir: string): Promise<SidecarData> {
|
|
18
|
+
const sidecarPath = join(dir, SIDECAR_FILENAME);
|
|
19
|
+
if (!(await fileExists(sidecarPath))) return {};
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(sidecarPath, 'utf-8');
|
|
22
|
+
const parsed = parseYaml(raw) as unknown;
|
|
23
|
+
if (parsed && typeof parsed === 'object') {
|
|
24
|
+
const d = parsed as Record<string, unknown>;
|
|
25
|
+
return {
|
|
26
|
+
display_name: typeof d['display_name'] === 'string' ? d['display_name'] : undefined,
|
|
27
|
+
description: typeof d['description'] === 'string' ? d['description'] : undefined,
|
|
28
|
+
spec_format: typeof d['spec_format'] === 'string' ? d['spec_format'] : undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Non-fatal — proceed without sidecar data
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return a numerically-sorted list of story-*.md filenames in the directory.
|
|
39
|
+
*
|
|
40
|
+
* Uses numeric sort so story-10.md follows story-9.md, not story-1.md.
|
|
41
|
+
* Extracts the leading number from the filename stem (e.g. "story-02" → 2).
|
|
42
|
+
* Files without a parseable number sort after numbered files, then lexicographically.
|
|
43
|
+
*/
|
|
44
|
+
async function listStoryFiles(dir: string): Promise<string[]> {
|
|
45
|
+
try {
|
|
46
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
47
|
+
return entries
|
|
48
|
+
.filter((e) => e.isFile() && e.name.startsWith('story-') && e.name.endsWith('.md'))
|
|
49
|
+
.map((e) => e.name)
|
|
50
|
+
.sort((a, b) => {
|
|
51
|
+
const numA = parseInt(a.replace(/^story-(\d+).*/, '$1'), 10);
|
|
52
|
+
const numB = parseInt(b.replace(/^story-(\d+).*/, '$1'), 10);
|
|
53
|
+
if (!isNaN(numA) && !isNaN(numB)) return numA - numB;
|
|
54
|
+
if (!isNaN(numA)) return -1;
|
|
55
|
+
if (!isNaN(numB)) return 1;
|
|
56
|
+
return a.localeCompare(b);
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return files from the candidates list that actually exist in dir */
|
|
64
|
+
async function presentFiles(dir: string, candidates: string[]): Promise<string[]> {
|
|
65
|
+
const found: string[] = [];
|
|
66
|
+
for (const f of candidates) {
|
|
67
|
+
if (await fileExists(join(dir, f))) found.push(f);
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Shared preamble block */
|
|
73
|
+
function preamble(sidecar: SidecarData): string {
|
|
74
|
+
const lines: string[] = [
|
|
75
|
+
'# SpecMarket Runner Instructions',
|
|
76
|
+
'',
|
|
77
|
+
'You are an AI coding agent executing a software specification.',
|
|
78
|
+
'Read this file carefully before starting work. Follow the instructions exactly.',
|
|
79
|
+
];
|
|
80
|
+
if (sidecar.display_name) {
|
|
81
|
+
lines.push('', `**Spec:** ${sidecar.display_name}`);
|
|
82
|
+
}
|
|
83
|
+
if (sidecar.description) {
|
|
84
|
+
lines.push(`**Description:** ${sidecar.description}`);
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Shared testing section */
|
|
90
|
+
function testingSection(): string {
|
|
91
|
+
return [
|
|
92
|
+
'## Running Tests',
|
|
93
|
+
'',
|
|
94
|
+
'After completing each major task, run the test suite:',
|
|
95
|
+
'',
|
|
96
|
+
'- `package.json` present → `npm test -- --run` (or `npx vitest run` if vitest is configured)',
|
|
97
|
+
'- `pytest.ini` or `pyproject.toml` present → `python -m pytest`',
|
|
98
|
+
'- `Makefile` with `test` target → `make test`',
|
|
99
|
+
'- No test runner found → verify functionality manually by running the application',
|
|
100
|
+
'',
|
|
101
|
+
'Fix all test failures before proceeding to the next task.',
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Shared completion reminder */
|
|
106
|
+
function completionReminder(): string {
|
|
107
|
+
return [
|
|
108
|
+
'## Important Reminders',
|
|
109
|
+
'',
|
|
110
|
+
'- Only mark a task complete when you have **fully implemented and tested** it.',
|
|
111
|
+
'- Do not skip tasks. Do not leave stubs or placeholders.',
|
|
112
|
+
'- If you discover a blocking issue, note it in the relevant task file and continue with other tasks if possible.',
|
|
113
|
+
'- Run the full test suite one final time before declaring the spec complete.',
|
|
114
|
+
].join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Format-specific generators ──────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generate meta-instructions for the native `specmarket` format.
|
|
121
|
+
*
|
|
122
|
+
* Layout: spec.yaml + PROMPT.md + SPEC.md + SUCCESS_CRITERIA.md + stdlib/
|
|
123
|
+
* Tasks tracked in TASKS.md (or implicitly via SUCCESS_CRITERIA.md checkboxes).
|
|
124
|
+
*/
|
|
125
|
+
async function generateSpecmarketInstructions(
|
|
126
|
+
dir: string,
|
|
127
|
+
sidecar: SidecarData
|
|
128
|
+
): Promise<string> {
|
|
129
|
+
const coreFiles = await presentFiles(dir, [
|
|
130
|
+
'PROMPT.md',
|
|
131
|
+
'SPEC.md',
|
|
132
|
+
'SUCCESS_CRITERIA.md',
|
|
133
|
+
'TASKS.md',
|
|
134
|
+
'spec.yaml',
|
|
135
|
+
'stdlib/STACK.md',
|
|
136
|
+
'stdlib/PATTERNS.md',
|
|
137
|
+
'stdlib/SECURITY.md',
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const fileList = coreFiles.length > 0
|
|
141
|
+
? coreFiles.map((f) => `- \`${f}\``).join('\n')
|
|
142
|
+
: '- *(no standard spec files found — inspect directory contents)*';
|
|
143
|
+
|
|
144
|
+
// Build reading order prose only for files that are actually present
|
|
145
|
+
const readingOrderParts: string[] = [];
|
|
146
|
+
if (coreFiles.includes('PROMPT.md')) {
|
|
147
|
+
readingOrderParts.push('`PROMPT.md` for the high-level goal');
|
|
148
|
+
}
|
|
149
|
+
if (coreFiles.includes('SPEC.md')) {
|
|
150
|
+
readingOrderParts.push('`SPEC.md` for full requirements');
|
|
151
|
+
}
|
|
152
|
+
if (coreFiles.includes('SUCCESS_CRITERIA.md')) {
|
|
153
|
+
readingOrderParts.push('`SUCCESS_CRITERIA.md` to understand exactly what "done" means');
|
|
154
|
+
}
|
|
155
|
+
const readingOrder = readingOrderParts.length > 0
|
|
156
|
+
? `Start with ${readingOrderParts.join(', then ')}.`
|
|
157
|
+
: 'Read all available files to understand the spec requirements.';
|
|
158
|
+
const stackNote = coreFiles.some((f) => f.startsWith('stdlib/'))
|
|
159
|
+
? '\nRead `stdlib/STACK.md` for technology and coding standards before writing any code.'
|
|
160
|
+
: '';
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
preamble(sidecar),
|
|
164
|
+
'',
|
|
165
|
+
'## Spec Format: SpecMarket (Native)',
|
|
166
|
+
'',
|
|
167
|
+
'## Files to Read First',
|
|
168
|
+
'',
|
|
169
|
+
fileList,
|
|
170
|
+
'',
|
|
171
|
+
readingOrder + stackNote,
|
|
172
|
+
'',
|
|
173
|
+
'## Finding Tasks',
|
|
174
|
+
'',
|
|
175
|
+
'Tasks are tracked in `TASKS.md` using this format:',
|
|
176
|
+
'',
|
|
177
|
+
'```',
|
|
178
|
+
'- [ ] Incomplete task',
|
|
179
|
+
'- [x] Completed task',
|
|
180
|
+
'```',
|
|
181
|
+
'',
|
|
182
|
+
coreFiles.includes('SPEC.md')
|
|
183
|
+
? 'If `TASKS.md` does not exist, derive tasks from `SPEC.md` and `SUCCESS_CRITERIA.md`.'
|
|
184
|
+
: 'If `TASKS.md` does not exist, derive tasks from the available spec files.',
|
|
185
|
+
'Work through tasks from top to bottom. Complete each fully before moving to the next.',
|
|
186
|
+
'',
|
|
187
|
+
'## Marking Tasks Complete',
|
|
188
|
+
'',
|
|
189
|
+
'When a task is done, update `TASKS.md`:',
|
|
190
|
+
'- Change `- [ ]` to `- [x]` for the completed task.',
|
|
191
|
+
'',
|
|
192
|
+
'## Completion Criteria',
|
|
193
|
+
'',
|
|
194
|
+
'The run is complete when ALL of the following are true:',
|
|
195
|
+
'',
|
|
196
|
+
'1. **All tasks checked** — No `- [ ]` lines remain in `TASKS.md`.',
|
|
197
|
+
'2. **Tests pass** — Run the test suite; all tests green.',
|
|
198
|
+
'3. **Success criteria met** — Every criterion in `SUCCESS_CRITERIA.md` is checked `- [x]`.',
|
|
199
|
+
' Update each criterion in `SUCCESS_CRITERIA.md` as it is satisfied.',
|
|
200
|
+
'',
|
|
201
|
+
testingSection(),
|
|
202
|
+
'',
|
|
203
|
+
completionReminder(),
|
|
204
|
+
].join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Generate meta-instructions for the `speckit` format.
|
|
209
|
+
*
|
|
210
|
+
* Layout: spec.md + tasks.md (or plan.md) + optional .specify/
|
|
211
|
+
* Tasks tracked in tasks.md or plan.md.
|
|
212
|
+
*/
|
|
213
|
+
async function generateSpeckitInstructions(
|
|
214
|
+
dir: string,
|
|
215
|
+
sidecar: SidecarData
|
|
216
|
+
): Promise<string> {
|
|
217
|
+
const hasTasksMd = await fileExists(join(dir, 'tasks.md'));
|
|
218
|
+
const hasPlanMd = await fileExists(join(dir, 'plan.md'));
|
|
219
|
+
const hasSpecifyDir = await directoryExists(join(dir, '.specify'));
|
|
220
|
+
|
|
221
|
+
const taskFile = hasTasksMd ? 'tasks.md' : hasPlanMd ? 'plan.md' : 'tasks.md';
|
|
222
|
+
|
|
223
|
+
const knownFiles = await presentFiles(dir, [
|
|
224
|
+
'spec.md',
|
|
225
|
+
'tasks.md',
|
|
226
|
+
'plan.md',
|
|
227
|
+
'requirements.md',
|
|
228
|
+
'README.md',
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const fileList = knownFiles.length > 0
|
|
232
|
+
? knownFiles.map((f) => `- \`${f}\``).join('\n')
|
|
233
|
+
: '- `spec.md` — primary specification';
|
|
234
|
+
|
|
235
|
+
const specifyNote = hasSpecifyDir
|
|
236
|
+
? '\nThe `.specify/` directory contains additional context files. Read them for implementation details.\n'
|
|
237
|
+
: '';
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
preamble(sidecar),
|
|
241
|
+
'',
|
|
242
|
+
'## Spec Format: Spec Kit',
|
|
243
|
+
'',
|
|
244
|
+
'## Files to Read First',
|
|
245
|
+
'',
|
|
246
|
+
fileList,
|
|
247
|
+
specifyNote,
|
|
248
|
+
'Start with `spec.md` for the full specification.',
|
|
249
|
+
`Then read \`${taskFile}\` to find your task list.`,
|
|
250
|
+
'',
|
|
251
|
+
'## Finding Tasks',
|
|
252
|
+
'',
|
|
253
|
+
`Tasks are listed in \`${taskFile}\`. Look for checkbox items:`,
|
|
254
|
+
'',
|
|
255
|
+
'```',
|
|
256
|
+
'- [ ] Incomplete task',
|
|
257
|
+
'- [x] Completed task',
|
|
258
|
+
'```',
|
|
259
|
+
'',
|
|
260
|
+
'If the task file uses numbered lists or headings instead of checkboxes,',
|
|
261
|
+
'treat each actionable item as a task. Work through them in order.',
|
|
262
|
+
'',
|
|
263
|
+
'## Marking Tasks Complete',
|
|
264
|
+
'',
|
|
265
|
+
`When a task is done, update \`${taskFile}\`:`,
|
|
266
|
+
'- Change `- [ ]` to `- [x]` for the completed task.',
|
|
267
|
+
'- If using a different format, add `[DONE]` next to the completed item.',
|
|
268
|
+
'',
|
|
269
|
+
'## Completion Criteria',
|
|
270
|
+
'',
|
|
271
|
+
'The run is complete when:',
|
|
272
|
+
'',
|
|
273
|
+
`1. **All tasks checked** — No unchecked \`- [ ]\` items remain in \`${taskFile}\`.`,
|
|
274
|
+
'2. **Tests pass** — Run the test suite; all tests green.',
|
|
275
|
+
'3. **Spec satisfied** — Implementation matches all requirements in `spec.md`.',
|
|
276
|
+
'',
|
|
277
|
+
testingSection(),
|
|
278
|
+
'',
|
|
279
|
+
completionReminder(),
|
|
280
|
+
].join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate meta-instructions for the `bmad` format (BMAD Method).
|
|
285
|
+
*
|
|
286
|
+
* Layout: prd.md + architecture.md + story-*.md files.
|
|
287
|
+
* Each story-*.md is a user story with acceptance criteria.
|
|
288
|
+
*/
|
|
289
|
+
async function generateBmadInstructions(
|
|
290
|
+
dir: string,
|
|
291
|
+
sidecar: SidecarData
|
|
292
|
+
): Promise<string> {
|
|
293
|
+
const storyFiles = await listStoryFiles(dir);
|
|
294
|
+
const coreFiles = await presentFiles(dir, ['prd.md', 'architecture.md', 'epic.md']);
|
|
295
|
+
|
|
296
|
+
const coreFileList = coreFiles.map((f) => `- \`${f}\``).join('\n') || '- *(no core files found)*';
|
|
297
|
+
|
|
298
|
+
const storySection = storyFiles.length > 0
|
|
299
|
+
? [
|
|
300
|
+
'**Story files found:**',
|
|
301
|
+
...storyFiles.map((f) => `- \`${f}\``),
|
|
302
|
+
].join('\n')
|
|
303
|
+
: '*(No story-*.md files found — check prd.md for tasks)*';
|
|
304
|
+
|
|
305
|
+
return [
|
|
306
|
+
preamble(sidecar),
|
|
307
|
+
'',
|
|
308
|
+
'## Spec Format: BMAD Method',
|
|
309
|
+
'',
|
|
310
|
+
'## Files to Read First',
|
|
311
|
+
'',
|
|
312
|
+
coreFileList,
|
|
313
|
+
'',
|
|
314
|
+
'Start with `prd.md` for product requirements. Read `architecture.md` for technical design.',
|
|
315
|
+
'Then read each story file in order.',
|
|
316
|
+
'',
|
|
317
|
+
'## Finding Tasks',
|
|
318
|
+
'',
|
|
319
|
+
'Work is organized as user stories in `story-*.md` files:',
|
|
320
|
+
'',
|
|
321
|
+
storySection,
|
|
322
|
+
'',
|
|
323
|
+
'Each story file contains:',
|
|
324
|
+
'- Story description and goal',
|
|
325
|
+
'- Acceptance criteria (what must be true for the story to be complete)',
|
|
326
|
+
'- Technical notes',
|
|
327
|
+
'',
|
|
328
|
+
'If no story files exist, derive tasks from `prd.md`.',
|
|
329
|
+
'',
|
|
330
|
+
'## Marking Tasks Complete',
|
|
331
|
+
'',
|
|
332
|
+
'For each story:',
|
|
333
|
+
'1. Implement everything required by the acceptance criteria.',
|
|
334
|
+
'2. Mark each acceptance criterion as met in the story file by checking it: `- [ ]` → `- [x]`.',
|
|
335
|
+
'3. Add a `## Status: Done` line at the top of the story file when all criteria are met.',
|
|
336
|
+
'',
|
|
337
|
+
'If working from `prd.md` directly, add a `Done:` checklist section as you complete items.',
|
|
338
|
+
'',
|
|
339
|
+
'## Completion Criteria',
|
|
340
|
+
'',
|
|
341
|
+
'The run is complete when:',
|
|
342
|
+
'',
|
|
343
|
+
'1. **All stories done** — Every `story-*.md` has `Status: Done` and all acceptance criteria checked.',
|
|
344
|
+
' (Or: all tasks in `prd.md` implemented if no story files.)',
|
|
345
|
+
'2. **Tests pass** — Run the test suite; all tests green.',
|
|
346
|
+
'3. **Architecture followed** — Implementation matches the design in `architecture.md`.',
|
|
347
|
+
'',
|
|
348
|
+
testingSection(),
|
|
349
|
+
'',
|
|
350
|
+
completionReminder(),
|
|
351
|
+
].join('\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Generate meta-instructions for the `ralph` format.
|
|
356
|
+
*
|
|
357
|
+
* Layout: prd.json with userStories[] array.
|
|
358
|
+
* Each user story is an atomic unit of work.
|
|
359
|
+
*/
|
|
360
|
+
async function generateRalphInstructions(
|
|
361
|
+
dir: string,
|
|
362
|
+
sidecar: SidecarData
|
|
363
|
+
): Promise<string> {
|
|
364
|
+
// Try to read user story titles from prd.json for a concrete task list
|
|
365
|
+
let userStoryList = '';
|
|
366
|
+
try {
|
|
367
|
+
const raw = await readFile(join(dir, 'prd.json'), 'utf-8');
|
|
368
|
+
const data = JSON.parse(raw) as unknown;
|
|
369
|
+
if (
|
|
370
|
+
data &&
|
|
371
|
+
typeof data === 'object' &&
|
|
372
|
+
'userStories' in data &&
|
|
373
|
+
Array.isArray((data as { userStories: unknown[] }).userStories)
|
|
374
|
+
) {
|
|
375
|
+
const stories = (data as { userStories: Array<Record<string, unknown>> }).userStories;
|
|
376
|
+
const titles = stories
|
|
377
|
+
.map((s, i) => {
|
|
378
|
+
const title =
|
|
379
|
+
typeof s['title'] === 'string'
|
|
380
|
+
? s['title']
|
|
381
|
+
: typeof s['name'] === 'string'
|
|
382
|
+
? s['name']
|
|
383
|
+
: `Story ${i + 1}`;
|
|
384
|
+
return `- [ ] ${title}`;
|
|
385
|
+
})
|
|
386
|
+
.join('\n');
|
|
387
|
+
userStoryList = titles
|
|
388
|
+
? `\n**User stories in prd.json:**\n\n${titles}\n`
|
|
389
|
+
: '';
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Non-fatal — prd.json may be unreadable at generation time
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const extraFiles = await presentFiles(dir, [
|
|
396
|
+
'architecture.md',
|
|
397
|
+
'README.md',
|
|
398
|
+
'CONTRIBUTING.md',
|
|
399
|
+
]);
|
|
400
|
+
const extraFileList = extraFiles.length > 0
|
|
401
|
+
? '\n**Additional files:**\n' + extraFiles.map((f) => `- \`${f}\``).join('\n')
|
|
402
|
+
: '';
|
|
403
|
+
|
|
404
|
+
return [
|
|
405
|
+
preamble(sidecar),
|
|
406
|
+
'',
|
|
407
|
+
'## Spec Format: Ralph',
|
|
408
|
+
'',
|
|
409
|
+
'## Files to Read First',
|
|
410
|
+
'',
|
|
411
|
+
'- `prd.json` — Product requirements document with user stories',
|
|
412
|
+
extraFileList,
|
|
413
|
+
'',
|
|
414
|
+
'Read `prd.json` carefully. The `userStories` array defines all the work to be done.',
|
|
415
|
+
'',
|
|
416
|
+
'## Finding Tasks',
|
|
417
|
+
'',
|
|
418
|
+
'Open `prd.json` and read the `userStories` array. Each entry is a task.',
|
|
419
|
+
userStoryList,
|
|
420
|
+
'Each user story typically has:',
|
|
421
|
+
'- `title` or `name` — what to build',
|
|
422
|
+
'- `description` or `details` — how to build it',
|
|
423
|
+
'- `acceptanceCriteria` — what must be true for the story to be complete',
|
|
424
|
+
'',
|
|
425
|
+
'## Marking Tasks Complete',
|
|
426
|
+
'',
|
|
427
|
+
'Ralph format does not have a built-in task-tracking file.',
|
|
428
|
+
'Create a `PROGRESS.md` file in the working directory and track progress there:',
|
|
429
|
+
'',
|
|
430
|
+
'```markdown',
|
|
431
|
+
'# Progress',
|
|
432
|
+
'',
|
|
433
|
+
'- [x] Story title (completed)',
|
|
434
|
+
'- [ ] Next story title',
|
|
435
|
+
'```',
|
|
436
|
+
'',
|
|
437
|
+
'Update `PROGRESS.md` as you complete each user story.',
|
|
438
|
+
'',
|
|
439
|
+
'## Completion Criteria',
|
|
440
|
+
'',
|
|
441
|
+
'The run is complete when:',
|
|
442
|
+
'',
|
|
443
|
+
'1. **All user stories implemented** — Every story in `prd.json` has a working implementation.',
|
|
444
|
+
'2. **All acceptance criteria met** — Verify each story\'s `acceptanceCriteria` is satisfied.',
|
|
445
|
+
'3. **Tests pass** — Run the test suite; all tests green.',
|
|
446
|
+
'4. **PROGRESS.md updated** — All stories checked `[x]` in `PROGRESS.md`.',
|
|
447
|
+
'',
|
|
448
|
+
testingSection(),
|
|
449
|
+
'',
|
|
450
|
+
completionReminder(),
|
|
451
|
+
].join('\n');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Generate meta-instructions for `custom` format specs.
|
|
456
|
+
*
|
|
457
|
+
* Layout: arbitrary markdown files — no fixed structure.
|
|
458
|
+
* Agent must discover tasks from whatever is present.
|
|
459
|
+
*/
|
|
460
|
+
async function generateCustomInstructions(
|
|
461
|
+
dir: string,
|
|
462
|
+
sidecar: SidecarData
|
|
463
|
+
): Promise<string> {
|
|
464
|
+
// Enumerate markdown files at top level (non-recursive for brevity)
|
|
465
|
+
let mdFiles: string[] = [];
|
|
466
|
+
try {
|
|
467
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
468
|
+
mdFiles = entries
|
|
469
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
470
|
+
.map((e) => e.name)
|
|
471
|
+
.sort();
|
|
472
|
+
} catch {
|
|
473
|
+
// Ignore
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const mdFileList = mdFiles.length > 0
|
|
477
|
+
? mdFiles.map((f) => `- \`${f}\``).join('\n')
|
|
478
|
+
: '- *(no .md files found at top level)*';
|
|
479
|
+
|
|
480
|
+
return [
|
|
481
|
+
preamble(sidecar),
|
|
482
|
+
'',
|
|
483
|
+
'## Spec Format: Custom',
|
|
484
|
+
'',
|
|
485
|
+
'## Files to Read First',
|
|
486
|
+
'',
|
|
487
|
+
'Markdown files found in this spec:',
|
|
488
|
+
'',
|
|
489
|
+
mdFileList,
|
|
490
|
+
'',
|
|
491
|
+
'Read all markdown files to understand the full scope of work.',
|
|
492
|
+
'Look for a README, specification, or requirements document as your primary source of truth.',
|
|
493
|
+
'',
|
|
494
|
+
'## Finding Tasks',
|
|
495
|
+
'',
|
|
496
|
+
'This spec uses a custom format. Scan all markdown files for:',
|
|
497
|
+
'',
|
|
498
|
+
'1. **Checkbox items**: `- [ ] task description` — these are explicit tasks',
|
|
499
|
+
'2. **Numbered lists**: actionable steps in requirements documents',
|
|
500
|
+
'3. **Heading-based sections**: major features or modules to implement',
|
|
501
|
+
'',
|
|
502
|
+
'If you find a file that looks like a task list (TASKS.md, TODO.md, checklist.md, etc.), use it.',
|
|
503
|
+
'',
|
|
504
|
+
'## Marking Tasks Complete',
|
|
505
|
+
'',
|
|
506
|
+
'If the spec has checkboxes (`- [ ]`), update them to `- [x]` as you complete tasks.',
|
|
507
|
+
'',
|
|
508
|
+
'If the spec has no checkboxes, create a `PROGRESS.md` file to track progress:',
|
|
509
|
+
'',
|
|
510
|
+
'```markdown',
|
|
511
|
+
'# Progress',
|
|
512
|
+
'',
|
|
513
|
+
'- [x] Task or feature completed',
|
|
514
|
+
'- [ ] Next task',
|
|
515
|
+
'```',
|
|
516
|
+
'',
|
|
517
|
+
'## Completion Criteria',
|
|
518
|
+
'',
|
|
519
|
+
'The run is complete when:',
|
|
520
|
+
'',
|
|
521
|
+
'1. **All tasks done** — All checkbox items checked, or all items in `PROGRESS.md` checked.',
|
|
522
|
+
'2. **Tests pass** — Run the test suite; all tests green.',
|
|
523
|
+
'3. **Requirements satisfied** — Implementation matches all requirements found in the spec files.',
|
|
524
|
+
'',
|
|
525
|
+
testingSection(),
|
|
526
|
+
'',
|
|
527
|
+
completionReminder(),
|
|
528
|
+
].join('\n');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Generate format-aware meta-instructions for the AI agent executing a spec.
|
|
535
|
+
*
|
|
536
|
+
* Returns the full markdown content for `.specmarket-runner.md`.
|
|
537
|
+
* The content tells the agent: which files to read, how to find tasks,
|
|
538
|
+
* how to mark completion, how to run tests, and when the run is done.
|
|
539
|
+
*
|
|
540
|
+
* @param specDir - Path to the spec source directory (may differ from runDir)
|
|
541
|
+
* @param format - Detected spec format (specmarket | speckit | bmad | ralph | custom | any string)
|
|
542
|
+
*/
|
|
543
|
+
export async function generateMetaInstructions(
|
|
544
|
+
specDir: string,
|
|
545
|
+
format: string
|
|
546
|
+
): Promise<string> {
|
|
547
|
+
const sidecar = await readSidecarData(specDir);
|
|
548
|
+
|
|
549
|
+
switch (format) {
|
|
550
|
+
case 'specmarket':
|
|
551
|
+
return generateSpecmarketInstructions(specDir, sidecar);
|
|
552
|
+
case 'speckit':
|
|
553
|
+
return generateSpeckitInstructions(specDir, sidecar);
|
|
554
|
+
case 'bmad':
|
|
555
|
+
return generateBmadInstructions(specDir, sidecar);
|
|
556
|
+
case 'ralph':
|
|
557
|
+
return generateRalphInstructions(specDir, sidecar);
|
|
558
|
+
case 'custom':
|
|
559
|
+
default:
|
|
560
|
+
return generateCustomInstructions(specDir, sidecar);
|
|
561
|
+
}
|
|
562
|
+
}
|