@polymorphism-tech/morph-spec 8.2.0 → 8.3.0
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 +70 -99
- package/bin/morph-spec.js +13 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +76 -135
- package/docs/QUICKSTART.md +35 -51
- package/framework/CLAUDE.md +3 -2
- package/framework/MORPH.md +20 -2
- package/framework/agents.json +3 -1
- package/framework/hooks/README.md +3 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +102 -0
- package/framework/hooks/shared/build-check.js +8 -1
- package/framework/hooks/shared/payload-utils.js +16 -2
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/telemetry-logger.js +75 -0
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/schemas/feature.schema.json +0 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +12 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +22 -10
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +19 -12
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +43 -1
- package/framework/standards/ai-agents/observability-patterns.md +156 -0
- package/framework/standards/ai-agents/production.md +3 -47
- package/framework/templates/REGISTRY.json +0 -93
- package/package.json +1 -1
- package/src/commands/observability/telemetry.js +75 -0
- package/src/commands/project/worktree.js +150 -36
- package/src/commands/state/score.js +13 -2
- package/src/commands/tasks/dag.js +11 -3
- package/src/core/paths/output-schema.js +28 -0
- package/src/core/state/state-manager.js +0 -128
- package/src/dashboard/app/pages/LiveState.jsx +0 -43
- package/src/lib/detectors/design-system-detector.js +0 -43
- package/src/lib/generators/index.js +0 -1
- package/src/lib/stack/stack-profile.js +40 -9
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/tasks/test-runner.js +26 -4
- package/src/lib/telemetry/telemetry-reader.js +103 -0
- package/src/lib/validators/validation-runner.js +13 -74
- package/src/utils/hooks-installer.js +27 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +0 -269
- package/framework/templates/context/CONTEXT.md +0 -181
- package/src/lib/agents/metrics-aggregator.js +0 -56
- package/src/lib/agents/metrics-calculator.js +0 -125
- package/src/lib/generators/metadata-extractor.js +0 -385
|
@@ -51,24 +51,55 @@ function readStack(cwd) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Merge two or more profiles' validator lists (deduped) and boolean flags. */
|
|
55
|
+
function mergeProfiles(profiles) {
|
|
56
|
+
return {
|
|
57
|
+
isDotnet: profiles.some((p) => p.isDotnet),
|
|
58
|
+
isNextjs: profiles.some((p) => p.isNextjs),
|
|
59
|
+
defaultValidators: [...new Set(profiles.flatMap((p) => p.defaultValidators))],
|
|
60
|
+
checkpointValidators: [...new Set(profiles.flatMap((p) => p.checkpointValidators))],
|
|
61
|
+
buildCommand: profiles.map((p) => p.buildCommand).find(Boolean) ?? null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* Get the StackProfile for the current project.
|
|
67
|
+
*
|
|
68
|
+
* `stack` may be a compound value (e.g. "dotnet-nextjs", "dotnet+nextjs") —
|
|
69
|
+
* the framework's own convention for monorepos (see stack-filter.js's
|
|
70
|
+
* getProjectTags, which splits project.stack the same way). Each segment is
|
|
71
|
+
* resolved independently and matching profiles are merged, instead of the
|
|
72
|
+
* whole compound string failing every exact-match check and silently
|
|
73
|
+
* collapsing to the empty profile (0 default validators).
|
|
56
74
|
*/
|
|
57
75
|
export function getStackProfile(cwd, stackOverride) {
|
|
58
76
|
const root = cwd ?? process.cwd();
|
|
59
77
|
const raw = stackOverride ?? readStack(root);
|
|
60
78
|
const stack = raw?.toLowerCase() ?? 'unknown';
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
const segments = stack.split(/[-+]/).filter(Boolean);
|
|
81
|
+
|
|
82
|
+
// Single-segment stack (the overwhelming common case): resolve exactly as
|
|
83
|
+
// before, byte-for-byte, including the 'unknown' → dotnet fallback.
|
|
84
|
+
if (segments.length <= 1) {
|
|
85
|
+
let profile;
|
|
86
|
+
if (NEXTJS_STACKS.has(stack)) {
|
|
87
|
+
profile = PROFILES.nextjs;
|
|
88
|
+
} else if (NO_VALIDATOR_STACKS.has(stack)) {
|
|
89
|
+
profile = PROFILES.empty;
|
|
90
|
+
} else if (DOTNET_STACKS.has(stack) || stack === 'unknown') {
|
|
91
|
+
profile = PROFILES.dotnet;
|
|
92
|
+
} else {
|
|
93
|
+
profile = PROFILES.empty;
|
|
94
|
+
}
|
|
95
|
+
return { stack, ...profile };
|
|
71
96
|
}
|
|
72
97
|
|
|
98
|
+
// Compound stack: resolve each recognized segment and merge.
|
|
99
|
+
const matched = [];
|
|
100
|
+
if (segments.some((s) => DOTNET_STACKS.has(s))) matched.push(PROFILES.dotnet);
|
|
101
|
+
if (segments.some((s) => NEXTJS_STACKS.has(s))) matched.push(PROFILES.nextjs);
|
|
102
|
+
|
|
103
|
+
const profile = matched.length > 0 ? mergeProfiles(matched) : PROFILES.empty;
|
|
73
104
|
return { stack, ...profile };
|
|
74
105
|
}
|
|
@@ -98,6 +98,27 @@ export async function parseTasksJson(filePath) {
|
|
|
98
98
|
return data;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Lenient variant for read-only analysis commands (e.g. `morph-spec dag`):
|
|
103
|
+
* a missing `version` field is defaulted to "1.0" instead of failing
|
|
104
|
+
* validation — legacy tasks.json predating the field is still analyzable.
|
|
105
|
+
* All other schema errors still throw exactly like `parseTasksJson`.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} filePath - Absolute path to tasks.json
|
|
108
|
+
* @returns {Promise<{ data: object, versionDefaulted: boolean }>}
|
|
109
|
+
*/
|
|
110
|
+
export async function parseTasksJsonLenient(filePath) {
|
|
111
|
+
const data = await fs.readJson(filePath);
|
|
112
|
+
const versionDefaulted = data.version === undefined;
|
|
113
|
+
if (versionDefaulted) data.version = '1.0';
|
|
114
|
+
|
|
115
|
+
const { valid, errors } = validateTasksJson(data);
|
|
116
|
+
if (!valid) {
|
|
117
|
+
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
118
|
+
}
|
|
119
|
+
return { data, versionDefaulted };
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
/**
|
|
102
123
|
* Synchronous variant — used by hooks.
|
|
103
124
|
*/
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* Resolution order for test command:
|
|
5
5
|
* 1. .morph/config/config.json → project.testCommand
|
|
6
6
|
* 2. .sln or .csproj present at project root → "dotnet test"
|
|
7
|
-
* 3.
|
|
8
|
-
*
|
|
7
|
+
* 3. Nested *Tests.csproj anywhere in the tree (monorepo without a root
|
|
8
|
+
* .sln) → "dotnet test <proj> --nologo -v q" for each match
|
|
9
|
+
* 4. package.json present AND has scripts.test → "npm test"
|
|
10
|
+
* 5. null (no test suite detected)
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { execSync } from 'child_process';
|
|
12
14
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
13
15
|
import { join } from 'path';
|
|
16
|
+
import { globSync } from 'glob';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Resolve the test command for a project.
|
|
@@ -46,7 +49,26 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
46
49
|
// Unreadable project directory — fall through to next detection
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
// 3.
|
|
52
|
+
// 3. No root-level project file — look for a nested test project
|
|
53
|
+
// (e.g. tests/MyApp.Tests/MyApp.Tests.csproj), common in monorepos that
|
|
54
|
+
// have no root .sln. Run each matched project explicitly rather than a
|
|
55
|
+
// bare `dotnet test` (which finds nothing to build at the root).
|
|
56
|
+
try {
|
|
57
|
+
const testProjects = globSync('**/*Tests.csproj', {
|
|
58
|
+
cwd: projectPath,
|
|
59
|
+
ignore: ['**/bin/**', '**/obj/**', '**/node_modules/**'],
|
|
60
|
+
});
|
|
61
|
+
if (testProjects.length > 0) {
|
|
62
|
+
return testProjects
|
|
63
|
+
.sort()
|
|
64
|
+
.map((p) => `dotnet test "${p}" --nologo -v q`)
|
|
65
|
+
.join(' && ');
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Glob failure — fall through to next detection
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. package.json present AND has scripts.test → npm test
|
|
50
72
|
const packageJsonPath = join(projectPath, 'package.json');
|
|
51
73
|
if (existsSync(packageJsonPath)) {
|
|
52
74
|
try {
|
|
@@ -59,7 +81,7 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
59
81
|
}
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
//
|
|
84
|
+
// 5. Nothing found
|
|
63
85
|
return null;
|
|
64
86
|
}
|
|
65
87
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry aggregation — reads the append-only harness event stream
|
|
3
|
+
* (.morph/logs/events.jsonl, written by the telemetry-log hook) and summarizes
|
|
4
|
+
* the whole execution path: which skills ran, which personas were dispatched,
|
|
5
|
+
* which standards (knowledge base) were read, the tool mix, and the prompts.
|
|
6
|
+
*
|
|
7
|
+
* Pure aggregation (`aggregateTelemetry`) is separated from disk I/O
|
|
8
|
+
* (`loadTelemetry`) so the summary logic is unit-testable without fixtures —
|
|
9
|
+
* the same split fleet-scanner uses. CLI-side; runs in the target project cwd.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const EVENTS_FILE = join('.morph', 'logs', 'events.jsonl');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read the raw event stream from a project root. A corrupt/truncated line is
|
|
19
|
+
* skipped, not fatal. Returns [] when there is no log.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} [projectPath]
|
|
22
|
+
* @returns {Object[]}
|
|
23
|
+
*/
|
|
24
|
+
export function loadTelemetry(projectPath) {
|
|
25
|
+
try {
|
|
26
|
+
const p = join(projectPath || process.cwd(), EVENTS_FILE);
|
|
27
|
+
if (!existsSync(p)) return [];
|
|
28
|
+
const events = [];
|
|
29
|
+
for (const line of readFileSync(p, 'utf-8').split('\n')) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed) continue;
|
|
32
|
+
try { events.push(JSON.parse(trimmed)); } catch { /* skip corrupt line */ }
|
|
33
|
+
}
|
|
34
|
+
return events;
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rank(map) {
|
|
41
|
+
return [...map.entries()]
|
|
42
|
+
.map(([name, count]) => ({ name, count }))
|
|
43
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Summarize an event stream.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object[]} events - raw events (from loadTelemetry)
|
|
50
|
+
* @param {{ feature?: string }} [opts] - optional feature filter
|
|
51
|
+
* @returns {{
|
|
52
|
+
* total: number,
|
|
53
|
+
* skills: Array<{name:string,count:number}>,
|
|
54
|
+
* personas: Array<{name:string,count:number}>,
|
|
55
|
+
* standards: Array<{path:string,count:number}>,
|
|
56
|
+
* tools: Array<{name:string,count:number}>,
|
|
57
|
+
* prompts: Array<{ts:string,source:'user'|'agent',feature:?string,phase:?string,text:string}>,
|
|
58
|
+
* features: string[]
|
|
59
|
+
* }}
|
|
60
|
+
*/
|
|
61
|
+
export function aggregateTelemetry(events, opts = {}) {
|
|
62
|
+
const scoped = opts.feature
|
|
63
|
+
? (events || []).filter(e => e.feature === opts.feature)
|
|
64
|
+
: (events || []);
|
|
65
|
+
|
|
66
|
+
const skills = new Map();
|
|
67
|
+
const personas = new Map();
|
|
68
|
+
const standards = new Map();
|
|
69
|
+
const tools = new Map();
|
|
70
|
+
const features = new Set();
|
|
71
|
+
const prompts = [];
|
|
72
|
+
|
|
73
|
+
for (const e of scoped) {
|
|
74
|
+
if (e.tool) tools.set(e.tool, (tools.get(e.tool) || 0) + 1);
|
|
75
|
+
if (e.feature) features.add(e.feature);
|
|
76
|
+
|
|
77
|
+
if (e.tool === 'Skill' && e.skill) {
|
|
78
|
+
skills.set(e.skill, (skills.get(e.skill) || 0) + 1);
|
|
79
|
+
} else if ((e.tool === 'Agent' || e.tool === 'Task') && e.persona) {
|
|
80
|
+
personas.set(e.persona, (personas.get(e.persona) || 0) + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (e.kind === 'standard' && e.target) {
|
|
84
|
+
standards.set(e.target, (standards.get(e.target) || 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (e.tool === 'UserPrompt' && typeof e.prompt === 'string') {
|
|
88
|
+
prompts.push({ ts: e.ts, source: 'user', feature: e.feature ?? null, phase: e.phase ?? null, text: e.prompt });
|
|
89
|
+
} else if ((e.tool === 'Agent' || e.tool === 'Task') && typeof e.prompt === 'string' && e.prompt) {
|
|
90
|
+
prompts.push({ ts: e.ts, source: 'agent', feature: e.feature ?? null, phase: e.phase ?? null, text: e.prompt });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
total: scoped.length,
|
|
96
|
+
skills: rank(skills),
|
|
97
|
+
personas: rank(personas),
|
|
98
|
+
standards: rank(standards).map(({ name, count }) => ({ path: name, count })),
|
|
99
|
+
tools: rank(tools),
|
|
100
|
+
prompts,
|
|
101
|
+
features: [...features],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MORPH-SPEC Validation Runner
|
|
3
3
|
*
|
|
4
|
-
* Unified orchestrator that runs the right validators based on the
|
|
5
|
-
*
|
|
4
|
+
* Unified orchestrator that runs the right validators based on the project's
|
|
5
|
+
* stack profile. Called by task-done before marking tasks complete.
|
|
6
6
|
*
|
|
7
7
|
* MORPH-SPEC 3.0 - Phase A: Active Validation Pipeline
|
|
8
8
|
*/
|
|
@@ -10,16 +10,10 @@
|
|
|
10
10
|
import { existsSync, readFileSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { loadState } from '../../core/state/state-manager.js';
|
|
14
13
|
import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../core/paths/output-schema.js';
|
|
15
14
|
import { getStackProfile } from '../stack/stack-profile.js';
|
|
16
15
|
import { aggregateScores, loadRubric } from './score-aggregator.js';
|
|
17
16
|
|
|
18
|
-
/** Validators that only make sense for .NET projects. */
|
|
19
|
-
const DOTNET_ONLY_VALIDATORS = new Set([
|
|
20
|
-
'architecture', 'packages', 'contract-compliance'
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
17
|
/**
|
|
24
18
|
* Heavy validators skipped in 'light' mode (checkpoint-only).
|
|
25
19
|
* Light mode runs only spec, content, and stack-specific lightweight validators.
|
|
@@ -29,31 +23,6 @@ const HEAVY_VALIDATORS = new Set([
|
|
|
29
23
|
'css', 'design-system'
|
|
30
24
|
]);
|
|
31
25
|
|
|
32
|
-
/**
|
|
33
|
-
* Load agent → validators map from agents.json (data-driven)
|
|
34
|
-
* @param {string} projectPath - Root path of the project
|
|
35
|
-
* @returns {Object} Map of agent IDs to validator arrays
|
|
36
|
-
*/
|
|
37
|
-
function loadAgentValidatorMap(projectPath) {
|
|
38
|
-
try {
|
|
39
|
-
const agentsPath = join(projectPath, '.morph/framework/agents.json');
|
|
40
|
-
const agentsConfig = JSON.parse(readFileSync(agentsPath, 'utf8'));
|
|
41
|
-
|
|
42
|
-
const map = {};
|
|
43
|
-
Object.entries(agentsConfig.agents || {}).forEach(([agentId, agent]) => {
|
|
44
|
-
if (!agentId.startsWith('_comment') && agent.validators && agent.validators.length > 0) {
|
|
45
|
-
map[agentId] = agent.validators;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return map;
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.warn(chalk.yellow('⚠️ Failed to load agents.json - no validators will run'));
|
|
52
|
-
console.warn(chalk.gray(` Run 'morph-spec init' to set up MORPH-SPEC configuration`));
|
|
53
|
-
return {}; // Empty map = honest failure
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
26
|
/**
|
|
58
27
|
* Run all relevant validators for a feature
|
|
59
28
|
*
|
|
@@ -61,7 +30,7 @@ function loadAgentValidatorMap(projectPath) {
|
|
|
61
30
|
* @param {string} featureName - Feature name from state.json
|
|
62
31
|
* @param {Object} options - Options
|
|
63
32
|
* @param {boolean} options.verbose - Show detailed output
|
|
64
|
-
* @param {string[]} options.validators - Specific validators to run (overrides
|
|
33
|
+
* @param {string[]} options.validators - Specific validators to run (overrides stack-based detection)
|
|
65
34
|
* @returns {Object} { passed, errors, warnings, issues, summary }
|
|
66
35
|
*/
|
|
67
36
|
export async function runValidation(projectPath, featureName, options = {}) {
|
|
@@ -81,7 +50,7 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
81
50
|
};
|
|
82
51
|
|
|
83
52
|
// Determine which validators to run
|
|
84
|
-
const validatorIds = options.validators || detectValidators(
|
|
53
|
+
const validatorIds = options.validators || detectValidators(projectPath);
|
|
85
54
|
|
|
86
55
|
// Filter by mode: 'light' skips heavy validators, 'full' (default) runs all
|
|
87
56
|
const mode = options.mode || 'full';
|
|
@@ -98,17 +67,8 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
98
67
|
validatorIds.push(...constraints.enabledValidators);
|
|
99
68
|
|
|
100
69
|
if (validatorIds.length === 0) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const activeAgents = state?.features?.[featureName]?.activeAgents || [];
|
|
104
|
-
if (activeAgents.length === 0) {
|
|
105
|
-
result.summary = 'No validators ran — feature has no active agents';
|
|
106
|
-
console.log(chalk.gray('\n ℹ️ No validators configured (no active agents in state.json)'));
|
|
107
|
-
console.log(chalk.gray(' Add agents: morph-spec state add-agent ' + featureName + ' <agent-id>'));
|
|
108
|
-
} else {
|
|
109
|
-
result.summary = `No validators mapped for active agents: ${activeAgents.join(', ')}`;
|
|
110
|
-
console.log(chalk.gray(`\n ℹ️ Agents [${activeAgents.join(', ')}] have no validators in agents.json`));
|
|
111
|
-
}
|
|
70
|
+
result.summary = 'No validators ran — no validators configured for this stack';
|
|
71
|
+
console.log(chalk.gray('\n ℹ️ No validators configured for this stack'));
|
|
112
72
|
result._skipReason = result.summary;
|
|
113
73
|
return result;
|
|
114
74
|
}
|
|
@@ -124,7 +84,10 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
124
84
|
const _profileForContracts = getStackProfile(projectPath);
|
|
125
85
|
const contractsType = resolveContractsOutputType(_profileForContracts.stack);
|
|
126
86
|
const contractsPath = getAbsoluteOutputPath(projectPath, featureName, contractsType);
|
|
127
|
-
|
|
87
|
+
// Positive check on isDotnet (contract-compliance is a VSA/.NET concern) —
|
|
88
|
+
// NOT `!isNextjs`, which used to wrongly suppress it for a compound
|
|
89
|
+
// "dotnet-nextjs" stack that has a .NET backend but is ALSO isNextjs.
|
|
90
|
+
if (existsSync(contractsPath) && _profileForContracts.isDotnet) {
|
|
128
91
|
validatorIds.push('contract-compliance');
|
|
129
92
|
}
|
|
130
93
|
}
|
|
@@ -188,37 +151,13 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
188
151
|
}
|
|
189
152
|
|
|
190
153
|
/**
|
|
191
|
-
* Detect which validators to run
|
|
192
|
-
* @param {string} featureName - Feature name
|
|
154
|
+
* Detect which validators to run, driven by the project's stack profile.
|
|
193
155
|
* @param {string} projectPath - Project root path (default: current directory)
|
|
194
156
|
* @returns {string[]} Array of validator IDs to run
|
|
195
157
|
*/
|
|
196
|
-
function detectValidators(
|
|
158
|
+
function detectValidators(projectPath = '.') {
|
|
197
159
|
const profile = getStackProfile(projectPath);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!state || !state.features[featureName]) {
|
|
201
|
-
return [...profile.defaultValidators]; // stack-aware defaults
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const feature = state.features[featureName];
|
|
205
|
-
const agents = feature.activeAgents || [];
|
|
206
|
-
const validatorSet = new Set(profile.defaultValidators); // stack defaults first
|
|
207
|
-
|
|
208
|
-
const agentValidatorMap = loadAgentValidatorMap(projectPath);
|
|
209
|
-
for (const agentId of agents) {
|
|
210
|
-
const validators = agentValidatorMap[agentId] || [];
|
|
211
|
-
validators.forEach(v => validatorSet.add(v));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Filter out .NET-only validators for non-.NET projects
|
|
215
|
-
if (!profile.isDotnet) {
|
|
216
|
-
for (const v of DOTNET_ONLY_VALIDATORS) {
|
|
217
|
-
validatorSet.delete(v);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return Array.from(validatorSet);
|
|
160
|
+
return [...profile.defaultValidators];
|
|
222
161
|
}
|
|
223
162
|
|
|
224
163
|
/**
|
|
@@ -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 = '3.
|
|
18
|
+
const HOOKS_VERSION = '3.4.0';
|
|
19
19
|
|
|
20
20
|
/** Marker for old dispatch.js (v1) */
|
|
21
21
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
@@ -165,13 +165,37 @@ const MORPH_HOOKS = [
|
|
|
165
165
|
]
|
|
166
166
|
},
|
|
167
167
|
|
|
168
|
-
// === PostToolUse: All — loop detection ===
|
|
168
|
+
// === PostToolUse: All — telemetry capture (silent) + loop detection ===
|
|
169
|
+
// telemetry-log is the single generic vantage point of an NLH: it appends
|
|
170
|
+
// every tool call (skill, sub-agent dispatch, knowledge-base read, …) to
|
|
171
|
+
// .morph/logs/events.jsonl. Silent and fail-open — never blocks, never
|
|
172
|
+
// injects. `morph-spec telemetry` reads the stream back.
|
|
169
173
|
{
|
|
170
174
|
event: 'PostToolUse',
|
|
171
175
|
matcher: null,
|
|
176
|
+
hooks: [
|
|
177
|
+
{
|
|
178
|
+
type: 'command',
|
|
179
|
+
command: 'node framework/hooks/claude-code/core/telemetry-log.js',
|
|
180
|
+
timeout: 5000
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: 'command',
|
|
184
|
+
command: 'node framework/hooks/claude-code/core/loop-detect.js',
|
|
185
|
+
timeout: 5000
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// === UserPromptSubmit — telemetry capture of the user's prompt (silent) ===
|
|
191
|
+
// The only vantage point on the user's own prompt; completes the execution
|
|
192
|
+
// path alongside sub-agent prompts captured on PostToolUse.
|
|
193
|
+
{
|
|
194
|
+
event: 'UserPromptSubmit',
|
|
195
|
+
matcher: null,
|
|
172
196
|
hooks: [{
|
|
173
197
|
type: 'command',
|
|
174
|
-
command: 'node framework/hooks/claude-code/core/
|
|
198
|
+
command: 'node framework/hooks/claude-code/core/telemetry-log.js',
|
|
175
199
|
timeout: 5000
|
|
176
200
|
}]
|
|
177
201
|
},
|