gentle-pi 4.1.0 → 4.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 (249) hide show
  1. package/README.md +352 -128
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/engram-guard +1 -1
  12. package/bin/gentle-shell.mjs +188 -35
  13. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  15. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  16. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  19. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  20. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  21. package/docs/assets/diagrams/measured-routing.svg +49 -0
  22. package/docs/assets/diagrams/native-review.svg +52 -12
  23. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  24. package/docs/assets/diagrams/permission-modes.svg +51 -0
  25. package/docs/assets/features/audio-notifications.png +0 -0
  26. package/docs/assets/features/command-palette.png +0 -0
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +40 -8
  30. package/docs/delegated-verification.md +10 -8
  31. package/docs/gentle-agents-activity.md +11 -5
  32. package/docs/gentle-shell-uninstall.md +8 -1
  33. package/docs/gentle-shell.md +20 -12
  34. package/docs/install-wizard.md +17 -10
  35. package/docs/permission-modes.md +51 -6
  36. package/docs/readme-reference.md +155 -31
  37. package/docs/session-profile-format.md +39 -0
  38. package/docs/yolo-mode.md +21 -7
  39. package/extensions/ask-user-choice.ts +4 -0
  40. package/extensions/ask-user-question.ts +6 -1
  41. package/extensions/codegraph-tools.ts +68 -6
  42. package/extensions/gentle-agents.ts +146 -28
  43. package/extensions/gentle-ai.ts +380 -68
  44. package/extensions/gentle-herdr-activity.ts +2 -3
  45. package/extensions/gentle-shell.ts +53 -43
  46. package/extensions/gentle-todo.ts +62 -18
  47. package/extensions/quiet-tools.ts +1 -1
  48. package/extensions/session-scratch.ts +365 -0
  49. package/extensions/startup-banner.ts +2 -3
  50. package/lib/agent-home.ts +8 -3
  51. package/lib/agents-protocol.ts +3 -0
  52. package/lib/agents-runner.ts +55 -6
  53. package/lib/agents-view.ts +3 -1
  54. package/lib/auto-permission-answers.ts +133 -0
  55. package/lib/auto-permission-classifier.ts +215 -36
  56. package/lib/auto-permission-evidence.ts +88 -0
  57. package/lib/auto-permission-memory.ts +69 -0
  58. package/lib/auto-permission-runtime.ts +251 -35
  59. package/lib/auto-permission-tool-context.ts +61 -0
  60. package/lib/bounded-writer-admission.ts +51 -0
  61. package/lib/command-palette-catalog.ts +0 -4
  62. package/lib/commit-slice-signal.ts +100 -0
  63. package/lib/context-backstop.ts +84 -0
  64. package/lib/delivered-test-lint.ts +80 -0
  65. package/lib/destructive-command-guard.ts +215 -28
  66. package/lib/gauge-theme.ts +146 -0
  67. package/lib/gentle-ai-binary.ts +28 -7
  68. package/lib/gentle-shell-bundled.ts +62 -8
  69. package/lib/gentle-shell-import-pi.ts +233 -0
  70. package/lib/gentle-shell-launcher.ts +146 -12
  71. package/lib/gentle-shell-uninstall.ts +18 -1
  72. package/lib/herdr-activity.ts +24 -0
  73. package/lib/model-routing-authority.ts +6 -1
  74. package/lib/native-review-cli.ts +9 -1
  75. package/lib/orchestrator-discovery.ts +5 -1
  76. package/lib/orchestrator-presence.ts +9 -0
  77. package/lib/orchestrator-scope.ts +21 -3
  78. package/lib/provider-contract-bundle.ts +99 -53
  79. package/lib/review-candidate-view-owner.ts +29 -19
  80. package/lib/review-candidate-view.ts +36 -2
  81. package/lib/review-risk-assessment.ts +43 -38
  82. package/lib/review-session-standing-permission.ts +3 -1
  83. package/lib/selection-engine.ts +4 -1
  84. package/lib/session-profile-freeze.ts +40 -16
  85. package/lib/session-profile-persistence.ts +187 -0
  86. package/lib/session-scratch.ts +474 -0
  87. package/lib/shell-bar.ts +25 -25
  88. package/lib/shell-changes-view.ts +80 -13
  89. package/lib/shell-changes.ts +2 -0
  90. package/lib/shell-gauge.ts +49 -6
  91. package/lib/shell-prompt.ts +6 -4
  92. package/lib/shell-todo.ts +39 -8
  93. package/lib/shell-usage-view.ts +5 -1
  94. package/lib/shell-usage.ts +7 -7
  95. package/lib/verifier-admission.ts +29 -0
  96. package/lib/vim-editor-adapter.ts +22 -4
  97. package/lib/yolo-temp-scratch.ts +69 -0
  98. package/package.json +5 -6
  99. package/runtime/gentle-ai-binary.mjs +28 -7
  100. package/runtime/gentle-shell-bundled.mjs +61 -7
  101. package/runtime/gentle-shell-import-pi.mjs +234 -0
  102. package/runtime/gentle-shell-launcher.mjs +144 -10
  103. package/runtime/gentle-shell-uninstall.mjs +18 -1
  104. package/runtime/native-review-cli.mjs +8 -0
  105. package/runtime/review-risk-assessment.mjs +43 -38
  106. package/scripts/build-runtime-modules.mjs +1 -0
  107. package/scripts/bundled-engram-migration.mjs +125 -0
  108. package/scripts/gentle-ai-installer.mjs +10 -10
  109. package/scripts/install-wizard-preview.mjs +2 -2
  110. package/scripts/installer-downloads.mjs +7 -7
  111. package/scripts/installer-preflight.mjs +1 -1
  112. package/scripts/installer-server.mjs +14 -4
  113. package/scripts/main-channel.mjs +3 -2
  114. package/scripts/pi-update.mjs +223 -0
  115. package/scripts/run-test-suite.mjs +5 -0
  116. package/scripts/verify-package-files.mjs +19 -13
  117. package/skills/gentle-ai/SKILL.md +3 -3
  118. package/skills/work-unit-commits/SKILL.md +5 -3
  119. package/tests/agent-risk-escalation.test.ts +37 -10
  120. package/tests/agents-runner.test.ts +260 -3
  121. package/tests/append-system-prompt-route.test.ts +18 -1
  122. package/tests/auto-mode-human-answers.test.ts +68 -0
  123. package/tests/auto-mode-invariants.test.ts +23 -5
  124. package/tests/auto-mode-sdk.test.ts +29 -2
  125. package/tests/auto-permission-answers.test.ts +170 -0
  126. package/tests/auto-permission-classifier.test.ts +353 -2
  127. package/tests/auto-permission-compound.test.ts +24 -0
  128. package/tests/auto-permission-evidence.test.ts +88 -0
  129. package/tests/auto-permission-memory.test.ts +46 -0
  130. package/tests/auto-permission-public-regressions.test.ts +111 -0
  131. package/tests/auto-permission-runtime.test.ts +270 -12
  132. package/tests/autonomous-guard.test.ts +208 -16
  133. package/tests/bounded-writer-admission.test.ts +11 -3
  134. package/tests/bundled-install.test.ts +3 -3
  135. package/tests/card-content-policy.test.ts +19 -9
  136. package/tests/child-safety.test.ts +39 -0
  137. package/tests/codegraph-tools.test.ts +123 -2
  138. package/tests/command-palette.test.ts +14 -4
  139. package/tests/commit-slice-signal.test.ts +162 -0
  140. package/tests/context-backstop.test.ts +159 -0
  141. package/tests/delegate-for-reason-contract.test.ts +91 -24
  142. package/tests/delivered-test-lint.test.ts +105 -0
  143. package/tests/destructive-command-guard.test.ts +85 -1
  144. package/tests/engram-safety.test.ts +35 -7
  145. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  148. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  149. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  150. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  151. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  152. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  153. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  154. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  155. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  156. package/tests/gauge-theme.test.ts +120 -0
  157. package/tests/gentle-agents.test.ts +443 -8
  158. package/tests/gentle-ai-binary.test.ts +1 -1
  159. package/tests/gentle-ai-child-guards.test.ts +61 -0
  160. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  161. package/tests/gentle-ai-installer.test.ts +51 -51
  162. package/tests/gentle-shell-bin.test.ts +180 -17
  163. package/tests/gentle-shell-bundled.test.ts +235 -38
  164. package/tests/gentle-shell-import-pi.test.ts +262 -0
  165. package/tests/gentle-shell-launcher.test.ts +97 -13
  166. package/tests/gentle-shell-uninstall.test.ts +30 -0
  167. package/tests/gentle-shell.test.ts +113 -14
  168. package/tests/gentle-todo.test.ts +258 -27
  169. package/tests/headless-delivery-question.test.ts +58 -0
  170. package/tests/herdr-activity.test.ts +34 -1
  171. package/tests/install-wizard.test.ts +2 -2
  172. package/tests/installer-posix-bootstrap.test.ts +8 -8
  173. package/tests/installer-preflight.test.ts +3 -3
  174. package/tests/installer-probes.test.ts +8 -8
  175. package/tests/installer-runner.test.ts +14 -13
  176. package/tests/installer-server.test.ts +13 -5
  177. package/tests/installer-windows-bootstrap.test.ts +33 -4
  178. package/tests/main-channel-upgrade.test.ts +7 -1
  179. package/tests/native-review-capability-contract.test.ts +7 -1
  180. package/tests/native-review-parity.test.ts +22 -1
  181. package/tests/no-self-pause-contract.test.ts +5 -2
  182. package/tests/odd-integration.test.ts +5 -1
  183. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  184. package/tests/odd-routing-contract.test.ts +115 -30
  185. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  186. package/tests/orchestrator-budget.test.ts +19 -8
  187. package/tests/orchestrator-discovery.test.ts +29 -0
  188. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  189. package/tests/orchestrator-scope.test.ts +66 -0
  190. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  191. package/tests/package-manifest.test.ts +28 -24
  192. package/tests/pi-update-launcher.test.ts +106 -0
  193. package/tests/pi-update.test.ts +186 -0
  194. package/tests/primary-session-prompt.test.ts +101 -0
  195. package/tests/provider-contract-bundle.test.ts +118 -0
  196. package/tests/provider-contract-mirror.test.ts +24 -0
  197. package/tests/quiet-tool-rendering.test.ts +23 -0
  198. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  199. package/tests/rdd-status-line.test.ts +131 -0
  200. package/tests/review-agent-end-preflight.test.ts +6 -0
  201. package/tests/review-candidate-view.test.ts +49 -0
  202. package/tests/review-contract-prompt.test.ts +5 -2
  203. package/tests/review-controller-native-routing.test.ts +14 -1
  204. package/tests/review-controller.test.ts +1 -1
  205. package/tests/review-host-relay-routing.test.ts +59 -2
  206. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  207. package/tests/review-owner-identity.test.ts +132 -0
  208. package/tests/review-risk-assessment.test.ts +163 -76
  209. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  210. package/tests/review-session-standing-permission.test.ts +27 -1
  211. package/tests/run-test-suite.test.ts +15 -0
  212. package/tests/runtime-harness.mjs +16 -6
  213. package/tests/selection-engine.test.ts +35 -5
  214. package/tests/session-changes-shell.test.ts +1 -1
  215. package/tests/session-profile-freeze.test.ts +37 -1
  216. package/tests/session-profile-persistence.test.ts +334 -0
  217. package/tests/session-scratch.test.ts +945 -0
  218. package/tests/session-subject-routing-contract.test.ts +2 -1
  219. package/tests/shell-bar.test.ts +7 -0
  220. package/tests/shell-changes-view.test.ts +137 -1
  221. package/tests/shell-changes.test.ts +18 -0
  222. package/tests/shell-gauge.test.ts +42 -0
  223. package/tests/shell-todo.test.ts +15 -12
  224. package/tests/small-task-inline-contract.test.ts +37 -0
  225. package/tests/standalone-config-home.test.ts +324 -0
  226. package/tests/startup-banner.test.ts +5 -4
  227. package/tests/startup-home-repo.test.ts +34 -0
  228. package/tests/support/orchestrator-modules.ts +1 -0
  229. package/tests/task-size-routing-contract.test.ts +69 -9
  230. package/tests/test-scratch-contract.test.ts +102 -0
  231. package/tests/test-strength-contract.test.ts +36 -1
  232. package/tests/tool-batching-contract.test.ts +64 -0
  233. package/tests/verification-proportionality.test.ts +46 -0
  234. package/tests/verifier-admission.test.ts +137 -0
  235. package/tests/verify-agent-contract.test.ts +8 -8
  236. package/tests/vim-editor-adapter.test.ts +41 -15
  237. package/tests/windows-hidden-processes.test.ts +5 -0
  238. package/tests/writer-edit-surface-scope.test.ts +3 -0
  239. package/tests/writer-feature-document-admission.test.ts +156 -0
  240. package/tests/yolo-mode.test.ts +24 -1
  241. package/tests/yolo-temp-scratch.test.ts +71 -0
  242. package/scripts/mirror-odd-routing.mjs +0 -242
  243. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  244. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  245. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  246. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  247. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  248. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  249. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -8,7 +8,8 @@ const source = readFileSync(join(root, "extensions/gentle-agents.ts"), "utf8");
