@lumenflow/cli 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wu-edit.js CHANGED
@@ -26,6 +26,7 @@
26
26
  * Part of WU-1274: Add wu:edit command for spec-only changes
27
27
  * @see {@link tools/lib/micro-worktree.mjs} - Shared micro-worktree logic
28
28
  */
29
+ import { fileURLToPath } from 'node:url';
29
30
  import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter.js';
30
31
  import { die } from '@lumenflow/core/dist/error-handler.js';
31
32
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -35,7 +36,9 @@ import { join, resolve } from 'node:path';
35
36
  import { parseYAML, stringifyYAML, readWU } from '@lumenflow/core/dist/wu-yaml.js';
36
37
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
37
38
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
38
- import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, WU_STATUS, CLAIMED_MODES, getLaneBranch, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, READINESS_UI, } from '@lumenflow/core/dist/wu-constants.js';
39
+ import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, WU_STATUS, CLAIMED_MODES, getLaneBranch, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, READINESS_UI,
40
+ // WU-1039: Import exposure values for validation (Library-First, no magic strings)
41
+ WU_EXPOSURE_VALUES, } from '@lumenflow/core/dist/wu-constants.js';
39
42
  // WU-1593: Use centralized validateWUIDFormat (DRY)
40
43
  import { ensureOnMain, ensureMainUpToDate, validateWUIDFormat, } from '@lumenflow/core/dist/wu-helpers.js';
41
44
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
@@ -57,6 +60,86 @@ import { normalizeWUSchema } from '@lumenflow/core/dist/wu-schema-normalization.
57
60
  import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
58
61
  /* eslint-disable security/detect-object-injection */
59
62
  const PREFIX = LOG_PREFIX.EDIT;
63
+ /**
64
+ * WU-1039: Validate which edits are allowed on done WUs
65
+ *
66
+ * Done WUs only allow metadata reassignment: initiative, phase, and exposure.
67
+ * All other edits are blocked to preserve WU immutability after completion.
68
+ *
69
+ * @param opts - Parsed CLI options
70
+ * @returns { valid: boolean, disallowedEdits: string[] }
71
+ */
72
+ export function validateDoneWUEdits(opts) {
73
+ const disallowedEdits = [];
74
+ // Check for disallowed edits on done WUs
75
+ if (opts.specFile)
76
+ disallowedEdits.push('--spec-file');
77
+ if (opts.description)
78
+ disallowedEdits.push('--description');
79
+ if (opts.acceptance && Array.isArray(opts.acceptance) && opts.acceptance.length > 0) {
80
+ disallowedEdits.push('--acceptance');
81
+ }
82
+ if (opts.notes)
83
+ disallowedEdits.push('--notes');
84
+ if (opts.codePaths && Array.isArray(opts.codePaths) && opts.codePaths.length > 0) {
85
+ disallowedEdits.push('--code-paths');
86
+ }
87
+ if (opts.lane)
88
+ disallowedEdits.push('--lane');
89
+ if (opts.type)
90
+ disallowedEdits.push('--type');
91
+ if (opts.priority)
92
+ disallowedEdits.push('--priority');
93
+ if (opts.testPathsManual &&
94
+ Array.isArray(opts.testPathsManual) &&
95
+ opts.testPathsManual.length > 0) {
96
+ disallowedEdits.push('--test-paths-manual');
97
+ }
98
+ if (opts.testPathsUnit && Array.isArray(opts.testPathsUnit) && opts.testPathsUnit.length > 0) {
99
+ disallowedEdits.push('--test-paths-unit');
100
+ }
101
+ if (opts.testPathsE2e && Array.isArray(opts.testPathsE2e) && opts.testPathsE2e.length > 0) {
102
+ disallowedEdits.push('--test-paths-e2e');
103
+ }
104
+ return {
105
+ valid: disallowedEdits.length === 0,
106
+ disallowedEdits,
107
+ };
108
+ }
109
+ /**
110
+ * WU-1039: Validate exposure value against schema
111
+ *
112
+ * Uses WU_EXPOSURE_VALUES from core constants (Library-First, no magic strings).
113
+ *
114
+ * @param exposure - Exposure value to validate
115
+ * @returns { valid: boolean, error?: string }
116
+ */
117
+ export function validateExposureValue(exposure) {
118
+ // WU_EXPOSURE_VALUES is readonly array, need to cast for includes check
119
+ const validValues = WU_EXPOSURE_VALUES;
120
+ if (!validValues.includes(exposure)) {
121
+ return {
122
+ valid: false,
123
+ error: `Invalid exposure value: "${exposure}"\n\nValid values: ${WU_EXPOSURE_VALUES.join(', ')}`,
124
+ };
125
+ }
126
+ return { valid: true };
127
+ }
128
+ /**
129
+ * WU-1039: Apply exposure edit to WU object
130
+ *
131
+ * Returns a new WU object with updated exposure (immutable pattern).
132
+ *
133
+ * @param wu - Original WU object
134
+ * @param exposure - New exposure value
135
+ * @returns Updated WU object (does not mutate original)
136
+ */
137
+ export function applyExposureEdit(wu, exposure) {
138
+ return {
139
+ ...wu,
140
+ exposure,
141
+ };
142
+ }
60
143
  /**
61
144
  * Custom options for wu-edit (not in shared WU_OPTIONS)
62
145
  */
