claude-code-workflow 7.2.29 → 7.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.
Files changed (169) hide show
  1. package/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json +2 -2
  2. package/.ccw/workflows/cli-templates/schemas/task-schema.json +14 -7
  3. package/.claude/agents/action-planning-agent.md +7 -4
  4. package/.claude/agents/cli-explore-agent.md +77 -63
  5. package/.claude/agents/cli-lite-planning-agent.md +11 -10
  6. package/.claude/agents/issue-plan-agent.md +421 -426
  7. package/.claude/commands/workflow/spec/setup.md +1 -1
  8. package/.claude/commands/workflow-skill.md +130 -0
  9. package/.claude/skills/ccw-chain/SKILL.md +92 -0
  10. package/.claude/skills/ccw-chain/chains/ccw-cycle.json +31 -0
  11. package/.claude/skills/ccw-chain/chains/ccw-exploration.json +58 -0
  12. package/.claude/skills/ccw-chain/chains/ccw-issue.json +44 -0
  13. package/.claude/skills/ccw-chain/chains/ccw-lightweight.json +71 -0
  14. package/.claude/skills/ccw-chain/chains/ccw-main.json +65 -0
  15. package/.claude/skills/ccw-chain/chains/ccw-standard.json +51 -0
  16. package/.claude/skills/ccw-chain/chains/ccw-team.json +15 -0
  17. package/.claude/skills/ccw-chain/chains/ccw-with-file.json +47 -0
  18. package/.claude/skills/ccw-chain/specs/auto-mode.md +47 -0
  19. package/.claude/skills/chain-loader/SKILL.md +78 -0
  20. package/.claude/skills/chain-loader/phases/01-analyze-skill.md +53 -0
  21. package/.claude/skills/chain-loader/phases/02-design-graph.md +73 -0
  22. package/.claude/skills/chain-loader/phases/03-generate-validate.md +75 -0
  23. package/.claude/skills/chain-loader/specs/chain-schema.md +126 -0
  24. package/.claude/skills/chain-loader/specs/design-patterns.md +99 -0
  25. package/.claude/skills/chain-loader/templates/chain-json.md +63 -0
  26. package/.claude/skills/review-cycle/phases/review-module.md +764 -764
  27. package/.claude/skills/review-cycle/phases/review-session.md +775 -775
  28. package/.claude/skills/workflow-multi-cli-plan/SKILL.md +2 -2
  29. package/.claude/skills/workflow-plan/SKILL.md +1 -0
  30. package/.claude/skills/workflow-plan/phases/01-session-discovery.md +19 -2
  31. package/.claude/skills/workflow-plan/phases/02-context-gathering.md +2 -2
  32. package/.claude/skills/workflow-plan/phases/03-conflict-resolution.md +422 -422
  33. package/.claude/skills/workflow-plan/phases/04-task-generation.md +9 -1
  34. package/.claude/skills/workflow-plan/phases/05-plan-verify.md +395 -395
  35. package/.claude/skills/workflow-tdd-plan/phases/02-context-gathering.md +407 -407
  36. package/.claude/skills/workflow-tdd-plan/phases/04-conflict-resolution.md +426 -426
  37. package/.claude/skills/workflow-test-fix/phases/02-test-context-gather.md +493 -493
  38. package/.codex/skills/analyze-with-file/SKILL.md +383 -134
  39. package/.codex/skills/brainstorm/SKILL.md +3 -3
  40. package/.codex/skills/brainstorm-with-file/SKILL.md +208 -88
  41. package/.codex/skills/clean/SKILL.md +1 -1
  42. package/.codex/skills/csv-wave-pipeline/SKILL.md +2 -2
  43. package/.codex/skills/investigate/orchestrator.md +24 -0
  44. package/.codex/skills/issue-discover/SKILL.md +374 -361
  45. package/.codex/skills/issue-discover/phases/01-issue-new.md +1 -1
  46. package/.codex/skills/issue-discover/phases/02-discover.md +2 -2
  47. package/.codex/skills/issue-discover/phases/03-discover-by-prompt.md +1 -1
  48. package/.codex/skills/issue-discover/phases/04-quick-execute.md +2 -2
  49. package/.codex/skills/parallel-dev-cycle/SKILL.md +44 -37
  50. package/.codex/skills/project-documentation-workflow/SKILL.md +1 -1
  51. package/.codex/skills/review-cycle/SKILL.md +31 -12
  52. package/.codex/skills/roadmap-with-file/SKILL.md +141 -133
  53. package/.codex/skills/security-audit/orchestrator.md +29 -0
  54. package/.codex/skills/session-sync/SKILL.md +1 -1
  55. package/.codex/skills/ship/orchestrator.md +24 -0
  56. package/.codex/skills/spec-add/SKILL.md +5 -5
  57. package/.codex/skills/spec-generator/SKILL.md +33 -2
  58. package/.codex/skills/spec-generator/phases/01-5-requirement-clarification.md +3 -3
  59. package/.codex/skills/spec-generator/phases/01-discovery.md +1 -1
  60. package/.codex/skills/spec-generator/phases/02-product-brief.md +1 -1
  61. package/.codex/skills/spec-generator/phases/03-requirements.md +1 -1
  62. package/.codex/skills/spec-generator/phases/04-architecture.md +1 -1
  63. package/.codex/skills/spec-generator/phases/05-epics-stories.md +1 -1
  64. package/.codex/skills/spec-generator/phases/06-readiness-check.md +1 -1
  65. package/.codex/skills/spec-generator/phases/07-issue-export.md +1 -1
  66. package/.codex/skills/spec-setup/SKILL.md +669 -669
  67. package/.codex/skills/team-arch-opt/specs/team-config.json +1 -1
  68. package/.codex/skills/team-brainstorm/SKILL.md +259 -259
  69. package/.codex/skills/team-coordinate/SKILL.md +359 -359
  70. package/.codex/skills/team-coordinate/roles/coordinator/commands/monitor.md +1 -1
  71. package/.codex/skills/team-designer/SKILL.md +27 -1
  72. package/.codex/skills/team-designer/phases/01-requirements-analysis.md +2 -2
  73. package/.codex/skills/team-designer/phases/02-scaffold-generation.md +1 -1
  74. package/.codex/skills/team-designer/phases/04-validation.md +1 -1
  75. package/.codex/skills/team-executor/SKILL.md +218 -218
  76. package/.codex/skills/team-frontend/SKILL.md +227 -227
  77. package/.codex/skills/team-frontend-debug/SKILL.md +278 -278
  78. package/.codex/skills/team-frontend-debug/roles/coordinator/commands/analyze.md +2 -2
  79. package/.codex/skills/team-interactive-craft/SKILL.md +220 -220
  80. package/.codex/skills/team-interactive-craft/roles/coordinator/role.md +209 -209
  81. package/.codex/skills/team-issue/SKILL.md +269 -269
  82. package/.codex/skills/team-issue/roles/coordinator/role.md +1 -1
  83. package/.codex/skills/team-lifecycle-v4/SKILL.md +305 -305
  84. package/.codex/skills/team-motion-design/SKILL.md +222 -222
  85. package/.codex/skills/team-motion-design/roles/coordinator/role.md +210 -210
  86. package/.codex/skills/team-perf-opt/SKILL.md +258 -258
  87. package/.codex/skills/team-perf-opt/specs/team-config.json +1 -1
  88. package/.codex/skills/team-planex/SKILL.md +216 -216
  89. package/.codex/skills/team-quality-assurance/SKILL.md +229 -229
  90. package/.codex/skills/team-review/SKILL.md +227 -227
  91. package/.codex/skills/team-roadmap-dev/SKILL.md +238 -238
  92. package/.codex/skills/team-roadmap-dev/roles/coordinator/commands/roadmap-discuss.md +5 -5
  93. package/.codex/skills/team-tech-debt/SKILL.md +206 -206
  94. package/.codex/skills/team-tech-debt/roles/coordinator/commands/monitor.md +1 -1
  95. package/.codex/skills/team-testing/SKILL.md +237 -237
  96. package/.codex/skills/team-ui-polish/SKILL.md +218 -218
  97. package/.codex/skills/team-ui-polish/roles/coordinator/role.md +213 -213
  98. package/.codex/skills/team-uidesign/SKILL.md +219 -219
  99. package/.codex/skills/team-uidesign/roles/coordinator/role.md +2 -2
  100. package/.codex/skills/team-ultra-analyze/SKILL.md +260 -260
  101. package/.codex/skills/team-ultra-analyze/roles/coordinator/commands/monitor.md +1 -1
  102. package/.codex/skills/team-ultra-analyze/roles/coordinator/role.md +1 -1
  103. package/.codex/skills/team-ux-improve/SKILL.md +227 -227
  104. package/.codex/skills/team-ux-improve/roles/coordinator/role.md +1 -1
  105. package/.codex/skills/team-ux-improve/specs/team-config.json +1 -1
  106. package/.codex/skills/team-visual-a11y/SKILL.md +319 -319
  107. package/.codex/skills/team-visual-a11y/roles/coordinator/role.md +213 -213
  108. package/.codex/skills/workflow-execute/SKILL.md +5 -5
  109. package/.codex/skills/workflow-lite-planex/SKILL.md +3 -3
  110. package/.codex/skills/workflow-plan/SKILL.md +3 -3
  111. package/.codex/skills/workflow-tdd-plan/SKILL.md +4 -4
  112. package/.codex/skills/workflow-test-fix-cycle/SKILL.md +403 -402
  113. package/README.md +14 -0
  114. package/ccw/dist/cli.d.ts.map +1 -1
  115. package/ccw/dist/cli.js +16 -0
  116. package/ccw/dist/cli.js.map +1 -1
  117. package/ccw/dist/commands/chain-loader.d.ts +2 -0
  118. package/ccw/dist/commands/chain-loader.d.ts.map +1 -0
  119. package/ccw/dist/commands/chain-loader.js +11 -0
  120. package/ccw/dist/commands/chain-loader.js.map +1 -0
  121. package/ccw/dist/commands/install.d.ts.map +1 -1
  122. package/ccw/dist/commands/install.js +52 -1
  123. package/ccw/dist/commands/install.js.map +1 -1
  124. package/ccw/dist/commands/launcher.d.ts +2 -0
  125. package/ccw/dist/commands/launcher.d.ts.map +1 -0
  126. package/ccw/dist/commands/launcher.js +434 -0
  127. package/ccw/dist/commands/launcher.js.map +1 -0
  128. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -1
  129. package/ccw/dist/core/routes/litellm-api-routes.js +0 -23
  130. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -1
  131. package/ccw/dist/tools/chain-loader.d.ts +10 -0
  132. package/ccw/dist/tools/chain-loader.d.ts.map +1 -0
  133. package/ccw/dist/tools/chain-loader.js +1054 -0
  134. package/ccw/dist/tools/chain-loader.js.map +1 -0
  135. package/ccw/dist/tools/index.d.ts.map +1 -1
  136. package/ccw/dist/tools/index.js +2 -0
  137. package/ccw/dist/tools/index.js.map +1 -1
  138. package/ccw/dist/tools/json-builder.js +20 -0
  139. package/ccw/dist/tools/json-builder.js.map +1 -1
  140. package/ccw/dist/tools/skill-context-loader.d.ts.map +1 -1
  141. package/ccw/dist/tools/skill-context-loader.js +12 -26
  142. package/ccw/dist/tools/skill-context-loader.js.map +1 -1
  143. package/ccw/dist/types/chain-types.d.ts +112 -0
  144. package/ccw/dist/types/chain-types.d.ts.map +1 -0
  145. package/ccw/dist/types/chain-types.js +5 -0
  146. package/ccw/dist/types/chain-types.js.map +1 -0
  147. package/ccw/dist/utils/chain-visualizer.d.ts +13 -0
  148. package/ccw/dist/utils/chain-visualizer.d.ts.map +1 -0
  149. package/ccw/dist/utils/chain-visualizer.js +164 -0
  150. package/ccw/dist/utils/chain-visualizer.js.map +1 -0
  151. package/ccw/scripts/prepublish-clean.mjs +0 -1
  152. package/package.json +1 -3
  153. package/.claude/commands/cli/cli-init.md +0 -441
  154. package/.claude/commands/cli/codex-review.md +0 -361
  155. package/.claude/commands/flow-create.md +0 -663
  156. package/.claude/skills/team-edict.zip +0 -0
  157. package/ccw-litellm/README.md +0 -180
  158. package/ccw-litellm/pyproject.toml +0 -35
  159. package/ccw-litellm/src/ccw_litellm/__init__.py +0 -47
  160. package/ccw-litellm/src/ccw_litellm/cli.py +0 -108
  161. package/ccw-litellm/src/ccw_litellm/clients/__init__.py +0 -12
  162. package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +0 -270
  163. package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +0 -198
  164. package/ccw-litellm/src/ccw_litellm/config/__init__.py +0 -22
  165. package/ccw-litellm/src/ccw_litellm/config/loader.py +0 -343
  166. package/ccw-litellm/src/ccw_litellm/config/models.py +0 -162
  167. package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +0 -14
  168. package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +0 -52
  169. package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +0 -45
