@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.
Files changed (79) hide show
  1. package/CLAUDE.md +14 -78
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +7 -0
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -3
  8. package/framework/MORPH.md +5 -8
  9. package/framework/agents.json +14 -9
  10. package/framework/commands/morph-status.md +2 -2
  11. package/framework/docs/CLI.md +8 -2
  12. package/framework/evals/review-checklist.md +4 -1
  13. package/framework/hooks/README.md +1 -0
  14. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
  16. package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
  17. package/framework/hooks/claude-code/core/worktree-context.js +23 -0
  18. package/framework/hooks/claude-code/statusline.py +3 -2
  19. package/framework/hooks/shared/command-tokens.js +17 -0
  20. package/framework/hooks/shared/gate-event.js +18 -0
  21. package/framework/hooks/shared/next-step.js +107 -0
  22. package/framework/hooks/shared/slice-progress.js +18 -5
  23. package/framework/hooks/shared/state-writer.js +6 -1
  24. package/framework/mcp/registry.json +31 -1
  25. package/framework/rules/csharp-standards.md +5 -3
  26. package/framework/rules/dotnet-templates.md +2 -2
  27. package/framework/rules/nextjs-standards.md +1 -0
  28. package/framework/schemas/feature.schema.json +1 -0
  29. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  30. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  31. package/framework/skills/core/morph-apply/SKILL.md +4 -2
  32. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  33. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  34. package/framework/skills/core/morph-media/SKILL.md +1 -1
  35. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  36. package/framework/skills/core/morph-review/SKILL.md +1 -1
  37. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  38. package/framework/skills/core/morph-uiux/SKILL.md +2 -1
  39. package/framework/standards/STANDARDS.json +55 -8
  40. package/framework/standards/STANDARDS.meta.json +29 -1
  41. package/framework/standards/ai-agents/setup.md +2 -0
  42. package/framework/standards/architecture/engineering-lens.md +100 -0
  43. package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
  44. package/framework/standards/backend/dotnet/analyzers.md +59 -0
  45. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
  46. package/framework/standards/integration/mcp/mcp-tools.md +3 -1
  47. package/framework/templates/README.md +0 -1
  48. package/framework/templates/REGISTRY.json +0 -207
  49. package/package.json +1 -1
  50. package/src/commands/observability/cost.js +12 -0
  51. package/src/commands/project/status.js +6 -68
  52. package/src/commands/project/worktree.js +1 -1
  53. package/src/commands/state/advance.js +1 -1
  54. package/src/commands/state/archive.js +1 -1
  55. package/src/commands/state/finish.js +1 -1
  56. package/src/commands/state/gate-check.js +2 -2
  57. package/src/commands/state/gate-decision.js +4 -1
  58. package/src/commands/state/next.js +24 -0
  59. package/src/commands/state/report-check.js +3 -1
  60. package/src/commands/validation/verify.js +6 -1
  61. package/src/core/state/state-manager.js +4 -0
  62. package/src/core/templates/template-registry.js +1 -43
  63. package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
  64. package/src/dashboard/scanner/features-scanner.js +5 -2
  65. package/src/lib/generators/handoff-generator.js +11 -25
  66. package/src/lib/next-step.js +10 -0
  67. package/src/lib/risk-detector.js +18 -7
  68. package/src/lib/stack/stack-scan.js +17 -7
  69. package/src/lib/telemetry/metrics.js +46 -0
  70. package/src/lib/validators/verify-runner.js +1 -1
  71. package/src/utils/agents-installer.js +24 -1
  72. package/src/utils/hooks-installer.js +1 -1
  73. package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
  74. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
  75. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
  76. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
  77. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
  78. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
  79. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "9.2.0",
3
+ "version": "9.4.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -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
- nextSteps: getNextSteps(feature)
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 nextSteps = getNextSteps(feature);
262
- if (nextSteps.length > 0) {
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-spec finish: clear ${feature} worktree registration`, '--', featPathspec], gitOpts);
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 PARCIAL:`));
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-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
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-spec finish: merge ${expectedBranch}`;
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
- '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
+ '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
- export function getAllTemplates(projectPath = null) {
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 { counts: { total: progress.total, done: progress.done, partial: progress.partial, pending: progress.pending }, list };
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 || []);