@@ -244,6 +327,8 @@ function parseArgs() {
244
327
  // WU-2564: Add blocked_by and dependencies
245
328
  EDIT_OPTIONS.blockedBy,
246
329
  EDIT_OPTIONS.addDep,
330
+ // WU-1039: Add exposure for done WU metadata updates
331
+ WU_OPTIONS.exposure,
247
332
  ],
248
333
  required: ['id'],
249
334
  allowPositionalId: true,
@@ -629,6 +714,14 @@ function applyEdits(wu, opts) {
629
714
  .filter(Boolean);
630
715
  updated.dependencies = mergeArrayField(wu.dependencies, depIds, opts.append);
631
716
  }
717
+ // WU-1039: Handle --exposure flag with validation
718
+ if (opts.exposure) {
719
+ const exposureResult = validateExposureValue(opts.exposure);
720
+ if (!exposureResult.valid) {
721
+ die(exposureResult.error);
722
+ }
723
+ updated.exposure = opts.exposure;
724
+ }
632
725
  return updated;
633
726
  }
634
727
  /**
@@ -642,38 +735,18 @@ async function main() {
642
735
  // Validate inputs
643
736
  validateWUIDFormat(id);
644
737
  const { wu: originalWU, editMode, isDone } = validateWUEditable(id);
645
- // WU-1929: Done WUs only allow initiative/phase edits (metadata reassignment)
738
+ // WU-1039: Done WUs allow initiative/phase/exposure edits only (metadata reassignment)
739
+ // Uses validateDoneWUEdits (DRY - centralized validation logic)
646
740
  if (isDone) {
647
- const disallowedEdits = [];
648
- if (opts.specFile)
649
- disallowedEdits.push('--spec-file');
650
- if (opts.description)
651
- disallowedEdits.push('--description');
652
- if (opts.acceptance && opts.acceptance.length > 0)
653
- disallowedEdits.push('--acceptance');
654
- if (opts.notes)
655
- disallowedEdits.push('--notes');
656
- if (opts.codePaths && opts.codePaths.length > 0)
657
- disallowedEdits.push('--code-paths');
658
- if (opts.lane)
659
- disallowedEdits.push('--lane');
660
- if (opts.type)
661
- disallowedEdits.push('--type');
662
- if (opts.priority)
663
- disallowedEdits.push('--priority');
664
- if (opts.testPathsManual && opts.testPathsManual.length > 0)
665
- disallowedEdits.push('--test-paths-manual');
666
- if (opts.testPathsUnit && opts.testPathsUnit.length > 0)
667
- disallowedEdits.push('--test-paths-unit');
668
- if (opts.testPathsE2e && opts.testPathsE2e.length > 0)
669
- disallowedEdits.push('--test-paths-e2e');
670
- if (disallowedEdits.length > 0) {
741
+ const doneValidation = validateDoneWUEdits(opts);
742
+ if (!doneValidation.valid) {
671
743
  die(`Cannot edit WU ${id}: WU is done/immutable.\n\n` +
672
- `Completed WUs only allow initiative/phase reassignment.\n` +
673
- `Disallowed edits: ${disallowedEdits.join(', ')}\n\n` +
744
+ `Completed WUs only allow initiative/phase/exposure reassignment.\n` +
745
+ `Disallowed edits: ${doneValidation.disallowedEdits.join(', ')}\n\n` +
674
746
  `Allowed for done WUs:\n` +
675
747
  ` --initiative <initId> Reassign to different initiative\n` +
676
- ` --phase <number> Update phase within initiative`);
748
+ ` --phase <number> Update phase within initiative\n` +
749
+ ` --exposure <type> Update exposure level`);
677
750
  }
678
751
  }
679
752
  // Check we have something to edit
@@ -698,7 +771,9 @@ async function main() {
698
771
  opts.phase ||
699
772
  // WU-2564: Add blocked_by and add_dep to hasEdits check
700
773
  opts.blockedBy ||
701
- opts.addDep;
774
+ opts.addDep ||
775
+ // WU-1039: Add exposure to hasEdits check
776
+ opts.exposure;
702
777
  if (!hasEdits) {
703
778
  die('No edits specified.\n\n' +
704
779
  'Provide one of:\n' +
@@ -716,7 +791,8 @@ async function main() {
716
791
  ' --test-paths-unit <path> Add unit test paths (repeatable; use --append to add)\n' +
717
792
  ' --test-paths-e2e <path> Add e2e test paths (repeatable; use --append to add)\n' +
718
793
  ' --blocked-by <wuIds> WU IDs that block this WU (comma-separated; use --append to add)\n' +
719
- ' --add-dep <wuIds> Add WU IDs to dependencies (comma-separated; use --append to add)');
794
+ ' --add-dep <wuIds> Add WU IDs to dependencies (comma-separated; use --append to add)\n' +
795
+ ' --exposure <type> Update exposure level (ui, api, backend-only, documentation)');
720
796
  }
721
797
  // Apply edits to get updated WU
722
798
  const updatedWU = applyEdits(originalWU, opts);
@@ -814,39 +890,54 @@ async function main() {
814
890
  const oldInitiative = originalWU.initiative;
815
891
  const newInitiative = opts.initiative;
816
892
  const initiativeChanged = newInitiative && newInitiative !== oldInitiative;
817
- await withMicroWorktree({
818
- operation: MICRO_WORKTREE_OPERATIONS.WU_EDIT,
819
- id: id,
820
- logPrefix: PREFIX,
821
- execute: async ({ worktreePath }) => {
822
- const files = [WU_PATHS.WU(id)];
823
- // Write updated WU to micro-worktree (WU-1750: use normalized data)
824
- const wuPath = join(worktreePath, WU_PATHS.WU(id));
825
- // WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
826
- normalizeWUDates(normalizedWU);
827
- // Emergency fix Session 2: Use centralized stringifyYAML helper
828
- const yamlContent = stringifyYAML(normalizedWU);
829
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
830
- writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
831
- console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
832
- // WU-1929: Handle bidirectional initiative updates
833
- if (initiativeChanged) {
834
- const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
835
- files.push(...initiativeFiles);
836
- }
837
- return {
838
- commitMessage: COMMIT_FORMATS.EDIT(id),
839
- files,
840
- };
841
- },
842
- });
893
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
894
+ process.env.LUMENFLOW_WU_TOOL = MICRO_WORKTREE_OPERATIONS.WU_EDIT;
895
+ try {
896
+ await withMicroWorktree({
897
+ operation: MICRO_WORKTREE_OPERATIONS.WU_EDIT,
898
+ id: id,
899
+ logPrefix: PREFIX,
900
+ execute: async ({ worktreePath }) => {
901
+ const files = [WU_PATHS.WU(id)];
902
+ // Write updated WU to micro-worktree (WU-1750: use normalized data)
903
+ const wuPath = join(worktreePath, WU_PATHS.WU(id));
904
+ // WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
905
+ normalizeWUDates(normalizedWU);
906
+ // Emergency fix Session 2: Use centralized stringifyYAML helper
907
+ const yamlContent = stringifyYAML(normalizedWU);
908
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
909
+ writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
910
+ console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
911
+ // WU-1929: Handle bidirectional initiative updates
912
+ if (initiativeChanged) {
913
+ const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
914
+ files.push(...initiativeFiles);
915
+ }
916
+ return {
917
+ commitMessage: COMMIT_FORMATS.EDIT(id),
918
+ files,
919
+ };
920
+ },
921
+ });
922
+ }
923
+ finally {
924
+ if (previousWuTool === undefined) {
925
+ delete process.env.LUMENFLOW_WU_TOOL;
926
+ }
927
+ else {
928
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
929
+ }
930
+ }
843
931
  console.log(`${PREFIX} ✅ Successfully edited ${id}`);
844
932
  console.log(`${PREFIX} Changes pushed to origin/main`);
845
933
  // WU-1620: Display readiness summary
846
934
  displayReadinessSummary(id);
847
935
  }
848
936
  }
849
- main().catch((err) => {
850
- console.error(`${PREFIX} ${err.message}`);
851
- process.exit(EXIT_CODES.ERROR);
852
- });
937
+ // Guard main() execution for testability (WU-1366)
938
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
939
+ main().catch((err) => {
940
+ console.error(`${PREFIX} ❌ ${err.message}`);
941
+ process.exit(EXIT_CODES.ERROR);
942
+ });
943
+ }
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { readFileSync, existsSync } from 'node:fs';
18
18
  import path from 'node:path';
19
- import yaml from 'js-yaml';
19
+ import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
20
20
  import { inferSubLane } from '@lumenflow/core/dist/lane-inference.js';
21
21
  import { die } from '@lumenflow/core/dist/error-handler.js';
22
22
  import { FILE_SYSTEM, EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
@@ -81,7 +81,7 @@ function loadWuYaml(id) {
81
81
  ` 2. Ensure you have read access to the repository`);
82
82
  }
83
83
  try {
84
- const doc = yaml.load(content);
84
+ const doc = parseYAML(content);
85
85
  return doc;
86
86
  }
87
87
  catch (err) {
package/dist/wu-spawn.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Output:
13
13
  * A complete Task tool invocation block with:
14
- * - Context loading preamble (CLAUDE-core.md, README, lumenflow, WU YAML)
14
+ * - Context loading preamble (.claude/CLAUDE.md, README, lumenflow, WU YAML)
15
15
  * - WU details and acceptance criteria
16
16
  * - Skills Selection section (sub-agent reads catalogue and selects at runtime)
17
17
  * - Mandatory agent advisory
@@ -25,7 +25,7 @@
25
25
  * Codex Mode:
26
26
  * When --codex is used, outputs a Codex/GPT-friendly Markdown prompt (no antml/XML escaping).
27
27
  *
28
- * @see {@link ai/onboarding/agent-invocation-guide.md} - Context loading templates
28
+ * @see {@link docs/04-operations/_frameworks/lumenflow/agent/onboarding/agent-invocation-guide.md} - Context loading templates
29
29
  */
