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.
@@ -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 = ["stonecut-interview", "stonecut-prd", "stonecut-issues"];
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.
@@ -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
+ }
@@ -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
+ }