gentle-pi 3.7.0 → 4.0.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 (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -1,20 +1,21 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFile } from "node:child_process";
3
- import { mkdtempSync, realpathSync, rmSync } from "node:fs";
3
+ import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
4
4
  import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
7
7
  import baseTest from "node:test";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { promisify } from "node:util";
10
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
12
12
  import {
13
13
  GENTLE_AI_DEV_BINARY_ENV,
14
14
  resolveGentleAiBinary,
15
15
  type GentleAiDevBinaryEnvironment,
16
16
  } from "../lib/gentle-ai-binary.ts";
17
- import { NativeReviewCliV216 } from "../lib/native-review-cli.ts";
17
+ import { createNodeExecFileAdapter, NativeReviewCliV216 } from "../lib/native-review-cli.ts";
18
+ import { resolveSessionWorktree } from "../lib/session-worktree-registry.ts";
18
19
  import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
19
20
  import { decodeReviewLastEventClosureV1 } from "../lib/review-integration-v2.ts";
20
21
  import { decodeReviewLastEventClosureV1 as decodeRuntimeReviewLastEventClosureV1 } from "../runtime/review-integration-v2.mjs";
@@ -129,6 +130,102 @@ async function run(command: string, arguments_: readonly string[], cwd: string,
129
130
  }
130
131
  }
131
132
 
