@akira-tl/forgerelay 0.3.4 → 0.3.6

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 (99) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/capabilities/artifacts-review/GUIDE.md +2 -4
  3. package/dist/artifact-tools.js +0 -128
  4. package/dist/capabilities.js +2 -5
  5. package/dist/db/migrations.js +19 -0
  6. package/dist/db/schema.js +9 -0
  7. package/dist/hooks.js +12 -0
  8. package/dist/mcp/server-instructions.js +4 -9
  9. package/dist/review-checkpoints.js +4 -4
  10. package/dist/server.js +154 -280
  11. package/dist/ui/.vite/manifest.json +280 -280
  12. package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
  13. package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
  14. package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
  15. package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
  16. package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
  17. package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
  18. package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
  19. package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
  20. package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
  21. package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
  22. package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
  23. package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
  24. package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
  25. package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
  26. package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
  27. package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
  28. package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
  29. package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
  30. package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
  31. package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
  32. package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
  33. package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
  34. package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
  35. package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
  36. package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
  37. package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
  38. package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
  39. package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
  40. package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
  41. package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
  42. package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
  43. package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
  44. package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
  45. package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
  46. package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
  47. package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
  48. package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
  49. package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
  50. package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
  51. package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
  52. package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
  53. package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
  54. package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
  55. package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
  56. package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
  57. package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
  58. package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
  59. package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
  60. package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
  61. package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
  62. package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
  63. package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
  64. package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
  65. package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
  66. package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
  67. package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
  68. package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
  69. package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
  70. package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
  71. package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
  72. package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
  73. package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
  74. package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
  75. package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
  76. package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
  77. package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
  78. package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
  79. package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
  80. package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
  81. package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
  82. package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
  83. package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
  84. package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
  85. package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
  86. package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
  87. package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
  88. package/dist/ui/workspace-app.html +1 -1
  89. package/dist/workspace-store.js +52 -1
  90. package/dist/workspaces.js +209 -29
  91. package/docs/artifact-exchange.md +4 -3
  92. package/docs/chatgpt-coding-workflow.md +70 -11
  93. package/docs/configuration.md +41 -16
  94. package/docs/debugging.md +2 -2
  95. package/docs/gotchas.md +3 -3
  96. package/docs/roadmap.md +12 -1
  97. package/docs/security.md +3 -2
  98. package/package.json +1 -1
  99. package/scripts/debug/accept.mjs +113 -15
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.6] - 2026-08-10
8
+
9
+ ### Added
10
+
11
+ - `open_workspace(action="list")` now provides paginated logical-workspace inventory without adding a tenth Core tool. Inventory entries include a compact `project/workspaceId` label, persisted status, derived lifecycle state, checkout/worktree backing metadata, creation/last-used timestamps, idle duration, root validity, current-conversation selection, and filters for workspace ID, status, state, mode, root, and stale-only views.
12
+
13
+ ### Changed
14
+
15
+ - Workspace bootstrap context is now deduplicated by conversation scope, canonical workspace target, and a content fingerprint instead of by logical `workspaceId`. `context="auto"` remains the default, `context="full"` forces a refresh, and `context="none"` opens or resumes a workspace without returning the full AGENTS/Skills/guide/profile bootstrap.
16
+ - Context-delivery state is persisted independently from logical-workspace selection, so switching or closing one logical handle does not make the same conversation forget already-delivered project context. Changes to loaded instruction contents or relevant Skill, guide, profile, diagnostic, or nested-instruction metadata change the fingerprint and cause `auto` to deliver the refreshed context again.
17
+ - Workspace inventory is read-only with respect to workspace activity timestamps and runs the existing idle-session GC before listing. Persisted `status="active"` continues to mean the session has not been explicitly closed, while the derived `state` distinguishes currently active, stale-but-valid, invalid/missing-root, and closed records.
18
+
19
+ ## [0.3.5] - 2026-08-10
20
+
21
+ ### Changed
22
+
23
+ - Regular `minimal` and `full` MCP modes now expose the same canonical 9-tool surface: `open_workspace`, `capability`, `close_workspace`, `read`, `write`, `edit`, `rename`, `delete`, and `bash`. Search and directory inspection use shell commands through `bash`; `full` remains only as a configuration-compatibility value.
24
+ - `review.changes` and `artifact.download` are now Capability Gateway-only workflows. Their dedicated top-level compatibility aliases have been removed, while review cards identify themselves as `capability` results with `capabilityName="review.changes"`.
25
+ - Capability fingerprints now report semantic runtime capabilities such as `review.changes` instead of implementation-shaped search/review tool names, and current Guides/docs/bootstrap instructions use the final Core Surface + Capability Gateway model.
26
+
27
+ ### Removed
28
+
29
+ - Removed the regular dedicated `grep`, `glob`, `ls`, aggregate-review, and native-artifact MCP adapters after their canonical workflows moved to `bash` or the Capability Gateway.
30
+
31
+ ### Fixed
32
+
33
+ - Successful `report: true` lifecycle Hook reports are now mirrored into the model-readable structured result as well as MCP text content, so Hosts that surface only structured tool results still expose the Hook outcome to the Agent.
34
+
7
35
  ## [0.3.4] - 2026-08-10
