@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.
- package/CLAUDE.md +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- 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
|
|
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
|
|
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
|
|
9
|
+
* - the review verdict → `reviewReportsFromDir` (review-reports.js)
|
|
10
10
|
* - path confinement → `canonicalPath`/`isInside` (test-runner.js)
|
|
11
|
-
* -
|
|
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 {
|
|
27
|
-
import {
|
|
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
|
-
*
|
|
145
|
-
*
|
|
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
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
201
|
-
const verdict =
|
|
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,
|
|
214
|
-
inEvaluation:
|
|
256
|
+
phase: derivePhaseFrom(feature, diskArtifactProbe(dir)),
|
|
257
|
+
inEvaluation: verdict !== null && !reviewApproved,
|
|
215
258
|
gates,
|
|
216
259
|
awaiting: !finished && gates.some((g) => g.paused),
|
|
217
|
-
|
|
218
|
-
// Titles of an active feature's
|
|
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
|
-
|
|
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,
|
|
265
|
+
? { status: feature.verification.status ?? null, ts: feature.verification.ts ?? null }
|
|
223
266
|
: null,
|
|
224
267
|
verdict: verdict?.verdict ?? null,
|
|
225
|
-
blockers: verdict
|
|
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
|
|
322
|
-
const
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
9
|
+
const sJson = loadStandardsRegistry(standardsJsonPath);
|
|
9
10
|
const refMap = {};
|
|
10
11
|
for (const agent of agents) {
|
|
11
12
|
for (const std of personaStandardIds(agent)) {
|
package/src/lib/ci/ci-verify.js
CHANGED
|
@@ -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)
|
|
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
|
package/src/lib/ci/github-ci.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
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:
|
|
16
|
-
*
|
|
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
|
-
['
|
|
35
|
-
|
|
36
|
-
['
|
|
37
|
-
['
|
|
38
|
-
['
|
|
39
|
-
['
|
|
40
|
-
['
|
|
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, '
|
|
42
|
+
const path = join(featureDir, '2-plan', 'checks.json');
|
|
68
43
|
if (!existsSync(path)) return [];
|
|
69
|
-
const
|
|
70
|
-
|
|
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,
|
|
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 '
|
|
95
|
-
|
|
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 —
|
|
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 =
|
|
77
|
+
const ready = readySlices(slices).map((s) => `${s.id} "${s.title}"`).join(', ');
|
|
109
78
|
return ready
|
|
110
|
-
? `
|
|
111
|
-
: `Todas as
|
|
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
|
-
: `
|
|
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,
|
|
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 = '
|
|
116
|
+
let phase = 'plan';
|
|
148
117
|
let pendingGates = [];
|
|
149
118
|
try {
|
|
150
|
-
phase = derivePhaseForFeature(featureName, cwd) || '
|
|
119
|
+
phase = derivePhaseForFeature(featureName, cwd) || 'plan';
|
|
151
120
|
pendingGates = getPendingGates(featureName, cwd) || [];
|
|
152
|
-
} catch { /* fail-open —
|
|
121
|
+
} catch { /* fail-open — plan defaults keep the doc useful */ }
|
|
153
122
|
|
|
154
123
|
const generatedAt = new Date().toISOString();
|
|
155
|
-
const
|
|
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
|
-
|
|
191
|
-
? `-
|
|
192
|
-
: '-
|
|
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,
|
|
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
|
-
|
|
218
|
-
|
|
186
|
+
slicesDone: slices?.done ?? 0,
|
|
187
|
+
slicesTotal: slices?.total ?? 0,
|
|
219
188
|
},
|
|
220
189
|
};
|
|
221
190
|
}
|