@lumenflow/core 1.0.0 → 1.3.2

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 (79) hide show
  1. package/dist/arg-parser.d.ts +6 -0
  2. package/dist/arg-parser.js +57 -1
  3. package/dist/backlog-generator.js +1 -1
  4. package/dist/backlog-sync-validator.js +3 -3
  5. package/dist/branch-check.d.ts +21 -0
  6. package/dist/branch-check.js +77 -0
  7. package/dist/cli/is-agent-branch.d.ts +11 -0
  8. package/dist/cli/is-agent-branch.js +15 -0
  9. package/dist/code-paths-overlap.js +2 -2
  10. package/dist/error-handler.d.ts +1 -0
  11. package/dist/error-handler.js +4 -1
  12. package/dist/git-adapter.d.ts +23 -0
  13. package/dist/git-adapter.js +38 -2
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +5 -0
  16. package/dist/lane-checker.d.ts +36 -3
  17. package/dist/lane-checker.js +128 -17
  18. package/dist/lane-inference.js +3 -4
  19. package/dist/lumenflow-config-schema.d.ts +125 -0
  20. package/dist/lumenflow-config-schema.js +76 -0
  21. package/dist/lumenflow-home.d.ts +130 -0
  22. package/dist/lumenflow-home.js +208 -0
  23. package/dist/manual-test-validator.js +1 -1
  24. package/dist/orchestration-rules.d.ts +1 -1
  25. package/dist/orchestration-rules.js +2 -2
  26. package/dist/orphan-detector.d.ts +16 -0
  27. package/dist/orphan-detector.js +24 -0
  28. package/dist/path-classifiers.d.ts +1 -1
  29. package/dist/path-classifiers.js +1 -1
  30. package/dist/rebase-artifact-cleanup.d.ts +17 -0
  31. package/dist/rebase-artifact-cleanup.js +49 -8
  32. package/dist/spawn-strategy.d.ts +53 -0
  33. package/dist/spawn-strategy.js +106 -0
  34. package/dist/spec-branch-helpers.d.ts +118 -0
  35. package/dist/spec-branch-helpers.js +192 -0
  36. package/dist/stamp-utils.d.ts +10 -0
  37. package/dist/stamp-utils.js +17 -19
  38. package/dist/token-counter.js +2 -2
  39. package/dist/wu-consistency-checker.d.ts +2 -0
  40. package/dist/wu-consistency-checker.js +40 -6
  41. package/dist/wu-constants.d.ts +98 -3
  42. package/dist/wu-constants.js +108 -3
  43. package/dist/wu-create-validators.d.ts +40 -2
  44. package/dist/wu-create-validators.js +76 -2
  45. package/dist/wu-done-branch-only.js +9 -0
  46. package/dist/wu-done-branch-utils.d.ts +10 -0
  47. package/dist/wu-done-branch-utils.js +31 -0
  48. package/dist/wu-done-cleanup.d.ts +8 -0
  49. package/dist/wu-done-cleanup.js +122 -0
  50. package/dist/wu-done-docs-generate.d.ts +73 -0
  51. package/dist/wu-done-docs-generate.js +108 -0
  52. package/dist/wu-done-docs-only.d.ts +20 -0
  53. package/dist/wu-done-docs-only.js +65 -0
  54. package/dist/wu-done-errors.d.ts +17 -0
  55. package/dist/wu-done-errors.js +24 -0
  56. package/dist/wu-done-inputs.d.ts +12 -0
  57. package/dist/wu-done-inputs.js +51 -0
  58. package/dist/wu-done-metadata.d.ts +100 -0
  59. package/dist/wu-done-metadata.js +193 -0
  60. package/dist/wu-done-paths.d.ts +69 -0
  61. package/dist/wu-done-paths.js +237 -0
  62. package/dist/wu-done-preflight.d.ts +48 -0
  63. package/dist/wu-done-preflight.js +185 -0
  64. package/dist/wu-done-validation.d.ts +82 -0
  65. package/dist/wu-done-validation.js +340 -0
  66. package/dist/wu-done-validators.d.ts +13 -409
  67. package/dist/wu-done-validators.js +9 -1225
  68. package/dist/wu-done-worktree.d.ts +0 -1
  69. package/dist/wu-done-worktree.js +24 -30
  70. package/dist/wu-schema.js +4 -4
  71. package/dist/wu-spawn-skills.d.ts +19 -0
  72. package/dist/wu-spawn-skills.js +148 -0
  73. package/dist/wu-spawn.d.ts +17 -4
  74. package/dist/wu-spawn.js +113 -177
  75. package/dist/wu-validation.d.ts +1 -0
  76. package/dist/wu-validation.js +21 -1
  77. package/dist/wu-validator.d.ts +51 -0
  78. package/dist/wu-validator.js +108 -0
  79. package/package.json +12 -8
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { existsSync, readFileSync } from 'node:fs';
9
9
  import path from 'node:path';
