speckit-assistant 0.1.3 → 0.1.4

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 (175) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +57 -21
  3. package/.next/app-path-routes-manifest.json +7 -2
  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/4.pack +0 -0
  66. package/.next/cache/webpack/server-production/5.pack +0 -0
  67. package/.next/cache/webpack/server-production/6.pack +0 -0
  68. package/.next/cache/webpack/server-production/7.pack +0 -0
  69. package/.next/cache/webpack/server-production/9.pack +0 -0
  70. package/.next/cache/webpack/server-production/index.pack +0 -0
  71. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  72. package/.next/prerender-manifest.json +7 -7
  73. package/.next/react-loadable-manifest.json +8 -1
  74. package/.next/required-server-files.json +3 -0
  75. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  76. package/.next/server/app/_not-found.html +1 -1
  77. package/.next/server/app/_not-found.rsc +2 -2
  78. package/.next/server/app/api/feature/route.js +1 -1
  79. package/.next/server/app/api/feature/route.js.nft.json +1 -1
  80. package/.next/server/app/api/feature/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/file/route.js +1 -1
  82. package/.next/server/app/api/file/route.js.nft.json +1 -1
  83. package/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  84. package/.next/server/app/api/git/route.js +1 -0
  85. package/.next/server/app/api/git/route.js.nft.json +1 -0
  86. package/.next/server/app/api/git/route_client-reference-manifest.js +1 -0
  87. package/.next/server/app/api/personas/route.js +1 -0
  88. package/.next/server/app/api/personas/route.js.nft.json +1 -0
  89. package/.next/server/app/api/personas/route_client-reference-manifest.js +1 -0
  90. package/.next/server/app/api/phase/route.js +3 -3
  91. package/.next/server/app/api/phase/route.js.nft.json +1 -1
  92. package/.next/server/app/api/phase/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/state/route.js +1 -1
  94. package/.next/server/app/api/state/route.js.nft.json +1 -1
  95. package/.next/server/app/api/state/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/state/watch/route.js +2 -2
  97. package/.next/server/app/api/state/watch/route.js.nft.json +1 -1
  98. package/.next/server/app/api/state/watch/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/task/route.js +1 -1
  100. package/.next/server/app/api/task/route.js.nft.json +1 -1
  101. package/.next/server/app/api/task/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/terminal/input/route.js +1 -0
  103. package/.next/server/app/api/terminal/input/route.js.nft.json +1 -0
  104. package/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -0
  105. package/.next/server/app/api/terminal/resize/route.js +1 -0
  106. package/.next/server/app/api/terminal/resize/route.js.nft.json +1 -0
  107. package/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -0
  108. package/.next/server/app/api/terminal/stream/route.js +3 -0
  109. package/.next/server/app/api/terminal/stream/route.js.nft.json +1 -0
  110. package/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -0
  111. package/.next/server/app/index.html +1 -1
  112. package/.next/server/app/index.rsc +4 -4
  113. package/.next/server/app/page.js +26 -7
  114. package/.next/server/app/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app-paths-manifest.json +7 -2
  116. package/.next/server/chunks/607.js +2 -2
  117. package/.next/server/chunks/897.js +13 -7
  118. package/.next/server/middleware-build-manifest.js +1 -1
  119. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  120. package/.next/server/pages/404.html +1 -1
  121. package/.next/server/pages/500.html +1 -1
  122. package/.next/static/ELGpn5csIINP2EpeEMCo6/_buildManifest.js +1 -0
  123. package/.next/static/chunks/111acf76-29d5e5905666f1e0.js +18 -0
  124. package/.next/static/chunks/343.91af0d46f6df0f05.js +1 -0
  125. package/.next/static/chunks/873-a995367ae371a5e4.js +1 -0
  126. package/.next/static/chunks/app/api/feature/route-b4fbc89d13fef983.js +1 -0
  127. package/.next/static/chunks/app/api/file/route-b4fbc89d13fef983.js +1 -0
  128. package/.next/static/chunks/app/api/git/route-b4fbc89d13fef983.js +1 -0
  129. package/.next/static/chunks/app/api/personas/route-b4fbc89d13fef983.js +1 -0
  130. package/.next/static/chunks/app/api/phase/route-b4fbc89d13fef983.js +1 -0
  131. package/.next/static/chunks/app/api/state/route-b4fbc89d13fef983.js +1 -0
  132. package/.next/static/chunks/app/api/state/watch/route-b4fbc89d13fef983.js +1 -0
  133. package/.next/static/chunks/app/api/task/route-b4fbc89d13fef983.js +1 -0
  134. package/.next/static/chunks/app/api/terminal/input/route-b4fbc89d13fef983.js +1 -0
  135. package/.next/static/chunks/app/api/terminal/resize/route-b4fbc89d13fef983.js +1 -0
  136. package/.next/static/chunks/app/api/terminal/stream/route-b4fbc89d13fef983.js +1 -0
  137. package/.next/static/chunks/app/page-63ac4ff563941667.js +1 -0
  138. package/.next/static/chunks/webpack-c997c7bd3e8f7cdf.js +1 -0
  139. package/.next/static/css/31544403e38a69f8.css +3 -0
  140. package/.next/static/css/{008a05b0ad6b854a.css → af3f40f14f702ff7.css} +1 -1
  141. package/.next/trace +3 -3
  142. package/.next/types/app/api/git/route.ts +347 -0
  143. package/.next/types/app/api/personas/route.ts +347 -0
  144. package/.next/types/app/api/terminal/input/route.ts +347 -0
  145. package/.next/types/app/api/terminal/resize/route.ts +347 -0
  146. package/.next/types/app/api/terminal/stream/route.ts +347 -0
  147. package/.next/types/routes.d.ts +6 -1
  148. package/.next/types/validator.ts +45 -0
  149. package/bin/adapters/primary/api/terminalManager.js +92 -0
  150. package/bin/adapters/secondary/agent/ProcessAgentRunner.js +124 -33
  151. package/bin/adapters/secondary/fs/FSWorkspaceRepository.js +40 -5
  152. package/bin/adapters/secondary/pty/ptyLoader.js +128 -0
  153. package/bin/app/api/git/route.js +156 -0
  154. package/bin/app/api/personas/route.js +76 -0
  155. package/bin/app/api/phase/route.js +18 -5
  156. package/bin/app/api/state/watch/route.js +7 -2
  157. package/bin/app/api/terminal/input/route.js +18 -0
  158. package/bin/app/api/terminal/resize/route.js +18 -0
  159. package/bin/app/api/terminal/stream/route.js +44 -0
  160. package/bin/domain/models/personas.js +65 -0
  161. package/bin/domain/services/WorkflowService.js +79 -5
  162. package/next.config.mjs +3 -0
  163. package/package.json +9 -1
  164. package/.next/static/Bxo1ckOxvQcndtSKD9Yna/_buildManifest.js +0 -1
  165. package/.next/static/chunks/590-a6568595ecd2a994.js +0 -1
  166. package/.next/static/chunks/app/api/feature/route-bb3c1a82e892ab58.js +0 -1
  167. package/.next/static/chunks/app/api/file/route-bb3c1a82e892ab58.js +0 -1
  168. package/.next/static/chunks/app/api/phase/route-bb3c1a82e892ab58.js +0 -1
  169. package/.next/static/chunks/app/api/state/route-bb3c1a82e892ab58.js +0 -1
  170. package/.next/static/chunks/app/api/state/watch/route-bb3c1a82e892ab58.js +0 -1
  171. package/.next/static/chunks/app/api/task/route-bb3c1a82e892ab58.js +0 -1
  172. package/.next/static/chunks/app/page-8a5248f7704cde29.js +0 -1
  173. package/.next/static/chunks/webpack-c460f8e58a9e9eff.js +0 -1
  174. package/.next/static/css/6fd2e11db3a59771.css +0 -3
  175. /package/.next/static/{Bxo1ckOxvQcndtSKD9Yna → ELGpn5csIINP2EpeEMCo6}/_ssgManifest.js +0 -0
