@polymorphism-tech/morph-spec 9.8.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 +10 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- 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 +83 -94
- 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/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 +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 +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/REGISTRY.json +1 -2
- 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/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 +1 -1
- package/src/commands/state/create.js +0 -5
- 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 +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/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 +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 +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/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
|
@@ -2,11 +2,46 @@ import { useState } from 'react';
|
|
|
2
2
|
import { Link } from 'react-router-dom';
|
|
3
3
|
import { FolderSimplePlus, X, ArrowRight } from '@phosphor-icons/react';
|
|
4
4
|
import { FeatureRow, isWorking } from './Features.jsx';
|
|
5
|
-
import { useProjects, projectHref, announceProjectsChanged } from '../lib/project.js';
|
|
5
|
+
import { useProjects, projectHref, featureHref, announceProjectsChanged } from '../lib/project.js';
|
|
6
6
|
import { useNow } from '../hooks/usePref.js';
|
|
7
7
|
import { timeAgo } from '../lib/format.js';
|
|
8
8
|
|
|
9
9
|
const ACTIVE_SHOWN = 4;
|
|
10
|
+
const GATE_LABEL = { plan: 'Plano', uiux: 'Design', review: 'Gate 3' };
|
|
11
|
+
const GATE_COLOR = { plan: 'var(--color-phase-plan)', uiux: 'var(--color-phase-uiux)', review: 'var(--color-phase-review)' };
|
|
12
|
+
|
|
13
|
+
// One row per gate that waits for the human, oldest first: the longest wait is the next decision.
|
|
14
|
+
export function decisionQueue(projects = []) {
|
|
15
|
+
return projects
|
|
16
|
+
.filter((p) => p.summary)
|
|
17
|
+
.flatMap((p) => p.summary.activeFeatures.flatMap((f) => (f.gates || [])
|
|
18
|
+
.filter((g) => g.paused)
|
|
19
|
+
.map((g) => ({ project: p, feature: f, gate: g.gate, reason: g.reason, since: g.since }))))
|
|
20
|
+
.sort((a, b) => (a.since || '').localeCompare(b.since || ''));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function DecisionRow({ item, now }) {
|
|
24
|
+
const { project, feature, gate, reason, since } = item;
|
|
25
|
+
return (
|
|
26
|
+
<li>
|
|
27
|
+
<Link to={featureHref(project.id, feature.name)}
|
|
28
|
+
className="fluid grid grid-cols-[4.5rem_minmax(0,1fr)] items-start gap-x-4 gap-y-1 px-5 py-3.5 transition-colors hover:bg-white/[0.025] md:grid-cols-[4.5rem_minmax(0,1fr)_auto]">
|
|
29
|
+
<span className="mt-0.5 rounded-full px-2 py-px text-center text-[11px] font-medium shadow-[inset_0_0_0_1px_var(--color-hair-strong)]"
|
|
30
|
+
style={{ color: GATE_COLOR[gate] }}>{GATE_LABEL[gate] ?? gate}</span>
|
|
31
|
+
<div className="min-w-0">
|
|
32
|
+
<div className="flex items-baseline gap-2">
|
|
33
|
+
<span className="truncate font-medium text-[var(--color-fg)]">{feature.name}</span>
|
|
34
|
+
<span className="shrink-0 text-xs text-[var(--color-faint)]">{project.name}</span>
|
|
35
|
+
</div>
|
|
36
|
+
{reason && <p className="mt-0.5 line-clamp-2 text-sm text-[var(--color-dim)]">{reason}</p>}
|
|
37
|
+
</div>
|
|
38
|
+
<span className="col-start-2 whitespace-nowrap text-xs text-[var(--color-faint)] md:col-start-auto md:text-right" title={since || undefined}>
|
|
39
|
+
{since ? `esperando ${timeAgo(since, now)}` : 'sem horário'}
|
|
40
|
+
</span>
|
|
41
|
+
</Link>
|
|
42
|
+
</li>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
10
45
|
|
|
11
46
|
async function send(method, url, body) {
|
|
12
47
|
const res = await fetch(url, {
|
|
@@ -79,7 +114,7 @@ export function PortfolioView({ data, now = Date.now(), onAdd = () => {}, onRemo
|
|
|
79
114
|
const ready = projects.filter((p) => p.summary);
|
|
80
115
|
const totalActive = ready.reduce((n, p) => n + p.summary.active, 0);
|
|
81
116
|
const totalAwaiting = ready.reduce((n, p) => n + p.summary.awaiting, 0);
|
|
82
|
-
const
|
|
117
|
+
const queue = decisionQueue(projects);
|
|
83
118
|
const ordered = [...projects].sort((a, b) => (b.summary?.lastActivityTs || '').localeCompare(a.summary?.lastActivityTs || ''));
|
|
84
119
|
|
|
85
120
|
return (
|
|
@@ -92,12 +127,12 @@ export function PortfolioView({ data, now = Date.now(), onAdd = () => {}, onRemo
|
|
|
92
127
|
</p>
|
|
93
128
|
</header>
|
|
94
129
|
|
|
95
|
-
{
|
|
130
|
+
{queue.length > 0 && (
|
|
96
131
|
<section className="rise-2 mb-10">
|
|
97
|
-
<h2 className="mb-3 px-1 text-sm font-medium">Esperando
|
|
132
|
+
<h2 className="mb-3 px-1 text-sm font-medium">Esperando você <span className="text-[var(--color-faint)]">· a mais antiga primeiro</span></h2>
|
|
98
133
|
<div className="bezel">
|
|
99
134
|
<ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
|
|
100
|
-
{
|
|
135
|
+
{queue.map((item) => <DecisionRow key={`${item.project.id}:${item.feature.name}:${item.gate}`} item={item} now={now} />)}
|
|
101
136
|
</ul>
|
|
102
137
|
</div>
|
|
103
138
|
</section>
|
|
@@ -263,6 +263,35 @@ export function createEventStore(projectDir, { loadTrees = loadTelemetryAllTrees
|
|
|
263
263
|
};
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
const SESSION_GAP_MS = 30 * 60 * 1000;
|
|
267
|
+
const SESSION_MARK_KINDS = new Set(['prompt', 'question', 'dispatch', 'handback', 'verify', 'gate', 'failure']);
|
|
268
|
+
|
|
269
|
+
// "Where was I": the feature's most recent stretch of work, split from the previous one by a 30-minute silence.
|
|
270
|
+
export function sessionMap(events, feature, { gapMs = SESSION_GAP_MS } = {}) {
|
|
271
|
+
const lines = describeAll(events).filter((l) => l.feature === feature && l.ts);
|
|
272
|
+
if (lines.length === 0) return null;
|
|
273
|
+
const session = [lines[0]];
|
|
274
|
+
for (let i = 1; i < lines.length; i++) {
|
|
275
|
+
if (Date.parse(session[session.length - 1].ts) - Date.parse(lines[i].ts) > gapMs) break;
|
|
276
|
+
session.push(lines[i]);
|
|
277
|
+
}
|
|
278
|
+
const files = new Map();
|
|
279
|
+
for (const l of session) {
|
|
280
|
+
if (l.kind === 'write' && l.refs.path) files.set(l.refs.path, (files.get(l.refs.path) || 0) + 1);
|
|
281
|
+
}
|
|
282
|
+
const lastGreen = session.find((l) => l.kind === 'verify' && l.status === 'ok');
|
|
283
|
+
const lastFailure = session.find((l) => l.status === 'fail' && (!lastGreen || l.ts > lastGreen.ts)) || null;
|
|
284
|
+
return {
|
|
285
|
+
start: session[session.length - 1].ts,
|
|
286
|
+
end: session[0].ts,
|
|
287
|
+
actions: session.length,
|
|
288
|
+
marks: session.filter((l) => SESSION_MARK_KINDS.has(l.kind)).reverse()
|
|
289
|
+
.map(({ ts, kind, status, title }) => ({ ts, kind, status, title })),
|
|
290
|
+
files: [...files].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([path, edits]) => ({ path, edits })),
|
|
291
|
+
lastFailure: lastFailure && { ts: lastFailure.ts, title: lastFailure.title, detail: lastFailure.detail },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
/** `sessionUsage` and `agentUsage` stay `null` when the stream has no summable sample. */
|
|
267
296
|
export function featureCost(events, feature) {
|
|
268
297
|
const c = aggregateCost(events, { feature });
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { personaStandardIds,
|
|
3
|
+
import { personaStandardIds, renderAgentMd, loadKnownMcpIds } from '../../utils/agents-installer.js';
|
|
4
4
|
|
|
5
5
|
const DISPATCH_LINE = /despach|dispatch|subagent_type/i;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Only dispatch lines count: morph-plan cites `morph-dotnet-senior` as a
|
|
11
|
-
* spelling example of the `agent` field, which dispatches nobody.
|
|
12
|
-
*/
|
|
7
|
+
// Only dispatch lines count: a skill that merely names a persona dispatches nobody. Two phases can
|
|
8
|
+
// share a skill, so beyond SKILL.md only the phase's own `references/{phase}.md` is read.
|
|
13
9
|
export function personaPhases(frameworkDir, agents) {
|
|
14
10
|
const result = new Map(Object.keys(agents).map((id) => [id, []]));
|
|
15
11
|
const phasesJsonPath = join(frameworkDir, 'phases.json');
|
|
@@ -18,13 +14,15 @@ export function personaPhases(frameworkDir, agents) {
|
|
|
18
14
|
const phases = Object.values(JSON.parse(readFileSync(phasesJsonPath, 'utf8')).phases || {})
|
|
19
15
|
.sort((a, b) => a.index - b.index);
|
|
20
16
|
for (const phase of phases) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
17
|
+
if (!phase.skill) continue;
|
|
18
|
+
const skillDir = join(frameworkDir, 'skills', 'core', phase.skill);
|
|
19
|
+
const sources = [join(skillDir, 'SKILL.md'), join(skillDir, 'references', `${phase.id}.md`)].filter(existsSync);
|
|
20
|
+
const dispatchLines = sources
|
|
21
|
+
.flatMap((f) => readFileSync(f, 'utf8').split(/\r?\n/))
|
|
22
|
+
.filter((l) => DISPATCH_LINE.test(l));
|
|
23
|
+
for (const id of Object.keys(agents)) {
|
|
26
24
|
const named = new RegExp(`morph-${id}(?![\\w-])`);
|
|
27
|
-
if (
|
|
25
|
+
if (dispatchLines.some((l) => named.test(l))) {
|
|
28
26
|
result.get(id).push(phase.id);
|
|
29
27
|
}
|
|
30
28
|
}
|
|
@@ -52,7 +50,6 @@ export function scanAgents(frameworkDir) {
|
|
|
52
50
|
domains: agent.domains || [],
|
|
53
51
|
keywords: agent.keywords || [],
|
|
54
52
|
standards: personaStandardIds(agent),
|
|
55
|
-
surfaces: agent.surfaces || null,
|
|
56
53
|
stack: agent.stack || ['*'],
|
|
57
54
|
activePhases: phasesOf.get(id),
|
|
58
55
|
persona: agent.persona || '',
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
|