agentic-dev 0.2.4 → 0.2.7

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 (72) hide show
  1. package/.agent/prd.json +1 -1
  2. package/.agent/prompt.md +1 -1
  3. package/.claude/agents/db-dev.md +1 -1
  4. package/.claude/agents/frontend-dev.md +1 -1
  5. package/.claude/agents/test-dev.md +1 -1
  6. package/.claude/skills/sdd/SKILL.md +15 -10
  7. package/.claude/skills/sdd/references/section-map.md +5 -5
  8. package/.codex/skills/sdd/SKILL.md +15 -10
  9. package/.codex/skills/sdd/references/section-map.md +5 -5
  10. package/AGENTS.md +12 -4
  11. package/README.md +45 -12
  12. package/SDD_SKILL.md +7 -7
  13. package/bin/agentic-dev.mjs +53 -18
  14. package/client/admin/scripts/ui-parity-admin-adapter.mjs +1 -1
  15. package/client/landing/scripts/ui-parity-landing-adapter.mjs +1 -1
  16. package/client/mobile/scripts/ui-parity-mobile-adapter.mjs +1 -1
  17. package/client/web/scripts/ui-parity-web-adapter.mjs +1 -1
  18. package/lib/scaffold.mjs +254 -279
  19. package/package.json +2 -2
  20. package/sdd/02_plan/03_architecture/repository_governance.md +1 -1
  21. package/sdd/02_plan/03_architecture/runtime_and_structure_governance.md +1 -1
  22. package/sdd/02_plan/03_architecture/toolchain_governance.md +2 -2
  23. package/sdd/02_plan/06_iac/dev_runtime_delivery.md +1 -1
  24. package/sdd/02_plan/10_test/regression_verification.md +2 -2
  25. package/sdd/02_plan/10_test/templates/ui_parity_web_contract.template.yaml +1 -1
  26. package/sdd/02_plan/10_test/verification_strategy.md +2 -2
  27. package/sdd/03_build/03_architecture/toolchain_governance.md +1 -1
  28. package/sdd/{04_verify → 03_verify}/01_feature/service_verification.md +1 -1
  29. package/sdd/{04_verify → 03_verify}/02_screen/README.md +1 -1
  30. package/sdd/{04_verify → 03_verify}/03_architecture/toolchain_governance.md +2 -2
  31. package/sdd/{04_verify → 03_verify}/06_iac/dev_runtime_delivery.md +1 -1
  32. package/sdd/{04_verify → 03_verify}/06_iac/template_runtime_delivery.md +1 -1
  33. package/sdd/{04_verify → 03_verify}/README.md +3 -3
  34. package/sdd/99_toolchain/01_automation/README.md +2 -2
  35. package/sdd/99_toolchain/01_automation/agentic-dev/assets/repo-contract.template.json +5 -5
  36. package/sdd/99_toolchain/01_automation/agentic-dev/bootstrap_frontend_parity.sh +1 -1
  37. package/sdd/99_toolchain/01_automation/agentic-dev/repo-contract.json +6 -6
  38. package/sdd/99_toolchain/01_automation/agentic-parity-harness-design.md +4 -4
  39. package/sdd/99_toolchain/01_automation/parity-execution-tooling-design.md +3 -3
  40. package/sdd/99_toolchain/01_automation/ui-parity/README.md +1 -1
  41. package/sdd/99_toolchain/01_automation/ui-parity/cli/materialize-reference-assets.mjs +1 -1
  42. package/sdd/99_toolchain/01_automation/ui-parity/cli/scaffold-contract.mjs +2 -2
  43. package/sdd/99_toolchain/01_automation/ui-parity/core/proof-runner.mjs +1 -1
  44. package/sdd/99_toolchain/01_automation/ui-parity/interfaces/ui-parity-artifact-layout.md +1 -1
  45. package/sdd/99_toolchain/02_policies/build-ast-governance-policy.md +2 -2
  46. package/sdd/99_toolchain/02_policies/compose-runtime-baseline-policy.md +2 -2
  47. package/sdd/99_toolchain/02_policies/regression-verification-policy.md +1 -1
  48. package/sdd/99_toolchain/README.md +1 -1
  49. package/sdd/README.md +1 -1
  50. /package/sdd/{04_verify → 03_verify}/01_feature/README.md +0 -0
  51. /package/sdd/{04_verify → 03_verify}/01_feature/domain_verification.md +0 -0
  52. /package/sdd/{04_verify → 03_verify}/02_screen/_screen_verify_template.md +0 -0
  53. /package/sdd/{04_verify → 03_verify}/02_screen/admin/README.md +0 -0
  54. /package/sdd/{04_verify → 03_verify}/02_screen/landing/README.md +0 -0
  55. /package/sdd/{04_verify → 03_verify}/02_screen/mobile/README.md +0 -0
  56. /package/sdd/{04_verify → 03_verify}/02_screen/web/README.md +0 -0
  57. /package/sdd/{04_verify → 03_verify}/03_architecture/README.md +0 -0
  58. /package/sdd/{04_verify → 03_verify}/03_architecture/architecture_document_governance.md +0 -0
  59. /package/sdd/{04_verify → 03_verify}/03_architecture/build_ast_runtime_tree_governance.md +0 -0
  60. /package/sdd/{04_verify → 03_verify}/03_architecture/repository_governance.md +0 -0
  61. /package/sdd/{04_verify → 03_verify}/06_iac/README.md +0 -0
  62. /package/sdd/{04_verify → 03_verify}/07_integration/README.md +0 -0
  63. /package/sdd/{04_verify → 03_verify}/07_integration/frontend_live_integration.md +0 -0
  64. /package/sdd/{04_verify → 03_verify}/08_nonfunctional/README.md +0 -0
  65. /package/sdd/{04_verify → 03_verify}/08_nonfunctional/repository_hygiene.md +0 -0
  66. /package/sdd/{04_verify → 03_verify}/10_test/README.md +0 -0
  67. /package/sdd/{04_verify → 03_verify}/10_test/regression_verification.md +0 -0
  68. /package/sdd/{04_verify → 03_verify}/10_test/ui_parity/README.md +0 -0
  69. /package/sdd/{04_verify → 03_verify}/10_test/ui_parity/loop_runs/.gitkeep +0 -0
  70. /package/sdd/{04_verify → 03_verify}/10_test/ui_parity/reference/.gitkeep +0 -0
  71. /package/sdd/{04_verify → 03_verify}/10_test/ui_parity/staged_runs/.gitkeep +0 -0
  72. /package/sdd/{04_verify → 03_verify}/10_test/verification_harness.md +0 -0