10
- import yaml from 'js-yaml';
10
+ import { parseYAML } from './wu-yaml.js';
11
11
  import { getSubLanesForParent } from './lane-inference.js';
12
12
  import { createError, ErrorCodes } from './error-handler.js';
13
13
  import { isInProgressHeader, WU_LINK_PATTERN } from './constants/backlog-patterns.js';
@@ -48,7 +48,7 @@ function hasSubLaneTaxonomy(parent) {
48
48
  }
49
49
  try {
50
50
  const taxonomyContent = readFileSync(taxonomyPath, { encoding: 'utf-8' });
51
- const taxonomy = yaml.load(taxonomyContent);
51
+ const taxonomy = parseYAML(taxonomyContent);
52
52
  // Check if parent exists as top-level key in taxonomy
53
53
  const normalizedParent = parent.trim();
54
54
  return Object.keys(taxonomy).some((key) => key.toLowerCase().trim() === normalizedParent.toLowerCase());
@@ -71,7 +71,7 @@ function isValidSubLane(parent, subdomain) {
71
71
  }
72
72
  try {
73
73
  const taxonomyContent = readFileSync(taxonomyPath, { encoding: 'utf-8' });
74
- const taxonomy = yaml.load(taxonomyContent);
74
+ const taxonomy = parseYAML(taxonomyContent);
75
75
  // Find parent key (case-insensitive)
76
76
  const normalizedParent = parent.trim().toLowerCase();
77
77
  const parentKey = Object.keys(taxonomy).find((key) => key.toLowerCase().trim() === normalizedParent);
@@ -188,6 +188,10 @@ export function validateLaneFormat(lane, configPath = null, options = {}) {
188
188
  }
189
189
  /**
190
190
  * Check if a parent lane exists in LumenFlow config
191
+ *
192
+ * WU-1022: Updated to support lanes.definitions with full "Parent: Sublane" format.
193
+ * When lanes.definitions exists, parent lanes are extracted from full lane names.
194
+ *
191
195
  * @param {string} parent - Parent lane name to check
192
196
  * @param {string} configPath - Path to config file (optional)
193
197
  * @returns {boolean} True if parent lane exists
@@ -206,16 +210,26 @@ function isValidParentLane(parent, configPath = null) {
206
210
  });
207
211
  }
208
212
  const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
209
- const config = yaml.load(configContent);
210
- // Extract all lane names - handle both flat array and nested engineering/business formats
213
+ const config = parseYAML(configContent);
214
+ // Extract all lane names - handle multiple config formats
211
215
  const allLanes = [];
216
+ const parentLanes = new Set();
212
217
  if (config.lanes) {
213
218
  if (Array.isArray(config.lanes)) {
214
219
  // Flat array format: lanes: [{name: "Core"}, {name: "CLI"}, ...]
215
220
  allLanes.push(...config.lanes.map((l) => l.name));
216
221
  }
217
222
  else {
218
- // Nested format: lanes: {engineering: [...], business: [...]}
223
+ // WU-1022: New format with lanes.definitions containing full "Parent: Sublane" names
224
+ if (config.lanes.definitions) {
225
+ for (const lane of config.lanes.definitions) {
226
+ allLanes.push(lane.name);
227
+ // Extract parent from full lane name for parent validation
228
+ const extractedParent = extractParent(lane.name);
229
+ parentLanes.add(extractedParent.toLowerCase().trim());
230
+ }
231
+ }
232
+ // Legacy nested format: lanes: {engineering: [...], business: [...]}
219
233
  if (config.lanes.engineering) {
220
234
  allLanes.push(...config.lanes.engineering.map((l) => l.name));
221
235
  }
@@ -226,16 +240,86 @@ function isValidParentLane(parent, configPath = null) {
226
240
  }
227
241
  // Case-insensitive comparison
228
242
  const normalizedParent = parent.toLowerCase().trim();
243
+ // WU-1022: If we have extracted parent lanes (from full lane names), check against those
244
+ if (parentLanes.size > 0) {
245
+ return parentLanes.has(normalizedParent);
246
+ }
247
+ // Legacy: check against direct lane names
229
248
  return allLanes.some((lane) => lane.toLowerCase().trim() === normalizedParent);
230
249
  }
250
+ /** WU-1016: Default WIP limit when not specified in config */
251
+ const DEFAULT_WIP_LIMIT = 1;
231
252
  /**
232
- * Check if a lane is free (no in_progress WU currently in that lane)
253
+ * WU-1016: Get WIP limit for a lane from config
254
+ *
255
+ * Reads the wip_limit field from .lumenflow.config.yaml for the specified lane.
256
+ * Returns DEFAULT_WIP_LIMIT (1) if the lane is not found or wip_limit is not specified.
257
+ *
258
+ * @param {string} lane - Lane name (e.g., "Core", "CLI")
259
+ * @param {GetWipLimitOptions} options - Options including configPath for testing
260
+ * @returns {number} The WIP limit for the lane (default: 1)
261
+ */
262
+ export function getWipLimitForLane(lane, options = {}) {
263
+ // Determine config path
264
+ let resolvedConfigPath = options.configPath;
265
+ if (!resolvedConfigPath) {
266
+ const projectRoot = findProjectRoot();
267
+ resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
268
+ }
269
+ // Check if config file exists
270
+ if (!existsSync(resolvedConfigPath)) {
271
+ return DEFAULT_WIP_LIMIT;
272
+ }
273
+ try {
274
+ const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
275
+ const config = parseYAML(configContent);
276
+ if (!config.lanes) {
277
+ return DEFAULT_WIP_LIMIT;
278
+ }
279
+ // Normalize lane name for case-insensitive comparison
280
+ const normalizedLane = lane.toLowerCase().trim();
281
+ // Extract all lanes with their wip_limit
282
+ let allLanes = [];
283
+ if (Array.isArray(config.lanes)) {
284
+ // Flat array format: lanes: [{name: "Core", wip_limit: 2}, ...]
285
+ allLanes = config.lanes;
286
+ }
287
+ else {
288
+ // Nested format: lanes: {engineering: [...], business: [...]}
289
+ if (config.lanes.engineering) {
290
+ allLanes.push(...config.lanes.engineering);
291
+ }
292
+ if (config.lanes.business) {
293
+ allLanes.push(...config.lanes.business);
294
+ }
295
+ }
296
+ // Find matching lane (case-insensitive)
297
+ const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
298
+ if (!matchingLane) {
299
+ return DEFAULT_WIP_LIMIT;
300
+ }
301
+ // Return wip_limit if specified, otherwise default
302
+ return matchingLane.wip_limit ?? DEFAULT_WIP_LIMIT;
303
+ }
304
+ catch {
305
+ // If config parsing fails, return default
306
+ return DEFAULT_WIP_LIMIT;
307
+ }
308
+ }
309
+ /**
310
+ * Check if a lane is free (in_progress WU count is below wip_limit)
311
+ *
312
+ * WU-1016: Now respects configurable wip_limit per lane from .lumenflow.config.yaml.
313
+ * Lane is considered "free" if current in_progress count < wip_limit.
314
+ * Default wip_limit is 1 if not specified in config (backward compatible).
315
+ *
233
316
  * @param {string} statusPath - Path to status.md
234
317
  * @param {string} lane - Lane name (e.g., "Operations", "Intelligence")
235
318
  * @param {string} wuid - WU ID being claimed (e.g., "WU-419")
236
- * @returns {{ free: boolean, occupiedBy: string | null, error: string | null }}
319
+ * @param {CheckLaneFreeOptions} options - Options including configPath for testing
320
+ * @returns {{ free: boolean, occupiedBy: string | null, error: string | null, inProgressWUs?: string[], wipLimit?: number, currentCount?: number }}
237
321
  */
238
- export function checkLaneFree(statusPath, lane, wuid) {
322
+ export function checkLaneFree(statusPath, lane, wuid, options = {}) {
239
323
  /** Section heading marker for H2 headings */
240
324
  const SECTION_HEADING_PREFIX = '## ';
241
325
  try {
@@ -264,19 +348,38 @@ export function checkLaneFree(statusPath, lane, wuid) {
264
348
  endIdx = inProgressIdx + 1 + endIdx;
265
349
  // Extract WU links from In Progress section
266
350
  const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
351
+ // WU-1016: Get WIP limit for this lane from config
352
+ const wipLimit = getWipLimitForLane(lane, { configPath: options.configPath });
267
353
  // Check for "No items" marker
268
354
  if (section.includes(NO_ITEMS_MARKER)) {
269
- return { free: true, occupiedBy: null, error: null };
355
+ return {
356
+ free: true,
357
+ occupiedBy: null,
358
+ error: null,
359
+ inProgressWUs: [],
360
+ wipLimit,
361
+ currentCount: 0,
362
+ };
270
363
  }
271
364
  // Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
272
365
  WU_LINK_PATTERN.lastIndex = 0; // Reset global regex state
273
366
  const matches = [...section.matchAll(WU_LINK_PATTERN)];
274
367
  if (matches.length === 0) {
275
- return { free: true, occupiedBy: null, error: null };
368
+ return {
369
+ free: true,
370
+ occupiedBy: null,
371
+ error: null,
372
+ inProgressWUs: [],
373
+ wipLimit,
374
+ currentCount: 0,
375
+ };
276
376
  }
277
377
  // Get project root from statusPath (docs/04-operations/tasks/status.md)
278
378
  // Use path.dirname 4 times: status.md -> tasks -> 04-operations -> docs -> root
279
379
  const projectRoot = path.dirname(path.dirname(path.dirname(path.dirname(statusPath))));
380
+ // WU-1016: Collect all WUs in the target lane
381
+ const inProgressWUs = [];
382
+ const targetLane = lane.toString().trim().toLowerCase();
280
383
  for (const match of matches) {
281
384
  const activeWuid = match[1]; // e.g., "WU-334"
282
385
  // Skip if it's the same WU we're trying to claim (shouldn't happen, but be safe)
@@ -290,17 +393,16 @@ export function checkLaneFree(statusPath, lane, wuid) {
290
393
  }
291
394
  try {
292
395
  const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
293
- const wuDoc = yaml.load(wuContent);
396
+ const wuDoc = parseYAML(wuContent);
294
397
  if (!wuDoc || !wuDoc.lane) {
295
398
  console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
296
399
  continue;
297
400
  }
298
401
  // Normalize lane names for comparison (case-insensitive, trim whitespace)
299
402
  const activeLane = wuDoc.lane.toString().trim().toLowerCase();
300
- const targetLane = lane.toString().trim().toLowerCase();
301
403
  if (activeLane === targetLane) {
302
- // Lane is occupied!
303
- return { free: false, occupiedBy: activeWuid, error: null };
404
+ // WU-1016: Add to list of in-progress WUs in this lane
405
+ inProgressWUs.push(activeWuid);
304
406
  }
305
407
  }
306
408
  catch (e) {
@@ -309,8 +411,17 @@ export function checkLaneFree(statusPath, lane, wuid) {
309
411
  continue;
310
412
  }
311
413
  }
312
- // No WUs found in target lane
313
- return { free: true, occupiedBy: null, error: null };
414
+ // WU-1016: Check if lane is free based on WIP limit
415
+ const currentCount = inProgressWUs.length;
416
+ const isFree = currentCount < wipLimit;
417
+ return {
418
+ free: isFree,
419
+ occupiedBy: isFree ? null : inProgressWUs[0] || null,
420
+ error: null,
421
+ inProgressWUs,
422
+ wipLimit,
423
+ currentCount,
424
+ };
314
425
  }
315
426
  catch (error) {
316
427
  const errMessage = error instanceof Error ? error.message : String(error);
@@ -12,12 +12,12 @@
12
12
  */
13
13
  import { readFileSync, existsSync } from 'node:fs';
14
14
  import path from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
15
  import YAML from 'yaml'; // Modern YAML library (not js-yaml)
17
16
  import micromatch from 'micromatch'; // Industry-standard glob matching (CommonJS)
18
17
  import { extractParent } from './lane-checker.js'; // Shared utility (WU-1137: consolidation)
19
18
  import { createError, ErrorCodes } from './error-handler.js';
20
19
  import { WEIGHTS, CONFIDENCE } from './wu-validation-constants.js';
20
+ import { findProjectRoot } from './lumenflow-config.js';
21
21
  /**
22
22
  * Load lane inference config from project root
23
23
  * @param {string|null} configPath - Optional path to config file (defaults to project root)
@@ -26,9 +26,8 @@ import { WEIGHTS, CONFIDENCE } from './wu-validation-constants.js';
26
26
  */
27
27
  function loadConfig(configPath = null) {
28
28
  if (!configPath) {
29
- // Default to project root
30
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
31
- const projectRoot = path.resolve(currentDir, '../..');
29
+ // Use findProjectRoot() to locate config from cwd
30
+ const projectRoot = findProjectRoot();
32
31
  configPath = path.join(projectRoot, '.lumenflow.lane-inference.yaml');
33
32
  }
34
33
  if (!existsSync(configPath)) {
@@ -52,6 +52,7 @@ export declare const GitConfigSchema: z.ZodObject<{
52
52
  maxBranchDrift: z.ZodDefault<z.ZodNumber>;
53
53
  branchDriftWarning: z.ZodDefault<z.ZodNumber>;
54
54
  branchDriftInfo: z.ZodDefault<z.ZodNumber>;
55
+ agentBranchPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
55
56
  }, z.core.$strip>;
56
57
  /**
57
58
  * WU (Work Unit) configuration
@@ -91,12 +92,84 @@ export declare const UiConfigSchema: z.ZodObject<{
91
92
  statusPreviewLines: z.ZodDefault<z.ZodNumber>;
92
93
  readinessBoxWidth: z.ZodDefault<z.ZodNumber>;
93
94
  }, z.core.$strip>;
95
+ /**
96
+ * YAML serialization configuration
97
+ */
94
98
  /**
95
99
  * YAML serialization configuration
96
100
  */
97
101
  export declare const YamlConfigSchema: z.ZodObject<{
98
102
  lineWidth: z.ZodDefault<z.ZodNumber>;
99
103
  }, z.core.$strip>;
104
+ /**
105
+ * Methodology defaults (agent-facing project defaults)
106
+ */
107
+ export declare const DEFAULT_METHODOLOGY_PRINCIPLES: string[];
108
+ export declare const MethodologyDefaultsSchema: z.ZodObject<{
109
+ enabled: z.ZodDefault<z.ZodBoolean>;
110
+ enforcement: z.ZodDefault<z.ZodEnum<{
111
+ required: "required";
112
+ recommended: "recommended";
113
+ }>>;
114
+ principles: z.ZodDefault<z.ZodArray<z.ZodString>>;
115
+ notes: z.ZodOptional<z.ZodString>;
116
+ }, z.core.$strip>;
117
+ /**
118
+ * Client-specific blocks (agent-facing spawn blocks)
119
+ */
120
+ export declare const ClientBlockSchema: z.ZodObject<{
121
+ title: z.ZodString;
122
+ content: z.ZodString;
123
+ }, z.core.$strip>;
124
+ /**
125
+ * Client-specific skills guidance
126
+ */
127
+ export declare const ClientSkillsSchema: z.ZodObject<{
128
+ instructions: z.ZodOptional<z.ZodString>;
129
+ recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
130
+ }, z.core.$strip>;
131
+ /**
132
+ * Client configuration (per-client settings)
133
+ */
134
+ export declare const ClientConfigSchema: z.ZodObject<{
135
+ preamble: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>>;
136
+ skillsDir: z.ZodOptional<z.ZodString>;
137
+ blocks: z.ZodDefault<z.ZodArray<z.ZodObject<{
138
+ title: z.ZodString;
139
+ content: z.ZodString;
140
+ }, z.core.$strip>>>;
141
+ skills: z.ZodOptional<z.ZodObject<{
142
+ instructions: z.ZodOptional<z.ZodString>;
143
+ recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
144
+ }, z.core.$strip>>;
145
+ }, z.core.$strip>;
146
+ /**
147
+ * Agents configuration
148
+ */
149
+ export declare const AgentsConfigSchema: z.ZodObject<{
150
+ defaultClient: z.ZodDefault<z.ZodString>;
151
+ clients: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
152
+ preamble: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>>;
153
+ skillsDir: z.ZodOptional<z.ZodString>;
154
+ blocks: z.ZodDefault<z.ZodArray<z.ZodObject<{
155
+ title: z.ZodString;
156
+ content: z.ZodString;
157
+ }, z.core.$strip>>>;
158
+ skills: z.ZodOptional<z.ZodObject<{
159
+ instructions: z.ZodOptional<z.ZodString>;
160
+ recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
161
+ }, z.core.$strip>>;
162
+ }, z.core.$strip>>>;
163
+ methodology: z.ZodDefault<z.ZodObject<{
164
+ enabled: z.ZodDefault<z.ZodBoolean>;
165
+ enforcement: z.ZodDefault<z.ZodEnum<{
166
+ required: "required";
167
+ recommended: "recommended";
168
+ }>>;
169
+ principles: z.ZodDefault<z.ZodArray<z.ZodString>>;
170
+ notes: z.ZodOptional<z.ZodString>;
171
+ }, z.core.$strip>>;
172
+ }, z.core.$strip>;
100
173
  /**
101
174
  * Complete LumenFlow configuration schema
102
175
  */
@@ -138,6 +211,7 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
138
211
  maxBranchDrift: z.ZodDefault<z.ZodNumber>;
139
212
  branchDriftWarning: z.ZodDefault<z.ZodNumber>;
140
213
  branchDriftInfo: z.ZodDefault<z.ZodNumber>;
214
+ agentBranchPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
141
215
  }, z.core.$strip>>;
142
216
  wu: z.ZodDefault<z.ZodObject<{
143
217
  idPattern: z.ZodDefault<z.ZodString>;
@@ -168,6 +242,30 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
168
242
  yaml: z.ZodDefault<z.ZodObject<{
169
243
  lineWidth: z.ZodDefault<z.ZodNumber>;
170
244
  }, z.core.$strip>>;
245
+ agents: z.ZodDefault<z.ZodObject<{
246
+ defaultClient: z.ZodDefault<z.ZodString>;
247
+ clients: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
248
+ preamble: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>>;
249
+ skillsDir: z.ZodOptional<z.ZodString>;
250
+ blocks: z.ZodDefault<z.ZodArray<z.ZodObject<{
251
+ title: z.ZodString;
252
+ content: z.ZodString;
253
+ }, z.core.$strip>>>;
254
+ skills: z.ZodOptional<z.ZodObject<{
255
+ instructions: z.ZodOptional<z.ZodString>;
256
+ recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
257
+ }, z.core.$strip>>;
258
+ }, z.core.$strip>>>;
259
+ methodology: z.ZodDefault<z.ZodObject<{
260
+ enabled: z.ZodDefault<z.ZodBoolean>;
261
+ enforcement: z.ZodDefault<z.ZodEnum<{
262
+ required: "required";
263
+ recommended: "recommended";
264
+ }>>;
265
+ principles: z.ZodDefault<z.ZodArray<z.ZodString>>;
266
+ notes: z.ZodOptional<z.ZodString>;
267
+ }, z.core.$strip>>;
268
+ }, z.core.$strip>>;
171
269
  }, z.core.$strip>;
172
270
  /**
173
271
  * TypeScript types inferred from schemas
@@ -180,6 +278,11 @@ export type GatesConfig = z.infer<typeof GatesConfigSchema>;
180
278
  export type MemoryConfig = z.infer<typeof MemoryConfigSchema>;
181
279
  export type UiConfig = z.infer<typeof UiConfigSchema>;
182
280
  export type YamlConfig = z.infer<typeof YamlConfigSchema>;
281
+ export type MethodologyDefaults = z.infer<typeof MethodologyDefaultsSchema>;
282
+ export type ClientBlock = z.infer<typeof ClientBlockSchema>;
283
+ export type ClientSkills = z.infer<typeof ClientSkillsSchema>;
284
+ export type ClientConfig = z.infer<typeof ClientConfigSchema>;
285
+ export type AgentsConfig = z.infer<typeof AgentsConfigSchema>;
183
286
  export type LumenFlowConfig = z.infer<typeof LumenFlowConfigSchema>;
184
287
  /**
185
288
  * Validate configuration data
@@ -225,6 +328,7 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
225
328
  maxBranchDrift: number;
226
329
  branchDriftWarning: number;
227
330
  branchDriftInfo: number;
331
+ agentBranchPatterns: string[];
228
332
  };
229
333
  wu: {
230
334
  idPattern: string;
@@ -255,6 +359,27 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
255
359
  yaml: {
256
360
  lineWidth: number;
257
361
  };
362
+ agents: {
363
+ defaultClient: string;
364
+ clients: Record<string, {
365
+ blocks: {
366
+ title: string;
367
+ content: string;
368
+ }[];
369
+ preamble?: string | boolean;
370
+ skillsDir?: string;
371
+ skills?: {
372
+ recommended: string[];
373
+ instructions?: string;
374
+ };
375
+ }>;
376
+ methodology: {
377
+ enabled: boolean;
378
+ enforcement: "required" | "recommended";
379
+ principles: string[];
380
+ notes?: string;
381
+ };
382
+ };
258
383
  }>;
259
384
  /**
260
385
  * Parse configuration with defaults
@@ -83,6 +83,13 @@ export const GitConfigSchema = z.object({
83
83
  branchDriftWarning: z.number().int().positive().default(15),
84
84
  /** Info threshold for branch drift */
85
85
  branchDriftInfo: z.number().int().positive().default(10),
86
+ /**
87
+ * Agent branch patterns that bypass worktree requirements.
88
+ * Branches matching these glob patterns can work in the main checkout.
89
+ * Default: ['agent/*'] - narrow default, add vendor patterns as needed.
90
+ * Protected branches (mainBranch + 'master') are NEVER bypassed.
91
+ */
92
+ agentBranchPatterns: z.array(z.string()).default(['agent/*']),
86
93
  });
87
94
  /**
88
95
  * WU (Work Unit) configuration
@@ -148,6 +155,9 @@ export const UiConfigSchema = z.object({
148
155
  /** Readiness box width (default: 50) */
149
156
  readinessBoxWidth: z.number().int().positive().default(50),
150
157
  });
158
+ /**
159
+ * YAML serialization configuration
160
+ */
151
161
  /**
152
162
  * YAML serialization configuration
153
163
  */
@@ -155,6 +165,70 @@ export const YamlConfigSchema = z.object({
155
165
  /** Line width for YAML output (default: 100, -1 for no wrap) */
156
166
  lineWidth: z.number().int().default(100),
157
167
  });
168
+ /**
169
+ * Methodology defaults (agent-facing project defaults)
170
+ */
171
+ export const DEFAULT_METHODOLOGY_PRINCIPLES = [
172
+ 'TDD',
173
+ 'Hexagonal Architecture',
174
+ 'SOLID',
175
+ 'DRY',
176
+ 'YAGNI',
177
+ 'KISS',
178
+ 'Library-First',
179
+ ];
180
+ export const MethodologyDefaultsSchema = z.object({
181
+ /** Enable or disable project defaults output */
182
+ enabled: z.boolean().default(true),
183
+ /** Whether defaults are required or recommended */
184
+ enforcement: z.enum(['required', 'recommended']).default('required'),
185
+ /** Default methodology principles to apply */
186
+ principles: z.array(z.string()).default(DEFAULT_METHODOLOGY_PRINCIPLES),
187
+ /** Optional notes appended to Project Defaults */
188
+ notes: z.string().optional(),
189
+ });
190
+ /**
191
+ * Client-specific blocks (agent-facing spawn blocks)
192
+ */
193
+ export const ClientBlockSchema = z.object({
194
+ /** Block title */
195
+ title: z.string(),
196
+ /** Block content (markdown allowed) */
197
+ content: z.string(),
198
+ });
199
+ /**
200
+ * Client-specific skills guidance
201
+ */
202
+ export const ClientSkillsSchema = z.object({
203
+ /** Optional skills selection guidance text */
204
+ instructions: z.string().optional(),
205
+ /** Recommended skills to load for this client */
206
+ recommended: z.array(z.string()).default([]),
207
+ });
208
+ /**
209
+ * Client configuration (per-client settings)
210
+ */
211
+ export const ClientConfigSchema = z.object({
212
+ /** Preamble file path (e.g. 'CLAUDE.md') or false to disable */
213
+ preamble: z.union([z.string(), z.boolean()]).optional(),
214
+ /** Skills directory path */
215
+ skillsDir: z.string().optional(),
216
+ /** Client-specific blocks injected into wu:spawn output */
217
+ blocks: z.array(ClientBlockSchema).default([]),
218
+ /** Client-specific skills guidance for wu:spawn */
219
+ skills: ClientSkillsSchema.optional(),
220
+ });
221
+ /**
222
+ * Agents configuration
223
+ */
224
+ export const AgentsConfigSchema = z.object({
225
+ /** Default client to use if not specified (default: 'claude-code') */
226
+ defaultClient: z.string().default('claude-code'),
227
+ /** Client-specific configurations */
228
+ clients: z.record(z.string(), ClientConfigSchema).default({}),
229
+ /** Project methodology defaults (agent-facing) */
230
+ methodology: MethodologyDefaultsSchema.default(() => MethodologyDefaultsSchema.parse({})),
231
+ });
158
232
  /**
159
233
  * Complete LumenFlow configuration schema
160
234
  */
@@ -177,6 +251,8 @@ export const LumenFlowConfigSchema = z.object({
177
251
  ui: UiConfigSchema.default(() => UiConfigSchema.parse({})),
178
252
  /** YAML configuration */
179
253
  yaml: YamlConfigSchema.default(() => YamlConfigSchema.parse({})),
254
+ /** Agents configuration */
255
+ agents: AgentsConfigSchema.default(() => AgentsConfigSchema.parse({})),
180
256
  });
181
257
  /**
182
258
  * Validate configuration data
@@ -0,0 +1,130 @@
1
+ /**
2
+ * LumenFlow Home Directory Resolution
3
+ *
4
+ * WU-1062: External plan storage and no-main-write mode
5
+ *
6
+ * Provides helpers for resolving the $LUMENFLOW_HOME directory and related paths.
7
+ * Plans are stored externally in $LUMENFLOW_HOME/plans/ instead of in the repo.
8
+ *
9
+ * Default: ~/.lumenflow/
10
+ * Override: Set $LUMENFLOW_HOME environment variable
11
+ *
12
+ * @module
13
+ */
14
+ /**
15
+ * Environment variable name for LumenFlow home directory
16
+ */
17
+ export declare const LUMENFLOW_HOME_ENV = "LUMENFLOW_HOME";
18
+ /**
19
+ * Default LumenFlow home directory name
20
+ */
21
+ export declare const DEFAULT_LUMENFLOW_DIR = ".lumenflow";
22
+ /**
23
+ * Plans subdirectory name
24
+ */
25
+ export declare const PLANS_SUBDIR = "plans";
26
+ /**
27
+ * Custom protocol for external LumenFlow paths
28
+ */
29
+ export declare const LUMENFLOW_PROTOCOL = "lumenflow://";
30
+ /**
31
+ * Environment variable prefix for spec_refs
32
+ */
33
+ export declare const LUMENFLOW_HOME_VAR_PREFIX = "$LUMENFLOW_HOME";
34
+ /**
35
+ * Get the LumenFlow home directory path
36
+ *
37
+ * Resolution order:
38
+ * 1. $LUMENFLOW_HOME environment variable (with ~ expansion)
39
+ * 2. ~/.lumenflow/ default
40
+ *
41
+ * @returns {string} Absolute path to LumenFlow home directory
42
+ *
43
+ * @example
44
+ * // With LUMENFLOW_HOME=/custom/path
45
+ * getLumenflowHome() // '/custom/path'
46
+ *
47
+ * @example
48
+ * // With LUMENFLOW_HOME=~/.custom-lumenflow
49
+ * getLumenflowHome() // '/home/user/.custom-lumenflow'
50
+ *
51
+ * @example
52
+ * // Without LUMENFLOW_HOME set
53
+ * getLumenflowHome() // '/home/user/.lumenflow'
54
+ */
55
+ export declare function getLumenflowHome(): string;
56
+ /**
57
+ * Get the plans directory path
58
+ *
59
+ * Plans are stored in $LUMENFLOW_HOME/plans/
60
+ *
61
+ * @returns {string} Absolute path to plans directory
62
+ *
63
+ * @example
64
+ * getPlansDir() // '/home/user/.lumenflow/plans'
65
+ */
66
+ export declare function getPlansDir(): string;
67
+ /**
68
+ * Check if a path is an external (non-repo) path
69
+ *
70
+ * External paths include:
71
+ * - Paths starting with ~/
72
+ * - Paths starting with $LUMENFLOW_HOME
73
+ * - Paths using lumenflow:// protocol
74
+ * - Absolute paths (starting with /)
75
+ *
76
+ * @param {string} path - Path to check
77
+ * @returns {boolean} True if path is external
78
+ *
79
+ * @example
80
+ * isExternalPath('~/.lumenflow/plans/plan.md') // true
81
+ * isExternalPath('$LUMENFLOW_HOME/plans/plan.md') // true
82
+ * isExternalPath('lumenflow://plans/plan.md') // true
83
+ * isExternalPath('/home/user/.lumenflow/plans/plan.md') // true
84
+ * isExternalPath('docs/04-operations/plans/plan.md') // false
85
+ */
86
+ export declare function isExternalPath(path: string): boolean;
87
+ /**
88
+ * Normalize a spec_ref path by expanding variables and protocols
89
+ *
90
+ * Expands:
91
+ * - lumenflow://path -> $LUMENFLOW_HOME/path
92
+ * - ~/path -> /home/user/path
93
+ * - $LUMENFLOW_HOME/path -> actual LUMENFLOW_HOME value
94
+ *
95
+ * Repo-relative paths are returned unchanged.
96
+ *
97
+ * @param {string} specRef - Spec reference path
98
+ * @returns {string} Normalized absolute path or unchanged relative path
99
+ *
100
+ * @example
101
+ * normalizeSpecRef('lumenflow://plans/WU-1062-plan.md')
102
+ * // '/home/user/.lumenflow/plans/WU-1062-plan.md'
103
+ *
104
+ * @example
105
+ * normalizeSpecRef('docs/04-operations/plans/plan.md')
106
+ * // 'docs/04-operations/plans/plan.md' (unchanged)
107
+ */
108
+ export declare function normalizeSpecRef(specRef: string): string;
109
+ /**
110
+ * Get the full path for a plan file given a WU ID
111
+ *
112
+ * @param {string} wuId - Work Unit ID (e.g., 'WU-1062')
113
+ * @returns {string} Full path to the plan file
114
+ *
115
+ * @example
116
+ * getPlanPath('WU-1062')
117
+ * // '/home/user/.lumenflow/plans/WU-1062-plan.md'
118
+ */
119
+ export declare function getPlanPath(wuId: string): string;
120
+ /**
121
+ * Get the lumenflow:// protocol reference for a plan
122
+ *
123
+ * @param {string} wuId - Work Unit ID (e.g., 'WU-1062')
124
+ * @returns {string} Protocol reference (e.g., 'lumenflow://plans/WU-1062-plan.md')
125
+ *
126
+ * @example
127
+ * getPlanProtocolRef('WU-1062')
128
+ * // 'lumenflow://plans/WU-1062-plan.md'
129
+ */
130
+ export declare function getPlanProtocolRef(wuId: string): string;