gentle-pi 4.2.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 (202) hide show
  1. package/README.md +336 -137
  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/gentle-shell.mjs +170 -22
  12. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  13. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  15. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  16. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  18. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  19. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  20. package/docs/assets/diagrams/measured-routing.svg +49 -0
  21. package/docs/assets/diagrams/native-review.svg +52 -12
  22. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  23. package/docs/assets/diagrams/permission-modes.svg +51 -0
  24. package/docs/assets/features/audio-notifications.png +0 -0
  25. package/docs/assets/features/command-palette.png +0 -0
  26. package/docs/bundled-install.md +33 -3
  27. package/docs/delegated-verification.md +10 -8
  28. package/docs/gentle-agents-activity.md +11 -5
  29. package/docs/gentle-shell-uninstall.md +8 -1
  30. package/docs/gentle-shell.md +5 -4
  31. package/docs/install-wizard.md +8 -1
  32. package/docs/permission-modes.md +1 -1
  33. package/docs/readme-reference.md +107 -19
  34. package/docs/yolo-mode.md +21 -8
  35. package/extensions/codegraph-tools.ts +68 -6
  36. package/extensions/gentle-agents.ts +99 -12
  37. package/extensions/gentle-ai.ts +284 -52
  38. package/extensions/gentle-herdr-activity.ts +2 -3
  39. package/extensions/gentle-shell.ts +27 -12
  40. package/extensions/gentle-todo.ts +43 -9
  41. package/extensions/quiet-tools.ts +1 -1
  42. package/extensions/session-scratch.ts +365 -0
  43. package/extensions/startup-banner.ts +2 -3
  44. package/lib/agent-home.ts +8 -3
  45. package/lib/agents-protocol.ts +3 -0
  46. package/lib/agents-runner.ts +55 -6
  47. package/lib/agents-view.ts +3 -1
  48. package/lib/bounded-writer-admission.ts +51 -0
  49. package/lib/command-palette-catalog.ts +0 -4
  50. package/lib/commit-slice-signal.ts +100 -0
  51. package/lib/context-backstop.ts +84 -0
  52. package/lib/delivered-test-lint.ts +80 -0
  53. package/lib/destructive-command-guard.ts +120 -9
  54. package/lib/gauge-theme.ts +146 -0
  55. package/lib/gentle-ai-binary.ts +28 -7
  56. package/lib/gentle-shell-bundled.ts +39 -3
  57. package/lib/gentle-shell-import-pi.ts +233 -0
  58. package/lib/gentle-shell-launcher.ts +145 -11
  59. package/lib/gentle-shell-uninstall.ts +18 -1
  60. package/lib/herdr-activity.ts +24 -0
  61. package/lib/native-review-cli.ts +9 -1
  62. package/lib/orchestrator-discovery.ts +5 -1
  63. package/lib/orchestrator-presence.ts +9 -0
  64. package/lib/orchestrator-scope.ts +21 -3
  65. package/lib/provider-contract-bundle.ts +99 -53
  66. package/lib/review-candidate-view-owner.ts +29 -19
  67. package/lib/review-candidate-view.ts +36 -2
  68. package/lib/review-risk-assessment.ts +43 -38
  69. package/lib/review-session-standing-permission.ts +3 -1
  70. package/lib/session-profile-freeze.ts +30 -1
  71. package/lib/session-scratch.ts +474 -0
  72. package/lib/shell-bar.ts +25 -25
  73. package/lib/shell-changes-view.ts +80 -13
  74. package/lib/shell-gauge.ts +49 -6
  75. package/lib/shell-todo.ts +23 -4
  76. package/lib/shell-usage-view.ts +5 -1
  77. package/lib/shell-usage.ts +7 -7
  78. package/lib/verifier-admission.ts +29 -0
  79. package/lib/yolo-temp-scratch.ts +69 -0
  80. package/package.json +1 -2
  81. package/runtime/gentle-ai-binary.mjs +28 -7
  82. package/runtime/gentle-shell-bundled.mjs +38 -2
  83. package/runtime/gentle-shell-import-pi.mjs +234 -0
  84. package/runtime/gentle-shell-launcher.mjs +143 -9
  85. package/runtime/gentle-shell-uninstall.mjs +18 -1
  86. package/runtime/native-review-cli.mjs +8 -0
  87. package/runtime/review-risk-assessment.mjs +43 -38
  88. package/scripts/build-runtime-modules.mjs +1 -0
  89. package/scripts/bundled-engram-migration.mjs +125 -0
  90. package/scripts/gentle-ai-installer.mjs +10 -10
  91. package/scripts/installer-server.mjs +14 -4
  92. package/scripts/main-channel.mjs +3 -2
  93. package/scripts/run-test-suite.mjs +5 -0
  94. package/scripts/verify-package-files.mjs +19 -13
  95. package/skills/gentle-ai/SKILL.md +3 -3
  96. package/skills/work-unit-commits/SKILL.md +5 -3
  97. package/tests/agent-risk-escalation.test.ts +37 -10
  98. package/tests/agents-runner.test.ts +260 -3
  99. package/tests/autonomous-guard.test.ts +122 -0
  100. package/tests/bounded-writer-admission.test.ts +11 -3
  101. package/tests/card-content-policy.test.ts +19 -9
  102. package/tests/child-safety.test.ts +39 -0
  103. package/tests/codegraph-tools.test.ts +123 -2
  104. package/tests/command-palette.test.ts +14 -4
  105. package/tests/commit-slice-signal.test.ts +162 -0
  106. package/tests/context-backstop.test.ts +159 -0
  107. package/tests/delegate-for-reason-contract.test.ts +91 -24
  108. package/tests/delivered-test-lint.test.ts +105 -0
  109. package/tests/destructive-command-guard.test.ts +85 -1
  110. package/tests/engram-safety.test.ts +1 -1
  111. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  115. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  116. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  117. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  118. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  119. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  120. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  121. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  122. package/tests/gauge-theme.test.ts +120 -0
  123. package/tests/gentle-agents.test.ts +282 -3
  124. package/tests/gentle-ai-binary.test.ts +1 -1
  125. package/tests/gentle-ai-child-guards.test.ts +61 -0
  126. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  127. package/tests/gentle-ai-installer.test.ts +51 -51
  128. package/tests/gentle-shell-bin.test.ts +163 -0
  129. package/tests/gentle-shell-bundled.test.ts +169 -6
  130. package/tests/gentle-shell-import-pi.test.ts +262 -0
  131. package/tests/gentle-shell-launcher.test.ts +79 -0
  132. package/tests/gentle-shell-uninstall.test.ts +30 -0
  133. package/tests/gentle-shell.test.ts +42 -9
  134. package/tests/gentle-todo.test.ts +179 -0
  135. package/tests/headless-delivery-question.test.ts +58 -0
  136. package/tests/herdr-activity.test.ts +34 -1
  137. package/tests/installer-server.test.ts +10 -2
  138. package/tests/installer-windows-bootstrap.test.ts +33 -4
  139. package/tests/native-review-capability-contract.test.ts +7 -1
  140. package/tests/native-review-parity.test.ts +22 -1
  141. package/tests/no-self-pause-contract.test.ts +5 -2
  142. package/tests/odd-integration.test.ts +5 -1
  143. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  144. package/tests/odd-routing-contract.test.ts +115 -30
  145. package/tests/orchestrator-budget.test.ts +19 -8
  146. package/tests/orchestrator-discovery.test.ts +29 -0
  147. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  148. package/tests/orchestrator-scope.test.ts +66 -0
  149. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  150. package/tests/package-manifest.test.ts +24 -20
  151. package/tests/pi-update-launcher.test.ts +21 -2
  152. package/tests/primary-session-prompt.test.ts +101 -0
  153. package/tests/provider-contract-bundle.test.ts +118 -0
  154. package/tests/provider-contract-mirror.test.ts +24 -0
  155. package/tests/quiet-tool-rendering.test.ts +23 -0
  156. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  157. package/tests/rdd-status-line.test.ts +79 -0
  158. package/tests/review-agent-end-preflight.test.ts +6 -0
  159. package/tests/review-candidate-view.test.ts +49 -0
  160. package/tests/review-contract-prompt.test.ts +5 -2
  161. package/tests/review-controller-native-routing.test.ts +14 -1
  162. package/tests/review-controller.test.ts +1 -1
  163. package/tests/review-host-relay-routing.test.ts +59 -2
  164. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  165. package/tests/review-owner-identity.test.ts +132 -0
  166. package/tests/review-risk-assessment.test.ts +163 -76
  167. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  168. package/tests/review-session-standing-permission.test.ts +27 -1
  169. package/tests/run-test-suite.test.ts +15 -0
  170. package/tests/runtime-harness.mjs +16 -6
  171. package/tests/session-changes-shell.test.ts +1 -1
  172. package/tests/session-profile-freeze.test.ts +23 -0
  173. package/tests/session-scratch.test.ts +945 -0
  174. package/tests/session-subject-routing-contract.test.ts +2 -1
  175. package/tests/shell-bar.test.ts +7 -0
  176. package/tests/shell-changes-view.test.ts +137 -1
  177. package/tests/shell-gauge.test.ts +42 -0
  178. package/tests/small-task-inline-contract.test.ts +37 -0
  179. package/tests/standalone-config-home.test.ts +324 -0
  180. package/tests/startup-banner.test.ts +5 -4
  181. package/tests/startup-home-repo.test.ts +34 -0
  182. package/tests/support/orchestrator-modules.ts +1 -0
  183. package/tests/task-size-routing-contract.test.ts +69 -9
  184. package/tests/test-scratch-contract.test.ts +102 -0
  185. package/tests/test-strength-contract.test.ts +36 -1
  186. package/tests/tool-batching-contract.test.ts +64 -0
  187. package/tests/verification-proportionality.test.ts +46 -0
  188. package/tests/verifier-admission.test.ts +137 -0
  189. package/tests/verify-agent-contract.test.ts +8 -8
  190. package/tests/windows-hidden-processes.test.ts +5 -0
  191. package/tests/writer-edit-surface-scope.test.ts +3 -0
  192. package/tests/writer-feature-document-admission.test.ts +156 -0
  193. package/tests/yolo-mode.test.ts +18 -0
  194. package/tests/yolo-temp-scratch.test.ts +71 -0
  195. package/scripts/mirror-odd-routing.mjs +0 -242
  196. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  197. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  198. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  199. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  200. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  201. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  202. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -267,8 +267,9 @@ export type VerificationTier = (typeof VERIFICATION_TIER)[keyof typeof VERIFICAT
267
267
  // ---------------------------------------------------------------------------
268
268
  // Agent-raised risk (gentle-shell#1494). The agent that made the change may
269
269
  // raise its risk to high by citing one item of the high-risk list in the
270
- // always-on Task Size section; it can never lower a tier. Lowering stays with
271
- // the deterministic native assessment.
270
+ // always-on Task Size section; it can never lower a tier. That escalation is
271
+ // the only way a plan gets an independent verifier: a native high or a failed
272
+ // assessment informs but never adds one by itself.
272
273
  // ---------------------------------------------------------------------------
273
274
 
274
275
  export const HIGH_RISK_ITEMS = Object.freeze({
@@ -305,10 +306,17 @@ export function decodeAgentRiskEscalation(value: unknown): AgentRiskEscalation {
305
306
  return Object.freeze({ item: item as HighRiskItem, reason });
306
307
  }
307
308
 
308
- /** Raises passive or medium to high when the agent escalated; never lowers. */
309
- export function escalatedRisk(risk: VerificationTier, escalation: AgentRiskEscalation | undefined): VerificationTier {
310
- if (escalation === undefined) return risk;
311
- return risk === VERIFICATION_TIER.PASSIVE || risk === VERIFICATION_TIER.MEDIUM ? VERIFICATION_TIER.HIGH : risk;
309
+ /**
310
+ * The risk tier the verification plan follows. The independent verifier is
311
+ * for concrete occasions only: the plan is high only when the agent escalated
312
+ * a named Task Size high-risk item. Without an escalation a native passive
313
+ * stays passive, and every other native tier -- medium, high, or unassessable
314
+ * (a failed or unavailable assessment) -- is medium: it informs, but adds no
315
+ * verifier by itself. The native tier stays visible to callers as `nativeRisk`.
316
+ */
317
+ export function verificationRisk(nativeRisk: VerificationTier, escalation: AgentRiskEscalation | undefined): VerificationTier {
318
+ if (escalation !== undefined) return VERIFICATION_TIER.HIGH;
319
+ return nativeRisk === VERIFICATION_TIER.PASSIVE ? VERIFICATION_TIER.PASSIVE : VERIFICATION_TIER.MEDIUM;
312
320
  }
313
321
 
314
322
  export const RDD_LINE = {
@@ -342,7 +350,9 @@ export type NativeReviewOutcome = (typeof NATIVE_REVIEW_OUTCOME)[keyof typeof NA
342
350
 
343
351
  export interface VerificationPlanInput {
344
352
  readonly rddLine: RddLine;
353
+ /** The plan risk from `verificationRisk`, not the raw native tier. */
345
354
  readonly risk: VerificationTier;
355
+ /** Reported context only: the writer's model or effort never changes the plan. */
346
356
  readonly writerProfile: WriterProfile;
347
357
  /**
348
358
  * Native review outcome for the current candidate. Only consulted when
@@ -366,7 +376,7 @@ export interface VerificationPlan {
366
376
  * naming which branch fired, up to and including the word "and"; the
367
377
  * tier-specific clause is appended after it.
368
378
  */
369
- function riskGatedPlan(gatePrefix: string, risk: VerificationTier, writerProfile: WriterProfile): VerificationPlan {
379
+ function riskGatedPlan(gatePrefix: string, risk: VerificationTier): VerificationPlan {
370
380
  if (risk === VERIFICATION_TIER.PASSIVE) {
371
381
  return Object.freeze({
372
382
  writerSelfVerification: false,
@@ -376,26 +386,22 @@ function riskGatedPlan(gatePrefix: string, risk: VerificationTier, writerProfile
376
386
  });
377
387
  }
378
388
 
379
- if (risk === VERIFICATION_TIER.MEDIUM) {
380
- const smallModelBias = writerProfile === WRITER_PROFILE.SMALL;
389
+ if (risk === VERIFICATION_TIER.HIGH) {
381
390
  return Object.freeze({
382
391
  writerSelfVerification: true,
383
392
  structuralReadbackOnly: false,
384
- independentVerifier: smallModelBias,
385
- reason: smallModelBias
386
- ? `${gatePrefix} the native assessment reports medium risk, but the small-model bias raises it one tier to high for verification purposes: the writer self-verifies and a separate independent verifier also runs.`
387
- : `${gatePrefix} the native assessment reports medium risk with a large writer profile: the writer's self-verification stands and no separate verifier is required.`,
393
+ independentVerifier: true,
394
+ reason: `${gatePrefix} an agent escalation names a Task Size high-risk item: the writer self-verifies and a separate independent verifier runs.`,
388
395
  });
389
396
  }
390
397
 
391
- // high or unassessable.
398
+ // medium, or an unassessable tier that did not go through verificationRisk:
399
+ // both inform only.
392
400
  return Object.freeze({
393
401
  writerSelfVerification: true,
394
402
  structuralReadbackOnly: false,
395
- independentVerifier: true,
396
- reason: risk === VERIFICATION_TIER.UNASSESSABLE
397
- ? `${gatePrefix} the native risk assessment could not be produced, so the candidate is treated as high risk: the writer self-verifies and a separate independent verifier always runs.`
398
- : `${gatePrefix} the native assessment reports high risk: the writer self-verifies and a separate independent verifier always runs.`,
403
+ independentVerifier: false,
404
+ reason: `${gatePrefix} no Task Size high-risk item was escalated: the writer's self-verification stands and no separate verifier runs, whatever the native tier or the model that wrote the change.`,
399
405
  });
400
406
  }
401
407
 
@@ -416,24 +422,23 @@ function nonClosedOutcomeClause(outcome: NativeReviewOutcome): string {
416
422
 
417
423
  /**
418
424
  * Computes who verifies a delegated writer's change. Verification follows the
419
- * native risk tier whatever the RDD line and native review outcome
425
+ * plan risk tier whatever the RDD line and native review outcome
420
426
  * (verify-always-rdd-high S1, superseding the gentle-pi#662/#668 on-path): the
421
427
  * native review is an additional outside view and never replaces or skips the
422
428
  * tier's verification. `nativeReviewOutcome` only shapes the reason text.
423
429
  *
430
+ * `risk` is the plan risk from `verificationRisk`; `writerProfile` is never
431
+ * consulted, so the model or effort that wrote the change never adds a
432
+ * verifier.
433
+ *
424
434
  * - `passive`: structural readback by the parent only; no verifier, no tests.
425
- * - `medium`: the writer self-verifies; a separate independent verifier runs
426
- * only when `writerProfile` is `"small"` -- the small-model bias raises the
427
- * tier by one for verification purposes (medium -> high). An unknown or
428
- * omitted writer profile resolves to `"small"` before it reaches this
429
- * function (see `resolveWriterProfile`), so an unrecognized profile never
430
- * lowers a tier below what a known small model would get.
431
- * - `high` or `unassessable` (a failed or unrecognized native assessment is
432
- * always treated as high): the writer self-verifies and an independent
433
- * verifier always runs.
435
+ * - `medium` (also an `unassessable` tier passed directly): the writer
436
+ * self-verifies; no separate verifier.
437
+ * - `high` (only through an agent escalation naming a Task Size high-risk
438
+ * item): the writer self-verifies and an independent verifier runs.
434
439
  */
435
440
  export function verificationPlan(input: VerificationPlanInput): VerificationPlan {
436
- const { rddLine, risk, writerProfile } = input;
441
+ const { rddLine, risk } = input;
437
442
 
438
443
  if (rddLine === RDD_LINE.ON) {
439
444
  const outcome = input.nativeReviewOutcome ?? NATIVE_REVIEW_OUTCOME.UNKNOWN;
@@ -443,14 +448,13 @@ export function verificationPlan(input: VerificationPlanInput): VerificationPlan
443
448
  return riskGatedPlan(
444
449
  `receipt-driven development is on and ${clause}; the native review is an additional outside view that never replaces verification, so the risk-gated path applies exactly as receipt-driven development off, and`,
445
450
  risk,
446
- writerProfile,
447
451
  );
448
452
  }
449
453
 
450
454
  // rddLine is "off" or "unknown" here -- both gate on the native risk
451
455
  // assessment identically, so an unknown RDD line never lowers a tier, and
452
456
  // neither line ever consults nativeReviewOutcome.
453
- return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk, writerProfile);
457
+ return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk);
454
458
  }
455
459
 
456
460
  // ---------------------------------------------------------------------------
@@ -475,15 +479,16 @@ export interface WriterModelProfileLike {
475
479
  const SMALL_MODEL_ID_TOKEN = /(?:^|[-_./:\s])mini(?:$|[-_./:\s])/i;
476
480
 
477
481
  /**
478
- * A writer profile counts as a "small model" for the medium-risk bias when:
482
+ * A writer profile counts as a "small model" (reported by ASSESS as
483
+ * `writerProfile`; it never changes the verification plan) when:
479
484
  * - its resolved effort is `low`, or
480
485
  * - its resolved model id carries `mini` as a whole token (see
481
486
  * `SMALL_MODEL_ID_TOKEN`), or
482
487
  * - it carries NO signal at all -- no profile object, or a profile with
483
488
  * neither a model id nor a resolved effort. An unknown or omitted writer
484
- * profile fails closed to small rather than defaulting to large, so a
485
- * caller that cannot resolve the writer's profile never gets a weaker
486
- * verification plan than a known small model would (gentle-pi#662).
489
+ * profile fails closed to small rather than defaulting to large. The
490
+ * result is reported in the assessment only: the verification plan never
491
+ * reads the writer profile, so it cannot add or remove a verifier.
487
492
  *
488
493
  * A profile that carries a model id or an effort, just not a matching one
489
494
  * (for example `{model: {id: "claude-sonnet-5"}, thinking: "high"}`), is
@@ -501,9 +506,9 @@ export function isSmallWriterProfile(profile: WriterModelProfileLike | undefined
501
506
  }
502
507
 
503
508
  /**
504
- * Maps a resolved writer profile to the `writerProfile` input `verificationPlan`
505
- * expects. An unknown or omitted profile never lowers a tier: it resolves to
506
- * `"small"`, exactly like `isSmallWriterProfile` (gentle-pi#662).
509
+ * Maps a resolved writer profile to the reported `writerProfile`. An unknown
510
+ * or omitted profile resolves to `"small"`, exactly like
511
+ * `isSmallWriterProfile` (gentle-pi#662).
507
512
  */
508
513
  export function resolveWriterProfile(profile: WriterModelProfileLike | undefined): WriterProfile {
509
514
  return isSmallWriterProfile(profile) ? WRITER_PROFILE.SMALL : WRITER_PROFILE.LARGE;
@@ -15,6 +15,7 @@ export interface GitCommandOptions {
15
15
  timeout: number;
16
16
  maxBuffer: number;
17
17
  windowsHide?: boolean;
18
+ stdio?: ["ignore", "pipe", "pipe"];
18
19
  }
19
20
 
20
21
  export type GitAsyncRunner = (command: string, args: readonly string[], options: GitCommandOptions) => Promise<{ stdout: string }>;
@@ -133,7 +134,8 @@ export function resolveCanonicalGitRepositoryIdentitySync(
133
134
  run: GitSyncRunner = execFileSync as unknown as GitSyncRunner,
134
135
  ): string | undefined {
135
136
  try {
136
- const output = run("git", ["-C", cwd, "rev-parse", "--git-common-dir"], { encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024, windowsHide: true }).trim();
137
+ // execFileSync otherwise echoes stderr on failure, even when caught below.
138
+ const output = run("git", ["-C", cwd, "rev-parse", "--git-common-dir"], { encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }).trim();
137
139
  if (output.length === 0 || output.includes("\n") || output.includes("\r")) return undefined;
138
140
  return canonicalRepositoryIdentity(realpathSync(resolve(cwd, output)));
139
141
  } catch {
@@ -27,9 +27,9 @@
27
27
  // `gentle-pi.session-profile/v1` entry with origin `local | repo | global` is
28
28
  // the integration point with slice 3b-i.
29
29
  import { createHash } from "node:crypto";
30
+ import { normalizeModelConfig, type AgentModelConfig } from "./model-routing-authority.ts";
30
31
  import { profileRoleEntries, profilesFilePath, readProfilesFileResult } from "./agent-profiles.ts";
31
32
  import { readProfilePinStatus, resolveProfilePin, resolveUnversionedProjectProfile } from "./agent-profile-pin.ts";
32
- import type { AgentModelConfig } from "./model-routing-authority.ts";
33
33
  import { readSessionProfileBinding } from "./session-profile-binding.ts";
34
34
  import type { WorktreeIdentity, WorktreeResolver } from "./session-worktree-registry.ts";
35
35
 
@@ -242,6 +242,7 @@ function describe(profile: InheritedProfile | undefined): string {
242
242
  */
243
243
  export function inheritedProfileDriftNotice(options: SessionProfileOptions): string | undefined {
244
244
  const resolution = resolveSessionProfile(options);
245
+ if (resolution.kind === "explicit") return bindingDefinitionDriftNotice(options, resolution);
245
246
  if (resolution.kind !== "inherited") return undefined;
246
247
  const current = resolveInheritedProfile(options);
247
248
  const key = fingerprint(current);
@@ -254,6 +255,34 @@ export function inheritedProfileDriftNotice(options: SessionProfileOptions): str
254
255
  return `el Gentleman: the default profile changed to ${describe(current)}, this session keeps ${describe(resolution.profile)}. Press Enter on a profile in /gentle:profiles to adopt it.`;
255
256
  }
256
257
 
258
+ // gentle-shell#2228: an explicit binding re-copies the profile definition at
259
+ // selection, so later edits to that entry in profiles.json no-op silently until
260
+ // the operator re-Enters. The same once-per-change fingerprint machine as the
261
+ // inherited notice names the drift; the comparison never adopts the edit.
262
+ function bindingDefinitionDriftNotice(
263
+ options: SessionProfileOptions,
264
+ resolution: { kind: "explicit"; name: string; modelProfiles: AgentModelConfig },
265
+ ): string | undefined {
266
+ const file = readProfilesFileResult(profilesFilePath(options.configHome));
267
+ if (file.status !== "valid") return undefined;
268
+ const definition = file.file.profiles[resolution.name];
269
+ if (definition === undefined) return undefined;
270
+ // Both sides normalize through the store's own route normalizer: a bound
271
+ // snapshot taken from the panel holds normalized routes, while raw string
272
+ // forms from other callers must hash equal to the same definition.
273
+ const normalized = (config: AgentModelConfig) =>
274
+ Object.fromEntries(Object.entries(config).map(([agent, route]) => [agent, normalizeModelConfig(route)]));
275
+ const key = createHash("sha256").update(stableJson({ modelProfiles: normalized(definition) })).digest("hex");
276
+ const current = createHash("sha256").update(stableJson({ modelProfiles: normalized(resolution.modelProfiles) })).digest("hex");
277
+ if (key === current) return undefined;
278
+ const notified = registry().notified;
279
+ const seen = notified.get(options.sessionId!) ?? new Set<string>();
280
+ if (seen.has(key)) return undefined;
281
+ seen.add(key);
282
+ notified.set(options.sessionId!, seen);
283
+ return `el Gentleman: the profile definition changed to "${resolution.name}" (profiles.json), this session keeps the binding from selection. Press Enter on a profile in /gentle:profiles to adopt it.`;
284
+ }
285
+
257
286
  /** Testing-only reset of every frozen snapshot and drift memory. */
258
287
  export function resetFrozenInheritedProfilesForTesting(): void {
259
288
  registry().frozen.clear();