chati-dev 4.2.2 → 4.3.1
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 +80 -53
- package/bin/chati.js +170 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +158 -22
- package/framework/agents/discover/brownfield-wu.md +26 -14
- package/framework/agents/discover/greenfield-wu.md +102 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +38 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +448 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +7 -6
- package/framework/constitution.md +127 -44
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +24 -5
- package/framework/hooks/license-guard.js +62 -27
- package/framework/hooks/mode-governance.js +13 -2
- package/framework/hooks/model-governance.js +1 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +274 -105
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +50 -1
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +25 -6
- package/framework/hooks/team-quality-gate.js +19 -12
- package/framework/hooks/undercover-guard.js +4 -2
- 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/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +441 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +295 -94
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +99 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +178 -0
- package/framework/scaffold/motion-premium-3d/README.md +90 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +136 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +686 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +2 -2
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +12 -6
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +27 -7
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +19 -7
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +431 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +46 -29
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1880 -71
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +339 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -505
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -221
- package/src/intelligence/elicitation.js +0 -265
|
@@ -1,465 +0,0 @@
|
|
|
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 = ['discover', 'plan', 'planning', '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
|
-
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Git worktree isolation for build loop retries.
|
|
3
|
-
*
|
|
4
|
-
* Each retry attempt can optionally run in an isolated git worktree,
|
|
5
|
-
* preventing failed attempts from corrupting the main working directory.
|
|
6
|
-
*
|
|
7
|
-
* Constitution Article XVII — Execution Mode Governance.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @deprecated Not currently imported by any production module.
|
|
12
|
-
* Retained for potential future integration. Review for removal or integration by v5.0.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { execSync } from 'child_process';
|
|
16
|
-
import { existsSync, mkdirSync, readdirSync, statSync, rmSync } from 'fs';
|
|
17
|
-
import { join, resolve } from 'path';
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Constants
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/** Directory for worktrees inside .chati/ */
|
|
24
|
-
const WORKTREE_DIR = '.chati/worktrees';
|
|
25
|
-
|
|
26
|
-
/** Maximum age (ms) before a worktree is considered stale (24h). */
|
|
27
|
-
const MAX_WORKTREE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Helpers
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Execute a git command in a given directory and return trimmed stdout.
|
|
35
|
-
* Sanitizes `cmd` to reject shell metacharacters (prevents injection).
|
|
36
|
-
*
|
|
37
|
-
* @param {string} cmd - Git command (without the `git` prefix)
|
|
38
|
-
* @param {string} cwd - Working directory
|
|
39
|
-
* @returns {string} Trimmed stdout
|
|
40
|
-
* @throws {Error} If cmd contains shell metacharacters
|
|
41
|
-
*/
|
|
42
|
-
function git(cmd, cwd) {
|
|
43
|
-
if (/[`$;|&><(){}]/.test(cmd)) {
|
|
44
|
-
throw new Error(`Unsafe characters in git command: ${cmd}`);
|
|
45
|
-
}
|
|
46
|
-
return execSync(`git ${cmd}`, {
|
|
47
|
-
cwd,
|
|
48
|
-
encoding: 'utf-8',
|
|
49
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
50
|
-
}).trim();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Public API
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check if a directory is inside a git repository.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} dir - Directory path
|
|
61
|
-
* @returns {boolean}
|
|
62
|
-
*/
|
|
63
|
-
export function isGitRepo(dir) {
|
|
64
|
-
try {
|
|
65
|
-
git('rev-parse --is-inside-work-tree', dir);
|
|
66
|
-
return true;
|
|
67
|
-
} catch { /* expected: content may be malformed */
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Create an isolated git worktree for a retry attempt.
|
|
74
|
-
*
|
|
75
|
-
* The worktree is placed at `.chati/worktrees/<taskId>-attempt-<N>`.
|
|
76
|
-
* A new branch is created from the current HEAD.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} projectDir - Root project directory
|
|
79
|
-
* @param {string} taskId - Task identifier
|
|
80
|
-
* @param {number} attempt - Attempt number
|
|
81
|
-
* @returns {{ path: string, branch: string, cleanup: () => void }}
|
|
82
|
-
* @throws {Error} If not a git repo or worktree creation fails
|
|
83
|
-
*/
|
|
84
|
-
export function createWorktree(projectDir, taskId, attempt) {
|
|
85
|
-
const absProject = resolve(projectDir);
|
|
86
|
-
|
|
87
|
-
if (!isGitRepo(absProject)) {
|
|
88
|
-
throw new Error(`Not a git repository: ${absProject}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const safeName = `${taskId}-attempt-${attempt}`.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
92
|
-
const worktreeBase = join(absProject, WORKTREE_DIR);
|
|
93
|
-
const worktreePath = join(worktreeBase, safeName);
|
|
94
|
-
const branch = `chati/${safeName}`;
|
|
95
|
-
|
|
96
|
-
// Ensure worktree base directory exists
|
|
97
|
-
mkdirSync(worktreeBase, { recursive: true });
|
|
98
|
-
|
|
99
|
-
// Clean up if a worktree already exists at this path
|
|
100
|
-
if (existsSync(worktreePath)) {
|
|
101
|
-
try {
|
|
102
|
-
git(`worktree remove "${worktreePath}" --force`, absProject);
|
|
103
|
-
} catch { /* expected: operation may fail gracefully */
|
|
104
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Delete branch if it exists from a previous run
|
|
109
|
-
try {
|
|
110
|
-
git(`branch -D "${branch}"`, absProject);
|
|
111
|
-
} catch { /* expected: operation may fail gracefully */
|
|
112
|
-
// Branch doesn't exist — fine
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Create worktree with new branch from HEAD
|
|
116
|
-
git(`worktree add -b "${branch}" "${worktreePath}"`, absProject);
|
|
117
|
-
|
|
118
|
-
const cleanup = () => {
|
|
119
|
-
try {
|
|
120
|
-
git(`worktree remove "${worktreePath}" --force`, absProject);
|
|
121
|
-
} catch { /* expected: operation may fail gracefully */
|
|
122
|
-
// Worktree already removed — ignore
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
git(`branch -D "${branch}"`, absProject);
|
|
126
|
-
} catch { /* expected: operation may fail gracefully */
|
|
127
|
-
// Branch already deleted — ignore
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
return { path: worktreePath, branch, cleanup };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Merge changes from a worktree branch back to the main branch.
|
|
136
|
-
*
|
|
137
|
-
* @param {string} projectDir - Root project directory
|
|
138
|
-
* @param {string} worktreeBranch - Worktree branch name
|
|
139
|
-
* @param {string} [mainBranch] - Target branch (auto-detected if omitted)
|
|
140
|
-
* @returns {{ success: boolean, conflicts: string[] }}
|
|
141
|
-
*/
|
|
142
|
-
export function mergeWorktree(projectDir, worktreeBranch, mainBranch) {
|
|
143
|
-
const absProject = resolve(projectDir);
|
|
144
|
-
|
|
145
|
-
// Auto-detect main branch if not specified
|
|
146
|
-
if (!mainBranch) {
|
|
147
|
-
try {
|
|
148
|
-
mainBranch = git('rev-parse --abbrev-ref HEAD', absProject);
|
|
149
|
-
} catch { /* expected: content may be malformed */
|
|
150
|
-
mainBranch = 'main';
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Ensure we're on the main branch
|
|
155
|
-
try {
|
|
156
|
-
git(`checkout "${mainBranch}"`, absProject);
|
|
157
|
-
} catch { /* expected: operation may fail gracefully */
|
|
158
|
-
return { success: false, conflicts: [`Failed to checkout ${mainBranch}`] };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Attempt merge
|
|
162
|
-
try {
|
|
163
|
-
git(`merge "${worktreeBranch}" --no-edit`, absProject);
|
|
164
|
-
return { success: true, conflicts: [] };
|
|
165
|
-
} catch { /* expected: operation may fail gracefully */
|
|
166
|
-
// Parse conflicts from error output
|
|
167
|
-
const conflicts = [];
|
|
168
|
-
try {
|
|
169
|
-
const status = git('diff --name-only --diff-filter=U', absProject);
|
|
170
|
-
if (status) {
|
|
171
|
-
conflicts.push(...status.split('\n').filter(Boolean));
|
|
172
|
-
}
|
|
173
|
-
} catch { /* expected: operation may fail gracefully */
|
|
174
|
-
conflicts.push('Unknown conflict — check git status');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Abort the failed merge
|
|
178
|
-
try {
|
|
179
|
-
git('merge --abort', absProject);
|
|
180
|
-
} catch { /* expected: operation may fail gracefully */
|
|
181
|
-
// Already aborted or not in merging state
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { success: false, conflicts };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Clean up stale worktrees that exceed the maximum age.
|
|
190
|
-
*
|
|
191
|
-
* @param {string} projectDir - Root project directory
|
|
192
|
-
* @param {{ maxAge?: number }} [options={}]
|
|
193
|
-
* @returns {{ cleaned: string[], errors: string[] }}
|
|
194
|
-
*/
|
|
195
|
-
export function cleanupWorktrees(projectDir, options = {}) {
|
|
196
|
-
const absProject = resolve(projectDir);
|
|
197
|
-
const maxAge = options.maxAge || MAX_WORKTREE_AGE_MS;
|
|
198
|
-
const worktreeBase = join(absProject, WORKTREE_DIR);
|
|
199
|
-
const cleaned = [];
|
|
200
|
-
const errors = [];
|
|
201
|
-
|
|
202
|
-
if (!existsSync(worktreeBase)) {
|
|
203
|
-
return { cleaned, errors };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const now = Date.now();
|
|
207
|
-
let entries;
|
|
208
|
-
try {
|
|
209
|
-
entries = readdirSync(worktreeBase);
|
|
210
|
-
} catch { /* expected: directory may not exist */
|
|
211
|
-
return { cleaned, errors: ['Failed to read worktree directory'] };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
for (const entry of entries) {
|
|
215
|
-
const entryPath = join(worktreeBase, entry);
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
const stat = statSync(entryPath);
|
|
219
|
-
if (!stat.isDirectory()) continue;
|
|
220
|
-
|
|
221
|
-
const age = now - stat.mtimeMs;
|
|
222
|
-
if (age > maxAge) {
|
|
223
|
-
try {
|
|
224
|
-
git(`worktree remove "${entryPath}" --force`, absProject);
|
|
225
|
-
} catch { /* expected: operation may fail gracefully */
|
|
226
|
-
rmSync(entryPath, { recursive: true, force: true });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Also remove the branch
|
|
230
|
-
const branch = `chati/${entry}`;
|
|
231
|
-
try {
|
|
232
|
-
git(`branch -D "${branch}"`, absProject);
|
|
233
|
-
} catch { /* expected: operation may fail gracefully */
|
|
234
|
-
// Branch already deleted
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
cleaned.push(entry);
|
|
238
|
-
}
|
|
239
|
-
} catch (err) {
|
|
240
|
-
errors.push(`${entry}: ${err.message}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return { cleaned, errors };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Exported constants for testing.
|
|
249
|
-
*/
|
|
250
|
-
export { WORKTREE_DIR, MAX_WORKTREE_AGE_MS };
|