gentle-pi 2.7.0 → 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 (122) hide show
  1. package/README.md +24 -6
  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 +14 -4
  18. package/docs/readme-reference.md +171 -32
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +281 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +74 -30
  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 +67 -111
  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 +10 -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 +2 -1
  47. package/lib/session-changes.ts +8 -1
  48. package/lib/shell-bar.ts +21 -12
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes.ts +3 -2
  51. package/lib/shell-prompt.ts +25 -8
  52. package/lib/shell-sidebar-banner.ts +2 -2
  53. package/lib/shell-sidebar-layout.ts +5 -2
  54. package/lib/windows-session-transport.ts +877 -0
  55. package/package.json +3 -3
  56. package/runtime/native-review-cli.mjs +9 -96
  57. package/runtime/windows-session-transport.ps1 +791 -0
  58. package/scripts/test-packed-runner.mjs +1668 -20
  59. package/scripts/verify-package-files.mjs +0 -1
  60. package/tests/agent-home.test.ts +52 -0
  61. package/tests/agent-profiles.test.ts +30 -1
  62. package/tests/agents-config.test.ts +44 -0
  63. package/tests/agents-history.test.ts +12 -24
  64. package/tests/agents-runner.test.ts +307 -58
  65. package/tests/agents-session-transport-process.test.ts +249 -0
  66. package/tests/agents-session-transport.test.ts +823 -0
  67. package/tests/artifact-language.test.ts +10 -7
  68. package/tests/command-palette.test.ts +378 -0
  69. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  70. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  71. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  72. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  73. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  74. package/tests/gentle-agents.test.ts +849 -356
  75. package/tests/gentle-ai.test.ts +472 -4
  76. package/tests/gentle-shell.test.ts +201 -8
  77. package/tests/native-choice-list.test.ts +13 -0
  78. package/tests/native-review-cli.test.ts +0 -33
  79. package/tests/odd-routing-contract.test.ts +208 -0
  80. package/tests/orchestrator-budget.test.ts +17 -2
  81. package/tests/package-manifest.test.ts +115 -27
  82. package/tests/persona-single-channel.test.ts +3 -3
  83. package/tests/pi-pretty.test.ts +45 -0
  84. package/tests/profile-pin.test.ts +370 -0
  85. package/tests/quiet-tool-rendering.test.ts +32 -5
  86. package/tests/review-contract-prompt.test.ts +9 -0
  87. package/tests/review-controller.test.ts +0 -44
  88. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  89. package/tests/runtime-harness.mjs +4 -4
  90. package/tests/sdd-agent-tools.test.ts +15 -36
  91. package/tests/sdd-archive-replay.test.ts +82 -0
  92. package/tests/sdd-classical-continuation.test.ts +74 -0
  93. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  94. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  95. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  96. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  97. package/tests/sdd-odd-integration.test.ts +33 -0
  98. package/tests/sdd-optional-research.test.ts +124 -0
  99. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  100. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  101. package/tests/sdd-preflight.test.ts +1 -1
  102. package/tests/sdd-research-capabilities.test.ts +20 -162
  103. package/tests/sdd-selection-transport.test.ts +180 -88
  104. package/tests/sdd-status.test.ts +5 -778
  105. package/tests/sdd-task-truth.test.ts +43 -0
  106. package/tests/session-change-capture.test.ts +20 -2
  107. package/tests/session-changes.test.ts +11 -0
  108. package/tests/shell-bar.test.ts +21 -0
  109. package/tests/shell-card.test.ts +8 -6
  110. package/tests/shell-changes.test.ts +8 -0
  111. package/tests/shell-prompt.test.ts +41 -7
  112. package/tests/shell-sidebar-banner.test.ts +4 -4
  113. package/tests/shell-sidebar-layout.test.ts +97 -13
  114. package/tests/startup-banner.test.ts +55 -2
  115. package/tests/windows-hidden-processes.test.ts +303 -0
  116. package/tests/windows-session-bootstrap.test.ts +1772 -0
  117. package/tests/windows-session-compile.test.ts +170 -0
  118. package/tests/windows-session-transport.test.ts +754 -0
  119. package/assets/agents/sdd-sync.md +0 -146
  120. package/lib/openspec-guardrails.ts +0 -99
  121. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  122. package/tests/openspec-guardrails.test.ts +0 -71
@@ -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",
@@ -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.7.0 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.7.0", "the release manifest must remain explicitly pinned to v2.7.0");
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(
@@ -0,0 +1,45 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import pretty from "../extensions/pi-pretty.ts";
4
+
5
+ test("bundled pretty cannot replace or restore an editor owned by the host", async () => {
6
+ const handlers = new Map<string, Function[]>();
7
+ const pi = { on(name: string, handler: Function) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); } };
8
+ const owner = () => "custom";
9
+ let editor: unknown = owner;
10
+ let indicator: unknown;
11
+ let message: unknown;
12
+ let visible = true;
13
+ const ctx = { mode: "tui", ui: {
14
+ getEditorComponent: () => editor,
15
+ setEditorComponent: (value: unknown) => { editor = value; },
16
+ setWorkingIndicator: (value: unknown) => { indicator = value; },
17
+ setWorkingMessage: (value: unknown) => { message = value; },
18
+ setWorkingVisible(value: boolean) { visible = value; },
19
+ theme: { fg: (role: string, text: string) => `<${role}>${text}</${role}>`, bold: (text: string) => text },
20
+ } };
21
+ await pretty(pi, undefined, async (api: any) => {
22
+ api.on("session_start", (_event: unknown, context: any) => {
23
+ context.ui.setEditorComponent(() => "pretty");
24
+ context.ui.setWorkingIndicator({ frames: ["foreign"] });
25
+ });
26
+ api.on("session_shutdown", (_event: unknown, context: any) => context.ui.setEditorComponent(undefined));
27
+ }, {});
28
+ for (const handler of handlers.get("session_start") ?? []) await handler({}, ctx);
29
+ assert.equal(editor, owner);
30
+ assert.equal(visible, false, "the prompt frame owns the live working state without a duplicate loader row");
31
+ assert.equal(indicator, undefined);
32
+ assert.equal(message, undefined);
33
+ for (const handler of handlers.get("session_shutdown") ?? []) await handler({}, ctx);
34
+ assert.equal(editor, owner);
35
+ assert.equal(visible, true);
36
+ assert.equal(indicator, undefined);
37
+ assert.equal(message, undefined);
38
+ });
39
+
40
+ test("disabled shell leaves bundled editor behavior untouched", async () => {
41
+ let received: unknown;
42
+ const pi = {};
43
+ await pretty(pi, undefined, async (api: unknown) => { received = api; }, { GENTLE_PI_SHELL: "0" });
44
+ assert.equal(received, pi);
45
+ });