gentle-pi 2.6.4 → 3.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 (128) hide show
  1. package/README.md +25 -7
  2. package/assets/agents/gentle-ai-worker.md +5 -1
  3. package/assets/agents/sdd-apply.md +9 -7
  4. package/assets/agents/sdd-archive.md +42 -23
  5. package/assets/agents/sdd-proposal.md +2 -2
  6. package/assets/agents/sdd-remediate.md +4 -4
  7. package/assets/agents/sdd-research.md +20 -48
  8. package/assets/agents/sdd-tasks.md +5 -5
  9. package/assets/agents/sdd-verify.md +6 -28
  10. package/assets/chains/sdd-full.chain.md +4 -22
  11. package/assets/chains/sdd-verify.chain.md +3 -12
  12. package/assets/orchestrator-delegation.md +33 -3
  13. package/assets/orchestrator-memory.md +20 -7
  14. package/assets/orchestrator.md +5 -3
  15. package/assets/sdd-orchestrator-workflow.md +25 -58
  16. package/assets/support/sdd-status-contract.md +9 -12
  17. package/docs/gentle-shell.md +41 -19
  18. package/docs/readme-reference.md +175 -36
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +289 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +123 -97
  23. package/extensions/pi-pretty.ts +63 -14
  24. package/extensions/quiet-tools.ts +1 -2
  25. package/extensions/startup-banner.ts +10 -9
  26. package/lib/agent-home.ts +8 -0
  27. package/lib/agent-profile-pin.ts +336 -0
  28. package/lib/agent-profiles.ts +28 -8
  29. package/lib/agents-config.ts +24 -2
  30. package/lib/agents-history.ts +3 -97
  31. package/lib/agents-keys.ts +27 -0
  32. package/lib/agents-protocol.ts +2 -15
  33. package/lib/agents-runner.ts +74 -113
  34. package/lib/agents-session-transport.ts +691 -0
  35. package/lib/command-palette-catalog.ts +87 -0
  36. package/lib/command-palette.ts +346 -0
  37. package/lib/native-choice-list.ts +5 -0
  38. package/lib/native-review-cli.ts +19 -97
  39. package/lib/review-publication-gate.ts +11 -1
  40. package/lib/review-repository.ts +1 -1
  41. package/lib/review-snapshot.ts +1 -0
  42. package/lib/review-transaction.ts +4 -2
  43. package/lib/sdd-preflight.ts +2 -1
  44. package/lib/sdd-research-capabilities.ts +18 -152
  45. package/lib/sdd-status.ts +7 -779
  46. package/lib/session-change-capture.ts +88 -0
  47. package/lib/session-changes.ts +147 -0
  48. package/lib/shell-bar.ts +24 -10
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes-view.ts +2 -1
  51. package/lib/shell-changes.ts +5 -2
  52. package/lib/shell-prompt.ts +25 -8
  53. package/lib/shell-sidebar-banner.ts +2 -2
  54. package/lib/shell-sidebar-layout.ts +5 -2
  55. package/lib/windows-session-transport.ts +877 -0
  56. package/package.json +3 -3
  57. package/runtime/native-review-cli.mjs +18 -96
  58. package/runtime/windows-session-transport.ps1 +791 -0
  59. package/scripts/gentle-ai-installer.mjs +10 -10
  60. package/scripts/test-packed-runner.mjs +1668 -20
  61. package/scripts/verify-package-files.mjs +2 -3
  62. package/tests/agent-home.test.ts +52 -0
  63. package/tests/agent-profiles.test.ts +30 -1
  64. package/tests/agents-config.test.ts +44 -0
  65. package/tests/agents-history.test.ts +12 -24
  66. package/tests/agents-runner.test.ts +321 -58
  67. package/tests/agents-session-transport-process.test.ts +249 -0
  68. package/tests/agents-session-transport.test.ts +823 -0
  69. package/tests/artifact-language.test.ts +10 -7
  70. package/tests/command-palette.test.ts +378 -0
  71. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  72. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  73. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  74. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  75. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  76. package/tests/gentle-agents.test.ts +883 -356
  77. package/tests/gentle-ai-binary.test.ts +1 -1
  78. package/tests/gentle-ai-installer.test.ts +47 -47
  79. package/tests/gentle-ai.test.ts +472 -4
  80. package/tests/gentle-shell.test.ts +338 -205
  81. package/tests/native-choice-list.test.ts +13 -0
  82. package/tests/native-review-capability-contract.test.ts +15 -1
  83. package/tests/native-review-cli.test.ts +0 -33
  84. package/tests/odd-routing-contract.test.ts +208 -0
  85. package/tests/orchestrator-budget.test.ts +17 -2
  86. package/tests/package-manifest.test.ts +119 -31
  87. package/tests/persona-single-channel.test.ts +3 -3
  88. package/tests/pi-pretty.test.ts +45 -0
  89. package/tests/profile-pin.test.ts +370 -0
  90. package/tests/quiet-tool-rendering.test.ts +32 -5
  91. package/tests/review-contract-prompt.test.ts +9 -0
  92. package/tests/review-controller.test.ts +0 -44
  93. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  94. package/tests/runtime-harness.mjs +4 -4
  95. package/tests/sdd-agent-tools.test.ts +15 -36
  96. package/tests/sdd-archive-replay.test.ts +82 -0
  97. package/tests/sdd-classical-continuation.test.ts +74 -0
  98. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  99. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  100. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  101. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  102. package/tests/sdd-odd-integration.test.ts +33 -0
  103. package/tests/sdd-optional-research.test.ts +124 -0
  104. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  105. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  106. package/tests/sdd-preflight.test.ts +1 -1
  107. package/tests/sdd-research-capabilities.test.ts +20 -162
  108. package/tests/sdd-selection-transport.test.ts +180 -88
  109. package/tests/sdd-status.test.ts +5 -778
  110. package/tests/sdd-task-truth.test.ts +43 -0
  111. package/tests/session-change-capture.test.ts +86 -0
  112. package/tests/session-changes-shell.test.ts +38 -0
  113. package/tests/session-changes.test.ts +114 -0
  114. package/tests/shell-bar.test.ts +35 -0
  115. package/tests/shell-card.test.ts +8 -6
  116. package/tests/shell-changes.test.ts +8 -0
  117. package/tests/shell-prompt.test.ts +41 -7
  118. package/tests/shell-sidebar-banner.test.ts +4 -4
  119. package/tests/shell-sidebar-layout.test.ts +97 -13
  120. package/tests/startup-banner.test.ts +55 -2
  121. package/tests/windows-hidden-processes.test.ts +303 -0
  122. package/tests/windows-session-bootstrap.test.ts +1772 -0
  123. package/tests/windows-session-compile.test.ts +170 -0
  124. package/tests/windows-session-transport.test.ts +754 -0
  125. package/assets/agents/sdd-sync.md +0 -146
  126. package/lib/openspec-guardrails.ts +0 -99
  127. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  128. package/tests/openspec-guardrails.test.ts +0 -71