@@ -34,7 +34,7 @@ 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 = {
@@ -49,43 +49,138 @@ const PHASE_COMMANDS = {
49
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
106
  const specArg = phase !== 'constitution' && featureName ? `specs/${featureName}` : null;
54
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
139
  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);
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
+ : ['-l', '-c', [cmd, ...args].map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')];
152
+ const child = (0, ptyLoader_1.loadPty)().spawn(ptyFile, ptyArgs, {
153
+ name: 'xterm-256color',
154
+ cols: 120,
155
+ rows: 30,
156
+ cwd: workspacePath,
157
+ env: {
158
+ ...process.env,
159
+ TERM: 'xterm-256color',
160
+ ...(opts.persona ? {
161
+ SPECKIT_PERSONA_ID: opts.persona.id || '',
162
+ SPECKIT_PERSONA_LABEL: opts.persona.label || '',
163
+ SPECKIT_PERSONA_MODEL: opts.persona.model || '',
164
+ SPECKIT_PERSONA_SYSTEM_PROMPT: opts.persona.systemPrompt || '',
165
+ SPECKIT_PERSONA_CAPABILITIES: Array.isArray(opts.persona.capabilities) ? opts.persona.capabilities.join(',') : '',
166
+ SPECKIT_PERSONA_TOOLS: Array.isArray(opts.persona.tools) ? opts.persona.tools.join(',') : '',
167
+ } : {})
168
+ },
82
169
  });
83
- child.stderr.on('data', (data) => {
84
- const text = data.toString();
85
- onData?.(text);
170
+ this.activeProcesses.set(procKey, child);
171
+ // For agents that receive their prompt over stdin, feed it now and send
172
+ // EOF (Ctrl-D) so the single-shot CLI starts processing. Agents that take
173
+ // the prompt as an argument keep the channel open so the user can answer
174
+ // interactive prompts (e.g. clarify) through writeStdin().
175
+ if (stdin !== undefined) {
176
+ child.write(stdin);
177
+ child.write('\x04');
178
+ }
179
+ child.onData((data) => {
180
+ onData?.(data);
86
181
  });
87
- child.on('close', (code) => {
88
- const exitCode = code === null ? -1 : code;
182
+ child.onExit(({ exitCode }) => {
183
+ this.activeProcesses.delete(procKey);
89
184
  onData?.(`\nProcess exited with code ${exitCode}\n`);
90
185
  // Write the phase done file so local watch logs it
91
186
  try {
@@ -93,17 +188,13 @@ class ProcessAgentRunner {
93
188
  if (!fs.existsSync(runtimeDir)) {
94
189
  fs.mkdirSync(runtimeDir, { recursive: true });
95
190
  }
96
- fs.writeFileSync(path.join(runtimeDir, 'phase-done.txt'), `${phase}:${exitCode}`, 'utf-8');
191
+ fs.writeFileSync(path.join(runtimeDir, 'phase-done.txt'), `${doneTag}:${exitCode}`, 'utf-8');
97
192
  }
98
193
  catch {
99
194
  // ignore
100
195
  }
101
196
  resolve(exitCode);
102
197
  });
103
- child.on('error', (err) => {
104
- onData?.(`\nFailed to start process: ${err.message}\n`);
105
- resolve(-1);
106
- });
107
198
  });
108
199
  }
109
200
  buildSpawnArgs(config, prompt) {
@@ -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
+ }
@@ -0,0 +1,156 @@
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.GET = GET;
37
+ const server_1 = require("next/server");
38
+ const utils_1 = require("../../../adapters/primary/api/utils");
39
+ const child_process_1 = require("child_process");
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ function runGit(cmd, cwd) {
43
+ try {
44
+ return (0, child_process_1.execSync)(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
45
+ }
46
+ catch {
47
+ return '';
48
+ }
49
+ }
50
+ function getRefBranch(cwd) {
51
+ const output = runGit('git branch --format="%(refname:short)"', cwd);
52
+ const branches = output.split('\n').map(b => b.trim());
53
+ if (branches.includes('main'))
54
+ return 'main';
55
+ if (branches.includes('master'))
56
+ return 'master';
57
+ return 'HEAD';
58
+ }
59
+ async function GET(req) {
60
+ try {
61
+ const { searchParams } = new URL(req.url);
62
+ const action = searchParams.get('action');
63
+ const filePath = searchParams.get('file');
64
+ const workspacePath = (0, utils_1.getWorkspacePath)();
65
+ // Check if git is initialized
66
+ if (!fs.existsSync(path.join(workspacePath, '.git'))) {
67
+ return server_1.NextResponse.json({
68
+ files: [],
69
+ log: [],
70
+ error: 'Git repository not found in workspace.'
71
+ });
72
+ }
73
+ const ref = getRefBranch(workspacePath);
74
+ if (action === 'diff' && filePath) {
75
+ const absPath = path.resolve(workspacePath, filePath);
76
+ // Safety check: ensure file is inside workspace
77
+ const normWorkspace = path.normalize(workspacePath).replace(/\\/g, '/').toLowerCase();
78
+ const normFile = path.normalize(absPath).replace(/\\/g, '/').toLowerCase();
79
+ if (!normFile.startsWith(normWorkspace)) {
80
+ return server_1.NextResponse.json({ error: 'Access denied' }, { status: 403 });
81
+ }
82
+ // Check if file is untracked
83
+ const statusOutput = runGit(`git status --porcelain "${filePath}"`, workspacePath);
84
+ const isUntracked = statusOutput.startsWith('??');
85
+ if (isUntracked && fs.existsSync(absPath)) {
86
+ // Return file contents prefixed with + to simulate unified diff for new file
87
+ const content = fs.readFileSync(absPath, 'utf-8');
88
+ const diffLines = content.split('\n').map(line => `+${line}`).join('\n');
89
+ return server_1.NextResponse.json({ diff: diffLines });
90
+ }
91
+ const diffOutput = runGit(`git diff ${ref} -- "${filePath}"`, workspacePath);
92
+ return server_1.NextResponse.json({ diff: diffOutput || 'No modifications found.' });
93
+ }
94
+ if (action === 'status') {
95
+ // 1. Get modified files with stats
96
+ const numstatOutput = runGit(`git diff ${ref} --numstat`, workspacePath);
97
+ const filesMap = {};
98
+ if (numstatOutput) {
99
+ numstatOutput.split('\n').forEach(line => {
100
+ const parts = line.split('\t');
101
+ if (parts.length >= 3) {
102
+ const additions = parseInt(parts[0], 10) || 0;
103
+ const deletions = parseInt(parts[1], 10) || 0;
104
+ const relPath = parts[2].trim();
105
+ const ext = path.extname(relPath).toLowerCase();
106
+ const isTest = relPath.endsWith('.test.ts') || relPath.endsWith('.test.js') || relPath.endsWith('.spec.ts') || relPath.endsWith('.spec.js');
107
+ const type = isTest ? 'test'
108
+ : (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx' ? 'code' : 'doc');
109
+ filesMap[relPath] = { path: relPath, additions, deletions, type };
110
+ }
111
+ });
112
+ }
113
+ // 2. Get untracked files and append them
114
+ const statusOutput = runGit('git status --porcelain', workspacePath);
115
+ if (statusOutput) {
116
+ statusOutput.split('\n').forEach(line => {
117
+ if (line.startsWith('??')) {
118
+ const relPath = line.substring(3).trim();
119
+ if (!filesMap[relPath]) {
120
+ // Estimate lines as additions
121
+ let additions = 0;
122
+ const absPath = path.join(workspacePath, relPath);
123
+ try {
124
+ if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
125
+ additions = fs.readFileSync(absPath, 'utf-8').split('\n').length;
126
+ }
127
+ }
128
+ catch {
129
+ // ignore
130
+ }
131
+ const ext = path.extname(relPath).toLowerCase();
132
+ const isTest = relPath.endsWith('.test.ts') || relPath.endsWith('.test.js') || relPath.endsWith('.spec.ts') || relPath.endsWith('.spec.js');
133
+ const type = isTest ? 'test'
134
+ : (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx' ? 'code' : 'doc');
135
+ filesMap[relPath] = { path: relPath, additions, deletions: 0, type };
136
+ }
137
+ }
138
+ });
139
+ }
140
+ const files = Object.values(filesMap);
141
+ // 3. Get Git Log
142
+ const logOutput = runGit('git log -n 10 --pretty=format:"%h|%s|%an|%ar"', workspacePath);
143
+ const log = logOutput ? logOutput.split('\n').map(line => {
144
+ const [hash, message, author, date] = line.split('|');
145
+ return { hash, message, author, date };
146
+ }) : [];
147
+ // 4. Get active branch name
148
+ const branch = runGit('git rev-parse --abbrev-ref HEAD', workspacePath) || 'HEAD';
149
+ return server_1.NextResponse.json({ files, log, branch });
150
+ }
151
+ return server_1.NextResponse.json({ error: 'Invalid action' }, { status: 400 });
152
+ }
153
+ catch (err) {
154
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
155
+ }
156
+ }
@@ -0,0 +1,76 @@
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.GET = GET;
37
+ exports.POST = POST;
38
+ const server_1 = require("next/server");
39
+ const utils_1 = require("../../../adapters/primary/api/utils");
40
+ const personas_1 = require("../../../domain/models/personas");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ async function GET() {
44
+ try {
45
+ const workspacePath = (0, utils_1.getWorkspacePath)();
46
+ const configPath = path.join(workspacePath, '.specify', 'personas-config.json');
47
+ if (!fs.existsSync(configPath)) {
48
+ return server_1.NextResponse.json(personas_1.DEFAULT_PERSONAS);
49
+ }
50
+ const content = fs.readFileSync(configPath, 'utf-8');
51
+ const parsed = JSON.parse(content);
52
+ return server_1.NextResponse.json(parsed);
53
+ }
54
+ catch (err) {
55
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
56
+ }
57
+ }
58
+ async function POST(req) {
59
+ try {
60
+ const workspacePath = (0, utils_1.getWorkspacePath)();
61
+ const body = await req.json();
62
+ if (!Array.isArray(body)) {
63
+ return server_1.NextResponse.json({ error: 'Body must be an array of persona configs' }, { status: 400 });
64
+ }
65
+ const configDir = path.join(workspacePath, '.specify');
66
+ if (!fs.existsSync(configDir)) {
67
+ fs.mkdirSync(configDir, { recursive: true });
68
+ }
69
+ const configPath = path.join(configDir, 'personas-config.json');
70
+ fs.writeFileSync(configPath, JSON.stringify(body, null, 2), 'utf-8');
71
+ return server_1.NextResponse.json({ success: true, personas: body });
72
+ }
73
+ catch (err) {
74
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
75
+ }
76
+ }