gentle-pi 3.2.1 → 3.3.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.
Files changed (71) hide show
  1. package/docs/gentle-shell.md +27 -14
  2. package/docs/readme-reference.md +41 -7
  3. package/docs/review-integration.md +22 -17
  4. package/extensions/gentle-agents.ts +85 -17
  5. package/extensions/gentle-ai.ts +123 -21
  6. package/extensions/gentle-shell.ts +379 -31
  7. package/extensions/gentle-todo.ts +19 -1
  8. package/lib/agents-view.ts +41 -14
  9. package/lib/agents-widget.ts +84 -13
  10. package/lib/command-palette-catalog.ts +1 -0
  11. package/lib/double-esc-cancel-policy.ts +138 -0
  12. package/lib/inprocess-reviewer.ts +260 -0
  13. package/lib/native-review-cli.ts +14 -0
  14. package/lib/odd-runtime-delegation-gate.ts +88 -0
  15. package/lib/review-host-relay.ts +246 -171
  16. package/lib/review-integration-v2.ts +110 -26
  17. package/lib/shell-bar.ts +150 -75
  18. package/lib/shell-card.ts +19 -9
  19. package/lib/shell-changes-view.ts +43 -5
  20. package/lib/shell-changes.ts +92 -5
  21. package/lib/shell-hover.ts +39 -0
  22. package/lib/shell-prompt.ts +10 -1
  23. package/lib/shell-sidebar-layout.ts +111 -15
  24. package/lib/shell-sidebar.ts +16 -0
  25. package/lib/shell-todo.ts +7 -1
  26. package/lib/shell-usage-view.ts +98 -10
  27. package/package.json +1 -1
  28. package/runtime/native-review-cli.mjs +14 -0
  29. package/runtime/review-integration-v2.mjs +110 -26
  30. package/scripts/gentle-ai-installer.mjs +10 -10
  31. package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
  32. package/scripts/verify-package-files.mjs +3 -3
  33. package/tests/agents-grouping.test.ts +75 -18
  34. package/tests/agents-view.test.ts +28 -18
  35. package/tests/agents-widget.test.ts +100 -12
  36. package/tests/command-palette.test.ts +1 -0
  37. package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
  38. package/tests/double-esc-cancel-policy.test.ts +194 -0
  39. package/tests/gentle-agents.test.ts +528 -5
  40. package/tests/gentle-ai-binary.test.ts +1 -1
  41. package/tests/gentle-ai-installer.test.ts +47 -47
  42. package/tests/gentle-ai.test.ts +69 -5
  43. package/tests/gentle-shell.test.ts +795 -23
  44. package/tests/gentle-todo.test.ts +17 -4
  45. package/tests/inprocess-reviewer.test.ts +368 -0
  46. package/tests/maintainer/provider-relay.maintest.ts +101 -143
  47. package/tests/native-review-capability-contract.test.ts +19 -1
  48. package/tests/odd-runtime-delegation-gate.test.ts +212 -0
  49. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  50. package/tests/package-manifest.test.ts +6 -6
  51. package/tests/review-host-relay-routing.test.ts +77 -0
  52. package/tests/review-host-relay.test.ts +277 -300
  53. package/tests/review-integration-v2-forward.test.ts +61 -0
  54. package/tests/review-integration-v2.test.ts +116 -1
  55. package/tests/review-relay-transport-agent.test.ts +22 -24
  56. package/tests/runtime-harness.mjs +11 -0
  57. package/tests/session-changes-shell.test.ts +27 -0
  58. package/tests/session-worktree-registry.test.ts +41 -0
  59. package/tests/shell-bar.test.ts +192 -124
  60. package/tests/shell-card.test.ts +5 -3
  61. package/tests/shell-changes-view.test.ts +47 -0
  62. package/tests/shell-changes.test.ts +177 -0
  63. package/tests/shell-hover.test.ts +19 -0
  64. package/tests/shell-prompt.test.ts +20 -0
  65. package/tests/shell-sidebar-fullscreen.test.ts +59 -0
  66. package/tests/shell-sidebar-layout.test.ts +243 -5
  67. package/tests/shell-sidebar.test.ts +25 -1
  68. package/tests/shell-todo.test.ts +36 -0
  69. package/tests/shell-usage-view.test.ts +120 -1
  70. package/lib/opaque-pi-reviewer-adapter.ts +0 -404
  71. package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
