ai-spec-dev 0.17.0 → 0.25.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/dist/cli/index.js CHANGED
@@ -117,9 +117,17 @@ CRITICAL \u2014 Route/Store index registration (MUST follow):
117
117
 
118
118
  CRITICAL \u2014 Cross-file function name consistency (MUST follow):
119
119
  17. When you see an "=== Files Already Generated in This Run ===" section, those file contents are the AUTHORITATIVE source
120
- of truth for exported function/variable names.
121
- NEVER rename, guess, or substitute alternative names. If the file exports "getTaskList", import "getTaskList" \u2014 not "getTasks".
122
- If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.`;
120
+ of truth for exported function/variable/action names.
121
+ NEVER rename, guess, or substitute alternative names. Copy-paste the exact identifier.
122
+ Common hallucination patterns to AVOID:
123
+ - Adding suffixes: fetchTasks \u2192 fetchTaskList, fetchTaskData, fetchTaskAll \u2190 ALL WRONG
124
+ - Changing verb: fetchTasks \u2192 getTasks, loadTasks, queryTasks \u2190 WRONG unless that's in the cache
125
+ - Changing number: createTask \u2192 createTasks \u2190 WRONG
126
+ For Pinia stores specifically: the "// public API (return object):" section or the full store content
127
+ shows EVERY available action name. If it shows "fetchTasks", that is the ONLY valid name.
128
+ If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.
129
+ ALSO applies to file paths: if you see "// exists: src/views/task-management/TaskManagement.vue",
130
+ the router import MUST use that exact path \u2014 NOT "@/views/task-management/index.vue" or any other guess.`;
123
131
  codeGenGoSystemPrompt = `You are a Senior Go Developer implementing features based on provided specifications.
124
132
 
125
133
  Rules:
@@ -4089,7 +4097,7 @@ ${content.slice(0, 1500)}`);
4089
4097
  try {
4090
4098
  const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
4091
4099
  parts.push(`// ${f}
4092
- ${content.slice(0, 800)}`);
4100
+ ${content.slice(0, 2e3)}`);
4093
4101
  } catch {
4094
4102
  }
4095
4103
  }
@@ -4494,7 +4502,7 @@ Each task object must have these exact fields:
4494
4502
  "id": "TASK-001", // sequential, zero-padded
4495
4503
  "title": "...", // short action phrase, e.g. "Add UserFavorite Prisma model"
4496
4504
  "description": "...", // 1-2 sentences, specific and actionable
4497
- "layer": "data|service|api|test|infra", // implementation layer
4505
+ "layer": "data|service|api|view|route|test|infra", // implementation layer
4498
4506
  "filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
4499
4507
  "acceptanceCriteria": ["..."], // verifiable completion conditions
4500
4508
  "dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
@@ -4502,11 +4510,34 @@ Each task object must have these exact fields:
4502
4510
  }
4503
4511
 
4504
4512
  Layer ordering guidance (implement in this order):
4505
- 1. "data" \u2014 DB schema changes, migrations, seed data
4513
+ 1. "data" \u2014 DB schema changes, migrations, seed data; TypeScript type/interface definition files
4506
4514
  2. "infra" \u2014 config, env vars, external service setup
4507
- 3. "service" \u2014 business logic, service classes
4508
- 4. "api" \u2014 controllers, routes, middleware, validators
4509
- 5. "test" \u2014 unit tests, integration tests
4515
+ 3. "service" \u2014 business logic, service classes; for frontend: HTTP API call files ONLY (src/api/ or src/apis/)
4516
+ 4. "api" \u2014 controllers, routes, middleware, validators; for frontend: state stores ONLY (src/stores/, Pinia/Vuex/Zustand/Redux)
4517
+ 5. "view" \u2014 FRONTEND ONLY: page/view components (src/views/, src/pages/) \u2014 generated AFTER stores
4518
+ 6. "route" \u2014 FRONTEND ONLY: router module files (src/router/routes/) \u2014 generated AFTER view components
4519
+ 7. "test" \u2014 unit tests, integration tests
4520
+
4521
+ CRITICAL \u2014 Frontend four-layer dependency rule (prevents BOTH naming AND filename hallucinations):
4522
+ For Vue/React frontend projects, STRICTLY follow this assignment:
4523
+ "service" \u2192 src/api/* or src/apis/* files (HTTP functions: getTaskList, createTask)
4524
+ "api" \u2192 src/stores/* files (stores call service layer \u2014 see exact function names)
4525
+ "view" \u2192 src/views/* or src/pages/* files (pages use stores \u2014 see exact action names)
4526
+ "route" \u2192 src/router/routes/* files (router imports views \u2014 sees EXACT component filenames)
4527
+
4528
+ WHY "route" must come after "view":
4529
+ The router file imports the view component by filename, e.g.:
4530
+ import('@/views/task-management/TaskManagement.vue')
4531
+ If the router is generated BEFORE the view file exists in cache, the AI will guess a generic name
4532
+ like "index.vue" (the most common fallback) instead of the real filename "TaskManagement.vue".
4533
+ By generating the router AFTER the view, the cache contains "// exists: src/views/task-management/TaskManagement.vue"
4534
+ and the AI uses the EXACT path.
4535
+
4536
+ EXAMPLE (correct, four-layer):
4537
+ TASK-001 layer:"service" src/apis/taskManagement.ts (exports getTaskList, createTask)
4538
+ TASK-002 layer:"api" src/stores/taskStore.ts (calls getTaskList \u2014 visible in cache \u2713)
4539
+ TASK-003 layer:"view" src/views/task-management/TaskManagement.vue (uses taskStore \u2014 visible in cache \u2713)
4540
+ TASK-004 layer:"route" src/router/routes/taskManagement.ts (imports TaskManagement.vue \u2014 filename visible in cache \u2713)
4510
4541
 
4511
4542
  CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
4512
4543
  - ONLY use paths that appear in the "Verified File Inventory" section of the prompt.
@@ -4556,7 +4587,7 @@ function buildTaskPrompt(spec, context) {
4556
4587
  if (context.constitution) {
4557
4588
  parts.push(`
4558
4589
  === Project Constitution (rules to follow) ===
4559
- ${context.constitution.slice(0, 1500)}`);
4590
+ ${context.constitution}`);
4560
4591
  }
