fluffy-context 0.7.7 → 1.0.0-beta.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +110 -553
  3. package/ROADMAP-1.0.md +70 -0
  4. package/dist/src/agent/api.d.ts +41 -0
  5. package/dist/src/agent/api.js +37 -0
  6. package/dist/src/agent/index.d.ts +9 -1
  7. package/dist/src/agent/index.js +3 -0
  8. package/dist/src/capture/context-filter.js +2 -0
  9. package/dist/src/cli/main.js +293 -15
  10. package/dist/src/cognition/admission.d.ts +8 -0
  11. package/dist/src/cognition/admission.js +24 -0
  12. package/dist/src/cognition/evidence-graph.d.ts +4 -0
  13. package/dist/src/cognition/evidence-graph.js +187 -0
  14. package/dist/src/cognition/evolution.d.ts +36 -0
  15. package/dist/src/cognition/evolution.js +221 -0
  16. package/dist/src/cognition/import-memory.d.ts +18 -0
  17. package/dist/src/cognition/import-memory.js +124 -0
  18. package/dist/src/cognition/journal-segments.d.ts +11 -0
  19. package/dist/src/cognition/journal-segments.js +129 -0
  20. package/dist/src/cognition/journal.d.ts +13 -0
  21. package/dist/src/cognition/journal.js +186 -20
  22. package/dist/src/cognition/migration-v1.js +39 -3
  23. package/dist/src/cognition/projections.js +124 -2
  24. package/dist/src/cognition/recipes.d.ts +25 -0
  25. package/dist/src/cognition/recipes.js +149 -0
  26. package/dist/src/cognition/runtime.js +4 -0
  27. package/dist/src/cognition/skills.d.ts +45 -0
  28. package/dist/src/cognition/skills.js +184 -0
  29. package/dist/src/cognition/types.d.ts +181 -1
  30. package/dist/src/cognition/usage-report.js +30 -15
  31. package/dist/src/compiler/compile.js +101 -31
  32. package/dist/src/compiler/graph.d.ts +7 -0
  33. package/dist/src/compiler/graph.js +117 -0
  34. package/dist/src/compiler/identity.d.ts +1 -1
  35. package/dist/src/compiler/identity.js +2 -0
  36. package/dist/src/compiler/inject.d.ts +23 -0
  37. package/dist/src/compiler/inject.js +27 -0
  38. package/dist/src/compiler/sources.d.ts +3 -1
  39. package/dist/src/compiler/sources.js +10 -1
  40. package/dist/src/compiler/types.d.ts +22 -1
  41. package/dist/src/hooks/claude-code.js +17 -56
  42. package/dist/src/integrations/claude-code.js +30 -8
  43. package/dist/src/mcp/server.js +228 -1
  44. package/dist/src/runtime/knowledge.js +57 -8
  45. package/dist/src/runtime/setup.d.ts +27 -0
  46. package/dist/src/runtime/setup.js +85 -0
  47. package/dist/src/storage/layout.d.ts +5 -0
  48. package/dist/src/storage/layout.js +15 -0
  49. package/dist/src/storage/lock.js +19 -2
  50. package/dist/src/version.d.ts +1 -1
  51. package/dist/src/version.js +1 -1
  52. package/package.json +4 -1
  53. package/skills/fluffy-context/SKILL.md +46 -8
@@ -2,5 +2,5 @@ import type { ContextManifest } from './types.js';
2
2
  export declare function canonicalJson(value: unknown): string;
3
3
  export declare function contentHash(value: unknown): string;
4
4
  export declare function estimateTokens(value: string): number;
5
- export declare function rebuildManifestHash(manifest: Pick<ContextManifest, 'status' | 'projectRoot' | 'mode' | 'anchor' | 'intent' | 'selected' | 'budget' | 'truncation'>): string;
5
+ export declare function rebuildManifestHash(manifest: Pick<ContextManifest, 'status' | 'projectRoot' | 'mode' | 'anchor' | 'intent' | 'selected' | 'budget' | 'truncation' | 'recipe' | 'graph'>): string;
6
6
  export declare function validateManifestIdentity(manifest: ContextManifest): boolean;
@@ -26,6 +26,8 @@ export function rebuildManifestHash(manifest) {
26
26
  selectedItemHashes: manifest.selected.map((item) => item.itemHash),
27
27
  budget: { usedChars: manifest.budget.usedChars, maxChars: manifest.budget.requested.total },
28
28
  truncation: manifest.truncation,
29
+ ...(manifest.recipe ? { recipe: { recipeId: manifest.recipe.recipeId, contentHash: manifest.recipe.contentHash, interpreterVersion: manifest.recipe.interpreterVersion } } : {}),
30
+ ...(manifest.graph ? { graph: manifest.graph } : {}),
29
31
  });
30
32
  }
