stonecut 1.0.0 → 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 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
- | `--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. |
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
- 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
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.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
- throw new Error("One of --local or --github is required.");
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(suggestedBranch: string): Promise<[string, string]> {
182
+ export async function preExecution(
183
+ suggestedBranch: string,
184
+ prefilled?: { branch?: string; baseBranch?: string },
185
+ ): Promise<[string, string]> {
136
186
  ensureCleanTree();
137
187
 
138
- const branch = await clack.text({
139
- message: "Branch name:",
140
- defaultValue: suggestedBranch,
141
- placeholder: suggestedBranch,
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
- if (clack.isCancel(branch)) {
145
- throw new Error("Cancelled.");
198
+ if (clack.isCancel(branchInput)) {
199
+ throw new Error("Cancelled.");
200
+ }
201
+ branch = branchInput;
146
202
  }
147
203
 
148
- const detectedDefault = defaultBranch();
149
- const baseBranch = await clack.text({
150
- message: "Base branch / PR target:",
151
- defaultValue: detectedDefault,
152
- placeholder: detectedDefault,
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
- if (clack.isCancel(baseBranch)) {
156
- throw new Error("Cancelled.");
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
- .requiredOption("-i, --iterations <value>", "Number of issues to process, or 'all'")
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 source = validateRunSource(opts.local, opts.github);
308
- const iterations = parseIterations(opts.iterations);
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 = join(".stonecut", "logs");
17
+ const logDir = resolve(".stonecut", "logs");
18
18
  mkdirSync(logDir, { recursive: true });
19
19
  this.filePath = join(logDir, `${prdIdentifier}-${timestamp}.log`);
20
20
  }
package/src/runner.ts CHANGED
@@ -120,6 +120,7 @@ export async function runAfkLoop<T extends { number: number }>(
120
120
  const { logger, git, runner, runnerName } = session;
121
121
 
122
122
  logger.log(`Session started — runner: ${runnerName}, iterations: ${iterations}`);
123
+ runner.logEnvironment(logger);
123
124
  logger.log("");
124
125
  const results: IterationResult[] = [];
125
126
  const sessionStart = performance.now();
@@ -6,7 +6,7 @@
6
6
  * human-readable messages.
7
7
  */
8
8
 
9
- import type { Runner, RunResult } from "../types.js";
9
+ import type { LogWriter, Runner, RunResult } from "../types.js";
10
10
 
11
11
  const ERROR_MESSAGES: Record<string, string> = {
12
12
  error_max_turns: "max turns exceeded",
@@ -14,6 +14,11 @@ const ERROR_MESSAGES: Record<string, string> = {
14
14
  };
15
15
 
16
16
  export class ClaudeRunner implements Runner {
17
+ logEnvironment(logger: LogWriter): void {
18
+ const configDir = process.env.CLAUDE_CONFIG_DIR || "~/.claude (default)";
19
+ logger.log(`Claude config: ${configDir}`);
20
+ }
21
+
17
22
  async run(prompt: string): Promise<RunResult> {
18
23
  const start = performance.now();
19
24
 
@@ -6,7 +6,7 @@
6
6
  * output on failure.
7
7
  */
8
8
 
9
- import type { Runner, RunResult } from "../types.js";
9
+ import type { LogWriter, Runner, RunResult } from "../types.js";
10
10
 
11
11
  function extractError(stdout: string): string {
12
12
  for (const raw of stdout.split("\n")) {
@@ -43,6 +43,10 @@ function extractError(stdout: string): string {
43
43
  }
44
44
 
45
45
  export class CodexRunner implements Runner {
46
+ logEnvironment(_logger: LogWriter): void {
47
+ // No environment-specific config to log for Codex.
48
+ }
49
+
46
50
  async run(prompt: string): Promise<RunResult> {
47
51
  const start = performance.now();
48
52
 
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ export interface IterationResult {
23
23
  /** Protocol that all runner adapters must satisfy. */
24
24
  export interface Runner {
25
25
  run(prompt: string): Promise<RunResult>;
26
+ logEnvironment(logger: LogWriter): void;
26
27
  }
27
28
 
28
29
  /** Snapshot of the working tree state before a runner session. */