8
36
 
9
37
  ### Changed
@@ -12,8 +12,6 @@ Native artifact download 默认关闭,且当前安全发布实现主要面向
12
12
  2. 调用 `capability`,`name="artifact.download"`、`action="run"`,把 Host 原样提供的 native `file` 值放在 Gateway 顶层 `file`,并在 `arguments.path` 传 workspace-relative 目标路径;
13
13
  3. 成功后使用返回的 normalized relative path 配合普通 `read` / `rename` / `delete` / shell 等工具继续处理。
14
14
 
15
- 0.3.3 仍保留 `download_artifact` 作为迁移期 compatibility alias,但新的 Agent workflow 不应把它视为长期 public surface。
16
-
17
15
  `artifact.download` 会创建缺失的父目录,但拒绝覆盖已有目标。不要把 native file 值改造成:
18
16
 
19
17
  - 任意 URL 字符串;
@@ -35,10 +33,10 @@ Native artifact download 默认关闭,且当前安全发布实现主要面向
35
33
 
36
34
  Plain MCP Host 可以忽略这些 UI metadata;不要把 UI 是否显示当成文件操作是否成功的判据。
37
35
 
38
- 当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。0.3.3 仍保留 `show_changes` compatibility alias,但它和 `review.changes` 共用同一套 checkpoint,不是两套 review 状态。
36
+ 当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。
39
37
 
40
38
  `review.changes` 使用 Git-backed review checkpoint。它按 workspace 跟踪 open/baseline 状态,展示自上次 review checkpoint 以来的 coherent diff,并在成功 review 后推进 baseline。当前版本要求可用的 Git workspace;checkpoint 缺失或 root 不匹配时会明确失败或使用受控 fallback,而不是凭空重建历史。
41
39
 
42
40
  ## Capability ownership
43
41
 
44
- Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list` 只负责暴露稳定 Gateway 与当前兼容 aliases;真正可用的低频能力以当前 workspace 的 Capability catalog 为准。本指南提供流程和边界,不额外创造隐藏执行入口。
42
+ Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list` 只暴露稳定 `capability` Gateway;真正可用的低频能力以当前 workspace 的 Capability catalog 为准。本指南提供流程和边界,不额外创造隐藏执行入口。
@@ -2,18 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import { link, lstat, mkdir, open, readdir, unlink, } from "node:fs/promises";
4
4
  import { isAbsolute, join, normalize, sep } from "node:path";
5
- import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
6
- import * as z from "zod/v4";
7
5
  import { ArtifactError } from "./artifact-error.js";
