speckit-assistant 0.1.3 → 0.1.5

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 (177) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +56 -20
  3. package/.next/app-path-routes-manifest.json +6 -1
  4. package/.next/build-manifest.json +5 -5
  5. package/.next/cache/next-devtools-config.json +1 -0
  6. package/.next/cache/webpack/client-development/0.pack.gz +0 -0
  7. package/.next/cache/webpack/client-development/index.pack.gz +0 -0
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/11.pack +0 -0
  11. package/.next/cache/webpack/client-production/12.pack +0 -0
  12. package/.next/cache/webpack/client-production/13.pack +0 -0
  13. package/.next/cache/webpack/client-production/15.pack +0 -0
  14. package/.next/cache/webpack/client-production/16.pack +0 -0
  15. package/.next/cache/webpack/client-production/17.pack +0 -0
  16. package/.next/cache/webpack/client-production/18.pack +0 -0
  17. package/.next/cache/webpack/client-production/19.pack +0 -0
  18. package/.next/cache/webpack/client-production/2.pack +0 -0
  19. package/.next/cache/webpack/client-production/20.pack +0 -0
  20. package/.next/cache/webpack/client-production/21.pack +0 -0
  21. package/.next/cache/webpack/client-production/22.pack +0 -0
  22. package/.next/cache/webpack/client-production/23.pack +0 -0
  23. package/.next/cache/webpack/client-production/24.pack +0 -0
  24. package/.next/cache/webpack/client-production/25.pack +0 -0
  25. package/.next/cache/webpack/client-production/26.pack +0 -0
  26. package/.next/cache/webpack/client-production/27.pack +0 -0
  27. package/.next/cache/webpack/client-production/28.pack +0 -0
  28. package/.next/cache/webpack/client-production/29.pack +0 -0
  29. package/.next/cache/webpack/client-production/3.pack +0 -0
  30. package/.next/cache/webpack/client-production/30.pack +0 -0
  31. package/.next/cache/webpack/client-production/31.pack +0 -0
  32. package/.next/cache/webpack/client-production/32.pack +0 -0
  33. package/.next/cache/webpack/client-production/33.pack +0 -0
  34. package/.next/cache/webpack/client-production/34.pack +0 -0
  35. package/.next/cache/webpack/client-production/4.pack +0 -0
  36. package/.next/cache/webpack/client-production/6.pack +0 -0
  37. package/.next/cache/webpack/client-production/7.pack +0 -0
  38. package/.next/cache/webpack/client-production/8.pack +0 -0
  39. package/.next/cache/webpack/client-production/9.pack +0 -0
  40. package/.next/cache/webpack/client-production/index.pack +0 -0
  41. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  42. package/.next/cache/webpack/server-production/0.pack +0 -0
  43. package/.next/cache/webpack/server-production/1.pack +0 -0
  44. package/.next/cache/webpack/server-production/10.pack +0 -0
  45. package/.next/cache/webpack/server-production/11.pack +0 -0
  46. package/.next/cache/webpack/server-production/12.pack +0 -0
  47. package/.next/cache/webpack/server-production/13.pack +0 -0
  48. package/.next/cache/webpack/server-production/14.pack +0 -0
  49. package/.next/cache/webpack/server-production/16.pack +0 -0
  50. package/.next/cache/webpack/server-production/18.pack +0 -0
  51. package/.next/cache/webpack/server-production/19.pack +0 -0
  52. package/.next/cache/webpack/server-production/2.pack +0 -0
  53. package/.next/cache/webpack/server-production/20.pack +0 -0
  54. package/.next/cache/webpack/server-production/21.pack +0 -0
  55. package/.next/cache/webpack/server-production/23.pack +0 -0
  56. package/.next/cache/webpack/server-production/24.pack +0 -0
  57. package/.next/cache/webpack/server-production/25.pack +0 -0
  58. package/.next/cache/webpack/server-production/26.pack +0 -0
  59. package/.next/cache/webpack/server-production/27.pack +0 -0
  60. package/.next/cache/webpack/server-production/28.pack +0 -0
  61. package/.next/cache/webpack/server-production/29.pack +0 -0
  62. package/.next/cache/webpack/server-production/3.pack +0 -0
  63. package/.next/cache/webpack/server-production/30.pack +0 -0
  64. package/.next/cache/webpack/server-production/31.pack +0 -0
  65. package/.next/cache/webpack/server-production/32.pack +0 -0
  66. package/.next/cache/webpack/server-production/33.pack +0 -0
  67. package/.next/cache/webpack/server-production/4.pack +0 -0
  68. package/.next/cache/webpack/server-production/5.pack +0 -0
  69. package/.next/cache/webpack/server-production/6.pack +0 -0
  70. package/.next/cache/webpack/server-production/7.pack +0 -0
  71. package/.next/cache/webpack/server-production/9.pack +0 -0
  72. package/.next/cache/webpack/server-production/index.pack +0 -0
  73. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  74. package/.next/prerender-manifest.json +7 -7
  75. package/.next/react-loadable-manifest.json +8 -1
  76. package/.next/required-server-files.json +3 -0
  77. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app/_not-found.html +1 -1
  79. package/.next/server/app/_not-found.rsc +2 -2
  80. package/.next/server/app/api/feature/route.js +1 -1
  81. package/.next/server/app/api/feature/route.js.nft.json +1 -1
  82. package/.next/server/app/api/feature/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/file/route.js +1 -1
  84. package/.next/server/app/api/file/route.js.nft.json +1 -1
  85. package/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/git/route.js +1 -0
  87. package/.next/server/app/api/git/route.js.nft.json +1 -0
  88. package/.next/server/app/api/git/route_client-reference-manifest.js +1 -0
  89. package/.next/server/app/api/personas/route.js +1 -0
  90. package/.next/server/app/api/personas/route.js.nft.json +1 -0
  91. package/.next/server/app/api/personas/route_client-reference-manifest.js +1 -0
  92. package/.next/server/app/api/phase/route.js +3 -3
  93. package/.next/server/app/api/phase/route.js.nft.json +1 -1
  94. package/.next/server/app/api/phase/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/state/route.js +1 -1
  96. package/.next/server/app/api/state/route.js.nft.json +1 -1
  97. package/.next/server/app/api/state/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/state/watch/route.js +2 -2
  99. package/.next/server/app/api/state/watch/route.js.nft.json +1 -1
  100. package/.next/server/app/api/state/watch/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/task/route.js +1 -1
  102. package/.next/server/app/api/task/route.js.nft.json +1 -1
  103. package/.next/server/app/api/task/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/terminal/input/route.js +1 -0
  105. package/.next/server/app/api/terminal/input/route.js.nft.json +1 -0
  106. package/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -0
  107. package/.next/server/app/api/terminal/resize/route.js +1 -0
  108. package/.next/server/app/api/terminal/resize/route.js.nft.json +1 -0
  109. package/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -0
  110. package/.next/server/app/api/terminal/stream/route.js +3 -0
  111. package/.next/server/app/api/terminal/stream/route.js.nft.json +1 -0
  112. package/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -0
  113. package/.next/server/app/index.html +1 -1
  114. package/.next/server/app/index.rsc +4 -4
  115. package/.next/server/app/page.js +26 -7
  116. package/.next/server/app/page_client-reference-manifest.js +1 -1
  117. package/.next/server/app-paths-manifest.json +6 -1
  118. package/.next/server/chunks/607.js +2 -2
  119. package/.next/server/chunks/897.js +13 -7
  120. package/.next/server/middleware-build-manifest.js +1 -1
  121. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  122. package/.next/server/pages/404.html +1 -1
  123. package/.next/server/pages/500.html +1 -1
  124. package/.next/static/8CUCTC2XnkR-leKsH4TwE/_buildManifest.js +1 -0
  125. package/.next/static/chunks/111acf76-29d5e5905666f1e0.js +18 -0
  126. package/.next/static/chunks/343.91af0d46f6df0f05.js +1 -0
  127. package/.next/static/chunks/873-a995367ae371a5e4.js +1 -0
  128. package/.next/static/chunks/app/api/feature/route-b4fbc89d13fef983.js +1 -0
  129. package/.next/static/chunks/app/api/file/route-b4fbc89d13fef983.js +1 -0
  130. package/.next/static/chunks/app/api/git/route-b4fbc89d13fef983.js +1 -0
  131. package/.next/static/chunks/app/api/personas/route-b4fbc89d13fef983.js +1 -0
  132. package/.next/static/chunks/app/api/phase/route-b4fbc89d13fef983.js +1 -0
  133. package/.next/static/chunks/app/api/state/route-b4fbc89d13fef983.js +1 -0
  134. package/.next/static/chunks/app/api/state/watch/route-b4fbc89d13fef983.js +1 -0
  135. package/.next/static/chunks/app/api/task/route-b4fbc89d13fef983.js +1 -0
  136. package/.next/static/chunks/app/api/terminal/input/route-b4fbc89d13fef983.js +1 -0
  137. package/.next/static/chunks/app/api/terminal/resize/route-b4fbc89d13fef983.js +1 -0
  138. package/.next/static/chunks/app/api/terminal/stream/route-b4fbc89d13fef983.js +1 -0
  139. package/.next/static/chunks/app/page-b3dea12c644f2054.js +1 -0
  140. package/.next/static/chunks/webpack-c997c7bd3e8f7cdf.js +1 -0
  141. package/.next/static/css/0024e90648d29466.css +3 -0
  142. package/.next/static/css/{008a05b0ad6b854a.css → af3f40f14f702ff7.css} +1 -1
  143. package/.next/trace +3 -3
  144. package/.next/types/app/api/git/route.ts +347 -0
  145. package/.next/types/app/api/personas/route.ts +347 -0
  146. package/.next/types/app/api/terminal/input/route.ts +347 -0
  147. package/.next/types/app/api/terminal/resize/route.ts +347 -0
  148. package/.next/types/app/api/terminal/stream/route.ts +347 -0
  149. package/.next/types/routes.d.ts +6 -1
  150. package/.next/types/validator.ts +45 -0
  151. package/bin/adapters/primary/api/terminalManager.js +92 -0
  152. package/bin/adapters/secondary/agent/ProcessAgentRunner.js +170 -65
  153. package/bin/adapters/secondary/fs/FSWorkspaceRepository.js +40 -5
  154. package/bin/adapters/secondary/pty/ptyLoader.js +128 -0
  155. package/bin/app/api/git/route.js +156 -0
  156. package/bin/app/api/personas/route.js +76 -0
  157. package/bin/app/api/phase/route.js +18 -5
  158. package/bin/app/api/state/watch/route.js +7 -2
  159. package/bin/app/api/terminal/input/route.js +18 -0
  160. package/bin/app/api/terminal/resize/route.js +18 -0
  161. package/bin/app/api/terminal/stream/route.js +44 -0
  162. package/bin/domain/models/personas.js +65 -0
  163. package/bin/domain/services/WorkflowService.js +79 -5
  164. package/next.config.mjs +3 -0
  165. package/package.json +13 -1
  166. package/.next/static/Bxo1ckOxvQcndtSKD9Yna/_buildManifest.js +0 -1
  167. package/.next/static/chunks/590-a6568595ecd2a994.js +0 -1
  168. package/.next/static/chunks/app/api/feature/route-bb3c1a82e892ab58.js +0 -1
  169. package/.next/static/chunks/app/api/file/route-bb3c1a82e892ab58.js +0 -1
  170. package/.next/static/chunks/app/api/phase/route-bb3c1a82e892ab58.js +0 -1
  171. package/.next/static/chunks/app/api/state/route-bb3c1a82e892ab58.js +0 -1
  172. package/.next/static/chunks/app/api/state/watch/route-bb3c1a82e892ab58.js +0 -1
  173. package/.next/static/chunks/app/api/task/route-bb3c1a82e892ab58.js +0 -1
  174. package/.next/static/chunks/app/page-8a5248f7704cde29.js +0 -1
  175. package/.next/static/chunks/webpack-c460f8e58a9e9eff.js +0 -1
  176. package/.next/static/css/6fd2e11db3a59771.css +0 -3
  177. /package/.next/static/{Bxo1ckOxvQcndtSKD9Yna → 8CUCTC2XnkR-leKsH4TwE}/_ssgManifest.js +0 -0
