@programinglive/dev-workflow-mcp-server 1.1.6 → 1.1.11
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/CHANGELOG.md +51 -0
- package/README.md +92 -5
- package/db/index.js +204 -0
- package/exec.js +46 -1
- package/index.js +12 -2
- package/install.js +5 -1
- package/package.json +15 -8
- package/release-wrapper.js +1 -1
- package/tests/build.test.js +37 -0
- package/tests/exec.test.js +32 -0
- package/tests/handlers.test.js +288 -0
- package/tests/release-wrapper.test.js +4 -2
- package/tests/workflowState.test.js +220 -1
- package/tools/definitions.js +103 -0
- package/tools/handlers.js +262 -9
- package/vite.config.js +43 -0
- package/web/index.html +307 -0
- package/web/server.js +57 -0
- package/workflow-state.js +439 -15
package/tests/handlers.test.js
CHANGED
|
@@ -96,6 +96,294 @@ test("commit_and_push commits changes and pushes", async () => {
|
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("run_full_workflow iterates until commit/release complete with clean tree", async () => {
|
|
100
|
+
await withWorkflowState(async (workflowState) => {
|
|
101
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
102
|
+
workflowState.state.taskType = "feature";
|
|
103
|
+
workflowState.state.currentPhase = "commit";
|
|
104
|
+
workflowState.state.bugFixed = true;
|
|
105
|
+
workflowState.state.testsCreated = true;
|
|
106
|
+
workflowState.state.testsPassed = true;
|
|
107
|
+
workflowState.state.documentationCreated = true;
|
|
108
|
+
workflowState.state.readyToCommit = true;
|
|
109
|
+
workflowState.state.readyCheckCompleted = true;
|
|
110
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
111
|
+
workflowState.state.released = false;
|
|
112
|
+
workflowState.state.documentationType = "README";
|
|
113
|
+
workflowState.state.documentationSummary = "Docs";
|
|
114
|
+
workflowState.state.fixSummary = "Feature done";
|
|
115
|
+
await workflowState.save();
|
|
116
|
+
|
|
117
|
+
const commands = [];
|
|
118
|
+
const execStub = async (command) => {
|
|
119
|
+
commands.push(command);
|
|
120
|
+
if (command === "git status --porcelain") {
|
|
121
|
+
return { stdout: "" };
|
|
122
|
+
}
|
|
123
|
+
return { stdout: "" };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const git = {
|
|
127
|
+
hasWorkingChanges: async () => false,
|
|
128
|
+
hasStagedChanges: async () => false,
|
|
129
|
+
hasTestChanges: async () => true,
|
|
130
|
+
getStagedChanges: async () => [],
|
|
131
|
+
getCurrentBranch: async () => "main",
|
|
132
|
+
getLastCommitMessage: async () => "chore: existing",
|
|
133
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const response = await handleToolCall({
|
|
137
|
+
request: createRequest("run_full_workflow", {
|
|
138
|
+
summary: "Feature done",
|
|
139
|
+
testCommand: "npm test",
|
|
140
|
+
documentationType: "README",
|
|
141
|
+
documentationSummary: "Docs",
|
|
142
|
+
commitMessage: "feat: ship feature",
|
|
143
|
+
releaseCommand: "npm run release:patch",
|
|
144
|
+
}),
|
|
145
|
+
normalizeRequestArgs,
|
|
146
|
+
workflowState,
|
|
147
|
+
exec: execStub,
|
|
148
|
+
git,
|
|
149
|
+
utils,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.ok(
|
|
153
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
154
|
+
"run_full_workflow should finish even when commit is already clean"
|
|
155
|
+
);
|
|
156
|
+
assert.ok(
|
|
157
|
+
response.content[0].text.includes("🎉 Cheers!"),
|
|
158
|
+
"run_full_workflow should include celebration message"
|
|
159
|
+
);
|
|
160
|
+
assert.equal(commands.includes("npm run release:patch"), true, "release command should run");
|
|
161
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
162
|
+
assert.equal(workflowState.state.history.length, 1);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("project_summary_data reads persisted file and falls back", async () => {
|
|
167
|
+
await withWorkflowState(async (workflowState) => {
|
|
168
|
+
// Seed some history
|
|
169
|
+
workflowState.addToHistory({ taskDescription: "Add login feature", taskType: "feature", commitMessage: "feat: login" });
|
|
170
|
+
await workflowState.save(); // This should create project-summary.json
|
|
171
|
+
|
|
172
|
+
const response = await handleToolCall({
|
|
173
|
+
request: createRequest("project_summary_data", {}),
|
|
174
|
+
normalizeRequestArgs,
|
|
175
|
+
workflowState,
|
|
176
|
+
exec: async () => ({ stdout: "" }),
|
|
177
|
+
git: {},
|
|
178
|
+
utils,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const text = response.content[0].text;
|
|
182
|
+
assert.ok(text.includes("from persisted data"), "should indicate persisted source");
|
|
183
|
+
assert.ok(text.includes("Total tasks completed: 1"), "should count tasks");
|
|
184
|
+
assert.ok(text.includes("feature: 1"), "should show feature count");
|
|
185
|
+
assert.ok(text.includes("Updated:"), "should show updated timestamp");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("project_summary aggregates task types and recent activity", async () => {
|
|
190
|
+
await withWorkflowState(async (workflowState) => {
|
|
191
|
+
// Seed some history
|
|
192
|
+
workflowState.addToHistory({ taskDescription: "Add login feature", taskType: "feature", commitMessage: "feat: login" });
|
|
193
|
+
workflowState.addToHistory({ taskDescription: "Fix auth bug", taskType: "bugfix", commitMessage: "fix: auth" });
|
|
194
|
+
workflowState.addToHistory({ taskDescription: "Refactor utils", taskType: "refactor", commitMessage: "refactor: utils" });
|
|
195
|
+
await workflowState.save();
|
|
196
|
+
|
|
197
|
+
const response = await handleToolCall({
|
|
198
|
+
request: createRequest("project_summary", {}),
|
|
199
|
+
normalizeRequestArgs,
|
|
200
|
+
workflowState,
|
|
201
|
+
exec: async () => ({ stdout: "" }),
|
|
202
|
+
git: {},
|
|
203
|
+
utils,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const text = response.content[0].text;
|
|
207
|
+
assert.ok(text.includes("Project Knowledge Summary"), "should include summary title");
|
|
208
|
+
assert.ok(text.includes("Total tasks completed: 3"), "should count tasks");
|
|
209
|
+
assert.ok(text.includes("feature: 1"), "should show feature count");
|
|
210
|
+
assert.ok(text.includes("bugfix: 1"), "should show bugfix count");
|
|
211
|
+
assert.ok(text.includes("refactor: 1"), "should show refactor count");
|
|
212
|
+
assert.ok(text.includes("Add login feature"), "should list recent tasks");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("run_full_workflow executes all steps successfully", async () => {
|
|
217
|
+
await withWorkflowState(async (workflowState) => {
|
|
218
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
219
|
+
workflowState.state.taskType = "feature";
|
|
220
|
+
workflowState.state.currentPhase = "coding";
|
|
221
|
+
await workflowState.save();
|
|
222
|
+
|
|
223
|
+
const commands = [];
|
|
224
|
+
let workingChanges = true;
|
|
225
|
+
const execStub = async (command) => {
|
|
226
|
+
commands.push(command);
|
|
227
|
+
if (command.startsWith("git commit") || command.startsWith("git push")) {
|
|
228
|
+
workingChanges = false;
|
|
229
|
+
}
|
|
230
|
+
if (command === "git status --porcelain") {
|
|
231
|
+
return { stdout: workingChanges ? " M src/app.js" : "" };
|
|
232
|
+
}
|
|
233
|
+
return { stdout: "" };
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const git = {
|
|
237
|
+
hasWorkingChanges: async () => workingChanges,
|
|
238
|
+
hasStagedChanges: async () => false,
|
|
239
|
+
hasTestChanges: async () => true,
|
|
240
|
+
getStagedChanges: async () => [{ status: "M", path: "src/app.js" }],
|
|
241
|
+
getCurrentBranch: async () => "main",
|
|
242
|
+
getLastCommitMessage: async () => "",
|
|
243
|
+
workingTreeSummary: () =>
|
|
244
|
+
workingChanges
|
|
245
|
+
? { hasChanges: true, lines: ["M src/app.js"] }
|
|
246
|
+
: { hasChanges: false, lines: [] },
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const response = await handleToolCall({
|
|
250
|
+
request: createRequest("run_full_workflow", {
|
|
251
|
+
summary: "Implement feature",
|
|
252
|
+
testCommand: "npm test",
|
|
253
|
+
documentationType: "README",
|
|
254
|
+
documentationSummary: "Document feature",
|
|
255
|
+
commitMessage: "feat: add feature",
|
|
256
|
+
releaseCommand: "npm run release:patch",
|
|
257
|
+
releaseNotes: "Automated release",
|
|
258
|
+
}),
|
|
259
|
+
normalizeRequestArgs,
|
|
260
|
+
workflowState,
|
|
261
|
+
exec: execStub,
|
|
262
|
+
git,
|
|
263
|
+
utils,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.ok(
|
|
267
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
268
|
+
"run_full_workflow should report success"
|
|
269
|
+
);
|
|
270
|
+
assert.ok(
|
|
271
|
+
commands.some((command) => command.startsWith("git add")),
|
|
272
|
+
"git add should be executed"
|
|
273
|
+
);
|
|
274
|
+
assert.ok(
|
|
275
|
+
commands.some((command) => command.startsWith("git commit")),
|
|
276
|
+
"git commit should be executed"
|
|
277
|
+
);
|
|
278
|
+
assert.ok(
|
|
279
|
+
commands.some((command) => command.startsWith("git push")),
|
|
280
|
+
"git push should be executed"
|
|
281
|
+
);
|
|
282
|
+
assert.ok(
|
|
283
|
+
commands.some((command) => command.startsWith("npm run release")),
|
|
284
|
+
"release command should be executed"
|
|
285
|
+
);
|
|
286
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("run_full_workflow resumes from current phase when steps are already completed", async () => {
|
|
291
|
+
await withWorkflowState(async (workflowState) => {
|
|
292
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
293
|
+
workflowState.state.taskType = "feature";
|
|
294
|
+
workflowState.state.currentPhase = "release";
|
|
295
|
+
workflowState.state.bugFixed = true;
|
|
296
|
+
workflowState.state.testsCreated = true;
|
|
297
|
+
workflowState.state.testsPassed = true;
|
|
298
|
+
workflowState.state.documentationCreated = true;
|
|
299
|
+
workflowState.state.readyCheckCompleted = true;
|
|
300
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
301
|
+
await workflowState.save();
|
|
302
|
+
|
|
303
|
+
const commands = [];
|
|
304
|
+
const execStub = async (command) => {
|
|
305
|
+
commands.push(command);
|
|
306
|
+
if (command === "git status --porcelain") {
|
|
307
|
+
return { stdout: "" };
|
|
308
|
+
}
|
|
309
|
+
return { stdout: "" };
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const git = {
|
|
313
|
+
hasWorkingChanges: async () => false,
|
|
314
|
+
hasStagedChanges: async () => false,
|
|
315
|
+
hasTestChanges: async () => true,
|
|
316
|
+
getStagedChanges: async () => [],
|
|
317
|
+
getCurrentBranch: async () => "main",
|
|
318
|
+
getLastCommitMessage: async () => "",
|
|
319
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const response = await handleToolCall({
|
|
323
|
+
request: createRequest("run_full_workflow", {
|
|
324
|
+
summary: "Implement feature",
|
|
325
|
+
testCommand: "npm test",
|
|
326
|
+
documentationType: "README",
|
|
327
|
+
documentationSummary: "Document feature",
|
|
328
|
+
commitMessage: "feat: add feature",
|
|
329
|
+
releaseCommand: "npm run release:patch",
|
|
330
|
+
releaseNotes: "Automated release",
|
|
331
|
+
}),
|
|
332
|
+
normalizeRequestArgs,
|
|
333
|
+
workflowState,
|
|
334
|
+
exec: execStub,
|
|
335
|
+
git,
|
|
336
|
+
utils,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
assert.ok(
|
|
340
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
341
|
+
"run_full_workflow should report success"
|
|
342
|
+
);
|
|
343
|
+
// Should only run release and complete since earlier steps are done
|
|
344
|
+
assert.ok(
|
|
345
|
+
commands.some((command) => command.startsWith("npm run release")),
|
|
346
|
+
"release command should be executed"
|
|
347
|
+
);
|
|
348
|
+
assert.equal(
|
|
349
|
+
commands.filter((c) => c.startsWith("git commit")).length,
|
|
350
|
+
0,
|
|
351
|
+
"git commit should not be executed when already completed"
|
|
352
|
+
);
|
|
353
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("run_full_workflow validates required arguments", async () => {
|
|
358
|
+
await withWorkflowState(async (workflowState) => {
|
|
359
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
360
|
+
workflowState.state.taskType = "feature";
|
|
361
|
+
workflowState.state.currentPhase = "coding";
|
|
362
|
+
await workflowState.save();
|
|
363
|
+
|
|
364
|
+
const response = await handleToolCall({
|
|
365
|
+
request: createRequest("run_full_workflow", {
|
|
366
|
+
summary: "",
|
|
367
|
+
testCommand: "npm test",
|
|
368
|
+
documentationType: "README",
|
|
369
|
+
documentationSummary: "Doc",
|
|
370
|
+
commitMessage: "feat: add feature",
|
|
371
|
+
releaseCommand: "npm run release:patch",
|
|
372
|
+
}),
|
|
373
|
+
normalizeRequestArgs,
|
|
374
|
+
workflowState,
|
|
375
|
+
exec: async () => ({ stdout: "" }),
|
|
376
|
+
git: {},
|
|
377
|
+
utils,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
assert.ok(
|
|
381
|
+
response.content[0].text.includes("Provide a non-empty 'summary'"),
|
|
382
|
+
"run_full_workflow should validate required fields"
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
99
387
|
test("force_complete_task records entry and resets state", async () => {
|
|
100
388
|
await withWorkflowState(async (workflowState) => {
|
|
101
389
|
workflowState.state.taskDescription = "Improve caching";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, rm, writeFile, readFile, mkdir } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
@@ -10,7 +10,9 @@ const RELEASE_WRAPPER = path.join(PROJECT_ROOT, "release-wrapper.js");
|
|
|
10
10
|
|
|
11
11
|
async function runWrapper({ state, args = ["patch"], env = {} }) {
|
|
12
12
|
const dir = await mkdtemp(path.join(tmpdir(), "release-wrapper-test-"));
|
|
13
|
-
const
|
|
13
|
+
const stateDir = path.join(dir, ".state");
|
|
14
|
+
const statePath = path.join(stateDir, "workflow-state.json");
|
|
15
|
+
await mkdir(stateDir, { recursive: true });
|
|
14
16
|
await writeFile(statePath, JSON.stringify(state, null, 2));
|
|
15
17
|
|
|
16
18
|
const childEnv = {
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
mkdtemp,
|
|
5
|
+
rm,
|
|
6
|
+
mkdir,
|
|
7
|
+
realpath,
|
|
8
|
+
readFile,
|
|
9
|
+
writeFile,
|
|
10
|
+
access,
|
|
11
|
+
copyFile,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
14
|
import { tmpdir } from "node:os";
|
|
5
15
|
import path from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
17
|
import {
|
|
7
18
|
WorkflowState,
|
|
8
19
|
getNextStep,
|
|
@@ -25,6 +36,161 @@ async function withWorkflowState(callback) {
|
|
|
25
36
|
}
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
test("WorkflowState mirrors state file to compatibility locations", async () => {
|
|
40
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-compat-"));
|
|
41
|
+
const moduleDir = path.join(tempRoot, "module");
|
|
42
|
+
const projectRoot = path.join(tempRoot, "project");
|
|
43
|
+
const stateDir = path.join(projectRoot, ".state");
|
|
44
|
+
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
45
|
+
await mkdir(moduleDir, { recursive: true });
|
|
46
|
+
await mkdir(path.join(moduleDir, "dist"), { recursive: true });
|
|
47
|
+
await mkdir(path.join(tempRoot, "dist"), { recursive: true });
|
|
48
|
+
await mkdir(stateDir, { recursive: true });
|
|
49
|
+
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
50
|
+
await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
|
|
51
|
+
|
|
52
|
+
const stateFile = path.join(stateDir, "workflow-state.json");
|
|
53
|
+
await writeFile(stateFile, JSON.stringify({ currentPhase: "testing" }, null, 2));
|
|
54
|
+
|
|
55
|
+
const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
|
|
56
|
+
const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
|
|
57
|
+
|
|
58
|
+
const state = new TempWorkflowState(stateFile);
|
|
59
|
+
await state.load();
|
|
60
|
+
state.state.currentPhase = "commit";
|
|
61
|
+
await state.save();
|
|
62
|
+
|
|
63
|
+
// Ensure primary state file was written correctly
|
|
64
|
+
const primaryContent = await readFile(stateFile, "utf-8");
|
|
65
|
+
const primaryParsed = JSON.parse(primaryContent);
|
|
66
|
+
assert.equal(primaryParsed.currentPhase, "commit", "primary state file should be updated");
|
|
67
|
+
|
|
68
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("WorkflowState avoids filesystem root when no project markers found", async () => {
|
|
72
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-rootless-"));
|
|
73
|
+
const moduleDir = path.join(tempRoot, "module");
|
|
74
|
+
await mkdir(moduleDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
|
|
77
|
+
await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
|
|
78
|
+
|
|
79
|
+
const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
|
|
80
|
+
const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
|
|
81
|
+
|
|
82
|
+
const originalCwd = process.cwd();
|
|
83
|
+
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
84
|
+
const originalInitCwd = process.env.INIT_CWD;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const cwdWithoutMarkers = path.join(tempRoot, "random", "deep");
|
|
88
|
+
await mkdir(cwdWithoutMarkers, { recursive: true });
|
|
89
|
+
|
|
90
|
+
process.chdir(cwdWithoutMarkers);
|
|
91
|
+
process.env.INIT_CWD = cwdWithoutMarkers;
|
|
92
|
+
|
|
93
|
+
const state = new TempWorkflowState();
|
|
94
|
+
const resolvedTempRoot = await realpath(tempRoot);
|
|
95
|
+
|
|
96
|
+
assert.ok(
|
|
97
|
+
state.stateFile.startsWith(path.join(resolvedTempRoot, ".state")),
|
|
98
|
+
"state file should resolve under temp root when no project markers exist"
|
|
99
|
+
);
|
|
100
|
+
assert.ok(
|
|
101
|
+
!state.stateFile.startsWith(path.join(path.sep, ".state")),
|
|
102
|
+
"state file should not resolve to filesystem root"
|
|
103
|
+
);
|
|
104
|
+
} finally {
|
|
105
|
+
process.chdir(originalCwd);
|
|
106
|
+
if (initWasSet) {
|
|
107
|
+
process.env.INIT_CWD = originalInitCwd;
|
|
108
|
+
} else {
|
|
109
|
+
delete process.env.INIT_CWD;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("resolveUserId persists generated ID", async () => {
|
|
117
|
+
const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-userid-"));
|
|
118
|
+
await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
|
|
119
|
+
|
|
120
|
+
const originalCwd = process.cwd();
|
|
121
|
+
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
122
|
+
const originalInitCwd = process.env.INIT_CWD;
|
|
123
|
+
const userEnvWasSet = Object.prototype.hasOwnProperty.call(process.env, "DEV_WORKFLOW_USER_ID");
|
|
124
|
+
const originalUserEnv = process.env.DEV_WORKFLOW_USER_ID;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
process.chdir(projectRoot);
|
|
128
|
+
process.env.INIT_CWD = projectRoot;
|
|
129
|
+
if (userEnvWasSet) {
|
|
130
|
+
delete process.env.DEV_WORKFLOW_USER_ID;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const firstModuleHref = new URL("../workflow-state.js?resolveUserId1", import.meta.url).href;
|
|
134
|
+
const { WorkflowState: FirstWorkflowState } = await import(firstModuleHref);
|
|
135
|
+
|
|
136
|
+
const state = new FirstWorkflowState();
|
|
137
|
+
await state.ensurePrimaryFile();
|
|
138
|
+
|
|
139
|
+
const userDir = path.dirname(state.stateFile);
|
|
140
|
+
const userId = path.basename(userDir);
|
|
141
|
+
assert.ok(userId.startsWith("user-"), "Generated user ID should use user- prefix");
|
|
142
|
+
|
|
143
|
+
const userIdFile = path.join(projectRoot, ".state", "user-id");
|
|
144
|
+
const persistedId = (await readFile(userIdFile, "utf-8")).trim();
|
|
145
|
+
assert.equal(persistedId, userId, "user-id file should match generated user ID");
|
|
146
|
+
|
|
147
|
+
const secondModuleHref = new URL("../workflow-state.js?resolveUserId2", import.meta.url).href;
|
|
148
|
+
const { WorkflowState: SecondWorkflowState } = await import(secondModuleHref);
|
|
149
|
+
|
|
150
|
+
const reloadedState = new SecondWorkflowState();
|
|
151
|
+
await reloadedState.ensurePrimaryFile();
|
|
152
|
+
|
|
153
|
+
assert.equal(reloadedState.stateFile, state.stateFile, "State file path should persist across reloads");
|
|
154
|
+
const reloadedUserId = path.basename(path.dirname(reloadedState.stateFile));
|
|
155
|
+
assert.equal(reloadedUserId, userId, "Reloaded module should reuse persisted user ID");
|
|
156
|
+
} finally {
|
|
157
|
+
process.chdir(originalCwd);
|
|
158
|
+
if (initWasSet) {
|
|
159
|
+
process.env.INIT_CWD = originalInitCwd;
|
|
160
|
+
} else {
|
|
161
|
+
delete process.env.INIT_CWD;
|
|
162
|
+
}
|
|
163
|
+
if (userEnvWasSet) {
|
|
164
|
+
process.env.DEV_WORKFLOW_USER_ID = originalUserEnv;
|
|
165
|
+
} else {
|
|
166
|
+
delete process.env.DEV_WORKFLOW_USER_ID;
|
|
167
|
+
}
|
|
168
|
+
await rm(projectRoot, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("WorkflowState generates project summary on load and updates on save", async () => {
|
|
173
|
+
await withWorkflowState(async (state) => {
|
|
174
|
+
const summaryPath = path.join(path.dirname(state.stateFile), "project-summary.json");
|
|
175
|
+
|
|
176
|
+
const initialSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
|
|
177
|
+
assert.equal(initialSummary.totalTasks, 0);
|
|
178
|
+
assert.deepEqual(initialSummary.taskTypes, {});
|
|
179
|
+
|
|
180
|
+
state.addToHistory({
|
|
181
|
+
taskDescription: "Seed summary",
|
|
182
|
+
taskType: "feature",
|
|
183
|
+
commitMessage: "feat: seed",
|
|
184
|
+
});
|
|
185
|
+
await state.save();
|
|
186
|
+
|
|
187
|
+
const updatedSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
|
|
188
|
+
assert.equal(updatedSummary.totalTasks, 1);
|
|
189
|
+
assert.equal(updatedSummary.taskTypes.feature, 1);
|
|
190
|
+
assert.equal(updatedSummary.recentTasks[0].description, "Seed summary");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
28
194
|
test("WorkflowState persists data across save/load", async () => {
|
|
29
195
|
await withWorkflowState(async (state) => {
|
|
30
196
|
state.state.currentPhase = "testing";
|
|
@@ -64,6 +230,59 @@ test("WorkflowState persists data across save/load", async () => {
|
|
|
64
230
|
});
|
|
65
231
|
});
|
|
66
232
|
|
|
233
|
+
test("WorkflowState resolves state file relative to project root", async () => {
|
|
234
|
+
const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-project-"));
|
|
235
|
+
const originalCwd = process.cwd();
|
|
236
|
+
const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
|
|
237
|
+
const originalInitCwd = process.env.INIT_CWD;
|
|
238
|
+
const devStateWasSet = Object.prototype.hasOwnProperty.call(
|
|
239
|
+
process.env,
|
|
240
|
+
"DEV_WORKFLOW_STATE_FILE"
|
|
241
|
+
);
|
|
242
|
+
const originalDevState = process.env.DEV_WORKFLOW_STATE_FILE;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await mkdir(path.join(projectRoot, ".git"));
|
|
246
|
+
const nestedDir = path.join(projectRoot, "dist", "server");
|
|
247
|
+
await mkdir(nestedDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
process.chdir(projectRoot);
|
|
250
|
+
process.env.INIT_CWD = projectRoot;
|
|
251
|
+
if (devStateWasSet) {
|
|
252
|
+
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
253
|
+
}
|
|
254
|
+
const state = new WorkflowState();
|
|
255
|
+
assert.ok(
|
|
256
|
+
state.stateFile.startsWith(path.join(projectRoot, ".state", "users")),
|
|
257
|
+
"state file should resolve under project .state/users directory"
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
process.chdir(nestedDir);
|
|
261
|
+
delete process.env.INIT_CWD;
|
|
262
|
+
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
263
|
+
|
|
264
|
+
const state2 = new WorkflowState();
|
|
265
|
+
const resolvedProjectRoot = await realpath(projectRoot);
|
|
266
|
+
assert.ok(
|
|
267
|
+
state2.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
|
|
268
|
+
"state file should resolve to project root"
|
|
269
|
+
);
|
|
270
|
+
} finally {
|
|
271
|
+
process.chdir(originalCwd);
|
|
272
|
+
if (initWasSet) {
|
|
273
|
+
process.env.INIT_CWD = originalInitCwd;
|
|
274
|
+
} else {
|
|
275
|
+
delete process.env.INIT_CWD;
|
|
276
|
+
}
|
|
277
|
+
if (devStateWasSet) {
|
|
278
|
+
process.env.DEV_WORKFLOW_STATE_FILE = originalDevState;
|
|
279
|
+
} else {
|
|
280
|
+
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
281
|
+
}
|
|
282
|
+
await rm(projectRoot, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
67
286
|
test("WorkflowState reset keeps history but clears progress", async () => {
|
|
68
287
|
await withWorkflowState(async (state) => {
|
|
69
288
|
state.state.currentPhase = "ready";
|
package/tools/definitions.js
CHANGED
|
@@ -225,5 +225,108 @@ export function getToolList() {
|
|
|
225
225
|
properties: {},
|
|
226
226
|
},
|
|
227
227
|
},
|
|
228
|
+
{
|
|
229
|
+
name: "rerun_workflow",
|
|
230
|
+
description:
|
|
231
|
+
"Reset and restart the current task from the beginning. Useful when you want to replay the entire workflow from start to finish.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "run_full_workflow",
|
|
239
|
+
description:
|
|
240
|
+
"Execute every workflow step in sequence using the provided arguments (mark fixed → tests → run tests → docs → ready check → commit → release → complete).",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
summary: {
|
|
245
|
+
type: "string",
|
|
246
|
+
description: "Brief summary recorded when marking the bug/feature as fixed",
|
|
247
|
+
},
|
|
248
|
+
testsPassed: {
|
|
249
|
+
type: "boolean",
|
|
250
|
+
description: "Whether tests passed (defaults to true)",
|
|
251
|
+
},
|
|
252
|
+
testCommand: {
|
|
253
|
+
type: "string",
|
|
254
|
+
description: "Command used to run tests",
|
|
255
|
+
},
|
|
256
|
+
testDetails: {
|
|
257
|
+
type: "string",
|
|
258
|
+
description: "Additional test output or notes",
|
|
259
|
+
},
|
|
260
|
+
documentationType: {
|
|
261
|
+
type: "string",
|
|
262
|
+
enum: ["README", "inline-comments", "API-docs", "changelog", "other"],
|
|
263
|
+
description: "Type of documentation created",
|
|
264
|
+
},
|
|
265
|
+
documentationSummary: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "Summary of documentation updates",
|
|
268
|
+
},
|
|
269
|
+
commitMessage: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Commit message used for commit_and_push and complete_task",
|
|
272
|
+
},
|
|
273
|
+
branch: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "Optional branch name to push to",
|
|
276
|
+
},
|
|
277
|
+
releaseCommand: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Release command executed during perform_release",
|
|
280
|
+
},
|
|
281
|
+
releaseNotes: {
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "Optional release notes to record",
|
|
284
|
+
},
|
|
285
|
+
releaseType: {
|
|
286
|
+
type: "string",
|
|
287
|
+
description: "Optional release type override (major/minor/patch)",
|
|
288
|
+
},
|
|
289
|
+
preset: {
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Optional preset argument for perform_release",
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
required: [
|
|
295
|
+
"summary",
|
|
296
|
+
"testCommand",
|
|
297
|
+
"documentationType",
|
|
298
|
+
"documentationSummary",
|
|
299
|
+
"commitMessage",
|
|
300
|
+
"releaseCommand",
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "project_summary",
|
|
306
|
+
description:
|
|
307
|
+
"Get a project knowledge summary that aggregates completed tasks, task types, and recent activity to help understand the project and fix bugs faster over time.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "project_summary_data",
|
|
315
|
+
description:
|
|
316
|
+
"Get the persisted project knowledge summary from a structured JSON file (project-summary.json). Falls back to in-memory if the file doesn't exist. This file can be used as a simple database for future features.",
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: "object",
|
|
319
|
+
properties: {},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "project_summary_db",
|
|
324
|
+
description:
|
|
325
|
+
"Get the per-user project knowledge summary from SQLite. Use DEV_WORKFLOW_USER_ID env var to isolate users.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
228
331
|
];
|
|
229
332
|
}
|