31
33
  export function validateManifestIdentity(manifest) {
@@ -0,0 +1,23 @@
1
+ import type { ContextCompileOptions, ContextScenario } from './types.js';
2
+ export declare function inferScenario(query: string): ContextScenario;
3
+ export interface InjectionOptions extends ContextCompileOptions {
4
+ explain?: boolean;
5
+ }
6
+ export declare function injectContext(startPath: string | undefined, options?: InjectionOptions, interfaceName?: 'cli' | 'hook' | 'agent' | 'mcp'): Promise<{
7
+ next: string | null;
8
+ sources?: {
9
+ candidateId: string;
10
+ itemHash: string;
11
+ section: import("./types.js").ContextCandidateSection;
12
+ provenance: import("./types.js").ContextProvenance;
13
+ }[] | undefined;
14
+ status: "ready" | "no_context";
15
+ scenario: ContextScenario;
16
+ text: string;
17
+ budget: {
18
+ maxChars: number;
19
+ usedChars: number;
20
+ };
21
+ manifestHash: string;
22
+ sourceCount: number;
23
+ }>;
@@ -0,0 +1,27 @@
1
+ import { compileContext } from './compile.js';
2
+ import { withUsageTelemetry } from '../cognition/usage.js';
3
+ export function inferScenario(query) {
4
+ if (/error|fail|bug|异常|报错|失败|修复/i.test(query))
5
+ return 'debugging';
6
+ if (/test|verify|验证|测试/i.test(query))
7
+ return 'verification';
8
+ if (/resume|恢复|继续/i.test(query))
9
+ return 'resume';
10
+ if (/handoff|交接|总结/i.test(query))
11
+ return 'handoff';
12
+ if (/implement|add|build|实现|新增|开发/i.test(query))
13
+ return 'implementation';
14
+ return 'exploration';
15
+ }
16
+ export async function injectContext(startPath, options = {}, interfaceName = 'agent') {
17
+ return withUsageTelemetry(startPath, 'compile', interfaceName === 'hook' ? 'claude-hook' : interfaceName, options, async () => {
18
+ const scenario = options.scenario ?? inferScenario(options.query ?? '');
19
+ const manifest = await compileContext(startPath, { ...options, scenario, maxChars: options.maxChars ?? 2400, activityLimit: options.activityLimit ?? 0 });
20
+ return { status: manifest.status, scenario, text: manifest.text,
21
+ budget: { maxChars: manifest.budget.requested.total, usedChars: manifest.budget.usedChars },
22
+ manifestHash: manifest.identity.manifestHash,
23
+ sourceCount: manifest.selected.length,
24
+ ...(options.explain ? { sources: manifest.selected.map((item) => ({ candidateId: item.candidateId, itemHash: item.itemHash, section: item.section, provenance: item.source.provenance })) } : {}),
25
+ next: manifest.truncation.truncated ? 'Use ctx compile / ctx expand for selected evidence.' : null };
26
+ }, interfaceName);
27
+ }
@@ -1,4 +1,4 @@
1
- import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
1
+ import type { BranchWorkspace, CognitionEvent, CompilerRecipeState, EvidenceGraphProjection } from '../cognition/types.js';
2
2
  import type { ContextOrientResult } from '../agent/types.js';
3
3
  import type { ContextAnchor, ContextProvenance, ContextSourceDescriptor, ContextSourceApplicability } from './types.js';
4
4
  import type { ContextCompileOptions } from './types.js';
@@ -7,6 +7,8 @@ export interface CompilerSources {
7
7
  anchor: ContextAnchor;
8
8
  events: CognitionEvent[];
9
9
  workspace: BranchWorkspace | null;
10
+ recipe: CompilerRecipeState | null;
11
+ evidenceGraph: EvidenceGraphProjection | null;
10
12
  fallbackReason: string | null;
11
13
  }
12
14
  export declare function sourceApplicability(event: CognitionEvent | null, anchor: ContextAnchor): ContextSourceApplicability;
@@ -107,7 +107,16 @@ export async function loadCompilerSources(startPath, options) {
107
107
  const anchor = anchorFromGit(git, projectRoot);
108
108
  const projection = events.length > 0 ? projectEvents(events) : null;
109
109
  const workspace = projection?.workspaces[workspaceKey(anchor)] ?? null;
110
- return { orientation, anchor, events, workspace, fallbackReason };
110
+ const recipeId = options.recipeId?.trim();
111
+ if (options.recipeId !== undefined && !recipeId)
112
+ throw new Error('compiler recipe ID must not be empty');
113
+ const recipe = recipeId ? projection?.recipes[recipeId] ?? null : null;
114
+ if (recipeId && !recipe)
115
+ throw new Error(`compiler recipe not found: ${recipeId}`);
116
+ if (recipe && !['verified', 'published'].includes(recipe.status))
117
+ throw new Error(`compiler recipe is not active: ${recipe.recipe.recipeId}`);
118
+ const evidenceGraph = options.graphHops !== undefined ? projection?.evidenceGraph ?? null : null;
119
+ return { orientation, anchor, events, workspace, recipe, evidenceGraph, fallbackReason };
111
120
  }
112
121
  export function latestHostIntent(events, anchor) {
113
122
  const event = [...events].reverse().find((candidate) => {
@@ -1,4 +1,4 @@
1
- import type { CognitionGitAnchor, WorkspacePathChange } from '../cognition/types.js';
1
+ import type { CognitionGitAnchor, CompilerRecipeSection, CompilerRecipeSource, WorkspacePathChange } from '../cognition/types.js';
2
2
  import type { ContextOrientResult } from '../agent/types.js';
3
3
  import type { Deadend, Knowledge, Note, ResumeSummary, StructuredContext } from '../runtime/types.js';
4
4
  export type ContextScenario = 'resume' | 'implementation' | 'debugging' | 'verification' | 'handoff' | 'exploration' | 'unknown';
@@ -155,8 +155,27 @@ export interface ContextManifestIdentity {
155
155
  selectedItemHashes: string[];
156
156
  tokenEstimate: ContextTokenEstimate;
157
157
  }
158
+ export interface ContextRecipeParticipation {
159
+ recipeId: string;
160
+ contentHash: string;
161
+ interpreterVersion: 'recipe-interpreter-v1';
162
+ preferredSources: CompilerRecipeSource[];
163
+ preferredSections: CompilerRecipeSection[];
164
+ sectionBudgets: Partial<Record<ContextCandidateSection, number>>;
165
+ }
166
+ export interface ContextGraphParticipation {
167
+ derivationVersion: 'evidence-graph-v1';
168
+ projectionHash: string | null;
169
+ hops: 0 | 1 | 2;
170
+ seedNodeIds: string[];
171
+ selectedNodeIds: string[];
172
+ selectedEdgeIds: string[];
173
+ fallbackReason: string | null;
174
+ }
158
175
  export interface ContextCompileOptions {
159
176
  contextId?: string;
177
+ recipeId?: string;
178
+ graphHops?: 0 | 1 | 2;
160
179
  query?: string;
161
180
  scenario?: ContextScenario;
162
181
  scope?: string;
@@ -227,6 +246,8 @@ export interface ContextManifest {
227
246
  budget: ContextBudgetReport;
228
247
  truncation: ContextTruncation;
229
248
  provenance: ContextManifestProvenance;
249
+ recipe?: ContextRecipeParticipation;
250
+ graph?: ContextGraphParticipation;
230
251
  text: string;
231
252
  }
232
253
  export type CompilerRecord = StructuredContext | Note | Knowledge | Deadend;
@@ -1,10 +1,10 @@
1
- import { contextOrient } from '../agent/api.js';
2
1
  import { appendEvent } from '../cognition/journal.js';
3
2
  import { v2Enabled } from '../cognition/orient.js';
4
3
  import { ensureRuntimeForSession, touchRuntime } from '../cognition/runtime.js';
5
4
  import { loadRuntimePolicy } from '../cognition/policy.js';
6
5
  import { readGitWorktreeState } from '../git/git-adapter.js';
7
6
  import { resolveProjectRoot } from '../project/project-resolver.js';
7
+ import { injectContext } from '../compiler/inject.js';
8
8
  const HOOK_RESPONSE = '{}\n';
9
9
  function text(value) {
10
10
  return typeof value === 'string' ? value : undefined;
@@ -13,50 +13,6 @@ function compact(value, maxChars) {
13
13
  const normalized = value.replace(/\s+/g, ' ').trim();
14
14
  return normalized.length <= maxChars ? normalized : `${normalized.slice(0, maxChars - 1)}…`;
15
15
  }
16
- function lines(label, values, limit) {
17
- return values.slice(0, limit).map((value) => `- ${label}: ${compact(value, 240)}`);
18
- }
19
- function readyContext(result) {
20
- const content = [
21
- `当前 Context:${compact(result.context.title, 160)}`,
22
- `进度:${compact(result.resumeSummary.progressSummary, 360)}`,
23
- ...lines('待办', result.resumeSummary.pendingTasks, 4),
24
- ...lines('决策', result.resumeSummary.decisions, 3),
25
- ...lines('风险', result.resumeSummary.risks, 3),
26
- ...result.knowledge.hits.slice(0, 5).map((hit) => `- 已验证知识:${compact(hit.knowledge.statement, 240)}`),
27
- ...result.deadends.hits.slice(0, 3).map((hit) => `- 已验证死路(避免):${compact(hit.deadend.attempt, 180)};${compact(hit.deadend.reason, 180)}`),
28
- ...result.notes.slice(0, 8).map((note) => `- 开放 Note:${compact(note.message, 220)}`),
29
- ];
30
- if (result.truncated)
31
- content.push('- 上下文已按预算截断;需要时使用 context_orient 或 ctx orient 进一步查询。');
32
- return content;
33
- }
34
- function sessionContext(result) {
35
- const phases = [
36
- '开发阶段:Orient → Plan → Implement → Verify → Handoff。',
37
- '先基于以下上下文明确计划;实现中记录重要观察/决策/阻塞为 ctx note;验证后显式 ctx checkpoint,总结完成、待办、决策和风险。不要自动保存 Snapshot。',
38
- ];
39
- if (result.status === 'no_context') {
40
- phases.push('当前没有可恢复的 Context。开始有意义的工作后,使用 ctx checkpoint 创建一个。');
41
- }
42
- else {
43
- phases.push(...readyContext(result));
44
- }
45
- return phases.join('\n');
46
- }
47
- function promptContext(result) {
48
- const phases = [
49
- '开发阶段:Implement → Verify → Handoff。',
50
- '实现时用 ctx note 记录重要观察、决策或阻塞;完成后运行适用的验证。阶段结束时显式 ctx checkpoint(可 --absorb-notes),不要由 Hook 自动保存。',
51
- ];
52
- if (result.status === 'no_context') {
53
- phases.push('当前没有可恢复的 Context;如果这项工作会继续,先建立 checkpoint。');
54
- }
55
- else {
56
- phases.push(...readyContext(result));
57
- }
58
- return phases.join('\n');
59
- }
60
16
  function stableId(input, name) {
61
17
  return name === 'session' ? text(input.session_id) ?? text(input.sessionId) : text(input.event_id) ?? text(input.eventId);
62
18
  }
@@ -111,17 +67,22 @@ export async function runClaudeCodeHook(event, raw) {
111
67
  await capture(event, input).catch(() => undefined);
112
68
  await touchRuntime(text(input.cwd)).catch(() => false);
113
69
  const cwd = text(input.cwd);
114
- const result = event === 'session-start'
115
- ? await contextOrient(cwd, { maxChars: 1200, noteLimit: 8 })
116
- : await contextOrient(cwd, {
117
- query: compact(text(input.prompt) ?? '', 500) || undefined,
118
- maxChars: 1800,
119
- knowledgeLimit: 5,
120
- deadendLimit: 3,
121
- noteLimit: 8,
122
- });
123
- const additionalContext = event === 'session-start' ? sessionContext(result) : promptContext(result);
124
- return `${JSON.stringify({ additionalContext })}\n`;
70
+ const result = await injectContext(cwd, {
71
+ query: compact(text(input.prompt) ?? '', 500) || undefined,
72
+ scenario: event === 'session-start' ? 'resume' : undefined,
73
+ trigger: event === 'session-start' ? 'session-start' : 'prompt',
74
+ adapter: 'claude-code',
75
+ maxChars: event === 'session-start' ? 1200 : 1800,
76
+ knowledgeLimit: 5,
77
+ deadendLimit: 3,
78
+ noteLimit: 8,
79
+ }, 'hook');
80
+ const guidance = event === 'session-start' ? 'Orient → Plan → Implement → Verify → Handoff' : 'Implement → Verify → Handoff';
81
+ const additionalContext = `${guidance}\n将项目上下文作为证据;重要发现用 ctx note,阶段结束用 ctx checkpoint。\n${result.status === 'no_context' ? '当前没有可恢复的 Context。\n' : ''}${result.text}`;
82
+ return `${JSON.stringify({ hookSpecificOutput: {
83
+ hookEventName: event === 'session-start' ? 'SessionStart' : 'UserPromptSubmit',
84
+ additionalContext,
85
+ } })}\n`;
125
86
  }
126
87
  catch {
127
88
  return HOOK_RESPONSE;
@@ -27,7 +27,7 @@ function hookInstalled(value, command) {
27
27
  && Array.isArray(group.hooks)
28
28
  && group.hooks.some((hook) => isRecord(hook) && hook.type === 'command' && hook.command === command));
29
29
  }
30
- function addHook(value, command, legacyCommand) {
30
+ function addHook(value, command, legacyCommand, relocatedCommand) {
31
31
  const groups = Array.isArray(value) ? value.filter(isRecord).map((group) => ({ ...group })) : [];
32
32
  if (hookInstalled(groups, command))
33
33
  return groups;
@@ -36,7 +36,7 @@ function addHook(value, command, legacyCommand) {
36
36
  ...group,
37
37
  hooks: Array.isArray(group.hooks)
38
38
  ? group.hooks.map((hook) => {
39
- if (!isRecord(hook) || hook.type !== 'command' || hook.command !== legacyCommand)
39
+ if (!isRecord(hook) || hook.type !== 'command' || (hook.command !== legacyCommand && hook.command !== relocatedCommand))
40
40
  return hook;
41
41
  replaced = true;
42
42
  return { ...hook, command };
@@ -77,14 +77,14 @@ function mcpInstalled(value, server = MCP_SERVER) {
77
77
  && value.args.length === server.args.length
78
78
  && value.args.every((item, index) => item === server.args[index]);
79
79
  }
80
- function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized) {
80
+ function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized, relocatable = false) {
81
81
  const hooks = object(settings.hooks);
82
82
  const sessionStartInstalled = hookInstalled(hooks.SessionStart, HOOK_COMMANDS.sessionStart);
83
83
  const userPromptInstalled = hookInstalled(hooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt);
84
84
  const mcpServers = object(mcp.mcpServers);
85
85
  const configuredMcp = mcpServers['fluffy-context'];
86
86
  const installed = mcpInstalled(configuredMcp);
87
- const mcpConflict = configuredMcp !== undefined && !installed && !mcpInstalled(configuredMcp, LEGACY_MCP_SERVER);
87
+ const mcpConflict = configuredMcp !== undefined && !installed && !mcpInstalled(configuredMcp, LEGACY_MCP_SERVER) && !relocatable;
88
88
  return {
89
89
  projectRoot,
90
90
  initialized,
@@ -99,6 +99,27 @@ function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExist
99
99
  installationNeeded: !sessionStartInstalled || !userPromptInstalled || !installed,
100
100
  };
101
101
  }
102
+ async function relocatedCommand(value) {
103
+ if (!isRecord(value) || typeof value.command !== 'string' || !path.isAbsolute(value.command)
104
+ || !/^node(?:\.exe)?$/i.test(path.basename(value.command)) || !Array.isArray(value.args)
105
+ || value.args.length !== 3 || value.args[1] !== 'agent' || value.args[2] !== 'serve'
106
+ || typeof value.args[0] !== 'string' || !path.isAbsolute(value.args[0]))
107
+ return;
108
+ const entry = value.args[0];
109
+ const packageRoot = path.resolve(path.dirname(entry), '../../..');
110
+ if (path.normalize(entry) !== path.join(packageRoot, 'dist', 'src', 'cli', 'main.js'))
111
+ return;
112
+ try {
113
+ const pkg = await readJson(path.join(packageRoot, 'package.json'), isRecord);
114
+ if (pkg.name !== 'fluffy-context' || !isRecord(pkg.bin) || pkg.bin.ctx !== 'dist/src/cli/main.js')
115
+ return;
116
+ await access(entry);
117
+ return `"${value.command}" "${entry}"`;
118
+ }
119
+ catch {
120
+ return;
121
+ }
122
+ }
102
123
  async function inspect(projectRoot) {
103
124
  const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
104
125
  const mcpPath = path.join(projectRoot, '.mcp.json');
@@ -109,7 +130,8 @@ async function inspect(projectRoot) {
109
130
  ]);
110
131
  const [settings, mcp] = await Promise.all([loadObject(settingsPath), loadObject(mcpPath)]);
111
132
  validateConfig(settings, mcp);
112
- return { status: status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized), settings, mcp };
133
+ const relocated = await relocatedCommand(object(mcp.mcpServers)['fluffy-context']);
134
+ return { status: status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized, relocated !== undefined), settings, mcp, relocated };
113
135
  }
114
136
  export async function inspectClaudeIntegration(startPath) {
115
137
  const projectRoot = await resolveProjectRoot(startPath);
@@ -128,14 +150,14 @@ export async function installClaudeIntegration(startPath, apply) {
128
150
  ...inspected.settings,
129
151
  hooks: {
130
152
  ...settingsHooks,
131
- SessionStart: addHook(settingsHooks.SessionStart, HOOK_COMMANDS.sessionStart, LEGACY_HOOK_COMMANDS.sessionStart),
132
- UserPromptSubmit: addHook(settingsHooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt, LEGACY_HOOK_COMMANDS.userPrompt),
153
+ SessionStart: addHook(settingsHooks.SessionStart, HOOK_COMMANDS.sessionStart, LEGACY_HOOK_COMMANDS.sessionStart, inspected.relocated ? `${inspected.relocated} hook claude-code session-start` : undefined),
154
+ UserPromptSubmit: addHook(settingsHooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt, LEGACY_HOOK_COMMANDS.userPrompt, inspected.relocated ? `${inspected.relocated} hook claude-code user-prompt` : undefined),
133
155
  },
134
156
  };
135
157
  const mcpServers = object(inspected.mcp.mcpServers);
136
158
  const mcp = {
137
159
  ...inspected.mcp,
138
- mcpServers: { ...mcpServers, 'fluffy-context': MCP_SERVER },
160
+ mcpServers: { ...mcpServers, 'fluffy-context': { ...object(mcpServers['fluffy-context']), ...MCP_SERVER } },
139
161
  };
140
162
  await Promise.all([
141
163
  atomicWriteJson(inspected.status.settingsPath, settings),
@@ -1,9 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { addNote, listNotes } from '../runtime/notes.js';
4
- import { contextCompile, contextExpand, contextOrient, contextUsageReport, loadContext, saveContext } from '../agent/api.js';
4
+ import { contextCompile, contextExpand, contextOrient, contextUsageReport, inspectRecipe, inspectSkill, listRecipes, listSkills, loadContext, materializeContextRecipe, materializeContextSkill, recordContextSkillInvocation, recordContextSkillOutcome, saveContext, transitionContextRecipe, transitionContextSkill, acceptContextEvolutionCandidate, inspectContextEvolutionProposal, listContextEvolutionProposals, proposeContextEvolution, transitionContextEvolutionProposal } from '../agent/api.js';
5
5
  import { ensureRuntimeForSession } from '../cognition/runtime.js';
6
6
  import { VERSION } from '../version.js';
7
+ import { injectContext } from '../compiler/inject.js';
8
+ import { importMemory } from '../cognition/import-memory.js';
7
9
  const orientInput = z.object({
8
10
  path: z.string().optional(),
9
11
  contextId: z.string().optional(),
@@ -21,6 +23,8 @@ const noteActor = z.enum(['agent', 'human']);
21
23
  const noteStatus = z.enum(['open', 'resolved']);
22
24
  const compileOptions = z.object({
23
25
  contextId: z.string().optional(),
26
+ recipeId: z.string().optional(),
27
+ graphHops: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),
24
28
  query: z.string().optional(),
25
29
  scenario: scenario.optional(),
26
30
  scope: z.string().optional(),
@@ -93,6 +97,25 @@ const usageReportInput = z.object({
93
97
  baselineTokens: z.number().nonnegative().optional(),
94
98
  baselineSource: z.enum(['manual', 'recorded-control']).optional(),
95
99
  });
100
+ const skillOutcome = z.enum(['success', 'failure', 'partial', 'irrelevant', 'unknown']);
101
+ const recipeEntity = z.enum(['knowledge', 'deadend', 'skill', 'compiler-recipe', 'experience', 'proposal']);
102
+ const recipeListInput = z.object({ path: z.string().optional() });
103
+ const recipeInspectInput = z.object({ path: z.string().optional(), recipeId: z.string() });
104
+ const recipeMaterializeInput = z.object({ path: z.string().optional(), proposalId: z.string(), title: z.string(), preferredSources: z.array(z.enum(['context', 'knowledge', 'deadend', 'note'])).optional(), preferredSections: z.array(z.enum(['summary', 'pending', 'decision', 'risk', 'note', 'knowledge', 'deadend', 'activity'])).optional(), sectionBudgets: z.record(z.string(), z.int().nonnegative().max(100_000)).optional() });
105
+ const recipeTransitionInput = z.object({ path: z.string().optional(), recipeId: z.string(), to: z.enum(['verified', 'published', 'deprecated', 'superseded']), rationale: z.string(), supersedes: z.object({ entity: recipeEntity, entityId: z.string(), relation: z.literal('supersedes') }).optional() });
106
+ const skillEntity = z.enum(['knowledge', 'deadend', 'skill', 'compiler-recipe', 'experience', 'proposal']);
107
+ const skillListInput = z.object({ path: z.string().optional() });
108
+ const skillInspectInput = z.object({ path: z.string().optional(), skillId: z.string() });
109
+ const skillMaterializeInput = z.object({ path: z.string().optional(), proposalId: z.string(), title: z.string(), procedure: z.array(z.string()) });
110
+ const skillTransitionInput = z.object({ path: z.string().optional(), skillId: z.string(), to: z.enum(['verified', 'published', 'deprecated', 'superseded']), rationale: z.string(), supersedes: z.object({ entity: skillEntity, entityId: z.string(), relation: z.literal('supersedes') }).optional() });
111
+ const skillInvocationInput = z.object({ path: z.string().optional(), skillId: z.string(), invocationId: z.string(), callerEventId: z.string(), inputHash: z.string() });
112
+ const skillOutcomeInput = z.object({ path: z.string().optional(), skillId: z.string(), invocationId: z.string(), outcomeId: z.string(), callerEventId: z.string(), outcome: skillOutcome, summary: z.string() });
113
+ const evolutionEntity = z.enum(['knowledge', 'deadend', 'skill', 'compiler-recipe', 'experience', 'proposal']);
114
+ const evolutionProposeInput = z.object({ path: z.string().optional(), eventIds: z.array(z.string()).min(1).max(32).optional(), recentLimit: z.int().nonnegative().max(32).optional(), targetKind: z.enum(['skill', 'compiler-recipe']).optional() });
115
+ const evolutionListInput = z.object({ path: z.string().optional() });
116
+ const evolutionInspectInput = z.object({ path: z.string().optional(), proposalId: z.string() });
117
+ const evolutionAcceptInput = z.object({ path: z.string().optional(), proposalId: z.string(), rationale: z.string().min(1) });
118
+ const evolutionTransitionInput = z.object({ path: z.string().optional(), proposalId: z.string(), to: z.enum(['verified', 'rejected', 'deprecated', 'superseded']), rationale: z.string().min(1), supersedes: z.object({ entity: evolutionEntity, entityId: z.string(), relation: z.literal('supersedes') }).optional() });
96
119
  function errorMessage(error) {
97
120
  return error instanceof Error ? error.message : String(error);
98
121
  }
@@ -110,6 +133,30 @@ function failureResponse(tool, error) {
110
133
  }
111
134
  export function createMcpServer() {
112
135
  const server = new McpServer({ name: 'fluffy-context', version: VERSION });
136
+ server.registerTool('context_inject', {
137
+ title: 'Prepare task context',
138
+ description: 'Use at task start, context reset, task change or a new failure. Returns bounded project evidence with automatic scenario selection. Expand only when more detail is needed.',
139
+ inputSchema: compileInput.extend({ explain: z.boolean().optional() }),
140
+ }, async ({ path, ...options }) => {
141
+ try {
142
+ return successResponse(await injectContext(path, options, 'mcp'));
143
+ }
144
+ catch (error) {
145
+ return failureResponse('context_inject', error);
146
+ }
147
+ });
148
+ server.registerTool('context_import', {
149
+ title: 'Import project memories',
150
+ description: 'Scan project agent instructions and memory files. Defaults to preview; apply stores source-linked unverified candidates, never trusted instructions.',
151
+ inputSchema: z.object({ path: z.string().optional(), apply: z.boolean().optional() }),
152
+ }, async ({ path, apply }) => {
153
+ try {
154
+ return successResponse(await importMemory(path, apply));
155
+ }
156
+ catch (error) {
157
+ return failureResponse('context_import', error);
158
+ }
159
+ });
113
160
  server.registerTool('context_orient', {
114
161
  title: 'Orient context',
115
162
  description: 'Load bounded, read-only task context with verified consensus and open notes. Compiler manifests expose deterministic levels, hashes, and estimated tokens through the Agent API and CLI.',
@@ -200,6 +247,186 @@ export function createMcpServer() {
200
247
  return failureResponse('context_usage_report', error);
201
248
  }
202
249
  });
250
+ server.registerTool('recipe_list', {
251
+ title: 'List Recipes',
252
+ description: 'List immutable v2 compiler Recipes without modifying project state.',
253
+ inputSchema: recipeListInput,
254
+ }, async ({ path }) => {
255
+ try {
256
+ return successResponse(await listRecipes(path));
257
+ }
258
+ catch (error) {
259
+ return failureResponse('recipe_list', error);
260
+ }
261
+ });
262
+ server.registerTool('recipe_inspect', {
263
+ title: 'Inspect Recipe',
264
+ description: 'Inspect a compiler Recipe and its lifecycle.',
265
+ inputSchema: recipeInspectInput,
266
+ }, async ({ path, recipeId }) => {
267
+ try {
268
+ return successResponse(await inspectRecipe(path, recipeId));
269
+ }
270
+ catch (error) {
271
+ return failureResponse('recipe_inspect', error);
272
+ }
273
+ });
274
+ server.registerTool('recipe_materialize', {
275
+ title: 'Materialize Recipe',
276
+ description: 'Materialize a Recipe from an accepted compiler-recipe proposal.',
277
+ inputSchema: recipeMaterializeInput,
278
+ }, async ({ path, ...input }) => {
279
+ try {
280
+ return successResponse(await materializeContextRecipe(path, input));
281
+ }
282
+ catch (error) {
283
+ return failureResponse('recipe_materialize', error);
284
+ }
285
+ });
286
+ server.registerTool('recipe_transition', {
287
+ title: 'Transition Recipe',
288
+ description: 'Apply an explicit append-only Recipe lifecycle transition.',
289
+ inputSchema: recipeTransitionInput,
290
+ }, async ({ path, ...input }) => {
291
+ try {
292
+ return successResponse(await transitionContextRecipe(path, input));
293
+ }
294
+ catch (error) {
295
+ return failureResponse('recipe_transition', error);
296
+ }
297
+ });
298
+ server.registerTool('skill_list', {
299
+ title: 'List Skills',
300
+ description: 'List immutable v2 Skills without modifying project state.',
301
+ inputSchema: skillListInput,
302
+ }, async ({ path }) => {
303
+ try {
304
+ return successResponse(await listSkills(path));
305
+ }
306
+ catch (error) {
307
+ return failureResponse('skill_list', error);
308
+ }
309
+ });
310
+ server.registerTool('skill_inspect', {
311
+ title: 'Inspect Skill',
312
+ description: 'Inspect an immutable v2 Skill and its lifecycle without modifying project state.',
313
+ inputSchema: skillInspectInput,
314
+ }, async ({ path, skillId }) => {
315
+ try {
316
+ return successResponse(await inspectSkill(path, skillId));
317
+ }
318
+ catch (error) {
319
+ return failureResponse('skill_inspect', error);
320
+ }
321
+ });
322
+ server.registerTool('skill_materialize', {
323
+ title: 'Materialize Skill',
324
+ description: 'Materialize a Skill from an accepted candidate Skill proposal.',
325
+ inputSchema: skillMaterializeInput,
326
+ }, async ({ path, ...input }) => {
327
+ try {
328
+ return successResponse(await materializeContextSkill(path, input));
329
+ }
330
+ catch (error) {
331
+ return failureResponse('skill_materialize', error);
332
+ }
333
+ });
334
+ server.registerTool('skill_transition', {
335
+ title: 'Transition Skill',
336
+ description: 'Apply an explicit append-only Skill lifecycle transition.',
337
+ inputSchema: skillTransitionInput,
338
+ }, async ({ path, ...input }) => {
339
+ try {
340
+ return successResponse(await transitionContextSkill(path, input));
341
+ }
342
+ catch (error) {
343
+ return failureResponse('skill_transition', error);
344
+ }
345
+ });
346
+ server.registerTool('skill_record_invocation', {
347
+ title: 'Record Skill invocation',
348
+ description: 'Record a published Skill invocation using only an input hash.',
349
+ inputSchema: skillInvocationInput,
350
+ }, async ({ path, ...input }) => {
351
+ try {
352
+ return successResponse(await recordContextSkillInvocation(path, input));
353
+ }
354
+ catch (error) {
355
+ return failureResponse('skill_record_invocation', error);
356
+ }
357
+ });
358
+ server.registerTool('skill_record_outcome', {
359
+ title: 'Record Skill outcome',
360
+ description: 'Record one explicit terminal outcome for a Skill invocation.',
361
+ inputSchema: skillOutcomeInput,
362
+ }, async ({ path, ...input }) => {
363
+ try {
364
+ return successResponse(await recordContextSkillOutcome(path, input));
365
+ }
366
+ catch (error) {
367
+ return failureResponse('skill_record_outcome', error);
368
+ }
369
+ });
370
+ server.registerTool('evolution_propose', {
371
+ title: 'Propose Evolution',
372
+ description: 'Derive a bounded candidate Evolution proposal from authorized metadata.',
373
+ inputSchema: evolutionProposeInput,
374
+ }, async ({ path, ...input }) => {
375
+ try {
376
+ return successResponse(await proposeContextEvolution(path, input));
377
+ }
378
+ catch (error) {
379
+ return failureResponse('evolution_propose', error);
380
+ }
381
+ });
382
+ server.registerTool('evolution_list', {
383
+ title: 'List Evolution proposals',
384
+ description: 'List Evolution proposals without modifying project state.',
385
+ inputSchema: evolutionListInput,
386
+ }, async ({ path }) => {
387
+ try {
388
+ return successResponse(await listContextEvolutionProposals(path));
389
+ }
390
+ catch (error) {
391
+ return failureResponse('evolution_list', error);
392
+ }
393
+ });
394
+ server.registerTool('evolution_inspect', {
395
+ title: 'Inspect Evolution proposal',
396
+ description: 'Inspect one Evolution proposal and its lifecycle.',
397
+ inputSchema: evolutionInspectInput,
398
+ }, async ({ path, proposalId }) => {
399
+ try {
400
+ return successResponse(await inspectContextEvolutionProposal(path, proposalId));
401
+ }
402
+ catch (error) {
403
+ return failureResponse('evolution_inspect', error);
404
+ }
405
+ });
406
+ server.registerTool('evolution_accept', {
407
+ title: 'Accept Evolution candidate',
408
+ description: 'Accept a proposal into an immutable candidate artifact.',
409
+ inputSchema: evolutionAcceptInput,
410
+ }, async ({ path, ...input }) => {
411
+ try {
412
+ return successResponse(await acceptContextEvolutionCandidate(path, input));
413
+ }
414
+ catch (error) {
415
+ return failureResponse('evolution_accept', error);
416
+ }
417
+ });
418
+ server.registerTool('evolution_transition', {
419
+ title: 'Transition Evolution proposal',
420
+ description: 'Apply an explicit append-only Evolution proposal lifecycle transition.',
421
+ inputSchema: evolutionTransitionInput,
422
+ }, async ({ path, ...input }) => {
423
+ try {
424
+ return successResponse(await transitionContextEvolutionProposal(path, input));
425
+ }
426
+ catch (error) {
427
+ return failureResponse('evolution_transition', error);
428
+ }
429
+ });
203
430
  server.registerTool('context_checkpoint', {
204
431
  title: 'Checkpoint context',
205
432
  description: 'Explicitly save context progress and create a Snapshot when changes are present; runtime rate limits remain enforced.',