ai-spec-dev 0.17.0 → 0.24.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.
@@ -96,9 +96,17 @@ CRITICAL \u2014 Route/Store index registration (MUST follow):
96
96
 
97
97
  CRITICAL \u2014 Cross-file function name consistency (MUST follow):
98
98
  17. When you see an "=== Files Already Generated in This Run ===" section, those file contents are the AUTHORITATIVE source
99
- of truth for exported function/variable names.
100
- NEVER rename, guess, or substitute alternative names. If the file exports "getTaskList", import "getTaskList" \u2014 not "getTasks".
101
- If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.`;
99
+ of truth for exported function/variable/action names.
100
+ NEVER rename, guess, or substitute alternative names. Copy-paste the exact identifier.
101
+ Common hallucination patterns to AVOID:
102
+ - Adding suffixes: fetchTasks \u2192 fetchTaskList, fetchTaskData, fetchTaskAll \u2190 ALL WRONG
103
+ - Changing verb: fetchTasks \u2192 getTasks, loadTasks, queryTasks \u2190 WRONG unless that's in the cache
104
+ - Changing number: createTask \u2192 createTasks \u2190 WRONG
105
+ For Pinia stores specifically: the "// public API (return object):" section or the full store content
106
+ shows EVERY available action name. If it shows "fetchTasks", that is the ONLY valid name.
107
+ If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.
108
+ ALSO applies to file paths: if you see "// exists: src/views/task-management/TaskManagement.vue",
109
+ the router import MUST use that exact path \u2014 NOT "@/views/task-management/index.vue" or any other guess.`;
102
110
  codeGenGoSystemPrompt = `You are a Senior Go Developer implementing features based on provided specifications.
103
111
 
104
112
  Rules:
@@ -4068,7 +4076,7 @@ ${content.slice(0, 1500)}`);
4068
4076
  try {
4069
4077
  const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
4070
4078
  parts.push(`// ${f}
4071
- ${content.slice(0, 800)}`);
4079
+ ${content.slice(0, 2e3)}`);
4072
4080
  } catch {
4073
4081
  }
4074
4082
  }
@@ -4473,7 +4481,7 @@ Each task object must have these exact fields:
4473
4481
  "id": "TASK-001", // sequential, zero-padded
4474
4482
  "title": "...", // short action phrase, e.g. "Add UserFavorite Prisma model"
4475
4483
  "description": "...", // 1-2 sentences, specific and actionable
4476
- "layer": "data|service|api|test|infra", // implementation layer
4484
+ "layer": "data|service|api|view|route|test|infra", // implementation layer
4477
4485
  "filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
4478
4486
  "acceptanceCriteria": ["..."], // verifiable completion conditions
4479
4487
  "dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
@@ -4481,11 +4489,34 @@ Each task object must have these exact fields:
4481
4489
  }
4482
4490
 
4483
4491
  Layer ordering guidance (implement in this order):
4484
- 1. "data" \u2014 DB schema changes, migrations, seed data
4492
+ 1. "data" \u2014 DB schema changes, migrations, seed data; TypeScript type/interface definition files
4485
4493
  2. "infra" \u2014 config, env vars, external service setup
4486
- 3. "service" \u2014 business logic, service classes
4487
- 4. "api" \u2014 controllers, routes, middleware, validators
4488
- 5. "test" \u2014 unit tests, integration tests
4494
+ 3. "service" \u2014 business logic, service classes; for frontend: HTTP API call files ONLY (src/api/ or src/apis/)
4495
+ 4. "api" \u2014 controllers, routes, middleware, validators; for frontend: state stores ONLY (src/stores/, Pinia/Vuex/Zustand/Redux)
4496
+ 5. "view" \u2014 FRONTEND ONLY: page/view components (src/views/, src/pages/) \u2014 generated AFTER stores
4497
+ 6. "route" \u2014 FRONTEND ONLY: router module files (src/router/routes/) \u2014 generated AFTER view components
4498
+ 7. "test" \u2014 unit tests, integration tests
4499
+
4500
+ CRITICAL \u2014 Frontend four-layer dependency rule (prevents BOTH naming AND filename hallucinations):
4501
+ For Vue/React frontend projects, STRICTLY follow this assignment:
4502
+ "service" \u2192 src/api/* or src/apis/* files (HTTP functions: getTaskList, createTask)
4503
+ "api" \u2192 src/stores/* files (stores call service layer \u2014 see exact function names)
4504
+ "view" \u2192 src/views/* or src/pages/* files (pages use stores \u2014 see exact action names)
4505
+ "route" \u2192 src/router/routes/* files (router imports views \u2014 sees EXACT component filenames)
4506
+
4507
+ WHY "route" must come after "view":
4508
+ The router file imports the view component by filename, e.g.:
4509
+ import('@/views/task-management/TaskManagement.vue')
4510
+ If the router is generated BEFORE the view file exists in cache, the AI will guess a generic name
4511
+ like "index.vue" (the most common fallback) instead of the real filename "TaskManagement.vue".
4512
+ By generating the router AFTER the view, the cache contains "// exists: src/views/task-management/TaskManagement.vue"
4513
+ and the AI uses the EXACT path.
4514
+
4515
+ EXAMPLE (correct, four-layer):
4516
+ TASK-001 layer:"service" src/apis/taskManagement.ts (exports getTaskList, createTask)
4517
+ TASK-002 layer:"api" src/stores/taskStore.ts (calls getTaskList \u2014 visible in cache \u2713)
4518
+ TASK-003 layer:"view" src/views/task-management/TaskManagement.vue (uses taskStore \u2014 visible in cache \u2713)
4519
+ TASK-004 layer:"route" src/router/routes/taskManagement.ts (imports TaskManagement.vue \u2014 filename visible in cache \u2713)
4489
4520
 
4490
4521
  CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
4491
4522
  - ONLY use paths that appear in the "Verified File Inventory" section of the prompt.
@@ -4535,7 +4566,7 @@ function buildTaskPrompt(spec, context) {
4535
4566
  if (context.constitution) {
4536
4567
  parts.push(`
4537
4568
  === Project Constitution (rules to follow) ===
4538
- ${context.constitution.slice(0, 1500)}`);
4569
+ ${context.constitution}`);
4539
4570
  }
4540
4571
  if (context.techStack.length > 0) {
4541
4572
  parts.push(`
@@ -4550,7 +4581,9 @@ var LAYER_ORDER = {
4550
4581
  infra: 1,
4551
4582
  service: 2,
4552
4583
  api: 3,
4553
- test: 4
4584
+ view: 4,
4585
+ route: 5,
4586
+ test: 6
4554
4587
  };
4555
4588
  var TaskGenerator = class {
4556
4589
  constructor(provider) {
@@ -4592,6 +4625,8 @@ function printTasks(tasks) {
4592
4625
  infra: chalk3.gray,
4593
4626
  service: chalk3.blue,
4594
4627
  api: chalk3.cyan,
4628
+ view: chalk3.yellow,
4629
+ route: chalk3.white,
4595
4630
  test: chalk3.green
4596
4631
  };
4597
4632
  console.log(chalk3.bold(`
@@ -5811,16 +5846,111 @@ function buildInstalledPackagesSection(context) {
5811
5846
  ${context.dependencies.join(", ")}
5812
5847
  `;
5813
5848
  }
5849
+ function extractBehavioralContract(content) {
5850
+ const lines = content.split("\n");
5851
+ const contractLines = [];
5852
+ const throwLines = [];
5853
+ let i = 0;
5854
+ while (i < lines.length) {
5855
+ const line = lines[i];
5856
+ const trimmed = line.trim();
5857
+ if (/^export\s+(interface|type|class|abstract\s+class|enum)\s/.test(trimmed)) {
5858
+ contractLines.push(line.trimEnd());
5859
+ if (trimmed.includes("{")) {
5860
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5861
+ i++;
5862
+ while (i < lines.length && depth > 0) {
5863
+ const inner = lines[i];
5864
+ contractLines.push(inner.trimEnd());
5865
+ depth += (inner.match(/\{/g) ?? []).length;
5866
+ depth -= (inner.match(/\}/g) ?? []).length;
5867
+ i++;
5868
+ }
5869
+ } else {
5870
+ i++;
5871
+ }
5872
+ continue;
5873
+ }
5874
+ if (/^export\s+const\s+\w+\s*=\s*(defineStore|createStore|createSlice)\s*\(/.test(trimmed)) {
5875
+ contractLines.push(line.trimEnd());
5876
+ let depth = (trimmed.match(/\(/g) ?? []).length - (trimmed.match(/\)/g) ?? []).length;
5877
+ i++;
5878
+ while (i < lines.length && depth > 0) {
5879
+ const inner = lines[i];
5880
+ contractLines.push(inner.trimEnd());
5881
+ depth += (inner.match(/\(/g) ?? []).length;
5882
+ depth -= (inner.match(/\)/g) ?? []).length;
5883
+ i++;
5884
+ }
5885
+ continue;
5886
+ }
5887
+ if (/^return\s*\{/.test(trimmed)) {
5888
+ contractLines.push("// public API (return object):");
5889
+ contractLines.push(line.trimEnd());
5890
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5891
+ i++;
5892
+ while (i < lines.length && depth > 0) {
5893
+ const inner = lines[i];
5894
+ contractLines.push(inner.trimEnd());
5895
+ depth += (inner.match(/\{/g) ?? []).length;
5896
+ depth -= (inner.match(/\}/g) ?? []).length;
5897
+ i++;
5898
+ }
5899
+ continue;
5900
+ }
5901
+ if (/^export\s+default\s+(async\s+)?(function|class)\b/.test(trimmed)) {
5902
+ contractLines.push(line.trimEnd());
5903
+ if (trimmed.includes("{")) {
5904
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5905
+ i++;
5906
+ while (i < lines.length && depth > 0) {
5907
+ const inner = lines[i];
5908
+ contractLines.push(inner.trimEnd());
5909
+ depth += (inner.match(/\{/g) ?? []).length;
5910
+ depth -= (inner.match(/\}/g) ?? []).length;
5911
+ i++;
5912
+ }
5913
+ } else {
5914
+ i++;
5915
+ }
5916
+ continue;
5917
+ }
5918
+ if (/^export\s/.test(trimmed)) {
5919
+ contractLines.push(line.trimEnd());
5920
+ }
5921
+ if (/throw\s+(new\s+)?\w*[Ee]rror\b|throw\s+create[A-Z]\w*|@throws/.test(line) && throwLines.length < 20) {
5922
+ throwLines.push(" // " + trimmed);
5923
+ }
5924
+ i++;
5925
+ }
5926
+ if (contractLines.length === 0 && throwLines.length === 0) {
5927
+ return content.slice(0, 3e3);
5928
+ }
5929
+ const parts = [...contractLines];
5930
+ if (throwLines.length > 0) {
5931
+ parts.push("", "// Error contracts (throws / validation):", ...throwLines);
5932
+ }
5933
+ return parts.join("\n");
5934
+ }
5814
5935
  function buildGeneratedFilesSection(cache) {
5815
5936
  if (cache.size === 0) return "";
5816
5937
  const lines = [
5817
- "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ==="
5938
+ "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ===",
5939
+ "// CRITICAL: function/action names and file paths below are ground truth. Copy them EXACTLY.",
5940
+ "// Do NOT add suffixes (List, Data, All, Info) or change casing.",
5941
+ "// For '// exists:' entries: use the EXACT filename shown \u2014 do NOT substitute index.vue or other defaults."
5818
5942
  ];
5819
5943
  for (const [filePath, content] of cache) {
5944
+ const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(filePath);
5945
+ if (isViewFile) {
5946
+ lines.push(`
5947
+ // exists: ${filePath}`);
5948
+ continue;
5949
+ }
5820
5950
  lines.push(`
5821
5951
  --- ${filePath} ---`);
5822
- lines.push(content.slice(0, 800));
5823
- if (content.length > 800) lines.push("... (truncated)");
5952
+ const isStoreOrComposable = /src[\\/](stores?|composables?)[\\/]/i.test(filePath);
5953
+ lines.push(isStoreOrComposable ? content : extractBehavioralContract(content));
5824
5954
  }
5825
5955
  return lines.join("\n") + "\n";
5826
5956
  }
