agent-planner-mcp 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/package.json +3 -1
- package/src/api-client.js +32 -2
- package/src/cli/config.js +64 -0
- package/src/cli/local-client.js +646 -0
- package/src/cli.js +142 -41
package/README.md
CHANGED
|
@@ -13,6 +13,117 @@ MCP server for [AgentPlanner](https://agentplanner.io) — AI agent orchestratio
|
|
|
13
13
|
|
|
14
14
|
## Setup
|
|
15
15
|
|
|
16
|
+
### Claude Desktop — one-click install (`.mcpb`)
|
|
17
|
+
|
|
18
|
+
The fastest path. Download `agent-planner.mcpb` from the [latest release](https://github.com/TAgents/agent-planner-mcp/releases), double-click it, and Claude Desktop will install the extension and prompt for your AgentPlanner API token. No Node.js setup, no JSON editing.
|
|
19
|
+
|
|
20
|
+
To build the bundle yourself:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run build:mcpb # produces agent-planner.mcpb
|
|
24
|
+
npm run validate:mcpb # schema-check manifest.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Manual config (Claude Desktop, Claude Code, Cursor, etc.)
|
|
28
|
+
|
|
29
|
+
Add to your MCP client config (`claude_desktop_config.json`, `.cursor/mcp.json`, etc.):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"agentplanner": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "agent-planner-mcp"],
|
|
37
|
+
"env": {
|
|
38
|
+
"API_URL": "https://agentplanner.io/api",
|
|
39
|
+
"USER_API_TOKEN": "your_token_here"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Thin local client (v1)
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
### Mental model
|
|
51
|
+
|
|
52
|
+
- AgentPlanner (the API) is the source of truth.
|
|
53
|
+
- `.agentplanner/` files are a regeneratable cache, written by the CLI for the agent to read.
|
|
54
|
+
- The agent works in the real repo. Status changes flow back via explicit writeback commands. There is no live sync.
|
|
55
|
+
|
|
56
|
+
> **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.
|
|
57
|
+
|
|
58
|
+
### The loop
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 1. Login — saves credentials and auto-selects a default plan
|
|
62
|
+
# (pass --plan-id to pick one, or it auto-selects if you have exactly one plan)
|
|
63
|
+
npx agent-planner-mcp login --token <token> --api-url https://agentplanner.io/api [--plan-id <id>]
|
|
64
|
+
# Localhost variant (after `docker compose -f docker-compose.local.yml up`):
|
|
65
|
+
npx agent-planner-mcp login --token <token> --api-url http://localhost:3000
|
|
66
|
+
|
|
67
|
+
# 2. See your task queue
|
|
68
|
+
npx agent-planner-mcp tasks [--plan-id <id>]
|
|
69
|
+
|
|
70
|
+
# 3. Pick the next task and pull context (claims it for 30 minutes)
|
|
71
|
+
npx agent-planner-mcp next [--plan-id <id>]
|
|
72
|
+
# Force a fresh recommendation even if you have active work:
|
|
73
|
+
npx agent-planner-mcp next --fresh
|
|
74
|
+
|
|
75
|
+
# 4. Or pull context for a specific plan/node (no claim, no status change)
|
|
76
|
+
npx agent-planner-mcp context --plan-id <plan-id> --node-id <node-id>
|
|
77
|
+
# If a default plan is set, --plan-id can be omitted:
|
|
78
|
+
npx agent-planner-mcp context --node-id <node-id>
|
|
79
|
+
|
|
80
|
+
# 5. Explicit writeback. No live sync.
|
|
81
|
+
npx agent-planner-mcp start # claim + mark in_progress
|
|
82
|
+
npx agent-planner-mcp blocked --message "Waiting on API decision"
|
|
83
|
+
npx agent-planner-mcp done --message "Implemented and verified"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `next` resolution order
|
|
87
|
+
|
|
88
|
+
`next` is a smart picker. It resolves in this order:
|
|
89
|
+
|
|
90
|
+
1. **Resume** — if any task in scope is `in_progress`, pick it. (Source: `resume_in_progress`.)
|
|
91
|
+
2. **Recommend** — call `suggest_next_tasks` (dependency- and RPI-aware) for a fresh pick. (Source: `suggest_next_tasks`.)
|
|
92
|
+
3. **Fallback** — first `not_started` task in your queue. (Source: `my_tasks_fallback`.)
|
|
93
|
+
|
|
94
|
+
`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.
|
|
95
|
+
|
|
96
|
+
### What `start`, `blocked`, `done` actually do
|
|
97
|
+
|
|
98
|
+
| Command | Status | Claim | Log entry | Learning written to Graphiti |
|
|
99
|
+
|---|---|---|---|---|
|
|
100
|
+
| `start` | `in_progress` | claim (30m TTL) | — | — |
|
|
101
|
+
| `blocked --message ...` | `blocked` | release | `challenge` | — |
|
|
102
|
+
| `done --message ...` | `completed` | release | `progress` | yes (entry_type: `learning`) |
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
### What `current-task.md` surfaces
|
|
107
|
+
|
|
108
|
+
Beyond title, description, agent_instructions, and acceptance criteria, the generated `current-task.md` includes BDI signals from the API responses already being fetched:
|
|
109
|
+
|
|
110
|
+
- **Plan health** — `quality_score`, rationale, `coherence_checked_at` (or "never")
|
|
111
|
+
- **Coherence warning** — flagged when `node.coherence_status` is `contradiction_detected` or `stale_beliefs`, with concrete next-step pointers (`check_contradictions`, `recall_knowledge`)
|
|
112
|
+
- **Detected contradictions** — listed when present in the node context
|
|
113
|
+
- **Task mode** — shown when not `free` (RPI awareness for `research`/`plan`/`implement`)
|
|
114
|
+
- **Linked goals**, **relevant knowledge** (top 5), **plan progress snapshot**
|
|
115
|
+
|
|
116
|
+
### When to use CLI vs MCP vs API skill
|
|
117
|
+
|
|
118
|
+
| You want… | Use |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Zero-setup local task context for any coding agent (Claude Code, OpenClaw, scripts) | **CLI** (this thin client) |
|
|
121
|
+
| Rich, structured tool access from inside an MCP-aware agent (Claude Desktop, Cursor, etc.) | **MCP** (run `npx agent-planner-mcp` as an MCP server) |
|
|
122
|
+
| Direct programmatic integration from your own service | **API** (REST endpoints; same routes the MCP and CLI use) |
|
|
123
|
+
|
|
124
|
+
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).
|
|
125
|
+
|
|
126
|
+
|
|
16
127
|
### Claude Desktop
|
|
17
128
|
|
|
18
129
|
Add to your `claude_desktop_config.json`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-planner-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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": {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"test:tools": "node test-tools.js",
|
|
16
16
|
"setup": "node src/setup.js",
|
|
17
17
|
"setup-claude-code": "node src/setup-claude-code.js",
|
|
18
|
+
"build:mcpb": "npm install --omit=dev --ignore-scripts && npx --yes @anthropic-ai/mcpb pack . agent-planner.mcpb",
|
|
19
|
+
"validate:mcpb": "npx --yes @anthropic-ai/mcpb validate manifest.json",
|
|
18
20
|
"prepublishOnly": "echo 'Ready to publish agent-planner-mcp'"
|
|
19
21
|
},
|
|
20
22
|
"keywords": [
|
package/src/api-client.js
CHANGED
|
@@ -727,6 +727,17 @@ const graphiti = {
|
|
|
727
727
|
}
|
|
728
728
|
};
|
|
729
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
|
+
|
|
730
741
|
// ─── Dependencies (cross-plan & external) ─────────────────────
|
|
731
742
|
const dependencies = {
|
|
732
743
|
/**
|
|
@@ -765,10 +776,10 @@ const dependencies = {
|
|
|
765
776
|
* @param {string} token - API token or JWT
|
|
766
777
|
* @returns {Object} - API client modules (plans, nodes, etc.)
|
|
767
778
|
*/
|
|
768
|
-
function createApiClient(token) {
|
|
779
|
+
function createApiClient(token, options = {}) {
|
|
769
780
|
const scheme = getAuthScheme(token);
|
|
770
781
|
const client = axios.create({
|
|
771
|
-
baseURL: process.env.API_URL || 'http://localhost:3000',
|
|
782
|
+
baseURL: options.apiUrl || process.env.API_URL || 'http://localhost:3000',
|
|
772
783
|
headers: {
|
|
773
784
|
'Content-Type': 'application/json',
|
|
774
785
|
'Authorization': token ? `${scheme} ${token}` : undefined
|
|
@@ -818,6 +829,10 @@ function createApiClient(token) {
|
|
|
818
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,
|
|
819
830
|
releaseTask: async (planId, nodeId, agentId = 'mcp-agent') => (await client.delete(`/plans/${planId}/nodes/${nodeId}/claim`, { data: { agent_id: agentId } })).data,
|
|
820
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
|
+
},
|
|
821
836
|
},
|
|
822
837
|
comments: {
|
|
823
838
|
getComments: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}/comments`)).data,
|
|
@@ -888,6 +903,12 @@ function createApiClient(token) {
|
|
|
888
903
|
removeAchiever: async (goalId, depId) => (await client.delete(`/goals/${goalId}/achievers/${depId}`)).data,
|
|
889
904
|
getKnowledgeGaps: async (goalId) => (await client.get(`/goals/${goalId}/knowledge-gaps`)).data,
|
|
890
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,
|
|
891
912
|
},
|
|
892
913
|
context: {
|
|
893
914
|
getNodeContext: async (nodeId, options = {}) => {
|
|
@@ -916,6 +937,14 @@ function createApiClient(token) {
|
|
|
916
937
|
listCrossPlan: async (planIds) => (await client.get('/dependencies/cross-plan', { params: { plan_ids: planIds.join(',') } })).data,
|
|
917
938
|
createExternal: async (data) => (await client.post('/dependencies/external', data)).data,
|
|
918
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
|
+
},
|
|
919
948
|
axiosInstance: client,
|
|
920
949
|
};
|
|
921
950
|
}
|
|
@@ -945,6 +974,7 @@ module.exports = {
|
|
|
945
974
|
graphiti,
|
|
946
975
|
dependencies,
|
|
947
976
|
coherence,
|
|
977
|
+
users,
|
|
948
978
|
axiosInstance, // Export for direct API calls
|
|
949
979
|
createApiClient // Factory for per-session clients (HTTP mode)
|
|
950
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
|
|
5
|
-
*
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
34
|
-
npx agent-planner-mcp setup-claude-code
|
|
35
|
-
npx agent-planner-mcp setup
|
|
36
|
-
npx agent-planner-mcp --
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
+
});
|