agent-planner-mcp 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_GUIDE.md CHANGED
@@ -5,6 +5,8 @@ This guide is optimized for AI agents using AgentPlanner MCP tools.
5
5
  ## Core Workflow
6
6
 
7
7
  ```
8
+ 0. check_coherence_pending() → See if plans/goals need alignment review
9
+ → If stale items: run_coherence_check(plan_id) on each
8
10
  1. suggest_next_tasks(plan_id) → Find ready tasks (dependency-aware)
9
11
  2. get_task_context(node_id, depth=2) → Load progressive context
10
12
  3. Work on tasks (quick_status to track)
@@ -15,6 +17,16 @@ This guide is optimized for AI agents using AgentPlanner MCP tools.
15
17
 
16
18
  ## Essential Tools
17
19
 
20
+ ### Preflight: Alignment Check
21
+ ```javascript
22
+ check_coherence_pending()
23
+ // → Returns stale plans/goals that changed since last review
24
+
25
+ run_coherence_check({ plan_id: "..." })
26
+ // → Evaluates quality (coverage, specificity, ordering, knowledge)
27
+ // → Stamps plan as reviewed, returns score breakdown
28
+ ```
29
+
18
30
  ### Before Starting Work
19
31
  ```javascript
20
32
  get_plan_context({ plan_id: "..." })
