@quinteroac/agents-coding-toolkit 0.1.0-preview

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 (85) hide show
  1. package/AGENTS.md +7 -0
  2. package/README.md +127 -0
  3. package/package.json +34 -0
  4. package/scaffold/.agents/flow/archived/tmpl_.gitkeep +0 -0
  5. package/scaffold/.agents/flow/tmpl_README.md +7 -0
  6. package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +11 -0
  7. package/scaffold/.agents/skills/automated-fix/tmpl_SKILL.md +67 -0
  8. package/scaffold/.agents/skills/create-issue/tmpl_SKILL.md +68 -0
  9. package/scaffold/.agents/skills/create-pr-document/tmpl_SKILL.md +125 -0
  10. package/scaffold/.agents/skills/create-project-context/tmpl_SKILL.md +168 -0
  11. package/scaffold/.agents/skills/create-test-plan/tmpl_SKILL.md +86 -0
  12. package/scaffold/.agents/skills/debug/tmpl_SKILL.md +19 -0
  13. package/scaffold/.agents/skills/evaluate/tmpl_SKILL.md +19 -0
  14. package/scaffold/.agents/skills/execute-test-batch/tmpl_SKILL.md +49 -0
  15. package/scaffold/.agents/skills/execute-test-case/tmpl_SKILL.md +47 -0
  16. package/scaffold/.agents/skills/implement-user-story/tmpl_SKILL.md +68 -0
  17. package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +19 -0
  18. package/scaffold/.agents/skills/refactor-prd/tmpl_SKILL.md +19 -0
  19. package/scaffold/.agents/skills/refine-pr-document/tmpl_SKILL.md +108 -0
  20. package/scaffold/.agents/skills/refine-project-context/tmpl_SKILL.md +157 -0
  21. package/scaffold/.agents/skills/refine-test-plan/tmpl_SKILL.md +76 -0
  22. package/scaffold/.agents/tmpl_PROJECT_CONTEXT.md +3 -0
  23. package/scaffold/.agents/tmpl_state.example.json +26 -0
  24. package/scaffold/.agents/tmpl_state_rules.md +29 -0
  25. package/scaffold/docs/nvst-flow/templates/tmpl_CHANGELOG.md +18 -0
  26. package/scaffold/docs/nvst-flow/templates/tmpl_TECHNICAL_DEBT.md +11 -0
  27. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_evaluation-report.md +19 -0
  28. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_product-requirement-document.md +19 -0
  29. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_refactor_plan.md +19 -0
  30. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_test-plan.md +19 -0
  31. package/scaffold/docs/nvst-flow/tmpl_COMMANDS.md +0 -0
  32. package/scaffold/docs/nvst-flow/tmpl_QUICK_USE.md +0 -0
  33. package/scaffold/docs/tmpl_PLACEHOLDER.md +0 -0
  34. package/scaffold/schemas/node-shims.d.ts +15 -0
  35. package/scaffold/schemas/tmpl_issues.ts +19 -0
  36. package/scaffold/schemas/tmpl_prd.ts +26 -0
  37. package/scaffold/schemas/tmpl_progress.ts +39 -0
  38. package/scaffold/schemas/tmpl_state.ts +81 -0
  39. package/scaffold/schemas/tmpl_test-plan.ts +20 -0
  40. package/scaffold/schemas/tmpl_validate-progress.ts +13 -0
  41. package/scaffold/schemas/tmpl_validate-state.ts +13 -0
  42. package/scaffold/tmpl_AGENTS.md +7 -0
  43. package/schemas/prd.ts +26 -0
  44. package/schemas/progress.ts +39 -0
  45. package/schemas/state.ts +81 -0
  46. package/schemas/test-plan.test.ts +53 -0
  47. package/schemas/test-plan.ts +20 -0
  48. package/schemas/validate-progress.ts +13 -0
  49. package/schemas/validate-state.ts +13 -0
  50. package/src/agent.test.ts +37 -0
  51. package/src/agent.ts +225 -0
  52. package/src/cli-path.ts +4 -0
  53. package/src/cli.ts +578 -0
  54. package/src/commands/approve-project-context.ts +37 -0
  55. package/src/commands/approve-requirement.ts +217 -0
  56. package/src/commands/approve-test-plan.test.ts +193 -0
  57. package/src/commands/approve-test-plan.ts +202 -0
  58. package/src/commands/create-issue.test.ts +484 -0
  59. package/src/commands/create-issue.ts +371 -0
  60. package/src/commands/create-project-context.ts +96 -0
  61. package/src/commands/create-prototype.test.ts +153 -0
  62. package/src/commands/create-prototype.ts +425 -0
  63. package/src/commands/create-test-plan.test.ts +381 -0
  64. package/src/commands/create-test-plan.ts +248 -0
  65. package/src/commands/define-requirement.ts +47 -0
  66. package/src/commands/destroy.ts +113 -0
  67. package/src/commands/execute-automated-fix.test.ts +580 -0
  68. package/src/commands/execute-automated-fix.ts +363 -0
  69. package/src/commands/execute-manual-fix.test.ts +343 -0
  70. package/src/commands/execute-manual-fix.ts +203 -0
  71. package/src/commands/execute-test-plan.test.ts +1891 -0
  72. package/src/commands/execute-test-plan.ts +722 -0
  73. package/src/commands/init.ts +85 -0
  74. package/src/commands/refine-project-context.ts +74 -0
  75. package/src/commands/refine-requirement.ts +60 -0
  76. package/src/commands/refine-test-plan.test.ts +200 -0
  77. package/src/commands/refine-test-plan.ts +93 -0
  78. package/src/commands/start-iteration.test.ts +144 -0
  79. package/src/commands/start-iteration.ts +101 -0
  80. package/src/commands/write-json.ts +136 -0
  81. package/src/install.test.ts +124 -0
  82. package/src/pack.test.ts +103 -0
  83. package/src/state.test.ts +66 -0
  84. package/src/state.ts +52 -0
  85. package/tsconfig.json +15 -0
