@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
|
@@ -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);
|
|
@@ -51,8 +51,14 @@ function liveHoldersPruningStale(dir, ctx) {
|
|
|
51
51
|
|
|
52
52
|
const kindSuffix = (kind) => (kind ? ` (${kind})` : '');
|
|
53
53
|
|
|
54
|
+
// Two full verify suites never run together; a sub-agent filter or a build only takes a slot.
|
|
55
|
+
export const isVerifySuite = ({ label, node }) => label === 'verify' && node === 'tests';
|
|
56
|
+
|
|
57
|
+
const runningSuite = (holders, ownPath = null) => holders.find((h) => h.path !== ownPath && isVerifySuite(h));
|
|
58
|
+
|
|
54
59
|
function blockedText(blocked, slots, now) {
|
|
55
60
|
const holders = blocked.holders.map((h) => describeHolder(h, now)).join('; ');
|
|
61
|
+
if (blocked.reason === 'suite') return `aguardando o verify de outra feature terminar a suíte de testes (${describeHolder(blocked.holders[0], now)})`;
|
|
56
62
|
if (blocked.reason === 'exclusive') return `aguardando limpeza do servidor de build (${describeHolder(blocked.exclusive, now)})`;
|
|
57
63
|
if (blocked.reason === 'memory') {
|
|
58
64
|
const cause = blocked.holders.length
|
|
@@ -65,13 +71,16 @@ function blockedText(blocked, slots, now) {
|
|
|
65
71
|
|
|
66
72
|
function tryClaim(dir, opts, ctx) {
|
|
67
73
|
const {
|
|
68
|
-
slots, label, feature, node, cwd, minFreeMemGb, freeMem, kind, lease: leaseSpec,
|
|
74
|
+
slots, label, feature, node, cwd, minFreeMemGb, freeMem, kind, suite, lease: leaseSpec,
|
|
69
75
|
} = opts;
|
|
70
76
|
const guarded = withClaimMutex(dir, () => {
|
|
71
77
|
const holders = liveHoldersPruningStale(dir, ctx);
|
|
72
78
|
const exclusive = readExclusive(dir, ctx);
|
|
73
79
|
if (exclusive) return { blocked: { reason: 'exclusive', holders, exclusive } };
|
|
80
|
+
// A full queue is the deeper reason: with no free slot, waiting out the other suite is not enough.
|
|
74
81
|
if (holders.length >= slots) return { blocked: { reason: 'slots', holders } };
|
|
82
|
+
const running = suite ? runningSuite(holders) : null;
|
|
83
|
+
if (running) return { blocked: { reason: 'suite', holders: [running] } };
|
|
75
84
|
const freeGb = freeMem() / GB;
|
|
76
85
|
if (minFreeMemGb > 0 && freeGb < minFreeMemGb) return { blocked: { reason: 'memory', holders, freeGb, minFreeMemGb, kind } };
|
|
77
86
|
|
|
@@ -116,10 +125,18 @@ export function acquireHeavy(opts) {
|
|
|
116
125
|
return { status: 'acquired', lease: null, waitedMs: 0, reentrant: true };
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
return pollClaim(() => tryClaim(dir, claimOpts, ctx), {
|
|
129
|
+
...opts, now, sleep, log, minFreeMemGb: claimOpts.minFreeMemGb,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pollClaim(claim, {
|
|
134
|
+
slots, waitTimeoutMs, minFreeMemGb = 0, kind = null, now, sleep, log,
|
|
135
|
+
}) {
|
|
119
136
|
const start = now();
|
|
120
137
|
let lastReport = -Infinity;
|
|
121
138
|
for (;;) {
|
|
122
|
-
const attempt =
|
|
139
|
+
const attempt = claim();
|
|
123
140
|
if (attempt.lease) return { status: 'acquired', lease: attempt.lease, waitedMs: now() - start };
|
|
124
141
|
|
|
125
142
|
const waited = now() - start;
|
|
@@ -130,8 +147,8 @@ export function acquireHeavy(opts) {
|
|
|
130
147
|
reason: blocked.reason,
|
|
131
148
|
holders: blocked.holders,
|
|
132
149
|
freeGb: blocked.freeGb ?? null,
|
|
133
|
-
minFreeMemGb
|
|
134
|
-
kind
|
|
150
|
+
minFreeMemGb,
|
|
151
|
+
kind,
|
|
135
152
|
waitedMs: waited,
|
|
136
153
|
};
|
|
137
154
|
}
|
|
@@ -143,6 +160,25 @@ export function acquireHeavy(opts) {
|
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
162
|
|
|
163
|
+
// Keeps the slot held while it waits: releasing it would let a build or filter take the place.
|
|
164
|
+
export function enterVerifySuite(opts) {
|
|
165
|
+
const {
|
|
166
|
+
dir, lease, maxHoldMs,
|
|
167
|
+
now = Date.now, sleep = sleepSync, isAlive = pidAlive, log = (m) => process.stderr.write(`${m}\n`),
|
|
168
|
+
} = opts;
|
|
169
|
+
const ctx = { now, isAlive, maxHoldMs };
|
|
170
|
+
const claim = () => {
|
|
171
|
+
const guarded = withClaimMutex(dir, () => {
|
|
172
|
+
const running = runningSuite(scanHolders(dir, ctx).live, lease.path);
|
|
173
|
+
if (running) return { blocked: { reason: 'suite', holders: [running] } };
|
|
174
|
+
updateLease(lease, { node: 'tests' });
|
|
175
|
+
return { lease };
|
|
176
|
+
}, ctx);
|
|
177
|
+
return guarded ? guarded.value : { blocked: { reason: 'contended', holders: [] } };
|
|
178
|
+
};
|
|
179
|
+
return pollClaim(claim, { ...opts, now, sleep, log });
|
|
180
|
+
}
|
|
181
|
+
|
|
146
182
|
// For a holder whose next node needs more RAM than it was admitted with: it keeps its slot and
|
|
147
183
|
// waits only on memory, since re-claiming would hit the re-entrant path and skip the check.
|
|
148
184
|
export function awaitFreeMemory(opts) {
|
|
@@ -206,6 +242,7 @@ export function describeBusy(result, config) {
|
|
|
206
242
|
const key = result.kind ? `heavy.minFreeMemGbByKind.${result.kind}` : 'heavy.minFreeMemGb';
|
|
207
243
|
return `sem RAM livre há ${waited}s (${result.freeGb?.toFixed(1)} GB < ${floor} GB mínimos${kindSuffix(result.kind)}, ${key})`;
|
|
208
244
|
}
|
|
245
|
+
if (result.reason === 'suite') return `o verify de outra feature segue na suíte de testes após ${waited}s (heavy.waitTimeoutSec)`;
|
|
209
246
|
if (result.reason === 'exclusive') return `limpeza do servidor de build ainda em curso após ${waited}s`;
|
|
210
247
|
return `${result.holders.length}/${config.slots} slots pesados ocupados por ${waited}s (heavy.waitTimeoutSec)`;
|
|
211
248
|
}
|
|
@@ -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.',
|
|
30
|
+
'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando.',
|
|
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
|
}
|