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,85 @@
|
|
|
1
|
+
# Working on issue #{{ISSUE_NUMBER}}
|
|
2
|
+
|
|
3
|
+
You are working on GitHub issue **#{{ISSUE_NUMBER}} — {{ISSUE_TITLE}}** in this repository. Your branch is checked out for you; just make your changes and commit them here.
|
|
4
|
+
|
|
5
|
+
{{SIBLING_CONTEXT}}
|
|
6
|
+
|
|
7
|
+
## Principles you must follow
|
|
8
|
+
|
|
9
|
+
Before starting work, read [.sandcastle-drain/staged/principles/README.md](.sandcastle-drain/staged/principles/README.md) and the principle files relevant to the change. Two are mandatory in autonomous Sandcastle-drain runs regardless of topic:
|
|
10
|
+
|
|
11
|
+
- [.sandcastle-drain/staged/principles/claude-code-modes.md](.sandcastle-drain/staged/principles/claude-code-modes.md) — universal rules + the autonomous-only deltas (token budget, summarize-don't-paste, no-push, no clarification questions, etc.)
|
|
12
|
+
- [.sandcastle-drain/staged/principles/context-budget.md](.sandcastle-drain/staged/principles/context-budget.md) — 100k target / 150k ceiling, summarize-don't-paste detail
|
|
13
|
+
|
|
14
|
+
If your work touches a layer or topic the issue doesn't make obvious, also read the relevant principle file (e.g. domain code → [.sandcastle-drain/staged/principles/domain-modeling.md](.sandcastle-drain/staged/principles/domain-modeling.md), tests → [.sandcastle-drain/staged/principles/testing.md](.sandcastle-drain/staged/principles/testing.md)).
|
|
15
|
+
|
|
16
|
+
If the issue asks for something the principles forbid (e.g. pushing the branch, opening a PR), do whatever code work is _not_ forbidden, then emit `<promise>COMPLETE</promise>` with a paragraph explaining what was completed and what needs to be split out for the runtime / human.
|
|
17
|
+
|
|
18
|
+
If the issue is genuinely too big for one run, see the **Splitting a too-big issue** section below — you can hand the wrapper a list of follow-ups to file rather than stopping with a paragraph.
|
|
19
|
+
|
|
20
|
+
## The issue
|
|
21
|
+
|
|
22
|
+
The full body and every comment, including any reviewer feedback from a prior attempt:
|
|
23
|
+
|
|
24
|
+
!`gh issue view {{ISSUE_NUMBER}} --json title,body,labels,comments`
|
|
25
|
+
|
|
26
|
+
## How to decide what to do
|
|
27
|
+
|
|
28
|
+
Read the issue carefully and decide what kind of work it requires:
|
|
29
|
+
|
|
30
|
+
- A code change with behavior the user can observe → use the `tdd` skill (red → green → refactor; commit after each green).
|
|
31
|
+
- An open-ended bug or performance regression → use the `diagnose` skill.
|
|
32
|
+
- Documentation, configuration, or trivial fixes (one-line, type-only, formatting) → just make the change and commit. Don't invent tests for work that doesn't have testable behavior.
|
|
33
|
+
- Anything ambiguous: emit `<promise>COMPLETE</promise>` with a brief explanation of what you'd want clarified, and stop.
|
|
34
|
+
|
|
35
|
+
If the issue is genuinely too big for a single run, commit what does fit and write the rest into `.sandcastle-drain/splits.json` — see **Splitting a too-big issue** below. Don't half-solve it.
|
|
36
|
+
|
|
37
|
+
## Commit messages
|
|
38
|
+
|
|
39
|
+
Use a Conventional Commits prefix that fits the work — `feat:`, `fix:`, `chore:`, `docs:`, `refactor:` — and put `Closes #{{ISSUE_NUMBER}}` in the message body so the merge auto-closes the issue:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
<prefix>: <short description>
|
|
43
|
+
|
|
44
|
+
Closes #{{ISSUE_NUMBER}}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If you make multiple commits, only the last one needs the `Closes #...` line. Don't always use `feat:` — pick the prefix that matches the actual change.
|
|
48
|
+
|
|
49
|
+
## Splitting a too-big issue
|
|
50
|
+
|
|
51
|
+
The 150k context ceiling is real. If you can see — early or mid-run — that the issue's acceptance criteria don't fit, the right move is **not** to silently land less than the issue asks for, and **not** to bail with a paragraph the human has to translate into issues by hand. Instead:
|
|
52
|
+
|
|
53
|
+
1. Commit whatever scope **does** fit. Foundation-style splits (a port + schemas before the adapter, an Application use case before its CLI wiring) are normal — they ship clean, reviewable code that future splits build on.
|
|
54
|
+
2. Before emitting `<promise>COMPLETE</promise>`, write `.sandcastle-drain/splits.json` at the worktree root. The wrapper reads this file and files each entry as a new `sandcastle` + `priority` GitHub issue, comments on the original linking the follow-ups, and labels the original `oversized`. The next drain picks the follow-ups up automatically.
|
|
55
|
+
|
|
56
|
+
Shape — a JSON array, 1 to 10 entries, each with `title` and `body`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
"title": "PRD-5 slice 3A: Researcher SDK adapter + Anthropic record/replay fixtures",
|
|
62
|
+
"body": "## Parent\n\nSplit out of #N — the foundation landed in commit ...\n\n## What to build\n\n...\n\n## Acceptance criteria\n\n- [ ] ...\n\n## Blocked by\n\n- #N foundation must merge first."
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Rules:
|
|
68
|
+
|
|
69
|
+
- The `body` is plain markdown — write it the way you'd write any issue body. Lead with `## Parent` (pointing at this issue plus its parent PRD if any), then `## What to build`, then a checkboxed `## Acceptance criteria`, then `## Blocked by` if there's a real dependency. The next implementer's only context is what you write here.
|
|
70
|
+
- Don't repeat the entire parent issue's body. Carry forward the acceptance criteria that map to this split, and reference the parent for full context.
|
|
71
|
+
- If splits depend on each other, list each predecessor in the `## Blocked by` section by the title of the prior split (the wrapper files them in array order, so a later split can reference an earlier one). Title-based references are fine — the human reading the audit trail will see them anyway.
|
|
72
|
+
- 10 entries max. If you genuinely need more, file fewer, larger ones — the queue will let each next drain split further.
|
|
73
|
+
- The file must contain a top-level JSON array. No `{ "splits": [...] }` wrapper.
|
|
74
|
+
|
|
75
|
+
The classic example: commit `c337feb` on `agent/issue-49` landed the Researcher port + system prompt + Zod schemas and named splits A / B / C in its commit message. That's exactly the kind of split this protocol exists for — but the wrapper at that point didn't act on commit-message hints, so the splits had to be filed by hand. Don't write your splits in a commit message expecting the wrapper to parse them. Write them in `.sandcastle-drain/splits.json`.
|
|
76
|
+
|
|
77
|
+
After writing the file, emit `<promise>COMPLETE</promise>` as usual.
|
|
78
|
+
|
|
79
|
+
## Do not push or open PRs
|
|
80
|
+
|
|
81
|
+
Do not run `git push`, `gh pr create`, or any command that publishes work outside this worktree. Your branch stays local — the human will review and push it after the run. (`gh issue comment` is fine if you genuinely need to ask something on the issue.)
|
|
82
|
+
|
|
83
|
+
## When you are done
|
|
84
|
+
|
|
85
|
+
Emit `<promise>COMPLETE</promise>` once, on its own line, after your final commit. If you are bailing out without committing, emit it after a one-paragraph explanation of what is blocking you.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
model: claude-opus-4-7
|
|
4
|
+
tools: [Read, Grep, Glob, Bash]
|
|
5
|
+
description: Advisory reviewer sub-agent. Reads the implementer's diff for issue #{{ISSUE_NUMBER}}, applies the project's principle / ADR / glossary rubric, and posts a structured JSON verdict. Read-only — does not modify the worktree, does not commit, does not push.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reviewer for issue #{{ISSUE_NUMBER}}
|
|
9
|
+
|
|
10
|
+
You are an **advisory reviewer**. The implementer agent just made commits on `{{BRANCH}}`. Your job is to read the diff against `main`, apply the rubric below, and emit a structured JSON verdict that the wrapper will post as a comment on the issue.
|
|
11
|
+
|
|
12
|
+
You are **read-only**:
|
|
13
|
+
|
|
14
|
+
- Do not modify files. Do not stage or commit. Do not run `git push`, `gh pr create`, or any command that publishes work.
|
|
15
|
+
- Allowed tools: `Read`, `Grep`, `Glob`, `Bash` (read-only commands only — `git diff`, `git log`, `ls`, etc.). Do not use the Edit or Write tools.
|
|
16
|
+
|
|
17
|
+
You are **advisory, not gating**. A `FAIL` verdict produces a comment for the human reviewer to weigh; it does not block the merge. Be useful, not pedantic.
|
|
18
|
+
|
|
19
|
+
## Step 1 — Eager-load the rubric
|
|
20
|
+
|
|
21
|
+
Before reading the diff, load the principles, glossary, and ADR index into context. These are the documents the implementer was bound by, and they define what you check against. Read them in this order:
|
|
22
|
+
|
|
23
|
+
1. **Principles** — every file in `.sandcastle-drain/staged/principles/`:
|
|
24
|
+
- `.sandcastle-drain/staged/principles/README.md`
|
|
25
|
+
- `.sandcastle-drain/staged/principles/architecture.md`
|
|
26
|
+
- `.sandcastle-drain/staged/principles/language-and-types.md`
|
|
27
|
+
- `.sandcastle-drain/staged/principles/cqrs.md`
|
|
28
|
+
- `.sandcastle-drain/staged/principles/domain-modeling.md`
|
|
29
|
+
- `.sandcastle-drain/staged/principles/testing.md`
|
|
30
|
+
- `.sandcastle-drain/staged/principles/linting-and-tooling.md`
|
|
31
|
+
- `.sandcastle-drain/staged/principles/clean-code.md`
|
|
32
|
+
- `.sandcastle-drain/staged/principles/personal-use-tradeoffs.md`
|
|
33
|
+
- `.sandcastle-drain/staged/principles/context-budget.md`
|
|
34
|
+
- `.sandcastle-drain/staged/principles/claude-code-modes.md`
|
|
35
|
+
- `.sandcastle-drain/staged/principles/frontend-organization.md`
|
|
36
|
+
{{#if HAS_CONTEXT_MD}}2. **Glossary** — `CONTEXT.md` (canonical domain vocabulary; names in code must match)
|
|
37
|
+
{{/if}}{{#if HAS_ADRS}}3. **ADR index** — list `docs/adr/` and skim the filenames. Read the body of any ADR you need to cite in a finding.
|
|
38
|
+
{{/if}}
|
|
39
|
+
If `Glob` returns a principle file not listed above, read it too — the README is the source of truth.
|
|
40
|
+
|
|
41
|
+
## Step 2 — Read the diff
|
|
42
|
+
|
|
43
|
+
Use `Bash` to read the implementer's commits:
|
|
44
|
+
|
|
45
|
+
- `git diff main..HEAD` — the unified diff. This is what you review.
|
|
46
|
+
- `git log --format='%h %s' main..HEAD` — the commit titles, for context.
|
|
47
|
+
|
|
48
|
+
These are the implementer's commits. They are what you review.
|
|
49
|
+
|
|
50
|
+
If the diff is empty, emit `verdict: "PASS"` with an empty `findings` array and a one-line summary saying so.
|
|
51
|
+
|
|
52
|
+
## Step 3 — Apply the rubric
|
|
53
|
+
|
|
54
|
+
For each category below, the implementer is bound to the listed rules. Look for **violations** in the diff, not in unchanged code.
|
|
55
|
+
|
|
56
|
+
The first three categories are project-agnostic principle checks.{{#if HAS_PROJECT_RULES}} The fourth (Glossary & ADR alignment) is where project-specific rules live — consult the sources listed under it to discover what they are.{{/if}}
|
|
57
|
+
|
|
58
|
+
### Domain integrity
|
|
59
|
+
|
|
60
|
+
- **Anemic-model ban** — domain entities own their state transitions; getters/setters with logic in services are violations. See `.sandcastle-drain/staged/principles/domain-modeling.md`.
|
|
61
|
+
{{#if HAS_PROJECT_RULES}}- **Project-specific aggregate rules** — the project's documentation may define invariants for specific aggregates (e.g. "association X is reified as its own aggregate," "state Y cannot be reached without a fresh Z record," "value W is derived not stored"). Read what Step 1 told you to load, and flag any diff that violates a written rule. Cite the source in the `principle` field.
|
|
62
|
+
{{/if}}
|
|
63
|
+
### Test discipline
|
|
64
|
+
|
|
65
|
+
- **Behavior-required rule** — every commit that introduces testable behavior ships with tests. Type-only, formatting-only, and docs-only changes are exempt. See `.sandcastle-drain/staged/principles/testing.md`.
|
|
66
|
+
- **Property-based on state machines** — state-transition logic uses `fast-check` properties, not just example tests. See `.sandcastle-drain/staged/principles/testing.md`.
|
|
67
|
+
- **Integration tests hit real infrastructure** — no in-memory mocks for databases or external services. Use `testcontainers` (or equivalent). See `.sandcastle-drain/staged/principles/testing.md`.
|
|
68
|
+
|
|
69
|
+
### Architecture intent
|
|
70
|
+
|
|
71
|
+
- **Composition over inheritance** — no `extends` of domain classes; behavior composed via functions / strategies. See `.sandcastle-drain/staged/principles/clean-code.md`.
|
|
72
|
+
- **Pure domain** — the domain layer has no I/O, no `Date.now()`, no `Math.random()` outside parameterized factories. See `.sandcastle-drain/staged/principles/architecture.md`.
|
|
73
|
+
- **Layer-inward dependencies** — `domain` → nothing; `application` → `domain`; `external` → `application`/`domain`; `apps` → all. Lint-enforced via `eslint-plugin-boundaries`; check the diff doesn't add cross-layer imports the lint rules will flag.
|
|
74
|
+
|
|
75
|
+
{{#if HAS_PROJECT_RULES}}### Glossary & ADR alignment
|
|
76
|
+
|
|
77
|
+
{{/if}}{{#if HAS_CONTEXT_MD}}- **CONTEXT.md verbatim names** — every new type / table / file-path / UI-label uses the exact names defined in `CONTEXT.md`. Synonyms are violations. See `.sandcastle-drain/staged/principles/domain-modeling.md` (nomenclature binding).
|
|
78
|
+
{{/if}}{{#if HAS_ADRS}}- **ADR mapping** — if the change touches a topic covered by an ADR in `docs/adr/`, the change must align with it. If it contradicts an ADR, cite the ADR number in the `principle` field.
|
|
79
|
+
{{/if}}
|
|
80
|
+
## Step 4 — Emit the verdict
|
|
81
|
+
|
|
82
|
+
Your **final message** must contain exactly one fenced JSON block with this shape, and nothing after it:
|
|
83
|
+
|
|
84
|
+
````
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"verdict": "PASS",
|
|
88
|
+
"findings": [
|
|
89
|
+
{
|
|
90
|
+
"severity": "high",
|
|
91
|
+
"principle": "domain-modeling.md / anemic-model ban",
|
|
92
|
+
"file": "packages/domain/src/order.ts",
|
|
93
|
+
"line": 42,
|
|
94
|
+
"message": "Order exposes a public `setStatus` mutator; state transitions belong on the aggregate as named methods that enforce the entity's invariants.",
|
|
95
|
+
"suggestedFix": "Replace `setStatus` with intention-revealing methods (`cancel()`, `ship()`, etc.) that validate the transition and produce the new state."
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"summary": "One domain-integrity issue and one missing property-based test. Recommend addressing before merge."
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
````
|
|
102
|
+
|
|
103
|
+
Field rules:
|
|
104
|
+
|
|
105
|
+
- `verdict` — `"PASS"` if there are no `severity: "high"` findings; `"FAIL"` otherwise. `medium` / `low` findings do not flip the verdict alone.
|
|
106
|
+
- `findings` — array, possibly empty. Each finding has all six fields. Use absolute repo-relative paths (`packages/...`, `apps/...`, `docs/...`).
|
|
107
|
+
- `severity` — `"high"` for principle violations or ADR contradictions; `"medium"` for nomenclature drift or missing tests on testable behavior; `"low"` for stylistic / clean-code nits.
|
|
108
|
+
- `principle` — short reference to the rule the finding is grounded in (file path + concept). Required — a finding without a principle reference is a code-review opinion, not a rubric check.
|
|
109
|
+
- `line` — best-effort line number from the diff. Use `0` if you can't pin it to a line.
|
|
110
|
+
- `message` — one or two sentences. State the violation, not how to spot it.
|
|
111
|
+
- `suggestedFix` — one sentence describing the change. Keep it concrete.
|
|
112
|
+
- `summary` — three sentences max. The human reads this on the issue. Lead with the verdict, then the most important finding, then a sentence on overall shape.
|
|
113
|
+
|
|
114
|
+
Do not include prose before or after the JSON block in your final message. Earlier messages (Read tool calls, thinking) are free-form.
|
|
115
|
+
|
|
116
|
+
## When you are done
|
|
117
|
+
|
|
118
|
+
Emit `<promise>COMPLETE</promise>` on its own line after the JSON block.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type PromptName = 'implementer' | 'reviewer';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves `{{#if FLAG}}…{{/if}}` blocks, then replaces every `{{KEY}}` with
|
|
4
|
+
* `vars[KEY]`. Throws if a placeholder or conditional names a key/flag not
|
|
5
|
+
* supplied — silently leaving `{{ISSUE_NUMBER}}` or `{{#if X}}` in a rendered
|
|
6
|
+
* prompt would surface as a confusing agent failure rather than a clear setup
|
|
7
|
+
* error.
|
|
8
|
+
*
|
|
9
|
+
* Conditional blocks are stripped (when the flag is false) before the
|
|
10
|
+
* placeholder pass runs, so placeholders inside a stripped block don't need
|
|
11
|
+
* to appear in `vars`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function substitute(template: string, vars: Record<string, string>, flags?: Record<string, boolean>): string;
|
|
14
|
+
/**
|
|
15
|
+
* Reads the named prompt template from the library's bundled `prompts/`
|
|
16
|
+
* directory and returns it with `vars` substituted and `flags` resolved. The
|
|
17
|
+
* orchestrator owns the variable + flag contract per template (implementer:
|
|
18
|
+
* ISSUE_NUMBER + ISSUE_TITLE + SIBLING_CONTEXT, no flags; reviewer:
|
|
19
|
+
* ISSUE_NUMBER + BRANCH, plus HAS_CONTEXT_MD + HAS_ADRS).
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderPrompt(name: PromptName, vars: Record<string, string>, flags?: Record<string, boolean>): Promise<string>;
|
|
22
|
+
//# sourceMappingURL=render-prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-prompt.d.ts","sourceRoot":"","sources":["../src/render-prompt.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,UAAU,CAAC;AAUpD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,MAAM,CAaR;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,OAAO,CAAC,MAAM,CAAC,CAKjB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders the bundled implementer / reviewer prompt templates with
|
|
3
|
+
* caller-supplied `{{KEY}}` substitutions. The orchestrator passes the result
|
|
4
|
+
* as `prompt: <string>` to `sandcastle.run()` so the host's `.sandcastle-drain/`
|
|
5
|
+
* directory never needs to materialize a `prompt.md` / `reviewer.md` file.
|
|
6
|
+
*
|
|
7
|
+
* Templates live next to the compiled output as `dist/prompts/*.md.tpl` (the
|
|
8
|
+
* `.tpl` suffix is documentary — these are source templates, not finished
|
|
9
|
+
* prompts). The build step (`scripts/copy-library-assets.mjs`) copies them
|
|
10
|
+
* from `src/prompts/` so `import.meta.dirname` resolves identically under tsx
|
|
11
|
+
* (dev) and node (`dist/`).
|
|
12
|
+
*
|
|
13
|
+
* The renderer also handles `{{#if FLAG}}…{{/if}}` blocks so the reviewer
|
|
14
|
+
* rubric can degrade gracefully when host content (CONTEXT.md, docs/adr/) is
|
|
15
|
+
* absent. Conditionals are stripped or retained in a single regex pass before
|
|
16
|
+
* `{{KEY}}` substitution runs; no nesting, no `{{else}}`.
|
|
17
|
+
*/
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
const PROMPT_FILES = {
|
|
21
|
+
implementer: 'implementer.md.tpl',
|
|
22
|
+
reviewer: 'reviewer.md.tpl',
|
|
23
|
+
};
|
|
24
|
+
const PLACEHOLDER_REGEX = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
|
|
25
|
+
const CONDITIONAL_REGEX = /\{\{#if ([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves `{{#if FLAG}}…{{/if}}` blocks, then replaces every `{{KEY}}` with
|
|
28
|
+
* `vars[KEY]`. Throws if a placeholder or conditional names a key/flag not
|
|
29
|
+
* supplied — silently leaving `{{ISSUE_NUMBER}}` or `{{#if X}}` in a rendered
|
|
30
|
+
* prompt would surface as a confusing agent failure rather than a clear setup
|
|
31
|
+
* error.
|
|
32
|
+
*
|
|
33
|
+
* Conditional blocks are stripped (when the flag is false) before the
|
|
34
|
+
* placeholder pass runs, so placeholders inside a stripped block don't need
|
|
35
|
+
* to appear in `vars`.
|
|
36
|
+
*/
|
|
37
|
+
export function substitute(template, vars, flags = {}) {
|
|
38
|
+
const resolved = template.replace(CONDITIONAL_REGEX, (_match, flag, body) => {
|
|
39
|
+
if (!Object.prototype.hasOwnProperty.call(flags, flag)) {
|
|
40
|
+
throw new Error(`render-prompt: missing template flag {{#if ${flag}}}`);
|
|
41
|
+
}
|
|
42
|
+
return flags[flag] ? body : '';
|
|
43
|
+
});
|
|
44
|
+
return resolved.replace(PLACEHOLDER_REGEX, (_match, key) => {
|
|
45
|
+
if (!Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
46
|
+
throw new Error(`render-prompt: missing template variable {{${key}}}`);
|
|
47
|
+
}
|
|
48
|
+
return vars[key];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reads the named prompt template from the library's bundled `prompts/`
|
|
53
|
+
* directory and returns it with `vars` substituted and `flags` resolved. The
|
|
54
|
+
* orchestrator owns the variable + flag contract per template (implementer:
|
|
55
|
+
* ISSUE_NUMBER + ISSUE_TITLE + SIBLING_CONTEXT, no flags; reviewer:
|
|
56
|
+
* ISSUE_NUMBER + BRANCH, plus HAS_CONTEXT_MD + HAS_ADRS).
|
|
57
|
+
*/
|
|
58
|
+
export async function renderPrompt(name, vars, flags = {}) {
|
|
59
|
+
const libraryRoot = import.meta.dirname;
|
|
60
|
+
const path = join(libraryRoot, 'prompts', PROMPT_FILES[name]);
|
|
61
|
+
const template = await readFile(path, 'utf8');
|
|
62
|
+
return substitute(template, vars, flags);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=render-prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-prompt.js","sourceRoot":"","sources":["../src/render-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,MAAM,YAAY,GAA+B;IAC/C,WAAW,EAAE,oBAAoB;IACjC,QAAQ,EAAE,iBAAiB;CAC5B,CAAC;AAEF,MAAM,iBAAiB,GAAG,6BAA6B,CAAC;AACxD,MAAM,iBAAiB,GAAG,uDAAuD,CAAC;AAElF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CACxB,QAAgB,EAChB,IAA4B,EAC5B,QAAiC,EAAE;IAEnC,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,IAAY,EAAE,EAAE;QAC1F,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,KAAK,CAAC,8CAA8C,IAAI,IAAI,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACjC,CAAC,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,GAAW,EAAE,EAAE;QACjE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,8CAA8C,GAAG,IAAI,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAgB,EAChB,IAA4B,EAC5B,QAAiC,EAAE;IAEnC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9C,OAAO,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC"}
|
package/dist/stage.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const STAGED_DIR_RELATIVE = ".sandcastle-drain/staged";
|
|
2
|
+
/**
|
|
3
|
+
* Absolute POSIX path inside the sandbox where the staged content is mounted.
|
|
4
|
+
* Sandcastle mounts each worktree at /home/agent/workspace (documented in
|
|
5
|
+
* @ai-hero/sandcastle's MountConfig). Passing an absolute POSIX sandboxPath
|
|
6
|
+
* sidesteps sandcastle's platform-native path.resolve, which on Windows
|
|
7
|
+
* mangles relative sandboxPath values into `C:\home\agent\workspace\...`
|
|
8
|
+
* and triggers Docker's "too many colons" mount parser.
|
|
9
|
+
*/
|
|
10
|
+
export declare const STAGED_SANDBOX_PATH = "/home/agent/workspace/.sandcastle-drain/staged";
|
|
11
|
+
/**
|
|
12
|
+
* Idempotently writes the library's content into `<cwd>/.sandcastle-drain/staged/`.
|
|
13
|
+
* Removes any prior staged tree first so a library upgrade is reflected
|
|
14
|
+
* immediately rather than merged into stale files.
|
|
15
|
+
*
|
|
16
|
+
* Safe to call once per CLI invocation, before the drain loop begins.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stage(cwd: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Rubric-availability flags the reviewer template branches on. `hasContextMd`
|
|
21
|
+
* is true when `CONTEXT.md` exists at the host root and is non-empty (an empty
|
|
22
|
+
* stub shouldn't enable the glossary rubric category). `hasAdrs` is true when
|
|
23
|
+
* `docs/adr/` exists at the host root and contains at least one `.md` file
|
|
24
|
+
* other than `README.md`.
|
|
25
|
+
*/
|
|
26
|
+
export interface HostRubricFlags {
|
|
27
|
+
readonly hasContextMd: boolean;
|
|
28
|
+
readonly hasAdrs: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detects whether the host project has a populated `CONTEXT.md` and/or
|
|
32
|
+
* non-empty `docs/adr/` directory. Memoized per `cwd` so the reviewer can call
|
|
33
|
+
* this once per issue and only the first call hits disk.
|
|
34
|
+
*
|
|
35
|
+
* Exposed for the orchestrator to pass into the reviewer's prompt rendering.
|
|
36
|
+
* The wrapper does not call this for the implementer prompt — the implementer
|
|
37
|
+
* is supposed to populate CONTEXT.md / ADRs as part of its work and shouldn't
|
|
38
|
+
* be told they're absent.
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectRubricFlags(cwd: string): HostRubricFlags;
|
|
41
|
+
/** Test-only escape hatch: clears the memoization table. */
|
|
42
|
+
export declare function resetRubricFlagsCache(): void;
|
|
43
|
+
//# sourceMappingURL=stage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,mDAAmD,CAAC;AAEpF;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CActD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAS9D;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
|
package/dist/stage.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stages library-bundled markdown into the host project's `.sandcastle-drain/staged/`
|
|
3
|
+
* so the implementer and reviewer agents — running inside per-issue worktrees —
|
|
4
|
+
* can read the canonical principles and agent-docs from inside the sandbox.
|
|
5
|
+
*
|
|
6
|
+
* The orchestrator bind-mounts `<host-cwd>/.sandcastle-drain/staged/` into each
|
|
7
|
+
* per-issue sandbox at the same relative path (read-only), so the agent can
|
|
8
|
+
* `Read .sandcastle-drain/staged/principles/testing.md` from inside the worktree.
|
|
9
|
+
*
|
|
10
|
+
* Prompt templates are NOT staged here — they're rendered in memory by
|
|
11
|
+
* `src/render-prompt.ts` and passed to `sandcastle.run()` as `prompt: <string>`.
|
|
12
|
+
*
|
|
13
|
+
* Library content is resolved relative to `import.meta.dirname`. The build
|
|
14
|
+
* script (`scripts/copy-library-assets.mjs`) copies `src/content/` next to the
|
|
15
|
+
* compiled `dist/stage.js` so the same resolver works under tsx (dev) and node
|
|
16
|
+
* (`dist/`).
|
|
17
|
+
*
|
|
18
|
+
* This module also owns host-rubric-flag detection (`detectRubricFlags`) so the
|
|
19
|
+
* reviewer prompt can degrade gracefully when `CONTEXT.md` or `docs/adr/` are
|
|
20
|
+
* absent or empty. The result is memoized per cwd so a 200-issue drain pays
|
|
21
|
+
* for the disk probes once.
|
|
22
|
+
*/
|
|
23
|
+
import { cp, mkdir, rm } from 'node:fs/promises';
|
|
24
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
export const STAGED_DIR_RELATIVE = '.sandcastle-drain/staged';
|
|
27
|
+
/**
|
|
28
|
+
* Absolute POSIX path inside the sandbox where the staged content is mounted.
|
|
29
|
+
* Sandcastle mounts each worktree at /home/agent/workspace (documented in
|
|
30
|
+
* @ai-hero/sandcastle's MountConfig). Passing an absolute POSIX sandboxPath
|
|
31
|
+
* sidesteps sandcastle's platform-native path.resolve, which on Windows
|
|
32
|
+
* mangles relative sandboxPath values into `C:\home\agent\workspace\...`
|
|
33
|
+
* and triggers Docker's "too many colons" mount parser.
|
|
34
|
+
*/
|
|
35
|
+
export const STAGED_SANDBOX_PATH = '/home/agent/workspace/.sandcastle-drain/staged';
|
|
36
|
+
/**
|
|
37
|
+
* Idempotently writes the library's content into `<cwd>/.sandcastle-drain/staged/`.
|
|
38
|
+
* Removes any prior staged tree first so a library upgrade is reflected
|
|
39
|
+
* immediately rather than merged into stale files.
|
|
40
|
+
*
|
|
41
|
+
* Safe to call once per CLI invocation, before the drain loop begins.
|
|
42
|
+
*/
|
|
43
|
+
export async function stage(cwd) {
|
|
44
|
+
const libraryRoot = import.meta.dirname;
|
|
45
|
+
const libraryContent = join(libraryRoot, 'content');
|
|
46
|
+
const stagedDir = join(cwd, STAGED_DIR_RELATIVE);
|
|
47
|
+
const stagedPrinciples = join(stagedDir, 'principles');
|
|
48
|
+
const stagedAgentDocs = join(stagedDir, 'agent-docs');
|
|
49
|
+
await rm(stagedDir, { recursive: true, force: true });
|
|
50
|
+
await mkdir(stagedPrinciples, { recursive: true });
|
|
51
|
+
await mkdir(stagedAgentDocs, { recursive: true });
|
|
52
|
+
await cp(join(libraryContent, 'principles'), stagedPrinciples, { recursive: true });
|
|
53
|
+
await cp(join(libraryContent, 'agent-docs'), stagedAgentDocs, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
const rubricFlagsCache = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Detects whether the host project has a populated `CONTEXT.md` and/or
|
|
58
|
+
* non-empty `docs/adr/` directory. Memoized per `cwd` so the reviewer can call
|
|
59
|
+
* this once per issue and only the first call hits disk.
|
|
60
|
+
*
|
|
61
|
+
* Exposed for the orchestrator to pass into the reviewer's prompt rendering.
|
|
62
|
+
* The wrapper does not call this for the implementer prompt — the implementer
|
|
63
|
+
* is supposed to populate CONTEXT.md / ADRs as part of its work and shouldn't
|
|
64
|
+
* be told they're absent.
|
|
65
|
+
*/
|
|
66
|
+
export function detectRubricFlags(cwd) {
|
|
67
|
+
const cached = rubricFlagsCache.get(cwd);
|
|
68
|
+
if (cached !== undefined)
|
|
69
|
+
return cached;
|
|
70
|
+
const result = {
|
|
71
|
+
hasContextMd: hasNonEmptyContextMd(cwd),
|
|
72
|
+
hasAdrs: hasAnyAdr(cwd),
|
|
73
|
+
};
|
|
74
|
+
rubricFlagsCache.set(cwd, result);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
/** Test-only escape hatch: clears the memoization table. */
|
|
78
|
+
export function resetRubricFlagsCache() {
|
|
79
|
+
rubricFlagsCache.clear();
|
|
80
|
+
}
|
|
81
|
+
function hasNonEmptyContextMd(cwd) {
|
|
82
|
+
const path = join(cwd, 'CONTEXT.md');
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
return statSync(path).size > 0;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function hasAnyAdr(cwd) {
|
|
93
|
+
const dir = join(cwd, 'docs', 'adr');
|
|
94
|
+
if (!existsSync(dir))
|
|
95
|
+
return false;
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(dir);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return entries.some((name) => name.endsWith('.md') && name !== 'README.md');
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=stage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage.js","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
FROM node:22-bookworm
|
|
2
|
+
|
|
3
|
+
# Install system dependencies
|
|
4
|
+
RUN apt-get update && apt-get install -y \
|
|
5
|
+
git \
|
|
6
|
+
curl \
|
|
7
|
+
jq \
|
|
8
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
|
+
|
|
10
|
+
# Install GitHub CLI
|
|
11
|
+
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
12
|
+
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
13
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
14
|
+
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
|
15
|
+
&& apt-get update && apt-get install -y gh \
|
|
16
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
17
|
+
|
|
18
|
+
# Install Playwright + Chromium system dependencies and the browser itself.
|
|
19
|
+
# Run as root before USER agent so apt-get can install libs. Browsers go to
|
|
20
|
+
# a world-readable shared path so the non-root agent user can launch them.
|
|
21
|
+
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
|
22
|
+
RUN npm install -g playwright@1.49.0 \
|
|
23
|
+
&& npx --yes playwright@1.49.0 install --with-deps chromium \
|
|
24
|
+
&& chmod -R a+rx /ms-playwright
|
|
25
|
+
|
|
26
|
+
# Rename the base image's "node" user (UID 1000) to "agent".
|
|
27
|
+
# This keeps UID 1000 so that --userns=keep-id (Podman) and
|
|
28
|
+
# --user 1000:1000 (Docker) map to the correct home directory owner.
|
|
29
|
+
RUN usermod -d /home/agent -m -l agent node
|
|
30
|
+
USER agent
|
|
31
|
+
|
|
32
|
+
# Install Claude Code CLI
|
|
33
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
34
|
+
|
|
35
|
+
# Add Claude to PATH
|
|
36
|
+
ENV PATH="/home/agent/.local/bin:$PATH"
|
|
37
|
+
|
|
38
|
+
WORKDIR /home/agent
|
|
39
|
+
|
|
40
|
+
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at the
|
|
41
|
+
# project root and overrides the working directory at container start.
|
|
42
|
+
ENTRYPOINT ["sleep", "infinity"]
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sandcastle-drain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Queue wrapper around @ai-hero/sandcastle: drains GitHub issues labelled `sandcastle` by running an autonomous Claude Code agent inside a Docker sandbox.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Downeys",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Downeys/sandcastle-drain.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Downeys/sandcastle-drain/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"sandcastle-drain": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/**/*",
|
|
20
|
+
"docker/Dockerfile",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"postbuild": "node scripts/copy-library-assets.mjs",
|
|
30
|
+
"prepublishOnly": "npm run build",
|
|
31
|
+
"drain": "tsx src/cli.ts drain",
|
|
32
|
+
"ship": "tsx src/cli.ts ship",
|
|
33
|
+
"sweep": "tsx src/cli.ts sweep",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"lint": "echo \"lint: no linter configured\"",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@ai-hero/sandcastle": "0.5.7",
|
|
40
|
+
"execa": "^9.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"tsx": "^4.19.0",
|
|
45
|
+
"typescript": "^5.6.0",
|
|
46
|
+
"vitest": "^2.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|