@programinglive/dev-workflow-mcp-server 1.7.16 ā 1.7.17
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 +17 -0
- package/README.md +3 -2
- package/package.json +4 -2
- package/scripts/release.cjs +28 -1
- package/tests/build.test.js +1 -1
- package/tests/handlers.test.js +39 -0
- package/tools/definitions.js +15 -0
- package/tools/handler-modules/shared.js +187 -0
- package/tools/handlers.js +80 -19
- package/workflow-state.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.7.17](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.16...v1.7.17) (2025-12-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### š Documentation
|
|
9
|
+
|
|
10
|
+
* update release notes for v1.7.16 ([0afd96b](https://github.com/programinglive/dev-workflow-mcp-server/commit/0afd96b754717953eb162f65426129457fc8626e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⨠Features
|
|
14
|
+
|
|
15
|
+
* allow skipping release step ([004e4fa](https://github.com/programinglive/dev-workflow-mcp-server/commit/004e4fae071e064b2c2efb9d69ecd97e794d3cb8))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### š§¹ Chores
|
|
19
|
+
|
|
20
|
+
* add @programinglive/commiter setup ([1d2a19d](https://github.com/programinglive/dev-workflow-mcp-server/commit/1d2a19d0453b50c6f0db35b15924b51a27c4891a))
|
|
21
|
+
|
|
5
22
|
### [1.7.16](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.15...v1.7.16) (2025-12-24)
|
|
6
23
|
|
|
7
24
|
|
package/README.md
CHANGED
|
@@ -512,7 +512,7 @@ This keeps workflow state local to each developer's machine.
|
|
|
512
512
|
- `create_documentation` - Mark documentation as created
|
|
513
513
|
- `check_ready_to_commit` - Verify all steps are complete
|
|
514
514
|
- `commit_and_push` - Commit and push changes
|
|
515
|
-
- `perform_release` - Record release details
|
|
515
|
+
- `perform_release` - Record release details (or use `skip_release` when the project has no release automation)
|
|
516
516
|
- `complete_task` - Mark task as complete and reset
|
|
517
517
|
- `force_complete_task` - Force completion with reason
|
|
518
518
|
- `drop_task` - Abandon current task
|
|
@@ -553,6 +553,7 @@ The tool will:
|
|
|
553
553
|
5. `check_ready_to_commit`
|
|
554
554
|
6. `commit_and_push` with `commitMessage` and optional `branch`
|
|
555
555
|
7. `perform_release` with `releaseCommand`, plus optional `releaseNotes`, `releaseType`, and `preset`
|
|
556
|
+
- Alternatively call `skip_release` with a justification when the repository has no Node-based release step (e.g., Python-only or docs-only tasks)
|
|
556
557
|
8. `complete_task` reusing `commitMessage`
|
|
557
558
|
|
|
558
559
|
All arguments except the optional flags are required and must be non-empty strings.
|
|
@@ -571,7 +572,7 @@ The package ships with a release guard (`release-wrapper.js`) that backs the `np
|
|
|
571
572
|
|
|
572
573
|
If any requirements are missing, the guard exits with guidance to return to the MCP tools. This prevents accidentally bumping versions or tagging releases outside the managed workflow. To release correctly:
|
|
573
574
|
|
|
574
|
-
1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client.
|
|
575
|
+
1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client, or `skip_release {"reason":"<explanation>"}` if no release applies.
|
|
575
576
|
2. The guard runs automatically, verifies the workflow state, and records the release before letting you finish with `complete_task`.
|
|
576
577
|
|
|
577
578
|
### Automated npm Publishing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.17",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"homepage": "https://devworkflow.programinglive.com/",
|
|
6
6
|
"mcpName": "io.github.programinglive/dev-workflow-mcp-server",
|
|
@@ -62,11 +62,13 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@commitlint/cli": "^20.2.0",
|
|
64
64
|
"@commitlint/config-conventional": "^20.2.0",
|
|
65
|
-
"@programinglive/commiter": "^1.2.
|
|
65
|
+
"@programinglive/commiter": "^1.2.17",
|
|
66
66
|
"autoprefixer": "^10.4.22",
|
|
67
|
+
"cross-env": "^10.1.0",
|
|
67
68
|
"husky": "^9.1.7",
|
|
68
69
|
"postcss": "^8.5.6",
|
|
69
70
|
"standard-version": "^9.5.0",
|
|
71
|
+
"supertest": "^7.1.4",
|
|
70
72
|
"tailwindcss": "^3.4.18"
|
|
71
73
|
},
|
|
72
74
|
"standard-version": {
|
package/scripts/release.cjs
CHANGED
|
@@ -11,6 +11,9 @@ const FS_FOK_PRELOAD_FLAG = buildPreloadFlag(fsFokPreloadPath);
|
|
|
11
11
|
|
|
12
12
|
require('./preload/fs-f-ok.cjs');
|
|
13
13
|
|
|
14
|
+
const DEFAULT_PRD_RELATIVE_PATH = path.join('docs', 'PRD.md');
|
|
15
|
+
const DEFAULT_PRD_DISPLAY_PATH = 'docs/PRD.md';
|
|
16
|
+
|
|
14
17
|
const VALID_RELEASE_TYPES = new Set([
|
|
15
18
|
'major',
|
|
16
19
|
'minor',
|
|
@@ -161,6 +164,8 @@ function runRelease({
|
|
|
161
164
|
throw new Error('Working tree has uncommitted changes. Commit or stash before running release.');
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
ensurePrdPresence({ cwd, dependencies });
|
|
168
|
+
|
|
164
169
|
const testResult = runProjectTests({ spawn, env });
|
|
165
170
|
if (testResult && typeof testResult.status === 'number' && testResult.status !== 0) {
|
|
166
171
|
return testResult;
|
|
@@ -219,9 +224,31 @@ module.exports = {
|
|
|
219
224
|
detectPackageManager,
|
|
220
225
|
runProjectTests,
|
|
221
226
|
runRelease,
|
|
222
|
-
isWorkingTreeClean
|
|
227
|
+
isWorkingTreeClean,
|
|
228
|
+
ensurePrdPresence
|
|
223
229
|
};
|
|
224
230
|
|
|
231
|
+
function ensurePrdPresence({ cwd = process.cwd(), dependencies = {} } = {}) {
|
|
232
|
+
const {
|
|
233
|
+
fsExistsSync = fs.existsSync,
|
|
234
|
+
logger = console,
|
|
235
|
+
prdRelativePath = DEFAULT_PRD_RELATIVE_PATH,
|
|
236
|
+
prdDisplayPath = DEFAULT_PRD_DISPLAY_PATH
|
|
237
|
+
} = dependencies;
|
|
238
|
+
|
|
239
|
+
const resolvedPath = path.isAbsolute(prdRelativePath)
|
|
240
|
+
? prdRelativePath
|
|
241
|
+
: path.join(cwd, prdRelativePath);
|
|
242
|
+
|
|
243
|
+
if (fsExistsSync(resolvedPath)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : console.warn.bind(console);
|
|
248
|
+
warn(`ā ļø Product Requirements Document missing (${prdDisplayPath}). Add ${DEFAULT_PRD_DISPLAY_PATH} so teams understand release expectations.`);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
225
252
|
function buildPreloadFlag(filePath) {
|
|
226
253
|
const resolved = path.resolve(filePath);
|
|
227
254
|
const escaped = resolved.includes(' ')
|
package/tests/build.test.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
const exec = promisify(execCallback);
|
|
9
9
|
|
|
10
10
|
test('build generates dist/index.mjs file', async () => {
|
|
11
|
-
const { stderr } = await exec('NODE_ENV=development npm run build:server');
|
|
11
|
+
const { stderr } = await exec('npx cross-env NODE_ENV=development npm run build:server');
|
|
12
12
|
// Allow the known vite warning about node:process
|
|
13
13
|
const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
|
|
14
14
|
const tailwindOutput = 'ā tailwindcss';
|
package/tests/handlers.test.js
CHANGED
|
@@ -970,6 +970,45 @@ test("perform_release accepts shorthand patch command", async () => {
|
|
|
970
970
|
});
|
|
971
971
|
});
|
|
972
972
|
|
|
973
|
+
test("skip_release records justification and advances to completion phase", async () => {
|
|
974
|
+
await withWorkflowState(async (workflowState) => {
|
|
975
|
+
workflowState.state.currentPhase = "release";
|
|
976
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
977
|
+
workflowState.state.readyCheckCompleted = true;
|
|
978
|
+
workflowState.state.lastCommitMessage = "chore: docs";
|
|
979
|
+
workflowState.state.releaseSkipped = false;
|
|
980
|
+
workflowState.state.released = false;
|
|
981
|
+
workflowState.state.releaseCommand = "";
|
|
982
|
+
await workflowState.save();
|
|
983
|
+
|
|
984
|
+
const response = await handleToolCall({
|
|
985
|
+
request: createRequest("skip_release", { reason: "Python project ā no npm release" }),
|
|
986
|
+
normalizeRequestArgs,
|
|
987
|
+
workflowState,
|
|
988
|
+
exec: async () => ({ stdout: "" }),
|
|
989
|
+
git: {
|
|
990
|
+
hasWorkingChanges: async () => false,
|
|
991
|
+
hasStagedChanges: async () => false,
|
|
992
|
+
hasTestChanges: async () => true,
|
|
993
|
+
getStagedChanges: async () => [],
|
|
994
|
+
getCurrentBranch: async () => "main",
|
|
995
|
+
getPrimaryBranch: async () => "main",
|
|
996
|
+
getLastCommitMessage: async () => "chore: docs",
|
|
997
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
998
|
+
},
|
|
999
|
+
utils,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const text = response.content[0].text;
|
|
1003
|
+
assert.ok(text.includes("Release step skipped"), "response should mention release was skipped");
|
|
1004
|
+
assert.equal(workflowState.state.releaseSkipped, true);
|
|
1005
|
+
assert.equal(workflowState.state.releaseSkippedReason, "Python project ā no npm release");
|
|
1006
|
+
assert.equal(workflowState.state.releaseCommand, "(skipped)");
|
|
1007
|
+
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
1008
|
+
assert.equal(workflowState.state.released, false);
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
973
1012
|
test("perform_release respects explicit releaseType option", async () => {
|
|
974
1013
|
await withWorkflowState(async (workflowState) => {
|
|
975
1014
|
workflowState.state.currentPhase = "release";
|
package/tools/definitions.js
CHANGED
|
@@ -148,6 +148,21 @@ export function getToolList() {
|
|
|
148
148
|
required: ["command"],
|
|
149
149
|
},
|
|
150
150
|
},
|
|
151
|
+
{
|
|
152
|
+
name: "skip_release",
|
|
153
|
+
description:
|
|
154
|
+
"Record that the release step was intentionally skipped (e.g., non-Node projects or documentation-only work). Requires a justification.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
reason: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "Why the release step is being skipped",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["reason"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
151
166
|
{
|
|
152
167
|
name: "complete_task",
|
|
153
168
|
description:
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
export const PROJECT_SUMMARY_FILENAME = "project-summary.json";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
export const textResponse = (text) => ({
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function isReleaseSatisfied(state) {
|
|
21
|
+
return Boolean(state?.released || state?.releaseSkipped);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
25
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const hasWorkingChanges = await git.hasWorkingChanges();
|
|
30
|
+
const hasStagedChanges = await git.hasStagedChanges();
|
|
31
|
+
|
|
32
|
+
if (!hasWorkingChanges && !hasStagedChanges) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
37
|
+
workflowState.state.released = false;
|
|
38
|
+
workflowState.state.releaseSkipped = false;
|
|
39
|
+
workflowState.state.releaseSkippedReason = "";
|
|
40
|
+
workflowState.state.currentPhase = "commit";
|
|
41
|
+
workflowState.state.releaseCommand = "";
|
|
42
|
+
workflowState.state.releaseNotes = "";
|
|
43
|
+
await workflowState.save();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatChecklist(checks) {
|
|
48
|
+
return checks.map((c) => `${c.done ? "ā
" : "ā"} ${c.name}`).join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function summarizeHistory(history, limit) {
|
|
52
|
+
const recent = history.slice(-limit).reverse();
|
|
53
|
+
if (recent.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const historyText = recent
|
|
58
|
+
.map(
|
|
59
|
+
(entry, index) =>
|
|
60
|
+
`${index + 1}. [${new Date(entry.timestamp).toLocaleString()}]\n Task: ${entry.taskDescription}\n Type: ${entry.taskType}\n Commit: ${entry.commitMessage}\n`
|
|
61
|
+
)
|
|
62
|
+
.join("\n");
|
|
63
|
+
|
|
64
|
+
return `š Workflow History (last ${recent.length} tasks)\n\n${historyText}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function generateProjectSummary(history) {
|
|
68
|
+
if (!history || history.length === 0) {
|
|
69
|
+
return "No workflow history available.";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const taskTypes = {};
|
|
73
|
+
const recentTasks = history.slice(-20);
|
|
74
|
+
|
|
75
|
+
for (const entry of recentTasks) {
|
|
76
|
+
const type = entry.taskType || "other";
|
|
77
|
+
taskTypes[type] = (taskTypes[type] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const typeSummary = Object.entries(taskTypes)
|
|
81
|
+
.map(([type, count]) => `${type}: ${count}`)
|
|
82
|
+
.join(", ");
|
|
83
|
+
|
|
84
|
+
const totalTasks = history.length;
|
|
85
|
+
const lastTask = history[history.length - 1];
|
|
86
|
+
const lastActive = lastTask ? new Date(lastTask.timestamp).toLocaleDateString() : "Never";
|
|
87
|
+
|
|
88
|
+
return `š§ Project Knowledge Summary\n\nš Stats\n- Total tasks completed: ${totalTasks}\n- Recent task mix: ${typeSummary}\n- Last active: ${lastActive}\n\nš Recent tasks (last 5)\n${history.slice(-5).reverse().map((e, i) => `${i + 1}. ${e.taskDescription} (${e.taskType})`).join("\n")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getContinueGuidance(status) {
|
|
92
|
+
switch (status.currentPhase) {
|
|
93
|
+
case "coding": {
|
|
94
|
+
const task = status.taskDescription ? ` "${status.taskDescription}"` : "";
|
|
95
|
+
return `Finish implementing${task} and then run 'mark_bug_fixed' with a brief summary of the change.`;
|
|
96
|
+
}
|
|
97
|
+
case "testing":
|
|
98
|
+
if (!status.testsCreated) {
|
|
99
|
+
return "Create automated coverage now using 'create_tests' before recording any results.";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (status.testsPassed) {
|
|
103
|
+
return "With tests green, move on to 'create_documentation' and capture what changed.";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (status.testsSkipped) {
|
|
107
|
+
return "Document why tests were skipped using 'create_documentation' so the workflow can proceed.";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return status.testCommand
|
|
111
|
+
? `Fix any failing tests and re-run '${status.testCommand}'. Once they pass, record the results with 'run_tests'.`
|
|
112
|
+
: "Run your automated tests and record the outcome with 'run_tests'. Fix any failures before moving forward.";
|
|
113
|
+
case "documentation":
|
|
114
|
+
return status.testsSkipped
|
|
115
|
+
? `Document the change (including manual QA for skipped tests) with 'create_documentation'. Reason recorded: ${status.testsSkippedReason || "(none provided)"}.`
|
|
116
|
+
: "Capture your updates with 'create_documentation' (provide the documentation type and a short summary).";
|
|
117
|
+
case "ready":
|
|
118
|
+
return "Run 'check_ready_to_commit' to verify all prerequisite steps before committing.";
|
|
119
|
+
case "commit":
|
|
120
|
+
return "All checks passedārun 'commit_and_push' with your commit message (and optional branch).";
|
|
121
|
+
case "release":
|
|
122
|
+
return "Commit recorded. Execute 'perform_release' with the release command you ran to publish changes, or call 'skip_release' with a justification if no release is required.";
|
|
123
|
+
case "ready_to_complete":
|
|
124
|
+
return "Release recorded. Finish up by calling 'complete_task' with the commit message you used.";
|
|
125
|
+
default:
|
|
126
|
+
return "Review your workflow progress and complete the next required step.";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function handleReadyCheck(workflowState) {
|
|
131
|
+
const status = workflowState.state;
|
|
132
|
+
const testsCreatedDone = status.testsSkipped || status.testsCreated;
|
|
133
|
+
const testsPassedDone = status.testsSkipped || status.testsPassed;
|
|
134
|
+
const checks = [
|
|
135
|
+
{ name: "Task started", done: status.currentPhase !== "idle" },
|
|
136
|
+
{ name: "Feature/bug fixed", done: status.bugFixed },
|
|
137
|
+
{
|
|
138
|
+
name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
|
|
139
|
+
done: testsCreatedDone,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: status.testsSkipped ? "Tests skipped acknowledged" : "Tests passed",
|
|
143
|
+
done: testsPassedDone,
|
|
144
|
+
},
|
|
145
|
+
{ name: "Documentation created", done: status.documentationCreated },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const allDone = checks.every((c) => c.done);
|
|
149
|
+
const checkList = formatChecklist(checks);
|
|
150
|
+
|
|
151
|
+
workflowState.state.readyCheckCompleted = allDone;
|
|
152
|
+
if (allDone) {
|
|
153
|
+
if (workflowState.state.commitAndPushCompleted) {
|
|
154
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
155
|
+
} else {
|
|
156
|
+
workflowState.state.currentPhase = "commit";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await workflowState.save();
|
|
160
|
+
|
|
161
|
+
if (allDone) {
|
|
162
|
+
return textResponse(
|
|
163
|
+
`š ALL CHECKS PASSED!\n\n${checkList}\n\nā
Next actions:\n1. Run 'commit_and_push' (commits and pushes your changes)\n2. Run 'perform_release' (handles versioning, tags, and final push)\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to 'commit_and_push' to push to a non-default branch.\n\nTask: ${status.taskDescription}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return textResponse(`ā ļø NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function loadProjectSummary(workflowState) {
|
|
171
|
+
const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
|
|
172
|
+
try {
|
|
173
|
+
const data = await fs.readFile(summaryPath, "utf-8");
|
|
174
|
+
return JSON.parse(data);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error?.code === "ENOENT") {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const prdPath = path.join(__dirname, "..", "docs", "product", "PRD.md");
|
|
184
|
+
|
|
185
|
+
export function ensurePrdExists() {
|
|
186
|
+
return existsSync(prdPath);
|
|
187
|
+
}
|
package/tools/handlers.js
CHANGED
|
@@ -16,6 +16,10 @@ const textResponse = (text) => ({
|
|
|
16
16
|
],
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
function isReleaseSatisfied(state) {
|
|
20
|
+
return Boolean(state?.released || state?.releaseSkipped);
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
20
24
|
if (!workflowState.state.commitAndPushCompleted) {
|
|
21
25
|
return false;
|
|
@@ -30,6 +34,8 @@ async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
|
30
34
|
|
|
31
35
|
workflowState.state.commitAndPushCompleted = false;
|
|
32
36
|
workflowState.state.released = false;
|
|
37
|
+
workflowState.state.releaseSkipped = false;
|
|
38
|
+
workflowState.state.releaseSkippedReason = "";
|
|
33
39
|
workflowState.state.currentPhase = "commit";
|
|
34
40
|
workflowState.state.releaseCommand = "";
|
|
35
41
|
workflowState.state.releaseNotes = "";
|
|
@@ -113,7 +119,7 @@ function getContinueGuidance(status) {
|
|
|
113
119
|
case "commit":
|
|
114
120
|
return "All checks passedārun 'commit_and_push' with your commit message (and optional branch).";
|
|
115
121
|
case "release":
|
|
116
|
-
return "Commit recorded. Execute 'perform_release' with the release command you ran to publish changes.";
|
|
122
|
+
return "Commit recorded. Execute 'perform_release' with the release command you ran to publish changes, or call 'skip_release' with a justification if no release is required.";
|
|
117
123
|
case "ready_to_complete":
|
|
118
124
|
return "Release recorded. Finish up by calling 'complete_task' with the commit message you used.";
|
|
119
125
|
default:
|
|
@@ -185,7 +191,7 @@ async function handleStartTask(args, workflowState) {
|
|
|
185
191
|
await workflowState.save();
|
|
186
192
|
|
|
187
193
|
return textResponse(
|
|
188
|
-
`ā
Task Started: ${args.description}\n\nšÆ Be conscious about what you're coding!\n\nWorkflow Steps:\n1. ā Start task (current)\n2. ā³ Fix/implement the feature\n3. ā³ Create tests\n4. ā³ Run tests (must pass!)\n5. ā³ Create documentation\n6. ā³ Run 'check_ready_to_commit'\n7. ā³ Run 'commit_and_push' (commits and pushes)\n8. ā³ Run 'perform_release' (handles versioning and tags)\n9. ā³ Complete task\n\nReminder: Focus on writing clean, maintainable code!`
|
|
194
|
+
`ā
Task Started: ${args.description}\n\nšÆ Be conscious about what you're coding!\n\nWorkflow Steps:\n1. ā Start task (current)\n2. ā³ Fix/implement the feature\n3. ā³ Create tests\n4. ā³ Run tests (must pass!)\n5. ā³ Create documentation\n6. ā³ Run 'check_ready_to_commit'\n7. ā³ Run 'commit_and_push' (commits and pushes)\n8. ā³ Run 'perform_release' (handles versioning and tags) or 'skip_release' (when no release is needed)\n9. ā³ Complete task\n\nReminder: Focus on writing clean, maintainable code!`
|
|
189
195
|
);
|
|
190
196
|
}
|
|
191
197
|
|
|
@@ -225,6 +231,8 @@ async function handleMarkBugFixed(args, workflowState) {
|
|
|
225
231
|
workflowState.state.fixSummary = args.summary;
|
|
226
232
|
workflowState.state.readyCheckCompleted = false;
|
|
227
233
|
workflowState.state.released = false;
|
|
234
|
+
workflowState.state.releaseSkipped = false;
|
|
235
|
+
workflowState.state.releaseSkippedReason = "";
|
|
228
236
|
workflowState.state.releaseCommand = "";
|
|
229
237
|
workflowState.state.releaseNotes = "";
|
|
230
238
|
workflowState.state.commitAndPushCompleted = false;
|
|
@@ -233,7 +241,7 @@ async function handleMarkBugFixed(args, workflowState) {
|
|
|
233
241
|
await workflowState.save();
|
|
234
242
|
|
|
235
243
|
return textResponse(
|
|
236
|
-
`ā
Feature/Bug marked as fixed!\n\nā ļø CRITICAL REMINDER: You MUST create tests now!\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā³ Create tests for: ${args.summary}\n3. ā³ Run tests (must be green!)\n4. ā³ Create documentation\n5. ā³ Run 'check_ready_to_commit'\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release'\n8. ā³ Complete task\n\nš« DO NOT SKIP TESTING!`
|
|
244
|
+
`ā
Feature/Bug marked as fixed!\n\nā ļø CRITICAL REMINDER: You MUST create tests now!\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā³ Create tests for: ${args.summary}\n3. ā³ Run tests (must be green!)\n4. ā³ Create documentation\n5. ā³ Run 'check_ready_to_commit'\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release' (or 'skip_release' if no release applies)\n8. ā³ Complete task\n\nš« DO NOT SKIP TESTING!`
|
|
237
245
|
);
|
|
238
246
|
}
|
|
239
247
|
|
|
@@ -247,13 +255,15 @@ async function handleCreateTests(workflowState) {
|
|
|
247
255
|
workflowState.state.testsSkippedReason = "";
|
|
248
256
|
workflowState.state.readyCheckCompleted = false;
|
|
249
257
|
workflowState.state.released = false;
|
|
258
|
+
workflowState.state.releaseSkipped = false;
|
|
259
|
+
workflowState.state.releaseSkippedReason = "";
|
|
250
260
|
workflowState.state.commitAndPushCompleted = false;
|
|
251
261
|
workflowState.state.lastCommitMessage = "";
|
|
252
262
|
workflowState.state.lastPushBranch = "";
|
|
253
263
|
await workflowState.save();
|
|
254
264
|
|
|
255
265
|
return textResponse(
|
|
256
|
-
"ā
Tests recorded!\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā Create tests\n3. ā³ Run tests (must be green!)\n4. ā³ Create documentation\n5. ā³ Run 'check_ready_to_commit'\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release'\n8. ā³ Complete task\n\nš§Ŗ Run your test command and record the results using 'run_tests'."
|
|
266
|
+
"ā
Tests recorded!\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā Create tests\n3. ā³ Run tests (must be green!)\n4. ā³ Create documentation\n5. ā³ Run 'check_ready_to_commit'\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release' (or 'skip_release' if appropriate)\n8. ā³ Complete task\n\nš§Ŗ Run your test command and record the results using 'run_tests'."
|
|
257
267
|
);
|
|
258
268
|
}
|
|
259
269
|
|
|
@@ -280,6 +290,8 @@ async function handleSkipTests(args, workflowState) {
|
|
|
280
290
|
workflowState.state.readyToCommit = false;
|
|
281
291
|
workflowState.state.readyCheckCompleted = false;
|
|
282
292
|
workflowState.state.released = false;
|
|
293
|
+
workflowState.state.releaseSkipped = false;
|
|
294
|
+
workflowState.state.releaseSkippedReason = "";
|
|
283
295
|
workflowState.state.commitAndPushCompleted = false;
|
|
284
296
|
workflowState.state.lastCommitMessage = "";
|
|
285
297
|
workflowState.state.lastPushBranch = "";
|
|
@@ -297,7 +309,7 @@ Next Steps:
|
|
|
297
309
|
3. ā³ Create/update documentation
|
|
298
310
|
4. ā³ Run 'check_ready_to_commit'
|
|
299
311
|
5. ā³ Run 'commit_and_push'
|
|
300
|
-
6. ā³ Run 'perform_release'
|
|
312
|
+
6. ā³ Run 'perform_release' or 'skip_release'
|
|
301
313
|
7. ā³ Complete task`
|
|
302
314
|
);
|
|
303
315
|
}
|
|
@@ -328,6 +340,8 @@ async function handleRunTests(args, workflowState) {
|
|
|
328
340
|
workflowState.state.testsSkippedReason = "";
|
|
329
341
|
workflowState.state.readyCheckCompleted = false;
|
|
330
342
|
workflowState.state.released = false;
|
|
343
|
+
workflowState.state.releaseSkipped = false;
|
|
344
|
+
workflowState.state.releaseSkippedReason = "";
|
|
331
345
|
workflowState.state.commitAndPushCompleted = false;
|
|
332
346
|
workflowState.state.lastCommitMessage = "";
|
|
333
347
|
workflowState.state.lastPushBranch = "";
|
|
@@ -345,7 +359,7 @@ async function handleRunTests(args, workflowState) {
|
|
|
345
359
|
await workflowState.save();
|
|
346
360
|
|
|
347
361
|
return textResponse(
|
|
348
|
-
`ā
All tests passed! š\n\nTest command: ${args.testCommand}\n\nš Now create or update documentation using 'create_documentation' with:\n- documentationType: "PRD", "README", "RELEASE_NOTES", "inline-comments", "API-docs", "changelog", or "other"\n- summary: Brief description of what was documented\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā Create tests\n3. ā Run tests (GREEN!)\n4. ā³ Create/update documentation\n5. ā³ Run 'check_ready_to_commit'\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release'\n8. ā³ Complete task`
|
|
362
|
+
`ā
All tests passed! š\n\nTest command: ${args.testCommand}\n\nš Now create or update documentation using 'create_documentation' with:\n- documentationType: "PRD", "README", "RELEASE_NOTES", "inline-comments", "API-docs", "changelog", or "other"\n- summary: Brief description of what was documented\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā Create tests\n3. ā Run tests (GREEN!)\n4. ā³ Create/update documentation\n5. ā³ Run 'check_ready_to_commit'\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release' (or use 'skip_release' if no release is needed)\n8. ā³ Complete task`
|
|
349
363
|
);
|
|
350
364
|
}
|
|
351
365
|
|
|
@@ -371,13 +385,58 @@ async function handleCreateDocumentation(args, workflowState) {
|
|
|
371
385
|
workflowState.state.readyToCommit = true;
|
|
372
386
|
workflowState.state.readyCheckCompleted = false;
|
|
373
387
|
workflowState.state.released = false;
|
|
388
|
+
workflowState.state.releaseSkipped = false;
|
|
389
|
+
workflowState.state.releaseSkippedReason = "";
|
|
374
390
|
workflowState.state.commitAndPushCompleted = false;
|
|
375
391
|
workflowState.state.lastCommitMessage = "";
|
|
376
392
|
workflowState.state.lastPushBranch = "";
|
|
377
393
|
await workflowState.save();
|
|
378
394
|
|
|
379
395
|
return textResponse(
|
|
380
|
-
`ā
Documentation created/updated!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\nā
PRD verified: docs/product/PRD.md exists\n\nš You're ready to verify your work!\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā Create tests\n3. ā Run tests (GREEN!)\n4. ā Create/update documentation\n5. ā³ Run 'check_ready_to_commit' to verify\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release'\n8. ā³ Mark as complete with 'complete_task'`
|
|
396
|
+
`ā
Documentation created/updated!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\nā
PRD verified: docs/product/PRD.md exists\n\nš You're ready to verify your work!\n\nNext Steps:\n1. ā Fix/implement feature\n2. ā Create tests\n3. ā Run tests (GREEN!)\n4. ā Create/update documentation\n5. ā³ Run 'check_ready_to_commit' to verify\n6. ā³ Run 'commit_and_push'\n7. ā³ Run 'perform_release' (or 'skip_release' when applicable)\n8. ā³ Mark as complete with 'complete_task'`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function handleSkipRelease(args, workflowState) {
|
|
401
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
402
|
+
return textResponse("ā ļø Please start a task first using 'start_task' before skipping release.");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
406
|
+
if (!reason) {
|
|
407
|
+
return textResponse("ā ļø Provide a non-empty 'reason' explaining why the release step is being skipped.");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (workflowState.state.releaseSkipped) {
|
|
411
|
+
workflowState.state.releaseSkippedReason = reason;
|
|
412
|
+
await workflowState.save();
|
|
413
|
+
return textResponse(
|
|
414
|
+
`ā¹ļø Release step was already skipped for this task.\nUpdated reason: ${reason}\n\nNext: run 'complete_task' to wrap up.`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (workflowState.state.released) {
|
|
419
|
+
return textResponse("ā ļø A release command has already been recorded. Continue with 'complete_task'.");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
423
|
+
return textResponse("ā ļø Please run 'check_ready_to_commit' and ensure all prerequisites pass before skipping release.");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
427
|
+
return textResponse("ā ļø Please complete 'commit_and_push' before skipping release.");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
workflowState.state.releaseSkipped = true;
|
|
431
|
+
workflowState.state.releaseSkippedReason = reason;
|
|
432
|
+
workflowState.state.released = false;
|
|
433
|
+
workflowState.state.releaseCommand = "(skipped)";
|
|
434
|
+
workflowState.state.releaseNotes = "";
|
|
435
|
+
workflowState.state.currentPhase = "ready_to_complete";
|
|
436
|
+
await workflowState.save();
|
|
437
|
+
|
|
438
|
+
return textResponse(
|
|
439
|
+
`ā ļø Release step skipped.\nReason: ${reason}\n\nā
Next: run 'complete_task' with your final commit message to close out the workflow.`
|
|
381
440
|
);
|
|
382
441
|
}
|
|
383
442
|
|
|
@@ -404,11 +463,11 @@ async function handleReadyCheck(workflowState) {
|
|
|
404
463
|
|
|
405
464
|
workflowState.state.readyCheckCompleted = allDone;
|
|
406
465
|
if (allDone) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
466
|
+
if (workflowState.state.commitAndPushCompleted) {
|
|
467
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
468
|
+
} else {
|
|
469
|
+
workflowState.state.currentPhase = "commit";
|
|
470
|
+
}
|
|
412
471
|
}
|
|
413
472
|
await workflowState.save();
|
|
414
473
|
|
|
@@ -448,7 +507,7 @@ async function handleCommitAndPush(args, context) {
|
|
|
448
507
|
workflowState.state.commitAndPushCompleted = true;
|
|
449
508
|
workflowState.state.lastCommitMessage = effectiveCommitMessage || "";
|
|
450
509
|
workflowState.state.lastPushBranch = branchForPush;
|
|
451
|
-
workflowState.state.currentPhase = workflowState.state
|
|
510
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
452
511
|
await workflowState.save();
|
|
453
512
|
|
|
454
513
|
return textResponse(
|
|
@@ -457,10 +516,10 @@ async function handleCommitAndPush(args, context) {
|
|
|
457
516
|
);
|
|
458
517
|
}
|
|
459
518
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
);
|
|
519
|
+
const statusOutput = await git.getStatusOutput();
|
|
520
|
+
|
|
521
|
+
if (!workflowState.state.testsSkipped && !git.containsTestFilesInStatus(statusOutput)) {
|
|
522
|
+
return textResponse("ā ļø Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.");
|
|
464
523
|
}
|
|
465
524
|
|
|
466
525
|
try {
|
|
@@ -514,7 +573,7 @@ async function handleCommitAndPush(args, context) {
|
|
|
514
573
|
workflowState.state.commitAndPushCompleted = true;
|
|
515
574
|
workflowState.state.lastCommitMessage = generatedSummary;
|
|
516
575
|
workflowState.state.lastPushBranch = branchForPush;
|
|
517
|
-
workflowState.state.currentPhase = workflowState.state
|
|
576
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
518
577
|
await workflowState.save();
|
|
519
578
|
|
|
520
579
|
return textResponse(
|
|
@@ -936,7 +995,7 @@ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils })
|
|
|
936
995
|
continue;
|
|
937
996
|
}
|
|
938
997
|
|
|
939
|
-
if (!state
|
|
998
|
+
if (!isReleaseSatisfied(state)) {
|
|
940
999
|
const resp = await executeStep("perform_release", handlePerformRelease, [
|
|
941
1000
|
releaseArgs,
|
|
942
1001
|
{ workflowState, exec, git, utils },
|
|
@@ -996,6 +1055,8 @@ export async function handleToolCall({
|
|
|
996
1055
|
return handleCommitAndPush(args, { workflowState, exec, git, utils });
|
|
997
1056
|
case "perform_release":
|
|
998
1057
|
return handlePerformRelease(args, { workflowState, exec, git, utils });
|
|
1058
|
+
case "skip_release":
|
|
1059
|
+
return handleSkipRelease(args, workflowState);
|
|
999
1060
|
case "complete_task":
|
|
1000
1061
|
return handleCompleteTask(args, workflowState);
|
|
1001
1062
|
case "force_complete_task":
|
package/workflow-state.js
CHANGED
|
@@ -440,6 +440,8 @@ export class WorkflowState {
|
|
|
440
440
|
readyToCommit: false,
|
|
441
441
|
readyCheckCompleted: false,
|
|
442
442
|
released: false,
|
|
443
|
+
releaseSkipped: false,
|
|
444
|
+
releaseSkippedReason: "",
|
|
443
445
|
releaseCommand: "",
|
|
444
446
|
releaseNotes: "",
|
|
445
447
|
commitAndPushCompleted: false,
|
|
@@ -477,6 +479,12 @@ export class WorkflowState {
|
|
|
477
479
|
if (typeof this.state.released !== "boolean") {
|
|
478
480
|
this.state.released = false;
|
|
479
481
|
}
|
|
482
|
+
if (typeof this.state.releaseSkipped !== "boolean") {
|
|
483
|
+
this.state.releaseSkipped = false;
|
|
484
|
+
}
|
|
485
|
+
if (typeof this.state.releaseSkippedReason !== "string") {
|
|
486
|
+
this.state.releaseSkippedReason = "";
|
|
487
|
+
}
|
|
480
488
|
if (typeof this.state.commitAndPushCompleted !== "boolean") {
|
|
481
489
|
this.state.commitAndPushCompleted = false;
|
|
482
490
|
}
|
|
@@ -622,6 +630,8 @@ export class WorkflowState {
|
|
|
622
630
|
readyToCommit: false,
|
|
623
631
|
readyCheckCompleted: false,
|
|
624
632
|
released: false,
|
|
633
|
+
releaseSkipped: false,
|
|
634
|
+
releaseSkippedReason: "",
|
|
625
635
|
releaseCommand: "",
|
|
626
636
|
releaseNotes: "",
|
|
627
637
|
commitAndPushCompleted: false,
|