@polymorphism-tech/morph-spec 4.8.12 → 4.8.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +379 -379
  2. package/bin/morph-spec.js +23 -2
  3. package/bin/{task-manager.cjs → task-manager.js} +249 -172
  4. package/claude-plugin.json +14 -14
  5. package/docs/CHEATSHEET.md +203 -203
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents.json +224 -140
  8. package/framework/hooks/README.md +202 -202
  9. package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
  10. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
  11. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +12 -0
  12. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
  13. package/framework/hooks/claude-code/session-start/inject-morph-context.js +34 -0
  14. package/framework/hooks/claude-code/statusline.py +6 -0
  15. package/framework/hooks/claude-code/stop/validate-completion.js +38 -4
  16. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
  18. package/framework/hooks/shared/phase-utils.js +4 -1
  19. package/framework/hooks/shared/state-reader.js +1 -0
  20. package/framework/skills/README.md +1 -0
  21. package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
  22. package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
  23. package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
  24. package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
  25. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
  26. package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
  27. package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
  28. package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
  30. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
  31. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
  32. package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
  33. package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
  34. package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
  36. package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
  37. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +50 -188
  38. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
  39. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
  40. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
  41. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  42. package/framework/skills/level-1-workflows/phase-design/SKILL.md +71 -109
  43. package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
  44. package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
  45. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +171 -114
  46. package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
  47. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
  48. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +35 -159
  49. package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
  50. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
  51. package/framework/squad-templates/backend-only.json +14 -1
  52. package/framework/squad-templates/frontend-only.json +14 -1
  53. package/framework/squad-templates/full-stack.json +25 -8
  54. package/framework/standards/STANDARDS.json +631 -86
  55. package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
  56. package/framework/templates/project/validate.js +122 -0
  57. package/framework/workflows/configs/zero-touch.json +7 -0
  58. package/package.json +87 -87
  59. package/src/commands/agents/dispatch-agents.js +53 -10
  60. package/src/commands/state/advance-phase.js +88 -13
  61. package/src/commands/state/index.js +2 -1
  62. package/src/commands/state/phase-runner.js +215 -0
  63. package/src/commands/tasks/task.js +25 -4
  64. package/src/core/paths/output-schema.js +2 -1
  65. package/src/lib/detectors/design-system-detector.js +5 -4
  66. package/src/lib/generators/recap-generator.js +16 -0
  67. package/src/lib/orchestration/team-orchestrator.js +171 -89
  68. package/src/lib/phase-chain/eligibility-checker.js +243 -0
  69. package/src/lib/standards/digest-builder.js +231 -0
  70. package/src/lib/tasks/task-parser.js +94 -0
  71. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
  72. package/src/lib/validators/content/content-validator.js +34 -106
  73. package/src/lib/validators/nextjs/next-component-validator.js +2 -0
  74. package/src/lib/validators/validation-runner.js +2 -2
  75. package/src/utils/file-copier.js +1 -0
  76. package/src/utils/hooks-installer.js +31 -7
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Standard Digest Builder
3
+ *
4
+ * Composes agent briefings from STANDARDS.json digests.
5
+ * Reduces context per agent from ~2000-8000 tokens (file reads)
6
+ * to ~400-600 tokens (digest inline).
7
+ *
8
+ * Usage:
9
+ * buildAgentBriefing(agentId, phase) → string
10
+ * Injected into dispatch taskPrompt as "\n\nConstraints:\n" + briefing
11
+ *
12
+ * Standards refs in agents.json support two formats:
13
+ * Legacy: "core/coding.md" (treated as scope:'all', priority:'required')
14
+ * Scoped: { id, scope, priority, anchor }
15
+ */
16
+
17
+ import { readFileSync, existsSync } from 'fs';
18
+ import { join, dirname } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+
23
+ const AGENTS_JSON_PATH = join(__dirname, '../../../framework/agents.json');
24
+ const STANDARDS_JSON_PATH = join(__dirname, '../../../framework/standards/STANDARDS.json');
25
+
26
+ let _agentsCache = null;
27
+ let _standardsCache = null;
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Loaders
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ /** Load and cache agents.json */
34
+ function loadAgents() {
35
+ if (_agentsCache) return _agentsCache;
36
+ if (!existsSync(AGENTS_JSON_PATH)) return { agents: {} };
37
+ try {
38
+ _agentsCache = JSON.parse(readFileSync(AGENTS_JSON_PATH, 'utf8'));
39
+ return _agentsCache;
40
+ } catch {
41
+ return { agents: {} };
42
+ }
43
+ }
44
+
45
+ /** Load STANDARDS.json and build path/id lookup maps (module-level cache) */
46
+ function loadStandards() {
47
+ if (_standardsCache) return _standardsCache;
48
+ if (!existsSync(STANDARDS_JSON_PATH)) {
49
+ _standardsCache = { byPath: new Map(), byId: new Map() };
50
+ return _standardsCache;
51
+ }
52
+ try {
53
+ const raw = JSON.parse(readFileSync(STANDARDS_JSON_PATH, 'utf8'));
54
+ const byPath = new Map();
55
+ const byId = new Map();
56
+ for (const entry of raw.standards || []) {
57
+ byPath.set(entry.path, entry);
58
+ byId.set(entry.id, entry);
59
+ }
60
+ _standardsCache = { byPath, byId };
61
+ return _standardsCache;
62
+ } catch {
63
+ _standardsCache = { byPath: new Map(), byId: new Map() };
64
+ return _standardsCache;
65
+ }
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+ // Helpers
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Normalize a standards reference to canonical shape.
74
+ * Supports legacy string format and new scoped object format.
75
+ *
76
+ * @param {string|Object} ref
77
+ * @returns {{ id?: string, path?: string, scope: string, priority: string, anchor?: string }}
78
+ */
79
+ function normalizeRef(ref) {
80
+ if (typeof ref === 'string') {
81
+ return { path: ref, scope: 'all', priority: 'required' };
82
+ }
83
+ return {
84
+ id: ref.id,
85
+ path: ref.path,
86
+ scope: ref.scope || 'all',
87
+ priority: ref.priority || 'required',
88
+ anchor: ref.anchor,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Resolve a normalized ref to a STANDARDS.json entry.
94
+ * Tries id lookup first, then exact path, then suffix match for short paths.
95
+ *
96
+ * @param {{ id?: string, path?: string }} ref
97
+ * @param {{ byPath: Map, byId: Map }} standards
98
+ * @returns {Object|null}
99
+ */
100
+ function resolveEntry(ref, standards) {
101
+ if (ref.id) {
102
+ const entry = standards.byId.get(ref.id);
103
+ if (entry) return entry;
104
+ }
105
+ if (ref.path) {
106
+ const entry = standards.byPath.get(ref.path);
107
+ if (entry) return entry;
108
+
109
+ // Some agents use short paths like "azure.md" — try suffix match
110
+ for (const [, e] of standards.byPath) {
111
+ if (e.path.endsWith('/' + ref.path)) return e;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Format a single standards entry as a briefing line.
119
+ *
120
+ * priority='required' with digest → inline digest text
121
+ * priority='reference' → path + optional anchor only
122
+ * no digest → path reference fallback
123
+ *
124
+ * @param {Object} entry - STANDARDS.json entry
125
+ * @param {string} priority - 'required' | 'reference'
126
+ * @param {string} [anchor] - specific anchor key from entry.anchors
127
+ * @returns {string}
128
+ */
129
+ function formatBriefingLine(entry, priority, anchor) {
130
+ const anchorFrag = anchor && entry.anchors?.[anchor] ? entry.anchors[anchor] : '';
131
+
132
+ if (priority === 'required' && entry.digest) {
133
+ const ref = anchorFrag ? ` (ref: ${entry.path}${anchorFrag})` : '';
134
+ return `[${entry.name.toUpperCase()}] ${entry.digest}${ref}`;
135
+ }
136
+
137
+ if (priority === 'reference') {
138
+ return `[REF: ${entry.path}${anchorFrag}]`;
139
+ }
140
+
141
+ // No digest — inject path reference as fallback
142
+ return `[REF: ${entry.path}${anchorFrag}]`;
143
+ }
144
+
145
+ // ─────────────────────────────────────────────────────────────────────────────
146
+ // Public API
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+
149
+ /**
150
+ * Build an agent briefing string for injection into taskPrompt.
151
+ * Returns empty string when agent not found, has no standards, or all refs
152
+ * are filtered by scope.
153
+ *
154
+ * @param {string} agentId - Agent ID from agents.json
155
+ * @param {string} phase - Current phase (design|tasks|implement)
156
+ * @returns {string} Formatted briefing or ''
157
+ */
158
+ export function buildAgentBriefing(agentId, phase) {
159
+ const agents = loadAgents();
160
+ const agentData = agents.agents?.[agentId];
161
+ if (!agentData) return '';
162
+
163
+ const standardsRefs = agentData.standards || [];
164
+ if (standardsRefs.length === 0) return '';
165
+
166
+ const standards = loadStandards();
167
+ const lines = [];
168
+
169
+ for (const rawRef of standardsRefs) {
170
+ const ref = normalizeRef(rawRef);
171
+
172
+ // Scope filtering: skip refs scoped to a different phase
173
+ if (ref.scope !== 'all' && ref.scope !== phase) continue;
174
+
175
+ const entry = resolveEntry(ref, standards);
176
+ if (!entry) {
177
+ // No entry found in registry — emit a path reference as fallback
178
+ const path = ref.path || ref.id;
179
+ if (path) lines.push(`[REF: ${path}]`);
180
+ continue;
181
+ }
182
+
183
+ const line = formatBriefingLine(entry, ref.priority, ref.anchor);
184
+ if (line) lines.push(line);
185
+ }
186
+
187
+ return lines.length > 0 ? lines.join('\n') : '';
188
+ }
189
+
190
+ /**
191
+ * Build a read-only validation task prompt for Tier-4 validator agents.
192
+ * Uses hook_behavior.validates[] from agents.json.
193
+ *
194
+ * @param {string} agentId - Agent ID (must be tier 4 with hook_behavior)
195
+ * @param {string} [taskId] - Optional task ID being validated
196
+ * @returns {string} Validator task prompt or ''
197
+ */
198
+ export function buildValidatorPrompt(agentId, taskId) {
199
+ const agents = loadAgents();
200
+ const agentData = agents.agents?.[agentId];
201
+ if (!agentData?.hook_behavior) return '';
202
+
203
+ const validates = agentData.hook_behavior.validates || [];
204
+ const severity = agentData.hook_behavior.severity || 'error';
205
+ const blocksOnFail = agentData.hook_behavior.blocks_on_fail ?? true;
206
+ const taskSuffix = taskId ? ` for task ${taskId}` : '';
207
+
208
+ const lines = [
209
+ `You are ${agentData.title || agentId} (Tier 4 Validator). Mode: READ-ONLY validation${taskSuffix}.`,
210
+ `DO NOT edit any files. Review the current implementation for the following violations:`,
211
+ ``,
212
+ `Checks (severity: ${severity}):`,
213
+ ...validates.map(v => ` - ${v}`),
214
+ ``,
215
+ `Output a single JSON line: { "passed": boolean, "issues": [{ "file": "path/to/file", "line": 0, "message": "description", "rule": "rule-name" }] }`,
216
+ blocksOnFail
217
+ ? `BLOCKING: if any issues found, implementation must be fixed before task can complete.`
218
+ : `NON-BLOCKING: issues are warnings only — task completion is not blocked.`,
219
+ ];
220
+
221
+ return lines.join('\n');
222
+ }
223
+
224
+ /**
225
+ * Invalidate module-level caches.
226
+ * Used in tests to ensure a fresh load of agents.json / STANDARDS.json.
227
+ */
228
+ export function clearCache() {
229
+ _agentsCache = null;
230
+ _standardsCache = null;
231
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Task Parser — Shared utilities for reading and syncing task state.
3
+ *
4
+ * Extracted from bin/task-manager.cjs so both the task-manager and any
5
+ * future consumers can share the same parsing logic without duplication.
6
+ */
7
+
8
+ import { readFile } from 'fs/promises';
9
+ import { join } from 'path';
10
+
11
+ // ============================================================================
12
+ // parseTasksMd
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Parse tasks.md to extract task stubs for the v3 state format.
17
+ * Accepts headings in either format:
18
+ * ### T001 — Task title (em-dash / en-dash / hyphen)
19
+ * ### T001: Task title (colon)
20
+ *
21
+ * @param {string} featureName
22
+ * @param {string} [cwd] - Project root (defaults to process.cwd())
23
+ * @returns {Promise<Array<{id, title, status, dependencies, files, checkpoint}>>}
24
+ */
25
+ export async function parseTasksMd(featureName, cwd = process.cwd()) {
26
+ const tasksPath = join(cwd, `.morph/features/${featureName}/3-tasks/tasks.md`);
27
+ let content = '';
28
+ try {
29
+ content = await readFile(tasksPath, 'utf-8');
30
+ } catch {
31
+ return [];
32
+ }
33
+
34
+ const tasks = [];
35
+ const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
36
+ let match;
37
+ while ((match = headingRe.exec(content)) !== null) {
38
+ tasks.push({
39
+ id: match[1],
40
+ title: match[2].trim(),
41
+ status: 'pending',
42
+ dependencies: [],
43
+ files: [],
44
+ checkpoint: null,
45
+ });
46
+ }
47
+ return tasks;
48
+ }
49
+
50
+ // ============================================================================
51
+ // ensureTaskList
52
+ // ============================================================================
53
+
54
+ /**
55
+ * Ensure feature.taskList exists (array of individual task objects).
56
+ *
57
+ * In v3 state, feature.tasks is a counter object {total, completed, …}.
58
+ * Individual task objects live in feature.taskList.
59
+ *
60
+ * @param {Object} feature - Feature state object (mutated in place)
61
+ * @param {string} featureName
62
+ * @param {string} [cwd]
63
+ * @returns {Promise<Array>} The task list
64
+ */
65
+ export async function ensureTaskList(feature, featureName, cwd = process.cwd()) {
66
+ if (Array.isArray(feature.tasks)) {
67
+ // v2 format: tasks IS the array
68
+ return feature.tasks;
69
+ }
70
+ // v3 format: use taskList or build from tasks.md
71
+ if (!feature.taskList || feature.taskList.length === 0) {
72
+ feature.taskList = await parseTasksMd(featureName, cwd);
73
+ }
74
+ return feature.taskList;
75
+ }
76
+
77
+ // ============================================================================
78
+ // syncCounters
79
+ // ============================================================================
80
+
81
+ /**
82
+ * After modifying taskList, sync the counter fields back to feature.tasks.
83
+ * tasks.total is derived from the actual taskList length (not trusted from state).
84
+ *
85
+ * @param {Object} feature - Feature state object (mutated in place)
86
+ */
87
+ export function syncCounters(feature) {
88
+ if (Array.isArray(feature.tasks)) return; // v2 format — nothing to sync
89
+ const list = feature.taskList || [];
90
+ feature.tasks.total = list.length;
91
+ feature.tasks.completed = list.filter(t => t.status === 'completed').length;
92
+ feature.tasks.inProgress = list.filter(t => t.status === 'in_progress').length;
93
+ feature.tasks.pending = list.filter(t => t.status === 'pending').length;
94
+ }
@@ -7,6 +7,8 @@
7
7
  * @module blazor-concurrency-analyzer
8
8
  */
9
9
 
10
+ import { readFileSync } from 'fs';
11
+ import { glob } from 'glob';
10
12
  import { countIssues } from '../shared/index.js';
11
13
 
12
14
  /**
@@ -174,6 +176,9 @@ export function checkScopedInSingleton(content, filePath) {
174
176
  const issues = [];
175
177
  const lines = content.split('\n');
176
178
 
179
+ // Type guard: filePath must be a string (validation-runner may pass options object)
180
+ if (typeof filePath !== 'string') return [];
181
+
177
182
  // Check if class might be a singleton (common naming patterns)
178
183
  const isSingletonCandidate =
179
184
  filePath.includes('BackgroundService') ||
@@ -267,8 +272,42 @@ export function analyzeConcurrency(content, filePath) {
267
272
  }
268
273
 
269
274
 
275
+ /**
276
+ * Project-level concurrency validator. Scans all .cs files in projectPath.
277
+ * Follows the same interface as other project-level validators (validateNextComponentFiles, etc.)
278
+ *
279
+ * @param {string} projectPath - Project root path
280
+ * @param {Object} [_options]
281
+ * @returns {Promise<{errors: number, warnings: number, issues: Array}>}
282
+ */
283
+ export async function validateConcurrencyFiles(projectPath, _options = {}) {
284
+ const result = { errors: 0, warnings: 0, issues: [] };
285
+ const pattern = projectPath.replace(/\\/g, '/') + '/**/*.cs';
286
+ const files = await glob(pattern, { ignore: ['**/node_modules/**', '**/obj/**', '**/bin/**'] });
287
+
288
+ for (const filePath of files) {
289
+ const content = readFileSync(filePath, 'utf8');
290
+ const relPath = filePath.replace(projectPath.replace(/\\/g, '/') + '/', '');
291
+ const fileIssues = analyzeConcurrency(content, relPath);
292
+ for (const issue of fileIssues) {
293
+ result.issues.push({
294
+ level: issue.type,
295
+ message: issue.message,
296
+ file: issue.file,
297
+ line: issue.line,
298
+ solution: issue.suggestion,
299
+ });
300
+ if (issue.type === 'error') result.errors++;
301
+ else result.warnings++;
302
+ }
303
+ }
304
+
305
+ return result;
306
+ }
307
+
270
308
  export default {
271
309
  analyzeConcurrency,
310
+ validateConcurrencyFiles,
272
311
  checkScopedDbContextInBackground,
273
312
  checkHangfireWithScopedServices,
274
313
  checkScopedInSingleton,
@@ -23,16 +23,19 @@ export function validateSpecContent(specPath) {
23
23
 
24
24
  const content = readFileSync(specPath, 'utf8');
25
25
 
26
- // Required sections for a complete spec
26
+ // Required sections for a complete spec (prefix-match to handle variants like
27
+ // "## Functional Requirements" and "## Technical Architecture")
27
28
  const requiredSections = [
28
29
  '## Overview',
29
- '## Requirements',
30
- '## Technical Design',
30
+ '## Functional Requirements',
31
+ '## Technical',
31
32
  '## Data Model',
32
- '## API Contracts'
33
33
  ];
34
34
 
35
- const missing = requiredSections.filter(section => !content.includes(section));
35
+ const lines = content.split('\n');
36
+ const missing = requiredSections.filter(section =>
37
+ !lines.some(line => line.startsWith(section))
38
+ );
36
39
 
37
40
  // Additional quality checks
38
41
  const errors = [];
@@ -79,8 +82,8 @@ export function validateSpecContent(specPath) {
79
82
  }
80
83
 
81
84
  /**
82
- * Validate tasks.json structure
83
- * @param {string} tasksPath - Path to tasks.json file
85
+ * Validate tasks.md structure
86
+ * @param {string} tasksPath - Path to tasks.md file
84
87
  * @returns {Object} Validation result
85
88
  */
86
89
  export function validateTasksContent(tasksPath) {
@@ -91,111 +94,36 @@ export function validateTasksContent(tasksPath) {
91
94
  };
92
95
  }
93
96
 
94
- let tasks;
95
- try {
96
- const content = readFileSync(tasksPath, 'utf8');
97
- tasks = JSON.parse(content);
98
- } catch (error) {
99
- return {
100
- valid: false,
101
- errors: ['Invalid JSON in tasks file: ' + error.message]
102
- };
103
- }
104
-
97
+ const content = readFileSync(tasksPath, 'utf8');
105
98
  const errors = [];
106
99
  const warnings = [];
107
100
 
108
- // Check required top-level fields
109
- if (!tasks.feature) {
110
- errors.push('Missing "feature" field in tasks.json');
111
- }
101
+ // Extract task IDs from headings: ### T001 — title OR ### T001: title
102
+ const taskRe = /^###\s+(T\d{3})\s*[—–:\-]\s*(.+)$/gm;
103
+ const checkpointRe = /^###\s+(CHECKPOINT[_-]\d+)\s*[—–:\-]\s*(.+)$/gm;
112
104
 
113
- if (!tasks.tasks || !Array.isArray(tasks.tasks)) {
114
- errors.push('Missing or invalid "tasks" array in tasks.json');
115
- return { valid: false, errors, warnings };
116
- }
105
+ const tasks = [...content.matchAll(taskRe)];
106
+ const checkpoints = [...content.matchAll(checkpointRe)];
117
107
 
118
- if (tasks.tasks.length === 0) {
119
- errors.push('Tasks array is empty - no tasks defined');
108
+ if (tasks.length === 0) {
109
+ errors.push('No tasks found in tasks.md expected headings like "### T001 — Title" or "### T001: Title"');
120
110
  return { valid: false, errors, warnings };
121
111
  }
122
112
 
123
- // Validate individual tasks
124
- tasks.tasks.forEach((task, index) => {
125
- const taskId = task.id || `Task ${index}`;
113
+ // Validate no duplicate IDs
114
+ const ids = tasks.map(m => m[1]);
115
+ const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
116
+ dupes.forEach(id => errors.push(`Duplicate task ID: ${id}`));
126
117
 
127
- // Check required fields
128
- if (!task.id) {
129
- errors.push(`${taskId}: Missing "id" field`);
130
- } else if (!/^(T\d{3}|CHECKPOINT_\d{3})$/.test(task.id)) {
131
- warnings.push(`${taskId}: ID should follow format T### or CHECKPOINT_###`);
132
- }
133
-
134
- if (!task.title) {
135
- errors.push(`${taskId}: Missing "title" field`);
136
- }
137
-
138
- if (!task.description) {
139
- errors.push(`${taskId}: Missing "description" field`);
140
- }
118
+ // Check for dependency references (look for "Dependencies: T###")
119
+ const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*([T\d, ]+)/gi;
120
+ const allDepRefs = [...content.matchAll(depRe)]
121
+ .flatMap(m => m[1].split(/[,\s]+/).filter(s => /^T\d{3}$/.test(s)));
141
122
 
142
- if (!task.dependencies) {
143
- errors.push(`${taskId}: Missing "dependencies" field (use empty array if no deps)`);
144
- }
145
-
146
- // For regular tasks (not checkpoints)
147
- if (task.id && task.id.startsWith('T')) {
148
- if (!task.category) {
149
- warnings.push(`${taskId}: Missing "category" field`);
150
- }
151
-
152
- if (!task.estimatedMinutes) {
153
- warnings.push(`${taskId}: Missing "estimatedMinutes" field`);
154
- }
155
-
156
- if (!task.files || task.files.length === 0) {
157
- warnings.push(`${taskId}: No files specified - consider adding affected files`);
158
- }
159
- }
160
-
161
- // For checkpoints
162
- if (task.id && task.id.startsWith('CHECKPOINT')) {
163
- if (!task.afterTasks || task.afterTasks.length === 0) {
164
- warnings.push(`${taskId}: Checkpoint should specify "afterTasks"`);
165
- }
166
-
167
- if (!task.validations || task.validations.length === 0) {
168
- warnings.push(`${taskId}: Checkpoint should specify "validations"`);
169
- }
170
- }
171
- });
172
-
173
- // Check for orphaned tasks (missing dependencies)
174
- const taskIds = new Set(tasks.tasks.map(t => t.id));
175
- tasks.tasks.forEach(task => {
176
- if (task.dependencies && Array.isArray(task.dependencies)) {
177
- task.dependencies.forEach(depId => {
178
- if (depId && !taskIds.has(depId)) {
179
- errors.push(`${task.id}: References non-existent dependency "${depId}"`);
180
- }
181
- });
182
- }
183
- });
184
-
185
- // Check for circular dependencies (simple check)
186
- const hasCycle = (taskId, visited = new Set()) => {
187
- if (visited.has(taskId)) return true;
188
- visited.add(taskId);
189
-
190
- const task = tasks.tasks.find(t => t.id === taskId);
191
- if (!task || !task.dependencies) return false;
192
-
193
- return task.dependencies.some(depId => hasCycle(depId, new Set(visited)));
194
- };
195
-
196
- tasks.tasks.forEach(task => {
197
- if (task.id && hasCycle(task.id)) {
198
- errors.push(`Circular dependency detected involving task ${task.id}`);
123
+ const idSet = new Set(ids);
124
+ allDepRefs.forEach(depId => {
125
+ if (!idSet.has(depId)) {
126
+ warnings.push(`Dependency reference "${depId}" not found in task list`);
199
127
  }
200
128
  });
201
129
 
@@ -204,10 +132,10 @@ export function validateTasksContent(tasksPath) {
204
132
  errors,
205
133
  warnings,
206
134
  stats: {
207
- totalTasks: tasks.tasks.length,
208
- regularTasks: tasks.tasks.filter(t => t.id?.startsWith('T')).length,
209
- checkpoints: tasks.tasks.filter(t => t.id?.startsWith('CHECKPOINT')).length
210
- }
135
+ totalTasks: tasks.length + checkpoints.length,
136
+ regularTasks: tasks.length,
137
+ checkpoints: checkpoints.length,
138
+ },
211
139
  };
212
140
  }
213
141
 
@@ -86,7 +86,9 @@ export function validateNextComponent(content, filePath) {
86
86
  }
87
87
 
88
88
  // Check 2: hooks used without use client
89
+ // .ts files (custom hooks, utilities) never render JSX — skip this check
89
90
  if (!hasUseClient) {
91
+ if (!filePath.endsWith('.tsx')) return issues;
90
92
  const usedHooks = CLIENT_HOOKS.filter(hook => new RegExp(`\\b${hook}\\b`).test(content));
91
93
  if (usedHooks.length > 0) {
92
94
  issues.push({
@@ -202,8 +202,8 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
202
202
  }
203
203
 
204
204
  case 'blazor-concurrency': {
205
- const { analyzeConcurrency } = await import('./blazor/blazor-concurrency-analyzer.js');
206
- return await analyzeConcurrency(projectPath, options);
205
+ const { validateConcurrencyFiles } = await import('./blazor/blazor-concurrency-analyzer.js');
206
+ return await validateConcurrencyFiles(projectPath, options);
207
207
  }
208
208
 
209
209
  case 'blazor-state': {
@@ -136,6 +136,7 @@ export async function updateGitignore(projectPath) {
136
136
  '# MORPH-SPEC',
137
137
  '.morph/framework/',
138
138
  '.morph/memory/',
139
+ '.morph/plans/',
139
140
  '.morph/analytics/',
140
141
  '.claude/commands/',
141
142
  '.claude/rules/',
@@ -15,7 +15,7 @@ import { homedir } from 'os';
15
15
  import { execSync } from 'child_process';
16
16
 
17
17
  /** Current hooks schema version — bump when hook definitions change */
18
- const HOOKS_VERSION = '2.5.1';
18
+ const HOOKS_VERSION = '2.6.0';
19
19
 
20
20
  /** Marker for old dispatch.js (v1) */
21
21
  const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
@@ -67,10 +67,16 @@ const MORPH_HOOKS = [
67
67
  {
68
68
  event: 'UserPromptSubmit',
69
69
  matcher: null,
70
- hooks: [{
71
- type: 'command',
72
- command: 'node framework/hooks/claude-code/user-prompt/enrich-prompt.js'
73
- }]
70
+ hooks: [
71
+ {
72
+ type: 'command',
73
+ command: 'node framework/hooks/claude-code/user-prompt/enrich-prompt.js'
74
+ },
75
+ {
76
+ type: 'command',
77
+ command: 'node framework/hooks/claude-code/user-prompt/set-terminal-title.js'
78
+ }
79
+ ]
74
80
  },
75
81
 
76
82
  // === PreToolUse: Write|Edit ===
@@ -155,6 +161,20 @@ Otherwise respond: {"ok": true}`
155
161
  type: 'command',
156
162
  command: 'node framework/hooks/claude-code/notification/approval-reminder.js'
157
163
  }]
164
+ },
165
+
166
+ // === TeammateIdle ===
167
+ // Fires when an Agent Team teammate finishes and is about to go idle.
168
+ // Exit 2 = send feedback to teammate to continue working (validation failed).
169
+ // Exit 0 = teammate may become idle (validation passed).
170
+ // Requires CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (set in env above).
171
+ {
172
+ event: 'TeammateIdle',
173
+ matcher: null,
174
+ hooks: [{
175
+ type: 'command',
176
+ command: 'node framework/hooks/claude-code/teammate-idle/teammate-idle.js'
177
+ }]
158
178
  }
159
179
  ];
160
180
 
@@ -241,8 +261,12 @@ export async function installClaudeHooks(targetPath) {
241
261
  // JSON Schema for IDE auto-complete and validation
242
262
  settings['$schema'] = 'https://json.schemastore.org/claude-code-settings.json';
243
263
 
244
- // Environment variables — always set MORPH_SPEC_ACTIVE, preserve user additions
245
- settings.env = { ...settings.env, MORPH_SPEC_ACTIVE: 'true' };
264
+ // Environment variables — always set MORPH_SPEC_ACTIVE + Agent Teams flag, preserve user additions
265
+ settings.env = {
266
+ ...settings.env,
267
+ MORPH_SPEC_ACTIVE: 'true',
268
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
269
+ };
246
270
 
247
271
  // Attribution — only set if not already customized by user
248
272
  if (!settings.attribution) {