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.
Files changed (95) hide show
  1. package/README.md +97 -671
  2. package/agents/architect.md +15 -93
  3. package/agents/implementer.md +16 -141
  4. package/agents/planner.md +16 -84
  5. package/agents/reviewer.md +16 -239
  6. package/agents/security-auditor.md +16 -114
  7. package/agents/tdd-guide.md +17 -228
  8. package/dist/adapters/cli/SDDToolAdapter.d.ts +14 -5
  9. package/dist/adapters/cli/SDDToolAdapter.js +189 -362
  10. package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
  11. package/dist/application/services/ContextCompactionService.d.ts +81 -16
  12. package/dist/application/services/ContextCompactionService.js +370 -187
  13. package/dist/application/services/ContextCompactionService.js.map +1 -1
  14. package/dist/application/services/SpecPathResolver.d.ts +24 -0
  15. package/dist/application/services/SpecPathResolver.js +70 -0
  16. package/dist/application/services/SpecPathResolver.js.map +1 -0
  17. package/dist/application/services/WorkflowEngineService.d.ts +100 -46
  18. package/dist/application/services/WorkflowEngineService.js +468 -288
  19. package/dist/application/services/WorkflowEngineService.js.map +1 -1
  20. package/dist/cli/install-skills.d.ts +3 -9
  21. package/dist/cli/install-skills.js +130 -175
  22. package/dist/cli/install-skills.js.map +1 -1
  23. package/dist/cli/install-target.d.ts +45 -14
  24. package/dist/cli/install-target.js +26 -12
  25. package/dist/cli/install-target.js.map +1 -1
  26. package/dist/cli/sdd-mcp-cli.d.ts +1 -1
  27. package/dist/cli/sdd-mcp-cli.js +7 -6
  28. package/dist/cli/sdd-mcp-cli.js.map +1 -1
  29. package/dist/cli/tool-support/claude-code.js +13 -34
  30. package/dist/cli/tool-support/claude-code.js.map +1 -1
  31. package/dist/cli/tool-support/codex.d.ts +0 -53
  32. package/dist/cli/tool-support/codex.js +6 -94
  33. package/dist/cli/tool-support/codex.js.map +1 -1
  34. package/dist/cli/tool-support/index.d.ts +3 -2
  35. package/dist/cli/tool-support/index.js +3 -1
  36. package/dist/cli/tool-support/index.js.map +1 -1
  37. package/dist/cli/tool-support/omp.d.ts +5 -0
  38. package/dist/cli/tool-support/omp.js +43 -0
  39. package/dist/cli/tool-support/omp.js.map +1 -0
  40. package/dist/cli/tool-support/root-guidance.d.ts +2 -9
  41. package/dist/cli/tool-support/root-guidance.js +44 -37
  42. package/dist/cli/tool-support/root-guidance.js.map +1 -1
  43. package/dist/cli/tool-support/target-agent-renderer.d.ts +1 -0
  44. package/dist/cli/tool-support/target-agent-renderer.js +37 -4
  45. package/dist/cli/tool-support/target-agent-renderer.js.map +1 -1
  46. package/dist/cli/tool-support/target-installer.d.ts +8 -2
  47. package/dist/cli/tool-support/target-installer.js +94 -26
  48. package/dist/cli/tool-support/target-installer.js.map +1 -1
  49. package/dist/cli/utils/preserving-writer.d.ts +22 -0
  50. package/dist/cli/utils/preserving-writer.js +233 -11
  51. package/dist/cli/utils/preserving-writer.js.map +1 -1
  52. package/dist/domain/ports.d.ts +4 -0
  53. package/dist/index.d.ts +13 -10
  54. package/dist/index.js +16 -1199
  55. package/dist/index.js.map +1 -1
  56. package/dist/infrastructure/adapters/NodeFileSystemAdapter.d.ts +3 -0
  57. package/dist/infrastructure/adapters/NodeFileSystemAdapter.js +10 -0
  58. package/dist/infrastructure/adapters/NodeFileSystemAdapter.js.map +1 -1
  59. package/dist/infrastructure/mcp/CapabilityNegotiator.js +3 -3
  60. package/dist/infrastructure/mcp/CapabilityNegotiator.js.map +1 -1
  61. package/dist/infrastructure/mcp/sddToolDefinitions.d.ts +6 -0
  62. package/dist/infrastructure/mcp/sddToolDefinitions.js +124 -0
  63. package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -0
  64. package/dist/utils/atomicWrite.d.ts +8 -35
  65. package/dist/utils/atomicWrite.js +12 -60
  66. package/dist/utils/atomicWrite.js.map +1 -1
  67. package/mcp-server.js +5 -2883
  68. package/package.json +5 -2
  69. package/scripts/context-usage-report.mjs +602 -0
  70. package/sdd-entry.js +17 -6
  71. package/skills/sdd-commit/REFERENCE.md +31 -0
  72. package/skills/sdd-commit/SKILL.md +17 -273
  73. package/skills/sdd-design/REFERENCE.md +35 -0
  74. package/skills/sdd-design/SKILL.md +19 -265
  75. package/skills/sdd-implement/REFERENCE.md +26 -0
  76. package/skills/sdd-implement/SKILL.md +22 -283
  77. package/skills/sdd-requirements/REFERENCE.md +31 -0
  78. package/skills/sdd-requirements/SKILL.md +23 -135
  79. package/skills/sdd-review/REFERENCE.md +26 -0
  80. package/skills/sdd-review/SKILL.md +17 -181
  81. package/skills/sdd-security-check/REFERENCE.md +19 -0
  82. package/skills/sdd-security-check/SKILL.md +18 -184
  83. package/skills/sdd-steering/REFERENCE.md +25 -0
  84. package/skills/sdd-steering/SKILL.md +18 -216
  85. package/skills/sdd-steering-custom/REFERENCE.md +27 -0
  86. package/skills/sdd-steering-custom/SKILL.md +19 -203
  87. package/skills/sdd-tasks/REFERENCE.md +25 -0
  88. package/skills/sdd-tasks/SKILL.md +19 -248
  89. package/skills/sdd-test-gen/REFERENCE.md +15 -0
  90. package/skills/sdd-test-gen/SKILL.md +17 -287
  91. package/skills/simple-task/REFERENCE.md +22 -0
  92. package/skills/simple-task/SKILL.md +17 -138
  93. package/templates/CLAUDE.md +18 -30
  94. package/rules/git-workflow.md +0 -92
  95. 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