@@ -1,5 +1,6 @@
1
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
1
+ import { Key, matchesKey, truncateToWidth, visibleWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
2
2
  import { renderUsagePanel, type ActiveProvider, type UsageStore, type UsageTheme } from "./shell-usage.ts";
3
+ import { paintHoverable } from "./shell-hover.ts";
3
4
 
4
5
  // Gentle Shell subscriptions overlay: a framed panel over the usage store.
5
6
  // It reads the store on every render, so a refresh only needs to record.
@@ -33,10 +34,32 @@ function fit(text: string, width: number): string {
33
34
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
34
35
  }
35
36
 
37
+ // Column offset of the footer's hint text within the rendered line: the
38
+ // frame draws "│ " before the fitted content starts.
39
+ const HINT_CONTENT_OFFSET = 2;
40
+ const HINT_GAP = " ";
41
+
42
+ type HintAction = "refresh" | "close";
43
+
44
+ interface HintSpan {
45
+ start: number;
46
+ end: number;
47
+ action: HintAction;
48
+ }
49
+
50
+ interface PointerLayout {
51
+ width: number;
52
+ height: number;
53
+ row: number;
54
+ spans: HintSpan[];
55
+ }
56
+
36
57
  export class UsageView {
37
58
  private readonly store: UsageStore;
38
59
  private readonly deps: UsageViewDeps;
39
60
  private refreshing = false;
61
+ private pointer: PointerLayout | undefined;
62
+ private hoveredHint: HintAction | undefined;
40
63
 
41
64
  constructor(store: UsageStore, deps: UsageViewDeps) {
42
65
  this.store = store;
@@ -48,13 +71,25 @@ export class UsageView {
48
71
  this.deps.onClose();
49
72
  return;
50
73
  }
51
- if (data === "r" && !this.refreshing) {
52
- this.refreshing = true;
74
+ if (data === "r") this.refresh();
75
+ }
76
+
77
+ // A failing usage fetch is the store's problem to report (its rows already
78
+ // carry the last error); the panel only clears its "refreshing" state. The
79
+ // rejection must never leave this method: an unhandled rejection is fatal
80
+ // to the whole shell on current Node.
81
+ private refresh(): void {
82
+ if (this.refreshing) return;
83
+ this.refreshing = true;
84
+ this.deps.requestRender();
85
+ const settle = () => {
86
+ this.refreshing = false;
53
87
  this.deps.requestRender();
54
- void this.deps.onRefresh().finally(() => {
55
- this.refreshing = false;
56
- this.deps.requestRender();
57
- });
88
+ };
89
+ try {
90
+ this.deps.onRefresh().then(settle, settle);
91
+ } catch {
92
+ settle();
58
93
  }
59
94
  }
60
95
 
@@ -66,11 +101,64 @@ export class UsageView {
66
101
  const body = renderUsagePanel(this.store.all(), theme, inner - 2, this.deps.now(), this.deps.active()).map(
67
102
  (line) => `${theme.fg(FRAME_ROLE, "│")} ${fit(line, inner - 2)} ${theme.fg(FRAME_ROLE, "│")}`,
68
103
  );
69
- const keys = KEYS.map(([key, label]) => `${theme.fg(KEY_ROLE, key)} ${theme.fg(KEY_TEXT_ROLE, label)}`).join(" ");
104
+ const hints = KEYS.map(([key, label]) => ({ key, label, text: `${key} ${label}`, action: (key === "r" ? "refresh" : "close") as HintAction }));
105
+ // The hovered hint paints entirely in the shared hover role (key and
106
+ // label together, one color) instead of its ordinary two-role split --
107
+ // the same treatment every other clickable surface uses.
108
+ const keys = hints
109
+ .map(({ key, label, action }) =>
110
+ this.hoveredHint === action ? paintHoverable(theme, `${key} ${label}`, true) : `${theme.fg(KEY_ROLE, key)} ${theme.fg(KEY_TEXT_ROLE, label)}`,
111
+ )
112
+ .join(HINT_GAP);
70
113
  const keysLine = `${theme.fg(FRAME_ROLE, "│")} ${fit(keys, inner - 2)} ${theme.fg(FRAME_ROLE, "│")}`;
71
114
  const bottom = theme.fg(FRAME_ROLE, `╰${rule(inner)}╯`);
72
- return [top, ...body, keysLine, bottom];
115
+ const lines = [top, ...body, keysLine, bottom];
116
+ this.pointer = this.hintLayout(width, lines.length, body.length + 1, hints, inner - 2);
117
+ return lines;
73
118
  }
74
119
 
75
- invalidate(): void {}
120
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
121
+ if (event.type === "move" && event.button === "none") {
122
+ const layout = this.pointer;
123
+ const action = layout && event.width === layout.width && event.height === layout.height && event.y === layout.row
124
+ ? layout.spans.find((candidate) => event.x >= candidate.start && event.x < candidate.end)?.action
125
+ : undefined;
126
+ if (action === this.hoveredHint) return action ? { handled: true } : undefined;
127
+ this.hoveredHint = action;
128
+ return { handled: true, render: true };
129
+ }
130
+ if (event.type !== "click" || event.button !== "left") return undefined;
131
+ const layout = this.pointer;
132
+ if (!layout || event.width !== layout.width || event.height !== layout.height || event.y !== layout.row) return undefined;
133
+ const span = layout.spans.find((candidate) => event.x >= candidate.start && event.x < candidate.end);
134
+ if (!span) return undefined;
135
+ if (span.action === "close") {
136
+ this.deps.onClose();
137
+ return { handled: true, render: true };
138
+ }
139
+ this.refresh();
140
+ return { handled: true, render: true };
141
+ }
142
+
143
+ invalidate(): void {
144
+ this.pointer = undefined;
145
+ this.hoveredHint = undefined;
146
+ }
147
+
148
+ // Spans are only registered when the hints text fits without truncation:
149
+ // past that point `fit` clips it with an ellipsis and per-hint columns no
150
+ // longer line up with the plain "key label" text used here.
151
+ private hintLayout(width: number, height: number, row: number, hints: Array<{ text: string; action: HintAction }>, contentWidth: number): PointerLayout | undefined {
152
+ const plainWidth = hints.reduce((total, hint) => total + hint.text.length, 0) + HINT_GAP.length * Math.max(0, hints.length - 1);
153
+ if (plainWidth > contentWidth) return undefined;
154
+ const spans: HintSpan[] = [];
155
+ let cursor = HINT_CONTENT_OFFSET;
156
+ for (const hint of hints) {
157
+ const start = cursor;
158
+ const end = start + hint.text.length;
159
+ spans.push({ start, end, action: hint.action });
160
+ cursor = end + HINT_GAP.length;
161
+ }
162
+ return { width, height, row, spans };
163
+ }
76
164
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1005,6 +1005,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
1005
1005
  // remain dark because neither is proven to reach the negotiated START
1006
1006
  // path Pi consumes.
1007
1007
  "3.2.1": 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 }),
