stonecut 1.0.1 → 1.1.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/README.md +33 -10
- package/package.json +2 -2
- package/src/cli.ts +144 -25
- package/src/logger.ts +2 -2
package/README.md
CHANGED
|
@@ -70,6 +70,23 @@ bun test
|
|
|
70
70
|
|
|
71
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
72
|
|
|
73
|
+
### `stonecut run` — Interactive wizard
|
|
74
|
+
|
|
75
|
+
When flags are omitted, Stonecut prompts for each missing parameter:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
# Full wizard — prompted for source, iterations, branch, and base branch
|
|
79
|
+
stonecut run
|
|
80
|
+
|
|
81
|
+
# Partial — only iterations, branch, and base are prompted
|
|
82
|
+
stonecut run --local my-feature
|
|
83
|
+
|
|
84
|
+
# Partial — only source, branch, and base are prompted
|
|
85
|
+
stonecut run -i all
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Flags provided via CLI skip the corresponding prompts. When all flags are given, the command runs without any prompts (the existing behavior).
|
|
89
|
+
|
|
73
90
|
### `stonecut run --local` — Local PRDs
|
|
74
91
|
|
|
75
92
|
```sh
|
|
@@ -92,20 +109,26 @@ stonecut run --github 42 -i all
|
|
|
92
109
|
|
|
93
110
|
### Flags
|
|
94
111
|
|
|
95
|
-
| Flag | Short | Required | Description
|
|
96
|
-
| -------------- | ----- | -------- |
|
|
97
|
-
| `--
|
|
98
|
-
| `--
|
|
99
|
-
| `--
|
|
112
|
+
| Flag | Short | Required | Description |
|
|
113
|
+
| -------------- | ----- | -------- | ------------------------------------------------------------------ |
|
|
114
|
+
| `--local` | — | No | Local PRD name (`.stonecut/<name>/`). Prompted if omitted. |
|
|
115
|
+
| `--github` | — | No | GitHub PRD issue number. Prompted if omitted. |
|
|
116
|
+
| `--iterations` | `-i` | No | Positive integer or `all`. Prompted with default `all` if omitted. |
|
|
117
|
+
| `--runner` | — | No | Agentic CLI runner (`claude`, `codex`). Default: `claude`. |
|
|
118
|
+
| `--version` | `-V` | — | Show version and exit. |
|
|
100
119
|
|
|
101
120
|
### Pre-execution prompts
|
|
102
121
|
|
|
103
|
-
Before starting, Stonecut:
|
|
122
|
+
Before starting, Stonecut prompts for any missing parameters in order:
|
|
123
|
+
|
|
124
|
+
1. **Source** — `--local` or `--github` (skipped when provided via flag)
|
|
125
|
+
2. **Spec name / issue number** — free-text input for the chosen source (skipped when provided via flag)
|
|
126
|
+
3. **Iterations** — number of issues to process, default `all` (skipped when `-i` provided)
|
|
127
|
+
4. **Branch name** — suggests `stonecut/<slug>` based on the source
|
|
128
|
+
5. **Base branch** — suggests the repository's default branch (usually `main`)
|
|
129
|
+
6. Creates or checks out the branch
|
|
104
130
|
|
|
105
|
-
|
|
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
|
|
131
|
+
When all parameters are provided via flags, only the branch and base branch prompts appear (steps 4–5).
|
|
109
132
|
|
|
110
133
|
### After a run
|
|
111
134
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stonecut",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI that drives PRD-driven development with agentic coding CLIs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"eslint",
|
|
40
40
|
"prettier --check"
|
|
41
41
|
],
|
|
42
|
-
"*.{json,md}": "prettier --check"
|
|
42
|
+
"*.{json,md,yml,yaml,css,html}": "prettier --check"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/bun": "^1.2.9",
|
package/src/cli.ts
CHANGED
|
@@ -66,17 +66,64 @@ export function parseGitHubIssueNumber(value: string): number {
|
|
|
66
66
|
export function validateRunSource(
|
|
67
67
|
local: string | undefined,
|
|
68
68
|
github: number | undefined,
|
|
69
|
-
): { kind: "local"; name: string } | { kind: "github"; number: number } {
|
|
69
|
+
): { kind: "local"; name: string } | { kind: "github"; number: number } | { kind: "prompt" } {
|
|
70
70
|
if (local !== undefined && github !== undefined) {
|
|
71
71
|
throw new Error("Use exactly one of --local or --github.");
|
|
72
72
|
}
|
|
73
73
|
if (local === undefined && github === undefined) {
|
|
74
|
-
|
|
74
|
+
return { kind: "prompt" };
|
|
75
75
|
}
|
|
76
76
|
if (local !== undefined) return { kind: "local", name: local };
|
|
77
77
|
return { kind: "github", number: github! };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Prompt the user interactively for the source type and value.
|
|
82
|
+
* Returns a tagged tuple matching the shape of validateRunSource().
|
|
83
|
+
*/
|
|
84
|
+
export async function promptForSource(): Promise<
|
|
85
|
+
{ kind: "local"; name: string } | { kind: "github"; number: number }
|
|
86
|
+
> {
|
|
87
|
+
const sourceType = await clack.select({
|
|
88
|
+
message: "Source type:",
|
|
89
|
+
options: [
|
|
90
|
+
{ value: "local", label: "Local PRD (.stonecut/<name>/)" },
|
|
91
|
+
{ value: "github", label: "GitHub PRD (issue number)" },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (clack.isCancel(sourceType)) {
|
|
96
|
+
throw new Error("Cancelled.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (sourceType === "local") {
|
|
100
|
+
const name = await clack.text({
|
|
101
|
+
message: "Spec name:",
|
|
102
|
+
placeholder: "my-spec",
|
|
103
|
+
validate: (value) => {
|
|
104
|
+
if (!value.trim()) return "Spec name is required.";
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
if (clack.isCancel(name)) {
|
|
108
|
+
throw new Error("Cancelled.");
|
|
109
|
+
}
|
|
110
|
+
return { kind: "local", name };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const issueStr = await clack.text({
|
|
114
|
+
message: "GitHub issue number:",
|
|
115
|
+
placeholder: "42",
|
|
116
|
+
validate: (value) => {
|
|
117
|
+
const n = Number(value);
|
|
118
|
+
if (!Number.isInteger(n) || n <= 0) return "Must be a positive integer.";
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
if (clack.isCancel(issueStr)) {
|
|
122
|
+
throw new Error("Cancelled.");
|
|
123
|
+
}
|
|
124
|
+
return { kind: "github", number: Number(issueStr) };
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
// ---------------------------------------------------------------------------
|
|
81
128
|
// Stonecut report
|
|
82
129
|
// ---------------------------------------------------------------------------
|
|
@@ -132,28 +179,43 @@ function commentOnIssue(issueNumber: number, runnerOutput: string | undefined):
|
|
|
132
179
|
* Run pre-execution prompts and git checks.
|
|
133
180
|
* Returns [branch, baseBranch].
|
|
134
181
|
*/
|
|
135
|
-
export async function preExecution(
|
|
182
|
+
export async function preExecution(
|
|
183
|
+
suggestedBranch: string,
|
|
184
|
+
prefilled?: { branch?: string; baseBranch?: string },
|
|
185
|
+
): Promise<[string, string]> {
|
|
136
186
|
ensureCleanTree();
|
|
137
187
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
let branch: string;
|
|
189
|
+
if (prefilled?.branch) {
|
|
190
|
+
branch = prefilled.branch;
|
|
191
|
+
} else {
|
|
192
|
+
const branchInput = await clack.text({
|
|
193
|
+
message: "Branch name:",
|
|
194
|
+
defaultValue: suggestedBranch,
|
|
195
|
+
placeholder: suggestedBranch,
|
|
196
|
+
});
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
198
|
+
if (clack.isCancel(branchInput)) {
|
|
199
|
+
throw new Error("Cancelled.");
|
|
200
|
+
}
|
|
201
|
+
branch = branchInput;
|
|
146
202
|
}
|
|
147
203
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
204
|
+
let baseBranch: string;
|
|
205
|
+
if (prefilled?.baseBranch) {
|
|
206
|
+
baseBranch = prefilled.baseBranch;
|
|
207
|
+
} else {
|
|
208
|
+
const detectedDefault = defaultBranch();
|
|
209
|
+
const baseBranchInput = await clack.text({
|
|
210
|
+
message: "Base branch / PR target:",
|
|
211
|
+
defaultValue: detectedDefault,
|
|
212
|
+
placeholder: detectedDefault,
|
|
213
|
+
});
|
|
154
214
|
|
|
155
|
-
|
|
156
|
-
|
|
215
|
+
if (clack.isCancel(baseBranchInput)) {
|
|
216
|
+
throw new Error("Cancelled.");
|
|
217
|
+
}
|
|
218
|
+
baseBranch = baseBranchInput;
|
|
157
219
|
}
|
|
158
220
|
|
|
159
221
|
checkoutOrCreateBranch(branch);
|
|
@@ -201,6 +263,7 @@ export async function runLocal(
|
|
|
201
263
|
name: string,
|
|
202
264
|
iterations: number | "all",
|
|
203
265
|
runnerName: string,
|
|
266
|
+
prefilled?: { branch?: string; baseBranch?: string },
|
|
204
267
|
): Promise<void> {
|
|
205
268
|
const runner = getRunner(runnerName);
|
|
206
269
|
const source = new LocalSource(name);
|
|
@@ -211,7 +274,7 @@ export async function runLocal(
|
|
|
211
274
|
|
|
212
275
|
try {
|
|
213
276
|
const suggestedBranch = prdIdentifier ? `stonecut/${prdIdentifier}` : "stonecut/spec";
|
|
214
|
-
const [branch, baseBranch] = await preExecution(suggestedBranch);
|
|
277
|
+
const [branch, baseBranch] = await preExecution(suggestedBranch, prefilled);
|
|
215
278
|
|
|
216
279
|
const prdContent = await source.getPrdContent();
|
|
217
280
|
const results = await runAfkLoop<Issue>(
|
|
@@ -247,6 +310,7 @@ export async function runGitHub(
|
|
|
247
310
|
number: number,
|
|
248
311
|
iterations: number | "all",
|
|
249
312
|
runnerName: string,
|
|
313
|
+
prefilled?: { branch?: string; baseBranch?: string },
|
|
250
314
|
): Promise<void> {
|
|
251
315
|
const runner = getRunner(runnerName);
|
|
252
316
|
const source = new GitHubSource(number);
|
|
@@ -259,7 +323,7 @@ export async function runGitHub(
|
|
|
259
323
|
const prdSlug = slugifyBranchComponent(prd.title);
|
|
260
324
|
const suggestedBranch = prdSlug ? `stonecut/${prdSlug}` : `stonecut/issue-${number}`;
|
|
261
325
|
const prTitle = prd.title || `PRD #${number}`;
|
|
262
|
-
const [branch, baseBranch] = await preExecution(suggestedBranch);
|
|
326
|
+
const [branch, baseBranch] = await preExecution(suggestedBranch, prefilled);
|
|
263
327
|
|
|
264
328
|
const prdContent = prd.body;
|
|
265
329
|
const results = await runAfkLoop<GitHubIssue>(
|
|
@@ -301,17 +365,72 @@ export function buildProgram(): Command {
|
|
|
301
365
|
.description("Execute issues from a local PRD or GitHub PRD.")
|
|
302
366
|
.option("--local <name>", "Local PRD name (.stonecut/<name>/)")
|
|
303
367
|
.option("--github <number>", "GitHub PRD issue number", parseGitHubIssueNumber)
|
|
304
|
-
.
|
|
368
|
+
.option("-i, --iterations <value>", "Number of issues to process, or 'all'")
|
|
305
369
|
.option("--runner <name>", "Agentic CLI runner (claude, codex)", "claude")
|
|
306
370
|
.action(async (opts) => {
|
|
307
|
-
const
|
|
308
|
-
const
|
|
371
|
+
const validated = validateRunSource(opts.local, opts.github);
|
|
372
|
+
const source = validated.kind === "prompt" ? await promptForSource() : validated;
|
|
373
|
+
|
|
374
|
+
let iterations: number | "all";
|
|
375
|
+
const needsIterationPrompt = opts.iterations === undefined;
|
|
376
|
+
if (!needsIterationPrompt) {
|
|
377
|
+
iterations = parseIterations(opts.iterations);
|
|
378
|
+
} else {
|
|
379
|
+
const iterationsInput = await clack.text({
|
|
380
|
+
message: "Iterations:",
|
|
381
|
+
defaultValue: "all",
|
|
382
|
+
placeholder: "all",
|
|
383
|
+
validate: (value) => {
|
|
384
|
+
try {
|
|
385
|
+
parseIterations(value);
|
|
386
|
+
} catch {
|
|
387
|
+
return "Must be a positive integer or 'all'.";
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
if (clack.isCancel(iterationsInput)) {
|
|
392
|
+
throw new Error("Cancelled.");
|
|
393
|
+
}
|
|
394
|
+
iterations = parseIterations(iterationsInput);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const isWizard = validated.kind === "prompt" || needsIterationPrompt;
|
|
398
|
+
let prefilled: { branch?: string; baseBranch?: string } | undefined;
|
|
399
|
+
|
|
400
|
+
if (isWizard) {
|
|
401
|
+
const suggestedBranch =
|
|
402
|
+
source.kind === "local"
|
|
403
|
+
? `stonecut/${slugifyBranchComponent(source.name) || "spec"}`
|
|
404
|
+
: `stonecut/issue-${source.number}`;
|
|
405
|
+
|
|
406
|
+
const branch = await clack.text({
|
|
407
|
+
message: "Branch name:",
|
|
408
|
+
defaultValue: suggestedBranch,
|
|
409
|
+
placeholder: suggestedBranch,
|
|
410
|
+
});
|
|
411
|
+
if (clack.isCancel(branch)) {
|
|
412
|
+
throw new Error("Cancelled.");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const detectedDefault = defaultBranch();
|
|
416
|
+
const baseBranch = await clack.text({
|
|
417
|
+
message: "Base branch / PR target:",
|
|
418
|
+
defaultValue: detectedDefault,
|
|
419
|
+
placeholder: detectedDefault,
|
|
420
|
+
});
|
|
421
|
+
if (clack.isCancel(baseBranch)) {
|
|
422
|
+
throw new Error("Cancelled.");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
prefilled = { branch, baseBranch };
|
|
426
|
+
}
|
|
427
|
+
|
|
309
428
|
const runnerName: string = opts.runner;
|
|
310
429
|
|
|
311
430
|
if (source.kind === "local") {
|
|
312
|
-
await runLocal(source.name, iterations, runnerName);
|
|
431
|
+
await runLocal(source.name, iterations, runnerName, prefilled);
|
|
313
432
|
} else {
|
|
314
|
-
await runGitHub(source.number, iterations, runnerName);
|
|
433
|
+
await runGitHub(source.number, iterations, runnerName, prefilled);
|
|
315
434
|
}
|
|
316
435
|
});
|
|
317
436
|
|
package/src/logger.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { appendFileSync, mkdirSync } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
10
|
import type { LogWriter } from "./types";
|
|
11
11
|
|
|
12
12
|
export class Logger implements LogWriter {
|
|
@@ -14,7 +14,7 @@ export class Logger implements LogWriter {
|
|
|
14
14
|
|
|
15
15
|
constructor(prdIdentifier: string) {
|
|
16
16
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
17
|
-
const logDir =
|
|
17
|
+
const logDir = resolve(".stonecut", "logs");
|
|
18
18
|
mkdirSync(logDir, { recursive: true });
|
|
19
19
|
this.filePath = join(logDir, `${prdIdentifier}-${timestamp}.log`);
|
|
20
20
|
}
|