@polymorphism-tech/morph-spec 9.4.0 → 9.5.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/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +23 -33
- package/framework/agents.json +6 -7
- package/framework/docs/CLI.md +3 -3
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/comment-guard.js +1 -0
- package/framework/hooks/claude-code/core/denylist.json +63 -1
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
- package/framework/hooks/claude-code/core/gate-guard.js +0 -8
- package/framework/hooks/claude-code/core/loop-detect.js +0 -3
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
- package/framework/hooks/claude-code/core/validate-completion.js +0 -4
- package/framework/hooks/claude-code/core/worktree-context.js +0 -2
- package/framework/hooks/claude-code/statusline.py +3 -44
- package/framework/hooks/shared/build-check.js +12 -19
- package/framework/hooks/shared/comment-rules.js +11 -1
- package/framework/hooks/shared/hook-response.js +0 -4
- package/framework/hooks/shared/payload-utils.js +1 -35
- package/framework/hooks/shared/phase-utils.js +0 -12
- package/framework/hooks/shared/slice-progress.js +0 -2
- package/framework/hooks/shared/state-reader.js +0 -19
- package/framework/hooks/shared/telemetry-logger.js +1 -1
- package/framework/rules/frontend-standards.md +1 -2
- package/framework/rules/nextjs-standards.md +9 -8
- 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 +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +32 -29
- package/framework/standards/STANDARDS.meta.json +13 -15
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/frontend/admin-app/data-table.md +1 -3
- package/framework/standards/frontend/admin-app/page-structure.md +8 -4
- package/framework/standards/frontend/nextjs/app-router.md +3 -4
- package/framework/standards/frontend/nextjs/components.md +2 -15
- package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
- package/framework/standards/frontend/nextjs/lint.md +141 -0
- package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
- package/framework/standards/frontend/nextjs/project-structure.md +3 -2
- package/framework/standards/frontend/nextjs/state-management.md +6 -7
- package/framework/templates/README.md +4 -4
- package/framework/templates/REGISTRY.json +21 -309
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
- package/package.json +3 -2
- package/src/core/paths/output-schema.js +0 -9
- package/src/core/state/feature-identity.js +0 -2
- package/src/core/state/state-manager.js +1 -169
- package/src/core/templates/template-renderer.js +0 -23
- package/src/dashboard/app/lib/format.js +0 -7
- package/src/dashboard/app/lib/phases.js +0 -7
- package/src/dashboard/scanner/activity-scanner.js +0 -4
- package/src/lib/ai/ai-project-scan.js +1 -6
- package/src/lib/graph/graphify-runtime.js +0 -1
- package/src/lib/mutation/snapshot.js +1 -5
- package/src/lib/risk-detector.js +0 -5
- package/src/lib/stack/stack-profile.js +0 -4
- package/src/lib/stack/stack-scan.js +15 -4
- package/src/lib/tasks/eval-node.js +1 -7
- package/src/lib/validators/validation-runner.js +0 -43
- package/src/lib/validators/verify-runner.js +18 -2
- package/src/utils/agents-installer.js +12 -19
- package/src/utils/claude-settings-manager.js +0 -28
- package/src/utils/file-copier.js +0 -53
- package/framework/hooks/shared/activity-logger.js +0 -131
- package/framework/standards/frontend/design-system/naming.md +0 -68
- package/src/commands/validation/index.js +0 -2
- package/src/lib/validators/code-budget/index.js +0 -2
- package/src/lib/validators/contracts/index.js +0 -1
- package/src/lib/validators/nextjs/index.js +0 -1
- package/src/lib/validators/packages/index.js +0 -1
- package/src/lib/validators/reuse/index.js +0 -2
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
PROTECTED_SPEC_FILES
|
|
10
10
|
} from '../../shared/phase-utils.js';
|
|
11
11
|
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
12
|
-
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
13
12
|
import { getFilePath, getToolName, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
|
|
14
13
|
import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
|
|
15
14
|
|
|
@@ -57,7 +56,6 @@ export async function run(payload) {
|
|
|
57
56
|
// Generated only by the CLI: a hand-written one would carry checks the parser never judged.
|
|
58
57
|
// Decided before the creation carve-out, since chore/hotfix are born with the plan gate stamped.
|
|
59
58
|
if (filename === CHECKS_FILE) {
|
|
60
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
61
59
|
return blockResult(
|
|
62
60
|
`MORPH-SPEC: '${CHECKS_FILE}' is generated — never written by hand.\n` +
|
|
63
61
|
`Edit 2-plan/plan.md and run \`morph-spec plan-check ${featureName}\`: it parses the plan and writes ${CHECKS_FILE}.\n` +
|
|
@@ -67,7 +65,6 @@ export async function run(payload) {
|
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
if (!isGateApproved(featureName, requiredGate, root)) {
|
|
70
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
71
68
|
return null;
|
|
72
69
|
}
|
|
73
70
|
|
|
@@ -76,7 +73,6 @@ export async function run(payload) {
|
|
|
76
73
|
// A file not yet on disk is its CREATION (chore/hotfix are born with `plan` stamped), not an edit of a frozen spec.
|
|
77
74
|
// The criterion is existence, never `approvedBy`: by signature, deleting a spec first would license rewriting it.
|
|
78
75
|
if (!existsSync(absPath)) {
|
|
79
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
|
|
80
76
|
return null;
|
|
81
77
|
}
|
|
82
78
|
|
|
@@ -90,11 +86,8 @@ export async function run(payload) {
|
|
|
90
86
|
} catch {}
|
|
91
87
|
|
|
92
88
|
if (errataCheck?.ok) {
|
|
93
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
94
89
|
return null;
|
|
95
90
|
}
|
|
96
|
-
|
|
97
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
98
91
|
return blockResult(
|
|
99
92
|
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
100
93
|
`Editing an approved artifact breaks its contract.\n\n` +
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
stateExists, getActiveFeature, getMostRecentFeature, derivePhaseForFeature,
|
|
8
8
|
} from '../../shared/state-reader.js';
|
|
9
9
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
10
|
-
import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
|
|
11
10
|
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
12
11
|
import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
|
|
13
12
|
import { sliceProgress, sliceReportPath, inspectReport } from '../../shared/slice-progress.js';
|
|
@@ -145,9 +144,6 @@ export async function run(payload) {
|
|
|
145
144
|
'MORPH-SPEC:',
|
|
146
145
|
...warnings.map(w => ` ${w}`),
|
|
147
146
|
].join('\n');
|
|
148
|
-
|
|
149
|
-
logHookActivity('validate-completion', 'Stop', `warnings(${warnings.length})`);
|
|
150
|
-
logEvent('session_end', { feature: name, phase: currentPhase, warnings: warnings.length });
|
|
151
147
|
return contextResult(message, 'Stop');
|
|
152
148
|
} catch {
|
|
153
149
|
return null;
|
|
@@ -13,7 +13,6 @@ import { sliceProgress } from '../../shared/slice-progress.js';
|
|
|
13
13
|
import { collectNextSignals, decideNext, formatNext } from '../../shared/next-step.js';
|
|
14
14
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
15
15
|
import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
|
|
16
|
-
import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
|
|
17
16
|
import { rotateEventsLogIfLarge } from '../../shared/telemetry-logger.js';
|
|
18
17
|
import { kickGraphRefresh, seedFromPrimary } from '../../shared/graph-refresh.js';
|
|
19
18
|
import { kickAgentReap } from '../../shared/agent-reap.js';
|
|
@@ -226,7 +225,6 @@ export async function run(payload) {
|
|
|
226
225
|
return null;
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
try { rotateActivityLogIfLarge(dir); } catch {}
|
|
230
228
|
try { rotateEventsLogIfLarge(dir); } catch {}
|
|
231
229
|
// Only the ROOT's graph is refreshed in background: a detached process holding a worktree directory made `finish` fail on win32.
|
|
232
230
|
try { seedFromPrimary(dir); } catch {}
|
|
@@ -457,49 +457,13 @@ def get_active_features(cwd):
|
|
|
457
457
|
return result
|
|
458
458
|
|
|
459
459
|
|
|
460
|
-
def get_last_hook(cwd):
|
|
461
|
-
"""Name of the most recent hook from .morph/logs/activity.jsonl.
|
|
462
|
-
|
|
463
|
-
activity-logger.js (R1.2) writes append-only JSONL now, one event per
|
|
464
|
-
line (`{"kind": "hook", ...}` or `{"kind": "event", ...}`), instead of a
|
|
465
|
-
single JSON doc rewritten whole on every call. The log can grow up to the
|
|
466
|
-
rotation threshold (2 MB, see ROTATE_THRESHOLD_BYTES in activity-logger.js)
|
|
467
|
-
between SessionStarts, and this function runs on every prompt redraw, so
|
|
468
|
-
only the trailing slice is read — never the whole file.
|
|
469
|
-
|
|
470
|
-
The age is deliberately not shown: activity-logger.js only records
|
|
471
|
-
HH:MM:SS, so any age computed from it is wrong across midnight.
|
|
472
|
-
"""
|
|
473
|
-
try:
|
|
474
|
-
path = Path(cwd) / '.morph' / 'logs' / 'activity.jsonl'
|
|
475
|
-
size = path.stat().st_size
|
|
476
|
-
tail_bytes = 8192
|
|
477
|
-
with path.open('rb') as f:
|
|
478
|
-
if size > tail_bytes:
|
|
479
|
-
f.seek(-tail_bytes, 2)
|
|
480
|
-
tail = f.read().decode('utf-8', errors='ignore')
|
|
481
|
-
for line in reversed(tail.splitlines()):
|
|
482
|
-
line = line.strip()
|
|
483
|
-
if not line:
|
|
484
|
-
continue
|
|
485
|
-
try:
|
|
486
|
-
obj = json.loads(line)
|
|
487
|
-
except Exception:
|
|
488
|
-
continue
|
|
489
|
-
if obj.get('kind') == 'hook':
|
|
490
|
-
return obj.get('name') or None
|
|
491
|
-
return None
|
|
492
|
-
except Exception:
|
|
493
|
-
return None
|
|
494
|
-
|
|
495
|
-
|
|
496
460
|
# ── Line renderers ────────────────────────────────────────────────────────────
|
|
497
461
|
|
|
498
|
-
def render_morph_line(feature,
|
|
462
|
+
def render_morph_line(feature, columns):
|
|
499
463
|
"""Line 0 — the active MORPH feature.
|
|
500
464
|
|
|
501
465
|
Goes through join_to_width like the status line: a feature name plus phase,
|
|
502
|
-
slice bar
|
|
466
|
+
slice bar and gate is the longest thing this script prints, and a
|
|
503
467
|
wrapping line 0 would defeat the whole point of the single-row layout.
|
|
504
468
|
"""
|
|
505
469
|
parts = [(0, f"{CYAN}{BOLD}► {feature['name']}{R}")]
|
|
@@ -523,9 +487,6 @@ def render_morph_line(feature, hook_name, columns):
|
|
|
523
487
|
if feature['pending']:
|
|
524
488
|
parts.append((2, f"{RED}⛔ gate: {feature['pending']}{R}"))
|
|
525
489
|
|
|
526
|
-
if hook_name:
|
|
527
|
-
parts.append((4, f"{GRAY}🪝 {hook_name}{R}"))
|
|
528
|
-
|
|
529
490
|
return join_to_width(parts, columns)
|
|
530
491
|
|
|
531
492
|
|
|
@@ -722,10 +683,8 @@ def main():
|
|
|
722
683
|
# worktree. In the primary tree it resurrects stale feature names
|
|
723
684
|
# during unrelated work, so there the status line is a single row.
|
|
724
685
|
if get_worktree_label(payload, cwd):
|
|
725
|
-
hook_name = get_last_hook(cwd)
|
|
726
686
|
for feature in get_active_features(cwd):
|
|
727
|
-
print(render_morph_line(feature,
|
|
728
|
-
hook_name = None # annotate the first line only
|
|
687
|
+
print(render_morph_line(feature, columns))
|
|
729
688
|
|
|
730
689
|
status_line = render_status_line(payload, cwd, settings, columns)
|
|
731
690
|
if status_line:
|
|
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
|
|
5
5
|
const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
|
|
6
6
|
const TSC_BUILD_CMD = 'npx tsc --noEmit';
|
|
7
|
+
const ESLINT_CONFIGS = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
|
|
7
8
|
|
|
8
9
|
export function isDotnetStack(s) {
|
|
9
10
|
return s.includes('dotnet') || s.includes('aspnet');
|
|
@@ -13,26 +14,9 @@ function isFrontendStack(s) {
|
|
|
13
14
|
return s.includes('nextjs') || s.includes('node') || s.includes('react') || s.includes('vue');
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
// Single stack only: a compound stack resolves to `dotnet build` alone. Use
|
|
17
|
-
// `getBuildSteps()` for those.
|
|
18
|
-
export function getBuildCommand(stack, hasTsConfig = false) {
|
|
19
|
-
if (!stack) return null;
|
|
20
|
-
const s = stack.toLowerCase();
|
|
21
|
-
|
|
22
|
-
if (isDotnetStack(s)) {
|
|
23
|
-
return DOTNET_BUILD_CMD;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (isFrontendStack(s) && hasTsConfig) {
|
|
27
|
-
return TSC_BUILD_CMD;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
17
|
// A compound stack checks BOTH sides, each from its own directory.
|
|
34
18
|
// `backendTarget` names the solution when the backend directory holds none.
|
|
35
|
-
export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
|
|
19
|
+
export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget, lintFiles = [] } = {}) {
|
|
36
20
|
if (!stack || !cwd) return [];
|
|
37
21
|
const s = stack.toLowerCase();
|
|
38
22
|
const steps = [];
|
|
@@ -51,6 +35,15 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTa
|
|
|
51
35
|
) {
|
|
52
36
|
steps.push({ command: TSC_BUILD_CMD, cwd: frontendDir });
|
|
53
37
|
}
|
|
38
|
+
// Only the files the feature touched: legacy lint debt never turns a feature's verify red.
|
|
39
|
+
if (
|
|
40
|
+
lintFiles.length > 0 &&
|
|
41
|
+
ESLINT_CONFIGS.some((name) => existsSync(join(frontendDir, name))) &&
|
|
42
|
+
existsSync(join(frontendDir, 'node_modules', 'eslint'))
|
|
43
|
+
) {
|
|
44
|
+
const files = lintFiles.map((file) => JSON.stringify(file)).join(' ');
|
|
45
|
+
steps.push({ command: `npx eslint --quiet --no-warn-ignored ${files}`, cwd: frontendDir });
|
|
46
|
+
}
|
|
54
47
|
}
|
|
55
48
|
|
|
56
49
|
return steps;
|
|
@@ -68,7 +61,7 @@ export function parseBuildResult(exitCode, output) {
|
|
|
68
61
|
|
|
69
62
|
/** @returns {{ status: 'pass'|'fail'|'skip', output: string }} */
|
|
70
63
|
export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
|
|
71
|
-
const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
|
|
64
|
+
const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget, lintFiles: paths.lintFiles });
|
|
72
65
|
if (steps.length === 0) return { status: 'skip', output: '' };
|
|
73
66
|
|
|
74
67
|
const failures = [];
|
|
@@ -37,6 +37,15 @@ function isBanner(text) {
|
|
|
37
37
|
return words === '' || /^[-=*#~_+]{3,}.*[-=*#~_+]{3,}$/.test(body);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const CODE_KEYWORD = /^(?:const|let|var|return|await|throw|if|else|for|foreach|while|switch|import|export|using|new|public|private|protected|internal|function|class)\b/;
|
|
41
|
+
|
|
42
|
+
function isCommentedCode(text) {
|
|
43
|
+
const body = bodyOf(text);
|
|
44
|
+
if (/^[}\])]+[;,)]*$/.test(body)) return true;
|
|
45
|
+
if (/\{$/.test(body)) return CODE_KEYWORD.test(body) || /[=(]/.test(body);
|
|
46
|
+
return /;$/.test(body) && (CODE_KEYWORD.test(body) || /[=(.]/.test(body));
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
function isDocComment(text, isCs) {
|
|
41
50
|
return isCs ? /^\/\/\/(?!\/)/.test(text) : /^\/\*\*(?!\*)/.test(text);
|
|
42
51
|
}
|
|
@@ -103,7 +112,7 @@ function addedRegionLines(before, after) {
|
|
|
103
112
|
return out;
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
/** @returns {Array<{ line: number, rule: 'process'|'banner'|'doc-comment'|'long-block', text: string }>} */
|
|
115
|
+
/** @returns {Array<{ line: number, rule: 'process'|'banner'|'commented-code'|'doc-comment'|'long-block', text: string }>} */
|
|
107
116
|
export function commentFindings({ file, before, after, docCommentsForbidden: docForbidden = false }) {
|
|
108
117
|
const isCs = /\.cs$/i.test(file);
|
|
109
118
|
const all = commentLines(after, file);
|
|
@@ -120,6 +129,7 @@ export function commentFindings({ file, before, after, docCommentsForbidden: doc
|
|
|
120
129
|
if (ALLOWED.some((re) => re.test(text))) continue;
|
|
121
130
|
if (citesProcess(text)) flag(line, 'process', text);
|
|
122
131
|
else if (isBanner(text)) flag(line, 'banner', text);
|
|
132
|
+
else if (!isDocComment(text, isCs) && isCommentedCode(text)) flag(line, 'commented-code', text);
|
|
123
133
|
}
|
|
124
134
|
for (const { line, text } of addedRegionLines(before, after)) flag(line, 'banner', text);
|
|
125
135
|
|
|
@@ -30,10 +30,6 @@ export function blockResult(reason) {
|
|
|
30
30
|
return { decision: 'block', reason };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function approveResult(context) {
|
|
34
|
-
return context ? { decision: 'approve', additionalContext: context } : null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
export function contextResult(text, eventName) {
|
|
38
34
|
return { hookSpecificOutput: { hookEventName: eventName, additionalContext: text } };
|
|
39
35
|
}
|
|
@@ -1,29 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join, isAbsolute } from 'path';
|
|
4
|
-
|
|
5
|
-
// A sub-agent may edit inside its own worktree: the build root comes from the
|
|
6
|
-
// file, not from the session's cwd.
|
|
7
|
-
export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.json']) {
|
|
8
|
-
try {
|
|
9
|
-
if (!filePath) return fallbackCwd;
|
|
10
|
-
let p = String(filePath).replace(/\\/g, '/');
|
|
11
|
-
if (!isAbsolute(p)) p = join(fallbackCwd, p);
|
|
12
|
-
|
|
13
|
-
let dir = dirname(p);
|
|
14
|
-
let prev = null;
|
|
15
|
-
while (dir && dir !== prev) {
|
|
16
|
-
for (const marker of markers) {
|
|
17
|
-
if (existsSync(join(dir, marker))) return dir;
|
|
18
|
-
}
|
|
19
|
-
prev = dir;
|
|
20
|
-
dir = dirname(dir);
|
|
21
|
-
}
|
|
22
|
-
return fallbackCwd;
|
|
23
|
-
} catch {
|
|
24
|
-
return fallbackCwd;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
2
|
+
import { join, isAbsolute } from 'path';
|
|
27
3
|
|
|
28
4
|
// State is resolved from the target PATH, never the session: a session in the
|
|
29
5
|
// primary root may write a feature that lives in a worktree. `lastIndexOf` picks
|
|
@@ -58,16 +34,6 @@ export function getFilePath(payload) {
|
|
|
58
34
|
return raw.replace(/\\/g, '/');
|
|
59
35
|
}
|
|
60
36
|
|
|
61
|
-
export function getContentToValidate(payload) {
|
|
62
|
-
const input = payload?.tool_input;
|
|
63
|
-
if (!input) return '';
|
|
64
|
-
return input.content || input.new_string || '';
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isWriteOperation(payload) {
|
|
68
|
-
return typeof payload?.tool_input?.content === 'string';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
37
|
export function getToolName(payload) {
|
|
72
38
|
return payload?.tool_name || 'unknown';
|
|
73
39
|
}
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// Source of truth: src/core/paths/output-schema.js
|
|
3
3
|
// Regenerate with: node scripts/generate-refs.js
|
|
4
4
|
|
|
5
|
-
export const PHASE_ORDER = ["plan","uiux","implement","review"];
|
|
6
|
-
|
|
7
5
|
export const PHASE_DIRS = {
|
|
8
6
|
plan: '2-plan',
|
|
9
7
|
uiux: '1-design',
|
|
@@ -63,16 +61,6 @@ export function isFeaturePath(filePath) {
|
|
|
63
61
|
return normalized.includes('.morph/features/');
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
export function isFrameworkPath(filePath) {
|
|
67
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
68
|
-
return normalized.includes('.morph/framework/');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function isStatePath(filePath) {
|
|
72
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
73
|
-
return normalized.endsWith('.morph/state.json');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
64
|
export function getBasename(filePath) {
|
|
77
65
|
const normalized = filePath.replace(/\\/g, '/');
|
|
78
66
|
return normalized.split('/').pop();
|
|
@@ -7,8 +7,6 @@ export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
|
7
7
|
/** `BLOQUEADO`: the slice waits on a decision only the human can make; re-dispatching it repeats the question. */
|
|
8
8
|
export const BLOCKED_MARKER = /^[\s#>*_]*BLOQUEADO\b/;
|
|
9
9
|
|
|
10
|
-
export const SLICE_STATUSES = ['pendente', 'parcial', 'bloqueado', 'feito'];
|
|
11
|
-
|
|
12
10
|
export function checksPath(featureDir) {
|
|
13
11
|
return join(featureDir, '2-plan', 'checks.json');
|
|
14
12
|
}
|
|
@@ -185,11 +185,6 @@ export function derivePhaseFrom(feature, hasArtifact) {
|
|
|
185
185
|
return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export function gateImpliedPhase(featureName, projectPath) {
|
|
189
|
-
const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
|
|
190
|
-
return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
188
|
export function derivePhaseForFeature(featureName, projectPath) {
|
|
194
189
|
const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
|
|
195
190
|
return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
|
|
@@ -219,17 +214,3 @@ export function pendingGatesFrom(feature) {
|
|
|
219
214
|
export function getPendingGates(featureName, projectPath) {
|
|
220
215
|
return pendingGatesFrom(getFeature(featureName, projectPath));
|
|
221
216
|
}
|
|
222
|
-
|
|
223
|
-
export function getOutputs(featureName, projectPath) {
|
|
224
|
-
const feature = getFeature(featureName, projectPath);
|
|
225
|
-
return feature?.outputs || null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function getMissingOutputs(featureName, projectPath) {
|
|
229
|
-
const feature = getFeature(featureName, projectPath);
|
|
230
|
-
if (!feature?.outputs) return [];
|
|
231
|
-
|
|
232
|
-
return Object.entries(feature.outputs)
|
|
233
|
-
.filter(([, output]) => !output.created)
|
|
234
|
-
.map(([type, output]) => ({ type, path: output.path }));
|
|
235
|
-
}
|
|
@@ -70,7 +70,7 @@ export function readTelemetryEvents(projectPath) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// `events-` prefix: activity-
|
|
73
|
+
// `events-` prefix: older activity-log archives in `sessions/` carry none.
|
|
74
74
|
function archiveEvents(projectPath) {
|
|
75
75
|
try {
|
|
76
76
|
const eventsPath = getEventsPath(projectPath);
|
|
@@ -9,8 +9,7 @@ stacks:
|
|
|
9
9
|
|
|
10
10
|
# Frontend Standards
|
|
11
11
|
|
|
12
|
-
- **frontend-design-system-
|
|
13
|
-
- **frontend-design-system-animations** (`frontend/design-system/animations.md`) — Classes `.animate-*`, `.duration-fast/normal/slow`, `.stagger-1..10`. Lista escalonada exige `animation-delay` E `animation-fill-mode: backwards`. Sempre o bloco `prefers-reduced-motion: reduce`. Além do CSS: Motion (`motion/react`) para enter/exit/layout; GSAP só para ScrollTrigger, SVG morph e timelines.
|
|
12
|
+
- **frontend-design-system-animations** (`frontend/design-system/animations.md`) — Lista escalonada exige `animation-delay` E `animation-fill-mode: backwards` (senão pisca antes do atraso). Sempre o bloco `prefers-reduced-motion: reduce`. Além do CSS: Motion (`motion/react`) para enter/exit/layout; GSAP só para ScrollTrigger, SVG morph e timelines.
|
|
14
13
|
- **frontend-design-system-art-direction** (`frontend/design-system/art-direction.md`) — Tela nova ou alterada segue a direção escrita em `1-design/direcao.md` e o protótipo de `1-design/prototipos/`: celular composto (folha inferior, alvos de 44px), movimento só em `transform`/`opacity` com versão estática em `prefers-reduced-motion`, nenhum número inventado em cena ilustrativa.
|
|
15
14
|
|
|
16
15
|
> Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
|
|
@@ -8,16 +8,17 @@ stacks:
|
|
|
8
8
|
|
|
9
9
|
# Next.js Standards
|
|
10
10
|
|
|
11
|
-
- **frontend-nextjs-
|
|
12
|
-
- **frontend-nextjs-
|
|
13
|
-
- **frontend-nextjs-
|
|
14
|
-
- **frontend-nextjs-
|
|
15
|
-
- **frontend-nextjs-
|
|
11
|
+
- **frontend-nextjs-lint** (`frontend/nextjs/lint.md`) — O lint cobra o mecânico: `framer-motion`, Zustand/Redux, driver de banco, Form legado, deep import de feature, fronteiras `components`/`features`/`app`/`hooks`, fetch em `useEffect`, `'use client'` no layout raiz, `export default` fora de rota, `any`. O `verify` roda o eslint nos arquivos da feature quando há `eslint.config.*`; sem `eslint.config.mjs`: `morph-spec standards --show frontend-nextjs-lint#files`. Regra mecânica nova vira linha nele, nunca prosa.
|
|
12
|
+
- **frontend-nextjs-naming-conventions** (`frontend/nextjs/naming-conventions.md`) — Arquivo SEMPRE kebab-case (`user-card.tsx`); componente PascalCase. Zod em `.schemas.ts`, tipos em `.types.ts` via `z.infer`. Nunca misture casos na mesma categoria.
|
|
13
|
+
- **frontend-nextjs-project-structure** (`frontend/nextjs/project-structure.md`) — `src/app/` só roteia; a lógica da tela mora em `src/features/{f}/` (`components/`, `hooks/`, `types/`, `index.ts` como API pública). NUNCA uma feature importa outra (extraia para `components/` ou `lib/`); `components/ui/` é shadcn e não se edita.
|
|
14
|
+
- **frontend-nextjs-app-router** (`frontend/nextjs/app-router.md`) — Server Component por padrão; `'use client'` só para interação, browser API ou TanStack Query. `loading.tsx`/`error.tsx` ao lado do `page.tsx`. Server Component chama o .NET com `BACKEND_API_PREFIX`; evite `route.ts`.
|
|
15
|
+
- **frontend-nextjs-components** (`frontend/nextjs/components.md`) — `components/ui/` é saída do shadcn CLI (nunca editar; envolva em `components/`); `components/` sem regra de negócio; `features/*/components/` conhece o domínio e entrega props tipadas, nunca a resposta crua da API.
|
|
16
|
+
- **frontend-nextjs-data-fetching** (`frontend/nextjs/data-fetching.md`) — Server Component com `BACKEND_API_PREFIX`; Client Component só pelo proxy same-origin com `PROXY_API_PREFIX`. TanStack Query 5 com key factory por feature e `invalidateQueries({ queryKey })`; toda resposta passa por Zod. PROIBIDO paginar/filtrar a coleção inteira no browser.
|
|
16
17
|
- **frontend-nextjs-forms** (`frontend/nextjs/forms.md`) — react-hook-form + Zod (`zodResolver`) + Field do shadcn via `Controller`; tipo por `z.infer`; nunca `useState` por campo. Submit por `useMutation`, erro em `root`. Edição reusa o schema de criação (`.partial()`). Zod 4: `z.email()`.
|
|
17
|
-
- **frontend-nextjs-state-management** (`frontend/nextjs/state-management.md`) — Estado de servidor no TanStack Query, nunca copiado para `useState`. Página, ordenação, filtros e aba ativa na URL (`searchParams`). Context só para tema, usuário ou locale.
|
|
18
|
+
- **frontend-nextjs-state-management** (`frontend/nextjs/state-management.md`) — Estado de servidor no TanStack Query, nunca copiado para `useState`. Página, ordenação, filtros e aba ativa na URL (`searchParams`). Context só para tema, usuário ou locale.
|
|
18
19
|
- **frontend-nextjs-testing** (`frontend/nextjs/testing.md`) — Vitest + Testing Library; MSW para API; Playwright para e2e. Teste comportamento, não implementação. Orçamento por risco: um teste por cenário com `risco:` + o de regressão do bug, componente sem risco é `leitura`/`e2e`; cenários do mesmo comportamento em `it.each`; nada de teste que só confere string de copy.
|
|
19
|
-
- **frontend-nextjs-nextjs-patterns** (`frontend/nextjs/nextjs-patterns.md`) — Route Handler é BFF: `auth.getSession()` (401 sem sessão) e delega ao .NET
|
|
20
|
-
- **frontend-nextjs-motion-patterns** (`frontend/nextjs/motion-patterns.md`) — Motion via `motion/react
|
|
20
|
+
- **frontend-nextjs-nextjs-patterns** (`frontend/nextjs/nextjs-patterns.md`) — Route Handler é BFF: `auth.getSession()` (401 sem sessão) e delega ao .NET. Zod no client e no server (`safeParse`, 400). `strict: true` + `noUncheckedIndexedAccess`. shadcn via `npx shadcn@latest add`.
|
|
21
|
+
- **frontend-nextjs-motion-patterns** (`frontend/nextjs/motion-patterns.md`) — Motion via `motion/react`. Variants em `motion/variants.ts` com durations/easings de `@/lib/design-tokens`. `useReducedMotion()` obrigatório. Cena guiada por scroll é do GSAP, nunca no mesmo elemento que o Motion.
|
|
21
22
|
- **architecture-code-comments** (`architecture/code-comments.md`) — Padrão é nenhum comentário: renomeie, extraia, tipe ou teste antes. Só o porquê que o código não mostra (invariante, ordem, quirk externo com link), até ~3 linhas. PROIBIDO: história de processo (task, UC, ADR, Gate), repetir o código, XML doc/JSDoc que repete a assinatura, banner, `#region`, código comentado, TODO solto. O hook `comment-guard` bloqueia.
|
|
22
23
|
- **architecture-engineering-lens** (`architecture/engineering-lens.md`) — Escreva para quem herda amanhã: convenção do projeto, o mais simples, causa raiz. PROIBIDO no que você adiciona: `catch` que engole, flag booleana que bifurca o método (são dois métodos), efeito colateral escondido, símbolo/flag/config sem consumidor, abstração ou camada sem segundo uso, literal repetido sem constante. Nomes de domínio buscáveis, funções pequenas, ≤ 3 parâmetros, condicional positiva. Commit em Conventional Commits (hook `conventional-commit`).
|
|
23
24
|
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando for iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o usuário pedir "use /migrate-lp-to-umbraco {projeto}" ou descrever a necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Executa as fatias do plano de uma feature morph-spec — dispatch p
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use com o gate do plano aprovado, em "use /morph-apply {feature}" ou "implementar feature"
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando chegar "/morph-hotfix {feature}", quando a morph-plan rotear o pedido como workType hotfix, ou quando o usuário disser "produção caiu / hotfix / urgente" a respeito de um incidente
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -4,7 +4,7 @@ description: Cuida da RAM e da CPU da máquina enquanto o morph-spec trabalha
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando a máquina estiver lenta ou sem RAM, um comando pesado esperou ou foi recusado pela fila, sobrou worktree de sub-agent, ou vários sub-agents pesados vão rodar em paralelo
|
|
6
6
|
user-invocable: false
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-machine
|
|
@@ -43,4 +43,4 @@ Limites: não cobre `run_in_background`, PowerShell nem comando fora da lista; u
|
|
|
43
43
|
## Referências
|
|
44
44
|
|
|
45
45
|
- `.morph/framework/CLI.md` — `### heavy`, `### worktree reap`, `### doctor --procs` e `### worktree.root`.
|
|
46
|
-
- `MORPH.md` §2.2 (worktrees) e a cláusula
|
|
46
|
+
- `MORPH.md` §2.2 (worktrees) e a cláusula PROCESSOS do contrato de dispatch.
|
|
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando a tarefa for PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisar decidir modelo, prompt e aprovação
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
stacks:
|
|
9
9
|
- dotnet
|
|
10
10
|
- nextjs
|
|
@@ -4,7 +4,7 @@ description: Leva uma feature do pedido ao plano aprovado — roteia o tipo de t
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Conduz o Gate 3 — despacha verificador ∥ revisor (∥ seguranç
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando a fase review começar, com todas as fatias com relatório final e o verify da feature verde
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX de uma feature com plano aprovado
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design, depois do gate do plano) e antes de /morph-apply
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "9.
|
|
7
|
+
cliVersion: "9.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|