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 +21 -0
- package/README.md +162 -0
- package/package.json +53 -0
- package/src/cli.ts +370 -0
- package/src/git.ts +189 -0
- package/src/github.ts +178 -0
- package/src/local.ts +116 -0
- package/src/logger.ts +32 -0
- package/src/naming.ts +14 -0
- package/src/prompt.ts +50 -0
- package/src/runner.ts +298 -0
- package/src/runners/claude.ts +89 -0
- package/src/runners/codex.ts +77 -0
- package/src/runners/index.ts +21 -0
- package/src/skills/stonecut-interview/SKILL.md +20 -0
- package/src/skills/stonecut-issues/SKILL.md +167 -0
- package/src/skills/stonecut-prd/SKILL.md +127 -0
- package/src/skills.ts +163 -0
- package/src/templates/.gitkeep +0 -0
- package/src/templates/execute.md +28 -0
- package/src/types.ts +83 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stonecut-prd
|
|
3
|
+
description: Write a PRD through structured user interview, codebase exploration, and module design. Saves the result as a local file or GitHub issue. Use when the user wants to create a product requirements document or plan a new feature.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are writing a PRD as part of the Stonecut workflow. Follow these steps, skipping any that aren't necessary for the situation.
|
|
7
|
+
|
|
8
|
+
## Process
|
|
9
|
+
|
|
10
|
+
### 1. Gather context
|
|
11
|
+
|
|
12
|
+
Ask the user for a detailed description of the problem they want to solve and any ideas they have for the solution.
|
|
13
|
+
|
|
14
|
+
### 2. Explore the codebase
|
|
15
|
+
|
|
16
|
+
Explore the repo to verify the user's assertions and understand the current state of the code.
|
|
17
|
+
|
|
18
|
+
### 3. Interview
|
|
19
|
+
|
|
20
|
+
Interview the user relentlessly about every aspect of the plan until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.
|
|
21
|
+
|
|
22
|
+
### 4. Design modules
|
|
23
|
+
|
|
24
|
+
Sketch out the major modules that need to be built or modified. Look for opportunities to extract deep modules — modules that encapsulate significant functionality behind a simple, testable interface that rarely changes.
|
|
25
|
+
|
|
26
|
+
Check with the user that these modules match their expectations. Ask which modules they want tests written for.
|
|
27
|
+
|
|
28
|
+
### 5. Choose a destination
|
|
29
|
+
|
|
30
|
+
Ask the user where to save the PRD:
|
|
31
|
+
|
|
32
|
+
- **Local file** — Save as `.stonecut/<name>/prd.md` in the project. Ask the user: "What should I name this spec?" The name can be anything — a ticket ID (e.g., `ASC-1`), a descriptive slug (e.g., `auth-refactor`), or whatever fits. Create the `.stonecut/<name>/` directory if it doesn't exist.
|
|
33
|
+
- **GitHub issue** — Create a GitHub issue using `gh issue create --label prd`. Before creating, ensure the `prd` label exists:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Only create the label if it doesn't already exist
|
|
37
|
+
if ! gh label list --search "prd" --json name --jq '.[].name' | grep -qx "prd"; then
|
|
38
|
+
gh label create prd --description "Product Requirements Document" --color "0052CC"
|
|
39
|
+
fi
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If the project already has a `.stonecut/` directory, default to suggesting local. Otherwise, just ask.
|
|
43
|
+
|
|
44
|
+
### 6. Documentation impact check
|
|
45
|
+
|
|
46
|
+
Before writing the PRD, ensure that documentation impact has been explicitly addressed. Based on everything you've learned from the interview and codebase exploration, analyze which user-facing documentation artifacts (README, CLI help text, docs/ content) would be affected by these changes.
|
|
47
|
+
|
|
48
|
+
Present your assessment to the user for confirmation:
|
|
49
|
+
|
|
50
|
+
- If changes are needed, list the specific artifacts and what would need updating.
|
|
51
|
+
- If no changes are needed, state why (e.g., "Internal refactor — no user-facing behavior changes") and ask the user to confirm.
|
|
52
|
+
|
|
53
|
+
Record the confirmed answer for inclusion in the PRD's Documentation Impact section.
|
|
54
|
+
|
|
55
|
+
This step is a gate — do not proceed to writing the PRD until documentation impact is resolved.
|
|
56
|
+
|
|
57
|
+
### 7. Write the PRD
|
|
58
|
+
|
|
59
|
+
Once you have a complete understanding of the problem and solution, write the PRD using the template below and save it to the chosen destination.
|
|
60
|
+
|
|
61
|
+
<prd-template>
|
|
62
|
+
|
|
63
|
+
## Problem Statement
|
|
64
|
+
|
|
65
|
+
The problem that the user is facing, from the user's perspective.
|
|
66
|
+
|
|
67
|
+
## Solution
|
|
68
|
+
|
|
69
|
+
The solution to the problem, from the user's perspective.
|
|
70
|
+
|
|
71
|
+
## User Stories
|
|
72
|
+
|
|
73
|
+
A LONG, numbered list of user stories. Each user story should be in the format of:
|
|
74
|
+
|
|
75
|
+
1. As an <actor>, I want a <feature>, so that <benefit>
|
|
76
|
+
|
|
77
|
+
<user-story-example>
|
|
78
|
+
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
|
|
79
|
+
</user-story-example>
|
|
80
|
+
|
|
81
|
+
This list of user stories should be extremely extensive and cover all aspects of the feature.
|
|
82
|
+
|
|
83
|
+
## Implementation Decisions
|
|
84
|
+
|
|
85
|
+
A list of implementation decisions that were made. This can include:
|
|
86
|
+
|
|
87
|
+
- The modules that will be built/modified
|
|
88
|
+
- The interfaces of those modules that will be modified
|
|
89
|
+
- Technical clarifications from the developer
|
|
90
|
+
- Architectural decisions
|
|
91
|
+
- Schema changes
|
|
92
|
+
- API contracts
|
|
93
|
+
- Specific interactions
|
|
94
|
+
|
|
95
|
+
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
|
|
96
|
+
|
|
97
|
+
## Testing Decisions
|
|
98
|
+
|
|
99
|
+
A list of testing decisions that were made. Include:
|
|
100
|
+
|
|
101
|
+
- A description of what makes a good test (only test external behavior, not implementation details)
|
|
102
|
+
- Which modules will be tested
|
|
103
|
+
- Prior art for the tests (i.e. similar types of tests in the codebase)
|
|
104
|
+
|
|
105
|
+
## Documentation Impact
|
|
106
|
+
|
|
107
|
+
Which user-facing documentation artifacts are affected by these changes:
|
|
108
|
+
|
|
109
|
+
- **README** — Which sections need adding or updating?
|
|
110
|
+
- **CLI help text** — Are commands, flags, or usage examples changing?
|
|
111
|
+
- **docs/ content** — Are there deeper documentation files that need updating?
|
|
112
|
+
|
|
113
|
+
If no documentation changes are needed, state why (e.g., "Internal refactor — no user-facing behavior changes"). This assessment should be provided by the model based on its analysis and confirmed by the user during the interview.
|
|
114
|
+
|
|
115
|
+
## Out of Scope
|
|
116
|
+
|
|
117
|
+
A description of the things that are out of scope for this PRD.
|
|
118
|
+
|
|
119
|
+
## Further Notes
|
|
120
|
+
|
|
121
|
+
Any further notes about the feature.
|
|
122
|
+
|
|
123
|
+
</prd-template>
|
|
124
|
+
|
|
125
|
+
## Next Step
|
|
126
|
+
|
|
127
|
+
Once the PRD is saved, ask the user: "Ready to break this into issues? I can run `/stonecut-issues` next."
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills management — setup and removal of Claude Code skill symlinks.
|
|
3
|
+
*
|
|
4
|
+
* Exports pure functions; CLI integration is in cli.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
statSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readlinkSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
symlinkSync,
|
|
15
|
+
unlinkSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
|
|
20
|
+
export const SKILL_NAMES = ["stonecut-interview", "stonecut-prd", "stonecut-issues"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return the path to the skills/ directory shipped with this package.
|
|
24
|
+
*/
|
|
25
|
+
export function getSkillsSourceDir(): string {
|
|
26
|
+
return resolve(import.meta.dir, "skills");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return the skills target directory, optionally creating it.
|
|
31
|
+
*/
|
|
32
|
+
export function getSkillsTargetDir(
|
|
33
|
+
opts: {
|
|
34
|
+
create?: boolean;
|
|
35
|
+
claudeRoot?: string;
|
|
36
|
+
} = {},
|
|
37
|
+
): string {
|
|
38
|
+
const { create = true, claudeRoot } = opts;
|
|
39
|
+
const target = claudeRoot ? join(claudeRoot, "skills") : join(homedir(), ".claude", "skills");
|
|
40
|
+
if (create) {
|
|
41
|
+
mkdirSync(target, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
return target;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Check if path exists as a symlink (without following it). */
|
|
47
|
+
function isSymlink(path: string): boolean {
|
|
48
|
+
try {
|
|
49
|
+
return lstatSync(path).isSymbolicLink();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Check if something exists at path (symlink or real). */
|
|
56
|
+
function pathExists(path: string): boolean {
|
|
57
|
+
// existsSync follows symlinks, so also check lstat for dangling symlinks
|
|
58
|
+
if (existsSync(path)) return true;
|
|
59
|
+
return isSymlink(path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SkillsOutput {
|
|
63
|
+
messages: string[];
|
|
64
|
+
warnings: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Install Stonecut skills as symlinks into the target skills directory.
|
|
69
|
+
*/
|
|
70
|
+
export function setupSkills(claudeRoot?: string): SkillsOutput {
|
|
71
|
+
const sourceDir = getSkillsSourceDir();
|
|
72
|
+
const output: SkillsOutput = { messages: [], warnings: [] };
|
|
73
|
+
|
|
74
|
+
if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
75
|
+
throw new Error(`Skills directory not found at ${sourceDir}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const targetDir = getSkillsTargetDir({ claudeRoot });
|
|
79
|
+
|
|
80
|
+
for (const name of SKILL_NAMES) {
|
|
81
|
+
const source = join(sourceDir, name);
|
|
82
|
+
const target = join(targetDir, name);
|
|
83
|
+
|
|
84
|
+
if (!existsSync(source) || !statSync(source).isDirectory()) {
|
|
85
|
+
output.warnings.push(`Warning: skill source not found: ${source}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isSymlink(target)) {
|
|
90
|
+
try {
|
|
91
|
+
const existing = realpathSync(target);
|
|
92
|
+
if (existing === realpathSync(source)) {
|
|
93
|
+
// Already points to the right place — skip silently
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Dangling symlink — remove it and re-create below
|
|
98
|
+
unlinkSync(target);
|
|
99
|
+
symlinkSync(source, target);
|
|
100
|
+
output.messages.push(`Replaced dangling link ${name} -> ${source}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const linkTarget = readlinkSync(target);
|
|
104
|
+
output.warnings.push(
|
|
105
|
+
`Warning: ${target} already exists as symlink -> ${linkTarget}. Skipping.`,
|
|
106
|
+
);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (pathExists(target)) {
|
|
111
|
+
// Regular file or directory
|
|
112
|
+
output.warnings.push(`Warning: ${target} already exists (not a symlink). Skipping.`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
symlinkSync(source, target);
|
|
117
|
+
output.messages.push(`Linked ${name} -> ${source}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove Stonecut skill symlinks from the target skills directory.
|
|
125
|
+
*/
|
|
126
|
+
export function removeSkills(claudeRoot?: string): SkillsOutput {
|
|
127
|
+
const sourceDir = getSkillsSourceDir();
|
|
128
|
+
const targetDir = getSkillsTargetDir({ create: false, claudeRoot });
|
|
129
|
+
const output: SkillsOutput = { messages: [], warnings: [] };
|
|
130
|
+
|
|
131
|
+
if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) {
|
|
132
|
+
return output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const name of SKILL_NAMES) {
|
|
136
|
+
const target = join(targetDir, name);
|
|
137
|
+
|
|
138
|
+
if (!isSymlink(target)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Only remove if it points into the Stonecut package
|
|
143
|
+
let resolved: string;
|
|
144
|
+
try {
|
|
145
|
+
resolved = realpathSync(target);
|
|
146
|
+
} catch {
|
|
147
|
+
// Dangling symlink — safe to remove
|
|
148
|
+
unlinkSync(target);
|
|
149
|
+
output.messages.push(`Removed dangling link ${name}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const expectedPath = join(sourceDir, name);
|
|
154
|
+
if (!existsSync(expectedPath) || resolved !== realpathSync(expectedPath)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
unlinkSync(target);
|
|
159
|
+
output.messages.push(`Removed ${name}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
You are executing a single task from {task_source}.
|
|
2
|
+
|
|
3
|
+
## Instructions
|
|
4
|
+
|
|
5
|
+
1. Read the PRD below — it contains the full context, architecture, and constraints.
|
|
6
|
+
2. Read the issue spec below — it contains exactly what to build and the acceptance criteria.
|
|
7
|
+
3. Implement everything described in the issue spec.
|
|
8
|
+
4. Verify your work compiles and passes any validation described in the issue.
|
|
9
|
+
5. Ensure `.gitignore` is updated for any generated artifacts (build outputs, dependencies, etc.).
|
|
10
|
+
|
|
11
|
+
IMPORTANT:
|
|
12
|
+
|
|
13
|
+
- Do ONLY this one issue. Stop after verifying.
|
|
14
|
+
- Do NOT commit — the orchestrator handles git operations.
|
|
15
|
+
- Do NOT modify files outside the scope of this issue unless fixing an import path that changed.
|
|
16
|
+
- Scope lint to specific files — do not run project-wide lint that auto-fixes unrelated files.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## PRD
|
|
21
|
+
|
|
22
|
+
{prd_content}
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Issue {issue_number}: {issue_filename}
|
|
27
|
+
|
|
28
|
+
{issue_content}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types and interfaces shared across the Stonecut CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Structured result from a single runner execution. */
|
|
6
|
+
export interface RunResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
durationSeconds: number;
|
|
10
|
+
output?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Result of a single afk iteration. */
|
|
15
|
+
export interface IterationResult {
|
|
16
|
+
issueNumber: number;
|
|
17
|
+
issueFilename: string;
|
|
18
|
+
success: boolean;
|
|
19
|
+
elapsedSeconds: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Protocol that all runner adapters must satisfy. */
|
|
24
|
+
export interface Runner {
|
|
25
|
+
run(prompt: string): Promise<RunResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Snapshot of the working tree state before a runner session. */
|
|
29
|
+
export interface WorkingTreeSnapshot {
|
|
30
|
+
untracked: Set<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Git operations used by the runner loop, injectable for testing. */
|
|
34
|
+
export interface GitOps {
|
|
35
|
+
snapshotWorkingTree(): WorkingTreeSnapshot;
|
|
36
|
+
stageChanges(snapshot: WorkingTreeSnapshot): boolean;
|
|
37
|
+
commitChanges(message: string): [boolean, string];
|
|
38
|
+
revertUncommitted(snapshot: WorkingTreeSnapshot): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A single issue from a local spec. */
|
|
42
|
+
export interface Issue {
|
|
43
|
+
number: number;
|
|
44
|
+
filename: string;
|
|
45
|
+
path: string;
|
|
46
|
+
content: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Structured metadata for a GitHub-backed PRD issue. */
|
|
50
|
+
export interface GitHubPrd {
|
|
51
|
+
number: number;
|
|
52
|
+
title: string;
|
|
53
|
+
body: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A single sub-issue from a GitHub PRD. */
|
|
57
|
+
export interface GitHubIssue {
|
|
58
|
+
number: number;
|
|
59
|
+
title: string;
|
|
60
|
+
body: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Source interface for reading issues from any backend (local or GitHub). */
|
|
64
|
+
export interface Source<T> {
|
|
65
|
+
getNextIssue(): Promise<T | null>;
|
|
66
|
+
completeIssue(issue: T): Promise<void>;
|
|
67
|
+
getRemainingCount(): Promise<[number, number]>;
|
|
68
|
+
getPrdContent(): Promise<string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Logger interface for session-scoped logging. */
|
|
72
|
+
export interface LogWriter {
|
|
73
|
+
log(message: string): void;
|
|
74
|
+
close(): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Session context threaded through a stonecut run. */
|
|
78
|
+
export interface Session {
|
|
79
|
+
logger: LogWriter;
|
|
80
|
+
git: GitOps;
|
|
81
|
+
runner: Runner;
|
|
82
|
+
runnerName: string;
|
|
83
|
+
}
|