gentle-pi 3.2.1 → 3.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 +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
|
@@ -13,12 +13,12 @@ import os from "node:os";
|
|
|
13
13
|
import { join, resolve, isAbsolute, sep } from "node:path";
|
|
14
14
|
import { createBashToolDefinition, createLocalBashOperations, type BashOperations, keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Text, type TUI } from "@earendil-works/pi-tui";
|
|
16
|
-
import { sidebarPart } from "../lib/shell-sidebar.ts";
|
|
17
16
|
import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
18
17
|
import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
|
|
19
18
|
import { AGENT_MODE, discoverAgents, parseAgentDefinition, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
|
|
20
19
|
import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
|
|
21
|
-
import {
|
|
20
|
+
import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
|
|
21
|
+
import { isFinished, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
22
22
|
import { AgentRunner, piCommand, abortReasonText, plannedCommands, type RemediationPlan, type RemediationScope, REMEDIATION_PLAN_ENV, parseRemediationPlan, type AskAnswer, type RunnerDeps, type SddChangeSelection, type TaskRequest } from "../lib/agents-runner.ts";
|
|
23
23
|
import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
|
|
24
24
|
import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
|
|
@@ -499,6 +499,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
499
499
|
let renderQueued = false;
|
|
500
500
|
let cancelClock: (() => void) | undefined;
|
|
501
501
|
const ownedTaskIds = new Set<string>();
|
|
502
|
+
// One diagnostic note per (task, guard): a dropped mutation says why once,
|
|
503
|
+
// not once per file, so a chatty child cannot flood its own thread.
|
|
504
|
+
const droppedAttributionGuards = new Set<string>();
|
|
502
505
|
const stoppingTaskIds = new Set<string>();
|
|
503
506
|
const yieldedTaskIds = new Set<string>();
|
|
504
507
|
const metricsNow = deps.metricsNow ?? (() => performance.now());
|
|
@@ -527,6 +530,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
527
530
|
// started before /new or /resume stays in the store and comes back with
|
|
528
531
|
// its session. Before the first session_start there is nothing to scope by.
|
|
529
532
|
const activeSessionId = (): string | undefined => (sessions === undefined ? undefined : sessions.getSessionId() ?? "");
|
|
533
|
+
// Pi 0.86.1 adds this event; the package's pinned 0.85.1 types predate it.
|
|
534
|
+
installBackgroundCacheWarming(pi as unknown as Parameters<typeof installBackgroundCacheWarming>[0], () => ({
|
|
535
|
+
sessionId: activeSessionId(),
|
|
536
|
+
ownedTaskIds,
|
|
537
|
+
tasks: store.list(activeSessionId()),
|
|
538
|
+
}));
|
|
530
539
|
const visibleTasks = (): TaskRecord[] => store.list(activeSessionId());
|
|
531
540
|
type SessionTransport = { generation: number; sessionId: string; sessionManager: ExtensionContext["sessionManager"]; client: SessionTransportClient; listener: SessionTransportListener; registry: SessionTransportRegistry; close(): Promise<void> };
|
|
532
541
|
let transportGeneration = 0;
|
|
@@ -744,16 +753,43 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
744
753
|
}
|
|
745
754
|
},
|
|
746
755
|
onSuccessfulMutation: (task, tool) => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
756
|
+
// Guard chain and posture are unchanged: only owned tasks of the
|
|
757
|
+
// active parent, inside registered roots, ever relay. The notes only
|
|
758
|
+
// explain a drop -- they never widen or narrow what gets attributed.
|
|
759
|
+
// The cheap session/ownership guards decide before any worktree
|
|
760
|
+
// resolution, so a foreign or stale mutation never reaches git.
|
|
761
|
+
let root: string | undefined;
|
|
762
|
+
let childRoot: string | undefined;
|
|
763
|
+
const noteDrop = (guard: string) => {
|
|
764
|
+
const key = `${task.id}:${guard}`;
|
|
765
|
+
if (droppedAttributionGuards.has(key)) return;
|
|
766
|
+
droppedAttributionGuards.add(key);
|
|
767
|
+
try { store.apply(task.id, { type: TASK_EVENT.NOTE, text: `changes not attributed: ${guard} (root=${root ?? "unknown"}, child=${childRoot ?? "unknown"})` }, deps.now()); }
|
|
768
|
+
catch { /* A note is best-effort explanation; it must never block the guard it is explaining. */ }
|
|
769
|
+
};
|
|
770
|
+
if (!sessions || !worktrees) return noteDrop("session-inactive");
|
|
771
|
+
if (task.parentSessionId !== activeSessionId()) return noteDrop("parent-session-mismatch");
|
|
772
|
+
if (!ownedTaskIds.has(task.id)) return noteDrop("not-owned");
|
|
773
|
+
root = deps.resolveWorktree(tool.path, task.cwd)?.root;
|
|
774
|
+
childRoot = deps.resolveWorktree(task.cwd, task.cwd)?.root;
|
|
775
|
+
if (!root) return noteDrop("root-unresolved");
|
|
776
|
+
if (root !== childRoot) return noteDrop("root-mismatch");
|
|
777
|
+
if (!worktrees.roots().includes(root)) return noteDrop("root-not-registered");
|
|
778
|
+
if (!tool.evidence) {
|
|
779
|
+
noteDrop("evidence-missing");
|
|
780
|
+
} else if (tool.evidence.root !== root) {
|
|
781
|
+
noteDrop("evidence-root-mismatch");
|
|
782
|
+
} else {
|
|
783
|
+
let path = tool.path.replace(/^@/, "");
|
|
784
|
+
if (path === "~" || path.startsWith("~/")) path = os.homedir() + path.slice(1);
|
|
785
|
+
let resolvedPath: string | undefined;
|
|
752
786
|
try {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
787
|
+
resolvedPath = realpathSync(resolve(task.cwd, path));
|
|
788
|
+
} catch {
|
|
789
|
+
noteDrop("evidence-path-unreadable");
|
|
790
|
+
}
|
|
791
|
+
if (resolvedPath === resolve(root, tool.evidence.path)) pi.events.emit(SESSION_CHANGE_RELAY, { sessionId: task.parentSessionId, evidence: { ...tool.evidence, id: `${task.id}:${tool.toolCallId}` } });
|
|
792
|
+
else if (resolvedPath !== undefined) noteDrop("evidence-path-mismatch");
|
|
757
793
|
}
|
|
758
794
|
recordReviewMutation(pi, sessions, root, { source: "subagent", taskId: task.id, toolName: tool.toolName, toolCallId: tool.toolCallId });
|
|
759
795
|
},
|
|
@@ -895,6 +931,33 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
895
931
|
return stored?.task;
|
|
896
932
|
};
|
|
897
933
|
|
|
934
|
+
// Restores this exact session's own finished subagents as visible history
|
|
935
|
+
// on an explicit resume, or on startup into a session that already has
|
|
936
|
+
// entries -- never on new, fork, or reload (see the session_start handler's
|
|
937
|
+
// preexisting check below, which is the actual gate). Marked in
|
|
938
|
+
// restoredTaskIds like any other disk restoration, so it stays
|
|
939
|
+
// non-cancellable (ownedTaskIds never gained the id either way) and is
|
|
940
|
+
// skipped if the id is somehow already live.
|
|
941
|
+
const restoreSessionHistory = async (ctx: ExtensionContext, sessionId: string): Promise<void> => {
|
|
942
|
+
let history: Awaited<ReturnType<typeof loadHistory>>;
|
|
943
|
+
try {
|
|
944
|
+
history = await loadHistory(tasksDir);
|
|
945
|
+
} catch {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
// The session may have moved on while disk was read; a stale restore
|
|
949
|
+
// must never land in the wrong session's store.
|
|
950
|
+
if (ctx.sessionManager.getSessionId() !== sessionId) return;
|
|
951
|
+
// Fire-and-forget from session_start: a throwing summary subscriber must
|
|
952
|
+
// never surface as an unhandled rejection. History is best-effort.
|
|
953
|
+
try {
|
|
954
|
+
for (const { task, thread } of history) {
|
|
955
|
+
if (task.parentSessionId !== sessionId) continue;
|
|
956
|
+
if (store.restore(task, thread)) restoredTaskIds.add(task.id);
|
|
957
|
+
}
|
|
958
|
+
} catch { /* Partial history is acceptable; the live session keeps running. */ }
|
|
959
|
+
};
|
|
960
|
+
|
|
898
961
|
const openOverlay = async (ctx: ExtensionContext) => {
|
|
899
962
|
if (!ctx.hasUI) return;
|
|
900
963
|
if (ctx.mode !== "tui") {
|
|
@@ -974,19 +1037,19 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
974
1037
|
ui = ctx.hasUI ? ctx.ui : undefined;
|
|
975
1038
|
sessions = ctx.sessionManager;
|
|
976
1039
|
tickClock();
|
|
1040
|
+
// Agents is not a rail part: the fullscreen sidebar only suppresses
|
|
1041
|
+
// bottom components registered through sidebarPart, and this widget is
|
|
1042
|
+
// the only Agents surface in every mode, so it stays a plain component.
|
|
977
1043
|
ui?.setWidget(AGENTS_WIDGET_KEY, (tui, theme) => {
|
|
978
1044
|
host = tui;
|
|
979
1045
|
sidebarTui = tui;
|
|
980
|
-
return
|
|
1046
|
+
return {
|
|
981
1047
|
render(width: number) {
|
|
982
1048
|
const lines = renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, maxRows: widgetRows(tui.terminal?.rows), viewKey });
|
|
983
1049
|
return lines.length === 0 ? [] : [...lines, ""];
|
|
984
1050
|
},
|
|
985
1051
|
invalidate() {},
|
|
986
|
-
}
|
|
987
|
-
render: (width) => renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, viewKey }),
|
|
988
|
-
invalidate() {},
|
|
989
|
-
});
|
|
1052
|
+
};
|
|
990
1053
|
});
|
|
991
1054
|
};
|
|
992
1055
|
|
|
@@ -1112,7 +1175,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1112
1175
|
if (observe) metricTasks.set(task.id, metrics);
|
|
1113
1176
|
ownedTaskIds.add(task.id);
|
|
1114
1177
|
store.subscribe(task.id, () => { publishActivity(); requestRender(); });
|
|
1115
|
-
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}.
|
|
1178
|
+
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
|
|
1116
1179
|
// A tool call aborted by the host (a human interrupting the turn, a timeout)
|
|
1117
1180
|
// would otherwise leave the child running and end the call with no result and
|
|
1118
1181
|
// no recorded reason. Cancel through the runner so the lifecycle runs and the
|
|
@@ -1362,13 +1425,25 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1362
1425
|
});
|
|
1363
1426
|
}
|
|
1364
1427
|
|
|
1365
|
-
pi.on("session_start", async (
|
|
1428
|
+
pi.on("session_start", async (event, ctx) => {
|
|
1366
1429
|
// A resumed, reloaded, or replaced session starts with an empty completion
|
|
1367
1430
|
// queue so nothing pending from another session can replay here.
|
|
1368
1431
|
completions.dropAll();
|
|
1369
1432
|
presence?.dispose();
|
|
1370
1433
|
registryFor(ctx);
|
|
1371
1434
|
showWidget(ctx);
|
|
1435
|
+
// An explicit in-session /resume always brings this session's own
|
|
1436
|
+
// finished subagents back from disk. Pi also reports "startup" (not
|
|
1437
|
+
// "resume") when the CLI is launched directly into an existing session
|
|
1438
|
+
// file, e.g. --continue or the --resume picker (agent-session.js:152
|
|
1439
|
+
// defaults to "startup"); that case restores too, but only when the
|
|
1440
|
+
// session actually has prior entries -- a brand-new session can also be
|
|
1441
|
+
// announced as "startup", and a fresh session has none. "new", "fork",
|
|
1442
|
+
// and "reload" never restore here, matching the existing on-demand
|
|
1443
|
+
// resolveTask path for anything else.
|
|
1444
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1445
|
+
const preexisting = event.reason === "resume" || (event.reason === "startup" && ctx.sessionManager.getEntries().length > 0);
|
|
1446
|
+
if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
|
|
1372
1447
|
try {
|
|
1373
1448
|
presence = PresencePublisher.start({ profile: agentHome, sessionId: activeSessionId() ?? "",
|
|
1374
1449
|
label: ctx.sessionManager.getSessionName?.() || ctx.sessionManager.getCwd().split(/[\\/]/).pop() || "Orchestrator", activity: [] });
|