speckit-assistant 0.1.0

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 (180) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +71 -0
  3. package/.next/app-path-routes-manifest.json +10 -0
  4. package/.next/build-manifest.json +33 -0
  5. package/.next/cache/.previewinfo +1 -0
  6. package/.next/cache/.rscinfo +1 -0
  7. package/.next/cache/webpack/client-production/0.pack +0 -0
  8. package/.next/cache/webpack/client-production/1.pack +0 -0
  9. package/.next/cache/webpack/client-production/10.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/14.pack +0 -0
  14. package/.next/cache/webpack/client-production/15.pack +0 -0
  15. package/.next/cache/webpack/client-production/16.pack +0 -0
  16. package/.next/cache/webpack/client-production/17.pack +0 -0
  17. package/.next/cache/webpack/client-production/18.pack +0 -0
  18. package/.next/cache/webpack/client-production/19.pack +0 -0
  19. package/.next/cache/webpack/client-production/2.pack +0 -0
  20. package/.next/cache/webpack/client-production/20.pack +0 -0
  21. package/.next/cache/webpack/client-production/21.pack +0 -0
  22. package/.next/cache/webpack/client-production/22.pack +0 -0
  23. package/.next/cache/webpack/client-production/3.pack +0 -0
  24. package/.next/cache/webpack/client-production/4.pack +0 -0
  25. package/.next/cache/webpack/client-production/5.pack +0 -0
  26. package/.next/cache/webpack/client-production/6.pack +0 -0
  27. package/.next/cache/webpack/client-production/7.pack +0 -0
  28. package/.next/cache/webpack/client-production/8.pack +0 -0
  29. package/.next/cache/webpack/client-production/9.pack +0 -0
  30. package/.next/cache/webpack/client-production/index.pack +0 -0
  31. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  32. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  33. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  34. package/.next/cache/webpack/server-production/0.pack +0 -0
  35. package/.next/cache/webpack/server-production/1.pack +0 -0
  36. package/.next/cache/webpack/server-production/10.pack +0 -0
  37. package/.next/cache/webpack/server-production/11.pack +0 -0
  38. package/.next/cache/webpack/server-production/12.pack +0 -0
  39. package/.next/cache/webpack/server-production/13.pack +0 -0
  40. package/.next/cache/webpack/server-production/14.pack +0 -0
  41. package/.next/cache/webpack/server-production/15.pack +0 -0
  42. package/.next/cache/webpack/server-production/16.pack +0 -0
  43. package/.next/cache/webpack/server-production/17.pack +0 -0
  44. package/.next/cache/webpack/server-production/18.pack +0 -0
  45. package/.next/cache/webpack/server-production/19.pack +0 -0
  46. package/.next/cache/webpack/server-production/2.pack +0 -0
  47. package/.next/cache/webpack/server-production/20.pack +0 -0
  48. package/.next/cache/webpack/server-production/3.pack +0 -0
  49. package/.next/cache/webpack/server-production/4.pack +0 -0
  50. package/.next/cache/webpack/server-production/5.pack +0 -0
  51. package/.next/cache/webpack/server-production/6.pack +0 -0
  52. package/.next/cache/webpack/server-production/7.pack +0 -0
  53. package/.next/cache/webpack/server-production/8.pack +0 -0
  54. package/.next/cache/webpack/server-production/9.pack +0 -0
  55. package/.next/cache/webpack/server-production/index.pack +0 -0
  56. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  57. package/.next/diagnostics/build-diagnostics.json +6 -0
  58. package/.next/diagnostics/framework.json +1 -0
  59. package/.next/export-marker.json +6 -0
  60. package/.next/images-manifest.json +58 -0
  61. package/.next/next-minimal-server.js.nft.json +1 -0
  62. package/.next/next-server.js.nft.json +1 -0
  63. package/.next/package.json +1 -0
  64. package/.next/prerender-manifest.json +61 -0
  65. package/.next/react-loadable-manifest.json +1 -0
  66. package/.next/required-server-files.json +320 -0
  67. package/.next/routes-manifest.json +53 -0
  68. package/.next/server/app/_not-found/page.js +2 -0
  69. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  70. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  71. package/.next/server/app/_not-found.html +1 -0
  72. package/.next/server/app/_not-found.meta +8 -0
  73. package/.next/server/app/_not-found.rsc +15 -0
  74. package/.next/server/app/api/feature/route.js +1 -0
  75. package/.next/server/app/api/feature/route.js.nft.json +1 -0
  76. package/.next/server/app/api/feature/route_client-reference-manifest.js +1 -0
  77. package/.next/server/app/api/file/route.js +1 -0
  78. package/.next/server/app/api/file/route.js.nft.json +1 -0
  79. package/.next/server/app/api/file/route_client-reference-manifest.js +1 -0
  80. package/.next/server/app/api/phase/route.js +4 -0
  81. package/.next/server/app/api/phase/route.js.nft.json +1 -0
  82. package/.next/server/app/api/phase/route_client-reference-manifest.js +1 -0
  83. package/.next/server/app/api/state/route.js +1 -0
  84. package/.next/server/app/api/state/route.js.nft.json +1 -0
  85. package/.next/server/app/api/state/route_client-reference-manifest.js +1 -0
  86. package/.next/server/app/api/state/watch/route.js +4 -0
  87. package/.next/server/app/api/state/watch/route.js.nft.json +1 -0
  88. package/.next/server/app/api/state/watch/route_client-reference-manifest.js +1 -0
  89. package/.next/server/app/api/task/route.js +1 -0
  90. package/.next/server/app/api/task/route.js.nft.json +1 -0
  91. package/.next/server/app/api/task/route_client-reference-manifest.js +1 -0
  92. package/.next/server/app/index.html +1 -0
  93. package/.next/server/app/index.meta +7 -0
  94. package/.next/server/app/index.rsc +20 -0
  95. package/.next/server/app/page.js +14 -0
  96. package/.next/server/app/page.js.nft.json +1 -0
  97. package/.next/server/app/page_client-reference-manifest.js +1 -0
  98. package/.next/server/app-paths-manifest.json +10 -0
  99. package/.next/server/chunks/355.js +22 -0
  100. package/.next/server/chunks/6.js +9 -0
  101. package/.next/server/chunks/607.js +6 -0
  102. package/.next/server/chunks/643.js +1 -0
  103. package/.next/server/chunks/897.js +9 -0
  104. package/.next/server/functions-config-manifest.json +4 -0
  105. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  106. package/.next/server/middleware-build-manifest.js +1 -0
  107. package/.next/server/middleware-manifest.json +6 -0
  108. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  109. package/.next/server/next-font-manifest.js +1 -0
  110. package/.next/server/next-font-manifest.json +1 -0
  111. package/.next/server/pages/404.html +1 -0
  112. package/.next/server/pages/500.html +1 -0
  113. package/.next/server/pages/_app.js +1 -0
  114. package/.next/server/pages/_app.js.nft.json +1 -0
  115. package/.next/server/pages/_document.js +1 -0
  116. package/.next/server/pages/_document.js.nft.json +1 -0
  117. package/.next/server/pages/_error.js +19 -0
  118. package/.next/server/pages/_error.js.nft.json +1 -0
  119. package/.next/server/pages-manifest.json +6 -0
  120. package/.next/server/server-reference-manifest.js +1 -0
  121. package/.next/server/server-reference-manifest.json +1 -0
  122. package/.next/server/webpack-runtime.js +1 -0
  123. package/.next/static/chunks/590-a6568595ecd2a994.js +1 -0
  124. package/.next/static/chunks/8381d2c4-9707dccab70b742b.js +1 -0
  125. package/.next/static/chunks/920-f9bfc1b0d0402c3e.js +1 -0
  126. package/.next/static/chunks/acfafb44-8249079a6627ac92.js +1 -0
  127. package/.next/static/chunks/app/_not-found/page-be798b363e27e8c5.js +1 -0
  128. package/.next/static/chunks/app/api/feature/route-bb3c1a82e892ab58.js +1 -0
  129. package/.next/static/chunks/app/api/file/route-bb3c1a82e892ab58.js +1 -0
  130. package/.next/static/chunks/app/api/phase/route-bb3c1a82e892ab58.js +1 -0
  131. package/.next/static/chunks/app/api/state/route-bb3c1a82e892ab58.js +1 -0
  132. package/.next/static/chunks/app/api/state/watch/route-bb3c1a82e892ab58.js +1 -0
  133. package/.next/static/chunks/app/api/task/route-bb3c1a82e892ab58.js +1 -0
  134. package/.next/static/chunks/app/layout-3226c76a5f7f74bc.js +1 -0
  135. package/.next/static/chunks/app/page-8a5248f7704cde29.js +1 -0
  136. package/.next/static/chunks/framework-20dd4f6054cc5446.js +1 -0
  137. package/.next/static/chunks/main-91029f76ac1b7110.js +1 -0
  138. package/.next/static/chunks/main-app-b9ad56d6b1dfa941.js +1 -0
  139. package/.next/static/chunks/pages/_app-aa33dc41c3472021.js +1 -0
  140. package/.next/static/chunks/pages/_error-78b0b3b591df0e73.js +1 -0
  141. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  142. package/.next/static/chunks/webpack-c460f8e58a9e9eff.js +1 -0
  143. package/.next/static/css/008a05b0ad6b854a.css +31 -0
  144. package/.next/static/css/6fd2e11db3a59771.css +3 -0
  145. package/.next/static/fWuQ3yoHovA7Cre6u89W7/_buildManifest.js +1 -0
  146. package/.next/static/fWuQ3yoHovA7Cre6u89W7/_ssgManifest.js +1 -0
  147. package/.next/trace +3 -0
  148. package/.next/types/app/api/feature/route.ts +347 -0
  149. package/.next/types/app/api/file/route.ts +347 -0
  150. package/.next/types/app/api/phase/route.ts +347 -0
  151. package/.next/types/app/api/state/route.ts +347 -0
  152. package/.next/types/app/api/state/watch/route.ts +347 -0
  153. package/.next/types/app/api/task/route.ts +347 -0
  154. package/.next/types/app/layout.ts +84 -0
  155. package/.next/types/app/page.ts +84 -0
  156. package/.next/types/cache-life.d.ts +141 -0
  157. package/.next/types/package.json +1 -0
  158. package/.next/types/routes.d.ts +78 -0
  159. package/.next/types/validator.ts +124 -0
  160. package/LICENSE +21 -0
  161. package/README.md +284 -0
  162. package/bin/adapters/di.js +9 -0
  163. package/bin/adapters/primary/api/utils.js +57 -0
  164. package/bin/adapters/secondary/agent/ProcessAgentRunner.js +161 -0
  165. package/bin/adapters/secondary/fs/FSWorkspaceRepository.js +283 -0
  166. package/bin/app/api/feature/route.js +35 -0
  167. package/bin/app/api/file/route.js +36 -0
  168. package/bin/app/api/phase/route.js +55 -0
  169. package/bin/app/api/state/route.js +28 -0
  170. package/bin/app/api/state/watch/route.js +92 -0
  171. package/bin/app/api/task/route.js +20 -0
  172. package/bin/bin/cli.js +317 -0
  173. package/bin/cli.js +85 -0
  174. package/bin/domain/models/types.js +2 -0
  175. package/bin/domain/ports/in/WorkflowUseCases.js +2 -0
  176. package/bin/domain/ports/out/AgentRunnerPort.js +2 -0
  177. package/bin/domain/ports/out/WorkspaceRepositoryPort.js +2 -0
  178. package/bin/domain/services/WorkflowService.js +174 -0
  179. package/next.config.ts +13 -0
  180. package/package.json +53 -0