@@ -0,0 +1,425 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { $ as dollar } from "bun";
4
+ import { z } from "zod";
5
+
6
+ import { PrdSchema } from "../../scaffold/schemas/tmpl_prd";
7
+ import {
8
+ buildPrompt,
9
+ invokeAgent,
10
+ loadSkill,
11
+ type AgentProvider,
12
+ } from "../agent";
13
+ import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
14
+
15
+ export interface CreatePrototypeOptions {
16
+ provider: AgentProvider;
17
+ iterations?: number;
18
+ retryOnFail?: number;
19
+ stopOnCritical?: boolean;
20
+ }
21
+
22
+ const ProgressEntrySchema = z.object({
23
+ use_case_id: z.string(),
24
+ status: z.enum(["pending", "failed", "completed"]),
25
+ attempt_count: z.number().int().nonnegative(),
26
+ last_agent_exit_code: z.number().int().nullable(),
27
+ quality_checks: z.array(
28
+ z.object({
29
+ command: z.string(),
30
+ exit_code: z.number().int(),
31
+ }),
32
+ ),
33
+ last_error_summary: z.string(),
34
+ updated_at: z.string(),
35
+ });
36
+
37
+ export const PrototypeProgressSchema = z.object({
38
+ entries: z.array(ProgressEntrySchema),
39
+ });
40
+
41
+ function sortedValues(values: string[]): string[] {
42
+ return [...values].sort((a, b) => a.localeCompare(b));
43
+ }
44
+
45
+ function idsMatchExactly(left: string[], right: string[]): boolean {
46
+ if (left.length !== right.length) {
47
+ return false;
48
+ }
49
+
50
+ for (let i = 0; i < left.length; i += 1) {
51
+ if (left[i] !== right[i]) {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ return true;
57
+ }
58
+
59
+ function parseQualityChecks(projectContextContent: string): string[] {
60
+ const normalized = projectContextContent.replace(/\r\n/g, "\n");
61
+ const lines = normalized.split("\n");
62
+
63
+ const testingStrategyStart = lines.findIndex((line) =>
64
+ /^##\s+Testing Strategy\b/.test(line),
65
+ );
66
+ if (testingStrategyStart === -1) {
67
+ return [];
68
+ }
69
+
70
+ const testingStrategyEndOffset = lines
71
+ .slice(testingStrategyStart + 1)
72
+ .findIndex((line) => /^##\s+/.test(line));
73
+ const testingStrategyEnd = testingStrategyEndOffset === -1
74
+ ? lines.length
75
+ : testingStrategyStart + 1 + testingStrategyEndOffset;
76
+ const testingStrategyLines = lines.slice(testingStrategyStart, testingStrategyEnd);
77
+
78
+ const qualityChecksStart = testingStrategyLines.findIndex((line) =>
79
+ /^###\s+Quality Checks\b/.test(line),
80
+ );
81
+ if (qualityChecksStart === -1) {
82
+ return [];
83
+ }
84
+
85
+ const qualityChecksEndOffset = testingStrategyLines
86
+ .slice(qualityChecksStart + 1)
87
+ .findIndex((line) => /^###\s+/.test(line));
88
+ const qualityChecksEnd = qualityChecksEndOffset === -1
89
+ ? testingStrategyLines.length
90
+ : qualityChecksStart + 1 + qualityChecksEndOffset;
91
+ const qualityChecksSection = testingStrategyLines
92
+ .slice(qualityChecksStart, qualityChecksEnd)
93
+ .join("\n");
94
+
95
+ const codeBlockMatch = qualityChecksSection.match(/```(?:\w+)?\n([\s\S]*?)```/m);
96
+ if (!codeBlockMatch) {
97
+ return [];
98
+ }
99
+
100
+ return codeBlockMatch[1]
101
+ .split("\n")
102
+ .map((line) => line.trim())
103
+ .filter((line) => line.length > 0);
104
+ }
105
+
106
+ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<void> {
107
+ const projectRoot = process.cwd();
108
+ const state = await readState(projectRoot);
109
+
110
+ if (opts.iterations !== undefined && (!Number.isInteger(opts.iterations) || opts.iterations < 1)) {
111
+ throw new Error(
112
+ "Invalid --iterations value. Expected an integer >= 1.",
113
+ );
114
+ }
115
+
116
+ if (
117
+ opts.retryOnFail !== undefined &&
118
+ (!Number.isInteger(opts.retryOnFail) || opts.retryOnFail < 0)
119
+ ) {
120
+ throw new Error(
121
+ "Invalid --retry-on-fail value. Expected an integer >= 0.",
122
+ );
123
+ }
124
+
125
+ const iteration = state.current_iteration;
126
+ const prdFileName = `it_${iteration}_PRD.json`;
127
+ const prdPath = join(projectRoot, FLOW_REL_DIR, prdFileName);
128
+
129
+ if (!(await exists(prdPath))) {
130
+ throw new Error(
131
+ `PRD source of truth missing: expected ${join(FLOW_REL_DIR, prdFileName)}. Run \`bun nvst approve requirement\` first.`,
132
+ );
133
+ }
134
+
135
+ let parsedPrd: unknown;
136
+ try {
137
+ parsedPrd = JSON.parse(await readFile(prdPath, "utf8"));
138
+ } catch {
139
+ throw new Error(
140
+ `Deterministic validation error: invalid PRD JSON in ${join(FLOW_REL_DIR, prdFileName)}.`,
141
+ );
142
+ }
143
+
144
+ const prdValidation = PrdSchema.safeParse(parsedPrd);
145
+ if (!prdValidation.success) {
146
+ throw new Error(
147
+ `Deterministic validation error: PRD schema mismatch in ${join(FLOW_REL_DIR, prdFileName)}.`,
148
+ );
149
+ }
150
+
151
+ if (state.current_phase === "define") {
152
+ if (
153
+ state.phases.define.prd_generation.status === "completed" &&
154
+ state.phases.prototype.project_context.status === "created"
155
+ ) {
156
+ const workingTreeBeforeTransition = await dollar`git status --porcelain`.cwd(projectRoot).nothrow().quiet();
157
+ if (workingTreeBeforeTransition.exitCode !== 0) {
158
+ throw new Error(
159
+ "Unable to verify git working tree status. Ensure this directory is a git repository and git is installed.",
160
+ );
161
+ }
162
+ if (workingTreeBeforeTransition.stdout.toString().trim().length > 0) {
163
+ throw new Error(
164
+ "Git working tree is dirty. Commit your changes or discard them before running `bun nvst create prototype` again.",
165
+ );
166
+ }
167
+ state.current_phase = "prototype";
168
+ await writeState(projectRoot, state);
169
+ } else {
170
+ throw new Error(
171
+ "Cannot create prototype: current_phase is define and prerequisites are not met. Complete define phase and run `bun nvst create project-context --agent <provider>` then `bun nvst approve project-context` first.",
172
+ );
173
+ }
174
+ } else if (state.current_phase !== "prototype") {
175
+ throw new Error(
176
+ "Cannot create prototype: current_phase must be define (with approved PRD) or prototype. Complete define phase and run `bun nvst create project-context --agent <provider>` then `bun nvst approve project-context` first.",
177
+ );
178
+ }
179
+
180
+ if (state.phases.prototype.project_context.status !== "created") {
181
+ throw new Error(
182
+ "Cannot create prototype: prototype.project_context.status must be created. Run `bun nvst create project-context --agent <provider>` and `bun nvst approve project-context` first.",
183
+ );
184
+ }
185
+
186
+ const workingTreeAfterPhase = await dollar`git status --porcelain`.cwd(projectRoot).nothrow().quiet();
187
+ if (workingTreeAfterPhase.exitCode !== 0) {
188
+ throw new Error(
189
+ "Unable to verify git working tree status. Ensure this directory is a git repository and git is installed.",
190
+ );
191
+ }
192
+ if (workingTreeAfterPhase.stdout.toString().trim().length > 0) {
193
+ throw new Error(
194
+ "Git working tree is dirty. Commit your changes or discard them before running `bun nvst create prototype` again.",
195
+ );
196
+ }
197
+
198
+ const branchName = `feature/it_${iteration}`;
199
+ const branchExistsResult = await dollar`git rev-parse --verify ${branchName}`
200
+ .cwd(projectRoot)
201
+ .nothrow()
202
+ .quiet();
203
+
204
+ if (branchExistsResult.exitCode !== 0) {
205
+ const createBranchResult = await dollar`git checkout -b ${branchName}`.cwd(projectRoot).nothrow().quiet();
206
+ if (createBranchResult.exitCode !== 0) {
207
+ throw new Error(
208
+ `Failed to create and checkout branch '${branchName}'. Resolve git errors and retry.`,
209
+ );
210
+ }
211
+ } else {
212
+ const checkoutBranchResult = await dollar`git checkout ${branchName}`.cwd(projectRoot).nothrow().quiet();
213
+ if (checkoutBranchResult.exitCode !== 0) {
214
+ throw new Error(
215
+ `Failed to checkout branch '${branchName}'. Resolve git errors and retry.`,
216
+ );
217
+ }
218
+ }
219
+
220
+ const progressFileName = `it_${iteration}_progress.json`;
221
+ const progressPath = join(projectRoot, FLOW_REL_DIR, progressFileName);
222
+ const storyIds = sortedValues(prdValidation.data.userStories.map((story) => story.id));
223
+ let progressData: z.infer<typeof PrototypeProgressSchema>;
224
+
225
+ if (await exists(progressPath)) {
226
+ let parsedProgress: unknown;
227
+ try {
228
+ parsedProgress = JSON.parse(await readFile(progressPath, "utf8"));
229
+ } catch {
230
+ throw new Error(
231
+ `Deterministic validation error: invalid progress JSON in ${join(FLOW_REL_DIR, progressFileName)}.`,
232
+ );
233
+ }
234
+
235
+ const progressValidation = PrototypeProgressSchema.safeParse(parsedProgress);
236
+ if (!progressValidation.success) {
237
+ throw new Error(
238
+ `Deterministic validation error: progress schema mismatch in ${join(FLOW_REL_DIR, progressFileName)}.`,
239
+ );
240
+ }
241
+
242
+ const existingIds = sortedValues(
243
+ progressValidation.data.entries.map((entry) => entry.use_case_id),
244
+ );
245
+
246
+ if (!idsMatchExactly(existingIds, storyIds)) {
247
+ throw new Error(
248
+ "Progress file out of sync: use_case_id values do not match PRD user story ids.",
249
+ );
250
+ }
251
+ progressData = progressValidation.data;
252
+ } else {
253
+ const now = new Date().toISOString();
254
+ const progress = {
255
+ entries: prdValidation.data.userStories.map((story) => ({
256
+ use_case_id: story.id,
257
+ status: "pending" as const,
258
+ attempt_count: 0,
259
+ last_agent_exit_code: null,
260
+ quality_checks: [],
261
+ last_error_summary: "",
262
+ updated_at: now,
263
+ })),
264
+ };
265
+
266
+ await writeFile(progressPath, `${JSON.stringify(progress, null, 2)}\n`, "utf8");
267
+ progressData = progress;
268
+ }
269
+
270
+ const eligibleStories = prdValidation.data.userStories.filter((story) => {
271
+ const entry = progressData.entries.find((item) => item.use_case_id === story.id);
272
+ return entry !== undefined && (entry.status === "pending" || entry.status === "failed");
273
+ });
274
+
275
+ if (eligibleStories.length === 0) {
276
+ console.log("No pending or failed user stories to implement. Exiting without changes.");
277
+ return;
278
+ }
279
+
280
+ state.phases.prototype.prototype_build.status = "in_progress";
281
+ state.phases.prototype.prototype_build.file = progressFileName;
282
+ state.last_updated = new Date().toISOString();
283
+ state.updated_by = "nvst:create-prototype";
284
+ await writeState(projectRoot, state);
285
+
286
+ let skillTemplate: string;
287
+ try {
288
+ skillTemplate = await loadSkill(projectRoot, "implement-user-story");
289
+ } catch {
290
+ throw new Error(
291
+ "Required skill missing: expected .agents/skills/implement-user-story/SKILL.md.",
292
+ );
293
+ }
294
+
295
+ const projectContextPath = join(projectRoot, ".agents", "PROJECT_CONTEXT.md");
296
+ if (!(await exists(projectContextPath))) {
297
+ throw new Error("Project context missing: expected .agents/PROJECT_CONTEXT.md.");
298
+ }
299
+ const projectContextContent = await readFile(projectContextPath, "utf8");
300
+ const qualityCheckCommands = parseQualityChecks(projectContextContent);
301
+
302
+ const maxStoriesToProcess = opts.iterations ?? Number.POSITIVE_INFINITY;
303
+ const maxRetriesPerStory = opts.retryOnFail ?? 0;
304
+
305
+ let storiesAttempted = 0;
306
+ let haltedByCritical = false;
307
+
308
+ for (const story of eligibleStories) {
309
+ if (storiesAttempted >= maxStoriesToProcess || haltedByCritical) {
310
+ break;
311
+ }
312
+
313
+ const entry = progressData.entries.find((item) => item.use_case_id === story.id);
314
+ if (!entry) {
315
+ continue;
316
+ }
317
+
318
+ const maxAttemptsForStory = 1 + maxRetriesPerStory;
319
+
320
+ for (let attempt = 1; attempt <= maxAttemptsForStory; attempt += 1) {
321
+ const prompt = buildPrompt(skillTemplate, {
322
+ iteration: iteration,
323
+ project_context: projectContextContent,
324
+ user_story: JSON.stringify(story, null, 2),
325
+ });
326
+
327
+ const agentResult = await invokeAgent({
328
+ provider: opts.provider,
329
+ prompt,
330
+ cwd: projectRoot,
331
+ interactive: false,
332
+ });
333
+
334
+ const qualityResults: Array<{ command: string; exit_code: number }> = [];
335
+ for (const cmd of qualityCheckCommands) {
336
+ const proc = Bun.spawn(["sh", "-c", cmd], {
337
+ cwd: projectRoot,
338
+ stdout: "ignore",
339
+ stderr: "ignore",
340
+ });
341
+ const exitCode = await proc.exited;
342
+ qualityResults.push({ command: cmd, exit_code: exitCode });
343
+ }
344
+
345
+ const checksPassed = qualityResults.every((result) => result.exit_code === 0);
346
+ const allPassed = agentResult.exitCode === 0 && checksPassed;
347
+
348
+ entry.attempt_count += 1;
349
+ entry.last_agent_exit_code = agentResult.exitCode;
350
+ entry.quality_checks = qualityResults;
351
+ entry.updated_at = new Date().toISOString();
352
+
353
+ if (allPassed) {
354
+ entry.status = "completed";
355
+ entry.last_error_summary = "";
356
+ } else {
357
+ entry.status = "failed";
358
+ entry.last_error_summary = "Agent or quality check failed";
359
+ }
360
+
361
+ await writeFile(progressPath, `${JSON.stringify(progressData, null, 2)}\n`, "utf8");
362
+
363
+ if (allPassed) {
364
+ const commitMessage = `feat: implement ${story.id} - ${story.title}`;
365
+ const commitResult = await dollar`git add -A && git commit -m ${commitMessage}`
366
+ .cwd(projectRoot)
367
+ .nothrow()
368
+ .quiet();
369
+
370
+ if (commitResult.exitCode !== 0) {
371
+ entry.status = "failed";
372
+ entry.last_error_summary = "Git commit failed";
373
+ entry.updated_at = new Date().toISOString();
374
+ await writeFile(progressPath, `${JSON.stringify(progressData, null, 2)}\n`, "utf8");
375
+
376
+ console.log(
377
+ `iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=commit_failed`,
378
+ );
379
+
380
+ if (opts.stopOnCritical) {
381
+ haltedByCritical = true;
382
+ }
383
+ } else {
384
+ console.log(
385
+ `iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=passed`,
386
+ );
387
+ }
388
+
389
+ break;
390
+ }
391
+
392
+ console.log(
393
+ `iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=failed`,
394
+ );
395
+
396
+ if (opts.stopOnCritical) {
397
+ haltedByCritical = true;
398
+ break;
399
+ }
400
+
401
+ if (attempt < maxAttemptsForStory) {
402
+ continue;
403
+ }
404
+ }
405
+
406
+ storiesAttempted += 1;
407
+ }
408
+
409
+ const allCompleted = progressData.entries.every((entry) => entry.status === "completed");
410
+ state.phases.prototype.prototype_build.status = allCompleted ? "created" : "in_progress";
411
+ state.last_updated = new Date().toISOString();
412
+ state.updated_by = "nvst:create-prototype";
413
+ await writeState(projectRoot, state);
414
+
415
+ if (storiesAttempted === 0) {
416
+ console.log("No user stories attempted.");
417
+ return;
418
+ }
419
+
420
+ if (allCompleted) {
421
+ console.log("Prototype implementation completed for all user stories.");
422
+ } else {
423
+ console.log("Prototype implementation paused with remaining pending or failed stories.");
424
+ }
425
+ }