@haposoft/cafekit 0.16.7 → 0.18.1

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 (231) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +169 -272
  4. package/bin/__tests__/codex-native.test.js +100 -202
  5. package/bin/__tests__/develop-contract.test.js +122 -1413
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +90 -925
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +30 -161
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
  21. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  22. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  23. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  24. package/bin/__tests__/validator-grounding.test.js +18 -2949
  25. package/bin/lib/managed-writer.js +2 -3
  26. package/bin/phases/codex-runtime.js +27 -0
  27. package/bin/phases/copy-payload.js +13 -28
  28. package/bin/phases/omp-runtime.js +25 -0
  29. package/package.json +2 -3
  30. package/src/claude/CLAUDE.md +3 -10
  31. package/src/claude/agents/brainstormer.md +7 -3
  32. package/src/claude/agents/code-auditor.md +44 -56
  33. package/src/claude/agents/debugger.md +8 -88
  34. package/src/claude/agents/deployer.md +3 -5
  35. package/src/claude/agents/docs-keeper.md +6 -14
  36. package/src/claude/agents/git-ops.md +2 -4
  37. package/src/claude/agents/implementer.md +12 -16
  38. package/src/claude/agents/inspector.md +4 -3
  39. package/src/claude/agents/project-manager.md +9 -17
  40. package/src/claude/agents/researcher.md +9 -3
  41. package/src/claude/agents/spec-maker.md +15 -15
  42. package/src/claude/agents/test-runner.md +8 -11
  43. package/src/claude/agents/ui-ux-designer.md +0 -1
  44. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
  46. package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
  47. package/src/claude/hooks/__tests__/state.test.js +68 -0
  48. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  49. package/src/claude/hooks/agent.cjs +2 -1
  50. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  51. package/src/claude/hooks/rules.cjs +2 -1
  52. package/src/claude/hooks/session.cjs +0 -1
  53. package/src/claude/hooks/spec-gate.cjs +17 -83
  54. package/src/claude/hooks/spec-state.cjs +142 -39
  55. package/src/claude/hooks/state.cjs +21 -2
  56. package/src/claude/migration-manifest.json +42 -22
  57. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  58. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  59. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  60. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  61. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  62. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  63. package/src/claude/rules/ai-dev-rules.md +2 -3
  64. package/src/claude/rules/hook-protocols.md +8 -15
  65. package/src/claude/rules/manage-docs.md +9 -23
  66. package/src/claude/rules/orchestrator.md +3 -4
  67. package/src/claude/rules/review-audit-self-decision.md +3 -3
  68. package/src/claude/rules/skill-domain-routing.md +1 -3
  69. package/src/claude/rules/skill-workflow-routing.md +4 -5
  70. package/src/claude/rules/state-sync.md +3 -12
  71. package/src/claude/rules/workflow.md +11 -19
  72. package/src/claude/runtime.schema.json +5 -3
  73. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  74. package/src/claude/scripts/provenance.cjs +23 -5
  75. package/src/claude/scripts/spec-receipt.cjs +24 -121
  76. package/src/claude/scripts/spec-resolver.cjs +41 -458
  77. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  78. package/src/claude/settings/settings.json +1 -32
  79. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  80. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  81. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  82. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  83. package/src/claude/skills/code-review/SKILL.md +29 -81
  84. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
  85. package/src/claude/skills/debug/SKILL.md +29 -149
  86. package/src/claude/skills/develop/SKILL.md +26 -83
  87. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  88. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  89. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  90. package/src/claude/skills/docs/SKILL.md +2 -2
  91. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  92. package/src/claude/skills/fix/SKILL.md +153 -0
  93. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  94. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  95. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  96. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  97. package/src/claude/skills/git/SKILL.md +48 -9
  98. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  99. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  100. package/src/claude/skills/orca/SKILL.md +9 -8
  101. package/src/claude/skills/research/SKILL.md +17 -46
  102. package/src/claude/skills/scout/SKILL.md +61 -0
  103. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  104. package/src/claude/skills/specs/SKILL.md +22 -29
  105. package/src/claude/skills/specs/references/review.md +8 -6
  106. package/src/claude/skills/specs/references/templates.md +34 -41
  107. package/src/claude/skills/sync/SKILL.md +16 -45
  108. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  109. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  110. package/src/claude/skills/test/SKILL.md +39 -104
  111. package/src/claude/skills/test/references/execution-strategy.md +93 -21
  112. package/src/claude/skills/test/references/failure-triage.md +1 -1
  113. package/src/claude/skills/ui-ux/LICENSE +21 -0
  114. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  115. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  116. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  117. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  118. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  119. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  120. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  121. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  122. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  123. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  124. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  125. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  126. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  127. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  128. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  129. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  130. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  131. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  132. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  133. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  134. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  135. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  136. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  137. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  138. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  139. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  140. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  141. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  142. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  143. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  144. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  145. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  146. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  147. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  148. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  149. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  150. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  151. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  152. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  153. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  154. package/src/claude/skills/ui-ux/references/review.md +528 -0
  155. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  156. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  157. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  158. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  159. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  160. package/src/claude/skills/ui-ux/references/system.md +219 -0
  161. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  162. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  163. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  164. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  165. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  166. package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
  167. package/src/claude/skills/web-testing/SKILL.md +2 -20
  168. package/src/claude/status.cjs +149 -144
  169. package/src/codex/AGENTS.md +8 -13
  170. package/src/codex/hooks/agent.cjs +2 -1
  171. package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
  172. package/src/codex/hooks/lib/spec-utils.cjs +6 -52
  173. package/src/codex/hooks/rules.cjs +2 -1
  174. package/src/codex/hooks/session.cjs +0 -2
  175. package/src/codex/hooks/spec-gate.cjs +13 -81
  176. package/src/codex/hooks/spec-state.cjs +37 -41
  177. package/src/codex/hooks.json +0 -20
  178. package/src/codex/rules/state-sync.md +3 -12
  179. package/src/common/AGENTS.md +6 -12
  180. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  181. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  182. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  183. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  184. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  185. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  186. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  187. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  188. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  189. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  190. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  191. package/src/claude/hooks/completion-authority.cjs +0 -163
  192. package/src/claude/hooks/lib/context.cjs +0 -616
  193. package/src/claude/hooks/lib/detect.cjs +0 -474
  194. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  195. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  196. package/src/claude/hooks/usage.cjs +0 -197
  197. package/src/claude/scripts/change-firewall.cjs +0 -1748
  198. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  199. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  200. package/src/claude/scripts/spec-final-state.cjs +0 -290
  201. package/src/claude/scripts/spec-ground.cjs +0 -650
  202. package/src/claude/scripts/spec-readiness.cjs +0 -425
  203. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  204. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  205. package/src/claude/scripts/validate-spec-output.cjs +0 -3941
  206. package/src/claude/skills/code-review/references/verification-gate.md +0 -46
  207. package/src/claude/skills/delegate/SKILL.md +0 -124
  208. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  209. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  210. package/src/claude/skills/hotfix/SKILL.md +0 -289
  211. package/src/claude/skills/inspect/SKILL.md +0 -142
  212. package/src/claude/skills/loop/SKILL.md +0 -114
  213. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  214. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  215. package/src/claude/skills/route/SKILL.md +0 -94
  216. package/src/claude/skills/route/references/agent-timing.md +0 -46
  217. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  218. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  219. package/src/claude/skills/specs/templates/design.md +0 -82
  220. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  221. package/src/claude/skills/specs/templates/requirements.md +0 -42
  222. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  223. package/src/claude/skills/specs/templates/task.md +0 -47
  224. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  225. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  226. package/src/codex/hooks/completion-authority.cjs +0 -109
  227. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  228. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  229. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  230. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  231. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -58,10 +58,6 @@ function assertSpecsRootContained(projectRoot, specsDir) {
58
58
  }
