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,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isKeyRelease,
|
|
3
|
+
matchesKey,
|
|
4
|
+
type KeybindingsManager,
|
|
5
|
+
wrapTextWithAnsi,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
export interface ReviewConsentAction {
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly effect: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReviewConsentContent {
|
|
14
|
+
readonly headline: string;
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
readonly value: string;
|
|
17
|
+
readonly risk: string;
|
|
18
|
+
readonly target: string;
|
|
19
|
+
readonly projection: string;
|
|
20
|
+
readonly evidence: readonly string[];
|
|
21
|
+
readonly ownership: string;
|
|
22
|
+
readonly offPathNote: string;
|
|
23
|
+
readonly offPathCommand: string;
|
|
24
|
+
readonly actions: readonly [ReviewConsentAction, ReviewConsentAction, ReviewConsentAction];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ReviewConsentComponentTheme {
|
|
28
|
+
fg(color: "accent" | "dim" | "mdHeading" | "muted", text: string): string;
|
|
29
|
+
bold(text: string): string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ReviewConsentComponentOptions {
|
|
33
|
+
readonly content: ReviewConsentContent;
|
|
34
|
+
readonly theme: ReviewConsentComponentTheme;
|
|
35
|
+
readonly keybindings: KeybindingsManager | undefined;
|
|
36
|
+
readonly terminalRows: () => number;
|
|
37
|
+
readonly onSelect: (index: number) => void;
|
|
38
|
+
readonly onCancel: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type LineKind = "body" | "header" | "action";
|
|
42
|
+
interface Line { text: string; kind: LineKind; action?: number; }
|
|
43
|
+
|
|
44
|
+
function wrap(prefix: string, text: string, width: number): string[] {
|
|
45
|
+
if (width <= prefix.length) return wrapTextWithAnsi(prefix + text, width);
|
|
46
|
+
const indent = " ".repeat(prefix.length);
|
|
47
|
+
return wrapTextWithAnsi(text, width - prefix.length).map((line, index) => index === 0 ? prefix + line : indent + line);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ReviewConsentComponent {
|
|
51
|
+
private readonly options: ReviewConsentComponentOptions;
|
|
52
|
+
private selected = 0;
|
|
53
|
+
private mode: "actions" | "details" = "actions";
|
|
54
|
+
private detailsScroll = 0;
|
|
55
|
+
private actionScroll = 0;
|
|
56
|
+
private actionLines: Line[] = [];
|
|
57
|
+
private actionRanges: Array<{ start: number; end: number }> = [];
|
|
58
|
+
private details: Line[] = [];
|
|
59
|
+
private bodyHeight = 1;
|
|
60
|
+
private contextHeight = 1;
|
|
61
|
+
private actionHeight = 1;
|
|
62
|
+
private renderedWidth: number | undefined;
|
|
63
|
+
private renderedRows: number | undefined;
|
|
64
|
+
private canConfirm = false;
|
|
65
|
+
|
|
66
|
+
constructor(options: ReviewConsentComponentOptions) {
|
|
67
|
+
this.options = options;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getActionOptions(): readonly string[] {
|
|
71
|
+
return this.options.content.actions.map((action, index) => `${index + 1}. ${action.label}\nEffect: ${action.effect}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): void {
|
|
75
|
+
if (isKeyRelease(data)) return;
|
|
76
|
+
if (this.matches(data, "tui.select.cancel")) return this.options.onCancel();
|
|
77
|
+
if (data === "d" || data === "D") {
|
|
78
|
+
this.mode = this.mode === "actions" ? "details" : "actions";
|
|
79
|
+
this.detailsScroll = 0;
|
|
80
|
+
this.canConfirm = false;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.matches(data, "tui.select.confirm")) {
|
|
84
|
+
if (this.mode === "actions" && this.canConfirm) this.options.onSelect(this.selected);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (this.mode === "actions") {
|
|
88
|
+
if (this.matches(data, "tui.select.up")) this.moveSelection(-1);
|
|
89
|
+
else if (this.matches(data, "tui.select.down")) this.moveSelection(1);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (this.matches(data, "tui.select.up") || matchesKey(data, "pageUp")) this.detailsScroll -= this.matches(data, "tui.select.up") ? 1 : this.step();
|
|
93
|
+
else if (this.matches(data, "tui.select.down") || matchesKey(data, "pageDown")) this.detailsScroll += this.matches(data, "tui.select.down") ? 1 : this.step();
|
|
94
|
+
this.canConfirm = false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(width: number): string[] {
|
|
98
|
+
const safeWidth = Math.max(1, width);
|
|
99
|
+
const rows = Math.max(1, this.options.terminalRows());
|
|
100
|
+
if (this.renderedWidth !== safeWidth || this.renderedRows !== rows) {
|
|
101
|
+
this.renderedWidth = safeWidth;
|
|
102
|
+
this.renderedRows = rows;
|
|
103
|
+
this.canConfirm = false;
|
|
104
|
+
}
|
|
105
|
+
const actions = this.buildActions(safeWidth);
|
|
106
|
+
this.actionLines = actions.lines;
|
|
107
|
+
this.actionRanges = actions.ranges;
|
|
108
|
+
this.details = this.buildDetails(safeWidth);
|
|
109
|
+
if (this.mode === "details") return this.renderDetailsScreen(safeWidth, rows);
|
|
110
|
+
if (rows < 8) return this.renderCompact(safeWidth, rows);
|
|
111
|
+
this.bodyHeight = rows;
|
|
112
|
+
return this.renderScreen(safeWidth).slice(0, rows);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
invalidate(): void {}
|
|
116
|
+
|
|
117
|
+
private renderScreen(width: number): string[] {
|
|
118
|
+
const title = wrapTextWithAnsi("Review consent", width).map((line) => this.options.theme.fg("mdHeading", this.options.theme.bold(line)));
|
|
119
|
+
const wrappedHeadline = wrapTextWithAnsi(this.options.content.headline, width);
|
|
120
|
+
const question = this.options.content.headline === "Review consent" || wrappedHeadline.length > 2 ? [] : wrappedHeadline;
|
|
121
|
+
const reason = wrapTextWithAnsi(this.options.content.reason, width);
|
|
122
|
+
const footer = wrapTextWithAnsi("↑↓ choose · Enter confirm · Esc cancel · D details", width).map((line) => this.options.theme.fg("dim", line));
|
|
123
|
+
const primary = [...title, ...question, ...this.actionLines.map((line) => this.style(line)), "", ...footer];
|
|
124
|
+
const lines = [...title, ...question, ...reason, ...this.actionLines.map((line) => this.style(line)), "", ...footer];
|
|
125
|
+
const height = Math.min(20, this.bodyHeight);
|
|
126
|
+
if (lines.length > height) {
|
|
127
|
+
if (primary.length <= height) {
|
|
128
|
+
this.canConfirm = true;
|
|
129
|
+
return primary;
|
|
130
|
+
}
|
|
131
|
+
this.canConfirm = false;
|
|
132
|
+
return [...title, ...this.safeActions(width).map((line) => this.style(line))];
|
|
133
|
+
}
|
|
134
|
+
this.canConfirm = true;
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private renderDetailsScreen(width: number, rows: number): string[] {
|
|
139
|
+
const title = wrapTextWithAnsi("Details (read-only)", width).map((line) => this.options.theme.fg("mdHeading", this.options.theme.bold(line)));
|
|
140
|
+
const footer = wrapTextWithAnsi("PgUp/PgDn · D return · Esc", width).map((line) => this.options.theme.fg("dim", line));
|
|
141
|
+
const height = rows - title.length - footer.length;
|
|
142
|
+
if (height < 1) return wrapTextWithAnsi("Details; resize; D return; Esc cancel.", width).slice(0, rows);
|
|
143
|
+
this.contextHeight = height;
|
|
144
|
+
this.detailsScroll = Math.max(0, Math.min(this.detailsScroll, Math.max(0, this.details.length - height)));
|
|
145
|
+
return [...title, ...this.renderDetails(height), ...footer];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private renderActions(width: number, height: number): string[] {
|
|
149
|
+
const range = this.actionRanges[this.selected];
|
|
150
|
+
if (!range || range.end - range.start + 1 > height) {
|
|
151
|
+
this.canConfirm = false;
|
|
152
|
+
return this.safeActions(width).slice(0, height).map((line) => this.style(line));
|
|
153
|
+
}
|
|
154
|
+
if (range.start < this.actionScroll) this.actionScroll = range.start;
|
|
155
|
+
if (range.end >= this.actionScroll + height) this.actionScroll = range.end - height + 1;
|
|
156
|
+
this.actionScroll = Math.max(0, Math.min(this.actionScroll, Math.max(0, this.actionLines.length - height)));
|
|
157
|
+
this.canConfirm = range.start >= this.actionScroll && range.end < this.actionScroll + height;
|
|
158
|
+
return this.actionLines.slice(this.actionScroll, this.actionScroll + height).map((line) => this.style(line));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private renderDetails(height: number): string[] {
|
|
162
|
+
this.detailsScroll = Math.max(0, Math.min(this.detailsScroll, Math.max(0, this.details.length - height)));
|
|
163
|
+
return this.details.slice(this.detailsScroll, this.detailsScroll + height).map((line) => this.style(line));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private renderCompact(width: number, rows: number): string[] {
|
|
167
|
+
this.canConfirm = false;
|
|
168
|
+
return this.safeActions(width).slice(0, rows).map((line) => this.style(line));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private renderTiny(width: number, rows: number): string[] {
|
|
172
|
+
this.canConfirm = false;
|
|
173
|
+
return wrapTextWithAnsi("Resize; Esc cancel; Enter off.", width).slice(0, rows);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private buildActions(width: number): { lines: Line[]; ranges: Array<{ start: number; end: number }> } {
|
|
177
|
+
const lines: Line[] = [];
|
|
178
|
+
const ranges: Array<{ start: number; end: number }> = [];
|
|
179
|
+
const add = (prefix: string, text: string, kind: LineKind = "body", action?: number) => {
|
|
180
|
+
for (const value of wrap(prefix, text, width)) lines.push({ text: value, kind, action });
|
|
181
|
+
};
|
|
182
|
+
for (const [index, action] of this.options.content.actions.entries()) {
|
|
183
|
+
const start = lines.length;
|
|
184
|
+
add(`${index === this.selected ? "→ " : " "}${index + 1}. `, action.label, "action", index);
|
|
185
|
+
ranges.push({ start, end: lines.length - 1 });
|
|
186
|
+
}
|
|
187
|
+
lines.push({ text: "", kind: "body" });
|
|
188
|
+
add("Effect: ", this.options.content.actions[this.selected].effect, "action", this.selected);
|
|
189
|
+
return { lines, ranges };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private safeActions(width: number): Line[] {
|
|
193
|
+
return [
|
|
194
|
+
...wrap("", "Resize; Esc cancel; Enter off.", width).map((text) => ({ text, kind: "body" as const })),
|
|
195
|
+
...wrap("", `Action ${this.selected + 1}/3 needs its full effect.`, width).map((text) => ({ text, kind: "action" as const, action: this.selected })),
|
|
196
|
+
...wrap("", "D opens details.", width).map((text) => ({ text, kind: "body" as const })),
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private buildDetails(width: number): Line[] {
|
|
201
|
+
const lines: Line[] = [];
|
|
202
|
+
const add = (prefix: string, text: string, kind: LineKind = "body") => {
|
|
203
|
+
for (const value of wrap(prefix, text, width)) lines.push({ text: value, kind });
|
|
204
|
+
};
|
|
205
|
+
add("Provider headline: ", this.options.content.headline);
|
|
206
|
+
add("Reason: ", this.options.content.reason);
|
|
207
|
+
add("Value: ", this.options.content.value);
|
|
208
|
+
add("Risk: ", this.options.content.risk);
|
|
209
|
+
add("Target: ", this.options.content.target);
|
|
210
|
+
add("Projection: ", this.options.content.projection);
|
|
211
|
+
add("", "Risk evidence:");
|
|
212
|
+
for (const item of this.options.content.evidence) add(" - ", item);
|
|
213
|
+
add("Ownership: ", this.options.content.ownership);
|
|
214
|
+
add("Off-path note: ", this.options.content.offPathNote);
|
|
215
|
+
add("Off-path command: ", this.options.content.offPathCommand);
|
|
216
|
+
add("", "Action consequences (read-only):", "header");
|
|
217
|
+
for (const action of this.options.content.actions) {
|
|
218
|
+
add("Action: ", action.label);
|
|
219
|
+
add("Effect: ", action.effect);
|
|
220
|
+
lines.push({ text: "", kind: "body" });
|
|
221
|
+
}
|
|
222
|
+
return lines;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private moveSelection(delta: number): void {
|
|
226
|
+
this.selected = Math.max(0, Math.min(2, this.selected + delta));
|
|
227
|
+
this.canConfirm = false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private style(line: Line): string {
|
|
231
|
+
if (line.kind === "header") return this.options.theme.fg("accent", this.options.theme.bold(line.text));
|
|
232
|
+
if (line.kind === "action" && line.action === this.selected) return this.options.theme.fg("accent", line.text);
|
|
233
|
+
return line.text;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private step(): number {
|
|
237
|
+
return Math.max(1, this.contextHeight - 1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private matches(data: string, binding: "tui.select.up" | "tui.select.down" | "tui.select.confirm" | "tui.select.cancel"): boolean {
|
|
241
|
+
if (this.options.keybindings) return this.options.keybindings.matches(data, binding);
|
|
242
|
+
const key = binding === "tui.select.up" ? "up"
|
|
243
|
+
: binding === "tui.select.down" ? "down"
|
|
244
|
+
: binding === "tui.select.confirm" ? "enter" : "escape";
|
|
245
|
+
return matchesKey(data, key);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { ReviewConsentComponent, type ReviewConsentContent } from "./review-consent-component.ts";
|
|
3
|
+
import type { ReviewConsentEnvelope, ReviewConsentV3 } from "./review-integration-v2.ts";
|
|
4
|
+
|
|
5
|
+
export const HOST_REVIEW_SESSION_PERMISSION_LABEL = "Review and allow this session";
|
|
6
|
+
|
|
7
|
+
const HOST_REVIEW_SESSION_PERMISSION_EFFECT =
|
|
8
|
+
"Reviews this change and allows later reviews in this session and repository; revoke anytime.";
|
|
9
|
+
|
|
10
|
+
const HOST_REVIEW_SESSION_PERMISSION_OWNERSHIP =
|
|
11
|
+
"The first two actions are provider-owned and apply only to this candidate. The third action is owned by the Pi host. It runs the current provider grant for this frozen candidate and lets the host use one fresh validated provider grant for each later candidate while this exact Pi session and canonical Git repository identity, including sibling worktrees, remain active. Reload preserves it; new, resume, fork, quit, process restart, or explicit revocation ends it. It grants no verdict, acknowledgement, maintenance, delivery, or cross-repository authority.";
|
|
12
|
+
|
|
13
|
+
export type ReviewConsentUiSelection =
|
|
14
|
+
| { kind: "provider"; answer: "granted" | "declined" }
|
|
15
|
+
| { kind: "host-session" };
|
|
16
|
+
|
|
17
|
+
export interface ReviewConsentUiModel {
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly options: readonly [string, string, string];
|
|
20
|
+
readonly content: ReviewConsentContent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isPiConsentV3(consent: ReviewConsentEnvelope): consent is ReviewConsentV3 {
|
|
24
|
+
return consent.schema === "gentle-ai.review-integration.consent/v3" && consent.agent === "pi";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatReviewConsentUi(consent: ReviewConsentV3): ReviewConsentUiModel {
|
|
28
|
+
const evidence = consent.riskEvidence.map((item) => `- ${item}`).join("\n");
|
|
29
|
+
const title = [
|
|
30
|
+
consent.headline,
|
|
31
|
+
"",
|
|
32
|
+
`Reason: ${consent.reason}`,
|
|
33
|
+
`Value: ${consent.value}`,
|
|
34
|
+
`Risk: ${consent.riskLevel}; ${consent.changedFiles} changed file(s), ${consent.changedLines} changed line(s).`,
|
|
35
|
+
`Target: ${consent.targetIdentity}`,
|
|
36
|
+
`Projection: ${consent.projection}`,
|
|
37
|
+
"Risk evidence:",
|
|
38
|
+
evidence,
|
|
39
|
+
"",
|
|
40
|
+
`Ownership: ${HOST_REVIEW_SESSION_PERMISSION_OWNERSHIP}`,
|
|
41
|
+
"",
|
|
42
|
+
`Off-path note: ${consent.offPath.note}`,
|
|
43
|
+
`Off-path command: ${consent.offPath.command}`,
|
|
44
|
+
].join("\n");
|
|
45
|
+
const options = [
|
|
46
|
+
`1. ${consent.choices[0].label}\nEffect: ${consent.choices[0].effect}`,
|
|
47
|
+
`2. ${consent.choices[1].label}\nEffect: ${consent.choices[1].effect}`,
|
|
48
|
+
`3. ${HOST_REVIEW_SESSION_PERMISSION_LABEL}\nEffect: ${HOST_REVIEW_SESSION_PERMISSION_EFFECT}`,
|
|
49
|
+
] as const;
|
|
50
|
+
return {
|
|
51
|
+
title,
|
|
52
|
+
options,
|
|
53
|
+
content: {
|
|
54
|
+
headline: consent.headline,
|
|
55
|
+
reason: consent.reason,
|
|
56
|
+
value: consent.value,
|
|
57
|
+
risk: `${consent.riskLevel}; ${consent.changedFiles} changed file(s), ${consent.changedLines} changed line(s).`,
|
|
58
|
+
target: consent.targetIdentity,
|
|
59
|
+
projection: consent.projection,
|
|
60
|
+
evidence: consent.riskEvidence,
|
|
61
|
+
ownership: HOST_REVIEW_SESSION_PERMISSION_OWNERSHIP,
|
|
62
|
+
offPathNote: consent.offPath.note,
|
|
63
|
+
offPathCommand: consent.offPath.command,
|
|
64
|
+
actions: [
|
|
65
|
+
{ label: consent.choices[0].label, effect: consent.choices[0].effect },
|
|
66
|
+
{ label: consent.choices[1].label, effect: consent.choices[1].effect },
|
|
67
|
+
{ label: HOST_REVIEW_SESSION_PERMISSION_LABEL, effect: HOST_REVIEW_SESSION_PERMISSION_EFFECT },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function presentReviewConsentUi(
|
|
74
|
+
context: ExtensionContext,
|
|
75
|
+
consent: ReviewConsentEnvelope,
|
|
76
|
+
): Promise<ReviewConsentUiSelection | undefined> {
|
|
77
|
+
if (!isPiConsentV3(consent)) return undefined;
|
|
78
|
+
const model = formatReviewConsentUi(consent);
|
|
79
|
+
try {
|
|
80
|
+
if (context.mode === "tui") {
|
|
81
|
+
return await context.ui.custom<ReviewConsentUiSelection | undefined>((tui, theme, keybindings, done) => {
|
|
82
|
+
const component = new ReviewConsentComponent({
|
|
83
|
+
content: model.content,
|
|
84
|
+
theme,
|
|
85
|
+
keybindings,
|
|
86
|
+
terminalRows: () => tui.terminal.rows,
|
|
87
|
+
onSelect: (index) => done(index === 0 ? { kind: "provider", answer: "granted" }
|
|
88
|
+
: index === 1 ? { kind: "provider", answer: "declined" } : { kind: "host-session" }),
|
|
89
|
+
onCancel: () => done(undefined),
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
getActionOptions: () => component.getActionOptions(),
|
|
93
|
+
render: (width) => component.render(width),
|
|
94
|
+
invalidate: () => component.invalidate(),
|
|
95
|
+
handleInput: (data) => {
|
|
96
|
+
component.handleInput(data);
|
|
97
|
+
tui.requestRender();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const selected = await context.ui.select(model.title, [...model.options]);
|
|
103
|
+
if (selected === model.options[0]) return { kind: "provider", answer: "granted" };
|
|
104
|
+
if (selected === model.options[1]) return { kind: "provider", answer: "declined" };
|
|
105
|
+
if (selected === model.options[2]) return { kind: "host-session" };
|
|
106
|
+
return undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/lib/review-host-relay.ts
CHANGED
|
@@ -140,6 +140,34 @@ export function classifyReviewHostRelayRefusal(stderr: string): "unknown-flag" |
|
|
|
140
140
|
return "other";
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// gentle-pi#638: only a relay-bound reviewer timeout is deterministic for the
|
|
145
|
+
// exact selected slot: relaunching the same materialized request reaches the
|
|
146
|
+
// same wall. Generic admission refusals, including malformed reviewer JSON and
|
|
147
|
+
// binding_mismatch [invalid_request], describe repairable submitted bytes; a
|
|
148
|
+
// fresh reviewer can change them. They keep the existing exact-reoffer path.
|
|
149
|
+
// A future provider-issued, typed slot-deterministic refusal may be added here
|
|
150
|
+
// only when its schema proves that this exact bound slot cannot be repaired.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
export const REVIEW_HOST_RELAY_UNACHIEVABLE_REASON = {
|
|
154
|
+
PI_TIMED_OUT: "relay_transport_bound_exceeded",
|
|
155
|
+
} as const;
|
|
156
|
+
|
|
157
|
+
export function reviewHostRelayUnachievableReason(error: ReviewHostRelayError): string | undefined {
|
|
158
|
+
return error.kind === REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT
|
|
159
|
+
? REVIEW_HOST_RELAY_UNACHIEVABLE_REASON.PI_TIMED_OUT
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Optional bounded evidence for the declaration's --detail. Only the killed reviewer carries measurements worth recording; an admission refusal's text already rides failure.stderr, and unmeasured failures never get a fabricated detail.
|
|
164
|
+
export function reviewHostRelayUnachievableDetail(error: ReviewHostRelayError): string | undefined {
|
|
165
|
+
if (error.kind === REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT && error.elapsedMs !== null && error.timeoutMs !== null) {
|
|
166
|
+
return `killed after ${error.elapsedMs}ms against a ${error.timeoutMs}ms relay bound`;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
// ---------------------------------------------------------------------------
|
|
144
172
|
// Slot detection — the provider decides. A collect input routes through the
|
|
145
173
|
// host relay ONLY when the provider itself issued the `--materialize` token
|