mixdog 1.3.2 → 1.3.3
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/package.json +7 -7
- package/scripts/verify-runtime-dependency-versions.mjs +158 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -3
- package/src/defaults/skills/local-provider/SKILL.md +33 -6
- package/src/rules/lead/LEAD.md +5 -8
- package/src/rules/shared/45-destructive.md +5 -1
- package/src/rules/shared/60-verification.md +6 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
- package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
- package/src/runtime/attachments/legacy-office.mjs +110 -0
- package/src/runtime/attachments/prompt-restore.mjs +70 -0
- package/src/runtime/attachments/store.mjs +60 -3
- package/src/runtime/effort-judge/judge-client.mjs +61 -14
- package/src/runtime/effort-judge/model-install.mjs +34 -2
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
- package/src/runtime/office/portable/portable-soffice.mjs +132 -30
- package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
- package/src/runtime/shared/agent-avatar.mjs +3 -2
- package/src/runtime/shared/agent-room-host.d.mts +31 -0
- package/src/runtime/shared/agent-room-host.mjs +58 -0
- package/src/runtime/shared/bot-hops.mjs +16 -11
- package/src/runtime/shared/redact-directories.mjs +18 -0
- package/src/runtime/shared/remove-directory.mjs +28 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
- package/src/runtime/shared/session-sidebar-host.mjs +16 -3
- package/src/runtime/shared/tool-execution-contract.mjs +4 -0
- package/src/runtime/shared/tool-work-units.mjs +12 -9
- package/src/runtime/shared/turn-snapshot.mjs +233 -122
- package/src/runtime/shared/user-data-guard.mjs +51 -19
- package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
- package/src/runtime/shared/worktrees/contract.mjs +11 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
- package/src/runtime/shared/worktrees/manager.mjs +419 -52
- package/src/runtime/shared/worktrees/registry.mjs +3 -1
- package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
- package/src/runtime/tidy/process.mjs +41 -8
- package/src/runtime/tidy/resolve.mjs +10 -7
- package/src/session-runtime/agent-home-delivery.mjs +73 -91
- package/src/session-runtime/agent-profile-changes.mjs +16 -0
- package/src/session-runtime/agent-room-delivery.mjs +2 -63
- package/src/session-runtime/agent-task-sessions.mjs +194 -64
- package/src/session-runtime/boot/apis.mjs +3 -0
- package/src/session-runtime/boot/facade.mjs +5 -6
- package/src/session-runtime/boot/tools.mjs +21 -1
- package/src/session-runtime/held-requests.mjs +20 -25
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
- package/src/session-runtime/model-recency.mjs +1 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
- package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
- package/src/session-runtime/runtime-review-api.mjs +4 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
- package/src/session-runtime/services/provider-admin.mjs +12 -2
- package/src/session-runtime/services/worktree-access.mjs +27 -0
- package/src/session-runtime/session-hooks.mjs +9 -17
- package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
- package/src/session-runtime/setup-tool/executor.mjs +4 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
- package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
- package/src/session-runtime/tool-policy-refresh.mjs +1 -0
- package/src/session-runtime/tool-surface.mjs +9 -5
- package/src/session-runtime/turn/session-ops.mjs +8 -0
- package/src/session-runtime/turn/turn-run.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
- package/src/session-runtime/workspace/transition-steps.mjs +1 -18
- package/src/session-runtime/workspace/transitions.mjs +279 -67
- package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
- package/src/standalone/daemon-lag-profiler.mjs +3 -1
- package/src/standalone/daemon-session-control.mjs +140 -23
- package/src/standalone/daemon-telemetry.mjs +9 -0
- package/src/standalone/daemon.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +11 -2
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
- package/src/standalone/session-service/agent-route-follow.mjs +69 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
- package/src/standalone/session-service.mjs +10 -0
- package/src/standalone/session-tool-output.mjs +1 -0
- package/src/standalone/staged-import.mjs +47 -0
- package/src/tui/App.jsx +2 -0
- package/src/tui/app/message-selector.mjs +15 -2
- package/src/tui/app/model-options.mjs +16 -2
- package/src/tui/app/model-picker.mjs +2 -1
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/dist/index.mjs +92 -40
- package/src/tui/paste-attachments.mjs +0 -1
- package/src/tui/paste-text-policy.mjs +4 -5
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
- package/src/tui/session/notice-surface.mjs +3 -0
- package/src/tui/session/session-api/intake/abort.mjs +33 -17
- package/src/tui/session/session-api/intake/rewind.mjs +39 -4
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +11 -48
- package/src/session-runtime/room-member-gate.mjs +0 -61
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The git tool's managed-worktree surface. Agents never create unmanaged
|
|
3
|
+
* worktrees: `action: "worktree"` drives the Lead session's managed worktree
|
|
4
|
+
* (the desktop branch menu's operations, same checks and refusals), and a raw
|
|
5
|
+
* `git worktree …` command maps onto a managed operation instead of running:
|
|
6
|
+
*
|
|
7
|
+
* add [-b <branch>] <path> [<commit-ish>] → managed worktree for that branch; the session stays put
|
|
8
|
+
* add --detach <path> [<commit>] → read-only snapshot, removed when the turn ends
|
|
9
|
+
* add <path> <commit> (not a branch) → the same snapshot (git would detach there too)
|
|
10
|
+
* remove [-f] <path> → managed removal of that worktree (same guards)
|
|
11
|
+
* list → managed worktrees (owners) plus unmanaged ones, flagged
|
|
12
|
+
* list --porcelain → raw git, unchanged (machine-readable truth)
|
|
13
|
+
* prune [-n] → managed prune (dry run: what it would remove)
|
|
14
|
+
* move | lock | unlock | repair → refused: managed locations are fixed
|
|
15
|
+
*
|
|
16
|
+
* Every operation but `list` belongs to the Lead: the session runtime that
|
|
17
|
+
* hosts the caller registers its host (shared/worktrees/tool-host.mjs); a
|
|
18
|
+
* caller no host claims (a delegated worker) is refused.
|
|
19
|
+
*/
|
|
20
|
+
import { basename, resolve } from 'node:path';
|
|
21
|
+
import { isPlainObject } from '../../../../shared/object.mjs';
|
|
22
|
+
import { WORKTREE_REFUSAL_REASONS } from '../../../../shared/worktrees/contract.mjs';
|
|
23
|
+
import { firstLine, listWorktrees, runGit } from '../../../../shared/worktrees/git.mjs';
|
|
24
|
+
import { canonicalPath, samePath } from '../../../../shared/worktrees/paths.mjs';
|
|
25
|
+
import { worktreeToolHost } from '../../../../shared/worktrees/tool-host.mjs';
|
|
26
|
+
|
|
27
|
+
export const WORKTREE_TOOL_ACTIONS = Object.freeze(['enter', 'return', 'merge', 'remove', 'list', 'prune']);
|
|
28
|
+
const CARRY_CHOICES = ['leave', 'carry'];
|
|
29
|
+
// The input fields of action:"worktree" (confirm_large also answers a mapped `git worktree add`).
|
|
30
|
+
export const WORKTREE_INPUT_FIELDS = Object.freeze([
|
|
31
|
+
'worktree_action',
|
|
32
|
+
'session_id',
|
|
33
|
+
'name',
|
|
34
|
+
'branch',
|
|
35
|
+
'create',
|
|
36
|
+
'carry',
|
|
37
|
+
'force',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const ADD_BARE_FLAGS = new Set([
|
|
41
|
+
'-q',
|
|
42
|
+
'--quiet',
|
|
43
|
+
'-f',
|
|
44
|
+
'--force',
|
|
45
|
+
'--lock',
|
|
46
|
+
'--checkout',
|
|
47
|
+
'--track',
|
|
48
|
+
'--no-track',
|
|
49
|
+
'--guess-remote',
|
|
50
|
+
'--no-guess-remote',
|
|
51
|
+
]);
|
|
52
|
+
const FIXED_LOCATION_SUBCOMMANDS = new Set(['move', 'lock', 'unlock', 'repair']);
|
|
53
|
+
|
|
54
|
+
const fail = (message) => `error: ${message}`;
|
|
55
|
+
const okJson = (data) => JSON.stringify({ ok: true, ...data });
|
|
56
|
+
|
|
57
|
+
/** The desktop's byte size text (worktree-branch-model.ts formatByteSize). */
|
|
58
|
+
function formatByteSize(bytes) {
|
|
59
|
+
if (!(bytes > 0)) return '';
|
|
60
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
61
|
+
let value = bytes;
|
|
62
|
+
let unit = 0;
|
|
63
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
64
|
+
value /= 1024;
|
|
65
|
+
unit += 1;
|
|
66
|
+
}
|
|
67
|
+
return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The runtime's own text after the last `<code>: ` prefix. */
|
|
71
|
+
function runtimeText(code, message) {
|
|
72
|
+
const marker = `${code}: `;
|
|
73
|
+
const index = message.lastIndexOf(marker);
|
|
74
|
+
return (index >= 0 ? message.slice(index + marker.length) : message).trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The desktop's wording of a typed failure (worktree-branch-model.ts
|
|
79
|
+
* worktreeFailureText, English source strings); the runtime's message where
|
|
80
|
+
* the desktop has none.
|
|
81
|
+
*/
|
|
82
|
+
function worktreeFailureText(code, message, details) {
|
|
83
|
+
switch (code) {
|
|
84
|
+
case 'WORKTREE_DISK_INSUFFICIENT': {
|
|
85
|
+
const required = formatByteSize(Number(details.requiredBytes));
|
|
86
|
+
const free = formatByteSize(Number(details.freeBytes));
|
|
87
|
+
return required && free
|
|
88
|
+
? `Not enough free disk space to create the worktree (${required} needed, ${free} free).`
|
|
89
|
+
: 'Not enough free disk space to create the worktree.';
|
|
90
|
+
}
|
|
91
|
+
case 'WORKTREE_FILE_RECOVERY':
|
|
92
|
+
return 'Retained file recovery records block worktree removal, including force. Inspect and explicitly delete them first.';
|
|
93
|
+
case 'WORKTREE_BUSY_JOBS':
|
|
94
|
+
return (
|
|
95
|
+
WORKTREE_REFUSAL_REASONS[details.reason] ??
|
|
96
|
+
(details.reason === 'snapshot-held'
|
|
97
|
+
? message
|
|
98
|
+
: 'Background tasks are still running. Wait for them or stop them, then try again.')
|
|
99
|
+
);
|
|
100
|
+
case 'WORKTREE_BRANCH_OCCUPIED':
|
|
101
|
+
return typeof details.path === 'string' && details.path
|
|
102
|
+
? `This branch is already checked out in ${details.path}.`
|
|
103
|
+
: 'This branch is already checked out in another worktree.';
|
|
104
|
+
case 'WORKTREE_REMOVAL_INCOMPLETE':
|
|
105
|
+
return 'The worktree could not be removed completely because a file is still in use. Close it and remove the worktree again.';
|
|
106
|
+
case 'WORKTREE_PATH_UNSAFE':
|
|
107
|
+
return 'The worktree folder is not a managed worktree of this project, so nothing was deleted.';
|
|
108
|
+
case 'WORKTREE_MISSING':
|
|
109
|
+
return 'The worktree folder is missing. Pick a project branch to continue.';
|
|
110
|
+
case 'WORKTREE_REVISION_CONFLICT':
|
|
111
|
+
return 'The workspace changed, so the menu was refreshed. Try again.';
|
|
112
|
+
case 'WORKTREE_NOT_IMPLEMENTED':
|
|
113
|
+
return 'Worktrees are not available yet.';
|
|
114
|
+
case 'WORKTREE_UNSUPPORTED_REPO':
|
|
115
|
+
return 'This folder does not support worktrees.';
|
|
116
|
+
case 'WORKTREE_DIRTY': {
|
|
117
|
+
// A refused carry names its own reason in the runtime's text.
|
|
118
|
+
if (details.reason && details.reason !== 'project-dirty') return runtimeText(code, message);
|
|
119
|
+
if (details.reason === 'project-dirty') {
|
|
120
|
+
return 'The project folder has uncommitted changes. Commit or stash them before switching branches.';
|
|
121
|
+
}
|
|
122
|
+
return 'The worktree has uncommitted changes.';
|
|
123
|
+
}
|
|
124
|
+
case 'WORKTREE_PARTIAL_TRANSITION':
|
|
125
|
+
return runtimeText(code, message);
|
|
126
|
+
case 'WORKTREE_PROJECT_UNKNOWN':
|
|
127
|
+
return 'Project is not available.';
|
|
128
|
+
case 'WORKTREE_PROJECT_MISMATCH':
|
|
129
|
+
return 'This session keeps a worktree of another project. Remove it before creating a new one here.';
|
|
130
|
+
case 'WORKTREE_UNMERGED':
|
|
131
|
+
return 'The worktree branch has commits that are not merged.';
|
|
132
|
+
case 'MERGE_TARGET_DIRTY':
|
|
133
|
+
return 'The branch to merge into has uncommitted changes. Commit or stash them first.';
|
|
134
|
+
case 'WORKTREE_CONFIRM_REQUIRED':
|
|
135
|
+
return 'Confirm the merge before it runs.';
|
|
136
|
+
case 'WORKTREE_TARGET_MISMATCH':
|
|
137
|
+
return 'The project folder switched to another branch. Check the target and merge again.';
|
|
138
|
+
case 'MERGE_FAILED': {
|
|
139
|
+
const reason = runtimeText(code, message);
|
|
140
|
+
return reason ? `The merge failed: ${reason}` : 'The merge failed.';
|
|
141
|
+
}
|
|
142
|
+
case 'MERGE_CONFLICT': {
|
|
143
|
+
const files = Array.isArray(details.files) ? details.files.map(String) : [];
|
|
144
|
+
if (!files.length) return 'Nothing was merged because the branches conflict.';
|
|
145
|
+
return `Nothing was merged. Conflicting files: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ', …' : ''}`;
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
return message;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** What the caller does next, for the refusals that need the user or a retry. */
|
|
153
|
+
function nextStep(code, details, { raw }) {
|
|
154
|
+
switch (code) {
|
|
155
|
+
case 'WORKTREE_DIRTY_CHOICE':
|
|
156
|
+
return `Ask the user whether to leave the ${details.count ?? 'uncommitted'} uncommitted change(s) in the project folder or carry them into the worktree; then call again with carry:"leave" or carry:"carry".`;
|
|
157
|
+
case 'WORKTREE_LARGE_REPO_CONFIRM': {
|
|
158
|
+
const size = formatByteSize(Number(details.estimateBytes));
|
|
159
|
+
return `Tell the user the worktree needs about ${size || 'a lot of disk space'} and ask to confirm; only on yes call again with confirm_large:true${raw ? ' (same command)' : ''}.`;
|
|
160
|
+
}
|
|
161
|
+
case 'WORKTREE_REVISION_CONFLICT':
|
|
162
|
+
return 'The workspace changed meanwhile; call again (the workspace is read afresh).';
|
|
163
|
+
case 'WORKTREE_BUSY_JOBS':
|
|
164
|
+
return 'Nothing was changed; try again once the session and its workers are idle.';
|
|
165
|
+
case 'MERGE_CONFLICT':
|
|
166
|
+
return 'Nothing was merged or forced. Resolve the conflict in the worktree (merge the target branch into it there), or ask the user.';
|
|
167
|
+
case 'WORKTREE_DIRTY':
|
|
168
|
+
case 'WORKTREE_UNMERGED':
|
|
169
|
+
return 'Nothing was removed. Commit or merge the work first; force only when the user explicitly asks to discard it.';
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A failed operation for the model: typed failures as JSON with their details, others as text. */
|
|
176
|
+
function failureText(error, { raw = false } = {}) {
|
|
177
|
+
const code = typeof error?.code === 'string' ? error.code : '';
|
|
178
|
+
const message = String(error?.message || error);
|
|
179
|
+
if (!/^(?:WORKTREE|MERGE)_/.test(code)) return fail(message);
|
|
180
|
+
const details = isPlainObject(error.details) ? error.details : {};
|
|
181
|
+
const next = nextStep(code, details, { raw });
|
|
182
|
+
return JSON.stringify({
|
|
183
|
+
ok: false,
|
|
184
|
+
code,
|
|
185
|
+
error: worktreeFailureText(code, message, details),
|
|
186
|
+
...(Object.keys(details).length ? { details } : {}),
|
|
187
|
+
...(next ? { next } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function compactWorkspace(workspace) {
|
|
192
|
+
if (!workspace) return null;
|
|
193
|
+
return {
|
|
194
|
+
cwd: workspace.cwd,
|
|
195
|
+
branch: workspace.branch ?? null,
|
|
196
|
+
mergeTarget: workspace.mergeTarget ?? null,
|
|
197
|
+
revision: workspace.revision,
|
|
198
|
+
worktreeId: workspace.worktreeId ?? null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A workspace operation's result: its own fields, the workspace compacted. */
|
|
203
|
+
function transitionResult(fields, result) {
|
|
204
|
+
const { workspace, revision: _revision, pending: _pending, ...rest } = isPlainObject(result) ? result : {};
|
|
205
|
+
return okJson({ ...fields, ...rest, ...(workspace ? { workspace: compactWorkspace(workspace) } : {}) });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The Lead host of the caller; a caller none claims is no Lead. */
|
|
209
|
+
function leadHost(options, what) {
|
|
210
|
+
const host = worktreeToolHost(options?.sessionId);
|
|
211
|
+
if (!host) {
|
|
212
|
+
throw new Error(`${what} belongs to the Lead session; delegated workers cannot create, change or remove worktrees`);
|
|
213
|
+
}
|
|
214
|
+
return host;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const runOn = (host, request, options) => host.run(request, { callerSessionId: options.sessionId });
|
|
218
|
+
|
|
219
|
+
/** The transaction id of one operation of this tool call (stable across a retry of the call). */
|
|
220
|
+
function txIdOf(options, discriminator) {
|
|
221
|
+
const id = String(options?.toolCallId || '').trim();
|
|
222
|
+
return id ? `${id}:${discriminator}` : '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Repository facts in `cwd` from git itself (null outside a repository). */
|
|
226
|
+
async function repositoryAt(cwd) {
|
|
227
|
+
const probe = await runGit(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir'], { allowFailure: true });
|
|
228
|
+
return probe.code === 0 ? { repoCommonDir: canonicalPath(firstLine(probe.stdout)) } : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function localBranchExists(cwd, branch) {
|
|
232
|
+
const result = await runGit(cwd, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], {
|
|
233
|
+
allowFailure: true,
|
|
234
|
+
});
|
|
235
|
+
return result.code === 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Full commit id of commit-ish `value` resolved in `cwd`, or null. */
|
|
239
|
+
async function commitIn(cwd, value) {
|
|
240
|
+
if (!value || value.startsWith('-')) return null;
|
|
241
|
+
const result = await runGit(cwd, ['rev-parse', '--verify', '--quiet', `${value}^{commit}`], { allowFailure: true });
|
|
242
|
+
return result.code === 0 ? firstLine(result.stdout) : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Managed rows (the owners) of the repository at `cwd` (every repository
|
|
247
|
+
* outside one) plus the repository's other git worktrees: the main checkout
|
|
248
|
+
* and unmanaged ones, flagged.
|
|
249
|
+
*/
|
|
250
|
+
async function listRows(options, cwd) {
|
|
251
|
+
const host = worktreeToolHost(options?.sessionId);
|
|
252
|
+
const managed = host
|
|
253
|
+
? await runOn(host, { op: 'list' }, options)
|
|
254
|
+
: await (await import('../../../../shared/worktrees/manager.mjs')).listManagedWorktrees();
|
|
255
|
+
const repo = await repositoryAt(cwd);
|
|
256
|
+
const ours = repo ? managed.filter((row) => samePath(row.repoCommonDir, repo.repoCommonDir)) : managed;
|
|
257
|
+
const rows = ours.map((row) => ({
|
|
258
|
+
id: row.id,
|
|
259
|
+
path: row.path,
|
|
260
|
+
branch: row.branch ?? null,
|
|
261
|
+
...(row.kind === 'snapshot'
|
|
262
|
+
? { snapshot: true, commit: row.baseCommit, holderSessionId: row.holderSessionId ?? null }
|
|
263
|
+
: { ownerSessionId: row.ownerSessionId ?? null, mergeTarget: row.mergeTarget ?? null }),
|
|
264
|
+
state: row.state,
|
|
265
|
+
...(typeof row.ownerLive === 'boolean' ? { ownerLive: row.ownerLive } : {}),
|
|
266
|
+
...(host && typeof row.removable === 'boolean' ? { removable: row.removable } : {}),
|
|
267
|
+
}));
|
|
268
|
+
if (!repo) return { worktrees: rows };
|
|
269
|
+
// git lists the main working tree first.
|
|
270
|
+
const [main, ...linked] = await listWorktrees(cwd);
|
|
271
|
+
const unmanaged = linked
|
|
272
|
+
.filter((worktree) => !ours.some((row) => samePath(row.path, worktree.path)))
|
|
273
|
+
.map((worktree) => ({
|
|
274
|
+
path: worktree.path,
|
|
275
|
+
branch: worktree.branch,
|
|
276
|
+
...(worktree.detached ? { head: worktree.head } : {}),
|
|
277
|
+
unmanaged: true,
|
|
278
|
+
...(worktree.prunable ? { prunable: true } : {}),
|
|
279
|
+
}));
|
|
280
|
+
return { worktrees: [{ path: main.path, branch: main.branch, main: true }, ...rows, ...unmanaged] };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `git worktree add`: a managed worktree for a branch (the session does not
|
|
285
|
+
* move), or a read-only snapshot for a detached commit. The path argument is
|
|
286
|
+
* never used; the result names the managed path.
|
|
287
|
+
*/
|
|
288
|
+
async function rawAdd(plan, input, options) {
|
|
289
|
+
let branch = null;
|
|
290
|
+
let reset = false;
|
|
291
|
+
let detach = false;
|
|
292
|
+
const positional = [];
|
|
293
|
+
const args = plan.args.slice(1);
|
|
294
|
+
for (let index = 0; index < args.length; index++) {
|
|
295
|
+
const value = args[index];
|
|
296
|
+
if (value === '--') {
|
|
297
|
+
positional.push(...args.slice(index + 1));
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
if (value === '-b' || value === '-B') {
|
|
301
|
+
branch = args[++index];
|
|
302
|
+
reset = value === '-B';
|
|
303
|
+
if (!branch) throw new Error(`git worktree add: ${value} requires a branch name`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (value === '--reason') {
|
|
307
|
+
index++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (value.startsWith('--reason=')) continue;
|
|
311
|
+
if (value === '--detach' || value === '-d') {
|
|
312
|
+
detach = true;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (value === '--orphan' || value === '--no-checkout') {
|
|
316
|
+
throw new Error(`git worktree add: ${value} is not supported for managed worktrees`);
|
|
317
|
+
}
|
|
318
|
+
if (ADD_BARE_FLAGS.has(value)) continue;
|
|
319
|
+
if (value.startsWith('-')) throw new Error(`git worktree add: option ${value} is not supported for managed worktrees`);
|
|
320
|
+
positional.push(value);
|
|
321
|
+
}
|
|
322
|
+
const [path, commitish = null] = positional;
|
|
323
|
+
if (!path || positional.length > 2) throw new Error('git worktree add requires <path> [<commit-ish>]');
|
|
324
|
+
if (detach && branch) throw new Error('git worktree add: --detach excludes -b/-B');
|
|
325
|
+
const host = leadHost(options, 'git worktree add');
|
|
326
|
+
const repo = await repositoryAt(plan.cwd);
|
|
327
|
+
if (!repo) throw new Error(`git worktree add: ${plan.cwd} is not a git repository`);
|
|
328
|
+
const confirmLarge = input.confirm_large === true;
|
|
329
|
+
const txId = txIdOf(options, plan.command);
|
|
330
|
+
const mapped = { mapped: plan.command, requestedPath: resolve(plan.cwd, path) };
|
|
331
|
+
// A detached checkout, or a commit-ish that names no local branch: a snapshot.
|
|
332
|
+
const snapshotCommit = detach
|
|
333
|
+
? (commitish ?? 'HEAD')
|
|
334
|
+
: !branch && commitish && !(await localBranchExists(plan.cwd, commitish))
|
|
335
|
+
? commitish
|
|
336
|
+
: null;
|
|
337
|
+
if (snapshotCommit) {
|
|
338
|
+
const commit = await commitIn(plan.cwd, snapshotCommit);
|
|
339
|
+
if (!commit) throw new Error(`git worktree add: ${snapshotCommit} is not a commit`);
|
|
340
|
+
const entry = await runOn(
|
|
341
|
+
host,
|
|
342
|
+
{ op: 'snapshot', commit, confirmLarge, txId, repoCommonDir: repo.repoCommonDir },
|
|
343
|
+
options
|
|
344
|
+
);
|
|
345
|
+
return okJson({
|
|
346
|
+
...mapped,
|
|
347
|
+
snapshot: true,
|
|
348
|
+
path: entry.path,
|
|
349
|
+
commit: entry.baseCommit,
|
|
350
|
+
worktreeId: entry.id,
|
|
351
|
+
note: 'Managed read-only snapshot at the path above (not the one requested); it is removed when this turn ends, and changes made in it are discarded. The session did not move.',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
let create;
|
|
355
|
+
let startPoint = null;
|
|
356
|
+
if (branch) {
|
|
357
|
+
const exists = await localBranchExists(plan.cwd, branch);
|
|
358
|
+
if (exists) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
reset
|
|
361
|
+
? `git worktree add: -B would reset the existing branch ${branch}; managed worktrees never reset a branch (omit -B to check it out as it is)`
|
|
362
|
+
: `git worktree add: a branch named '${branch}' already exists (omit -b to check it out)`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
create = true;
|
|
366
|
+
startPoint = await commitIn(plan.cwd, commitish ?? 'HEAD');
|
|
367
|
+
if (!startPoint) throw new Error(`git worktree add: ${commitish} is not a commit`);
|
|
368
|
+
} else {
|
|
369
|
+
// git's convenience: no branch named, so the branch is the commit-ish
|
|
370
|
+
// (an existing local branch) or the path's last segment.
|
|
371
|
+
branch = commitish ?? basename(resolve(plan.cwd, path));
|
|
372
|
+
create = !(await localBranchExists(plan.cwd, branch));
|
|
373
|
+
if (create) startPoint = await commitIn(plan.cwd, 'HEAD');
|
|
374
|
+
}
|
|
375
|
+
const added = await runOn(
|
|
376
|
+
host,
|
|
377
|
+
{ op: 'add', branch, create, startPoint, confirmLarge, txId, repoCommonDir: repo.repoCommonDir },
|
|
378
|
+
options
|
|
379
|
+
);
|
|
380
|
+
return okJson({
|
|
381
|
+
...mapped,
|
|
382
|
+
...added,
|
|
383
|
+
note: `Managed worktree at the path above (not the one requested), retained by this session. The session did not move; worktree_action:"enter" with branch:"${branch}" moves it there.`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function rawRemove(plan, options) {
|
|
388
|
+
let force = false;
|
|
389
|
+
const positional = [];
|
|
390
|
+
for (const value of plan.args.slice(1)) {
|
|
391
|
+
if (value === '-f' || value === '--force') force = true;
|
|
392
|
+
else if (value.startsWith('-')) throw new Error(`git worktree remove: option ${value} is not supported`);
|
|
393
|
+
else positional.push(value);
|
|
394
|
+
}
|
|
395
|
+
if (positional.length !== 1) throw new Error('git worktree remove requires exactly one <path>');
|
|
396
|
+
const host = leadHost(options, 'git worktree remove');
|
|
397
|
+
const path = resolve(plan.cwd, positional[0]);
|
|
398
|
+
const result = await runOn(host, { op: 'remove-path', path, force }, options);
|
|
399
|
+
return transitionResult({ mapped: plan.command, path }, result);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** `git worktree prune` arguments, all validated before anything runs. */
|
|
403
|
+
function parsePruneArgs(args) {
|
|
404
|
+
let dryRun = false;
|
|
405
|
+
for (const value of args) {
|
|
406
|
+
if (value === '-n' || value === '--dry-run') dryRun = true;
|
|
407
|
+
else if (value === '-v' || value === '--verbose') continue;
|
|
408
|
+
else if (value.startsWith('-')) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`git worktree prune: option ${value} is not supported (managed prune takes -n/--dry-run and -v/--verbose)`
|
|
411
|
+
);
|
|
412
|
+
} else throw new Error(`git worktree prune: unexpected argument ${value}`);
|
|
413
|
+
}
|
|
414
|
+
return { dryRun };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The prune and its dry run share one scope: the repository of the command's
|
|
419
|
+
* directory (every repository outside one), so the preview names exactly what
|
|
420
|
+
* the real prune may touch.
|
|
421
|
+
*/
|
|
422
|
+
async function rawPrune(plan, options) {
|
|
423
|
+
const { dryRun } = parsePruneArgs(plan.args.slice(1));
|
|
424
|
+
const host = leadHost(options, 'git worktree prune');
|
|
425
|
+
if (dryRun) {
|
|
426
|
+
const { worktrees } = await listRows(options, plan.cwd);
|
|
427
|
+
return okJson({ mapped: plan.command, dryRun: true, wouldRemove: worktrees.filter((row) => row.removable) });
|
|
428
|
+
}
|
|
429
|
+
const repo = await repositoryAt(plan.cwd);
|
|
430
|
+
return okJson({
|
|
431
|
+
mapped: plan.command,
|
|
432
|
+
...(await runOn(host, { op: 'prune', repoCommonDir: repo?.repoCommonDir ?? null }, options)),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const WORKTREE_HELP = Object.freeze({
|
|
437
|
+
add: 'git worktree add [-b <branch>] [--detach] <path> [<commit-ish>]: a managed worktree for the branch (the session stays put), or a read-only snapshot for a detached commit.',
|
|
438
|
+
remove: 'git worktree remove [-f] <path>: the managed removal of that worktree.',
|
|
439
|
+
list: 'git worktree list [-v]: managed worktrees plus the repository\'s other worktrees. --porcelain stays raw git.',
|
|
440
|
+
prune: 'git worktree prune [-n|--dry-run] [-v]: the managed prune of this repository (clean, merged worktrees no session retains); -n only lists what it would remove.',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
/** -h/--help before any `--` (the help answers instead of the operation running). */
|
|
444
|
+
function wantsHelp(args) {
|
|
445
|
+
const end = args.indexOf('--');
|
|
446
|
+
return (end < 0 ? args : args.slice(0, end)).some((value) => value === '-h' || value === '--help');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** `git worktree list` takes no managed option beyond -v; the rest is validated, not ignored. */
|
|
450
|
+
function checkListArgs(args) {
|
|
451
|
+
for (const value of args) {
|
|
452
|
+
if (value === '-v' || value === '--verbose') continue;
|
|
453
|
+
throw new Error(`git worktree list: ${value.startsWith('-') ? `option ${value} is not supported` : `unexpected argument ${value}`}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* A raw `git worktree …` command as its managed operation: `{ text, failed }`,
|
|
459
|
+
* or null for the one form that still runs as git (`list --porcelain`).
|
|
460
|
+
*/
|
|
461
|
+
export async function executeRawWorktree(plan, input, options = {}) {
|
|
462
|
+
const sub = String(plan.args[0] || '');
|
|
463
|
+
if (WORKTREE_HELP[sub] && wantsHelp(plan.args.slice(1))) {
|
|
464
|
+
return { text: okJson({ mapped: plan.command, help: WORKTREE_HELP[sub] }), failed: false };
|
|
465
|
+
}
|
|
466
|
+
if (sub === 'list' && plan.args.includes('--porcelain')) return null;
|
|
467
|
+
try {
|
|
468
|
+
if (sub === 'list') checkListArgs(plan.args.slice(1));
|
|
469
|
+
if (sub === 'list') return { text: okJson({ mapped: plan.command, ...(await listRows(options, plan.cwd)) }), failed: false };
|
|
470
|
+
if (sub === 'add') return { text: await rawAdd(plan, input, options), failed: false };
|
|
471
|
+
if (sub === 'remove') return { text: await rawRemove(plan, options), failed: false };
|
|
472
|
+
if (sub === 'prune') return { text: await rawPrune(plan, options), failed: false };
|
|
473
|
+
if (FIXED_LOCATION_SUBCOMMANDS.has(sub)) {
|
|
474
|
+
return {
|
|
475
|
+
text: fail(
|
|
476
|
+
`git worktree ${sub} is refused: Mixdog-managed worktrees stay at their fixed, locked locations. Use git worktree list/remove/prune or action:"worktree"`
|
|
477
|
+
),
|
|
478
|
+
failed: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
text: fail(`git worktree ${sub || '(no subcommand)'} is not supported; use add, remove, list, prune or action:"worktree"`),
|
|
483
|
+
failed: true,
|
|
484
|
+
};
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return { text: failureText(error, { raw: true }), failed: true };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Validation of action:"worktree" input; the problem text, or null. */
|
|
491
|
+
function worktreeInputError(input) {
|
|
492
|
+
const action = input.worktree_action;
|
|
493
|
+
if (!WORKTREE_TOOL_ACTIONS.includes(action)) {
|
|
494
|
+
return `git action:worktree requires worktree_action: ${WORKTREE_TOOL_ACTIONS.join(', ')}`;
|
|
495
|
+
}
|
|
496
|
+
for (const field of ['command', 'diff_id', 'change_ids', 'include_stage_ids', 'output_limit']) {
|
|
497
|
+
if (input[field] !== undefined) return `git action:worktree does not take ${field}`;
|
|
498
|
+
}
|
|
499
|
+
const only = (field, allowed) =>
|
|
500
|
+
input[field] !== undefined && !allowed.includes(action)
|
|
501
|
+
? `${field} applies to worktree_action ${allowed.join(', ')}, not ${action}`
|
|
502
|
+
: null;
|
|
503
|
+
const misplaced =
|
|
504
|
+
only('session_id', ['enter', 'return', 'merge', 'remove']) ||
|
|
505
|
+
only('name', ['enter']) ||
|
|
506
|
+
only('branch', ['enter']) ||
|
|
507
|
+
only('create', ['enter']) ||
|
|
508
|
+
only('carry', ['enter']) ||
|
|
509
|
+
only('confirm_large', ['enter']) ||
|
|
510
|
+
only('force', ['remove']);
|
|
511
|
+
if (misplaced) return misplaced;
|
|
512
|
+
if (input.session_id !== undefined && (typeof input.session_id !== 'string' || !input.session_id.trim())) {
|
|
513
|
+
return 'session_id must be a session id';
|
|
514
|
+
}
|
|
515
|
+
if (input.create !== undefined && !input.branch) return 'create needs branch';
|
|
516
|
+
if (input.branch !== undefined && (typeof input.branch !== 'string' || !input.branch.trim())) {
|
|
517
|
+
return 'branch must be a branch name';
|
|
518
|
+
}
|
|
519
|
+
if (input.branch && (input.name !== undefined || input.carry !== undefined)) {
|
|
520
|
+
return 'branch excludes name and carry (the branch names the worktree; changes stay in the project folder)';
|
|
521
|
+
}
|
|
522
|
+
if (input.carry !== undefined && !CARRY_CHOICES.includes(input.carry)) return 'carry must be leave or carry';
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** action:"worktree": one managed-worktree operation of the caller's (or `session_id`'s) session. */
|
|
527
|
+
export async function executeWorktreeAction(input, workDir, options = {}) {
|
|
528
|
+
const problem = worktreeInputError(input);
|
|
529
|
+
if (problem) return fail(problem);
|
|
530
|
+
const action = input.worktree_action;
|
|
531
|
+
try {
|
|
532
|
+
if (action === 'list') return okJson({ worktree_action: action, ...(await listRows(options, resolve(workDir || process.cwd()))) });
|
|
533
|
+
const host = leadHost(options, `git worktree_action ${action}`);
|
|
534
|
+
const sessionId = input.session_id?.trim() || null;
|
|
535
|
+
const request = {
|
|
536
|
+
op: action,
|
|
537
|
+
...(sessionId ? { sessionId } : {}),
|
|
538
|
+
...(action === 'enter'
|
|
539
|
+
? {
|
|
540
|
+
name: input.name,
|
|
541
|
+
branch: input.branch?.trim() || null,
|
|
542
|
+
create: input.create === true,
|
|
543
|
+
carry: input.carry,
|
|
544
|
+
confirmLarge: input.confirm_large === true,
|
|
545
|
+
}
|
|
546
|
+
: {}),
|
|
547
|
+
...(action === 'remove' ? { force: input.force === true } : {}),
|
|
548
|
+
...(action === 'prune' ? { repoCommonDir: (await repositoryAt(resolve(workDir || process.cwd())))?.repoCommonDir ?? null } : {}),
|
|
549
|
+
txId: txIdOf(options, action),
|
|
550
|
+
};
|
|
551
|
+
const result = await runOn(host, request, options);
|
|
552
|
+
if (action === 'prune') return okJson({ worktree_action: action, ...result });
|
|
553
|
+
return transitionResult({ worktree_action: action, ...(sessionId ? { sessionId } : {}) }, result);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
return failureText(error);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
@@ -344,6 +344,12 @@ export function markShellJobTerminationUnconfirmed(jobId) {
|
|
|
344
344
|
return patchRunningShellJobRecord(jobId, { terminationUnconfirmed: true });
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/** Keep the bot-chain hop of the turn that launched a running job on its record,
|
|
348
|
+
* so a completion recovered after a restart returns at it (bot-hops.mjs). */
|
|
349
|
+
export function recordShellJobHop(jobId, hop) {
|
|
350
|
+
return patchRunningShellJobRecord(jobId, { hop });
|
|
351
|
+
}
|
|
352
|
+
|
|
347
353
|
function patchRunningShellJobRecord(jobId, patch) {
|
|
348
354
|
const key = validJobId(jobId);
|
|
349
355
|
if (!key) return Promise.resolve(false);
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
listShellJobRecords,
|
|
32
32
|
markShellJobTerminationUnconfirmed,
|
|
33
33
|
publishShellJobRecord,
|
|
34
|
+
recordShellJobHop,
|
|
34
35
|
registerTerminationHold,
|
|
35
36
|
retireShellJobRecord,
|
|
36
37
|
takeReleaseRequest,
|
|
@@ -68,6 +69,7 @@ function prepareTrackedKill(entry) {
|
|
|
68
69
|
);
|
|
69
70
|
}
|
|
70
71
|
import { isPidAlive } from '../../../../shared/pid-liveness.mjs';
|
|
72
|
+
import { hopOf } from '../../../../shared/bot-hops.mjs';
|
|
71
73
|
import {
|
|
72
74
|
renderShellCompletionEnvelope,
|
|
73
75
|
shellCompletionInstruction,
|
|
@@ -456,6 +458,10 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
|
456
458
|
: jobNotifyCtxByJobId.get(jobId);
|
|
457
459
|
if (!jobId || !getNativeTask(jobId) || backgroundShellJobWatchers.has(jobId)) return;
|
|
458
460
|
if (ctx) jobNotifyCtxByJobId.set(jobId, ctx);
|
|
461
|
+
// The launching turn's bot-chain hop travels with the job: on its notify
|
|
462
|
+
// context, and on its record for a completion recovered after a restart.
|
|
463
|
+
const hop = hopOf(notifyCtx?.hop) || ctx?.hop || 0;
|
|
464
|
+
if (hop) void recordShellJobHop(jobId, hop);
|
|
459
465
|
let settled = false;
|
|
460
466
|
let unsubscribe = () => {};
|
|
461
467
|
const cancel = () => {
|
|
@@ -501,7 +507,7 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
|
501
507
|
text: completion.notification,
|
|
502
508
|
resultType: 'shell_task_result',
|
|
503
509
|
instruction: completion.instruction,
|
|
504
|
-
context: ctx || { callerSessionId: owner },
|
|
510
|
+
context: ctx || { callerSessionId: owner, hop },
|
|
505
511
|
enqueueFallback: enqueueCompletionFallback,
|
|
506
512
|
logPrefix: 'shell-jobs',
|
|
507
513
|
});
|
|
@@ -663,7 +669,7 @@ export async function reconcileRecoveredShellJobCompletions() {
|
|
|
663
669
|
text: completion.notification,
|
|
664
670
|
resultType: 'shell_task_result',
|
|
665
671
|
instruction: completion.instruction,
|
|
666
|
-
context: { callerSessionId: owner },
|
|
672
|
+
context: { callerSessionId: owner, hop: record.hop },
|
|
667
673
|
enqueueFallback: enqueueCompletionFallback,
|
|
668
674
|
logPrefix: 'shell-jobs-recovery',
|
|
669
675
|
});
|