@stackwright-pro/mcp 0.2.0-alpha.21 → 0.2.0-alpha.22

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,26 +2210,38 @@ 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"];
2237
+ const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
2003
2238
  if (!artifactOtter) {
2004
2239
  missingDependencies.push(dep);
2005
2240
  artifactSections.push(
2006
2241
  `[${artifactFile}]:
2007
2242
  (integrity check failed: missing generatedBy field)`
2008
2243
  );
2009
- } else if (artifactOtter !== expectedOtter) {
2244
+ } else if (normalizedOtter !== expectedOtter) {
2010
2245
  missingDependencies.push(dep);
2011
2246
  artifactSections.push(
2012
2247
  `[${artifactFile}]:
@@ -2022,7 +2257,11 @@ ${JSON.stringify(content, null, 2)}`);
2022
2257
  (not yet available)`);
2023
2258
  }
2024
2259
  }
2025
- const parts = ["ANSWERS:", JSON.stringify(answers, null, 2)];
2260
+ const parts = [];
2261
+ if (buildContextText) {
2262
+ parts.push("BUILD_CONTEXT:", buildContextText, "");
2263
+ }
2264
+ parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
2026
2265
  if (artifactSections.length > 0) {
2027
2266
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
2028
2267
  }
@@ -2121,7 +2360,7 @@ function handleValidateArtifact(input) {
2121
2360
  }
2122
2361
  try {
2123
2362
  const artifactsDir = join3(cwd, ".stackwright", "artifacts");
2124
- mkdirSync2(artifactsDir, { recursive: true });
2363
+ mkdirSync3(artifactsDir, { recursive: true });
2125
2364
  const artifactFile = PHASE_ARTIFACT[phase];
2126
2365
  const artifactPath = join3(artifactsDir, artifactFile);
2127
2366
  safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
@@ -2181,9 +2420,11 @@ function registerPipelineTools(server2) {
2181
2420
  );
2182
2421
  server2.tool(
2183
2422
  "stackwright_pro_check_execution_ready",
2184
- `Check all phases have answer files in .stackwright/answers/. ${DESC}`,
2185
- {},
2186
- async () => res(handleCheckExecutionReady())
2423
+ `Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
2424
+ {
2425
+ phase: z10.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
2426
+ },
2427
+ async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
2187
2428
  );
