toolroll 0.9.36 → 0.9.38

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.
Files changed (175) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/assignment-adapters.d.ts +12 -48
  3. package/dist/assignment-adapters.js +33 -43
  4. package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +26 -26
  7. package/dist/browser-crew.js +5 -0
  8. package/dist/browser-workspace.d.ts +2 -0
  9. package/dist/build-review.d.ts +1 -1
  10. package/dist/build-review.js +2 -1
  11. package/dist/chat-acceptance.d.ts +1 -1
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-channel.js +20 -9
  16. package/dist/chat-decide.d.ts +18 -4
  17. package/dist/chat-decide.js +31 -11
  18. package/dist/chat-delivery-state.d.ts +12 -70
  19. package/dist/chat-delivery-state.js +17 -135
  20. package/dist/chat-delivery.js +58 -30
  21. package/dist/chat-flow.js +8 -6
  22. package/dist/chat-inbox.js +1 -1
  23. package/dist/coding-context.d.ts +2 -39
  24. package/dist/coding-context.js +2 -1
  25. package/dist/coding-handoff.d.ts +6 -25
  26. package/dist/coding-handoff.js +11 -5
  27. package/dist/coding-types.d.ts +2 -26
  28. package/dist/coding-workspace.d.ts +2 -0
  29. package/dist/coding-workspace.js +50 -14
  30. package/dist/contracts/acceptance-terms.d.ts +14 -0
  31. package/dist/contracts/acceptance-terms.js +16 -0
  32. package/dist/contracts/chat-actions.d.ts +328 -0
  33. package/dist/contracts/chat-actions.js +133 -0
  34. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  35. package/dist/contracts/chat-callback-rows.js +46 -0
  36. package/dist/contracts/chat-content.d.ts +276 -0
  37. package/dist/contracts/chat-content.js +127 -0
  38. package/dist/contracts/chat-tables.d.ts +6 -0
  39. package/dist/contracts/chat-tables.js +136 -0
  40. package/dist/contracts/coding-context.d.ts +97 -0
  41. package/dist/contracts/coding-context.js +60 -0
  42. package/dist/contracts/coding-handoff.d.ts +44 -0
  43. package/dist/contracts/coding-handoff.js +47 -0
  44. package/dist/contracts/coding-workspace.d.ts +165 -0
  45. package/dist/contracts/coding-workspace.js +71 -0
  46. package/dist/contracts/contract.d.ts +7 -2
  47. package/dist/contracts/contract.js +21 -12
  48. package/dist/contracts/decision.d.ts +45 -0
  49. package/dist/contracts/decision.js +45 -0
  50. package/dist/contracts/discord-callback.d.ts +45 -0
  51. package/dist/contracts/discord-callback.js +34 -0
  52. package/dist/contracts/flow-card.d.ts +50 -0
  53. package/dist/contracts/flow-card.js +46 -0
  54. package/dist/contracts/flow-send.d.ts +119 -0
  55. package/dist/contracts/flow-send.js +112 -0
  56. package/dist/contracts/flow.d.ts +5 -0
  57. package/dist/contracts/flow.js +5 -0
  58. package/dist/contracts/gateway-tools.d.ts +374 -0
  59. package/dist/contracts/gateway-tools.js +94 -0
  60. package/dist/contracts/lead-context.d.ts +165 -0
  61. package/dist/contracts/lead-context.js +73 -0
  62. package/dist/contracts/lead-tools.d.ts +1905 -0
  63. package/dist/contracts/lead-tools.js +265 -0
  64. package/dist/contracts/memory-pass.d.ts +158 -0
  65. package/dist/contracts/memory-pass.js +102 -0
  66. package/dist/contracts/project-knowledge.d.ts +126 -0
  67. package/dist/contracts/project-knowledge.js +93 -0
  68. package/dist/contracts/project-memory.d.ts +117 -0
  69. package/dist/contracts/project-memory.js +51 -0
  70. package/dist/contracts/project-skills.d.ts +121 -0
  71. package/dist/contracts/project-skills.js +76 -0
  72. package/dist/contracts/review-findings.d.ts +46 -0
  73. package/dist/contracts/review-findings.js +48 -0
  74. package/dist/contracts/route.d.ts +484 -0
  75. package/dist/contracts/route.js +128 -0
  76. package/dist/contracts/scope.d.ts +105 -0
  77. package/dist/contracts/scope.js +114 -0
  78. package/dist/contracts/scout-report.d.ts +234 -0
  79. package/dist/contracts/scout-report.js +141 -0
  80. package/dist/contracts/slack-callback.d.ts +43 -0
  81. package/dist/contracts/slack-callback.js +35 -0
  82. package/dist/contracts/sort-answer.d.ts +83 -0
  83. package/dist/contracts/sort-answer.js +77 -0
  84. package/dist/contracts/stage-output.d.ts +127 -0
  85. package/dist/contracts/stage-output.js +166 -0
  86. package/dist/contracts/task-sizing.d.ts +24 -0
  87. package/dist/contracts/task-sizing.js +28 -0
  88. package/dist/contracts/teammate-turn.d.ts +43 -0
  89. package/dist/contracts/teammate-turn.js +51 -0
  90. package/dist/contracts/teams-callback.d.ts +14 -0
  91. package/dist/contracts/teams-callback.js +14 -0
  92. package/dist/contracts/telegram-callback.d.ts +120 -0
  93. package/dist/contracts/telegram-callback.js +97 -0
  94. package/dist/decision.d.ts +11 -20
  95. package/dist/decision.js +86 -167
  96. package/dist/desktop-update.d.ts +1 -1
  97. package/dist/discord-chat.js +31 -20
  98. package/dist/flow-engine.js +14 -17
  99. package/dist/flow-items.d.ts +4 -13
  100. package/dist/flow-items.js +1 -15
  101. package/dist/flow-send.d.ts +6 -37
  102. package/dist/flow-send.js +18 -14
  103. package/dist/flow-share.js +7 -1
  104. package/dist/flow-sort.d.ts +7 -26
  105. package/dist/flow-sort.js +25 -20
  106. package/dist/flow-steps.js +3 -2
  107. package/dist/flows.d.ts +2 -4
  108. package/dist/flows.js +9 -5
  109. package/dist/invoke.js +4 -4
  110. package/dist/lead-context.d.ts +4 -2
  111. package/dist/lead-context.js +24 -17
  112. package/dist/mate-progress.d.ts +26 -0
  113. package/dist/mate-progress.js +24 -0
  114. package/dist/mate-tools.d.ts +4 -0
  115. package/dist/mate-tools.js +108 -275
  116. package/dist/mate.js +11 -2
  117. package/dist/mcp-connect.d.ts +24 -4
  118. package/dist/mcp-connect.js +78 -15
  119. package/dist/mcp.js +84 -216
  120. package/dist/memory-pass.d.ts +17 -42
  121. package/dist/memory-pass.js +54 -26
  122. package/dist/phase-routing.d.ts +19 -72
  123. package/dist/phase-routing.js +34 -164
  124. package/dist/project-knowledge.d.ts +16 -35
  125. package/dist/project-knowledge.js +42 -21
  126. package/dist/project-memory.d.ts +2 -28
  127. package/dist/project-memory.js +28 -13
  128. package/dist/project-skills.d.ts +16 -38
  129. package/dist/project-skills.js +41 -32
  130. package/dist/project-tools.d.ts +1 -0
  131. package/dist/project-tools.js +4 -4
  132. package/dist/provider.d.ts +2 -0
  133. package/dist/provider.js +4 -16
  134. package/dist/result-shots.d.ts +1 -1
  135. package/dist/result-shots.js +2 -1
  136. package/dist/review-switch.d.ts +3 -8
  137. package/dist/reviewer.d.ts +8 -12
  138. package/dist/reviewer.js +11 -35
  139. package/dist/scope.d.ts +21 -43
  140. package/dist/scope.js +42 -128
  141. package/dist/scout-report.d.ts +9 -194
  142. package/dist/scout-report.js +113 -264
  143. package/dist/scout.js +15 -11
  144. package/dist/serve.js +23 -5
  145. package/dist/setup-guide.d.ts +2 -2
  146. package/dist/slack-chat.js +26 -20
  147. package/dist/slack-state.js +2 -1
  148. package/dist/store.d.ts +9 -32
  149. package/dist/store.js +18 -15
  150. package/dist/structured-output.js +2 -1
  151. package/dist/subscription-chat.d.ts +4 -0
  152. package/dist/subscription-chat.js +4 -1
  153. package/dist/task-review-brief.d.ts +1 -7
  154. package/dist/task-review-brief.js +6 -6
  155. package/dist/task-sizing.d.ts +3 -27
  156. package/dist/task-sizing.js +11 -26
  157. package/dist/task-text.d.ts +0 -5
  158. package/dist/task-text.js +1 -3
  159. package/dist/teammate-question.js +7 -5
  160. package/dist/teammate-work.js +9 -5
  161. package/dist/teammates.d.ts +13 -64
  162. package/dist/teammates.js +32 -33
  163. package/dist/teams-chat.js +20 -11
  164. package/dist/telegram-flow.d.ts +9 -0
  165. package/dist/telegram-flow.js +44 -8
  166. package/dist/telegram-mate.d.ts +4 -14
  167. package/dist/telegram-mate.js +9 -8
  168. package/dist/telegram-team.d.ts +2 -19
  169. package/dist/telegram.d.ts +1 -1
  170. package/dist/telegram.js +94 -68
  171. package/dist/text-limits.d.ts +142 -1
  172. package/dist/text-limits.js +144 -1
  173. package/dist/tools-ui.d.ts +2 -1
  174. package/dist/tools-ui.js +7 -4
  175. package/package.json +17 -12