@@ -6008,7 +6138,7 @@ Implement ONLY this task. Do not implement other tasks.`;
6008
6138
  const spec = await fs8.readFile(specFilePath, "utf-8");
6009
6139
  const constitutionSection = context?.constitution ? `
6010
6140
  === Project Constitution (MUST follow) ===
6011
- ${context.constitution.slice(0, 2e3)}
6141
+ ${context.constitution}
6012
6142
  ` : "";
6013
6143
  const contextSummary = context ? `Tech Stack: ${context.techStack.join(", ")}
6014
6144
  Existing files: ${context.fileStructure.slice(0, 20).join(", ")}` : "";
@@ -6102,7 +6232,7 @@ Output ONLY a valid JSON array:
6102
6232
  printTaskProgress(completedTasks++, tasks.length, task, "skip");
6103
6233
  }
6104
6234
  }
6105
- const LAYER_ORDER2 = ["data", "infra", "service", "api", "test"];
6235
+ const LAYER_ORDER2 = ["data", "infra", "service", "api", "view", "route", "test"];
6106
6236
  const layerGroups = [];
6107
6237
  for (const layer of LAYER_ORDER2) {
6108
6238
  const group = pendingTasks.filter((t) => t.layer === layer);
@@ -6125,10 +6255,9 @@ Output ONLY a valid JSON array:
6125
6255
  } else {
6126
6256
  printTaskProgress(completedTasks, tasks.length, layerTasks[0], "run");
6127
6257
  }
6128
- const generatedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6129
- const taskResultPromises = layerTasks.map(async (task) => {
6258
+ const executeTask = async (task, batchIsParallel) => {
6130
6259
  if (task.filesToTouch.length === 0) {
6131
- if (!isParallel) console.log(chalk6.gray(" No files specified, skipping."));
6260
+ if (!batchIsParallel) console.log(chalk6.gray(" No files specified, skipping."));
6132
6261
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6133
6262
  }
6134
6263
  const filePlan = await Promise.all(
@@ -6143,10 +6272,11 @@ Output ONLY a valid JSON array:
6143
6272
  );
6144
6273
  const createsNewFiles = filePlan.some((f) => f.action === "create");
6145
6274
  const taskText = `${task.title} ${task.description}`.toLowerCase();
6146
- 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"));
6275
+ 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"));
6147
6276
  if (filePlan.length === 0) {
6148
6277
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration };
6149
6278
  }
6279
+ const currentGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6150
6280
  const taskContext = `Task: ${task.id} \u2014 ${task.title}
6151
6281
  ${task.description}
6152
6282
  Acceptance: ${task.acceptanceCriteria.join("; ")}`;
@@ -6157,15 +6287,38 @@ Acceptance: ${task.acceptanceCriteria.join("; ")}`;
6157
6287
  === Current Task ===
6158
6288
  ${taskContext}`,
6159
6289
  workingDir,
6160
- constitutionSection + frontendSection + sharedConfigSection + generatedFilesSection,
6290
+ constitutionSection + frontendSection + sharedConfigSection + currentGeneratedFilesSection,
6161
6291
  systemPrompt,
6162
- isParallel ? task.id : void 0
6292
+ batchIsParallel ? task.id : void 0
6163
6293
  // prefix output lines with task ID in parallel mode
6164
6294
  );
6165
6295
  const createdFiles = filePlan.filter((fp) => fp.action === "create").map((fp) => fp.file);
6166
6296
  return { task, files, createdFiles, success, total, impliesRegistration };
6167
- });
6168
- const layerResults = await Promise.all(taskResultPromises);
6297
+ };
6298
+ const updateCacheFromBatch = async (results) => {
6299
+ for (const result of results) {
6300
+ for (const writtenFile of result.files) {
6301
+ const isCodeFile = /src[\\/](api[s]?|services?|stores?|composables?)[\\/]/i.test(writtenFile);
6302
+ const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
6303
+ if (isCodeFile || isViewFile) {
6304
+ try {
6305
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
6306
+ generatedFileCache.set(writtenFile, content);
6307
+ } catch {
6308
+ }
6309
+ }
6310
+ }
6311
+ }
6312
+ };
6313
+ const taskBatches = topoSortLayerTasks(layerTasks);
6314
+ const layerResults = [];
6315
+ for (const batch of taskBatches) {
6316
+ const batchIsParallel = batch.length > 1;
6317
+ const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6318
+ const batchResults = await Promise.all(batchResultPromises);
6319
+ layerResults.push(...batchResults);
6320
+ await updateCacheFromBatch(batchResults);
6321
+ }
6169
6322
  if (isParallel) {
6170
6323
  console.log("");
6171
6324
  }
@@ -6185,17 +6338,6 @@ ${taskContext}`,
6185
6338
  }
6186
6339
  }
6187
6340
  completedTasks += layerTasks.length;
6188
- for (const result of layerResults) {
6189
- for (const writtenFile of result.files) {
6190
- if (/src[\\/](api[s]?|services?|stores?|composables?)[\\/]/.test(writtenFile)) {
6191
- try {
6192
- const content = await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
6193
- generatedFileCache.set(writtenFile, content);
6194
- } catch {
6195
- }
6196
- }
6197
- }
6198
- }
6199
6341
  const anyImpliesRegistration = layerResults.some((r) => r.impliesRegistration);
