stonecut 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elkin Torres
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # Stonecut
2
+
3
+ A CLI that drives PRD-driven development with agentic coding CLIs. You write the PRD, Stonecut executes the issues one by one.
4
+
5
+ ## Workflow
6
+
7
+ Ideas can come from anywhere — Jira tickets, Slack threads, MCP servers, or just a conversation. The pipeline starts once you're ready to act on one:
8
+
9
+ 1. **`/stonecut-interview`** — Stress-test the idea. Get grilled on the plan until it's solid.
10
+ 2. **`/stonecut-prd`** — Turn the validated idea into a PRD (local file or GitHub issue).
11
+ 3. **`/stonecut-issues`** — Break the PRD into independently-grabbable issues (local markdown files or GitHub sub-issues).
12
+ 4. **`stonecut run`** — Execute the issues sequentially with an agentic coding CLI.
13
+
14
+ Steps 1–3 are Claude Code skills installed via `stonecut setup-skills`. Step 4 is the Stonecut CLI.
15
+
16
+ ### Suggested: managing your idea backlog
17
+
18
+ For projects using GitHub issues, we recommend tracking ideas with a `roadmap` label. When an idea is ready, interview it, write the PRD (which closes the roadmap issue), break it into sub-issues, and execute. See [DESIGN.md](DESIGN.md#suggested-practice-managing-your-idea-backlog-with-github-labels) for the full flow.
19
+
20
+ ## Installation
21
+
22
+ ### Prerequisites
23
+
24
+ - [Bun](https://bun.sh/) — install with `curl -fsSL https://bun.sh/install | bash`
25
+ - An agentic coding CLI — [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) is the default runner and must be in your PATH. [OpenAI Codex CLI](https://github.com/openai/codex) (`codex`) is required only when using `--runner codex`.
26
+ - [GitHub CLI](https://cli.github.com/) — `gh`, authenticated. Required for GitHub mode and for pushing branches / creating PRs in local mode.
27
+
28
+ ### Install from npm
29
+
30
+ ```sh
31
+ bun add -g stonecut
32
+ ```
33
+
34
+ This makes the `stonecut` command globally available. Then install the Claude Code skills:
35
+
36
+ ```sh
37
+ stonecut setup-skills
38
+ ```
39
+
40
+ ### Install from source
41
+
42
+ ```sh
43
+ git clone https://github.com/elkinjosetm/stonecut.git
44
+ cd stonecut
45
+ bun install
46
+ ```
47
+
48
+ To run the CLI from source:
49
+
50
+ ```sh
51
+ bun run src/cli.ts
52
+ ```
53
+
54
+ ### Dev setup
55
+
56
+ ```sh
57
+ bun install
58
+ git config core.hooksPath .githooks
59
+ ```
60
+
61
+ This installs all dependencies and activates a pre-commit hook that runs eslint and prettier checks before each commit.
62
+
63
+ Run tests:
64
+
65
+ ```sh
66
+ bun test
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ Stonecut has one execution command (`run`) with two sources (`--local` for local PRDs, `--github` for GitHub PRDs). All execution is headless — Stonecut runs the issues autonomously and creates a PR when done.
72
+
73
+ ### `stonecut run --local` — Local PRDs
74
+
75
+ ```sh
76
+ # Run 5 issues, then push and create a PR
77
+ stonecut run --local my-feature -i 5
78
+
79
+ # Run all remaining issues
80
+ stonecut run --local my-feature -i all
81
+ ```
82
+
83
+ ### `stonecut run --github` — GitHub PRDs
84
+
85
+ ```sh
86
+ # Run 5 sub-issues
87
+ stonecut run --github 42 -i 5
88
+
89
+ # Run all remaining sub-issues
90
+ stonecut run --github 42 -i all
91
+ ```
92
+
93
+ ### Flags
94
+
95
+ | Flag | Short | Required | Description |
96
+ | -------------- | ----- | -------- | ----------------------------------------------------------------- |
97
+ | `--iterations` | `-i` | Always | Positive integer or `all`. |
98
+ | `--runner` | — | No | Agentic CLI runner to use (`claude`, `codex`). Default: `claude`. |
99
+ | `--version` | `-V` | — | Show version and exit. |
100
+
101
+ ### Pre-execution prompts
102
+
103
+ Before starting, Stonecut:
104
+
105
+ 1. Checks for a clean working tree
106
+ 2. Prompts for a branch name (suggests `stonecut/<slug>` — local uses the spec name, GitHub uses the PRD title slug, with `stonecut/issue-<number>` fallback)
107
+ 3. Prompts for a base branch / PR target (suggests `main`)
108
+ 4. Creates or checks out the branch
109
+
110
+ ### After a run
111
+
112
+ Stonecut automatically pushes the branch, creates a PR, and includes a Stonecut Report listing each issue with its status (completed or failed with error reason). The report also shows which runner was used. Timing stats are printed per iteration and for the full session.
113
+ In GitHub mode, the PR title defaults to the PRD issue title with a `PRD #<number>` fallback if the title is unavailable.
114
+
115
+ ## Sources
116
+
117
+ ### Local mode (`stonecut run --local <name>`)
118
+
119
+ Expects a local PRD directory at `.stonecut/<name>/` with this structure:
120
+
121
+ ```
122
+ .stonecut/my-feature/
123
+ ├── prd.md # The full PRD
124
+ ├── issues/
125
+ │ ├── 01-setup.md # Issue files, numbered for ordering
126
+ │ ├── 02-core.md
127
+ │ └── 03-api.md
128
+ ├── status.json # Auto-created: tracks completed issues
129
+ └── progress.txt # Auto-created: timestamped completion log
130
+ ```
131
+
132
+ ### GitHub mode (`stonecut run --github <number>`)
133
+
134
+ Works with GitHub issues instead of local files:
135
+
136
+ - The PRD is a GitHub issue labeled `prd`
137
+ - Tasks are sub-issues of the PRD
138
+ - Progress is tracked by issue state (open/closed)
139
+ - Completed issues are closed via `gh issue close`
140
+
141
+ ## Skills
142
+
143
+ The repo ships three Claude Code skills for steps 1–3 of the workflow. Install them with:
144
+
145
+ ```sh
146
+ stonecut setup-skills
147
+ ```
148
+
149
+ This creates symlinks in `~/.claude/skills/` pointing to the installed package. Once linked, they're available as `/stonecut-interview`, `/stonecut-prd`, and `/stonecut-issues` in any Claude Code session.
150
+
151
+ For non-default Claude Code installations, pass `--target` with the Claude root path:
152
+
153
+ ```sh
154
+ stonecut setup-skills --target ~/.claude-acme
155
+ ```
156
+
157
+ To remove the symlinks:
158
+
159
+ ```sh
160
+ stonecut remove-skills # default (~/.claude)
161
+ stonecut remove-skills --target ~/.claude-acme
162
+ ```
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "stonecut",
3
+ "version": "1.0.0",
4
+ "description": "CLI that drives PRD-driven development with agentic coding CLIs",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/elkinjosetm/stonecut.git"
9
+ },
10
+ "keywords": [
11
+ "cli",
12
+ "prd",
13
+ "agentic",
14
+ "claude-code",
15
+ "codex"
16
+ ],
17
+ "type": "module",
18
+ "files": [
19
+ "src",
20
+ "LICENSE",
21
+ "README.md"
22
+ ],
23
+ "bin": {
24
+ "stonecut": "./src/cli.ts"
25
+ },
26
+ "scripts": {
27
+ "lint": "eslint src/ tests/",
28
+ "format": "prettier --write .",
29
+ "format:check": "prettier --check .",
30
+ "test": "bun test",
31
+ "prepare": "husky"
32
+ },
33
+ "dependencies": {
34
+ "@clack/prompts": "^0.10.0",
35
+ "commander": "^13.1.0"
36
+ },
37
+ "lint-staged": {
38
+ "*.ts": [
39
+ "eslint",
40
+ "prettier --check"
41
+ ],
42
+ "*.{json,md}": "prettier --check"
43
+ },
44
+ "devDependencies": {
45
+ "@types/bun": "^1.2.9",
46
+ "eslint": "^9.24.0",
47
+ "husky": "^9.1.7",
48
+ "lint-staged": "^16.4.0",
49
+ "prettier": "^3.5.3",
50
+ "typescript": "^5.8.3",
51
+ "typescript-eslint": "^8.30.1"
52
+ }
53
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Stonecut CLI — PRD-driven development workflow orchestrator.
5
+ *
6
+ * Modules throw errors; only this file catches them, formats user-facing
7
+ * messages, and calls process.exit().
8
+ */
9
+
10
+ import * as clack from "@clack/prompts";
11
+ import { Command, InvalidArgumentError } from "commander";
12
+ import { createRequire } from "module";
13
+ import {
14
+ checkoutOrCreateBranch,
15
+ createPr,
16
+ defaultBranch,
17
+ ensureCleanTree,
18
+ pushBranch,
19
+ } from "./git";
20
+ import { GitHubSource } from "./github";
21
+ import { LocalSource } from "./local";
22
+ import { slugifyBranchComponent } from "./naming";
23
+ import { renderGithub, renderLocal } from "./prompt";
24
+ import { Logger } from "./logger";
25
+ import { defaultGitOps, runAfkLoop } from "./runner";
26
+ import { getRunner } from "./runners/index";
27
+ import { setupSkills, removeSkills } from "./skills";
28
+ import type { GitHubIssue, Issue, IterationResult, Session } from "./types";
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const { version } = require("../package.json");
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Validation helpers (exported for testing)
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Parse the --iterations value: positive integer or "all".
39
+ * Throws InvalidArgumentError on bad input so Commander surfaces it.
40
+ */
41
+ export function parseIterations(value: string): number | "all" {
42
+ if (value === "all") return "all";
43
+ const n = Number(value);
44
+ if (!Number.isInteger(n) || n <= 0) {
45
+ throw new InvalidArgumentError(`Must be a positive integer or 'all', got '${value}'`);
46
+ }
47
+ return n;
48
+ }
49
+
50
+ /**
51
+ * Parse the --github value: must be a positive integer.
52
+ * Throws InvalidArgumentError on bad input so Commander surfaces it.
53
+ */
54
+ export function parseGitHubIssueNumber(value: string): number {
55
+ const n = Number(value);
56
+ if (!Number.isInteger(n) || n <= 0) {
57
+ throw new InvalidArgumentError(`Must be a positive integer, got '${value}'`);
58
+ }
59
+ return n;
60
+ }
61
+
62
+ /**
63
+ * Ensure exactly one of --local or --github was provided.
64
+ * Returns a tagged tuple so the caller can dispatch.
65
+ */
66
+ export function validateRunSource(
67
+ local: string | undefined,
68
+ github: number | undefined,
69
+ ): { kind: "local"; name: string } | { kind: "github"; number: number } {
70
+ if (local !== undefined && github !== undefined) {
71
+ throw new Error("Use exactly one of --local or --github.");
72
+ }
73
+ if (local === undefined && github === undefined) {
74
+ throw new Error("One of --local or --github is required.");
75
+ }
76
+ if (local !== undefined) return { kind: "local", name: local };
77
+ return { kind: "github", number: github! };
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Stonecut report
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Build the Stonecut Report section for a PR body.
86
+ */
87
+ export function buildReport(
88
+ results: IterationResult[],
89
+ runnerName: string,
90
+ prdNumber?: number,
91
+ ): string {
92
+ const lines = ["## Stonecut Report", `**Runner:** ${runnerName}`, ""];
93
+ for (const r of results) {
94
+ if (r.success) {
95
+ lines.push(`- #${r.issueNumber} ${r.issueFilename}: completed`);
96
+ } else {
97
+ const reason = r.error || "unknown error";
98
+ lines.push(`- #${r.issueNumber} ${r.issueFilename}: failed — ${reason}`);
99
+ }
100
+ }
101
+
102
+ if (prdNumber !== undefined && results.every((r) => r.success)) {
103
+ lines.push("");
104
+ lines.push(`Closes #${prdNumber}`);
105
+ }
106
+
107
+ return lines.join("\n");
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // GitHub comment on no-changes
112
+ // ---------------------------------------------------------------------------
113
+
114
+ function commentOnIssue(issueNumber: number, runnerOutput: string | undefined): void {
115
+ let body = "**Stonecut:** Runner completed but produced no file changes.\n";
116
+ if (runnerOutput) {
117
+ body +=
118
+ "\n<details><summary>Runner output</summary>\n\n" +
119
+ `\`\`\`\n${runnerOutput}\n\`\`\`\n\n</details>`;
120
+ }
121
+ Bun.spawnSync(["gh", "issue", "comment", String(issueNumber), "--body", body], {
122
+ stdout: "pipe",
123
+ stderr: "pipe",
124
+ });
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Pre-execution flow
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /**
132
+ * Run pre-execution prompts and git checks.
133
+ * Returns [branch, baseBranch].
134
+ */
135
+ export async function preExecution(suggestedBranch: string): Promise<[string, string]> {
136
+ ensureCleanTree();
137
+
138
+ const branch = await clack.text({
139
+ message: "Branch name:",
140
+ defaultValue: suggestedBranch,
141
+ placeholder: suggestedBranch,
142
+ });
143
+
144
+ if (clack.isCancel(branch)) {
145
+ throw new Error("Cancelled.");
146
+ }
147
+
148
+ const detectedDefault = defaultBranch();
149
+ const baseBranch = await clack.text({
150
+ message: "Base branch / PR target:",
151
+ defaultValue: detectedDefault,
152
+ placeholder: detectedDefault,
153
+ });
154
+
155
+ if (clack.isCancel(baseBranch)) {
156
+ throw new Error("Cancelled.");
157
+ }
158
+
159
+ checkoutOrCreateBranch(branch);
160
+ console.log("");
161
+
162
+ return [branch, baseBranch];
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Post-loop: push and conditionally create PR
167
+ // ---------------------------------------------------------------------------
168
+
169
+ export async function pushAndMaybePr(
170
+ results: IterationResult[],
171
+ source: { getRemainingCount(): Promise<[number, number]> },
172
+ branch: string,
173
+ baseBranch: string,
174
+ prTitle: string,
175
+ runnerName: string,
176
+ logger: { log(message: string): void },
177
+ prdNumber?: number,
178
+ ): Promise<void> {
179
+ if (!results.some((r) => r.success)) {
180
+ return;
181
+ }
182
+
183
+ pushBranch(branch);
184
+ logger.log(`Pushed branch '${branch}'.`);
185
+
186
+ const [remaining, total] = await source.getRemainingCount();
187
+ if (remaining === 0) {
188
+ const body = buildReport(results, runnerName, prdNumber);
189
+ createPr(prTitle, body, baseBranch);
190
+ logger.log("Created PR.");
191
+ } else {
192
+ logger.log(`${remaining}/${total} issues remaining — PR deferred.`);
193
+ }
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Execution paths
198
+ // ---------------------------------------------------------------------------
199
+
200
+ export async function runLocal(
201
+ name: string,
202
+ iterations: number | "all",
203
+ runnerName: string,
204
+ ): Promise<void> {
205
+ const runner = getRunner(runnerName);
206
+ const source = new LocalSource(name);
207
+ const prdIdentifier = slugifyBranchComponent(name) || "spec";
208
+ const logger = new Logger(prdIdentifier);
209
+
210
+ const session: Session = { logger, git: defaultGitOps, runner, runnerName };
211
+
212
+ try {
213
+ const suggestedBranch = prdIdentifier ? `stonecut/${prdIdentifier}` : "stonecut/spec";
214
+ const [branch, baseBranch] = await preExecution(suggestedBranch);
215
+
216
+ const prdContent = await source.getPrdContent();
217
+ const results = await runAfkLoop<Issue>(
218
+ source,
219
+ iterations,
220
+ (issue) =>
221
+ renderLocal({
222
+ prdContent,
223
+ issueNumber: issue.number,
224
+ issueFilename: issue.filename,
225
+ issueContent: issue.content,
226
+ }),
227
+ (issue) => issue.filename,
228
+ (issue) => `Issue ${issue.number}: ${issue.filename}`,
229
+ session,
230
+ );
231
+
232
+ await pushAndMaybePr(
233
+ results,
234
+ source,
235
+ branch,
236
+ baseBranch,
237
+ `Stonecut: ${name}`,
238
+ runnerName,
239
+ logger,
240
+ );
241
+ } finally {
242
+ logger.close();
243
+ }
244
+ }
245
+
246
+ export async function runGitHub(
247
+ number: number,
248
+ iterations: number | "all",
249
+ runnerName: string,
250
+ ): Promise<void> {
251
+ const runner = getRunner(runnerName);
252
+ const source = new GitHubSource(number);
253
+ const logger = new Logger(`prd-${number}`);
254
+
255
+ const session: Session = { logger, git: defaultGitOps, runner, runnerName };
256
+
257
+ try {
258
+ const prd = source.getPrd();
259
+ const prdSlug = slugifyBranchComponent(prd.title);
260
+ const suggestedBranch = prdSlug ? `stonecut/${prdSlug}` : `stonecut/issue-${number}`;
261
+ const prTitle = prd.title || `PRD #${number}`;
262
+ const [branch, baseBranch] = await preExecution(suggestedBranch);
263
+
264
+ const prdContent = prd.body;
265
+ const results = await runAfkLoop<GitHubIssue>(
266
+ source,
267
+ iterations,
268
+ (issue) =>
269
+ renderGithub({
270
+ prdContent,
271
+ issueNumber: issue.number,
272
+ issueTitle: issue.title,
273
+ issueContent: issue.body,
274
+ }),
275
+ (issue) => issue.title,
276
+ (issue) => `Issue #${issue.number}: ${issue.title}`,
277
+ session,
278
+ (issue, output) => commentOnIssue(issue.number, output),
279
+ );
280
+
281
+ await pushAndMaybePr(results, source, branch, baseBranch, prTitle, runnerName, logger, number);
282
+ } finally {
283
+ logger.close();
284
+ }
285
+ }
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // Program definition
289
+ // ---------------------------------------------------------------------------
290
+
291
+ export function buildProgram(): Command {
292
+ const program = new Command();
293
+
294
+ program
295
+ .name("stonecut")
296
+ .description("Stonecut — execute PRD-driven development workflows using agentic coding CLIs.")
297
+ .version(`stonecut ${version}`, "-V, --version");
298
+
299
+ program
300
+ .command("run")
301
+ .description("Execute issues from a local PRD or GitHub PRD.")
302
+ .option("--local <name>", "Local PRD name (.stonecut/<name>/)")
303
+ .option("--github <number>", "GitHub PRD issue number", parseGitHubIssueNumber)
304
+ .requiredOption("-i, --iterations <value>", "Number of issues to process, or 'all'")
305
+ .option("--runner <name>", "Agentic CLI runner (claude, codex)", "claude")
306
+ .action(async (opts) => {
307
+ const source = validateRunSource(opts.local, opts.github);
308
+ const iterations = parseIterations(opts.iterations);
309
+ const runnerName: string = opts.runner;
310
+
311
+ if (source.kind === "local") {
312
+ await runLocal(source.name, iterations, runnerName);
313
+ } else {
314
+ await runGitHub(source.number, iterations, runnerName);
315
+ }
316
+ });
317
+
318
+ program
319
+ .command("setup-skills")
320
+ .description("Install Stonecut skills as symlinks into ~/.claude/skills/.")
321
+ .option(
322
+ "--target <path>",
323
+ "Claude root path (e.g. ~/.claude-acme). Skills are installed into <target>/skills/.",
324
+ )
325
+ .action((opts) => {
326
+ const result = setupSkills(opts.target);
327
+ for (const msg of result.messages) {
328
+ console.log(msg);
329
+ }
330
+ for (const warn of result.warnings) {
331
+ console.error(warn);
332
+ }
333
+ });
334
+
335
+ program
336
+ .command("remove-skills")
337
+ .description("Remove Stonecut skill symlinks from ~/.claude/skills/.")
338
+ .option(
339
+ "--target <path>",
340
+ "Claude root path (e.g. ~/.claude-acme). Skills are removed from <target>/skills/.",
341
+ )
342
+ .action((opts) => {
343
+ const result = removeSkills(opts.target);
344
+ for (const msg of result.messages) {
345
+ console.log(msg);
346
+ }
347
+ for (const warn of result.warnings) {
348
+ console.error(warn);
349
+ }
350
+ });
351
+
352
+ return program;
353
+ }
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // Entry point — top-level error catching
357
+ // ---------------------------------------------------------------------------
358
+
359
+ async function main(): Promise<void> {
360
+ const program = buildProgram();
361
+ await program.parseAsync(process.argv);
362
+ }
363
+
364
+ if (import.meta.main) {
365
+ main().catch((err: unknown) => {
366
+ const message = err instanceof Error ? err.message : String(err);
367
+ console.error(`Error: ${message}`);
368
+ process.exit(1);
369
+ });
370
+ }