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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +139 -0
- package/dist/cli.js.map +1 -0
- package/dist/content/agent-docs/issue-tracker.md +22 -0
- package/dist/content/agent-docs/sandcastle-windows-cleanup.md +45 -0
- package/dist/content/agent-docs/triage-labels.md +101 -0
- package/dist/content/principles/README.md +39 -0
- package/dist/content/principles/architecture.md +124 -0
- package/dist/content/principles/claude-code-modes.md +47 -0
- package/dist/content/principles/clean-code.md +102 -0
- package/dist/content/principles/context-budget.md +81 -0
- package/dist/content/principles/cqrs.md +70 -0
- package/dist/content/principles/domain-modeling.md +62 -0
- package/dist/content/principles/frontend-organization.md +120 -0
- package/dist/content/principles/language-and-types.md +85 -0
- package/dist/content/principles/linting-and-tooling.md +122 -0
- package/dist/content/principles/personal-use-tradeoffs.md +55 -0
- package/dist/content/principles/testing.md +89 -0
- package/dist/orchestrator/blocked-by.d.ts +17 -0
- package/dist/orchestrator/blocked-by.d.ts.map +1 -0
- package/dist/orchestrator/blocked-by.js +48 -0
- package/dist/orchestrator/blocked-by.js.map +1 -0
- package/dist/orchestrator/ci-gate.d.ts +28 -0
- package/dist/orchestrator/ci-gate.d.ts.map +1 -0
- package/dist/orchestrator/ci-gate.js +198 -0
- package/dist/orchestrator/ci-gate.js.map +1 -0
- package/dist/orchestrator/main.d.ts +10 -0
- package/dist/orchestrator/main.d.ts.map +1 -0
- package/dist/orchestrator/main.js +883 -0
- package/dist/orchestrator/main.js.map +1 -0
- package/dist/orchestrator/prereqs.d.ts +30 -0
- package/dist/orchestrator/prereqs.d.ts.map +1 -0
- package/dist/orchestrator/prereqs.js +191 -0
- package/dist/orchestrator/prereqs.js.map +1 -0
- package/dist/orchestrator/rejection.d.ts +60 -0
- package/dist/orchestrator/rejection.d.ts.map +1 -0
- package/dist/orchestrator/rejection.js +187 -0
- package/dist/orchestrator/rejection.js.map +1 -0
- package/dist/orchestrator/reviewer.d.ts +75 -0
- package/dist/orchestrator/reviewer.d.ts.map +1 -0
- package/dist/orchestrator/reviewer.js +260 -0
- package/dist/orchestrator/reviewer.js.map +1 -0
- package/dist/orchestrator/ship.d.ts +19 -0
- package/dist/orchestrator/ship.d.ts.map +1 -0
- package/dist/orchestrator/ship.js +73 -0
- package/dist/orchestrator/ship.js.map +1 -0
- package/dist/orchestrator/sibling-context.d.ts +16 -0
- package/dist/orchestrator/sibling-context.d.ts.map +1 -0
- package/dist/orchestrator/sibling-context.js +61 -0
- package/dist/orchestrator/sibling-context.js.map +1 -0
- package/dist/orchestrator/splits.d.ts +60 -0
- package/dist/orchestrator/splits.d.ts.map +1 -0
- package/dist/orchestrator/splits.js +149 -0
- package/dist/orchestrator/splits.js.map +1 -0
- package/dist/orchestrator/status.d.ts +13 -0
- package/dist/orchestrator/status.d.ts.map +1 -0
- package/dist/orchestrator/status.js +43 -0
- package/dist/orchestrator/status.js.map +1 -0
- package/dist/orchestrator/summary.d.ts +33 -0
- package/dist/orchestrator/summary.d.ts.map +1 -0
- package/dist/orchestrator/summary.js +59 -0
- package/dist/orchestrator/summary.js.map +1 -0
- package/dist/orchestrator/sweep.d.ts +18 -0
- package/dist/orchestrator/sweep.d.ts.map +1 -0
- package/dist/orchestrator/sweep.js +79 -0
- package/dist/orchestrator/sweep.js.map +1 -0
- package/dist/orchestrator/teardown.d.ts +12 -0
- package/dist/orchestrator/teardown.d.ts.map +1 -0
- package/dist/orchestrator/teardown.js +42 -0
- package/dist/orchestrator/teardown.js.map +1 -0
- package/dist/orchestrator/worktree-cleanup.d.ts +2 -0
- package/dist/orchestrator/worktree-cleanup.d.ts.map +1 -0
- package/dist/orchestrator/worktree-cleanup.js +39 -0
- package/dist/orchestrator/worktree-cleanup.js.map +1 -0
- package/dist/prompts/implementer.md.tpl +85 -0
- package/dist/prompts/reviewer.md.tpl +118 -0
- package/dist/render-prompt.d.ts +22 -0
- package/dist/render-prompt.d.ts.map +1 -0
- package/dist/render-prompt.js +64 -0
- package/dist/render-prompt.js.map +1 -0
- package/dist/stage.d.ts +43 -0
- package/dist/stage.d.ts.map +1 -0
- package/dist/stage.js +105 -0
- package/dist/stage.js.map +1 -0
- package/docker/Dockerfile +42 -0
- 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":"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"}
|
|
@@ -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"}
|