133
+ test("SDK explicit review delegates enabled-only non-Git preparation to the pinned native binary without START", async (t) => {
134
+ const previous: Record<string, string | undefined> = {};
135
+ for (const key of Object.keys(process.env).filter((key) => key.startsWith("GIT_"))) {
136
+ previous[key] = process.env[key];
137
+ delete process.env[key];
138
+ }
139
+ const home = await reviewEnabledHome(t);
140
+ for (const [key, value] of Object.entries({
141
+ GENTLE_PI_AGENT_HOME: join(home, "agent"), PI_CODING_AGENT_DIR: join(home, "agent"),
142
+ GENTLE_PI_CONFIG_HOME: join(home, "pi-config"), PI_OFFLINE: "1",
143
+ GENTLE_PI_REVIEW_RELAY_CONTRACT: "invalid-ambient-relay",
144
+ })) {
145
+ previous[key] = process.env[key];
146
+ process.env[key] = value;
147
+ }
148
+ t.after(() => {
149
+ for (const [key, value] of Object.entries(previous)) {
150
+ if (value === undefined) delete process.env[key]; else process.env[key] = value;
151
+ }
152
+ });
153
+ const version = await run(binary, ["version"], home);
154
+ t.diagnostic(`native binary=${binary}; version=${version.stdout.trim()}`);
155
+ const calls: string[][] = [];
156
+ const agentDir = process.env.GENTLE_PI_AGENT_HOME!;
157
+ await mkdir(agentDir, { recursive: true });
158
+ const modelRuntime = await ModelRuntime.create({
159
+ authPath: join(agentDir, "empty-auth.json"), modelsPath: null,
160
+ modelsStorePath: join(agentDir, "models-store.json"), allowModelNetwork: false,
161
+ });
162
+ const startup = async (cwd: string, enabled = true) => {
163
+ const before = calls.length;
164
+ const wasVersioned = resolveSessionWorktree(cwd, cwd) !== undefined;
165
+ // Production adapter overwrites the poisoned ambient relay declaration.
166
+ // Fresh native clients also prevent a cached refusal from masking later
167
+ // parent/broken-metadata scenarios without actually invoking STATUS.
168
+ const adapter = createNodeExecFileAdapter();
169
+ const native = new NativeReviewCliV216(async (request) => {
170
+ calls.push([...request.arguments]);
171
+ return adapter(request);
172
+ }, binary);
173
+ const resourceLoader = new DefaultResourceLoader({
174
+ cwd, agentDir, noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true,
175
+ extensionFactories: [createGentleAiExtension({ nativeReviewCli: native, candidateViews: null, processEnv: {} })],
176
+ });
177
+ await resourceLoader.reload();
178
+ assert.deepEqual(resourceLoader.getExtensions().errors, []);
179
+ const { session } = await createAgentSession({
180
+ cwd, agentDir, resourceLoader, modelRuntime, sessionManager: SessionManager.inMemory(cwd), noTools: "builtin",
181
+ settingsManager: SettingsManager.inMemory({ retry: { enabled: false }, compaction: { enabled: false } }),
182
+ });
183
+ try {
184
+ await session.bindExtensions({ mode: "print" });
185
+ assert.equal(calls.slice(before).some(args => args[0] === "review" && args[1] === "status"), false, "passive non-Git startup cannot prepare");
186
+ session.setActiveToolsByName(["gentle_review"]);
187
+ const inspect = session.agent.state.tools.find(tool => tool.name === "gentle_review");
188
+ assert.ok(inspect, "real SDK registers the facade");
189
+ await inspect.execute("explicit-review", { operation: "inspect", workspaceRoot: cwd });
190
+ assert.equal(session.messages.length, 0, "explicit review entry must not begin a model turn");
191
+ } finally { await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" }); session.dispose(); }
192
+ const scenarioCalls = calls.slice(before);
193
+ if (!wasVersioned) assert.ok(scenarioCalls.some((args) => args[0] === "review" && args[1] === "mode"), "every non-Git explicit review invokes native mode");
194
+ const statuses = scenarioCalls.filter((args) => args[0] === "review" && args[1] === "status");
195
+ assert.equal(statuses.length > 0, enabled, "every on scenario invokes native STATUS; off cannot");
196
+ t.diagnostic(`startup cwd=${cwd}; STATUS calls=${statuses.length}`);
197
+ };
198
+ const plain = join(home, "enabled-project");
199
+ await mkdir(plain);
200
+ await writeFile(join(plain, "candidate.txt"), "candidate bytes\n");
201
+ await startup(plain);
202
+ assert.equal(existsSync(join(plain, ".git")), true, "explicit enabled review must reach native bootstrap");
203
+ assert.equal(await readFile(join(plain, "candidate.txt"), "utf8"), "candidate bytes\n");
204
+ assert.notEqual((await run("git", ["rev-parse", "--verify", "HEAD"], plain, true)).exitCode, 0, "HEAD remains unborn");
205
+ assert.equal((await run("git", ["remote"], plain)).stdout, "");
206
+ assert.equal(existsSync(join(plain, ".git", "gentle-ai", "reviews")), false, "entry does not create review lineage storage");
207
+ const nested = join(plain, "nested");
208
+ await mkdir(nested);
209
+ await startup(nested);
210
+ assert.equal(existsSync(join(nested, ".git")), false, "containing repository is reused");
211
+ const broken = join(home, "broken-project");
212
+ await mkdir(broken);
213
+ await writeFile(join(broken, ".git"), "gitdir: missing-metadata\n");
214
+ await startup(broken);
215
+ assert.equal(await readFile(join(broken, ".git"), "utf8"), "gitdir: missing-metadata\n");
216
+ const brokenChild = join(broken, "child");
217
+ await mkdir(brokenChild);
218
+ await startup(brokenChild);
219
+ assert.equal(existsSync(join(brokenChild, ".git")), false);
220
+ await run(binary, ["review", "mode", "disable", "--scope", "global", "--cwd", plain, "--json"], plain);
221
+ const off = join(home, "off-project");
222
+ await mkdir(off);
223
+ await startup(off, false);
224
+ assert.equal(existsSync(join(off, ".git")), false, "off explicit review leaves the target unversioned");
225
+ assert.equal(calls.some((args) => args[0] === "review" && args[1] === "start"), false);
226
+ t.diagnostic(`native SDK startup calls=${JSON.stringify(calls)}`);
227
+ });
228
+
132
229
  test("generated runtime decoder consumes the captured terminal closure directly", async () => {
133
230
  const fixture = JSON.parse(await readFile(join(packageRoot, "tests", "fixtures", "devbinary", "last-event-capture-result-approved.captured.json"), "utf8"));
134
231
  const source = decodeReviewLastEventClosureV1(fixture);
@@ -0,0 +1,67 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+
5
+ const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
6
+
7
+ test("ODD continuity persists the task file and complete Engram mirror across resume", () => {
8
+ const memory = read("assets/orchestrator-memory.md");
9
+ assert.match(memory, /one feature document, not a separate plan file or topic/);
10
+ assert.match(memory, /odd\/tasks\/<feature-name>\.md/);
11
+ assert.match(memory, /odd\/<feature-name>\/tasks/);
12
+ assert.match(memory, /Mirror the full current document and repository-relative file locator/);
13
+ assert.match(memory, /Read back both writes; they are not atomic/);
14
+ assert.match(memory, /mirror pending/);
15
+ assert.match(memory, /On resume, use `mem_context`, then project\/feature-scoped `mem_search`, and `mem_get_observation`/);
16
+ assert.match(memory, /read the actual task file/);
17
+ });
18
+
19
+ test("optional research stays output-only and delegates to a general worker", () => {
20
+ const delegation = read("assets/orchestrator-delegation.md");
21
+ assert.match(delegation, /Recommend optional research only for a named uncertainty/);
22
+ assert.match(delegation, /Forward these research instructions to an existing fresh general exploration\/research worker/);
23
+ assert.match(delegation, /Research remains read-only and requires no new persistence or readiness machinery/);
24
+ assert.match(delegation, /If tools are unavailable, disclose limitations without inventing access or evidence/);
25
+ });
26
+
27
+ test("applicability, fallback and honest evidence flow through ODD actors", () => {
28
+ const wrapper = read("extensions/gentle-ai.ts");
29
+ const delegation = read("assets/orchestrator-delegation.md");
30
+ const support = read("assets/support/strict-tdd.md");
31
+ const worker = read("assets/agents/gentle-ai-worker.md");
32
+ const verifier = read("assets/agents/gentle-ai-verify.md");
33
+ assert.match(wrapper, /behavior changes with applicable runnable deterministic tests and a clear expected outcome/);
34
+ assert.match(delegation, /Test or framework presence alone does not establish applicability/);
35
+ for (const [actor, text] of [["implementation support", support], ["worker", worker], ["verifier", verifier]] as const) {
36
+ assert.match(text, /behavior changes with applicable runnable deterministic tests and a clear expected outcome/i, `${actor} must assess behavior-level applicability`);
37
+ assert.match(text, /passive documentation/i, `${actor} must handle passive docs`);
38
+ assert.match(text, /unavailable runner/i, `${actor} must handle an unavailable runner`);
39
+ assert.match(text, /ordinary functional or structural verification/i, `${actor} must verify fallbacks`);
40
+ }
41
+ assert.match(support, /no meaningful RED/);
42
+ assert.match(worker, /RED — add the smallest behavior-level test and capture its intended observed failure/);
43
+ assert.match(verifier, /execute only exact test, build, or lint commands explicitly authorized by the parent/);
44
+ assert.match(verifier, /Do not infer RED from a test file existing/);
45
+ });
46
+
47
+ test("retired SDD routes and assets are absent while ODD entry and generic workers remain", () => {
48
+ const core = read("assets/orchestrator.md");
49
+ const delegation = read("assets/orchestrator-delegation.md");
50
+ assert.match(core, /ODD \(Default Workflow, harness section above\) is mandatory on every request/);
51
+ assert.match(delegation, /generic writer chain is unavailable/);
52
+ for (const path of [
53
+ "assets/agents/gentle-ai-worker.md",
54
+ "assets/agents/gentle-ai-verify.md",
55
+ "assets/support/strict-tdd.md",
56
+ ]) assert.equal(existsSync(new URL(`../${path}`, import.meta.url)), true, path);
57
+ for (const path of [
58
+ "assets/sdd-orchestrator-workflow.md",
59
+ "assets/support/sdd-status-contract.md",
60
+ "assets/agents/sdd-apply.md",
61
+ "assets/agents/sdd-research.md",
62
+ "assets/chains/sdd-full.chain.md",
63
+ "assets/chains/sdd-plan.chain.md",
64
+ "assets/chains/sdd-verify.chain.md",
65
+ ]) assert.equal(existsSync(new URL(`../${path}`, import.meta.url)), false, path);
66
+ assert.doesNotMatch(core + delegation + read("extensions/gentle-ai.ts"), /(?:\/sdd-(?:init|explore|status|apply|verify|archive)|sdd-full\.chain|sdd-orchestrator-workflow\.md)/);
67
+ });
@@ -0,0 +1,213 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { inferOddPhase } from "../lib/odd-phase-inference.ts";
4
+
5
+ // Deterministic, conservative tool -> ODD phase mapping for the Gentle Shell
6
+ // working label. Unknown tools and ambiguous shell commands never change it.
7
+
8
+ test("read-only file tools infer exploring", () => {
9
+ for (const tool of ["read", "grep", "find", "ls", "codegraph"]) {
10
+ assert.equal(inferOddPhase(tool, { path: "src/index.ts" }), "exploring", tool);
11
+ }
12
+ });
13
+
14
+ test("asking the user infers deciding", () => {
15
+ assert.equal(inferOddPhase("ask_user_choice", {}), "deciding");
16
+ assert.equal(inferOddPhase("ask_user_question", {}), "deciding");
17
+ });
18
+
19
+ test("todo and edits to ODD feature documents infer planning", () => {
20
+ assert.equal(inferOddPhase("todo", { items: [] }), "planning");
21
+ assert.equal(inferOddPhase("write", { path: "odd/tasks/feature.md" }), "planning");
22
+ assert.equal(inferOddPhase("edit", { path: "/repo/odd/tasks/feature.md" }), "planning");
23
+ assert.equal(inferOddPhase("edit", { path: "C:\\repo\\odd\\tasks\\feature.md" }), "planning");
24
+ });
25
+
26
+ test("edits and writes to other paths infer implementing", () => {
27
+ assert.equal(inferOddPhase("edit", { path: "lib/odd-phase.ts" }), "implementing");
28
+ assert.equal(inferOddPhase("write", { path: "tests/new.test.ts" }), "implementing");
29
+ assert.equal(inferOddPhase("write", {}), "implementing", "a write without a readable path is still a write");
30
+ assert.equal(inferOddPhase("edit", undefined), "implementing");
31
+ });
32
+
33
+ test("native review tools infer checking", () => {
34
+ for (const tool of ["gentle_review", "gentle_review_scope", "gentle_review_capture", "gentle_review_capture_group"]) {
35
+ assert.equal(inferOddPhase(tool, {}), "checking", tool);
36
+ }
37
+ });
38
+
39
+ test("shell test, typecheck, lint, and build commands infer checking", () => {
40
+ for (const command of [
41
+ "pnpm test",
42
+ "npm test",
43
+ "npm run test -- --watch=false",
44
+ "node --experimental-strip-types --test tests/odd-phase.test.ts",
45
+ "npx vitest run",
46
+ "jest --ci",
47
+ "go test ./...",
48
+ "cargo test",
49
+ "python -m pytest -q",
50
+ "pytest tests/",
51
+ "tsc --noEmit",
52
+ "pnpm typecheck",
53
+ "node scripts/check-types.mjs",
54
+ "pnpm lint",
55
+ "eslint .",
56
+ "npm run build",
57
+ "go build ./...",
58
+ "make",
59
+ "cd sub && make check",
60
+ ]) {
61
+ assert.equal(inferOddPhase("bash", { command }), "checking", command);
62
+ }
63
+ assert.equal(inferOddPhase("powershell", { command: "npm test" }), "checking");
64
+ });
65
+
66
+ test("waiting on or reading CI results infers checking", () => {
67
+ for (const command of [
68
+ "gh pr checks 1511",
69
+ "gh pr checks 1511 --repo owner/name --watch --interval 20",
70
+ "sleep 30; gh pr checks 1511 --watch 2>&1 | tail -10",
71
+ "gh run watch 123",
72
+ "gh run view 123 --log-failed",
73
+ "gh run list --branch main",
74
+ ]) {
75
+ assert.equal(inferOddPhase("bash", { command }), "checking", command);
76
+ }
77
+ for (const command of ["gh pr view 1511", "gh pr diff 1511", "gh issue list"]) {
78
+ assert.equal(inferOddPhase("bash", { command }), "exploring", command);
79
+ }
80
+ });
81
+
82
+ test("read-only shell inspection infers exploring", () => {
83
+ for (const command of [
84
+ "git status",
85
+ "git log --oneline -5",
86
+ "git diff --stat",
87
+ "git show HEAD",
88
+ "git branch -a",
89
+ "ls -la",
90
+ "cat package.json",
91
+ "head -20 README.md",
92
+ "tail -n 5 log.txt",
93
+ "grep -rn foo lib",
94
+ "rg oddPhase",
95
+ "find . -name '*.ts'",
96
+ "wc -l lib/odd-phase.ts",
97
+ "pwd",
98
+ ]) {
99
+ assert.equal(inferOddPhase("bash", { command }), "exploring", command);
100
+ }
101
+ });
102
+
103
+ test("a checking segment wins over an exploring segment in the same command", () => {
104
+ assert.equal(inferOddPhase("bash", { command: "git status && pnpm test" }), "checking");
105
+ assert.equal(inferOddPhase("bash", { command: "pnpm test | tail -20" }), "checking");
106
+ });
107
+
108
+ // Real compound commands an orchestrator ran during one live session: loops,
109
+ // command substitutions, quoted pipes, and stderr redirects are routine, so
110
+ // read-only inspection must still read as exploring.
111
+ test("real compound read-only shell commands infer exploring", () => {
112
+ for (const command of [
113
+ `cd /repo && git rev-parse --show-toplevel && ls -d .codegraph 2>/dev/null; git log -1 --oneline; grep -rniI "bridge" --include=*.ts -l . 2>/dev/null | grep -v node_modules | head -30`,
114
+ `ls ~/work | grep -i -E "shell|pi"; for d in ~/work/a ~/dev/b; do [ -d $d ] && echo "== $d" && cd $d && git log -1 --oneline && grep -rniI "bridge" -l . 2>/dev/null | grep -v node_modules | head -20; done`,
115
+ `cd ~/w && grep -rniI "bridge" src | grep -v test | head -15; echo ===; cat ~/.config/settings.json 2>/dev/null | head -40; cd ~/repo && git branch -a --contains 6776520be 2>/dev/null | head`,
116
+ `f=~/.local/bin/tool; ls -la $f; file -h $f; readlink -f $f; head -c 1500 "$(readlink -f $f)" | strings | head -40`,
117
+ `which -a pi; readlink -f "$(which pi)"; cd ~/work/pi 2>/dev/null && git branch --show-current && git rev-list --left-right --count HEAD...origin/main 2>/dev/null`,
118
+ `ps -axo pid,command | grep -E "gentle-shell|pi-coding-agent|/pi " | grep -v grep | cut -c1-300; echo ===; cd ~/w && grep -n "isInteractiveMode" -A8 lib/rpc-host.ts | head -30`,
119
+ `pgrep -P 96445; for p in 96445 $(pgrep -P 96445); do echo "== $p"; ps -o command= -p $p | cut -c1-250; lsof -p $p 2>/dev/null | grep -E "lib/(a|b)" | awk '{print $NF}' | sort -u | head; done`,
120
+ `d=~/.sessions; for x in $(grep -l "probe" $(find $d -name "*.jsonl" -mmin -120) 2>/dev/null); do echo "== $x"; grep -o '"toolName":"[^"]*"' $x | sort | uniq -c; done`,
121
+ `cd ~/w && git tag --contains 53d62fbf3 | head; git log -1 --format='%h %ad' --date=short 53d62fbf3; git tag --sort=-creatordate | head -3`,
122
+ `git -C ~/w remote -v && git worktree list && git stash list && git config --get user.name && gh pr view 1415 && jq .version package.json && sed -n '1,20p' README.md`,
123
+ ]) {
124
+ assert.equal(inferOddPhase("bash", { command }), "exploring", command);
125
+ }
126
+ });
127
+
128
+ test("compound commands that mutate anything leave the label unchanged", () => {
129
+ for (const command of [
130
+ "git switch main 2>&1 && git pull --ff-only 2>&1 | tail -3",
131
+ "sed -i '' 's/a/b/' odd/tasks/f.md && grep -n x odd/tasks/f.md",
132
+ "ln -s ~/a node_modules && echo linked",
133
+ `git add f && git commit -q -m "x" && git log --oneline -3`,
134
+ "cat >> f.md <<'EOF'\nhello\nEOF",
135
+ "x=$(rm -rf dist); echo $x",
136
+ `for f in *.ts; do grep -l x "$f" && rm "$f"; done`,
137
+ "echo hi > out.txt",
138
+ "sort -o out.txt in.txt",
139
+ "git tag v1.0",
140
+ "git branch new-branch",
141
+ "git remote add origin https://example.com/r.git",
142
+ "git config user.name someone",
143
+ "ls $(touch marker)",
144
+ "grep x f | xargs rm",
145
+ ]) {
146
+ assert.equal(inferOddPhase("bash", { command }), undefined, command);
147
+ }
148
+ });
149
+
150
+ // Inference runs synchronously on tool_execution_start, so a pathological
151
+ // command must never backtrack exponentially and freeze the host.
152
+ test("adversarial assignment runs classify in bounded time", () => {
153
+ for (const command of ["a=".repeat(26) + '"', "a=".repeat(26) + "\\\"", "a=".repeat(26) + "'"]) {
154
+ const start = performance.now();
155
+ assert.equal(inferOddPhase("bash", { command }), undefined, command);
156
+ assert.ok(performance.now() - start < 50, `${command} took ${performance.now() - start}ms`);
157
+ }
158
+ });
159
+
160
+ test("no-op-only commands leave the label unchanged", () => {
161
+ for (const command of ["sleep 4", "echo ===", "cd /repo && echo done", "true"]) {
162
+ assert.equal(inferOddPhase("bash", { command }), undefined, command);
163
+ }
164
+ });
165
+
166
+ test("ambiguous or mutating shell commands leave the label unchanged", () => {
167
+ for (const command of [
168
+ "git commit -m 'feat: x'",
169
+ "git push",
170
+ "rm -rf dist",
171
+ "echo hello",
172
+ "pnpm install",
173
+ "curl https://example.com",
174
+ "ls && rm -rf dist",
175
+ "",
176
+ ]) {
177
+ assert.equal(inferOddPhase("bash", { command }), undefined, command);
178
+ }
179
+ assert.equal(inferOddPhase("bash", {}), undefined);
180
+ assert.equal(inferOddPhase("bash", { command: 42 }), undefined);
181
+ assert.equal(inferOddPhase("bash", undefined), undefined);
182
+ });
183
+
184
+ test("known delegated agents infer the parent work phase in foreground and background", () => {
185
+ for (const [agent, phase] of [
186
+ ["gentle-ai-worker", "implementing"],
187
+ ["gentle-ai-verify", "checking"],
188
+ ["gentle-ai-explore", "exploring"],
189
+ ] as const) {
190
+ for (const mode of ["task", "background"]) {
191
+ assert.equal(inferOddPhase("subagent_run", { agent, mode, task: "untrusted task prose" }), phase);
192
+ }
193
+ }
194
+ });
195
+
196
+ test("unknown or malformed delegation arguments never infer from task prose", () => {
197
+ for (const args of [
198
+ {}, null, "gentle-ai-worker", { agent: 5 }, { agent: "gentle-ai-writer" },
199
+ { task: "gentle-ai-verify checking" }, { agent: "gentle-ai-verify-extra" },
200
+ ]) assert.equal(inferOddPhase("subagent_run", args), undefined);
201
+ for (const tool of ["subagent_start", "subagent_wait", "gentle_odd_phase", "mem_save", "web_fetch", ""]) {
202
+ assert.equal(inferOddPhase(tool, {}), undefined, tool);
203
+ }
204
+ });
205
+
206
+ test("MCP-prefixed tool names are normalized before mapping", () => {
207
+ assert.equal(inferOddPhase("mcp__custom-tools__read", { path: "a.ts" }), "exploring");
208
+ assert.equal(inferOddPhase("mcp__custom-tools__edit", { path: "a.ts" }), "implementing");
209
+ assert.equal(inferOddPhase("mcp__custom-tools__write", { path: "odd/tasks/f.md" }), "planning");
210
+ assert.equal(inferOddPhase("mcp__custom-tools__bash", { command: "pnpm test" }), "checking");
211
+ assert.equal(inferOddPhase("mcp__custom-tools__mem_save", {}), undefined);
212
+ assert.equal(inferOddPhase("mcp__custom-tools__subagent_run", { agent: "gentle-ai-worker" }), "implementing");
213
+ });