phewsh-mcp-server 0.2.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/README.md +161 -0
- package/package.json +45 -0
- package/src/index.js +1029 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# phewsh-mcp-server
|
|
2
|
+
|
|
3
|
+
**Your projects have intelligence. Now your agents can use it.**
|
|
4
|
+
|
|
5
|
+
PHEWSH MCP gives any AI agent — Claude Code, Cursor, custom bots — instant access to your project's vision, plan, constraints, and task queue. The agent starts a session, gets a structured task with verification criteria, executes it, reports back, and automatically gets the next one.
|
|
6
|
+
|
|
7
|
+
You shape the intent. Agents do the work. PHEWSH keeps the thread.
|
|
8
|
+
|
|
9
|
+
## The experience
|
|
10
|
+
|
|
11
|
+
**If you've never used an MCP server before:** this lets your AI assistant know what you're building, what matters, and what to do next — without you re-explaining it every time. Set it up once, and every conversation starts with full context.
|
|
12
|
+
|
|
13
|
+
**If you build with agents:** this is the coordination protocol. Structured dispatch packets (not chat prompts), smart task ordering, verification gates, blocker tracking, session history across agents, and auto-chaining. Your agents share a brain.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
### 1. Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g phewsh-mcp-server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or if you have the PHEWSH repo:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd mcp && npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Configure Claude Code
|
|
30
|
+
|
|
31
|
+
Add to `~/.claude/settings.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"phewsh": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "phewsh-mcp-server"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with a local install:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"phewsh": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["/path/to/phewsh/mcp/src/index.js"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Sync your projects
|
|
58
|
+
|
|
59
|
+
If you use the PHEWSH CLI:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
phewsh mcp sync
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If you use the web app at phewsh.com/intent, export your projects from the settings menu.
|
|
66
|
+
|
|
67
|
+
The server also automatically reads any `.intent/` directory in your current working directory.
|
|
68
|
+
|
|
69
|
+
### 4. Start working
|
|
70
|
+
|
|
71
|
+
Open Claude Code in your project and say:
|
|
72
|
+
|
|
73
|
+
> "Check phewsh for what I should work on next"
|
|
74
|
+
|
|
75
|
+
Claude will call `phewsh_start`, get your full project briefing and first task, and begin executing. When it finishes, it reports back and gets the next task automatically.
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
You shape intent PHEWSH coordinates Agents execute
|
|
81
|
+
(web app or CLI) --> (MCP server) --> (Claude, Cursor, etc.)
|
|
82
|
+
^ |
|
|
83
|
+
|______ results _____________|
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
1. You define what you're building in PHEWSH (vision, plan, constraints, tasks)
|
|
87
|
+
2. The MCP server exposes this as structured dispatch packets
|
|
88
|
+
3. Any agent connects, pulls a task, and gets: objective, constraints, verification criteria, rollback plan
|
|
89
|
+
4. Agent executes, reports result — server auto-chains to the next task
|
|
90
|
+
5. Session history tracks what happened across agents and sessions
|
|
91
|
+
|
|
92
|
+
The dispatch packet is NOT a chat prompt. It's an execution contract:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Objective "Deploy the landing page to production"
|
|
96
|
+
Success "Live and accessible at target URL"
|
|
97
|
+
Constraints Budget: $0 | Urgency: urgent | Autonomy: guided
|
|
98
|
+
Verification [ ] Accessible at URL [ ] No error responses
|
|
99
|
+
Rollback Reversible — redeploy previous version
|
|
100
|
+
After Report back, then: "Set up analytics tracking"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Tools
|
|
104
|
+
|
|
105
|
+
| Tool | Purpose |
|
|
106
|
+
|------|---------|
|
|
107
|
+
| `phewsh_start` | **Start here.** Full project briefing + first task in one call. |
|
|
108
|
+
| `phewsh_next_task` | Get next highest-priority task as a dispatch packet. |
|
|
109
|
+
| `phewsh_complete_task` | Report results. Auto-chains to the next task. |
|
|
110
|
+
| `phewsh_check_verification` | Confirm what needs to be true before marking done. |
|
|
111
|
+
| `phewsh_flag_blocker` | Flag a blocked task. Suggests alternative work. |
|
|
112
|
+
| `phewsh_list_projects` | List all projects with progress and constraints. |
|
|
113
|
+
| `phewsh_get_context` | Deep project briefing — vision, plan, gate, state. |
|
|
114
|
+
| `phewsh_session_history` | What agents have done — completions, blockers, sessions. |
|
|
115
|
+
|
|
116
|
+
## Prompts
|
|
117
|
+
|
|
118
|
+
Pre-built workflows agents can invoke:
|
|
119
|
+
|
|
120
|
+
| Prompt | What it does |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| `work_session` | Full autonomous session: get briefed, execute up to N tasks, report results. |
|
|
123
|
+
| `status_check` | Quick status: what's done, what's pending, any blockers. No execution. |
|
|
124
|
+
| `review_results` | Human oversight: review what agents did, what failed, what needs attention. |
|
|
125
|
+
|
|
126
|
+
## Resources
|
|
127
|
+
|
|
128
|
+
Project artifacts are readable as MCP resources:
|
|
129
|
+
|
|
130
|
+
- `phewsh://projects/{id}/briefing` — full project context
|
|
131
|
+
- `phewsh://projects/{id}/artifacts/vision` — vision artifact
|
|
132
|
+
- `phewsh://projects/{id}/artifacts/plan` — plan artifact
|
|
133
|
+
|
|
134
|
+
## What makes this different
|
|
135
|
+
|
|
136
|
+
**Smart ordering.** Tasks aren't first-in-first-out. Priority considers urgency, execution type, category freshness, and age. Agents get the highest-impact work first.
|
|
137
|
+
|
|
138
|
+
**Auto-chaining.** Complete a task, get the next one in the response. No extra calls. Agents can run through an entire work queue in a single session.
|
|
139
|
+
|
|
140
|
+
**Blocker awareness.** Can't do a task? Flag it and get an alternative. The human gets notified. No one wastes time.
|
|
141
|
+
|
|
142
|
+
**Agent identity.** Every session start, completion, and blocker is tagged with who did it. Multiple agents work on the same project without confusion.
|
|
143
|
+
|
|
144
|
+
**Verification gates.** Every task has concrete "done when" criteria. Agents know exactly what to prove before reporting complete.
|
|
145
|
+
|
|
146
|
+
**Constraint-aware.** Budget, time, skill level, urgency, autonomy — all flow into dispatch packets. An agent won't suggest a $500 service on a $0 budget.
|
|
147
|
+
|
|
148
|
+
## Project sources
|
|
149
|
+
|
|
150
|
+
The server loads projects from:
|
|
151
|
+
|
|
152
|
+
1. **`.intent/` in cwd** — automatically loaded as the "local" project
|
|
153
|
+
2. **`~/.phewsh/projects.json`** — synced via `phewsh mcp sync` or web app export
|
|
154
|
+
|
|
155
|
+
## Philosophy
|
|
156
|
+
|
|
157
|
+
PHEWSH is the coordination layer, not another agent. It doesn't execute — it remembers, reasons about constraints, orders work, and verifies results. The agent does the work. PHEWSH makes sure it's the right work, done right.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
*Built by [PHEWSH](https://phewsh.com) — software that adapts to you.*
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "phewsh-mcp-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Connect AI agents to your project intelligence. One call to start. One call to finish. Everything in between is execution.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"phewsh-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node --watch src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"phewsh",
|
|
18
|
+
"intent",
|
|
19
|
+
"dispatch",
|
|
20
|
+
"agent",
|
|
21
|
+
"coordination",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"cursor",
|
|
25
|
+
"project-management"
|
|
26
|
+
],
|
|
27
|
+
"author": "Phewsh <hello@phewsh.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/cleverIdeaz/phewsh",
|
|
32
|
+
"directory": "mcp"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://phewsh.com/intent",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"src/",
|
|
43
|
+
"README.md"
|
|
44
|
+
]
|
|
45
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PHEWSH MCP Server v0.2.0
|
|
5
|
+
*
|
|
6
|
+
* The coordination layer between humans and AI agents.
|
|
7
|
+
*
|
|
8
|
+
* Any MCP-capable agent connects here and immediately knows:
|
|
9
|
+
* - What project they're working on
|
|
10
|
+
* - What needs to happen next (structured, not vague)
|
|
11
|
+
* - What constraints to respect
|
|
12
|
+
* - How to prove they did it right
|
|
13
|
+
* - What to do after they finish
|
|
14
|
+
*
|
|
15
|
+
* One call to start. One call to finish. Everything in between is execution.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import {
|
|
21
|
+
CallToolRequestSchema,
|
|
22
|
+
ListToolsRequestSchema,
|
|
23
|
+
ListResourcesRequestSchema,
|
|
24
|
+
ReadResourceRequestSchema,
|
|
25
|
+
ListPromptsRequestSchema,
|
|
26
|
+
GetPromptRequestSchema,
|
|
27
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
28
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
|
|
29
|
+
import { join, resolve } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
|
|
32
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const PHEWSH_DIR = join(homedir(), ".phewsh");
|
|
35
|
+
const PROJECTS_FILE = join(PHEWSH_DIR, "projects.json");
|
|
36
|
+
const RESULTS_DIR = join(PHEWSH_DIR, "results");
|
|
37
|
+
const SESSIONS_DIR = join(PHEWSH_DIR, "sessions");
|
|
38
|
+
|
|
39
|
+
// Ensure directories exist
|
|
40
|
+
[PHEWSH_DIR, RESULTS_DIR, SESSIONS_DIR].forEach(dir => {
|
|
41
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── Project Loading ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function loadProjects() {
|
|
47
|
+
const projects = [];
|
|
48
|
+
|
|
49
|
+
// 1. Check for local .intent/ directory
|
|
50
|
+
const localIntent = join(process.cwd(), ".intent");
|
|
51
|
+
if (existsSync(localIntent)) {
|
|
52
|
+
const local = loadLocalIntentProject(localIntent);
|
|
53
|
+
if (local) projects.push(local);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Check for synced projects from web app
|
|
57
|
+
if (existsSync(PROJECTS_FILE)) {
|
|
58
|
+
try {
|
|
59
|
+
const data = JSON.parse(readFileSync(PROJECTS_FILE, "utf-8"));
|
|
60
|
+
if (Array.isArray(data)) {
|
|
61
|
+
projects.push(...data);
|
|
62
|
+
}
|
|
63
|
+
} catch { /* ignore parse errors */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return projects;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadLocalIntentProject(dir) {
|
|
70
|
+
const project = {
|
|
71
|
+
id: "local",
|
|
72
|
+
name: "Current Project",
|
|
73
|
+
source: "local",
|
|
74
|
+
artifacts: {},
|
|
75
|
+
actions: [],
|
|
76
|
+
decisionGate: null,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Read standard .intent/ files
|
|
80
|
+
const files = ["vision.md", "plan.md", "next.md", "status.md", "narrative.md"];
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const path = join(dir, file);
|
|
83
|
+
if (existsSync(path)) {
|
|
84
|
+
const kind = file.replace(".md", "");
|
|
85
|
+
project.artifacts[kind] = {
|
|
86
|
+
kind,
|
|
87
|
+
content: readFileSync(path, "utf-8"),
|
|
88
|
+
};
|
|
89
|
+
if (kind === "vision") {
|
|
90
|
+
const firstLine = project.artifacts[kind].content.split("\n").find(l => l.trim().length > 5);
|
|
91
|
+
if (firstLine) project.name = firstLine.replace(/^#+\s*/, "").trim().slice(0, 60);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Read project.json if it exists (has constraints, gate, actions, etc.)
|
|
97
|
+
const metaPath = join(dir, "project.json");
|
|
98
|
+
if (existsSync(metaPath)) {
|
|
99
|
+
try {
|
|
100
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
101
|
+
Object.assign(project, meta);
|
|
102
|
+
} catch { /* ignore */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.keys(project.artifacts).length > 0 ? project : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Smart Task Ordering ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
const URGENCY_WEIGHT = { critical: 4, urgent: 3, moderate: 2, relaxed: 1 };
|
|
111
|
+
const TYPE_PRIORITY = { agent: 3, ai: 2, human: 1 }; // agents should grab agent tasks first
|
|
112
|
+
|
|
113
|
+
function scoreTask(action, project) {
|
|
114
|
+
let score = 0;
|
|
115
|
+
const gate = project.decisionGate;
|
|
116
|
+
|
|
117
|
+
// Urgency from gate constraints
|
|
118
|
+
if (gate?.constraints?.urgency) {
|
|
119
|
+
score += (URGENCY_WEIGHT[gate.constraints.urgency] || 2) * 10;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Execution type match — prefer agent-executable tasks
|
|
123
|
+
const execType = action.executionType || classifyExecutionType(action.intent);
|
|
124
|
+
score += (TYPE_PRIORITY[execType] || 1) * 5;
|
|
125
|
+
|
|
126
|
+
// Category clustering — if previous completed actions share category, boost same-category
|
|
127
|
+
const completedCategories = (project.actions || [])
|
|
128
|
+
.filter(a => a.state === "reconciled" && a.reconciliation?.decision === "accept")
|
|
129
|
+
.slice(-3)
|
|
130
|
+
.map(a => a.category);
|
|
131
|
+
if (action.category && !completedCategories.includes(action.category)) {
|
|
132
|
+
score += 3; // Fresh category = higher priority (avoids tunnel vision)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Recency penalty — older tasks get slight boost
|
|
136
|
+
if (action.createdAt) {
|
|
137
|
+
const age = Date.now() - new Date(action.createdAt).getTime();
|
|
138
|
+
score += Math.min(5, Math.floor(age / (1000 * 60 * 60 * 24))); // +1 per day, max 5
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return score;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getOrderedTasks(project, filter = "any") {
|
|
145
|
+
const pending = (project.actions || []).filter(a => {
|
|
146
|
+
if (a.state !== "intended") return false;
|
|
147
|
+
if (filter === "any") return true;
|
|
148
|
+
const aType = a.executionType || classifyExecutionType(a.intent);
|
|
149
|
+
return aType === filter;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return pending
|
|
153
|
+
.map(a => ({ action: a, score: scoreTask(a, project) }))
|
|
154
|
+
.sort((a, b) => b.score - a.score)
|
|
155
|
+
.map(s => s.action);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Agent Session Tracking ─────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
function recordSession(agentId, projectId, event, data = {}) {
|
|
161
|
+
const session = {
|
|
162
|
+
agentId: agentId || "anonymous",
|
|
163
|
+
projectId,
|
|
164
|
+
event,
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
...data,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const file = join(SESSIONS_DIR, `${projectId}_sessions.json`);
|
|
170
|
+
let sessions = [];
|
|
171
|
+
if (existsSync(file)) {
|
|
172
|
+
try { sessions = JSON.parse(readFileSync(file, "utf-8")); } catch { sessions = []; }
|
|
173
|
+
}
|
|
174
|
+
sessions.push(session);
|
|
175
|
+
// Keep last 100 sessions per project
|
|
176
|
+
if (sessions.length > 100) sessions = sessions.slice(-100);
|
|
177
|
+
writeFileSync(file, JSON.stringify(sessions, null, 2));
|
|
178
|
+
return session;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getRecentSessions(projectId, limit = 10) {
|
|
182
|
+
const file = join(SESSIONS_DIR, `${projectId}_sessions.json`);
|
|
183
|
+
if (!existsSync(file)) return [];
|
|
184
|
+
try {
|
|
185
|
+
const sessions = JSON.parse(readFileSync(file, "utf-8"));
|
|
186
|
+
return sessions.slice(-limit);
|
|
187
|
+
} catch { return []; }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Execution Type Classification ─────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function classifyExecutionType(intent) {
|
|
193
|
+
const lower = intent.toLowerCase();
|
|
194
|
+
if (/\b(meet|call|email|talk|discuss|decide|choose|buy|purchase|sign up|register|hire|interview|negotiate|pitch|present|attend|schedule|book|contact|reach out|approve)\b/.test(lower)) return "human";
|
|
195
|
+
if (/\b(deploy|publish|push|install|run|execute|build|compile|test|debug|browse|navigate|click|fill|submit|upload|download|configure|ssh|terminal|git push|npm|docker|automate|scrape|monitor)\b/.test(lower)) return "agent";
|
|
196
|
+
return "ai";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Dispatch Packet Generation ─────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function generatePacket(action, project) {
|
|
202
|
+
const constraints = project.decisionGate?.constraints || {};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
version: "1.0",
|
|
206
|
+
id: action.id || `task_${Date.now()}`,
|
|
207
|
+
createdAt: new Date().toISOString(),
|
|
208
|
+
|
|
209
|
+
objective: {
|
|
210
|
+
task: action.intent,
|
|
211
|
+
category: action.category,
|
|
212
|
+
successState: deriveSuccessState(action.intent),
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
constraints: {
|
|
216
|
+
budget: constraints.budget ? `$${constraints.budget}` : undefined,
|
|
217
|
+
time: constraints.timeHoursPerWeek ? `${constraints.timeHoursPerWeek}h/week available` : undefined,
|
|
218
|
+
skill: constraints.skillLevel,
|
|
219
|
+
urgency: constraints.urgency,
|
|
220
|
+
boundaries: deriveBoundaries(constraints),
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
context: {
|
|
224
|
+
project: `${project.name}${project.tldr ? ` — ${project.tldr}` : ""}`,
|
|
225
|
+
vision: project.artifacts?.vision?.content?.slice(0, 600),
|
|
226
|
+
plan: findRelevantPlan(action.intent, project.artifacts?.plan?.content),
|
|
227
|
+
priorWork: getPriorWork(action, project),
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
verification: deriveVerification(action.intent),
|
|
231
|
+
rollback: assessReversibility(action.intent),
|
|
232
|
+
|
|
233
|
+
continuation: {
|
|
234
|
+
reportBack: "When done, call phewsh_complete_task with your result. Include: what you did, what worked, any issues.",
|
|
235
|
+
nextActions: getNextActions(action, project),
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
runtime: inferRuntime(action.intent),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function deriveSuccessState(intent) {
|
|
243
|
+
const lower = intent.toLowerCase();
|
|
244
|
+
if (/deploy|publish|push|ship/.test(lower)) return "Live and accessible at target URL";
|
|
245
|
+
if (/set up|configure|install/.test(lower)) return "Service running and verified";
|
|
246
|
+
if (/write|draft|create/.test(lower)) return "Document complete and saved";
|
|
247
|
+
if (/research|investigate|analyze/.test(lower)) return "Findings documented with recommendations";
|
|
248
|
+
if (/fix|debug|resolve/.test(lower)) return "Issue no longer reproducible";
|
|
249
|
+
if (/build|implement|code/.test(lower)) return "Feature working and integrated";
|
|
250
|
+
if (/test|verify/.test(lower)) return "All checks pass";
|
|
251
|
+
return `"${intent}" completed and verified`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function deriveBoundaries(constraints) {
|
|
255
|
+
const b = [];
|
|
256
|
+
if (constraints.budget && constraints.budget < 100) b.push("Do not use paid services without approval");
|
|
257
|
+
if (constraints.urgency === "critical") b.push("Do not spend time on non-essential polish");
|
|
258
|
+
if (constraints.autonomy === "hands-on") b.push("Do not make irreversible decisions without presenting options");
|
|
259
|
+
return b.length > 0 ? b : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function findRelevantPlan(intent, planContent) {
|
|
263
|
+
if (!planContent) return undefined;
|
|
264
|
+
const lines = planContent.split("\n");
|
|
265
|
+
const words = intent.toLowerCase().split(/\s+/).filter(w => w.length > 4);
|
|
266
|
+
let bestIdx = -1, bestScore = 0;
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
const score = words.filter(w => lines[i].toLowerCase().includes(w)).length;
|
|
269
|
+
if (score > bestScore) { bestScore = score; bestIdx = i; }
|
|
270
|
+
}
|
|
271
|
+
if (bestIdx >= 0 && bestScore >= 2) {
|
|
272
|
+
return lines.slice(Math.max(0, bestIdx - 2), bestIdx + 8).join("\n");
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getPriorWork(action, project) {
|
|
278
|
+
const done = (project.actions || [])
|
|
279
|
+
.filter(a => a.id !== action.id && a.state === "reconciled" && a.reconciliation?.decision === "accept")
|
|
280
|
+
.slice(-5)
|
|
281
|
+
.map(a => a.intent);
|
|
282
|
+
return done.length > 0 ? done : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function getNextActions(action, project) {
|
|
286
|
+
const next = (project.actions || [])
|
|
287
|
+
.filter(a => a.id !== action.id && a.state === "intended" && a.category === action.category)
|
|
288
|
+
.slice(0, 2)
|
|
289
|
+
.map(a => a.intent);
|
|
290
|
+
return next.length > 0 ? next : undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function deriveVerification(intent) {
|
|
294
|
+
const lower = intent.toLowerCase();
|
|
295
|
+
const criteria = [];
|
|
296
|
+
if (/deploy|publish/.test(lower)) { criteria.push("Accessible at target URL", "No error responses"); }
|
|
297
|
+
else if (/build|implement|code/.test(lower)) { criteria.push("Compiles without errors", "Feature works as described"); }
|
|
298
|
+
else if (/write|draft/.test(lower)) { criteria.push("Content complete and coherent", "Saved to expected location"); }
|
|
299
|
+
else if (/set up|configure/.test(lower)) { criteria.push("Responds to test interaction", "Persists across restart"); }
|
|
300
|
+
else { criteria.push("Objective achieved as described", "No unintended side effects"); }
|
|
301
|
+
return { criteria };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function assessReversibility(intent) {
|
|
305
|
+
const lower = intent.toLowerCase();
|
|
306
|
+
if (/delete|remove|send email|publish|announce/.test(lower)) return { canRevert: false, warning: "Confirm before executing" };
|
|
307
|
+
if (/deploy|push|migrate/.test(lower)) return { canRevert: true, how: "Redeploy previous version" };
|
|
308
|
+
return { canRevert: true };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function inferRuntime(intent) {
|
|
312
|
+
const lower = intent.toLowerCase();
|
|
313
|
+
if (/deploy|push|git|npm|build|compile|test|ssh|terminal/.test(lower)) return { preferred: "claude-code", needs: ["filesystem", "terminal"] };
|
|
314
|
+
if (/browse|navigate|click|fill|submit|sign up|scrape/.test(lower)) return { preferred: "browser-agent", needs: ["browser", "network"] };
|
|
315
|
+
if (/meet|call|email|discuss|decide|buy|hire/.test(lower)) return { preferred: "human", needs: ["judgment", "social"] };
|
|
316
|
+
return { preferred: "claude-code", needs: ["text-generation"] };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Project Summary Helpers ────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function summarizeProject(p) {
|
|
322
|
+
const actions = p.actions || [];
|
|
323
|
+
const total = actions.length;
|
|
324
|
+
const done = actions.filter(a => a.state === "reconciled" && a.reconciliation?.decision === "accept").length;
|
|
325
|
+
const pending = actions.filter(a => a.state === "intended").length;
|
|
326
|
+
const dispatched = actions.filter(a => a.state === "dispatched").length;
|
|
327
|
+
const gate = p.decisionGate;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
id: p.id,
|
|
331
|
+
name: p.name,
|
|
332
|
+
tldr: p.tldr,
|
|
333
|
+
source: p.source || "synced",
|
|
334
|
+
progress: total > 0 ? { done, total, percent: Math.round(done / total * 100) } : null,
|
|
335
|
+
pending,
|
|
336
|
+
dispatched,
|
|
337
|
+
feasibility: gate?.feasibility,
|
|
338
|
+
constraints: gate?.constraints ? {
|
|
339
|
+
budget: gate.constraints.budget || null,
|
|
340
|
+
timePerWeek: gate.constraints.timeHoursPerWeek || null,
|
|
341
|
+
urgency: gate.constraints.urgency,
|
|
342
|
+
autonomy: gate.constraints.autonomy,
|
|
343
|
+
} : null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildFullBriefing(project) {
|
|
348
|
+
const sections = [];
|
|
349
|
+
sections.push(`# ${project.name}`);
|
|
350
|
+
if (project.tldr) sections.push(`> ${project.tldr}`);
|
|
351
|
+
sections.push("");
|
|
352
|
+
|
|
353
|
+
// Gate + Constraints
|
|
354
|
+
const gate = project.decisionGate;
|
|
355
|
+
if (gate) {
|
|
356
|
+
sections.push("## Your Operating Reality");
|
|
357
|
+
if (gate.goal) sections.push(`**Goal:** ${gate.goal}`);
|
|
358
|
+
if (gate.feasibility) sections.push(`**Feasibility:** ${gate.feasibility}`);
|
|
359
|
+
const c = gate.constraints || {};
|
|
360
|
+
const constraintLines = [];
|
|
361
|
+
if (c.budget) constraintLines.push(`Budget: $${c.budget}`);
|
|
362
|
+
if (c.timeHoursPerWeek) constraintLines.push(`Time: ${c.timeHoursPerWeek}h/week`);
|
|
363
|
+
if (c.skillLevel) constraintLines.push(`Skill: ${c.skillLevel}`);
|
|
364
|
+
if (c.urgency) constraintLines.push(`Urgency: ${c.urgency}`);
|
|
365
|
+
if (c.autonomy) constraintLines.push(`Autonomy: ${c.autonomy}`);
|
|
366
|
+
if (constraintLines.length > 0) {
|
|
367
|
+
sections.push("");
|
|
368
|
+
constraintLines.forEach(l => sections.push(`- ${l}`));
|
|
369
|
+
}
|
|
370
|
+
if (gate.successCriteria?.length > 0) {
|
|
371
|
+
sections.push("");
|
|
372
|
+
sections.push("**Success looks like:**");
|
|
373
|
+
gate.successCriteria.forEach(c => sections.push(`- ${c}`));
|
|
374
|
+
}
|
|
375
|
+
sections.push("");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Vision (condensed)
|
|
379
|
+
if (project.artifacts?.vision?.content) {
|
|
380
|
+
sections.push("## Vision");
|
|
381
|
+
sections.push(project.artifacts.vision.content.slice(0, 800));
|
|
382
|
+
sections.push("");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Plan (condensed)
|
|
386
|
+
if (project.artifacts?.plan?.content) {
|
|
387
|
+
sections.push("## Plan");
|
|
388
|
+
sections.push(project.artifacts.plan.content.slice(0, 1200));
|
|
389
|
+
sections.push("");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Execution state
|
|
393
|
+
const actions = project.actions || [];
|
|
394
|
+
if (actions.length > 0) {
|
|
395
|
+
const done = actions.filter(a => a.state === "reconciled" && a.reconciliation?.decision === "accept");
|
|
396
|
+
const pending = actions.filter(a => a.state === "intended");
|
|
397
|
+
const dispatched = actions.filter(a => a.state === "dispatched");
|
|
398
|
+
|
|
399
|
+
sections.push("## Execution State");
|
|
400
|
+
sections.push(`Progress: ${done.length}/${actions.length} complete (${Math.round(done.length / actions.length * 100)}%)`);
|
|
401
|
+
|
|
402
|
+
if (dispatched.length > 0) {
|
|
403
|
+
sections.push("");
|
|
404
|
+
sections.push("**In progress:**");
|
|
405
|
+
dispatched.forEach(a => sections.push(`- ${a.intent}`));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (done.length > 0) {
|
|
409
|
+
sections.push("");
|
|
410
|
+
sections.push("**Recently completed:**");
|
|
411
|
+
done.slice(-5).forEach(a => sections.push(`- ${a.intent}`));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (pending.length > 0) {
|
|
415
|
+
sections.push("");
|
|
416
|
+
sections.push("**Ready for you:**");
|
|
417
|
+
const ordered = getOrderedTasks(project, "any").slice(0, 8);
|
|
418
|
+
ordered.forEach(a => {
|
|
419
|
+
const type = a.executionType || classifyExecutionType(a.intent);
|
|
420
|
+
sections.push(`- [${type}] ${a.intent}`);
|
|
421
|
+
});
|
|
422
|
+
if (pending.length > 8) sections.push(` ...and ${pending.length - 8} more`);
|
|
423
|
+
}
|
|
424
|
+
sections.push("");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return sections.join("\n");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── MCP Server ─────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
const server = new Server(
|
|
433
|
+
{ name: "phewsh", version: "0.2.0" },
|
|
434
|
+
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// ── Tools ──
|
|
438
|
+
|
|
439
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
440
|
+
tools: [
|
|
441
|
+
{
|
|
442
|
+
name: "phewsh_start",
|
|
443
|
+
description: "Start a work session. Returns everything you need: project context, constraints, what changed, and your next task — all in one call. Call this first.",
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties: {
|
|
447
|
+
project_id: { type: "string", description: "Project ID (use 'local' for .intent/ project in cwd). Omit to auto-detect." },
|
|
448
|
+
agent_id: { type: "string", description: "Your agent identifier (e.g. 'claude-code', 'cursor', 'custom-agent')" },
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: "phewsh_next_task",
|
|
454
|
+
description: "Get the next highest-priority task as a structured dispatch packet. Contains objective, constraints, verification criteria, and what to do after. Call this when you're ready for more work.",
|
|
455
|
+
inputSchema: {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
project_id: { type: "string", description: "Project ID" },
|
|
459
|
+
type: { type: "string", enum: ["agent", "ai", "human", "any"], description: "Filter by who can do it (default: agent)" },
|
|
460
|
+
skip_ids: { type: "array", items: { type: "string" }, description: "Task IDs to skip (already attempted)" },
|
|
461
|
+
},
|
|
462
|
+
required: ["project_id"],
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "phewsh_complete_task",
|
|
467
|
+
description: "Report that you finished a task. Include what you did and whether it worked. Returns the next task automatically — keep the momentum going.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
project_id: { type: "string", description: "Project ID" },
|
|
472
|
+
task_id: { type: "string", description: "The task ID from the dispatch packet" },
|
|
473
|
+
result: { type: "string", description: "What you accomplished — be specific about outputs and artifacts" },
|
|
474
|
+
success: { type: "boolean", description: "Did it work?" },
|
|
475
|
+
issues: { type: "string", description: "Problems encountered, decisions made, or things the human should know" },
|
|
476
|
+
agent_id: { type: "string", description: "Your agent identifier" },
|
|
477
|
+
},
|
|
478
|
+
required: ["project_id", "task_id", "result", "success"],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "phewsh_list_projects",
|
|
483
|
+
description: "List all available projects with their progress, constraints, and pending task counts.",
|
|
484
|
+
inputSchema: { type: "object", properties: {} },
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "phewsh_get_context",
|
|
488
|
+
description: "Get the full project briefing — vision, plan, constraints, execution state. Use when you need deeper context on a specific project.",
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {
|
|
492
|
+
project_id: { type: "string", description: "Project ID" },
|
|
493
|
+
},
|
|
494
|
+
required: ["project_id"],
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "phewsh_check_verification",
|
|
499
|
+
description: "Before marking a task complete, check its verification criteria. Returns what you need to confirm.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
task_id: { type: "string", description: "The task ID to check verification for" },
|
|
504
|
+
project_id: { type: "string", description: "Project ID" },
|
|
505
|
+
},
|
|
506
|
+
required: ["task_id", "project_id"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "phewsh_flag_blocker",
|
|
511
|
+
description: "Flag a task as blocked — something is preventing execution. The human will be notified.",
|
|
512
|
+
inputSchema: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {
|
|
515
|
+
project_id: { type: "string", description: "Project ID" },
|
|
516
|
+
task_id: { type: "string", description: "Task ID that's blocked" },
|
|
517
|
+
reason: { type: "string", description: "What's blocking execution" },
|
|
518
|
+
needs: { type: "string", description: "What you need to unblock (e.g. 'API key', 'human decision', 'access to service')" },
|
|
519
|
+
agent_id: { type: "string", description: "Your agent identifier" },
|
|
520
|
+
},
|
|
521
|
+
required: ["project_id", "task_id", "reason"],
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "phewsh_session_history",
|
|
526
|
+
description: "See what agents have done on this project — completions, blockers, session starts. Useful for understanding context across sessions.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
project_id: { type: "string", description: "Project ID" },
|
|
531
|
+
limit: { type: "number", description: "Number of events to return (default: 20)" },
|
|
532
|
+
},
|
|
533
|
+
required: ["project_id"],
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
}));
|
|
538
|
+
|
|
539
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
540
|
+
const { name, arguments: args } = request.params;
|
|
541
|
+
const projects = loadProjects();
|
|
542
|
+
|
|
543
|
+
switch (name) {
|
|
544
|
+
// ─── START SESSION ──────────────────────────────────────────────────────
|
|
545
|
+
case "phewsh_start": {
|
|
546
|
+
// Auto-detect project if none specified
|
|
547
|
+
let projectId = args.project_id;
|
|
548
|
+
if (!projectId) {
|
|
549
|
+
if (projects.length === 1) projectId = projects[0].id;
|
|
550
|
+
else if (projects.find(p => p.id === "local")) projectId = "local";
|
|
551
|
+
else {
|
|
552
|
+
return { content: [{ type: "text", text: `Multiple projects available. Specify one:\n${projects.map(p => `- ${p.id}: ${p.name}`).join("\n")}` }] };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const project = projects.find(p => p.id === projectId);
|
|
557
|
+
if (!project) {
|
|
558
|
+
return { content: [{ type: "text", text: `Project "${projectId}" not found. Available: ${projects.map(p => `${p.id} (${p.name})`).join(", ")}` }] };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Record session start
|
|
562
|
+
recordSession(args.agent_id, projectId, "session_start");
|
|
563
|
+
|
|
564
|
+
const sections = [];
|
|
565
|
+
|
|
566
|
+
// Greeting + orientation
|
|
567
|
+
sections.push(`# Session Started: ${project.name}`);
|
|
568
|
+
if (args.agent_id) sections.push(`Agent: ${args.agent_id}`);
|
|
569
|
+
sections.push("");
|
|
570
|
+
|
|
571
|
+
// What changed since last session
|
|
572
|
+
const recentSessions = getRecentSessions(projectId, 20);
|
|
573
|
+
const recentCompletions = recentSessions.filter(s => s.event === "task_complete" && s.timestamp > new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
|
|
574
|
+
const recentBlockers = recentSessions.filter(s => s.event === "blocker_flagged" && !s.resolved);
|
|
575
|
+
|
|
576
|
+
if (recentCompletions.length > 0 || recentBlockers.length > 0) {
|
|
577
|
+
sections.push("## Since Last Session");
|
|
578
|
+
if (recentCompletions.length > 0) {
|
|
579
|
+
sections.push("**Completed:**");
|
|
580
|
+
recentCompletions.slice(-5).forEach(s => sections.push(`- ${s.result?.split("\n")[0] || s.taskId}`));
|
|
581
|
+
}
|
|
582
|
+
if (recentBlockers.length > 0) {
|
|
583
|
+
sections.push("**Blocked:**");
|
|
584
|
+
recentBlockers.forEach(s => sections.push(`- ${s.reason} (needs: ${s.needs || "unknown"})`));
|
|
585
|
+
}
|
|
586
|
+
sections.push("");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Full briefing
|
|
590
|
+
sections.push(buildFullBriefing(project));
|
|
591
|
+
|
|
592
|
+
// Your first task
|
|
593
|
+
const nextTasks = getOrderedTasks(project, "any");
|
|
594
|
+
if (nextTasks.length > 0) {
|
|
595
|
+
const first = nextTasks[0];
|
|
596
|
+
const packet = generatePacket(first, project);
|
|
597
|
+
sections.push("---");
|
|
598
|
+
sections.push("## Your First Task");
|
|
599
|
+
sections.push("");
|
|
600
|
+
sections.push(`**${packet.objective.task}**`);
|
|
601
|
+
sections.push(`Done when: ${packet.objective.successState}`);
|
|
602
|
+
sections.push(`Task ID: \`${packet.id}\``);
|
|
603
|
+
sections.push("");
|
|
604
|
+
if (packet.constraints.boundaries) {
|
|
605
|
+
sections.push("Boundaries:");
|
|
606
|
+
packet.constraints.boundaries.forEach(b => sections.push(`- ${b}`));
|
|
607
|
+
sections.push("");
|
|
608
|
+
}
|
|
609
|
+
sections.push("Verify before completing:");
|
|
610
|
+
packet.verification.criteria.forEach(c => sections.push(`- [ ] ${c}`));
|
|
611
|
+
sections.push("");
|
|
612
|
+
if (!packet.rollback.canRevert) {
|
|
613
|
+
sections.push(`**Warning:** This action is not easily reversible. ${packet.rollback.warning || "Confirm before executing."}`);
|
|
614
|
+
sections.push("");
|
|
615
|
+
}
|
|
616
|
+
if (packet.continuation.nextActions?.length > 0) {
|
|
617
|
+
sections.push("After this, next up:");
|
|
618
|
+
packet.continuation.nextActions.forEach(a => sections.push(`- ${a}`));
|
|
619
|
+
sections.push("");
|
|
620
|
+
}
|
|
621
|
+
sections.push(`When done, call \`phewsh_complete_task\` with task_id: "${packet.id}"`);
|
|
622
|
+
} else {
|
|
623
|
+
sections.push("---");
|
|
624
|
+
sections.push("## No Pending Tasks");
|
|
625
|
+
sections.push("All tasks are complete or dispatched. Check back later or ask the project owner for new work.");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ─── NEXT TASK ──────────────────────────────────────────────────────────
|
|
632
|
+
case "phewsh_next_task": {
|
|
633
|
+
const project = projects.find(p => p.id === args.project_id);
|
|
634
|
+
if (!project) return { content: [{ type: "text", text: `Project "${args.project_id}" not found.` }] };
|
|
635
|
+
|
|
636
|
+
const typeFilter = args.type || "agent";
|
|
637
|
+
const skipIds = args.skip_ids || [];
|
|
638
|
+
let ordered = getOrderedTasks(project, typeFilter);
|
|
639
|
+
ordered = ordered.filter(a => !skipIds.includes(a.id));
|
|
640
|
+
|
|
641
|
+
if (ordered.length === 0) {
|
|
642
|
+
// Try broader filter
|
|
643
|
+
if (typeFilter !== "any") {
|
|
644
|
+
ordered = getOrderedTasks(project, "any").filter(a => !skipIds.includes(a.id));
|
|
645
|
+
if (ordered.length > 0) {
|
|
646
|
+
return { content: [{ type: "text", text: `No "${typeFilter}" tasks pending. ${ordered.length} tasks available with type filter "any". Call again with type: "any" if you can handle them.` }] };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return { content: [{ type: "text", text: "All caught up. No pending tasks matching your criteria." }] };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const action = ordered[0];
|
|
653
|
+
const packet = generatePacket(action, project);
|
|
654
|
+
const remaining = ordered.length - 1;
|
|
655
|
+
|
|
656
|
+
const output = {
|
|
657
|
+
...packet,
|
|
658
|
+
_meta: {
|
|
659
|
+
remaining_tasks: remaining,
|
|
660
|
+
queue_position: 1,
|
|
661
|
+
total_in_project: (project.actions || []).length,
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ─── COMPLETE TASK ──────────────────────────────────────────────────────
|
|
669
|
+
case "phewsh_complete_task": {
|
|
670
|
+
const result = {
|
|
671
|
+
projectId: args.project_id,
|
|
672
|
+
taskId: args.task_id,
|
|
673
|
+
result: args.result,
|
|
674
|
+
success: args.success,
|
|
675
|
+
issues: args.issues,
|
|
676
|
+
agentId: args.agent_id,
|
|
677
|
+
reportedAt: new Date().toISOString(),
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Save result to disk
|
|
681
|
+
const resultFile = join(RESULTS_DIR, `${args.task_id}_${Date.now()}.json`);
|
|
682
|
+
writeFileSync(resultFile, JSON.stringify(result, null, 2));
|
|
683
|
+
|
|
684
|
+
// Record session event
|
|
685
|
+
recordSession(args.agent_id, args.project_id, "task_complete", {
|
|
686
|
+
taskId: args.task_id,
|
|
687
|
+
success: args.success,
|
|
688
|
+
result: args.result?.slice(0, 200),
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Update local .intent/status.md if applicable
|
|
692
|
+
if (args.project_id === "local") {
|
|
693
|
+
const intentDir = join(process.cwd(), ".intent");
|
|
694
|
+
if (existsSync(intentDir)) {
|
|
695
|
+
const statusFile = join(intentDir, "status.md");
|
|
696
|
+
const entry = `\n- [${args.success ? "x" : "!"}] ${args.result.split("\n")[0]} (${new Date().toISOString().split("T")[0]})${args.agent_id ? ` [${args.agent_id}]` : ""}\n`;
|
|
697
|
+
try {
|
|
698
|
+
if (existsSync(statusFile)) {
|
|
699
|
+
const existing = readFileSync(statusFile, "utf-8");
|
|
700
|
+
writeFileSync(statusFile, existing + entry);
|
|
701
|
+
} else {
|
|
702
|
+
writeFileSync(statusFile, `# Execution Log\n${entry}`);
|
|
703
|
+
}
|
|
704
|
+
} catch { /* non-critical — skip if write fails */ }
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Build response with auto-chain
|
|
709
|
+
const sections = [];
|
|
710
|
+
if (args.success) {
|
|
711
|
+
sections.push(`Task "${args.task_id}" completed successfully.`);
|
|
712
|
+
if (args.issues) sections.push(`Note: ${args.issues}`);
|
|
713
|
+
} else {
|
|
714
|
+
sections.push(`Task "${args.task_id}" reported as failed.`);
|
|
715
|
+
sections.push(`Issues: ${args.issues || args.result}`);
|
|
716
|
+
sections.push("The project owner will be notified to review.");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Auto-chain: show next task
|
|
720
|
+
const project = projects.find(p => p.id === args.project_id);
|
|
721
|
+
if (project && args.success) {
|
|
722
|
+
const nextTasks = getOrderedTasks(project, "any")
|
|
723
|
+
.filter(a => a.id !== args.task_id);
|
|
724
|
+
|
|
725
|
+
if (nextTasks.length > 0) {
|
|
726
|
+
const next = nextTasks[0];
|
|
727
|
+
const packet = generatePacket(next, project);
|
|
728
|
+
sections.push("");
|
|
729
|
+
sections.push("---");
|
|
730
|
+
sections.push("## Next Task");
|
|
731
|
+
sections.push(`**${packet.objective.task}**`);
|
|
732
|
+
sections.push(`Done when: ${packet.objective.successState}`);
|
|
733
|
+
sections.push(`Task ID: \`${packet.id}\``);
|
|
734
|
+
sections.push("");
|
|
735
|
+
sections.push("Verify:");
|
|
736
|
+
packet.verification.criteria.forEach(c => sections.push(`- [ ] ${c}`));
|
|
737
|
+
sections.push("");
|
|
738
|
+
sections.push(`${nextTasks.length - 1} more tasks after this.`);
|
|
739
|
+
} else {
|
|
740
|
+
sections.push("");
|
|
741
|
+
sections.push("All tasks complete! Great work.");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ─── LIST PROJECTS ──────────────────────────────────────────────────────
|
|
749
|
+
case "phewsh_list_projects": {
|
|
750
|
+
const summary = projects.map(summarizeProject);
|
|
751
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ─── GET CONTEXT ────────────────────────────────────────────────────────
|
|
755
|
+
case "phewsh_get_context": {
|
|
756
|
+
const project = projects.find(p => p.id === args.project_id);
|
|
757
|
+
if (!project) return { content: [{ type: "text", text: `Project "${args.project_id}" not found.` }] };
|
|
758
|
+
return { content: [{ type: "text", text: buildFullBriefing(project) }] };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ─── CHECK VERIFICATION ─────────────────────────────────────────────────
|
|
762
|
+
case "phewsh_check_verification": {
|
|
763
|
+
const project = projects.find(p => p.id === args.project_id);
|
|
764
|
+
if (!project) return { content: [{ type: "text", text: `Project "${args.project_id}" not found.` }] };
|
|
765
|
+
|
|
766
|
+
const action = (project.actions || []).find(a => a.id === args.task_id);
|
|
767
|
+
if (!action) return { content: [{ type: "text", text: `Task "${args.task_id}" not found.` }] };
|
|
768
|
+
|
|
769
|
+
const verification = deriveVerification(action.intent);
|
|
770
|
+
const rollback = assessReversibility(action.intent);
|
|
771
|
+
|
|
772
|
+
const sections = [];
|
|
773
|
+
sections.push(`# Verification: ${action.intent}`);
|
|
774
|
+
sections.push("");
|
|
775
|
+
sections.push("Before marking this complete, confirm ALL of:");
|
|
776
|
+
sections.push("");
|
|
777
|
+
verification.criteria.forEach((c, i) => sections.push(`${i + 1}. ${c}`));
|
|
778
|
+
sections.push("");
|
|
779
|
+
if (!rollback.canRevert) {
|
|
780
|
+
sections.push(`**This is irreversible.** ${rollback.warning}`);
|
|
781
|
+
} else {
|
|
782
|
+
sections.push("This is reversible if needed.");
|
|
783
|
+
if (rollback.how) sections.push(`Rollback: ${rollback.how}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ─── FLAG BLOCKER ───────────────────────────────────────────────────────
|
|
790
|
+
case "phewsh_flag_blocker": {
|
|
791
|
+
recordSession(args.agent_id, args.project_id, "blocker_flagged", {
|
|
792
|
+
taskId: args.task_id,
|
|
793
|
+
reason: args.reason,
|
|
794
|
+
needs: args.needs,
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Write blocker file for visibility
|
|
798
|
+
const blockerFile = join(RESULTS_DIR, `blocker_${args.task_id}_${Date.now()}.json`);
|
|
799
|
+
writeFileSync(blockerFile, JSON.stringify({
|
|
800
|
+
type: "blocker",
|
|
801
|
+
projectId: args.project_id,
|
|
802
|
+
taskId: args.task_id,
|
|
803
|
+
reason: args.reason,
|
|
804
|
+
needs: args.needs,
|
|
805
|
+
agentId: args.agent_id,
|
|
806
|
+
flaggedAt: new Date().toISOString(),
|
|
807
|
+
}, null, 2));
|
|
808
|
+
|
|
809
|
+
// Suggest next task that isn't blocked
|
|
810
|
+
const project = projects.find(p => p.id === args.project_id);
|
|
811
|
+
let suggestion = "";
|
|
812
|
+
if (project) {
|
|
813
|
+
const nextTasks = getOrderedTasks(project, "any").filter(a => a.id !== args.task_id);
|
|
814
|
+
if (nextTasks.length > 0) {
|
|
815
|
+
suggestion = `\n\nMeanwhile, you can work on: "${nextTasks[0].intent}" (task_id: ${nextTasks[0].id})`;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return { content: [{ type: "text", text: `Blocker flagged for task "${args.task_id}". The project owner will be notified.\n\nReason: ${args.reason}\nNeeds: ${args.needs || "Unknown"}${suggestion}` }] };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ─── SESSION HISTORY ────────────────────────────────────────────────────
|
|
823
|
+
case "phewsh_session_history": {
|
|
824
|
+
const sessions = getRecentSessions(args.project_id, args.limit || 20);
|
|
825
|
+
if (sessions.length === 0) {
|
|
826
|
+
return { content: [{ type: "text", text: "No session history for this project yet." }] };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const sections = [`# Session History: ${args.project_id}`, ""];
|
|
830
|
+
sessions.reverse().forEach(s => {
|
|
831
|
+
const time = s.timestamp.split("T")[0];
|
|
832
|
+
const agent = s.agentId || "unknown";
|
|
833
|
+
switch (s.event) {
|
|
834
|
+
case "session_start":
|
|
835
|
+
sections.push(`- [${time}] ${agent} started session`);
|
|
836
|
+
break;
|
|
837
|
+
case "task_complete":
|
|
838
|
+
sections.push(`- [${time}] ${agent} completed: ${s.result || s.taskId} ${s.success ? "OK" : "FAILED"}`);
|
|
839
|
+
break;
|
|
840
|
+
case "blocker_flagged":
|
|
841
|
+
sections.push(`- [${time}] ${agent} blocked on: ${s.reason}`);
|
|
842
|
+
break;
|
|
843
|
+
default:
|
|
844
|
+
sections.push(`- [${time}] ${agent}: ${s.event}`);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
default:
|
|
852
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// ── Prompts (pre-built workflows agents can discover) ──
|
|
857
|
+
|
|
858
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
859
|
+
prompts: [
|
|
860
|
+
{
|
|
861
|
+
name: "work_session",
|
|
862
|
+
description: "Full work session: get briefed, execute tasks, report results. Best for autonomous agents that want to do as much as possible.",
|
|
863
|
+
arguments: [
|
|
864
|
+
{ name: "project_id", description: "Project to work on (omit for auto-detect)", required: false },
|
|
865
|
+
{ name: "max_tasks", description: "Maximum tasks to complete this session (default: 5)", required: false },
|
|
866
|
+
],
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "status_check",
|
|
870
|
+
description: "Quick status check: what's done, what's pending, any blockers. No execution.",
|
|
871
|
+
arguments: [
|
|
872
|
+
{ name: "project_id", description: "Project to check", required: false },
|
|
873
|
+
],
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
name: "review_results",
|
|
877
|
+
description: "Review recent agent work: what was completed, what failed, what's blocked. For human oversight.",
|
|
878
|
+
arguments: [
|
|
879
|
+
{ name: "project_id", description: "Project to review", required: false },
|
|
880
|
+
],
|
|
881
|
+
},
|
|
882
|
+
],
|
|
883
|
+
}));
|
|
884
|
+
|
|
885
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
886
|
+
const { name, arguments: args } = request.params;
|
|
887
|
+
const projects = loadProjects();
|
|
888
|
+
|
|
889
|
+
switch (name) {
|
|
890
|
+
case "work_session": {
|
|
891
|
+
const projectId = args?.project_id || (projects.length === 1 ? projects[0].id : "local");
|
|
892
|
+
const maxTasks = args?.max_tasks || 5;
|
|
893
|
+
const project = projects.find(p => p.id === projectId);
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
messages: [
|
|
897
|
+
{
|
|
898
|
+
role: "user",
|
|
899
|
+
content: {
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `You are an autonomous agent working on the "${project?.name || projectId}" project via PHEWSH coordination.
|
|
902
|
+
|
|
903
|
+
Your workflow:
|
|
904
|
+
1. Call \`phewsh_start\` with project_id: "${projectId}" to get your briefing and first task
|
|
905
|
+
2. Execute the task described in the dispatch packet
|
|
906
|
+
3. Call \`phewsh_complete_task\` with your results — it will auto-chain to the next task
|
|
907
|
+
4. Repeat until you've completed ${maxTasks} tasks or hit a blocker
|
|
908
|
+
5. If blocked, call \`phewsh_flag_blocker\` and move to the next available task
|
|
909
|
+
|
|
910
|
+
Rules:
|
|
911
|
+
- Always check verification criteria before marking complete
|
|
912
|
+
- If a task is irreversible, confirm with the user before executing
|
|
913
|
+
- Report issues honestly — failed tasks help the project owner make better decisions
|
|
914
|
+
- Stay within the constraints provided in each dispatch packet
|
|
915
|
+
- You are the executor, not the decision-maker. Execute what's been planned.
|
|
916
|
+
|
|
917
|
+
Start now by calling phewsh_start.`,
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
],
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
case "status_check": {
|
|
925
|
+
const projectId = args?.project_id || (projects.length === 1 ? projects[0].id : "local");
|
|
926
|
+
return {
|
|
927
|
+
messages: [
|
|
928
|
+
{
|
|
929
|
+
role: "user",
|
|
930
|
+
content: {
|
|
931
|
+
type: "text",
|
|
932
|
+
text: `Give me a quick status check on the "${projectId}" project. Call \`phewsh_list_projects\` and \`phewsh_session_history\` with project_id: "${projectId}" to get the current state. Summarize: what's done, what's pending, any blockers, and what needs my attention.`,
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
],
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
case "review_results": {
|
|
940
|
+
const projectId = args?.project_id || (projects.length === 1 ? projects[0].id : "local");
|
|
941
|
+
return {
|
|
942
|
+
messages: [
|
|
943
|
+
{
|
|
944
|
+
role: "user",
|
|
945
|
+
content: {
|
|
946
|
+
type: "text",
|
|
947
|
+
text: `Review the recent agent work on "${projectId}". Call \`phewsh_session_history\` with project_id: "${projectId}" and limit: 30. Then summarize:
|
|
948
|
+
- What was completed successfully
|
|
949
|
+
- What failed and why
|
|
950
|
+
- Any unresolved blockers
|
|
951
|
+
- Recommendations for what the project owner should do next
|
|
952
|
+
|
|
953
|
+
Be honest and specific. This is for human oversight.`,
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
default:
|
|
961
|
+
return { messages: [] };
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// ── Resources ──
|
|
966
|
+
|
|
967
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
968
|
+
const projects = loadProjects();
|
|
969
|
+
const resources = [];
|
|
970
|
+
|
|
971
|
+
for (const project of projects) {
|
|
972
|
+
resources.push({
|
|
973
|
+
uri: `phewsh://projects/${project.id}/briefing`,
|
|
974
|
+
name: `${project.name} — Full Briefing`,
|
|
975
|
+
description: `Complete project context: vision, plan, constraints, execution state`,
|
|
976
|
+
mimeType: "text/markdown",
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
if (project.artifacts) {
|
|
980
|
+
for (const [kind, artifact] of Object.entries(project.artifacts)) {
|
|
981
|
+
if (artifact?.content) {
|
|
982
|
+
resources.push({
|
|
983
|
+
uri: `phewsh://projects/${project.id}/artifacts/${kind}`,
|
|
984
|
+
name: `${project.name} — ${kind}`,
|
|
985
|
+
description: `${kind} artifact`,
|
|
986
|
+
mimeType: "text/markdown",
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return { resources };
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
997
|
+
const { uri } = request.params;
|
|
998
|
+
const projects = loadProjects();
|
|
999
|
+
|
|
1000
|
+
const match = uri.match(/^phewsh:\/\/projects\/([^/]+)\/(briefing|artifacts\/(.+))$/);
|
|
1001
|
+
if (!match) return { contents: [{ uri, text: "Invalid resource URI", mimeType: "text/plain" }] };
|
|
1002
|
+
|
|
1003
|
+
const [, projectId, path, artifactKind] = match;
|
|
1004
|
+
const project = projects.find(p => p.id === projectId);
|
|
1005
|
+
if (!project) return { contents: [{ uri, text: `Project "${projectId}" not found`, mimeType: "text/plain" }] };
|
|
1006
|
+
|
|
1007
|
+
if (path === "briefing") {
|
|
1008
|
+
return { contents: [{ uri, text: buildFullBriefing(project), mimeType: "text/markdown" }] };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (artifactKind && project.artifacts?.[artifactKind]?.content) {
|
|
1012
|
+
return { contents: [{ uri, text: project.artifacts[artifactKind].content, mimeType: "text/markdown" }] };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return { contents: [{ uri, text: "Resource not found", mimeType: "text/plain" }] };
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// ── Start ──
|
|
1019
|
+
|
|
1020
|
+
async function main() {
|
|
1021
|
+
const transport = new StdioServerTransport();
|
|
1022
|
+
await server.connect(transport);
|
|
1023
|
+
console.error("PHEWSH MCP Server v0.2.0 — coordination layer active");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
main().catch((err) => {
|
|
1027
|
+
console.error("Failed to start PHEWSH MCP server:", err);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
});
|