@mir-code/specs-platform 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +460 -0
  3. package/assets/specs-usage-statusline.sh +77 -0
  4. package/assets/template/.agents/README.md +123 -0
  5. package/assets/template/.agents/RUNTIME.md +64 -0
  6. package/assets/template/.agents/agents/discovery-agent.md +15 -0
  7. package/assets/template/.agents/agents/drawing-agent.md +15 -0
  8. package/assets/template/.agents/agents/feature-runner.md +15 -0
  9. package/assets/template/.agents/agents/refinement-runner.md +15 -0
  10. package/assets/template/.agents/agents/refinement.md +15 -0
  11. package/assets/template/.agents/project.md +158 -0
  12. package/assets/template/.agents/scripts/check-commit.mjs +70 -0
  13. package/assets/template/.agents/scripts/validate-spec.mjs +345 -0
  14. package/assets/template/.agents/skills/discovery-agent/SKILL.md +80 -0
  15. package/assets/template/.agents/skills/discovery-writing/SKILL.md +76 -0
  16. package/assets/template/.agents/skills/drawing-agent/SKILL.md +85 -0
  17. package/assets/template/.agents/skills/drawing-writing/SKILL.md +100 -0
  18. package/assets/template/.agents/skills/execution-protocol/SKILL.md +110 -0
  19. package/assets/template/.agents/skills/execution-protocol/references/git.md +104 -0
  20. package/assets/template/.agents/skills/execution-protocol/references/relatorio-halt.md +103 -0
  21. package/assets/template/.agents/skills/feature-runner/SKILL.md +81 -0
  22. package/assets/template/.agents/skills/feature-runner/references/divergencias.md +38 -0
  23. package/assets/template/.agents/skills/feature-runner/references/implementar.md +80 -0
  24. package/assets/template/.agents/skills/feature-runner/references/localizar.md +46 -0
  25. package/assets/template/.agents/skills/mermaid-diagramming/SKILL.md +198 -0
  26. package/assets/template/.agents/skills/prototype-check/SKILL.md +90 -0
  27. package/assets/template/.agents/skills/refinement/SKILL.md +136 -0
  28. package/assets/template/.agents/skills/refinement/references/estrutura.md +27 -0
  29. package/assets/template/.agents/skills/refinement/references/mapear-terreno.md +46 -0
  30. package/assets/template/.agents/skills/refinement/references/validar-renderizacao.md +29 -0
  31. package/assets/template/.agents/skills/refinement-runner/SKILL.md +145 -0
  32. package/assets/template/.agents/skills/requirements-closure/SKILL.md +236 -0
  33. package/assets/template/.agents/skills/spec-writing/SKILL.md +258 -0
  34. package/assets/template/.agents/skills/test-strategy/SKILL.md +176 -0
  35. package/assets/template/.agents/skills/verification/SKILL.md +143 -0
  36. package/assets/template/.specs/config.json +39 -0
  37. package/assets/template/.specs/discoveries/meta.json +4 -0
  38. package/assets/template/.specs/drawings/meta.json +4 -0
  39. package/assets/template/.specs/specs/_templates/task-backend.md +75 -0
  40. package/assets/template/.specs/specs/_templates/task-frontend.md +89 -0
  41. package/assets/template/.specs/specs/_templates/task-integracao.md +72 -0
  42. package/assets/template/.specs/specs/exemplo/feat-exemplo-primeira-task.md +82 -0
  43. package/assets/template/.specs/specs/exemplo/meta.json +8 -0
  44. package/assets/template/.specs/specs/exemplo/overview.md +45 -0
  45. package/assets/template/.specs/specs/meta.json +4 -0
  46. package/assets/template/SPECS.md +213 -0
  47. package/dist/chunk-SOOETS3N.js +3490 -0
  48. package/dist/chunk-SOOETS3N.js.map +1 -0
  49. package/dist/cli.js +15 -0
  50. package/dist/cli.js.map +1 -0
  51. package/dist/index.d.ts +69 -0
  52. package/dist/index.js +16 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/ui/assets/abnfDiagram-VCTEODGH-DYXtkHNq.js +2 -0
  55. package/dist/ui/assets/abnfDiagram-VCTEODGH-DYXtkHNq.js.map +1 -0
  56. package/dist/ui/assets/arc-CJs0gz4E.js +2 -0
  57. package/dist/ui/assets/arc-CJs0gz4E.js.map +1 -0
  58. package/dist/ui/assets/architectureDiagram-5GKGNRK7-Br7ZNRfe.js +37 -0
  59. package/dist/ui/assets/architectureDiagram-5GKGNRK7-Br7ZNRfe.js.map +1 -0
  60. package/dist/ui/assets/blockDiagram-I7D4REHJ-172V7R5o.js +130 -0
  61. package/dist/ui/assets/blockDiagram-I7D4REHJ-172V7R5o.js.map +1 -0
  62. package/dist/ui/assets/c4Diagram-7LVT6UL2-B4AYFhRs.js +39 -0
  63. package/dist/ui/assets/c4Diagram-7LVT6UL2-B4AYFhRs.js.map +1 -0
  64. package/dist/ui/assets/channel-BfWO3B41.js +2 -0
  65. package/dist/ui/assets/channel-BfWO3B41.js.map +1 -0
  66. package/dist/ui/assets/chunk-2Q5K7J3B-B-u7gtZN.js +2 -0
  67. package/dist/ui/assets/chunk-2Q5K7J3B-B-u7gtZN.js.map +1 -0
  68. package/dist/ui/assets/chunk-5VM5RSS4-CjFDq_cq.js +16 -0
  69. package/dist/ui/assets/chunk-5VM5RSS4-CjFDq_cq.js.map +1 -0
  70. package/dist/ui/assets/chunk-F27PBJKO-BDYeB7B8.js +2 -0
  71. package/dist/ui/assets/chunk-F27PBJKO-BDYeB7B8.js.map +1 -0
  72. package/dist/ui/assets/chunk-IMKFNOWR-y2kXPnac.js +232 -0
  73. package/dist/ui/assets/chunk-IMKFNOWR-y2kXPnac.js.map +1 -0
  74. package/dist/ui/assets/chunk-JWPE2WC7-DyCryvDN.js +2 -0
  75. package/dist/ui/assets/chunk-JWPE2WC7-DyCryvDN.js.map +1 -0
  76. package/dist/ui/assets/chunk-POPQ4Y6H-DeaxKHvR.js +2 -0
  77. package/dist/ui/assets/chunk-POPQ4Y6H-DeaxKHvR.js.map +1 -0
  78. package/dist/ui/assets/chunk-SVP7TREG-CmcZEncL.js +89 -0
  79. package/dist/ui/assets/chunk-SVP7TREG-CmcZEncL.js.map +1 -0
  80. package/dist/ui/assets/chunk-TICWLB2K-Df1RRXWR.js +207 -0
  81. package/dist/ui/assets/chunk-TICWLB2K-Df1RRXWR.js.map +1 -0
  82. package/dist/ui/assets/chunk-XXDRQBXY-ClTat8Q0.js +2 -0
  83. package/dist/ui/assets/chunk-XXDRQBXY-ClTat8Q0.js.map +1 -0
  84. package/dist/ui/assets/classDiagram-ZZMXUADV-YTUiu8rF.js +2 -0
  85. package/dist/ui/assets/classDiagram-ZZMXUADV-YTUiu8rF.js.map +1 -0
  86. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-YTUiu8rF.js +2 -0
  87. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-YTUiu8rF.js.map +1 -0
  88. package/dist/ui/assets/cose-bilkent-JH36ORCC-QvqQLoy5.js +2 -0
  89. package/dist/ui/assets/cose-bilkent-JH36ORCC-QvqQLoy5.js.map +1 -0
  90. package/dist/ui/assets/cynefin-OW5HDTMX-BY6w3pAQ.js +179 -0
  91. package/dist/ui/assets/cynefin-OW5HDTMX-BY6w3pAQ.js.map +1 -0
  92. package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-luPP4ha1.js +63 -0
  93. package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-luPP4ha1.js.map +1 -0
  94. package/dist/ui/assets/cytoscape.esm-Ix0LnXOy.js +332 -0
  95. package/dist/ui/assets/cytoscape.esm-Ix0LnXOy.js.map +1 -0
  96. package/dist/ui/assets/dagre-GXQ25YYZ-DhpPwKqq.js +5 -0
  97. package/dist/ui/assets/dagre-GXQ25YYZ-DhpPwKqq.js.map +1 -0
  98. package/dist/ui/assets/defaultLocale-DX6XiGOO.js +2 -0
  99. package/dist/ui/assets/defaultLocale-DX6XiGOO.js.map +1 -0
  100. package/dist/ui/assets/diagram-S7CK7UJ4-B9wvQM0w.js +31 -0
  101. package/dist/ui/assets/diagram-S7CK7UJ4-B9wvQM0w.js.map +1 -0
  102. package/dist/ui/assets/diagram-UQ7AKVKN-gm1M_iZS.js +42 -0
  103. package/dist/ui/assets/diagram-UQ7AKVKN-gm1M_iZS.js.map +1 -0
  104. package/dist/ui/assets/diagram-VSXAHHWV-DkEB6nEx.js +4 -0
  105. package/dist/ui/assets/diagram-VSXAHHWV-DkEB6nEx.js.map +1 -0
  106. package/dist/ui/assets/diagram-VX7I27RA-Ce2F1bHi.js +25 -0
  107. package/dist/ui/assets/diagram-VX7I27RA-Ce2F1bHi.js.map +1 -0
  108. package/dist/ui/assets/diagram-Z3DM3KII-B5ArCh-E.js +25 -0
  109. package/dist/ui/assets/diagram-Z3DM3KII-B5ArCh-E.js.map +1 -0
  110. package/dist/ui/assets/ebnfDiagram-PWID7BFC-DgBM80MD.js +2 -0
  111. package/dist/ui/assets/ebnfDiagram-PWID7BFC-DgBM80MD.js.map +1 -0
  112. package/dist/ui/assets/erDiagram-RLTQ6QDP-C161af8G.js +100 -0
  113. package/dist/ui/assets/erDiagram-RLTQ6QDP-C161af8G.js.map +1 -0
  114. package/dist/ui/assets/flowDiagram-HODETNUW-B8N_WyW5.js +169 -0
  115. package/dist/ui/assets/flowDiagram-HODETNUW-B8N_WyW5.js.map +1 -0
  116. package/dist/ui/assets/ganttDiagram-EL5Y4UJY-CD2RJpTb.js +293 -0
  117. package/dist/ui/assets/ganttDiagram-EL5Y4UJY-CD2RJpTb.js.map +1 -0
  118. package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-BrDAzeFT.js +107 -0
  119. package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-BrDAzeFT.js.map +1 -0
  120. package/dist/ui/assets/index-CVWRdirI.css +32 -0
  121. package/dist/ui/assets/index-DoR97Zqf.js +408 -0
  122. package/dist/ui/assets/index-DoR97Zqf.js.map +1 -0
  123. package/dist/ui/assets/infoDiagram-27XIBGKW-HYGaMzrA.js +3 -0
  124. package/dist/ui/assets/infoDiagram-27XIBGKW-HYGaMzrA.js.map +1 -0
  125. package/dist/ui/assets/init-Gi6I4Gst.js +2 -0
  126. package/dist/ui/assets/init-Gi6I4Gst.js.map +1 -0
  127. package/dist/ui/assets/ishikawaDiagram-5VMMS53U-DXTLlBiw.js +71 -0
  128. package/dist/ui/assets/ishikawaDiagram-5VMMS53U-DXTLlBiw.js.map +1 -0
  129. package/dist/ui/assets/journeyDiagram-3NMN7TZE-DSJ8QN1S.js +140 -0
  130. package/dist/ui/assets/journeyDiagram-3NMN7TZE-DSJ8QN1S.js.map +1 -0
  131. package/dist/ui/assets/kanban-definition-UXKFOSKX-Bm5iz0k3.js +90 -0
  132. package/dist/ui/assets/kanban-definition-UXKFOSKX-Bm5iz0k3.js.map +1 -0
  133. package/dist/ui/assets/katex-C5jXJg4s.js +258 -0
  134. package/dist/ui/assets/katex-C5jXJg4s.js.map +1 -0
  135. package/dist/ui/assets/layout-DfJgW6eG.js +2 -0
  136. package/dist/ui/assets/layout-DfJgW6eG.js.map +1 -0
  137. package/dist/ui/assets/linear-B_kyVotV.js +2 -0
  138. package/dist/ui/assets/linear-B_kyVotV.js.map +1 -0
  139. package/dist/ui/assets/mermaid-block-CUJSBQFx.js +2 -0
  140. package/dist/ui/assets/mermaid-block-CUJSBQFx.js.map +1 -0
  141. package/dist/ui/assets/mermaid.core-ExHG1WnM.js +313 -0
  142. package/dist/ui/assets/mermaid.core-ExHG1WnM.js.map +1 -0
  143. package/dist/ui/assets/mindmap-definition-YA3MSWOX-B1Hh9dQz.js +97 -0
  144. package/dist/ui/assets/mindmap-definition-YA3MSWOX-B1Hh9dQz.js.map +1 -0
  145. package/dist/ui/assets/ordinal-Cboi1Yqb.js +2 -0
  146. package/dist/ui/assets/ordinal-Cboi1Yqb.js.map +1 -0
  147. package/dist/ui/assets/pegDiagram-XKGWAZYB-x242yOd7.js +2 -0
  148. package/dist/ui/assets/pegDiagram-XKGWAZYB-x242yOd7.js.map +1 -0
  149. package/dist/ui/assets/pieDiagram-E7YTZNPT-Dl-YP7Xs.js +40 -0
  150. package/dist/ui/assets/pieDiagram-E7YTZNPT-Dl-YP7Xs.js.map +1 -0
  151. package/dist/ui/assets/quadrantDiagram-AXDQQJYC-CMJ920V9.js +8 -0
  152. package/dist/ui/assets/quadrantDiagram-AXDQQJYC-CMJ920V9.js.map +1 -0
  153. package/dist/ui/assets/railroadDiagram-O6MQD6OU-1kV83y5t.js +2 -0
  154. package/dist/ui/assets/railroadDiagram-O6MQD6OU-1kV83y5t.js.map +1 -0
  155. package/dist/ui/assets/requirementDiagram-BXWQKSXE-BFd8ZZD3.js +85 -0
  156. package/dist/ui/assets/requirementDiagram-BXWQKSXE-BFd8ZZD3.js.map +1 -0
  157. package/dist/ui/assets/sankeyDiagram-P5KCCOFB-C0LZevnf.js +41 -0
  158. package/dist/ui/assets/sankeyDiagram-P5KCCOFB-C0LZevnf.js.map +1 -0
  159. package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-BasIXLJ0.js +163 -0
  160. package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-BasIXLJ0.js.map +1 -0
  161. package/dist/ui/assets/sizeCapture-INFHLROL-qAtnqB7N.js +2 -0
  162. package/dist/ui/assets/sizeCapture-INFHLROL-qAtnqB7N.js.map +1 -0
  163. package/dist/ui/assets/stateDiagram-D77RDMKH-CllWDWZP.js +2 -0
  164. package/dist/ui/assets/stateDiagram-D77RDMKH-CllWDWZP.js.map +1 -0
  165. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-Dwntf8Tk.js +2 -0
  166. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-Dwntf8Tk.js.map +1 -0
  167. package/dist/ui/assets/swimlanes-42K2YHIH-Bgt0KslH.js +2 -0
  168. package/dist/ui/assets/swimlanes-42K2YHIH-Bgt0KslH.js.map +1 -0
  169. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-C9abicdw.js +9 -0
  170. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-C9abicdw.js.map +1 -0
  171. package/dist/ui/assets/timeline-definition-24CTP7MA-BYO2h9Qr.js +121 -0
  172. package/dist/ui/assets/timeline-definition-24CTP7MA-BYO2h9Qr.js.map +1 -0
  173. package/dist/ui/assets/vennDiagram-4TSXK5OY-ZzyiwIUi.js +35 -0
  174. package/dist/ui/assets/vennDiagram-4TSXK5OY-ZzyiwIUi.js.map +1 -0
  175. package/dist/ui/assets/wardleyDiagram-VM6X3IG4-DfjKCK0N.js +79 -0
  176. package/dist/ui/assets/wardleyDiagram-VM6X3IG4-DfjKCK0N.js.map +1 -0
  177. package/dist/ui/assets/xychartDiagram-S5SC5T6Z-Z7daOfmM.js +8 -0
  178. package/dist/ui/assets/xychartDiagram-S5SC5T6Z-Z7daOfmM.js.map +1 -0
  179. package/dist/ui/favicon.svg +6 -0
  180. package/dist/ui/index.html +21 -0
  181. package/package.json +70 -0
  182. package/scripts/copy-ui.mjs +21 -0
  183. package/scripts/fix-node-pty-perms.cjs +73 -0