6200
6342
  if (anyImpliesRegistration && sharedConfigPaths.size > 0 && context?.sharedConfigFiles) {
6201
6343
  const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
@@ -6295,11 +6437,48 @@ ${spec}`,
6295
6437
  console.log(chalk6.cyan("\n") + plan);
6296
6438
  }
6297
6439
  };
6440
+ function topoSortLayerTasks(tasks) {
6441
+ if (tasks.length <= 1) return [tasks];
6442
+ const idSet = new Set(tasks.map((t) => t.id));
6443
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
6444
+ const inDegree = /* @__PURE__ */ new Map();
6445
+ const dependents = /* @__PURE__ */ new Map();
6446
+ for (const task of tasks) {
6447
+ inDegree.set(task.id, 0);
6448
+ dependents.set(task.id, []);
6449
+ }
6450
+ for (const task of tasks) {
6451
+ const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
6452
+ inDegree.set(task.id, intraDeps.length);
6453
+ for (const dep of intraDeps) {
6454
+ dependents.get(dep).push(task.id);
6455
+ }
6456
+ }
6457
+ const batches = [];
6458
+ const remaining = new Set(tasks.map((t) => t.id));
6459
+ while (remaining.size > 0) {
6460
+ const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
6461
+ if (batch.length === 0) {
6462
+ batches.push([...remaining].map((id) => taskById.get(id)));
6463
+ break;
6464
+ }
6465
+ batches.push(batch);
6466
+ for (const task of batch) {
6467
+ remaining.delete(task.id);
6468
+ for (const dependent of dependents.get(task.id)) {
6469
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
6470
+ }
6471
+ }
6472
+ }
6473
+ return batches;
6474
+ }
6298
6475
  var LAYER_ICONS = {
6299
6476
  data: "\u{1F4BE}",
6300
6477
  infra: "\u2699\uFE0F ",
6301
6478
  service: "\u{1F527}",
6302
6479
  api: "\u{1F310}",
6480
+ view: "\u{1F5A5}\uFE0F ",
6481
+ route: "\u{1F5FA}\uFE0F ",
6303
6482
  test: "\u{1F9EA}"
6304
6483
  };
6305
6484
  function printTaskProgress(completed, total, task, mode) {
@@ -6808,7 +6987,7 @@ function parseConstitutionStats(content) {
6808
6987
  for (let i = section9Start + 1; i < lines.length; i++) {
6809
6988
  if (lines[i].match(/^## \d/) && i > section9Start) break;
6810
6989
  section9Lines++;
6811
- if (lines[i].trim().startsWith("-")) lessonCount++;
6990
+ if (/^-\s+.*\*\*\[\d{4}-\d{2}-\d{2}\]\*\*/.test(lines[i].trim())) lessonCount++;
6812
6991
  }
6813
6992
  return { totalLines, section9Lines, lessonCount };
6814
6993
  }
@@ -7297,7 +7476,7 @@ function detectLintCommand(workingDir) {
7297
7476
  function parseErrors(output, source) {
7298
7477
  const errors = [];
7299
7478
  if (!output.trim()) return errors;
7300
- const lines = output.split("\n").slice(-80);
7479
+ const lines = output.split("\n");
7301
7480
  for (const line of lines) {
7302
7481
  const trimmed = line.trim();
7303
7482
  if (!trimmed) continue;
@@ -7306,13 +7485,15 @@ function parseErrors(output, source) {
7306
7485
  if (trimmed.startsWith("at ")) continue;
7307
7486
  if (trimmed.startsWith("Node.js ")) continue;
7308
7487
  const fileMatch = trimmed.match(/^([^:]+\.(?:ts|js|tsx|jsx|go|py|java|rs|php)):\d+/);
7488
+ if (!fileMatch) continue;
7309
7489
  errors.push({
7310
7490
  source,
7311
- message: trimmed.slice(0, 300),
7312
- file: fileMatch?.[1]
7491
+ message: trimmed.slice(0, 400),
7492
+ file: fileMatch[1]
7313
7493
  });
7494
+ if (errors.length >= 20) break;
7314
7495
  }
7315
- return errors.slice(0, 20);
7496
+ return errors;
7316
7497
  }
7317
7498
  async function attemptFix(provider, errors, workingDir, dsl) {
7318
7499
  const results = [];
@@ -7503,7 +7684,7 @@ async function assessSpec(provider, spec, constitution) {
7503
7684
  const prompt = `Assess the following feature specification.
7504
7685
  ${constitution ? `
7505
7686
  === Project Constitution (check consistency against this) ===
7506
- ${constitution.slice(0, 1500)}
7687
+ ${constitution}
7507
7688
  ` : ""}
7508
7689
  === Feature Spec ===
7509
7690
  ${spec}`;
@@ -7622,6 +7803,42 @@ async function appendLessonsToConstitution(projectRoot, issues) {
7622
7803
  );
7623
7804
  }
7624
7805
  }
7806
+ async function appendDirectLesson(projectRoot, lessonText) {
7807
+ const constitutionPath = path14.join(projectRoot, CONSTITUTION_FILE);
7808
+ let content = "";
7809
+ try {
7810
+ content = await fs15.readFile(constitutionPath, "utf-8");
7811
+ } catch {
7812
+ return { appended: false, reason: "No constitution file found. Run `ai-spec init` first." };
7813
+ }
7814
+ const normalized = lessonText.toLowerCase().slice(0, 60);
7815
+ if (content.toLowerCase().includes(normalized)) {
7816
+ return { appended: false, reason: "Similar lesson already exists in the constitution." };
7817
+ }
7818
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7819
+ const entry = `- \u{1F4DD} **[${date}]** ${lessonText.trim()}`;
7820
+ const hasMemorySection = content.includes(MEMORY_SECTION_MARKER);
7821
+ let updatedContent;
7822
+ if (hasMemorySection) {
7823
+ const sectionStart = content.indexOf(MEMORY_SECTION_MARKER);
7824
+ const afterHeader = sectionStart + MEMORY_SECTION_HEADER.length;
7825
+ const nextSectionMatch = content.slice(afterHeader).match(/\n## \d/);
7826
+ const insertPos = nextSectionMatch ? afterHeader + nextSectionMatch.index : content.length;
7827
+ updatedContent = content.slice(0, insertPos) + entry + "\n" + content.slice(insertPos);
7828
+ } else {
7829
+ updatedContent = content + MEMORY_SECTION_HEADER + entry + "\n";
7830
+ }
7831
+ await fs15.writeFile(constitutionPath, updatedContent, "utf-8");
7832
+ const stats = parseConstitutionStats(updatedContent);
7833
+ if (stats.lessonCount >= 8) {
7834
+ console.log(
7835
+ chalk14.yellow(
7836
+ ` \u26A0 \xA79 now has ${stats.lessonCount} lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
7837
+ )
7838
+ );
7839
+ }
7840
+ return { appended: true };
7841
+ }
7625
7842
  async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