package/dist/mcp.js CHANGED
@@ -1,8 +1,10 @@
1
- import { TASK_SCOPE_TEXT_SCHEMA } from "./task-text.js";
2
1
  import { dirname, join } from 'node:path';
3
2
  import { statSync } from 'node:fs';
4
3
  import { repositoryContextRead } from './repository-context.js';
5
4
  import { ASSIGNMENT_TOOLS, assignmentForCoordinator } from "./assignment-adapters.js";
5
+ import { parseContract, toModelSchema } from "./contracts/contract.js";
6
+ import { GATEWAY_TOOL_INPUTS, GATEWAY_TOOL_OUTPUTS } from "./contracts/gateway-tools.js";
7
+ import { reportToolOutput } from "./contracts/lead-tools.js";
6
8
  /**
7
9
  * `toolroll mcp` — the MCP stdio server (MCP gateway spec v6).
8
10
  *
@@ -71,167 +73,73 @@ const CONTRACT_GUIDE = [
71
73
  "Approve, steer, answer, pick, mint, configure, merge: those verbs do not exist on this surface, by construction.",
72
74
  "Claim one assignment, then follow list_assignment_updates with a saved cursor; inspect get_assignment when a current result or decision needs attention. claim_assignment records its lead; acknowledge_assignment records that lead's check of an exact ready receipt. Neither operation grants authority, accepts proof, answers decisions, approves work, publishes, or deploys.",
73
75
  ].join("\n");
74
- /** The descriptor's inputSchema IS the runtime parser (review finding 2):
75
- * unknown fields, bad types, bad enums, and bound violations are protocol
76
- * errors (InvalidParams), never tool refusals. Small on purpose — it
77
- * covers exactly the schema features the descriptors use. */
78
- function invalidArgs(schema, args) {
79
- const shape = schema;
80
- const properties = shape.properties ?? {};
81
- for (const key of Object.keys(args)) {
82
- if (properties[key] === undefined)
83
- return "unknown argument `" + key + "`";
84
- }
85
- for (const key of shape.required ?? []) {
86
- if (args[key] === undefined)
87
- return "missing required argument `" + key + "`";
88
- }
89
- for (const [key, rule] of Object.entries(properties)) {
90
- const value = args[key];
91
- if (value === undefined)
92
- continue;
93
- // A union type (`["string","null"]`) admits null or its other member;
94
- // an array checks its items and its cap (v3 review, finding 8).
95
- if (Array.isArray(rule["type"])) {
96
- const types = rule["type"];
97
- if (value === null) {
98
- if (!types.includes("null"))
99
- return "`" + key + "` must not be null";
100
- continue;
101
- }
102
- const member = types.find(one => one !== "null");
103
- const problem = invalidArgs({ type: "object", properties: { [key]: { ...rule, type: member ?? "string" } } }, { [key]: value });
104
- if (problem !== null)
105
- return problem;
106
- continue;
107
- }
108
- if (rule["type"] === "array") {
109
- if (!Array.isArray(value))
110
- return "`" + key + "` must be an array";
111
- const maxItems = rule["maxItems"];
112
- if (typeof maxItems === "number" && value.length > maxItems)
113
- return "`" + key + "` has more than " + String(maxItems) + " items";
114
- const items = rule["items"];
115
- if (items !== undefined) {
116
- for (const one of value) {
117
- const problem = invalidArgs({ type: "object", properties: { item: items } }, { item: one });
118
- if (problem !== null)
119
- return "`" + key + "`: " + problem.replace("`item`", "an item");
120
- }
121
- }
122
- continue;
123
- }
124
- if (rule["type"] === "boolean" && typeof value !== "boolean")
125
- return "`" + key + "` must be a boolean";
126
- if (rule["type"] === "string") {
127
- if (typeof value !== "string")
128
- return "`" + key + "` must be a string";
129
- const min = rule["minLength"];
130
- const max = rule["maxLength"];
131
- if (typeof min === "number" && value.length < min)
132
- return "`" + key + "` is shorter than " + String(min);
133
- if (typeof max === "number" && value.length > max)
134
- return "`" + key + "` is longer than " + String(max);
135
- const pattern = rule["pattern"];
136
- if (typeof pattern === "string" && !new RegExp(pattern).test(value))
137
- return "`" + key + "` does not match its required format";
138
- const allowed = rule["enum"];
139
- if (Array.isArray(allowed) && !allowed.includes(value))
140
- return "`" + key + "` must be one of " + allowed.join(", ");
141
- }
142
- if (rule["type"] === "integer") {
143
- if (typeof value !== "number" || !Number.isInteger(value))
144
- return "`" + key + "` must be an integer";
145
- const min = rule["minimum"];
146
- const max = rule["maximum"];
147
- if (typeof min === "number" && value < min)
148
- return "`" + key + "` is below " + String(min);
149
- if (typeof max === "number" && value > max)
150
- return "`" + key + "` is above " + String(max);
151
- }
152
- }
153
- return null;
154
- }
155
- function str(args, name, max) {
156
- const value = args[name];
157
- if (typeof value !== "string" || value.length === 0 || value.length > max)
158
- return null;
159
- return value;
160
- }
161
- const TOOLS = [
162
- {
163
- name: 'get_project_context', description: 'Read bounded source excerpts or advisory static import impact in an admitted project. Falls back to text search without an index.',
164
- inputSchema: { type: 'object', properties: { repo: { type: 'string', minLength: 1, maxLength: 4096 }, query: { type: 'string', minLength: 1, maxLength: 1000 }, mode: { type: 'string', enum: ['search', 'impact'] } }, required: ['repo', 'query'], additionalProperties: false },
76
+ /** An assignment tool: the adapter reads its call with the same schema `toolroll assignment` does. */
77
+ const assignmentHandler = (operation) => (ctx, args) => {
78
+ const result = assignmentForCoordinator(ctx.store, ctx.token, operation, args, ctx.now, ctx.evidenceRoot);
79
+ return result.ok ? { ok: true, body: result.body } : { ok: false, message: `${result.reason}: ${result.message}` };
80
+ };
81
+ const assignmentTool = (name) => {
82
+ const spec = ASSIGNMENT_TOOLS.find(one => one.name === name);
83
+ return { description: spec.description, handle: assignmentHandler(spec.operation) };
84
+ };
85
+ /** A propose_* tool: it writes one proposal row through the coordinator door. */
86
+ const proposeTool = (kind, description) => ({
87
+ description,
88
+ handle: (ctx, args) => {
89
+ const outcome = proposeAsCoordinator(ctx.store, ctx.token, kind, args, ctx.now, { readDecisions: ctx.readDecisions });
90
+ if (!outcome.ok)
91
+ return { ok: false, message: outcome.message };
92
+ return { ok: true, body: { proposal: outcome.id, kind: outcome.kind, awaiting: outcome.awaiting } };
93
+ },
94
+ });
95
+ const HANDLERS = {
96
+ get_project_context: {
97
+ description: 'Read bounded source excerpts or advisory static import impact in an admitted project. Falls back to text search without an index.',
165
98
  handle: (ctx, args) => {
166
- const repo = String(args['repo']);
99
+ const repo = args.repo;
167
100
  if (!ctx.who.repos.includes(repo) || ctx.enrolled !== null && !ctx.enrolled.includes(repo))
168
101
  return { ok: false, message: 'That project is outside your access.' };
169
- return { ok: true, body: repositoryContextRead({ repo, query: String(args['query']), mode: args['mode'] === 'impact' ? 'impact' : 'search', audience: 'lead',
102
+ return { ok: true, body: repositoryContextRead({ repo, query: args.query, mode: args.mode === 'impact' ? 'impact' : 'search', audience: 'lead',
170
103
  ...(ctx.evidenceRoot === undefined ? {} : { cacheRoot: join(dirname(ctx.evidenceRoot), 'repository-context') }) }) };
171
104
  },
172
105
  },
173
- ...ASSIGNMENT_TOOLS.map(spec => ({
174
- name: spec.name, description: spec.description, inputSchema: spec.inputSchema,
175
- handle: (ctx, args) => {
176
- const result = assignmentForCoordinator(ctx.store, ctx.token, spec.operation, args, ctx.now, ctx.evidenceRoot);
177
- return result.ok ? { ok: true, body: result.body } : { ok: false, message: `${result.reason}: ${result.message}` };
178
- },
179
- })),
180
- {
181
- name: "status",
106
+ get_assignment: assignmentTool("get_assignment"),
107
+ list_assignment_updates: assignmentTool("list_assignment_updates"),
108
+ claim_assignment: assignmentTool("claim_assignment"),
109
+ acknowledge_assignment: assignmentTool("acknowledge_assignment"),
110
+ get_assignment_brief: assignmentTool("get_assignment_brief"),
111
+ get_assignment_inbox: assignmentTool("get_assignment_inbox"),
112
+ acknowledge_assignment_delivery: assignmentTool("acknowledge_assignment_delivery"),
113
+ status: {
182
114
  description: "The plane's liveness facts over your repo allowlist: what waits on the operator, what runs, what finished in the last 24h.",
183
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
184
115
  handle: ctx => ({ ok: true, body: statusFor(ctx.store, ctx.who, ctx.now) }),
185
116
  },
186
- {
187
- name: "list_tasks",
117
+ list_tasks: {
188
118
  description: "Tasks in your repo allowlist. Filter by state and repo; cursor-paginated, stable order.",
189
- inputSchema: {
190
- type: "object",
191
- properties: {
192
- state: { type: "string", enum: ["queued", "running", "done", "failed", "cancelled"] },
193
- repo: { type: "string", maxLength: 800 },
194
- cursor: { type: "integer", minimum: 0 },
195
- limit: { type: "integer", minimum: 1, maximum: 50 },
196
- },
197
- additionalProperties: false,
198
- },
199
119
  handle: (ctx, args) => {
200
120
  const filter = {};
201
- if (typeof args["state"] === "string")
202
- filter.state = args["state"];
203
- if (typeof args["repo"] === "string")
204
- filter.repo = args["repo"];
205
- if (typeof args["cursor"] === "number")
206
- filter.cursor = args["cursor"];
207
- if (typeof args["limit"] === "number")
208
- filter.limit = args["limit"];
121
+ if (args.state !== undefined)
122
+ filter.state = args.state;
123
+ if (args.repo !== undefined)
124
+ filter.repo = args.repo;
125
+ if (args.cursor !== undefined)
126
+ filter.cursor = args.cursor;
127
+ if (args.limit !== undefined)
128
+ filter.limit = args.limit;
209
129
  return { ok: true, body: listTasksFor(ctx.store, ctx.who, filter, ctx.now) };
210
130
  },
211
131
  },
212
- {
213
- name: "get_task",
132
+ get_task: {
214
133
  description: "One task's deliverable (branch or report), scope standing, filer provenance, attempt ledger, and — for a finished scout — the report's title, summary, and follow-ups. A ref outside your allowlist answers not-found.",
215
- inputSchema: {
216
- type: "object",
217
- properties: { ref: { type: "string", minLength: 1, maxLength: 64 } },
218
- required: ["ref"],
219
- additionalProperties: false,
220
- },
221
134
  handle: (ctx, args) => {
222
- const ref = str(args, "ref", 64);
223
- if (ref === null)
224
- return { ok: false, message: "ref is a task id, 1-64 characters" };
225
- const detail = taskDetailFor(ctx.store, ctx.who, ref, ctx.evidenceRoot, ctx.now);
135
+ const detail = taskDetailFor(ctx.store, ctx.who, args.ref, ctx.evidenceRoot, ctx.now);
226
136
  if (detail === null)
227
- return { ok: false, message: `not-found: no task \`${ref}\` in your repositories` };
137
+ return { ok: false, message: `not-found: no task \`${args.ref}\` in your repositories` };
228
138
  return { ok: true, body: detail };
229
139
  },
230
140
  },
231
- {
232
- name: "list_repos",
141
+ list_repos: {
233
142
  description: "The repositories your credential may see and file into, with their operating-mode standing.",
234
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
235
143
  handle: ctx => {
236
144
  // FAIL CLOSED when the project registry cannot be read (round-2
237
145
  // finding 4): answering the full allowlist would claim enrollment
@@ -252,31 +160,25 @@ const TOOLS = [
252
160
  };
253
161
  },
254
162
  },
255
- {
256
- name: "recap",
163
+ recap: {
257
164
  description: "How things stand per repository in your allowlist, counts and ids: what waits on the operator (decisions, incidents, scopes awaiting approval), what runs, what is queued, finished, failed. Pass `since` (an ISO timestamp) to count only decisions, incidents, and attempts newer than it, to the hour; queued work and scopes awaiting approval always count.",
258
- inputSchema: { type: "object", properties: { since: { type: "string", maxLength: 30 } }, additionalProperties: false },
259
165
  handle: (ctx, args) => {
260
- const since = args["since"];
261
- if (since !== undefined && (typeof since !== "string" || !ISO_STAMP_RULE.test(since) || Number.isNaN(Date.parse(since)))) {
166
+ const since = args.since;
167
+ if (since !== undefined && (!ISO_STAMP_RULE.test(since) || Number.isNaN(Date.parse(since)))) {
262
168
  return { ok: false, message: "since is an ISO timestamp like 2026-09-02T12:00:00Z" };
263
169
  }
264
- return { ok: true, body: labelRepos(recapOver(ctx.store, ctx.who.repos, ctx.now, typeof since === "string" ? since : null), index => ctx.who.repos[index] ?? "") };
170
+ return { ok: true, body: labelRepos(recapOver(ctx.store, ctx.who.repos, ctx.now, since ?? null), index => ctx.who.repos[index] ?? "") };
265
171
  },
266
172
  },
267
- {
268
- name: "list_decisions",
173
+ list_decisions: {
269
174
  description: "Open decisions in your allowlist: id, task, question, options (id, label, reversible), age in hours. Never consequences or recommendations; the operator answers them.",
270
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
271
175
  handle: ctx => ({ ok: true, body: labelRepos(decisionsOver(ctx.store, ctx.who.repos, ctx.now), index => ctx.who.repos[index] ?? "") }),
272
176
  },
273
- {
274
- name: "queue",
177
+ queue: {
275
178
  description: "One repository's queue by column — the shared column, then each worker's reserved column — each in dispatch order.",
276
- inputSchema: { type: "object", properties: { repo: { type: "string", minLength: 1, maxLength: 800 } }, required: ["repo"], additionalProperties: false },
277
179
  handle: (ctx, args) => {
278
- const repo = str(args, "repo", 800);
279
- if (repo === null || !ctx.who.repos.includes(repo))
180
+ const repo = args.repo;
181
+ if (!ctx.who.repos.includes(repo))
280
182
  return { ok: false, message: "not-found: that repository is not in your allowlist" };
281
183
  // The installation-wide revision stays home (slice-2 review, finding
282
184
  // 12): a coordinator cannot move queues, and the counter would tell it
@@ -285,52 +187,33 @@ const TOOLS = [
285
187
  return { ok: true, body: { repo, ...columns } };
286
188
  },
287
189
  },
288
- {
289
- name: "get_decision",
190
+ get_decision: {
290
191
  description: "One open decision in your allowlist in full: question, options with id, label, reversible, and consequence. Never the builder's recommendation.",
291
- inputSchema: { type: "object", properties: { decision: { type: "integer", minimum: 1 } }, required: ["decision"], additionalProperties: false },
292
192
  handle: (ctx, args) => {
293
- const found = decisionOver(ctx.store, ctx.who.repos, Number(args["decision"]), ctx.now);
193
+ const found = decisionOver(ctx.store, ctx.who.repos, args.decision, ctx.now);
294
194
  if (found === null)
295
195
  return { ok: false, message: "not-found: no such decision in your repositories" };
296
- ctx.readDecisions.add(Number(args["decision"]));
196
+ ctx.readDecisions.add(args.decision);
297
197
  return { ok: true, body: labelRepos(found, index => ctx.who.repos[index] ?? "") };
298
198
  },
299
199
  },
300
- ...proposeTools(),
301
- {
302
- name: "get_contract",
200
+ propose_next: proposeTool("next", "Propose moving a queued task to the front of its column. An approver confirms; a queue that moved meanwhile refuses."),
201
+ propose_reserve: proposeTool("reserve", "Propose reserving a queued task for one worker, or releasing it to the shared queue with worker null."),
202
+ propose_hold: proposeTool("hold", "Propose holding a task's next attempt, with a reason. A running attempt is never interrupted."),
203
+ propose_unhold: proposeTool("unhold", "Propose lifting the operator's own hold on a task."),
204
+ propose_scope: proposeTool("scope", "Propose rewriting a task's scope. An approver confirms the rewrite, then approves it with a password — a scope you wrote never seals under a mode."),
205
+ propose_cancel: proposeTool("cancel", "Propose cancelling a task, with a reason. The approver arms and confirms it on the task itself."),
206
+ propose_answer: proposeTool("answer", "Propose an answer to an open decision you read with get_decision, with a rationale. The approver confirms where every consequence and the builder's recommendation are shown; an irreversible option needs their explicit confirmation."),
207
+ get_contract: {
303
208
  description: "This surface's contract: what a coordinator may do, the proposal lifecycle, and the admission promise.",
304
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
305
209
  handle: () => ({ ok: true, body: { contract: CONTRACT_GUIDE } }),
306
210
  },
307
- {
308
- name: "file_proposal",
211
+ file_proposal: {
309
212
  description: "File a task proposal into one of your repositories. It stays quarantined until the operator signs its scope.",
310
- inputSchema: {
311
- type: "object",
312
- properties: {
313
- repo: { type: "string", minLength: 1, maxLength: 800 },
314
- title: { type: "string", minLength: 1, maxLength: 200 },
315
- intent: TASK_SCOPE_TEXT_SCHEMA,
316
- idempotency_key: { type: "string", minLength: 8, maxLength: 64 },
317
- deliverable: { type: "string", enum: ["branch", "report"] },
318
- },
319
- required: ["repo", "title", "idempotency_key"],
320
- additionalProperties: false,
321
- },
322
213
  handle: (ctx, args) => {
323
- const repo = str(args, "repo", 800);
324
- const title = str(args, "title", 200);
325
- const key = str(args, "idempotency_key", 64);
326
- const intent = typeof args["intent"] === "string" ? args["intent"] : undefined;
327
- const deliverable = args["deliverable"] === "report" ? "report" : args["deliverable"] === "branch" ? "branch" : undefined;
328
- if (repo === null || title === null || key === null) {
329
- return { ok: false, message: "file_proposal needs repo, title, and idempotency_key (8-64 chars)" };
330
- }
331
214
  // The token, not the pre-verified identity: filing re-authenticates
332
215
  // INSIDE its own transaction (the session's `who` is a courtesy).
333
- const outcome = fileCoordinatorProposal(ctx.store, ctx.token, { repo, title, ...(intent === undefined ? {} : { intent }), ...(deliverable === undefined ? {} : { deliverable }), idempotencyKey: key }, ctx.now);
216
+ const outcome = fileCoordinatorProposal(ctx.store, ctx.token, { repo: args.repo, title: args.title, ...(args.intent === undefined ? {} : { intent: args.intent }), ...(args.deliverable === undefined ? {} : { deliverable: args.deliverable }), idempotencyKey: args.idempotency_key }, ctx.now);
334
217
  if (!outcome.ok)
335
218
  return { ok: false, message: outcome.message };
336
219
  return {
@@ -343,31 +226,12 @@ const TOOLS = [
343
226
  };
344
227
  },
345
228
  },
346
- ];
347
- /** The propose_* tools: each writes one proposal row through the coordinator door. */
348
- function proposeTools() {
349
- const ref = { type: "string", minLength: 1, maxLength: 64 };
350
- const make = (kind, description, properties, required) => ({
351
- name: `propose_${kind}`,
352
- description,
353
- inputSchema: { type: "object", properties, required, additionalProperties: false },
354
- handle: (ctx, args) => {
355
- const outcome = proposeAsCoordinator(ctx.store, ctx.token, kind, args, ctx.now, { readDecisions: ctx.readDecisions });
356
- if (!outcome.ok)
357
- return { ok: false, message: outcome.message };
358
- return { ok: true, body: { proposal: outcome.id, kind: outcome.kind, awaiting: outcome.awaiting } };
359
- },
360
- });
361
- return [
362
- make("next", "Propose moving a queued task to the front of its column. An approver confirms; a queue that moved meanwhile refuses.", { ref }, ["ref"]),
363
- make("reserve", "Propose reserving a queued task for one worker, or releasing it to the shared queue with worker null.", { ref, worker: { type: ["string", "null"], maxLength: 60 } }, ["ref", "worker"]),
364
- make("hold", "Propose holding a task's next attempt, with a reason. A running attempt is never interrupted.", { ref, reason: { type: "string", maxLength: 200 } }, ["ref", "reason"]),
365
- make("unhold", "Propose lifting the operator's own hold on a task.", { ref }, ["ref"]),
366
- make("scope", "Propose rewriting a task's scope. An approver confirms the rewrite, then approves it with a password — a scope you wrote never seals under a mode.", { ref, goal: TASK_SCOPE_TEXT_SCHEMA, not: TASK_SCOPE_TEXT_SCHEMA, touches: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 50 } }, ["ref", "goal"]),
367
- make("cancel", "Propose cancelling a task, with a reason. The approver arms and confirms it on the task itself.", { ref, reason: { type: "string", maxLength: 200 } }, ["ref", "reason"]),
368
- make("answer", "Propose an answer to an open decision you read with get_decision, with a rationale. The approver confirms where every consequence and the builder's recommendation are shown; an irreversible option needs their explicit confirmation.", { decision: { type: "integer", minimum: 1 }, option: { type: "string", minLength: 1, maxLength: 64 }, rationale: { type: "string", maxLength: 400 } }, ["decision", "option", "rationale"]),
369
- ];
370
- }
229
+ };
230
+ /** The gateway's tools in tools/list order, each inputSchema derived from its contract. */
231
+ const TOOLS = Object.keys(GATEWAY_TOOL_INPUTS).map((name) => {
232
+ const handler = HANDLERS[name];
233
+ return { name, description: handler.description, inputSchema: toModelSchema(GATEWAY_TOOL_INPUTS[name]), handle: (ctx, args) => handler.handle(ctx, args) };
234
+ });
371
235
  function fileIdentity(path) {
372
236
  if (path === null)
373
237
  return null;
@@ -474,17 +338,21 @@ export function serveMcp(store, token, io, clock = () => new Date(), enrolled =
474
338
  error(id, -32602, "arguments must be an object");
475
339
  return;
476
340
  }
477
- const args = rawArgs;
478
- const invalid = invalidArgs(tool.inputSchema, args);
479
- if (invalid !== null) {
480
- error(id, -32602, invalid);
341
+ // The call is read by the schema tools/list advertised (review finding
342
+ // 2): unknown fields, bad types, bad enums, and bound violations are
343
+ // protocol errors (InvalidParams) naming each path, never tool refusals.
344
+ const read = parseContract(GATEWAY_TOOL_INPUTS[tool.name], rawArgs);
345
+ if (!read.ok) {
346
+ error(id, -32602, read.issues.map(one => one.line).join("; "));
481
347
  return;
482
348
  }
483
- const answered = tool.handle({ store, who: session.who, token, enrolled, now: clock(), readDecisions, ...(evidenceRoot === undefined ? {} : { evidenceRoot }) }, args);
349
+ const answered = tool.handle({ store, who: session.who, token, enrolled, now: clock(), readDecisions, ...(evidenceRoot === undefined ? {} : { evidenceRoot }) }, read.value);
484
350
  if (!answered.ok) {
485
351
  respond(id, era, { content: [{ type: "text", text: answered.message }], isError: true }, false);
486
352
  return;
487
353
  }
354
+ // Checked against its output schema before delivery; a disagreement is reported, never refused.
355
+ reportToolOutput("gateway", tool.name, GATEWAY_TOOL_OUTPUTS[tool.name], answered.body);
488
356
  respond(id, era, { content: [{ type: "text", text: JSON.stringify(answered.body) }] }, false);
489
357
  });
490
358
  };
@@ -1,9 +1,13 @@
1
1
  import type { Store } from './store.js';
2
2
  import { performSubscriptionMateRequest } from './subscription-chat.js';
3
- export declare const MEMORY_TRACE_BYTES = 40000;
3
+ import { type MemoryProposal, type Verdict } from './contracts/memory-pass.js';
4
+ export type { MemoryProposal, Verdict };
5
+ /** One session's trace, in characters (TEXT_LIMITS.memoryTrace). */
6
+ export declare const MEMORY_TRACE_BYTES: 40000;
4
7
  export declare const MEMORY_MIN_SESSIONS = 2;
5
8
  export declare const MEMORY_MAX_PROPOSALS = 6;
6
- export declare const INSTRUCTION_BUDGET_BYTES = 4000;
9
+ /** The project instructions' budget: the same limit knowledge holds them to. */
10
+ export declare const INSTRUCTION_BUDGET_BYTES: 4000;
7
11
  export declare const MEMORY_GAP_MAX_AGE_MS: number;
8
12
  export type MemorySource = {
9
13
  id: string;
@@ -37,26 +41,6 @@ export type MemorySurface = {
37
41
  };
38
42
  /** The always-loaded surface: instruction lines with stable ids, active decisions with theirs. Its hash keys the analysis cache. */
39
43
  export declare function memorySurface(store: Store, repo: string, actor: string): MemorySurface;
40
- export type Verdict = {
41
- positive: {
42
- instruction: string;
43
- effect: string;
44
- quote: string;
45
- }[];
46
- negative: {
47
- instruction: string;
48
- effect: string;
49
- class: 'harm' | 'non-compliance' | 'irrelevant';
50
- quote: string;
51
- }[];
52
- gaps: {
53
- mistake: string;
54
- proposedInstruction: string;
55
- domain: 'project' | 'orchestration';
56
- quote: string;
57
- matchesGap?: string;
58
- }[];
59
- };
60
44
  export type MemoryAnalyzer = (input: {
61
45
  trace: string;
62
46
  surface: MemorySurface;
@@ -84,8 +68,17 @@ export declare function defaultAnalyzer(store: Store, options: {
84
68
  timeoutMs?: number;
85
69
  runner?: Parameters<typeof performSubscriptionMateRequest>[1];
86
70
  }): MemoryAnalyzer;
87
- /** Parse strictly and keep only claims whose quote is really in the trace. */
88
- export declare function parseVerdict(text: string, trace: string, surface: MemorySurface): Verdict | null;
71
+ /**
72
+ * Read the analyser's answer with the tolerant parse-side schema, then keep only claims whose quote
73
+ * is really in the trace and, for an instruction's effect, whose id is on the surface audited.
74
+ */
75
+ export declare function parseVerdict(text: string, trace: string, surface: MemorySurface): {
76
+ ok: true;
77
+ verdict: Verdict;
78
+ } | {
79
+ ok: false;
80
+ problem: string;
81
+ };
89
82
  export type PassReport = {
90
83
  repo: string;
91
84
  surface: string;
@@ -111,24 +104,6 @@ export declare function runMemoryPass(store: Store, args: {
111
104
  local?: boolean;
112
105
  maxSessions?: number;
113
106
  }, now?: Date): Promise<PassReport>;
114
- export type MemoryProposal = {
115
- id: number;
116
- repo: string;
117
- kind: 'instruction-add' | 'instruction-remove' | 'decision-add';
118
- fingerprint: string;
119
- title: string;
120
- rationale: string;
121
- beforeText: string | null;
122
- afterText: string;
123
- evidence: {
124
- session: string;
125
- quote: string;
126
- }[];
127
- sessions: number;
128
- status: string;
129
- createdAt: string;
130
- surface: string;
131
- };
132
107
  /**
133
108
  * Turn corroborated gaps and harm into a few proposals: an addition needs
134
109
  * the gap seen in at least two distinct sessions; a removal needs harm from