create-agent-rig 0.9.1 → 0.10.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/CHANGELOG.md +419 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +472 -90
- package/packages/cli/dist/index.js +651 -46
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +8 -4
- package/templates/release-ledger.json +2 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// PreToolUse hook: the web app is a consumer of the domain, not the backend.
|
|
2
|
-
// `apps/web` may import the pure core and shared utilities — never the storage
|
|
3
|
-
// layer (`…/db`) and never the services. Enforced at the tool layer, same as
|
|
4
|
-
// core purity: best-effort text scan, failing safe toward a false block.
|
|
5
|
-
//
|
|
6
|
-
// Contract (Claude Code and Codex): JSON on stdin; exit 0 = allow, exit 2 = block, and
|
|
7
|
-
// stderr is shown to the agent as the reason.
|
|
8
|
-
// Generator-owned coverage for the neutral bounded-inspection refusal lives upstream in
|
|
9
|
-
// codex.test.ts (absent in a generated rig) › "$guard blocks with a neutral,
|
|
10
|
-
// actionable size-limit refusal" — a downstream edit requires a local replacement test.
|
|
11
|
-
import { editFragments } from './lib/edit-input.mjs';
|
|
12
|
-
import { readHookInput } from './lib/hook-input.mjs';
|
|
13
|
-
|
|
14
|
-
const WEB_PATH = /(^|\/)apps\/web\//;
|
|
15
|
-
const CODE_FILE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
16
|
-
|
|
17
|
-
// Workspace package names arrive rewritten to the project scope, so match the
|
|
18
|
-
// package *suffix* under any scope: @<anything>/db, @<anything>/api, …
|
|
19
|
-
const FORBIDDEN_WORKSPACE = /^@[^/]+\/(db|api|worker)$/;
|
|
20
|
-
const FORBIDDEN_RELATIVE = /(^|\/)(packages\/db|services)(\/|$)/;
|
|
21
|
-
|
|
22
|
-
function main() {
|
|
23
|
-
const input = readHookInput();
|
|
24
|
-
if (input === null) return 0; // unparseable payload: not ours to judge
|
|
25
|
-
const violations = [];
|
|
26
|
-
const importRe =
|
|
27
|
-
/(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)['"]([^'"]+)['"]/gm;
|
|
28
|
-
const fragments = editFragments(input);
|
|
29
|
-
const blocked = fragments.find(
|
|
30
|
-
({ inspectionRefusal, appliesToAll }) => appliesToAll && inspectionRefusal,
|
|
31
|
-
);
|
|
32
|
-
const globalRefusal = blocked?.inspectionRefusal;
|
|
33
|
-
if (globalRefusal) {
|
|
34
|
-
process.stderr.write(
|
|
35
|
-
`BLOCKED — cannot safely inspect this edit: ${globalRefusal}\n` +
|
|
36
|
-
// The remedy has to match the refusal: splitting cannot change a
|
|
37
|
-
// container shape, and a fixed line sent the agent into a retry loop
|
|
38
|
-
// on the one path it could not retry out of.
|
|
39
|
-
`${blocked.remedy ?? 'Split it into a smaller patch and retry.'}\n`,
|
|
40
|
-
);
|
|
41
|
-
return 2;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
for (const { filePath, fragment, inspectionRefusal } of fragments) {
|
|
45
|
-
if (!WEB_PATH.test(filePath) || !CODE_FILE.test(filePath)) continue;
|
|
46
|
-
if (inspectionRefusal) {
|
|
47
|
-
violations.push(`cannot safely inspect this move — ${inspectionRefusal}`);
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
for (const match of fragment.matchAll(importRe)) {
|
|
51
|
-
const spec = match[1];
|
|
52
|
-
if (FORBIDDEN_WORKSPACE.test(spec) || FORBIDDEN_RELATIVE.test(spec)) {
|
|
53
|
-
violations.push(spec);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (violations.length === 0) return 0;
|
|
58
|
-
|
|
59
|
-
process.stderr.write(
|
|
60
|
-
`BLOCKED — apps/web imports the domain (core, shared), never the backend:\n` +
|
|
61
|
-
violations.map((v) => ` - "${v}" crosses the web boundary`).join('\n') +
|
|
62
|
-
`\nThe web talks to services over HTTP only; storage stays behind the API ` +
|
|
63
|
-
`(see .claude/rules/architecture.md).\n`,
|
|
64
|
-
);
|
|
65
|
-
return 2;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
process.exit(main());
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Architecture — layers and boundaries
|
|
2
|
-
|
|
3
|
-
The layout is the same in every target; only the adapters at the edges change.
|
|
4
|
-
|
|
5
|
-
## Layers
|
|
6
|
-
|
|
7
|
-
| Layer | Location | May depend on | Never contains |
|
|
8
|
-
| --- | --- | --- | --- |
|
|
9
|
-
| **core** | `packages/core/` | nothing (plus its schema library) | I/O, clock, randomness, environment, SDKs |
|
|
10
|
-
| **shared** | `packages/shared/` | nothing external of note | domain logic |
|
|
11
|
-
| **db** | `packages/db/` | core, shared | HTTP handling, business decisions |
|
|
12
|
-
| **services** | `services/*` | core, db, shared | direct SDK/storage access |
|
|
13
|
-
| **web** | `apps/web/` | core, shared | db, services, any storage or backend SDK |
|
|
14
|
-
|
|
15
|
-
Dependency direction is one-way: `services → (core, db, shared)`, `db → (core, shared)`,
|
|
16
|
-
`core → nothing`. A dependency pointing the other way is a defect, not a style choice.
|
|
17
|
-
|
|
18
|
-
## The web boundary is mechanical too
|
|
19
|
-
|
|
20
|
-
`apps/web` is a consumer of the domain, not the backend: it imports the pure
|
|
21
|
-
core (schemas, domain functions — the same validation the server trusts) and
|
|
22
|
-
shared utilities, and talks to services **over HTTP only**. It never imports
|
|
23
|
-
the storage layer or a service module — the `guard-web-boundary` hook refuses
|
|
24
|
-
such an edit at the tool layer, exactly like core purity. The payoff is that
|
|
25
|
-
one shared domain function validates on both sides of the wire: client-side
|
|
26
|
-
for instant feedback, server-side for trust.
|
|
27
|
-
|
|
28
|
-
**The web layer's test contract** (so "TDD, without exception" is read
|
|
29
|
-
correctly here): its tests are the **shared-validation suite** — the same core
|
|
30
|
-
function must accept/reject identically on both sides — plus the production
|
|
31
|
-
build and typecheck. Component-level testing (DOM rendering, interaction
|
|
32
|
-
frameworks) is deliberately out of scope; do **not** introduce a component
|
|
33
|
-
testing apparatus unless real work in this project shows the need. New web
|
|
34
|
-
*logic* still starts with a failing test — put the logic in a plain module
|
|
35
|
-
(like `lib/validate`) and test it directly.
|
|
36
|
-
|
|
37
|
-
## The request path is fixed
|
|
38
|
-
|
|
39
|
-
Every operation travels the same route, with no shortcuts:
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
payload → handler → usecase → model
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
- **Handlers** translate transport into schema-validated, typed input and back.
|
|
46
|
-
Nothing else.
|
|
47
|
-
- **The usecase layer is mandatory.** Every business operation has exactly one
|
|
48
|
-
usecase function. Handlers never call models or SDKs directly — even for a
|
|
49
|
-
"trivial" read. The uniformity is the point: it is what makes the codebase
|
|
50
|
-
predictable for both humans and agents.
|
|
51
|
-
- **Usecases receive their dependencies** (models, publishers, clock, id
|
|
52
|
-
generation) as arguments, and invoke the core's pure domain functions
|
|
53
|
-
themselves. That is what keeps the core pure and the tests fast.
|
|
54
|
-
- A dedicated *service* layer between usecase and model is **deliberately
|
|
55
|
-
absent** from the minimal skeleton: today a usecase is one domain function
|
|
56
|
-
plus one model call. Introduce a service only when a usecase outgrows that —
|
|
57
|
-
and then state it in this file, so the chain stays written down in one place.
|
|
58
|
-
|
|
59
|
-
## The core is pure — and the rule is mechanical
|
|
60
|
-
|
|
61
|
-
`packages/core/src/` contains domain logic only: pure functions and schemas.
|
|
62
|
-
No I/O, no clock, no randomness, no environment, no SDK. Values like "now" and
|
|
63
|
-
"a new id" enter as arguments from the usecase layer.
|
|
64
|
-
|
|
65
|
-
This is not a convention you are trusted to follow; the
|
|
66
|
-
`guard-core-purity` hook refuses the edit at the tool layer. If the hook blocks
|
|
67
|
-
you, the answer is to move the impure part out — never to look for a way around
|
|
68
|
-
the hook.
|
|
69
|
-
|
|
70
|
-
Precision about the claim, so it is never overstated: the guard is a
|
|
71
|
-
**best-effort text scan of each edit fragment before it lands** — an `Edit`
|
|
72
|
-
shows the hook its new text, not the whole resulting file. It stops the normal
|
|
73
|
-
path cold; a determined evasion is caught by the layers behind it (review and
|
|
74
|
-
tests), not by the hook. A rulebook that sells enforcement must describe its
|
|
75
|
-
enforcement exactly.
|
|
76
|
-
|
|
77
|
-
## Storage has exactly one owner
|
|
78
|
-
|
|
79
|
-
`packages/db/` is the only place that touches the storage SDK/driver. Every
|
|
80
|
-
other external SDK likewise gets exactly one owning module. If you need a second
|
|
81
|
-
place, you actually need a function exported from the first place.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
# The default token is read-only for this job: CI needs nothing else, and a
|
|
4
|
-
# workflow that can write is a workflow a compromised dependency can write with.
|
|
5
|
-
permissions:
|
|
6
|
-
contents: read
|
|
7
|
-
|
|
8
|
-
on:
|
|
9
|
-
push:
|
|
10
|
-
pull_request:
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
ci:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
- uses: pnpm/action-setup@v4
|
|
18
|
-
with:
|
|
19
|
-
version: 11
|
|
20
|
-
- uses: actions/setup-node@v4
|
|
21
|
-
with:
|
|
22
|
-
node-version: 22
|
|
23
|
-
cache: pnpm
|
|
24
|
-
- run: pnpm install --frozen-lockfile
|
|
25
|
-
- run: pnpm lint
|
|
26
|
-
- run: pnpm typecheck
|
|
27
|
-
- run: pnpm test
|
|
28
|
-
- run: pnpm synth
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
name: Deploy (dev)
|
|
2
|
-
|
|
3
|
-
# DEV deployment only. Production is a HUMAN step (see README) — the Never tier
|
|
4
|
-
# (.claude/rules/autonomy.md) forbids an agent from triggering a production
|
|
5
|
-
# deploy, so no production path exists in this workflow at all.
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
branches: [main, master]
|
|
9
|
-
workflow_dispatch:
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
# OIDC: assume a short-lived federated role. No long-lived keys in secrets —
|
|
13
|
-
# a starter multiplies whatever it ships, so it ships zero static credentials.
|
|
14
|
-
id-token: write
|
|
15
|
-
contents: read
|
|
16
|
-
|
|
17
|
-
jobs:
|
|
18
|
-
deploy:
|
|
19
|
-
runs-on: ubuntu-latest
|
|
20
|
-
steps:
|
|
21
|
-
- uses: actions/checkout@v4
|
|
22
|
-
|
|
23
|
-
# Degrade cleanly: with no configured role this SKIPS with a message and
|
|
24
|
-
# the job stays green — never a red X on a fresh project with no cloud
|
|
25
|
-
# account. Enabling deployment is "add the AWS_DEPLOY_ROLE_ARN secret",
|
|
26
|
-
# not "edit this file".
|
|
27
|
-
- id: guard
|
|
28
|
-
env:
|
|
29
|
-
ROLE: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
30
|
-
run: |
|
|
31
|
-
if [ -z "$ROLE" ]; then
|
|
32
|
-
echo "deploy skipped: no credentials configured — see README (Deploy)."
|
|
33
|
-
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
37
|
-
uses: pnpm/action-setup@v4
|
|
38
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
39
|
-
uses: actions/setup-node@v4
|
|
40
|
-
with:
|
|
41
|
-
node-version: 22
|
|
42
|
-
cache: pnpm
|
|
43
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
44
|
-
run: pnpm install --frozen-lockfile
|
|
45
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
46
|
-
run: pnpm build:web
|
|
47
|
-
env:
|
|
48
|
-
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
|
|
49
|
-
|
|
50
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
51
|
-
uses: aws-actions/configure-aws-credentials@v4
|
|
52
|
-
with:
|
|
53
|
-
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
54
|
-
aws-region: ${{ vars.AWS_REGION || '__REGION__' }}
|
|
55
|
-
|
|
56
|
-
# `--require-approval never` is deliberate and dev-only: this workflow has
|
|
57
|
-
# no production path at all (see the header), so the approval prompt would
|
|
58
|
-
# only ever block an unattended dev deploy on a change nobody is watching.
|
|
59
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
60
|
-
run: >
|
|
61
|
-
pnpm --filter @app/infra exec cdk deploy AppStack WebStack
|
|
62
|
-
--require-approval never --outputs-file cdk-outputs.json
|
|
63
|
-
|
|
64
|
-
# The bundle is built above and the bucket is created by the deploy — but
|
|
65
|
-
# nothing puts one into the other, so without this step CloudFront keeps
|
|
66
|
-
# serving whatever was uploaded by hand, last time somebody remembered.
|
|
67
|
-
# `web-stack.ts` deliberately does not use BucketDeployment (synth must
|
|
68
|
-
# never depend on `next build` having run), which is why the upload lives
|
|
69
|
-
# here rather than in the stack.
|
|
70
|
-
- if: steps.guard.outputs.skip != 'true'
|
|
71
|
-
name: Upload the web bundle and invalidate the edge cache
|
|
72
|
-
run: |
|
|
73
|
-
# `jq -r` prints the string "null" and exits 0 for a missing key, and
|
|
74
|
-
# GitHub's shell is `bash -e` with no `-u` and no pipefail — so a
|
|
75
|
-
# renamed stack or a half-finished deploy would sync the bundle into
|
|
76
|
-
# `s3://null`, a real bucket in a global namespace someone else owns,
|
|
77
|
-
# and issue --delete against it. `-e` makes jq exit non-zero instead.
|
|
78
|
-
set -euo pipefail
|
|
79
|
-
BUCKET=$(jq -er '.WebStack.WebBucketName' infra/cdk-outputs.json)
|
|
80
|
-
DISTRIBUTION=$(jq -er '.WebStack.WebDistributionId' infra/cdk-outputs.json)
|
|
81
|
-
# An existing-but-empty bundle syncs zero objects, and --delete then
|
|
82
|
-
# empties the live site with a green checkmark.
|
|
83
|
-
[ -n "$(ls -A apps/web/out 2>/dev/null)" ] || {
|
|
84
|
-
echo "apps/web/out is empty — refusing to sync"; exit 1;
|
|
85
|
-
}
|
|
86
|
-
# --delete makes the bucket the bundle's territory alone: anything not
|
|
87
|
-
# in this build is removed from it.
|
|
88
|
-
aws s3 sync apps/web/out "s3://$BUCKET" --delete
|
|
89
|
-
aws cloudfront create-invalidation \
|
|
90
|
-
--distribution-id "$DISTRIBUTION" --paths '/*'
|
|
@@ -1,181 +0,0 @@
|
|
|
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
|
-
### Dev — automated, on push (OIDC, no static keys)
|
|
40
|
-
|
|
41
|
-
`.github/workflows/deploy.yml` runs a **dev** deploy on every push to the
|
|
42
|
-
default branch. It ships with **no credentials**, so on a fresh repo it skips
|
|
43
|
-
cleanly with a message — never a red X. To enable it, you *add a secret*, you
|
|
44
|
-
do not edit the workflow:
|
|
45
|
-
|
|
46
|
-
1. In AWS, create an IAM role your repo can assume via GitHub's OIDC provider
|
|
47
|
-
(`token.actions.githubusercontent.com`) — a short-lived federated role, no
|
|
48
|
-
long-lived access keys anywhere. Beyond what `cdk deploy` needs, the
|
|
49
|
-
workflow uploads the bundle itself, so the role also needs
|
|
50
|
-
**`s3:ListBucket`, `s3:PutObject`, `s3:DeleteObject`** on the web bucket and
|
|
51
|
-
**`cloudfront:CreateInvalidation`** on the distribution. A role scoped only
|
|
52
|
-
to CDK's bootstrap roles gets through the deploy and fails on the upload —
|
|
53
|
-
after both stacks are already up.
|
|
54
|
-
2. Add its ARN as the repository secret **`AWS_DEPLOY_ROLE_ARN`**, and set the
|
|
55
|
-
repo variable **`API_URL`** to the deployed API's URL (the `ApiUrl` output
|
|
56
|
-
of a first deploy). It becomes `NEXT_PUBLIC_API_URL`, which Next **inlines
|
|
57
|
-
into the bundle at build time** — unset, the site calls `/notes` on its own
|
|
58
|
-
CloudFront domain, where nothing answers. `AWS_REGION` is genuinely
|
|
59
|
-
optional. The first deploy is therefore two passes: deploy, read `ApiUrl`
|
|
60
|
-
from the job's `cdk deploy` Outputs, set the variable, then re-run the
|
|
61
|
-
workflow (push, or **Run workflow** — it is `workflow_dispatch`-enabled).
|
|
62
|
-
|
|
63
|
-
The workflow then assumes the role, builds the web bundle, runs
|
|
64
|
-
`cdk deploy AppStack WebStack --outputs-file`, then **uploads `apps/web/out` to
|
|
65
|
-
the web bucket (`aws s3 sync --delete`) and invalidates the CloudFront cache**,
|
|
66
|
-
reading both destinations from the stack outputs. You do not sync anything by
|
|
67
|
-
hand on this path. The site is served from S3 + CloudFront (the `WebUrl`
|
|
68
|
-
output).
|
|
69
|
-
|
|
70
|
-
### Local / manual
|
|
71
|
-
|
|
72
|
-
```sh
|
|
73
|
-
# needs AWS credentials; region comes from your profile (generator default: __REGION__)
|
|
74
|
-
cd infra
|
|
75
|
-
npx cdk bootstrap # first time per account/region
|
|
76
|
-
npx cdk deploy AppStack WebStack --outputs-file cdk-outputs.json
|
|
77
|
-
# --delete makes the bucket the bundle's territory alone, so a stale or missing
|
|
78
|
-
# `out/` would empty the live site — or restore last month's. The workflow
|
|
79
|
-
# builds two steps before its sync; by hand, build here.
|
|
80
|
-
# Read the outputs into variables FIRST. `jq -er` exits non-zero on a missing
|
|
81
|
-
# key, but a command substitution inside an assignment or an argument throws
|
|
82
|
-
# that status away — which is how a stale outputs file becomes a bundle built
|
|
83
|
-
# against `null` and an `aws s3 sync … s3://null --delete`.
|
|
84
|
-
API=$(jq -er '.AppStack.ApiUrl' cdk-outputs.json) || { echo "no ApiUrl"; exit 1; }
|
|
85
|
-
BUCKET=$(jq -er '.WebStack.WebBucketName' cdk-outputs.json) || { echo "no WebBucketName"; exit 1; }
|
|
86
|
-
DIST=$(jq -er '.WebStack.WebDistributionId' cdk-outputs.json) || { echo "no WebDistributionId"; exit 1; }
|
|
87
|
-
|
|
88
|
-
# NEXT_PUBLIC_API_URL is inlined at build time: without it the bundle calls its
|
|
89
|
-
# own CloudFront domain instead of the API, and the sync below makes that live.
|
|
90
|
-
(cd .. && NEXT_PUBLIC_API_URL="$API" pnpm build:web)
|
|
91
|
-
[ -f ../apps/web/out/index.html ] || { echo "no web bundle — build failed"; exit 1; }
|
|
92
|
-
aws s3 sync ../apps/web/out "s3://$BUCKET" --delete
|
|
93
|
-
# a synced bucket whose distribution still serves the old objects has not
|
|
94
|
-
# deployed — invalidate, or you are looking at the previous build
|
|
95
|
-
aws cloudfront create-invalidation --paths '/*' --distribution-id "$DIST"
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Production — a human step, on purpose
|
|
99
|
-
|
|
100
|
-
There is **no production deploy in this repo**, by design: the agent operating
|
|
101
|
-
system's Never tier forbids an agent from triggering a production deploy, so
|
|
102
|
-
shipping an automated prod path would contradict the rules on day one.
|
|
103
|
-
Promote to production yourself — a separate account/role, a reviewed change,
|
|
104
|
-
your own approval — reusing the dev workflow's OIDC pattern.
|
|
105
|
-
|
|
106
|
-
## Verify runtime health (CI-green ≠ runtime-healthy)
|
|
107
|
-
|
|
108
|
-
After every deploy:
|
|
109
|
-
|
|
110
|
-
```sh
|
|
111
|
-
# From the deploy's outputs file — run this from `infra/`, where the manual
|
|
112
|
-
# section leaves you. Deployed through CI instead? That file is written on the
|
|
113
|
-
# runner and never lands here: take both values from the job's `cdk deploy`
|
|
114
|
-
# Outputs, or re-run `cdk deploy … --outputs-file cdk-outputs.json` locally.
|
|
115
|
-
API_URL=$(jq -er '.AppStack.ApiUrl' cdk-outputs.json)
|
|
116
|
-
WEB_URL=$(jq -er '.WebStack.WebUrl' cdk-outputs.json)
|
|
117
|
-
|
|
118
|
-
curl -s -X POST "$API_URL/notes" \
|
|
119
|
-
-H 'content-type: application/json' \
|
|
120
|
-
-d '{"title":"smoke test","tags":["deploy"]}'
|
|
121
|
-
# expect: HTTP 201 with { "note": { … } }
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**That checks the API, not the site.** The two fail independently: a bundle
|
|
125
|
-
built without `NEXT_PUBLIC_API_URL` calls its own CloudFront domain, and a
|
|
126
|
-
misnamed origin is refused by the browser — neither is visible to a `curl` that
|
|
127
|
-
sends no `Origin` header. So also open the `WebUrl` output, create a note in
|
|
128
|
-
the form, and reload:
|
|
129
|
-
|
|
130
|
-
```sh
|
|
131
|
-
curl -s -I "$WEB_URL" | head -1 # the bundle is served
|
|
132
|
-
|
|
133
|
-
# API Gateway answers a preflight 204 whether or not the origin matched, so the
|
|
134
|
-
# status code proves nothing here. The browser gates on the echoed header —
|
|
135
|
-
# that is what to look for. `content-type` is sent because the real call uses
|
|
136
|
-
# it, and a non-safelisted header is what makes `allowHeaders` matter.
|
|
137
|
-
curl -s -X OPTIONS "$API_URL/notes" -D - -o /dev/null \
|
|
138
|
-
-H "Origin: $WEB_URL" \
|
|
139
|
-
-H 'access-control-request-method: POST' \
|
|
140
|
-
-H 'access-control-request-headers: content-type' \
|
|
141
|
-
| grep -i '^access-control-allow-origin:'
|
|
142
|
-
# expect: a line echoing $WEB_URL. No line = the browser will refuse the call,
|
|
143
|
-
# whatever the status code said.
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Then confirm the pipeline: the worker logs `note.created processed`, and the
|
|
147
|
-
**DLQ alarm stays quiet**. If the smoke test regresses: `npx cdk deploy` the
|
|
148
|
-
previous revision (or `git revert` and redeploy) **first**, diagnose second.
|
|
149
|
-
|
|
150
|
-
## Boundaries (the short version)
|
|
151
|
-
|
|
152
|
-
- Every request follows `payload → handler → usecase → model`. The usecase
|
|
153
|
-
layer is mandatory even when it looks like ceremony.
|
|
154
|
-
- `packages/core` is pure — the `guard-core-purity` hook refuses I/O, clock,
|
|
155
|
-
randomness, and environment access at the tool layer.
|
|
156
|
-
- `packages/db` is the only module that touches the storage SDK.
|
|
157
|
-
- A failing queue message is poison: it throws, SQS retries ×3, the DLQ alarm
|
|
158
|
-
fires. Never wrap the worker in a broad catch.
|
|
159
|
-
- **Creating a note is a dual write, and it is not atomic.** `create-note`
|
|
160
|
-
puts to DynamoDB and then publishes to SQS. If the publish fails, the note
|
|
161
|
-
is stored and its event never happened — the worker never runs, and nothing
|
|
162
|
-
compensates; the caller gets a 500 for a note that exists. That is a
|
|
163
|
-
deliberate simplification for a starter. When it starts to matter, the two
|
|
164
|
-
ways out are an **outbox** (write the event alongside the note, relay it
|
|
165
|
-
afterwards) or DynamoDB Streams feeding the worker, which deletes the second
|
|
166
|
-
write instead of coordinating it.
|
|
167
|
-
- **CORS names who may call the API, and that half is wired for you.**
|
|
168
|
-
`bin/app.ts` builds `WebStack` first and passes its CloudFront origin to
|
|
169
|
-
`AppStack`, so the API allows the deployed site without you configuring it.
|
|
170
|
-
The other half is not automatic: the bundle has to know where the API *is*,
|
|
171
|
-
and that is `NEXT_PUBLIC_API_URL` above. For a custom domain or a second
|
|
172
|
-
origin, deploy with `-c allowedOrigins=https://app.example.com` (comma-
|
|
173
|
-
separated for several) — the entrypoint prefers the flag over the wired
|
|
174
|
-
default, and the cross-stack export disappears with it. An `allowedOrigins`
|
|
175
|
-
that parses to nothing, or an entry no browser could send, is refused at
|
|
176
|
-
synth rather than deployed as an API nobody can call. `*` is not the default
|
|
177
|
-
and never will be, but the flag does take it: you get a warning naming the
|
|
178
|
-
consequence, not a refusal, because the alternative is people editing the
|
|
179
|
-
stack.
|
|
180
|
-
|
|
181
|
-
See `.claude/rules/architecture.md` for the full rules.
|
|
@@ -1,17 +0,0 @@
|
|
|
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;
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
// In THIS target the API is never same-origin: the bundle is served from
|
|
6
|
-
// CloudFront and the API is API Gateway, so `NEXT_PUBLIC_API_URL` has to be set
|
|
7
|
-
// at build time — Next inlines it, and an unset one leaves the empty string
|
|
8
|
-
// here, which sends every call to whatever is serving this page.
|
|
9
|
-
//
|
|
10
|
-
// The `?? ''` is a fallback to nothing, not to a working default: there is no
|
|
11
|
-
// dev proxy in this project, so `pnpm --filter @app/web dev` needs the variable
|
|
12
|
-
// too. It exists only so a missing value is a relative URL rather than the
|
|
13
|
-
// string "undefined" in a request path.
|
|
14
|
-
const base = process.env.NEXT_PUBLIC_API_URL ?? '';
|
|
15
|
-
|
|
16
|
-
export async function createNote(input: { title: string; tags: string[] }): Promise<Note> {
|
|
17
|
-
const response = await fetch(`${base}/notes`, {
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers: { 'content-type': 'application/json' },
|
|
20
|
-
body: JSON.stringify(input),
|
|
21
|
-
});
|
|
22
|
-
const body = (await response.json()) as { note?: Note; error?: string; issues?: string[] };
|
|
23
|
-
if (!response.ok || !body.note) {
|
|
24
|
-
throw new Error(body.issues?.join('; ') ?? body.error ?? `HTTP ${response.status}`);
|
|
25
|
-
}
|
|
26
|
-
return body.note;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function listNotes(): Promise<Note[]> {
|
|
30
|
-
const response = await fetch(`${base}/notes`);
|
|
31
|
-
const body = (await response.json()) as { notes?: Note[]; error?: string };
|
|
32
|
-
if (!response.ok || !body.notes) {
|
|
33
|
-
throw new Error(body.error ?? `HTTP ${response.status}`);
|
|
34
|
-
}
|
|
35
|
-
return body.notes;
|
|
36
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|