@feelingmindful/thinking-graph 1.10.0 → 1.11.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/dist/index.js CHANGED
@@ -12,6 +12,10 @@ import { learnSchema, learnHandler } from './tools/learn.js';
12
12
  import { exportSchema, exportHandler } from './tools/export.js';
13
13
  import { researchSchema, researchHandler } from './tools/research.js';
14
14
  import { recommendSkillsSchema, recommendSkillsHandler } from './tools/recommend-skills.js';
15
+ import { routeSkillsSchema, routeSkillsHandler } from './tools/route-skills.js';
16
+ import { planSkillsSchema, planSkillsHandler } from './tools/plan-skills.js';
17
+ import { executePlanSchema, executePlanHandler } from './tools/execute-plan.js';
18
+ import { executeSkillsSchema, executeSkillsHandler } from './tools/execute-skills.js';
15
19
  // Legacy compat shim removed — use `think` tool directly
16
20
  // ─── Storage setup ───────────────────────────────────────
17
21
  const memoryOnly = process.env.THINKING_GRAPH_MEMORY_ONLY === 'true';
@@ -49,6 +53,10 @@ server.tool('learn', 'Store durable knowledge — code facts, tech debt, insight
49
53
  server.tool('export', 'Export the thinking graph as JSON or a human-readable markdown summary.', exportSchema.shape, async (input) => exportHandler(graph, input));
50
54
  server.tool('research', 'Research a topic using Perplexity/Firecrawl, then ingest findings into the graph and Obsidian vault. Two-phase: call once to get an action plan, then again with findings to store them.', researchSchema.shape, async (input) => researchHandler(graph, input, vault, projectSlug));
51
55
  server.tool('recommend-skills', 'Recommend installed marketplace skills by area, verb, platform, or what they produce/detect. Use during reasoning to find skills that can help with the current task.', recommendSkillsSchema.shape, async (input) => recommendSkillsHandler(graph, input));
56
+ server.tool('route-skills', 'Shortlist and rank installed skills for a task using routing heuristics based on platform, verb, areas, detections, and graph context.', routeSkillsSchema.shape, async (input) => routeSkillsHandler(graph, input));
57
+ server.tool('plan-skills', 'Convert a request into an ordered skill execution plan with explicit approval gates before execution. This tool plans but does not execute skills.', planSkillsSchema.shape, async (input) => planSkillsHandler(graph, input));
58
+ server.tool('execute-plan', 'Start or resume a planned workflow run: persist plan state, create/select a runId, enforce approval gates, and return the next runnable step. If multiple runs exist, use availableRunIds to choose the right one.', executePlanSchema.shape, async (input) => executePlanHandler(graph, input));
59
+ server.tool('execute-skills', 'Record the outcome of a planned skill step for a specific executionId/runId, including skill_result, decision, and tech_debt nodes linked back to that run.', executeSkillsSchema.shape, async (input) => executeSkillsHandler(graph, input, vault, projectSlug));
52
60
  // ─── Startup ─────────────────────────────────────────────
53
61
  async function main() {
54
62
  await storage.initialize();
@@ -0,0 +1,195 @@
1
+ import { z } from 'zod';
2
+ import type { ThinkingGraph } from '../engine/graph.js';
3
+ import type { Node, Session } from '../engine/types.js';
4
+ import { type ApprovalGate, type SkillPlanStep } from './skill-routing.js';
5
+ export declare const skillPlanStepSchema: z.ZodObject<{
6
+ stepNumber: z.ZodNumber;
7
+ invocation: z.ZodString;
8
+ plugin: z.ZodString;
9
+ skill: z.ZodString;
10
+ verb: z.ZodOptional<z.ZodString>;
11
+ purpose: z.ZodString;
12
+ approvalGateIds: z.ZodArray<z.ZodString, "many">;
13
+ }, "strip", z.ZodTypeAny, {
14
+ invocation: string;
15
+ skill: string;
16
+ stepNumber: number;
17
+ plugin: string;
18
+ purpose: string;
19
+ approvalGateIds: string[];
20
+ verb?: string | undefined;
21
+ }, {
22
+ invocation: string;
23
+ skill: string;
24
+ stepNumber: number;
25
+ plugin: string;
26
+ purpose: string;
27
+ approvalGateIds: string[];
28
+ verb?: string | undefined;
29
+ }>;
30
+ export declare const approvalGateSchema: z.ZodObject<{
31
+ id: z.ZodString;
32
+ title: z.ZodString;
33
+ beforeStepNumber: z.ZodNumber;
34
+ reason: z.ZodString;
35
+ prompt: z.ZodString;
36
+ }, "strip", z.ZodTypeAny, {
37
+ id: string;
38
+ title: string;
39
+ beforeStepNumber: number;
40
+ reason: string;
41
+ prompt: string;
42
+ }, {
43
+ id: string;
44
+ title: string;
45
+ beforeStepNumber: number;
46
+ reason: string;
47
+ prompt: string;
48
+ }>;
49
+ export declare const executePlanSchema: z.ZodObject<{
50
+ planId: z.ZodOptional<z.ZodString>;
51
+ runId: z.ZodOptional<z.ZodString>;
52
+ request: z.ZodOptional<z.ZodString>;
53
+ plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
+ stepNumber: z.ZodNumber;
55
+ invocation: z.ZodString;
56
+ plugin: z.ZodString;
57
+ skill: z.ZodString;
58
+ verb: z.ZodOptional<z.ZodString>;
59
+ purpose: z.ZodString;
60
+ approvalGateIds: z.ZodArray<z.ZodString, "many">;
61
+ }, "strip", z.ZodTypeAny, {
62
+ invocation: string;
63
+ skill: string;
64
+ stepNumber: number;
65
+ plugin: string;
66
+ purpose: string;
67
+ approvalGateIds: string[];
68
+ verb?: string | undefined;
69
+ }, {
70
+ invocation: string;
71
+ skill: string;
72
+ stepNumber: number;
73
+ plugin: string;
74
+ purpose: string;
75
+ approvalGateIds: string[];
76
+ verb?: string | undefined;
77
+ }>, "many">>;
78
+ approvalGates: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
79
+ id: z.ZodString;
80
+ title: z.ZodString;
81
+ beforeStepNumber: z.ZodNumber;
82
+ reason: z.ZodString;
83
+ prompt: z.ZodString;
84
+ }, "strip", z.ZodTypeAny, {
85
+ id: string;
86
+ title: string;
87
+ beforeStepNumber: number;
88
+ reason: string;
89
+ prompt: string;
90
+ }, {
91
+ id: string;
92
+ title: string;
93
+ beforeStepNumber: number;
94
+ reason: string;
95
+ prompt: string;
96
+ }>, "many">>>;
97
+ approvedGateIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
98
+ sessionId: z.ZodOptional<z.ZodString>;
99
+ projectId: z.ZodOptional<z.ZodString>;
100
+ }, "strip", z.ZodTypeAny, {
101
+ approvalGates: {
102
+ id: string;
103
+ title: string;
104
+ beforeStepNumber: number;
105
+ reason: string;
106
+ prompt: string;
107
+ }[];
108
+ sessionId?: string | undefined;
109
+ projectId?: string | undefined;
110
+ plan?: {
111
+ invocation: string;
112
+ skill: string;
113
+ stepNumber: number;
114
+ plugin: string;
115
+ purpose: string;
116
+ approvalGateIds: string[];
117
+ verb?: string | undefined;
118
+ }[] | undefined;
119
+ request?: string | undefined;
120
+ planId?: string | undefined;
121
+ runId?: string | undefined;
122
+ approvedGateIds?: string[] | undefined;
123
+ }, {
124
+ sessionId?: string | undefined;
125
+ projectId?: string | undefined;
126
+ plan?: {
127
+ invocation: string;
128
+ skill: string;
129
+ stepNumber: number;
130
+ plugin: string;
131
+ purpose: string;
132
+ approvalGateIds: string[];
133
+ verb?: string | undefined;
134
+ }[] | undefined;
135
+ approvalGates?: {
136
+ id: string;
137
+ title: string;
138
+ beforeStepNumber: number;
139
+ reason: string;
140
+ prompt: string;
141
+ }[] | undefined;
142
+ request?: string | undefined;
143
+ planId?: string | undefined;
144
+ runId?: string | undefined;
145
+ approvedGateIds?: string[] | undefined;
146
+ }>;
147
+ export type ExecutePlanInput = z.infer<typeof executePlanSchema>;
148
+ export interface StoredPlan {
149
+ nodeId: string;
150
+ planId: string;
151
+ request?: string;
152
+ plan: SkillPlanStep[];
153
+ approvalGates: ApprovalGate[];
154
+ }
155
+ export interface ExecutionProgress {
156
+ approvedGateIds: Set<string>;
157
+ stepResults: Map<number, Node>;
158
+ completedSteps: number[];
159
+ failedStep: {
160
+ stepNumber: number;
161
+ node: Node;
162
+ } | null;
163
+ nextStep: SkillPlanStep | null;
164
+ blockingGate: ApprovalGate | null;
165
+ }
166
+ export interface StoredExecutionInstance {
167
+ nodeId: string;
168
+ executionId: string;
169
+ planId: string;
170
+ runId: string;
171
+ legacy?: boolean;
172
+ }
173
+ export declare function parseExecutionId(executionId: string): {
174
+ projectScope: string;
175
+ planId: string;
176
+ runId: string;
177
+ } | null;
178
+ export declare function findStoredPlan(graph: ThinkingGraph, _sessionId: string, projectId: string | undefined, planId: string): Promise<StoredPlan | null>;
179
+ export declare function buildExecutionId(planId: string, projectId?: string): string;
180
+ export declare function buildExecutionInstanceId(planId: string, projectId: string | undefined, runId?: string): string;
181
+ export declare function findExecutionInstances(graph: ThinkingGraph, projectId: string | undefined, planId: string): Promise<StoredExecutionInstance[]>;
182
+ export declare function resolveExecutionInstance(graph: ThinkingGraph, session: Session, projectId: string | undefined, storedPlan: StoredPlan, requestedRunId?: string): Promise<{
183
+ instance?: StoredExecutionInstance;
184
+ error?: string;
185
+ availableRunIds?: string[];
186
+ }>;
187
+ export declare function getApprovedGateIds(graph: ThinkingGraph, _sessionId: string, projectId: string | undefined, executionId: string): Promise<Set<string>>;
188
+ export declare function getExecutionResults(graph: ThinkingGraph, _sessionId: string, projectId: string | undefined, executionId: string): Promise<Map<number, Node>>;
189
+ export declare function resolveExecutionProgress(graph: ThinkingGraph, sessionId: string, projectId: string | undefined, storedPlan: StoredPlan, executionId: string): Promise<ExecutionProgress>;
190
+ export declare function executePlanHandler(graph: ThinkingGraph, input: ExecutePlanInput): Promise<{
191
+ content: {
192
+ type: "text";
193
+ text: string;
194
+ }[];
195
+ }>;
@@ -0,0 +1,416 @@
1
+ import { z } from 'zod';
2
+ import { buildPlanId } from './skill-routing.js';
3
+ export const skillPlanStepSchema = z.object({
4
+ stepNumber: z.coerce.number().int().min(1),
5
+ invocation: z.string(),
6
+ plugin: z.string(),
7
+ skill: z.string(),
8
+ verb: z.string().optional(),
9
+ purpose: z.string(),
10
+ approvalGateIds: z.array(z.string()),
11
+ });
12
+ export const approvalGateSchema = z.object({
13
+ id: z.string(),
14
+ title: z.string(),
15
+ beforeStepNumber: z.coerce.number().int().min(1),
16
+ reason: z.string(),
17
+ prompt: z.string(),
18
+ });
19
+ export const executePlanSchema = z.object({
20
+ planId: z.string().optional().describe('Stable ID returned by plan-skills'),
21
+ runId: z.string().optional().describe('Execution instance ID for this plan within the project. Provide a stable runId when starting parallel runs, and reuse a value from availableRunIds when resuming an ambiguous execution.'),
22
+ request: z.string().optional().describe('Original request used to create the plan'),
23
+ plan: z.array(skillPlanStepSchema).optional().describe('Ordered plan to execute; required the first time unless the plan was already stored'),
24
+ approvalGates: z.array(approvalGateSchema).optional().default([]),
25
+ approvedGateIds: z.array(z.string()).optional().describe('Gate IDs newly approved on this call for the selected run'),
26
+ sessionId: z.string().optional(),
27
+ projectId: z.string().optional(),
28
+ });
29
+ function asRecord(value) {
30
+ return value && typeof value === 'object' ? value : {};
31
+ }
32
+ function normalizeProjectScope(projectId) {
33
+ return projectId && projectId.trim().length > 0 ? projectId : 'global';
34
+ }
35
+ function normalizeRunId(runId) {
36
+ return runId && runId.trim().length > 0 ? runId.trim() : 'run-1';
37
+ }
38
+ function parseStoredPlan(node) {
39
+ const metadata = asRecord(node.metadata);
40
+ if (metadata.kind !== 'execution_plan' || typeof metadata.planId !== 'string')
41
+ return null;
42
+ if (!Array.isArray(metadata.plan) || !Array.isArray(metadata.approvalGates))
43
+ return null;
44
+ return {
45
+ nodeId: node.id,
46
+ planId: metadata.planId,
47
+ request: typeof metadata.request === 'string' ? metadata.request : undefined,
48
+ plan: metadata.plan,
49
+ approvalGates: metadata.approvalGates,
50
+ };
51
+ }
52
+ async function resolveSession(graph, sessionId, projectId) {
53
+ if (!sessionId)
54
+ return graph.getCurrentSession();
55
+ return (await graph.storage.getSession(sessionId)) ?? graph.createSession({ id: sessionId, projectId });
56
+ }
57
+ function parseStoredExecutionInstance(node) {
58
+ const metadata = asRecord(node.metadata);
59
+ if (metadata.kind !== 'execution_instance')
60
+ return null;
61
+ if (typeof metadata.executionId !== 'string' || typeof metadata.planId !== 'string' || typeof metadata.runId !== 'string') {
62
+ return null;
63
+ }
64
+ return {
65
+ nodeId: node.id,
66
+ executionId: metadata.executionId,
67
+ planId: metadata.planId,
68
+ runId: metadata.runId,
69
+ };
70
+ }
71
+ export function parseExecutionId(executionId) {
72
+ const prefix = 'execution:';
73
+ if (!executionId.startsWith(prefix))
74
+ return null;
75
+ const remainder = executionId.slice(prefix.length);
76
+ const parts = remainder.split(':');
77
+ if (parts.length === 2) {
78
+ return {
79
+ projectScope: parts[0],
80
+ planId: parts[1],
81
+ runId: 'run-1',
82
+ };
83
+ }
84
+ if (parts.length >= 3) {
85
+ return {
86
+ projectScope: parts[0],
87
+ planId: parts[1],
88
+ runId: parts.slice(2).join(':'),
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+ export async function findStoredPlan(graph, _sessionId, projectId, planId) {
94
+ const result = await graph.findNodes({ type: 'decision', projectId, limit: 500 });
95
+ for (const node of result.items) {
96
+ const stored = parseStoredPlan(node);
97
+ if (stored?.planId === planId)
98
+ return stored;
99
+ }
100
+ return null;
101
+ }
102
+ async function ensurePlanStored(graph, session, projectId, storedPlan) {
103
+ const existing = await findStoredPlan(graph, session.id, projectId, storedPlan.planId);
104
+ if (existing)
105
+ return existing;
106
+ const node = await graph.addNode({
107
+ type: 'decision',
108
+ content: storedPlan.request
109
+ ? `Execution plan selected for ${storedPlan.request}`
110
+ : `Execution plan ${storedPlan.planId}`,
111
+ sessionId: session.id,
112
+ projectId,
113
+ metadata: {
114
+ kind: 'execution_plan',
115
+ planId: storedPlan.planId,
116
+ request: storedPlan.request,
117
+ plan: storedPlan.plan,
118
+ approvalGates: storedPlan.approvalGates,
119
+ },
120
+ });
121
+ return { ...storedPlan, nodeId: node.id };
122
+ }
123
+ export function buildExecutionId(planId, projectId) {
124
+ return `execution:${normalizeProjectScope(projectId)}:${planId}:${normalizeRunId(undefined)}`;
125
+ }
126
+ export function buildExecutionInstanceId(planId, projectId, runId) {
127
+ return `execution:${normalizeProjectScope(projectId)}:${planId}:${normalizeRunId(runId)}`;
128
+ }
129
+ function buildLegacyExecutionId(planId, projectId) {
130
+ return `execution:${normalizeProjectScope(projectId)}:${planId}`;
131
+ }
132
+ export async function findExecutionInstances(graph, projectId, planId) {
133
+ const result = await graph.findNodes({ type: 'decision', projectId, limit: 1000 });
134
+ const instances = result.items
135
+ .map(parseStoredExecutionInstance)
136
+ .filter((instance) => Boolean(instance && instance.planId === planId));
137
+ const legacyExecutionId = buildLegacyExecutionId(planId, projectId);
138
+ const [approvedGateIds, stepResults] = await Promise.all([
139
+ getApprovedGateIds(graph, '', projectId, legacyExecutionId),
140
+ getExecutionResults(graph, '', projectId, legacyExecutionId),
141
+ ]);
142
+ if (approvedGateIds.size > 0 || stepResults.size > 0) {
143
+ const hasExplicitRunOne = instances.some(instance => instance.runId === 'run-1');
144
+ if (!hasExplicitRunOne) {
145
+ instances.push({
146
+ nodeId: '',
147
+ executionId: legacyExecutionId,
148
+ planId,
149
+ runId: 'run-1',
150
+ legacy: true,
151
+ });
152
+ }
153
+ }
154
+ return instances.sort((a, b) => a.runId.localeCompare(b.runId));
155
+ }
156
+ async function ensureExecutionInstanceStored(graph, session, projectId, storedPlan, runId) {
157
+ const existing = (await findExecutionInstances(graph, projectId, storedPlan.planId))
158
+ .find(instance => instance.runId === runId);
159
+ if (existing)
160
+ return existing;
161
+ const executionId = buildExecutionInstanceId(storedPlan.planId, projectId, runId);
162
+ const node = await graph.addNode({
163
+ type: 'decision',
164
+ content: `Execution instance ${runId} for ${storedPlan.planId}`,
165
+ sessionId: session.id,
166
+ projectId,
167
+ metadata: {
168
+ kind: 'execution_instance',
169
+ executionId,
170
+ planId: storedPlan.planId,
171
+ runId,
172
+ },
173
+ });
174
+ await graph.addEdge({
175
+ sourceId: node.id,
176
+ targetId: storedPlan.nodeId,
177
+ type: 'supports',
178
+ reasoning: 'Execution instance created for this stored plan.',
179
+ });
180
+ return {
181
+ nodeId: node.id,
182
+ executionId,
183
+ planId: storedPlan.planId,
184
+ runId,
185
+ };
186
+ }
187
+ export async function resolveExecutionInstance(graph, session, projectId, storedPlan, requestedRunId) {
188
+ const instances = await findExecutionInstances(graph, projectId, storedPlan.planId);
189
+ if (requestedRunId) {
190
+ const runId = normalizeRunId(requestedRunId);
191
+ const existing = instances.find(instance => instance.runId === runId);
192
+ if (existing)
193
+ return { instance: existing };
194
+ return { instance: await ensureExecutionInstanceStored(graph, session, projectId, storedPlan, runId) };
195
+ }
196
+ if (instances.length === 0) {
197
+ return { instance: await ensureExecutionInstanceStored(graph, session, projectId, storedPlan, 'run-1') };
198
+ }
199
+ if (instances.length === 1) {
200
+ return { instance: instances[0] };
201
+ }
202
+ return {
203
+ error: `Multiple execution instances exist for ${storedPlan.planId}. Specify runId to continue and reuse one of availableRunIds when resuming.`,
204
+ availableRunIds: instances.map(instance => instance.runId).sort(),
205
+ };
206
+ }
207
+ export async function getApprovedGateIds(graph, _sessionId, projectId, executionId) {
208
+ const result = await graph.findNodes({ type: 'decision', projectId, limit: 500 });
209
+ const approved = new Set();
210
+ for (const node of result.items) {
211
+ const metadata = asRecord(node.metadata);
212
+ if (metadata.kind === 'approval_gate' && metadata.executionId === executionId && typeof metadata.gateId === 'string') {
213
+ approved.add(metadata.gateId);
214
+ }
215
+ }
216
+ return approved;
217
+ }
218
+ async function recordApprovals(graph, session, projectId, executionId, plan, gateIds) {
219
+ if (gateIds.length === 0)
220
+ return;
221
+ const existing = await getApprovedGateIds(graph, session.id, projectId, executionId);
222
+ for (const gateId of gateIds) {
223
+ if (existing.has(gateId))
224
+ continue;
225
+ const gate = plan.approvalGates.find(candidate => candidate.id === gateId);
226
+ if (!gate)
227
+ continue;
228
+ const approval = await graph.addNode({
229
+ type: 'decision',
230
+ content: `Approved gate ${gate.title} before step ${gate.beforeStepNumber}`,
231
+ sessionId: session.id,
232
+ projectId,
233
+ metadata: {
234
+ kind: 'approval_gate',
235
+ executionId,
236
+ planId: plan.planId,
237
+ gateId: gate.id,
238
+ beforeStepNumber: gate.beforeStepNumber,
239
+ },
240
+ });
241
+ await graph.addEdge({
242
+ sourceId: approval.id,
243
+ targetId: plan.nodeId,
244
+ type: 'supports',
245
+ reasoning: 'Approval recorded for this execution plan.',
246
+ });
247
+ }
248
+ }
249
+ export async function getExecutionResults(graph, _sessionId, projectId, executionId) {
250
+ const result = await graph.findNodes({ type: 'skill_result', projectId, limit: 1000 });
251
+ const relevant = result.items
252
+ .filter(node => asRecord(node.metadata).executionId === executionId)
253
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
254
+ const latestByStep = new Map();
255
+ for (const node of relevant) {
256
+ const stepNumber = asRecord(node.metadata).stepNumber;
257
+ if (typeof stepNumber === 'number' && !latestByStep.has(stepNumber))
258
+ latestByStep.set(stepNumber, node);
259
+ }
260
+ return latestByStep;
261
+ }
262
+ export async function resolveExecutionProgress(graph, sessionId, projectId, storedPlan, executionId) {
263
+ const approvedGateIds = await getApprovedGateIds(graph, sessionId, projectId, executionId);
264
+ const stepResults = await getExecutionResults(graph, sessionId, projectId, executionId);
265
+ const completedSteps = [...stepResults.entries()]
266
+ .filter(([, node]) => asRecord(node.metadata).status === 'completed')
267
+ .map(([stepNumber]) => stepNumber)
268
+ .sort((a, b) => a - b);
269
+ const failedEntry = [...stepResults.entries()]
270
+ .find(([, node]) => asRecord(node.metadata).status === 'failed');
271
+ const failedStep = failedEntry
272
+ ? { stepNumber: failedEntry[0], node: failedEntry[1] }
273
+ : null;
274
+ const nextStep = storedPlan.plan.find(step => !completedSteps.includes(step.stepNumber)) ?? null;
275
+ const blockingGate = nextStep
276
+ ? storedPlan.approvalGates.find(gate => gate.beforeStepNumber === nextStep.stepNumber && !approvedGateIds.has(gate.id)) ?? null
277
+ : null;
278
+ return {
279
+ approvedGateIds,
280
+ stepResults,
281
+ completedSteps,
282
+ failedStep,
283
+ nextStep,
284
+ blockingGate,
285
+ };
286
+ }
287
+ export async function executePlanHandler(graph, input) {
288
+ const session = await resolveSession(graph, input.sessionId, input.projectId);
289
+ const explicitPlanId = input.planId ?? (input.plan ? buildPlanId({
290
+ request: input.request,
291
+ plan: input.plan,
292
+ approvalGates: (input.approvalGates ?? []),
293
+ }) : undefined);
294
+ if (!explicitPlanId) {
295
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'planId or plan is required' }) }] };
296
+ }
297
+ const suppliedPlan = input.plan
298
+ ? {
299
+ nodeId: '',
300
+ planId: explicitPlanId,
301
+ request: input.request,
302
+ plan: input.plan,
303
+ approvalGates: (input.approvalGates ?? []),
304
+ }
305
+ : await findStoredPlan(graph, session.id, input.projectId, explicitPlanId);
306
+ if (!suppliedPlan) {
307
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `No stored plan found for ${explicitPlanId}. Provide the plan structure.` }) }] };
308
+ }
309
+ const storedPlan = await ensurePlanStored(graph, session, input.projectId, suppliedPlan);
310
+ const executionInstance = await resolveExecutionInstance(graph, session, input.projectId, storedPlan, input.runId);
311
+ if (!executionInstance.instance) {
312
+ return {
313
+ content: [{
314
+ type: 'text',
315
+ text: JSON.stringify({
316
+ status: 'error',
317
+ planId: storedPlan.planId,
318
+ error: executionInstance.error,
319
+ availableRunIds: executionInstance.availableRunIds ?? [],
320
+ }),
321
+ }],
322
+ };
323
+ }
324
+ const { executionId, runId } = executionInstance.instance;
325
+ await recordApprovals(graph, session, input.projectId, executionId, storedPlan, input.approvedGateIds ?? []);
326
+ const progress = await resolveExecutionProgress(graph, session.id, input.projectId, storedPlan, executionId);
327
+ if (progress.failedStep) {
328
+ const { stepNumber, node } = progress.failedStep;
329
+ return {
330
+ content: [{
331
+ type: 'text',
332
+ text: JSON.stringify({
333
+ status: 'failed',
334
+ planId: storedPlan.planId,
335
+ executionId,
336
+ runId,
337
+ approvedGateIds: [...progress.approvedGateIds],
338
+ completedSteps: progress.completedSteps,
339
+ failedStep: {
340
+ stepNumber,
341
+ invocation: asRecord(node.metadata).invocation,
342
+ summary: node.content,
343
+ },
344
+ nextAction: `Investigate step ${stepNumber} and record a successful retry with execute-skills before continuing.`,
345
+ }),
346
+ }],
347
+ };
348
+ }
349
+ if (!progress.nextStep) {
350
+ return {
351
+ content: [{
352
+ type: 'text',
353
+ text: JSON.stringify({
354
+ status: 'completed',
355
+ planId: storedPlan.planId,
356
+ executionId,
357
+ runId,
358
+ approvedGateIds: [...progress.approvedGateIds],
359
+ completedSteps: progress.completedSteps,
360
+ nextStep: null,
361
+ nextAction: 'Execution is complete.',
362
+ }),
363
+ }],
364
+ };
365
+ }
366
+ if (progress.blockingGate) {
367
+ return {
368
+ content: [{
369
+ type: 'text',
370
+ text: JSON.stringify({
371
+ status: 'awaiting_approval',
372
+ planId: storedPlan.planId,
373
+ executionId,
374
+ runId,
375
+ approvedGateIds: [...progress.approvedGateIds],
376
+ completedSteps: progress.completedSteps,
377
+ blockedByGate: progress.blockingGate,
378
+ nextStep: progress.nextStep,
379
+ nextAction: `Approve ${progress.blockingGate.id} and rerun execute-plan with runId ${runId} to continue at step ${progress.nextStep.stepNumber}.`,
380
+ }),
381
+ }],
382
+ };
383
+ }
384
+ return {
385
+ content: [{
386
+ type: 'text',
387
+ text: JSON.stringify({
388
+ status: 'ready_to_execute',
389
+ planId: storedPlan.planId,
390
+ executionId,
391
+ runId,
392
+ approvedGateIds: [...progress.approvedGateIds],
393
+ completedSteps: progress.completedSteps,
394
+ nextStep: progress.nextStep,
395
+ executeSkillCall: {
396
+ tool: 'execute-skills',
397
+ args: {
398
+ executionId,
399
+ planId: storedPlan.planId,
400
+ runId,
401
+ stepNumber: progress.nextStep.stepNumber,
402
+ invocation: progress.nextStep.invocation,
403
+ plugin: progress.nextStep.plugin,
404
+ skill: progress.nextStep.skill,
405
+ purpose: progress.nextStep.purpose,
406
+ resultSummary: '<summarize what the step produced>',
407
+ status: 'completed',
408
+ decisions: [{ content: '<decision extracted from the skill output>' }],
409
+ techDebt: [{ content: '<tech debt or follow-up surfaced by the step>' }],
410
+ },
411
+ },
412
+ nextAction: `Invoke ${progress.nextStep.invocation}, then record the result with execute-skills using executionId ${executionId} (runId ${runId}).`,
413
+ }),
414
+ }],
415
+ };
416
+ }