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,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate Tasks — Validates task definition files (.md with YAML frontmatter).
|
|
5
|
+
*
|
|
6
|
+
* Exports:
|
|
7
|
+
* validateAllTasks(tasksDir) → TaskValidationReport
|
|
8
|
+
* validateTask(tasksDir, taskId) → single task validation
|
|
9
|
+
* validateHandoffChain(tasksDir) → checks the entire handoff chain is connected
|
|
10
|
+
* getTaskStats(tasksDir) → { total, byAgent, byPhase, withHandoffs, withoutHandoffs }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
14
|
+
import { join, basename } from 'path';
|
|
15
|
+
import yaml from 'js-yaml';
|
|
16
|
+
|
|
17
|
+
import { ALL_AGENT_NAMES } from './validate-agents.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Required fields in task frontmatter.
|
|
21
|
+
*/
|
|
22
|
+
const REQUIRED_FIELDS = ['id', 'agent'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Valid phase values.
|
|
26
|
+
*/
|
|
27
|
+
const VALID_PHASES = ['clarity', 'build', 'deploy', 'validate', 'quality'];
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Internal Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse YAML frontmatter from a markdown file.
|
|
35
|
+
* @param {string} content - Full file content.
|
|
36
|
+
* @returns {{ frontmatter: object|null, body: string, error: string|null }}
|
|
37
|
+
*/
|
|
38
|
+
function parseFrontmatter(content) {
|
|
39
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return { frontmatter: null, body: content, error: 'No YAML frontmatter found' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const frontmatter = yaml.load(match[1]);
|
|
46
|
+
const body = content.slice(match[0].length).trim();
|
|
47
|
+
return { frontmatter, body, error: null };
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return { frontmatter: null, body: content, error: `Invalid YAML: ${err.message}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate a single task definition.
|
|
55
|
+
* @param {string} tasksDir - Directory containing task .md files.
|
|
56
|
+
* @param {string} filename - Task filename (e.g., 'brief-extract-requirements.md').
|
|
57
|
+
* @returns {object} Validation result.
|
|
58
|
+
*/
|
|
59
|
+
export function validateTask(tasksDir, filename) {
|
|
60
|
+
const fullPath = join(tasksDir, filename);
|
|
61
|
+
const taskName = basename(filename, '.md');
|
|
62
|
+
|
|
63
|
+
const result = {
|
|
64
|
+
name: taskName,
|
|
65
|
+
file: filename,
|
|
66
|
+
valid: true,
|
|
67
|
+
errors: [],
|
|
68
|
+
warnings: [],
|
|
69
|
+
frontmatter: null,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Check file exists
|
|
73
|
+
if (!existsSync(fullPath)) {
|
|
74
|
+
result.valid = false;
|
|
75
|
+
result.errors.push(`File not found: ${filename}`);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let content;
|
|
80
|
+
try {
|
|
81
|
+
content = readFileSync(fullPath, 'utf8');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
result.valid = false;
|
|
84
|
+
result.errors.push(`Cannot read file: ${err.message}`);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse frontmatter
|
|
89
|
+
const { frontmatter, error } = parseFrontmatter(content);
|
|
90
|
+
if (error) {
|
|
91
|
+
result.valid = false;
|
|
92
|
+
result.errors.push(error);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result.frontmatter = frontmatter;
|
|
97
|
+
|
|
98
|
+
// Check required fields
|
|
99
|
+
for (const field of REQUIRED_FIELDS) {
|
|
100
|
+
if (!frontmatter[field]) {
|
|
101
|
+
result.valid = false;
|
|
102
|
+
result.errors.push(`Missing required field: '${field}'`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check id matches filename
|
|
107
|
+
if (frontmatter.id && frontmatter.id !== taskName) {
|
|
108
|
+
result.warnings.push(`Task id '${frontmatter.id}' does not match filename '${taskName}'`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check agent is a known agent
|
|
112
|
+
if (frontmatter.agent) {
|
|
113
|
+
const knownAgents = [...ALL_AGENT_NAMES, 'orchestrator'];
|
|
114
|
+
if (!knownAgents.includes(frontmatter.agent)) {
|
|
115
|
+
result.warnings.push(`Unknown agent: '${frontmatter.agent}'`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check phase is valid
|
|
120
|
+
if (frontmatter.phase && !VALID_PHASES.includes(frontmatter.phase)) {
|
|
121
|
+
result.warnings.push(`Unknown phase: '${frontmatter.phase}'`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check criteria is not empty (if present)
|
|
125
|
+
if (frontmatter.criteria !== undefined) {
|
|
126
|
+
if (!Array.isArray(frontmatter.criteria) || frontmatter.criteria.length === 0) {
|
|
127
|
+
result.warnings.push('criteria field is empty or not an array');
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
result.warnings.push('No criteria defined');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check outputs is not empty (if present)
|
|
134
|
+
if (frontmatter.outputs !== undefined) {
|
|
135
|
+
if (!Array.isArray(frontmatter.outputs) || frontmatter.outputs.length === 0) {
|
|
136
|
+
result.warnings.push('outputs field is empty or not an array');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check handoff_to is a valid reference
|
|
141
|
+
if (frontmatter.handoff_to) {
|
|
142
|
+
// handoff_to could be a task id or agent name — validated in chain check
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate all task files in a directory.
|
|
150
|
+
* @param {string} tasksDir - Directory containing task .md files.
|
|
151
|
+
* @returns {object} TaskValidationReport.
|
|
152
|
+
*/
|
|
153
|
+
export function validateAllTasks(tasksDir) {
|
|
154
|
+
const report = {
|
|
155
|
+
valid: true,
|
|
156
|
+
tasks: [],
|
|
157
|
+
errors: [],
|
|
158
|
+
warnings: [],
|
|
159
|
+
totalFiles: 0,
|
|
160
|
+
totalValid: 0,
|
|
161
|
+
duplicateIds: [],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (!existsSync(tasksDir)) {
|
|
165
|
+
report.valid = false;
|
|
166
|
+
report.errors.push(`Tasks directory not found: ${tasksDir}`);
|
|
167
|
+
return report;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let files;
|
|
171
|
+
try {
|
|
172
|
+
files = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
|
173
|
+
} catch (err) {
|
|
174
|
+
report.valid = false;
|
|
175
|
+
report.errors.push(`Cannot read directory: ${err.message}`);
|
|
176
|
+
return report;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
report.totalFiles = files.length;
|
|
180
|
+
|
|
181
|
+
if (files.length === 0) {
|
|
182
|
+
report.warnings.push('No task .md files found');
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Track IDs for duplicate detection
|
|
187
|
+
const seenIds = new Map();
|
|
188
|
+
|
|
189
|
+
for (const file of files) {
|
|
190
|
+
const result = validateTask(tasksDir, file);
|
|
191
|
+
report.tasks.push(result);
|
|
192
|
+
|
|
193
|
+
if (result.valid) {
|
|
194
|
+
report.totalValid++;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Track duplicate IDs
|
|
198
|
+
const taskId = result.frontmatter?.id;
|
|
199
|
+
if (taskId) {
|
|
200
|
+
if (seenIds.has(taskId)) {
|
|
201
|
+
report.duplicateIds.push({ id: taskId, files: [seenIds.get(taskId), file] });
|
|
202
|
+
} else {
|
|
203
|
+
seenIds.set(taskId, file);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Report duplicate IDs
|
|
209
|
+
if (report.duplicateIds.length > 0) {
|
|
210
|
+
report.valid = false;
|
|
211
|
+
for (const dup of report.duplicateIds) {
|
|
212
|
+
report.errors.push(`Duplicate task id '${dup.id}' in: ${dup.files.join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return report;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validate the handoff chain across all tasks.
|
|
221
|
+
* @param {string} tasksDir - Directory containing task .md files.
|
|
222
|
+
* @returns {object} Handoff chain validation result.
|
|
223
|
+
*/
|
|
224
|
+
export function validateHandoffChain(tasksDir) {
|
|
225
|
+
const result = {
|
|
226
|
+
valid: true,
|
|
227
|
+
chains: [],
|
|
228
|
+
orphans: [],
|
|
229
|
+
brokenLinks: [],
|
|
230
|
+
errors: [],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (!existsSync(tasksDir)) {
|
|
234
|
+
result.valid = false;
|
|
235
|
+
result.errors.push(`Tasks directory not found: ${tasksDir}`);
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let files;
|
|
240
|
+
try {
|
|
241
|
+
files = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
result.valid = false;
|
|
244
|
+
result.errors.push(`Cannot read directory: ${err.message}`);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Load all tasks
|
|
249
|
+
const tasks = new Map();
|
|
250
|
+
const tasksByAgent = new Map();
|
|
251
|
+
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
const fullPath = join(tasksDir, file);
|
|
254
|
+
try {
|
|
255
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
256
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
257
|
+
if (frontmatter?.id) {
|
|
258
|
+
tasks.set(frontmatter.id, frontmatter);
|
|
259
|
+
const agent = frontmatter.agent;
|
|
260
|
+
if (agent) {
|
|
261
|
+
if (!tasksByAgent.has(agent)) tasksByAgent.set(agent, []);
|
|
262
|
+
tasksByAgent.get(agent).push(frontmatter.id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// Skip unparseable files
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check handoff_to references
|
|
271
|
+
const targetedBy = new Set();
|
|
272
|
+
|
|
273
|
+
for (const [taskId, task] of tasks) {
|
|
274
|
+
if (task.handoff_to) {
|
|
275
|
+
// handoff_to can reference a task id or an agent-prefixed task
|
|
276
|
+
const target = task.handoff_to;
|
|
277
|
+
// Check if it's a known task ID
|
|
278
|
+
const isKnownTask = tasks.has(target);
|
|
279
|
+
// Check if it's a known agent (handoff to any task of that agent)
|
|
280
|
+
const isKnownAgent = [...ALL_AGENT_NAMES, 'orchestrator'].includes(target);
|
|
281
|
+
|
|
282
|
+
if (isKnownTask) {
|
|
283
|
+
targetedBy.add(target);
|
|
284
|
+
} else if (isKnownAgent) {
|
|
285
|
+
// Valid — handoff to an agent
|
|
286
|
+
targetedBy.add(target);
|
|
287
|
+
} else {
|
|
288
|
+
result.brokenLinks.push({ from: taskId, to: target });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Find orphaned tasks (not targeted by any handoff, and not triggered by orchestrator)
|
|
294
|
+
for (const [taskId, task] of tasks) {
|
|
295
|
+
if (!targetedBy.has(taskId) && task.trigger !== 'orchestrator') {
|
|
296
|
+
result.orphans.push(taskId);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check agent coverage — every agent should have at least 1 task
|
|
301
|
+
const knownAgents = [...ALL_AGENT_NAMES, 'orchestrator'];
|
|
302
|
+
const uncoveredAgents = knownAgents.filter(agent => !tasksByAgent.has(agent));
|
|
303
|
+
|
|
304
|
+
if (uncoveredAgents.length > 0) {
|
|
305
|
+
result.errors.push(`Agents without tasks: ${uncoveredAgents.join(', ')}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (result.brokenLinks.length > 0) {
|
|
309
|
+
result.valid = false;
|
|
310
|
+
for (const link of result.brokenLinks) {
|
|
311
|
+
result.errors.push(`Broken handoff: '${link.from}' -> '${link.to}'`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get statistics about task definitions.
|
|
320
|
+
* @param {string} tasksDir - Directory containing task .md files.
|
|
321
|
+
* @returns {object} Task statistics.
|
|
322
|
+
*/
|
|
323
|
+
export function getTaskStats(tasksDir) {
|
|
324
|
+
const stats = {
|
|
325
|
+
total: 0,
|
|
326
|
+
byAgent: {},
|
|
327
|
+
byPhase: {},
|
|
328
|
+
withHandoffs: 0,
|
|
329
|
+
withoutHandoffs: 0,
|
|
330
|
+
withCriteria: 0,
|
|
331
|
+
withoutCriteria: 0,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (!existsSync(tasksDir)) return stats;
|
|
335
|
+
|
|
336
|
+
let files;
|
|
337
|
+
try {
|
|
338
|
+
files = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
|
339
|
+
} catch {
|
|
340
|
+
return stats;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const file of files) {
|
|
344
|
+
const fullPath = join(tasksDir, file);
|
|
345
|
+
try {
|
|
346
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
347
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
348
|
+
if (!frontmatter?.id) continue;
|
|
349
|
+
|
|
350
|
+
stats.total++;
|
|
351
|
+
|
|
352
|
+
// By agent
|
|
353
|
+
if (frontmatter.agent) {
|
|
354
|
+
stats.byAgent[frontmatter.agent] = (stats.byAgent[frontmatter.agent] || 0) + 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// By phase
|
|
358
|
+
if (frontmatter.phase) {
|
|
359
|
+
stats.byPhase[frontmatter.phase] = (stats.byPhase[frontmatter.phase] || 0) + 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Handoffs
|
|
363
|
+
if (frontmatter.handoff_to) {
|
|
364
|
+
stats.withHandoffs++;
|
|
365
|
+
} else {
|
|
366
|
+
stats.withoutHandoffs++;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Criteria
|
|
370
|
+
if (Array.isArray(frontmatter.criteria) && frontmatter.criteria.length > 0) {
|
|
371
|
+
stats.withCriteria++;
|
|
372
|
+
} else {
|
|
373
|
+
stats.withoutCriteria++;
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// Skip
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return stats;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Format task validation report as a human-readable string.
|
|
385
|
+
* @param {object} report - Report from validateAllTasks.
|
|
386
|
+
* @returns {string}
|
|
387
|
+
*/
|
|
388
|
+
export function formatTaskReport(report) {
|
|
389
|
+
const lines = [
|
|
390
|
+
'=== Task Validation Report ===',
|
|
391
|
+
`Status: ${report.valid ? '[PASS]' : '[FAIL]'}`,
|
|
392
|
+
`Tasks: ${report.totalValid}/${report.totalFiles} valid`,
|
|
393
|
+
'',
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
if (report.duplicateIds.length > 0) {
|
|
397
|
+
lines.push('Duplicate IDs:');
|
|
398
|
+
for (const dup of report.duplicateIds) {
|
|
399
|
+
lines.push(` - '${dup.id}' in ${dup.files.join(', ')}`);
|
|
400
|
+
}
|
|
401
|
+
lines.push('');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (const task of report.tasks) {
|
|
405
|
+
if (task.errors.length > 0 || task.warnings.length > 0) {
|
|
406
|
+
const symbol = task.valid ? '[WARN]' : '[FAIL]';
|
|
407
|
+
lines.push(` ${symbol} ${task.name}`);
|
|
408
|
+
for (const err of task.errors) lines.push(` ERROR: ${err}`);
|
|
409
|
+
for (const warn of task.warnings) lines.push(` WARN: ${warn}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (report.errors.length > 0) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push('Errors:');
|
|
416
|
+
for (const err of report.errors) lines.push(` - ${err}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
lines.push('');
|
|
420
|
+
lines.push('=== End Report ===');
|
|
421
|
+
return lines.join('\n');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// CLI entrypoint
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
const isMainModule = process.argv[1] && (
|
|
429
|
+
process.argv[1].endsWith('validate-tasks.js') ||
|
|
430
|
+
process.argv[1].endsWith('validate-tasks')
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (isMainModule) {
|
|
434
|
+
const tasksDir = process.argv[2] || join(process.cwd(), 'chati.dev', 'tasks');
|
|
435
|
+
|
|
436
|
+
console.log(`Validating tasks in: ${tasksDir}`);
|
|
437
|
+
|
|
438
|
+
const report = validateAllTasks(tasksDir);
|
|
439
|
+
console.log(formatTaskReport(report));
|
|
440
|
+
|
|
441
|
+
console.log('\n--- Task Stats ---');
|
|
442
|
+
const stats = getTaskStats(tasksDir);
|
|
443
|
+
console.log(`Total: ${stats.total}`);
|
|
444
|
+
console.log(`By agent:`, stats.byAgent);
|
|
445
|
+
console.log(`By phase:`, stats.byPhase);
|
|
446
|
+
console.log(`With handoffs: ${stats.withHandoffs}, Without: ${stats.withoutHandoffs}`);
|
|
447
|
+
console.log(`With criteria: ${stats.withCriteria}, Without: ${stats.withoutCriteria}`);
|
|
448
|
+
|
|
449
|
+
console.log('\n--- Handoff Chain ---');
|
|
450
|
+
const chain = validateHandoffChain(tasksDir);
|
|
451
|
+
if (chain.brokenLinks.length > 0) {
|
|
452
|
+
console.log('Broken links:');
|
|
453
|
+
for (const link of chain.brokenLinks) console.log(` ${link.from} -> ${link.to}`);
|
|
454
|
+
}
|
|
455
|
+
if (chain.orphans.length > 0) {
|
|
456
|
+
console.log(`Orphaned tasks: ${chain.orphans.join(', ')}`);
|
|
457
|
+
}
|
|
458
|
+
if (chain.errors.length > 0) {
|
|
459
|
+
for (const err of chain.errors) console.log(` ERROR: ${err}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!report.valid) {
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
}
|