gentle-pi 2.5.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 +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -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 +21 -3
- 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/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- 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 +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- 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 +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- 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 +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -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 +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- 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 +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- 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 +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- 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-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- 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 +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -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-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- 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 +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
package/lib/agents-widget.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { CARD_TONE, cardInnerWidth, renderCard, type CardTheme, type CardTone }
|
|
|
6
6
|
// Gentle Agents widget: the card above the editor. Reads task records only
|
|
7
7
|
// (status, prompt, counters, timestamps), so drawing it costs nothing per
|
|
8
8
|
// event. One row per task: glyph, agent, task summary, then
|
|
9
|
-
// model · tokens · cost · time right-aligned.
|
|
9
|
+
// model · effort · tokens · cost · time right-aligned.
|
|
10
10
|
|
|
11
11
|
export const AGENTS_GLYPH = "❀";
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ interface Columns {
|
|
|
29
29
|
name: number;
|
|
30
30
|
task: number;
|
|
31
31
|
meta: number;
|
|
32
|
-
|
|
32
|
+
fullMetrics: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const LOOK: Record<TaskStatus, StatusLook> = {
|
|
@@ -73,6 +73,7 @@ export function formatElapsed(ms: number): string {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function clip(text: string, width: number): string {
|
|
76
|
+
if (width <= 0) return "";
|
|
76
77
|
if (visibleWidth(text) <= width) return text;
|
|
77
78
|
let out = "";
|
|
78
79
|
for (const char of text) {
|
|
@@ -139,9 +140,20 @@ function modelLabel(task: TaskRecord): string {
|
|
|
139
140
|
return id === "default" ? "" : id;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function
|
|
143
|
+
function executionLabel(task: TaskRecord, width = Infinity): string {
|
|
144
|
+
const model = modelLabel(task);
|
|
145
|
+
const effort = task.thinking ?? "";
|
|
146
|
+
if (!model) return clip(effort, width);
|
|
147
|
+
if (!effort) return clip(model, width);
|
|
148
|
+
const suffix = ` · ${effort}`;
|
|
149
|
+
if (width <= visibleWidth(suffix)) return clip(`${model} · ${effort}`, width);
|
|
150
|
+
return clip(model, width - visibleWidth(suffix)) + suffix;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function meta(task: TaskRecord, now: number, fullMetrics: boolean): string {
|
|
143
154
|
if (task.status === TASK_STATUS.QUEUED) return "queued";
|
|
144
|
-
|
|
155
|
+
if (!fullMetrics) return executionLabel(task);
|
|
156
|
+
const parts = [executionLabel(task), task.tokens > 0 ? formatTokens(task.tokens) : "", task.cost > 0 ? `$${task.cost.toFixed(2)}` : "", elapsed(task, now)];
|
|
145
157
|
return parts.filter((part) => part.length > 0).join(" · ");
|
|
146
158
|
}
|
|
147
159
|
|
|
@@ -151,23 +163,30 @@ function taskText(task: TaskRecord): string {
|
|
|
151
163
|
return task.label;
|
|
152
164
|
}
|
|
153
165
|
|
|
154
|
-
// Narrow cards give up the task
|
|
166
|
+
// Narrow cards give up the task and usage columns before execution metadata.
|
|
155
167
|
function columns(tasks: readonly TaskRecord[], inner: number, now: number): Columns {
|
|
156
|
-
const name = Math.min(NAME_MAX, Math.max(...tasks.map((task) => visibleWidth(task.agent))));
|
|
168
|
+
const name = Math.max(0, Math.min(NAME_MAX, inner - 3, Math.max(...tasks.map((task) => visibleWidth(task.agent)))));
|
|
157
169
|
const fixed = 1 + GLYPH_GAP.length + name + COLUMN_GAP.length;
|
|
158
|
-
const metaWidth = (
|
|
170
|
+
const metaWidth = (fullMetrics: boolean) => Math.max(...tasks.map((task) => visibleWidth(meta(task, now, fullMetrics))));
|
|
159
171
|
const full = metaWidth(true);
|
|
160
172
|
const task = inner - fixed - full - COLUMN_GAP.length;
|
|
161
|
-
if (task >= TASK_MIN) return { inner, name, meta: full, task,
|
|
162
|
-
return { inner, name, meta: metaWidth(false), task: 0,
|
|
173
|
+
if (task >= TASK_MIN) return { inner, name, meta: full, task, fullMetrics: true };
|
|
174
|
+
return { inner, name, meta: Math.max(0, Math.min(inner - fixed, metaWidth(false))), task: 0, fullMetrics: false };
|
|
163
175
|
}
|
|
164
176
|
|
|
165
|
-
function row(task: TaskRecord, theme: CardTheme, cols: Columns, now: number): string[] {
|
|
177
|
+
function row(task: TaskRecord, theme: CardTheme, cols: Columns, now: number, allowMetadataRow: boolean): string[] {
|
|
166
178
|
const look = LOOK[task.status];
|
|
167
179
|
const name = clip(task.agent, cols.name);
|
|
168
180
|
const head = `${theme.fg(look.role, look.glyph)}${GLYPH_GAP}${theme.fg(NAME_ROLE, name)}${" ".repeat(cols.name - visibleWidth(name))}`;
|
|
169
|
-
const
|
|
170
|
-
|
|
181
|
+
const metadata = cols.fullMetrics ? meta(task, now, true) : task.status === TASK_STATUS.QUEUED ? clip("queued", cols.meta) : executionLabel(task, cols.meta);
|
|
182
|
+
const tail = theme.fg(META_ROLE, " ".repeat(Math.max(0, cols.meta - visibleWidth(metadata))) + metadata);
|
|
183
|
+
if (cols.inner < 3) return [theme.fg(look.role, clip(look.glyph, cols.inner))];
|
|
184
|
+
// The scrollable sidebar can preserve identity and execution metadata on
|
|
185
|
+
// separate rows. The height-capped above-editor widget keeps its row budget.
|
|
186
|
+
if (allowMetadataRow && cols.task === 0 && task.status !== TASK_STATUS.QUEUED && visibleWidth(executionLabel(task)) > cols.meta) {
|
|
187
|
+
return [head, theme.fg(META_ROLE, executionLabel(task, cols.inner))];
|
|
188
|
+
}
|
|
189
|
+
if (cols.task === 0) return [`${head}${" ".repeat(Math.max(0, cols.inner - visibleWidth(head) - visibleWidth(tail)))}${tail}`];
|
|
171
190
|
const text = clip(taskText(task), cols.task);
|
|
172
191
|
return [`${head}${COLUMN_GAP}${theme.fg(TASK_ROLE, text)}${" ".repeat(cols.task - visibleWidth(text))}${COLUMN_GAP}${tail}`];
|
|
173
192
|
}
|
|
@@ -210,8 +229,8 @@ export function renderAgentsCard(tasks: readonly TaskRecord[], theme: CardTheme,
|
|
|
210
229
|
if (shown.length === 0) return [];
|
|
211
230
|
const cols = columns(shown, cardInnerWidth(width), now);
|
|
212
231
|
const { listed, hidden } = options.collapsed ? { listed: [shown[0]], hidden: 0 } : visibleRows(shown, options.maxRows);
|
|
213
|
-
const hint = options.collapsed && options.collapseKey ? `${options.collapseKey} expand` : batchElapsed(shown, now);
|
|
214
|
-
const body = listed.flatMap((task) => row(task, theme, cols, now));
|
|
232
|
+
const hint = options.collapsed && options.collapseKey ? `${options.collapseKey} expand` : shown.length > 1 ? batchElapsed(shown, now) : undefined;
|
|
233
|
+
const body = listed.flatMap((task) => row(task, theme, cols, now, options.maxRows === undefined));
|
|
215
234
|
if (hidden > 0) body.push(overflowRow(hidden, theme, options.viewKey));
|
|
216
235
|
return renderCard(
|
|
217
236
|
{ title: "Agents", subtitle: counts(shown), body, tone: tone(shown), glyph: AGENTS_GLYPH },
|
package/lib/gentle-ai-binary.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
|
|
18
18
|
export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
|
|
19
|
+
export const GENTLE_AI_INSTALL_RECOVERY_COMMAND = "node scripts/install-gentle-ai.mjs";
|
|
20
|
+
export const GENTLE_AI_INSTALL_RECOVERY_INSTRUCTIONS = `If GENTLE_PI_SKIP_GENTLE_AI_INSTALL is set, remove or unset it before changing to the installed gentle-pi package directory and running \`${GENTLE_AI_INSTALL_RECOVERY_COMMAND}\`.`;
|
|
19
21
|
// Derived from the one authoritative pinned version in
|
|
20
22
|
// scripts/gentle-ai-installer.mjs rather than repeating the literal here, so
|
|
21
23
|
// the two can never independently drift apart the way they once did.
|
|
@@ -25,7 +27,7 @@ export class PackageLocalGentleAiBinaryMissingError extends Error {
|
|
|
25
27
|
readonly code = GENTLE_AI_BINARY_MISSING_CODE;
|
|
26
28
|
constructor(path: string) {
|
|
27
29
|
super(
|
|
28
|
-
`${GENTLE_AI_BINARY_MISSING_CODE}: Gentle AI v${GENTLE_AI_VERSION} is not installed at ${path}.
|
|
30
|
+
`${GENTLE_AI_BINARY_MISSING_CODE}: Gentle AI v${GENTLE_AI_VERSION} is not installed at ${path}. ${GENTLE_AI_INSTALL_RECOVERY_INSTRUCTIONS} A missing binary can result from skipped lifecycle scripts, but does not prove install lifecycle scripts were disabled.`,
|
|
29
31
|
);
|
|
30
32
|
this.name = "PackageLocalGentleAiBinaryMissingError";
|
|
31
33
|
}
|
|
@@ -7,7 +7,9 @@ import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
|
7
7
|
// gentle_review tool draws the same card as the other Gentle notices. The
|
|
8
8
|
// call component owns the top rule; the result component closes the frame.
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export interface GentleAiRenderTheme extends CardTheme {
|
|
11
|
+
bg?(color: string, text: string): string;
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
export interface GentleAiRenderState {
|
|
13
15
|
lifecycleComponent?: boolean;
|
|
@@ -67,12 +69,12 @@ export function getGentleAiRenderState(state: unknown): GentleAiRenderState | un
|
|
|
67
69
|
// result is in, the result card closes it instead.
|
|
68
70
|
export class GentleAiCallCard {
|
|
69
71
|
private card: Card = { title: CARD_TITLE, body: [], tone: CARD_TONE.WARNING };
|
|
70
|
-
private theme:
|
|
72
|
+
private theme: GentleAiRenderTheme = passthroughTheme;
|
|
71
73
|
private detail: string | undefined;
|
|
72
74
|
private hint: string | undefined;
|
|
73
75
|
private open = true;
|
|
74
76
|
|
|
75
|
-
update(status: LifecycleStatus, operationPath: string, theme:
|
|
77
|
+
update(status: LifecycleStatus, operationPath: string, theme: GentleAiRenderTheme, detail?: string, hint?: string): void {
|
|
76
78
|
this.card = { title: CARD_TITLE, subtitle: `${status} · ${operationPath}`, body: [], tone: STATUS_TONE[status], glyph: CARD_GLYPH };
|
|
77
79
|
this.theme = theme;
|
|
78
80
|
this.detail = detail;
|
|
@@ -98,10 +100,10 @@ export class GentleAiResultCard {
|
|
|
98
100
|
private readonly text: string;
|
|
99
101
|
private readonly expanded: boolean;
|
|
100
102
|
private readonly tone: CardTone;
|
|
101
|
-
private readonly theme:
|
|
103
|
+
private readonly theme: GentleAiRenderTheme;
|
|
102
104
|
private readonly partial: boolean;
|
|
103
105
|
|
|
104
|
-
constructor(text: string, expanded: boolean, tone: CardTone, theme:
|
|
106
|
+
constructor(text: string, expanded: boolean, tone: CardTone, theme: GentleAiRenderTheme, partial = false) {
|
|
105
107
|
this.text = text;
|
|
106
108
|
this.expanded = expanded;
|
|
107
109
|
this.tone = tone;
|
|
@@ -139,7 +141,7 @@ export interface GentleAiResultRenderOptions {
|
|
|
139
141
|
export function renderGentleAiResult(
|
|
140
142
|
result: AgentToolResult<unknown>,
|
|
141
143
|
options: GentleAiResultRenderOptions,
|
|
142
|
-
theme:
|
|
144
|
+
theme: GentleAiRenderTheme = passthroughTheme,
|
|
143
145
|
context?: GentleAiRenderContext,
|
|
144
146
|
): GentleAiResultCard {
|
|
145
147
|
const textItems = result.content.flatMap((content) => (content.type === "text" ? [sanitizeTerminalText(content.text)] : []));
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -65,7 +65,11 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
65
65
|
CAPTURE_RESULT: "review/capture-result",
|
|
66
66
|
CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
|
|
67
67
|
CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
|
|
68
|
+
CAPTURE_UNACHIEVABLE: "review/capture-unachievable",
|
|
68
69
|
ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
|
|
70
|
+
SDD_STATUS: "sdd-status",
|
|
71
|
+
SDD_ATTEMPT: "sdd-attempt",
|
|
72
|
+
SDD_CONTINUE: "sdd-continue",
|
|
69
73
|
} as const;
|
|
70
74
|
export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
|
|
71
75
|
|
|
@@ -106,6 +110,8 @@ export interface NativeReviewCli {
|
|
|
106
110
|
captureResult?(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome>;
|
|
107
111
|
captureCorrectionPlan?(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1>;
|
|
108
112
|
captureProviderRole?(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureOutcome>;
|
|
113
|
+
// gentle-pi#638: records the host relay's declaration that one bound selected lens slot cannot be completed under current conditions. Younger than every binary pinned in NATIVE_CLI_CONTRACTS, so the capability is gated invocation-adjacent (see isNativeReviewUnachievableVerbRefused) instead of by a contract row.
|
|
114
|
+
captureUnachievableLens?(request: NativeReviewUnachievableLensCaptureRequest): Promise<NativeReviewUnachievableLensCaptureArtifact>;
|
|
109
115
|
// Dark until a negotiated version reports the `mode` capability true
|
|
110
116
|
// (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
|
|
111
117
|
// outside the negotiated review-integration protocol — same shape as
|
|
@@ -116,6 +122,44 @@ export interface NativeReviewCli {
|
|
|
116
122
|
// an older binary without the verb, or any other process/decode failure,
|
|
117
123
|
// rejects the returned promise -- callers fail closed to `high` risk.
|
|
118
124
|
assess?(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1>;
|
|
125
|
+
// Exact native SDD readiness authority for an explicitly selected phase.
|
|
126
|
+
// This is intentionally separate from review/RDD gates and carries no
|
|
127
|
+
// delivery or review-transaction authority.
|
|
128
|
+
sddStatus?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
|
|
129
|
+
sddContinue?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
|
|
130
|
+
sddAttemptAcquire?(request: NativeSddAcquireRequest): Promise<NativeSddAttemptResult>;
|
|
131
|
+
sddAttemptSettle?(request: NativeSddSettleRequest): Promise<NativeSddAttemptResult>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface NativeSddAttemptRequest extends Pick<NativeUntrackedSelectionRequest, "untrackedScope" | "expectedUntrackedInventory" | "intendedUntracked"> {
|
|
135
|
+
workspaceRoot: string;
|
|
136
|
+
changeName: string;
|
|
137
|
+
requestId: string;
|
|
138
|
+
remediatesEvidenceRevision?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface NativeSddAcquireRequest extends NativeSddAttemptRequest {
|
|
141
|
+
workUnit: string;
|
|
142
|
+
evidenceGoal: string;
|
|
143
|
+
maxAttempts?: number;
|
|
144
|
+
maxChangedLines?: number;
|
|
145
|
+
expectedRevision?: string;
|
|
146
|
+
token?: string;
|
|
147
|
+
}
|
|
148
|
+
export const SDD_ATTEMPT_OUTCOME = { PASSED: "passed", FAILED: "failed", INTERRUPTED: "interrupted" } as const;
|
|
149
|
+
export interface NativeSddSettleRequest extends NativeSddAttemptRequest {
|
|
150
|
+
token: string;
|
|
151
|
+
outcome: (typeof SDD_ATTEMPT_OUTCOME)[keyof typeof SDD_ATTEMPT_OUTCOME];
|
|
152
|
+
evidenceRevision?: string;
|
|
153
|
+
remediationEvidence?: string;
|
|
154
|
+
diagnosis: string;
|
|
155
|
+
harnessDisposition: "reused" | "invalidated";
|
|
156
|
+
cleanupEvidence: string;
|
|
157
|
+
processEvidence: string;
|
|
158
|
+
}
|
|
159
|
+
export interface NativeSddAttemptResult {
|
|
160
|
+
state: "proceed" | "blocked" | "complete";
|
|
161
|
+
token?: string;
|
|
162
|
+
reason?: string;
|
|
119
163
|
}
|
|
120
164
|
|
|
121
165
|
export const NATIVE_REVIEW_MODE_OPERATION = {
|
|
@@ -152,6 +196,35 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
|
152
196
|
} as const;
|
|
153
197
|
export type NativeReviewModeScope = (typeof NATIVE_REVIEW_MODE_SCOPE)[keyof typeof NATIVE_REVIEW_MODE_SCOPE];
|
|
154
198
|
|
|
199
|
+
export interface NativeSddStatusRequest {
|
|
200
|
+
changeName?: string;
|
|
201
|
+
workspaceRoot: string;
|
|
202
|
+
signal?: AbortSignal;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type NativeSddPhase = "apply" | "verify" | "archive";
|
|
206
|
+
export type NativeSddDependencyState = "blocked" | "ready" | "all_done";
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The native CLI owns this complete v2 record. The decoder validates the fields
|
|
210
|
+
* Pi relies on and returns the original object without adding, omitting, or
|
|
211
|
+
* reconciling local SDD state.
|
|
212
|
+
*/
|
|
213
|
+
export interface NativeSddStatusV2 extends Readonly<Record<string, unknown>> {
|
|
214
|
+
schemaName: "gentle-ai.sdd-status";
|
|
215
|
+
schemaVersion: 2;
|
|
216
|
+
changeName: string | null;
|
|
217
|
+
artifactStore: "openspec" | "engram" | "hybrid" | "none";
|
|
218
|
+
planningHome: Readonly<Record<string, unknown>> & { mode: "repo-local"; path: string };
|
|
219
|
+
changeRoot: string | null;
|
|
220
|
+
actionContext: Readonly<Record<string, unknown>> & { mode: "repo-local"; workspaceRoot: string; allowedEditRoots: readonly string[] };
|
|
221
|
+
dependencies: Readonly<Record<(typeof NATIVE_SDD_DEPENDENCIES)[number], NativeSddDependencyState>>;
|
|
222
|
+
phaseInstructions?: Readonly<Record<(typeof NATIVE_SDD_INSTRUCTION_PHASES)[number], readonly string[]>>;
|
|
223
|
+
blockedReasons: readonly string[];
|
|
224
|
+
nextRecommended: string;
|
|
225
|
+
remediationState?: { required: boolean; complete: boolean; failedEvidenceRevision: string };
|
|
226
|
+
}
|
|
227
|
+
|
|
155
228
|
export interface NativeReviewModeRequest {
|
|
156
229
|
cwd: string;
|
|
157
230
|
operation: NativeReviewModeOperation;
|
|
@@ -431,6 +504,49 @@ export interface NativeReviewProviderRoleCaptureArtifact {
|
|
|
431
504
|
readonly captured: true;
|
|
432
505
|
}
|
|
433
506
|
|
|
507
|
+
// gentle-pi#638: the typed declaration that one bound selected lens slot cannot be completed under current conditions. Mirrored from Go's reviewUnachievableLensCaptureArtifact (internal/cli/review_capture_unachievable.go): same schema identity, same closed field set, and the same 512-byte detail bound Go enforces, so a declaration this client refuses locally can never reach a binary that would accept it, and vice versa.
|
|
508
|
+
export const NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA = "gentle-ai.review-capture-unachievable/v1";
|
|
509
|
+
export const NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT = 512;
|
|
510
|
+
|
|
511
|
+
export interface NativeReviewUnachievableLensCaptureRequest {
|
|
512
|
+
/** Exact frozen review lineage identifier of the offered slot. */
|
|
513
|
+
readonly lineageId: string;
|
|
514
|
+
/** Exact frozen target identity of the offered slot. */
|
|
515
|
+
readonly targetIdentity: string;
|
|
516
|
+
/** Exact reviewing-authority revision the collect transition offered. */
|
|
517
|
+
readonly expectedRevision: string;
|
|
518
|
+
/** The offered slot's subject-hash; Go resolves the lens from it. */
|
|
519
|
+
readonly requestHash: string;
|
|
520
|
+
/** Short machine-readable cause, e.g. relay_transport_bound_exceeded. */
|
|
521
|
+
readonly reason: string;
|
|
522
|
+
/** Optional bounded evidence; refused above the Go-side byte limit. */
|
|
523
|
+
readonly detail?: string;
|
|
524
|
+
/** Opaque provider-issued repository context the slot carried, if any. */
|
|
525
|
+
readonly repositoryContext?: string;
|
|
526
|
+
/** Process working directory only; never rendered into the invocation. */
|
|
527
|
+
readonly cwd: string;
|
|
528
|
+
readonly signal?: AbortSignal;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export interface NativeReviewUnachievableLensCaptureArtifact {
|
|
532
|
+
readonly schema: typeof NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA;
|
|
533
|
+
readonly lineageId: string;
|
|
534
|
+
readonly targetIdentity: string;
|
|
535
|
+
readonly lens: string;
|
|
536
|
+
readonly selectedOrder: number;
|
|
537
|
+
readonly reason: string;
|
|
538
|
+
readonly recorded: true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// gentle-pi#638 fail-open capability gate: `review capture-unachievable` is younger than every released binary pinned in NATIVE_CLI_CONTRACTS, so the verb is gated invocation-adjacent instead of by a capability row. An older binary renders Go's exact `unknown review command "capture-unachievable"` refusal (internal/cli/review_facade.go) on stderr with no stdout, so the invocation rejects before any decode and the captured diagnostics are the only place that text survives. Every other failure -- a typed binding-mismatch refusal, a timeout, a decode failure -- is a real outcome the caller must surface, never a capability signal. Duck-typed on purpose: the classifier must survive a duplicated module instance exactly like the error it inspects.
|
|
542
|
+
const NATIVE_REVIEW_UNKNOWN_UNACHIEVABLE_VERB_REFUSAL = /unknown review command "capture-unachievable"/;
|
|
543
|
+
|
|
544
|
+
export function isNativeReviewUnachievableVerbRefused(error: unknown): boolean {
|
|
545
|
+
if (typeof error !== "object" || error === null) return false;
|
|
546
|
+
const stderr = (error as { diagnostics?: { stderr?: unknown } }).diagnostics?.stderr;
|
|
547
|
+
return typeof stderr === "string" && NATIVE_REVIEW_UNKNOWN_UNACHIEVABLE_VERB_REFUSAL.test(stderr);
|
|
548
|
+
}
|
|
549
|
+
|
|
434
550
|
/** Refuter and validator captures close only when they are the native last event. */
|
|
435
551
|
export type NativeReviewProviderRoleCaptureOutcome = NativeReviewProviderRoleCaptureArtifact | ReviewLastEventClosureV1;
|
|
436
552
|
|
|
@@ -843,6 +959,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
843
959
|
// negotiated START path Pi reads. The closed fields Pi consumes did not
|
|
844
960
|
// change between 2.6.0 and 2.7.0, so the columns match the 2.6.0 row.
|
|
845
961
|
"2.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
962
|
+
// Ground-truthed against the published v2.8.0 linux/amd64 binary from its
|
|
963
|
+
// signed release archive. The v2 lane still advertises capabilities/v2.5
|
|
964
|
+
// (protocol minor 5) with status/v7, consent/v3, and start/v4 schemas.
|
|
965
|
+
// The closed fields Pi consumes did not change between 2.7.0 and 2.8.0,
|
|
966
|
+
// so this row repeats 2.7.0. riskEvidence and hint remain dark because
|
|
967
|
+
// neither is proven to reach the negotiated START path Pi consumes.
|
|
968
|
+
"2.8.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
846
969
|
});
|
|
847
970
|
|
|
848
971
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -874,7 +997,7 @@ export class NativeReviewCliError extends Error {
|
|
|
874
997
|
this.launchAttempted = launchAttempted;
|
|
875
998
|
this.mutating = mutating;
|
|
876
999
|
this.mutationOutcome = launchAttempted && mutating ? "unknown" : "none";
|
|
877
|
-
this.nextAction = this.mutationOutcome === "unknown" ? "review.status" : undefined;
|
|
1000
|
+
this.nextAction = this.mutationOutcome === "unknown" && operation !== NATIVE_REVIEW_OPERATION.SDD_ATTEMPT ? "review.status" : undefined;
|
|
878
1001
|
this.diagnostics = diagnostics ?? { operation, error_code: code, timed_out: false, output_limit_exceeded: false };
|
|
879
1002
|
this.auditRecord = auditRecord;
|
|
880
1003
|
}
|
|
@@ -1299,6 +1422,46 @@ interface NativeJsonExecution {
|
|
|
1299
1422
|
process: ExecFileResult;
|
|
1300
1423
|
}
|
|
1301
1424
|
|
|
1425
|
+
const NATIVE_SDD_DEPENDENCIES = ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"] as const;
|
|
1426
|
+
const NATIVE_SDD_INSTRUCTION_PHASES = ["apply", "verify", "remediate", "archive"] as const;
|
|
1427
|
+
const NATIVE_SDD_NEXT_RECOMMENDATIONS = ["apply", "verify", "remediate", "archive", "archived", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
|
|
1428
|
+
const NATIVE_SDD_DEPENDENCY_STATES = ["blocked", "ready", "all_done"] as const;
|
|
1429
|
+
|
|
1430
|
+
/** Strictly validates the native v2 contract while preserving its whole record. */
|
|
1431
|
+
export function decodeNativeSddStatusV2(value: unknown, request: Pick<NativeSddStatusRequest, "changeName" | "workspaceRoot">): NativeSddStatusV2 {
|
|
1432
|
+
const status = object(value);
|
|
1433
|
+
if (status.schemaName !== "gentle-ai.sdd-status" || status.schemaVersion !== 2) throw new Error("wrong native SDD status schema");
|
|
1434
|
+
if ((request.changeName !== undefined && status.changeName !== request.changeName) || (status.changeName !== null && !isCanonicalProcessString(status.changeName))) throw new Error("native SDD status change identity mismatch");
|
|
1435
|
+
const artifactStore = enumString(status.artifactStore, ["openspec", "engram", "hybrid", "none"]);
|
|
1436
|
+
const planningHome = object(status.planningHome);
|
|
1437
|
+
if (planningHome.mode !== "repo-local" || !isCanonicalProcessString(planningHome.path)) throw new Error("invalid native SDD planning home");
|
|
1438
|
+
const expectedOpenSpecHome = join(request.workspaceRoot, "openspec");
|
|
1439
|
+
if (planningHome.path !== expectedOpenSpecHome && !((artifactStore === "engram" || artifactStore === "hybrid") && planningHome.path === "engram:sdd")) throw new Error("native SDD planning home escaped its workspace");
|
|
1440
|
+
if (status.changeRoot !== null && !isCanonicalProcessString(status.changeRoot)) throw new Error("invalid native SDD change root");
|
|
1441
|
+
const actionContext = object(status.actionContext);
|
|
1442
|
+
if (actionContext.mode !== "repo-local" || actionContext.workspaceRoot !== request.workspaceRoot || !isCanonicalProcessString(actionContext.workspaceRoot)) throw new Error("native SDD status workspace root mismatch");
|
|
1443
|
+
const allowedEditRoots = stringArray(actionContext.allowedEditRoots);
|
|
1444
|
+
if (!allowedEditRoots.includes(request.workspaceRoot) || allowedEditRoots.some((root) => !isAbsolute(root) || root !== join(root))) throw new Error("invalid native SDD allowed edit roots");
|
|
1445
|
+
const dependencies = object(status.dependencies);
|
|
1446
|
+
for (const phase of NATIVE_SDD_DEPENDENCIES) {
|
|
1447
|
+
if (enumString(dependencies[phase], NATIVE_SDD_DEPENDENCY_STATES) !== dependencies[phase]) throw new Error("invalid native SDD dependency");
|
|
1448
|
+
}
|
|
1449
|
+
if (Object.keys(dependencies).length !== NATIVE_SDD_DEPENDENCIES.length) throw new Error("native SDD dependencies have an unsupported shape");
|
|
1450
|
+
if (status.instructions !== undefined) throw new Error("native SDD status uses phaseInstructions, not instructions");
|
|
1451
|
+
if (status.phaseInstructions !== undefined) {
|
|
1452
|
+
const instructions = object(status.phaseInstructions);
|
|
1453
|
+
for (const phase of NATIVE_SDD_INSTRUCTION_PHASES) stringArray(instructions[phase]);
|
|
1454
|
+
if (Object.keys(instructions).length !== NATIVE_SDD_INSTRUCTION_PHASES.length) throw new Error("native SDD instructions have an unsupported shape");
|
|
1455
|
+
}
|
|
1456
|
+
if (status.nextRecommended === "remediate" || status.remediationState !== undefined) {
|
|
1457
|
+
const remediation = object(status.remediationState);
|
|
1458
|
+
if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || typeof remediation.failedEvidenceRevision !== "string" || (remediation.failedEvidenceRevision !== "" && !/^sha256:[0-9a-f]{64}$/.test(remediation.failedEvidenceRevision)) || (status.nextRecommended === "remediate" && (!remediation.required || remediation.complete || !remediation.failedEvidenceRevision))) throw new Error("Invalid native remediation state");
|
|
1459
|
+
}
|
|
1460
|
+
stringArray(status.blockedReasons);
|
|
1461
|
+
enumString(status.nextRecommended, NATIVE_SDD_NEXT_RECOMMENDATIONS);
|
|
1462
|
+
return status as NativeSddStatusV2;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1302
1465
|
class NativeReviewPlainCli {
|
|
1303
1466
|
private readonly adapter: ExecFileAdapter;
|
|
1304
1467
|
private readonly executable: string | (() => string);
|
|
@@ -1829,6 +1992,23 @@ function decodeNativeProviderRoleCaptureArtifact(value: unknown): NativeReviewPr
|
|
|
1829
1992
|
});
|
|
1830
1993
|
}
|
|
1831
1994
|
|
|
1995
|
+
// gentle-pi#638: the recorded declaration Go prints for one unachievable slot. Closed key set, exact schema identity, and the lens Go itself resolved from the request hash -- the host never asserts which lens failed, only reads back the one the provider bound.
|
|
1996
|
+
function decodeNativeUnachievableLensCaptureArtifact(value: unknown): NativeReviewUnachievableLensCaptureArtifact {
|
|
1997
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native unachievable lens capture artifact must be an object");
|
|
1998
|
+
const body = value as Record<string, unknown>;
|
|
1999
|
+
const allowed = new Set(["schema", "lineage_id", "target_identity", "lens", "selected_order", "reason", "recorded"]);
|
|
2000
|
+
for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`native unachievable lens capture artifact carries unexpected key ${key}`);
|
|
2001
|
+
const text = (key: string): string => {
|
|
2002
|
+
const found = body[key];
|
|
2003
|
+
if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native unachievable lens capture artifact ${key} must be a non-empty trimmed string`);
|
|
2004
|
+
return found;
|
|
2005
|
+
};
|
|
2006
|
+
if (text("schema") !== NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA) throw new TypeError(`native unachievable lens capture artifact schema must be ${NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA}`);
|
|
2007
|
+
if (typeof body.selected_order !== "number" || !Number.isSafeInteger(body.selected_order) || body.selected_order < 0) throw new TypeError("native unachievable lens capture artifact selected_order must be a non-negative integer");
|
|
2008
|
+
if (body.recorded !== true) throw new TypeError("native unachievable lens capture artifact must report recorded: true");
|
|
2009
|
+
return Object.freeze({ schema: NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA, lineageId: text("lineage_id"), targetIdentity: text("target_identity"), lens: text("lens"), selectedOrder: body.selected_order, reason: text("reason"), recorded: true });
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1832
2012
|
export class NativeReviewCliV216 implements NativeReviewCli {
|
|
1833
2013
|
private readonly plain: NativeReviewPlainCli;
|
|
1834
2014
|
private readonly adapter: ExecFileAdapter;
|
|
@@ -1920,6 +2100,89 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1920
2100
|
return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
|
|
1921
2101
|
}
|
|
1922
2102
|
|
|
2103
|
+
async sddAttemptAcquire(request: NativeSddAcquireRequest): Promise<NativeSddAttemptResult> {
|
|
2104
|
+
return this.sddAttempt("acquire", request);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
async sddAttemptSettle(request: NativeSddSettleRequest): Promise<NativeSddAttemptResult> {
|
|
2108
|
+
return this.sddAttempt("settle", request);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
private async sddAttempt(verb: "acquire" | "settle", request: NativeSddAcquireRequest | NativeSddSettleRequest): Promise<NativeSddAttemptResult> {
|
|
2112
|
+
const args = ["sdd-attempt", verb, "--cwd", request.workspaceRoot, "--change", request.changeName, "--request-id", request.requestId];
|
|
2113
|
+
if (!isAbsolute(request.workspaceRoot) || !isCanonicalProcessString(request.workspaceRoot) || !isCanonicalProcessString(request.changeName) || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(request.requestId)) throw new TypeError("Invalid SDD attempt identity");
|
|
2114
|
+
const text = (flag: string, value: string, max: number) => {
|
|
2115
|
+
if (!isCanonicalProcessString(value) || /[\r\n]/.test(value) || Buffer.byteLength(value) > max) throw new TypeError(`Invalid ${flag}`);
|
|
2116
|
+
args.push(flag, value);
|
|
2117
|
+
};
|
|
2118
|
+
const revision = (flag: string, value: string | undefined) => {
|
|
2119
|
+
if (value === undefined) return;
|
|
2120
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`Invalid ${flag}`);
|
|
2121
|
+
args.push(flag, value);
|
|
2122
|
+
};
|
|
2123
|
+
if (verb === "acquire") {
|
|
2124
|
+
const acquire = request as NativeSddAcquireRequest;
|
|
2125
|
+
text("--work-unit", acquire.workUnit, 160);
|
|
2126
|
+
text("--evidence-goal", acquire.evidenceGoal, 240);
|
|
2127
|
+
for (const [flag, value, max] of [["--max-attempts", acquire.maxAttempts, 100], ["--max-changed-lines", acquire.maxChangedLines, 1_000_000]] as const) {
|
|
2128
|
+
if (value === undefined) continue;
|
|
2129
|
+
if (!Number.isInteger(value) || value < 1 || value > max) throw new TypeError(`Invalid ${flag}`);
|
|
2130
|
+
args.push(flag, String(value));
|
|
2131
|
+
}
|
|
2132
|
+
if (acquire.expectedRevision === "") args.push("--expected-revision", "");
|
|
2133
|
+
else revision("--expected-revision", acquire.expectedRevision);
|
|
2134
|
+
if (acquire.token !== undefined) text("--token", acquire.token, 500);
|
|
2135
|
+
} else {
|
|
2136
|
+
const settle = request as NativeSddSettleRequest;
|
|
2137
|
+
text("--token", settle.token, 500);
|
|
2138
|
+
if (!Object.values(SDD_ATTEMPT_OUTCOME).includes(settle.outcome)) throw new TypeError("Invalid outcome");
|
|
2139
|
+
args.push("--outcome", settle.outcome);
|
|
2140
|
+
if (settle.outcome === "interrupted" ? settle.evidenceRevision !== undefined || settle.remediationEvidence !== undefined : !settle.evidenceRevision && !(settle.outcome === "passed" && settle.remediationEvidence)) throw new TypeError("Invalid terminal evidence");
|
|
2141
|
+
revision("--evidence-revision", settle.evidenceRevision);
|
|
2142
|
+
text("--diagnosis", settle.diagnosis, 500);
|
|
2143
|
+
if (!["reused", "invalidated"].includes(settle.harnessDisposition)) throw new TypeError("Invalid harness disposition");
|
|
2144
|
+
args.push("--harness-disposition", settle.harnessDisposition);
|
|
2145
|
+
text("--cleanup-evidence", settle.cleanupEvidence, 500);
|
|
2146
|
+
text("--process-evidence", settle.processEvidence, 500);
|
|
2147
|
+
if (settle.remediationEvidence !== undefined) args.push("--remediation-evidence", settle.remediationEvidence);
|
|
2148
|
+
}
|
|
2149
|
+
revision("--remediates-evidence-revision", request.remediatesEvidenceRevision);
|
|
2150
|
+
args.push(...nativeUntrackedSelectionArguments(nativeUntrackedSelection(request)));
|
|
2151
|
+
const operation = NATIVE_REVIEW_OPERATION.SDD_ATTEMPT;
|
|
2152
|
+
const { body } = await this.negotiated(operation, request.workspaceRoot, args, true);
|
|
2153
|
+
return decode(operation, true, () => {
|
|
2154
|
+
const result = body as unknown as NativeSddAttemptResult;
|
|
2155
|
+
if (!result || !["proceed", "blocked", "complete"].includes(result.state)) throw new TypeError("Invalid compact state");
|
|
2156
|
+
if (verb === "acquire" && result.state === "proceed" && !isCanonicalProcessString(result.token)) throw new TypeError("Missing compact token");
|
|
2157
|
+
if (result.reason !== undefined && typeof result.reason !== "string") throw new TypeError("Invalid compact reason");
|
|
2158
|
+
return { state: result.state, ...(result.token === undefined ? {} : { token: result.token }), ...(result.reason === undefined ? {} : { reason: result.reason }) };
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
|
|
2163
|
+
return this.sddProjection(request, false);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
async sddContinue(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
|
|
2167
|
+
if (!isCanonicalProcessString(request.changeName)) throw new TypeError("Native SDD continuation requires an exact selected change");
|
|
2168
|
+
return this.sddProjection(request, true);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
private async sddProjection(request: NativeSddStatusRequest, mutating: boolean): Promise<NativeSddStatusV2> {
|
|
2172
|
+
if ((request.changeName !== undefined && !isCanonicalProcessString(request.changeName)) || !isCanonicalProcessString(request.workspaceRoot) || !isAbsolute(request.workspaceRoot)) {
|
|
2173
|
+
throw new TypeError("Native SDD status requires a canonical change and absolute workspace root");
|
|
2174
|
+
}
|
|
2175
|
+
const operation = mutating ? NATIVE_REVIEW_OPERATION.SDD_CONTINUE : NATIVE_REVIEW_OPERATION.SDD_STATUS;
|
|
2176
|
+
const execution = await this.negotiated(
|
|
2177
|
+
operation,
|
|
2178
|
+
request.workspaceRoot,
|
|
2179
|
+
[operation, ...(request.changeName === undefined ? [] : [request.changeName]), "--cwd", request.workspaceRoot, "--json", "--instructions"],
|
|
2180
|
+
mutating,
|
|
2181
|
+
request.signal,
|
|
2182
|
+
);
|
|
2183
|
+
return decode(operation, mutating, () => decodeNativeSddStatusV2(execution.body, request));
|
|
2184
|
+
}
|
|
2185
|
+
|
|
1923
2186
|
async start(request: NativeStartRequest): Promise<NativeStartResult> {
|
|
1924
2187
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1925
2188
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
@@ -2194,6 +2457,25 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2194
2457
|
});
|
|
2195
2458
|
}
|
|
2196
2459
|
|
|
2460
|
+
// gentle-pi#638: `gentle-ai review capture-unachievable` records one bound declaration that the exact slot the collect transition offered cannot be completed under current conditions. Unlike every other capture verb the host runs, this one reports a failure instead of a result, so it never takes the provider-issued tokens verbatim: the declaration's binding values are named fields re-derived from the slot, and Go verifies them against the frozen lineage, revision, target, and subject hash before recording anything (internal/cli/review_capture_unachievable.go). A binding that no longer matches is a typed refusal the caller surfaces, never a host-side reconstruction.
|
|
2461
|
+
async captureUnachievableLens(request: NativeReviewUnachievableLensCaptureRequest): Promise<NativeReviewUnachievableLensCaptureArtifact> {
|
|
2462
|
+
for (const [field, value] of (["lineageId", "reason"] as const).map((field) => [field, request[field]] as const)) {
|
|
2463
|
+
if (!isCanonicalProcessString(value)) throw new TypeError(`Native CAPTURE_UNACHIEVABLE ${field} must be a non-empty, trimmed, NUL-free string`);
|
|
2464
|
+
}
|
|
2465
|
+
// The three identity fields are digests on every slot the provider offers, so a non-sha value means the declaration binding was parsed wrong; refuse it locally instead of invoking a doomed process.
|
|
2466
|
+
for (const [field, value] of (["targetIdentity", "expectedRevision", "requestHash"] as const).map((field) => [field, request[field]] as const)) {
|
|
2467
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`Native CAPTURE_UNACHIEVABLE ${field} must be a canonical SHA-256 identity`);
|
|
2468
|
+
}
|
|
2469
|
+
const detail = request.detail === undefined ? "" : request.detail.trim();
|
|
2470
|
+
// gentle-pi#822: the native limit is 512 UTF-8 bytes, not 512 UTF-16 code units — the Go side measures the encoded payload, so a multibyte detail needs Buffer.byteLength; a .length check would let 300 two-byte characters through.
|
|
2471
|
+
if (Buffer.byteLength(detail, "utf8") > NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT) throw new TypeError(`Native CAPTURE_UNACHIEVABLE detail exceeds ${NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT} bytes`);
|
|
2472
|
+
if (request.repositoryContext !== undefined && !isCanonicalProcessString(request.repositoryContext)) throw new TypeError("Native CAPTURE_UNACHIEVABLE repositoryContext must be a non-empty, trimmed, NUL-free string");
|
|
2473
|
+
const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_UNACHIEVABLE, true);
|
|
2474
|
+
// gentle-pi#822: --repository-context is authoritative and mutually exclusive with a path (the captureProviderRole discipline), so --cwd rides the invocation only when no repository context names it; the process working directory stays request.cwd either way.
|
|
2475
|
+
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_UNACHIEVABLE, request.cwd, ["review", "capture-unachievable", "--lineage", request.lineageId, "--target", request.targetIdentity, "--expected-revision", request.expectedRevision, "--request-hash", request.requestHash, "--reason", request.reason, ...(detail === "" ? [] : ["--detail", detail]), ...(request.repositoryContext === undefined ? ["--cwd", request.cwd] : ["--repository-context", request.repositoryContext])], true, request.signal, executable);
|
|
2476
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_UNACHIEVABLE, true, () => decodeNativeUnachievableLensCaptureArtifact(execution.body));
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2197
2479
|
// gentle-pi#311 P5: executes one provider-rendered `review.finalize`
|
|
2198
2480
|
// execute transition exactly as rendered. The tokens come verbatim from
|
|
2199
2481
|
// the negotiated next_transition (e.g. `--lineage=<id>
|