8
- import { runToolWithHooks } from "./hooks.js";
9
- import { describeIncomingArtifactValue, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
10
- import { logEvent, workspaceLogLabel } from "./logger.js";
11
- const ARTIFACT_WRITE_ANNOTATIONS = {
12
- readOnlyHint: false,
13
- destructiveHint: false,
14
- idempotentHint: false,
15
- openWorldHint: true,
16
- };
17
6
  const NO_FOLLOW = fsConstants.O_NOFOLLOW ?? 0;
18
7
  const DIRECTORY_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0) | NO_FOLLOW;
19
8
  const PARTIAL_PREFIX = ".forgerelay-download-";
@@ -21,64 +10,9 @@ const PARTIAL_SUFFIX = ".partial";
21
10
  const STALE_PARTIAL_AGE_MS = 24 * 60 * 60 * 1_000;
22
11
  const MAX_STALE_PARTIAL_CLEANUP = 32;
23
12
  const ARTIFACT_DOWNLOAD_PLATFORMS = new Set(["linux"]);
24
- const openAIFileReferenceInputSchema = z.strictObject({
25
- download_url: z.string(),
26
- file_id: z.string(),
27
- mime_type: z.string().nullable().optional(),
28
- file_name: z.string().nullable().optional(),
29
- name: z.string().nullable().optional(),
30
- size: z.number().int().nonnegative().nullable().optional(),
31
- });
32
13
  export function isArtifactDownloadSupportedPlatform(platform = process.platform) {
33
14
  return ARTIFACT_DOWNLOAD_PLATFORMS.has(platform);
34
15
  }
