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.
- package/README.md +40 -24
- package/framework/agents/build/dev.md +343 -0
- package/framework/agents/clarity/architect.md +112 -0
- package/framework/agents/clarity/brief.md +182 -0
- package/framework/agents/clarity/brownfield-wu.md +181 -0
- package/framework/agents/clarity/detail.md +110 -0
- package/framework/agents/clarity/greenfield-wu.md +153 -0
- package/framework/agents/clarity/ux.md +112 -0
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +31 -1
- package/framework/context/governance.md +37 -0
- package/framework/context/protocols.md +34 -0
- package/framework/context/quality.md +27 -0
- package/framework/context/root.md +24 -0
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/architect.yaml +51 -0
- package/framework/domains/agents/brief.yaml +47 -0
- package/framework/domains/agents/brownfield-wu.yaml +49 -0
- package/framework/domains/agents/detail.yaml +47 -0
- package/framework/domains/agents/dev.yaml +49 -0
- package/framework/domains/agents/devops.yaml +43 -0
- package/framework/domains/agents/greenfield-wu.yaml +47 -0
- package/framework/domains/agents/orchestrator.yaml +49 -0
- package/framework/domains/agents/phases.yaml +47 -0
- package/framework/domains/agents/qa-implementation.yaml +43 -0
- package/framework/domains/agents/qa-planning.yaml +44 -0
- package/framework/domains/agents/tasks.yaml +48 -0
- package/framework/domains/agents/ux.yaml +50 -0
- package/framework/domains/constitution.yaml +77 -0
- package/framework/domains/global.yaml +64 -0
- package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
- package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
- package/framework/domains/workflows/brownfield-service.yaml +22 -0
- package/framework/domains/workflows/brownfield-ui.yaml +22 -0
- package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
- package/framework/hooks/constitution-guard.js +101 -0
- package/framework/hooks/mode-governance.js +92 -0
- package/framework/hooks/model-governance.js +76 -0
- package/framework/hooks/prism-engine.js +89 -0
- package/framework/hooks/session-digest.js +60 -0
- package/framework/hooks/settings.json +44 -0
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
- package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
- package/framework/orchestrator/chati.md +284 -6
- package/framework/tasks/architect-api-design.md +63 -0
- package/framework/tasks/architect-consolidate.md +47 -0
- package/framework/tasks/architect-db-design.md +73 -0
- package/framework/tasks/architect-design.md +95 -0
- package/framework/tasks/architect-security-review.md +62 -0
- package/framework/tasks/architect-stack-selection.md +53 -0
- package/framework/tasks/brief-consolidate.md +249 -0
- package/framework/tasks/brief-constraint-identify.md +277 -0
- package/framework/tasks/brief-extract-requirements.md +339 -0
- package/framework/tasks/brief-stakeholder-map.md +176 -0
- package/framework/tasks/brief-validate-completeness.md +121 -0
- package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
- package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
- package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
- package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
- package/framework/tasks/brownfield-wu-report.md +325 -0
- package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
- package/framework/tasks/detail-acceptance-criteria.md +372 -0
- package/framework/tasks/detail-consolidate.md +138 -0
- package/framework/tasks/detail-edge-case-analysis.md +300 -0
- package/framework/tasks/detail-expand-prd.md +389 -0
- package/framework/tasks/detail-nfr-extraction.md +223 -0
- package/framework/tasks/dev-code-review.md +404 -0
- package/framework/tasks/dev-consolidate.md +543 -0
- package/framework/tasks/dev-debug.md +322 -0
- package/framework/tasks/dev-implement.md +252 -0
- package/framework/tasks/dev-iterate.md +411 -0
- package/framework/tasks/dev-pr-prepare.md +497 -0
- package/framework/tasks/dev-refactor.md +342 -0
- package/framework/tasks/dev-test-write.md +306 -0
- package/framework/tasks/devops-ci-setup.md +412 -0
- package/framework/tasks/devops-consolidate.md +712 -0
- package/framework/tasks/devops-deploy-config.md +598 -0
- package/framework/tasks/devops-monitoring-setup.md +658 -0
- package/framework/tasks/devops-release-prepare.md +673 -0
- package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
- package/framework/tasks/greenfield-wu-report.md +266 -0
- package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
- package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
- package/framework/tasks/orchestrator-deviation.md +260 -0
- package/framework/tasks/orchestrator-escalate.md +276 -0
- package/framework/tasks/orchestrator-handoff.md +243 -0
- package/framework/tasks/orchestrator-health.md +372 -0
- package/framework/tasks/orchestrator-mode-switch.md +262 -0
- package/framework/tasks/orchestrator-resume.md +189 -0
- package/framework/tasks/orchestrator-route.md +169 -0
- package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
- package/framework/tasks/orchestrator-status.md +260 -0
- package/framework/tasks/orchestrator-suggest-mode.md +372 -0
- package/framework/tasks/phases-breakdown.md +91 -0
- package/framework/tasks/phases-dependency-mapping.md +67 -0
- package/framework/tasks/phases-mvp-scoping.md +94 -0
- package/framework/tasks/qa-impl-consolidate.md +522 -0
- package/framework/tasks/qa-impl-performance-test.md +487 -0
- package/framework/tasks/qa-impl-regression-check.md +413 -0
- package/framework/tasks/qa-impl-sast-scan.md +402 -0
- package/framework/tasks/qa-impl-test-execute.md +344 -0
- package/framework/tasks/qa-impl-verdict.md +339 -0
- package/framework/tasks/qa-planning-consolidate.md +309 -0
- package/framework/tasks/qa-planning-coverage-plan.md +338 -0
- package/framework/tasks/qa-planning-gate-define.md +339 -0
- package/framework/tasks/qa-planning-risk-matrix.md +631 -0
- package/framework/tasks/qa-planning-test-strategy.md +217 -0
- package/framework/tasks/tasks-acceptance-write.md +75 -0
- package/framework/tasks/tasks-consolidate.md +57 -0
- package/framework/tasks/tasks-decompose.md +80 -0
- package/framework/tasks/tasks-estimate.md +66 -0
- package/framework/tasks/ux-a11y-check.md +49 -0
- package/framework/tasks/ux-component-map.md +55 -0
- package/framework/tasks/ux-consolidate.md +46 -0
- package/framework/tasks/ux-user-flow.md +46 -0
- package/framework/tasks/ux-wireframe.md +76 -0
- package/package.json +2 -2
- package/scripts/bundle-framework.js +2 -0
- package/scripts/changelog-generator.js +222 -0
- package/scripts/codebase-mapper.js +728 -0
- package/scripts/commit-message-generator.js +167 -0
- package/scripts/coverage-analyzer.js +260 -0
- package/scripts/dependency-analyzer.js +280 -0
- package/scripts/framework-analyzer.js +308 -0
- package/scripts/generate-constitution-domain.js +253 -0
- package/scripts/health-check.js +481 -0
- package/scripts/ide-sync.js +327 -0
- package/scripts/performance-analyzer.js +325 -0
- package/scripts/plan-tracker.js +278 -0
- package/scripts/populate-entity-registry.js +481 -0
- package/scripts/pr-review.js +317 -0
- package/scripts/rollback-manager.js +310 -0
- package/scripts/stuck-detector.js +343 -0
- package/scripts/test-quality-assessment.js +257 -0
- package/scripts/validate-agents.js +367 -0
- package/scripts/validate-tasks.js +465 -0
- package/src/autonomy/autonomous-gate.js +293 -0
- package/src/autonomy/index.js +51 -0
- package/src/autonomy/mode-manager.js +225 -0
- package/src/autonomy/mode-suggester.js +283 -0
- package/src/autonomy/progress-reporter.js +268 -0
- package/src/autonomy/safety-net.js +320 -0
- package/src/context/bracket-tracker.js +79 -0
- package/src/context/domain-loader.js +107 -0
- package/src/context/engine.js +144 -0
- package/src/context/formatter.js +184 -0
- package/src/context/index.js +4 -0
- package/src/context/layers/l0-constitution.js +28 -0
- package/src/context/layers/l1-global.js +37 -0
- package/src/context/layers/l2-agent.js +39 -0
- package/src/context/layers/l3-workflow.js +42 -0
- package/src/context/layers/l4-task.js +24 -0
- package/src/decision/analyzer.js +167 -0
- package/src/decision/engine.js +270 -0
- package/src/decision/index.js +38 -0
- package/src/decision/registry-healer.js +450 -0
- package/src/decision/registry-updater.js +330 -0
- package/src/gates/circuit-breaker.js +119 -0
- package/src/gates/g1-planning-complete.js +153 -0
- package/src/gates/g2-qa-planning.js +153 -0
- package/src/gates/g3-implementation.js +188 -0
- package/src/gates/g4-qa-implementation.js +207 -0
- package/src/gates/g5-deploy-ready.js +180 -0
- package/src/gates/gate-base.js +144 -0
- package/src/gates/index.js +46 -0
- package/src/installer/brownfield-upgrader.js +249 -0
- package/src/installer/core.js +82 -11
- package/src/installer/file-hasher.js +51 -0
- package/src/installer/manifest.js +117 -0
- package/src/installer/templates.js +17 -15
- package/src/installer/transaction.js +229 -0
- package/src/installer/validator.js +18 -1
- package/src/intelligence/registry-manager.js +2 -2
- package/src/memory/agent-memory.js +255 -0
- package/src/memory/gotchas-injector.js +72 -0
- package/src/memory/gotchas.js +361 -0
- package/src/memory/index.js +35 -0
- package/src/memory/search.js +233 -0
- package/src/memory/session-digest.js +239 -0
- package/src/merger/env-merger.js +112 -0
- package/src/merger/index.js +56 -0
- package/src/merger/replace-merger.js +51 -0
- package/src/merger/yaml-merger.js +127 -0
- package/src/orchestrator/agent-selector.js +285 -0
- package/src/orchestrator/deviation-handler.js +350 -0
- package/src/orchestrator/handoff-engine.js +271 -0
- package/src/orchestrator/index.js +67 -0
- package/src/orchestrator/intent-classifier.js +264 -0
- package/src/orchestrator/pipeline-manager.js +492 -0
- package/src/orchestrator/pipeline-state.js +223 -0
- package/src/orchestrator/session-manager.js +409 -0
- package/src/tasks/executor.js +195 -0
- package/src/tasks/handoff.js +226 -0
- package/src/tasks/index.js +4 -0
- package/src/tasks/loader.js +210 -0
- package/src/tasks/router.js +182 -0
- package/src/terminal/collector.js +216 -0
- package/src/terminal/index.js +30 -0
- package/src/terminal/isolation.js +129 -0
- package/src/terminal/monitor.js +277 -0
- package/src/terminal/spawner.js +269 -0
- package/src/upgrade/checker.js +1 -1
- 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
|
+
}
|