@akira-tl/forgerelay 0.8.10 → 0.9.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.0] - 2026-09-04
8
+
9
+ ### Added
10
+
11
+ - Added bounded read-only managed-worktree recovery diagnostics to `open_workspace` inventory and inspection, distinguishing healthy, recoverable, and manual-intervention states across missing backing/source/branches, stale Git worktree registration, and branch mismatches. Relayed inspection carries the sanitized diagnostic facts from the Execution ForgeRelay without mutating Git state.
12
+
13
+ ### Changed
14
+
15
+ - Aligned current roadmap and product documentation with already-shipped first-class Subagent delegation, Hook-backed managed-worktree close verification, and explicitly authorized live managed TypeScript/Pyright installation. The 0.9 line is now defined as recovery diagnostics/repair, Workspace checkpoints/restore, and owner-facing retention maintenance.
16
+ - Added managed-worktree recovery/inventory tests to the regular release test suite so the diagnostic contract is exercised on every cloud platform.
17
+
18
+ ### Fixed
19
+
20
+ - Canonicalized managed-worktree registration paths through their real filesystem location, preventing macOS `/var/...` and `/private/var/...` aliases (and equivalent symlink aliases) from being misreported as missing Git worktree registration.
21
+
7
22
  ## [0.8.10] - 2026-09-03
8
23
 
9
24
  ### Added
package/README.md CHANGED
@@ -165,10 +165,13 @@ symbols, workspace symbols, and diagnostics. Results use ForgeRelay-owned normal
165
165
  locations, ranges, symbols, hover content, and diagnostic shapes rather than raw LSP
166
166
  wire unions.
167
167
 
