@stackwright-pro/mcp 0.2.0-alpha.16 → 0.2.0-alpha.18

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/server.mjs CHANGED
@@ -1110,7 +1110,8 @@ function setupPackages(opts) {
1110
1110
  }
1111
1111
 
1112
1112
  // src/tools/questions.ts
1113
- import { readFile } from "fs/promises";
1113
+ import { readFile, writeFile } from "fs/promises";
1114
+ import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync, renameSync } from "fs";
1114
1115
  import { join } from "path";
1115
1116
  import { z as z8 } from "zod";
1116
1117
 
@@ -1383,12 +1384,12 @@ function registerQuestionTools(server2) {
1383
1384
  content: [
1384
1385
  {
1385
1386
  type: "text",
1386
- text: JSON.stringify({
1387
- phase,
1388
- skipped: true,
1389
- reason: "No questions to present (all filtered by dependsOn conditions)",
1390
- answers: []
1391
- })
1387
+ text: `Phase "${phase}" has no questions to present. Do NOT call ask_user_question. Call stackwright_pro_save_phase_answers({ phase: "${phase}", rawAnswers: [] }) directly, then proceed to the execution step for this phase.`
1388
+ },
1389
+ {
1390
+ type: "text",
1391
+ // Empty array — second block always present so the foreman's two-block contract holds
1392
+ text: JSON.stringify([])
1392
1393
  }
1393
1394
  ],
1394
1395
  isError: false
@@ -1409,11 +1410,149 @@ function registerQuestionTools(server2) {
1409
1410
  };
1410
1411
  }
1411
1412
  );
