@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 +15 -0
- package/README.md +13 -10
- package/dist/mcp/server/core/schemas.js +23 -0
- package/dist/workspaces/git/worktree-recovery.js +189 -0
- package/dist/workspaces/inventory.js +4 -1
- package/dist/workspaces/relay/result-support.js +36 -0
- package/dist/workspaces/relay/workspace-relay.js +4 -1
- package/docs/configuration.md +19 -12
- package/docs/roadmap.md +45 -35
- package/package.json +2 -2
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
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.
|
|
305
|
-
2.
|
|
306
|
-
3.
|
|
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,
|
|
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;
|
package/docs/configuration.md
CHANGED
|
@@ -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`.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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,
|
|
352
|
-
alias/execution-location presentation
|
|
353
|
-
summary. Inspection never returns
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
##
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
-
|
|
337
|
-
|
|
338
|
-
-
|
|
339
|
-
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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.
|
|
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",
|