@polymorphism-tech/morph-spec 4.8.19 → 4.10.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 +21 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +44 -55
- package/bin/task-manager.js +133 -20
- package/bin/validate.js +67 -33
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +201 -203
- package/docs/QUICKSTART.md +2 -2
- package/framework/CLAUDE.md +99 -77
- package/framework/agents.json +734 -182
- package/framework/commands/commit.md +166 -0
- package/framework/commands/morph-apply.md +13 -2
- package/framework/commands/morph-archive.md +8 -2
- package/framework/commands/morph-infra.md +6 -0
- package/framework/commands/morph-preflight.md +6 -0
- package/framework/commands/morph-proposal.md +56 -7
- package/framework/commands/morph-status.md +6 -0
- package/framework/commands/morph-troubleshoot.md +6 -0
- package/framework/hooks/claude-code/notification/approval-reminder.js +3 -2
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +155 -32
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +78 -0
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +8 -17
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +16 -3
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +4 -3
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +4 -3
- package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +60 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +124 -2
- package/framework/hooks/claude-code/session-start/post-compact-restore.js +41 -0
- package/framework/hooks/claude-code/statusline.py +76 -30
- package/framework/hooks/claude-code/stop/validate-completion.js +2 -15
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +23 -5
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
- package/framework/hooks/shared/activity-logger.js +0 -24
- package/framework/hooks/shared/compact-restore.js +100 -0
- package/framework/hooks/shared/dispatch-helpers.js +116 -0
- package/framework/hooks/shared/phase-utils.js +12 -5
- package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
- package/framework/hooks/shared/stale-task-reset.js +57 -0
- package/framework/hooks/shared/state-reader.js +29 -5
- package/framework/hooks/shared/worktree-helpers.js +53 -0
- package/framework/phases.json +69 -14
- package/framework/rules/morph-workflow.md +88 -86
- package/framework/skills/level-0-meta/mcp-registry.json +86 -51
- package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/SKILL.md +14 -17
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{code-review → morph-code-review}/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/SKILL.md +163 -163
- package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/SKILL.md +9 -9
- package/framework/skills/level-0-meta/morph-init/SKILL.md +77 -12
- package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/SKILL.md +62 -15
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
- package/framework/skills/level-0-meta/{simulation-checklist → morph-simulation-checklist}/SKILL.md +1 -1
- package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/SKILL.md +3 -4
- package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/references/tools-per-phase.md +7 -7
- package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/scripts/check-phase-outputs.mjs +2 -2
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +238 -0
- package/framework/skills/level-1-workflows/{phase-codebase-analysis → morph-phase-codebase-analysis}/SKILL.md +3 -3
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +507 -0
- package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/SKILL.md +168 -27
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +50 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +45 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +47 -0
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +254 -0
- package/framework/skills/level-1-workflows/{phase-setup → morph-phase-setup}/SKILL.md +50 -3
- package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/SKILL.md +48 -11
- package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/scripts/validate-tasks.mjs +3 -3
- package/framework/skills/level-1-workflows/{phase-uiux → morph-phase-uiux}/SKILL.md +46 -11
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +97 -0
- package/framework/standards/STANDARDS.json +640 -88
- package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
- package/framework/standards/integration/mcp/mcp-tools.md +25 -7
- package/framework/templates/REGISTRY.json +1825 -1909
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/onboarding.md +3 -7
- package/package.json +2 -7
- package/src/commands/agents/dispatch-agents.js +104 -6
- package/src/commands/mcp/mcp-setup.js +39 -2
- package/src/commands/phase/phase-reset.js +74 -0
- package/src/commands/project/doctor.js +34 -51
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/status.js +2 -2
- package/src/commands/project/update.js +381 -365
- package/src/commands/project/worktree.js +154 -0
- package/src/commands/scope/escalate.js +215 -0
- package/src/commands/state/advance-phase.js +132 -68
- package/src/commands/state/approve.js +2 -2
- package/src/commands/state/index.js +7 -8
- package/src/commands/state/phase-runner.js +1 -1
- package/src/commands/state/state.js +61 -6
- package/src/commands/task/expand.js +100 -0
- package/src/commands/tasks/task.js +78 -99
- package/src/commands/templates/template-render.js +93 -173
- package/src/commands/trust/trust.js +26 -21
- package/src/core/paths/output-schema.js +19 -3
- package/src/core/state/phase-state-machine.js +7 -4
- package/src/core/state/state-manager.js +32 -57
- package/src/core/workflows/workflow-detector.js +9 -87
- package/src/lib/detectors/claude-config-detector.js +93 -347
- package/src/lib/detectors/design-system-detector.js +189 -189
- package/src/lib/detectors/index.js +155 -57
- package/src/lib/generators/context-generator.js +2 -2
- package/src/lib/installers/mcp-installer.js +37 -5
- package/src/lib/phase-chain/phase-validator.js +336 -0
- package/src/lib/scope/impact-analyzer.js +106 -0
- package/src/lib/stack/stack-profile.js +88 -0
- package/src/lib/tasks/task-classifier.js +16 -0
- package/src/lib/tasks/task-parser.js +1 -1
- package/src/lib/tasks/test-runner.js +77 -0
- package/src/lib/trust/trust-manager.js +32 -144
- package/src/lib/validators/shared/emit-validator-dispatch.js +64 -0
- package/src/lib/validators/spec-validator.js +58 -4
- package/src/lib/validators/validation-runner.js +23 -11
- package/src/scripts/setup-infra.js +255 -224
- package/src/utils/agents-installer.js +34 -14
- package/src/utils/banner.js +1 -1
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +1 -1
- package/src/utils/hooks-installer.js +272 -8
- package/framework/hooks/dev/check-sync-health.js +0 -117
- package/framework/hooks/dev/guard-version-numbers.js +0 -57
- package/framework/hooks/dev/sync-standards-registry.js +0 -60
- package/framework/hooks/dev/sync-template-registry.js +0 -60
- package/framework/hooks/dev/validate-skill-format.js +0 -70
- package/framework/hooks/dev/validate-standard-format.js +0 -73
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +0 -190
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +0 -366
- package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
- package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
- package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
- package/framework/workflows/configs/design-impl.json +0 -49
- package/framework/workflows/configs/express.json +0 -45
- package/framework/workflows/configs/fast-track.json +0 -42
- package/framework/workflows/configs/full-morph.json +0 -79
- package/framework/workflows/configs/fusion.json +0 -39
- package/framework/workflows/configs/long-running.json +0 -33
- package/framework/workflows/configs/spec-only.json +0 -43
- package/framework/workflows/configs/ui-refresh.json +0 -49
- package/framework/workflows/configs/zero-touch.json +0 -82
- package/src/commands/project/index.js +0 -8
- package/src/commands/project/monitor.js +0 -295
- package/src/commands/project/tutorial.js +0 -115
- package/src/commands/state/validate-phase.js +0 -238
- package/src/commands/templates/generate-contracts.js +0 -445
- package/src/core/index.js +0 -10
- package/src/core/orchestrator.js +0 -171
- package/src/core/registry/command-registry.js +0 -28
- package/src/core/registry/index.js +0 -8
- package/src/core/registry/validator-registry.js +0 -204
- package/src/core/state/index.js +0 -8
- package/src/core/templates/index.js +0 -9
- package/src/core/templates/template-data-sources.js +0 -325
- package/src/core/templates/template-validator.js +0 -296
- package/src/core/workflows/index.js +0 -7
- package/src/generator/config-generator.js +0 -206
- package/src/generator/templates/config.json.template +0 -40
- package/src/generator/templates/project.md.template +0 -67
- package/src/lib/agents/micro-agent-factory.js +0 -161
- package/src/lib/analysis/complexity-analyzer.js +0 -441
- package/src/lib/analysis/index.js +0 -7
- package/src/lib/analytics/analytics-engine.js +0 -345
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
- package/src/lib/checkpoints/index.js +0 -7
- package/src/lib/context/context-bundler.js +0 -241
- package/src/lib/context/context-optimizer.js +0 -212
- package/src/lib/context/context-tracker.js +0 -273
- package/src/lib/context/core-four-tracker.js +0 -201
- package/src/lib/context/mcp-optimizer.js +0 -200
- package/src/lib/detectors/config-detector.js +0 -223
- package/src/lib/detectors/standards-generator.js +0 -335
- package/src/lib/detectors/structure-detector.js +0 -275
- package/src/lib/execution/fusion-executor.js +0 -304
- package/src/lib/execution/parallel-executor.js +0 -270
- package/src/lib/hooks/stop-hook-executor.js +0 -286
- package/src/lib/hops/hop-composer.js +0 -221
- package/src/lib/monitor/agent-resolver.js +0 -144
- package/src/lib/monitor/renderer.js +0 -230
- package/src/lib/orchestration/index.js +0 -7
- package/src/lib/orchestration/team-orchestrator.js +0 -404
- package/src/lib/phase-chain/eligibility-checker.js +0 -243
- package/src/lib/threads/thread-coordinator.js +0 -238
- package/src/lib/threads/thread-manager.js +0 -317
- package/src/lib/tracking/artifact-trail.js +0 -202
- package/src/sanitizer/context-sanitizer.js +0 -221
- package/src/sanitizer/patterns.js +0 -163
- package/src/scanner/project-scanner.js +0 -242
- package/src/ui/diff-display.js +0 -91
- package/src/ui/interactive-wizard.js +0 -96
- package/src/ui/user-review.js +0 -211
- package/src/ui/wizard-questions.js +0 -188
- package/src/utils/color-utils.js +0 -70
- package/src/utils/process-handler.js +0 -97
- package/src/writer/file-writer.js +0 -86
- /package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/references/proposal-example.md +0 -0
- /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-example.md +0 -0
- /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-guidelines.md +0 -0
- /package/framework/skills/level-0-meta/{code-review → morph-code-review}/scripts/scan-csharp.mjs +0 -0
- /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/references/review-example-nextjs.md +0 -0
- /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/scripts/scan-nextjs.mjs +0 -0
- /package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/scripts/scan-accessibility.mjs +0 -0
- /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-dev-server.mjs +0 -0
- /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-stack.mjs +0 -0
- /package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/scripts/set_title.sh +0 -0
- /package/framework/skills/level-1-workflows/{phase-clarify → morph-phase-clarify}/references/clarifications-example.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/architecture-analysis-guide.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-authoring-guide.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-example.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/recap-example.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/vsa-implementation-guide.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/task-planning-patterns.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/tasks-example.md +0 -0
|
@@ -15,8 +15,11 @@ import { loadState, getActiveFeature, getPendingGates, getMissingOutputs, derive
|
|
|
15
15
|
import { stateExists } from '../../shared/state-reader.js';
|
|
16
16
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
17
17
|
import { resetActivity, logHookActivity } from '../../shared/activity-logger.js';
|
|
18
|
-
import { readFileSync, existsSync } from 'fs';
|
|
18
|
+
import { readFileSync, existsSync, writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
|
+
import { resetStaleTasks } from '../../shared/stale-task-reset.js';
|
|
21
|
+
import { execSync as _execSync } from 'child_process';
|
|
22
|
+
import { parseWorktreeResult, buildWorktreeContextLine } from '../../shared/worktree-helpers.js';
|
|
20
23
|
|
|
21
24
|
const DEFAULT_SPEC_MAX_CHARS = 3000;
|
|
22
25
|
|
|
@@ -43,6 +46,26 @@ function getProjectConfig() {
|
|
|
43
46
|
|
|
44
47
|
const SPEC_MAX_CHARS = getSpecMaxChars();
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Atomic state save for hook context.
|
|
51
|
+
* Same approach as state-manager.js saveState — write to tmp, then rename.
|
|
52
|
+
* Fail-open: any error is silently ignored.
|
|
53
|
+
* @param {Object} state - Full state object
|
|
54
|
+
* @param {string} cwd - Project root path
|
|
55
|
+
*/
|
|
56
|
+
function saveStateSync(state, cwd) {
|
|
57
|
+
const statePath = join(cwd, '.morph/state.json');
|
|
58
|
+
const tmpPath = `${statePath}.tmp.hook.${process.pid}`;
|
|
59
|
+
try {
|
|
60
|
+
state.metadata = state.metadata || {};
|
|
61
|
+
state.metadata.lastUpdated = new Date().toISOString();
|
|
62
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
|
|
63
|
+
renameSync(tmpPath, statePath);
|
|
64
|
+
} catch {
|
|
65
|
+
try { unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
try {
|
|
47
70
|
if (!stateExists()) pass();
|
|
48
71
|
|
|
@@ -57,7 +80,19 @@ try {
|
|
|
57
80
|
resetActivity(new Date().toISOString(), activeFeatureName, activePhase);
|
|
58
81
|
logHookActivity('inject-morph-context', 'SessionStart', 'ok');
|
|
59
82
|
|
|
83
|
+
// ── Stale task cleanup ──────────────────────────────────────────────────────
|
|
84
|
+
// Reset any in_progress tasks older than 1 hour — they are orphans from an
|
|
85
|
+
// interrupted session and are no longer being actively implemented.
|
|
86
|
+
const staleResetLog = resetStaleTasks(state);
|
|
87
|
+
if (staleResetLog.length > 0) {
|
|
88
|
+
saveStateSync(state, process.cwd());
|
|
89
|
+
}
|
|
90
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
60
92
|
const lines = ['MORPH-SPEC Status:'];
|
|
93
|
+
if (staleResetLog.length > 0) {
|
|
94
|
+
lines.push(`⚠️ ${staleResetLog.length} task(s) auto-reset in_progress → pending (orphaned from previous session): ${staleResetLog.join(', ')}`);
|
|
95
|
+
}
|
|
61
96
|
|
|
62
97
|
if (active) {
|
|
63
98
|
const { name, feature } = active;
|
|
@@ -129,6 +164,40 @@ try {
|
|
|
129
164
|
// Non-blocking: skip spec injection on read error
|
|
130
165
|
}
|
|
131
166
|
}
|
|
167
|
+
|
|
168
|
+
// ── Worktree setup ──────────────────────────────────────────────────────────
|
|
169
|
+
// For the active feature, create or detect an existing git worktree.
|
|
170
|
+
// Trigger: uses getActiveFeature() (status-based) rather than checking
|
|
171
|
+
// activeAgents, because status is the canonical signal already used by the
|
|
172
|
+
// rest of this hook. activeAgents is populated later in the spec pipeline,
|
|
173
|
+
// so a status-based gate ensures the worktree is always created before work
|
|
174
|
+
// begins — even for features still in early phases.
|
|
175
|
+
// Fail-open: any error is silently ignored.
|
|
176
|
+
try {
|
|
177
|
+
let worktreeStdout = '';
|
|
178
|
+
try {
|
|
179
|
+
worktreeStdout = _execSync(`npx morph-spec worktree setup ${active.name}`, {
|
|
180
|
+
cwd: process.cwd(),
|
|
181
|
+
stdio: 'pipe',
|
|
182
|
+
timeout: 10000
|
|
183
|
+
}).toString();
|
|
184
|
+
} catch (execErr) {
|
|
185
|
+
// exit code 2 = already exists — stdout still has the JSON result
|
|
186
|
+
worktreeStdout = execErr.stdout?.toString() || '';
|
|
187
|
+
}
|
|
188
|
+
const worktreeResult = parseWorktreeResult(worktreeStdout);
|
|
189
|
+
if (worktreeResult) {
|
|
190
|
+
worktreeResult.feature = active.name; // enrich for context builder
|
|
191
|
+
const contextLine = buildWorktreeContextLine(worktreeResult);
|
|
192
|
+
if (contextLine) {
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push(contextLine);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Fail-open — worktree setup must never block the session
|
|
199
|
+
}
|
|
200
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
132
201
|
} else {
|
|
133
202
|
// Show summary of all features
|
|
134
203
|
const featureNames = Object.keys(state.features);
|
|
@@ -140,6 +209,59 @@ try {
|
|
|
140
209
|
}
|
|
141
210
|
}
|
|
142
211
|
|
|
212
|
+
// ── MCP status injection ────────────────────────────────────────────────────
|
|
213
|
+
// Cross-reference configured MCPs with phase recommendations from mcp-registry.json.
|
|
214
|
+
// Fail-open: all inside try/catch, no crash on missing files.
|
|
215
|
+
try {
|
|
216
|
+
const currentPhase = active ? derivePhaseForFeature(active.name) : '';
|
|
217
|
+
if (currentPhase) {
|
|
218
|
+
// Read configured MCPs from settings
|
|
219
|
+
const configuredMcps = new Set();
|
|
220
|
+
const settingsFiles = [
|
|
221
|
+
join(process.cwd(), '.claude', 'settings.local.json'),
|
|
222
|
+
join(process.cwd(), '.claude', 'settings.json'),
|
|
223
|
+
];
|
|
224
|
+
for (const sf of settingsFiles) {
|
|
225
|
+
if (existsSync(sf)) {
|
|
226
|
+
try {
|
|
227
|
+
const s = JSON.parse(readFileSync(sf, 'utf8'));
|
|
228
|
+
for (const name of Object.keys(s.mcpServers || {})) {
|
|
229
|
+
configuredMcps.add(name.toLowerCase());
|
|
230
|
+
}
|
|
231
|
+
} catch { /* ignore */ }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Read recommended MCPs from phases.json or mcp-registry.json
|
|
236
|
+
let recommended = [];
|
|
237
|
+
const registryPaths = [
|
|
238
|
+
join(process.cwd(), 'framework', 'skills', 'level-0-meta', 'mcp-registry.json'),
|
|
239
|
+
join(process.cwd(), '.morph', 'framework', 'skills', 'level-0-meta', 'mcp-registry.json'),
|
|
240
|
+
];
|
|
241
|
+
for (const rp of registryPaths) {
|
|
242
|
+
if (existsSync(rp)) {
|
|
243
|
+
try {
|
|
244
|
+
const registry = JSON.parse(readFileSync(rp, 'utf8'));
|
|
245
|
+
recommended = registry.phaseMatrix?.[currentPhase] || [];
|
|
246
|
+
break;
|
|
247
|
+
} catch { /* ignore */ }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (recommended.length > 0) {
|
|
252
|
+
const mcpStatus = recommended.map(name => {
|
|
253
|
+
const isConfigured = configuredMcps.has(name.toLowerCase());
|
|
254
|
+
return `${name} ${isConfigured ? '✓' : '✗ (recommended)'}`;
|
|
255
|
+
});
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push(`MCPs: ${mcpStatus.join(', ')}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// Fail-open — MCP status must never block the session
|
|
262
|
+
}
|
|
263
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
143
265
|
// Remind about key commands
|
|
144
266
|
lines.push('');
|
|
145
267
|
lines.push('Key commands: morph-spec status <feature> | morph-spec phase advance <feature> | morph-spec approve <feature> <gate>');
|
|
@@ -151,7 +273,7 @@ try {
|
|
|
151
273
|
lines.push('');
|
|
152
274
|
lines.push('── MORPH SYSTEM MAP ─────────────────────────────────────────────');
|
|
153
275
|
lines.push(`🪝 Hooks: 10 registrados | 📏 Rules: em .claude/rules/ | 🎯 Skills: em .claude/skills/`);
|
|
154
|
-
lines.push(`
|
|
276
|
+
lines.push(` Use \`morph-spec doctor\` para health check | \`morph-spec status ${name}\` para dashboard`);
|
|
155
277
|
lines.push('─────────────────────────────────────────────────────────────────');
|
|
156
278
|
}
|
|
157
279
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SessionStart Hook: Post-Compact Context Restore
|
|
5
|
+
*
|
|
6
|
+
* Event: SessionStart | Matcher: compact
|
|
7
|
+
*
|
|
8
|
+
* Fires ONLY when a session starts after context compaction.
|
|
9
|
+
* Reads the latest pre-compact memory file and injects the richContext
|
|
10
|
+
* block (decisions.md snippet + task list) so Claude resumes with full
|
|
11
|
+
* morph awareness even if the compact summary was truncated.
|
|
12
|
+
*
|
|
13
|
+
* Fail-open: exits 0 on any error.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
19
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
20
|
+
import { buildRestoreBlock, findLatestMemoryFile } from '../../shared/compact-restore.js';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (!stateExists()) pass();
|
|
24
|
+
|
|
25
|
+
const memoryDir = join(process.cwd(), '.morph', 'memory');
|
|
26
|
+
if (!existsSync(memoryDir)) pass();
|
|
27
|
+
|
|
28
|
+
const files = readdirSync(memoryDir);
|
|
29
|
+
const latest = findLatestMemoryFile(files);
|
|
30
|
+
if (!latest) pass();
|
|
31
|
+
|
|
32
|
+
const raw = readFileSync(join(memoryDir, latest), 'utf-8');
|
|
33
|
+
const snapshot = JSON.parse(raw);
|
|
34
|
+
|
|
35
|
+
const block = buildRestoreBlock(snapshot);
|
|
36
|
+
if (!block) pass();
|
|
37
|
+
|
|
38
|
+
injectContext(block);
|
|
39
|
+
} catch {
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
@@ -151,7 +151,10 @@ def get_session_feature_names(features_dict, entries):
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def get_all_active_features(cwd, entries):
|
|
154
|
-
"""Return in_progress features that are active in the current session.
|
|
154
|
+
"""Return in_progress features that are active in the current session.
|
|
155
|
+
|
|
156
|
+
Caller is responsible for only calling this when inside a secondary worktree.
|
|
157
|
+
"""
|
|
155
158
|
state_path = Path(cwd) / '.morph' / 'state.json'
|
|
156
159
|
if not state_path.exists():
|
|
157
160
|
return []
|
|
@@ -162,11 +165,6 @@ def get_all_active_features(cwd, entries):
|
|
|
162
165
|
# Filter to features belonging to this session (mentioned in transcript)
|
|
163
166
|
session_names = get_session_feature_names(features, entries)
|
|
164
167
|
|
|
165
|
-
# Auto-detect: if only one feature is in_progress, show it regardless of transcript
|
|
166
|
-
in_progress = [n for n, f in features.items() if f.get('status') == 'in_progress']
|
|
167
|
-
if len(in_progress) == 1:
|
|
168
|
-
session_names.add(in_progress[0])
|
|
169
|
-
|
|
170
168
|
if not session_names:
|
|
171
169
|
return []
|
|
172
170
|
|
|
@@ -310,8 +308,14 @@ def get_git_info(cwd):
|
|
|
310
308
|
return ""
|
|
311
309
|
|
|
312
310
|
|
|
313
|
-
def
|
|
314
|
-
"""Detect
|
|
311
|
+
def get_worktree_data(cwd):
|
|
312
|
+
"""Detect worktree status for cwd.
|
|
313
|
+
|
|
314
|
+
Returns (is_secondary: bool, display_str: str).
|
|
315
|
+
is_secondary is True only when cwd is a non-primary git worktree.
|
|
316
|
+
display_str is the formatted label (non-empty only when is_secondary).
|
|
317
|
+
Called once in main() to avoid duplicate git subprocess.
|
|
318
|
+
"""
|
|
315
319
|
try:
|
|
316
320
|
out = _run_git(['worktree', 'list', '--porcelain'], cwd)
|
|
317
321
|
entries, current = [], {}
|
|
@@ -329,10 +333,45 @@ def get_worktree_info(cwd):
|
|
|
329
333
|
for entry in entries[1:]:
|
|
330
334
|
if str(Path(entry.get('path', '')).resolve()) == cwd_r:
|
|
331
335
|
branch = entry.get('branch', '').replace('refs/heads/', '')
|
|
332
|
-
return f"{MAGENTA}worktree:{branch}{R}"
|
|
336
|
+
return True, f"{MAGENTA}worktree:{branch}{R}"
|
|
333
337
|
except Exception:
|
|
334
338
|
pass
|
|
335
|
-
return ""
|
|
339
|
+
return False, ""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_recent_tool_calls(entries):
|
|
343
|
+
"""Scan the last 50 transcript entries for the most recent Skill and Agent tool calls.
|
|
344
|
+
|
|
345
|
+
Returns (last_skill_name, last_agent_name) — either may be None.
|
|
346
|
+
Looks at assistant messages containing tool_use blocks.
|
|
347
|
+
"""
|
|
348
|
+
last_skill = None
|
|
349
|
+
last_agent = None
|
|
350
|
+
recent = entries[-50:] if len(entries) > 50 else entries
|
|
351
|
+
for entry in reversed(recent):
|
|
352
|
+
if entry.get('isSidechain') or entry.get('isApiErrorMessage'):
|
|
353
|
+
continue
|
|
354
|
+
msg = entry.get('message') or {}
|
|
355
|
+
content = msg.get('content') or []
|
|
356
|
+
if not isinstance(content, list):
|
|
357
|
+
continue
|
|
358
|
+
# Within each entry, scan content from last to first to get most-recent call
|
|
359
|
+
for item in reversed(content):
|
|
360
|
+
if not isinstance(item, dict) or item.get('type') != 'tool_use':
|
|
361
|
+
continue
|
|
362
|
+
tool = item.get('name', '')
|
|
363
|
+
inp = item.get('input') or {}
|
|
364
|
+
if tool == 'Skill' and last_skill is None:
|
|
365
|
+
last_skill = inp.get('skill') or ''
|
|
366
|
+
elif tool == 'Agent' and last_agent is None:
|
|
367
|
+
agent_name = inp.get('subagent_type') or ''
|
|
368
|
+
if not agent_name:
|
|
369
|
+
desc = inp.get('description') or inp.get('prompt') or ''
|
|
370
|
+
agent_name = (desc[:22] + '…') if len(desc) > 25 else desc
|
|
371
|
+
last_agent = agent_name
|
|
372
|
+
if last_skill is not None and last_agent is not None:
|
|
373
|
+
break
|
|
374
|
+
return last_skill or None, last_agent or None
|
|
336
375
|
|
|
337
376
|
|
|
338
377
|
# ── Transcript / JSONL helpers ────────────────────────────────────────────────
|
|
@@ -493,12 +532,18 @@ def main():
|
|
|
493
532
|
cwd = data.get('cwd', os.getcwd())
|
|
494
533
|
transcript_path = data.get('transcript_path')
|
|
495
534
|
|
|
496
|
-
# Read JSONL transcript once — shared by session clock,
|
|
497
|
-
#
|
|
535
|
+
# Read JSONL transcript once — shared by session clock, token metrics,
|
|
536
|
+
# session name, and skill/agent detection.
|
|
498
537
|
entries = read_transcript_jsonl(transcript_path) if transcript_path else []
|
|
499
538
|
|
|
539
|
+
# ── Worktree detection (single git call) ─────────────────────────────────
|
|
540
|
+
# Feature lines are only shown when inside a secondary git worktree.
|
|
541
|
+
# This prevents stale feature names showing up during unrelated work
|
|
542
|
+
# in the main worktree.
|
|
543
|
+
is_worktree, wt_display = get_worktree_data(cwd)
|
|
544
|
+
|
|
500
545
|
# ── MORPH feature lines (one line per active feature) ────────────────────
|
|
501
|
-
features = get_all_active_features(cwd, entries)
|
|
546
|
+
features = get_all_active_features(cwd, entries) if is_worktree else []
|
|
502
547
|
for feat in features:
|
|
503
548
|
# Feature name with visual prefix
|
|
504
549
|
parts = [f"{CYAN}{BOLD}► {feat['name']}{R}"]
|
|
@@ -536,19 +581,14 @@ def main():
|
|
|
536
581
|
|
|
537
582
|
print(' | '.join(parts))
|
|
538
583
|
|
|
539
|
-
# ── Activity info line (hooks
|
|
540
|
-
if features:
|
|
584
|
+
# ── Activity info line (hooks; only shown when a feature is active) ───────
|
|
585
|
+
if features:
|
|
541
586
|
activity = get_activity_info(cwd)
|
|
542
|
-
if activity and
|
|
543
|
-
|
|
544
|
-
if activity['
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
act_parts.append(f"{BLUE}🪝 {hook_label} {age_str}{R}".strip())
|
|
548
|
-
if activity['skill_count'] > 0:
|
|
549
|
-
act_parts.append(f"{YELLOW}🎯 {activity['skill_count']} skill(s){R}")
|
|
550
|
-
if act_parts:
|
|
551
|
-
print(f" {GRAY}└{R} " + f" {GRAY}|{R} ".join(act_parts))
|
|
587
|
+
if activity and activity['hook_count'] > 0:
|
|
588
|
+
hook_label = activity['last_hook'] or '?'
|
|
589
|
+
age_str = f"({activity['last_hook_age']})" if activity['last_hook_age'] else ''
|
|
590
|
+
hook_str = f"{BLUE}🪝 {hook_label} {age_str}{R}".strip()
|
|
591
|
+
print(f" {GRAY}└{R} {hook_str}")
|
|
552
592
|
|
|
553
593
|
# ── Session info line (always shown) ─────────────────────────────────────
|
|
554
594
|
parts2 = []
|
|
@@ -580,6 +620,14 @@ def main():
|
|
|
580
620
|
}
|
|
581
621
|
parts2.append(f"{YELLOW}{_perm_labels.get(perm, perm)}{R}")
|
|
582
622
|
|
|
623
|
+
# Last skill and agent invoked (parsed from transcript tool_use blocks)
|
|
624
|
+
if entries:
|
|
625
|
+
last_skill, last_agent = get_recent_tool_calls(entries)
|
|
626
|
+
if last_skill:
|
|
627
|
+
parts2.append(f"{YELLOW}🎯 {last_skill}{R}")
|
|
628
|
+
if last_agent:
|
|
629
|
+
parts2.append(f"{MAGENTA}⚡ {last_agent}{R}")
|
|
630
|
+
|
|
583
631
|
# Session clock (elapsed time since session start, survives transcript transitions)
|
|
584
632
|
session_start = get_session_start(cwd, transcript_path, entries)
|
|
585
633
|
duration = get_session_duration(session_start)
|
|
@@ -611,16 +659,14 @@ def main():
|
|
|
611
659
|
line += f" ({toks})"
|
|
612
660
|
parts2.append(line + suffix)
|
|
613
661
|
|
|
614
|
-
|
|
615
662
|
# Git info (branch + diff stats)
|
|
616
663
|
git = get_git_info(cwd)
|
|
617
664
|
if git:
|
|
618
665
|
parts2.append(git)
|
|
619
666
|
|
|
620
|
-
# Worktree
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
parts2.append(wt)
|
|
667
|
+
# Worktree label (already computed above — reuse result)
|
|
668
|
+
if wt_display:
|
|
669
|
+
parts2.append(wt_display)
|
|
624
670
|
|
|
625
671
|
if parts2:
|
|
626
672
|
print(' | '.join(parts2))
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
22
22
|
import { join } from 'path';
|
|
23
23
|
import {
|
|
24
|
-
stateExists,
|
|
24
|
+
stateExists, getActiveFeature, getMostRecentFeature, getMissingOutputs, derivePhaseForFeature,
|
|
25
25
|
} from '../../shared/state-reader.js';
|
|
26
26
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
27
27
|
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
@@ -40,20 +40,7 @@ try {
|
|
|
40
40
|
|
|
41
41
|
// getActiveFeature() only returns in_progress/draft features.
|
|
42
42
|
// Fall back to the most recently updated feature when all features are 'done'.
|
|
43
|
-
let active = getActiveFeature();
|
|
44
|
-
if (!active) {
|
|
45
|
-
const state = loadState();
|
|
46
|
-
if (state?.features) {
|
|
47
|
-
let latestUpdate = '';
|
|
48
|
-
for (const [name, feature] of Object.entries(state.features)) {
|
|
49
|
-
const updated = feature.updatedAt || feature.createdAt || '';
|
|
50
|
-
if (updated >= latestUpdate) {
|
|
51
|
-
latestUpdate = updated;
|
|
52
|
-
active = { name, feature };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
43
|
+
let active = getActiveFeature() || getMostRecentFeature();
|
|
57
44
|
if (!active) pass();
|
|
58
45
|
|
|
59
46
|
const { name, feature } = active;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { readStdin } from '../../shared/stdin-reader.js';
|
|
18
|
-
import { stateExists, loadState, getActiveFeature, getFeature, getPendingGates } from '../../shared/state-reader.js';
|
|
18
|
+
import { stateExists, loadState, getActiveFeature, getFeature, getPendingGates, derivePhaseForFeature } from '../../shared/state-reader.js';
|
|
19
19
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
20
20
|
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
21
21
|
|
|
@@ -37,7 +37,8 @@ try {
|
|
|
37
37
|
// Check if a feature name is mentioned
|
|
38
38
|
for (const [featureName, feature] of Object.entries(state.features)) {
|
|
39
39
|
if (promptLower.includes(featureName.toLowerCase())) {
|
|
40
|
-
|
|
40
|
+
const featurePhase = feature.phase || derivePhaseForFeature(featureName);
|
|
41
|
+
context.push(`[morph-spec] Feature '${featureName}': phase=${featurePhase}, status=${feature.status}`);
|
|
41
42
|
if (feature.tasks?.total > 0) {
|
|
42
43
|
context.push(` Tasks: ${feature.tasks.completed || 0}/${feature.tasks.total} completed`);
|
|
43
44
|
}
|
|
@@ -50,16 +51,33 @@ try {
|
|
|
50
51
|
if (active) {
|
|
51
52
|
const { name, feature } = active;
|
|
52
53
|
|
|
54
|
+
const activePhase = feature.phase || derivePhaseForFeature(name);
|
|
53
55
|
const codeKeywords = ['implement', 'code', 'start coding', 'write the code', 'build it', 'let\'s build'];
|
|
54
56
|
const wantsToCode = codeKeywords.some(kw => promptLower.includes(kw));
|
|
55
57
|
|
|
56
|
-
if (wantsToCode &&
|
|
58
|
+
if (wantsToCode && activePhase !== 'implement' && activePhase !== 'sync') {
|
|
57
59
|
context.push(
|
|
58
|
-
`[morph-spec] WARNING: Feature '${name}' is in '${
|
|
60
|
+
`[morph-spec] WARNING: Feature '${name}' is in '${activePhase}' phase, not 'implement'.` +
|
|
59
61
|
` Complete the current phase first or advance: morph-spec phase advance ${name}`
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
// Coding intent during implement phase but no task in_progress
|
|
66
|
+
if (wantsToCode && activePhase === 'implement') {
|
|
67
|
+
let hasActiveTask = feature.tasks?.inProgress > 0;
|
|
68
|
+
if (!hasActiveTask && feature.taskList) {
|
|
69
|
+
hasActiveTask = feature.taskList.some(t => t.status === 'in_progress');
|
|
70
|
+
}
|
|
71
|
+
if (!hasActiveTask) {
|
|
72
|
+
context.push(
|
|
73
|
+
`[morph-spec] REMINDER: No task is currently in_progress for '${name}'.` +
|
|
74
|
+
` Start a task before coding:\n` +
|
|
75
|
+
` npx morph-spec task next ${name} # see what's next\n` +
|
|
76
|
+
` npx morph-spec task start ${name} <id> # mark it in progress`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
// Check for approval intent
|
|
64
82
|
const approvalKeywords = ['approve', 'approved', 'looks good', 'lgtm', 'ship it'];
|
|
65
83
|
const wantsToApprove = approvalKeywords.some(kw => promptLower.includes(kw));
|
|
@@ -75,7 +93,7 @@ try {
|
|
|
75
93
|
|
|
76
94
|
// Check for "next task" intent
|
|
77
95
|
if (promptLower.includes('next task') || promptLower.includes('what\'s next')) {
|
|
78
|
-
if (
|
|
96
|
+
if (activePhase === 'implement') {
|
|
79
97
|
context.push(
|
|
80
98
|
`[morph-spec] Use: morph-spec task next ${name}`
|
|
81
99
|
);
|
|
@@ -40,21 +40,29 @@ try {
|
|
|
40
40
|
const prefix = process.env.CLAUDE_TITLE_PREFIX ? `${process.env.CLAUDE_TITLE_PREFIX} ` : '';
|
|
41
41
|
const finalTitle = `${prefix}${title}`;
|
|
42
42
|
|
|
43
|
-
// Save to
|
|
43
|
+
// Save to session-specific file (MORPH_TERMINAL_TITLE_FILE) or fallback to global
|
|
44
|
+
// MORPH_TERMINAL_TITLE_FILE is set by the shell wrapper (Invoke-Claude / claude())
|
|
45
|
+
// per-session so multiple terminal windows don't interfere with each other.
|
|
44
46
|
try {
|
|
47
|
+
const titleFilePath = process.env.MORPH_TERMINAL_TITLE_FILE
|
|
48
|
+
|| join(homedir(), '.claude', 'terminal_title');
|
|
45
49
|
const claudeDir = join(homedir(), '.claude');
|
|
46
50
|
await mkdir(claudeDir, { recursive: true });
|
|
47
|
-
await writeFile(
|
|
51
|
+
await writeFile(titleFilePath, finalTitle, 'utf-8');
|
|
48
52
|
} catch { /* non-critical */ }
|
|
49
53
|
|
|
50
|
-
// Write ANSI escape to
|
|
54
|
+
// Write ANSI escape to terminal device (bypasses stdout capture by Claude Code)
|
|
55
|
+
// Windows uses \\.\CONOUT$ as the equivalent of /dev/tty
|
|
56
|
+
let wrote = false;
|
|
51
57
|
try {
|
|
52
|
-
const
|
|
58
|
+
const devicePath = process.platform === 'win32' ? '\\\\.\\CONOUT$' : '/dev/tty';
|
|
59
|
+
const tty = openSync(devicePath, 'w');
|
|
53
60
|
writeSync(tty, `\x1b]0;${finalTitle}\x07`);
|
|
54
61
|
closeSync(tty);
|
|
55
|
-
|
|
62
|
+
wrote = true;
|
|
63
|
+
} catch { /* terminal may not support direct device access */ }
|
|
56
64
|
|
|
57
|
-
logHookActivity('set-terminal-title', 'UserPromptSubmit', 'ok');
|
|
65
|
+
logHookActivity('set-terminal-title', 'UserPromptSubmit', wrote ? 'ok' : 'failed');
|
|
58
66
|
} catch { /* fail-open */ }
|
|
59
67
|
|
|
60
68
|
process.exit(0);
|
|
@@ -64,30 +64,6 @@ export function logHookActivity(name, event, result, projectPath) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Log a skill invocation to the session activity log.
|
|
69
|
-
*
|
|
70
|
-
* @param {string} name - Skill name (e.g. 'brainstorming')
|
|
71
|
-
* @param {string} [projectPath]
|
|
72
|
-
*/
|
|
73
|
-
export function logSkillActivity(name, projectPath) {
|
|
74
|
-
try {
|
|
75
|
-
const activityPath = getActivityPath(projectPath);
|
|
76
|
-
ensureLogsDir(projectPath);
|
|
77
|
-
|
|
78
|
-
const now = new Date();
|
|
79
|
-
const ts = now.toTimeString().slice(0, 8);
|
|
80
|
-
|
|
81
|
-
const data = readRaw(activityPath) || { sessionId: '', feature: '', phase: '', hooks: [], skills: [] };
|
|
82
|
-
|
|
83
|
-
data.skills = data.skills || [];
|
|
84
|
-
data.skills.push({ name, ts });
|
|
85
|
-
|
|
86
|
-
writeFileSync(activityPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
87
|
-
} catch {
|
|
88
|
-
// Fail-silent
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
67
|
|
|
92
68
|
/**
|
|
93
69
|
* Read the current activity log.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for post-compact context restoration.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - pre-compact hook: buildRichContext() to enrich memory file
|
|
6
|
+
* - post-compact-restore hook: buildRestoreBlock() + findLatestMemoryFile() to inject context
|
|
7
|
+
*
|
|
8
|
+
* Pure functions — no I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DECISIONS_MAX_CHARS = 1500;
|
|
12
|
+
export const MAX_PENDING_TASKS = 8;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the richContext object to embed in pre-compact memory file.
|
|
16
|
+
* @param {Object} feature - Feature state object (tasks, taskList)
|
|
17
|
+
* @param {string} phase - Current derived phase string
|
|
18
|
+
* @param {string} decisionsContent - Raw text of decisions.md (may be empty string)
|
|
19
|
+
* @returns {Object} richContext
|
|
20
|
+
*/
|
|
21
|
+
export function buildRichContext(feature, phase, decisionsContent) {
|
|
22
|
+
const taskList = Array.isArray(feature.taskList) ? feature.taskList : [];
|
|
23
|
+
|
|
24
|
+
const inProgress = taskList
|
|
25
|
+
.filter(t => t.status === 'in_progress')
|
|
26
|
+
.map(t => t.id);
|
|
27
|
+
|
|
28
|
+
const nextPending = taskList
|
|
29
|
+
.filter(t => t.status === 'pending')
|
|
30
|
+
.slice(0, MAX_PENDING_TASKS)
|
|
31
|
+
.map(t => ({ id: t.id, title: t.title }));
|
|
32
|
+
|
|
33
|
+
const decisionsSnippet = decisionsContent.length > DECISIONS_MAX_CHARS
|
|
34
|
+
? decisionsContent.slice(0, DECISIONS_MAX_CHARS)
|
|
35
|
+
: decisionsContent;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
phase,
|
|
39
|
+
tasks: feature.tasks || {},
|
|
40
|
+
decisionsSnippet,
|
|
41
|
+
inProgress,
|
|
42
|
+
nextPending,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the context block string to inject in session-start after compact.
|
|
48
|
+
* @param {Object} memorySnapshot - Parsed pre-compact-*.json file contents
|
|
49
|
+
* @returns {string|null} Block string, or null if no richContext present
|
|
50
|
+
*/
|
|
51
|
+
export function buildRestoreBlock(memorySnapshot) {
|
|
52
|
+
const rc = memorySnapshot?.richContext;
|
|
53
|
+
if (!rc) return null;
|
|
54
|
+
|
|
55
|
+
const { timestamp, activeFeature } = memorySnapshot;
|
|
56
|
+
const { phase, tasks, decisionsSnippet, inProgress, nextPending } = rc;
|
|
57
|
+
|
|
58
|
+
const done = tasks?.completed ?? '?';
|
|
59
|
+
const total = tasks?.total ?? '?';
|
|
60
|
+
|
|
61
|
+
const lines = [
|
|
62
|
+
`\uD83D\uDD04 POST-COMPACT RESTORE \u2014 context from pre-compact snapshot (${timestamp})`,
|
|
63
|
+
`Active feature: ${activeFeature} | Phase: ${phase} | Tasks: ${done}/${total}`,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
if (decisionsSnippet) {
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('Key decisions (at time of compact):');
|
|
69
|
+
lines.push(decisionsSnippet);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (inProgress.length > 0) {
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(`In progress at compact time: [${inProgress.join(', ')}]`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (nextPending.length > 0) {
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push('Next pending at compact time:');
|
|
80
|
+
for (const t of nextPending) {
|
|
81
|
+
lines.push(` [${t.id}] ${t.title}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find the most recent pre-compact memory file from a list of filenames.
|
|
90
|
+
* Files named pre-compact-{ISO-timestamp-sanitized}.json — sort descending picks latest.
|
|
91
|
+
* @param {string[]} filenames - Array of filenames in the memory directory
|
|
92
|
+
* @returns {string|null} Latest filename, or null if none found
|
|
93
|
+
*/
|
|
94
|
+
export function findLatestMemoryFile(filenames) {
|
|
95
|
+
const matches = filenames
|
|
96
|
+
.filter(f => f.startsWith('pre-compact-') && f.endsWith('.json'))
|
|
97
|
+
.sort()
|
|
98
|
+
.reverse();
|
|
99
|
+
return matches.length > 0 ? matches[0] : null;
|
|
100
|
+
}
|