@@ -0,0 +1,1054 @@
1
+ /**
2
+ * Chain Loader Tool - Progressive skill chain execution with step-by-step
3
+ * content delivery and LLM-driven decision routing.
4
+ *
5
+ * Commands: list, start, next, done, status, content, complete
6
+ */
7
+ import { z } from 'zod';
8
+ import { renderChainTopology, renderChainProgress } from '../utils/chain-visualizer.js';
9
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, } from 'fs';
10
+ import { resolve, join, dirname } from 'path';
11
+ import { homedir } from 'os';
12
+ // ─── Params ──────────────────────────────────────────────────
13
+ const ParamsSchema = z.object({
14
+ cmd: z.enum(['list', 'start', 'next', 'done', 'status', 'content', 'complete', 'inspect', 'visualize']),
15
+ skill: z.string().optional(),
16
+ chain: z.string().optional(),
17
+ session_id: z.string().optional(),
18
+ choice: z.number().optional(),
19
+ node: z.string().optional(), // start from specific node
20
+ entry_name: z.string().optional(), // select named entry
21
+ });
22
+ // ─── Tool Schema ─────────────────────────────────────────────
23
+ export const schema = {
24
+ name: 'chain_loader',
25
+ description: `Progressive skill chain loader. Auto-detects skill from cwd (cd to skill dir first) or pass skill explicitly.
26
+ list: List chains with triggers/entries. Params: skill? (string)
27
+ inspect: Show chain node graph with topology. Params: chain (string), skill? (string)
28
+ start: Start chain session (resolves preload, initializes variables). Params: chain (string), skill? (string), node? (string), entry_name? (string)
29
+ next: Read current node (idempotent if active, advance if completed). Params: session_id (string)
30
+ done: Complete current node and advance. Params: session_id (string), choice? (number, 1-based for decisions)
31
+ status: Query session state with variables and preload info. Params: session_id (string)
32
+ content: Get all loaded content. Params: session_id (string)
33
+ complete: Mark session completed. Params: session_id (string)
34
+ visualize: Show live execution progress with chain nesting. Params: session_id (string)`,
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ cmd: { type: 'string', description: 'Command: list|inspect|start|next|done|status|content|complete|visualize' },
39
+ skill: { type: 'string', description: 'Skill name (optional, auto-detected from cwd if omitted)' },
40
+ chain: { type: 'string', description: 'Chain name (for start/inspect)' },
41
+ session_id: { type: 'string', description: 'Session ID (for next/done/status/content/complete)' },
42
+ choice: { type: 'number', description: 'Decision choice index (1-based, for done on decision nodes)' },
43
+ node: { type: 'string', description: 'Start from specific node ID (for start, bypasses entry)' },
44
+ entry_name: { type: 'string', description: 'Named entry point (for start, looks up in chain entries)' },
45
+ },
46
+ required: ['cmd'],
47
+ },
48
+ };
49
+ // ─── Constants ───────────────────────────────────────────────
50
+ const SESSIONS_BASE = '.workflow/.chain-sessions';
51
+ // ─── Handler ─────────────────────────────────────────────────
52
+ export async function handler(params) {
53
+ const parsed = ParamsSchema.safeParse(params);
54
+ if (!parsed.success) {
55
+ return { success: false, error: `Invalid params: ${parsed.error.message}` };
56
+ }
57
+ const p = parsed.data;
58
+ try {
59
+ switch (p.cmd) {
60
+ case 'list': return cmdList(p);
61
+ case 'inspect': return cmdInspect(p);
62
+ case 'start': return cmdStart(p);
63
+ case 'next': return cmdNext(p);
64
+ case 'done': return cmdDone(p);
65
+ case 'status': return cmdStatus(p);
66
+ case 'content': return cmdContent(p);
67
+ case 'complete': return cmdComplete(p);
68
+ case 'visualize': return cmdVisualize(p);
69
+ default:
70
+ return { success: false, error: `Unknown command: ${p.cmd}` };
71
+ }
72
+ }
73
+ catch (err) {
74
+ return { success: false, error: err.message };
75
+ }
76
+ }
77
+ // ─── list ────────────────────────────────────────────────────
78
+ function cmdList(p) {
79
+ // If no skill given, try cwd auto-detect first; fall back to scanning all
80
+ const cwdSkill = !p.skill ? detectSkillFromCwd() : null;
81
+ const skillDirs = cwdSkill ? [cwdSkill] : discoverSkillDirs(p.skill);
82
+ const results = [];
83
+ for (const { skillName, skillPath } of skillDirs) {
84
+ const chainsDir = join(skillPath, 'chains');
85
+ if (!existsSync(chainsDir))
86
+ continue;
87
+ const files = readdirSync(chainsDir).filter(f => f.endsWith('.json'));
88
+ for (const file of files) {
89
+ try {
90
+ const chain = loadChainJson(join(chainsDir, file));
91
+ results.push({
92
+ skill: skillName,
93
+ chain_id: chain.chain_id,
94
+ name: chain.name,
95
+ description: chain.description,
96
+ node_count: Object.keys(chain.nodes).length,
97
+ ...(chain.triggers ? { triggers: chain.triggers } : {}),
98
+ ...(chain.entries ? { entries: chain.entries } : {}),
99
+ });
100
+ }
101
+ catch { /* skip invalid chain files */ }
102
+ }
103
+ }
104
+ return { success: true, result: { chains: results, total: results.length } };
105
+ }
106
+ // ─── inspect ────────────────────────────────────────────────
107
+ function cmdInspect(p) {
108
+ if (!p.chain)
109
+ return { success: false, error: 'chain is required for inspect' };
110
+ const resolved = resolveSkill(p.skill);
111
+ if (!resolved) {
112
+ return { success: false, error: p.skill
113
+ ? `Skill not found: ${p.skill}`
114
+ : 'Cannot auto-detect skill from cwd. Pass skill explicitly.' };
115
+ }
116
+ const { skillName, skillPath } = resolved;
117
+ let chainPath = join(skillPath, 'chains', `${p.chain}.json`);
118
+ if (!existsSync(chainPath)) {
119
+ const crossPath = findChainAcrossSkills(p.chain);
120
+ if (!crossPath) {
121
+ return { success: false, error: `Chain not found: ${p.chain} in skill ${skillName}` };
122
+ }
123
+ chainPath = crossPath;
124
+ }
125
+ const chain = loadChainJson(chainPath);
126
+ const nodeList = Object.entries(chain.nodes).map(([id, node]) => {
127
+ const edges = [];
128
+ if (node.type === 'step') {
129
+ if (node.next)
130
+ edges.push(node.next);
131
+ }
132
+ else if (node.type === 'decision') {
133
+ for (const c of node.choices)
134
+ edges.push(c.next);
135
+ }
136
+ else if (node.type === 'delegate') {
137
+ if (node.next)
138
+ edges.push(node.next);
139
+ edges.push(`[delegate:${node.chain}]`);
140
+ }
141
+ return { id, type: node.type, name: node.name, next: edges };
142
+ });
143
+ return {
144
+ success: true,
145
+ result: {
146
+ chain_id: chain.chain_id,
147
+ name: chain.name,
148
+ description: chain.description,
149
+ ...(chain.triggers ? { triggers: chain.triggers } : {}),
150
+ entries: chain.entries || (chain.entry ? [{ name: 'default', node: chain.entry, description: 'Default entry' }] : []),
151
+ node_count: nodeList.length,
152
+ nodes: nodeList,
153
+ topology: renderChainTopology(chain),
154
+ },
155
+ };
156
+ }
157
+ // ─── start ───────────────────────────────────────────────────
158
+ function cmdStart(p) {
159
+ if (!p.chain)
160
+ return { success: false, error: 'chain is required for start' };
161
+ // Resolve skill: explicit param → cwd auto-detect → error
162
+ const resolved = resolveSkill(p.skill);
163
+ if (!resolved) {
164
+ return { success: false, error: p.skill
165
+ ? `Skill not found: ${p.skill}`
166
+ : 'Cannot auto-detect skill from cwd. Pass skill explicitly or cd to a skill directory (must contain chains/ dir).' };
167
+ }
168
+ const { skillName, skillPath } = resolved;
169
+ let chainPath = join(skillPath, 'chains', `${p.chain}.json`);
170
+ if (!existsSync(chainPath)) {
171
+ const crossPath = findChainAcrossSkills(p.chain);
172
+ if (!crossPath) {
173
+ return { success: false, error: `Chain not found: ${p.chain} in skill ${skillName}` };
174
+ }
175
+ chainPath = crossPath;
176
+ }
177
+ const chain = loadChainJson(chainPath);
178
+ // Resolve entry point: explicit node > entry_name > entries[0] > entry
179
+ const startNode = p.node
180
+ || (p.entry_name && chain.entries?.find(e => e.name === p.entry_name)?.node)
181
+ || (chain.entries?.[0]?.node)
182
+ || chain.entry;
183
+ if (!startNode) {
184
+ return { success: false, error: 'No entry point found. Provide node, entry_name, or define entry/entries in chain.' };
185
+ }
186
+ if (!chain.nodes[startNode]) {
187
+ return { success: false, error: `Node "${startNode}" not found in chain "${chain.chain_id}"` };
188
+ }
189
+ // Generate session ID
190
+ const now = new Date();
191
+ const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '');
192
+ const sessionId = `CL-${skillName}-${timeStr}`;
193
+ // Initialize variables from chain defaults
194
+ const variables = {};
195
+ if (chain.variables) {
196
+ for (const [key, def] of Object.entries(chain.variables)) {
197
+ if (def.default !== undefined)
198
+ variables[key] = def.default;
199
+ }
200
+ }
201
+ // Resolve preloads (normalize object format { key: source } to array format)
202
+ const preloaded = resolvePreloads(normalizePreload(chain.preload), skillPath);
203
+ // Create session
204
+ const session = {
205
+ session_id: sessionId,
206
+ skill_name: skillName,
207
+ skill_path: skillPath,
208
+ status: 'active',
209
+ current_chain: chain.chain_id,
210
+ current_node: startNode,
211
+ node_status: 'active',
212
+ chain_stack: [],
213
+ history: [],
214
+ loaded_content: [],
215
+ variables,
216
+ preloaded,
217
+ started_at: now.toISOString(),
218
+ updated_at: now.toISOString(),
219
+ };
220
+ // Load entry node content
221
+ const entryNode = chain.nodes[startNode];
222
+ const nodeContent = loadNodeContent(entryNode, skillPath);
223
+ // Record in history
224
+ session.history.push({
225
+ node_id: startNode,
226
+ chain_id: chain.chain_id,
227
+ node_type: entryNode.type,
228
+ node_status: 'active',
229
+ timestamp: now.toISOString(),
230
+ });
231
+ // Add to loaded content
232
+ if (nodeContent) {
233
+ session.loaded_content.push({
234
+ node_id: startNode,
235
+ chain_id: chain.chain_id,
236
+ content: nodeContent,
237
+ loaded_at: now.toISOString(),
238
+ });
239
+ }
240
+ saveSession(session);
241
+ return {
242
+ success: true,
243
+ result: {
244
+ session_id: sessionId,
245
+ current_chain: chain.chain_id,
246
+ current_node: startNode,
247
+ node_status: 'active',
248
+ preloaded_keys: preloaded.map(p => p.key),
249
+ variables,
250
+ ...formatNodeOutput(entryNode, nodeContent),
251
+ },
252
+ };
253
+ }
254
+ // ─── next ────────────────────────────────────────────────────
255
+ function cmdNext(p) {
256
+ if (!p.session_id)
257
+ return { success: false, error: 'session_id is required for next' };
258
+ const session = loadSession(p.session_id);
259
+ if (!session)
260
+ return { success: false, error: `Session not found: ${p.session_id}` };
261
+ if (session.status === 'completed') {
262
+ return { success: true, result: { status: 'completed', message: 'Session already completed.' } };
263
+ }
264
+ // If current node is active → return current content (idempotent)
265
+ if (session.node_status === 'active') {
266
+ const chain = loadChainFromSession(session);
267
+ const node = chain.nodes[session.current_node];
268
+ const content = getLastLoadedContent(session, session.current_node);
269
+ return {
270
+ success: true,
271
+ result: {
272
+ session_id: session.session_id,
273
+ current_chain: session.current_chain,
274
+ current_node: session.current_node,
275
+ node_status: 'active',
276
+ ...formatNodeOutput(node, content),
277
+ },
278
+ };
279
+ }
280
+ // If current node is completed → auto-advance (same as done without choice)
281
+ return advanceToNext(session, undefined);
282
+ }
283
+ // ─── done ────────────────────────────────────────────────────
284
+ function cmdDone(p) {
285
+ if (!p.session_id)
286
+ return { success: false, error: 'session_id is required for done' };
287
+ const session = loadSession(p.session_id);
288
+ if (!session)
289
+ return { success: false, error: `Session not found: ${p.session_id}` };
290
+ if (session.status === 'completed') {
291
+ return { success: true, result: { status: 'completed', message: 'Session already completed.' } };
292
+ }
293
+ // Mark current node as completed
294
+ session.node_status = 'completed';
295
+ session.history.push({
296
+ node_id: session.current_node,
297
+ chain_id: session.current_chain,
298
+ node_type: getNodeType(session),
299
+ node_status: 'completed',
300
+ timestamp: new Date().toISOString(),
301
+ choice: p.choice,
302
+ });
303
+ return advanceToNext(session, p.choice);
304
+ }
305
+ // ─── status ──────────────────────────────────────────────────
306
+ function cmdStatus(p) {
307
+ if (!p.session_id)
308
+ return { success: false, error: 'session_id is required for status' };
309
+ const session = loadSession(p.session_id);
310
+ if (!session)
311
+ return { success: false, error: `Session not found: ${p.session_id}` };
312
+ // Count total nodes in current chain
313
+ let currentChainTotalNodes = 0;
314
+ try {
315
+ const chain = loadChainFromSession(session);
316
+ currentChainTotalNodes = Object.keys(chain.nodes).length;
317
+ }
318
+ catch { /* skip */ }
319
+ return {
320
+ success: true,
321
+ result: {
322
+ session_id: session.session_id,
323
+ skill_name: session.skill_name,
324
+ status: session.status,
325
+ current_chain: session.current_chain,
326
+ current_node: session.current_node,
327
+ node_status: session.node_status,
328
+ nesting_depth: session.chain_stack.length,
329
+ current_chain_total_nodes: currentChainTotalNodes,
330
+ chain_stack_depth: session.chain_stack.length,
331
+ history_length: session.history.length,
332
+ loaded_count: session.loaded_content.length,
333
+ preloaded_keys: (session.preloaded || []).map(p => p.key),
334
+ variables: session.variables || {},
335
+ started_at: session.started_at,
336
+ updated_at: session.updated_at,
337
+ },
338
+ };
339
+ }
340
+ // ─── content ─────────────────────────────────────────────────
341
+ function cmdContent(p) {
342
+ if (!p.session_id)
343
+ return { success: false, error: 'session_id is required for content' };
344
+ const session = loadSession(p.session_id);
345
+ if (!session)
346
+ return { success: false, error: `Session not found: ${p.session_id}` };
347
+ return {
348
+ success: true,
349
+ result: {
350
+ session_id: session.session_id,
351
+ entries: session.loaded_content,
352
+ total: session.loaded_content.length,
353
+ },
354
+ };
355
+ }
356
+ // ─── complete ────────────────────────────────────────────────
357
+ function cmdComplete(p) {
358
+ if (!p.session_id)
359
+ return { success: false, error: 'session_id is required for complete' };
360
+ const session = loadSession(p.session_id);
361
+ if (!session)
362
+ return { success: false, error: `Session not found: ${p.session_id}` };
363
+ session.status = 'completed';
364
+ session.updated_at = new Date().toISOString();
365
+ saveSession(session);
366
+ const stepCount = session.history.filter(h => h.node_type === 'step' && h.node_status === 'completed').length;
367
+ const decisionCount = session.history.filter(h => h.node_type === 'decision' && h.node_status === 'completed').length;
368
+ return {
369
+ success: true,
370
+ result: {
371
+ status: 'completed',
372
+ session_id: session.session_id,
373
+ summary: `Chain completed. ${stepCount} steps, ${decisionCount} decisions.`,
374
+ loaded_content_count: session.loaded_content.length,
375
+ },
376
+ };
377
+ }
378
+ // ─── Core Logic ──────────────────────────────────────────────
379
+ function advanceToNext(session, choice) {
380
+ const chain = loadChainFromSession(session);
381
+ const currentNode = chain.nodes[session.current_node];
382
+ if (!currentNode) {
383
+ return { success: false, error: `Node "${session.current_node}" not found in chain "${session.current_chain}"` };
384
+ }
385
+ // Determine next node ID
386
+ let nextNodeId = null;
387
+ if (currentNode.type === 'step') {
388
+ nextNodeId = currentNode.next;
389
+ }
390
+ else if (currentNode.type === 'decision') {
391
+ if (choice !== undefined && choice >= 1 && choice <= currentNode.choices.length) {
392
+ nextNodeId = currentNode.choices[choice - 1].next;
393
+ }
394
+ else {
395
+ nextNodeId = currentNode.default;
396
+ }
397
+ }
398
+ else if (currentNode.type === 'delegate') {
399
+ // Push current chain onto stack and switch to sub-chain
400
+ const subChainId = currentNode.chain;
401
+ let subChainPath = join(session.skill_path, 'chains', `${subChainId}.json`);
402
+ if (!existsSync(subChainPath)) {
403
+ const crossPath = findChainAcrossSkills(subChainId);
404
+ if (!crossPath) {
405
+ return { success: false, error: `Delegate chain not found: ${subChainId}` };
406
+ }
407
+ subChainPath = crossPath;
408
+ }
409
+ const subChain = loadChainJson(subChainPath);
410
+ // Snapshot parent variables and push frame
411
+ const variablesSnapshot = { ...(session.variables || {}) };
412
+ session.chain_stack.push({
413
+ chain_id: session.current_chain,
414
+ return_node: currentNode.next || null,
415
+ variables_snapshot: variablesSnapshot,
416
+ });
417
+ // Pass variables to child scope
418
+ const childVars = {};
419
+ // Initialize from child chain defaults
420
+ if (subChain.variables) {
421
+ for (const [key, def] of Object.entries(subChain.variables)) {
422
+ if (def.default !== undefined)
423
+ childVars[key] = def.default;
424
+ }
425
+ }
426
+ // Override with passed parent variables
427
+ const keysToPass = currentNode.pass_variables || Object.keys(session.variables || {});
428
+ for (const key of keysToPass) {
429
+ if (key in (session.variables || {})) {
430
+ childVars[key] = session.variables[key];
431
+ }
432
+ }
433
+ session.variables = childVars;
434
+ // Resolve entry: entry_name from delegate node > chain entries > chain entry
435
+ let subEntry;
436
+ if (currentNode.entry_name && subChain.entries) {
437
+ const found = subChain.entries.find(e => e.name === currentNode.entry_name);
438
+ subEntry = found?.node || resolveChainEntry(subChain);
439
+ }
440
+ else {
441
+ subEntry = resolveChainEntry(subChain);
442
+ }
443
+ session.current_chain = subChain.chain_id;
444
+ session.current_node = subEntry;
445
+ session.node_status = 'active';
446
+ session.updated_at = new Date().toISOString();
447
+ const entryNode = subChain.nodes[subEntry];
448
+ const content = loadNodeContent(entryNode, session.skill_path);
449
+ session.history.push({
450
+ node_id: subEntry,
451
+ chain_id: subChain.chain_id,
452
+ node_type: entryNode.type,
453
+ node_status: 'active',
454
+ timestamp: new Date().toISOString(),
455
+ });
456
+ if (content) {
457
+ session.loaded_content.push({
458
+ node_id: subEntry,
459
+ chain_id: subChain.chain_id,
460
+ content,
461
+ loaded_at: new Date().toISOString(),
462
+ });
463
+ }
464
+ saveSession(session);
465
+ return {
466
+ success: true,
467
+ result: {
468
+ session_id: session.session_id,
469
+ current_chain: session.current_chain,
470
+ current_node: session.current_node,
471
+ node_status: 'active',
472
+ delegate_depth: session.chain_stack.length,
473
+ variables: session.variables,
474
+ ...formatNodeOutput(entryNode, content),
475
+ },
476
+ };
477
+ }
478
+ // Handle cross-chain routing (→chain-name)
479
+ if (nextNodeId && nextNodeId.startsWith('→')) {
480
+ const targetChainId = nextNodeId.slice(1);
481
+ let targetChainPath = join(session.skill_path, 'chains', `${targetChainId}.json`);
482
+ if (!existsSync(targetChainPath)) {
483
+ const crossPath = findChainAcrossSkills(targetChainId);
484
+ if (!crossPath) {
485
+ return { success: false, error: `Cross-chain target not found: ${targetChainId}` };
486
+ }
487
+ targetChainPath = crossPath;
488
+ }
489
+ const targetChain = loadChainJson(targetChainPath);
490
+ const targetEntry = resolveChainEntry(targetChain);
491
+ session.current_chain = targetChain.chain_id;
492
+ session.current_node = targetEntry;
493
+ session.node_status = 'active';
494
+ session.updated_at = new Date().toISOString();
495
+ const entryNode = targetChain.nodes[targetEntry];
496
+ const content = loadNodeContent(entryNode, session.skill_path);
497
+ session.history.push({
498
+ node_id: targetEntry,
499
+ chain_id: targetChain.chain_id,
500
+ node_type: entryNode.type,
501
+ node_status: 'active',
502
+ timestamp: new Date().toISOString(),
503
+ });
504
+ if (content) {
505
+ session.loaded_content.push({
506
+ node_id: targetEntry,
507
+ chain_id: targetChain.chain_id,
508
+ content,
509
+ loaded_at: new Date().toISOString(),
510
+ });
511
+ }
512
+ saveSession(session);
513
+ return {
514
+ success: true,
515
+ result: {
516
+ session_id: session.session_id,
517
+ current_chain: session.current_chain,
518
+ current_node: session.current_node,
519
+ node_status: 'active',
520
+ routed_from: chain.chain_id,
521
+ ...formatNodeOutput(entryNode, content),
522
+ },
523
+ };
524
+ }
525
+ // next is null → check chain stack for return
526
+ if (nextNodeId === null || nextNodeId === undefined) {
527
+ if (session.chain_stack.length > 0) {
528
+ const frame = session.chain_stack.pop();
529
+ if (frame.return_node) {
530
+ // Return to parent chain's return node
531
+ let parentChainPath = join(session.skill_path, 'chains', `${frame.chain_id}.json`);
532
+ if (!existsSync(parentChainPath)) {
533
+ const crossPath = findChainAcrossSkills(frame.chain_id);
534
+ if (crossPath)
535
+ parentChainPath = crossPath;
536
+ }
537
+ const parentChain = loadChainJson(parentChainPath);
538
+ // Receive variables from child back to parent
539
+ const childVars = { ...(session.variables || {}) };
540
+ const parentVars = { ...(frame.variables_snapshot || {}) };
541
+ // Find the delegate node that spawned this child to get receive_variables
542
+ const delegateNode = findDelegateNodeForChain(parentChain, session.current_chain);
543
+ const receiveKeys = delegateNode?.receive_variables;
544
+ if (receiveKeys) {
545
+ for (const key of receiveKeys) {
546
+ if (key in childVars)
547
+ parentVars[key] = childVars[key];
548
+ }
549
+ }
550
+ session.variables = parentVars;
551
+ session.current_chain = frame.chain_id;
552
+ session.current_node = frame.return_node;
553
+ session.node_status = 'active';
554
+ session.updated_at = new Date().toISOString();
555
+ const returnNode = parentChain.nodes[frame.return_node];
556
+ const content = loadNodeContent(returnNode, session.skill_path);
557
+ session.history.push({
558
+ node_id: frame.return_node,
559
+ chain_id: frame.chain_id,
560
+ node_type: returnNode.type,
561
+ node_status: 'active',
562
+ timestamp: new Date().toISOString(),
563
+ });
564
+ if (content) {
565
+ session.loaded_content.push({
566
+ node_id: frame.return_node,
567
+ chain_id: frame.chain_id,
568
+ content,
569
+ loaded_at: new Date().toISOString(),
570
+ });
571
+ }
572
+ saveSession(session);
573
+ return {
574
+ success: true,
575
+ result: {
576
+ session_id: session.session_id,
577
+ current_chain: session.current_chain,
578
+ current_node: session.current_node,
579
+ node_status: 'active',
580
+ returned_from_delegate: true,
581
+ variables: session.variables,
582
+ ...formatNodeOutput(returnNode, content),
583
+ },
584
+ };
585
+ }
586
+ }
587
+ // No more nodes, no more stack → session completed
588
+ session.status = 'completed';
589
+ session.updated_at = new Date().toISOString();
590
+ saveSession(session);
591
+ const stepCount = session.history.filter(h => h.node_type === 'step' && h.node_status === 'completed').length;
592
+ const decisionCount = session.history.filter(h => h.node_type === 'decision' && h.node_status === 'completed').length;
593
+ return {
594
+ success: true,
595
+ result: {
596
+ status: 'completed',
597
+ session_id: session.session_id,
598
+ summary: `Chain ${session.current_chain} completed. ${stepCount} steps, ${decisionCount} decisions.`,
599
+ },
600
+ };
601
+ }
602
+ // Advance to next node in current chain
603
+ const nextNode = chain.nodes[nextNodeId];
604
+ if (!nextNode) {
605
+ return { success: false, error: `Next node "${nextNodeId}" not found in chain "${session.current_chain}"` };
606
+ }
607
+ session.current_node = nextNodeId;
608
+ session.node_status = 'active';
609
+ session.updated_at = new Date().toISOString();
610
+ const content = loadNodeContent(nextNode, session.skill_path);
611
+ session.history.push({
612
+ node_id: nextNodeId,
613
+ chain_id: session.current_chain,
614
+ node_type: nextNode.type,
615
+ node_status: 'active',
616
+ timestamp: new Date().toISOString(),
617
+ });
618
+ if (content) {
619
+ session.loaded_content.push({
620
+ node_id: nextNodeId,
621
+ chain_id: session.current_chain,
622
+ content,
623
+ loaded_at: new Date().toISOString(),
624
+ });
625
+ }
626
+ saveSession(session);
627
+ return {
628
+ success: true,
629
+ result: {
630
+ session_id: session.session_id,
631
+ current_chain: session.current_chain,
632
+ current_node: nextNodeId,
633
+ node_status: 'active',
634
+ ...formatNodeOutput(nextNode, content),
635
+ },
636
+ };
637
+ }
638
+ // ─── Entry Resolver ─────────────────────────────────────────
639
+ function resolveChainEntry(chain) {
640
+ return chain.entries?.[0]?.node || chain.entry || Object.keys(chain.nodes)[0];
641
+ }
642
+ // ─── Node Helpers ────────────────────────────────────────────
643
+ function loadNodeContent(node, skillPath) {
644
+ if (node.type === 'step') {
645
+ const parts = [];
646
+ // 1. content_files: multiple file references, resolved in order
647
+ if (node.content_files?.length) {
648
+ for (const ref of node.content_files) {
649
+ const content = resolveFileRef(ref, skillPath);
650
+ if (content !== null)
651
+ parts.push(content);
652
+ }
653
+ }
654
+ // 2. content_ref: single file reference (backward compat)
655
+ if (node.content_ref) {
656
+ const content = resolveFileRef(node.content_ref, skillPath);
657
+ if (content !== null)
658
+ parts.push(content);
659
+ }
660
+ // 3. content_inline: direct text with embedded @ref expansion
661
+ if (node.content_inline) {
662
+ parts.push(resolveInlineRefs(node.content_inline, skillPath));
663
+ }
664
+ return parts.length > 0 ? parts.join('\n\n') : null;
665
+ }
666
+ if (node.type === 'decision') {
667
+ return node.prompt;
668
+ }
669
+ if (node.type === 'delegate') {
670
+ return `[Delegate to chain: ${node.chain}]`;
671
+ }
672
+ return null;
673
+ }
674
+ /**
675
+ * Resolve a single @file reference to its content.
676
+ * Supports: @path/file.md (skill-relative), @~/path/file.md (home-relative)
677
+ */
678
+ function resolveFileRef(ref, skillPath) {
679
+ try {
680
+ const raw = ref.replace(/^@/, '');
681
+ if (raw.startsWith('~/')) {
682
+ const fullPath = resolve(homedir(), raw.slice(2));
683
+ if (existsSync(fullPath))
684
+ return readFileSync(fullPath, 'utf-8');
685
+ }
686
+ else if (raw.startsWith('skills/')) {
687
+ // @skills/workflow-plan/phases/01.md → .claude/skills/workflow-plan/phases/01.md
688
+ const relPath = raw.slice(7);
689
+ const projectPath = resolve(process.cwd(), '.claude', 'skills', relPath);
690
+ if (existsSync(projectPath))
691
+ return readFileSync(projectPath, 'utf-8');
692
+ const userPath = resolve(homedir(), '.claude', 'skills', relPath);
693
+ if (existsSync(userPath))
694
+ return readFileSync(userPath, 'utf-8');
695
+ }
696
+ else if (raw.startsWith('commands/')) {
697
+ // @commands/workflow/analyze-with-file.md → .claude/commands/workflow/analyze-with-file.md
698
+ const relPath = raw.slice(9);
699
+ const projectPath = resolve(process.cwd(), '.claude', 'commands', relPath);
700
+ if (existsSync(projectPath))
701
+ return readFileSync(projectPath, 'utf-8');
702
+ const userPath = resolve(homedir(), '.claude', 'commands', relPath);
703
+ if (existsSync(userPath))
704
+ return readFileSync(userPath, 'utf-8');
705
+ }
706
+ else {
707
+ const fullPath = resolve(skillPath, raw);
708
+ if (existsSync(fullPath))
709
+ return readFileSync(fullPath, 'utf-8');
710
+ }
711
+ return `[Content not found: ${ref}]`;
712
+ }
713
+ catch {
714
+ return `[Error loading: ${ref}]`;
715
+ }
716
+ }
717
+ /**
718
+ * Expand embedded @file references in inline content.
719
+ * Pattern: @path/to/file.md (must end with file extension)
720
+ * Supports: @skill-relative/path.md, @~/home-relative/path.md
721
+ * Skips @-references inside code blocks.
722
+ */
723
+ function resolveInlineRefs(text, skillPath) {
724
+ // Match @skills/path.ext, @~/path/file.ext, or @path/file.ext — must have at least one / and end with .ext
725
+ return text.replace(/@((?:skills|commands|~)?\/[\w./-]+\.\w+)/g, (match, refPath) => {
726
+ try {
727
+ let fullPath;
728
+ if (refPath.startsWith('skills/')) {
729
+ const relPath = refPath.slice(7);
730
+ const projectPath = resolve(process.cwd(), '.claude', 'skills', relPath);
731
+ if (existsSync(projectPath))
732
+ return readFileSync(projectPath, 'utf-8');
733
+ const userPath = resolve(homedir(), '.claude', 'skills', relPath);
734
+ if (existsSync(userPath))
735
+ return readFileSync(userPath, 'utf-8');
736
+ return match;
737
+ }
738
+ else if (refPath.startsWith('commands/')) {
739
+ const relPath = refPath.slice(9);
740
+ const projectPath = resolve(process.cwd(), '.claude', 'commands', relPath);
741
+ if (existsSync(projectPath))
742
+ return readFileSync(projectPath, 'utf-8');
743
+ const userPath = resolve(homedir(), '.claude', 'commands', relPath);
744
+ if (existsSync(userPath))
745
+ return readFileSync(userPath, 'utf-8');
746
+ return match;
747
+ }
748
+ else if (refPath.startsWith('~/')) {
749
+ fullPath = resolve(homedir(), refPath.slice(2));
750
+ }
751
+ else {
752
+ fullPath = resolve(skillPath, refPath.startsWith('/') ? refPath.slice(1) : refPath);
753
+ }
754
+ if (existsSync(fullPath))
755
+ return readFileSync(fullPath, 'utf-8');
756
+ }
757
+ catch { /* fall through */ }
758
+ return match; // leave unresolved references as-is
759
+ });
760
+ }
761
+ function formatNodeOutput(node, content) {
762
+ const output = {
763
+ type: node.type,
764
+ name: node.name,
765
+ };
766
+ if (node.type === 'step') {
767
+ output.content = content;
768
+ }
769
+ else if (node.type === 'decision') {
770
+ output.prompt = node.prompt;
771
+ output.choices = node.choices.map((c, i) => ({
772
+ index: i + 1,
773
+ label: c.label,
774
+ description: c.description,
775
+ next: c.next,
776
+ }));
777
+ output.default = node.default;
778
+ }
779
+ else if (node.type === 'delegate') {
780
+ output.chain = node.chain;
781
+ output.content = content;
782
+ }
783
+ return output;
784
+ }
785
+ function getNodeType(session) {
786
+ try {
787
+ const chain = loadChainFromSession(session);
788
+ const node = chain.nodes[session.current_node];
789
+ return node?.type || 'step';
790
+ }
791
+ catch {
792
+ return 'step';
793
+ }
794
+ }
795
+ function getLastLoadedContent(session, nodeId) {
796
+ for (let i = session.loaded_content.length - 1; i >= 0; i--) {
797
+ if (session.loaded_content[i].node_id === nodeId) {
798
+ return session.loaded_content[i].content;
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+ // ─── Chain/Session I/O ───────────────────────────────────────
804
+ function loadChainJson(filePath) {
805
+ const raw = readFileSync(filePath, 'utf-8');
806
+ return JSON.parse(raw);
807
+ }
808
+ function loadChainFromSession(session) {
809
+ const chainPath = join(session.skill_path, 'chains', `${session.current_chain}.json`);
810
+ if (existsSync(chainPath))
811
+ return loadChainJson(chainPath);
812
+ const crossPath = findChainAcrossSkills(session.current_chain);
813
+ if (crossPath)
814
+ return loadChainJson(crossPath);
815
+ throw new Error(`Chain file not found: ${chainPath}`);
816
+ }
817
+ function getSessionDir(sessionId) {
818
+ return resolve(process.cwd(), SESSIONS_BASE, sessionId);
819
+ }
820
+ function saveSession(session) {
821
+ const dir = getSessionDir(session.session_id);
822
+ if (!existsSync(dir)) {
823
+ mkdirSync(dir, { recursive: true });
824
+ }
825
+ writeFileSync(join(dir, 'state.json'), JSON.stringify(session, null, 2), 'utf-8');
826
+ }
827
+ function loadSession(sessionId) {
828
+ const statePath = join(getSessionDir(sessionId), 'state.json');
829
+ if (!existsSync(statePath))
830
+ return null;
831
+ return JSON.parse(readFileSync(statePath, 'utf-8'));
832
+ }
833
+ /**
834
+ * Auto-detect skill from cwd.
835
+ * Checks if cwd (or a parent up to 3 levels) has a chains/ subdirectory.
836
+ * e.g. cwd = .claude/skills/investigate → detect "investigate"
837
+ */
838
+ function detectSkillFromCwd() {
839
+ let dir = process.cwd();
840
+ for (let i = 0; i < 4; i++) {
841
+ if (existsSync(join(dir, 'chains'))) {
842
+ const name = dir.replace(/[\\/]+$/, '').split(/[\\/]/).pop();
843
+ return { skillName: name, skillPath: dir };
844
+ }
845
+ const parent = dirname(dir);
846
+ if (parent === dir)
847
+ break;
848
+ dir = parent;
849
+ }
850
+ return null;
851
+ }
852
+ /**
853
+ * Resolve skill: explicit name → cwd auto-detect → null
854
+ */
855
+ function resolveSkill(skillName) {
856
+ if (skillName) {
857
+ const path = findSkillPath(skillName);
858
+ return path ? { skillName, skillPath: path } : null;
859
+ }
860
+ return detectSkillFromCwd();
861
+ }
862
+ function discoverSkillDirs(filterSkill) {
863
+ const results = [];
864
+ const searchLocations = [
865
+ join(process.cwd(), '.claude', 'skills'),
866
+ join(process.cwd(), '.claude', 'workflow-skills'),
867
+ join(homedir(), '.claude', 'skills'),
868
+ join(homedir(), '.claude', 'workflow-skills'),
869
+ ];
870
+ for (const base of searchLocations) {
871
+ if (!existsSync(base))
872
+ continue;
873
+ const entries = readdirSync(base, { withFileTypes: true });
874
+ for (const entry of entries) {
875
+ if (!entry.isDirectory())
876
+ continue;
877
+ if (filterSkill && entry.name !== filterSkill)
878
+ continue;
879
+ const chainsDir = join(base, entry.name, 'chains');
880
+ if (existsSync(chainsDir)) {
881
+ results.push({ skillName: entry.name, skillPath: join(base, entry.name) });
882
+ }
883
+ }
884
+ }
885
+ return results;
886
+ }
887
+ // ─── visualize ─────────────────────────────────────────────
888
+ function cmdVisualize(p) {
889
+ if (!p.session_id)
890
+ return { success: false, error: 'session_id is required for visualize' };
891
+ const session = loadSession(p.session_id);
892
+ if (!session)
893
+ return { success: false, error: `Session not found: ${p.session_id}` };
894
+ // Load all chains referenced in session (current + stack)
895
+ const chains = new Map();
896
+ const chainIds = new Set([session.current_chain]);
897
+ for (const frame of session.chain_stack)
898
+ chainIds.add(frame.chain_id);
899
+ for (const chainId of chainIds) {
900
+ try {
901
+ const chainPath = join(session.skill_path, 'chains', `${chainId}.json`);
902
+ if (existsSync(chainPath)) {
903
+ chains.set(chainId, loadChainJson(chainPath));
904
+ }
905
+ else {
906
+ const crossPath = findChainAcrossSkills(chainId);
907
+ if (crossPath)
908
+ chains.set(chainId, loadChainJson(crossPath));
909
+ }
910
+ }
911
+ catch { /* skip unloadable */ }
912
+ }
913
+ const visualization = renderChainProgress(session, chains);
914
+ const completed = session.history.filter(h => h.node_status === 'completed').length;
915
+ let total = 0;
916
+ for (const chain of chains.values())
917
+ total += Object.keys(chain.nodes).length;
918
+ return {
919
+ success: true,
920
+ result: {
921
+ visualization,
922
+ nesting_depth: session.chain_stack.length,
923
+ progress: { completed, total },
924
+ },
925
+ };
926
+ }
927
+ // ─── Preload Resolver ─────────────────────────────────────
928
+ /**
929
+ * Normalize preload from either format:
930
+ * - Array: [{ key, source, required? }] (typed format)
931
+ * - Object: { key: source } (shorthand in chain JSON)
932
+ */
933
+ function normalizePreload(preload) {
934
+ if (!preload)
935
+ return [];
936
+ if (Array.isArray(preload))
937
+ return preload;
938
+ if (typeof preload === 'object') {
939
+ return Object.entries(preload).map(([key, source]) => ({
940
+ key,
941
+ source: typeof source === 'string' ? source : String(source),
942
+ }));
943
+ }
944
+ return [];
945
+ }
946
+ function resolvePreloads(entries, skillPath) {
947
+ const results = [];
948
+ for (const entry of entries) {
949
+ const content = resolvePreloadSource(entry.source, skillPath);
950
+ if (content !== null) {
951
+ results.push({
952
+ key: entry.key,
953
+ content,
954
+ source: entry.source,
955
+ loaded_at: new Date().toISOString(),
956
+ });
957
+ }
958
+ else if (entry.required !== false) {
959
+ results.push({
960
+ key: entry.key,
961
+ content: `[WARN: Failed to load required preload: ${entry.source}]`,
962
+ source: entry.source,
963
+ loaded_at: new Date().toISOString(),
964
+ });
965
+ }
966
+ }
967
+ return results;
968
+ }
969
+ function resolvePreloadSource(source, skillPath) {
970
+ try {
971
+ if (source.startsWith('$env:')) {
972
+ const varName = source.slice(5);
973
+ return process.env[varName] || null;
974
+ }
975
+ if (source.startsWith('memory:')) {
976
+ const memFile = source.slice(7);
977
+ const memPath = resolve(process.cwd(), 'memory', memFile);
978
+ if (existsSync(memPath))
979
+ return readFileSync(memPath, 'utf-8');
980
+ return null;
981
+ }
982
+ if (source.startsWith('@~/')) {
983
+ const relPath = source.slice(3);
984
+ const fullPath = resolve(homedir(), relPath);
985
+ if (existsSync(fullPath))
986
+ return readFileSync(fullPath, 'utf-8');
987
+ return null;
988
+ }
989
+ if (source.startsWith('@skills/')) {
990
+ const relPath = source.slice(8);
991
+ const projectPath = resolve(process.cwd(), '.claude', 'skills', relPath);
992
+ if (existsSync(projectPath))
993
+ return readFileSync(projectPath, 'utf-8');
994
+ const userPath = resolve(homedir(), '.claude', 'skills', relPath);
995
+ if (existsSync(userPath))
996
+ return readFileSync(userPath, 'utf-8');
997
+ return null;
998
+ }
999
+ if (source.startsWith('@')) {
1000
+ const relPath = source.slice(1);
1001
+ const fullPath = resolve(skillPath, relPath);
1002
+ if (existsSync(fullPath))
1003
+ return readFileSync(fullPath, 'utf-8');
1004
+ return null;
1005
+ }
1006
+ return null;
1007
+ }
1008
+ catch {
1009
+ return null;
1010
+ }
1011
+ }
1012
+ // ─── Delegate Helper ─────────────────────────────────────
1013
+ function findDelegateNodeForChain(parentChain, childChainId) {
1014
+ for (const node of Object.values(parentChain.nodes)) {
1015
+ if (node.type === 'delegate' && node.chain === childChainId) {
1016
+ return node;
1017
+ }
1018
+ }
1019
+ return null;
1020
+ }
1021
+ function findSkillPath(skillName) {
1022
+ const searchLocations = [
1023
+ join(process.cwd(), '.claude', 'skills', skillName),
1024
+ join(process.cwd(), '.claude', 'workflow-skills', skillName),
1025
+ join(homedir(), '.claude', 'skills', skillName),
1026
+ join(homedir(), '.claude', 'workflow-skills', skillName),
1027
+ ];
1028
+ for (const path of searchLocations) {
1029
+ if (existsSync(path))
1030
+ return path;
1031
+ }
1032
+ return null;
1033
+ }
1034
+ function findChainAcrossSkills(chainId) {
1035
+ const bases = [
1036
+ join(process.cwd(), '.claude', 'workflow-skills'),
1037
+ join(process.cwd(), '.claude', 'skills'),
1038
+ join(homedir(), '.claude', 'workflow-skills'),
1039
+ join(homedir(), '.claude', 'skills'),
1040
+ ];
1041
+ for (const base of bases) {
1042
+ if (!existsSync(base))
1043
+ continue;
1044
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
1045
+ if (!entry.isDirectory())
1046
+ continue;
1047
+ const path = join(base, entry.name, 'chains', `${chainId}.json`);
1048
+ if (existsSync(path))
1049
+ return path;
1050
+ }
1051
+ }
1052
+ return null;
1053
+ }
1054
+ //# sourceMappingURL=chain-loader.js.map