4561
4592
  if (context.techStack.length > 0) {
4562
4593
  parts.push(`
@@ -4571,7 +4602,9 @@ var LAYER_ORDER = {
4571
4602
  infra: 1,
4572
4603
  service: 2,
4573
4604
  api: 3,
4574
- test: 4
4605
+ view: 4,
4606
+ route: 5,
4607
+ test: 6
4575
4608
  };
4576
4609
  var TaskGenerator = class {
4577
4610
  constructor(provider) {
@@ -4613,6 +4646,8 @@ function printTasks(tasks) {
4613
4646
  infra: import_chalk3.default.gray,
4614
4647
  service: import_chalk3.default.blue,
4615
4648
  api: import_chalk3.default.cyan,
4649
+ view: import_chalk3.default.yellow,
4650
+ route: import_chalk3.default.white,
4616
4651
  test: import_chalk3.default.green
4617
4652
  };
4618
4653
  console.log(import_chalk3.default.bold(`
@@ -5832,16 +5867,111 @@ function buildInstalledPackagesSection(context) {
5832
5867
  ${context.dependencies.join(", ")}
5833
5868
  `;
5834
5869
  }
5870
+ function extractBehavioralContract(content) {
5871
+ const lines = content.split("\n");
5872
+ const contractLines = [];
5873
+ const throwLines = [];
5874
+ let i = 0;
5875
+ while (i < lines.length) {
5876
+ const line = lines[i];
5877
+ const trimmed = line.trim();
5878
+ if (/^export\s+(interface|type|class|abstract\s+class|enum)\s/.test(trimmed)) {
5879
+ contractLines.push(line.trimEnd());
5880
+ if (trimmed.includes("{")) {
5881
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5882
+ i++;
5883
+ while (i < lines.length && depth > 0) {
5884
+ const inner = lines[i];
5885
+ contractLines.push(inner.trimEnd());
5886
+ depth += (inner.match(/\{/g) ?? []).length;
5887
+ depth -= (inner.match(/\}/g) ?? []).length;
5888
+ i++;
5889
+ }
5890
+ } else {
5891
+ i++;
5892
+ }
5893
+ continue;
5894
+ }
5895
+ if (/^export\s+const\s+\w+\s*=\s*(defineStore|createStore|createSlice)\s*\(/.test(trimmed)) {
5896
+ contractLines.push(line.trimEnd());
5897
+ let depth = (trimmed.match(/\(/g) ?? []).length - (trimmed.match(/\)/g) ?? []).length;
5898
+ i++;
5899
+ while (i < lines.length && depth > 0) {
5900
+ const inner = lines[i];
5901
+ contractLines.push(inner.trimEnd());
5902
+ depth += (inner.match(/\(/g) ?? []).length;
5903
+ depth -= (inner.match(/\)/g) ?? []).length;
5904
+ i++;
5905
+ }
5906
+ continue;
5907
+ }
5908
+ if (/^return\s*\{/.test(trimmed)) {
5909
+ contractLines.push("// public API (return object):");
5910
+ contractLines.push(line.trimEnd());
5911
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5912
+ i++;
5913
+ while (i < lines.length && depth > 0) {
5914
+ const inner = lines[i];
5915
+ contractLines.push(inner.trimEnd());
5916
+ depth += (inner.match(/\{/g) ?? []).length;
5917
+ depth -= (inner.match(/\}/g) ?? []).length;
5918
+ i++;
5919
+ }
5920
+ continue;
5921
+ }
5922
+ if (/^export\s+default\s+(async\s+)?(function|class)\b/.test(trimmed)) {
5923
+ contractLines.push(line.trimEnd());
5924
+ if (trimmed.includes("{")) {
5925
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5926
+ i++;
5927
+ while (i < lines.length && depth > 0) {
5928
+ const inner = lines[i];
5929
+ contractLines.push(inner.trimEnd());
5930
+ depth += (inner.match(/\{/g) ?? []).length;
5931
+ depth -= (inner.match(/\}/g) ?? []).length;
5932
+ i++;
5933
+ }
5934
+ } else {
5935
+ i++;
5936
+ }
5937
+ continue;
5938
+ }
5939
+ if (/^export\s/.test(trimmed)) {
5940
+ contractLines.push(line.trimEnd());
5941
+ }
5942
+ if (/throw\s+(new\s+)?\w*[Ee]rror\b|throw\s+create[A-Z]\w*|@throws/.test(line) && throwLines.length < 20) {
5943
+ throwLines.push(" // " + trimmed);
5944
+ }
5945
+ i++;
5946
+ }
5947
+ if (contractLines.length === 0 && throwLines.length === 0) {
5948
+ return content.slice(0, 3e3);
5949
+ }
5950
+ const parts = [...contractLines];
5951
+ if (throwLines.length > 0) {
5952
+ parts.push("", "// Error contracts (throws / validation):", ...throwLines);
5953
+ }
5954
+ return parts.join("\n");
5955
+ }
5835
5956
  function buildGeneratedFilesSection(cache) {
5836
5957
  if (cache.size === 0) return "";
5837
5958
  const lines = [
5838
- "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ==="
5959
+ "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ===",
5960
+ "// CRITICAL: function/action names and file paths below are ground truth. Copy them EXACTLY.",
5961
+ "// Do NOT add suffixes (List, Data, All, Info) or change casing.",
5962
+ "// For '// exists:' entries: use the EXACT filename shown \u2014 do NOT substitute index.vue or other defaults."
5839
5963
  ];
5840
5964
  for (const [filePath, content] of cache) {
5965
+ const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(filePath);
5966
+ if (isViewFile) {
5967
+ lines.push(`
5968
+ // exists: ${filePath}`);
5969
+ continue;
5970
+ }
5841
5971
  lines.push(`
5842
5972
  --- ${filePath} ---`);
5843
- lines.push(content.slice(0, 800));
5844
- if (content.length > 800) lines.push("... (truncated)");
5973
+ const isStoreOrComposable = /src[\\/](stores?|composables?)[\\/]/i.test(filePath);
5974
+ lines.push(isStoreOrComposable ? content : extractBehavioralContract(content));
5845
5975
  }
5846
5976
  return lines.join("\n") + "\n";
5847
5977
  }
