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
@@ -0,0 +1,324 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { chmodSync, constants, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, statSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import test from "node:test";
8
+ import { gentlePiConfigHome } from "../lib/agent-home.ts";
9
+ import { gentleAiDevBinaryRegistrationPath, gentleAiIsolationEnv } from "../lib/gentle-ai-binary.ts";
10
+ import { GENTLE_SHELL_CONFIG_HOME_ENV, setBundledInstallForTesting } from "../lib/gentle-shell-bundled.ts";
11
+ import { buildPiInvocation, migrateStandaloneConfig, standaloneConfigHome, type ResolvedHome } from "../lib/gentle-shell-launcher.ts";
12
+ import { createActiveProfileReader } from "../extensions/gentle-shell.ts";
13
+ import { readBannerConfig } from "../extensions/startup-banner.ts";
14
+
15
+ // #2201 (S1, S1a): the standalone launcher's isolated home keeps Gentle Shell's
16
+ // config next to its agent dir (~/.gentle-shell/gentle-ai), not in the shared
17
+ // ~/.pi/gentle-ai that a Pi install with gentle-pi also uses.
18
+
19
+ const posixOnly = { skip: process.platform === "win32" };
20
+ const binPath = fileURLToPath(new URL("../bin/gentle-shell.mjs", import.meta.url));
21
+ const MARKER = "GENTLE_SHELL_CONFIG_HOME";
22
+
23
+ function scratch(): string {
24
+ return realpathSync(mkdtempSync(join(tmpdir(), "standalone-config-home-")));
25
+ }
26
+
27
+ function launch(home: ResolvedHome, baseEnv: Record<string, string | undefined> = {}, bundled = false) {
28
+ return buildPiInvocation({
29
+ runtime: { kind: "path", command: "fake-pi", args: [] },
30
+ home,
31
+ packageRoot: "/pkg",
32
+ declaration: undefined,
33
+ takeOver: false,
34
+ otherPackagePaths: [],
35
+ passthrough: [],
36
+ baseEnv,
37
+ homedir: "/os-home",
38
+ cwd: "/cwd",
39
+ bundled,
40
+ });
41
+ }
42
+
43
+ function isolated(dir = "/os-home/.gentle-shell/agent"): { home: ResolvedHome } {
44
+ return { home: { mode: "isolated", dir, source: "default" } };
45
+ }
46
+
47
+ test("the marker is exported under its launcher-owned name, never GENTLE_PI_CONFIG_HOME", () => {
48
+ assert.equal(GENTLE_SHELL_CONFIG_HOME_ENV, MARKER);
49
+ });
50
+
51
+ test("config home precedence: explicit GENTLE_PI_CONFIG_HOME > launcher marker > ~/.pi/gentle-ai; empty counts as unset", () => {
52
+ const home = scratch();
53
+ const env = (extra: Record<string, string>) => ({ HOME: home, USERPROFILE: home, ...extra });
54
+ const previous = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE };
55
+ process.env.HOME = home;
56
+ process.env.USERPROFILE = home;
57
+ try {
58
+ assert.equal(gentlePiConfigHome(env({})), join(home, ".pi", "gentle-ai"));
59
+ assert.equal(gentlePiConfigHome(env({ [MARKER]: "/shell/gentle-ai" })), "/shell/gentle-ai");
60
+ assert.equal(gentlePiConfigHome(env({ [MARKER]: "/shell/gentle-ai", GENTLE_PI_CONFIG_HOME: "/explicit" })), "/explicit");
61
+ assert.equal(gentlePiConfigHome(env({ [MARKER]: "/shell/gentle-ai", GENTLE_PI_CONFIG_HOME: "" })), "/shell/gentle-ai");
62
+ assert.equal(gentlePiConfigHome(env({ [MARKER]: "" })), join(home, ".pi", "gentle-ai"));
63
+ assert.equal(gentlePiConfigHome(env({ [MARKER]: "relative/gentle-ai" })), join(home, ".pi", "gentle-ai"), "a relative marker is ignored");
64
+ } finally {
65
+ for (const [key, value] of Object.entries(previous)) {
66
+ if (value === undefined) delete process.env[key];
67
+ else process.env[key] = value;
68
+ }
69
+ }
70
+ });
71
+
72
+ test("a bundled prefix still outranks the explicit override and the marker", (t) => {
73
+ const prefix = scratch();
74
+ setBundledInstallForTesting({ root: prefix });
75
+ t.after(() => setBundledInstallForTesting(undefined));
76
+ assert.equal(
77
+ gentlePiConfigHome({ GENTLE_SHELL_PREFIX: prefix, GENTLE_PI_CONFIG_HOME: "/explicit", [MARKER]: "/shell/gentle-ai" }),
78
+ join(prefix, "config", "gentle-ai"),
79
+ );
80
+ });
81
+
82
+ test("the banner and profile readers follow the same resolver, including an empty GENTLE_PI_CONFIG_HOME", async (t) => {
83
+ const root = scratch();
84
+ const config = join(root, "shell", "gentle-ai");
85
+ mkdirSync(config, { recursive: true });
86
+ writeFileSync(join(config, "banner.json"), JSON.stringify({ showTextLogo: false }));
87
+ writeFileSync(join(config, "profiles.json"), JSON.stringify({ kind: "gentle-pi.agent_model_profiles", version: 1, active: "fast", profiles: { fast: {} } }));
88
+ const previous = { marker: process.env[MARKER], explicit: process.env.GENTLE_PI_CONFIG_HOME };
89
+ process.env[MARKER] = config;
90
+ process.env.GENTLE_PI_CONFIG_HOME = "";
91
+ t.after(() => {
92
+ if (previous.marker === undefined) delete process.env[MARKER];
93
+ else process.env[MARKER] = previous.marker;
94
+ if (previous.explicit === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
95
+ else process.env.GENTLE_PI_CONFIG_HOME = previous.explicit;
96
+ });
97
+ assert.equal((await readBannerConfig()).showTextLogo, false);
98
+ assert.equal(createActiveProfileReader({ [MARKER]: config, GENTLE_PI_CONFIG_HOME: "" })(), "fast");
99
+ });
100
+
101
+ test("the launcher gives Pi the marker only in its isolated home", () => {
102
+ const isolatedLaunch = launch(isolated().home);
103
+ assert.equal(isolatedLaunch.env[MARKER], "/os-home/.gentle-shell/gentle-ai");
104
+ assert.equal(isolatedLaunch.env.GENTLE_PI_CONFIG_HOME, undefined, "GENTLE_PI_CONFIG_HOME is never exported to Pi");
105
+ assert.equal(standaloneConfigHome(isolated().home), "/os-home/.gentle-shell/gentle-ai");
106
+ // A custom GENTLE_SHELL_HOME's parent may be anyone's folder (~/myagent ->
107
+ // ~/gentle-ai), so only a .gentle-shell/agent home gets its own config folder.
108
+ assert.equal(launch(isolated("/custom/shell/agent").home).env[MARKER], undefined, "no marker beside a custom GENTLE_SHELL_HOME");
109
+ assert.equal(standaloneConfigHome(isolated("/os-home/myagent").home), undefined, "never the generic ~/gentle-ai");
110
+ assert.equal(standaloneConfigHome(isolated("/data/.gentle-shell/agent").home), "/data/.gentle-shell/gentle-ai");
111
+ });
112
+
113
+ test("--link, a custom --home and a bundled launch keep today's config home and drop an inherited marker", () => {
114
+ const inherited = { [MARKER]: "/outer/gentle-ai" };
115
+ const link = launch({ mode: "link", dir: "/os-home/.pi/agent", source: "flag" }, inherited);
116
+ const path = launch({ mode: "path", dir: "/custom/home", source: "flag" }, inherited);
117
+ const bundled = launch(isolated().home, inherited, true);
118
+ for (const invocation of [link, path, bundled]) assert.equal(MARKER in invocation.env, false);
119
+ assert.equal(standaloneConfigHome({ mode: "link", dir: "/os-home/.pi/agent", source: "flag" }), undefined);
120
+ assert.equal(standaloneConfigHome({ mode: "path", dir: "/custom/home", source: "flag" }), undefined);
121
+ });
122
+
123
+ test("Gentle AI processes started from a standalone isolated session get the bundled-style config pair", () => {
124
+ const env = gentleAiIsolationEnv({ PATH: "/bin", [MARKER]: "/os-home/.gentle-shell/gentle-ai" });
125
+ assert.equal(env.GENTLE_AI_PI_CONFIG_DIR, "/os-home/.gentle-shell/gentle-ai");
126
+ assert.equal(env.GENTLE_PI_CONFIG_HOME, "/os-home/.gentle-shell", "Gentle AI 4.0.0 reads it as the parent of its gentle-ai folder");
127
+ assert.equal(env.PATH, "/bin");
128
+ assert.equal(env.GENTLE_AI_HOME, undefined, "standalone keeps Gentle AI's own state where it was");
129
+ });
130
+
131
+ test("without the marker, or with an explicit GENTLE_PI_CONFIG_HOME, Gentle AI's environment is unchanged", () => {
132
+ const plain = { PATH: "/bin" };
133
+ assert.equal(gentleAiIsolationEnv(plain), plain);
134
+ const explicit = { PATH: "/bin", GENTLE_PI_CONFIG_HOME: "/explicit", [MARKER]: "/os-home/.gentle-shell/gentle-ai" };
135
+ assert.equal(gentleAiIsolationEnv(explicit), explicit);
136
+ });
137
+
138
+ test("the main-channel dev-binary registration ignores the marker and an empty GENTLE_PI_CONFIG_HOME", () => {
139
+ assert.equal(
140
+ gentleAiDevBinaryRegistrationPath({ env: { [MARKER]: "/os-home/.gentle-shell/gentle-ai", GENTLE_PI_CONFIG_HOME: "" }, home: "/os-home" }),
141
+ join("/os-home", ".pi", "gentle-ai", "dev-binary.json"),
142
+ );
143
+ assert.equal(gentleAiDevBinaryRegistrationPath({ env: { GENTLE_PI_CONFIG_HOME: "/explicit" }, home: "/os-home" }), join("/explicit", "dev-binary.json"));
144
+ });
145
+
146
+ // --- migration -----------------------------------------------------------------
147
+
148
+ // The same real filesystem bin/gentle-shell.mjs gives migrateStandaloneConfig.
149
+ const fs = {
150
+ kind: (path: string) => {
151
+ try {
152
+ const stat = lstatSync(path);
153
+ return stat.isDirectory() ? "dir" as const : stat.isFile() ? "file" as const : "other" as const;
154
+ } catch {
155
+ return "missing" as const;
156
+ }
157
+ },
158
+ list: (path: string) => readdirSync(path),
159
+ mkdirPrivate: (path: string) => { mkdirSync(path, { recursive: true, mode: 0o700 }); },
160
+ copyNew: (from: string, to: string) => copyFileSync(from, to, constants.COPYFILE_EXCL),
161
+ };
162
+
163
+ function migrationWorld() {
164
+ const root = scratch();
165
+ const source = join(root, "home", ".pi", "gentle-ai");
166
+ const target = join(root, "home", ".gentle-shell", "gentle-ai");
167
+ mkdirSync(source, { recursive: true });
168
+ mkdirSync(dirname(target), { recursive: true });
169
+ writeFileSync(join(source, "banner.json"), "{\"showTextLogo\":false}\n");
170
+ writeFileSync(join(source, "profiles.json"), "{\"active\":\"fast\"}\n");
171
+ writeFileSync(join(source, "dev-binary.json"), "{}\n");
172
+ writeFileSync(join(source, "channel.json"), "{}\n");
173
+ mkdirSync(join(source, "main", "gentle-ai"), { recursive: true });
174
+ writeFileSync(join(source, "main", "gentle-ai", "marker"), "x");
175
+ return { root, source, target };
176
+ }
177
+
178
+ function snapshotTree(dir: string): Record<string, string> {
179
+ const out: Record<string, string> = {};
180
+ for (const name of readdirSync(dir)) {
181
+ const path = join(dir, name);
182
+ const stat = lstatSync(path);
183
+ if (stat.isDirectory()) for (const [key, value] of Object.entries(snapshotTree(path))) out[join(name, key)] = value;
184
+ else out[name] = stat.isSymbolicLink() ? "symlink" : `${stat.mode}:${stat.mtimeMs}:${readFileSync(path, "utf8")}`;
185
+ }
186
+ return out;
187
+ }
188
+
189
+ test("migration copies the regular files once into a new 0700 folder and never changes the source", posixOnly, () => {
190
+ const w = migrationWorld();
191
+ symlinkSync(join(w.source, "banner.json"), join(w.source, "linked.json"));
192
+ const before = snapshotTree(w.source);
193
+ const result = migrateStandaloneConfig({ source: w.source, target: w.target, fs });
194
+ assert.deepEqual(readdirSync(w.target).sort(), ["banner.json", "profiles.json"]);
195
+ assert.equal(readFileSync(join(w.target, "banner.json"), "utf8"), "{\"showTextLogo\":false}\n");
196
+ assert.equal(statSync(w.target).mode & 0o777, 0o700);
197
+ assert.deepEqual(snapshotTree(w.source), before);
198
+ assert.equal(result.lines.length, 1);
199
+ assert.match(result.lines[0], new RegExp(`${w.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*${w.target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
200
+ });
201
+
202
+ test("migration skips the main-channel files and does nothing once the folder exists", posixOnly, () => {
203
+ const w = migrationWorld();
204
+ migrateStandaloneConfig({ source: w.source, target: w.target, fs });
205
+ assert.equal(existsSync(join(w.target, "dev-binary.json")), false);
206
+ assert.equal(existsSync(join(w.target, "channel.json")), false);
207
+ assert.equal(existsSync(join(w.target, "main")), false);
208
+ writeFileSync(join(w.target, "banner.json"), "{\"color\":\"cyan\"}\n");
209
+ writeFileSync(join(w.source, "vim.json"), "{}\n");
210
+ const again = migrateStandaloneConfig({ source: w.source, target: w.target, fs });
211
+ assert.deepEqual(again.lines, []);
212
+ assert.equal(readFileSync(join(w.target, "banner.json"), "utf8"), "{\"color\":\"cyan\"}\n", "never overwrites");
213
+ assert.equal(existsSync(join(w.target, "vim.json")), false, "runs once");
214
+ });
215
+
216
+ test("without ~/.pi/gentle-ai the migration only creates the empty 0700 folder, silently", posixOnly, () => {
217
+ const root = scratch();
218
+ const target = join(root, ".gentle-shell", "gentle-ai");
219
+ const result = migrateStandaloneConfig({ source: join(root, ".pi", "gentle-ai"), target, fs });
220
+ assert.deepEqual(result.lines, []);
221
+ assert.deepEqual(readdirSync(target), []);
222
+ assert.equal(statSync(target).mode & 0o777, 0o700);
223
+ });
224
+
225
+ test("a failed copy warns and keeps whatever was copied; it never throws", posixOnly, () => {
226
+ const w = migrationWorld();
227
+ chmodSync(join(w.source, "profiles.json"), 0o000);
228
+ if (spawnSync("cat", [join(w.source, "profiles.json")]).status === 0) return; // running as root: nothing is unreadable
229
+ const result = migrateStandaloneConfig({ source: w.source, target: w.target, fs });
230
+ chmodSync(join(w.source, "profiles.json"), 0o644);
231
+ assert.deepEqual(readdirSync(w.target), ["banner.json"]);
232
+ assert.equal(result.lines.length, 1);
233
+ assert.match(result.lines[0], /could not copy/);
234
+ assert.match(result.lines[0], /profiles\.json/);
235
+ const blocked = scratch();
236
+ writeFileSync(join(blocked, "not-a-dir"), "");
237
+ const failed = migrateStandaloneConfig({ source: w.source, target: join(blocked, "not-a-dir", "gentle-ai"), fs });
238
+ assert.equal(failed.lines.length, 1);
239
+ assert.match(failed.lines[0], /could not/);
240
+ });
241
+
242
+ // --- the real launcher -----------------------------------------------------------
243
+
244
+ function writeFakePi(path: string) {
245
+ writeFileSync(path, [
246
+ "#!/usr/bin/env node",
247
+ "const args = process.argv.slice(2);",
248
+ "if (args.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
249
+ `console.log(JSON.stringify({ marker: process.env.${MARKER} ?? null, explicit: process.env.GENTLE_PI_CONFIG_HOME ?? null }));`,
250
+ "",
251
+ ].join("\n"));
252
+ chmodSync(path, 0o755);
253
+ }
254
+
255
+ function writeFakeGentleAi(path: string) {
256
+ writeFileSync(path, [
257
+ "#!/usr/bin/env node",
258
+ "import { mkdirSync, writeFileSync } from 'node:fs';",
259
+ "import { homedir } from 'node:os';",
260
+ "import { join } from 'node:path';",
261
+ "for (const dir of [process.env.GENTLE_AI_PI_CONFIG_DIR, join(homedir(), '.pi', 'gentle-ai')]) {",
262
+ " if (!dir) continue;",
263
+ " mkdirSync(dir, { recursive: true });",
264
+ " writeFileSync(join(dir, 'persona.json'), JSON.stringify({ mode: 'gentleman' }));",
265
+ "}",
266
+ "process.stdout.write(JSON.stringify({ dir: process.env.GENTLE_AI_PI_CONFIG_DIR ?? null, parent: process.env.GENTLE_PI_CONFIG_HOME ?? null }) + '\\n');",
267
+ "",
268
+ ].join("\n"));
269
+ chmodSync(path, 0o755);
270
+ }
271
+
272
+ function launcherWorld() {
273
+ const root = scratch();
274
+ const home = join(root, "home");
275
+ mkdirSync(join(home, ".pi", "gentle-ai"), { recursive: true });
276
+ writeFileSync(join(home, ".pi", "gentle-ai", "banner.json"), "{\"showTextLogo\":false}\n");
277
+ const pi = join(root, "fake-pi.mjs");
278
+ writeFakePi(pi);
279
+ const env: NodeJS.ProcessEnv = { PATH: process.env.PATH, HOME: home, USERPROFILE: home, GENTLE_SHELL_PI: pi, GENTLE_SHELL_NO_AUTO_SETUP: "1" };
280
+ const run = (args: string[], extra: NodeJS.ProcessEnv = {}) => spawnSync(process.execPath, [binPath, ...args], { encoding: "utf8", env: { ...env, ...extra } });
281
+ return { root, home, env, run, shellConfig: join(home, ".gentle-shell", "gentle-ai") };
282
+ }
283
+
284
+ test("a standalone isolated launch migrates ~/.pi/gentle-ai and points Pi at ~/.gentle-shell/gentle-ai", posixOnly, () => {
285
+ const w = launcherWorld();
286
+ const result = w.run(["list"]);
287
+ assert.equal(result.status, 0, result.stderr);
288
+ assert.deepEqual(JSON.parse(result.stdout.trim().split("\n").at(-1)!), { marker: w.shellConfig, explicit: null });
289
+ assert.equal(readFileSync(join(w.shellConfig, "banner.json"), "utf8"), "{\"showTextLogo\":false}\n");
290
+ assert.equal(readFileSync(join(w.home, ".pi", "gentle-ai", "banner.json"), "utf8"), "{\"showTextLogo\":false}\n");
291
+ assert.equal(result.stderr.split("\n").filter((line) => line.includes(w.shellConfig) && line.includes("copied")).length, 1);
292
+ const second = w.run(["list"]);
293
+ assert.doesNotMatch(second.stderr, /copied/);
294
+ });
295
+
296
+ test("--link launches keep ~/.pi/gentle-ai: no marker and no migration", posixOnly, () => {
297
+ const w = launcherWorld();
298
+ mkdirSync(join(w.home, ".pi", "agent"), { recursive: true });
299
+ const result = w.run(["--link", "list"], { [MARKER]: "/outer/gentle-ai" });
300
+ assert.equal(result.status, 0, result.stderr);
301
+ assert.deepEqual(JSON.parse(result.stdout.trim().split("\n").at(-1)!), { marker: null, explicit: null });
302
+ assert.equal(existsSync(w.shellConfig), false);
303
+ });
304
+
305
+ test("an explicit GENTLE_PI_CONFIG_HOME still wins: it reaches Pi and no migration runs", posixOnly, () => {
306
+ const w = launcherWorld();
307
+ const result = w.run(["list"], { GENTLE_PI_CONFIG_HOME: join(w.root, "explicit") });
308
+ assert.equal(result.status, 0, result.stderr);
309
+ assert.equal(JSON.parse(result.stdout.trim().split("\n").at(-1)!).explicit, join(w.root, "explicit"));
310
+ assert.equal(existsSync(w.shellConfig), false);
311
+ });
312
+
313
+ test("standalone setup gives Gentle AI the config pair and keeps both persona files unchanged", posixOnly, () => {
314
+ const w = launcherWorld();
315
+ const gentleAi = join(w.root, "fake-gentle-ai.mjs");
316
+ writeFakeGentleAi(gentleAi);
317
+ writeFileSync(join(w.home, ".pi", "gentle-ai", "persona.json"), "{\"mode\":\"neutral\"}");
318
+ const result = w.run(["setup"], { GENTLE_SHELL_GENTLE_AI_BIN: gentleAi });
319
+ assert.equal(result.status, 0, result.stderr);
320
+ const seen = JSON.parse(result.stdout.split("\n").find((line) => line.includes("\"dir\""))!);
321
+ assert.deepEqual(seen, { dir: w.shellConfig, parent: join(w.home, ".gentle-shell") });
322
+ assert.equal(readFileSync(join(w.home, ".pi", "gentle-ai", "persona.json"), "utf8"), "{\"mode\":\"neutral\"}");
323
+ assert.equal(readFileSync(join(w.shellConfig, "persona.json"), "utf8"), "{\"mode\":\"neutral\"}", "the migrated copy is restored too");
324
+ });
@@ -462,19 +462,20 @@ test("delegated children never paint the startup banner", async (t) => {
462
462
 
463
463
  // S9: the config home each of these reads resolves, per case. The OS home is
464
464
  // a temporary folder in every case, so no case can reach the real one.
465
- // "unset" and "empty" are exactly as before: an empty GENTLE_PI_CONFIG_HOME
466
- // names the current folder. "bundled" stands in for this package running from
465
+ // "unset" and "empty" both name ~/.pi/gentle-ai: an empty GENTLE_PI_CONFIG_HOME
466
+ // counts as unset (#2201; it used to name the current folder). "bundled" stands in for this package running from
467
467
  // a bundled install (testing overlay) whose prefix the launcher passed; the
468
468
  // user's own GENTLE_PI_CONFIG_HOME is then not this install's.
469
469
  type ConfigHomeCase = "unset" | "empty" | "bundled";
470
470
  function configHomeCase(t: { after: (fn: () => void) => void }, kind: ConfigHomeCase): { env: NodeJS.ProcessEnv; dir: string; userConfig: string } {
471
471
  const base = realpathSync(mkdtempSync(join(tmpdir(), `config-home-${kind}-`)));
472
- const saved = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, GENTLE_PI_CONFIG_HOME: process.env.GENTLE_PI_CONFIG_HOME, GENTLE_SHELL_PREFIX: process.env.GENTLE_SHELL_PREFIX };
472
+ const saved = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, GENTLE_PI_CONFIG_HOME: process.env.GENTLE_PI_CONFIG_HOME, GENTLE_SHELL_PREFIX: process.env.GENTLE_SHELL_PREFIX, GENTLE_SHELL_CONFIG_HOME: process.env.GENTLE_SHELL_CONFIG_HOME };
473
473
  const cwd = process.cwd();
474
474
  process.env.HOME = join(base, "home");
475
475
  process.env.USERPROFILE = join(base, "home");
476
476
  delete process.env.GENTLE_PI_CONFIG_HOME;
477
477
  delete process.env.GENTLE_SHELL_PREFIX;
478
+ delete process.env.GENTLE_SHELL_CONFIG_HOME;
478
479
  t.after(() => {
479
480
  process.chdir(cwd);
480
481
  setBundledInstallForTesting(undefined);
@@ -489,7 +490,7 @@ function configHomeCase(t: { after: (fn: () => void) => void }, kind: ConfigHome
489
490
  if (kind === "empty") {
490
491
  mkdirSync(join(base, "cwd"));
491
492
  process.chdir(join(base, "cwd"));
492
- return { env: { GENTLE_PI_CONFIG_HOME: "" }, dir: join(base, "cwd"), userConfig };
493
+ return { env: { GENTLE_PI_CONFIG_HOME: "" }, dir: join(base, "home", ".pi", "gentle-ai"), userConfig };
493
494
  }
494
495
  const prefix = join(base, "prefix");
495
496
  mkdirSync(prefix);
@@ -168,3 +168,37 @@ test("startup returns before the native review negotiation finishes, and the fir
168
168
  await __testing.startupNegotiationSettled();
169
169
  assert.equal(calls.target, 1, "the negotiation still completes after startup");
170
170
  });
171
+
172
+ // gentle-shell#2195: the RDD line sits in the cached system-prompt prefix, so a
173
+ // session keeps the line it first rendered and re-renders only when RDD turns
174
+ // off or back on.
175
+ test("a session keeps its first RDD status line until the off state changes", async (t) => {
176
+ const { cwd, environment } = fixture(t, false);
177
+ let status: { effective: string; source: string } | undefined;
178
+ const cli = {
179
+ reviewMode: async () => {
180
+ if (status === undefined) throw new Error("native review CLI unavailable");
181
+ return { operation: "status", scope: "clone", status: { global: "", cloneLocal: status.effective, ...status } };
182
+ },
183
+ } as unknown as NativeReviewCli;
184
+ const { handlers, ctx } = start(cli, cwd, environment);
185
+ const line = async () => {
186
+ __testing.clearRddStatusMemoForTesting();
187
+ const event = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
188
+ await handlers.get("before_agent_start")!(event, ctx);
189
+ return /Receipt-driven development: [^;\n]*/.exec(event.systemPromptOptions.appendSystemPrompt)?.[0];
190
+ };
191
+ await handlers.get("session_shutdown")!({}, ctx);
192
+ assert.equal(await line(), "Receipt-driven development: unknown (native status unavailable)");
193
+ status = { effective: "on", source: "clone_local" };
194
+ assert.equal(await line(), "Receipt-driven development: unknown (native status unavailable)", "unknown to on keeps the prompt prefix");
195
+ status = { effective: "on", source: "global" };
196
+ assert.equal(await line(), "Receipt-driven development: unknown (native status unavailable)", "a source change keeps the prompt prefix");
197
+ status = { effective: "off", source: "clone_local" };
198
+ assert.equal(await line(), "Receipt-driven development: off (decided by clone_local)", "turning RDD off re-renders");
199
+ status = { effective: "on", source: "global" };
200
+ assert.equal(await line(), "Receipt-driven development: on (decided by global)", "turning RDD back on re-renders");
201
+ await handlers.get("session_shutdown")!({}, ctx);
202
+ status = { effective: "on", source: "clone_local" };
203
+ assert.equal(await line(), "Receipt-driven development: on (decided by clone_local)", "a new session renders the current line");
204
+ });
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  // so a moved clause stays pinned instead of silently disappearing.
7
7
  export const DELEGATION_MODULES = [
8
8
  "orchestrator-delegation.md",
9
+ "orchestrator-delegation-reference.md",
9
10
  "orchestrator-tracking.md",
10
11
  "orchestrator-verification.md",
11
12
  "orchestrator-writer.md",
@@ -80,7 +80,8 @@ test("AC3: small work runs its checks inline and keeps test-first", () => {
80
80
  test("AC4: each mechanism turns on only by its own trigger and size is re-evaluated after", () => {
81
81
  const triggers = sectionOf(core, "## Mechanisms");
82
82
  for (const clause of [
83
- "each mechanism turns on only by its own trigger",
83
+ // Writer-rule fix: a trigger holds only when its condition truly holds.
84
+ "each mechanism turns on only when its own trigger condition truly holds",
84
85
  "re-evaluate task size",
85
86
  "1. **Ask**",
86
87
  "2. **Evidence-budget rule**",
@@ -90,7 +91,8 @@ test("AC4: each mechanism turns on only by its own trigger and size is re-evalua
90
91
  "6. **Incident rule**",
91
92
  "7. **Context backstop**",
92
93
  "open product or design decision",
93
- "high risk",
94
+ // Verification by named risk: rule 3 names the escalated high-risk item.
95
+ "named high-risk item (assess `escalate`)",
94
96
  "never by file count",
95
97
  // gentle-shell#1731: the writer fires on reasons, not on size alone.
96
98
  "never by file count or a large task alone",
@@ -134,15 +136,29 @@ test("AC5: the high-risk list lives once in the core, native tier wins, unclear
134
136
  const MODULE_BUDGETS: Record<string, number> = {
135
137
  // gentle-shell#1731 merge with main: main's session subject/state guidance plus the reason-based Writer rule (20,000 -> 20,500 B).
136
138
  // gentle-shell#1269: capability-compatible routing rule checked before delegation (20,500 -> 20,800 B).
137
- "orchestrator-delegation.md": 20_800,
139
+ // S9 canon change: conditional triggers, behavior-unit writer threshold, named-risk verifier, invariants,
140
+ // and the PRESERVE differential pointer mirrored from gentle-ai 5f6faa16 (20,800 -> 21,900 B).
141
+ // Pain-point iteration S11 T2: the parent read this module in most runs and kept ~5K tokens of it in the
142
+ // transcript on every later call, so it became the lean delegation card (21,900 -> 7,000 B) and the
143
+ // reference material moved to orchestrator-delegation-reference.md, read only when a situation names it.
144
+ // Pain-point iteration S13 (C10): the Preparation trigger tells the parent not to read what the worker will
145
+ // read and to pass paths, not content (7,000 -> 7,100 B; every other card sentence is pinned by a contract).
146
+ // S15: units of about 100 changed lines or less always run inline (7,100 -> 7,300 B).
147
+ "orchestrator-delegation.md": 7_300,
148
+ "orchestrator-delegation-reference.md": 14_800,
138
149
  // gentle-shell#1731 T23: Close gate accepts one Needs your decision result as a stop (12,500 -> 12,600 B).
139
- "orchestrator-tracking.md": 12_600,
150
+ // S14: close the open slice before a high-risk commit; outcomes per commit (12,600 -> 12,850 B).
151
+ "orchestrator-tracking.md": 12_850,
140
152
  // gentle-shell#1731 T3: parallel review protocol; lazy (delegation or high risk only), core and normative rule untouched.
141
153
  // gentle-shell#1731 T11/T12: verify-per-unit timing and the every-S# verify handoff (6,000 -> 6,400 B).
142
154
  // gentle-shell#1731 T15: one end-of-feature verify for same-model inline deliveries (6,400 -> 6,500 B).
143
155
  // gentle-shell#1731 T23: bounded self-review and partial/blocked writer -> Needs your decision (7,000 -> 7,300 B).
144
- "orchestrator-verification.md": 7_300,
145
- "orchestrator-writer.md": 4_500,
156
+ // Verification by named risk: escalate-only tier table and the concrete-occasions rule (7,300 -> 7,500 B).
157
+ "orchestrator-verification.md": 7_500,
158
+ // Writer-rule fix (size threshold, one behavior per unit) and C3 (writer prompts name the helpers and
159
+ // invariants to reuse): 4,500 -> 5,300 B.
160
+ // Pain-point iteration S10 (C7): writer handoffs keep test scratch outside the worktree (5,300 -> 5,500 B).
161
+ "orchestrator-writer.md": 5_500,
146
162
  "orchestrator-prompts.md": 13_000,
147
163
  };
148
164
 
@@ -167,6 +183,46 @@ test("AC7: each delegation module stays under its byte budget and is loaded by i
167
183
  }
168
184
  });
169
185
 
186
+ // Pain-point iteration S11 T2: once read, a lazy module rides along on every later model call, so the
187
+ // default delegation read must be the lean card; reference material loads only when a pointer names it.
188
+ test("S11: the default delegation pointer resolves to the lean card and the reference loads only by name", () => {
189
+ const card = read("assets/orchestrator-delegation.md");
190
+ const reference = read("assets/orchestrator-delegation-reference.md");
191
+ assert.ok(Buffer.byteLength(card, "utf8") <= MODULE_BUDGETS["orchestrator-delegation.md"], "the delegation card is over its budget");
192
+ // Every core line that names a delegation module names the card, except the one reference pointer.
193
+ const pointerLines = core.split("\n").filter((line) => line.includes("`orchestrator-delegation"));
194
+ const referenceLines = pointerLines.filter((line) => line.includes("`orchestrator-delegation-reference.md`"));
195
+ assert.equal(referenceLines.length, 1, "the core names the reference exactly once");
196
+ for (const name of ["Per-action table", "Work Routing Ladder", "Canonical Workflows"]) {
197
+ assert.ok(referenceLines[0].includes(name), `the reference pointer must name ${name}`);
198
+ }
199
+ assert.ok(pointerLines.some((line) => line.includes("`orchestrator-delegation.md`")), "the core must point at the card");
200
+ // The card carries what the parent needs at the moment it delegates.
201
+ for (const clause of [
202
+ "#### Delegation Triggers",
203
+ "Each trigger holds only when its condition truly holds",
204
+ "A unit is one independent piece of behavior with its code, tests, and docs together",
205
+ "`## Allowed edit surfaces`",
206
+ "Spec: odd/tasks/<feature>.md (read until \"## Log\"). Do T#; S#.",
207
+ "`## Skills to load before work`",
208
+ "`orchestrator-verification.md`",
209
+ "#### Key Learnings closing block",
210
+ "disjoint Allowed edit surfaces",
211
+ "Subagent-facing prompts should be written in English by default",
212
+ "technical artifacts default to English",
213
+ "returns a task id",
214
+ "`orchestrator-delegation-reference.md`",
215
+ ]) {
216
+ assert.ok(card.includes(clause), `the delegation card is missing: ${clause}`);
217
+ }
218
+ // Reference material lives once, in the reference.
219
+ for (const heading of ["### Work Routing Ladder", "#### Background Subagent Policy", "### Canonical Lightweight Workflows", "#### Pi Subagent Model Routing"]) {
220
+ assert.ok(reference.includes(heading), `the reference is missing: ${heading}`);
221
+ assert.ok(!card.includes(heading), `the card still carries reference material: ${heading}`);
222
+ }
223
+ assert.ok(!reference.includes("#### Delegation Triggers"), "the triggers live once, in the card");
224
+ });
225
+
170
226
  // gentle-shell#1269/#1598: the capability rule is the substantive part of the fix,
171
227
  // so losing it must fail a test instead of quietly passing the AC7 budget gate.
172
228
  // Focused on the two claims that carry the fix, not on the exact wording around them.
@@ -178,9 +234,11 @@ test("delegation reports a capability gap instead of substituting inline work fo
178
234
  });
179
235
 
180
236
  test("work usage stays complete in the human guide while routing precedence stays canonical", () => {
181
- const asset = read("assets/orchestrator-delegation.md");
237
+ // S11 T2: the work-publication pointer and the runtime bindings moved to the reference; the writer
238
+ // fallback stays in the card, so the counts read the card plus the reference.
239
+ const asset = `${read("assets/orchestrator-delegation.md")}\n${read("assets/orchestrator-delegation-reference.md")}`;
182
240
  const guide = read("docs/gentle-agents-activity.md");
183
- assert.ok(asset.includes("tool schemas and `docs/gentle-agents-activity.md`"));
241
+ assert.ok(read("assets/orchestrator-delegation-reference.md").includes("tool schemas and `docs/gentle-agents-activity.md`"));
184
242
  for (const clause of [
185
243
  '`{"area":"Auth","topic":"Login","tags":["Review"],"refs":[{"kind":"issue","repository":"github.com/Owner/Repo","id":"12"}]}`',
186
244
  '`filter: {"related_to":{"session_id":"<stable owner ID>"}}`',
@@ -216,8 +274,10 @@ test("T4: tracking updates edit in place and mirror without re-emitting the docu
216
274
  test("T6: each delegation module carries its own clauses and names the modules it depends on", () => {
217
275
  const placement: Record<string, readonly string[]> = {
218
276
  "orchestrator-tracking.md": ["#### Authorization and progress", "Delivery follows work units", "Raise a candidate you know is high risk"],
277
+ // The tier table header lost "Native risk" with verification by named
278
+ // risk: the high row is reached only through escalate, not a native tier.
219
279
  "orchestrator-verification.md": [
220
- "| Native risk tier | Verification (any RDD state) |",
280
+ "| Tier | Verification (any RDD state) |",
221
281
  "## Agent escalation (gentle-shell#1494)",
222
282
  "## Parallel review protocol (gentle-shell#1731)",
223
283
  ],
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // Pain-point iteration S10 (C7): blind reviewers flagged tool artifacts in every
7
+ // Gentle Shell delivery. Orchestrators told writers to create a scratch
8
+ // directory in the repository root and point TMPDIR at it, so test state and
9
+ // tool caches entered the diff. Test scratch now lives outside the worktree by
10
+ // default. These are instruction-delivery contracts, not proof of model adherence.
11
+
12
+ const ROOT = join(import.meta.dirname, "..");
13
+ const read = (path: string): string => readFileSync(join(ROOT, path), "utf8");
14
+ const worker = read("assets/agents/gentle-ai-worker.md");
15
+ const writer = read("assets/orchestrator-writer.md");
16
+ const verifier = read("assets/agents/gentle-ai-verify.md");
17
+
18
+ function section(text: string, heading: string): string {
19
+ const start = text.indexOf(`\n## ${heading}\n`);
20
+ assert.ok(start >= 0, `missing section: ${heading}`);
21
+ const end = text.indexOf("\n## ", start + heading.length + 4);
22
+ return text.slice(start, end < 0 ? undefined : end);
23
+ }
24
+
25
+ function containsAll(text: string, clauses: readonly string[], label: string): void {
26
+ for (const clause of clauses) assert.ok(text.includes(clause), `${label} is missing: ${clause}`);
27
+ }
28
+
29
+ test("S10: the worker keeps test scratch outside the worktree and checks git status before returning", () => {
30
+ const discipline = section(worker, "Test discipline");
31
+ containsAll(discipline, [
32
+ "Test scratch lives outside the worktree",
33
+ "`mktemp -d`",
34
+ "never point `TMPDIR` or a cache directory into the repository",
35
+ "use an excluded path that the test removes before it ends",
36
+ "Never leave tool caches or scratch paths in the change",
37
+ ], "worker test discipline");
38
+ const finish = discipline.split("\n").find((line) => line.includes("`git status --short`"));
39
+ assert.ok(finish, "worker finish checks must run `git status --short`");
40
+ containsAll(finish, ["Before returning", "no tool cache or scratch path", "`risks`"], "worker finish check");
41
+ });
42
+
43
+ test("S10: writer handoffs never place test scratch inside the repository", () => {
44
+ const line = writer.split("\n").find((entry) => entry.includes("Test scratch"));
45
+ assert.ok(line, "orchestrator-writer.md must state where writers put test scratch");
46
+ containsAll(line, [
47
+ "outside the worktree",
48
+ "`mktemp -d`",
49
+ "never in the repository",
50
+ "`TMPDIR`",
51
+ "excluded path",
52
+ ], "writer handoff scratch rule");
53
+ });
54
+
55
+ test("S10: no shipped guidance points test temp state into the repository", () => {
56
+ const offenders: string[] = [];
57
+ const walk = (dir: string): void => {
58
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
59
+ const path = join(dir, entry.name);
60
+ if (entry.isDirectory()) walk(path);
61
+ else if (/\.(md|ts)$/.test(entry.name) && /TMPDIR=\$(PWD|\(pwd\))|\.test-tmp|\.atl\/test-tmp/.test(readFileSync(path, "utf8"))) {
62
+ offenders.push(relative(ROOT, path));
63
+ }
64
+ }
65
+ };
66
+ for (const dir of ["assets", "extensions", "docs"]) walk(join(ROOT, dir));
67
+ assert.deepEqual(offenders, []);
68
+ });
69
+
70
+ // S10 follow-up (user, 2026-10-14: "lo de los temporales tambien hacemos limpieza luego?"):
71
+ // scratch outside the worktree was never removed; verifier scratch copies piled up
72
+ // in the system temp directory. The verifier removes only the exact directory its
73
+ // own `mktemp -d` printed; the worker, which may not delete, has tests clean up in
74
+ // teardown and names any leftover outside directory so the parent removes it.
75
+ test("S10: scratch outside the worktree is removed after use", () => {
76
+ // Independent verification of C7 found child safety blocks `rm -rf` in every
77
+ // subagent (lib/destructive-command-guard.ts), so a verifier told to delete its
78
+ // own scratch always failed and nobody removed it. Subagents now report exact
79
+ // scratch paths and the parent, where deletion is only confirmed, removes them.
80
+ containsAll(
81
+ verifier,
82
+ ["list every scratch directory you created by exact path in your result so the parent removes it", "never delete it yourself"],
83
+ "gentle-ai-verify scratch report",
84
+ );
85
+ assert.ok(!/\brm\s+-r/.test(verifier), "gentle-ai-verify must not instruct a deletion child safety blocks");
86
+ containsAll(
87
+ section(worker, "Test discipline"),
88
+ [
89
+ "Tests delete the temp directories they create in teardown",
90
+ "name any scratch directory you created outside the worktree in `risks` so the parent removes it",
91
+ ],
92
+ "worker scratch cleanup",
93
+ );
94
+ // Session scratch and artifact ledger: the runtime owns the parent session's
95
+ // TMPDIR (subagent TMPDIRs nest under it) and records clones and worktrees,
96
+ // so the writer rule points at the command instead of a model cleanup step.
97
+ containsAll(writer, ["The runtime owns session scratch", "`/gentle:scratch` lists or cleans recorded clones and worktrees"], "writer runtime-owned cleanup");
98
+ // The verifier's mktemp copy lives in its own TMPDIR, which the runner removes.
99
+ const verification = read("assets/orchestrator-verification.md");
100
+ containsAll(verification, ["the runtime removes each subagent's own TMPDIR, which holds that copy"], "verification runtime cleanup");
101
+ assert.ok(!verification.includes("the parent removes the scratch paths the verifier reports"), "the parent no longer removes verifier scratch");
102
+ });