@polymorphism-tech/morph-spec 8.2.1 → 8.4.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/README.md +73 -102
- package/bin/morph-spec.js +36 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +77 -134
- package/docs/QUICKSTART.md +47 -57
- package/framework/CLAUDE.md +6 -3
- package/framework/MORPH.md +2 -2
- package/framework/agents.json +3 -1
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/hooks/README.md +3 -0
- package/framework/hooks/claude-code/core/state-sync.js +41 -9
- package/framework/hooks/claude-code/core/telemetry-log.js +102 -0
- package/framework/hooks/shared/state-writer.js +14 -3
- package/framework/hooks/shared/telemetry-logger.js +75 -0
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/schemas/feature.schema.json +15 -7
- 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 +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +4 -4
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +43 -1
- package/framework/standards/ai-agents/observability-patterns.md +156 -0
- package/framework/standards/ai-agents/production.md +3 -47
- package/framework/templates/REGISTRY.json +0 -93
- package/package.json +1 -1
- package/src/commands/observability/telemetry.js +75 -0
- package/src/commands/project/doctor.js +28 -0
- package/src/commands/state/advance.js +17 -0
- package/src/commands/state/approve.js +13 -0
- package/src/commands/state/create.js +21 -1
- package/src/commands/state/delete.js +143 -0
- package/src/commands/state/list.js +110 -0
- package/src/core/state/feature-identity.js +36 -0
- package/src/core/state/gate-order.js +51 -0
- package/src/core/state/lifecycle-audit.js +79 -0
- package/src/core/state/state-manager.js +47 -140
- package/src/dashboard/app/pages/LiveState.jsx +0 -43
- package/src/lib/detectors/design-system-detector.js +0 -43
- package/src/lib/generators/index.js +0 -1
- package/src/lib/telemetry/telemetry-reader.js +103 -0
- package/src/lib/validators/validation-runner.js +9 -73
- package/src/utils/hooks-installer.js +27 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +0 -269
- package/framework/templates/context/CONTEXT.md +0 -181
- package/src/lib/agents/metrics-aggregator.js +0 -56
- package/src/lib/agents/metrics-calculator.js +0 -125
- package/src/lib/generators/metadata-extractor.js +0 -385
|
@@ -66,8 +66,9 @@ function registerFeatureIfMissing(state, featureName) {
|
|
|
66
66
|
if (!state.features) state.features = {};
|
|
67
67
|
if (state.features[featureName]) return false;
|
|
68
68
|
|
|
69
|
-
// Canonical object-form gates (incl. optional uiux)
|
|
70
|
-
|
|
69
|
+
// Canonical object-form gates (incl. optional uiux) + stamped identity
|
|
70
|
+
// (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
|
|
71
|
+
state.features[featureName] = createDefaultFeature(featureName);
|
|
71
72
|
return true;
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -114,6 +115,15 @@ function gateFromQuestion(question) {
|
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
|
|
119
|
+
// installed standalone and cannot import from src/). Keep in sync:
|
|
120
|
+
// proposal → plan → review; uiux requires proposal.
|
|
121
|
+
const GATE_PREREQ = { proposal: [], uiux: ['proposal'], plan: ['proposal'], review: ['plan'] };
|
|
122
|
+
function unmetGatePrereqs(gate, gates) {
|
|
123
|
+
const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
|
|
124
|
+
return (GATE_PREREQ[gate] || []).filter((p) => !isApproved(p));
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
function handleWriteEdit(input, cwd) {
|
|
118
128
|
const filePath = input.tool_input?.file_path;
|
|
119
129
|
const featureName = extractFeatureName(filePath);
|
|
@@ -139,23 +149,38 @@ function handleWriteEdit(input, cwd) {
|
|
|
139
149
|
|
|
140
150
|
function handleAskUserQuestion(input, cwd) {
|
|
141
151
|
const answers = input.tool_response?.answers ?? input.tool_response?.responses ?? {};
|
|
142
|
-
if (typeof answers !== 'object' || answers === null) return
|
|
152
|
+
if (typeof answers !== 'object' || answers === null) return null;
|
|
143
153
|
|
|
144
154
|
const state = loadState(cwd);
|
|
145
|
-
if (!state) return
|
|
155
|
+
if (!state) return null;
|
|
146
156
|
|
|
147
157
|
const featureName = findActiveFeature(state);
|
|
148
|
-
if (!featureName) return
|
|
158
|
+
if (!featureName) return null;
|
|
149
159
|
|
|
160
|
+
const warnings = [];
|
|
150
161
|
for (const [question, answer] of Object.entries(answers)) {
|
|
151
162
|
if (!isApprovalAnswer(answer)) continue;
|
|
152
163
|
const gate = gateFromQuestion(question);
|
|
153
164
|
if (!gate) continue;
|
|
154
165
|
|
|
155
|
-
const
|
|
166
|
+
const gates = state.features[featureName].approvalGates;
|
|
167
|
+
const existing = gates?.[gate];
|
|
156
168
|
const alreadyApproved = existing === true || existing?.approved === true;
|
|
157
169
|
if (alreadyApproved) continue;
|
|
158
170
|
|
|
171
|
+
// Gate monotonicity: fail-open. The human said "aprovado", so we DO approve
|
|
172
|
+
// (never block a human in the loop), but if a predecessor gate is still
|
|
173
|
+
// pending we surface a non-blocking warning — the CLI `morph-spec approve`
|
|
174
|
+
// hard-refuses the same shape; here we alert so the operator can reconcile.
|
|
175
|
+
const unmet = unmetGatePrereqs(gate, gates);
|
|
176
|
+
if (unmet.length) {
|
|
177
|
+
warnings.push(
|
|
178
|
+
`⚠️ [morph-spec] GATE FORA DE ORDEM — o Gate "${gate}" de "${featureName}" foi aprovado, ` +
|
|
179
|
+
`mas o(s) gate(s) anterior(es) ${unmet.join(', ')} não estão aprovados (ordem: ${unmet.join(' → ')} → ${gate}). ` +
|
|
180
|
+
`Aprove os gates anteriores ou reveja com 'morph-spec doctor'.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
// An "aprovado" answer to AskUserQuestion is a human approving in the loop.
|
|
160
185
|
// Route through the shared canonical gate writer so the hook and the CLI
|
|
161
186
|
// (`morph-spec approve`) produce one consistent shape
|
|
@@ -163,7 +188,7 @@ function handleAskUserQuestion(input, cwd) {
|
|
|
163
188
|
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
164
189
|
setGateApproved(featureName, gate, { approvedBy: 'manual' }, cwd);
|
|
165
190
|
}
|
|
166
|
-
return
|
|
191
|
+
return warnings.length ? warnings.join('\n') : null;
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
async function main() {
|
|
@@ -176,17 +201,24 @@ async function main() {
|
|
|
176
201
|
const input = readStdin();
|
|
177
202
|
const toolName = input.tool_name;
|
|
178
203
|
|
|
204
|
+
let warning = null;
|
|
179
205
|
try {
|
|
180
206
|
if (toolName === 'Write' || toolName === 'Edit') {
|
|
181
207
|
handleWriteEdit(input, cwd);
|
|
182
208
|
} else if (toolName === 'AskUserQuestion') {
|
|
183
|
-
handleAskUserQuestion(input, cwd);
|
|
209
|
+
warning = handleAskUserQuestion(input, cwd);
|
|
184
210
|
}
|
|
185
211
|
} catch {
|
|
186
212
|
// fail-open
|
|
187
213
|
}
|
|
188
214
|
|
|
189
|
-
|
|
215
|
+
// A non-blocking order-violation warning is surfaced as injected context
|
|
216
|
+
// (hookSpecificOutput.additionalContext); everything else exits silently.
|
|
217
|
+
if (warning) {
|
|
218
|
+
exit({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: warning } });
|
|
219
|
+
} else {
|
|
220
|
+
exit();
|
|
221
|
+
}
|
|
190
222
|
}
|
|
191
223
|
|
|
192
224
|
main().catch(() => exit());
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telemetry Hook: harness execution-path capture
|
|
5
|
+
*
|
|
6
|
+
* Events: PostToolUse (all tools) + UserPromptSubmit
|
|
7
|
+
*
|
|
8
|
+
* morph-spec is a Natural Language Harness: there is no central runtime to
|
|
9
|
+
* instrument — Claude Code executes the skills and sub-agents. The only
|
|
10
|
+
* vantage points are hooks. This one is the generic capture point: every tool
|
|
11
|
+
* call and every user prompt is appended (one JSON line) to
|
|
12
|
+
* .morph/logs/events.jsonl, from which `morph-spec telemetry` reconstructs the
|
|
13
|
+
* whole path — which skills ran, which personas were dispatched (with their
|
|
14
|
+
* prompts), which standards/knowledge-base files were read.
|
|
15
|
+
*
|
|
16
|
+
* Correlation is by an EXPLICIT key (feature + phase) written into each event,
|
|
17
|
+
* not by ambient span context — hooks fire as separate node processes with no
|
|
18
|
+
* shared trace context (the enrichment lesson from the observability standard:
|
|
19
|
+
* propagate a correlation key, a TraceId does not survive the boundary).
|
|
20
|
+
*
|
|
21
|
+
* SILENT: never blocks, never injects context — only appends. Fail-open on any
|
|
22
|
+
* error (exit 0). Only active inside a morph project (stateExists gate).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readStdin } from '../../shared/stdin-reader.js';
|
|
26
|
+
import { stateExists, getActiveFeature, derivePhaseForFeature } from '../../shared/state-reader.js';
|
|
27
|
+
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
28
|
+
import { pass } from '../../shared/hook-response.js';
|
|
29
|
+
|
|
30
|
+
function normalize(p) {
|
|
31
|
+
return (p || '').replace(/\\/g, '/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (!stateExists()) pass();
|
|
36
|
+
|
|
37
|
+
const payload = await readStdin();
|
|
38
|
+
if (!payload) pass();
|
|
39
|
+
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
|
|
42
|
+
// Correlation key: active feature + its phase (explicit, not ambient).
|
|
43
|
+
// NOTE (perf): getActiveFeature → loadState hydrates every .morph/features/*/
|
|
44
|
+
// feature.json, so enrichment is O(active-feature-count) per tool call. That
|
|
45
|
+
// is fine for realistic projects (archive removes finished features, so the
|
|
46
|
+
// dir holds only the few in-flight ones) but would grow on a project that
|
|
47
|
+
// keeps many un-archived features. If that ever bites, resolve the active
|
|
48
|
+
// feature from the thin state.json index alone. Kept accurate-over-cheap for
|
|
49
|
+
// now because phase is not stored in feature.json — it needs derivation.
|
|
50
|
+
let feature = null;
|
|
51
|
+
let phase = null;
|
|
52
|
+
try {
|
|
53
|
+
const active = getActiveFeature(cwd);
|
|
54
|
+
if (active) {
|
|
55
|
+
feature = active.name;
|
|
56
|
+
phase = active.feature?.phase || derivePhaseForFeature(active.name, cwd);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// enrichment is best-effort — a missing feature never blocks capture
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let event;
|
|
63
|
+
|
|
64
|
+
// UserPromptSubmit carries the user's prompt and no tool_name.
|
|
65
|
+
if (payload.hook_event_name === 'UserPromptSubmit' || (!payload.tool_name && typeof payload.prompt === 'string')) {
|
|
66
|
+
event = { event: 'UserPromptSubmit', tool: 'UserPrompt', prompt: payload.prompt || '' };
|
|
67
|
+
} else {
|
|
68
|
+
const tool = payload.tool_name;
|
|
69
|
+
if (!tool) pass();
|
|
70
|
+
const input = payload.tool_input || {};
|
|
71
|
+
event = { event: 'PostToolUse', tool };
|
|
72
|
+
|
|
73
|
+
if (tool === 'Skill') {
|
|
74
|
+
event.skill = input.skill || '';
|
|
75
|
+
} else if (tool === 'Agent' || tool === 'Task') {
|
|
76
|
+
event.persona = input.subagent_type || '';
|
|
77
|
+
if (input.description) event.desc = input.description;
|
|
78
|
+
event.prompt = input.prompt || '';
|
|
79
|
+
} else if (tool === 'Read') {
|
|
80
|
+
event.target = normalize(input.file_path || input.path);
|
|
81
|
+
if (/\/standards\//.test(event.target)) event.kind = 'standard';
|
|
82
|
+
} else if (tool === 'Bash') {
|
|
83
|
+
event.command = input.command || '';
|
|
84
|
+
} else if (tool === 'Write' || tool === 'Edit') {
|
|
85
|
+
event.target = normalize(input.file_path || input.path);
|
|
86
|
+
} else {
|
|
87
|
+
// Every other tool still counts toward "the whole path" — record a
|
|
88
|
+
// compact target so Grep/Glob/WebFetch bursts remain distinguishable.
|
|
89
|
+
const target = input.file_path || input.path || input.pattern || input.query || input.url || '';
|
|
90
|
+
if (target) event.target = normalize(String(target));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
event.feature = feature;
|
|
95
|
+
event.phase = phase;
|
|
96
|
+
|
|
97
|
+
appendTelemetryEvent(event, cwd);
|
|
98
|
+
pass();
|
|
99
|
+
} catch {
|
|
100
|
+
// Fail-open
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
@@ -91,7 +91,7 @@ export function persistFeature(featureName, feature, projectPath) {
|
|
|
91
91
|
*/
|
|
92
92
|
export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
93
93
|
try {
|
|
94
|
-
const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature();
|
|
94
|
+
const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature(featureName);
|
|
95
95
|
const now = new Date().toISOString();
|
|
96
96
|
feature.approvalGates = feature.approvalGates || {};
|
|
97
97
|
feature.approvalGates[gate] = {
|
|
@@ -106,11 +106,22 @@ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Default feature shape (canonical object-form gates, incl. optional uiux).
|
|
111
|
+
* Stamps identity — name (the slug), workType, createdAt — so the implicit
|
|
112
|
+
* birth path (state-sync registering a feature on the first Write) never
|
|
113
|
+
* produces an anonymous {status,updatedAt} shell. `description` stays absent
|
|
114
|
+
* (a hook can't invent it); `list`/`doctor` flag its absence. Mirrors the CLI
|
|
115
|
+
* createFeature's stamping.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} [featureName] - the feature slug, stamped as `name`
|
|
118
|
+
*/
|
|
119
|
+
export function createDefaultFeature(featureName) {
|
|
111
120
|
const now = new Date().toISOString();
|
|
112
121
|
return {
|
|
122
|
+
...(featureName ? { name: featureName } : {}),
|
|
113
123
|
status: 'in_progress',
|
|
124
|
+
workType: 'feature',
|
|
114
125
|
createdAt: now,
|
|
115
126
|
updatedAt: now,
|
|
116
127
|
approvalGates: {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Logger — append-only harness event stream
|
|
3
|
+
*
|
|
4
|
+
* Writes one JSON line per event to .morph/logs/events.jsonl. This is the sink
|
|
5
|
+
* for the `telemetry-log` hook, which fires on every tool call (PostToolUse,
|
|
6
|
+
* all tools) and on every user prompt (UserPromptSubmit) — the only vantage
|
|
7
|
+
* points an NLH exposes. The whole execution path (skills, sub-agent dispatch,
|
|
8
|
+
* knowledge-base reads, prompts) is reconstructable by reading this file.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints (same discipline as activity-logger, plus one):
|
|
11
|
+
* - Synchronous only (hooks run in process, no async overhead)
|
|
12
|
+
* - Fail-silent (never throws — hooks must not be blocked by the logger)
|
|
13
|
+
* - APPEND-ONLY: one `appendFileSync` line per event, O(1) per call. The
|
|
14
|
+
* read-modify-write of a whole JSON doc (activity-logger) is O(n²) across a
|
|
15
|
+
* session and blows the ≤5ms budget once tool calls number in the hundreds.
|
|
16
|
+
* - Windows-safe paths via path.join
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'fs';
|
|
20
|
+
import { join, dirname } from 'path';
|
|
21
|
+
|
|
22
|
+
const LOGS_DIR = '.morph/logs';
|
|
23
|
+
export const EVENTS_FILE = join(LOGS_DIR, 'events.jsonl');
|
|
24
|
+
|
|
25
|
+
function getEventsPath(projectPath) {
|
|
26
|
+
return join(projectPath || process.cwd(), EVENTS_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Append a single telemetry event as one JSON line.
|
|
31
|
+
* Stamps an ISO `ts` when the caller omits one.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} event - Event payload (tool, skill/persona/target/prompt, feature, phase…)
|
|
34
|
+
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
35
|
+
*/
|
|
36
|
+
export function appendTelemetryEvent(event, projectPath) {
|
|
37
|
+
try {
|
|
38
|
+
const eventsPath = getEventsPath(projectPath);
|
|
39
|
+
const dir = dirname(eventsPath);
|
|
40
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
41
|
+
|
|
42
|
+
const stamped = { ts: event?.ts || new Date().toISOString(), ...event };
|
|
43
|
+
appendFileSync(eventsPath, JSON.stringify(stamped) + '\n', 'utf-8');
|
|
44
|
+
} catch {
|
|
45
|
+
// Fail-silent — never block hooks
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read the full event stream back into an array. A corrupt/truncated line
|
|
51
|
+
* (e.g. a killed process mid-write) is skipped, not fatal.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} [projectPath]
|
|
54
|
+
* @returns {Object[]} events in write order (may be empty)
|
|
55
|
+
*/
|
|
56
|
+
export function readTelemetryEvents(projectPath) {
|
|
57
|
+
try {
|
|
58
|
+
const eventsPath = getEventsPath(projectPath);
|
|
59
|
+
if (!existsSync(eventsPath)) return [];
|
|
60
|
+
const raw = readFileSync(eventsPath, 'utf-8');
|
|
61
|
+
const events = [];
|
|
62
|
+
for (const line of raw.split('\n')) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed) continue;
|
|
65
|
+
try {
|
|
66
|
+
events.push(JSON.parse(trimmed));
|
|
67
|
+
} catch {
|
|
68
|
+
// skip a single corrupt line — don't lose the whole stream
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return events;
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -20,6 +20,7 @@ stacks:
|
|
|
20
20
|
@.morph/framework/standards/ai-agents/prompts-in-db.md
|
|
21
21
|
@.morph/framework/standards/ai-agents/rag-custom-pgvector.md
|
|
22
22
|
@.morph/framework/standards/ai-agents/middleware-patterns.md
|
|
23
|
+
@.morph/framework/standards/ai-agents/observability-patterns.md
|
|
23
24
|
@.morph/framework/standards/ai-agents/workflows.md
|
|
24
25
|
@.morph/framework/standards/ai-agents/production.md
|
|
25
26
|
|
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["status", "approvalGates"],
|
|
7
7
|
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Human-readable feature name, stamped at birth by `morph-spec create` (defaults to the slug) and by the state-sync hook's implicit registration. Kept out of the schema's `required` array — adding it there would invalidate every feature.json that predates identity stamping; instead `create` enforces it at birth and `morph-spec doctor`/`list` flag its absence leniently."
|
|
11
|
+
},
|
|
12
|
+
"description": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "What/why of the feature. The one identity field a human must supply — `morph-spec create` refuses a new feature without it (--description, or --request which seeds it). Absent on features born via the implicit hook path; surfaced as a warning by `doctor`/`list`, never a hard read-time rejection (see name)."
|
|
15
|
+
},
|
|
8
16
|
"status": {
|
|
9
17
|
"type": "string",
|
|
10
|
-
"enum": ["draft", "in_progress", "completed"],
|
|
11
|
-
"description": "Feature lifecycle status. `completed` is stamped by `morph-spec archive` together with archivedAt."
|
|
18
|
+
"enum": ["draft", "in_progress", "completed", "abandoned"],
|
|
19
|
+
"description": "Feature lifecycle status. `completed` is stamped by `morph-spec archive` together with archivedAt. `abandoned` is stamped by `morph-spec delete --archive` together with abandonedAt (dead scaffold / killed draft, kept in .morph/archive)."
|
|
12
20
|
},
|
|
13
21
|
"createdAt": { "type": "string", "format": "date-time" },
|
|
14
22
|
"updatedAt": {
|
|
@@ -21,6 +29,11 @@
|
|
|
21
29
|
"format": "date-time",
|
|
22
30
|
"description": "Stamped by `morph-spec archive`."
|
|
23
31
|
},
|
|
32
|
+
"abandonedAt": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"format": "date-time",
|
|
35
|
+
"description": "Stamped by `morph-spec delete --archive` when a dead/abandoned feature is moved to .morph/archive."
|
|
36
|
+
},
|
|
24
37
|
"approvalGates": {
|
|
25
38
|
"type": "object",
|
|
26
39
|
"description": "One entry per gate. Written ONLY by `morph-spec approve`/`advance` (approvedBy: \"auto\") or the state-sync hook on a human \"aprovado\" answer (approvedBy: \"manual\").",
|
|
@@ -195,12 +208,7 @@
|
|
|
195
208
|
},
|
|
196
209
|
"additionalProperties": true
|
|
197
210
|
},
|
|
198
|
-
"activeAgents": { "type": "array", "items": { "type": "string" } },
|
|
199
211
|
"checkpoints": { "type": "array" },
|
|
200
|
-
"dispatchLog": {
|
|
201
|
-
"type": "array",
|
|
202
|
-
"description": "Dispatch events (capped at 50). Entries: { timestamp, phase, mode, agents: [{ id, subagentType, group, tasks }] }."
|
|
203
|
-
},
|
|
204
212
|
"trustConfig": { "type": "object" },
|
|
205
213
|
"contextBundles": { "type": "array" },
|
|
206
214
|
"fileChanges": { "type": "array" },
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# create-umbraco-lp
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# migrate-lp-to-umbraco
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-brainstorming
|
|
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-eval
|
|
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -28,10 +28,10 @@ Resolver uma queda ou bug crítico de produção o mais rápido possível — pr
|
|
|
28
28
|
### 1. Nascimento da feature
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
morph-spec create {feature} --type hotfix
|
|
31
|
+
morph-spec create {feature} --type hotfix --description "<o incidente: o que quebrou / sintoma>"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Carimba
|
|
34
|
+
Carimba `name` (o slug) + `description` (obrigatória no nascimento — descreva o incidente) e os gates `proposal`/`plan` como `approvedBy: "policy:hotfix"`, e cria as pastas `0-proposal/`, `2-plan/`, `3-implement/` num passo só — a feature já nasce em `implement` (fase derivada é gate-aware). Se a feature já existe, siga adiante sem recriar.
|
|
35
35
|
|
|
36
36
|
### 2. Scout — mapa do território (obrigatório)
|
|
37
37
|
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos três artefatos de planejamento de uma featur
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -38,10 +38,10 @@ Antes de qualquer trabalho de negócio, classifique o pedido. Se `{feature-name}
|
|
|
38
38
|
morph-spec create {feature-name} --request "<pedido do usuário>"
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
(ou `--type <tipo
|
|
41
|
+
(ou `--type <tipo> --description "<o quê/porquê>"` diretamente quando o tipo é óbvio pelo pedido — ex.: o usuário já diz "isso é um hotfix"). O `--request` **serve de semente da descrição**: toda feature nasce com `name` (o slug) e `description` (obrigatória) carimbados. Sem `--request` nem `--description`, o `create` recusa — nada de scaffolds anônimos.
|
|
42
42
|
|
|
43
43
|
- **Classificação `high` confidence** — a feature nasce com o `workType` decidido pelo detector determinístico (keywords: `hotfix` — "produção caiu/urgente/incident/p0"; `bug` — "bug/corrigir/exception/regressão"; `chore` — "typo/rename/bump/config/docs/cleanup"). Prossiga para o branch correspondente abaixo.
|
|
44
|
-
- **Exit 2** (empate `tie` ou nenhum sinal `none`) — o comando sai sem criar a feature e imprime `{ needsDecision, candidates, matched }`. **Você é o roteador**: decida o tipo entre os candidatos e re-rode com `morph-spec create {feature-name} --type <tipo
|
|
44
|
+
- **Exit 2** (empate `tie` ou nenhum sinal `none`) — o comando sai sem criar a feature e imprime `{ needsDecision, candidates, matched }`. **Você é o roteador**: decida o tipo entre os candidatos e re-rode com `morph-spec create {feature-name} --type <tipo> --request "<pedido do usuário>"` (o `--request` re-passado carimba a descrição). Só use `AskUserQuestion` para o usuário quando o pedido for genuinamente indeterminado — a maioria dos empates o roteador resolve sozinho pelo contexto da conversa.
|
|
45
45
|
|
|
46
46
|
**Branches por tipo** (dimensionamento completo em `MORPH.md` §2.1):
|
|
47
47
|
|
|
@@ -55,7 +55,7 @@ morph-spec create {feature-name} --request "<pedido do usuário>"
|
|
|
55
55
|
- Se argumento ausente: perguntar via `AskUserQuestion` (1 pergunta, header "Feature").
|
|
56
56
|
- Verificar `.morph/features/{feature-name}/`:
|
|
57
57
|
- Se `0-proposal/proposal.md` existe e tem conteúdo: pular para passo 3.
|
|
58
|
-
- Caso contrário: rode `morph-spec create {feature-name}` — cria `.morph/features/{feature-name}/0-proposal/` e o `feature.json` canônico (shape em `.morph/framework/schemas/feature.schema.json`) num passo só, server-side. Não crie a estrutura à mão nem dependa do nascimento implícito via hook no primeiro Write. Depois siga o passo 2.
|
|
58
|
+
- Caso contrário: rode `morph-spec create {feature-name} --description "<resumo do pedido do usuário>"` — cria `.morph/features/{feature-name}/0-proposal/` e o `feature.json` canônico (shape em `.morph/framework/schemas/feature.schema.json`) num passo só, server-side, com `name`+`description` carimbados. A `--description` é **obrigatória** no nascimento (ou `--request`, que a semeia). Não crie a estrutura à mão nem dependa do nascimento implícito via hook no primeiro Write. Depois siga o passo 2.
|
|
59
59
|
|
|
60
60
|
### 2. Invocar `Skill(morph-brainstorming)`
|
|
61
61
|
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"mcp": "#mcp-integration-model-context-protocol",
|
|
30
30
|
"observability": "#observability--opentelemetry"
|
|
31
31
|
},
|
|
32
|
-
"digest": "Production panorama for MAF agents. Middleware (3 levels)
|
|
32
|
+
"digest": "Production panorama for MAF agents. Middleware (3 levels), MCP integration, and observability/OpenTelemetry are summarized here and treated canonically in ai-agents-middleware-patterns / ai-agents-mcp-tools / ai-agents-mcp-server / ai-agents-observability-patterns. Owns: A2A protocol via Microsoft.Agents.AI.A2A; Redis semantic + HybridCache caching; security and production checklists."
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"id": "ai-agents-setup",
|
|
@@ -309,6 +309,40 @@
|
|
|
309
309
|
},
|
|
310
310
|
"digest": "Three middleware levels intercept agent behavior: Agent Run (whole invocation), Function Calling (individual tool calls), IChatClient (raw LLM calls). Register via .AsBuilder().Use(...). Patterns: telemetry, resilience (retry/rate-limit), anti-hallucination (validate tool args before next())."
|
|
311
311
|
},
|
|
312
|
+
{
|
|
313
|
+
"id": "ai-agents-observability-patterns",
|
|
314
|
+
"name": "observability patterns",
|
|
315
|
+
"path": "ai-agents/observability-patterns.md",
|
|
316
|
+
"category": "ai-agents",
|
|
317
|
+
"subcategory": null,
|
|
318
|
+
"tags": [
|
|
319
|
+
"ai-agents",
|
|
320
|
+
"maf",
|
|
321
|
+
"observability"
|
|
322
|
+
],
|
|
323
|
+
"stacks": [
|
|
324
|
+
"dotnet"
|
|
325
|
+
],
|
|
326
|
+
"aliases": [
|
|
327
|
+
"observability",
|
|
328
|
+
"opentelemetry",
|
|
329
|
+
"telemetry",
|
|
330
|
+
"logging",
|
|
331
|
+
"tracing",
|
|
332
|
+
"log investigation",
|
|
333
|
+
"troubleshooting",
|
|
334
|
+
"aspire dashboard",
|
|
335
|
+
"correlation id",
|
|
336
|
+
"agent observability",
|
|
337
|
+
"agent telemetry"
|
|
338
|
+
],
|
|
339
|
+
"anchors": {
|
|
340
|
+
"setup": "#setup--opentelemetry",
|
|
341
|
+
"enrichment": "#enrichment--o-que-preencher-em-spans-e-logs",
|
|
342
|
+
"investigation": "#investigação-e-análise--quando-algo-dá-errado"
|
|
343
|
+
},
|
|
344
|
+
"digest": "OpenTelemetry setup for MAF agents (AgentOpenTelemetryConsts.DefaultSourceName, traces+metrics+logging). Enrichment: correlation_id propagation across async boundaries, gen_ai.* span attributes, structured ILogger scopes, sensitive-data logging rules. Investigation workflow for production incidents: anchor on correlation_id/time window, drill from root span to children, check metrics before theorizing, reproduce with Debug in staging (never production)."
|
|
345
|
+
},
|
|
312
346
|
{
|
|
313
347
|
"id": "ai-agents-modalities-vision",
|
|
314
348
|
"name": "modalities vision",
|
|
@@ -1756,6 +1790,14 @@
|
|
|
1756
1790
|
"ai-agents-production",
|
|
1757
1791
|
"ai-agents-middleware-patterns"
|
|
1758
1792
|
],
|
|
1793
|
+
"agent observability": [
|
|
1794
|
+
"ai-agents-production",
|
|
1795
|
+
"ai-agents-observability-patterns"
|
|
1796
|
+
],
|
|
1797
|
+
"agent telemetry": [
|
|
1798
|
+
"ai-agents-production",
|
|
1799
|
+
"ai-agents-observability-patterns"
|
|
1800
|
+
],
|
|
1759
1801
|
"when not to use workflow": [
|
|
1760
1802
|
"ai-agents-workflows",
|
|
1761
1803
|
"ai-agents-sweet-spot"
|