1008
+ // v3.4.0 (gentle-pi never pinned the intervening v3.3.0 tag, so it gets no
1009
+ // row here) added capabilities/v2.6 and status/v8-v9, and extended
1010
+ // `review assess` with review_due/review_due_reason/next_transition
1011
+ // (gentle-ai #4714 follow-up). Ground-truthed by diffing
1012
+ // contracts/review-integration/v2 and contracts/review-provider-contract
1013
+ // between the v3.2.1 and v3.4.0 tags in the gentle-ai source tree: the
1014
+ // provider contract stays byte-identical at 1.2.0, and every
1015
+ // review-integration/v2 change is an additive superset (new optional
1016
+ // schema/fields) that decodeReviewStatusV3 and the capabilities
1017
+ // negotiator already accept without touching the closed START/STATUS
1018
+ // fields this row negotiates, so it repeats 3.2.1 exactly. riskEvidence
1019
+ // and hint remain dark because neither is proven to reach the negotiated
1020
+ // START path Pi consumes.
1021
+ "3.4.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 }),
1008
1022
  });
1009
1023
 
1010
1024
 
@@ -200,6 +200,22 @@ const CAPABILITIES_SCHEMA_IDENTITIES
200
200
  requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
201
201
  optionalFeatureFloor: 14,
