@polymorphism-tech/morph-spec 9.7.0 → 10.0.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 +11 -11
- package/README.md +4 -4
- package/bin/morph-spec.js +41 -44
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- 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-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +100 -102
- package/framework/evals/review-checklist.md +46 -54
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- 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/heavy-slots.js +41 -4
- 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/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +9 -1
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- 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 +12 -19
- 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 +38 -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 +42 -108
- package/framework/skills/core/morph-plan/references/critique.md +40 -0
- package/framework/skills/core/morph-plan/references/interview.md +25 -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 +32 -5
- package/framework/standards/STANDARDS.meta.json +5 -1
- 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 +6 -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 +47 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
- 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/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- 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/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +12 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- 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/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/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- 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 +27 -16
- 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 +1 -1
- package/src/commands/state/create.js +0 -5
- package/src/commands/state/finish.js +43 -1
- package/src/commands/state/gate-check.js +37 -30
- 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 +39 -146
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +6 -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/main.jsx +13 -13
- 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/dashboard/vite.config.js +24 -24
- 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/heavy/gate.js +22 -1
- package/src/lib/installers/memory-installer.js +2 -2
- 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 +55 -110
- 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 +33 -7
- 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/tasks/test-nodes.js +24 -6
- package/src/lib/tasks/test-runner.js +40 -1
- 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 +50 -19
- 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/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
|
@@ -7,7 +7,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
|
7
7
|
import { join, basename, isAbsolute } from 'path';
|
|
8
8
|
import { derivePhaseFrom, diskArtifactProbe, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
|
|
9
9
|
import { GATE_PREREQ } from '../../core/state/gate-order.js';
|
|
10
|
-
import {
|
|
10
|
+
import { implementProgress, checksPath } from '../../../framework/hooks/shared/report-progress.js';
|
|
11
11
|
import {
|
|
12
12
|
reviewReportsFromDir, requiredReviewRoles, combinedVerdict, allBlockers, checksCoverage, reviewFileName, REVIEW_ROLES,
|
|
13
13
|
} from '../../lib/review-reports.js';
|
|
@@ -110,26 +110,16 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
export function
|
|
114
|
-
const progress =
|
|
113
|
+
export function readProgress(dir) {
|
|
114
|
+
const progress = implementProgress(dir);
|
|
115
115
|
if (!progress) return null;
|
|
116
|
-
const extracted = new Map((readJson(checksPath(dir))?.slices || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
117
|
-
const list = progress.slices.map((sl) => {
|
|
118
|
-
const x = extracted.get(sl.id) || {};
|
|
119
|
-
return {
|
|
120
|
-
...sl,
|
|
121
|
-
persona: x.persona ?? null,
|
|
122
|
-
files: Array.isArray(x.files) ? x.files : [],
|
|
123
|
-
covers: Array.isArray(x.covers) ? x.covers : [],
|
|
124
|
-
};
|
|
125
|
-
});
|
|
126
116
|
return {
|
|
127
|
-
counts: { total: progress.total, done: progress.done,
|
|
128
|
-
|
|
117
|
+
counts: { total: progress.total, done: progress.done, blocked: progress.blocked, pending: progress.pending },
|
|
118
|
+
byId: new Map(progress.checks.map((c) => [c.id, c])),
|
|
129
119
|
};
|
|
130
120
|
}
|
|
131
121
|
|
|
132
|
-
const
|
|
122
|
+
const NO_SCENARIOS = { total: 0, done: 0, blocked: 0, pending: 0 };
|
|
133
123
|
|
|
134
124
|
function gatesOf(feature) {
|
|
135
125
|
const skipped = new Set(feature.skippedPhases || []);
|
|
@@ -142,9 +132,11 @@ function gatesOf(feature) {
|
|
|
142
132
|
approved,
|
|
143
133
|
approvedBy: g.approvedBy ?? (g.auto ? 'auto' : null),
|
|
144
134
|
at: g.approvedAt || g.timestamp || null,
|
|
145
|
-
paused
|
|
135
|
+
// `paused` is "waiting for the human"; a session-owned pause is work in progress.
|
|
136
|
+
paused: !approved && decision?.pause === true && decision.resolver !== 'session',
|
|
146
137
|
skipped: skipped.has(gate),
|
|
147
138
|
reason: decision?.reason ?? null,
|
|
139
|
+
since: decision?.decidedAt ?? null,
|
|
148
140
|
};
|
|
149
141
|
});
|
|
150
142
|
}
|
|
@@ -174,8 +166,39 @@ export function reviewRows(reports, workType) {
|
|
|
174
166
|
});
|
|
175
167
|
}
|
|
176
168
|
|
|
169
|
+
// The contract opens the evidence with "Cn:"; a later mention is context, not the target.
|
|
170
|
+
const LEADING_CHECKS_RE = /^\s*(C\d+(?:\s*(?:,|e|\/)\s*C\d+)*)\s*:/;
|
|
171
|
+
function citedChecks(b) {
|
|
172
|
+
const lead = String(b.evidence).match(LEADING_CHECKS_RE)?.[1];
|
|
173
|
+
return [...new Set((lead ?? `${b.where} ${b.evidence}`).match(/\bC\d+\b/g) ?? [])];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A role re-run fewer times keeps its last round; verifier rows accumulate across rounds.
|
|
177
|
+
export function scoreboard(reports) {
|
|
178
|
+
const roles = Object.keys(REVIEW_ROLES).filter((role) => reports[role].status !== 'missing' && reports[role].rounds?.length);
|
|
179
|
+
const total = Math.max(0, ...roles.map((role) => reports[role].rounds.length));
|
|
180
|
+
const rows = new Map();
|
|
181
|
+
return Array.from({ length: total }, (_, i) => {
|
|
182
|
+
const byRole = {};
|
|
183
|
+
for (const role of roles) {
|
|
184
|
+
const all = reports[role].rounds;
|
|
185
|
+
const r = all[Math.min(i, all.length - 1)];
|
|
186
|
+
byRole[role] = {
|
|
187
|
+
verdict: r.verdict,
|
|
188
|
+
reviewedSha: r.reviewedSha,
|
|
189
|
+
carried: i >= all.length,
|
|
190
|
+
blockers: r.blockers.map((b) => ({ ...b, checks: citedChecks(b) })),
|
|
191
|
+
};
|
|
192
|
+
if (role === 'verifier' && i < all.length) {
|
|
193
|
+
for (const c of r.checks ?? []) rows.set(c.id, { ...c, round: i });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { roles: byRole, checks: [...rows.values()] };
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
177
200
|
/** `coverage` is `null` until a verifier report exists; only PROVADO covers. */
|
|
178
|
-
export function readChecks(dir, verifier) {
|
|
201
|
+
export function readChecks(dir, verifier, progress = readProgress(dir)) {
|
|
179
202
|
const checks = readJson(checksPath(dir))?.checks;
|
|
180
203
|
if (!Array.isArray(checks)) return null;
|
|
181
204
|
const list = checks.filter((c) => c && c.id);
|
|
@@ -192,6 +215,7 @@ export function readChecks(dir, verifier) {
|
|
|
192
215
|
proof: c.proof ?? null,
|
|
193
216
|
status: byId.get(c.id)?.status ?? null,
|
|
194
217
|
evidence: byId.get(c.id)?.evidence ?? null,
|
|
218
|
+
implement: progress?.byId.get(c.id)?.status ?? 'pendente',
|
|
195
219
|
})),
|
|
196
220
|
};
|
|
197
221
|
}
|
|
@@ -201,7 +225,7 @@ function summarize(name, resolved) {
|
|
|
201
225
|
const feature = normalizeFeature(resolved.feature);
|
|
202
226
|
const gates = gatesOf(feature);
|
|
203
227
|
const reviewApproved = gates.find((g) => g.gate === 'review')?.approved === true;
|
|
204
|
-
const
|
|
228
|
+
const progress = readProgress(dir);
|
|
205
229
|
const verdict = reviewVerdict(dir, feature.workType);
|
|
206
230
|
const status = feature.status || (location === 'archive' ? 'completed' : 'draft');
|
|
207
231
|
const finished = isFinished({ status, location });
|
|
@@ -218,9 +242,7 @@ function summarize(name, resolved) {
|
|
|
218
242
|
inEvaluation: verdict !== null && !reviewApproved,
|
|
219
243
|
gates,
|
|
220
244
|
awaiting: !finished && gates.some((g) => g.paused),
|
|
221
|
-
|
|
222
|
-
// Finished features leave slice titles out: archived plans would bloat every list reply.
|
|
223
|
-
sliceIndex: finished || !slices ? [] : slices.list.map((sl) => ({ id: sl.id, title: sl.title, status: sl.status })),
|
|
245
|
+
scenarios: progress?.counts ?? NO_SCENARIOS,
|
|
224
246
|
verification: feature.verification
|
|
225
247
|
? { status: feature.verification.status ?? null, ts: feature.verification.ts ?? null }
|
|
226
248
|
: null,
|
|
@@ -304,16 +326,15 @@ export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatu
|
|
|
304
326
|
if (!resolved) return null;
|
|
305
327
|
const summary = summarize(name, resolved);
|
|
306
328
|
const feature = normalizeFeature(resolved.feature);
|
|
307
|
-
const slices = readSlices(resolved.dir);
|
|
308
329
|
const reports = reviewReportsFromDir(resolved.dir);
|
|
309
330
|
const verdict = reviewVerdict(resolved.dir, feature.workType, reports);
|
|
310
331
|
const checks = readChecks(resolved.dir, reports.verifier);
|
|
311
332
|
return {
|
|
312
333
|
...summary,
|
|
313
|
-
slicesList: slices?.list ?? [],
|
|
314
334
|
checksList: checks?.list ?? [],
|
|
315
335
|
checksCoverage: checks?.coverage ?? null,
|
|
316
336
|
reviews: reviewRows(reports, feature.workType),
|
|
337
|
+
rounds: scoreboard(reports),
|
|
317
338
|
gateDecisions: feature.gateDecisions || {},
|
|
318
339
|
verificationNodes: feature.verification?.nodes || null,
|
|
319
340
|
featureVerification: feature.verifications?.feature || null,
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { existsSync, readdirSync } from 'fs';
|
|
8
8
|
import { scanFeatures, readFeatureDetail, readFeatureFile } from './features-scanner.js';
|
|
9
|
-
import { createEventStore, buildFeed, lastActivityByFeature, ALL_KINDS, DEFAULT_KINDS } from './activity-scanner.js';
|
|
9
|
+
import { createEventStore, buildFeed, lastActivityByFeature, sessionMap, ALL_KINDS, DEFAULT_KINDS } from './activity-scanner.js';
|
|
10
10
|
import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js';
|
|
11
11
|
import { isFinished } from './feature-status.js';
|
|
12
12
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
13
|
+
import { computeMetrics } from '../../lib/telemetry/metrics.js';
|
|
13
14
|
|
|
14
15
|
const TREE_TTL_MS = 60_000;
|
|
15
16
|
|
|
@@ -76,6 +77,7 @@ export function createProjectApi(projectDir, deps = {}) {
|
|
|
76
77
|
|
|
77
78
|
const features = () => (cache.features ||= scan(projectDir, { resolveTree: rememberedTree }));
|
|
78
79
|
const events = () => store.events();
|
|
80
|
+
let gatesCache = { from: null, length: -1, gates: null };
|
|
79
81
|
const known = (name) => features().features?.find((f) => f.name === name) || undefined;
|
|
80
82
|
|
|
81
83
|
/** Any `event` but 'change' may move a feature to another tree. */
|
|
@@ -111,7 +113,14 @@ export function createProjectApi(projectDir, deps = {}) {
|
|
|
111
113
|
const limit = Math.min(Number(u.searchParams.get('limit')) || 100, 500);
|
|
112
114
|
const all = events();
|
|
113
115
|
const feed = buildFeed(all, { feature, kinds, before: u.searchParams.get('before') || undefined, limit });
|
|
114
|
-
return json(200, { ...feed, kinds, cost: feature ? store.cost(feature) : null });
|
|
116
|
+
return json(200, { ...feed, kinds, cost: feature ? store.cost(feature) : null, session: feature ? sessionMap(all, feature) : null });
|
|
117
|
+
}
|
|
118
|
+
if (parts[0] === 'gates' && parts.length === 1) {
|
|
119
|
+
const all = events();
|
|
120
|
+
if (gatesCache.from !== all || gatesCache.length !== all.length) {
|
|
121
|
+
gatesCache = { from: all, length: all.length, gates: computeMetrics(all.filter((e) => e?.event === 'Gate')).gates };
|
|
122
|
+
}
|
|
123
|
+
return json(200, { gates: gatesCache.gates });
|
|
115
124
|
}
|
|
116
125
|
return json(404, { error: 'rota desconhecida' });
|
|
117
126
|
} catch (err) {
|
|
@@ -85,10 +85,20 @@ export function scanSkills(frameworkDir, phases = []) {
|
|
|
85
85
|
stacks: typeof fm.stacks === 'string' ? fm.stacks.replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
86
86
|
allowedTools: typeof fm['allowed-tools'] === 'string' ? fm['allowed-tools'].split(',').map((t) => t.trim()) : [],
|
|
87
87
|
phase: phaseOf.get(skillDir.name) || null,
|
|
88
|
-
dispatches:
|
|
88
|
+
dispatches: skillDispatches(join(levelPath, skillDir.name), content),
|
|
89
89
|
content,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
return skills.sort((a, b) => a.id.localeCompare(b.id));
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
function skillDispatches(skillDir, content) {
|
|
97
|
+
const refsDir = join(skillDir, 'references');
|
|
98
|
+
const refs = existsSync(refsDir)
|
|
99
|
+
? readdirSync(refsDir).filter((f) => f.endsWith('.md')).sort().map((f) => readFileSync(join(refsDir, f), 'utf8'))
|
|
100
|
+
: [];
|
|
101
|
+
return [content, ...refs]
|
|
102
|
+
.flatMap((text) => extractDispatchPrompts(text))
|
|
103
|
+
.map((d, index) => ({ ...d, index }));
|
|
104
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
3
|
-
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
-
import morphDataPlugin from './vite-plugin-morph-data.js';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { dirname, resolve } from 'path';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = dirname(__filename);
|
|
10
|
-
|
|
11
|
-
const frameworkDir = resolve(__dirname, '..', '..', 'framework');
|
|
12
|
-
const projectDir = process.env.MORPH_PROJECT_DIR || process.cwd();
|
|
13
|
-
|
|
14
|
-
export default defineConfig({
|
|
15
|
-
root: __dirname,
|
|
16
|
-
plugins: [
|
|
17
|
-
react(),
|
|
18
|
-
tailwindcss(),
|
|
19
|
-
morphDataPlugin(frameworkDir, projectDir),
|
|
20
|
-
],
|
|
21
|
-
server: {
|
|
22
|
-
port: 3200,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
import morphDataPlugin from './vite-plugin-morph-data.js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, resolve } from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const frameworkDir = resolve(__dirname, '..', '..', 'framework');
|
|
12
|
+
const projectDir = process.env.MORPH_PROJECT_DIR || process.cwd();
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
root: __dirname,
|
|
16
|
+
plugins: [
|
|
17
|
+
react(),
|
|
18
|
+
tailwindcss(),
|
|
19
|
+
morphDataPlugin(frameworkDir, projectDir),
|
|
20
|
+
],
|
|
21
|
+
server: {
|
|
22
|
+
port: 3200,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -9,6 +9,8 @@ export const E2E_NOT_APPLICABLE = 'not applicable: no `prova: e2e` scenario and
|
|
|
9
9
|
const UI_FILE = /\.(tsx|jsx|vue|svelte|astro|css|scss|sass|less|html)$/i;
|
|
10
10
|
const RUNTIME_FILE = /(^|\/)(dockerfile[^/]*|\.dockerignore|(docker-)?compose[^/]*\.ya?ml|playwright\.config\.[cm]?[jt]s)$/i;
|
|
11
11
|
const E2E_DIR = /(^|\/)e2e\//i;
|
|
12
|
+
// The harness writes here itself (verify syncs .claude/rules/), so these never mean the app changed.
|
|
13
|
+
const HARNESS_DIR = /^\.(morph|claude)\//;
|
|
12
14
|
const KNOWN_PACKAGE_DIRS = E2E_SPEC_LOCATIONS.filter((dir) => dir !== '.');
|
|
13
15
|
|
|
14
16
|
function packageDirsOf(config) {
|
|
@@ -28,7 +30,7 @@ export function e2eApplicability(projectPath, featureName, config) {
|
|
|
28
30
|
const { checks } = readPlanChecks(projectPath, featureName);
|
|
29
31
|
if (checks?.checks?.some((c) => c.proof === 'e2e')) return { applicable: true, reason: 'prova: e2e' };
|
|
30
32
|
const base = detectDefaultBranch(projectPath);
|
|
31
|
-
const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !
|
|
33
|
+
const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !HARNESS_DIR.test(f)) : [];
|
|
32
34
|
if (changed.length === 0) return { applicable: true, reason: 'diff unknown' };
|
|
33
35
|
const composeFile = resolveComposeFile(projectPath, config);
|
|
34
36
|
const packageDirs = packageDirsOf(config);
|
|
@@ -4,7 +4,6 @@ import { OUTSIDE_MORPH, treeFingerprint, git as gitBuffer } from './tree-fingerp
|
|
|
4
4
|
import { detectDefaultBranch } from './default-branch.js';
|
|
5
5
|
import { resolveBaseRef } from './branch-classifier.js';
|
|
6
6
|
|
|
7
|
-
export const MUTATION_MARKER = 'MUTACAO';
|
|
8
7
|
const MAX_LISTED = 50;
|
|
9
8
|
const MAX_COMMITS = 10;
|
|
10
9
|
|
|
@@ -51,7 +50,7 @@ export function snapshotTree(cwd) {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
|
-
* Compares content, not status:
|
|
53
|
+
* Compares content, not status: an edit committed since shows up, and a file
|
|
55
54
|
* committed unchanged does not.
|
|
56
55
|
* @returns {{ at: string, headChanged: boolean, changed: string[], changedCount: number }}
|
|
57
56
|
*/
|
|
@@ -104,19 +103,6 @@ function commitsOnTop(cwd) {
|
|
|
104
103
|
.split('\n').filter(Boolean);
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
function markers(cwd) {
|
|
108
|
-
try {
|
|
109
|
-
const out = git(cwd, ['grep', '-n', '-I', '--untracked', '-F', '-e', MUTATION_MARKER, ...OUTSIDE_MORPH]);
|
|
110
|
-
return out.split('\n').filter(Boolean).slice(0, MAX_LISTED).map((line) => {
|
|
111
|
-
const [file, lineNo] = line.split(':');
|
|
112
|
-
return { file, line: Number(lineNo) };
|
|
113
|
-
});
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (err.status === 1) return [];
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
106
|
/** @returns {object|null} null when git cannot answer: unknown, never a clean tree. */
|
|
121
107
|
export function treeState(cwd, { snapshot = null } = {}) {
|
|
122
108
|
try {
|
|
@@ -131,7 +117,6 @@ export function treeState(cwd, { snapshot = null } = {}) {
|
|
|
131
117
|
dirtyCount: dirty.length,
|
|
132
118
|
diffstat: head ? diffstat(cwd) : null,
|
|
133
119
|
commits: head ? commitsOnTop(cwd) : null,
|
|
134
|
-
markers: markers(cwd),
|
|
135
120
|
sinceBeforeEval: snapshot ? compareToSnapshot(cwd, snapshot) : null,
|
|
136
121
|
};
|
|
137
122
|
} catch {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { buildIndex, linksOf, resolveEdgeTarget, isTestPath } from './sanitize.js';
|
|
2
|
+
|
|
3
|
+
const CALL_RELATIONS = new Set(['calls', 'indirect_call', 'dispatches_to']);
|
|
4
|
+
const HUNK_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
5
|
+
|
|
6
|
+
/** `git diff -U0` → per file: whether it is new, and the new-side line ranges the diff touched. */
|
|
7
|
+
export function parseDiff(text) {
|
|
8
|
+
const files = new Map();
|
|
9
|
+
let current = null;
|
|
10
|
+
let fromNull = false;
|
|
11
|
+
for (const line of String(text || '').split('\n')) {
|
|
12
|
+
if (line.startsWith('--- ')) { fromNull = line === '--- /dev/null'; continue; }
|
|
13
|
+
if (line.startsWith('+++ ')) {
|
|
14
|
+
const path = line.slice(4).replace(/^b\//, '').trim();
|
|
15
|
+
current = path === '/dev/null' ? null : { added: fromNull, ranges: [] };
|
|
16
|
+
if (current) files.set(path, current);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const hunk = current && HUNK_RE.exec(line);
|
|
20
|
+
if (!hunk) continue;
|
|
21
|
+
const start = Number(hunk[1]);
|
|
22
|
+
const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
|
|
23
|
+
// A pure deletion has no new-side line: it touches the line it left behind.
|
|
24
|
+
current.ranges.push(count === 0 ? [Math.max(1, start), Math.max(1, start)] : [start, start + count - 1]);
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lineOf = (node) => Number(/^L(\d+)/.exec(node.source_location || '')?.[1]) || null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The call tree of what changed: every graph symbol whose lines the diff touched, `+` when its file is
|
|
33
|
+
* new and `~` otherwise, linked by the calls between them, entry points (no changed caller) first.
|
|
34
|
+
*/
|
|
35
|
+
export function buildDiffTree(graph, changes) {
|
|
36
|
+
const index = buildIndex(graph);
|
|
37
|
+
const perFile = new Map();
|
|
38
|
+
for (const node of graph.nodes ?? []) {
|
|
39
|
+
if (!node._callable || !changes.has(node.source_file) || lineOf(node) === null) continue;
|
|
40
|
+
if (!perFile.has(node.source_file)) perFile.set(node.source_file, []);
|
|
41
|
+
perFile.get(node.source_file).push(node);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const changed = new Map();
|
|
45
|
+
for (const [file, nodes] of perFile) {
|
|
46
|
+
const change = changes.get(file);
|
|
47
|
+
nodes.sort((a, b) => lineOf(a) - lineOf(b));
|
|
48
|
+
nodes.forEach((node, i) => {
|
|
49
|
+
const start = lineOf(node);
|
|
50
|
+
const end = i + 1 < nodes.length ? lineOf(nodes[i + 1]) - 1 : Infinity;
|
|
51
|
+
const touched = change.added || change.ranges.some(([a, b]) => a <= end && b >= start);
|
|
52
|
+
if (touched) {
|
|
53
|
+
changed.set(node.id, { id: node.id, label: node.label, file, line: start, mark: change.added ? '+' : '~', test: isTestPath(file) });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const callees = new Map([...changed.keys()].map((id) => [id, new Set()]));
|
|
59
|
+
const hasCaller = new Set();
|
|
60
|
+
for (const link of linksOf(index, graph)) {
|
|
61
|
+
if (!CALL_RELATIONS.has(link.relation) || !changed.has(link.source)) continue;
|
|
62
|
+
const target = resolveEdgeTarget(index, link).id;
|
|
63
|
+
if (!target || target === link.source || !changed.has(target)) continue;
|
|
64
|
+
callees.get(link.source).add(target);
|
|
65
|
+
hasCaller.add(target);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const byPlace = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
|
|
69
|
+
const shown = new Set();
|
|
70
|
+
const grow = (id, path) => {
|
|
71
|
+
const node = changed.get(id);
|
|
72
|
+
if (path.has(id)) return { ...node, cycle: true, children: [] };
|
|
73
|
+
if (shown.has(id)) return { ...node, repeat: true, children: [] };
|
|
74
|
+
shown.add(id);
|
|
75
|
+
const next = new Set(path).add(id);
|
|
76
|
+
const children = [...callees.get(id)].map((c) => changed.get(c)).sort(byPlace).map((c) => grow(c.id, next));
|
|
77
|
+
return { ...node, children };
|
|
78
|
+
};
|
|
79
|
+
const entries = [...changed.values()].filter((n) => !hasCaller.has(n.id)).sort(byPlace);
|
|
80
|
+
const roots = entries.map((n) => grow(n.id, new Set()));
|
|
81
|
+
// Only cycles left: every node has a changed caller, so start from what was not reached.
|
|
82
|
+
for (const n of [...changed.values()].sort(byPlace)) if (!shown.has(n.id)) roots.push(grow(n.id, new Set()));
|
|
83
|
+
|
|
84
|
+
const mapped = new Set([...changed.values()].map((n) => n.file));
|
|
85
|
+
const values = [...changed.values()];
|
|
86
|
+
return {
|
|
87
|
+
roots,
|
|
88
|
+
counts: {
|
|
89
|
+
added: values.filter((n) => n.mark === '+').length,
|
|
90
|
+
modified: values.filter((n) => n.mark === '~').length,
|
|
91
|
+
entries: entries.length,
|
|
92
|
+
files: changes.size,
|
|
93
|
+
},
|
|
94
|
+
unmapped: [...changes.keys()].filter((f) => !mapped.has(f)).sort(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Symbols whose graph line does not declare them in the committed file: the commit stamp can match HEAD
|
|
100
|
+
* while the extraction read a dirty tree, and then every line in the tree is off.
|
|
101
|
+
*/
|
|
102
|
+
export function declarationDrift(tree, readLines) {
|
|
103
|
+
const drift = [];
|
|
104
|
+
const files = new Map();
|
|
105
|
+
const walk = (node) => {
|
|
106
|
+
if (!node.repeat && !node.cycle) {
|
|
107
|
+
if (!files.has(node.file)) files.set(node.file, readLines(node.file));
|
|
108
|
+
const name = String(node.label).replace(/^\./, '').split('(')[0].trim();
|
|
109
|
+
if (!(files.get(node.file)?.[node.line - 1] ?? '').includes(name)) drift.push({ file: node.file, line: node.line, label: node.label });
|
|
110
|
+
}
|
|
111
|
+
node.children.forEach(walk);
|
|
112
|
+
};
|
|
113
|
+
tree.roots.forEach(walk);
|
|
114
|
+
return drift;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function renderDiffTree(tree) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
const walk = (node, depth) => {
|
|
120
|
+
const tag = node.cycle ? ' ↺ ciclo' : node.repeat ? ' ↑ já mostrado' : '';
|
|
121
|
+
const test = node.test ? ' [teste]' : '';
|
|
122
|
+
lines.push(`${' '.repeat(depth)}${node.mark} ${node.label} ${node.file}:${node.line}${test}${tag}`);
|
|
123
|
+
for (const c of node.children) walk(c, depth + 1);
|
|
124
|
+
};
|
|
125
|
+
for (const r of tree.roots) walk(r, 0);
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// O binário é `graphify` (e `graphify-mcp`). NUNCA `python -m graphify.serve`, como manda o README oficial:
|
|
2
2
|
// com `uv tool install` o pacote vive num venv isolado que o Python do sistema não enxerga.
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { GRAPHIFY_PACKAGE, resolveGraphify } from '../../../framework/hooks/shared/graph-refresh.js';
|
|
5
|
+
|
|
6
|
+
export { GRAPHIFY_PACKAGE };
|
|
4
7
|
|
|
5
8
|
// Pin exato: o graphify é pré-1.0 com release quase diária e semver não garante nada; subir é decisão
|
|
6
9
|
// consciente, com o fixture de regressão regerado.
|
|
7
10
|
export const GRAPHIFY_VERSION = '0.9.50';
|
|
8
|
-
export const GRAPHIFY_PACKAGE = 'graphifyy';
|
|
9
11
|
export const GRAPHIFY_BIN = 'graphify';
|
|
10
12
|
|
|
11
13
|
const PREFERENCE = ['uv', 'pipx', 'python3', 'python'];
|
|
@@ -56,10 +58,15 @@ export function installArgs(kind, { withSql }) {
|
|
|
56
58
|
|
|
57
59
|
/** Versão instalada, ou null se o binário não responde. */
|
|
58
60
|
export function detectGraphifyVersion(deps = {}) {
|
|
59
|
-
const run = deps._run ?? (() =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const run = deps._run ?? (() => {
|
|
62
|
+
const bin = resolveGraphify();
|
|
63
|
+
if (!bin) return '';
|
|
64
|
+
return execFileSync(bin.file, ['--version'], {
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
67
|
+
shell: bin.shell,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
63
70
|
try {
|
|
64
71
|
const m = /(\d+\.\d+\.\d+)/.exec(run());
|
|
65
72
|
return m ? m[1] : null;
|
package/src/lib/heavy/gate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { minFreeMemFor, readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
2
2
|
import { heavyClass } from '../../../framework/hooks/shared/heavy-command.js';
|
|
3
3
|
import {
|
|
4
|
-
acquireHeavy, awaitFreeMemory, describeBusy, heavyDir, releaseHeavy, updateLease,
|
|
4
|
+
acquireHeavy, awaitFreeMemory, describeBusy, enterVerifySuite, heavyDir, isVerifySuite, releaseHeavy, updateLease,
|
|
5
5
|
} from '../../../framework/hooks/shared/heavy-slots.js';
|
|
6
6
|
|
|
7
7
|
export class HeavyBusyError extends Error {
|
|
@@ -33,6 +33,7 @@ export function createHeavyGate({
|
|
|
33
33
|
let lease = null;
|
|
34
34
|
let admittedGb = 0;
|
|
35
35
|
let waitedMs = 0;
|
|
36
|
+
let suiteEntered = false;
|
|
36
37
|
let exitHandler = null;
|
|
37
38
|
let saved = null;
|
|
38
39
|
|
|
@@ -52,6 +53,7 @@ export function createHeavyGate({
|
|
|
52
53
|
lease = null;
|
|
53
54
|
restoreEnv();
|
|
54
55
|
entered = false;
|
|
56
|
+
suiteEntered = false;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
const raiseFloor = (kind) => {
|
|
@@ -65,12 +67,29 @@ export function createHeavyGate({
|
|
|
65
67
|
admittedGb = floor;
|
|
66
68
|
};
|
|
67
69
|
|
|
70
|
+
const awaitSuite = () => {
|
|
71
|
+
const result = enterVerifySuite({
|
|
72
|
+
dir: heavyDir(env),
|
|
73
|
+
lease,
|
|
74
|
+
slots: config.slots,
|
|
75
|
+
waitTimeoutMs: config.waitTimeoutSec * 1000,
|
|
76
|
+
maxHoldMs: config.maxHoldMinutes * 60_000,
|
|
77
|
+
log,
|
|
78
|
+
...deps,
|
|
79
|
+
});
|
|
80
|
+
if (result.status === 'busy') throw new HeavyBusyError(result, config);
|
|
81
|
+
waitedMs += result.waitedMs;
|
|
82
|
+
suiteEntered = true;
|
|
83
|
+
};
|
|
84
|
+
|
|
68
85
|
return {
|
|
69
86
|
config,
|
|
70
87
|
get waitedMs() { return waitedMs; },
|
|
71
88
|
|
|
72
89
|
enter(node, kind = heavyClass(node)) {
|
|
90
|
+
const suite = isVerifySuite({ label, node });
|
|
73
91
|
if (entered) {
|
|
92
|
+
if (suite && lease && !suiteEntered) awaitSuite();
|
|
74
93
|
raiseFloor(kind);
|
|
75
94
|
updateLease(lease, { node });
|
|
76
95
|
return;
|
|
@@ -86,6 +105,7 @@ export function createHeavyGate({
|
|
|
86
105
|
feature,
|
|
87
106
|
node,
|
|
88
107
|
kind,
|
|
108
|
+
suite,
|
|
89
109
|
cwd: projectPath,
|
|
90
110
|
minFreeMemGb,
|
|
91
111
|
waitTimeoutMs: config.waitTimeoutSec * 1000,
|
|
@@ -96,6 +116,7 @@ export function createHeavyGate({
|
|
|
96
116
|
});
|
|
97
117
|
if (result.status === 'busy') throw new HeavyBusyError(result, config);
|
|
98
118
|
lease = result.lease;
|
|
119
|
+
suiteEntered = suite;
|
|
99
120
|
admittedGb = minFreeMemGb;
|
|
100
121
|
waitedMs = result.waitedMs;
|
|
101
122
|
}
|
|
@@ -29,7 +29,7 @@ metadata:
|
|
|
29
29
|
## Key paths
|
|
30
30
|
|
|
31
31
|
- \`.morph/framework/MORPH.md\` — NLH charter (read once per session)
|
|
32
|
-
- \`.morph/framework/agents.json\` —
|
|
32
|
+
- \`.morph/framework/agents.json\` — the 3 read-only Gate 3 agents (\`verifier\`, \`reviewer\`, \`security\`)
|
|
33
33
|
- \`.morph/framework/standards/STANDARDS.json\` — standards index (load on demand)
|
|
34
34
|
- \`.morph/features/{feature}/\` — feature artifacts: 2-plan / 1-design / 3-implement / 4-review
|
|
35
35
|
- \`.morph/context/\` — long-lived architectural context (design-system, decisions)
|
|
@@ -72,7 +72,7 @@ metadata:
|
|
|
72
72
|
- \`.morph/framework/MORPH.md\` — NLH charter, Trust Mode rules, gates, skills table
|
|
73
73
|
- \`.morph/framework/standards/STANDARDS.json\` — index; load specific standard via \`path\` field
|
|
74
74
|
- \`.morph/framework/phases.json\` — phase folder mapping and skill associations
|
|
75
|
-
- \`.morph/framework/agents.json\` —
|
|
75
|
+
- \`.morph/framework/agents.json\` — Gate 3 agent definitions (verifier, reviewer, security)
|
|
76
76
|
- \`.morph/framework/evals/review-checklist.md\` — what the Gate 3 verifier, reviewer and security check (verdict SHIP/FIX + evidence-typed blockers)
|
|
77
77
|
`;
|
|
78
78
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { parseStacks, shouldInstall } from '../stack-filter.js';
|
|
4
|
+
import { realEffects } from './effects.js';
|
|
5
|
+
|
|
6
|
+
export const MAF_TAG = 'maf';
|
|
7
|
+
export const MAF_RULE = 'ai-agents-standards.md';
|
|
8
|
+
const MAF_PATHS = ['**/*.cs'];
|
|
9
|
+
|
|
10
|
+
/** The text a rule is installed with: in a MAF project the agents rule covers every `.cs`, not only `Agents/` folders. */
|
|
11
|
+
export function ruleContentFor(file, content, projectTags) {
|
|
12
|
+
if (file !== MAF_RULE || !projectTags.includes(MAF_TAG)) return content;
|
|
13
|
+
const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
14
|
+
if (!fm) return content;
|
|
15
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
16
|
+
const list = MAF_PATHS.map((p) => ` - "${p}"`).join(eol);
|
|
17
|
+
const rewritten = fm[1].replace(
|
|
18
|
+
/^paths:[ \t]*(?:\r?\n[ \t]+-[^\r\n]*)*(\r?\n)?/m,
|
|
19
|
+
(_, tail) => `paths:${eol}${list}${tail ? eol : ''}`,
|
|
20
|
+
);
|
|
21
|
+
const start = fm[0].indexOf(fm[1]);
|
|
22
|
+
return content.slice(0, start) + rewritten + content.slice(start + fm[1].length);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installRuleFile(rulesSrc, rulesDest, file, projectTags, fx = realEffects()) {
|
|
26
|
+
const from = join(rulesSrc, file);
|
|
27
|
+
const to = join(rulesDest, file);
|
|
28
|
+
const content = readFileSync(from, 'utf-8');
|
|
29
|
+
const desired = ruleContentFor(file, content, projectTags);
|
|
30
|
+
if (desired === content) fx.copyFile(from, to);
|
|
31
|
+
else fx.writeFile(to, desired);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Also removes orphan rules in rulesDest that are no longer in the install set. */
|
|
35
|
+
export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = realEffects()) {
|
|
36
|
+
const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
|
|
37
|
+
const installed = new Set();
|
|
38
|
+
|
|
39
|
+
for (const file of sourceFiles) {
|
|
40
|
+
const content = readFileSync(join(rulesSrc, file), 'utf-8');
|
|
41
|
+
if (shouldInstall(parseStacks(content), projectTags)) {
|
|
42
|
+
installRuleFile(rulesSrc, rulesDest, file, projectTags, fx);
|
|
43
|
+
installed.add(file);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Calado em `dryRun`: `cleanFrameworkDirs()` já apagou `.claude/rules/` inteiro antes da execução real.
|
|
48
|
+
if (!fx.dryRun) {
|
|
49
|
+
for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
|
|
50
|
+
if (!installed.has(file)) {
|
|
51
|
+
fx.remove(join(rulesDest, file));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { installed: installed.size };
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { installRulesForStack } from './rules-installer.js';
|
|
3
|
+
import { installCommandsForStack } from '../../scripts/setup-infra.js';
|
|
4
|
+
import { installSkills } from '../../utils/skills-installer.js';
|
|
5
|
+
import { installAgents } from '../../utils/agents-installer.js';
|
|
6
|
+
|
|
7
|
+
const FRAMEWORK_SRC = join(import.meta.dirname, '..', '..', '..', 'framework');
|
|
8
|
+
|
|
9
|
+
/** Everything filtered by `stacks:` — rules, commands, skills, agents — for the project's tags. The one path init and stack-set share. */
|
|
10
|
+
export async function installStackAssets(targetPath, stackTags, { frameworkSrc = FRAMEWORK_SRC } = {}) {
|
|
11
|
+
if (stackTags.length === 0) return false;
|
|
12
|
+
installRulesForStack(join(frameworkSrc, 'rules'), join(targetPath, '.claude', 'rules'), stackTags);
|
|
13
|
+
installCommandsForStack(join(frameworkSrc, 'commands'), join(targetPath, '.claude', 'commands'), stackTags);
|
|
14
|
+
await installSkills(targetPath, { projectTags: stackTags });
|
|
15
|
+
await installAgents(targetPath, frameworkSrc, { projectTags: stackTags });
|
|
16
|
+
return true;
|
|
17
|
+
}
|