30
30
  import { existsSync, readFileSync } from 'node:fs';
31
31
  import path from 'node:path';
@@ -40,6 +40,9 @@ import { minimatch } from 'minimatch';
40
40
  // WU-2252: Import invariants loader for spawn output injection
41
41
  import { loadInvariants, INVARIANT_TYPES } from '@lumenflow/core/dist/invariants-runner.js';
42
42
  import { validateSpawnArgs, generateExecutionModeSection, generateThinkToolGuidance, recordSpawnToRegistry, formatSpawnRecordedMessage, } from '@lumenflow/core/dist/wu-spawn-helpers.js';
43
+ import { SpawnStrategyFactory } from '@lumenflow/core/dist/spawn-strategy.js';
44
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
45
+ import { generateClientSkillsGuidance, generateSkillsSelectionSection, resolveClientConfig, } from '@lumenflow/core/dist/wu-spawn-skills.js';
43
46
  import { validateSpawnDependencies, formatDependencyError, } from '@lumenflow/core/dist/dependency-validator.js';
44
47
  /**
45
48
  * Mandatory agent trigger patterns.
@@ -50,27 +53,6 @@ const MANDATORY_TRIGGERS = {
50
53
  'beacon-guardian': ['**/prompts/**', '**/classification/**', '**/detector/**', '**/llm/**'],
51
54
  };
52
55
  const LOG_PREFIX = '[wu:spawn]';
53
- /** @type {string} */
54
- const AGENTS_DIR = '.claude/agents';
55
- /**
56
- * Load skills configured in agent's frontmatter
57
- *
58
- * @param {string} agentName - Agent name (e.g., 'general-purpose')
59
- * @returns {string[]} Array of skill names or empty array if not found
60
- */
61
- function loadAgentConfiguredSkills(agentName) {
62
- const agentPath = `${AGENTS_DIR}/${agentName}.md`;
63
- if (!existsSync(agentPath)) {
64
- return [];
65
- }
66
- try {
67
- const content = readFileSync(agentPath, { encoding: FILE_SYSTEM.UTF8 });
68
- return []; // Skills loading removed - vendor agnostic
69
- }
70
- catch {
71
- return [];
72
- }
73
- }
74
56
  /**
75
57
  * Detect mandatory agents based on code paths.
76
58
  *
@@ -319,27 +301,15 @@ function generateTDDDirective() {
319
301
  * @param {string} id - WU ID
320
302
  * @returns {string} Context loading preamble
321
303
  */