202
202
  }),
203
+ // Ground-truthed against the published v3.4.0 binary: capabilities/v2.6
204
+ // advertises the same required surface as v2.5 (status/v6 is still
205
+ // advertised for compatibility) plus the status/v7 and status/v8 schemas,
206
+ // which are superset-checked additions, not requirements --
207
+ // decodeReviewStatusV3 already accepts v7/v8/v9 as additive extensions of
208
+ // v6, so the required-schema floor stays unchanged. `review assess` also
209
+ // gained review_due/review_due_reason/next_transition, which is unrelated
210
+ // to this negotiated capabilities surface. The v3.4.0 binary advertised
211
+ // 15 optional features, same as v2.5's binary (floor stays at 14, its
212
+ // established minimum).
213
+ "gentle-ai.review-integration.capabilities/v2.6": Object.freeze({
214
+ protocolMinor: 6,
215
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.6", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
216
+ requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
217
+ optionalFeatureFloor: 14,
218
+ }),
203
219
  });
204
220
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
205
221
  "base_ref_workspace_overlay",
@@ -439,11 +455,21 @@ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "
439
455
 
440
456
 
441
457
 
442
- // The two Go-owned non-lens provider role capture operations (gentle-pi#311
443
- // P4-roles; provider side gentle-ai#3264). Their collect inputs are
444
- // self-contained authority-advancing vectors: binding tokens plus
445
- // `--agent=pi --execute=true`, with NO submission descriptor. The known set
446
- // is closed — an unknown role capture operation is never executed.
458
+ // The two non-lens provider role capture operations (gentle-pi#311 P4-roles;
459
+ // provider side gentle-ai#3264). An older gentle-ai still renders each one as
460
+ // a self-contained authority-advancing vector: binding tokens plus
461
+ // `--agent=pi --execute=true`, with NO submission descriptor — executing the
462
+ // exact rendered tokens makes Go materialize the role prompt, run its own
463
+ // locked-down pi subprocess, and admit the verdict itself.
464
+ //
465
+ // gentle-ai's v9 contract makes both roles host-mediated exactly like a lens
466
+ // materialize slot instead (gentle-pi#311 P3): the collect input carries
467
+ // `--materialize=true` (never alongside `--execute`) plus a provider-owned
468
+ // `submission` descriptor, and the host completes the frozen prompt
469
+ // in-process and submits the result through that exact descriptor. The two
470
+ // wire forms are mutually exclusive on one input; see the gating in
471
+ // decodeCollectInput below. The known operation set is closed — an unknown
472
+ // role capture operation is never executed either way.
447
473
  export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
448
474
  CAPTURE_REFUTER: "review.capture-refuter",
449
475
  CAPTURE_VALIDATION: "review.capture-validation",