1413
+ server2.tool(
1414
+ "stackwright_pro_get_next_question",
1415
+ "Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
1416
+ { phase: z8.string().describe('Phase name e.g. "designer", "api", "auth"') },
1417
+ async ({ phase }) => {
1418
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1419
+ if (!SAFE_PHASE.test(phase)) {
1420
+ return {
1421
+ content: [
1422
+ {
1423
+ type: "text",
1424
+ text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
1425
+ }
1426
+ ],
1427
+ isError: true
1428
+ };
1429
+ }
1430
+ const cwd = process.cwd();
1431
+ const questionsPath = join(cwd, ".stackwright", "questions", `${phase}.json`);
1432
+ let questions = [];
1433
+ try {
1434
+ const raw = await readFile(questionsPath, "utf-8");
1435
+ const parsed = JSON.parse(raw);
1436
+ questions = parsed.questions ?? [];
1437
+ } catch {
1438
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1439
+ }
1440
+ if (questions.length === 0) {
1441
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1442
+ }
1443
+ const answersPath = join(cwd, ".stackwright", "answers", `${phase}.json`);
1444
+ let answeredIds = /* @__PURE__ */ new Set();
1445
+ try {
1446
+ const raw = await readFile(answersPath, "utf-8");
1447
+ const parsed = JSON.parse(raw);
1448
+ answeredIds = new Set(Object.keys(parsed.answers ?? {}));
1449
+ } catch {
1450
+ }
1451
+ const next = questions.find((q) => !answeredIds.has(q.id));
1452
+ if (!next) {
1453
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1454
+ }
1455
+ return {
1456
+ content: [
1457
+ {
1458
+ type: "text",
1459
+ text: JSON.stringify({
1460
+ done: false,
1461
+ questionId: next.id,
1462
+ question: next.question,
1463
+ type: next.type,
1464
+ options: next.options ?? null,
1465
+ help: next.help ?? null,
1466
+ index: questions.indexOf(next) + 1,
1467
+ total: questions.length
1468
+ })
1469
+ }
1470
+ ]
1471
+ };
1472
+ }
1473
+ );
1474
+ server2.tool(
1475
+ "stackwright_pro_record_answer",
1476
+ "Records a single answer to a phase question. Appends to .stackwright/answers/{phase}.json incrementally. Idempotent \u2014 calling twice for the same questionId overwrites. Call after receiving each user reply in the conversational question loop.",
1477
+ {
1478
+ phase: z8.string().describe('Phase name e.g. "designer"'),
1479
+ questionId: z8.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
1480
+ answer: z8.string().describe("The user's free-text answer")
1481
+ },
1482
+ async ({ phase, questionId, answer }) => {
1483
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1484
+ if (!SAFE_PHASE.test(phase)) {
1485
+ return {
1486
+ content: [
1487
+ { type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
1488
+ ],
1489
+ isError: true
1490
+ };
1491
+ }
1492
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
1493
+ return {
1494
+ content: [
1495
+ { type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
1496
+ ],
1497
+ isError: true
1498
+ };
1499
+ }
1500
+ const safeAnswer = answer.slice(0, 2e3);
1501
+ const cwd = process.cwd();
1502
+ const answersDir = join(cwd, ".stackwright", "answers");
1503
+ const answersPath = join(answersDir, `${phase}.json`);
1504
+ if (existsSync2(answersDir) && lstatSync2(answersDir).isSymbolicLink()) {
1505
+ return {
1506
+ content: [
1507
+ {
1508
+ type: "text",
1509
+ text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
1510
+ }
1511
+ ],
1512
+ isError: true
1513
+ };
1514
+ }
1515
+ mkdirSync(answersDir, { recursive: true });
1516
+ let existing = {
1517
+ version: "1.0",
1518
+ phase,
1519
+ answers: {}
1520
+ };
1521
+ try {
1522
+ if (existsSync2(answersPath)) {
1523
+ if (lstatSync2(answersPath).isSymbolicLink()) {
1524
+ return {
1525
+ content: [
1526
+ {
1527
+ type: "text",
1528
+ text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
1529
+ }
1530
+ ],
1531
+ isError: true
1532
+ };
1533
+ }
1534
+ const raw = await readFile(answersPath, "utf-8");
1535
+ existing = JSON.parse(raw);
1536
+ }
1537
+ } catch {
1538
+ }
1539
+ existing.answers[questionId] = safeAnswer;
1540
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1541
+ const tmp = `${answersPath}.tmp`;
1542
+ await writeFile(tmp, JSON.stringify(existing, null, 2), "utf-8");
1543
+ renameSync(tmp, answersPath);
1544
+ return {
1545
+ content: [
1546
+ { type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
1547
+ ]
1548
+ };
1549
+ }
1550
+ );
1412
1551
  }
1413
1552
 
1414
1553
  // src/tools/orchestration.ts
1415
1554
  import { z as z9 } from "zod";
1416
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, lstatSync as lstatSync2 } from "fs";
1555
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, lstatSync as lstatSync3 } from "fs";
1417
1556
  import { join as join2 } from "path";
