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.
- package/.claude/settings.local.json +5 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +1 -0
- package/PROMPT.md +95 -0
- package/README.md +138 -0
- package/WIP.md +5 -0
- package/dist/docs-templates/domain.md +36 -0
- package/dist/docs-templates/issue-tracker-github.md +228 -0
- package/dist/docs-templates/issue-tracker.md +30 -0
- package/dist/docs-templates/triage-labels.md +15 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +7 -0
- package/dist/meta.json +207 -0
- package/dist/orchestrator.js +273 -0
- package/dist/orchestrator.js.map +7 -0
- package/dist/resolve.js +71 -0
- package/dist/resolve.js.map +7 -0
- package/dist/setup.js +151 -0
- package/dist/setup.js.map +7 -0
- package/dist/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/dist/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/dist/skills/domain-modeling/SKILL.md +74 -0
- package/dist/skills/domain-modeling/agents/openai.yaml +3 -0
- package/dist/skills/grill/SKILL.md +7 -0
- package/dist/skills/grill/agents/openai.yaml +5 -0
- package/dist/skills/grilling/SKILL.md +28 -0
- package/dist/skills/grilling/agents/openai.yaml +3 -0
- package/dist/skills/tdd/SKILL.md +38 -0
- package/dist/skills/tdd/agents/openai.yaml +3 -0
- package/dist/skills/tdd/mocking.md +59 -0
- package/dist/skills/tdd/tests.md +77 -0
- package/dist/skills/to-spec/SKILL.md +75 -0
- package/dist/skills/to-spec/agents/openai.yaml +5 -0
- package/dist/skills/to-tickets/SKILL.md +105 -0
- package/dist/skills/to-tickets/agents/openai.yaml +5 -0
- package/dist/skills/upstream.json +5 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +31 -0
- package/dist/src/orchestrator.d.ts.map +1 -0
- package/dist/src/resolve.d.ts +10 -0
- package/dist/src/resolve.d.ts.map +1 -0
- package/dist/src/setup.d.ts +13 -0
- package/dist/src/setup.d.ts.map +1 -0
- package/dist/test/integration.test.js +848 -0
- package/dist/test/integration.test.js.map +7 -0
- package/dist/test/orchestrator.test.js +749 -0
- package/dist/test/orchestrator.test.js.map +7 -0
- package/dist/test/setup.test.js +574 -0
- package/dist/test/setup.test.js.map +7 -0
- package/docs/README.md +12 -0
- package/docs/agents/domain.md +36 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +144 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +292 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +212 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +203 -0
- package/docs/skills.md +58 -0
- package/docs/test-plans/2026-08-27-issue-tracker-choice.md +95 -0
- package/eslint.config.js +142 -0
- package/lib.es5.d.ts +19 -0
- package/package.json +36 -0
- package/progress.log +190 -0
- package/ralph_claude.sh +263 -0
- package/scripts/clone-skills.sh +52 -0
- package/specs/prd.json +172 -0
- package/src/docs-templates/domain.md +36 -0
- package/src/docs-templates/issue-tracker-github.md +228 -0
- package/src/docs-templates/issue-tracker.md +30 -0
- package/src/docs-templates/triage-labels.md +15 -0
- package/src/index.ts +184 -0
- package/src/orchestrator.ts +324 -0
- package/src/resolve.ts +73 -0
- package/src/setup.ts +157 -0
- package/src/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skills/domain-modeling/SKILL.md +74 -0
- package/src/skills/domain-modeling/agents/openai.yaml +3 -0
- package/src/skills/grill/SKILL.md +7 -0
- package/src/skills/grill/agents/openai.yaml +5 -0
- package/src/skills/grilling/SKILL.md +28 -0
- package/src/skills/grilling/agents/openai.yaml +3 -0
- package/src/skills/tdd/SKILL.md +38 -0
- package/src/skills/tdd/agents/openai.yaml +3 -0
- package/src/skills/tdd/mocking.md +59 -0
- package/src/skills/tdd/tests.md +77 -0
- package/src/skills/to-spec/SKILL.md +75 -0
- package/src/skills/to-spec/agents/openai.yaml +5 -0
- package/src/skills/to-tickets/SKILL.md +105 -0
- package/src/skills/to-tickets/agents/openai.yaml +5 -0
- package/src/skills/upstream.json +5 -0
- package/tasks/prd-integration-test-coverage.md +243 -0
- package/test/integration.test.ts +636 -0
- package/test/mock-agent-echo-args.js +4 -0
- package/test/mock-agent-fail.js +3 -0
- package/test/mock-agent-pass.js +3 -0
- package/test/orchestrator.test.ts +609 -0
- package/test/setup.test.ts +417 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
// test/integration.test.ts
|
|
2
|
+
import { test } from "@substrate-system/tapzero";
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync as mkdirSync2,
|
|
6
|
+
writeFileSync as writeFileSync2,
|
|
7
|
+
readFileSync as readFileSync2,
|
|
8
|
+
existsSync as existsSync3,
|
|
9
|
+
rmSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join as join3 } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
// src/resolve.ts
|
|
16
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { select } from "@inquirer/prompts";
|
|
19
|
+
function findFeatureDirs(cwd) {
|
|
20
|
+
const scratchDir = join(cwd, ".scratch");
|
|
21
|
+
if (!existsSync(scratchDir)) return [];
|
|
22
|
+
const entries = readdirSync(
|
|
23
|
+
scratchDir,
|
|
24
|
+
{ withFileTypes: true }
|
|
25
|
+
);
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
const issuesDir = join(
|
|
30
|
+
scratchDir,
|
|
31
|
+
entry.name,
|
|
32
|
+
"issues"
|
|
33
|
+
);
|
|
34
|
+
const specFile = join(
|
|
35
|
+
scratchDir,
|
|
36
|
+
entry.name,
|
|
37
|
+
"spec.md"
|
|
38
|
+
);
|
|
39
|
+
if (existsSync(issuesDir)) {
|
|
40
|
+
results.push({
|
|
41
|
+
name: entry.name,
|
|
42
|
+
dir: issuesDir,
|
|
43
|
+
spec: specFile
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
async function resolveIssuesDir(dirFlag, specFlag, cwd = process.cwd()) {
|
|
50
|
+
if (dirFlag && specFlag) {
|
|
51
|
+
return { dir: dirFlag, spec: specFlag };
|
|
52
|
+
}
|
|
53
|
+
const features = findFeatureDirs(cwd);
|
|
54
|
+
if (features.length === 0) {
|
|
55
|
+
console.error(
|
|
56
|
+
"No .scratch/<feature>/issues/ directories found.\nProvide --dir and --spec explicitly."
|
|
57
|
+
);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
let chosen;
|
|
61
|
+
if (features.length === 1) {
|
|
62
|
+
chosen = features[0];
|
|
63
|
+
console.log(`Using feature: ${chosen.name}`);
|
|
64
|
+
} else {
|
|
65
|
+
const name = await select({
|
|
66
|
+
message: "Which feature?",
|
|
67
|
+
choices: features.map((f) => ({
|
|
68
|
+
name: f.name,
|
|
69
|
+
value: f.name
|
|
70
|
+
}))
|
|
71
|
+
});
|
|
72
|
+
chosen = features.find((f) => f.name === name);
|
|
73
|
+
}
|
|
74
|
+
const dir = dirFlag ?? chosen.dir;
|
|
75
|
+
const spec = specFlag ?? chosen.spec;
|
|
76
|
+
return { dir, spec };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/setup.ts
|
|
80
|
+
import {
|
|
81
|
+
cpSync,
|
|
82
|
+
existsSync as existsSync2,
|
|
83
|
+
mkdirSync,
|
|
84
|
+
readdirSync as readdirSync2,
|
|
85
|
+
readFileSync,
|
|
86
|
+
symlinkSync,
|
|
87
|
+
writeFileSync
|
|
88
|
+
} from "node:fs";
|
|
89
|
+
import { join as join2 } from "node:path";
|
|
90
|
+
import { execSync } from "node:child_process";
|
|
91
|
+
var SKILL_DIRS = [
|
|
92
|
+
"grill",
|
|
93
|
+
"grilling",
|
|
94
|
+
"domain-modeling",
|
|
95
|
+
"to-spec",
|
|
96
|
+
"to-tickets",
|
|
97
|
+
"tdd"
|
|
98
|
+
];
|
|
99
|
+
var DOC_TEMPLATES = [
|
|
100
|
+
"triage-labels.md",
|
|
101
|
+
"domain.md"
|
|
102
|
+
];
|
|
103
|
+
var TRACKER_TEMPLATES = {
|
|
104
|
+
local: "issue-tracker.md",
|
|
105
|
+
github: "issue-tracker-github.md"
|
|
106
|
+
};
|
|
107
|
+
function setup(opts) {
|
|
108
|
+
const {
|
|
109
|
+
bundledSkillsDir,
|
|
110
|
+
docsTemplatesDir,
|
|
111
|
+
projectDir,
|
|
112
|
+
claude,
|
|
113
|
+
tracker
|
|
114
|
+
} = opts;
|
|
115
|
+
const agentsSkillsDir = join2(
|
|
116
|
+
projectDir,
|
|
117
|
+
".agents",
|
|
118
|
+
"skills"
|
|
119
|
+
);
|
|
120
|
+
const upstream = JSON.parse(
|
|
121
|
+
readFileSync(
|
|
122
|
+
join2(bundledSkillsDir, "upstream.json"),
|
|
123
|
+
"utf8"
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
console.log("Installing skills to .agents/skills/\n");
|
|
127
|
+
for (const skill of SKILL_DIRS) {
|
|
128
|
+
const src = join2(bundledSkillsDir, skill);
|
|
129
|
+
const dest = join2(agentsSkillsDir, skill);
|
|
130
|
+
mkdirSync(dest, { recursive: true });
|
|
131
|
+
cpSync(src, dest, { recursive: true });
|
|
132
|
+
const files = readdirSync2(src);
|
|
133
|
+
console.log(
|
|
134
|
+
` ${skill}/ (${files.join(", ")})`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (claude) {
|
|
138
|
+
const claudeSkillsDir = join2(
|
|
139
|
+
projectDir,
|
|
140
|
+
".claude",
|
|
141
|
+
"skills"
|
|
142
|
+
);
|
|
143
|
+
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
144
|
+
for (const skill of SKILL_DIRS) {
|
|
145
|
+
const linkPath = join2(claudeSkillsDir, skill);
|
|
146
|
+
const target = join2(
|
|
147
|
+
"..",
|
|
148
|
+
"..",
|
|
149
|
+
".agents",
|
|
150
|
+
"skills",
|
|
151
|
+
skill
|
|
152
|
+
);
|
|
153
|
+
if (existsSync2(linkPath)) continue;
|
|
154
|
+
symlinkSync(target, linkPath);
|
|
155
|
+
}
|
|
156
|
+
console.log(
|
|
157
|
+
"\nCreated symlinks in .claude/skills/"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const docsAgentsDir = join2(
|
|
161
|
+
projectDir,
|
|
162
|
+
"docs",
|
|
163
|
+
"agents"
|
|
164
|
+
);
|
|
165
|
+
mkdirSync(docsAgentsDir, { recursive: true });
|
|
166
|
+
console.log("");
|
|
167
|
+
for (const tmpl of DOC_TEMPLATES) {
|
|
168
|
+
const dest = join2(docsAgentsDir, tmpl);
|
|
169
|
+
if (existsSync2(dest)) {
|
|
170
|
+
console.log(
|
|
171
|
+
` docs/agents/${tmpl} (already exists)`
|
|
172
|
+
);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const src = join2(docsTemplatesDir, tmpl);
|
|
176
|
+
writeFileSync(dest, readFileSync(src, "utf8"));
|
|
177
|
+
console.log(` docs/agents/${tmpl} (created)`);
|
|
178
|
+
}
|
|
179
|
+
const trackerSrc = join2(
|
|
180
|
+
docsTemplatesDir,
|
|
181
|
+
TRACKER_TEMPLATES[tracker]
|
|
182
|
+
);
|
|
183
|
+
const trackerDest = join2(docsAgentsDir, "issue-tracker.md");
|
|
184
|
+
writeFileSync(trackerDest, readFileSync(trackerSrc, "utf8"));
|
|
185
|
+
console.log(
|
|
186
|
+
` docs/agents/issue-tracker.md (${tracker})`
|
|
187
|
+
);
|
|
188
|
+
const trackerLine = tracker === "github" ? "Issues are tracked via GitHub Issues using the `gh` CLI." : "Issues are tracked as local markdown files under `.scratch/`.";
|
|
189
|
+
const agentsMd = [
|
|
190
|
+
"## Agent skills",
|
|
191
|
+
"",
|
|
192
|
+
"### Issue tracker",
|
|
193
|
+
"",
|
|
194
|
+
`${trackerLine} See \`docs/agents/issue-tracker.md\`.`,
|
|
195
|
+
"",
|
|
196
|
+
"### Triage labels",
|
|
197
|
+
"",
|
|
198
|
+
"Default label vocabulary. See `docs/agents/triage-labels.md`.",
|
|
199
|
+
"",
|
|
200
|
+
"### Domain docs",
|
|
201
|
+
"",
|
|
202
|
+
"Single-context layout. See `docs/agents/domain.md`.",
|
|
203
|
+
""
|
|
204
|
+
].join("\n");
|
|
205
|
+
writeFileSync(join2(projectDir, "AGENTS.md"), agentsMd);
|
|
206
|
+
console.log("\n AGENTS.md (updated)");
|
|
207
|
+
console.log(
|
|
208
|
+
`
|
|
209
|
+
Source: ${upstream.repo} @ ${upstream.commit}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// test/integration.test.ts
|
|
214
|
+
function tmp() {
|
|
215
|
+
return mkdtempSync(
|
|
216
|
+
join3(tmpdir(), "nightralph-int-test-")
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
test("findFeatureDirs - no .scratch dir", (t) => {
|
|
220
|
+
const dir = tmp();
|
|
221
|
+
try {
|
|
222
|
+
const result = findFeatureDirs(dir);
|
|
223
|
+
t.deepEqual(
|
|
224
|
+
result,
|
|
225
|
+
[],
|
|
226
|
+
"returns empty array when .scratch/ missing"
|
|
227
|
+
);
|
|
228
|
+
} finally {
|
|
229
|
+
rmSync(dir, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
test("findFeatureDirs - dirs without issues/", (t) => {
|
|
233
|
+
const dir = tmp();
|
|
234
|
+
try {
|
|
235
|
+
const scratch = join3(dir, ".scratch");
|
|
236
|
+
mkdirSync2(join3(scratch, "feat-a"), {
|
|
237
|
+
recursive: true
|
|
238
|
+
});
|
|
239
|
+
mkdirSync2(join3(scratch, "feat-b"), {
|
|
240
|
+
recursive: true
|
|
241
|
+
});
|
|
242
|
+
const result = findFeatureDirs(dir);
|
|
243
|
+
t.deepEqual(
|
|
244
|
+
result,
|
|
245
|
+
[],
|
|
246
|
+
"returns empty when no issues/ subdirs"
|
|
247
|
+
);
|
|
248
|
+
} finally {
|
|
249
|
+
rmSync(dir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
test("findFeatureDirs - valid feature dirs", (t) => {
|
|
253
|
+
const dir = tmp();
|
|
254
|
+
try {
|
|
255
|
+
const scratch = join3(dir, ".scratch");
|
|
256
|
+
mkdirSync2(
|
|
257
|
+
join3(scratch, "feat-x", "issues"),
|
|
258
|
+
{ recursive: true }
|
|
259
|
+
);
|
|
260
|
+
writeFileSync2(
|
|
261
|
+
join3(scratch, "feat-x", "spec.md"),
|
|
262
|
+
"# spec"
|
|
263
|
+
);
|
|
264
|
+
const result = findFeatureDirs(dir);
|
|
265
|
+
t.equal(result.length, 1, "finds one feature");
|
|
266
|
+
t.equal(
|
|
267
|
+
result[0].name,
|
|
268
|
+
"feat-x",
|
|
269
|
+
"correct name"
|
|
270
|
+
);
|
|
271
|
+
t.equal(
|
|
272
|
+
result[0].dir,
|
|
273
|
+
join3(scratch, "feat-x", "issues"),
|
|
274
|
+
"correct dir path"
|
|
275
|
+
);
|
|
276
|
+
t.equal(
|
|
277
|
+
result[0].spec,
|
|
278
|
+
join3(scratch, "feat-x", "spec.md"),
|
|
279
|
+
"correct spec path"
|
|
280
|
+
);
|
|
281
|
+
} finally {
|
|
282
|
+
rmSync(dir, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
test(
|
|
286
|
+
"findFeatureDirs - ignores non-directory entries",
|
|
287
|
+
(t) => {
|
|
288
|
+
const dir = tmp();
|
|
289
|
+
try {
|
|
290
|
+
const scratch = join3(dir, ".scratch");
|
|
291
|
+
mkdirSync2(scratch, { recursive: true });
|
|
292
|
+
writeFileSync2(
|
|
293
|
+
join3(scratch, "not-a-dir.txt"),
|
|
294
|
+
"just a file"
|
|
295
|
+
);
|
|
296
|
+
mkdirSync2(
|
|
297
|
+
join3(scratch, "real-feat", "issues"),
|
|
298
|
+
{ recursive: true }
|
|
299
|
+
);
|
|
300
|
+
const result = findFeatureDirs(dir);
|
|
301
|
+
t.equal(
|
|
302
|
+
result.length,
|
|
303
|
+
1,
|
|
304
|
+
"only returns the directory entry"
|
|
305
|
+
);
|
|
306
|
+
t.equal(
|
|
307
|
+
result[0].name,
|
|
308
|
+
"real-feat",
|
|
309
|
+
"returns the valid feature"
|
|
310
|
+
);
|
|
311
|
+
} finally {
|
|
312
|
+
rmSync(dir, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
test(
|
|
317
|
+
"resolveIssuesDir - returns explicit flags directly",
|
|
318
|
+
async (t) => {
|
|
319
|
+
const dir = tmp();
|
|
320
|
+
try {
|
|
321
|
+
const result = await resolveIssuesDir(
|
|
322
|
+
"/my/issues",
|
|
323
|
+
"/my/spec.md",
|
|
324
|
+
dir
|
|
325
|
+
);
|
|
326
|
+
t.equal(
|
|
327
|
+
result.dir,
|
|
328
|
+
"/my/issues",
|
|
329
|
+
"dir matches the flag value"
|
|
330
|
+
);
|
|
331
|
+
t.equal(
|
|
332
|
+
result.spec,
|
|
333
|
+
"/my/spec.md",
|
|
334
|
+
"spec matches the flag value"
|
|
335
|
+
);
|
|
336
|
+
} finally {
|
|
337
|
+
rmSync(dir, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
test(
|
|
342
|
+
"resolveIssuesDir - no filesystem scanning with flags",
|
|
343
|
+
async (t) => {
|
|
344
|
+
const dir = tmp();
|
|
345
|
+
try {
|
|
346
|
+
const flagDir = join3(dir, "explicit-issues");
|
|
347
|
+
const flagSpec = join3(dir, "explicit-spec.md");
|
|
348
|
+
const result = await resolveIssuesDir(
|
|
349
|
+
flagDir,
|
|
350
|
+
flagSpec,
|
|
351
|
+
dir
|
|
352
|
+
);
|
|
353
|
+
t.equal(
|
|
354
|
+
result.dir,
|
|
355
|
+
flagDir,
|
|
356
|
+
"returns dir without scanning"
|
|
357
|
+
);
|
|
358
|
+
t.equal(
|
|
359
|
+
result.spec,
|
|
360
|
+
flagSpec,
|
|
361
|
+
"returns spec without scanning"
|
|
362
|
+
);
|
|
363
|
+
} finally {
|
|
364
|
+
rmSync(dir, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
var CLI = join3(process.cwd(), "dist", "index.js");
|
|
369
|
+
var MOCK_AGENT = join3(
|
|
370
|
+
process.cwd(),
|
|
371
|
+
"test",
|
|
372
|
+
"mock-agent-pass.js"
|
|
373
|
+
);
|
|
374
|
+
var MOCK_ECHO = join3(
|
|
375
|
+
process.cwd(),
|
|
376
|
+
"test",
|
|
377
|
+
"mock-agent-echo-args.js"
|
|
378
|
+
);
|
|
379
|
+
function spawnCli(args) {
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
execFile(
|
|
382
|
+
process.execPath,
|
|
383
|
+
[CLI, ...args],
|
|
384
|
+
{ timeout: 3e4 },
|
|
385
|
+
(err, stdout, stderr) => {
|
|
386
|
+
const code = err && "code" in err ? err.code : err ? 1 : 0;
|
|
387
|
+
resolve({
|
|
388
|
+
code,
|
|
389
|
+
stdout,
|
|
390
|
+
stderr
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function makeTicketBody(opts) {
|
|
397
|
+
const lines = [
|
|
398
|
+
"# Test ticket",
|
|
399
|
+
"",
|
|
400
|
+
`**Status:** ${opts.status}`
|
|
401
|
+
];
|
|
402
|
+
if (opts.blockers && opts.blockers.length > 0) {
|
|
403
|
+
lines.push(
|
|
404
|
+
`**Blocked by:** ${opts.blockers.join(", ")}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return lines.join("\n");
|
|
408
|
+
}
|
|
409
|
+
test(
|
|
410
|
+
"CLI subprocess - --model flag forwarded to agent",
|
|
411
|
+
async (t) => {
|
|
412
|
+
const base = tmp();
|
|
413
|
+
try {
|
|
414
|
+
const issuesDir = join3(base, "issues");
|
|
415
|
+
const logsDir = join3(base, "logs");
|
|
416
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
417
|
+
const specPath = join3(base, "spec.md");
|
|
418
|
+
writeFileSync2(specPath, "# Test spec");
|
|
419
|
+
writeFileSync2(
|
|
420
|
+
join3(issuesDir, "01-echo.md"),
|
|
421
|
+
makeTicketBody({
|
|
422
|
+
status: "ready-for-agent"
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
const { code } = await spawnCli([
|
|
426
|
+
MOCK_ECHO,
|
|
427
|
+
"--dir",
|
|
428
|
+
issuesDir,
|
|
429
|
+
"--spec",
|
|
430
|
+
specPath,
|
|
431
|
+
"--timeout-secs",
|
|
432
|
+
"10",
|
|
433
|
+
"-m",
|
|
434
|
+
"test-model"
|
|
435
|
+
]);
|
|
436
|
+
t.equal(code, 0, "exit code 0");
|
|
437
|
+
const logPath = join3(logsDir, "01-echo.log");
|
|
438
|
+
t.ok(
|
|
439
|
+
existsSync3(logPath),
|
|
440
|
+
"log file created"
|
|
441
|
+
);
|
|
442
|
+
const logContent = readFileSync2(
|
|
443
|
+
logPath,
|
|
444
|
+
"utf8"
|
|
445
|
+
);
|
|
446
|
+
t.ok(
|
|
447
|
+
logContent.includes("--model"),
|
|
448
|
+
"log contains --model flag"
|
|
449
|
+
);
|
|
450
|
+
t.ok(
|
|
451
|
+
logContent.includes("test-model"),
|
|
452
|
+
"log contains model value"
|
|
453
|
+
);
|
|
454
|
+
} finally {
|
|
455
|
+
rmSync(base, { recursive: true, force: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
);
|
|
459
|
+
test("CLI subprocess - --dry-run output", async (t) => {
|
|
460
|
+
const base = tmp();
|
|
461
|
+
try {
|
|
462
|
+
const issuesDir = join3(base, "issues");
|
|
463
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
464
|
+
const specPath = join3(base, "spec.md");
|
|
465
|
+
writeFileSync2(specPath, "# Test spec");
|
|
466
|
+
writeFileSync2(
|
|
467
|
+
join3(issuesDir, "01-alpha.md"),
|
|
468
|
+
makeTicketBody({ status: "ready-for-agent" })
|
|
469
|
+
);
|
|
470
|
+
writeFileSync2(
|
|
471
|
+
join3(issuesDir, "02-beta.md"),
|
|
472
|
+
makeTicketBody({
|
|
473
|
+
status: "ready-for-agent",
|
|
474
|
+
blockers: [1]
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
const { code, stdout } = await spawnCli([
|
|
478
|
+
MOCK_AGENT,
|
|
479
|
+
"--dir",
|
|
480
|
+
issuesDir,
|
|
481
|
+
"--spec",
|
|
482
|
+
specPath,
|
|
483
|
+
"--dry-run"
|
|
484
|
+
]);
|
|
485
|
+
t.equal(code, 0, "exit code 0");
|
|
486
|
+
t.ok(
|
|
487
|
+
stdout.includes("Wave 1:") && stdout.includes("01-alpha.md"),
|
|
488
|
+
"stdout contains Wave 1 with first ticket"
|
|
489
|
+
);
|
|
490
|
+
t.ok(
|
|
491
|
+
stdout.includes("Wave 2:") && stdout.includes("02-beta.md"),
|
|
492
|
+
"stdout contains Wave 2 with second ticket"
|
|
493
|
+
);
|
|
494
|
+
t.ok(
|
|
495
|
+
stdout.includes("AGENT INSTRUCTIONS"),
|
|
496
|
+
"stdout contains prompt preview"
|
|
497
|
+
);
|
|
498
|
+
const ticket1 = readFileSync2(
|
|
499
|
+
join3(issuesDir, "01-alpha.md"),
|
|
500
|
+
"utf8"
|
|
501
|
+
);
|
|
502
|
+
t.ok(
|
|
503
|
+
ticket1.includes("**Status:** ready-for-agent"),
|
|
504
|
+
"ticket 1 NOT modified"
|
|
505
|
+
);
|
|
506
|
+
const ticket2 = readFileSync2(
|
|
507
|
+
join3(issuesDir, "02-beta.md"),
|
|
508
|
+
"utf8"
|
|
509
|
+
);
|
|
510
|
+
t.ok(
|
|
511
|
+
ticket2.includes("**Status:** ready-for-agent"),
|
|
512
|
+
"ticket 2 NOT modified"
|
|
513
|
+
);
|
|
514
|
+
} finally {
|
|
515
|
+
rmSync(base, { recursive: true, force: true });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
test(
|
|
519
|
+
"CLI subprocess - no tickets exits gracefully",
|
|
520
|
+
async (t) => {
|
|
521
|
+
const base = tmp();
|
|
522
|
+
try {
|
|
523
|
+
const issuesDir = join3(base, "issues");
|
|
524
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
525
|
+
const specPath = join3(base, "spec.md");
|
|
526
|
+
writeFileSync2(specPath, "# Empty spec");
|
|
527
|
+
const { code, stdout } = await spawnCli([
|
|
528
|
+
MOCK_AGENT,
|
|
529
|
+
"--dir",
|
|
530
|
+
issuesDir,
|
|
531
|
+
"--spec",
|
|
532
|
+
specPath
|
|
533
|
+
]);
|
|
534
|
+
t.equal(code, 0, "exit code 0 (graceful)");
|
|
535
|
+
t.ok(
|
|
536
|
+
stdout.includes("No ticket files found"),
|
|
537
|
+
"stdout contains no-tickets message"
|
|
538
|
+
);
|
|
539
|
+
} finally {
|
|
540
|
+
rmSync(base, { recursive: true, force: true });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
test("CLI subprocess - error for missing --dir", async (t) => {
|
|
545
|
+
const base = tmp();
|
|
546
|
+
try {
|
|
547
|
+
const specPath = join3(base, "spec.md");
|
|
548
|
+
writeFileSync2(specPath, "# Valid spec");
|
|
549
|
+
const { code, stderr } = await spawnCli([
|
|
550
|
+
MOCK_AGENT,
|
|
551
|
+
"--dir",
|
|
552
|
+
"/nonexistent/path",
|
|
553
|
+
"--spec",
|
|
554
|
+
specPath
|
|
555
|
+
]);
|
|
556
|
+
t.ok(code !== 0, "exit code is non-zero");
|
|
557
|
+
t.ok(
|
|
558
|
+
stderr.includes("not found"),
|
|
559
|
+
"stderr contains not found"
|
|
560
|
+
);
|
|
561
|
+
} finally {
|
|
562
|
+
rmSync(base, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
test("CLI subprocess - error for missing --spec", async (t) => {
|
|
566
|
+
const base = tmp();
|
|
567
|
+
try {
|
|
568
|
+
const issuesDir = join3(base, "issues");
|
|
569
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
570
|
+
const { code, stderr } = await spawnCli([
|
|
571
|
+
MOCK_AGENT,
|
|
572
|
+
"--dir",
|
|
573
|
+
issuesDir,
|
|
574
|
+
"--spec",
|
|
575
|
+
"/nonexistent/spec.md"
|
|
576
|
+
]);
|
|
577
|
+
t.ok(code !== 0, "exit code is non-zero");
|
|
578
|
+
t.ok(
|
|
579
|
+
stderr.includes("not found"),
|
|
580
|
+
"stderr contains not found"
|
|
581
|
+
);
|
|
582
|
+
} finally {
|
|
583
|
+
rmSync(base, { recursive: true, force: true });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
test(
|
|
587
|
+
"CLI subprocess - unknown blocker warning and stripping",
|
|
588
|
+
async (t) => {
|
|
589
|
+
const base = tmp();
|
|
590
|
+
try {
|
|
591
|
+
const issuesDir = join3(base, "issues");
|
|
592
|
+
const logsDir = join3(base, "logs");
|
|
593
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
594
|
+
const specPath = join3(base, "spec.md");
|
|
595
|
+
writeFileSync2(specPath, "# Test spec");
|
|
596
|
+
writeFileSync2(
|
|
597
|
+
join3(issuesDir, "01-task.md"),
|
|
598
|
+
makeTicketBody({
|
|
599
|
+
status: "ready-for-agent",
|
|
600
|
+
blockers: [99]
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
const { code, stdout, stderr } = await spawnCli([
|
|
604
|
+
MOCK_AGENT,
|
|
605
|
+
"--dir",
|
|
606
|
+
issuesDir,
|
|
607
|
+
"--spec",
|
|
608
|
+
specPath,
|
|
609
|
+
"--timeout-secs",
|
|
610
|
+
"10"
|
|
611
|
+
]);
|
|
612
|
+
const combined = stdout + stderr;
|
|
613
|
+
t.ok(
|
|
614
|
+
combined.includes("99"),
|
|
615
|
+
"output warns about blocker 99"
|
|
616
|
+
);
|
|
617
|
+
t.ok(
|
|
618
|
+
combined.includes("not found"),
|
|
619
|
+
"output mentions blocker not found"
|
|
620
|
+
);
|
|
621
|
+
t.equal(code, 0, "exit code 0");
|
|
622
|
+
const ticket = readFileSync2(
|
|
623
|
+
join3(issuesDir, "01-task.md"),
|
|
624
|
+
"utf8"
|
|
625
|
+
);
|
|
626
|
+
t.ok(
|
|
627
|
+
ticket.includes("**Status:** done"),
|
|
628
|
+
"ticket completed after blocker stripped"
|
|
629
|
+
);
|
|
630
|
+
t.ok(
|
|
631
|
+
existsSync3(join3(logsDir, "01-task.log")),
|
|
632
|
+
"log file created"
|
|
633
|
+
);
|
|
634
|
+
} finally {
|
|
635
|
+
rmSync(base, { recursive: true, force: true });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
test(
|
|
640
|
+
"CLI subprocess - dry-run deadlock detection",
|
|
641
|
+
async (t) => {
|
|
642
|
+
const base = tmp();
|
|
643
|
+
try {
|
|
644
|
+
const issuesDir = join3(base, "issues");
|
|
645
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
646
|
+
const specPath = join3(base, "spec.md");
|
|
647
|
+
writeFileSync2(specPath, "# Test spec");
|
|
648
|
+
writeFileSync2(
|
|
649
|
+
join3(issuesDir, "01-alpha.md"),
|
|
650
|
+
makeTicketBody({
|
|
651
|
+
status: "ready-for-agent",
|
|
652
|
+
blockers: [2]
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
writeFileSync2(
|
|
656
|
+
join3(issuesDir, "02-beta.md"),
|
|
657
|
+
makeTicketBody({
|
|
658
|
+
status: "ready-for-agent",
|
|
659
|
+
blockers: [1]
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
const { code, stdout } = await spawnCli([
|
|
663
|
+
MOCK_AGENT,
|
|
664
|
+
"--dir",
|
|
665
|
+
issuesDir,
|
|
666
|
+
"--spec",
|
|
667
|
+
specPath,
|
|
668
|
+
"--dry-run"
|
|
669
|
+
]);
|
|
670
|
+
t.equal(code, 0, "exit code 0");
|
|
671
|
+
t.ok(
|
|
672
|
+
stdout.includes("Deadlocked tickets"),
|
|
673
|
+
"output contains Deadlocked tickets"
|
|
674
|
+
);
|
|
675
|
+
t.ok(
|
|
676
|
+
stdout.includes("01-alpha.md"),
|
|
677
|
+
"deadlock lists first ticket"
|
|
678
|
+
);
|
|
679
|
+
t.ok(
|
|
680
|
+
stdout.includes("02-beta.md"),
|
|
681
|
+
"deadlock lists second ticket"
|
|
682
|
+
);
|
|
683
|
+
} finally {
|
|
684
|
+
rmSync(base, { recursive: true, force: true });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
test("CLI subprocess - execute smoke test", async (t) => {
|
|
689
|
+
const base = tmp();
|
|
690
|
+
try {
|
|
691
|
+
const issuesDir = join3(base, "issues");
|
|
692
|
+
const logsDir = join3(base, "logs");
|
|
693
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
694
|
+
const specPath = join3(base, "spec.md");
|
|
695
|
+
writeFileSync2(specPath, "# Test spec\nDo things.");
|
|
696
|
+
writeFileSync2(
|
|
697
|
+
join3(issuesDir, "01-first.md"),
|
|
698
|
+
makeTicketBody({ status: "ready-for-agent" })
|
|
699
|
+
);
|
|
700
|
+
writeFileSync2(
|
|
701
|
+
join3(issuesDir, "02-second.md"),
|
|
702
|
+
makeTicketBody({
|
|
703
|
+
status: "ready-for-agent",
|
|
704
|
+
blockers: [1]
|
|
705
|
+
})
|
|
706
|
+
);
|
|
707
|
+
const { code } = await spawnCli([
|
|
708
|
+
MOCK_AGENT,
|
|
709
|
+
"--dir",
|
|
710
|
+
issuesDir,
|
|
711
|
+
"--spec",
|
|
712
|
+
specPath,
|
|
713
|
+
"--timeout-secs",
|
|
714
|
+
"10"
|
|
715
|
+
]);
|
|
716
|
+
t.equal(code, 0, "exit code 0");
|
|
717
|
+
const ticket1 = readFileSync2(
|
|
718
|
+
join3(issuesDir, "01-first.md"),
|
|
719
|
+
"utf8"
|
|
720
|
+
);
|
|
721
|
+
t.ok(
|
|
722
|
+
ticket1.includes("**Status:** done"),
|
|
723
|
+
"ticket 1 marked done"
|
|
724
|
+
);
|
|
725
|
+
const ticket2 = readFileSync2(
|
|
726
|
+
join3(issuesDir, "02-second.md"),
|
|
727
|
+
"utf8"
|
|
728
|
+
);
|
|
729
|
+
t.ok(
|
|
730
|
+
ticket2.includes("**Status:** done"),
|
|
731
|
+
"ticket 2 marked done"
|
|
732
|
+
);
|
|
733
|
+
t.ok(
|
|
734
|
+
existsSync3(join3(logsDir, "01-first.log")),
|
|
735
|
+
"log file created for ticket 1"
|
|
736
|
+
);
|
|
737
|
+
t.ok(
|
|
738
|
+
existsSync3(join3(logsDir, "02-second.log")),
|
|
739
|
+
"log file created for ticket 2"
|
|
740
|
+
);
|
|
741
|
+
} finally {
|
|
742
|
+
rmSync(base, { recursive: true, force: true });
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
var DIST_DIR = join3(process.cwd(), "dist");
|
|
746
|
+
test("setup-then-execute composition", async (t) => {
|
|
747
|
+
const projectDir = tmp();
|
|
748
|
+
try {
|
|
749
|
+
setup({
|
|
750
|
+
bundledSkillsDir: join3(DIST_DIR, "skills"),
|
|
751
|
+
docsTemplatesDir: join3(
|
|
752
|
+
DIST_DIR,
|
|
753
|
+
"docs-templates"
|
|
754
|
+
),
|
|
755
|
+
projectDir,
|
|
756
|
+
tracker: "local"
|
|
757
|
+
});
|
|
758
|
+
const feature = "my-feature";
|
|
759
|
+
const issuesDir = join3(
|
|
760
|
+
projectDir,
|
|
761
|
+
".scratch",
|
|
762
|
+
feature,
|
|
763
|
+
"issues"
|
|
764
|
+
);
|
|
765
|
+
const specPath = join3(
|
|
766
|
+
projectDir,
|
|
767
|
+
".scratch",
|
|
768
|
+
feature,
|
|
769
|
+
"spec.md"
|
|
770
|
+
);
|
|
771
|
+
mkdirSync2(issuesDir, { recursive: true });
|
|
772
|
+
writeFileSync2(specPath, "# Feature spec\nDo things.");
|
|
773
|
+
writeFileSync2(
|
|
774
|
+
join3(issuesDir, "01-alpha.md"),
|
|
775
|
+
makeTicketBody({ status: "ready-for-agent" })
|
|
776
|
+
);
|
|
777
|
+
writeFileSync2(
|
|
778
|
+
join3(issuesDir, "02-beta.md"),
|
|
779
|
+
makeTicketBody({
|
|
780
|
+
status: "ready-for-agent",
|
|
781
|
+
blockers: [1]
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
const { code } = await spawnCli([
|
|
785
|
+
MOCK_AGENT,
|
|
786
|
+
"--dir",
|
|
787
|
+
issuesDir,
|
|
788
|
+
"--spec",
|
|
789
|
+
specPath,
|
|
790
|
+
"--timeout-secs",
|
|
791
|
+
"10"
|
|
792
|
+
]);
|
|
793
|
+
t.equal(code, 0, "exit code 0");
|
|
794
|
+
const ticket1 = readFileSync2(
|
|
795
|
+
join3(issuesDir, "01-alpha.md"),
|
|
796
|
+
"utf8"
|
|
797
|
+
);
|
|
798
|
+
t.ok(
|
|
799
|
+
ticket1.includes("**Status:** done"),
|
|
800
|
+
"ticket 1 marked done"
|
|
801
|
+
);
|
|
802
|
+
const ticket2 = readFileSync2(
|
|
803
|
+
join3(issuesDir, "02-beta.md"),
|
|
804
|
+
"utf8"
|
|
805
|
+
);
|
|
806
|
+
t.ok(
|
|
807
|
+
ticket2.includes("**Status:** done"),
|
|
808
|
+
"ticket 2 marked done"
|
|
809
|
+
);
|
|
810
|
+
const logsDir = join3(
|
|
811
|
+
projectDir,
|
|
812
|
+
".scratch",
|
|
813
|
+
feature,
|
|
814
|
+
"logs"
|
|
815
|
+
);
|
|
816
|
+
t.ok(
|
|
817
|
+
existsSync3(join3(logsDir, "01-alpha.log")),
|
|
818
|
+
"log file created for ticket 1"
|
|
819
|
+
);
|
|
820
|
+
t.ok(
|
|
821
|
+
existsSync3(join3(logsDir, "02-beta.log")),
|
|
822
|
+
"log file created for ticket 2"
|
|
823
|
+
);
|
|
824
|
+
t.ok(
|
|
825
|
+
existsSync3(join3(
|
|
826
|
+
projectDir,
|
|
827
|
+
"docs",
|
|
828
|
+
"agents",
|
|
829
|
+
"issue-tracker.md"
|
|
830
|
+
)),
|
|
831
|
+
"docs/agents/issue-tracker.md exists from setup"
|
|
832
|
+
);
|
|
833
|
+
for (const skill of SKILL_DIRS) {
|
|
834
|
+
t.ok(
|
|
835
|
+
existsSync3(join3(
|
|
836
|
+
projectDir,
|
|
837
|
+
".agents",
|
|
838
|
+
"skills",
|
|
839
|
+
skill
|
|
840
|
+
)),
|
|
841
|
+
`.agents/skills/${skill}/ exists from setup`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
} finally {
|
|
845
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
//# sourceMappingURL=integration.test.js.map
|