- validation;
28
- stateMachine;
29
- constructor(projectRepository, projectService, templateService, contextCompactionService, logger, validation) {
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.validation = validation;
36
- this.stateMachine = new WorkflowStateMachine();
37
+ this.fileSystem = fileSystem;
37
38
  }
38
- async progressToNextPhase(projectId, triggeredBy) {
39
- const correlationId = uuidv4();
40
- this.logger.info('Progressing project to next phase', {
41
- correlationId,
42
- projectId,
43
- triggeredBy
44
- });
45
- const project = await this.projectRepository.findById(projectId);
46
- if (!project) {
47
- return {
48
- success: false,
49
- message: `Project not found: ${projectId}`
50
- };
51
- }
52
- const nextPhase = this.stateMachine.getNextPhase(project.phase);
53
- if (!nextPhase) {
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
- success: false,
56
- message: `Project is already at final phase: ${project.phase}`
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 transitionToPhase(projectId, targetPhase, triggeredBy) {
62
- const correlationId = uuidv4();
63
- this.logger.info('Transitioning project to phase', {
64
- correlationId,
65
- projectId,
66
- targetPhase,
67
- triggeredBy
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
- const project = await this.projectRepository.findById(projectId);
70
- if (!project) {
71
- return {
72
- success: false,
73
- message: `Project not found: ${projectId}`
74
- };
75
- }
76
- if (project.phase === targetPhase) {
77
- return {
78
- success: true,
79
- updatedProject: project,
80
- message: `Project is already in ${targetPhase} phase`
81
- };
82
- }
83
- const result = await this.stateMachine.executeTransition(project, targetPhase, triggeredBy);
84
- if (!result.success) {
85
- this.logger.warn('Phase transition failed', {
86
- correlationId,
87
- projectId,
88
- fromPhase: project.phase,
89
- targetPhase,
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
- return {
93
- success: false,
94
- message: result.error || 'Phase transition failed',
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
- const nextSteps = this.getNextSteps(result.updatedProject || project, targetPhase);
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
- success: true,
113
- updatedProject: result.updatedProject,
114
- message: `Project successfully transitioned to ${targetPhase} phase`,
115
- auditEntry: result.auditEntry,
116
- nextSteps
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 rollbackToPreviousPhase(projectId, triggeredBy) {
120
- const correlationId = uuidv4();
121
- this.logger.info('Rolling back project to previous phase', {
122
- correlationId,
123
- projectId,
124
- triggeredBy
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
- const project = await this.projectRepository.findById(projectId);
127
- if (!project) {
128
- return {
129
- success: false,
130
- message: `Project not found: ${projectId}`
131
- };
132
- }
133
- const previousPhase = this.stateMachine.getPreviousPhase(project.phase);
134
- if (!previousPhase) {
135
- return {
136
- success: false,
137
- message: `Project is already at initial phase: ${project.phase}`
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
- const result = await this.stateMachine.executeTransition(project, previousPhase, triggeredBy);
141
- if (!result.success) {
142
- this.logger.warn('Phase rollback failed', {
143
- correlationId,
144
- projectId,
145
- fromPhase: project.phase,
146
- targetPhase: previousPhase,
147
- error: result.error
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
- // Save updated project
156
- if (result.updatedProject) {
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 updateApprovalStatus(projectId, phase, approval, triggeredBy) {
175
- const correlationId = uuidv4();
176
- this.logger.info('Updating approval status', {
177
- correlationId,
178
- projectId,
179
- phase,
180
- approval,
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
- // Check if project can progress to next phase
192
- const nextPhase = this.stateMachine.getNextPhase(updatedProject.phase);
193
- let canProgressToNext = false;
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
- async getWorkflowStatus(projectId) {
218
- const project = await this.projectRepository.findById(projectId);
219
- if (!project) {
220
- return null;
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
- return {
223
- project,
224
- progress: this.stateMachine.getPhaseProgress(project),
225
- auditTrail: this.stateMachine.getAuditTrail(projectId),
226
- integrity: this.stateMachine.validateWorkflowIntegrity(project),
227
- validTransitions: this.stateMachine.getValidTransitions(project.phase)
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 validateWorkflowState(projectId) {
231
- const status = await this.getWorkflowStatus(projectId);
232
- if (!status) {
233
- return {
234
- isValid: false,
235
- violations: ['Project not found'],
236
- recommendations: [],
237
- canProgress: false
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 integrity = status.integrity;
241
- const progress = status.progress;
242
- return {
243
- isValid: integrity.isValid,
244
- violations: integrity.violations,
245
- recommendations: integrity.recommendations,
246
- canProgress: progress.canProgress,
247
- nextPhase: progress.nextPhase
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
- async generatePhaseDeliverables(project, phase) {
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
- switch (phase) {
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
- catch (error) {
277
- this.logger.error('Failed to generate phase deliverables', error, {
278
- projectId: project.id,
279
- phase
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 resetPhaseApprovals(project, rolledBackFromPhase) {
284
- // Reset approvals for the phase we're rolling back from
285
- const resetApprovals = {};
286
- switch (rolledBackFromPhase) {
287
- case WorkflowPhase.REQUIREMENTS:
288
- resetApprovals.requirements = { approved: false };
289
- break;
290
- case WorkflowPhase.DESIGN:
291
- resetApprovals.design = { approved: false };
292
- break;
293
- case WorkflowPhase.TASKS:
294
- resetApprovals.tasks = { approved: false };
295
- break;
296
- case WorkflowPhase.IMPLEMENTATION:
297
- // No approvals to reset for implementation phase
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
- for (const [phase, approval] of Object.entries(resetApprovals)) {
301
- await this.projectService.updateApprovalStatus(project.id, phase, approval);
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
- getNextSteps(project, currentPhase) {
305
- const steps = [];
306
- switch (currentPhase) {
307
- case WorkflowPhase.INIT:
308
- steps.push('Run sdd-requirements to generate requirements document');
309
- break;
310
- case WorkflowPhase.REQUIREMENTS:
311
- if (!project.metadata.approvals.requirements.approved) {
312
- steps.push('Review and approve requirements document');
313
- steps.push('Run sdd-design after requirements approval');
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 steps;
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.ValidationPort)),
537
+ __param(5, inject(TYPES.FileSystemPort)),
358
538
  __metadata("design:paramtypes", [Object, ProjectService,
359
539
  TemplateService,
360
540
  ContextCompactionService, Object, Object])