59
59
  }
60
60
 
61
- function isPlainObject(value) {
62
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
63
- }
64
-
65
61
  function candidateError(reason, code = 'malformed') {
66
62
  const error = new Error(reason);
67
63
  error.code = code;
@@ -221,10 +217,6 @@ function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
221
217
  }
222
218
 
223
219
  const featureName = path.basename(canonicalFeature);
224
- if (fs.existsSync(path.join(canonicalFeature, 'spec.json'))) {
225
- return { legacyPresent: true, featureName, featureDir: canonicalFeature };
226
- }
227
-
228
220
  const planFile = path.join(canonicalFeature, 'plan.md');
229
221
  let planLstat;
230
222
  try {
@@ -289,210 +281,6 @@ function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
289
281
  };
290
282
  }
291
283
 
292
- // Legacy registries written before the current vocabulary use `completed`, which today's
293
- // validator rejects but which exists in upgraded repositories. Reading it as finished is
294
- // what lets those packets stop competing for the gate's attention; `spec-final-state.cjs`
295
- // already reads spec status the same way. Process-first packets keep the strict set.
296
- const LEGACY_TASK_DONE = new Set(['done', 'completed', 'complete']);
297
-
298
- function normalizeLegacyWorkflowCandidate(candidate) {
299
- const registry = candidate.spec?.task_registry || {};
300
- const tasks = Object.entries(registry).map(([taskPath, task]) => ({
301
- path: taskPath,
302
- status: task?.status || 'pending',
303
- }));
304
- return {
305
- ...candidate,
306
- layoutKind: 'legacy-spec',
307
- stateFile: candidate.specFile,
308
- phase: candidate.spec?.current_phase || candidate.spec?.phase || 'unknown',
309
- taskRegistry: registry,
310
- tasks,
311
- allTasksDone: tasks.length > 0 && tasks.every((task) => LEGACY_TASK_DONE.has(task.status)),
312
- };
313
- }
314
-
315
- function inspectFeature(specsDir, canonicalSpecs, requestedName) {
316
- const featureDir = path.join(specsDir, requestedName);
317
- let featureLstat;
318
- try {
319
- featureLstat = fs.lstatSync(featureDir);
320
- } catch (e) {
321
- if (isMissingError(e)) return { missing: true };
322
- throw candidateError(`feature lstat error: ${e.message}`);
323
- }
324
-
325
- let canonicalFeature;
326
- try {
327
- canonicalFeature = fs.realpathSync(featureDir);
328
- if (!fs.statSync(featureDir).isDirectory()) {
329
- throw candidateError('feature entry must be a directory');
330
- }
331
- } catch (e) {
332
- if (e && e.code) throw e;
333
- throw candidateError(`feature canonicalization error: ${e.message}`);
334
- }
335
- if (canonicalFeature !== canonicalSpecs && !isPathInside(canonicalSpecs, canonicalFeature)) {
336
- throw candidateError('feature symlink escapes specs root');
337
- }
338
- if (path.dirname(canonicalFeature) !== canonicalSpecs) {
339
- throw candidateError('feature directory must resolve to a direct child of specs root');
340
- }
341
-
342
- const featureName = path.basename(canonicalFeature);
343
- const specFile = path.join(featureDir, 'spec.json');
344
- let specLstat;
345
- try {
346
- specLstat = fs.lstatSync(specFile);
347
- } catch (e) {
348
- if (isMissingError(e)) return { missingSpec: true, featureName, featureDir, canonicalFeature, specFile };
349
- throw candidateError(`spec.json lstat error: ${e.message}`);
350
- }
351
-
352
- let canonicalSpecFile;
353
- try {
354
- canonicalSpecFile = fs.realpathSync(specFile);
355
- if (!fs.statSync(specFile).isFile()) {
356
- throw candidateError('spec.json must be a regular file');
357
- }
358
- } catch (e) {
359
- if (e && e.code) throw e;
360
- throw candidateError(`spec.json canonicalization error: ${e.message}`);
361
- }
362
- if (canonicalSpecFile !== canonicalSpecs && !isPathInside(canonicalSpecs, canonicalSpecFile)) {
363
- throw candidateError('spec file symlink escapes specs root');
364
- }
365
- const expectedSpecFile = path.join(canonicalFeature, 'spec.json');
366
- if (canonicalSpecFile !== expectedSpecFile) {
367
- throw candidateError(`spec.json does not belong to feature directory ${featureName}`);
368
- }
369
-
370
- let spec;
371
- try {
372
- spec = JSON.parse(fs.readFileSync(canonicalSpecFile, 'utf8'));
373
- } catch (e) {
374
- throw candidateError(e.message);
375
- }
376
- if (!isPlainObject(spec)) throw candidateError('spec.json must contain a JSON object');
377
- if (typeof spec.feature_name !== 'string' || spec.feature_name.trim() !== featureName) {
378
- throw candidateError(`spec.json.feature_name must equal canonical feature name ${featureName}`);
379
- }
380
-
381
- return {
382
- featureName,
383
- spec,
384
- specsDir,
385
- featureDir: canonicalFeature,
386
- specFile: canonicalSpecFile,
387
- canonicalFeature,
388
- canonicalSpecFile,
389
- featureLstat,
390
- specLstat,
391
- };
392
- }
393
-
394
- function scanSpecs(specsDir) {
395
- let canonicalSpecs;
396
- let specsLstat = null;
397
- try {
398
- specsLstat = fs.lstatSync(specsDir);
399
- } catch (e) {
400
- if (isMissingError(e)) return { active: [], candidates: [], invalid: [], canonicalSpecs: path.resolve(specsDir) };
401
- return { active: [], candidates: [], invalid: [{ featureName: '<specs>', reason: `specs lstat error: ${e.message}`, specFile: specsDir }], canonicalSpecs: path.resolve(specsDir) };
402
- }
403
- try {
404
- canonicalSpecs = fs.realpathSync(specsDir);
405
- } catch (e) {
406
- return { active: [], candidates: [], invalid: [{ featureName: '<specs>', reason: `specs root canonicalization error: ${e.message}`, specFile: specsDir }], canonicalSpecs: path.resolve(specsDir) };
407
- }
408
-
409
- let entries;
410
- try {
411
- entries = fs.readdirSync(specsDir, { withFileTypes: true });
412
- } catch (e) {
413
- return { active: [], candidates: [], invalid: [{ featureName: '<specs>', reason: `specs directory read error: ${e.message}`, specFile: specsDir }], canonicalSpecs };
414
- }
415
- const active = [];
416
- const candidates = [];
417
- const invalid = [];
418
- const seenCanonicalFeatures = new Set();
419
- const sorted = entries.sort((a, b) => a.name.localeCompare(b.name));
420
- for (const entry of sorted) {
421
- try {
422
- const candidate = inspectFeature(specsDir, canonicalSpecs, entry.name);
423
- if (candidate.missing) {
424
- invalid.push({
425
- featureName: entry.name,
426
- reason: 'feature entry disappeared during scan',
427
- specFile: path.join(specsDir, entry.name, 'spec.json'),
428
- });
429
- continue;
430
- }
431
- if (candidate.missingSpec) {
432
- continue;
433
- }
434
- if (seenCanonicalFeatures.has(candidate.canonicalFeature)) continue;
435
- seenCanonicalFeatures.add(candidate.canonicalFeature);
436
- candidates.push(candidate);
437
- if (candidate.spec.status === 'in_progress' || candidate.spec.status === 'in-progress') {
438
- active.push(candidate);
439
- }
440
- } catch (e) {
441
- invalid.push({
442
- featureName: entry.name,
443
- reason: e.message,
444
- specFile: path.join(specsDir, entry.name, 'spec.json'),
445
- });
446
- }
447
- }
448
- return { active, candidates, invalid, canonicalSpecs };
449
- }
450
-
451
- function findAllSpecCandidates(projectRoot, runtime) {
452
- let specsDir;
453
- try {
454
- specsDir = specsDirectory(projectRoot, runtime);
455
- } catch (error) {
456
- const wrapped = new Error(`Invalid spec candidates: <specs>: ${error.message}`);
457
- wrapped.code = 'INVALID_SPECS';
458
- wrapped.invalid = [{ featureName: '<specs>', reason: error.message, specFile: path.resolve(projectRoot, 'specs') }];
459
- throw wrapped;
460
- }
461
- const scanned = scanSpecs(specsDir);
462
- if (scanned.invalid.length > 0) {
463
- const error = new Error(`Invalid spec candidates: ${scanned.invalid.map((item) => `${item.featureName}: ${item.reason}`).join('; ')}`);
464
- error.code = 'INVALID_SPECS';
465
- error.invalid = scanned.invalid;
466
- throw error;
467
- }
468
- return scanned.candidates.sort((left, right) => left.featureName.localeCompare(right.featureName));
469
- }
470
-
471
- function findAllActiveSpecs(projectRoot, runtime) {
472
- let specsDir;
473
- try {
474
- specsDir = specsDirectory(projectRoot, runtime);
475
- } catch (error) {
476
- const wrapped = new Error(`Invalid spec candidates: <specs>: ${error.message}`);
477
- wrapped.code = 'INVALID_SPECS';
478
- wrapped.invalid = [{ featureName: '<specs>', reason: error.message, specFile: path.resolve(projectRoot, 'specs') }];
479
- throw wrapped;
480
- }
481
- const { active, invalid } = scanSpecs(specsDir);
482
- if (invalid.length > 0) {
483
- const error = new Error(`Invalid spec candidates: ${invalid.map((item) => `${item.featureName}: ${item.reason}`).join('; ')}`);
484
- error.code = 'INVALID_SPECS';
485
- error.invalid = invalid;
486
- throw error;
487
- }
488
- return active;
489
- }
490
-
491
- function isPathInsideLegacy(parent, child) {
492
- const rel = path.relative(parent, child);
493
- return rel !== '' && !rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel);
494
- }
495
-
496
284
  function explicitTargetValue(value) {
497
285
  return typeof value === 'string' ? value.trim() : value;
498
286
  }
