@bicorne/task-flow 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/LICENSE +21 -0
- package/README.md +252 -0
- package/SKILL.md +250 -0
- package/assets/.harnessrc +10 -0
- package/assets/PHASE-task.json.example +50 -0
- package/assets/design.md +69 -0
- package/assets/hooks.json +15 -0
- package/assets/product-requirement.md +82 -0
- package/assets/schema.json +127 -0
- package/assets/tasks.md +26 -0
- package/dist/commands/analyze.d.ts +32 -0
- package/dist/commands/analyze.js +338 -0
- package/dist/commands/archive.d.ts +11 -0
- package/dist/commands/archive.js +53 -0
- package/dist/commands/design.d.ts +38 -0
- package/dist/commands/design.js +492 -0
- package/dist/commands/extract.d.ts +31 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +165 -0
- package/dist/commands/merge/index.d.ts +17 -0
- package/dist/commands/merge/index.js +322 -0
- package/dist/commands/merge/merger.d.ts +18 -0
- package/dist/commands/merge/merger.js +151 -0
- package/dist/commands/merge/types.d.ts +67 -0
- package/dist/commands/merge/types.js +6 -0
- package/dist/commands/merge/validators.d.ts +14 -0
- package/dist/commands/merge/validators.js +147 -0
- package/dist/commands/merge.d.ts +7 -0
- package/dist/commands/merge.js +15 -0
- package/dist/commands/start.d.ts +32 -0
- package/dist/commands/start.js +265 -0
- package/dist/commands/status.d.ts +15 -0
- package/dist/commands/status.js +143 -0
- package/dist/commands/sync.d.ts +11 -0
- package/dist/commands/sync.js +58 -0
- package/dist/commands/tasks-gen/doc-parser.d.ts +7 -0
- package/dist/commands/tasks-gen/doc-parser.js +259 -0
- package/dist/commands/tasks-gen/generators.d.ts +33 -0
- package/dist/commands/tasks-gen/generators.js +141 -0
- package/dist/commands/tasks-gen/index.d.ts +30 -0
- package/dist/commands/tasks-gen/index.js +345 -0
- package/dist/commands/tasks-gen/parsers.d.ts +29 -0
- package/dist/commands/tasks-gen/parsers.js +272 -0
- package/dist/commands/tasks-gen/templates.d.ts +8 -0
- package/dist/commands/tasks-gen/templates.js +37 -0
- package/dist/commands/tasks-gen/types.d.ts +71 -0
- package/dist/commands/tasks-gen/types.js +17 -0
- package/dist/commands/tasks-gen/validators.d.ts +14 -0
- package/dist/commands/tasks-gen/validators.js +54 -0
- package/dist/commands/tasks.d.ts +9 -0
- package/dist/commands/tasks.js +22 -0
- package/dist/commands/worktree.d.ts +28 -0
- package/dist/commands/worktree.js +275 -0
- package/dist/hooks/check-prd-exists.d.ts +20 -0
- package/dist/hooks/check-prd-exists.js +61 -0
- package/dist/hooks/check-worktree-conflict.d.ts +34 -0
- package/dist/hooks/check-worktree-conflict.js +107 -0
- package/dist/hooks/hook-runner/executor.d.ts +18 -0
- package/dist/hooks/hook-runner/executor.js +143 -0
- package/dist/hooks/hook-runner/index.d.ts +64 -0
- package/dist/hooks/hook-runner/index.js +220 -0
- package/dist/hooks/hook-runner/loader.d.ts +23 -0
- package/dist/hooks/hook-runner/loader.js +126 -0
- package/dist/hooks/hook-runner/types.d.ts +59 -0
- package/dist/hooks/hook-runner/types.js +6 -0
- package/dist/hooks/hook-runner.d.ts +9 -0
- package/dist/hooks/hook-runner.js +30 -0
- package/dist/hooks/phase-complete-detector.d.ts +35 -0
- package/dist/hooks/phase-complete-detector.js +203 -0
- package/dist/hooks/phase-gate-validator.d.ts +76 -0
- package/dist/hooks/phase-gate-validator.js +407 -0
- package/dist/hooks/save-checkpoint.d.ts +43 -0
- package/dist/hooks/save-checkpoint.js +144 -0
- package/dist/hooks/start-mcp-servers.d.ts +50 -0
- package/dist/hooks/start-mcp-servers.js +75 -0
- package/dist/hooks/stop-mcp-servers.d.ts +40 -0
- package/dist/hooks/stop-mcp-servers.js +58 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +238 -0
- package/dist/lib/archive.d.ts +31 -0
- package/dist/lib/archive.js +226 -0
- package/dist/lib/config.d.ts +93 -0
- package/dist/lib/config.js +251 -0
- package/dist/lib/constants.d.ts +222 -0
- package/dist/lib/constants.js +247 -0
- package/dist/lib/interactive.d.ts +31 -0
- package/dist/lib/interactive.js +166 -0
- package/dist/lib/mcp-client.d.ts +156 -0
- package/dist/lib/mcp-client.js +370 -0
- package/dist/lib/state.d.ts +119 -0
- package/dist/lib/state.js +293 -0
- package/dist/slash/executor.d.ts +22 -0
- package/dist/slash/executor.js +259 -0
- package/dist/slash/index.d.ts +11 -0
- package/dist/slash/index.js +45 -0
- package/dist/slash/parser.d.ts +24 -0
- package/dist/slash/parser.js +101 -0
- package/dist/slash/registry.d.ts +22 -0
- package/dist/slash/registry.js +155 -0
- package/dist/spec/openspec-to-task/builders.d.ts +107 -0
- package/dist/spec/openspec-to-task/builders.js +138 -0
- package/dist/spec/openspec-to-task/index.d.ts +20 -0
- package/dist/spec/openspec-to-task/index.js +182 -0
- package/dist/spec/openspec-to-task/parsers.d.ts +65 -0
- package/dist/spec/openspec-to-task/parsers.js +232 -0
- package/dist/spec/openspec-to-task/types.d.ts +49 -0
- package/dist/spec/openspec-to-task/types.js +6 -0
- package/dist/spec/sync-openspec-to-task.d.ts +7 -0
- package/dist/spec/sync-openspec-to-task.js +21 -0
- package/dist/spec/sync-task-to-openspec.d.ts +27 -0
- package/dist/spec/sync-task-to-openspec.js +288 -0
- package/dist/types/ai-context.d.ts +108 -0
- package/dist/types/ai-context.js +9 -0
- package/package.json +66 -0
- package/references/AI-CONVERSATION-TUTORIAL.md +270 -0
- package/references/CLI-TUTORIAL.md +447 -0
- package/references/GIT-WORKTREE-SOP.md +109 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* hooks/phase-gate-validator.ts
|
|
4
|
+
* Validate phase transition gates
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.normalizePath = normalizePath;
|
|
41
|
+
exports.normalizeValidationName = normalizeValidationName;
|
|
42
|
+
exports.getValidationAliases = getValidationAliases;
|
|
43
|
+
exports.isPassedStatus = isPassedStatus;
|
|
44
|
+
exports.parseReviewConclusion = parseReviewConclusion;
|
|
45
|
+
exports.parseReviewValidationMap = parseReviewValidationMap;
|
|
46
|
+
exports.parseReviewReport = parseReviewReport;
|
|
47
|
+
exports.deriveTransitionFromFilePath = deriveTransitionFromFilePath;
|
|
48
|
+
exports.getTaskFilePath = getTaskFilePath;
|
|
49
|
+
exports.readTaskJson = readTaskJson;
|
|
50
|
+
exports.getValidationItems = getValidationItems;
|
|
51
|
+
exports.getPlanningSummary = getPlanningSummary;
|
|
52
|
+
exports.getPlanningList = getPlanningList;
|
|
53
|
+
exports.getSnapshotValidationMap = getSnapshotValidationMap;
|
|
54
|
+
exports.hasValidationPassed = hasValidationPassed;
|
|
55
|
+
exports.assessTransitionGate = assessTransitionGate;
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const config_1 = require("../lib/config");
|
|
59
|
+
const state_1 = require("../lib/state");
|
|
60
|
+
const constants_1 = require("../lib/constants");
|
|
61
|
+
function normalizePath(inputPath, projectRoot) {
|
|
62
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
const absolutePath = path.isAbsolute(inputPath) ? inputPath : path.resolve(projectRoot, inputPath);
|
|
66
|
+
return path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
67
|
+
}
|
|
68
|
+
function normalizeValidationName(name) {
|
|
69
|
+
return String(name || '')
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/_/g, '-')
|
|
73
|
+
.replace(/\s+/g, '-');
|
|
74
|
+
}
|
|
75
|
+
function getValidationAliases(validationName) {
|
|
76
|
+
const normalized = normalizeValidationName(validationName);
|
|
77
|
+
return constants_1.VALIDATION_ALIASES[normalized] || [normalized];
|
|
78
|
+
}
|
|
79
|
+
function isPassedStatus(status) {
|
|
80
|
+
if (!status) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const normalized = status.trim().toLowerCase();
|
|
84
|
+
return constants_1.VALIDATED_STATUSES.includes(normalized);
|
|
85
|
+
}
|
|
86
|
+
function parseReviewConclusion(content) {
|
|
87
|
+
const patterns = [
|
|
88
|
+
{ regex: /^\s*(?:结论 |Conclusion)\s*:\s*PASS\b/m, result: constants_1.REVIEW_CONCLUSION.PASS },
|
|
89
|
+
{ regex: /^\s*(?:结论 |Conclusion)\s*:\s*NEEDS_WORK\b/m, result: constants_1.REVIEW_CONCLUSION.NEEDS_WORK },
|
|
90
|
+
{ regex: /^\s*(?:结论 |Conclusion)\s*:\s*FAIL\b/m, result: constants_1.REVIEW_CONCLUSION.FAIL },
|
|
91
|
+
{ regex: /^##\s*结论\s*:\s*PASS\b/m, result: constants_1.REVIEW_CONCLUSION.PASS },
|
|
92
|
+
{ regex: /^##\s*结论\s*:\s*NEEDS_WORK\b/m, result: constants_1.REVIEW_CONCLUSION.NEEDS_WORK },
|
|
93
|
+
{ regex: /^##\s*结论\s*:\s*FAIL\b/m, result: constants_1.REVIEW_CONCLUSION.FAIL },
|
|
94
|
+
{ regex: /^#\s*PASS\b/m, result: constants_1.REVIEW_CONCLUSION.PASS },
|
|
95
|
+
{ regex: /^#\s*NEEDS_WORK\b/m, result: constants_1.REVIEW_CONCLUSION.NEEDS_WORK },
|
|
96
|
+
{ regex: /^#\s*FAIL\b/m, result: constants_1.REVIEW_CONCLUSION.FAIL },
|
|
97
|
+
{ regex: /\*\*结论\*\*\s*:\s*PASS\b/, result: constants_1.REVIEW_CONCLUSION.PASS },
|
|
98
|
+
{ regex: /\*\*结论\*\*\s*:\s*NEEDS_WORK\b/, result: constants_1.REVIEW_CONCLUSION.NEEDS_WORK },
|
|
99
|
+
{ regex: /\*\*结论\*\*\s*:\s*FAIL\b/, result: constants_1.REVIEW_CONCLUSION.FAIL },
|
|
100
|
+
{ regex: /\*\*PASS\*\*/, result: constants_1.REVIEW_CONCLUSION.PASS },
|
|
101
|
+
{ regex: /\*\*NEEDS_WORK\*\*/, result: constants_1.REVIEW_CONCLUSION.NEEDS_WORK },
|
|
102
|
+
{ regex: /\*\*FAIL\*\*/, result: constants_1.REVIEW_CONCLUSION.FAIL },
|
|
103
|
+
// Additional pattern for "结论 : NEEDS_WORK" format (with space before colon)
|
|
104
|
+
{ regex: /结论\s*:\s*NEEDS_WORK\b/, result: constants_1.REVIEW_CONCLUSION.NEEDS_WORK },
|
|
105
|
+
{ regex: /结论\s*:\s*PASS\b/, result: constants_1.REVIEW_CONCLUSION.PASS },
|
|
106
|
+
{ regex: /结论\s*:\s*FAIL\b/, result: constants_1.REVIEW_CONCLUSION.FAIL },
|
|
107
|
+
];
|
|
108
|
+
for (const { regex, result } of patterns) {
|
|
109
|
+
if (regex.test(content)) {
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function parseReviewValidationMap(content) {
|
|
116
|
+
const validationMap = {};
|
|
117
|
+
const regex = /^\s*[-*]?\s*([a-z][a-z0-9_-]*)\s*:\s*([A-Za-z0-9_-]+)\s*$/gm;
|
|
118
|
+
let match = regex.exec(content);
|
|
119
|
+
while (match) {
|
|
120
|
+
const key = match[1];
|
|
121
|
+
const value = match[2];
|
|
122
|
+
if (key && value) {
|
|
123
|
+
validationMap[normalizeValidationName(key)] = value;
|
|
124
|
+
}
|
|
125
|
+
match = regex.exec(content);
|
|
126
|
+
}
|
|
127
|
+
return validationMap;
|
|
128
|
+
}
|
|
129
|
+
function parseReviewReport(content) {
|
|
130
|
+
const conclusion = parseReviewConclusion(content);
|
|
131
|
+
const validationMap = parseReviewValidationMap(content);
|
|
132
|
+
if (conclusion && !validationMap['review-conclusion']) {
|
|
133
|
+
validationMap['review-conclusion'] = conclusion;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
conclusion,
|
|
137
|
+
validationMap,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function deriveTransitionFromFilePath(filePath, config) {
|
|
141
|
+
const relativePath = normalizePath(filePath, config.projectRoot);
|
|
142
|
+
// Security: Prevent path traversal by ensuring path doesn't escape expected directories
|
|
143
|
+
if (relativePath.includes('..') || (relativePath.startsWith('/') && !relativePath.startsWith(config.projectRoot))) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Check if path is in harness directory or matches expected patterns
|
|
147
|
+
const isInHarness = relativePath.includes('.harness/');
|
|
148
|
+
const matchesTaskPattern = new RegExp(`^${normalizePath(config.tasksDirAbs, config.projectRoot)}/.+\\.json$`).test(relativePath);
|
|
149
|
+
const matchesSnapshotPattern = new RegExp(`^${normalizePath(config.snapshotsDirAbs, config.projectRoot)}/.+\\.json$`).test(relativePath);
|
|
150
|
+
const matchesReportPattern = new RegExp(`^${normalizePath(config.reportsDirAbs, config.projectRoot)}/.+\\.md$`).test(relativePath);
|
|
151
|
+
if (!isInHarness && !matchesTaskPattern && !matchesSnapshotPattern && !matchesReportPattern) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const tasksPattern = new RegExp(`^${normalizePath(config.tasksDirAbs, config.projectRoot)}/.+\\.json$`);
|
|
155
|
+
const snapshotPattern = new RegExp(`^${normalizePath(config.snapshotsDirAbs, config.projectRoot)}/.+\\.json$`);
|
|
156
|
+
const reportPattern = new RegExp(`^${normalizePath(config.reportsDirAbs, config.projectRoot)}/.+\\.md$`);
|
|
157
|
+
if (tasksPattern.test(relativePath)) {
|
|
158
|
+
return { phaseCompleted: 'planning', nextPhase: 'implementation', relativePath };
|
|
159
|
+
}
|
|
160
|
+
if (snapshotPattern.test(relativePath) && /-impl\.json$/.test(relativePath)) {
|
|
161
|
+
return { phaseCompleted: 'implementation', nextPhase: 'review', relativePath };
|
|
162
|
+
}
|
|
163
|
+
if (reportPattern.test(relativePath) && /-review\.md$/.test(relativePath)) {
|
|
164
|
+
return { phaseCompleted: 'review', nextPhase: 'merge', relativePath };
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function getTaskFilePath(config, taskId) {
|
|
169
|
+
return path.resolve(config.tasksDirAbs, `${taskId}.json`);
|
|
170
|
+
}
|
|
171
|
+
function readTaskJson(config, taskId) {
|
|
172
|
+
const taskPath = getTaskFilePath(config, taskId);
|
|
173
|
+
if (!fs.existsSync(taskPath)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(fs.readFileSync(taskPath, 'utf8'));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function getValidationItems(taskJson, phaseName) {
|
|
184
|
+
if (!taskJson) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
if (phaseName === 'implementation') {
|
|
188
|
+
return Array.isArray(taskJson.validation?.commands) ? taskJson.validation.commands : [];
|
|
189
|
+
}
|
|
190
|
+
if (phaseName === 'review') {
|
|
191
|
+
return Array.isArray(taskJson.validation?.manual_checks) ? taskJson.validation.manual_checks : [];
|
|
192
|
+
}
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
function getPlanningSummary(taskJson) {
|
|
196
|
+
return taskJson?.implementation?.notes || '';
|
|
197
|
+
}
|
|
198
|
+
function getPlanningList(taskJson, key) {
|
|
199
|
+
if (!taskJson) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
if (key === 'implementationSteps') {
|
|
203
|
+
const steps = Array.isArray(taskJson.implementation?.steps) ? taskJson.implementation.steps : [];
|
|
204
|
+
return steps.map((item) => (typeof item === 'string' ? item : item.step || ''));
|
|
205
|
+
}
|
|
206
|
+
if (key === 'testStrategy') {
|
|
207
|
+
return Array.isArray(taskJson.validation?.commands) ? taskJson.validation.commands : [];
|
|
208
|
+
}
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
function getSnapshotValidationMap(snapshotPath) {
|
|
212
|
+
if (!snapshotPath || !fs.existsSync(snapshotPath)) {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
|
|
217
|
+
const results = Array.isArray(snapshot.validationResults) ? snapshot.validationResults : [];
|
|
218
|
+
const map = {};
|
|
219
|
+
for (const result of results) {
|
|
220
|
+
if (!result?.name) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const name = normalizeValidationName(result.name);
|
|
224
|
+
map[name] = isPassedStatus(result.status);
|
|
225
|
+
}
|
|
226
|
+
return map;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function hasValidationPassed(validationName, snapshotValidationMap) {
|
|
233
|
+
const candidates = getValidationAliases(validationName).map(normalizeValidationName);
|
|
234
|
+
return candidates.some((item) => snapshotValidationMap[item] === true || isPassedStatus(snapshotValidationMap[item]));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Validate planning phase
|
|
238
|
+
*/
|
|
239
|
+
function validatePlanningPhase(taskJson, validations, config) {
|
|
240
|
+
const missingValidations = [];
|
|
241
|
+
const details = [];
|
|
242
|
+
const taskOutput = taskJson.metadata?.outputs?.task;
|
|
243
|
+
const taskOutputExists = taskOutput && fs.existsSync(path.resolve(config.projectRoot, taskOutput));
|
|
244
|
+
if (!taskOutputExists) {
|
|
245
|
+
missingValidations.push('planning-task-exists');
|
|
246
|
+
}
|
|
247
|
+
if (validations.includes('planning-ready')) {
|
|
248
|
+
const summary = getPlanningSummary(taskJson);
|
|
249
|
+
const implementationSteps = getPlanningList(taskJson, 'implementationSteps');
|
|
250
|
+
const testStrategy = getPlanningList(taskJson, 'testStrategy');
|
|
251
|
+
if (!summary) {
|
|
252
|
+
missingValidations.push('planning.summary');
|
|
253
|
+
}
|
|
254
|
+
if (implementationSteps.length === 0) {
|
|
255
|
+
missingValidations.push('planning.implementationSteps');
|
|
256
|
+
}
|
|
257
|
+
if (testStrategy.length === 0) {
|
|
258
|
+
missingValidations.push('planning.testStrategy');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { missingValidations, details };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Validate implementation phase
|
|
265
|
+
*/
|
|
266
|
+
function validateImplementationPhase(validations, relativePath, config) {
|
|
267
|
+
const missingValidations = [];
|
|
268
|
+
const details = [];
|
|
269
|
+
const snapshotPath = path.resolve(config.projectRoot, relativePath);
|
|
270
|
+
const snapshotValidationMap = getSnapshotValidationMap(snapshotPath);
|
|
271
|
+
for (const validation of validations) {
|
|
272
|
+
if (!hasValidationPassed(validation, snapshotValidationMap)) {
|
|
273
|
+
missingValidations.push(validation);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
details.push(`snapshot:${relativePath}`);
|
|
277
|
+
return { missingValidations, details };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Validate review phase
|
|
281
|
+
*/
|
|
282
|
+
function validateReviewPhase(validations, relativePath, config, stateReviewConclusion) {
|
|
283
|
+
const missingValidations = [];
|
|
284
|
+
const details = [];
|
|
285
|
+
let reviewConclusion = stateReviewConclusion;
|
|
286
|
+
const reportPath = path.resolve(config.projectRoot, relativePath);
|
|
287
|
+
if (!fs.existsSync(reportPath)) {
|
|
288
|
+
missingValidations.push('review-report-exists');
|
|
289
|
+
return { missingValidations, details, reviewConclusion };
|
|
290
|
+
}
|
|
291
|
+
const reportContent = fs.readFileSync(reportPath, 'utf8');
|
|
292
|
+
const parsedReport = parseReviewReport(reportContent);
|
|
293
|
+
if (parsedReport.conclusion) {
|
|
294
|
+
reviewConclusion = parsedReport.conclusion;
|
|
295
|
+
}
|
|
296
|
+
for (const validation of validations) {
|
|
297
|
+
const normalizedValidation = normalizeValidationName(validation);
|
|
298
|
+
if (normalizedValidation === 'review-conclusion') {
|
|
299
|
+
if (reviewConclusion !== 'PASS') {
|
|
300
|
+
missingValidations.push('review-conclusion=PASS');
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (!hasValidationPassed(normalizedValidation, parsedReport.validationMap)) {
|
|
305
|
+
missingValidations.push(validation);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
details.push(`report:${relativePath}`);
|
|
309
|
+
return { missingValidations, details, reviewConclusion };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Assess transition gate
|
|
313
|
+
* @param options - Options
|
|
314
|
+
* @returns Gate assessment result
|
|
315
|
+
*/
|
|
316
|
+
function assessTransitionGate(options) {
|
|
317
|
+
const { taskId, filePath, state, config } = options;
|
|
318
|
+
const transition = deriveTransitionFromFilePath(filePath, config);
|
|
319
|
+
if (!transition) {
|
|
320
|
+
return { allowed: false, skipped: true, reason: 'not-transition-artifact' };
|
|
321
|
+
}
|
|
322
|
+
const taskJson = readTaskJson(config, taskId);
|
|
323
|
+
if (!taskJson) {
|
|
324
|
+
return {
|
|
325
|
+
allowed: false,
|
|
326
|
+
skipped: false,
|
|
327
|
+
phaseCompleted: transition.phaseCompleted,
|
|
328
|
+
nextPhase: transition.nextPhase,
|
|
329
|
+
reason: `task-not-found:${normalizePath(getTaskFilePath(config, taskId), config.projectRoot)}`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const validations = getValidationItems(taskJson, transition.phaseCompleted);
|
|
333
|
+
let missingValidations = [];
|
|
334
|
+
let details = [];
|
|
335
|
+
let reviewConclusion = state?.reviewConclusion || null;
|
|
336
|
+
if (transition.phaseCompleted === 'planning') {
|
|
337
|
+
const planningResult = validatePlanningPhase(taskJson, validations, config);
|
|
338
|
+
missingValidations = planningResult.missingValidations;
|
|
339
|
+
details = planningResult.details;
|
|
340
|
+
}
|
|
341
|
+
if (transition.phaseCompleted === 'implementation') {
|
|
342
|
+
const implResult = validateImplementationPhase(validations, transition.relativePath, config);
|
|
343
|
+
missingValidations = implResult.missingValidations;
|
|
344
|
+
details = implResult.details;
|
|
345
|
+
}
|
|
346
|
+
if (transition.phaseCompleted === 'review') {
|
|
347
|
+
const reviewResult = validateReviewPhase(validations, transition.relativePath, config, reviewConclusion);
|
|
348
|
+
missingValidations = reviewResult.missingValidations;
|
|
349
|
+
details = reviewResult.details;
|
|
350
|
+
reviewConclusion = reviewResult.reviewConclusion;
|
|
351
|
+
}
|
|
352
|
+
if (missingValidations.length > 0) {
|
|
353
|
+
return {
|
|
354
|
+
allowed: false,
|
|
355
|
+
skipped: false,
|
|
356
|
+
phaseCompleted: transition.phaseCompleted,
|
|
357
|
+
nextPhase: transition.nextPhase,
|
|
358
|
+
missingValidations,
|
|
359
|
+
details,
|
|
360
|
+
reviewConclusion,
|
|
361
|
+
reason: 'phase-validation-failed',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
allowed: true,
|
|
366
|
+
skipped: false,
|
|
367
|
+
phaseCompleted: transition.phaseCompleted,
|
|
368
|
+
nextPhase: transition.nextPhase,
|
|
369
|
+
validations,
|
|
370
|
+
details,
|
|
371
|
+
reviewConclusion,
|
|
372
|
+
reason: 'phase-validation-passed',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Main entry point for hook usage
|
|
377
|
+
*/
|
|
378
|
+
function main() {
|
|
379
|
+
const input = JSON.parse(process.argv[2] || '{}');
|
|
380
|
+
const filePath = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
381
|
+
const config = (0, config_1.loadConfig)();
|
|
382
|
+
const state = (0, state_1.loadState)(config);
|
|
383
|
+
if (!state?.currentTask) {
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|
|
386
|
+
const result = assessTransitionGate({ taskId: state.currentTask, filePath, state, config });
|
|
387
|
+
if (result.skipped) {
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
if (!result.allowed) {
|
|
391
|
+
console.error(`[PHASE-GATE] blocked ${result.phaseCompleted} -> ${result.nextPhase}: ${result.reason}`);
|
|
392
|
+
if (Array.isArray(result.missingValidations) && result.missingValidations.length > 0) {
|
|
393
|
+
console.error(`[PHASE-GATE] missing: ${result.missingValidations.join(', ')}`);
|
|
394
|
+
}
|
|
395
|
+
process.exit(2);
|
|
396
|
+
}
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
if (require.main === module) {
|
|
400
|
+
main();
|
|
401
|
+
}
|
|
402
|
+
exports.default = {
|
|
403
|
+
assessTransitionGate,
|
|
404
|
+
deriveTransitionFromFilePath,
|
|
405
|
+
parseReviewConclusion,
|
|
406
|
+
parseReviewReport,
|
|
407
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks/save-checkpoint.ts
|
|
3
|
+
* Save runtime snapshot/checkpoint
|
|
4
|
+
*/
|
|
5
|
+
import { Config } from '../lib/config';
|
|
6
|
+
import { State } from '../lib/state';
|
|
7
|
+
export interface HookInput {
|
|
8
|
+
hook_event_name?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface CheckpointResult {
|
|
12
|
+
saved: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
snapshotId?: string;
|
|
15
|
+
snapshotPath?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CheckpointOptions {
|
|
18
|
+
input: HookInput;
|
|
19
|
+
config: Config;
|
|
20
|
+
state: State;
|
|
21
|
+
}
|
|
22
|
+
export interface Snapshot {
|
|
23
|
+
snapshotId: string;
|
|
24
|
+
snapshotType: string;
|
|
25
|
+
taskId: string;
|
|
26
|
+
phase: string;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
status: string;
|
|
29
|
+
source: string;
|
|
30
|
+
metrics: Record<string, unknown>;
|
|
31
|
+
files: string[];
|
|
32
|
+
validationResults: unknown[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Save runtime snapshot
|
|
36
|
+
* @param options - Options
|
|
37
|
+
* @returns Result
|
|
38
|
+
*/
|
|
39
|
+
export declare function saveCheckpoint(options: CheckpointOptions): CheckpointResult;
|
|
40
|
+
declare const _default: {
|
|
41
|
+
saveCheckpoint: typeof saveCheckpoint;
|
|
42
|
+
};
|
|
43
|
+
export default _default;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* hooks/save-checkpoint.ts
|
|
4
|
+
* Save runtime snapshot/checkpoint
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.saveCheckpoint = saveCheckpoint;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const config_1 = require("../lib/config");
|
|
45
|
+
const state_1 = require("../lib/state");
|
|
46
|
+
/**
|
|
47
|
+
* Validate config has required properties for checkpoint
|
|
48
|
+
*/
|
|
49
|
+
function validateConfigForCheckpoint(config) {
|
|
50
|
+
if (!config.projectRoot) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
reason: 'missing-project-root',
|
|
54
|
+
message: 'Config missing projectRoot',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!config.snapshotsDirAbs) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
reason: 'missing-snapshots-dir',
|
|
61
|
+
message: 'Config missing snapshotsDirAbs',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { valid: true };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Save runtime snapshot
|
|
68
|
+
* @param options - Options
|
|
69
|
+
* @returns Result
|
|
70
|
+
*/
|
|
71
|
+
function saveCheckpoint(options) {
|
|
72
|
+
const { input, config, state } = options;
|
|
73
|
+
const taskId = process.env.HARNESS_TASK_ID || state.currentTask;
|
|
74
|
+
const currentPhase = process.env.HARNESS_PHASE || state.currentPhase || 'unknown';
|
|
75
|
+
if (!taskId) {
|
|
76
|
+
return { saved: false, reason: 'missing-task-id' };
|
|
77
|
+
}
|
|
78
|
+
// Validate config before use
|
|
79
|
+
const configValidation = validateConfigForCheckpoint(config);
|
|
80
|
+
if (!configValidation.valid) {
|
|
81
|
+
console.error(`[SNAPSHOT] ${configValidation.message}`);
|
|
82
|
+
return { saved: false, reason: configValidation.reason || 'config-validation-failed' };
|
|
83
|
+
}
|
|
84
|
+
const projectRoot = config.projectRoot || process.cwd();
|
|
85
|
+
const snapshotDir = config.snapshotsDirAbs || path.resolve(projectRoot, '.harness', 'snapshots');
|
|
86
|
+
fs.mkdirSync(snapshotDir, { recursive: true });
|
|
87
|
+
const timestamp = Date.now();
|
|
88
|
+
const snapshotId = `${taskId}-${currentPhase}-${timestamp}`;
|
|
89
|
+
const snapshotRelativePath = path.posix.join(path.relative(projectRoot, snapshotDir), 'runtime', `${snapshotId}.json`);
|
|
90
|
+
const snapshot = {
|
|
91
|
+
snapshotId,
|
|
92
|
+
snapshotType: 'runtime',
|
|
93
|
+
taskId,
|
|
94
|
+
phase: currentPhase,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
status: 'in_progress',
|
|
97
|
+
source: input.hook_event_name || 'runtime',
|
|
98
|
+
metrics: {},
|
|
99
|
+
files: [],
|
|
100
|
+
validationResults: [],
|
|
101
|
+
};
|
|
102
|
+
// Try to collect changed files from git diff
|
|
103
|
+
try {
|
|
104
|
+
const diffOutput = (0, child_process_1.execSync)('git diff --name-only HEAD', { encoding: 'utf8' });
|
|
105
|
+
snapshot.files = diffOutput.trim().split('\n').filter((f) => f);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Git not available or no changes
|
|
109
|
+
}
|
|
110
|
+
// Save snapshot
|
|
111
|
+
const runtimeSnapshotDir = path.resolve(snapshotDir, 'runtime');
|
|
112
|
+
fs.mkdirSync(runtimeSnapshotDir, { recursive: true });
|
|
113
|
+
const snapshotPath = path.resolve(runtimeSnapshotDir, `${snapshotId}.json`);
|
|
114
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), 'utf8');
|
|
115
|
+
console.log(`[SNAPSHOT] Saved: ${snapshotId}`);
|
|
116
|
+
// Update state with current snapshot reference
|
|
117
|
+
(0, state_1.updateState)({
|
|
118
|
+
currentSnapshot: snapshotRelativePath,
|
|
119
|
+
}, config);
|
|
120
|
+
return {
|
|
121
|
+
saved: true,
|
|
122
|
+
snapshotId,
|
|
123
|
+
snapshotPath: snapshotRelativePath,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Main entry point for hook usage
|
|
128
|
+
*/
|
|
129
|
+
function main() {
|
|
130
|
+
const input = JSON.parse(process.argv[2] || '{}');
|
|
131
|
+
const config = (0, config_1.loadConfig)();
|
|
132
|
+
const state = (0, state_1.loadState)(config);
|
|
133
|
+
const result = saveCheckpoint({ input, config, state });
|
|
134
|
+
if (!result.saved) {
|
|
135
|
+
console.log(`[SNAPSHOT] skipped: ${result.reason}`);
|
|
136
|
+
}
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
if (require.main === module) {
|
|
140
|
+
main();
|
|
141
|
+
}
|
|
142
|
+
exports.default = {
|
|
143
|
+
saveCheckpoint,
|
|
144
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks/start-mcp-servers.ts
|
|
3
|
+
* Hook for starting MCP servers with autoStart enabled
|
|
4
|
+
*
|
|
5
|
+
* This hook:
|
|
6
|
+
* 1. Gets MCP server configurations from context
|
|
7
|
+
* 2. Starts servers with lifecycle.autoStart = true
|
|
8
|
+
* 3. Returns connection information for downstream use
|
|
9
|
+
*/
|
|
10
|
+
import { McpServerConfig, StartResult, ConnectionInfo } from '../lib/mcp-client';
|
|
11
|
+
export interface StartMcpContext {
|
|
12
|
+
config?: {
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
};
|
|
15
|
+
tools?: {
|
|
16
|
+
mcp: McpServerConfig[];
|
|
17
|
+
};
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface StartMcpResult {
|
|
21
|
+
check: 'mcp-started';
|
|
22
|
+
success: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
result?: {
|
|
27
|
+
success: boolean;
|
|
28
|
+
results: StartResult[];
|
|
29
|
+
};
|
|
30
|
+
connectionInfo?: ConnectionInfo[];
|
|
31
|
+
started?: unknown[];
|
|
32
|
+
summary?: {
|
|
33
|
+
totalCount: number;
|
|
34
|
+
startedCount: number;
|
|
35
|
+
skippedCount: number;
|
|
36
|
+
failedCount: number;
|
|
37
|
+
};
|
|
38
|
+
stack?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Start MCP servers hook
|
|
42
|
+
* @param context - Hook execution context
|
|
43
|
+
* @returns Hook execution result
|
|
44
|
+
*/
|
|
45
|
+
export declare function run(context: StartMcpContext): StartMcpResult;
|
|
46
|
+
declare const _default: {
|
|
47
|
+
run: typeof run;
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
export default _default;
|