@@ -6029,7 +6159,7 @@ Implement ONLY this task. Do not implement other tasks.`;
6029
6159
  const spec = await fs8.readFile(specFilePath, "utf-8");
6030
6160
  const constitutionSection = context?.constitution ? `
6031
6161
  === Project Constitution (MUST follow) ===
6032
- ${context.constitution.slice(0, 2e3)}
6162
+ ${context.constitution}
6033
6163
  ` : "";
6034
6164
  const contextSummary = context ? `Tech Stack: ${context.techStack.join(", ")}
6035
6165
  Existing files: ${context.fileStructure.slice(0, 20).join(", ")}` : "";
@@ -6123,7 +6253,7 @@ Output ONLY a valid JSON array:
6123
6253
  printTaskProgress(completedTasks++, tasks.length, task, "skip");
6124
6254
  }
6125
6255
  }
6126
- const LAYER_ORDER2 = ["data", "infra", "service", "api", "test"];
6256
+ const LAYER_ORDER2 = ["data", "infra", "service", "api", "view", "route", "test"];
6127
6257
  const layerGroups = [];
6128
6258
  for (const layer of LAYER_ORDER2) {
6129
6259
  const group = pendingTasks.filter((t) => t.layer === layer);
@@ -6146,10 +6276,9 @@ Output ONLY a valid JSON array:
6146
6276
  } else {
6147
6277
  printTaskProgress(completedTasks, tasks.length, layerTasks[0], "run");
6148
6278
  }
6149
- const generatedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6150
- const taskResultPromises = layerTasks.map(async (task) => {
6279
+ const executeTask = async (task, batchIsParallel) => {
6151
6280
  if (task.filesToTouch.length === 0) {
6152
- if (!isParallel) console.log(import_chalk6.default.gray(" No files specified, skipping."));
6281
+ if (!batchIsParallel) console.log(import_chalk6.default.gray(" No files specified, skipping."));
6153
6282
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6154
6283
  }
6155
6284
  const filePlan = await Promise.all(
@@ -6164,10 +6293,11 @@ Output ONLY a valid JSON array:
6164
6293
  );
6165
6294
  const createsNewFiles = filePlan.some((f) => f.action === "create");
6166
6295
  const taskText = `${task.title} ${task.description}`.toLowerCase();
6167
- const impliesRegistration = createsNewFiles && (taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
6296
+ const impliesRegistration = createsNewFiles && (task.layer === "route" || task.layer === "view" || task.layer === "api" || taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
6168
6297
  if (filePlan.length === 0) {
6169
6298
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration };
6170
6299
  }
6300
+ const currentGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6171
6301
  const taskContext = `Task: ${task.id} \u2014 ${task.title}
6172
6302
  ${task.description}
6173
6303
  Acceptance: ${task.acceptanceCriteria.join("; ")}`;
@@ -6178,15 +6308,38 @@ Acceptance: ${task.acceptanceCriteria.join("; ")}`;
6178
6308
  === Current Task ===
6179
6309
  ${taskContext}`,
6180
6310
  workingDir,
6181
- constitutionSection + frontendSection + sharedConfigSection + generatedFilesSection,
6311
+ constitutionSection + frontendSection + sharedConfigSection + currentGeneratedFilesSection,
6182
6312
  systemPrompt,
6183
- isParallel ? task.id : void 0
6313
+ batchIsParallel ? task.id : void 0
6184
6314
  // prefix output lines with task ID in parallel mode
6185
6315
  );
6186
6316
  const createdFiles = filePlan.filter((fp) => fp.action === "create").map((fp) => fp.file);
6187
6317
  return { task, files, createdFiles, success, total, impliesRegistration };
6188
- });
6189
- const layerResults = await Promise.all(taskResultPromises);
6318
+ };
6319
+ const updateCacheFromBatch = async (results) => {
6320
+ for (const result of results) {
6321
+ for (const writtenFile of result.files) {
6322
+ const isCodeFile = /src[\\/](api[s]?|services?|stores?|composables?)[\\/]/i.test(writtenFile);
6323
+ const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
6324
+ if (isCodeFile || isViewFile) {
6325
+ try {
6326
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
6327
+ generatedFileCache.set(writtenFile, content);
6328
+ } catch {
6329
+ }
6330
+ }
6331
+ }
6332
+ }
6333
+ };
6334
+ const taskBatches = topoSortLayerTasks(layerTasks);
6335
+ const layerResults = [];
6336
+ for (const batch of taskBatches) {
6337
+ const batchIsParallel = batch.length > 1;
6338
+ const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6339
+ const batchResults = await Promise.all(batchResultPromises);
6340
+ layerResults.push(...batchResults);
6341
+ await updateCacheFromBatch(batchResults);
6342
+ }
6190
6343
  if (isParallel) {
6191
6344
  console.log("");
6192
6345
  }
@@ -6206,17 +6359,6 @@ ${taskContext}`,
6206
6359
  }
6207
6360
  }
6208
6361
  completedTasks += layerTasks.length;
6209
- for (const result of layerResults) {
6210
- for (const writtenFile of result.files) {
6211
- if (/src[\\/](api[s]?|services?|stores?|composables?)[\\/]/.test(writtenFile)) {
6212
- try {
6213
- const content = await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
6214
- generatedFileCache.set(writtenFile, content);
6215
- } catch {
6216
- }
6217
- }
6218
- }
6219
- }
6220
6362
  const anyImpliesRegistration = layerResults.some((r) => r.impliesRegistration);