322
- function generatePreamble(id) {
323
- return `Load the following context in this order:
324
-
325
- 1. Read CLAUDE.md (workflow fundamentals and critical rules)
326
- 2. Read README.md (project structure and tech stack)
327
- 3. Read docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md sections 1-7 (TDD, gates, Definition of Done)
328
- 4. Read docs/04-operations/tasks/wu/${id}.yaml (the specific WU you're working on)
329
-
330
- ## WIP=1 Lane Check (BEFORE claiming)
331
-
332
- Before running wu:claim, check docs/04-operations/tasks/status.md to ensure the lane is free.
333
- Only ONE WU can be in_progress per lane at any time.
334
-
335
- ## Context Recovery (Session Resumption)
336
-
337
- Before starting work, check for prior context from previous sessions:
338
-
339
- 1. \`pnpm mem:ready --wu ${id}\` — Query pending nodes (what's next?)
340
- 2. \`pnpm mem:inbox --wu ${id}\` — Check coordination signals from parallel agents
341
-
342
- If prior context exists, resume from the last checkpoint. Otherwise, proceed with the task below.`;
304
+ /**
305
+ * Generate the context loading preamble using the strategy
306
+ *
307
+ * @param {string} id - WU ID
308
+ * @param {SpawnStrategy} strategy - Client strategy
309
+ * @returns {string} Context loading preamble
310
+ */
311
+ function generatePreamble(id, strategy) {
312
+ return strategy.getPreamble(id);
343
313
  }
344
314
  /**
345
315
  * Generate the constraints block (appended at end per Lost in the Middle research)
@@ -739,7 +709,7 @@ pnpm mem:triage --wu ${id} # List discoveries for this WU
739
709
  pnpm mem:triage --promote <node-id> --lane "<lane>" # Create Bug WU (human action)
740
710
  \`\`\`
741
711
 
742
- See: ai/onboarding/agent-invocation-guide.md §Bug Discovery`;
712
+ See: docs/04-operations/_frameworks/lumenflow/agent/onboarding/agent-invocation-guide.md §Bug Discovery`;
743
713
  }
744
714
  /**
745
715
  * Generate lane-specific guidance
@@ -838,128 +808,37 @@ pnpm wu:done --id ${id}
838
808
 
839
809
  **Do not ask** "should I run wu:done?" — just run it when gates pass.`;
840
810
  }
841
- /**
842
- * Generate the Skills Selection section for sub-agents.
843
- *
844
- * Unlike /wu-prompt (human-facing, skills selected at generation time),
845
- * wu:spawn instructs the sub-agent to read the catalogue and select skills
846
- * at execution time based on WU context.
847
- *
848
- * If an agentName is provided, that agent's configured skills (from frontmatter)
849
- * are auto-loaded at the top.
850
- *
851
- * @param {object} doc - WU YAML document
852
- * @param {string} [agentName='general-purpose'] - Agent to spawn
853
- * @returns {string} Skills Selection section
854
- */
855
- // eslint-disable-next-line sonarjs/cognitive-complexity -- WU-2025: Pre-existing complexity, refactor tracked
856
- function generateSkillsSection(doc, agentName = 'general-purpose') {
857
- const lane = doc.lane || '';
858
- const type = doc.type || 'feature';
859
- const laneParent = lane.split(':')[0].trim();
860
- // Load agent's configured skills from frontmatter
861
- const agentSkills = loadAgentConfiguredSkills(agentName);
862
- const hasAgentSkills = agentSkills.length > 0;
863
- // Build auto-load section if agent has configured skills
864
- const autoLoadSection = hasAgentSkills
865
- ? `### Auto-Loaded Skills (from ${agentName} agent config)
866
-
867
- These skills are pre-configured for this agent and should be loaded first:
868
-
869
- ${agentSkills.map((s) => `- \`${s}\` — Load via \`/skill ${s}\``).join('\n')}
870
-
871
- `
872
- : '';
873
- // Build context hints for the sub-agent
874
- const contextHints = [];
875
- // Universal baselines (only if not already in agent skills)
876
- if (!agentSkills.includes('wu-lifecycle')) {
877
- contextHints.push('- `wu-lifecycle` — ALL WUs need workflow automation');
878
- }
879
- if (!agentSkills.includes('worktree-discipline')) {
880
- contextHints.push('- `worktree-discipline` — ALL WUs need path safety');
881
- }
882
- // Type-based hints
883
- if ((type === 'feature' || type === 'enhancement') && !agentSkills.includes('tdd-workflow')) {
884
- contextHints.push('- `tdd-workflow` — TDD is mandatory for feature/enhancement WUs');
885
- }
886
- if (type === 'bug' && !agentSkills.includes('bug-classification')) {
887
- contextHints.push('- `bug-classification` — Bug severity assessment');
888
- }
889
- // Lane-based hints
890
- if (laneParent === 'Operations' &&
891
- lane.includes('Tooling') &&
892
- !agentSkills.includes('lumenflow-gates')) {
893
- contextHints.push('- `lumenflow-gates` — Tooling often affects gates');
894
- }
895
- if (laneParent === 'Intelligence') {
896
- if (!agentSkills.includes('beacon-compliance')) {
897
- contextHints.push('- `beacon-compliance` — Intelligence lane requires Beacon validation');
898
- }
899
- if (!agentSkills.includes('prompt-management')) {
900
- contextHints.push('- `prompt-management` — For prompt template work');
901
- }
902
- }
903
- if (laneParent === 'Experience' && !agentSkills.includes('frontend-design')) {
904
- contextHints.push('- `frontend-design` — For UI component work');
905
- }
906
- const softPolicySection = contextHints.length > 0
907
- ? `### Soft Policy (baselines for this WU)
908
-
909
- Based on WU context, consider loading:
910
-
911
- ${contextHints.join('\n')}
912
-
913
- `
914
- : '';
915
- return `## Skills Selection
916
-
917
- **IMPORTANT**: Before starting work, select and load relevant skills.
918
-
919
- ${autoLoadSection}### How to Select Skills
920
-
921
- 1. Read the skill catalogue frontmatter from \`.claude/skills/*/SKILL.md\`
922
- 2. Match skills to WU context (lane, type, code_paths, description)
923
- 3. Load selected skills via \`/skill <skill-name>\`
924
-
925
- ${softPolicySection}### Additional Skills (load if needed)
926
-
927
- | Skill | Use When |
928
- |-------|----------|
929
- | lumenflow-gates | Gates fail, debugging format/lint/typecheck errors |
930
- | bug-classification | Bug discovered mid-WU, need priority classification |
931
- | beacon-compliance | Code touches LLM, prompts, classification |
932
- | prompt-management | Working with prompt templates, golden datasets |
933
- | frontend-design | Building UI components, pages |
934
- | initiative-management | Multi-phase projects, INIT-XXX coordination |
935
- | multi-agent-coordination | Spawning sub-agents, parallel WU work |
936
- | orchestration | Agent coordination, mandatory agent checks |
937
- | ops-maintenance | Metrics, validation, health checks |
938
-
939
- ### Graceful Degradation
940
-
941
- If the skill catalogue is missing or invalid:
942
- - Load baseline skills: \`/skill wu-lifecycle\`, \`/skill tdd-workflow\` (for features)
943
- - Continue with implementation using Mandatory Standards below
944
- `;
811
+ function generateClientBlocksSection(clientContext) {
812
+ if (!clientContext?.config?.blocks?.length)
813
+ return '';
814
+ const blocks = clientContext.config.blocks
815
+ .map((block) => `### ${block.title}\n\n${block.content}`)
816
+ .join('\n\n');
817
+ return `## Client Guidance (${clientContext.name})\n\n${blocks}`;
945
818
  }