8
8
  const match = source.match(/name: "orchestrator_session_id",\s*label: "[^"]+",\s*description: ("(?:[^"\\]|\\.)*")/);
9
9
  assert.ok(match, "Registered session tool must expose a public description");
10
10
  const description: string = JSON.parse(match[1]);
11
- const detail = readFileSync(join(root, "assets/orchestrator-delegation.md"), "utf8")
11
+ // S11 T2: the session subject rules moved from the delegation card to its reference.
12
+ const detail = readFileSync(join(root, "assets/orchestrator-delegation-reference.md"), "utf8")
12
13
  .split("## Session subject and display identity\n")[1].split("### Publish and find classified work")[0];
13
14
  const docs = readFileSync(join(root, "docs/gentle-agents-activity.md"), "utf8")
14
15
  .split("### Declare a recognizable subject\n")[1].split("### Publish curated state")[0];
@@ -219,6 +219,13 @@ test("renderShellBar renders one line with the segments in order", () => {
219
219
  );
220
220
  });
221
221
 
222
+ test("renderShellBar paints the context gauge with the given gauge roles", () => {
223
+ const [line] = renderShellBar(model(), taggedTheme, 400, undefined, { ok: "gaugeOk", empty: "gaugeEmpty" });
224
+ assert.match(line, /<gaugeOk>▰▰▰▰<\/gaugeOk><gaugeEmpty>▱▱▱▱<\/gaugeEmpty>/);
225
+ const [fallback] = renderShellBar(model(), taggedTheme, 400);
226
+ assert.match(fallback, /<accent>▰▰▰▰<\/accent><border>▱▱▱▱<\/border>/);
227
+ });
228
+
222
229
  test("renderShellBar colors the brand, model, effort, and gauge by role", () => {
223
230
  const [line] = renderShellBar(model(), taggedTheme, 400);
224
231
  assert.match(line, /<accent>✿ gentle shell<\/accent>/);
@@ -4,6 +4,7 @@ import { TuiAltScreen, visibleWidth, type Terminal, type TuiMouseEvent } from "@
4
4
  import { CHANGE_STATUS, DIFF_TOO_LARGE, changesModel, type ChangedFile } from "../lib/shell-changes.ts";
5
5
  import { WorktreeChangesView, ChangesView, colorDiff, type ChangesViewDeps } from "../lib/shell-changes-view.ts";
6
6
  import { stripAnsi } from "../lib/terminal-theme.ts";
7
+ import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
7
8
 
8
9
  // The changes overlay: files on the left, the selected file's diff on the
9
10
  // right, keys at the bottom. Rendering is pure; git access is injected.
@@ -458,7 +459,7 @@ const statusCases = [
458
459
  [CHANGE_STATUS.ADDED, "A", 3, 0],
459
460
  [CHANGE_STATUS.DELETED, "D", 0, 4],
460
461
  [CHANGE_STATUS.RENAMED, "R", 0, 0],
461
- [CHANGE_STATUS.UNTRACKED, "??", 5, 0],
462
+ [CHANGE_STATUS.UNTRACKED, "U", 5, 0],
462
463
  ] as const;
463
464
 
464
465
  for (const [status, code, added, deleted] of statusCases) {
@@ -757,3 +758,138 @@ test("ChangesView.update keeps the selected file, reloads moved diffs, and survi
757
758
  component.handleInput("j");
758
759
  component.handleInput("o");
759
760
  });
761
+
762
+ // Diff-only mode (F): the file column and its separator leave so the diff
763
+ // spans the whole inner width; j/k still walk files, esc returns first.
764
+ function diffOnlyView(overrides: { rows?: number; loadDiff?: (root: string, target: ChangedFile) => Promise<string> } = {}) {
765
+ const trees = [
766
+ { root: "/main", branch: "main", model: changesModel([file("a.ts", 1, 0), file("b.ts", 1, 0)]) },
767
+ { root: "/linked", branch: "agent", model: changesModel([file("c.ts", 1, 0)]) },
768
+ ];
769
+ const events: string[] = [];
770
+ const component = new WorktreeChangesView(trees, {
771
+ theme: plainTheme, rows: overrides.rows ?? 10,
772
+ loadDiff: overrides.loadDiff ?? (async (root, target) => `+${root}:${target.path} ${"x".repeat(80)}`),
773
+ onOpen: (root, target) => { events.push(`open:${root}:${target.path}`); },
774
+ onClose: () => { events.push("close"); },
775
+ onRefresh: () => { events.push("refresh"); },
776
+ requestRender() {},
777
+ });
778
+ return { component, events };
779
+ }
780
+
781
+ test("F hides the file list and its separator so the diff spans the full inner width", async () => {
782
+ const { component } = diffOnlyView();
783
+ component.handleInput("\r");
784
+ component.handleInput("j");
785
+ await settle();
786
+ const normal = component.render(100);
787
+ assert.match(normal[1]!, /^│ +▾ main · main +│ M a\.ts +│$/, "the normal view keeps the two-pane layout");
788
+ assert.match(stripAnsi(normal[2]!), /^│ ▸ M a\.ts .*│ \+\/main:a\.ts x+…│$/, "the narrow diff pane clips a long line");
789
+ assert.match(normal[8]!, /F diff only/);
790
+ component.handleInput("F");
791
+ await settle();
792
+ const wide = component.render(100);
793
+ assert.equal(wide.length, 10);
794
+ for (const line of wide) assert.equal(visibleWidth(line), 100);
795
+ assert.match(wide[1]!, /^│ M a\.ts +│$/, "the path header starts at the frame edge");
796
+ assert.equal(wide[2]!, `│ ${`+/main:a.ts ${"x".repeat(80)}`.padEnd(96)} │`, "the diff uses the full inner width");
797
+ assert.doesNotMatch(wide.slice(1, 8).join("\n"), /main · main|│.*│.*│/, "no list column and no separator remain");
798
+ assert.match(wide[8]!, /F files/);
799
+ component.handleInput("F");
800
+ assert.match(component.render(100)[1]!, /▾ main · main/, "F toggles back to the file list");
801
+ });
802
+
803
+ test("j/k in diff-only mode walk files across worktrees and the path header follows", async () => {
804
+ const { component } = diffOnlyView();
805
+ component.handleInput("F");
806
+ await settle();
807
+ assert.match(component.render(100)[1]!, /^│ M a\.ts +│$/, "entering on a group header jumps to its first file");
808
+ component.handleInput("j");
809
+ await settle();
810
+ assert.match(component.render(100)[1]!, /^│ M b\.ts +│$/);
811
+ component.handleInput("j");
812
+ await settle();
813
+ assert.match(component.render(100)[1]!, /^│ M c\.ts +│$/, "j crosses into the next worktree's files");
814
+ assert.match(component.render(100)[2]!, /\+\/linked:c\.ts/);
815
+ component.handleInput("j");
816
+ assert.match(component.render(100)[1]!, /^│ M c\.ts +│$/, "the last file stays selected");
817
+ component.handleInput("k");
818
+ await settle();
819
+ assert.match(component.render(100)[1]!, /^│ M b\.ts +│$/);
820
+ component.handleInput("j");
821
+ component.handleInput("\x1b");
822
+ assert.match(component.render(100).join("\n"), /▸ M c\.ts/, "the files view shows the file chosen in diff-only mode");
823
+ });
824
+
825
+ test("esc leaves diff-only mode before closing; q, o and r keep working there", async () => {
826
+ const { component, events } = diffOnlyView();
827
+ component.handleInput("F");
828
+ component.handleInput("\x1b");
829
+ assert.deepEqual(events, [], "the first esc only returns to the file list");
830
+ assert.match(component.render(100)[1]!, /main · main/);
831
+ component.handleInput("\x1b");
832
+ assert.deepEqual(events, ["close"]);
833
+ component.handleInput("F");
834
+ component.handleInput("o");
835
+ component.handleInput("r");
836
+ component.handleInput("q");
837
+ assert.deepEqual(events, ["close", "open:/main:a.ts", "refresh", "close"]);
838
+ });
839
+
840
+ test("diff-only mode has no list mouse zone; the wheel scrolls the widened diff", async () => {
841
+ const long = Array.from({ length: 30 }, (_, index) => `+line ${index}`).join("\n");
842
+ const { component } = diffOnlyView({ rows: 8, loadDiff: async () => `@@ -0,0 +1,30 @@\n${long}` });
843
+ component.handleInput("F");
844
+ await settle();
845
+ component.render(80);
846
+ assert.equal(component.handleMouse(mouseButton("move", "none", 3, 2, 8)), undefined, "no hover target");
847
+ assert.equal(component.handleMouse(mouseButton("click", "left", 3, 2, 8)), undefined, "no click target");
848
+ assert.match(component.render(80)[1]!, /^│ M a\.ts +│$/, "a click where the list was does not select");
849
+ assert.deepEqual(component.handleMouse(mouse("wheel", 3, 2, 3, 8)), { handled: true, render: true });
850
+ assert.doesNotMatch(component.render(80)[2]!, /@@/, "the left edge now scrolls the diff");
851
+ component.handleInput("\x1b");
852
+ component.render(80);
853
+ assert.deepEqual(component.handleMouse(mouse("click", 3, 3, undefined, 8)), { handled: true, render: true }, "leaving diff-only restores the list zone");
854
+ assert.match(component.render(80)[3]!, /▸ M b\.ts/);
855
+ });
856
+
857
+ test("the native fullscreen wrapper keeps keyboard, hover, click and wheel on the changes view", async () => {
858
+ let onInput: ((data: string) => void) | undefined;
859
+ const terminal: Terminal = {
860
+ start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 8; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
861
+ };
862
+ const long = Array.from({ length: 30 }, (_, index) => `+line ${index}`).join("\n");
863
+ let renders = 0;
864
+ const trees = [{ root: "/main", branch: "main", model: changesModel([file("a.ts", 1, 0), file("b.ts", 1, 0)]) }];
865
+ const component = new WorktreeChangesView(trees, {
866
+ theme: { fg: (color: string, text: string) => color === "warning" ? `\x1b[33m${text}\x1b[39m` : text }, rows: 8, loadDiff: async () => `@@ -0,0 +1,30 @@\n${long}`,
867
+ onOpen() {}, onClose() {}, onRefresh() {}, requestRender() {},
868
+ });
869
+ const interaction = createNativeFullscreenInteraction({ keyboardTarget: component, requestRender: () => { renders++; } });
870
+ interaction.addChild(component);
871
+ const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true });
872
+ tui.setLayoutRoot(interaction);
873
+ tui.start();
874
+ interaction.handleInput("\r");
875
+ assert.ok(renders > 0, "keys reach the view and request a render");
876
+ tui.renderNow(true);
877
+ try {
878
+ onInput?.("\x1b[<35;4;4M");
879
+ assert.match(interaction.render(80)[3]!, /\x1b\[33mM b\.ts/, "hover paints the row under the pointer");
880
+ onInput?.("\x1b[<0;4;4M");
881
+ onInput?.("\x1b[<0;4;4m");
882
+ assert.match(stripAnsi(interaction.render(80)[3]!), /▸ M b\.ts/, "a click selects the file");
883
+ await settle();
884
+ tui.renderNow(true);
885
+ onInput?.("\x1b[<65;60;3M");
886
+ assert.doesNotMatch(stripAnsi(interaction.render(80)[2]!), /@@/, "the wheel scrolls the diff");
887
+ interaction.handleInput("F");
888
+ tui.renderNow(true);
889
+ onInput?.("\x1b[<0;4;3M");
890
+ onInput?.("\x1b[<0;4;3m");
891
+ assert.match(stripAnsi(interaction.render(80)[1]!), /^│ M b\.ts +│$/, "diff-only mode ignores clicks where the list was");
892
+ } finally {
893
+ tui.stop();
894
+ }
895
+ });
@@ -159,6 +159,24 @@ test("ChangesTracker reads the working tree on start and again on refresh", asyn
159
159
  assert.deepEqual(calls[1], ["status", "--porcelain=v1", "--untracked-files=all", "-z"]);
160
160
  });
