/**
* Linting and initialization tools.
* Handles: lint_project_docs, init_project
*/
import {
PROJECT_DIR,
TODOS_DIR,
ARCHIVE_DIR,
VALID_STATUSES,
VALID_PRIORITIES,
} from '../lib/constants.js';
import {
readFile,
writeFile,
join,
ensureProjectDir,
ensureTodosDir,
ensureArchiveDir,
fileExists,
} from '../lib/files.js';
import { getCurrentDate, getISODate, isValidDateFormat, isDateInPast } from '../lib/dates.js';
import { loadAllTasks, normalizePriority } from '../lib/tasks.js';
/**
* Tool definitions
*/
export const definitions = [
{
name: 'lint_project_docs',
description:
'Validates project documentation against standards. Checks for required files, valid frontmatter, broken dependencies, missing fields, and formatting issues. Can auto-fix common problems. Run this before commits to ensure documentation quality.',
inputSchema: {
type: 'object',
properties: {
fix: {
type: 'boolean',
description:
'If true, automatically fix issues that can be auto-corrected (missing timestamps, formatting, etc.). Default: false (report only).',
default: false,
},
strict: {
type: 'boolean',
description:
'If true, enforce stricter rules (all tasks must have estimates, due dates, descriptions). Default: false.',
default: false,
},
scope: {
type: 'string',
description:
'What to lint: "all" (everything), "tasks" (only task files), "docs" (only documentation files). Default: "all".',
enum: ['all', 'tasks', 'docs'],
default: 'all',
},
},
},
},
{
name: 'init_project',
description:
'Initializes the .project/ directory with all standard files following strict templates. Creates index.md (contract), TODO.md (dashboard), BACKLOG.md (prioritized work queue), ROADMAP.md, STATUS.md, DECISIONS.md, and todos/ directory. Use this to bootstrap a new project with proper structure.',
inputSchema: {
type: 'object',
properties: {
project_name: {
type: 'string',
description: 'Name of the project. Used in headers and metadata.',
},
project_description: {
type: 'string',
description: 'Brief description of the project.',
},
overwrite: {
type: 'boolean',
description: 'If true, overwrites existing files. Default: false (skip existing).',
default: false,
},
},
required: ['project_name'],
},
},
];
/**
* Lint project docs handler
*/
async function lintProjectDocs(args) {
const { fix = false, strict = false, scope = 'all' } = args || {};
await ensureProjectDir();
const issues = [];
const fixed = [];
const warnings = [];
// Define required documentation files
const requiredDocs = [
{ file: 'index.md', name: 'Contract file', required: ['# ', 'Intent'] },
{ file: 'TODO.md', name: 'Todo index', required: ['# TODO'] },
{ file: 'ROADMAP.md', name: 'Roadmap', required: ['# '] },
{ file: 'STATUS.md', name: 'Status', required: ['# '] },
{ file: 'DECISIONS.md', name: 'Decisions log', required: ['# '] },
];
// Check documentation files
if (scope === 'all' || scope === 'docs') {
for (const doc of requiredDocs) {
const docPath = join(PROJECT_DIR, doc.file);
const exists = await fileExists(docPath);
if (!exists) {
issues.push({
type: 'error',
file: doc.file,
message: `Missing required file: ${doc.name}`,
fix: `Run the appropriate create_or_update_* tool to create ${doc.file}`,
});
} else {
const content = await readFile(docPath, 'utf-8');
// Check for required content patterns
for (const pattern of doc.required) {
if (!content.includes(pattern)) {
warnings.push({
type: 'warning',
file: doc.file,
message: `Missing expected content pattern: "${pattern}"`,
});
}
}
// Check for timestamp
if (!content.includes('Last Updated:') && !content.includes('updated:')) {
if (fix) {
const updatedContent = content + `\n\n---\n*Last Updated: ${getCurrentDate()}*\n`;
await writeFile(docPath, updatedContent, 'utf-8');
fixed.push({ file: doc.file, action: 'Added timestamp' });
} else {
issues.push({
type: 'warning',
file: doc.file,
message: 'Missing timestamp',
fix: 'Add *Last Updated: DATE* to the file',
});
}
}
}
}
}
// Check task files
if (scope === 'all' || scope === 'tasks') {
const tasks = await loadAllTasks();
const taskIds = new Set(tasks.map((t) => t.id));
// Required task fields
const requiredFields = ['id', 'title', 'project', 'status', 'priority'];
const strictFields = ['owner', 'estimate', 'description'];
for (const task of tasks) {
const taskFile = `todos/${task.file}`;
// Check required fields
for (const field of requiredFields) {
if (!task[field]) {
issues.push({
type: 'error',
file: taskFile,
message: `Missing required field: ${field}`,
fix: `Add ${field} to frontmatter`,
});
}
}
// Strict mode checks
if (strict) {
for (const field of strictFields) {
if (!task[field]) {
warnings.push({
type: 'warning',
file: taskFile,
message: `Missing recommended field: ${field} (strict mode)`,
});
}
}
}
// Validate status
if (task.status && !VALID_STATUSES.includes(task.status)) {
issues.push({
type: 'error',
file: taskFile,
message: `Invalid status: "${task.status}". Must be one of: ${VALID_STATUSES.join(', ')}`,
fix: 'Update status to a valid value',
});
}
// Validate priority
if (task.priority && !VALID_PRIORITIES.includes(task.priority)) {
if (fix) {
const normalized = normalizePriority(task.priority);
const filePath = join(TODOS_DIR, task.file);
let content = await readFile(filePath, 'utf-8');
content = content.replace(/priority:\s*.+/, `priority: ${normalized}`);
await writeFile(filePath, content, 'utf-8');
fixed.push({ file: taskFile, action: `Normalized priority to ${normalized}` });
} else {
issues.push({
type: 'error',
file: taskFile,
message: `Invalid priority: "${task.priority}". Must be one of: ${VALID_PRIORITIES.join(', ')}`,
fix: 'Use P0, P1, P2, or P3',
});
}
}
// Check for broken dependencies
if (task.depends_on && Array.isArray(task.depends_on)) {
for (const depId of task.depends_on) {
if (!taskIds.has(depId)) {
issues.push({
type: 'error',
file: taskFile,
message: `Broken dependency: "${depId}" does not exist`,
fix: `Remove ${depId} from depends_on or create the missing task`,
});
}
}
}
// Check for circular dependencies
if (task.depends_on && task.depends_on.includes(task.id)) {
issues.push({
type: 'error',
file: taskFile,
message: 'Circular dependency: task depends on itself',
fix: 'Remove self-reference from depends_on',
});
}
// Check for missing updated timestamp
if (!task.updated) {
if (fix) {
const filePath = join(TODOS_DIR, task.file);
let content = await readFile(filePath, 'utf-8');
if (content.includes('created:') && !content.includes('updated:')) {
content = content.replace(/(created:\s*.+)/, `$1\nupdated: ${getISODate()}`);
await writeFile(filePath, content, 'utf-8');
fixed.push({ file: taskFile, action: 'Added updated timestamp' });
}
} else {
warnings.push({
type: 'warning',
file: taskFile,
message: 'Missing updated timestamp in frontmatter',
});
}
}
// Check ID matches filename
const expectedFilename = `${task.id}.md`;
if (task.file !== expectedFilename) {
issues.push({
type: 'error',
file: taskFile,
message: `Filename "${task.file}" doesn't match task ID "${task.id}"`,
fix: `Rename file to ${expectedFilename}`,
});
}
// Check due date format if present
if (task.due && !isValidDateFormat(task.due)) {
issues.push({
type: 'warning',
file: taskFile,
message: `Invalid due date format: "${task.due}". Expected YYYY-MM-DD`,
fix: 'Update due date to YYYY-MM-DD format',
});
}
// Check for overdue tasks
if (task.due && task.status !== 'done' && isDateInPast(task.due)) {
warnings.push({
type: 'warning',
file: taskFile,
message: `Task is overdue (due: ${task.due})`,
});
}
}
// Check for orphaned tasks (done tasks that others depend on)
for (const task of tasks) {
if (task.status === 'done' && task.depends_on?.length > 0) {
const unresolvedDeps = task.depends_on.filter((depId) => {
const dep = tasks.find((t) => t.id === depId);
return dep && dep.status !== 'done';
});
if (unresolvedDeps.length > 0) {
warnings.push({
type: 'warning',
file: `todos/${task.file}`,
message: `Task marked done but has unresolved dependencies: ${unresolvedDeps.join(', ')}`,
});
}
}
}
}
// Build result
const errorCount = issues.filter((i) => i.type === 'error').length;
const warningCount = issues.filter((i) => i.type === 'warning').length + warnings.length;
let result = `## Documentation Lint Results\n\n`;
result += `**Scope:** ${scope} | **Strict:** ${strict} | **Auto-fix:** ${fix}\n\n`;
if (errorCount === 0 && warningCount === 0) {
result += `✅ **All checks passed!** Documentation conforms to standards.\n`;
} else {
result += `| Type | Count |\n|------|-------|\n`;
result += `| 🔴 Errors | ${errorCount} |\n`;
result += `| 🟡 Warnings | ${warningCount} |\n\n`;
}
if (fixed.length > 0) {
result += `### ✅ Auto-fixed (${fixed.length})\n\n`;
for (const f of fixed) {
result += `- \`${f.file}\`: ${f.action}\n`;
}
result += '\n';
}
if (issues.length > 0) {
result += `### 🔴 Issues\n\n`;
for (const issue of issues) {
result += `**${issue.file}**\n`;
result += `- ${issue.type === 'error' ? '❌' : '⚠️'} ${issue.message}\n`;
if (issue.fix) result += ` - Fix: ${issue.fix}\n`;
result += '\n';
}
}
if (warnings.length > 0) {
result += `### 🟡 Warnings\n\n`;
for (const warn of warnings) {
result += `- \`${warn.file}\`: ${warn.message}\n`;
}
result += '\n';
}
if (errorCount > 0) {
result += `---\n\n⚠️ **${errorCount} error(s) found.** Run with \`fix: true\` to auto-fix where possible.\n`;
}
return {
content: [{ type: 'text', text: result }],
};
}
/**
* Init project handler
*/
async function initProject(args) {
const { project_name, project_description = '', overwrite = false } = args;
const date = getCurrentDate();
const files = [];
const skipped = [];
// Ensure directories exist
await ensureProjectDir();
await ensureTodosDir();
await ensureArchiveDir();
// Standard file templates
const templates = {
'index.md': `---
title: ${project_name} - Project Index
created: ${getISODate()}
updated: ${getISODate()}
---
# ${project_name}
${project_description}
## Contract for AI Agents
When a user says **"project"**, **"the project"**, or **"my project"**, the canonical sources of truth are:
1. **\`.project/\`** — Current state, plans, todos, decisions (operational truth)
2. **Root markdown files** — README.md, CONTRIBUTING.md, etc.
3. **\`docs/\`** — Long-form reference documentation
## Source Mappings
### "project" / "the project"
Searches: \`.project/\` + root files + \`docs/\`
### "docs" / "documentation"
Searches: \`docs/\` only
### "plan" / "todos" / "roadmap" / "status"
Searches: \`.project/\` only
## File Structure
| File | Purpose |
|------|---------|
| \`index.md\` | This file - contract and source mappings |
| \`BACKLOG.md\` | Prioritized work queue (future tasks) |
| \`TODO.md\` | Task dashboard (active work, auto-generated) |
| \`ROADMAP.md\` | Project phases and milestones |
| \`STATUS.md\` | Current project health and progress |
| \`DECISIONS.md\` | Architecture decisions and rationale |
| \`todos/\` | Active task files (10-30 items, not hundreds) |
| \`archive/\` | Completed tasks (for history) |
## Principles
- **Natural language stays natural** — Users say "project" not ".project/"
- **Agents don't guess** — Explicit mappings defined here
- **Intent over structure** — Language maps to intent, not directory names
- **Operational truth** — This directory is the source of truth for current state
---
*Last Updated: ${date}*
`,
'ROADMAP.md': `---
title: ${project_name} - Roadmap
created: ${getISODate()}
updated: ${getISODate()}
---
# Roadmap
## Overview
${project_description || 'Project roadmap and milestones.'}
## Phases
### Phase 1: Foundation
**Status:** Not Started
**Target:** TBD
- [ ] Initial setup
- [ ] Core infrastructure
- [ ] Basic functionality
### Phase 2: Core Features
**Status:** Not Started
**Target:** TBD
- [ ] Feature development
- [ ] Testing
- [ ] Documentation
### Phase 3: Polish & Launch
**Status:** Not Started
**Target:** TBD
- [ ] Bug fixes
- [ ] Performance optimization
- [ ] Launch preparation
## Milestones
| Milestone | Target Date | Status |
|-----------|-------------|--------|
| MVP | TBD | Not Started |
| Beta | TBD | Not Started |
| v1.0 | TBD | Not Started |
## Future Considerations
- Future feature ideas
- Technical debt items
- Nice-to-have improvements
---
*Last Updated: ${date}*
`,
'STATUS.md': `---
title: ${project_name} - Status
created: ${getISODate()}
updated: ${getISODate()}
---
# Project Status
**Last Updated:** ${date}
## Current Phase
**Foundation** — Initial setup and planning
## Health
🟡 **Yellow** — Project initialized, work not yet started
## Progress
| Area | Status | Notes |
|------|--------|-------|
| Planning | 🟢 In Progress | Setting up project structure |
| Development | ⚪ Not Started | |
| Testing | ⚪ Not Started | |
| Documentation | 🟢 In Progress | Initial docs created |
## Recent Changes
- ✅ Initialized project structure
- ✅ Created standard documentation files
- 📋 Ready for planning phase
## Active Work
None yet — see TODO.md for task dashboard
## Blockers
None currently
## Next Steps
1. Define project requirements
2. Create initial tasks using \`create_task\`
3. Begin Phase 1 development
---
*Last Updated: ${date}*
`,
'DECISIONS.md': `---
title: ${project_name} - Architecture Decisions
created: ${getISODate()}
updated: ${getISODate()}
---
# Architecture Decisions
This document records architecture decisions, trade-offs, and rationale for ${project_name}.
## Decision Log
### ADR-001: Project Structure
**Date:** ${date}
**Status:** Accepted
**Context:**
Need a standard way to organize project documentation and tasks.
**Decision:**
Use \`.project/\` directory with:
- YAML frontmatter for task metadata
- Jira-like task IDs ({PROJECT}-{NNN})
- Dependency tracking between tasks
**Consequences:**
- Consistent structure across projects
- Machine-readable task metadata
- Clear separation of operational vs reference docs
---
## Template for New Decisions
\`\`\`markdown
### ADR-XXX: Title
**Date:** YYYY-MM-DD
**Status:** Proposed | Accepted | Deprecated | Superseded
**Context:**
What is the issue that we're seeing that is motivating this decision?
**Decision:**
What is the change that we're proposing and/or doing?
**Consequences:**
What becomes easier or more difficult to do because of this change?
\`\`\`
---
*Last Updated: ${date}*
`,
'TODO.md': `---
title: ${project_name} - Task Dashboard
created: ${getISODate()}
updated: ${getISODate()}
---
# TODO Dashboard
**Last Updated:** ${date}
## Overview
| Status | Count |
|--------|-------|
| 🔵 In Progress | 0 |
| ⚪ Todo | 0 |
| 🔴 Blocked | 0 |
| ✅ Done | 0 |
## 🎯 Next Up (Active Tasks)
*No active tasks. Promote tasks from BACKLOG.md using \`promote_task\`.*
## Workflow
\`\`\`
ROADMAP.md → import_tasks → BACKLOG.md → promote_task → todos/*.md → archive_task → archive/
(plan) (extract) (queue) (activate) (work) (complete) (history)
\`\`\`
1. **Plan:** Define phases in ROADMAP.md
2. **Import:** Use \`import_tasks\` to populate BACKLOG.md
3. **Promote:** Use \`promote_task\` to activate work (creates YAML file)
4. **Work:** Use \`get_next_task\` to see what to do, \`update_task\` to track
5. **Complete:** Use \`archive_task\` to move done items to archive
---
*This file is auto-generated by \`sync_todo_index\`. Active tasks are in \`.project/todos/\`.*
`,
'BACKLOG.md': `---
title: ${project_name} - Backlog
created: ${getISODate()}
updated: ${getISODate()}
---
# Backlog
**Last Updated:** ${date}
This is the prioritized queue of future work. Items here are **planned but not yet active**.
## How to Use
1. **Add items:** Use \`import_tasks\` to import from ROADMAP.md or add manually
2. **Promote to active:** Use \`promote_task\` when ready to start work
3. **Keep it prioritized:** P0 items at top, P3 at bottom
## Queue
### P0 - Critical
*No critical items*
### P1 - High Priority
*No high priority items*
### P2 - Medium Priority
*No medium priority items*
### P3 - Low Priority
*No low priority items*
---
## Item Format
Each backlog item should follow this format:
\`\`\`markdown
- [ ] **{ID}**: {Title} [P{0-3}] [{tags}]
{Optional description}
\`\`\`
Example:
\`\`\`markdown
- [ ] **AUTH-001**: Implement user login [P1] [security]
Basic username/password authentication
\`\`\`
---
*Use \`promote_task\` to move items to active work in todos/*
`,
};
// Create each file
for (const [filename, content] of Object.entries(templates)) {
const filePath = join(PROJECT_DIR, filename);
const exists = await fileExists(filePath);
if (exists && !overwrite) {
skipped.push(filename);
continue;
}
await writeFile(filePath, content, 'utf-8');
files.push({ file: filename, action: exists ? 'overwritten' : 'created' });
}
// Create .gitkeep in todos/ if empty
const todosGitkeep = join(TODOS_DIR, '.gitkeep');
if (!(await fileExists(todosGitkeep))) {
await writeFile(todosGitkeep, '', 'utf-8');
}
// Create .gitkeep in archive/
const archiveGitkeep = join(ARCHIVE_DIR, '.gitkeep');
if (!(await fileExists(archiveGitkeep))) {
await writeFile(archiveGitkeep, '', 'utf-8');
}
let result = `## Project Initialized: ${project_name}\n\n`;
result += `**Location:** \`.project/\`\n\n`;
if (files.length > 0) {
result += `### Files Created\n\n`;
for (const f of files) {
result += `- ✅ \`${f.file}\` (${f.action})\n`;
}
result += `- ✅ \`todos/\` directory (active work)\n`;
result += `- ✅ \`archive/\` directory (completed work)\n\n`;
}
if (skipped.length > 0) {
result += `### Files Skipped (already exist)\n\n`;
for (const f of skipped) {
result += `- ⏭️ \`${f}\`\n`;
}
result += `\n*Use \`overwrite: true\` to replace existing files.*\n\n`;
}
result += `### Next Steps\n\n`;
result += `1. Review and customize \`.project/ROADMAP.md\`\n`;
result += `2. Create tasks with \`create_task\`\n`;
result += `3. Import tasks from roadmap with \`import_tasks\`\n`;
result += `4. Run \`lint_project_docs\` to validate structure\n`;
return {
content: [{ type: 'text', text: result }],
};
}
/**
* Handler map
*/
export const handlers = {
lint_project_docs: lintProjectDocs,
init_project: initProject,
};