@polymorphism-tech/morph-spec 8.33.0 â 8.34.2
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 +3 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +25 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +4 -5
- package/framework/agents.json +1 -0
- package/framework/docs/CLI.md +5 -2
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/infrastructure-standards.md +3 -1
- 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 +3 -3
- 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 +3 -1
- package/framework/skills/core/morph-plan/SKILL.md +6 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/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 +30 -0
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -1
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +77 -39
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +8 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/worktree.js +266 -52
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +29 -34
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +6 -11
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +21 -4
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +7 -0
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/env-allocator.js +45 -11
- package/src/lib/worktree/worktree-audit.js +40 -2
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +18 -13
- package/src/utils/hooks-installer.js +46 -3
- package/src/utils/skills-installer.js +187 -187
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* worktree-create.js â WorktreeCreate hook
|
|
4
|
+
*
|
|
5
|
+
* Event: WorktreeCreate (fires for `claude -w`, the EnterWorktree tool, and
|
|
6
|
+
* `Agent isolation:"worktree"` â measured, not assumed).
|
|
7
|
+
*
|
|
8
|
+
* Claude Code's native worktree is a bare `git worktree add` somewhere under
|
|
9
|
+
* `.claude/worktrees/`: no infra junctions (so no hooks run in there), no port
|
|
10
|
+
* block, no identity, invisible to the fleet. When this hook is registered it
|
|
11
|
+
* REPLACES that logic entirely: it must print the path of the tree it created
|
|
12
|
+
* as the last non-empty stdout line and exit 0; any non-zero exit fails the
|
|
13
|
+
* creation. It cannot return JSON â the standalone tail below is therefore
|
|
14
|
+
* NOT `runStandalone` (which prints a JSON verdict). Diagnostics go to stderr.
|
|
15
|
+
*
|
|
16
|
+
* What it does:
|
|
17
|
+
* 1. root = the PRIMARY root of the repo `payload.cwd` is in (never a
|
|
18
|
+
* secondary worktree â a tree under a tree is the fleet-invisible shape).
|
|
19
|
+
* 2. name â kebab slug (`Bold_Oak` â `bold-oak`, `agent-a1b2` stays).
|
|
20
|
+
* 3. feature or task? `<root>/.morph/features/<n>/feature.json` exists, or
|
|
21
|
+
* branch `morph/<n>` exists â `morph-spec worktree setup <n>`; otherwise
|
|
22
|
+
* `morph-spec worktree provision <n> [--base <ref>]`.
|
|
23
|
+
* 4. `worktree.baseRef` from the merged settings (`~/.claude/settings.json`
|
|
24
|
+
* < `<root>/.claude/settings.json` < `<root>/.claude/settings.local.json`):
|
|
25
|
+
* `"head"` â the current branch (or sha) of `payload.cwd`; anything else
|
|
26
|
+
* â the repo's default branch (the CLI's own default).
|
|
27
|
+
* 5. CLI exit 0 or 2 (already exists) â its `path`. Exit 1/3 â error.
|
|
28
|
+
* CLI unavailable â in-hook git fallback that still lands the tree under
|
|
29
|
+
* `<root>/worktrees/<n>` on `morph-task/<n>` (degraded: no junctions, no
|
|
30
|
+
* ports â but never outside the governed directory).
|
|
31
|
+
*
|
|
32
|
+
* `stateExists(root)` gate: outside a morph project (no `.morph/state.json`
|
|
33
|
+
* and no feature.json) there is nothing to govern â the same in-hook git
|
|
34
|
+
* fallback runs, so a non-morph repo with this hook installed still gets a
|
|
35
|
+
* worktree instead of a failed creation.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
39
|
+
import { execFileSync } from 'child_process';
|
|
40
|
+
import { dirname, join, resolve } from 'path';
|
|
41
|
+
import { homedir } from 'os';
|
|
42
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
43
|
+
import { resolveGitCommonDir } from '../../shared/git-paths.js';
|
|
44
|
+
import { runMorphSpec } from '../../shared/morph-cli.js';
|
|
45
|
+
|
|
46
|
+
const WORKTREES_DIR = 'worktrees';
|
|
47
|
+
|
|
48
|
+
const fwd = (p) => String(p).replace(/\\/g, '/');
|
|
49
|
+
|
|
50
|
+
function git(args, cwd) {
|
|
51
|
+
return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The primary root of the repo containing `startDir`. `git worktree list`'s
|
|
56
|
+
* first entry is the primary checkout, from any worktree; the pure-filesystem
|
|
57
|
+
* common-dir resolver is the no-subprocess fallback. Null outside a repo.
|
|
58
|
+
*/
|
|
59
|
+
export function resolvePrimaryRoot(startDir) {
|
|
60
|
+
try {
|
|
61
|
+
const first = git(['worktree', 'list', '--porcelain'], startDir).split('\n').find((l) => l.startsWith('worktree '));
|
|
62
|
+
if (first) return fwd(first.slice('worktree '.length).trim());
|
|
63
|
+
} catch { /* fall through */ }
|
|
64
|
+
const common = resolveGitCommonDir(startDir);
|
|
65
|
+
return common ? fwd(dirname(common)) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `Bold_Oak` â `bold-oak`; empty â a timestamped slug. */
|
|
69
|
+
export function slugify(name) {
|
|
70
|
+
const slug = String(name || '')
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
73
|
+
.replace(/-{2,}/g, '-')
|
|
74
|
+
.replace(/^-+|-+$/g, '');
|
|
75
|
+
return slug || `wt-${Date.now().toString(36)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readJsonSafe(path) {
|
|
79
|
+
try { return JSON.parse(readFileSync(path, 'utf8').replace(/^īģŋ/, '')); } catch { return null; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `worktree.baseRef` across the three settings layers, last one wins.
|
|
84
|
+
* Fail-open per file. Returns `'fresh'` (the documented default) when unset.
|
|
85
|
+
*/
|
|
86
|
+
export function readBaseRefSetting(root, home = homedir()) {
|
|
87
|
+
let value = 'fresh';
|
|
88
|
+
for (const p of [
|
|
89
|
+
join(home, '.claude', 'settings.json'),
|
|
90
|
+
join(root, '.claude', 'settings.json'),
|
|
91
|
+
join(root, '.claude', 'settings.local.json'),
|
|
92
|
+
]) {
|
|
93
|
+
const s = readJsonSafe(p);
|
|
94
|
+
const v = s?.worktree?.baseRef;
|
|
95
|
+
if (typeof v === 'string' && v) value = v;
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The ref `baseRef:"head"` means: the branch checked out in `cwd`, or its sha. */
|
|
101
|
+
function headRef(cwd) {
|
|
102
|
+
try {
|
|
103
|
+
const name = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
104
|
+
if (name && name !== 'HEAD') return name;
|
|
105
|
+
return git(['rev-parse', 'HEAD'], cwd) || null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isKnownFeature(root, name) {
|
|
112
|
+
if (existsSync(join(root, '.morph', 'features', name, 'feature.json'))) return true;
|
|
113
|
+
try {
|
|
114
|
+
git(['show-ref', '--verify', '--quiet', `refs/heads/morph/${name}`], root);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function defaultBranch(root) {
|
|
122
|
+
try {
|
|
123
|
+
const m = git(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], root).match(/^refs\/remotes\/origin\/(.+)$/);
|
|
124
|
+
if (m) return m[1];
|
|
125
|
+
} catch { /* no origin/HEAD */ }
|
|
126
|
+
for (const c of ['main', 'master']) {
|
|
127
|
+
try { git(['show-ref', '--verify', '--quiet', `refs/heads/${c}`], root); return c; } catch { /* next */ }
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Degraded path â the CLI is not reachable, but the tree still has to land
|
|
134
|
+
* in the governed directory on the governed branch namespace.
|
|
135
|
+
*/
|
|
136
|
+
function gitFallback(root, name, base) {
|
|
137
|
+
const branch = `morph-task/${name}`;
|
|
138
|
+
const path = `${root}/${WORKTREES_DIR}/${name}`;
|
|
139
|
+
if (existsSync(path)) return { path };
|
|
140
|
+
try { git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], root); }
|
|
141
|
+
catch {
|
|
142
|
+
const from = base || defaultBranch(root);
|
|
143
|
+
git(from ? ['branch', branch, from] : ['branch', branch], root);
|
|
144
|
+
}
|
|
145
|
+
mkdirSync(join(root, WORKTREES_DIR), { recursive: true });
|
|
146
|
+
git(['worktree', 'add', path, branch], root);
|
|
147
|
+
return { path };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {object} payload - parsed WorktreeCreate stdin payload
|
|
152
|
+
* ({ session_id, cwd, hook_event_name, name })
|
|
153
|
+
* @returns {Promise<{ path: string }|{ error: string }>}
|
|
154
|
+
*/
|
|
155
|
+
export async function run(payload) {
|
|
156
|
+
const start = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
157
|
+
const root = resolvePrimaryRoot(start) || (process.env.CLAUDE_PROJECT_DIR ? fwd(resolve(process.env.CLAUDE_PROJECT_DIR)) : null);
|
|
158
|
+
if (!root) return { error: `worktree-create: "${start}" is not inside a git repository` };
|
|
159
|
+
|
|
160
|
+
const name = slugify(payload?.name);
|
|
161
|
+
const baseSetting = readBaseRefSetting(root);
|
|
162
|
+
const base = baseSetting === 'head' ? headRef(start) : null;
|
|
163
|
+
|
|
164
|
+
// Outside a morph project there is nothing to govern beyond WHERE the tree
|
|
165
|
+
// lands â plain git, under <root>/worktrees/.
|
|
166
|
+
if (!stateExists(root)) {
|
|
167
|
+
try { return gitFallback(root, name, base); }
|
|
168
|
+
catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const args = isKnownFeature(root, name)
|
|
172
|
+
? ['worktree', 'setup', name, '--json']
|
|
173
|
+
: ['worktree', 'provision', name, '--json', ...(base ? ['--base', base] : [])];
|
|
174
|
+
|
|
175
|
+
const r = runMorphSpec(args, { cwd: root });
|
|
176
|
+
if (r.unavailable) {
|
|
177
|
+
process.stderr.write('worktree-create: morph-spec CLI not found â creating the tree with plain git under worktrees/ (no infra links, no port block). Run `morph-spec worktree link --all` inside it.\n');
|
|
178
|
+
try { return gitFallback(root, name, base); }
|
|
179
|
+
catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
|
|
180
|
+
}
|
|
181
|
+
if (r.stderr?.trim()) process.stderr.write(r.stderr);
|
|
182
|
+
|
|
183
|
+
const ok = r.code === 0 || r.code === 2;
|
|
184
|
+
const path = r.json?.path;
|
|
185
|
+
if (ok && path && !r.json?.error) return { path: fwd(path) };
|
|
186
|
+
const why = r.json?.error || r.json?.hint || r.stderr?.trim() || `exit ${r.code}`;
|
|
187
|
+
return { error: `worktree-create: morph-spec ${args.slice(0, 2).join(' ')} ${name} failed â ${why}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Standalone tail â NOT runStandalone(): this event's contract is "path on
|
|
191
|
+
// stdout, exit 0" / "non-zero exit", never a JSON verdict.
|
|
192
|
+
if (!process.env.MORPH_HOOK_CHAINED) {
|
|
193
|
+
const { readStdin } = await import('../../shared/stdin-reader.js');
|
|
194
|
+
let result;
|
|
195
|
+
try {
|
|
196
|
+
result = await run(await readStdin());
|
|
197
|
+
} catch (err) {
|
|
198
|
+
result = { error: `worktree-create: ${err?.message || err}` };
|
|
199
|
+
}
|
|
200
|
+
if (result?.path) {
|
|
201
|
+
process.stdout.write(`${result.path}\n`);
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
process.stderr.write(`${result?.error || 'worktree-create: no path produced'}\n`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* worktree-env-inject.js â PreToolUse hook (Bash matcher), last link of the
|
|
4
|
+
* Bash chain.
|
|
5
|
+
*
|
|
6
|
+
* `.morph/worktree.env` used to be a manifest nobody executed: SessionStart
|
|
7
|
+
* printed the port block and the model was expected to pass `PORT` by hand
|
|
8
|
+
* every time it started a dev server. Two worktrees whose sessions forgot
|
|
9
|
+
* collided on 3000 â a runtime clash dressed as a broken app. This hook makes
|
|
10
|
+
* the block reach the process deterministically: when the command is a
|
|
11
|
+
* dev-server / compose / spec launcher and the cwd is a worktree that owns a
|
|
12
|
+
* manifest, the command is rewritten to `export K='V' âĻ; <command>` via
|
|
13
|
+
* `updatedInput`, and the rewrite is named in `additionalContext`.
|
|
14
|
+
*
|
|
15
|
+
* Scope is deliberately narrow: only launcher-shaped commands, only when the
|
|
16
|
+
* command does not already set a layout key itself (`PORT=âĻ`, `--port`,
|
|
17
|
+
* `compose -p`), only for the Bash tool. Everything else passes untouched.
|
|
18
|
+
*
|
|
19
|
+
* Fail-open: no state, no manifest, unreadable manifest â null (pass).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
import { join } from 'path';
|
|
24
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
25
|
+
import { updatedInputResult, runStandalone } from '../../shared/hook-response.js';
|
|
26
|
+
import { loadEnvFile } from '../../shared/e2e-check.js';
|
|
27
|
+
|
|
28
|
+
/** Commands that start a server or a stack and therefore need the block. */
|
|
29
|
+
const LAUNCHER_RE = /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start)\b|next\s+(?:dev|start)\b|vite\b|dotnet\s+(?:run|watch)\b|docker(?:\s+|-)compose\b|playwright\s+test\b)/;
|
|
30
|
+
|
|
31
|
+
/** The command already chose its own port/project â respect it. */
|
|
32
|
+
const SELF_CONFIGURED_RE = /(?:^|[\s;&|])(?:PORT|API_PORT|POSTGRES_PORT|COMPOSE_PROJECT_NAME|ASPNETCORE_URLS)=|--port\b|(?:^|\s)-p\s+\d|compose\b[^|;&]*\s-p\s/;
|
|
33
|
+
|
|
34
|
+
/** Keys the layout owns; anything else in the manifest is left alone. */
|
|
35
|
+
const LAYOUT_KEYS = ['COMPOSE_PROJECT_NAME', 'MORPH_PORT_BASE', 'MORPH_PORT_RANGE', 'PORT', 'API_PORT', 'POSTGRES_PORT', 'REDIS_PORT', 'ASPNETCORE_URLS', 'PLAYWRIGHT_BASE_URL'];
|
|
36
|
+
|
|
37
|
+
/** POSIX single-quote, safe for Git Bash on Windows too. */
|
|
38
|
+
function shq(v) {
|
|
39
|
+
return `'${String(v).replace(/'/g, "'\\''")}'`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure decision: the rewritten command, or null when nothing should change.
|
|
44
|
+
* @param {string} command
|
|
45
|
+
* @param {Record<string,string>} env - parsed manifest
|
|
46
|
+
* @returns {{ command: string, injected: string[] }|null}
|
|
47
|
+
*/
|
|
48
|
+
export function rewriteCommand(command, env) {
|
|
49
|
+
if (!command || !LAUNCHER_RE.test(command)) return null;
|
|
50
|
+
if (SELF_CONFIGURED_RE.test(command)) return null;
|
|
51
|
+
const pairs = LAYOUT_KEYS.filter((k) => env[k]).map((k) => [k, env[k]]);
|
|
52
|
+
if (!pairs.some(([k]) => k === 'PORT')) return null;
|
|
53
|
+
// The web tier's URL is what the spec suite needs; derive it once here.
|
|
54
|
+
if (!env.PLAYWRIGHT_BASE_URL) pairs.push(['PLAYWRIGHT_BASE_URL', `http://localhost:${env.PORT}`]);
|
|
55
|
+
const prefix = `export ${pairs.map(([k, v]) => `${k}=${shq(v)}`).join(' ')}; `;
|
|
56
|
+
return { command: prefix + command, injected: pairs.map(([k, v]) => `${k}=${v}`) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
61
|
+
* @returns {Promise<object|null>} an updatedInput result, or null (pass).
|
|
62
|
+
*/
|
|
63
|
+
export async function run(payload) {
|
|
64
|
+
try {
|
|
65
|
+
if (payload?.tool_name && payload.tool_name !== 'Bash') return null;
|
|
66
|
+
if (!stateExists()) return null;
|
|
67
|
+
const command = payload?.tool_input?.command || '';
|
|
68
|
+
if (!command) return null;
|
|
69
|
+
|
|
70
|
+
const dir = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
71
|
+
const manifest = join(dir, '.morph', 'worktree.env');
|
|
72
|
+
if (!existsSync(manifest)) return null;
|
|
73
|
+
|
|
74
|
+
const rewritten = rewriteCommand(command, loadEnvFile(manifest));
|
|
75
|
+
if (!rewritten) return null;
|
|
76
|
+
|
|
77
|
+
return updatedInputResult(
|
|
78
|
+
{ ...payload.tool_input, command: rewritten.command },
|
|
79
|
+
'PreToolUse',
|
|
80
|
+
`đ worktree env injetado (.morph/worktree.env): ${rewritten.injected.join(' ')}`,
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await runStandalone(run);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* worktree-remove.js â WorktreeRemove hook
|
|
4
|
+
*
|
|
5
|
+
* Event: WorktreeRemove (Claude Code's own cleanup of a tree it created via
|
|
6
|
+
* `claude -w` / EnterWorktree / Agent isolation). Input carries
|
|
7
|
+
* `worktree_path` (absolute). No decision control, output ignored â this hook
|
|
8
|
+
* ALWAYS exits 0.
|
|
9
|
+
*
|
|
10
|
+
* Why it exists: Claude Code's native removal is a raw `git worktree remove`,
|
|
11
|
+
* which FOLLOWS the infra junctions the WorktreeCreate hook put in that tree
|
|
12
|
+
* and empties the primary root's `.claude/` and `.morph/framework/`. This
|
|
13
|
+
* hook routes the removal through `morph-spec worktree remove --target
|
|
14
|
+
* --if-clean` instead: junctions dropped first, PROVE-or-DECLINE, and a tree
|
|
15
|
+
* holding uncommitted work is left alone (Claude Code's notion of "clean" is
|
|
16
|
+
* not the user's).
|
|
17
|
+
*
|
|
18
|
+
* Secondary net: in headless runs the event was observed NOT to fire; the
|
|
19
|
+
* PreToolUse guard (block-destructive-worktree-removal) stays the primary
|
|
20
|
+
* defense. Fail-open everywhere â diagnostics to stderr, never a throw, never
|
|
21
|
+
* a non-zero exit.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync } from 'fs';
|
|
25
|
+
import { execFileSync } from 'child_process';
|
|
26
|
+
import { dirname, resolve } from 'path';
|
|
27
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
28
|
+
import { resolveGitCommonDir } from '../../shared/git-paths.js';
|
|
29
|
+
import { runMorphSpec } from '../../shared/morph-cli.js';
|
|
30
|
+
|
|
31
|
+
const fwd = (p) => String(p).replace(/\\/g, '/');
|
|
32
|
+
|
|
33
|
+
function primaryRoot(startDir) {
|
|
34
|
+
try {
|
|
35
|
+
const out = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: startDir, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
36
|
+
const first = out.split('\n').find((l) => l.startsWith('worktree '));
|
|
37
|
+
if (first) return fwd(first.slice('worktree '.length).trim());
|
|
38
|
+
} catch { /* fall through */ }
|
|
39
|
+
const common = resolveGitCommonDir(startDir);
|
|
40
|
+
return common ? fwd(dirname(common)) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} payload - parsed WorktreeRemove stdin payload ({ cwd, worktree_path })
|
|
45
|
+
* @returns {Promise<null>} always null â the event ignores output
|
|
46
|
+
*/
|
|
47
|
+
export async function run(payload) {
|
|
48
|
+
try {
|
|
49
|
+
const target = payload?.worktree_path;
|
|
50
|
+
if (!target || !existsSync(target)) return null;
|
|
51
|
+
|
|
52
|
+
const start = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || dirname(resolve(target));
|
|
53
|
+
const root = primaryRoot(start) || primaryRoot(dirname(resolve(target)));
|
|
54
|
+
if (!root) return null;
|
|
55
|
+
// Nothing to govern outside a morph project â and never a raw removal
|
|
56
|
+
// from here either: the tree is simply left for git's own cleanup.
|
|
57
|
+
if (!stateExists(root)) return null;
|
|
58
|
+
|
|
59
|
+
const r = runMorphSpec(['worktree', 'remove', '--target', fwd(resolve(target)), '--if-clean', '--json'], { cwd: root });
|
|
60
|
+
if (r.unavailable) {
|
|
61
|
+
process.stderr.write(`worktree-remove: morph-spec CLI not found â "${target}" left in place (remove it with \`morph-spec worktree remove --target\`, never with raw git: it follows the infra junctions out).\n`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (!r.ok) {
|
|
65
|
+
const why = r.json?.error || r.json?.hint || r.stderr?.trim() || `exit ${r.code}`;
|
|
66
|
+
process.stderr.write(`worktree-remove: "${target}" NOT removed â ${why}\n`);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
process.stderr.write(`worktree-remove: ${err?.message || err}\n`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Standalone tail: output is ignored by this event, so nothing is printed;
|
|
76
|
+
// the exit code is always 0.
|
|
77
|
+
if (!process.env.MORPH_HOOK_CHAINED) {
|
|
78
|
+
const { readStdin } = await import('../../shared/stdin-reader.js');
|
|
79
|
+
try { await run(await readStdin()); } catch { /* fail-open */ }
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# morph-spec statusline â installed globally to ~/.claude/statusline.sh
|
|
3
|
-
# Claude Code invokes this with JSON via stdin after each response.
|
|
4
|
-
# Requires: Python 3 available as `python3` or `python` on PATH.
|
|
5
|
-
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
if command -v python3 &>/dev/null; then
|
|
8
|
-
python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
9
|
-
elif command -v python &>/dev/null; then
|
|
10
|
-
python "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
11
|
-
fi
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# morph-spec statusline â installed globally to ~/.claude/statusline.sh
|
|
3
|
+
# Claude Code invokes this with JSON via stdin after each response.
|
|
4
|
+
# Requires: Python 3 available as `python3` or `python` on PATH.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
if command -v python3 &>/dev/null; then
|
|
8
|
+
python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
9
|
+
elif command -v python &>/dev/null; then
|
|
10
|
+
python "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
11
|
+
fi
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Scope: universal | Validates commit message format (Conventional Commits)
|
|
3
|
-
# MORPH-SPEC Commit Message Hook
|
|
4
|
-
# Enforces Conventional Commits specification
|
|
5
|
-
|
|
6
|
-
COMMIT_MSG_FILE="$1"
|
|
7
|
-
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
8
|
-
|
|
9
|
-
# Conventional Commits pattern: type(scope): description
|
|
10
|
-
# Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert
|
|
11
|
-
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .{1,100}"
|
|
12
|
-
|
|
13
|
-
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
|
|
14
|
-
echo "â Invalid commit message format!"
|
|
15
|
-
echo ""
|
|
16
|
-
echo "Required format: <type>(<scope>): <description>"
|
|
17
|
-
echo ""
|
|
18
|
-
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert"
|
|
19
|
-
echo ""
|
|
20
|
-
echo "Examples:"
|
|
21
|
-
echo " feat(auth): add JWT refresh token rotation"
|
|
22
|
-
echo " fix(api): resolve null reference in UserService"
|
|
23
|
-
echo " docs(readme): update installation instructions"
|
|
24
|
-
echo ""
|
|
25
|
-
echo "Your message:"
|
|
26
|
-
echo " $COMMIT_MSG"
|
|
27
|
-
echo ""
|
|
28
|
-
echo "Override with: git commit --no-verify"
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
echo "â
Commit message format valid"
|
|
33
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scope: universal | Validates commit message format (Conventional Commits)
|
|
3
|
+
# MORPH-SPEC Commit Message Hook
|
|
4
|
+
# Enforces Conventional Commits specification
|
|
5
|
+
|
|
6
|
+
COMMIT_MSG_FILE="$1"
|
|
7
|
+
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
8
|
+
|
|
9
|
+
# Conventional Commits pattern: type(scope): description
|
|
10
|
+
# Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert
|
|
11
|
+
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .{1,100}"
|
|
12
|
+
|
|
13
|
+
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
|
|
14
|
+
echo "â Invalid commit message format!"
|
|
15
|
+
echo ""
|
|
16
|
+
echo "Required format: <type>(<scope>): <description>"
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert"
|
|
19
|
+
echo ""
|
|
20
|
+
echo "Examples:"
|
|
21
|
+
echo " feat(auth): add JWT refresh token rotation"
|
|
22
|
+
echo " fix(api): resolve null reference in UserService"
|
|
23
|
+
echo " docs(readme): update installation instructions"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Your message:"
|
|
26
|
+
echo " $COMMIT_MSG"
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Override with: git commit --no-verify"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "â
Commit message format valid"
|
|
33
|
+
exit 0
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Scope: universal | Validates agents.json Schema
|
|
3
|
-
# MORPH-SPEC Pre-Commit Hook: Agent Configuration Validation
|
|
4
|
-
# Uses validate-agents.js to check agents.json
|
|
5
|
-
|
|
6
|
-
echo "đ¤ Validating agent configuration..."
|
|
7
|
-
|
|
8
|
-
# Check if agents.json is being modified
|
|
9
|
-
if git diff --cached --name-only | grep -q 'agents\.json$'; then
|
|
10
|
-
echo "Detected changes to agents.json"
|
|
11
|
-
|
|
12
|
-
# Run validator
|
|
13
|
-
if ! morph-spec validate-agents-skills; then
|
|
14
|
-
echo ""
|
|
15
|
-
echo "â COMMIT BLOCKED: agents.json validation failed"
|
|
16
|
-
echo " Fix errors above before committing"
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
echo "â agents.json is valid"
|
|
21
|
-
else
|
|
22
|
-
echo "â No changes to agents.json"
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
exit 0
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Scope: universal | Validates agents.json Schema
|
|
3
|
+
# MORPH-SPEC Pre-Commit Hook: Agent Configuration Validation
|
|
4
|
+
# Uses validate-agents.js to check agents.json
|
|
5
|
+
|
|
6
|
+
echo "đ¤ Validating agent configuration..."
|
|
7
|
+
|
|
8
|
+
# Check if agents.json is being modified
|
|
9
|
+
if git diff --cached --name-only | grep -q 'agents\.json$'; then
|
|
10
|
+
echo "Detected changes to agents.json"
|
|
11
|
+
|
|
12
|
+
# Run validator
|
|
13
|
+
if ! morph-spec validate-agents-skills; then
|
|
14
|
+
echo ""
|
|
15
|
+
echo "â COMMIT BLOCKED: agents.json validation failed"
|
|
16
|
+
echo " Fix errors above before committing"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
echo "â agents.json is valid"
|
|
21
|
+
else
|
|
22
|
+
echo "â No changes to agents.json"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
exit 0
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Scope: universal | Orchestrates framework + stack-specific pre-commit hooks
|
|
3
|
-
# MORPH-SPEC Master Pre-Commit Hook
|
|
4
|
-
# Runs universal framework hooks + stack-specific hooks in sequence
|
|
5
|
-
|
|
6
|
-
echo "ââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
7
|
-
echo "â MORPH-SPEC PRE-COMMIT VALIDATION â"
|
|
8
|
-
echo "ââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
9
|
-
echo ""
|
|
10
|
-
|
|
11
|
-
FRAMEWORK_HOOKS_DIR="$(dirname "$0")"
|
|
12
|
-
STACK_HOOKS_DIR=".morph/hooks/pre-commit"
|
|
13
|
-
|
|
14
|
-
HAS_FAILURES=false
|
|
15
|
-
|
|
16
|
-
# Run framework hooks (universal - all stacks)
|
|
17
|
-
echo "đĻ Running framework hooks (universal)..."
|
|
18
|
-
for hook in "$FRAMEWORK_HOOKS_DIR"/*.sh; do
|
|
19
|
-
# Skip orchestrator itself
|
|
20
|
-
if [[ "$hook" == */orchestrator.sh ]]; then
|
|
21
|
-
continue
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
if [[ -f "$hook" ]]; then
|
|
25
|
-
hook_name=$(basename "$hook")
|
|
26
|
-
echo "âââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
27
|
-
if ! bash "$hook"; then
|
|
28
|
-
HAS_FAILURES=true
|
|
29
|
-
echo "â Framework hook failed: $hook_name"
|
|
30
|
-
fi
|
|
31
|
-
echo ""
|
|
32
|
-
fi
|
|
33
|
-
done
|
|
34
|
-
|
|
35
|
-
# Run stack-specific hooks (if exist)
|
|
36
|
-
if [[ -d "$STACK_HOOKS_DIR" ]]; then
|
|
37
|
-
echo "đ¯ Running stack-specific hooks..."
|
|
38
|
-
for hook in "$STACK_HOOKS_DIR"/*.sh; do
|
|
39
|
-
if [[ -f "$hook" ]]; then
|
|
40
|
-
hook_name=$(basename "$hook")
|
|
41
|
-
echo "âââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
42
|
-
if ! bash "$hook"; then
|
|
43
|
-
HAS_FAILURES=true
|
|
44
|
-
echo "â Stack hook failed: $hook_name"
|
|
45
|
-
fi
|
|
46
|
-
echo ""
|
|
47
|
-
fi
|
|
48
|
-
done
|
|
49
|
-
else
|
|
50
|
-
echo "âšī¸ No stack-specific hooks found (optional)"
|
|
51
|
-
echo ""
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
echo "âââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
55
|
-
|
|
56
|
-
if [[ "$HAS_FAILURES" == "true" ]]; then
|
|
57
|
-
echo "â PRE-COMMIT VALIDATION FAILED"
|
|
58
|
-
echo " Fix errors above before committing"
|
|
59
|
-
echo " Or use: git commit --no-verify to skip"
|
|
60
|
-
exit 1
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
echo "â
ALL PRE-COMMIT VALIDATIONS PASSED"
|
|
64
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scope: universal | Orchestrates framework + stack-specific pre-commit hooks
|
|
3
|
+
# MORPH-SPEC Master Pre-Commit Hook
|
|
4
|
+
# Runs universal framework hooks + stack-specific hooks in sequence
|
|
5
|
+
|
|
6
|
+
echo "ââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
7
|
+
echo "â MORPH-SPEC PRE-COMMIT VALIDATION â"
|
|
8
|
+
echo "ââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
FRAMEWORK_HOOKS_DIR="$(dirname "$0")"
|
|
12
|
+
STACK_HOOKS_DIR=".morph/hooks/pre-commit"
|
|
13
|
+
|
|
14
|
+
HAS_FAILURES=false
|
|
15
|
+
|
|
16
|
+
# Run framework hooks (universal - all stacks)
|
|
17
|
+
echo "đĻ Running framework hooks (universal)..."
|
|
18
|
+
for hook in "$FRAMEWORK_HOOKS_DIR"/*.sh; do
|
|
19
|
+
# Skip orchestrator itself
|
|
20
|
+
if [[ "$hook" == */orchestrator.sh ]]; then
|
|
21
|
+
continue
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ -f "$hook" ]]; then
|
|
25
|
+
hook_name=$(basename "$hook")
|
|
26
|
+
echo "âââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
27
|
+
if ! bash "$hook"; then
|
|
28
|
+
HAS_FAILURES=true
|
|
29
|
+
echo "â Framework hook failed: $hook_name"
|
|
30
|
+
fi
|
|
31
|
+
echo ""
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
# Run stack-specific hooks (if exist)
|
|
36
|
+
if [[ -d "$STACK_HOOKS_DIR" ]]; then
|
|
37
|
+
echo "đ¯ Running stack-specific hooks..."
|
|
38
|
+
for hook in "$STACK_HOOKS_DIR"/*.sh; do
|
|
39
|
+
if [[ -f "$hook" ]]; then
|
|
40
|
+
hook_name=$(basename "$hook")
|
|
41
|
+
echo "âââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
42
|
+
if ! bash "$hook"; then
|
|
43
|
+
HAS_FAILURES=true
|
|
44
|
+
echo "â Stack hook failed: $hook_name"
|
|
45
|
+
fi
|
|
46
|
+
echo ""
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
else
|
|
50
|
+
echo "âšī¸ No stack-specific hooks found (optional)"
|
|
51
|
+
echo ""
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo "âââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
55
|
+
|
|
56
|
+
if [[ "$HAS_FAILURES" == "true" ]]; then
|
|
57
|
+
echo "â PRE-COMMIT VALIDATION FAILED"
|
|
58
|
+
echo " Fix errors above before committing"
|
|
59
|
+
echo " Or use: git commit --no-verify to skip"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo "â
ALL PRE-COMMIT VALIDATIONS PASSED"
|
|
64
|
+
exit 0
|