946
819
  /**
947
820
  * Generate the complete Task tool invocation
948
821
  *
949
822
  * @param {object} doc - WU YAML document
950
823
  * @param {string} id - WU ID
824
+ * @param {SpawnStrategy} strategy - Client strategy
951
825
  * @param {object} [options={}] - Thinking mode options
952
826
  * @param {boolean} [options.thinking] - Whether extended thinking is enabled
953
827
  * @param {boolean} [options.noThinking] - Whether thinking is explicitly disabled
954
828
  * @param {string} [options.budget] - Token budget for thinking
955
829
  * @returns {string} Complete Task tool invocation
956
830
  */
957
- export function generateTaskInvocation(doc, id, options = {}) {
831
+ export function generateTaskInvocation(doc, id, strategy, options = {}) {
958
832
  const codePaths = doc.code_paths || [];
959
833
  const mandatoryAgents = detectMandatoryAgents(codePaths);
960
- const preamble = generatePreamble(id);
834
+ const preamble = generatePreamble(id, strategy);
961
835
  const tddDirective = generateTDDDirective();
962
- const skillsSection = generateSkillsSection(doc);
836
+ const clientContext = options.client;
837
+ const config = options.config || getConfig();
838
+ const clientSkillsGuidance = generateClientSkillsGuidance(clientContext);
839
+ const skillsSection = generateSkillsSelectionSection(doc, config, clientContext?.name) +
840
+ (clientSkillsGuidance ? `\n${clientSkillsGuidance}` : '');
841
+ const clientBlocks = generateClientBlocksSection(clientContext);
963
842
  const mandatorySection = generateMandatoryAgentSection(mandatoryAgents, id);
964
843
  const laneGuidance = generateLaneGuidance(doc.lane);
965
844
  const bugDiscoverySection = generateBugDiscoverySection(id);
@@ -1038,7 +917,7 @@ ${thinkingBlock}${skillsSection}
1038
917
  - **Documentation**: Update tooling docs if changing tools. Keep docs in sync with code
1039
918
  - **Sub-agents**: Use Explore agent for codebase investigation. Activate mandatory agents (security-auditor for PHI/auth, beacon-guardian for LLM/prompts)
1040
919
 
1041
- ${worktreeGuidance ? `---\n\n${worktreeGuidance}\n\n` : ''}---
920
+ ${clientBlocks ? `---\n\n${clientBlocks}\n\n` : ''}${worktreeGuidance ? `---\n\n${worktreeGuidance}\n\n` : ''}---
1042
921
 
1043
922
  ${bugDiscoverySection}
1044
923
 
@@ -1103,10 +982,10 @@ ${constraints}`;
1103
982
  ].join('\n');
1104
983
  return invocation;
1105
984
  }
1106
- export function generateCodexPrompt(doc, id, options = {}) {
985
+ export function generateCodexPrompt(doc, id, strategy, options = {}) {
1107
986
  const codePaths = doc.code_paths || [];
1108
987
  const mandatoryAgents = detectMandatoryAgents(codePaths);
1109
- const preamble = generatePreamble(id);
988
+ const preamble = generatePreamble(id, strategy);
1110
989
  const tddDirective = generateTDDDirective();
1111
990
  const mandatorySection = generateMandatoryAgentSection(mandatoryAgents, id);
1112
991
  const laneGuidance = generateLaneGuidance(doc.lane);
@@ -1114,6 +993,12 @@ export function generateCodexPrompt(doc, id, options = {}) {
1114
993
  const implementationContext = generateImplementationContext(doc);
1115
994
  const action = generateActionSection(doc, id);
1116
995
  const constraints = generateCodexConstraints(id);
996
+ const clientContext = options.client;
997
+ const config = options.config || getConfig();
998
+ const clientSkillsGuidance = generateClientSkillsGuidance(clientContext);
999
+ const skillsSection = generateSkillsSelectionSection(doc, config, clientContext?.name) +
1000
+ (clientSkillsGuidance ? `\n${clientSkillsGuidance}` : '');
1001
+ const clientBlocks = generateClientBlocksSection(clientContext);
1117
1002
  const executionModeSection = generateExecutionModeSection(options);
1118
1003
  const thinkToolGuidance = generateThinkToolGuidance(options);
1119
1004
  const thinkingSections = [executionModeSection, thinkToolGuidance]
@@ -1156,6 +1041,10 @@ ${formatAcceptance(doc.acceptance)}
1156
1041
 
1157
1042
  ---
1158
1043
 
1044
+ ${skillsSection}
1045
+
1046
+ ---
1047
+
1159
1048
  ## Action
1160
1049
 
1161
1050
  ${action}
@@ -1169,7 +1058,7 @@ ${action}
1169
1058
 
1170
1059
  ---
1171
1060
 
1172
- ${mandatorySection}${implementationContext ? `${implementationContext}\n\n---\n\n` : ''}${thinkingBlock}${bugDiscoverySection}
1061
+ ${mandatorySection}${implementationContext ? `${implementationContext}\n\n---\n\n` : ''}${clientBlocks ? `${clientBlocks}\n\n---\n\n` : ''}${thinkingBlock}${bugDiscoverySection}
1173
1062
 
1174
1063
  ---
1175
1064
 
@@ -1223,6 +1112,8 @@ async function main() {
1223
1112
  WU_OPTIONS.budget,
1224
1113
  WU_OPTIONS.codex,
1225
1114
  WU_OPTIONS.parentWu, // WU-1945: Parent WU for spawn registry tracking
1115
+ WU_OPTIONS.client,
1116
+ WU_OPTIONS.vendor,
1226
1117
  ],
1227
1118
  required: ['id'],
1228
1119
  allowPositionalId: true,
@@ -1295,15 +1186,43 @@ async function main() {
1295
1186
  noThinking: args.noThinking,
1296
1187
  budget: args.budget,
1297
1188
  };
1189
+ // Client Resolution
1190
+ const config = getConfig();
1191
+ let clientName = args.client;
1192
+ if (!clientName && args.vendor) {
1193
+ console.warn(`${LOG_PREFIX} ${EMOJI.WARNING} Warning: --vendor is deprecated. Use --client.`);
1194
+ clientName = args.vendor;
1195
+ }
1196
+ // Codex handling (deprecated legacy flag)
1298
1197
  if (args.codex) {
1299
- const prompt = generateCodexPrompt(doc, id, thinkingOptions);
1198
+ if (!clientName) {
1199
+ console.warn(`${LOG_PREFIX} ${EMOJI.WARNING} Warning: --codex is deprecated. Use --client codex-cli.`);
1200
+ clientName = 'codex-cli';
1201
+ }
1202
+ }
1203
+ if (!clientName) {
1204
+ clientName = config.agents.defaultClient || 'claude-code';
1205
+ }
1206
+ // Create strategy
1207
+ const strategy = SpawnStrategyFactory.create(clientName);
1208
+ const clientContext = { name: clientName, config: resolveClientConfig(config, clientName) };
1209
+ if (clientName === 'codex-cli' || args.codex) {
1210
+ const prompt = generateCodexPrompt(doc, id, strategy, {
1211
+ ...thinkingOptions,
1212
+ client: clientContext,
1213
+ config,
1214
+ });
1300
1215
  console.log(`${LOG_PREFIX} Generated Codex/GPT prompt for ${id}`);
1301
1216
  console.log(`${LOG_PREFIX} Copy the Markdown below:\n`);
1302
1217
  console.log(prompt.trimEnd());
1303
1218
  return;
1304
1219
  }
1305
1220
  // Generate and output the Task invocation
1306
- const invocation = generateTaskInvocation(doc, id, thinkingOptions);
1221
+ const invocation = generateTaskInvocation(doc, id, strategy, {
1222
+ ...thinkingOptions,
1223
+ client: clientContext,
1224
+ config,
1225
+ });
1307
1226
  console.log(`${LOG_PREFIX} Generated Task tool invocation for ${id}`);
1308
1227
  console.log(`${LOG_PREFIX} Copy the block below to spawn a sub-agent:\n`);
1309
1228
  console.log(invocation);