create-lumen 2.0.0-alpha.0 → 2.0.0-alpha.1

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/README.md CHANGED
@@ -72,7 +72,7 @@ create-lumen my-app # after npm link
72
72
  - Optional testing: Vitest or Jest
73
73
  - Linting: ESLint (default), Oxlint, or Biome
74
74
  - Code formatter: Prettier or Oxfmt (chosen after the linter; `eslint-config-prettier` is auto-wired for the ESLint + Prettier combo)
75
- - Manifest v2: Emits a deterministic `lumen.config.json` validated against JSON Schema Draft 2020-12
75
+ - Manifest v2: Emits a deterministic `lumen.config.json` (optional but recommended) validated against JSON Schema Draft 2020-12, including architecture validation modes and harness command configuration.
76
76
  - Explicit path mapping: Target directories declared for `lumen-cli` code generation
77
77
  - Optional Axios setup and icon libraries
78
78
  - Auto `git init`, generated `README` (with a project description reflecting the chosen tooling) and `LICENSE`
@@ -187,15 +187,30 @@ src/
187
187
 
188
188
  `create-lumen` is part of the Lumen ecosystem alongside **`lumen-cli`**:
189
189
 
190
- - **`create-lumen` (this repo):** The bootstrap scaffolder that initializes project repositories and writes an explicit, strictly validated `lumen.config.json` manifest (`manifestVersion: 2`) into the project root.
191
- - **`lumen-cli`:** The companion developer CLI that reads `lumen.config.json` to generate components, features, hooks, and services at the paths declared in `manifest.paths`.
190
+ - **`create-lumen` (this repo):** The bootstrap scaffolder that initializes project repositories and writes an explicit, strictly validated `lumen.config.json` manifest (`manifestVersion: 2`) into the project root. The manifest is **optional but highly recommended** for full editor integration and agent interoperability. If omitted, `lumen-cli` will attempt to infer a basic configuration, but advanced features (like configurable harness and architecture validation) require the manifest.
191
+ - **`lumen-cli`:** The companion developer CLI that reads `lumen.config.json` to generate components, features, hooks, services, and run diagnostics (harness and doctor) at the paths declared in `manifest.paths`. It also provides commands to inspect and update the manifest.
192
+
193
+ ### Manifest v2 Schema (v2.0.0-alpha.1+)
194
+
195
+ The manifest now includes:
196
+ - **`architecture.preset`**: The base architecture (`feature-based`, `type-based`, `hybrid`, `none`).
197
+ - **`architecture.validation`**: Validation mode for `lumen doctor` (`strict`, `relaxed`, `none`).
198
+ - **`harness.commands`**: An array of commands to run with `lumen harness` (e.g., lint, format, types, test, build), each with a name, shell command, required flag, and description.
192
199
 
193
200
  ### Ecosystem Documentation & Contracts
194
201
 
195
- - **[Manifest v2 Overview](./docs/manifest-v2.md):** Specification of the nested schema and validation behavior.
196
- - **[Shared Contract: `create-lumen` ↔ `lumen-cli`](./docs/contracts/manifest-v2-contract.md):** The agreed contract for path mapping, barrel conventions, and version negotiation.
202
+ - **[Manifest v2 Overview](./docs/manifest-v2.md):** Specification of the nested schema, validation behavior, and the new harness and architecture validation fields.
203
+ - **[Shared Contract: `create-lumen` ↔ `lumen-cli`](./docs/contracts/manifest-v2-contract.md):** The agreed contract for path mapping, barrel conventions, version negotiation, and the harness/configuration interface.
197
204
  - **[v1 to v2 Migration Guide](./docs/migration/v1-to-v2.md):** Complete guide for migrating generated projects and legacy flat configs to v2.
198
205
 
206
+ ### Agent Interoperability
207
+
208
+ Lumen is designed for both people and agents. When `agentDocs: true` is set in the manifest (or via the `--agent-docs` flag), `create-lumen` generates a `.lumen/` directory in the project root containing:
209
+ - `project.json`: The resolved manifest.
210
+ - `architecture.json`: The effective architecture preset and validation mode.
211
+ - `conventions.md`: A markdown file documenting project-specific conventions (e.g., component naming, file organization).
212
+ These files enable agents to understand the project without parsing the source code.
213
+
199
214
  ## Roadmap & Planning
200
215
 
