gentle-pi 3.7.0 → 4.0.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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
SessionMessagingGrants,
|
|
5
|
+
MESSAGING_CONSENT_DECISIONS,
|
|
6
|
+
} from "../lib/session-messaging-grants.ts";
|
|
7
|
+
|
|
8
|
+
function context(
|
|
9
|
+
selectResult?: (title: string, options: string[]) => Promise<string | undefined>,
|
|
10
|
+
hasUI = true
|
|
11
|
+
) {
|
|
12
|
+
let id = "session-1";
|
|
13
|
+
let calls = 0;
|
|
14
|
+
const dialogs: { title: string; options: string[] }[] = [];
|
|
15
|
+
const sessionManager = { getSessionId: () => id };
|
|
16
|
+
const ctx = {
|
|
17
|
+
sessionManager,
|
|
18
|
+
hasUI,
|
|
19
|
+
ui: hasUI && selectResult
|
|
20
|
+
? {
|
|
21
|
+
select: async (title: string, options: string[]) => {
|
|
22
|
+
calls++;
|
|
23
|
+
dialogs.push({ title, options });
|
|
24
|
+
return selectResult(title, options);
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
: undefined,
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
ctx,
|
|
31
|
+
setId: (next: string) => {
|
|
32
|
+
id = next;
|
|
33
|
+
},
|
|
34
|
+
calls: () => calls,
|
|
35
|
+
dialogs: () => dialogs,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("SessionMessagingGrants - Allow once authorizes only the current message and prompts again on next send", async () => {
|
|
40
|
+
const grants = new SessionMessagingGrants();
|
|
41
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE);
|
|
42
|
+
|
|
43
|
+
await grants.authorize(h.ctx as never, "peer-alpha", {
|
|
44
|
+
message: "hello peer",
|
|
45
|
+
reason: "sync state",
|
|
46
|
+
});
|
|
47
|
+
assert.equal(h.calls(), 1);
|
|
48
|
+
assert.match(h.dialogs()[0].title, /peer-alpha/);
|
|
49
|
+
assert.match(h.dialogs()[0].title, /sync state/);
|
|
50
|
+
assert.match(h.dialogs()[0].title, /hello peer/);
|
|
51
|
+
assert.deepEqual(h.dialogs()[0].options, [
|
|
52
|
+
MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE,
|
|
53
|
+
MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION,
|
|
54
|
+
MESSAGING_CONSENT_DECISIONS.DENY,
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// Second send to same peer must prompt again because it was only allowed once
|
|
58
|
+
await grants.authorize(h.ctx as never, "peer-alpha", {
|
|
59
|
+
message: "second message",
|
|
60
|
+
reason: "because another update is needed",
|
|
61
|
+
});
|
|
62
|
+
assert.equal(h.calls(), 2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("SessionMessagingGrants - Allow for this session caches permission and skips prompt on subsequent sends to same recipient", async () => {
|
|
66
|
+
const grants = new SessionMessagingGrants();
|
|
67
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION);
|
|
68
|
+
|
|
69
|
+
await grants.authorize(h.ctx as never, "peer-alpha", {
|
|
70
|
+
message: "first message",
|
|
71
|
+
reason: "because the first update is needed",
|
|
72
|
+
});
|
|
73
|
+
assert.equal(h.calls(), 1);
|
|
74
|
+
|
|
75
|
+
// Subsequent sends to peer-alpha in the same session must NOT prompt
|
|
76
|
+
await grants.authorize(h.ctx as never, "peer-alpha", {
|
|
77
|
+
message: "second message",
|
|
78
|
+
reason: "because another update is needed",
|
|
79
|
+
});
|
|
80
|
+
assert.equal(h.calls(), 1, "must not prompt again for the same session-granted recipient");
|
|
81
|
+
|
|
82
|
+
// But a different recipient must prompt!
|
|
83
|
+
await grants.authorize(h.ctx as never, "peer-beta", {
|
|
84
|
+
message: "hello beta",
|
|
85
|
+
reason: "because beta needs a separate update",
|
|
86
|
+
});
|
|
87
|
+
assert.equal(h.calls(), 2, "changed recipient must require a new decision");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("SessionMessagingGrants - Deny fails closed and sends nothing", async () => {
|
|
91
|
+
const grants = new SessionMessagingGrants();
|
|
92
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.DENY);
|
|
93
|
+
|
|
94
|
+
await assert.rejects(
|
|
95
|
+
grants.authorize(h.ctx as never, "peer-alpha", { message: "hello", reason: "because this is needed" }),
|
|
96
|
+
/denied/i
|
|
97
|
+
);
|
|
98
|
+
assert.equal(h.calls(), 1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("SessionMessagingGrants - cancelling prompt (Escape / undefined) fails closed", async () => {
|
|
102
|
+
const grants = new SessionMessagingGrants();
|
|
103
|
+
const h = context(async () => undefined);
|
|
104
|
+
|
|
105
|
+
await assert.rejects(
|
|
106
|
+
grants.authorize(h.ctx as never, "peer-alpha", { message: "hello", reason: "because this is needed" }),
|
|
107
|
+
/denied|cancelled/i
|
|
108
|
+
);
|
|
109
|
+
assert.equal(h.calls(), 1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("SessionMessagingGrants - headless or missing UI fails closed without prompting", async () => {
|
|
113
|
+
const grants = new SessionMessagingGrants();
|
|
114
|
+
const headless = context(undefined, false);
|
|
115
|
+
|
|
116
|
+
await assert.rejects(
|
|
117
|
+
grants.authorize(headless.ctx as never, "peer-alpha", { message: "hello", reason: "because this is needed" }),
|
|
118
|
+
/interactive.*consent/i
|
|
119
|
+
);
|
|
120
|
+
assert.equal(headless.calls(), 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("SessionMessagingGrants - AbortSignal aborts consent and fails closed", async () => {
|
|
124
|
+
const grants = new SessionMessagingGrants();
|
|
125
|
+
const ac = new AbortController();
|
|
126
|
+
ac.abort();
|
|
127
|
+
|
|
128
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE);
|
|
129
|
+
await assert.rejects(
|
|
130
|
+
grants.authorize(h.ctx as never, "peer-alpha", {
|
|
131
|
+
message: "hello",
|
|
132
|
+
reason: "because this is needed",
|
|
133
|
+
signal: ac.signal,
|
|
134
|
+
}),
|
|
135
|
+
/aborted/i
|
|
136
|
+
);
|
|
137
|
+
assert.equal(h.calls(), 0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("SessionMessagingGrants - session change invalidates previous session grants", async () => {
|
|
141
|
+
const grants = new SessionMessagingGrants();
|
|
142
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION);
|
|
143
|
+
|
|
144
|
+
await grants.authorize(h.ctx as never, "peer-alpha", { message: "first", reason: "because session one needs it" });
|
|
145
|
+
assert.equal(h.calls(), 1);
|
|
146
|
+
|
|
147
|
+
// Change session ID
|
|
148
|
+
h.setId("session-2");
|
|
149
|
+
|
|
150
|
+
// Now peer-alpha must prompt again because session changed
|
|
151
|
+
await grants.authorize(h.ctx as never, "peer-alpha", { message: "in new session", reason: "because session two needs it" });
|
|
152
|
+
assert.equal(h.calls(), 2, "session change must invalidate previous session grants");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("SessionMessagingGrants - sessionManager replacement revokes grants", async () => {
|
|
156
|
+
const grants = new SessionMessagingGrants();
|
|
157
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION);
|
|
158
|
+
|
|
159
|
+
await grants.authorize(h.ctx as never, "peer-alpha", { message: "first", reason: "because session one needs it" });
|
|
160
|
+
assert.equal(h.calls(), 1);
|
|
161
|
+
|
|
162
|
+
// Replace sessionManager object
|
|
163
|
+
const replacedCtx = {
|
|
164
|
+
...h.ctx,
|
|
165
|
+
sessionManager: { getSessionId: () => "session-1" },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await grants.authorize(replacedCtx as never, "peer-alpha", { message: "with new manager", reason: "because manager changed" });
|
|
169
|
+
assert.equal(h.calls(), 2, "sessionManager replacement must invalidate previous grants");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("SessionMessagingGrants - session ID change during prompt selection rejects and records no grant", async () => {
|
|
173
|
+
const grants = new SessionMessagingGrants();
|
|
174
|
+
const h = context(async () => {
|
|
175
|
+
h.setId("session-mutated");
|
|
176
|
+
return MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await assert.rejects(
|
|
180
|
+
grants.authorize(h.ctx as never, "peer-alpha", { message: "concurrent", reason: "because concurrent work is needed" }),
|
|
181
|
+
/identity changed/i
|
|
182
|
+
);
|
|
183
|
+
assert.equal(h.calls(), 1);
|
|
184
|
+
|
|
185
|
+
// Subsequent authorization in the mutated session must prompt again
|
|
186
|
+
await grants.authorize(h.ctx as never, "peer-alpha", { message: "fresh", reason: "because fresh consent is needed" });
|
|
187
|
+
assert.equal(h.calls(), 2, "must prompt again because no grant was recorded for mutated session");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("SessionMessagingGrants - truncates long messages with char count notice and shows the supplied reason", async () => {
|
|
191
|
+
const grants = new SessionMessagingGrants();
|
|
192
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE);
|
|
193
|
+
const longMessage = "x".repeat(300);
|
|
194
|
+
|
|
195
|
+
await grants.authorize(h.ctx as never, "peer-long", { message: longMessage, reason: "because the long message needs review" });
|
|
196
|
+
assert.equal(h.calls(), 1);
|
|
197
|
+
assert.match(h.dialogs()[0].title, /Reason: because the long message needs review/);
|
|
198
|
+
assert.match(h.dialogs()[0].title, /Message \(300 chars, preview\): x{197}\.\.\./);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("SessionMessagingGrants - validates a concrete bounded reason before prompting", async () => {
|
|
202
|
+
const grants = new SessionMessagingGrants();
|
|
203
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE);
|
|
204
|
+
|
|
205
|
+
for (const reason of [undefined, " short ", "é".repeat(257)]) {
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
grants.authorize(h.ctx as never, "peer-alpha", { message: "hello", reason } as never),
|
|
208
|
+
/reason/i,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
await grants.authorize(h.ctx as never, "peer-min", { message: "hello", reason: " 12345678 " });
|
|
212
|
+
await grants.authorize(h.ctx as never, "peer-max", { message: "hello", reason: "é".repeat(256) });
|
|
213
|
+
assert.equal(h.calls(), 2, "8 trimmed characters and exactly 512 UTF-8 bytes are valid");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("SessionMessagingGrants - cached grant still requires an available interactive selector", async () => {
|
|
217
|
+
const grants = new SessionMessagingGrants();
|
|
218
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION);
|
|
219
|
+
const options = { message: "first", reason: "because a session grant is needed" };
|
|
220
|
+
await grants.authorize(h.ctx as never, "peer-alpha", options);
|
|
221
|
+
assert.equal(h.calls(), 1);
|
|
222
|
+
|
|
223
|
+
(h.ctx as { hasUI: boolean }).hasUI = false;
|
|
224
|
+
await assert.rejects(grants.authorize(h.ctx as never, "peer-alpha", options), /interactive.*consent/i);
|
|
225
|
+
(h.ctx as { hasUI: boolean }).hasUI = true;
|
|
226
|
+
(h.ctx as { ui?: unknown }).ui = undefined;
|
|
227
|
+
await assert.rejects(grants.authorize(h.ctx as never, "peer-alpha", options), /interactive.*consent/i);
|
|
228
|
+
assert.equal(h.calls(), 1, "cached grants cannot bypass an unavailable selector");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("SessionMessagingGrants - snapshots message and reason while consent selection is pending", async () => {
|
|
232
|
+
let release!: (decision: string) => void;
|
|
233
|
+
const selection = new Promise<string>((resolve) => { release = resolve; });
|
|
234
|
+
const h = context(async () => selection);
|
|
235
|
+
const grants = new SessionMessagingGrants();
|
|
236
|
+
const options = { message: "original\r\nMessage: forged", reason: "because original request" };
|
|
237
|
+
const pending = grants.authorize(h.ctx as never, "peer-alpha", options);
|
|
238
|
+
options.message = "mutated message";
|
|
239
|
+
options.reason = "mutated reason";
|
|
240
|
+
release(MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE);
|
|
241
|
+
await pending;
|
|
242
|
+
assert.match(h.dialogs()[0].title, /Reason: because original request/);
|
|
243
|
+
assert.match(h.dialogs()[0].title, /Message: original\\r\\nMessage: forged/);
|
|
244
|
+
assert.doesNotMatch(h.dialogs()[0].title, /mutated/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("SessionMessagingGrants - escapes preview controls and Unicode line separators", async () => {
|
|
248
|
+
const grants = new SessionMessagingGrants();
|
|
249
|
+
const h = context(async () => MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE);
|
|
250
|
+
await grants.authorize(h.ctx as never, "peer\nReason: forged", {
|
|
251
|
+
message: "line1\r\nReason: forged\ttab\u2028next\u2029end\u001b[31m",
|
|
252
|
+
reason: "because\r\nMessage: forged\tline\u2028sep",
|
|
253
|
+
});
|
|
254
|
+
assert.equal(h.dialogs()[0].title, "Authorize cross-orchestrator message to peer\\nReason: forged?\nReason: because\\r\\nMessage: forged\\tline\\u2028sep\nMessage: line1\\r\\nReason: forged\\ttab\\u2028next\\u2029end\\u001B[31m");
|
|
255
|
+
});
|
|
@@ -52,6 +52,83 @@ test("canonical roots include cwd, dedupe aliases immediately and reject unrelat
|
|
|
52
52
|
assert.equal(resolveSessionWorktree(f.alias, f.main)?.root, f.linked);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
test("a registry constructed before Git init adopts only its original cwd in the same session", (t) => {
|
|
56
|
+
const f = fixture(t);
|
|
57
|
+
const cwd = join(f.dir, "bootstrap");
|
|
58
|
+
mkdirSync(cwd);
|
|
59
|
+
const h = host(cwd);
|
|
60
|
+
const registry = h.registry();
|
|
61
|
+
const id = h.session.getSessionId();
|
|
62
|
+
registry.start();
|
|
63
|
+
assert.deepEqual(registry.roots(), []);
|
|
64
|
+
assert.throws(() => registry.register(f.other, "explicit"), /same Git clone/);
|
|
65
|
+
assert.throws(() => registry.register(cwd, "explicit"), /same Git clone/);
|
|
66
|
+
execFileSync("git", ["init", "--quiet", `--template=${join(f.dir, "empty")}`, cwd], {
|
|
67
|
+
env: { ...worktreeGitEnvironment(), GIT_CONFIG_GLOBAL: join(f.dir, "empty", "config"), GIT_CONFIG_NOSYSTEM: "1" }, stdio: "pipe",
|
|
68
|
+
});
|
|
69
|
+
const poisoned = { GIT_DIR: join(f.other, ".git"), GIT_WORK_TREE: f.other, GIT_COMMON_DIR: join(f.other, ".git") };
|
|
70
|
+
const previous = Object.fromEntries(Object.keys(poisoned).map(key => [key, process.env[key]]));
|
|
71
|
+
try {
|
|
72
|
+
Object.assign(process.env, poisoned);
|
|
73
|
+
assert.equal(registry.register(cwd, "explicit"), cwd, "ambient Git routing cannot select authority");
|
|
74
|
+
} finally {
|
|
75
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
76
|
+
if (value === undefined) delete process.env[key]; else process.env[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
registry.register(cwd, "subagent:spawn");
|
|
80
|
+
assert.deepEqual(registry.roots(), [cwd]);
|
|
81
|
+
assert.equal(h.session.getSessionId(), id);
|
|
82
|
+
assert.equal(h.session.getEntries().length, 1, "adoption does not double append");
|
|
83
|
+
assert.throws(() => registry.register(f.other, "explicit"), /same Git clone/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("malformed local metadata cannot adopt a foreign registration target", (t) => {
|
|
87
|
+
const f = fixture(t);
|
|
88
|
+
const cwd = join(f.dir, "broken-bootstrap");
|
|
89
|
+
mkdirSync(cwd);
|
|
90
|
+
writeFileSync(join(cwd, ".git"), "gitdir: absent-metadata\n");
|
|
91
|
+
const h = host(cwd);
|
|
92
|
+
const registry = h.registry();
|
|
93
|
+
registry.start();
|
|
94
|
+
assert.throws(() => registry.register(f.other, "explicit"), /same Git clone/);
|
|
95
|
+
assert.throws(() => registry.register(cwd, "explicit"), /same Git clone/);
|
|
96
|
+
assert.deepEqual(registry.roots(), []);
|
|
97
|
+
assert.deepEqual(h.session.getEntries(), []);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for (const drift of ["root", "commonDir", "missing"] as const) {
|
|
101
|
+
test(`established session authority rejects ${drift} drift without adopting it`, () => {
|
|
102
|
+
const original = { root: "/session", commonDir: "/clone/git" };
|
|
103
|
+
let current: typeof original | undefined = original;
|
|
104
|
+
const entries: unknown[] = [];
|
|
105
|
+
const registry = new SessionWorktreeRegistry({ appendEntry: (_type, data) => { entries.push(data); }, events: { emit() {} } },
|
|
106
|
+
{ getSessionId: () => "bound", getEntries: () => [] }, "/session/subdir", (path) => path === "/session/subdir" ? current : original);
|
|
107
|
+
registry.start();
|
|
108
|
+
current = drift === "missing" ? undefined : { ...original, [drift]: "/foreign" };
|
|
109
|
+
assert.throws(() => registry.register(original.root, "explicit"), /identity changed/);
|
|
110
|
+
assert.deepEqual(registry.roots(), []);
|
|
111
|
+
assert.equal(entries.length, 1);
|
|
112
|
+
current = original;
|
|
113
|
+
assert.equal(registry.validate(original.root), original.root, "a rejected refresh never overwrites authority");
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test("closed and replaced sessions cannot adopt bootstrap identity", () => {
|
|
118
|
+
for (const closed of [false, true]) {
|
|
119
|
+
let id = "original";
|
|
120
|
+
let identity: { root: string; commonDir: string } | undefined;
|
|
121
|
+
let resolutions = 0;
|
|
122
|
+
const registry = new SessionWorktreeRegistry({ appendEntry() { assert.fail("inactive append"); }, events: { emit() {} } },
|
|
123
|
+
{ getSessionId: () => id, getEntries: () => [] }, "/bound", () => { resolutions++; return identity; });
|
|
124
|
+
identity = { root: "/bound", commonDir: "/bound/.git" };
|
|
125
|
+
if (closed) registry.close(); else id = "replacement";
|
|
126
|
+
assert.throws(() => registry.register("/bound", "explicit"), /inactive session/);
|
|
127
|
+
assert.deepEqual(registry.roots(), []);
|
|
128
|
+
assert.equal(resolutions, 1, "the inactive guard precedes refresh");
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
55
132
|
test("restore reads the whole session tree, resumes the same id, and ignores new/fork/clone ids", (t) => {
|
|
56
133
|
const f = fixture(t);
|
|
57
134
|
const session = SessionManager.create(f.main, join(f.dir, "sessions"));
|