@@ -1452,11 +1478,18 @@ function decodeCaptureSubmission(value , label , v5 , v6
1452
1478
  // one-entry values array the host relay already consumes. A payload
1453
1479
  // carrying both wire forms at once matches no captured shape and falls
1454
1480
  // through to the legacy decoder, which rejects the unknown `value` key.
1481
+ //
1482
+ // gentle-ai's v9 contract renders the same singular-value shape for the
1483
+ // two host-mediated provider role operations (capture-refuter,
1484
+ // capture-validation): they carry a `schema` key exactly like
1485
+ // capture-result, since both bind one artifact-path-or-stdin value, never
1486
+ // the correction-plan's numeric bounds (gentle-pi#311 P3).
1455
1487
  if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
1456
1488
  const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1457
- const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan"] , `${label}.operation_token`);
1489
+ const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan", "capture-refuter", "capture-validation"] , `${label}.operation_token`);
1458
1490
  const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1459
- const row = operationToken === "capture-result"
1491
+ const bindsArtifactSchema = operationToken === "capture-result" || operationToken === "capture-refuter" || operationToken === "capture-validation";
1492
+ const row = bindsArtifactSchema
1460
1493
  ? exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"])
1461
1494
  : exactRecord(submission.value, `${label}.value`, ["slot", "domain", "minimum", "maximum", "substitution_location"]);
1462
1495
  return {
@@ -1465,7 +1498,11 @@ function decodeCaptureSubmission(value , label , v5 , v6
1465
1498
  values: [{
1466
1499
  slot: operationToken === "capture-result"
1467
1500
  ? enumeration(row.slot, ["reviewer_result"] , `${label}.value.slot`)
1468
- : enumeration(row.slot, ["correction_lines"] , `${label}.value.slot`),
1501
+ : operationToken === "capture-correction-plan"
1502
+ ? enumeration(row.slot, ["correction_lines"] , `${label}.value.slot`)
1503
+ : operationToken === "capture-refuter"
1504
+ ? enumeration(row.slot, ["provider_refuter"] , `${label}.value.slot`)
1505
+ : enumeration(row.slot, ["provider_targeted_validator"] , `${label}.value.slot`),
1469
1506
  domain: nonempty(row.domain, `${label}.value.domain`),
1470
1507
  ...(row.schema === undefined ? {} : { schema: nonempty(row.schema, `${label}.value.schema`) }),
1471
1508
  ...(row.minimum === undefined ? {} : { minimum: integer(row.minimum, `${label}.value.minimum`, 1, 200) }),
@@ -1606,7 +1643,7 @@ const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required
1606
1643
  // v5 capture operations that must carry a submission descriptor.
1607
1644
  const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] );
1608
1645
 
1609
- function decodeCollectInput(value , label , v5 , v6 ) {
1646
+ function decodeCollectInput(value , label , v5 , v6 , v9 ) {
1610
1647
  const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "submission", ...(v5 ? ["provider_task", "validation_request"] : [])]);
1611
1648
  const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1612
1649
  const schema = nonempty(input.schema, `${label}.schema`);
@@ -1657,13 +1694,18 @@ function decodeCollectInput(value , label , v5 , v6
1657
1694
  sha256(argumentsList[5] .value, `${label}.arguments[5].value`);
1658
1695
  }
1659
1696
 
1660
- // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
1661
- // operations render SELF-CONTAINED authority-advancing vectors. Executing
1697
+ // gentle-pi#311 P4-roles / P3: the two non-lens provider role capture
1698
+ // operations render either wire form on one input, never both at once.
1699
+ // An older gentle-ai renders a SELF-CONTAINED authority-advancing vector
1700
+ // (binding tokens + --agent=pi --execute=true, no submission): executing
1662
1701
  // the exact rendered tokens makes Go materialize the role prompt, run its
1663
- // own locked-down pi subprocess, and admit the raw verdict — so a
1664
- // submission descriptor (the host-mediated completing form) on one of
1665
- // these inputs would hand the caller a way to author the verdict and is
1666
- // rejected as a provider contract violation.
1702
+ // own locked-down pi subprocess, and admit the raw verdict. gentle-ai's
1703
+ // v9 contract instead renders a HOST-MEDIATED form: the same binding
1704
+ // tokens plus --materialize=true (never --execute) and a provider-owned
1705
+ // submission descriptor, exactly like a lens capture-result materialize
1706
+ // slot — the host completes the frozen prompt in-process and submits
1707
+ // through that descriptor.
1708
+ const isRoleCaptureOperation = (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation);
1667
1709
  if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
1668
1710
  throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
1669
1711
  }
@@ -1694,8 +1736,30 @@ function decodeCollectInput(value , label , v5 , v6
1694
1736
  if (providerArgument("target") !== validationRequest.correctionTargetIdentity) throw new TypeError(`${label}.arguments target must bind validation_request.correction_target_identity`);
1695
1737
  if (providerArgument("request-hash") !== validationRequest.requestHash) throw new TypeError(`${label}.arguments request-hash must bind validation_request.request_hash`);
1696
1738
  }
1697
- if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation)) {
1698
- throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
1739
+ if (isRoleCaptureOperation && input.submission !== undefined) {
1740
+ // A submission descriptor only ever rides the v9 host-mediated form
1741
+ // (gentle-ai's v9 provider contract; the sibling branch ahead of the
1742
+ // currently-released v8). A v8-or-earlier provider (status/v3..v8) never
1743
+ // renders one, even if it happened to carry a submission-shaped payload,
1744
+ // so this is gated on v9 specifically — not on v5, which v8 already
1745
+ // satisfies. Within a v9 payload the discriminator is exactly the
1746
+ // materialize slot's --materialize=true with no --execute, the same one
1747
+ // a lens capture-result materialize slot already uses. Mixing a
1748
+ // submission with the old self-contained --execute=true vector, or with
1749
+ // neither flag, is a malformed vector.
1750
+ if (!v9) {
1751
+ throw new TypeError(`${label}.submission requires the v9 provider contract for the host-mediated ${captureOperation} form`);
1752
+ }
1753
+ const argumentNamed = (argumentName ) => {
1754
+ const matches = argumentsList.filter((argument) => argument.name === argumentName);
1755
+ return matches.length === 1 ? matches[0] .value : undefined;
1756
+ };
1757
+ if (argumentNamed("materialize") !== "true") {
1758
+ throw new TypeError(`${label}.submission requires --materialize=true on the host-mediated ${captureOperation} form`);
1759
+ }
1760
+ if (argumentNamed("execute") !== undefined) {
1761
+ throw new TypeError(`${label} must not carry --execute alongside a submission descriptor on ${captureOperation}`);
1762
+ }
1699
1763
  }
1700
1764
 
1701
1765
  if (captureOperation === "review.capture-result") {
@@ -1712,7 +1776,12 @@ function decodeCollectInput(value , label , v5 , v6
1712
1776
  const submissionOperations = v5
1713
1777
  ? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
1714
1778
  : ["review.capture-result"];
1715
- if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
1779
+ // Role operations are gated above by the materialize/execute discriminator
1780
+ // instead of this allowlist: an older gentle-ai's self-contained vector
1781
+ // legitimately carries no submission regardless of v5/v6, so they cannot
1782
+ // simply join the list the way every other submission-carrying operation
1783
+ // does.
1784
+ if (input.submission !== undefined && !isRoleCaptureOperation && !submissionOperations.includes(captureOperation)) {
1716
1785
  throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
1717
1786
  }
1718
1787
  if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
@@ -1750,8 +1819,14 @@ export function decodeReviewManagedAssetsContinuationV1(value , label
1750
1819
  return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
1751
1820
  }
1752
1821
 
1753
- export function decodeReviewNextTransitionV3(value , options = {}) {
1754
- const v6 = options.v6 === true;
1822
+ export function decodeReviewNextTransitionV3(value , options = {}) {
1823
+ // v9 (gentle-ai's next contract, ahead of the currently-released v8) is
1824
+ // the only rung this sub-decoder distinguishes beyond v6: v7 and v8 add
1825
+ // nothing to next_transition/collect-input decoding (v8 only extended the
1826
+ // reviewer-result transition for OpenCode provider tasks at the top
1827
+ // status level), so they decode on the exact v6 surface here.
1828
+ const v9 = options.v9 === true;
1829
+ const v6 = options.v6 === true || v9;
1755
1830
  const v5 = options.v5 === true || v6;
1756
1831
  const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation", "unachievable_lens_slots"]);
1757
1832
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
@@ -1813,7 +1888,7 @@ export function decodeReviewNextTransitionV3(value , options
1813
1888
  }
1814
1889
  if (kind === "collect") {
1815
1890
  const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
1816
- const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6), { minimum: 1 });
1891
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6, v9), { minimum: 1 });
1817
1892
  if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1818
1893
  return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1819
1894
  }
@@ -1926,16 +2001,25 @@ export function decodeReviewStatusV3(value ) {
1926
2001
  // surfaces. status/v6 adds the intended-untracked selection; status/v7
1927
2002
  // (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
1928
2003
  // adds optional `eligible_untracked_inventory` and escalation metadata,
1929
- // so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
1930
- // field.
2004
+ // so it is decoded on the v6 surface. status/v8 (gentle-ai main, PR #4765;
2005
+ // the current released contract) only extended the reviewer-result
2006
+ // transition for OpenCode provider tasks -- no new top-level key -- so it
2007
+ // decodes on the exact v7 surface too. status/v9 (the sibling gentle-ai
2008
+ // branch's contract, ahead of the released v8) adds nothing at this top
2009
+ // level either: its one addition is the host-mediated role `submission`
2010
+ // on a next_transition.collect input (gentle-pi#311 P3), gated on v9 in
2011
+ // decodeCollectInput, not here. v3 keeps rejecting every
2012
+ // v5/v6/v7/v8/v9-only field.
1931
2013
  const schema = typeof value === "object" && value !== null ? (value ).schema : undefined;
1932
- const v7 = schema === "gentle-ai.review-integration.status/v7";
2014
+ const v9 = schema === "gentle-ai.review-integration.status/v9";
2015
+ const v8 = v9 || schema === "gentle-ai.review-integration.status/v8";
2016
+ const v7 = v8 || schema === "gentle-ai.review-integration.status/v7";
1933
2017
  const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
1934
2018
  const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
1935
2019
  const body = exactRecord(value, "status", [
1936
2020
  "schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1937
2021
  ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory", "escalation"] : [])]);
1938
- requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
2022
+ requireIdentity(body, v9 ? "gentle-ai.review-integration.status/v9" : v8 ? "gentle-ai.review-integration.status/v8" : v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1939
2023
 
1940
2024
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
1941
2025
  let receipt ;
@@ -1983,7 +2067,7 @@ export function decodeReviewStatusV3(value ) {
1983
2067
  if (action === "recover" && actionDisposition === undefined) throw new TypeError("recover status requires action_disposition");
1984
2068
  if (action !== "recover" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover action");
1985
2069
  if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1986
- const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6 });
2070
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6, v9 });
1987
2071
  const validationRequest = v5 && body.validation_request !== undefined
1988
2072
  ? decodeReviewTargetedValidationRequestV1(body.validation_request, "status.validation_request")
1989
2073
  : undefined;
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
36
36
  // version check below) derives from this constant instead of repeating the
37
37
  // literal, so a pin bump cannot leave a stale copy behind. See
38
38
  // scripts/install-gentle-ai.mjs for the incident that motivated this.
39
- export const INSTALLER_VERSION = "3.2.1";
39
+ export const INSTALLER_VERSION = "3.4.0";
40
40
  export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
41
41
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
42
42
  SIGNED_RELEASE_ASSET: "signed-release-asset",
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
45
45
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
48
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.2.1`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.4.0`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit e7729359, the published v3.2.1 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0QFo0ERv8/3lgTepEG0E/P7yD9qXXCx2M7ppQviVHMo=";
50
+ // tag resolves to commit 82a6de96, the published v3.4.0 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:bQOM+Xa3WN5+idTagEk15rgEEQQWVhZvq8BPF/FHtSs=";
52
52
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
53
53
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
54
54
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
67
67
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
68
68
  // sentinel digest can never match a real SHA-256, so installation fails closed,
69
69
  // and verify-package-files.mjs refuses to pack/publish while any digest below
70
- // still holds it. The v3.2.1 digests are pinned from the published release:
70
+ // still holds it. The v3.4.0 digests are pinned from the published release:
71
71
  // archive sha256 values verified against the minisign-signed checksums.txt and
72
72
  // freshly computed hashes; binary sha256 values computed from the extracted
73
73
  // executables.
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
109
109
  }
110
110
 
111
111
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
112
- // distributing unsigned Windows builds in c4b764d0, so v3.2.1 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v3.4.0 publishes signed
113
113
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
114
114
  // exact-tag Go SumDB source-build path below; restore archive rows only when
115
115
  // upstream ships signed Windows assets.
116
116
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
117
- "darwin/amd64": asset("gentle-ai_3.2.1_darwin_amd64.tar.gz", "1696a4852435920ebae351a5173adead6da17db6762359dcb12dc3971d983c3f", "ed7d18b7446c63f265a827a736ed4465ec562cc3c07c470743d3740fb4d2f4a9", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_3.2.1_darwin_arm64.tar.gz", "e3a991a03c6fb6373f6a254b808c3fabf36330634d93704c9d9ba365467fc6f8", "2ae66cfa11f8ab93ea46c20e79bd191dedde40a4898f0b312330a0790f802ca3", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_3.2.1_linux_amd64.tar.gz", "e2e3171377c040c27a93ae66b4adf824626f23cacd398b3b652af0d87ff65807", "3464525775b60cad6358a589c298a2d4098466bc0a5ae5f0d1e578ab10134659", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_3.2.1_linux_arm64.tar.gz", "342c7fc60cf062730c8f593f09269f75485b3b1fa2907b2b188eafded1a0e222", "d2c4d8ebfdc681ceece254d0d12cd04dd7af3b435c945e42623e3475981c2f8f", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_3.4.0_darwin_amd64.tar.gz", "7d13fa45489098ff9e82dee2232204f72bf1c04ef998ceadfe1ea972697f2e56", "d9096b4bb13e56aae1251dfacd2487821a15c40b7c9397555ad615ede47e658a", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_3.4.0_darwin_arm64.tar.gz", "b9052d8dc02923663d4451a4b031c4ef7328cd866c0cb6729e8ff2b9853603f0", "0aca1239cafd87ce63b194ab69da90e762801d82a96418fd6b7b12ea3806dc07", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_3.4.0_linux_amd64.tar.gz", "c287289a514420381e890991bb3fbea4a2c36d7b4b1774fcf6bc4deb83378915", "309d9aafb48de5ef90ba0a212e8a98e8fdbb82d0852e24015e36a5ce0fe04c06", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_3.4.0_linux_arm64.tar.gz", "dfd1fe70acfe577d2a36e3a658895f481467518a2923b7200403dbcca0181399", "83ba7d27d250019115690be51664bd356e09085da8858761e362e6053c6290d3", "gentle-ai"),
121
121
  });
122
122
 
123
123
  // A pinned asset is either a signed archive or, for a prerelease pin only,