@medicine-wheel/mcp 4.21.0 → 4.22.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.
@@ -9,701 +9,1412 @@
9
9
  * - SOUTH: Growth/Action Steps (embodied practice)
10
10
  * - WEST: Reflection/Current Reality assessment (honest truth)
11
11
  * - NORTH: Wisdom/Completion (integration)
12
+ *
13
+ * Delayed resolution (Episode 297, 2026-10-19): a step can record that someone
14
+ * strategically chose to delay its resolution — when resolution is wanted, why
15
+ * waiting serves the work, and which agent session to rehydrate when the time
16
+ * or the event comes. Contract: rispecs/delayed-resolution.spec.md.
12
17
  */
13
18
  import { store } from "../store.js";
14
- export const structuralTensionTools = [
15
- {
16
- name: "create_structural_tension_chart",
17
- description: "Create a structural tension chart with desired outcome, current reality, and optional action steps. Charts are associated with a medicine wheel direction. REMEMBER: Desired outcome is what you want to CREATE (not solve). Current reality must be factual assessment (never 'ready to begin').",
18
- inputSchema: {
19
- type: "object",
20
- properties: {
21
- desired_outcome: {
22
- type: "string",
23
- description: "What you want to CREATE (not solve/fix). Focus on positive outcomes, not problems to eliminate. Must be visualizable, specific, quantified where possible.",
24
- },
25
- current_reality: {
26
- type: "string",
27
- description: "Factual assessment of where you are NOW relative to the desired outcome. NEVER use 'ready to begin' or readiness statements. Just facts.",
28
- },
29
- direction: {
30
- type: "string",
31
- enum: ["east", "south", "west", "north"],
32
- description: "Which medicine wheel direction this chart serves (east=vision, south=growth, west=reflection, north=wisdom)",
33
- },
34
- due_date: {
35
- type: "string",
36
- description: "When you want to achieve this outcome (ISO date string, optional)",
37
- },
38
- action_steps: {
39
- type: "array",
40
- items: { type: "string" },
41
- description: "Optional strategic secondary choices (NOT tasks). Each step should support advancement toward the desired outcome.",
19
+ const DAY_MS = 86_400_000;
20
+ // ── Helpers ────────────────────────────────────────────────────────────────
21
+ /** ISO form of a date the caller gave, or null when it is not a date. */
22
+ function isoOrNull(value) {
23
+ if (typeof value !== 'string' || value.trim() === '')
24
+ return null;
25
+ const t = Date.parse(value);
26
+ return Number.isNaN(t) ? null : new Date(t).toISOString();
27
+ }
28
+ function refuse(refusal, message, extra = {}) {
29
+ return { status: "refused", refusal, message, ...extra };
30
+ }
31
+ function nonEmpty(value) {
32
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
33
+ }
34
+ /**
35
+ * The next step id for a chart: one past the highest index already used.
36
+ * The array length is not an index once a step can be removed — two steps
37
+ * would then share an id.
38
+ */
39
+ function nextStepId(chart) {
40
+ const prefix = `${chart.id}_action_`;
41
+ let max = -1;
42
+ for (const s of chart.action_steps) {
43
+ if (typeof s.id === 'string' && s.id.startsWith(prefix)) {
44
+ const n = Number(s.id.slice(prefix.length));
45
+ if (Number.isInteger(n) && n > max)
46
+ max = n;
47
+ }
48
+ }
49
+ const ids = new Set(chart.action_steps.map(s => s.id));
50
+ let next = max + 1;
51
+ while (ids.has(`${prefix}${next}`))
52
+ next++;
53
+ return `${prefix}${next}`;
54
+ }
55
+ /**
56
+ * Due dates for steps written with a chart: spread evenly between now and the
57
+ * chart's due date, leaving one interval before it. A chart already past its
58
+ * date gives each step that date; a chart without one gives none.
59
+ */
60
+ function spreadDueDates(chartDue, count, now = Date.now()) {
61
+ const due = chartDue ? Date.parse(chartDue) : NaN;
62
+ if (Number.isNaN(due))
63
+ return Array.from({ length: count }, () => undefined);
64
+ if (due <= now)
65
+ return Array.from({ length: count }, () => new Date(due).toISOString());
66
+ const interval = (due - now) / (count + 1);
67
+ return Array.from({ length: count }, (_, i) => new Date(now + interval * (i + 1)).toISOString());
68
+ }
69
+ function newStep(id, title, due, currentReality = 'Not yet assessed') {
70
+ const step = {
71
+ id,
72
+ title,
73
+ current_reality: currentReality,
74
+ status: 'pending',
75
+ progress: 0,
76
+ ceremonies_honored: [],
77
+ relations_honored: [],
78
+ };
79
+ if (due)
80
+ step.due_date = due;
81
+ return step;
82
+ }
83
+ function findStep(chart, stepId) {
84
+ return chart.action_steps.find(s => s.id === stepId);
85
+ }
86
+ function isOpen(delay) {
87
+ return !!delay && !delay.ended_at;
88
+ }
89
+ /** The status a step returns to when its delay ends: it never completed while waiting. */
90
+ function statusAfterDelay(step) {
91
+ return (step.progress ?? 0) > 0 ? 'in_progress' : 'pending';
92
+ }
93
+ function daysRemaining(resolveBy, now) {
94
+ // `+ 0` turns -0 into 0 so a delay due within the day reads as 0.
95
+ return Math.ceil((Date.parse(resolveBy) - now) / DAY_MS) + 0;
96
+ }
97
+ function isOwed(delay, now) {
98
+ return isOpen(delay) && Date.parse(delay.resolve_by) <= now;
99
+ }
100
+ function settlePhase(chart) {
101
+ const total = chart.action_steps.length;
102
+ const completed = chart.action_steps.filter(s => s.status === 'completed').length;
103
+ if (total > 0) {
104
+ if (completed === total)
105
+ chart.phase = 'completion';
106
+ else if (completed > 0)
107
+ chart.phase = 'assimilation';
108
+ }
109
+ return { completed, total };
110
+ }
111
+ function appendReality(chart, line) {
112
+ chart.current_reality = chart.current_reality ? `${chart.current_reality}\n${line}` : line;
113
+ }
114
+ function shellQuote(value) {
115
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
116
+ }
117
+ /** The command that resumes a session: the stored one, or for Claude, `claude --resume <id>`. */
118
+ function resumeCommandFor(session) {
119
+ if (!session)
120
+ return { resume_command: null };
121
+ if (session.resume_command)
122
+ return { resume_command: session.resume_command, resume_command_derived: false };
123
+ if ((session.agent ?? 'claude') === 'claude') {
124
+ const cmd = `claude --resume ${shellQuote(session.session_id)}`;
125
+ return {
126
+ resume_command: session.cwd ? `cd ${shellQuote(session.cwd)} && ${cmd}` : cmd,
127
+ resume_command_derived: true,
128
+ };
129
+ }
130
+ return { resume_command: null };
131
+ }
132
+ function parseMaybeJson(value) {
133
+ if (typeof value !== 'string')
134
+ return value;
135
+ try {
136
+ return JSON.parse(value);
137
+ }
138
+ catch {
139
+ return value;
140
+ }
141
+ }
142
+ function normalizeSession(input) {
143
+ const value = parseMaybeJson(input);
144
+ if (value === undefined || value === null || value === '')
145
+ return {};
146
+ if (typeof value !== 'object' || Array.isArray(value)) {
147
+ return { error: 'session must be an object carrying session_id' };
148
+ }
149
+ const s = value;
150
+ const sessionId = nonEmpty(s.session_id);
151
+ if (!sessionId)
152
+ return { error: 'session.session_id is required' };
153
+ const session = { session_id: sessionId, agent: nonEmpty(s.agent) ?? 'claude' };
154
+ for (const key of ['host', 'cwd', 'tmux', 'resume_command', 'inventory_record']) {
155
+ const v = nonEmpty(s[key]);
156
+ if (v)
157
+ session[key] = v;
158
+ }
159
+ return { session };
160
+ }
161
+ function normalizeWakeOn(input) {
162
+ const value = parseMaybeJson(input);
163
+ if (value === undefined || value === null || value === '')
164
+ return undefined;
165
+ const list = Array.isArray(value) ? value : [value];
166
+ const cues = list.map(v => nonEmpty(v)).filter((v) => !!v);
167
+ return cues.length > 0 ? cues : undefined;
168
+ }
169
+ /** One row of a delay listing. `owed` is true only for an open delay whose date has passed. */
170
+ function delayRow(chart, step, delay, state, now) {
171
+ return {
172
+ chart_id: chart.id,
173
+ desired_outcome: chart.desired_outcome,
174
+ step_id: step.id,
175
+ step_title: step.title,
176
+ step_status: step.status,
177
+ state,
178
+ resolve_by: delay.resolve_by,
179
+ days_remaining: daysRemaining(delay.resolve_by, now),
180
+ owed: state === 'open' && isOwed(delay, now),
181
+ reason: delay.reason,
182
+ chosen_by: delay.chosen_by,
183
+ chosen_at: delay.chosen_at,
184
+ primary_choice: delay.primary_choice,
185
+ wake_on: delay.wake_on ?? [],
186
+ session: delay.session ?? null,
187
+ ...(delay.ended_at
188
+ ? { ended_at: delay.ended_at, ended_by: delay.ended_by, outcome: delay.outcome, end_note: delay.end_note }
189
+ : {}),
190
+ };
191
+ }
192
+ function stepSummary(step, now) {
193
+ const delay = isOpen(step.delayed_resolution) ? step.delayed_resolution : undefined;
194
+ return {
195
+ id: step.id,
196
+ title: step.title,
197
+ status: step.status,
198
+ progress: step.progress,
199
+ due_date: step.due_date,
200
+ has_sub_chart: !!step.sub_chart_id,
201
+ ...(delay
202
+ ? { delayed: { resolve_by: delay.resolve_by, days_remaining: daysRemaining(delay.resolve_by, now), owed: isOwed(delay, now) } }
203
+ : {}),
204
+ };
205
+ }
206
+ function openDelayCounts(chart, now) {
207
+ let delayed = 0;
208
+ let owed = 0;
209
+ for (const s of chart.action_steps) {
210
+ if (isOpen(s.delayed_resolution)) {
211
+ delayed++;
212
+ if (isOwed(s.delayed_resolution, now))
213
+ owed++;
214
+ }
215
+ }
216
+ return { delayed_steps: delayed, owed_delays: owed };
217
+ }
218
+ // ── Tools ──────────────────────────────────────────────────────────────────
219
+ export function createStructuralTensionTools(targetStore) {
220
+ const store = targetStore;
221
+ async function loadChart(chartId) {
222
+ return (await store.getChart(chartId));
223
+ }
224
+ return [
225
+ {
226
+ name: "create_structural_tension_chart",
227
+ description: "Create a structural tension chart with desired outcome, current reality, and optional action steps. Charts are associated with a medicine wheel direction. REMEMBER: Desired outcome is what you want to CREATE (not solve). Current reality must be factual assessment (never 'ready to begin'). When the chart has a due date, the initial steps get due dates spread between now and that date.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ desired_outcome: {
232
+ type: "string",
233
+ description: "What you want to CREATE (not solve/fix). Focus on positive outcomes, not problems to eliminate. Must be visualizable, specific, quantified where possible.",
234
+ },
235
+ current_reality: {
236
+ type: "string",
237
+ description: "Factual assessment of where you are NOW relative to the desired outcome. NEVER use 'ready to begin' or readiness statements. Just facts.",
238
+ },
239
+ direction: {
240
+ type: "string",
241
+ enum: ["east", "south", "west", "north"],
242
+ description: "Which medicine wheel direction this chart serves (east=vision, south=growth, west=reflection, north=wisdom)",
243
+ },
244
+ due_date: {
245
+ type: "string",
246
+ description: "When you want to achieve this outcome (ISO date string, optional). Stored in ISO form.",
247
+ },
248
+ action_steps: {
249
+ type: "array",
250
+ items: { type: "string" },
251
+ description: "Optional strategic secondary choices (NOT tasks). Each step should support advancement toward the desired outcome.",
252
+ },
42
253
  },
254
+ required: ["desired_outcome", "current_reality", "direction"],
43
255
  },
44
- required: ["desired_outcome", "current_reality", "direction"],
45
- },
46
- handler: async (args) => {
47
- try {
48
- const chartId = `stc:${args.direction}:${Date.now()}`;
49
- const actionSteps = (args.action_steps || []).map((title, idx) => ({
50
- id: `${chartId}_action_${idx}`,
51
- title,
52
- current_reality: 'Not yet assessed',
53
- status: 'pending',
54
- progress: 0,
55
- ceremonies_honored: [],
56
- relations_honored: [],
57
- }));
58
- const chart = {
59
- id: chartId,
60
- desired_outcome: args.desired_outcome,
61
- current_reality: args.current_reality,
62
- direction: args.direction,
63
- action_steps: actionSteps,
64
- due_date: args.due_date,
65
- created_at: new Date().toISOString(),
66
- updated_at: new Date().toISOString(),
67
- phase: 'germination',
68
- ceremonies_linked: [],
69
- };
70
- await store.saveChart(chart);
71
- return {
72
- chart_id: chartId,
73
- direction: args.direction,
74
- phase: 'germination',
75
- action_steps_count: actionSteps.length,
76
- chart: chart,
77
- teaching: "Structural tension is the generative force. The tension between desired outcome and current reality naturally seeks resolution through advancement.",
78
- };
79
- }
80
- catch (error) {
81
- const errorMsg = error instanceof Error ? error.message : String(error);
82
- return { status: "error", message: `Failed to create chart: ${errorMsg}` };
83
- }
84
- },
85
- },
86
- {
87
- name: "add_action_step",
88
- description: "Add a strategic secondary choice (action step) to an existing structural tension chart. Each action step is NOT a task — it's a strategic choice that supports the primary goal. Can be telescoped into its own sub-chart.",
89
- inputSchema: {
90
- type: "object",
91
- properties: {
92
- chart_id: {
93
- type: "string",
94
- description: "ID of the chart to add action step to",
95
- },
96
- title: {
97
- type: "string",
98
- description: "Title of the action step (strategic secondary choice)",
99
- },
100
- current_reality: {
101
- type: "string",
102
- description: "Current reality for this specific action step",
103
- },
104
- due_date: {
105
- type: "string",
106
- description: "Optional due date for this action step",
107
- },
256
+ handler: async (args) => {
257
+ try {
258
+ let dueDate;
259
+ if (args.due_date !== undefined && args.due_date !== null && args.due_date !== '') {
260
+ const iso = isoOrNull(args.due_date);
261
+ if (!iso)
262
+ return refuse("due_date_invalid", `due_date is not a date: ${JSON.stringify(args.due_date)}. Use an ISO date, e.g. 2026-11-30T17:00:00Z`);
263
+ dueDate = iso;
264
+ }
265
+ const chartId = `stc:${args.direction}:${Date.now()}`;
266
+ const titles = args.action_steps || [];
267
+ const stepDue = spreadDueDates(dueDate, titles.length);
268
+ const actionSteps = titles.map((title, idx) => newStep(`${chartId}_action_${idx}`, title, stepDue[idx]));
269
+ const chart = {
270
+ id: chartId,
271
+ desired_outcome: args.desired_outcome,
272
+ current_reality: args.current_reality,
273
+ direction: args.direction,
274
+ action_steps: actionSteps,
275
+ due_date: dueDate,
276
+ created_at: new Date().toISOString(),
277
+ updated_at: new Date().toISOString(),
278
+ phase: 'germination',
279
+ ceremonies_linked: [],
280
+ };
281
+ await store.saveChart(chart);
282
+ return {
283
+ chart_id: chartId,
284
+ direction: args.direction,
285
+ phase: 'germination',
286
+ action_steps_count: actionSteps.length,
287
+ chart: chart,
288
+ teaching: "Structural tension is the generative force. The tension between desired outcome and current reality naturally seeks resolution through advancement.",
289
+ };
290
+ }
291
+ catch (error) {
292
+ const errorMsg = error instanceof Error ? error.message : String(error);
293
+ return { status: "error", message: `Failed to create chart: ${errorMsg}` };
294
+ }
108
295
  },
109
- required: ["chart_id", "title", "current_reality"],
110
- },
111
- handler: async (args) => {
112
- try {
113
- const chart = (await store.getChart(args.chart_id));
114
- if (!chart) {
115
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
116
- }
117
- const actionStep = {
118
- id: `${args.chart_id}_action_${chart.action_steps.length}`,
119
- title: args.title,
120
- current_reality: args.current_reality,
121
- status: 'pending',
122
- progress: 0,
123
- due_date: args.due_date,
124
- ceremonies_honored: [],
125
- relations_honored: [],
126
- };
127
- chart.action_steps.push(actionStep);
128
- chart.updated_at = new Date().toISOString();
129
- await store.saveChart(chart);
130
- return {
131
- action_step_id: actionStep.id,
132
- chart_id: args.chart_id,
133
- total_steps: chart.action_steps.length,
134
- teaching: "Action steps are strategic secondary choices understood in context of the structural tension, not isolated tasks.",
135
- };
136
- }
137
- catch (error) {
138
- const errorMsg = error instanceof Error ? error.message : String(error);
139
- return { status: "error", message: `Failed to add action step: ${errorMsg}` };
140
- }
141
296
  },
142
- },
143
- {
144
- name: "telescope_action_step",
145
- description: "Expand an action step into its own structural tension chart (sub-chart). The action step title becomes the desired outcome of the new chart. This enables hierarchical planning while maintaining structural tension at each level.",
146
- inputSchema: {
147
- type: "object",
148
- properties: {
149
- chart_id: {
150
- type: "string",
151
- description: "ID of the parent chart",
152
- },
153
- action_step_id: {
154
- type: "string",
155
- description: "ID of the action step to telescope",
156
- },
157
- new_current_reality: {
158
- type: "string",
159
- description: "Current reality for the telescoped chart (required, must be factual)",
160
- },
161
- initial_action_steps: {
162
- type: "array",
163
- items: { type: "string" },
164
- description: "Optional initial action steps for the sub-chart",
297
+ {
298
+ name: "get_chart",
299
+ description: "Read a structural tension chart whole: desired outcome, current reality, due date and every action step as stored, including delayed resolutions and their history.",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ chart_id: { type: "string", description: "ID of the chart" },
165
304
  },
305
+ required: ["chart_id"],
306
+ },
307
+ handler: async (args) => {
308
+ try {
309
+ const chart = await loadChart(args.chart_id);
310
+ if (!chart) {
311
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
312
+ }
313
+ const now = Date.now();
314
+ return {
315
+ chart_id: chart.id,
316
+ chart,
317
+ ...openDelayCounts(chart, now),
318
+ teaching: "The chart is read as a whole: the tension lives between its desired outcome and its current reality, not in any single step.",
319
+ };
320
+ }
321
+ catch (error) {
322
+ const errorMsg = error instanceof Error ? error.message : String(error);
323
+ return { status: "error", message: `Failed to get chart: ${errorMsg}` };
324
+ }
166
325
  },
167
- required: ["chart_id", "action_step_id", "new_current_reality"],
168
- },
169
- handler: async (args) => {
170
- try {
171
- const parentChart = (await store.getChart(args.chart_id));
172
- if (!parentChart) {
173
- return { status: "not_found", message: `Parent chart ${args.chart_id} not found` };
174
- }
175
- const actionStep = parentChart.action_steps.find((s) => s.id === args.action_step_id);
176
- if (!actionStep) {
177
- return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
178
- }
179
- const subChartId = `stc:${parentChart.direction}:${Date.now()}:sub`;
180
- const subActionSteps = (args.initial_action_steps || []).map((title, idx) => ({
181
- id: `${subChartId}_action_${idx}`,
182
- title,
183
- current_reality: 'Not yet assessed',
184
- status: 'pending',
185
- progress: 0,
186
- ceremonies_honored: [],
187
- relations_honored: [],
188
- }));
189
- const subChart = {
190
- id: subChartId,
191
- desired_outcome: actionStep.title,
192
- current_reality: args.new_current_reality,
193
- direction: parentChart.direction,
194
- action_steps: subActionSteps,
195
- due_date: actionStep.due_date || parentChart.due_date,
196
- created_at: new Date().toISOString(),
197
- updated_at: new Date().toISOString(),
198
- phase: 'germination',
199
- ceremonies_linked: [],
200
- };
201
- await store.saveChart(subChart);
202
- // Link sub-chart to action step
203
- actionStep.sub_chart_id = subChartId;
204
- parentChart.updated_at = new Date().toISOString();
205
- await store.saveChart(parentChart);
206
- return {
207
- sub_chart_id: subChartId,
208
- parent_chart_id: args.chart_id,
209
- action_step_id: args.action_step_id,
210
- desired_outcome: actionStep.title,
211
- sub_action_steps: subActionSteps.length,
212
- teaching: "Each action step can become its own structural tension chart. The hierarchy maintains tension at every level.",
213
- };
214
- }
215
- catch (error) {
216
- const errorMsg = error instanceof Error ? error.message : String(error);
217
- return { status: "error", message: `Failed to telescope action step: ${errorMsg}` };
218
- }
219
326
  },
220
- },
221
- {
222
- name: "update_action_progress",
223
- description: "Update progress on an action step without marking it complete. Progress can advance while work continues.",
224
- inputSchema: {
225
- type: "object",
226
- properties: {
227
- chart_id: {
228
- type: "string",
229
- description: "ID of the chart",
230
- },
231
- action_step_id: {
232
- type: "string",
233
- description: "ID of the action step",
234
- },
235
- progress: {
236
- type: "number",
237
- description: "Progress percentage (0-100)",
238
- minimum: 0,
239
- maximum: 100,
240
- },
241
- new_current_reality: {
242
- type: "string",
243
- description: "Optional updated current reality for this action step",
327
+ {
328
+ name: "get_action_step",
329
+ description: "Read one action step of a chart, with its due date, any delayed resolution (open or ended) and its history, and the sub-chart it telescopes into when it has one.",
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {
333
+ chart_id: { type: "string", description: "ID of the chart holding the step" },
334
+ step_id: { type: "string", description: "ID of the action step" },
244
335
  },
336
+ required: ["chart_id", "step_id"],
337
+ },
338
+ handler: async (args) => {
339
+ try {
340
+ const chart = await loadChart(args.chart_id);
341
+ if (!chart) {
342
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
343
+ }
344
+ const step = findStep(chart, args.step_id);
345
+ if (!step) {
346
+ return { status: "not_found", message: `Action step ${args.step_id} not found in chart ${args.chart_id}` };
347
+ }
348
+ const now = Date.now();
349
+ let subChart;
350
+ if (step.sub_chart_id)
351
+ subChart = await loadChart(step.sub_chart_id);
352
+ return {
353
+ chart_id: chart.id,
354
+ chart: {
355
+ id: chart.id,
356
+ desired_outcome: chart.desired_outcome,
357
+ direction: chart.direction,
358
+ due_date: chart.due_date,
359
+ },
360
+ step,
361
+ delay: step.delayed_resolution
362
+ ? delayRow(chart, step, step.delayed_resolution, isOpen(step.delayed_resolution) ? 'open' : 'ended', now)
363
+ : null,
364
+ ...(step.sub_chart_id
365
+ ? subChart
366
+ ? { sub_chart: subChart }
367
+ : { sub_chart: null, sub_chart_missing: step.sub_chart_id }
368
+ : {}),
369
+ teaching: "A step is a strategic secondary choice, read in the context of the chart it serves.",
370
+ };
371
+ }
372
+ catch (error) {
373
+ const errorMsg = error instanceof Error ? error.message : String(error);
374
+ return { status: "error", message: `Failed to get action step: ${errorMsg}` };
375
+ }
245
376
  },
246
- required: ["chart_id", "action_step_id", "progress"],
247
- },
248
- handler: async (args) => {
249
- try {
250
- const chart = (await store.getChart(args.chart_id));
251
- if (!chart) {
252
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
253
- }
254
- const actionStep = chart.action_steps.find((s) => s.id === args.action_step_id);
255
- if (!actionStep) {
256
- return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
257
- }
258
- actionStep.progress = args.progress;
259
- if (args.progress > 0 && actionStep.status === 'pending') {
260
- actionStep.status = 'in_progress';
261
- }
262
- if (args.new_current_reality) {
263
- actionStep.current_reality = args.new_current_reality;
264
- }
265
- chart.updated_at = new Date().toISOString();
266
- await store.saveChart(chart);
267
- return {
268
- action_step_id: args.action_step_id,
269
- progress: args.progress,
270
- status: actionStep.status,
271
- teaching: "Progress without completion is honest advancement. The structure continues to seek resolution.",
272
- };
273
- }
274
- catch (error) {
275
- const errorMsg = error instanceof Error ? error.message : String(error);
276
- return { status: "error", message: `Failed to update progress: ${errorMsg}` };
277
- }
278
377
  },
279
- },
280
- {
281
- name: "mark_action_complete",
282
- description: "Mark an action step as complete. Optionally link a ceremony that honored this completion.",
283
- inputSchema: {
284
- type: "object",
285
- properties: {
286
- chart_id: {
287
- type: "string",
288
- description: "ID of the chart",
289
- },
290
- action_step_id: {
291
- type: "string",
292
- description: "ID of the action step to complete",
293
- },
294
- ceremony_id: {
295
- type: "string",
296
- description: "Optional ceremony ID that honored this completion",
378
+ {
379
+ name: "add_action_step",
380
+ description: "Add a strategic secondary choice (action step) to an existing structural tension chart. Each action step is NOT a task — it's a strategic choice that supports the primary goal. Can be telescoped into its own sub-chart. A step without its own due date takes the chart's.",
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {
384
+ chart_id: {
385
+ type: "string",
386
+ description: "ID of the chart to add action step to",
387
+ },
388
+ title: {
389
+ type: "string",
390
+ description: "Title of the action step (strategic secondary choice)",
391
+ },
392
+ current_reality: {
393
+ type: "string",
394
+ description: "Current reality for this specific action step",
395
+ },
396
+ due_date: {
397
+ type: "string",
398
+ description: "Optional due date for this action step (ISO date). When omitted, the step takes the chart's due date.",
399
+ },
297
400
  },
401
+ required: ["chart_id", "title", "current_reality"],
298
402
  },
299
- required: ["chart_id", "action_step_id"],
300
- },
301
- handler: async (args) => {
302
- try {
303
- const chart = (await store.getChart(args.chart_id));
304
- if (!chart) {
305
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
306
- }
307
- const actionStep = chart.action_steps.find((s) => s.id === args.action_step_id);
308
- if (!actionStep) {
309
- return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
310
- }
311
- actionStep.status = 'completed';
312
- actionStep.progress = 100;
313
- actionStep.completed_at = new Date().toISOString();
314
- if (args.ceremony_id) {
315
- actionStep.ceremonies_honored.push(args.ceremony_id);
316
- if (!chart.ceremonies_linked.includes(args.ceremony_id)) {
317
- chart.ceremonies_linked.push(args.ceremony_id);
403
+ handler: async (args) => {
404
+ try {
405
+ let dueDate;
406
+ if (args.due_date !== undefined && args.due_date !== null && args.due_date !== '') {
407
+ const iso = isoOrNull(args.due_date);
408
+ if (!iso)
409
+ return refuse("due_date_invalid", `due_date is not a date: ${JSON.stringify(args.due_date)}. Use an ISO date, e.g. 2026-11-30T17:00:00Z`);
410
+ dueDate = iso;
318
411
  }
412
+ const chart = await loadChart(args.chart_id);
413
+ if (!chart) {
414
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
415
+ }
416
+ const inherited = dueDate === undefined;
417
+ if (inherited)
418
+ dueDate = isoOrNull(chart.due_date) ?? undefined;
419
+ const actionStep = newStep(nextStepId(chart), args.title, dueDate, args.current_reality);
420
+ chart.action_steps.push(actionStep);
421
+ chart.updated_at = new Date().toISOString();
422
+ await store.saveChart(chart);
423
+ return {
424
+ action_step_id: actionStep.id,
425
+ chart_id: args.chart_id,
426
+ due_date: actionStep.due_date ?? null,
427
+ due_date_inherited: inherited && actionStep.due_date !== undefined,
428
+ total_steps: chart.action_steps.length,
429
+ teaching: "Action steps are strategic secondary choices understood in context of the structural tension, not isolated tasks.",
430
+ };
431
+ }
432
+ catch (error) {
433
+ const errorMsg = error instanceof Error ? error.message : String(error);
434
+ return { status: "error", message: `Failed to add action step: ${errorMsg}` };
319
435
  }
320
- // Update chart phase based on completion
321
- const completedCount = chart.action_steps.filter((s) => s.status === 'completed').length;
322
- const totalCount = chart.action_steps.length;
323
- if (totalCount > 0) {
324
- if (completedCount === totalCount) {
325
- chart.phase = 'completion';
326
- }
327
- else if (completedCount > 0) {
328
- chart.phase = 'assimilation';
329
- }
330
- }
331
- chart.updated_at = new Date().toISOString();
332
- await store.saveChart(chart);
333
- const allComplete = completedCount === totalCount && totalCount > 0;
334
- return {
335
- action_step_id: args.action_step_id,
336
- completed_at: actionStep.completed_at,
337
- chart_phase: chart.phase,
338
- completed_count: completedCount,
339
- total_count: totalCount,
340
- all_complete: allComplete,
341
- ceremony_suggestion: allComplete
342
- ? "All action steps complete. Consider logging a closing ceremony (direction: north) to honor this achievement."
343
- : undefined,
344
- teaching: "Completion flows into current reality. Each completed step advances the whole structure.",
345
- };
346
- }
347
- catch (error) {
348
- const errorMsg = error instanceof Error ? error.message : String(error);
349
- return { status: "error", message: `Failed to mark complete: ${errorMsg}` };
350
- }
436
+ },
351
437
  },
352
- },
353
- {
354
- name: "update_current_reality",
355
- description: "Update the current reality assessment of a chart. Current reality should be updated as circumstances change. Always factual, never readiness statements.",
356
- inputSchema: {
357
- type: "object",
358
- properties: {
359
- chart_id: {
360
- type: "string",
361
- description: "ID of the chart",
362
- },
363
- new_current_reality: {
364
- type: "string",
365
- description: "Updated factual assessment of current reality",
438
+ {
439
+ name: "remove_action_step",
440
+ description: "Remove an action step from a chart. A sub-chart the step telescoped into is not deleted; its id is returned so it can be reached.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ chart_id: { type: "string", description: "ID of the chart" },
445
+ step_id: { type: "string", description: "ID of the action step to remove" },
366
446
  },
447
+ required: ["chart_id", "step_id"],
448
+ },
449
+ handler: async (args) => {
450
+ try {
451
+ const chart = await loadChart(args.chart_id);
452
+ if (!chart) {
453
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
454
+ }
455
+ const index = chart.action_steps.findIndex(s => s.id === args.step_id);
456
+ if (index === -1) {
457
+ return { status: "not_found", message: `Action step ${args.step_id} not found in chart ${args.chart_id}` };
458
+ }
459
+ const [removed] = chart.action_steps.splice(index, 1);
460
+ settlePhase(chart);
461
+ chart.updated_at = new Date().toISOString();
462
+ await store.saveChart(chart);
463
+ return {
464
+ chart_id: chart.id,
465
+ removed_step: removed,
466
+ remaining_steps: chart.action_steps.length,
467
+ ...(removed.sub_chart_id ? { sub_chart_left: removed.sub_chart_id } : {}),
468
+ teaching: "Removing a step is a strategic choice too: the structure is rebuilt around what still serves the desired outcome.",
469
+ };
470
+ }
471
+ catch (error) {
472
+ const errorMsg = error instanceof Error ? error.message : String(error);
473
+ return { status: "error", message: `Failed to remove action step: ${errorMsg}` };
474
+ }
367
475
  },
368
- required: ["chart_id", "new_current_reality"],
369
- },
370
- handler: async (args) => {
371
- try {
372
- const chart = (await store.getChart(args.chart_id));
373
- if (!chart) {
374
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
375
- }
376
- const previous = chart.current_reality;
377
- chart.current_reality = args.new_current_reality;
378
- chart.updated_at = new Date().toISOString();
379
- await store.saveChart(chart);
380
- return {
381
- chart_id: args.chart_id,
382
- previous_reality: previous,
383
- current_reality: args.new_current_reality,
384
- teaching: "Honest assessment of current reality is essential. The tension between reality and vision drives advancement.",
385
- };
386
- }
387
- catch (error) {
388
- const errorMsg = error instanceof Error ? error.message : String(error);
389
- return { status: "error", message: `Failed to update current reality: ${errorMsg}` };
390
- }
391
476
  },
392
- },
393
- {
394
- name: "get_chart_progress",
395
- description: "Get progress report for a structural tension chart. Shows desired outcome, current reality, action step status, and overall phase.",
396
- inputSchema: {
397
- type: "object",
398
- properties: {
399
- chart_id: {
400
- type: "string",
401
- description: "ID of the chart",
477
+ {
478
+ name: "telescope_action_step",
479
+ description: "Expand an action step into its own structural tension chart (sub-chart). The action step title becomes the desired outcome of the new chart. This enables hierarchical planning while maintaining structural tension at each level.",
480
+ inputSchema: {
481
+ type: "object",
482
+ properties: {
483
+ chart_id: {
484
+ type: "string",
485
+ description: "ID of the parent chart",
486
+ },
487
+ action_step_id: {
488
+ type: "string",
489
+ description: "ID of the action step to telescope",
490
+ },
491
+ new_current_reality: {
492
+ type: "string",
493
+ description: "Current reality for the telescoped chart (required, must be factual)",
494
+ },
495
+ initial_action_steps: {
496
+ type: "array",
497
+ items: { type: "string" },
498
+ description: "Optional initial action steps for the sub-chart",
499
+ },
402
500
  },
501
+ required: ["chart_id", "action_step_id", "new_current_reality"],
502
+ },
503
+ handler: async (args) => {
504
+ try {
505
+ const parentChart = await loadChart(args.chart_id);
506
+ if (!parentChart) {
507
+ return { status: "not_found", message: `Parent chart ${args.chart_id} not found` };
508
+ }
509
+ const actionStep = findStep(parentChart, args.action_step_id);
510
+ if (!actionStep) {
511
+ return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
512
+ }
513
+ const subChartId = `stc:${parentChart.direction}:${Date.now()}:sub`;
514
+ const subDue = actionStep.due_date || parentChart.due_date;
515
+ const titles = args.initial_action_steps || [];
516
+ const stepDue = spreadDueDates(subDue, titles.length);
517
+ const subActionSteps = titles.map((title, idx) => newStep(`${subChartId}_action_${idx}`, title, stepDue[idx]));
518
+ const subChart = {
519
+ id: subChartId,
520
+ desired_outcome: actionStep.title,
521
+ current_reality: args.new_current_reality,
522
+ direction: parentChart.direction,
523
+ action_steps: subActionSteps,
524
+ due_date: subDue,
525
+ created_at: new Date().toISOString(),
526
+ updated_at: new Date().toISOString(),
527
+ phase: 'germination',
528
+ ceremonies_linked: [],
529
+ };
530
+ await store.saveChart(subChart);
531
+ // Link sub-chart to action step
532
+ actionStep.sub_chart_id = subChartId;
533
+ parentChart.updated_at = new Date().toISOString();
534
+ await store.saveChart(parentChart);
535
+ return {
536
+ sub_chart_id: subChartId,
537
+ parent_chart_id: args.chart_id,
538
+ action_step_id: args.action_step_id,
539
+ desired_outcome: actionStep.title,
540
+ sub_action_steps: subActionSteps.length,
541
+ teaching: "Each action step can become its own structural tension chart. The hierarchy maintains tension at every level.",
542
+ };
543
+ }
544
+ catch (error) {
545
+ const errorMsg = error instanceof Error ? error.message : String(error);
546
+ return { status: "error", message: `Failed to telescope action step: ${errorMsg}` };
547
+ }
403
548
  },
404
- required: ["chart_id"],
405
549
  },
406
- handler: async (args) => {
407
- try {
408
- const chart = (await store.getChart(args.chart_id));
409
- if (!chart) {
410
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
411
- }
412
- const completedSteps = chart.action_steps.filter((s) => s.status === 'completed').length;
413
- const inProgressSteps = chart.action_steps.filter((s) => s.status === 'in_progress').length;
414
- const pendingSteps = chart.action_steps.filter((s) => s.status === 'pending').length;
415
- const avgProgress = chart.action_steps.length > 0
416
- ? chart.action_steps.reduce((sum, s) => sum + s.progress, 0) / chart.action_steps.length
417
- : 0;
418
- return {
419
- chart_id: chart.id,
420
- direction: chart.direction,
421
- phase: chart.phase,
422
- desired_outcome: chart.desired_outcome,
423
- current_reality: chart.current_reality,
424
- due_date: chart.due_date,
550
+ {
551
+ name: "update_action_progress",
552
+ description: "Update progress on an action step without marking it complete. Progress can advance while work continues.",
553
+ inputSchema: {
554
+ type: "object",
555
+ properties: {
556
+ chart_id: {
557
+ type: "string",
558
+ description: "ID of the chart",
559
+ },
560
+ action_step_id: {
561
+ type: "string",
562
+ description: "ID of the action step",
563
+ },
425
564
  progress: {
426
- overall_percentage: Math.round(avgProgress),
427
- completed: completedSteps,
428
- in_progress: inProgressSteps,
429
- pending: pendingSteps,
430
- total: chart.action_steps.length,
431
- },
432
- action_steps: chart.action_steps.map((s) => ({
433
- id: s.id,
434
- title: s.title,
435
- status: s.status,
436
- progress: s.progress,
437
- has_sub_chart: !!s.sub_chart_id,
438
- })),
439
- ceremonies_linked: chart.ceremonies_linked.length,
440
- teaching: "Progress is not linear. The structure advances through maintained tension, not willpower.",
441
- };
442
- }
443
- catch (error) {
444
- const errorMsg = error instanceof Error ? error.message : String(error);
445
- return { status: "error", message: `Failed to get chart progress: ${errorMsg}` };
446
- }
447
- },
448
- },
449
- {
450
- name: "list_structural_tension_charts",
451
- description: "List all active structural tension charts. Filter by medicine wheel direction.",
452
- inputSchema: {
453
- type: "object",
454
- properties: {
455
- direction: {
456
- type: "string",
457
- enum: ["east", "south", "west", "north"],
458
- description: "Filter by medicine wheel direction (optional)",
565
+ type: "number",
566
+ description: "Progress percentage (0-100)",
567
+ minimum: 0,
568
+ maximum: 100,
569
+ },
570
+ new_current_reality: {
571
+ type: "string",
572
+ description: "Optional updated current reality for this action step",
573
+ },
459
574
  },
575
+ required: ["chart_id", "action_step_id", "progress"],
576
+ },
577
+ handler: async (args) => {
578
+ try {
579
+ const chart = await loadChart(args.chart_id);
580
+ if (!chart) {
581
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
582
+ }
583
+ const actionStep = findStep(chart, args.action_step_id);
584
+ if (!actionStep) {
585
+ return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
586
+ }
587
+ actionStep.progress = args.progress;
588
+ if (args.progress > 0 && actionStep.status === 'pending') {
589
+ actionStep.status = 'in_progress';
590
+ }
591
+ if (args.new_current_reality) {
592
+ actionStep.current_reality = args.new_current_reality;
593
+ }
594
+ chart.updated_at = new Date().toISOString();
595
+ await store.saveChart(chart);
596
+ return {
597
+ action_step_id: args.action_step_id,
598
+ progress: args.progress,
599
+ status: actionStep.status,
600
+ teaching: "Progress without completion is honest advancement. The structure continues to seek resolution.",
601
+ };
602
+ }
603
+ catch (error) {
604
+ const errorMsg = error instanceof Error ? error.message : String(error);
605
+ return { status: "error", message: `Failed to update progress: ${errorMsg}` };
606
+ }
460
607
  },
461
608
  },
462
- handler: async (args) => {
463
- try {
464
- const charts = (await store.getAllCharts(args.direction));
465
- return {
466
- count: charts.length,
467
- filter: args.direction || 'all',
468
- charts: charts.map(c => ({
469
- id: c.id,
470
- direction: c.direction,
471
- desired_outcome: c.desired_outcome,
472
- current_reality: c.current_reality,
473
- phase: c.phase,
474
- action_steps_count: c.action_steps.length,
475
- completed_steps: c.action_steps.filter((s) => s.status === 'completed').length,
476
- due_date: c.due_date,
477
- updated_at: c.updated_at,
478
- })),
479
- // An empty chart store and "there is no structural tension work" are
480
- // different facts, and this tool used to make them look identical:
481
- // `{count: 0}` plus a teaching about the strategic landscape reads as
482
- // an answer. Meanwhile `list_relational_nodes` with kind "stc_chart"
483
- // returns rows — a DIFFERENT record, a relational node carrying that
484
- // metadata kind rather than a chart in the chart store. Saying where
485
- // this tool looked lets a caller tell the two apart instead of
486
- // concluding nothing exists.
487
- ...(charts.length === 0
488
- ? {
489
- searched: "the chart store (/api/charts)",
490
- note: "No charts in the chart store. Nodes carrying metadata.kind 'stc_chart' are a separate record — list_relational_nodes with kind 'stc_chart' searches those.",
609
+ {
610
+ name: "mark_action_complete",
611
+ description: "Mark an action step as complete and add 'Completed: <title>' to the chart's current reality. Optionally link a ceremony that honored this completion. A step completed while its resolution was delayed ends that delay with outcome 'resolved'.",
612
+ inputSchema: {
613
+ type: "object",
614
+ properties: {
615
+ chart_id: {
616
+ type: "string",
617
+ description: "ID of the chart",
618
+ },
619
+ action_step_id: {
620
+ type: "string",
621
+ description: "ID of the action step to complete",
622
+ },
623
+ ceremony_id: {
624
+ type: "string",
625
+ description: "Optional ceremony ID that honored this completion",
626
+ },
627
+ completed_by: {
628
+ type: "string",
629
+ description: "Optional: the person or seat completing the step. Recorded as ended_by when the completion ends a delayed resolution.",
630
+ },
631
+ },
632
+ required: ["chart_id", "action_step_id"],
633
+ },
634
+ handler: async (args) => {
635
+ try {
636
+ const chart = await loadChart(args.chart_id);
637
+ if (!chart) {
638
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
639
+ }
640
+ const actionStep = findStep(chart, args.action_step_id);
641
+ if (!actionStep) {
642
+ return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
643
+ }
644
+ const wasCompleted = actionStep.status === 'completed';
645
+ const now = new Date().toISOString();
646
+ let delayEnded;
647
+ if (isOpen(actionStep.delayed_resolution)) {
648
+ // Rule 4b: resolution reached is the third way a delay ends.
649
+ Object.assign(actionStep.delayed_resolution, {
650
+ ended_at: now,
651
+ ended_by: nonEmpty(args.completed_by) ?? 'mark_action_complete',
652
+ outcome: 'resolved',
653
+ end_note: 'completed',
654
+ });
655
+ delayEnded = actionStep.delayed_resolution;
656
+ }
657
+ actionStep.status = 'completed';
658
+ actionStep.progress = 100;
659
+ if (!wasCompleted)
660
+ actionStep.completed_at = now;
661
+ if (args.ceremony_id) {
662
+ actionStep.ceremonies_honored.push(args.ceremony_id);
663
+ if (!chart.ceremonies_linked.includes(args.ceremony_id)) {
664
+ chart.ceremonies_linked.push(args.ceremony_id);
491
665
  }
492
- : {}),
493
- teaching: "Each chart holds structural tension that seeks resolution. Together they form the strategic landscape.",
494
- };
495
- }
496
- catch (error) {
497
- const errorMsg = error instanceof Error ? error.message : String(error);
498
- return { status: "error", message: `Failed to list charts: ${errorMsg}` };
499
- }
666
+ }
667
+ // Completion flows into current reality, once.
668
+ if (!wasCompleted)
669
+ appendReality(chart, `Completed: ${actionStep.title}`);
670
+ const { completed: completedCount, total: totalCount } = settlePhase(chart);
671
+ chart.updated_at = now;
672
+ await store.saveChart(chart);
673
+ const allComplete = completedCount === totalCount && totalCount > 0;
674
+ return {
675
+ action_step_id: args.action_step_id,
676
+ completed_at: actionStep.completed_at,
677
+ chart_phase: chart.phase,
678
+ completed_count: completedCount,
679
+ total_count: totalCount,
680
+ all_complete: allComplete,
681
+ current_reality: chart.current_reality,
682
+ ...(delayEnded ? { delayed_resolution_ended: delayEnded } : {}),
683
+ ceremony_suggestion: allComplete
684
+ ? "All action steps complete. Consider logging a closing ceremony (direction: north) to honor this achievement."
685
+ : undefined,
686
+ teaching: "Completion flows into current reality. Each completed step advances the whole structure.",
687
+ };
688
+ }
689
+ catch (error) {
690
+ const errorMsg = error instanceof Error ? error.message : String(error);
691
+ return { status: "error", message: `Failed to mark complete: ${errorMsg}` };
692
+ }
693
+ },
500
694
  },
501
- },
502
- {
503
- name: "creator_moment_of_truth",
504
- description: "Four-step review process (MMOT) for a structural tension chart. Acknowledges truth, analyzes how things happened, creates plan for adjustment, and sets up feedback. Aligns with West direction (reflection/truth).",
505
- inputSchema: {
506
- type: "object",
507
- properties: {
508
- chart_id: {
509
- type: "string",
510
- description: "ID of the chart to review",
511
- },
512
- expected_outcome: {
513
- type: "string",
514
- description: "What was expected to happen",
515
- },
516
- actual_outcome: {
517
- type: "string",
518
- description: "What actually happened (factual)",
519
- },
520
- analysis: {
521
- type: "string",
522
- description: "How did this come to pass? Step-by-step tracking, not blame.",
523
- },
524
- adjustments: {
525
- type: "array",
526
- items: { type: "string" },
527
- description: "What specific changes will be made based on learnings",
528
- },
529
- feedback_system: {
530
- type: "string",
531
- description: "How will you track whether changes are being made",
695
+ {
696
+ name: "delay_resolution",
697
+ description: "Record on an action step that someone strategically chose to delay its resolution: when resolution is wanted (resolve_by, in the future), why waiting serves the work now, what the delay serves (defaults to the chart's desired outcome), event cues to wake on, and the agent session to rehydrate. Sets the step's due date to resolve_by and its status to 'delayed'. Re-delaying keeps the earlier record in delayed_resolution_history. A completed step cannot be delayed.",
698
+ inputSchema: {
699
+ type: "object",
700
+ properties: {
701
+ chart_id: { type: "string", description: "ID of the chart holding the step" },
702
+ step_id: { type: "string", description: "ID of the action step" },
703
+ resolve_by: { type: "string", description: "When resolution is wanted (ISO date-time, in the future). Becomes the step's due date." },
704
+ reason: { type: "string", description: "Why delaying resolution serves the work now" },
705
+ chosen_by: { type: "string", description: "The person or seat that made the choice" },
706
+ primary_choice: { type: "string", description: "What the delay serves. Defaults to the chart's desired outcome, copied now." },
707
+ wake_on: {
708
+ type: "array",
709
+ items: { type: "string" },
710
+ description: "Event cues, in words, that should end the wait before resolve_by",
711
+ },
712
+ session: {
713
+ type: "object",
714
+ description: "The agent session to rehydrate when the delay ends",
715
+ properties: {
716
+ session_id: { type: "string", description: "Claude Code session id, or another agent's" },
717
+ agent: { type: "string", description: "Agent kind (default 'claude')" },
718
+ host: { type: "string", description: "Host the session lives on (gaia, ilex, eury)" },
719
+ cwd: { type: "string", description: "Working directory of the session" },
720
+ tmux: { type: "string", description: "tmux session or pane" },
721
+ resume_command: { type: "string", description: "Command that resumes it, e.g. `cd <cwd> && claude --resume <id>`" },
722
+ inventory_record: { type: "string", description: "Path of its record in the session inventory" },
723
+ },
724
+ required: ["session_id"],
725
+ },
532
726
  },
727
+ required: ["chart_id", "step_id", "resolve_by", "reason", "chosen_by"],
728
+ },
729
+ handler: async (args) => {
730
+ try {
731
+ const reason = nonEmpty(args.reason);
732
+ if (!reason)
733
+ return refuse("reason_missing", "reason is required: say why delaying resolution serves the work now");
734
+ const chosenBy = nonEmpty(args.chosen_by);
735
+ if (!chosenBy)
736
+ return refuse("chosen_by_missing", "chosen_by is required: the person or seat that made the choice");
737
+ const resolveBy = isoOrNull(args.resolve_by);
738
+ if (!resolveBy) {
739
+ return refuse("resolve_by_invalid", `resolve_by is not a date: ${JSON.stringify(args.resolve_by)}. Use an ISO date-time, e.g. 2026-11-02T09:00:00Z`);
740
+ }
741
+ const now = Date.now();
742
+ if (Date.parse(resolveBy) <= now) {
743
+ return refuse("resolve_by_past", `resolve_by ${resolveBy} is not in the future. A delay names when resolution is wanted, later than now.`);
744
+ }
745
+ const { session, error: sessionError } = normalizeSession(args.session);
746
+ if (sessionError)
747
+ return refuse("session_invalid", sessionError);
748
+ const chart = await loadChart(args.chart_id);
749
+ if (!chart) {
750
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
751
+ }
752
+ const step = findStep(chart, args.step_id);
753
+ if (!step) {
754
+ return { status: "not_found", message: `Action step ${args.step_id} not found in chart ${args.chart_id}` };
755
+ }
756
+ if (step.status === 'completed') {
757
+ return refuse("step_completed", `Action step ${step.id} is completed; a completed step cannot be delayed.`);
758
+ }
759
+ const record = {
760
+ chosen_at: new Date(now).toISOString(),
761
+ chosen_by: chosenBy,
762
+ resolve_by: resolveBy,
763
+ reason,
764
+ primary_choice: nonEmpty(args.primary_choice) ?? chart.desired_outcome,
765
+ };
766
+ const wakeOn = normalizeWakeOn(args.wake_on);
767
+ if (wakeOn)
768
+ record.wake_on = wakeOn;
769
+ if (session)
770
+ record.session = session;
771
+ // Nothing is overwritten: an earlier record, open or ended, moves to history.
772
+ const previous = step.delayed_resolution;
773
+ if (previous) {
774
+ step.delayed_resolution_history = [...(step.delayed_resolution_history ?? []), previous];
775
+ }
776
+ step.delayed_resolution = record;
777
+ step.due_date = resolveBy;
778
+ step.status = 'delayed';
779
+ chart.updated_at = new Date(now).toISOString();
780
+ await store.saveChart(chart);
781
+ return {
782
+ chart_id: chart.id,
783
+ step_id: step.id,
784
+ status: step.status,
785
+ due_date: step.due_date,
786
+ days_remaining: daysRemaining(resolveBy, now),
787
+ delayed_resolution: record,
788
+ redelayed: !!previous && isOpen(previous),
789
+ history_length: step.delayed_resolution_history?.length ?? 0,
790
+ step,
791
+ teaching: "Delaying resolution is a secondary choice in service of a primary choice. The tension is held on purpose, with a date when resolution is wanted.",
792
+ };
793
+ }
794
+ catch (error) {
795
+ const errorMsg = error instanceof Error ? error.message : String(error);
796
+ return { status: "error", message: `Failed to delay resolution: ${errorMsg}` };
797
+ }
533
798
  },
534
- required: ["chart_id", "expected_outcome", "actual_outcome", "analysis", "adjustments"],
535
799
  },
536
- handler: async (args) => {
537
- try {
538
- const chart = (await store.getChart(args.chart_id));
539
- if (!chart) {
540
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
541
- }
542
- const mmotId = `mmot:${args.chart_id}:${Date.now()}`;
543
- await store.saveMmot({
544
- id: mmotId,
545
- chart_id: args.chart_id,
546
- timestamp: new Date().toISOString(),
547
- step1_expected: args.expected_outcome,
548
- step1_actual: args.actual_outcome,
549
- step2_analysis: args.analysis,
550
- step3_adjustments: args.adjustments,
551
- step4_feedback: args.feedback_system || '',
552
- });
553
- // Update current reality based on MMOT
554
- chart.current_reality = args.actual_outcome;
555
- chart.updated_at = new Date().toISOString();
556
- await store.saveChart(chart);
557
- return {
558
- mmot_id: mmotId,
559
- chart_id: args.chart_id,
560
- steps: {
561
- step1_acknowledge: {
562
- expected: args.expected_outcome,
563
- actual: args.actual_outcome,
564
- discrepancy: args.expected_outcome !== args.actual_outcome,
800
+ {
801
+ name: "list_delayed_resolutions",
802
+ description: "List open delayed resolutions across all charts, sorted by resolve_by (soonest first), with days_remaining and owed (resolve_by has passed). Each row names the step, its chart, the primary choice, wake_on cues and the session to rehydrate.",
803
+ inputSchema: {
804
+ type: "object",
805
+ properties: {
806
+ due_within_days: {
807
+ type: "number",
808
+ minimum: 0,
809
+ description: "Only delays whose resolve_by falls within this many days from now (owed ones included)",
810
+ },
811
+ include_ended: {
812
+ type: "boolean",
813
+ description: "Also list ended delays and earlier records kept in history (default false)",
814
+ },
815
+ },
816
+ },
817
+ handler: async (args) => {
818
+ try {
819
+ let windowEnd = Infinity;
820
+ const now = Date.now();
821
+ if (args.due_within_days !== undefined && args.due_within_days !== null) {
822
+ const days = Number(args.due_within_days);
823
+ if (!Number.isFinite(days) || days < 0) {
824
+ return refuse("due_within_days_invalid", `due_within_days must be a number of days, 0 or more: ${JSON.stringify(args.due_within_days)}`);
825
+ }
826
+ windowEnd = now + days * DAY_MS;
827
+ }
828
+ const includeEnded = args.include_ended === true || args.include_ended === 'true';
829
+ const charts = (await store.getAllCharts());
830
+ const rows = [];
831
+ for (const chart of charts) {
832
+ for (const step of chart.action_steps ?? []) {
833
+ const current = step.delayed_resolution;
834
+ if (current) {
835
+ const state = isOpen(current) ? 'open' : 'ended';
836
+ if (state === 'open' || includeEnded)
837
+ rows.push(delayRow(chart, step, current, state, now));
838
+ }
839
+ if (includeEnded) {
840
+ for (const past of step.delayed_resolution_history ?? []) {
841
+ rows.push(delayRow(chart, step, past, past.ended_at ? 'ended' : 'superseded', now));
842
+ }
843
+ }
844
+ }
845
+ }
846
+ const listed = rows
847
+ .filter(r => Date.parse(r.resolve_by) <= windowEnd)
848
+ .sort((a, b) => Date.parse(a.resolve_by) - Date.parse(b.resolve_by));
849
+ return {
850
+ count: listed.length,
851
+ open_count: listed.filter(r => r.state === 'open').length,
852
+ owed_count: listed.filter(r => r.owed).length,
853
+ as_of: new Date(now).toISOString(),
854
+ filter: {
855
+ due_within_days: args.due_within_days ?? null,
856
+ include_ended: includeEnded,
565
857
  },
566
- step2_analyze: args.analysis,
567
- step3_plan: args.adjustments,
568
- step4_feedback: args.feedback_system || 'Not specified',
569
- },
570
- current_reality_updated: true,
571
- teaching: "The goal is effectiveness, not perfection. Use discrepancies to learn, not to judge. Truth as a verb.",
572
- };
573
- }
574
- catch (error) {
575
- const errorMsg = error instanceof Error ? error.message : String(error);
576
- return { status: "error", message: `Failed to complete MMOT: ${errorMsg}` };
577
- }
858
+ delays: listed,
859
+ teaching: "A delay whose date has passed is owed: the choice to wait has come due and asks to be resolved, re-chosen, or released.",
860
+ };
861
+ }
862
+ catch (error) {
863
+ const errorMsg = error instanceof Error ? error.message : String(error);
864
+ return { status: "error", message: `Failed to list delayed resolutions: ${errorMsg}` };
865
+ }
866
+ },
578
867
  },
579
- },
580
- {
581
- name: "link_ceremony_to_chart",
582
- description: "Link a ceremony to a structural tension chart. Ceremonies witness and honor the work of advancing toward desired outcomes.",
583
- inputSchema: {
584
- type: "object",
585
- properties: {
586
- chart_id: {
587
- type: "string",
588
- description: "ID of the chart",
589
- },
590
- ceremony_id: {
591
- type: "string",
592
- description: "ID of the ceremony to link",
868
+ {
869
+ name: "end_delayed_resolution",
870
+ description: "End a step's open delayed resolution, either by rehydrating its agent session or by releasing the step. The record is kept with ended_at, ended_by and outcome, and the step returns to pending or in_progress. When rehydrated, returns the session's resume_command.",
871
+ inputSchema: {
872
+ type: "object",
873
+ properties: {
874
+ chart_id: { type: "string", description: "ID of the chart holding the step" },
875
+ step_id: { type: "string", description: "ID of the action step" },
876
+ ended_by: { type: "string", description: "The person or seat ending the delay" },
877
+ outcome: {
878
+ type: "string",
879
+ enum: ["rehydrated", "released"],
880
+ description: "rehydrated: the session is being resumed; released: the step is let go from the wait",
881
+ },
882
+ end_note: { type: "string", description: "Optional note on how the delay ended" },
593
883
  },
884
+ required: ["chart_id", "step_id", "ended_by", "outcome"],
885
+ },
886
+ handler: async (args) => {
887
+ try {
888
+ const endedBy = nonEmpty(args.ended_by);
889
+ if (!endedBy)
890
+ return refuse("ended_by_missing", "ended_by is required: the person or seat ending the delay");
891
+ if (args.outcome !== 'rehydrated' && args.outcome !== 'released') {
892
+ return refuse("outcome_invalid", `outcome must be 'rehydrated' or 'released', got ${JSON.stringify(args.outcome)}`);
893
+ }
894
+ const chart = await loadChart(args.chart_id);
895
+ if (!chart) {
896
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
897
+ }
898
+ const step = findStep(chart, args.step_id);
899
+ if (!step) {
900
+ return { status: "not_found", message: `Action step ${args.step_id} not found in chart ${args.chart_id}` };
901
+ }
902
+ const delay = step.delayed_resolution;
903
+ if (!isOpen(delay)) {
904
+ return refuse("no_open_delay", `Action step ${step.id} has no open delayed resolution to end.`);
905
+ }
906
+ const now = new Date().toISOString();
907
+ delay.ended_at = now;
908
+ delay.ended_by = endedBy;
909
+ delay.outcome = args.outcome;
910
+ const note = nonEmpty(args.end_note);
911
+ if (note)
912
+ delay.end_note = note;
913
+ if (step.status === 'delayed')
914
+ step.status = statusAfterDelay(step);
915
+ chart.updated_at = now;
916
+ await store.saveChart(chart);
917
+ const resume = args.outcome === 'rehydrated' ? resumeCommandFor(delay.session) : undefined;
918
+ return {
919
+ chart_id: chart.id,
920
+ step_id: step.id,
921
+ status: step.status,
922
+ outcome: delay.outcome,
923
+ delayed_resolution: delay,
924
+ ...(resume
925
+ ? {
926
+ session: delay.session ?? null,
927
+ ...resume,
928
+ ...(resume.resume_command === null
929
+ ? { note: delay.session ? `No resume command is known for agent '${delay.session.agent}'.` : 'This delay carried no session to rehydrate.' }
930
+ : {}),
931
+ }
932
+ : {}),
933
+ teaching: args.outcome === 'rehydrated'
934
+ ? "Rehydrating brings the waiting session back with its state, so the work resumes where the choice to wait left it."
935
+ : "Releasing ends the wait. The step returns to the structure as an ordinary secondary choice.",
936
+ };
937
+ }
938
+ catch (error) {
939
+ const errorMsg = error instanceof Error ? error.message : String(error);
940
+ return { status: "error", message: `Failed to end delayed resolution: ${errorMsg}` };
941
+ }
594
942
  },
595
- required: ["chart_id", "ceremony_id"],
596
943
  },
597
- handler: async (args) => {
598
- try {
599
- const chart = (await store.getChart(args.chart_id));
600
- if (!chart) {
601
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
602
- }
603
- if (!chart.ceremonies_linked.includes(args.ceremony_id)) {
604
- chart.ceremonies_linked.push(args.ceremony_id);
944
+ {
945
+ name: "update_current_reality",
946
+ description: "Update the current reality assessment of a chart. Current reality should be updated as circumstances change. Always factual, never readiness statements.",
947
+ inputSchema: {
948
+ type: "object",
949
+ properties: {
950
+ chart_id: {
951
+ type: "string",
952
+ description: "ID of the chart",
953
+ },
954
+ new_current_reality: {
955
+ type: "string",
956
+ description: "Updated factual assessment of current reality",
957
+ },
958
+ },
959
+ required: ["chart_id", "new_current_reality"],
960
+ },
961
+ handler: async (args) => {
962
+ try {
963
+ const chart = await loadChart(args.chart_id);
964
+ if (!chart) {
965
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
966
+ }
967
+ const previous = chart.current_reality;
968
+ chart.current_reality = args.new_current_reality;
605
969
  chart.updated_at = new Date().toISOString();
606
- store.saveChart(chart);
607
- }
608
- return {
609
- chart_id: args.chart_id,
610
- ceremony_id: args.ceremony_id,
611
- total_ceremonies: chart.ceremonies_linked.length,
612
- teaching: "Ceremony witnesses the work. Strategic advancement is relational practice.",
613
- };
614
- }
615
- catch (error) {
616
- const errorMsg = error instanceof Error ? error.message : String(error);
617
- return { status: "error", message: `Failed to link ceremony: ${errorMsg}` };
618
- }
970
+ await store.saveChart(chart);
971
+ return {
972
+ chart_id: args.chart_id,
973
+ previous_reality: previous,
974
+ current_reality: args.new_current_reality,
975
+ teaching: "Honest assessment of current reality is essential. The tension between reality and vision drives advancement.",
976
+ };
977
+ }
978
+ catch (error) {
979
+ const errorMsg = error instanceof Error ? error.message : String(error);
980
+ return { status: "error", message: `Failed to update current reality: ${errorMsg}` };
981
+ }
982
+ },
619
983
  },
620
- },
621
- {
622
- name: "link_chart_to_cycle",
623
- description: "Link a structural tension chart to a medicine wheel research cycle. Charts can belong to cycles for organizational grouping.",
624
- inputSchema: {
625
- type: "object",
626
- properties: {
627
- chart_id: {
628
- type: "string",
629
- description: "ID of the chart",
984
+ {
985
+ name: "update_desired_outcome",
986
+ description: "Replace the desired outcome of a chart. The desired outcome names what is being created, not a problem to remove.",
987
+ inputSchema: {
988
+ type: "object",
989
+ properties: {
990
+ chart_id: { type: "string", description: "ID of the chart" },
991
+ new_desired_outcome: { type: "string", description: "What you want to CREATE, stated as something that will exist" },
630
992
  },
631
- cycle_id: {
632
- type: "string",
633
- description: "ID of the medicine wheel cycle to link to",
993
+ required: ["chart_id", "new_desired_outcome"],
994
+ },
995
+ handler: async (args) => {
996
+ try {
997
+ const outcome = nonEmpty(args.new_desired_outcome);
998
+ if (!outcome)
999
+ return refuse("desired_outcome_missing", "new_desired_outcome must be a non-empty statement of what is being created");
1000
+ const chart = await loadChart(args.chart_id);
1001
+ if (!chart) {
1002
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1003
+ }
1004
+ const previous = chart.desired_outcome;
1005
+ chart.desired_outcome = outcome;
1006
+ chart.updated_at = new Date().toISOString();
1007
+ await store.saveChart(chart);
1008
+ return {
1009
+ chart_id: chart.id,
1010
+ previous_desired_outcome: previous,
1011
+ desired_outcome: outcome,
1012
+ teaching: "Changing the desired outcome changes the whole structure: every step is re-read against what is now being created.",
1013
+ };
1014
+ }
1015
+ catch (error) {
1016
+ const errorMsg = error instanceof Error ? error.message : String(error);
1017
+ return { status: "error", message: `Failed to update desired outcome: ${errorMsg}` };
1018
+ }
1019
+ },
1020
+ },
1021
+ {
1022
+ name: "update_chart_due_date",
1023
+ description: "Move a chart's due date. The date is stored in ISO form and the move is recorded in the chart's due_date_history. Optionally respread the due dates of open, non-delayed steps between now and the new date. Reports how many open steps fall after the new date.",
1024
+ inputSchema: {
1025
+ type: "object",
1026
+ properties: {
1027
+ chart_id: { type: "string", description: "ID of the chart" },
1028
+ due_date: { type: "string", description: "The new due date (ISO date)" },
1029
+ redistribute_action_steps: {
1030
+ type: "boolean",
1031
+ description: "Respread open steps' due dates between now and the new date (default false). Steps with an open delay keep their resolve_by.",
1032
+ },
634
1033
  },
1034
+ required: ["chart_id", "due_date"],
1035
+ },
1036
+ handler: async (args) => {
1037
+ try {
1038
+ const iso = isoOrNull(args.due_date);
1039
+ if (!iso)
1040
+ return refuse("due_date_invalid", `due_date is not a date: ${JSON.stringify(args.due_date)}. Use an ISO date, e.g. 2026-11-30T17:00:00Z`);
1041
+ const chart = await loadChart(args.chart_id);
1042
+ if (!chart) {
1043
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1044
+ }
1045
+ const now = Date.now();
1046
+ const at = new Date(now).toISOString();
1047
+ const previous = chart.due_date ?? null;
1048
+ chart.due_date = iso;
1049
+ chart.due_date_history = [...(chart.due_date_history ?? []), { from: previous, to: iso, at }];
1050
+ const open = chart.action_steps.filter(s => s.status !== 'completed');
1051
+ let rescheduled = 0;
1052
+ if (args.redistribute_action_steps === true || args.redistribute_action_steps === 'true') {
1053
+ const movable = open
1054
+ .filter(s => !isOpen(s.delayed_resolution))
1055
+ .sort((a, b) => {
1056
+ const da = a.due_date ? Date.parse(a.due_date) : Infinity;
1057
+ const db = b.due_date ? Date.parse(b.due_date) : Infinity;
1058
+ return da - db;
1059
+ });
1060
+ const dates = spreadDueDates(iso, movable.length, now);
1061
+ movable.forEach((s, i) => { s.due_date = dates[i]; });
1062
+ rescheduled = movable.length;
1063
+ }
1064
+ const newTime = Date.parse(iso);
1065
+ const pastDue = open.filter(s => s.due_date && Date.parse(s.due_date) > newTime);
1066
+ chart.updated_at = at;
1067
+ await store.saveChart(chart);
1068
+ return {
1069
+ chart_id: chart.id,
1070
+ previous_due_date: previous,
1071
+ new_due_date: iso,
1072
+ action_steps_rescheduled: rescheduled,
1073
+ action_steps_past_due_date: pastDue.length,
1074
+ ...(pastDue.length > 0 ? { steps_after_due_date: pastDue.map(s => ({ id: s.id, title: s.title, due_date: s.due_date, status: s.status })) } : {}),
1075
+ teaching: "The due date is part of the structure. Moving it is a choice that every open step is read against.",
1076
+ };
1077
+ }
1078
+ catch (error) {
1079
+ const errorMsg = error instanceof Error ? error.message : String(error);
1080
+ return { status: "error", message: `Failed to update chart due date: ${errorMsg}` };
1081
+ }
635
1082
  },
636
- required: ["chart_id", "cycle_id"],
637
1083
  },
638
- handler: async (args) => {
639
- try {
640
- const chart = (await store.getChart(args.chart_id));
641
- if (!chart) {
642
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
643
- }
644
- chart.cycle_id = args.cycle_id;
645
- chart.updated_at = new Date().toISOString();
646
- await store.saveChart(chart);
647
- return {
648
- chart_id: args.chart_id,
649
- cycle_id: args.cycle_id,
650
- teaching: "Charts within cycles form the strategic landscape of a research journey.",
651
- };
652
- }
653
- catch (error) {
654
- const errorMsg = error instanceof Error ? error.message : String(error);
655
- return { status: "error", message: `Failed to link chart to cycle: ${errorMsg}` };
656
- }
1084
+ {
1085
+ name: "get_chart_progress",
1086
+ description: "Get progress report for a structural tension chart. Shows desired outcome, current reality, action step status and due dates, delayed resolutions (owed when their date has passed), the next action, and overall phase.",
1087
+ inputSchema: {
1088
+ type: "object",
1089
+ properties: {
1090
+ chart_id: {
1091
+ type: "string",
1092
+ description: "ID of the chart",
1093
+ },
1094
+ },
1095
+ required: ["chart_id"],
1096
+ },
1097
+ handler: async (args) => {
1098
+ try {
1099
+ const chart = await loadChart(args.chart_id);
1100
+ if (!chart) {
1101
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1102
+ }
1103
+ const now = Date.now();
1104
+ const steps = chart.action_steps;
1105
+ const count = (status) => steps.filter(s => s.status === status).length;
1106
+ const avgProgress = steps.length > 0
1107
+ ? steps.reduce((sum, s) => sum + (s.progress ?? 0), 0) / steps.length
1108
+ : 0;
1109
+ // The next action is the open, not-delayed step due soonest. A delayed
1110
+ // step was chosen to wait; when its date passes it is reported as owed.
1111
+ const next = steps
1112
+ .filter(s => s.status !== 'completed' && !isOpen(s.delayed_resolution))
1113
+ .sort((a, b) => {
1114
+ const da = a.due_date ? Date.parse(a.due_date) : Infinity;
1115
+ const db = b.due_date ? Date.parse(b.due_date) : Infinity;
1116
+ return da - db;
1117
+ })[0];
1118
+ return {
1119
+ chart_id: chart.id,
1120
+ direction: chart.direction,
1121
+ phase: chart.phase,
1122
+ desired_outcome: chart.desired_outcome,
1123
+ current_reality: chart.current_reality,
1124
+ due_date: chart.due_date,
1125
+ progress: {
1126
+ overall_percentage: Math.round(avgProgress),
1127
+ completed: count('completed'),
1128
+ in_progress: count('in_progress'),
1129
+ pending: count('pending'),
1130
+ delayed: count('delayed'),
1131
+ total: steps.length,
1132
+ },
1133
+ ...openDelayCounts(chart, now),
1134
+ next_action: next ? { id: next.id, title: next.title, status: next.status, due_date: next.due_date ?? null } : null,
1135
+ action_steps: steps.map(s => stepSummary(s, now)),
1136
+ ceremonies_linked: chart.ceremonies_linked.length,
1137
+ teaching: "Progress is not linear. The structure advances through maintained tension, not willpower.",
1138
+ };
1139
+ }
1140
+ catch (error) {
1141
+ const errorMsg = error instanceof Error ? error.message : String(error);
1142
+ return { status: "error", message: `Failed to get chart progress: ${errorMsg}` };
1143
+ }
1144
+ },
657
1145
  },
658
- },
659
- {
660
- name: "honor_relation_in_action_step",
661
- description: "Honor a relational node in an action step. Links the strategic work to relational accountability.",
662
- inputSchema: {
663
- type: "object",
664
- properties: {
665
- chart_id: {
666
- type: "string",
667
- description: "ID of the chart",
1146
+ {
1147
+ name: "list_structural_tension_charts",
1148
+ description: "List all active structural tension charts, each with its steps' status, due dates and delayed resolutions. Filter by medicine wheel direction.",
1149
+ inputSchema: {
1150
+ type: "object",
1151
+ properties: {
1152
+ direction: {
1153
+ type: "string",
1154
+ enum: ["east", "south", "west", "north"],
1155
+ description: "Filter by medicine wheel direction (optional)",
1156
+ },
668
1157
  },
669
- action_step_id: {
670
- type: "string",
671
- description: "ID of the action step",
1158
+ },
1159
+ handler: async (args) => {
1160
+ try {
1161
+ const charts = (await store.getAllCharts(args.direction));
1162
+ const now = Date.now();
1163
+ return {
1164
+ count: charts.length,
1165
+ filter: args.direction || 'all',
1166
+ charts: charts.map(c => ({
1167
+ id: c.id,
1168
+ direction: c.direction,
1169
+ desired_outcome: c.desired_outcome,
1170
+ current_reality: c.current_reality,
1171
+ phase: c.phase,
1172
+ action_steps_count: c.action_steps.length,
1173
+ completed_steps: c.action_steps.filter(s => s.status === 'completed').length,
1174
+ ...openDelayCounts(c, now),
1175
+ due_date: c.due_date,
1176
+ updated_at: c.updated_at,
1177
+ action_steps: c.action_steps.map(s => {
1178
+ const delay = isOpen(s.delayed_resolution) ? s.delayed_resolution : undefined;
1179
+ return {
1180
+ id: s.id,
1181
+ title: s.title,
1182
+ status: s.status,
1183
+ due_date: s.due_date,
1184
+ ...(delay ? { resolve_by: delay.resolve_by, owed: isOwed(delay, now) } : {}),
1185
+ };
1186
+ }),
1187
+ })),
1188
+ // An empty chart store and "there is no structural tension work" are
1189
+ // different facts, and this tool used to make them look identical:
1190
+ // `{count: 0}` plus a teaching about the strategic landscape reads as
1191
+ // an answer. Meanwhile `list_relational_nodes` with kind "stc_chart"
1192
+ // returns rows — a DIFFERENT record, a relational node carrying that
1193
+ // metadata kind rather than a chart in the chart store. Saying where
1194
+ // this tool looked lets a caller tell the two apart instead of
1195
+ // concluding nothing exists.
1196
+ ...(charts.length === 0
1197
+ ? {
1198
+ searched: "the chart store (/api/charts)",
1199
+ note: "No charts in the chart store. Nodes carrying metadata.kind 'stc_chart' are a separate record — list_relational_nodes with kind 'stc_chart' searches those.",
1200
+ }
1201
+ : {}),
1202
+ teaching: "Each chart holds structural tension that seeks resolution. Together they form the strategic landscape.",
1203
+ };
1204
+ }
1205
+ catch (error) {
1206
+ const errorMsg = error instanceof Error ? error.message : String(error);
1207
+ return { status: "error", message: `Failed to list charts: ${errorMsg}` };
1208
+ }
1209
+ },
1210
+ },
1211
+ {
1212
+ name: "creator_moment_of_truth",
1213
+ description: "Four-step review process (MMOT) for a structural tension chart. Acknowledges truth, analyzes how things happened, creates plan for adjustment, and sets up feedback. Aligns with West direction (reflection/truth).",
1214
+ inputSchema: {
1215
+ type: "object",
1216
+ properties: {
1217
+ chart_id: {
1218
+ type: "string",
1219
+ description: "ID of the chart to review",
1220
+ },
1221
+ expected_outcome: {
1222
+ type: "string",
1223
+ description: "What was expected to happen",
1224
+ },
1225
+ actual_outcome: {
1226
+ type: "string",
1227
+ description: "What actually happened (factual)",
1228
+ },
1229
+ analysis: {
1230
+ type: "string",
1231
+ description: "How did this come to pass? Step-by-step tracking, not blame.",
1232
+ },
1233
+ adjustments: {
1234
+ type: "array",
1235
+ items: { type: "string" },
1236
+ description: "What specific changes will be made based on learnings",
1237
+ },
1238
+ feedback_system: {
1239
+ type: "string",
1240
+ description: "How will you track whether changes are being made",
1241
+ },
672
1242
  },
673
- node_id: {
674
- type: "string",
675
- description: "ID of the relational node to honor",
1243
+ required: ["chart_id", "expected_outcome", "actual_outcome", "analysis", "adjustments"],
1244
+ },
1245
+ handler: async (args) => {
1246
+ try {
1247
+ const chart = await loadChart(args.chart_id);
1248
+ if (!chart) {
1249
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1250
+ }
1251
+ const mmotId = `mmot:${args.chart_id}:${Date.now()}`;
1252
+ await store.saveMmot({
1253
+ id: mmotId,
1254
+ chart_id: args.chart_id,
1255
+ timestamp: new Date().toISOString(),
1256
+ step1_expected: args.expected_outcome,
1257
+ step1_actual: args.actual_outcome,
1258
+ step2_analysis: args.analysis,
1259
+ step3_adjustments: args.adjustments,
1260
+ step4_feedback: args.feedback_system || '',
1261
+ });
1262
+ // Update current reality based on MMOT
1263
+ chart.current_reality = args.actual_outcome;
1264
+ chart.updated_at = new Date().toISOString();
1265
+ await store.saveChart(chart);
1266
+ return {
1267
+ mmot_id: mmotId,
1268
+ chart_id: args.chart_id,
1269
+ steps: {
1270
+ step1_acknowledge: {
1271
+ expected: args.expected_outcome,
1272
+ actual: args.actual_outcome,
1273
+ discrepancy: args.expected_outcome !== args.actual_outcome,
1274
+ },
1275
+ step2_analyze: args.analysis,
1276
+ step3_plan: args.adjustments,
1277
+ step4_feedback: args.feedback_system || 'Not specified',
1278
+ },
1279
+ current_reality_updated: true,
1280
+ teaching: "The goal is effectiveness, not perfection. Use discrepancies to learn, not to judge. Truth as a verb.",
1281
+ };
1282
+ }
1283
+ catch (error) {
1284
+ const errorMsg = error instanceof Error ? error.message : String(error);
1285
+ return { status: "error", message: `Failed to complete MMOT: ${errorMsg}` };
1286
+ }
1287
+ },
1288
+ },
1289
+ {
1290
+ name: "link_ceremony_to_chart",
1291
+ description: "Link a ceremony to a structural tension chart. Ceremonies witness and honor the work of advancing toward desired outcomes.",
1292
+ inputSchema: {
1293
+ type: "object",
1294
+ properties: {
1295
+ chart_id: {
1296
+ type: "string",
1297
+ description: "ID of the chart",
1298
+ },
1299
+ ceremony_id: {
1300
+ type: "string",
1301
+ description: "ID of the ceremony to link",
1302
+ },
676
1303
  },
1304
+ required: ["chart_id", "ceremony_id"],
1305
+ },
1306
+ handler: async (args) => {
1307
+ try {
1308
+ const chart = await loadChart(args.chart_id);
1309
+ if (!chart) {
1310
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1311
+ }
1312
+ if (!chart.ceremonies_linked.includes(args.ceremony_id)) {
1313
+ chart.ceremonies_linked.push(args.ceremony_id);
1314
+ chart.updated_at = new Date().toISOString();
1315
+ await store.saveChart(chart);
1316
+ }
1317
+ return {
1318
+ chart_id: args.chart_id,
1319
+ ceremony_id: args.ceremony_id,
1320
+ total_ceremonies: chart.ceremonies_linked.length,
1321
+ teaching: "Ceremony witnesses the work. Strategic advancement is relational practice.",
1322
+ };
1323
+ }
1324
+ catch (error) {
1325
+ const errorMsg = error instanceof Error ? error.message : String(error);
1326
+ return { status: "error", message: `Failed to link ceremony: ${errorMsg}` };
1327
+ }
677
1328
  },
678
- required: ["chart_id", "action_step_id", "node_id"],
679
1329
  },
680
- handler: async (args) => {
681
- try {
682
- const chart = (await store.getChart(args.chart_id));
683
- if (!chart) {
684
- return { status: "not_found", message: `Chart ${args.chart_id} not found` };
685
- }
686
- const actionStep = chart.action_steps.find((s) => s.id === args.action_step_id);
687
- if (!actionStep) {
688
- return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
689
- }
690
- if (!actionStep.relations_honored.includes(args.node_id)) {
691
- actionStep.relations_honored.push(args.node_id);
1330
+ {
1331
+ name: "link_chart_to_cycle",
1332
+ description: "Link a structural tension chart to a medicine wheel research cycle. Charts can belong to cycles for organizational grouping.",
1333
+ inputSchema: {
1334
+ type: "object",
1335
+ properties: {
1336
+ chart_id: {
1337
+ type: "string",
1338
+ description: "ID of the chart",
1339
+ },
1340
+ cycle_id: {
1341
+ type: "string",
1342
+ description: "ID of the medicine wheel cycle to link to",
1343
+ },
1344
+ },
1345
+ required: ["chart_id", "cycle_id"],
1346
+ },
1347
+ handler: async (args) => {
1348
+ try {
1349
+ const chart = await loadChart(args.chart_id);
1350
+ if (!chart) {
1351
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1352
+ }
1353
+ chart.cycle_id = args.cycle_id;
692
1354
  chart.updated_at = new Date().toISOString();
693
- store.saveChart(chart);
694
- }
695
- return {
696
- action_step_id: args.action_step_id,
697
- node_id: args.node_id,
698
- total_relations_honored: actionStep.relations_honored.length,
699
- teaching: "Strategic action is relational. Honoring relations grounds advancement in accountability.",
700
- };
701
- }
702
- catch (error) {
703
- const errorMsg = error instanceof Error ? error.message : String(error);
704
- return { status: "error", message: `Failed to honor relation: ${errorMsg}` };
705
- }
1355
+ await store.saveChart(chart);
1356
+ return {
1357
+ chart_id: args.chart_id,
1358
+ cycle_id: args.cycle_id,
1359
+ teaching: "Charts within cycles form the strategic landscape of a research journey.",
1360
+ };
1361
+ }
1362
+ catch (error) {
1363
+ const errorMsg = error instanceof Error ? error.message : String(error);
1364
+ return { status: "error", message: `Failed to link chart to cycle: ${errorMsg}` };
1365
+ }
1366
+ },
1367
+ },
1368
+ {
1369
+ name: "honor_relation_in_action_step",
1370
+ description: "Honor a relational node in an action step. Links the strategic work to relational accountability.",
1371
+ inputSchema: {
1372
+ type: "object",
1373
+ properties: {
1374
+ chart_id: {
1375
+ type: "string",
1376
+ description: "ID of the chart",
1377
+ },
1378
+ action_step_id: {
1379
+ type: "string",
1380
+ description: "ID of the action step",
1381
+ },
1382
+ node_id: {
1383
+ type: "string",
1384
+ description: "ID of the relational node to honor",
1385
+ },
1386
+ },
1387
+ required: ["chart_id", "action_step_id", "node_id"],
1388
+ },
1389
+ handler: async (args) => {
1390
+ try {
1391
+ const chart = await loadChart(args.chart_id);
1392
+ if (!chart) {
1393
+ return { status: "not_found", message: `Chart ${args.chart_id} not found` };
1394
+ }
1395
+ const actionStep = findStep(chart, args.action_step_id);
1396
+ if (!actionStep) {
1397
+ return { status: "not_found", message: `Action step ${args.action_step_id} not found` };
1398
+ }
1399
+ if (!actionStep.relations_honored.includes(args.node_id)) {
1400
+ actionStep.relations_honored.push(args.node_id);
1401
+ chart.updated_at = new Date().toISOString();
1402
+ await store.saveChart(chart);
1403
+ }
1404
+ return {
1405
+ action_step_id: args.action_step_id,
1406
+ node_id: args.node_id,
1407
+ total_relations_honored: actionStep.relations_honored.length,
1408
+ teaching: "Strategic action is relational. Honoring relations grounds advancement in accountability.",
1409
+ };
1410
+ }
1411
+ catch (error) {
1412
+ const errorMsg = error instanceof Error ? error.message : String(error);
1413
+ return { status: "error", message: `Failed to honor relation: ${errorMsg}` };
1414
+ }
1415
+ },
706
1416
  },
707
- },
708
- ];
1417
+ ];
1418
+ }
1419
+ export const structuralTensionTools = createStructuralTensionTools(store);
709
1420
  //# sourceMappingURL=structural-tension.js.map