@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.
- package/AGENTS.md +7 -0
- package/README.md +127 -0
- package/package.json +34 -0
- package/scaffold/.agents/flow/archived/tmpl_.gitkeep +0 -0
- package/scaffold/.agents/flow/tmpl_README.md +7 -0
- package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +11 -0
- package/scaffold/.agents/skills/automated-fix/tmpl_SKILL.md +67 -0
- package/scaffold/.agents/skills/create-issue/tmpl_SKILL.md +68 -0
- package/scaffold/.agents/skills/create-pr-document/tmpl_SKILL.md +125 -0
- package/scaffold/.agents/skills/create-project-context/tmpl_SKILL.md +168 -0
- package/scaffold/.agents/skills/create-test-plan/tmpl_SKILL.md +86 -0
- package/scaffold/.agents/skills/debug/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/evaluate/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/execute-test-batch/tmpl_SKILL.md +49 -0
- package/scaffold/.agents/skills/execute-test-case/tmpl_SKILL.md +47 -0
- package/scaffold/.agents/skills/implement-user-story/tmpl_SKILL.md +68 -0
- package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/refactor-prd/tmpl_SKILL.md +19 -0
- package/scaffold/.agents/skills/refine-pr-document/tmpl_SKILL.md +108 -0
- package/scaffold/.agents/skills/refine-project-context/tmpl_SKILL.md +157 -0
- package/scaffold/.agents/skills/refine-test-plan/tmpl_SKILL.md +76 -0
- package/scaffold/.agents/tmpl_PROJECT_CONTEXT.md +3 -0
- package/scaffold/.agents/tmpl_state.example.json +26 -0
- package/scaffold/.agents/tmpl_state_rules.md +29 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_CHANGELOG.md +18 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_TECHNICAL_DEBT.md +11 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_evaluation-report.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_product-requirement-document.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_refactor_plan.md +19 -0
- package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_test-plan.md +19 -0
- package/scaffold/docs/nvst-flow/tmpl_COMMANDS.md +0 -0
- package/scaffold/docs/nvst-flow/tmpl_QUICK_USE.md +0 -0
- package/scaffold/docs/tmpl_PLACEHOLDER.md +0 -0
- package/scaffold/schemas/node-shims.d.ts +15 -0
- package/scaffold/schemas/tmpl_issues.ts +19 -0
- package/scaffold/schemas/tmpl_prd.ts +26 -0
- package/scaffold/schemas/tmpl_progress.ts +39 -0
- package/scaffold/schemas/tmpl_state.ts +81 -0
- package/scaffold/schemas/tmpl_test-plan.ts +20 -0
- package/scaffold/schemas/tmpl_validate-progress.ts +13 -0
- package/scaffold/schemas/tmpl_validate-state.ts +13 -0
- package/scaffold/tmpl_AGENTS.md +7 -0
- package/schemas/prd.ts +26 -0
- package/schemas/progress.ts +39 -0
- package/schemas/state.ts +81 -0
- package/schemas/test-plan.test.ts +53 -0
- package/schemas/test-plan.ts +20 -0
- package/schemas/validate-progress.ts +13 -0
- package/schemas/validate-state.ts +13 -0
- package/src/agent.test.ts +37 -0
- package/src/agent.ts +225 -0
- package/src/cli-path.ts +4 -0
- package/src/cli.ts +578 -0
- package/src/commands/approve-project-context.ts +37 -0
- package/src/commands/approve-requirement.ts +217 -0
- package/src/commands/approve-test-plan.test.ts +193 -0
- package/src/commands/approve-test-plan.ts +202 -0
- package/src/commands/create-issue.test.ts +484 -0
- package/src/commands/create-issue.ts +371 -0
- package/src/commands/create-project-context.ts +96 -0
- package/src/commands/create-prototype.test.ts +153 -0
- package/src/commands/create-prototype.ts +425 -0
- package/src/commands/create-test-plan.test.ts +381 -0
- package/src/commands/create-test-plan.ts +248 -0
- package/src/commands/define-requirement.ts +47 -0
- package/src/commands/destroy.ts +113 -0
- package/src/commands/execute-automated-fix.test.ts +580 -0
- package/src/commands/execute-automated-fix.ts +363 -0
- package/src/commands/execute-manual-fix.test.ts +343 -0
- package/src/commands/execute-manual-fix.ts +203 -0
- package/src/commands/execute-test-plan.test.ts +1891 -0
- package/src/commands/execute-test-plan.ts +722 -0
- package/src/commands/init.ts +85 -0
- package/src/commands/refine-project-context.ts +74 -0
- package/src/commands/refine-requirement.ts +60 -0
- package/src/commands/refine-test-plan.test.ts +200 -0
- package/src/commands/refine-test-plan.ts +93 -0
- package/src/commands/start-iteration.test.ts +144 -0
- package/src/commands/start-iteration.ts +101 -0
- package/src/commands/write-json.ts +136 -0
- package/src/install.test.ts +124 -0
- package/src/pack.test.ts +103 -0
- package/src/state.test.ts +66 -0
- package/src/state.ts +52 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { buildCommand, invokeAgent, parseAgentArg, parseProvider } from "./agent";
|
|
4
|
+
|
|
5
|
+
describe("agent provider parsing", () => {
|
|
6
|
+
test("accepts cursor as a valid provider in --agent argument parsing", () => {
|
|
7
|
+
const parsed = parseAgentArg(["create", "--agent", "cursor", "--force"]);
|
|
8
|
+
|
|
9
|
+
expect(parsed.provider).toBe("cursor");
|
|
10
|
+
expect(parsed.remainingArgs).toEqual(["create", "--force"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("maps cursor provider to the Cursor agent CLI binary", () => {
|
|
14
|
+
expect(parseProvider("cursor")).toBe("cursor");
|
|
15
|
+
expect(buildCommand("cursor")).toEqual({ cmd: "agent", args: [] });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("unknown provider error includes cursor in valid provider list", () => {
|
|
19
|
+
expect(() => parseProvider("unknown-provider")).toThrow(
|
|
20
|
+
"Unknown agent provider 'unknown-provider'. Valid providers: claude, codex, gemini, cursor",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("agent invocation command availability", () => {
|
|
26
|
+
test("returns a clear error when cursor provider is selected but `agent` is not in PATH", async () => {
|
|
27
|
+
await expect(
|
|
28
|
+
invokeAgent({
|
|
29
|
+
provider: "cursor",
|
|
30
|
+
prompt: "Test prompt",
|
|
31
|
+
resolveCommandPath: () => null,
|
|
32
|
+
}),
|
|
33
|
+
).rejects.toThrow(
|
|
34
|
+
"Cursor agent CLI is unavailable: `agent` command not found in PATH. Install/configure Cursor Agent CLI or use another provider.",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { exists } from "./state";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export type AgentProvider = "claude" | "codex" | "gemini" | "cursor";
|
|
11
|
+
|
|
12
|
+
export interface AgentInvokeOptions {
|
|
13
|
+
provider: AgentProvider;
|
|
14
|
+
prompt: string;
|
|
15
|
+
cwd?: string;
|
|
16
|
+
/** Use interactive mode (e.g. Codex TUI) so the agent can interview the user. */
|
|
17
|
+
interactive?: boolean;
|
|
18
|
+
resolveCommandPath?: ResolveCommandPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AgentResult {
|
|
22
|
+
exitCode: number;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ResolveCommandPath = (command: string) => string | null | undefined;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Provider helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const PROVIDERS: Record<AgentProvider, { cmd: string; args: string[] }> = {
|
|
34
|
+
claude: { cmd: "claude", args: ["--dangerously-skip-permissions", "--print"] },
|
|
35
|
+
codex: { cmd: "codex", args: ["exec", "--dangerously-bypass-approvals-and-sandbox"] },
|
|
36
|
+
gemini: { cmd: "gemini", args: ["--yolo"] },
|
|
37
|
+
cursor: { cmd: "agent", args: [] },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function parseProvider(name: string): AgentProvider {
|
|
41
|
+
if (name in PROVIDERS) return name as AgentProvider;
|
|
42
|
+
const valid = Object.keys(PROVIDERS).join(", ");
|
|
43
|
+
throw new Error(`Unknown agent provider '${name}'. Valid providers: ${valid}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildCommand(provider: AgentProvider): { cmd: string; args: string[] } {
|
|
47
|
+
return PROVIDERS[provider];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defaultResolveCommandPath(command: string): string | null {
|
|
51
|
+
const resolved = Bun.which(command);
|
|
52
|
+
return resolved ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ensureAgentCommandAvailable(
|
|
56
|
+
provider: AgentProvider,
|
|
57
|
+
resolveCommandPath: ResolveCommandPath = defaultResolveCommandPath,
|
|
58
|
+
): void {
|
|
59
|
+
const { cmd } = buildCommand(provider);
|
|
60
|
+
if (resolveCommandPath(cmd)) return;
|
|
61
|
+
|
|
62
|
+
if (provider === "cursor") {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Cursor agent CLI is unavailable: `agent` command not found in PATH. Install/configure Cursor Agent CLI or use another provider.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error(`Required CLI '${cmd}' for provider '${provider}' is not in PATH.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Agent invocation
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// For interactive interviews, claude/codex need a real TTY: pass prompt as
|
|
75
|
+
// positional arg and use stdin: "inherit" so the agent can read user input.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** Codex interactive (TUI) args: no "exec", so it can read stdin for interview. */
|
|
79
|
+
const CODEX_INTERACTIVE_ARGS = ["--sandbox", "workspace-write"];
|
|
80
|
+
|
|
81
|
+
export async function invokeAgent(options: AgentInvokeOptions): Promise<AgentResult> {
|
|
82
|
+
const {
|
|
83
|
+
provider,
|
|
84
|
+
prompt,
|
|
85
|
+
cwd = process.cwd(),
|
|
86
|
+
interactive = false,
|
|
87
|
+
resolveCommandPath = defaultResolveCommandPath,
|
|
88
|
+
} = options;
|
|
89
|
+
const { cmd, args } = buildCommand(provider);
|
|
90
|
+
ensureAgentCommandAvailable(provider, resolveCommandPath);
|
|
91
|
+
|
|
92
|
+
let finalArgs: string[];
|
|
93
|
+
let stdinOption: "ignore" | "inherit";
|
|
94
|
+
if (provider === "gemini" && interactive) {
|
|
95
|
+
// Interactive mode: drop -p so Gemini runs as a conversational session.
|
|
96
|
+
// Flags must come before the positional prompt arg.
|
|
97
|
+
finalArgs = [...args, prompt];
|
|
98
|
+
stdinOption = "inherit";
|
|
99
|
+
} else if (provider === "gemini") {
|
|
100
|
+
finalArgs = [...args, "-p", prompt];
|
|
101
|
+
stdinOption = "ignore";
|
|
102
|
+
} else if (provider === "codex" && interactive) {
|
|
103
|
+
// Codex interactive mode (no "exec"): prompt first, then TUI can read user input.
|
|
104
|
+
finalArgs = [prompt, ...CODEX_INTERACTIVE_ARGS];
|
|
105
|
+
stdinOption = "inherit";
|
|
106
|
+
} else if (provider === "claude" && interactive) {
|
|
107
|
+
// Interactive mode: drop --print so Claude runs as a conversational TUI.
|
|
108
|
+
finalArgs = ["--dangerously-skip-permissions", prompt];
|
|
109
|
+
stdinOption = "inherit";
|
|
110
|
+
} else {
|
|
111
|
+
// Claude or Codex exec: prompt as last arg, inherit stdin.
|
|
112
|
+
finalArgs = [...args, prompt];
|
|
113
|
+
stdinOption = "inherit";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Interactive mode: use real TTY (inherit) so the agent stays in interactive mode and
|
|
117
|
+
// waits for user input. The agent writes output via write-json or to a file.
|
|
118
|
+
// Non-interactive: use pipe to capture stdout/stderr.
|
|
119
|
+
const useTty = interactive;
|
|
120
|
+
const proc = Bun.spawn([cmd, ...finalArgs], {
|
|
121
|
+
cwd,
|
|
122
|
+
stdin: stdinOption,
|
|
123
|
+
stdout: useTty ? "inherit" : "pipe",
|
|
124
|
+
stderr: useTty ? "inherit" : "pipe",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
let exitCode: number;
|
|
128
|
+
let stdoutStr: string;
|
|
129
|
+
let stderrStr: string;
|
|
130
|
+
|
|
131
|
+
if (useTty) {
|
|
132
|
+
exitCode = await proc.exited;
|
|
133
|
+
stdoutStr = "";
|
|
134
|
+
stderrStr = "";
|
|
135
|
+
} else {
|
|
136
|
+
const stdoutChunks: string[] = [];
|
|
137
|
+
const stderrChunks: string[] = [];
|
|
138
|
+
const readStream = async (
|
|
139
|
+
stream: ReadableStream<Uint8Array<ArrayBuffer>> | undefined,
|
|
140
|
+
chunks: string[],
|
|
141
|
+
passthrough?: { write: (chunk: Uint8Array) => void },
|
|
142
|
+
) => {
|
|
143
|
+
if (!stream) return;
|
|
144
|
+
const reader = stream.getReader();
|
|
145
|
+
const decoder = new TextDecoder();
|
|
146
|
+
try {
|
|
147
|
+
while (true) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
151
|
+
if (passthrough) passthrough.write(value);
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
reader.releaseLock();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
await Promise.all([
|
|
158
|
+
readStream(proc.stdout, stdoutChunks, process.stdout),
|
|
159
|
+
readStream(proc.stderr, stderrChunks, process.stderr),
|
|
160
|
+
]);
|
|
161
|
+
exitCode = await proc.exited;
|
|
162
|
+
stdoutStr = stdoutChunks.join("");
|
|
163
|
+
stderrStr = stderrChunks.join("");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
exitCode,
|
|
168
|
+
stdout: stdoutStr,
|
|
169
|
+
stderr: stderrStr,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Skill loading
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
export async function loadSkill(projectRoot: string, skillName: string): Promise<string> {
|
|
178
|
+
const skillPath = join(projectRoot, ".agents", "skills", skillName, "SKILL.md");
|
|
179
|
+
if (!(await exists(skillPath))) {
|
|
180
|
+
throw new Error(`Skill '${skillName}' not found at ${skillPath}`);
|
|
181
|
+
}
|
|
182
|
+
const raw = await readFile(skillPath, "utf8");
|
|
183
|
+
return stripFrontmatter(raw);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stripFrontmatter(content: string): string {
|
|
187
|
+
const trimmed = content.trimStart();
|
|
188
|
+
if (!trimmed.startsWith("---")) return content;
|
|
189
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
190
|
+
if (endIndex === -1) return content;
|
|
191
|
+
return trimmed.slice(endIndex + 3).trimStart();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Prompt building
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
export function buildPrompt(skillBody: string, context: Record<string, string>): string {
|
|
199
|
+
const parts = [skillBody];
|
|
200
|
+
const entries = Object.entries(context);
|
|
201
|
+
if (entries.length > 0) {
|
|
202
|
+
parts.push("\n---\n\n## Context\n");
|
|
203
|
+
for (const [key, value] of entries) {
|
|
204
|
+
parts.push(`### ${key}\n\n${value}\n`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return parts.join("\n");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// CLI arg parsing for --agent
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
export function parseAgentArg(args: string[]): {
|
|
215
|
+
provider: AgentProvider;
|
|
216
|
+
remainingArgs: string[];
|
|
217
|
+
} {
|
|
218
|
+
const idx = args.indexOf("--agent");
|
|
219
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
220
|
+
throw new Error("Missing required --agent <provider> argument.");
|
|
221
|
+
}
|
|
222
|
+
const provider = parseProvider(args[idx + 1]);
|
|
223
|
+
const remainingArgs = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
224
|
+
return { provider, remainingArgs };
|
|
225
|
+
}
|
package/src/cli-path.ts
ADDED