@polymorphism-tech/morph-spec 9.2.0 → 9.4.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 +14 -78
- package/README.md +2 -2
- package/bin/morph-spec.js +7 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -3
- package/framework/MORPH.md +5 -8
- package/framework/agents.json +14 -9
- package/framework/commands/morph-status.md +2 -2
- package/framework/docs/CLI.md +8 -2
- package/framework/evals/review-checklist.md +4 -1
- package/framework/hooks/README.md +1 -0
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
- package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
- package/framework/hooks/claude-code/core/worktree-context.js +23 -0
- package/framework/hooks/claude-code/statusline.py +3 -2
- package/framework/hooks/shared/command-tokens.js +17 -0
- package/framework/hooks/shared/gate-event.js +18 -0
- package/framework/hooks/shared/next-step.js +107 -0
- package/framework/hooks/shared/slice-progress.js +18 -5
- package/framework/hooks/shared/state-writer.js +6 -1
- package/framework/mcp/registry.json +31 -1
- package/framework/rules/csharp-standards.md +5 -3
- package/framework/rules/dotnet-templates.md +2 -2
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/feature.schema.json +1 -0
- 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 +4 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +3 -3
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +2 -1
- package/framework/standards/STANDARDS.json +55 -8
- package/framework/standards/STANDARDS.meta.json +29 -1
- package/framework/standards/ai-agents/setup.md +2 -0
- package/framework/standards/architecture/engineering-lens.md +100 -0
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
- package/framework/standards/backend/dotnet/analyzers.md +59 -0
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
- package/framework/standards/integration/mcp/mcp-tools.md +3 -1
- package/framework/templates/README.md +0 -1
- package/framework/templates/REGISTRY.json +0 -207
- package/package.json +1 -1
- package/src/commands/observability/cost.js +12 -0
- package/src/commands/project/status.js +6 -68
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +1 -1
- package/src/commands/state/archive.js +1 -1
- package/src/commands/state/finish.js +1 -1
- package/src/commands/state/gate-check.js +2 -2
- package/src/commands/state/gate-decision.js +4 -1
- package/src/commands/state/next.js +24 -0
- package/src/commands/state/report-check.js +3 -1
- package/src/commands/validation/verify.js +6 -1
- package/src/core/state/state-manager.js +4 -0
- package/src/core/templates/template-registry.js +1 -43
- package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
- package/src/dashboard/scanner/features-scanner.js +5 -2
- package/src/lib/generators/handoff-generator.js +11 -25
- package/src/lib/next-step.js +10 -0
- package/src/lib/risk-detector.js +18 -7
- package/src/lib/stack/stack-scan.js +17 -7
- package/src/lib/telemetry/metrics.js +46 -0
- package/src/lib/validators/verify-runner.js +1 -1
- package/src/utils/agents-installer.js +24 -1
- package/src/utils/hooks-installer.js +1 -1
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
- package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +0 -94
|
@@ -642,213 +642,6 @@
|
|
|
642
642
|
"isFragment": true,
|
|
643
643
|
"validatePlaceholders": true
|
|
644
644
|
},
|
|
645
|
-
{
|
|
646
|
-
"id": "hop-squad-leader",
|
|
647
|
-
"kind": "template",
|
|
648
|
-
"name": "Squad Leader HOP",
|
|
649
|
-
"path": "meta-prompts/squad-leaders/squad-leader.md",
|
|
650
|
-
"location": "framework",
|
|
651
|
-
"category": "meta-prompts",
|
|
652
|
-
"technology": "handlebars",
|
|
653
|
-
"phase": "implement",
|
|
654
|
-
"required": false,
|
|
655
|
-
"description": "Generic squad leader higher-order prompt with mission, domain-leaders, standards, tasks, deliverables, constraints",
|
|
656
|
-
"outputName": "squad-leader-prompt.md",
|
|
657
|
-
"placeholders": [
|
|
658
|
-
"AGENT_ID",
|
|
659
|
-
"CONSTRAINTS",
|
|
660
|
-
"DELIVERABLES",
|
|
661
|
-
"DOMAIN",
|
|
662
|
-
"DOMAIN_LEADERS",
|
|
663
|
-
"FEATURE_NAME",
|
|
664
|
-
"MISSION",
|
|
665
|
-
"SPEC_SUMMARY",
|
|
666
|
-
"STANDARDS",
|
|
667
|
-
"TASKS"
|
|
668
|
-
],
|
|
669
|
-
"stackSpecific": false,
|
|
670
|
-
"engine": "handlebars",
|
|
671
|
-
"deprecated": false,
|
|
672
|
-
"hopType": "squad-leader",
|
|
673
|
-
"validatePlaceholders": true
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
"id": "hop-backend-squad",
|
|
677
|
-
"kind": "template",
|
|
678
|
-
"name": "Backend Squad Leader HOP",
|
|
679
|
-
"path": "meta-prompts/squad-leaders/backend-squad.md",
|
|
680
|
-
"location": "framework",
|
|
681
|
-
"category": "meta-prompts",
|
|
682
|
-
"technology": "handlebars",
|
|
683
|
-
"phase": "implement",
|
|
684
|
-
"required": false,
|
|
685
|
-
"description": "Backend squad leader HOP for dotnet-senior with backend domain leaders, .NET 10 standards, architecture and security guidelines",
|
|
686
|
-
"outputName": "backend-squad-prompt.md",
|
|
687
|
-
"placeholders": [
|
|
688
|
-
"ADDITIONAL_LEADERS",
|
|
689
|
-
"AGENT_ID",
|
|
690
|
-
"CONSTRAINTS",
|
|
691
|
-
"DELIVERABLES",
|
|
692
|
-
"FEATURE_NAME",
|
|
693
|
-
"MISSION",
|
|
694
|
-
"SPEC_SUMMARY",
|
|
695
|
-
"TASKS"
|
|
696
|
-
],
|
|
697
|
-
"stackSpecific": false,
|
|
698
|
-
"engine": "handlebars",
|
|
699
|
-
"deprecated": false,
|
|
700
|
-
"hopType": "squad-leader",
|
|
701
|
-
"applicableStacks": [
|
|
702
|
-
"nextjs-neon"
|
|
703
|
-
],
|
|
704
|
-
"validatePlaceholders": true
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
"id": "hop-frontend-squad",
|
|
708
|
-
"kind": "template",
|
|
709
|
-
"name": "Frontend Squad Leader HOP",
|
|
710
|
-
"path": "meta-prompts/squad-leaders/frontend-squad.md",
|
|
711
|
-
"location": "framework",
|
|
712
|
-
"category": "meta-prompts",
|
|
713
|
-
"technology": "handlebars",
|
|
714
|
-
"phase": "implement",
|
|
715
|
-
"required": false,
|
|
716
|
-
"description": "Frontend squad leader HOP for ui-designer with Next.js domain leaders and design system standards",
|
|
717
|
-
"outputName": "frontend-squad-prompt.md",
|
|
718
|
-
"placeholders": [
|
|
719
|
-
"ADDITIONAL_LEADERS",
|
|
720
|
-
"AGENT_ID",
|
|
721
|
-
"CONSTRAINTS",
|
|
722
|
-
"DELIVERABLES",
|
|
723
|
-
"DOMAIN",
|
|
724
|
-
"FEATURE_NAME",
|
|
725
|
-
"MISSION",
|
|
726
|
-
"SPEC_SUMMARY",
|
|
727
|
-
"TASKS"
|
|
728
|
-
],
|
|
729
|
-
"stackSpecific": false,
|
|
730
|
-
"engine": "handlebars",
|
|
731
|
-
"deprecated": false,
|
|
732
|
-
"hopType": "squad-leader",
|
|
733
|
-
"applicableStacks": [
|
|
734
|
-
"nextjs-neon"
|
|
735
|
-
],
|
|
736
|
-
"validatePlaceholders": true
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
"id": "hop-checkpoint-validator",
|
|
740
|
-
"kind": "template",
|
|
741
|
-
"name": "Checkpoint Validator HOP",
|
|
742
|
-
"path": "meta-prompts/validators/checkpoint-validator.md",
|
|
743
|
-
"location": "framework",
|
|
744
|
-
"category": "meta-prompts",
|
|
745
|
-
"technology": "handlebars",
|
|
746
|
-
"phase": "implement",
|
|
747
|
-
"required": false,
|
|
748
|
-
"description": "Checkpoint validator HOP running architecture, security, design-system, and packages validators with pass/fail decision logic",
|
|
749
|
-
"outputName": "checkpoint-validator-prompt.md",
|
|
750
|
-
"placeholders": [
|
|
751
|
-
"CHECKPOINT_FREQUENCY",
|
|
752
|
-
"CHECKPOINT_NUM",
|
|
753
|
-
"FEATURE_NAME",
|
|
754
|
-
"FILES_CHANGED",
|
|
755
|
-
"RECENT_TASKS",
|
|
756
|
-
"TASKS_COMPLETED",
|
|
757
|
-
"TASKS_SINCE_LAST",
|
|
758
|
-
"TASKS_TOTAL"
|
|
759
|
-
],
|
|
760
|
-
"stackSpecific": false,
|
|
761
|
-
"engine": "handlebars",
|
|
762
|
-
"deprecated": false,
|
|
763
|
-
"hopType": "validator",
|
|
764
|
-
"validatePlaceholders": true
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
"id": "hop-pre-commit-validator",
|
|
768
|
-
"kind": "template",
|
|
769
|
-
"name": "Pre-Commit Validator HOP",
|
|
770
|
-
"path": "meta-prompts/validators/pre-commit-validator.md",
|
|
771
|
-
"location": "framework",
|
|
772
|
-
"category": "meta-prompts",
|
|
773
|
-
"technology": "handlebars",
|
|
774
|
-
"phase": "implement",
|
|
775
|
-
"required": false,
|
|
776
|
-
"description": "Pre-commit hook validator HOP checking conventional commits, secrets, agent spec validity, and stack-specific rules",
|
|
777
|
-
"outputName": "pre-commit-validator-prompt.md",
|
|
778
|
-
"placeholders": [
|
|
779
|
-
"BRANCH_NAME",
|
|
780
|
-
"FEATURE_NAME",
|
|
781
|
-
"STAGED_FILES",
|
|
782
|
-
"STAGED_FILE_COUNT",
|
|
783
|
-
"TODO"
|
|
784
|
-
],
|
|
785
|
-
"stackSpecific": false,
|
|
786
|
-
"engine": "handlebars",
|
|
787
|
-
"deprecated": false,
|
|
788
|
-
"hopType": "validator",
|
|
789
|
-
"validatePlaceholders": false
|
|
790
|
-
},
|
|
791
|
-
{
|
|
792
|
-
"id": "hop-fusion-agent",
|
|
793
|
-
"kind": "template",
|
|
794
|
-
"name": "Fusion Agent HOP",
|
|
795
|
-
"path": "meta-prompts/fusion/fusion-agent.md",
|
|
796
|
-
"location": "framework",
|
|
797
|
-
"category": "meta-prompts",
|
|
798
|
-
"technology": "handlebars",
|
|
799
|
-
"phase": "implement",
|
|
800
|
-
"required": false,
|
|
801
|
-
"description": "F-Thread fusion participant HOP with approach hints per thread index (standard, alternative, optimized) and self-assessment scoring",
|
|
802
|
-
"outputName": "fusion-agent-prompt.md",
|
|
803
|
-
"placeholders": [
|
|
804
|
-
"AGENT_ID",
|
|
805
|
-
"DELIVERABLES",
|
|
806
|
-
"FUSION_STRATEGY",
|
|
807
|
-
"SPEC_SUMMARY",
|
|
808
|
-
"STANDARDS",
|
|
809
|
-
"TASK_DESCRIPTION",
|
|
810
|
-
"THREAD_COUNT",
|
|
811
|
-
"THREAD_COUNT_MINUS_ONE",
|
|
812
|
-
"THREAD_ID",
|
|
813
|
-
"THREAD_INDEX"
|
|
814
|
-
],
|
|
815
|
-
"stackSpecific": false,
|
|
816
|
-
"engine": "handlebars",
|
|
817
|
-
"deprecated": false,
|
|
818
|
-
"hopType": "fusion",
|
|
819
|
-
"validatePlaceholders": true
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
"id": "hop-fusion-aggregator",
|
|
823
|
-
"kind": "template",
|
|
824
|
-
"name": "Fusion Aggregator HOP",
|
|
825
|
-
"path": "meta-prompts/fusion/fusion-aggregator.md",
|
|
826
|
-
"location": "framework",
|
|
827
|
-
"category": "meta-prompts",
|
|
828
|
-
"technology": "handlebars",
|
|
829
|
-
"phase": "implement",
|
|
830
|
-
"required": false,
|
|
831
|
-
"description": "F-Thread aggregator HOP that scores and selects the winning implementation using best-of-n, consensus, or manual-select strategies",
|
|
832
|
-
"outputName": "fusion-aggregator-prompt.md",
|
|
833
|
-
"placeholders": [
|
|
834
|
-
"AGENT_1",
|
|
835
|
-
"AGENT_2",
|
|
836
|
-
"AGENT_3",
|
|
837
|
-
"FEATURE_NAME",
|
|
838
|
-
"FUSION_RESULTS",
|
|
839
|
-
"FUSION_STRATEGY",
|
|
840
|
-
"SESSION_ID",
|
|
841
|
-
"THREAD_COUNT",
|
|
842
|
-
"WINNING_AGENT",
|
|
843
|
-
"WINNING_SCORE",
|
|
844
|
-
"WINNING_THREAD"
|
|
845
|
-
],
|
|
846
|
-
"stackSpecific": false,
|
|
847
|
-
"engine": "handlebars",
|
|
848
|
-
"deprecated": false,
|
|
849
|
-
"hopType": "fusion",
|
|
850
|
-
"validatePlaceholders": true
|
|
851
|
-
},
|
|
852
645
|
{
|
|
853
646
|
"id": "dotnet-abstractions-handler",
|
|
854
647
|
"kind": "fragment",
|
package/package.json
CHANGED
|
@@ -145,6 +145,18 @@ function printMetricsBody(m) {
|
|
|
145
145
|
? trees.map(([tree, s]) => `${tree} rodou ${s.ran}, pulou ${s.skipped}`).join(' · ')
|
|
146
146
|
: 'nenhum evento Verify na janela'}`);
|
|
147
147
|
for (const [reason, n] of Object.entries(m.reuse.skipReasons)) console.log(chalk.gray(` pulou ${n}×: ${reason}`));
|
|
148
|
+
const gates = Object.entries(m.gates);
|
|
149
|
+
if (!gates.length) {
|
|
150
|
+
console.log(' gates: nenhum evento Gate na janela');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const [gate, g] of gates) {
|
|
154
|
+
console.log(` gate ${gate}: ${g.decisions} decisões, ${g.paused} pausas → ${g.approvedAfterPause} aprovadas como estavam, `
|
|
155
|
+
+ `${g.reworkedAfterPause} retrabalhadas${g.unapproved ? `, ${g.unapproved} desaprovações` : ''}`);
|
|
156
|
+
for (const [signal, s] of Object.entries(g.bySignal)) {
|
|
157
|
+
console.log(chalk.gray(` ${signal}: ${s.paused} pausas, ${s.approvedAfterPause} aprovadas como estavam, ${s.reworkedAfterPause} retrabalhadas`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
function printMetrics(feature, options, cwd) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadState, getFeature, derivePhase, deriveOutputs } from '../../core/state/state-manager.js';
|
|
3
3
|
import { sliceProgress } from '../../../framework/hooks/shared/slice-progress.js';
|
|
4
|
+
import { formatNext } from '../../../framework/hooks/shared/next-step.js';
|
|
5
|
+
import { nextStepFor } from '../../lib/next-step.js';
|
|
4
6
|
|
|
5
7
|
const PHASE_SYMBOLS = {
|
|
6
8
|
completed: '✅',
|
|
@@ -87,60 +89,6 @@ function calculateCompletionPercentage(feature) {
|
|
|
87
89
|
return Math.round((completedPhases / totalPhases) * 100);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
function getNextSteps(feature) {
|
|
91
|
-
const steps = [];
|
|
92
|
-
|
|
93
|
-
const phaseApprovalMap = {
|
|
94
|
-
plan: 'plan',
|
|
95
|
-
uiux: 'uiux',
|
|
96
|
-
implement: 'plan',
|
|
97
|
-
review: 'review'
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const relevantGate = phaseApprovalMap[feature.phase];
|
|
101
|
-
const approvalGates = feature.approvalGates || {};
|
|
102
|
-
|
|
103
|
-
if (relevantGate && approvalGates[relevantGate] && !approvalGates[relevantGate].approved) {
|
|
104
|
-
steps.push({
|
|
105
|
-
type: 'approval',
|
|
106
|
-
message: `Approve ${relevantGate} to advance`,
|
|
107
|
-
command: `morph-spec approve ${feature.name || 'feature'} ${relevantGate}`
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const requiredOutputs = feature.phase === 'uiux' ? [] : PHASE_OUTPUTS[feature.phase] || [];
|
|
112
|
-
const missingOutputs = requiredOutputs.filter(o => !feature.outputs[o]?.created);
|
|
113
|
-
|
|
114
|
-
if (missingOutputs.length > 0) {
|
|
115
|
-
steps.push({
|
|
116
|
-
type: 'output',
|
|
117
|
-
message: `Create missing outputs: ${missingOutputs.join(', ')}`,
|
|
118
|
-
command: null
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const hasApprovalPending = relevantGate && approvalGates[relevantGate] && !approvalGates[relevantGate].approved;
|
|
123
|
-
if (missingOutputs.length === 0 && !hasApprovalPending) {
|
|
124
|
-
const currentPhaseIndex = PHASE_ORDER.findIndex(p => p.key === feature.phase);
|
|
125
|
-
if (currentPhaseIndex >= 0 && currentPhaseIndex < PHASE_ORDER.length - 1) {
|
|
126
|
-
const nextPhase = PHASE_ORDER[currentPhaseIndex + 1];
|
|
127
|
-
steps.push({
|
|
128
|
-
type: 'advance',
|
|
129
|
-
message: `Advance to ${nextPhase.name}`,
|
|
130
|
-
command: `morph-spec phase advance ${feature.name || 'feature'}`
|
|
131
|
-
});
|
|
132
|
-
} else if (currentPhaseIndex === PHASE_ORDER.length - 1) {
|
|
133
|
-
steps.push({
|
|
134
|
-
type: 'complete',
|
|
135
|
-
message: 'Feature complete! Consider archiving.',
|
|
136
|
-
command: null
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return steps;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
92
|
export async function statusCommand(featureName, options = {}) {
|
|
145
93
|
if (!featureName) {
|
|
146
94
|
console.error(chalk.red('✗ Error: Feature name required'));
|
|
@@ -191,7 +139,7 @@ export async function statusCommand(featureName, options = {}) {
|
|
|
191
139
|
})),
|
|
192
140
|
slices,
|
|
193
141
|
approvals: feature.approvalGates || {},
|
|
194
|
-
|
|
142
|
+
next: nextStepFor(featureName, process.cwd())
|
|
195
143
|
};
|
|
196
144
|
|
|
197
145
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -235,6 +183,7 @@ export async function statusCommand(featureName, options = {}) {
|
|
|
235
183
|
console.log(` Total: ${slices.total}`);
|
|
236
184
|
console.log(chalk.green(` ✓ Feitas: ${slices.done} (${percentage}%)`));
|
|
237
185
|
if (slices.partial > 0) console.log(chalk.cyan(` ▶ Parciais: ${slices.partial}`));
|
|
186
|
+
if (slices.blocked > 0) console.log(chalk.yellow(` ⏸ Bloqueadas (esperam decisão): ${slices.blocked}`));
|
|
238
187
|
if (slices.pending > 0) console.log(chalk.gray(` ⬜ Pendentes: ${slices.pending}`));
|
|
239
188
|
console.log('');
|
|
240
189
|
}
|
|
@@ -258,19 +207,8 @@ export async function statusCommand(featureName, options = {}) {
|
|
|
258
207
|
console.log('');
|
|
259
208
|
}
|
|
260
209
|
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
console.log(chalk.white.bold('Next Steps:'));
|
|
264
|
-
nextSteps.forEach((step, index) => {
|
|
265
|
-
console.log(chalk.yellow(` ${index + 1}. ${step.message}`));
|
|
266
|
-
if (step.command) {
|
|
267
|
-
console.log(chalk.gray(` → ${step.command}`));
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
console.log('');
|
|
271
|
-
} else {
|
|
272
|
-
console.log(chalk.green('✓ Nada pendente.\n'));
|
|
273
|
-
}
|
|
210
|
+
const next = nextStepFor(featureName, process.cwd());
|
|
211
|
+
if (next) console.log(chalk.yellow(`${formatNext(next)}\n`));
|
|
274
212
|
|
|
275
213
|
console.log(chalk.gray('─'.repeat(60)));
|
|
276
214
|
console.log(chalk.gray('Run with --verbose for detailed output'));
|
|
@@ -743,7 +743,7 @@ export async function clearRootWorktreeRegistration(root, feature, { stamp } = {
|
|
|
743
743
|
let unchanged = true;
|
|
744
744
|
try { execFileSync('git', ['diff', '--quiet', 'HEAD', '--', featPathspec], gitOpts); } catch { unchanged = false; }
|
|
745
745
|
if (!unchanged) {
|
|
746
|
-
execFileSync('git', ['commit', '-m', `morph
|
|
746
|
+
execFileSync('git', ['commit', '-m', `chore(morph): clear ${feature} worktree registration`, '--', featPathspec], gitOpts);
|
|
747
747
|
}
|
|
748
748
|
} catch { /* fail-open — a dirty root is a nuisance, not a reason to fail */ }
|
|
749
749
|
return { shedBackup: null };
|
|
@@ -143,7 +143,7 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
143
143
|
process.exit(1);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
console.error(chalk.red(`\n❌ A fase "${phase}" não pode ser encerrada — artefato(s) obrigatório(s) ausente(s) ou
|
|
146
|
+
console.error(chalk.red(`\n❌ A fase "${phase}" não pode ser encerrada — artefato(s) obrigatório(s) ausente(s), PARCIAL ou BLOQUEADO:`));
|
|
147
147
|
for (const rel of missingArtifacts) console.error(chalk.gray(` • ${rel}`));
|
|
148
148
|
console.error(chalk.gray('\n Produza o artefato e rode de novo, ou passe --force para avançar assim mesmo.\n'));
|
|
149
149
|
process.exit(1);
|
|
@@ -123,7 +123,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
123
123
|
...(adrIndex.changed ? [ADR_INDEX_PATHSPEC] : []),
|
|
124
124
|
];
|
|
125
125
|
try {
|
|
126
|
-
execFileSync('git', ['commit', '-m', `morph
|
|
126
|
+
execFileSync('git', ['commit', '-m', `chore(morph): archive ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
|
|
127
127
|
committed = true;
|
|
128
128
|
} catch { /* nothing to commit / no identity — not fatal, the move + stage stand */ }
|
|
129
129
|
}
|
|
@@ -494,7 +494,7 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
|
|
497
|
-
const mergeMsg = `morph
|
|
497
|
+
const mergeMsg = `chore(morph): merge ${expectedBranch}`;
|
|
498
498
|
const adrNote = adrIndexNote(arch);
|
|
499
499
|
|
|
500
500
|
if (isWorktreeFlow) {
|
|
@@ -189,10 +189,10 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
189
189
|
const computed = shouldPauseGate({ trust }, signals);
|
|
190
190
|
const manual = feature.gateDecisions?.[gateName];
|
|
191
191
|
const manualPause = manual?.source === 'manual' && manual.pause === true;
|
|
192
|
-
const { pause, reason } = manualPause ? { pause: true, reason: manual.reason } : computed;
|
|
192
|
+
const { pause, reason, signal } = manualPause ? { pause: true, reason: manual.reason, signal: 'manual' } : computed;
|
|
193
193
|
|
|
194
194
|
const entry = await persistGateDecision(featureName, gateName, {
|
|
195
|
-
pause, reason, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
|
|
195
|
+
pause, reason, signal, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
if (options.json) {
|
|
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
5
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
6
6
|
import { mockupsHash } from '../../lib/specs/screen-checklist.js';
|
|
7
|
+
import { recordGateEvent } from '../../../framework/hooks/shared/gate-event.js';
|
|
7
8
|
|
|
8
9
|
export const VALID_GATES = ['plan', 'uiux', 'review'];
|
|
9
10
|
|
|
@@ -31,7 +32,7 @@ export function readTrustFromConfig() {
|
|
|
31
32
|
* The ONE write path of `gateDecisions.{gate}`, shared by `gate-decision` and `gate-check`.
|
|
32
33
|
* Does not validate `gateName`. A uiux decision records the `mockupsHash` it saw.
|
|
33
34
|
*/
|
|
34
|
-
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd(), source } = {}) {
|
|
35
|
+
export async function persistGateDecision(featureName, gateName, { pause, reason, signal, signals = {}, trust, cwd = process.cwd(), source } = {}) {
|
|
35
36
|
const entry = {
|
|
36
37
|
pause,
|
|
37
38
|
reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
@@ -43,6 +44,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
|
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
47
|
+
recordGateEvent(cwd, { feature: featureName, gate: gateName, step: 'decision', pause, signal, source });
|
|
46
48
|
return entry;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -91,6 +93,7 @@ export async function gateDecisionCommand(featureName, gateName, options = {}) {
|
|
|
91
93
|
const entry = await persistGateDecision(featureName, gateName, {
|
|
92
94
|
pause,
|
|
93
95
|
reason: options.reason,
|
|
96
|
+
signal: 'manual',
|
|
94
97
|
signals,
|
|
95
98
|
trust: options.trust,
|
|
96
99
|
source: 'manual',
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { formatNext } from '../../../framework/hooks/shared/next-step.js';
|
|
3
|
+
import { nextStepFor } from '../../lib/next-step.js';
|
|
4
|
+
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
5
|
+
|
|
6
|
+
// Read-only: decides in the tree that owns the feature, since the root copy of a worktree feature is a birth skeleton.
|
|
7
|
+
export function nextCommand(featureName, options = {}) {
|
|
8
|
+
const owner = resolveFeatureTree(featureName, process.cwd());
|
|
9
|
+
const next = owner.source === 'none' || owner.source === 'branch-only' ? null : nextStepFor(featureName, owner.tree);
|
|
10
|
+
|
|
11
|
+
if (!next) {
|
|
12
|
+
const error = owner.source === 'branch-only' ? 'feature-not-in-tree' : 'feature-not-found';
|
|
13
|
+
if (options.json) console.log(JSON.stringify({ error, feature: featureName, branch: owner.branch ?? null }));
|
|
14
|
+
else console.error(chalk.red(`\n❌ ${error}: ${featureName}${owner.branch ? ` (morph-spec worktree setup ${featureName})` : ''}\n`));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify({ ...next, feature: featureName, tree: owner.tree }));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(formatNext(next));
|
|
23
|
+
if (owner.tree !== process.cwd()) console.log(chalk.gray(` árvore dona: ${owner.tree}`));
|
|
24
|
+
}
|
|
@@ -130,6 +130,7 @@ function decide(report, observed, stale) {
|
|
|
130
130
|
}
|
|
131
131
|
// PARCIAL: the agent stopped short (turn ceiling) and resumes from its last
|
|
132
132
|
// `wip` commit — completing it DEGRADADO would stamp unfinished work as done.
|
|
133
|
+
if (report.exists && report.blocked) return { verdict: 'report-blocked', action: 'ask-human' };
|
|
133
134
|
if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
|
|
134
135
|
if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
|
|
135
136
|
if (!observed.available) return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
|
|
@@ -151,7 +152,8 @@ const ACTION_HINT = {
|
|
|
151
152
|
'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: o agente da fatia seguiu escrevendo arquivos depois de gravá-lo, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
|
|
152
153
|
'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre nele a falha de protocolo.',
|
|
153
154
|
resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a fatia NÃO está pronta. Não carimbe DEGRADADO — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
|
|
154
|
-
'
|
|
155
|
+
'ask-human': 'relatório BLOQUEADO: o sub-agent parou numa decisão que não é dele (requisito que contradiz o plano ou um ADR, escolha do humano) e o relatório traz a pergunta e as opções. NÃO re-despache — a mesma pergunta volta. Se o plano ou um ADR já decide, decida você e registre no relatório; senão, faça a pergunta por `AskUserQuestion`. Depois retome o MESMO agente por `SendMessage` com a resposta.',
|
|
156
|
+
'accept-degraded':'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório da fatia a partir da mensagem de retorno, carimbado como DEGRADADO, com a falha de protocolo registrada nele.',
|
|
155
157
|
redispatch: 're-despache a fatia UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
|
|
156
158
|
};
|
|
157
159
|
|
|
@@ -240,6 +240,7 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
240
240
|
status: result.status,
|
|
241
241
|
nodes: nodeStatuses(result),
|
|
242
242
|
attempts: (prevEntry?.attempts ?? 0) + 1,
|
|
243
|
+
failStreak: result.status === 'fail' ? (prevEntry?.failStreak ?? 0) + 1 : 0,
|
|
243
244
|
updatedAt: now,
|
|
244
245
|
};
|
|
245
246
|
await updateFeature(featureName, historyKey, compact);
|
|
@@ -277,8 +278,12 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
if (options.json) {
|
|
281
|
+
// Each node's tail is already in `feedback`, prefixed by node: printing it twice doubles what the session ingests.
|
|
282
|
+
const withoutOutput = ({ output: _, ...rest }) => rest;
|
|
283
|
+
const nodes = Object.fromEntries(Object.entries(result.nodes).map(([k, node]) => [k,
|
|
284
|
+
Array.isArray(node.runs) ? { ...withoutOutput(node), runs: node.runs.map(withoutOutput) } : withoutOutput(node)]));
|
|
280
285
|
const output = {
|
|
281
|
-
...result, persisted: persist, ...(persist ? {} : { reason }), repeatOf, cleanup,
|
|
286
|
+
...result, nodes, persisted: persist, ...(persist ? {} : { reason }), repeatOf, cleanup,
|
|
282
287
|
...(ci ? { source: 'ci', ciRun: { id: ci.run.databaseId, url: ci.run.url, sha: ci.run.headSha }, workflowCreated: ci.created } : {}),
|
|
283
288
|
};
|
|
284
289
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirS
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
|
|
4
4
|
import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
|
|
5
|
+
import { recordGateEvent } from '../../../framework/hooks/shared/gate-event.js';
|
|
5
6
|
|
|
6
7
|
const STATE_FILE_NAME = '.morph/state.json';
|
|
7
8
|
const FEATURES_DIR_NAME = '.morph/features';
|
|
@@ -649,6 +650,9 @@ export async function setApprovalGate(featureName, gate, approved, metadata = {}
|
|
|
649
650
|
|
|
650
651
|
state.features[featureName] = feature;
|
|
651
652
|
saveState(state);
|
|
653
|
+
recordGateEvent(process.cwd(), {
|
|
654
|
+
feature: featureName, gate, step: approved ? 'approve' : 'unapprove', approvedBy: feature.approvalGates[gate].approvedBy,
|
|
655
|
+
});
|
|
652
656
|
}
|
|
653
657
|
|
|
654
658
|
export function getApprovalGate(featureName, gate) {
|
|
@@ -36,7 +36,7 @@ export function clearRegistryCache() {
|
|
|
36
36
|
_registryCache.clear();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
function getAllTemplates(projectPath = null) {
|
|
40
40
|
const registry = loadTemplateRegistry(projectPath);
|
|
41
41
|
return registry.templates || [];
|
|
42
42
|
}
|
|
@@ -46,48 +46,6 @@ export function getTemplateById(templateId, projectPath = null) {
|
|
|
46
46
|
return templates.find(t => t.id === templateId) || null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function getTemplatesByCategory(category, projectPath = null) {
|
|
50
|
-
const templates = getAllTemplates(projectPath);
|
|
51
|
-
return templates.filter(t => t.category === category);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function getTemplatesByStack(stackId, projectPath = null) {
|
|
55
|
-
const templates = getAllTemplates(projectPath);
|
|
56
|
-
return templates.filter(t => {
|
|
57
|
-
if (!t.stackSpecific) return true;
|
|
58
|
-
|
|
59
|
-
return t.stacks && t.stacks.includes(stackId);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getRequiredTemplates(stackId = null, projectPath = null) {
|
|
64
|
-
let templates = getAllTemplates(projectPath);
|
|
65
|
-
|
|
66
|
-
templates = templates.filter(t => t.required === true);
|
|
67
|
-
|
|
68
|
-
if (stackId) {
|
|
69
|
-
templates = templates.filter(t => {
|
|
70
|
-
if (!t.stackSpecific) return true;
|
|
71
|
-
return t.stacks && t.stacks.includes(stackId);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return templates;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function searchTemplates(keyword, projectPath = null) {
|
|
79
|
-
const templates = getAllTemplates(projectPath);
|
|
80
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
81
|
-
|
|
82
|
-
return templates.filter(t => {
|
|
83
|
-
return (
|
|
84
|
-
t.id.toLowerCase().includes(lowerKeyword) ||
|
|
85
|
-
t.name.toLowerCase().includes(lowerKeyword) ||
|
|
86
|
-
(t.description && t.description.toLowerCase().includes(lowerKeyword))
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
49
|
const FRAMEWORK_TEMPLATES_DIR = join(__dirname, '../../../framework/templates');
|
|
92
50
|
|
|
93
51
|
/** Path inside this package's `framework/templates/`, not a project's `.morph/`; throws when missing. */
|
|
@@ -26,11 +26,12 @@ const TABS = [
|
|
|
26
26
|
const SLICE_STATUS = {
|
|
27
27
|
feito: { label: 'feita', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
|
|
28
28
|
parcial: { label: 'parcial', tone: 'text-[var(--color-phase-implement)]', icon: CircleDashed },
|
|
29
|
+
bloqueado: { label: 'bloqueada', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
|
|
29
30
|
pendente: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
function isReady(sl, byId) {
|
|
33
|
-
if (sl.status === 'feito') return false;
|
|
34
|
+
if (sl.status === 'feito' || sl.status === 'bloqueado') return false;
|
|
34
35
|
return (sl.depends || []).every((d) => byId[d]?.status === 'feito');
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -67,6 +68,8 @@ export function nextAction(f) {
|
|
|
67
68
|
if (paused) return `O ${GATE_LABEL[paused.gate] || paused.gate} pausou${paused.reason ? `: ${paused.reason}` : ''}. A decisão é sua, no Claude Code.`;
|
|
68
69
|
if (f.inEvaluation) return 'Os relatórios do Gate 3 chegaram. A decisão sai dos vereditos, dos bloqueadores e dos cenários provados.';
|
|
69
70
|
if (f.phase === 'implement' && f.slicesList?.length) {
|
|
71
|
+
const blocked = f.slicesList.find((sl) => sl.status === 'bloqueado');
|
|
72
|
+
if (blocked) return `${blocked.id} está bloqueada: o relatório traz uma pergunta que só você responde, no Claude Code.`;
|
|
70
73
|
const open = f.slicesList.find((sl) => sl.status === 'parcial') || f.slicesList.find((sl) => sl.status === 'pendente');
|
|
71
74
|
if (open) return `${f.slices.done} de ${f.slices.total} fatias feitas. Próxima: ${open.id}, ${open.title}.`;
|
|
72
75
|
return 'Todas as fatias feitas. Falta o verify da feature e a revisão.';
|
|
@@ -330,6 +333,7 @@ function SliceRow({ sl, f, open, onToggle, ready }) {
|
|
|
330
333
|
const BOARD_COLUMNS = [
|
|
331
334
|
{ id: 'pendente', label: 'Pendentes' },
|
|
332
335
|
{ id: 'parcial', label: 'Parciais' },
|
|
336
|
+
{ id: 'bloqueado', label: 'Bloqueadas' },
|
|
333
337
|
{ id: 'feito', label: 'Feitas' },
|
|
334
338
|
];
|
|
335
339
|
|
|
@@ -389,7 +393,7 @@ function SlicesTab({ f, view = 'list', onView = () => {} }) {
|
|
|
389
393
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
390
394
|
<Segmented label="Visualização das fatias" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
|
|
391
395
|
<p className="text-sm text-[var(--color-dim)]">
|
|
392
|
-
{f.slices.done} de {f.slices.total} feitas{f.slices.partial ? `, ${f.slices.partial} parciais` : ''}{ready ? `, ${ready} prontas para começar` : ''}
|
|
396
|
+
{f.slices.done} de {f.slices.total} feitas{f.slices.partial ? `, ${f.slices.partial} parciais` : ''}{f.slices.blocked ? `, ${f.slices.blocked} bloqueadas` : ''}{ready ? `, ${ready} prontas para começar` : ''}
|
|
393
397
|
</p>
|
|
394
398
|
</div>
|
|
395
399
|
{view === 'board' ? <SlicesBoard f={f} byId={byId} /> : (
|
|
@@ -123,10 +123,13 @@ export function readSlices(dir) {
|
|
|
123
123
|
covers: Array.isArray(x.covers) ? x.covers : [],
|
|
124
124
|
};
|
|
125
125
|
});
|
|
126
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
counts: { total: progress.total, done: progress.done, partial: progress.partial, blocked: progress.blocked, pending: progress.pending },
|
|
128
|
+
list,
|
|
129
|
+
};
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
const NO_SLICES = { total: 0, done: 0, partial: 0, pending: 0 };
|
|
132
|
+
const NO_SLICES = { total: 0, done: 0, partial: 0, blocked: 0, pending: 0 };
|
|
130
133
|
|
|
131
134
|
function gatesOf(feature) {
|
|
132
135
|
const skipped = new Set(feature.skippedPhases || []);
|