@north-light/crouter 0.3.198 → 0.3.199
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/dist/api/client.d.ts +18 -18
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +81 -34
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +6 -6
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +31 -26
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +3 -3
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +17 -6
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +21 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +39 -115
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.js +19 -5
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +44 -2
- package/dist/types.js +7 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
package/dist/core/worktree.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { gitSync } from './git.js';
|
|
@@ -76,7 +76,6 @@ function checkoutPathForBranchInList(porcelain, branch) {
|
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
78
|
const WORKTREE_LOCK_WAIT_MS = 30_000;
|
|
79
|
-
const WORKTREE_CLEANUP_TIMEOUT_MS = 5_000;
|
|
80
79
|
function commonGitDir(repoRoot) {
|
|
81
80
|
const commonDir = runGit(repoRoot, ['rev-parse', '--git-common-dir'], 'git_common_dir_failed', 'Inspect the repository git metadata, then retry.');
|
|
82
81
|
return realpathSync(resolve(repoRoot, commonDir));
|
|
@@ -240,18 +239,23 @@ export function isRebaseInProgress(cwd) {
|
|
|
240
239
|
/** The ONE authoritative closed-worktree state transition, shared by both
|
|
241
240
|
* closing paths (explicit `closeManagedWorktreeLocked` land, and the clean
|
|
242
241
|
* `autoDropCleanManagedWorktreeLocked` push-final drop): persist
|
|
243
|
-
* `managed_worktree.state: 'closed'
|
|
244
|
-
* the durable base checkout
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
242
|
+
* `managed_worktree.state: 'closed'`, name its cleanup state, and repoint the
|
|
243
|
+
* node's launch `cwd` to the durable base checkout atomically in the same
|
|
244
|
+
* `updateNode` write. A live node keeps its checkout intact for explicit
|
|
245
|
+
* post-exit cleanup: `git worktree remove` recursively deletes the caller's
|
|
246
|
+
* cwd and cannot safely run against it. Both closing paths MUST go through this helper instead of
|
|
247
|
+
* hand-rolling the `updateNode` call, so neither can drift out of sync. */
|
|
248
|
+
function persistClosedWorktreeState(nodeId, wt, cleanup = 'pending') {
|
|
249
|
+
updateNode(nodeId, { cwd: wt.repo_root, managed_worktree: { ...wt, state: 'closed', cleanup, closed: new Date().toISOString() } });
|
|
250
|
+
}
|
|
251
|
+
function deferredCleanupResult(wt) {
|
|
252
|
+
const remove = `git -C ${shellQuote(wt.repo_root)} worktree remove ${shellQuote(wt.path)}`;
|
|
253
|
+
return {
|
|
254
|
+
worktree_removed: false,
|
|
255
|
+
worktree_remove_error: `cleanup pending: the caller checkout at ${wt.path} remains intact. After its node exits, run \`${remove}\` after confirming it is safe to delete.`,
|
|
256
|
+
branch_deleted: false,
|
|
257
|
+
branch_delete_error: `cleanup pending: local branch ${wt.branch} remains checked out at ${wt.path}; delete it only after that checkout is removed and its landed commit is still contained in ${wt.base_ref}.`,
|
|
258
|
+
};
|
|
255
259
|
}
|
|
256
260
|
function closeManagedWorktreeLocked(nodeId) {
|
|
257
261
|
const node = getNode(nodeId);
|
|
@@ -289,18 +293,18 @@ function closeManagedWorktreeLocked(nodeId) {
|
|
|
289
293
|
}
|
|
290
294
|
const dirty = statusPorcelain(wt.path);
|
|
291
295
|
if (dirty !== '') {
|
|
292
|
-
throw new WorktreeError('dirty_worktree', 'managed worktree has uncommitted changes', `Commit the work in ${wt.path} (or ask the
|
|
296
|
+
throw new WorktreeError('dirty_worktree', 'managed worktree has uncommitted changes', `Commit the work in ${wt.path} (or ask the user with \`crtr human send\` if you do not know whether to keep it), then rerun \`crtr node worktree close\`.`);
|
|
293
297
|
}
|
|
294
298
|
const branch = currentBranch(wt.path);
|
|
295
299
|
if (branch !== wt.branch) {
|
|
296
300
|
const found = branch === 'HEAD' ? 'detached HEAD' : branch;
|
|
297
|
-
throw new WorktreeError('wrong_branch', `managed worktree is not on its recorded branch (expected ${wt.branch}, found ${found})`, `Check out ${wt.branch} in ${wt.path} before closing, or ask the
|
|
301
|
+
throw new WorktreeError('wrong_branch', `managed worktree is not on its recorded branch (expected ${wt.branch}, found ${found})`, `Check out ${wt.branch} in ${wt.path} before closing, or ask the user with \`crtr human send\` if you are unsure why it changed, then rerun \`crtr node worktree close\`.`);
|
|
298
302
|
}
|
|
299
303
|
const baseSha = resolveLandableBase(wt.repo_root, wt.base_ref);
|
|
300
304
|
const rebase = gitSync(['rebase', baseSha], wt.path);
|
|
301
305
|
if (rebase.status !== 0) {
|
|
302
306
|
const detail = (rebase.stderr.trim() || rebase.stdout.trim()).trim();
|
|
303
|
-
throw new WorktreeError('rebase_failed', `rebase onto local base branch '${wt.base_ref}' did not complete`, `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the
|
|
307
|
+
throw new WorktreeError('rebase_failed', `rebase onto local base branch '${wt.base_ref}' did not complete`, `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the user with \`crtr human send\`.`, detail);
|
|
304
308
|
}
|
|
305
309
|
const landedSha = runGit(wt.path, ['rev-parse', 'HEAD'], 'landed_sha_failed', 'Inspect the worktree checkout manually, then rerun `crtr node worktree close`.');
|
|
306
310
|
// Fast-forward the local base branch to the worktree tip rebased onto the
|
|
@@ -309,7 +313,7 @@ function closeManagedWorktreeLocked(nodeId) {
|
|
|
309
313
|
// captured tip, so neither can overwrite a concurrent base change.
|
|
310
314
|
const fastForward = gitSync(['merge-base', '--is-ancestor', baseSha, landedSha], wt.repo_root);
|
|
311
315
|
if (fastForward.status !== 0) {
|
|
312
|
-
throw new WorktreeError('land_base_failed', `rebased worktree is not a fast-forward of local base branch '${wt.base_ref}'`, 'Ask the
|
|
316
|
+
throw new WorktreeError('land_base_failed', `rebased worktree is not a fast-forward of local base branch '${wt.base_ref}'`, 'Ask the user with `crtr human send` how to land this managed worktree.', (fastForward.stderr.trim() || fastForward.stdout.trim()).trim());
|
|
313
317
|
}
|
|
314
318
|
const baseCheckout = checkoutPathForBranch(wt.repo_root, wt.base_ref);
|
|
315
319
|
if (baseCheckout !== null) {
|
|
@@ -319,7 +323,7 @@ function closeManagedWorktreeLocked(nodeId) {
|
|
|
319
323
|
const detail = checkoutHead.status !== 0
|
|
320
324
|
? (checkoutHead.stderr.trim() || actualHead).trim()
|
|
321
325
|
: `expected ${baseSha}, found ${actualHead}`;
|
|
322
|
-
throw new WorktreeError('land_base_failed', `local base branch '${wt.base_ref}' changed while this worktree was closing`, `Reconcile the base checkout at ${baseCheckout}, then rerun \`crtr node worktree close\`. If you are unsure, ask the
|
|
326
|
+
throw new WorktreeError('land_base_failed', `local base branch '${wt.base_ref}' changed while this worktree was closing`, `Reconcile the base checkout at ${baseCheckout}, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`, detail);
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
329
|
const land = baseCheckout !== null
|
|
@@ -328,46 +332,21 @@ function closeManagedWorktreeLocked(nodeId) {
|
|
|
328
332
|
if (land.status !== 0) {
|
|
329
333
|
const detail = (land.stderr.trim() || land.stdout.trim()).trim();
|
|
330
334
|
throw new WorktreeError('land_base_failed', `could not fast-forward local base branch '${wt.base_ref}' to this worktree`, baseCheckout !== null
|
|
331
|
-
? `Local base branch '${wt.base_ref}' is checked out at ${baseCheckout} in a state that blocks a fast-forward (uncommitted changes, or it has diverged). Reconcile it there, then rerun \`crtr node worktree close\`. If you are unsure, ask the
|
|
332
|
-
: 'Ask the
|
|
333
|
-
}
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
persistClosedWorktreeState(nodeId, wt);
|
|
341
|
-
const { worktreeRemoved, worktreeRemoveError } = bestEffortRemoveWorktreeCheckout(wt, { timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS });
|
|
342
|
-
// Delete via the atomic base+branch transaction, keyed on `landedSha` for
|
|
343
|
-
// BOTH refs: the fast-forward above just moved `refs/heads/<wt.base_ref>`
|
|
344
|
-
// to `landedSha`, and `wt.branch`'s tip (as of `landedSha`) is provably
|
|
345
|
-
// contained in it. Binding both refs into one transaction closes the
|
|
346
|
-
// window a branch-only check leaves open: if a concurrent commit lands a
|
|
347
|
-
// new tip on this branch, or a concurrent force-reset moves the base off
|
|
348
|
-
// `landedSha`, before this call runs, the transaction fails closed and
|
|
349
|
-
// preserves the branch — it cannot ever delete the last ref to work that
|
|
350
|
-
// is no longer contained in the base. A refusal here is cosmetic and
|
|
351
|
-
// non-fatal exactly as before: the land already happened (state is closed
|
|
352
|
-
// above), only cleanup of the source branch is best-effort. Skip the
|
|
353
|
-
// attempt entirely when the checkout itself could not be removed — this
|
|
354
|
-
// branch is still checked out somewhere, so deleting its ref underneath
|
|
355
|
-
// that checkout must never be attempted.
|
|
356
|
-
const { branchDeleted, branchDeleteError } = worktreeRemoved
|
|
357
|
-
? bestEffortDeleteBranchWithBaseProof(wt, landedSha, landedSha)
|
|
358
|
-
: {
|
|
359
|
-
branchDeleted: false,
|
|
360
|
-
branchDeleteError: `local branch ${wt.branch} was left in place because its checkout at ${wt.path} could not be removed automatically; resolve the checkout, then run \`git -C ${shellQuote(wt.repo_root)} branch -D ${shellQuote(wt.branch)}\` after confirming it is merged.`,
|
|
361
|
-
};
|
|
335
|
+
? `Local base branch '${wt.base_ref}' is checked out at ${baseCheckout} in a state that blocks a fast-forward (uncommitted changes, or it has diverged). Reconcile it there, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`
|
|
336
|
+
: 'Ask the user with `crtr human send` how to land this managed worktree.', detail);
|
|
337
|
+
}
|
|
338
|
+
// The caller broker still runs from wt.path after this API response. Do not
|
|
339
|
+
// invoke `git worktree remove`: it recursively deletes the caller's cwd and
|
|
340
|
+
// a timeout can kill Git between individual unlink operations. Keep the
|
|
341
|
+
// checkout and its checked-out branch intact in the named pending-cleanup
|
|
342
|
+
// state; the successful land is still the point of no return for this node.
|
|
343
|
+
persistClosedWorktreeState(nodeId, wt, 'pending');
|
|
362
344
|
return {
|
|
363
345
|
node_id: nodeId,
|
|
364
346
|
branch: wt.branch,
|
|
365
347
|
worktree_path: wt.path,
|
|
366
348
|
landed_sha: landedSha,
|
|
367
|
-
|
|
368
|
-
...(worktreeRemoveError !== undefined ? { worktree_remove_error: worktreeRemoveError } : {}),
|
|
369
|
-
branch_deleted: branchDeleted,
|
|
370
|
-
...(branchDeleteError !== undefined ? { branch_delete_error: branchDeleteError } : {}),
|
|
349
|
+
...deferredCleanupResult(wt),
|
|
371
350
|
};
|
|
372
351
|
}
|
|
373
352
|
export function closeManagedWorktree(nodeId) {
|
|
@@ -378,114 +357,9 @@ export function closeManagedWorktree(nodeId) {
|
|
|
378
357
|
return closeManagedWorktreeLocked(nodeId);
|
|
379
358
|
return withRepositoryWorktreeLock(wt.repo_root, () => closeManagedWorktreeLocked(nodeId));
|
|
380
359
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
export function setWorktreeCleanupRunnerForTest(runner) {
|
|
385
|
-
worktreeCleanupRunner = runner ?? defaultWorktreeCleanupRunner;
|
|
386
|
-
}
|
|
387
|
-
function isValidRegisteredCheckout(output, wt) {
|
|
388
|
-
let path = null;
|
|
389
|
-
let prunable = false;
|
|
390
|
-
const samePath = (candidate) => {
|
|
391
|
-
try {
|
|
392
|
-
return realpathSync(candidate) === realpathSync(wt.path);
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
return resolve(candidate) === resolve(wt.path);
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
const matches = () => path !== null
|
|
399
|
-
&& samePath(path)
|
|
400
|
-
&& existsSync(path)
|
|
401
|
-
&& existsSync(join(path, '.git'))
|
|
402
|
-
&& !prunable;
|
|
403
|
-
for (const line of output.split('\n')) {
|
|
404
|
-
if (line.startsWith('worktree ')) {
|
|
405
|
-
if (matches())
|
|
406
|
-
return true;
|
|
407
|
-
path = line.slice('worktree '.length).trim();
|
|
408
|
-
prunable = false;
|
|
409
|
-
}
|
|
410
|
-
else if (path !== null && line.startsWith('prunable')) {
|
|
411
|
-
prunable = true;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return matches();
|
|
415
|
-
}
|
|
416
|
-
function timeoutRecoveryGuidance(wt, options) {
|
|
417
|
-
const force = `git -C ${shellQuote(wt.repo_root)} worktree remove --force ${shellQuote(wt.path)}`;
|
|
418
|
-
const prune = `git -C ${shellQuote(wt.repo_root)} worktree prune`;
|
|
419
|
-
const removeResidual = `rm -rf ${shellQuote(wt.path)}`;
|
|
420
|
-
try {
|
|
421
|
-
const inspection = worktreeCleanupRunner(['worktree', 'list', '--porcelain'], wt.repo_root, options);
|
|
422
|
-
if (inspection.status === 0) {
|
|
423
|
-
return isValidRegisteredCheckout(inspection.stdout, wt)
|
|
424
|
-
? `run \`${force}\` after confirming it is safe to delete.`
|
|
425
|
-
: `run \`${prune}\`, then run \`${removeResidual}\` after confirming the leftover directory is safe to delete.`;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
// Inspection is advisory only. Emit both recovery shapes when Git could
|
|
430
|
-
// not tell us which residual state remains.
|
|
431
|
-
}
|
|
432
|
-
return `if ${shellQuote(wt.path)} is a valid non-prunable checkout (its .git exists and its worktree-list record is not prunable), run \`${force}\`; otherwise run \`${prune}\` and then \`${removeResidual}\` after confirming it is safe to delete.`;
|
|
433
|
-
}
|
|
434
|
-
/** Best-effort `git worktree remove` shared by a real land (close) and an
|
|
435
|
-
* auto-drop (nothing was ever landed). Either way `wt.path` IS the calling
|
|
436
|
-
* node's own pi-process cwd — a process can never chdir itself out of a
|
|
437
|
-
* directory that vanishes out from under it, so once removal deletes it,
|
|
438
|
-
* every subsequent bash/chdir in this node fails permanently and the
|
|
439
|
-
* mandated post-close `crtr push final` can never run. Recreate it as an
|
|
440
|
-
* empty tombstone directory so the path still resolves regardless. */
|
|
441
|
-
function bestEffortRemoveWorktreeCheckout(wt, options = {}) {
|
|
442
|
-
const remove = worktreeCleanupRunner(['worktree', 'remove', wt.path], wt.repo_root, options);
|
|
443
|
-
const worktreeRemoved = remove.status === 0;
|
|
444
|
-
if (worktreeRemoved && existsSync(wt.path)) {
|
|
445
|
-
// `git worktree remove` can leave the (now empty) mount dir behind; tidy it best-effort.
|
|
446
|
-
try {
|
|
447
|
-
rmdirSync(wt.path);
|
|
448
|
-
}
|
|
449
|
-
catch { /* non-empty or busy — leave it */ }
|
|
450
|
-
}
|
|
451
|
-
if (!existsSync(wt.path)) {
|
|
452
|
-
try {
|
|
453
|
-
mkdirSync(wt.path, { recursive: true });
|
|
454
|
-
}
|
|
455
|
-
catch { /* best-effort; leaves cwd broken only if this also fails */ }
|
|
456
|
-
}
|
|
457
|
-
if (worktreeRemoved)
|
|
458
|
-
return { worktreeRemoved };
|
|
459
|
-
const detail = (remove.stderr.trim() || remove.stdout.trim()).trim();
|
|
460
|
-
const timeoutDetail = remove.timedOut
|
|
461
|
-
? `cleanup timed out after ${options.timeoutMs}ms`
|
|
462
|
-
: `could not be removed automatically${detail ? ` (${detail})` : ''}`;
|
|
463
|
-
const manualGuidance = remove.timedOut
|
|
464
|
-
? timeoutRecoveryGuidance(wt, options)
|
|
465
|
-
: `run \`git -C ${shellQuote(wt.repo_root)} worktree remove ${shellQuote(wt.path)}\` after confirming it is safe to delete.`;
|
|
466
|
-
return {
|
|
467
|
-
worktreeRemoved,
|
|
468
|
-
worktreeRemoveError: `the checkout at ${wt.path} ${timeoutDetail} ${manualGuidance}`,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
/** Atomic multi-ref transaction shared by close and auto-drop: as ONE Git ref
|
|
472
|
-
* transaction, verifies `refs/heads/<wt.base_ref>` still equals `baseSha`
|
|
473
|
-
* AND deletes `refs/heads/<wt.branch>` only if it still equals `branchSha`
|
|
474
|
-
* — both preconditions and the delete are checked and applied together via
|
|
475
|
-
* `git update-ref --stdin`, which commits every command in its input
|
|
476
|
-
* atomically (any failed precondition aborts the whole transaction; see
|
|
477
|
-
* `rollbackManagedWorktreeLocked` above for the single-ref precursor of
|
|
478
|
-
* this pattern). A single-ref compare-and-delete on the branch alone is not
|
|
479
|
-
* enough: both callers prove containment against a specific base SHA, and
|
|
480
|
-
* that base can move (an external force-reset) in the window between the
|
|
481
|
-
* proof and this call just as easily as the branch can. Binding the base's
|
|
482
|
-
* live value into the SAME transaction as the branch delete closes that
|
|
483
|
-
* window — the delete can only succeed at the instant both refs are
|
|
484
|
-
* simultaneously exactly what was proven, so a base rewrite can never leave
|
|
485
|
-
* a proven commit unreachable from any head. */
|
|
486
|
-
function bestEffortDeleteBranchWithBaseProof(wt, branchSha, baseSha) {
|
|
487
|
-
return bestEffortDeleteBranchWithContainmentProof(wt, branchSha, `refs/heads/${wt.base_ref}`, baseSha);
|
|
488
|
-
}
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Push-final auto-drop of a clean, zero-commit managed worktree
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
489
363
|
/** Delete a source branch only while the ref that proved it already delivered
|
|
490
364
|
* is still at the exact inspected SHA. `containingRef` can be the recorded
|
|
491
365
|
* local base or that base's remote-tracking upstream. */
|
|
@@ -573,7 +447,7 @@ function proveAbsentPathManagedWorktree(wt) {
|
|
|
573
447
|
* work is delivered. This deliberately never recommends `worktree close`:
|
|
574
448
|
* that command cannot operate on the absent checkout. */
|
|
575
449
|
export function missingWorktreeRecoveryHint(wt) {
|
|
576
|
-
return `The recorded checkout path is gone, but crtr cannot prove ${wt.branch} is already delivered. Push or land ${wt.branch}, then rerun \`crtr push final\`; if you cannot establish what happened to the branch, ask the
|
|
450
|
+
return `The recorded checkout path is gone, but crtr cannot prove ${wt.branch} is already delivered. Push or land ${wt.branch}, then rerun \`crtr push final\`; if you cannot establish what happened to the branch, ask the user with \`crtr human send\`.`;
|
|
577
451
|
}
|
|
578
452
|
/** Fix B's shared "nothing to land" test — the SAME clean-worktree definition
|
|
579
453
|
* `close`'s own dirty check now naturally satisfies once `.git/info/exclude`
|
|
@@ -582,20 +456,11 @@ export function missingWorktreeRecoveryHint(wt) {
|
|
|
582
456
|
* be a git-ancestor of (or equal to) the CURRENT tip of its recorded base
|
|
583
457
|
* branch — i.e. landing it would be a pure no-op. Any ambiguity (missing
|
|
584
458
|
* path, mid-rebase, wrong branch, an unresolvable base, or a real ancestry
|
|
585
|
-
* divergence) returns null: this function only ever proves
|
|
459
|
+
* divergence) returns null: this function only ever proves a no-op land
|
|
586
460
|
* when a live git check establishes it, never on a heuristic guess, so real
|
|
587
|
-
* unlanded work always falls through to the
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
* re-proves containment against the LIVE base immediately before the
|
|
591
|
-
* destructive delete, refuses outright if the checkout cannot be removed,
|
|
592
|
-
* and conditions the delete itself on BOTH `branchSha` and that same live
|
|
593
|
-
* base SHA via `bestEffortDeleteBranchWithBaseProof`'s atomic multi-ref
|
|
594
|
-
* transaction — those checks, not this one, are what close the TOCTOU gap.
|
|
595
|
-
* Exported (alongside
|
|
596
|
-
* `autoDropCleanManagedWorktreeLocked`) purely so regression tests can
|
|
597
|
-
* inject a race in the window between proving and cleanup; production code
|
|
598
|
-
* must always go through `autoDropCleanManagedWorktree`. */
|
|
461
|
+
* unlanded work always falls through to the explicit-close guard. The locked
|
|
462
|
+
* path re-checks this predicate before recording deferred cleanup; production
|
|
463
|
+
* code must always go through `autoDropCleanManagedWorktree`. */
|
|
599
464
|
export function proveCleanEmptyManagedWorktree(wt) {
|
|
600
465
|
if (wt.state !== 'open')
|
|
601
466
|
return null;
|
|
@@ -627,13 +492,13 @@ export function proveCleanEmptyManagedWorktree(wt) {
|
|
|
627
492
|
}
|
|
628
493
|
/** Conclude an absent-path record only after `proveAbsentPathManagedWorktree`
|
|
629
494
|
* has established the same no-unlanded-work property required by auto-drop.
|
|
630
|
-
* There is no checkout left to remove; persistClosedWorktreeState
|
|
631
|
-
*
|
|
495
|
+
* There is no checkout left to remove; persistClosedWorktreeState records the
|
|
496
|
+
* completed cleanup and repoints cwd to the durable base checkout. */
|
|
632
497
|
function autoDropAbsentPathManagedWorktreeLocked(nodeId, wt, proof) {
|
|
633
498
|
if (existsSync(wt.path))
|
|
634
499
|
return null;
|
|
635
500
|
if (proof.branchSha === null) {
|
|
636
|
-
persistClosedWorktreeState(nodeId, wt);
|
|
501
|
+
persistClosedWorktreeState(nodeId, wt, 'complete');
|
|
637
502
|
return { node_id: nodeId, branch: wt.branch, worktree_path: wt.path, worktree_removed: true, branch_deleted: true };
|
|
638
503
|
}
|
|
639
504
|
if (proof.containingRef === undefined || proof.containingSha === undefined)
|
|
@@ -641,93 +506,26 @@ function autoDropAbsentPathManagedWorktreeLocked(nodeId, wt, proof) {
|
|
|
641
506
|
const { branchDeleted } = bestEffortDeleteBranchWithContainmentProof(wt, proof.branchSha, proof.containingRef, proof.containingSha);
|
|
642
507
|
if (!branchDeleted)
|
|
643
508
|
return null;
|
|
644
|
-
persistClosedWorktreeState(nodeId, wt);
|
|
509
|
+
persistClosedWorktreeState(nodeId, wt, 'complete');
|
|
645
510
|
return { node_id: nodeId, branch: wt.branch, worktree_path: wt.path, worktree_removed: true, branch_deleted: true };
|
|
646
511
|
}
|
|
647
|
-
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* refuses: the caller (`autoDropCleanManagedWorktree`, and `push final`
|
|
652
|
-
* above it) then falls through to the existing `open_managed_worktree`
|
|
653
|
-
* guard exactly as if this worktree had never looked droppable. In every
|
|
654
|
-
* refusal case the branch, and every commit reachable from it, is
|
|
655
|
-
* unconditionally preserved — that is the actual safety property this
|
|
656
|
-
* function guarantees. A checkout-removal refusal in particular must be
|
|
657
|
-
* treated as a full refusal, not a partial success: if the checkout cannot
|
|
658
|
-
* be removed (e.g. a file reappeared after the clean predicate ran), the
|
|
659
|
-
* branch is still checked out somewhere, so this function must never reach
|
|
660
|
-
* the ref-deletion transaction or mutate managed-worktree state — both the
|
|
661
|
-
* branch and the live checkout stay fully recoverable and the caller's
|
|
662
|
-
* `open_managed_worktree` guard fires. */
|
|
512
|
+
/** Re-check a clean auto-drop before closing its record. `push final` runs
|
|
513
|
+
* inside the owner broker, so it has the same destructive-cwd boundary as
|
|
514
|
+
* explicit close: removal and branch deletion are deferred until that owner
|
|
515
|
+
* has exited. */
|
|
663
516
|
export function autoDropCleanManagedWorktreeLocked(nodeId, wt, proof) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
// we get here — `withRepositoryWorktreeLock` only serializes crtr's OWN
|
|
667
|
-
// worktree mutations, not ordinary `git commit`/`git reset` run by any
|
|
668
|
-
// other process in the checkout, nor an external force-reset of the base
|
|
669
|
-
// branch. Re-resolving the base and re-running the ancestor check fresh
|
|
670
|
-
// (rather than trusting `proof.baseSha`) means a base fast-forward keeps a
|
|
671
|
-
// genuinely-contained tip provably contained, while any other base
|
|
672
|
-
// rewrite (or the base vanishing) fails this closed before we touch
|
|
673
|
-
// anything. This SAME `liveBaseSha` is what the ref transaction below
|
|
674
|
-
// verifies again at the moment of the delete — re-checking it here only
|
|
675
|
-
// decides whether to attempt cleanup at all; the transaction is what
|
|
676
|
-
// actually closes the race.
|
|
677
|
-
let liveBaseSha;
|
|
678
|
-
try {
|
|
679
|
-
liveBaseSha = resolveLandableBase(wt.repo_root, wt.base_ref);
|
|
680
|
-
}
|
|
681
|
-
catch {
|
|
682
|
-
return null;
|
|
683
|
-
}
|
|
684
|
-
const stillContained = gitSync(['merge-base', '--is-ancestor', proof.branchSha, liveBaseSha], wt.repo_root);
|
|
685
|
-
if (stillContained.status !== 0)
|
|
686
|
-
return null;
|
|
687
|
-
// Checkout removal has to come BEFORE the ref-deletion transaction, not
|
|
688
|
-
// after: `git worktree remove`'s cleanliness check needs the checked-out
|
|
689
|
-
// branch's ref to still resolve to compute a diff, so deleting the ref
|
|
690
|
-
// first turns every tracked file into what looks like an unresolvable path
|
|
691
|
-
// and `worktree remove` refuses outright (verified directly against git).
|
|
692
|
-
// This ordering is still safe: removal alone never touches
|
|
693
|
-
// `refs/heads/<branch>` or anything reachable from it — a commit that
|
|
694
|
-
// lands here is fully preserved as an ordinary (no longer checked-out)
|
|
695
|
-
// local branch, exactly as droppable-or-not as it was, until the
|
|
696
|
-
// transaction below either consumes it or refuses. A removal REFUSAL,
|
|
697
|
-
// though, must stop cleanup here entirely: low-level ref deletion bypasses
|
|
698
|
-
// Git's own checked-out-branch protection, so deleting the branch while
|
|
699
|
-
// its checkout is still live would strand that checkout with no branch to
|
|
700
|
-
// point at it.
|
|
701
|
-
const { worktreeRemoved } = bestEffortRemoveWorktreeCheckout(wt);
|
|
702
|
-
if (!worktreeRemoved)
|
|
703
|
-
return null;
|
|
704
|
-
// THE safety boundary: this only succeeds if `refs/heads/<wt.base_ref>` is
|
|
705
|
-
// still exactly `liveBaseSha` AND `refs/heads/<branch>` is still exactly
|
|
706
|
-
// `proof.branchSha`, verified and applied as one atomic transaction. A
|
|
707
|
-
// commit made anywhere in the checkout, or an external rewrite of the
|
|
708
|
-
// base, at any point up to and including this instant makes the whole
|
|
709
|
-
// transaction fail closed instead of silently destroying new work. On
|
|
710
|
-
// refusal the branch, and everything reachable from it, is fully intact
|
|
711
|
-
// and enumerable; only the convenience checkout may already be gone
|
|
712
|
-
// (tombstoned by the removal above), recoverable with a fresh
|
|
713
|
-
// `git worktree add` against the still-live branch. `state` is left
|
|
714
|
-
// untouched ('open') below specifically so this refusal is never mistaken
|
|
715
|
-
// for a completed drop.
|
|
716
|
-
const { branchDeleted } = bestEffortDeleteBranchWithBaseProof(wt, proof.branchSha, liveBaseSha);
|
|
717
|
-
if (!branchDeleted)
|
|
517
|
+
const current = proveCleanEmptyManagedWorktree(wt);
|
|
518
|
+
if (current === null || current.branchSha !== proof.branchSha)
|
|
718
519
|
return null;
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
// close — a later revive's tombstone-vs-missing-cwd check depends on it.
|
|
724
|
-
persistClosedWorktreeState(nodeId, wt);
|
|
520
|
+
// The owner is still running in this checkout while it publishes final.
|
|
521
|
+
// Preserve its files and checked-out branch; physical cleanup is explicitly
|
|
522
|
+
// recorded as pending rather than racing Git's recursive removal.
|
|
523
|
+
persistClosedWorktreeState(nodeId, wt, 'pending');
|
|
725
524
|
return {
|
|
726
525
|
node_id: nodeId,
|
|
727
526
|
branch: wt.branch,
|
|
728
527
|
worktree_path: wt.path,
|
|
729
|
-
|
|
730
|
-
branch_deleted: true,
|
|
528
|
+
...deferredCleanupResult(wt),
|
|
731
529
|
};
|
|
732
530
|
}
|
|
733
531
|
/** `push final`'s escape hatch: a managed worktree that has
|
|
@@ -737,7 +535,7 @@ export function autoDropCleanManagedWorktreeLocked(nodeId, wt, proof) {
|
|
|
737
535
|
* that never had anything to close in the first place (a read-only
|
|
738
536
|
* review node). Returns null and changes nothing when the worktree is not
|
|
739
537
|
* PROVABLY empty-and-clean by `proveCleanEmptyManagedWorktree`, OR when a
|
|
740
|
-
* concurrent mutation invalidates that proof before the
|
|
538
|
+
* concurrent mutation invalidates that proof before the record is closed
|
|
741
539
|
* (see `autoDropCleanManagedWorktreeLocked`); the caller's existing
|
|
742
540
|
* `open_managed_worktree` guard then still applies unchanged — real
|
|
743
541
|
* commits, real uncommitted changes, and a lost race all require the
|
|
@@ -757,8 +555,8 @@ export function autoDropCleanManagedWorktree(nodeId) {
|
|
|
757
555
|
const locked = openManagedWorktreeForNode(nodeId);
|
|
758
556
|
if (locked === null)
|
|
759
557
|
return null;
|
|
760
|
-
// Re-prove the matching predicate under the lock before
|
|
761
|
-
//
|
|
558
|
+
// Re-prove the matching predicate under the lock before changing state.
|
|
559
|
+
// A path that reappeared follows the ordinary checkout path.
|
|
762
560
|
if (!existsSync(locked.path)) {
|
|
763
561
|
const proof = proveAbsentPathManagedWorktree(locked);
|
|
764
562
|
return proof === null ? null : autoDropAbsentPathManagedWorktreeLocked(nodeId, locked, proof);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|