35
- export function registerArtifactTools(server, { config, workspaces, hooks, incomingArtifactAdapters = [], incomingArtifactRegistry, }) {
36
- const incomingRegistry = incomingArtifactRegistry
37
- ?? new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
38
- registerAppTool(server, "download_artifact", {
39
- title: "Download attached or generated file",
40
- description: "Stream one MCP-host-provided native file to a requested relative path inside an already-open workspace. Existing destinations, arbitrary URLs, absolute paths, traversal, symlinked parents, local source paths, and malformed file objects are rejected.",
41
- inputSchema: {
42
- file: openAIFileReferenceInputSchema.describe("Native file value authorized and supplied by the MCP host."),
43
- workspaceId: z.string().min(1).describe("Workspace identifier returned by open_workspace."),
44
- path: z.string().min(1).describe("Relative destination path inside the selected workspace. The destination must not already exist."),
45
- },
46
- outputSchema: {
47
- path: z.string(),
48
- },
49
- _meta: { "openai/fileParams": ["file"] },
50
- annotations: ARTIFACT_WRITE_ANNOTATIONS,
51
- }, async (input) => {
52
- const workspace = workspaces.getWorkspace(input.workspaceId);
53
- return runToolWithHooks(hooks, {
54
- tool: "download_artifact",
55
- invocation: {
56
- workspaceId: workspace.id,
57
- workspaceRoot: workspace.root,
58
- workspaceMode: workspace.mode,
59
- sourceRoot: workspace.sourceRoot,
60
- },
61
- payload: { path: input.path },
62
- changedPaths: (result) => [result.structuredContent.path],
63
- operation: () => executeArtifactTool(config, input, {
64
- workspace: workspaceLogLabel(workspace.root, workspace.id),
65
- }, async () => {
66
- const downloaded = await downloadIncomingArtifact({
67
- registry: incomingRegistry,
68
- workspaceId: workspace.id,
69
- workspaceRoot: workspace.root,
70
- maxFileBytes: config.artifactMaxFileBytes,
71
- file: input.file,
72
- path: input.path,
73
- });
74
- return {
75
- publicResult: { path: downloaded.path },
76
- logResult: downloaded,
77
- };
78
- }),
79
- });
80
- });
81
- }
82
16
  /**
83
17
  * Stream a trusted native file directly into one already-open workspace.
84
18
  *
@@ -159,53 +93,6 @@ export async function downloadIncomingArtifact({ registry, workspaceId, workspac
159
93
  await workspaceHandle?.close().catch(() => undefined);
160
94
  }
161
95
  }
162
- export function artifactToolLogFields(input) {
163
- return {
164
- fileProvided: input.file !== undefined,
165
- fileReferenceShape: describeIncomingArtifactValue(input.file),
166
- downloadUrlHostname: incomingFileDownloadHostname(input.file),
167
- workspaceId: input.workspaceId,
168
- path: input.path,
169
- };
170
- }
171
- async function executeArtifactTool(config, input, logContext, operation) {
172
- const startedAt = performance.now();
173
- try {
174
- const { publicResult, logResult } = await operation();
175
- if (config.logging.toolCalls) {
176
- logEvent(config.logging, "info", "artifact_tool_call", {
177
- tool: "download_artifact",
178
- ...artifactToolLogFields(input),
179
- ...logContext,
180
- path: logResult.path,
181
- size: logResult.size,
182
- sha256: logResult.sha256,
183
- success: true,
184
- durationMs: Math.round(performance.now() - startedAt),
185
- });
186
- }
187
- return artifactToolResponse(publicResult);
188
- }
189
- catch (error) {
190
- if (config.logging.toolCalls) {
191
- logEvent(config.logging, "warn", "artifact_tool_call", {
192
- tool: "download_artifact",
193
- ...artifactToolLogFields(input),
194
- ...logContext,
195
- success: false,
196
- errorCode: error instanceof ArtifactError ? error.code : "internal_error",
197
- durationMs: Math.round(performance.now() - startedAt),
198
- });
199
- }
200
- throw error;
201
- }
202
- }
203
- function artifactToolResponse(result) {
204
- return {
205
- content: [{ type: "text", text: JSON.stringify(result) }],
206
- structuredContent: result,
207
- };
208
- }
209
96
  async function openDirectoryNoFollow(path, code, message) {
210
97
  let handle;
211
98
  try {
@@ -356,21 +243,6 @@ async function writeAll(handle, buffer, position) {
356
243
  offset += bytesWritten;
357
244
  }
358
245
  }
359
- function incomingFileDownloadHostname(value) {
360
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
361
- return undefined;
362
- }
363
- const rawUrl = value.download_url;
364
- if (typeof rawUrl !== "string")
365
- return undefined;
366
- try {
367
- const hostname = new URL(rawUrl).hostname.toLowerCase();
368
- return hostname.length > 0 && hostname.length <= 253 ? hostname : undefined;
369
- }
370
- catch {
371
- return undefined;
372
- }
373
- }
374
246
  function incomingStreamChunk(value) {
375
247
  if (Buffer.isBuffer(value))
376
248
  return value;
@@ -21,7 +21,7 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
21
21
  {
22
22
  name: "artifacts-review",
23
23
  description: "Native artifact transfer and aggregate change review.",
24
- whenToRead: "Read for host-provided files or show_changes.",
24
+ whenToRead: "Read for host-provided files or aggregate change review.",
25
25
  enabled: (config) => config.artifactsEnabled || config.widgets === "changes",
26
26
  },
27
27
  {
@@ -76,9 +76,6 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
76
76
  "hooks.lifecycle",
77
77
  "capability-guides.read",
78
78
  ];
79
- if (config.toolMode === "full") {
80
- capabilities.push("inspection.search-tools");
81
- }
82
79
  if (config.subagents) {
83
80
  capabilities.push("subagent.profiles");
84
81
  }
@@ -89,7 +86,7 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
89
86
  capabilities.push("ui.mcp-app");
90
87
  }
91
88
  if (config.widgets === "changes") {
92
- capabilities.push("review.show-changes");
89
+ capabilities.push("review.changes");
93
90
  }
94
91
  return {
95
92
  version,
@@ -29,6 +29,11 @@ const migrations = [
29
29
  name: "local-agent-hook-reports",
30
30
  up: migrateLocalAgentHookReports,
31
31
  },
32
+ {
33
+ version: 7,
34
+ name: "workspace-context-deliveries",
35
+ up: migrateWorkspaceContextDeliveries,
36
+ },
32
37
  ];
33
38
  export function migrateDatabase(sqlite) {
34
39
  const migrate = sqlite.transaction(() => {
@@ -189,6 +194,20 @@ function migrateWorkspaceWorktreeBranches(sqlite) {
189
194
  function migrateLocalAgentHookReports(sqlite) {
190
195
  addColumnIfMissing(sqlite, "local_agent_sessions", "hook_reports_json", "text");
191
196
  }
197
+ function migrateWorkspaceContextDeliveries(sqlite) {
198
+ sqlite.exec(`
199
+ create table if not exists workspace_context_deliveries (
200
+ conversation_scope_id text not null,
201
+ target_key text not null,
202
+ context_fingerprint text not null,
203
+ delivered_at text not null,
204
+ primary key (conversation_scope_id, target_key)
205
+ );
206
+
207
+ create index if not exists workspace_context_deliveries_delivered_idx
208
+ on workspace_context_deliveries(delivered_at desc);
209
+ `);
210
+ }
192
211
  function addColumnIfMissing(sqlite, table, column, definition) {
193
212
  const columns = sqlite.prepare(`pragma table_info(${table})`).all();
194
213
  if (columns.some((existingColumn) => existingColumn.name === column))
package/dist/db/schema.js CHANGED
@@ -41,6 +41,15 @@ export const workspaceConversationBindings = sqliteTable("workspace_conversation
41
41
  primaryKey({ columns: [table.conversationScopeId, table.targetKey] }),
42
42
  index("workspace_conversation_bindings_workspace_idx").on(table.workspaceSessionId),
43
43
  ]);
44
+ export const workspaceContextDeliveries = sqliteTable("workspace_context_deliveries", {
45
+ conversationScopeId: text("conversation_scope_id").notNull(),
46
+ targetKey: text("target_key").notNull(),
47
+ contextFingerprint: text("context_fingerprint").notNull(),
48
+ deliveredAt: text("delivered_at").notNull(),
49
+ }, (table) => [
50
+ primaryKey({ columns: [table.conversationScopeId, table.targetKey] }),
51
+ index("workspace_context_deliveries_delivered_idx").on(table.deliveredAt),
52
+ ]);
44
53
  export const oauthClients = sqliteTable("oauth_clients", {
45
54
  clientId: text("client_id").primaryKey(),
46
55
  clientJson: text("client_json").notNull(),
package/dist/hooks.js CHANGED
@@ -152,6 +152,10 @@ export function attachHookReports(result, executions) {
152
152
  const summary = formatVisibleHookReports(executions);
153
153
  if (!summary || !isRecord(result) || !Array.isArray(result.content))
154
154
  return result;
155
+ const structuredContent = isRecord(result.structuredContent) ? result.structuredContent : undefined;
156
+ const structuredResult = typeof structuredContent?.result === "string"
157
+ ? structuredContent.result
158
+ : undefined;
155
159
  return {
156
160
  ...result,
157
161
  content: [
@@ -161,6 +165,14 @@ export function attachHookReports(result, executions) {
161
165
  text: summary,
162
166
  },
163
167
  ],
168
+ ...(structuredContent && structuredResult !== undefined
169
+ ? {
170
+ structuredContent: {
171
+ ...structuredContent,
172
+ result: `${structuredResult}\n\n${summary}`,
173
+ },
174
+ }
175
+ : {}),
164
176
  };
165
177
  }
166
178
  function appendHookReportsToError(error, executions) {
@@ -6,9 +6,6 @@ export const toolNames = {
6
6
  edit: "edit",
7
7
  rename: "rename",
8
8
  delete: "delete",
9
- grep: "grep",
10
- glob: "glob",
11
- ls: "ls",
12
9
  shell: "bash",
13
10
  writeStdin: "write_stdin",
14
11
  capability: "capability",
@@ -23,9 +20,9 @@ export function buildToolDescriptions(config) {
23
20
  const skillCapability = config.skillsEnabled
24
21
  ? " Advertised skill paths may also be outside the workspace."
25
22
  : "";
26
- const shellSurface = config.toolMode === "minimal"
27
- ? ` In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled, so shell commands may be used for equivalent search and directory inspection.`
28
- : "";
23
+ const shellSurface = config.toolMode === "codex"
24
+ ? ""
25
+ : " Use shell commands for search and directory inspection instead of dedicated MCP search tools.";
29
26
  return {
30
27
  read: `Read a file inside an open workspace or the OS temp directory. Instruction files and advertised capability guides returned by ${toolNames.openWorkspace} are also readable when applicable.${skillCapability} Only advertised entry files and files under already-loaded advertised directories are readable outside the normal roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
31
28
  write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
@@ -62,9 +59,7 @@ function defaultWorkflowInstructions(config) {
62
59
  if (config.toolMode === "codex") {
63
60
  return `Use ${toolNames.read} for direct file reads, ${toolNames.rename} and ${toolNames.delete} for direct path moves or removals, apply_patch for content modifications, exec_command for inspection, tests, builds, and other commands, and ${toolNames.writeStdin} to poll or interact with running processes.`;
64
61
  }
65
- const inspection = config.toolMode === "full"
66
- ? `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection.`
67
- : `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
62
+ const inspection = `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
68
63
  return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell. If ${toolNames.shell} returns a running process with a processId, call ${toolNames.shell} again with action=\"process\" when you need to poll, wait, interact, resize, or interrupt it; otherwise you may continue other work and consume its completion notice from a later tool result.`);
69
64
  }
70
65
  function joinInstructions(...parts) {
@@ -39,19 +39,19 @@ export function createReviewCheckpointManager() {
39
39
  }
40
40
  assertWorkspaceRoot(state, workspaceId, root);
41
41
  if (!state?.gitRoot) {
42
- throw new Error(state?.diagnostic ?? "show_changes requires a Git workspace in this version.");
42
+ throw new Error(state?.diagnostic ?? "review.changes requires a Git workspace in this version.");
43
43
  }
44
44
  let effectiveSince = since;
45
45
  let usedWorkspaceOpenFallback = false;
46
46
  if (since === "last_shown" && !state.baselineRefAvailable) {
47
47
  if (!state.openRefAvailable) {
48
- throw new Error("Review checkpoints are missing; show_changes cannot reconstruct that history safely.");
48
+ throw new Error("Review checkpoints are missing; review.changes cannot reconstruct that history safely.");
49
49
  }
50
50
  effectiveSince = "workspace_open";
51
51
  usedWorkspaceOpenFallback = true;
52
52
  }
53
53
  else if (since === "workspace_open" && !state.openRefAvailable) {
54
- throw new Error("The workspace-open review checkpoint is missing; show_changes cannot reconstruct that history safely.");
54
+ throw new Error("The workspace-open review checkpoint is missing; review.changes cannot reconstruct that history safely.");
55
55
  }
56
56
  const baselineRef = effectiveSince === "workspace_open" ? state.openRef : state.baselineRef;
57
57
  const baseline = (await git(state.gitRoot, ["rev-parse", "--verify", `${baselineRef}^{commit}`])).stdout.trim();
@@ -98,7 +98,7 @@ async function initializeWorkspaceState(states, workspaceId, root) {
98
98
  try {
99
99
  const eligibility = await getGitEligibility(root);
100
100
  if (!eligibility.ok || !eligibility.gitRoot) {
101
- state.diagnostic = eligibility.message ?? "show_changes requires a Git workspace in this version.";
101
+ state.diagnostic = eligibility.message ?? "review.changes requires a Git workspace in this version.";
102
102
  return;
103
103
  }
104
104
  const [openCommit, baselineCommit] = await Promise.all([