@lumenflow/cli 5.5.0 → 5.7.14

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 (229) hide show
  1. package/README.md +41 -40
  2. package/dist/db-journal-recover.js +400 -0
  3. package/dist/db-journal-recover.js.map +1 -0
  4. package/dist/docs-sync.js +8 -3
  5. package/dist/docs-sync.js.map +1 -1
  6. package/dist/doctor.js +11 -0
  7. package/dist/doctor.js.map +1 -1
  8. package/dist/gate-defaults.js +37 -0
  9. package/dist/gate-defaults.js.map +1 -1
  10. package/dist/gates/monolithic-file-contention-guard.js +167 -0
  11. package/dist/gates/monolithic-file-contention-guard.js.map +1 -0
  12. package/dist/gates/prod-migration-drift.js +207 -0
  13. package/dist/gates/prod-migration-drift.js.map +1 -0
  14. package/dist/gates/test-over-deletion-guard.js +280 -0
  15. package/dist/gates/test-over-deletion-guard.js.map +1 -0
  16. package/dist/gates-runners.js +44 -3
  17. package/dist/gates-runners.js.map +1 -1
  18. package/dist/gates.js +3 -2
  19. package/dist/gates.js.map +1 -1
  20. package/dist/hooks/config-resolver.js +16 -1
  21. package/dist/hooks/config-resolver.js.map +1 -1
  22. package/dist/hooks/dirty-guard.js +43 -2
  23. package/dist/hooks/dirty-guard.js.map +1 -1
  24. package/dist/hooks/git-status-parser.js +22 -8
  25. package/dist/hooks/git-status-parser.js.map +1 -1
  26. package/dist/init-templates.js +241 -0
  27. package/dist/init-templates.js.map +1 -1
  28. package/dist/init.js +122 -16
  29. package/dist/init.js.map +1 -1
  30. package/dist/lumenflow-setup.js +144 -0
  31. package/dist/lumenflow-setup.js.map +1 -0
  32. package/dist/lumenflow-upgrade.js +43 -1
  33. package/dist/lumenflow-upgrade.js.map +1 -1
  34. package/dist/mem-create.js +10 -1
  35. package/dist/mem-create.js.map +1 -1
  36. package/dist/mem-signal.js +21 -4
  37. package/dist/mem-signal.js.map +1 -1
  38. package/dist/orchestrate-initiative.js +28 -3
  39. package/dist/orchestrate-initiative.js.map +1 -1
  40. package/dist/public-manifest.js +17 -7
  41. package/dist/public-manifest.js.map +1 -1
  42. package/dist/release.js +53 -18
  43. package/dist/release.js.map +1 -1
  44. package/dist/wu-done-gates.js +13 -9
  45. package/dist/wu-done-gates.js.map +1 -1
  46. package/dist/wu-done.js +14 -2
  47. package/dist/wu-done.js.map +1 -1
  48. package/dist/wu-edit-operations.js +74 -0
  49. package/dist/wu-edit-operations.js.map +1 -1
  50. package/dist/wu-edit-validators.js +58 -0
  51. package/dist/wu-edit-validators.js.map +1 -1
  52. package/dist/wu-edit.js +106 -4
  53. package/dist/wu-edit.js.map +1 -1
  54. package/dist/wu-prep.js +57 -9
  55. package/dist/wu-prep.js.map +1 -1
  56. package/dist/wu-recover.js +6 -0
  57. package/dist/wu-recover.js.map +1 -1
  58. package/dist/wu-release.js +120 -2
  59. package/dist/wu-release.js.map +1 -1
  60. package/dist/wu-sizing-validation.js +47 -17
  61. package/dist/wu-sizing-validation.js.map +1 -1
  62. package/dist/wu-status.js +33 -0
  63. package/dist/wu-status.js.map +1 -1
  64. package/package.json +13 -12
  65. package/packs/agent-runtime/package.json +1 -1
  66. package/packs/sidekick/package.json +1 -1
  67. package/packs/software-delivery/package.json +1 -1
  68. package/templates/core/AGENTS.md.template +67 -3
  69. package/templates/core/LUMENFLOW.md.template +196 -47
  70. package/dist/distribution-preflight.js +0 -230
  71. package/dist/distribution-preflight.js.map +0 -1
  72. package/packs/agent-runtime/agent-heartbeat.ts +0 -163
  73. package/packs/agent-runtime/auto-session-integration.ts +0 -888
  74. package/packs/agent-runtime/capability-factory.ts +0 -104
  75. package/packs/agent-runtime/constants.ts +0 -21
  76. package/packs/agent-runtime/delegation-registry-schema.ts +0 -220
  77. package/packs/agent-runtime/delegation-registry-store.ts +0 -269
  78. package/packs/agent-runtime/delegation-tree.ts +0 -328
  79. package/packs/agent-runtime/index.ts +0 -20
  80. package/packs/agent-runtime/manifest.ts +0 -348
  81. package/packs/agent-runtime/memory-coordination-contract.ts +0 -86
  82. package/packs/agent-runtime/orchestration.ts +0 -2027
  83. package/packs/agent-runtime/pack-registration.ts +0 -110
  84. package/packs/agent-runtime/policy-factory.ts +0 -165
  85. package/packs/agent-runtime/remote-controls/index.ts +0 -7
  86. package/packs/agent-runtime/remote-controls/operations.ts +0 -405
  87. package/packs/agent-runtime/remote-controls/port.ts +0 -48
  88. package/packs/agent-runtime/remote-controls/state-store.ts +0 -258
  89. package/packs/agent-runtime/remote-controls/types.ts +0 -105
  90. package/packs/agent-runtime/session-schema.ts +0 -467
  91. package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +0 -793
  92. package/packs/agent-runtime/tool-impl/index.ts +0 -6
  93. package/packs/agent-runtime/tool-impl/provider-adapters.ts +0 -1245
  94. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +0 -256
  95. package/packs/agent-runtime/tool-impl/remote-controls.ts +0 -273
  96. package/packs/agent-runtime/tools/index.ts +0 -4
  97. package/packs/agent-runtime/tools/types.ts +0 -47
  98. package/packs/agent-runtime/turn-lifecycle-events.ts +0 -590
  99. package/packs/agent-runtime/types.ts +0 -128
  100. package/packs/agent-runtime/vitest.config.ts +0 -11
  101. package/packs/sidekick/channel-ingress.ts +0 -137
  102. package/packs/sidekick/constants.ts +0 -10
  103. package/packs/sidekick/index.ts +0 -8
  104. package/packs/sidekick/manifest-schema.ts +0 -49
  105. package/packs/sidekick/manifest.ts +0 -512
  106. package/packs/sidekick/pack-registration.ts +0 -110
  107. package/packs/sidekick/policy-factory.ts +0 -38
  108. package/packs/sidekick/sidekick-events.ts +0 -694
  109. package/packs/sidekick/src/adapters/cloud-queue.ts +0 -101
  110. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +0 -386
  111. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +0 -228
  112. package/packs/sidekick/src/domain/channel.types.ts +0 -64
  113. package/packs/sidekick/src/ports/channel-bridge.port.ts +0 -92
  114. package/packs/sidekick/src/routines/commit.ts +0 -74
  115. package/packs/sidekick/tool-impl/channel-tools.ts +0 -577
  116. package/packs/sidekick/tool-impl/channel-transports.ts +0 -75
  117. package/packs/sidekick/tool-impl/index.ts +0 -29
  118. package/packs/sidekick/tool-impl/memory-tools.ts +0 -290
  119. package/packs/sidekick/tool-impl/routine-commit.ts +0 -102
  120. package/packs/sidekick/tool-impl/routine-tools.ts +0 -440
  121. package/packs/sidekick/tool-impl/runtime-context.ts +0 -28
  122. package/packs/sidekick/tool-impl/shared.ts +0 -125
  123. package/packs/sidekick/tool-impl/storage.ts +0 -325
  124. package/packs/sidekick/tool-impl/system-tools.ts +0 -160
  125. package/packs/sidekick/tool-impl/task-tools.ts +0 -506
  126. package/packs/sidekick/tools/channel-tools.ts +0 -53
  127. package/packs/sidekick/tools/index.ts +0 -9
  128. package/packs/sidekick/tools/memory-tools.ts +0 -53
  129. package/packs/sidekick/tools/routine-tools.ts +0 -53
  130. package/packs/sidekick/tools/system-tools.ts +0 -47
  131. package/packs/sidekick/tools/task-tools.ts +0 -61
  132. package/packs/sidekick/tools/types.ts +0 -57
  133. package/packs/sidekick/vitest.config.ts +0 -11
  134. package/packs/software-delivery/constants.ts +0 -10
  135. package/packs/software-delivery/extensions.ts +0 -140
  136. package/packs/software-delivery/gate-policies.ts +0 -134
  137. package/packs/software-delivery/index.ts +0 -8
  138. package/packs/software-delivery/manifest-schema.ts +0 -268
  139. package/packs/software-delivery/manifest.ts +0 -657
  140. package/packs/software-delivery/pack-registration.ts +0 -113
  141. package/packs/software-delivery/src/commands/index.ts +0 -5
  142. package/packs/software-delivery/src/config/delivery-review-contract.ts +0 -256
  143. package/packs/software-delivery/src/config/env-accessors.ts +0 -66
  144. package/packs/software-delivery/src/config/index.ts +0 -8
  145. package/packs/software-delivery/src/config/normalize-config-keys.ts +0 -9
  146. package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +0 -460
  147. package/packs/software-delivery/src/config/workspace-reader.ts +0 -375
  148. package/packs/software-delivery/src/constants/backlog-patterns.ts +0 -31
  149. package/packs/software-delivery/src/constants/client-ids.ts +0 -19
  150. package/packs/software-delivery/src/constants/config-contract.ts +0 -7
  151. package/packs/software-delivery/src/constants/docs-layout-presets.ts +0 -50
  152. package/packs/software-delivery/src/constants/duration-constants.ts +0 -20
  153. package/packs/software-delivery/src/constants/gate-constants.ts +0 -32
  154. package/packs/software-delivery/src/constants/index.ts +0 -29
  155. package/packs/software-delivery/src/constants/lock-constants.ts +0 -35
  156. package/packs/software-delivery/src/constants/object-guards.ts +0 -12
  157. package/packs/software-delivery/src/constants/section-headings.ts +0 -107
  158. package/packs/software-delivery/src/constants/wu-cli-constants.ts +0 -500
  159. package/packs/software-delivery/src/constants/wu-domain-constants.ts +0 -466
  160. package/packs/software-delivery/src/constants/wu-git-constants.ts +0 -7
  161. package/packs/software-delivery/src/constants/wu-id-format.ts +0 -327
  162. package/packs/software-delivery/src/constants/wu-paths-constants.ts +0 -384
  163. package/packs/software-delivery/src/constants/wu-statuses.ts +0 -287
  164. package/packs/software-delivery/src/constants/wu-type-helpers.ts +0 -67
  165. package/packs/software-delivery/src/constants/wu-ui-constants.ts +0 -267
  166. package/packs/software-delivery/src/constants/wu-validation-constants.ts +0 -73
  167. package/packs/software-delivery/src/domain/index.ts +0 -5
  168. package/packs/software-delivery/src/domain/orchestration.constants.ts +0 -166
  169. package/packs/software-delivery/src/domain/orchestration.schemas.ts +0 -238
  170. package/packs/software-delivery/src/domain/orchestration.types.ts +0 -176
  171. package/packs/software-delivery/src/methodology/incremental-test.ts +0 -122
  172. package/packs/software-delivery/src/methodology/index.ts +0 -6
  173. package/packs/software-delivery/src/methodology/manual-test-validator.ts +0 -292
  174. package/packs/software-delivery/src/policy/coverage-gate.ts +0 -270
  175. package/packs/software-delivery/src/policy/gates-agent-mode.ts +0 -223
  176. package/packs/software-delivery/src/policy/gates-config-internal.ts +0 -121
  177. package/packs/software-delivery/src/policy/gates-config.ts +0 -300
  178. package/packs/software-delivery/src/policy/gates-coverage.ts +0 -356
  179. package/packs/software-delivery/src/policy/gates-presets.ts +0 -134
  180. package/packs/software-delivery/src/policy/gates-schemas.ts +0 -173
  181. package/packs/software-delivery/src/policy/index.ts +0 -22
  182. package/packs/software-delivery/src/policy/package-manager-resolver.ts +0 -319
  183. package/packs/software-delivery/src/policy/resolve-policy.ts +0 -601
  184. package/packs/software-delivery/src/ports/config.ports.ts +0 -90
  185. package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +0 -125
  186. package/packs/software-delivery/src/ports/index.ts +0 -10
  187. package/packs/software-delivery/src/ports/sync-validator.ports.ts +0 -59
  188. package/packs/software-delivery/src/ports/wu-helpers.ports.ts +0 -168
  189. package/packs/software-delivery/src/ports/wu-state.ports.ts +0 -241
  190. package/packs/software-delivery/src/primitives/index.ts +0 -5
  191. package/packs/software-delivery/src/runtime/index.ts +0 -6
  192. package/packs/software-delivery/src/runtime/work-classifier.ts +0 -561
  193. package/packs/software-delivery/src/sandbox/index.ts +0 -10
  194. package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +0 -118
  195. package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +0 -88
  196. package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +0 -154
  197. package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +0 -47
  198. package/packs/software-delivery/src/sandbox/sandbox-profile.ts +0 -153
  199. package/packs/software-delivery/src/schemas/index.ts +0 -5
  200. package/packs/software-delivery/src/state/date-utils.ts +0 -158
  201. package/packs/software-delivery/src/state/index.ts +0 -15
  202. package/packs/software-delivery/src/state/state-machine.ts +0 -119
  203. package/packs/software-delivery/src/state/wu-doc-types.ts +0 -51
  204. package/packs/software-delivery/src/state/wu-paths.ts +0 -381
  205. package/packs/software-delivery/src/state/wu-schema.ts +0 -1139
  206. package/packs/software-delivery/src/state/wu-state-schema.ts +0 -255
  207. package/packs/software-delivery/src/state/wu-yaml.ts +0 -338
  208. package/packs/software-delivery/tool-impl/agent-tools.ts +0 -263
  209. package/packs/software-delivery/tool-impl/delegation-tools.ts +0 -66
  210. package/packs/software-delivery/tool-impl/flow-metrics-tools.ts +0 -219
  211. package/packs/software-delivery/tool-impl/git-runner.ts +0 -113
  212. package/packs/software-delivery/tool-impl/git-tools.ts +0 -316
  213. package/packs/software-delivery/tool-impl/index.ts +0 -15
  214. package/packs/software-delivery/tool-impl/initiative-orchestration-tools.ts +0 -720
  215. package/packs/software-delivery/tool-impl/lane-lock.ts +0 -246
  216. package/packs/software-delivery/tool-impl/memory-tools.ts +0 -470
  217. package/packs/software-delivery/tool-impl/pending-runtime-tools.ts +0 -21
  218. package/packs/software-delivery/tool-impl/runtime-cli-adapter.ts +0 -329
  219. package/packs/software-delivery/tool-impl/runtime-native-tools.ts +0 -687
  220. package/packs/software-delivery/tool-impl/worker-loader.ts +0 -52
  221. package/packs/software-delivery/tool-impl/worktree-tools.ts +0 -46
  222. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +0 -807
  223. package/packs/software-delivery/tools/delegation-tools.ts +0 -23
  224. package/packs/software-delivery/tools/git-tools.ts +0 -55
  225. package/packs/software-delivery/tools/index.ts +0 -8
  226. package/packs/software-delivery/tools/lane-lock-tool.ts +0 -37
  227. package/packs/software-delivery/tools/types.ts +0 -71
  228. package/packs/software-delivery/tools/worktree-tools.ts +0 -49
  229. package/packs/software-delivery/vitest.config.ts +0 -11
