@polymorphism-tech/morph-spec 7.8.1 → 7.9.1
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 +2 -2
- package/bin/morph-spec.js +30 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -1
- package/framework/MORPH.md +4 -3
- package/framework/evals/trace-schema.md +22 -12
- package/framework/hooks/README.md +2 -0
- package/framework/hooks/claude-code/core/subagent-report-guard.js +95 -0
- package/framework/hooks/claude-code/core/trace-autogen.js +183 -0
- package/framework/hooks/shared/tasks-validator.js +47 -4
- package/framework/schemas/feature.schema.json +128 -0
- 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 +23 -10
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +3 -6
- package/framework/skills/core/morph-implement/SKILL.md +28 -15
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +2 -6
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +2 -1
- package/src/commands/project/doctor.js +23 -18
- package/src/commands/project/update.js +19 -9
- package/src/commands/state/create.js +58 -0
- package/src/commands/state/gate-decision.js +99 -0
- package/src/core/state/state-manager.js +55 -0
- package/src/scripts/setup-infra.js +2 -0
- package/src/utils/agents-installer.js +25 -10
- package/src/utils/banner.js +37 -4
- package/src/utils/hooks-installer.js +32 -4
- package/src/utils/skills-installer.js +2 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
|
|
6
|
+
/** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
|
|
7
|
+
const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
8
|
+
|
|
9
|
+
const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `morph-spec create <feature>` — explicit feature birth: creates
|
|
13
|
+
* `.morph/features/{feature}/0-proposal/` and the canonical feature.json
|
|
14
|
+
* (shape documented in framework/schemas/feature.schema.json).
|
|
15
|
+
*
|
|
16
|
+
* Replaces the implicit birth via the state-sync hook on the first Write —
|
|
17
|
+
* the skills call this at proposal start so the authoritative state exists
|
|
18
|
+
* with the full canonical shape before any artifact lands. Idempotent: an
|
|
19
|
+
* existing feature is reported and left untouched.
|
|
20
|
+
*/
|
|
21
|
+
export async function createCommand(featureName, options = {}) {
|
|
22
|
+
try {
|
|
23
|
+
if (!FEATURE_NAME_RE.test(featureName)) {
|
|
24
|
+
console.error(chalk.red(`\n❌ Invalid feature name: "${featureName}"`));
|
|
25
|
+
console.error(chalk.gray('Use kebab-case: lowercase letters, digits and hyphens (e.g. "user-onboarding")\n'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.status !== undefined && !VALID_STATUSES.includes(options.status)) {
|
|
30
|
+
console.error(chalk.red(`\n❌ Invalid --status: "${options.status}"`));
|
|
31
|
+
console.error(chalk.gray(`Valid statuses: ${VALID_STATUSES.join(', ')}\n`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { feature, created } = await createFeature(featureName, { status: options.status });
|
|
36
|
+
|
|
37
|
+
// The proposal folder is the first working directory of every feature.
|
|
38
|
+
const featureDir = join(process.cwd(), '.morph', 'features', featureName);
|
|
39
|
+
mkdirSync(join(featureDir, '0-proposal'), { recursive: true });
|
|
40
|
+
|
|
41
|
+
if (options.json) {
|
|
42
|
+
console.log(JSON.stringify({ feature: featureName, created, ...feature }, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (created) {
|
|
47
|
+
console.log(chalk.green(`\n✅ Feature created: ${featureName}`));
|
|
48
|
+
console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape) + 0-proposal/`));
|
|
49
|
+
console.log(chalk.gray(` status: ${feature.status} | createdAt: ${feature.createdAt}\n`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already exists — left untouched`));
|
|
52
|
+
console.log(chalk.gray(` status: ${feature.status} | createdAt: ${feature.createdAt || 'unknown'}\n`));
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(chalk.red(`\n❌ Error creating feature: ${error.message}\n`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
|
|
6
|
+
const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
|
|
7
|
+
|
|
8
|
+
function parsePauseValue(raw) {
|
|
9
|
+
const v = String(raw).toLowerCase();
|
|
10
|
+
if (v === 'true') return true;
|
|
11
|
+
if (v === 'false') return false;
|
|
12
|
+
console.error(chalk.red(`\n❌ Invalid --pause: "${raw}" — expected true or false\n`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** trust from the committed config (BOM-tolerant), defaulting to "auto". */
|
|
17
|
+
function readTrustFromConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const path = join(process.cwd(), '.morph', 'config', 'config.json');
|
|
20
|
+
if (!existsSync(path)) return 'auto';
|
|
21
|
+
const raw = readFileSync(path, 'utf8').replace(/^/, '');
|
|
22
|
+
return JSON.parse(raw).trust || 'auto';
|
|
23
|
+
} catch {
|
|
24
|
+
return 'auto';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>]
|
|
30
|
+
* [--signals <json>] [--trust <mode>]` — record the trust decision for a gate
|
|
31
|
+
* under `gateDecisions` in the authoritative feature.json.
|
|
32
|
+
*
|
|
33
|
+
* Same philosophy as `morph-spec score`: the timestamp (`decidedAt`) is
|
|
34
|
+
* stamped server-side and the write is atomic (state-manager). This replaces
|
|
35
|
+
* the manual Edit of feature.json from the skills — which collided with the
|
|
36
|
+
* state-sync hook rewriting the file between the agent's Read and Edit
|
|
37
|
+
* ("file modified since read"). The canonical shape is documented in
|
|
38
|
+
* framework/schemas/feature.schema.json and consumed by gate-guard.js.
|
|
39
|
+
*/
|
|
40
|
+
export async function gateDecisionCommand(featureName, gateName, options = {}) {
|
|
41
|
+
try {
|
|
42
|
+
if (!VALID_GATES.includes(gateName)) {
|
|
43
|
+
console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
|
|
44
|
+
console.error(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.pause === undefined) {
|
|
49
|
+
console.error(chalk.red('\n❌ --pause is required (true or false)\n'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const pause = parsePauseValue(options.pause);
|
|
53
|
+
|
|
54
|
+
const feature = getFeature(featureName);
|
|
55
|
+
if (!feature) {
|
|
56
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let signals = {};
|
|
61
|
+
if (options.signals !== undefined) {
|
|
62
|
+
try {
|
|
63
|
+
signals = JSON.parse(options.signals);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(chalk.red(`\n❌ Invalid --signals: not valid JSON (${err.message})\n`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!signals || typeof signals !== 'object' || Array.isArray(signals)) {
|
|
69
|
+
console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"taskCount":12}\'\n'));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const entry = {
|
|
75
|
+
pause,
|
|
76
|
+
reason: options.reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
77
|
+
signals,
|
|
78
|
+
trust: options.trust || readTrustFromConfig(),
|
|
79
|
+
decidedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
83
|
+
|
|
84
|
+
if (options.json) {
|
|
85
|
+
console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(chalk.green(`\n✅ Gate decision recorded: ${featureName}/${gateName} → pause: ${entry.pause}`));
|
|
90
|
+
console.log(chalk.gray(` reason: ${entry.reason} | trust: ${entry.trust} | decidedAt: ${entry.decidedAt}`));
|
|
91
|
+
if (Object.keys(entry.signals).length > 0) {
|
|
92
|
+
console.log(chalk.gray(` signals: ${JSON.stringify(entry.signals)}`));
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(chalk.red(`\n❌ Error recording gate decision: ${error.message}\n`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -484,6 +484,61 @@ async function ensureFeature(featureName) {
|
|
|
484
484
|
return state.features[featureName];
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Explicitly create a feature with the canonical shape (documented in
|
|
489
|
+
* framework/schemas/feature.schema.json). Idempotent: an existing feature is
|
|
490
|
+
* returned untouched. This is the server-side birth path behind
|
|
491
|
+
* `morph-spec create` — replacing the implicit registration on the first
|
|
492
|
+
* Write into .morph/features/ (the state-sync hook remains as fallback).
|
|
493
|
+
*
|
|
494
|
+
* Works even before any state exists (scaffolds the root index), so a
|
|
495
|
+
* feature can be born as the very first morph action in a project.
|
|
496
|
+
*
|
|
497
|
+
* @param {string} featureName - Feature name (kebab-case)
|
|
498
|
+
* @param {{ status?: string }} [options]
|
|
499
|
+
* @returns {Promise<{ feature: Object, created: boolean }>}
|
|
500
|
+
*/
|
|
501
|
+
export async function createFeature(featureName, options = {}) {
|
|
502
|
+
const { status = 'in_progress' } = options;
|
|
503
|
+
const state = loadState(false) || scaffoldState();
|
|
504
|
+
|
|
505
|
+
const existing = state.features[featureName];
|
|
506
|
+
if (existing) {
|
|
507
|
+
return { feature: existing, created: false };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const now = new Date().toISOString();
|
|
511
|
+
state.features[featureName] = {
|
|
512
|
+
status,
|
|
513
|
+
createdAt: now,
|
|
514
|
+
updatedAt: now,
|
|
515
|
+
activeAgents: [],
|
|
516
|
+
approvalGates: {
|
|
517
|
+
proposal: { approved: false, timestamp: null, approvedBy: null },
|
|
518
|
+
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
519
|
+
plan: { approved: false, timestamp: null, approvedBy: null },
|
|
520
|
+
review: { approved: false, timestamp: null, approvedBy: null }
|
|
521
|
+
},
|
|
522
|
+
gateDecisions: {},
|
|
523
|
+
taskScores: {},
|
|
524
|
+
tasks: {
|
|
525
|
+
total: 0,
|
|
526
|
+
completed: 0,
|
|
527
|
+
inProgress: 0,
|
|
528
|
+
pending: 0
|
|
529
|
+
},
|
|
530
|
+
checkpoints: [],
|
|
531
|
+
dispatchLog: [],
|
|
532
|
+
trustConfig: {},
|
|
533
|
+
contextBundles: [],
|
|
534
|
+
fileChanges: []
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
state.metadata.totalFeatures = Object.keys(state.features).length;
|
|
538
|
+
saveState(state);
|
|
539
|
+
return { feature: state.features[featureName], created: true };
|
|
540
|
+
}
|
|
541
|
+
|
|
487
542
|
/**
|
|
488
543
|
* Update feature property (supports nested keys like "tasks.completed")
|
|
489
544
|
* @param {string} featureName - Feature name
|
|
@@ -45,6 +45,26 @@ export function loadKnownMcpIds(frameworkDir) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Personas install-all default (friction fix, MORPH_GHLBrain 2026-07-11):
|
|
50
|
+
* with NO known stack tags (greenfield / install before config.json exists)
|
|
51
|
+
* EVERY persona is generated — a project that dispatches `morph-{id}` must
|
|
52
|
+
* find the subagent registered, and an install that only lands
|
|
53
|
+
* morph-evaluator forces every domain dispatch into the general-purpose
|
|
54
|
+
* fallback. Stack filtering only applies when the project's stack is KNOWN
|
|
55
|
+
* (tags non-empty): the fallback stays reserved for stacks the project
|
|
56
|
+
* genuinely does not have. Note this deliberately differs from rules/skills
|
|
57
|
+
* filtering (stack-filter.js), which stays minimal on greenfield.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} agent - agents.json entry (with `stack`)
|
|
60
|
+
* @param {string[]} projectTags
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function agentEligible(agent, projectTags = []) {
|
|
64
|
+
if (projectTags.length === 0) return true;
|
|
65
|
+
return shouldInstall(agent.stack ?? ['*'], projectTags);
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
const MANAGED_HEADER =
|
|
49
69
|
'<!-- MANAGED BY morph-spec — generated from .morph/framework/agents.json (single source of truth).\n' +
|
|
50
70
|
' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
|
|
@@ -75,11 +95,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
75
95
|
|
|
76
96
|
const knownMcpIds = loadKnownMcpIds(frameworkDir);
|
|
77
97
|
|
|
78
|
-
// Filter by stack
|
|
79
|
-
const eligible = agents.filter(a =>
|
|
80
|
-
const stack = a.stack ?? ['*'];
|
|
81
|
-
return shouldInstall(stack, projectTags);
|
|
82
|
-
});
|
|
98
|
+
// Filter by stack — install-all when the stack is unknown (empty tags).
|
|
99
|
+
const eligible = agents.filter(a => agentEligible(a, projectTags));
|
|
83
100
|
|
|
84
101
|
// Build set of all possible filenames for orphan cleanup
|
|
85
102
|
const allPossible = new Set(agents.map(a => `morph-${a.id}.md`));
|
|
@@ -231,12 +248,11 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
231
248
|
let expected = 0;
|
|
232
249
|
|
|
233
250
|
for (const a of agents) {
|
|
234
|
-
|
|
235
|
-
if (!shouldInstall(stack, projectTags)) continue;
|
|
251
|
+
if (!agentEligible(a, projectTags)) continue;
|
|
236
252
|
expected++;
|
|
237
253
|
const filename = `morph-${a.id}.md`;
|
|
238
254
|
if (!installedFiles.has(filename)) {
|
|
239
|
-
missing.push({ name: a.title ?? a.id, stacks: stack });
|
|
255
|
+
missing.push({ name: a.title ?? a.id, stacks: a.stack ?? ['*'] });
|
|
240
256
|
}
|
|
241
257
|
}
|
|
242
258
|
|
|
@@ -276,8 +292,7 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
|
|
|
276
292
|
let expected = 0;
|
|
277
293
|
|
|
278
294
|
for (const agent of agents) {
|
|
279
|
-
|
|
280
|
-
if (!shouldInstall(stack, projectTags)) continue;
|
|
295
|
+
if (!agentEligible(agent, projectTags)) continue;
|
|
281
296
|
expected++;
|
|
282
297
|
const filename = `morph-${agent.id}.md`;
|
|
283
298
|
const filePath = join(targetDir, filename);
|
package/src/utils/banner.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/utils/banner.js
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { type as osType, release as osRelease } from "os";
|
|
3
|
+
import { type as osType, release as osRelease, homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
6
|
|
|
5
7
|
const BRAND = chalk.hex("#7C3AED");
|
|
6
8
|
const DIM = chalk.dim;
|
|
@@ -54,6 +56,39 @@ export function renderLogo(version) {
|
|
|
54
56
|
return `\n${art}\n${rule}\n`;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Print the install banner once per version change, on the first CLI run
|
|
61
|
+
* after an install/upgrade. npm ≥7 swallows lifecycle-script output, so the
|
|
62
|
+
* postinstall banner is invisible on a normal `npm i -g` — this is the
|
|
63
|
+
* fallback that guarantees the next-steps guidance is actually seen.
|
|
64
|
+
*
|
|
65
|
+
* Marker: ~/.claude/morph-spec-banner-version (last version shown).
|
|
66
|
+
* Skips silently when stdout is not a TTY (piped/parsed output) or when
|
|
67
|
+
* MORPH_QUIET=1. Fail-open: any fs error just skips the banner.
|
|
68
|
+
*
|
|
69
|
+
* @param {{version: string, nodeVersion: string, markerDir?: string}} opts
|
|
70
|
+
* @returns {boolean} true if the banner was printed
|
|
71
|
+
*/
|
|
72
|
+
export function maybePrintFirstRunBanner({ version, nodeVersion, markerDir }) {
|
|
73
|
+
try {
|
|
74
|
+
if (!process.stdout.isTTY) return false;
|
|
75
|
+
if (process.env.MORPH_QUIET === "1") return false;
|
|
76
|
+
|
|
77
|
+
const dir = markerDir ?? join(homedir(), ".claude");
|
|
78
|
+
const marker = join(dir, "morph-spec-banner-version");
|
|
79
|
+
const last = existsSync(marker) ? readFileSync(marker, "utf8").trim() : null;
|
|
80
|
+
if (last === version) return false;
|
|
81
|
+
|
|
82
|
+
mkdirSync(dir, { recursive: true });
|
|
83
|
+
writeFileSync(marker, version, "utf8");
|
|
84
|
+
|
|
85
|
+
printInstallBanner({ version, nodeVersion, results: [] });
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
function platformLabel() {
|
|
58
93
|
const t = osType();
|
|
59
94
|
const r = osRelease().split(".").slice(0, 2).join(".");
|
|
@@ -78,9 +113,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
|
|
|
78
113
|
` ${DIM("Spec-driven development harness for Claude Code")}`,
|
|
79
114
|
` ${DIM("by Polymorphism Tech")} ${DIM("·")} ${DIM("Node")} ${chalk.white(nodeVersion)} ${DIM("·")} ${chalk.white(platformLabel())}`,
|
|
80
115
|
"",
|
|
81
|
-
sep("Installed"),
|
|
82
|
-
...itemLines,
|
|
83
|
-
"",
|
|
116
|
+
...(itemLines.length > 0 ? [sep("Installed"), ...itemLines, ""] : []),
|
|
84
117
|
sep("Next · new project"),
|
|
85
118
|
` ${chalk.cyan("1.")} ${DIM("cd")} ${chalk.white("your-project")} ${DIM("(git repo)")}`,
|
|
86
119
|
` ${chalk.cyan("2.")} Run ${chalk.bold.white("morph-spec init")}`,
|
|
@@ -15,7 +15,7 @@ import { homedir } from 'os';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
|
|
17
17
|
/** Current hooks schema version — bump when hook definitions change */
|
|
18
|
-
const HOOKS_VERSION = '3.
|
|
18
|
+
const HOOKS_VERSION = '3.1.0';
|
|
19
19
|
|
|
20
20
|
/** Marker for old dispatch.js (v1) */
|
|
21
21
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
@@ -103,7 +103,9 @@ const MORPH_HOOKS = [
|
|
|
103
103
|
},
|
|
104
104
|
|
|
105
105
|
// === PostToolUse: Write|Edit — typecheck (debounced, silent on success) +
|
|
106
|
-
// tasks.json schema guard (validate-and-warn; status enum, effort enum)
|
|
106
|
+
// tasks.json schema guard (validate-and-warn; status enum, effort enum) +
|
|
107
|
+
// trace auto-generation (tasks.json write → .morph/traces/, replaces the
|
|
108
|
+
// manual trace-writing step the skills used to carry) ===
|
|
107
109
|
{
|
|
108
110
|
event: 'PostToolUse',
|
|
109
111
|
matcher: 'Write|Edit',
|
|
@@ -116,6 +118,11 @@ const MORPH_HOOKS = [
|
|
|
116
118
|
type: 'command',
|
|
117
119
|
command: 'node framework/hooks/claude-code/core/tasks-json-guard.js',
|
|
118
120
|
timeout: 3000
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'command',
|
|
124
|
+
command: 'node framework/hooks/claude-code/core/trace-autogen.js',
|
|
125
|
+
timeout: 3000
|
|
119
126
|
}
|
|
120
127
|
]
|
|
121
128
|
},
|
|
@@ -153,13 +160,34 @@ const MORPH_HOOKS = [
|
|
|
153
160
|
}]
|
|
154
161
|
},
|
|
155
162
|
|
|
156
|
-
// === Stop — build check + session snapshot ===
|
|
163
|
+
// === Stop — build check + session snapshot + dispatch-report guard ===
|
|
164
|
+
// subagent-report-guard blocks (once) a dispatched sub-agent stopping
|
|
165
|
+
// without SendMessage — clause 1 of the dispatch protocol. In the
|
|
166
|
+
// orchestrator's session it is inert (marker never in a user message).
|
|
157
167
|
{
|
|
158
168
|
event: 'Stop',
|
|
159
169
|
matcher: null,
|
|
170
|
+
hooks: [
|
|
171
|
+
{
|
|
172
|
+
type: 'command',
|
|
173
|
+
command: 'node framework/hooks/claude-code/core/validate-completion.js'
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'command',
|
|
177
|
+
command: 'node framework/hooks/claude-code/core/subagent-report-guard.js',
|
|
178
|
+
timeout: 5000
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// === SubagentStop — dispatch-report guard for Task-tool subagents ===
|
|
184
|
+
{
|
|
185
|
+
event: 'SubagentStop',
|
|
186
|
+
matcher: null,
|
|
160
187
|
hooks: [{
|
|
161
188
|
type: 'command',
|
|
162
|
-
command: 'node framework/hooks/claude-code/core/
|
|
189
|
+
command: 'node framework/hooks/claude-code/core/subagent-report-guard.js',
|
|
190
|
+
timeout: 5000
|
|
163
191
|
}]
|
|
164
192
|
}
|
|
165
193
|
];
|