gentle-pi 2.4.0 → 2.6.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
import { ReviewConsentComponent } from "../lib/review-consent-component.ts";
|
|
7
|
+
import { formatReviewConsentUi } from "../lib/review-consent-ui.ts";
|
|
8
|
+
import { decodeReviewConsentV3 } from "../lib/review-integration-v2.ts";
|
|
9
|
+
|
|
10
|
+
function consent() {
|
|
11
|
+
const path = join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json");
|
|
12
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
13
|
+
raw.agent = "pi";
|
|
14
|
+
return formatReviewConsentUi(decodeReviewConsentV3(raw, "pi"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createComponent(
|
|
18
|
+
rows: number,
|
|
19
|
+
selected: number[] = [],
|
|
20
|
+
cancelled: number[] = [],
|
|
21
|
+
content = consent().content,
|
|
22
|
+
) {
|
|
23
|
+
return new ReviewConsentComponent({
|
|
24
|
+
content,
|
|
25
|
+
theme: { fg: (_color, text) => text, bold: (text) => text },
|
|
26
|
+
keybindings: undefined,
|
|
27
|
+
terminalRows: () => rows,
|
|
28
|
+
onSelect: (index) => selected.push(index),
|
|
29
|
+
onCancel: () => cancelled.push(1),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function joined(lines: string[]): string {
|
|
34
|
+
return lines.join(" ").replaceAll("→ ", "").replace(/\s+/g, " ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertBounded(lines: string[], width: number, height: number): void {
|
|
38
|
+
assert.ok(lines.length <= height, `${width}x${height} must fit its terminal`);
|
|
39
|
+
for (const line of lines) assert.ok(visibleWidth(line) <= width, `${width}: ${line}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("primary consent is compact by default and opens exact details only with D", () => {
|
|
43
|
+
const base = consent().content;
|
|
44
|
+
const content = {
|
|
45
|
+
...base,
|
|
46
|
+
headline: "Review this change now?",
|
|
47
|
+
reason: "This is the exact provider reason.",
|
|
48
|
+
actions: [
|
|
49
|
+
{ label: "Review this change", effect: "Reviews only this change; delivery stays separate." },
|
|
50
|
+
{ label: "Skip this time", effect: "Skips only this change; no preference is saved." },
|
|
51
|
+
{ label: "Review and allow this session", effect: "Reviews this change and permits later session reviews." },
|
|
52
|
+
] as typeof base.actions,
|
|
53
|
+
};
|
|
54
|
+
const selected: number[] = [];
|
|
55
|
+
const component = createComponent(24, selected, [], content);
|
|
56
|
+
const primary = component.render(80);
|
|
57
|
+
assertBounded(primary, 80, 24);
|
|
58
|
+
assert.deepEqual(component.render(80), primary, "80x24 is stable without terminal-height padding");
|
|
59
|
+
assert.deepEqual(createComponent(40, [], [], content).render(80), primary, "80x40 keeps the same compact primary content");
|
|
60
|
+
assert.ok(primary.length >= 9 && primary.length <= 12, "the concise fixture keeps its provider reason within the primary row budget");
|
|
61
|
+
assert.ok(joined(primary).includes(content.headline));
|
|
62
|
+
assert.ok(joined(primary).includes(content.reason), "the exact provider reason is visible without opening details");
|
|
63
|
+
assert.ok(!joined(primary).includes("CONTEXT"));
|
|
64
|
+
assert.ok(!joined(primary).includes("Provider headline:"));
|
|
65
|
+
for (const action of content.actions) assert.ok(joined(primary).includes(action.label));
|
|
66
|
+
assert.ok(joined(primary).includes(content.actions[0].effect));
|
|
67
|
+
assert.ok(!joined(primary).includes(content.actions[1].effect));
|
|
68
|
+
assert.match(joined(primary), /Enter confirm.*Esc cancel.*D details/);
|
|
69
|
+
component.handleInput("d");
|
|
70
|
+
const details = component.render(80);
|
|
71
|
+
assert.ok(joined(details).includes("Details (read-only)"));
|
|
72
|
+
assert.ok(joined(details).includes(content.reason));
|
|
73
|
+
assert.ok(joined(details).includes("Provider headline:"));
|
|
74
|
+
component.handleInput("\r");
|
|
75
|
+
assert.deepEqual(selected, [], "details cannot confirm");
|
|
76
|
+
component.handleInput("D");
|
|
77
|
+
component.handleInput("\r");
|
|
78
|
+
assert.deepEqual(selected, [], "returning to main recomputes its visible-confirm guard");
|
|
79
|
+
component.render(80);
|
|
80
|
+
component.handleInput("\r");
|
|
81
|
+
assert.deepEqual(selected, [0]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("context and complete effects precede enabled consent, while short terminals stay safe", () => {
|
|
85
|
+
const selected: number[] = [];
|
|
86
|
+
const full = createComponent(24, selected);
|
|
87
|
+
const initial = full.render(80);
|
|
88
|
+
const content = consent().content;
|
|
89
|
+
assertBounded(initial, 80, 24);
|
|
90
|
+
for (const text of [content.headline, content.actions[0].effect]) {
|
|
91
|
+
assert.ok(joined(initial).includes(text), `initial screen preserves ${text}`);
|
|
92
|
+
}
|
|
93
|
+
assert.ok(joined(initial).includes(content.reason), "primary keeps the exact provider reason visible");
|
|
94
|
+
assert.ok(!joined(initial).includes("CONTEXT"), "primary has no permanent context block");
|
|
95
|
+
full.handleInput("\r");
|
|
96
|
+
assert.deepEqual(selected, [0], "a complete visible grant may be confirmed");
|
|
97
|
+
|
|
98
|
+
for (const width of [20, 40, 80]) {
|
|
99
|
+
const shortSelected: number[] = [];
|
|
100
|
+
const cancelled: number[] = [];
|
|
101
|
+
const short = createComponent(7, shortSelected, cancelled);
|
|
102
|
+
const view = short.render(width);
|
|
103
|
+
assertBounded(view, width, 7);
|
|
104
|
+
assert.ok(joined(view).includes("Action 1/3"), "safe view names the unavailable action without partial labels");
|
|
105
|
+
assert.ok(joined(view).includes("Resize; Esc cancel; Enter off"), "safe view explains disabled confirmation");
|
|
106
|
+
short.handleInput("\r");
|
|
107
|
+
short.handleInput("\u001b");
|
|
108
|
+
assert.deepEqual(shortSelected, [], "compact screens never hide effects then confirm");
|
|
109
|
+
assert.deepEqual(cancelled, [1], "compact screens retain cancellation");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tiny = createComponent(4);
|
|
113
|
+
const tinyView = tiny.render(20);
|
|
114
|
+
assertBounded(tinyView, 20, 4);
|
|
115
|
+
assert.ok(joined(tinyView).includes("Resize; Esc cancel; Enter off"), "tiny fallback leads with resize and cancellation guidance");
|
|
116
|
+
const oneRowCancelled: number[] = [];
|
|
117
|
+
const oneRow = createComponent(1, [], oneRowCancelled);
|
|
118
|
+
assert.ok(joined(oneRow.render(20)).includes("Resize; Esc cancel"), "one-row fallback leads with resize and cancellation guidance");
|
|
119
|
+
oneRow.handleInput("\u001b");
|
|
120
|
+
assert.deepEqual(oneRowCancelled, [1], "one-row fallback retains cancellation");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("primary preserves provider risk reasons without paraphrasing", () => {
|
|
124
|
+
const base = consent().content;
|
|
125
|
+
for (const reason of [
|
|
126
|
+
"These changes may affect execution; review can help detect problems.",
|
|
127
|
+
"Security-sensitive changes need an authoritative review decision.",
|
|
128
|
+
"Behavior updates may warrant review before delivery.",
|
|
129
|
+
]) {
|
|
130
|
+
const primary = createComponent(24, [], [], { ...base, reason }).render(80);
|
|
131
|
+
assert.ok(joined(primary).includes(reason), `80x24 shows the exact provider reason: ${reason}`);
|
|
132
|
+
assert.ok(!joined(primary).includes("Reason:"), "primary does not add a context label around the reason");
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("selection anchors fitting effects despite long context and invalidates rapid confirmation", () => {
|
|
137
|
+
const base = consent().content;
|
|
138
|
+
const content = {
|
|
139
|
+
...base,
|
|
140
|
+
headline: "Question?",
|
|
141
|
+
reason: "long provider context ".repeat(200),
|
|
142
|
+
actions: [
|
|
143
|
+
{ label: "one", effect: "one effect" },
|
|
144
|
+
{ label: "two", effect: "two effect" },
|
|
145
|
+
{ label: "three", effect: "three effect" },
|
|
146
|
+
] as typeof base.actions,
|
|
147
|
+
};
|
|
148
|
+
const anchorActions = (width: number) => {
|
|
149
|
+
const selected: number[] = [];
|
|
150
|
+
const component = createComponent(24, selected, [], content);
|
|
151
|
+
assert.ok(!joined(component.render(width)).includes("long provider context"), `${width}x24 keeps provider context in details`);
|
|
152
|
+
assert.ok(joined(component.render(width)).includes(content.actions[0].effect), `${width}x24 keeps the selected consequence visible`);
|
|
153
|
+
component.handleInput("\r");
|
|
154
|
+
assert.deepEqual(selected, [0], "a separately visible complete action may confirm without scrolling the long context");
|
|
155
|
+
for (const [input, index, effect] of [
|
|
156
|
+
["\u001b[A", 0, content.actions[0].effect],
|
|
157
|
+
["\u001b[B", 1, content.actions[1].effect],
|
|
158
|
+
["\u001b[B", 2, content.actions[2].effect],
|
|
159
|
+
] as const) {
|
|
160
|
+
component.handleInput(input);
|
|
161
|
+
const before = [...selected];
|
|
162
|
+
component.handleInput("\r");
|
|
163
|
+
assert.deepEqual(selected, before, "rapid navigation invalidates confirmation");
|
|
164
|
+
assert.ok(joined(component.render(width)).includes(effect), `choice ${index + 1} anchors at ${width} columns`);
|
|
165
|
+
component.handleInput("\r");
|
|
166
|
+
assert.equal(selected.at(-1), index);
|
|
167
|
+
}
|
|
168
|
+
return { component, selected };
|
|
169
|
+
};
|
|
170
|
+
const { component, selected } = anchorActions(80);
|
|
171
|
+
anchorActions(40);
|
|
172
|
+
component.handleInput("d");
|
|
173
|
+
assert.ok(joined(component.render(80)).includes("long provider context"), "D opens complete provider context");
|
|
174
|
+
component.handleInput("D");
|
|
175
|
+
component.handleInput("\u001b[A");
|
|
176
|
+
component.render(60);
|
|
177
|
+
assert.ok(joined(component.render(80)).includes(content.actions[1].effect), "resize back anchors the selected consequence");
|
|
178
|
+
|
|
179
|
+
component.handleInput("d");
|
|
180
|
+
const details = component.render(12);
|
|
181
|
+
assertBounded(details, 12, 24);
|
|
182
|
+
component.handleInput("\r");
|
|
183
|
+
assert.deepEqual(selected, [0, 0, 1, 2], "details never confirm a hidden action");
|
|
184
|
+
let sawOffPath = false;
|
|
185
|
+
for (let row = 0; row < 200; row += 1) {
|
|
186
|
+
sawOffPath ||= joined(component.render(12)).includes("Off-path");
|
|
187
|
+
component.handleInput("\u001b[6~");
|
|
188
|
+
}
|
|
189
|
+
assert.ok(sawOffPath, "long fixture off-path details remain reachable");
|
|
190
|
+
const risk = createComponent(40);
|
|
191
|
+
risk.render(12);
|
|
192
|
+
risk.handleInput("d");
|
|
193
|
+
let sawRiskEvidence = false;
|
|
194
|
+
for (let row = 0; row < 200; row += 1) {
|
|
195
|
+
const riskDetails = risk.render(12);
|
|
196
|
+
assertBounded(riskDetails, 12, 40);
|
|
197
|
+
sawRiskEvidence ||= joined(riskDetails).includes("Risk evidence:");
|
|
198
|
+
risk.handleInput("\u001b[B");
|
|
199
|
+
}
|
|
200
|
+
assert.ok(sawRiskEvidence, "context scrolling exposes risk-evidence headings at twelve columns");
|
|
201
|
+
const original = consent().content;
|
|
202
|
+
const shortContent = {
|
|
203
|
+
...original,
|
|
204
|
+
reason: "short provider context",
|
|
205
|
+
value: "short provider value",
|
|
206
|
+
actions: [
|
|
207
|
+
{ label: "one", effect: "one" },
|
|
208
|
+
{ label: "two", effect: "two" },
|
|
209
|
+
{ label: "three", effect: "three" },
|
|
210
|
+
] as typeof original.actions,
|
|
211
|
+
};
|
|
212
|
+
assertBounded(createComponent(40, [], [], shortContent).render(12), 12, 40);
|
|
213
|
+
|
|
214
|
+
const oneLineDetails = createComponent(4);
|
|
215
|
+
oneLineDetails.render(20);
|
|
216
|
+
oneLineDetails.handleInput("d");
|
|
217
|
+
const before = oneLineDetails.render(20);
|
|
218
|
+
oneLineDetails.handleInput("\u001b[6~");
|
|
219
|
+
assert.notDeepEqual(oneLineDetails.render(20), before, "one-line details still page forward");
|
|
220
|
+
|
|
221
|
+
const resized = createComponent(7);
|
|
222
|
+
resized.render(80);
|
|
223
|
+
resized.handleInput("\u001b[B");
|
|
224
|
+
resized.handleInput("\u001b[B");
|
|
225
|
+
const resizedView = resized.render(20);
|
|
226
|
+
assertBounded(resizedView, 20, 7);
|
|
227
|
+
assert.ok(joined(resizedView).includes("Action 3/3"), "resize keeps the selected action identifiable without partial labels");
|
|
228
|
+
assert.ok(joined(resizedView).includes("Resize; Esc cancel; Enter off"), "resize disables hidden confirmation with guidance");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("fixed title separates an enormous provider context from anchored consent actions", () => {
|
|
232
|
+
const base = consent().content;
|
|
233
|
+
const content = {
|
|
234
|
+
...base,
|
|
235
|
+
headline: "ENORMOUS PROVIDER HEADLINE: review authority for a frozen candidate needs explicit consent before any reviewer can inspect immutable evidence, with no summary substituted by Pi.",
|
|
236
|
+
reason: "The provider requires an explicit decision because the frozen candidate changes review-sensitive behavior and the user must see the complete reason before choosing.",
|
|
237
|
+
value: "A bounded review can identify candidate-caused defects while preserving ordinary repository delivery decisions and no automatic approval.",
|
|
238
|
+
evidence: [
|
|
239
|
+
"Risk evidence: the candidate changes authority-bearing review behavior.",
|
|
240
|
+
"Risk evidence: the provider recorded a scoped immutable target identity.",
|
|
241
|
+
],
|
|
242
|
+
offPathNote: "OFF PATH UNIQUE: cancellation declines this prompt only and does not change the review switch.",
|
|
243
|
+
actions: [
|
|
244
|
+
{ label: "Grant this frozen candidate review", effect: "GRANT UNIQUE: starts only this provider-bound review for the frozen candidate; it creates no delivery approval or standing permission." },
|
|
245
|
+
{ label: "Decline this frozen candidate review", effect: "DECLINE UNIQUE: declines only this candidate; no review starts and no preference is saved." },
|
|
246
|
+
{ label: "Grant one Pi-session review permission", effect: "SESSION UNIQUE: runs this exact provider grant, then permits later fresh validated candidates only for this Pi session and canonical repository identity; it grants no verdict, delivery, or cross-repository authority." },
|
|
247
|
+
] as typeof base.actions,
|
|
248
|
+
};
|
|
249
|
+
const selected: number[] = [];
|
|
250
|
+
const component = createComponent(24, selected, [], content);
|
|
251
|
+
const initial = component.render(80);
|
|
252
|
+
assertBounded(initial, 80, 24);
|
|
253
|
+
assert.equal(initial[0], "Review consent", "the visual title is app-owned and never becomes the provider headline");
|
|
254
|
+
assert.ok(!initial.includes("CONTEXT"), "provider fields are absent from the default primary");
|
|
255
|
+
assert.ok(!joined(initial).includes("Provider headline:"), "the provider headline is not a primary option");
|
|
256
|
+
assert.ok(joined(initial).includes("GRANT UNIQUE"), "the selected action's complete consequence is readable beside the context viewport");
|
|
257
|
+
component.handleInput("d");
|
|
258
|
+
for (let row = 0; row < 200; row += 1) component.handleInput("\u001b[6~");
|
|
259
|
+
const scrolled = component.render(80);
|
|
260
|
+
assertBounded(scrolled, 80, 24);
|
|
261
|
+
assert.equal(scrolled[0], "Details (read-only)", "details use their own fixed read-only title");
|
|
262
|
+
assert.ok(scrolled.includes("Details (read-only)"), "details retain a read-only heading while scrolling");
|
|
263
|
+
assert.ok(joined(scrolled).includes("OFF PATH UNIQUE"), "the complete provider context is recoverable through its viewport");
|
|
264
|
+
component.handleInput("\r");
|
|
265
|
+
assert.deepEqual(selected, [], "context details cannot confirm an action");
|
|
266
|
+
component.handleInput("D");
|
|
267
|
+
component.handleInput("\u001b[B");
|
|
268
|
+
const second = component.render(80);
|
|
269
|
+
assert.ok(joined(second).includes("DECLINE UNIQUE"), "navigation anchors each selected action's exact consequence");
|
|
270
|
+
component.handleInput("\r");
|
|
271
|
+
assert.deepEqual(selected, [1], "a rendered complete selected action remains confirmable after context recovery");
|
|
272
|
+
|
|
273
|
+
const tallComponent = createComponent(40, [], [], content);
|
|
274
|
+
tallComponent.render(80);
|
|
275
|
+
tallComponent.handleInput("\u001b[B");
|
|
276
|
+
tallComponent.handleInput("\u001b[B");
|
|
277
|
+
const tall = tallComponent.render(80);
|
|
278
|
+
assertBounded(tall, 80, 40);
|
|
279
|
+
assert.equal(tall[0], "Review consent", "80x40 keeps the same fixed brief title");
|
|
280
|
+
assert.ok(joined(tall).includes("SESSION UNIQUE"), "80x40 exposes the realistic session-permission consequence in the action area");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("fixed title uses a heading token without recoloring context or selection", () => {
|
|
284
|
+
const foregrounds: Array<{ color: string; text: string }> = [];
|
|
285
|
+
const component = new ReviewConsentComponent({
|
|
286
|
+
content: consent().content,
|
|
287
|
+
theme: {
|
|
288
|
+
fg: (color, text) => {
|
|
289
|
+
foregrounds.push({ color, text });
|
|
290
|
+
return text;
|
|
291
|
+
},
|
|
292
|
+
bold: (text) => `**${text}**`,
|
|
293
|
+
},
|
|
294
|
+
keybindings: undefined,
|
|
295
|
+
terminalRows: () => 24,
|
|
296
|
+
onSelect: () => {},
|
|
297
|
+
onCancel: () => {},
|
|
298
|
+
});
|
|
299
|
+
const lines = component.render(80);
|
|
300
|
+
assert.equal(lines[0], "**Review consent**", "fixed title retains bold styling");
|
|
301
|
+
assert.deepEqual(foregrounds.find(({ text }) => text === "**Review consent**"), {
|
|
302
|
+
color: "mdHeading",
|
|
303
|
+
text: "**Review consent**",
|
|
304
|
+
}, "fixed title uses the heading semantic token");
|
|
305
|
+
assert.ok(foregrounds.some(({ color, text }) => color === "accent" && text.startsWith("→ 1.")), "selected action remains accent styled");
|
|
306
|
+
assert.ok(!foregrounds.some(({ text }) => text.startsWith("Provider headline:")), "common body remains unstyled");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("oversized selected consequences stay complete in read-only context without enabling consent", () => {
|
|
310
|
+
const base = consent().content;
|
|
311
|
+
const effects = [
|
|
312
|
+
"OVERSIZED ONE BEGIN " + "one-context-token ".repeat(140) + "OVERSIZED_ONE_COMPLETE_END",
|
|
313
|
+
"OVERSIZED TWO BEGIN " + "two-context-token ".repeat(140) + "OVERSIZED_TWO_COMPLETE_END",
|
|
314
|
+
"OVERSIZED THREE BEGIN " + "three-context-token ".repeat(140) + "OVERSIZED_THREE_COMPLETE_END",
|
|
315
|
+
] as const;
|
|
316
|
+
const content = {
|
|
317
|
+
...base,
|
|
318
|
+
actions: [
|
|
319
|
+
{ label: "Oversized grant action", effect: effects[0] },
|
|
320
|
+
{ label: "Oversized decline action", effect: effects[1] },
|
|
321
|
+
{ label: "Oversized session action", effect: effects[2] },
|
|
322
|
+
] as typeof base.actions,
|
|
323
|
+
};
|
|
324
|
+
for (const rows of [24, 40]) {
|
|
325
|
+
const selected: number[] = [];
|
|
326
|
+
const component = createComponent(rows, selected, [], content);
|
|
327
|
+
const initial = component.render(80);
|
|
328
|
+
assertBounded(initial, 80, rows);
|
|
329
|
+
assert.ok(joined(initial).includes("Action 1/3 needs its full effect"), `${rows} rows uses safe action guidance for an oversized effect`);
|
|
330
|
+
component.handleInput("\r");
|
|
331
|
+
assert.deepEqual(selected, [], `${rows} rows never confirms a hidden oversized effect`);
|
|
332
|
+
component.handleInput("d");
|
|
333
|
+
let sawActionConsequences = false;
|
|
334
|
+
const visited = new Set<string>();
|
|
335
|
+
for (let page = 0; page < 700; page += 1) {
|
|
336
|
+
const view = component.render(80);
|
|
337
|
+
assertBounded(view, 80, rows);
|
|
338
|
+
const text = joined(view);
|
|
339
|
+
sawActionConsequences ||= text.includes("Action consequences");
|
|
340
|
+
for (const marker of ["OVERSIZED ONE BEGIN", "OVERSIZED_ONE_COMPLETE_END", "OVERSIZED TWO BEGIN", "OVERSIZED_TWO_COMPLETE_END", "OVERSIZED THREE BEGIN", "OVERSIZED_THREE_COMPLETE_END"]) {
|
|
341
|
+
if (text.includes(marker)) visited.add(marker);
|
|
342
|
+
}
|
|
343
|
+
component.handleInput("\u001b[B");
|
|
344
|
+
}
|
|
345
|
+
assert.ok(sawActionConsequences, `${rows} rows labels read-only action consequences in context`);
|
|
346
|
+
assert.deepEqual([...visited].sort(), ["OVERSIZED ONE BEGIN", "OVERSIZED_ONE_COMPLETE_END", "OVERSIZED THREE BEGIN", "OVERSIZED_THREE_COMPLETE_END", "OVERSIZED TWO BEGIN", "OVERSIZED_TWO_COMPLETE_END"].sort(), `${rows} rows exposes every oversized effect boundary through context paging`);
|
|
347
|
+
const afterDetails = component.render(80);
|
|
348
|
+
assert.ok(afterDetails.includes("Details (read-only)"), `${rows} rows retains a read-only details heading while paging`);
|
|
349
|
+
component.handleInput("\r");
|
|
350
|
+
assert.deepEqual(selected, [], `${rows} rows keeps details read-only after paging`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
@@ -67,6 +67,9 @@ test("before_agent_start injects the mirrored review execution contract for the
|
|
|
67
67
|
assert.match(result.systemPrompt, /call `gentle_review` with {"operation":"inspect"}/);
|
|
68
68
|
assert.match(result.systemPrompt, /call `gentle_review` with operation `status`, the exact retained `lineageId`, and `workspaceRoot`/);
|
|
69
69
|
assert.match(result.systemPrompt, /Use `gentle_review_capture` for one current returned slot or `gentle_review_capture_group` for the complete current reviewer group/);
|
|
70
|
+
assert.match(result.systemPrompt, /An eligible interactive Pi host may resolve `gentle-ai\.review-integration\.consent\/v3` before the envelope reaches the model/);
|
|
71
|
+
assert.match(result.systemPrompt, /If `gentle_review` returns the envelope unresolved, it is still the original provider-owned two-choice contract/);
|
|
72
|
+
assert.match(result.systemPrompt, /Never add the host action to a decoded or relayed provider envelope/);
|
|
70
73
|
assert.match(result.systemPrompt, /An approved capture awaits acknowledgement; it is not burned\. On `approved`, use bound facade STATUS to obtain or replay the exact provider-issued `acknowledge-approved` continuation, then execute it unchanged\. Only its successful returned envelope burns authority; do not issue STATUS after that burn\./);
|
|
71
74
|
let previousLifecycleIndex = result.systemPrompt.indexOf("## Gentle AI review execution contract");
|
|
72
75
|
for (const marker of [
|
|
@@ -90,6 +93,20 @@ test("before_agent_start does not inject the review execution contract for a nam
|
|
|
90
93
|
assert.doesNotMatch(result.systemPrompt, /Gentle AI review execution contract/);
|
|
91
94
|
});
|
|
92
95
|
|
|
96
|
+
test("before_agent_start does not inject the review execution contract for gentle-ai-worker", async () => {
|
|
97
|
+
const { beforeAgentStart } = harness({} as NativeReviewCli);
|
|
98
|
+
const result = await beforeAgentStart({ agentName: "gentle-ai-worker", systemPrompt: "base" }, ctx());
|
|
99
|
+
assert.equal(result.systemPrompt, "base");
|
|
100
|
+
assert.doesNotMatch(result.systemPrompt, /Gentle AI review execution contract/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("before_agent_start does not inject the review execution contract for jd-fix-agent", async () => {
|
|
104
|
+
const { beforeAgentStart } = harness({} as NativeReviewCli);
|
|
105
|
+
const result = await beforeAgentStart({ agentName: "jd-fix-agent", systemPrompt: "base" }, ctx());
|
|
106
|
+
assert.equal(result.systemPrompt, "base");
|
|
107
|
+
assert.doesNotMatch(result.systemPrompt, /Gentle AI review execution contract/);
|
|
108
|
+
});
|
|
109
|
+
|
|
93
110
|
test("before_agent_start does not inject the review execution contract for an SDD executor session", async () => {
|
|
94
111
|
const { beforeAgentStart } = harness({} as NativeReviewCli);
|
|
95
112
|
const result = await beforeAgentStart({ systemPrompt: "SDD apply executor body" }, ctx());
|
|
@@ -75,7 +75,6 @@ test("native abandon carries the exact discarded-work authorization and audit re
|
|
|
75
75
|
snapshotIdentity: SHA,
|
|
76
76
|
capturedLensResults: ["00-risk.json"],
|
|
77
77
|
findingsPresent: true,
|
|
78
|
-
evidenceRecordsPresent: false,
|
|
79
78
|
actor: "maintainer",
|
|
80
79
|
reason: "discard candidate",
|
|
81
80
|
};
|
|
@@ -85,6 +84,33 @@ test("native abandon carries the exact discarded-work authorization and audit re
|
|
|
85
84
|
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "abandon", "--cwd", "/repo", "--lineage", "abandoned", "--expected-revision", "revision-1", "--actor", "maintainer"]);
|
|
86
85
|
});
|
|
87
86
|
|
|
87
|
+
// gentle-ai 0ed9225f ("close on the last causal event") removed evidence records
|
|
88
|
+
// from CompactDiscardedWorkSummary, so the native v2 gate verifies an exact
|
|
89
|
+
// EIGHT-line binding with no evidence_records_present line. The facade-side
|
|
90
|
+
// derivation must match that contract byte-for-byte or native rejects every
|
|
91
|
+
// abandoned binding (issue #466).
|
|
92
|
+
test("nativeReviewAbandonAuthorization renders the native eight-line v2 binding", () => {
|
|
93
|
+
const binding = nativeReviewAbandonAuthorization({
|
|
94
|
+
lineage: "review-abc",
|
|
95
|
+
expectedRevision: "revision-9",
|
|
96
|
+
snapshotIdentity: "snapshot-1",
|
|
97
|
+
capturedLensResults: ["00-risk.json", "01-refuter.json"],
|
|
98
|
+
findingsPresent: true,
|
|
99
|
+
actor: "maintainer",
|
|
100
|
+
reason: "operator_disposition",
|
|
101
|
+
});
|
|
102
|
+
assert.equal(binding, [
|
|
103
|
+
"gentle-ai.review-abandon-authorization/v2",
|
|
104
|
+
"lineage=review-abc",
|
|
105
|
+
"revision=revision-9",
|
|
106
|
+
"snapshot_identity=snapshot-1",
|
|
107
|
+
"reason=operator_disposition",
|
|
108
|
+
"captured_lens_results=00-risk.json,01-refuter.json",
|
|
109
|
+
"findings_present=true",
|
|
110
|
+
"actor=maintainer",
|
|
111
|
+
].join("\n"));
|
|
112
|
+
});
|
|
113
|
+
|
|
88
114
|
test("native quarantine retains published legacy-v1 maintenance compatibility", async () => {
|
|
89
115
|
const request = {
|
|
90
116
|
cwd: "/repo",
|
|
@@ -215,7 +241,7 @@ test("native recovery operations reject malformed canonical inputs before a proc
|
|
|
215
241
|
test("partial maintenance failures preserve the provider audit record and unknown mutation outcome", async () => {
|
|
216
242
|
const request = {
|
|
217
243
|
cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
|
|
218
|
-
capturedLensResults: ["00-risk.json"], findingsPresent: true,
|
|
244
|
+
capturedLensResults: ["00-risk.json"], findingsPresent: true, actor: "maintainer", reason: "discard candidate",
|
|
219
245
|
};
|
|
220
246
|
const queue = queuedAdapter([{ exitCode: 1, stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: request.lineage, status: "partial" } }) }]);
|
|
221
247
|
await assert.rejects(
|
|
@@ -263,7 +289,6 @@ test("native abandon rejects a legacy authorization before process launch", asyn
|
|
|
263
289
|
snapshotIdentity: SHA,
|
|
264
290
|
capturedLensResults: ["00-risk.json"],
|
|
265
291
|
findingsPresent: true,
|
|
266
|
-
evidenceRecordsPresent: false,
|
|
267
292
|
actor: "maintainer",
|
|
268
293
|
reason: "discard candidate",
|
|
269
294
|
};
|
|
@@ -311,7 +336,7 @@ test("maintenance cancellation preserves the exact signal and unknown mutation o
|
|
|
311
336
|
const controller = new AbortController();
|
|
312
337
|
const request = {
|
|
313
338
|
cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
|
|
314
|
-
capturedLensResults: ["00-risk.json"], findingsPresent: true,
|
|
339
|
+
capturedLensResults: ["00-risk.json"], findingsPresent: true,
|
|
315
340
|
actor: "maintainer", reason: "discard candidate",
|
|
316
341
|
};
|
|
317
342
|
const adapter: ExecFileAdapter = async (call) => {
|