@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,277 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* trace-autogen.js — PostToolUse hook (Write|Edit)
|
|
4
|
-
*
|
|
5
|
-
* Auto-generates the execution trace
|
|
6
|
-
* `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json +
|
|
7
|
-
* recap.md whenever tasks.json is written during
|
|
8
|
-
* the implement phase.
|
|
9
|
-
*
|
|
10
|
-
* This removes the manual trace-writing step from the skills (friction item
|
|
11
|
-
* observed in MORPH_GHLBrain 2026-07-11: 5 writes per task). The tasks.json
|
|
12
|
-
* status flip — the last write of every task's ceremony — now produces the
|
|
13
|
-
* trace for free: deterministic, idempotent, always in sync with the real
|
|
14
|
-
* on-disk state. The skills must NOT write traces by hand anymore.
|
|
15
|
-
*
|
|
16
|
-
* Sources:
|
|
17
|
-
* - tasks.json → task set, status (done→completed, skipped→skipped,
|
|
18
|
-
* blocked→failed; pending/in_progress stay out), outputs
|
|
19
|
-
* - recap.md → per-task `**Standard:**` id and `**Persona:**` line
|
|
20
|
-
*
|
|
21
|
-
* The most recent existing `*-{feature}-implement.json` is updated in place
|
|
22
|
-
* (session_id/started_at preserved); a new dated file is created otherwise.
|
|
23
|
-
* `completed_at` + phase_summary are stamped when no task is left
|
|
24
|
-
* pending/in_progress. Fail-open: any error exits 0 silently.
|
|
25
|
-
*
|
|
26
|
-
* This is also the vantage point for TaskStart/TaskEnd telemetry: a
|
|
27
|
-
* pending→in_progress flip and a flip to done/skipped are the only two task
|
|
28
|
-
* transitions the harness can observe (tasks.json is the single place task
|
|
29
|
-
* status is written), so this hook diffs each task's previous status against
|
|
30
|
-
* `.morph/logs/task-times.json` and appends the matching event via the shared
|
|
31
|
-
* telemetry logger (see `updateTaskTimes` below). The same start/end pair
|
|
32
|
-
* feeds `duration_seconds` in the trace entry.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
36
|
-
import { join } from 'path';
|
|
37
|
-
import { runStandalone } from '../../shared/hook-response.js';
|
|
38
|
-
import { stateExists } from '../../shared/state-reader.js';
|
|
39
|
-
import { getFilePath, resolveMorphRoot } from '../../shared/payload-utils.js';
|
|
40
|
-
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
41
|
-
|
|
42
|
-
function projectDir() {
|
|
43
|
-
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function extractFeature(filePath) {
|
|
47
|
-
const normalized = String(filePath).replace(/\\/g, '/');
|
|
48
|
-
const match = normalized.match(/\.morph\/features\/([^/]+)\/2-plan\/tasks\.json$/);
|
|
49
|
-
return match ? match[1] : null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Parse recap.md task blocks (`## T{N} — ...`) into
|
|
54
|
-
* { [id]: { standard, persona } }. Best-effort: missing fields stay null.
|
|
55
|
-
*/
|
|
56
|
-
function parseRecap(raw) {
|
|
57
|
-
const map = {};
|
|
58
|
-
const blocks = String(raw).split(/^## (?=T\d+)/m).slice(1);
|
|
59
|
-
for (const block of blocks) {
|
|
60
|
-
const id = (block.match(/^(T\d+)/) ?? [])[1];
|
|
61
|
-
if (!id) continue;
|
|
62
|
-
const standard = (block.match(/\*\*Standard:\*\*\s*([^\s(]+)/) ?? [])[1] ?? null;
|
|
63
|
-
const persona = (block.match(/\*\*Persona:\*\*\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
|
|
64
|
-
map[id] = { standard, persona };
|
|
65
|
-
}
|
|
66
|
-
return map;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** session_id in the trace-schema format YYYY-MM-DD-HH-MM-SS. */
|
|
70
|
-
function sessionId(d) {
|
|
71
|
-
const p = (n) => String(n).padStart(2, '0');
|
|
72
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** tasks.json status → trace-schema status. Unsettled statuses are omitted. */
|
|
76
|
-
const STATUS_MAP = { done: 'completed', skipped: 'skipped', blocked: 'failed' };
|
|
77
|
-
|
|
78
|
-
const TASK_TIMES_PATH = join('.morph', 'logs', 'task-times.json');
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Read `.morph/logs/task-times.json`: `{ [feature]: { [taskId]: { status,
|
|
82
|
-
* start?, end? } } }`. Nested by feature — not flat by task id, as a literal
|
|
83
|
-
* reading of "keyed by taskId" might suggest — because task ids (T1, T2…)
|
|
84
|
-
* are only unique WITHIN a feature; a flat map would let two sibling
|
|
85
|
-
* features in the same tree (see worktree-state-resolution.test.js for that
|
|
86
|
-
* shape) clobber each other's start/end timestamps.
|
|
87
|
-
* Fail-open: a missing/corrupt file reads as {}.
|
|
88
|
-
*/
|
|
89
|
-
function readTaskTimes(root) {
|
|
90
|
-
const p = join(root, TASK_TIMES_PATH);
|
|
91
|
-
if (!existsSync(p)) return {};
|
|
92
|
-
try {
|
|
93
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
94
|
-
} catch {
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function writeTaskTimes(root, allTimes) {
|
|
100
|
-
mkdirSync(join(root, '.morph', 'logs'), { recursive: true });
|
|
101
|
-
writeFileSync(join(root, TASK_TIMES_PATH), JSON.stringify(allTimes, null, 2), 'utf8');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Diff each task's PREVIOUS status (from `times`, this feature's slice of
|
|
106
|
-
* task-times.json) against its status in the tasks.json that was just
|
|
107
|
-
* written, and append the matching telemetry event via the shared logger —
|
|
108
|
-
* same shape/mechanism as every other event in events.jsonl, just a
|
|
109
|
-
* different `event` value.
|
|
110
|
-
*
|
|
111
|
-
* - pending → in_progress ⇒ TaskStart, stamp `start`
|
|
112
|
-
* - anything → done/skipped ⇒ TaskEnd (carries the new status), stamp `end`
|
|
113
|
-
* - first sight of a task id (no prior entry) only seeds the baseline,
|
|
114
|
-
* silently — there is no prior status to diff, and emitting an event here
|
|
115
|
-
* would fabricate a transition that never happened (e.g. a task whose
|
|
116
|
-
* first-ever tasks.json write already shows it `done`, from historical
|
|
117
|
-
* data predating this file).
|
|
118
|
-
*
|
|
119
|
-
* Mutates `times` in place and returns whether anything changed, so the
|
|
120
|
-
* caller only rewrites task-times.json when needed and the entries built
|
|
121
|
-
* right after this call see the freshly-stamped start/end for
|
|
122
|
-
* `duration_seconds`.
|
|
123
|
-
*/
|
|
124
|
-
function updateTaskTimes(times, tasks, feature, root, nowIso) {
|
|
125
|
-
let changed = false;
|
|
126
|
-
for (const t of tasks) {
|
|
127
|
-
const prev = times[t.id];
|
|
128
|
-
if (!prev) {
|
|
129
|
-
times[t.id] = { status: t.status };
|
|
130
|
-
changed = true;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (prev.status === t.status) continue;
|
|
134
|
-
|
|
135
|
-
if (prev.status === 'pending' && t.status === 'in_progress') {
|
|
136
|
-
appendTelemetryEvent({ event: 'TaskStart', feature, task: t.id, phase: 'implement' }, root);
|
|
137
|
-
times[t.id] = { ...prev, status: t.status, start: nowIso };
|
|
138
|
-
} else if (t.status === 'done' || t.status === 'skipped') {
|
|
139
|
-
appendTelemetryEvent({ event: 'TaskEnd', feature, task: t.id, status: t.status, phase: 'implement' }, root);
|
|
140
|
-
times[t.id] = { ...prev, status: t.status, end: nowIso };
|
|
141
|
-
} else {
|
|
142
|
-
times[t.id] = { ...prev, status: t.status };
|
|
143
|
-
}
|
|
144
|
-
changed = true;
|
|
145
|
-
}
|
|
146
|
-
return changed;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Seconds between recorded start/end for a task, or null when either is missing. */
|
|
150
|
-
function taskDuration(times, taskId) {
|
|
151
|
-
const rec = times[taskId];
|
|
152
|
-
if (!rec?.start || !rec?.end) return null;
|
|
153
|
-
const secs = (new Date(rec.end).getTime() - new Date(rec.start).getTime()) / 1000;
|
|
154
|
-
return Number.isFinite(secs) ? Math.round(secs * 10) / 10 : null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* @param {object} payload - parsed PostToolUse stdin payload (Write|Edit)
|
|
159
|
-
* @returns {Promise<null>} always null — this hook is pure side-effect (trace
|
|
160
|
-
* file + task-times.json), never prints/injects context.
|
|
161
|
-
*/
|
|
162
|
-
export async function run(payload) {
|
|
163
|
-
try {
|
|
164
|
-
await generateTrace(payload);
|
|
165
|
-
} catch {
|
|
166
|
-
// Fail-open
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function generateTrace(payload) {
|
|
172
|
-
if (!payload) return;
|
|
173
|
-
|
|
174
|
-
const filePath = getFilePath(payload);
|
|
175
|
-
const feature = filePath ? extractFeature(filePath) : null;
|
|
176
|
-
if (!feature) return;
|
|
177
|
-
|
|
178
|
-
// Read the sources — and write the trace — in the tree that owns the written
|
|
179
|
-
// tasks.json. Resolving from the session instead made this hook INERT for
|
|
180
|
-
// every worktree feature: the root's copy of the feature has no
|
|
181
|
-
// `2-plan/tasks.json`, so the `existsSync` guard below returned silently and
|
|
182
|
-
// no trace was ever produced for work done in a worktree.
|
|
183
|
-
const cwd = resolveMorphRoot(filePath, projectDir());
|
|
184
|
-
if (!stateExists(cwd)) return;
|
|
185
|
-
|
|
186
|
-
const featureDir = join(cwd, '.morph', 'features', feature);
|
|
187
|
-
const tasksPath = join(featureDir, '2-plan', 'tasks.json');
|
|
188
|
-
if (!existsSync(tasksPath)) return;
|
|
189
|
-
const tasksDoc = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
190
|
-
const tasks = Array.isArray(tasksDoc.tasks) ? tasksDoc.tasks : [];
|
|
191
|
-
|
|
192
|
-
const now = new Date();
|
|
193
|
-
|
|
194
|
-
// TaskStart/TaskEnd telemetry + duration_seconds source: diff each task's
|
|
195
|
-
// previous status (persisted below) against the status just written.
|
|
196
|
-
const allTimes = readTaskTimes(cwd);
|
|
197
|
-
const times = allTimes[feature] ?? (allTimes[feature] = {});
|
|
198
|
-
if (updateTaskTimes(times, tasks, feature, cwd, now.toISOString())) {
|
|
199
|
-
writeTaskTimes(cwd, allTimes);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
let recap = {};
|
|
203
|
-
const recapPath = join(featureDir, '3-implement', 'recap.md');
|
|
204
|
-
if (existsSync(recapPath)) {
|
|
205
|
-
try { recap = parseRecap(readFileSync(recapPath, 'utf8')); } catch { recap = {}; }
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const tracesDir = join(cwd, '.morph', 'traces');
|
|
209
|
-
mkdirSync(tracesDir, { recursive: true });
|
|
210
|
-
|
|
211
|
-
// Multi-session phases update the same file incrementally (trace-schema):
|
|
212
|
-
// reuse the most recent trace for this feature+phase if one exists.
|
|
213
|
-
const suffix = `-${feature}-implement.json`;
|
|
214
|
-
const existing = readdirSync(tracesDir)
|
|
215
|
-
.filter(f => /^\d{4}-\d{2}-\d{2}-/.test(f) && f.endsWith(suffix))
|
|
216
|
-
.sort()
|
|
217
|
-
.pop();
|
|
218
|
-
|
|
219
|
-
let previous = null;
|
|
220
|
-
if (existing) {
|
|
221
|
-
try { previous = JSON.parse(readFileSync(join(tracesDir, existing), 'utf8')); } catch { previous = null; }
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const fileName = existing ?? `${now.toISOString().slice(0, 10)}${suffix}`;
|
|
225
|
-
|
|
226
|
-
const entries = tasks
|
|
227
|
-
.filter(t => STATUS_MAP[t.status])
|
|
228
|
-
.map(t => {
|
|
229
|
-
const recapEntry = recap[t.id] ?? {};
|
|
230
|
-
return {
|
|
231
|
-
id: t.id,
|
|
232
|
-
title: t.title ?? null,
|
|
233
|
-
status: STATUS_MAP[t.status],
|
|
234
|
-
standards_consulted: recapEntry.standard ? [recapEntry.standard] : [],
|
|
235
|
-
agent_persona: recapEntry.persona ?? null,
|
|
236
|
-
sub_agent_dispatched: recapEntry.persona
|
|
237
|
-
? /^morph-|general-purpose/.test(recapEntry.persona)
|
|
238
|
-
: false,
|
|
239
|
-
files_modified: Array.isArray(t.outputs) ? t.outputs.filter(o => typeof o === 'string') : [],
|
|
240
|
-
duration_seconds: taskDuration(times, t.id),
|
|
241
|
-
};
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const allSettled = tasks.length > 0 &&
|
|
245
|
-
tasks.every(t => t.status !== 'pending' && t.status !== 'in_progress');
|
|
246
|
-
|
|
247
|
-
const standardCounts = {};
|
|
248
|
-
for (const entry of entries) {
|
|
249
|
-
for (const std of entry.standards_consulted) {
|
|
250
|
-
standardCounts[std] = (standardCounts[std] ?? 0) + 1;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const mostUsed = Object.entries(standardCounts)
|
|
254
|
-
.sort((a, b) => b[1] - a[1])
|
|
255
|
-
.slice(0, 3)
|
|
256
|
-
.map(([std]) => std);
|
|
257
|
-
|
|
258
|
-
const trace = {
|
|
259
|
-
schema_version: '1.0',
|
|
260
|
-
session_id: previous?.session_id ?? sessionId(now),
|
|
261
|
-
feature,
|
|
262
|
-
phase: 'implement',
|
|
263
|
-
started_at: previous?.started_at ?? now.toISOString(),
|
|
264
|
-
completed_at: allSettled ? (previous?.completed_at ?? now.toISOString()) : null,
|
|
265
|
-
tasks: entries,
|
|
266
|
-
phase_summary: {
|
|
267
|
-
tasks_completed: entries.filter(e => e.status === 'completed').length,
|
|
268
|
-
tasks_failed: entries.filter(e => e.status === 'failed').length,
|
|
269
|
-
standards_most_used: mostUsed,
|
|
270
|
-
sub_agents_dispatched: entries.filter(e => e.sub_agent_dispatched).length,
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
writeFileSync(join(tracesDir, fileName), JSON.stringify(trace, null, 2), 'utf8');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
await runStandalone(run);
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Scope: universal | Validates spec.md required sections
|
|
3
|
-
# MORPH-SPEC Pre-Commit Hook: Spec Validation
|
|
4
|
-
# Validates that spec.md files have required sections
|
|
5
|
-
|
|
6
|
-
echo "🔍 Validating spec files..."
|
|
7
|
-
|
|
8
|
-
# Find modified spec.md files
|
|
9
|
-
SPEC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep 'spec\.md$')
|
|
10
|
-
|
|
11
|
-
if [ -z "$SPEC_FILES" ]; then
|
|
12
|
-
echo "✓ No spec files modified"
|
|
13
|
-
exit 0
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
HAS_ERRORS=false
|
|
17
|
-
|
|
18
|
-
for spec_file in $SPEC_FILES; do
|
|
19
|
-
echo "Checking: $spec_file"
|
|
20
|
-
|
|
21
|
-
# Required sections
|
|
22
|
-
REQUIRED_SECTIONS=(
|
|
23
|
-
"## 📋 Metadata"
|
|
24
|
-
"## 🎯 Overview"
|
|
25
|
-
"## 🏗️ Technical Design"
|
|
26
|
-
"## ✅ Acceptance Criteria"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
for section in "${REQUIRED_SECTIONS[@]}"; do
|
|
30
|
-
if ! grep -q "$section" "$spec_file"; then
|
|
31
|
-
echo " ❌ Missing section: $section"
|
|
32
|
-
HAS_ERRORS=true
|
|
33
|
-
fi
|
|
34
|
-
done
|
|
35
|
-
|
|
36
|
-
# Check if has at least one user story or requirement
|
|
37
|
-
if ! grep -qi "user story\|requirement\|acceptance criteria" "$spec_file"; then
|
|
38
|
-
echo " ⚠️ Warning: No user stories or requirements found"
|
|
39
|
-
fi
|
|
40
|
-
done
|
|
41
|
-
|
|
42
|
-
if [ "$HAS_ERRORS" = true ]; then
|
|
43
|
-
echo ""
|
|
44
|
-
echo "❌ COMMIT BLOCKED: spec.md files are incomplete"
|
|
45
|
-
echo " Add missing sections before committing"
|
|
46
|
-
exit 1
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
echo "✓ All spec files are valid"
|
|
50
|
-
exit 0
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tasks-validator.js — minimal hook-side schema validation for tasks.json.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors VALID_STATUS / VALID_EFFORT from src/lib/tasks/tasks-json-parser.js.
|
|
5
|
-
* Hooks are installed standalone into target projects (.morph/framework/hooks/),
|
|
6
|
-
* so they cannot import from src/ — the enum lives here, duplicated on purpose
|
|
7
|
-
* and covered by tests on both sides.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const VALID_TASK_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
|
|
11
|
-
export const VALID_TASK_EFFORT = ['S', 'M', 'L', 'XL'];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Validate the parts of a tasks.json document that hooks care about:
|
|
15
|
-
* a tasks[] array whose entries carry a valid status (and effort, when set).
|
|
16
|
-
* The production failure this guards: a leaf agent writing status "completed"
|
|
17
|
-
* (not in the enum) without any validation.
|
|
18
|
-
*
|
|
19
|
-
* @param {object} doc - parsed tasks.json
|
|
20
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
21
|
-
*/
|
|
22
|
-
/** Human-readable type name for error messages ('array', not 'object'). */
|
|
23
|
-
function typeName(value) {
|
|
24
|
-
return Array.isArray(value) ? 'array' : typeof value;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function validateTasksDoc(doc) {
|
|
28
|
-
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.tasks)) {
|
|
29
|
-
return { valid: false, errors: ['tasks.json must be an object with a tasks[] array'] };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const errors = [];
|
|
33
|
-
for (const task of doc.tasks) {
|
|
34
|
-
const id = task?.id ?? '?';
|
|
35
|
-
if (!VALID_TASK_STATUS.includes(task?.status)) {
|
|
36
|
-
errors.push(`task '${id}': invalid status '${task?.status}' (valid: ${VALID_TASK_STATUS.join(', ')})`);
|
|
37
|
-
}
|
|
38
|
-
if (task?.effort !== undefined && !VALID_TASK_EFFORT.includes(task.effort)) {
|
|
39
|
-
errors.push(`task '${id}': invalid effort '${task.effort}' (valid: ${VALID_TASK_EFFORT.join(', ')})`);
|
|
40
|
-
}
|
|
41
|
-
// Mutable-field type checks with the exact field/value in the message —
|
|
42
|
-
// a malformed notes/outputs used to surface as the misleading generic
|
|
43
|
-
// "notes is empty" downstream (MORPH_GHLBrain 2026-07-11).
|
|
44
|
-
if (task?.notes !== undefined && typeof task.notes !== 'string') {
|
|
45
|
-
errors.push(`task '${id}': 'notes' must be a string (got ${typeName(task.notes)})`);
|
|
46
|
-
}
|
|
47
|
-
if (task?.outputs !== undefined) {
|
|
48
|
-
if (!Array.isArray(task.outputs)) {
|
|
49
|
-
errors.push(`task '${id}': 'outputs' must be an array of path strings (got ${typeName(task.outputs)})`);
|
|
50
|
-
} else {
|
|
51
|
-
task.outputs.forEach((entry, i) => {
|
|
52
|
-
if (typeof entry !== 'string') {
|
|
53
|
-
errors.push(`task '${id}': outputs[${i}] must be a string path (got ${typeName(entry)})`);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return { valid: errors.length === 0, errors };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Planned-vs-real outputs divergence: tasks whose `outputs` changed relative
|
|
64
|
-
* to the previous document but whose `notes` is still empty. The divergence
|
|
65
|
-
* itself is legitimate (outputs is a mutable progress field) — shipping it
|
|
66
|
-
* without an explanatory note is what gets flagged.
|
|
67
|
-
*
|
|
68
|
-
* @param {object} oldDoc - parsed previous tasks.json
|
|
69
|
-
* @param {object} newDoc - parsed proposed tasks.json
|
|
70
|
-
* @returns {string[]} human-readable violation messages (empty = OK)
|
|
71
|
-
*/
|
|
72
|
-
/** Compact outputs rendering for messages, capped so huge arrays stay readable. */
|
|
73
|
-
function fmtOutputs(outputs) {
|
|
74
|
-
const s = JSON.stringify(outputs ?? []);
|
|
75
|
-
return s.length > 120 ? s.slice(0, 117) + '...' : s;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function outputsDivergenceViolations(oldDoc, newDoc) {
|
|
79
|
-
const violations = [];
|
|
80
|
-
const oldById = new Map((oldDoc?.tasks ?? []).map(t => [t.id, t]));
|
|
81
|
-
|
|
82
|
-
for (const task of newDoc?.tasks ?? []) {
|
|
83
|
-
const old = oldById.get(task.id);
|
|
84
|
-
if (!old) continue;
|
|
85
|
-
const outputsChanged =
|
|
86
|
-
JSON.stringify(old.outputs ?? []) !== JSON.stringify(task.outputs ?? []);
|
|
87
|
-
if (!outputsChanged) continue;
|
|
88
|
-
|
|
89
|
-
// A filled string note explains the divergence — done.
|
|
90
|
-
if (typeof task.notes === 'string' && task.notes.trim().length > 0) continue;
|
|
91
|
-
|
|
92
|
-
// Root-cause specific messages: a malformed notes value is a DIFFERENT
|
|
93
|
-
// failure than a missing one — claiming "notes is empty" when the agent
|
|
94
|
-
// just wrote an array reads as a false positive and burns a retry cycle.
|
|
95
|
-
if (task.notes !== undefined && typeof task.notes !== 'string') {
|
|
96
|
-
violations.push(
|
|
97
|
-
`task '${task.id}': outputs diverged from the plan and 'notes' is not a string ` +
|
|
98
|
-
`(got ${typeName(task.notes)}) — write the explanation as a single string in 'notes'`
|
|
99
|
-
);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
violations.push(
|
|
104
|
-
`task '${task.id}': outputs diverged from the plan ` +
|
|
105
|
-
`(${fmtOutputs(old.outputs)} → ${fmtOutputs(task.outputs)}) but 'notes' is empty — ` +
|
|
106
|
-
`record why in 'notes' in the same edit. ` +
|
|
107
|
-
`Deletions and annotations belong in 'notes', not in 'outputs' (outputs = produced paths only).`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
return violations;
|
|
111
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
stacks:
|
|
3
|
-
- "*"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# MORPH-SPEC Task Tracking
|
|
7
|
-
|
|
8
|
-
> Progress lives in `tasks.json` — nowhere else. There is no CLI command to mark a task done and
|
|
9
|
-
> no session-level checklist to keep in sync: this file IS the record of what happened.
|
|
10
|
-
|
|
11
|
-
## Persisting Progress to `tasks.json`
|
|
12
|
-
|
|
13
|
-
To record task progress for the feature, write `.morph/features/{feature}/2-plan/tasks.json`,
|
|
14
|
-
touching ONLY the mutable fields:
|
|
15
|
-
|
|
16
|
-
- `status` — `pending` → `in_progress` → `done` (também: `skipped`, `blocked`)
|
|
17
|
-
- `outputs` — the files the task produced
|
|
18
|
-
- `notes` — free-text record (deviations, context, resolved blockers)
|
|
19
|
-
|
|
20
|
-
> **O status terminal é `done`.** `completed` **não existe** no enum de
|
|
21
|
-
> `.morph/framework/schemas/tasks.schema.json` — é vocabulário de outro sistema, e o hook
|
|
22
|
-
> `protect-spec-files` rejeita a gravação.
|
|
23
|
-
|
|
24
|
-
The plan contract — `title`, `description`, `dependencies`, `effort`, `doneCriteria`,
|
|
25
|
-
`group`, `agent`, `standards` — is frozen after Gate 2. Any attempt to change it is
|
|
26
|
-
blocked by the `protect-spec-files` hook. `agent` (persona id, no `morph-` prefix)
|
|
27
|
-
and `standards` (paths pinned by morph-plan) are optional — legacy plans without
|
|
28
|
-
them remain valid.
|
|
29
|
-
|
|
30
|
-
Use the `Write` tool (whole-file rewrite) OR the `Edit` tool — `Edit` is allowed
|
|
31
|
-
as long as it touches only `status`, `outputs`, and `notes`. State is then synced
|
|
32
|
-
automatically by the `state-sync` PostToolUse hook; you do not call a CLI to mark
|
|
33
|
-
output.
|
|
34
|
-
|
|
35
|
-
Update each task as you go — `in_progress` when you pick it up, `done` when it is finished —
|
|
36
|
-
rather than batching every write to the end of the phase. `morph-spec status`, `dag`, `verify`
|
|
37
|
-
and the dashboard all read this file, so a stale `tasks.json` is a blind orchestrator.
|
|
38
|
-
|
|
39
|
-
## Deviation Tracking
|
|
40
|
-
|
|
41
|
-
When completing a task differently than originally planned, record the reason in the
|
|
42
|
-
task's `notes` field. `notes` is **uma única string** (o schema declara
|
|
43
|
-
`"notes": { "type": "string" }`) — para registrar mais de um desvio, separe-os com
|
|
44
|
-
`;` ou `\n` dentro da mesma string. Um array é **rejeitado** pelo validador, com a
|
|
45
|
-
mensagem `'notes' must be a string`.
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{
|
|
49
|
-
"id": "T3",
|
|
50
|
-
"status": "done",
|
|
51
|
-
"outputs": ["Features/User/Update/Handler.cs"],
|
|
52
|
-
"notes": "deviation: split validation into a separate validator for testability; blocker-resolved: EF migration conflict fixed by regenerating the snapshot"
|
|
53
|
-
}
|
|
54
|
-
```
|