package/lib/scaffold.mjs CHANGED
@@ -1,68 +1,56 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
4
- import { fileURLToPath } from "node:url";
5
-
6
- export const TEMPLATE_CONFIG = {
7
- landing: {
8
- id: "landing",
9
- serviceName: "client-landing",
10
- composePortVar: "CLIENT_LANDING_PORT",
11
- composeApiVar: "CLIENT_LANDING_VITE_API_BASE_URL",
12
- defaultPort: 3000,
13
- },
14
- web: {
15
- id: "web",
16
- serviceName: "client-web",
17
- composePortVar: "CLIENT_WEB_PORT",
18
- composeApiVar: "CLIENT_WEB_VITE_API_BASE_URL",
19
- defaultPort: 3001,
20
- },
21
- mobile: {
22
- id: "mobile",
23
- serviceName: "client-mobile",
24
- composePortVar: "CLIENT_MOBILE_PORT",
25
- composeApiVar: "CLIENT_MOBILE_VITE_API_BASE_URL",
26
- defaultPort: 3002,
27
- },
28
- admin: {
29
- id: "admin",
30
- serviceName: "client-admin",
31
- composePortVar: "CLIENT_ADMIN_PORT",
32
- composeApiVar: "CLIENT_ADMIN_VITE_API_BASE_URL",
33
- defaultPort: 4000,
34
- },
35
- };
36
-
37
- const ROOT_FILES = [
38
- ".dockerignore",
39
- ".env.example",
40
- ".gitignore",
41
- "AGENTS.md",
42
- "README.md",
43
- "SDD_SKILL.md",
44
- ];
45
-
46
- const ROOT_DIRS = [
47
- ".agent",
48
- ".claude",
49
- ".codex",
50
- "infra",
51
- "scripts",
52
- "sdd",
53
- "server",
54
- ];
55
-
56
- const __filename = fileURLToPath(import.meta.url);
57
- const __dirname = path.dirname(__filename);
58
- const PACKAGE_ROOT = path.resolve(__dirname, "..");
59
-
60
- export function listTemplates() {
61
- return Object.keys(TEMPLATE_CONFIG);
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ export const DEFAULT_TEMPLATE_OWNER = "say828";
8
+
9
+ function runCommand(command, args, { cwd = process.cwd(), label = "" } = {}) {
10
+ if (label) {
11
+ console.log(`\n> ${label}`);
12
+ }
13
+ const result = spawnSync(command, args, {
14
+ cwd,
15
+ stdio: "inherit",
16
+ });
17
+ if (result.status !== 0) {
18
+ throw new Error(`Command failed: ${command} ${args.join(" ")}`);
19
+ }
20
+ }
21
+
22
+ function commandExists(command) {
23
+ const result = spawnSync("bash", ["-lc", `command -v ${command}`], {
24
+ stdio: "ignore",
25
+ });
26
+ return result.status === 0;
27
+ }
28
+
29
+ function copyDirectoryContents(sourceRoot, destinationRoot) {
30
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
31
+ if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "dist") {
32
+ continue;
33
+ }
34
+ fs.cpSync(path.join(sourceRoot, entry.name), path.join(destinationRoot, entry.name), {
35
+ recursive: true,
36
+ force: true,
37
+ filter: (source) => {
38
+ const baseName = path.basename(source);
39
+ return baseName !== ".git" && baseName !== "node_modules" && baseName !== "dist";
40
+ },
41
+ });
42
+ }
62
43
  }
