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
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Reviewer sub-agent invocation.
3
+ *
4
+ * After the implementer commits on `agent/issue-N`, the wrapper spawns a
5
+ * separate Sandcastle run with the rendered reviewer prompt. The reviewer is
6
+ * read-only against the worktree, eager-loads the project's principles, and
7
+ * emits a single fenced JSON block as its final message. This module owns:
8
+ *
9
+ * - `runReviewer` — spawn the reviewer run, capture stdout + log
10
+ * - `parseReviewerOutput` — extract the JSON verdict from stdout
11
+ * - `formatReviewerComment` — render the verdict as a GitHub issue comment
12
+ *
13
+ * The reviewer is advisory: a `FAIL` verdict produces a comment for the human
14
+ * to weigh; it does not gate the merge. See `src/prompts/reviewer.md.tpl`.
15
+ */
16
+ import { run, claudeCode } from '@ai-hero/sandcastle';
17
+ import { docker } from '@ai-hero/sandcastle/sandboxes/docker';
18
+ import { copyFile, mkdir } from 'node:fs/promises';
19
+ import { dirname } from 'node:path';
20
+ import { detectRubricFlags, STAGED_SANDBOX_PATH } from '../stage.js';
21
+ import { REPO_ROOT } from './prereqs.js';
22
+ import { renderPrompt } from '../render-prompt.js';
23
+ const FENCED_JSON_REGEX = /```json\s*\n([\s\S]*?)\n```/g;
24
+ const VALID_VERDICTS = new Set(['PASS', 'FAIL']);
25
+ const VALID_SEVERITIES = new Set(['high', 'medium', 'low']);
26
+ function isString(v) {
27
+ return typeof v === 'string';
28
+ }
29
+ function isNumber(v) {
30
+ return typeof v === 'number' && Number.isFinite(v);
31
+ }
32
+ function parseFinding(raw) {
33
+ if (raw === null || typeof raw !== 'object')
34
+ return 'finding is not an object';
35
+ const f = raw;
36
+ if (!isString(f.severity) || !VALID_SEVERITIES.has(f.severity)) {
37
+ return `severity must be one of ${[...VALID_SEVERITIES].join('|')}; got ${JSON.stringify(f.severity)}`;
38
+ }
39
+ if (!isString(f.principle))
40
+ return 'principle must be a string';
41
+ if (!isString(f.file))
42
+ return 'file must be a string';
43
+ if (!isNumber(f.line))
44
+ return 'line must be a number';
45
+ if (!isString(f.message))
46
+ return 'message must be a string';
47
+ if (!isString(f.suggestedFix))
48
+ return 'suggestedFix must be a string';
49
+ return {
50
+ severity: f.severity,
51
+ principle: f.principle,
52
+ file: f.file,
53
+ line: f.line,
54
+ message: f.message,
55
+ suggestedFix: f.suggestedFix,
56
+ };
57
+ }
58
+ /**
59
+ * Extracts the last fenced ```json``` block from the reviewer's stdout and
60
+ * validates it against the expected shape. We take the *last* block to be
61
+ * resilient against the reviewer including an example JSON block earlier in
62
+ * its thinking — the final answer is always the last one.
63
+ */
64
+ export function parseReviewerOutput(stdout) {
65
+ const matches = [...stdout.matchAll(FENCED_JSON_REGEX)];
66
+ if (matches.length === 0) {
67
+ return { ok: false, reason: 'no fenced ```json``` block found in reviewer output' };
68
+ }
69
+ const lastMatch = matches[matches.length - 1];
70
+ const rawJson = lastMatch[1].trim();
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(rawJson);
74
+ }
75
+ catch (err) {
76
+ return {
77
+ ok: false,
78
+ reason: `JSON.parse failed: ${err instanceof Error ? err.message : String(err)}`,
79
+ };
80
+ }
81
+ if (parsed === null || typeof parsed !== 'object') {
82
+ return { ok: false, reason: 'top-level JSON value is not an object' };
83
+ }
84
+ const obj = parsed;
85
+ if (!isString(obj.verdict) || !VALID_VERDICTS.has(obj.verdict)) {
86
+ return {
87
+ ok: false,
88
+ reason: `verdict must be "PASS" or "FAIL"; got ${JSON.stringify(obj.verdict)}`,
89
+ };
90
+ }
91
+ if (!Array.isArray(obj.findings)) {
92
+ return { ok: false, reason: 'findings must be an array' };
93
+ }
94
+ if (!isString(obj.summary)) {
95
+ return { ok: false, reason: 'summary must be a string' };
96
+ }
97
+ const findings = [];
98
+ for (let i = 0; i < obj.findings.length; i++) {
99
+ const result = parseFinding(obj.findings[i]);
100
+ if (typeof result === 'string') {
101
+ return { ok: false, reason: `findings[${i}]: ${result}` };
102
+ }
103
+ findings.push(result);
104
+ }
105
+ return {
106
+ ok: true,
107
+ value: {
108
+ verdict: obj.verdict,
109
+ findings,
110
+ summary: obj.summary,
111
+ },
112
+ };
113
+ }
114
+ function severityBadge(s) {
115
+ if (s === 'high')
116
+ return '🔴 high';
117
+ if (s === 'medium')
118
+ return '🟡 medium';
119
+ return '🔵 low';
120
+ }
121
+ function formatFinding(f) {
122
+ const location = f.line > 0 ? `${f.file}:${f.line}` : f.file;
123
+ return [
124
+ `**${severityBadge(f.severity)} — ${f.principle}**`,
125
+ `\`${location}\``,
126
+ '',
127
+ f.message,
128
+ '',
129
+ `_Suggested fix:_ ${f.suggestedFix}`,
130
+ ].join('\n');
131
+ }
132
+ /**
133
+ * Renders the reviewer's verdict as a GitHub issue comment body. The summary
134
+ * leads, then findings are listed by severity (high → medium → low) so the
135
+ * human sees the load-bearing items first.
136
+ */
137
+ export function formatReviewerComment(output) {
138
+ const verdictEmoji = output.verdict === 'PASS' ? '✅' : '❌';
139
+ const lines = [];
140
+ lines.push(`**Reviewer verdict:** ${verdictEmoji} \`${output.verdict}\` _(advisory — not a merge gate)_`);
141
+ lines.push('');
142
+ lines.push(output.summary);
143
+ if (output.findings.length > 0) {
144
+ const ordered = [...output.findings].sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
145
+ lines.push('');
146
+ lines.push(`### Findings (${output.findings.length})`);
147
+ lines.push('');
148
+ for (const f of ordered) {
149
+ lines.push(formatFinding(f));
150
+ lines.push('');
151
+ }
152
+ }
153
+ return lines.join('\n').trimEnd();
154
+ }
155
+ function severityRank(s) {
156
+ if (s === 'high')
157
+ return 0;
158
+ if (s === 'medium')
159
+ return 1;
160
+ return 2;
161
+ }
162
+ /**
163
+ * Formats a fallback comment when the reviewer either didn't run, or ran but
164
+ * didn't emit parseable JSON. We still want the human to know the reviewer
165
+ * was invoked and that its output is in the log file.
166
+ */
167
+ export function formatReviewerErrorComment(args) {
168
+ const lines = [];
169
+ lines.push('**Reviewer verdict:** ⚠️ `error` _(advisory — not a merge gate)_');
170
+ lines.push('');
171
+ lines.push(`The reviewer sub-agent ran but produced no parseable verdict: ${args.reason}`);
172
+ if (args.logFilePath) {
173
+ lines.push('');
174
+ lines.push(`See \`${args.logFilePath}\` for the full reviewer transcript.`);
175
+ }
176
+ return lines.join('\n');
177
+ }
178
+ /**
179
+ * Spawns the reviewer Sandcastle run. The branch already exists with the
180
+ * implementer's commits — the reviewer checks it out into a separate worktree,
181
+ * reads the diff, and emits its verdict. We do not pass a `branchStrategy` that
182
+ * would create a new branch; sandcastle reuses the existing one.
183
+ *
184
+ * On any failure (timeout, missing JSON, throw), we return a result with
185
+ * `output: undefined` and a non-empty `parseError`. The wrapper posts an error
186
+ * comment in that case rather than skipping the comment entirely — the absence
187
+ * of a reviewer verdict on an issue would itself be confusing.
188
+ */
189
+ export async function runReviewer(args) {
190
+ let result;
191
+ let runError;
192
+ try {
193
+ const flags = detectRubricFlags(REPO_ROOT);
194
+ const prompt = await renderPrompt('reviewer', {
195
+ ISSUE_NUMBER: String(args.issueNumber),
196
+ BRANCH: args.branch,
197
+ }, {
198
+ HAS_CONTEXT_MD: flags.hasContextMd,
199
+ HAS_ADRS: flags.hasAdrs,
200
+ HAS_PROJECT_RULES: flags.hasContextMd || flags.hasAdrs,
201
+ });
202
+ result = await run({
203
+ agent: claudeCode('claude-opus-4-7'),
204
+ sandbox: docker({
205
+ imageName: args.imageName,
206
+ mounts: [
207
+ { hostPath: args.hostCredsPath, sandboxPath: args.sandboxCredsPath },
208
+ { hostPath: args.stagedHostPath, sandboxPath: STAGED_SANDBOX_PATH, readonly: true },
209
+ ],
210
+ env: { GH_TOKEN: args.ghToken },
211
+ }),
212
+ prompt,
213
+ branchStrategy: { type: 'branch', branch: args.branch },
214
+ idleTimeoutSeconds: args.idleTimeoutSeconds,
215
+ signal: AbortSignal.timeout(args.wallClockTimeoutMs),
216
+ });
217
+ }
218
+ catch (err) {
219
+ runError = err;
220
+ }
221
+ const stdout = result?.stdout ?? (runError instanceof Error ? runError.message : String(runError ?? ''));
222
+ const sourceLogPath = result?.logFilePath;
223
+ // Best-effort copy the sandcastle log to our well-known path. The branch's
224
+ // implementer log lives alongside the reviewer log so post-mortem is easy.
225
+ let copiedLogPath;
226
+ if (sourceLogPath !== undefined) {
227
+ try {
228
+ await mkdir(dirname(args.reviewerLogPath), { recursive: true });
229
+ await copyFile(sourceLogPath, args.reviewerLogPath);
230
+ copiedLogPath = args.reviewerLogPath;
231
+ }
232
+ catch (err) {
233
+ console.error(`[reviewer] failed to copy log ${sourceLogPath} → ${args.reviewerLogPath}:`, err.message);
234
+ }
235
+ }
236
+ if (runError !== undefined && result === undefined) {
237
+ return {
238
+ output: undefined,
239
+ parseError: `reviewer run threw: ${runError instanceof Error ? runError.message : String(runError)}`,
240
+ stdout,
241
+ logFilePath: copiedLogPath,
242
+ };
243
+ }
244
+ const parsed = parseReviewerOutput(stdout);
245
+ if (!parsed.ok) {
246
+ return {
247
+ output: undefined,
248
+ parseError: parsed.reason,
249
+ stdout,
250
+ logFilePath: copiedLogPath,
251
+ };
252
+ }
253
+ return {
254
+ output: parsed.value,
255
+ parseError: undefined,
256
+ stdout,
257
+ logFilePath: copiedLogPath,
258
+ };
259
+ }
260
+ //# sourceMappingURL=reviewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewer.js","sourceRoot":"","sources":["../../src/orchestrator/reviewer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export declare class ShipError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface ShipBranchArgs {
5
+ issue: number;
6
+ }
7
+ export interface ShipBranchResult {
8
+ branch: string;
9
+ prUrl: string | undefined;
10
+ }
11
+ /**
12
+ * Pushes `agent/issue-N`, opens (or reuses) a PR with an explicit `Closes #N`
13
+ * body, squash-merges it, and deletes the remote branch. Throws `ShipError`
14
+ * with a human-readable message on any step that can't be recovered. Idempotent
15
+ * to the extent that re-running after a partial failure picks up where it left
16
+ * off — push is a no-op when up-to-date, an already-open PR is reused.
17
+ */
18
+ export declare function shipBranch(args: ShipBranchArgs): Promise<ShipBranchResult>;
19
+ //# sourceMappingURL=ship.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ship.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ship.ts"],"names":[],"mappings":"AA6BA,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA+DhF"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Pushes an `agent/issue-N` branch, opens a PR, and merges it (squash + delete
3
+ * remote branch). The PR body is explicitly set to include `Closes #N` so the
4
+ * merge auto-closes the issue regardless of what the agent's commit messages
5
+ * looked like.
6
+ *
7
+ * Invoked by `src/cli.ts` as `sandcastle-drain ship <issue>`. After this completes
8
+ * successfully, the user runs `sandcastle-drain sweep <issue>` to clean up the
9
+ * local worktree, branch, and pull main. The drain orchestrator (`main.ts`)
10
+ * also calls `shipBranch` inline when the CI gate and reviewer both pass.
11
+ */
12
+ import { execa } from 'execa';
13
+ import { REPO_ROOT } from './prereqs.js';
14
+ async function run(cmd, args, opts = {}) {
15
+ const r = await execa(cmd, args, { cwd: REPO_ROOT, reject: opts.reject ?? true });
16
+ return { exitCode: r.exitCode ?? 0, stdout: r.stdout, stderr: r.stderr };
17
+ }
18
+ export class ShipError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'ShipError';
22
+ }
23
+ }
24
+ /**
25
+ * Pushes `agent/issue-N`, opens (or reuses) a PR with an explicit `Closes #N`
26
+ * body, squash-merges it, and deletes the remote branch. Throws `ShipError`
27
+ * with a human-readable message on any step that can't be recovered. Idempotent
28
+ * to the extent that re-running after a partial failure picks up where it left
29
+ * off — push is a no-op when up-to-date, an already-open PR is reused.
30
+ */
31
+ export async function shipBranch(args) {
32
+ const branch = `agent/issue-${args.issue}`;
33
+ const branchCheck = await run('git', ['rev-parse', '--verify', branch], { reject: false });
34
+ if (branchCheck.exitCode !== 0) {
35
+ throw new ShipError(`Branch \`${branch}\` not found locally. Did you already ship this issue, or has \`sandcastle-drain drain\` run yet?`);
36
+ }
37
+ console.log(`[ship] Pushing ${branch} to origin...`);
38
+ await run('git', ['push', '-u', 'origin', branch]);
39
+ const titleResult = await run('git', ['log', '-1', '--pretty=%s', branch]);
40
+ const title = titleResult.stdout.trim();
41
+ // Explicit `Closes #N` so the squash-merge auto-closes the issue regardless of
42
+ // what's in commit messages — `gh pr create --fill` only reads the first
43
+ // commit's body, which is fragile when an agent makes multiple commits.
44
+ const body = `Closes #${args.issue}\n\n_Created via \`sandcastle-drain ship ${args.issue}\`._`;
45
+ console.log(`[ship] Creating PR for ${branch}...`);
46
+ const prCreate = await run('gh', ['pr', 'create', '--head', branch, '--base', 'main', '--title', title, '--body', body], { reject: false });
47
+ let prUrl;
48
+ if (prCreate.exitCode === 0) {
49
+ prUrl = prCreate.stdout.trim().split(/\s+/).pop();
50
+ }
51
+ else {
52
+ // A PR may already exist for this branch (e.g. ship was re-run after a
53
+ // merge failure). Surface the existing one and continue to the merge.
54
+ const list = await run('gh', ['pr', 'list', '--head', branch, '--state', 'open', '--json', 'number,url'], { reject: false });
55
+ const open = JSON.parse(list.stdout || '[]');
56
+ if (open.length === 0) {
57
+ throw new ShipError(`gh pr create failed and no open PR exists for ${branch}.\n${prCreate.stderr}`);
58
+ }
59
+ prUrl = open[0].url;
60
+ console.log(`[ship] PR already open: ${prUrl}`);
61
+ }
62
+ // Squash keeps main's history one-commit-per-slice. We do NOT use
63
+ // `gh pr merge --delete-branch` because that flag tries to delete the
64
+ // *local* branch too, which always fails at ship time — the branch is
65
+ // checked out in the worktree. Explicit remote-only delete follows.
66
+ console.log(`[ship] Merging (squash)...`);
67
+ await run('gh', ['pr', 'merge', branch, '--squash']);
68
+ console.log(`[ship] Deleting remote branch...`);
69
+ await run('git', ['push', 'origin', '--delete', branch]);
70
+ console.log(`[ship] Done. Run \`sandcastle-drain sweep ${args.issue}\` to clean up the local worktree and branch.`);
71
+ return { branch, prUrl };
72
+ }
73
+ //# sourceMappingURL=ship.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ship.js","sourceRoot":"","sources":["../../src/orchestrator/ship.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAQzC,KAAK,UAAU,GAAG,CAChB,GAAW,EACX,IAAc,EACd,OAA6B,EAAE;IAE/B,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;IAClF,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;AAC3E,CAAC;AAED,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAWD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAoB;IACnD,MAAM,MAAM,GAAG,eAAe,IAAI,CAAC,KAAK,EAAE,CAAC;IAE3C,MAAM,WAAW,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,IAAI,WAAW,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CACjB,YAAY,MAAM,mGAAmG,CACtH,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,kBAAkB,MAAM,eAAe,CAAC,CAAC;IACrD,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAEnD,MAAM,WAAW,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3E,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAExC,+EAA+E;IAC/E,yEAAyE;IACzE,wEAAwE;IACxE,MAAM,IAAI,GAAG,WAAW,IAAI,CAAC,KAAK,4CAA4C,IAAI,CAAC,KAAK,MAAM,CAAC;IAE/F,OAAO,CAAC,GAAG,CAAC,0BAA0B,MAAM,KAAK,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,MAAM,GAAG,CACxB,IAAI,EACJ,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,EACtF,EAAE,MAAM,EAAE,KAAK,EAAE,CAClB,CAAC;IAEF,IAAI,KAAyB,CAAC;IAC9B,IAAI,QAAQ,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC5B,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;IACpD,CAAC;SAAM,CAAC;QACN,uEAAuE;QACvE,sEAAsE;QACtE,MAAM,IAAI,GAAG,MAAM,GAAG,CACpB,IAAI,EACJ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,EAC3E,EAAE,MAAM,EAAE,KAAK,EAAE,CAClB,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAA2C,CAAC;QACvF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,SAAS,CACjB,iDAAiD,MAAM,MAAM,QAAQ,CAAC,MAAM,EAAE,CAC/E,CAAC;QACJ,CAAC;QACD,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,2BAA2B,KAAK,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,kEAAkE;IAClE,sEAAsE;IACtE,sEAAsE;IACtE,oEAAoE;IACpE,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAC1C,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAErD,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAChD,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IAEzD,OAAO,CAAC,GAAG,CACT,6CAA6C,IAAI,CAAC,KAAK,+CAA+C,CACvG,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface SiblingSummary {
2
+ issue: number;
3
+ branch: string;
4
+ changedFiles: string[];
5
+ newExports: string[];
6
+ }
7
+ export declare function extractNewExports(diff: string): string[];
8
+ export declare function summarizeBranch(args: {
9
+ issue: number;
10
+ branch: string;
11
+ baseBranch: string;
12
+ cwd: string;
13
+ }): Promise<SiblingSummary>;
14
+ export declare function estimateTokens(text: string): number;
15
+ export declare function buildSiblingContextBlock(siblings: readonly SiblingSummary[]): string;
16
+ //# sourceMappingURL=sibling-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sibling-context.d.ts","sourceRoot":"","sources":["../../src/orchestrator/sibling-context.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAOD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMxD;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,cAAc,CAAC,CAiB1B;AAKD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CA6BpF"}
@@ -0,0 +1,61 @@
1
+ import { execa } from 'execa';
2
+ // Match `+export ...` lines added in a unified diff. Intentionally narrow:
3
+ // missing matches just don't appear in the prompt — the goal is informational
4
+ // hints, not a complete export index.
5
+ const EXPORT_REGEX = /^\+export (?:async )?(?:function|const|class|type|interface|enum) (\w+)/gm;
6
+ export function extractNewExports(diff) {
7
+ const found = new Set();
8
+ for (const match of diff.matchAll(EXPORT_REGEX)) {
9
+ found.add(match[1]);
10
+ }
11
+ return [...found];
12
+ }
13
+ export async function summarizeBranch(args) {
14
+ const { issue, branch, baseBranch, cwd } = args;
15
+ const range = `${baseBranch}..${branch}`;
16
+ const namesResult = await execa('git', ['diff', '--name-only', range], {
17
+ cwd,
18
+ reject: false,
19
+ });
20
+ const changedFiles = namesResult.exitCode === 0
21
+ ? namesResult.stdout.split(/\r?\n/).filter((line) => line.length > 0)
22
+ : [];
23
+ const diffResult = await execa('git', ['diff', range], { cwd, reject: false });
24
+ const newExports = diffResult.exitCode === 0 ? extractNewExports(diffResult.stdout) : [];
25
+ return { issue, branch, changedFiles, newExports };
26
+ }
27
+ // Rough token estimate: Anthropic tokenizers average ~3.5–4 chars/token for
28
+ // English prose. We use 4 because it's the standard heuristic and we only need
29
+ // an order-of-magnitude signal to monitor for context bloat. Empty string → 0.
30
+ export function estimateTokens(text) {
31
+ if (text.length === 0)
32
+ return 0;
33
+ return Math.ceil(text.length / 4);
34
+ }
35
+ export function buildSiblingContextBlock(siblings) {
36
+ if (siblings.length === 0)
37
+ return '';
38
+ const header = [
39
+ '## Sibling work in this drain session',
40
+ '',
41
+ 'The following branches were just created by this same drain run and are awaiting review. They are NOT yet on `main`. If your work overlaps with any of them:',
42
+ '',
43
+ '- Prefer importing their exported symbols over re-implementing.',
44
+ '- If you import, your PR will be reviewed as a stack with the sibling.',
45
+ '- If you have a strong reason to differ, do so and explain in the commit.',
46
+ '',
47
+ 'Siblings:',
48
+ '',
49
+ ].join('\n');
50
+ const siblingLines = siblings
51
+ .map((s) => [
52
+ `- \`${s.branch}\` (issue #${s.issue}):`,
53
+ s.changedFiles.length > 0 ? ` - Changed: ${s.changedFiles.join(', ')}` : null,
54
+ s.newExports.length > 0 ? ` - New exports: ${s.newExports.join(', ')}` : null,
55
+ ]
56
+ .filter((line) => line !== null)
57
+ .join('\n'))
58
+ .join('\n');
59
+ return `${header}\n${siblingLines}`;
60
+ }
61
+ //# sourceMappingURL=sibling-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sibling-context.js","sourceRoot":"","sources":["../../src/orchestrator/sibling-context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ export declare const SPLITS_FILE_RELATIVE_PATH = ".sandcastle-drain/splits.json";
2
+ export declare const OVERSIZED_LABEL = "oversized";
3
+ export declare const MAX_SPLITS = 10;
4
+ export declare const MAX_TITLE_LENGTH = 256;
5
+ export interface Split {
6
+ title: string;
7
+ body: string;
8
+ }
9
+ export interface CreatedSplit {
10
+ number: number;
11
+ url: string;
12
+ title: string;
13
+ }
14
+ export type SplitsParseResult = {
15
+ ok: true;
16
+ value: Split[];
17
+ } | {
18
+ ok: false;
19
+ reason: string;
20
+ };
21
+ /**
22
+ * Validates the raw JSON read from `.sandcastle-drain/splits.json`. The shape is
23
+ * intentionally tiny: an array of `{ title, body }`. Body is markdown the
24
+ * implementer wrote — we never massage it on the way through.
25
+ */
26
+ export declare function parseSplitsFile(rawJson: string): SplitsParseResult;
27
+ export declare function splitsFilePath(worktreePath: string): string;
28
+ /**
29
+ * Reads + parses the splits file from a worktree. Returns `undefined` when
30
+ * the file isn't present — the common case, since most implementers won't
31
+ * split. File-read failures other than ENOENT surface as parse errors so the
32
+ * wrapper can post the same error-comment path.
33
+ */
34
+ export declare function readSplitsFile(worktreePath: string): Promise<SplitsParseResult | undefined>;
35
+ /**
36
+ * Renders the comment left on the parent issue when splits land successfully.
37
+ * The reader gets a numbered checklist of the follow-ups so they can spot at a
38
+ * glance how the work was decomposed, plus a note explaining the label.
39
+ */
40
+ export declare function buildOriginalIssueSplitComment(args: {
41
+ parentIssue: number;
42
+ splits: readonly CreatedSplit[];
43
+ }): string;
44
+ /**
45
+ * Renders the error comment when the splits file is malformed. We still want
46
+ * the human to know the implementer tried to split — silent dropping would
47
+ * make the next drain look like the implementer just bailed out.
48
+ */
49
+ export declare function buildSplitErrorComment(args: {
50
+ reason: string;
51
+ }): string;
52
+ /**
53
+ * Renders the message the wrapper logs to its own stdout when the splits
54
+ * flow fires. Not posted to GitHub — this is just for the drain operator.
55
+ */
56
+ export declare function formatSplitsLogLine(args: {
57
+ parentIssue: number;
58
+ splits: readonly CreatedSplit[];
59
+ }): string;
60
+ //# sourceMappingURL=splits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splits.d.ts","sourceRoot":"","sources":["../../src/orchestrator/splits.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,yBAAyB,kCAAkC,CAAC;AACzE,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmB7F;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CA6BlE;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAajG;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CACjC,GAAG,MAAM,CAkBT;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAUvE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CACjC,GAAG,MAAM,CAGT"}