@polymorphism-tech/morph-spec 8.46.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 +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- 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/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- 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 +17 -19
- 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 +67 -31
- 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 +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- 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/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- 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/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- 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/observability/telemetry.js +3 -5
- 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 +6 -1
- 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 +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- 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 +86 -95
- 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 +41 -82
- package/src/core/paths/output-schema.js +20 -67
- 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/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- 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/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- 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 +134 -135
- 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-profile.js +1 -1
- 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 +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- 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/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- 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/project/validate.js +0 -97
- 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/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- 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
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -6,19 +6,9 @@
|
|
|
6
6
|
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
7
|
*
|
|
8
8
|
* Blocks Write/Edit to spec artifacts after their approval gate has passed:
|
|
9
|
-
* - 2-plan/
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
|
|
13
|
-
* the plan contract is frozen, but per-task progress fields (status, outputs,
|
|
14
|
-
* notes) may still change during the implement phase. For those files BOTH a
|
|
15
|
-
* full Write and a targeted Edit are allowed — but only when the semantic diff
|
|
16
|
-
* touches solely the mutable fields. The Edit is simulated (old_string →
|
|
17
|
-
* new_string applied to the current file) and diffed exactly like a Write, so a
|
|
18
|
-
* one-field status flip via Edit is permitted while any change to the frozen
|
|
19
|
-
* plan contract (titles, dependencies, doneCriteria, the task set) is blocked.
|
|
20
|
-
* This is the sanctioned progress path — the agent never has to rewrite the
|
|
21
|
-
* whole file nor reach for a Node script to record task status.
|
|
9
|
+
* - 2-plan/plan.md → after 'plan', only an append under `## Errata`
|
|
10
|
+
* - 2-plan/checks.json → never by hand: `morph-spec plan-check` is its only writer
|
|
11
|
+
* - 1-design/design-system.md, etc. → after 'uiux', only an errata append
|
|
22
12
|
*
|
|
23
13
|
* A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
|
|
24
14
|
* exist on disk is a CREATION and passes — the criterion is the file's
|
|
@@ -33,152 +23,14 @@ import {
|
|
|
33
23
|
isFeaturePath,
|
|
34
24
|
extractFeatureName,
|
|
35
25
|
getBasename,
|
|
36
|
-
PROTECTED_SPEC_FILES
|
|
37
|
-
MUTABLE_SPEC_FIELDS
|
|
26
|
+
PROTECTED_SPEC_FILES
|
|
38
27
|
} from '../../shared/phase-utils.js';
|
|
39
28
|
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
40
29
|
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
41
30
|
import { getFilePath, getToolName, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Compare two tasks.json structures and return the list of plan-contract
|
|
46
|
-
* violations — changes that touch anything other than the mutable fields.
|
|
47
|
-
*
|
|
48
|
-
* Frozen-by-default, not an enumerated frozen list: the loop below walks the
|
|
49
|
-
* UNION of old/new task keys and flags any key not in `mutableFields`. A new
|
|
50
|
-
* plan-contract field (e.g. `agent`/`standards`, stamped by morph-plan — R2
|
|
51
|
-
* token-cut) needs NO entry added here to become protected; it is frozen the
|
|
52
|
-
* moment it exists on a task, simply by never being added to
|
|
53
|
-
* MUTABLE_SPEC_FIELDS (output-schema.js → `npm run generate` →
|
|
54
|
-
* framework/hooks/shared/phase-utils.js). Only fields that must stay
|
|
55
|
-
* EDITABLE post-Gate-2 (today: status, outputs, notes) belong in that list.
|
|
56
|
-
*
|
|
57
|
-
* @param {object} oldDoc - parsed previous file content
|
|
58
|
-
* @param {object} newDoc - parsed proposed file content
|
|
59
|
-
* @param {string[]} mutableFields - field names allowed to change
|
|
60
|
-
* @returns {string[]} human-readable violation messages (empty = OK)
|
|
61
|
-
*/
|
|
62
|
-
function diffPlanContract(oldDoc, newDoc, mutableFields) {
|
|
63
|
-
const violations = [];
|
|
64
|
-
const mutable = new Set(mutableFields);
|
|
65
|
-
|
|
66
|
-
// Backfilling a previously-absent `version` (undefined → any value) is a
|
|
67
|
-
// benign schema-tooling update, not a plan-contract change — it's what
|
|
68
|
-
// lets legacy tasks.json adopt the field so `morph-spec dag` can analyze
|
|
69
|
-
// it without a lenient fallback. An actual change between two SET
|
|
70
|
-
// versions is still a real contract change and stays blocked.
|
|
71
|
-
const versionBackfilled = oldDoc.version === undefined && newDoc.version !== undefined;
|
|
72
|
-
if (!versionBackfilled && oldDoc.version !== newDoc.version) {
|
|
73
|
-
violations.push(`'version' changed (${oldDoc.version} → ${newDoc.version})`);
|
|
74
|
-
}
|
|
75
|
-
if (oldDoc.feature !== newDoc.feature) {
|
|
76
|
-
violations.push(`'feature' changed (${oldDoc.feature} → ${newDoc.feature})`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const oldTasks = Array.isArray(oldDoc.tasks) ? oldDoc.tasks : [];
|
|
80
|
-
const newTasks = Array.isArray(newDoc.tasks) ? newDoc.tasks : [];
|
|
81
|
-
|
|
82
|
-
if (oldTasks.length !== newTasks.length) {
|
|
83
|
-
violations.push(`task count changed (${oldTasks.length} → ${newTasks.length}) — tasks cannot be added or removed`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const oldById = new Map(oldTasks.map(t => [t.id, t]));
|
|
87
|
-
const newById = new Map(newTasks.map(t => [t.id, t]));
|
|
88
|
-
|
|
89
|
-
// A task present before must still exist (no removals / id renames).
|
|
90
|
-
for (const id of oldById.keys()) {
|
|
91
|
-
if (!newById.has(id)) violations.push(`task '${id}' was removed`);
|
|
92
|
-
}
|
|
93
|
-
for (const id of newById.keys()) {
|
|
94
|
-
if (!oldById.has(id)) violations.push(`task '${id}' was added`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// For tasks present on both sides, only mutable fields may differ.
|
|
98
|
-
for (const [id, newTask] of newById) {
|
|
99
|
-
const oldTask = oldById.get(id);
|
|
100
|
-
if (!oldTask) continue;
|
|
101
|
-
const keys = new Set([...Object.keys(oldTask), ...Object.keys(newTask)]);
|
|
102
|
-
for (const key of keys) {
|
|
103
|
-
if (mutable.has(key)) continue;
|
|
104
|
-
if (JSON.stringify(oldTask[key]) !== JSON.stringify(newTask[key])) {
|
|
105
|
-
violations.push(`task '${id}': immutable field '${key}' changed`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return violations;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* O heading que abre o bloco de errata, no fim de um arquivo congelado.
|
|
115
|
-
*
|
|
116
|
-
* Congelar o CONTRATO não é a mesma coisa que congelar um ERRO DE FATO. Caso
|
|
117
|
-
* real: a spec afirmava, entre parênteses, que certos registros também eram
|
|
118
|
-
* escritos por humanos; o código mostrava que os três escritores eram do
|
|
119
|
-
* próprio motor. A afirmação era falsa sobre o código, não uma mudança de
|
|
120
|
-
* escopo — mas a trava cega não distingue as duas coisas, então a mentira
|
|
121
|
-
* ficava na spec e a correção morava em outro arquivo. Quem abrisse a spec
|
|
122
|
-
* primeiro lia a versão errada.
|
|
123
|
-
*
|
|
124
|
-
* A errata resolve isso SEM abrir o contrato: o corpo continua byte-idêntico e
|
|
125
|
-
* só se acrescenta texto ao fim, num bloco identificado. Escopo negociado ⇒
|
|
126
|
-
* revogar o gate. Fato errado sobre o código ⇒ errata.
|
|
127
|
-
*/
|
|
128
|
-
const ERRATA_HEADING = '## Errata';
|
|
129
|
-
|
|
130
|
-
/** Índice onde começa o bloco de errata (-1 se não há). Só reconhece o heading em início de linha. */
|
|
131
|
-
function errataIndex(text) {
|
|
132
|
-
if (text.startsWith(ERRATA_HEADING)) return 0;
|
|
133
|
-
const at = text.indexOf(`\n${ERRATA_HEADING}`);
|
|
134
|
-
return at === -1 ? -1 : at + 1;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Corpo congelado = tudo ANTES do bloco de errata (ou o texto inteiro, se não
|
|
139
|
-
* há bloco), sem o branco final.
|
|
140
|
-
*
|
|
141
|
-
* O `trimEnd` não é frouxidão: separar o bloco por uma linha em branco é o
|
|
142
|
-
* formato markdown natural, e sem isso a primeira errata de todas seria
|
|
143
|
-
* recusada por "mudou o corpo" — quando o que mudou foi um `\n` de formatação.
|
|
144
|
-
* A garantia que importa (nenhum caractere SIGNIFICATIVO do texto aprovado
|
|
145
|
-
* muda) fica intacta, porque só espaço em branco terminal é ignorado.
|
|
146
|
-
*/
|
|
147
|
-
function frozenBody(text) {
|
|
148
|
-
const at = errataIndex(text);
|
|
149
|
-
return (at === -1 ? text : text.slice(0, at)).trimEnd();
|
|
150
|
-
}
|
|
31
|
+
import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
|
|
151
32
|
|
|
152
|
-
|
|
153
|
-
* A mudança proposta é uma errata legítima?
|
|
154
|
-
*
|
|
155
|
-
* Três condições, todas necessárias — frouxo em qualquer uma delas e a trava
|
|
156
|
-
* cega vira uma trava que uma edição criativa atravessa:
|
|
157
|
-
* 1. o corpo congelado permanece BYTE-IDÊNTICO;
|
|
158
|
-
* 2. o resultado de fato tem um bloco `## Errata` (senão é edição do corpo);
|
|
159
|
-
* 3. a errata só CRESCE — o que já estava escrito nela não é reescrito.
|
|
160
|
-
*
|
|
161
|
-
* A (3) existe porque uma errata reescrita perde a serventia: o valor dela é
|
|
162
|
-
* ser trilha acumulada, não um segundo lugar editável.
|
|
163
|
-
*
|
|
164
|
-
* @returns {{ok: true} | {ok: false, why: string}}
|
|
165
|
-
*/
|
|
166
|
-
function errataViolation(original, proposed) {
|
|
167
|
-
if (frozenBody(proposed) !== frozenBody(original)) {
|
|
168
|
-
return { ok: false, why: 'o corpo da spec mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
|
|
169
|
-
}
|
|
170
|
-
if (errataIndex(proposed) === -1) {
|
|
171
|
-
return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
|
|
172
|
-
}
|
|
173
|
-
const oldAt = errataIndex(original);
|
|
174
|
-
if (oldAt !== -1) {
|
|
175
|
-
const oldErrata = original.slice(oldAt);
|
|
176
|
-
if (!proposed.slice(errataIndex(proposed)).startsWith(oldErrata)) {
|
|
177
|
-
return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return { ok: true };
|
|
181
|
-
}
|
|
33
|
+
const CHECKS_FILE = 'checks.json';
|
|
182
34
|
|
|
183
35
|
/**
|
|
184
36
|
* Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
|
|
@@ -221,7 +73,7 @@ export async function run(payload) {
|
|
|
221
73
|
// The gate that locks this file is recorded in the feature.json of the tree
|
|
222
74
|
// that OWNS it. Asking the session's tree instead made this guard go DARK in
|
|
223
75
|
// a secondary worktree: the root had no approved gate, so a post-Gate-2
|
|
224
|
-
//
|
|
76
|
+
// plan.md rewrite sailed through. Same defect as enforce-phase-writes,
|
|
225
77
|
// opposite direction — there it over-blocked, here it under-protected.
|
|
226
78
|
const root = resolveMorphRoot(filePath, process.cwd());
|
|
227
79
|
if (!stateExists(root)) return null;
|
|
@@ -235,6 +87,19 @@ export async function run(payload) {
|
|
|
235
87
|
// Not a protected file
|
|
236
88
|
if (!requiredGate) return null;
|
|
237
89
|
|
|
90
|
+
// Only plan-check writes checks.json; a hand-written one would carry
|
|
91
|
+
// checks the parser never judged. Decided before the creation carve-out:
|
|
92
|
+
// chore/hotfix are born with the plan gate stamped.
|
|
93
|
+
if (filename === CHECKS_FILE) {
|
|
94
|
+
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
95
|
+
return blockResult(
|
|
96
|
+
`MORPH-SPEC: '${CHECKS_FILE}' is generated — never written by hand.\n` +
|
|
97
|
+
`Edit 2-plan/plan.md and run \`morph-spec plan-check ${featureName}\`: it parses the plan and writes ${CHECKS_FILE}.\n` +
|
|
98
|
+
`After the plan gate the plan is frozen: a wrong fact goes in \`${ERRATA_HEADING}\` (append-only, ${CHECKS_FILE} unchanged);\n` +
|
|
99
|
+
`a real scope change needs \`morph-spec unapprove ${featureName} plan\`, then plan-check and the gate again.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
238
103
|
// Gate not yet approved — allow editing
|
|
239
104
|
if (!isGateApproved(featureName, requiredGate, root)) {
|
|
240
105
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
@@ -247,9 +112,9 @@ export async function run(payload) {
|
|
|
247
112
|
// Um gate aprovado congela o ARTEFATO APROVADO. Se o arquivo ainda não
|
|
248
113
|
// existe no disco, não há artefato aprovado a proteger: isto é a CRIAÇÃO
|
|
249
114
|
// inicial, não a edição de uma spec fechada. O caso é real e não é raro —
|
|
250
|
-
// `chore`/`hotfix` nascem com `
|
|
115
|
+
// `chore`/`hotfix` nascem com `plan` carimbado `policy:{tipo}`
|
|
251
116
|
// (create.js → work-types.js skippedGates), então o primeiro `Write` de
|
|
252
|
-
// `2-plan/
|
|
117
|
+
// `2-plan/plan.md` já chega aqui com o gate "aprovado" sem que humano
|
|
253
118
|
// nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
|
|
254
119
|
// mandando revogar o gate de um arquivo inexistente.
|
|
255
120
|
//
|
|
@@ -259,93 +124,16 @@ export async function run(payload) {
|
|
|
259
124
|
// sobre o disco, a janela fecha sozinha no instante em que o arquivo
|
|
260
125
|
// existe — os contra-canários de test/hooks/protect-spec-files.test.js
|
|
261
126
|
// pinam exatamente isso.
|
|
262
|
-
//
|
|
263
|
-
// A decisão é explícita e vale para as DUAS metades do hook. Antes, o ramo
|
|
264
|
-
// de campos mutáveis (tasks.json) já deixava a primeira criação passar, mas
|
|
265
|
-
// por acidente — o ENOENT caía no catch fail-open da leitura — enquanto o
|
|
266
|
-
// ramo travado bloqueava. Duas metades respondendo à mesma pergunta em
|
|
267
|
-
// direções opostas; agora respondem aqui, uma vez só. O `result` distinto
|
|
268
|
-
// no log é o que torna essa unificação observável.
|
|
127
|
+
// O `result` distinto no log é o que torna a criação observável.
|
|
269
128
|
if (!existsSync(absPath)) {
|
|
270
129
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
|
|
271
130
|
return null;
|
|
272
131
|
}
|
|
273
132
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Files with mutable progress fields (e.g. tasks.json): allow a full Write
|
|
277
|
-
// that touches only those fields. Anything else stays blocked.
|
|
278
|
-
if (mutableFields) {
|
|
279
|
-
const toolName = getToolName(payload);
|
|
280
|
-
|
|
281
|
-
// Build the proposed document from either a full Write or a targeted Edit,
|
|
282
|
-
// then semantic-diff it against the current file on disk. Any failure to
|
|
283
|
-
// read / parse / apply → fail-open (pass), never a false block. O arquivo
|
|
284
|
-
// EXISTE aqui (a inexistência já foi decidida acima), então este catch
|
|
285
|
-
// volta a significar só o que dizia originalmente: não deu para diffar.
|
|
286
|
-
let oldDoc, newDoc;
|
|
287
|
-
try {
|
|
288
|
-
const original = readFileSync(absPath, 'utf-8');
|
|
289
|
-
oldDoc = JSON.parse(original);
|
|
290
|
-
|
|
291
|
-
if (toolName === 'Edit') {
|
|
292
|
-
// Simulate the edit: apply old_string → new_string to the current file.
|
|
293
|
-
const oldStr = payload?.tool_input?.old_string;
|
|
294
|
-
const newStr = payload?.tool_input?.new_string;
|
|
295
|
-
if (typeof oldStr !== 'string' || typeof newStr !== 'string') throw new Error('no edit strings');
|
|
296
|
-
const edited = payload?.tool_input?.replace_all
|
|
297
|
-
? original.split(oldStr).join(newStr)
|
|
298
|
-
: original.replace(oldStr, newStr);
|
|
299
|
-
newDoc = JSON.parse(edited);
|
|
300
|
-
} else {
|
|
301
|
-
const newContent = payload?.tool_input?.content;
|
|
302
|
-
if (typeof newContent !== 'string') throw new Error('no content');
|
|
303
|
-
newDoc = JSON.parse(newContent);
|
|
304
|
-
}
|
|
305
|
-
} catch {
|
|
306
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
|
|
311
|
-
|
|
312
|
-
// Schema check on the proposed doc: even mutable-field writes must keep a
|
|
313
|
-
// valid status enum (a leaf agent writing status:"completed" was the
|
|
314
|
-
// production failure this closes). Only NEWLY introduced schema errors
|
|
315
|
-
// block — a pre-existing violation must not deadlock progress writes.
|
|
316
|
-
const preExisting = new Set(validateTasksDoc(oldDoc).errors);
|
|
317
|
-
violations.push(...validateTasksDoc(newDoc).errors.filter(e => !preExisting.has(e)));
|
|
318
|
-
|
|
319
|
-
// Planned-vs-real outputs diff: changing `outputs` is allowed, but the
|
|
320
|
-
// divergence must be explained in `notes` in the same edit.
|
|
321
|
-
violations.push(...outputsDivergenceViolations(oldDoc, newDoc));
|
|
322
|
-
|
|
323
|
-
if (violations.length === 0) {
|
|
324
|
-
// Only mutable progress fields changed — a legitimate implement-phase
|
|
325
|
-
// update via Write OR Edit.
|
|
326
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
331
|
-
return blockResult(
|
|
332
|
-
`MORPH-SPEC: '${filename}' write rejected — the '${requiredGate}' gate has been approved.\n` +
|
|
333
|
-
`This ${toolName} violates the plan contract or the tasks schema:\n` +
|
|
334
|
-
violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
|
|
335
|
-
`During the implement phase only these fields may change: ${mutableFields.join(', ')}\n` +
|
|
336
|
-
`(status must stay in the schema enum, and outputs that diverge from the plan\n` +
|
|
337
|
-
`require an explanatory 'notes' in the same edit).\n` +
|
|
338
|
-
`To change the plan itself, ask the user to revoke the '${requiredGate}' gate,\n` +
|
|
339
|
-
`then re-present it for approval.`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ── Arquivo totalmente travado (spec.md, design) ────────────
|
|
133
|
+
// ── Arquivo totalmente travado (plan.md, design) ────────────
|
|
344
134
|
// O corpo continua congelado. A ÚNICA passagem é a errata append-only:
|
|
345
135
|
// corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
|
|
346
|
-
// de leitura/reconstrução cai no bloqueio
|
|
347
|
-
// ao contrário do ramo do tasks.json, onde não conseguir diffar significa
|
|
348
|
-
// não ter como provar violação; aqui a regra base é "não se edita").
|
|
136
|
+
// de leitura/reconstrução cai no bloqueio: a regra base é "não se edita".
|
|
349
137
|
//
|
|
350
138
|
// O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
|
|
351
139
|
// é a única coisa que este catch NÃO significa mais. ENOENT tratado como
|
|
@@ -368,13 +156,13 @@ export async function run(payload) {
|
|
|
368
156
|
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
369
157
|
return blockResult(
|
|
370
158
|
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
371
|
-
`Editing approved
|
|
159
|
+
`Editing an approved artifact breaks its contract.\n\n` +
|
|
372
160
|
(errataCheck ? `Esta mudança não é uma errata válida: ${errataCheck.why}.\n\n` : '') +
|
|
373
|
-
`Corrigir um FATO ERRADO sobre o código (
|
|
374
|
-
`desmente) não exige revogar o gate: acrescente
|
|
161
|
+
`Corrigir um FATO ERRADO sobre o código (o texto aprovado afirma algo que o\n` +
|
|
162
|
+
`código desmente) não exige revogar o gate: acrescente sob \`${ERRATA_HEADING}\` no\n` +
|
|
375
163
|
`FIM do arquivo, deixando o corpo intacto — é append-only e fica na trilha.\n\n` +
|
|
376
|
-
`Mudar o ESCOPO acordado é outra coisa:
|
|
377
|
-
`
|
|
164
|
+
`Mudar o ESCOPO acordado é outra coisa: \`morph-spec unapprove ${featureName} ${requiredGate}\`,\n` +
|
|
165
|
+
`edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
|
|
378
166
|
);
|
|
379
167
|
} catch {
|
|
380
168
|
// Fail-open
|
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
* the agent to think about state. Fail-open: any error returns {} silently.
|
|
7
7
|
*
|
|
8
8
|
* Triggers:
|
|
9
|
-
* - Write/Edit of .morph/features/{feature}/
|
|
9
|
+
* - Write/Edit of .morph/features/{feature}/2-plan/plan.md → register feature
|
|
10
10
|
* - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
|
|
11
11
|
* - AskUserQuestion answered with option starting "aprovado" → advance gate
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
14
16
|
import { loadState, stateExists } from '../../shared/state-reader.js';
|
|
15
17
|
import { persistFeature, setGateApproved, createDefaultFeature, readFeatureFile } from '../../shared/state-writer.js';
|
|
16
18
|
import { resolveMorphRoot } from '../../shared/payload-utils.js';
|
|
17
19
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
20
|
+
import { gateFromQuestion } from '../../shared/gate-question.js';
|
|
18
21
|
import {
|
|
19
22
|
locateFeatureForQuestion,
|
|
20
23
|
allWorkTrees,
|
|
@@ -23,7 +26,6 @@ import {
|
|
|
23
26
|
} from '../../shared/feature-locator.js';
|
|
24
27
|
|
|
25
28
|
const PHASE_FOLDERS = {
|
|
26
|
-
'0-proposal': 'proposal',
|
|
27
29
|
'1-design': 'design',
|
|
28
30
|
'1-ui': 'uiux', // legacy alias — still recognized during folder transition
|
|
29
31
|
'2-plan': 'plan',
|
|
@@ -92,24 +94,10 @@ function isApprovalAnswer(answer) {
|
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
function gateFromQuestion(question) {
|
|
96
|
-
if (typeof question !== 'string') return null;
|
|
97
|
-
// Checked BEFORE the generic patterns below: the pinned uiux template is
|
|
98
|
-
// "Gate uiux — Design de {feature} aprovado?" (morph-uiux/SKILL.md) — it
|
|
99
|
-
// must never fall through to 'plan' via a stray "plan"/"plano" substring in
|
|
100
|
-
// the feature name. `/gate\s+uiux/i` alone is unambiguous either way.
|
|
101
|
-
if (/gate\s+uiux/i.test(question)) return 'uiux';
|
|
102
|
-
const lower = question.toLowerCase();
|
|
103
|
-
if (lower.includes('gate 1') || lower.includes('proposta')) return 'proposal';
|
|
104
|
-
if (lower.includes('gate 2') || lower.includes('plano') || lower.includes('plan')) return 'plan';
|
|
105
|
-
if (lower.includes('gate 3') || lower.includes('review')) return 'review';
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
97
|
// Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
|
|
110
98
|
// installed standalone and cannot import from src/). Keep in sync:
|
|
111
|
-
//
|
|
112
|
-
const GATE_PREREQ = {
|
|
99
|
+
// plan first; uiux and review require plan.
|
|
100
|
+
const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
|
|
113
101
|
function unmetGatePrereqs(gate, gates) {
|
|
114
102
|
const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
|
|
115
103
|
return (GATE_PREREQ[gate] || []).filter((p) => !isApproved(p));
|
|
@@ -216,7 +204,17 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
216
204
|
// (`morph-spec approve`) produce one consistent shape
|
|
217
205
|
// ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
|
|
218
206
|
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
219
|
-
|
|
207
|
+
// Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
|
|
208
|
+
// independent review reports, but the degradation is stamped, never silent.
|
|
209
|
+
const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
|
|
210
|
+
&& ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
|
|
211
|
+
if (selfAssessed) {
|
|
212
|
+
warnings.push(
|
|
213
|
+
`⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM os relatórios de review independentes (verifier.md, reviewer.md) — ` +
|
|
214
|
+
`registrado como selfAssessed (aparece no list/fleet).`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
setGateApproved(featureName, gate, { approvedBy: 'manual', ...(selfAssessed ? { selfAssessed: true } : {}) }, root);
|
|
220
218
|
}
|
|
221
219
|
return warnings.length ? warnings.join('\n') : null;
|
|
222
220
|
}
|
|
@@ -95,11 +95,11 @@ function classifyReadKind(target) {
|
|
|
95
95
|
if (inFeature) {
|
|
96
96
|
const rel = inFeature[1];
|
|
97
97
|
const PLAN_ARTIFACT_BASENAMES = new Set([
|
|
98
|
-
'
|
|
99
|
-
'
|
|
98
|
+
'plan.md', 'checks.json',
|
|
99
|
+
'handoff.md',
|
|
100
100
|
]);
|
|
101
101
|
const base = rel.split('/').pop();
|
|
102
|
-
if (PLAN_ARTIFACT_BASENAMES.has(base) || rel.includes('
|
|
102
|
+
if (PLAN_ARTIFACT_BASENAMES.has(base) || rel.includes('reports/')) {
|
|
103
103
|
return 'plan-artifact';
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Stop Hook: phase-transition alert,
|
|
4
|
+
* Stop Hook: phase-transition alert, slice report audit, session usage.
|
|
5
5
|
*
|
|
6
6
|
* Phase transition tracking: stores last-seen phase per feature in
|
|
7
7
|
* .morph/memory/phase-watch.json so the alert fires exactly once.
|
|
@@ -19,97 +19,34 @@ import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
|
19
19
|
import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
|
|
20
20
|
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
21
21
|
import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
|
|
22
|
+
import { sliceProgress, sliceReportPath, inspectReport } from '../../shared/slice-progress.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Two checks:
|
|
29
|
-
* 1. Task IDs that appear as a "## T<n>" heading in recap.md but still
|
|
30
|
-
* carry status != 'done' in tasks.json — claim/state divergence.
|
|
31
|
-
* 2. Task headings without a "**Standard:**" line nearby (within 10 lines
|
|
32
|
-
* after the heading) — no standard citation = unauditable decision.
|
|
25
|
+
* Slices whose report is final but lacks what the report contract asks for:
|
|
26
|
+
* prose, the files touched, every scenario the slice covers. Advisory: the
|
|
27
|
+
* same rule `report-check` applies when the orchestrator resumes control.
|
|
33
28
|
*/
|
|
34
|
-
function
|
|
29
|
+
function auditSliceReports(featurePath) {
|
|
30
|
+
const progress = sliceProgress(featurePath);
|
|
31
|
+
if (!progress) return [];
|
|
35
32
|
const warnings = [];
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
recap = readFileSync(recapPath, 'utf-8');
|
|
43
|
-
tasksJson = JSON.parse(readFileSync(tasksJsonPath, 'utf-8'));
|
|
44
|
-
} catch {
|
|
45
|
-
return warnings;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const tasks = Array.isArray(tasksJson.tasks) ? tasksJson.tasks : [];
|
|
49
|
-
if (tasks.length === 0) return warnings;
|
|
50
|
-
|
|
51
|
-
// Index recap by task ID — collect line numbers where each task is mentioned
|
|
52
|
-
// as completed. Supports two formats agents commonly use:
|
|
53
|
-
// (a) "## T<n>" — heading style (per the SKILL.md template)
|
|
54
|
-
// (b) "| T<n> | ... done" — markdown table row with done in same row
|
|
55
|
-
const lines = recap.split('\n');
|
|
56
|
-
const recapMentions = new Map(); // id → starting line index
|
|
57
|
-
const headingRe = /^##\s+(T\d+)\b/;
|
|
58
|
-
const tableRowRe = /^\|\s*(T\d+)\s*\|.*\bdone\b/i;
|
|
59
|
-
lines.forEach((ln, i) => {
|
|
60
|
-
const heading = ln.match(headingRe);
|
|
61
|
-
if (heading && !recapMentions.has(heading[1])) {
|
|
62
|
-
recapMentions.set(heading[1], i);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const tableRow = ln.match(tableRowRe);
|
|
66
|
-
if (tableRow && !recapMentions.has(tableRow[1])) {
|
|
67
|
-
recapMentions.set(tableRow[1], i);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Check 1: status drift — task mentioned as done in recap but tasks.json
|
|
72
|
-
// still shows it as pending/in_progress.
|
|
73
|
-
for (const t of tasks) {
|
|
74
|
-
if (recapMentions.has(t.id) && t.status !== 'done') {
|
|
75
|
-
warnings.push(
|
|
76
|
-
`tasks.json drift: ${t.id} appears as done in recap.md but tasks.json shows status="${t.status}". Run writeTasksJson to sync.`
|
|
77
|
-
);
|
|
33
|
+
for (const slice of progress.slices) {
|
|
34
|
+
if (slice.status !== 'feito') continue;
|
|
35
|
+
const { missing } = inspectReport(sliceReportPath(featurePath, slice.id), { covers: slice.covers });
|
|
36
|
+
if (missing.length) {
|
|
37
|
+
const hints = missing.map((id) => MISSING_HINT[id] ?? `cite ${id} pelo id`);
|
|
38
|
+
warnings.push(`relatório de ${slice.id} sem: ${missing.join(', ')} — ${hints.join('; ')} (formato na skill morph-apply; morph-spec report-check)`);
|
|
78
39
|
}
|
|
79
40
|
}
|
|
80
|
-
|
|
81
|
-
// Check 2: missing Standard citation per task mention. Skip pure
|
|
82
|
-
// table-row mentions (they're a summary, the heading-style block
|
|
83
|
-
// is where the standard belongs) — only flag when the agent did
|
|
84
|
-
// not produce a heading block at all for a task that is also marked
|
|
85
|
-
// done in tasks.json.
|
|
86
|
-
const standardRe = /\*\*Standard:\*\*|\bStandard\s*consulted\b|\bstandards?\s+used\b/i;
|
|
87
|
-
const headingMentions = new Map();
|
|
88
|
-
lines.forEach((ln, i) => {
|
|
89
|
-
const m = ln.match(headingRe);
|
|
90
|
-
if (m && !headingMentions.has(m[1])) headingMentions.set(m[1], i);
|
|
91
|
-
});
|
|
92
|
-
for (const t of tasks) {
|
|
93
|
-
if (t.status !== 'done') continue;
|
|
94
|
-
const lineIdx = headingMentions.get(t.id);
|
|
95
|
-
if (lineIdx === undefined) {
|
|
96
|
-
// Task is done but has no heading block at all — recap is summary-only.
|
|
97
|
-
warnings.push(
|
|
98
|
-
`recap.md task ${t.id} (status=done in tasks.json) has no detailed "## ${t.id}" block. Per morph-implement SKILL.md, each done task needs a heading block with a "**Standard:** ..." citation so the reviewer can audit the decision.`
|
|
99
|
-
);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const window = lines.slice(lineIdx, lineIdx + 12).join('\n');
|
|
103
|
-
if (!standardRe.test(window)) {
|
|
104
|
-
warnings.push(
|
|
105
|
-
`recap.md task ${t.id} is missing a "**Standard:** ..." citation within 12 lines of its heading. Cite which standard from STANDARDS.json justified the implementation (or "ad-hoc (see decisions.md ADR-N)" if intentional).`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
41
|
return warnings;
|
|
111
42
|
}
|
|
112
43
|
|
|
44
|
+
const MISSING_HINT = {
|
|
45
|
+
what: '2-3 frases do que foi feito e como foi provado',
|
|
46
|
+
files: 'linha **Arquivos:** com todo arquivo criado ou modificado',
|
|
47
|
+
partial: '1ª linha PARCIAL: a fatia não terminou',
|
|
48
|
+
};
|
|
49
|
+
|
|
113
50
|
// Phases where compaction is strongly recommended before starting
|
|
114
51
|
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
115
52
|
|
|
@@ -123,7 +60,7 @@ const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
|
123
60
|
* `payload.session_id` for the throttle key) — the one thing only the
|
|
124
61
|
* payload carries.
|
|
125
62
|
* @returns {Promise<object|null>} a hookSpecificOutput advisory (phase
|
|
126
|
-
* transition / build failure /
|
|
63
|
+
* transition / build failure / incomplete slice report), or null (pass/silent).
|
|
127
64
|
*/
|
|
128
65
|
export async function run(payload) {
|
|
129
66
|
try {
|
|
@@ -171,7 +108,7 @@ export async function run(payload) {
|
|
|
171
108
|
`⚡ Phase advanced: ${lastPhase} → implement`,
|
|
172
109
|
``,
|
|
173
110
|
`💡 Recommended before starting implementation:`,
|
|
174
|
-
` Run /compact to free up context —
|
|
111
|
+
` Run /compact to free up context — plan.md and the design`,
|
|
175
112
|
` loaded during planning take significant space. A compact now gives`,
|
|
176
113
|
` Claude a clean slate focused on writing code.`,
|
|
177
114
|
``,
|
|
@@ -179,9 +116,8 @@ export async function run(payload) {
|
|
|
179
116
|
);
|
|
180
117
|
}
|
|
181
118
|
|
|
182
|
-
// ──
|
|
183
|
-
//
|
|
184
|
-
// citations. Advisory warnings only — Stop hook never blocks.
|
|
119
|
+
// ── Slice report audit ────────────────────────────────────────────────────
|
|
120
|
+
// Advisory warnings only — Stop hook never blocks.
|
|
185
121
|
//
|
|
186
122
|
// The same audit re-runs every Stop; without dedup it re-injects the same
|
|
187
123
|
// ~500-token block turn after turn even when nothing changed. Hash-gate it:
|
|
@@ -190,7 +126,7 @@ export async function run(payload) {
|
|
|
190
126
|
// hash, so a later recurrence — even an identical one — is reported fresh.
|
|
191
127
|
try {
|
|
192
128
|
const featurePath = join(cwd, '.morph', 'features', name);
|
|
193
|
-
const
|
|
129
|
+
const reportWarnings = auditSliceReports(featurePath);
|
|
194
130
|
|
|
195
131
|
const warnedHashesPath = join(memDir, 'warned-hashes.json');
|
|
196
132
|
let warnedHashes = {};
|
|
@@ -198,17 +134,17 @@ export async function run(payload) {
|
|
|
198
134
|
if (existsSync(warnedHashesPath)) warnedHashes = JSON.parse(readFileSync(warnedHashesPath, 'utf-8'));
|
|
199
135
|
} catch { /* corrupt file — start fresh */ }
|
|
200
136
|
|
|
201
|
-
if (
|
|
202
|
-
const hash = createHash('sha1').update(
|
|
203
|
-
if (warnedHashes[name]?.
|
|
204
|
-
warnings.push('📋
|
|
205
|
-
for (const w of
|
|
206
|
-
warnedHashes[name] = { ...warnedHashes[name],
|
|
137
|
+
if (reportWarnings.length > 0) {
|
|
138
|
+
const hash = createHash('sha1').update(reportWarnings.join('\n')).digest('hex');
|
|
139
|
+
if (warnedHashes[name]?.reports !== hash) {
|
|
140
|
+
warnings.push('📋 Relatórios de fatia:');
|
|
141
|
+
for (const w of reportWarnings) warnings.push(` • ${w}`);
|
|
142
|
+
warnedHashes[name] = { ...warnedHashes[name], reports: hash };
|
|
207
143
|
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch { /* non-blocking */ }
|
|
208
144
|
}
|
|
209
145
|
// same hash as last time it fired → already surfaced, stay silent
|
|
210
|
-
} else if (warnedHashes[name]?.
|
|
211
|
-
delete warnedHashes[name].
|
|
146
|
+
} else if (warnedHashes[name]?.reports) {
|
|
147
|
+
delete warnedHashes[name].reports;
|
|
212
148
|
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch { /* non-blocking */ }
|
|
213
149
|
}
|
|
214
150
|
} catch {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* warn-bash-source-write — PreToolUse Bash, warn only: a project source file written
|
|
4
|
+
* by the shell (`writeTargets`) skips every Write/Edit guard (phase folders,
|
|
5
|
+
* protected files, comment-guard). The command still runs.
|
|
6
|
+
*/
|
|
7
|
+
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
8
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
9
|
+
import { writeTargets } from '../../shared/command-tokens.js';
|
|
10
|
+
import { isProjectSource } from '../../shared/source-files.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
14
|
+
* @returns {Promise<object|null>} additionalContext naming the files, or null
|
|
15
|
+
*/
|
|
16
|
+
export async function run(payload) {
|
|
17
|
+
try {
|
|
18
|
+
if (!stateExists()) return null;
|
|
19
|
+
const sources = writeTargets(payload?.tool_input?.command || '').filter(isProjectSource);
|
|
20
|
+
if (!sources.length) return null;
|
|
21
|
+
return contextResult(
|
|
22
|
+
`⚠️ Código do projeto escrito pelo Bash (${sources.join(', ')}): os guards de escrita (fase, arquivo protegido, comment-guard) ` +
|
|
23
|
+
'só enxergam Write/Edit. Reescreva esse arquivo por Write/Edit.',
|
|
24
|
+
'PreToolUse',
|
|
25
|
+
);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await runStandalone(run);
|