7626
7843
  console.log(chalk14.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"));
7627
7844
  const issues = extractIssuesFromReview(reviewText);
@@ -8290,6 +8507,7 @@ Existing API/service files:`);
8290
8507
  // core/mock-server-generator.ts
8291
8508
  import * as path18 from "path";
8292
8509
  import * as fs19 from "fs-extra";
8510
+ import { spawn } from "child_process";
8293
8511
  function typeToFixture(fieldName, typeDesc) {
8294
8512
  const t = typeDesc.toLowerCase();
8295
8513
  if (t.includes("boolean") || t === "bool") return true;
@@ -8616,6 +8834,151 @@ import { handlers } from './handlers';
8616
8834
  export const worker = setupWorker(...handlers);
8617
8835
  `;
8618
8836
  }
8837
+ var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
8838
+ function findViteConfigFile(projectDir) {
8839
+ for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
8840
+ if (fs19.existsSync(path18.join(projectDir, f))) return f;
8841
+ }
8842
+ return null;
8843
+ }
8844
+ function buildViteProxyEntries(endpoints, mockPort) {
8845
+ const prefixes = /* @__PURE__ */ new Set();
8846
+ for (const ep of endpoints) {
8847
+ const parts = ep.path.split("/").filter(Boolean);
8848
+ if (parts.length > 0) prefixes.add(`/${parts[0]}`);
8849
+ }
8850
+ if (prefixes.size === 0) prefixes.add("/api");
8851
+ const target = `http://localhost:${mockPort}`;
8852
+ return Array.from(prefixes).map((p) => ` '${p}': { target: '${target}', changeOrigin: true },`).join("\n");
8853
+ }
8854
+ function generateViteMockConfigTs(baseConfigFile, mockPort, endpoints) {
8855
+ const importPath = `./${baseConfigFile.replace(/\.(ts|mts|js|mjs)$/, "")}`;
8856
+ const proxyEntries = buildViteProxyEntries(endpoints, mockPort);
8857
+ return `// Auto-generated by ai-spec mock --serve
8858
+ // LOCAL DEVELOPMENT ONLY \u2014 do not commit this file
8859
+ // Remove with: ai-spec mock --restore
8860
+ import { defineConfig, mergeConfig } from 'vite';
8861
+
8862
+ export default defineConfig(async (env) => {
8863
+ const mod = await import('${importPath}');
8864
+ const baseConfigOrFn = mod.default;
8865
+ const baseConfig =
8866
+ typeof baseConfigOrFn === 'function'
8867
+ ? await baseConfigOrFn(env)
8868
+ : baseConfigOrFn;
8869
+
8870
+ return mergeConfig(baseConfig ?? {}, {
8871
+ server: {
8872
+ proxy: {
8873
+ ${proxyEntries}
8874
+ },
8875
+ },
8876
+ });
8877
+ });
8878
+ `;
8879
+ }
8880
+ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
8881
+ const framework = detectFrontendFramework(frontendDir);
8882
+ const actions = [];
8883
+ if (framework === "vite") {
8884
+ const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
8885
+ const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
8886
+ const mockConfigPath = path18.join(frontendDir, "vite.config.ai-spec-mock.ts");
8887
+ await fs19.writeFile(mockConfigPath, mockConfigContent, "utf-8");
8888
+ actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
8889
+ const pkgPath = path18.join(frontendDir, "package.json");
8890
+ if (await fs19.pathExists(pkgPath)) {
8891
+ const pkg = await fs19.readJson(pkgPath);
8892
+ pkg.scripts = pkg.scripts ?? {};
8893
+ const originalValue = pkg.scripts["dev:mock"] ?? null;
8894
+ pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
8895
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8896
+ actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
8897
+ }
8898
+ const lock2 = { framework, mockPort, frontendDir, actions };
8899
+ await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
8900
+ return { framework, applied: true, devCommand: "npm run dev:mock" };
8901
+ }
8902
+ if (framework === "cra") {
8903
+ const pkgPath = path18.join(frontendDir, "package.json");
8904
+ if (await fs19.pathExists(pkgPath)) {
8905
+ const pkg = await fs19.readJson(pkgPath);
8906
+ const originalProxy = pkg.proxy ?? null;
8907
+ pkg.proxy = `http://localhost:${mockPort}`;
8908
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8909
+ actions.push({ type: "patched-pkg-proxy", originalProxy });
8910
+ const lock2 = { framework, mockPort, frontendDir, actions };
8911
+ await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
8912
+ return { framework, applied: true, devCommand: "npm start" };
8913
+ }
8914
+ return { framework, applied: false, devCommand: null, note: "No package.json found." };
8915
+ }
8916
+ const lock = { framework, mockPort, frontendDir, actions };
8917
+ await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
8918
+ 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}`;
8919
+ return { framework, applied: false, devCommand: null, note: manualNote };
8920
+ }
8921
+ async function restoreMockProxy(frontendDir) {
8922
+ const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
8923
+ if (!await fs19.pathExists(lockPath)) {
8924
+ return { restored: false, note: "No lock file found \u2014 nothing to restore." };
8925
+ }
8926
+ const lock = await fs19.readJson(lockPath);
8927
+ for (const action of lock.actions) {
8928
+ if (action.type === "wrote-file") {
8929
+ const fp = path18.join(frontendDir, action.filePath);
8930
+ if (await fs19.pathExists(fp)) await fs19.remove(fp);
8931
+ } else if (action.type === "added-pkg-script") {
8932
+ const pkgPath = path18.join(frontendDir, "package.json");
8933
+ if (await fs19.pathExists(pkgPath)) {
8934
+ const pkg = await fs19.readJson(pkgPath);
8935
+ if (action.originalValue == null) {
8936
+ delete pkg.scripts?.[action.key];
8937
+ } else {
8938
+ pkg.scripts = pkg.scripts ?? {};
8939
+ pkg.scripts[action.key] = action.originalValue;
8940
+ }
8941
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8942
+ }
8943
+ } else if (action.type === "patched-pkg-proxy") {
8944
+ const pkgPath = path18.join(frontendDir, "package.json");
8945
+ if (await fs19.pathExists(pkgPath)) {
8946
+ const pkg = await fs19.readJson(pkgPath);
8947
+ if (action.originalProxy == null) {
8948
+ delete pkg.proxy;
8949
+ } else {
8950
+ pkg.proxy = action.originalProxy;
8951
+ }
8952
+ await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
8953
+ }
8954
+ }
8955
+ }
8956
+ if (lock.mockServerPid) {
8957
+ try {
8958
+ process.kill(lock.mockServerPid, "SIGTERM");
8959
+ } catch {
8960
+ }
8961
+ }
8962
+ await fs19.remove(lockPath);
8963
+ return { restored: true };
8964
+ }
8965
+ function startMockServerBackground(serverJsPath, port) {
8966
+ const child = spawn("node", [serverJsPath], {
8967
+ detached: true,
8968
+ stdio: "ignore",
8969
+ env: { ...process.env, MOCK_PORT: String(port) }
8970
+ });
8971
+ child.unref();
8972
+ return child.pid;
8973
+ }
8974
+ async function saveMockServerPid(frontendDir, pid) {
8975
+ const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
8976
+ if (await fs19.pathExists(lockPath)) {
8977
+ const lock = await fs19.readJson(lockPath);
8978
+ lock.mockServerPid = pid;
8979
+ await fs19.writeJson(lockPath, lock, { spaces: 2 });
8980
+ }
8981
+ }
8619
8982
  async function generateMockAssets(dsl, projectDir, opts = {}) {
8620
8983
  const port = opts.port ?? 3001;
8621
8984
  const outputDir = path18.join(projectDir, opts.outputDir ?? "mock");
@@ -8752,7 +9115,7 @@ Rules:
8752
9115
  function buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context) {
8753
9116
  const constitutionSection = context?.constitution ? `
