agent-dealer 0.1.9 → 0.1.11

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.
@@ -1,11 +1,12 @@
1
1
  import { getTemporalOutputDir } from "../paths.js";
2
2
  export const DENY_SEND_TOOL = "mcp__agent-deck__call_service_tool";
3
3
  const DECK_READ_TOOLS = "mcp__agent-deck__get_playbook,mcp__agent-deck__get_bound_deck,mcp__agent-deck__bind_workspace,mcp__agent-deck__list_service_tools";
4
+ const DECK_EXECUTE_TOOLS = `${DECK_READ_TOOLS},mcp__agent-deck__call_service_tool`;
4
5
  const PLAN_REFLECT_TOOLS = `Read,Glob,Grep,Skill,${DECK_READ_TOOLS}`;
5
6
  /** Q&A about a finished result — read-only, no deck writes, no workspace mutation. */
6
7
  const QA_TOOLS = "Read,Glob,Grep,Skill";
7
8
  function executeToolsForCategory(category) {
8
- const base = ["Read", "Write", "Edit", "Glob", "Grep", "Skill", DECK_READ_TOOLS];
9
+ const base = ["Read", "Write", "Edit", "Glob", "Grep", "Skill", DECK_EXECUTE_TOOLS];
9
10
  if (category !== "communication" && category !== "email") {
10
11
  base.splice(5, 0, "Bash");
11
12
  }
@@ -24,6 +25,9 @@ export function buildClaudePhaseArgs(run, mode) {
24
25
  args.push("--add-dir", getTemporalOutputDir());
25
26
  args.push("--allowedTools", executeToolsForCategory(run.taskCategory));
26
27
  }
27
- args.push("--disallowedTools", DENY_SEND_TOOL);
28
+ // Soft gate: execute may call_service_tool; plan/reflect/qa stay denied.
29
+ if (mode !== "execute") {
30
+ args.push("--disallowedTools", DENY_SEND_TOOL);
31
+ }
28
32
  return args;
29
33
  }
@@ -33,8 +33,12 @@ function hasDisallowSend(args) {
33
33
  const i = args.indexOf("--disallowedTools");
34
34
  return i >= 0 && args[i + 1] === DENY_SEND_TOOL;
35
35
  }
36
- test("all phases deny call_service_tool", () => {
37
- for (const mode of ["plan", "execute", "reflect", "qa"]) {
36
+ test("execute allows call_service_tool; plan/reflect/qa deny it", () => {
37
+ const executeArgs = buildClaudePhaseArgs(runWithCategory("code"), "execute");
38
+ assert.equal(hasDisallowSend(executeArgs), false, "execute");
39
+ const i = executeArgs.indexOf("--allowedTools");
40
+ assert.match(executeArgs[i + 1], /call_service_tool/);
41
+ for (const mode of ["plan", "reflect", "qa"]) {
38
42
  const args = buildClaudePhaseArgs(runWithCategory("code"), mode);
39
43
  assert.equal(hasDisallowSend(args), true, mode);
40
44
  }
@@ -32,17 +32,19 @@ function artifactMarkdown(kind, runId) {
32
32
  }
33
33
  function outboundDraftContractSection() {
34
34
  return [
35
- "## Outbound actions (required when sending Slack or email)",
36
- "Do NOT send Slack messages, post to chat, or send email during execution call_service_tool is blocked.",
37
- "If the task requires an outward-facing message, end your reply with exactly one fenced ```json block shaped like:",
38
- '{"actionType":"slack_message"|"email","summary":{"target":"#channel or recipient","body":"exact message text"},"toolCall":{"serviceName":"<deck service UUID>","toolName":"<tool from list_service_tools>","arguments":{...}}}',
35
+ "## Outbound actions (Slack/email prefer draft; other services may write directly)",
36
+ "Prefer ending with a fenced ```json draft for Slack/email (and any write you want human approval on before send).",
37
+ "call_service_tool IS available during execution — use it for non-message deck writes (Linear, GitHub, Docmost, …) when the task must complete the write now.",
38
+ "For Slack/email (or when drafting), do NOT send mid-run: end your reply with exactly one fenced ```json block shaped like:",
39
+ '{"actionType":"slack_message"|"email"|"service_tool_call","summary":{"target":"#channel, recipient, or service action","body":"exact message text or short human summary"},"toolCall":{"serviceName":"<deck service UUID>","toolName":"<tool from list_service_tools>","arguments":{...}}}',
39
40
  "Rules:",
40
- "- At most one outbound draft block per run.",
41
+ "- At most one outbound draft block per run (omit the block if you already called call_service_tool to finish the write).",
41
42
  "- Call bind_workspace, then get_bound_deck and list_service_tools — never invent a service UUID or tool name.",
42
- "- toolCall.serviceName must be the exact Slack/email service id from the bound deck (not a display name).",
43
+ "- toolCall.serviceName must be the exact service id from the bound deck (not a display name).",
43
44
  "- Slack: toolName is slack_send_message; arguments.channel_id is the user or channel id from slack_search_users (never guess IDs from meeting notes); arguments.message must byte-match summary.body.",
44
45
  "- Email: use the real tool name and message field from list_service_tools; message body must byte-match summary.body.",
45
- "- Do not perform the send — the human approves and the server delivers verbatim.",
46
+ "- service_tool_call: summary is for the human review card; arguments need not byte-match summary.body.",
47
+ "- Draft path: do not perform the send — the human approves and the server delivers verbatim.",
46
48
  ].join("\n");
47
49
  }
48
50
  function reviewQaSections(run) {
@@ -178,6 +178,8 @@ test("execution prompt includes outbound draft contract", () => {
178
178
  addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
179
179
  const prompt = buildExecutionPrompt(run);
180
180
  assert.match(prompt, /Outbound actions/);
181
- assert.match(prompt, /call_service_tool is blocked/);
181
+ assert.match(prompt, /call_service_tool IS available/);
182
+ assert.doesNotMatch(prompt, /call_service_tool is blocked/);
183
+ assert.match(prompt, /service_tool_call/);
182
184
  assert.match(prompt, /"actionType"/);
183
185
  });
@@ -100,7 +100,7 @@ export function extractOutboundDraft(resultMarkdown) {
100
100
  const strippedMarkdown = (resultMarkdown.slice(0, last.index) + resultMarkdown.slice(last.index + last[0].length)).trim();
101
101
  try {
102
102
  const parsed = OutboundDraftBlock.parse(JSON.parse(last[1]));
103
- const mismatch = !outboundMessageMatchesSummary(parsed.toolCall, parsed.summary.body);
103
+ const mismatch = !outboundMessageMatchesSummary(parsed.toolCall, parsed.summary.body, parsed.actionType);
104
104
  return {
105
105
  markdown: strippedMarkdown,
106
106
  draft: parsed,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/server",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [