create-agent-rig 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 +87 -0
- package/package.json +54 -0
- package/packages/cli/dist/commands/create.js +109 -0
- package/packages/cli/dist/index.js +102 -0
- package/packages/cli/dist/lib/colors.js +14 -0
- package/packages/cli/dist/lib/composition.js +20 -0
- package/packages/cli/dist/lib/copy-tree.js +91 -0
- package/packages/cli/dist/lib/prompts.js +24 -0
- package/packages/cli/dist/lib/substitute.js +22 -0
- package/packages/cli/dist/lib/summary.js +41 -0
- package/packages/cli/dist/lib/targets.js +14 -0
- package/packages/cli/dist/templates.js +21 -0
- package/scripts/prepare.mjs +29 -0
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +49 -0
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +59 -0
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +51 -0
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +39 -0
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +36 -0
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
- package/templates/agent-os/universal/.claude/agents/test-writer.md +36 -0
- package/templates/agent-os/universal/.claude/hooks/block-no-verify.mjs +43 -0
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +79 -0
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +53 -0
- package/templates/agent-os/universal/.claude/rules/architecture.md +74 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +81 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +62 -0
- package/templates/agent-os/universal/.claude/settings.json +28 -0
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +53 -0
- package/templates/agent-os/universal/CLAUDE.md +62 -0
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +23 -0
- package/templates/skeleton/aws-serverless/README.md +78 -0
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +17 -0
- package/templates/skeleton/aws-serverless/apps/web/package.json +19 -0
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +17 -0
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +96 -0
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +29 -0
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +23 -0
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +38 -0
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +14 -0
- package/templates/skeleton/aws-serverless/eslint.config.mjs +20 -0
- package/templates/skeleton/aws-serverless/gitignore +9 -0
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +19 -0
- package/templates/skeleton/aws-serverless/infra/cdk.json +3 -0
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +116 -0
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +32 -0
- package/templates/skeleton/aws-serverless/infra/package.json +18 -0
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +104 -0
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +41 -0
- package/templates/skeleton/aws-serverless/package.json +30 -0
- package/templates/skeleton/aws-serverless/packages/core/package.json +11 -0
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +14 -0
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +15 -0
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +69 -0
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +23 -0
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +101 -0
- package/templates/skeleton/aws-serverless/packages/db/package.json +14 -0
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +17 -0
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +2 -0
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +52 -0
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +91 -0
- package/templates/skeleton/aws-serverless/packages/shared/package.json +11 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +17 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +33 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +3 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +20 -0
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +26 -0
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +28 -0
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +19 -0
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +2855 -0
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +14 -0
- package/templates/skeleton/aws-serverless/services/api/package.json +15 -0
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +26 -0
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +42 -0
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +24 -0
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +12 -0
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +21 -0
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +30 -0
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +14 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +92 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +45 -0
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +51 -0
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +22 -0
- package/templates/skeleton/aws-serverless/services/worker/package.json +12 -0
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +15 -0
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +7 -0
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +37 -0
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +61 -0
- package/templates/skeleton/aws-serverless/tsconfig.base.json +15 -0
- package/templates/skeleton/aws-serverless/tsconfig.json +16 -0
- package/templates/skeleton/aws-serverless/vitest.config.ts +14 -0
- package/templates/skeleton/node-service/.github/workflows/ci.yml +22 -0
- package/templates/skeleton/node-service/README.md +74 -0
- package/templates/skeleton/node-service/apps/web/next.config.mjs +17 -0
- package/templates/skeleton/node-service/apps/web/package.json +19 -0
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +17 -0
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +96 -0
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +29 -0
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +23 -0
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +38 -0
- package/templates/skeleton/node-service/apps/web/tsconfig.json +14 -0
- package/templates/skeleton/node-service/eslint.config.mjs +20 -0
- package/templates/skeleton/node-service/gitignore +9 -0
- package/templates/skeleton/node-service/package.json +28 -0
- package/templates/skeleton/node-service/packages/core/package.json +11 -0
- package/templates/skeleton/node-service/packages/core/src/events.ts +14 -0
- package/templates/skeleton/node-service/packages/core/src/index.ts +15 -0
- package/templates/skeleton/node-service/packages/core/src/note.ts +69 -0
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +23 -0
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +101 -0
- package/templates/skeleton/node-service/packages/db/package.json +12 -0
- package/templates/skeleton/node-service/packages/db/src/index.ts +1 -0
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +63 -0
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +80 -0
- package/templates/skeleton/node-service/packages/shared/package.json +11 -0
- package/templates/skeleton/node-service/packages/shared/src/env.ts +17 -0
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +33 -0
- package/templates/skeleton/node-service/packages/shared/src/index.ts +3 -0
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +20 -0
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +26 -0
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +28 -0
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +19 -0
- package/templates/skeleton/node-service/pnpm-lock.yaml +2399 -0
- package/templates/skeleton/node-service/pnpm-workspace.yaml +13 -0
- package/templates/skeleton/node-service/services/api/package.json +17 -0
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +23 -0
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +40 -0
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +23 -0
- package/templates/skeleton/node-service/services/api/src/main.ts +47 -0
- package/templates/skeleton/node-service/services/api/src/server.ts +89 -0
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +30 -0
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +14 -0
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +64 -0
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +43 -0
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +48 -0
- package/templates/skeleton/node-service/services/api/test/server.test.ts +123 -0
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +32 -0
- package/templates/skeleton/node-service/services/worker/package.json +16 -0
- package/templates/skeleton/node-service/services/worker/src/main.ts +28 -0
- package/templates/skeleton/node-service/services/worker/src/spool.ts +60 -0
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +38 -0
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +34 -0
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +76 -0
- package/templates/skeleton/node-service/tsconfig.base.json +15 -0
- package/templates/skeleton/node-service/tsconfig.json +13 -0
- package/templates/skeleton/node-service/vitest.config.ts +12 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Workflow — TDD, branches, PR policy, Definition of Done
|
|
2
|
+
|
|
3
|
+
## TDD is the default motion
|
|
4
|
+
|
|
5
|
+
Red → Green → Refactor, in that order, every time:
|
|
6
|
+
|
|
7
|
+
1. **Red** — write the test that describes the behavior; run it; watch it fail.
|
|
8
|
+
Use the `test-writer` agent for this step: it writes the failing test and is
|
|
9
|
+
scoped so it cannot "helpfully" write the implementation too.
|
|
10
|
+
2. **Green** — the minimum implementation that makes the test pass.
|
|
11
|
+
3. **Refactor** — clean up with the tests staying green.
|
|
12
|
+
|
|
13
|
+
No implementation before its failing test exists. A bug fix starts with a test
|
|
14
|
+
that reproduces the bug.
|
|
15
|
+
|
|
16
|
+
## Tests are load-bearing
|
|
17
|
+
|
|
18
|
+
- Never delete, skip, or weaken a test to make a run green.
|
|
19
|
+
- A flaky test is a defect to investigate, **not** a thing to re-run until it
|
|
20
|
+
passes (see stop rules in `autonomy.md`).
|
|
21
|
+
- Fast checks run pre-commit; the full suite runs in CI. Bypassing pre-commit
|
|
22
|
+
(`--no-verify`) is refused by a hook — fix the failure instead.
|
|
23
|
+
|
|
24
|
+
## Branches and commits
|
|
25
|
+
|
|
26
|
+
- Work happens on short-lived branches off the default branch; the default
|
|
27
|
+
branch stays releasable.
|
|
28
|
+
- Commits are small and single-purpose; the message says *why*, not just *what*.
|
|
29
|
+
|
|
30
|
+
## Review-context isolation
|
|
31
|
+
|
|
32
|
+
The session that wrote the code is measurably worse at reviewing it: it
|
|
33
|
+
carries its own reasoning in context and will not challenge its own decisions
|
|
34
|
+
the way a cold reader does. That is *why* `code-reviewer` is a separate
|
|
35
|
+
subagent with a fresh context, and why the `pr-ship` gate fans reviewers out
|
|
36
|
+
instead of self-checking. This isolation is load-bearing, not ceremony — do
|
|
37
|
+
not "optimise" it away by reviewing in the authoring session.
|
|
38
|
+
|
|
39
|
+
## PR policy
|
|
40
|
+
|
|
41
|
+
- One concern per PR. If the description needs the word "also", split it.
|
|
42
|
+
- The `pr-ship` skill is the pre-merge gate: full checks, reviewer fan-out,
|
|
43
|
+
DoD walk, and a SHIP / HOLD verdict with named blockers.
|
|
44
|
+
- The PR description states: intent, what changed, how it was verified, and any
|
|
45
|
+
autonomy-tier judgment calls made (see `autonomy.md`).
|
|
46
|
+
- The `code-reviewer` agent runs before a PR is opened; its blocking findings
|
|
47
|
+
are resolved, not argued with. The `security-scanner` agent runs whenever the
|
|
48
|
+
change touches auth, secrets, input parsing, or outbound calls.
|
|
49
|
+
- CI must be green before merge. A red check is fixed or the PR is closed —
|
|
50
|
+
never merged around.
|
|
51
|
+
|
|
52
|
+
## Definition of Done
|
|
53
|
+
|
|
54
|
+
A change is done when **all** of these hold:
|
|
55
|
+
|
|
56
|
+
- [ ] A test written first demonstrates the new behavior (and failed before the change)
|
|
57
|
+
- [ ] The full test suite is green — nothing skipped, nothing weakened
|
|
58
|
+
- [ ] Lint and typecheck are clean
|
|
59
|
+
- [ ] Layer boundaries respected (no new cross-layer imports; core still pure)
|
|
60
|
+
- [ ] No secrets, credentials, or personal data in code, config, or fixtures
|
|
61
|
+
- [ ] Docs touched by the change (README, rules) are updated
|
|
62
|
+
- [ ] The autonomy tier of the change was checked and honored (`autonomy.md`)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Write|Edit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-core-purity.mjs\""
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-web-boundary.mjs\""
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Bash",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-no-verify.mjs\""
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-ship
|
|
3
|
+
description: The pre-merge gate. MUST run before a PR is opened or merged — runs the full check suite, fans out the reviewer gates, walks the DoD, and returns a SHIP / HOLD verdict with named blockers.
|
|
4
|
+
allowed-tools: Read, Grep, Glob, Bash, Task
|
|
5
|
+
argument-hint: [branch-or-pr]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the last gate before a change ships. You verify and report; you do not
|
|
9
|
+
fix — a HOLD goes back to the author (usually the main session) with named
|
|
10
|
+
blockers.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **The diff first.** Establish what is actually shipping: fetch, then diff
|
|
15
|
+
against the **remote** default branch (`origin/<default>`), not a local
|
|
16
|
+
copy that may be behind — diagnosing from stale local code produces
|
|
17
|
+
confidently-wrong reviews. Everything below is scoped to this diff.
|
|
18
|
+
2. **The project's own checks.** Run the full check suite the project defines
|
|
19
|
+
(see its README / package scripts). Any failure is an instant HOLD — never
|
|
20
|
+
argue with a red check, never rerun flakiness to green
|
|
21
|
+
(`.claude/rules/workflow.md`).
|
|
22
|
+
3. **Reviewer fan-out.** Launch the `code-reviewer` agent on the diff — always.
|
|
23
|
+
Launch `security-scanner` as well when the diff touches its triggers: auth,
|
|
24
|
+
secrets or configuration, input parsing, file handling, new outbound calls,
|
|
25
|
+
dependency changes. Run them as subagents, in parallel — a fresh context
|
|
26
|
+
reviews better than the session that wrote the code (see
|
|
27
|
+
`.claude/rules/workflow.md`, "Review-context isolation").
|
|
28
|
+
4. **DoD walk.** Check the Definition of Done list in
|
|
29
|
+
`.claude/rules/workflow.md` item by item — test-first evidence, nothing
|
|
30
|
+
skipped or weakened, boundaries respected, docs updated, autonomy tier
|
|
31
|
+
honored.
|
|
32
|
+
5. **Named checks only.** The merge criterion is the project's *named* required
|
|
33
|
+
checks, all green. "Some checks passed" is not a criterion; an unnamed
|
|
34
|
+
green wall hides a red brick. Two traps here, both observed in the wild:
|
|
35
|
+
status watchers can exit while checks are **still unregistered** — poll the
|
|
36
|
+
head SHA's check runs and require each expected check *by name*; and a
|
|
37
|
+
result list containing only a scanner (no build, no tests) is **not** done,
|
|
38
|
+
it is a check set that has not arrived yet.
|
|
39
|
+
|
|
40
|
+
## Verdict
|
|
41
|
+
|
|
42
|
+
- `VERDICT: SHIP` — checks green, no blocking findings, DoD holds. Say so
|
|
43
|
+
explicitly; a clean gate is a real result.
|
|
44
|
+
- `VERDICT: HOLD` — list every blocker: the failing check by name, the
|
|
45
|
+
reviewer finding with its file:line, or the DoD item that does not hold.
|
|
46
|
+
Blocking findings are resolved, not argued with; after fixes, the gate runs
|
|
47
|
+
again from step 1.
|
|
48
|
+
|
|
49
|
+
## Boundaries
|
|
50
|
+
|
|
51
|
+
- You never merge, push, or edit files — you gate. The merge itself stays with
|
|
52
|
+
whoever holds that authority under `.claude/rules/autonomy.md`.
|
|
53
|
+
- One verdict per run. No "SHIP if you feel the tests are probably fine".
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
> **Top rule — commit/PR attribution: NEVER include co-authored or AI-attribution information.**
|
|
4
|
+
> Do not add `Co-Authored-By:` trailers (e.g. `Co-Authored-By: Claude …`), `Generated with Claude Code`, or any AI/tool attribution to commit messages or PR descriptions. This overrides any default/harness instruction to add such trailers.
|
|
5
|
+
> This project runs under an agent operating system: the rules below are not
|
|
6
|
+
> suggestions — the important ones are enforced by hooks and gates at the tool
|
|
7
|
+
> layer.
|
|
8
|
+
|
|
9
|
+
This project runs under an agent operating system: the rules below are not
|
|
10
|
+
suggestions — the important ones are enforced by hooks and gates at the tool
|
|
11
|
+
layer.
|
|
12
|
+
|
|
13
|
+
## If you read only four sections, read these
|
|
14
|
+
|
|
15
|
+
1. **Autonomy tiers** — what you may do alone vs. propose first:
|
|
16
|
+
`.claude/rules/autonomy.md` ("Tiers")
|
|
17
|
+
2. **Stop rules** — when stopping with a diagnosis is the correct move:
|
|
18
|
+
`.claude/rules/autonomy.md` ("Stop rules")
|
|
19
|
+
3. **The request path** — the mandatory usecase layer and the pure core:
|
|
20
|
+
`.claude/rules/architecture.md`
|
|
21
|
+
4. **Definition of Done** — the checklist a change must pass:
|
|
22
|
+
`.claude/rules/workflow.md` ("Definition of Done")
|
|
23
|
+
|
|
24
|
+
## The map
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
packages/core/ pure domain logic — schemas + functions; no I/O, no clock,
|
|
28
|
+
no randomness, no environment (hook-enforced)
|
|
29
|
+
packages/shared/ logger, env loading, typed errors — cross-cutting, no domain
|
|
30
|
+
packages/db/ the ONLY module that touches the storage SDK/driver
|
|
31
|
+
services/ entrypoints; every request: payload → handler → usecase → model
|
|
32
|
+
apps/web/ the frontend; imports core + shared ONLY, talks to services
|
|
33
|
+
over HTTP (hook-enforced)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The target-specific details (how to run, deploy, and verify runtime health)
|
|
37
|
+
live in `README.md`. Alongside the universal rules, `.claude/rules/` carries
|
|
38
|
+
the stack-specific conventions composed in for this project's target — read
|
|
39
|
+
them all; they are one rulebook.
|
|
40
|
+
|
|
41
|
+
## How work happens here
|
|
42
|
+
|
|
43
|
+
- **TDD, without exception.** The failing test comes first — use the
|
|
44
|
+
`test-writer` agent for it. See `.claude/rules/workflow.md`.
|
|
45
|
+
- **Gates.** `code-reviewer` runs before every PR; `security-scanner` runs when
|
|
46
|
+
a change touches auth, secrets, parsing, or outbound calls. Blocking findings
|
|
47
|
+
are resolved, not argued with.
|
|
48
|
+
- **Enforcement is mechanical.** `guard-core-purity` refuses impure edits to
|
|
49
|
+
the core; `block-no-verify` refuses pre-commit bypasses. If a hook blocks
|
|
50
|
+
you, fix the cause; never route around a hook.
|
|
51
|
+
|
|
52
|
+
## Foot-guns
|
|
53
|
+
|
|
54
|
+
- Don't "simplify" a handler by calling a model directly — the usecase layer is
|
|
55
|
+
mandatory even when it looks like ceremony.
|
|
56
|
+
- Don't inline `Date.now()`/randomness into the core "just this once" — inject
|
|
57
|
+
them; the hook will refuse anyway.
|
|
58
|
+
- Don't weaken a failing test to get green — a red check is information, and
|
|
59
|
+
test integrity is a blocking review finding.
|
|
60
|
+
- After a deploy, CI-green ≠ runtime-healthy: verify per the README, and on
|
|
61
|
+
regression revert first (`.claude/rules/autonomy.md`, "Post-deploy
|
|
62
|
+
verification").
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
ci:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: pnpm/action-setup@v4
|
|
13
|
+
with:
|
|
14
|
+
version: 11
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
cache: pnpm
|
|
19
|
+
- run: pnpm install --no-frozen-lockfile
|
|
20
|
+
- run: pnpm lint
|
|
21
|
+
- run: pnpm typecheck
|
|
22
|
+
- run: pnpm test
|
|
23
|
+
- run: pnpm synth
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
A serverless notes service generated by `create-agent-rig`, target
|
|
4
|
+
`aws-serverless`. It is deliberately the **smallest project that proves the
|
|
5
|
+
architecture** — one HTTP route, one queue consumer — with every layer visible
|
|
6
|
+
and every gate wired. This project is yours: **delete what you don't need.**
|
|
7
|
+
|
|
8
|
+
## Layout
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
packages/core/ pure domain: zod schemas + createNote() (no I/O — hook-enforced)
|
|
12
|
+
packages/shared/ logger, loadEnv(zod), typed errors
|
|
13
|
+
packages/db/ NoteModel — the only code touching the DynamoDB SDK
|
|
14
|
+
services/api/ POST /notes + GET /notes: payload → handler → usecase → model
|
|
15
|
+
services/worker/ note.created consumer (batchSize 1 → DLQ after 3 strikes)
|
|
16
|
+
apps/web/ static Next export; validates with the SAME core schema the
|
|
17
|
+
server trusts (imports core+shared only — hook-enforced)
|
|
18
|
+
infra/ CDK stacks: table, queue+DLQ+alarm, three lambdas, HTTP API,
|
|
19
|
+
and S3+CloudFront for the web bundle
|
|
20
|
+
.claude/ the agent operating system: rules, gates, blocking hooks
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The agent-facing map is `CLAUDE.md`; the rules live in `.claude/rules/`.
|
|
24
|
+
|
|
25
|
+
## Run it
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pnpm install
|
|
29
|
+
pnpm test # every layer, core exhaustively
|
|
30
|
+
pnpm lint
|
|
31
|
+
pnpm typecheck
|
|
32
|
+
pnpm build:web # static Next export → apps/web/out
|
|
33
|
+
pnpm synth # CloudFormation synth (no AWS credentials needed)
|
|
34
|
+
pnpm check # all of the above
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Deploy it
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# needs AWS credentials; region comes from your profile (generator default: __REGION__)
|
|
41
|
+
cd infra
|
|
42
|
+
npx cdk bootstrap # first time per account/region
|
|
43
|
+
npx cdk deploy AppStack
|
|
44
|
+
|
|
45
|
+
# the web bundle: build against the deployed API, then sync to the web stack
|
|
46
|
+
cd ..
|
|
47
|
+
NEXT_PUBLIC_API_URL=<ApiUrl output> pnpm build:web
|
|
48
|
+
cd infra && npx cdk deploy WebStack
|
|
49
|
+
aws s3 sync ../apps/web/out "s3://<WebBucketName output>"
|
|
50
|
+
# the site is at the WebUrl output
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Verify runtime health (CI-green ≠ runtime-healthy)
|
|
54
|
+
|
|
55
|
+
After every deploy:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
curl -s -X POST "$API_URL/notes" \
|
|
59
|
+
-H 'content-type: application/json' \
|
|
60
|
+
-d '{"title":"smoke test","tags":["deploy"]}'
|
|
61
|
+
# expect: HTTP 201 with { "note": { … } }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then confirm the pipeline: the worker logs `note.created processed`, and the
|
|
65
|
+
**DLQ alarm stays quiet**. If the smoke test regresses: `npx cdk deploy` the
|
|
66
|
+
previous revision (or `git revert` and redeploy) **first**, diagnose second.
|
|
67
|
+
|
|
68
|
+
## Boundaries (the short version)
|
|
69
|
+
|
|
70
|
+
- Every request follows `payload → handler → usecase → model`. The usecase
|
|
71
|
+
layer is mandatory even when it looks like ceremony.
|
|
72
|
+
- `packages/core` is pure — the `guard-core-purity` hook refuses I/O, clock,
|
|
73
|
+
randomness, and environment access at the tool layer.
|
|
74
|
+
- `packages/db` is the only module that touches the storage SDK.
|
|
75
|
+
- A failing queue message is poison: it throws, SQS retries ×3, the DLQ alarm
|
|
76
|
+
fires. Never wrap the worker in a broad catch.
|
|
77
|
+
|
|
78
|
+
See `.claude/rules/architecture.md` for the full rules.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
// Static export: one build, no second runtime — the API server (or a CDN)
|
|
4
|
+
// serves the bundle. See README for how this target serves it.
|
|
5
|
+
output: 'export',
|
|
6
|
+
// The core ships as TypeScript source; Next transpiles it for the browser.
|
|
7
|
+
transpilePackages: ['@app/core'],
|
|
8
|
+
// The core uses NodeNext-style relative imports ("./note.js" resolving to
|
|
9
|
+
// note.ts). Turbopack cannot map that, so the build runs webpack (see the
|
|
10
|
+
// build script) with the standard extension alias.
|
|
11
|
+
webpack: (config) => {
|
|
12
|
+
config.resolve.extensionAlias = { '.js': ['.ts', '.tsx', '.js'] };
|
|
13
|
+
return config;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default nextConfig;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev --port 3001 --webpack",
|
|
7
|
+
"build": "next build --webpack"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@app/core": "workspace:*",
|
|
11
|
+
"next": "^16.2.11",
|
|
12
|
+
"react": "^19.2.8",
|
|
13
|
+
"react-dom": "^19.2.8"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/react": "^19.2.17",
|
|
17
|
+
"@types/react-dom": "^19.2.3"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// Deliberately unstyled: a starter's frontend is scaffolding, not a product.
|
|
4
|
+
// Polish belongs in the mechanism (the shared validation, the boundary hook),
|
|
5
|
+
// not in the pixels — replace all of this freely.
|
|
6
|
+
export const metadata = {
|
|
7
|
+
title: '__PROJECT_NAME__',
|
|
8
|
+
description: 'Notes — generated by create-agent-rig',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
12
|
+
return (
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<body>{children}</body>
|
|
15
|
+
</html>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// One page, one point: the SAME domain schema validates here (instant
|
|
3
|
+
// feedback) and on the server (trust). See src/lib/validate.ts.
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import type { Note } from '@app/core';
|
|
6
|
+
import { createNote, listNotes } from '../lib/api';
|
|
7
|
+
import { validateNewNote } from '../lib/validate';
|
|
8
|
+
|
|
9
|
+
export default function NotesPage() {
|
|
10
|
+
const [title, setTitle] = useState('');
|
|
11
|
+
const [tags, setTags] = useState('');
|
|
12
|
+
const [issues, setIssues] = useState<string[]>([]);
|
|
13
|
+
const [notes, setNotes] = useState<Note[]>([]);
|
|
14
|
+
const [status, setStatus] = useState<string>('');
|
|
15
|
+
|
|
16
|
+
const refresh = useCallback(async () => {
|
|
17
|
+
try {
|
|
18
|
+
setNotes(await listNotes());
|
|
19
|
+
setStatus('');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
setStatus(`could not load notes: ${String(error)}`);
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
void refresh();
|
|
27
|
+
}, [refresh]);
|
|
28
|
+
|
|
29
|
+
async function onSubmit(event: React.FormEvent) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
const input = {
|
|
32
|
+
title,
|
|
33
|
+
tags: tags
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((tag) => tag.trim())
|
|
36
|
+
.filter(Boolean),
|
|
37
|
+
};
|
|
38
|
+
// Client-side: the same core schema the server will apply again.
|
|
39
|
+
const verdict = validateNewNote(input);
|
|
40
|
+
setIssues(verdict.issues);
|
|
41
|
+
if (!verdict.ok) return;
|
|
42
|
+
try {
|
|
43
|
+
await createNote(input);
|
|
44
|
+
setTitle('');
|
|
45
|
+
setTags('');
|
|
46
|
+
await refresh();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
setIssues([String(error)]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<main>
|
|
54
|
+
<h1>Notes</h1>
|
|
55
|
+
<p>
|
|
56
|
+
The form validates with the <em>same</em> core function the server
|
|
57
|
+
trusts — one schema, both sides of the wire.
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<form onSubmit={onSubmit}>
|
|
61
|
+
<label>
|
|
62
|
+
Title{' '}
|
|
63
|
+
<input
|
|
64
|
+
name="title"
|
|
65
|
+
value={title}
|
|
66
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
67
|
+
/>
|
|
68
|
+
</label>{' '}
|
|
69
|
+
<label>
|
|
70
|
+
Tags (comma-separated){' '}
|
|
71
|
+
<input name="tags" value={tags} onChange={(event) => setTags(event.target.value)} />
|
|
72
|
+
</label>{' '}
|
|
73
|
+
<button type="submit">Create</button>
|
|
74
|
+
</form>
|
|
75
|
+
|
|
76
|
+
{issues.length > 0 && (
|
|
77
|
+
<ul role="alert">
|
|
78
|
+
{issues.map((issue) => (
|
|
79
|
+
<li key={issue}>{issue}</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
)}
|
|
83
|
+
{status && <p role="status">{status}</p>}
|
|
84
|
+
|
|
85
|
+
<h2>Existing</h2>
|
|
86
|
+
<ul>
|
|
87
|
+
{notes.map((note) => (
|
|
88
|
+
<li key={note.id}>
|
|
89
|
+
<strong>{note.title}</strong> <code>{note.slug}</code>{' '}
|
|
90
|
+
<small>{note.tags.join(', ')}</small>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
</main>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The web talks to the backend over HTTP only — never by importing it
|
|
2
|
+
// (the guard-web-boundary hook refuses such imports at the tool layer).
|
|
3
|
+
import type { Note } from '@app/core';
|
|
4
|
+
|
|
5
|
+
// Same-origin by default (the API server serves this bundle); set
|
|
6
|
+
// NEXT_PUBLIC_API_URL at build time when the API lives elsewhere.
|
|
7
|
+
const base = process.env.NEXT_PUBLIC_API_URL ?? '';
|
|
8
|
+
|
|
9
|
+
export async function createNote(input: { title: string; tags: string[] }): Promise<Note> {
|
|
10
|
+
const response = await fetch(`${base}/notes`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'content-type': 'application/json' },
|
|
13
|
+
body: JSON.stringify(input),
|
|
14
|
+
});
|
|
15
|
+
const body = (await response.json()) as { note?: Note; error?: string; issues?: string[] };
|
|
16
|
+
if (!response.ok || !body.note) {
|
|
17
|
+
throw new Error(body.issues?.join('; ') ?? body.error ?? `HTTP ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
return body.note;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function listNotes(): Promise<Note[]> {
|
|
23
|
+
const response = await fetch(`${base}/notes`);
|
|
24
|
+
const body = (await response.json()) as { notes?: Note[]; error?: string };
|
|
25
|
+
if (!response.ok || !body.notes) {
|
|
26
|
+
throw new Error(body.error ?? `HTTP ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
return body.notes;
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The load-bearing import of this whole app: the browser validates with the
|
|
2
|
+
// SAME schema the server trusts. Client-side for instant feedback,
|
|
3
|
+
// server-side for trust — one function, two sides of the wire. This is what
|
|
4
|
+
// core purity buys: `@app/core` has no I/O, so it runs anywhere.
|
|
5
|
+
import { NewNoteSchema } from '@app/core';
|
|
6
|
+
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
issues: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateNewNote(input: unknown): ValidationResult {
|
|
13
|
+
const parsed = NewNoteSchema.safeParse(input);
|
|
14
|
+
if (parsed.success) {
|
|
15
|
+
return { ok: true, issues: [] };
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
issues: parsed.error.issues.map(
|
|
20
|
+
(issue) => `${issue.path.join('.') || 'input'}: ${issue.message}`,
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// THE load-bearing test of the frontend (web brief §5): the browser-side
|
|
2
|
+
// validator and the server-side schema must be the same decision procedure.
|
|
3
|
+
// No jsdom, no rendering — the boundary is the point, not the pixels.
|
|
4
|
+
import { InvalidNoteError, NewNoteSchema, createNote } from '@app/core';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { validateNewNote } from '../src/lib/validate';
|
|
7
|
+
|
|
8
|
+
const identity = { id: 'n1', createdAt: '2024-01-01T00:00:00.000Z' };
|
|
9
|
+
|
|
10
|
+
const CASES: Array<{ name: string; input: unknown }> = [
|
|
11
|
+
{ name: 'valid minimal', input: { title: 'Hello' } },
|
|
12
|
+
{ name: 'valid with tags', input: { title: 'Hello', tags: ['a', 'b'] } },
|
|
13
|
+
{ name: 'empty title', input: { title: '' } },
|
|
14
|
+
{ name: 'whitespace title', input: { title: ' ' } },
|
|
15
|
+
{ name: 'title too long', input: { title: 'x'.repeat(201) } },
|
|
16
|
+
{ name: 'too many tags', input: { title: 'T', tags: Array.from({ length: 11 }, (_, i) => `${i}`) } },
|
|
17
|
+
{ name: 'empty tag', input: { title: 'T', tags: [''] } },
|
|
18
|
+
{ name: 'wrong types', input: { title: 42 } },
|
|
19
|
+
{ name: 'not an object', input: 'nope' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe('one schema, both sides of the wire', () => {
|
|
23
|
+
for (const { name, input } of CASES) {
|
|
24
|
+
it(`web and core agree on: ${name}`, () => {
|
|
25
|
+
const webVerdict = validateNewNote(input);
|
|
26
|
+
const coreVerdict = NewNoteSchema.safeParse(input);
|
|
27
|
+
expect(webVerdict.ok).toBe(coreVerdict.success);
|
|
28
|
+
|
|
29
|
+
// …and the domain function (used by the API usecase) agrees with both.
|
|
30
|
+
if (coreVerdict.success) {
|
|
31
|
+
expect(() => createNote(input, identity)).not.toThrow();
|
|
32
|
+
} else {
|
|
33
|
+
expect(() => createNote(input, identity)).toThrow(InvalidNoteError);
|
|
34
|
+
expect(webVerdict.issues.length).toBeGreaterThan(0);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "preserve",
|
|
7
|
+
"lib": ["dom", "dom.iterable", "es2022"],
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"plugins": [{ "name": "next" }]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"],
|
|
13
|
+
"exclude": ["node_modules", "out", ".next"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import tseslint from 'typescript-eslint';
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
{
|
|
7
|
+
ignores: [
|
|
8
|
+
'**/node_modules/**',
|
|
9
|
+
'**/dist/**',
|
|
10
|
+
'**/cdk.out/**',
|
|
11
|
+
'**/coverage/**',
|
|
12
|
+
'**/.next/**',
|
|
13
|
+
'**/out/**',
|
|
14
|
+
'**/next-env.d.ts',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
eslint.configs.recommended,
|
|
18
|
+
...tseslint.configs.recommended,
|
|
19
|
+
{ languageOptions: { globals: globals.node } },
|
|
20
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { App } from 'aws-cdk-lib';
|
|
2
|
+
import { AppStack } from '../lib/app-stack.js';
|
|
3
|
+
import { WebStack } from '../lib/web-stack.js';
|
|
4
|
+
|
|
5
|
+
const app = new App();
|
|
6
|
+
|
|
7
|
+
// Region-agnostic by default; `cdk deploy` uses your CLI profile. Pin it here
|
|
8
|
+
// when the project settles (the generator's default region is __REGION__).
|
|
9
|
+
const env = { region: process.env.CDK_DEFAULT_REGION };
|
|
10
|
+
|
|
11
|
+
new AppStack(app, 'AppStack', {
|
|
12
|
+
description: 'API + worker for __PROJECT_NAME__ (generated by create-agent-rig)',
|
|
13
|
+
env,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
new WebStack(app, 'WebStack', {
|
|
17
|
+
description: 'Static web serving for __PROJECT_NAME__ (generated by create-agent-rig)',
|
|
18
|
+
env,
|
|
19
|
+
});
|