63
44
 
64
- export function resolvePackageRoot() {
65
- return PACKAGE_ROOT;
45
+ function fetchPage(url) {
46
+ const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.AGENTIC_GITHUB_TOKEN;
47
+ return fetch(url, {
48
+ headers: {
49
+ Accept: "application/vnd.github+json",
50
+ "User-Agent": "agentic-dev",
51
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
52
+ },
53
+ });
66
54
  }
67
55
 
68
56
  export function parseArgs(argv) {
@@ -71,8 +59,10 @@ export function parseArgs(argv) {
71
59
  command: "init",
72
60
  targetDir: "",
73
61
  template: "",
62
+ owner: DEFAULT_TEMPLATE_OWNER,
74
63
  force: false,
75
64
  yes: false,
65
+ skipBootstrap: false,
76
66
  };
77
67
 
78
68
  if (args.length > 0 && !args[0].startsWith("-")) {
@@ -86,6 +76,9 @@ export function parseArgs(argv) {
86
76
  case "-t":
87
77
  options.template = args.shift() ?? "";
88
78
  break;
79
+ case "--owner":
80
+ options.owner = args.shift() ?? DEFAULT_TEMPLATE_OWNER;
81
+ break;
89
82
  case "--force":
90
83
  options.force = true;
91
84
  break;
@@ -93,6 +86,9 @@ export function parseArgs(argv) {
93
86
  case "-y":
94
87
  options.yes = true;
95
88
  break;
89
+ case "--skip-bootstrap":
90
+ options.skipBootstrap = true;
91
+ break;
96
92
  case "--help":
97
93
  case "-h":
98
94
  options.command = "help";
@@ -112,29 +108,24 @@ export function parseArgs(argv) {
112
108
  export function usage() {
113
109
  return [
114
110
  "Usage:",
115
- " agentic-dev init <target-dir> --template <landing|web|mobile|admin>",
111
+ " agentic-dev",
112
+ " agentic-dev init <target-dir>",
116
113
  "",
117
114
  "Examples:",
118
- " npx agentic-dev init my-app --template web",
119
- " npx agentic-dev my-app --template mobile",
115
+ " npx agentic-dev",
116
+ " npx agentic-dev init my-app",
117
+ " npx agentic-dev init my-app --template template-web --yes",
120
118
  "",
121
119
  "Options:",
122
- " --template, -t Frontend template to install",
123
- " --force Allow scaffolding into a non-empty directory",
124
- " --yes, -y Skip interactive confirmation",
125
- " --help, -h Show this help",
120
+ " --template, -t Template repo name or suffix to use",
121
+ " --owner GitHub owner for template-* repos (default: say828)",
122
+ " --force Allow scaffolding into a non-empty directory",
123
+ " --yes, -y Skip interactive prompts",
124
+ " --skip-bootstrap Copy the template but skip install/bootstrap",
125
+ " --help, -h Show this help",
126
126
  ].join("\n");
127
127
  }
128
128
 
129
- export function validateTemplate(template) {
130
- if (!TEMPLATE_CONFIG[template]) {
131
- throw new Error(
132
- `Unsupported template: ${template}. Expected one of: ${listTemplates().join(", ")}.`,
133
- );
134
- }
135
- return TEMPLATE_CONFIG[template];
136
- }
137
-
138
129
  export function ensureTargetDir(targetDir, { force = false } = {}) {
139
130
  if (!targetDir) {
140
131
  throw new Error("Missing target directory.");
@@ -150,230 +141,214 @@ export function ensureTargetDir(targetDir, { force = false } = {}) {
150
141
  return resolved;
151
142
  }
152
143
 
153
- function copyPath(relativePath, destinationRoot) {
154
- let sourcePath = path.join(PACKAGE_ROOT, relativePath);
155
- if (!fs.existsSync(sourcePath) && relativePath === ".gitignore") {
156
- const npmIgnorePath = path.join(PACKAGE_ROOT, ".npmignore");
157
- if (fs.existsSync(npmIgnorePath)) {
158
- sourcePath = npmIgnorePath;
144
+ export async function fetchTemplateRepos({ owner = DEFAULT_TEMPLATE_OWNER } = {}) {
145
+ const repos = [];
146
+ let page = 1;
147
+
148
+ while (true) {
149
+ const response = await fetchPage(
150
+ `https://api.github.com/users/${owner}/repos?per_page=100&page=${page}&sort=updated`,
151
+ );
152
+ if (!response.ok) {
153
+ throw new Error(`Failed to fetch template repos from GitHub: ${response.status}`);
159
154
  }
155
+ const payload = await response.json();
156
+ if (!Array.isArray(payload)) {
157
+ throw new Error("Unexpected GitHub response while listing template repos.");
158
+ }
159
+ const publicRepos = payload
160
+ .filter((repo) => !repo.private && typeof repo.name === "string")
161
+ .filter((repo) => repo.name.startsWith("template-"))
162
+ .map((repo) => ({
163
+ name: repo.name,
164
+ description: repo.description ?? "",
165
+ cloneUrl: repo.clone_url,
166
+ htmlUrl: repo.html_url,
167
+ defaultBranch: repo.default_branch || "main",
168
+ }));
169
+ repos.push(...publicRepos);
170
+ if (payload.length < 100) {
171
+ break;
172
+ }
173
+ page += 1;
160
174
  }
161
- const destinationPath = path.join(destinationRoot, relativePath);
162
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
163
- fs.cpSync(sourcePath, destinationPath, {
164
- recursive: true,
165
- force: true,
166
- filter: (source) => {
167
- const baseName = path.basename(source);
168
- return baseName !== ".git" && baseName !== "node_modules" && baseName !== "dist";
169
- },
170
- });
175
+
176
+ repos.sort((a, b) => a.name.localeCompare(b.name));
177
+ return repos;
178
+ }
179
+
180
+ export function resolveTemplateRepo(input, repos) {
181
+ const normalized = (input || "").trim();
182
+ if (!normalized) {
183
+ return null;
184
+ }
185
+
186
+ const exact = repos.find((repo) => repo.name === normalized);
187
+ if (exact) {
188
+ return exact;
189
+ }
190
+
191
+ const normalizedSuffix = normalized.startsWith("template-") ? normalized.slice(9) : normalized;
192
+ const matches = repos.filter((repo) => repo.name === `template-${normalizedSuffix}`);
193
+ if (matches.length === 1) {
194
+ return matches[0];
195
+ }
196
+ if (matches.length > 1) {
197
+ throw new Error(`Ambiguous template repo: ${normalized}`);
198
+ }
199
+ throw new Error(`Template repo not found: ${normalized}`);
171
200
  }
172
201
 
173
- function writeFile(destinationPath, content) {
174
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
175
- fs.writeFileSync(destinationPath, content, "utf8");
202
+ export function selectTemplateRepo(choice, repos) {
203
+ const normalized = (choice || "").trim();
204
+ if (!normalized) {
205
+ throw new Error("Missing template selection.");
206
+ }
207
+
208
+ if (/^\d+$/.test(normalized)) {
209
+ const index = Number.parseInt(normalized, 10) - 1;
210
+ if (index < 0 || index >= repos.length) {
211
+ throw new Error(`Invalid selection: ${normalized}`);
212
+ }
213
+ return repos[index];
214
+ }
215
+
216
+ return resolveTemplateRepo(normalized, repos);
176
217
  }
177
218
 
178
- function renderWorkspace(template) {
179
- return `packages:\n - client/${template}\n`;
219
+ function ensurePnpm() {
220
+ if (commandExists("pnpm")) {
221
+ return ["pnpm"];
222
+ }
223
+ if (commandExists("corepack")) {
224
+ runCommand("corepack", ["enable"], { label: "Enabling corepack" });
225
+ return ["corepack", "pnpm"];
226
+ }
227
+ runCommand("npm", ["install", "-g", "pnpm"], { label: "Installing pnpm globally" });
228
+ return ["pnpm"];
180
229
  }
181
230
 
182
- function renderCompose(template) {
183
- const config = TEMPLATE_CONFIG[template];
184
- return `name: templates
185
-
186
- services:
187
- postgres:
188
- image: postgres:16-alpine
189
- environment:
190
- POSTGRES_DB: \${POSTGRES_DB:-template}
191
- POSTGRES_USER: \${POSTGRES_USER:-template}
192
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-template}
193
- ports:
194
- - "\${POSTGRES_PORT:-5432}:5432"
195
- volumes:
196
- - postgres_data:/var/lib/postgresql/data
197
- healthcheck:
198
- test:
199
- [
200
- "CMD-SHELL",
201
- "pg_isready -U \${POSTGRES_USER:-template} -d \${POSTGRES_DB:-template}",
202
- ]
203
- interval: 10s
204
- timeout: 5s
205
- retries: 5
206
- networks:
207
- - template-dev
208
-
209
- mysql:
210
- image: mysql:8.4
211
- profiles: ["mysql"]
212
- environment:
213
- MYSQL_DATABASE: \${MYSQL_DATABASE:-template}
214
- MYSQL_USER: \${MYSQL_USER:-template}
215
- MYSQL_PASSWORD: \${MYSQL_PASSWORD:-template}
216
- MYSQL_ROOT_PASSWORD: \${MYSQL_ROOT_PASSWORD:-template-root}
217
- ports:
218
- - "\${MYSQL_PORT:-3306}:3306"
219
- volumes:
220
- - mysql_data:/var/lib/mysql
221
- healthcheck:
222
- test:
223
- [
224
- "CMD-SHELL",
225
- "mysqladmin ping -h 127.0.0.1 -u\${MYSQL_USER:-template} -p\${MYSQL_PASSWORD:-template}",
226
- ]
227
- interval: 10s
228
- timeout: 5s
229
- retries: 10
230
- start_period: 20s
231
- networks:
232
- - template-dev
233
-
234
- mariadb:
235
- image: mariadb:11.7
236
- profiles: ["mariadb"]
237
- environment:
238
- MARIADB_DATABASE: \${MARIADB_DATABASE:-template}
239
- MARIADB_USER: \${MARIADB_USER:-template}
240
- MARIADB_PASSWORD: \${MARIADB_PASSWORD:-template}
241
- MARIADB_ROOT_PASSWORD: \${MARIADB_ROOT_PASSWORD:-template-root}
242
- ports:
243
- - "\${MARIADB_PORT:-3307}:3306"
244
- volumes:
245
- - mariadb_data:/var/lib/mysql
246
- healthcheck:
247
- test:
248
- [
249
- "CMD-SHELL",
250
- "mariadb-admin ping -h 127.0.0.1 -u\${MARIADB_USER:-template} -p\${MARIADB_PASSWORD:-template}",
251
- ]
252
- interval: 10s
253
- timeout: 5s
254
- retries: 10
255
- start_period: 20s
256
- networks:
257
- - template-dev
258
-
259
- mongo:
260
- image: mongo:8
261
- profiles: ["mongo"]
262
- ports:
263
- - "\${MONGO_PORT:-27017}:27017"
264
- volumes:
265
- - mongo_data:/data/db
266
- networks:
267
- - template-dev
268
-
269
- server:
270
- build:
271
- context: .
272
- dockerfile: server/Dockerfile.dev
273
- network: host
274
- environment:
275
- APP_NAME: \${SERVER_APP_NAME:-Template Server}
276
- ENVIRONMENT: \${SERVER_ENVIRONMENT:-development}
277
- API_PREFIX: \${SERVER_API_PREFIX:-/api/v1}
278
- DATABASE_BACKEND: \${SERVER_DATABASE_BACKEND:-postgres}
279
- POSTGRES_URL: \${SERVER_POSTGRES_URL:-postgresql+psycopg://template:template@postgres:5432/template}
280
- MYSQL_URL: \${SERVER_MYSQL_URL:-mysql+pymysql://template:template@mysql:3306/template}
281
- MARIADB_URL: \${SERVER_MARIADB_URL:-mysql+pymysql://template:template@mariadb:3306/template}
282
- MONGODB_URL: \${SERVER_MONGODB_URL:-mongodb://mongo:27017}
283
- MONGODB_DATABASE: \${SERVER_MONGODB_DATABASE:-template}
284
- JWT_SECRET: \${SERVER_JWT_SECRET:-template-local-dev-secret}
285
- JWT_ALGORITHM: \${SERVER_JWT_ALGORITHM:-HS256}
286
- ACCESS_TOKEN_TTL_MINUTES: \${SERVER_ACCESS_TOKEN_TTL_MINUTES:-120}
287
- BOOTSTRAP_ADMIN_EMAIL: \${SERVER_BOOTSTRAP_ADMIN_EMAIL:-admin@example.com}
288
- BOOTSTRAP_ADMIN_PASSWORD: \${SERVER_BOOTSTRAP_ADMIN_PASSWORD:-change-me-admin}
289
- BOOTSTRAP_ADMIN_NAME: \${SERVER_BOOTSTRAP_ADMIN_NAME:-Template Admin}
290
- BOOTSTRAP_OPERATOR_EMAIL: \${SERVER_BOOTSTRAP_OPERATOR_EMAIL:-operator@example.com}
291
- BOOTSTRAP_OPERATOR_PASSWORD: \${SERVER_BOOTSTRAP_OPERATOR_PASSWORD:-change-me-operator}
292
- BOOTSTRAP_OPERATOR_NAME: \${SERVER_BOOTSTRAP_OPERATOR_NAME:-Template Operator}
293
- CORS_ORIGINS: '\${SERVER_CORS_ORIGINS:-["http://localhost:${config.defaultPort}","http://127.0.0.1:${config.defaultPort}"]}'
294
- SERVER_HTTP_PORT: \${SERVER_HTTP_PORT:-8000}
295
- ports:
296
- - "\${SERVER_HTTP_PORT:-8000}:\${SERVER_HTTP_PORT:-8000}"
297
- volumes:
298
- - ./server:/app/server
299
- depends_on:
300
- postgres:
301
- condition: service_healthy
302
- networks:
303
- - template-dev
304
-
305
- ${config.serviceName}:
306
- build:
307
- context: .
308
- dockerfile: client/${template}/Dockerfile.dev
309
- network: host
310
- environment:
311
- PORT: \${${config.composePortVar}:-${config.defaultPort}}
312
- VITE_API_BASE_URL: \${${config.composeApiVar}:-http://127.0.0.1:8000/api/v1}
313
- CHOKIDAR_USEPOLLING: \${CLIENT_WATCH_USE_POLLING:-false}
314
- ports:
315
- - "\${${config.composePortVar}:-${config.defaultPort}}:\${${config.composePortVar}:-${config.defaultPort}}"
316
- volumes:
317
- - ./client/${template}:/app/client/${template}
318
- depends_on:
319
- server:
320
- condition: service_started
321
- networks:
322
- - template-dev
323
-
324
- networks:
325
- template-dev:
326
- driver: bridge
327
-
328
- volumes:
329
- postgres_data:
330
- mysql_data:
331
- mariadb_data:
332
- mongo_data:
333
- `;
231
+ function maybeCreateEnvFile(destinationRoot) {
232
+ const envExample = path.join(destinationRoot, ".env.example");
233
+ const envFile = path.join(destinationRoot, ".env");
234
+ if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
235
+ fs.copyFileSync(envExample, envFile);
236
+ }
334
237
  }
335
238
 
336
- function updateRepoContract(destinationRoot, template) {
337
- const contractPath = path.join(
239
+ function bootstrapScriptPath(destinationRoot) {
240
+ return path.join(
338
241
  destinationRoot,
339
- "sdd/99_toolchain/01_automation/agentic-dev/repo-contract.json",
242
+ "sdd/99_toolchain/01_automation/agentic-dev/bootstrap_frontend_parity.sh",
340
243
  );
341
- const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
342
- const selectedTarget = contract.frontend.targets[template];
343
- contract.frontend.default_target = template;
344
- contract.frontend.targets = {
345
- [template]: selectedTarget,
346
- };
347
- contract.commands.verify_dev = `test -f "$PWD/${selectedTarget.proof_output}" && echo verify-dev-placeholder-ok`;
348
- contract.artifacts.route_gap_output = selectedTarget.route_gap_output;
349
- contract.artifacts.proof_output = selectedTarget.proof_output;
350
- writeFile(contractPath, `${JSON.stringify(contract, null, 2)}\n`);
351
244
  }
352
245
 
353
- export function scaffoldTemplate({ destinationRoot, template }) {
354
- validateTemplate(template);
246
+ function detectClientSurfaces(destinationRoot) {
247
+ const clientDir = path.join(destinationRoot, "client");
248
+ if (!fs.existsSync(clientDir)) {
249
+ return [];
250
+ }
251
+ return fs
252
+ .readdirSync(clientDir, { withFileTypes: true })
253
+ .filter((entry) => entry.isDirectory())
254
+ .map((entry) => entry.name)
255
+ .sort();
256
+ }
257
+
258
+ function validateTemplateRepoContents(destinationRoot, templateRepo) {
259
+ const requiredPaths = [
260
+ ".env.example",
261
+ "pnpm-workspace.yaml",
262
+ "sdd/99_toolchain/01_automation/agentic-dev/bootstrap_frontend_parity.sh",
263
+ ];
264
+
265
+ const missing = requiredPaths.filter(
266
+ (relativePath) => !fs.existsSync(path.join(destinationRoot, relativePath)),
267
+ );
268
+ if (missing.length > 0) {
269
+ throw new Error(
270
+ `Template repo ${templateRepo.name} is missing required files: ${missing.join(", ")}`,
271
+ );
272
+ }
273
+ }
355
274
 
356
- for (const relativePath of ROOT_FILES) {
357
- copyPath(relativePath, destinationRoot);
275
+ function runInstallSteps(destinationRoot) {
276
+ maybeCreateEnvFile(destinationRoot);
277
+ const clientSurfaces = detectClientSurfaces(destinationRoot);
278
+ if (clientSurfaces.length !== 1) {
279
+ throw new Error(`Expected exactly one client surface in template repo, found ${clientSurfaces.length}.`);
358
280
  }
359
- for (const relativePath of ROOT_DIRS) {
360
- copyPath(relativePath, destinationRoot);
281
+ const clientDir = path.join("client", clientSurfaces[0]);
282
+
283
+ const pnpmCommand = ensurePnpm();
284
+ runCommand(pnpmCommand[0], [...pnpmCommand.slice(1), "install"], {
285
+ cwd: destinationRoot,
286
+ label: "Installing workspace dependencies",
287
+ });
288
+
289
+ runCommand(
290
+ pnpmCommand[0],
291
+ [...pnpmCommand.slice(1), "--dir", clientDir, "exec", "playwright", "install", "chromium"],
292
+ {
293
+ cwd: destinationRoot,
294
+ label: "Installing Playwright Chromium",
295
+ },
296
+ );
297
+
298
+ return ["pnpm install", "pnpm exec playwright install chromium"];
299
+ }
300
+
301
+ function runBootstrapStep(destinationRoot) {
302
+ const bootstrapScript = bootstrapScriptPath(destinationRoot);
303
+ if (!fs.existsSync(bootstrapScript)) {
304
+ throw new Error(
305
+ `Missing bootstrap script in template repo: ${path.relative(destinationRoot, bootstrapScript)}`,
306
+ );
361
307
  }
362
308
 
363
- copyPath(path.join("client", template), destinationRoot);
309
+ runCommand("bash", [bootstrapScript, "."], {
310
+ cwd: destinationRoot,
311
+ label: "Running frontend parity bootstrap",
312
+ });
364
313
 
365
- writeFile(path.join(destinationRoot, "pnpm-workspace.yaml"), renderWorkspace(template));
366
- writeFile(path.join(destinationRoot, "compose.yml"), renderCompose(template));
367
- updateRepoContract(destinationRoot, template);
314
+ return ["bootstrap_frontend_parity.sh ."];
315
+ }
368
316
 
317
+ export function scaffoldFromTemplateRepo({ destinationRoot, templateRepo }) {
318
+ const cloneRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentic-dev-template-"));
319
+ const repoRoot = path.join(cloneRoot, templateRepo.name);
320
+
321
+ try {
322
+ runCommand("git", ["clone", "--depth", "1", templateRepo.cloneUrl, repoRoot], {
323
+ label: `Cloning ${templateRepo.name}`,
324
+ });
325
+ copyDirectoryContents(repoRoot, destinationRoot);
326
+ } finally {
327
+ fs.rmSync(cloneRoot, { recursive: true, force: true });
328
+ }
329
+ }
330
+
331
+ export function installTemplateRepo({
332
+ destinationRoot,
333
+ templateRepo,
334
+ skipBootstrap = false,
335
+ }) {
336
+ scaffoldFromTemplateRepo({ destinationRoot, templateRepo });
337
+ validateTemplateRepoContents(destinationRoot, templateRepo);
338
+
339
+ const executedSteps = runInstallSteps(destinationRoot);
340
+ if (!skipBootstrap) {
341
+ executedSteps.push(...runBootstrapStep(destinationRoot));
342
+ }
343
+ const clientSurfaces = detectClientSurfaces(destinationRoot);
344
+ const bootstrapHint =
345
+ clientSurfaces.length === 1
346
+ ? `cd client/${clientSurfaces[0]} && npm run ui:parity:bootstrap`
347
+ : "npm run ui:parity:bootstrap";
369
348
  return {
370
349
  destinationRoot,
371
- template,
372
- nextSteps: [
373
- "cp .env.example .env",
374
- "npm install -g pnpm",
375
- "pnpm install",
376
- `cd client/${template} && npm run ui:parity:bootstrap`,
377
- ],
350
+ templateRepo: templateRepo.name,
351
+ executedSteps,
352
+ nextSteps: skipBootstrap ? [bootstrapHint] : [],
378
353
  };
379
354
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-dev",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "Scaffold an agentic template repo with the selected frontend surface and shared toolchain assets.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.32.0",
@@ -28,7 +28,7 @@
28
28
  "pnpm-workspace.yaml"
29
29
  ],
30
30
  "scripts": {
31
- "smoke:init": "node ./bin/agentic-dev.mjs init ./.tmp-agentic-smoke --template web --yes --force"
31
+ "smoke:init": "node ./bin/agentic-dev.mjs init ./.tmp-agentic-smoke --template template-web --yes --force"
32
32
  },
33
33
  "keywords": [
34
34
  "agentic",
@@ -36,4 +36,4 @@
36
36
  - current references:
37
37
  - `AGENTS.md`
38
38
  - `sdd/03_build/03_architecture/repository_governance.md`
39
- - `sdd/04_verify/03_architecture/repository_governance.md`
39
+ - `sdd/03_verify/03_architecture/repository_governance.md`
@@ -35,4 +35,4 @@
35
35
  - current references:
36
36
  - `sdd/01_planning/README.md`
37
37
  - `sdd/03_build/03_architecture/repository_governance.md`
38
- - `sdd/04_verify/03_architecture/repository_governance.md`
38
+ - `sdd/03_verify/03_architecture/repository_governance.md`
@@ -61,7 +61,7 @@
61
61
  - toolchain은 `sdd/99_toolchain/01_automation`, `02_policies`, `03_templates` current split을 따른다.
62
62
  - Playwright local exactness는 `run_playwright_exactness.py`와 `playwright_exactness_manifest.py`를 canonical entrypoint로 사용한다.
63
63
  - screen 작업은 가능하면 `npx playwright test ...`를 직접 쓰지 않고 toolchain runner를 통해 suite id 기준으로 실행한다.
64
- - proof artifact는 `sdd/04_verify/10_test/ui_parity/` current path를 기준으로 관리한다.
64
+ - proof artifact는 `sdd/03_verify/10_test/ui_parity/` current path를 기준으로 관리한다.
65
65
  - toolchain 정책 문서의 정본은 `sdd/99_toolchain/02_policies`에 둔다.
66
66
  - skill/tooling 사용 여부는 개별 dated memo가 아니라 현재 workflow rule로만 유지한다.
67
67
  - 화면명세서의 icon/image/logo 등 재사용 가능한 정적 자산은 `spec_asset_builder.py` 또는 wrapper를 먼저 사용해 추출하고, 수동 재작성은 builder가 표현하지 못하는 경우에만 예외로 허용한다.
@@ -93,6 +93,6 @@
93
93
  - `sdd/99_toolchain/01_automation/playwright_exactness_manifest.py`
94
94
  - `sdd/99_toolchain/01_automation/run_playwright_exactness.py`
95
95
  - `sdd/02_plan/10_test/verification_strategy.md`
96
- - `sdd/04_verify/10_test/verification_harness.md`
96
+ - `sdd/03_verify/10_test/verification_harness.md`
97
97
  - `sdd/99_toolchain/02_policies/regression-verification-policy.md`
98
98
  - `sdd/02_plan/10_test/regression_verification.md`
@@ -33,4 +33,4 @@
33
33
 
34
34
  - current references:
35
35
  - `sdd/03_build/06_iac/dev_runtime_delivery.md`
36
- - `sdd/04_verify/06_iac/dev_runtime_delivery.md`
36
+ - `sdd/03_verify/06_iac/dev_runtime_delivery.md`
@@ -15,7 +15,7 @@
15
15
  ## Acceptance Criteria
16
16
 
17
17
  - [x] 회귀 검수는 direct target-only로 끝내지 않는다.
18
- - [x] selected regression surface는 `sdd/02_plan`, `sdd/03_build`, `sdd/04_verify`에 current-state로 이어진다.
18
+ - [x] selected regression surface는 `sdd/02_plan`, `sdd/03_build`, `sdd/03_verify`에 current-state로 이어진다.
19
19
  - [x] automation gap은 scope 축소가 아니라 residual risk로 기록한다.
20
20
 
21
21
  ## Execution Checklist
@@ -36,4 +36,4 @@
36
36
  - `AGENTS.md`
37
37
  - `.codex/skills/sdd/SKILL.md`
38
38
  - `sdd/99_toolchain/01_automation/README.md`
39
- - `sdd/04_verify/10_test/verification_harness.md`
39
+ - `sdd/03_verify/10_test/verification_harness.md`