2188
2429
  server2.tool(
2189
2430
  "stackwright_pro_list_artifacts",
@@ -2193,12 +2434,49 @@ function registerPipelineTools(server2) {
2193
2434
  );
2194
2435
  server2.tool(
2195
2436
  "stackwright_pro_write_phase_questions",
2196
- `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
2437
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2197
2438
  {
2198
- phase: z10.string().describe('Phase name, e.g. "designer"'),
2199
- responseText: z10.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
2439
+ phase: z10.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
2440
+ responseText: z10.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
2441
+ questions: jsonCoerce(z10.array(z10.any()).optional()).describe(
2442
+ "Questions array for direct specialist write"
2443
+ )
2200
2444
  },
2201
- async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
2445
+ async ({ phase, responseText, questions }) => {
2446
+ if (phase && questions && Array.isArray(questions)) {
2447
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
2448
+ if (!SAFE_PHASE.test(phase)) {
2449
+ return {
2450
+ content: [
2451
+ { type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
2452
+ ],
2453
+ isError: true
2454
+ };
2455
+ }
2456
+ const questionsDir = join3(process.cwd(), ".stackwright", "questions");
2457
+ mkdirSync3(questionsDir, { recursive: true });
2458
+ const outPath = join3(questionsDir, `${phase}.json`);
2459
+ writeFileSync3(
2460
+ outPath,
2461
+ JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
2462
+ );
2463
+ return {
2464
+ content: [
2465
+ {
2466
+ type: "text",
2467
+ text: JSON.stringify({
2468
+ written: true,
2469
+ phase,
2470
+ count: questions.length
2471
+ })
2472
+ }
2473
+ ]
2474
+ };
2475
+ }
2476
+ return res(
2477
+ handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
2478
+ );
2479
+ }
2202
2480
  );
2203
2481
  server2.tool(
2204
2482
  "stackwright_pro_build_specialist_prompt",
@@ -2219,7 +2497,7 @@ function registerPipelineTools(server2) {
2219
2497
 
2220
2498
  // src/tools/safe-write.ts
2221
2499
  import { z as z11 } from "zod";
2222
- import { writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, lstatSync as lstatSync4 } from "fs";
2500
+ import { writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
2223
2501
  import { normalize, isAbsolute, dirname, join as join4 } from "path";
2224
2502
  var OTTER_WRITE_ALLOWLISTS = {
2225
2503
  "stackwright-pro-designer-otter": [
@@ -2393,9 +2671,9 @@ function handleSafeWrite(input) {
2393
2671
  }
2394
2672
  const normalized = normalize(filePath);
2395
2673
  const fullPath = join4(cwd, normalized);
2396
- if (existsSync4(fullPath)) {
2674
+ if (existsSync5(fullPath)) {
2397
2675
  try {
2398
- const stat = lstatSync4(fullPath);
2676
+ const stat = lstatSync5(fullPath);
2399
2677
  if (stat.isSymbolicLink()) {
2400
2678
  const result = {
2401
2679
  success: false,
@@ -2422,7 +2700,7 @@ function handleSafeWrite(input) {
2422
2700
  }
2423
2701
  try {
2424
2702
  if (createDirectories) {
2425
- mkdirSync3(dirname(fullPath), { recursive: true });
2703
+ mkdirSync4(dirname(fullPath), { recursive: true });
2426
2704
  }
2427
2705
  writeFileSync4(fullPath, content, { encoding: "utf-8" });
2428
2706
  const result = {
@@ -2453,7 +2731,9 @@ function registerSafeWriteTools(server2) {
2453
2731
  callerOtter: z11.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
2454
2732
  filePath: z11.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
2455
2733
  content: z11.string().describe("File content to write"),
2456
- createDirectories: z11.boolean().optional().describe("Create parent directories if they don't exist. Default: true")
2734
+ createDirectories: boolCoerce(z11.boolean().optional().default(true)).describe(
2735
+ "Create parent directories if they don't exist. Default: true"
2736
+ )
2457
2737
  },
2458
2738
  async ({ callerOtter, filePath, content, createDirectories }) => {
2459
2739
  const result = handleSafeWrite({
@@ -2469,7 +2749,7 @@ function registerSafeWriteTools(server2) {
2469
2749
 
2470
2750
  // src/tools/auth.ts
2471
2751
  import { z as z12 } from "zod";
2472
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
2752
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2473
2753
  import { join as join5 } from "path";
2474
2754
  function buildHierarchy(roles) {
2475
2755
  const h = {};
@@ -2749,7 +3029,7 @@ async function configureAuthHandler(params, cwd) {
2749
3029
  try {
2750
3030
  const envBlock = generateEnvBlock(method, params);
2751
3031
  const envPath = join5(cwd, ".env.example");
2752
- if (existsSync5(envPath)) {
3032
+ if (existsSync6(envPath)) {
2753
3033
  const existing = readFileSync4(envPath, "utf8");
2754
3034
  writeFileSync5(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
2755
3035
  } else {
@@ -2780,7 +3060,7 @@ async function configureAuthHandler(params, cwd) {
2780
3060
  protectedRoutes
2781
3061
  );
2782
3062
  const ymlPath = join5(cwd, "stackwright.yml");
2783
- if (!existsSync5(ymlPath)) {
3063
+ if (!existsSync6(ymlPath)) {
2784
3064
  writeFileSync5(ymlPath, authYaml, "utf8");
2785
3065
  } else {
2786
3066
  const existing = readFileSync4(ymlPath, "utf8");
@@ -2862,44 +3142,44 @@ function registerAuthTools(server2) {
2862
3142
 
2863
3143
  // src/integrity.ts
2864
3144
  import { createHash as createHash2, timingSafeEqual } from "crypto";
2865
- import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync5 } from "fs";
3145
+ import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync6 } from "fs";
2866
3146
  import { join as join6, basename } from "path";
2867
3147
  var _checksums = /* @__PURE__ */ new Map([
2868
3148
  [
2869
3149
  "stackwright-pro-api-otter.json",
2870
- "f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d"
3150
+ "0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
2871
3151
  ],
2872
3152
  [
2873
3153
  "stackwright-pro-auth-otter.json",
2874
- "a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725"
3154
+ "d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
2875
3155
  ],
2876
3156
  [
2877
3157
  "stackwright-pro-dashboard-otter.json",
2878
- "b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001"
3158
+ "600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
2879
3159
  ],
2880
3160
  [
2881
3161
  "stackwright-pro-data-otter.json",
2882
- "bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac"
3162
+ "b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
2883
3163
  ],
2884
3164
  [
2885
3165
  "stackwright-pro-designer-otter.json",
2886
- "c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88"
3166
+ "f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
2887
3167
  ],
2888
3168
  [
2889
3169
  "stackwright-pro-foreman-otter.json",
2890
- "f52264c1f6297b72f3da6d92d41b6d63356db776242caeab25571e6a8df628e4"
3170
+ "7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5"
2891
3171
  ],
2892
3172
  [
2893
3173
  "stackwright-pro-page-otter.json",
2894
- "65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67"
3174
+ "12aca7b666b3c85c1d96c700a2da7f209604cb75d0f064995e052711ddafd657"
2895
3175
  ],
2896
3176
  [
2897
3177
  "stackwright-pro-theme-otter.json",
2898
- "1f182326f1acd3d4091a38c7012085cbb4945893e95be4ca3de72318ad092767"
3178
+ "a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc"
2899
3179
  ],
2900
3180
  [
2901
3181
  "stackwright-pro-workflow-otter.json",
2902
- "0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510"
3182
+ "16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
2903
3183
  ]
2904
3184
  ]);
2905
3185
  Object.freeze(_checksums);
@@ -2928,7 +3208,7 @@ function verifyOtterFile(filePath) {
2928
3208
  }
2929
3209
  let stat;
2930
3210
  try {
2931
- stat = lstatSync5(filePath);
3211
+ stat = lstatSync6(filePath);
2932
3212
  } catch (err) {
2933
3213
  const msg = err instanceof Error ? err.message : String(err);
2934
3214
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -2997,7 +3277,7 @@ function verifyAllOtters(otterDir) {
2997
3277
  for (const filename of otterFiles) {
2998
3278
  const filePath = join6(otterDir, filename);
2999
3279
  try {
3000
- if (lstatSync5(filePath).isSymbolicLink()) {
3280
+ if (lstatSync6(filePath).isSymbolicLink()) {
3001
3281
  failed.push({ filename, error: "Skipped: symlink" });
3002
3282
  continue;
3003
3283
  }
@@ -3025,7 +3305,7 @@ function resolveOtterDir() {
3025
3305
  for (const relative of DEFAULT_SEARCH_PATHS) {
3026
3306
  const candidate = join6(cwd, relative);
3027
3307
  try {
3028
- lstatSync5(candidate);
3308
+ lstatSync6(candidate);
3029
3309
  return candidate;
3030
3310
  } catch {
3031
3311
  }
@@ -3080,7 +3360,7 @@ function registerIntegrityTools(server2) {
3080
3360
 
3081
3361
  // src/tools/domain.ts
3082
3362
  import { z as z13 } from "zod";
3083
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
3363
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
3084
3364
  import { join as join7 } from "path";
3085
3365
  function handleListCollections(input) {
3086
3366
  const cwd = input._cwd ?? process.cwd();
@@ -3103,7 +3383,7 @@ function handleListCollections(input) {
3103
3383
  }
3104
3384
  ];
3105
3385
  for (const { path: path3, source, parse } of sources) {
3106
- if (!existsSync6(path3)) continue;
3386
+ if (!existsSync7(path3)) continue;
3107
3387
  try {
3108
3388
  const collections = parse(readFileSync6(path3, "utf8"));
3109
3389
  return {
@@ -3257,7 +3537,7 @@ function handleValidateWorkflow(input) {
3257
3537
  raw = input.workflow;
3258
3538
  } else {
3259
3539
  const artifactPath = join7(cwd, ".stackwright", "artifacts", "workflow-config.json");
3260
- if (!existsSync6(artifactPath)) {
3540
+ if (!existsSync7(artifactPath)) {
3261
3541
  return fail([
3262
3542
  {
3263
3543
  code: "NO_WORKFLOW",
@@ -3505,7 +3785,7 @@ var package_default = {
3505
3785
  "test:coverage": "vitest run --coverage"
3506
3786
  },
3507
3787
  name: "@stackwright-pro/mcp",
3508
- version: "0.2.0-alpha.13",
3788
+ version: "0.2.0-alpha.22",
3509
3789
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3510
3790
  license: "PROPRIETARY",
3511
3791
  main: "./dist/server.js",