package/README.md CHANGED
@@ -13,6 +13,87 @@ MCP server for [AgentPlanner](https://agentplanner.io) — AI agent orchestratio
13
13
 
14
14
  ## Setup
15
15
 
16
+ ## Thin local client (v1)
17
+
18
+ A lightweight CLI loop for task-driven workflows. No MCP client required — useful when an agent (Claude Code, OpenClaw, a script) just needs to read its current task as files and write status back.
19
+
20
+ ### Mental model
21
+
22
+ - AgentPlanner (the API) is the source of truth.
23
+ - `.agentplanner/` files are a regeneratable cache, written by the CLI for the agent to read.
24
+ - The agent works in the real repo. Status changes flow back via explicit writeback commands. There is no live sync.
25
+
26
+ > **Running locally?** See [agent-planner/LOCAL_QUICKSTART.md](https://github.com/TAgents/agent-planner/blob/main/LOCAL_QUICKSTART.md) for the 5-minute path to a full local stack you can point this CLI at. Use `--api-url http://localhost:3000` in the `login` step below.
27
+
28
+ ### The loop
29
+
30
+ ```bash
31
+ # 1. Login — saves credentials and auto-selects a default plan
32
+ # (pass --plan-id to pick one, or it auto-selects if you have exactly one plan)
33
+ npx agent-planner-mcp login --token <token> --api-url https://agentplanner.io/api [--plan-id <id>]
34
+ # Localhost variant (after `docker compose -f docker-compose.local.yml up`):
35
+ npx agent-planner-mcp login --token <token> --api-url http://localhost:3000
36
+
37
+ # 2. See your task queue
38
+ npx agent-planner-mcp tasks [--plan-id <id>]
39
+
40
+ # 3. Pick the next task and pull context (claims it for 30 minutes)
41
+ npx agent-planner-mcp next [--plan-id <id>]
42
+ # Force a fresh recommendation even if you have active work:
43
+ npx agent-planner-mcp next --fresh
44
+
45
+ # 4. Or pull context for a specific plan/node (no claim, no status change)
46
+ npx agent-planner-mcp context --plan-id <plan-id> --node-id <node-id>
47
+ # If a default plan is set, --plan-id can be omitted:
48
+ npx agent-planner-mcp context --node-id <node-id>
49
+
50
+ # 5. Explicit writeback. No live sync.
51
+ npx agent-planner-mcp start # claim + mark in_progress
52
+ npx agent-planner-mcp blocked --message "Waiting on API decision"
53
+ npx agent-planner-mcp done --message "Implemented and verified"
54
+ ```
55
+
56
+ ### `next` resolution order
57
+
58
+ `next` is a smart picker. It resolves in this order:
59
+
60
+ 1. **Resume** — if any task in scope is `in_progress`, pick it. (Source: `resume_in_progress`.)
61
+ 2. **Recommend** — call `suggest_next_tasks` (dependency- and RPI-aware) for a fresh pick. (Source: `suggest_next_tasks`.)
62
+ 3. **Fallback** — first `not_started` task in your queue. (Source: `my_tasks_fallback`.)
63
+
64
+ `tasks` is the queue view; `next` is the smart picker; `next --fresh` skips step 1 and forces a fresh recommendation even when active work exists.
65
+
66
+ ### What `start`, `blocked`, `done` actually do
67
+
68
+ | Command | Status | Claim | Log entry | Learning written to Graphiti |
69
+ |---|---|---|---|---|
70
+ | `start` | `in_progress` | claim (30m TTL) | — | — |
71
+ | `blocked --message ...` | `blocked` | release | `challenge` | — |
72
+ | `done --message ...` | `completed` | release | `progress` | yes (entry_type: `learning`) |
73
+
74
+ All hooks are best-effort: claim/release/learning failures do not block the status update. Claim collisions (another agent already holds the lease) are reported but not fatal.
75
+
76
+ ### What `current-task.md` surfaces
77
+
78
+ Beyond title, description, agent_instructions, and acceptance criteria, the generated `current-task.md` includes BDI signals from the API responses already being fetched:
79
+
80
+ - **Plan health** — `quality_score`, rationale, `coherence_checked_at` (or "never")
81
+ - **Coherence warning** — flagged when `node.coherence_status` is `contradiction_detected` or `stale_beliefs`, with concrete next-step pointers (`check_contradictions`, `recall_knowledge`)
82
+ - **Detected contradictions** — listed when present in the node context
83
+ - **Task mode** — shown when not `free` (RPI awareness for `research`/`plan`/`implement`)
84
+ - **Linked goals**, **relevant knowledge** (top 5), **plan progress snapshot**
85
+
86
+ ### When to use CLI vs MCP vs API skill
87
+
88
+ | You want… | Use |
89
+ |---|---|
90
+ | Zero-setup local task context for any coding agent (Claude Code, OpenClaw, scripts) | **CLI** (this thin client) |
91
+ | Rich, structured tool access from inside an MCP-aware agent (Claude Desktop, Cursor, etc.) | **MCP** (run `npx agent-planner-mcp` as an MCP server) |
92
+ | Direct programmatic integration from your own service | **API** (REST endpoints; same routes the MCP and CLI use) |
93
+
94
+ The CLI is intentionally thin: it covers the read context + writeback loop and nothing else. For decomposition, dependency creation, knowledge graph queries, RPI chains, coherence runs, and goal management, use the MCP server (or the API directly).
95
+
96
+
16
97
  ### Claude Desktop
17
98
 
18
99
  Add to your `claude_desktop_config.json`:
@@ -149,6 +230,10 @@ Add the same JSON config to your Cline MCP settings in VS Code.
149
230
  - `claim_task` / `release_task` - Task locking
150
231
  - `share_plan` - Collaboration management
151
232
 
233
+ ### Alignment & Review
234
+ - `check_coherence_pending` - See which plans/goals need alignment review (staleness check)
235
+ - `run_coherence_check` - Evaluate plan quality and stamp as reviewed
236
+
152
237
  ## LLM Skill Reference
153
238
 
154
239
  See **[SKILL.md](./SKILL.md)** for a complete reference designed to be consumed by LLMs. Include it in system prompts or agent configurations to give any LLM full knowledge of how to use AgentPlanner tools effectively.
package/SKILL.md CHANGED
@@ -31,12 +31,43 @@ You have access to the AgentPlanner MCP tools. AgentPlanner is a collaborative p
31
31
  - You need to coordinate with humans on multi-step projects
32
32
  - You want to persist findings, decisions, or progress across sessions
33
33
  - You are asked to plan, research, or implement something as part of a tracked workflow
34
+ - A user wants help defining or refining a goal
35
+
36
+ ## Goal Coaching
37
+
38
+ When a user expresses an intent, objective, or want — help them turn it into a well-structured goal. Don't just create a goal from their words. Coach them through it:
39
+
40
+ ```
41
+ 1. Listen to the user's intent (however vague)
42
+ 2. recall_knowledge() → Search for related context in the knowledge graph
43
+ 3. list_goals() → Check for overlap with existing goals
44
+ 4. list_plans() → Find related work that might link to this goal
45
+ 5. Propose a structured goal:
46
+ - Clear title
47
+ - Description explaining why this matters
48
+ - Success criteria with specific metrics + targets
49
+ - Linked plans (existing or suggest new ones)
50
+ 6. create_goal() + link plans
51
+ 7. assess_goal_quality() → Check quality and get improvement suggestions
52
+ 8. Iterate with the user until quality is high
53
+ ```
54
+
55
+ **What makes a good goal:**
56
+ - **Clear** — has a title and description that explain what and why
57
+ - **Measurable** — has success criteria with specific metrics and targets (e.g., "API latency < 100ms p99")
58
+ - **Actionable** — has at least one linked plan with concrete tasks
59
+ - **Knowledge-grounded** — related facts exist in the knowledge graph (if not, suggest researching first)
60
+ - **Committed** — promoted from desire to intention when ready, with a time reference
61
+
62
+ Use `assess_goal_quality(goal_id)` after creation to check quality and surface suggestions. Share the results with the user and help them address gaps.
34
63
 
35
64
  ## Workflow
36
65
 
37
66
  Follow this sequence when working on a plan:
38
67
 
39
68
  ```
69
+ 0. PREFLIGHT → check_coherence_pending to see if anything needs alignment review
70
+ ↳ If stale plans found, run_coherence_check on each before starting task work
40
71
  1. ORIENT → suggest_next_tasks or get_task_context to understand what needs doing
41
72
  2. CLAIM → quick_status to mark the task in_progress
42
73
  3. WORK → Do the actual work (code, research, analysis, etc.)
@@ -46,6 +77,22 @@ Follow this sequence when working on a plan:
46
77
  6. NEXT → suggest_next_tasks to find the next ready task
47
78
  ```
48
79
 
80
+ ### Preflight: Alignment Check
81
+
82
+ Before diving into tasks, check if goals, plans, and knowledge are aligned:
83
+
84
+ ```
85
+ check_coherence_pending()
86
+ → Returns stale plans/goals that changed since last review
87
+ → If stale items found:
88
+ run_coherence_check({ plan_id: "<plan_id>" })
89
+ → Evaluates quality (coverage, specificity, ordering, knowledge)
90
+ → Stamps the plan as reviewed
91
+ → Returns quality breakdown + coherence issues
92
+ ```
93
+
94
+ This is a lightweight check (seconds, not minutes). Do it at the start of each work session. Skip if you already checked recently.
95
+
49
96
  ## Loading Context
50
97
 
51
98
  Always load context before starting work. Use `get_task_context` — it gives you exactly the right amount of information based on depth level.
@@ -205,6 +252,16 @@ Cross-plan dependencies work the same as regular dependencies (`blocks`, `requir
205
252
  | `claim_task` | Claim exclusive ownership of a task (prevents agent collisions) |
206
253
  | `release_task` | Release a previously claimed task |
207
254
 
255
+ ### Alignment & Review
256
+
257
+ Check if goals, plans, and knowledge are aligned. Run as a preflight check before starting work.
258
+
259
+ | Tool | Purpose |
260
+ |------|---------|
261
+ | `check_coherence_pending` | See what needs review — returns stale plans/goals that changed since last check |
262
+ | `run_coherence_check` | Evaluate a plan's quality (coverage, specificity, ordering, knowledge) and stamp as reviewed |
263
+ | `assess_goal_quality` | Check how well-defined a goal is (clarity, measurability, actionability, knowledge, commitment) |
264
+
208
265
  ### Knowledge (Temporal Knowledge Graph)
209
266
 
210
267
  All knowledge is stored in the Graphiti temporal knowledge graph, which automatically extracts entities and relationships and enables cross-plan knowledge retrieval. The temporal graph is **cross-plan** and **persists across sessions** — anything recorded is available to all future agents and conversations within the organization.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-planner-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server for AgentPlanner — AI agent orchestration with planning, dependencies, knowledge graphs, and human oversight",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/api-client.js CHANGED
@@ -613,6 +613,11 @@ const goals = {
613
613
  const response = await apiClient.get('/goals/dashboard');
614
614
  return response.data;
615
615
  },
616
+
617
+ getQuality: async (goalId) => {
618
+ const response = await apiClient.get(`/goals/${goalId}/quality`);
619
+ return response.data;
620
+ },
616
621
  };
617
622
 
618
623
  /**
@@ -722,6 +727,17 @@ const graphiti = {
722
727
  }
723
728
  };
724
729
 
730
+ // ─── Users (my-tasks queue) ────────────────────────────────────
731
+ const users = {
732
+ getMyTasks: async (options = {}) => {
733
+ const params = new URLSearchParams();
734
+ if (options.plan_id) params.append('plan_id', options.plan_id);
735
+ const qs = params.toString() ? `?${params.toString()}` : '';
736
+ const response = await apiClient.get(`/users/my-tasks${qs}`);
737
+ return response.data;
738
+ },
739
+ };
740
+
725
741
  // ─── Dependencies (cross-plan & external) ─────────────────────
726
742
  const dependencies = {
727
743
  /**
@@ -760,10 +776,10 @@ const dependencies = {
760
776
  * @param {string} token - API token or JWT
761
777
  * @returns {Object} - API client modules (plans, nodes, etc.)
762
778
  */
763
- function createApiClient(token) {
779
+ function createApiClient(token, options = {}) {
764
780
  const scheme = getAuthScheme(token);
765
781
  const client = axios.create({
766
- baseURL: process.env.API_URL || 'http://localhost:3000',
782
+ baseURL: options.apiUrl || process.env.API_URL || 'http://localhost:3000',
767
783
  headers: {
768
784
  'Content-Type': 'application/json',
769
785
  'Authorization': token ? `${scheme} ${token}` : undefined
@@ -813,6 +829,10 @@ function createApiClient(token) {
813
829
  claimTask: async (planId, nodeId, agentId = 'mcp-agent', ttlMinutes = 30) => (await client.post(`/plans/${planId}/nodes/${nodeId}/claim`, { agent_id: agentId, ttl_minutes: ttlMinutes })).data,
814
830
  releaseTask: async (planId, nodeId, agentId = 'mcp-agent') => (await client.delete(`/plans/${planId}/nodes/${nodeId}/claim`, { data: { agent_id: agentId } })).data,
815
831
  getTaskClaim: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}/claim`)).data,
832
+ suggestNextTasks: async (planId, limit = 5) => {
833
+ const params = new URLSearchParams({ plan_id: planId, limit: String(limit) });
834
+ return (await client.get(`/context/suggest?${params.toString()}`)).data;
835
+ },
816
836
  },
