chati-dev 1.4.0 → 2.0.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 (208) hide show
  1. package/README.md +40 -24
  2. package/framework/agents/build/dev.md +343 -0
  3. package/framework/agents/clarity/architect.md +112 -0
  4. package/framework/agents/clarity/brief.md +182 -0
  5. package/framework/agents/clarity/brownfield-wu.md +181 -0
  6. package/framework/agents/clarity/detail.md +110 -0
  7. package/framework/agents/clarity/greenfield-wu.md +153 -0
  8. package/framework/agents/clarity/ux.md +112 -0
  9. package/framework/config.yaml +3 -3
  10. package/framework/constitution.md +31 -1
  11. package/framework/context/governance.md +37 -0
  12. package/framework/context/protocols.md +34 -0
  13. package/framework/context/quality.md +27 -0
  14. package/framework/context/root.md +24 -0
  15. package/framework/data/entity-registry.yaml +1 -1
  16. package/framework/domains/agents/architect.yaml +51 -0
  17. package/framework/domains/agents/brief.yaml +47 -0
  18. package/framework/domains/agents/brownfield-wu.yaml +49 -0
  19. package/framework/domains/agents/detail.yaml +47 -0
  20. package/framework/domains/agents/dev.yaml +49 -0
  21. package/framework/domains/agents/devops.yaml +43 -0
  22. package/framework/domains/agents/greenfield-wu.yaml +47 -0
  23. package/framework/domains/agents/orchestrator.yaml +49 -0
  24. package/framework/domains/agents/phases.yaml +47 -0
  25. package/framework/domains/agents/qa-implementation.yaml +43 -0
  26. package/framework/domains/agents/qa-planning.yaml +44 -0
  27. package/framework/domains/agents/tasks.yaml +48 -0
  28. package/framework/domains/agents/ux.yaml +50 -0
  29. package/framework/domains/constitution.yaml +77 -0
  30. package/framework/domains/global.yaml +64 -0
  31. package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
  32. package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
  33. package/framework/domains/workflows/brownfield-service.yaml +22 -0
  34. package/framework/domains/workflows/brownfield-ui.yaml +22 -0
  35. package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
  36. package/framework/hooks/constitution-guard.js +101 -0
  37. package/framework/hooks/mode-governance.js +92 -0
  38. package/framework/hooks/model-governance.js +76 -0
  39. package/framework/hooks/prism-engine.js +89 -0
  40. package/framework/hooks/session-digest.js +60 -0
  41. package/framework/hooks/settings.json +44 -0
  42. package/framework/i18n/en.yaml +3 -3
  43. package/framework/i18n/es.yaml +3 -3
  44. package/framework/i18n/fr.yaml +3 -3
  45. package/framework/i18n/pt.yaml +3 -3
  46. package/framework/intelligence/decision-engine.md +1 -1
  47. package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
  48. package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
  49. package/framework/orchestrator/chati.md +284 -6
  50. package/framework/tasks/architect-api-design.md +63 -0
  51. package/framework/tasks/architect-consolidate.md +47 -0
  52. package/framework/tasks/architect-db-design.md +73 -0
  53. package/framework/tasks/architect-design.md +95 -0
  54. package/framework/tasks/architect-security-review.md +62 -0
  55. package/framework/tasks/architect-stack-selection.md +53 -0
  56. package/framework/tasks/brief-consolidate.md +249 -0
  57. package/framework/tasks/brief-constraint-identify.md +277 -0
  58. package/framework/tasks/brief-extract-requirements.md +339 -0
  59. package/framework/tasks/brief-stakeholder-map.md +176 -0
  60. package/framework/tasks/brief-validate-completeness.md +121 -0
  61. package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
  62. package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
  63. package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
  64. package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
  65. package/framework/tasks/brownfield-wu-report.md +325 -0
  66. package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
  67. package/framework/tasks/detail-acceptance-criteria.md +372 -0
  68. package/framework/tasks/detail-consolidate.md +138 -0
  69. package/framework/tasks/detail-edge-case-analysis.md +300 -0
  70. package/framework/tasks/detail-expand-prd.md +389 -0
  71. package/framework/tasks/detail-nfr-extraction.md +223 -0
  72. package/framework/tasks/dev-code-review.md +404 -0
  73. package/framework/tasks/dev-consolidate.md +543 -0
  74. package/framework/tasks/dev-debug.md +322 -0
  75. package/framework/tasks/dev-implement.md +252 -0
  76. package/framework/tasks/dev-iterate.md +411 -0
  77. package/framework/tasks/dev-pr-prepare.md +497 -0
  78. package/framework/tasks/dev-refactor.md +342 -0
  79. package/framework/tasks/dev-test-write.md +306 -0
  80. package/framework/tasks/devops-ci-setup.md +412 -0
  81. package/framework/tasks/devops-consolidate.md +712 -0
  82. package/framework/tasks/devops-deploy-config.md +598 -0
  83. package/framework/tasks/devops-monitoring-setup.md +658 -0
  84. package/framework/tasks/devops-release-prepare.md +673 -0
  85. package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
  86. package/framework/tasks/greenfield-wu-report.md +266 -0
  87. package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
  88. package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
  89. package/framework/tasks/orchestrator-deviation.md +260 -0
  90. package/framework/tasks/orchestrator-escalate.md +276 -0
  91. package/framework/tasks/orchestrator-handoff.md +243 -0
  92. package/framework/tasks/orchestrator-health.md +372 -0
  93. package/framework/tasks/orchestrator-mode-switch.md +262 -0
  94. package/framework/tasks/orchestrator-resume.md +189 -0
  95. package/framework/tasks/orchestrator-route.md +169 -0
  96. package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
  97. package/framework/tasks/orchestrator-status.md +260 -0
  98. package/framework/tasks/orchestrator-suggest-mode.md +372 -0
  99. package/framework/tasks/phases-breakdown.md +91 -0
  100. package/framework/tasks/phases-dependency-mapping.md +67 -0
  101. package/framework/tasks/phases-mvp-scoping.md +94 -0
  102. package/framework/tasks/qa-impl-consolidate.md +522 -0
  103. package/framework/tasks/qa-impl-performance-test.md +487 -0
  104. package/framework/tasks/qa-impl-regression-check.md +413 -0
  105. package/framework/tasks/qa-impl-sast-scan.md +402 -0
  106. package/framework/tasks/qa-impl-test-execute.md +344 -0
  107. package/framework/tasks/qa-impl-verdict.md +339 -0
  108. package/framework/tasks/qa-planning-consolidate.md +309 -0
  109. package/framework/tasks/qa-planning-coverage-plan.md +338 -0
  110. package/framework/tasks/qa-planning-gate-define.md +339 -0
  111. package/framework/tasks/qa-planning-risk-matrix.md +631 -0
  112. package/framework/tasks/qa-planning-test-strategy.md +217 -0
  113. package/framework/tasks/tasks-acceptance-write.md +75 -0
  114. package/framework/tasks/tasks-consolidate.md +57 -0
  115. package/framework/tasks/tasks-decompose.md +80 -0
  116. package/framework/tasks/tasks-estimate.md +66 -0
  117. package/framework/tasks/ux-a11y-check.md +49 -0
  118. package/framework/tasks/ux-component-map.md +55 -0
  119. package/framework/tasks/ux-consolidate.md +46 -0
  120. package/framework/tasks/ux-user-flow.md +46 -0
  121. package/framework/tasks/ux-wireframe.md +76 -0
  122. package/package.json +2 -2
  123. package/scripts/bundle-framework.js +2 -0
  124. package/scripts/changelog-generator.js +222 -0
  125. package/scripts/codebase-mapper.js +728 -0
  126. package/scripts/commit-message-generator.js +167 -0
  127. package/scripts/coverage-analyzer.js +260 -0
  128. package/scripts/dependency-analyzer.js +280 -0
  129. package/scripts/framework-analyzer.js +308 -0
  130. package/scripts/generate-constitution-domain.js +253 -0
  131. package/scripts/health-check.js +481 -0
  132. package/scripts/ide-sync.js +327 -0
  133. package/scripts/performance-analyzer.js +325 -0
  134. package/scripts/plan-tracker.js +278 -0
  135. package/scripts/populate-entity-registry.js +481 -0
  136. package/scripts/pr-review.js +317 -0
  137. package/scripts/rollback-manager.js +310 -0
  138. package/scripts/stuck-detector.js +343 -0
  139. package/scripts/test-quality-assessment.js +257 -0
  140. package/scripts/validate-agents.js +367 -0
  141. package/scripts/validate-tasks.js +465 -0
  142. package/src/autonomy/autonomous-gate.js +293 -0
  143. package/src/autonomy/index.js +51 -0
  144. package/src/autonomy/mode-manager.js +225 -0
  145. package/src/autonomy/mode-suggester.js +283 -0
  146. package/src/autonomy/progress-reporter.js +268 -0
  147. package/src/autonomy/safety-net.js +320 -0
  148. package/src/context/bracket-tracker.js +79 -0
  149. package/src/context/domain-loader.js +107 -0
  150. package/src/context/engine.js +144 -0
  151. package/src/context/formatter.js +184 -0
  152. package/src/context/index.js +4 -0
  153. package/src/context/layers/l0-constitution.js +28 -0
  154. package/src/context/layers/l1-global.js +37 -0
  155. package/src/context/layers/l2-agent.js +39 -0
  156. package/src/context/layers/l3-workflow.js +42 -0
  157. package/src/context/layers/l4-task.js +24 -0
  158. package/src/decision/analyzer.js +167 -0
  159. package/src/decision/engine.js +270 -0
  160. package/src/decision/index.js +38 -0
  161. package/src/decision/registry-healer.js +450 -0
  162. package/src/decision/registry-updater.js +330 -0
  163. package/src/gates/circuit-breaker.js +119 -0
  164. package/src/gates/g1-planning-complete.js +153 -0
  165. package/src/gates/g2-qa-planning.js +153 -0
  166. package/src/gates/g3-implementation.js +188 -0
  167. package/src/gates/g4-qa-implementation.js +207 -0
  168. package/src/gates/g5-deploy-ready.js +180 -0
  169. package/src/gates/gate-base.js +144 -0
  170. package/src/gates/index.js +46 -0
  171. package/src/installer/brownfield-upgrader.js +249 -0
  172. package/src/installer/core.js +82 -11
  173. package/src/installer/file-hasher.js +51 -0
  174. package/src/installer/manifest.js +117 -0
  175. package/src/installer/templates.js +17 -15
  176. package/src/installer/transaction.js +229 -0
  177. package/src/installer/validator.js +18 -1
  178. package/src/intelligence/registry-manager.js +2 -2
  179. package/src/memory/agent-memory.js +255 -0
  180. package/src/memory/gotchas-injector.js +72 -0
  181. package/src/memory/gotchas.js +361 -0
  182. package/src/memory/index.js +35 -0
  183. package/src/memory/search.js +233 -0
  184. package/src/memory/session-digest.js +239 -0
  185. package/src/merger/env-merger.js +112 -0
  186. package/src/merger/index.js +56 -0
  187. package/src/merger/replace-merger.js +51 -0
  188. package/src/merger/yaml-merger.js +127 -0
  189. package/src/orchestrator/agent-selector.js +285 -0
  190. package/src/orchestrator/deviation-handler.js +350 -0
  191. package/src/orchestrator/handoff-engine.js +271 -0
  192. package/src/orchestrator/index.js +67 -0
  193. package/src/orchestrator/intent-classifier.js +264 -0
  194. package/src/orchestrator/pipeline-manager.js +492 -0
  195. package/src/orchestrator/pipeline-state.js +223 -0
  196. package/src/orchestrator/session-manager.js +409 -0
  197. package/src/tasks/executor.js +195 -0
  198. package/src/tasks/handoff.js +226 -0
  199. package/src/tasks/index.js +4 -0
  200. package/src/tasks/loader.js +210 -0
  201. package/src/tasks/router.js +182 -0
  202. package/src/terminal/collector.js +216 -0
  203. package/src/terminal/index.js +30 -0
  204. package/src/terminal/isolation.js +129 -0
  205. package/src/terminal/monitor.js +277 -0
  206. package/src/terminal/spawner.js +269 -0
  207. package/src/upgrade/checker.js +1 -1
  208. package/src/wizard/i18n.js +3 -3
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Handoff Manager — Manages context handoffs between tasks and agents.
3
+ *
4
+ * A handoff preserves critical context when execution transitions from
5
+ * one agent to another. It captures what was done, what was produced,
6
+ * and what the next agent needs to know.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Build a handoff document from task execution results.
14
+ *
15
+ * @param {object} params
16
+ * @param {object} params.task - Completed task definition
17
+ * @param {object} [params.validation] - Validation results
18
+ * @param {string[]} [params.outputs] - Produced artifact paths
19
+ * @param {object} [params.decisions] - Key decisions made during execution
20
+ * @param {string} [params.summary] - Human-readable summary
21
+ * @param {string[]} [params.blockers] - Unresolved issues
22
+ * @returns {object} Handoff document
23
+ */
24
+ export function buildHandoff(params) {
25
+ const { task, validation, outputs, decisions, summary, blockers } = params;
26
+
27
+ return {
28
+ from: {
29
+ agent: task.agent,
30
+ task_id: task.id,
31
+ phase: task.phase,
32
+ },
33
+ to: task.handoff_to || null,
34
+ timestamp: new Date().toISOString(),
35
+ status: validation?.valid ? 'complete' : 'partial',
36
+ score: validation?.score ?? null,
37
+ summary: summary || `Task ${task.id} completed.`,
38
+ outputs: outputs || task.outputs || [],
39
+ decisions: decisions || {},
40
+ blockers: blockers || [],
41
+ criteria_met: task.criteria.filter((_, i) => {
42
+ const unmet = new Set(validation?.unmet || []);
43
+ return !unmet.has(task.criteria[i]);
44
+ }),
45
+ criteria_unmet: validation?.unmet || [],
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Serialize a handoff document to YAML-like markdown.
51
+ *
52
+ * @param {object} handoff - Handoff document from buildHandoff
53
+ * @returns {string} Formatted handoff string
54
+ */
55
+ export function formatHandoff(handoff) {
56
+ const lines = [
57
+ '---',
58
+ `from_agent: ${handoff.from.agent}`,
59
+ `from_task: ${handoff.from.task_id}`,
60
+ `from_phase: ${handoff.from.phase}`,
61
+ `to: ${handoff.to || 'orchestrator'}`,
62
+ `timestamp: ${handoff.timestamp}`,
63
+ `status: ${handoff.status}`,
64
+ `score: ${handoff.score ?? 'N/A'}`,
65
+ '---',
66
+ '',
67
+ `# Handoff: ${handoff.from.agent} → ${handoff.to || 'orchestrator'}`,
68
+ '',
69
+ '## Summary',
70
+ handoff.summary,
71
+ '',
72
+ ];
73
+
74
+ if (handoff.outputs.length > 0) {
75
+ lines.push('## Outputs');
76
+ for (const output of handoff.outputs) {
77
+ lines.push(`- ${output}`);
78
+ }
79
+ lines.push('');
80
+ }
81
+
82
+ if (Object.keys(handoff.decisions).length > 0) {
83
+ lines.push('## Decisions');
84
+ for (const [key, value] of Object.entries(handoff.decisions)) {
85
+ lines.push(`- **${key}**: ${value}`);
86
+ }
87
+ lines.push('');
88
+ }
89
+
90
+ if (handoff.blockers.length > 0) {
91
+ lines.push('## Blockers');
92
+ for (const blocker of handoff.blockers) {
93
+ lines.push(`- ${blocker}`);
94
+ }
95
+ lines.push('');
96
+ }
97
+
98
+ if (handoff.criteria_met.length > 0) {
99
+ lines.push('## Criteria Met');
100
+ for (const c of handoff.criteria_met) {
101
+ lines.push(`- [x] ${c}`);
102
+ }
103
+ lines.push('');
104
+ }
105
+
106
+ if (handoff.criteria_unmet.length > 0) {
107
+ lines.push('## Criteria Unmet');
108
+ for (const c of handoff.criteria_unmet) {
109
+ lines.push(`- [ ] ${c}`);
110
+ }
111
+ lines.push('');
112
+ }
113
+
114
+ return lines.join('\n');
115
+ }
116
+
117
+ /**
118
+ * Save a handoff document to the artifacts directory.
119
+ *
120
+ * @param {string} projectDir - Project root directory
121
+ * @param {object} handoff - Handoff document
122
+ * @returns {{ saved: boolean, path: string|null, error: string|null }}
123
+ */
124
+ export function saveHandoff(projectDir, handoff) {
125
+ try {
126
+ const handoffsDir = join(projectDir, 'chati.dev', 'artifacts', 'handoffs');
127
+ if (!existsSync(handoffsDir)) {
128
+ mkdirSync(handoffsDir, { recursive: true });
129
+ }
130
+
131
+ const filename = `${handoff.from.agent}-handoff.md`;
132
+ const filePath = join(handoffsDir, filename);
133
+ const content = formatHandoff(handoff);
134
+
135
+ writeFileSync(filePath, content, 'utf-8');
136
+ return { saved: true, path: filePath, error: null };
137
+ } catch (err) {
138
+ return { saved: false, path: null, error: `Failed to save handoff: ${err.message}` };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Load the most recent handoff from a specific agent.
144
+ *
145
+ * @param {string} projectDir - Project root directory
146
+ * @param {string} agentName - Agent that produced the handoff
147
+ * @returns {{ loaded: boolean, handoff: object|null, error: string|null }}
148
+ */
149
+ export function loadHandoff(projectDir, agentName) {
150
+ const filePath = join(projectDir, 'chati.dev', 'artifacts', 'handoffs', `${agentName}-handoff.md`);
151
+
152
+ if (!existsSync(filePath)) {
153
+ return { loaded: false, handoff: null, error: `No handoff found from agent '${agentName}'.` };
154
+ }
155
+
156
+ try {
157
+ const raw = readFileSync(filePath, 'utf-8');
158
+ const parsed = parseHandoffContent(raw);
159
+ return { loaded: true, handoff: parsed, error: null };
160
+ } catch (err) {
161
+ return { loaded: false, handoff: null, error: `Failed to parse handoff: ${err.message}` };
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Parse a handoff markdown document back into structured data.
167
+ *
168
+ * @param {string} content - Handoff file content
169
+ * @returns {object} Parsed handoff data
170
+ */
171
+ export function parseHandoffContent(content) {
172
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
173
+
174
+ const meta = {};
175
+ let body = content;
176
+
177
+ if (frontmatterMatch) {
178
+ const yamlPart = frontmatterMatch[1];
179
+ body = frontmatterMatch[2];
180
+
181
+ for (const line of yamlPart.split('\n')) {
182
+ const match = line.match(/^(\w[\w_-]*):\s*(.+)$/);
183
+ if (match) {
184
+ meta[match[1]] = match[2].trim();
185
+ }
186
+ }
187
+ }
188
+
189
+ // Extract sections from body
190
+ const sections = {};
191
+ let currentSection = null;
192
+ const sectionLines = {};
193
+
194
+ for (const line of body.split('\n')) {
195
+ const headerMatch = line.match(/^## (.+)/);
196
+ if (headerMatch) {
197
+ currentSection = headerMatch[1].toLowerCase().replace(/\s+/g, '_');
198
+ sectionLines[currentSection] = [];
199
+ } else if (currentSection) {
200
+ sectionLines[currentSection].push(line);
201
+ }
202
+ }
203
+
204
+ // Parse list items from sections
205
+ for (const [key, lines] of Object.entries(sectionLines)) {
206
+ sections[key] = lines
207
+ .map(l => l.trim())
208
+ .filter(l => l.startsWith('- '))
209
+ .map(l => l.replace(/^- (\[.\] )?/, '').trim());
210
+ }
211
+
212
+ return {
213
+ from_agent: meta.from_agent || null,
214
+ from_task: meta.from_task || null,
215
+ to: meta.to || null,
216
+ timestamp: meta.timestamp || null,
217
+ status: meta.status || 'unknown',
218
+ score: meta.score !== 'N/A' ? parseInt(meta.score, 10) || null : null,
219
+ summary: (sectionLines.summary || []).join(' ').trim(),
220
+ outputs: sections.outputs || [],
221
+ decisions: sections.decisions || [],
222
+ blockers: sections.blockers || [],
223
+ criteria_met: sections.criteria_met || [],
224
+ criteria_unmet: sections.criteria_unmet || [],
225
+ };
226
+ }
@@ -0,0 +1,4 @@
1
+ export { loadTask, parseTaskContent, loadAllTasks, getAgentTasks, getTaskSummary } from './loader.js';
2
+ export { TaskRouter, createRouter } from './router.js';
3
+ export { buildExecutionPayload, validateResults, determinePostAction } from './executor.js';
4
+ export { buildHandoff, formatHandoff, saveHandoff, loadHandoff, parseHandoffContent } from './handoff.js';
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Task Loader — Parses task definition files (YAML frontmatter + markdown body).
3
+ *
4
+ * Task files live in chati.dev/tasks/{agent}-{action}.md and contain:
5
+ * - YAML frontmatter: id, agent, trigger, phase, requires_input, parallelizable, outputs, handoff_to, autonomous_gate, criteria
6
+ * - Markdown body: Step-by-step instructions for the agent
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Parse a single task definition file.
14
+ * @param {string} filePath - Absolute path to .md file with YAML frontmatter
15
+ * @returns {{ loaded: boolean, task: object|null, error: string|null }}
16
+ */
17
+ export function loadTask(filePath) {
18
+ if (!existsSync(filePath)) {
19
+ return { loaded: false, task: null, error: `Task file not found: ${filePath}` };
20
+ }
21
+
22
+ try {
23
+ const raw = readFileSync(filePath, 'utf-8');
24
+ return parseTaskContent(raw, filePath);
25
+ } catch (err) {
26
+ return { loaded: false, task: null, error: `Failed to read ${filePath}: ${err.message}` };
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Parse task content string (YAML frontmatter + markdown body).
32
+ * @param {string} content - Raw file content
33
+ * @param {string} [source] - Source file path for error messages
34
+ * @returns {{ loaded: boolean, task: object|null, error: string|null }}
35
+ */
36
+ export function parseTaskContent(content, source = 'unknown') {
37
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
38
+
39
+ if (!frontmatterMatch) {
40
+ return { loaded: false, task: null, error: `No YAML frontmatter found in ${source}` };
41
+ }
42
+
43
+ const yamlStr = frontmatterMatch[1];
44
+ const body = frontmatterMatch[2].trim();
45
+
46
+ try {
47
+ const meta = parseSimpleYaml(yamlStr);
48
+
49
+ if (!meta.id) {
50
+ return { loaded: false, task: null, error: `Missing required field 'id' in ${source}` };
51
+ }
52
+ if (!meta.agent) {
53
+ return { loaded: false, task: null, error: `Missing required field 'agent' in ${source}` };
54
+ }
55
+
56
+ return {
57
+ loaded: true,
58
+ task: {
59
+ id: meta.id,
60
+ agent: meta.agent,
61
+ trigger: meta.trigger || 'orchestrator',
62
+ phase: meta.phase || 'clarity',
63
+ requires_input: meta.requires_input === true || meta.requires_input === 'true',
64
+ parallelizable: meta.parallelizable === true || meta.parallelizable === 'true',
65
+ outputs: parseArray(meta.outputs),
66
+ handoff_to: meta.handoff_to || null,
67
+ autonomous_gate: meta.autonomous_gate !== false && meta.autonomous_gate !== 'false',
68
+ criteria: parseArray(meta.criteria),
69
+ instructions: body,
70
+ source,
71
+ },
72
+ error: null,
73
+ };
74
+ } catch (err) {
75
+ return { loaded: false, task: null, error: `YAML parse error in ${source}: ${err.message}` };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Load all task definitions from a directory.
81
+ * @param {string} tasksDir - Path to chati.dev/tasks/
82
+ * @returns {{ tasks: Map<string, object>, errors: string[] }}
83
+ */
84
+ export function loadAllTasks(tasksDir) {
85
+ const tasks = new Map();
86
+ const errors = [];
87
+
88
+ if (!existsSync(tasksDir)) {
89
+ errors.push(`Tasks directory not found: ${tasksDir}`);
90
+ return { tasks, errors };
91
+ }
92
+
93
+ const files = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
94
+
95
+ for (const file of files) {
96
+ const result = loadTask(join(tasksDir, file));
97
+ if (result.loaded) {
98
+ tasks.set(result.task.id, result.task);
99
+ } else {
100
+ errors.push(result.error);
101
+ }
102
+ }
103
+
104
+ return { tasks, errors };
105
+ }
106
+
107
+ /**
108
+ * Get all tasks for a specific agent.
109
+ * @param {Map<string, object>} taskMap - Map from loadAllTasks
110
+ * @param {string} agentName - Agent name to filter by
111
+ * @returns {object[]}
112
+ */
113
+ export function getAgentTasks(taskMap, agentName) {
114
+ const result = [];
115
+ for (const task of taskMap.values()) {
116
+ if (task.agent === agentName) {
117
+ result.push(task);
118
+ }
119
+ }
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Get a task's info summary (without instructions body).
125
+ * @param {object} task - Task object from loadTask
126
+ * @returns {object}
127
+ */
128
+ export function getTaskSummary(task) {
129
+ return {
130
+ id: task.id,
131
+ agent: task.agent,
132
+ phase: task.phase,
133
+ parallelizable: task.parallelizable,
134
+ outputs: task.outputs,
135
+ handoff_to: task.handoff_to,
136
+ criteria_count: task.criteria.length,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Simple YAML parser for frontmatter (no external deps).
142
+ * Handles key: value, key: [array], and key: true/false.
143
+ */
144
+ function parseSimpleYaml(str) {
145
+ const result = {};
146
+ let currentKey = null;
147
+ let currentArray = null;
148
+
149
+ for (const line of str.split('\n')) {
150
+ const trimmed = line.trim();
151
+ if (!trimmed || trimmed.startsWith('#')) continue;
152
+
153
+ // Array item under current key
154
+ if (trimmed.startsWith('- ') && currentKey && currentArray) {
155
+ currentArray.push(trimmed.slice(2).trim());
156
+ continue;
157
+ }
158
+
159
+ // Key: value pair
160
+ const kvMatch = trimmed.match(/^(\w[\w_-]*):\s*(.*)$/);
161
+ if (kvMatch) {
162
+ // Save previous array if any
163
+ if (currentKey && currentArray) {
164
+ result[currentKey] = currentArray;
165
+ }
166
+
167
+ const key = kvMatch[1];
168
+ const val = kvMatch[2].trim();
169
+
170
+ if (val === '') {
171
+ // Could be start of an array
172
+ currentKey = key;
173
+ currentArray = [];
174
+ } else if (val.startsWith('[') && val.endsWith(']')) {
175
+ // Inline array: [a, b, c]
176
+ result[key] = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
177
+ currentKey = null;
178
+ currentArray = null;
179
+ } else if (val === 'true') {
180
+ result[key] = true;
181
+ currentKey = null;
182
+ currentArray = null;
183
+ } else if (val === 'false') {
184
+ result[key] = false;
185
+ currentKey = null;
186
+ currentArray = null;
187
+ } else {
188
+ result[key] = val;
189
+ currentKey = null;
190
+ currentArray = null;
191
+ }
192
+ }
193
+ }
194
+
195
+ // Save trailing array
196
+ if (currentKey && currentArray) {
197
+ result[currentKey] = currentArray;
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ /**
204
+ * Ensure a value is an array.
205
+ */
206
+ function parseArray(val) {
207
+ if (Array.isArray(val)) return val;
208
+ if (typeof val === 'string') return [val];
209
+ return [];
210
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Task Router — Maps orchestrator intents to the correct task definition.
3
+ *
4
+ * The router is used internally by the orchestrator to determine
5
+ * which task an agent should execute based on:
6
+ * - Current pipeline position
7
+ * - Agent state
8
+ * - User intent signals
9
+ */
10
+
11
+ import { loadAllTasks, getAgentTasks } from './loader.js';
12
+
13
+ /**
14
+ * Task Router instance.
15
+ * Holds the loaded task registry and provides routing methods.
16
+ */
17
+ export class TaskRouter {
18
+ /**
19
+ * @param {string} tasksDir - Path to chati.dev/tasks/
20
+ */
21
+ constructor(tasksDir) {
22
+ this.tasksDir = tasksDir;
23
+ this.taskMap = new Map();
24
+ this.errors = [];
25
+ this.loaded = false;
26
+ }
27
+
28
+ /**
29
+ * Initialize the router by loading all task definitions.
30
+ * @returns {this}
31
+ */
32
+ load() {
33
+ const result = loadAllTasks(this.tasksDir);
34
+ this.taskMap = result.tasks;
35
+ this.errors = result.errors;
36
+ this.loaded = true;
37
+ return this;
38
+ }
39
+
40
+ /**
41
+ * Route to the correct task for an agent at a given pipeline position.
42
+ *
43
+ * Routing priority:
44
+ * 1. Exact task ID match (if provided)
45
+ * 2. Agent's consolidate task (if pipeline position = consolidate)
46
+ * 3. Agent's first non-consolidate task (default entry point)
47
+ *
48
+ * @param {object} intent
49
+ * @param {string} intent.agent - Target agent name
50
+ * @param {string} [intent.taskId] - Explicit task ID (highest priority)
51
+ * @param {string} [intent.action] - Action keyword (e.g., 'consolidate', 'extract')
52
+ * @param {string} [intent.phase] - Pipeline phase filter
53
+ * @returns {{ found: boolean, task: object|null, reason: string }}
54
+ */
55
+ route(intent) {
56
+ if (!this.loaded) {
57
+ return { found: false, task: null, reason: 'Router not loaded. Call load() first.' };
58
+ }
59
+
60
+ if (!intent || !intent.agent) {
61
+ return { found: false, task: null, reason: 'No agent specified in routing intent.' };
62
+ }
63
+
64
+ // 1. Exact task ID match
65
+ if (intent.taskId) {
66
+ const task = this.taskMap.get(intent.taskId);
67
+ if (task) {
68
+ return { found: true, task, reason: `Exact match: ${intent.taskId}` };
69
+ }
70
+ return { found: false, task: null, reason: `Task ID '${intent.taskId}' not found in registry.` };
71
+ }
72
+
73
+ // Get all tasks for this agent
74
+ const agentTasks = getAgentTasks(this.taskMap, intent.agent);
75
+ if (agentTasks.length === 0) {
76
+ return { found: false, task: null, reason: `No tasks registered for agent '${intent.agent}'.` };
77
+ }
78
+
79
+ // Filter by phase if provided
80
+ let candidates = agentTasks;
81
+ if (intent.phase) {
82
+ const phaseFiltered = agentTasks.filter(t => t.phase === intent.phase);
83
+ if (phaseFiltered.length > 0) {
84
+ candidates = phaseFiltered;
85
+ }
86
+ }
87
+
88
+ // 2. Action keyword match
89
+ if (intent.action) {
90
+ const actionTask = candidates.find(t =>
91
+ t.id.includes(intent.action) || t.id.endsWith(`-${intent.action}`)
92
+ );
93
+ if (actionTask) {
94
+ return { found: true, task: actionTask, reason: `Action match: ${intent.action}` };
95
+ }
96
+ }
97
+
98
+ // 3. Consolidate task (if action is consolidate or implied)
99
+ if (intent.action === 'consolidate') {
100
+ const consolidateTask = candidates.find(t => t.id.endsWith('-consolidate'));
101
+ if (consolidateTask) {
102
+ return { found: true, task: consolidateTask, reason: 'Consolidate task selected.' };
103
+ }
104
+ }
105
+
106
+ // 4. Default: first non-consolidate task (entry point)
107
+ const entryTask = candidates.find(t => !t.id.endsWith('-consolidate')) || candidates[0];
108
+ return { found: true, task: entryTask, reason: `Default entry: ${entryTask.id}` };
109
+ }
110
+
111
+ /**
112
+ * Get the next task in an agent's task sequence.
113
+ * Uses the handoff_to field to determine chaining.
114
+ *
115
+ * @param {string} currentTaskId - Currently executing task ID
116
+ * @returns {{ found: boolean, task: object|null, reason: string }}
117
+ */
118
+ getNextTask(currentTaskId) {
119
+ const current = this.taskMap.get(currentTaskId);
120
+ if (!current) {
121
+ return { found: false, task: null, reason: `Current task '${currentTaskId}' not found.` };
122
+ }
123
+
124
+ // If handoff_to is an agent name, it means we switch agents (orchestrator handles this)
125
+ // If handoff_to is a task ID within the same agent, it's internal chaining
126
+ if (!current.handoff_to) {
127
+ return { found: false, task: null, reason: 'No handoff defined for this task.' };
128
+ }
129
+
130
+ // Check if handoff_to is a task ID
131
+ const nextTask = this.taskMap.get(current.handoff_to);
132
+ if (nextTask) {
133
+ return { found: true, task: nextTask, reason: `Internal chain: ${current.handoff_to}` };
134
+ }
135
+
136
+ // Otherwise it's an agent-level handoff (handled by orchestrator)
137
+ return {
138
+ found: false,
139
+ task: null,
140
+ reason: `Agent handoff to '${current.handoff_to}'. Orchestrator handles inter-agent routing.`,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get all parallelizable tasks for a given agent.
146
+ * @param {string} agentName
147
+ * @returns {object[]}
148
+ */
149
+ getParallelTasks(agentName) {
150
+ return getAgentTasks(this.taskMap, agentName).filter(t => t.parallelizable);
151
+ }
152
+
153
+ /**
154
+ * Get routing statistics.
155
+ * @returns {{ totalTasks: number, byAgent: object, byPhase: object, errors: number }}
156
+ */
157
+ getStats() {
158
+ const byAgent = {};
159
+ const byPhase = {};
160
+
161
+ for (const task of this.taskMap.values()) {
162
+ byAgent[task.agent] = (byAgent[task.agent] || 0) + 1;
163
+ byPhase[task.phase] = (byPhase[task.phase] || 0) + 1;
164
+ }
165
+
166
+ return {
167
+ totalTasks: this.taskMap.size,
168
+ byAgent,
169
+ byPhase,
170
+ errors: this.errors.length,
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Create and load a TaskRouter instance.
177
+ * @param {string} tasksDir - Path to chati.dev/tasks/
178
+ * @returns {TaskRouter}
179
+ */
180
+ export function createRouter(tasksDir) {
181
+ return new TaskRouter(tasksDir).load();
182
+ }