@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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { dirname, join, resolve } from 'path';
|
|
6
|
-
import { homedir } from 'os';
|
|
7
6
|
import { stateExists } from '../../shared/state-reader.js';
|
|
8
7
|
import { resolveGitCommonDir } from '../../shared/git-paths.js';
|
|
9
8
|
import { runMorphSpec } from '../../shared/morph-cli.js';
|
|
@@ -34,43 +33,6 @@ export function slugify(name) {
|
|
|
34
33
|
return slug || `wt-${Date.now().toString(36)}`;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
function readJsonSafe(path) {
|
|
38
|
-
try { return JSON.parse(readFileSync(path, 'utf8').replace(/^/, '')); } catch { return null; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// `null` when unset, distinct from an explicit `'fresh'`, which overrides the feature-branch default.
|
|
42
|
-
export function readBaseRefSetting(root, home = homedir()) {
|
|
43
|
-
let value = null;
|
|
44
|
-
for (const p of [
|
|
45
|
-
join(home, '.claude', 'settings.json'),
|
|
46
|
-
join(root, '.claude', 'settings.json'),
|
|
47
|
-
join(root, '.claude', 'settings.local.json'),
|
|
48
|
-
]) {
|
|
49
|
-
const s = readJsonSafe(p);
|
|
50
|
-
const v = s?.worktree?.baseRef;
|
|
51
|
-
if (typeof v === 'string' && v) value = v;
|
|
52
|
-
}
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function headRef(cwd) {
|
|
57
|
-
try {
|
|
58
|
-
const name = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
59
|
-
if (name && name !== 'HEAD') return name;
|
|
60
|
-
return git(['rev-parse', 'HEAD'], cwd) || null;
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Unset → the `morph/<f>` branch `cwd` is on: a sub-agent tree born from the default branch lacks the feature's work.
|
|
67
|
-
export function resolveBase(setting, cwd) {
|
|
68
|
-
if (setting === 'head') return headRef(cwd);
|
|
69
|
-
if (setting) return null;
|
|
70
|
-
const current = headRef(cwd);
|
|
71
|
-
return current && /^morph\/.+/.test(current) ? current : null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
36
|
function isKnownFeature(root, name) {
|
|
75
37
|
if (existsSync(join(root, '.morph', 'features', name, 'feature.json'))) return true;
|
|
76
38
|
try {
|
|
@@ -92,14 +54,14 @@ function defaultBranch(root) {
|
|
|
92
54
|
return null;
|
|
93
55
|
}
|
|
94
56
|
|
|
95
|
-
function gitFallback(root, name
|
|
57
|
+
function gitFallback(root, name) {
|
|
96
58
|
const branch = `morph-task/${name}`;
|
|
97
59
|
const container = newWorktreesDir(root);
|
|
98
60
|
const path = `${container}/${name}`;
|
|
99
61
|
if (existsSync(path)) return { path };
|
|
100
62
|
try { git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], root); }
|
|
101
63
|
catch {
|
|
102
|
-
const from =
|
|
64
|
+
const from = defaultBranch(root);
|
|
103
65
|
git(from ? ['branch', branch, from] : ['branch', branch], root);
|
|
104
66
|
}
|
|
105
67
|
mkdirSync(container, { recursive: true });
|
|
@@ -113,21 +75,20 @@ export async function run(payload) {
|
|
|
113
75
|
if (!root) return { error: `worktree-create: "${start}" is not inside a git repository` };
|
|
114
76
|
|
|
115
77
|
const name = slugify(payload?.name);
|
|
116
|
-
const base = resolveBase(readBaseRefSetting(root), start);
|
|
117
78
|
|
|
118
79
|
if (!stateExists(root)) {
|
|
119
|
-
try { return gitFallback(root, name
|
|
80
|
+
try { return gitFallback(root, name); }
|
|
120
81
|
catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
|
|
121
82
|
}
|
|
122
83
|
|
|
123
84
|
const args = isKnownFeature(root, name)
|
|
124
85
|
? ['worktree', 'setup', name, '--json']
|
|
125
|
-
: ['worktree', 'provision', name, '--json'
|
|
86
|
+
: ['worktree', 'provision', name, '--json'];
|
|
126
87
|
|
|
127
88
|
const r = runMorphSpec(args, { cwd: root });
|
|
128
89
|
if (r.unavailable) {
|
|
129
90
|
process.stderr.write('worktree-create: morph-spec CLI not found — creating the tree with plain git under worktrees/ (no infra links, no port block). Run `morph-spec worktree link --all` inside it.\n');
|
|
130
|
-
try { return gitFallback(root, name
|
|
91
|
+
try { return gitFallback(root, name); }
|
|
131
92
|
catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
|
|
132
93
|
}
|
|
133
94
|
if (r.stderr?.trim()) process.stderr.write(r.stderr);
|
|
@@ -49,11 +49,7 @@ export async function run(payload) {
|
|
|
49
49
|
// CLAUDE_PROJECT_DIR first: it is what the project hooks resolve `.morph/framework` against.
|
|
50
50
|
const anchored = anchorSessionCommand(command, process.env.CLAUDE_PROJECT_DIR || payload?.cwd || '');
|
|
51
51
|
if (anchored) {
|
|
52
|
-
return updatedInputResult(
|
|
53
|
-
{ ...payload.tool_input, command: anchored.command },
|
|
54
|
-
'PreToolUse',
|
|
55
|
-
`⚓ âncora da sessão exportada para o CLI: ${ANCHOR_KEY}=${anchored.anchor} — o finish/worktree remove adia o desmonte do worktree em que esta sessão está ancorada`,
|
|
56
|
-
);
|
|
52
|
+
return updatedInputResult({ ...payload.tool_input, command: anchored.command }, 'PreToolUse');
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
const dir = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
@@ -63,11 +59,7 @@ export async function run(payload) {
|
|
|
63
59
|
const rewritten = rewriteCommand(command, loadEnvFile(manifest));
|
|
64
60
|
if (!rewritten) return null;
|
|
65
61
|
|
|
66
|
-
return updatedInputResult(
|
|
67
|
-
{ ...payload.tool_input, command: rewritten.command },
|
|
68
|
-
'PreToolUse',
|
|
69
|
-
`🔌 worktree env injetado (.morph/worktree.env): ${rewritten.injected.join(' ')}`,
|
|
70
|
-
);
|
|
62
|
+
return updatedInputResult({ ...payload.tool_input, command: rewritten.command }, 'PreToolUse');
|
|
71
63
|
} catch {
|
|
72
64
|
return null;
|
|
73
65
|
}
|
|
Binary file
|
|
@@ -9,10 +9,10 @@ export const GATE_STEPS = ['decision', 'approve', 'unapprove'];
|
|
|
9
9
|
* The trail `cost --metrics` turns into "what did the human do with each pause":
|
|
10
10
|
* `gateDecisions` keeps only the latest decision, so the history lives here.
|
|
11
11
|
*/
|
|
12
|
-
export function recordGateEvent(projectPath, { feature, gate, step, pause, signal, source, approvedBy }) {
|
|
12
|
+
export function recordGateEvent(projectPath, { feature, gate, step, pause, signal, source, resolver, approvedBy }) {
|
|
13
13
|
if (!existsSync(join(projectPath || process.cwd(), '.morph'))) return;
|
|
14
14
|
const event = { event: GATE_EVENT, feature, gate, step };
|
|
15
|
-
if (step === 'decision') Object.assign(event, { pause: pause === true, signal: signal ?? null, source: source ?? null });
|
|
15
|
+
if (step === 'decision') Object.assign(event, { pause: pause === true, signal: signal ?? null, source: source ?? null, ...(pause === true ? { resolver: resolver ?? 'human' } : {}) });
|
|
16
16
|
else event.approvedBy = approvedBy ?? null;
|
|
17
17
|
appendTelemetryEvent(event, projectPath);
|
|
18
18
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync,
|
|
9
9
|
} from 'fs';
|
|
10
10
|
import { dirname, join, resolve } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
11
12
|
import { fileURLToPath } from 'url';
|
|
12
13
|
import { readPrimaryHead, resolveGitCommonDir, workTreeTop } from './git-paths.js';
|
|
13
14
|
import { findFirstOnPath, quoteForCmd } from './morph-cli.js';
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
|
|
21
22
|
export { primaryTree };
|
|
22
23
|
|
|
24
|
+
export const GRAPHIFY_PACKAGE = 'graphifyy';
|
|
25
|
+
|
|
23
26
|
// Ficam fora de propósito: `.graphify_root` guarda o caminho absoluto da raiz, `cache/`
|
|
24
27
|
// é indexado pelo mtime da origem, e excluir `.graphifyignore` esconderia o da raiz.
|
|
25
28
|
const SEED_FILES = ['manifest.json', '.graphify_analysis.json', '.graphify_labels.json', 'graph.json'];
|
|
@@ -131,12 +134,26 @@ function appendRefreshLog(dir, entry) {
|
|
|
131
134
|
} catch { /* log é diagnóstico, nunca bloqueia */ }
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// The tool venv's own executable, for when the shim on PATH is missing: `uv tool install` writes the shim
|
|
138
|
+
// under $HOME, so an install run with another HOME leaves the venv in place and the shim elsewhere.
|
|
139
|
+
function venvGraphify(env, platform) {
|
|
140
|
+
const win = platform === 'win32';
|
|
141
|
+
const home = (win ? env.USERPROFILE : env.HOME) || homedir();
|
|
142
|
+
const venvs = [
|
|
143
|
+
join(env.UV_TOOL_DIR || (win ? join(env.APPDATA || join(home, 'AppData', 'Roaming'), 'uv', 'tools')
|
|
144
|
+
: join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'uv', 'tools')), GRAPHIFY_PACKAGE),
|
|
145
|
+
join(env.PIPX_HOME || (win ? join(home, 'pipx') : join(home, '.local', 'pipx')), 'venvs', GRAPHIFY_PACKAGE),
|
|
146
|
+
];
|
|
147
|
+
const file = win ? join('Scripts', 'graphify.exe') : join('bin', 'graphify');
|
|
148
|
+
return venvs.map((v) => join(v, file)).find((f) => existsSync(f)) ?? null;
|
|
149
|
+
}
|
|
150
|
+
|
|
134
151
|
// No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat` passa pelo shell.
|
|
135
|
-
export function resolveGraphify(env = process.env) {
|
|
136
|
-
const names =
|
|
152
|
+
export function resolveGraphify(env = process.env, platform = process.platform) {
|
|
153
|
+
const names = platform === 'win32'
|
|
137
154
|
? ['graphify.exe', 'graphify.cmd', 'graphify.bat']
|
|
138
155
|
: ['graphify'];
|
|
139
|
-
const file = findFirstOnPath(names, env);
|
|
156
|
+
const file = findFirstOnPath(names, env) ?? venvGraphify(env, platform);
|
|
140
157
|
return file ? { file, shell: /\.(cmd|bat)$/i.test(file) } : null;
|
|
141
158
|
}
|
|
142
159
|
|
|
@@ -45,6 +45,8 @@ function skipWrappers(tokens) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// The package managers' "run in that directory" flags: `npm --prefix <dir> test` is still `npm test`.
|
|
49
|
+
const PM_DIR_FLAGS = new Set(['--prefix', '--cwd', '-C', '--dir']);
|
|
48
50
|
const DOCKER_VALUE_FLAGS = new Set(['-f', '--file', '-p', '--project-name', '--profile', '--env-file', '--project-directory', '-H', '--host', '--context']);
|
|
49
51
|
|
|
50
52
|
function firstArg(args, valueFlags = new Set()) {
|
|
@@ -59,7 +61,8 @@ function kindOfTokens(tokens) {
|
|
|
59
61
|
const [head, ...args] = tokens;
|
|
60
62
|
if (!head || args.some((a) => INFO_FLAGS.has(a))) return null;
|
|
61
63
|
const exe = executable(head);
|
|
62
|
-
const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS
|
|
64
|
+
const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS
|
|
65
|
+
: PACKAGE_MANAGERS.has(exe) ? PM_DIR_FLAGS : undefined);
|
|
63
66
|
|
|
64
67
|
if (exe === 'dotnet') return DOTNET_HEAVY.has(sub) ? `dotnet ${sub}` : null;
|
|
65
68
|
if (exe === 'msbuild') return 'msbuild';
|
|
@@ -148,6 +151,34 @@ function testKindOfTokens(tokens) {
|
|
|
148
151
|
return kind && TEST_LABEL.test(kind) ? kind : null;
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
const DOTNET_FILTERS = /^(--filter|--tests|--treenode-filter|\/testcasefilter:)/i;
|
|
155
|
+
const NAME_FILTERS = /^(-t|-g|--grep|--testNamePattern|--test-name-pattern|--testPathPattern|--filter)(=|$)/;
|
|
156
|
+
// A bare word may be a flag's value (`--reporter dot`); only a path-shaped argument selects files.
|
|
157
|
+
const TEST_PATH = /[\\/]|\.(spec|test)\b|\.[cm]?[jt]sx?$/;
|
|
158
|
+
const PATH_VALUE_FLAGS = new Set(['-c', '--config', '--project', '--root', '--workspace', '--outputFile', '--reporter', ...PM_DIR_FLAGS]);
|
|
159
|
+
|
|
160
|
+
function isFilteredRun(kind, tokens) {
|
|
161
|
+
const runnerAt = executable(tokens[0]) === 'node'
|
|
162
|
+
? tokens.findIndex((t, i) => i > 0 && executable(t).replace(/\.[cm]?js$/, '') === kind)
|
|
163
|
+
: 0;
|
|
164
|
+
const args = tokens.slice(runnerAt + 1);
|
|
165
|
+
if (kind.startsWith('dotnet')) return args.some((a) => DOTNET_FILTERS.test(a));
|
|
166
|
+
for (let i = 0; i < args.length; i++) {
|
|
167
|
+
if (PATH_VALUE_FLAGS.has(args[i])) { i++; continue; }
|
|
168
|
+
if (NAME_FILTERS.test(args[i]) || (!args[i].startsWith('-') && TEST_PATH.test(args[i]))) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `{ kind, filtered }` for the first test run in `command`, `null` when there is none. */
|
|
174
|
+
export function testRunCall(command) {
|
|
175
|
+
for (const tokens of commandTokens(command)) {
|
|
176
|
+
const kind = testKindOfTokens(tokens);
|
|
177
|
+
if (kind) return { kind, filtered: isFilteredRun(kind, tokens) };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
export function testRunKind(command) {
|
|
152
183
|
for (const tokens of commandTokens(command)) {
|
|
153
184
|
const kind = testKindOfTokens(tokens);
|
|
@@ -4,26 +4,71 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { derivePhaseForFeature } from './state-reader.js';
|
|
7
|
-
import {
|
|
7
|
+
import { implementProgress } from './report-progress.js';
|
|
8
8
|
|
|
9
9
|
export const FIX_ATTEMPTS = 3;
|
|
10
10
|
|
|
11
11
|
const step = (action, command, reason, extra = {}) => ({ action, command, reason, ...extra });
|
|
12
12
|
|
|
13
|
+
const HOW = {
|
|
14
|
+
plan: (f) => [
|
|
15
|
+
`Rode \`/morph-plan ${f}\`: escreva 2-plan/plan.md e rode \`morph-spec plan-check ${f}\` até zero erro.`,
|
|
16
|
+
],
|
|
17
|
+
gate: (f) => [
|
|
18
|
+
`Rode \`morph-spec gate-check ${f} plan\`; se pausar, AskUserQuestion "Gate do plano — Plano de ${f} aprovado?".`,
|
|
19
|
+
],
|
|
20
|
+
design: (f) => [
|
|
21
|
+
`Rode \`/morph-uiux ${f}\` (opções em 1-design/opcoes/, protótipo e prints); depois \`morph-spec gate-check ${f} uiux\`.`,
|
|
22
|
+
`Se pausar, AskUserQuestion "Gate uiux — Design de ${f} aprovado?".`,
|
|
23
|
+
],
|
|
24
|
+
implement: (f) => [
|
|
25
|
+
'Implemente os Cn pendentes em sequência; ao fechar cada um, marque `feito` + evidência (teste ou arquivo:linha) '
|
|
26
|
+
+ `em 3-implement/report.md e commite \`wip(${f}): Cn …\`.`,
|
|
27
|
+
],
|
|
28
|
+
decide: () => [
|
|
29
|
+
'Leia a pergunta do Cn bloqueado em 3-implement/report.md ## Notas; decida pelo plano/ADR ou faça AskUserQuestion com ela. Se o `Resultado buscado` ficou falso, o plano não decide: pergunte.',
|
|
30
|
+
'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando; resposta que muda escopo vai por `unapprove`.',
|
|
31
|
+
],
|
|
32
|
+
verify: (f) => [
|
|
33
|
+
`\`morph-spec verify ${f} --json\` sem filtro; passou de 10 min, run_in_background.`,
|
|
34
|
+
],
|
|
35
|
+
fix: (f) => [
|
|
36
|
+
`Leia o \`feedback\` do último verify, corrija a causa e rode \`morph-spec verify ${f} --json\` de novo, sem filtro.`,
|
|
37
|
+
],
|
|
38
|
+
escalate: () => [
|
|
39
|
+
'AskUserQuestion com o texto do reason (aceitar/continuar/parar) e siga a resposta; não rode outro verify antes dela.',
|
|
40
|
+
],
|
|
41
|
+
review: () => [
|
|
42
|
+
'Siga .claude/skills/morph-apply/references/review.md.',
|
|
43
|
+
],
|
|
44
|
+
finish: (f) => [
|
|
45
|
+
`AskUserQuestion header "Integração" (PR ou merge); depois \`morph-spec finish ${f} --pr\` ou \`--merge\`.`,
|
|
46
|
+
],
|
|
47
|
+
status: (f) => [
|
|
48
|
+
`Rode \`morph-spec status ${f}\` e confira a fase em .morph/features/${f}/feature.json.`,
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
13
52
|
/**
|
|
14
|
-
* @param {{ feature: string, phase: string,
|
|
53
|
+
* @param {{ feature: string, phase: string, progress: object|null, gates: object,
|
|
15
54
|
* verification: { status?: string }|null, verificationStale?: boolean, failStreak?: number }} s
|
|
16
55
|
* `verificationStale` undefined = unknown (the hook has no git): read as fresh.
|
|
17
56
|
*/
|
|
18
57
|
export function decideNext(s) {
|
|
58
|
+
const n = decide(s);
|
|
59
|
+
const how = (HOW[n.action] ?? HOW.status)(s.feature);
|
|
60
|
+
const hotfix = s.workType === 'hotfix' && n.command.startsWith('/morph-hotfix');
|
|
61
|
+
return { ...n, how: hotfix ? ['Fluxo de hotfix: siga .claude/skills/morph-apply/references/hotfix.md.', ...how] : how };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function decide(s) {
|
|
19
65
|
const f = s.feature;
|
|
20
66
|
const approved = (g) => s.gates?.[g]?.approved === true;
|
|
21
|
-
// The hotfix pipeline (scout, human approval, ship) lives in its own skill, end to end.
|
|
22
67
|
const skill = (name) => (s.workType === 'hotfix' ? `/morph-hotfix ${f}` : `/${name} ${f}`);
|
|
23
68
|
|
|
24
69
|
switch (s.phase) {
|
|
25
70
|
case 'plan':
|
|
26
|
-
return s.
|
|
71
|
+
return s.progress
|
|
27
72
|
? step('gate', `morph-spec gate-check ${f} plan`, 'plano extraído: o gate do plano decide a pausa')
|
|
28
73
|
: step('plan', `/morph-plan ${f}`, 'sem checks.json: escreva o plan.md e rode plan-check até zero erro');
|
|
29
74
|
|
|
@@ -31,26 +76,20 @@ export function decideNext(s) {
|
|
|
31
76
|
return step('design', `morph-spec gate-check ${f} uiux`, 'complete 1-design/ (morph-uiux) e passe o gate de uiux');
|
|
32
77
|
|
|
33
78
|
case 'implement': {
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return step('blocked', `morph-spec report-check ${f}`, 'fatias pendentes sem dependência satisfeita');
|
|
45
|
-
}
|
|
46
|
-
const partial = ready.filter((sl) => sl.status === 'parcial').map((sl) => sl.id);
|
|
47
|
-
return step('dispatch', skill('morph-apply'),
|
|
48
|
-
partial.length ? `retome ${partial.join(', ')} (PARCIAL) e despache as prontas` : 'despache as fatias prontas',
|
|
49
|
-
{ slices: ready.map((sl) => sl.id) });
|
|
79
|
+
const p = s.progress;
|
|
80
|
+
if (!p) return step('plan', skill('morph-plan'), 'sem checks.json: plan.md curto e plan-check até zero erro');
|
|
81
|
+
const blocked = p.checks.filter((c) => c.status === 'bloqueado').map((c) => c.id);
|
|
82
|
+
if (blocked.length) {
|
|
83
|
+
return step('decide', 'AskUserQuestion (decisão do cenário)',
|
|
84
|
+
'Cn bloqueado no report.md: decida pelo plano/ADR ou pergunte, e siga implementando', { checks: blocked });
|
|
85
|
+
}
|
|
86
|
+
const pending = p.checks.filter((c) => c.status === 'pendente').map((c) => c.id);
|
|
87
|
+
if (pending.length) {
|
|
88
|
+
return step('implement', skill('morph-apply'), 'implemente nesta sessão e marque cada Cn feito no report.md', { checks: pending });
|
|
50
89
|
}
|
|
51
90
|
const v = s.verification;
|
|
52
91
|
if (!v?.status || s.verificationStale === true) {
|
|
53
|
-
return step('verify', `morph-spec verify ${f}`, v?.status ? 'a árvore andou desde o último verify' : '
|
|
92
|
+
return step('verify', `morph-spec verify ${f}`, v?.status ? 'a árvore andou desde o último verify' : 'todo Cn feito, sem verify');
|
|
54
93
|
}
|
|
55
94
|
if (v.status === 'fail') {
|
|
56
95
|
const streak = s.failStreak ?? 0;
|
|
@@ -77,8 +116,8 @@ export function decideNext(s) {
|
|
|
77
116
|
}
|
|
78
117
|
|
|
79
118
|
export function formatNext(n) {
|
|
80
|
-
const
|
|
81
|
-
return `próximo: ${n.action}${
|
|
119
|
+
const ids = n.checks?.length ? ` [${n.checks.join(', ')}]` : '';
|
|
120
|
+
return `próximo: ${n.action}${ids} → ${n.command} — ${n.reason}`;
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
/** Facts readable from `hooks/shared`; the CLI adds `verificationStale` (git) on top. Null without feature.json. */
|
|
@@ -99,7 +138,7 @@ export function collectNextSignals(featureName, cwd) {
|
|
|
99
138
|
workType: feature.workType ?? 'feature',
|
|
100
139
|
worktreePath: feature.worktree?.path ?? null,
|
|
101
140
|
phase,
|
|
102
|
-
|
|
141
|
+
progress: implementProgress(featureDir),
|
|
103
142
|
gates: feature.approvals || feature.approvalGates || {},
|
|
104
143
|
verification: feature.verification ?? null,
|
|
105
144
|
failStreak: feature.verifications?.feature?.failStreak ?? 0,
|
|
@@ -12,10 +12,10 @@ export const PHASE_DIRS = {
|
|
|
12
12
|
export const OUTPUT_PHASE_MAP = {
|
|
13
13
|
plan: 'plan',
|
|
14
14
|
checks: 'plan',
|
|
15
|
-
|
|
15
|
+
planCritique: 'plan',
|
|
16
|
+
planView: 'plan',
|
|
16
17
|
uiMockups: 'uiux',
|
|
17
|
-
|
|
18
|
-
uiFlows: 'uiux',
|
|
18
|
+
implementReport: 'implement',
|
|
19
19
|
evidencias: 'implement',
|
|
20
20
|
verifierReport: 'review',
|
|
21
21
|
reviewerReport: 'review',
|
|
@@ -25,10 +25,10 @@ export const OUTPUT_PHASE_MAP = {
|
|
|
25
25
|
export const FILENAME_TO_OUTPUT = {
|
|
26
26
|
'plan.md': 'plan',
|
|
27
27
|
'checks.json': 'checks',
|
|
28
|
-
'
|
|
28
|
+
'critica.md': 'planCritique',
|
|
29
|
+
'plan.html': 'planView',
|
|
29
30
|
'mockups.md': 'uiMockups',
|
|
30
|
-
'
|
|
31
|
-
'flows.md': 'uiFlows',
|
|
31
|
+
'report.md': 'implementReport',
|
|
32
32
|
'verifier.md': 'verifierReport',
|
|
33
33
|
'reviewer.md': 'reviewerReport',
|
|
34
34
|
'security.md': 'securityReport',
|
|
@@ -37,10 +37,7 @@ export const FILENAME_TO_OUTPUT = {
|
|
|
37
37
|
export const PROTECTED_SPEC_FILES = {
|
|
38
38
|
'plan.md': 'plan',
|
|
39
39
|
'checks.json': 'plan',
|
|
40
|
-
'design-system.md': 'uiux',
|
|
41
40
|
'mockups.md': 'uiux',
|
|
42
|
-
'components.md': 'uiux',
|
|
43
|
-
'flows.md': 'uiux',
|
|
44
41
|
};
|
|
45
42
|
|
|
46
43
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
|
|
5
|
+
export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
6
|
+
|
|
7
|
+
export const STATUSES = ['pendente', 'feito', 'bloqueado'];
|
|
8
|
+
|
|
9
|
+
export function checksPath(featureDir) {
|
|
10
|
+
return join(featureDir, '2-plan', 'checks.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function reportPath(featureDir) {
|
|
14
|
+
return join(featureDir, '3-implement', 'report.md');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ROW = /^\|\s*(C\d+)\s*\|\s*([^|]*?)\s*\|\s*(.*?)\s*\|?\s*$/;
|
|
18
|
+
|
|
19
|
+
/** Status per Cn from the report's table; an unknown status reads as `pendente`. */
|
|
20
|
+
export function parseReport(text) {
|
|
21
|
+
const rows = new Map();
|
|
22
|
+
for (const line of String(text ?? '').split(/\r?\n/)) {
|
|
23
|
+
const m = ROW.exec(line);
|
|
24
|
+
if (!m) continue;
|
|
25
|
+
const status = m[2].toLowerCase();
|
|
26
|
+
rows.set(m[1], { status: STATUSES.includes(status) ? status : 'pendente', evidence: m[3] });
|
|
27
|
+
}
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Features started before 10.0 have per-slice reports and no report.md: a Cn counts as done when a
|
|
32
|
+
// slice that covers it has a final report, so an in-flight feature keeps moving after the upgrade.
|
|
33
|
+
function legacyRows(checks, readSliceReport) {
|
|
34
|
+
const rows = new Map();
|
|
35
|
+
for (const slice of Array.isArray(checks.slices) ? checks.slices : []) {
|
|
36
|
+
const first = String(readSliceReport(slice.id) ?? '').split(/\r?\n/).find((l) => l.trim() !== '');
|
|
37
|
+
if (first === undefined) continue;
|
|
38
|
+
const status = /^[\s#>*_]*BLOQUEADO\b/.test(first) ? 'bloqueado' : PARTIAL_MARKER.test(first) ? null : 'feito';
|
|
39
|
+
if (!status) continue;
|
|
40
|
+
for (const id of slice.covers ?? []) {
|
|
41
|
+
if (rows.get(id)?.status !== 'bloqueado') rows.set(id, { status, evidence: `${slice.id}.md` });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @returns {{ total, done, blocked, pending, checks: Array<{id, status, evidence}>, legacy: boolean } | null} */
|
|
48
|
+
export function implementProgressFrom(checksText, reportText, readSliceReport = () => null) {
|
|
49
|
+
let checks;
|
|
50
|
+
try {
|
|
51
|
+
checks = JSON.parse(checksText ?? '');
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(checks?.checks)) return null;
|
|
56
|
+
const legacy = reportText == null && Array.isArray(checks.slices);
|
|
57
|
+
const rows = legacy ? legacyRows(checks, readSliceReport) : parseReport(reportText);
|
|
58
|
+
const out = checks.checks
|
|
59
|
+
.filter((c) => c && typeof c.id === 'string')
|
|
60
|
+
.map((c) => ({ id: c.id, status: rows.get(c.id)?.status ?? 'pendente', evidence: rows.get(c.id)?.evidence ?? '' }));
|
|
61
|
+
const count = (status) => out.filter((c) => c.status === status).length;
|
|
62
|
+
return { total: out.length, done: count('feito'), blocked: count('bloqueado'), pending: count('pendente'), checks: out, legacy };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readOrNull(path) {
|
|
66
|
+
try {
|
|
67
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function implementProgress(featureDir) {
|
|
74
|
+
const sliceDir = join(featureDir, '3-implement', 'reports');
|
|
75
|
+
return implementProgressFrom(
|
|
76
|
+
readOrNull(checksPath(featureDir)),
|
|
77
|
+
readOrNull(reportPath(featureDir)),
|
|
78
|
+
(id) => (existsSync(sliceDir) ? readOrNull(join(sliceDir, `${id}.md`)) : null),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The report template body for a feature's checks: every Cn starts `pendente`. */
|
|
83
|
+
export function reportSkeleton(featureName, checkIds) {
|
|
84
|
+
const rows = checkIds.map((id) => `| ${id} | pendente | |`).join('\n');
|
|
85
|
+
return `# ${featureName}\n\n| Cn | status | evidência |\n|----|--------|-----------|\n${rows}\n\n## Notas\n\n`;
|
|
86
|
+
}
|
|
@@ -131,8 +131,9 @@ export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
|
|
|
131
131
|
// not advance the phase.
|
|
132
132
|
export const PHASE_ARTIFACTS = [
|
|
133
133
|
['4-review', 'review', 'verifier.md'],
|
|
134
|
+
['3-implement', 'implement', 'report.md'],
|
|
134
135
|
['3-implement', 'implement', 'reports/'],
|
|
135
|
-
['1-design', 'uiux', '
|
|
136
|
+
['1-design', 'uiux', 'mockups.md'],
|
|
136
137
|
['2-plan', 'plan', 'plan.md'],
|
|
137
138
|
];
|
|
138
139
|
|
|
@@ -180,7 +181,7 @@ export function derivePhaseFrom(feature, hasArtifact) {
|
|
|
180
181
|
|
|
181
182
|
// An approved gate advances the phase before the next artifact exists, or
|
|
182
183
|
// that artifact could never be written.
|
|
183
|
-
const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/
|
|
184
|
+
const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/mockups.md`);
|
|
184
185
|
const implied = gateImpliedPhaseFrom(feature, designStarted);
|
|
185
186
|
return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
|
|
186
187
|
}
|