sdd-mcp-server 3.5.0 → 4.0.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/README.md +97 -671
- package/agents/architect.md +15 -93
- package/agents/implementer.md +16 -141
- package/agents/planner.md +16 -84
- package/agents/reviewer.md +16 -239
- package/agents/security-auditor.md +16 -114
- package/agents/tdd-guide.md +17 -228
- package/dist/adapters/cli/SDDToolAdapter.d.ts +14 -5
- package/dist/adapters/cli/SDDToolAdapter.js +189 -362
- package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
- package/dist/application/services/ContextCompactionService.d.ts +81 -16
- package/dist/application/services/ContextCompactionService.js +370 -187
- package/dist/application/services/ContextCompactionService.js.map +1 -1
- package/dist/application/services/SpecPathResolver.d.ts +24 -0
- package/dist/application/services/SpecPathResolver.js +70 -0
- package/dist/application/services/SpecPathResolver.js.map +1 -0
- package/dist/application/services/WorkflowEngineService.d.ts +100 -46
- package/dist/application/services/WorkflowEngineService.js +468 -288
- package/dist/application/services/WorkflowEngineService.js.map +1 -1
- package/dist/cli/install-skills.d.ts +3 -9
- package/dist/cli/install-skills.js +130 -175
- package/dist/cli/install-skills.js.map +1 -1
- package/dist/cli/install-target.d.ts +45 -14
- package/dist/cli/install-target.js +26 -12
- package/dist/cli/install-target.js.map +1 -1
- package/dist/cli/sdd-mcp-cli.d.ts +1 -1
- package/dist/cli/sdd-mcp-cli.js +7 -6
- package/dist/cli/sdd-mcp-cli.js.map +1 -1
- package/dist/cli/tool-support/claude-code.js +13 -34
- package/dist/cli/tool-support/claude-code.js.map +1 -1
- package/dist/cli/tool-support/codex.d.ts +0 -53
- package/dist/cli/tool-support/codex.js +6 -94
- package/dist/cli/tool-support/codex.js.map +1 -1
- package/dist/cli/tool-support/index.d.ts +3 -2
- package/dist/cli/tool-support/index.js +3 -1
- package/dist/cli/tool-support/index.js.map +1 -1
- package/dist/cli/tool-support/omp.d.ts +5 -0
- package/dist/cli/tool-support/omp.js +43 -0
- package/dist/cli/tool-support/omp.js.map +1 -0
- package/dist/cli/tool-support/root-guidance.d.ts +2 -9
- package/dist/cli/tool-support/root-guidance.js +44 -37
- package/dist/cli/tool-support/root-guidance.js.map +1 -1
- package/dist/cli/tool-support/target-agent-renderer.d.ts +1 -0
- package/dist/cli/tool-support/target-agent-renderer.js +37 -4
- package/dist/cli/tool-support/target-agent-renderer.js.map +1 -1
- package/dist/cli/tool-support/target-installer.d.ts +8 -2
- package/dist/cli/tool-support/target-installer.js +94 -26
- package/dist/cli/tool-support/target-installer.js.map +1 -1
- package/dist/cli/utils/preserving-writer.d.ts +22 -0
- package/dist/cli/utils/preserving-writer.js +233 -11
- package/dist/cli/utils/preserving-writer.js.map +1 -1
- package/dist/domain/ports.d.ts +4 -0
- package/dist/index.d.ts +13 -10
- package/dist/index.js +16 -1199
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/adapters/NodeFileSystemAdapter.d.ts +3 -0
- package/dist/infrastructure/adapters/NodeFileSystemAdapter.js +10 -0
- package/dist/infrastructure/adapters/NodeFileSystemAdapter.js.map +1 -1
- package/dist/infrastructure/mcp/CapabilityNegotiator.js +3 -3
- package/dist/infrastructure/mcp/CapabilityNegotiator.js.map +1 -1
- package/dist/infrastructure/mcp/sddToolDefinitions.d.ts +6 -0
- package/dist/infrastructure/mcp/sddToolDefinitions.js +124 -0
- package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -0
- package/dist/utils/atomicWrite.d.ts +8 -35
- package/dist/utils/atomicWrite.js +12 -60
- package/dist/utils/atomicWrite.js.map +1 -1
- package/mcp-server.js +5 -2883
- package/package.json +5 -2
- package/scripts/context-usage-report.mjs +602 -0
- package/sdd-entry.js +17 -6
- package/skills/sdd-commit/REFERENCE.md +31 -0
- package/skills/sdd-commit/SKILL.md +17 -273
- package/skills/sdd-design/REFERENCE.md +35 -0
- package/skills/sdd-design/SKILL.md +19 -265
- package/skills/sdd-implement/REFERENCE.md +26 -0
- package/skills/sdd-implement/SKILL.md +22 -283
- package/skills/sdd-requirements/REFERENCE.md +31 -0
- package/skills/sdd-requirements/SKILL.md +23 -135
- package/skills/sdd-review/REFERENCE.md +26 -0
- package/skills/sdd-review/SKILL.md +17 -181
- package/skills/sdd-security-check/REFERENCE.md +19 -0
- package/skills/sdd-security-check/SKILL.md +18 -184
- package/skills/sdd-steering/REFERENCE.md +25 -0
- package/skills/sdd-steering/SKILL.md +18 -216
- package/skills/sdd-steering-custom/REFERENCE.md +27 -0
- package/skills/sdd-steering-custom/SKILL.md +19 -203
- package/skills/sdd-tasks/REFERENCE.md +25 -0
- package/skills/sdd-tasks/SKILL.md +19 -248
- package/skills/sdd-test-gen/REFERENCE.md +15 -0
- package/skills/sdd-test-gen/SKILL.md +17 -287
- package/skills/simple-task/REFERENCE.md +22 -0
- package/skills/simple-task/SKILL.md +17 -138
- package/templates/CLAUDE.md +18 -30
- package/rules/git-workflow.md +0 -92
- package/rules/sdd-workflow.md +0 -116
|
@@ -10,351 +10,531 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
|
+
var WorkflowEngineService_1;
|
|
14
|
+
import path from 'node:path';
|
|
13
15
|
import { injectable, inject } from 'inversify';
|
|
14
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
15
16
|
import { TYPES } from '../../infrastructure/di/types.js';
|
|
16
|
-
import { WorkflowPhase } from '../../domain/types.js';
|
|
17
|
-
import { WorkflowStateMachine } from '../../domain/workflow/WorkflowStateMachine.js';
|
|
17
|
+
import { WorkflowPhase, } from '../../domain/types.js';
|
|
18
18
|
import { ProjectService } from './ProjectService.js';
|
|
19
19
|
import { TemplateService } from './TemplateService.js';
|
|
20
20
|
import { ContextCompactionService } from './ContextCompactionService.js';
|
|
21
|
+
import { SpecPathResolver, validateFeatureName } from './SpecPathResolver.js';
|
|
21
22
|
let WorkflowEngineService = class WorkflowEngineService {
|
|
23
|
+
static { WorkflowEngineService_1 = this; }
|
|
22
24
|
projectRepository;
|
|
23
25
|
projectService;
|
|
24
26
|
templateService;
|
|
25
27
|
contextCompactionService;
|
|
26
28
|
logger;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
constructor(projectRepository, projectService, templateService, contextCompactionService, logger,
|
|
29
|
+
fileSystem;
|
|
30
|
+
static featureLocks = new Map();
|
|
31
|
+
constructor(projectRepository, projectService, templateService, contextCompactionService, logger, fileSystem) {
|
|
30
32
|
this.projectRepository = projectRepository;
|
|
31
33
|
this.projectService = projectService;
|
|
32
34
|
this.templateService = templateService;
|
|
33
35
|
this.contextCompactionService = contextCompactionService;
|
|
34
36
|
this.logger = logger;
|
|
35
|
-
this.
|
|
36
|
-
this.stateMachine = new WorkflowStateMachine();
|
|
37
|
+
this.fileSystem = fileSystem;
|
|
37
38
|
}
|
|
38
|
-
async
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
async approve(request) {
|
|
40
|
+
return this.withFeatureLock(request.projectRoot, request.featureName, async () => {
|
|
41
|
+
const { featureRoot, specPath, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
42
|
+
const approvals = this.readApprovals(spec);
|
|
43
|
+
const current = approvals[request.phase];
|
|
44
|
+
if (!current.generated) {
|
|
45
|
+
throw new Error(`Cannot approve ${request.phase}: phase document has not been generated`);
|
|
46
|
+
}
|
|
47
|
+
const phaseIndex = ['requirements', 'design', 'tasks'].indexOf(request.phase);
|
|
48
|
+
for (const prior of ['requirements', 'design', 'tasks'].slice(0, phaseIndex)) {
|
|
49
|
+
if (!approvals[prior].approved)
|
|
50
|
+
throw new Error(`Cannot approve ${request.phase}: ${prior} is not approved`);
|
|
51
|
+
}
|
|
52
|
+
if (!this.fileSystem)
|
|
53
|
+
throw new Error('FileSystemPort is required for disk-addressed workflow operations');
|
|
54
|
+
const resolver = new SpecPathResolver(this.fileSystem);
|
|
55
|
+
for (const selectedPhase of ['requirements', 'design', 'tasks'].slice(0, phaseIndex + 1)) {
|
|
56
|
+
const documentPath = path.join(featureRoot, `${selectedPhase}.md`);
|
|
57
|
+
await resolver.assertContained(featureRoot, documentPath);
|
|
58
|
+
if (!(await this.fileSystem.exists(documentPath))) {
|
|
59
|
+
throw new Error(`Cannot approve ${request.phase}: required document is missing: ${selectedPhase}.md`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const review = this.readReviewState(spec);
|
|
63
|
+
if (request.phase === 'tasks' && review.required && !review.reviewed) {
|
|
64
|
+
throw new Error('Cannot approve tasks: test-case review is required');
|
|
65
|
+
}
|
|
66
|
+
if (!current.approved) {
|
|
67
|
+
approvals[request.phase] = { ...current, approved: true };
|
|
68
|
+
const prospective = {
|
|
69
|
+
...spec,
|
|
70
|
+
updated_at: new Date().toISOString(),
|
|
71
|
+
phase: `${request.phase}-approved`,
|
|
72
|
+
approvals,
|
|
73
|
+
ready_for_implementation: request.phase === 'tasks',
|
|
74
|
+
};
|
|
75
|
+
await this.writeSpecAtomic(specPath, prospective);
|
|
76
|
+
await this.synchronizeRepository(featureRoot, approvals);
|
|
77
|
+
}
|
|
78
|
+
const handoff = await this.publishHandoff(request.projectRoot, request.featureName);
|
|
54
79
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
80
|
+
featureName: request.featureName,
|
|
81
|
+
phase: request.phase,
|
|
82
|
+
approved: true,
|
|
83
|
+
canProgressToNext: request.phase !== 'tasks' || !review.required || review.reviewed,
|
|
84
|
+
handoff,
|
|
57
85
|
};
|
|
58
|
-
}
|
|
59
|
-
return await this.transitionToPhase(projectId, nextPhase, triggeredBy);
|
|
86
|
+
});
|
|
60
87
|
}
|
|
61
|
-
async
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
async reviewTestCases(request) {
|
|
89
|
+
return this.withFeatureLock(request.projectRoot, request.featureName, async () => {
|
|
90
|
+
const { specPath, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
91
|
+
const approvals = this.readApprovals(spec);
|
|
92
|
+
const review = this.readReviewState(spec);
|
|
93
|
+
if (!review.required)
|
|
94
|
+
throw new Error('Test-case review is not configured for this feature');
|
|
95
|
+
if (!approvals.tasks.generated)
|
|
96
|
+
throw new Error('Cannot review test cases before tasks are generated');
|
|
97
|
+
if (!review.reviewed) {
|
|
98
|
+
const checkpointsValue = spec.checkpoints;
|
|
99
|
+
const checkpoints = checkpointsValue && typeof checkpointsValue === 'object'
|
|
100
|
+
? checkpointsValue
|
|
101
|
+
: {};
|
|
102
|
+
const key = 'test_cases' in checkpoints ? 'test_cases' : 'testCases';
|
|
103
|
+
const testValue = checkpoints[key];
|
|
104
|
+
const test = testValue && typeof testValue === 'object' ? testValue : {};
|
|
105
|
+
const prospective = {
|
|
106
|
+
...spec,
|
|
107
|
+
updated_at: new Date().toISOString(),
|
|
108
|
+
checkpoints: {
|
|
109
|
+
...checkpoints,
|
|
110
|
+
[key]: { ...test, required: true, reviewed: true, reviewed_at: new Date().toISOString() },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
await this.writeSpecAtomic(specPath, prospective);
|
|
114
|
+
}
|
|
115
|
+
const handoff = await this.publishHandoff(request.projectRoot, request.featureName);
|
|
116
|
+
return { featureName: request.featureName, reviewed: true, canApproveTasks: true, handoff };
|
|
68
117
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
error: result.error
|
|
118
|
+
}
|
|
119
|
+
async rollback(request) {
|
|
120
|
+
return this.withFeatureLock(request.projectRoot, request.featureName, async () => {
|
|
121
|
+
const { specPath, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
122
|
+
const approvals = this.readApprovals(spec);
|
|
123
|
+
const rolledBackPhase = ([...['requirements', 'design', 'tasks']].reverse())
|
|
124
|
+
.find((phase) => approvals[phase].approved);
|
|
125
|
+
if (!rolledBackPhase)
|
|
126
|
+
throw new Error('Feature has no approved phase to roll back');
|
|
127
|
+
const phaseIndex = ['requirements', 'design', 'tasks'].indexOf(rolledBackPhase);
|
|
128
|
+
for (const phase of ['requirements', 'design', 'tasks'].slice(phaseIndex)) {
|
|
129
|
+
approvals[phase] = { ...approvals[phase], approved: false };
|
|
130
|
+
}
|
|
131
|
+
const prior = phaseIndex > 0 ? ['requirements', 'design', 'tasks'][phaseIndex - 1] : undefined;
|
|
132
|
+
await this.writeSpecAtomic(specPath, {
|
|
133
|
+
...spec,
|
|
134
|
+
updated_at: new Date().toISOString(),
|
|
135
|
+
phase: prior ? `${prior}-approved` : 'init',
|
|
136
|
+
approvals,
|
|
137
|
+
ready_for_implementation: false,
|
|
138
|
+
rollback_triggered_by: request.triggeredBy,
|
|
91
139
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
auditEntry: result.auditEntry
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
// Save updated project
|
|
99
|
-
if (result.updatedProject) {
|
|
100
|
-
await this.projectRepository.save(result.updatedProject);
|
|
101
|
-
// Generate phase-specific deliverables if needed
|
|
102
|
-
await this.generatePhaseDeliverables(result.updatedProject, targetPhase);
|
|
103
|
-
}
|
|
104
|
-
this.logger.info('Phase transition completed successfully', {
|
|
105
|
-
correlationId,
|
|
106
|
-
projectId,
|
|
107
|
-
fromPhase: project.phase,
|
|
108
|
-
targetPhase
|
|
140
|
+
await this.contextCompactionService.invalidateCanonicalHandoff(request);
|
|
141
|
+
const handoff = await this.publishHandoff(request.projectRoot, request.featureName);
|
|
142
|
+
return { featureName: request.featureName, rolledBackPhase, handoff };
|
|
109
143
|
});
|
|
110
|
-
|
|
144
|
+
}
|
|
145
|
+
async initializeFeature(request) {
|
|
146
|
+
return this.withFeatureLock(request.projectRoot, request.featureName, async () => {
|
|
147
|
+
if (!this.fileSystem)
|
|
148
|
+
throw new Error('FileSystemPort is required for disk-addressed workflow operations');
|
|
149
|
+
validateFeatureName(request.featureName);
|
|
150
|
+
const absoluteProject = path.resolve(request.projectRoot);
|
|
151
|
+
const resolver = new SpecPathResolver(this.fileSystem);
|
|
152
|
+
const canonicalProject = this.fileSystem.realpath
|
|
153
|
+
? await this.fileSystem.realpath(absoluteProject)
|
|
154
|
+
: absoluteProject;
|
|
155
|
+
await resolver.assertContained(canonicalProject, path.join(canonicalProject, '.spec'));
|
|
156
|
+
await this.fileSystem.mkdir(path.join(canonicalProject, '.spec', 'specs'));
|
|
157
|
+
const resolved = await resolver.resolve(canonicalProject, request.featureName);
|
|
158
|
+
if (await this.fileSystem.exists(resolved.featureRoot)) {
|
|
159
|
+
throw new Error(`Feature already exists: ${request.featureName}`);
|
|
160
|
+
}
|
|
161
|
+
const project = await this.projectService.createProject(request.featureName, resolved.projectRoot, request.language, { reviewTestCases: request.reviewTestCases });
|
|
162
|
+
try {
|
|
163
|
+
const rendered = await this.templateService.generateSpecJson(project);
|
|
164
|
+
const parsed = JSON.parse(rendered);
|
|
165
|
+
await this.writeSpecAtomic(path.join(resolved.featureRoot, 'spec.json'), parsed);
|
|
166
|
+
return project;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
await this.projectRepository.delete(project.id);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async listFeatureStatuses(request) {
|
|
175
|
+
if (!this.fileSystem)
|
|
176
|
+
throw new Error('FileSystemPort is required for disk-addressed workflow operations');
|
|
177
|
+
const absoluteProject = path.resolve(request.projectRoot);
|
|
178
|
+
const specsRoot = path.join(absoluteProject, '.spec', 'specs');
|
|
179
|
+
if (!(await this.fileSystem.exists(specsRoot)))
|
|
180
|
+
return [];
|
|
181
|
+
const resolver = new SpecPathResolver(this.fileSystem);
|
|
182
|
+
await resolver.assertContained(absoluteProject, specsRoot);
|
|
183
|
+
const names = (await this.fileSystem.readdir(specsRoot)).sort();
|
|
184
|
+
const results = [];
|
|
185
|
+
for (const featureName of names) {
|
|
186
|
+
const candidate = path.join(specsRoot, featureName);
|
|
187
|
+
const stat = await this.fileSystem.stat(candidate);
|
|
188
|
+
if (!stat.isDirectory())
|
|
189
|
+
continue;
|
|
190
|
+
const project = await this.loadProject({ projectRoot: absoluteProject, featureName });
|
|
191
|
+
results.push({ featureName: project.name, phase: project.phase });
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
async getFeatureStatus(request) {
|
|
196
|
+
const { projectRoot, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
197
|
+
const project = await this.hydrateProject(projectRoot, request.featureName, spec);
|
|
198
|
+
const approvals = this.readApprovals(spec);
|
|
199
|
+
const review = this.readReviewState(spec);
|
|
200
|
+
const progression = this.resolveProgression(approvals, review, project.phase);
|
|
111
201
|
return {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
202
|
+
featureName: project.name,
|
|
203
|
+
phase: project.phase,
|
|
204
|
+
currentPhase: project.phase,
|
|
205
|
+
nextPhase: progression.nextPhase,
|
|
206
|
+
canProgress: progression.nextPhase !== undefined && progression.blockers.length === 0,
|
|
207
|
+
blockers: progression.blockers,
|
|
208
|
+
testCases: review,
|
|
117
209
|
};
|
|
118
210
|
}
|
|
119
|
-
async
|
|
120
|
-
const
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
211
|
+
async loadProject(request) {
|
|
212
|
+
const { projectRoot, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
213
|
+
return this.hydrateProject(projectRoot, request.featureName, spec);
|
|
214
|
+
}
|
|
215
|
+
async generatePhase(request) {
|
|
216
|
+
return this.withFeatureLock(request.projectRoot, request.featureName, async () => {
|
|
217
|
+
const fileSystem = this.requireAtomicFileSystem();
|
|
218
|
+
const { projectRoot, featureRoot, specPath, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
219
|
+
const approvals = this.readApprovals(spec);
|
|
220
|
+
this.assertPhaseCanBeGenerated(request.phase, approvals);
|
|
221
|
+
const project = await this.hydrateProject(projectRoot, request.featureName, spec);
|
|
222
|
+
const content = await this.renderPhase(project, request.phase);
|
|
223
|
+
await this.persistPhaseDocument(fileSystem, featureRoot, request.phase, content, approvals[request.phase].generated);
|
|
224
|
+
const prospective = this.buildGeneratedSpec(spec, request);
|
|
225
|
+
await this.writeSpecAtomic(specPath, prospective);
|
|
226
|
+
await this.synchronizeRepository(featureRoot, this.readApprovals(prospective));
|
|
227
|
+
return `${this.phaseTitle(request.phase)} document generated for project "${request.featureName}"`;
|
|
125
228
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
229
|
+
}
|
|
230
|
+
async beginImplementation(request) {
|
|
231
|
+
return this.withFeatureLock(request.projectRoot, request.featureName, async () => {
|
|
232
|
+
const { featureRoot, specPath, spec } = await this.readDiskSpec(request.projectRoot, request.featureName);
|
|
233
|
+
const approvals = this.readApprovals(spec);
|
|
234
|
+
const review = this.readReviewState(spec);
|
|
235
|
+
if (!approvals.tasks.approved)
|
|
236
|
+
throw new Error('Cannot begin implementation: tasks are not approved');
|
|
237
|
+
if (review.required && !review.reviewed) {
|
|
238
|
+
throw new Error('Cannot begin implementation: test-case review is required');
|
|
239
|
+
}
|
|
240
|
+
if (spec.phase === WorkflowPhase.IMPLEMENTATION && spec.ready_for_implementation === true) {
|
|
241
|
+
return { featureName: request.featureName, phase: WorkflowPhase.IMPLEMENTATION, ready: true };
|
|
242
|
+
}
|
|
243
|
+
const prospective = {
|
|
244
|
+
...spec,
|
|
245
|
+
updated_at: new Date().toISOString(),
|
|
246
|
+
phase: WorkflowPhase.IMPLEMENTATION,
|
|
247
|
+
ready_for_implementation: true,
|
|
138
248
|
};
|
|
249
|
+
await this.writeSpecAtomic(specPath, prospective);
|
|
250
|
+
await this.synchronizeRepository(featureRoot, approvals);
|
|
251
|
+
return { featureName: request.featureName, phase: WorkflowPhase.IMPLEMENTATION, ready: true };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
requireAtomicFileSystem() {
|
|
255
|
+
if (!this.fileSystem?.writeFileAtomic) {
|
|
256
|
+
throw new Error('FileSystemPort.writeFileAtomic is required for workflow persistence');
|
|
139
257
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
return {
|
|
150
|
-
success: false,
|
|
151
|
-
message: result.error || 'Phase rollback failed',
|
|
152
|
-
auditEntry: result.auditEntry
|
|
153
|
-
};
|
|
258
|
+
return this.fileSystem;
|
|
259
|
+
}
|
|
260
|
+
assertPhaseCanBeGenerated(phase, approvals) {
|
|
261
|
+
const phases = ['requirements', 'design', 'tasks'];
|
|
262
|
+
for (const prior of phases.slice(0, phases.indexOf(phase))) {
|
|
263
|
+
if (!approvals[prior].approved) {
|
|
264
|
+
throw new Error(`Cannot generate ${phase}: ${prior} is not approved`);
|
|
265
|
+
}
|
|
154
266
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await this.projectRepository.save(result.updatedProject);
|
|
158
|
-
// Reset approvals for rolled back phase
|
|
159
|
-
await this.resetPhaseApprovals(result.updatedProject, project.phase);
|
|
267
|
+
if (approvals[phase].approved) {
|
|
268
|
+
throw new Error(`Cannot generate ${phase}: phase is already approved`);
|
|
160
269
|
}
|
|
161
|
-
this.logger.info('Phase rollback completed successfully', {
|
|
162
|
-
correlationId,
|
|
163
|
-
projectId,
|
|
164
|
-
fromPhase: project.phase,
|
|
165
|
-
targetPhase: previousPhase
|
|
166
|
-
});
|
|
167
|
-
return {
|
|
168
|
-
success: true,
|
|
169
|
-
updatedProject: result.updatedProject,
|
|
170
|
-
message: `Project rolled back to ${previousPhase} phase`,
|
|
171
|
-
auditEntry: result.auditEntry
|
|
172
|
-
};
|
|
173
270
|
}
|
|
174
|
-
async
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
triggeredBy
|
|
182
|
-
});
|
|
183
|
-
const updatedProject = await this.projectService.updateApprovalStatus(projectId, phase, approval);
|
|
184
|
-
// Update spec.json file
|
|
185
|
-
const specContent = await this.templateService.generateSpecJson(updatedProject);
|
|
186
|
-
await this.templateService.writeProjectFile(updatedProject, 'spec.json', specContent);
|
|
187
|
-
let handoffResult;
|
|
188
|
-
if (approval.approved) {
|
|
189
|
-
handoffResult = await this.contextCompactionService.generatePhaseHandoff(updatedProject, phase);
|
|
271
|
+
async persistPhaseDocument(fileSystem, featureRoot, phase, content, alreadyGenerated) {
|
|
272
|
+
const resolver = new SpecPathResolver(fileSystem);
|
|
273
|
+
const documentPath = path.join(featureRoot, `${phase}.md`);
|
|
274
|
+
await resolver.assertContained(featureRoot, documentPath);
|
|
275
|
+
if (!(await fileSystem.exists(documentPath))) {
|
|
276
|
+
await fileSystem.writeFileAtomic(documentPath, content);
|
|
277
|
+
return;
|
|
190
278
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (nextPhase) {
|
|
195
|
-
const validation = this.stateMachine.canTransition(updatedProject, nextPhase);
|
|
196
|
-
canProgressToNext = validation.allowed;
|
|
279
|
+
const existing = await fileSystem.readFile(documentPath);
|
|
280
|
+
if (alreadyGenerated || existing !== content) {
|
|
281
|
+
throw new Error(`Cannot generate ${phase}: phase document already exists`);
|
|
197
282
|
}
|
|
198
|
-
const message = approval.approved
|
|
199
|
-
? `${phase} phase approved and ready for progression`
|
|
200
|
-
: `${phase} phase approval status updated`;
|
|
201
|
-
const messageWithHandoff = handoffResult
|
|
202
|
-
? `${message}. Compact handoff: ${handoffResult.path} (${handoffResult.estimate.reductionPercentage}% estimated context reduction, ~${handoffResult.estimate.sourceTokens} -> ~${handoffResult.estimate.compactTokens} tokens)`
|
|
203
|
-
: message;
|
|
204
|
-
this.logger.info('Approval status updated successfully', {
|
|
205
|
-
correlationId,
|
|
206
|
-
projectId,
|
|
207
|
-
phase,
|
|
208
|
-
canProgressToNext
|
|
209
|
-
});
|
|
210
|
-
return {
|
|
211
|
-
success: true,
|
|
212
|
-
updatedProject,
|
|
213
|
-
message: messageWithHandoff,
|
|
214
|
-
canProgressToNext
|
|
215
|
-
};
|
|
216
283
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
284
|
+
buildGeneratedSpec(spec, request) {
|
|
285
|
+
const approvalRecord = spec.approvals && typeof spec.approvals === 'object'
|
|
286
|
+
? spec.approvals
|
|
287
|
+
: {};
|
|
288
|
+
const prospective = {
|
|
289
|
+
...spec,
|
|
290
|
+
updated_at: new Date().toISOString(),
|
|
291
|
+
phase: `${request.phase}-generated`,
|
|
292
|
+
approvals: {
|
|
293
|
+
...approvalRecord,
|
|
294
|
+
[request.phase]: { generated: true, approved: false },
|
|
295
|
+
},
|
|
296
|
+
ready_for_implementation: false,
|
|
297
|
+
};
|
|
298
|
+
if (request.phase !== 'tasks' || typeof request.reviewTestCases !== 'boolean') {
|
|
299
|
+
return prospective;
|
|
221
300
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
301
|
+
const options = spec.workflow_options && typeof spec.workflow_options === 'object'
|
|
302
|
+
? spec.workflow_options
|
|
303
|
+
: {};
|
|
304
|
+
const checkpoints = spec.checkpoints && typeof spec.checkpoints === 'object'
|
|
305
|
+
? spec.checkpoints
|
|
306
|
+
: {};
|
|
307
|
+
const checkpointKey = 'test_cases' in checkpoints ? 'test_cases' : 'testCases';
|
|
308
|
+
const existingTest = checkpoints[checkpointKey] && typeof checkpoints[checkpointKey] === 'object'
|
|
309
|
+
? checkpoints[checkpointKey]
|
|
310
|
+
: {};
|
|
311
|
+
prospective.workflow_options = {
|
|
312
|
+
...options,
|
|
313
|
+
review_test_cases: request.reviewTestCases,
|
|
228
314
|
};
|
|
315
|
+
prospective.checkpoints = {
|
|
316
|
+
...checkpoints,
|
|
317
|
+
[checkpointKey]: {
|
|
318
|
+
...existingTest,
|
|
319
|
+
required: request.reviewTestCases,
|
|
320
|
+
reviewed: !request.reviewTestCases,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
return prospective;
|
|
229
324
|
}
|
|
230
|
-
async
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
325
|
+
async renderPhase(project, phase) {
|
|
326
|
+
if (phase === 'requirements')
|
|
327
|
+
return this.templateService.generateRequirementsTemplate(project);
|
|
328
|
+
if (phase === 'design')
|
|
329
|
+
return this.templateService.generateDesignTemplate(project);
|
|
330
|
+
return this.templateService.generateTasksTemplate(project);
|
|
331
|
+
}
|
|
332
|
+
phaseTitle(phase) {
|
|
333
|
+
if (phase === 'requirements')
|
|
334
|
+
return 'Requirements';
|
|
335
|
+
if (phase === 'design')
|
|
336
|
+
return 'Design';
|
|
337
|
+
return 'Tasks';
|
|
338
|
+
}
|
|
339
|
+
async hydrateProject(projectRoot, featureName, spec) {
|
|
340
|
+
const persistedName = typeof spec.feature_name === 'string'
|
|
341
|
+
? spec.feature_name
|
|
342
|
+
: typeof spec.name === 'string'
|
|
343
|
+
? spec.name
|
|
344
|
+
: undefined;
|
|
345
|
+
if (persistedName !== undefined && persistedName !== featureName) {
|
|
346
|
+
throw new Error(`Feature metadata name does not match directory: ${featureName}`);
|
|
239
347
|
}
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
348
|
+
const approvals = this.readApprovals(spec);
|
|
349
|
+
const review = this.readReviewState(spec);
|
|
350
|
+
const existing = (await this.projectRepository.list()).find((candidate) => candidate.name === featureName);
|
|
351
|
+
const project = {
|
|
352
|
+
id: existing?.id ?? `disk:${featureName}`,
|
|
353
|
+
name: featureName,
|
|
354
|
+
path: projectRoot,
|
|
355
|
+
phase: this.readWorkflowPhase(spec, approvals),
|
|
356
|
+
metadata: {
|
|
357
|
+
createdAt: this.readDate(spec.created_at ?? spec.createdAt),
|
|
358
|
+
updatedAt: this.readDate(spec.updated_at ?? spec.updatedAt),
|
|
359
|
+
language: typeof spec.language === 'string' ? spec.language : 'en',
|
|
360
|
+
approvals,
|
|
361
|
+
workflowOptions: { reviewTestCases: review.required },
|
|
362
|
+
checkpoints: { testCases: review },
|
|
363
|
+
},
|
|
248
364
|
};
|
|
365
|
+
await this.projectRepository.save(project);
|
|
366
|
+
return project;
|
|
249
367
|
}
|
|
250
|
-
|
|
368
|
+
readDate(value) {
|
|
369
|
+
if (typeof value === 'string' || value instanceof Date) {
|
|
370
|
+
const parsed = new Date(value);
|
|
371
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
return new Date(0);
|
|
375
|
+
}
|
|
376
|
+
readWorkflowPhase(spec, approvals) {
|
|
377
|
+
const phase = typeof spec.phase === 'string' ? spec.phase : '';
|
|
378
|
+
if (phase.startsWith('implementation'))
|
|
379
|
+
return WorkflowPhase.IMPLEMENTATION;
|
|
380
|
+
if (phase.startsWith('tasks') || approvals.tasks.generated)
|
|
381
|
+
return WorkflowPhase.TASKS;
|
|
382
|
+
if (phase.startsWith('design') || approvals.design.generated)
|
|
383
|
+
return WorkflowPhase.DESIGN;
|
|
384
|
+
if (phase.startsWith('requirements') || approvals.requirements.generated)
|
|
385
|
+
return WorkflowPhase.REQUIREMENTS;
|
|
386
|
+
return WorkflowPhase.INIT;
|
|
387
|
+
}
|
|
388
|
+
resolveProgression(approvals, review, currentPhase) {
|
|
389
|
+
if (currentPhase === WorkflowPhase.IMPLEMENTATION)
|
|
390
|
+
return { blockers: [] };
|
|
391
|
+
if (!approvals.requirements.generated)
|
|
392
|
+
return { nextPhase: WorkflowPhase.REQUIREMENTS, blockers: [] };
|
|
393
|
+
if (!approvals.requirements.approved) {
|
|
394
|
+
return { nextPhase: WorkflowPhase.DESIGN, blockers: ['Requirements must be approved'] };
|
|
395
|
+
}
|
|
396
|
+
if (!approvals.design.generated)
|
|
397
|
+
return { nextPhase: WorkflowPhase.DESIGN, blockers: [] };
|
|
398
|
+
if (!approvals.design.approved) {
|
|
399
|
+
return { nextPhase: WorkflowPhase.TASKS, blockers: ['Design must be approved'] };
|
|
400
|
+
}
|
|
401
|
+
if (!approvals.tasks.generated)
|
|
402
|
+
return { nextPhase: WorkflowPhase.TASKS, blockers: [] };
|
|
403
|
+
if (!approvals.tasks.approved) {
|
|
404
|
+
return { nextPhase: WorkflowPhase.IMPLEMENTATION, blockers: ['Tasks must be approved'] };
|
|
405
|
+
}
|
|
406
|
+
if (review.required && !review.reviewed) {
|
|
407
|
+
return { nextPhase: WorkflowPhase.IMPLEMENTATION, blockers: ['Test cases must be reviewed'] };
|
|
408
|
+
}
|
|
409
|
+
return { nextPhase: WorkflowPhase.IMPLEMENTATION, blockers: [] };
|
|
410
|
+
}
|
|
411
|
+
async withFeatureLock(projectRoot, featureName, operation) {
|
|
412
|
+
const key = `${path.resolve(projectRoot)}\0${featureName}`;
|
|
413
|
+
const previous = WorkflowEngineService_1.featureLocks.get(key) ?? Promise.resolve();
|
|
414
|
+
let release = () => undefined;
|
|
415
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
416
|
+
const current = previous.then(() => gate);
|
|
417
|
+
WorkflowEngineService_1.featureLocks.set(key, current);
|
|
418
|
+
await previous;
|
|
251
419
|
try {
|
|
252
|
-
|
|
253
|
-
case WorkflowPhase.REQUIREMENTS:
|
|
254
|
-
if (!project.metadata.approvals.requirements.generated) {
|
|
255
|
-
const content = await this.templateService.generateRequirementsTemplate(project);
|
|
256
|
-
await this.templateService.writeProjectFile(project, 'requirements.md', content);
|
|
257
|
-
await this.projectService.updateApprovalStatus(project.id, 'requirements', { generated: true });
|
|
258
|
-
}
|
|
259
|
-
break;
|
|
260
|
-
case WorkflowPhase.DESIGN:
|
|
261
|
-
if (!project.metadata.approvals.design.generated) {
|
|
262
|
-
const content = await this.templateService.generateDesignTemplate(project);
|
|
263
|
-
await this.templateService.writeProjectFile(project, 'design.md', content);
|
|
264
|
-
await this.projectService.updateApprovalStatus(project.id, 'design', { generated: true });
|
|
265
|
-
}
|
|
266
|
-
break;
|
|
267
|
-
case WorkflowPhase.TASKS:
|
|
268
|
-
if (!project.metadata.approvals.tasks.generated) {
|
|
269
|
-
const content = await this.templateService.generateTasksTemplate(project);
|
|
270
|
-
await this.templateService.writeProjectFile(project, 'tasks.md', content);
|
|
271
|
-
await this.projectService.updateApprovalStatus(project.id, 'tasks', { generated: true });
|
|
272
|
-
}
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
420
|
+
return await operation();
|
|
275
421
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
422
|
+
finally {
|
|
423
|
+
release();
|
|
424
|
+
if (WorkflowEngineService_1.featureLocks.get(key) === current) {
|
|
425
|
+
WorkflowEngineService_1.featureLocks.delete(key);
|
|
426
|
+
}
|
|
281
427
|
}
|
|
282
428
|
}
|
|
283
|
-
async
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
break;
|
|
429
|
+
async readDiskSpec(projectRoot, featureName) {
|
|
430
|
+
if (!this.fileSystem)
|
|
431
|
+
throw new Error('FileSystemPort is required for disk-addressed workflow operations');
|
|
432
|
+
const resolver = new SpecPathResolver(this.fileSystem);
|
|
433
|
+
const resolved = await resolver.resolve(projectRoot, featureName);
|
|
434
|
+
const specPath = path.join(resolved.featureRoot, 'spec.json');
|
|
435
|
+
await resolver.assertContained(resolved.featureRoot, specPath);
|
|
436
|
+
if (!(await this.fileSystem.exists(specPath)))
|
|
437
|
+
throw new Error(`Feature metadata not found: ${featureName}`);
|
|
438
|
+
let parsed;
|
|
439
|
+
try {
|
|
440
|
+
parsed = JSON.parse(await this.fileSystem.readFile(specPath));
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
throw new Error(`Feature metadata is malformed: ${featureName}`);
|
|
299
444
|
}
|
|
300
|
-
|
|
301
|
-
|
|
445
|
+
if (!parsed || typeof parsed !== 'object')
|
|
446
|
+
throw new Error(`Feature metadata is invalid: ${featureName}`);
|
|
447
|
+
const record = parsed;
|
|
448
|
+
const persistedName = typeof record.feature_name === 'string'
|
|
449
|
+
? record.feature_name
|
|
450
|
+
: typeof record.name === 'string'
|
|
451
|
+
? record.name
|
|
452
|
+
: undefined;
|
|
453
|
+
if (persistedName !== undefined && persistedName !== featureName) {
|
|
454
|
+
throw new Error(`Feature metadata name does not match directory: ${featureName}`);
|
|
302
455
|
}
|
|
456
|
+
return { projectRoot: resolved.projectRoot, featureRoot: resolved.featureRoot, specPath, spec: record };
|
|
303
457
|
}
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
steps.push('Run sdd-design to generate technical design');
|
|
317
|
-
}
|
|
318
|
-
break;
|
|
319
|
-
case WorkflowPhase.DESIGN:
|
|
320
|
-
if (!project.metadata.approvals.design.approved) {
|
|
321
|
-
steps.push('Review and approve design document');
|
|
322
|
-
steps.push('Run sdd-tasks after design approval');
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
steps.push('Run sdd-tasks to generate implementation tasks');
|
|
326
|
-
}
|
|
327
|
-
break;
|
|
328
|
-
case WorkflowPhase.TASKS:
|
|
329
|
-
if (!project.metadata.approvals.tasks.approved) {
|
|
330
|
-
steps.push('Review and approve task breakdown');
|
|
331
|
-
if (project.metadata.checkpoints?.testCases.required && !project.metadata.checkpoints.testCases.reviewed) {
|
|
332
|
-
steps.push('Review TDD test cases and run sdd-review-test-cases');
|
|
333
|
-
}
|
|
334
|
-
steps.push('Begin implementation after tasks approval');
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
steps.push('Begin implementation following the task breakdown');
|
|
338
|
-
steps.push('Use sdd-quality-check for code reviews');
|
|
339
|
-
}
|
|
340
|
-
break;
|
|
341
|
-
case WorkflowPhase.IMPLEMENTATION:
|
|
342
|
-
steps.push('Continue implementation following approved tasks');
|
|
343
|
-
steps.push('Use sdd-quality-check for ongoing code quality');
|
|
344
|
-
steps.push('Update task completion status as you progress');
|
|
345
|
-
break;
|
|
458
|
+
readApprovals(spec) {
|
|
459
|
+
const value = spec.approvals;
|
|
460
|
+
if (!value || typeof value !== 'object')
|
|
461
|
+
throw new Error('Feature metadata has no approval state');
|
|
462
|
+
const record = value;
|
|
463
|
+
const result = {};
|
|
464
|
+
for (const phase of ['requirements', 'design', 'tasks']) {
|
|
465
|
+
const phaseValue = record[phase];
|
|
466
|
+
const state = phaseValue && typeof phaseValue === 'object' ? phaseValue : {};
|
|
467
|
+
result[phase] = { generated: state.generated === true, approved: state.approved === true };
|
|
346
468
|
}
|
|
347
|
-
return
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
readReviewState(spec) {
|
|
472
|
+
const checkpointsValue = spec.checkpoints;
|
|
473
|
+
const checkpoints = checkpointsValue && typeof checkpointsValue === 'object'
|
|
474
|
+
? checkpointsValue
|
|
475
|
+
: {};
|
|
476
|
+
const testValue = checkpoints.test_cases ?? checkpoints.testCases;
|
|
477
|
+
const test = testValue && typeof testValue === 'object' ? testValue : {};
|
|
478
|
+
const optionsValue = spec.workflow_options ?? spec.workflowOptions;
|
|
479
|
+
const options = optionsValue && typeof optionsValue === 'object' ? optionsValue : {};
|
|
480
|
+
return {
|
|
481
|
+
required: test.required === true || options.review_test_cases === true || options.reviewTestCases === true,
|
|
482
|
+
reviewed: test.reviewed === true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async writeSpecAtomic(specPath, spec) {
|
|
486
|
+
if (!this.fileSystem?.writeFileAtomic)
|
|
487
|
+
throw new Error('FileSystemPort.writeFileAtomic is required for workflow persistence');
|
|
488
|
+
await this.fileSystem.writeFileAtomic(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
489
|
+
}
|
|
490
|
+
async publishHandoff(projectRoot, featureName) {
|
|
491
|
+
try {
|
|
492
|
+
const result = await this.contextCompactionService.loadContext({ projectRoot, featureName, mode: 'compact' });
|
|
493
|
+
return {
|
|
494
|
+
status: 'published',
|
|
495
|
+
path: path.join(projectRoot, '.spec', 'specs', featureName, 'context', 'handoff.md'),
|
|
496
|
+
fingerprint: result.fingerprint,
|
|
497
|
+
payloadEstimatedTokens: result.payloadEstimatedTokens,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
try {
|
|
502
|
+
await this.contextCompactionService.invalidateCanonicalHandoff({ projectRoot, featureName });
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// A missing cache is already invalid; publication remains repairable.
|
|
506
|
+
}
|
|
507
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
508
|
+
this.logger.warn('Workflow state committed but handoff publication failed', { featureName, message });
|
|
509
|
+
return {
|
|
510
|
+
status: 'pending-regeneration',
|
|
511
|
+
warning: { code: 'HandoffPublicationFailed', message },
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async synchronizeRepository(featureRoot, approvals) {
|
|
516
|
+
const projects = await this.projectRepository.list();
|
|
517
|
+
const existing = projects.find((project) => project.name === path.basename(featureRoot));
|
|
518
|
+
if (!existing)
|
|
519
|
+
return;
|
|
520
|
+
await this.projectRepository.save({
|
|
521
|
+
...existing,
|
|
522
|
+
metadata: {
|
|
523
|
+
...existing.metadata,
|
|
524
|
+
updatedAt: new Date(),
|
|
525
|
+
approvals,
|
|
526
|
+
},
|
|
527
|
+
});
|
|
348
528
|
}
|
|
349
529
|
};
|
|
350
|
-
WorkflowEngineService = __decorate([
|
|
530
|
+
WorkflowEngineService = WorkflowEngineService_1 = __decorate([
|
|
351
531
|
injectable(),
|
|
352
532
|
__param(0, inject(TYPES.ProjectRepository)),
|
|
353
533
|
__param(1, inject(TYPES.ProjectService)),
|
|
354
534
|
__param(2, inject(TYPES.TemplateService)),
|
|
355
535
|
__param(3, inject(TYPES.ContextCompactionService)),
|
|
356
536
|
__param(4, inject(TYPES.LoggerPort)),
|
|
357
|
-
__param(5, inject(TYPES.
|
|
537
|
+
__param(5, inject(TYPES.FileSystemPort)),
|
|
358
538
|
__metadata("design:paramtypes", [Object, ProjectService,
|
|
359
539
|
TemplateService,
|
|
360
540
|
ContextCompactionService, Object, Object])
|