@@ -0,0 +1,3490 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../../internal/toolkit-core/src/log.ts
4
+ import pc from "picocolors";
5
+ var log = {
6
+ title: (msg) => console.log(pc.bold(pc.cyan(msg))),
7
+ step: (msg) => console.log(`${pc.cyan("\u2192")} ${msg}`),
8
+ added: (msg) => console.log(pc.green(` + ${msg}`)),
9
+ kept: (msg) => console.log(pc.dim(` \xB7 ${msg}`)),
10
+ removed: (msg) => console.log(pc.yellow(` - ${msg}`)),
11
+ warn: (msg) => console.log(pc.yellow(` ! ${msg}`)),
12
+ success: (msg) => console.log(pc.bold(pc.green(`\u2713 ${msg}`))),
13
+ error: (msg) => console.error(pc.red(`\u2717 ${msg}`)),
14
+ blank: () => console.log("")
15
+ };
16
+
17
+ // ../../internal/toolkit-core/src/tool.ts
18
+ function defineTool(tool) {
19
+ return tool;
20
+ }
21
+
22
+ // ../../internal/toolkit-core/src/fs.ts
23
+ import { constants } from "fs";
24
+ import {
25
+ access,
26
+ cp,
27
+ lstat,
28
+ mkdir,
29
+ readFile,
30
+ readlink,
31
+ rm,
32
+ symlink,
33
+ writeFile
34
+ } from "fs/promises";
35
+ import path from "path";
36
+ async function exists(p2) {
37
+ try {
38
+ await access(p2, constants.F_OK);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ async function copyReplacing(src, dest) {
45
+ await mkdir(path.dirname(dest), { recursive: true });
46
+ await rm(dest, { recursive: true, force: true });
47
+ await cp(src, dest, { recursive: true });
48
+ }
49
+ async function copyIfAbsent(src, dest, force = false) {
50
+ if (!force && await exists(dest)) return false;
51
+ await copyReplacing(src, dest);
52
+ return true;
53
+ }
54
+ async function ensureSymlink(target, linkPath) {
55
+ await mkdir(path.dirname(linkPath), { recursive: true });
56
+ let result = "created";
57
+ try {
58
+ const stat3 = await lstat(linkPath);
59
+ if (stat3.isSymbolicLink()) {
60
+ if (await readlink(linkPath) === target) return "unchanged";
61
+ await rm(linkPath);
62
+ } else {
63
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
64
+ await cp(linkPath, `${linkPath}.bak-${stamp}`, { recursive: true });
65
+ await rm(linkPath, { recursive: true, force: true });
66
+ result = "backed-up";
67
+ }
68
+ } catch {
69
+ }
70
+ await symlink(target, linkPath);
71
+ return result;
72
+ }
73
+ async function ensureGitignore(projectRoot, header, entries) {
74
+ const file = path.join(projectRoot, ".gitignore");
75
+ const current = await exists(file) ? await readFile(file, "utf8") : "";
76
+ const lines = new Set(current.split(/\r?\n/).map((l) => l.trim().replace(/\/$/, "")));
77
+ const missing = entries.filter((e) => !lines.has(e.replace(/\/$/, "")));
78
+ if (missing.length === 0) return [];
79
+ const prefix = current === "" || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
80
+ await writeFile(file, `${current}${prefix}# ${header}
81
+ ${missing.join("\n")}
82
+ `, "utf8");
83
+ return missing;
84
+ }
85
+
86
+ // ../../internal/toolkit-core/src/package-root.ts
87
+ import { existsSync } from "fs";
88
+ import path2 from "path";
89
+ import { fileURLToPath } from "url";
90
+ function findPackageRoot(importMetaUrl) {
91
+ let dir = path2.dirname(fileURLToPath(importMetaUrl));
92
+ for (; ; ) {
93
+ if (existsSync(path2.join(dir, "package.json"))) return dir;
94
+ const parent = path2.dirname(dir);
95
+ if (parent === dir) throw new Error(`package.json n\xE3o encontrado a partir de ${importMetaUrl}`);
96
+ dir = parent;
97
+ }
98
+ }
99
+
100
+ // src/commands/install.ts
101
+ import { spawn } from "child_process";
102
+ import { readFile as readFile2, readdir, rm as rm2, writeFile as writeFile2 } from "fs/promises";
103
+ import path4 from "path";
104
+
105
+ // src/paths.ts
106
+ import path3 from "path";
107
+ var packageRoot = findPackageRoot(import.meta.url);
108
+ var templateDir = path3.join(packageRoot, "assets", "template");
109
+ var uiDir = path3.join(packageRoot, "dist", "ui");
110
+ var cliEntry = path3.join(packageRoot, "dist", "cli.js");
111
+ var statuslineScript = path3.join(packageRoot, "assets", "specs-usage-statusline.sh");
112
+ function runDir(projectRoot) {
113
+ return path3.join(projectRoot, ".specs", ".run");
114
+ }
115
+
116
+ // src/commands/install.ts
117
+ async function runInstall(opts) {
118
+ const { projectRoot, force = false } = opts;
119
+ const src = (...p2) => path4.join(templateDir, ...p2);
120
+ const dest = (...p2) => path4.join(projectRoot, ...p2);
121
+ const fresh = !await exists(dest(".specs", "specs", "meta.json"));
122
+ log.title(`specs install \u2014 ${projectRoot}`);
123
+ log.kept(
124
+ fresh ? "modo: instala\xE7\xE3o" : force ? "modo: for\xE7a (sobrescreve customiza\xE7\xF5es)" : "modo: atualiza\xE7\xE3o"
125
+ );
126
+ log.blank();
127
+ log.step("Agentes e skills (.agents/)");
128
+ for (const kind of ["agents", "skills", "scripts"]) {
129
+ for (const entry of await readdir(src(".agents", kind))) {
130
+ await copyReplacing(src(".agents", kind, entry), dest(".agents", kind, entry));
131
+ log.added(`.agents/${kind}/${entry}`);
132
+ }
133
+ }
134
+ for (const file of ["README.md", "RUNTIME.md"]) {
135
+ await copyReplacing(src(".agents", file), dest(".agents", file));
136
+ log.added(`.agents/${file}`);
137
+ }
138
+ await installCustomizable(
139
+ src(".agents", "project.md"),
140
+ dest(".agents", "project.md"),
141
+ ".agents/project.md",
142
+ force
143
+ );
144
+ for (const legacy of [
145
+ dest(".agents", "agents", "specs-runner.md"),
146
+ dest(".claude", "agents", "specs-runner.md")
147
+ ]) {
148
+ if (await exists(legacy)) {
149
+ await rm2(legacy);
150
+ log.removed(`${path4.relative(projectRoot, legacy)} (descontinuado)`);
151
+ }
152
+ }
153
+ for (const consumer of [".claude", ".cursor"]) {
154
+ for (const kind of ["agents", "skills"]) {
155
+ const result = await ensureSymlink(`../.agents/${kind}`, dest(consumer, kind));
156
+ const label = `${consumer}/${kind} \u2192 .agents/${kind}`;
157
+ if (result === "backed-up") log.warn(`${label} (diret\xF3rio real movido para ${kind}.bak-*)`);
158
+ else if (result === "created") log.added(label);
159
+ else log.kept(label);
160
+ }
161
+ }
162
+ log.blank();
163
+ log.step("Scaffold (.specs/)");
164
+ const configPath = dest(".specs", "config.json");
165
+ if (await copyIfAbsent(src(".specs", "config.json"), configPath, force)) {
166
+ const terminal = await detectDefaultTerminal();
167
+ await setOpenTerminal(configPath, terminal);
168
+ log.added(`.specs/config.json (openTerminal: ${terminal})`);
169
+ } else {
170
+ log.kept(".specs/config.json (preservado)");
171
+ }
172
+ for (const dir of ["specs", "discoveries", "drawings"]) {
173
+ await installCustomizable(
174
+ src(".specs", dir, "meta.json"),
175
+ dest(".specs", dir, "meta.json"),
176
+ `.specs/${dir}/meta.json`,
177
+ force
178
+ );
179
+ }
180
+ for (const tpl of await readdir(src(".specs", "specs", "_templates"))) {
181
+ await installCustomizable(
182
+ src(".specs", "specs", "_templates", tpl),
183
+ dest(".specs", "specs", "_templates", tpl),
184
+ `.specs/specs/_templates/${tpl}`,
185
+ force
186
+ );
187
+ }
188
+ if (fresh || force) {
189
+ await copyReplacing(src(".specs", "specs", "exemplo"), dest(".specs", "specs", "exemplo"));
190
+ log.added(".specs/specs/exemplo/ (feature exemplo)");
191
+ }
192
+ await installCustomizable(src("SPECS.md"), dest("SPECS.md"), "SPECS.md", force);
193
+ if (!force && (await readFile2(dest("SPECS.md"), "utf8")).includes("install-here.sh")) {
194
+ log.warn(
195
+ "SPECS.md \xE9 da vers\xE3o antiga (fala de .specs/app/ e specs:run). Apague-o e rode de novo para regenerar."
196
+ );
197
+ }
198
+ log.blank();
199
+ log.step(".gitignore");
200
+ const added = await ensureGitignore(
201
+ projectRoot,
202
+ "Specs Platform \u2014 estado local da m\xE1quina, n\xE3o do projeto",
203
+ [".specs/.jobs.json", ".specs/.run/"]
204
+ );
205
+ if (added.length > 0) log.added(`.gitignore (${added.join(", ")})`);
206
+ else log.kept(".gitignore j\xE1 cobre o estado local");
207
+ const legacyAppDir = await exists(dest(".specs", "app"));
208
+ if (legacyAppDir) {
209
+ log.blank();
210
+ if (opts.cleanLegacy) {
211
+ await rm2(dest(".specs", "app"), { recursive: true, force: true });
212
+ log.removed(".specs/app/ (runtime antigo \u2014 agora a plataforma roda do pacote npm)");
213
+ } else {
214
+ log.warn(
215
+ ".specs/app/ \xE9 de uma instala\xE7\xE3o antiga e n\xE3o \xE9 mais usado. Remova com `specs install --clean-legacy`."
216
+ );
217
+ }
218
+ }
219
+ log.blank();
220
+ log.success(`Specs Platform ${fresh ? "instalada" : "atualizada"} em ${projectRoot}`);
221
+ console.log(" Pr\xF3ximo passo: preencha .agents/project.md e rode `specs start`.");
222
+ return { fresh, legacyAppDir };
223
+ }
224
+ async function installCustomizable(src, dest, label, force) {
225
+ if (await copyIfAbsent(src, dest, force)) log.added(label);
226
+ else log.kept(`${label} (preservado)`);
227
+ }
228
+ async function setOpenTerminal(configPath, terminal) {
229
+ const config = JSON.parse(await readFile2(configPath, "utf8"));
230
+ if (config.agent && typeof config.agent === "object") config.agent.openTerminal = terminal;
231
+ await writeFile2(configPath, `${JSON.stringify(config, null, 2)}
232
+ `, "utf8");
233
+ }
234
+ async function detectDefaultTerminal() {
235
+ if (process.platform === "darwin") return "osascript";
236
+ if (process.platform !== "linux") return "none";
237
+ const which = (bin) => new Promise((resolve) => {
238
+ const child = spawn("which", [bin], { stdio: "ignore" });
239
+ child.on("exit", (code) => resolve(code === 0));
240
+ child.on("error", () => resolve(false));
241
+ });
242
+ if (process.env.KITTY_WINDOW_ID || await which("kitty")) return "kitty";
243
+ if (process.env.WEZTERM_PANE || await which("wezterm")) return "wezterm";
244
+ if (await which("gnome-terminal")) return "gnome-terminal";
245
+ return "none";
246
+ }
247
+
248
+ // src/commands/start.ts
249
+ import { spawn as spawn5 } from "child_process";
250
+ import { openSync } from "fs";
251
+ import { mkdir as mkdir3, readFile as readFile9, rm as rm4, writeFile as writeFile4 } from "fs/promises";
252
+ import net from "net";
253
+ import path15 from "path";
254
+
255
+ // ../../internal/specs-server/src/start-server.ts
256
+ import path14 from "path";
257
+ import { readFile as readFile8, rm as rm3, unlink } from "fs/promises";
258
+ import Fastify from "fastify";
259
+ import fastifyCors from "@fastify/cors";
260
+ import fastifyStatic from "@fastify/static";
261
+ import { z as z2 } from "zod";
262
+
263
+ // ../../internal/specs-server/src/config-defaults.ts
264
+ var DEFAULT_INPUT_PROMPT_PATTERNS = [
265
+ "\\?\\s*$",
266
+ "\\((?:y\\/n|Y\\/n|y\\/N|Y\\/N)\\)",
267
+ "press\\s+(?:enter|any key)",
268
+ "Do you want to",
269
+ "Would you like to",
270
+ "waiting for input",
271
+ "\u276F\\s*$"
272
+ ];
273
+ var DEFAULT_REVIEW_OPEN_COMMAND = "cursor --goto {path}:{line}";
274
+ var DEFAULT_CLAUDE_COMMAND = 'claude {model} {effort} --agent feature-runner "{prompt}"';
275
+ var DEFAULT_CURSOR_COMMAND = 'cursor-agent {model} "Use the feature-runner subagent. {prompt}"';
276
+ var DEFAULT_MODELS_COMMAND = {
277
+ cursor: "cursor-agent --list-models"
278
+ };
279
+ var DEFAULT_STATIC_MODELS = {
280
+ claude: ["opus", "sonnet", "haiku", "fable"]
281
+ };
282
+ var DEFAULT_PROTOTYPE_OPEN_COMMAND = {
283
+ pencil: "open -a Pencil {file}",
284
+ "claude-design": "open {url}"
285
+ };
286
+ function normalizePrototype(raw) {
287
+ if (!raw || typeof raw !== "object") return null;
288
+ const p2 = raw;
289
+ if (p2.tool !== "claude-design" && p2.tool !== "pencil") return null;
290
+ if (p2.tool === "claude-design" && !p2.url) return null;
291
+ if (p2.tool === "pencil" && !p2.file) return null;
292
+ return {
293
+ tool: p2.tool,
294
+ ...p2.title ? { title: p2.title } : {},
295
+ ...p2.url ? { url: p2.url } : {},
296
+ ...p2.file ? { file: p2.file } : {},
297
+ ...p2.designDir ? { designDir: p2.designDir } : {},
298
+ ...Array.isArray(p2.artboards) ? { artboards: p2.artboards.map(String) } : {},
299
+ ...Array.isArray(p2.nodeIds) ? { nodeIds: p2.nodeIds.map(String) } : {},
300
+ ...p2.openCommand ? { openCommand: p2.openCommand } : {},
301
+ ...p2.snapshot ? { snapshot: p2.snapshot } : {}
302
+ };
303
+ }
304
+ function applyDefaults(partial) {
305
+ const agent = partial.agent ?? {};
306
+ const scopes = agent.scopes ?? {};
307
+ const userCommands = agent.commands ?? {};
308
+ const commands = {
309
+ claude: userCommands.claude ?? agent.command ?? DEFAULT_CLAUDE_COMMAND,
310
+ cursor: userCommands.cursor ?? DEFAULT_CURSOR_COMMAND,
311
+ ...userCommands
312
+ };
313
+ const cli = agent.cli && commands[agent.cli] ? agent.cli : "claude";
314
+ const modelsCommand = {
315
+ ...DEFAULT_MODELS_COMMAND,
316
+ ...agent.modelsCommand ?? {}
317
+ };
318
+ const models = {
319
+ ...DEFAULT_STATIC_MODELS,
320
+ ...agent.models ?? {}
321
+ };
322
+ return {
323
+ agent: {
324
+ cli,
325
+ commands,
326
+ modelsCommand,
327
+ models,
328
+ command: agent.command ?? commands[cli] ?? DEFAULT_CLAUDE_COMMAND,
329
+ openTerminal: agent.openTerminal ?? (process.platform === "darwin" ? "osascript" : "none"),
330
+ cwd: agent.cwd ?? ".",
331
+ scopes: {
332
+ feature: scopes.feature ?? 'Execute a feature "{featureTitle}" ({featurePath}). Se houver task em progresso, continue de onde parou; sen\xE3o, escolha a pr\xF3xima task em pages[] do meta.json.',
333
+ featureNoPause: scopes.featureNoPause ?? 'Execute a feature "{featureTitle}" ({featurePath}) por inteiro, sem pausar entre tasks. Continue executando cada pr\xF3xima task pendente do meta.json automaticamente at\xE9 completar todas. Os testes E2E e a solicita\xE7\xE3o de code review devem ser feitos apenas ao final de todas as tasks.',
334
+ task: scopes.task ?? "Execute a task {featurePath}{task}.md. Se j\xE1 houver trabalho em progresso nela, continue de onde parou."
335
+ },
336
+ defaultExecutionMode: agent.defaultExecutionMode ?? "inline",
337
+ sound: agent.sound ?? true,
338
+ inputPromptPatterns: agent.inputPromptPatterns && agent.inputPromptPatterns.length > 0 ? agent.inputPromptPatterns : DEFAULT_INPUT_PROMPT_PATTERNS,
339
+ bufferBytesCap: agent.bufferBytesCap ?? 2e6
340
+ },
341
+ prototype: normalizePrototype(partial.prototype),
342
+ review: {
343
+ openCommand: partial.review?.openCommand ?? DEFAULT_REVIEW_OPEN_COMMAND
344
+ },
345
+ theme: "dark",
346
+ warnBelowWidth: partial.warnBelowWidth ?? 1024,
347
+ featuresDir: partial.featuresDir ?? ".specs/specs",
348
+ discoveriesDir: partial.discoveriesDir ?? ".specs/discoveries",
349
+ drawingsDir: partial.drawingsDir ?? ".specs/drawings",
350
+ port: partial.port ?? 4321
351
+ };
352
+ }
353
+
354
+ // ../../internal/specs-server/src/claude-usage.ts
355
+ import { readFile as readFile3, stat } from "fs/promises";
356
+ import os from "os";
357
+ import path5 from "path";
358
+ var HOUR_MS = 36e5;
359
+ var FIVE_HOURS_MS = 5 * HOUR_MS;
360
+ var SEVEN_DAYS_MS = 7 * 24 * HOUR_MS;
361
+ function desktopHistoryPath(home = os.homedir()) {
362
+ return path5.join(home, "Library", "Application Support", "Claude", "plan-usage-history.json");
363
+ }
364
+ function cliCachePath(home = os.homedir()) {
365
+ return path5.join(home, ".claude", "rate-limits-cache.json");
366
+ }
367
+ function statuslineSnapshotPath(home = os.homedir()) {
368
+ return path5.join(home, ".claude", "specs-usage.json");
369
+ }
370
+ function parseSamples(raw) {
371
+ if (typeof raw !== "object" || raw === null) return [];
372
+ const list = raw.samples;
373
+ if (!Array.isArray(list)) return [];
374
+ const out = [];
375
+ for (const item of list) {
376
+ if (typeof item !== "object" || item === null) continue;
377
+ const { t, u } = item;
378
+ if (typeof t !== "number" || !Number.isFinite(t)) continue;
379
+ if (typeof u !== "object" || u === null) continue;
380
+ const { fh, sd } = u;
381
+ if (typeof fh !== "number" || typeof sd !== "number") continue;
382
+ out.push({ t, fh, sd });
383
+ }
384
+ return out.sort((a, b) => a.t - b.t);
385
+ }
386
+ function dropIntervals(samples, key) {
387
+ const out = [];
388
+ for (let i = 1; i < samples.length; i += 1) {
389
+ const prev = samples[i - 1];
390
+ const cur = samples[i];
391
+ if (!prev || !cur) continue;
392
+ if (cur[key] < prev[key]) out.push({ from: prev.t, to: cur.t });
393
+ }
394
+ return out;
395
+ }
396
+ function hourBoundariesIn(from, to) {
397
+ const out = [];
398
+ let h = Math.floor(from / HOUR_MS) * HOUR_MS + HOUR_MS;
399
+ while (h <= to) {
400
+ out.push(h);
401
+ h += HOUR_MS;
402
+ }
403
+ return out;
404
+ }
405
+ function inferSessionReset(samples, now) {
406
+ const drops = dropIntervals(samples, "fh");
407
+ const last = drops[drops.length - 1];
408
+ if (!last) return null;
409
+ if (last.to - last.from > HOUR_MS) return null;
410
+ const boundaries = hourBoundariesIn(last.from, last.to);
411
+ const anchor = boundaries.length === 1 ? boundaries[0] : void 0;
412
+ if (anchor === void 0) return null;
413
+ const reset = anchor + FIVE_HOURS_MS;
414
+ return reset > now ? reset : null;
415
+ }
416
+ function inferPeriodicReset(samples, key, periodMs, now) {
417
+ const drops = dropIntervals(samples, key);
418
+ const lastDrop = drops[drops.length - 1];
419
+ if (!lastDrop) return null;
420
+ const base = Math.floor(lastDrop.to / HOUR_MS) * HOUR_MS;
421
+ const slots = Math.round(periodMs / HOUR_MS);
422
+ let best = null;
423
+ let bestScore = -1;
424
+ for (let h = 0; h < slots; h += 1) {
425
+ const candidate = base - h * HOUR_MS;
426
+ let score = 0;
427
+ for (const drop of drops) {
428
+ const k = Math.floor((drop.to - candidate) / periodMs);
429
+ const instant = candidate + k * periodMs;
430
+ if (instant > drop.from && instant <= drop.to) {
431
+ score += HOUR_MS / Math.max(drop.to - drop.from, HOUR_MS);
432
+ }
433
+ }
434
+ if (score > bestScore) {
435
+ bestScore = score;
436
+ best = candidate;
437
+ }
438
+ }
439
+ if (best === null || bestScore <= 0) return null;
440
+ let reset = best;
441
+ while (reset <= now) reset += periodMs;
442
+ return reset;
443
+ }
444
+ function windowFrom(percentage, resetMs, estimated) {
445
+ return {
446
+ usedPercentage: Math.max(0, Math.min(100, percentage)),
447
+ resetsAt: resetMs === null ? null : Math.round(resetMs / 1e3),
448
+ resetEstimated: estimated
449
+ };
450
+ }
451
+ async function readDesktopHistory(home, now) {
452
+ try {
453
+ const samples = parseSamples(JSON.parse(await readFile3(desktopHistoryPath(home), "utf8")));
454
+ const latest = samples[samples.length - 1];
455
+ if (!latest) return null;
456
+ return {
457
+ fiveHour: windowFrom(latest.fh, inferSessionReset(samples, now), true),
458
+ sevenDay: windowFrom(latest.sd, inferPeriodicReset(samples, "sd", SEVEN_DAYS_MS, now), true),
459
+ measuredAt: latest.t,
460
+ source: "desktop-history"
461
+ };
462
+ } catch {
463
+ return null;
464
+ }
465
+ }
466
+ function parseCliWindow(raw) {
467
+ if (typeof raw !== "object" || raw === null) return null;
468
+ const { used_percentage: used, resets_at: resets } = raw;
469
+ if (typeof used !== "number" || !Number.isFinite(used)) return null;
470
+ const resetsAt = typeof resets === "number" && Number.isFinite(resets) ? resets * 1e3 : null;
471
+ return windowFrom(used, resetsAt, false);
472
+ }
473
+ async function readStatuslineSnapshot(home) {
474
+ try {
475
+ const parsed = JSON.parse(await readFile3(statuslineSnapshotPath(home), "utf8"));
476
+ if (typeof parsed !== "object" || parsed === null) return null;
477
+ const record = parsed;
478
+ const fiveHour = parseCliWindow(record.five_hour);
479
+ const sevenDay = parseCliWindow(record.seven_day);
480
+ if (!fiveHour && !sevenDay) return null;
481
+ const measuredAt = typeof record.measuredAt === "number" && Number.isFinite(record.measuredAt) ? record.measuredAt : 0;
482
+ if (measuredAt <= 0) return null;
483
+ return { fiveHour, sevenDay, measuredAt, source: "statusline" };
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+ async function readCliCache(home) {
489
+ const file = cliCachePath(home);
490
+ try {
491
+ const [raw, info] = await Promise.all([readFile3(file, "utf8"), stat(file)]);
492
+ const parsed = JSON.parse(raw);
493
+ if (typeof parsed !== "object" || parsed === null) return null;
494
+ const record = parsed;
495
+ const fiveHour = parseCliWindow(record.five_hour);
496
+ const sevenDay = parseCliWindow(record.seven_day);
497
+ if (!fiveHour && !sevenDay) return null;
498
+ return { fiveHour, sevenDay, measuredAt: info.mtimeMs, source: "cli-cache" };
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
503
+ async function readClaudeUsage(home = os.homedir(), now = Date.now()) {
504
+ const candidates = await Promise.all([
505
+ readDesktopHistory(home, now),
506
+ readCliCache(home),
507
+ readStatuslineSnapshot(home)
508
+ ]);
509
+ let best = null;
510
+ for (const candidate of candidates) {
511
+ if (!candidate) continue;
512
+ if (!best || candidate.measuredAt > best.measuredAt) best = candidate;
513
+ }
514
+ return best;
515
+ }
516
+
517
+ // ../../internal/specs-server/src/specs-watcher.ts
518
+ import { watch } from "fs";
519
+ import path6 from "path";
520
+ function segments(relPath) {
521
+ return relPath.split(path6.sep).filter((s) => s.length > 0 && s !== ".");
522
+ }
523
+ function isUnder(relPath, dir) {
524
+ const base = segments(dir);
525
+ const parts = segments(relPath);
526
+ if (parts.length <= base.length) return null;
527
+ for (let i = 0; i < base.length; i += 1) {
528
+ if (parts[i] !== base[i]) return null;
529
+ }
530
+ return parts.slice(base.length).join("/");
531
+ }
532
+ function classifyPath(relPath, dirs) {
533
+ const normalized = relPath.split(path6.sep).join("/");
534
+ const name = normalized.split("/").pop() ?? "";
535
+ if (name.startsWith(".") || name.endsWith("~")) return [];
536
+ if (normalized === ".specs/config.json") return [{ type: "config" }];
537
+ const inFeatures = isUnder(relPath, dirs.featuresDir);
538
+ if (inFeatures !== null) {
539
+ const parts = inFeatures.split("/");
540
+ if (parts.length === 1 && parts[0] === "meta.json") return [{ type: "tree" }];
541
+ const [feature, file] = parts;
542
+ if (!feature || !file || parts.length !== 2) return [{ type: "tree" }];
543
+ if (file === "meta.json") return [{ type: "tree" }];
544
+ if (file.endsWith(".md")) {
545
+ const task = file.slice(0, -3);
546
+ return [{ type: "content", feature, task }, { type: "tree" }];
547
+ }
548
+ return [];
549
+ }
550
+ const inDiscoveries = isUnder(relPath, dirs.discoveriesDir);
551
+ if (inDiscoveries !== null) {
552
+ const parts = inDiscoveries.split("/");
553
+ if (parts.length !== 1) return [];
554
+ const file = parts[0] ?? "";
555
+ if (file === "meta.json") return [{ type: "tree" }];
556
+ if (file.endsWith(".md")) {
557
+ return [{ type: "discovery", slug: file.slice(0, -3) }, { type: "tree" }];
558
+ }
559
+ return [];
560
+ }
561
+ const inDrawings = isUnder(relPath, dirs.drawingsDir);
562
+ if (inDrawings !== null) {
563
+ const parts = inDrawings.split("/");
564
+ if (parts.length !== 1) return [];
565
+ const file = parts[0] ?? "";
566
+ if (file === "meta.json") return [{ type: "tree" }];
567
+ if (file.endsWith(".md")) {
568
+ return [{ type: "drawing", slug: file.slice(0, -3) }, { type: "tree" }];
569
+ }
570
+ return [];
571
+ }
572
+ return [];
573
+ }
574
+ function eventKey(event) {
575
+ switch (event.type) {
576
+ case "content":
577
+ return `content:${event.feature}/${event.task}`;
578
+ case "discovery":
579
+ return `discovery:${event.slug}`;
580
+ case "drawing":
581
+ return `drawing:${event.slug}`;
582
+ default:
583
+ return event.type;
584
+ }
585
+ }
586
+ var REARM_RETRY_MS = 5e3;
587
+ function watchFileRearming(file, onChange) {
588
+ let current = null;
589
+ let retry = null;
590
+ let stopped = false;
591
+ function scheduleRetry() {
592
+ if (stopped || retry) return;
593
+ retry = setTimeout(() => {
594
+ retry = null;
595
+ arm();
596
+ }, REARM_RETRY_MS);
597
+ retry.unref?.();
598
+ }
599
+ function arm() {
600
+ if (stopped) return;
601
+ try {
602
+ current = watch(file, { persistent: false }, () => {
603
+ onChange();
604
+ try {
605
+ current?.close();
606
+ } catch {
607
+ }
608
+ current = null;
609
+ arm();
610
+ });
611
+ } catch {
612
+ current = null;
613
+ scheduleRetry();
614
+ }
615
+ }
616
+ arm();
617
+ return () => {
618
+ stopped = true;
619
+ if (retry) clearTimeout(retry);
620
+ try {
621
+ current?.close();
622
+ } catch {
623
+ }
624
+ current = null;
625
+ };
626
+ }
627
+ function createSpecsWatcher(opts) {
628
+ const {
629
+ projectRoot,
630
+ featuresDir,
631
+ discoveriesDir,
632
+ drawingsDir,
633
+ extraFiles = [],
634
+ debounceMs = 150
635
+ } = opts;
636
+ const listeners = /* @__PURE__ */ new Set();
637
+ const watchers = [];
638
+ const fileWatchers = [];
639
+ const pending = /* @__PURE__ */ new Map();
640
+ let timer = null;
641
+ let watching = false;
642
+ function flush() {
643
+ timer = null;
644
+ if (pending.size === 0) return;
645
+ const batch = [...pending.values()];
646
+ pending.clear();
647
+ for (const listener of listeners) listener(batch);
648
+ }
649
+ function queue(events) {
650
+ if (events.length === 0) return;
651
+ for (const event of events) pending.set(eventKey(event), event);
652
+ if (timer) clearTimeout(timer);
653
+ timer = setTimeout(flush, debounceMs);
654
+ }
655
+ const specsRoot = path6.join(projectRoot, ".specs");
656
+ try {
657
+ watchers.push(
658
+ watch(specsRoot, { recursive: true, persistent: false }, (_event, filename) => {
659
+ if (!filename) return;
660
+ const rel = path6.join(".specs", filename.toString());
661
+ queue(classifyPath(rel, { featuresDir, discoveriesDir, drawingsDir }));
662
+ })
663
+ );
664
+ watching = true;
665
+ } catch {
666
+ watching = false;
667
+ }
668
+ for (const { file, event } of extraFiles) {
669
+ fileWatchers.push(watchFileRearming(file, () => queue([event])));
670
+ }
671
+ return {
672
+ subscribe(listener) {
673
+ listeners.add(listener);
674
+ return () => listeners.delete(listener);
675
+ },
676
+ get watching() {
677
+ return watching;
678
+ },
679
+ close() {
680
+ if (timer) clearTimeout(timer);
681
+ for (const w of watchers) {
682
+ try {
683
+ w.close();
684
+ } catch {
685
+ }
686
+ }
687
+ for (const stop of fileWatchers) stop();
688
+ listeners.clear();
689
+ }
690
+ };
691
+ }
692
+
693
+ // ../../internal/specs-server/src/fs-utils.ts
694
+ import { constants as constants2 } from "fs";
695
+ import { access as access2, readFile as readFile4, rename, stat as stat2, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
696
+ import path7 from "path";
697
+ var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*$/;
698
+ function assertInsideProject(absPath, projectRoot) {
699
+ const rel = path7.relative(projectRoot, absPath);
700
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
701
+ throw new Error(`Path ${absPath} fora do projectRoot ${projectRoot}.`);
702
+ }
703
+ }
704
+ async function fileExists(p2) {
705
+ try {
706
+ await access2(p2, constants2.F_OK);
707
+ const s = await stat2(p2);
708
+ return s.isFile();
709
+ } catch {
710
+ return false;
711
+ }
712
+ }
713
+ async function dirExists(p2) {
714
+ try {
715
+ const s = await stat2(p2);
716
+ return s.isDirectory();
717
+ } catch {
718
+ return false;
719
+ }
720
+ }
721
+ async function readText(p2) {
722
+ return readFile4(p2, "utf8");
723
+ }
724
+ function detectEOL(content) {
725
+ const sample = content.length > 4096 ? content.slice(0, 4096) : content;
726
+ return sample.includes("\r\n") ? "\r\n" : "\n";
727
+ }
728
+ async function atomicWrite(finalPath, content) {
729
+ const dir = path7.dirname(finalPath);
730
+ await mkdir2(dir, { recursive: true });
731
+ const tmp = `${finalPath}.tmp-${process.pid}-${Date.now()}`;
732
+ await writeFile3(tmp, content, "utf8");
733
+ await rename(tmp, finalPath);
734
+ }
735
+
736
+ // ../../internal/specs-server/src/tree-builder.ts
737
+ import path8 from "path";
738
+ import { readFile as readFile5 } from "fs/promises";
739
+
740
+ // ../../internal/specs-server/src/frontmatter.ts
741
+ import matter from "gray-matter";
742
+ var VALID_STATUS = ["pending", "in-progress", "completed", "blocked"];
743
+ function isValidStatus(s) {
744
+ return typeof s === "string" && VALID_STATUS.includes(s);
745
+ }
746
+ var VALID_DISCOVERY_TYPE = ["rfc", "spike", "adr", "note"];
747
+ function isValidDiscoveryType(s) {
748
+ return typeof s === "string" && VALID_DISCOVERY_TYPE.includes(s);
749
+ }
750
+ var VALID_DRAWING_TYPE = [
751
+ "architecture",
752
+ "flow",
753
+ "sequence",
754
+ "data",
755
+ "state"
756
+ ];
757
+ function isValidDrawingType(s) {
758
+ return typeof s === "string" && VALID_DRAWING_TYPE.includes(s);
759
+ }
760
+ function toIsoDate(value) {
761
+ if (typeof value === "string") return value;
762
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
763
+ return value.toISOString().slice(0, 10);
764
+ }
765
+ return void 0;
766
+ }
767
+ function parseFrontmatter(raw) {
768
+ const parsed = matter(raw);
769
+ return { data: parsed.data, body: parsed.content };
770
+ }
771
+ function patchStatusInFrontmatter(raw, newStatus) {
772
+ const eol = detectEOL(raw);
773
+ const leading = raw.slice(0, 4);
774
+ if (!leading.startsWith("---")) return raw;
775
+ const startIdx = raw.indexOf(`---${eol}`);
776
+ if (startIdx !== 0) return raw;
777
+ const afterStart = raw.slice(3 + eol.length);
778
+ const endIdx = afterStart.indexOf(`${eol}---`);
779
+ if (endIdx < 0) return raw;
780
+ const yamlBlock = afterStart.slice(0, endIdx);
781
+ const rest = afterStart.slice(endIdx);
782
+ const statusLineRegex = /^status:[ \t]*.*$/m;
783
+ let newYaml;
784
+ if (statusLineRegex.test(yamlBlock)) {
785
+ newYaml = yamlBlock.replace(statusLineRegex, `status: ${newStatus}`);
786
+ } else {
787
+ const trimmedEnd = yamlBlock.endsWith(eol) ? yamlBlock : `${yamlBlock}${eol}`;
788
+ newYaml = `${trimmedEnd}status: ${newStatus}`;
789
+ }
790
+ return `---${eol}${newYaml}${rest}`;
791
+ }
792
+ function extractFrontmatterFields(data) {
793
+ const title = typeof data.title === "string" ? data.title : void 0;
794
+ const description = typeof data.description === "string" ? data.description : void 0;
795
+ const rawStatus = data.status;
796
+ const status = isValidStatus(rawStatus) ? rawStatus : "pending";
797
+ return { title, description, status };
798
+ }
799
+ function extractDiscoveryFrontmatterFields(data) {
800
+ const title = typeof data.title === "string" ? data.title : void 0;
801
+ const rawType = data.type;
802
+ const type = isValidDiscoveryType(rawType) ? rawType : "note";
803
+ const date = toIsoDate(data.date);
804
+ return { title, type, date };
805
+ }
806
+ function extractDrawingFrontmatterFields(data) {
807
+ const title = typeof data.title === "string" ? data.title : void 0;
808
+ const rawType = data.type;
809
+ const type = isValidDrawingType(rawType) ? rawType : "architecture";
810
+ const date = toIsoDate(data.date);
811
+ return { title, type, date };
812
+ }
813
+
814
+ // ../../internal/specs-server/src/tree-builder.ts
815
+ function aggregateStatus(children) {
816
+ if (children.length === 0) return "pending";
817
+ if (children.some((s) => s === "blocked")) return "blocked";
818
+ if (children.every((s) => s === "completed")) return "completed";
819
+ if (children.some((s) => s === "in-progress" || s === "completed")) return "in-progress";
820
+ return "pending";
821
+ }
822
+ async function buildTree(projectRoot, featuresDir, discoveriesDir, drawingsDir) {
823
+ const featuresAbs = path8.join(projectRoot, featuresDir);
824
+ const rootMetaPath = path8.join(featuresAbs, "meta.json");
825
+ if (!await fileExists(rootMetaPath)) {
826
+ throw new Error("no_features_dir");
827
+ }
828
+ const rootRaw = await readFile5(rootMetaPath, "utf8");
829
+ const rootMeta = JSON.parse(rootRaw);
830
+ const rootPages = Array.isArray(rootMeta.pages) ? rootMeta.pages : [];
831
+ const features = [];
832
+ for (const slugUnknown of rootPages) {
833
+ if (typeof slugUnknown !== "string") continue;
834
+ const slug = slugUnknown;
835
+ const featureDir = path8.join(featuresAbs, slug);
836
+ const featureMetaPath = path8.join(featureDir, "meta.json");
837
+ if (!await dirExists(featureDir) || !await fileExists(featureMetaPath)) {
838
+ console.warn(
839
+ `[tree] feature "${slug}" listada em ${featuresDir}/meta.json mas sem pasta/meta \u2014 ignorando conte\xFAdo.`
840
+ );
841
+ features.push({
842
+ slug,
843
+ title: slug,
844
+ status: "pending",
845
+ tasks: []
846
+ });
847
+ continue;
848
+ }
849
+ let featureMeta;
850
+ try {
851
+ featureMeta = JSON.parse(await readText(featureMetaPath));
852
+ } catch (err) {
853
+ console.warn(`[tree] meta.json inv\xE1lido em ${featureMetaPath}: ${err.message}`);
854
+ featureMeta = {};
855
+ }
856
+ const title = typeof featureMeta.title === "string" ? featureMeta.title : slug;
857
+ const description = typeof featureMeta.description === "string" ? featureMeta.description : void 0;
858
+ const icon = typeof featureMeta.icon === "string" ? featureMeta.icon : void 0;
859
+ const pages = Array.isArray(featureMeta.pages) ? featureMeta.pages : [];
860
+ const tasks = [];
861
+ for (const pageUnknown of pages) {
862
+ if (typeof pageUnknown !== "string") continue;
863
+ const pageSlug = pageUnknown;
864
+ const taskFile = path8.join(featureDir, `${pageSlug}.md`);
865
+ if (!await fileExists(taskFile)) {
866
+ console.warn(
867
+ `[tree] task "${pageSlug}" listada em ${slug}/meta.json mas sem arquivo \u2014 omitindo.`
868
+ );
869
+ continue;
870
+ }
871
+ let raw;
872
+ try {
873
+ raw = await readText(taskFile);
874
+ } catch {
875
+ continue;
876
+ }
877
+ const parsed = parseFrontmatter(raw);
878
+ const {
879
+ title: taskTitle,
880
+ description: taskDescription,
881
+ status
882
+ } = extractFrontmatterFields(parsed.data);
883
+ tasks.push({
884
+ slug: pageSlug,
885
+ title: taskTitle ?? pageSlug,
886
+ description: taskDescription,
887
+ status,
888
+ file: path8.relative(projectRoot, taskFile)
889
+ });
890
+ }
891
+ features.push({
892
+ slug,
893
+ title,
894
+ description,
895
+ icon,
896
+ status: aggregateStatus(tasks.map((t) => t.status)),
897
+ tasks
898
+ });
899
+ }
900
+ const discoveries = await buildDiscoveries(projectRoot, discoveriesDir);
901
+ const drawings = await buildDrawings(projectRoot, drawingsDir);
902
+ return { features, discoveries, drawings };
903
+ }
904
+ async function buildFlatSection(projectRoot, dir, kind, toNode) {
905
+ const abs = path8.join(projectRoot, dir);
906
+ const metaPath = path8.join(abs, "meta.json");
907
+ if (!await dirExists(abs) || !await fileExists(metaPath)) {
908
+ return [];
909
+ }
910
+ let meta;
911
+ try {
912
+ meta = JSON.parse(await readText(metaPath));
913
+ } catch (err) {
914
+ console.warn(`[tree] meta.json inv\xE1lido em ${metaPath}: ${err.message}`);
915
+ return [];
916
+ }
917
+ const pages = Array.isArray(meta.pages) ? meta.pages : [];
918
+ const nodes = [];
919
+ for (const pageUnknown of pages) {
920
+ if (typeof pageUnknown !== "string") continue;
921
+ const pageSlug = pageUnknown;
922
+ const file = path8.join(abs, `${pageSlug}.md`);
923
+ if (!await fileExists(file)) {
924
+ console.warn(
925
+ `[tree] ${kind} "${pageSlug}" listada em ${dir}/meta.json mas sem arquivo \u2014 omitindo.`
926
+ );
927
+ continue;
928
+ }
929
+ let raw;
930
+ try {
931
+ raw = await readText(file);
932
+ } catch {
933
+ continue;
934
+ }
935
+ const parsed = parseFrontmatter(raw);
936
+ nodes.push(toNode(pageSlug, parsed.data, path8.relative(projectRoot, file)));
937
+ }
938
+ return nodes;
939
+ }
940
+ async function buildDiscoveries(projectRoot, discoveriesDir) {
941
+ return buildFlatSection(projectRoot, discoveriesDir, "discovery", (slug, data, file) => {
942
+ const { title, type, date } = extractDiscoveryFrontmatterFields(data);
943
+ return { slug, title: title ?? slug, type, date, file };
944
+ });
945
+ }
946
+ async function buildDrawings(projectRoot, drawingsDir) {
947
+ return buildFlatSection(projectRoot, drawingsDir, "desenho", (slug, data, file) => {
948
+ const { title, type, date } = extractDrawingFrontmatterFields(data);
949
+ return { slug, title: title ?? slug, type, date, file };
950
+ });
951
+ }
952
+
953
+ // ../../internal/specs-server/src/headings.ts
954
+ import GithubSlugger from "github-slugger";
955
+ function extractHeadings(body) {
956
+ const slugger = new GithubSlugger();
957
+ const lines = body.split(/\r?\n/);
958
+ const out = [];
959
+ let inFence = false;
960
+ for (const line of lines) {
961
+ const trimmed = line.trimStart();
962
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
963
+ inFence = !inFence;
964
+ continue;
965
+ }
966
+ if (inFence) continue;
967
+ const match = /^(#{1,6})\s+(.+)$/.exec(line);
968
+ if (!match) continue;
969
+ const level = match[1].length;
970
+ const text2 = match[2].replace(/\s+#+\s*$/, "").trim();
971
+ const id = slugger.slug(text2);
972
+ out.push({ id, text: text2, level });
973
+ }
974
+ return out;
975
+ }
976
+
977
+ // ../../internal/specs-server/src/run-agent.ts
978
+ import { spawn as spawn2 } from "child_process";
979
+ import path9 from "path";
980
+ import { readFile as readFile6 } from "fs/promises";
981
+ function resolveCommand(config, cli) {
982
+ const key = cli && config.agent.commands?.[cli] ? cli : config.agent.cli;
983
+ return config.agent.commands?.[key] ?? config.agent.command;
984
+ }
985
+ function injectModel(template, model, quote) {
986
+ const flag = model ? `--model ${quote(model)}` : "";
987
+ if (template.includes("{model}")) return template.replaceAll("{model}", flag);
988
+ if (!flag) return template;
989
+ return template.replace(/^(\s*\S+)/, `$1 ${flag}`);
990
+ }
991
+ var EFFORT_MODE = {
992
+ claude: "flag",
993
+ cursor: "model-param"
994
+ };
995
+ function effortMode(cli) {
996
+ return EFFORT_MODE[cli ?? "claude"] ?? "flag";
997
+ }
998
+ function withEffortParam(model, effort) {
999
+ if (/\[.*effort=/.test(model)) return model;
1000
+ const match = /^([^[]+)\[(.+)\]$/.exec(model);
1001
+ if (match) return `${match[1]}[${match[2]},effort=${effort}]`;
1002
+ return `${model}[effort=${effort}]`;
1003
+ }
1004
+ function injectEffort(template, effort, quote) {
1005
+ const flag = effort ? `--effort ${quote(effort)}` : "";
1006
+ if (template.includes("{effort}")) {
1007
+ return flag ? template.replaceAll("{effort}", flag) : template.replace(/ ?\{effort\}/g, "");
1008
+ }
1009
+ if (!flag) return template;
1010
+ return template.replace(/^(\s*\S+)/, `$1 ${flag}`);
1011
+ }
1012
+ function buildCommand(config, cli, model, effort, quote) {
1013
+ const mode = effortMode(cli);
1014
+ const effectiveModel = mode === "model-param" && model && effort ? withEffortParam(model, effort) : model;
1015
+ const withModel = injectModel(resolveCommand(config, cli), effectiveModel, quote);
1016
+ return mode === "flag" ? injectEffort(withModel, effort, quote) : withModel;
1017
+ }
1018
+ function shellQuote(value) {
1019
+ return `'${value.replaceAll("'", String.raw`'\''`)}'`;
1020
+ }
1021
+ async function readFeatureTitle(metaPath, fallback) {
1022
+ try {
1023
+ const raw = await readFile6(metaPath, "utf8");
1024
+ const parsed = JSON.parse(raw);
1025
+ if (typeof parsed.title === "string" && parsed.title.trim().length > 0) {
1026
+ return parsed.title.trim();
1027
+ }
1028
+ } catch {
1029
+ }
1030
+ return fallback;
1031
+ }
1032
+ function resolvePrompt(template, ctx) {
1033
+ return template.replaceAll("{feature}", ctx.feature).replaceAll("{task}", ctx.task ?? "").replaceAll("{featureTitle}", ctx.featureTitle).replaceAll("{featurePath}", ctx.featurePath);
1034
+ }
1035
+ function buildTerminalCommand(terminal, cwd, shellCommand) {
1036
+ switch (terminal) {
1037
+ case "osascript": {
1038
+ const quotedCwd = shellQuote(cwd);
1039
+ const fullShell = `cd ${quotedCwd} && ${shellCommand}`;
1040
+ const appleScript = [
1041
+ 'tell application "Terminal"',
1042
+ " activate",
1043
+ ` do script ${JSON.stringify(fullShell)}`,
1044
+ "end tell"
1045
+ ].join("\n");
1046
+ return { file: "osascript", args: ["-e", appleScript] };
1047
+ }
1048
+ case "gnome-terminal":
1049
+ return {
1050
+ file: "gnome-terminal",
1051
+ args: ["--", "bash", "-c", `cd ${shellQuote(cwd)} && ${shellCommand}; exec bash`]
1052
+ };
1053
+ case "kitty":
1054
+ return {
1055
+ file: "kitty",
1056
+ args: ["--hold", "bash", "-c", `cd ${shellQuote(cwd)} && ${shellCommand}`]
1057
+ };
1058
+ case "wezterm":
1059
+ return {
1060
+ file: "wezterm",
1061
+ args: ["start", "--cwd", cwd, "--", "bash", "-c", shellCommand]
1062
+ };
1063
+ case "none":
1064
+ return null;
1065
+ }
1066
+ }
1067
+ function spawnInTerminal(config, projectRoot, shellCommand) {
1068
+ const terminal = config.agent.openTerminal;
1069
+ if (terminal === "none") {
1070
+ return {
1071
+ ok: false,
1072
+ status: 501,
1073
+ error: "terminal_disabled",
1074
+ hint: "Ajuste agent.openTerminal no .specs/config.json"
1075
+ };
1076
+ }
1077
+ const cwd = path9.resolve(projectRoot, config.agent.cwd ?? ".");
1078
+ const termCmd = buildTerminalCommand(terminal, cwd, shellCommand);
1079
+ if (!termCmd) {
1080
+ return {
1081
+ ok: false,
1082
+ status: 501,
1083
+ error: "terminal_disabled",
1084
+ hint: "openTerminal n\xE3o reconhecido."
1085
+ };
1086
+ }
1087
+ try {
1088
+ const child = spawn2(termCmd.file, termCmd.args, {
1089
+ detached: true,
1090
+ stdio: "ignore"
1091
+ });
1092
+ child.on("error", (err) => {
1093
+ console.error(`[run-agent] spawn error: ${err.message}`);
1094
+ });
1095
+ child.unref();
1096
+ return { ok: true };
1097
+ } catch (err) {
1098
+ return {
1099
+ ok: false,
1100
+ status: 500,
1101
+ error: "spawn_failed",
1102
+ detail: err.message
1103
+ };
1104
+ }
1105
+ }
1106
+ function parseInlineCommand(baseCommand, agentName, prompt) {
1107
+ const tokens = baseCommand.match(/"[^"]*"|'[^']*'|\S+/g);
1108
+ if (!tokens || tokens.length === 0) return null;
1109
+ const cleaned = tokens.map((t) => {
1110
+ const unquoted = t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'") ? t.slice(1, -1) : t;
1111
+ return unquoted.includes("feature-runner") ? unquoted.replaceAll("feature-runner", agentName) : unquoted;
1112
+ });
1113
+ const promptIdx = cleaned.findIndex((t) => t.includes("{prompt}"));
1114
+ if (promptIdx === -1) {
1115
+ cleaned.push(prompt);
1116
+ } else {
1117
+ cleaned[promptIdx] = (cleaned[promptIdx] ?? "").replaceAll("{prompt}", prompt);
1118
+ }
1119
+ const [file, ...args] = cleaned;
1120
+ if (!file) return null;
1121
+ return { file, args };
1122
+ }
1123
+ async function buildAgentInlineCommand(input, config, projectRoot) {
1124
+ const { scope, feature, task, cli, model, effort } = input;
1125
+ const featuresDir = config.featuresDir ?? "features";
1126
+ const featureMetaPath = path9.join(projectRoot, featuresDir, feature, "meta.json");
1127
+ const featureTitle = await readFeatureTitle(featureMetaPath, feature);
1128
+ const promptTemplate = config.agent.scopes[scope] ?? "";
1129
+ const prompt = resolvePrompt(promptTemplate, {
1130
+ feature,
1131
+ task,
1132
+ featureTitle,
1133
+ featurePath: `${featuresDir}/${feature}/`
1134
+ });
1135
+ const command = buildCommand(config, cli, model, effort, (s) => s);
1136
+ const parsed = parseInlineCommand(command, "feature-runner", prompt);
1137
+ if (!parsed) return null;
1138
+ const cwd = path9.resolve(projectRoot, config.agent.cwd ?? ".");
1139
+ const label = scope === "task" && task ? `${featureTitle} \u2014 ${task}` : `${featureTitle} (${scope})`;
1140
+ return { file: parsed.file, args: parsed.args, cwd, label };
1141
+ }
1142
+ function buildNamedAgentInlineCommand(agentName, userPrompt, config, projectRoot, label, cli, model, effort) {
1143
+ const command = buildCommand(config, cli, model, effort, (s) => s);
1144
+ const parsed = parseInlineCommand(command, agentName, userPrompt);
1145
+ if (!parsed) return null;
1146
+ const cwd = path9.resolve(projectRoot, config.agent.cwd ?? ".");
1147
+ return { file: parsed.file, args: parsed.args, cwd, label };
1148
+ }
1149
+ function stripAgentFlag(template) {
1150
+ return template.replace(/\s--agent(?:=|\s+)[^\s"']+/g, "").replace(/Use the feature-runner subagent\.\s*/g, "").replace(/\s+/g, " ").trim();
1151
+ }
1152
+ function buildDesignInlineCommand(prompt, config, projectRoot, label, cli, model, effort) {
1153
+ const command = stripAgentFlag(buildCommand(config, cli, model, effort, (s) => s));
1154
+ const parsed = parseInlineCommand(command, "design", prompt);
1155
+ if (!parsed) return null;
1156
+ const cwd = path9.resolve(projectRoot, config.agent.cwd ?? ".");
1157
+ return { file: parsed.file, args: parsed.args, cwd, label };
1158
+ }
1159
+ function runDesign(prompt, config, projectRoot, cli, model, effort) {
1160
+ const command = stripAgentFlag(buildCommand(config, cli, model, effort, shellQuote));
1161
+ const shellCommand = command.replaceAll("{prompt}", shellQuote(prompt));
1162
+ return spawnInTerminal(config, projectRoot, shellCommand);
1163
+ }
1164
+ async function runAgent(input, config, projectRoot) {
1165
+ const { scope, feature, task, cli, model, effort } = input;
1166
+ const featuresDir = config.featuresDir ?? "features";
1167
+ const featureMetaPath = path9.join(projectRoot, featuresDir, feature, "meta.json");
1168
+ const featureTitle = await readFeatureTitle(featureMetaPath, feature);
1169
+ const scopeKey = scope;
1170
+ const promptTemplate = config.agent.scopes[scopeKey] ?? "";
1171
+ const prompt = resolvePrompt(promptTemplate, {
1172
+ feature,
1173
+ task,
1174
+ featureTitle,
1175
+ featurePath: `${featuresDir}/${feature}/`
1176
+ });
1177
+ const commandTemplate = buildCommand(config, cli, model, effort, shellQuote);
1178
+ const shellCommand = commandTemplate.replaceAll("{prompt}", shellQuote(prompt));
1179
+ return spawnInTerminal(config, projectRoot, shellCommand);
1180
+ }
1181
+ function runRefinement(userPrompt, config, projectRoot, cli, model, effort) {
1182
+ return runNamedAgent("refinement", userPrompt, config, projectRoot, cli, model, effort);
1183
+ }
1184
+ function runDrawingAgent(userPrompt, config, projectRoot, cli, model, effort) {
1185
+ return runNamedAgent("drawing-agent", userPrompt, config, projectRoot, cli, model, effort);
1186
+ }
1187
+ function runRefinementRunner(userPrompt, config, projectRoot, cli, model, effort) {
1188
+ return runNamedAgent("refinement-runner", userPrompt, config, projectRoot, cli, model, effort);
1189
+ }
1190
+ function runDiscoveryAgent(userPrompt, config, projectRoot, cli, model, effort) {
1191
+ return runNamedAgent("discovery-agent", userPrompt, config, projectRoot, cli, model, effort);
1192
+ }
1193
+ function runNamedAgent(agentName, userPrompt, config, projectRoot, cli, model, effort) {
1194
+ const baseCommand = buildCommand(config, cli, model, effort, shellQuote);
1195
+ const command = baseCommand.includes("feature-runner") ? baseCommand.replaceAll("feature-runner", agentName) : `claude --agent ${agentName} "{prompt}"`;
1196
+ const shellCommand = command.replaceAll("{prompt}", shellQuote(userPrompt));
1197
+ return spawnInTerminal(config, projectRoot, shellCommand);
1198
+ }
1199
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
1200
+ var MODEL_LINE_RE = /^([A-Za-z0-9][A-Za-z0-9._\-/:]*)\s+-\s+(.+)$/;
1201
+ var MODEL_BARE_RE = /^[A-Za-z0-9][A-Za-z0-9._\-/:]*$/;
1202
+ function parseModelsOutput(output) {
1203
+ const seen = /* @__PURE__ */ new Set();
1204
+ const models = [];
1205
+ for (const raw of output.replace(ANSI_RE, "").split("\n")) {
1206
+ const line = raw.trim().replace(/^[-*•]\s+/, "");
1207
+ if (!line) continue;
1208
+ const matched = line.match(MODEL_LINE_RE);
1209
+ let id;
1210
+ let label;
1211
+ if (matched) {
1212
+ id = matched[1];
1213
+ label = (matched[2] ?? "").trim();
1214
+ } else if (MODEL_BARE_RE.test(line)) {
1215
+ id = line;
1216
+ label = line;
1217
+ }
1218
+ if (!id || seen.has(id)) continue;
1219
+ seen.add(id);
1220
+ models.push({ id, label: label ?? id });
1221
+ if (models.length >= 200) break;
1222
+ }
1223
+ return models;
1224
+ }
1225
+ function runForStdout(file, args, cwd, timeoutMs) {
1226
+ return new Promise((resolve, reject) => {
1227
+ let stdout = "";
1228
+ let stderr = "";
1229
+ const child = spawn2(file, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1230
+ const timer = setTimeout(() => {
1231
+ child.kill("SIGKILL");
1232
+ reject(new Error("timeout"));
1233
+ }, timeoutMs);
1234
+ child.stdout.on("data", (d) => {
1235
+ stdout += d.toString();
1236
+ });
1237
+ child.stderr.on("data", (d) => {
1238
+ stderr += d.toString();
1239
+ });
1240
+ child.on("error", (err) => {
1241
+ clearTimeout(timer);
1242
+ reject(err);
1243
+ });
1244
+ child.on("close", (code) => {
1245
+ clearTimeout(timer);
1246
+ if (code === 0) resolve(stdout);
1247
+ else reject(new Error(stderr.trim() || `exit ${code}`));
1248
+ });
1249
+ });
1250
+ }
1251
+ async function listAgentModels(config, projectRoot, cli) {
1252
+ const cwd = path9.resolve(projectRoot, config.agent.cwd ?? ".");
1253
+ const staticList = (config.agent.models?.[cli] ?? []).map((id) => ({ id, label: id }));
1254
+ const command = config.agent.modelsCommand?.[cli];
1255
+ if (command) {
1256
+ const tokens = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
1257
+ const cleaned = tokens.map(
1258
+ (t) => t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'") ? t.slice(1, -1) : t
1259
+ );
1260
+ const [file, ...args] = cleaned;
1261
+ if (file) {
1262
+ try {
1263
+ const out = await runForStdout(file, args, cwd, 2e4);
1264
+ const models = parseModelsOutput(out);
1265
+ if (models.length > 0) return { models, source: "dynamic" };
1266
+ } catch (err) {
1267
+ if (staticList.length > 0) {
1268
+ return { models: staticList, source: "static", error: err.message };
1269
+ }
1270
+ return { models: [], source: "none", error: err.message };
1271
+ }
1272
+ }
1273
+ }
1274
+ if (staticList.length > 0) return { models: staticList, source: "static" };
1275
+ return { models: [], source: "none" };
1276
+ }
1277
+
1278
+ // ../../internal/specs-server/src/review.ts
1279
+ import { execFile } from "child_process";
1280
+ import path10 from "path";
1281
+ import { promisify } from "util";
1282
+ var run = promisify(execFile);
1283
+ var MAX_DIFF_LINES = 1500;
1284
+ var GENERATED_PATTERNS = [
1285
+ /(^|\/)pnpm-lock\.yaml$/,
1286
+ /(^|\/)package-lock\.json$/,
1287
+ /(^|\/)yarn\.lock$/,
1288
+ /\.snap$/,
1289
+ /(^|\/)dist\//,
1290
+ /(^|\/)node_modules\//,
1291
+ /(^|\/)migrations?\/.+\.sql$/
1292
+ ];
1293
+ function isGenerated(file) {
1294
+ return GENERATED_PATTERNS.some((re) => re.test(file));
1295
+ }
1296
+ function statusFromCode(code) {
1297
+ if (code === "??") return "untracked";
1298
+ const flags = code.replace(/\s/g, "");
1299
+ if (flags.includes("R")) return "renamed";
1300
+ if (flags.includes("D")) return "deleted";
1301
+ if (flags.includes("A")) return "added";
1302
+ return "modified";
1303
+ }
1304
+ async function git(cwd, args) {
1305
+ try {
1306
+ const { stdout } = await run("git", args, { cwd, maxBuffer: 64 * 1024 * 1024 });
1307
+ return stdout;
1308
+ } catch (err) {
1309
+ const withStdout = err;
1310
+ if (typeof withStdout.stdout === "string" && withStdout.stdout.length > 0) {
1311
+ return withStdout.stdout;
1312
+ }
1313
+ throw err;
1314
+ }
1315
+ }
1316
+ function parseStatus(raw) {
1317
+ const parts = raw.split("\0").filter((p2) => p2.length > 0);
1318
+ const out = [];
1319
+ for (let i = 0; i < parts.length; i += 1) {
1320
+ const entry = parts[i];
1321
+ const code = entry.slice(0, 2);
1322
+ const file = entry.slice(3);
1323
+ if (code.includes("R") || code.includes("C")) {
1324
+ const previous = parts[i + 1];
1325
+ i += 1;
1326
+ out.push({ code, file, previous });
1327
+ continue;
1328
+ }
1329
+ out.push({ code, file });
1330
+ }
1331
+ return out;
1332
+ }
1333
+ function parseNumstat(raw) {
1334
+ const out = /* @__PURE__ */ new Map();
1335
+ const parts = raw.split("\0").filter((p2) => p2.length > 0);
1336
+ for (let i = 0; i < parts.length; i += 1) {
1337
+ const entry = parts[i];
1338
+ const match = /^(\d+|-)\t(\d+|-)\t?(.*)$/.exec(entry);
1339
+ if (!match) continue;
1340
+ const additions = match[1] === "-" ? 0 : Number(match[1]);
1341
+ const deletions = match[2] === "-" ? 0 : Number(match[2]);
1342
+ let file = match[3] ?? "";
1343
+ if (!file) {
1344
+ i += 1;
1345
+ file = parts[i + 1] ?? parts[i] ?? "";
1346
+ i += 1;
1347
+ }
1348
+ if (file) out.set(file, { additions, deletions });
1349
+ }
1350
+ return out;
1351
+ }
1352
+ async function countUntracked(projectRoot, file) {
1353
+ try {
1354
+ const { readFile: readFile10 } = await import("fs/promises");
1355
+ const content = await readFile10(path10.resolve(projectRoot, file), "utf8");
1356
+ if (content === "") return 0;
1357
+ return content.split("\n").length;
1358
+ } catch {
1359
+ return 0;
1360
+ }
1361
+ }
1362
+ function truncate(diff) {
1363
+ const lines = diff.split("\n");
1364
+ if (lines.length <= MAX_DIFF_LINES) return { diff, truncated: false };
1365
+ return { diff: lines.slice(0, MAX_DIFF_LINES).join("\n"), truncated: true };
1366
+ }
1367
+ async function resolveUpstream(projectRoot, branch) {
1368
+ const tracked = await git(projectRoot, [
1369
+ "rev-parse",
1370
+ "--abbrev-ref",
1371
+ "--symbolic-full-name",
1372
+ "@{u}"
1373
+ ]).then((out) => out.trim()).catch(() => "");
1374
+ if (tracked) return tracked;
1375
+ const fallback = `origin/${branch}`;
1376
+ const exists2 = await git(projectRoot, ["rev-parse", "--verify", "--quiet", fallback]).then((out) => out.trim().length > 0).catch(() => false);
1377
+ return exists2 ? fallback : null;
1378
+ }
1379
+ function parseShowNumstat(raw) {
1380
+ const files = [];
1381
+ for (const line of raw.split("\n")) {
1382
+ const match = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line.trim());
1383
+ if (!match) continue;
1384
+ const file = match[3];
1385
+ files.push({
1386
+ path: file,
1387
+ status: "modified",
1388
+ additions: match[1] === "-" ? 0 : Number(match[1]),
1389
+ deletions: match[2] === "-" ? 0 : Number(match[2]),
1390
+ generated: isGenerated(file),
1391
+ binary: match[1] === "-" && match[2] === "-"
1392
+ });
1393
+ }
1394
+ return files;
1395
+ }
1396
+ async function collectLocalCommits(projectRoot, upstream) {
1397
+ if (!upstream) return [];
1398
+ const log2 = await git(projectRoot, [
1399
+ "log",
1400
+ "--reverse",
1401
+ "--format=%H%x1f%h%x1f%s%x1f%aI",
1402
+ `${upstream}..HEAD`
1403
+ ]).catch(() => "");
1404
+ const commits = [];
1405
+ for (const line of log2.split("\n")) {
1406
+ if (!line.trim()) continue;
1407
+ const [hash, shortHash, subject, date] = line.split("");
1408
+ if (!hash) continue;
1409
+ const files = parseShowNumstat(
1410
+ await git(projectRoot, ["show", "--numstat", "--format=", hash]).catch(() => "")
1411
+ );
1412
+ commits.push({
1413
+ hash,
1414
+ shortHash: shortHash ?? hash.slice(0, 7),
1415
+ subject: subject ?? "",
1416
+ date: date ?? "",
1417
+ files,
1418
+ additions: files.reduce((sum, f) => sum + f.additions, 0),
1419
+ deletions: files.reduce((sum, f) => sum + f.deletions, 0)
1420
+ });
1421
+ }
1422
+ return commits;
1423
+ }
1424
+ async function diffForFile(projectRoot, file, status) {
1425
+ if (status === "untracked") {
1426
+ return git(projectRoot, ["diff", "--no-index", "--unified=3", "--", "/dev/null", file]);
1427
+ }
1428
+ return git(projectRoot, ["diff", "HEAD", "--unified=3", "--", file]);
1429
+ }
1430
+ async function collectChanges(projectRoot) {
1431
+ const branch = (await git(projectRoot, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
1432
+ const upstream = await resolveUpstream(projectRoot, branch);
1433
+ const commits = await collectLocalCommits(projectRoot, upstream);
1434
+ const status = parseStatus(
1435
+ await git(projectRoot, ["status", "--porcelain=v1", "--untracked-files=all", "-z"])
1436
+ );
1437
+ const numstat = parseNumstat(await git(projectRoot, ["diff", "--numstat", "-z", "HEAD"]));
1438
+ const files = [];
1439
+ for (const entry of status) {
1440
+ const fileStatus = statusFromCode(entry.code);
1441
+ const counted = numstat.get(entry.file);
1442
+ const additions = counted?.additions ?? (fileStatus === "untracked" ? await countUntracked(projectRoot, entry.file) : 0);
1443
+ files.push({
1444
+ path: entry.file,
1445
+ previousPath: entry.previous,
1446
+ status: fileStatus,
1447
+ additions,
1448
+ deletions: counted?.deletions ?? 0,
1449
+ generated: isGenerated(entry.file),
1450
+ binary: false
1451
+ });
1452
+ }
1453
+ files.sort((a, b) => {
1454
+ if (a.generated !== b.generated) return a.generated ? 1 : -1;
1455
+ return a.path.localeCompare(b.path);
1456
+ });
1457
+ return {
1458
+ branch,
1459
+ upstream,
1460
+ commits,
1461
+ files,
1462
+ totals: {
1463
+ files: files.length,
1464
+ additions: files.reduce((sum, f) => sum + f.additions, 0),
1465
+ deletions: files.reduce((sum, f) => sum + f.deletions, 0)
1466
+ }
1467
+ };
1468
+ }
1469
+ async function fileDiff(projectRoot, file, commit) {
1470
+ const absolute = path10.resolve(projectRoot, file);
1471
+ const relative = path10.relative(projectRoot, absolute);
1472
+ if (relative.startsWith("..") || path10.isAbsolute(relative)) {
1473
+ throw new Error("invalid_path");
1474
+ }
1475
+ if (commit) {
1476
+ if (!/^[0-9a-f]{7,40}$/i.test(commit)) throw new Error("invalid_commit");
1477
+ const raw2 = await git(projectRoot, [
1478
+ "show",
1479
+ "--format=",
1480
+ "--unified=3",
1481
+ commit,
1482
+ "--",
1483
+ file
1484
+ ]).catch(() => "");
1485
+ const { diff: diff2, truncated: truncated2 } = truncate(raw2);
1486
+ return { path: file, binary: raw2.includes("Binary files"), truncated: truncated2, diff: diff2 };
1487
+ }
1488
+ const tracked = await git(projectRoot, ["ls-files", "--error-unmatch", "--", file]).then(() => true).catch(() => false);
1489
+ const raw = await diffForFile(projectRoot, file, tracked ? "modified" : "untracked").catch(
1490
+ () => ""
1491
+ );
1492
+ const { diff, truncated } = truncate(raw);
1493
+ return { path: file, binary: raw.includes("Binary files"), truncated, diff };
1494
+ }
1495
+ async function openInEditor(projectRoot, template, file, line) {
1496
+ const absolute = path10.resolve(projectRoot, file);
1497
+ const relative = path10.relative(projectRoot, absolute);
1498
+ if (relative.startsWith("..") || path10.isAbsolute(relative)) {
1499
+ return { ok: false, error: "invalid_path" };
1500
+ }
1501
+ const filled = template.replaceAll("{path}", absolute).replaceAll("{line}", String(line ?? 1)).trim();
1502
+ const [bin, ...args] = filled.split(/\s+/);
1503
+ if (!bin) return { ok: false, error: "spawn_failed", detail: "comando vazio" };
1504
+ try {
1505
+ await run(bin, args, { cwd: projectRoot });
1506
+ return { ok: true, command: filled };
1507
+ } catch (err) {
1508
+ return { ok: false, error: "spawn_failed", detail: err.message };
1509
+ }
1510
+ }
1511
+
1512
+ // ../../internal/specs-server/src/prototype.ts
1513
+ import path11 from "path";
1514
+ import { spawn as spawn3 } from "child_process";
1515
+ import { readFile as readFile7 } from "fs/promises";
1516
+ async function resolvePrototype(projectRoot, config, feature) {
1517
+ let resolved = config.prototype ? { ...config.prototype, source: "config" } : null;
1518
+ if (feature) {
1519
+ const metaPath = path11.resolve(projectRoot, config.featuresDir, feature, "meta.json");
1520
+ try {
1521
+ assertInsideProject(metaPath, projectRoot);
1522
+ if (await fileExists(metaPath)) {
1523
+ const meta = JSON.parse(await readFile7(metaPath, "utf8"));
1524
+ const override = normalizePrototype(meta.prototype);
1525
+ if (override) resolved = { ...override, source: "feature", feature };
1526
+ }
1527
+ } catch {
1528
+ }
1529
+ }
1530
+ if (!resolved) return null;
1531
+ resolved = await withSnapshot(projectRoot, resolved);
1532
+ if (resolved.tool === "pencil" && resolved.file) {
1533
+ const filePath = path11.resolve(projectRoot, resolved.file);
1534
+ try {
1535
+ assertInsideProject(filePath, projectRoot);
1536
+ return { ...resolved, filePath, fileExists: await fileExists(filePath) };
1537
+ } catch {
1538
+ return { ...resolved, fileExists: false };
1539
+ }
1540
+ }
1541
+ return resolved;
1542
+ }
1543
+ async function withSnapshot(projectRoot, prototype) {
1544
+ const candidate = prototype.snapshot ?? (prototype.designDir ? `${prototype.designDir.replace(/\/$/, "")}/canvas.html` : void 0);
1545
+ if (!candidate) return prototype;
1546
+ try {
1547
+ const abs = path11.resolve(projectRoot, candidate);
1548
+ assertInsideProject(abs, projectRoot);
1549
+ return { ...prototype, snapshotPath: candidate, snapshotExists: await fileExists(abs) };
1550
+ } catch {
1551
+ return { ...prototype, snapshotPath: candidate, snapshotExists: false };
1552
+ }
1553
+ }
1554
+ function snapshotAbsolutePath(projectRoot, prototype) {
1555
+ if (!prototype.snapshotPath || !prototype.snapshotExists) return null;
1556
+ try {
1557
+ const abs = path11.resolve(projectRoot, prototype.snapshotPath);
1558
+ assertInsideProject(abs, projectRoot);
1559
+ return abs;
1560
+ } catch {
1561
+ return null;
1562
+ }
1563
+ }
1564
+ function buildSnapshotPrompt(prototype) {
1565
+ const target = prototype.snapshotPath ?? "design/prototipo/canvas.html";
1566
+ if (prototype.tool === "claude-design") {
1567
+ return `Atualize o snapshot local do prot\xF3tipo: leia o canvas ${prototype.url} com a tool Artifact (action "read") e grave o HTML completo devolvido em ${target} (crie o diret\xF3rio se n\xE3o existir, e sobrescreva o arquivo se j\xE1 houver um). N\xE3o altere o canvas nem outros arquivos do reposit\xF3rio; ao terminar, diga o tamanho do arquivo gravado.`;
1568
+ }
1569
+ return `Atualize o snapshot local do prot\xF3tipo Pencil ${prototype.file}: exporte a vis\xE3o atual do arquivo e grave-a em ${target}. Se o Pencil MCP n\xE3o permitir exportar, diga isso em vez de improvisar, e informe qual export manual o usu\xE1rio precisa fazer.`;
1570
+ }
1571
+ function buildDesignPrompt(prototype, request, ctx = {}, intent = "change") {
1572
+ if (intent === "snapshot") return buildSnapshotPrompt(prototype);
1573
+ const context = [];
1574
+ if (ctx.featureTitle || ctx.feature) {
1575
+ context.push(`Feature: ${ctx.featureTitle ?? ctx.feature}`);
1576
+ }
1577
+ if (ctx.task) context.push(`Task: ${ctx.task}`);
1578
+ if (ctx.specPath) context.push(`Spec: ${ctx.specPath}`);
1579
+ const suffix = context.length > 0 ? `
1580
+
1581
+ Contexto \u2014 ${context.join(" \xB7 ")}.` : "";
1582
+ if (prototype.tool === "claude-design") {
1583
+ const target = [
1584
+ prototype.title ? `"${prototype.title}"` : null,
1585
+ prototype.url ? `(${prototype.url})` : null
1586
+ ].filter(Boolean).join(" ");
1587
+ const dir = prototype.designDir ? ` As notas do prot\xF3tipo est\xE3o em ${prototype.designDir}/README.md \u2014 atualize-as se a mudan\xE7a alterar as decis\xF5es descritas l\xE1.` : "";
1588
+ return `/design Atualize o prot\xF3tipo ${target} do canvas existente: ${request}. Mantenha os artboards j\xE1 publicados e o sistema de tokens atual; re-seed apenas o que a mudan\xE7a exigir.${dir}${suffix}`;
1589
+ }
1590
+ const file = prototype.file ?? "(arquivo .pen n\xE3o declarado)";
1591
+ return `Altere o prot\xF3tipo Pencil ${file}: ${request}. Use as tools do Pencil MCP (o .pen \xE9 encriptado \u2014 nunca leia com Read/Grep): confirme o estado do editor, localize os n\xF3s alvo, aplique a mudan\xE7a e devolva os node IDs afetados com o antes/depois de cada um.${suffix}`;
1592
+ }
1593
+ function openPrototype(projectRoot, prototype) {
1594
+ if (prototype.tool !== "pencil" || !prototype.filePath) {
1595
+ return { ok: false, status: 400, error: "not_openable" };
1596
+ }
1597
+ if (prototype.fileExists === false) {
1598
+ return { ok: false, status: 404, error: "prototype_file_not_found", detail: prototype.file };
1599
+ }
1600
+ const template = prototype.openCommand ?? DEFAULT_PROTOTYPE_OPEN_COMMAND[prototype.tool] ?? "";
1601
+ const command = template.replaceAll("{file}", prototype.filePath).replaceAll("{url}", prototype.url ?? "");
1602
+ const tokens = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
1603
+ const cleaned = tokens.map(
1604
+ (t) => t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'") ? t.slice(1, -1) : t
1605
+ );
1606
+ const [file, ...args] = cleaned;
1607
+ if (!file) return { ok: false, status: 400, error: "invalid_open_command" };
1608
+ try {
1609
+ const child = spawn3(file, args, { cwd: projectRoot, detached: true, stdio: "ignore" });
1610
+ child.on("error", (err) => {
1611
+ console.error(`[prototype] open error: ${err.message}`);
1612
+ });
1613
+ child.unref();
1614
+ return { ok: true, command };
1615
+ } catch (err) {
1616
+ return { ok: false, status: 500, error: "spawn_failed", detail: err.message };
1617
+ }
1618
+ }
1619
+
1620
+ // ../../internal/specs-server/src/job-manager.ts
1621
+ import { EventEmitter } from "events";
1622
+ import * as pty from "node-pty";
1623
+ import { nanoid } from "nanoid";
1624
+
1625
+ // ../../internal/specs-server/src/job-store.ts
1626
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
1627
+ import path12 from "path";
1628
+ var MAX_PERSISTED_JOBS = 30;
1629
+ var MAX_PERSISTED_BUFFER = 64 * 1024;
1630
+ function jobStorePath(projectRoot) {
1631
+ return path12.join(projectRoot, ".specs", ".jobs.json");
1632
+ }
1633
+ function isJobStatus(v) {
1634
+ return v === "running" || v === "needs-input" || v === "done" || v === "failed" || v === "cancelled";
1635
+ }
1636
+ function isJobKind(v) {
1637
+ return v === "agent" || v === "refinement" || v === "discovery-agent" || v === "drawing-agent" || v === "design";
1638
+ }
1639
+ function reviveJob(raw) {
1640
+ if (typeof raw !== "object" || raw === null) return null;
1641
+ const r = raw;
1642
+ if (typeof r.id !== "string" || r.id.length === 0) return null;
1643
+ if (typeof r.label !== "string") return null;
1644
+ if (!isJobKind(r.kind)) return null;
1645
+ if (typeof r.startedAt !== "number" || !Number.isFinite(r.startedAt)) return null;
1646
+ const status = isJobStatus(r.status) ? r.status : "cancelled";
1647
+ const wasActive = status === "running" || status === "needs-input";
1648
+ const buffer = typeof r.buffer === "string" ? r.buffer.slice(-MAX_PERSISTED_BUFFER) : "";
1649
+ return {
1650
+ id: r.id,
1651
+ kind: r.kind,
1652
+ label: r.label,
1653
+ feature: typeof r.feature === "string" ? r.feature : void 0,
1654
+ task: typeof r.task === "string" ? r.task : void 0,
1655
+ status: wasActive ? "cancelled" : status,
1656
+ pid: typeof r.pid === "number" ? r.pid : 0,
1657
+ startedAt: r.startedAt,
1658
+ // Um job que estava vivo não tem fim registrado — o servidor caiu antes.
1659
+ // Sem `endedAt`, a duração exibida cresceria para sempre.
1660
+ endedAt: typeof r.endedAt === "number" ? r.endedAt : wasActive ? typeof r.lastSeenAt === "number" ? r.lastSeenAt : r.startedAt : void 0,
1661
+ exitCode: typeof r.exitCode === "number" ? r.exitCode : void 0,
1662
+ bufferBytes: buffer.length,
1663
+ needsInputHint: typeof r.needsInputHint === "string" ? r.needsInputHint : void 0,
1664
+ buffer,
1665
+ restored: true
1666
+ };
1667
+ }
1668
+ function loadJobHistory(projectRoot) {
1669
+ let parsed;
1670
+ try {
1671
+ parsed = JSON.parse(readFileSync(jobStorePath(projectRoot), "utf8"));
1672
+ } catch {
1673
+ return [];
1674
+ }
1675
+ if (typeof parsed !== "object" || parsed === null) return [];
1676
+ const file = parsed;
1677
+ if (file.version !== 1 || !Array.isArray(file.jobs)) return [];
1678
+ const out = [];
1679
+ for (const raw of file.jobs) {
1680
+ const job = reviveJob(raw);
1681
+ if (job) out.push(job);
1682
+ }
1683
+ return out.sort((a, b) => b.startedAt - a.startedAt).slice(0, MAX_PERSISTED_JOBS);
1684
+ }
1685
+ function saveJobHistory(projectRoot, jobs2) {
1686
+ const trimmed = jobs2.slice().sort((a, b) => b.startedAt - a.startedAt).slice(0, MAX_PERSISTED_JOBS).map((job) => ({ ...job, buffer: job.buffer.slice(-MAX_PERSISTED_BUFFER) }));
1687
+ const finalPath = jobStorePath(projectRoot);
1688
+ const tmpPath = `${finalPath}.tmp`;
1689
+ const payload = { version: 1, jobs: trimmed };
1690
+ try {
1691
+ mkdirSync(path12.dirname(finalPath), { recursive: true });
1692
+ writeFileSync(tmpPath, `${JSON.stringify(payload)}
1693
+ `, "utf8");
1694
+ renameSync(tmpPath, finalPath);
1695
+ } catch (err) {
1696
+ console.error(`[job-store] falha ao gravar hist\xF3rico: ${err.message}`);
1697
+ }
1698
+ }
1699
+
1700
+ // ../../internal/specs-server/src/job-manager.ts
1701
+ var OSC_REGEX = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g;
1702
+ var CSI_REGEX = /\x1b\[[0-9;?]*[ -\/]*[@-~]/g;
1703
+ var SIMPLE_ESC_REGEX = /\x1b[()#][0-9A-Za-z]|\x1b[0-9A-Za-z=><]/g;
1704
+ var CONTROL_REGEX = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
1705
+ var HINT_MAX_CHARS = 160;
1706
+ var DEFAULT_COLS = 120;
1707
+ var DEFAULT_ROWS = 30;
1708
+ var INACTIVITY_MS = 2500;
1709
+ var DEBOUNCE_MS = 200;
1710
+ var STOP_SIGTERM_DELAY_MS = 2e3;
1711
+ var STOP_SIGKILL_DELAY_MS = 2e3;
1712
+ var INPUT_GRACE_MS = 3e3;
1713
+ var PERSIST_DEBOUNCE_MS = 800;
1714
+ function stripAnsi(s) {
1715
+ return s.replace(OSC_REGEX, "").replace(CSI_REGEX, "").replace(SIMPLE_ESC_REGEX, "").replace(CONTROL_REGEX, "");
1716
+ }
1717
+ function trimHint(text2) {
1718
+ const clean = text2.replace(/\s+/g, " ").trim();
1719
+ if (clean.length <= HINT_MAX_CHARS) return clean;
1720
+ const cut = clean.slice(0, HINT_MAX_CHARS);
1721
+ const space = cut.lastIndexOf(" ");
1722
+ return `${(space > 40 ? cut.slice(0, space) : cut).trimEnd()}\u2026`;
1723
+ }
1724
+ function lastNonEmptyLine(text2, limitLines = 8) {
1725
+ const plain = stripAnsi(text2);
1726
+ const lines = plain.split(/\r?\n/).slice(-limitLines);
1727
+ for (let i = lines.length - 1; i >= 0; i--) {
1728
+ const t = lines[i]?.trim() ?? "";
1729
+ if (t.length > 0) return t;
1730
+ }
1731
+ return "";
1732
+ }
1733
+ var jobs = /* @__PURE__ */ new Map();
1734
+ function createJobManager(options = {}) {
1735
+ const { projectRoot } = options;
1736
+ let persistTimer = null;
1737
+ function toStorable(job) {
1738
+ return { ...summarize(job), buffer: job.buffer, lastSeenAt: job.lastChunkAt };
1739
+ }
1740
+ function persistNow() {
1741
+ if (!projectRoot) return;
1742
+ if (persistTimer) {
1743
+ clearTimeout(persistTimer);
1744
+ persistTimer = null;
1745
+ }
1746
+ saveJobHistory(projectRoot, Array.from(jobs.values()).map(toStorable));
1747
+ }
1748
+ function schedulePersist() {
1749
+ if (!projectRoot || persistTimer) return;
1750
+ persistTimer = setTimeout(() => {
1751
+ persistTimer = null;
1752
+ persistNow();
1753
+ }, PERSIST_DEBOUNCE_MS);
1754
+ persistTimer.unref?.();
1755
+ }
1756
+ if (projectRoot) {
1757
+ for (const persisted of loadJobHistory(projectRoot)) {
1758
+ if (jobs.has(persisted.id)) continue;
1759
+ const emitter = new EventEmitter();
1760
+ emitter.setMaxListeners(32);
1761
+ jobs.set(persisted.id, {
1762
+ ...persisted,
1763
+ buffer: persisted.buffer,
1764
+ bufferBytes: persisted.buffer.length,
1765
+ emitter,
1766
+ lastChunkAt: persisted.endedAt ?? persisted.startedAt,
1767
+ promptPatterns: [],
1768
+ bufferBytesCap: Math.max(persisted.buffer.length, 1),
1769
+ restored: true
1770
+ });
1771
+ }
1772
+ }
1773
+ function summarize(job) {
1774
+ return {
1775
+ id: job.id,
1776
+ kind: job.kind,
1777
+ label: job.label,
1778
+ feature: job.feature,
1779
+ task: job.task,
1780
+ status: job.status,
1781
+ pid: job.pid,
1782
+ startedAt: job.startedAt,
1783
+ endedAt: job.endedAt,
1784
+ exitCode: job.exitCode,
1785
+ bufferBytes: job.bufferBytes,
1786
+ needsInputHint: job.needsInputHint,
1787
+ restored: job.restored
1788
+ };
1789
+ }
1790
+ function setStatus(job, next, extra) {
1791
+ if (job.status === next && extra?.exitCode === void 0) return;
1792
+ job.status = next;
1793
+ if (extra?.exitCode !== void 0) job.exitCode = extra.exitCode;
1794
+ job.emitter.emit("event", {
1795
+ type: "status",
1796
+ payload: {
1797
+ status: next,
1798
+ exitCode: job.exitCode,
1799
+ needsInputHint: job.needsInputHint
1800
+ }
1801
+ });
1802
+ schedulePersist();
1803
+ }
1804
+ function matchPrompt(job) {
1805
+ const tail = job.buffer.slice(-8e3);
1806
+ const last = lastNonEmptyLine(tail);
1807
+ if (!last) return null;
1808
+ for (const re of job.promptPatterns) {
1809
+ if (re.test(last)) return last;
1810
+ }
1811
+ return null;
1812
+ }
1813
+ function evaluateNeedsInput(job, trigger) {
1814
+ if (job.status === "done" || job.status === "failed" || job.status === "cancelled") return;
1815
+ if (job.status === "needs-input") return;
1816
+ if (job.graceUntil && Date.now() < job.graceUntil) return;
1817
+ const match = matchPrompt(job);
1818
+ const quietLongEnough = Date.now() - job.lastChunkAt >= INACTIVITY_MS;
1819
+ const shouldFlag = Boolean(match) || trigger === "inactivity" && quietLongEnough;
1820
+ if (shouldFlag) {
1821
+ job.needsInputHint = trimHint(match ?? lastNonEmptyLine(job.buffer.slice(-2e3)));
1822
+ setStatus(job, "needs-input");
1823
+ }
1824
+ }
1825
+ function scheduleInactivity(job) {
1826
+ if (job.inactivityTimer) clearTimeout(job.inactivityTimer);
1827
+ job.inactivityTimer = setTimeout(() => {
1828
+ evaluateNeedsInput(job, "inactivity");
1829
+ }, INACTIVITY_MS + 100);
1830
+ }
1831
+ function appendToBuffer(job, chunk) {
1832
+ job.buffer += chunk;
1833
+ if (job.buffer.length > job.bufferBytesCap) {
1834
+ job.buffer = job.buffer.slice(job.buffer.length - job.bufferBytesCap);
1835
+ }
1836
+ job.bufferBytes = job.buffer.length;
1837
+ job.lastChunkAt = Date.now();
1838
+ schedulePersist();
1839
+ }
1840
+ function startJob(input) {
1841
+ const id = nanoid(10);
1842
+ const cols = input.cols ?? DEFAULT_COLS;
1843
+ const rows = input.rows ?? DEFAULT_ROWS;
1844
+ const env = {
1845
+ ...process.env,
1846
+ ...input.env,
1847
+ TERM: "xterm-256color",
1848
+ COLORTERM: "truecolor",
1849
+ FORCE_COLOR: "1"
1850
+ };
1851
+ const ptyProc = pty.spawn(input.file, input.args, {
1852
+ name: "xterm-256color",
1853
+ cwd: input.cwd,
1854
+ cols,
1855
+ rows,
1856
+ env
1857
+ });
1858
+ const emitter = new EventEmitter();
1859
+ emitter.setMaxListeners(32);
1860
+ const job = {
1861
+ id,
1862
+ kind: input.kind,
1863
+ label: input.label,
1864
+ feature: input.feature,
1865
+ task: input.task,
1866
+ status: "running",
1867
+ pid: ptyProc.pid,
1868
+ startedAt: Date.now(),
1869
+ buffer: "",
1870
+ bufferBytes: 0,
1871
+ pty: ptyProc,
1872
+ emitter,
1873
+ lastChunkAt: Date.now(),
1874
+ promptPatterns: input.promptPatterns,
1875
+ bufferBytesCap: input.bufferBytesCap
1876
+ };
1877
+ jobs.set(id, job);
1878
+ ptyProc.onData((chunk) => {
1879
+ appendToBuffer(job, chunk);
1880
+ emitter.emit("event", { type: "chunk", payload: chunk });
1881
+ if (job.debounceTimer) clearTimeout(job.debounceTimer);
1882
+ job.debounceTimer = setTimeout(() => {
1883
+ evaluateNeedsInput(job, "chunk");
1884
+ }, DEBOUNCE_MS);
1885
+ scheduleInactivity(job);
1886
+ });
1887
+ ptyProc.onExit(({ exitCode, signal }) => {
1888
+ if (job.inactivityTimer) clearTimeout(job.inactivityTimer);
1889
+ if (job.debounceTimer) clearTimeout(job.debounceTimer);
1890
+ job.endedAt = Date.now();
1891
+ if (signal !== void 0 && signal !== 0 && job.status === "cancelled") {
1892
+ setStatus(job, "cancelled", { exitCode });
1893
+ } else if (exitCode === 0) {
1894
+ setStatus(job, "done", { exitCode });
1895
+ } else {
1896
+ setStatus(job, "failed", { exitCode });
1897
+ }
1898
+ });
1899
+ scheduleInactivity(job);
1900
+ schedulePersist();
1901
+ return summarize(job);
1902
+ }
1903
+ function writeInput(jobId, data) {
1904
+ const job = jobs.get(jobId);
1905
+ if (!job || !job.pty) return false;
1906
+ if (job.status === "done" || job.status === "failed" || job.status === "cancelled") {
1907
+ return false;
1908
+ }
1909
+ job.pty.write(data);
1910
+ job.graceUntil = Date.now() + INPUT_GRACE_MS;
1911
+ if (job.status === "needs-input") {
1912
+ job.needsInputHint = void 0;
1913
+ setStatus(job, "running");
1914
+ }
1915
+ return true;
1916
+ }
1917
+ function resize(jobId, cols, rows) {
1918
+ const job = jobs.get(jobId);
1919
+ if (!job || !job.pty) return false;
1920
+ if (job.status === "done" || job.status === "failed" || job.status === "cancelled") {
1921
+ return false;
1922
+ }
1923
+ try {
1924
+ job.pty.resize(Math.max(1, cols | 0), Math.max(1, rows | 0));
1925
+ return true;
1926
+ } catch {
1927
+ return false;
1928
+ }
1929
+ }
1930
+ function stopJob(jobId) {
1931
+ const job = jobs.get(jobId);
1932
+ if (!job) return false;
1933
+ if (job.status === "done" || job.status === "failed" || job.status === "cancelled") {
1934
+ return true;
1935
+ }
1936
+ if (!job.pty) return true;
1937
+ try {
1938
+ job.pty.kill("SIGINT");
1939
+ } catch {
1940
+ }
1941
+ setStatus(job, "cancelled");
1942
+ const pty1 = job.pty;
1943
+ const t1 = setTimeout(() => {
1944
+ if (job.status === "cancelled" && job.endedAt === void 0) {
1945
+ try {
1946
+ pty1.kill("SIGTERM");
1947
+ } catch {
1948
+ }
1949
+ }
1950
+ }, STOP_SIGTERM_DELAY_MS);
1951
+ const t2 = setTimeout(() => {
1952
+ if (job.status === "cancelled" && job.endedAt === void 0) {
1953
+ try {
1954
+ pty1.kill("SIGKILL");
1955
+ } catch {
1956
+ }
1957
+ }
1958
+ }, STOP_SIGTERM_DELAY_MS + STOP_SIGKILL_DELAY_MS);
1959
+ job.stopTimers = [t1, t2];
1960
+ return true;
1961
+ }
1962
+ function deleteJob(jobId) {
1963
+ const job = jobs.get(jobId);
1964
+ if (!job) return false;
1965
+ if (job.status === "running" || job.status === "needs-input") return false;
1966
+ if (job.stopTimers) job.stopTimers.forEach(clearTimeout);
1967
+ if (job.inactivityTimer) clearTimeout(job.inactivityTimer);
1968
+ if (job.debounceTimer) clearTimeout(job.debounceTimer);
1969
+ job.emitter.removeAllListeners();
1970
+ jobs.delete(jobId);
1971
+ schedulePersist();
1972
+ return true;
1973
+ }
1974
+ function listJobs() {
1975
+ return Array.from(jobs.values()).sort((a, b) => b.startedAt - a.startedAt).map(summarize);
1976
+ }
1977
+ function getJob(jobId) {
1978
+ const job = jobs.get(jobId);
1979
+ if (!job) return null;
1980
+ return { ...summarize(job), buffer: job.buffer };
1981
+ }
1982
+ function subscribe(jobId, handler) {
1983
+ const job = jobs.get(jobId);
1984
+ if (!job) return () => {
1985
+ };
1986
+ job.emitter.on("event", handler);
1987
+ return () => {
1988
+ job.emitter.off("event", handler);
1989
+ };
1990
+ }
1991
+ async function shutdownAll() {
1992
+ persistNow();
1993
+ for (const job of jobs.values()) {
1994
+ if (job.inactivityTimer) clearTimeout(job.inactivityTimer);
1995
+ if (job.debounceTimer) clearTimeout(job.debounceTimer);
1996
+ if (job.stopTimers) job.stopTimers.forEach(clearTimeout);
1997
+ try {
1998
+ if (job.pty && (job.status === "running" || job.status === "needs-input")) {
1999
+ job.pty.kill("SIGKILL");
2000
+ }
2001
+ } catch {
2002
+ }
2003
+ job.emitter.removeAllListeners();
2004
+ }
2005
+ jobs.clear();
2006
+ }
2007
+ return {
2008
+ startJob,
2009
+ writeInput,
2010
+ resize,
2011
+ stopJob,
2012
+ deleteJob,
2013
+ listJobs,
2014
+ getJob,
2015
+ subscribe,
2016
+ shutdownAll
2017
+ };
2018
+ }
2019
+ function compilePromptPatterns(patterns) {
2020
+ const compiled = [];
2021
+ for (const p2 of patterns) {
2022
+ try {
2023
+ compiled.push(new RegExp(p2, "i"));
2024
+ } catch (err) {
2025
+ console.error(`[job-manager] invalid prompt pattern "${p2}": ${err.message}`);
2026
+ }
2027
+ }
2028
+ return compiled;
2029
+ }
2030
+
2031
+ // ../../internal/specs-server/src/job-routes.ts
2032
+ import path13 from "path";
2033
+ import { z } from "zod";
2034
+
2035
+ // ../../internal/specs-server/src/types.ts
2036
+ var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
2037
+
2038
+ // ../../internal/specs-server/src/job-routes.ts
2039
+ var slugSchema = z.string().regex(SLUG_REGEX);
2040
+ var cliSchema = z.string().min(1).max(40).regex(/^[a-z0-9-]+$/).optional();
2041
+ var modelSchema = z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._\-/:]*$/).optional();
2042
+ var effortSchema = z.enum(AGENT_EFFORTS).optional();
2043
+ var createJobBodySchema = z.discriminatedUnion("kind", [
2044
+ z.object({
2045
+ kind: z.literal("agent"),
2046
+ mode: z.enum(["inline", "external"]).default("inline"),
2047
+ cli: cliSchema,
2048
+ model: modelSchema,
2049
+ effort: effortSchema,
2050
+ scope: z.enum(["feature", "featureNoPause", "task"]),
2051
+ feature: slugSchema,
2052
+ task: slugSchema.optional(),
2053
+ cols: z.number().int().min(20).max(400).optional(),
2054
+ rows: z.number().int().min(5).max(200).optional()
2055
+ }),
2056
+ z.object({
2057
+ kind: z.literal("refinement"),
2058
+ mode: z.enum(["inline", "external"]).default("inline"),
2059
+ cli: cliSchema,
2060
+ model: modelSchema,
2061
+ effort: effortSchema,
2062
+ prompt: z.string().min(1).max(1e4),
2063
+ cols: z.number().int().min(20).max(400).optional(),
2064
+ rows: z.number().int().min(5).max(200).optional()
2065
+ }),
2066
+ z.object({
2067
+ kind: z.literal("refinement-runner"),
2068
+ mode: z.enum(["inline", "external"]).default("inline"),
2069
+ cli: cliSchema,
2070
+ model: modelSchema,
2071
+ effort: effortSchema,
2072
+ prompt: z.string().min(1).max(1e4),
2073
+ cols: z.number().int().min(20).max(400).optional(),
2074
+ rows: z.number().int().min(5).max(200).optional()
2075
+ }),
2076
+ z.object({
2077
+ kind: z.literal("design"),
2078
+ mode: z.enum(["inline", "external"]).default("inline"),
2079
+ cli: cliSchema,
2080
+ model: modelSchema,
2081
+ effort: effortSchema,
2082
+ /** Pedido de alteração escrito pelo usuário no painel de protótipo. */
2083
+ prompt: z.string().min(1).max(1e4),
2084
+ /** `snapshot` regrava a cópia local usada no iframe; `change` (default) altera o design. */
2085
+ intent: z.enum(["change", "snapshot"]).default("change"),
2086
+ feature: slugSchema.optional(),
2087
+ task: slugSchema.optional(),
2088
+ cols: z.number().int().min(20).max(400).optional(),
2089
+ rows: z.number().int().min(5).max(200).optional()
2090
+ }),
2091
+ z.object({
2092
+ kind: z.literal("discovery-agent"),
2093
+ mode: z.enum(["inline", "external"]).default("inline"),
2094
+ cli: cliSchema,
2095
+ model: modelSchema,
2096
+ effort: effortSchema,
2097
+ prompt: z.string().min(1).max(1e4),
2098
+ cols: z.number().int().min(20).max(400).optional(),
2099
+ rows: z.number().int().min(5).max(200).optional()
2100
+ }),
2101
+ z.object({
2102
+ kind: z.literal("drawing-agent"),
2103
+ mode: z.enum(["inline", "external"]).default("inline"),
2104
+ cli: cliSchema,
2105
+ model: modelSchema,
2106
+ effort: effortSchema,
2107
+ prompt: z.string().min(1).max(1e4),
2108
+ cols: z.number().int().min(20).max(400).optional(),
2109
+ rows: z.number().int().min(5).max(200).optional()
2110
+ })
2111
+ ]);
2112
+ var jobIdParamSchema = z.object({ id: z.string().min(1).max(64) });
2113
+ var inputBodySchema = z.object({ data: z.string().max(1e4) });
2114
+ var resizeBodySchema = z.object({
2115
+ cols: z.number().int().min(20).max(400),
2116
+ rows: z.number().int().min(5).max(200)
2117
+ });
2118
+ function sseEvent(ev) {
2119
+ return `event: ${ev.type}
2120
+ data: ${JSON.stringify(ev.payload)}
2121
+
2122
+ `;
2123
+ }
2124
+ function registerJobRoutes(fastify, jobs2, getConfig, projectRoot) {
2125
+ fastify.post("/api/jobs", async (req, reply) => {
2126
+ const parsed = createJobBodySchema.safeParse(req.body);
2127
+ if (!parsed.success) {
2128
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2129
+ }
2130
+ const config = await getConfig();
2131
+ const body = parsed.data;
2132
+ let designPrompt = null;
2133
+ let designLabel = "";
2134
+ if (body.kind === "design") {
2135
+ const prototype = await resolvePrototype(projectRoot, config, body.feature);
2136
+ if (!prototype) {
2137
+ return reply.code(400).send({
2138
+ error: "no_prototype",
2139
+ detail: "Nenhum prot\xF3tipo declarado em .specs/config.json nem no meta.json da feature."
2140
+ });
2141
+ }
2142
+ designPrompt = buildDesignPrompt(
2143
+ prototype,
2144
+ body.prompt,
2145
+ {
2146
+ feature: body.feature,
2147
+ task: body.task,
2148
+ specPath: body.feature && body.task ? `${config.featuresDir}/${body.feature}/${body.task}.md` : body.feature ? `${config.featuresDir}/${body.feature}/` : void 0
2149
+ },
2150
+ body.intent
2151
+ );
2152
+ designLabel = body.intent === "snapshot" ? "Design: sincronizar snapshot" : `Design: ${body.prompt.slice(0, 60)}${body.prompt.length > 60 ? "\u2026" : ""}`;
2153
+ }
2154
+ if (body.mode === "external") {
2155
+ if (body.kind === "agent") {
2156
+ const result2 = await runAgent(
2157
+ {
2158
+ scope: body.scope,
2159
+ feature: body.feature,
2160
+ task: body.task,
2161
+ cli: body.cli,
2162
+ model: body.model,
2163
+ effort: body.effort
2164
+ },
2165
+ config,
2166
+ projectRoot
2167
+ );
2168
+ if (result2.ok) return { ok: true, mode: "external" };
2169
+ return reply.code(result2.status).send({
2170
+ error: result2.error,
2171
+ ..."detail" in result2 ? { detail: result2.detail } : {},
2172
+ ..."hint" in result2 ? { hint: result2.hint } : {}
2173
+ });
2174
+ }
2175
+ if (body.kind === "refinement") {
2176
+ const result2 = runRefinement(
2177
+ body.prompt,
2178
+ config,
2179
+ projectRoot,
2180
+ body.cli,
2181
+ body.model,
2182
+ body.effort
2183
+ );
2184
+ if (result2.ok) return { ok: true, mode: "external" };
2185
+ return reply.code(result2.status).send({
2186
+ error: result2.error,
2187
+ ..."detail" in result2 ? { detail: result2.detail } : {},
2188
+ ..."hint" in result2 ? { hint: result2.hint } : {}
2189
+ });
2190
+ }
2191
+ if (body.kind === "refinement-runner") {
2192
+ const result2 = runRefinementRunner(
2193
+ body.prompt,
2194
+ config,
2195
+ projectRoot,
2196
+ body.cli,
2197
+ body.model,
2198
+ body.effort
2199
+ );
2200
+ if (result2.ok) return { ok: true, mode: "external" };
2201
+ return reply.code(result2.status).send({
2202
+ error: result2.error,
2203
+ ..."detail" in result2 ? { detail: result2.detail } : {},
2204
+ ..."hint" in result2 ? { hint: result2.hint } : {}
2205
+ });
2206
+ }
2207
+ if (body.kind === "design") {
2208
+ const result2 = runDesign(
2209
+ designPrompt,
2210
+ config,
2211
+ projectRoot,
2212
+ body.cli,
2213
+ body.model,
2214
+ body.effort
2215
+ );
2216
+ if (result2.ok) return { ok: true, mode: "external" };
2217
+ return reply.code(result2.status).send({
2218
+ error: result2.error,
2219
+ ..."detail" in result2 ? { detail: result2.detail } : {},
2220
+ ..."hint" in result2 ? { hint: result2.hint } : {}
2221
+ });
2222
+ }
2223
+ if (body.kind === "drawing-agent") {
2224
+ const result2 = runDrawingAgent(
2225
+ body.prompt,
2226
+ config,
2227
+ projectRoot,
2228
+ body.cli,
2229
+ body.model,
2230
+ body.effort
2231
+ );
2232
+ if (result2.ok) return { ok: true, mode: "external" };
2233
+ return reply.code(result2.status).send({
2234
+ error: result2.error,
2235
+ ..."detail" in result2 ? { detail: result2.detail } : {},
2236
+ ..."hint" in result2 ? { hint: result2.hint } : {}
2237
+ });
2238
+ }
2239
+ const result = runDiscoveryAgent(
2240
+ body.prompt,
2241
+ config,
2242
+ projectRoot,
2243
+ body.cli,
2244
+ body.model,
2245
+ body.effort
2246
+ );
2247
+ if (result.ok) return { ok: true, mode: "external" };
2248
+ return reply.code(result.status).send({
2249
+ error: result.error,
2250
+ ..."detail" in result ? { detail: result.detail } : {},
2251
+ ..."hint" in result ? { hint: result.hint } : {}
2252
+ });
2253
+ }
2254
+ const patterns = compilePromptPatterns(config.agent.inputPromptPatterns);
2255
+ const cwd = path13.resolve(projectRoot, config.agent.cwd ?? ".");
2256
+ let inline = null;
2257
+ if (body.kind === "agent") {
2258
+ inline = await buildAgentInlineCommand(
2259
+ {
2260
+ scope: body.scope,
2261
+ feature: body.feature,
2262
+ task: body.task,
2263
+ cli: body.cli,
2264
+ model: body.model,
2265
+ effort: body.effort
2266
+ },
2267
+ config,
2268
+ projectRoot
2269
+ );
2270
+ } else if (body.kind === "refinement") {
2271
+ inline = buildNamedAgentInlineCommand(
2272
+ "refinement",
2273
+ body.prompt,
2274
+ config,
2275
+ projectRoot,
2276
+ `Refinement: ${body.prompt.slice(0, 60)}${body.prompt.length > 60 ? "\u2026" : ""}`,
2277
+ body.cli,
2278
+ body.model,
2279
+ body.effort
2280
+ );
2281
+ } else if (body.kind === "refinement-runner") {
2282
+ inline = buildNamedAgentInlineCommand(
2283
+ "refinement-runner",
2284
+ body.prompt,
2285
+ config,
2286
+ projectRoot,
2287
+ `Refinar + executar: ${body.prompt.slice(0, 60)}${body.prompt.length > 60 ? "\u2026" : ""}`,
2288
+ body.cli,
2289
+ body.model,
2290
+ body.effort
2291
+ );
2292
+ } else if (body.kind === "design") {
2293
+ inline = buildDesignInlineCommand(
2294
+ designPrompt,
2295
+ config,
2296
+ projectRoot,
2297
+ designLabel,
2298
+ body.cli,
2299
+ body.model,
2300
+ body.effort
2301
+ );
2302
+ } else if (body.kind === "drawing-agent") {
2303
+ inline = buildNamedAgentInlineCommand(
2304
+ "drawing-agent",
2305
+ body.prompt,
2306
+ config,
2307
+ projectRoot,
2308
+ `Desenho: ${body.prompt.slice(0, 60)}${body.prompt.length > 60 ? "\u2026" : ""}`,
2309
+ body.cli,
2310
+ body.model,
2311
+ body.effort
2312
+ );
2313
+ } else {
2314
+ inline = buildNamedAgentInlineCommand(
2315
+ "discovery-agent",
2316
+ body.prompt,
2317
+ config,
2318
+ projectRoot,
2319
+ `Discovery: ${body.prompt.slice(0, 60)}${body.prompt.length > 60 ? "\u2026" : ""}`,
2320
+ body.cli,
2321
+ body.model,
2322
+ body.effort
2323
+ );
2324
+ }
2325
+ if (!inline) {
2326
+ return reply.code(500).send({
2327
+ error: "inline_build_failed",
2328
+ detail: "N\xE3o foi poss\xEDvel montar o comando para execu\xE7\xE3o inline."
2329
+ });
2330
+ }
2331
+ try {
2332
+ const summary = jobs2.startJob({
2333
+ kind: body.kind,
2334
+ label: inline.label,
2335
+ file: inline.file,
2336
+ args: inline.args,
2337
+ cwd: inline.cwd ?? cwd,
2338
+ feature: body.kind === "agent" || body.kind === "design" ? body.feature : void 0,
2339
+ task: body.kind === "agent" || body.kind === "design" ? body.task : void 0,
2340
+ cols: body.cols,
2341
+ rows: body.rows,
2342
+ promptPatterns: patterns,
2343
+ bufferBytesCap: config.agent.bufferBytesCap
2344
+ });
2345
+ return { ok: true, mode: "inline", jobId: summary.id, job: summary };
2346
+ } catch (err) {
2347
+ return reply.code(500).send({
2348
+ error: "spawn_failed",
2349
+ detail: err.message
2350
+ });
2351
+ }
2352
+ });
2353
+ fastify.get("/api/jobs", async () => ({ jobs: jobs2.listJobs() }));
2354
+ const modelsQuerySchema = z.object({
2355
+ cli: z.string().min(1).max(40).regex(/^[a-z0-9-]+$/)
2356
+ });
2357
+ fastify.get("/api/agent-models", async (req, reply) => {
2358
+ const parsed = modelsQuerySchema.safeParse(req.query);
2359
+ if (!parsed.success) return reply.code(400).send({ error: "invalid_query" });
2360
+ const config = await getConfig();
2361
+ const result = await listAgentModels(config, projectRoot, parsed.data.cli);
2362
+ return result;
2363
+ });
2364
+ fastify.get("/api/jobs/:id", async (req, reply) => {
2365
+ const parsed = jobIdParamSchema.safeParse(req.params);
2366
+ if (!parsed.success) return reply.code(400).send({ error: "invalid_id" });
2367
+ const job = jobs2.getJob(parsed.data.id);
2368
+ if (!job) return reply.code(404).send({ error: "job_not_found" });
2369
+ return job;
2370
+ });
2371
+ fastify.post("/api/jobs/:id/input", async (req, reply) => {
2372
+ const idParsed = jobIdParamSchema.safeParse(req.params);
2373
+ if (!idParsed.success) return reply.code(400).send({ error: "invalid_id" });
2374
+ const bodyParsed = inputBodySchema.safeParse(req.body);
2375
+ if (!bodyParsed.success) return reply.code(400).send({ error: "invalid_body" });
2376
+ const ok = jobs2.writeInput(idParsed.data.id, bodyParsed.data.data);
2377
+ if (!ok) return reply.code(404).send({ error: "job_not_found_or_terminated" });
2378
+ return { ok: true };
2379
+ });
2380
+ fastify.post("/api/jobs/:id/resize", async (req, reply) => {
2381
+ const idParsed = jobIdParamSchema.safeParse(req.params);
2382
+ if (!idParsed.success) return reply.code(400).send({ error: "invalid_id" });
2383
+ const bodyParsed = resizeBodySchema.safeParse(req.body);
2384
+ if (!bodyParsed.success) return reply.code(400).send({ error: "invalid_body" });
2385
+ const ok = jobs2.resize(idParsed.data.id, bodyParsed.data.cols, bodyParsed.data.rows);
2386
+ if (!ok) return reply.code(404).send({ error: "job_not_found_or_terminated" });
2387
+ return { ok: true };
2388
+ });
2389
+ fastify.post("/api/jobs/:id/stop", async (req, reply) => {
2390
+ const parsed = jobIdParamSchema.safeParse(req.params);
2391
+ if (!parsed.success) return reply.code(400).send({ error: "invalid_id" });
2392
+ const ok = jobs2.stopJob(parsed.data.id);
2393
+ if (!ok) return reply.code(404).send({ error: "job_not_found" });
2394
+ return { ok: true };
2395
+ });
2396
+ fastify.delete("/api/jobs/:id", async (req, reply) => {
2397
+ const parsed = jobIdParamSchema.safeParse(req.params);
2398
+ if (!parsed.success) return reply.code(400).send({ error: "invalid_id" });
2399
+ const ok = jobs2.deleteJob(parsed.data.id);
2400
+ if (!ok) return reply.code(409).send({ error: "job_still_running_or_not_found" });
2401
+ return { ok: true };
2402
+ });
2403
+ fastify.get("/api/jobs/:id/stream", async (req, reply) => {
2404
+ const parsed = jobIdParamSchema.safeParse(req.params);
2405
+ if (!parsed.success) return reply.code(400).send({ error: "invalid_id" });
2406
+ const jobId = parsed.data.id;
2407
+ const snapshot = jobs2.getJob(jobId);
2408
+ if (!snapshot) return reply.code(404).send({ error: "job_not_found" });
2409
+ reply.raw.setHeader("content-type", "text/event-stream");
2410
+ reply.raw.setHeader("cache-control", "no-cache, no-transform");
2411
+ reply.raw.setHeader("connection", "keep-alive");
2412
+ reply.raw.setHeader("x-accel-buffering", "no");
2413
+ reply.raw.flushHeaders?.();
2414
+ if (snapshot.buffer.length > 0) {
2415
+ reply.raw.write(sseEvent({ type: "chunk", payload: snapshot.buffer }));
2416
+ }
2417
+ reply.raw.write(
2418
+ sseEvent({
2419
+ type: "status",
2420
+ payload: {
2421
+ status: snapshot.status,
2422
+ exitCode: snapshot.exitCode,
2423
+ needsInputHint: snapshot.needsInputHint
2424
+ }
2425
+ })
2426
+ );
2427
+ const keepAlive = setInterval(() => {
2428
+ reply.raw.write(": ping\n\n");
2429
+ }, 15e3);
2430
+ const unsubscribe = jobs2.subscribe(jobId, (event) => {
2431
+ reply.raw.write(sseEvent(event));
2432
+ });
2433
+ req.raw.on("close", () => {
2434
+ clearInterval(keepAlive);
2435
+ unsubscribe();
2436
+ });
2437
+ return reply;
2438
+ });
2439
+ }
2440
+
2441
+ // ../../internal/specs-server/src/start-server.ts
2442
+ var statusSchema = z2.enum(["pending", "in-progress", "completed", "blocked"]);
2443
+ var slugSchema2 = z2.string().regex(SLUG_REGEX);
2444
+ var contentQuerySchema = z2.object({
2445
+ feature: slugSchema2,
2446
+ task: slugSchema2
2447
+ });
2448
+ var reviewDiffQuerySchema = z2.object({
2449
+ path: z2.string().min(1).max(1024),
2450
+ commit: z2.string().regex(/^[0-9a-f]{7,40}$/i).optional()
2451
+ });
2452
+ var openEditorBodySchema = z2.object({
2453
+ path: z2.string().min(1).max(1024),
2454
+ line: z2.number().int().positive().optional()
2455
+ });
2456
+ var statusBodySchema = z2.object({
2457
+ feature: slugSchema2,
2458
+ task: slugSchema2,
2459
+ status: statusSchema
2460
+ });
2461
+ var reorderFeaturesBodySchema = z2.object({
2462
+ pages: z2.array(slugSchema2)
2463
+ });
2464
+ var reorderTasksBodySchema = z2.object({
2465
+ feature: slugSchema2,
2466
+ pages: z2.array(slugSchema2)
2467
+ });
2468
+ var runAgentBodySchema = z2.object({
2469
+ scope: z2.enum(["feature", "featureNoPause", "task"]),
2470
+ feature: slugSchema2,
2471
+ task: slugSchema2.optional()
2472
+ });
2473
+ var deleteTaskQuerySchema = z2.object({
2474
+ feature: slugSchema2,
2475
+ task: slugSchema2
2476
+ });
2477
+ var deleteFeatureQuerySchema = z2.object({
2478
+ feature: slugSchema2
2479
+ });
2480
+ var updateContentBodySchema = z2.object({
2481
+ feature: slugSchema2,
2482
+ task: slugSchema2,
2483
+ body: z2.string().max(1e6)
2484
+ // 1MB — bem mais que o necessário para um .md de task
2485
+ });
2486
+ var runRefinementBodySchema = z2.object({
2487
+ prompt: z2.string().min(1).max(1e4)
2488
+ });
2489
+ var discoveryContentQuerySchema = z2.object({
2490
+ slug: slugSchema2
2491
+ });
2492
+ var updateDiscoveryContentBodySchema = z2.object({
2493
+ slug: slugSchema2,
2494
+ body: z2.string().max(1e6)
2495
+ });
2496
+ var deleteDiscoveryQuerySchema = z2.object({
2497
+ slug: slugSchema2
2498
+ });
2499
+ var reorderDiscoveriesBodySchema = z2.object({
2500
+ pages: z2.array(slugSchema2)
2501
+ });
2502
+ var prototypeQuerySchema = z2.object({
2503
+ feature: slugSchema2.optional()
2504
+ });
2505
+ var runDiscoveryAgentBodySchema = z2.object({
2506
+ prompt: z2.string().min(1).max(1e4)
2507
+ });
2508
+ var drawingContentQuerySchema = z2.object({
2509
+ slug: slugSchema2
2510
+ });
2511
+ var updateDrawingContentBodySchema = z2.object({
2512
+ slug: slugSchema2,
2513
+ body: z2.string().max(1e6)
2514
+ });
2515
+ var deleteDrawingQuerySchema = z2.object({
2516
+ slug: slugSchema2
2517
+ });
2518
+ var reorderDrawingsBodySchema = z2.object({
2519
+ pages: z2.array(slugSchema2)
2520
+ });
2521
+ var runDrawingAgentBodySchema = z2.object({
2522
+ prompt: z2.string().min(1).max(1e4)
2523
+ });
2524
+ function logLine(method, url, startMs, status) {
2525
+ const dur = Date.now() - startMs;
2526
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
2527
+ console.log(`[${stamp}] ${method} ${url} ${dur}ms -> ${status}`);
2528
+ }
2529
+ async function readConfigFile(projectRoot) {
2530
+ const configPath = path14.join(projectRoot, ".specs", "config.json");
2531
+ if (!await fileExists(configPath)) {
2532
+ return applyDefaults({});
2533
+ }
2534
+ try {
2535
+ const raw = await readFile8(configPath, "utf8");
2536
+ const parsed = JSON.parse(raw);
2537
+ return applyDefaults(parsed);
2538
+ } catch (err) {
2539
+ throw new Error(`invalid_config: ${err.message}`);
2540
+ }
2541
+ }
2542
+ function setTaskPath(projectRoot, featuresDir, feature, task) {
2543
+ const taskPath = path14.resolve(projectRoot, featuresDir, feature, `${task}.md`);
2544
+ assertInsideProject(taskPath, projectRoot);
2545
+ return taskPath;
2546
+ }
2547
+ function setMetaPath(projectRoot, featuresDir, feature) {
2548
+ const p2 = feature ? path14.resolve(projectRoot, featuresDir, feature, "meta.json") : path14.resolve(projectRoot, featuresDir, "meta.json");
2549
+ assertInsideProject(p2, projectRoot);
2550
+ return p2;
2551
+ }
2552
+ function setDiscoveryPath(projectRoot, discoveriesDir, slug) {
2553
+ const p2 = path14.resolve(projectRoot, discoveriesDir, `${slug}.md`);
2554
+ assertInsideProject(p2, projectRoot);
2555
+ return p2;
2556
+ }
2557
+ function setDiscoveryMetaPath(projectRoot, discoveriesDir) {
2558
+ const p2 = path14.resolve(projectRoot, discoveriesDir, "meta.json");
2559
+ assertInsideProject(p2, projectRoot);
2560
+ return p2;
2561
+ }
2562
+ function setDrawingPath(projectRoot, drawingsDir, slug) {
2563
+ const p2 = path14.resolve(projectRoot, drawingsDir, `${slug}.md`);
2564
+ assertInsideProject(p2, projectRoot);
2565
+ return p2;
2566
+ }
2567
+ function setDrawingMetaPath(projectRoot, drawingsDir) {
2568
+ const p2 = path14.resolve(projectRoot, drawingsDir, "meta.json");
2569
+ assertInsideProject(p2, projectRoot);
2570
+ return p2;
2571
+ }
2572
+ async function startServer(userConfig, opts) {
2573
+ const { projectRoot } = opts;
2574
+ const config = applyDefaults(userConfig);
2575
+ const featuresDir = config.featuresDir;
2576
+ const discoveriesDir = config.discoveriesDir;
2577
+ const drawingsDir = config.drawingsDir;
2578
+ const rootMetaPath = path14.join(projectRoot, featuresDir, "meta.json");
2579
+ if (!await fileExists(rootMetaPath)) {
2580
+ throw new Error(
2581
+ `${featuresDir}/meta.json n\xE3o encontrado em ${projectRoot}. Rode 'specs install' antes de 'specs start'.`
2582
+ );
2583
+ }
2584
+ const fastify = Fastify({ logger: false });
2585
+ await fastify.register(fastifyCors, {
2586
+ origin: (origin, cb) => {
2587
+ if (!origin) return cb(null, true);
2588
+ try {
2589
+ const url = new URL(origin);
2590
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
2591
+ return cb(null, true);
2592
+ }
2593
+ } catch {
2594
+ }
2595
+ return cb(new Error("cors_blocked"), false);
2596
+ }
2597
+ });
2598
+ if (opts.uiDir && await dirExists(opts.uiDir)) {
2599
+ await fastify.register(fastifyStatic, {
2600
+ root: opts.uiDir,
2601
+ prefix: "/"
2602
+ });
2603
+ fastify.setNotFoundHandler((req, reply) => {
2604
+ if (req.method !== "GET" || req.url.startsWith("/api")) {
2605
+ return reply.code(404).send({ error: "not_found" });
2606
+ }
2607
+ return reply.sendFile("index.html");
2608
+ });
2609
+ }
2610
+ fastify.addHook("onRequest", async (req) => {
2611
+ ;
2612
+ req._startMs = Date.now();
2613
+ });
2614
+ fastify.addHook("onResponse", async (req, reply) => {
2615
+ const startMs = req._startMs ?? Date.now();
2616
+ logLine(req.method, req.url, startMs, reply.statusCode);
2617
+ });
2618
+ const jobManager = createJobManager({ projectRoot });
2619
+ registerJobRoutes(
2620
+ fastify,
2621
+ jobManager,
2622
+ () => readConfigFile(projectRoot).catch(() => config),
2623
+ projectRoot
2624
+ );
2625
+ const watcher = createSpecsWatcher({
2626
+ projectRoot,
2627
+ featuresDir,
2628
+ discoveriesDir,
2629
+ drawingsDir,
2630
+ extraFiles: [
2631
+ { file: desktopHistoryPath(), event: { type: "usage" } },
2632
+ { file: cliCachePath(), event: { type: "usage" } },
2633
+ { file: statuslineSnapshotPath(), event: { type: "usage" } }
2634
+ ]
2635
+ });
2636
+ fastify.get("/api/tree", async (_req, reply) => {
2637
+ try {
2638
+ const tree = await buildTree(projectRoot, featuresDir, discoveriesDir, drawingsDir);
2639
+ return tree;
2640
+ } catch (err) {
2641
+ const msg = err.message;
2642
+ if (msg === "no_features_dir") {
2643
+ return reply.code(500).send({ error: "no_features_dir" });
2644
+ }
2645
+ return reply.code(500).send({ error: "internal", detail: msg });
2646
+ }
2647
+ });
2648
+ fastify.get("/api/content", async (req, reply) => {
2649
+ const parsed = contentQuerySchema.safeParse(req.query);
2650
+ if (!parsed.success) {
2651
+ return reply.code(400).send({ error: "invalid_query" });
2652
+ }
2653
+ const { feature, task } = parsed.data;
2654
+ let taskPath;
2655
+ try {
2656
+ taskPath = setTaskPath(projectRoot, featuresDir, feature, task);
2657
+ } catch {
2658
+ return reply.code(400).send({ error: "invalid_path" });
2659
+ }
2660
+ if (!await fileExists(taskPath)) {
2661
+ return reply.code(404).send({ error: "task_not_found" });
2662
+ }
2663
+ const raw = await readText(taskPath);
2664
+ const { data, body } = parseFrontmatter(raw);
2665
+ const frontmatter = {
2666
+ ...data,
2667
+ ...extractFrontmatterFields(data)
2668
+ };
2669
+ const headings = extractHeadings(body);
2670
+ return { frontmatter, body, headings, raw };
2671
+ });
2672
+ fastify.get("/api/review/changes", async (_req, reply) => {
2673
+ try {
2674
+ return await collectChanges(projectRoot);
2675
+ } catch (err) {
2676
+ return reply.code(500).send({ error: "git_failed", detail: err.message });
2677
+ }
2678
+ });
2679
+ fastify.get("/api/review/diff", async (req, reply) => {
2680
+ const parsed = reviewDiffQuerySchema.safeParse(req.query);
2681
+ if (!parsed.success) {
2682
+ return reply.code(400).send({ error: "invalid_query" });
2683
+ }
2684
+ try {
2685
+ return await fileDiff(projectRoot, parsed.data.path, parsed.data.commit);
2686
+ } catch (err) {
2687
+ const message = err.message;
2688
+ const status = message === "invalid_path" ? 400 : 500;
2689
+ return reply.code(status).send({ error: message });
2690
+ }
2691
+ });
2692
+ fastify.post("/api/review/open", async (req, reply) => {
2693
+ const parsed = openEditorBodySchema.safeParse(req.body);
2694
+ if (!parsed.success) {
2695
+ return reply.code(400).send({ error: "invalid_body" });
2696
+ }
2697
+ const current = await readConfigFile(projectRoot).catch(() => config);
2698
+ const result = await openInEditor(
2699
+ projectRoot,
2700
+ current.review.openCommand,
2701
+ parsed.data.path,
2702
+ parsed.data.line
2703
+ );
2704
+ if (!result.ok) {
2705
+ const status = result.error === "invalid_path" ? 400 : 500;
2706
+ return reply.code(status).send(result);
2707
+ }
2708
+ return result;
2709
+ });
2710
+ fastify.post("/api/status", async (req, reply) => {
2711
+ const parsed = statusBodySchema.safeParse(req.body);
2712
+ if (!parsed.success) {
2713
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2714
+ }
2715
+ const { feature, task, status } = parsed.data;
2716
+ let taskPath;
2717
+ try {
2718
+ taskPath = setTaskPath(projectRoot, featuresDir, feature, task);
2719
+ } catch {
2720
+ return reply.code(400).send({ error: "invalid_path" });
2721
+ }
2722
+ if (!await fileExists(taskPath)) {
2723
+ return reply.code(404).send({ error: "task_not_found" });
2724
+ }
2725
+ const raw = await readText(taskPath);
2726
+ const next = patchStatusInFrontmatter(raw, status);
2727
+ if (next !== raw) {
2728
+ await atomicWrite(taskPath, next);
2729
+ }
2730
+ return { ok: true, status };
2731
+ });
2732
+ fastify.post("/api/reorder-features", async (req, reply) => {
2733
+ const parsed = reorderFeaturesBodySchema.safeParse(req.body);
2734
+ if (!parsed.success) {
2735
+ return reply.code(400).send({ error: "invalid_body" });
2736
+ }
2737
+ const { pages } = parsed.data;
2738
+ const metaPath = setMetaPath(projectRoot, featuresDir);
2739
+ const raw = await readText(metaPath);
2740
+ let meta;
2741
+ try {
2742
+ meta = JSON.parse(raw);
2743
+ } catch {
2744
+ return reply.code(500).send({ error: "invalid_meta" });
2745
+ }
2746
+ const currentSet = new Set(Array.isArray(meta.pages) ? meta.pages : []);
2747
+ const newSet = new Set(pages);
2748
+ if (currentSet.size !== newSet.size || [...currentSet].some((s) => !newSet.has(s))) {
2749
+ return reply.code(400).send({ error: "invalid_pages" });
2750
+ }
2751
+ meta.pages = pages;
2752
+ await atomicWrite(metaPath, `${JSON.stringify(meta, null, 2)}
2753
+ `);
2754
+ return { ok: true };
2755
+ });
2756
+ fastify.post("/api/reorder-tasks", async (req, reply) => {
2757
+ const parsed = reorderTasksBodySchema.safeParse(req.body);
2758
+ if (!parsed.success) {
2759
+ return reply.code(400).send({ error: "invalid_body" });
2760
+ }
2761
+ const { feature, pages } = parsed.data;
2762
+ const metaPath = setMetaPath(projectRoot, featuresDir, feature);
2763
+ if (!await fileExists(metaPath)) {
2764
+ return reply.code(404).send({ error: "feature_not_found" });
2765
+ }
2766
+ const raw = await readText(metaPath);
2767
+ let meta;
2768
+ try {
2769
+ meta = JSON.parse(raw);
2770
+ } catch {
2771
+ return reply.code(500).send({ error: "invalid_meta" });
2772
+ }
2773
+ const currentSet = new Set(Array.isArray(meta.pages) ? meta.pages : []);
2774
+ const newSet = new Set(pages);
2775
+ if (currentSet.size !== newSet.size || [...currentSet].some((s) => !newSet.has(s))) {
2776
+ return reply.code(400).send({ error: "invalid_pages" });
2777
+ }
2778
+ meta.pages = pages;
2779
+ await atomicWrite(metaPath, `${JSON.stringify(meta, null, 2)}
2780
+ `);
2781
+ return { ok: true };
2782
+ });
2783
+ fastify.post("/api/run-agent", async (req, reply) => {
2784
+ const parsed = runAgentBodySchema.safeParse(req.body);
2785
+ if (!parsed.success) {
2786
+ return reply.code(400).send({ error: "invalid_body" });
2787
+ }
2788
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
2789
+ const result = await runAgent(parsed.data, fresh, projectRoot);
2790
+ if (result.ok) return { ok: true };
2791
+ return reply.code(result.status).send({
2792
+ error: result.error,
2793
+ ..."detail" in result ? { detail: result.detail } : {},
2794
+ ..."hint" in result ? { hint: result.hint } : {}
2795
+ });
2796
+ });
2797
+ fastify.post("/api/run-refinement", async (req, reply) => {
2798
+ const parsed = runRefinementBodySchema.safeParse(req.body);
2799
+ if (!parsed.success) {
2800
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2801
+ }
2802
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
2803
+ const result = runRefinement(parsed.data.prompt, fresh, projectRoot);
2804
+ if (result.ok) return { ok: true };
2805
+ return reply.code(result.status).send({
2806
+ error: result.error,
2807
+ ..."detail" in result ? { detail: result.detail } : {},
2808
+ ..."hint" in result ? { hint: result.hint } : {}
2809
+ });
2810
+ });
2811
+ fastify.post("/api/run-discovery-agent", async (req, reply) => {
2812
+ const parsed = runDiscoveryAgentBodySchema.safeParse(req.body);
2813
+ if (!parsed.success) {
2814
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2815
+ }
2816
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
2817
+ const result = runDiscoveryAgent(parsed.data.prompt, fresh, projectRoot);
2818
+ if (result.ok) return { ok: true };
2819
+ return reply.code(result.status).send({
2820
+ error: result.error,
2821
+ ..."detail" in result ? { detail: result.detail } : {},
2822
+ ..."hint" in result ? { hint: result.hint } : {}
2823
+ });
2824
+ });
2825
+ fastify.post("/api/run-drawing-agent", async (req, reply) => {
2826
+ const parsed = runDrawingAgentBodySchema.safeParse(req.body);
2827
+ if (!parsed.success) {
2828
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2829
+ }
2830
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
2831
+ const result = runDrawingAgent(parsed.data.prompt, fresh, projectRoot);
2832
+ if (result.ok) return { ok: true };
2833
+ return reply.code(result.status).send({
2834
+ error: result.error,
2835
+ ..."detail" in result ? { detail: result.detail } : {},
2836
+ ..."hint" in result ? { hint: result.hint } : {}
2837
+ });
2838
+ });
2839
+ fastify.put("/api/content", async (req, reply) => {
2840
+ const parsed = updateContentBodySchema.safeParse(req.body);
2841
+ if (!parsed.success) {
2842
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2843
+ }
2844
+ const { feature, task, body } = parsed.data;
2845
+ let taskPath;
2846
+ try {
2847
+ taskPath = setTaskPath(projectRoot, featuresDir, feature, task);
2848
+ } catch {
2849
+ return reply.code(400).send({ error: "invalid_path" });
2850
+ }
2851
+ if (!await fileExists(taskPath)) {
2852
+ return reply.code(404).send({ error: "task_not_found" });
2853
+ }
2854
+ await atomicWrite(taskPath, body);
2855
+ return { ok: true };
2856
+ });
2857
+ fastify.delete("/api/task", async (req, reply) => {
2858
+ const parsed = deleteTaskQuerySchema.safeParse(req.query);
2859
+ if (!parsed.success) {
2860
+ return reply.code(400).send({ error: "invalid_query" });
2861
+ }
2862
+ const { feature, task } = parsed.data;
2863
+ let taskPath;
2864
+ let featureMetaPath;
2865
+ try {
2866
+ taskPath = setTaskPath(projectRoot, featuresDir, feature, task);
2867
+ featureMetaPath = setMetaPath(projectRoot, featuresDir, feature);
2868
+ } catch {
2869
+ return reply.code(400).send({ error: "invalid_path" });
2870
+ }
2871
+ if (await fileExists(taskPath)) {
2872
+ await unlink(taskPath);
2873
+ }
2874
+ if (await fileExists(featureMetaPath)) {
2875
+ const raw = await readText(featureMetaPath);
2876
+ try {
2877
+ const meta = JSON.parse(raw);
2878
+ if (Array.isArray(meta.pages)) {
2879
+ const filtered = meta.pages.filter((p2) => p2 !== task);
2880
+ if (filtered.length !== meta.pages.length) {
2881
+ meta.pages = filtered;
2882
+ await atomicWrite(featureMetaPath, `${JSON.stringify(meta, null, 2)}
2883
+ `);
2884
+ }
2885
+ }
2886
+ } catch {
2887
+ }
2888
+ }
2889
+ return { ok: true };
2890
+ });
2891
+ fastify.delete("/api/feature", async (req, reply) => {
2892
+ const parsed = deleteFeatureQuerySchema.safeParse(req.query);
2893
+ if (!parsed.success) {
2894
+ return reply.code(400).send({ error: "invalid_query" });
2895
+ }
2896
+ const { feature } = parsed.data;
2897
+ const featureDir = path14.resolve(projectRoot, featuresDir, feature);
2898
+ try {
2899
+ assertInsideProject(featureDir, projectRoot);
2900
+ } catch {
2901
+ return reply.code(400).send({ error: "invalid_path" });
2902
+ }
2903
+ if (await dirExists(featureDir)) {
2904
+ await rm3(featureDir, { recursive: true, force: true });
2905
+ }
2906
+ const rootMetaPath2 = setMetaPath(projectRoot, featuresDir);
2907
+ if (await fileExists(rootMetaPath2)) {
2908
+ const raw = await readText(rootMetaPath2);
2909
+ try {
2910
+ const meta = JSON.parse(raw);
2911
+ if (Array.isArray(meta.pages)) {
2912
+ const filtered = meta.pages.filter((p2) => p2 !== feature);
2913
+ if (filtered.length !== meta.pages.length) {
2914
+ meta.pages = filtered;
2915
+ await atomicWrite(rootMetaPath2, `${JSON.stringify(meta, null, 2)}
2916
+ `);
2917
+ }
2918
+ }
2919
+ } catch {
2920
+ }
2921
+ }
2922
+ return { ok: true };
2923
+ });
2924
+ fastify.get("/api/discovery/content", async (req, reply) => {
2925
+ const parsed = discoveryContentQuerySchema.safeParse(req.query);
2926
+ if (!parsed.success) {
2927
+ return reply.code(400).send({ error: "invalid_query" });
2928
+ }
2929
+ const { slug } = parsed.data;
2930
+ let discoveryPath;
2931
+ try {
2932
+ discoveryPath = setDiscoveryPath(projectRoot, discoveriesDir, slug);
2933
+ } catch {
2934
+ return reply.code(400).send({ error: "invalid_path" });
2935
+ }
2936
+ if (!await fileExists(discoveryPath)) {
2937
+ return reply.code(404).send({ error: "discovery_not_found" });
2938
+ }
2939
+ const raw = await readText(discoveryPath);
2940
+ const { data, body } = parseFrontmatter(raw);
2941
+ const frontmatter = {
2942
+ ...data,
2943
+ ...extractDiscoveryFrontmatterFields(data)
2944
+ };
2945
+ const headings = extractHeadings(body);
2946
+ return { frontmatter, body, headings, raw };
2947
+ });
2948
+ fastify.put("/api/discovery/content", async (req, reply) => {
2949
+ const parsed = updateDiscoveryContentBodySchema.safeParse(req.body);
2950
+ if (!parsed.success) {
2951
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
2952
+ }
2953
+ const { slug, body } = parsed.data;
2954
+ let discoveryPath;
2955
+ try {
2956
+ discoveryPath = setDiscoveryPath(projectRoot, discoveriesDir, slug);
2957
+ } catch {
2958
+ return reply.code(400).send({ error: "invalid_path" });
2959
+ }
2960
+ if (!await fileExists(discoveryPath)) {
2961
+ return reply.code(404).send({ error: "discovery_not_found" });
2962
+ }
2963
+ await atomicWrite(discoveryPath, body);
2964
+ return { ok: true };
2965
+ });
2966
+ fastify.delete("/api/discovery", async (req, reply) => {
2967
+ const parsed = deleteDiscoveryQuerySchema.safeParse(req.query);
2968
+ if (!parsed.success) {
2969
+ return reply.code(400).send({ error: "invalid_query" });
2970
+ }
2971
+ const { slug } = parsed.data;
2972
+ let discoveryPath;
2973
+ let discoveryMetaPath;
2974
+ try {
2975
+ discoveryPath = setDiscoveryPath(projectRoot, discoveriesDir, slug);
2976
+ discoveryMetaPath = setDiscoveryMetaPath(projectRoot, discoveriesDir);
2977
+ } catch {
2978
+ return reply.code(400).send({ error: "invalid_path" });
2979
+ }
2980
+ if (await fileExists(discoveryPath)) {
2981
+ await unlink(discoveryPath);
2982
+ }
2983
+ if (await fileExists(discoveryMetaPath)) {
2984
+ const raw = await readText(discoveryMetaPath);
2985
+ try {
2986
+ const meta = JSON.parse(raw);
2987
+ if (Array.isArray(meta.pages)) {
2988
+ const filtered = meta.pages.filter((p2) => p2 !== slug);
2989
+ if (filtered.length !== meta.pages.length) {
2990
+ meta.pages = filtered;
2991
+ await atomicWrite(discoveryMetaPath, `${JSON.stringify(meta, null, 2)}
2992
+ `);
2993
+ }
2994
+ }
2995
+ } catch {
2996
+ }
2997
+ }
2998
+ return { ok: true };
2999
+ });
3000
+ fastify.post("/api/reorder-discoveries", async (req, reply) => {
3001
+ const parsed = reorderDiscoveriesBodySchema.safeParse(req.body);
3002
+ if (!parsed.success) {
3003
+ return reply.code(400).send({ error: "invalid_body" });
3004
+ }
3005
+ const { pages } = parsed.data;
3006
+ const metaPath = setDiscoveryMetaPath(projectRoot, discoveriesDir);
3007
+ if (!await fileExists(metaPath)) {
3008
+ return reply.code(404).send({ error: "discoveries_meta_not_found" });
3009
+ }
3010
+ const raw = await readText(metaPath);
3011
+ let meta;
3012
+ try {
3013
+ meta = JSON.parse(raw);
3014
+ } catch {
3015
+ return reply.code(500).send({ error: "invalid_meta" });
3016
+ }
3017
+ const currentSet = new Set(Array.isArray(meta.pages) ? meta.pages : []);
3018
+ const newSet = new Set(pages);
3019
+ if (currentSet.size !== newSet.size || [...currentSet].some((s) => !newSet.has(s))) {
3020
+ return reply.code(400).send({ error: "invalid_pages" });
3021
+ }
3022
+ meta.pages = pages;
3023
+ await atomicWrite(metaPath, `${JSON.stringify(meta, null, 2)}
3024
+ `);
3025
+ return { ok: true };
3026
+ });
3027
+ fastify.get("/api/drawing/content", async (req, reply) => {
3028
+ const parsed = drawingContentQuerySchema.safeParse(req.query);
3029
+ if (!parsed.success) {
3030
+ return reply.code(400).send({ error: "invalid_query" });
3031
+ }
3032
+ let drawingPath;
3033
+ try {
3034
+ drawingPath = setDrawingPath(projectRoot, drawingsDir, parsed.data.slug);
3035
+ } catch {
3036
+ return reply.code(400).send({ error: "invalid_path" });
3037
+ }
3038
+ if (!await fileExists(drawingPath)) {
3039
+ return reply.code(404).send({ error: "drawing_not_found" });
3040
+ }
3041
+ const raw = await readText(drawingPath);
3042
+ const { data, body } = parseFrontmatter(raw);
3043
+ const frontmatter = {
3044
+ ...data,
3045
+ ...extractDrawingFrontmatterFields(data)
3046
+ };
3047
+ const headings = extractHeadings(body);
3048
+ return { frontmatter, body, headings, raw };
3049
+ });
3050
+ fastify.put("/api/drawing/content", async (req, reply) => {
3051
+ const parsed = updateDrawingContentBodySchema.safeParse(req.body);
3052
+ if (!parsed.success) {
3053
+ return reply.code(400).send({ error: "invalid_body", detail: parsed.error.message });
3054
+ }
3055
+ const { slug, body } = parsed.data;
3056
+ let drawingPath;
3057
+ try {
3058
+ drawingPath = setDrawingPath(projectRoot, drawingsDir, slug);
3059
+ } catch {
3060
+ return reply.code(400).send({ error: "invalid_path" });
3061
+ }
3062
+ if (!await fileExists(drawingPath)) {
3063
+ return reply.code(404).send({ error: "drawing_not_found" });
3064
+ }
3065
+ await atomicWrite(drawingPath, body);
3066
+ return { ok: true };
3067
+ });
3068
+ fastify.delete("/api/drawing", async (req, reply) => {
3069
+ const parsed = deleteDrawingQuerySchema.safeParse(req.query);
3070
+ if (!parsed.success) {
3071
+ return reply.code(400).send({ error: "invalid_query" });
3072
+ }
3073
+ const { slug } = parsed.data;
3074
+ let drawingPath;
3075
+ let drawingMetaPath;
3076
+ try {
3077
+ drawingPath = setDrawingPath(projectRoot, drawingsDir, slug);
3078
+ drawingMetaPath = setDrawingMetaPath(projectRoot, drawingsDir);
3079
+ } catch {
3080
+ return reply.code(400).send({ error: "invalid_path" });
3081
+ }
3082
+ if (await fileExists(drawingPath)) {
3083
+ await unlink(drawingPath);
3084
+ }
3085
+ if (await fileExists(drawingMetaPath)) {
3086
+ const raw = await readText(drawingMetaPath);
3087
+ try {
3088
+ const meta = JSON.parse(raw);
3089
+ if (Array.isArray(meta.pages)) {
3090
+ const filtered = meta.pages.filter((p2) => p2 !== slug);
3091
+ if (filtered.length !== meta.pages.length) {
3092
+ meta.pages = filtered;
3093
+ await atomicWrite(drawingMetaPath, `${JSON.stringify(meta, null, 2)}
3094
+ `);
3095
+ }
3096
+ }
3097
+ } catch {
3098
+ }
3099
+ }
3100
+ return { ok: true };
3101
+ });
3102
+ fastify.post("/api/reorder-drawings", async (req, reply) => {
3103
+ const parsed = reorderDrawingsBodySchema.safeParse(req.body);
3104
+ if (!parsed.success) {
3105
+ return reply.code(400).send({ error: "invalid_body" });
3106
+ }
3107
+ const { pages } = parsed.data;
3108
+ const metaPath = setDrawingMetaPath(projectRoot, drawingsDir);
3109
+ if (!await fileExists(metaPath)) {
3110
+ return reply.code(404).send({ error: "drawings_meta_not_found" });
3111
+ }
3112
+ const raw = await readText(metaPath);
3113
+ let meta;
3114
+ try {
3115
+ meta = JSON.parse(raw);
3116
+ } catch {
3117
+ return reply.code(500).send({ error: "invalid_meta" });
3118
+ }
3119
+ const currentSet = new Set(Array.isArray(meta.pages) ? meta.pages : []);
3120
+ const newSet = new Set(pages);
3121
+ if (currentSet.size !== newSet.size || [...currentSet].some((s) => !newSet.has(s))) {
3122
+ return reply.code(400).send({ error: "invalid_pages" });
3123
+ }
3124
+ meta.pages = pages;
3125
+ await atomicWrite(metaPath, `${JSON.stringify(meta, null, 2)}
3126
+ `);
3127
+ return { ok: true };
3128
+ });
3129
+ fastify.get("/api/prototype", async (req, reply) => {
3130
+ const parsed = prototypeQuerySchema.safeParse(req.query);
3131
+ if (!parsed.success) {
3132
+ return reply.code(400).send({ error: "invalid_query" });
3133
+ }
3134
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
3135
+ const prototype = await resolvePrototype(projectRoot, fresh, parsed.data.feature);
3136
+ return { prototype };
3137
+ });
3138
+ fastify.get("/api/prototype/frame", async (req, reply) => {
3139
+ const parsed = prototypeQuerySchema.safeParse(req.query);
3140
+ if (!parsed.success) {
3141
+ return reply.code(400).send({ error: "invalid_query" });
3142
+ }
3143
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
3144
+ const prototype = await resolvePrototype(projectRoot, fresh, parsed.data.feature);
3145
+ if (!prototype) return reply.code(404).send({ error: "no_prototype" });
3146
+ const abs = snapshotAbsolutePath(projectRoot, prototype);
3147
+ if (!abs) return reply.code(404).send({ error: "no_snapshot" });
3148
+ const html = await readText(abs);
3149
+ return reply.type("text/html; charset=utf-8").header("cache-control", "no-store").send(html);
3150
+ });
3151
+ fastify.post("/api/prototype/open", async (req, reply) => {
3152
+ const parsed = prototypeQuerySchema.safeParse(req.body ?? {});
3153
+ if (!parsed.success) {
3154
+ return reply.code(400).send({ error: "invalid_body" });
3155
+ }
3156
+ const fresh = await readConfigFile(projectRoot).catch(() => config);
3157
+ const prototype = await resolvePrototype(projectRoot, fresh, parsed.data.feature);
3158
+ if (!prototype) return reply.code(404).send({ error: "no_prototype" });
3159
+ const result = openPrototype(projectRoot, prototype);
3160
+ if (result.ok) return result;
3161
+ return reply.code(result.status).send(result);
3162
+ });
3163
+ fastify.get("/api/events", async (req, reply) => {
3164
+ reply.raw.setHeader("content-type", "text/event-stream");
3165
+ reply.raw.setHeader("cache-control", "no-cache, no-transform");
3166
+ reply.raw.setHeader("connection", "keep-alive");
3167
+ reply.raw.setHeader("x-accel-buffering", "no");
3168
+ reply.raw.flushHeaders?.();
3169
+ reply.raw.write(`data: ${JSON.stringify({ type: "hello", watching: watcher.watching })}
3170
+
3171
+ `);
3172
+ const keepAlive = setInterval(() => {
3173
+ reply.raw.write(": ping\n\n");
3174
+ }, 15e3);
3175
+ const unsubscribe = watcher.subscribe((events) => {
3176
+ reply.raw.write(`data: ${JSON.stringify({ type: "change", events })}
3177
+
3178
+ `);
3179
+ });
3180
+ req.raw.on("close", () => {
3181
+ clearInterval(keepAlive);
3182
+ unsubscribe();
3183
+ });
3184
+ return reply;
3185
+ });
3186
+ fastify.get("/api/usage", async (_req, reply) => {
3187
+ return reply.send({ usage: await readClaudeUsage() });
3188
+ });
3189
+ fastify.get("/api/config", async (_req, reply) => {
3190
+ try {
3191
+ const fresh = await readConfigFile(projectRoot);
3192
+ return { config: fresh, projectRoot };
3193
+ } catch (err) {
3194
+ return reply.code(500).send({ error: "invalid_config", detail: err.message });
3195
+ }
3196
+ });
3197
+ fastify.get("/health", async () => ({ ok: true }));
3198
+ const host = "127.0.0.1";
3199
+ const port = config.port;
3200
+ await fastify.listen({ host, port });
3201
+ return {
3202
+ url: `http://localhost:${port}`,
3203
+ port,
3204
+ close: async () => {
3205
+ watcher.close();
3206
+ await jobManager.shutdownAll();
3207
+ await fastify.close();
3208
+ }
3209
+ };
3210
+ }
3211
+
3212
+ // src/commands/start.ts
3213
+ import open from "open";
3214
+ import pc2 from "picocolors";
3215
+ var stateFile = (projectRoot) => path15.join(runDir(projectRoot), "specs.json");
3216
+ var logFile = (projectRoot) => path15.join(runDir(projectRoot), "specs.log");
3217
+ async function runStart(opts) {
3218
+ const config = await readConfig(opts.projectRoot);
3219
+ const port = opts.port ?? config.port ?? 4321;
3220
+ const url = `http://localhost:${port}`;
3221
+ if (opts.foreground) {
3222
+ await startForeground(opts, { ...config, port }, url);
3223
+ return;
3224
+ }
3225
+ const running = await readRunningState(opts.projectRoot);
3226
+ if (running) {
3227
+ log.success(`specs j\xE1 est\xE1 rodando (pid ${running.pid})`);
3228
+ printUrls(running.url, opts.projectRoot);
3229
+ return;
3230
+ }
3231
+ if (await isPortInUse(port)) {
3232
+ throw new Error(
3233
+ `a porta ${port} j\xE1 est\xE1 em uso (outra inst\xE2ncia? veja \`lsof -i :${port}\`). Use \`specs start -p <porta>\`.`
3234
+ );
3235
+ }
3236
+ await mkdir3(runDir(opts.projectRoot), { recursive: true });
3237
+ const out = openSync(logFile(opts.projectRoot), "w");
3238
+ const args = [cliEntry, "start", "--foreground", "--no-open", "--port", String(port)];
3239
+ if (opts.apiOnly) args.push("--api-only");
3240
+ const child = spawn5(process.execPath, args, {
3241
+ cwd: opts.projectRoot,
3242
+ detached: true,
3243
+ stdio: ["ignore", out, out]
3244
+ });
3245
+ child.unref();
3246
+ if (!child.pid) throw new Error("n\xE3o foi poss\xEDvel iniciar o processo em background.");
3247
+ const state = { pid: child.pid, url, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3248
+ await writeFile4(stateFile(opts.projectRoot), `${JSON.stringify(state, null, 2)}
3249
+ `, "utf8");
3250
+ const ready = await waitForPort(port, 15e3, () => isAlive(state.pid));
3251
+ if (!ready) {
3252
+ await rm4(stateFile(opts.projectRoot), { force: true });
3253
+ const tail = (await readFile9(logFile(opts.projectRoot), "utf8")).split("\n").slice(-30).join("\n");
3254
+ throw new Error(`o servidor n\xE3o subiu. \xDAltimas linhas do log:
3255
+ ${tail}`);
3256
+ }
3257
+ log.success(`specs iniciado em background (pid ${state.pid})`);
3258
+ printUrls(url, opts.projectRoot);
3259
+ if (opts.openBrowser && !opts.apiOnly) await open(url).catch(() => {
3260
+ });
3261
+ }
3262
+ async function runStop(projectRoot) {
3263
+ const state = await readState(projectRoot);
3264
+ if (!state) {
3265
+ log.kept("specs n\xE3o est\xE1 rodando em background neste projeto.");
3266
+ return;
3267
+ }
3268
+ if (isAlive(state.pid)) {
3269
+ process.kill(state.pid, "SIGTERM");
3270
+ for (let i = 0; i < 50 && isAlive(state.pid); i++) await sleep(100);
3271
+ if (isAlive(state.pid)) process.kill(state.pid, "SIGKILL");
3272
+ }
3273
+ await rm4(stateFile(projectRoot), { force: true });
3274
+ log.success(`specs parado (pid ${state.pid})`);
3275
+ }
3276
+ async function runStatus(projectRoot) {
3277
+ const running = await readRunningState(projectRoot);
3278
+ if (!running) {
3279
+ log.kept("specs n\xE3o est\xE1 rodando em background neste projeto.");
3280
+ return;
3281
+ }
3282
+ log.success(`specs rodando (pid ${running.pid}, desde ${running.startedAt})`);
3283
+ printUrls(running.url, projectRoot);
3284
+ }
3285
+ async function startForeground(opts, config, url) {
3286
+ const server = await startServer(config, {
3287
+ projectRoot: opts.projectRoot,
3288
+ uiDir: opts.apiOnly ? void 0 : uiDir
3289
+ });
3290
+ log.title(`specs \u2014 ${opts.projectRoot}`);
3291
+ console.log(
3292
+ `${pc2.green("\u2713")} ${opts.apiOnly ? "API" : "UI + API"}: ${pc2.bold(pc2.cyan(server.url))}`
3293
+ );
3294
+ console.log(pc2.dim("Pressione Ctrl+C para encerrar."));
3295
+ if (opts.openBrowser && !opts.apiOnly) await open(url).catch(() => {
3296
+ });
3297
+ let closing = false;
3298
+ const shutdown = async () => {
3299
+ if (closing) return;
3300
+ closing = true;
3301
+ await server.close().catch(() => {
3302
+ });
3303
+ process.exit(0);
3304
+ };
3305
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
3306
+ process.on(signal, () => void shutdown());
3307
+ }
3308
+ }
3309
+ async function readConfig(projectRoot) {
3310
+ const configPath = path15.join(projectRoot, ".specs", "config.json");
3311
+ if (!await exists(configPath)) {
3312
+ throw new Error(
3313
+ ".specs/config.json n\xE3o encontrado. Rode `specs install` na raiz do projeto antes."
3314
+ );
3315
+ }
3316
+ return JSON.parse(await readFile9(configPath, "utf8"));
3317
+ }
3318
+ async function readState(projectRoot) {
3319
+ try {
3320
+ return JSON.parse(await readFile9(stateFile(projectRoot), "utf8"));
3321
+ } catch {
3322
+ return null;
3323
+ }
3324
+ }
3325
+ async function readRunningState(projectRoot) {
3326
+ const state = await readState(projectRoot);
3327
+ if (!state) return null;
3328
+ if (isAlive(state.pid)) return state;
3329
+ await rm4(stateFile(projectRoot), { force: true });
3330
+ return null;
3331
+ }
3332
+ function printUrls(url, projectRoot) {
3333
+ console.log(` UI: ${pc2.cyan(url)}`);
3334
+ console.log(
3335
+ ` logs: ${path15.relative(process.cwd(), logFile(projectRoot)) || logFile(projectRoot)}`
3336
+ );
3337
+ console.log(" stop: specs stop");
3338
+ }
3339
+ function isAlive(pid) {
3340
+ try {
3341
+ process.kill(pid, 0);
3342
+ return true;
3343
+ } catch {
3344
+ return false;
3345
+ }
3346
+ }
3347
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3348
+ function tryConnect(host, port) {
3349
+ return new Promise((resolve) => {
3350
+ const socket = net.createConnection({ host, port });
3351
+ const finish = (ok) => {
3352
+ socket.destroy();
3353
+ resolve(ok);
3354
+ };
3355
+ socket.once("connect", () => finish(true));
3356
+ socket.once("error", () => finish(false));
3357
+ socket.setTimeout(1e3, () => finish(false));
3358
+ });
3359
+ }
3360
+ async function waitForPort(port, timeoutMs, alive) {
3361
+ const start = Date.now();
3362
+ while (Date.now() - start < timeoutMs && alive()) {
3363
+ if (await isPortInUse(port)) return true;
3364
+ await sleep(250);
3365
+ }
3366
+ return false;
3367
+ }
3368
+ async function isPortInUse(port) {
3369
+ const [v4, v6] = await Promise.all([tryConnect("127.0.0.1", port), tryConnect("::1", port)]);
3370
+ return v4 || v6;
3371
+ }
3372
+
3373
+ // src/tool.ts
3374
+ import { readFileSync as readFileSync2 } from "fs";
3375
+ import path16 from "path";
3376
+ import * as p from "@clack/prompts";
3377
+ var pkg = JSON.parse(readFileSync2(path16.join(packageRoot, "package.json"), "utf8"));
3378
+ function register(cmd) {
3379
+ cmd.command("install").alias("update").description(
3380
+ "Instala ou atualiza a Specs Platform no projeto (.agents/, .specs/, symlinks, SPECS.md)."
3381
+ ).argument("[dir]", "Raiz do projeto.", ".").option("-f, --force", "Sobrescreve tamb\xE9m config, project.md, SPECS.md e scaffolds.", false).option("--clean-legacy", "Remove o .specs/app/ de instala\xE7\xF5es antigas.", false).action(async (dir, opts) => {
3382
+ await runInstall({
3383
+ projectRoot: path16.resolve(dir),
3384
+ force: opts.force,
3385
+ cleanLegacy: opts.cleanLegacy
3386
+ });
3387
+ });
3388
+ cmd.command("start").description("Sobe a UI + API (background por default) e abre o browser.").option("-f, --foreground", "Roda em foreground (Ctrl+C para parar).", false).option("-p, --port <port>", "Porta (default: `port` do .specs/config.json).").option("--no-open", "N\xE3o abre o browser.").option("--api-only", "S\xF3 a API, sem a UI (para desenvolver a UI com o Vite).", false).action(
3389
+ async (opts) => {
3390
+ await runStart({
3391
+ projectRoot: process.cwd(),
3392
+ foreground: opts.foreground,
3393
+ port: opts.port ? Number.parseInt(opts.port, 10) : void 0,
3394
+ openBrowser: opts.open,
3395
+ apiOnly: opts.apiOnly
3396
+ });
3397
+ }
3398
+ );
3399
+ cmd.command("stop").description("Para a inst\xE2ncia em background deste projeto.").action(() => runStop(process.cwd()));
3400
+ cmd.command("status").description("Mostra se a inst\xE2ncia em background deste projeto est\xE1 rodando.").action(() => runStatus(process.cwd()));
3401
+ cmd.command("statusline").description("Mostra como plugar o medidor de uso do plano no statusLine do Claude Code.").action(() => printStatusline());
3402
+ }
3403
+ function printStatusline() {
3404
+ console.log(
3405
+ "Adicione em ~/.claude/settings.json (troque o argumento pelo seu statusLine atual, ou remova):\n"
3406
+ );
3407
+ console.log(
3408
+ JSON.stringify(
3409
+ {
3410
+ statusLine: {
3411
+ type: "command",
3412
+ command: `bash ${statuslineScript} ~/.claude/statusline-command.sh`
3413
+ }
3414
+ },
3415
+ null,
3416
+ 2
3417
+ )
3418
+ );
3419
+ }
3420
+ async function interactiveInstall(cwd) {
3421
+ const dir = await p.text({ message: "Diret\xF3rio do projeto", initialValue: cwd });
3422
+ if (p.isCancel(dir)) return;
3423
+ const projectRoot = path16.resolve(dir);
3424
+ let force = false;
3425
+ if (await exists(path16.join(projectRoot, ".specs", "config.json"))) {
3426
+ const mode = await p.select({
3427
+ message: "A Specs Platform j\xE1 est\xE1 instalada aqui. O que fazer?",
3428
+ options: [
3429
+ {
3430
+ value: "update",
3431
+ label: "Atualizar",
3432
+ hint: "regrava agents/skills, preserva suas customiza\xE7\xF5es"
3433
+ },
3434
+ {
3435
+ value: "force",
3436
+ label: "Reinstalar do zero",
3437
+ hint: "sobrescreve config, project.md e SPECS.md"
3438
+ }
3439
+ ]
3440
+ });
3441
+ if (p.isCancel(mode)) return;
3442
+ force = mode === "force";
3443
+ }
3444
+ let cleanLegacy = false;
3445
+ if (await exists(path16.join(projectRoot, ".specs", "app"))) {
3446
+ const answer = await p.confirm({
3447
+ message: "Encontrei .specs/app/ de uma instala\xE7\xE3o antiga (n\xE3o \xE9 mais usado). Remover?"
3448
+ });
3449
+ if (p.isCancel(answer)) return;
3450
+ cleanLegacy = answer;
3451
+ }
3452
+ await runInstall({ projectRoot, force, cleanLegacy });
3453
+ }
3454
+ var specsPlatformTool = defineTool({
3455
+ id: "specs-platform",
3456
+ packageName: pkg.name,
3457
+ title: "Specs Platform",
3458
+ description: pkg.description,
3459
+ version: pkg.version,
3460
+ register,
3461
+ actions: [
3462
+ {
3463
+ id: "install",
3464
+ label: "Instalar / atualizar no projeto",
3465
+ run: ({ cwd }) => interactiveInstall(cwd)
3466
+ },
3467
+ {
3468
+ id: "start",
3469
+ label: "Subir a UI",
3470
+ hint: "background",
3471
+ run: ({ cwd }) => runStart({ projectRoot: cwd, foreground: false, openBrowser: true })
3472
+ },
3473
+ { id: "stop", label: "Parar a UI", run: ({ cwd }) => runStop(cwd) },
3474
+ {
3475
+ id: "statusline",
3476
+ label: "Configurar medidor de uso do Claude",
3477
+ run: async () => printStatusline()
3478
+ }
3479
+ ]
3480
+ });
3481
+
3482
+ export {
3483
+ log,
3484
+ runInstall,
3485
+ runStart,
3486
+ runStop,
3487
+ runStatus,
3488
+ specsPlatformTool
3489
+ };
3490
+ //# sourceMappingURL=chunk-SOOETS3N.js.map