@polymorphism-tech/morph-spec 7.9.4 → 8.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 +2 -2
- package/README.md +31 -2
- package/bin/morph-spec.js +50 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +38 -1
- package/docs/QUICKSTART.md +60 -1
- package/framework/CLAUDE.md +13 -2
- package/framework/MORPH.md +38 -2
- package/framework/agents.json +22 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-hotfix.md +39 -0
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/hooks/README.md +25 -10
- package/framework/hooks/claude-code/core/gate-guard.js +4 -2
- package/framework/hooks/claude-code/core/loop-detect.js +2 -1
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
- package/framework/hooks/claude-code/core/validate-completion.js +1 -1
- package/framework/hooks/claude-code/core/worktree-context.js +160 -0
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/build-check.js +2 -2
- package/framework/hooks/shared/hook-response.js +17 -5
- package/framework/hooks/shared/payload-utils.js +42 -0
- package/framework/hooks/shared/state-reader.js +22 -2
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -86
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/schemas/feature.schema.json +95 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +39 -8
- package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
- package/framework/skills/core/morph-eval/SKILL.md +6 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
- package/framework/skills/core/morph-implement/SKILL.md +8 -4
- package/framework/skills/core/morph-plan/SKILL.md +5 -1
- package/framework/skills/core/morph-proposal/SKILL.md +24 -1
- package/framework/skills/core/morph-review/SKILL.md +3 -2
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- 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 +0 -62
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- 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/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- 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/README.md +56 -56
- 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/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- 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/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +4 -2
- package/src/commands/project/init.js +46 -2
- package/src/commands/project/update.js +5 -3
- package/src/commands/project/worktree.js +1005 -46
- package/src/commands/state/advance.js +17 -2
- package/src/commands/state/approve.js +18 -1
- package/src/commands/state/create.js +158 -14
- package/src/commands/state/retype.js +126 -0
- package/src/commands/state/score.js +4 -0
- package/src/commands/tasks/dag.js +32 -0
- package/src/commands/validation/index.js +1 -0
- package/src/commands/validation/validate-feature.js +33 -5
- package/src/commands/validation/verify.js +190 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +35 -10
- package/src/core/templates/template-renderer.js +477 -477
- package/src/core/work-types.js +79 -0
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +11 -1
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/scanner/state-scanner.js +42 -0
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/installers/memory-installer.js +1 -1
- package/src/lib/risk-detector.js +16 -0
- package/src/lib/tasks/dispatch-planner.js +122 -4
- package/src/lib/tasks/mermaid-renderer.js +132 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +27 -6
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/lib/validators/verify-runner.js +322 -0
- package/src/lib/work-type-detector.js +143 -0
- package/src/scripts/setup-infra.js +32 -5
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +10 -6
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +43 -0
- package/src/utils/hooks-installer.js +41 -11
- package/src/utils/skills-installer.js +174 -174
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
- package/src/lib/tasks/wave-resolver.js +0 -353
|
@@ -55,9 +55,24 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
55
55
|
|
|
56
56
|
// 1) Approve the gate that completes the current phase.
|
|
57
57
|
const gate = PHASE_GATE[phase];
|
|
58
|
+
const approvedBy = resolveApprovedBy(options);
|
|
59
|
+
|
|
60
|
+
// Hotfix backstop (defensive, mirrors approve.js): the phase==='review'
|
|
61
|
+
// early-return above already stops advance from ever reaching a
|
|
62
|
+
// gate==='review' approval, but guard the approval itself too so this
|
|
63
|
+
// stays true even if the early-return's shape changes later. Gate on the
|
|
64
|
+
// RESOLVED approvedBy — headless contexts have no TTY and resolve to
|
|
65
|
+
// "auto" regardless of --mode, so checking the raw --mode string alone
|
|
66
|
+
// would let them slip through (same bug the approve.js backstop had).
|
|
67
|
+
if (gate === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
|
|
68
|
+
console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
|
|
69
|
+
console.error(chalk.gray(' Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) ao review-report via AskUserQuestion.\n'));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
if (gate) {
|
|
59
74
|
await setApprovalGate(featureName, gate, true, {
|
|
60
|
-
approvedBy
|
|
75
|
+
approvedBy,
|
|
61
76
|
approvedAt: new Date().toISOString(),
|
|
62
77
|
notes: options.notes,
|
|
63
78
|
});
|
|
@@ -72,7 +87,7 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
console.log(chalk.green(`\n✅ Advanced "${featureName}" past the ${phase} phase.`));
|
|
75
|
-
if (gate) console.log(chalk.gray(` Gate "${gate}" approved (${
|
|
90
|
+
if (gate) console.log(chalk.gray(` Gate "${gate}" approved (${approvedBy}).`));
|
|
76
91
|
if (nextDir) console.log(chalk.gray(` Scaffolded ${nextDir}/ — ready for the next phase's first artifact.\n`));
|
|
77
92
|
else console.log('');
|
|
78
93
|
} catch (error) {
|
|
@@ -99,7 +99,8 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
//
|
|
102
|
+
// Resolve who approved BEFORE the hotfix backstop — the backstop gates on
|
|
103
|
+
// the resolved signature, not the raw --mode string (see below).
|
|
103
104
|
//
|
|
104
105
|
// approvedBy resolution (most explicit wins):
|
|
105
106
|
// 1. --approver flag (CLI caller signalled who approved)
|
|
@@ -125,6 +126,22 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
125
126
|
approvedBy = 'auto';
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// Hotfix backstop: Gate 3 (review) requires a genuine HUMAN signature even
|
|
130
|
+
// in trust:auto — a work-type policy (forcedPauseGates in
|
|
131
|
+
// src/core/work-types.js, mirrored by risk-detector's
|
|
132
|
+
// FORCED_PAUSE_WORK_TYPES), enforced server-side rather than trusting the
|
|
133
|
+
// calling skill. It gates on the RESOLVED approvedBy, NOT the raw --mode
|
|
134
|
+
// string: every headless context (claude -p, CI, the Bash tool) runs
|
|
135
|
+
// without a TTY, so an omitted --mode — or any --mode ≠ 'auto' — still
|
|
136
|
+
// resolves to "auto" and would otherwise rubber-stamp the gate. A human
|
|
137
|
+
// approval is --approver <name> or an interactive TTY ($USER); anything
|
|
138
|
+
// resolving to "auto" is refused here.
|
|
139
|
+
if (gateName === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
|
|
140
|
+
console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
|
|
141
|
+
console.error(chalk.gray(' Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) ao review-report via AskUserQuestion.\n'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
128
145
|
await setApprovalGate(featureName, gateName, true, {
|
|
129
146
|
approvedBy,
|
|
130
147
|
approvedAt: new Date().toISOString(),
|
|
@@ -1,22 +1,85 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { mkdirSync } from 'fs';
|
|
2
|
+
import { mkdirSync, cpSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { createFeature } from '../../core/state/state-manager.js';
|
|
4
|
+
import { createFeature, setApprovalGate, getFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
import { WORK_TYPES, DEFAULT_WORK_TYPE, POLICY_APPROVER_PREFIX, isValidWorkType, getWorkType } from '../../core/work-types.js';
|
|
6
|
+
import { detectWorkType } from '../../lib/work-type-detector.js';
|
|
7
|
+
import { runWorktreeSetup, materializeState } from '../project/worktree.js';
|
|
5
8
|
|
|
6
9
|
/** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
|
|
7
10
|
const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
8
11
|
|
|
9
12
|
const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
|
|
13
|
+
const VALID_DETECT_SOURCES = ['deterministic', 'llm', 'user'];
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* Resolve the work type for a NEW feature from `--type`/`--request`, or fall
|
|
17
|
+
* back to the default. Never touches the filesystem/state — pure control
|
|
18
|
+
* flow so it can `process.exit` before any side effect happens.
|
|
19
|
+
*
|
|
20
|
+
* @param {{ type?: string, request?: string, detectSource?: string }} options
|
|
21
|
+
* @param {string} now - ISO timestamp, stamped into workTypeDetection.decidedAt
|
|
22
|
+
* @returns {{ workType: string, workTypeDetection?: object }}
|
|
23
|
+
*/
|
|
24
|
+
function resolveWorkType(options, now) {
|
|
25
|
+
if (options.type !== undefined) {
|
|
26
|
+
if (!isValidWorkType(options.type)) {
|
|
27
|
+
console.error(chalk.red(`\n❌ Invalid --type: "${options.type}"`));
|
|
28
|
+
console.error(chalk.gray(`Valid types: ${Object.keys(WORK_TYPES).join(', ')}\n`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (options.detectSource !== undefined && !VALID_DETECT_SOURCES.includes(options.detectSource)) {
|
|
32
|
+
console.error(chalk.red(`\n❌ Invalid --detect-source: "${options.detectSource}"`));
|
|
33
|
+
console.error(chalk.gray(`Valid sources: ${VALID_DETECT_SOURCES.join(', ')}\n`));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
workType: options.type,
|
|
38
|
+
workTypeDetection: {
|
|
39
|
+
source: options.detectSource || 'user',
|
|
40
|
+
confidence: 'high',
|
|
41
|
+
decidedAt: now,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.request !== undefined) {
|
|
47
|
+
const d = detectWorkType({ text: options.request });
|
|
48
|
+
if (d.confidence !== 'high') {
|
|
49
|
+
// Router contract: determinism first, ambiguity escalates to the
|
|
50
|
+
// LLM/HCOP caller (which re-runs `create --type <chosen>`). The
|
|
51
|
+
// feature must NOT be created on this path — always JSON, regardless
|
|
52
|
+
// of --json, since this is the machine-consumable half of the contract.
|
|
53
|
+
console.log(JSON.stringify({ needsDecision: true, candidates: d.candidates, matched: d.matched }));
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
workType: d.workType,
|
|
58
|
+
workTypeDetection: {
|
|
59
|
+
source: 'deterministic',
|
|
60
|
+
confidence: 'high',
|
|
61
|
+
matched: d.matched,
|
|
62
|
+
decidedAt: now,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { workType: DEFAULT_WORK_TYPE };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `morph-spec create <feature>` — explicit feature birth: classifies the
|
|
72
|
+
* work type (`--type`/`--request`), creates `.morph/features/{feature}/`
|
|
73
|
+
* scaffolded per the type's dimensioned pipeline, stamps policy-approved
|
|
74
|
+
* gates (`approvedBy: "policy:{type}"`) for the gates that type skips, and
|
|
75
|
+
* (with `--worktree`) births the feature already inside its own git
|
|
76
|
+
* worktree on branch `morph/{feature}`.
|
|
15
77
|
*
|
|
16
78
|
* Replaces the implicit birth via the state-sync hook on the first Write —
|
|
17
79
|
* the skills call this at proposal start so the authoritative state exists
|
|
18
80
|
* with the full canonical shape before any artifact lands. Idempotent: an
|
|
19
|
-
* existing feature is reported and left untouched
|
|
81
|
+
* existing feature is reported and left untouched — no re-stamping, no
|
|
82
|
+
* re-scaffolding, no worktree setup.
|
|
20
83
|
*/
|
|
21
84
|
export async function createCommand(featureName, options = {}) {
|
|
22
85
|
try {
|
|
@@ -32,24 +95,105 @@ export async function createCommand(featureName, options = {}) {
|
|
|
32
95
|
process.exit(1);
|
|
33
96
|
}
|
|
34
97
|
|
|
35
|
-
const
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const { workType, workTypeDetection } = resolveWorkType(options, now);
|
|
100
|
+
|
|
101
|
+
const { feature, created } = await createFeature(featureName, {
|
|
102
|
+
status: options.status,
|
|
103
|
+
workType,
|
|
104
|
+
workTypeDetection,
|
|
105
|
+
});
|
|
36
106
|
|
|
37
|
-
// The proposal folder is the first working directory of every feature.
|
|
38
107
|
const featureDir = join(process.cwd(), '.morph', 'features', featureName);
|
|
39
|
-
|
|
108
|
+
const wt = getWorkType(feature);
|
|
109
|
+
|
|
110
|
+
let worktreeResult = null;
|
|
111
|
+
|
|
112
|
+
if (created) {
|
|
113
|
+
// Scaffold the phase folders this work type starts with.
|
|
114
|
+
for (const folder of wt.scaffold) {
|
|
115
|
+
mkdirSync(join(featureDir, folder), { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Stamp the gates this type skips by policy — NEVER "auto" (the
|
|
119
|
+
// gate-guard hook only special-cases approvedBy === "auto"; policy:{type}
|
|
120
|
+
// keeps the audit trail honest while staying silent to the guard).
|
|
121
|
+
for (const gate of wt.skippedGates) {
|
|
122
|
+
await setApprovalGate(featureName, gate, true, {
|
|
123
|
+
approvedBy: `${POLICY_APPROVER_PREFIX}${workType}`,
|
|
124
|
+
approvedAt: now,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.worktree) {
|
|
129
|
+
worktreeResult = await runWorktreeSetup(featureName, {});
|
|
130
|
+
if (worktreeResult?.created && worktreeResult.path) {
|
|
131
|
+
try {
|
|
132
|
+
// feature.json in the root is NOT committed yet (create just
|
|
133
|
+
// wrote it), so the branch `runWorktreeSetup` checked out from
|
|
134
|
+
// HEAD does not have it — copy the freshly-scaffolded folder
|
|
135
|
+
// into the worktree and re-materialize its local state.json.
|
|
136
|
+
cpSync(featureDir, join(worktreeResult.path, '.morph', 'features', featureName), { recursive: true });
|
|
137
|
+
// The `.worktree` registration is ROOT-OWNED metadata (runWorktreeSetup
|
|
138
|
+
// stamped it onto the root's feature.json, which cpSync just copied in).
|
|
139
|
+
// Strip it from the worktree's branch-local copy so the branch commits a
|
|
140
|
+
// clean feature.json — otherwise it merges back into the root at
|
|
141
|
+
// `worktree finish` and the post-merge cleanup leaves the root dirty.
|
|
142
|
+
// verify.decidePersistence reads the registration from the ROOT's copy,
|
|
143
|
+
// never this branch-local one (see verify.js), so stripping is safe and
|
|
144
|
+
// restores the invariant that comment relies on.
|
|
145
|
+
try {
|
|
146
|
+
const wtFeatureJson = join(worktreeResult.path, '.morph', 'features', featureName, 'feature.json');
|
|
147
|
+
const fj = JSON.parse(readFileSync(wtFeatureJson, 'utf8'));
|
|
148
|
+
if (fj.worktree) {
|
|
149
|
+
delete fj.worktree;
|
|
150
|
+
writeFileSync(wtFeatureJson, JSON.stringify(fj, null, 2), 'utf8');
|
|
151
|
+
}
|
|
152
|
+
} catch { /* fail-open — root copy still carries the registration for verify */ }
|
|
153
|
+
// Re-materialize now that the feature.json exists in the worktree, and
|
|
154
|
+
// report THIS result — runWorktreeSetup's earlier materialize ran before
|
|
155
|
+
// the copy (no features yet), so its `materialized:false` was stale.
|
|
156
|
+
const remat = await materializeState(worktreeResult.path);
|
|
157
|
+
worktreeResult.materialized = remat.materialized;
|
|
158
|
+
} catch { /* fail-open — the feature already exists in root */ }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// `feature` is a snapshot from BEFORE the gate-stamping/worktree steps —
|
|
164
|
+
// setApprovalGate/runWorktreeSetup each do their own loadState()/saveState()
|
|
165
|
+
// round-trip on disk, so re-read the authoritative copy for reporting.
|
|
166
|
+
const finalFeature = created ? (getFeature(featureName) || feature) : feature;
|
|
40
167
|
|
|
41
168
|
if (options.json) {
|
|
42
|
-
|
|
169
|
+
const out = { feature: featureName, created, ...finalFeature };
|
|
170
|
+
if (worktreeResult) out.worktree = worktreeResult;
|
|
171
|
+
console.log(JSON.stringify(out, null, 2));
|
|
43
172
|
return;
|
|
44
173
|
}
|
|
45
174
|
|
|
46
175
|
if (created) {
|
|
47
|
-
console.log(chalk.green(`\n✅ Feature created: ${featureName}`));
|
|
48
|
-
console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)
|
|
49
|
-
console.log(chalk.gray(` status: ${
|
|
176
|
+
console.log(chalk.green(`\n✅ Feature created: ${featureName} (type: ${finalFeature.workType})`));
|
|
177
|
+
console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)`));
|
|
178
|
+
console.log(chalk.gray(` status: ${finalFeature.status} | createdAt: ${finalFeature.createdAt}`));
|
|
179
|
+
console.log(chalk.gray(` scaffolded: ${wt.scaffold.join(', ')}`));
|
|
180
|
+
if (wt.skippedGates.length) {
|
|
181
|
+
console.log(chalk.yellow(` gates skipped by policy:${finalFeature.workType}: ${wt.skippedGates.join(', ')}`));
|
|
182
|
+
}
|
|
183
|
+
console.log(chalk.cyan(` next: ${wt.entrySkill}\n`));
|
|
184
|
+
|
|
185
|
+
if (worktreeResult) {
|
|
186
|
+
if (worktreeResult.created) {
|
|
187
|
+
console.log(chalk.green(` worktree: ${worktreeResult.path} (branch ${worktreeResult.branch})`));
|
|
188
|
+
console.log(chalk.gray(` ${worktreeResult.nextStep}\n`));
|
|
189
|
+
} else {
|
|
190
|
+
const why = worktreeResult.error || (worktreeResult.alreadyExists ? 'already exists' : 'unknown');
|
|
191
|
+
console.log(chalk.yellow(` ⚠️ worktree setup skipped (${why}) — feature stays in the root worktree\n`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
50
194
|
} else {
|
|
51
195
|
console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already exists — left untouched`));
|
|
52
|
-
console.log(chalk.gray(` status: ${
|
|
196
|
+
console.log(chalk.gray(` status: ${finalFeature.status} | workType: ${finalFeature.workType || 'feature'} | createdAt: ${finalFeature.createdAt || 'unknown'}\n`));
|
|
53
197
|
}
|
|
54
198
|
} catch (error) {
|
|
55
199
|
console.error(chalk.red(`\n❌ Error creating feature: ${error.message}\n`));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getFeature, setApprovalGate, updateFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/work-types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `morph-spec retype <feature> <newType>` — mid-flight work-type change with
|
|
9
|
+
* asymmetric gate rules that preserve GENUINE human/auto approvals and only
|
|
10
|
+
* ever undo policy stamps:
|
|
11
|
+
*
|
|
12
|
+
* - Blocked outright once Gate 3 (review) is approved — the feature already
|
|
13
|
+
* shipped its pipeline, retyping it retroactively would be revisionist.
|
|
14
|
+
* - Downgrade (fewer skipped gates → more skipped gates, e.g. feature/bug
|
|
15
|
+
* → chore/hotfix) is blocked once Gate 2 (plan) carries a genuine
|
|
16
|
+
* (non-policy) approval — the plan was already accepted under the old
|
|
17
|
+
* type's scrutiny; retyping around it would erase that judgment call.
|
|
18
|
+
* - Upgrade (more skipped gates → fewer, e.g. chore/hotfix → feature/bug)
|
|
19
|
+
* is always allowed before review: it only ever ADDS scrutiny.
|
|
20
|
+
*
|
|
21
|
+
* Applying a retype resets exactly the gates that were stamped
|
|
22
|
+
* `policy:{oldType}` and the NEW type no longer skips (so the human is asked
|
|
23
|
+
* again), stamps `policy:{newType}` on any of the new type's skippedGates
|
|
24
|
+
* that aren't already approved (a prior genuine approval is left standing —
|
|
25
|
+
* that's the "preserves human approvals" half of the contract), scaffolds
|
|
26
|
+
* any phase folders the new type needs that don't exist yet, and appends an
|
|
27
|
+
* audit entry to `workTypeHistory`.
|
|
28
|
+
*/
|
|
29
|
+
export async function retypeCommand(featureName, newType, options = {}) {
|
|
30
|
+
try {
|
|
31
|
+
if (!isValidWorkType(newType)) {
|
|
32
|
+
console.error(chalk.red(`\n❌ Invalid work type: "${newType}"`));
|
|
33
|
+
console.error(chalk.gray(`Valid types: ${Object.keys(WORK_TYPES).join(', ')}\n`));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const feature = getFeature(featureName);
|
|
38
|
+
if (!feature) {
|
|
39
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const oldType = feature.workType || 'feature';
|
|
44
|
+
|
|
45
|
+
if (oldType === newType) {
|
|
46
|
+
console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already is type "${newType}" — no-op\n`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (feature.approvalGates?.review?.approved === true) {
|
|
51
|
+
console.error(chalk.red(`\n❌ Feature "${featureName}" já passou pelo Gate 3 (review); retype bloqueado\n`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const oldWorkType = WORK_TYPES[oldType] || WORK_TYPES.feature;
|
|
56
|
+
const newWorkType = WORK_TYPES[newType];
|
|
57
|
+
const isDowngrade = newWorkType.skippedGates.length > oldWorkType.skippedGates.length;
|
|
58
|
+
|
|
59
|
+
if (isDowngrade) {
|
|
60
|
+
const planGate = feature.approvalGates?.plan;
|
|
61
|
+
const genuinelyApproved = planGate?.approved === true
|
|
62
|
+
&& !String(planGate.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
|
|
63
|
+
if (genuinelyApproved) {
|
|
64
|
+
console.error(chalk.red(`\n❌ Gate 2 (plan) já aprovado por "${planGate.approvedBy}"; downgrade bloqueado — o plano já foi aceito\n`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
|
|
71
|
+
// Reset gates that were stamped policy:{oldType} and the new type no
|
|
72
|
+
// longer skips — a genuine (non-policy) approval is never touched.
|
|
73
|
+
const resetGates = [];
|
|
74
|
+
for (const [gate, data] of Object.entries(feature.approvalGates || {})) {
|
|
75
|
+
const isPolicyStamped = String(data?.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
|
|
76
|
+
if (isPolicyStamped && !newWorkType.skippedGates.includes(gate)) {
|
|
77
|
+
await setApprovalGate(featureName, gate, false, { approvedBy: null, approvedAt: null });
|
|
78
|
+
resetGates.push(gate);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Stamp the new type's skipped gates — but only the ones not already
|
|
83
|
+
// approved (preserves a prior genuine approval untouched).
|
|
84
|
+
const stampedGates = [];
|
|
85
|
+
for (const gate of newWorkType.skippedGates) {
|
|
86
|
+
if (!feature.approvalGates?.[gate]?.approved) {
|
|
87
|
+
await setApprovalGate(featureName, gate, true, {
|
|
88
|
+
approvedBy: `${POLICY_APPROVER_PREFIX}${newType}`,
|
|
89
|
+
approvedAt: now,
|
|
90
|
+
});
|
|
91
|
+
stampedGates.push(gate);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const featureDir = join(process.cwd(), '.morph', 'features', featureName);
|
|
96
|
+
for (const folder of newWorkType.scaffold) {
|
|
97
|
+
mkdirSync(join(featureDir, folder), { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await updateFeature(featureName, 'workType', newType);
|
|
101
|
+
await updateFeature(featureName, 'workTypeHistory', [
|
|
102
|
+
...(feature.workTypeHistory || []),
|
|
103
|
+
{ from: oldType, to: newType, reason: options.reason || null, at: now },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const finalFeature = getFeature(featureName);
|
|
107
|
+
|
|
108
|
+
if (options.json) {
|
|
109
|
+
console.log(JSON.stringify({ feature: featureName, from: oldType, to: newType, resetGates, stampedGates, ...finalFeature }, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.green(`\n✅ Feature retyped: ${featureName} (${oldType} → ${newType})`));
|
|
114
|
+
if (resetGates.length) {
|
|
115
|
+
console.log(chalk.yellow(` gates reset (were policy-stamped, no longer skipped): ${resetGates.join(', ')}`));
|
|
116
|
+
}
|
|
117
|
+
if (stampedGates.length) {
|
|
118
|
+
console.log(chalk.yellow(` gates stamped policy:${newType}: ${stampedGates.join(', ')}`));
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.gray(` scaffolded: ${newWorkType.scaffold.join(', ')}`));
|
|
121
|
+
console.log('');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(chalk.red(`\n❌ Error retyping feature: ${error.message}\n`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -33,6 +33,10 @@ export async function scoreCommand(featureName, taskId, scoreArg, options = {})
|
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
if (feature.taskVerifications?.[taskId]?.status === 'fail') {
|
|
37
|
+
console.error(chalk.yellow(`⚠️ Último verify de ${taskId} está VERMELHO — score sendo gravado sobre código que não passou no verify determinístico`));
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
let dimensions;
|
|
37
41
|
if (options.dims) {
|
|
38
42
|
const parts = String(options.dims).split(',').map(s => s.trim());
|
|
@@ -3,6 +3,7 @@ import { existsSync } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { parseTasksJson } from '../../lib/tasks/tasks-json-parser.js';
|
|
5
5
|
import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
|
|
6
|
+
import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
|
|
@@ -10,6 +11,9 @@ import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
|
|
|
10
11
|
*
|
|
11
12
|
* This is the command morph-apply §2 runs instead of re-deriving the rules
|
|
12
13
|
* (and instead of the old inline JS snippets the LLM could not execute).
|
|
14
|
+
*
|
|
15
|
+
* `--mermaid` emits the DAG as a Mermaid `flowchart TD` instead (for
|
|
16
|
+
* visualizing the plan at Gate 2), bypassing the dispatch analysis entirely.
|
|
13
17
|
*/
|
|
14
18
|
export async function dagCommand(featureName, options = {}) {
|
|
15
19
|
try {
|
|
@@ -22,6 +26,14 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
22
26
|
|
|
23
27
|
// parseTasksJson validates the schema and throws with the error list.
|
|
24
28
|
const data = await parseTasksJson(tasksPath);
|
|
29
|
+
|
|
30
|
+
// --mermaid short-circuits before the dispatch analysis: visualizing the
|
|
31
|
+
// plan's DAG doesn't need a sequential-vs-parallel decision.
|
|
32
|
+
if (options.mermaid) {
|
|
33
|
+
console.log(renderMermaidFlowchart(data));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
const plan = planDispatch(data);
|
|
26
38
|
|
|
27
39
|
if (options.json) {
|
|
@@ -60,6 +72,26 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
60
72
|
console.log(` ${chalk.gray('→')} Sequencial: ${plan.sequentialRemainder.join(', ')}`);
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
// Isolamento — only meaningful when there IS a parallel batch. In a shared
|
|
76
|
+
// working tree two agents writing the same path corrupt each other; the
|
|
77
|
+
// dag recommends `isolation: "worktree"` so each runs in its own worktree
|
|
78
|
+
// (morph-apply §3b-w).
|
|
79
|
+
const iso = plan.isolation;
|
|
80
|
+
if (iso && (iso.recommended === 'worktree' || iso.unknownFootprint.length > 0)) {
|
|
81
|
+
console.log(chalk.bold('\nIsolamento:'));
|
|
82
|
+
if (iso.recommended === 'worktree') {
|
|
83
|
+
console.log(` ${chalk.yellow('⚠')} recomendado: ${chalk.bold('worktree')} — ${iso.reason}`);
|
|
84
|
+
for (const c of iso.collisions) {
|
|
85
|
+
console.log(chalk.gray(` colisão em "${c.path}": ${c.units.join(' ‖ ')} (${c.tasks.join(', ')})`));
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
console.log(` ${chalk.gray('○')} ${iso.reason}`);
|
|
89
|
+
}
|
|
90
|
+
if (iso.unknownFootprint.length > 0) {
|
|
91
|
+
console.log(chalk.gray(` footprint desconhecido (sem outputs): ${iso.unknownFootprint.join(', ')}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
console.log('\n' + '━'.repeat(60) + '\n');
|
|
64
96
|
} catch (error) {
|
|
65
97
|
console.error(chalk.red(`\n❌ Error analyzing DAG: ${error.message}\n`));
|
|
@@ -132,6 +132,14 @@ const PHASE_OUTPUTS = {
|
|
|
132
132
|
review: ['review-report.md'],
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
+
// Work types whose plan phase (see src/core/work-types.js) skips proposal.md/
|
|
136
|
+
// spec.md/mandate.md — chore and hotfix stamp proposal+plan gates at birth
|
|
137
|
+
// (policy-approved, no human deliberation over those docs) and go straight to
|
|
138
|
+
// tasks.json. Validating the full plan doc set against a feature that was
|
|
139
|
+
// never asked to write those docs would be a false failure, not a real gap.
|
|
140
|
+
const LEAN_PLAN_WORK_TYPES = ['chore', 'hotfix'];
|
|
141
|
+
const LEAN_PLAN_OUTPUTS = ['tasks.json'];
|
|
142
|
+
|
|
135
143
|
/**
|
|
136
144
|
* Phase-to-folder mapping (v5 filesystem convention)
|
|
137
145
|
*/
|
|
@@ -295,11 +303,16 @@ function validateMarkdownSections(content, sections, result) {
|
|
|
295
303
|
*
|
|
296
304
|
* @param {string} featurePath Absolute path to the feature directory.
|
|
297
305
|
* @param {string|null} phase Phase key, or null to validate everything.
|
|
306
|
+
* @param {string|undefined} workType Feature's workType (see src/core/work-types.js).
|
|
307
|
+
* When `phase === 'plan'` and workType is 'chore'/'hotfix', only tasks.json
|
|
308
|
+
* is required (see LEAN_PLAN_WORK_TYPES above).
|
|
298
309
|
* @returns {{ outputFiles?: Array<{file:string, dir:string}>, error?: string }}
|
|
299
310
|
*/
|
|
300
|
-
function resolveOutputFiles(featurePath, phase) {
|
|
311
|
+
function resolveOutputFiles(featurePath, phase, workType) {
|
|
301
312
|
if (phase) {
|
|
302
|
-
const files =
|
|
313
|
+
const files = (phase === 'plan' && LEAN_PLAN_WORK_TYPES.includes(workType))
|
|
314
|
+
? LEAN_PLAN_OUTPUTS
|
|
315
|
+
: PHASE_OUTPUTS[phase];
|
|
303
316
|
if (!files) return { error: `Unknown phase: ${phase}` };
|
|
304
317
|
const phaseFolder = PHASE_FOLDERS[phase];
|
|
305
318
|
const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
|
|
@@ -327,7 +340,7 @@ function resolveOutputFiles(featurePath, phase) {
|
|
|
327
340
|
* Pure validation core — no console output, no process.exit.
|
|
328
341
|
*
|
|
329
342
|
* @param {string} featurePath Absolute path to the feature directory.
|
|
330
|
-
* @param {{ phase?: string }} [options]
|
|
343
|
+
* @param {{ phase?: string, workType?: string }} [options]
|
|
331
344
|
* @returns {{
|
|
332
345
|
* feature: string, phase: string, valid: boolean, error?: string,
|
|
333
346
|
* totalErrors: number, totalWarnings: number, totalFound: number,
|
|
@@ -336,9 +349,10 @@ function resolveOutputFiles(featurePath, phase) {
|
|
|
336
349
|
*/
|
|
337
350
|
export function runFeatureValidation(featurePath, options = {}) {
|
|
338
351
|
const phase = options.phase || null;
|
|
352
|
+
const workType = options.workType || null;
|
|
339
353
|
const feature = path.basename(featurePath);
|
|
340
354
|
|
|
341
|
-
const { outputFiles, error } = resolveOutputFiles(featurePath, phase);
|
|
355
|
+
const { outputFiles, error } = resolveOutputFiles(featurePath, phase, workType);
|
|
342
356
|
if (error) {
|
|
343
357
|
return {
|
|
344
358
|
feature,
|
|
@@ -392,7 +406,21 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
392
406
|
process.exit(1);
|
|
393
407
|
}
|
|
394
408
|
|
|
395
|
-
|
|
409
|
+
// workType is read straight off the feature's own feature.json (not via
|
|
410
|
+
// state-manager's getFeature/loadState) so a broken/missing root state.json
|
|
411
|
+
// elsewhere in the project can never make `validate` itself throw — it just
|
|
412
|
+
// falls back to the full (non-lean) output set, same as today.
|
|
413
|
+
let workType;
|
|
414
|
+
try {
|
|
415
|
+
const fjPath = path.join(featurePath, 'feature.json');
|
|
416
|
+
if (fs.existsSync(fjPath)) {
|
|
417
|
+
workType = JSON.parse(fs.readFileSync(fjPath, 'utf8')).workType;
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
workType = undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const validation = runFeatureValidation(featurePath, { phase, workType });
|
|
396
424
|
const { results, totalErrors, totalWarnings, totalFound } = validation;
|
|
397
425
|
const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
|
|
398
426
|
|