sdd-mcp-server 5.3.0 → 5.5.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 (117) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +85 -13
  3. package/agents/security-auditor.md +2 -2
  4. package/dist/__tests__/helpers/message-clarity.d.ts +15 -0
  5. package/dist/__tests__/helpers/message-clarity.js +26 -0
  6. package/dist/__tests__/helpers/message-clarity.js.map +1 -0
  7. package/dist/__tests__/helpers/message-extractor.d.ts +22 -0
  8. package/dist/__tests__/helpers/message-extractor.js +135 -0
  9. package/dist/__tests__/helpers/message-extractor.js.map +1 -0
  10. package/dist/__tests__/unit/jev/helpers.d.ts +84 -0
  11. package/dist/__tests__/unit/jev/helpers.js +188 -0
  12. package/dist/__tests__/unit/jev/helpers.js.map +1 -0
  13. package/dist/adapters/cli/SDDToolAdapter.d.ts +5 -1
  14. package/dist/adapters/cli/SDDToolAdapter.js +22 -3
  15. package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
  16. package/dist/application/services/JevAdvisoryService.d.ts +53 -0
  17. package/dist/application/services/JevAdvisoryService.js +310 -0
  18. package/dist/application/services/JevAdvisoryService.js.map +1 -0
  19. package/dist/application/services/JevBaseRef.d.ts +21 -0
  20. package/dist/application/services/JevBaseRef.js +46 -0
  21. package/dist/application/services/JevBaseRef.js.map +1 -0
  22. package/dist/application/services/JevChangeManifest.d.ts +33 -0
  23. package/dist/application/services/JevChangeManifest.js +208 -0
  24. package/dist/application/services/JevChangeManifest.js.map +1 -0
  25. package/dist/application/services/JevCheckTypes.d.ts +74 -0
  26. package/dist/application/services/JevCheckTypes.js +2 -0
  27. package/dist/application/services/JevCheckTypes.js.map +1 -0
  28. package/dist/application/services/JevCodeExcerpts.d.ts +23 -0
  29. package/dist/application/services/JevCodeExcerpts.js +102 -0
  30. package/dist/application/services/JevCodeExcerpts.js.map +1 -0
  31. package/dist/application/services/JevCommitLookup.d.ts +17 -0
  32. package/dist/application/services/JevCommitLookup.js +72 -0
  33. package/dist/application/services/JevCommitLookup.js.map +1 -0
  34. package/dist/application/services/JevExchange.d.ts +48 -0
  35. package/dist/application/services/JevExchange.js +119 -0
  36. package/dist/application/services/JevExchange.js.map +1 -0
  37. package/dist/application/services/JevQuestionCatalog.d.ts +28 -0
  38. package/dist/application/services/JevQuestionCatalog.js +194 -0
  39. package/dist/application/services/JevQuestionCatalog.js.map +1 -0
  40. package/dist/application/services/JevRedaction.d.ts +12 -0
  41. package/dist/application/services/JevRedaction.js +56 -0
  42. package/dist/application/services/JevRedaction.js.map +1 -0
  43. package/dist/application/services/JevReport.d.ts +41 -0
  44. package/dist/application/services/JevReport.js +101 -0
  45. package/dist/application/services/JevReport.js.map +1 -0
  46. package/dist/application/services/JevResponseValidator.d.ts +34 -0
  47. package/dist/application/services/JevResponseValidator.js +98 -0
  48. package/dist/application/services/JevResponseValidator.js.map +1 -0
  49. package/dist/application/services/JevResults.d.ts +3 -0
  50. package/dist/application/services/JevResults.js +7 -0
  51. package/dist/application/services/JevResults.js.map +1 -0
  52. package/dist/application/services/JevReviewCheck.d.ts +20 -0
  53. package/dist/application/services/JevReviewCheck.js +120 -0
  54. package/dist/application/services/JevReviewCheck.js.map +1 -0
  55. package/dist/application/services/JevSettings.d.ts +18 -0
  56. package/dist/application/services/JevSettings.js +72 -0
  57. package/dist/application/services/JevSettings.js.map +1 -0
  58. package/dist/application/services/JevSidecarStore.d.ts +68 -0
  59. package/dist/application/services/JevSidecarStore.js +201 -0
  60. package/dist/application/services/JevSidecarStore.js.map +1 -0
  61. package/dist/application/services/JevSpecState.d.ts +38 -0
  62. package/dist/application/services/JevSpecState.js +109 -0
  63. package/dist/application/services/JevSpecState.js.map +1 -0
  64. package/dist/application/services/JevTestcaseState.d.ts +22 -0
  65. package/dist/application/services/JevTestcaseState.js +85 -0
  66. package/dist/application/services/JevTestcaseState.js.map +1 -0
  67. package/dist/application/services/RedEvidenceClassifier.d.ts +10 -0
  68. package/dist/application/services/RedEvidenceClassifier.js +32 -0
  69. package/dist/application/services/RedEvidenceClassifier.js.map +1 -0
  70. package/dist/application/services/WorkflowEngineService.d.ts +28 -0
  71. package/dist/application/services/WorkflowEngineService.js +111 -63
  72. package/dist/application/services/WorkflowEngineService.js.map +1 -1
  73. package/dist/application/services/WorkflowValidationService.d.ts +16 -0
  74. package/dist/application/services/WorkflowValidationService.js +39 -34
  75. package/dist/application/services/WorkflowValidationService.js.map +1 -1
  76. package/dist/cli/install-skills.js +0 -0
  77. package/dist/cli/install-target.d.ts +8 -6
  78. package/dist/cli/install-target.js +11 -3
  79. package/dist/cli/install-target.js.map +1 -1
  80. package/dist/cli/tool-support/root-guidance.js +1 -0
  81. package/dist/cli/tool-support/root-guidance.js.map +1 -1
  82. package/dist/cli/tool-support/target-installer.d.ts +1 -0
  83. package/dist/cli/tool-support/target-installer.js +9 -5
  84. package/dist/cli/tool-support/target-installer.js.map +1 -1
  85. package/dist/domain/ports.d.ts +34 -0
  86. package/dist/domain/ports.js +11 -1
  87. package/dist/domain/ports.js.map +1 -1
  88. package/dist/infrastructure/adapters/FetchJevHttpAdapter.d.ts +4 -0
  89. package/dist/infrastructure/adapters/FetchJevHttpAdapter.js +73 -0
  90. package/dist/infrastructure/adapters/FetchJevHttpAdapter.js.map +1 -0
  91. package/dist/infrastructure/adapters/NodeGitAdapter.d.ts +5 -0
  92. package/dist/infrastructure/adapters/NodeGitAdapter.js +31 -0
  93. package/dist/infrastructure/adapters/NodeGitAdapter.js.map +1 -0
  94. package/dist/infrastructure/di/container.js +10 -0
  95. package/dist/infrastructure/di/container.js.map +1 -1
  96. package/dist/infrastructure/di/types.d.ts +4 -0
  97. package/dist/infrastructure/di/types.js +4 -0
  98. package/dist/infrastructure/di/types.js.map +1 -1
  99. package/dist/infrastructure/mcp/sddToolDefinitions.d.ts +1 -1
  100. package/dist/infrastructure/mcp/sddToolDefinitions.js +72 -1
  101. package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -1
  102. package/dist/shared/version.d.ts +1 -1
  103. package/dist/shared/version.js +1 -1
  104. package/package.json +1 -1
  105. package/skills/output-clarity-ladder/SKILL.md +43 -0
  106. package/skills/output-clarity-ladder/references/ja.md +30 -0
  107. package/skills/output-clarity-ladder/references/zh-TW.md +30 -0
  108. package/skills/sdd-commit/SKILL.md +4 -3
  109. package/skills/sdd-design/SKILL.md +1 -1
  110. package/skills/sdd-implement/SKILL.md +4 -4
  111. package/skills/sdd-requirements/SKILL.md +6 -6
  112. package/skills/sdd-review/SKILL.md +3 -2
  113. package/skills/sdd-security-check/SKILL.md +3 -2
  114. package/skills/sdd-tasks/REFERENCE.md +4 -0
  115. package/skills/sdd-tasks/SKILL.md +8 -6
  116. package/templates/CLAUDE.md +1 -1
  117. package/templates/codex-AGENTS.md +1 -1
