@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.
- package/README.md +379 -379
- package/bin/morph-spec.js +23 -2
- package/bin/{task-manager.cjs → task-manager.js} +249 -172
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +203 -203
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +224 -140
- package/framework/hooks/README.md +202 -202
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +12 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +34 -0
- package/framework/hooks/claude-code/statusline.py +6 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +38 -4
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
- package/framework/hooks/shared/phase-utils.js +4 -1
- package/framework/hooks/shared/state-reader.js +1 -0
- package/framework/skills/README.md +1 -0
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
- package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
- package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
- package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
- package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +50 -188
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +71 -109
- package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
- package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +171 -114
- package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +35 -159
- package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
- package/framework/squad-templates/backend-only.json +14 -1
- package/framework/squad-templates/frontend-only.json +14 -1
- package/framework/squad-templates/full-stack.json +25 -8
- package/framework/standards/STANDARDS.json +631 -86
- package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
- package/framework/templates/project/validate.js +122 -0
- package/framework/workflows/configs/zero-touch.json +7 -0
- package/package.json +87 -87
- package/src/commands/agents/dispatch-agents.js +53 -10
- package/src/commands/state/advance-phase.js +88 -13
- package/src/commands/state/index.js +2 -1
- package/src/commands/state/phase-runner.js +215 -0
- package/src/commands/tasks/task.js +25 -4
- package/src/core/paths/output-schema.js +2 -1
- package/src/lib/detectors/design-system-detector.js +5 -4
- package/src/lib/generators/recap-generator.js +16 -0
- package/src/lib/orchestration/team-orchestrator.js +171 -89
- package/src/lib/phase-chain/eligibility-checker.js +243 -0
- package/src/lib/standards/digest-builder.js +231 -0
- package/src/lib/tasks/task-parser.js +94 -0
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
- package/src/lib/validators/content/content-validator.js +34 -106
- package/src/lib/validators/nextjs/next-component-validator.js +2 -0
- package/src/lib/validators/validation-runner.js +2 -2
- package/src/utils/file-copier.js +1 -0
- 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
|
|
30
|
+
'## Functional Requirements',
|
|
31
|
+
'## Technical',
|
|
31
32
|
'## Data Model',
|
|
32
|
-
'## API Contracts'
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
const
|
|
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.
|
|
83
|
-
* @param {string} tasksPath - Path to tasks.
|
|
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
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
return { valid: false, errors, warnings };
|
|
116
|
-
}
|
|
105
|
+
const tasks = [...content.matchAll(taskRe)];
|
|
106
|
+
const checkpoints = [...content.matchAll(checkpointRe)];
|
|
117
107
|
|
|
118
|
-
if (tasks.
|
|
119
|
-
errors.push('
|
|
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
|
|
124
|
-
tasks.
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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.
|
|
208
|
-
regularTasks: tasks.
|
|
209
|
-
checkpoints:
|
|
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 {
|
|
206
|
-
return await
|
|
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': {
|
package/src/utils/file-copier.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
72
|
-
|
|
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 = {
|
|
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) {
|