@polymorphism-tech/morph-spec 9.8.0 → 10.1.1
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 +12 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +28 -36
- package/docs/QUICKSTART.md +31 -191
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-preflight.md +3 -3
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +84 -95
- package/framework/evals/review-checklist.md +45 -53
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/mcp/registry.json +4 -4
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +10 -2
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +11 -18
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +39 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +49 -109
- package/framework/skills/core/morph-plan/references/critique.md +43 -0
- package/framework/skills/core/morph-plan/references/interview.md +37 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +51 -28
- package/framework/standards/STANDARDS.meta.json +16 -19
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +70 -0
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +15 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +3 -14
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +2 -2
- package/src/commands/state/create.js +1 -6
- package/src/commands/state/gate-check.js +38 -31
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +36 -178
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +3 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/installers/memory-installer.js +3 -3
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +61 -109
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +28 -4
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +0 -7
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- package/src/lib/worktree/agent-reaper.js +0 -247
|
@@ -4,15 +4,8 @@ import { shouldInstall } from '../lib/stack-filter.js';
|
|
|
4
4
|
import { realEffects } from '../lib/installers/effects.js';
|
|
5
5
|
import { REVIEW_ROLES, VERDICT_TOKEN, REVIEWED_SHA_TOKEN, VERDICTS } from '../lib/review-reports.js';
|
|
6
6
|
|
|
7
|
-
export const DEFAULT_AGENT_TOOLS = 'Read,
|
|
7
|
+
export const DEFAULT_AGENT_TOOLS = 'Read, Glob, Grep, Bash';
|
|
8
8
|
|
|
9
|
-
/** Derived from the effective tool scope (Write or Edit), so a persona made read-only stops being one. */
|
|
10
|
-
export function isImplementer(agent) {
|
|
11
|
-
const tools = String(agent?.tools ?? DEFAULT_AGENT_TOOLS).split(',').map((t) => t.trim());
|
|
12
|
-
return tools.includes('Write') || tools.includes('Edit');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// The value hardcoded before the field existed: an agents.json without `maxTurns` renders byte-for-byte as before.
|
|
16
9
|
export const DEFAULT_MAX_TURNS = 20;
|
|
17
10
|
|
|
18
11
|
const CHECKPOINT_PERCENT = 70;
|
|
@@ -43,84 +36,12 @@ export function resolveMaxTurns(agent) {
|
|
|
43
36
|
return agent.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
export const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
47
|
-
|
|
48
|
-
// Absent: no `effort:` line, so the sub-agent inherits the session effort.
|
|
49
|
-
export function effortProblem(value) {
|
|
50
|
-
if (value === undefined) return null;
|
|
51
|
-
return VALID_EFFORTS.includes(value) ? null : `esperado um de ${VALID_EFFORTS.join('|')}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function effortError(id, value) {
|
|
55
|
-
const problem = effortProblem(value);
|
|
56
|
-
if (!problem) return null;
|
|
57
|
-
return `agents.json: persona "${id}" tem effort inválido (${JSON.stringify(value)}) — ${problem}. ` +
|
|
58
|
-
'Remova o campo para herdar o effort da sessão.';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
62
|
-
|
|
63
|
-
const SURFACE_KEYS = new Set(['when', 'standards']);
|
|
64
|
-
|
|
65
|
-
// A standard both universal and in a surface is refused: the persona would keep
|
|
66
|
-
// loading it on every surface without anyone noticing.
|
|
67
|
-
export function surfacesProblem(surfaces, standards = []) {
|
|
68
|
-
if (surfaces === undefined) return null;
|
|
69
|
-
if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
|
|
70
|
-
return 'esperado objeto { "<superfície>": { when, standards } }';
|
|
71
|
-
}
|
|
72
|
-
const entries = Object.entries(surfaces);
|
|
73
|
-
if (entries.length === 0) return 'esperado ao menos uma superfície';
|
|
74
|
-
|
|
75
|
-
const universal = new Set(Array.isArray(standards) ? standards : []);
|
|
76
|
-
for (const [name, def] of entries) {
|
|
77
|
-
if (!SURFACE_ID_RE.test(name)) return `superfície "${name}" — o id é kebab-case (ex.: "admin-app")`;
|
|
78
|
-
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
79
|
-
return `superfície "${name}" — esperado { when, standards }`;
|
|
80
|
-
}
|
|
81
|
-
const unknown = Object.keys(def).filter((k) => !SURFACE_KEYS.has(k));
|
|
82
|
-
if (unknown.length > 0) return `superfície "${name}" — campo desconhecido: ${unknown.join(', ')}`;
|
|
83
|
-
if (typeof def.when !== 'string' || def.when.trim() === '') {
|
|
84
|
-
return `superfície "${name}" — "when" precisa dizer que telas são dessa superfície`;
|
|
85
|
-
}
|
|
86
|
-
const ids = def.standards;
|
|
87
|
-
if (!Array.isArray(ids) || ids.length === 0 || ids.some((s) => typeof s !== 'string' || s.trim() === '')) {
|
|
88
|
-
return `superfície "${name}" — "standards" precisa ser uma lista não vazia de ids`;
|
|
89
|
-
}
|
|
90
|
-
const repeated = ids.filter((s, i) => ids.indexOf(s) !== i);
|
|
91
|
-
if (repeated.length > 0) return `superfície "${name}" repete ${[...new Set(repeated)].join(', ')}`;
|
|
92
|
-
const overlap = ids.filter((s) => universal.has(s));
|
|
93
|
-
if (overlap.length > 0) {
|
|
94
|
-
return `superfície "${name}" repete standard da lista universal (${overlap.join(', ')}) — ` +
|
|
95
|
-
'um standard é universal OU de uma superfície';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function surfacesError(id, surfaces, standards) {
|
|
102
|
-
const problem = surfacesProblem(surfaces, standards);
|
|
103
|
-
if (!problem) return null;
|
|
104
|
-
return `agents.json: persona "${id}" tem surfaces inválido — ${problem}. ` +
|
|
105
|
-
'Remova o campo para a persona ler só `standards`.';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** @returns {string[]} unique ids of `standards` plus every surface's, universal first */
|
|
109
39
|
export function personaStandardIds(persona) {
|
|
110
|
-
|
|
111
|
-
const surfaces = persona?.surfaces;
|
|
112
|
-
if (surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces)) {
|
|
113
|
-
for (const def of Object.values(surfaces)) {
|
|
114
|
-
if (Array.isArray(def?.standards)) ids.push(...def.standards);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return [...new Set(ids)];
|
|
40
|
+
return [...new Set(Array.isArray(persona?.standards) ? persona.standards : [])];
|
|
118
41
|
}
|
|
119
42
|
|
|
120
43
|
function personaError(agent) {
|
|
121
|
-
return maxTurnsError(agent.id, agent.maxTurns)
|
|
122
|
-
?? surfacesError(agent.id, agent.surfaces, agent.standards)
|
|
123
|
-
?? effortError(agent.id, agent.effort);
|
|
44
|
+
return maxTurnsError(agent.id, agent.maxTurns);
|
|
124
45
|
}
|
|
125
46
|
|
|
126
47
|
// `tools:` is a strict allowlist: without Skill no skill runs, without ToolSearch no deferred MCP tool loads.
|
|
@@ -165,6 +86,8 @@ const MANAGED_HEADER =
|
|
|
165
86
|
' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
|
|
166
87
|
' To change this persona, edit agents.json and regenerate. Drift is flagged by `morph-spec doctor`. -->';
|
|
167
88
|
|
|
89
|
+
const MANAGED_HEADER_MARKER = MANAGED_HEADER.split('\n')[0];
|
|
90
|
+
|
|
168
91
|
export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
|
|
169
92
|
const { projectTags = [], fx = realEffects() } = options;
|
|
170
93
|
|
|
@@ -213,24 +136,30 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
213
136
|
// Silent in dryRun: the real update's cleanFrameworkDirs() already emptied the dir.
|
|
214
137
|
if (!fx.dryRun) {
|
|
215
138
|
for (const file of readdirSync(targetDir)) {
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
139
|
+
if (installedFilenames.has(file)) continue;
|
|
140
|
+
const path = join(targetDir, file);
|
|
141
|
+
if (allPossible.has(file) || isManagedOrphan(file, path)) fx.remove(path);
|
|
219
142
|
}
|
|
220
143
|
}
|
|
221
144
|
|
|
222
145
|
return { installed, agents: installedAgents };
|
|
223
146
|
}
|
|
224
147
|
|
|
148
|
+
// Only the managed header proves the file is ours: a hand-written morph-*.md is never deleted.
|
|
149
|
+
function isManagedOrphan(file, path) {
|
|
150
|
+
if (!file.startsWith('morph-') || !file.endsWith('.md')) return false;
|
|
151
|
+
try {
|
|
152
|
+
return readFileSync(path, 'utf-8').includes(MANAGED_HEADER_MARKER);
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
225
158
|
// Deterministic: the doctor drift check compares files byte-for-byte against this output.
|
|
226
159
|
export function renderAgentMd(agent, knownMcpIds = null) {
|
|
227
160
|
const description = buildDescription(agent);
|
|
228
161
|
// One value for the frontmatter and the body: the sub-agent never sees its own frontmatter.
|
|
229
162
|
const maxTurns = resolveMaxTurns(agent);
|
|
230
|
-
const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
|
|
231
|
-
if (surfaces) throw new Error(surfaces);
|
|
232
|
-
const effort = effortError(agent.id, agent.effort);
|
|
233
|
-
if (effort) throw new Error(effort);
|
|
234
163
|
const frontmatter = buildFrontmatter(agent, description, knownMcpIds, maxTurns);
|
|
235
164
|
const body = buildBody(agent, maxTurns);
|
|
236
165
|
return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
|
|
@@ -251,10 +180,8 @@ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
|
|
|
251
180
|
`description: ${description}`,
|
|
252
181
|
`model: ${agent.model ?? 'inherit'}`,
|
|
253
182
|
`tools: ${buildToolsLine(agent, knownMcpIds)}`,
|
|
254
|
-
// Position fixed so an agents.json without the field renders byte-for-byte as before.
|
|
255
183
|
`maxTurns: ${maxTurns}`,
|
|
256
184
|
];
|
|
257
|
-
if (agent.effort) lines.push(`effort: ${agent.effort}`);
|
|
258
185
|
if (agent.color) lines.push(`color: ${agent.color}`);
|
|
259
186
|
lines.push(`memory: project`, '');
|
|
260
187
|
return lines.join('\n');
|
|
@@ -267,58 +194,9 @@ function buildDescription(agent) {
|
|
|
267
194
|
return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
|
|
268
195
|
}
|
|
269
196
|
|
|
270
|
-
const NEON_CAUTION = 'NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável ' +
|
|
271
|
-
'(prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — ' +
|
|
272
|
-
'nunca chame tool destrutiva sem confirmação humana explícita.';
|
|
273
|
-
|
|
274
197
|
const STANDARDS_HINT = 'Digest de cada um: `morph-spec standards --find <id>`; só a seção que importa: ' +
|
|
275
198
|
'`morph-spec standards --show <id>#<âncora>` (`--show <id>` lista as âncoras).';
|
|
276
199
|
|
|
277
|
-
export const DISPATCH_CONTRACT = [
|
|
278
|
-
'## Contrato de dispatch',
|
|
279
|
-
'',
|
|
280
|
-
'O dispatch traz só o que muda por fatia: id e título, `cobre` com o texto de cada cenário, `arquivos`, `standards`, ' +
|
|
281
|
-
'o caminho do relatório e, se houver, o worktree. O resto é este contrato.',
|
|
282
|
-
'',
|
|
283
|
-
'1. RELATÓRIO: GRAVE o arquivo de relatório que o dispatch nomeia (`.morph/features/{feature}/3-implement/reports/F{n}.md`) ' +
|
|
284
|
-
'ANTES de encerrar — é o retorno autoritativo, não a sua mensagem final.',
|
|
285
|
-
'2. ESTADO: NÃO toque em `plan.md`, `checks.json`, `feature.json` nem `state.json`: o estado é do orquestrador.',
|
|
286
|
-
'3. TESTE: a prova é do orquestrador (`morph-spec verify {feature}`, depois do seu retorno), não sua. Escreva os testes pelo orçamento: ' +
|
|
287
|
-
'um por Cn `prova: teste`, na camada mais baixa que o prova (a de cima só para o que só existe lá), variantes em tabela (`[Theory]`/`it.each`); `leitura` sem teste. NÃO rode teste a cada marco nem faça ciclo red→green: ' +
|
|
288
|
-
'escreva código e testes, compile, e rode SÓ o filtro dos seus testes UMA vez, em primeiro plano, antes do relatório ' +
|
|
289
|
-
'(`vitest related <arquivos da fatia> --run`; `dotnet test --filter "FullyQualifiedName~<Classe>" --no-build` após o build); ' +
|
|
290
|
-
'se falhar, corrija e rode só os que falharam. Teto: 3 rodadas (`verify --filter` conta, exit 3 não); ' +
|
|
291
|
-
'o hook nega a 4ª e o `verify` sem `--filter`: grave `PARCIAL` com o que ainda falha e devolva. ' +
|
|
292
|
-
'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`, sem nada depois) e leia o arquivo — um pipe troca o exit do teste pelo do último comando. ' +
|
|
293
|
-
'Em batch paralelo SEM isolamento, só compile. Com `isolation: "worktree"`, o mesmo vale no seu worktree, e o último ato é o commit na branch dele.',
|
|
294
|
-
'4. SOBREVIVÊNCIA: o limite de turnos corta sem aviso: commite `chore(F{n}): wip …` a cada marco (um arquivo ou uma fatia escrita), não só ao fim — ' +
|
|
295
|
-
'e só os seus arquivos (`git add <paths>`, nunca diretório). Sem commit, a retomada recomeça do zero.',
|
|
296
|
-
'5. PROCESSOS: encerre dev server pelo PID que você mesmo subiu. Build e suíte pesados rodam por `morph-spec heavy run -- <comando>`, em primeiro plano; ' +
|
|
297
|
-
'exit 3 é fila cheia: não repita nem espere em laço, diga no relatório o que não rodou.',
|
|
298
|
-
'6. REUSO: antes de criar tipo, componente, hook ou serviço NOVO, verifique se já existe: `morph-spec graph explain <nome>` e ' +
|
|
299
|
-
'`graph affected <conceito> --uses-only`; sem grafo (`no-graph`), caia para `grep -rn "<nome>"` e siga — a ausência de grafo nunca bloqueia. ' +
|
|
300
|
-
'Se criar mesmo assim, diga por que em uma linha no relatório.',
|
|
301
|
-
'7. MUTAÇÃO: nunca altere, reverta, mova ou apague código de produção para ver um teste falhar; a prova de que o teste morde é do verificador.',
|
|
302
|
-
'8. ESCRITA: código e teste do projeto só por Write/Edit, nunca pelo Bash (heredoc, `sed -i`, script que grava): os guards só enxergam Write/Edit.',
|
|
303
|
-
'9. BLOQUEIO: decisão que não é sua (requisito contra o plano ou um ADR, escolha deixada ao humano) → `BLOQUEADO` na 1ª linha, ' +
|
|
304
|
-
'com a pergunta e as opções. Dificuldade técnica não é bloqueio.',
|
|
305
|
-
'',
|
|
306
|
-
'Formato do relatório (o `report-check` lê estas marcas):',
|
|
307
|
-
'',
|
|
308
|
-
'```',
|
|
309
|
-
'# F1 — <título>',
|
|
310
|
-
'O que foi feito, em 2-3 frases: o comportamento entregue e onde ele mora, sem repetir a lista de arquivos.',
|
|
311
|
-
'**Arquivos:** `src/a.cs`, `src/a.test.cs`',
|
|
312
|
-
'**Checks:**',
|
|
313
|
-
'- C1 — provado: `a.test.cs` › nome do teste (ou `arquivo:linha`)',
|
|
314
|
-
'- C2 — não provado: por quê',
|
|
315
|
-
'```',
|
|
316
|
-
'',
|
|
317
|
-
'`**Arquivos:**` lista TODO arquivo criado ou modificado, inclusive artefato auxiliar fora do plano (factory, seed, config gerada): ' +
|
|
318
|
-
'arquivo em produção sem dono é falha de protocolo que o Gate 3 aponta. Todo cenário que a fatia `cobre` aparece em `**Checks:**`. ' +
|
|
319
|
-
'Problemas e como foram resolvidos vão em prosa depois.',
|
|
320
|
-
];
|
|
321
|
-
|
|
322
200
|
export const REVIEW_REPORT_CONTRACT = [
|
|
323
201
|
'## Relatório do Gate 3',
|
|
324
202
|
'',
|
|
@@ -346,57 +224,20 @@ function buildBody(agent, maxTurns) {
|
|
|
346
224
|
'',
|
|
347
225
|
];
|
|
348
226
|
|
|
349
|
-
// The shape without effort or surfaces renders byte-for-byte as the old fixture.
|
|
350
|
-
const legacyShape = !agent.effort && !agent.surfaces && isImplementer(agent);
|
|
351
|
-
|
|
352
|
-
if (legacyShape && agent.domains?.length) {
|
|
353
|
-
lines.push('## Domains', ...agent.domains.map(d => `- ${d}`), '');
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (!legacyShape && agent.mcp?.includes('neon')) {
|
|
357
|
-
lines.push(NEON_CAUTION, '');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
227
|
if (agent.standards?.length) {
|
|
361
|
-
lines.push('## Standards to consult on demand');
|
|
362
|
-
if (!legacyShape) lines.push(STANDARDS_HINT);
|
|
228
|
+
lines.push('## Standards to consult on demand', STANDARDS_HINT);
|
|
363
229
|
for (const s of agent.standards) lines.push(`- ${s}`);
|
|
364
230
|
lines.push('');
|
|
365
231
|
}
|
|
366
232
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
'',
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
);
|
|
376
|
-
for (const [name, def] of Object.entries(agent.surfaces)) {
|
|
377
|
-
lines.push(`### ${name}`, def.when, ...def.standards.map((s) => `- ${s}`), '');
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (agent.effort && isImplementer(agent)) {
|
|
382
|
-
lines.push(...DISPATCH_CONTRACT, '');
|
|
383
|
-
} else if (isImplementer(agent)) {
|
|
384
|
-
lines.push(
|
|
385
|
-
'## How you operate',
|
|
386
|
-
'',
|
|
387
|
-
'You are a domain persona dispatched per task. Read the spec/tasks/standards passed to you, do the work, and report each doneCriteria with its evidence. Refer to MORPH.md section 6 for the dispatch contract.',
|
|
388
|
-
'',
|
|
389
|
-
);
|
|
390
|
-
} else {
|
|
391
|
-
lines.push(
|
|
392
|
-
'## Como você opera',
|
|
393
|
-
'',
|
|
394
|
-
'Você é read-only: nunca escreva, edite nem reverta código, testes, o plano ou `feature.json` (nada de `git checkout --`). ' +
|
|
395
|
-
'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
|
|
396
|
-
'',
|
|
397
|
-
);
|
|
398
|
-
if (Object.hasOwn(REVIEW_ROLES, agent.id)) lines.push(...REVIEW_REPORT_CONTRACT, '');
|
|
399
|
-
}
|
|
233
|
+
lines.push(
|
|
234
|
+
'## Como você opera',
|
|
235
|
+
'',
|
|
236
|
+
'Você é read-only: nunca escreva, edite nem reverta código, testes, o plano ou `feature.json` (nada de `git checkout --`). ' +
|
|
237
|
+
'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final.',
|
|
238
|
+
'',
|
|
239
|
+
);
|
|
240
|
+
if (Object.hasOwn(REVIEW_ROLES, agent.id)) lines.push(...REVIEW_REPORT_CONTRACT, '');
|
|
400
241
|
|
|
401
242
|
lines.push(
|
|
402
243
|
// `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps a partial
|
|
@@ -409,17 +250,6 @@ function buildBody(agent, maxTurns) {
|
|
|
409
250
|
'`PARCIAL` on line 1, done, remaining, files, no verdict (final report only) — then continue or stop, your call; the final report replaces it.',
|
|
410
251
|
);
|
|
411
252
|
|
|
412
|
-
// The only text every implementer dispatch is guaranteed to carry.
|
|
413
|
-
if (agent.effort && isImplementer(agent)) {
|
|
414
|
-
lines.push(
|
|
415
|
-
'',
|
|
416
|
-
'## Working fast',
|
|
417
|
-
'',
|
|
418
|
-
'Independent tool calls go in the SAME response: read every listed file at once, fire the searches together. ' +
|
|
419
|
-
'Check a library API in the `context7` MCP before guessing (load a deferred `mcp__` tool with ToolSearch).',
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
253
|
return lines.join('\n');
|
|
424
254
|
}
|
|
425
255
|
|
|
@@ -428,10 +258,6 @@ function checkpointTurn(maxTurns) {
|
|
|
428
258
|
return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
|
|
429
259
|
}
|
|
430
260
|
|
|
431
|
-
export async function installDomainAgents(projectDir, frameworkDir, options) {
|
|
432
|
-
return { specialists: 0 };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
261
|
export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
436
262
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
437
263
|
const installedFiles = new Set();
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { join, dirname } from 'path';
|
|
2
2
|
import { readFile } from 'fs/promises';
|
|
3
|
-
import { existsSync,
|
|
3
|
+
import { existsSync, readdirSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
5
|
import { realEffects } from '../lib/installers/effects.js';
|
|
7
6
|
|
|
8
|
-
const HOOKS_VERSION = '3.
|
|
7
|
+
const HOOKS_VERSION = '3.17.0';
|
|
9
8
|
|
|
10
9
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
11
10
|
|
|
@@ -25,9 +24,6 @@ const MORPH_ALLOW = [
|
|
|
25
24
|
'Edit(.morph/context/**)',
|
|
26
25
|
'Bash(mkdir:.morph/**)',
|
|
27
26
|
'Bash(ls:.morph/**)',
|
|
28
|
-
// An auto-mode deny here pauses the review mutation round.
|
|
29
|
-
'Bash(morph-spec mutate:*)',
|
|
30
|
-
'Bash(npx morph-spec mutate:*)',
|
|
31
27
|
];
|
|
32
28
|
|
|
33
29
|
const MORPH_PERMISSIONS = MORPH_DENY;
|
|
@@ -64,7 +60,7 @@ const MORPH_HOOKS = [
|
|
|
64
60
|
hooks: [{
|
|
65
61
|
type: 'command',
|
|
66
62
|
command: 'node framework/hooks/claude-code/core/chain.js',
|
|
67
|
-
args: ['pre-bash-denylist', 'test-budget', 'conventional-commit', 'block-destructive-worktree-removal', 'block-raw-worktree-add', '
|
|
63
|
+
args: ['pre-bash-denylist', 'test-budget', 'conventional-commit', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'heavy-gate', 'worktree-env-inject']
|
|
68
64
|
}]
|
|
69
65
|
},
|
|
70
66
|
|
|
@@ -117,7 +113,7 @@ const MORPH_HOOKS = [
|
|
|
117
113
|
hooks: [{
|
|
118
114
|
type: 'command',
|
|
119
115
|
command: 'node framework/hooks/claude-code/core/chain.js',
|
|
120
|
-
args: ['telemetry-log', 'loop-detect', 'graph-refresh-kick', 'heavy-release', '
|
|
116
|
+
args: ['telemetry-log', 'loop-detect', 'graph-refresh-kick', 'heavy-release', 'masked-exit'],
|
|
121
117
|
timeout: 10
|
|
122
118
|
}]
|
|
123
119
|
},
|
|
@@ -129,7 +125,7 @@ const MORPH_HOOKS = [
|
|
|
129
125
|
hooks: [{
|
|
130
126
|
type: 'command',
|
|
131
127
|
command: 'node framework/hooks/claude-code/core/chain.js',
|
|
132
|
-
args: ['telemetry-log', 'heavy-release'
|
|
128
|
+
args: ['telemetry-log', 'heavy-release'],
|
|
133
129
|
timeout: 5,
|
|
134
130
|
async: true
|
|
135
131
|
}]
|
|
@@ -254,6 +250,8 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
|
|
|
254
250
|
const STALE_ALLOW = [
|
|
255
251
|
'Write(.morph/features/**)',
|
|
256
252
|
'Write(.morph/context/**)',
|
|
253
|
+
'Bash(morph-spec mutate:*)',
|
|
254
|
+
'Bash(npx morph-spec mutate:*)',
|
|
257
255
|
];
|
|
258
256
|
settings.permissions.allow = settings.permissions.allow.filter(
|
|
259
257
|
p => !MORPH_ALLOW.includes(p) && !STALE_ALLOW.includes(p)
|
|
@@ -292,78 +290,6 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
|
|
|
292
290
|
return { installed, updated: !alreadyCurrent };
|
|
293
291
|
}
|
|
294
292
|
|
|
295
|
-
function findPython() {
|
|
296
|
-
if (process.platform !== 'win32') {
|
|
297
|
-
for (const cmd of ['python3', 'python']) {
|
|
298
|
-
try {
|
|
299
|
-
const ver = execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
300
|
-
if ((ver + '').includes('Python 3')) return cmd;
|
|
301
|
-
} catch { /* not on PATH */ }
|
|
302
|
-
}
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
for (const cmd of ['python3', 'py', 'python']) {
|
|
306
|
-
try {
|
|
307
|
-
const ver = execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
308
|
-
if ((ver + '').includes('Python 3')) return cmd;
|
|
309
|
-
} catch { /* not on PATH */ }
|
|
310
|
-
}
|
|
311
|
-
const base = join(homedir(), 'AppData', 'Local', 'Programs', 'Python');
|
|
312
|
-
if (existsSync(base)) {
|
|
313
|
-
let dirs;
|
|
314
|
-
try { dirs = readdirSync(base); } catch { dirs = []; }
|
|
315
|
-
const py3 = dirs
|
|
316
|
-
.filter(d => /^Python3\d*/i.test(d))
|
|
317
|
-
.sort()
|
|
318
|
-
.reverse();
|
|
319
|
-
for (const dir of py3) {
|
|
320
|
-
const exe = join(base, dir, 'python.exe');
|
|
321
|
-
if (existsSync(exe)) return exe;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function tryInstallPython() {
|
|
328
|
-
const log = (msg) => process.stdout.write(` ${msg}\n`);
|
|
329
|
-
log('⚠ Python 3 not found — attempting automatic installation...');
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
if (process.platform === 'win32') {
|
|
333
|
-
execSync('winget install -e --id Python.Python.3.12 --accept-source-agreements --accept-package-agreements', {
|
|
334
|
-
stdio: 'inherit',
|
|
335
|
-
timeout: 120_000,
|
|
336
|
-
});
|
|
337
|
-
} else if (process.platform === 'darwin') {
|
|
338
|
-
execSync('brew install python3', { stdio: 'inherit', timeout: 120_000 });
|
|
339
|
-
} else {
|
|
340
|
-
try {
|
|
341
|
-
execSync('sudo apt-get update -qq && sudo apt-get install -y python3', { stdio: 'inherit', timeout: 120_000 });
|
|
342
|
-
} catch {
|
|
343
|
-
execSync('sudo dnf install -y python3', { stdio: 'inherit', timeout: 120_000 });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
log('✓ Python 3 installed successfully.');
|
|
347
|
-
return true;
|
|
348
|
-
} catch (err) {
|
|
349
|
-
log(`✗ Automatic Python installation failed: ${err.message}`);
|
|
350
|
-
log(' Please install Python 3 manually: https://www.python.org/downloads/');
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function resolvePythonForStatusline() {
|
|
356
|
-
let python = findPython();
|
|
357
|
-
if (python) return python;
|
|
358
|
-
|
|
359
|
-
if (tryInstallPython()) {
|
|
360
|
-
python = findPython();
|
|
361
|
-
if (python) return python;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return 'python3';
|
|
365
|
-
}
|
|
366
|
-
|
|
367
293
|
/**
|
|
368
294
|
* Writes OUTSIDE the project (`~/.claude/`), so `--dry-run` must name these effects separately.
|
|
369
295
|
* @returns {Promise<{ installed: boolean, globalDir: string }>}
|
|
@@ -372,23 +298,13 @@ export async function installGlobalStatusline(hooksSourceDir, globalClaudeDirOve
|
|
|
372
298
|
const globalDir = globalClaudeDirOverride || join(homedir(), '.claude');
|
|
373
299
|
fx.ensureDir(globalDir);
|
|
374
300
|
|
|
375
|
-
|
|
376
|
-
for (const
|
|
377
|
-
|
|
378
|
-
if (existsSync(src)) {
|
|
379
|
-
fx.copyFile(src, join(globalDir, fname));
|
|
380
|
-
if (process.platform !== 'win32' && !fx.dryRun) {
|
|
381
|
-
try { chmodSync(join(globalDir, fname), 0o755); } catch { /* non-critical */ }
|
|
382
|
-
}
|
|
383
|
-
}
|
|
301
|
+
fx.copyFile(join(hooksSourceDir, '..', 'statusline.js'), join(globalDir, 'statusline.js'));
|
|
302
|
+
for (const legacy of ['statusline.py', 'statusline.sh']) {
|
|
303
|
+
if (existsSync(join(globalDir, legacy))) fx.remove(join(globalDir, legacy));
|
|
384
304
|
}
|
|
385
305
|
|
|
386
306
|
const settingsPath = join(globalDir, 'settings.json');
|
|
387
|
-
|
|
388
|
-
// Claude Code on Windows cannot execute .sh directly.
|
|
389
|
-
const statuslineCommand = process.platform === 'win32'
|
|
390
|
-
? `"${resolvePythonForStatusline()}" "${join(globalDir, 'statusline.py').replace(/\\/g, '/')}"`
|
|
391
|
-
: join(globalDir, 'statusline.sh').replace(/\\/g, '/');
|
|
307
|
+
const statuslineCommand = `node "${join(globalDir, 'statusline.js').replace(/\\/g, '/')}"`;
|
|
392
308
|
|
|
393
309
|
// Without refreshInterval the status line only re-renders on events, freezing
|
|
394
310
|
// the rate-limit countdown through a long turn. Shallow merge: only `statusLine` is ours.
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Warn only: a source file written by the shell skips every Write/Edit guard. The command still runs.
|
|
3
|
-
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
4
|
-
import { stateExists } from '../../shared/state-reader.js';
|
|
5
|
-
import { writeTargets } from '../../shared/command-tokens.js';
|
|
6
|
-
import { isProjectSource } from '../../shared/source-files.js';
|
|
7
|
-
|
|
8
|
-
export async function run(payload) {
|
|
9
|
-
try {
|
|
10
|
-
if (!stateExists()) return null;
|
|
11
|
-
const sources = writeTargets(payload?.tool_input?.command || '').filter(isProjectSource);
|
|
12
|
-
if (!sources.length) return null;
|
|
13
|
-
return contextResult(
|
|
14
|
-
`⚠️ Código do projeto escrito pelo Bash (${sources.join(', ')}): os guards de escrita (fase, arquivo protegido, comment-guard) ` +
|
|
15
|
-
'só enxergam Write/Edit. Reescreva esse arquivo por Write/Edit.',
|
|
16
|
-
'PreToolUse',
|
|
17
|
-
);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
await runStandalone(run);
|