817
837
  comments: {
818
838
  getComments: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}/comments`)).data,
@@ -883,6 +903,12 @@ function createApiClient(token) {
883
903
  removeAchiever: async (goalId, depId) => (await client.delete(`/goals/${goalId}/achievers/${depId}`)).data,
884
904
  getKnowledgeGaps: async (goalId) => (await client.get(`/goals/${goalId}/knowledge-gaps`)).data,
885
905
  getDashboard: async () => (await client.get('/goals/dashboard')).data,
906
+ getQuality: async (goalId) => (await client.get(`/goals/${goalId}/quality`)).data,
907
+ },
908
+ coherence: {
909
+ getPending: async () => (await client.get('/coherence/pending')).data,
910
+ runCheck: async (planId, goalId) => (await client.post(`/plans/${planId}/coherence/check`, goalId ? { goal_id: goalId } : {})).data,
911
+ getPlanCoherence: async (planId) => (await client.get(`/plans/${planId}/coherence`)).data,
886
912
  },
887
913
  context: {
888
914
  getNodeContext: async (nodeId, options = {}) => {
@@ -911,6 +937,14 @@ function createApiClient(token) {
911
937
  listCrossPlan: async (planIds) => (await client.get('/dependencies/cross-plan', { params: { plan_ids: planIds.join(',') } })).data,
912
938
  createExternal: async (data) => (await client.post('/dependencies/external', data)).data,
913
939
  },
940
+ users: {
941
+ getMyTasks: async (options = {}) => {
942
+ const params = new URLSearchParams();
943
+ if (options.plan_id) params.append('plan_id', options.plan_id);
944
+ const qs = params.toString() ? `?${params.toString()}` : '';
945
+ return (await client.get(`/users/my-tasks${qs}`)).data;
946
+ },
947
+ },
914
948
  axiosInstance: client,
915
949
  };
916
950
  }
@@ -919,6 +953,13 @@ function createApiClient(token) {
919
953
  // Export the axios instance for direct use
920
954
  const axiosInstance = apiClient;
921
955
 
956
+ // ─── Coherence ────────────────────────────────────────────────
957
+ const coherence = {
958
+ getPending: () => apiClient.get('/coherence/pending').then(r => r.data),
959
+ runCheck: (planId, goalId) => apiClient.post(`/plans/${planId}/coherence/check`, goalId ? { goal_id: goalId } : {}).then(r => r.data),
960
+ getPlanCoherence: (planId) => apiClient.get(`/plans/${planId}/coherence`).then(r => r.data),
961
+ };
962
+
922
963
  module.exports = {
923
964
  plans,
924
965
  nodes,
@@ -932,6 +973,8 @@ module.exports = {
932
973
  context,
933
974
  graphiti,
934
975
  dependencies,
976
+ coherence,
977
+ users,
935
978
  axiosInstance, // Export for direct API calls
936
979
  createApiClient // Factory for per-session clients (HTTP mode)
937
980
  };
@@ -0,0 +1,64 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ function getConfigDir() {
6
+ if (process.env.AGENT_PLANNER_CONFIG_DIR) {
7
+ return process.env.AGENT_PLANNER_CONFIG_DIR;
8
+ }
9
+
10
+ if (process.platform === 'win32' && process.env.APPDATA) {
11
+ return path.join(process.env.APPDATA, 'agent-planner');
12
+ }
13
+
14
+ return path.join(os.homedir(), '.config', 'agent-planner');
15
+ }
16
+
17
+ function getConfigPath() {
18
+ return path.join(getConfigDir(), 'config.json');
19
+ }
20
+
21
+ function ensureDir(dirPath) {
22
+ fs.mkdirSync(dirPath, { recursive: true });
23
+ }
24
+
25
+ function readConfig() {
26
+ const configPath = getConfigPath();
27
+ if (!fs.existsSync(configPath)) {
28
+ return {};
29
+ }
30
+
31
+ const raw = fs.readFileSync(configPath, 'utf8');
32
+ return JSON.parse(raw);
33
+ }
34
+
35
+ function writeConfig(config) {
36
+ const configDir = getConfigDir();
37
+ ensureDir(configDir);
38
+ const configPath = getConfigPath();
39
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
40
+ return configPath;
41
+ }
42
+
43
+ function resolveApiConfig(overrides = {}) {
44
+ const config = readConfig();
45
+ return {
46
+ apiUrl: overrides.apiUrl || process.env.API_URL || config.apiUrl || 'http://localhost:3000',
47
+ token: overrides.token || process.env.USER_API_TOKEN || process.env.API_TOKEN || config.token || null,
48
+ };
49
+ }
50
+
51
+ function mergeConfig(partial) {
52
+ const existing = readConfig();
53
+ return writeConfig({ ...existing, ...partial, updatedAt: new Date().toISOString() });
54
+ }
55
+
56
+ module.exports = {
57
+ ensureDir,
58
+ getConfigDir,
59
+ getConfigPath,
60
+ readConfig,
61
+ writeConfig,
62
+ mergeConfig,
63
+ resolveApiConfig,
64
+ };
@@ -0,0 +1,646 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+ const { createApiClient } = require('../api-client');
5
+ const { ensureDir, getConfigPath, mergeConfig, readConfig, resolveApiConfig, writeConfig } = require('./config');
6
+
7
+ const DEFAULT_AGENT_ID = 'ap-cli';
8
+ const DEFAULT_CLAIM_TTL_MIN = 30;
9
+
10
+ function parseArgs(args = []) {
11
+ const positional = [];
12
+ const options = {};
13
+
14
+ for (let i = 0; i < args.length; i += 1) {
15
+ const arg = args[i];
16
+ if (!arg.startsWith('--')) {
17
+ positional.push(arg);
18
+ continue;
19
+ }
20
+
21
+ const keyValue = arg.slice(2).split('=');
22
+ const key = keyValue[0].replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
23
+ if (keyValue.length > 1) {
24
+ options[key] = keyValue.slice(1).join('=');
25
+ continue;
26
+ }
27
+
28
+ const next = args[i + 1];
29
+ if (!next || next.startsWith('--')) {
30
+ options[key] = true;
31
+ continue;
32
+ }
33
+
34
+ options[key] = next;
35
+ i += 1;
36
+ }
37
+
38
+ return { positional, options };
39
+ }
40
+
41
+ async function promptForLogin(options) {
42
+ const rl = readline.createInterface({
43
+ input: process.stdin,
44
+ output: process.stdout,
45
+ });
46
+ const prompt = (question) => new Promise((resolve) => {
47
+ rl.question(question, (answer) => resolve(answer.trim()));
48
+ });
49
+
50
+ const apiUrl = options.apiUrl || await prompt('API URL (default: https://agentplanner.io/api): ') || 'https://agentplanner.io/api';
51
+ const token = options.token || await prompt('API token: ');
52
+ rl.close();
53
+
54
+ if (!token) {
55
+ throw new Error('API token is required. Pass --token or run in an interactive terminal.');
56
+ }
57
+
58
+ return { apiUrl, token };
59
+ }
60
+
61
+ async function login(options = {}) {
62
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
63
+ const credentials = (options.token && options.apiUrl)
64
+ ? { apiUrl: options.apiUrl, token: options.token }
65
+ : interactive
66
+ ? await promptForLogin(options)
67
+ : (() => {
68
+ if (!options.token) {
69
+ throw new Error('Missing token. Pass --token for non-interactive login.');
70
+ }
71
+ return {
72
+ apiUrl: options.apiUrl || 'https://agentplanner.io/api',
73
+ token: options.token,
74
+ };
75
+ })();
76
+
77
+ const api = createApiClient(credentials.token, { apiUrl: credentials.apiUrl });
78
+ const plans = await api.plans.getPlans();
79
+
80
+ const configData = {
81
+ apiUrl: credentials.apiUrl,
82
+ token: credentials.token,
83
+ updatedAt: new Date().toISOString(),
84
+ };
85
+
86
+ let defaultPlanId = null;
87
+ if (options.planId) {
88
+ defaultPlanId = options.planId;
89
+ } else if (Array.isArray(plans) && plans.length === 1) {
90
+ defaultPlanId = plans[0].id;
91
+ }
92
+ if (defaultPlanId) {
93
+ configData.defaultPlanId = defaultPlanId;
94
+ }
95
+
96
+ const configPath = writeConfig(configData);
97
+
98
+ return {
99
+ configPath,
100
+ apiUrl: credentials.apiUrl,
101
+ defaultPlanId,
102
+ };
103
+ }
104
+
105
+ function getWorkspaceStatePath(baseDir = process.cwd()) {
106
+ return path.join(baseDir, '.agentplanner');
107
+ }
108
+
109
+ function getWorkspaceContextPath(baseDir = process.cwd()) {
110
+ return path.join(getWorkspaceStatePath(baseDir), 'context.json');
111
+ }
112
+
113
+ function readWorkspaceContext(baseDir = process.cwd()) {
114
+ const contextPath = getWorkspaceContextPath(baseDir);
115
+ if (!fs.existsSync(contextPath)) {
116
+ return null;
117
+ }
118
+
119
+ return JSON.parse(fs.readFileSync(contextPath, 'utf8'));
120
+ }
121
+
122
+ function resolveSelection(options = {}, baseDir = process.cwd()) {
123
+ const workspaceContext = readWorkspaceContext(baseDir) || {};
124
+ const config = readConfig();
125
+ return {
126
+ planId: options.planId || workspaceContext.selection?.planId || config.defaultPlanId || null,
127
+ nodeId: options.nodeId || workspaceContext.selection?.nodeId || null,
128
+ };
129
+ }
130
+
131
+ function renderPlanTree(nodes = []) {
132
+ const lines = [];
133
+
134
+ function walk(node, depth) {
135
+ const indent = ' '.repeat(depth);
136
+ const marker = node.node_type === 'phase' ? '▸' : node.node_type === 'milestone' ? '◆' : '•';
137
+ const status = node.status ? ` [${node.status}]` : '';
138
+ lines.push(`${indent}${marker} ${node.title}${status}`);
139
+ for (const child of node.children || []) {
140
+ walk(child, depth + 1);
141
+ }
142
+ }
143
+
144
+ for (const node of nodes) {
145
+ walk(node, 0);
146
+ }
147
+
148
+ return `${lines.join('\n')}\n`;
149
+ }
150
+
151
+ function stringifyJson(data) {
152
+ return JSON.stringify(data, null, 2) + '\n';
153
+ }
154
+
155
+ function renderBulletList(items = []) {
156
+ return items.filter(Boolean).map((item) => `- ${item}`);
157
+ }
158
+
159
+ function extractAcceptanceCriteria(text = '') {
160
+ const match = text.match(/Acceptance criteria:\s*([\s\S]*)/i);
161
+ if (!match) return [];
162
+
163
+ return match[1]
164
+ .split('\n')
165
+ .map((line) => line.trim())
166
+ .filter((line) => line.startsWith('-'))
167
+ .map((line) => line.replace(/^-\s*/, '').trim());
168
+ }
169
+
170
+ function stripAcceptanceCriteria(text = '') {
171
+ return text.replace(/\n*Acceptance criteria:\s*[\s\S]*/i, '').trim();
172
+ }
173
+
174
+ function renderPlanHealth(plan) {
175
+ if (!plan) return [];
176
+ const hasQuality = plan.quality_score !== null && plan.quality_score !== undefined;
177
+ const hasChecked = Boolean(plan.coherence_checked_at);
178
+ if (!hasQuality && !hasChecked) return [];
179
+
180
+ const out = ['## Plan health', ''];
181
+ if (hasQuality) {
182
+ out.push(`- Quality score: ${plan.quality_score}`);
183
+ if (plan.quality_rationale) {
184
+ out.push(`- Rationale: ${plan.quality_rationale}`);
185
+ }
186
+ }
187
+ out.push(`- Last coherence check: ${hasChecked ? plan.coherence_checked_at : 'never'}`);
188
+ if (!hasChecked) {
189
+ out.push('- Run `run_coherence_check` (MCP) before acting on stale plans.');
190
+ }
191
+ out.push('');
192
+ return out;
193
+ }
194
+
195
+ function renderCoherenceWarning(task) {
196
+ if (!task || !task.coherence_status) return [];
197
+ const status = task.coherence_status;
198
+ if (status === 'clean' || status === 'unchecked') return [];
199
+
200
+ const out = ['## Coherence warning', ''];
201
+ out.push(`- Status: ${status}`);
202
+ if (status === 'contradiction_detected') {
203
+ out.push('- Supporting knowledge contains contradictions. Run `check_contradictions` (MCP) and re-verify before acting.');
204
+ } else if (status === 'stale_beliefs') {
205
+ out.push('- Knowledge backing this task may be outdated. Run `recall_knowledge` (MCP) to refresh before deciding.');
206
+ }
207
+ out.push('');
208
+ return out;
209
+ }
210
+
211
+ function renderContradictions(nodeContext) {
212
+ const list = Array.isArray(nodeContext?.contradictions) ? nodeContext.contradictions : [];
213
+ if (!list.length) return [];
214
+
215
+ const out = ['## Detected contradictions', ''];
216
+ out.push(...renderBulletList(
217
+ list.slice(0, 5).map((c) => c.summary || c.content || c.message || JSON.stringify(c)),
218
+ ));
219
+ out.push('');
220
+ return out;
221
+ }
222
+
223
+ function renderCurrentTask(selection, plan, nodeContext, planContext) {
224
+ const lines = [];
225
+ lines.push(`# ${selection.nodeId && nodeContext?.node ? nodeContext.node.title : plan.title}`);
226
+ lines.push('');
227
+
228
+ lines.push('## Summary');
229
+ lines.push('');
230
+ lines.push(`- Plan: ${plan.title}`);
231
+ lines.push(`- Plan ID: ${plan.id}`);
232
+ if (selection.nodeId && nodeContext?.node) {
233
+ lines.push(`- Task ID: ${selection.nodeId}`);
234
+ lines.push(`- Status: ${nodeContext.node.status || 'unknown'}`);
235
+ if (nodeContext.node.task_mode && nodeContext.node.task_mode !== 'free') {
236
+ lines.push(`- Task mode: ${nodeContext.node.task_mode}`);
237
+ }
238
+ }
239
+ lines.push(`- Generated: ${new Date().toISOString()}`);
240
+ lines.push('');
241
+
242
+ lines.push(...renderPlanHealth(plan));
243
+
244
+ if (!selection.nodeId || !nodeContext?.node) {
245
+ lines.push('No node selected. Re-run with --node-id to materialize a task-specific current-task.md.');
246
+ lines.push('');
247
+ lines.push('Generated file. Safe to overwrite with `agent-planner-mcp context ...`.');
248
+ lines.push('');
249
+ return lines.join('\n');
250
+ }
251
+
252
+ const task = nodeContext.node;
253
+ const ancestry = Array.isArray(nodeContext.ancestry) ? nodeContext.ancestry : [];
254
+ const parentPhase = ancestry.find((item) => item.node_type === 'phase');
255
+ const acceptanceCriteria = extractAcceptanceCriteria(task.description || '');
256
+ const description = stripAcceptanceCriteria(task.description || '');
257
+ const knowledge = (nodeContext.knowledge || []).slice(0, 5).map((item) => item.content);
258
+ const phaseSummaries = (planContext?.phases || []).map((phase) => `${phase.title}: ${phase.completed_tasks}/${phase.total_tasks} complete`);
259
+
260
+ lines.push(...renderCoherenceWarning(task));
261
+ lines.push(...renderContradictions(nodeContext));
262
+
263
+ if (parentPhase) {
264
+ lines.push('## Placement');
265
+ lines.push('');
266
+ lines.push(`- Phase: ${parentPhase.title}`);
267
+ lines.push(`- Phase status: ${parentPhase.status || 'unknown'}`);
268
+ lines.push('');
269
+ }
270
+
271
+ if (description) {
272
+ lines.push('## Task');
273
+ lines.push('');
274
+ lines.push(description);
275
+ lines.push('');
276
+ }
277
+
278
+ if (task.context) {
279
+ lines.push('## Implementation context');
280
+ lines.push('');
281
+ lines.push(task.context);
282
+ lines.push('');
283
+ }
284
+
285
+ if (task.agent_instructions) {
286
+ lines.push('## Agent instructions');
287
+ lines.push('');
288
+ lines.push(task.agent_instructions);
289
+ lines.push('');
290
+ }
291
+
292
+ if (acceptanceCriteria.length) {
293
+ lines.push('## Acceptance criteria');
294
+ lines.push('');
295
+ lines.push(...renderBulletList(acceptanceCriteria));
296
+ lines.push('');
297
+ }
298
+
299
+ const goalsData = (nodeContext.goals || []).concat(
300
+ (planContext?.goals || []).filter(
301
+ (pg) => !(nodeContext.goals || []).some((ng) => ng.id === pg.id)
302
+ )
303
+ );
304
+ if (goalsData.length) {
305
+ lines.push('## Linked goals');
306
+ lines.push('');
307
+ lines.push(...goalsData.map((g) => `- ${g.title || g.name}${g.status ? ` [${g.status}]` : ''}`));
308
+ lines.push('');
309
+ }
310
+
311
+ if (knowledge.length) {
312
+ lines.push('## Relevant knowledge');
313
+ lines.push('');
314
+ lines.push(...renderBulletList(knowledge));
315
+ lines.push('');
316
+ }
317
+
318
+ if (phaseSummaries.length) {
319
+ lines.push('## Plan progress snapshot');
320
+ lines.push('');
321
+ lines.push(...renderBulletList(phaseSummaries));
322
+ lines.push('');
323
+ }
324
+
325
+ lines.push('## Suggested loop');
326
+ lines.push('');
327
+ lines.push('- Run `agent-planner-mcp start` when you begin active work (claims the task for 30 min).');
328
+ lines.push('- Do the implementation work in the repo, not in `.agentplanner/`.');
329
+ lines.push('- If blocked, run `agent-planner-mcp blocked --message "why"` (releases the claim).');
330
+ lines.push('- When complete, run `agent-planner-mcp done --message "what changed"` (logs progress and writes a learning to the temporal graph).');
331
+ lines.push('- Refresh with `agent-planner-mcp context --plan-id ... --node-id ...` when you need updated context.');
332
+ lines.push('');
333
+ lines.push('## Source of truth');
334
+ lines.push('');
335
+ lines.push('- AgentPlanner (the API) is the source of truth for this plan and task.');
336
+ lines.push('- Files under `.agentplanner/` are a regeneratable cache produced by `agent-planner-mcp context`.');
337
+ lines.push('- Do not hand-edit `.agentplanner/` files; changes here are not synced back. Use the writeback commands above.');
338
+ lines.push('- Safe to delete `.agentplanner/` at any time — re-run `context` or `next` to repopulate.');
339
+ lines.push('');
340
+ return lines.join('\n');
341
+ }
342
+
343
+ async function materializeContext(options = {}) {
344
+ const baseDir = path.resolve(options.dir || process.cwd());
345
+ const selection = resolveSelection(options, baseDir);
346
+ if (!selection.planId) {
347
+ throw new Error('Missing plan id. Pass --plan-id or run context after generating workspace state.');
348
+ }
349
+
350
+ const { apiUrl, token } = resolveApiConfig(options);
351
+ if (!token) {
352
+ throw new Error(`Not logged in. Run \`agent-planner-mcp login\` first. Config path: ${getConfigPath()}`);
353
+ }
354
+
355
+ const api = createApiClient(token, { apiUrl });
356
+ const stateDir = getWorkspaceStatePath(baseDir);
357
+ ensureDir(stateDir);
358
+
359
+ const [plan, nodes, planContext, nodeContext] = await Promise.all([
360
+ api.plans.getPlan(selection.planId),
361
+ api.nodes.getNodes(selection.planId),
362
+ api.context.getPlanContext(selection.planId),
363
+ selection.nodeId ? api.context.getNodeContext(selection.nodeId) : Promise.resolve(null),
364
+ ]);
365
+
366
+ const payload = {
367
+ generatedAt: new Date().toISOString(),
368
+ apiUrl,
369
+ selection,
370
+ plan,
371
+ planContext,
372
+ nodeContext,
373
+ };
374
+
375
+ fs.writeFileSync(path.join(stateDir, 'context.json'), stringifyJson(payload));
376
+ fs.writeFileSync(path.join(stateDir, 'plan-tree.md'), renderPlanTree(nodes));
377
+ fs.writeFileSync(path.join(stateDir, 'current-task.md'), renderCurrentTask(selection, plan, nodeContext, planContext));
378
+
379
+ return {
380
+ stateDir,
381
+ selection,
382
+ };
383
+ }
384
+
385
+ async function tryClaim(api, planId, nodeId, options = {}) {
386
+ if (typeof api.nodes?.claimTask !== 'function') return false;
387
+ try {
388
+ await api.nodes.claimTask(
389
+ planId,
390
+ nodeId,
391
+ options.agentId || DEFAULT_AGENT_ID,
392
+ Number(options.ttl) || DEFAULT_CLAIM_TTL_MIN,
393
+ );
394
+ return true;
395
+ } catch (_err) {
396
+ return false;
397
+ }
398
+ }
399
+
400
+ async function tryRelease(api, planId, nodeId, options = {}) {
401
+ if (typeof api.nodes?.releaseTask !== 'function') return false;
402
+ try {
403
+ await api.nodes.releaseTask(planId, nodeId, options.agentId || DEFAULT_AGENT_ID);
404
+ return true;
405
+ } catch (_err) {
406
+ return false;
407
+ }
408
+ }
409
+
410
+ async function tryRecordLearning(api, { planId, nodeId, taskTitle, message }) {
411
+ if (!message || typeof api.graphiti?.addEpisode !== 'function') return false;
412
+ try {
413
+ await api.graphiti.addEpisode({
414
+ content: message,
415
+ name: taskTitle ? `[done] ${taskTitle}` : `[done] ${nodeId}`,
416
+ plan_id: planId,
417
+ node_id: nodeId,
418
+ metadata: { entry_type: 'learning', source: DEFAULT_AGENT_ID },
419
+ });
420
+ return true;
421
+ } catch (_err) {
422
+ return false;
423
+ }
424
+ }
425
+
426
+ async function updateStatus(command, options = {}) {
427
+ const statusMap = {
428
+ start: 'in_progress',
429
+ blocked: 'blocked',
430
+ done: 'completed',
431
+ };
432
+ const logTypeMap = {
433
+ blocked: 'challenge',
434
+ done: 'progress',
435
+ };
436
+
437
+ const baseDir = path.resolve(options.dir || process.cwd());
438
+ const selection = resolveSelection(options, baseDir);
439
+ if (!selection.planId || !selection.nodeId) {
440
+ throw new Error('Missing plan/node selection. Pass --plan-id and --node-id, or run context with both first.');
441
+ }
442
+
443
+ const { apiUrl, token } = resolveApiConfig(options);
444
+ if (!token) {
445
+ throw new Error(`Not logged in. Run \`agent-planner-mcp login\` first. Config path: ${getConfigPath()}`);
446
+ }
447
+
448
+ const api = createApiClient(token, { apiUrl });
449
+ await api.nodes.updateNodeStatus(selection.planId, selection.nodeId, statusMap[command]);
450
+
451
+ let logged = false;
452
+ if (options.message && logTypeMap[command]) {
453
+ await api.logs.addLogEntry(selection.planId, selection.nodeId, {
454
+ content: options.message,
455
+ log_type: logTypeMap[command],
456
+ });
457
+ logged = true;
458
+ }
459
+
460
+ let claimed = false;
461
+ let released = false;
462
+ let learned = false;
463
+
464
+ if (command === 'start') {
465
+ claimed = await tryClaim(api, selection.planId, selection.nodeId, options);
466
+ } else if (command === 'blocked' || command === 'done') {
467
+ released = await tryRelease(api, selection.planId, selection.nodeId, options);
468
+ }
469
+
470
+ if (command === 'done' && options.message) {
471
+ const workspaceContext = readWorkspaceContext(baseDir);
472
+ const taskTitle = workspaceContext?.nodeContext?.node?.title;
473
+ learned = await tryRecordLearning(api, {
474
+ planId: selection.planId,
475
+ nodeId: selection.nodeId,
476
+ taskTitle,
477
+ message: options.message,
478
+ });
479
+ }
480
+
481
+ return {
482
+ planId: selection.planId,
483
+ nodeId: selection.nodeId,
484
+ status: statusMap[command],
485
+ logged,
486
+ claimed,
487
+ released,
488
+ learned,
489
+ };
490
+ }
491
+
492
+ async function getMyTasks(options = {}) {
493
+ const { apiUrl, token } = resolveApiConfig(options);
494
+ if (!token) {
495
+ throw new Error(`Not logged in. Run \`agent-planner-mcp login\` first. Config path: ${getConfigPath()}`);
496
+ }
497
+
498
+ const config = readConfig();
499
+ const planId = options.planId || config.defaultPlanId || null;
500
+ const api = createApiClient(token, { apiUrl });
501
+ const fetchOptions = {};
502
+ if (planId) fetchOptions.plan_id = planId;
503
+
504
+ const tasks = await api.users.getMyTasks(fetchOptions);
505
+ if (!planId) {
506
+ return { tasks, planId };
507
+ }
508
+
509
+ const taskList = Array.isArray(tasks) ? tasks : tasks?.tasks;
510
+ if (!Array.isArray(taskList)) {
511
+ return { tasks, planId };
512
+ }
513
+
514
+ const filteredTasks = taskList.filter((task) => task.plan_id === planId);
515
+ if (Array.isArray(tasks)) {
516
+ return { tasks: filteredTasks, planId };
517
+ }
518
+
519
+ return {
520
+ tasks: {
521
+ ...tasks,
522
+ tasks: filteredTasks,
523
+ },
524
+ planId,
525
+ };
526
+ }
527
+
528
+ function normalizeSuggestion(suggestion, planId) {
529
+ if (!suggestion) return null;
530
+ const id = suggestion.id || suggestion.node_id;
531
+ if (!id) return null;
532
+ return {
533
+ id,
534
+ title: suggestion.title,
535
+ status: suggestion.status,
536
+ plan_id: suggestion.plan_id || planId,
537
+ task_mode: suggestion.task_mode,
538
+ knowledge_ready: suggestion.knowledge_ready,
539
+ ...suggestion,
540
+ };
541
+ }
542
+
543
+ async function pickViaSuggestNextTasks(api, planId, limit) {
544
+ if (!planId || typeof api.nodes?.suggestNextTasks !== 'function') return null;
545
+ try {
546
+ const suggestions = await api.nodes.suggestNextTasks(planId, limit);
547
+ const list = Array.isArray(suggestions)
548
+ ? suggestions
549
+ : (suggestions?.suggestions || suggestions?.tasks || []);
550
+ if (!list.length) return null;
551
+ return normalizeSuggestion(list[0], planId);
552
+ } catch (_err) {
553
+ return null;
554
+ }
555
+ }
556
+
557
+ async function pickFromQueue(options, status) {
558
+ try {
559
+ const { tasks, planId: queuePlanId } = await getMyTasks(options);
560
+ const list = Array.isArray(tasks) ? tasks : tasks?.tasks || [];
561
+ const match = list.find((t) => t.status === status);
562
+ if (!match) return null;
563
+ if (!match.plan_id && queuePlanId) match.plan_id = queuePlanId;
564
+ return match;
565
+ } catch (_err) {
566
+ return null;
567
+ }
568
+ }
569
+
570
+ async function getNextTask(options = {}) {
571
+ const { apiUrl, token } = resolveApiConfig(options);
572
+ if (!token) {
573
+ throw new Error(`Not logged in. Run \`agent-planner-mcp login\` first. Config path: ${getConfigPath()}`);
574
+ }
575
+
576
+ const config = readConfig();
577
+ const planId = options.planId || config.defaultPlanId || null;
578
+ const api = createApiClient(token, { apiUrl });
579
+ const fresh = Boolean(options.fresh);
580
+
581
+ // Resolution order:
582
+ // 1. Resume any in_progress task in scope (unless --fresh).
583
+ // 2. Dependency-aware recommendation via suggest_next_tasks.
584
+ // 3. Fallback: first not_started task in the my-tasks queue.
585
+ //
586
+ // `tasks` is the queue view; `next` is the smart picker. `next --fresh`
587
+ // forces a fresh recommendation even when active work exists.
588
+
589
+ let chosen = null;
590
+ let source = null;
591
+
592
+ if (!fresh) {
593
+ chosen = await pickFromQueue(options, 'in_progress');
594
+ if (chosen) source = 'resume_in_progress';
595
+ }
596
+
597
+ if (!chosen) {
598
+ chosen = await pickViaSuggestNextTasks(api, planId, options.limit ? Number(options.limit) : 5);
599
+ if (chosen) source = 'suggest_next_tasks';
600
+ }
601
+
602
+ if (!chosen) {
603
+ chosen = await pickFromQueue(options, 'not_started');
604
+ if (chosen) source = 'my_tasks_fallback';
605
+ }
606
+
607
+ if (!chosen) {
608
+ throw new Error('No actionable tasks (in_progress or not_started) found.');
609
+ }
610
+
611
+ const taskPlanId = chosen.plan_id || planId;
612
+ if (!taskPlanId) {
613
+ throw new Error('Could not determine plan_id for the selected task. Pass --plan-id explicitly.');
614
+ }
615
+
616
+ const claimed = await tryClaim(api, taskPlanId, chosen.id, options);
617
+
618
+ const contextResult = await materializeContext({
619
+ ...options,
620
+ planId: taskPlanId,
621
+ nodeId: chosen.id,
622
+ });
623
+
624
+ return {
625
+ task: chosen,
626
+ planId: taskPlanId,
627
+ stateDir: contextResult.stateDir,
628
+ claimed,
629
+ source,
630
+ };
631
+ }
632
+
633
+ module.exports = {
634
+ getMyTasks,
635
+ getNextTask,
636
+ getWorkspaceContextPath,
637
+ getWorkspaceStatePath,
638
+ login,
639
+ materializeContext,
640
+ parseArgs,
641
+ readWorkspaceContext,
642
+ renderCurrentTask,
643
+ renderPlanTree,
644
+ resolveSelection,
645
+ updateStatus,
646
+ };
package/src/cli.js CHANGED
@@ -1,39 +1,57 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CLI Entry Point for agent-planner-mcp
5
- * Routes to different commands or starts the MCP server
4
+ * CLI entry point for agent-planner-mcp.
5
+ * Preserves the existing MCP server/setup commands while adding
6
+ * a thin local-client loop for login/context/status writeback.
6
7
  */
