@polymorphism-tech/morph-spec 7.3.0 → 7.5.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 +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +22 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +10 -5
- package/framework/MORPH.md +31 -14
- package/framework/agents.json +132 -132
- package/framework/commands/create-umbraco-lp.md +35 -0
- package/framework/commands/migrate-lp-to-umbraco.md +33 -0
- package/framework/commands/morph-archive.md +17 -27
- package/framework/evals/rubrics/README.md +9 -8
- package/framework/hooks/README.md +2 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
- package/framework/hooks/claude-code/core/gate-guard.js +160 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
- package/framework/hooks/claude-code/core/state-sync.js +16 -36
- package/framework/hooks/claude-code/core/validate-completion.js +105 -0
- package/framework/hooks/shared/state-reader.js +302 -221
- package/framework/hooks/shared/state-writer.js +124 -0
- package/framework/rules/morph-task-tracking.md +69 -49
- package/framework/rules/morph-workflow.md +192 -191
- package/framework/skills/README.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
- package/framework/skills/core/morph-apply/SKILL.md +69 -27
- package/framework/skills/core/morph-brainstorming/SKILL.md +6 -2
- package/framework/skills/core/morph-eval/SKILL.md +4 -5
- package/framework/skills/core/morph-implement/SKILL.md +51 -22
- package/framework/skills/core/morph-plan/SKILL.md +10 -4
- package/framework/skills/core/morph-proposal/SKILL.md +8 -7
- package/framework/skills/core/morph-review/SKILL.md +15 -8
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/setup.md +258 -244
- package/framework/templates/REGISTRY.json +1 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
- package/framework/templates/context/CONTEXT.md +181 -180
- package/framework/templates/docs/onboarding.md +122 -118
- package/framework/templates/docs/proposal.md +10 -0
- package/framework/templates/docs/spec.md +228 -228
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
- package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
- package/package.json +1 -1
- package/src/commands/project/init.js +517 -515
- package/src/commands/project/update.js +415 -368
- package/src/commands/project/worktree.js +212 -210
- package/src/commands/state/advance.js +82 -0
- package/src/commands/state/approve.js +251 -213
- package/src/commands/state/archive.js +111 -0
- package/src/commands/validation/validate-feature.js +244 -79
- package/src/core/state/state-manager.js +256 -74
- package/src/dashboard/scanner/state-scanner.js +35 -8
- package/src/lib/generators/context-generator.js +12 -8
- package/src/lib/risk-detector.js +2 -2
- package/src/utils/file-copier.js +7 -0
- package/src/utils/hooks-installer.js +527 -516
|
@@ -11,11 +11,9 @@
|
|
|
11
11
|
* - AskUserQuestion answered with option starting "aprovado" → advance gate
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readFileSync
|
|
15
|
-
import { join } from 'path';
|
|
14
|
+
import { readFileSync } from 'fs';
|
|
16
15
|
import { loadState, stateExists } from '../../shared/state-reader.js';
|
|
17
|
-
|
|
18
|
-
const STATE_FILE = '.morph/state.json';
|
|
16
|
+
import { persistFeature, setGateApproved, createDefaultFeature } from '../../shared/state-writer.js';
|
|
19
17
|
|
|
20
18
|
const PHASE_FOLDERS = {
|
|
21
19
|
'0-proposal': 'proposal',
|
|
@@ -64,31 +62,12 @@ function extractFilename(filePath) {
|
|
|
64
62
|
return normalized.split('/').pop();
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
/**
|
|
68
|
-
* Atomic write — temp file then rename. Mirrors state-manager.js semantics.
|
|
69
|
-
*/
|
|
70
|
-
function writeStateAtomic(cwd, state) {
|
|
71
|
-
const path = join(cwd, STATE_FILE);
|
|
72
|
-
const tmpPath = `${path}.tmp.${process.pid}`;
|
|
73
|
-
writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
|
|
74
|
-
renameSync(tmpPath, path);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
65
|
function registerFeatureIfMissing(state, featureName) {
|
|
78
66
|
if (!state.features) state.features = {};
|
|
79
67
|
if (state.features[featureName]) return false;
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
state.features[featureName] =
|
|
83
|
-
status: 'in_progress',
|
|
84
|
-
createdAt: now,
|
|
85
|
-
updatedAt: now,
|
|
86
|
-
approvalGates: { proposal: false, plan: false, review: false },
|
|
87
|
-
outputs: {},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (!state.metadata) state.metadata = {};
|
|
91
|
-
state.metadata.totalFeatures = Object.keys(state.features).length;
|
|
69
|
+
// Canonical object-form gates (incl. optional uiux) — no boolean drift.
|
|
70
|
+
state.features[featureName] = createDefaultFeature();
|
|
92
71
|
return true;
|
|
93
72
|
}
|
|
94
73
|
|
|
@@ -151,7 +130,9 @@ function handleWriteEdit(input, cwd) {
|
|
|
151
130
|
}
|
|
152
131
|
|
|
153
132
|
if (changed) {
|
|
154
|
-
|
|
133
|
+
// Persist the touched feature to its authoritative feature.json and refresh
|
|
134
|
+
// the thin root index. The feature's state thus travels in its own folder.
|
|
135
|
+
persistFeature(featureName, state.features[featureName], cwd);
|
|
155
136
|
}
|
|
156
137
|
return true;
|
|
157
138
|
}
|
|
@@ -166,22 +147,21 @@ function handleAskUserQuestion(input, cwd) {
|
|
|
166
147
|
const featureName = findActiveFeature(state);
|
|
167
148
|
if (!featureName) return false;
|
|
168
149
|
|
|
169
|
-
let changed = false;
|
|
170
150
|
for (const [question, answer] of Object.entries(answers)) {
|
|
171
151
|
if (!isApprovalAnswer(answer)) continue;
|
|
172
152
|
const gate = gateFromQuestion(question);
|
|
173
153
|
if (!gate) continue;
|
|
174
154
|
|
|
175
|
-
state.features[featureName].approvalGates
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
state.features[featureName].updatedAt = new Date().toISOString();
|
|
179
|
-
changed = true;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
155
|
+
const existing = state.features[featureName].approvalGates?.[gate];
|
|
156
|
+
const alreadyApproved = existing === true || existing?.approved === true;
|
|
157
|
+
if (alreadyApproved) continue;
|
|
182
158
|
|
|
183
|
-
|
|
184
|
-
|
|
159
|
+
// An "aprovado" answer to AskUserQuestion is a human approving in the loop.
|
|
160
|
+
// Route through the shared canonical gate writer so the hook and the CLI
|
|
161
|
+
// (`morph-spec approve`) produce one consistent shape
|
|
162
|
+
// ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
|
|
163
|
+
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
164
|
+
setGateApproved(featureName, gate, { approvedBy: 'manual' }, cwd);
|
|
185
165
|
}
|
|
186
166
|
return true;
|
|
187
167
|
}
|
|
@@ -25,6 +25,96 @@ import {
|
|
|
25
25
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
26
26
|
import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Audit recap.md against tasks.json + standard-citation rule.
|
|
30
|
+
*
|
|
31
|
+
* Returns an array of human-readable warnings. Empty array = audit clean.
|
|
32
|
+
*
|
|
33
|
+
* Two checks:
|
|
34
|
+
* 1. Task IDs that appear as a "## T<n>" heading in recap.md but still
|
|
35
|
+
* carry status != 'done' in tasks.json — claim/state divergence.
|
|
36
|
+
* 2. Task headings without a "**Standard:**" line nearby (within 10 lines
|
|
37
|
+
* after the heading) — no standard citation = unauditable decision.
|
|
38
|
+
*/
|
|
39
|
+
function auditRecapAndTasks(featurePath) {
|
|
40
|
+
const warnings = [];
|
|
41
|
+
const recapPath = join(featurePath, '3-implement', 'recap.md');
|
|
42
|
+
const tasksJsonPath = join(featurePath, '2-plan', 'tasks.json');
|
|
43
|
+
if (!existsSync(recapPath) || !existsSync(tasksJsonPath)) return warnings;
|
|
44
|
+
|
|
45
|
+
let recap, tasksJson;
|
|
46
|
+
try {
|
|
47
|
+
recap = readFileSync(recapPath, 'utf-8');
|
|
48
|
+
tasksJson = JSON.parse(readFileSync(tasksJsonPath, 'utf-8'));
|
|
49
|
+
} catch {
|
|
50
|
+
return warnings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tasks = Array.isArray(tasksJson.tasks) ? tasksJson.tasks : [];
|
|
54
|
+
if (tasks.length === 0) return warnings;
|
|
55
|
+
|
|
56
|
+
// Index recap by task ID — collect line numbers where each task is mentioned
|
|
57
|
+
// as completed. Supports two formats agents commonly use:
|
|
58
|
+
// (a) "## T<n>" — heading style (per the SKILL.md template)
|
|
59
|
+
// (b) "| T<n> | ... done" — markdown table row with done in same row
|
|
60
|
+
const lines = recap.split('\n');
|
|
61
|
+
const recapMentions = new Map(); // id → starting line index
|
|
62
|
+
const headingRe = /^##\s+(T\d+)\b/;
|
|
63
|
+
const tableRowRe = /^\|\s*(T\d+)\s*\|.*\bdone\b/i;
|
|
64
|
+
lines.forEach((ln, i) => {
|
|
65
|
+
const heading = ln.match(headingRe);
|
|
66
|
+
if (heading && !recapMentions.has(heading[1])) {
|
|
67
|
+
recapMentions.set(heading[1], i);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const tableRow = ln.match(tableRowRe);
|
|
71
|
+
if (tableRow && !recapMentions.has(tableRow[1])) {
|
|
72
|
+
recapMentions.set(tableRow[1], i);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Check 1: status drift — task mentioned as done in recap but tasks.json
|
|
77
|
+
// still shows it as pending/in_progress.
|
|
78
|
+
for (const t of tasks) {
|
|
79
|
+
if (recapMentions.has(t.id) && t.status !== 'done') {
|
|
80
|
+
warnings.push(
|
|
81
|
+
`tasks.json drift: ${t.id} appears as done in recap.md but tasks.json shows status="${t.status}". Run writeTasksJson to sync.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check 2: missing Standard citation per task mention. Skip pure
|
|
87
|
+
// table-row mentions (they're a summary, the heading-style block
|
|
88
|
+
// is where the standard belongs) — only flag when the agent did
|
|
89
|
+
// not produce a heading block at all for a task that is also marked
|
|
90
|
+
// done in tasks.json.
|
|
91
|
+
const standardRe = /\*\*Standard:\*\*|\bStandard\s*consulted\b|\bstandards?\s+used\b/i;
|
|
92
|
+
const headingMentions = new Map();
|
|
93
|
+
lines.forEach((ln, i) => {
|
|
94
|
+
const m = ln.match(headingRe);
|
|
95
|
+
if (m && !headingMentions.has(m[1])) headingMentions.set(m[1], i);
|
|
96
|
+
});
|
|
97
|
+
for (const t of tasks) {
|
|
98
|
+
if (t.status !== 'done') continue;
|
|
99
|
+
const lineIdx = headingMentions.get(t.id);
|
|
100
|
+
if (lineIdx === undefined) {
|
|
101
|
+
// Task is done but has no heading block at all — recap is summary-only.
|
|
102
|
+
warnings.push(
|
|
103
|
+
`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.`
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const window = lines.slice(lineIdx, lineIdx + 12).join('\n');
|
|
108
|
+
if (!standardRe.test(window)) {
|
|
109
|
+
warnings.push(
|
|
110
|
+
`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).`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return warnings;
|
|
116
|
+
}
|
|
117
|
+
|
|
28
118
|
// Phases where compaction is strongly recommended before starting
|
|
29
119
|
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
30
120
|
|
|
@@ -104,6 +194,21 @@ try {
|
|
|
104
194
|
}
|
|
105
195
|
// ────────────────────────────────────────────────────────────────────────
|
|
106
196
|
|
|
197
|
+
// ── Recap + tasks.json audit ──────────────────────────────────────────────
|
|
198
|
+
// Surface drift between recap.md and tasks.json, plus missing standard
|
|
199
|
+
// citations. Advisory warnings only — Stop hook never blocks.
|
|
200
|
+
try {
|
|
201
|
+
const featurePath = join(cwd, '.morph', 'features', name);
|
|
202
|
+
const recapWarnings = auditRecapAndTasks(featurePath);
|
|
203
|
+
if (recapWarnings.length > 0) {
|
|
204
|
+
warnings.push('📋 Recap audit:');
|
|
205
|
+
for (const w of recapWarnings) warnings.push(` • ${w}`);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Fail-open — audit must never block Stop
|
|
209
|
+
}
|
|
210
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
107
212
|
// ── Session snapshot ──────────────────────────────────────────────────────
|
|
108
213
|
// Save session state for next session's "getting up to speed" routine.
|
|
109
214
|
try {
|