agent-planner-mcp 0.9.0 → 0.9.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/SKILL.md +31 -0
- package/package.json +1 -1
- package/src/tools/bdi/intentions.js +99 -12
package/SKILL.md
CHANGED
|
@@ -94,6 +94,37 @@ The `update_task` call is atomic — status change, log entry, claim release, an
|
|
|
94
94
|
4. release_task(task_id, message='handoff to teammate') for explicit handoff
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
### Peeking before claiming (v0.9.1+)
|
|
98
|
+
|
|
99
|
+
Pass `dry_run: true` to `claim_next_task` to see the candidate without claiming. Useful when an agent wants to inspect the next task and decide whether to take it, without leaving a phantom claim if it bails.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
claim_next_task({ scope: { plan_id }, dry_run: true })
|
|
103
|
+
// → returns { candidate, source, claim: null, dry_run: true }
|
|
104
|
+
// Then claim for real:
|
|
105
|
+
claim_next_task({ scope: { plan_id } }) // dry_run defaults to false
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Proposing subtasks for human approval (v0.9.1+)
|
|
109
|
+
|
|
110
|
+
Agents can't create plan structure directly — that's the human's job. But agents *can* propose subtasks via `queue_decision` and have them materialize on approval. This preserves the "agents drive execution, humans steer structure" boundary while removing the manual follow-through tax.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
queue_decision({
|
|
114
|
+
plan_id: '<plan>',
|
|
115
|
+
title: 'Approve adding 3 launch tasks?',
|
|
116
|
+
context: 'Found gap in launch goal — no Product Hunt subtasks exist yet',
|
|
117
|
+
smallest_input_needed: 'approve|defer|reject',
|
|
118
|
+
proposed_subtasks: [
|
|
119
|
+
{ parent_id: '<phase-id>', title: 'Draft PH listing copy', node_type: 'task' },
|
|
120
|
+
{ parent_id: '<phase-id>', title: 'Set up PH preview', node_type: 'task' },
|
|
121
|
+
{ parent_id: '<phase-id>', title: 'Schedule launch day', node_type: 'task' }
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
// On resolve_decision({ action: 'approve' }), the 3 tasks are atomically created
|
|
125
|
+
// and their IDs returned in created_subtasks[]. Defer/reject does nothing.
|
|
126
|
+
```
|
|
127
|
+
|
|
97
128
|
## Goal coaching
|
|
98
129
|
|
|
99
130
|
When a user expresses intent — "I want to launch a feature", "we need better testing" — coach them into a structured goal before creating it.
|
package/package.json
CHANGED
|
@@ -58,13 +58,30 @@ const queueDecisionDefinition = {
|
|
|
58
58
|
default: 'normal',
|
|
59
59
|
},
|
|
60
60
|
goal_id: { type: 'string', description: 'Optional goal this decision serves' },
|
|
61
|
+
proposed_subtasks: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
description: "Tasks to materialize if the human approves. Agents propose; humans steer structure. On resolve_decision(action='approve'), these are atomically created under the given parent_id and their IDs are returned.",
|
|
64
|
+
items: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
parent_id: { type: 'string', description: 'Where to attach. Must be a node the agent already has access to.' },
|
|
68
|
+
title: { type: 'string' },
|
|
69
|
+
description: { type: 'string' },
|
|
70
|
+
node_type: { type: 'string', enum: ['phase', 'task', 'milestone'], default: 'task' },
|
|
71
|
+
task_mode: { type: 'string', enum: ['research', 'plan', 'implement', 'free'], default: 'free' },
|
|
72
|
+
agent_instructions: { type: 'string' },
|
|
73
|
+
acceptance_criteria: { type: 'string' },
|
|
74
|
+
},
|
|
75
|
+
required: ['parent_id', 'title'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
61
78
|
},
|
|
62
79
|
required: ['title', 'context', 'smallest_input_needed'],
|
|
63
80
|
},
|
|
64
81
|
};
|
|
65
82
|
|
|
66
83
|
async function queueDecisionHandler(args, apiClient) {
|
|
67
|
-
const { plan_id, node_id, title, context, options, recommendation, smallest_input_needed, urgency, goal_id } = args;
|
|
84
|
+
const { plan_id, node_id, title, context, options, recommendation, smallest_input_needed, urgency, goal_id, proposed_subtasks } = args;
|
|
68
85
|
|
|
69
86
|
let planId = plan_id;
|
|
70
87
|
if (!planId && node_id) {
|
|
@@ -85,7 +102,12 @@ async function queueDecisionHandler(args, apiClient) {
|
|
|
85
102
|
options: options || [],
|
|
86
103
|
recommendation: recommendation || null,
|
|
87
104
|
urgency: urgency || 'normal',
|
|
88
|
-
metadata: {
|
|
105
|
+
metadata: {
|
|
106
|
+
smallest_input_needed,
|
|
107
|
+
goal_id: goal_id || null,
|
|
108
|
+
source: 'bdi.queue_decision',
|
|
109
|
+
proposed_subtasks: Array.isArray(proposed_subtasks) ? proposed_subtasks : undefined,
|
|
110
|
+
},
|
|
89
111
|
};
|
|
90
112
|
if (node_id) body.node_id = node_id;
|
|
91
113
|
|
|
@@ -142,28 +164,70 @@ const resolveDecisionDefinition = {
|
|
|
142
164
|
|
|
143
165
|
async function resolveDecisionHandler(args, apiClient) {
|
|
144
166
|
const { decision_id, plan_id, action, message, selected_option } = args;
|
|
167
|
+
|
|
168
|
+
// Fetch the decision first so we can read proposed_subtasks if any.
|
|
169
|
+
let decision = null;
|
|
170
|
+
try {
|
|
171
|
+
decision = await apiClient.axiosInstance
|
|
172
|
+
.get(`/plans/${plan_id}/decisions/${decision_id}`)
|
|
173
|
+
.then((r) => r.data);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// Best-effort — if fetch fails, we still try to resolve.
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let resolved;
|
|
145
179
|
try {
|
|
146
|
-
|
|
180
|
+
resolved = await apiClient.axiosInstance
|
|
147
181
|
.post(`/plans/${plan_id}/decisions/${decision_id}/resolve`, {
|
|
148
182
|
resolution: action,
|
|
149
183
|
message: message || null,
|
|
150
184
|
selected_option: selected_option || null,
|
|
151
185
|
})
|
|
152
186
|
.then((r) => r.data);
|
|
153
|
-
return formatResponse({
|
|
154
|
-
as_of: asOf(),
|
|
155
|
-
decision_id,
|
|
156
|
-
plan_id,
|
|
157
|
-
status: resolved.status || action,
|
|
158
|
-
resolved_at: resolved.resolved_at || asOf(),
|
|
159
|
-
message: resolved.message || message || null,
|
|
160
|
-
});
|
|
161
187
|
} catch (err) {
|
|
162
188
|
return errorResponse(
|
|
163
189
|
'upstream_unavailable',
|
|
164
190
|
`Failed to resolve decision: ${err.response?.data?.error || err.message}`
|
|
165
191
|
);
|
|
166
192
|
}
|
|
193
|
+
|
|
194
|
+
// On approve, materialize any proposed_subtasks atomically (best-effort per task).
|
|
195
|
+
const created = [];
|
|
196
|
+
const createFailures = [];
|
|
197
|
+
if (action === 'approve' && decision?.metadata?.proposed_subtasks?.length) {
|
|
198
|
+
for (const proposal of decision.metadata.proposed_subtasks) {
|
|
199
|
+
try {
|
|
200
|
+
const node = await apiClient.nodes.createNode(plan_id, {
|
|
201
|
+
parent_id: proposal.parent_id,
|
|
202
|
+
node_type: proposal.node_type || 'task',
|
|
203
|
+
title: proposal.title,
|
|
204
|
+
description: proposal.description,
|
|
205
|
+
status: 'not_started',
|
|
206
|
+
task_mode: proposal.task_mode || 'free',
|
|
207
|
+
agent_instructions: proposal.agent_instructions,
|
|
208
|
+
acceptance_criteria: proposal.acceptance_criteria,
|
|
209
|
+
});
|
|
210
|
+
created.push({ id: node.id || node.node?.id, title: proposal.title, parent_id: proposal.parent_id });
|
|
211
|
+
} catch (err) {
|
|
212
|
+
createFailures.push({
|
|
213
|
+
title: proposal.title,
|
|
214
|
+
parent_id: proposal.parent_id,
|
|
215
|
+
error: err.response?.data?.error || err.message,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return formatResponse({
|
|
222
|
+
as_of: asOf(),
|
|
223
|
+
decision_id,
|
|
224
|
+
plan_id,
|
|
225
|
+
status: resolved.status || action,
|
|
226
|
+
resolved_at: resolved.resolved_at || asOf(),
|
|
227
|
+
message: resolved.message || message || null,
|
|
228
|
+
created_subtasks: created,
|
|
229
|
+
create_failures: createFailures,
|
|
230
|
+
});
|
|
167
231
|
}
|
|
168
232
|
|
|
169
233
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -320,13 +384,18 @@ const claimNextTaskDefinition = {
|
|
|
320
384
|
ttl_minutes: { type: 'integer', default: 30 },
|
|
321
385
|
fresh: { type: 'boolean', default: false },
|
|
322
386
|
context_depth: { type: 'integer', enum: [1, 2, 3, 4], default: 2 },
|
|
387
|
+
dry_run: {
|
|
388
|
+
type: 'boolean',
|
|
389
|
+
default: false,
|
|
390
|
+
description: "If true, return the candidate task without claiming. Lets the caller peek before committing. No phantom claim left behind.",
|
|
391
|
+
},
|
|
323
392
|
},
|
|
324
393
|
required: ['scope'],
|
|
325
394
|
},
|
|
326
395
|
};
|
|
327
396
|
|
|
328
397
|
async function claimNextTaskHandler(args, apiClient) {
|
|
329
|
-
const { scope = {}, ttl_minutes = 30, fresh = false, context_depth = 2 } = args;
|
|
398
|
+
const { scope = {}, ttl_minutes = 30, fresh = false, context_depth = 2, dry_run = false } = args;
|
|
330
399
|
const { plan_id, goal_id } = scope;
|
|
331
400
|
|
|
332
401
|
let chosen = null;
|
|
@@ -377,6 +446,24 @@ async function claimNextTaskHandler(args, apiClient) {
|
|
|
377
446
|
const taskPlanId = chosen.plan_id || plan_id;
|
|
378
447
|
const taskId = chosen.id;
|
|
379
448
|
|
|
449
|
+
// Dry run: return candidate without claiming.
|
|
450
|
+
if (dry_run) {
|
|
451
|
+
return formatResponse({
|
|
452
|
+
as_of: asOf(),
|
|
453
|
+
candidate: {
|
|
454
|
+
id: taskId,
|
|
455
|
+
title: chosen.title,
|
|
456
|
+
status: chosen.status,
|
|
457
|
+
plan_id: taskPlanId,
|
|
458
|
+
task_mode: chosen.task_mode,
|
|
459
|
+
},
|
|
460
|
+
source,
|
|
461
|
+
claim: null,
|
|
462
|
+
dry_run: true,
|
|
463
|
+
next_action_hint: 'Call again with dry_run=false to claim, or pick a different task.',
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
380
467
|
// Claim
|
|
381
468
|
let claim = null;
|
|
382
469
|
try {
|