sandcastle-drain 0.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +139 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/content/agent-docs/issue-tracker.md +22 -0
  8. package/dist/content/agent-docs/sandcastle-windows-cleanup.md +45 -0
  9. package/dist/content/agent-docs/triage-labels.md +101 -0
  10. package/dist/content/principles/README.md +39 -0
  11. package/dist/content/principles/architecture.md +124 -0
  12. package/dist/content/principles/claude-code-modes.md +47 -0
  13. package/dist/content/principles/clean-code.md +102 -0
  14. package/dist/content/principles/context-budget.md +81 -0
  15. package/dist/content/principles/cqrs.md +70 -0
  16. package/dist/content/principles/domain-modeling.md +62 -0
  17. package/dist/content/principles/frontend-organization.md +120 -0
  18. package/dist/content/principles/language-and-types.md +85 -0
  19. package/dist/content/principles/linting-and-tooling.md +122 -0
  20. package/dist/content/principles/personal-use-tradeoffs.md +55 -0
  21. package/dist/content/principles/testing.md +89 -0
  22. package/dist/orchestrator/blocked-by.d.ts +17 -0
  23. package/dist/orchestrator/blocked-by.d.ts.map +1 -0
  24. package/dist/orchestrator/blocked-by.js +48 -0
  25. package/dist/orchestrator/blocked-by.js.map +1 -0
  26. package/dist/orchestrator/ci-gate.d.ts +28 -0
  27. package/dist/orchestrator/ci-gate.d.ts.map +1 -0
  28. package/dist/orchestrator/ci-gate.js +198 -0
  29. package/dist/orchestrator/ci-gate.js.map +1 -0
  30. package/dist/orchestrator/main.d.ts +10 -0
  31. package/dist/orchestrator/main.d.ts.map +1 -0
  32. package/dist/orchestrator/main.js +883 -0
  33. package/dist/orchestrator/main.js.map +1 -0
  34. package/dist/orchestrator/prereqs.d.ts +30 -0
  35. package/dist/orchestrator/prereqs.d.ts.map +1 -0
  36. package/dist/orchestrator/prereqs.js +191 -0
  37. package/dist/orchestrator/prereqs.js.map +1 -0
  38. package/dist/orchestrator/rejection.d.ts +60 -0
  39. package/dist/orchestrator/rejection.d.ts.map +1 -0
  40. package/dist/orchestrator/rejection.js +187 -0
  41. package/dist/orchestrator/rejection.js.map +1 -0
  42. package/dist/orchestrator/reviewer.d.ts +75 -0
  43. package/dist/orchestrator/reviewer.d.ts.map +1 -0
  44. package/dist/orchestrator/reviewer.js +260 -0
  45. package/dist/orchestrator/reviewer.js.map +1 -0
  46. package/dist/orchestrator/ship.d.ts +19 -0
  47. package/dist/orchestrator/ship.d.ts.map +1 -0
  48. package/dist/orchestrator/ship.js +73 -0
  49. package/dist/orchestrator/ship.js.map +1 -0
  50. package/dist/orchestrator/sibling-context.d.ts +16 -0
  51. package/dist/orchestrator/sibling-context.d.ts.map +1 -0
  52. package/dist/orchestrator/sibling-context.js +61 -0
  53. package/dist/orchestrator/sibling-context.js.map +1 -0
  54. package/dist/orchestrator/splits.d.ts +60 -0
  55. package/dist/orchestrator/splits.d.ts.map +1 -0
  56. package/dist/orchestrator/splits.js +149 -0
  57. package/dist/orchestrator/splits.js.map +1 -0
  58. package/dist/orchestrator/status.d.ts +13 -0
  59. package/dist/orchestrator/status.d.ts.map +1 -0
  60. package/dist/orchestrator/status.js +43 -0
  61. package/dist/orchestrator/status.js.map +1 -0
  62. package/dist/orchestrator/summary.d.ts +33 -0
  63. package/dist/orchestrator/summary.d.ts.map +1 -0
  64. package/dist/orchestrator/summary.js +59 -0
  65. package/dist/orchestrator/summary.js.map +1 -0
  66. package/dist/orchestrator/sweep.d.ts +18 -0
  67. package/dist/orchestrator/sweep.d.ts.map +1 -0
  68. package/dist/orchestrator/sweep.js +79 -0
  69. package/dist/orchestrator/sweep.js.map +1 -0
  70. package/dist/orchestrator/teardown.d.ts +12 -0
  71. package/dist/orchestrator/teardown.d.ts.map +1 -0
  72. package/dist/orchestrator/teardown.js +42 -0
  73. package/dist/orchestrator/teardown.js.map +1 -0
  74. package/dist/orchestrator/worktree-cleanup.d.ts +2 -0
  75. package/dist/orchestrator/worktree-cleanup.d.ts.map +1 -0
  76. package/dist/orchestrator/worktree-cleanup.js +39 -0
  77. package/dist/orchestrator/worktree-cleanup.js.map +1 -0
  78. package/dist/prompts/implementer.md.tpl +85 -0
  79. package/dist/prompts/reviewer.md.tpl +118 -0
  80. package/dist/render-prompt.d.ts +22 -0
  81. package/dist/render-prompt.d.ts.map +1 -0
  82. package/dist/render-prompt.js +64 -0
  83. package/dist/render-prompt.js.map +1 -0
  84. package/dist/stage.d.ts +43 -0
  85. package/dist/stage.d.ts.map +1 -0
  86. package/dist/stage.js +105 -0
  87. package/dist/stage.js.map +1 -0
  88. package/docker/Dockerfile +42 -0
  89. package/package.json +48 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Downeys
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # sandcastle-drain
2
+
3
+ A wrapper around [`@ai-hero/sandcastle`](https://github.com/mattpocock/sandcastle) that drains a queue of GitHub issues labeled `sandcastle`, runs Claude Code against each in an isolated Docker worktree, and posts results back to the issue. Ships an opinionated set of engineering principles and a reviewer rubric that enforces them.
4
+
5
+ ## Prerequisites
6
+
7
+ The wrapper relies on the host machine to supply these. None of them are installed for you.
8
+
9
+ - **Docker installed and running.** The agent runs inside the container declared in `docker/Dockerfile` (Node 22 + git + gh + Claude Code CLI + Playwright + Chromium).
10
+ - **Node.js 20+** on the host (the wrapper itself is a Node CLI).
11
+ - **`gh` CLI installed and `gh auth login` complete.** The wrapper shells out to `gh issue list / edit / comment / create` and `gh pr create / merge`.
12
+ - **Claude Code CLI installed locally**, with OAuth credentials persisted to `~/.config/sandcastle-claude-creds/`. The wrapper bind-mounts that directory into every sandbox so the agent reuses your Pro/Max subscription. Bootstrap once with:
13
+ ```sh
14
+ mkdir -p ~/.config/sandcastle-claude-creds
15
+ docker run -it --rm \
16
+ --entrypoint claude \
17
+ -v ~/.config/sandcastle-claude-creds:/home/agent/.claude \
18
+ sandcastle:<your-image-name> \
19
+ login
20
+ ```
21
+ `--entrypoint claude` overrides the base image's `sleep infinity` so the device-code flow runs. Re-run if a drain reports auth errors mid-flight.
22
+ - **Matt Pocock's `tdd` and `diagnose` skills** installed at `<host>/.claude/skills/{tdd,diagnose}/` via:
23
+ ```sh
24
+ npx skills@latest add mattpocock/skills/tdd mattpocock/skills/diagnose
25
+ ```
26
+ The wrapper probes for these at startup and refuses to drain without them.
27
+ - **A GitHub repo with the canonical labels.** The wrapper auto-creates any missing labels (`sandcastle`, `in-progress`, `needs-review`, `blocked`, `retry`, `priority`, `oversized`, `skipped-this-run`, `needs-info`) on first run.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npm install --save-dev sandcastle-drain
33
+ ```
34
+
35
+ The package exposes a single binary, `sandcastle-drain`. Invoke it via `npx`:
36
+
37
+ ```sh
38
+ npx sandcastle-drain <subcommand>
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ | Command | What it does |
44
+ | ----------------------- | ------------------------------------------------------------------------------------------------------- |
45
+ | `npx sandcastle-drain drain` | Process every open issue labeled `sandcastle`. One agent run per issue, on a branch `agent/issue-N`. |
46
+ | `npx sandcastle-drain ship N` | Push `agent/issue-N`, open a PR with `Closes #N`, squash-merge it, and delete the remote branch. |
47
+ | `npx sandcastle-drain sweep N`| Post-merge cleanup: pull main, remove the worktree directory, prune git's worktree metadata, delete the local branch. Refuses to run unless a MERGED PR exists for the branch. |
48
+
49
+ All paths resolve relative to the host working directory where you ran `npx sandcastle-drain`. The wrapper writes runtime artifacts to `<host-cwd>/.sandcastle-drain/` (logs, worktrees, staged content, optional `splits.json`).
50
+
51
+ ## What the wrapper enforces
52
+
53
+ Two layers run on every implementer commit: a fixed set of **development principles** the implementer must follow, and a four-category **reviewer rubric** that audits the diff after the commits land.
54
+
55
+ The principle files ship inside the package at `dist/content/principles/` and are staged into `<host-cwd>/.sandcastle-drain/staged/principles/` before each drain so the agent can read them from inside the sandbox. Twelve files cover language and types, architecture (onion layers), CQRS, frontend organization, domain modeling, testing, linting and tooling, clean code, personal-use trade-offs, context-budget discipline (100k target / 150k ceiling), Claude Code interactive-vs-autonomous mode deltas, and a README that indexes the rest. Both the implementer and the reviewer eager-load the relevant files.
56
+
57
+ The reviewer rubric is four categories. **Domain integrity** flags anemic-model violations and any aggregate-specific invariants the host has written into `CONTEXT.md` or an ADR. **Test discipline** enforces the behavior-required test rule (every commit that introduces testable behavior ships with tests), property-based testing on state machines, and integration tests that hit real infrastructure rather than mocks. **Architecture intent** rejects inheritance of domain classes, impurity in the domain layer, and cross-layer imports that violate the onion direction. **Glossary & ADR alignment** checks that new names match `CONTEXT.md` verbatim and that diffs don't contradict any ADR under `docs/adr/`.
58
+
59
+ ## Reviewer-gating behavior
60
+
61
+ The reviewer is **gating in the success path** and emits findings advisorily on the rejection path — it is not "advisory only." `handleRejection` in `dist/orchestrator/main.ts` is the load-bearing function; the flow is:
62
+
63
+ 1. After the implementer commits and the CI gate passes, the reviewer sub-agent runs read-only against the worktree and emits a JSON verdict (`PASS` or `FAIL`) with a structured findings array.
64
+ 2. **`PASS` + CI green** → the wrapper auto-ships and sweeps: push, open a PR with `Closes #N`, squash-merge, delete the remote branch. The issue auto-closes via the squash-merge body.
65
+ 3. **`FAIL`** → `handleRejection` tags the branch tip as `rejected/issue-N-attempt-K` (preserving the work), discards the local branch, files a new GitHub issue titled `[follow-up #N] <original title>` labeled `sandcastle` + `priority` whose body carries the reviewer findings + the list of changed files + commit titles, comments on the original linking the follow-up, and closes the original. The next drain cycle picks up the `priority`-labeled follow-up first, combined with auto-ship this prevents the rejected branch from merging until a follow-up passes.
66
+ 4. **Reviewer parse error or throw** → the wrapper posts an error comment on the issue, labels it `needs-review`, and leaves the branch in place for the human to inspect.
67
+
68
+ So `PASS` is required for auto-merge, and `FAIL` actively gates the merge by closing the original issue out and queueing a follow-up. The reviewer's findings remain advisory only in the sense that the wrapper does not modify the rejected diff for you — the next implementer run on the follow-up is what addresses them.
69
+
70
+ ## Optional host content
71
+
72
+ Two host artifacts deepen the reviewer rubric. Both are optional; the wrapper degrades gracefully when they're absent.
73
+
74
+ - **`CONTEXT.md`** is the canonical domain glossary. If populated, the reviewer enforces nomenclature binding — every new type / table / file path / UI label in a diff must use the exact names defined in `CONTEXT.md`. If `CONTEXT.md` is still the empty stub, the nomenclature check is silently dropped (per the conditional rubric).
75
+ - **`docs/adr/`** holds architectural decision records. If populated, the reviewer reads the ADR index and flags any diff that contradicts a written decision. If the directory is empty, the ADR-alignment check is silently dropped.
76
+
77
+ Both files / directories live in the **host project's** working directory, not inside the installed library. The reviewer prompt template eager-loads them from the worktree at review time.
78
+
79
+ ## Configuration knobs that exist today
80
+
81
+ None, intentionally. The wrapper is opinionated:
82
+
83
+ - Model is pinned to `claude-opus-4-7`.
84
+ - Label set is fixed (`sandcastle`, `in-progress`, `needs-review`, `blocked`, `retry`, `priority`, `oversized`, `skipped-this-run`, `needs-info`).
85
+ - Paths are fixed (`<host-cwd>/.sandcastle-drain/staged/`, `<host-cwd>/.sandcastle-drain/worktrees/`, `<host-cwd>/.sandcastle-drain/logs/`).
86
+ - Idle timeout: 10 minutes per run. Wall-clock cap: 90 minutes per run. One auto-retry on idle / wall-clock timeout.
87
+ - Reviewer budget: 5 minute idle, 30 minute wall-clock.
88
+
89
+ If you need different values, fork the wrapper or open an issue. Future versions may expose a `sandcastle.config.ts` if users need it; today there is no escape hatch beyond editing source.
90
+
91
+ ## Versioning discipline
92
+
93
+ This package follows semver with two specific contracts:
94
+
95
+ - **Principle file changes are minor.** Renaming a rule, adding a new principle file, tightening guidance — the host's `^x.y.z` range picks them up automatically and the next drain enforces them.
96
+ - **Reviewer rubric changes and reviewer JSON output schema changes are major.** Hosts may parse the verdict comment, and the set of review outcomes hosts see is part of the public contract. A new severity level, a renamed category, or a changed field shape bumps the major version. Pin the major version (`~x.y.z` or `x.y.x`) if you depend on a specific rubric shape.
97
+
98
+ Other public-API changes (CLI subcommand names, the staged-content layout under `dist/content/`, the orchestrator's exit codes) also bump major.
99
+
100
+ ## Limitations
101
+
102
+ - **Windows worktree teardown.** pnpm's `node_modules/.pnpm/` symlink farm defeats standard recursive deletion on Windows; `git worktree remove` surfaces `Function not implemented`. The wrapper ships `removeWorktreeDir` in `dist/orchestrator/worktree-cleanup.ts` (uses `robocopy /MIR` against an empty source) and runs it before every drain to clean up orphans. Sandcastle's own internal teardown still throws on Windows after a successful agent run — the wrapper recovers commits via `tryRecoverCommits` and labels the run `ok (windows-teardown)`. This is the documented success path on Windows, not a failure mode.
103
+ - **No CI / GitHub Actions variant in v1.** The wrapper runs locally only. Authentication uses your volume-mounted Pro/Max OAuth credentials, which Sandcastle upstream does not first-class — don't deploy this to a cloud VM. See [issue #191 on mattpocock/sandcastle](https://github.com/mattpocock/sandcastle/issues/191) for upstream context.
104
+ - **Sandcastle is pinned to an exact version** in this package's dependencies. Treat upstream upgrades as breaking until you've re-tested the auth path and the worktree lifecycle.
105
+
106
+ ## License
107
+
108
+ MIT. See [`LICENSE`](LICENSE).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `sandcastle-drain` CLI — the single entry point users invoke via `npx sandcastle-drain
4
+ * <subcommand>`. Three subcommands route to the existing orchestrators:
5
+ *
6
+ * sandcastle-drain drain — drain the GitHub issue queue
7
+ * sandcastle-drain ship <issue> — push, PR, squash-merge an `agent/issue-N` branch
8
+ * sandcastle-drain sweep <issue> — post-merge worktree/branch cleanup
9
+ *
10
+ * Startup probes (`probeSkills`, `probeAuth`, `probeGhAuth`, `probeLabels`) run
11
+ * once up-front regardless of subcommand so the user gets an actionable error
12
+ * before any work begins. Paths inside the orchestrators resolve relative to
13
+ * `process.cwd()` — the host project where `npx sandcastle-drain` was invoked — not
14
+ * the installed library's directory.
15
+ */
16
+ import { mkdirSync, writeFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { runDrain } from './orchestrator/main.js';
19
+ import { runAllPrereqs } from './orchestrator/prereqs.js';
20
+ import { ShipError, shipBranch } from './orchestrator/ship.js';
21
+ import { SweepError, sweepBranch } from './orchestrator/sweep.js';
22
+ import { stage } from './stage.js';
23
+ const HELP_TEXT = `Usage: sandcastle-drain <command> [args]
24
+
25
+ Commands:
26
+ drain Drain the queue of \`sandcastle\`-labeled GitHub issues
27
+ ship <issue> Push, open a PR, and squash-merge \`agent/issue-<N>\`
28
+ sweep <issue> Post-merge cleanup: remove worktree, delete local branch
29
+
30
+ Options:
31
+ -h, --help Show this help message
32
+
33
+ All paths resolve relative to the current working directory.`;
34
+ function printHelp() {
35
+ console.log(HELP_TEXT);
36
+ }
37
+ function fail(msg, opts = {}) {
38
+ console.error(msg);
39
+ if (opts.showHelp) {
40
+ console.error('');
41
+ console.error(HELP_TEXT);
42
+ }
43
+ process.exit(1);
44
+ }
45
+ function parseIssueArg(subcommand, arg) {
46
+ if (!arg || !/^\d+$/.test(arg)) {
47
+ fail(`Usage: sandcastle-drain ${subcommand} <issue-number> (e.g. \`sandcastle-drain ${subcommand} 3\`)`);
48
+ }
49
+ return Number(arg);
50
+ }
51
+ function reportUnexpectedError(when, err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ const stack = err instanceof Error && err.stack ? err.stack : String(err);
54
+ const logPath = join(process.cwd(), '.sandcastle-drain', 'logs', 'sandcastle-startup.log');
55
+ let logRef = logPath;
56
+ try {
57
+ mkdirSync(join(process.cwd(), '.sandcastle-drain', 'logs'), { recursive: true });
58
+ writeFileSync(logPath, `${new Date().toISOString()}\n${stack}\n`);
59
+ }
60
+ catch {
61
+ logRef = '(could not write log file)';
62
+ }
63
+ console.error(`sandcastle-drain: unexpected error ${when}: ${message}`);
64
+ console.error(`See ${logRef} for details.`);
65
+ process.exit(1);
66
+ }
67
+ async function main() {
68
+ const [, , subcommand, ...rest] = process.argv;
69
+ if (!subcommand || subcommand === '-h' || subcommand === '--help') {
70
+ printHelp();
71
+ return;
72
+ }
73
+ // Validate args before running prereqs so usage errors don't depend on
74
+ // `gh` being authed — a missing issue number should fail fast with help text.
75
+ let issue;
76
+ switch (subcommand) {
77
+ case 'drain':
78
+ break;
79
+ case 'ship':
80
+ case 'sweep':
81
+ issue = parseIssueArg(subcommand, rest[0]);
82
+ break;
83
+ default:
84
+ fail(`Unknown command: ${subcommand}`, { showHelp: true });
85
+ }
86
+ // Prereqs run once before any subcommand. The probes are cheap (a few exec
87
+ // calls + a single `gh label list`), and surfacing a missing skill or
88
+ // expired token up-front beats failing mid-drain. Wrap in a try/catch so an
89
+ // unexpected throw (e.g. `gh` returning unparseable JSON, an EACCES on the
90
+ // skills check) becomes a clean one-liner with a log file pointer instead
91
+ // of a raw stack trace.
92
+ let token;
93
+ try {
94
+ ({ token } = await runAllPrereqs());
95
+ }
96
+ catch (err) {
97
+ reportUnexpectedError('during startup', err);
98
+ }
99
+ switch (subcommand) {
100
+ case 'drain':
101
+ // Stage library content (principles, agent-docs) into
102
+ // <host-cwd>/.sandcastle-drain/staged/ before the drain begins, so the
103
+ // per-issue worktrees can copy it in via `copyToWorktree`. Prompts are
104
+ // rendered in memory by `src/render-prompt.ts` and never materialize on
105
+ // the host filesystem.
106
+ await stage(process.cwd());
107
+ await runDrain({ token });
108
+ return;
109
+ case 'ship':
110
+ try {
111
+ await shipBranch({ issue: issue });
112
+ }
113
+ catch (err) {
114
+ if (err instanceof ShipError)
115
+ fail(`[ship] ${err.message}`);
116
+ throw err;
117
+ }
118
+ return;
119
+ case 'sweep':
120
+ try {
121
+ await sweepBranch({ issue: issue });
122
+ }
123
+ catch (err) {
124
+ if (err instanceof SweepError)
125
+ fail(`[sweep] ${err.message}`);
126
+ throw err;
127
+ }
128
+ return;
129
+ }
130
+ }
131
+ try {
132
+ await main();
133
+ }
134
+ catch (err) {
135
+ // Catches anything that escaped the subcommand handlers (the prereq path
136
+ // has its own wrapper that adds "during startup" context).
137
+ reportUnexpectedError('after startup', err);
138
+ }
139
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. `gh` infers the repo from `git remote -v` — verify that `origin` points at the right GitHub repository before running `gh` commands.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
11
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
12
+ - **Close**: `gh issue close <number> --comment "..."`
13
+
14
+ Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.
15
+
16
+ ## When a skill says "publish to the issue tracker"
17
+
18
+ Create a GitHub issue.
19
+
20
+ ## When a skill says "fetch the relevant ticket"
21
+
22
+ Run `gh issue view <number> --comments`.
@@ -0,0 +1,45 @@
1
+ # Sandcastle on Windows: worktree teardown is the success path
2
+
3
+ ## Behavior
4
+
5
+ On Windows, `sandcastle.run()` throws `error: failed to delete '.sandcastle-drain/worktrees/agent-issue-N': Function not implemented` _after_ the agent has committed. This is the **expected** exit path for any drain that runs `pnpm install`, not a failure mode. The wrapper:
6
+
7
+ 1. Catches the throw, recording `runError`. `result` is undefined.
8
+ 2. Reads `git log main..agent/issue-N` (via [`src/orchestrator/teardown.ts`](../../orchestrator/teardown.ts)) to recover the commit list directly from the branch.
9
+ 3. Labels the run `ok (windows-teardown)` in [`src/orchestrator/status.ts`](../../orchestrator/status.ts) — a success-tier status that sits alongside `completed` and `partial-work` under needs-review.
10
+
11
+ The teardown throw and the wrapper's post-hoc recovery are now considered routine Windows behavior. The status name makes it visible in the per-run GitHub comment so reviewers know what happened, without implying anything went wrong.
12
+
13
+ ## Cause
14
+
15
+ pnpm's `node_modules/.pnpm/` symlink farm defeats Windows recursive deletion (Node's `fs.rm`, `Remove-Item`, `rmdir /s`, and git's own worktree teardown — git surfaces `Function not implemented` from the kernel). Sandcastle's internal `WorktreeManager.remove` runs in the success path and trips this.
16
+
17
+ The wrapper's own `removeWorktreeDir` mitigation (`robocopy /MIR` in [`src/orchestrator/worktree-cleanup.ts`](../../orchestrator/worktree-cleanup.ts)) handles the same root cause for the _next-run_ orphan cleanup — but it cannot run inside sandcastle's lifecycle, because sandcastle owns its own worktree teardown.
18
+
19
+ ## What we tried (for context)
20
+
21
+ Probed sandcastle 0.5.7's public API for a way to disable internal worktree cleanup so the wrapper could own it:
22
+
23
+ - `RunOptions` in `node_modules/@ai-hero/sandcastle/dist/run.d.ts` — no `cleanupWorktree`, `keepWorktree`, or equivalent flag.
24
+ - `SandboxHooks` in `dist/SandboxLifecycle.d.ts` — only `onWorktreeReady` / `onSandboxReady` setup hooks; no teardown hook.
25
+ - `WorktreeManager.remove` exists in `dist/WorktreeManager.d.ts` but is internal — not callable from the wrapper without forking.
26
+ - One escape hatch: `RunOptions.signal` docs say "The worktree is preserved on disk after abort (error-path behavior)." Aborting works, but defeats the purpose — we want a normal completion that doesn't tear down.
27
+
28
+ ## Decision
29
+
30
+ We accept the throw-then-read flow as the durable Windows path until sandcastle exposes a cleanup-ownership hook. It is the success path, not an error path. The pre-run cleanup at `processIssue` step (b.5) calls `removeWorktreeDir` against any orphaned dir from a prior drain so the symlink farm doesn't accumulate.
31
+
32
+ ## When to revisit
33
+
34
+ When sandcastle ships a release that:
35
+
36
+ - Adds a `cleanupWorktree: false` (or similarly-named) option to `RunOptions`, **or**
37
+ - Adds a `beforeWorktreeRemove` / `onTeardown` hook to `SandboxHooks`, **or**
38
+ - Switches its own teardown to use long-path-aware Win32 APIs (e.g. invokes our same `robocopy /MIR` trick or `RemoveDirectoryW` with `FILE_FLAG_BACKUP_SEMANTICS`).
39
+
40
+ At that point, take ownership of cleanup in the wrapper and remove the `ok (windows-teardown)` status — runs will exit cleanly as `completed`.
41
+
42
+ ## Do not
43
+
44
+ - Fork sandcastle to add the option (per memory `sandcastle_api_drift_v0_5_7.md`).
45
+ - Pre-empt sandcastle's cleanup by deleting `node_modules/.pnpm/` ourselves before `run()` returns — that races sandcastle's lifecycle and corrupts the agent's workspace.
@@ -0,0 +1,101 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker, plus the workflow labels the sandcastle-drain wrapper auto-manages.
4
+
5
+ ## Triage state labels
6
+
7
+ Used by the `triage` skill's state machine.
8
+
9
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
10
+ | -------------------------- | -------------------- | --------------------------------------- |
11
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
12
+ | `needs-info` | `needs-info` | Waiting for more information (see note) |
13
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
14
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
15
+ | `wontfix` | `wontfix` | Will not be actioned |
16
+
17
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
18
+
19
+ > **Note on `needs-info`:** the sandcastle-drain wrapper at `src/orchestrator/main.ts` also writes this label automatically whenever a run produces 0 commits — bail-out, timeout, or hard error. See the workflow section below. Same label, two writers (you, manually, and the wrapper).
20
+
21
+ ## sandcastle-drain workflow labels
22
+
23
+ Eight labels that exist only for the sandcastle-drain wrapper at `src/orchestrator/main.ts`. `sandcastle`, `blocked`, and `retry` are user-applied; `in-progress`, `needs-review`, `priority`, `oversized`, and `skipped-this-run` are wrapper-managed — don't touch them by hand unless you're recovering from a crashed run.
24
+
25
+ | Label | Applied when | Removed when |
26
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
27
+ | `sandcastle` | Issue is queued for the agent. Apply manually. | Wrapper transitions the issue to `needs-review` or `needs-info`. |
28
+ | `in-progress` | Wrapper picks up the issue at the start of a run. | Wrapper finishes the run (any outcome). |
29
+ | `needs-review` | Wrapper finishes a run that produced commits — success OR partial work after timeout/abort, both go here. | Manually, after review. |
30
+ | `blocked` | Skip this issue. Apply manually when blocked. | Manually, once unblocked. |
31
+ | `retry` | You apply alongside `sandcastle` to discard a prior agent attempt and re-run. | Wrapper removes it as part of the retry handling on the next drain. |
32
+ | `priority` | Wrapper applies to a rejection-loop follow-up issue (alongside `sandcastle`). The drain serves `priority`-labeled issues before unflagged ones, then by issue number. May also be applied manually to jump-the-queue urgent work. | Manually, after the issue is resolved or no longer urgent. |
33
+ | `oversized` | Wrapper applies to a parent issue when its implementer wrote `.sandcastle-drain/splits.json` during the run and the wrapper filed the listed follow-ups. Audit trail signal — see the **split protocol** section below. | Manually, when the follow-ups have all landed and the parent can be closed (or kept open as a tracker — your call). |
34
+ | `skipped-this-run` | Wrapper applies when a drain bypassed the issue without running the agent — blocked-by-failed-sibling, an existing `agent/issue-N` branch already in place, or the rate-limit tail of a curtailed drain. The accompanying comment names the reason. | Wrapper removes it at the start of the next outcome block so a successful run never carries a stale breadcrumb. |
35
+
36
+ The wrapper also writes the triage-state `needs-info` label as one of the run outcomes — see the state machine below.
37
+
38
+ The wrapper drains in this order: `priority`-labeled `sandcastle` issues first (oldest by issue number), then unflagged `sandcastle` issues (oldest by issue number). Issues with `in-progress` or `blocked` are skipped at queue-fetch time.
39
+
40
+ ## Outcome state machine
41
+
42
+ After every `sandcastle.run()`, the wrapper:
43
+
44
+ 1. Posts a status comment on the issue containing the run's status string, branch name, commit count + SHAs, last ~50 lines of agent stdout in a `<details>` block, and the host-side log file path. This is best-effort; if `gh` fails, the run still proceeds.
45
+ 2. Runs the CI gate, then the advisory reviewer sub-agent. Both produce comments on the issue.
46
+ 3. Applies labels:
47
+
48
+ | Run outcome | Label change |
49
+ | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | Commits + CI green + reviewer `PASS` → wrapper auto-ships + sweeps | remove `in-progress` + `sandcastle` (issue auto-closed by squash-merge `Closes #N`) |
51
+ | Commits + reviewer `FAIL` → rejection loop | remove `in-progress` + `sandcastle`; tag `rejected/issue-N-attempt-K`; discard branch; open a follow-up issue with `sandcastle` + `priority`; comment on the original. The original gets no new label — the follow-up carries the work forward. |
52
+ | Commits + CI green + reviewer parse error / throw | remove `in-progress` + `sandcastle`, add `needs-review` (branch and worktree remain) |
53
+ | Commits + CI red | remove `in-progress` + `sandcastle`, add `needs-info` |
54
+ | No commits (bail-out, timeout, hard error — any 0-commit outcome) | remove `in-progress` + `sandcastle`, add `needs-info` |
55
+
56
+ The wrapper never leaves `sandcastle` on the issue after a run — silent re-queue is a footgun. To re-run, you re-apply `sandcastle` (with `retry` to discard the prior branch) explicitly.
57
+
58
+ **Auto-merge fallback.** If `npm run ship <N>` itself errors out (merge conflict, missing main protection, gh auth lapse), the wrapper logs the failure and falls back to the `needs-review` path so the slice is recoverable by hand. If ship succeeded but sweep failed, the merge is on main and the issue auto-closed — only the local worktree / branch leaks, and `npm run sweep <N>` cleans them up.
59
+
60
+ 4. Defensive check: verifies the agent didn't push the branch (`git rev-parse --verify origin/agent/issue-<N>` should fail). If it succeeded, flags a warning in the status comment but doesn't fail the run. The auto-merge path at step 3 pushes from the wrapper, not the agent — `gh pr merge` runs _after_ `git push -u origin <branch>` and is unrelated to this defensive check.
61
+
62
+ ## The reviewer-rejection flow
63
+
64
+ When the reviewer returns `FAIL` on commits, the wrapper closes out the run automatically rather than parking the issue for human review:
65
+
66
+ 1. Tags the branch tip as `rejected/issue-N-attempt-K`. `K` increments per rejection on the same issue — `git tag --list 'rejected/issue-N-attempt-*'` counts the prior attempts.
67
+ 2. Discards the local branch (`git branch -D agent/issue-N`).
68
+ 3. Files a new issue titled `[follow-up #N] <original title>` with labels `sandcastle` + `priority`. The body links back to the original, includes the reviewer's findings as a checklist, lists the files and commit titles from the prior attempt, and points at the `rejected/issue-N-attempt-K` tag so the next implementer can `git diff main..rejected/issue-N-attempt-K` for context.
69
+ 4. Comments on the original issue with a pointer to the follow-up.
70
+
71
+ The next drain iteration picks up the follow-up first (because of `priority`). To inspect a rejected attempt: `git show rejected/issue-N-attempt-K` for the annotated message, or `git diff main..rejected/issue-N-attempt-K` for the full diff.
72
+
73
+ ## The `retry` flow
74
+
75
+ When the agent's branch is wrong-headed and you want a fresh attempt:
76
+
77
+ 1. Comment on the issue explaining what was wrong (this comment becomes input to the next agent run, since the prompt pulls all comments).
78
+ 2. Swap `needs-review` for `sandcastle` AND add `retry`.
79
+ 3. Next drain: the wrapper sees `sandcastle` + `retry`, runs `git branch -D agent/issue-<N>`, removes `retry`, and processes the issue normally.
80
+
81
+ The wrapper only honors `retry` alongside `sandcastle`, never on `needs-review`. Adding `retry` to a `needs-review` issue does nothing destructive.
82
+
83
+ If the branch is mostly right but needs only minor tweaks, **don't use `retry`** — just `git checkout agent/issue-<N>`, edit, commit, push, and open a PR yourself. No agent involvement, no label changes needed. The agent has no memory between runs anyway, so a retry starts fresh from `main` with only the issue body and comments as feedback — re-running for small tweaks is much more expensive than fixing them by hand.
84
+
85
+ ## The split protocol
86
+
87
+ When the implementer realises mid-run that an issue's acceptance criteria don't fit under the 150k context ceiling, it commits what does fit and writes a list of follow-up issues into `.sandcastle-drain/splits.json` at the worktree root. The wrapper acts on the file after the implementer run completes:
88
+
89
+ 1. Reads + validates the file (array of `{ title, body }`, 1 to 10 entries).
90
+ 2. Files each entry as a new GitHub issue with `sandcastle` + `priority` labels. The next drain iteration picks them up before unflagged work — matching the rejection-loop precedent.
91
+ 3. Comments on the parent issue with a checklist linking the follow-ups.
92
+ 4. Applies the `oversized` label to the parent.
93
+ 5. Refetches the drain queue so the new follow-ups land at the front of the remaining queue.
94
+
95
+ The parent issue's foundation commits still flow through the normal `needs-review` / auto-merge / rejection path — splitting does not throw away the work the implementer did finish. Rejection takes precedence: if the reviewer FAILs on the run, the rejection-loop follow-up subsumes any split intent and the split flow is skipped.
96
+
97
+ The implementer prompt (`src/prompts/implementer.md.tpl`) documents the file shape and rules. `.sandcastle-drain/splits.json` is gitignored so a sloppy `git add -A` doesn't capture it.
98
+
99
+ ---
100
+
101
+ Edit any of these tables to match whatever vocabulary you actually use. If you change a label string, also update the wrapper at [`src/orchestrator/main.ts`](../../orchestrator/main.ts) and any references in [`README.md`](../../../README.md).
@@ -0,0 +1,39 @@
1
+ # Development principles
2
+
3
+ The rules Claude Code (interactive + sandcastle-drain-autonomous) follows when building code in this repo, plus the architectural constraints those rules force the _product code_ to take.
4
+
5
+ These principles serve two readers:
6
+
7
+ - **Claude Code** — what to enforce when writing TypeScript, when to skip ceremony, how to size sandcastle-drain issues, what's relaxed because this is personal-use, what is not.
8
+ - **The product code itself** — what shape the codebase must take to remain testable, maintainable, and faithful to the domain.
9
+
10
+ The product itself — its lifecycle, domain types, workflows — is **out of scope** here. Those decisions land in [CONTEXT.md](../../../CONTEXT.md) and [docs/adr/](../../../docs/adr/) as they crystallise.
11
+
12
+ ## Index
13
+
14
+ | File | Scope | Topic |
15
+ | ------------------------------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | [language-and-types.md](language-and-types.md) | Dev | TypeScript strict mode, Zod at boundaries, branded types, tagged-union results, no-Effect rationale |
17
+ | [architecture.md](architecture.md) | Dev + Product | Onion layers (Domain / Application / External / Presentation), package layout, dependency direction |
18
+ | [cqrs.md](cqrs.md) | Dev + Product | Read/write port separation at the Application boundary; one `<Aggregate>CommandRepository` + one `<Aggregate>QueryRepository` per new aggregate |
19
+ | [frontend-organization.md](frontend-organization.md) | Dev | Co-locate at the lowest level shared; per-component folders with `Component.{tsx,test,hook,util,mock,module.scss}`; feature folders; `shared/` for cross-feature |
20
+ | [domain-modeling.md](domain-modeling.md) | Dev + Product | DDD ceremony rule (wrong-if-violated), anemic-model ban, reified-association pattern, decomposed-display values, nomenclature binding to CONTEXT.md |
21
+ | [testing.md](testing.md) | Dev | Vitest + Playwright, per-layer coverage, property-based for domain, testcontainers + recorded fixtures, behavior-required test rule |
22
+ | [linting-and-tooling.md](linting-and-tooling.md) | Dev | ESLint + plugins, three custom rules, Husky + lint-staged + pre-commit, no `--no-verify` ever |
23
+ | [clean-code.md](clean-code.md) | Dev | DRY / YAGNI / KISS, small focused functions with lint-enforced max-depth and complexity, composition over inheritance, pure functions and immutability in the domain layer |
24
+ | [personal-use-tradeoffs.md](personal-use-tradeoffs.md) | Dev + Product | What's relaxed (UI, auth, ops), what's not (domain correctness, types, backups), tech-selection rule with paid-service ADR requirement |
25
+ | [context-budget.md](context-budget.md) | Dev (sandcastle-drain) | 100k target / 150k ceiling, BUDGET config location, mechanical post-run measurement, summarize-don't-paste |
26
+ | [claude-code-modes.md](claude-code-modes.md) | Dev | Universal rules + sandcastle-drain-only deltas (token budget, summarize-don't-paste, no-push, no-clarification) |
27
+
28
+ ## How to use this folder
29
+
30
+ - **Read [CLAUDE.md](../../../CLAUDE.md) first** — it links here and to the other agent-skill docs in [src/content/agent-docs/](../agent-docs/).
31
+ - **Then read the file relevant to what you're about to do.** Don't load the whole folder unless you're auditing.
32
+ - **If two files contradict, the more specific one wins** (e.g. `claude-code-modes.md` overrides general guidance for autonomous runs).
33
+ - **If a principle conflicts with what you're being asked to do**, surface the conflict in the issue or session — don't silently override.
34
+
35
+ ## What's deliberately _not_ here
36
+
37
+ - The product's lifecycle, workflow, domain types — those go in CONTEXT.md and ADRs once decided.
38
+ - Tooling enforcement (lint config, tsconfig flags, pre-commit hook wiring) — those are queued as separate sandcastle-drain issues; principles describe the rules, follow-up issues implement them.
39
+ - Scaffolded package folders — created when the first product issue needs them.