@@ -85,6 +85,19 @@ test("native choice list presents one hovered wrapped option without changing se
85
85
  assert.doesNotMatch(list.render(56).join("\n"), new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")));
86
86
  });
87
87
 
88
+ test("native choice list refreshes existing item content without changing selection", () => {
89
+ const items = [
90
+ { id: "first", label: "First", description: "Initial detail." },
91
+ { id: "second", label: "Second", description: "Second detail." },
92
+ ];
93
+ const list = new NativeChoiceList(items, theme);
94
+ list.setSelectedIndex(1);
95
+ items[1]!.description = "Updated detail.";
96
+ list.refreshItems();
97
+ assert.equal(list.getSelectedItem()?.id, "second");
98
+ assert.match(stripTerminalSequences(list.render(40).join("\n")), /Updated detail\./);
99
+ });
100
+
88
101
  test("native choice list ignores Kitty key releases", () => {
89
102
  const list = new NativeChoiceList(
90
103
  [{ id: "first", label: "First" }, { id: "second", label: "Second" }],
@@ -232,11 +232,25 @@ test("2.9.0 repeats 2.8.2 because the negotiated lane Pi consumes is unchanged",
232
232
  assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.8.2"] as Record<string, boolean>);
233
233
  });
234
234
 
235
+ test("2.9.1 repeats 2.9.0 because the negotiated lane Pi consumes is unchanged", () => {
236
+ // v2.9.1 shipped restoring compatible OpenCode review consent (#4584) and
237
+ // deriving Claude Code SDD dispatch authority from the session transcript
238
+ // (#4575, #4551). Diffing contracts/review-integration/v2 and
239
+ // contracts/review-provider-contract between the v2.9.0 and v2.9.1 tags
240
+ // in the gentle-ai source tree showed zero byte changes, so this row
241
+ // repeats 2.9.0. riskEvidence and hint remain dark because neither is
242
+ // proven to reach Pi's negotiated START path.
243
+ const contract = NATIVE_CLI_CONTRACTS["2.9.1"] as Record<string, boolean>;
244
+ assert.equal(contract.riskEvidence, false);
245
+ assert.equal(contract.hint, false);
246
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.9.0"] as Record<string, boolean>);
247
+ });
248
+
235
249
  test("no shipped version key was added beyond the pin bump", () => {
236
250
  // Rows are promises to consumers, so a new key only ever appears in a
237
251
  // dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
238
252
  // v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
239
253
  // so they get no row: a row asserts ground truth measured against a binary
240
254
  // Pi actually ran, and the table only has to be ascending, not gapless.
241
- assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0"]);
255
+ assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1"]);
242
256
  });
@@ -1023,36 +1023,3 @@ test("all twelve native actions retain their exact tokens without prose routing"
1023
1023
  assert.equal(decodeNativeSddStatusV2(status, { workspaceRoot: "/repo" }).nextRecommended, nextRecommended);
1024
1024
  }
1025
1025
  });
