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,149 @@
1
+ /**
2
+ * Split protocol — let an implementer hand the wrapper a list of new issues.
3
+ *
4
+ * When an implementer realises mid-run that the issue won't fit under the
5
+ * 150k context ceiling, the right move is to commit what does fit, write the
6
+ * remaining acceptance criteria as a list of follow-up issues into
7
+ * `.sandcastle-drain/splits.json` in the worktree, then emit `<promise>COMPLETE</promise>`.
8
+ *
9
+ * The wrapper picks up the file after the run, files each entry as a new
10
+ * `sandcastle` + `priority` issue (so it jumps the queue on the next refetch,
11
+ * matching the rejection-loop precedent), comments on the original linking
12
+ * the new issues, and applies the `oversized` label so the audit trail is
13
+ * unambiguous.
14
+ *
15
+ * This module owns the pure pieces: locating + reading the splits file,
16
+ * validating the shape, and rendering the comments. The actual `gh issue
17
+ * create` and label/comment plumbing lives in `main.ts` next to the
18
+ * analogous rejection-loop calls.
19
+ */
20
+ import { existsSync } from 'node:fs';
21
+ import { readFile } from 'node:fs/promises';
22
+ import { join } from 'node:path';
23
+ export const SPLITS_FILE_RELATIVE_PATH = '.sandcastle-drain/splits.json';
24
+ export const OVERSIZED_LABEL = 'oversized';
25
+ export const MAX_SPLITS = 10;
26
+ export const MAX_TITLE_LENGTH = 256;
27
+ function isString(v) {
28
+ return typeof v === 'string';
29
+ }
30
+ function parseSplit(raw, index) {
31
+ if (raw === null || typeof raw !== 'object')
32
+ return `splits[${index}] is not an object`;
33
+ const s = raw;
34
+ if (!isString(s.title))
35
+ return `splits[${index}].title must be a string`;
36
+ if (s.title.length === 0)
37
+ return `splits[${index}].title must not be empty`;
38
+ if (s.title.length > MAX_TITLE_LENGTH) {
39
+ return `splits[${index}].title must be <= ${MAX_TITLE_LENGTH} chars (got ${s.title.length})`;
40
+ }
41
+ if (!isString(s.body))
42
+ return `splits[${index}].body must be a string`;
43
+ if (s.body.length === 0)
44
+ return `splits[${index}].body must not be empty`;
45
+ return { title: s.title, body: s.body };
46
+ }
47
+ /**
48
+ * Validates the raw JSON read from `.sandcastle-drain/splits.json`. The shape is
49
+ * intentionally tiny: an array of `{ title, body }`. Body is markdown the
50
+ * implementer wrote — we never massage it on the way through.
51
+ */
52
+ export function parseSplitsFile(rawJson) {
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(rawJson);
56
+ }
57
+ catch (err) {
58
+ return {
59
+ ok: false,
60
+ reason: `JSON.parse failed: ${err instanceof Error ? err.message : String(err)}`,
61
+ };
62
+ }
63
+ if (!Array.isArray(parsed)) {
64
+ return { ok: false, reason: 'splits file must contain a top-level JSON array' };
65
+ }
66
+ if (parsed.length === 0) {
67
+ return { ok: false, reason: 'splits array must contain at least one entry' };
68
+ }
69
+ if (parsed.length > MAX_SPLITS) {
70
+ return {
71
+ ok: false,
72
+ reason: `splits array must contain at most ${MAX_SPLITS} entries (got ${parsed.length})`,
73
+ };
74
+ }
75
+ const splits = [];
76
+ for (let i = 0; i < parsed.length; i++) {
77
+ const result = parseSplit(parsed[i], i);
78
+ if (typeof result === 'string')
79
+ return { ok: false, reason: result };
80
+ splits.push(result);
81
+ }
82
+ return { ok: true, value: splits };
83
+ }
84
+ export function splitsFilePath(worktreePath) {
85
+ return join(worktreePath, SPLITS_FILE_RELATIVE_PATH);
86
+ }
87
+ /**
88
+ * Reads + parses the splits file from a worktree. Returns `undefined` when
89
+ * the file isn't present — the common case, since most implementers won't
90
+ * split. File-read failures other than ENOENT surface as parse errors so the
91
+ * wrapper can post the same error-comment path.
92
+ */
93
+ export async function readSplitsFile(worktreePath) {
94
+ const path = splitsFilePath(worktreePath);
95
+ if (!existsSync(path))
96
+ return undefined;
97
+ let raw;
98
+ try {
99
+ raw = await readFile(path, 'utf8');
100
+ }
101
+ catch (err) {
102
+ return {
103
+ ok: false,
104
+ reason: `failed to read ${SPLITS_FILE_RELATIVE_PATH}: ${err instanceof Error ? err.message : String(err)}`,
105
+ };
106
+ }
107
+ return parseSplitsFile(raw);
108
+ }
109
+ /**
110
+ * Renders the comment left on the parent issue when splits land successfully.
111
+ * The reader gets a numbered checklist of the follow-ups so they can spot at a
112
+ * glance how the work was decomposed, plus a note explaining the label.
113
+ */
114
+ export function buildOriginalIssueSplitComment(args) {
115
+ const lines = [];
116
+ lines.push(`**sandcastle-drain implementer split this issue into ${args.splits.length} follow-up${args.splits.length === 1 ? '' : 's'}.**`);
117
+ lines.push('');
118
+ lines.push(`The implementer wrote \`.sandcastle-drain/splits.json\` during the run, signalling that the remaining acceptance criteria could not fit under the 150k context ceiling. Each follow-up has been filed with the \`sandcastle\` + \`priority\` labels and will run on the next drain.`);
119
+ lines.push('');
120
+ for (const split of args.splits) {
121
+ lines.push(`- #${split.number} — ${split.title}`);
122
+ }
123
+ lines.push('');
124
+ lines.push(`This issue has been labelled \`${OVERSIZED_LABEL}\`. The commits made during this run still flow through the normal review path — splitting does not throw away the work the implementer did finish.`);
125
+ return lines.join('\n');
126
+ }
127
+ /**
128
+ * Renders the error comment when the splits file is malformed. We still want
129
+ * the human to know the implementer tried to split — silent dropping would
130
+ * make the next drain look like the implementer just bailed out.
131
+ */
132
+ export function buildSplitErrorComment(args) {
133
+ const lines = [];
134
+ lines.push(`**sandcastle-drain implementer wrote \`.sandcastle-drain/splits.json\` but it was malformed.**`);
135
+ lines.push('');
136
+ lines.push(`Parse error: \`${args.reason}\``);
137
+ lines.push('');
138
+ lines.push(`No follow-up issues were filed. The implementer's commits (if any) still flow through the normal review path; re-file the splits by hand or apply \`retry\` after fixing the prompt.`);
139
+ return lines.join('\n');
140
+ }
141
+ /**
142
+ * Renders the message the wrapper logs to its own stdout when the splits
143
+ * flow fires. Not posted to GitHub — this is just for the drain operator.
144
+ */
145
+ export function formatSplitsLogLine(args) {
146
+ const numbers = args.splits.map((s) => `#${s.number}`).join(', ');
147
+ return `[wrapper] split #${args.parentIssue} into ${args.splits.length} follow-up(s): ${numbers}`;
148
+ }
149
+ //# sourceMappingURL=splits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splits.js","sourceRoot":"","sources":["../../src/orchestrator/splits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type RunStatus = 'completed' | 'partial-work' | 'ok (windows-teardown)' | 'bailed-out' | 'failed (rate limit)' | 'failed (timeout)' | 'failed (unknown)';
2
+ export declare function containsRateLimit(text: string): boolean;
3
+ export declare function isRateLimitError(err: unknown): boolean;
4
+ export declare function determineRunStatus(args: {
5
+ commits: {
6
+ sha: string;
7
+ }[];
8
+ completionSignal: string | undefined;
9
+ runError: unknown;
10
+ stdout: string;
11
+ windowsTeardownThrew?: boolean;
12
+ }): RunStatus;
13
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/orchestrator/status.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,WAAW,GACX,cAAc,GACd,uBAAuB,GACvB,YAAY,GACZ,qBAAqB,GACrB,kBAAkB,GAClB,kBAAkB,CAAC;AAIvB,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGtD;AAYD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GAAG,SAAS,CAuBZ"}
@@ -0,0 +1,43 @@
1
+ const RATE_LIMIT_MARKERS = ['rate limit', 'usage limit', 'Please try again'];
2
+ export function containsRateLimit(text) {
3
+ const lower = text.toLowerCase();
4
+ return RATE_LIMIT_MARKERS.some((m) => lower.includes(m.toLowerCase()));
5
+ }
6
+ export function isRateLimitError(err) {
7
+ const text = err instanceof Error ? err.message : String(err);
8
+ return containsRateLimit(text);
9
+ }
10
+ function isAbortError(err) {
11
+ if (err instanceof Error) {
12
+ const name = err.name;
13
+ if (name === 'AbortError' || name === 'TimeoutError')
14
+ return true;
15
+ const msg = err.message.toLowerCase();
16
+ if (msg.includes('abort') || msg.includes('timeout') || msg.includes('idle'))
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ export function determineRunStatus(args) {
22
+ const { commits, completionSignal, runError, stdout, windowsTeardownThrew = false } = args;
23
+ if (commits.length > 0) {
24
+ if (windowsTeardownThrew && runError !== undefined)
25
+ return 'ok (windows-teardown)';
26
+ return completionSignal !== undefined ? 'completed' : 'partial-work';
27
+ }
28
+ if (completionSignal !== undefined) {
29
+ return 'bailed-out';
30
+ }
31
+ if (runError) {
32
+ if (isRateLimitError(runError))
33
+ return 'failed (rate limit)';
34
+ if (isAbortError(runError))
35
+ return 'failed (timeout)';
36
+ return 'failed (unknown)';
37
+ }
38
+ if (containsRateLimit(stdout)) {
39
+ return 'failed (rate limit)';
40
+ }
41
+ return 'failed (unknown)';
42
+ }
43
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/orchestrator/status.ts"],"names":[],"mappings":"AASA,MAAM,kBAAkB,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,kBAAkB,CAAC,CAAC;AAE7E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,MAAM,IAAI,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC9D,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,YAAY,CAAC,GAAY;IAChC,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACtB,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,cAAc;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACtC,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;IAC5F,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAMlC;IACC,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC;IAE3F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,IAAI,oBAAoB,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,uBAAuB,CAAC;QACnF,OAAO,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC;IACvE,CAAC;IAED,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,gBAAgB,CAAC,QAAQ,CAAC;YAAE,OAAO,qBAAqB,CAAC;QAC7D,IAAI,YAAY,CAAC,QAAQ,CAAC;YAAE,OAAO,kBAAkB,CAAC;QACtD,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,IAAI,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IAED,OAAO,kBAAkB,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { RunStatus } from './status.js';
2
+ export interface RunSummary {
3
+ issue: number;
4
+ status: RunStatus | 'skipped (existing branch)' | 'skipped (rate-limited)' | `skipped (blocked by #${number})`;
5
+ branch?: string;
6
+ commitCount: number;
7
+ ciOk?: boolean;
8
+ autoMerged?: boolean;
9
+ rejected?: boolean;
10
+ split?: {
11
+ count: number;
12
+ followUpNumbers: readonly number[];
13
+ };
14
+ attempt?: number;
15
+ }
16
+ export interface SummaryCounts {
17
+ attempted: number;
18
+ completed: number;
19
+ partialWork: number;
20
+ windowsTeardown: number;
21
+ bailedOut: number;
22
+ failed: number;
23
+ ciFailed: number;
24
+ needsReview: number;
25
+ needsInfo: number;
26
+ autoMerged: number;
27
+ rejected: number;
28
+ split: number;
29
+ skipped: number;
30
+ }
31
+ export declare function computeCounts(summaries: readonly RunSummary[]): SummaryCounts;
32
+ export declare function formatSummary(summaries: readonly RunSummary[]): string;
33
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/orchestrator/summary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EACF,SAAS,GACT,2BAA2B,GAC3B,wBAAwB,GACxB,wBAAwB,MAAM,GAAG,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IAGpB,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,UAAU,CAAC,EAAE,OAAO,CAAC;IAIrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAMnB,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;IAI9D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA6BD,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,UAAU,EAAE,GAAG,aAAa,CAgB7E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CA4BtE"}
@@ -0,0 +1,59 @@
1
+ const hasReviewStatus = (s) => s.status === 'completed' || s.status === 'partial-work' || s.status === 'ok (windows-teardown)';
2
+ const isAutoMerged = (s) => s.autoMerged === true;
3
+ const isRejected = (s) => s.rejected === true;
4
+ const isSplit = (s) => s.split !== undefined && s.split.count > 0;
5
+ function splitSuffix(s) {
6
+ if (!s.split || s.split.count === 0)
7
+ return '';
8
+ const plural = s.split.count === 1 ? '' : 's';
9
+ return ` [split: ${s.split.count} follow-up${plural}]`;
10
+ }
11
+ const isReview = (s) => hasReviewStatus(s) && s.ciOk !== false && !isAutoMerged(s) && !isRejected(s);
12
+ const isFailed = (s) => typeof s.status === 'string' && s.status.startsWith('failed');
13
+ const isInfo = (s) => s.status === 'bailed-out' || isFailed(s) || (hasReviewStatus(s) && s.ciOk === false);
14
+ const isSkipped = (s) => typeof s.status === 'string' && s.status.startsWith('skipped');
15
+ export function computeCounts(summaries) {
16
+ return {
17
+ attempted: summaries.length,
18
+ completed: summaries.filter((s) => s.status === 'completed').length,
19
+ partialWork: summaries.filter((s) => s.status === 'partial-work').length,
20
+ windowsTeardown: summaries.filter((s) => s.status === 'ok (windows-teardown)').length,
21
+ bailedOut: summaries.filter((s) => s.status === 'bailed-out').length,
22
+ failed: summaries.filter(isFailed).length,
23
+ ciFailed: summaries.filter((s) => hasReviewStatus(s) && s.ciOk === false).length,
24
+ needsReview: summaries.filter(isReview).length,
25
+ needsInfo: summaries.filter(isInfo).length,
26
+ autoMerged: summaries.filter(isAutoMerged).length,
27
+ rejected: summaries.filter(isRejected).length,
28
+ split: summaries.filter(isSplit).length,
29
+ skipped: summaries.filter(isSkipped).length,
30
+ };
31
+ }
32
+ export function formatSummary(summaries) {
33
+ const c = computeCounts(summaries);
34
+ const lines = [
35
+ '',
36
+ '[wrapper] === Drain summary ===',
37
+ ` attempted : ${c.attempted}`,
38
+ ` auto-merged : ${c.autoMerged}`,
39
+ ` rejected : ${c.rejected}`,
40
+ ` split : ${c.split}`,
41
+ ` needs-review: ${c.needsReview} (${c.completed} completed, ${c.partialWork} partial, ${c.windowsTeardown} windows-teardown)`,
42
+ ` needs-info : ${c.needsInfo} (${c.bailedOut} bailed-out, ${c.failed} failed, ${c.ciFailed} ci-failed)`,
43
+ ` skipped : ${c.skipped}`,
44
+ ` failed : ${c.failed}`,
45
+ '',
46
+ ];
47
+ for (const s of summaries) {
48
+ const branchPart = s.branch ? ` (${s.branch}, ${s.commitCount} commits)` : '';
49
+ const reviewHint = s.branch ? ` — review with: git diff main..${s.branch}` : '';
50
+ const ciSuffix = s.ciOk === false ? ' [CI FAILED]' : '';
51
+ const mergedSuffix = s.autoMerged ? ' [auto-merged]' : '';
52
+ const rejectedSuffix = s.rejected ? ' [rejected]' : '';
53
+ const split = splitSuffix(s);
54
+ const attemptSuffix = s.attempt && s.attempt > 1 ? ` (attempt ${s.attempt})` : '';
55
+ lines.push(` #${s.issue}: ${s.status}${ciSuffix}${mergedSuffix}${rejectedSuffix}${split}${attemptSuffix}${branchPart}${reviewHint}`);
56
+ }
57
+ return lines.join('\n');
58
+ }
59
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/orchestrator/summary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export declare class SweepError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface SweepBranchArgs {
5
+ issue: number;
6
+ }
7
+ export interface SweepBranchResult {
8
+ branch: string;
9
+ prUrl: string;
10
+ }
11
+ /**
12
+ * Pulls main, removes the per-issue worktree, prunes worktree metadata, and
13
+ * deletes the local branch. Throws `SweepError` if no MERGED PR exists for the
14
+ * branch — that guard is the only safety net keeping a sweep from discarding
15
+ * in-flight work.
16
+ */
17
+ export declare function sweepBranch(args: SweepBranchArgs): Promise<SweepBranchResult>;
18
+ //# sourceMappingURL=sweep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep.d.ts","sourceRoot":"","sources":["../../src/orchestrator/sweep.ts"],"names":[],"mappings":"AAiCA,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAqBD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0CnF"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Post-merge cleanup for an `agent/issue-N` slice: pulls main, removes the
3
+ * worktree directory (Windows-safe via the shared helper), prunes git's
4
+ * worktree metadata, and deletes the local branch.
5
+ *
6
+ * Invoked by `src/cli.ts` as `sandcastle-drain sweep <issue>`. Refuses to run unless
7
+ * a MERGED PR exists for the branch — sweep is post-merge cleanup, not a way
8
+ * to discard in-flight work. To discard a still-open branch intentionally,
9
+ * use `git worktree remove` and `git branch -D` directly. The drain
10
+ * orchestrator (`main.ts`) also calls `sweepBranch` inline after a successful
11
+ * auto-ship.
12
+ */
13
+ import { execa } from 'execa';
14
+ import { existsSync } from 'node:fs';
15
+ import { resolve } from 'node:path';
16
+ import { REPO_ROOT } from './prereqs.js';
17
+ import { removeWorktreeDir } from './worktree-cleanup.js';
18
+ async function run(cmd, args, opts = {}) {
19
+ const r = await execa(cmd, args, { cwd: opts.cwd ?? REPO_ROOT, reject: opts.reject ?? true });
20
+ return { exitCode: r.exitCode ?? 0, stdout: r.stdout, stderr: r.stderr };
21
+ }
22
+ export class SweepError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = 'SweepError';
26
+ }
27
+ }
28
+ async function findMergedPr(branch) {
29
+ const result = await run('gh', ['pr', 'list', '--head', branch, '--state', 'all', '--json', 'number,state,url'], { reject: false });
30
+ if (result.exitCode !== 0) {
31
+ throw new SweepError(`gh pr list failed: ${result.stderr}`);
32
+ }
33
+ const prs = JSON.parse(result.stdout || '[]');
34
+ return prs.find((p) => p.state === 'MERGED');
35
+ }
36
+ /**
37
+ * Pulls main, removes the per-issue worktree, prunes worktree metadata, and
38
+ * deletes the local branch. Throws `SweepError` if no MERGED PR exists for the
39
+ * branch — that guard is the only safety net keeping a sweep from discarding
40
+ * in-flight work.
41
+ */
42
+ export async function sweepBranch(args) {
43
+ const branch = `agent/issue-${args.issue}`;
44
+ const worktreePath = resolve(REPO_ROOT, '.sandcastle-drain', 'worktrees', `agent-issue-${args.issue}`);
45
+ console.log(`[sweep] Checking that PR for ${branch} is merged...`);
46
+ const merged = await findMergedPr(branch);
47
+ if (!merged) {
48
+ throw new SweepError(`No MERGED PR found for ${branch}. Sweep is post-merge cleanup only — run \`sandcastle-drain ship ${args.issue}\` first, or remove the worktree manually if you want to discard the branch without merging.`);
49
+ }
50
+ console.log(`[sweep] Found merged PR: ${merged.url}`);
51
+ // Pull main so the local main has the squash commit. (Without this, the
52
+ // `git branch -d` step below refuses with "not yet merged to HEAD".)
53
+ console.log(`[sweep] Pulling main...`);
54
+ await run('git', ['checkout', 'main']);
55
+ await run('git', ['pull', 'origin', 'main']);
56
+ if (existsSync(worktreePath)) {
57
+ console.log(`[sweep] Removing worktree...`);
58
+ await removeWorktreeDir(worktreePath);
59
+ }
60
+ else {
61
+ console.log(`[sweep] Worktree already gone — skipping.`);
62
+ }
63
+ await run('git', ['worktree', 'prune']);
64
+ // -D = force. -d would refuse because we squash-merge: the branch tip never
65
+ // becomes an ancestor of main, so git's "not fully merged" check fires even
66
+ // though the work IS on main under a different SHA. The PR-merge check above
67
+ // is the real safety net — if we got here, the work is upstream.
68
+ const branchCheck = await run('git', ['rev-parse', '--verify', branch], { reject: false });
69
+ if (branchCheck.exitCode === 0) {
70
+ console.log(`[sweep] Deleting local branch ${branch}...`);
71
+ await run('git', ['branch', '-D', branch]);
72
+ }
73
+ else {
74
+ console.log(`[sweep] Local branch ${branch} already gone — skipping.`);
75
+ }
76
+ console.log(`[sweep] Done. #${args.issue} is fully cleaned up.`);
77
+ return { branch, prUrl: merged.url };
78
+ }
79
+ //# sourceMappingURL=sweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep.js","sourceRoot":"","sources":["../../src/orchestrator/sweep.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare function recoverCommitsFromBranch(branch: string, cwd: string): Promise<{
2
+ sha: string;
3
+ }[]>;
4
+ export declare function tryRecoverCommits(args: {
5
+ result: unknown;
6
+ runError: unknown;
7
+ branch: string;
8
+ cwd: string;
9
+ }): Promise<{
10
+ sha: string;
11
+ }[]>;
12
+ //# sourceMappingURL=teardown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teardown.d.ts","sourceRoot":"","sources":["../../src/orchestrator/teardown.ts"],"names":[],"mappings":"AAmBA,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAY5B;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAS7B"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Windows teardown handling for sandcastle worktrees.
3
+ *
4
+ * On Windows, sandcastle's internal `WorktreeManager.remove()` throws after a
5
+ * successful agent run because pnpm's `node_modules/.pnpm/` symlink farm
6
+ * defeats git's recursive teardown ("Function not implemented"). That throw is
7
+ * the expected exit path for any Windows drain that runs `pnpm install`, not
8
+ * an error — the agent's commits are already on the branch. We read them back
9
+ * here and let the wrapper label the run as `ok (windows-teardown)`.
10
+ *
11
+ * See src/content/agent-docs/sandcastle-windows-cleanup.md for background.
12
+ */
13
+ import { execa } from 'execa';
14
+ async function branchExists(branch, cwd) {
15
+ const result = await execa('git', ['rev-parse', '--verify', branch], { cwd, reject: false });
16
+ return result.exitCode === 0;
17
+ }
18
+ export async function recoverCommitsFromBranch(branch, cwd) {
19
+ if (!(await branchExists(branch, cwd)))
20
+ return [];
21
+ const result = await execa('git', ['log', 'main..' + branch, '--format=%H'], {
22
+ cwd,
23
+ reject: false,
24
+ });
25
+ if (result.exitCode !== 0)
26
+ return [];
27
+ return result.stdout
28
+ .split(/\r?\n/)
29
+ .map((s) => s.trim())
30
+ .filter((s) => s.length > 0)
31
+ .map((sha) => ({ sha }));
32
+ }
33
+ export async function tryRecoverCommits(args) {
34
+ if (args.result !== undefined || args.runError === undefined)
35
+ return [];
36
+ const recovered = await recoverCommitsFromBranch(args.branch, args.cwd);
37
+ if (recovered.length > 0) {
38
+ console.log(`[wrapper] read ${recovered.length} commit(s) from ${args.branch} after sandcastle.run() threw on Windows teardown`);
39
+ }
40
+ return recovered;
41
+ }
42
+ //# sourceMappingURL=teardown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teardown.js","sourceRoot":"","sources":["../../src/orchestrator/teardown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,KAAK,UAAU,YAAY,CAAC,MAAc,EAAE,GAAW;IACrD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7F,OAAO,MAAM,CAAC,QAAQ,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAc,EACd,GAAW;IAEX,IAAI,CAAC,CAAC,MAAM,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,EAAE,aAAa,CAAC,EAAE;QAC3E,GAAG;QACH,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM;SACjB,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAKvC;IACC,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACxE,MAAM,SAAS,GAAG,MAAM,wBAAwB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CACT,kBAAkB,SAAS,CAAC,MAAM,mBAAmB,IAAI,CAAC,MAAM,mDAAmD,CACpH,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function removeWorktreeDir(worktreePath: string): Promise<void>;
2
+ //# sourceMappingURL=worktree-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-cleanup.d.ts","sourceRoot":"","sources":["../../src/orchestrator/worktree-cleanup.ts"],"names":[],"mappings":"AAgBA,wBAAsB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B3E"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cross-platform recursive removal of a sandcastle worktree directory.
3
+ *
4
+ * Windows-specific quirk: pnpm's `node_modules/.pnpm/<long-hash>/...` symlink
5
+ * farm defeats standard recursive deletion (Node's `fs.rm`, PowerShell
6
+ * `Remove-Item`, `rmdir /s`, even git's own worktree cleanup — git surfaces
7
+ * `Function not implemented` from the kernel). `robocopy /MIR` against an
8
+ * empty source mirror-deletes everything in the target using long-path-aware
9
+ * Win32 APIs that handle the symlink chain.
10
+ */
11
+ import { execa } from 'execa';
12
+ import { existsSync } from 'node:fs';
13
+ import { mkdir, rm } from 'node:fs/promises';
14
+ import { tmpdir } from 'node:os';
15
+ import { basename, join } from 'node:path';
16
+ export async function removeWorktreeDir(worktreePath) {
17
+ if (!existsSync(worktreePath))
18
+ return;
19
+ if (process.platform !== 'win32') {
20
+ await rm(worktreePath, { recursive: true, force: true });
21
+ return;
22
+ }
23
+ // PID + basename keeps concurrent calls from colliding on the empty source.
24
+ const emptyDir = join(tmpdir(), `sandcastle-empty-${basename(worktreePath)}-${process.pid}`);
25
+ await mkdir(emptyDir, { recursive: true });
26
+ try {
27
+ const robo = await execa('robocopy', [emptyDir, worktreePath, '/MIR', '/NFL', '/NDL', '/NJH', '/NJS'], { reject: false });
28
+ // robocopy exit codes: 0-7 are success-ish, 8+ is an actual failure.
29
+ if ((robo.exitCode ?? 0) >= 8) {
30
+ throw new Error(`robocopy failed with code ${robo.exitCode} on ${worktreePath}.\n${robo.stderr}`);
31
+ }
32
+ }
33
+ finally {
34
+ await rm(emptyDir, { recursive: true, force: true });
35
+ }
36
+ // Worktree dir is now empty — remove the dir itself.
37
+ await rm(worktreePath, { recursive: true, force: true });
38
+ }
39
+ //# sourceMappingURL=worktree-cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../src/orchestrator/worktree-cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,YAAoB;IAC1D,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO;IAEtC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,EAAE,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,OAAO;IACT,CAAC;IAED,4EAA4E;IAC5E,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,oBAAoB,QAAQ,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7F,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,KAAK,CACtB,UAAU,EACV,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAChE,EAAE,MAAM,EAAE,KAAK,EAAE,CAClB,CAAC;QACF,qEAAqE;QACrE,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,6BAA6B,IAAI,CAAC,QAAQ,OAAO,YAAY,MAAM,IAAI,CAAC,MAAM,EAAE,CACjF,CAAC;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,qDAAqD;IACrD,MAAM,EAAE,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAC3D,CAAC"}