gentle-pi 2.2.0 → 2.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 (130) hide show
  1. package/README.md +51 -80
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. package/tests/git-commit-transaction.test.ts +0 -530
@@ -0,0 +1,264 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import askUserChoice from "../extensions/ask-user-choice.ts";
4
+
5
+ interface ChoiceResult {
6
+ content: Array<{ type: string; text: string }>;
7
+ details: Record<string, unknown>;
8
+ }
9
+
10
+ interface ChoiceOptionSchema {
11
+ additionalProperties?: boolean;
12
+ properties?: Record<string, unknown>;
13
+ }
14
+
15
+ interface ChoiceParameters {
16
+ additionalProperties?: boolean;
17
+ properties?: {
18
+ question?: unknown;
19
+ options?: {
20
+ minItems?: number;
21
+ maxItems?: number;
22
+ items?: ChoiceOptionSchema;
23
+ };
24
+ };
25
+ }
26
+
27
+ interface ChoiceTool {
28
+ name: string;
29
+ parameters: ChoiceParameters;
30
+ execute: (...args: unknown[]) => Promise<ChoiceResult>;
31
+ }
32
+
33
+ interface ChoiceLifecycleEvent {
34
+ channel: string;
35
+ data: { active: boolean };
36
+ }
37
+
38
+ type BeforeAgentStart = (event: unknown, ctx: { mode: string }) => void | Promise<void>;
39
+
40
+ function registerChoiceTool(
41
+ initialTools: string[] = [],
42
+ onLifecycleEvent?: (event: ChoiceLifecycleEvent) => void,
43
+ ) {
44
+ let activeTools = initialTools;
45
+ let runtimeActionsAllowed = false;
46
+ let getActiveToolsCalls = 0;
47
+ let setActiveToolsCalls = 0;
48
+ let tool: ChoiceTool | undefined;
49
+ const hooks: BeforeAgentStart[] = [];
50
+ const registeredToolNames: string[] = [];
51
+ const emittedEvents: ChoiceLifecycleEvent[] = [];
52
+ const pi = {
53
+ getActiveTools: () => {
54
+ getActiveToolsCalls++;
55
+ if (!runtimeActionsAllowed) {
56
+ throw new Error("runtime actions are unavailable while the extension is loading");
57
+ }
58
+ return activeTools;
59
+ },
60
+ setActiveTools: (names: string[]) => {
61
+ setActiveToolsCalls++;
62
+ if (!runtimeActionsAllowed) {
63
+ throw new Error("runtime actions are unavailable while the extension is loading");
64
+ }
65
+ activeTools = names;
66
+ },
67
+ registerTool: (candidate: unknown) => {
68
+ tool = candidate as ChoiceTool;
69
+ registeredToolNames.push(tool.name);
70
+ },
71
+ on: (event: string, handler: BeforeAgentStart) => {
72
+ if (event === "before_agent_start") hooks.push(handler);
73
+ },
74
+ events: {
75
+ emit(channel: string, data: { active: boolean }) {
76
+ const event = { channel, data };
77
+ emittedEvents.push(event);
78
+ onLifecycleEvent?.(event);
79
+ },
80
+ },
81
+ };
82
+ askUserChoice(pi as never);
83
+ assert.ok(tool, "ask_user_choice must register");
84
+ return {
85
+ hooks,
86
+ tool,
87
+ registeredToolNames,
88
+ activeTools: () => [...activeTools],
89
+ emittedEvents: () => [...emittedEvents],
90
+ runtimeActionCalls: () => ({ getActiveTools: getActiveToolsCalls, setActiveTools: setActiveToolsCalls }),
91
+ allowRuntimeActions: () => {
92
+ runtimeActionsAllowed = true;
93
+ },
94
+ };
95
+ }
96
+
97
+ const options = [
98
+ { label: "Authorize observed hash", description: "Accept the baseline hash observed in this runtime.", value: "authorize_observed_hash" },
99
+ { label: "Preserve requested hash", description: "Keep the hash from the original request.", value: "preserve_requested_hash" },
100
+ ];
101
+
102
+ function tuiContext(inputs: readonly string[], rendered: { value: string }) {
103
+ return {
104
+ mode: "tui",
105
+ ui: {
106
+ custom: async (factory: (tui: { requestRender(): void }, theme: { fg(_color: string, text: string): string; bold(text: string): string }, keybindings: unknown, done: (value: unknown) => void) => { render(width: number): string[]; handleInput(data: string): void }) => {
107
+ let result: unknown;
108
+ const component = factory(
109
+ { requestRender() {} },
110
+ { fg: (_color, text) => text, bold: (text) => text },
111
+ {},
112
+ (value) => {
113
+ result = value;
114
+ },
115
+ );
116
+ rendered.value = component.render(100).join("\n");
117
+ for (const input of inputs) component.handleInput(input);
118
+ return result;
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ test("ask_user_choice registers without runtime actions or overriding the open questionnaire", () => {
125
+ const registration = registerChoiceTool(["read", "ask_user_question"]);
126
+
127
+ assert.deepEqual(registration.registeredToolNames, ["ask_user_choice"]);
128
+ assert.deepEqual(registration.runtimeActionCalls(), { getActiveTools: 0, setActiveTools: 0 });
129
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
130
+ });
131
+
132
+ test("ask_user_choice exposes a strict closed single-select schema", () => {
133
+ const { tool } = registerChoiceTool();
134
+ const optionsSchema = tool.parameters.properties?.options;
135
+ const optionSchema = optionsSchema?.items;
136
+
137
+ assert.equal(tool.name, "ask_user_choice");
138
+ assert.equal(tool.parameters.additionalProperties, false);
139
+ assert.deepEqual(Object.keys(tool.parameters.properties ?? {}).sort(), ["options", "question"]);
140
+ assert.equal(optionsSchema?.minItems, 2);
141
+ assert.equal(optionsSchema?.maxItems, 4);
142
+ assert.equal(optionSchema?.additionalProperties, false);
143
+ assert.deepEqual(Object.keys(optionSchema?.properties ?? {}).sort(), ["description", "label", "value"]);
144
+ });
145
+
146
+ test("ask_user_choice handles a closed Kilo hash decision with an opaque envelope value", async () => {
147
+ const { tool } = registerChoiceTool(["read"]);
148
+ const rendered = { value: "" };
149
+ const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, tuiContext(["\x1b[B", "\r"], rendered));
150
+ assert.match(rendered.value, /Proceed\?|Authorize observed hash|Accept the baseline hash|Preserve requested hash|Keep the hash/);
151
+ assert.doesNotMatch(rendered.value, /Type something|authorize_observed_hash|preserve_requested_hash/);
152
+ assert.deepEqual(result.details.selection, { value: "preserve_requested_hash", label: "Preserve requested hash", index: 2 });
153
+ assert.equal(result.content[0]?.text, "User selected: 2. Preserve requested hash (value: preserve_requested_hash)");
154
+ });
155
+
156
+ test("ask_user_choice cancels without a value and remains unavailable outside the TUI", async () => {
157
+ const { tool } = registerChoiceTool();
158
+ const rendered = { value: "" };
159
+ const cancelled = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, tuiContext(["\x1b"], rendered));
160
+ assert.equal(cancelled.details.selection, undefined);
161
+ assert.equal(cancelled.details.cancelled, true);
162
+ await assert.rejects(
163
+ () => tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, { mode: "print" }),
164
+ /unavailable outside the interactive TUI/,
165
+ );
166
+ });
167
+
168
+ test("ask_user_choice emits a private balanced lifecycle around selection and cancellation", async () => {
169
+ const sequence: string[] = [];
170
+ const selectedRegistration = registerChoiceTool([], ({ data }) => {
171
+ sequence.push(data.active ? "active" : "inactive");
172
+ });
173
+ const selected = await selectedRegistration.tool.execute(
174
+ "call",
175
+ { question: "private choice question", options },
176
+ new AbortController().signal,
177
+ undefined,
178
+ {
179
+ mode: "tui",
180
+ ui: {
181
+ custom: async () => {
182
+ sequence.push("custom");
183
+ return { value: "preserve_requested_hash", label: "Preserve requested hash", index: 2 };
184
+ },
185
+ },
186
+ },
187
+ );
188
+ assert.equal(selected.details.selection?.value, "preserve_requested_hash");
189
+ assert.deepEqual(sequence, ["active", "custom", "inactive"]);
190
+ assert.deepEqual(selectedRegistration.emittedEvents(), [
191
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
192
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
193
+ ]);
194
+ assert.doesNotMatch(
195
+ JSON.stringify(selectedRegistration.emittedEvents()),
196
+ /private choice question|Authorize observed hash|Accept the baseline hash|preserve_requested_hash/,
197
+ );
198
+
199
+ const cancelledRegistration = registerChoiceTool();
200
+ const cancelled = await cancelledRegistration.tool.execute(
201
+ "call",
202
+ { question: "Proceed?", options },
203
+ new AbortController().signal,
204
+ undefined,
205
+ { mode: "tui", ui: { custom: async () => undefined } },
206
+ );
207
+ assert.equal(cancelled.details.cancelled, true);
208
+ assert.deepEqual(cancelledRegistration.emittedEvents(), [
209
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
210
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
211
+ ]);
212
+ });
213
+
214
+ test("ask_user_choice settles its lifecycle after a custom UI error and emits nothing outside the TUI", async () => {
215
+ const failedRegistration = registerChoiceTool();
216
+ const customError = new Error("custom UI failed");
217
+ await assert.rejects(
218
+ failedRegistration.tool.execute(
219
+ "call",
220
+ { question: "Proceed?", options },
221
+ new AbortController().signal,
222
+ undefined,
223
+ { mode: "tui", ui: { custom: async () => { throw customError; } } },
224
+ ),
225
+ (error) => error === customError,
226
+ );
227
+ assert.deepEqual(failedRegistration.emittedEvents(), [
228
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
229
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
230
+ ]);
231
+
232
+ const nonTuiRegistration = registerChoiceTool();
233
+ let customCalled = false;
234
+ await assert.rejects(
235
+ nonTuiRegistration.tool.execute(
236
+ "call",
237
+ { question: "Proceed?", options },
238
+ new AbortController().signal,
239
+ undefined,
240
+ {
241
+ mode: "print",
242
+ ui: {
243
+ custom: async () => {
244
+ customCalled = true;
245
+ return undefined;
246
+ },
247
+ },
248
+ },
249
+ ),
250
+ /unavailable outside the interactive TUI/,
251
+ );
252
+ assert.equal(customCalled, false);
253
+ assert.deepEqual(nonTuiRegistration.emittedEvents(), []);
254
+ });
255
+
256
+ test("ask_user_choice is offered only for interactive TUI turns and preserves the open questionnaire", async () => {
257
+ const registration = registerChoiceTool(["read", "ask_user_question"]);
258
+ registration.allowRuntimeActions();
259
+
260
+ for (const hook of registration.hooks) await hook({}, { mode: "tui" });
261
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_question", "ask_user_choice"]);
262
+ for (const hook of registration.hooks) await hook({}, { mode: "print" });
263
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
264
+ });
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import test from "node:test";
@@ -11,14 +11,14 @@ import codeGraphTools, {
11
11
  } from "../extensions/codegraph-tools.ts";
12
12
 
13
13
  function workspace(t: test.TestContext): string {
14
- const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-"));
14
+ const cwd = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-")));
15
15
  execFileSync("git", ["init", "-b", "main"], { cwd, stdio: "ignore" });
16
16
  t.after(() => rmSync(cwd, { recursive: true, force: true }));
17
17
  return cwd;
18
18
  }
19
19
 
20
20
  test("CodeGraph tool rejects non-project, nested-project, HOME, and temporary workspaces before init", async (t) => {
21
- const nonProject = mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-non-project-"));
21
+ const nonProject = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-non-project-")));
22
22
  t.after(() => rmSync(nonProject, { recursive: true, force: true }));
23
23
  const root = workspace(t);
24
24
  const nested = join(root, "nested");