1026
-
1027
-
1028
- test("compact SDD bracket transports exact selection and remediation binding", async () => {
1029
- const revision = `sha256:${"a".repeat(64)}`;
1030
- const queued = queuedAdapter([{ stdout: JSON.stringify({ state: "proceed", token: revision }) }, { stdout: '{"state":"complete"}' }]);
1031
- const cli = client(queued.adapter);
1032
- const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "acquire-one", workUnit: "correction", evidenceGoal: "Observed correction", maxAttempts: 1, maxChangedLines: 300, remediatesEvidenceRevision: revision };
1033
- assert.deepEqual(await cli.sddAttemptAcquire(request), { state: "proceed", token: revision });
1034
- assert.deepEqual(queued.calls[0].arguments, ["sdd-attempt", "acquire", "--cwd", "/repo", "--change", "fix", "--request-id", "acquire-one", "--work-unit", "correction", "--evidence-goal", "Observed correction", "--max-attempts", "1", "--max-changed-lines", "300", "--remediates-evidence-revision", revision]);
1035
- assert.deepEqual(await cli.sddAttemptSettle({ workspaceRoot: "/repo", changeName: "fix", token: revision, requestId: "settle-one", outcome: "interrupted", diagnosis: "Child could not spawn", harnessDisposition: "invalidated", cleanupEvidence: "No process created", processEvidence: "Spawn rejected", remediatesEvidenceRevision: revision }), { state: "complete" });
1036
- assert.equal(queued.calls[1].arguments.includes("--evidence-revision"), false);
1037
- assert.equal(queued.calls[1].timeoutMs, undefined);
1038
- });
1039
-
1040
- test("compact SDD refuses malformed result and invalid terminal evidence before execution", async () => {
1041
- const queued = queuedAdapter([{ stdout: '{"state":"proceed"}' }, { stdout: '{"state":"blocked","reason":"budget"}' }]);
1042
- const cli = client(queued.adapter);
1043
- const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "one", workUnit: "correction", evidenceGoal: "Observed correction" };
1044
- await assert.rejects(cli.sddAttemptAcquire(request), /schema incompatible/);
1045
- assert.deepEqual(await cli.sddAttemptAcquire(request), { state: "blocked", reason: "budget" });
1046
- await assert.rejects(cli.sddAttemptSettle({ ...request, token: `sha256:${"a".repeat(64)}`, outcome: "failed", diagnosis: "Test failed", harnessDisposition: "reused", cleanupEvidence: "Process exited", processEvidence: "Exit one" }), /evidence/i);
1047
- assert.equal(queued.calls.length, 2);
1048
- });
1049
-
1050
-
1051
- test("compact admission accepts native empty CAS and never redirects uncertainty into review", async () => {
1052
- const queued = queuedAdapter([{ stdout: '{"state":"blocked","reason":"budget"}' }, { stdout: "", timedOut: true }]);
1053
- const cli = client(queued.adapter);
1054
- const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "one", workUnit: "fix", evidenceGoal: "Observed correction", expectedRevision: "" };
1055
- await cli.sddAttemptAcquire(request);
1056
- assert.equal(queued.calls[0].arguments[queued.calls[0].arguments.indexOf("--expected-revision") + 1], "");
1057
- await assert.rejects(cli.sddAttemptAcquire({ ...request, expectedRevision: undefined }), error => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.nextAction === undefined);
1058
- });
@@ -0,0 +1,208 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { __testing } from "../extensions/gentle-ai.ts";
6
+
7
+ // These are instruction-delivery contracts, not proof of autonomous model adherence.
8
+ const read = (path: string) => readFileSync(join(import.meta.dirname, "..", path), "utf8");
9
+ const core = read("assets/orchestrator.md");
10
+ const delegation = read("assets/orchestrator-delegation.md");
11
+ const memory = read("assets/orchestrator-memory.md");
12
+ const wrapper = read("extensions/gentle-ai.ts");
13
+
14
+ function containsAll(text: string, clauses: readonly string[]): void {
15
+ for (const clause of clauses) assert.ok(text.includes(clause), `missing contract: ${clause}`);
16
+ }
17
+
18
+ test("organic entry stays read-only without authorization and loads detail before work", () => {
19
+ containsAll(core, [
20
+ "Substantial authorized work: use ODD",
21
+ "ODD (Default Workflow, harness section above) is mandatory on every request",
22
+ "orchestrator-delegation.md",
23
+ "orchestrator-memory.md",
24
+ ]);
25
+ containsAll(delegation, [
26
+ "Investigation, explanation, review, comparison, and proposal-only requests remain read-only",
27
+ "without a task or storage permission prompt",
28
+ "Small, understood work creates no durable task artifacts",
29
+ ]);
30
+ assert.doesNotMatch(core + wrapper, /Prefer SDD\/OpenSpec artifacts|Substantial feature: suggest SDD organically/);
31
+ assert.doesNotMatch(core + delegation, /Suggest it when proposal\/spec\/design\/tasks|propose SDD only when durable proposal\/spec\/design\/tasks/);
32
+ });
33
+
34
+ test("research uses adaptive evidence gathering and existing general workers only", () => {
35
+ containsAll(delegation, [
36
+ "problem, intended outcome, constraints, and current evidence",
37
+ "no fixed questionnaire or mandatory rounds",
38
+ "one focused user question",
39
+ "stop and wait",
40
+ "workers return gaps to the parent",
41
+ "available authorized documentation/web tools",
42
+ "prefer primary sources",
43
+ "URLs or code locations",
44
+ "verified facts, assumptions, contradictions, freshness, and gaps",
45
+ "recommendation, tradeoffs, open questions, and implementation implications",
46
+ "Forward these research instructions",
47
+ "existing fresh general exploration/research worker",
48
+ "do not create a specialized agent or invoke `sdd-research`",
49
+ "no new persistence or readiness machinery",
50
+ ]);
51
+ });
52
+
53
+ test("task sizing is explicitly advisory and forwarded without cosmetic savings", () => {
54
+ containsAll(delegation, [
55
+ "about 400 authored changed lines",
56
+ "additions plus deletions",
57
+ "not a task acceptance criterion, hard cap, counter-trigger, automatic stop, forced split, or RDD trigger",
58
+ "Forward this same advisory-only instruction",
59
+ "Never delete spaces, blank lines, or comments",
60
+ "never omit tests, minify, add gratuitous abstractions, or split artificially",
61
+ "Existing PR size gates remain unchanged",
62
+ ]);
63
+ });
64
+
65
+ test("organic progress preserves both complete feature copies and reconciles actual evidence", () => {
66
+ containsAll(memory, [
67
+ "odd/tasks/<feature-name>.md",
68
+ "odd/<feature-name>/tasks",
69
+ "stable task IDs",
70
+ "full current document",
71
+ "repository-relative file locator",
72
+ "preserve valid completed and unrelated work",
73
+ "reopen invalidated items",
74
+ "Check off only observed outcomes",
75
+ "Read back both writes",
76
+ "not atomic",
77
+ "mirror pending",
78
+ "Preserve both versions",
79
+ "mem_context",
80
+ "mem_search",
81
+ "mem_get_observation",
82
+ "read the actual task file",
83
+ "not a third authority",
84
+ ]);
85
+ });
86
+
87
+ test("assumption challenge and task checks do not activate or duplicate native review", () => {
88
+ containsAll(delegation, [
89
+ "at most one scoped independent read-only assumption challenge",
90
+ "high-consequence unproven premise",
91
+ "Deterministic failures need fixes",
92
+ "native RDD refuter",
93
+ "functional checks per task, not an RDD cycle per TODO",
94
+ "deliverable candidate boundary",
95
+ "native candidate risk assessment",
96
+ "gentle_review` with `{\"operation\":\"assess\"}",
97
+ "Passive/low",
98
+ "no reviewer or consent ceremony",
99
+ "only on grant",
100
+ "decline continues under ordinary policy",
101
+ "never infer low risk from a failed assessment",
102
+ "When RDD is disabled, do not start or prompt for RDD",
103
+ ]);
104
+ });
105
+
106
+ test("user documentation shows recovery and candidate-level consent without claiming model proof", () => {
107
+ const docs = read("docs/readme-reference.md");
108
+ containsAll(docs, [
109
+ "## Organic Driven Development",
110
+ "```mermaid",
111
+ "Full feature memory and actual task file",
112
+ "Native candidate risk",
113
+ "advisory",
114
+ "Static prompt tests",
115
+ "autonomous",
116
+ ]);
117
+ assert.ok(read("README.md").includes("#organic-driven-development"));
118
+ });
119
+
120
+
121
+ test("one feature document carries intent, accepted rationale and worker context", () => {
122
+ containsAll(memory, [
123
+ "one feature document, not a separate plan file or topic",
124
+ "objective, problem, why, scope, constraints",
125
+ "progress, verification evidence, and next step",
126
+ "concise rationale for meaningful accepted changes",
127
+ "Routine corrections stay with their tasks; no exhaustive decision journal",
128
+ "Accepted user, review, or verification changes",
129
+ "automatically update affected intent and TODOs",
130
+ "add genuinely new tasks or reopen invalidated items with a reason",
131
+ "Findings alone never authorize scope expansion or automatic acceptance",
132
+ "Before implementation or resume, the parent reads both the actual file and full observation",
133
+ "passes the locator and relevant context; workers read the document before edits",
134
+ ]);
135
+ containsAll(read("assets/agents/gentle-ai-worker.md"), [
136
+ "Read the parent's ODD feature document locator before edits",
137
+ "Preserve valid completed work; return proposed intent/task changes and their reasons",
138
+ ]);
139
+ });
140
+
141
+ test("ODD forwards configured TDD without equating test presence with enablement", () => {
142
+ containsAll(delegation, [
143
+ "Resolve effective TDD on/off from existing project/session configuration or explicit user choice",
144
+ "retain its source and exact test runner",
145
+ "Record resolved mode, source, and runner in the feature document when present",
146
+ "Tests or frameworks being present does not enable TDD",
147
+ "Forward mode, source, and runner on every implementation delegation; refresh on resume",
148
+ "When enabled, require observed RED before implementation, GREEN, then REFACTOR",
149
+ "When disabled, run ordinary functional checks, not no checks",
150
+ "If mode is unknown/conflicting or the runner is missing",
151
+ "resolve only the ambiguity affecting the next action",
152
+ "never invent precedence or a command, and never invoke sdd-init to determine ODD TDD",
153
+ ]);
154
+ containsAll(read("assets/agents/gentle-ai-worker.md"), [
155
+ "Consume the parent's effective TDD mode, configuration/choice source, and exact runner",
156
+ "Missing or conflicting mode/source/runner is not disabled TDD",
157
+ ]);
158
+ assert.doesNotMatch(wrapper, /If tests exist, use strict TDD/);
159
+ });
160
+
161
+ test("ODD protocol is always-on in the rendered system prompt and runs by default", () => {
162
+ const orderedClauses = [
163
+ "Default workflow: Organic Driven Development (MANDATORY)",
164
+ "predefined workflow of this orchestrator",
165
+ "SDD is a branch inside ODD",
166
+ "Never describe this workflow only when asked about it: run it.",
167
+ "1. **Authorize.**",
168
+ "2. **Explore.**",
169
+ "3. **Resolve uncertainty.**",
170
+ "4. **Classify.**",
171
+ "two or more meaningful implementation steps",
172
+ "5. **Track before the first write.**",
173
+ "Tell the user in one line which feature document was created and how many tasks it holds",
174
+ "6. **Implement task by task.**",
175
+ "7. **Close.**",
176
+ "Harness principles:",
177
+ "# el Gentleman Orchestrator",
178
+ ];
179
+ for (const persona of ["gentleman", "neutral"] as const) {
180
+ const prompt = __testing.buildGentlePrompt(persona);
181
+ let cursor = -1;
182
+ for (const clause of orderedClauses) {
183
+ const index = prompt.indexOf(clause);
184
+ assert.ok(index !== -1, `[${persona}] missing contract: ${clause}`);
185
+ assert.ok(
186
+ index > cursor,
187
+ `[${persona}] clause out of order (must appear after the previous one): ${clause}`,
188
+ );
189
+ cursor = index;
190
+ }
191
+ }
192
+
193
+ assert.ok(
194
+ wrapper.includes("Organic Driven Development (ODD) is the predefined workflow for every request"),
195
+ "missing contract: extensions/gentle-ai.ts harness principle",
196
+ );
197
+ assert.ok(
198
+ wrapper.includes(
199
+ "I run Organic Driven Development by default and SDD/OpenSpec when explicitly selected",
200
+ ),
201
+ "missing contract: extensions/gentle-ai.ts identity sentence",
202
+ );
203
+ assert.ok(
204
+ core.includes("ODD (Default Workflow, harness section above) is mandatory on every request"),
205
+ "missing contract: assets/orchestrator.md pointer sentence",
206
+ );
207
+ containsAll(core, ["orchestrator-delegation.md", "orchestrator-memory.md"]);
208
+ });
@@ -281,8 +281,8 @@ function isNormativeLine(line: string): boolean {
281
281
  }
