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.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +71 -0
- package/.next/app-path-routes-manifest.json +10 -0
- package/.next/build-manifest.json +33 -0
- package/.next/cache/.previewinfo +1 -0
- package/.next/cache/.rscinfo +1 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/10.pack +0 -0
- package/.next/cache/webpack/client-production/11.pack +0 -0
- package/.next/cache/webpack/client-production/12.pack +0 -0
- package/.next/cache/webpack/client-production/13.pack +0 -0
- package/.next/cache/webpack/client-production/14.pack +0 -0
- package/.next/cache/webpack/client-production/15.pack +0 -0
- package/.next/cache/webpack/client-production/16.pack +0 -0
- package/.next/cache/webpack/client-production/17.pack +0 -0
- package/.next/cache/webpack/client-production/18.pack +0 -0
- package/.next/cache/webpack/client-production/19.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/20.pack +0 -0
- package/.next/cache/webpack/client-production/21.pack +0 -0
- package/.next/cache/webpack/client-production/22.pack +0 -0
- package/.next/cache/webpack/client-production/3.pack +0 -0
- package/.next/cache/webpack/client-production/4.pack +0 -0
- package/.next/cache/webpack/client-production/5.pack +0 -0
- package/.next/cache/webpack/client-production/6.pack +0 -0
- package/.next/cache/webpack/client-production/7.pack +0 -0
- package/.next/cache/webpack/client-production/8.pack +0 -0
- package/.next/cache/webpack/client-production/9.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/1.pack +0 -0
- package/.next/cache/webpack/server-production/10.pack +0 -0
- package/.next/cache/webpack/server-production/11.pack +0 -0
- package/.next/cache/webpack/server-production/12.pack +0 -0
- package/.next/cache/webpack/server-production/13.pack +0 -0
- package/.next/cache/webpack/server-production/14.pack +0 -0
- package/.next/cache/webpack/server-production/15.pack +0 -0
- package/.next/cache/webpack/server-production/16.pack +0 -0
- package/.next/cache/webpack/server-production/17.pack +0 -0
- package/.next/cache/webpack/server-production/18.pack +0 -0
- package/.next/cache/webpack/server-production/19.pack +0 -0
- package/.next/cache/webpack/server-production/2.pack +0 -0
- package/.next/cache/webpack/server-production/20.pack +0 -0
- package/.next/cache/webpack/server-production/3.pack +0 -0
- package/.next/cache/webpack/server-production/4.pack +0 -0
- package/.next/cache/webpack/server-production/5.pack +0 -0
- package/.next/cache/webpack/server-production/6.pack +0 -0
- package/.next/cache/webpack/server-production/7.pack +0 -0
- package/.next/cache/webpack/server-production/8.pack +0 -0
- package/.next/cache/webpack/server-production/9.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +61 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +320 -0
- package/.next/routes-manifest.json +53 -0
- package/.next/server/app/_not-found/page.js +2 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +15 -0
- package/.next/server/app/api/feature/route.js +1 -0
- package/.next/server/app/api/feature/route.js.nft.json +1 -0
- package/.next/server/app/api/feature/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/file/route.js +1 -0
- package/.next/server/app/api/file/route.js.nft.json +1 -0
- package/.next/server/app/api/file/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/phase/route.js +4 -0
- package/.next/server/app/api/phase/route.js.nft.json +1 -0
- package/.next/server/app/api/phase/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/state/route.js +1 -0
- package/.next/server/app/api/state/route.js.nft.json +1 -0
- package/.next/server/app/api/state/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/state/watch/route.js +4 -0
- package/.next/server/app/api/state/watch/route.js.nft.json +1 -0
- package/.next/server/app/api/state/watch/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/task/route.js +1 -0
- package/.next/server/app/api/task/route.js.nft.json +1 -0
- package/.next/server/app/api/task/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +7 -0
- package/.next/server/app/index.rsc +20 -0
- package/.next/server/app/page.js +14 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +10 -0
- package/.next/server/chunks/355.js +22 -0
- package/.next/server/chunks/6.js +9 -0
- package/.next/server/chunks/607.js +6 -0
- package/.next/server/chunks/643.js +1 -0
- package/.next/server/chunks/897.js +9 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/590-a6568595ecd2a994.js +1 -0
- package/.next/static/chunks/8381d2c4-9707dccab70b742b.js +1 -0
- package/.next/static/chunks/920-f9bfc1b0d0402c3e.js +1 -0
- package/.next/static/chunks/acfafb44-8249079a6627ac92.js +1 -0
- package/.next/static/chunks/app/_not-found/page-be798b363e27e8c5.js +1 -0
- package/.next/static/chunks/app/api/feature/route-bb3c1a82e892ab58.js +1 -0
- package/.next/static/chunks/app/api/file/route-bb3c1a82e892ab58.js +1 -0
- package/.next/static/chunks/app/api/phase/route-bb3c1a82e892ab58.js +1 -0
- package/.next/static/chunks/app/api/state/route-bb3c1a82e892ab58.js +1 -0
- package/.next/static/chunks/app/api/state/watch/route-bb3c1a82e892ab58.js +1 -0
- package/.next/static/chunks/app/api/task/route-bb3c1a82e892ab58.js +1 -0
- package/.next/static/chunks/app/layout-3226c76a5f7f74bc.js +1 -0
- package/.next/static/chunks/app/page-8a5248f7704cde29.js +1 -0
- package/.next/static/chunks/framework-20dd4f6054cc5446.js +1 -0
- package/.next/static/chunks/main-91029f76ac1b7110.js +1 -0
- package/.next/static/chunks/main-app-b9ad56d6b1dfa941.js +1 -0
- package/.next/static/chunks/pages/_app-aa33dc41c3472021.js +1 -0
- package/.next/static/chunks/pages/_error-78b0b3b591df0e73.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-c460f8e58a9e9eff.js +1 -0
- package/.next/static/css/008a05b0ad6b854a.css +31 -0
- package/.next/static/css/6fd2e11db3a59771.css +3 -0
- package/.next/static/fWuQ3yoHovA7Cre6u89W7/_buildManifest.js +1 -0
- package/.next/static/fWuQ3yoHovA7Cre6u89W7/_ssgManifest.js +1 -0
- package/.next/trace +3 -0
- package/.next/types/app/api/feature/route.ts +347 -0
- package/.next/types/app/api/file/route.ts +347 -0
- package/.next/types/app/api/phase/route.ts +347 -0
- package/.next/types/app/api/state/route.ts +347 -0
- package/.next/types/app/api/state/watch/route.ts +347 -0
- package/.next/types/app/api/task/route.ts +347 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +78 -0
- package/.next/types/validator.ts +124 -0
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/bin/adapters/di.js +9 -0
- package/bin/adapters/primary/api/utils.js +57 -0
- package/bin/adapters/secondary/agent/ProcessAgentRunner.js +161 -0
- package/bin/adapters/secondary/fs/FSWorkspaceRepository.js +283 -0
- package/bin/app/api/feature/route.js +35 -0
- package/bin/app/api/file/route.js +36 -0
- package/bin/app/api/phase/route.js +55 -0
- package/bin/app/api/state/route.js +28 -0
- package/bin/app/api/state/watch/route.js +92 -0
- package/bin/app/api/task/route.js +20 -0
- package/bin/bin/cli.js +317 -0
- package/bin/cli.js +85 -0
- package/bin/domain/models/types.js +2 -0
- package/bin/domain/ports/in/WorkflowUseCases.js +2 -0
- package/bin/domain/ports/out/AgentRunnerPort.js +2 -0
- package/bin/domain/ports/out/WorkspaceRepositoryPort.js +2 -0
- package/bin/domain/services/WorkflowService.js +174 -0
- package/next.config.ts +13 -0
- 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
|
+
}
|