1418
1557
  var OTTER_NAME_TO_PHASE = [
1419
1558
  ["designer", "designer"],
@@ -1469,9 +1608,9 @@ function handleSaveManifest(input) {
1469
1608
  const dir = join2(cwd, ".stackwright");
1470
1609
  const filePath = join2(dir, "question-manifest.json");
1471
1610
  try {
1472
- mkdirSync(dir, { recursive: true });
1473
- if (existsSync2(filePath)) {
1474
- const stat = lstatSync2(filePath);
1611
+ mkdirSync2(dir, { recursive: true });
1612
+ if (existsSync3(filePath)) {
1613
+ const stat = lstatSync3(filePath);
1475
1614
  if (stat.isSymbolicLink()) {
1476
1615
  const message = `Refusing to write to symlink: ${filePath}`;
1477
1616
  return {
@@ -1503,7 +1642,7 @@ function handleSavePhaseAnswers(input) {
1503
1642
  const dir = join2(cwd, ".stackwright", "answers");
1504
1643
  const filePath = join2(dir, `${input.phase}.json`);
1505
1644
  try {
1506
- mkdirSync(dir, { recursive: true });
1645
+ mkdirSync2(dir, { recursive: true });
1507
1646
  let answers;
1508
1647
  if (input.questions && input.questions.length > 0) {
1509
1648
  answers = answersToManifestFormat(input.rawAnswers, input.questions);
@@ -1518,8 +1657,8 @@ function handleSavePhaseAnswers(input) {
1518
1657
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1519
1658
  answers
1520
1659
  };
1521
- if (existsSync2(filePath)) {
1522
- const stat = lstatSync2(filePath);
1660
+ if (existsSync3(filePath)) {
1661
+ const stat = lstatSync3(filePath);
1523
1662
  if (stat.isSymbolicLink()) {
1524
1663
  const message = `Refusing to write to symlink: ${filePath}`;
1525
1664
  return {
@@ -1548,7 +1687,7 @@ function handleSavePhaseAnswers(input) {
1548
1687
  function handleReadPhaseAnswers(input) {
1549
1688
  const cwd = input._cwd ?? process.cwd();
1550
1689
  const filePath = join2(cwd, ".stackwright", "answers", `${input.phase}.json`);
1551
- if (!existsSync2(filePath)) {
1690
+ if (!existsSync3(filePath)) {
1552
1691
  return {
1553
1692
  text: JSON.stringify({ missing: true, phase: input.phase }),
1554
1693
  isError: false
@@ -1580,7 +1719,57 @@ function handleGetOtterName(input) {
1580
1719
  isError: false
1581
1720
  };
1582
1721
  }
1722
+ function handleSaveBuildContext(input) {
1723
+ const cwd = input._cwd ?? process.cwd();
1724
+ const dir = join2(cwd, ".stackwright");
1725
+ const filePath = join2(dir, "build-context.json");
1726
+ try {
1727
+ mkdirSync2(dir, { recursive: true });
1728
+ if (existsSync3(filePath)) {
1729
+ const stat = lstatSync3(filePath);
1730
+ if (stat.isSymbolicLink()) {
1731
+ return {
1732
+ text: JSON.stringify({
1733
+ success: false,
1734
+ error: `Refusing to write to symlink: ${filePath}`
1735
+ }),
1736
+ isError: true
1737
+ };
1738
+ }
1739
+ }
1740
+ const payload = {
1741
+ version: "1.0",
1742
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1743
+ buildContext: input.buildContext
1744
+ };
1745
+ writeFileSync2(filePath, JSON.stringify(payload, null, 2) + "\n");
1746
+ return {
1747
+ text: JSON.stringify({ success: true, path: filePath }),
1748
+ isError: false
1749
+ };
1750
+ } catch (err) {
1751
+ const message = err instanceof Error ? err.message : String(err);
1752
+ return {
1753
+ text: JSON.stringify({ success: false, error: message }),
1754
+ isError: true
1755
+ };
1756
+ }
1757
+ }
1583
1758
  function registerOrchestrationTools(server2) {
1759
+ server2.tool(
1760
+ "stackwright_pro_save_build_context",
1761
+ `Save the user's initial build description to .stackwright/build-context.json. Call this once at startup after the user answers the opening "what are you building" question. The saved context is automatically prepended to specialist prompts by stackwright_pro_build_specialist_prompt.`,
1762
+ {
1763
+ buildContext: z9.string().describe("Free-text description of what the user wants to build")
1764
+ },
1765
+ async ({ buildContext }) => {
1766
+ const { text, isError } = handleSaveBuildContext({ buildContext });
1767
+ return {
1768
+ content: [{ type: "text", text }],
1769
+ isError
1770
+ };
1771
+ }
1772
+ );
1584
1773
  server2.tool(
1585
1774
  "stackwright_pro_parse_otter_response",
1586
1775
  "Parse and validate a specialist otter's QUESTION_COLLECTION_MODE JSON response. Handles JSON extraction from LLM responses (strips markdown, fixes single quotes, trailing commas). Detects the phase from the otter name. Use this immediately after invoke_agent() to get a validated manifest phase object.",
@@ -1686,7 +1875,7 @@ function registerOrchestrationTools(server2) {
1686
1875
 
1687
1876
  // src/tools/pipeline.ts
1688
1877
  import { z as z10 } from "zod";
1689
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, lstatSync as lstatSync3 } from "fs";
1878
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, lstatSync as lstatSync4 } from "fs";
1690
1879
  import { join as join3 } from "path";
1691
1880
  var PHASE_ORDER = [
1692
1881
  "designer",
@@ -1760,7 +1949,7 @@ function statePath(cwd) {
1760
1949
  }
1761
1950
  function readState(cwd) {
1762
1951
  const p = statePath(cwd);
1763
- if (!existsSync3(p)) return createDefaultState();
1952
+ if (!existsSync4(p)) return createDefaultState();
1764
1953
  const raw = JSON.parse(safeReadSync(p));
1765
1954
  if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
1766
1955
  return createDefaultState();
@@ -1768,8 +1957,8 @@ function readState(cwd) {
1768
1957
  return raw;
1769
1958
  }
1770
1959
  function safeWriteSync(filePath, content) {
1771
- if (existsSync3(filePath)) {
1772
- const stat = lstatSync3(filePath);
1960
+ if (existsSync4(filePath)) {
1961
+ const stat = lstatSync4(filePath);
1773
1962
  if (stat.isSymbolicLink()) {
1774
1963
  throw new Error(`Refusing to write to symlink: ${filePath}`);
1775
1964
  }
@@ -1777,8 +1966,8 @@ function safeWriteSync(filePath, content) {
1777
1966
  writeFileSync3(filePath, content);
1778
1967
  }
1779
1968
  function safeReadSync(filePath) {
1780
- if (existsSync3(filePath)) {
1781
- const stat = lstatSync3(filePath);
1969
+ if (existsSync4(filePath)) {
1970
+ const stat = lstatSync4(filePath);
1782
1971
  if (stat.isSymbolicLink()) {
1783
1972
  throw new Error(`Refusing to read symlink: ${filePath}`);
1784
1973
  }
@@ -1787,7 +1976,7 @@ function safeReadSync(filePath) {
1787
1976
  }
1788
1977
  function writeState(cwd, state) {
1789
1978
  const dir = join3(cwd, ".stackwright");
1790
- mkdirSync2(dir, { recursive: true });
1979
+ mkdirSync3(dir, { recursive: true });
1791
1980
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1792
1981
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
1793
1982
  }
@@ -1859,28 +2048,62 @@ function handleSetPipelineState(input) {
1859
2048
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1860
2049
  }
1861
2050
  }
1862
- function handleCheckExecutionReady(_cwd) {
2051
+ function handleCheckExecutionReady(_cwd, phase) {
1863
2052
  const cwd = _cwd ?? process.cwd();
2053
+ if (phase) {
2054
+ if (!isValidPhase(phase)) {
2055
+ return {
2056
+ text: JSON.stringify({
2057
+ error: true,
2058
+ message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
2059
+ }),
2060
+ isError: true
2061
+ };
2062
+ }
2063
+ const answerFile = join3(cwd, ".stackwright", "answers", `${phase}.json`);
2064
+ if (!existsSync4(answerFile)) {
2065
+ return {
2066
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
2067
+ isError: false
2068
+ };
2069
+ }
2070
+ try {
2071
+ const raw = safeReadSync(answerFile);
2072
+ const parsed = JSON.parse(raw);
2073
+ if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
2074
+ return {
2075
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
2076
+ isError: false
2077
+ };
2078
+ }
2079
+ return { text: JSON.stringify({ ready: true, phase }), isError: false };
2080
+ } catch {
2081
+ return {
2082
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
2083
+ isError: false
2084
+ };
2085
+ }
2086
+ }
1864
2087
  try {
1865
2088
  const answersDir = join3(cwd, ".stackwright", "answers");
1866
2089
  const answeredPhases = [];
1867
2090
  const missingPhases = [];
1868
- for (const phase of PHASE_ORDER) {
1869
- const answerFile = join3(answersDir, `${phase}.json`);
1870
- if (existsSync3(answerFile)) {
2091
+ for (const phase2 of PHASE_ORDER) {
2092
+ const answerFile = join3(answersDir, `${phase2}.json`);
2093
+ if (existsSync4(answerFile)) {
1871
2094
  try {
1872
2095
  const raw = safeReadSync(answerFile);
1873
2096
  const parsed = JSON.parse(raw);
1874
2097
  if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
1875
- missingPhases.push(phase);
2098
+ missingPhases.push(phase2);
1876
2099
  continue;
1877
2100
  }
1878
- answeredPhases.push(phase);
2101
+ answeredPhases.push(phase2);
1879
2102
  } catch {
1880
- missingPhases.push(phase);
2103
+ missingPhases.push(phase2);
1881
2104
  }
1882
2105
  } else {
1883
- missingPhases.push(phase);
2106
+ missingPhases.push(phase2);
1884
2107
  }
1885
2108
  }
1886
2109
  return {
@@ -1905,7 +2128,7 @@ function handleListArtifacts(_cwd) {
1905
2128
  for (const phase of PHASE_ORDER) {
1906
2129
  const expectedFile = PHASE_ARTIFACT[phase];
1907
2130
  const fullPath = join3(artifactsDir, expectedFile);
1908
- const exists = existsSync3(fullPath);
2131
+ const exists = existsSync4(fullPath);
1909
2132
  if (exists) completedCount++;
1910
2133
  artifacts.push({ phase, expectedFile, exists, path: fullPath });
1911
2134
  }
@@ -1944,7 +2167,7 @@ function handleWritePhaseQuestions(input) {
1944
2167
  } catch {
1945
2168
  }
1946
2169
  const questionsDir = join3(cwd, ".stackwright", "questions");
1947
- mkdirSync2(questionsDir, { recursive: true });
2170
+ mkdirSync3(questionsDir, { recursive: true });
1948
2171
  const filePath = join3(questionsDir, `${phase}.json`);
1949
2172
  const payload = {
1950
2173
  version: "1.0",
@@ -1987,16 +2210,27 @@ function handleBuildSpecialistPrompt(input) {
1987
2210
  try {
1988
2211
  const answersPath = join3(cwd, ".stackwright", "answers", `${phase}.json`);
1989
2212
  let answers = {};
1990
- if (existsSync3(answersPath)) {
2213
+ if (existsSync4(answersPath)) {
1991
2214
  answers = JSON.parse(safeReadSync(answersPath));
1992
2215
  }
2216
+ let buildContextText = "";
2217
+ const buildContextPath = join3(cwd, ".stackwright", "build-context.json");
2218
+ if (existsSync4(buildContextPath)) {
2219
+ try {
2220
+ const bcRaw = JSON.parse(safeReadSync(buildContextPath));
2221
+ if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
2222
+ buildContextText = bcRaw.buildContext.trim();
2223
+ }
2224
+ } catch {
2225
+ }
2226
+ }
1993
2227
  const deps = PHASE_DEPENDENCIES[phase];
1994
2228
  const artifactSections = [];
1995
2229
  const missingDependencies = [];
1996
2230
  for (const dep of deps) {
1997
2231
  const artifactFile = PHASE_ARTIFACT[dep];
1998
2232
  const artifactPath = join3(cwd, ".stackwright", "artifacts", artifactFile);
1999
- if (existsSync3(artifactPath)) {
2233
+ if (existsSync4(artifactPath)) {
2000
2234
  const content = JSON.parse(safeReadSync(artifactPath));
2001
2235
  const expectedOtter = PHASE_TO_OTTER2[dep];
2002
2236
  const artifactOtter = content["generatedBy"];
@@ -2022,7 +2256,11 @@ ${JSON.stringify(content, null, 2)}`);
2022
2256
  (not yet available)`);
2023
2257
  }
2024
2258
  }
2025
- const parts = ["ANSWERS:", JSON.stringify(answers, null, 2)];
2259
+ const parts = [];
2260
+ if (buildContextText) {
2261
+ parts.push("BUILD_CONTEXT:", buildContextText, "");
2262
+ }
2263
+ parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
2026
2264
  if (artifactSections.length > 0) {
2027
2265
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
2028
2266
  }
@@ -2121,7 +2359,7 @@ function handleValidateArtifact(input) {
2121
2359
  }
2122
2360
  try {
2123
2361
  const artifactsDir = join3(cwd, ".stackwright", "artifacts");
2124
- mkdirSync2(artifactsDir, { recursive: true });
2362
+ mkdirSync3(artifactsDir, { recursive: true });
2125
2363
  const artifactFile = PHASE_ARTIFACT[phase];
2126
2364
  const artifactPath = join3(artifactsDir, artifactFile);
2127
2365
  safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
@@ -2181,9 +2419,11 @@ function registerPipelineTools(server2) {
2181
2419
  );
2182
2420
  server2.tool(
2183
2421
  "stackwright_pro_check_execution_ready",
2184
- `Check all phases have answer files in .stackwright/answers/. ${DESC}`,
2185
- {},
2186
- async () => res(handleCheckExecutionReady())
2422
+ `Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
2423
+ {
2424
+ phase: z10.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
2425
+ },
2426
+ async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
2187
2427
  );
2188
2428
  server2.tool(
2189
2429
  "stackwright_pro_list_artifacts",
@@ -2193,12 +2433,49 @@ function registerPipelineTools(server2) {
2193
2433
  );
2194
2434
  server2.tool(
2195
2435
  "stackwright_pro_write_phase_questions",
2196
- `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
2436
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2197
2437
  {
2198
- phase: z10.string().describe('Phase name, e.g. "designer"'),
2199
- responseText: z10.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
2438
+ phase: z10.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
2439
+ responseText: z10.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
2440
+ questions: jsonCoerce(z10.array(z10.any()).optional()).describe(
2441
+ "Questions array for direct specialist write"
2442
+ )
2200
2443
  },
2201
- async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
2444
+ async ({ phase, responseText, questions }) => {
2445
+ if (phase && questions && Array.isArray(questions)) {
2446
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
2447
+ if (!SAFE_PHASE.test(phase)) {
2448
+ return {
2449
+ content: [
2450
+ { type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
2451
+ ],
2452
+ isError: true
2453
+ };
2454
+ }
2455
+ const questionsDir = join3(process.cwd(), ".stackwright", "questions");
2456
+ mkdirSync3(questionsDir, { recursive: true });
2457
+ const outPath = join3(questionsDir, `${phase}.json`);
2458
+ writeFileSync3(
2459
+ outPath,
2460
+ JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
2461
+ );
2462
+ return {
2463
+ content: [
2464
+ {
2465
+ type: "text",
2466
+ text: JSON.stringify({
2467
+ written: true,
2468
+ phase,
2469
+ count: questions.length
2470
+ })
2471
+ }
2472
+ ]
2473
+ };
2474
+ }
2475
+ return res(
2476
+ handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
2477
+ );
2478
+ }
2202
2479
  );
2203
2480
  server2.tool(
2204
2481
  "stackwright_pro_build_specialist_prompt",
@@ -2219,7 +2496,7 @@ function registerPipelineTools(server2) {
2219
2496
 
2220
2497
  // src/tools/safe-write.ts
2221
2498
  import { z as z11 } from "zod";
2222
- import { writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, lstatSync as lstatSync4 } from "fs";
2499
+ import { writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
2223
2500
  import { normalize, isAbsolute, dirname, join as join4 } from "path";
2224
2501
  var OTTER_WRITE_ALLOWLISTS = {
2225
2502
  "stackwright-pro-designer-otter": [
@@ -2393,9 +2670,9 @@ function handleSafeWrite(input) {
2393
2670
  }
2394
2671
  const normalized = normalize(filePath);
2395
2672
  const fullPath = join4(cwd, normalized);
2396
- if (existsSync4(fullPath)) {
2673
+ if (existsSync5(fullPath)) {
2397
2674
  try {
2398
- const stat = lstatSync4(fullPath);
2675
+ const stat = lstatSync5(fullPath);
2399
2676
  if (stat.isSymbolicLink()) {
2400
2677
  const result = {
2401
2678
  success: false,
@@ -2422,7 +2699,7 @@ function handleSafeWrite(input) {
2422
2699
  }
2423
2700
  try {
2424
2701
  if (createDirectories) {
2425
- mkdirSync3(dirname(fullPath), { recursive: true });
2702
+ mkdirSync4(dirname(fullPath), { recursive: true });
2426
2703
  }
2427
2704
  writeFileSync4(fullPath, content, { encoding: "utf-8" });
2428
2705
  const result = {
@@ -2469,7 +2746,7 @@ function registerSafeWriteTools(server2) {
2469
2746
 
2470
2747
  // src/tools/auth.ts
2471
2748
  import { z as z12 } from "zod";
2472
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
2749
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2473
2750
  import { join as join5 } from "path";
2474
2751
  function buildHierarchy(roles) {
2475
2752
  const h = {};
@@ -2749,7 +3026,7 @@ async function configureAuthHandler(params, cwd) {
2749
3026
  try {
2750
3027
  const envBlock = generateEnvBlock(method, params);
2751
3028
  const envPath = join5(cwd, ".env.example");
2752
- if (existsSync5(envPath)) {
3029
+ if (existsSync6(envPath)) {
2753
3030
  const existing = readFileSync4(envPath, "utf8");
2754
3031
  writeFileSync5(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
2755
3032
  } else {
@@ -2780,7 +3057,7 @@ async function configureAuthHandler(params, cwd) {
2780
3057
  protectedRoutes
2781
3058
  );
2782
3059
  const ymlPath = join5(cwd, "stackwright.yml");
2783
- if (!existsSync5(ymlPath)) {
3060
+ if (!existsSync6(ymlPath)) {
2784
3061
  writeFileSync5(ymlPath, authYaml, "utf8");
2785
3062
  } else {
2786
3063
  const existing = readFileSync4(ymlPath, "utf8");
@@ -2862,44 +3139,44 @@ function registerAuthTools(server2) {
2862
3139
 
2863
3140
  // src/integrity.ts
2864
3141
  import { createHash as createHash2, timingSafeEqual } from "crypto";
2865
- import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync5 } from "fs";
3142
+ import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync6 } from "fs";
2866
3143
  import { join as join6, basename } from "path";
2867
3144
  var _checksums = /* @__PURE__ */ new Map([
2868
3145
  [
2869
3146
  "stackwright-pro-api-otter.json",
2870
- "f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d"
3147
+ "0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
2871
3148
  ],
2872
3149
  [
2873
3150
  "stackwright-pro-auth-otter.json",
2874
- "a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725"
3151
+ "d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
2875
3152
  ],
2876
3153
  [
2877
3154
  "stackwright-pro-dashboard-otter.json",
2878
- "b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001"
3155
+ "600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
2879
3156
  ],
2880
3157
  [
2881
3158
  "stackwright-pro-data-otter.json",
2882
- "bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac"
3159
+ "b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
2883
3160
  ],
2884
3161
  [
2885
3162
  "stackwright-pro-designer-otter.json",
2886
- "c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88"
3163
+ "f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
2887
3164
  ],
2888
3165
  [
2889
3166
  "stackwright-pro-foreman-otter.json",
2890
- "f52264c1f6297b72f3da6d92d41b6d63356db776242caeab25571e6a8df628e4"
3167
+ "dc86d6f234d073ead2ccb179c45f911add901ebd3ebf541824addc124df639ee"
2891
3168
  ],
2892
3169
  [
2893
3170
  "stackwright-pro-page-otter.json",
2894
- "65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67"
3171
+ "d75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60"
2895
3172
  ],
2896
3173
  [
2897
3174
  "stackwright-pro-theme-otter.json",
2898
- "1f182326f1acd3d4091a38c7012085cbb4945893e95be4ca3de72318ad092767"
3175
+ "a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc"
2899
3176
  ],
2900
3177
  [
2901
3178
  "stackwright-pro-workflow-otter.json",
2902
- "0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510"
3179
+ "16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
2903
3180
  ]
2904
3181
  ]);
2905
3182
  Object.freeze(_checksums);
@@ -2928,7 +3205,7 @@ function verifyOtterFile(filePath) {
2928
3205
  }
2929
3206
  let stat;
2930
3207
  try {
2931
- stat = lstatSync5(filePath);
3208
+ stat = lstatSync6(filePath);
2932
3209
  } catch (err) {
2933
3210
  const msg = err instanceof Error ? err.message : String(err);
2934
3211
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -2997,7 +3274,7 @@ function verifyAllOtters(otterDir) {
2997
3274
  for (const filename of otterFiles) {
2998
3275
  const filePath = join6(otterDir, filename);
2999
3276
  try {
3000
- if (lstatSync5(filePath).isSymbolicLink()) {
3277
+ if (lstatSync6(filePath).isSymbolicLink()) {
3001
3278
  failed.push({ filename, error: "Skipped: symlink" });
3002
3279
  continue;
3003
3280
  }
@@ -3025,7 +3302,7 @@ function resolveOtterDir() {
3025
3302
  for (const relative of DEFAULT_SEARCH_PATHS) {
3026
3303
  const candidate = join6(cwd, relative);
3027
3304
  try {
3028
- lstatSync5(candidate);
3305
+ lstatSync6(candidate);
3029
3306
  return candidate;
3030
3307
  } catch {
3031
3308
  }
@@ -3080,7 +3357,7 @@ function registerIntegrityTools(server2) {
3080
3357
 
3081
3358
  // src/tools/domain.ts
3082
3359
  import { z as z13 } from "zod";
3083
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
3360
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
3084
3361
  import { join as join7 } from "path";
3085
3362
  function handleListCollections(input) {
3086
3363
  const cwd = input._cwd ?? process.cwd();
@@ -3103,7 +3380,7 @@ function handleListCollections(input) {
3103
3380
  }
3104
3381
  ];
3105
3382
  for (const { path: path3, source, parse } of sources) {
3106
- if (!existsSync6(path3)) continue;
3383
+ if (!existsSync7(path3)) continue;
3107
3384
  try {
3108
3385
  const collections = parse(readFileSync6(path3, "utf8"));
3109
3386
  return {
@@ -3257,7 +3534,7 @@ function handleValidateWorkflow(input) {
3257
3534
  raw = input.workflow;
3258
3535
  } else {
3259
3536
  const artifactPath = join7(cwd, ".stackwright", "artifacts", "workflow-config.json");
3260
- if (!existsSync6(artifactPath)) {
3537
+ if (!existsSync7(artifactPath)) {
3261
3538
  return fail([
3262
3539
  {
3263
3540
  code: "NO_WORKFLOW",
@@ -3505,7 +3782,7 @@ var package_default = {
3505
3782
  "test:coverage": "vitest run --coverage"
3506
3783
  },
3507
3784
  name: "@stackwright-pro/mcp",
3508
- version: "0.2.0-alpha.16",
3785
+ version: "0.2.0-alpha.18",
3509
3786
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3510
3787
  license: "PROPRIETARY",
3511
3788
  main: "./dist/server.js",