@arnilo/prism-tools 0.13.0 → 0.13.1
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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.1] - 2026-11-12
|
|
4
|
+
|
|
5
|
+
### ./coding
|
|
6
|
+
|
|
7
|
+
#### Fixed
|
|
8
|
+
|
|
9
|
+
- **`workspace.worktree.create({ sessionId })` keeps one live worktree per session.** Creating a worktree for a session that already holds one now fails with the new code `workspace_session_bound` and creates nothing. Previously a second record was created, and `bound(sessionId)` and tool routing became ambiguous. Two concurrent creates for one session bind once. `worktree.rebind` refuses a session that already holds a different worktree with the same code. Reuse the held worktree through `bound(sessionId)`, or dispose it first.
|
|
10
|
+
|
|
3
11
|
## [Unreleased]
|
|
4
12
|
|
|
5
13
|
Consolidated seven tool packages into `@arnilo/prism-tools` with explicit subpaths.
|
package/coding/README.md
CHANGED
|
@@ -135,7 +135,7 @@ await wt.dispose({ force: true });
|
|
|
135
135
|
|
|
136
136
|
| member | behavior |
|
|
137
137
|
| --- | --- |
|
|
138
|
-
| `worktree.create({ branch?, base?, sessionId? })` | Adds a linked worktree under `worktreeRoot` on a new branch (default `prism/<short id>`) from `base` (default `HEAD`), locks it with a `prism-workspace:` reason, and returns `{ path, branch, base, parentBranch, sessionId?, dispose }`. `base` is the resolved commit the branch started at. `parentBranch` is the main tree's checked-out branch at creation, recorded because `merge` only accepts a branch name; `HEAD` means the main tree was detached. `sessionId` binds the worktree for tool routing (see [Worktree roots for one session](#worktree-roots-for-one-session)); under a sandbox the binding fails with `workspace_sandbox_unsupported`. More than `maxWorktrees` active worktrees fails with `workspace_limit`; a destination outside `worktreeRoot` fails with `workspace_path_escape`. |
|
|
138
|
+
| `worktree.create({ branch?, base?, sessionId? })` | Adds a linked worktree under `worktreeRoot` on a new branch (default `prism/<short id>`) from `base` (default `HEAD`), locks it with a `prism-workspace:` reason, and returns `{ path, branch, base, parentBranch, sessionId?, dispose }`. `base` is the resolved commit the branch started at. `parentBranch` is the main tree's checked-out branch at creation, recorded because `merge` only accepts a branch name; `HEAD` means the main tree was detached. `sessionId` binds the worktree for tool routing (see [Worktree roots for one session](#worktree-roots-for-one-session)); under a sandbox the binding fails with `workspace_sandbox_unsupported`. A session holds at most one live worktree: a second `create` (or a `rebind` of another path) for a session that already holds one fails with `workspace_session_bound` and creates nothing, so `bound(sessionId)` is never ambiguous. Reuse the held worktree through `bound`, or dispose it first. More than `maxWorktrees` active worktrees fails with `workspace_limit`; a destination outside `worktreeRoot` fails with `workspace_path_escape`. |
|
|
139
139
|
| `worktree.bound(sessionId)` | The active worktree bound to that session, or `undefined`. |
|
|
140
140
|
| `worktree.rebind({ path, sessionId, branch?, base? })` | Re-adopts a worktree a dead process left behind and binds it to `sessionId`, so a resumed workflow node runs in its recorded worktree. It fails closed with `workspace_unknown` unless the path is inside `worktreeRoot`, registered with git, locked with this service's `prism-workspace:` marker, and on `branch` when given. `base` defaults to the merge base of the branch and the main tree's `HEAD`. Idempotent for the same session; counts against `maxWorktrees`; refused under a sandbox. Never creates a worktree. |
|
|
141
141
|
| `diff(path, bounds?)` | `{ changes, truncated }` for tracked changes against the worktree's `base` plus untracked files. Paths are worktree-relative and each entry is the driver kit's `FileActivity` shape. A file whose diff exceeds `maxBytes` is listed with no `diff`; more than `maxFiles` sets `truncated`. Overlay files never appear. |
|
|
@@ -15,7 +15,8 @@ export type WorkspaceServiceErrorCode =
|
|
|
15
15
|
| "workspace_identity_missing"
|
|
16
16
|
| "workspace_git_unsupported"
|
|
17
17
|
| "workspace_merge_failed"
|
|
18
|
-
| "workspace_sandbox_unsupported"
|
|
18
|
+
| "workspace_sandbox_unsupported"
|
|
19
|
+
| "workspace_session_bound";
|
|
19
20
|
|
|
20
21
|
export class WorkspaceServiceError extends Error {
|
|
21
22
|
readonly code: WorkspaceServiceErrorCode;
|
|
@@ -59,7 +59,8 @@ export interface WorkspaceWorktreeCreate {
|
|
|
59
59
|
readonly base?: string;
|
|
60
60
|
/**
|
|
61
61
|
* Binds this worktree to a session for tool routing (plan 165 Task 7). A sandboxed service
|
|
62
|
-
* refuses the binding: the routed tools would leave the sandbox root.
|
|
62
|
+
* refuses the binding: the routed tools would leave the sandbox root. A session holds at most one
|
|
63
|
+
* live worktree: creating another for it fails with `workspace_session_bound` (use `bound(sessionId)`).
|
|
63
64
|
*/
|
|
64
65
|
readonly sessionId?: string;
|
|
65
66
|
}
|
|
@@ -204,6 +205,8 @@ export async function createWorkspaceService(options: WorkspaceServiceOptions):
|
|
|
204
205
|
|
|
205
206
|
const emitter = createCodingLifecycleEmitter(options.onEvent === undefined ? {} : { onEvent: options.onEvent });
|
|
206
207
|
const records = new Map<string, WorktreeRecord>();
|
|
208
|
+
/** Sessions with a create or rebind in flight, so two concurrent calls cannot both bind one session. */
|
|
209
|
+
const binding = new Set<string>();
|
|
207
210
|
const ops = new Map<string, Promise<GitOperations>>();
|
|
208
211
|
let identityPromise: Promise<{ name: string; email: string } | undefined> | undefined;
|
|
209
212
|
let mainPath: string | undefined;
|
|
@@ -306,6 +309,34 @@ export async function createWorkspaceService(options: WorkspaceServiceOptions):
|
|
|
306
309
|
"a sandboxed run cannot bind a worktree to a session: routed tool paths would leave the sandbox root",
|
|
307
310
|
);
|
|
308
311
|
}
|
|
312
|
+
if (request.sessionId === undefined) return await createRecord(request);
|
|
313
|
+
const sessionId = request.sessionId;
|
|
314
|
+
assertSessionFree(sessionId);
|
|
315
|
+
binding.add(sessionId);
|
|
316
|
+
try {
|
|
317
|
+
return await createRecord(request);
|
|
318
|
+
} finally {
|
|
319
|
+
binding.delete(sessionId);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** One live worktree per session: a second one would make `bound()` and tool routing ambiguous. */
|
|
324
|
+
function assertSessionFree(sessionId: string, path?: string): void {
|
|
325
|
+
const held = liveFor(sessionId);
|
|
326
|
+
if (binding.has(sessionId) || (held !== undefined && held.path !== path)) {
|
|
327
|
+
throw new WorkspaceServiceError(
|
|
328
|
+
"workspace_session_bound",
|
|
329
|
+
`session ${sessionId} already holds a worktree; reuse it through bound(), or dispose it first`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function liveFor(sessionId: string): WorktreeRecord | undefined {
|
|
335
|
+
for (const record of records.values()) if (record.sessionId === sessionId && !record.disposed) return record;
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function createRecord(request: WorkspaceWorktreeCreate): Promise<WorkspaceWorktree> {
|
|
309
340
|
if (records.size >= maxWorktrees) {
|
|
310
341
|
throw new WorkspaceServiceError("workspace_limit", `at most ${maxWorktrees} worktrees may be active at once`);
|
|
311
342
|
}
|
|
@@ -351,6 +382,16 @@ export async function createWorkspaceService(options: WorkspaceServiceOptions):
|
|
|
351
382
|
if (held.sessionId === request.sessionId && !held.disposed) return handle(held);
|
|
352
383
|
throw new WorkspaceServiceError("workspace_unknown", `worktree ${path} is already held for another session`);
|
|
353
384
|
}
|
|
385
|
+
assertSessionFree(request.sessionId, path);
|
|
386
|
+
binding.add(request.sessionId);
|
|
387
|
+
try {
|
|
388
|
+
return await adopt(request, path);
|
|
389
|
+
} finally {
|
|
390
|
+
binding.delete(request.sessionId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function adopt(request: WorkspaceWorktreeRebind, path: string): Promise<WorkspaceWorktree> {
|
|
354
395
|
const repo = await gitFor(root);
|
|
355
396
|
const listed = await repo.worktree({ action: "list" });
|
|
356
397
|
const entry = listed.worktrees.find((worktree) => resolve(worktree.path) === path);
|
|
@@ -397,10 +438,8 @@ export async function createWorkspaceService(options: WorkspaceServiceOptions):
|
|
|
397
438
|
}
|
|
398
439
|
|
|
399
440
|
function bound(sessionId: string): WorkspaceWorktree | undefined {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
return undefined;
|
|
441
|
+
const record = liveFor(sessionId);
|
|
442
|
+
return record === undefined ? undefined : handle(record);
|
|
404
443
|
}
|
|
405
444
|
|
|
406
445
|
async function disposeWorktree(record: WorktreeRecord, force: boolean): Promise<void> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-tools",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Prism coding, web, browser, research, OpenAPI, computer-use and work tools. Import each package through its subpath.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@arnilo/prism": ">=0.13.0 <1.0.0",
|
|
107
|
-
"@arnilo/prism-extensions": "^0.13.
|
|
107
|
+
"@arnilo/prism-extensions": "^0.13.1",
|
|
108
108
|
"@office-open/docx": "0.14.5",
|
|
109
109
|
"@office-open/pptx": "0.14.5",
|
|
110
110
|
"@office-open/xlsx": "0.14.5",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"@types/diff": "^7.0.2",
|
|
157
157
|
"@types/node": "^22.20.0",
|
|
158
158
|
"typescript": "^7.0.2",
|
|
159
|
-
"@arnilo/prism-extensions": "0.13.
|
|
159
|
+
"@arnilo/prism-extensions": "0.13.1",
|
|
160
160
|
"@types/turndown": "5.0.6",
|
|
161
161
|
"playwright-core": "1.63.0",
|
|
162
162
|
"@firecrawl/anydoc": "0.2.4",
|