gentle-pi 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +101 -151
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell.md +77 -7
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +19 -15
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +173 -38
- package/extensions/gentle-ai.ts +214 -69
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +332 -73
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +123 -49
- package/lib/gentle-shell-launcher.ts +9 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +4 -3
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +122 -48
- package/runtime/gentle-shell-launcher.mjs +9 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +34 -6
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +3 -3
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +1 -1
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +365 -20
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bundled.test.ts +301 -248
- package/tests/gentle-shell-launcher.test.ts +28 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +183 -8
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +99 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +14 -2
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
|
@@ -13,11 +13,11 @@ import test, { after, afterEach, before, mock } from "node:test";
|
|
|
13
13
|
import type { TestContext } from "node:test";
|
|
14
14
|
import { generateUnifiedPatch, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai";
|
|
16
|
-
import { visibleWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
16
|
+
import { visibleWidth, type Component, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
17
17
|
import { sidebarState } from "../lib/shell-sidebar.ts";
|
|
18
18
|
import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection } from "../lib/child-package-injection.ts";
|
|
19
19
|
import gentleAgents, { agentRuntimePaths, agentsCollapseKey, agentsEnabled, agentsStopKey, agentsViewKey, agentResultPreview, answerThroughUi, childContextExtensionPaths, completionText, createDefaultSessionTransport, legacySubagentsInstalled, PARENT_WAKE_GRACE_MS, type AgentsDeps, type SessionTransportFactory } from "../extensions/gentle-agents.ts";
|
|
20
|
-
import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry } from "../lib/agents-session-transport.ts";
|
|
20
|
+
import { ActiveSessionClient, ActiveSessionListener, SessionPresenceError, SessionPresenceRegistry, type PresenceRecord } from "../lib/agents-session-transport.ts";
|
|
21
21
|
import { WindowsActiveSessionClient, WindowsActiveSessionListener } from "../lib/windows-session-transport.ts";
|
|
22
22
|
import { historyDir, loadHistory, saveTask } from "../lib/agents-history.ts";
|
|
23
23
|
import { STALE_COMPLETION_MS } from "../lib/agents-completion-delivery.ts";
|
|
@@ -243,7 +243,8 @@ function fakeContext(tui: { requestRender(): void } = fakeTui, confirmResult: (t
|
|
|
243
243
|
const factory = widgets.get("gentle-agents");
|
|
244
244
|
return factory ? factory(tui, plainTheme).render(72).map(stripAnsi) : undefined;
|
|
245
245
|
};
|
|
246
|
-
|
|
246
|
+
const widgetComponent = () => widgets.get("gentle-agents")?.(tui, plainTheme) as Component | undefined;
|
|
247
|
+
return { ctx, widget, widgetComponent, dialogs, overlays, customCompletions, customOptions };
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
for (const scenario of ["background", "task", "append-failure", "overflow", "bytes", "replacement", "task-replacement", "unsafe-id", "no-transport"] as const) {
|
|
@@ -580,7 +581,9 @@ test("gentle-agents notifies once, deduplicated, when the RPC activity publisher
|
|
|
580
581
|
const runtime = deps();
|
|
581
582
|
const scheduler = fakeScheduler();
|
|
582
583
|
runtime.deps.schedule = scheduler.schedule;
|
|
583
|
-
|
|
584
|
+
// The launch reads the session profile through this env, so it must keep the
|
|
585
|
+
// scratch config home instead of reaching the runner's real one.
|
|
586
|
+
runtime.deps.env = { PATH: "/bin", GENTLE_SHELL_INTERACTIVE_HOST: "1", GENTLE_PI_CONFIG_HOME: process.env.GENTLE_PI_CONFIG_HOME };
|
|
584
587
|
gentleAgents(h.pi, {}, runtime.deps);
|
|
585
588
|
const { ctx } = fakeContext();
|
|
586
589
|
Object.assign(ctx, { mode: "rpc", hasUI: true });
|
|
@@ -1415,6 +1418,152 @@ test("live-only extension instances discover same-profile peers across cwd bound
|
|
|
1415
1418
|
await Promise.all([panel.opened, peerPanel.opened]);
|
|
1416
1419
|
});
|
|
1417
1420
|
|
|
1421
|
+
test("rapid child deltas coalesce presence publication to a leading publish plus one trailing publish", async (t) => {
|
|
1422
|
+
const profile = liveProfile("live-coalesce-profile");
|
|
1423
|
+
const h = fakePi();
|
|
1424
|
+
const runtime = deps();
|
|
1425
|
+
const timers = recordTimers(runtime.deps);
|
|
1426
|
+
const { ctx } = fakeContext();
|
|
1427
|
+
Object.assign(ctx.sessionManager, {
|
|
1428
|
+
getSessionId: () => "coalesce-live",
|
|
1429
|
+
getSessionName: () => "coalesce-live",
|
|
1430
|
+
getCwd: () => join(realpathSync(root), "coalesce-live"),
|
|
1431
|
+
});
|
|
1432
|
+
gentleAgents(h.pi, {}, { ...runtime.deps, agentHome: profile });
|
|
1433
|
+
const update = t.mock.method(PresencePublisher.prototype, "update");
|
|
1434
|
+
await h.fire("session_start", ctx);
|
|
1435
|
+
timers.run(400); // expire session_start's own publication window
|
|
1436
|
+
update.mock.resetCalls(); // session_start's own publication is out of scope
|
|
1437
|
+
|
|
1438
|
+
const launch = await h.tools.get("subagent_run")!.execute("coalesce", { agent: "local", task: "Stream", mode: "background" }, undefined, undefined, ctx);
|
|
1439
|
+
await tick();
|
|
1440
|
+
const taskId = (launch.details.gentleAgents as { taskId: string }).taskId;
|
|
1441
|
+
assert.ok(taskId);
|
|
1442
|
+
assert.equal(runtime.children.length, 1);
|
|
1443
|
+
// The admission burst publishes its discrete semantic events immediately:
|
|
1444
|
+
// the admission itself plus the child's worktree registration. Per-delta
|
|
1445
|
+
// events below still collapse into one trailing publish.
|
|
1446
|
+
assert.equal(update.mock.callCount(), 2, "the admission burst publishes its semantic events on the leading edge");
|
|
1447
|
+
update.mock.resetCalls();
|
|
1448
|
+
|
|
1449
|
+
for (let delta = 0; delta < 20; delta++) {
|
|
1450
|
+
runtime.children[0].emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: `delta ${delta}` } });
|
|
1451
|
+
}
|
|
1452
|
+
assert.equal(update.mock.callCount(), 0, "per-delta events must not publish while the coalescing window is open");
|
|
1453
|
+
|
|
1454
|
+
timers.run(400);
|
|
1455
|
+
assert.equal(update.mock.callCount(), 1, "exactly one trailing publish after the burst");
|
|
1456
|
+
update.mock.resetCalls();
|
|
1457
|
+
|
|
1458
|
+
// After the window closes, a new burst publishes on the leading edge again.
|
|
1459
|
+
runtime.children[0].emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "after window" } });
|
|
1460
|
+
assert.equal(update.mock.callCount(), 1, "a call after the window publishes immediately again");
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
test("the trailing publication reuses cached scope facts: a served metadata query sees no Git probes", async (t) => {
|
|
1464
|
+
// gentle-shell#2522 regression: the coalesced trailing publish ran while a
|
|
1465
|
+
// peer metadata query (orchestrator_consult/orchestrator_list) was being
|
|
1466
|
+
// served, and its scope projection cache-missed into a synchronous Git
|
|
1467
|
+
// resolve. A metadata query adds no caller Git probes, so the trailing
|
|
1468
|
+
// publication reuses the snapshot the last event-time publish projected —
|
|
1469
|
+
// never probing, never dropping — until an event-time publish refreshes.
|
|
1470
|
+
const profile = liveProfile("live-coalesce-scope-profile");
|
|
1471
|
+
const h = fakePi();
|
|
1472
|
+
const runtime = deps();
|
|
1473
|
+
const timers = recordTimers(runtime.deps);
|
|
1474
|
+
let releaseActivations: () => void = () => {};
|
|
1475
|
+
// A recorded listener makes publishActivityNow project the discovery scope;
|
|
1476
|
+
// the registry holds the consult's activation read until the test releases it.
|
|
1477
|
+
runtime.deps.sessionTransport = {
|
|
1478
|
+
...inertSessionTransport,
|
|
1479
|
+
createRegistry: async () => ({ list: async () => [], listActivations: async () => await new Promise<readonly PresenceRecord[]>(resolve => { releaseActivations = () => resolve([]); }) }),
|
|
1480
|
+
createListener: registry => ({ registry, record: { version: 1, sessionId: "coalesce-scope", endpoint: "/fixture/coalesce-scope", createdAt: 1 }, start: async () => {}, close: async () => {} }),
|
|
1481
|
+
};
|
|
1482
|
+
const probes: string[] = [];
|
|
1483
|
+
// Count only scope-projection probes: admission and launch also resolve
|
|
1484
|
+
// worktrees outside the projection, and only the projection can leak into a
|
|
1485
|
+
// peer metadata query.
|
|
1486
|
+
const realProject = OrchestratorScopeCache.prototype.project;
|
|
1487
|
+
let projecting = false;
|
|
1488
|
+
OrchestratorScopeCache.prototype.project = function (...args: Parameters<typeof realProject>) {
|
|
1489
|
+
projecting = true;
|
|
1490
|
+
try { return realProject.apply(this, args); } finally { projecting = false; }
|
|
1491
|
+
};
|
|
1492
|
+
t.after(() => { OrchestratorScopeCache.prototype.project = realProject; });
|
|
1493
|
+
runtime.deps.resolveWorktree = (path, base) => {
|
|
1494
|
+
if (projecting) probes.push(resolve(base, path));
|
|
1495
|
+
return { root: resolve(base, path), commonDir: "/fixture/common" };
|
|
1496
|
+
};
|
|
1497
|
+
const { ctx } = fakeContext();
|
|
1498
|
+
Object.assign(ctx.sessionManager, {
|
|
1499
|
+
getSessionId: () => "coalesce-scope",
|
|
1500
|
+
getSessionName: () => "coalesce-scope",
|
|
1501
|
+
getCwd: () => join(realpathSync(root), "coalesce-scope"),
|
|
1502
|
+
});
|
|
1503
|
+
gentleAgents(h.pi, {}, { ...runtime.deps, agentHome: profile });
|
|
1504
|
+
const update = t.mock.method(PresencePublisher.prototype, "update");
|
|
1505
|
+
await h.fire("session_start", ctx);
|
|
1506
|
+
assert.ok(probes.length > 0, "the semantic startup publish resolves its facts eagerly");
|
|
1507
|
+
// Wait for the asynchronous session transport to bind its record without
|
|
1508
|
+
// publishing; the metadata tool answers "not ready" until it has.
|
|
1509
|
+
for (let i = 0; i < 100; i++) {
|
|
1510
|
+
const result = await h.tools.get("orchestrator_session_id")!.execute("id", {}, undefined, undefined, ctx);
|
|
1511
|
+
if ((result.details?.gentleAgents as { senderSessionId?: string } | undefined)?.senderSessionId) break;
|
|
1512
|
+
assert.ok(i < 99, "session transport binds within bounded ticks");
|
|
1513
|
+
await tick();
|
|
1514
|
+
}
|
|
1515
|
+
timers.run(400); // expire startup's own publication window
|
|
1516
|
+
update.mock.resetCalls();
|
|
1517
|
+
probes.length = 0;
|
|
1518
|
+
|
|
1519
|
+
const launch = await h.tools.get("subagent_run")!.execute("coalesce", { agent: "local", task: "Stream", mode: "background" }, undefined, undefined, ctx);
|
|
1520
|
+
await tick();
|
|
1521
|
+
const taskId = (launch.details.gentleAgents as { taskId: string }).taskId;
|
|
1522
|
+
assert.ok(taskId);
|
|
1523
|
+
assert.ok(probes.length > 0, "the semantic admission publish resolves new membership facts eagerly");
|
|
1524
|
+
// Let a settle publish absorb any late membership change before the measured window.
|
|
1525
|
+
timers.run(400);
|
|
1526
|
+
runtime.children[0].emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "settle" } });
|
|
1527
|
+
await tick();
|
|
1528
|
+
timers.run(400);
|
|
1529
|
+
update.mock.resetCalls();
|
|
1530
|
+
probes.length = 0;
|
|
1531
|
+
|
|
1532
|
+
runtime.children[0].emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "delta" } });
|
|
1533
|
+
assert.equal(update.mock.callCount(), 1, "the delta publishes on the leading edge");
|
|
1534
|
+
assert.equal(probes.length, 0, "an unchanged membership never re-resolves facts");
|
|
1535
|
+
|
|
1536
|
+
// Membership changes mid-window (the child settles, leaving the coalesced
|
|
1537
|
+
// path to queue the publish) while a peer metadata query starts being
|
|
1538
|
+
// served — the exact collision of the regression.
|
|
1539
|
+
runtime.children[0].emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "done" }], stopReason: "stop" }] });
|
|
1540
|
+
runtime.children[0].emit({ type: "agent_settled" });
|
|
1541
|
+
for (let i = 0; i < 100; i++) {
|
|
1542
|
+
const status = await h.tools.get("subagent_status")!.execute("status", { task_id: taskId }, undefined, undefined, ctx);
|
|
1543
|
+
if (/completed/.test(status.content[0]?.text ?? "")) break;
|
|
1544
|
+
assert.ok(i < 99, "child completes within bounded ticks");
|
|
1545
|
+
await tick();
|
|
1546
|
+
}
|
|
1547
|
+
assert.equal(update.mock.callCount(), 1, "the membership change stays behind the trailing edge");
|
|
1548
|
+
assert.equal(probes.length, 0, "queued events must not publish");
|
|
1549
|
+
const pendingConsult = h.tools.get("orchestrator_consult")!.execute("consult", { recipient_session_id: "peer123" }, undefined, undefined, ctx);
|
|
1550
|
+
await tick(); // the consult suspends on its activation read, holding the window open
|
|
1551
|
+
|
|
1552
|
+
timers.run(400); // the trailing publish comes due while the query is being served
|
|
1553
|
+
assert.equal(update.mock.callCount(), 2, "the trailing publication still runs, never dropped");
|
|
1554
|
+
assert.equal(probes.length, 0, "a served metadata query adds no caller Git probes");
|
|
1555
|
+
|
|
1556
|
+
releaseActivations();
|
|
1557
|
+
await pendingConsult;
|
|
1558
|
+
assert.equal(probes.length, 0, "the whole served query stayed probe-free");
|
|
1559
|
+
|
|
1560
|
+
// After the window, an event-time publish refreshes the facts the trailing
|
|
1561
|
+
// reuse deferred: the changed membership resolves eagerly again.
|
|
1562
|
+
h.pi.events.emit(SESSION_WORKTREE_CHANGED, { sessionId: "coalesce-scope" });
|
|
1563
|
+
assert.ok(probes.length > 0, "an event-time publish refreshes the deferred facts");
|
|
1564
|
+
assert.equal(update.mock.callCount(), 3, "the refresh publishes immediately");
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1418
1567
|
test("manual agents command warns once in RPC mode and stays quiet without UI", async (t) => {
|
|
1419
1568
|
const local = liveInstance(t, liveProfile("live-non-tui"), "non-tui");
|
|
1420
1569
|
Object.assign(local.ctx, { mode: "rpc" });
|
|
@@ -2718,11 +2867,16 @@ test("foreign clone rejects aliases, absent UI, decline and changed session befo
|
|
|
2718
2867
|
const declined = run.execute("decline", args(foreign), undefined, undefined, ctx);
|
|
2719
2868
|
await tick(); decision(false);
|
|
2720
2869
|
await assert.rejects(declined, /interactive/);
|
|
2870
|
+
// gentle-shell#2314: Git prints Windows roots with forward slashes; that spelling
|
|
2871
|
+
// must pass the canonical-root guard and reach consent like the native one.
|
|
2872
|
+
const forwardSlashes = run.execute("forward-slashes", args(foreign.replaceAll("\\", "/")), undefined, undefined, ctx);
|
|
2873
|
+
await tick(); decision(false);
|
|
2874
|
+
await assert.rejects(forwardSlashes, /interactive/);
|
|
2721
2875
|
const drift = run.execute("drift", args(foreign), undefined, undefined, ctx);
|
|
2722
2876
|
await tick(); id = "replacement"; decision(true);
|
|
2723
2877
|
await assert.rejects(drift, /identity changed/);
|
|
2724
2878
|
assert.equal(runtime.spawned.length, 0);
|
|
2725
|
-
assert.equal(dialogs.filter(dialog => dialog.startsWith("confirm:")).length,
|
|
2879
|
+
assert.equal(dialogs.filter(dialog => dialog.startsWith("confirm:")).length, 3);
|
|
2726
2880
|
assert.deepEqual(h.entries.filter(entry => entry.customType === SESSION_WORKTREE_ENTRY), []);
|
|
2727
2881
|
await h.fire("session_shutdown", ctx);
|
|
2728
2882
|
} finally { rmSync(fixture, TEST_DIR_REMOVAL); }
|
|
@@ -3622,7 +3776,7 @@ test("subagent_list_agents and subagent_run in task mode launch a child with the
|
|
|
3622
3776
|
assert.equal(args[args.indexOf("--tools") + 1], "read,grep,subagent_parent_message");
|
|
3623
3777
|
await tick();
|
|
3624
3778
|
assert.match(String(harness.children[0].written[1].message), /Map lib\/ and report every module\.\n\n## Context\nFocus on agents-\*\.ts/);
|
|
3625
|
-
assert.match(widget()![0], /^╭─ ❀ Agents · 1 active ─+╮$/);
|
|
3779
|
+
assert.match(widget()![0], /^╭─ ❀ Agents ▾ Collapse · 1 active ─+╮$/);
|
|
3626
3780
|
assert.match(widget()![1], /^│ ◐ explore map lib modules +gpt-5\.6-terra · low · \d+s │$/);
|
|
3627
3781
|
harness.children[0].emit({ type: "tool_execution_start", toolCallId: "c", toolName: "grep", args: {} });
|
|
3628
3782
|
harness.children[0].emit({ type: "message_end", message: { role: "assistant", usage: { totalTokens: 12_000, cost: { total: 0.09 } } } });
|
|
@@ -3672,7 +3826,7 @@ test("running subagent_result polls hide only their tool chrome, not the model r
|
|
|
3672
3826
|
assert.match(renderers.get("gentle-agents.result")!(sent[0].message, { expanded: true }, plainTheme).render(80).join("\n"), /Agent result.*All done\./s, "background completion card remains visible");
|
|
3673
3827
|
});
|
|
3674
3828
|
|
|
3675
|
-
test("the Agents
|
|
3829
|
+
test("the Agents card moves to the fullscreen rail like Todos and its header toggles collapse on left click", async () => {
|
|
3676
3830
|
const { pi, tools, fire } = fakePi();
|
|
3677
3831
|
const harness = deps();
|
|
3678
3832
|
gentleAgents(pi, {}, harness.deps);
|
|
@@ -3680,22 +3834,54 @@ test("the Agents widget never registers a sidebar rail part and stays visible ev
|
|
|
3680
3834
|
// (a host with no .terminal is already a passthrough); this is the host
|
|
3681
3835
|
// shape the fullscreen layout actually uses.
|
|
3682
3836
|
const terminalTui = { requestRender() {}, terminal: { columns: 160, rows: 40 } };
|
|
3683
|
-
const { ctx,
|
|
3837
|
+
const { ctx, widgetComponent } = fakeContext(terminalTui);
|
|
3684
3838
|
await fire("session_start", ctx);
|
|
3685
3839
|
await tools.get("subagent_run")!.execute("c1", { agent: "explore", task: "Map lib/", label: "map lib modules", mode: "background" }, undefined, undefined, ctx);
|
|
3840
|
+
await tools.get("subagent_run")!.execute("c2", { agent: "explore", task: "Map tests/", label: "map test suites", mode: "background" }, undefined, undefined, ctx);
|
|
3686
3841
|
await tick();
|
|
3687
3842
|
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
assert.match(
|
|
3843
|
+
const component = widgetComponent()!;
|
|
3844
|
+
const bottom = component.render(72).map(stripAnsi);
|
|
3845
|
+
assert.match(bottom[0]!, /Agents ▾ Collapse/, "the header carries the same clickable control as Todos");
|
|
3846
|
+
assert.match(bottom.join("\n"), /explore map lib modules/);
|
|
3847
|
+
assert.match(bottom.join("\n"), /explore map test suites/);
|
|
3848
|
+
|
|
3691
3849
|
const state = sidebarState(terminalTui as unknown as TUI);
|
|
3692
|
-
|
|
3850
|
+
const rail = state.parts.get("agents");
|
|
3851
|
+
assert.ok(rail, "Agents claims the rail slot, so a wide fullscreen terminal shows it at the right");
|
|
3852
|
+
assert.equal(rail.render(46).filter((line) => /explore/.test(stripAnsi(line))).length, 2, "the rail lists every task");
|
|
3853
|
+
assert.ok(rail.digest?.(), "the rail declares a digest so its clock and counters repaint");
|
|
3693
3854
|
|
|
3694
|
-
//
|
|
3695
|
-
// condition sidebarPart used to suppress a registered bottom widget under.
|
|
3855
|
+
// While the fullscreen sidebar owns the host, the bottom copy steps aside.
|
|
3696
3856
|
state.active = true;
|
|
3697
3857
|
state.ownsHost = () => true;
|
|
3698
|
-
assert.
|
|
3858
|
+
assert.deepEqual(component.render(72), [], "the bottom card paints nothing while the rail shows it");
|
|
3859
|
+
state.active = false;
|
|
3860
|
+
|
|
3861
|
+
const event = (type: "press" | "click", button: "left" | "right", y = 0) => ({
|
|
3862
|
+
type, button, x: 1, y, screenX: 1, screenY: y, width: 72, height: bottom.length, shift: false, alt: false, ctrl: false,
|
|
3863
|
+
});
|
|
3864
|
+
assert.equal(component.handleMouse?.(event("click", "right")), undefined);
|
|
3865
|
+
assert.equal(component.handleMouse?.(event("click", "left", 1)), undefined);
|
|
3866
|
+
assert.match(stripAnsi(component.render(72)[0]!), /Agents ▾ Collapse/, "only a left click on the header toggles");
|
|
3867
|
+
assert.equal(component.handleMouse?.(event("click", "left"))?.handled, true);
|
|
3868
|
+
const collapsed = component.render(72).map(stripAnsi);
|
|
3869
|
+
assert.match(collapsed[0]!, /Agents ▸ Expand/);
|
|
3870
|
+
assert.doesNotMatch(collapsed.join("\n"), /map test suites/, "the collapsed card keeps one row");
|
|
3871
|
+
assert.equal(rail.render(46).filter((line) => /explore/.test(stripAnsi(line))).length, 1, "both surfaces share the collapsed state");
|
|
3872
|
+
});
|
|
3873
|
+
|
|
3874
|
+
test("the Agents rail digest stays stable while no task is running, so an idle card never rebuilds the rail every second", async () => {
|
|
3875
|
+
const { pi, fire } = fakePi();
|
|
3876
|
+
gentleAgents(pi, {}, deps().deps);
|
|
3877
|
+
const terminalTui = { requestRender() {}, terminal: { columns: 160, rows: 40 } };
|
|
3878
|
+
const { ctx, widgetComponent } = fakeContext(terminalTui);
|
|
3879
|
+
await fire("session_start", ctx);
|
|
3880
|
+
widgetComponent();
|
|
3881
|
+
const rail = sidebarState(terminalTui as unknown as TUI).parts.get("agents")!;
|
|
3882
|
+
// The harness clock advances on every read, so three digests span more than a second.
|
|
3883
|
+
const digests = [rail.digest!(), rail.digest!(), rail.digest!()];
|
|
3884
|
+
assert.deepEqual(new Set(digests).size, 1, "the clock only enters the digest while a task runs");
|
|
3699
3885
|
});
|
|
3700
3886
|
|
|
3701
3887
|
test("background runs return at once; status, result, send_message, cancel, and continue follow the task", async () => {
|
|
@@ -4438,6 +4624,7 @@ test("session transport startup failure cleans the constructed Windows-capable t
|
|
|
4438
4624
|
|
|
4439
4625
|
test("registered task aliases persist independently of human names and curated state", async () => {
|
|
4440
4626
|
const h = fakePi(), runtime = deps(), { ctx } = fakeContext();
|
|
4627
|
+
const timers = recordTimers(runtime.deps);
|
|
4441
4628
|
const profile = realpathSync(mkdtempSync(join(root, "aliases-runtime-")));
|
|
4442
4629
|
runtime.deps.agentHome = profile;
|
|
4443
4630
|
const peer = { version: 1 as const, sessionId: "s1", endpoint: "/fixture/aliases.sock", createdAt: 1 };
|
|
@@ -4454,8 +4641,15 @@ test("registered task aliases persist independently of human names and curated s
|
|
|
4454
4641
|
gentleAgents(h.pi, {}, runtime.deps);
|
|
4455
4642
|
await h.fire("session_start", ctx);
|
|
4456
4643
|
const tool = h.tools.get("orchestrator_session_id")!;
|
|
4457
|
-
const declare = (params: unknown = {}, context = ctx) =>
|
|
4458
|
-
|
|
4644
|
+
const declare = async (params: unknown = {}, context = ctx) => {
|
|
4645
|
+
const result = await tool.execute("aliases", params, undefined, undefined, context);
|
|
4646
|
+
timers.run(400); // the activity publication is coalesced; close the window
|
|
4647
|
+
return result;
|
|
4648
|
+
};
|
|
4649
|
+
const aliases = () => {
|
|
4650
|
+
timers.run(400);
|
|
4651
|
+
return readDiscovery(profile, listPresence(profile).entries[0])?.aliases;
|
|
4652
|
+
};
|
|
4459
4653
|
const first = await declare({ subject: "Task A", state: { progress: "Starting", work: { area: "Auth" } } });
|
|
4460
4654
|
const initial = h.entries.at(-1)!;
|
|
4461
4655
|
assert.deepEqual(aliases(), { initialAlias: "Task A", currentAlias: "Task A" });
|
|
@@ -4505,6 +4699,7 @@ test("registered task aliases persist independently of human names and curated s
|
|
|
4505
4699
|
test("registered session identity declares subjects and refreshes canonical idle renames", async (t) => {
|
|
4506
4700
|
const h = fakePi();
|
|
4507
4701
|
const runtime = deps();
|
|
4702
|
+
const timers = recordTimers(runtime.deps);
|
|
4508
4703
|
const profile = realpathSync(mkdtempSync(join(root, "subject-runtime-")));
|
|
4509
4704
|
runtime.deps.agentHome = profile;
|
|
4510
4705
|
let ready = false;
|
|
@@ -4532,7 +4727,11 @@ test("registered session identity declares subjects and refreshes canonical idle
|
|
|
4532
4727
|
await h.fire("session_start", ctx);
|
|
4533
4728
|
await eventually(() => ready, "subject transport ready");
|
|
4534
4729
|
const tool = h.tools.get("orchestrator_session_id")!;
|
|
4535
|
-
const declare = (subject?: unknown, context = ctx) =>
|
|
4730
|
+
const declare = async (subject?: unknown, context = ctx) => {
|
|
4731
|
+
const result = await tool.execute("id", { subject }, undefined, undefined, context);
|
|
4732
|
+
timers.run(400); // the activity publication is coalesced; close the window
|
|
4733
|
+
return result;
|
|
4734
|
+
};
|
|
4536
4735
|
const initialWrites = h.entries.length;
|
|
4537
4736
|
await assert.rejects(() => tool.execute("invalid-work", { subject: "No effect", state: { work: { topic: "Login" } } }, undefined, undefined, ctx), /invalid/);
|
|
4538
4737
|
assert.equal(name, "");
|
|
@@ -4550,6 +4749,7 @@ test("registered session identity declares subjects and refreshes canonical idle
|
|
|
4550
4749
|
{ kind: "issue", repository: "github.com/Owner/Repo", id: "12" },
|
|
4551
4750
|
] };
|
|
4552
4751
|
await tool.execute("publish", { state: { objective: "Verify auth", decisions: "Advisory only", work } }, undefined, undefined, ctx);
|
|
4752
|
+
timers.run(400);
|
|
4553
4753
|
assert.equal(readDiscovery(profile, listPresence(profile).entries[0])?.state?.state?.objective, "Verify auth");
|
|
4554
4754
|
assert.deepEqual(readDiscovery(profile, listPresence(profile).entries[0])?.state?.state?.work, work);
|
|
4555
4755
|
const published = h.entries.at(-1)!;
|
|
@@ -4571,6 +4771,7 @@ test("registered session identity declares subjects and refreshes canonical idle
|
|
|
4571
4771
|
assert.equal(readDiscovery(profile, renamed)?.scope?.host.resolvedAt, readDiscovery(profile, before)?.scope?.host.resolvedAt);
|
|
4572
4772
|
registrations.push({ ...registrations[0], data: { ...registrations[0].data, root: join(cwd, "registered") } });
|
|
4573
4773
|
h.pi.events.emit(SESSION_WORKTREE_CHANGED, { sessionId: "s1" });
|
|
4774
|
+
timers.run(400);
|
|
4574
4775
|
assert.deepEqual(readDiscovery(profile, listPresence(profile).entries[0])?.scope?.registered.map(fact => fact.root), [cwd, join(cwd, "registered")]);
|
|
4575
4776
|
await declare("Do not overwrite");
|
|
4576
4777
|
assert.deepEqual(names, ["Fix auth"]);
|
|
@@ -4581,10 +4782,12 @@ test("registered session identity declares subjects and refreshes canonical idle
|
|
|
4581
4782
|
await declare("😀".repeat(130));
|
|
4582
4783
|
assert.equal(Array.from(name).length, 120);
|
|
4583
4784
|
await tool.execute("withdraw", { state: null }, undefined, undefined, ctx);
|
|
4785
|
+
timers.run(400);
|
|
4584
4786
|
await h.fire("session_start", ctx, { reason: "reload" });
|
|
4585
4787
|
await eventually(() => readDiscovery(profile, listPresence(profile).entries[0])?.state?.state === null, "withdrawal reload");
|
|
4586
4788
|
Object.assign(ctx.sessionManager, { getBranch: () => [published] });
|
|
4587
4789
|
await h.fire("session_tree", ctx);
|
|
4790
|
+
timers.run(400);
|
|
4588
4791
|
assert.equal(readDiscovery(profile, listPresence(profile).entries[0])?.state?.state?.objective, "Verify auth");
|
|
4589
4792
|
const sameId = fakeContext().ctx;
|
|
4590
4793
|
Object.assign(sameId.sessionManager, { getSessionName: () => name, getBranch: () => [] });
|
|
@@ -4593,6 +4796,7 @@ test("registered session identity declares subjects and refreshes canonical idle
|
|
|
4593
4796
|
const replacement = fakeContext().ctx;
|
|
4594
4797
|
Object.assign(replacement.sessionManager, { getSessionId: () => "s2", getSessionName: () => "Replacement" });
|
|
4595
4798
|
await h.fire("session_start", replacement, { reason: "new" });
|
|
4799
|
+
timers.run(400); // the post-transport-start publication is coalesced; close the window
|
|
4596
4800
|
heartbeats[0]();
|
|
4597
4801
|
assert.equal(listPresence(profile).entries.length, 1);
|
|
4598
4802
|
assert.equal(listPresence(profile).entries[0].label, "Replacement");
|
|
@@ -4728,6 +4932,33 @@ test("registered orchestrator_list joins peer metadata without child launches or
|
|
|
4728
4932
|
} finally { publisher.dispose(); }
|
|
4729
4933
|
});
|
|
4730
4934
|
|
|
4935
|
+
test("orchestrator_list surfaces the real session-presence failure code", async () => {
|
|
4936
|
+
const h = fakePi();
|
|
4937
|
+
const runtime = deps();
|
|
4938
|
+
const profile = realpathSync(mkdtempSync(join(root, "discovery-failure-")));
|
|
4939
|
+
let ready = false;
|
|
4940
|
+
const registry = { list: async () => [], listActivations: async () => { throw new SessionPresenceError("busy", "presence registry is busy"); } };
|
|
4941
|
+
runtime.deps.agentHome = profile;
|
|
4942
|
+
runtime.deps.sessionTransport = {
|
|
4943
|
+
createRegistry: async () => registry,
|
|
4944
|
+
createListener: () => ({ registry, start: async () => { ready = true; }, close: async () => {} }),
|
|
4945
|
+
createClient: () => ({ close() {}, sendNotification: async () => { throw new Error("no messages expected"); } }),
|
|
4946
|
+
};
|
|
4947
|
+
gentleAgents(h.pi, {}, runtime.deps);
|
|
4948
|
+
const { ctx } = fakeContext();
|
|
4949
|
+
try {
|
|
4950
|
+
await h.fire("session_start", ctx);
|
|
4951
|
+
await eventually(() => ready, "discovery transport ready");
|
|
4952
|
+
const result = await h.tools.get("orchestrator_list")!.execute("list", {}, undefined, undefined, ctx);
|
|
4953
|
+
assert.match(result.content[0].text, /session discovery is unavailable \(busy\)/);
|
|
4954
|
+
assert.equal((result.details as { error?: string }).error, "unavailable");
|
|
4955
|
+
assert.equal((result.details as { reason?: string }).reason, "busy");
|
|
4956
|
+
} finally {
|
|
4957
|
+
await h.fire("session_shutdown", ctx);
|
|
4958
|
+
rmSync(profile, { recursive: true, force: true });
|
|
4959
|
+
}
|
|
4960
|
+
});
|
|
4961
|
+
|
|
4731
4962
|
test("orchestrator_list labels a live peer that predates discovery publication (#2305)", async () => {
|
|
4732
4963
|
const h = fakePi();
|
|
4733
4964
|
const runtime = deps();
|
|
@@ -6704,6 +6935,7 @@ test("a human stop of a monitor from /gentle:jobs delivers its pending lines, th
|
|
|
6704
6935
|
// orchestrator_session_id, and never replaces the owner's curated fields.
|
|
6705
6936
|
test("todo progress publishes merged into the owner's curated state, only for this session", async () => {
|
|
6706
6937
|
const h = fakePi(), runtime = deps(), { ctx } = fakeContext();
|
|
6938
|
+
const timers = recordTimers(runtime.deps);
|
|
6707
6939
|
const profile = realpathSync(mkdtempSync(join(root, "todo-progress-")));
|
|
6708
6940
|
runtime.deps.agentHome = profile;
|
|
6709
6941
|
const peer = { version: 1 as const, sessionId: "s1", endpoint: "/fixture/todo-progress.sock", createdAt: 1 };
|
|
@@ -6717,7 +6949,10 @@ test("todo progress publishes merged into the owner's curated state, only for th
|
|
|
6717
6949
|
Object.assign(ctx.sessionManager, { getSessionName: () => "", getBranch: () => h.entries });
|
|
6718
6950
|
gentleAgents(h.pi, {}, runtime.deps);
|
|
6719
6951
|
await h.fire("session_start", ctx);
|
|
6720
|
-
const published = () =>
|
|
6952
|
+
const published = () => {
|
|
6953
|
+
timers.run(400); // the activity publication is coalesced; close the window
|
|
6954
|
+
return readDiscovery(profile, listPresence(profile).entries[0])?.state?.state;
|
|
6955
|
+
};
|
|
6721
6956
|
h.pi.events.emit(TODO_PROGRESS_EVENT, { sessionId: "s1", done: 0, total: 2, current: "Write parser" });
|
|
6722
6957
|
// PR #2290 review: a todo title is private work text; peers see counts only.
|
|
6723
6958
|
assert.deepEqual(published(), { progress: "0/2 done" }, "progress publishes before any curated state, without the todo title");
|
|
@@ -6745,6 +6980,7 @@ test("todo progress publishes merged into the owner's curated state, only for th
|
|
|
6745
6980
|
// progress line, so peers lost it until the next todo write.
|
|
6746
6981
|
test("an owner publication without progress keeps the automatic progress; explicit progress and null still win", async () => {
|
|
6747
6982
|
const h = fakePi(), runtime = deps(), { ctx } = fakeContext();
|
|
6983
|
+
const timers = recordTimers(runtime.deps);
|
|
6748
6984
|
const profile = realpathSync(mkdtempSync(join(root, "todo-progress-merge-")));
|
|
6749
6985
|
runtime.deps.agentHome = profile;
|
|
6750
6986
|
const peer = { version: 1 as const, sessionId: "s1", endpoint: "/fixture/todo-progress-merge.sock", createdAt: 1 };
|
|
@@ -6758,7 +6994,10 @@ test("an owner publication without progress keeps the automatic progress; explic
|
|
|
6758
6994
|
Object.assign(ctx.sessionManager, { getSessionName: () => "", getBranch: () => h.entries });
|
|
6759
6995
|
gentleAgents(h.pi, {}, runtime.deps);
|
|
6760
6996
|
await h.fire("session_start", ctx);
|
|
6761
|
-
const published = () =>
|
|
6997
|
+
const published = () => {
|
|
6998
|
+
timers.run(400); // the activity publication is coalesced; close the window
|
|
6999
|
+
return readDiscovery(profile, listPresence(profile).entries[0])?.state?.state;
|
|
7000
|
+
};
|
|
6762
7001
|
const own = (state: unknown) => h.tools.get("orchestrator_session_id")!.execute("own", { state }, undefined, undefined, ctx);
|
|
6763
7002
|
h.pi.events.emit(TODO_PROGRESS_EVENT, { sessionId: "s1", done: 1, total: 3, current: "Parser" });
|
|
6764
7003
|
await own({ objective: "Ship the ledger" });
|
|
@@ -6779,3 +7018,109 @@ test("an owner publication without progress keeps the automatic progress; explic
|
|
|
6779
7018
|
await own({ objective: full });
|
|
6780
7019
|
assert.deepEqual(published(), { objective: full }, "a merge over the state budget publishes the owner's state as given");
|
|
6781
7020
|
});
|
|
7021
|
+
|
|
7022
|
+
// Issue #2196 (PR #2233 maintainer decision): at launch, a model_profiles key
|
|
7023
|
+
// that names no discovered agent but suggests the launched one is reported
|
|
7024
|
+
// instead of being silently ignored while the child inherits the orchestrator
|
|
7025
|
+
// model. An omitted or matched role keeps today's inheritance untouched.
|
|
7026
|
+
test("subagent_run warns when a model profile key suggests the launched agent", async () => {
|
|
7027
|
+
const child = fakePi();
|
|
7028
|
+
const runtime = deps();
|
|
7029
|
+
gentleAgents(child.pi, {}, runtime.deps);
|
|
7030
|
+
const { ctx, dialogs } = fakeContext();
|
|
7031
|
+
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
|
7032
|
+
writeFileSync(join(cwd, ".pi", "subagents.json"), `${JSON.stringify({ model_profiles: { "gentle-ai-explore": { model: "openai/alpha" } } })}\n`);
|
|
7033
|
+
await child.fire("session_start", ctx);
|
|
7034
|
+
await child.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "warn", mode: "background" }, undefined, undefined, ctx);
|
|
7035
|
+
await tick();
|
|
7036
|
+
assert.equal(runtime.children.length, 1, "the launch itself still proceeds");
|
|
7037
|
+
assert.equal(dialogs.filter((message) => /"gentle-ai-explore" did not match any agent, did you mean explore\?/.test(message)).length, 1);
|
|
7038
|
+
|
|
7039
|
+
// A key that matches the agent exactly never warns, and the fixture's own
|
|
7040
|
+
// global effort profile (explore) stays matched.
|
|
7041
|
+
dialogs.length = 0;
|
|
7042
|
+
writeFileSync(join(cwd, ".pi", "subagents.json"), `${JSON.stringify({ model_profiles: { explore: { model: "openai/alpha" } } })}\n`);
|
|
7043
|
+
await child.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "quiet", mode: "background" }, undefined, undefined, ctx);
|
|
7044
|
+
await tick();
|
|
7045
|
+
assert.equal(dialogs.length, 0, "a matched key is silent");
|
|
7046
|
+
});
|
|
7047
|
+
|
|
7048
|
+
// CodeRabbit follow-up on PR #2397: a key that names another discovered agent
|
|
7049
|
+
// is valid, so launching a different agent whose name it merely resembles
|
|
7050
|
+
// (explore vs gentle-ai-explore) must not label it unmatched.
|
|
7051
|
+
test("subagent_run does not warn about a key that matches another discovered agent", async (t) => {
|
|
7052
|
+
const child = fakePi();
|
|
7053
|
+
const runtime = deps();
|
|
7054
|
+
gentleAgents(child.pi, {}, runtime.deps);
|
|
7055
|
+
const { ctx, dialogs } = fakeContext();
|
|
7056
|
+
mkdirSync(join(cwd, ".pi", "agents"), { recursive: true });
|
|
7057
|
+
const projectAgent = join(cwd, ".pi", "agents", "gentle-ai-explore.md");
|
|
7058
|
+
const subagents = join(cwd, ".pi", "subagents.json");
|
|
7059
|
+
t.after(() => rmSync(projectAgent, { force: true }));
|
|
7060
|
+
t.after(() => rmSync(subagents, { force: true }));
|
|
7061
|
+
writeFileSync(projectAgent, "---\ndescription: packaged explore\ntools: [read]\n---\nYou map things.");
|
|
7062
|
+
writeFileSync(subagents, `${JSON.stringify({ model_profiles: { "gentle-ai-explore": { model: "openai/alpha" } } })}\n`);
|
|
7063
|
+
await child.fire("session_start", ctx);
|
|
7064
|
+
await child.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "quiet", mode: "background" }, undefined, undefined, ctx);
|
|
7065
|
+
await tick();
|
|
7066
|
+
assert.equal(runtime.children.length, 1, "the launch proceeds");
|
|
7067
|
+
assert.equal(dialogs.filter((message) => /did not match any agent/.test(message)).length, 0, "a key naming a discovered agent is never unmatched");
|
|
7068
|
+
});
|
|
7069
|
+
|
|
7070
|
+
// Issue #2400: a model_profiles key that matches no discovered agent and has no
|
|
7071
|
+
// did-you-mean suggestion (a typo such as `explor`) was still silent, because
|
|
7072
|
+
// the launch warning only named keys suggesting the launched agent. The first
|
|
7073
|
+
// launch of a session now lists every such key once; later launches in the
|
|
7074
|
+
// same session stay quiet, and the key is never routed.
|
|
7075
|
+
test("subagent_run warns once per session about model profile keys with no suggestion", async (t) => {
|
|
7076
|
+
const child = fakePi();
|
|
7077
|
+
const runtime = deps();
|
|
7078
|
+
const captures = captureLaunches(t);
|
|
7079
|
+
gentleAgents(child.pi, {}, runtime.deps);
|
|
7080
|
+
const { ctx, dialogs } = fakeContext();
|
|
7081
|
+
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
|
7082
|
+
const subagents = join(cwd, ".pi", "subagents.json");
|
|
7083
|
+
t.after(() => rmSync(subagents, { force: true }));
|
|
7084
|
+
writeFileSync(subagents, `${JSON.stringify({ model_profiles: { explor: { model: "openai/alpha" }, "sdd-desing": { model: "openai/beta" } } })}\n`);
|
|
7085
|
+
await child.fire("session_start", ctx);
|
|
7086
|
+
const unmatched = () => dialogs.filter((message) => /did not match any agent and was ignored/.test(message));
|
|
7087
|
+
await child.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "first", mode: "background" }, undefined, undefined, ctx);
|
|
7088
|
+
await tick();
|
|
7089
|
+
assert.equal(unmatched().length, 1, "the first launch names the unmatched keys");
|
|
7090
|
+
assert.match(unmatched()[0]!, /"explor" did not match any agent and was ignored\./);
|
|
7091
|
+
assert.match(unmatched()[0]!, /"sdd-desing" did not match any agent and was ignored\./);
|
|
7092
|
+
assert.equal(dialogs.filter((message) => /did you mean/.test(message)).length, 0, "a key without a suggestion never claims one");
|
|
7093
|
+
assert.equal(captures[0]!.request.modelSource === "profile", false, "an unmatched key is never routed");
|
|
7094
|
+
|
|
7095
|
+
await child.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "second", mode: "background" }, undefined, undefined, ctx);
|
|
7096
|
+
await tick();
|
|
7097
|
+
assert.equal(unmatched().length, 1, "later launches in the same session do not repeat it");
|
|
7098
|
+
|
|
7099
|
+
// A different parent session is told again.
|
|
7100
|
+
const other = fakeContext();
|
|
7101
|
+
other.ctx.sessionManager.getSessionId = () => "s2";
|
|
7102
|
+
await child.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "other", mode: "background" }, undefined, undefined, other.ctx);
|
|
7103
|
+
await tick();
|
|
7104
|
+
assert.equal(other.dialogs.filter((message) => /"explor" did not match any agent and was ignored\./.test(message)).length, 1);
|
|
7105
|
+
});
|
|
7106
|
+
|
|
7107
|
+
// Cortex QA adversarial follow-up (end-to-end pin for the #2196 fix): a key
|
|
7108
|
+
// that matches a discovered agent must not only stay silent, it must actually
|
|
7109
|
+
// route: the task request carries the profile model and thinking, so the child
|
|
7110
|
+
// launches with --model exactly as the issue's expected behavior describes.
|
|
7111
|
+
test("a matched model_profiles key routes the task request model end to end", async (t) => {
|
|
7112
|
+
const h = fakePi();
|
|
7113
|
+
const runtime = deps();
|
|
7114
|
+
const captures = captureLaunches(t);
|
|
7115
|
+
gentleAgents(h.pi, {}, runtime.deps);
|
|
7116
|
+
const { ctx } = fakeContext();
|
|
7117
|
+
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
|
7118
|
+
writeFileSync(join(cwd, ".pi", "subagents.json"), `${JSON.stringify({ model_profiles: { explore: { model: "openai/alpha", effort: "high" } } })}\n`);
|
|
7119
|
+
await h.fire("session_start", ctx);
|
|
7120
|
+
await h.tools.get("subagent_run")!.execute("run", { agent: "explore", task: "route", mode: "background" }, undefined, undefined, ctx);
|
|
7121
|
+
await tick();
|
|
7122
|
+
assert.equal(captures.length, 1);
|
|
7123
|
+
assert.deepEqual(captures[0]!.request.model, { provider: "openai", id: "alpha" }, "the matched key routes the configured model into the launch");
|
|
7124
|
+
assert.equal(captures[0]!.request.thinking, "high");
|
|
7125
|
+
assert.equal(captures[0]!.request.modelSource, "profile");
|
|
7126
|
+
});
|
|
@@ -98,7 +98,7 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
98
98
|
method: "go-sumdb-source-build",
|
|
99
99
|
package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
100
100
|
module: "github.com/gentleman-programming/gentle-ai/v4",
|
|
101
|
-
tag: "v4.
|
|
101
|
+
tag: "v4.3.1",
|
|
102
102
|
architecture: process.arch === "x64" ? "x64" : "arm64",
|
|
103
103
|
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
104
104
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|