@@ -1,1139 +0,0 @@
1
- // Copyright (c) 2026 Hellmai Ltd
2
- // SPDX-License-Identifier: LicenseRef-LumenFlow-Proprietary
3
-
4
- /**
5
- * Work Unit YAML Schema
6
- *
7
- * Zod schema for runtime validation of WU YAML structure.
8
- * Provides compile-time type inference and semantic validation.
9
- *
10
- * Part of WU-1162: Add Zod schema validation to prevent placeholder WU completions
11
- * Part of WU-1539: Add BaseWUSchema pattern for create/edit validation
12
- *
13
- * Schema Architecture (DRY pattern):
14
- * - BaseWUSchema: Structural validation only (field types, formats, lengths)
15
- * - WUSchema: Extends base + placeholder rejection (for wu:claim, wu:done)
16
- * - ReadyWUSchema: Alias for BaseWUSchema (for wu:create, wu:edit)
17
- *
18
- * @see {@link packages/@lumenflow/cli/src/wu-done.ts} - Consumer (validates spec completeness, uses WUSchema)
19
- * @see {@link packages/@lumenflow/cli/src/wu-claim.ts} - Consumer (validates spec completeness, uses WUSchema)
20
- * @see {@link packages/@lumenflow/cli/src/wu-create.ts} - Consumer (structural validation, uses ReadyWUSchema)
21
- * @see {@link packages/@lumenflow/cli/src/wu-edit.ts} - Consumer (structural validation, uses ReadyWUSchema)
22
- * @see {@link packages/@lumenflow/cli/src/validate.ts} - Consumer (CI validation)
23
- * @see {@link apps/web/src/lib/llm/schemas/orchestrator.ts} - Pattern reference
24
- */
25
-
26
- import { z } from 'zod';
27
-
28
- import {
29
- WU_STATUS_GROUPS,
30
- WU_EXPOSURE_VALUES,
31
- WU_TYPES,
32
- WU_TYPE_VALUES,
33
- type WUType,
34
- } from '../constants/wu-statuses.js';
35
- import { WU_DEFAULTS } from '../constants/wu-domain-constants.js';
36
- import { STRING_LITERALS } from '../constants/wu-ui-constants.js';
37
- import { isDocsOrProcessType, isWUType } from '../constants/wu-type-helpers.js';
38
-
39
- import { createWuPaths } from './wu-paths.js';
40
- import { normalizeISODateTime } from './date-utils.js';
41
- // WU-2225: getConfig import removed (getEscalationEmail was the only consumer)
42
-
43
- /**
44
- * Valid WU status values derived from WU_STATUS constant (DRY principle)
45
- * Used for Zod enum validation with improved error messages
46
- * Note: Defined as tuple for Zod enum compatibility
47
- */
48
- const VALID_STATUSES = [
49
- 'todo',
50
- 'ready',
51
- 'backlog',
52
- 'in_progress',
53
- 'blocked',
54
- 'done',
55
- 'completed',
56
- 'cancelled',
57
- 'abandoned',
58
- 'deferred',
59
- 'closed',
60
- 'superseded',
61
- ] as const;
62
-
63
- /**
64
- * Placeholder sentinel constant
65
- *
66
- * Used in wu:create template generation and validation.
67
- * Single source of truth for placeholder detection (DRY principle).
68
- *
69
- * @example
70
- * // tools/wu-create.ts
71
- * description: `${PLACEHOLDER_SENTINEL} Describe the work...`
72
- *
73
- * @example
74
- * // tools/validate.ts
75
- * if (doc.description.includes(PLACEHOLDER_SENTINEL)) { error(); }
76
- */
77
- export const PLACEHOLDER_SENTINEL = '[PLACEHOLDER]';
78
-
79
- /**
80
- * Minimum description length requirement
81
- * Stored as constant for DRY error message generation
82
- */
83
- const MIN_DESCRIPTION_LENGTH = 50;
84
-
85
- /**
86
- * WU ID format validation message (DRY principle)
87
- * Used across blocks, blocked_by, and ui_pairing_wus fields
88
- */
89
- const WU_ID_FORMAT_MESSAGE = 'Must be WU-XXX format';
90
-
91
- /**
92
- * Acceptance criterion error message
93
- * Stored as constant for DRY error message generation (sonarjs/no-duplicate-string)
94
- */
95
- const ACCEPTANCE_REQUIRED_MSG = 'At least one acceptance criterion required';
96
- const CONTEXT_REQUIRED_TYPES = [WU_TYPES.FEATURE, WU_TYPES.BUG, WU_TYPES.REFACTOR] as const;
97
-
98
- interface WUDoneValidationInput {
99
- type?: string;
100
- code_paths?: string[];
101
- }
102
-
103
- interface WUEscalationInput {
104
- id: string;
105
- lane?: string;
106
- code_paths?: string[];
107
- priority?: string;
108
- escalation_triggers?: string[];
109
- requires_human_escalation?: boolean;
110
- escalation_resolved_by?: string;
111
- escalation_resolved_at?: string;
112
- requires_cso_approval?: boolean;
113
- requires_cto_approval?: boolean;
114
- requires_design_approval?: boolean;
115
- }
116
-
117
- interface WUCompletenessInput {
118
- id: string;
119
- status?: string;
120
- type?: string;
121
- notes?: string;
122
- tests?: {
123
- manual?: string[];
124
- };
125
- spec_refs?: unknown;
126
- }
127
-
128
- interface WUNormalizationInput {
129
- description?: unknown;
130
- code_paths?: unknown;
131
- acceptance?: unknown;
132
- }
133
-
134
- type EscalationDetectionInput = Pick<WUEscalationInput, 'lane' | 'code_paths' | 'priority'>;
135
-
136
- // =============================================================================
137
- // WU-1750: NORMALIZATION TRANSFORMS (Watertight YAML validation)
138
- // =============================================================================
139
-
140
- /**
141
- * Regex pattern matching embedded newlines (both literal and escaped)
142
- * Handles: "a\nb" (literal newline) and "a\\nb" (escaped backslash-n)
143
- */
144
- const NEWLINE_PATTERN = /\\n|\n/;
145
-
146
- /**
147
- * Transform: Normalize string arrays by splitting embedded newlines
148
- *
149
- * WU-1750: Agents sometimes pass multi-item content as single strings with \n.
150
- * This transform auto-repairs: ["a\nb\nc"] → ["a", "b", "c"]
151
- *
152
- * @example
153
- * // Input: ["tools/a.ts\ntools/b.js"]
154
- * // Output: ["tools/a.js", "tools/b.js"]
155
- */
156
- const normalizedStringArray = z.array(z.string()).transform((arr) =>
157
- arr
158
- .flatMap((s) => s.split(NEWLINE_PATTERN))
159
- .map((s) => s.trim())
160
- .filter(Boolean),
161
- );
162
-
163
- /**
164
- * Transform: Normalize description/notes strings by converting escaped newlines
165
- *
166
- * WU-1750: YAML quoted strings preserve literal \\n as two characters.
167
- * This transform converts them to actual newlines: "a\\n\\nb" → "a\n\nb"
168
- *
169
- * @example
170
- * // Input: "Problem:\\n\\n1. First issue"
171
- * // Output: "Problem:\n\n1. First issue"
172
- */
173
- const _normalizedMultilineString = z.string().transform((s) => s.replace(/\\n/g, '\n'));
174
-
175
- /**
176
- * Refinement: File path cannot contain newlines (post-normalization safety check)
177
- *
178
- * WU-1750: After normalization, paths should be clean. This catches UnsafeAny edge cases.
179
- */
180
- const filePathItem = z.string().refine((s) => !s.includes('\n') && !s.includes('\\n'), {
181
- message: 'File path cannot contain newlines - split into separate array items',
182
- });
183
-
184
- /**
185
- * Normalized code_paths: split embedded newlines + validate each path
186
- */
187
- const normalizedCodePaths = normalizedStringArray.pipe(z.array(filePathItem)).default([]);
188
-
189
- /**
190
- * Normalized test paths object: all test arrays normalized
191
- */
192
- const normalizedTestPaths = z
193
- .object({
194
- manual: normalizedStringArray.optional(),
195
- unit: normalizedStringArray.optional(),
196
- integration: normalizedStringArray.optional(),
197
- e2e: normalizedStringArray.optional(),
198
- })
199
- .optional();
200
-
201
- // =============================================================================
202
- // BASE FIELD DEFINITIONS (DRY - shared between BaseWUSchema and WUSchema)
203
- // =============================================================================
204
-
205
- /**
206
- * Base description field (structural validation only)
207
- * WU-1539: Fixed template string bug (single quotes → function message)
208
- * WU-1750: Added normalization of escaped newlines (\\n → actual newlines)
209
- */
210
- const baseDescriptionField = z
211
- .string()
212
- .min(1, 'Description is required')
213
- .transform((s) => s.replace(/\\n/g, '\n')) // WU-1750: Normalize escaped newlines
214
- .refine((val) => val.trim().length >= MIN_DESCRIPTION_LENGTH, {
215
- // WU-1539 fix: Use function message for dynamic interpolation
216
- message: `Description must be at least ${MIN_DESCRIPTION_LENGTH} characters`,
217
- });
218
-
219
- /**
220
- * Strict description field (with placeholder rejection)
221
- * Used by wu:claim and wu:done to ensure placeholders are filled
222
- * WU-1750: Added normalization of escaped newlines (\\n → actual newlines)
223
- */
224
- const strictDescriptionField = z
225
- .string()
226
- .min(1, 'Description is required')
227
- .transform((s) => s.replace(/\\n/g, '\n')) // WU-1750: Normalize escaped newlines
228
- .refine((val) => !val.includes(PLACEHOLDER_SENTINEL), {
229
- message: `Description cannot contain ${PLACEHOLDER_SENTINEL} marker`,
230
- })
231
- .refine((val) => val.trim().length >= MIN_DESCRIPTION_LENGTH, {
232
- // WU-1539 fix: Use function message for dynamic interpolation
233
- message: `Description must be at least ${MIN_DESCRIPTION_LENGTH} characters`,
234
- });
235
-
236
- /**
237
- * Recursive helper: Check all nested values for at least one item
238
- * Shared between base and strict acceptance schemas
239
- */
240
- const hasItems = (value: unknown): boolean => {
241
- if (Array.isArray(value)) {
242
- return value.length > 0;
243
- }
244
- if (typeof value === 'object' && value !== null) {
245
- return Object.values(value as Record<string, unknown>).some(hasItems);
246
- }
247
- return false;
248
- };
249
-
250
- /**
251
- * Recursive helper: Check all strings for PLACEHOLDER_SENTINEL
252
- * Used only by strict acceptance schema
253
- */
254
- const checkStringsForPlaceholder = (value: unknown): boolean => {
255
- if (typeof value === 'string') {
256
- return !value.includes(PLACEHOLDER_SENTINEL);
257
- }
258
- if (Array.isArray(value)) {
259
- return value.every(checkStringsForPlaceholder);
260
- }
261
- if (typeof value === 'object' && value !== null) {
262
- return Object.values(value).every(checkStringsForPlaceholder);
263
- }
264
- return true;
265
- };
266
-
267
- /**
268
- * Base acceptance field (structural validation only)
269
- * Validates format but allows placeholder markers
270
- * WU-1750: Added normalization of embedded newlines in array items
271
- */
272
- const baseAcceptanceField = z.union([
273
- // Flat array format (legacy): acceptance: ["item1", "item2"]
274
- // WU-1750: Normalize embedded newlines: ["1. a\n2. b"] → ["1. a", "2. b"]
275
- normalizedStringArray.pipe(z.array(z.string()).min(1, ACCEPTANCE_REQUIRED_MSG)),
276
- // Nested object format (structured): acceptance: { category1: ["item1"], category2: ["item2"] }
277
- z.record(z.string(), normalizedStringArray).refine((obj) => Object.values(obj).some(hasItems), {
278
- message: ACCEPTANCE_REQUIRED_MSG,
279
- }),
280
- ]);
281
-
282
- /**
283
- * Strict acceptance field (with placeholder rejection)
284
- * Used by wu:claim and wu:done to ensure placeholders are filled
285
- * WU-1750: Added normalization of embedded newlines in array items
286
- */
287
- const strictAcceptanceField = z.union([
288
- // Flat array format (legacy): acceptance: ["item1", "item2"]
289
- // WU-1750: Normalize embedded newlines: ["1. a\n2. b"] → ["1. a", "2. b"]
290
- normalizedStringArray
291
- .pipe(z.array(z.string()).min(1, ACCEPTANCE_REQUIRED_MSG))
292
- .refine((arr) => !arr.some((item) => item.includes(PLACEHOLDER_SENTINEL)), {
293
- message: `Acceptance criteria cannot contain ${PLACEHOLDER_SENTINEL} markers`,
294
- }),
295
- // Nested object format (structured): acceptance: { category1: ["item1"], category2: ["item2"] }
296
- z
297
- .record(z.string(), normalizedStringArray)
298
- .refine((obj) => Object.values(obj).some(hasItems), {
299
- message: ACCEPTANCE_REQUIRED_MSG,
300
- })
301
- .refine((obj) => checkStringsForPlaceholder(obj), {
302
- message: `Acceptance criteria cannot contain ${PLACEHOLDER_SENTINEL} markers`,
303
- }),
304
- ]);
305
-
306
- /**
307
- * Shared field definitions (same for both base and strict schemas)
308
- * DRY: Defined once, used in both schema variants
309
- */
310
- const sharedFields = {
311
- /** WU identifier (e.g., WU-1162) */
312
- id: z.string().regex(/^WU-\d+$/, 'ID must match pattern WU-XXX'),
313
-
314
- /** Short title describing the work */
315
- title: z.string().min(1, 'Title is required'),
316
-
317
- /** Lane assignment (parent or sub-lane) */
318
- lane: z.string().min(1, 'Lane is required'),
319
-
320
- /** Work type classification */
321
- type: z
322
- .enum(WU_TYPE_VALUES, {
323
- error: `Invalid type. Valid values: ${WU_TYPE_VALUES.join(', ')}`,
324
- })
325
- .default(WU_DEFAULTS.type as WUType),
326
-
327
- /** Current status in workflow */
328
- status: z
329
- .enum(VALID_STATUSES, {
330
- error: `Invalid status. Valid values: ${VALID_STATUSES.join(', ')}`,
331
- })
332
- .default(WU_DEFAULTS.status as (typeof VALID_STATUSES)[number]),
333
-
334
- /** Priority level */
335
- priority: z
336
- .enum(['P0', 'P1', 'P2', 'P3'] as const, {
337
- error: 'Invalid priority',
338
- })
339
- .default(WU_DEFAULTS.priority as 'P0' | 'P1' | 'P2' | 'P3'),
340
-
341
- /** Creation date (YYYY-MM-DD) */
342
- created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Created must be YYYY-MM-DD'),
343
-
344
- /** Files modified by this WU - WU-1750: Normalized to split embedded newlines */
345
- code_paths: normalizedCodePaths,
346
-
347
- /** Test specifications - WU-1750: All test arrays normalized */
348
- tests: normalizedTestPaths.default(WU_DEFAULTS.tests),
349
-
350
- /** Output artifacts (stamps, docs, etc.) - WU-1750: Normalized */
351
- artifacts: normalizedStringArray.optional().default(WU_DEFAULTS.artifacts),
352
-
353
- /** Upstream WU dependencies (informational, legacy field) - WU-1750: Normalized */
354
- dependencies: normalizedStringArray.optional().default(WU_DEFAULTS.dependencies),
355
-
356
- // === Initiative System Fields (WU-1246) ===
357
-
358
- /** Parent initiative reference (format: INIT-{number} or slug) */
359
- initiative: z.string().optional(),
360
-
361
- /** Phase number within parent initiative */
362
- phase: z.number().int().positive().optional(),
363
-
364
- /** WU IDs that this WU blocks (downstream dependencies) - WU-1750: Normalized + validated */
365
- blocks: normalizedStringArray
366
- .pipe(z.array(z.string().regex(/^WU-\d+$/, WU_ID_FORMAT_MESSAGE)))
367
- .optional(),
368
-
369
- /** WU IDs that block this WU (upstream dependencies) - WU-1750: Normalized + validated */
370
- blocked_by: normalizedStringArray
371
- .pipe(z.array(z.string().regex(/^WU-\d+$/, WU_ID_FORMAT_MESSAGE)))
372
- .optional(),
373
-
374
- /** Cross-cutting tags (orthogonal to initiative) - WU-1750: Normalized */
375
- labels: normalizedStringArray.optional(),
376
-
377
- // === End Initiative System Fields ===
378
-
379
- /**
380
- * WU-1683: First-class plan field, symmetric with initiative `related_plan`.
381
- * Set via wu:create --plan, wu:edit --plan, or plan:link --id WU-XXX.
382
- */
383
- plan: z.string().optional(),
384
-
385
- /**
386
- * WU-1833: References to plans, design docs, external specifications
387
- * WU-1834: Supports both flat string array AND nested object format for backwards compatibility
388
- *
389
- * Flat format (WU-1833+): ['docs/plans/WU-XXX-plan.md']
390
- * Nested format (legacy): [{file: 'docs/path.md', section: 'heading'}]
391
- * Mixed format allowed: ['path.md', {section: 'heading'}]
392
- * Bare object (WU-428): {file: 'docs/path.md', section: 'heading'}
393
- */
394
- spec_refs: z
395
- .union([
396
- // Single object format (WU-428 style): {file: '...', section: '...'}
397
- z.object({
398
- file: z.string().optional(),
399
- section: z.string(),
400
- }),
401
- // Array format (WU-1833+): strings, objects, or mixed
402
- z.array(
403
- z.union([
404
- z.string(), // Flat format: 'docs/path.md'
405
- z.object({
406
- // Nested format: {file: 'path', section: 'heading'}
407
- file: z.string().optional(),
408
- section: z.string(),
409
- }),
410
- ]),
411
- ),
412
- ])
413
- .optional(),
414
-
415
- /** Known risks or constraints - WU-1750: Normalized */
416
- risks: normalizedStringArray.optional().default(WU_DEFAULTS.risks),
417
-
418
- /**
419
- * Free-form notes - supports string or array (auto-converted to string)
420
- * WU-1750: Normalizes escaped newlines (\\n → actual newlines)
421
- */
422
- notes: z
423
- .union([
424
- z.string(),
425
- z.array(z.string()), // Legacy array format - will be converted
426
- ])
427
- .optional()
428
- .transform((val) => {
429
- // Convert array to newline-joined string (legacy format)
430
- if (Array.isArray(val)) {
431
- return val.filter((s) => s.trim().length > 0).join(STRING_LITERALS.NEWLINE);
432
- }
433
- // WU-1750: Normalize escaped newlines in string format
434
- if (typeof val === 'string') {
435
- return val.replace(/\\n/g, '\n');
436
- }
437
- return val ?? WU_DEFAULTS.notes;
438
- }),
439
-
440
- /** Requires human review before merge */
441
- requires_review: z.boolean().optional().default(WU_DEFAULTS.requires_review),
442
-
443
- /** Locked state (done WUs only) */
444
- locked: z.boolean().optional(),
445
-
446
- /** Completion date (done WUs only) - auto-normalized to ISO datetime */
447
- completed_at: z
448
- .string()
449
- .optional()
450
- .transform((val) => normalizeISODateTime(val)),
451
-
452
- /** Claimed mode (worktree/branch-only/worktree-pr/branch-pr) */
453
- claimed_mode: z.enum(['worktree', 'branch-only', 'worktree-pr', 'branch-pr']).optional(),
454
-
455
- /**
456
- * WU-1589: Canonical branch name for this WU claim.
457
- *
458
- * Set at claim time to record the actual branch used.
459
- * Used by defaultBranchFrom() as highest-priority source for branch resolution.
460
- * Essential for branch-pr mode where cloud agents may use non-lane-derived branch names.
461
- * Cleared on rollback/release/recover when resetting to ready.
462
- */
463
- claimed_branch: z.string().optional(),
464
-
465
- /** Assigned agent email */
466
- assigned_to: z.string().email().optional(),
467
-
468
- /** Claim timestamp - auto-normalized to ISO datetime */
469
- claimed_at: z
470
- .string()
471
- .optional()
472
- .transform((val) => normalizeISODateTime(val)),
473
-
474
- /** Block reason (blocked WUs only) */
475
- blocked_reason: z.string().optional(),
476
-
477
- /** Worktree path (claimed WUs only) */
478
- worktree_path: z.string().optional(),
479
-
480
- /** Current active session ID (WU-1438: auto-set on claim, cleared on done) */
481
- session_id: z.string().uuid().optional(),
482
-
483
- /** Agent sessions (issue logging metadata, WU-1231) */
484
- agent_sessions: z
485
- .array(
486
- z.object({
487
- session_id: z.string().uuid(),
488
- started: z.string().datetime(),
489
- completed: z.string().datetime().optional(),
490
- agent_type: z.enum([
491
- 'claude-code',
492
- 'codex-cli',
493
- 'cursor',
494
- 'gemini-cli',
495
- 'windsurf',
496
- 'copilot',
497
- 'other',
498
- ]),
499
- context_tier: z.union([z.literal(1), z.literal(2), z.literal(3)]),
500
- incidents_logged: z.number().int().min(0).default(0),
501
- incidents_major: z.number().int().min(0).default(0),
502
- artifacts: z.array(z.string()).optional(),
503
- }),
504
- )
505
- .optional(),
506
-
507
- // === Exposure System Fields (WU-1998) ===
508
-
509
- /**
510
- * WU-1998: Exposure level - defines how the WU exposes functionality to users
511
- *
512
- * Valid values:
513
- * - 'ui': User-facing UI changes (pages, components, widgets)
514
- * - 'api': API endpoints called by UI or external clients
515
- * - 'backend-only': Backend-only changes (no user visibility)
516
- * - 'documentation': Documentation changes only
517
- *
518
- * Optional during transition period, will become required after backlog update.
519
- */
520
- exposure: z
521
- .enum(WU_EXPOSURE_VALUES, {
522
- error: `Invalid exposure value. Valid values: ${WU_EXPOSURE_VALUES.join(', ')}`,
523
- })
524
- .optional(),
525
-
526
- /**
527
- * WU-1998: User journey description for user-facing WUs
528
- *
529
- * Recommended for exposure: 'ui' and 'api'.
530
- * Describes the end-user interaction flow affected by this WU.
531
- */
532
- user_journey: z.string().optional(),
533
-
534
- /**
535
- * WU-1998: Related UI WUs for backend/API changes
536
- *
537
- * For WUs with exposure: 'api', this field lists UI WUs that consume the API.
538
- * Ensures backend features have corresponding UI coverage.
539
- * Each entry must match WU-XXX format.
540
- */
541
- ui_pairing_wus: normalizedStringArray
542
- .pipe(z.array(z.string().regex(/^WU-\d+$/, WU_ID_FORMAT_MESSAGE)))
543
- .optional(),
544
-
545
- /**
546
- * WU-2022: Navigation path for UI-exposed features
547
- *
548
- * For WUs with exposure: 'ui', specifies the route where the feature is accessible.
549
- * Used by wu:done to verify that UI features are actually navigable.
550
- * Prevents "orphaned code" where features exist but users cannot access them.
551
- *
552
- * Example: '/dashboard', '/settings/preferences', '/space'
553
- */
554
- navigation_path: z.string().optional(),
555
-
556
- // === End Exposure System Fields ===
557
-
558
- // === Sizing Estimate Fields (WU-2141) ===
559
-
560
- /**
561
- * WU-2141: Optional sizing estimate metadata.
562
- *
563
- * Records expected WU complexity for tooling-backed sizing enforcement.
564
- * Absent for historical WUs (backward compatible).
565
- *
566
- * Fields:
567
- * - estimated_files: Expected number of files to modify
568
- * - estimated_tool_calls: Expected tool call count
569
- * - strategy: Execution strategy from wu-sizing-guide.md
570
- * - exception_type: Override type when thresholds intentionally exceeded
571
- * - exception_reason: Justification for the exception (required with exception_type)
572
- */
573
- sizing_estimate: z
574
- .object({
575
- estimated_files: z.number().int().min(0),
576
- estimated_tool_calls: z.number().int().min(0),
577
- strategy: z.enum([
578
- 'single-session',
579
- 'checkpoint-resume',
580
- 'orchestrator-worker',
581
- 'decomposition',
582
- ]),
583
- exception_type: z.enum(['docs-only', 'shallow-multi-file']).optional(),
584
- exception_reason: z.string().optional(),
585
- })
586
- .refine(
587
- (data) => {
588
- if (data.exception_type !== undefined) {
589
- return data.exception_reason !== undefined && data.exception_reason.trim().length > 0;
590
- }
591
- return true;
592
- },
593
- {
594
- message:
595
- 'sizing_estimate.exception_reason is required and must be non-empty when exception_type is set',
596
- path: ['exception_reason'],
597
- },
598
- )
599
- .optional(),
600
-
601
- // === End Sizing Estimate Fields ===
602
-
603
- // === Agent-First Approval Fields (WU-2079 → WU-2080) ===
604
-
605
- /**
606
- * WU-2080: Escalation triggers detected for this WU
607
- *
608
- * Agent-first model: agents auto-approve by default.
609
- * Human escalation only when these triggers are detected:
610
- * - sensitive_data: Changes to sensitive data handling
611
- * - security_p0: P0 security incident or vulnerability
612
- * - budget: Budget/resource allocation above threshold
613
- * - cross_lane_arch: Cross-lane architectural decision
614
- *
615
- * Empty array = no escalation needed, agent proceeds autonomously.
616
- */
617
- escalation_triggers: z
618
- .array(z.enum(['sensitive_data', 'security_p0', 'budget', 'cross_lane_arch']))
619
- .optional()
620
- .default([]),
621
-
622
- /**
623
- * WU-2080: Human escalation required flag
624
- *
625
- * Auto-set to true when escalation_triggers is non-empty.
626
- * When true, wu:done requires human confirmation before completion.
627
- */
628
- requires_human_escalation: z.boolean().optional().default(false),
629
-
630
- /**
631
- * WU-2080: Email(s) of approvers who signed off
632
- *
633
- * Auto-populated with claiming agent at wu:claim.
634
- * Additional human approvers added when escalation is resolved.
635
- */
636
- approved_by: z.array(z.string().email()).optional(),
637
-
638
- /**
639
- * WU-2080: Timestamp when approval was granted
640
- *
641
- * Auto-set at wu:claim for agent auto-approval.
642
- * Updated when human escalation is resolved.
643
- */
644
- approved_at: z
645
- .string()
646
- .optional()
647
- .transform((val) => normalizeISODateTime(val)),
648
-
649
- /**
650
- * WU-2080: Human who resolved escalation (if UnsafeAny)
651
- *
652
- * Only set when requires_human_escalation was true and resolved.
653
- */
654
- escalation_resolved_by: z.string().email().optional(),
655
-
656
- /**
657
- * WU-2080: Timestamp when human resolved escalation
658
- */
659
- escalation_resolved_at: z
660
- .string()
661
- .optional()
662
- .transform((val) => normalizeISODateTime(val)),
663
-
664
- // Legacy fields (deprecated, kept for backwards compatibility)
665
- /** @deprecated Use escalation_triggers instead */
666
- requires_cso_approval: z.boolean().optional().default(false),
667
- /** @deprecated Use escalation_triggers instead */
668
- requires_cto_approval: z.boolean().optional().default(false),
669
- /** @deprecated Use escalation_triggers instead */
670
- requires_design_approval: z.boolean().optional().default(false),
671
-
672
- // === End Agent-First Approval Fields ===
673
- };
674
-
675
- // =============================================================================
676
- // SCHEMA DEFINITIONS
677
- // =============================================================================
678
-
679
- /**
680
- * Base WU Schema (structural validation only)
681
- *
682
- * WU-1539: Used by wu:create and wu:edit for fail-fast structural validation.
683
- * Allows placeholder markers - only checks field types, formats, and lengths.
684
- *
685
- * Use case: Validate WU structure at creation/edit time before placeholders are filled.
686
- */
687
- export const BaseWUSchema = z.object({
688
- ...sharedFields,
689
- description: baseDescriptionField,
690
- acceptance: baseAcceptanceField,
691
- });
692
-
693
- /**
694
- * Ready WU Schema (alias for BaseWUSchema)
695
- *
696
- * WU-1539: Semantic alias for clarity in wu:create and wu:edit.
697
- * Same validation as BaseWUSchema - allows placeholders, enforces structure.
698
- */
699
- export const ReadyWUSchema = BaseWUSchema;
700
-
701
- /**
702
- * Strict WU Schema (structural + placeholder rejection)
703
- *
704
- * Validates WU files against LumenFlow requirements:
705
- * - No placeholder text in done WUs
706
- * - Minimum description length (50 chars)
707
- * - Code paths present for non-documentation WUs
708
- * - Proper status/lane/type enums
709
- *
710
- * Used by wu:claim and wu:done to ensure specs are complete.
711
- * Provides runtime validation and TypeScript type inference.
712
- */
713
- export const WUSchema = z.object({
714
- ...sharedFields,
715
- description: strictDescriptionField,
716
- acceptance: strictAcceptanceField,
717
- });
718
-
719
- /**
720
- * TypeScript type inferred from schema
721
- *
722
- * Single source of truth for both runtime validation and compile-time types.
723
- * Replaces manual WU interfaces (DRY principle).
724
- *
725
- * Note: Type inference available in TypeScript via z.infer<typeof WUSchema>
726
- * This is a JavaScript file, so the type export is not needed here.
727
- *
728
- * @typedef {import('zod').z.infer<typeof WUSchema>} WU
729
- */
730
-
731
- /**
732
- * Validates WU data against strict schema (placeholder rejection)
733
- *
734
- * Used by wu:claim and wu:done to ensure specs are complete.
735
- * Rejects WUs with placeholder markers.
736
- *
737
- * @param {unknown} data - Parsed YAML data to validate
738
- * @returns {z.SafeParseReturnType<WU, WU>} Validation result
739
- *
740
- * @example
741
- * const result = validateWU(yamlData);
742
- * if (!result.success) {
743
- * result.error.issues.forEach(issue => {
744
- * console.error(`${issue.path.join('.')}: ${issue.message}`);
745
- * });
746
- * }
747
- */
748
- export function validateWU(data: unknown) {
749
- return WUSchema.safeParse(data);
750
- }
751
-
752
- /**
753
- * Validates WU data against base schema (structural only)
754
- *
755
- * WU-1539: Used by wu:create and wu:edit for fail-fast structural validation.
756
- * Allows placeholder markers - only checks field types, formats, and lengths.
757
- *
758
- * @param {unknown} data - Parsed YAML data to validate
759
- * @returns {z.SafeParseReturnType<WU, WU>} Validation result
760
- *
761
- * @example
762
- * const result = validateReadyWU(yamlData);
763
- * if (!result.success) {
764
- * const errors = result.error.issues
765
- * .map(issue => ` • ${issue.path.join('.')}: ${issue.message}`)
766
- * .join('\n');
767
- * die(`WU YAML validation failed:\n\n${errors}`);
768
- * }
769
- */
770
- export function validateReadyWU(data: unknown) {
771
- return ReadyWUSchema.safeParse(data);
772
- }
773
-
774
- /**
775
- * Validates WU spec completeness for done status
776
- *
777
- * Additional validation beyond schema for WUs marked as done:
778
- * - Code paths required for non-documentation WUs
779
- * - Locked must be true
780
- * - Completed timestamp must be present
781
- *
782
- * @param {WU} wu - Validated WU data
783
- * @returns {{valid: boolean, errors: string[]}} Validation result
784
- *
785
- * @example
786
- * const schemaResult = validateWU(data);
787
- * if (schemaResult.success && data.status === 'done') {
788
- * const completenessResult = validateDoneWU(schemaResult.data);
789
- * if (!completenessResult.valid) {
790
- * console.error(completenessResult.errors);
791
- * }
792
- * }
793
- */
794
- export function validateDoneWU(wu: WUDoneValidationInput): { valid: boolean; errors: string[] } {
795
- const errors: string[] = [];
796
-
797
- // Check code_paths for non-documentation WUs
798
- if (!isDocsOrProcessType(wu.type)) {
799
- if (!wu.code_paths || wu.code_paths.length === 0) {
800
- errors.push('Code paths required for non-documentation WUs');
801
- }
802
- }
803
-
804
- // Note: locked and completed_at are set automatically by wu:done
805
- // No need to validate them here (they don't exist yet at validation time)
806
-
807
- return {
808
- valid: errors.length === 0,
809
- errors,
810
- };
811
- }
812
-
813
- /**
814
- * WU-2080: Valid escalation trigger types
815
- *
816
- * These are the only conditions that require human intervention.
817
- * Everything else is auto-approved by agents.
818
- */
819
- export const ESCALATION_TRIGGER_TYPES = [
820
- 'sensitive_data', // Sensitive data handling changes
821
- 'security_p0', // P0 security incident
822
- 'budget', // Budget/resource above threshold
823
- 'cross_lane_arch', // Cross-lane architectural decision
824
- ];
825
-
826
- /**
827
- * WU-2080: Agent-first approval validation
828
- *
829
- * AGENT-FIRST MODEL: Agents auto-approve by default.
830
- * Human escalation only when escalation_triggers is non-empty
831
- * AND requires_human_escalation is true AND not yet resolved.
832
- *
833
- * Returns:
834
- * - valid: true if agent can proceed (no unresolved escalation)
835
- * - errors: blocking issues requiring human resolution
836
- * - warnings: advisory messages (non-blocking)
837
- *
838
- * @param {object} wu - Validated WU data
839
- * @returns {{valid: boolean, errors: string[], warnings: string[]}}
840
- */
841
- export function validateApprovalGates(wu: WUEscalationInput): {
842
- valid: boolean;
843
- errors: string[];
844
- warnings: string[];
845
- } {
846
- const errors: string[] = [];
847
- const warnings: string[] = [];
848
-
849
- // Agent-first: check for unresolved escalation triggers
850
- const triggers = wu.escalation_triggers || [];
851
- const requiresEscalation = wu.requires_human_escalation || triggers.length > 0;
852
-
853
- if (requiresEscalation) {
854
- // Check if escalation was resolved by human
855
- const resolved = wu.escalation_resolved_by && wu.escalation_resolved_at;
856
-
857
- if (!resolved) {
858
- errors.push(
859
- `Human escalation required for: ${triggers.join(', ')}\n` +
860
- ` To resolve: pnpm wu:escalate --resolve --id ${wu.id}`,
861
- );
862
- }
863
- }
864
-
865
- // Legacy backwards compatibility: map old fields to new model
866
- if (wu.requires_cso_approval || wu.requires_cto_approval || wu.requires_design_approval) {
867
- warnings.push(
868
- 'Using deprecated requires_X_approval fields. Migrate to escalation_triggers model.',
869
- );
870
- }
871
-
872
- return {
873
- valid: errors.length === 0,
874
- errors,
875
- warnings,
876
- };
877
- }
878
-
879
- /**
880
- * WU-2080: Detect escalation triggers from WU content
881
- *
882
- * Analyzes WU metadata to detect conditions requiring human escalation.
883
- * Called by wu:claim to auto-set escalation_triggers.
884
- *
885
- * @param {object} wu - WU data with lane, type, code_paths
886
- * @returns {string[]} Array of triggered escalation types
887
- */
888
- export function detectEscalationTriggers(wu: EscalationDetectionInput): string[] {
889
- const triggers: string[] = [];
890
- const lane = (wu.lane || '').toLowerCase();
891
- const codePaths = wu.code_paths || [];
892
-
893
- // Sensitive data: Changes to user data or auth
894
- const sensitivePatterns = ['pii', 'user-data', 'auth', 'credentials'];
895
- const touchesSensitive = codePaths.some((p) =>
896
- sensitivePatterns.some((pat) => p.toLowerCase().includes(pat)),
897
- );
898
- if (touchesSensitive || lane.includes('pii')) {
899
- triggers.push('sensitive_data');
900
- }
901
-
902
- // Security P0: Explicit security lane or auth changes
903
- if (wu.priority === 'P0' && lane.includes('security')) {
904
- triggers.push('security_p0');
905
- }
906
-
907
- return triggers;
908
- }
909
-
910
- /**
911
- * WU-2080: Generate auto-approval metadata for wu:claim
912
- *
913
- * Called by wu:claim to auto-approve agents within policy.
914
- * Sets approved_by and approved_at, detects escalation triggers.
915
- *
916
- * @param {object} wu - WU data
917
- * @param {string} agentEmail - Email of claiming agent
918
- * @returns {{approved_by: string[], approved_at: string, escalation_triggers: string[], requires_human_escalation: boolean}}
919
- */
920
- export function generateAutoApproval(
921
- wu: EscalationDetectionInput,
922
- agentEmail: string,
923
- ): {
924
- approved_by: string[];
925
- approved_at: string;
926
- escalation_triggers: string[];
927
- requires_human_escalation: boolean;
928
- } {
929
- const triggers = detectEscalationTriggers(wu);
930
- const now = new Date().toISOString();
931
-
932
- return {
933
- approved_by: [agentEmail],
934
- approved_at: now,
935
- escalation_triggers: triggers,
936
- requires_human_escalation: triggers.length > 0,
937
- };
938
- }
939
-
940
- /**
941
- * @deprecated Use detectEscalationTriggers instead
942
- * WU-2079: Legacy function for backwards compatibility
943
- */
944
- export function determineRequiredApprovals(wu: EscalationDetectionInput): {
945
- requires_cso_approval: boolean;
946
- requires_cto_approval: boolean;
947
- requires_design_approval: boolean;
948
- } {
949
- const triggers = detectEscalationTriggers(wu);
950
- return {
951
- requires_cso_approval: triggers.includes('security_p0') || triggers.includes('sensitive_data'),
952
- requires_cto_approval: triggers.includes('cross_lane_arch'),
953
- requires_design_approval: false, // Design no longer requires human escalation
954
- };
955
- }
956
-
957
- /**
958
- * WU-1811: Validates and normalizes WU YAML data with auto-fixable normalisations
959
- *
960
- * This function validates the WU YAML schema and applies fixable normalisations:
961
- * - Trimming whitespace from string fields
962
- * - Normalizing escaped newlines (\\n → \n)
963
- * - Splitting embedded newlines in arrays (["a\nb"] → ["a", "b"])
964
- *
965
- * Returns:
966
- * - valid: true if schema validation passes (after normalisations)
967
- * - normalized: the normalized data (even if validation fails, partial normalization is returned)
968
- * - errors: validation errors if UnsafeAny
969
- * - wasNormalized: true if UnsafeAny normalisations were applied
970
- *
971
- * @param {unknown} data - Parsed YAML data to validate and normalize
972
- * @returns {{valid: boolean, normalized: object|null, errors: string[], wasNormalized: boolean}}
973
- *
974
- * @example
975
- * const { valid, normalized, errors, wasNormalized } = validateAndNormalizeWUYAML(yamlData);
976
- * if (valid && wasNormalized) {
977
- * // Write normalized data back to YAML file
978
- * writeWU(wuPath, normalized);
979
- * }
980
- * if (!valid) {
981
- * die(`Validation failed:\n${errors.join('\n')}`);
982
- * }
983
- */
984
- export function validateAndNormalizeWUYAML(data: unknown): {
985
- valid: boolean;
986
- normalized: z.infer<typeof WUSchema> | null;
987
- errors: string[];
988
- wasNormalized: boolean;
989
- } {
990
- // First try to parse with schema (which applies normalizations)
991
- const result = WUSchema.safeParse(data);
992
-
993
- if (!result.success) {
994
- // Schema validation failed - return errors
995
- const errors = result.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
996
- return {
997
- valid: false,
998
- normalized: null,
999
- errors,
1000
- wasNormalized: false,
1001
- };
1002
- }
1003
-
1004
- // Schema passed - check if data was normalized (compare key fields)
1005
- const normalized = result.data;
1006
- const original = typeof data === 'object' && data !== null ? (data as WUNormalizationInput) : {};
1007
- const wasNormalized = detectNormalizationChanges(original, normalized);
1008
-
1009
- return {
1010
- valid: true,
1011
- normalized,
1012
- errors: [],
1013
- wasNormalized,
1014
- };
1015
- }
1016
-
1017
- /**
1018
- * WU-1833: Validate WU spec completeness with advisory warnings
1019
- *
1020
- * Provides soft validation that warns (doesn't fail) when recommended fields are missing.
1021
- * Used by wu:validate command to surface quality issues without blocking workflow.
1022
- *
1023
- * Feature and bug WUs should have:
1024
- * - notes (implementation context, deployment instructions)
1025
- * - tests.manual (verification steps)
1026
- * - spec_refs (links to plans, design docs) - for features only
1027
- *
1028
- * @param {object} wu - Validated WU data (must pass WUSchema first)
1029
- * @returns {{warnings: string[]}} Array of warning messages
1030
- *
1031
- * @example
1032
- * const schemaResult = validateWU(data);
1033
- * if (schemaResult.success) {
1034
- * const { warnings } = validateWUCompleteness(schemaResult.data);
1035
- * if (warnings.length > 0) {
1036
- * console.warn('Quality warnings:');
1037
- * warnings.forEach(w => console.warn(` ⚠️ ${w}`));
1038
- * }
1039
- * }
1040
- */
1041
- export function validateWUCompleteness(wu: WUCompletenessInput): { warnings: string[] } {
1042
- const warnings: string[] = [];
1043
-
1044
- // WU-1384: Skip completeness checks for terminal WUs (done, cancelled, etc.)
1045
- // These are immutable historical records - enforcing completeness is pointless
1046
- const status = wu.status ?? '';
1047
- const isTerminal = WU_STATUS_GROUPS.TERMINAL.includes(status);
1048
- if (isTerminal) {
1049
- return { warnings };
1050
- }
1051
-
1052
- const type = isWUType(wu.type) ? wu.type : WU_TYPES.FEATURE;
1053
-
1054
- // Only feature, bug, and refactor WUs require completeness context.
1055
- const requiresContext = CONTEXT_REQUIRED_TYPES.includes(type);
1056
-
1057
- if (!requiresContext) {
1058
- return { warnings };
1059
- }
1060
-
1061
- // Check for notes (implementation context)
1062
- if (!wu.notes || wu.notes.trim().length === 0) {
1063
- warnings.push(
1064
- `${wu.id}: Missing 'notes' field. Add implementation context, deployment instructions, or plan links.`,
1065
- );
1066
- }
1067
-
1068
- // Check for manual tests
1069
- const hasManualTests = wu.tests?.manual && wu.tests.manual.length > 0;
1070
- if (!hasManualTests) {
1071
- warnings.push(
1072
- `${wu.id}: Missing 'tests.manual' field. Add manual verification steps for acceptance criteria.`,
1073
- );
1074
- }
1075
-
1076
- // Check for spec_refs (features should link to plans/specs)
1077
- // WU-1062: Accepts both repo-relative paths (<configured plansDir>/) and
1078
- // external paths (~/.lumenflow/plans/, $LUMENFLOW_HOME/plans/, lumenflow://plans/)
1079
- if (type === 'feature') {
1080
- const specRefs = wu.spec_refs as { length?: number } | undefined;
1081
- const hasSpecRefs = !!specRefs && (specRefs.length ?? 0) > 0;
1082
- if (!hasSpecRefs) {
1083
- const plansDirHint = `${createWuPaths().PLANS_DIR().replace(/\/+$/, '')}/`;
1084
- warnings.push(
1085
- `${wu.id}: Missing 'spec_refs' field. Link to plan file (${plansDirHint}, lumenflow://plans/, or ~/.lumenflow/plans/) for traceability.`,
1086
- );
1087
- }
1088
- }
1089
-
1090
- return { warnings };
1091
- }
1092
-
1093
- /**
1094
- * WU-1811: Detect if normalizations were applied by comparing original and normalized data
1095
- *
1096
- * Compares fields that are commonly normalized:
1097
- * - description (escaped newlines)
1098
- * - code_paths (embedded newlines split)
1099
- * - acceptance (embedded newlines split)
1100
- *
1101
- * @param {object} original - Original parsed YAML data
1102
- * @param {object} normalized - Schema-normalized data
1103
- * @returns {boolean} True if UnsafeAny normalisations were applied
1104
- */
1105
- function detectNormalizationChanges(
1106
- original: WUNormalizationInput,
1107
- normalized: WUNormalizationInput,
1108
- ): boolean {
1109
- // Compare description (newline normalization)
1110
- if (original.description !== normalized.description) {
1111
- return true;
1112
- }
1113
-
1114
- // Compare code_paths (array splitting)
1115
- const origPaths = Array.isArray(original.code_paths) ? original.code_paths : [];
1116
- const normPaths = Array.isArray(normalized.code_paths) ? normalized.code_paths : [];
1117
- if (origPaths.length !== normPaths.length) {
1118
- return true;
1119
- }
1120
- for (let i = 0; i < origPaths.length; i++) {
1121
- if (origPaths[i] !== normPaths[i]) {
1122
- return true;
1123
- }
1124
- }
1125
-
1126
- // Compare acceptance if both are arrays (most common case)
1127
- if (Array.isArray(original.acceptance) && Array.isArray(normalized.acceptance)) {
1128
- if (original.acceptance.length !== normalized.acceptance.length) {
1129
- return true;
1130
- }
1131
- for (let i = 0; i < original.acceptance.length; i++) {
1132
- if (original.acceptance[i] !== normalized.acceptance[i]) {
1133
- return true;
1134
- }
1135
- }
1136
- }
1137
-
1138
- return false;
1139
- }