@@ -34,128 +34,233 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ProcessAgentRunner = void 0;
37
- const child_process_1 = require("child_process");
37
+ const ptyLoader_1 = require("../pty/ptyLoader");
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const PHASE_COMMANDS = {
41
- constitution: '/speckit.constitution',
42
- specification: '/speckit.specify',
43
- clarification: '/speckit.clarify',
44
- planning: '/speckit.plan',
45
- checklist: '/speckit.checklist',
46
- analyze: '/speckit.analyze',
47
- tasks: '/speckit.tasks',
48
- taskstoissues: '/speckit.taskstoissues',
49
- implementation: '/speckit.implement',
41
+ constitution: "/speckit.constitution",
42
+ specification: "/speckit.specify",
43
+ clarification: "/speckit.clarify",
44
+ planning: "/speckit.plan",
45
+ checklist: "/speckit.checklist",
46
+ analyze: "/speckit.analyze",
47
+ tasks: "/speckit.tasks",
48
+ taskstoissues: "/speckit.taskstoissues",
49
+ implementation: "/speckit.implement",
50
50
  };
51
51
  class ProcessAgentRunner {
52
+ activeProcesses = new Map();
53
+ phaseKey(featureName, phase) {
54
+ return `${featureName || "global"}-${phase}`;
55
+ }
56
+ personaKey(featureName, id) {
57
+ return `${featureName}-impl-persona-${id}`;
58
+ }
59
+ async writeStdin(phase, featureName, text, personaId) {
60
+ const procKey = personaId && featureName
61
+ ? this.personaKey(featureName, personaId)
62
+ : this.phaseKey(featureName, phase);
63
+ const child = this.activeProcesses.get(procKey);
64
+ if (child) {
65
+ // The frontend xterm sends raw keystrokes (arrow keys, Enter as '\r', etc.)
66
+ // so we forward them verbatim — this is what lets interactive pickers like
67
+ // the clarify Q&A be navigated, not just line-based answers.
68
+ child.write(text);
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ async resize(phase, featureName, cols, rows, personaId) {
74
+ const procKey = personaId && featureName
75
+ ? this.personaKey(featureName, personaId)
76
+ : this.phaseKey(featureName, phase);
77
+ const child = this.activeProcesses.get(procKey);
78
+ if (!child)
79
+ return false;
80
+ try {
81
+ child.resize(cols, rows);
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ async stop(phase, featureName, personaId) {
89
+ const procKey = personaId && featureName
90
+ ? this.personaKey(featureName, personaId)
91
+ : this.phaseKey(featureName, phase);
92
+ const child = this.activeProcesses.get(procKey);
93
+ if (child) {
94
+ try {
95
+ child.kill();
96
+ this.activeProcesses.delete(procKey);
97
+ return true;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ return false;
104
+ }
52
105
  async runPhase(workspacePath, phase, featureName, agentConfig, userPrompt, onData) {
53
- const specArg = phase !== 'constitution' && featureName ? `specs/${featureName}` : null;
54
- const slashCmd = `${PHASE_COMMANDS[phase]}${specArg ? ` ${specArg}` : ''}`;
106
+ const specArg = phase !== "constitution" && featureName ? `specs/${featureName}` : null;
107
+ const slashCmd = `${PHASE_COMMANDS[phase]}${specArg ? ` ${specArg}` : ""}`;
55
108
  const context = userPrompt?.trim();
56
109
  const fullPrompt = context ? `${context}\n\n${slashCmd}` : slashCmd;
110
+ return this.spawnAgent({
111
+ workspacePath,
112
+ procKey: this.phaseKey(featureName, phase),
113
+ doneTag: phase,
114
+ agentConfig,
115
+ fullPrompt,
116
+ onData,
117
+ });
118
+ }
119
+ async runPersona(workspacePath, featureName, persona, agentConfig, onData) {
120
+ // Personas always operate on a specific feature's artifacts.
121
+ const fullPrompt = `${persona.command} specs/${featureName}`;
122
+ return this.spawnAgent({
123
+ workspacePath,
124
+ procKey: this.personaKey(featureName, persona.id),
125
+ doneTag: `persona:${persona.id}`,
126
+ agentConfig,
127
+ fullPrompt,
128
+ onData,
129
+ persona,
130
+ });
131
+ }
132
+ // Shared spawn path for both phase and persona runs: spawns the agent CLI
133
+ // under a PTY (so interactive CLIs detect a TTY), streams output, and resolves
134
+ // with the exit code.
135
+ spawnAgent(opts) {
136
+ const { workspacePath, procKey, doneTag, agentConfig, fullPrompt, onData } = opts;
57
137
  const { cmd, args, stdin } = this.buildSpawnArgs(agentConfig, fullPrompt);
58
- const isWin = process.platform === 'win32';
59
138
  return new Promise((resolve) => {
60
- onData?.(`Running: ${cmd} ${args.join(' ')}\n\n`);
61
- const child = isWin
62
- ? (0, child_process_1.spawn)(cmd, args, {
63
- cwd: workspacePath,
64
- shell: true,
65
- stdio: ['pipe', 'pipe', 'pipe']
66
- })
67
- : (() => {
68
- const shell = process.env.SHELL || '/bin/sh';
69
- const escapedArgs = [cmd, ...args].map(arg => `'${arg.replace(/'/g, "'\\''")}'`).join(' ');
70
- return (0, child_process_1.spawn)(shell, ['-l', '-c', escapedArgs], {
71
- cwd: workspacePath,
72
- stdio: ['pipe', 'pipe', 'pipe']
73
- });
74
- })();
75
- if (stdin !== undefined && child.stdin) {
76
- child.stdin.write(stdin);
77
- child.stdin.end();
78
- }
79
- child.stdout.on('data', (data) => {
80
- const text = data.toString();
81
- onData?.(text);
139
+ onData?.(`Running: ${cmd} ${args.join(" ")}\n\n`);
140
+ // Spawn the agent under a real pseudo-terminal. This is what makes
141
+ // interactive CLIs (e.g. the clarify Q&A) detect a TTY via isatty() and
142
+ // actually prompt the user instead of running single-shot. We still go
143
+ // through a login shell so PATH/aliases resolve the agent binary the same
144
+ // way they would in the user's own terminal.
145
+ const isWin = process.platform === "win32";
146
+ const ptyFile = isWin
147
+ ? process.env.COMSPEC || "cmd.exe"
148
+ : process.env.SHELL || "/bin/bash";
149
+ const ptyArgs = isWin
150
+ ? ["/c", [cmd, ...args].join(" ")]
151
+ : [
152
+ "-l",
153
+ "-c",
154
+ [cmd, ...args]
155
+ .map((a) => `'${a.replace(/'/g, "'\\''")}'`)
156
+ .join(" "),
157
+ ];
158
+ const child = (0, ptyLoader_1.loadPty)().spawn(ptyFile, ptyArgs, {
159
+ name: "xterm-256color",
160
+ cols: 120,
161
+ rows: 30,
162
+ cwd: workspacePath,
163
+ env: {
164
+ ...process.env,
165
+ TERM: "xterm-256color",
166
+ ...(opts.persona
167
+ ? {
168
+ SPECKIT_PERSONA_ID: opts.persona.id || "",
169
+ SPECKIT_PERSONA_LABEL: opts.persona.label || "",
170
+ SPECKIT_PERSONA_MODEL: opts.persona.model || "",
171
+ SPECKIT_PERSONA_SYSTEM_PROMPT: opts.persona.systemPrompt || "",
172
+ SPECKIT_PERSONA_CAPABILITIES: Array.isArray(opts.persona.capabilities)
173
+ ? opts.persona.capabilities.join(",")
174
+ : "",
175
+ SPECKIT_PERSONA_TOOLS: Array.isArray(opts.persona.tools)
176
+ ? opts.persona.tools.join(",")
177
+ : "",
178
+ }
179
+ : {}),
180
+ },
82
181
  });
83
- child.stderr.on('data', (data) => {
84
- const text = data.toString();
85
- onData?.(text);
182
+ this.activeProcesses.set(procKey, child);
183
+ // For agents that receive their prompt over stdin, feed it now and send
184
+ // EOF (Ctrl-D) so the single-shot CLI starts processing. Agents that take
185
+ // the prompt as an argument keep the channel open so the user can answer
186
+ // interactive prompts (e.g. clarify) through writeStdin().
187
+ if (stdin !== undefined) {
188
+ child.write(stdin);
189
+ child.write("\x04");
190
+ }
191
+ child.onData((data) => {
192
+ onData?.(data);
86
193
  });
87
- child.on('close', (code) => {
88
- const exitCode = code === null ? -1 : code;
194
+ child.onExit(({ exitCode }) => {
195
+ this.activeProcesses.delete(procKey);
89
196
  onData?.(`\nProcess exited with code ${exitCode}\n`);
90
197
  // Write the phase done file so local watch logs it
91
198
  try {
92
- const runtimeDir = path.join(workspacePath, '.specify', '.runtime');
199
+ const runtimeDir = path.join(workspacePath, ".specify", ".runtime");
93
200
  if (!fs.existsSync(runtimeDir)) {
94
201
  fs.mkdirSync(runtimeDir, { recursive: true });
95
202
  }
96
- fs.writeFileSync(path.join(runtimeDir, 'phase-done.txt'), `${phase}:${exitCode}`, 'utf-8');
203
+ fs.writeFileSync(path.join(runtimeDir, "phase-done.txt"), `${doneTag}:${exitCode}`, "utf-8");
97
204
  }
98
205
  catch {
99
206
  // ignore
100
207
  }
101
208
  resolve(exitCode);
102
209
  });
103
- child.on('error', (err) => {
104
- onData?.(`\nFailed to start process: ${err.message}\n`);
105
- resolve(-1);
106
- });
107
210
  });
108
211
  }
109
212
  buildSpawnArgs(config, prompt) {
110
213
  const agentType = config.agentType;
111
214
  const cliPath = config.agentPath || this.getDefaultCli(agentType);
112
215
  switch (agentType) {
113
- case 'claude':
216
+ case "claude":
114
217
  return {
115
218
  cmd: cliPath,
116
- args: ['--permission-mode', 'bypassPermissions', prompt]
219
+ args: ["--permission-mode", "bypassPermissions", prompt],
117
220
  };
118
- case 'gemini':
221
+ case "gemini":
119
222
  return {
120
223
  cmd: cliPath,
121
224
  args: [],
122
- stdin: prompt
225
+ stdin: prompt,
123
226
  };
124
- case 'copilot':
227
+ case "copilot":
125
228
  return {
126
229
  cmd: cliPath,
127
- args: [prompt]
230
+ args: [prompt],
128
231
  };
129
- case 'openai':
232
+ case "openai":
130
233
  return {
131
234
  cmd: cliPath,
132
- args: ['exec', '-'],
133
- stdin: prompt
235
+ args: ["exec", "-"],
236
+ stdin: prompt,
134
237
  };
135
- case 'custom':
238
+ case "custom":
136
239
  if (config.customCommand) {
137
- const parts = config.customCommand.split(' ');
240
+ const parts = config.customCommand.split(" ");
138
241
  const cmd = parts[0];
139
- const args = parts.slice(1).map(arg => arg === '{{prompt}}' ? prompt : arg);
242
+ const args = parts
243
+ .slice(1)
244
+ .map((arg) => (arg === "{{prompt}}" ? prompt : arg));
140
245
  // If {{prompt}} isn't in args, append it
141
- if (!config.customCommand.includes('{{prompt}}')) {
246
+ if (!config.customCommand.includes("{{prompt}}")) {
142
247
  args.push(prompt);
143
248
  }
144
249
  return { cmd, args };
145
250
  }
146
- return { cmd: 'specify', args: [prompt] };
251
+ return { cmd: "specify", args: [prompt] };
147
252
  default:
148
253
  return { cmd: cliPath, args: [prompt] };
149
254
  }
150
255
  }
151
256
  getDefaultCli(agentType) {
152
257
  const defaults = {
153
- claude: 'claude',
154
- gemini: 'gemini',
155
- copilot: 'ghcs',
156
- openai: 'codex'
258
+ claude: "claude",
259
+ gemini: "gemini",
260
+ copilot: "ghcs",
261
+ openai: "codex",
157
262
  };
158
- return defaults[agentType] || 'specify';
263
+ return defaults[agentType] || "specify";
159
264
  }
160
265
  }
161
266
  exports.ProcessAgentRunner = ProcessAgentRunner;
@@ -61,7 +61,12 @@ class FSWorkspaceRepository {
61
61
  else {
62
62
  state.constitutionPhase.filePath = null;
63
63
  state.constitutionPhase.content = null;
64
- state.constitutionPhase.status = 'idle';
64
+ // Don't clobber explicit user/runtime states when no file is on disk:
65
+ // 'running' (agent still generating) and 'approved' (a stored user decision,
66
+ // e.g. advanced via Kanban drag) must survive reconciliation.
67
+ if (!this.isUserState(state.constitutionPhase.status)) {
68
+ state.constitutionPhase.status = 'idle';
69
+ }
65
70
  }
66
71
  // 2. Reconcile Features in specs/
67
72
  const specsDir = path.join(workspacePath, 'specs');
@@ -100,7 +105,9 @@ class FSWorkspaceRepository {
100
105
  else {
101
106
  ps.filePath = null;
102
107
  ps.content = null;
103
- ps.status = 'idle';
108
+ if (!this.isUserState(ps.status)) {
109
+ ps.status = 'idle';
110
+ }
104
111
  }
105
112
  }
106
113
  // Reconcile clarification (clarification.md or clarify.md)
@@ -118,7 +125,9 @@ class FSWorkspaceRepository {
118
125
  else {
119
126
  clarifyPhase.filePath = null;
120
127
  clarifyPhase.content = null;
121
- clarifyPhase.status = 'idle';
128
+ if (!this.isUserState(clarifyPhase.status)) {
129
+ clarifyPhase.status = 'idle';
130
+ }
122
131
  }
123
132
  // Implementation phase doesn't have its own file, but we can resolve its status.
124
133
  // If specs/[feature]/tasks.md exists, and all checkboxes are checked, it could be complete.
@@ -127,6 +136,24 @@ class FSWorkspaceRepository {
127
136
  // Make sure its filePath is null (since there is no implementation.md file)
128
137
  implPhase.filePath = null;
129
138
  implPhase.content = null;
139
+ // Reconcile review-gate personas against their report files. Statuses
140
+ // saved by the gate are kept; this is a safety net that also reflects a
141
+ // report written/edited out of band. Never downgrade a 'running' persona.
142
+ if (implPhase.personas) {
143
+ for (const persona of implPhase.personas) {
144
+ const reportFp = path.join(specsDir, feature.name, 'reviews', `${persona.id}.md`);
145
+ if (fs.existsSync(reportFp)) {
146
+ persona.reportPath = path.join('specs', feature.name, 'reviews', `${persona.id}.md`);
147
+ if (persona.status !== 'running') {
148
+ const verdict = fs.readFileSync(reportFp, 'utf-8').toUpperCase();
149
+ if (/VERDICT:\s*FAIL/.test(verdict))
150
+ persona.status = 'failed';
151
+ else if (/VERDICT:\s*PASS/.test(verdict))
152
+ persona.status = 'passed';
153
+ }
154
+ }
155
+ }
156
+ }
130
157
  }
131
158
  }
132
159
  else {
@@ -156,7 +183,9 @@ class FSWorkspaceRepository {
156
183
  phases: f.phases.map(p => ({
157
184
  phase: p.phase,
158
185
  status: p.status,
159
- stale: p.stale
186
+ stale: p.stale,
187
+ // Persist the implementation review-gate persona statuses.
188
+ ...(p.personas ? { personas: p.personas.map(ps => ({ id: ps.id, status: ps.status })) } : {})
160
189
  }))
161
190
  })),
162
191
  activeFeatureName: state.activeFeatureName
@@ -245,6 +274,9 @@ class FSWorkspaceRepository {
245
274
  phases: FEATURE_PHASES.map(p => this.makePhaseState(p))
246
275
  };
247
276
  }
277
+ isUserState(status) {
278
+ return status === 'running' || status === 'approved';
279
+ }
248
280
  loadStateFile(workspacePath) {
249
281
  const statePath = path.join(workspacePath, '.specify', '.runtime', 'workflow-state.json');
250
282
  if (!fs.existsSync(statePath)) {
@@ -267,7 +299,10 @@ class FSWorkspaceRepository {
267
299
  status: savedPhase?.status || 'idle',
268
300
  stale: savedPhase?.stale || false,
269
301
  filePath: null,
270
- content: null
302
+ content: null,
303
+ ...(Array.isArray(savedPhase?.personas)
304
+ ? { personas: savedPhase.personas.map((ps) => ({ id: ps.id, status: ps.status || 'idle' })) }
305
+ : {})
271
306
  };
272
307
  })
273
308
  }));
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadPty = loadPty;
37
+ exports._clearPtyCacheForTesting = _clearPtyCacheForTesting;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const child_process_1 = require("child_process");
41
+ let cached = null;
42
+ class PureJsPtyLike {
43
+ child;
44
+ constructor(child) {
45
+ this.child = child;
46
+ }
47
+ write(data) {
48
+ this.child.stdin?.write(data);
49
+ }
50
+ resize(cols, rows) {
51
+ // No-op for standard child_process pipes
52
+ }
53
+ kill(signal) {
54
+ this.child.kill(signal);
55
+ }
56
+ onData(cb) {
57
+ this.child.stdout?.on("data", (chunk) => cb(chunk.toString()));
58
+ this.child.stderr?.on("data", (chunk) => cb(chunk.toString()));
59
+ }
60
+ onExit(cb) {
61
+ this.child.on("exit", (code, signal) => {
62
+ cb({ exitCode: code ?? 0, signal: signal ? 1 : undefined });
63
+ });
64
+ }
65
+ }
66
+ const pureJsPtyModule = {
67
+ spawn(file, args, options) {
68
+ const cp = (0, child_process_1.spawn)(file, args, {
69
+ cwd: options.cwd,
70
+ env: options.env,
71
+ });
72
+ return new PureJsPtyLike(cp);
73
+ },
74
+ };
75
+ /**
76
+ * Loads node-pty defensively.
77
+ *
78
+ * node-pty relies on a native `spawn-helper` binary (on POSIX) that must be
79
+ * executable. Some package managers / extraction paths drop the exec bit when
80
+ * unpacking the prebuilt binaries, which makes pty.spawn fail with
81
+ * "posix_spawnp failed". We restore the exec bit before first use so the
82
+ * workspace shell and interactive agent phases work regardless of how the
83
+ * dependency was installed.
84
+ */
85
+ function loadPty() {
86
+ if (cached)
87
+ return cached;
88
+ if (process.env.SPECKIT_TEST_FORCE_FALLBACK === "true") {
89
+ cached = pureJsPtyModule;
90
+ return pureJsPtyModule;
91
+ }
92
+ try {
93
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
94
+ const pty = require("node-pty");
95
+ if (process.platform !== "win32") {
96
+ try {
97
+ const ptyDir = path.dirname(require.resolve("node-pty/package.json"));
98
+ const prebuilds = path.join(ptyDir, "prebuilds");
99
+ if (fs.existsSync(prebuilds)) {
100
+ for (const entry of fs.readdirSync(prebuilds)) {
101
+ const helper = path.join(prebuilds, entry, "spawn-helper");
102
+ if (fs.existsSync(helper)) {
103
+ try {
104
+ fs.chmodSync(helper, 0o755);
105
+ }
106
+ catch {
107
+ // best effort
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ catch {
114
+ // best effort — if anything fails we still return the module
115
+ }
116
+ }
117
+ cached = pty;
118
+ return pty;
119
+ }
120
+ catch (err) {
121
+ console.warn("node-pty failed to load, falling back to pure child_process pty emulation.");
122
+ cached = pureJsPtyModule;
123
+ return pureJsPtyModule;
124
+ }
125
+ }
126
+ function _clearPtyCacheForTesting() {
127
+ cached = null;
128
+ }