@@ -0,0 +1,161 @@
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.ProcessAgentRunner = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
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',
50
+ };
51
+ class ProcessAgentRunner {
52
+ 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}` : ''}`;
55
+ const context = userPrompt?.trim();
56
+ const fullPrompt = context ? `${context}\n\n${slashCmd}` : slashCmd;
57
+ const { cmd, args, stdin } = this.buildSpawnArgs(agentConfig, fullPrompt);
58
+ const isWin = process.platform === 'win32';
59
+ 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);
82
+ });
83
+ child.stderr.on('data', (data) => {
84
+ const text = data.toString();
85
+ onData?.(text);
86
+ });
87
+ child.on('close', (code) => {
88
+ const exitCode = code === null ? -1 : code;
89
+ onData?.(`\nProcess exited with code ${exitCode}\n`);
90
+ // Write the phase done file so local watch logs it
91
+ try {
92
+ const runtimeDir = path.join(workspacePath, '.specify', '.runtime');
93
+ if (!fs.existsSync(runtimeDir)) {
94
+ fs.mkdirSync(runtimeDir, { recursive: true });
95
+ }
96
+ fs.writeFileSync(path.join(runtimeDir, 'phase-done.txt'), `${phase}:${exitCode}`, 'utf-8');
97
+ }
98
+ catch {
99
+ // ignore
100
+ }
101
+ resolve(exitCode);
102
+ });
103
+ child.on('error', (err) => {
104
+ onData?.(`\nFailed to start process: ${err.message}\n`);
105
+ resolve(-1);
106
+ });
107
+ });
108
+ }
109
+ buildSpawnArgs(config, prompt) {
110
+ const agentType = config.agentType;
111
+ const cliPath = config.agentPath || this.getDefaultCli(agentType);
112
+ switch (agentType) {
113
+ case 'claude':
114
+ return {
115
+ cmd: cliPath,
116
+ args: ['--permission-mode', 'bypassPermissions', prompt]
117
+ };
118
+ case 'gemini':
119
+ return {
120
+ cmd: cliPath,
121
+ args: [],
122
+ stdin: prompt
123
+ };
124
+ case 'copilot':
125
+ return {
126
+ cmd: cliPath,
127
+ args: [prompt]
128
+ };
129
+ case 'openai':
130
+ return {
131
+ cmd: cliPath,
132
+ args: ['exec', '-'],
133
+ stdin: prompt
134
+ };
135
+ case 'custom':
136
+ if (config.customCommand) {
137
+ const parts = config.customCommand.split(' ');
138
+ const cmd = parts[0];
139
+ const args = parts.slice(1).map(arg => arg === '{{prompt}}' ? prompt : arg);
140
+ // If {{prompt}} isn't in args, append it
141
+ if (!config.customCommand.includes('{{prompt}}')) {
142
+ args.push(prompt);
143
+ }
144
+ return { cmd, args };
145
+ }
146
+ return { cmd: 'specify', args: [prompt] };
147
+ default:
148
+ return { cmd: cliPath, args: [prompt] };
149
+ }
150
+ }
151
+ getDefaultCli(agentType) {
152
+ const defaults = {
153
+ claude: 'claude',
154
+ gemini: 'gemini',
155
+ copilot: 'ghcs',
156
+ openai: 'codex'
157
+ };
158
+ return defaults[agentType] || 'specify';
159
+ }
160
+ }
161
+ exports.ProcessAgentRunner = ProcessAgentRunner;
@@ -0,0 +1,283 @@
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.FSWorkspaceRepository = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const FEATURE_PHASES = [
40
+ 'specification',
41
+ 'clarification',
42
+ 'planning',
43
+ 'checklist',
44
+ 'analyze',
45
+ 'tasks',
46
+ 'taskstoissues',
47
+ 'implementation'
48
+ ];
49
+ class FSWorkspaceRepository {
50
+ async getWorkflowState(workspacePath) {
51
+ const state = this.loadStateFile(workspacePath) || this.defaultWorkflowState();
52
+ // 1. Reconcile Constitution
53
+ const constitutionPath = path.join(workspacePath, '.specify', 'memory', 'constitution.md');
54
+ if (fs.existsSync(constitutionPath)) {
55
+ state.constitutionPhase.filePath = constitutionPath;
56
+ state.constitutionPhase.content = fs.readFileSync(constitutionPath, 'utf-8');
57
+ if (state.constitutionPhase.status === 'idle') {
58
+ state.constitutionPhase.status = 'awaiting_review';
59
+ }
60
+ }
61
+ else {
62
+ state.constitutionPhase.filePath = null;
63
+ state.constitutionPhase.content = null;
64
+ state.constitutionPhase.status = 'idle';
65
+ }
66
+ // 2. Reconcile Features in specs/
67
+ const specsDir = path.join(workspacePath, 'specs');
68
+ if (fs.existsSync(specsDir)) {
69
+ const entries = fs.readdirSync(specsDir, { withFileTypes: true });
70
+ const foundFeatureNames = entries
71
+ .filter(e => e.isDirectory())
72
+ .map(e => e.name);
73
+ // Add newly found features that aren't in state
74
+ for (const name of foundFeatureNames) {
75
+ if (!state.features.some(f => f.name === name)) {
76
+ state.features.push(this.makeFeatureWorkflow(name));
77
+ }
78
+ }
79
+ // Filter out deleted features
80
+ state.features = state.features.filter(f => foundFeatureNames.includes(f.name));
81
+ // Reconcile each feature's files
82
+ for (const feature of state.features) {
83
+ const fileMap = [
84
+ { file: 'spec.md', phase: 'specification' },
85
+ { file: 'plan.md', phase: 'planning' },
86
+ { file: 'checklist.md', phase: 'checklist' },
87
+ { file: 'analysis.md', phase: 'analyze' },
88
+ { file: 'tasks.md', phase: 'tasks' }
89
+ ];
90
+ for (const { file, phase } of fileMap) {
91
+ const fp = path.join(specsDir, feature.name, file);
92
+ const ps = feature.phases.find(p => p.phase === phase);
93
+ if (fs.existsSync(fp)) {
94
+ ps.filePath = fp;
95
+ ps.content = fs.readFileSync(fp, 'utf-8');
96
+ if (ps.status === 'idle') {
97
+ ps.status = 'awaiting_review';
98
+ }
99
+ }
100
+ else {
101
+ ps.filePath = null;
102
+ ps.content = null;
103
+ ps.status = 'idle';
104
+ }
105
+ }
106
+ // Reconcile clarification (clarification.md or clarify.md)
107
+ const clarifyPhase = feature.phases.find(p => p.phase === 'clarification');
108
+ const clarifyPath1 = path.join(specsDir, feature.name, 'clarification.md');
109
+ const clarifyPath2 = path.join(specsDir, feature.name, 'clarify.md');
110
+ const finalClarifyPath = fs.existsSync(clarifyPath1) ? clarifyPath1 : (fs.existsSync(clarifyPath2) ? clarifyPath2 : null);
111
+ if (finalClarifyPath) {
112
+ clarifyPhase.filePath = finalClarifyPath;
113
+ clarifyPhase.content = fs.readFileSync(finalClarifyPath, 'utf-8');
114
+ if (clarifyPhase.status === 'idle') {
115
+ clarifyPhase.status = 'awaiting_review';
116
+ }
117
+ }
118
+ else {
119
+ clarifyPhase.filePath = null;
120
+ clarifyPhase.content = null;
121
+ clarifyPhase.status = 'idle';
122
+ }
123
+ // Implementation phase doesn't have its own file, but we can resolve its status.
124
+ // If specs/[feature]/tasks.md exists, and all checkboxes are checked, it could be complete.
125
+ // It relies on tasks status or manual review.
126
+ const implPhase = feature.phases.find(p => p.phase === 'implementation');
127
+ // Make sure its filePath is null (since there is no implementation.md file)
128
+ implPhase.filePath = null;
129
+ implPhase.content = null;
130
+ }
131
+ }
132
+ else {
133
+ state.features = [];
134
+ }
135
+ if (!state.activeFeatureName && state.features.length > 0) {
136
+ state.activeFeatureName = state.features[0].name;
137
+ }
138
+ return state;
139
+ }
140
+ async saveWorkflowState(workspacePath, state) {
141
+ const runtimeDir = path.join(workspacePath, '.specify', '.runtime');
142
+ if (!fs.existsSync(runtimeDir)) {
143
+ fs.mkdirSync(runtimeDir, { recursive: true });
144
+ }
145
+ const statePath = path.join(runtimeDir, 'workflow-state.json');
146
+ // We only save metadata (statuses and stale flag) to file to avoid writing bloated contents.
147
+ // Content is read dynamically from disk during reconciliation anyway.
148
+ const minimalState = {
149
+ constitutionPhase: {
150
+ phase: state.constitutionPhase.phase,
151
+ status: state.constitutionPhase.status,
152
+ stale: state.constitutionPhase.stale
153
+ },
154
+ features: state.features.map(f => ({
155
+ name: f.name,
156
+ phases: f.phases.map(p => ({
157
+ phase: p.phase,
158
+ status: p.status,
159
+ stale: p.stale
160
+ }))
161
+ })),
162
+ activeFeatureName: state.activeFeatureName
163
+ };
164
+ fs.writeFileSync(statePath, JSON.stringify(minimalState, null, 2), 'utf-8');
165
+ }
166
+ async toggleTask(workspacePath, featureName, lineIndex, checked) {
167
+ const tasksPath = path.join(workspacePath, 'specs', featureName, 'tasks.md');
168
+ if (!fs.existsSync(tasksPath)) {
169
+ throw new Error(`Tasks file not found at ${tasksPath}`);
170
+ }
171
+ const content = fs.readFileSync(tasksPath, 'utf-8');
172
+ const lines = content.split('\n');
173
+ if (lineIndex < 0 || lineIndex >= lines.length) {
174
+ throw new Error(`Line index ${lineIndex} out of range`);
175
+ }
176
+ lines[lineIndex] = lines[lineIndex].replace(/^(\s*(?:[-*]|\d+\.)\s+\[)( |x|X)(\])/, (_, pre, _c, post) => `${pre}${checked ? 'x' : ' '}${post}`);
177
+ fs.writeFileSync(tasksPath, lines.join('\n'), 'utf-8');
178
+ // Return the updated state
179
+ return this.getWorkflowState(workspacePath);
180
+ }
181
+ async createFeature(workspacePath, name) {
182
+ const specsDir = path.join(workspacePath, 'specs');
183
+ const featureDir = path.join(specsDir, name);
184
+ if (!fs.existsSync(featureDir)) {
185
+ fs.mkdirSync(featureDir, { recursive: true });
186
+ }
187
+ // Try to run specify specify command or let the user do it via UI.
188
+ // We just create the directory. The files will be generated when the agent runs.
189
+ return this.getWorkflowState(workspacePath);
190
+ }
191
+ async deleteFeature(workspacePath, name) {
192
+ const featureDir = path.join(workspacePath, 'specs', name);
193
+ if (fs.existsSync(featureDir)) {
194
+ fs.rmSync(featureDir, { recursive: true, force: true });
195
+ }
196
+ const state = await this.getWorkflowState(workspacePath);
197
+ if (state.activeFeatureName === name) {
198
+ state.activeFeatureName = state.features.length > 0 ? state.features[0].name : null;
199
+ }
200
+ await this.saveWorkflowState(workspacePath, state);
201
+ return state;
202
+ }
203
+ async readFile(workspacePath, filePath) {
204
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(workspacePath, filePath);
205
+ if (!this.isPathSafe(workspacePath, absolutePath)) {
206
+ throw new Error('Access denied: Path is outside the workspace directory.');
207
+ }
208
+ if (!fs.existsSync(absolutePath)) {
209
+ return '';
210
+ }
211
+ return fs.readFileSync(absolutePath, 'utf-8');
212
+ }
213
+ async writeFile(workspacePath, filePath, content) {
214
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(workspacePath, filePath);
215
+ if (!this.isPathSafe(workspacePath, absolutePath)) {
216
+ throw new Error('Access denied: Path is outside the workspace directory.');
217
+ }
218
+ const dir = path.dirname(absolutePath);
219
+ if (!fs.existsSync(dir)) {
220
+ fs.mkdirSync(dir, { recursive: true });
221
+ }
222
+ fs.writeFileSync(absolutePath, content, 'utf-8');
223
+ }
224
+ isPathSafe(workspacePath, absolutePath) {
225
+ const normWorkspace = path.normalize(workspacePath).replace(/\\/g, '/').toLowerCase();
226
+ const normAbsolute = path.normalize(absolutePath).replace(/\\/g, '/').toLowerCase();
227
+ if (normAbsolute === normWorkspace)
228
+ return true;
229
+ return normAbsolute.startsWith(normWorkspace.endsWith('/') ? normWorkspace : normWorkspace + '/');
230
+ }
231
+ // State initialization helpers
232
+ defaultWorkflowState() {
233
+ return {
234
+ constitutionPhase: { phase: 'constitution', status: 'idle', filePath: null, content: null },
235
+ features: [],
236
+ activeFeatureName: null
237
+ };
238
+ }
239
+ makePhaseState(phase) {
240
+ return { phase, status: 'idle', filePath: null, content: null };
241
+ }
242
+ makeFeatureWorkflow(name) {
243
+ return {
244
+ name,
245
+ phases: FEATURE_PHASES.map(p => this.makePhaseState(p))
246
+ };
247
+ }
248
+ loadStateFile(workspacePath) {
249
+ const statePath = path.join(workspacePath, '.specify', '.runtime', 'workflow-state.json');
250
+ if (!fs.existsSync(statePath)) {
251
+ return null;
252
+ }
253
+ try {
254
+ const payload = fs.readFileSync(statePath, 'utf-8');
255
+ const data = JSON.parse(payload);
256
+ // Merge with default formats
257
+ const state = this.defaultWorkflowState();
258
+ state.constitutionPhase.status = data.constitutionPhase?.status || 'idle';
259
+ state.constitutionPhase.stale = data.constitutionPhase?.stale || false;
260
+ if (Array.isArray(data.features)) {
261
+ state.features = data.features.map((f) => ({
262
+ name: f.name,
263
+ phases: FEATURE_PHASES.map(p => {
264
+ const savedPhase = f.phases?.find((sp) => sp.phase === p);
265
+ return {
266
+ phase: p,
267
+ status: savedPhase?.status || 'idle',
268
+ stale: savedPhase?.stale || false,
269
+ filePath: null,
270
+ content: null
271
+ };
272
+ })
273
+ }));
274
+ }
275
+ state.activeFeatureName = data.activeFeatureName || null;
276
+ return state;
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
282
+ }
283
+ exports.FSWorkspaceRepository = FSWorkspaceRepository;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = POST;
4
+ exports.DELETE = DELETE;
5
+ const server_1 = require("next/server");
6
+ const di_1 = require("../../../adapters/di");
7
+ const utils_1 = require("../../../adapters/primary/api/utils");
8
+ async function POST(req) {
9
+ try {
10
+ const { name } = await req.json();
11
+ if (!name) {
12
+ return server_1.NextResponse.json({ error: 'Feature name is required' }, { status: 400 });
13
+ }
14
+ const workspacePath = (0, utils_1.getWorkspacePath)();
15
+ const state = await di_1.workflowService.createFeature(workspacePath, name);
16
+ return server_1.NextResponse.json(state);
17
+ }
18
+ catch (err) {
19
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
20
+ }
21
+ }
22
+ async function DELETE(req) {
23
+ try {
24
+ const { name } = await req.json();
25
+ if (!name) {
26
+ return server_1.NextResponse.json({ error: 'Feature name is required' }, { status: 400 });
27
+ }
28
+ const workspacePath = (0, utils_1.getWorkspacePath)();
29
+ const state = await di_1.workflowService.deleteFeature(workspacePath, name);
30
+ return server_1.NextResponse.json(state);
31
+ }
32
+ catch (err) {
33
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
34
+ }
35
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = GET;
4
+ exports.POST = POST;
5
+ const server_1 = require("next/server");
6
+ const di_1 = require("../../../adapters/di");
7
+ const utils_1 = require("../../../adapters/primary/api/utils");
8
+ async function GET(req) {
9
+ try {
10
+ const url = new URL(req.url);
11
+ const filePath = url.searchParams.get('path');
12
+ if (!filePath) {
13
+ return server_1.NextResponse.json({ error: 'Path is required' }, { status: 400 });
14
+ }
15
+ const workspacePath = (0, utils_1.getWorkspacePath)();
16
+ const content = await di_1.workflowService.readFile(workspacePath, filePath);
17
+ return server_1.NextResponse.json({ path: filePath, content });
18
+ }
19
+ catch (err) {
20
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
21
+ }
22
+ }
23
+ async function POST(req) {
24
+ try {
25
+ const { path: filePath, content } = await req.json();
26
+ if (!filePath || content === undefined) {
27
+ return server_1.NextResponse.json({ error: 'Path and content are required' }, { status: 400 });
28
+ }
29
+ const workspacePath = (0, utils_1.getWorkspacePath)();
30
+ const state = await di_1.workflowService.writeFile(workspacePath, filePath, content);
31
+ return server_1.NextResponse.json({ success: true, state });
32
+ }
33
+ catch (err) {
34
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
35
+ }
36
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = POST;
4
+ const server_1 = require("next/server");
5
+ const di_1 = require("../../../adapters/di");
6
+ const utils_1 = require("../../../adapters/primary/api/utils");
7
+ async function POST(req) {
8
+ try {
9
+ const { action, phase, featureName, agentConfig, prompt } = await req.json();
10
+ if (!action || !phase) {
11
+ return server_1.NextResponse.json({ error: 'Missing action or phase' }, { status: 400 });
12
+ }
13
+ const workspacePath = (0, utils_1.getWorkspacePath)();
14
+ if (action === 'approve') {
15
+ const state = await di_1.workflowService.approvePhase(workspacePath, phase, featureName);
16
+ return server_1.NextResponse.json(state);
17
+ }
18
+ if (action === 'discard') {
19
+ const state = await di_1.workflowService.discardPhase(workspacePath, phase, featureName);
20
+ return server_1.NextResponse.json(state);
21
+ }
22
+ if (action === 'run') {
23
+ const encoder = new TextEncoder();
24
+ const customReadableStream = new ReadableStream({
25
+ async start(controller) {
26
+ const sendEvent = (event, data) => {
27
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
28
+ };
29
+ try {
30
+ const finalState = await di_1.workflowService.runPhase(workspacePath, phase, featureName, agentConfig, prompt, (text) => {
31
+ sendEvent('log', { text });
32
+ });
33
+ sendEvent('done', { state: finalState });
34
+ controller.close();
35
+ }
36
+ catch (err) {
37
+ sendEvent('error', { message: err.message });
38
+ controller.close();
39
+ }
40
+ }
41
+ });
42
+ return new Response(customReadableStream, {
43
+ headers: {
44
+ 'Content-Type': 'text/event-stream',
45
+ 'Cache-Control': 'no-cache',
46
+ 'Connection': 'keep-alive',
47
+ }
48
+ });
49
+ }
50
+ return server_1.NextResponse.json({ error: 'Invalid action' }, { status: 400 });
51
+ }
52
+ catch (err) {
53
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
54
+ }
55
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = GET;
4
+ exports.POST = POST;
5
+ const server_1 = require("next/server");
6
+ const di_1 = require("../../../adapters/di");
7
+ const utils_1 = require("../../../adapters/primary/api/utils");
8
+ async function GET() {
9
+ try {
10
+ const workspacePath = (0, utils_1.getWorkspacePath)();
11
+ const state = await di_1.workflowService.getWorkflowState(workspacePath);
12
+ return server_1.NextResponse.json(state);
13
+ }
14
+ catch (err) {
15
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
16
+ }
17
+ }
18
+ async function POST(req) {
19
+ try {
20
+ const { activeFeatureName } = await req.json();
21
+ const workspacePath = (0, utils_1.getWorkspacePath)();
22
+ const state = await di_1.workflowService.setActiveFeature(workspacePath, activeFeatureName);
23
+ return server_1.NextResponse.json(state);
24
+ }
25
+ catch (err) {
26
+ return server_1.NextResponse.json({ error: err.message }, { status: 500 });
27
+ }
28
+ }