coaia-spiral
Version:
Creative-Oriented AI Assistant Memory - MCP server with structural tension charts and advancing spiral patterns, based on principles by Robert Fritz
1,059 lines (1,002 loc) ⢠86.4 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { promises as fs } from 'fs';
import { existsSync } from 'fs';
import path from 'path';
import { LLM_GUIDANCE } from "./generated-llm-guidance.js";
import { fileURLToPath } from 'url';
import minimist from 'minimist';
import { isAbsolute } from 'path';
// Parse args and handle paths safely
const argv = minimist(process.argv.slice(2));
// Handle help command
if (argv.help || argv.h) {
console.log(`
š§ COAIA Spiral - Creative-Oriented AI Assistant Memory System v2.4.0
Based on Robert Fritz's Structural Tension methodology
Enhanced with .coaia project organization
DESCRIPTION:
MCP server that extends knowledge graphs with structural tension charts for
creative-oriented spiral memory management. Supports advancing patterns, telescoping
charts, and natural language interaction for AI assistants.
Project Organization:
⢠.coaia directories for project-specific structural tension charts
⢠Organized chart collections (active, completed, archived)
⢠Chart templates and patterns for common goals
⢠Project-local vs global chart management
USAGE:
coaia-spiral [OPTIONS]
npx coaia-spiral [OPTIONS]
OPTIONS:
--memory-path PATH Custom path for memory storage (default: ./memory.jsonl)
--help, -h Show this help message
CORE FEATURES:
š Structural Tension Charts
⢠Create charts with desired outcomes, current reality, and action steps
⢠Automatic due date distribution for strategic timing
⢠Progress tracking and completion monitoring
š Telescoping Support
⢠Break down action steps into detailed sub-charts
⢠Proper due date inheritance from parent steps
⢠Navigate between overview and details seamlessly
š Advancing Patterns
⢠Completed actions flow into current reality automatically
⢠Success builds momentum for continued advancement
⢠Prevents oscillating patterns through structural awareness
MCP TOOLS AVAILABLE:
Chart Management (Common Workflow):
⢠list_active_charts - START HERE: See all charts and their progress
⢠add_action_step - Add strategic actions to existing charts
⢠telescope_action_step - Break down action steps into detailed sub-charts
⢠update_action_progress - Track progress without completing actions
⢠mark_action_complete - Complete actions & update reality
⢠update_current_reality - Add observations directly to current reality
⢠create_structural_tension_chart - Create new chart with outcome & reality
Project Organization (.coaia directories):
⢠initialize_coaia_project - Set up .coaia directory for organized charts
⢠list_coaia_projects - Show project status and chart organization
⢠move_chart_to_completed - Archive completed charts as learning examples
Chart Analysis (Advanced):
⢠get_chart_progress - Detailed progress (redundant after list_active_charts)
⢠open_nodes - Inspect specific chart components by exact name
⢠read_graph - Dump all data (rarely needed)
Knowledge Graph (Traditional):
⢠create_entities - Add entities (people, concepts, events)
⢠create_relations - Connect entities with relationships
⢠add_observations - Record information about entities
⢠search_nodes - Search across all stored information
⢠read_graph - Export complete graph structure
EXAMPLE USAGE:
# Start with custom memory path
coaia-spiral --memory-path /path/to/my-charts.jsonl
# Use in Claude Desktop (add to claude_desktop_config.json):
{
"mcpServers": {
"my-spiral-project": {
"command": "npx",
"args": ["-y", "coaia-spiral", "--memory-path", "./charts.jsonl"]
}
}
}
# Initialize project-local chart organization
mkdir my-project && cd my-project
git init # or npm init, etc. (any project marker)
# Then use initialize_coaia_project tool in your AI assistant
PROJECT ORGANIZATION PATTERNS:
Project Structure:
my-project/
āāā .coaia/
ā āāā active-charts.jsonl # Current structural tension charts
ā āāā completed-charts.jsonl # Archived successful charts
ā āāā templates/
ā āāā common-goals.jsonl # Reusable patterns
āāā src/
Benefits:
⢠Separate active from completed charts
⢠Learn from successful patterns
⢠Project-specific goal organization
⢠Templates for common desired outcomes
NATURAL LANGUAGE PATTERNS:
Creating Charts:
"I want to create a mobile app in 3 months"
"My desired outcome is to establish a morning routine"
Progress Tracking:
"I completed the research phase yesterday"
"Show me progress on my Python learning goal"
Telescoping:
"Break down the Django tutorial step further"
"I need more detail on the deployment action"
CREATIVE ORIENTATION PRINCIPLES:
ā
Focus on Creation (not problem-solving):
⢠"I want to create..." vs "I need to fix..."
⢠"My desired outcome..." vs "The problem is..."
ā
Structural Tension Awareness:
⢠Always pair desired outcomes with current reality
⢠Honest assessment creates productive tension
⢠Action steps are strategic secondary action we choose todo to achive the primary goal
ā
Advancing Patterns:
⢠Success builds on success
⢠Completed actions become part of current reality
⢠Momentum creates natural progression toward goals
PHILOSOPHY:
COAIA Memory recognizes that structure determines behavior. By organizing
memory around structural tension rather than problem-solving patterns, it
naturally forms a structure that advances and helps build, not just the life you want, but the technologies to supports it's manifestation (hopefully!).
CREDITS:
⢠Author: J.Guillaume D.-Isabelle <jgi@jgwill.com>
⢠Methodology: Robert Fritz - https://robertfritz.com
⢠Foundation: Shane Holloman (original mcp-knowledge-graph)
⢠License: MIT
For more information, see: CLAUDE.md in the package directory
`);
process.exit(0);
}
let memoryPath = argv['memory-path'];
// If a custom path is provided, ensure it's absolute
if (memoryPath && !isAbsolute(memoryPath)) {
memoryPath = path.resolve(process.cwd(), memoryPath);
}
// Define the path to the JSONL file
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// COAIA Project Detection - look for common project markers
function findProjectRoot(startDir = process.cwd()) {
const projectMarkers = ['.git', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod'];
let currentDir = startDir;
const maxDepth = 5;
for (let i = 0; i < maxDepth; i++) {
// Check for project markers
for (const marker of projectMarkers) {
if (existsSync(path.join(currentDir, marker))) {
return currentDir;
}
}
// Move up one directory
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
// Reached root directory
break;
}
currentDir = parentDir;
}
return null;
}
// COAIA-specific storage management for structural tension charts with flexible contexts
// Priority: 1) Configured --memory-path, 2) Project .coaia directory, 3) Default
function getCoaiaStoragePath(context) {
const filename = context ? `charts-${context}.jsonl` : 'charts.jsonl';
// FIRST PRIORITY: Use configured --memory-path (AIM philosophy)
if (memoryPath) {
// If memoryPath points to a directory, use it as base for chart files
if (memoryPath.endsWith('/') || !memoryPath.endsWith('.jsonl')) {
const dir = memoryPath.endsWith('/') ? memoryPath : memoryPath + '/';
return path.join(dir, filename);
}
// If memoryPath is a specific file, use it for default context or create contextual version
if (context) {
const dir = path.dirname(memoryPath);
const name = path.basename(memoryPath, '.jsonl');
return path.join(dir, `${name}-${context}.jsonl`);
}
return memoryPath;
}
// SECOND PRIORITY: Check for project .coaia directory
const projectRoot = findProjectRoot();
if (projectRoot) {
const coaiaDir = path.join(projectRoot, '.coaia');
if (existsSync(coaiaDir)) {
return path.join(coaiaDir, filename);
}
}
// THIRD PRIORITY: Default fallback
return path.join(__dirname, 'memory.jsonl');
}
// Use the custom path or default to the installation directory
const MEMORY_FILE_PATH = getCoaiaStoragePath();
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager {
async loadGraph() {
try {
const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8");
const lines = data.split("\n").filter(line => line.trim() !== "");
return lines.reduce((graph, line) => {
const item = JSON.parse(line);
if (item.type === "entity")
graph.entities.push(item);
if (item.type === "relation")
graph.relations.push(item);
return graph;
}, { entities: [], relations: [] });
}
catch (error) {
if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
return { entities: [], relations: [] };
}
throw error;
}
}
// Helper function to extract current reality from user context
// Maintains structural tension by requiring explicit assessment
extractCurrentRealityFromContext(userInput, actionStepTitle) {
// Common patterns that indicate current reality assessment
const realityPatterns = [
/(?:currently|right now|at present|today)\s+(.{10,})/i,
/(?:i am|we are|the situation is)\s+(.{10,})/i,
/(?:i have|we have|there is|there are)\s+(.{10,})/i,
/(?:my current|our current|the current)\s+(.{10,})/i
];
for (const pattern of realityPatterns) {
const match = userInput.match(pattern);
if (match && match[1]) {
return match[1].trim();
}
}
// If no explicit current reality found, require assessment
return null;
}
// Helper method to determine if we should use COAIA context-aware persistence
shouldUseCoaiaPersistence() {
// If --memory-path is configured, always use COAIA persistence for context support
if (memoryPath) {
return true;
}
// Otherwise, check for project .coaia directory
const projectRoot = findProjectRoot();
return projectRoot !== null && existsSync(path.join(projectRoot, '.coaia'));
}
// Helper method to save graph using appropriate persistence method
async saveGraphAppropriate(graph, context) {
if (this.shouldUseCoaiaPersistence()) {
await this.saveCoaiaGraph(graph, context);
}
else {
await this.saveGraph(graph);
}
}
// COAIA context-aware methods (similar to AIM approach but for structural tension charts)
async loadCoaiaGraph(context) {
const filePath = getCoaiaStoragePath(context);
try {
const data = await fs.readFile(filePath, "utf-8");
const lines = data.split("\n").filter(line => line.trim() !== "" && !line.startsWith("#"));
return lines.reduce((graph, line) => {
const item = JSON.parse(line);
if (item.type === "entity")
graph.entities.push(item);
if (item.type === "relation")
graph.relations.push(item);
return graph;
}, { entities: [], relations: [] });
}
catch (error) {
if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
return { entities: [], relations: [] };
}
throw error;
}
}
async saveCoaiaGraph(graph, context) {
const filePath = getCoaiaStoragePath(context);
const lines = [
...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
];
// Ensure directory exists
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, lines.join("\n"));
}
// Legacy saveGraph method for backward compatibility
async saveGraph(graph) {
const lines = [
...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
];
await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n"));
}
async createEntities(entities) {
const graph = this.shouldUseCoaiaPersistence() ? await this.loadCoaiaGraph() : await this.loadGraph();
const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
graph.entities.push(...newEntities);
await this.saveGraphAppropriate(graph);
return newEntities;
}
async createRelations(relations) {
const graph = this.shouldUseCoaiaPersistence() ? await this.loadCoaiaGraph() : await this.loadGraph();
const newRelations = relations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
existingRelation.to === r.to &&
existingRelation.relationType === r.relationType));
graph.relations.push(...newRelations);
await this.saveGraphAppropriate(graph);
return newRelations;
}
async addObservations(observations) {
const graph = await this.loadGraph();
const results = observations.map(o => {
const entity = graph.entities.find(e => e.name === o.entityName);
if (!entity) {
throw new Error(`Entity with name ${o.entityName} not found`);
}
const newObservations = o.contents.filter(content => !entity.observations.includes(content));
entity.observations.push(...newObservations);
return { entityName: o.entityName, addedObservations: newObservations };
});
await this.saveGraph(graph);
return results;
}
async deleteEntities(entityNames) {
const graph = await this.loadGraph();
graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
await this.saveGraph(graph);
}
async deleteObservations(deletions) {
const graph = await this.loadGraph();
deletions.forEach(d => {
const entity = graph.entities.find(e => e.name === d.entityName);
if (entity) {
entity.observations = entity.observations.filter(o => !d.observations.includes(o));
}
});
await this.saveGraph(graph);
}
async deleteRelations(relations) {
const graph = await this.loadGraph();
graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
r.to === delRelation.to &&
r.relationType === delRelation.relationType));
await this.saveGraph(graph);
}
async readGraph() {
return this.loadGraph();
}
// Very basic search function
async searchNodes(query) {
const graph = await this.loadGraph();
// Filter entities
const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())));
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
const filteredGraph = {
entities: filteredEntities,
relations: filteredRelations,
};
return filteredGraph;
}
async openNodes(names) {
const graph = await this.loadGraph();
// Filter entities
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
const filteredGraph = {
entities: filteredEntities,
relations: filteredRelations,
};
return filteredGraph;
}
// COAIA-specific methods for structural tension charts and creative processes
async createStructuralTensionChart(desiredOutcome, currentReality, dueDate, actionSteps) {
// Educational validation for creative orientation
const problemSolvingWords = ['fix', 'solve', 'eliminate', 'prevent', 'stop', 'avoid', 'reduce', 'remove'];
const detectedProblemWords = problemSolvingWords.filter(word => desiredOutcome.toLowerCase().includes(word));
if (detectedProblemWords.length > 0) {
throw new Error(`š CREATIVE ORIENTATION REQUIRED
Desired Outcome: "${desiredOutcome}"
ā **Problem**: Contains problem-solving language: "${detectedProblemWords.join(', ')}"
š **Principle**: Structural Tension Charts use creative orientation - focus on what you want to CREATE, not what you want to eliminate.
šÆ **Reframe Your Outcome**:
Instead of elimination ā Creation focus
ā
**Examples**:
- Instead of: "Fix communication problems"
- Use: "Establish clear, effective communication practices"
- Instead of: "Reduce website loading time"
- Use: "Achieve fast, responsive website performance"
**Why This Matters**: Problem-solving creates oscillating patterns. Creative orientation creates advancing patterns toward desired outcomes.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
// Educational validation for current reality
const readinessWords = ['ready to', 'prepared to', 'all set', 'ready for', 'set to'];
const detectedReadinessWords = readinessWords.filter(phrase => currentReality.toLowerCase().includes(phrase));
if (detectedReadinessWords.length > 0) {
throw new Error(`š DELAYED RESOLUTION PRINCIPLE VIOLATION
Current Reality: "${currentReality}"
ā **Problem**: Contains readiness assumptions: "${detectedReadinessWords.join(', ')}"
š **Principle**: "Tolerate discrepancy, tension, and delayed resolution" - Robert Fritz
šÆ **What's Needed**: Factual assessment of your actual current state (not readiness or preparation).
ā
**Examples**:
- Instead of: "Ready to learn Python"
- Use: "Never programmed before, interested in web development"
- Instead of: "Prepared to start the project"
- Use: "Have project requirements, no code written yet"
**Why This Matters**: Readiness assumptions prematurely resolve the structural tension needed for creative advancement.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
const chartId = `chart_${Date.now()}`;
const timestamp = new Date().toISOString();
// Create chart, desired outcome, and current reality entities
const entities = [
{
name: `${chartId}_chart`,
entityType: 'structural_tension_chart',
observations: [`Chart created on ${timestamp}`],
metadata: {
chartId,
dueDate,
level: 0,
createdAt: timestamp,
updatedAt: timestamp
}
},
{
name: `${chartId}_desired_outcome`,
entityType: 'desired_outcome',
observations: [desiredOutcome],
metadata: {
chartId,
dueDate,
createdAt: timestamp,
updatedAt: timestamp
}
},
{
name: `${chartId}_current_reality`,
entityType: 'current_reality',
observations: [currentReality],
metadata: {
chartId,
createdAt: timestamp,
updatedAt: timestamp
}
}
];
// Add action steps if provided
if (actionSteps && actionSteps.length > 0) {
const stepDueDates = this.distributeActionStepDates(new Date(), new Date(dueDate), actionSteps.length);
actionSteps.forEach((step, index) => {
entities.push({
name: `${chartId}_action_${index + 1}`,
entityType: 'action_step',
observations: [step],
metadata: {
chartId,
dueDate: stepDueDates[index].toISOString(),
completionStatus: false,
createdAt: timestamp,
updatedAt: timestamp
}
});
});
}
// Create relations
const relations = [
{
from: `${chartId}_chart`,
to: `${chartId}_desired_outcome`,
relationType: 'contains',
metadata: { createdAt: timestamp }
},
{
from: `${chartId}_chart`,
to: `${chartId}_current_reality`,
relationType: 'contains',
metadata: { createdAt: timestamp }
},
{
from: `${chartId}_current_reality`,
to: `${chartId}_desired_outcome`,
relationType: 'creates_tension_with',
metadata: { createdAt: timestamp }
}
];
// Add action step relations
if (actionSteps && actionSteps.length > 0) {
actionSteps.forEach((_, index) => {
const actionName = `${chartId}_action_${index + 1}`;
relations.push({
from: `${chartId}_chart`,
to: actionName,
relationType: 'contains',
metadata: { createdAt: timestamp }
}, {
from: actionName,
to: `${chartId}_desired_outcome`,
relationType: 'advances_toward',
metadata: { createdAt: timestamp }
});
});
}
// Save to graph
await this.createEntities(entities);
await this.createRelations(relations);
return { chartId, entities, relations };
}
async telescopeActionStep(actionStepName, newCurrentReality, initialActionSteps) {
const graph = this.shouldUseCoaiaPersistence() ? await this.loadCoaiaGraph() : await this.loadGraph();
const actionStep = graph.entities.find(e => e.name === actionStepName && e.entityType === 'action_step');
if (!actionStep || !actionStep.metadata?.chartId) {
throw new Error(`Action step ${actionStepName} not found or not properly configured`);
}
const parentChartId = actionStep.metadata.chartId;
const inheritedDueDate = actionStep.metadata.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
const desiredOutcome = actionStep.observations[0]; // Use the action step description as the new desired outcome
const result = await this.createStructuralTensionChart(desiredOutcome, newCurrentReality, inheritedDueDate, initialActionSteps);
// Update the new chart's metadata to reflect telescoping relationship
const newChart = this.shouldUseCoaiaPersistence() ? await this.loadCoaiaGraph() : await this.loadGraph();
const chartEntity = newChart.entities.find(e => e.name === `${result.chartId}_chart`);
if (chartEntity && chartEntity.metadata) {
chartEntity.metadata.parentChart = parentChartId;
chartEntity.metadata.parentActionStep = actionStepName;
chartEntity.metadata.level = (actionStep.metadata.level || 0) + 1;
chartEntity.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraphAppropriate(newChart);
return { chartId: result.chartId, parentChart: parentChartId };
}
async markActionStepComplete(actionStepName) {
const graph = this.shouldUseCoaiaPersistence() ? await this.loadCoaiaGraph() : await this.loadGraph();
// An "action step" can be a 'desired_outcome' of a sub-chart, or a simple 'action_step' entity.
const actionStep = graph.entities.find(e => e.name === actionStepName && (e.entityType === 'action_step' || e.entityType === 'desired_outcome'));
if (!actionStep) {
throw new Error(`Action step ${actionStepName} not found`);
}
const chartId = actionStep.metadata?.chartId;
if (!chartId) {
throw new Error(`Chart ID not found for action step ${actionStepName}`);
}
// Mark the action step itself as complete
if (actionStep.metadata) {
actionStep.metadata.completionStatus = true;
actionStep.metadata.updatedAt = new Date().toISOString();
}
// Also mark the parent chart entity as complete
const chartEntity = graph.entities.find(e => e.name === `${chartId}_chart`);
if (chartEntity && chartEntity.metadata) {
chartEntity.metadata.completionStatus = true;
chartEntity.metadata.updatedAt = new Date().toISOString();
}
// Structural tension principle: completed action steps flow into the CURRENT REALITY
// of the PARENT chart, advancing the overall structure.
const parentChartId = chartEntity?.metadata?.parentChart;
if (parentChartId) {
const parentCurrentReality = graph.entities.find(e => e.name === `${parentChartId}_current_reality` &&
e.entityType === 'current_reality');
if (parentCurrentReality) {
const completionMessage = `Completed: ${actionStep.observations[0]}`;
if (!parentCurrentReality.observations.includes(completionMessage)) {
parentCurrentReality.observations.push(completionMessage);
if (parentCurrentReality.metadata) {
parentCurrentReality.metadata.updatedAt = new Date().toISOString();
}
}
}
}
await this.saveGraphAppropriate(graph);
}
async getChartProgress(chartId) {
const graph = await this.loadGraph();
const actionSteps = graph.entities.filter(e => e.entityType === 'action_step' &&
e.metadata?.chartId === chartId);
const completedActions = actionSteps.filter(e => e.metadata?.completionStatus === true).length;
const totalActions = actionSteps.length;
const progress = totalActions > 0 ? completedActions / totalActions : 0;
// Find next incomplete action step with earliest due date
const incompleteActions = actionSteps
.filter(e => e.metadata?.completionStatus !== true)
.sort((a, b) => {
const dateA = new Date(a.metadata?.dueDate || '').getTime();
const dateB = new Date(b.metadata?.dueDate || '').getTime();
return dateA - dateB;
});
const chart = graph.entities.find(e => e.name === `${chartId}_chart`);
return {
chartId,
progress,
completedActions,
totalActions,
nextAction: incompleteActions[0]?.name,
dueDate: chart?.metadata?.dueDate
};
}
distributeActionStepDates(startDate, endDate, stepCount) {
const totalTime = endDate.getTime() - startDate.getTime();
const stepInterval = totalTime / (stepCount + 1); // +1 to leave space before final due date
const dates = [];
for (let i = 1; i <= stepCount; i++) {
dates.push(new Date(startDate.getTime() + (stepInterval * i)));
}
return dates;
}
async listActiveCharts() {
const graph = await this.loadGraph();
const charts = graph.entities.filter(e => e.entityType === 'structural_tension_chart');
const chartSummaries = await Promise.all(charts.map(async (chart) => {
const chartId = chart.metadata?.chartId || chart.name.replace('_chart', '');
const progress = await this.getChartProgress(chartId);
// Get desired outcome
const desiredOutcome = graph.entities.find(e => e.name === `${chartId}_desired_outcome` && e.entityType === 'desired_outcome');
return {
chartId,
desiredOutcome: desiredOutcome?.observations[0] || 'Unknown outcome',
dueDate: chart.metadata?.dueDate,
progress: progress.progress,
completedActions: progress.completedActions,
totalActions: progress.totalActions,
level: chart.metadata?.level || 0,
parentChart: chart.metadata?.parentChart
};
}));
return chartSummaries.sort((a, b) => {
// Sort by level first (master charts first), then by due date
if (a.level !== b.level)
return a.level - b.level;
const dateA = new Date(a.dueDate || '').getTime();
const dateB = new Date(b.dueDate || '').getTime();
return dateA - dateB;
});
}
async updateActionProgress(actionStepName, progressObservation, updateCurrentReality) {
const graph = await this.loadGraph();
const actionStep = graph.entities.find(e => e.name === actionStepName && (e.entityType === 'action_step' || e.entityType === 'desired_outcome'));
if (!actionStep) {
throw new Error(`Action step ${actionStepName} not found`);
}
// Add progress observation to action step
actionStep.observations.push(progressObservation);
if (actionStep.metadata) {
actionStep.metadata.updatedAt = new Date().toISOString();
}
// Optionally update current reality with progress
if (updateCurrentReality && actionStep.metadata?.chartId) {
const chartEntity = graph.entities.find(e => e.name === `${actionStep.metadata.chartId}_chart`);
const parentChartId = chartEntity?.metadata?.parentChart;
const targetChartId = parentChartId || actionStep.metadata.chartId;
const currentReality = graph.entities.find(e => e.name === `${targetChartId}_current_reality` &&
e.entityType === 'current_reality');
if (currentReality) {
// Progress observations flow into current reality, changing the structural dynamic
const progressMessage = `Progress on ${actionStep.observations[0]}: ${progressObservation}`;
if (!currentReality.observations.includes(progressMessage)) {
currentReality.observations.push(progressMessage);
if (currentReality.metadata) {
currentReality.metadata.updatedAt = new Date().toISOString();
}
}
}
}
await this.saveGraph(graph);
}
async updateCurrentReality(chartId, newObservations) {
const graph = await this.loadGraph();
const currentReality = graph.entities.find(e => e.name === `${chartId}_current_reality` &&
e.entityType === 'current_reality');
if (!currentReality) {
throw new Error(`Chart ${chartId} not found or missing current reality`);
}
// Add new observations to current reality
const uniqueObservations = newObservations.filter(obs => !currentReality.observations.includes(obs));
currentReality.observations.push(...uniqueObservations);
if (currentReality.metadata) {
currentReality.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraph(graph);
}
async updateDesiredOutcome(chartId, newDesiredOutcome) {
const graph = await this.loadGraph();
const desiredOutcomeEntity = graph.entities.find(e => e.name === `${chartId}_desired_outcome` && e.entityType === 'desired_outcome');
if (!desiredOutcomeEntity) {
throw new Error(`Chart ${chartId} desired outcome not found`);
}
// Replace the first observation (which is the desired outcome text)
desiredOutcomeEntity.observations[0] = newDesiredOutcome;
if (desiredOutcomeEntity.metadata) {
desiredOutcomeEntity.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraph(graph);
}
async updateActionStepTitle(actionStepName, newTitle) {
const graph = await this.loadGraph();
const actionStepEntity = graph.entities.find(e => e.name === actionStepName);
if (!actionStepEntity) {
throw new Error(`Action step ${actionStepName} not found`);
}
// Replace the first observation (which is the action step title)
actionStepEntity.observations[0] = newTitle;
if (actionStepEntity.metadata) {
actionStepEntity.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraph(graph);
}
async addActionStep(parentChartId, actionStepTitle, dueDate, currentReality) {
const graph = await this.loadGraph();
const parentChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' && e.metadata?.chartId === parentChartId);
if (!parentChart) {
throw new Error(`Parent chart ${parentChartId} not found`);
}
// Get parent chart's due date for auto-distribution
const parentDueDate = parentChart.metadata?.dueDate;
if (!parentDueDate) {
throw new Error(`Parent chart ${parentChartId} has no due date`);
}
// Calculate due date for action step if not provided
let actionStepDueDate = dueDate;
if (!actionStepDueDate) {
// Distribute between now and parent due date (simple midpoint for now)
const now = new Date();
const parentEnd = new Date(parentDueDate);
const midpoint = new Date(now.getTime() + (parentEnd.getTime() - now.getTime()) / 2);
actionStepDueDate = midpoint.toISOString();
}
// Require current reality assessment - no defaults that prematurely resolve tension
if (!currentReality) {
throw new Error(`š DELAYED RESOLUTION PRINCIPLE VIOLATION
Action step: "${actionStepTitle}"
ā **Problem**: Current reality assessment missing
š **Principle**: "Tolerate discrepancy, tension, and delayed resolution" - Robert Fritz
šÆ **What's Needed**: Honest assessment of your actual current state relative to this action step.
ā
**Examples**:
- "Never used Django, completed Python basics"
- "Built one API, struggling with authentication"
- "Read 3 chapters, concepts still unclear"
ā **Avoid**: "Ready to begin", "Prepared to start", "All set to..."
**Why This Matters**: Premature resolution destroys the structural tension that generates creative advancement. The system NEEDS honest current reality to create productive tension.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
const actionCurrentReality = currentReality;
// Create telescoped structural tension chart
const telescopedChart = await this.createStructuralTensionChart(actionStepTitle, actionCurrentReality, actionStepDueDate);
// Update the telescoped chart's metadata to show parent relationship
const updatedGraph = await this.loadGraph();
const telescopedChartEntity = updatedGraph.entities.find(e => e.name === `${telescopedChart.chartId}_chart`);
if (telescopedChartEntity && telescopedChartEntity.metadata) {
telescopedChartEntity.metadata.parentChart = parentChartId;
telescopedChartEntity.metadata.level = (parentChart.metadata?.level || 0) + 1;
telescopedChartEntity.metadata.updatedAt = new Date().toISOString();
}
// Create relationship: telescoped chart advances toward parent's desired outcome
const parentDesiredOutcome = updatedGraph.entities.find(e => e.name === `${parentChartId}_desired_outcome` && e.entityType === 'desired_outcome');
if (parentDesiredOutcome) {
const timestamp = new Date().toISOString();
await this.createRelations([{
from: `${telescopedChart.chartId}_desired_outcome`,
to: parentDesiredOutcome.name,
relationType: 'advances_toward',
metadata: { createdAt: timestamp }
}]);
}
await this.saveGraph(updatedGraph);
return {
chartId: telescopedChart.chartId,
actionStepName: `${telescopedChart.chartId}_desired_outcome`
};
}
// Enhanced method for LLMs to telescope with intelligent current reality extraction
async telescopeActionStepWithContext(parentChartId, actionStepTitle, userContext, currentReality, dueDate) {
// If current reality not provided, try to extract from context
let finalCurrentReality = currentReality;
if (!finalCurrentReality) {
finalCurrentReality = this.extractCurrentRealityFromContext(userContext, actionStepTitle) ?? undefined;
}
// If still no current reality, provide guidance while maintaining tension
if (!finalCurrentReality) {
throw new Error(`Current reality assessment needed for "${actionStepTitle}". ` +
`Please assess your actual current state relative to this action step. ` +
`Example: "I have never used Django before" or "I completed the basics but haven't built a real project" ` +
`rather than assuming readiness. Structural tension requires honest current reality assessment.`);
}
// Proceed with telescoping using the assessed current reality
return this.addActionStep(parentChartId, actionStepTitle, finalCurrentReality, dueDate);
}
async removeActionStep(parentChartId, actionStepName) {
const graph = await this.loadGraph();
// Find the action step (which is actually a telescoped chart's desired outcome)
const actionStepEntity = graph.entities.find(e => e.name === actionStepName);
if (!actionStepEntity || !actionStepEntity.metadata?.chartId) {
throw new Error(`Action step ${actionStepName} not found`);
}
const telescopedChartId = actionStepEntity.metadata.chartId;
// Verify it belongs to the parent chart
const telescopedChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' &&
e.metadata?.chartId === telescopedChartId &&
e.metadata?.parentChart === parentChartId);
if (!telescopedChart) {
throw new Error(`Action step ${actionStepName} does not belong to chart ${parentChartId}`);
}
// Remove all entities belonging to the telescoped chart
const entitiesToRemove = graph.entities
.filter(e => e.metadata?.chartId === telescopedChartId)
.map(e => e.name);
await this.deleteEntities(entitiesToRemove);
}
// COAIA Project Organization - Enhanced chart management for .coaia directories
async initializeCoaiaProject() {
// Determine the target directory based on configuration priority (same as getCoaiaStoragePath)
let coaiaDir;
if (memoryPath) {
// If --memory-path is configured, use it as the base directory
coaiaDir = memoryPath.endsWith('/') ? memoryPath : (memoryPath.endsWith('.jsonl') ? path.dirname(memoryPath) : memoryPath);
}
else {
// Fallback to project root detection
const projectRoot = findProjectRoot();
if (!projectRoot) {
throw new Error('No project detected and no --memory-path configured. Either run from within a project directory (must contain .git, package.json, etc.) or provide --memory-path');
}
coaiaDir = path.join(projectRoot, '.coaia');
}
// Create directory structure
await fs.mkdir(coaiaDir, { recursive: true });
await fs.mkdir(path.join(coaiaDir, 'templates'), { recursive: true });
// Initialize default chart file (master database equivalent)
const defaultChartFile = path.join(coaiaDir, 'charts.jsonl');
if (!existsSync(defaultChartFile)) {
await fs.writeFile(defaultChartFile, '', 'utf-8');
}
// Create sample context files to demonstrate the concept
const sampleContexts = ['work', 'personal', 'learning'];
for (const context of sampleContexts) {
const contextFile = path.join(coaiaDir, `charts-${context}.jsonl`);
if (!existsSync(contextFile)) {
await fs.writeFile(contextFile, `# COAIA Charts for ${context} context\n`, 'utf-8');
}
}
// Create template files
const templatePath = path.join(coaiaDir, 'templates', 'common-goals.txt');
const templateContent = [
'# COAIA Memory - Structural Tension Chart Templates',
'# Use different contexts for different areas of life/work',
'# Examples: charts-work.jsonl, charts-personal.jsonl, charts-health.jsonl',
'',
'## Context Usage:',
'# Default/Master: charts.jsonl (no context specified)',
'# Work context: charts-work.jsonl (context: "work")',
'# Personal context: charts-personal.jsonl (context: "personal")',
'# Health context: charts-health.jsonl (context: "health")',
'# Learning context: charts-learning.jsonl (context: "learning")',
'',
'## Template Patterns:',
'# Focus on what you want to CREATE (not solve or fix)',
'# Action steps ARE structural tension charts (can be telescoped)',
'# Current reality must be factual, not "ready to begin"'
].join('\n');
await fs.writeFile(templatePath, templateContent);
const structure = {
location: coaiaDir,
files: {
'charts.jsonl': 'Master/default chart database',
'charts-work.jsonl': 'Work-related structural tension charts',
'charts-personal.jsonl': 'Personal goals and projects',
'charts-learning.jsonl': 'Learning and skill development charts',
'templates/common-goals.txt': 'Usage patterns and methodology guidance'
}
};
return {
message: `ā
COAIA project initialized at ${coaiaDir}. Multiple chart contexts available (default, work, personal, learning). Create additional contexts as needed.`,
structure
};
}
async listCoaiaProjects() {
const result = {
global_charts: 0,
project_contexts: undefined
};
// Show configured memory path if set
if (memoryPath) {
result.configured_path = memoryPath;
result.message = `Using configured memory path: ${memoryPath}`;
}
// Check charts in configured/detected COAIA directory
const coaiaDir = memoryPath ?
(memoryPath.endsWith('/') ? memoryPath : (memoryPath.endsWith('.jsonl') ? path.dirname(memoryPath) : memoryPath)) :
(() => {
const projectRoot = findProjectRoot();
return projectRoot ? path.join(projectRoot, '.coaia') : null;
})();
if (coaiaDir && existsSync(coaiaDir)) {
result.current_project = coaiaDir;
// Find all chart files in COAIA directory
try {
const files = await fs.readdir(coaiaDir);
const chartFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('charts'));
const contexts = {};
let totalProjectCharts = 0;
for (const file of chartFiles) {
const filePath = path.join(coaiaDir, file);
const contextName = file === 'charts.jsonl' ? 'default' : file.replace('charts-', '').replace('.jsonl', '');
try {
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n').filter(line => line.trim() && !line.startsWith('#'));
const chartCount = lines.filter(line => {
try {
const item = JSON.parse(line);
return item.type === 'entity' && item.entityType === 'structural_tension_chart';
}
catch {
return false;
}
}).length;
contexts[contextName] = {
file: file,
path: filePath,
charts: chartCount
};
totalProjectCharts += chartCount;
}
catch {
// File not readable, skip
contexts[contextName] = {
file: file,
path: filePath,
charts: 0
};
}
}
result.project_contexts = contexts;
result.total_project_charts = totalProjectCharts;
result.coaia_structure = {
directory: coaiaDir,
available_contexts: Object.keys(contexts).sort(),
templates: path.join(coaiaDir, 'templates')
};
}
catch {
// Directory not readable
result.project_contexts = {};
}
}
return result;
}
// Context-aware structural tension chart methods
async createStructuralTensionChartInContext(desiredOutcome, currentReality, dueDate, context, actionSteps) {
// Use the existing validation logic
const problemSolvingWords = ['fix', 'solve', 'eliminate', 'prevent', 'stop', 'avoid', 'reduce', 'remove'];
const detectedProblemWords = problemSolvingWords.filter(word => desiredOutcome.toLowerCase().includes(word));
if (detectedProblemWords.length > 0) {
throw new Error(`š CREATIVE ORIENTATION REQUIRED
Desired Outcome: "${desiredOutcome}"
ā **Problem**: Contains problem-solving language: "${detectedProblemWords.join(', ')}"
š **Principle**: Structural Tension Charts use creative orientation - focus on what you want to CREATE, not what you want to eliminate.
šÆ **Reframe Your Outcome**:
Instead of elimination ā Creation focus
ā
**Examples**:
- Instead of: "Fix communication problems"
- Use: "Establish clear, effective communication practices"
**Why This Matters**: Problem-solving creates oscillating patterns. Creative orientation creates advancing patterns toward desired outcomes.`);
}
// Create chart using COAIA context system
const graph = await this.loadCoaiaGraph(context);
const chartId = `chart_${Date.now()}`;
// Create chart entities
const entities = [
{
name: `${chartId}_chart`,
entityType: 'structural_tension_chart',
observations: [`Chart created on ${new Date().toISOString()}`],
metadata: {
chartId,
dueDate,
level: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
context: context || 'default'
}
},
{
name: `${chartId}_desired_outcome`,
entityType: 'desired_outcome',
observations: [desiredOutcome],
metadata: {
chartId,
dueDate,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
},
{
name: `${chartId}_cur