6221
6363
  if (anyImpliesRegistration && sharedConfigPaths.size > 0 && context?.sharedConfigFiles) {
6222
6364
  const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
@@ -6316,11 +6458,48 @@ ${spec}`,
6316
6458
  console.log(import_chalk6.default.cyan("\n") + plan);
6317
6459
  }
6318
6460
  };
6461
+ function topoSortLayerTasks(tasks) {
6462
+ if (tasks.length <= 1) return [tasks];
6463
+ const idSet = new Set(tasks.map((t) => t.id));
6464
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
6465
+ const inDegree = /* @__PURE__ */ new Map();
6466
+ const dependents = /* @__PURE__ */ new Map();
6467
+ for (const task of tasks) {
6468
+ inDegree.set(task.id, 0);
6469
+ dependents.set(task.id, []);
6470
+ }
6471
+ for (const task of tasks) {
6472
+ const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
6473
+ inDegree.set(task.id, intraDeps.length);
6474
+ for (const dep of intraDeps) {
6475
+ dependents.get(dep).push(task.id);
6476
+ }
6477
+ }
6478
+ const batches = [];
6479
+ const remaining = new Set(tasks.map((t) => t.id));
6480
+ while (remaining.size > 0) {
6481
+ const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
6482
+ if (batch.length === 0) {
6483
+ batches.push([...remaining].map((id) => taskById.get(id)));
6484
+ break;
6485
+ }
6486
+ batches.push(batch);
6487
+ for (const task of batch) {
6488
+ remaining.delete(task.id);
6489
+ for (const dependent of dependents.get(task.id)) {
6490
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
6491
+ }
6492
+ }
6493
+ }
6494
+ return batches;
6495
+ }
6319
6496
  var LAYER_ICONS = {
6320
6497
  data: "\u{1F4BE}",
6321
6498
  infra: "\u2699\uFE0F ",
6322
6499
  service: "\u{1F527}",
6323
6500
  api: "\u{1F310}",
6501
+ view: "\u{1F5A5}\uFE0F ",
6502
+ route: "\u{1F5FA}\uFE0F ",
6324
6503
  test: "\u{1F9EA}"
6325
6504
  };
6326
6505
  function printTaskProgress(completed, total, task, mode) {
@@ -6829,7 +7008,7 @@ function parseConstitutionStats(content) {
6829
7008
  for (let i = section9Start + 1; i < lines.length; i++) {
6830
7009
  if (lines[i].match(/^## \d/) && i > section9Start) break;
6831
7010
  section9Lines++;
6832
- if (lines[i].trim().startsWith("-")) lessonCount++;
7011
+ if (/^-\s+.*\*\*\[\d{4}-\d{2}-\d{2}\]\*\*/.test(lines[i].trim())) lessonCount++;
6833
7012
  }
6834
7013
  return { totalLines, section9Lines, lessonCount };
6835
7014
  }
@@ -7318,7 +7497,7 @@ function detectLintCommand(workingDir) {
7318
7497
  function parseErrors(output, source) {
7319
7498
  const errors = [];
7320
7499
  if (!output.trim()) return errors;
7321
- const lines = output.split("\n").slice(-80);
7500
+ const lines = output.split("\n");
7322
7501
  for (const line of lines) {
7323
7502
  const trimmed = line.trim();
7324
7503
  if (!trimmed) continue;
@@ -7327,13 +7506,15 @@ function parseErrors(output, source) {
7327
7506
  if (trimmed.startsWith("at ")) continue;
7328
7507
  if (trimmed.startsWith("Node.js ")) continue;
7329
7508
  const fileMatch = trimmed.match(/^([^:]+\.(?:ts|js|tsx|jsx|go|py|java|rs|php)):\d+/);
7509
+ if (!fileMatch) continue;
7330
7510
  errors.push({
7331
7511
  source,
7332
- message: trimmed.slice(0, 300),
7333
- file: fileMatch?.[1]
7512
+ message: trimmed.slice(0, 400),
7513
+ file: fileMatch[1]
7334
7514
  });
7515
+ if (errors.length >= 20) break;
7335
7516
  }
7336
- return errors.slice(0, 20);
7517
+ return errors;
7337
7518
  }
7338
7519
  async function attemptFix(provider, errors, workingDir, dsl) {
7339
7520
  const results = [];
@@ -7524,7 +7705,7 @@ async function assessSpec(provider, spec, constitution) {
7524
7705
  const prompt = `Assess the following feature specification.
7525
7706
  ${constitution ? `
7526
7707
  === Project Constitution (check consistency against this) ===
7527
- ${constitution.slice(0, 1500)}
7708
+ ${constitution}
7528
7709
  ` : ""}
7529
7710
  === Feature Spec ===
7530
7711
  ${spec}`;
@@ -7643,6 +7824,42 @@ async function appendLessonsToConstitution(projectRoot, issues) {
7643
7824
  );
7644
7825
  }
7645
7826
  }
7827
+ async function appendDirectLesson(projectRoot, lessonText) {
7828
+ const constitutionPath = path14.join(projectRoot, CONSTITUTION_FILE);
7829
+ let content = "";
7830
+ try {
7831
+ content = await fs15.readFile(constitutionPath, "utf-8");
7832
+ } catch {
7833
+ return { appended: false, reason: "No constitution file found. Run `ai-spec init` first." };
7834
+ }
7835
+ const normalized = lessonText.toLowerCase().slice(0, 60);
7836
+ if (content.toLowerCase().includes(normalized)) {
7837
+ return { appended: false, reason: "Similar lesson already exists in the constitution." };
7838
+ }
7839
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7840
+ const entry = `- \u{1F4DD} **[${date}]** ${lessonText.trim()}`;
7841
+ const hasMemorySection = content.includes(MEMORY_SECTION_MARKER);
7842
+ let updatedContent;
7843
+ if (hasMemorySection) {
7844
+ const sectionStart = content.indexOf(MEMORY_SECTION_MARKER);
7845
+ const afterHeader = sectionStart + MEMORY_SECTION_HEADER.length;
7846
+ const nextSectionMatch = content.slice(afterHeader).match(/\n## \d/);
7847
+ const insertPos = nextSectionMatch ? afterHeader + nextSectionMatch.index : content.length;
7848
+ updatedContent = content.slice(0, insertPos) + entry + "\n" + content.slice(insertPos);
7849
+ } else {
7850
+ updatedContent = content + MEMORY_SECTION_HEADER + entry + "\n";
7851
+ }
7852
+ await fs15.writeFile(constitutionPath, updatedContent, "utf-8");
7853
+ const stats = parseConstitutionStats(updatedContent);
7854
+ if (stats.lessonCount >= 8) {
7855
+ console.log(
7856
+ import_chalk14.default.yellow(
7857
+ ` \u26A0 \xA79 now has ${stats.lessonCount} lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
7858
+ )
7859
+ );
7860
+ }
7861
+ return { appended: true };
7862
+ }
7646
7863
  async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
