@north-light/crouter 0.3.198 → 0.3.200
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__/helpers/harness.js +17 -55
- 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/broker-crash-teardown.test.js +4 -5
- 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 +6 -5
- 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
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { test, before, after, beforeEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, existsSync,
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, existsSync, statSync, writeFileSync, chmodSync } from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { gitSync } from '../../git.js';
|
|
8
|
-
import {
|
|
9
|
-
import { createManagedWorktree, closeManagedWorktree, managedWorktreePath, rollbackManagedWorktree, setWorktreeCleanupRunnerForTest } from '../../worktree.js';
|
|
8
|
+
import { createManagedWorktree, closeManagedWorktree, managedWorktreePath, rollbackManagedWorktree } from '../../worktree.js';
|
|
10
9
|
import { createNode, getNode } from '../../canvas/canvas.js';
|
|
11
10
|
import { closeDb } from '../../canvas/db.js';
|
|
12
11
|
let home;
|
|
@@ -60,14 +59,9 @@ after(() => {
|
|
|
60
59
|
rmSync(sourceDir, { recursive: true, force: true });
|
|
61
60
|
delete process.env['CRTR_HOME'];
|
|
62
61
|
});
|
|
63
|
-
// Regression for #
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
// dir is gone every subsequent bash in that node fails permanently and the
|
|
67
|
-
// mandated post-close `crtr push final` can never run. closeManagedWorktree
|
|
68
|
-
// must leave the path resolvable (as an empty tombstone dir) even though the
|
|
69
|
-
// git worktree registration itself is gone.
|
|
70
|
-
test('closeManagedWorktree recreates the worktree path as a tombstone dir so the node cwd still resolves', () => {
|
|
62
|
+
// Regression for #710: close lands from a live caller, so it must preserve the
|
|
63
|
+
// caller checkout rather than starting Git's recursive removal beneath it.
|
|
64
|
+
test('closeManagedWorktree leaves the active caller checkout intact with named deferred cleanup', () => {
|
|
71
65
|
const nodeId = 'wt-close-1';
|
|
72
66
|
const wt = createManagedWorktree(sourceDir, nodeId);
|
|
73
67
|
assert.equal(wt.base_ref, 'main', 'default base must remain backward-compatible with persisted main worktrees');
|
|
@@ -75,22 +69,16 @@ test('closeManagedWorktree recreates the worktree path as a tombstone dir so the
|
|
|
75
69
|
assert.ok(existsSync(wt.path), 'worktree checkout should exist after create');
|
|
76
70
|
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
77
71
|
const result = closeManagedWorktree(nodeId);
|
|
78
|
-
assert.equal(result.worktree_removed,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
assert.ok(
|
|
82
|
-
assert.ok(statSync(wt.path).isDirectory(), 'recreated path must be a directory');
|
|
83
|
-
assert.deepEqual(readdirSync(wt.path), [], 'tombstone dir should be empty — git already removed the real checkout');
|
|
84
|
-
// git itself no longer tracks it as a worktree.
|
|
72
|
+
assert.equal(result.worktree_removed, false);
|
|
73
|
+
assert.match(result.worktree_remove_error ?? '', /cleanup pending/);
|
|
74
|
+
assert.ok(existsSync(join(wt.path, 'README.md')), 'the caller checkout keeps its tracked files');
|
|
75
|
+
assert.ok(statSync(wt.path).isDirectory(), 'the caller cwd remains a directory');
|
|
85
76
|
const list = gitSync(['worktree', 'list', '--porcelain'], sourceDir);
|
|
86
|
-
assert.ok(
|
|
77
|
+
assert.ok(list.stdout.includes(wt.path), 'Git keeps the intact caller checkout registered');
|
|
87
78
|
const meta = getNode(nodeId);
|
|
88
79
|
assert.equal(meta?.managed_worktree?.state, 'closed');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// reapable by concurrent git GC, and not a real repo checkout even while
|
|
92
|
-
// it survives). closeManagedWorktree must repoint the persisted cwd.
|
|
93
|
-
assert.equal(meta?.cwd, wt.repo_root, 'closed node cwd must repoint to the repo root, not the removed worktree path');
|
|
80
|
+
assert.equal(meta?.managed_worktree?.cleanup, 'pending');
|
|
81
|
+
assert.equal(meta?.cwd, wt.repo_root, 'a later revive uses the durable base checkout');
|
|
94
82
|
});
|
|
95
83
|
test('two concurrent creates from linked worktrees serialize on their shared repository', async () => {
|
|
96
84
|
const worktreeModule = new URL('../../worktree.ts', import.meta.url).href;
|
|
@@ -372,7 +360,8 @@ test('close ignores crtr\'s own untracked .crouter/config.json and state.json bo
|
|
|
372
360
|
// reports a clean tree, not just that close happens not to notice.
|
|
373
361
|
assert.equal(gitSync(['status', '--porcelain'], wt.path).stdout.trim(), '', 'git status must already be clean with only crtr bookkeeping present');
|
|
374
362
|
const result = closeManagedWorktree(nodeId);
|
|
375
|
-
assert.equal(result.worktree_removed,
|
|
363
|
+
assert.equal(result.worktree_removed, false);
|
|
364
|
+
assert.match(result.worktree_remove_error ?? '', /cleanup pending/);
|
|
376
365
|
});
|
|
377
366
|
// The exclude must be narrow: it must NEVER hide real, committable node work
|
|
378
367
|
// that merely happens to live under `.crouter/` — this mirrors crouter's OWN
|
|
@@ -416,21 +405,15 @@ test('autoDropCleanManagedWorktree drops a clean, zero-commit worktree', async (
|
|
|
416
405
|
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
417
406
|
const result = autoDropCleanManagedWorktree(nodeId);
|
|
418
407
|
assert.ok(result !== null, 'a worktree with zero commits and a clean tree must be droppable');
|
|
419
|
-
assert.equal(result?.worktree_removed,
|
|
408
|
+
assert.equal(result?.worktree_removed, false);
|
|
409
|
+
assert.match(result?.worktree_remove_error ?? '', /cleanup pending/);
|
|
420
410
|
const meta = getNode(nodeId);
|
|
421
411
|
assert.equal(meta?.managed_worktree?.state, 'closed');
|
|
422
|
-
assert.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// empty tombstone dir (see `bestEffortRemoveWorktreeCheckout`), so a later
|
|
428
|
-
// `--reopen` revive's `existsSync(meta.cwd)` preflight passed anyway and
|
|
429
|
-
// launched into a non-checkout instead of failing loud or landing somewhere
|
|
430
|
-
// real. `cwd` must be durable: a real git checkout, not a reapable stand-in.
|
|
431
|
-
assert.equal(meta?.cwd, wt.repo_root, 'auto-dropped node cwd must repoint to the repo root, not the removed worktree path');
|
|
432
|
-
assert.equal(gitSync(['rev-parse', '--git-dir'], meta.cwd).status, 0, 'repointed cwd must resolve to a real git checkout a reopen/revive can launch from');
|
|
433
|
-
assert.equal(existsSync(join(wt.path, '.git')), false, 'the tombstoned worktree path must NOT itself resolve as a git checkout');
|
|
412
|
+
assert.equal(meta?.managed_worktree?.cleanup, 'pending');
|
|
413
|
+
assert.ok(gitSync(['worktree', 'list', '--porcelain'], sourceDir).stdout.includes(wt.path), 'Git keeps the caller checkout registered until it exits');
|
|
414
|
+
assert.equal(meta?.cwd, wt.repo_root, 'auto-dropped node cwd repoints to the durable base checkout');
|
|
415
|
+
assert.equal(gitSync(['rev-parse', '--git-dir'], meta.cwd).status, 0, 'repointed cwd resolves to a real git checkout');
|
|
416
|
+
assert.ok(existsSync(join(wt.path, '.git')), 'the intact caller worktree remains a git checkout');
|
|
434
417
|
});
|
|
435
418
|
// The same real-commit case close already protects: a worktree with actual
|
|
436
419
|
// unlanded work must NEVER be auto-dropped by push-final.
|
|
@@ -570,9 +553,10 @@ test('autoDropCleanManagedWorktree drops a branch with real commits already cont
|
|
|
570
553
|
assert.equal(gitSync(['rev-parse', 'main'], repo).stdout.trim(), branchSha);
|
|
571
554
|
const result = autoDropCleanManagedWorktree(nodeId);
|
|
572
555
|
assert.ok(result !== null, 'a branch whose commits are already contained in a moved base has nothing left to land');
|
|
573
|
-
assert.equal(result?.branch_deleted,
|
|
556
|
+
assert.equal(result?.branch_deleted, false);
|
|
574
557
|
assert.equal(getNode(nodeId)?.managed_worktree?.state, 'closed');
|
|
575
|
-
assert.equal(
|
|
558
|
+
assert.equal(getNode(nodeId)?.managed_worktree?.cleanup, 'pending');
|
|
559
|
+
assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], repo).status, 0, 'the caller branch remains checked out until cleanup runs after exit');
|
|
576
560
|
// The work itself is NOT lost — it stays reachable from main, which is the point.
|
|
577
561
|
assert.equal(gitSync(['merge-base', '--is-ancestor', branchSha, 'main'], repo).status, 0, 'the landed commit must remain reachable from main after the drop');
|
|
578
562
|
rmSync(repo, { recursive: true, force: true });
|
|
@@ -687,63 +671,6 @@ test('the re-verified base containment refuses when the base is rewritten betwee
|
|
|
687
671
|
// for the branch-only race, and the same one the independent re-review used
|
|
688
672
|
// to reproduce this exact Critical against `6de1c8d`.
|
|
689
673
|
// ---------------------------------------------------------------------------
|
|
690
|
-
test('auto-drop atomic delete refuses when the base is force-reset between the live ancestry check and the ref transaction', async () => {
|
|
691
|
-
const { proveCleanEmptyManagedWorktree, autoDropCleanManagedWorktreeLocked } = await import('../../worktree.js');
|
|
692
|
-
const repo = mkdtempSync(join(tmpdir(), 'crtr-worktree-auto-drop-base-window-'));
|
|
693
|
-
run(['init', '-b', 'main'], repo);
|
|
694
|
-
run(['config', 'user.email', 'test@example.com'], repo);
|
|
695
|
-
run(['config', 'user.name', 'Test'], repo);
|
|
696
|
-
writeFileSync(join(repo, 'README.md'), 'hello\n');
|
|
697
|
-
run(['add', '.'], repo);
|
|
698
|
-
run(['commit', '-m', 'init'], repo);
|
|
699
|
-
const preMergeMainSha = gitSync(['rev-parse', 'main'], repo).stdout.trim();
|
|
700
|
-
const nodeId = 'wt-auto-drop-base-window';
|
|
701
|
-
const wt = createManagedWorktree(repo, nodeId);
|
|
702
|
-
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
703
|
-
writeFileSync(join(wt.path, 'feature.txt'), 'landed elsewhere\n');
|
|
704
|
-
run(['add', '.'], wt.path);
|
|
705
|
-
run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'feature'], wt.path);
|
|
706
|
-
const branchSha = gitSync(['rev-parse', 'HEAD'], wt.path).stdout.trim();
|
|
707
|
-
// A manager fast-forwards main past this branch's own tip, exactly like
|
|
708
|
-
// the #327 positive case, so the branch proves clean-and-contained.
|
|
709
|
-
run(['merge', '--ff-only', branchSha], repo);
|
|
710
|
-
assert.equal(gitSync(['rev-parse', 'main'], repo).stdout.trim(), branchSha);
|
|
711
|
-
const proof = proveCleanEmptyManagedWorktree(wt);
|
|
712
|
-
assert.ok(proof !== null, 'the branch, now contained in the moved base, must prove droppable');
|
|
713
|
-
// Wrap `git worktree remove` — the call `autoDropCleanManagedWorktreeLocked`
|
|
714
|
-
// makes strictly AFTER its own live ancestry re-check and strictly BEFORE
|
|
715
|
-
// its ref-deletion transaction — to force-reset main back to its pre-merge
|
|
716
|
-
// tip right before the real removal runs. A sequential re-check placed
|
|
717
|
-
// anywhere before this point cannot see this rewrite; only binding the
|
|
718
|
-
// base's live value into the delete transaction itself can.
|
|
719
|
-
const wrapperDir = mkdtempSync(join(tmpdir(), 'crtr-git-wrapper-'));
|
|
720
|
-
const wrapperPath = join(wrapperDir, 'git');
|
|
721
|
-
const originalPath = process.env['PATH'] ?? '';
|
|
722
|
-
writeFileSync(wrapperPath, `#!/bin/sh
|
|
723
|
-
if [ "$1" = "worktree" ] && [ "$2" = "remove" ] && [ "$3" = "${wt.path}" ]; then
|
|
724
|
-
PATH="$CRTR_TEST_REAL_PATH" git -C "${repo}" update-ref refs/heads/main "${preMergeMainSha}"
|
|
725
|
-
fi
|
|
726
|
-
PATH="$CRTR_TEST_REAL_PATH" exec git "$@"
|
|
727
|
-
`);
|
|
728
|
-
chmodSync(wrapperPath, 0o755);
|
|
729
|
-
process.env['CRTR_TEST_REAL_PATH'] = originalPath;
|
|
730
|
-
process.env['PATH'] = `${wrapperDir}:${originalPath}`;
|
|
731
|
-
let result;
|
|
732
|
-
try {
|
|
733
|
-
result = autoDropCleanManagedWorktreeLocked(nodeId, wt, proof);
|
|
734
|
-
}
|
|
735
|
-
finally {
|
|
736
|
-
process.env['PATH'] = originalPath;
|
|
737
|
-
delete process.env['CRTR_TEST_REAL_PATH'];
|
|
738
|
-
rmSync(wrapperDir, { recursive: true, force: true });
|
|
739
|
-
}
|
|
740
|
-
assert.equal(result, null, 'a base rewritten strictly after the live ancestry check must still refuse the atomic delete');
|
|
741
|
-
assert.equal(getNode(nodeId)?.managed_worktree?.state, 'open', 'state must stay open — nothing may be marked safely dropped');
|
|
742
|
-
const refCheck = gitSync(['rev-parse', '--verify', `refs/heads/${wt.branch}`], repo);
|
|
743
|
-
assert.equal(refCheck.status, 0, 'the source branch must be preserved');
|
|
744
|
-
assert.equal(refCheck.stdout.trim(), branchSha, 'the branch must still name the proven commit — the source commit remains reachable from this head');
|
|
745
|
-
rmSync(repo, { recursive: true, force: true });
|
|
746
|
-
});
|
|
747
674
|
test('close-landed atomic delete refuses when the base is force-reset between landing and the cleanup delete', () => {
|
|
748
675
|
const repo = mkdtempSync(join(tmpdir(), 'crtr-worktree-close-base-window-'));
|
|
749
676
|
run(['init', '-b', 'main'], repo);
|
|
@@ -817,107 +744,6 @@ test('auto-drop refuses and preserves the branch and checkout when checkout remo
|
|
|
817
744
|
assert.ok(list.stdout.includes(wt.path), 'the checkout must remain registered and recoverable');
|
|
818
745
|
assert.ok(existsSync(join(wt.path, 'late-arrival.txt')), 'the live checkout, including its uncommitted file, must remain intact');
|
|
819
746
|
});
|
|
820
|
-
test('post-land cleanup timeout preserves the land and releases the repository lock', async () => {
|
|
821
|
-
const nodeId = 'wt-close-cleanup-timeout';
|
|
822
|
-
const wt = createManagedWorktree(sourceDir, nodeId);
|
|
823
|
-
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
824
|
-
writeFileSync(join(wt.path, 'cleanup-timeout-work.txt'), 'work\n');
|
|
825
|
-
run(['add', '.'], wt.path);
|
|
826
|
-
run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'cleanup timeout work'], wt.path);
|
|
827
|
-
const baseBefore = gitSync(['rev-parse', 'main'], sourceDir).stdout.trim();
|
|
828
|
-
setWorktreeCleanupRunnerForTest((args, cwd, options) => {
|
|
829
|
-
if (args[0] === 'worktree' && args[1] === 'remove' && args[2] === wt.path) {
|
|
830
|
-
assert.equal(options.timeoutMs, 5_000, 'only post-land cleanup gets the finite deadline');
|
|
831
|
-
return { status: 1, stdout: '', stderr: '', timedOut: true };
|
|
832
|
-
}
|
|
833
|
-
if (args[0] === 'worktree' && args[1] === 'list') {
|
|
834
|
-
assert.equal(options.timeoutMs, 5_000, 'residual inspection gets the same finite deadline');
|
|
835
|
-
const inspection = gitSync(args, cwd, undefined, options);
|
|
836
|
-
return {
|
|
837
|
-
...inspection,
|
|
838
|
-
// Branch identity is not checkout-validity: a switch/detach racing the
|
|
839
|
-
// timed-out remove still leaves a live registration that needs --force.
|
|
840
|
-
stdout: inspection.stdout.replace(`branch refs/heads/${wt.branch}`, 'branch refs/heads/externally-switched'),
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
return gitSync(args, cwd, undefined, options);
|
|
844
|
-
});
|
|
845
|
-
try {
|
|
846
|
-
const started = Date.now();
|
|
847
|
-
const result = closeManagedWorktree(nodeId);
|
|
848
|
-
const elapsed = Date.now() - started;
|
|
849
|
-
const cleanupDeadlineMs = 5_000;
|
|
850
|
-
const schedulingMarginMs = 500;
|
|
851
|
-
assert.ok(elapsed < cleanupDeadlineMs - schedulingMarginMs, `cleanup timeout must return promptly, took ${elapsed}ms`);
|
|
852
|
-
assert.notEqual(result.landed_sha, baseBefore, 'the land must contain a new commit');
|
|
853
|
-
assert.equal(gitSync(['rev-parse', 'main'], sourceDir).stdout.trim(), result.landed_sha, 'the base must contain the landed commit');
|
|
854
|
-
assert.equal(getNode(nodeId)?.managed_worktree?.state, 'closed', 'timeout must preserve the closed state');
|
|
855
|
-
assert.equal(result.worktree_removed, false);
|
|
856
|
-
assert.equal(result.branch_deleted, false);
|
|
857
|
-
assert.match(result.worktree_remove_error ?? '', /timed out/);
|
|
858
|
-
assert.match(result.worktree_remove_error ?? '', /git -C .* worktree remove --force/);
|
|
859
|
-
assert.doesNotMatch(result.worktree_remove_error ?? '', /worktree prune/, 'a valid registered checkout needs force-removal even if its branch changed');
|
|
860
|
-
assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], sourceDir).status, 0, 'the source branch must remain reachable');
|
|
861
|
-
// The node that timed out must still be able to complete its own finalization
|
|
862
|
-
// from the recreated cwd; a different-node lock check alone is insufficient.
|
|
863
|
-
await push(nodeId, { kind: 'final', body: 'timed-out cleanup node finalized' });
|
|
864
|
-
assert.equal(getNode(nodeId)?.status, 'done', 'the timed-out node must finalize itself successfully');
|
|
865
|
-
assert.ok(getNode(nodeId)?.final_report, 'the timed-out node must record its canonical final');
|
|
866
|
-
// A second close must acquire the same repository lock and complete, proving
|
|
867
|
-
// the timed-out cleanup did not strand the lock around the finished land.
|
|
868
|
-
const followupId = 'wt-close-after-cleanup-timeout';
|
|
869
|
-
const followup = createManagedWorktree(sourceDir, followupId);
|
|
870
|
-
createNode(node(followupId, { cwd: followup.path, managed_worktree: followup }));
|
|
871
|
-
writeFileSync(join(followup.path, 'followup-work.txt'), 'followup\n');
|
|
872
|
-
run(['add', '.'], followup.path);
|
|
873
|
-
run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'followup work'], followup.path);
|
|
874
|
-
const followupResult = closeManagedWorktree(followupId);
|
|
875
|
-
assert.equal(followupResult.worktree_removed, true, 'subsequent finalization must complete');
|
|
876
|
-
assert.equal(getNode(followupId)?.managed_worktree?.state, 'closed');
|
|
877
|
-
}
|
|
878
|
-
finally {
|
|
879
|
-
setWorktreeCleanupRunnerForTest();
|
|
880
|
-
}
|
|
881
|
-
});
|
|
882
|
-
test('post-land cleanup timeout gives prune guidance for a tombstone residual', () => {
|
|
883
|
-
const nodeId = 'wt-close-cleanup-timeout-tombstone';
|
|
884
|
-
const wt = createManagedWorktree(sourceDir, nodeId);
|
|
885
|
-
createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
|
|
886
|
-
writeFileSync(join(wt.path, 'cleanup-tombstone-work.txt'), 'work\n');
|
|
887
|
-
run(['add', '.'], wt.path);
|
|
888
|
-
run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'cleanup tombstone work'], wt.path);
|
|
889
|
-
setWorktreeCleanupRunnerForTest((args, cwd, options) => {
|
|
890
|
-
if (args[0] === 'worktree' && args[1] === 'remove' && args[2] === wt.path) {
|
|
891
|
-
assert.equal(options.timeoutMs, 5_000, 'the timed-out remove gets the finite deadline');
|
|
892
|
-
rmSync(wt.path, { recursive: true, force: true });
|
|
893
|
-
mkdirSync(wt.path, { recursive: true });
|
|
894
|
-
return { status: 1, stdout: '', stderr: '', timedOut: true };
|
|
895
|
-
}
|
|
896
|
-
if (args[0] === 'worktree' && args[1] === 'list') {
|
|
897
|
-
assert.equal(options.timeoutMs, 5_000, 'residual inspection gets the same finite deadline');
|
|
898
|
-
const branchSha = gitSync(['rev-parse', `refs/heads/${wt.branch}`], sourceDir).stdout.trim();
|
|
899
|
-
return {
|
|
900
|
-
status: 0,
|
|
901
|
-
stdout: `worktree ${wt.path}\nHEAD ${branchSha}\nbranch refs/heads/${wt.branch}\nprunable gitdir file points to non-existent location\n`,
|
|
902
|
-
stderr: '',
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
return gitSync(args, cwd, undefined, options);
|
|
906
|
-
});
|
|
907
|
-
try {
|
|
908
|
-
const result = closeManagedWorktree(nodeId);
|
|
909
|
-
const guidance = result.worktree_remove_error ?? '';
|
|
910
|
-
assert.equal(result.worktree_removed, false);
|
|
911
|
-
assert.equal(result.branch_deleted, false);
|
|
912
|
-
assert.match(guidance, /git -C .* worktree prune/);
|
|
913
|
-
assert.match(guidance, /rm -rf/);
|
|
914
|
-
assert.doesNotMatch(guidance, /worktree remove --force/);
|
|
915
|
-
assert.ok(existsSync(wt.path), 'the tombstone residual must remain available for guided cleanup');
|
|
916
|
-
}
|
|
917
|
-
finally {
|
|
918
|
-
setWorktreeCleanupRunnerForTest();
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
747
|
test('close-landed never low-level-deletes the source branch when checkout removal is refused', () => {
|
|
922
748
|
const nodeId = 'wt-close-removal-refused';
|
|
923
749
|
const wt = createManagedWorktree(sourceDir, nodeId);
|
|
@@ -105,3 +105,25 @@ test('cycleAwareMessages: a yielded cycle keeps terminal assistant content witho
|
|
|
105
105
|
assert.equal(yieldedAssistant.errorMessage, undefined, 'the provider abort text is omitted with the yield boundary');
|
|
106
106
|
assert.equal(out.filter((message) => message.customType === 'crtr-cycle-divider').length, 1);
|
|
107
107
|
});
|
|
108
|
+
// Observed shape: the yield's self-abort landed as the NEXT request was
|
|
109
|
+
// starting, so the provider caught a bare AbortError instead of seeing its own
|
|
110
|
+
// signal and stamped `stopReason: 'error'` on an empty turn. Pi's assistant
|
|
111
|
+
// component reserves a red `Error: …` line for exactly that stopReason, so the
|
|
112
|
+
// annotation must come off here too — a yield is not a failure.
|
|
113
|
+
test('cycleAwareMessages: a yield abort recorded as a provider error is still not a failed turn', () => {
|
|
114
|
+
const m1 = userMsg('m1', null, 'yield after this turn');
|
|
115
|
+
const m2 = assistantMsg('m2', 'm1', [], 'error', 'This operation was aborted');
|
|
116
|
+
const cy1 = {
|
|
117
|
+
type: 'custom',
|
|
118
|
+
id: 'cy1',
|
|
119
|
+
parentId: null,
|
|
120
|
+
timestamp: new Date(1).toISOString(),
|
|
121
|
+
customType: CRTR_CYCLE_CUSTOM_TYPE,
|
|
122
|
+
data: { cycle: 2, fromLeaf: 'm2' },
|
|
123
|
+
};
|
|
124
|
+
const m3 = userMsg('m3', 'cy1', 'fresh cycle');
|
|
125
|
+
const out = cycleAwareMessages(cycleManager([m1, m2, cy1, m3], 'm3'));
|
|
126
|
+
const yieldedAssistant = out.find((message) => message.role === 'assistant');
|
|
127
|
+
assert.equal(yieldedAssistant.stopReason, undefined, 'yield is not presented as a failed assistant turn');
|
|
128
|
+
assert.equal(yieldedAssistant.errorMessage, undefined, 'the aborted-request text is omitted with the yield boundary');
|
|
129
|
+
});
|
|
@@ -28,7 +28,7 @@ function node(id, over = {}) {
|
|
|
28
28
|
...over,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
const noSignals = { pushedFinal: false,
|
|
31
|
+
const noSignals = { pushedFinal: false, requestedHumanReply: false };
|
|
32
32
|
before(() => {
|
|
33
33
|
home = mkdtempSync(join(tmpdir(), 'crtr-stopguard-'));
|
|
34
34
|
process.env['CRTR_HOME'] = home;
|
|
@@ -132,10 +132,10 @@ test('a TERMINAL node whose only worker is dead is stalled (no LIVE subscription
|
|
|
132
132
|
assert.equal(d.action, 'reprompt');
|
|
133
133
|
assert.equal(d.reason, 'stalled');
|
|
134
134
|
});
|
|
135
|
-
test('pushedFinal → finished;
|
|
135
|
+
test('pushedFinal → finished; requestedHumanReply → escalated (both short-circuit before lifecycle)', () => {
|
|
136
136
|
createNode(node('t', { parent: 'mgr', lifecycle: 'terminal' }));
|
|
137
|
-
assert.deepEqual(evaluateStop('t', { pushedFinal: true,
|
|
138
|
-
assert.deepEqual(evaluateStop('t', { pushedFinal: false,
|
|
137
|
+
assert.deepEqual(evaluateStop('t', { pushedFinal: true, requestedHumanReply: false }), { action: 'allow', reason: 'finished' });
|
|
138
|
+
assert.deepEqual(evaluateStop('t', { pushedFinal: false, requestedHumanReply: true }), { action: 'allow', reason: 'escalated' });
|
|
139
139
|
});
|
|
140
140
|
test('an active follow-up cycle with a canonical final is allowed without another final', () => {
|
|
141
141
|
createNode(node('followUp', {
|
|
@@ -14,7 +14,7 @@ after(() => {
|
|
|
14
14
|
rmSync(home, { recursive: true, force: true });
|
|
15
15
|
});
|
|
16
16
|
test('only a watchdog-stamped aborted settlement records an automatic connection fault', () => {
|
|
17
|
-
const settleAbort = (nodeId, stagedWatchdogAbort) => {
|
|
17
|
+
const settleAbort = (nodeId, stagedWatchdogAbort, stagedRefreshAbort, stopReason = 'aborted') => {
|
|
18
18
|
const session = {
|
|
19
19
|
sessionManager: { getLeafId: () => 'leaf-entry' },
|
|
20
20
|
model: undefined,
|
|
@@ -24,9 +24,14 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
24
24
|
session,
|
|
25
25
|
fallback: { blocked: false },
|
|
26
26
|
stagedProviderAgentEnd: {
|
|
27
|
-
messages: [{
|
|
27
|
+
messages: [{
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
stopReason,
|
|
30
|
+
errorMessage: stopReason === 'error' ? 'This operation was aborted' : 'Request was aborted',
|
|
31
|
+
}],
|
|
28
32
|
},
|
|
29
33
|
stagedWatchdogAbort,
|
|
34
|
+
stagedRefreshAbort,
|
|
30
35
|
stagedOverflowFailure: null,
|
|
31
36
|
executingTools: new Set(),
|
|
32
37
|
};
|
|
@@ -38,15 +43,24 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
38
43
|
retry.publishSettledProviderFault(generation);
|
|
39
44
|
return { retry, generation };
|
|
40
45
|
};
|
|
41
|
-
const watchdog = settleAbort('watchdog-node', true);
|
|
46
|
+
const watchdog = settleAbort('watchdog-node', true, false);
|
|
42
47
|
const fault = readFault('watchdog-node');
|
|
43
48
|
assert.equal(fault?.link, 'pi→provider');
|
|
44
49
|
assert.equal(fault?.kind, 'connection');
|
|
45
50
|
assert.deepEqual(fault?.retry, { disposition: 'auto', by: 'daemon' });
|
|
46
51
|
assert.equal(watchdog.retry.isActiveAutoFault(fault), true);
|
|
47
52
|
assert.equal(watchdog.generation.stagedWatchdogAbort, false, 'settlement consumes the one-shot provenance');
|
|
48
|
-
settleAbort('human-node', false);
|
|
53
|
+
settleAbort('human-node', false, false);
|
|
49
54
|
assert.equal(readFault('human-node'), null, 'an otherwise-identical human abort must not arm recovery');
|
|
55
|
+
// Pi 0.84 currently serializes a host abort from `node yield` as an `error`
|
|
56
|
+
// settlement. Only the refresh-abort provenance suppresses that provider-looking error.
|
|
57
|
+
const refresh = settleAbort('refresh-node', false, true, 'error');
|
|
58
|
+
assert.equal(readFault('refresh-node'), null, 'a refresh-aborted error settlement must not enter provider fault recovery');
|
|
59
|
+
assert.equal(refresh.generation.stagedRefreshAbort, false, 'settlement consumes the one-shot refresh provenance');
|
|
60
|
+
settleAbort('provider-node', false, false, 'error');
|
|
61
|
+
const providerFault = readFault('provider-node');
|
|
62
|
+
assert.equal(providerFault?.kind, 'other', 'the same provider error without refresh provenance still classifies');
|
|
63
|
+
assert.deepEqual(providerFault?.retry, { disposition: 'fatal' }, 'the same provider error without refresh provenance still records its fault');
|
|
50
64
|
});
|
|
51
65
|
test('a failed retry settlement carries the episode forward instead of restarting it', () => {
|
|
52
66
|
// The episode marker a daemon retry records before re-prompting: original
|
|
@@ -68,6 +82,7 @@ test('a failed retry settlement carries the episode forward instead of restartin
|
|
|
68
82
|
messages: [{ role: 'assistant', stopReason: 'error', errorMessage: 'fetch failed' }],
|
|
69
83
|
},
|
|
70
84
|
stagedWatchdogAbort: false,
|
|
85
|
+
stagedRefreshAbort: false,
|
|
71
86
|
stagedOverflowFailure: null,
|
|
72
87
|
executingTools: new Set(),
|
|
73
88
|
};
|
package/dist/core/bash-jobs.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ export declare function jobStartedAtMs(jobId: string): number | undefined;
|
|
|
77
77
|
* notice, the `node bash background` result/toast, and the attach cockpit)
|
|
78
78
|
* shows it: `<60s -> "12s"`; `60s-59m -> "3m 40s"`; `>=60m -> "1h 04m"`. */
|
|
79
79
|
export declare function formatBashElapsed(ms: number): string;
|
|
80
|
+
/** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
|
|
81
|
+
* injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
|
|
82
|
+
* literal first line names no command at all: skip leading assignment lines and
|
|
83
|
+
* return the first real one, whitespace-collapsed to a single line. A job whose
|
|
84
|
+
* command is nothing but assignments falls back to its first non-empty line. */
|
|
85
|
+
export declare function bashJobCommandLine(command: string): string;
|
|
80
86
|
export interface BashJobStatus {
|
|
81
87
|
jobId: string;
|
|
82
88
|
/** Full text of cmd.sh. */
|
package/dist/core/bash-jobs.js
CHANGED
|
@@ -189,6 +189,16 @@ export function formatBashElapsed(ms) {
|
|
|
189
189
|
const minutes = totalMinutes % 60;
|
|
190
190
|
return `${hours}h ${String(minutes).padStart(2, '0')}m`;
|
|
191
191
|
}
|
|
192
|
+
/** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
|
|
193
|
+
* injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
|
|
194
|
+
* literal first line names no command at all: skip leading assignment lines and
|
|
195
|
+
* return the first real one, whitespace-collapsed to a single line. A job whose
|
|
196
|
+
* command is nothing but assignments falls back to its first non-empty line. */
|
|
197
|
+
export function bashJobCommandLine(command) {
|
|
198
|
+
const lines = command.split('\n').map((line) => line.trim()).filter((line) => line !== '');
|
|
199
|
+
const real = lines.find((line) => !/^(export\s+)?[A-Za-z_][A-Za-z0-9_]*=/.test(line));
|
|
200
|
+
return (real ?? lines[0] ?? '').replace(/\s+/g, ' ');
|
|
201
|
+
}
|
|
192
202
|
/** Every live backgrounded bash job for this node — job.bg present, job.exit
|
|
193
203
|
* absent — whether handed off by the menu, the valve deadline, or broker
|
|
194
204
|
* termination. Read-only; never mutates. Skips malformed/partially-written
|
|
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
|
|
4
4
|
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { atomicWriteJson, pageManifestPath } from '../../human/convention.js';
|
|
7
|
+
import { atomicWriteJson, bindTicketReplyRoute, pageManifestPath } from '../../human/convention.js';
|
|
8
8
|
import { createNode } from '../canvas.js';
|
|
9
9
|
import { closeDb } from '../db.js';
|
|
10
10
|
import { nodeDir } from '../paths.js';
|
|
@@ -22,16 +22,19 @@ function meta(id, cwd, extra) {
|
|
|
22
22
|
...extra,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
// A ticket only counts as attention when it routes a reply back, so the fixture binds
|
|
26
|
+
// the reply route the same way `human send` does before writing the page manifest.
|
|
25
27
|
function seedPage(bridgeId) {
|
|
26
28
|
const dir = nodeDir(bridgeId);
|
|
27
29
|
mkdirSync(dir, { recursive: true });
|
|
30
|
+
bindTicketReplyRoute(dir, bridgeId);
|
|
28
31
|
atomicWriteJson(pageManifestPath(dir), {
|
|
29
|
-
schema: 'crtr.page/
|
|
30
|
-
dialect: '
|
|
32
|
+
schema: 'crtr.page/v2',
|
|
33
|
+
dialect: 'jsx',
|
|
31
34
|
title: bridgeId,
|
|
32
35
|
subtitle: 'Acknowledge this ticket so the request can proceed.',
|
|
33
|
-
placement: 'inline',
|
|
34
|
-
document: 'page.
|
|
36
|
+
delivery: { placement: 'inline', inbox: true, reply: true },
|
|
37
|
+
document: 'page.tsx',
|
|
35
38
|
steps: 1,
|
|
36
39
|
slots: [],
|
|
37
40
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// This is adversarial, not merely a clean-machine assertion: BEFORE calling
|
|
9
9
|
// the render functions, the test seeds real, CONFLICTING local state for the
|
|
10
10
|
// same node id `a` (a local focus, an active fault, telemetry, a goal file, a
|
|
11
|
-
// session transcript, and a pending human
|
|
11
|
+
// session transcript, and a pending human reply) — every historically-broken
|
|
12
12
|
// local read this suppression covers. If the production suppression ever
|
|
13
13
|
// regressed back to consulting local disk/SQLite for a remote source, the
|
|
14
14
|
// assertions below would observe the poisoned values instead of the safe
|
|
@@ -114,12 +114,12 @@ function seedPoisonedLocalState() {
|
|
|
114
114
|
const askDir = nodeDir('poison-ask');
|
|
115
115
|
mkdirSync(askDir, { recursive: true });
|
|
116
116
|
atomicWriteJson(pageManifestPath(askDir), {
|
|
117
|
-
schema: 'crtr.page/
|
|
118
|
-
dialect: '
|
|
117
|
+
schema: 'crtr.page/v2',
|
|
118
|
+
dialect: 'jsx',
|
|
119
119
|
title: 'poison',
|
|
120
120
|
subtitle: 'This local ticket must not affect the remote rendering.',
|
|
121
|
-
placement: 'inline',
|
|
122
|
-
document: 'page.
|
|
121
|
+
delivery: { placement: 'inline', inbox: true, reply: true },
|
|
122
|
+
document: 'page.tsx',
|
|
123
123
|
steps: 1,
|
|
124
124
|
slots: [],
|
|
125
125
|
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// attention.ts — pending human-ticket counters across the canvas.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// node
|
|
6
|
-
// than duplicated attribution fields in page/review data.
|
|
7
|
-
import { basename } from 'node:path';
|
|
3
|
+
// Reply-bearing tickets carry an explicit bridge route. The bridge's parent is
|
|
4
|
+
// the asking node; standalone pages have no bridge and do not contribute to a
|
|
5
|
+
// node's pending-reply count.
|
|
8
6
|
import { scanInbox } from '../human/scan.js';
|
|
9
7
|
import { filterTerminalReviewTickets } from '../review/ticket-filter.js';
|
|
8
|
+
import { readTicketReplyRoute } from '../human/convention.js';
|
|
10
9
|
import { getNode, listNodes, view } from './canvas.js';
|
|
11
10
|
function askingNodeId(ticketDir) {
|
|
12
|
-
const
|
|
11
|
+
const replyRoute = readTicketReplyRoute(ticketDir);
|
|
12
|
+
if (replyRoute === null)
|
|
13
|
+
return null;
|
|
14
|
+
const bridge = getNode(replyRoute.bridge_node_id);
|
|
13
15
|
if (bridge === null)
|
|
14
16
|
return null;
|
|
15
17
|
return bridge.parent ?? bridge.node_id;
|
|
@@ -11,8 +11,14 @@ export type SortMode = 'tree' | 'attention' | 'relevance' | 'recency';
|
|
|
11
11
|
export declare const SORTS: readonly SortMode[];
|
|
12
12
|
/** Does a node belong to this tab's slice?
|
|
13
13
|
* All — every node.
|
|
14
|
-
* Live — active | idle
|
|
15
|
-
*
|
|
14
|
+
* Live — what you can pick up right now: active | idle, PLUS a resident
|
|
15
|
+
* agent that stopped without finishing (its broker exited, but
|
|
16
|
+
* Enter resumes the conversation in place — see `isFinishedWork`).
|
|
17
|
+
* Finished work never appears here.
|
|
18
|
+
* Dormant — done | dead | canceled: literally "no engine running". A stopped
|
|
19
|
+
* resident agent is deliberately on BOTH tabs — it is resumable
|
|
20
|
+
* (Live) and it is not running (Dormant); both are true and each
|
|
21
|
+
* tab is the one a different search starts from.
|
|
16
22
|
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
17
23
|
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
18
24
|
export declare function tabPredicate(tab: Tab, row: DashboardRow, isPinned?: boolean): boolean;
|
|
@@ -4,19 +4,26 @@
|
|
|
4
4
|
// it is exhaustively unit-testable. The app layer (app.ts) wires the canvas data
|
|
5
5
|
// access (dashboardRowsAll / listNodes / subscriptionsOf) into buildTree, then
|
|
6
6
|
// drives flatten() on each keystroke.
|
|
7
|
-
import { statusRank } from '../node-order.js';
|
|
7
|
+
import { isFinishedWork, isLiveStatus, statusRank } from '../node-order.js';
|
|
8
8
|
export const TABS = ['All', 'Live', 'Dormant', 'Attention', 'Pinned'];
|
|
9
9
|
export const SORTS = ['tree', 'attention', 'relevance', 'recency'];
|
|
10
10
|
/** Does a node belong to this tab's slice?
|
|
11
11
|
* All — every node.
|
|
12
|
-
* Live — active | idle
|
|
13
|
-
*
|
|
12
|
+
* Live — what you can pick up right now: active | idle, PLUS a resident
|
|
13
|
+
* agent that stopped without finishing (its broker exited, but
|
|
14
|
+
* Enter resumes the conversation in place — see `isFinishedWork`).
|
|
15
|
+
* Finished work never appears here.
|
|
16
|
+
* Dormant — done | dead | canceled: literally "no engine running". A stopped
|
|
17
|
+
* resident agent is deliberately on BOTH tabs — it is resumable
|
|
18
|
+
* (Live) and it is not running (Dormant); both are true and each
|
|
19
|
+
* tab is the one a different search starts from.
|
|
14
20
|
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
15
21
|
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
16
22
|
export function tabPredicate(tab, row, isPinned = false) {
|
|
17
23
|
switch (tab) {
|
|
18
24
|
case 'All': return true;
|
|
19
|
-
case 'Live': return row.status
|
|
25
|
+
case 'Live': return isLiveStatus(row.status)
|
|
26
|
+
|| !isFinishedWork({ lifecycle: row.lifecycle, finalized: row.finalized === true });
|
|
20
27
|
case 'Dormant': return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
|
|
21
28
|
case 'Attention': return row.asks > 0 || row.hanging != null;
|
|
22
29
|
case 'Pinned': return isPinned;
|
|
@@ -90,6 +90,10 @@ export declare function clearPid(nodeId: string): void;
|
|
|
90
90
|
export declare function migrateLegacyPidIdentities(captureIdentity?: (pid: number) => string | undefined): {
|
|
91
91
|
migrated: string[];
|
|
92
92
|
};
|
|
93
|
+
/** Every hydrated node whose durable profile identity equals `profileId`.
|
|
94
|
+
* Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
|
|
95
|
+
* deletion owns every matching row, whether or not it is user-visible. */
|
|
96
|
+
export declare function listNodesByProfile(profileId: string): NodeMeta[];
|
|
93
97
|
/** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
|
|
94
98
|
export declare function listNodes(filter?: {
|
|
95
99
|
status?: NodeStatus | NodeStatus[];
|
|
@@ -306,6 +310,9 @@ export interface DeleteNodeResult {
|
|
|
306
310
|
* state are removed in one canvas write boundary. Best-effort dir removal
|
|
307
311
|
* follows the commit. */
|
|
308
312
|
export declare function deleteNode(nodeId: string): DeleteNodeResult;
|
|
313
|
+
/** Hard-delete a validated set of nodes in one row transaction, then remove
|
|
314
|
+
* their directories. The caller must tear down engines and viewers first. */
|
|
315
|
+
export declare function deleteNodes(nodeIds: readonly string[]): string[];
|
|
309
316
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
310
317
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
311
318
|
* node rows + dirs. Open managed-worktree nodes are retained so their checkout
|