stonecut 1.2.0 → 1.3.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 +21 -15
- package/package.json +2 -1
- package/src/cli.ts +32 -334
- package/src/execute.ts +142 -0
- package/src/git.ts +1 -20
- package/src/import.ts +1 -1
- package/src/local.ts +28 -2
- package/src/prd.ts +136 -0
- package/src/runner.ts +4 -79
- package/src/skills/stonecut-issues/SKILL.md +4 -4
- package/src/skills/stonecut-prd/SKILL.md +1 -1
- package/src/skills/stonecut-review-architecture/REFERENCE.md +109 -0
- package/src/skills/stonecut-review-architecture/SKILL.md +98 -0
- package/src/skills.ts +6 -1
- package/src/sources/github.ts +1 -12
- package/src/spawn.ts +22 -0
- package/src/sync-back.ts +88 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stonecut-review-architecture
|
|
3
|
+
description: Explore a codebase to find opportunities for architectural improvement, focusing on making the codebase more testable by deepening shallow modules. Use when user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more AI-navigable.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Review Architecture
|
|
7
|
+
|
|
8
|
+
Explore a codebase like an AI would, surface architectural friction, discover opportunities for improving testability, and propose module-deepening refactors.
|
|
9
|
+
|
|
10
|
+
A **deep module** (John Ousterhout, "A Philosophy of Software Design") has a small interface hiding a large implementation. Deep modules are more testable, more AI-navigable, and let you test at the boundary instead of inside.
|
|
11
|
+
|
|
12
|
+
## Process
|
|
13
|
+
|
|
14
|
+
### 1. Explore the codebase
|
|
15
|
+
|
|
16
|
+
Use the Agent tool with subagent_type=Explore to navigate the codebase naturally. Do NOT follow rigid heuristics — explore organically and note where you experience friction:
|
|
17
|
+
|
|
18
|
+
- Where does understanding one concept require bouncing between many small files?
|
|
19
|
+
- Where are modules so shallow that the interface is nearly as complex as the implementation?
|
|
20
|
+
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called?
|
|
21
|
+
- Where do tightly-coupled modules create integration risk in the seams between them?
|
|
22
|
+
- Which parts of the codebase are untested, or hard to test?
|
|
23
|
+
|
|
24
|
+
The friction you encounter IS the signal.
|
|
25
|
+
|
|
26
|
+
### 2. Present candidates
|
|
27
|
+
|
|
28
|
+
Present a numbered list of deepening opportunities. For each candidate, show:
|
|
29
|
+
|
|
30
|
+
- **Cluster**: Which modules/concepts are involved
|
|
31
|
+
- **Why they're coupled**: Shared types, call patterns, co-ownership of a concept
|
|
32
|
+
- **Dependency category**: See [REFERENCE.md](REFERENCE.md) for the four categories
|
|
33
|
+
- **Test impact**: What existing tests would be replaced by boundary tests
|
|
34
|
+
|
|
35
|
+
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"
|
|
36
|
+
|
|
37
|
+
### 3. User picks a candidate
|
|
38
|
+
|
|
39
|
+
### 4. Frame the problem space
|
|
40
|
+
|
|
41
|
+
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
|
42
|
+
|
|
43
|
+
- The constraints any new interface would need to satisfy
|
|
44
|
+
- The dependencies it would need to rely on
|
|
45
|
+
- A rough illustrative code sketch to make the constraints concrete
|
|
46
|
+
|
|
47
|
+
Show this to the user, then immediately proceed to Step 5.
|
|
48
|
+
|
|
49
|
+
### 5. Design multiple interfaces
|
|
50
|
+
|
|
51
|
+
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
|
|
52
|
+
|
|
53
|
+
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category, what's being hidden). This brief is independent of the user-facing explanation in Step 4. Give each agent a different design constraint:
|
|
54
|
+
|
|
55
|
+
- Agent 1: "Minimize the interface — aim for 1-3 entry points max"
|
|
56
|
+
- Agent 2: "Maximize flexibility — support many use cases and extension"
|
|
57
|
+
- Agent 3: "Optimize for the most common caller — make the default case trivial"
|
|
58
|
+
- Agent 4 (if applicable): "Design around the ports & adapters pattern for cross-boundary dependencies"
|
|
59
|
+
|
|
60
|
+
Each sub-agent outputs:
|
|
61
|
+
|
|
62
|
+
1. Interface signature (types, methods, params)
|
|
63
|
+
2. Usage example showing how callers use it
|
|
64
|
+
3. What complexity it hides internally
|
|
65
|
+
4. Dependency strategy (how deps are handled — see [REFERENCE.md](REFERENCE.md))
|
|
66
|
+
5. Trade-offs
|
|
67
|
+
|
|
68
|
+
Present designs sequentially, then compare them in prose.
|
|
69
|
+
|
|
70
|
+
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid.
|
|
71
|
+
|
|
72
|
+
### 6. User picks an interface (or accepts recommendation)
|
|
73
|
+
|
|
74
|
+
### 7. Choose a destination
|
|
75
|
+
|
|
76
|
+
Ask the user where to save the RFC:
|
|
77
|
+
|
|
78
|
+
- **Local file** — Save as `.stonecut/specs/<name>/rfc.md` in the project. Ask the user: "What should I name this spec?" Create the `.stonecut/specs/<name>/` directory if it doesn't exist.
|
|
79
|
+
- **GitHub issue** — Create a GitHub issue using `gh issue create --label rfc`. Before creating, ensure the `rfc` label exists:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Only create the label if it doesn't already exist
|
|
83
|
+
if ! gh label list --search "rfc" --json name --jq '.[].name' | grep -qx "rfc"; then
|
|
84
|
+
gh label create rfc --description "Request for Comments — architecture/design decision" --color "D93F0B"
|
|
85
|
+
fi
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If the project already has a `.stonecut/` directory, default to suggesting local. Otherwise, just ask.
|
|
89
|
+
|
|
90
|
+
### 8. Write the RFC
|
|
91
|
+
|
|
92
|
+
Use the template in [REFERENCE.md](REFERENCE.md). Do NOT ask the user to review before creating — just create it and share the result.
|
|
93
|
+
|
|
94
|
+
The RFC captures design decisions — what was chosen, what was rejected, and why. The PRD writer (whether in this session or a future one) will explore the codebase itself; the RFC's job is to carry the decisions so they don't need to be re-derived. Leave implementation-level detail (exact signatures, migration steps, test file changes) for the PRD and issues.
|
|
95
|
+
|
|
96
|
+
## Next Step
|
|
97
|
+
|
|
98
|
+
Once the RFC is saved, ask the user: "Ready to write the PRD? I can run `/stonecut-prd` next."
|
package/src/skills.ts
CHANGED
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
import { join, resolve } from "node:path";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
|
|
20
|
-
export const SKILL_NAMES = [
|
|
20
|
+
export const SKILL_NAMES = [
|
|
21
|
+
"stonecut-interview",
|
|
22
|
+
"stonecut-prd",
|
|
23
|
+
"stonecut-issues",
|
|
24
|
+
"stonecut-review-architecture",
|
|
25
|
+
];
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Return the path to the skills/ directory shipped with this package.
|
package/src/sources/github.ts
CHANGED
|
@@ -5,20 +5,9 @@
|
|
|
5
5
|
* structured as a stateless SourceProvider.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { runSync } from "../spawn";
|
|
8
9
|
import type { IssueData, PrdData, PrdSummary, SourceProvider } from "./types.js";
|
|
9
10
|
|
|
10
|
-
function runSync(cmd: string[]): { exitCode: number; stdout: string; stderr: string } {
|
|
11
|
-
const proc = Bun.spawnSync(cmd, {
|
|
12
|
-
stdout: "pipe",
|
|
13
|
-
stderr: "pipe",
|
|
14
|
-
});
|
|
15
|
-
return {
|
|
16
|
-
exitCode: proc.exitCode,
|
|
17
|
-
stdout: proc.stdout.toString(),
|
|
18
|
-
stderr: proc.stderr.toString(),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
11
|
export class GitHubSourceProvider implements SourceProvider {
|
|
23
12
|
readonly owner: string;
|
|
24
13
|
readonly repo: string;
|
package/src/spawn.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared synchronous process wrapper used by git.ts and sources/github.ts.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for Bun.spawnSync stdout/stderr conversion.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Run a command synchronously, optionally in a specific working directory. */
|
|
8
|
+
export function runSync(
|
|
9
|
+
cmd: string[],
|
|
10
|
+
cwd?: string,
|
|
11
|
+
): { exitCode: number; stdout: string; stderr: string } {
|
|
12
|
+
const proc = Bun.spawnSync(cmd, {
|
|
13
|
+
stdout: "pipe",
|
|
14
|
+
stderr: "pipe",
|
|
15
|
+
...(cwd && { cwd }),
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
exitCode: proc.exitCode,
|
|
19
|
+
stdout: proc.stdout.toString(),
|
|
20
|
+
stderr: proc.stderr.toString(),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/src/sync-back.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync-back — notify external sources when issues or PRDs complete.
|
|
3
|
+
*
|
|
4
|
+
* syncBackIssue: read frontmatter from an issue file, resolve provider, call onIssueComplete.
|
|
5
|
+
* syncBackPrd: read frontmatter from a PRD file, resolve provider, call onPrdComplete.
|
|
6
|
+
*
|
|
7
|
+
* Failures are logged as warnings but never thrown — sync-back is best-effort.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
12
|
+
import { getSourceProvider } from "./sources/index";
|
|
13
|
+
import type { LogWriter } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for syncing issue/PRD completion back to an external source.
|
|
17
|
+
*
|
|
18
|
+
* When provided to runAfkLoop, the runner reads frontmatter from completed
|
|
19
|
+
* issue files. If a `source` field is present, the corresponding provider
|
|
20
|
+
* is resolved and notified of the completion.
|
|
21
|
+
*/
|
|
22
|
+
export interface SyncBackConfig<T> {
|
|
23
|
+
/** Return the file path for a completed issue, or undefined to skip sync-back. */
|
|
24
|
+
getIssuePath: (issue: T) => string | undefined;
|
|
25
|
+
/** Path to the PRD file, used for sync-back after all issues complete. */
|
|
26
|
+
prdPath?: string;
|
|
27
|
+
/** Read a file's contents. Injectable for testing; defaults to fs.readFileSync. */
|
|
28
|
+
readFile?: (path: string) => string;
|
|
29
|
+
/** Resolve a source provider by name. Injectable for testing; defaults to getSourceProvider. */
|
|
30
|
+
resolveProvider?: (name: string) => {
|
|
31
|
+
onIssueComplete(id: string): Promise<void>;
|
|
32
|
+
onPrdComplete(id: string): Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sync a completed issue back to its external source.
|
|
38
|
+
*
|
|
39
|
+
* Reads frontmatter from the issue file. If `source` and `issue` fields
|
|
40
|
+
* are present, resolves the provider and calls onIssueComplete.
|
|
41
|
+
* Failures are logged as warnings but never thrown.
|
|
42
|
+
*/
|
|
43
|
+
export async function syncBackIssue(
|
|
44
|
+
filePath: string,
|
|
45
|
+
logger: LogWriter,
|
|
46
|
+
readFile: (path: string) => string = (p) => readFileSync(p, "utf-8"),
|
|
47
|
+
resolveProvider: SyncBackConfig<unknown>["resolveProvider"] = getSourceProvider,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
const content = readFile(filePath);
|
|
51
|
+
const { meta } = parseFrontmatter(content);
|
|
52
|
+
if (!meta.source || !meta.issue) return;
|
|
53
|
+
|
|
54
|
+
const provider = resolveProvider!(meta.source);
|
|
55
|
+
await provider.onIssueComplete(meta.issue);
|
|
56
|
+
logger.log(`Synced issue #${meta.issue} back to ${meta.source}`);
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
logger.log(`Warning: sync-back failed for issue at ${filePath}: ${message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sync PRD completion back to its external source.
|
|
65
|
+
*
|
|
66
|
+
* Reads frontmatter from the PRD file. If `source` and `issue` fields
|
|
67
|
+
* are present, resolves the provider and calls onPrdComplete.
|
|
68
|
+
* Failures are logged as warnings but never thrown.
|
|
69
|
+
*/
|
|
70
|
+
export async function syncBackPrd(
|
|
71
|
+
filePath: string,
|
|
72
|
+
logger: LogWriter,
|
|
73
|
+
readFile: (path: string) => string = (p) => readFileSync(p, "utf-8"),
|
|
74
|
+
resolveProvider: SyncBackConfig<unknown>["resolveProvider"] = getSourceProvider,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const content = readFile(filePath);
|
|
78
|
+
const { meta } = parseFrontmatter(content);
|
|
79
|
+
if (!meta.source || !meta.issue) return;
|
|
80
|
+
|
|
81
|
+
const provider = resolveProvider!(meta.source);
|
|
82
|
+
await provider.onPrdComplete(meta.issue);
|
|
83
|
+
logger.log(`Synced PRD #${meta.issue} back to ${meta.source}`);
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
logger.log(`Warning: sync-back failed for PRD at ${filePath}: ${message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|