168
- Language servers remain external dependencies. ForgeRelay can discover
169
- `typescript-language-server`, `pyright-langserver`, `rust-analyzer`, `gopls`, and
170
- `clangd` when they already exist on `PATH`, or use structured project/global
171
- configuration, but it never installs a server automatically. See
168
+ Language servers are never installed automatically or without user authorization.
169
+ ForgeRelay can discover configured or `PATH`-available servers, and `forgerelay init`
170
+ can optionally enable Agent-managed installation of TypeScript/JavaScript and Pyright
171
+ inside ForgeRelay's private config directory. That permission is disabled by default;
172
+ a successful managed install becomes available to the same running ForgeRelay process
173
+ on the next semantic request without a restart. `rust-analyzer`, `gopls`, and `clangd`
174
+ remain external toolchain/system installations. See
172
175
  [Configuration Reference](docs/configuration.md#lsp-code-intelligence) and
173
176
  [`examples/language-servers.json`](examples/language-servers.json).
174
177
 
@@ -297,13 +300,13 @@ forgerelay doctor
297
300
 
298
301
  ## Where ForgeRelay is going
299
302
 
300
- With LSP code intelligence established in the 0.4 line, the next additions remain
301
- focused on making the local execution layer more useful, not on turning ForgeRelay
302
- into another all-in-one agent framework:
303
+ With persistent Workspace lifecycle, first-class Subagent delegation, managed-LSP
304
+ support, and worktree-close verification already shipped, the 0.9 line focuses on
305
+ Workspace recovery and history without expanding the canonical Core tool surface:
303
306
 
304
- 1. first-class MCP subagent delegation;
305
- 2. stronger worktree verification and recovery;
306
- 3. checkpoint/rewind and retention improvements.
307
+ 1. read-only managed-worktree recovery diagnostics, followed by safe repair/cleanup;
308
+ 2. persistent Workspace checkpoints and concurrency-safe restore;
309
+ 3. owner-facing retention inspection and explicitly authorized maintenance.
307
310
 
308
311
  ForgeRelay does not plan to add its own shell sandbox, long-term memory system,
309
312
  or plugin marketplace. Conversation, planning, web access, and other host-native
@@ -94,6 +94,26 @@ export const workspaceSubagentProviderOutputSchema = z.object({
94
94
  export const workspaceAvailableAgentsFileOutputSchema = z.object({
95
95
  path: z.string(),
96
96
  });
97
+ export const managedWorktreeRecoveryOutputSchema = z.object({
98
+ classification: z.enum(["healthy", "recoverable", "manual-intervention"]),
99
+ conditions: z.array(z.enum([
100
+ "backing-missing",
101
+ "managed-branch-missing",
102
+ "git-registration-stale",
103
+ "git-registration-missing",
104
+ "git-registration-unavailable",
105
+ "branch-mismatch",
106
+ "source-missing",
107
+ "source-unavailable",
108
+ "target-branch-missing",
109
+ ])),
110
+ backing: z.enum(["present", "missing"]),
111
+ source: z.enum(["available", "missing", "unavailable"]),
112
+ gitRegistration: z.enum(["registered", "stale", "missing", "unavailable"]),
113
+ managedBranch: z.enum(["present", "missing", "unknown"]),
114
+ targetBranch: z.enum(["present", "missing", "unknown"]),
115
+ backingBranch: z.enum(["matching", "mismatched", "unavailable"]),
116
+ });
97
117
  export const workspaceInventoryEntryOutputSchema = z.object({
98
118
  label: z.string(),
99
119
  workspaceId: z.string(),
@@ -109,6 +129,7 @@ export const workspaceInventoryEntryOutputSchema = z.object({
109
129
  lastUsedAt: z.string(),
110
130
  idleMs: z.number().nonnegative(),
111
131
  rootValid: z.boolean(),
132
+ recovery: managedWorktreeRecoveryOutputSchema.optional(),
112
133
  current: z.boolean(),
113
134
  });
114
135
  export const workspaceInventorySummaryOutputSchema = z.object({
@@ -167,6 +188,7 @@ export const workspaceInspectionOutputSchema = z.union([
167
188
  lastUsedAt: z.string(),
168
189
  idleMs: z.number().nonnegative(),
169
190
  rootValid: z.boolean(),
191
+ recovery: managedWorktreeRecoveryOutputSchema.optional(),
170
192
  taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
171
193
  }),
172
194
  z.object({
@@ -186,6 +208,7 @@ export const workspaceInspectionOutputSchema = z.union([
186
208
  lastUsedAt: z.string().optional(),
187
209
  idleMs: z.number().nonnegative().optional(),
188
210
  rootValid: z.boolean().optional(),
211
+ recovery: managedWorktreeRecoveryOutputSchema.optional(),
189
212
  taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
190
213
  relay: z.string(),
191
214
  executionLocation: z.string(),
@@ -0,0 +1,189 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import { platform } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { assertAllowedPath } from "../../mcp/filesystem/roots.js";
5
+ import { git } from "./git.js";
6
+ export async function inspectManagedWorktreeRecovery(session, config) {
7
+ if (session.status !== "active" ||
8
+ session.mode !== "worktree" ||
9
+ !session.managed)
10
+ return undefined;
11
+ const backing = await directoryState(session.root, [config.worktreeRoot]);
12
+ const source = await sourceState(session.sourceRoot, config.allowedRoots);
13
+ const conditions = [];
14
+ if (source === "missing")
15
+ conditions.push("source-missing");
16
+ else if (source === "unavailable")
17
+ conditions.push("source-unavailable");
18
+ if (backing === "missing")
19
+ conditions.push("backing-missing");
20
+ if (source !== "available" || !session.sourceRoot) {
21
+ return {
22
+ classification: "manual-intervention",
23
+ conditions,
24
+ backing,
25
+ source,
26
+ gitRegistration: "unavailable",
27
+ managedBranch: "unknown",
28
+ targetBranch: "unknown",
29
+ backingBranch: "unavailable",
30
+ };
31
+ }
32
+ const sourceRoot = assertAllowedPath(session.sourceRoot, config.allowedRoots);
33
+ const [managedBranch, targetBranch, registration] = await Promise.all([
34
+ branchState(sourceRoot, session.branch),
35
+ branchState(sourceRoot, session.targetBranch),
36
+ registrationState(sourceRoot, session.root),
37
+ ]);
38
+ if (managedBranch === "missing")
39
+ conditions.push("managed-branch-missing");
40
+ if (targetBranch === "missing")
41
+ conditions.push("target-branch-missing");
42
+ if (registration === "stale")
43
+ conditions.push("git-registration-stale");
44
+ else if (registration === "missing")
45
+ conditions.push("git-registration-missing");
46
+ else if (registration === "unavailable")
47
+ conditions.push("git-registration-unavailable");
48
+ const backingBranch = backing === "present"
49
+ ? await backingBranchState(session.root, session.branch)
50
+ : "unavailable";
51
+ if (backingBranch === "mismatched")
52
+ conditions.push("branch-mismatch");
53
+ const recoverable = backing === "missing" &&
54
+ managedBranch === "present" &&
55
+ targetBranch === "present" &&
56
+ (registration === "stale" || registration === "missing") &&
57
+ conditions.every((condition) => condition === "backing-missing" ||
58
+ condition === "git-registration-stale" ||
59
+ condition === "git-registration-missing");
60
+ return {
61
+ classification: conditions.length === 0
62
+ ? "healthy"
63
+ : recoverable
64
+ ? "recoverable"
65
+ : "manual-intervention",
66
+ conditions,
67
+ backing,
68
+ source,
69
+ gitRegistration: registration,
70
+ managedBranch,
71
+ targetBranch,
72
+ backingBranch,
73
+ };
74
+ }
75
+ async function directoryState(path, allowedRoots) {
76
+ const allowedPath = assertAllowedPath(path, allowedRoots);
77
+ try {
78
+ return (await stat(allowedPath)).isDirectory() ? "present" : "missing";
79
+ }
80
+ catch (error) {
81
+ if (isMissingPath(error))
82
+ return "missing";
83
+ throw error;
84
+ }
85
+ }
86
+ async function sourceState(sourceRoot, allowedRoots) {
87
+ if (!sourceRoot)
88
+ return "unavailable";
89
+ let sourcePath;
90
+ try {
91
+ sourcePath = assertAllowedPath(sourceRoot, allowedRoots);
92
+ }
93
+ catch {
94
+ return "unavailable";
95
+ }
96
+ try {
97
+ if (!(await stat(sourcePath)).isDirectory())
98
+ return "missing";
99
+ }
100
+ catch (error) {
101
+ return isMissingPath(error) ? "missing" : "unavailable";
102
+ }
103
+ try {
104
+ const gitRoot = (await git(sourcePath, ["rev-parse", "--show-toplevel"])).stdout.trim();
105
+ const [canonicalGitRoot, canonicalSource] = await Promise.all([realpath(gitRoot), realpath(sourcePath)]);
106
+ return pathKey(canonicalGitRoot) === pathKey(canonicalSource) ? "available" : "unavailable";
107
+ }
108
+ catch {
109
+ return "unavailable";
110
+ }
111
+ }
112
+ async function branchState(sourceRoot, branch) {
113
+ if (!branch)
114
+ return "missing";
115
+ try {
116
+ await git(sourceRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
117
+ return "present";
118
+ }
119
+ catch {
120
+ return "missing";
121
+ }
122
+ }
123
+ async function registrationState(sourceRoot, worktreePath) {
124
+ let registrations;
125
+ try {
126
+ registrations = parseWorktreeRegistrations((await git(sourceRoot, ["worktree", "list", "--porcelain"])).stdout);
127
+ }
128
+ catch {
129
+ return "unavailable";
130
+ }
131
+ const worktreeKey = await registrationPathKey(worktreePath);
132
+ for (const registration of registrations) {
133
+ if (await registrationPathKey(registration.path) !== worktreeKey)
134
+ continue;
135
+ return registration.prunable ? "stale" : "registered";
136
+ }
137
+ return "missing";
138
+ }
139
+ async function backingBranchState(worktreeRoot, expectedBranch) {
140
+ if (!expectedBranch)
141
+ return "mismatched";
142
+ try {
143
+ const actual = (await git(worktreeRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"])).stdout.trim();
144
+ return actual === expectedBranch ? "matching" : "mismatched";
145
+ }
146
+ catch {
147
+ return "mismatched";
148
+ }
149
+ }
150
+ function parseWorktreeRegistrations(output) {
151
+ return output
152
+ .trim()
153
+ .split(/\n\n+/)
154
+ .map((block) => {
155
+ const lines = block.split("\n");
156
+ const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
157
+ if (!path)
158
+ return undefined;
159
+ return {
160
+ path,
161
+ prunable: lines.some((line) => line.startsWith("prunable ")),
162
+ };
163
+ })
164
+ .filter((entry) => entry !== undefined);
165
+ }
166
+ async function registrationPathKey(path) {
167
+ const resolved = resolve(path);
168
+ try {
169
+ return pathKey(await realpath(resolved));
170
+ }
171
+ catch (error) {
172
+ if (!isMissingPath(error))
173
+ return pathKey(resolved);
174
+ try {
175
+ return pathKey(join(await realpath(dirname(resolved)), basename(resolved)));
176
+ }
177
+ catch {
178
+ return pathKey(resolved);
179
+ }
180
+ }
181
+ }
182
+ function pathKey(path) {
183
+ const resolved = resolve(path);
184
+ return platform() === "win32" ? resolved.toLowerCase() : resolved;
185
+ }
186
+ function isMissingPath(error) {
187
+ return error instanceof Error && "code" in error &&
188
+ (error.code === "ENOENT" || error.code === "ENOTDIR");
189
+ }
@@ -1,6 +1,7 @@
1
1
  import { basename, resolve } from "node:path";
2
2
  import { assertAllowedPath } from "../mcp/filesystem/roots.js";
3
3
  import { canonicalPath } from "./paths.js";
4
+ import { inspectManagedWorktreeRecovery } from "./git/worktree-recovery.js";
4
5
  const WORKSPACE_STALE_REMINDER_MS = 2 * 24 * 60 * 60 * 1_000;
5
6
  /** Build bounded read-only projections over persistent Workspace sessions. */
6
7
  export class WorkspaceInventoryService {
@@ -84,11 +85,12 @@ export class WorkspaceInventoryService {
84
85
  }
85
86
  async inventoryEntryForSession(session, now, current) {
86
87
  const rootValid = await this.sessions.validSessionRoot(session) !== undefined;
88
+ const recovery = await inspectManagedWorktreeRecovery(session, this.config);
87
89
  const lastUsedAt = Date.parse(session.lastUsedAt);
88
90
  const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
89
91
  const state = session.status !== "active"
90
92
  ? "closed"
91
- : !rootValid
93
+ : !rootValid || (recovery !== undefined && recovery.classification !== "healthy")
92
94
  ? "invalid"
93
95
  : idleMs >= WORKSPACE_STALE_REMINDER_MS
94
96
  ? "stale"
@@ -109,6 +111,7 @@ export class WorkspaceInventoryService {
109
111
  lastUsedAt: session.lastUsedAt,
110
112
  idleMs,
111
113
  rootValid,
114
+ ...(recovery ? { recovery } : {}),
112
115
  current,
113
116
  };
114
117
  }
@@ -25,6 +25,42 @@ export function copyNumberField(source, target, field) {
25
25
  if (typeof value === "number" && Number.isFinite(value) && value >= 0)
26
26
  target[field] = value;
27
27
  }
28
+ export function safeManagedWorktreeRecovery(value) {
29
+ if (!value || typeof value !== "object")
30
+ return undefined;
31
+ const recovery = value;
32
+ const classifications = new Set(["healthy", "recoverable", "manual-intervention"]);
33
+ const conditions = new Set([
34
+ "backing-missing",
35
+ "managed-branch-missing",
36
+ "git-registration-stale",
37
+ "git-registration-missing",
38
+ "git-registration-unavailable",
39
+ "branch-mismatch",
40
+ "source-missing",
41
+ "source-unavailable",
42
+ "target-branch-missing",
43
+ ]);
44
+ if (typeof recovery.classification !== "string" || !classifications.has(recovery.classification) ||
45
+ !Array.isArray(recovery.conditions) || recovery.conditions.some((condition) => typeof condition !== "string" || !conditions.has(condition)) ||
46
+ (recovery.backing !== "present" && recovery.backing !== "missing") ||
47
+ (recovery.source !== "available" && recovery.source !== "missing" && recovery.source !== "unavailable") ||
48
+ (recovery.gitRegistration !== "registered" && recovery.gitRegistration !== "stale" && recovery.gitRegistration !== "missing" && recovery.gitRegistration !== "unavailable") ||
49
+ (recovery.managedBranch !== "present" && recovery.managedBranch !== "missing" && recovery.managedBranch !== "unknown") ||
50
+ (recovery.targetBranch !== "present" && recovery.targetBranch !== "missing" && recovery.targetBranch !== "unknown") ||
51
+ (recovery.backingBranch !== "matching" && recovery.backingBranch !== "mismatched" && recovery.backingBranch !== "unavailable"))
52
+ return undefined;
53
+ return {
54
+ classification: recovery.classification,
55
+ conditions: [...recovery.conditions],
56
+ backing: recovery.backing,
57
+ source: recovery.source,
58
+ gitRegistration: recovery.gitRegistration,
59
+ managedBranch: recovery.managedBranch,
60
+ targetBranch: recovery.targetBranch,
61
+ backingBranch: recovery.backingBranch,
62
+ };
63
+ }
28
64
  export function safeTaskSummary(value) {
29
65
  if (!value || typeof value !== "object")
30
66
  return undefined;
@@ -7,7 +7,7 @@ import { RemoteMcpConnectionPool } from "./transport/remote-mcp-connection-pool.
7
7
  import { withFileLock } from "../../runtime/state/lock/file-lock.js";
8
8
  import { withRemoteServiceEndpoint } from "./transport/remote-transport.js";
9
9
  import { loadForgeRelayFiles, writeForgeRelayRemote, } from "../../runtime/config/user-config.js";
10
- import { assertRemoteToolSucceeded, stringField, copyStringField, copyBooleanField, copyNumberField, safeTaskSummary, toolResultText, remapToolResultWorkspaceId, replaceExactWorkspaceId, sanitizedRemoteError, errorMessage } from "./result-support.js";
10
+ import { assertRemoteToolSucceeded, copyBooleanField, copyNumberField, copyStringField, errorMessage, remapToolResultWorkspaceId, replaceExactWorkspaceId, safeManagedWorktreeRecovery, safeTaskSummary, sanitizedRemoteError, stringField, toolResultText, } from "./result-support.js";
11
11
  export class RemoteWorkspaceRelay {
12
12
  routes = new Map();
13
13
  turnRoutes = new Map();
@@ -77,6 +77,9 @@ export class RemoteWorkspaceRelay {
77
77
  copyStringField(remoteInspection, projection, "lastUsedAt");
78
78
  copyNumberField(remoteInspection, projection, "idleMs");
79
79
  copyBooleanField(remoteInspection, projection, "rootValid");
80
+ const recovery = safeManagedWorktreeRecovery(remoteInspection.recovery);
81
+ if (recovery)
82
+ projection.recovery = recovery;
80
83
  const taskSummary = safeTaskSummary(remoteInspection.taskSummary);
81
84
  if (taskSummary)
82
85
  projection.taskSummary = taskSummary;
@@ -338,23 +338,30 @@ inventory is paginated (50 records by default, at most 100) and can filter by Wo
338
338
  ID, persisted status, derived state, mode, canonical root/source root, or stale-only
339
339
  state. Reading inventory does not refresh `lastUsedAt`. Persisted `status="active"`
340
340
  means the record has not been explicitly closed; the derived `state` distinguishes
341
- `active`, `stale`, `invalid`, and `closed`. A missing checkout or externally removed
342
- managed-worktree root can therefore remain diagnostically `status="active"` while
343
- appearing as `state="invalid"`. Canonical identity means ordinary same-target opens no
344
- longer accumulate duplicate inventory rows; `action="list"` remains the formal
345
- on-demand inventory path.
341
+ `active`, `stale`, `invalid`, and `closed`. Active managed-worktree entries also expose
342
+ a bounded `recovery` projection: ForgeRelay observes backing/source availability, the
343
+ recorded managed and target branches, Git worktree registration, and the backing's
344
+ current branch, then classifies the result as `healthy`, `recoverable`, or
345
+ `manual-intervention`. A missing checkout or externally damaged managed worktree can
346
+ therefore remain diagnostically `status="active"` while appearing as `state="invalid"`.
347
+ This projection is observation only: inventory never runs `git worktree prune`, creates
348
+ or removes worktrees/branches, or otherwise repairs Git state. Canonical identity means
349
+ ordinary same-target opens no longer accumulate duplicate inventory rows;
350
+ `action="list"` remains the formal on-demand inventory path.
346
351
 
347
352
  `open_workspace(action="inspect", workspaceId="...")` is the bounded read-only detail
348
353
  path for one known Workspace. It uses an explicit allowlist and never opens/resumes the
349
354
  target, changes conversation bindings or bootstrap-delivery records, refreshes
350
355
  `lastUsedAt`, or grants file/process/Git/Capability authority. Safe projections include
351
- ordinary/worktree lifecycle metadata, Composite member availability summaries, Relay
352
- alias/execution-location presentation metadata, and an already-existing Task List
353
- summary. Inspection never returns AGENTS/CLAUDE contents, Skills, Capability-guide
354
- paths or contents, Subagent bodies/sessions, files, Git diffs, process/Activity output,
355
- Hook/review artifacts, credentials, network/SSH routes, or Task bodies. Relay inspection
356
- reports only that the Gateway route is known; it does not probe or claim the remote
357
- Workspace lifecycle state.
356
+ ordinary/worktree lifecycle metadata, managed-worktree recovery observations,
357
+ Composite member availability summaries, Relay alias/execution-location presentation
358
+ metadata, and an already-existing Task List summary. Inspection never returns
359
+ AGENTS/CLAUDE contents, Skills, Capability-guide paths or contents, Subagent
360
+ bodies/sessions, files, Git diffs, process/Activity output, Hook/review artifacts,
361
+ credentials, network/SSH routes, or Task bodies. For Relay Workspaces the Gateway asks
362
+ the Execution ForgeRelay for this same bounded inspection and forwards sanitized
363
+ lifecycle/recovery facts under the Gateway Workspace identity; the Gateway does not
364
+ inspect or mutate the remote Git repository itself.
358
365
 
359
366
  For checkout-backed Workspaces, `close_workspace` defaults to `action="close"`:
360
367
  it marks the persistent Workspace closed, removes current conversation bindings, and
package/docs/roadmap.md CHANGED
@@ -38,7 +38,7 @@ ForgeRelay does not plan to add:
38
38
  - a plugin marketplace/runtime;
39
39
  - a second conversation/session runtime;
40
40
  - host-owned commands such as plan mode, context inspection, or model selection;
41
- - automatic installation and management of language servers.
41
+ - automatic or unapproved language-server installation. Agent-managed TypeScript/JavaScript and Pyright installs require explicit user opt-in and remain disabled by default; Rust Analyzer, `gopls`, and `clangd` stay external toolchain/system dependencies.
42
42
 
43
43
  Shell execution remains a trusted local-user capability. Workspace filesystem
44
44
  containment must not be described as a shell sandbox.
@@ -197,11 +197,12 @@ capability
197
197
 
198
198
  ## 0.4 — LSP code intelligence v1
199
199
 
200
- LSP is moderate implementation complexity if ForgeRelay does not become a
201
- language-server installer.
202
-
203
- The first version should launch only language servers already available on the
204
- user's machine or explicitly configured by the user/project.
200
+ LSP v1 was designed around language servers already available on the user's machine
201
+ or explicitly configured by the user/project. ForgeRelay later added a narrow managed
202
+ installation path for TypeScript/JavaScript and Pyright: it is disabled by default,
203
+ requires explicit user authorization during setup, installs only into ForgeRelay's
204
+ private config directory, and refreshes live without a server restart. This does not
205
+ turn arbitrary language-server installation into an implicit Agent capability.
205
206
 
206
207
  During 0.4 development, MCP App UI hardening can land alongside the LSP work when
207
208
  it does not distort the code-intelligence scope. In particular, evaluate a more
@@ -245,9 +246,10 @@ and verify publication before work begins on the next boundary:
245
246
  The shipping 0.4 LSP v1 contract keeps the canonical nine Core MCP tools unchanged
246
247
  and exposes semantic operations only through `code.intelligence`. Deterministic
247
248
  fake-LSP coverage remains the primary cross-platform protocol/lifecycle gate, while
248
- `npm run lsp:interop` exercises `typescript-language-server`, Pyright,
249
- `rust-analyzer`, `gopls`, and `clangd` only when those external executables are
250
- already present and otherwise reports explicit skips without installing them.
249
+ `npm run lsp:interop` exercises detected servers only when executables are already
250
+ present and never downloads them. The later managed TypeScript/Pyright installation
251
+ path is a separate explicitly authorized runtime capability and does not change that
252
+ interop-test contract.
251
253
 
252
254
  ## 0.5 — Durable Activity and batch execution
253
255
 
@@ -315,32 +317,40 @@ milestone: the next stage remains blocked until the previous version's tag-trigg
315
317
  release workflow has completed successfully. Runtime acceptance uses only the
316
318
  reserved 7677/7678 debug instances and never touches the normal 7676 installation.
317
319
 
318
- ## Later: first-class subagent MCP
319
-
320
- ForgeRelay already owns provider adapters and resumable local agent sessions. A
321
- future release may remove the current `bash -> forgerelay agents ...` indirection
322
- for MCP hosts.
323
-
324
- First-class subagent operations should reuse the Capability Gateway rather than
325
- add another top-level MCP tool. The parent agent should continue choosing from
326
- available provider/profile metadata while ForgeRelay owns provider-backed worker
327
- lifecycle state. This remains provider-backed delegation, not an attempt to
328
- emulate a Host-native subagent implementation.
329
-
330
- ## Worktree and history refinements
331
-
332
- These can land alongside the main release lines when the underlying interfaces
333
- are ready:
334
-
335
- - `.worktreeinclude`-style explicit copying of selected gitignored files;
336
- - hook-backed worktree close verification;
337
- - stale managed-worktree recovery and cleanup;
338
- - checkpoint/list/restore primitives based on the existing Git snapshot engine;
339
- - retention/GC for stale workspace sessions, conversation bindings, and review
340
- refs.
341
-
342
- Checkpoint restore must protect concurrent/external user edits rather than
343
- blindly overwriting a working tree.
320
+ ## 0.9 Workspace Recovery & History
321
+
322
+ 0.9 builds on persistent Workspace identity, first-class `subagent.session`, and the
323
+ existing Hook-backed managed-worktree finalize lifecycle. It does not add another Core
324
+ MCP tool. Each stage is release-gated: the following stage begins only after the prior
325
+ version's tag-triggered Linux/macOS/Windows verification and publication succeed.
326
+
327
+ - **0.9.0** add bounded read-only managed-worktree recovery observations to
328
+ `open_workspace(action="list"|"inspect")`, distinguishing healthy, recoverable,
329
+ and manual-intervention states without repairing or pruning Git state; align current
330
+ Roadmap/product documentation with already-shipped Subagent, worktree verification,
331
+ and explicitly authorized managed-LSP behavior;
332
+ - **0.9.1** — add Workspace-scoped safe managed-worktree repair and cleanup through
333
+ the Capability Gateway. Recovery must preserve surviving managed-branch work and
334
+ refuse ambiguous ownership rather than guessing;
335
+ - **0.9.2** — add persistent Workspace checkpoints with explicit create/list/inspect/
336
+ delete lifecycle, stored outside normal project history and surviving Workspace
337
+ close/reopen;
338
+ - **0.9.3** — add checkpoint restore with optimistic-concurrency preflight so external
339
+ or user edits made after inspection cannot be silently overwritten. Restore changes
340
+ working-tree content without moving branch HEAD or using `git reset --hard`;
341
+ - **0.9.4** add owner-facing retention inspection and explicitly authorized prune
342
+ through the CLI. Audit retention remains unlimited by default, persistent Workspace
343
+ identity/Tasks are not ordinary GC targets, and destructive global maintenance is
344
+ not exposed as an ordinary Agent MCP Capability.
345
+
346
+ Workspace Relay continues to treat the Execution ForgeRelay as the owner of Git,
347
+ recovery, checkpoint, and retention facts; the Gateway only routes and presents bounded
348
+ results. Development acceptance remains on isolated 7677/7678 instances and never uses
349
+ the normal 7676 installation.
350
+
351
+ Later refinements may add `.worktreeinclude`-style explicit copying of selected
352
+ Git-ignored files. Native PowerShell/`cmd.exe` shell execution is also a separate
353
+ compatibility decision rather than part of the 0.9 recovery/history line.
344
354
 
345
355
  ## Workspace Task Lists
346
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.8.10",
3
+ "version": "0.9.0",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -52,7 +52,7 @@
52
52
  "release:push-ready": "node scripts/release/push-ready.mjs",
53
53
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
54
54
  "start": "node dist/cli.js serve",
55
- "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/cli.test.ts",
55
+ "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/cli.test.ts",
56
56
  "typecheck": "tsc -p tsconfig.json --noEmit",
57
57
  "release:check": "node scripts/release-version.mjs check",
58
58
  "release:tag-check": "node scripts/release-version.mjs tag",