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.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/package.json +54 -0
  4. package/packages/cli/dist/commands/create.js +109 -0
  5. package/packages/cli/dist/index.js +102 -0
  6. package/packages/cli/dist/lib/colors.js +14 -0
  7. package/packages/cli/dist/lib/composition.js +20 -0
  8. package/packages/cli/dist/lib/copy-tree.js +91 -0
  9. package/packages/cli/dist/lib/prompts.js +24 -0
  10. package/packages/cli/dist/lib/substitute.js +22 -0
  11. package/packages/cli/dist/lib/summary.js +41 -0
  12. package/packages/cli/dist/lib/targets.js +14 -0
  13. package/packages/cli/dist/templates.js +21 -0
  14. package/scripts/prepare.mjs +29 -0
  15. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +49 -0
  16. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +59 -0
  17. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +51 -0
  18. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +39 -0
  19. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +36 -0
  20. package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
  21. package/templates/agent-os/universal/.claude/agents/test-writer.md +36 -0
  22. package/templates/agent-os/universal/.claude/hooks/block-no-verify.mjs +43 -0
  23. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +79 -0
  24. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +53 -0
  25. package/templates/agent-os/universal/.claude/rules/architecture.md +74 -0
  26. package/templates/agent-os/universal/.claude/rules/autonomy.md +81 -0
  27. package/templates/agent-os/universal/.claude/rules/workflow.md +62 -0
  28. package/templates/agent-os/universal/.claude/settings.json +28 -0
  29. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +53 -0
  30. package/templates/agent-os/universal/CLAUDE.md +62 -0
  31. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +23 -0
  32. package/templates/skeleton/aws-serverless/README.md +78 -0
  33. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +17 -0
  34. package/templates/skeleton/aws-serverless/apps/web/package.json +19 -0
  35. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +17 -0
  36. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +96 -0
  37. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +29 -0
  38. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +23 -0
  39. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +38 -0
  40. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +14 -0
  41. package/templates/skeleton/aws-serverless/eslint.config.mjs +20 -0
  42. package/templates/skeleton/aws-serverless/gitignore +9 -0
  43. package/templates/skeleton/aws-serverless/infra/bin/app.ts +19 -0
  44. package/templates/skeleton/aws-serverless/infra/cdk.json +3 -0
  45. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +116 -0
  46. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +32 -0
  47. package/templates/skeleton/aws-serverless/infra/package.json +18 -0
  48. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +104 -0
  49. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +41 -0
  50. package/templates/skeleton/aws-serverless/package.json +30 -0
  51. package/templates/skeleton/aws-serverless/packages/core/package.json +11 -0
  52. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +14 -0
  53. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +15 -0
  54. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +69 -0
  55. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +23 -0
  56. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +101 -0
  57. package/templates/skeleton/aws-serverless/packages/db/package.json +14 -0
  58. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +17 -0
  59. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +2 -0
  60. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +52 -0
  61. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +91 -0
  62. package/templates/skeleton/aws-serverless/packages/shared/package.json +11 -0
  63. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +17 -0
  64. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +33 -0
  65. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +3 -0
  66. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +20 -0
  67. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +26 -0
  68. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +28 -0
  69. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +19 -0
  70. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +2855 -0
  71. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +14 -0
  72. package/templates/skeleton/aws-serverless/services/api/package.json +15 -0
  73. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +26 -0
  74. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +42 -0
  75. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +24 -0
  76. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +12 -0
  77. package/templates/skeleton/aws-serverless/services/api/src/main.ts +21 -0
  78. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +30 -0
  79. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +14 -0
  80. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +92 -0
  81. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +45 -0
  82. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +51 -0
  83. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +22 -0
  84. package/templates/skeleton/aws-serverless/services/worker/package.json +12 -0
  85. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +15 -0
  86. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +7 -0
  87. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +37 -0
  88. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +61 -0
  89. package/templates/skeleton/aws-serverless/tsconfig.base.json +15 -0
  90. package/templates/skeleton/aws-serverless/tsconfig.json +16 -0
  91. package/templates/skeleton/aws-serverless/vitest.config.ts +14 -0
  92. package/templates/skeleton/node-service/.github/workflows/ci.yml +22 -0
  93. package/templates/skeleton/node-service/README.md +74 -0
  94. package/templates/skeleton/node-service/apps/web/next.config.mjs +17 -0
  95. package/templates/skeleton/node-service/apps/web/package.json +19 -0
  96. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +17 -0
  97. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +96 -0
  98. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +29 -0
  99. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +23 -0
  100. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +38 -0
  101. package/templates/skeleton/node-service/apps/web/tsconfig.json +14 -0
  102. package/templates/skeleton/node-service/eslint.config.mjs +20 -0
  103. package/templates/skeleton/node-service/gitignore +9 -0
  104. package/templates/skeleton/node-service/package.json +28 -0
  105. package/templates/skeleton/node-service/packages/core/package.json +11 -0
  106. package/templates/skeleton/node-service/packages/core/src/events.ts +14 -0
  107. package/templates/skeleton/node-service/packages/core/src/index.ts +15 -0
  108. package/templates/skeleton/node-service/packages/core/src/note.ts +69 -0
  109. package/templates/skeleton/node-service/packages/core/test/events.test.ts +23 -0
  110. package/templates/skeleton/node-service/packages/core/test/note.test.ts +101 -0
  111. package/templates/skeleton/node-service/packages/db/package.json +12 -0
  112. package/templates/skeleton/node-service/packages/db/src/index.ts +1 -0
  113. package/templates/skeleton/node-service/packages/db/src/note-store.ts +63 -0
  114. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +80 -0
  115. package/templates/skeleton/node-service/packages/shared/package.json +11 -0
  116. package/templates/skeleton/node-service/packages/shared/src/env.ts +17 -0
  117. package/templates/skeleton/node-service/packages/shared/src/errors.ts +33 -0
  118. package/templates/skeleton/node-service/packages/shared/src/index.ts +3 -0
  119. package/templates/skeleton/node-service/packages/shared/src/logger.ts +20 -0
  120. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +26 -0
  121. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +28 -0
  122. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +19 -0
  123. package/templates/skeleton/node-service/pnpm-lock.yaml +2399 -0
  124. package/templates/skeleton/node-service/pnpm-workspace.yaml +13 -0
  125. package/templates/skeleton/node-service/services/api/package.json +17 -0
  126. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +23 -0
  127. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +40 -0
  128. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +23 -0
  129. package/templates/skeleton/node-service/services/api/src/main.ts +47 -0
  130. package/templates/skeleton/node-service/services/api/src/server.ts +89 -0
  131. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +30 -0
  132. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +14 -0
  133. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +64 -0
  134. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +43 -0
  135. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +48 -0
  136. package/templates/skeleton/node-service/services/api/test/server.test.ts +123 -0
  137. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +32 -0
  138. package/templates/skeleton/node-service/services/worker/package.json +16 -0
  139. package/templates/skeleton/node-service/services/worker/src/main.ts +28 -0
  140. package/templates/skeleton/node-service/services/worker/src/spool.ts +60 -0
  141. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +38 -0
  142. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +34 -0
  143. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +76 -0
  144. package/templates/skeleton/node-service/tsconfig.base.json +15 -0
  145. package/templates/skeleton/node-service/tsconfig.json +13 -0
  146. 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,9 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ cdk.out/
5
+ *.tsbuildinfo
6
+ .DS_Store
7
+ .next/
8
+ out/
9
+ next-env.d.ts
@@ -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
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "app": "npx tsx bin/app.ts"
3
+ }