@@ -19,7 +19,7 @@ import { TYPES } from '../../infrastructure/di/types.js';
19
19
  import { WorkflowPhase, } from '../../domain/types.js';
20
20
  import { ContextCompactionService, } from './ContextCompactionService.js';
21
21
  import { InvalidFeatureNameError, SpecPathResolver, validateFeatureName } from './SpecPathResolver.js';
22
- import { WorkflowValidationService } from './WorkflowValidationService.js';
22
+ import { WorkflowValidationService, taskAcceptanceCriteria } from './WorkflowValidationService.js';
23
23
  import { GovernanceError } from './WorkflowErrors.js';
24
24
  import { FilesystemLockCompromisedError, withFilesystemLock } from '../../utils/withFilesystemLock.js';
25
25
  const PHASES = ['requirements', 'design', 'tasks'];
@@ -76,7 +76,10 @@ let WorkflowEngineService = class WorkflowEngineService {
76
76
  const initializationLock = path.join(resolved.specsRoot, `.${request.featureName}.init.lock`);
77
77
  await resolver.assertContained(resolved.specsRoot, initializationLock);
78
78
  return withFilesystemLock(initializationLock, async (lease) => {
79
- if (await this.fileSystem.exists(resolved.featureRoot)) {
79
+ // Older versions left empty feature directories behind; reuse those, never populated ones.
80
+ const occupied = await this.fileSystem.exists(resolved.featureRoot)
81
+ && (await this.fileSystem.readdir(resolved.featureRoot)).length > 0;
82
+ if (occupied) {
80
83
  throw new GovernanceError('StateInvariantViolation', `Feature already exists: ${request.featureName}`);
81
84
  }
82
85
  await this.fileSystem.mkdir(resolved.featureRoot);
@@ -125,14 +128,14 @@ let WorkflowEngineService = class WorkflowEngineService {
125
128
  const record = spec.approvals[request.phase];
126
129
  this.assertPriorApproved(request.phase, spec.approvals);
127
130
  if (record.approved)
128
- throw new GovernanceError('PhaseNotApproved', `${request.phase} is already approved`);
131
+ throw new GovernanceError('PhaseNotApproved', `${request.phase} is already approved. Reload status and continue with the next phase.`);
129
132
  if (record.revision !== request.expectedRevision)
130
- throw new GovernanceError('RevisionConflict', `Expected revision ${request.expectedRevision}, found ${record.revision}`);
133
+ throw new GovernanceError('RevisionConflict', `Expected revision ${request.expectedRevision}, found ${record.revision}. Reload status and submit again with the current revision.`);
131
134
  const artifactPath = path.join(featureRoot, `${request.phase}.md`);
132
135
  const priorArtifact = await this.readOptional(artifactPath);
133
136
  const observedHash = priorArtifact === null ? null : sha256(priorArtifact);
134
137
  if (observedHash !== request.expectedArtifactSha256) {
135
- throw new GovernanceError('ArtifactDrift', `${request.phase}.md changed since it was observed`, { observedArtifactSha256: observedHash });
138
+ throw new GovernanceError('ArtifactDrift', `${request.phase}.md changed since it was observed. Reread the artifact and submit again with its current content.`, { observedArtifactSha256: observedHash });
136
139
  }
137
140
  if (request.phase === 'tasks') {
138
141
  if (typeof request.reviewTestCases !== 'boolean')
@@ -172,27 +175,27 @@ let WorkflowEngineService = class WorkflowEngineService {
172
175
  await this.assertNoApprovedArtifactDrift(featureRoot, spec);
173
176
  const record = spec.approvals[request.phase];
174
177
  if (!record.generated)
175
- throw new GovernanceError('PhaseNotApproved', `${request.phase} has not been submitted`);
178
+ throw new GovernanceError('PhaseNotApproved', `${request.phase} has not been submitted. Submit the phase artifact before approval.`);
176
179
  if (record.revision !== request.expectedRevision)
177
- throw new GovernanceError('RevisionConflict', 'The reviewed revision is stale');
180
+ throw new GovernanceError('RevisionConflict', 'The reviewed revision is stale. Reload status and review the current revision.');
178
181
  if (record.artifactSha256 !== request.expectedArtifactSha256)
179
- throw new GovernanceError('ArtifactDrift', 'The reviewed artifact hash is stale');
182
+ throw new GovernanceError('ArtifactDrift', 'The reviewed artifact hash is stale. Reload status and review the current artifact.');
180
183
  const artifact = await this.requireArtifact(featureRoot, request.phase);
181
184
  if (sha256(artifact) !== record.artifactSha256)
182
- throw new GovernanceError('ArtifactDrift', `${request.phase}.md differs from the submitted artifact`);
185
+ throw new GovernanceError('ArtifactDrift', `${request.phase}.md differs from the submitted artifact. Inspect the changed artifact, then revise it in its phase Skill.`);
183
186
  this.assertPriorApproved(request.phase, spec.approvals);
184
187
  if (record.validation.status !== 'passed' && record.validation.status !== 'legacy-accepted')
185
- throw new GovernanceError('PhaseValidationFailed', `${request.phase} did not pass validation`, { blockers: record.validation.blockers });
188
+ throw new GovernanceError('PhaseValidationFailed', `${request.phase} did not pass validation. Fix the listed blockers and submit the phase again.`, { blockers: record.validation.blockers });
186
189
  if (record.validation.status === 'passed') {
187
190
  const upstream = await this.readUpstream(featureRoot, request.phase);
188
191
  const validation = this.validate(request.phase, artifact, upstream.requirements, upstream.design);
189
192
  if (validation.status !== 'passed')
190
- throw new GovernanceError('PhaseValidationFailed', `${request.phase} no longer validates`, { blockers: validation.blockers });
193
+ throw new GovernanceError('PhaseValidationFailed', `${request.phase} no longer validates. Fix the listed blockers and submit the phase again.`, { blockers: validation.blockers });
191
194
  }
192
195
  const checkpoint = spec.checkpoints.test_cases;
193
196
  if (request.phase === 'tasks' && spec.workflow_options.review_test_cases === true &&
194
197
  (!checkpoint.reviewed || checkpoint.reviewed_revision !== record.revision || checkpoint.reviewed_artifact_sha256 !== record.artifactSha256)) {
195
- throw new GovernanceError('PhaseNotApproved', 'The current tasks revision requires explicit test-case review');
198
+ throw new GovernanceError('PhaseNotApproved', 'The current tasks revision requires explicit test-case review. Record the test-case review decision before approving tasks.');
196
199
  }
197
200
  if (!record.approved) {
198
201
  spec.approvals[request.phase] = { ...record, approved: true };
@@ -211,18 +214,18 @@ let WorkflowEngineService = class WorkflowEngineService {
211
214
  if (spec.workflow_options.review_test_cases !== true)
212
215
  throw new GovernanceError('StateInvariantViolation', 'Test-case review is not required');
213
216
  if (!tasks.generated)
214
- throw new GovernanceError('PhaseNotApproved', 'Tasks are not ready for test-case review');
217
+ throw new GovernanceError('PhaseNotApproved', 'Tasks are not generated yet. Submit the tasks phase before test-case review.');
215
218
  if (tasks.revision !== request.expectedTasksRevision)
216
- throw new GovernanceError('RevisionConflict', 'The tasks review revision is stale');
219
+ throw new GovernanceError('RevisionConflict', 'The tasks review revision is stale. Reload status and review the current tasks revision.');
217
220
  if (tasks.artifactSha256 !== request.expectedArtifactSha256 || sha256(await this.requireArtifact(featureRoot, 'tasks')) !== request.expectedArtifactSha256)
218
- throw new GovernanceError('ArtifactDrift', 'The tasks artifact changed before review');
221
+ throw new GovernanceError('ArtifactDrift', 'The tasks artifact changed before review. Reload status and review the current tasks artifact.');
219
222
  const checkpoint = spec.checkpoints.test_cases;
220
223
  const alreadyReviewed = checkpoint.reviewed
221
224
  && checkpoint.reviewed_revision === tasks.revision
222
225
  && checkpoint.reviewed_artifact_sha256 === tasks.artifactSha256;
223
226
  if (!alreadyReviewed) {
224
227
  if (tasks.validation.status !== 'passed' || !spec.approvals.design.approved || tasks.approved)
225
- throw new GovernanceError('PhaseNotApproved', 'Tasks are not ready for test-case review');
228
+ throw new GovernanceError('PhaseNotApproved', 'Tasks are not ready for test-case review. Submit passing tasks after design approval and before tasks approval.');
226
229
  spec.checkpoints.test_cases = { required: true, reviewed: true, reviewed_at: new Date().toISOString(), reviewed_revision: tasks.revision, reviewed_artifact_sha256: tasks.artifactSha256 };
227
230
  spec.updated_at = new Date().toISOString();
228
231
  await commitSpec(spec);
@@ -236,13 +239,13 @@ let WorkflowEngineService = class WorkflowEngineService {
236
239
  await this.assertNoApprovedArtifactDrift(featureRoot, spec);
237
240
  const tasksRecord = spec.approvals.tasks;
238
241
  if (!tasksRecord.approved)
239
- throw new GovernanceError('PhaseNotApproved', 'Tasks are not approved');
242
+ throw new GovernanceError('PhaseNotApproved', 'Tasks are not approved. Approve the tasks phase before starting implementation.');
240
243
  if (sha256(await this.requireArtifact(featureRoot, 'tasks')) !== tasksRecord.artifactSha256)
241
- throw new GovernanceError('ArtifactDrift', 'Approved tasks artifact has drifted');
244
+ throw new GovernanceError('ArtifactDrift', 'Approved tasks artifact has drifted. Inspect the changed artifact, then revise it in the tasks phase Skill.');
242
245
  if (spec.workflow_options.review_test_cases === true) {
243
246
  const cp = spec.checkpoints.test_cases;
244
247
  if (!cp.reviewed || cp.reviewed_revision !== tasksRecord.revision || cp.reviewed_artifact_sha256 !== tasksRecord.artifactSha256)
245
- throw new GovernanceError('PhaseNotApproved', 'Tasks review checkpoint is stale');
248
+ throw new GovernanceError('PhaseNotApproved', 'Tasks review checkpoint is stale. Record test-case review for the current tasks revision.');
246
249
  }
247
250
  if (!spec.implementation) {
248
251
  const content = await this.requireArtifact(featureRoot, 'tasks');
@@ -251,12 +254,12 @@ let WorkflowEngineService = class WorkflowEngineService {
251
254
  return this.parseLegacyTasks(content);
252
255
  const current = this.validator.parseTasks(content);
253
256
  if (current.status !== 'passed') {
254
- throw new GovernanceError('PhaseValidationFailed', 'Approved tasks no longer satisfy the executable task contract', { blockers: current.blockers });
257
+ throw new GovernanceError('PhaseValidationFailed', 'Approved tasks no longer satisfy the executable task contract. Fix the listed blockers in the tasks phase Skill.', { blockers: current.blockers });
255
258
  }
256
259
  return Object.entries(current.tasks).map(([id, task]) => ({ id, ...task }));
257
260
  })();
258
261
  if (parsed.length === 0)
259
- throw new GovernanceError('LegacyTaskConflict', 'Tasks artifact contains no executable leaf tasks');
262
+ throw new GovernanceError('LegacyTaskConflict', 'Tasks artifact contains no executable leaf tasks. Add at least one executable task in the tasks phase Skill.');
260
263
  const taskStates = {};
261
264
  for (const task of parsed) {
262
265
  taskStates[task.id] = {
@@ -282,15 +285,15 @@ let WorkflowEngineService = class WorkflowEngineService {
282
285
  await this.assertNoApprovedArtifactDrift(featureRoot, spec);
283
286
  const implementation = spec.implementation;
284
287
  if (!implementation)
285
- throw new GovernanceError('TaskTransitionInvalid', 'Implementation has not started');
288
+ throw new GovernanceError('TaskTransitionInvalid', 'Implementation has not started. Start implementation before changing task state.');
286
289
  if (implementation.revision !== request.expectedRevision)
287
- throw new GovernanceError('RevisionConflict', 'Implementation revision is stale');
290
+ throw new GovernanceError('RevisionConflict', 'Implementation revision is stale. Reload status and submit again with the current revision.');
288
291
  const task = implementation.tasks[request.taskNumber];
289
292
  if (!task)
290
- throw new GovernanceError('TaskTransitionInvalid', `Unknown task ${request.taskNumber}`);
293
+ throw new GovernanceError('TaskTransitionInvalid', `Unknown task ${request.taskNumber}. Use a task number from the approved tasks artifact.`);
291
294
  const incomplete = task.dependencies.filter((dependency) => implementation.tasks[dependency]?.status !== 'completed');
292
295
  if (request.action === 'start' && incomplete.length > 0)
293
- throw new GovernanceError('TaskDependencyIncomplete', 'Task dependencies are incomplete', { dependencies: incomplete });
296
+ throw new GovernanceError('TaskDependencyIncomplete', 'Task dependencies are incomplete. Complete the listed dependency tasks first.', { dependencies: incomplete });
294
297
  const prior = task.status;
295
298
  if (request.action === 'start') {
296
299
  if (prior === 'pending')
@@ -321,7 +324,7 @@ let WorkflowEngineService = class WorkflowEngineService {
321
324
  this.invalidTransition(request.action, prior);
322
325
  const artifacts = this.normalizeArtifacts(request.affectedArtifacts ?? []);
323
326
  if (task.planned_artifacts.length > 0 && artifacts.length === 0)
324
- throw new GovernanceError('TaskTransitionInvalid', 'At least one observed artifact is required');
327
+ throw new GovernanceError('TaskTransitionInvalid', 'At least one observed artifact is required. Name the changed artifacts when completing the task.');
325
328
  task.status = 'completed';
326
329
  task.evidence.verification = this.evidence(request.evidence);
327
330
  task.observed_artifacts = artifacts;
@@ -375,6 +378,47 @@ let WorkflowEngineService = class WorkflowEngineService {
375
378
  }
376
379
  return statuses;
377
380
  }
381
+ /**
382
+ * Read-only view for advisory checks. Never commits state, publishes a handoff, or creates
383
+ * files for an unknown feature (the lock helper would create the feature directory).
384
+ */
385
+ async readAdvisorySnapshot(request) {
386
+ validateFeatureName(request.featureName);
387
+ if (!this.fileSystem)
388
+ throw new GovernanceError('StateInvariantViolation', 'FileSystemPort is required');
389
+ const specPath = path.join(path.resolve(request.projectRoot), '.spec', 'specs', request.featureName, 'spec.json');
390
+ if (!(await this.fileSystem.exists(specPath))) {
391
+ throw new GovernanceError('StateInvariantViolation', `Feature metadata not found: ${request.featureName}`);
392
+ }
393
+ return this.withLockedFeature(request.projectRoot, request.featureName, false, async ({ featureRoot, spec }) => {
394
+ const artifact = (name) => this.readOptional(path.join(featureRoot, `${name}.md`));
395
+ const record = spec.approvals[request.phase];
396
+ const content = await artifact(request.phase);
397
+ const requirementsContent = request.phase === 'requirements' ? null : await artifact('requirements');
398
+ const designContent = request.phase === 'tasks' ? await artifact('design') : null;
399
+ const tasksContent = spec.implementation ? (request.phase === 'tasks' ? content : await artifact('tasks')) : null;
400
+ const criteria = taskAcceptanceCriteria(tasksContent ?? '');
401
+ return {
402
+ featureName: request.featureName,
403
+ description: spec.description,
404
+ phase: request.phase,
405
+ revision: record.revision,
406
+ recordedSha256: record.artifactSha256,
407
+ observedSha256: content === null ? undefined : sha256(content),
408
+ validationStatus: record.validation.status,
409
+ content: content ?? '',
410
+ requirementsContent: requirementsContent ?? undefined,
411
+ designContent: designContent ?? undefined,
412
+ implementation: spec.implementation && {
413
+ revision: spec.implementation.revision,
414
+ tasks: Object.fromEntries(Object.entries(spec.implementation.tasks).map(([id, task]) => [
415
+ id,
416
+ { status: task.status, tddRequired: task.tdd_required, acceptanceCriteria: criteria[id] ?? '' },
417
+ ])),
418
+ },
419
+ };
420
+ });
421
+ }
378
422
  async loadProject(request) {
379
423
  return this.withLockedFeature(request.projectRoot, request.featureName, false, async ({ projectRoot, spec }) => this.hydrateProject(projectRoot, request.featureName, spec));
380
424
  }
@@ -390,6 +434,10 @@ let WorkflowEngineService = class WorkflowEngineService {
390
434
  throw new GovernanceError('StateInvariantViolation', 'FileSystemPort is required');
391
435
  const resolver = new SpecPathResolver(this.fileSystem);
392
436
  const resolved = await resolver.resolve(projectRoot, featureName);
437
+ // The lock helper creates its parent directory; never let it materialize a missing feature.
438
+ if (!(await this.fileSystem.exists(resolved.featureRoot))) {
439
+ throw new GovernanceError('StateInvariantViolation', `Feature metadata not found: ${featureName}`);
440
+ }
393
441
  const lockPath = path.join(resolved.featureRoot, '.workflow.lock');
394
442
  await resolver.assertContained(resolved.featureRoot, lockPath);
395
443
  return withFilesystemLock(lockPath, async (lease) => {
@@ -428,12 +476,12 @@ let WorkflowEngineService = class WorkflowEngineService {
428
476
  && (!Number.isInteger(raw.schema_version)
429
477
  || raw.schema_version < 1
430
478
  || raw.schema_version > 4)) {
431
- throw new GovernanceError('LegacyStateConflict', `Unsupported workflow schema: ${String(raw.schema_version)}`);
479
+ throw new GovernanceError('LegacyStateConflict', `Unsupported workflow schema: ${String(raw.schema_version)}. Restore a supported workflow state file.`);
432
480
  }
433
481
  const accepted = new Set(['init', 'requirements-generated', 'requirements-approved', 'design-generated', 'design-approved', 'tasks-generated', 'tasks-approved', 'implementation-ready', 'implementation', 'completed', 'implementation-completed']);
434
482
  const legacyPhase = typeof raw.phase === 'string' ? raw.phase : 'init';
435
483
  if (!accepted.has(legacyPhase))
436
- throw new GovernanceError('LegacyStateConflict', `Unknown legacy phase: ${legacyPhase}`);
484
+ throw new GovernanceError('LegacyStateConflict', `Unknown legacy phase: ${legacyPhase}. Correct the phase in the workflow state file.`);
437
485
  const legacyApprovals = raw.approvals && typeof raw.approvals === 'object' ? raw.approvals : {};
438
486
  const approvals = {};
439
487
  for (const phase of PHASES) {
@@ -441,10 +489,10 @@ let WorkflowEngineService = class WorkflowEngineService {
441
489
  const generated = value.generated === true;
442
490
  const approved = value.approved === true;
443
491
  if (approved && !generated)
444
- throw new GovernanceError('LegacyStateConflict', `${phase} is approved but not generated`);
492
+ throw new GovernanceError('LegacyStateConflict', `${phase} is approved but not generated. Correct the approval records in the workflow state file.`);
445
493
  const artifact = this.readOptionalSync(path.join(featureRoot, `${phase}.md`));
446
494
  if (generated && artifact === null)
447
- throw new GovernanceError('LegacyStateConflict', `Generated ${phase} artifact is missing`);
495
+ throw new GovernanceError('LegacyStateConflict', `Generated ${phase} artifact is missing. Restore the artifact file or correct the workflow state file.`);
448
496
  approvals[phase] = {
449
497
  generated,
450
498
  approved,
@@ -482,14 +530,14 @@ let WorkflowEngineService = class WorkflowEngineService {
482
530
  const completedRank = legacyPhase === 'completed' || legacyPhase === 'implementation-completed';
483
531
  const implementationRank = ['implementation-ready', 'implementation', 'completed', 'implementation-completed'].includes(legacyPhase);
484
532
  if (implementationRank && (!approvals.tasks.approved || (choice === true && !reviewed)))
485
- throw new GovernanceError('LegacyStateConflict', 'Legacy implementation rank does not satisfy tasks governance');
533
+ throw new GovernanceError('LegacyStateConflict', 'Legacy implementation rank does not satisfy tasks governance. Approve and review the tasks phase first.');
486
534
  const description = typeof raw.description === 'string' ? raw.description : '';
487
535
  const language = typeof raw.language === 'string' ? raw.language : 'en';
488
536
  if (description.length > 20_000) {
489
- throw new GovernanceError('LegacyStateConflict', 'Legacy description exceeds the schema-v5 limit');
537
+ throw new GovernanceError('LegacyStateConflict', 'Legacy description exceeds the schema-v5 limit. Shorten the description in the workflow state file.');
490
538
  }
491
539
  if (!['en', 'ja', 'zh-TW'].includes(language)) {
492
- throw new GovernanceError('LegacyStateConflict', `Unsupported legacy language: ${language}`);
540
+ throw new GovernanceError('LegacyStateConflict', `Unsupported legacy language: ${language}. Use a supported language in the workflow state file.`);
493
541
  }
494
542
  const updatedAt = typeof raw.updated_at === 'string'
495
543
  ? raw.updated_at
@@ -511,10 +559,10 @@ let WorkflowEngineService = class WorkflowEngineService {
511
559
  if (implementationRank) {
512
560
  const tasks = this.parseLegacyTasks(this.readOptionalSync(path.join(featureRoot, 'tasks.md')) ?? '');
513
561
  if (tasks.length === 0)
514
- throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain no leaves');
562
+ throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain no leaves. Add at least one executable task to the tasks artifact.');
515
563
  spec.implementation = { revision: 1, tasks_revision: approvals.tasks.revision, legacy_imported: true, tasks: Object.fromEntries(tasks.map((task) => [task.id, { title: task.title, tdd_required: task.tddRequired, dependencies: [], status: task.completed ? 'completed' : 'pending', evidence: {}, planned_artifacts: [], observed_artifacts: [] }])) };
516
564
  if (completedRank && !this.allComplete(spec.implementation))
517
- throw new GovernanceError('LegacyStateConflict', 'Completed legacy phase has unchecked tasks');
565
+ throw new GovernanceError('LegacyStateConflict', 'Completed legacy phase has unchecked tasks. Complete the unchecked tasks or correct the legacy phase.');
518
566
  }
519
567
  return spec;
520
568
  }
@@ -592,7 +640,7 @@ let WorkflowEngineService = class WorkflowEngineService {
592
640
  const content = await this.readOptional(path.join(featureRoot, `${phase}.md`));
593
641
  const observedArtifactSha256 = content === null ? undefined : sha256(content);
594
642
  if (observedArtifactSha256 !== record.artifactSha256) {
595
- throw new GovernanceError('ArtifactDrift', `Approved ${phase}.md differs from revision ${record.revision}`, { phase, observedArtifactSha256 });
643
+ throw new GovernanceError('ArtifactDrift', `Approved ${phase}.md differs from revision ${record.revision}. Inspect the changed artifact, then revise it in its phase Skill.`, { phase, observedArtifactSha256 });
596
644
  }
597
645
  }
598
646
  }
@@ -607,7 +655,7 @@ let WorkflowEngineService = class WorkflowEngineService {
607
655
  nextArtifact,
608
656
  ];
609
657
  if (endpoints.some((value) => Buffer.byteLength(value, 'utf8') > JOURNAL_LIMIT)) {
610
- throw new GovernanceError('RecoveryConflict', 'Submission endpoint exceeds the journal size limit');
658
+ throw new GovernanceError('RecoveryConflict', 'Submission endpoint exceeds the journal size limit. Shorten the artifact and submit again.');
611
659
  }
612
660
  const journal = {
613
661
  schema_version: 1,
@@ -631,7 +679,7 @@ let WorkflowEngineService = class WorkflowEngineService {
631
679
  await this.replaceCas(journalPath, journalBytes, null, assertHeld);
632
680
  }
633
681
  catch (rollbackError) {
634
- throw new GovernanceError('RecoveryConflict', 'Submission failed and CAS rollback could not safely restore prior bytes', { cause: errorSummary(error), rollback: errorSummary(rollbackError) });
682
+ throw new GovernanceError('RecoveryConflict', 'Submission failed and CAS rollback could not safely restore prior bytes. Inspect the spec files before retrying.', { cause: errorSummary(error), rollback: errorSummary(rollbackError) });
635
683
  }
636
684
  throw error;
637
685
  }
@@ -643,7 +691,7 @@ let WorkflowEngineService = class WorkflowEngineService {
643
691
  if (await this.fileSystem.exists(journalPath)) {
644
692
  const journalStats = await stat(journalPath);
645
693
  if (!journalStats.isFile() || journalStats.size > JOURNAL_FILE_LIMIT) {
646
- throw new GovernanceError('RecoveryConflict', 'Submission journal exceeds its raw size limit');
694
+ throw new GovernanceError('RecoveryConflict', 'Submission journal exceeds its raw size limit. Inspect the journal and spec files, then restore a known state.');
647
695
  }
648
696
  }
649
697
  const raw = await this.readOptional(journalPath);
@@ -654,10 +702,10 @@ let WorkflowEngineService = class WorkflowEngineService {
654
702
  journal = JSON.parse(raw);
655
703
  }
656
704
  catch {
657
- throw new GovernanceError('RecoveryConflict', 'Submission journal is malformed');
705
+ throw new GovernanceError('RecoveryConflict', 'Submission journal is malformed. Inspect the journal and spec files, then restore a known state.');
658
706
  }
659
707
  if (journal.schema_version !== 1) {
660
- throw new GovernanceError('RecoveryConflict', 'Submission journal schema is unsupported');
708
+ throw new GovernanceError('RecoveryConflict', 'Submission journal schema is unsupported. Inspect the journal and spec files, then restore a known state.');
661
709
  }
662
710
  const priorSpec = decode(journal.spec?.prior);
663
711
  const nextSpec = decode(journal.spec?.next);
@@ -670,10 +718,10 @@ let WorkflowEngineService = class WorkflowEngineService {
670
718
  const currentArtifact = await this.readOptional(artifactPath);
671
719
  const convergeNext = currentSpec === nextSpec;
672
720
  if (currentSpec !== priorSpec && !convergeNext) {
673
- throw new GovernanceError('RecoveryConflict', 'Current spec bytes match neither journal endpoint');
721
+ throw new GovernanceError('RecoveryConflict', 'Current spec bytes match neither journal endpoint. Inspect the spec file and restore a known state.');
674
722
  }
675
723
  if (currentArtifact !== priorArtifact && currentArtifact !== nextArtifact) {
676
- throw new GovernanceError('RecoveryConflict', 'Current artifact bytes match neither journal endpoint');
724
+ throw new GovernanceError('RecoveryConflict', 'Current artifact bytes match neither journal endpoint. Inspect the artifact and restore a known state.');
677
725
  }
678
726
  await this.assertJournalValidation(featureRoot, journalPhase, nextSpec, nextArtifact);
679
727
  await this.replaceCas(artifactPath, currentArtifact, convergeNext ? nextArtifact : priorArtifact, assertHeld);
@@ -688,13 +736,13 @@ let WorkflowEngineService = class WorkflowEngineService {
688
736
  await assertHeld();
689
737
  return;
690
738
  }
691
- throw new GovernanceError('RecoveryConflict', `Concurrent byte change at ${path.basename(filePath)}`);
739
+ throw new GovernanceError('RecoveryConflict', `Concurrent byte change at ${path.basename(filePath)}. Retry after the other writer finishes.`);
692
740
  }
693
741
  if (desired === null) {
694
742
  if (current !== null)
695
743
  await unlink(filePath);
696
744
  if (await this.readOptional(filePath) !== null) {
697
- throw new GovernanceError('RecoveryConflict', `Delete verification failed for ${path.basename(filePath)}`);
745
+ throw new GovernanceError('RecoveryConflict', `Delete verification failed for ${path.basename(filePath)}. Inspect the file and retry.`);
698
746
  }
699
747
  }
700
748
  else {
@@ -716,7 +764,7 @@ let WorkflowEngineService = class WorkflowEngineService {
716
764
  if (heading) {
717
765
  const id = heading[1];
718
766
  if (headings.has(id)) {
719
- throw new GovernanceError('LegacyTaskConflict', `Duplicate legacy task ${id}`);
767
+ throw new GovernanceError('LegacyTaskConflict', `Duplicate legacy task ${id}. Rename one task so each identifier is unique.`);
720
768
  }
721
769
  const body = [];
722
770
  for (let cursor = index + 1; cursor < lines.length && !headingPattern.test(lines[cursor]); cursor += 1) {
@@ -745,12 +793,12 @@ let WorkflowEngineService = class WorkflowEngineService {
745
793
  }
746
794
  const ids = new Set([...headings.keys(), ...checklist.keys()]);
747
795
  if (ids.size > 5_000) {
748
- throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain too many task identifiers');
796
+ throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain too many task identifiers. Split the work into a smaller tasks artifact.');
749
797
  }
750
798
  for (const id of ids) {
751
799
  const title = headings.get(id)?.title ?? checklist.get(id)?.title ?? id;
752
800
  if (id.length > 50 || title.trim().length === 0 || title.length > 500) {
753
- throw new GovernanceError('LegacyTaskConflict', `Legacy task ${id.slice(0, 50)} exceeds schema-v5 field limits`);
801
+ throw new GovernanceError('LegacyTaskConflict', `Legacy task ${id.slice(0, 50)} exceeds schema-v5 field limits. Shorten its fields in the tasks artifact.`);
754
802
  }
755
803
  }
756
804
  const parents = new Set();
@@ -763,7 +811,7 @@ let WorkflowEngineService = class WorkflowEngineService {
763
811
  }
764
812
  const leaves = [...ids].filter((id) => !parents.has(id));
765
813
  if (leaves.length > 1_000) {
766
- throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain more than 1000 executable leaves');
814
+ throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain more than 1000 executable leaves. Merge tasks to stay within 1000 leaves.');
767
815
  }
768
816
  return leaves.map((id) => {
769
817
  const heading = headings.get(id);
@@ -813,7 +861,7 @@ let WorkflowEngineService = class WorkflowEngineService {
813
861
  assertPriorApproved(phase, approvals) {
814
862
  for (const prior of PHASES.slice(0, PHASES.indexOf(phase)))
815
863
  if (!approvals[prior].approved)
816
- throw new GovernanceError('PhaseNotApproved', `${prior} is not approved`);
864
+ throw new GovernanceError('PhaseNotApproved', `${prior} is not approved. Approve the ${prior} phase first.`);
817
865
  }
818
866
  assertLegacyOrdering(approvals) {
819
867
  for (const [index, phase] of PHASES.entries()) {
@@ -821,7 +869,7 @@ let WorkflowEngineService = class WorkflowEngineService {
821
869
  continue;
822
870
  for (const prior of PHASES.slice(0, index)) {
823
871
  if (!approvals[prior].approved) {
824
- throw new GovernanceError('LegacyStateConflict', `${phase} exists before ${prior} approval`);
872
+ throw new GovernanceError('LegacyStateConflict', `${phase} exists before ${prior} approval. Approve ${prior} first, or move the ${phase} artifact out of the feature folder.`);
825
873
  }
826
874
  }
827
875
  }
@@ -842,15 +890,15 @@ let WorkflowEngineService = class WorkflowEngineService {
842
890
  ? 'requirements-generated'
843
891
  : 'init';
844
892
  if (!implementationRank && legacyPhase !== expected) {
845
- throw new GovernanceError('LegacyStateConflict', `Legacy phase ${legacyPhase} disagrees with approval records (${expected})`);
893
+ throw new GovernanceError('LegacyStateConflict', `Legacy phase ${legacyPhase} disagrees with approval records (${expected}). Correct the phase in the workflow state file.`);
846
894
  }
847
895
  if (typeof raw.ready_for_implementation === 'boolean' && raw.ready_for_implementation !== implementationRank) {
848
- throw new GovernanceError('LegacyStateConflict', 'ready_for_implementation disagrees with legacy phase rank');
896
+ throw new GovernanceError('LegacyStateConflict', 'ready_for_implementation disagrees with legacy phase rank. Correct the flag in the workflow state file.');
849
897
  }
850
898
  if (typeof raw.implementation_completed === 'boolean') {
851
899
  const completedRank = legacyPhase === 'completed' || legacyPhase === 'implementation-completed';
852
900
  if (raw.implementation_completed !== completedRank) {
853
- throw new GovernanceError('LegacyStateConflict', 'implementation_completed disagrees with legacy phase rank');
901
+ throw new GovernanceError('LegacyStateConflict', 'implementation_completed disagrees with legacy phase rank. Correct the flag in the workflow state file.');
854
902
  }
855
903
  }
856
904
  }
@@ -861,7 +909,7 @@ let WorkflowEngineService = class WorkflowEngineService {
861
909
  return WorkflowPhase.REQUIREMENTS; return WorkflowPhase.INIT; }
862
910
  implementationCounts(value) { const tasks = Object.values(value.tasks); return { completed: tasks.filter((t) => t.status === 'completed').length, total: tasks.length, active: tasks.filter((t) => ['in-progress', 'red-observed', 'green-observed'].includes(t.status)).length, blocked: tasks.filter((t) => t.status === 'blocked').length }; }
863
911
  allComplete(value) { const tasks = Object.values(value.tasks); return tasks.length > 0 && tasks.every((task) => task.status === 'completed'); }
864
- invalidTransition(action, status) { throw new GovernanceError('TaskTransitionInvalid', `Cannot ${action} from ${status}`); }
912
+ invalidTransition(action, status) { throw new GovernanceError('TaskTransitionInvalid', `Cannot ${action} from ${status}. Select a task action valid for its current status.`); }
865
913
  evidence(value) {
866
914
  if (typeof value.command !== 'string'
867
915
  || value.command.trim().length === 0
@@ -1382,7 +1430,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1382
1430
  const priorRaw = this.parseSpecBytes(priorBytes);
1383
1431
  const nextRaw = this.parseSpecBytes(nextBytes);
1384
1432
  if (priorRaw.schema_version !== 5 || nextRaw.schema_version !== 5) {
1385
- throw new GovernanceError('RecoveryConflict', 'Submission journal endpoints must use schema-v5');
1433
+ throw new GovernanceError('RecoveryConflict', 'Submission journal endpoints must use schema-v5. Inspect the journal and spec files, then restore a known state.');
1386
1434
  }
1387
1435
  let prior;
1388
1436
  let next;
@@ -1391,14 +1439,14 @@ let WorkflowEngineService = class WorkflowEngineService {
1391
1439
  next = this.normalizeV5(nextRaw);
1392
1440
  }
1393
1441
  catch (error) {
1394
- throw new GovernanceError('RecoveryConflict', 'Submission journal contains invalid workflow state', { cause: errorSummary(error) });
1442
+ throw new GovernanceError('RecoveryConflict', 'Submission journal contains invalid workflow state. Inspect the journal and spec files, then restore a known state.', { cause: errorSummary(error) });
1395
1443
  }
1396
1444
  const changed = PHASES.filter((phase) => next.approvals[phase].revision === prior.approvals[phase].revision + 1);
1397
1445
  if (changed.length !== 1
1398
1446
  || prior.feature_name !== next.feature_name
1399
1447
  || PHASES.some((phase) => phase !== changed[0]
1400
1448
  && next.approvals[phase].revision !== prior.approvals[phase].revision)) {
1401
- throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe one phase revision');
1449
+ throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe one phase revision. Inspect the journal and spec files, then restore a known state.');
1402
1450
  }
1403
1451
  const changedPhase = changed[0];
1404
1452
  const priorRecord = prior.approvals[changedPhase];
@@ -1433,7 +1481,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1433
1481
  || !priorArtifactValid
1434
1482
  || !submissionRecordValid
1435
1483
  || !workflowMetadataValid) {
1436
- throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe a valid phase submission');
1484
+ throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe a valid phase submission. Inspect the journal and spec files, then restore a known state.');
1437
1485
  }
1438
1486
  return changedPhase;
1439
1487
  }
@@ -1449,7 +1497,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1449
1497
  }
1450
1498
  }
1451
1499
  catch (error) {
1452
- throw new GovernanceError('RecoveryConflict', 'Submission journal validation cannot be reproduced', { cause: errorSummary(error) });
1500
+ throw new GovernanceError('RecoveryConflict', 'Submission journal validation cannot be reproduced. Inspect the artifact, then submit the phase again.', { cause: errorSummary(error) });
1453
1501
  }
1454
1502
  }
1455
1503
  journalAncillaryState(spec) {
@@ -1511,8 +1559,8 @@ export { WorkflowEngineService };
1511
1559
  function sha256(content) { return createHash('sha256').update(content).digest('hex'); }
1512
1560
  function encode(content) { return { base64: Buffer.from(content).toString('base64'), sha256: sha256(content) }; }
1513
1561
  function decode(value) { if (!value || typeof value.base64 !== 'string' || typeof value.sha256 !== 'string')
1514
- throw new GovernanceError('RecoveryConflict', 'Journal byte record is malformed'); const bytes = Buffer.from(value.base64, 'base64'); if (bytes.length > JOURNAL_LIMIT)
1515
- throw new GovernanceError('RecoveryConflict', 'Journal byte record exceeds size limit'); const content = bytes.toString('utf8'); if (sha256(content) !== value.sha256 || Buffer.from(content).toString('base64') !== value.base64)
1516
- throw new GovernanceError('RecoveryConflict', 'Journal byte hash or encoding is invalid'); return content; }
1562
+ throw new GovernanceError('RecoveryConflict', 'Journal byte record is malformed. Inspect the journal and spec files, then restore a known state.'); const bytes = Buffer.from(value.base64, 'base64'); if (bytes.length > JOURNAL_LIMIT)
1563
+ throw new GovernanceError('RecoveryConflict', 'Journal byte record exceeds size limit. Inspect the journal and spec files, then restore a known state.'); const content = bytes.toString('utf8'); if (sha256(content) !== value.sha256 || Buffer.from(content).toString('base64') !== value.base64)
1564
+ throw new GovernanceError('RecoveryConflict', 'Journal byte hash or encoding is invalid. Inspect the journal and spec files, then restore a known state.'); return content; }
1517
1565
  function errorSummary(error) { return String(error).slice(0, 2_000); }
1518
1566
  //# sourceMappingURL=WorkflowEngineService.js.map