@@ -535,7 +323,39 @@ function readActiveFeatureTarget({ projectRoot, runtime } = {}) {
535
323
  const value = parsed.featureName;
536
324
  if (typeof value !== 'string' || /[\r\n]/.test(value)) return null;
537
325
  const trimmed = value.trim();
538
- return trimmed ? { explicitFeature: trimmed } : null;
326
+ if (!trimmed) return null;
327
+ // A recorded target naming a legacy spec.json packet is ignored rather than turned into
328
+ // a Stop block: CafeKit no longer reads those packets. A missing directory still reaches
329
+ // the resolver and fails as explicit_not_found; a name that is not a plain directory
330
+ // segment is left for the resolver to reject before anything outside specs is touched.
331
+ if (SAFE_PACKET_NAME.test(trimmed) && isLegacyPacketDir(path.join(specsDir, trimmed))) return null;
332
+ return { explicitFeature: trimmed };
333
+ }
334
+
335
+ // Names printed back to the model are limited to plain directory segments.
336
+ const SAFE_PACKET_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
337
+
338
+ // A legacy packet is a direct specs/<x>/ directory holding spec.json and no plan.md.
339
+ // A directory with both files is read as a process-first packet.
340
+ function isLegacyPacketDir(featureDir) {
341
+ const info = lstatOptional(featureDir);
342
+ if (!info.exists || info.isSymlink || !info.stat.isDirectory()) return false;
343
+ return fs.existsSync(path.join(featureDir, 'spec.json'))
344
+ && !fs.existsSync(path.join(featureDir, 'plan.md'));
345
+ }
346
+
347
+ function findLegacyPackets(projectRoot, runtime) {
348
+ let specsDir;
349
+ try {
350
+ specsDir = specsDirectory(projectRoot, runtime || {});
351
+ return fs.readdirSync(specsDir, { withFileTypes: true })
352
+ .filter((entry) => entry.isDirectory() && SAFE_PACKET_NAME.test(entry.name)
353
+ && isLegacyPacketDir(path.join(specsDir, entry.name)))
354
+ .map((entry) => entry.name)
355
+ .sort();
356
+ } catch {
357
+ return [];
358
+ }
539
359
  }
