@polymorphism-tech/morph-spec 8.47.0 → 9.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.
Files changed (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The reading material of the framework — the charter, the runtime
3
- * instructions, the CLI reference and the Gate 3 evaluator checklist — for the
3
+ * instructions, the CLI reference and the Gate 3 review checklist — for the
4
4
  * dashboard's Guide page.
5
5
  */
6
6
  import { readFileSync } from 'fs';
@@ -27,7 +27,7 @@ const GUIDE = [
27
27
  { id: 'morph', path: 'MORPH.md', title: 'Carta do harness (MORPH.md)', summary: 'Como o Claude trabalha: papéis, fases, gates, sub-agents. É a fonte única do comportamento.' },
28
28
  { id: 'runtime', path: 'CLAUDE.md', title: 'Instruções de runtime (CLAUDE.md)', summary: 'O que é injetado no CLAUDE.md do projeto: como começar cada sessão, regras críticas, comandos.' },
29
29
  { id: 'cli', path: 'docs/CLI.md', title: 'Referência do CLI', summary: 'Todos os comandos morph-spec: gates, verify, worktrees, grafo, telemetria.' },
30
- { id: 'review-checklist', path: 'evals/review-checklist.md', title: 'Checklist do avaliador (Gate 3)', summary: 'O que o avaliador independente procura antes do ship: veredito SHIP/FIX e bloqueador com evidência.' },
30
+ { id: 'review-checklist', path: 'evals/review-checklist.md', title: 'Checklist do Gate 3', summary: 'O que verificador, revisor e segurança procuram antes do ship: veredito SHIP/FIX e bloqueador com evidência.' },
31
31
  ];
32
32
 
33
33
  export function scanDocs(frameworkDir) {
@@ -6,9 +6,9 @@
6
6
  * - which tree owns a feature → `resolveFeatureTree` (feature-truth.js)
7
7
  * - the current phase → `derivePhaseFrom` (state-reader.js)
8
8
  * - gate order → `GATE_PREREQ` (gate-order.js)
9
- * - the evaluator's verdict → `evaluatorVerdictFromText` (evaluator-report.js)
9
+ * - the review verdict → `reviewReportsFromDir` (review-reports.js)
10
10
  * - path confinement → `canonicalPath`/`isInside` (test-runner.js)
11
- * - tasks.json validity → `validateTasksJson` (tasks-json-parser.js)
11
+ * - slice progress → `sliceProgress` (slice-progress.js)
12
12
  *
13
13
  * Three places a feature can live: the root `.morph/features/{f}`, a worktree
14
14
  * `{worktrees}/{wt}/.morph/features/{f}`, and `.morph/archive/{f}` once
@@ -21,10 +21,12 @@
21
21
 
22
22
  import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
23
23
  import { join, basename, isAbsolute } from 'path';
24
- import { derivePhaseFrom, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
24
+ import { derivePhaseFrom, diskArtifactProbe, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
25
25
  import { GATE_PREREQ } from '../../core/state/gate-order.js';
26
- import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
27
- import { evaluatorVerdictFromText } from '../../lib/evaluator-report.js';
26
+ import { sliceProgress, checksPath } from '../../../framework/hooks/shared/slice-progress.js';
27
+ import {
28
+ reviewReportsFromDir, requiredReviewRoles, combinedVerdict, allBlockers, checksCoverage, reviewFileName, REVIEW_ROLES,
29
+ } from '../../lib/review-reports.js';
28
30
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
29
31
  import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js';
30
32
  import { canonicalPath, isInside, readMorphConfig } from '../../lib/tasks/test-runner.js';
@@ -33,7 +35,6 @@ import { isFinished } from './feature-status.js';
33
35
  export const GATES = Object.keys(GATE_PREREQ);
34
36
  const FINISHED = new Set(['completed', 'abandoned']);
35
37
  const FOLDER_PHASE = Object.fromEntries(PHASE_ARTIFACTS.map(([folder, phase]) => [folder, phase]));
36
- const EVALUATOR_REPORT = '4-review/evaluator-report.md';
37
38
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
38
39
  const MAX_DEPTH = 4;
39
40
 
@@ -141,31 +142,26 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
141
142
  }
142
143
 
143
144
  /**
144
- * tasks.json as the dashboard needs it: the list even when the file breaks the
145
- * schema (a read-only viewer shows what is there), with the errors alongside.
145
+ * The plan's slices with the status their reports give, plus the fields the
146
+ * extraction carries for the card. `null` when the plan has not been checked yet.
146
147
  */
147
- export function readTasks(dir) {
148
- const path = join(dir, '2-plan', 'tasks.json');
149
- if (!existsSync(path)) return { list: [], errors: [], present: false };
150
- const data = readJson(path);
151
- if (!data) return { list: [], errors: ['tasks.json não é JSON válido'], present: true };
152
- const { valid, errors } = validateTasksJson(data);
153
- const list = Array.isArray(data.tasks) ? data.tasks.filter((t) => t && typeof t === 'object') : [];
154
- return { list, errors: valid ? [] : errors, present: true };
148
+ export function readSlices(dir) {
149
+ const progress = sliceProgress(dir);
150
+ if (!progress) return null;
151
+ const extracted = new Map((readJson(checksPath(dir))?.slices || []).filter((x) => x && x.id).map((x) => [x.id, x]));
152
+ const list = progress.slices.map((sl) => {
153
+ const x = extracted.get(sl.id) || {};
154
+ return {
155
+ ...sl,
156
+ persona: x.persona ?? null,
157
+ files: Array.isArray(x.files) ? x.files : [],
158
+ covers: Array.isArray(x.covers) ? x.covers : [],
159
+ };
160
+ });
161
+ return { counts: { total: progress.total, done: progress.done, partial: progress.partial, pending: progress.pending }, list };
155
162
  }
156
163
 
157
- function taskCounts(list) {
158
- const counts = { total: list.length, done: 0, inProgress: 0, blocked: 0, pending: 0, skipped: 0 };
159
- for (const t of list) {
160
- // `completed` is the pre-schema spelling of `done` (MORPH_OS archives carry both).
161
- if (t.status === 'done' || t.status === 'completed') counts.done += 1;
162
- else if (t.status === 'in_progress') counts.inProgress += 1;
163
- else if (t.status === 'blocked') counts.blocked += 1;
164
- else if (t.status === 'skipped') counts.skipped += 1;
165
- else counts.pending += 1;
166
- }
167
- return counts;
168
- }
164
+ const NO_SLICES = { total: 0, done: 0, partial: 0, pending: 0 };
169
165
 
170
166
  function gatesOf(feature) {
171
167
  const skipped = new Set(feature.skippedPhases || []);
@@ -185,20 +181,67 @@ function gatesOf(feature) {
185
181
  });
186
182
  }
187
183
 
188
- function evaluatorVerdict(dir) {
189
- const path = join(dir, ...EVALUATOR_REPORT.split('/'));
190
- if (!existsSync(path)) return null;
191
- try { return evaluatorVerdictFromText(readFileSync(path, 'utf8')); } catch { return null; }
184
+ // Security counts only when its report exists: deciding it is required takes git.
185
+ function reviewVerdict(dir, workType, reports = reviewReportsFromDir(dir)) {
186
+ const present = Object.values(reports).some((r) => r.status !== 'missing');
187
+ if (!present) return null;
188
+ const required = requiredReviewRoles({ workType, securityRequired: reports.security.status !== 'missing' });
189
+ return { verdict: combinedVerdict(reports, required), blockers: allBlockers(reports) };
190
+ }
191
+
192
+ /**
193
+ * One row per review report the feature asks for or already has: status,
194
+ * verdict, blockers and the SHA it reviewed.
195
+ */
196
+ export function reviewRows(reports, workType) {
197
+ const required = requiredReviewRoles({ workType, securityRequired: reports.security.status !== 'missing' });
198
+ return Object.keys(REVIEW_ROLES)
199
+ .filter((role) => required.includes(role) || reports[role].status !== 'missing')
200
+ .map((role) => {
201
+ const r = reports[role];
202
+ return {
203
+ role,
204
+ path: `4-review/${reviewFileName(role)}`,
205
+ status: r.status,
206
+ verdict: r.verdict ?? null,
207
+ blockers: r.blockers?.length ?? 0,
208
+ reviewedSha: r.reviewedSha ?? null,
209
+ };
210
+ });
211
+ }
212
+
213
+ /**
214
+ * The plan's scenarios, each with the verifier's row for it. `coverage` is
215
+ * `null` until a verifier report exists; only PROVADO covers.
216
+ */
217
+ export function readChecks(dir, verifier) {
218
+ const checks = readJson(checksPath(dir))?.checks;
219
+ if (!Array.isArray(checks)) return null;
220
+ const list = checks.filter((c) => c && c.id);
221
+ const rows = verifier?.checks ?? null;
222
+ const byId = new Map((rows ?? []).map((r) => [r.id, r]));
223
+ const coverage = verifier && verifier.status !== 'missing'
224
+ ? { total: list.length, ...checksCoverage(list.map((c) => c.id), rows) }
225
+ : null;
226
+ return {
227
+ coverage,
228
+ list: list.map((c) => ({
229
+ id: c.id,
230
+ text: c.text ?? '',
231
+ proof: c.proof ?? null,
232
+ status: byId.get(c.id)?.status ?? null,
233
+ evidence: byId.get(c.id)?.evidence ?? null,
234
+ })),
235
+ };
192
236
  }
193
237
 
194
238
  function summarize(name, resolved) {
195
239
  const { location, dir, ownerTree, rootStale } = resolved;
196
240
  const feature = normalizeFeature(resolved.feature);
197
- const has = (rel) => existsSync(join(dir, ...rel.split('/')));
198
241
  const gates = gatesOf(feature);
199
242
  const reviewApproved = gates.find((g) => g.gate === 'review')?.approved === true;
200
- const tasks = readTasks(dir);
201
- const verdict = evaluatorVerdict(dir);
243
+ const slices = readSlices(dir);
244
+ const verdict = reviewVerdict(dir, feature.workType);
202
245
  const status = feature.status || (location === 'archive' ? 'completed' : 'draft');
203
246
  const finished = isFinished({ status, location });
204
247
  return {
@@ -210,19 +253,19 @@ function summarize(name, resolved) {
210
253
  dir,
211
254
  ownerTree,
212
255
  rootStale,
213
- phase: derivePhaseFrom(feature, has),
214
- inEvaluation: has(EVALUATOR_REPORT) && !reviewApproved,
256
+ phase: derivePhaseFrom(feature, diskArtifactProbe(dir)),
257
+ inEvaluation: verdict !== null && !reviewApproved,
215
258
  gates,
216
259
  awaiting: !finished && gates.some((g) => g.paused),
217
- tasks: taskCounts(tasks.list),
218
- // Titles of an active feature's tasks, for the global search. Finished
260
+ slices: slices?.counts ?? NO_SLICES,
261
+ // Titles of an active feature's slices, for the global search. Finished
219
262
  // features leave them out: 170 archived plans would bloat every list reply.
220
- taskIndex: finished ? [] : tasks.list.map((t) => ({ id: t.id, title: t.title || '', status: t.status || 'pending' })),
263
+ sliceIndex: finished || !slices ? [] : slices.list.map((sl) => ({ id: sl.id, title: sl.title, status: sl.status })),
221
264
  verification: feature.verification
222
- ? { status: feature.verification.status ?? null, scope: feature.verification.scope ?? null, ts: feature.verification.ts ?? null }
265
+ ? { status: feature.verification.status ?? null, ts: feature.verification.ts ?? null }
223
266
  : null,
224
267
  verdict: verdict?.verdict ?? null,
225
- blockers: verdict?.verdict ? verdict.blockers.length : null,
268
+ blockers: verdict ? verdict.blockers.length : null,
226
269
  createdAt: feature.createdAt || null,
227
270
  updatedAt: feature.updatedAt || feature.archivedAt || feature.createdAt || null,
228
271
  archivedAt: feature.archivedAt || null,
@@ -309,24 +352,21 @@ export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } =
309
352
  };
310
353
  }
311
354
 
312
- /**
313
- * One feature in full: summary + tasks with their verify, the gate
314
- * decisions, the verify nodes and the document tree.
315
- */
316
355
  export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatureTree, known } = {}) {
317
356
  const resolved = resolveOne(projectDir, name, resolveTree, known);
318
357
  if (!resolved) return null;
319
358
  const summary = summarize(name, resolved);
320
359
  const feature = normalizeFeature(resolved.feature);
321
- const tasks = readTasks(resolved.dir);
322
- const verdict = evaluatorVerdict(resolved.dir);
360
+ const slices = readSlices(resolved.dir);
361
+ const reports = reviewReportsFromDir(resolved.dir);
362
+ const verdict = reviewVerdict(resolved.dir, feature.workType, reports);
363
+ const checks = readChecks(resolved.dir, reports.verifier);
323
364
  return {
324
365
  ...summary,
325
- tasksList: tasks.list.map((t) => ({
326
- ...t,
327
- verification: feature.taskVerifications?.[t.id] || null,
328
- })),
329
- tasksErrors: tasks.errors,
366
+ slicesList: slices?.list ?? [],
367
+ checksList: checks?.list ?? [],
368
+ checksCoverage: checks?.coverage ?? null,
369
+ reviews: reviewRows(reports, feature.workType),
330
370
  gateDecisions: feature.gateDecisions || {},
331
371
  verificationNodes: feature.verification?.nodes || null,
332
372
  featureVerification: feature.verifications?.feature || null,
@@ -336,13 +376,8 @@ export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatu
336
376
  };
337
377
  }
338
378
 
339
- /**
340
- * The content of one file of a feature, confined to the feature's directory.
341
- * Junctions and win32 case are resolved by `canonicalPath` before the check,
342
- * so `..`, an absolute path or a link that leaves the folder are all refused.
343
- *
344
- * @returns {{ path: string, content: string, mtime: string } | { error: string, status: number }}
345
- */
379
+ // Junctions and win32 case are resolved by `canonicalPath` before the check,
380
+ // so `..`, an absolute path or a link that leaves the feature folder are refused.
346
381
  export function readFeatureFile(projectDir, name, relPath, { resolveTree = resolveFeatureTree, known } = {}) {
347
382
  if (typeof relPath !== 'string' || relPath === '') return { error: 'path ausente', status: 400 };
348
383
  if (isAbsolute(relPath) || /^[a-zA-Z]:/.test(relPath)) return { error: 'path fora da feature', status: 403 };
@@ -89,7 +89,7 @@ export function createProjectApi(projectDir, deps = {}) {
89
89
 
90
90
  // Which tree owns a feature costs a few git calls (~0.3 s each on Windows)
91
91
  // and only changes when a file or folder appears or disappears — never when
92
- // an agent rewrites feature.json or tasks.json, which is what happens all
92
+ // an agent rewrites feature.json or a slice report, which is what happens all
93
93
  // the time. Remembered per feature; a structural change or TTL_MS forgets it.
94
94
  const resolveTree = deps.resolveTree || resolveFeatureTree;
95
95
  const now = deps.now || Date.now;
@@ -1,11 +1,12 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { personaStandardIds } from '../../utils/agents-installer.js';
4
+ import { loadStandardsRegistry } from '../../lib/standards/registry.js';
4
5
 
5
6
  export function scanStandards(frameworkDir, agents) {
6
7
  const standardsJsonPath = join(frameworkDir, 'standards', 'STANDARDS.json');
7
8
  if (!existsSync(standardsJsonPath)) return { version: 'unknown', list: [] };
8
- const sJson = JSON.parse(readFileSync(standardsJsonPath, 'utf8'));
9
+ const sJson = loadStandardsRegistry(standardsJsonPath);
9
10
  const refMap = {};
10
11
  for (const agent of agents) {
11
12
  for (const std of personaStandardIds(agent)) {
@@ -18,7 +18,7 @@ export class CiUsageError extends Error {}
18
18
  const NODE_KEYS = ['build', 'tests', 'validators', 'e2e'];
19
19
 
20
20
  function isVerifyResult(result) {
21
- return Boolean(result) && ['pass', 'fail'].includes(result.status) && result.scope === 'feature'
21
+ return Boolean(result) && ['pass', 'fail'].includes(result.status)
22
22
  && NODE_KEYS.every((k) => typeof result.nodes?.[k]?.status === 'string');
23
23
  }
24
24
 
@@ -47,6 +47,22 @@ function dispatchWorkflow(root, workflow, branch) {
47
47
  }
48
48
  }
49
49
 
50
+ /**
51
+ * When CI falls back to local, the last green feature stamp still proves the tree if nothing moved
52
+ * since it by the gate's own staleness rule and nothing is uncommitted (treeMovedSince sees only
53
+ * commits). A null sha or HEAD would read as "not moved", so both are required.
54
+ *
55
+ * @returns {object|null} the stamp to reuse, or null = run local
56
+ */
57
+ export function reusableLocalStamp(root, stamp) {
58
+ if (stamp?.status !== 'pass' || !stamp.sha) return null;
59
+ const head = getHeadSha(root);
60
+ if (!head) return null;
61
+ const dirty = uncommittedCode(root);
62
+ if (dirty === null || dirty.length > 0) return null;
63
+ return treeMovedSince(root, stamp.sha, head) ? null : stamp;
64
+ }
65
+
50
66
  /**
51
67
  * `verify {f} --ci`: the feature-scope proof, run by the project's CI over the pushed commit.
52
68
  * Every way CI cannot answer is `mode: 'local'` with the reason, and the caller runs the local
@@ -140,7 +140,7 @@ export function listRuns(root, branch, workflow) {
140
140
 
141
141
  /**
142
142
  * The newest run over the same code as HEAD. `isCurrent` is a tree comparison, not SHA equality:
143
- * the evaluator and the archive commit only `.morph/`, and the workflow does not re-run for them.
143
+ * the review agents and the archive commit only `.morph/`, and the workflow does not re-run for them.
144
144
  */
145
145
  export function pickRun(runs, isCurrent) {
146
146
  return (runs || []).find((run) => !(run.status === 'completed' && NO_VERDICT.has(run.conclusion))
@@ -10,14 +10,9 @@
10
10
  import { existsSync, readFileSync, readdirSync } from 'fs';
11
11
  import { join } from 'path';
12
12
 
13
- /**
14
- * Common monorepo locations for a frontend `package.json`, root included.
15
- *
16
- * Exported so `discoverFrontendPackages()` (src/lib/tasks/test-nodes.js, #87
17
- * PR B) scans the SAME candidate list this detector already uses — one
18
- * spelling of "where a frontend package might live", not two that drift.
19
- */
20
- export const PACKAGE_LOCATIONS = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
13
+ import { PACKAGE_LOCATIONS } from '../../../framework/hooks/shared/frontend-locations.js';
14
+
15
+ export { PACKAGE_LOCATIONS };
21
16
 
22
17
  /**
23
18
  * Detect project structure from filesystem evidence.
@@ -7,67 +7,43 @@
7
7
  * fresh instance starts focused instead of inheriting a wall of stale context.
8
8
  *
9
9
  * Deterministic by construction — everything here is derived from feature.json,
10
- * tasks.json, decisions.md and the worktree env manifest. The document is
10
+ * the extracted plan checks, the slice reports and the worktree env manifest. The document is
11
11
  * therefore REGENERABLE (never hand-edited, never merged): `morph-spec handoff`
12
12
  * rebuilds it, and setup/create/advance/approve regenerate it at the moments a
13
13
  * new session is most likely to pick the feature up.
14
14
  *
15
- * Every section is fail-open: a missing tasks.json or a corrupt decisions.md
16
- * degrades that section, never the document.
15
+ * Every section is fail-open: missing or corrupt plan checks degrade that
16
+ * section, never the document.
17
17
  */
18
18
 
19
19
  import { existsSync, readFileSync, writeFileSync } from 'fs';
20
20
  import { join } from 'path';
21
21
  import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
22
22
  import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
23
-
24
- const PHASE_DIRS = {
25
- proposal: '0-proposal',
26
- uiux: '1-design',
27
- plan: '2-plan',
28
- implement: '3-implement',
29
- review: '4-review',
30
- };
23
+ import { sliceProgress, readySlices } from '../../../framework/hooks/shared/slice-progress.js';
31
24
 
32
25
  /** Ordered pointer candidates — only existing paths make it into the doc. */
33
26
  const POINTER_CANDIDATES = [
34
- ['0-proposal/proposal.md', 'proposta: problema, escopo e riscos'],
35
- // Behaviour before solution — the same order morph-plan writes them in.
36
- ['2-plan/usecases', 'use cases: comportamento e pós-condições testáveis (pasta)'],
37
- ['2-plan/spec.md', 'especificação técnica'],
38
- ['2-plan/tasks.json', 'DAG de tasks (status vivo)'],
39
- ['decisions.md', 'decisões registradas'],
40
- ['3-implement/recap.md', 'recap da implementação'],
41
- ['4-review/review-report.md', 'relatório do Gate 3'],
27
+ ['2-plan/plan.md', 'plano: problema, cenários com prova, decisões e fatias'],
28
+ ['2-plan/checks.json', 'cenários e fatias extraídos pelo plan-check'],
29
+ ['1-design/design-system.md', 'design system da feature'],
30
+ ['3-implement/reports', 'relatórios das fatias (**Arquivos:**/**Standards:**): o registro da implementação'],
31
+ ['4-review/verifier.md', 'verificador do Gate 3: cada cenário provado'],
32
+ ['4-review/reviewer.md', 'revisor do Gate 3: escopo, arquitetura, regressão'],
33
+ ['4-review/security.md', 'segurança do Gate 3 (só com caminho sensível)'],
42
34
  ];
43
35
 
44
36
  function readJson(path) {
45
37
  return JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
46
38
  }
47
39
 
48
- function taskSummary(featureDir) {
49
- try {
50
- const tasksPath = join(featureDir, '2-plan', 'tasks.json');
51
- if (!existsSync(tasksPath)) return null;
52
- const data = readJson(tasksPath);
53
- if (!Array.isArray(data.tasks)) return null;
54
- const done = data.tasks.filter((t) => t.status === 'done').length;
55
- const doneIds = new Set(data.tasks.filter((t) => t.status === 'done').map((t) => t.id));
56
- const ready = data.tasks.filter(
57
- (t) => t.status === 'pending' && Array.isArray(t.dependencies) && t.dependencies.every((d) => doneIds.has(d)),
58
- );
59
- return { done, total: data.tasks.length, ready };
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
40
  function decisionTitles(featureDir, limit = 5) {
66
41
  try {
67
- const path = join(featureDir, 'decisions.md');
42
+ const path = join(featureDir, '2-plan', 'checks.json');
68
43
  if (!existsSync(path)) return [];
69
- const titles = [...readFileSync(path, 'utf8').matchAll(/^##\s+(.+)$/gm)].map((m) => m[1].trim());
70
- return titles.slice(-limit);
44
+ const adrs = readJson(path).adrs;
45
+ if (!Array.isArray(adrs)) return [];
46
+ return adrs.map((a) => `${a.id} — ${a.title}`).slice(-limit);
71
47
  } catch {
72
48
  return [];
73
49
  }
@@ -86,34 +62,27 @@ function readWorktreeEnv(cwd) {
86
62
  }
87
63
  }
88
64
 
89
- function nextAction(feature, phase, tasks, featureName) {
65
+ function nextAction(feature, phase, slices, featureName) {
90
66
  const gates = feature.approvals || feature.approvalGates || {};
91
67
  const approved = (g) => gates[g]?.approved === true;
92
68
 
93
69
  switch (phase) {
94
- case 'proposal':
95
- // An approved Gate 1 with no plan artifact yet means "plan work is open":
96
- // the derived phase only flips when 2-plan/spec.md lands (derivePhase is
97
- // artifact-first). Pointing back at the proposal here would be wrong.
98
- return approved('proposal')
99
- ? `Gate 1 aprovado — produza \`2-plan/spec.md\` + \`tasks.json\` (a fase avança com o primeiro artefato; Gate 2 em seguida).`
100
- : `Complete \`0-proposal/proposal.md\` e aprove o Gate 1 (\`morph-spec advance ${featureName}\`). Skill: /morph-proposal ${featureName}.`;
70
+ case 'plan':
71
+ return `Escreva \`2-plan/plan.md\`, rode \`morph-spec plan-check ${featureName}\` até zero erro e aprove o gate do plano (\`morph-spec advance ${featureName}\`). Skill: /morph-proposal ${featureName}.`;
101
72
  case 'uiux':
102
73
  return approved('uiux')
103
- ? `Gate de uiux aprovado — produza \`2-plan/spec.md\` + \`tasks.json\` (Gate 2 em seguida).`
74
+ ? `Gate de uiux aprovado — implemente as fatias do plano via /morph-apply ${featureName}.`
104
75
  : `Complete \`1-design/design-system.md\` e aprove o gate de uiux (\`morph-spec advance ${featureName}\`).`;
105
- case 'plan':
106
- return `Produza \`2-plan/spec.md\` + \`tasks.json\` e aprove o Gate 2 (\`morph-spec advance ${featureName}\`).`;
107
76
  case 'implement': {
108
- const ready = tasks?.ready?.map((t) => `${t.id} "${t.title}"`).join(', ');
77
+ const ready = readySlices(slices).map((s) => `${s.id} "${s.title}"`).join(', ');
109
78
  return ready
110
- ? `Execute as tasks ready — ${ready} — via /morph-apply ${featureName} (task fecha com verify verde e critérios provados).`
111
- : `Todas as tasks concluídas ou bloqueadas — rode \`morph-spec advance ${featureName}\` para escancarar a review, ou destrave dependências. Skill: /morph-apply ${featureName}.`;
79
+ ? `Implemente as fatias prontas — ${ready} — via /morph-apply ${featureName} (cada fatia grava 3-implement/reports/{id}.md).`
80
+ : `Todas as fatias têm relatório final — rode \`morph-spec verify ${featureName}\` e siga para a review. Skill: /morph-apply ${featureName}.`;
112
81
  }
113
82
  case 'review':
114
83
  return approved('review')
115
84
  ? `Gate 3 aprovado — feche: \`morph-spec finish ${featureName} --pr\` (abre PR) ou \`--merge\` (merge local na default). Arquiva + integra num passo.`
116
- : `Produza \`4-review/review-report.md\` (avaliador independente) e aprove o Gate 3; depois \`morph-spec finish ${featureName} --pr|--merge\`.`;
85
+ : `Rode /morph-review ${featureName} (verificador ∥ revisor, segurança se exigida) e aprove o Gate 3; depois \`morph-spec finish ${featureName} --pr|--merge\`.`;
117
86
  default:
118
87
  return `Rode \`morph-spec status ${featureName}\` para orientar o próximo passo.`;
119
88
  }
@@ -131,7 +100,7 @@ function nextAction(feature, phase, tasks, featureName) {
131
100
  * copy at birth (create.js) — a handoff built FROM the worktree's own copy
132
101
  * would otherwise always say "no worktree registered", even while sitting
133
102
  * inside one. Pass `undefined` (the default) to fall back to `feature.worktree`.
134
- * @returns {{ markdown: string, meta: { generatedAt: string, phase: string, tasksDone: number, tasksTotal: number } } | null}
103
+ * @returns {{ markdown: string, meta: { generatedAt: string, phase: string, slicesDone: number, slicesTotal: number } } | null}
135
104
  */
136
105
  export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
137
106
  const featureDir = join(cwd, '.morph', 'features', featureName);
@@ -144,15 +113,15 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
144
113
  return null;
145
114
  }
146
115
 
147
- let phase = 'proposal';
116
+ let phase = 'plan';
148
117
  let pendingGates = [];
149
118
  try {
150
- phase = derivePhaseForFeature(featureName, cwd) || 'proposal';
119
+ phase = derivePhaseForFeature(featureName, cwd) || 'plan';
151
120
  pendingGates = getPendingGates(featureName, cwd) || [];
152
- } catch { /* fail-open — proposal defaults keep the doc useful */ }
121
+ } catch { /* fail-open — plan defaults keep the doc useful */ }
153
122
 
154
123
  const generatedAt = new Date().toISOString();
155
- const tasks = taskSummary(featureDir);
124
+ const slices = sliceProgress(featureDir);
156
125
  const env = readWorktreeEnv(cwd);
157
126
  const decisions = decisionTitles(featureDir);
158
127
  const worktree = opts.worktree !== undefined ? opts.worktree : (feature.worktree || null);
@@ -187,9 +156,9 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
187
156
  '## Estado',
188
157
  '',
189
158
  `- Gates: ${gateLine}`,
190
- tasks
191
- ? `- Tasks: ${tasks.done}/${tasks.total} done · ready agora: ${tasks.ready.length ? tasks.ready.map((t) => `**${t.id}** "${t.title}"`).join(', ') : 'nenhuma'}`
192
- : '- Tasks: sem tasks.json ainda (fase de planejamento não concluída)',
159
+ slices
160
+ ? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
161
+ : '- Fatias: sem checks.json ainda (rode plan-check)',
193
162
  pendingGates.length ? `- Gates pendentes: ${pendingGates.join(', ')}` : null,
194
163
  '',
195
164
  '## Leia nesta ordem (ponteiros — nada copiado)',
@@ -200,7 +169,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
200
169
  '',
201
170
  '## Próxima ação',
202
171
  '',
203
- nextAction(feature, phase, tasks, featureName),
172
+ nextAction(feature, phase, slices, featureName),
204
173
  '',
205
174
  '## Retomar',
206
175
  '',
@@ -214,8 +183,8 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
214
183
  meta: {
215
184
  generatedAt,
216
185
  phase,
217
- tasksDone: tasks?.done ?? 0,
218
- tasksTotal: tasks?.total ?? 0,
186
+ slicesDone: slices?.done ?? 0,
187
+ slicesTotal: slices?.total ?? 0,
219
188
  },
220
189
  };
221
190
  }