7647
7864
  console.log(import_chalk14.default.blue("\n\u2500\u2500\u2500 Knowledge Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7648
7865
  const issues = extractIssuesFromReview(reviewText);
@@ -8311,6 +8528,7 @@ Existing API/service files:`);
8311
8528
  // core/mock-server-generator.ts
8312
8529
  var path18 = __toESM(require("path"));
8313
8530
  var fs19 = __toESM(require("fs-extra"));
8531
+ var import_child_process5 = require("child_process");
8314
8532
  function typeToFixture(fieldName, typeDesc) {
8315
8533
  const t = typeDesc.toLowerCase();
8316
8534
  if (t.includes("boolean") || t === "bool") return true;
@@ -8637,6 +8855,151 @@ import { handlers } from './handlers';
8637
8855
  export const worker = setupWorker(...handlers);
8638
8856
  `;
8639
8857
  }
8858
+ var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
8859
+ function findViteConfigFile(projectDir) {
8860
+ for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
8861
+ if (fs19.existsSync(path18.join(projectDir, f))) return f;
8862
+ }
8863
+ return null;
8864
+ }
8865
+ function buildViteProxyEntries(endpoints, mockPort) {
8866
+ const prefixes = /* @__PURE__ */ new Set();
8867
+ for (const ep of endpoints) {
8868
+ const parts = ep.path.split("/").filter(Boolean);
8869
+ if (parts.length > 0) prefixes.add(`/${parts[0]}`);
8870
+ }
8871
+ if (prefixes.size === 0) prefixes.add("/api");
8872
+ const target = `http://localhost:${mockPort}`;
8873
+ return Array.from(prefixes).map((p) => ` '${p}': { target: '${target}', changeOrigin: true },`).join("\n");
8874
+ }
8875
+ function generateViteMockConfigTs(baseConfigFile, mockPort, endpoints) {
8876
+ const importPath = `./${baseConfigFile.replace(/\.(ts|mts|js|mjs)$/, "")}`;
8877
+ const proxyEntries = buildViteProxyEntries(endpoints, mockPort);
8878
+ return `// Auto-generated by ai-spec mock --serve
8879
+ // LOCAL DEVELOPMENT ONLY \u2014 do not commit this file
8880
+ // Remove with: ai-spec mock --restore
8881
+ import { defineConfig, mergeConfig } from 'vite';
8882
+
8883
+ export default defineConfig(async (env) => {
8884
+ const mod = await import('${importPath}');
8885
+ const baseConfigOrFn = mod.default;
8886
+ const baseConfig =
8887
+ typeof baseConfigOrFn === 'function'
8888
+ ? await baseConfigOrFn(env)
8889
+ : baseConfigOrFn;
8890
+
8891
+ return mergeConfig(baseConfig ?? {}, {
8892
+ server: {
8893
+ proxy: {
8894
+ ${proxyEntries}
8895
+ },
8896
+ },
8897
+ });
8898
+ });
8899
+ `;
8900
+ }
8901
+ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
8902
+ const framework = detectFrontendFramework(frontendDir);
8903
+ const actions = [];
8904
+ if (framework === "vite") {
8905
+ const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
8906
+ const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
8907
+ const mockConfigPath = path18.join(frontendDir, "vite.config.ai-spec-mock.ts");
8908
+ await fs19.writeFile(mockConfigPath, mockConfigContent, "utf-8");
8909
+ actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
8910
+ const pkgPath = path18.join(frontendDir, "package.json");
8911
+ if (await fs19.pathExists(pkgPath)) {
8912
+ const pkg = await fs19.readJson(pkgPath);
8913
+ pkg.scripts = pkg.scripts ?? {};
8914
+ const originalValue = pkg.scripts["dev:mock"] ?? null;
8915
+ pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
8916
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8917
+ actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
8918
+ }
8919
+ const lock2 = { framework, mockPort, frontendDir, actions };
8920
+ await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
8921
+ return { framework, applied: true, devCommand: "npm run dev:mock" };
8922
+ }
8923
+ if (framework === "cra") {
8924
+ const pkgPath = path18.join(frontendDir, "package.json");
8925
+ if (await fs19.pathExists(pkgPath)) {
8926
+ const pkg = await fs19.readJson(pkgPath);
8927
+ const originalProxy = pkg.proxy ?? null;
8928
+ pkg.proxy = `http://localhost:${mockPort}`;
8929
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8930
+ actions.push({ type: "patched-pkg-proxy", originalProxy });
8931
+ const lock2 = { framework, mockPort, frontendDir, actions };
8932
+ await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
8933
+ return { framework, applied: true, devCommand: "npm start" };
8934
+ }
8935
+ return { framework, applied: false, devCommand: null, note: "No package.json found." };
8936
+ }
8937
+ const lock = { framework, mockPort, frontendDir, actions };
8938
+ await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
8939
+ const manualNote = framework === "next" ? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}` : `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
8940
+ return { framework, applied: false, devCommand: null, note: manualNote };
8941
+ }
8942
+ async function restoreMockProxy(frontendDir) {
8943
+ const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
8944
+ if (!await fs19.pathExists(lockPath)) {
8945
+ return { restored: false, note: "No lock file found \u2014 nothing to restore." };
8946
+ }
8947
+ const lock = await fs19.readJson(lockPath);
8948
+ for (const action of lock.actions) {
8949
+ if (action.type === "wrote-file") {
8950
+ const fp = path18.join(frontendDir, action.filePath);
8951
+ if (await fs19.pathExists(fp)) await fs19.remove(fp);
8952
+ } else if (action.type === "added-pkg-script") {
8953
+ const pkgPath = path18.join(frontendDir, "package.json");
8954
+ if (await fs19.pathExists(pkgPath)) {
8955
+ const pkg = await fs19.readJson(pkgPath);
8956
+ if (action.originalValue == null) {
8957
+ delete pkg.scripts?.[action.key];
8958
+ } else {
8959
+ pkg.scripts = pkg.scripts ?? {};
8960
+ pkg.scripts[action.key] = action.originalValue;
8961
+ }
8962
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8963
+ }
8964
+ } else if (action.type === "patched-pkg-proxy") {
8965
+ const pkgPath = path18.join(frontendDir, "package.json");
8966
+ if (await fs19.pathExists(pkgPath)) {
8967
+ const pkg = await fs19.readJson(pkgPath);
8968
+ if (action.originalProxy == null) {
8969
+ delete pkg.proxy;
8970
+ } else {
8971
+ pkg.proxy = action.originalProxy;
8972
+ }
8973
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8974
+ }
8975
+ }
8976
+ }
8977
+ if (lock.mockServerPid) {
8978
+ try {
8979
+ process.kill(lock.mockServerPid, "SIGTERM");
8980
+ } catch {
8981
+ }
8982
+ }
8983
+ await fs19.remove(lockPath);
8984
+ return { restored: true };
8985
+ }
8986
+ function startMockServerBackground(serverJsPath, port) {
8987
+ const child = (0, import_child_process5.spawn)("node", [serverJsPath], {
8988
+ detached: true,
8989
+ stdio: "ignore",
8990
+ env: { ...process.env, MOCK_PORT: String(port) }
8991
+ });
8992
+ child.unref();
8993
+ return child.pid;
8994
+ }
8995
+ async function saveMockServerPid(frontendDir, pid) {
8996
+ const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
8997
+ if (await fs19.pathExists(lockPath)) {
8998
+ const lock = await fs19.readJson(lockPath);
8999
+ lock.mockServerPid = pid;
9000
+ await fs19.writeJson(lockPath, lock, { spaces: 2 });
9001
+ }
9002
+ }
8640
9003
  async function generateMockAssets(dsl, projectDir, opts = {}) {
8641
9004
  const port = opts.port ?? 3001;
8642
9005
  const outputDir = path18.join(projectDir, opts.outputDir ?? "mock");
@@ -8773,7 +9136,7 @@ Rules:
8773
9136
  function buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context) {
8774
9137
  const constitutionSection = context?.constitution ? `
8775
9138
  === Project Constitution (all changes must comply) ===
8776
- ${context.constitution.slice(0, 1500)}
9139
+ ${context.constitution}
8777
9140
  ` : "";
8778
9141
  const dslSummary = existingDsl ? `
8779
9142
  === Current DSL Summary (for reference) ===
@@ -9285,7 +9648,7 @@ program.command("create").description("Generate a feature spec and kick off code
9285
9648
  ).option(
9286
9649
  "--codegen-provider <name>",
9287
9650
  "AI provider for code generation (defaults to --provider)"
9288
- ).option("--codegen-model <name>", "Model for code generation").option("--codegen-key <key>", "API key for code generation (if different)").option("--skip-worktree", "Skip git worktree creation (auto-set for frontend projects)").option("--worktree", "Force git worktree creation even for frontend projects").option("--skip-review", "Skip automated code review").option("--skip-tasks", "Skip task generation (just generate spec)").option("--auto", "Run claude non-interactively via -p flag (saves tokens)").option("--fast", "Skip interactive spec refinement, proceed immediately with initial spec").option("--resume", "Resume an interrupted run \u2014 skip tasks already marked as done").option("--skip-dsl", "Skip DSL extraction step").option("--skip-tests", "Skip test skeleton generation").option("--skip-error-feedback", "Skip error feedback loop (test/lint auto-fix)").option("--tdd", "TDD mode: generate failing tests first, then generate implementation to pass them").option("--skip-assessment", "Skip spec quality pre-assessment before the Approval Gate").action(async (idea, opts) => {
9651
+ ).option("--codegen-model <name>", "Model for code generation").option("--codegen-key <key>", "API key for code generation (if different)").option("--skip-worktree", "Skip git worktree creation (auto-set for frontend projects)").option("--worktree", "Force git worktree creation even for frontend projects").option("--skip-review", "Skip automated code review").option("--skip-tasks", "Skip task generation (just generate spec)").option("--auto", "Run claude non-interactively via -p flag (saves tokens)").option("--fast", "Skip interactive spec refinement, proceed immediately with initial spec").option("--resume", "Resume an interrupted run \u2014 skip tasks already marked as done").option("--skip-dsl", "Skip DSL extraction step").option("--skip-tests", "Skip test skeleton generation").option("--skip-error-feedback", "Skip error feedback loop (test/lint auto-fix)").option("--tdd", "TDD mode: generate failing tests first, then generate implementation to pass them").option("--skip-assessment", "Skip spec quality pre-assessment before the Approval Gate").option("--force", "Bypass the spec quality score gate even if score is below minSpecScore").option("--serve", "After workspace pipeline completes, auto-start mock server + patch frontend proxy").action(async (idea, opts) => {
9289
9652
  const currentDir = process.cwd();
9290
9653
  const config2 = await loadConfig(currentDir);
9291
9654
  if (!idea) {
@@ -9300,7 +9663,42 @@ program.command("create").description("Generate a feature spec and kick off code
9300
9663
  console.log(import_chalk17.default.cyan(`
9301
9664
  [Workspace] Detected workspace: ${workspaceConfig.name}`));
9302
9665
  console.log(import_chalk17.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
9303
- await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9666
+ const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9667
+ if (opts.serve) {
9668
+ console.log(import_chalk17.default.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9669
+ const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
9670
+ const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
9671
+ if (!backendResult) {
9672
+ console.log(import_chalk17.default.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
9673
+ } else {
9674
+ const mockPort = 3001;
9675
+ const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
9676
+ const serverJsPath = path21.join(backendResult.repoAbsPath, "mock", "server.js");
9677
+ console.log(import_chalk17.default.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
9678
+ const pid = startMockServerBackground(serverJsPath, mockPort);
9679
+ console.log(import_chalk17.default.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
9680
+ if (frontendResult) {
9681
+ const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
9682
+ await saveMockServerPid(frontendResult.repoAbsPath, pid);
9683
+ if (proxyResult.applied) {
9684
+ console.log(import_chalk17.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
9685
+ console.log(import_chalk17.default.bold.cyan(`
9686
+ Ready! Run your frontend dev server:`));
9687
+ console.log(import_chalk17.default.white(` cd ${frontendResult.repoAbsPath}`));
9688
+ console.log(import_chalk17.default.white(` ${proxyResult.devCommand}`));
9689
+ console.log(import_chalk17.default.gray(`
9690
+ When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
9691
+ } else {
9692
+ console.log(import_chalk17.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
9693
+ if (proxyResult.note) console.log(import_chalk17.default.gray(` ${proxyResult.note}`));
9694
+ console.log(import_chalk17.default.gray(` Mock server: http://localhost:${mockPort}`));
9695
+ }
9696
+ } else {
9697
+ console.log(import_chalk17.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
9698
+ console.log(import_chalk17.default.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
9699
+ }
9700
+ }
9701
+ }
9304
9702
  return;