161
161
 
162
+ test("ChangesTracker skips untracked dependency backups but preserves tracked edits and legitimate new files", async () => {
163
+ const backup = "node_modules.backup-20261007-192802/pkg/index.js";
164
+ const nestedBackup = "packages/app/node_modules.backup-local/pkg/index.js";
165
+ const tracked = "node_modules.backup-local/tracked.ts";
166
+ const legitimate = ["src/new.ts", "node_modules.backup-notes.md", "node_modules.backup-/new.ts"];
167
+ const { git } = fakeGit(
168
+ [`4\t2\t${tracked}\n`],
169
+ [[`?? ${backup}`, `?? ${nestedBackup}`, ` M ${tracked}`, ...legitimate.map((path) => `?? ${path}`)].join("\0") + "\0"],
170
+ );
171
+ const counted: string[] = [];
172
+ const tracker = new ChangesTracker(git, async (path) => { counted.push(path); return 3; });
173
+ await tracker.start();
174
+ assert.deepEqual(counted.sort(), [...legitimate].sort());
175
+ assert.deepEqual(tracker.model, changesModel([
176
+ file(tracked, 4, 2), ...legitimate.map((path) => file(path, 3, 0, CHANGE_STATUS.UNTRACKED)),
177
+ ]));
178
+ });
179
+
162
180
  test("ChangesTracker stays quiet outside a git repository", async () => {
163
181
  const { git, calls } = fakeGit([""], [""], 128);
164
182
  const tracker = new ChangesTracker(git);
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { paintGauge, renderGauge, type GaugePaint } from "../lib/shell-gauge.ts";
4
+
5
+ // Issue #2222: gauge-specific theme roles fall back to today's roles when a
6
+ // theme does not define them, so existing themes render byte-identically.
7
+ // (Fixtures here are unit-scoped stubs; Pi's theme schema is validated
8
+ // separately by the loader and was checked directly against ThemeJsonSchema.)
9
+
10
+ const taggedTheme = {
11
+ fg(color: string, text: string) {
12
+ return `<${color}>${text}</${color}>`;
13
+ },
14
+ };
15
+
16
+ test("paintGauge falls back to today's roles when no gauge roles are given", () => {
17
+ assert.equal(paintGauge(45, taggedTheme), `<accent>▰▰▰▰</accent><border>▱▱▱▱</border>`);
18
+ assert.equal(paintGauge(85, taggedTheme), `<warning>▰▰▰▰▰▰▰</warning><border>▱</border>`);
19
+ assert.equal(paintGauge(96, taggedTheme), `<error>▰▰▰▰▰▰▰▰</error><border></border>`);
20
+ assert.equal(paintGauge(null, taggedTheme), `<dim></dim><border>▱▱▱▱▱▱▱▱</border>`);
21
+ });
22
+
23
+ test("paintGauge uses the gauge-specific roles when the theme defines them", () => {
24
+ const roles: GaugePaint = { ok: "gaugeOk", warning: "gaugeWarning", error: "gaugeError", empty: "gaugeEmpty" };
25
+ assert.equal(paintGauge(45, taggedTheme, 8, roles), `<gaugeOk>▰▰▰▰</gaugeOk><gaugeEmpty>▱▱▱▱</gaugeEmpty>`);
26
+ assert.equal(paintGauge(85, taggedTheme, 8, roles), `<gaugeWarning>▰▰▰▰▰▰▰</gaugeWarning><gaugeEmpty>▱</gaugeEmpty>`);
27
+ assert.equal(paintGauge(96, taggedTheme, 8, roles), `<gaugeError>▰▰▰▰▰▰▰▰</gaugeError><gaugeEmpty></gaugeEmpty>`);
28
+ // A null percentage stays dim: there is nothing to gauge yet.
29
+ assert.equal(paintGauge(null, taggedTheme, 8, roles), `<dim></dim><gaugeEmpty>▱▱▱▱▱▱▱▱</gaugeEmpty>`);
30
+ });
31
+
32
+ test("paintGauge falls back per role, not per block", () => {
33
+ // A theme that defines only gaugeWarning keeps accent for the ok range.
34
+ assert.equal(paintGauge(45, taggedTheme, 8, { warning: "gaugeWarning" }), `<accent>▰▰▰▰</accent><border>▱▱▱▱</border>`);
35
+ assert.equal(paintGauge(85, taggedTheme, 8, { warning: "gaugeWarning" }), `<gaugeWarning>▰▰▰▰▰▰▰</gaugeWarning><border>▱</border>`);
36
+ });
37
+
38
+ test("renderGauge uses theme-defined glyphs when given, defaults otherwise", () => {
39
+ assert.equal(renderGauge(50, 4, { filled: "█", empty: "░" }), "██░░");
40
+ assert.equal(renderGauge(50, 4), "▰▰▱▱");
41
+ assert.equal(renderGauge(50, 4, { filled: "█" }), "██▱▱");
42
+ });
@@ -9,6 +9,7 @@ import {
9
9
  staleTurns,
10
10
  TODO_STATUS,
11
11
  todoPromptBlock,
12
+ todoStateMessage,
12
13
  todoSummary,
13
14
  type TodoState,
14
15
  } from "../lib/shell-todo.ts";
@@ -152,19 +153,21 @@ test("replayTodo rebuilds the latest state from Gentle and rpiv tool results ali
152
153
  assert.deepEqual(replayTodo([]), emptyTodo());
153
154
  });
154
155
 
155
- test("todoPromptBlock lists open work with the rules, and stays silent when there is nothing open", () => {
156
- const block = todoPromptBlock(seeded(), 0);
157
- assert.ok(block);
156
+ test("todoPromptBlock is the byte-stable rules block; todoStateMessage carries the open work (#1662)", () => {
157
+ const block = todoPromptBlock();
158
158
  assert.match(block, /^## Todo list/m);
159
159
  assert.match(block, /mark a task in_progress before starting/);
160
- assert.match(block, /1\. \[done\] Add quiet tool rendering/);
161
- assert.match(block, /2\. \[in_progress\] Fix quiet tools conflict — fixing conflict/);
162
- assert.match(block, /3\. \[pending\] Show git bash tails/);
163
160
  assert.doesNotMatch(block, /stale/);
164
- assert.match(todoPromptBlock(seeded(), 2) ?? "", /stale: 2 turns without an update/);
165
- assert.equal(todoPromptBlock(emptyTodo(), 0), undefined);
161
+ assert.doesNotMatch(block, /Add quiet tool rendering/, "the rules block never contains task content");
162
+ const message = todoStateMessage(seeded());
163
+ assert.ok(message);
164
+ assert.match(message, /1\. \[done\] Add quiet tool rendering/);
165
+ assert.match(message, /2\. \[in_progress\] Fix quiet tools conflict — fixing conflict/);
166
+ assert.match(message, /3\. \[pending\] Show git bash tails/);
167
+ assert.doesNotMatch(message, /stale/);
168
+ assert.equal(todoStateMessage(emptyTodo()), undefined);
166
169
  const allDone = applyTodo(seeded(), { action: "write", tasks: [{ title: "A", status: "done" }] }, 1).state;
167
- assert.equal(todoPromptBlock(allDone, 0), undefined);
170
+ assert.equal(todoStateMessage(allDone), undefined);
168
171
  });
169
172
 
170
173
  test("collapsed long tasks occupy one body line, including stale state, without losing task data", (t) => {
@@ -446,7 +449,7 @@ test("applyTodo rejects blocked without a note and leaves the list unchanged", (
446
449
  test("dropped tasks never count as open: a list of done and dropped is finished, silent, and never stale", () => {
447
450
  const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "A", status: "done" }, { title: "B", status: "dropped" }, { title: "C", status: "dropped" }] }, 1).state;
448
451
  assert.deepEqual(todoSummary(state), { done: 1, total: 3, open: 0 });
449
- assert.equal(todoPromptBlock(state, 5), undefined);
452
+ assert.equal(todoStateMessage(state), undefined);
450
453
  assert.equal(staleTurns(state, 9), 0);
451
454
  });
452
455
 
@@ -454,8 +457,8 @@ test("blocked tasks stay open but alone never make the list stale; an actionable
454
457
  const blockedOnly = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "A", status: "done" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }] }, 1).state;
455
458
  assert.deepEqual(todoSummary(blockedOnly), { done: 1, total: 2, open: 1 });
456
459
  assert.equal(staleTurns(blockedOnly, 9), 0);
457
- const block = todoPromptBlock(blockedOnly, 0) ?? "";
458
- assert.match(block, /2\. \[blocked\] Deploy — waiting for an admin/);
460
+ assert.match(todoStateMessage(blockedOnly) ?? "", /2\. \[blocked\] Deploy — waiting for an admin/);
461
+ const block = todoPromptBlock();
459
462
  assert.match(block, /A blocked task waits on something outside the list: do not work on it until its condition holds/);
460
463
  assert.match(block, /mark the tasks it made obsolete dropped instead of leaving them pending or marking them done/);
461
464
  const mixed = applyTodo(blockedOnly, { action: "add", title: "Write docs" }, 1).state;
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { __testing } from "../extensions/gentle-ai.ts";
6
+
7
+ // Pain-point iteration S15. Bench evidence (pps14 x5): one run read "Two unrelated wishes" as separate
8
+ // deliverables, classified a two-feature request as large (branch, feature document, session state), and
9
+ // started two writers for ~100-line units: USD 0.55 against 0.20 for the run that stayed inline. Blind round 4
10
+ // also found delivered tests that pin `git show <baseline commit>`, so the differential check had leaked into
11
+ // committed tests that fail in another clone. These are instruction-delivery contracts, not proof of adherence.
12
+
13
+ const read = (path: string): string => readFileSync(join(import.meta.dirname, "..", path), "utf8");
14
+
15
+ test("S15 (a): separate deliverables are ones the user wants apart, not several features asked at once", () => {
16
+ const core = read("assets/orchestrator.md");
17
+ assert.ok(
18
+ core.includes("deliverables the user wants apart (never several features asked at once)"),
19
+ "Task Size must define separate deliverables",
20
+ );
21
+ });
22
+
23
+ test("S15 (b): parallel writers never fire for units of about 100 changed lines or less", () => {
24
+ const card = read("assets/orchestrator-delegation.md");
25
+ assert.ok(
26
+ card.includes("Units of about 100 changed lines or less always run inline: writing them costs less than starting workers."),
27
+ "the Writer trigger must keep small units inline",
28
+ );
29
+ });
30
+
31
+ test("S15 (c): the differential check runs in scratch and is never committed as a test", () => {
32
+ const clause = "a copy of the base commit in a `mktemp -d` scratch directory, never committed as a test; committed tests never read git history or a fixed commit";
33
+ const harness = __testing.buildGentlePrompt("neutral");
34
+ assert.ok(harness.includes(clause), "the always-on test-first policy must keep the differential check in scratch");
35
+ assert.ok(!harness.includes("(`git stash` or a copy of HEAD)"), "the harness must not suggest git stash or HEAD copies");
36
+ assert.ok(read("assets/agents/gentle-ai-worker.md").includes(clause), "the worker must keep the differential check in scratch");
37
+ });