201
216
  - **Changelog:** Notable changes are documented in [`docs/CHANGELOG.md`](./docs/CHANGELOG.md) (Keep a Changelog format).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumen",
3
- "version": "2.0.0-alpha.0",
3
+ "version": "2.0.0-alpha.1",
4
4
  "description": "Scaffold a React + Vite project with architecture choice: feature-based or type-based.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "register.js",
16
16
  "src/",
17
17
  "templates/",
18
+ "skills/",
18
19
  "schema/",
19
20
  "README.md",
20
21
  "LICENSE"
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "type": "type-based",
14
+ "validation": "strict"
15
+ },
16
+ "ui": {
17
+ "kit": "none"
18
+ },
19
+ "docs": {
20
+ "language": "en"
21
+ },
22
+ "paths": {
23
+ "features": "src/features",
24
+ "components": "src/shared/components",
25
+ "services": "src/shared/services",
26
+ "hooks": "src/shared/hooks",
27
+ "pages": "src/app/router",
28
+ "ui": "src/shared/components/ui"
29
+ },
30
+ "tooling": {
31
+ "language": "ts",
32
+ "linter": "eslint",
33
+ "formatter": "prettier"
34
+ }
35
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "validation": "strict"
14
+ },
15
+ "harness": {
16
+ "commands": []
17
+ },
18
+ "ui": {
19
+ "kit": "none"
20
+ },
21
+ "docs": {
22
+ "language": "en"
23
+ },
24
+ "paths": {
25
+ "features": "src/features",
26
+ "components": "src/shared/components",
27
+ "services": "src/shared/services",
28
+ "hooks": "src/shared/hooks",
29
+ "pages": "src/app/router",
30
+ "ui": "src/shared/components/ui"
31
+ },
32
+ "tooling": {
33
+ "language": "ts",
34
+ "linter": "eslint",
35
+ "formatter": "prettier"
36
+ }
37
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "validation": "strict"
14
+ },
15
+ "harness": {
16
+ "commands": [
17
+ { "name": "lint", "required": true },
18
+ { "name": "test", "command": "vitest run", "required": true },
19
+ { "name": "build", "command": "npm run build", "required": true }
20
+ ]
21
+ },
22
+ "ui": {
23
+ "kit": "none"
24
+ },
25
+ "docs": {
26
+ "language": "en"
27
+ },
28
+ "paths": {
29
+ "features": "src/features",
30
+ "components": "src/shared/components",
31
+ "services": "src/shared/services",
32
+ "hooks": "src/shared/hooks",
33
+ "pages": "src/app/router",
34
+ "ui": "src/shared/components/ui"
35
+ },
36
+ "tooling": {
37
+ "language": "ts",
38
+ "linter": "eslint",
39
+ "formatter": "prettier"
40
+ }
41
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "validation": "super-strict"
14
+ },
15
+ "ui": {
16
+ "kit": "none"
17
+ },
18
+ "docs": {
19
+ "language": "en"
20
+ },
21
+ "paths": {
22
+ "features": "src/features",
23
+ "components": "src/shared/components",
24
+ "services": "src/shared/services",
25
+ "hooks": "src/shared/hooks",
26
+ "pages": "src/app/router",
27
+ "ui": "src/shared/components/ui"
28
+ },
29
+ "tooling": {
30
+ "language": "ts",
31
+ "linter": "eslint",
32
+ "formatter": "prettier"
33
+ }
34
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "type": "feature-based",
14
+ "validation": "strict"
15
+ },
16
+ "harness": {
17
+ "commands": [
18
+ { "name": "lint", "command": "oxlint .", "required": true },
19
+ { "name": "format", "command": "oxfmt --check .", "required": true },
20
+ { "name": "types", "command": "tsc -b", "required": true },
21
+ { "name": "test", "command": "vitest run", "required": true },
22
+ { "name": "build", "command": "npm run build", "required": true }
23
+ ]
24
+ },
25
+ "ui": {
26
+ "kit": "none"
27
+ },
28
+ "docs": {
29
+ "language": "en"
30
+ },
31
+ "paths": {
32
+ "features": "src/features",
33
+ "components": "src/shared/components",
34
+ "services": "src/shared/services",
35
+ "hooks": "src/shared/hooks",
36
+ "pages": "src/app/router",
37
+ "ui": "src/shared/components/ui"
38
+ },
39
+ "tooling": {
40
+ "language": "ts",
41
+ "linter": "eslint",
42
+ "formatter": "prettier"
43
+ }
44
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "validation": "none"
14
+ },
15
+ "harness": {
16
+ "commands": [
17
+ { "name": "lint", "command": "oxlint .", "required": true },
18
+ { "name": "test", "command": "vitest run", "required": true },
19
+ { "name": "build", "command": "npm run build", "required": true }
20
+ ]
21
+ },
22
+ "ui": {
23
+ "kit": "none"
24
+ },
25
+ "docs": {
26
+ "language": "en"
27
+ },
28
+ "paths": {
29
+ "features": "src/features",
30
+ "components": "src/shared/components",
31
+ "services": "src/shared/services",
32
+ "hooks": "src/shared/hooks",
33
+ "pages": "src/app/router",
34
+ "ui": "src/shared/components/ui"
35
+ },
36
+ "tooling": {
37
+ "language": "ts",
38
+ "linter": "eslint",
39
+ "formatter": "prettier"
40
+ }
41
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
3
+ "manifestVersion": 2,
4
+ "framework": {
5
+ "name": "react",
6
+ "variant": "vite"
7
+ },
8
+ "styling": {
9
+ "engine": "tailwind"
10
+ },
11
+ "architecture": {
12
+ "preset": "feature-based",
13
+ "validation": "relaxed"
14
+ },
15
+ "harness": {
16
+ "commands": [
17
+ { "name": "lint", "command": "oxlint .", "required": true },
18
+ { "name": "test", "command": "vitest run", "required": true },
19
+ { "name": "build", "command": "npm run build", "required": true }
20
+ ]
21
+ },
22
+ "ui": {
23
+ "kit": "none"
24
+ },
25
+ "docs": {
26
+ "language": "en"
27
+ },
28
+ "paths": {
29
+ "features": "src/features",
30
+ "components": "src/shared/components",
31
+ "services": "src/shared/services",
32
+ "hooks": "src/shared/hooks",
33
+ "pages": "src/app/router",
34
+ "ui": "src/shared/components/ui"
35
+ },
36
+ "tooling": {
37
+ "language": "ts",
38
+ "linter": "eslint",
39
+ "formatter": "prettier"
40
+ }
41
+ }
@@ -75,6 +75,15 @@
75
75
  "architecture": {
76
76
  "type": "object",
77
77
  "properties": {
78
+ "preset": {
79
+ "type": "string",
80
+ "enum": [
81
+ "feature-based",
82
+ "type-based",
83
+ "hybrid",
84
+ "none"
85
+ ]
86
+ },
78
87
  "type": {
79
88
  "type": "string",
80
89
  "enum": [
@@ -83,10 +92,54 @@
83
92
  "hybrid",
84
93
  "none"
85
94
  ]
95
+ },
96
+ "validation": {
97
+ "default": "strict",
98
+ "type": "string",
99
+ "enum": [
100
+ "strict",
101
+ "relaxed",
102
+ "none"
103
+ ]
104
+ }
105
+ },
106
+ "additionalProperties": false
107
+ },
108
+ "harness": {
109
+ "type": "object",
110
+ "properties": {
111
+ "commands": {
112
+ "minItems": 1,
113
+ "type": "array",
114
+ "items": {
115
+ "type": "object",
116
+ "properties": {
117
+ "name": {
118
+ "type": "string",
119
+ "minLength": 1
120
+ },
121
+ "command": {
122
+ "type": "string",
123
+ "minLength": 1
124
+ },
125
+ "required": {
126
+ "default": true,
127
+ "type": "boolean"
128
+ },
129
+ "description": {
130
+ "type": "string"
131
+ }
132
+ },
133
+ "required": [
134
+ "name",
135
+ "command"
136
+ ],
137
+ "additionalProperties": false
138
+ }
86
139
  }
87
140
  },
88
141
  "required": [
89
- "type"
142
+ "commands"
90
143
  ],
91
144
  "additionalProperties": false
92
145
  },
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: create-lumen
3
+ description: Scaffold and extend a lumen v2 project (manifestVersion 2). Read lumen.config.json, resolve generator paths via manifest.paths, respect feature barrels, @/* alias, harness commands, and validation modes.
4
+ ---
5
+
6
+ # create-lumen
7
+
8
+ ## When to use
9
+
10
+ Use this skill when the working directory contains (or should contain) a
11
+ `lumen.config.json` with `manifestVersion: 2`: scaffolding follow-ups,
12
+ `lumen g` generation, `lumen doctor` / `lumen harness` diagnostics.
13
+
14
+ Do NOT guess layout. Every target directory comes from the manifest.
15
+
16
+ ## 1. Read `lumen.config.json`
17
+
18
+ ```bash
19
+ cat lumen.config.json
20
+ ```
21
+
22
+ Required top-level keys: `manifestVersion: 2`, `framework`, `styling`,
23
+ `architecture`, `ui`, `docs`, `paths`, `tooling`, `harness`.
24
+ Source of truth: `src/manifest/schema.js` (`parseManifest`, Zod strict).
25
+ Full spec: `docs/contracts/manifest-v2-contract.md`, `docs/manifest-v2.md`.
26
+
27
+ If `manifestVersion` is missing / `framework` is a string → **v1 manifest**:
28
+ fail fast (exit 1), generate nothing, point to
29
+ `docs/migration/v1-to-v2.md`.
30
+
31
+ ## 2. Resolve paths via `manifest.paths` (never hardcode)
32
+
33
+ Generators MUST do `<root>/<manifest.paths.<key>>`. POSIX `/` separators.
34
+
35
+ | Framework | Preset | features | components | services | hooks | pages | ui |
36
+ |---|---|---|---|---|---|---|---|
37
+ | React+Vite | `feature-based` | `src/features` | `src/shared/components` | `src/shared/services` | `src/shared/hooks` | `src/app/router` | `src/shared/components/ui` |
38
+ | React+Vite | `type-based` | `src/features` | `src/components` | `src/services` | `src/hooks` | `src/pages` | `src/ui` |
39
+ | Next App | `feature-based`/`hybrid` | `src/features` | `src/shared/components` | `src/shared/services` | `src/shared/hooks` | `app` | `src/shared/components/ui` |
40
+ | Next Pages | `feature-based` | `src/features` | `src/shared/components` | `src/shared/services` | `src/shared/hooks` | `pages` | `src/shared/components/ui` |
41
+ | Any | `none` | `src` | `src` (`components/` for Next) | `src/services` | `src/hooks` | per framework | `src…/ui` |
42
+
43
+ Resolver: `src/manifest/paths.js` (`resolvePaths`). Fixtures:
44
+ `schema/fixtures/v2/valid/react-vite-feature-based.json`,
45
+ `full-kitchen-sink.json`.
46
+
47
+ ## 3. Feature structure + barrels
48
+
49
+ New feature at `<paths.features>/<name>/` (e.g. `src/features/billing`):
50
+
51
+ ```bash
52
+ lumen g feature billing
53
+ lumen g component billing/InvoiceCard --feature billing
54
+ lumen g hook useBilling --feature billing
55
+ lumen g service billing-api --feature billing
56
+ ```
57
+
58
+ Rules:
59
+
60
+ - Private subfolders: `components/ hooks/ layouts/ pages/ services/ store/ types/`.
61
+ - Public barrel required: `<feature>/index.ts` (or `.js` when
62
+ `tooling.language === "js"`). External code imports only the barrel:
63
+ `import { InvoiceCard } from "@/features/billing"`.
64
+ - Extensions: `.tsx`/`.jsx` for JSX components, `.ts`/`.js` for pure logic,
65
+ per `tooling.language`.
66
+
67
+ ## 4. Path alias `@/*`
68
+
69
+ `@/*` → `<root>/src/*` (guaranteed by `tsconfig` + Vite/Next config).
70
+ Feature-based: shared imports via `@/shared/...`.
71
+ Type-based: `@/components/...`, `@/hooks/...`, `@/services/...`.
72
+
73
+ ```ts
74
+ import { Button } from "@/shared/components/ui/button";
75
+ import { useBilling } from "@/features/billing";
76
+ ```
77
+
78
+ ## 5. `harness.commands` execution
79
+
80
+ `lumen harness` runs each entry in order; `required: true` (default) fails
81
+ the run on error, `required: false` warns and continues.
82
+
83
+ ```json
84
+ { "name": "lint", "command": "oxlint .", "required": true }
85
+ ```
86
+
87
+ Defaults: `lint (oxlint .)`, `format (oxfmt --check .)`, `types (tsc -b)`,
88
+ `test (vitest run)`, `build (npm run build)`.
89
+
90
+ ## 6. `architecture.validation` modes
91
+
92
+ | Mode | Behavior |
93
+ |---|---|
94
+ | `strict` | Enforce barrels, encapsulation, path rules; fail on violation. |
95
+ | `relaxed` | Warn on violation, still generate. |
96
+ | `none` | Skip architecture checks. |
97
+
98
+ Note: `architecture.preset` is canonical; legacy `type` is accepted as an
99
+ alias (`parseManifest` normalizes both). Valid presets: React →
100
+ `feature-based|type-based|none`; Next → `feature-based|hybrid|none`.
101
+
102
+ ## 7. Common errors — reject, don't patch around
103
+
104
+ - **v1 rejection:** flat config (`css`, string `framework`/`architecture`) →
105
+ exit 1 + migration pointer. Never partially generate.
106
+ - **`shadcn` → `tailwind`:** `ui.kit: "shadcn"` requires
107
+ `styling.engine: "tailwind"` (v4 CSS-first, no `tailwind.config.js`).
108
+ - **`bundler`/`adapter` Next-only:** `framework.bundler` (`turbopack|webpack`)
109
+ and `adapter` (`node|vercel|cloudflare|static`) are prohibited on `react`.
110
+ `react` variant MUST be `vite`; `next` MUST be `app-router|pages-router`.
@@ -0,0 +1,9 @@
1
+ # manifest-v2 pointer
2
+
3
+ Canonical spec lives in `docs/` — do not duplicate it here.
4
+
5
+ - Contract: `docs/contracts/manifest-v2-contract.md`
6
+ - Shape / v1→v2 diff: `docs/manifest-v2.md`
7
+ - Zod source of truth: `src/manifest/schema.js`
8
+ - Path resolver: `src/manifest/paths.js`
9
+ - Valid fixtures: `schema/fixtures/v2/valid/`
@@ -0,0 +1,114 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildManifest } from "./manifest/emit.js";
4
+
5
+ const VALIDATION_NOTES = {
6
+ strict: "strict: architecture boundaries are enforced — do not import across features except via barrels or shared paths; violations must be fixed.",
7
+ relaxed: "relaxed: architecture boundaries are advisory — prefer barrels and shared paths, but cross-imports only warn.",
8
+ none: "none: no architecture validation — paths are still the canonical locations.",
9
+ };
10
+
11
+ function resolveManifest(manifestOrResponses) {
12
+ if (manifestOrResponses && typeof manifestOrResponses.manifestVersion !== "undefined") {
13
+ return manifestOrResponses;
14
+ }
15
+ if (manifestOrResponses && manifestOrResponses._manifestSource) {
16
+ return manifestOrResponses._manifestSource;
17
+ }
18
+ return buildManifest(manifestOrResponses);
19
+ }
20
+
21
+ /**
22
+ * Render the short agent conventions markdown from a manifest.
23
+ * Deterministic: derived only from stable manifest fields.
24
+ * @param {object} manifest - validated manifest v2
25
+ * @returns {string}
26
+ */
27
+ export function renderConventions(manifest) {
28
+ const preset = manifest.architecture?.preset ?? manifest.architecture?.type ?? "feature-based";
29
+ const validation = manifest.architecture?.validation ?? "strict";
30
+ const paths = manifest.paths ?? {};
31
+ const harness = manifest.harness ?? { commands: [] };
32
+ const barrelFile = manifest.tooling?.language === "js" ? "index.js" : "index.ts";
33
+
34
+ const pathLines = Object.entries(paths)
35
+ .map(([key, value]) => `- ${key}: \`${value}\``)
36
+ .join("\n");
37
+
38
+ const harnessLines = (harness.commands ?? [])
39
+ .map((c) => `- \`${c.name}\`: \`${c.command}\` (${c.required === false ? "optional" : "required"})`)
40
+ .join("\n");
41
+
42
+ const harnessSummary = (harness.commands ?? []).map((c) => `\`${c.command}\``).join(", ");
43
+
44
+ return (
45
+ `# Lumen Agent Conventions\n` +
46
+ `\n` +
47
+ `Generated from \`lumen.config.json\`. Resolve every file location via \`paths.*\` in \`.lumen/architecture.json\` (preset: \`${preset}\`, validation: \`${validation}\`).\n` +
48
+ `\n` +
49
+ `## Rules\n` +
50
+ `\n` +
51
+ `- Resolve all paths via \`paths.*\` — never hardcode alternative \`src/...\` locations.\n` +
52
+ `- Feature barrel: every \`src/features/<feature>/\` must re-export its public API via \`${barrelFile}\`.\n` +
53
+ `- Shared code: import shared utilities via \`@/shared\` (plus the \`@/...\` aliases from tsconfig/jsconfig).\n` +
54
+ `- Harness: before finishing, run each command in \`.lumen/project.json\` \`harness.commands\` in order (${harnessSummary}).\n` +
55
+ `- Validation mode \`${validation}\`: ${VALIDATION_NOTES[validation] ?? VALIDATION_NOTES.strict}\n` +
56
+ `\n` +
57
+ `## Paths\n` +
58
+ `\n` +
59
+ `${pathLines}\n` +
60
+ `\n` +
61
+ `## Harness\n` +
62
+ `\n` +
63
+ `${harnessLines}\n`
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Build the deterministic agent-context payload from a validated manifest v2.
69
+ * No timestamps — output must be byte-stable.
70
+ * @param {object} manifest - validated manifest v2
71
+ * @returns {{ project: object, architecture: object, conventionsMd: string }}
72
+ */
73
+ export function buildAgentContext(manifest) {
74
+ const project = {
75
+ manifestVersion: manifest.manifestVersion,
76
+ framework: manifest.framework,
77
+ styling: manifest.styling,
78
+ tooling: manifest.tooling,
79
+ harness: manifest.harness,
80
+ docs: manifest.docs,
81
+ };
82
+ const architecture = {
83
+ preset: manifest.architecture?.preset ?? manifest.architecture?.type,
84
+ validation: manifest.architecture?.validation ?? "strict",
85
+ paths: manifest.paths,
86
+ };
87
+ const conventionsMd = renderConventions(manifest);
88
+ return { project, architecture, conventionsMd };
89
+ }
90
+
91
+ function writeJson(filePath, value) {
92
+ return fsp.writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
93
+ }
94
+
95
+ /**
96
+ * Emit the .lumen agent context dir into a scaffolded project.
97
+ * Accepts either a validated manifest v2 or scaffolder responses
98
+ * (uses responses._manifestSource when present, else buildManifest).
99
+ * Writes .lumen/project.json, .lumen/architecture.json, .lumen/conventions.md
100
+ * with 2-space indent + trailing newline. Byte-deterministic (no timestamps).
101
+ * @param {string} projectPath - absolute path to scaffolded project
102
+ * @param {object} manifestOrResponses
103
+ * @returns {Promise<string>} absolute path to the .lumen dir
104
+ */
105
+ export async function emitAgentContext(projectPath, manifestOrResponses) {
106
+ const manifest = resolveManifest(manifestOrResponses);
107
+ const { project, architecture, conventionsMd } = buildAgentContext(manifest);
108
+ const dir = path.join(projectPath, ".lumen");
109
+ await fsp.mkdir(dir, { recursive: true });
110
+ await writeJson(path.join(dir, "project.json"), project);
111
+ await writeJson(path.join(dir, "architecture.json"), architecture);
112
+ await fsp.writeFile(path.join(dir, "conventions.md"), conventionsMd, "utf8");
113
+ return dir;
114
+ }
package/src/main.js CHANGED
@@ -17,6 +17,7 @@ import { installAllDeps } from "@/dependencies.js";
17
17
  import { generateReadme } from "@/readme.js";
18
18
  import { runProjectFormat } from "@/format.js";
19
19
  import { emitManifest } from "@/manifest/emit.js";
20
+ import { emitAgentContext } from "@/agent-context.js";
20
21
 
21
22
  import { parseCliArgs, loadManifestSource, manifestToResponses, PRESETS } from "@/cli-args.js";
22
23
 
@@ -247,6 +248,19 @@ export async function main(options = {}) {
247
248
  throw err;
248
249
  }
249
250
 
251
+ // 12.7 Emit .lumen agent context dir (only when agentDocs is enabled)
252
+ if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
253
+ const agentSpin = spinner();
254
+ agentSpin.start("Writing .lumen agent context...");
255
+ try {
256
+ await emitAgentContext(projectPath, responses._manifestSource ?? responses);
257
+ agentSpin.stop(chalk.green(".lumen agent context written."));
258
+ } catch (err) {
259
+ agentSpin.stop(chalk.red("Failed to write .lumen agent context."));
260
+ throw err;
261
+ }
262
+ }
263
+
250
264
  // 13. Generate README.md + LICENSE (if requested)
251
265
  if (responses.readme) {
252
266
  const readmeSpin = spinner();
@@ -1,8 +1,43 @@
1
1
  import { promises as fsp } from "fs";
2
2
  import path from "path";
3
- import { parseManifest, SCHEMA_URL, MANIFEST_VERSION } from "./schema.js";
3
+ import { parseManifest, SCHEMA_URL, MANIFEST_VERSION, HARNESS_DEFAULT_COMMANDS } from "./schema.js";
4
4
  import { resolvePaths } from "./paths.js";
5
5
 
6
+ const LINT_COMMANDS = {
7
+ eslint: "eslint .",
8
+ oxlint: "oxlint .",
9
+ biome: "biome check .",
10
+ };
11
+
12
+ const FORMAT_COMMANDS = {
13
+ prettier: "prettier --check .",
14
+ oxfmt: "oxfmt --check .",
15
+ };
16
+
17
+ /**
18
+ * Build default harness commands from tooling choices.
19
+ * Skips lint/format when tooling is "none"; skips types when language is not "ts".
20
+ * Test + build are always included so commands.length >= 1.
21
+ * @param {{ linter?: string, formatter?: string, language?: string }} tooling
22
+ * @returns {Array<{ name: string, command: string, required: boolean }>}
23
+ */
24
+ export function defaultHarnessCommands(tooling = {}) {
25
+ const { linter = "none", formatter = "none", language = "ts" } = tooling;
26
+ const commands = [];
27
+ if (linter && linter !== "none" && LINT_COMMANDS[linter]) {
28
+ commands.push({ name: "lint", command: LINT_COMMANDS[linter], required: true });
29
+ }
30
+ if (formatter && formatter !== "none" && FORMAT_COMMANDS[formatter]) {
31
+ commands.push({ name: "format", command: FORMAT_COMMANDS[formatter], required: true });
32
+ }
33
+ if (language === "ts") {
34
+ commands.push({ name: "types", command: "tsc -b", required: true });
35
+ }
36
+ commands.push({ name: "test", command: "vitest run", required: true });
37
+ commands.push({ name: "build", command: "npm run build", required: true });
38
+ return commands.length > 0 ? commands : [...HARNESS_DEFAULT_COMMANDS];
39
+ }
40
+
6
41
  /**
7
42
  * Build an ordered manifest v2 object from scaffolder responses.
8
43
  * Stable key ordering for byte-determinism.
@@ -35,9 +70,14 @@ export function buildManifest(responses, opts = {}) {
35
70
  engine: responses.cssFramework || "tailwind",
36
71
  };
37
72
 
38
- const rawArch = responses.architecture || "feature-based";
73
+ const rawArch =
74
+ responses.architecturePreset || responses.architecture || "feature-based";
75
+ const normalizedArch = rawArch === "component-based" ? "type-based" : rawArch;
39
76
  const architecture = {
40
- type: rawArch === "component-based" ? "type-based" : rawArch,
77
+ preset: normalizedArch,
78
+ type: normalizedArch,
79
+ validation:
80
+ responses.architectureValidation || responses.validation || "strict",
41
81
  };
42
82
 
43
83
  const ui = {
@@ -59,12 +99,17 @@ export function buildManifest(responses, opts = {}) {
59
99
  formatter: responses.formatter || "none",
60
100
  };
61
101
 
102
+ const harness = responses.harness ?? {
103
+ commands: defaultHarnessCommands(tooling),
104
+ };
105
+
62
106
  const manifest = {
63
107
  $schema: SCHEMA_URL,
64
108
  manifestVersion: MANIFEST_VERSION,
65
109
  framework,
66
110
  styling,
67
111
  architecture,
112
+ harness,
68
113
  ui,
69
114
  docs,
70
115
  paths,
@@ -83,6 +128,7 @@ export function buildManifest(responses, opts = {}) {
83
128
  "framework",
84
129
  "styling",
85
130
  "architecture",
131
+ "harness",
86
132
  "ui",
87
133
  "docs",
88
134
  "paths",
@@ -13,7 +13,11 @@ export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
13
13
  export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
14
14
 
15
15
  export const STYLING_ENGINES = ["tailwind", "bootstrap", "none"];
16
- export const ARCHITECTURE_TYPES = ["feature-based", "type-based", "hybrid", "none"];
16
+ export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "hybrid", "none"];
17
+ // Legacy alias: docs used architecture.type, canonical is architecture.preset.
18
+ // Keep both in sync; parseManifest normalizes to expose both keys.
19
+ export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
20
+ export const ARCHITECTURE_VALIDATIONS = ["strict", "relaxed", "none"];
17
21
  export const UI_KITS = ["shadcn", "none"];
18
22
  export const DOCS_LANGUAGES = ["en", "es"];
19
23
  export const TOOLING_LANGUAGES = ["ts", "js"];
@@ -40,7 +44,49 @@ const stylingSchema = z
40
44
 
41
45
  const architectureSchema = z
42
46
  .object({
43
- type: z.enum(ARCHITECTURE_TYPES),
47
+ // Canonical key per docs/manifest-v2.md; legacy `type` kept as alias.
48
+ preset: z.enum(ARCHITECTURE_PRESETS).optional(),
49
+ type: z.enum(ARCHITECTURE_TYPES).optional(),
50
+ validation: z.enum(ARCHITECTURE_VALIDATIONS).optional().default("strict"),
51
+ })
52
+ .strict()
53
+ .superRefine((val, ctx) => {
54
+ if (!val.preset && !val.type) {
55
+ ctx.addIssue({
56
+ code: "custom",
57
+ path: ["preset"],
58
+ message: 'architecture must define "preset" or legacy "type"',
59
+ });
60
+ }
61
+ if (val.preset && val.type && val.preset !== val.type) {
62
+ ctx.addIssue({
63
+ code: "custom",
64
+ path: ["preset"],
65
+ message: `architecture.preset "${val.preset}" must match architecture.type "${val.type}"`,
66
+ });
67
+ }
68
+ });
69
+
70
+ export const HARNESS_DEFAULT_COMMANDS = [
71
+ { name: "lint", command: "oxlint .", required: true },
72
+ { name: "format", command: "oxfmt --check .", required: true },
73
+ { name: "types", command: "tsc -b", required: true },
74
+ { name: "test", command: "vitest run", required: true },
75
+ { name: "build", command: "npm run build", required: true },
76
+ ];
77
+
78
+ const harnessCommandSchema = z
79
+ .object({
80
+ name: z.string().min(1),
81
+ command: z.string().min(1),
82
+ required: z.boolean().optional().default(true),
83
+ description: z.string().optional(),
84
+ })
85
+ .strict();
86
+
87
+ const harnessSchema = z
88
+ .object({
89
+ commands: z.array(harnessCommandSchema).min(1),
44
90
  })
45
91
  .strict();
46
92
 
@@ -85,6 +131,7 @@ export const manifestSchemaV2 = z
85
131
  framework: frameworkSchema,
86
132
  styling: stylingSchema,
87
133
  architecture: architectureSchema,
134
+ harness: harnessSchema.optional(),
88
135
  ui: uiSchema,
89
136
  docs: docsSchema,
90
137
  paths: pathsSchema,
@@ -130,16 +177,17 @@ export const manifestSchemaV2 = z
130
177
  });
131
178
  }
132
179
 
133
- // architecture scoping per framework (#34)
180
+ // architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
181
+ const archValue = architecture.preset ?? architecture.type;
134
182
  const allowedByFramework =
135
183
  framework.name === "react"
136
184
  ? ["feature-based", "type-based", "none"]
137
185
  : ["feature-based", "hybrid", "none"];
138
- if (!allowedByFramework.includes(architecture.type)) {
186
+ if (!allowedByFramework.includes(archValue)) {
139
187
  ctx.addIssue({
140
188
  code: "custom",
141
189
  path: ["architecture", "type"],
142
- message: `architecture.type "${architecture.type}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
190
+ message: `architecture.type "${archValue}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
143
191
  });
144
192
  }
145
193
 
@@ -205,7 +253,17 @@ export function parseManifest(raw) {
205
253
  err.cause = result.error;
206
254
  throw err;
207
255
  }
208
- return result.data;
256
+ // Normalize: always expose architecture.preset + architecture.type (synced)
257
+ // + architecture.validation default, for backward/forward compat.
258
+ const data = result.data;
259
+ const preset = data.architecture.preset ?? data.architecture.type;
260
+ const type = data.architecture.type ?? data.architecture.preset;
261
+ data.architecture = {
262
+ preset,
263
+ type,
264
+ validation: data.architecture.validation ?? "strict",
265
+ };
266
+ return data;
209
267
  }
210
268
 
211
269
  // Re-export type helper for JSDoc (z.infer)