282
282
 
283
283
  const fixtureLines = readFileSync(FIXTURE_PATH, "utf8").split("\n");
284
- // Fixture lines 187 and 191 predate the root-relative lazy-asset contract and
285
- // canonical-authority resolution. Keep their coverage by asserting the
284
+ // Fixture line 36 is superseded by ODD (#1035); 187 and 191 predate
285
+ // root-relative lazy assets and canonical-authority resolution. Keep coverage by asserting the
286
286
  // intentionally updated production wording instead of weakening the range.
287
287
  const CURRENT_SDD_WORKFLOW_PATH = "`sdd-orchestrator-workflow.md`";
288
288
  const CURRENT_HARD_PREFLIGHT_INVARIANT = "Hard preflight invariant: `openspec/config.yaml`, existing SDD changes, installed `.pi`/global SDD assets, or a todo named \"preflight\" are not session preflight. Do not mark SDD preflight complete, start `sdd-init`, launch SDD subagents/chains, or move to explore/proposal/spec/design/tasks until this session has an injected `## SDD Session Preflight` block or a canonical-authority resolution. Defaults and capability constraints may resolve fields without confirmation prompts; preserve unresolved-choice and safety gates.";
@@ -320,11 +320,26 @@ for (const range of DISPOSITION_MAP) {
320
320
  if (raw === undefined || !isNormativeLine(raw)) continue;
321
321
  const trimmed = raw.trim();
322
322
  const expected =
323
+ ln === 36 ? "- Substantial authorized work: use ODD; track feature progress automatically." :
324
+ // #1051 keeps the selected store when memory is unavailable.
325
+ ln === 221 ? "do not switch the selected store" :
326
+ // Research returns findings; other phases keep direct backend ownership.
327
+ ln === 205 ? trimmed.replace("Each SDD phase", "Except for output-only `sdd-research`, each SDD phase") :
328
+ ln === 185 ? trimmed.replace("apply/verify/sync/archive", "apply/verify/archive") :
323
329
  ln === 187
324
330
  ? CURRENT_SDD_WORKFLOW_PATH
325
331
  : ln === 191
326
332
  ? CURRENT_HARD_PREFLIGHT_INVARIANT
327
333
  : trimmed;
334
+ // #1051 retires the standalone sync row and its artifact key, not
335
+ // the surrounding memory/recovery contract or historical fixture.
336
+ if (ln === 216 || ln === 220) {
337
+ assert.ok(!targetContent.includes(trimmed), `retired sync contract remains at fixture:${ln}`);
338
+ assert.match(targetContent, /sdd\/<change>\/archive-report/);
339
+ assert.match(targetContent, /sdd\/<change>\/verify-report/);
340
+ assert.doesNotMatch(targetContent, /sdd\/<change>\/sync-report|\| `sdd-sync`/);
341
+ continue;
342
+ }
328
343
  if (SUPERSEDED_LIFECYCLE_REVIEW_LINES.has(ln)) {
329
344
  assert.ok(
330
345
  !targetContent.includes(trimmed),
@@ -12,6 +12,7 @@ import {
12
12
  import { homedir, tmpdir } from "node:os";
13
13
  import { dirname, join } from "node:path";
14
14
  import test from "node:test";
15
+ import ts from "typescript";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import { applyModelConfig } from "../extensions/gentle-ai.ts";
17
18
  import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
@@ -182,7 +183,7 @@ test("npm publication is bound to the exact package tag and triggering commit",
182
183
  packageJson.repository,
183
184
  {
184
185
  type: "git",
185
- url: "git+https://github.com/Gentleman-Programming/gentle-pi.git",
186
+ url: "git+https://github.com/Gentleman-Programming/gentle-shell.git",
186
187
  },
187
188
  "trusted publishing requires the exact case-sensitive npm repository identity",
188
189
  );
@@ -218,11 +219,29 @@ test("generated runtime modules and packed-package checks are deterministic", ()
218
219
  assert.match(ci, /pnpm run check:runtime-modules/);
219
220
  assert.match(ci, /pnpm run test:packed-package/);
220
221
  assert.match(generator, /Generated by scripts\/build-runtime-modules\.mjs/);
221
- assert.match(packedRunner, /\["install"[^\]]*"--ignore-scripts=false"/s, "packed install must explicitly enable postinstall");
222
- assert.doesNotMatch(packedRunner, /\["install"[^\]]*"--ignore-scripts"(?!\=false)/s);
223
- assert.match(packedRunner, /execFileSync\("where\.exe", \["npm"\]/);
224
- assert.match(packedRunner, /could not resolve npm-cli\.js without a command shell/);
225
- assert.doesNotMatch(packedRunner, /ComSpec|cmd\.exe/);
222
+ const ordinaryPostinstall = readNamedFunction(packedRunner, "testHookedPackedRunner");
223
+ assert.match(ordinaryPostinstall, /\["install"[^\]]*"--ignore-scripts=false"/s, "ordinary packed install must explicitly enable postinstall");
224
+ assert.doesNotMatch(ordinaryPostinstall, /\["install"[^\]]*"--ignore-scripts"(?!\=false)/s);
225
+ for (const name of [
226
+ "testSdkLifecyclePackedSession",
227
+ "testWindowsStartupTimingPackedHelper",
228
+ "testWindowsStartupTimingEnvironmentExperiment",
229
+ "testUnhookedPackedImports",
230
+ ]) {
231
+ const isolatedInstall = readNamedFunction(packedRunner, name);
232
+ assert.match(isolatedInstall, /\["install"[^\]]*"--ignore-scripts"(?!\=false)/s, `${name} must isolate installation scripts`);
233
+ assert.doesNotMatch(isolatedInstall, /\["install"[^\]]*"--ignore-scripts=false"/s, `${name} must not enable postinstall`);
234
+ }
235
+ const windowsNpmInvocation = readNamedFunction(packedRunner, "windowsNpmInvocation");
236
+ const runNpmWithEnv = readNamedFunction(packedRunner, "runNpmWithEnv");
237
+ assert.match(windowsNpmInvocation, /execFileSync\("where\.exe", \["npm"\]/);
238
+ assert.match(windowsNpmInvocation, /return \{ file: process\.execPath, prefix: \[installedCli\] \}/);
239
+ assert.match(windowsNpmInvocation, /return \{ file: path, prefix: \[\] \}/);
240
+ assert.match(windowsNpmInvocation, /could not resolve npm-cli\.js without a command shell/);
241
+ assert.doesNotMatch(windowsNpmInvocation, /ComSpec|cmd\.exe/);
242
+ assert.match(runNpmWithEnv, /execFileSync\(invocation\.file, \[\.\.\.invocation\.prefix, \.\.\.arguments_\]/);
243
+ assert.match(runNpmWithEnv, /\.\.\.options, env/);
244
+ assert.doesNotMatch(packedRunner, /shell\s*:\s*true/);
226
245
  assert.match(packedRunner, /review", "capabilities", "--contract", "gentle-ai\.review-integration\/v2"/);
227
246
  assert.doesNotMatch(packedRunner, /git-commit-transaction|transaction runner/i);
228
247
  });
@@ -247,8 +266,8 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
247
266
 
248
267
  assert.equal(
249
268
  packageJson.dependencies?.["@heyhuynhgiabuu/pi-pretty"],
250
- "0.6.14",
251
- "gentle-pi must install the tested pi-pretty version as a normal dependency",
269
+ "0.6.27",
270
+ "gentle-pi must install the tested pi-pretty version containing the model-visible result integrity fix",
252
271
  );
253
272
  assert.ok(
254
273
  packageJson.pi?.extensions?.includes("./extensions"),
@@ -260,10 +279,22 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
260
279
  ),
261
280
  "gentle-pi must not reference pnpm-unportable nested node_modules paths",
262
281
  );
282
+ const piPrettyWrapperPath = join(PACKAGE_ROOT, "extensions", "pi-pretty.ts");
263
283
  assert.ok(
264
- existsSync(join(PACKAGE_ROOT, "extensions", "pi-pretty.ts")),
284
+ existsSync(piPrettyWrapperPath),
265
285
  "gentle-pi must expose pi-pretty through a packaged wrapper extension",
266
286
  );
287
+ const piPrettyWrapper = readFileSync(piPrettyWrapperPath, "utf8");
288
+ assert.match(
289
+ piPrettyWrapper,
290
+ /import\("@heyhuynhgiabuu\/pi-pretty"\)/,
291
+ "the wrapper must use the compiled-runtime-safe ESM loader",
292
+ );
293
+ assert.doesNotMatch(
294
+ piPrettyWrapper,
295
+ /requireFromRealPackage\("@heyhuynhgiabuu\/pi-pretty"\)/,
296
+ "the wrapper must not resolve the pi-pretty package root through createRequire",
297
+ );
267
298
  assert.ok(
268
299
  existsSync(join(PACKAGE_ROOT, "extensions", "quiet-tools.ts")),
269
300
  "gentle-pi must expose quiet built-in tool rendering through a packaged extension",
@@ -278,20 +309,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
278
309
  );
279
310
  });
280
311
 
281
- test("package verification binds the published Gentle AI v2.9.0 runtime pin", () => {
312
+ test("package verification binds the published Gentle AI v2.9.1 runtime pin", () => {
282
313
  const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
283
314
  const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
284
315
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
285
316
 
286
- assert.match(installer, /INSTALLER_VERSION = "2\.9\.0"/);
317
+ assert.match(installer, /INSTALLER_VERSION = "2\.9\.1"/);
287
318
  assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
288
- assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:WqmYw1UfVYCbtOxU1uTuiYq9\+ROCUIDrmKIOjyYggQI="/);
319
+ assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:roCuxlF\+L4YOb1X0los4RpjcLfxAmwmHxteSQMcfThY="/);
289
320
  assert.match(installer, /GOTOOLCHAIN: "local"/);
290
321
  assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
291
322
  assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
292
323
  assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
293
324
  assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
294
- assert.match(verifier, /v2\.9\.0/);
325
+ assert.match(verifier, /v2\.9\.1/);
295
326
  });
296
327
 
297
328
 
@@ -355,6 +386,46 @@ function nestedContractFields(contract: string, parent: string): string[] {
355
386
  return contractFields(nestedBlock.join("\n"), 2);
356
387
  }
357
388
 
389
+ function readNamedFunction(source: string, name: string): string {
390
+ const fileName = "test-packed-runner.mjs";
391
+ const options: ts.CompilerOptions = {
392
+ allowJs: true,
393
+ noLib: true,
394
+ noResolve: true,
395
+ target: ts.ScriptTarget.Latest,
396
+ };
397
+ const host = ts.createCompilerHost(options, true);
398
+ host.getSourceFile = (requested, languageVersion) =>
399
+ requested === fileName
400
+ ? ts.createSourceFile(requested, source, languageVersion, true, ts.ScriptKind.JS)
401
+ : undefined;
402
+ host.fileExists = (requested) => requested === fileName;
403
+ host.readFile = (requested) => requested === fileName ? source : undefined;
404
+ host.getCurrentDirectory = () => ".";
405
+ host.writeFile = () => {};
406
+ const program = ts.createProgram([fileName], options, host);
407
+ const sourceFile = program.getSourceFile(fileName);
408
+ assert.ok(sourceFile, "packed runner source must be available to the in-memory compiler host");
409
+ assert.equal(program.getSyntacticDiagnostics(sourceFile).length, 0, "packed runner source must have no syntactic diagnostics");
410
+ const declarations = sourceFile.statements.filter(
411
+ (statement): statement is ts.FunctionDeclaration =>
412
+ ts.isFunctionDeclaration(statement) && statement.name?.text === name,
413
+ );
414
+ assert.equal(declarations.length, 1, `packed runner must define exactly one top-level ${name}`);
415
+ const declaration = declarations[0];
416
+ assert.ok(declaration.body, `${name} must have a complete function body`);
417
+ assert.ok(declaration.body.end > declaration.body.pos, `${name} must have a non-empty function body`);
418
+ return source.slice(declaration.getStart(sourceFile), declaration.end);
419
+ }
420
+
421
+ test("readNamedFunction fails closed for missing, duplicate, and incomplete declarations", () => {
422
+ assert.equal(readNamedFunction("function target() { return 1; }\nfunction neighbor() { return 2; }\n", "target"), "function target() { return 1; }");
423
+ assert.throws(() => readNamedFunction("const target = () => {};\n", "target"), /exactly one top-level target/);
424
+ assert.throws(() => readNamedFunction("function target() {}\nfunction target() {}\n", "target"), /exactly one top-level target/);
425
+ assert.throws(() => readNamedFunction("function target() {\n", "target"), /packed runner source must have no syntactic diagnostics/);
426
+ assert.throws(() => readNamedFunction("function target() {}\nfunction neighbor( {}\n", "target"), /packed runner source must have no syntactic diagnostics/);
427
+ });
428
+
358
429
  function readMarkdownSection(source: string, heading: string): string {
359
430
  const lines = source.split(/\r?\n/);
360
431
  const matches = lines.flatMap((line, index) => {
@@ -548,7 +619,7 @@ const EXPECTED_OWNER_ASSETS: Record<PackageAssetOwner, readonly string[]> = {
548
619
  "agents/sdd-apply.md", "agents/sdd-archive.md", "agents/sdd-design.md",
549
620
  "agents/sdd-explore.md", "agents/sdd-init.md", "agents/sdd-onboard.md",
550
621
  "agents/sdd-proposal.md", "agents/sdd-remediate.md", "agents/sdd-research.md", "agents/sdd-spec.md",
551
- "agents/sdd-status.md", "agents/sdd-sync.md", "agents/sdd-tasks.md", "agents/sdd-verify.md",
622
+ "agents/sdd-status.md", "agents/sdd-tasks.md", "agents/sdd-verify.md",
552
623
  "chains/sdd-full.chain.md", "chains/sdd-plan.chain.md", "chains/sdd-verify.chain.md",
553
624
  "gentle-ai/support/sdd-status-contract.md", "gentle-ai/support/strict-tdd-verify.md",
554
625
  "gentle-ai/support/strict-tdd.md",
@@ -591,10 +662,10 @@ test("legacy all-assets installation covers every packaged file with explicit ow
591
662
  assert.equal(getPackageAssetOwner(key), undefined, "unknown assets must not default to SDD");
592
663
  }
593
664
  withIsolatedAssetHome((agentHome) => {
594
- assert.deepEqual(installSddAssets(agentHome, false), { agents: 24, chains: 4, support: 3, skipped: 0 });
665
+ assert.deepEqual(installSddAssets(agentHome, false), { agents: 23, chains: 4, support: 3, skipped: 0 });
595
666
  assert.deepEqual(Object.keys(installedAssetManifest(agentHome).assets).sort(), packaged);
596
- assert.deepEqual(installSddAssets(agentHome, false), { agents: 0, chains: 0, support: 0, skipped: 31 });
597
- assert.deepEqual(installSddAssets(agentHome, true), { agents: 24, chains: 4, support: 3, skipped: 0 });
667
+ assert.deepEqual(installSddAssets(agentHome, false), { agents: 0, chains: 0, support: 0, skipped: 30 });
668
+ assert.deepEqual(installSddAssets(agentHome, true), { agents: 23, chains: 4, support: 3, skipped: 0 });
598
669
  });
599
670
  });
600
671
 
@@ -651,14 +722,8 @@ test("selective review migration adopts only untouched legacy copies and preserv
651
722
  });
652
723
 
653
724
  test("unowned legacy research migrates by exact normalized hash, preserving routing and user edits", () => {
654
- const packaged = readFileSync(join(PACKAGE_ROOT, "assets", "agents", "sdd-research.md"), "utf8");
655
- const oldAdmission = "- Evidence grants for this runtime are `documentation=[]; open-web=[]`. Never infer evidence capability from bash, persistence tools, or any inherited tool; persistence tools are not evidence grants. Unsupported or undeclared classes deny admission and emit no claims.\n- Because this runtime declares no evidence grants, retain the selected request, persist a `blocked` outcome with no claims, and stop.\n";
656
- const legacy = packaged
657
- .replace(/## Parent Preflight Transport\n[\s\S]*?(?=## Skill Resolution Contract)/, "")
658
- .replace(/## Bounded artifact handoff\n[\s\S]*?(?=## Memory Contract)/, "")
659
- .replace(/ - fetch_content\n - web_search\n - source_check\n - get_search_content\n/, "")
660
- .replace(/- Use the injected `## SDD Research Capabilities`[\s\S]*?(?=- Admission denial)/, oldAdmission)
661
- .replace(/Use `done` only when all selected questions[\s\S]*?product decisions remain separately confirmed by the parent\./i, "For this runtime the outcome is `blocked` with an admission denial and no claims.");
725
+ // Historical bytes must not be reconstructed from the evolving current agent.
726
+ const legacy = readFileSync(join(PACKAGE_ROOT, "tests/fixtures/legacy/sdd-research-v2.5.0.md"), "utf8");
662
727
  const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "assets", "migrations", "managed-assets-v2.5.0.json"), "utf8"));
663
728
  assert.equal(sha256(legacy), manifest.assets["agents/sdd-research.md"], "fixture reconstruction must match observed old package bytes");
664
729
  const temporary = mkdtempSync(join(tmpdir(), "gentle-research-migration-"));
@@ -1496,22 +1561,22 @@ test("orchestrator routes generic roles without static RDD lens routing", () =>
1496
1561
  assert.match(core, /this package invents no lifecycle instructions/);
1497
1562
  });
1498
1563
 
1499
- test("pi-pretty wrapper uses real package path resolution for pnpm symlink installs", () => {
1564
+ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink installs", () => {
1500
1565
  const wrapper = readFileSync(
1501
1566
  join(PACKAGE_ROOT, "extensions", "pi-pretty.ts"),
1502
1567
  "utf8",
1503
1568
  );
1504
1569
 
1505
- assert.match(wrapper, /realpathSync/);
1506
- assert.match(wrapper, /createRequire/);
1507
- assert.match(wrapper, /@heyhuynhgiabuu\/pi-pretty/);
1570
+ assert.doesNotMatch(wrapper, /realpathSync|createRequire/);
1571
+ assert.match(wrapper, /piPrettyExtensionPromise/);
1572
+ assert.match(wrapper, /import\("@heyhuynhgiabuu\/pi-pretty"\)/);
1508
1573
  assert.match(wrapper, /PI_PRETTY_SUPPRESSED_TOOL_NAMES/);
1509
1574
  assert.match(wrapper, /quietToolsEnabled/);
1510
1575
  });
1511
1576
 
1512
- test("v2.6.4 release package and runtime stop before publication", () => {
1577
+ test("v3.0.0 release package and runtime stop before publication", () => {
1513
1578
  const packageJson = readPackageJson();
1514
- assert.equal(packageJson.version, "2.6.4", "the release manifest must remain explicitly pinned to v2.6.4");
1579
+ assert.equal(packageJson.version, "3.0.0", "the release manifest must remain explicitly pinned to v3.0.0");
1515
1580
  assert.equal(
1516
1581
  packageJson.scripts?.test,
1517
1582
  "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
@@ -1559,3 +1624,26 @@ test("technical reference documents dynamic Gentle AI RDD ownership and the inst
1559
1624
  test("package verification explicitly requires the managed remediation actor", () => {
1560
1625
  assert.match(readFileSync(join(PACKAGE_ROOT, "scripts/verify-package-files.mjs"), "utf8"), /assets\/agents\/sdd-remediate\.md/);
1561
1626
  });
1627
+
1628
+ test("SDD installation retires owned sync but preserves modified copies and unrelated owners", () => {
1629
+ withIsolatedAssetHome((agentHome) => {
1630
+ installSddAssets(agentHome, false);
1631
+ const path = join(agentHome, "agents/sdd-sync.md");
1632
+ const manifestPath = join(agentHome, "gentle-ai/managed-assets.json");
1633
+ const legacy = "Previously managed sync executor\n";
1634
+ const manifest = installedAssetManifest(agentHome);
1635
+ manifest.assets["agents/sdd-sync.md"] = sha256(legacy);
1636
+ writeFileSync(path, legacy);
1637
+ writeFileSync(manifestPath, JSON.stringify(manifest));
1638
+ installPackageAssets(agentHome, true, ["delegation"]);
1639
+ assert.equal(readFileSync(path, "utf8"), legacy);
1640
+ installPackageAssets(agentHome, true, ["sdd"]);
1641
+ assert.equal(existsSync(path), false);
1642
+ assert.equal(installedAssetManifest(agentHome).assets["agents/sdd-sync.md"], undefined);
1643
+ writeFileSync(path, "User-modified sync instructions\n");
1644
+ writeFileSync(manifestPath, JSON.stringify(manifest));
1645
+ installPackageAssets(agentHome, true, ["sdd"]);
1646
+ assert.equal(readFileSync(path, "utf8"), "User-modified sync instructions\n");
1647
+ assert.equal(installedAssetManifest(agentHome).assets["agents/sdd-sync.md"], undefined);
1648
+ });
1649
+ });
@@ -262,7 +262,7 @@ test("Table A rule: wrapper :180/:181 + orchestrator :9,:12 self-description MER
262
262
  );
263
263
  assert.match(
264
264
  prompt,
265
- /I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona\. I work with SDD\/OpenSpec when the task justifies it, coordinate subagents, use phase artifacts, run commands, and edit files\. I am not a generic chatbot\./,
265
+ /I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona\. I run Organic Driven Development by default and SDD\/OpenSpec when explicitly selected, coordinate subagents, use phase artifacts, run commands, and edit files\. I am not a generic chatbot\./,
266
266
  `[${persona}] the richer translated self-description paragraph (orchestrator :9,:12) must survive in the wrapper`,
267
267
  );
268
268
  }
@@ -295,7 +295,7 @@ test("Table A rule: SDD/OpenSpec artifacts + subagents core-capabilities bullet
295
295
  const prompt = __testing.buildGentlePrompt(persona);
296
296
  assert.match(
297
297
  prompt,
298
- /Mention SDD\/OpenSpec phase artifacts and subagents as core capabilities\./,
298
+ /Mention ODD as the default workflow, SDD\/OpenSpec phase artifacts, and subagents as core capabilities\./,
299
299
  `[${persona}] wrapper :182 rule must survive`,
300
300
  );
301
301
  }
@@ -439,7 +439,7 @@ test("dup guard (exact-string): 'Do not claim portability outside the Pi runtime
439
439
 
440
440
  test("dup guard (exact-string): identity self-description sentence occurs exactly once", () => {
441
441
  const selfDescription =
442
- "I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona. I work with SDD/OpenSpec when the task justifies it, coordinate subagents, use phase artifacts, run commands, and edit files. I am not a generic chatbot.";
442
+ "I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona. I run Organic Driven Development by default and SDD/OpenSpec when explicitly selected, coordinate subagents, use phase artifacts, run commands, and edit files. I am not a generic chatbot.";
443
443
  for (const persona of ["gentleman", "neutral"] as const) {
444
444
  const prompt = __testing.buildGentlePrompt(persona);
445
445
  assert.equal(