7
8
 
8
- const path = require('path');
9
+ const { getMyTasks, getNextTask, materializeContext, login, parseArgs, updateStatus } = require('./cli/local-client');
9
10
 
10
11
  const args = process.argv.slice(2);
11
12
  const command = args[0];
13
+ const { options } = parseArgs(args.slice(1));
12
14
 
13
- // Route to different commands
14
- switch (command) {
15
- case 'setup-claude-code':
16
- // Run the setup-claude-code script
17
- const setupClaudeCode = require('./setup-claude-code.js');
18
- setupClaudeCode.main();
19
- break;
20
-
21
- case 'setup':
22
- // Run the interactive setup wizard
23
- require('./setup.js');
24
- break;
25
-
26
- case '--help':
27
- case '-h':
28
- case 'help':
29
- console.log(`
30
- Agent Planner MCP - Model Context Protocol Server
15
+ function printHelp() {
16
+ console.log(`
17
+ Agent Planner MCP - MCP server + thin local client
31
18
 
32
19
  Usage:
33
- npx agent-planner-mcp Start MCP server (requires USER_API_TOKEN)
34
- npx agent-planner-mcp setup-claude-code Install orchestration commands to .claude/
35
- npx agent-planner-mcp setup Interactive setup wizard
36
- npx agent-planner-mcp --help Show this help message
20
+ npx agent-planner-mcp Start MCP server (requires USER_API_TOKEN)
21
+ npx agent-planner-mcp setup-claude-code Install orchestration commands to .claude/
22
+ npx agent-planner-mcp setup Interactive setup wizard
23
+ npx agent-planner-mcp login --token <token> [--api-url <url>] [--plan-id <id>]
24
+ npx agent-planner-mcp tasks [--plan-id <id>]
25
+ npx agent-planner-mcp next [--plan-id <id>] [--fresh]
26
+ npx agent-planner-mcp context --plan-id <id> [--node-id <id>] [--dir <path>]
27
+ npx agent-planner-mcp start [--plan-id <id>] [--node-id <id>]
28
+ npx agent-planner-mcp blocked [--plan-id <id>] [--node-id <id>] [--message "..."]
29
+ npx agent-planner-mcp done [--plan-id <id>] [--node-id <id>] [--message "..."]
30
+ npx agent-planner-mcp --help
31
+
32
+ Commands:
33
+ login Authenticate and store credentials. If --plan-id is passed it is
34
+ saved as the default plan. If exactly one plan is accessible, it is
35
+ auto-selected as the default.
36
+ tasks Queue view: list tasks assigned to you (uses /users/my-tasks).
37
+ Filters by --plan-id or falls back to the stored default plan.
38
+ next Smart picker. Resolution order: (1) resume any in_progress task
39
+ in scope, (2) dependency-aware recommendation via suggest_next_tasks,
40
+ (3) fall back to first not_started task in your queue. Claims the
41
+ picked task for 30 minutes and materializes its context files.
42
+ Pass --fresh to skip step 1 and force a fresh recommendation
43
+ even when active work exists.
44
+ context Pull context for a specific plan/node and write .agentplanner/ files
45
+ (a regeneratable cache; AgentPlanner remains the source of truth).
46
+ Surfaces plan health (quality, coherence) and any contradictions
47
+ detected on the task. --node-id can be used alone when a default
48
+ plan is set.
49
+ start Mark the current task as in_progress and claim it (30-minute TTL).
50
+ blocked Mark the current task as blocked and release the claim. Optional
51
+ --message is logged as a challenge entry.
52
+ done Mark the current task as completed and release the claim. Optional
53
+ --message is logged as progress AND written to the temporal
54
+ knowledge graph as a learning episode.
37
55
 
38
56
  Environment Variables:
39
57
  API_URL - Agent Planner API URL (default: http://localhost:3000)
@@ -44,21 +62,104 @@ Environment Variables:
44
62
  Documentation:
45
63
  https://github.com/talkingagents/agent-planner-mcp
46
64
  `);
47
- break;
48
-
49
- case '--version':
50
- case '-v':
51
- const pkg = require('../package.json');
52
- console.log(`agent-planner-mcp v${pkg.version}`);
53
- break;
54
-
55
- default:
56
- // No command or unknown command - start MCP server
57
- if (command && !command.startsWith('-')) {
58
- console.error(`Unknown command: ${command}`);
59
- console.error('Run "npx agent-planner-mcp --help" for usage information.');
60
- process.exit(1);
65
+ }
66
+
67
+ async function main() {
68
+ switch (command) {
69
+ case 'setup-claude-code': {
70
+ const setupClaudeCode = require('./setup-claude-code.js');
71
+ setupClaudeCode.main();
72
+ return;
73
+ }
74
+
75
+ case 'setup':
76
+ require('./setup.js');
77
+ return;
78
+
79
+ case 'login': {
80
+ const result = await login(options);
81
+ console.log(`Saved credentials to ${result.configPath}`);
82
+ console.log(`API URL: ${result.apiUrl}`);
83
+ if (result.defaultPlanId) {
84
+ console.log(`Default plan: ${result.defaultPlanId}`);
85
+ }
86
+ return;
87
+ }
88
+
89
+ case 'tasks': {
90
+ const result = await getMyTasks(options);
91
+ const taskList = Array.isArray(result.tasks) ? result.tasks : result.tasks?.tasks || [];
92
+ if (result.planId) {
93
+ console.log(`Tasks for plan ${result.planId}:`);
94
+ } else {
95
+ console.log('All tasks:');
96
+ }
97
+ if (!taskList.length) {
98
+ console.log(' (no tasks)');
99
+ return;
100
+ }
101
+ for (const t of taskList) {
102
+ const plan = t.plan_id && t.plan_id !== result.planId ? ` (plan: ${t.plan_id})` : '';
103
+ console.log(` [${t.status || '?'}] ${t.title || t.id}${plan}`);
104
+ }
105
+ return;
106
+ }
107
+
108
+ case 'next': {
109
+ const result = await getNextTask(options);
110
+ console.log(`Selected task: ${result.task.title || result.task.id} [${result.task.status}]`);
111
+ console.log(`Plan: ${result.planId}`);
112
+ console.log(`Source: ${result.source}`);
113
+ if (result.claimed) console.log('Claimed task for 30 minutes.');
114
+ console.log(`Context written to ${result.stateDir}`);
115
+ return;
61
116
  }
62
- // Start the MCP server
63
- require('./index.js');
117
+
118
+ case 'context': {
119
+ const result = await materializeContext(options);
120
+ console.log(`Wrote generated context files to ${result.stateDir}`);
121
+ if (result.selection.nodeId) {
122
+ console.log(`Selected node: ${result.selection.nodeId}`);
123
+ }
124
+ return;
125
+ }
126
+
127
+ case 'start':
128
+ case 'blocked':
129
+ case 'done': {
130
+ const result = await updateStatus(command, options);
131
+ console.log(`Updated ${result.nodeId} to ${result.status}`);
132
+ if (result.logged) console.log('Added log entry.');
133
+ if (result.claimed) console.log('Claimed task.');
134
+ if (result.released) console.log('Released task claim.');
135
+ if (result.learned) console.log('Recorded learning to temporal knowledge graph.');
136
+ return;
137
+ }
138
+
139
+ case '--help':
140
+ case '-h':
141
+ case 'help':
142
+ printHelp();
143
+ return;
144
+
145
+ case '--version':
146
+ case '-v': {
147
+ const pkg = require('../package.json');
148
+ console.log(`agent-planner-mcp v${pkg.version}`);
149
+ return;
150
+ }
151
+
152
+ default:
153
+ if (command && !command.startsWith('-')) {
154
+ console.error(`Unknown command: ${command}`);
155
+ console.error('Run "npx agent-planner-mcp --help" for usage information.');
156
+ process.exit(1);
157
+ }
158
+ require('./index.js');
159
+ }
64
160
  }
161
+
162
+ main().catch((error) => {
163
+ console.error(error.message || error);
164
+ process.exit(1);
165
+ });
package/src/tools.js CHANGED
@@ -178,6 +178,41 @@ function setupTools(server, apiClientOverride) {
178
178
  }
179
179
  },
180
180
 
181
+ // ========================================
182
+ // COHERENCE - Check alignment across goals/plans/knowledge
183
+ // ========================================
184
+ {
185
+ name: "check_coherence_pending",
186
+ description: "Check what needs coherence review. Returns stale plans and goals that have changed since their last coherence check. Call this at the start of a maintenance cycle to discover what needs attention. Uses timestamp comparison (updated_at vs coherence_checked_at) — no expensive processing.",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: {}
190
+ }
191
+ },
192
+ {
193
+ name: "run_coherence_check",
194
+ description: "Run a coherence check on a specific plan. Evaluates quality (coverage, specificity, ordering, knowledge completeness), flags contradictions, and stamps the plan as checked. Returns quality breakdown with sub-scores and rationale.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ plan_id: { type: "string", description: "Plan ID to check" },
199
+ goal_id: { type: "string", description: "Optional goal ID to evaluate coverage against" }
200
+ },
201
+ required: ["plan_id"]
202
+ }
203
+ },
204
+ {
205
+ name: "assess_goal_quality",
206
+ description: "Assess how well-defined a goal is. Evaluates 5 dimensions: clarity (title+description), measurability (success criteria), actionability (linked plans), knowledge grounding (related facts), and commitment (desire vs intention, deadline). Returns score, dimension breakdown, and specific improvement suggestions. Use this when helping users define or refine goals.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ goal_id: { type: "string", description: "Goal ID to assess" }
211
+ },
212
+ required: ["goal_id"]
213
+ }
214
+ },
215
+
181
216
  // ========================================
182
217
  // CONTEXT LOADING - Get everything you need
183
218
  // Use before starting work on a plan/goal
@@ -2233,12 +2268,14 @@ function setupTools(server, apiClientOverride) {
2233
2268
  "Knowledge - Persistent storage for decisions, context, constraints, and learnings"
2234
2269
  ],
2235
2270
  recommended_workflow: [
2236
- "1. Check list_goals to understand current objectives",
2237
- "2. Use list_plans to see existing plans",
2238
- "3. Before working on a plan, use get_plan_context to get the full picture",
2239
- "4. Update task statuses as you work (update_node with status)",
2240
- "5. Store important decisions and learnings using add_learning",
2241
- "6. Check recall_knowledge before making decisions to see past context"
2271
+ "1. PREFLIGHT: check_coherence_pending to see if any plans/goals need alignment review",
2272
+ " If stale items found, run_coherence_check on each before starting task work",
2273
+ "2. Check list_goals to understand current objectives",
2274
+ "3. Use list_plans to see existing plans",
2275
+ "4. Before working on a plan, use get_plan_context to get the full picture",
2276
+ "5. Update task statuses as you work (update_node with status)",
2277
+ "6. Store important decisions and learnings using add_learning",
2278
+ "7. Check recall_knowledge before making decisions to see past context"
2242
2279
  ],
2243
2280
  quick_tips: [
2244
2281
  "Always capture WHY decisions were made, not just WHAT",
@@ -2375,6 +2412,43 @@ function setupTools(server, apiClientOverride) {
2375
2412
  });
2376
2413
  }
2377
2414
 
2415
+ // ===== COHERENCE =====
2416
+ if (name === "check_coherence_pending") {
2417
+ const result = await apiClient.coherence.getPending();
2418
+ const totalStale = (result.stale_plans?.length || 0) + (result.stale_goals?.length || 0);
2419
+ return formatResponse({
2420
+ ...result,
2421
+ tip: totalStale > 0
2422
+ ? "Review stale items. For each stale plan, call run_coherence_check to evaluate quality and stamp as checked."
2423
+ : "Everything is up to date. No coherence review needed."
2424
+ });
2425
+ }
2426
+
2427
+ if (name === "assess_goal_quality") {
2428
+ const { goal_id } = args;
2429
+ const result = await apiClient.goals.getQuality(goal_id);
2430
+ const lowDims = Object.entries(result.dimensions || {})
2431
+ .filter(([, v]) => v.score < 0.5)
2432
+ .map(([k]) => k);
2433
+ return formatResponse({
2434
+ ...result,
2435
+ tip: result.suggestions?.length > 0
2436
+ ? `Goal needs improvement in: ${lowDims.join(', ') || 'minor areas'}. Follow the suggestions to strengthen it.`
2437
+ : 'Goal is well-defined. Ready for agent execution.'
2438
+ });
2439
+ }
2440
+
2441
+ if (name === "run_coherence_check") {
2442
+ const { plan_id, goal_id } = args;
2443
+ const result = await apiClient.coherence.runCheck(plan_id, goal_id);
2444
+ return formatResponse({
2445
+ ...result,
2446
+ tip: result.coherence_issues_count > 0
2447
+ ? `${result.coherence_issues_count} coherence issues found. Review tasks with stale_beliefs or contradiction_detected status.`
2448
+ : "Plan is coherent. Quality score and checked_at timestamp updated."
2449
+ });
2450
+ }
2451
+
2378
2452
  // Tool not found
2379
2453
  throw new Error(`Unknown tool: ${name}`);
2380
2454
  } catch (error) {