9305
9703
  }
9306
9704
  const specProviderName = opts.provider || config2.provider || "gemini";
@@ -9329,6 +9727,9 @@ program.command("create").description("Generate a feature spec and kick off code
9329
9727
  }
9330
9728
  if (context.constitution) {
9331
9729
  console.log(import_chalk17.default.green(` Constitution : found (.ai-spec-constitution.md)`));
9730
+ if (context.constitution.length > 6e3) {
9731
+ console.log(import_chalk17.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
9732
+ }
9332
9733
  } else {
9333
9734
  console.log(import_chalk17.default.yellow(" Constitution : not found \u2014 auto-generating..."));
9334
9735
  try {
@@ -9378,12 +9779,32 @@ program.command("create").description("Generate a feature spec and kick off code
9378
9779
  finalSpec = await refiner.refineLoop(initialSpec);
9379
9780
  }
9380
9781
  const featureSlug = slugify(idea);
9381
- if (!opts.auto && !opts.skipAssessment) {
9382
- console.log(import_chalk17.default.blue("\n[3.4/6] Spec quality assessment..."));
9782
+ const minScore = config2.minSpecScore ?? 0;
9783
+ const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
9784
+ if (shouldRunAssessment) {
9785
+ if (!opts.auto) {
9786
+ console.log(import_chalk17.default.blue("\n[3.4/6] Spec quality assessment..."));
9787
+ }
9383
9788
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
9384
9789
  if (assessment) {
9385
- printSpecAssessment(assessment);
9386
- } else {
9790
+ if (!opts.auto) printSpecAssessment(assessment);
9791
+ if (minScore > 0 && assessment.overallScore < minScore) {
9792
+ if (opts.force) {
9793
+ console.log(import_chalk17.default.yellow(`
9794
+ \u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
9795
+ } else {
9796
+ console.log(import_chalk17.default.red(`
9797
+ \u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
9798
+ if (!opts.auto) {
9799
+ console.log(import_chalk17.default.gray(` Address the issues above and re-run, or use --force to bypass.`));
9800
+ } else {
9801
+ console.log(import_chalk17.default.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
9802
+ }
9803
+ console.log(import_chalk17.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
9804
+ process.exit(1);
9805
+ }
9806
+ }
9807
+ } else if (!opts.auto) {
9387
9808
  console.log(import_chalk17.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
9388
9809
  }
9389
9810
  }
@@ -9717,7 +10138,7 @@ program.command("init").description(`Analyze codebase and generate Project Const
9717
10138
  program.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option(
9718
10139
  "--codegen <mode>",
9719
10140
  "Default code generation mode (claude-code|api|plan)"
9720
- ).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
10141
+ ).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
9721
10142
  const currentDir = process.cwd();
9722
10143
  const configPath = path21.join(currentDir, CONFIG_FILE);
9723
10144
  if (opts.clearKeys) {
@@ -9767,6 +10188,14 @@ File: ${KEY_STORE_FILE}`));
9767
10188
  if (opts.codegen) updated.codegen = opts.codegen;
9768
10189
  if (opts.codegenProvider) updated.codegenProvider = opts.codegenProvider;
9769
10190
  if (opts.codegenModel) updated.codegenModel = opts.codegenModel;
10191
+ if (opts.minSpecScore !== void 0) {
10192
+ const score = parseInt(opts.minSpecScore, 10);
10193
+ if (isNaN(score) || score < 0 || score > 10) {
10194
+ console.error(import_chalk17.default.red(" --min-spec-score must be a number between 0 and 10"));
10195
+ process.exit(1);
10196
+ }
10197
+ updated.minSpecScore = score;
10198
+ }
9770
10199
  await fs22.writeJson(configPath, updated, { spaces: 2 });
9771
10200
  console.log(import_chalk17.default.green(`\u2714 Config saved to ${configPath}`));
9772
10201
  console.log(JSON.stringify(updated, null, 2));
@@ -9910,6 +10339,9 @@ async function runSingleRepoPipelineInWorkspace(opts) {
9910
10339
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
9911
10340
  console.log(import_chalk17.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
9912
10341
  console.log(import_chalk17.default.gray(` Dependencies: ${context.dependencies.length} packages`));
10342
+ if (context.constitution && context.constitution.length > 6e3) {
10343
+ console.log(import_chalk17.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10344
+ }
9913
10345
  if (!context.constitution) {
9914
10346
  console.log(import_chalk17.default.yellow(` Constitution: not found \u2014 auto-generating...`));
9915
10347
  try {
@@ -10130,7 +10562,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10130
10562
  const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
10131
10563
  if (!repoConfig) {
10132
10564
  console.log(import_chalk17.default.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
10133
- results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null });
10565
+ results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
10134
10566
  continue;
10135
10567
  }
10136
10568
  const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
@@ -10181,11 +10613,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10181
10613
  contractDsls.set(repoReq.repoName, dsl);
10182
10614
  console.log(import_chalk17.default.green(` Contract stored for downstream repos.`));
10183
10615
  }
10184
- results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl });
10616
+ results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
10185
10617
  console.log(import_chalk17.default.green(` \u2714 ${repoReq.repoName} complete`));
10186
10618
  } catch (err) {
10187
10619
  console.error(import_chalk17.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
10188
- results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null });
10620
+ results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
10189
10621
  }
10190
10622
  }
10191
10623
  console.log(import_chalk17.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
@@ -10197,6 +10629,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10197
10629
  const specInfo = r.specFile ? import_chalk17.default.gray(` \u2192 ${r.specFile}`) : "";
10198
10630
  console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
10199
10631
  }
10632
+ return results;
10200
10633
  }
10201
10634
  var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
10202
10635
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
@@ -10394,6 +10827,9 @@ program.command("update").description("Update an existing spec with a change req
10394
10827
  console.log(import_chalk17.default.gray(" Loading project context..."));
10395
10828
  const loader = new ContextLoader(currentDir);
10396
10829
  const context = await loader.loadProjectContext();
10830
+ if (context.constitution && context.constitution.length > 6e3) {
10831
+ console.log(import_chalk17.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10832
+ }
10397
10833
  const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
10398
10834
  const { type: repoType } = await _detectRepoType(currentDir);
10399
10835
  const updater = new SpecUpdater(provider);
@@ -10429,7 +10865,7 @@ program.command("update").description("Update an existing spec with a change req
10429
10865
  const specContent = await fs22.readFile(result.newSpecPath, "utf-8");
10430
10866
  const constitutionSection = context.constitution ? `
10431
10867
  === Project Constitution (MUST follow) ===
10432
- ${context.constitution.slice(0, 2e3)}
10868
+ ${context.constitution}
10433
10869
  ` : "";
10434
10870
  const dslSection = result.updatedDsl ? `
10435
10871
  === DSL Context ===
@@ -10514,13 +10950,20 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
10514
10950
  process.exit(1);
10515
10951
  }
10516
10952
  });
10517
- program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option(
10518
- "--workspace",
10519
- "Generate mock assets for all backend repos in the workspace"
10520
- ).action(async (opts) => {
10953
+ program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
10521
10954
  const currentDir = process.cwd();
10522
10955
  const port = parseInt(opts.port, 10) || 3001;
10523
10956
  console.log(import_chalk17.default.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10957
+ if (opts.restore) {
10958
+ const frontendDir = opts.frontend ? path21.resolve(opts.frontend) : currentDir;
10959
+ const r = await restoreMockProxy(frontendDir);
10960
+ if (r.restored) {
10961
+ console.log(import_chalk17.default.green(" \u2714 Proxy restored and mock server stopped."));
10962
+ } else {
10963
+ console.log(import_chalk17.default.yellow(` ${r.note ?? "Nothing to restore."}`));
10964
+ }
10965
+ return;
10966
+ }
10524
10967
  if (opts.workspace) {
10525
10968
  const workspaceLoader = new WorkspaceLoader(currentDir);
10526
10969
  const workspaceConfig = await workspaceLoader.load();
@@ -10586,11 +11029,43 @@ program.command("mock").description("Generate a standalone mock server + proxy c
10586
11029
  console.log(import_chalk17.default.green(` ${f.path}`));
10587
11030
  console.log(import_chalk17.default.gray(` ${f.description}`));
10588
11031
  }
11032
+ if (opts.serve) {
11033
+ const serverJsPath = path21.join(currentDir, "mock", "server.js");
11034
+ if (!await fs22.pathExists(serverJsPath)) {
11035
+ console.error(import_chalk17.default.red(" mock/server.js not found \u2014 generation may have failed."));
11036
+ process.exit(1);
11037
+ }
11038
+ const pid = startMockServerBackground(serverJsPath, port);
11039
+ console.log(import_chalk17.default.green(`
11040
+ \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
11041
+ if (opts.frontend) {
11042
+ const frontendDir = path21.resolve(opts.frontend);
11043
+ const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
11044
+ await saveMockServerPid(frontendDir, pid);
11045
+ if (proxyResult.applied) {
11046
+ console.log(import_chalk17.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11047
+ console.log(import_chalk17.default.bold.cyan(`
11048
+ Ready! Open a new terminal and run:`));
11049
+ console.log(import_chalk17.default.white(` cd ${frontendDir}`));
11050
+ console.log(import_chalk17.default.white(` ${proxyResult.devCommand}`));
11051
+ console.log(import_chalk17.default.gray(`
11052
+ When done: ai-spec mock --restore --frontend ${frontendDir}`));
11053
+ } else {
11054
+ console.log(import_chalk17.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11055
+ if (proxyResult.note) console.log(import_chalk17.default.gray(` ${proxyResult.note}`));
11056
+ }
11057
+ } else {
11058
+ console.log(import_chalk17.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11059
+ console.log(import_chalk17.default.gray(` Mock server: http://localhost:${port}`));
11060
+ }
11061
+ return;
11062
+ }
10589
11063
  console.log(import_chalk17.default.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10590
11064
  console.log(import_chalk17.default.white(` 1. Install express (if not already):`));
10591
11065
  console.log(import_chalk17.default.gray(` npm install --save-dev express`));
10592
11066
  console.log(import_chalk17.default.white(` 2. Start mock server:`));
10593
11067
  console.log(import_chalk17.default.gray(` node mock/server.js`));
11068
+ console.log(import_chalk17.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
10594
11069
  console.log(import_chalk17.default.white(` 3. Configure your frontend to proxy API calls to:`));
10595
11070
  console.log(import_chalk17.default.gray(` http://localhost:${port}`));
10596
11071
  if (opts.proxy) {
@@ -10602,6 +11077,27 @@ program.command("mock").description("Generate a standalone mock server + proxy c
10602
11077
  console.log(import_chalk17.default.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
10603
11078
  }
10604
11079
  });
11080
+ program.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
11081
+ const currentDir = process.cwd();
11082
+ if (!lesson) {
11083
+ const { default: inquirerInput } = await import("@inquirer/prompts").then(
11084
+ (m) => ({ default: m.input })
11085
+ );
11086
+ lesson = await inquirerInput({
11087
+ message: "What lesson or engineering decision should be recorded?",
11088
+ validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
11089
+ });
11090
+ }
11091
+ const result = await appendDirectLesson(currentDir, lesson.trim());
11092
+ if (result.appended) {
11093
+ console.log(import_chalk17.default.green(`
11094
+ \u2714 Lesson appended to constitution \xA79`));
11095
+ console.log(import_chalk17.default.gray(` File: .ai-spec-constitution.md`));
11096
+ } else {
11097
+ console.log(import_chalk17.default.yellow(`
11098
+ \u26A0 Not appended: ${result.reason}`));
11099
+ }
11100
+ });
10605
11101
  if (process.argv.length <= 2) {
10606
11102
  (async () => {
10607
11103
  const currentDir = process.cwd();