8754
9117
  === Project Constitution (all changes must comply) ===
8755
- ${context.constitution.slice(0, 1500)}
9118
+ ${context.constitution}
8756
9119
  ` : "";
8757
9120
  const dslSummary = existingDsl ? `
8758
9121
  === Current DSL Summary (for reference) ===
@@ -9264,7 +9627,7 @@ program.command("create").description("Generate a feature spec and kick off code
9264
9627
  ).option(
9265
9628
  "--codegen-provider <name>",
9266
9629
  "AI provider for code generation (defaults to --provider)"
9267
- ).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) => {
9630
+ ).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) => {
9268
9631
  const currentDir = process.cwd();
9269
9632
  const config2 = await loadConfig(currentDir);
9270
9633
  if (!idea) {
@@ -9279,7 +9642,42 @@ program.command("create").description("Generate a feature spec and kick off code
9279
9642
  console.log(chalk17.cyan(`
9280
9643
  [Workspace] Detected workspace: ${workspaceConfig.name}`));
9281
9644
  console.log(chalk17.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
9282
- await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9645
+ const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9646
+ if (opts.serve) {
9647
+ console.log(chalk17.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9648
+ const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
9649
+ const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
9650
+ if (!backendResult) {
9651
+ console.log(chalk17.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
9652
+ } else {
9653
+ const mockPort = 3001;
9654
+ const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
9655
+ const serverJsPath = path21.join(backendResult.repoAbsPath, "mock", "server.js");
9656
+ console.log(chalk17.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
9657
+ const pid = startMockServerBackground(serverJsPath, mockPort);
9658
+ console.log(chalk17.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
9659
+ if (frontendResult) {
9660
+ const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
9661
+ await saveMockServerPid(frontendResult.repoAbsPath, pid);
9662
+ if (proxyResult.applied) {
9663
+ console.log(chalk17.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
9664
+ console.log(chalk17.bold.cyan(`
9665
+ Ready! Run your frontend dev server:`));
9666
+ console.log(chalk17.white(` cd ${frontendResult.repoAbsPath}`));
9667
+ console.log(chalk17.white(` ${proxyResult.devCommand}`));
9668
+ console.log(chalk17.gray(`
9669
+ When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
9670
+ } else {
9671
+ console.log(chalk17.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
9672
+ if (proxyResult.note) console.log(chalk17.gray(` ${proxyResult.note}`));
9673
+ console.log(chalk17.gray(` Mock server: http://localhost:${mockPort}`));
9674
+ }
9675
+ } else {
9676
+ console.log(chalk17.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
9677
+ console.log(chalk17.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
9678
+ }
9679
+ }
9680
+ }
9283
9681
  return;
9284
9682
  }
9285
9683
  const specProviderName = opts.provider || config2.provider || "gemini";
@@ -9308,6 +9706,9 @@ program.command("create").description("Generate a feature spec and kick off code
9308
9706
  }
9309
9707
  if (context.constitution) {
9310
9708
  console.log(chalk17.green(` Constitution : found (.ai-spec-constitution.md)`));
9709
+ if (context.constitution.length > 6e3) {
9710
+ console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
9711
+ }
9311
9712
  } else {
9312
9713
  console.log(chalk17.yellow(" Constitution : not found \u2014 auto-generating..."));
9313
9714
  try {
@@ -9357,12 +9758,32 @@ program.command("create").description("Generate a feature spec and kick off code
9357
9758
  finalSpec = await refiner.refineLoop(initialSpec);
9358
9759
  }
9359
9760
  const featureSlug = slugify(idea);
9360
- if (!opts.auto && !opts.skipAssessment) {
9361
- console.log(chalk17.blue("\n[3.4/6] Spec quality assessment..."));
9761
+ const minScore = config2.minSpecScore ?? 0;
9762
+ const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
9763
+ if (shouldRunAssessment) {
9764
+ if (!opts.auto) {
9765
+ console.log(chalk17.blue("\n[3.4/6] Spec quality assessment..."));
9766
+ }
9362
9767
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
9363
9768
  if (assessment) {
9364
- printSpecAssessment(assessment);
9365
- } else {
9769
+ if (!opts.auto) printSpecAssessment(assessment);
9770
+ if (minScore > 0 && assessment.overallScore < minScore) {
9771
+ if (opts.force) {
9772
+ console.log(chalk17.yellow(`
9773
+ \u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
9774
+ } else {
9775
+ console.log(chalk17.red(`
9776
+ \u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
9777
+ if (!opts.auto) {
9778
+ console.log(chalk17.gray(` Address the issues above and re-run, or use --force to bypass.`));
9779
+ } else {
9780
+ console.log(chalk17.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
9781
+ }
9782
+ console.log(chalk17.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
9783
+ process.exit(1);
9784
+ }
9785
+ }
9786
+ } else if (!opts.auto) {
9366
9787
  console.log(chalk17.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
9367
9788
  }
9368
9789
  }
@@ -9696,7 +10117,7 @@ program.command("init").description(`Analyze codebase and generate Project Const
9696
10117
  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(
9697
10118
  "--codegen <mode>",
9698
10119
  "Default code generation mode (claude-code|api|plan)"
9699
- ).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) => {
10120
+ ).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) => {
9700
10121
  const currentDir = process.cwd();
9701
10122
  const configPath = path21.join(currentDir, CONFIG_FILE);
9702
10123
  if (opts.clearKeys) {
@@ -9746,6 +10167,14 @@ File: ${KEY_STORE_FILE}`));
9746
10167
  if (opts.codegen) updated.codegen = opts.codegen;
9747
10168
  if (opts.codegenProvider) updated.codegenProvider = opts.codegenProvider;
9748
10169
  if (opts.codegenModel) updated.codegenModel = opts.codegenModel;
10170
+ if (opts.minSpecScore !== void 0) {
10171
+ const score = parseInt(opts.minSpecScore, 10);
10172
+ if (isNaN(score) || score < 0 || score > 10) {
10173
+ console.error(chalk17.red(" --min-spec-score must be a number between 0 and 10"));
10174
+ process.exit(1);
10175
+ }
10176
+ updated.minSpecScore = score;
10177
+ }
9749
10178
  await fs22.writeJson(configPath, updated, { spaces: 2 });
9750
10179
  console.log(chalk17.green(`\u2714 Config saved to ${configPath}`));
9751
10180
  console.log(JSON.stringify(updated, null, 2));
@@ -9889,6 +10318,9 @@ async function runSingleRepoPipelineInWorkspace(opts) {
9889
10318
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
9890
10319
  console.log(chalk17.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
9891
10320
  console.log(chalk17.gray(` Dependencies: ${context.dependencies.length} packages`));
10321
+ if (context.constitution && context.constitution.length > 6e3) {
10322
+ console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10323
+ }
9892
10324
  if (!context.constitution) {
9893
10325
  console.log(chalk17.yellow(` Constitution: not found \u2014 auto-generating...`));
9894
10326
  try {
@@ -10109,7 +10541,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10109
10541
  const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
10110
10542
  if (!repoConfig) {
10111
10543
  console.log(chalk17.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
10112
- results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null });
10544
+ results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
10113
10545
  continue;
10114
10546
  }
10115
10547
  const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
@@ -10160,11 +10592,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10160
10592
  contractDsls.set(repoReq.repoName, dsl);
10161
10593
  console.log(chalk17.green(` Contract stored for downstream repos.`));
10162
10594
  }
10163
- results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl });
10595
+ results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
10164
10596
  console.log(chalk17.green(` \u2714 ${repoReq.repoName} complete`));
10165
10597
  } catch (err) {
10166
10598
  console.error(chalk17.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
10167
- results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null });
10599
+ results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
10168
10600
  }
10169
10601
  }
10170
10602
  console.log(chalk17.bold.green("\n\u2714 Multi-repo pipeline complete!"));
@@ -10176,6 +10608,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10176
10608
  const specInfo = r.specFile ? chalk17.gray(` \u2192 ${r.specFile}`) : "";
10177
10609
  console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
10178
10610
  }
10611
+ return results;
10179
10612
  }
10180
10613
  var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
10181
10614
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
@@ -10373,6 +10806,9 @@ program.command("update").description("Update an existing spec with a change req
10373
10806
  console.log(chalk17.gray(" Loading project context..."));
10374
10807
  const loader = new ContextLoader(currentDir);
10375
10808
  const context = await loader.loadProjectContext();
10809
+ if (context.constitution && context.constitution.length > 6e3) {
10810
+ console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10811
+ }
10376
10812
  const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
10377
10813
  const { type: repoType } = await _detectRepoType(currentDir);
10378
10814
  const updater = new SpecUpdater(provider);
@@ -10408,7 +10844,7 @@ program.command("update").description("Update an existing spec with a change req
10408
10844
  const specContent = await fs22.readFile(result.newSpecPath, "utf-8");
10409
10845
  const constitutionSection = context.constitution ? `
10410
10846
  === Project Constitution (MUST follow) ===
10411
- ${context.constitution.slice(0, 2e3)}
10847
+ ${context.constitution}
10412
10848
  ` : "";
10413
10849
  const dslSection = result.updatedDsl ? `
10414
10850
  === DSL Context ===
@@ -10493,13 +10929,20 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
10493
10929
  process.exit(1);
10494
10930
  }
10495
10931
  });
10496
- 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(
10497
- "--workspace",
10498
- "Generate mock assets for all backend repos in the workspace"
10499
- ).action(async (opts) => {
10932
+ 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) => {
10500
10933
  const currentDir = process.cwd();
10501
10934
  const port = parseInt(opts.port, 10) || 3001;
10502
10935
  console.log(chalk17.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"));
10936
+ if (opts.restore) {
10937
+ const frontendDir = opts.frontend ? path21.resolve(opts.frontend) : currentDir;
10938
+ const r = await restoreMockProxy(frontendDir);
10939
+ if (r.restored) {
10940
+ console.log(chalk17.green(" \u2714 Proxy restored and mock server stopped."));
10941
+ } else {
10942
+ console.log(chalk17.yellow(` ${r.note ?? "Nothing to restore."}`));
10943
+ }
10944
+ return;
10945
+ }
10503
10946
  if (opts.workspace) {
10504
10947
  const workspaceLoader = new WorkspaceLoader(currentDir);
10505
10948
  const workspaceConfig = await workspaceLoader.load();
@@ -10565,11 +11008,43 @@ program.command("mock").description("Generate a standalone mock server + proxy c
10565
11008
  console.log(chalk17.green(` ${f.path}`));
10566
11009
  console.log(chalk17.gray(` ${f.description}`));
10567
11010
  }
11011
+ if (opts.serve) {
11012
+ const serverJsPath = path21.join(currentDir, "mock", "server.js");
11013
+ if (!await fs22.pathExists(serverJsPath)) {
11014
+ console.error(chalk17.red(" mock/server.js not found \u2014 generation may have failed."));
11015
+ process.exit(1);
11016
+ }
11017
+ const pid = startMockServerBackground(serverJsPath, port);
11018
+ console.log(chalk17.green(`
11019
+ \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
11020
+ if (opts.frontend) {
11021
+ const frontendDir = path21.resolve(opts.frontend);
11022
+ const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
11023
+ await saveMockServerPid(frontendDir, pid);
11024
+ if (proxyResult.applied) {
11025
+ console.log(chalk17.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11026
+ console.log(chalk17.bold.cyan(`
11027
+ Ready! Open a new terminal and run:`));
11028
+ console.log(chalk17.white(` cd ${frontendDir}`));
11029
+ console.log(chalk17.white(` ${proxyResult.devCommand}`));
11030
+ console.log(chalk17.gray(`
11031
+ When done: ai-spec mock --restore --frontend ${frontendDir}`));
11032
+ } else {
11033
+ console.log(chalk17.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11034
+ if (proxyResult.note) console.log(chalk17.gray(` ${proxyResult.note}`));
11035
+ }
11036
+ } else {
11037
+ console.log(chalk17.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11038
+ console.log(chalk17.gray(` Mock server: http://localhost:${port}`));
11039
+ }
11040
+ return;
11041
+ }
10568
11042
  console.log(chalk17.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"));
10569
11043
  console.log(chalk17.white(` 1. Install express (if not already):`));
10570
11044
  console.log(chalk17.gray(` npm install --save-dev express`));
10571
11045
  console.log(chalk17.white(` 2. Start mock server:`));
10572
11046
  console.log(chalk17.gray(` node mock/server.js`));
11047
+ console.log(chalk17.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
10573
11048
  console.log(chalk17.white(` 3. Configure your frontend to proxy API calls to:`));
10574
11049
  console.log(chalk17.gray(` http://localhost:${port}`));
10575
11050
  if (opts.proxy) {
@@ -10581,6 +11056,27 @@ program.command("mock").description("Generate a standalone mock server + proxy c
10581
11056
  console.log(chalk17.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
10582
11057
  }
10583
11058
  });
11059
+ 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) => {
11060
+ const currentDir = process.cwd();
11061
+ if (!lesson) {
11062
+ const { default: inquirerInput } = await import("@inquirer/prompts").then(
11063
+ (m) => ({ default: m.input })
11064
+ );
11065
+ lesson = await inquirerInput({
11066
+ message: "What lesson or engineering decision should be recorded?",
11067
+ validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
11068
+ });
11069
+ }
11070
+ const result = await appendDirectLesson(currentDir, lesson.trim());
11071
+ if (result.appended) {
11072
+ console.log(chalk17.green(`
11073
+ \u2714 Lesson appended to constitution \xA79`));
11074
+ console.log(chalk17.gray(` File: .ai-spec-constitution.md`));
11075
+ } else {
11076
+ console.log(chalk17.yellow(`
11077
+ \u26A0 Not appended: ${result.reason}`));
11078
+ }
11079
+ });
10584
11080
  if (process.argv.length <= 2) {
10585
11081
  (async () => {
10586
11082
  const currentDir = process.cwd();