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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-planner-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.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": {
@@ -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: { smallest_input_needed, goal_id: goal_id || null, source: 'bdi.queue_decision' },
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
- const resolved = await apiClient.axiosInstance
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 {