540
360
 
541
361
  /**
@@ -583,212 +403,6 @@ function lstatOptional(p) {
583
403
  }
584
404
  }
585
405
 
586
- function resolveActiveSpec({ projectRoot, runtime, explicitFeature, explicitPath, target } = {}) {
587
- if (!projectRoot) throw new TypeError('projectRoot required');
588
- const rt = runtime || {};
589
- const normalizedTarget = extractExplicitTarget(target);
590
- if (explicitFeature === undefined && normalizedTarget
591
- && Object.prototype.hasOwnProperty.call(normalizedTarget, 'explicitFeature')) {
592
- explicitFeature = normalizedTarget.explicitFeature;
593
- }
594
- if (explicitPath === undefined && normalizedTarget
595
- && Object.prototype.hasOwnProperty.call(normalizedTarget, 'explicitPath')) {
596
- explicitPath = normalizedTarget.explicitPath;
597
- }
598
- const hasExplicitFeature = explicitFeature !== undefined && explicitFeature !== null;
599
- const hasExplicitPath = explicitPath !== undefined && explicitPath !== null;
600
- const hasExplicitTarget = hasExplicitFeature || hasExplicitPath;
601
- let specsDir;
602
- try {
603
- specsDir = specsDirectory(projectRoot, rt);
604
- } catch (error) {
605
- return {
606
- error: hasExplicitTarget ? 'explicit_malformed' : 'invalid_specs',
607
- candidates: ['<specs>'],
608
- explicitFeature,
609
- explicitPath,
610
- reason: error.message,
611
- };
612
- }
613
- let canonicalSpecs;
614
- const specsLstat = lstatOptional(specsDir);
615
- if (specsLstat.error) {
616
- if (hasExplicitTarget) {
617
- return { error: 'explicit_malformed', explicitFeature, explicitPath, reason: `specs lstat error: ${specsLstat.error.message}` };
618
- }
619
- return { error: 'invalid_specs', candidates: ['<specs>'], invalid: [{ featureName: '<specs>', reason: `specs lstat error: ${specsLstat.error.message}`, specFile: specsDir }], reason: `Invalid spec JSON: <specs>: specs lstat error: ${specsLstat.error.message}` };
620
- }
621
- if (!specsLstat.exists) {
622
- canonicalSpecs = path.resolve(specsDir);
623
- } else {
624
- try {
625
- canonicalSpecs = fs.realpathSync(specsDir);
626
- } catch (e) {
627
- if (hasExplicitTarget) {
628
- return { error: 'explicit_malformed', explicitFeature, explicitPath, reason: `specs root canonicalization error: ${e.message}` };
629
- }
630
- return { error: 'invalid_specs', candidates: ['<specs>'], invalid: [{ featureName: '<specs>', reason: `specs root canonicalization error: ${e.message}`, specFile: specsDir }], reason: `Invalid spec JSON: <specs>: specs root canonicalization error: ${e.message}` };
631
- }
632
- }
633
-
634
- if (hasExplicitFeature) {
635
- if (typeof explicitFeature !== 'string' || explicitFeature.trim() === '' || explicitFeature.includes('/') || explicitFeature.includes('\\') || explicitFeature.includes('..')) {
636
- return { error: 'explicit_malformed', explicitFeature, reason: `malformed feature name: ${explicitFeature}` };
637
- }
638
- const featureDir = path.join(specsDir, explicitFeature);
639
- const resolvedFeature = path.resolve(featureDir);
640
- if (!isPathInsideLegacy(path.resolve(specsDir), resolvedFeature) && resolvedFeature !== path.resolve(specsDir)) {
641
- return { error: 'explicit_malformed', explicitFeature, reason: 'feature path escapes specs root' };
642
- }
643
- if (path.relative(path.resolve(specsDir), resolvedFeature) !== explicitFeature) {
644
- return { error: 'explicit_malformed', explicitFeature, reason: 'feature name does not canonicalize inside specs root' };
645
- }
646
- try {
647
- const candidate = inspectFeature(specsDir, canonicalSpecs, explicitFeature);
648
- if (candidate.missing || candidate.missingSpec) {
649
- return { error: 'explicit_not_found', explicitFeature, reason: `spec not found for feature ${explicitFeature}` };
650
- }
651
- if (candidate.featureName !== explicitFeature) {
652
- return {
653
- error: 'explicit_malformed',
654
- explicitFeature,
655
- reason: `feature directory resolves to canonical feature ${candidate.featureName}`,
656
- };
657
- }
658
- return candidate;
659
- } catch (e) {
660
- return { error: 'explicit_malformed', explicitFeature, reason: e.message };
661
- }
662
- }
663
-
664
- if (hasExplicitPath) {
665
- if (typeof explicitPath !== 'string' || explicitPath.trim() === '') {
666
- return { error: 'explicit_malformed', explicitPath, reason: 'empty explicit path' };
667
- }
668
- let specFile = explicitPath;
669
- if (!path.isAbsolute(specFile)) {
670
- specFile = path.resolve(projectRoot, specFile);
671
- } else {
672
- specFile = path.resolve(specFile);
673
- }
674
- try {
675
- const st = fs.lstatSync(specFile);
676
- if (st.isDirectory()) {
677
- specFile = path.join(specFile, 'spec.json');
678
- } else if (st.isSymbolicLink()) {
679
- try {
680
- if (fs.statSync(specFile).isDirectory()) specFile = path.join(specFile, 'spec.json');
681
- } catch (e) {
682
- if (!isMissingError(e)) throw e;
683
- }
684
- }
685
- } catch (e) {
686
- if (!isMissingError(e)) {
687
- return { error: 'explicit_malformed', explicitPath, reason: `explicit path stat error: ${e.message}` };
688
- }
689
- // If lstat is missing, the candidate is handled as an explicit not-found below.
690
- }
691
- const featureDir = path.dirname(specFile);
692
- if (!isPathInsideLegacy(path.resolve(specsDir), path.resolve(featureDir)) && path.resolve(featureDir) !== path.resolve(specsDir)) {
693
- return { error: 'explicit_malformed', explicitPath, reason: 'explicit path escapes specs root' };
694
- }
695
- const rel = path.relative(path.resolve(specsDir), path.resolve(specFile));
696
- const segs = rel.split(path.sep);
697
- if (segs.length !== 2 || segs[1] !== 'spec.json') {
698
- return { error: 'explicit_malformed', explicitPath, reason: 'explicit path must be <specs>/<feature>/spec.json or <specs>/<feature>' };
699
- }
700
- try {
701
- const candidate = inspectFeature(specsDir, canonicalSpecs, segs[0]);
702
- if (candidate.missing || candidate.missingSpec) {
703
- return { error: 'explicit_not_found', explicitPath, reason: `spec not found at ${explicitPath}` };
704
- }
705
- if (candidate.featureName !== segs[0]) {
706
- return {
707
- error: 'explicit_malformed',
708
- explicitPath,
709
- reason: `explicit path resolves to canonical feature ${candidate.featureName}, not ${segs[0]}`,
710
- };
711
- }
712
- return candidate;
713
- } catch (e) {
714
- return { error: 'explicit_malformed', explicitPath, reason: e.message };
715
- }
716
- }
717
-
718
- const { active, invalid } = scanSpecs(specsDir);
719
- if (invalid.length > 0) {
720
- return {
721
- error: 'invalid_specs',
722
- candidates: invalid.map((i) => i.featureName),
723
- invalid,
724
- reason: `Invalid spec JSON: ${invalid.map((i) => `${i.featureName}: ${i.reason}`).join('; ')}`,
725
- };
726
- }
727
- active.sort((left, right) => left.featureName.localeCompare(right.featureName));
728
- if (active.length === 0) return null;
729
- if (active.length === 1) return active[0];
730
- return {
731
- error: 'multiple_active',
732
- candidates: active.map((a) => a.featureName),
733
- active,
734
- reason: `Multiple active specs found: ${active.map((a) => a.featureName).join(', ')}. Provide explicit feature.`,
735
- };
736
- }
737
-
738
- /**
739
- * Resolve one persisted feature identity. Explicit host targets are inspected
740
- * directly and never scan siblings; only the no-target path performs a global
741
- * persisted-candidate scan and therefore owns ambiguity/invalid-sibling errors.
742
- */
743
- function resolvePersistedSpec({ projectRoot, runtime, explicitFeature, explicitPath, target } = {}) {
744
- const normalized = extractExplicitTarget(
745
- target,
746
- explicitFeature !== undefined ? { explicitFeature } : null,
747
- explicitPath !== undefined ? { explicitPath } : null,
748
- );
749
- if (normalized) {
750
- const value = Object.prototype.hasOwnProperty.call(normalized, 'explicitFeature')
751
- ? normalized.explicitFeature
752
- : normalized.explicitPath;
753
- if (value === null || value === undefined) {
754
- return { error: 'explicit_malformed', ...normalized, reason: 'explicit target must be a non-empty string' };
755
- }
756
- return resolveActiveSpec({ projectRoot, runtime, ...normalized });
757
- }
758
- // Only now, with the caller having named nothing: a recorded target must never
759
- // redirect a hook that already knows which feature its turn is about.
760
- const recorded = readActiveFeatureTarget({ projectRoot, runtime });
761
- if (recorded) return resolveActiveSpec({ projectRoot, runtime, ...recorded });
762
- try {
763
- const candidates = findAllSpecCandidates(projectRoot, runtime);
764
- if (candidates.length === 0) return null;
765
- // One packet resolves on its own terms. Closeout approval is claimed against a
766
- // finished spec, so status must never decide identity here.
767
- if (candidates.length === 1) return candidates[0];
768
- // This path serves closeout approval, so only a packet actually claiming closeout is
769
- // a real rival. Counting finished history as ambiguity is what made the gate
770
- // unsatisfiable in a repository that had simply accumulated features.
771
- const { isDurableCloseout } = require('./spec-final-state.cjs');
772
- const claiming = candidates.filter((candidate) => isDurableCloseout(candidate.spec));
773
- if (claiming.length === 1) return claiming[0];
774
- if (claiming.length === 0) return null;
775
- return {
776
- error: 'multiple_persisted',
777
- candidates: claiming.map((candidate) => candidate.featureName),
778
- reason: `Multiple persisted specs found: ${claiming.map((candidate) => candidate.featureName).join(', ')}. Provide explicit feature.`,
779
- };
780
- } catch (error) {
781
- return {
782
- error: 'invalid_specs',
783
- candidates: Array.isArray(error.invalid)
784
- ? error.invalid.map((entry) => entry.featureName)
785
- : ['<specs>'],
786
- invalid: error.invalid || [],
787
- reason: error.message,
788
- };
789
- }
790
- }
791
-
792
406
  function scanWorkflowFeatures(specsDir) {
793
407
  let canonicalSpecs;
794
408
  try {
@@ -807,7 +421,7 @@ function scanWorkflowFeatures(specsDir) {
807
421
  for (const entry of entries) {
808
422
  try {
809
423
  const candidate = inspectWorkflowFeature(canonicalSpecs, canonicalSpecs, entry.name);
810
- if (candidate.missing || candidate.missingWorkflow || candidate.legacyPresent) continue;
424
+ if (candidate.missing || candidate.missingWorkflow) continue;
811
425
  candidates.push(candidate);
812
426
  if (!candidate.allTasksDone) active.push(candidate);
813
427
  } catch (error) {
@@ -859,9 +473,8 @@ function explicitWorkflowFeatureName({ projectRoot, specsDir, explicitFeature, e
859
473
  }
860
474
 
861
475
  /**
862
- * Resolve the authoring/execution workflow visible to prompt and Stop hooks.
863
- * Persisted spec resolution remains unchanged; process-v3 is an additive adapter
864
- * and never becomes completion-authority input by way of findAllSpecCandidates.
476
+ * Resolve the process-first packet (plan.md plus flat task files) visible to prompt
477
+ * and Stop hooks.
865
478
  */
866
479
  function resolveWorkflowCandidate({ projectRoot, runtime, explicitFeature, explicitPath, target, includeCompleted = false } = {}) {
867
480
  if (!projectRoot) throw new TypeError('projectRoot required');
@@ -884,20 +497,14 @@ function resolveWorkflowCandidate({ projectRoot, runtime, explicitFeature, expli
884
497
  }
885
498
 
886
499
  if (hasExplicit) {
887
- const legacyDirect = resolveActiveSpec({ projectRoot, runtime, explicitFeature, explicitPath });
888
- if (legacyDirect && !legacyDirect.error) return normalizeLegacyWorkflowCandidate(legacyDirect);
889
500
  const identity = explicitWorkflowFeatureName({ projectRoot, specsDir, explicitFeature, explicitPath });
890
501
  if (identity.error) return identity;
891
502
 
892
- const legacy = resolveActiveSpec({ projectRoot, runtime, explicitFeature: identity.featureName });
893
- if (legacy && !legacy.error) return normalizeLegacyWorkflowCandidate(legacy);
894
- if (legacy && legacy.error !== 'explicit_not_found') return legacy;
895
-
896
503
  let canonicalSpecs;
897
504
  try {
898
505
  canonicalSpecs = fs.realpathSync(specsDir);
899
506
  const candidate = inspectWorkflowFeature(specsDir, canonicalSpecs, identity.featureName);
900
- if (candidate.missing || candidate.missingWorkflow || candidate.legacyPresent) {
507
+ if (candidate.missing || candidate.missingWorkflow) {
901
508
  return { error: 'explicit_not_found', explicitFeature: identity.featureName, explicitPath, reason: `workflow not found for feature ${identity.featureName}` };
902
509
  }
903
510
  return candidate;
@@ -906,20 +513,6 @@ function resolveWorkflowCandidate({ projectRoot, runtime, explicitFeature, expli
906
513
  }
907
514
  }
908
515
 
909
- let legacyActive;
910
- try {
911
- const legacyCandidates = includeCompleted
912
- ? findAllSpecCandidates(projectRoot, runtime || {})
913
- : findAllActiveSpecs(projectRoot, runtime || {});
914
- legacyActive = legacyCandidates.map(normalizeLegacyWorkflowCandidate);
915
- } catch (error) {
916
- return {
917
- error: 'invalid_specs',
918
- candidates: Array.isArray(error.invalid) ? error.invalid.map((entry) => entry.featureName) : ['<specs>'],
919
- invalid: error.invalid || [],
920
- reason: error.message,
921
- };
922
- }
923
516
  const workflow = scanWorkflowFeatures(specsDir);
924
517
  if (workflow.invalid.length > 0) {
925
518
  return {
@@ -930,7 +523,7 @@ function resolveWorkflowCandidate({ projectRoot, runtime, explicitFeature, expli
930
523
  };
931
524
  }
932
525
  const workflowCandidates = includeCompleted ? workflow.candidates : workflow.active;
933
- const active = [...legacyActive, ...workflowCandidates]
526
+ const active = [...workflowCandidates]
934
527
  .sort((left, right) => left.featureName.localeCompare(right.featureName));
935
528
  if (active.length === 0) return null;
936
529
  if (active.length === 1) return active[0];
@@ -943,8 +536,7 @@ function resolveWorkflowCandidate({ projectRoot, runtime, explicitFeature, expli
943
536
  }
944
537
 
945
538
  /**
946
- * Narrow a Stop-gate ambiguity without changing legacy persisted-spec rules.
947
- * A single unfinished process-v3 packet is the current workflow; completed
539
+ * Narrow a Stop-gate ambiguity. A single unfinished process-v3 packet is the current workflow; completed
948
540
  * packets are historical. If every process-v3 packet is complete, return the
949
541
  * set so the gate can revalidate all receipts instead of blocking on identity.
950
542
  */
@@ -955,9 +547,7 @@ function refineWorkflowGateResolution(resolved) {
955
547
  const candidates = resolved.active;
956
548
  if (candidates.length === 0) return resolved;
957
549
 
958
- // Which packet still has work decides this, not which layout it uses: a repository of
959
- // legacy packets was permanently ambiguous only because the check below used to sit
960
- // here and reject the whole set on sight.
550
+ // Which packet still has work decides this.
961
551
  const unfinished = candidates.filter((candidate) => !candidate.allTasksDone);
962
552
  if (unfinished.length === 1) return unfinished[0];
963
553
  if (unfinished.length > 1) {
@@ -969,10 +559,6 @@ function refineWorkflowGateResolution(resolved) {
969
559
  reason: `Multiple active workflows found: ${unfinished.map((candidate) => candidate.featureName).join(', ')}. Provide explicit feature.`,
970
560
  };
971
561
  }
972
- // The bulk-audit branch exits before the semantic-digest, FLASH_UNVERIFIED,
973
- // feature-receipt, and completion-policy layers that the single-candidate path runs, so
974
- // only process-first packets may reach it. Anything else keeps the existing ambiguity.
975
- if (candidates.some((candidate) => candidate.layoutKind !== 'process-v3')) return resolved;
976
562
  return {
977
563
  layoutKind: 'process-v3-completed-set',
978
564
  candidates,
@@ -983,13 +569,10 @@ function refineWorkflowGateResolution(resolved) {
983
569
 
984
570
  module.exports = {
985
571
  annotatedMarkdownLines,
572
+ findLegacyPackets,
986
573
  specsDirectory,
987
- findAllActiveSpecs,
988
- findAllSpecCandidates,
989
574
  extractExplicitTarget,
990
- resolveActiveSpec,
991
575
  readActiveFeatureTarget,
992
- resolvePersistedSpec,
993
576
  resolveWorkflowCandidate,
994
577
  refineWorkflowGateResolution,
995
578
  };