@code-yeongyu/senpi 2026.10.16-2 → 2026.10.16-4

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 (154) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/bundle/chunks/{chunk-QQUHMCOM.js → chunk-2NO5ZOYE.js} +6 -6
  3. package/dist/bundle/chunks/chunk-74YFRVGQ.js +2 -0
  4. package/dist/bundle/chunks/{chunk-4R4PRTF5.js → chunk-CJRE5LLR.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-Q7RD3NOC.js → chunk-GY5O7NIQ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-KRAABTZY.js → chunk-JD4CV3GH.js} +2 -2
  7. package/dist/bundle/chunks/chunk-K2EVQGGT.js +31 -0
  8. package/dist/bundle/chunks/{chunk-SSEGIXRL.js → chunk-KUQ3K33L.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-V5JFWWGJ.js → chunk-KWC3MCLP.js} +1 -1
  10. package/dist/bundle/chunks/chunk-MLL7AGVM.js +3 -0
  11. package/dist/bundle/chunks/chunk-PAJKEG2L.js +8 -0
  12. package/dist/bundle/chunks/{chunk-DPRV3FR7.js → chunk-PWHAOWNH.js} +1 -1
  13. package/dist/bundle/chunks/chunk-WL4Y6XN6.js +18 -0
  14. package/dist/bundle/chunks/{chunk-VY43SSYL.js → chunk-XCRGEMY2.js} +144 -134
  15. package/dist/bundle/chunks/{cli-main-JDL52ADK.js → cli-main-I6JF2GPS.js} +1 -1
  16. package/dist/bundle/chunks/google-generative-ai-AI6PAUPA.js +2 -0
  17. package/dist/bundle/chunks/google-vertex-USNRIFF7.js +2 -0
  18. package/dist/bundle/chunks/google-vertex-adc-JZURL4XM.js +2 -0
  19. package/dist/bundle/chunks/{help-fast-path-SVCMHCYG.js → help-fast-path-APSHUDY6.js} +1 -1
  20. package/dist/bundle/chunks/{host-session-command-46PTZGIB.js → host-session-command-BUJUJHDA.js} +1 -1
  21. package/dist/bundle/chunks/{interactive-mode-7IM7YJYH.js → interactive-mode-4Q3TIXOZ.js} +1 -1
  22. package/dist/bundle/chunks/{multi-session-host-3MEEUMYU.js → multi-session-host-XEZB2XVR.js} +6 -5
  23. package/dist/bundle/chunks/{openai-codex-responses-T26WW2RE.js → openai-codex-responses-NV27Q6VV.js} +1 -1
  24. package/dist/bundle/chunks/{package-manager-cli-UCHXVBQ2.js → package-manager-cli-YWZHOZOY.js} +1 -1
  25. package/dist/bundle/chunks/{rotation-stream-QX7YSKDU.js → rotation-stream-HY3JPQCX.js} +1 -1
  26. package/dist/bundle/chunks/{rpc-mode-D4BJKWFL.js → rpc-mode-SLWXVXXE.js} +1 -1
  27. package/dist/bundle/chunks/{schedule-command-2RHSWBQO.js → schedule-command-JRWZGEDU.js} +1 -1
  28. package/dist/bundle/chunks/{session-control-endpoint-NBSRSAJS.js → session-control-endpoint-E2ZSEWXW.js} +1 -1
  29. package/dist/bundle/chunks/session-picker-5IZY7E3G.js +2 -0
  30. package/dist/bundle/chunks/session-worker.js +266 -255
  31. package/dist/bundle/cli.js +2 -2
  32. package/dist/bundle/index.js +1 -1
  33. package/dist/bundle/rpc-entry.js +1 -1
  34. package/dist/bundle/runtime-manifest.json +1 -1
  35. package/dist/core/agent-session-runtime.d.ts +15 -0
  36. package/dist/core/agent-session-runtime.js +53 -2
  37. package/dist/core/agent-session.d.ts +28 -0
  38. package/dist/core/agent-session.js +88 -3
  39. package/dist/core/context-files.d.ts +21 -0
  40. package/dist/core/context-files.js +175 -0
  41. package/dist/core/diagnostics.d.ts +1 -0
  42. package/dist/core/dynamic-prompt/intent-gate.d.ts +3 -0
  43. package/dist/core/dynamic-prompt/intent-gate.js +4 -3
  44. package/dist/core/dynamic-prompt/policies.d.ts +4 -1
  45. package/dist/core/dynamic-prompt/policies.js +3 -2
  46. package/dist/core/dynamic-prompt/working-task.d.ts +4 -1
  47. package/dist/core/dynamic-prompt/working-task.js +3 -2
  48. package/dist/core/dynamic-prompt/workstation.d.ts +4 -3
  49. package/dist/core/dynamic-prompt/workstation.js +1 -0
  50. package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +26 -3
  51. package/dist/core/extensions/builtin/anthropic-subscription/account-management.d.ts +5 -0
  52. package/dist/core/extensions/builtin/anthropic-subscription/account-management.js +40 -0
  53. package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
  54. package/dist/core/extensions/builtin/anthropic-subscription/accounts.js +1 -1
  55. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +1 -1
  56. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
  57. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +10 -5
  58. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +158 -17
  59. package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.d.ts +13 -0
  60. package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.js +89 -0
  61. package/dist/core/extensions/builtin/goal/index.js +18 -8
  62. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -0
  63. package/dist/core/extensions/builtin/goal/reload-reengagement.js +5 -0
  64. package/dist/core/extensions/builtin/index.js +3 -0
  65. package/dist/core/extensions/builtin/permission-system/always-scope.d.ts +34 -0
  66. package/dist/core/extensions/builtin/permission-system/always-scope.js +77 -0
  67. package/dist/core/extensions/builtin/permission-system/index.js +3 -1
  68. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +1 -1
  69. package/dist/core/extensions/builtin/permission-system/parsers.js +24 -3
  70. package/dist/core/extensions/builtin/permission-system/prompt.js +5 -1
  71. package/dist/core/extensions/builtin/permission-system/service.js +1 -1
  72. package/dist/core/extensions/builtin/permission-system/types.d.ts +7 -0
  73. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  74. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.d.ts +7 -0
  75. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.js +83 -0
  76. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  77. package/dist/core/extensions/builtin/prompt-preset/matchers.js +12 -0
  78. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  79. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  80. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  81. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +16 -0
  82. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +19 -0
  83. package/dist/core/extensions/builtin/vertex-model-garden/index.d.ts +6 -0
  84. package/dist/core/extensions/builtin/vertex-model-garden/index.js +27 -0
  85. package/dist/core/extensions/builtin/vertex-model-garden/models.d.ts +3 -0
  86. package/dist/core/extensions/builtin/vertex-model-garden/models.js +34 -0
  87. package/dist/core/extensions/builtin/vertex-model-garden/transport.d.ts +14 -0
  88. package/dist/core/extensions/builtin/vertex-model-garden/transport.js +35 -0
  89. package/dist/core/extensions/types.d.ts +32 -1
  90. package/dist/core/package-manager.js +4 -32
  91. package/dist/core/project-trust.d.ts +8 -0
  92. package/dist/core/project-trust.js +12 -0
  93. package/dist/core/provider-api-key-auth.d.ts +9 -1
  94. package/dist/core/provider-api-key-auth.js +34 -0
  95. package/dist/core/provider-composer.d.ts +23 -0
  96. package/dist/core/resource-loader.d.ts +5 -7
  97. package/dist/core/resource-loader.js +11 -84
  98. package/dist/core/retry-fallback/circuit-probes.d.ts +1 -0
  99. package/dist/core/retry-fallback/circuit-probes.js +3 -0
  100. package/dist/core/retry-fallback/circuit.d.ts +5 -0
  101. package/dist/core/retry-fallback/circuit.js +27 -4
  102. package/dist/core/retry-fallback/controller.d.ts +9 -4
  103. package/dist/core/retry-fallback/controller.js +21 -4
  104. package/dist/core/settings-manager.d.ts +19 -0
  105. package/dist/core/settings-manager.js +27 -1
  106. package/dist/core/settings-shapes.d.ts +4 -0
  107. package/dist/core/skills-prompt.d.ts +18 -0
  108. package/dist/core/skills-prompt.js +131 -0
  109. package/dist/core/skills.d.ts +1 -9
  110. package/dist/core/skills.js +1 -59
  111. package/dist/core/trust-manager.d.ts +13 -4
  112. package/dist/core/trust-manager.js +52 -21
  113. package/dist/core/turn-deadline.d.ts +92 -0
  114. package/dist/core/turn-deadline.js +179 -0
  115. package/dist/main.js +23 -4
  116. package/dist/modes/interactive/interactive-mode.js +10 -3
  117. package/dist/modes/rpc/abort-grace.d.ts +9 -0
  118. package/dist/modes/rpc/abort-grace.js +11 -0
  119. package/dist/modes/rpc/client-admission-record.d.ts +1 -0
  120. package/dist/modes/rpc/client-admissions.d.ts +6 -3
  121. package/dist/modes/rpc/client-admissions.js +9 -4
  122. package/dist/modes/rpc/connection-handler.js +8 -3
  123. package/dist/modes/rpc/effect-commands.d.ts +1 -3
  124. package/dist/modes/rpc/effect-commands.js +4 -5
  125. package/dist/modes/rpc/effect-outcome-copy.d.ts +4 -4
  126. package/dist/modes/rpc/effect-outcome-copy.js +4 -4
  127. package/dist/modes/rpc/effect-record.d.ts +2 -0
  128. package/dist/modes/rpc/effect-record.js +2 -0
  129. package/dist/modes/rpc/effect-status.js +1 -1
  130. package/dist/modes/rpc/host-handover-parked.d.ts +6 -0
  131. package/dist/modes/rpc/host-handover-parked.js +1 -0
  132. package/dist/modes/rpc/host-handover-reopen.js +11 -2
  133. package/dist/modes/rpc/rpc-client.d.ts +28 -2
  134. package/dist/modes/rpc/rpc-client.js +24 -11
  135. package/dist/modes/rpc/rpc-request-deadline.d.ts +12 -0
  136. package/dist/modes/rpc/rpc-request-deadline.js +15 -0
  137. package/dist/modes/rpc/session-command-router.js +7 -4
  138. package/dist/modes/rpc/session-registry-attach.js +6 -1
  139. package/dist/modes/rpc/session-registry-claims.d.ts +22 -2
  140. package/dist/modes/rpc/session-registry-claims.js +68 -0
  141. package/dist/modes/rpc/session-registry-opening.d.ts +23 -0
  142. package/dist/modes/rpc/session-registry-opening.js +57 -0
  143. package/dist/modes/rpc/session-registry.d.ts +1 -0
  144. package/dist/modes/rpc/session-registry.js +48 -60
  145. package/dist/modes/rpc/session-unload.js +4 -1
  146. package/docs/rpc.md +10 -3
  147. package/docs/settings.md +16 -1
  148. package/package.json +6 -6
  149. package/dist/bundle/chunks/chunk-6MEETY7N.js +0 -8
  150. package/dist/bundle/chunks/chunk-7ASXJ4AD.js +0 -47
  151. package/dist/bundle/chunks/chunk-TEGEPQIA.js +0 -3
  152. package/dist/bundle/chunks/google-generative-ai-YNIOL5FT.js +0 -2
  153. package/dist/bundle/chunks/google-vertex-CF6AYVXL.js +0 -2
  154. package/dist/bundle/chunks/session-picker-6TSBGWOA.js +0 -2
@@ -4,6 +4,7 @@ import type { RetryPolicyProfile } from "@earendil-works/pi-ai/utils/retry-profi
4
4
  import type { TuiMode as RendererTuiMode, ScrollViewScrollbar, TerminalCapabilities, WheelScrollLines } from "@earendil-works/pi-tui";
5
5
  import type { CompactionModelSelector, CompactionSettings } from "./compaction-settings-access.ts";
6
6
  import { type ResolvedCompactionSettings } from "./compaction-settings-resolver.ts";
7
+ import type { ContextFileLimits } from "./context-files.ts";
7
8
  import { type FallbackCircuitSettings, type ResolvedFallbackCircuitSettings } from "./retry-fallback/circuit.ts";
8
9
  import { type ResolvedHintPolicySettings, type ResolvedRetryFallbackSettings, type RetrySettings as RetrySettingsConfig } from "./retry-fallback/settings.ts";
9
10
  import { parseSettingsJson } from "./settings-json.ts";
@@ -68,6 +69,7 @@ export interface Settings {
68
69
  compaction?: CompactionSettings & {
69
70
  model?: string;
70
71
  };
72
+ contextFiles?: ContextFileLimits;
71
73
  branchSummary?: BranchSummarySettings;
72
74
  retry?: RetrySettingsConfig;
73
75
  fallback?: FallbackCircuitSettings;
@@ -108,6 +110,8 @@ export interface Settings {
108
110
  maxPerUserInput?: number;
109
111
  maxToolFreePerMinute?: number;
110
112
  };
113
+ turn_inactivity_ms?: number;
114
+ turn_max_runtime_ms?: number;
111
115
  todo?: TodoSettings;
112
116
  recommendedModels?: string[];
113
117
  favoriteModels?: string[];
@@ -165,6 +169,8 @@ export declare function getSettingsPath(cwd: string, agentDir: string, scope: Se
165
169
  export declare function getInMemorySettingsPath(scope: SettingsScope): string;
166
170
  export interface SettingsManagerCreateOptions {
167
171
  projectTrusted?: boolean;
172
+ /** The project trust exists only because the session runs Full access (senpi#3414): leaving it withdraws the trust. */
173
+ trustFromFullAccess?: boolean;
168
174
  }
169
175
  export interface SettingsStorage {
170
176
  withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void;
@@ -274,9 +280,22 @@ export declare class SettingsManager {
274
280
  maxPerUserInput: number;
275
281
  maxToolFreePerMinute: number;
276
282
  };
283
+ getTurnInactivityMs(): number | undefined;
284
+ getTurnMaxRuntimeMs(): number | undefined;
277
285
  getTodoFirstTurnPlan(): TodoFirstTurnPlan;
278
286
  getTodoTurnEndBackstop(): boolean;
279
287
  isProjectTrusted(): boolean;
288
+ /**
289
+ * True when this session's project trust came only from its Full access preset (never asked, never
290
+ * saved): leaving Full access withdraws it, and going back restores it (senpi#3414, #3420).
291
+ */
292
+ private trustFromFullAccess;
293
+ isTrustFromFullAccess(): boolean;
294
+ /** True when a Full-access-only trust was withdrawn in this session and Full access may restore it. */
295
+ canRestoreFullAccessTrust(): boolean;
296
+ /** Counts project-trust changes; a reload compares it before installing what it loaded (#3420). */
297
+ private trustEpoch;
298
+ getTrustEpoch(): number;
280
299
  setProjectTrusted(trusted: boolean): void;
281
300
  reload(): Promise<void>;
282
301
  getSelectedSettingsSources(): SettingsSourceSelection[];
@@ -465,6 +465,13 @@ export class SettingsManager {
465
465
  this.selectedSources = new Map();
466
466
  this.sourceListeners = [];
467
467
  this.providerSettingsListeners = new Set();
468
+ /**
469
+ * True when this session's project trust came only from its Full access preset (never asked, never
470
+ * saved): leaving Full access withdraws it, and going back restores it (senpi#3414, #3420).
471
+ */
472
+ this.trustFromFullAccess = false;
473
+ /** Counts project-trust changes; a reload compares it before installing what it loaded (#3420). */
474
+ this.trustEpoch = 0;
468
475
  this.storage = storage;
469
476
  this.globalSettings = initialGlobal;
470
477
  this.projectSettings = initialProject;
@@ -506,7 +513,9 @@ export class SettingsManager {
506
513
  if (projectLoad.error) {
507
514
  initialErrors.push(toSettingsError("project", projectLoad.error, settingsPaths.project));
508
515
  }
509
- return new SettingsManager(storage, globalLoad.settings, projectLoad.settings, globalLoad.error, projectLoad.error, initialErrors, projectTrusted, initialSources, settingsPaths);
516
+ const manager = new SettingsManager(storage, globalLoad.settings, projectLoad.settings, globalLoad.error, projectLoad.error, initialErrors, projectTrusted, initialSources, settingsPaths);
517
+ manager.trustFromFullAccess = options.trustFromFullAccess === true;
518
+ return manager;
510
519
  }
511
520
  /** Create an in-memory SettingsManager (no file I/O) */
512
521
  static inMemory(settings = {}, options = {}) {
@@ -717,6 +726,12 @@ export class SettingsManager {
717
726
  maxToolFreePerMinute: limit(configured?.maxToolFreePerMinute, MAX_TOOL_FREE_ENGINE_TURNS_PER_WINDOW),
718
727
  };
719
728
  }
729
+ getTurnInactivityMs() {
730
+ return this.settings.turn_inactivity_ms;
731
+ }
732
+ getTurnMaxRuntimeMs() {
733
+ return this.settings.turn_max_runtime_ms;
734
+ }
720
735
  getTodoFirstTurnPlan() {
721
736
  const configured = this.settings.todo?.firstTurnPlan;
722
737
  return configured === "remind" || configured === "off" ? configured : "force";
@@ -728,11 +743,22 @@ export class SettingsManager {
728
743
  isProjectTrusted() {
729
744
  return this.projectTrusted;
730
745
  }
746
+ isTrustFromFullAccess() {
747
+ return this.trustFromFullAccess && this.projectTrusted;
748
+ }
749
+ /** True when a Full-access-only trust was withdrawn in this session and Full access may restore it. */
750
+ canRestoreFullAccessTrust() {
751
+ return this.trustFromFullAccess && !this.projectTrusted;
752
+ }
753
+ getTrustEpoch() {
754
+ return this.trustEpoch;
755
+ }
731
756
  setProjectTrusted(trusted) {
732
757
  if (this.projectTrusted === trusted) {
733
758
  return;
734
759
  }
735
760
  this.projectTrusted = trusted;
761
+ this.trustEpoch += 1;
736
762
  this.modifiedProjectFields.clear();
737
763
  this.modifiedProjectNestedFields.clear();
738
764
  if (!trusted) {
@@ -49,4 +49,8 @@ export interface OpenAISettings {
49
49
  export interface ProviderConcurrencySettings {
50
50
  maxConcurrency?: number;
51
51
  }
52
+ export interface TurnDeadlineSettings {
53
+ turn_inactivity_ms?: number;
54
+ turn_max_runtime_ms?: number;
55
+ }
52
56
  //# sourceMappingURL=settings-shapes.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { ResourceDiagnostic } from "./diagnostics.ts";
2
+ import type { Skill } from "./skills.ts";
3
+ export declare const COMPACT_SKILL_CATALOG_COUNT = 100;
4
+ export declare const COMPACT_SKILL_CATALOG_DESCRIPTION_BYTES: number;
5
+ export declare const MAX_SKILL_CATALOG_BYTES: number;
6
+ /** A catalog this large is listed by name plus a one-line description, since it rides on every request. */
7
+ export declare function isLargeSkillCatalog(skills: readonly Skill[]): boolean;
8
+ export declare function warnIfLargeSkillCatalog(skills: readonly Skill[]): ResourceDiagnostic[];
9
+ /**
10
+ * Format skills for inclusion in a system prompt.
11
+ * Uses XML format per Agent Skills standard.
12
+ * See: https://agentskills.io/integrate-skills
13
+ *
14
+ * Skills with disableModelInvocation=true are excluded from the prompt
15
+ * (they can only be invoked explicitly via /skill:name commands).
16
+ */
17
+ export declare function formatSkillsForPrompt(skills: Skill[], fileReadTool?: "read" | "bash"): string;
18
+ //# sourceMappingURL=skills-prompt.d.ts.map
@@ -0,0 +1,131 @@
1
+ export const COMPACT_SKILL_CATALOG_COUNT = 100;
2
+ export const COMPACT_SKILL_CATALOG_DESCRIPTION_BYTES = 64 * 1024;
3
+ export const MAX_SKILL_CATALOG_BYTES = 48 * 1024;
4
+ const ONE_LINE_DESCRIPTION_MAX_CHARS = 160;
5
+ /** A catalog this large is listed by name plus a one-line description, since it rides on every request. */
6
+ export function isLargeSkillCatalog(skills) {
7
+ if (skills.length > COMPACT_SKILL_CATALOG_COUNT)
8
+ return true;
9
+ const bytes = skills.reduce((sum, skill) => sum + Buffer.byteLength(skill.description, "utf8"), 0);
10
+ return bytes > COMPACT_SKILL_CATALOG_DESCRIPTION_BYTES;
11
+ }
12
+ export function warnIfLargeSkillCatalog(skills) {
13
+ const catalog = buildSkillCatalog(skills, "read");
14
+ if (!catalog.compact && !catalog.namesOnly)
15
+ return [];
16
+ return [
17
+ {
18
+ type: "warning",
19
+ category: "size-limit",
20
+ message: catalog.namesOnly
21
+ ? `The skills section is limited to ${MAX_SKILL_CATALOG_BYTES / 1024} KiB: descriptions were dropped and ${catalog.omitted} skills were omitted. Remove unused skills to shrink every request.`
22
+ : `The skills catalog uses one-line descriptions to limit request size (above ${COMPACT_SKILL_CATALOG_COUNT} skills, ${COMPACT_SKILL_CATALOG_DESCRIPTION_BYTES / 1024} KiB of descriptions, or ${MAX_SKILL_CATALOG_BYTES / 1024} KiB rendered). Remove unused skills to shrink every request.`,
23
+ },
24
+ ];
25
+ }
26
+ function buildSkillCatalog(skills, fileReadTool) {
27
+ const visible = skills.filter((skill) => !skill.disableModelInvocation);
28
+ const compact = isLargeSkillCatalog(visible);
29
+ const options = { fileReadTool, compact, namesOnly: false, omitted: 0 };
30
+ const prompt = renderSkills(visible, options);
31
+ if (Buffer.byteLength(prompt) <= MAX_SKILL_CATALOG_BYTES)
32
+ return { prompt, compact, namesOnly: false, omitted: 0 };
33
+ const compactPrompt = compact ? prompt : renderSkills(visible, { ...options, compact: true });
34
+ if (Buffer.byteLength(compactPrompt) <= MAX_SKILL_CATALOG_BYTES) {
35
+ return { prompt: compactPrompt, compact: true, namesOnly: false, omitted: 0 };
36
+ }
37
+ const minimal = { ...options, namesOnly: true };
38
+ const namesPrompt = renderSkills(visible, minimal);
39
+ if (Buffer.byteLength(namesPrompt) <= MAX_SKILL_CATALOG_BYTES) {
40
+ return { prompt: namesPrompt, compact, namesOnly: true, omitted: 0 };
41
+ }
42
+ let low = 0;
43
+ let high = visible.length;
44
+ while (low < high) {
45
+ const count = Math.ceil((low + high) / 2);
46
+ const candidate = renderSkills(visible.slice(0, count), { ...minimal, omitted: visible.length - count });
47
+ if (Buffer.byteLength(candidate) <= MAX_SKILL_CATALOG_BYTES)
48
+ low = count;
49
+ else
50
+ high = count - 1;
51
+ }
52
+ const omitted = visible.length - low;
53
+ return {
54
+ prompt: renderSkills(visible.slice(0, low), { ...minimal, omitted }),
55
+ compact,
56
+ namesOnly: true,
57
+ omitted,
58
+ };
59
+ }
60
+ function oneLineDescription(description) {
61
+ const firstLine = description.trim().split("\n", 1)[0]?.trim() ?? "";
62
+ return firstLine.length <= ONE_LINE_DESCRIPTION_MAX_CHARS
63
+ ? firstLine
64
+ : `${firstLine.slice(0, ONE_LINE_DESCRIPTION_MAX_CHARS - 1).trimEnd()}…`;
65
+ }
66
+ /**
67
+ * Format skills for inclusion in a system prompt.
68
+ * Uses XML format per Agent Skills standard.
69
+ * See: https://agentskills.io/integrate-skills
70
+ *
71
+ * Skills with disableModelInvocation=true are excluded from the prompt
72
+ * (they can only be invoked explicitly via /skill:name commands).
73
+ */
74
+ // Skill roots share long absolute prefixes across every skill under them; rendering
75
+ // that prefix per skill bills it once per skill. The roots table pays it once per
76
+ // distinct root and each location becomes a short alias/relative path. A one-line
77
+ // rule tells the model to expand aliases by joining them back to the root.
78
+ // `fileReadTool` lets a bash-only tool set still load skills: the loading rule names
79
+ // the tool the session actually has.
80
+ export function formatSkillsForPrompt(skills, fileReadTool = "read") {
81
+ return buildSkillCatalog(skills, fileReadTool).prompt;
82
+ }
83
+ function renderSkills(visibleSkills, options) {
84
+ if (visibleSkills.length === 0 && options.omitted === 0) {
85
+ return "";
86
+ }
87
+ const rootByPath = new Map();
88
+ for (const skill of visibleSkills) {
89
+ const dir = skill.filePath.split("/").slice(0, -2).join("/"); // parent of <name>/SKILL.md
90
+ if (!rootByPath.has(dir))
91
+ rootByPath.set(dir, `r${rootByPath.size}`);
92
+ }
93
+ const lines = [
94
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
95
+ options.fileReadTool === "read"
96
+ ? "Use the read tool to load a skill's file when its description matches the task and its instructions would change the work; keyword overlap or mere availability is not a reason."
97
+ : "Use bash to load a skill's file when its description matches the task and its instructions would change the work; keyword overlap or mere availability is not a reason.",
98
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
99
+ "",
100
+ "<skill_roots>",
101
+ ];
102
+ for (const [dir, alias] of rootByPath) {
103
+ lines.push(` <${alias}>${escapeXml(dir)}</${alias}>`);
104
+ }
105
+ lines.push("</skill_roots>", "A location's `rN/` prefix expands to the matching root above.", "", "<available_skills>");
106
+ for (const skill of visibleSkills) {
107
+ const dir = skill.filePath.split("/").slice(0, -2).join("/");
108
+ const alias = rootByPath.get(dir);
109
+ const relative = skill.filePath.startsWith(`${dir}/`) ? skill.filePath.slice(dir.length + 1) : skill.filePath;
110
+ lines.push(" <skill>");
111
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
112
+ if (!options.namesOnly) {
113
+ lines.push(` <description>${escapeXml(options.compact ? oneLineDescription(skill.description) : skill.description)}</description>`);
114
+ }
115
+ lines.push(` <location>${escapeXml(`${alias}/${relative}`)}</location>`);
116
+ lines.push(" </skill>");
117
+ }
118
+ if (options.omitted > 0)
119
+ lines.push(` <omitted_skills>${options.omitted}</omitted_skills>`);
120
+ lines.push("</available_skills>");
121
+ return lines.join("\n");
122
+ }
123
+ function escapeXml(str) {
124
+ return str
125
+ .replace(/&/g, "&amp;")
126
+ .replace(/</g, "&lt;")
127
+ .replace(/>/g, "&gt;")
128
+ .replace(/"/g, "&quot;")
129
+ .replace(/'/g, "&apos;");
130
+ }
131
+ //# sourceMappingURL=skills-prompt.js.map
@@ -1,5 +1,6 @@
1
1
  import type { ResourceDiagnostic } from "./diagnostics.ts";
2
2
  import { type SourceInfo } from "./source-info.ts";
3
+ export { formatSkillsForPrompt } from "./skills-prompt.ts";
3
4
  export interface SkillFrontmatter {
4
5
  name?: string;
5
6
  description?: string;
@@ -39,15 +40,6 @@ export interface LoadSkillsFromDirOptions {
39
40
  * - recurse into subdirectories to find SKILL.md
40
41
  */
41
42
  export declare function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult;
42
- /**
43
- * Format skills for inclusion in a system prompt.
44
- * Uses XML format per Agent Skills standard.
45
- * See: https://agentskills.io/integrate-skills
46
- *
47
- * Skills with disableModelInvocation=true are excluded from the prompt
48
- * (they can only be invoked explicitly via /skill:name commands).
49
- */
50
- export declare function formatSkillsForPrompt(skills: Skill[], fileReadTool?: "read" | "bash"): string;
51
43
  export interface LoadSkillsOptions {
52
44
  /** Working directory for project-local skills. */
53
45
  cwd: string;
@@ -6,6 +6,7 @@ import { parseFrontmatter } from "../utils/frontmatter.js";
6
6
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
7
7
  import { readSkillMarkdownSource, shouldSkipSkillWalkDirectoryName } from "./skill-discovery.js";
8
8
  import { createSyntheticSourceInfo } from "./source-info.js";
9
+ export { formatSkillsForPrompt } from "./skills-prompt.js";
9
10
  /** Max name length per spec */
10
11
  const MAX_NAME_LENGTH = 64;
11
12
  /** Max description length per spec */
@@ -266,65 +267,6 @@ function loadSkillFromFile(filePath, source) {
266
267
  diagnostics,
267
268
  };
268
269
  }
269
- /**
270
- * Format skills for inclusion in a system prompt.
271
- * Uses XML format per Agent Skills standard.
272
- * See: https://agentskills.io/integrate-skills
273
- *
274
- * Skills with disableModelInvocation=true are excluded from the prompt
275
- * (they can only be invoked explicitly via /skill:name commands).
276
- */
277
- // Skill roots share long absolute prefixes across every skill under them; rendering
278
- // that prefix per skill bills it once per skill. The roots table pays it once per
279
- // distinct root and each location becomes a short alias/relative path. A one-line
280
- // rule tells the model to expand aliases by joining them back to the root.
281
- // `fileReadTool` lets a bash-only tool set still load skills: the loading rule names
282
- // the tool the session actually has.
283
- export function formatSkillsForPrompt(skills, fileReadTool = "read") {
284
- const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
285
- if (visibleSkills.length === 0) {
286
- return "";
287
- }
288
- const rootByPath = new Map();
289
- for (const skill of visibleSkills) {
290
- const dir = skill.filePath.split("/").slice(0, -2).join("/"); // parent of <name>/SKILL.md
291
- if (!rootByPath.has(dir))
292
- rootByPath.set(dir, `r${rootByPath.size}`);
293
- }
294
- const lines = [
295
- "\n\nThe following skills provide specialized instructions for specific tasks.",
296
- fileReadTool === "read"
297
- ? "Use the read tool to load a skill's file when its description matches the task and its instructions would change the work; keyword overlap or mere availability is not a reason."
298
- : "Use bash to load a skill's file when its description matches the task and its instructions would change the work; keyword overlap or mere availability is not a reason.",
299
- "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
300
- "",
301
- "<skill_roots>",
302
- ];
303
- for (const [dir, alias] of rootByPath) {
304
- lines.push(` <${alias}>${escapeXml(dir)}</${alias}>`);
305
- }
306
- lines.push("</skill_roots>", "A location's `rN/` prefix expands to the matching root above.", "", "<available_skills>");
307
- for (const skill of visibleSkills) {
308
- const dir = skill.filePath.split("/").slice(0, -2).join("/");
309
- const alias = rootByPath.get(dir);
310
- const relative = skill.filePath.startsWith(`${dir}/`) ? skill.filePath.slice(dir.length + 1) : skill.filePath;
311
- lines.push(" <skill>");
312
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
313
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
314
- lines.push(` <location>${escapeXml(`${alias}/${relative}`)}</location>`);
315
- lines.push(" </skill>");
316
- }
317
- lines.push("</available_skills>");
318
- return lines.join("\n");
319
- }
320
- function escapeXml(str) {
321
- return str
322
- .replace(/&/g, "&amp;")
323
- .replace(/</g, "&lt;")
324
- .replace(/>/g, "&gt;")
325
- .replace(/"/g, "&quot;")
326
- .replace(/'/g, "&apos;");
327
- }
328
270
  /**
329
271
  * Load skills from all configured locations.
330
272
  * Returns skills and any validation diagnostics.
@@ -17,13 +17,22 @@ export declare function getProjectTrustParentPath(cwd: string): string | undefin
17
17
  export declare function getProjectTrustOptions(cwd: string, options?: {
18
18
  includeSessionOnly?: boolean;
19
19
  }): ProjectTrustOption[];
20
+ /**
21
+ * The project `.agents/skills` directories for `cwd`, nearest first: one per folder from cwd up to
22
+ * the git root (or the filesystem root), stopping below the user's home. Home and every folder
23
+ * above it are never a project; `~/.agents/skills` is the user's own global skills. The ONE walk
24
+ * both the trust check and the loader use, over the same spelling and the same git root (#3420
25
+ * review): paths stay as spelled (resources are compared as strings), and each folder's real path
26
+ * decides whether it is home or above, so a symlink can't carry the walk past home either way.
27
+ */
28
+ export declare function projectAgentsSkillDirs(cwd: string): string[];
20
29
  /**
21
30
  * Returns true when cwd has project-local resources that must be gated by
22
31
  * project trust: trust-requiring entries under the project config dir or the
23
- * legacy cwd/.pi dir (whose resources are also discovered), or .agents/skills in
24
- * cwd or one of its ancestors. Returns false when no such project resources
25
- * exist. The user/global ~/.agents/skills directory is always treated as a
26
- * trusted user resource and is ignored here, even when cwd is $HOME.
32
+ * legacy cwd/.pi dir (whose resources are also discovered), or a project
33
+ * `.agents/skills` (`projectAgentsSkillDirs`, the same walk the loader uses).
34
+ * The user's home and every folder above it are never a project: their config
35
+ * dirs and .agents/skills are the user's own global resources (#2465).
27
36
  */
28
37
  export declare function hasTrustRequiringProjectResources(cwd: string): boolean;
29
38
  export declare class ProjectTrustStore {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, sep } from "node:path";
4
4
  import lockfile from "proper-lockfile";
5
5
  import { CONFIG_DIR_NAME } from "../config.js";
6
6
  import { canonicalizePath, canonicalizePathStrict, resolvePath } from "../utils/paths.js";
@@ -201,35 +201,66 @@ function withTrustFileLock(path, fn) {
201
201
  release();
202
202
  }
203
203
  }
204
+ /** True when `dir` is the user's home or a folder above it: never a project (#2465). */
205
+ function isHomeOrAboveDir(dir, homeDir) {
206
+ return dir === homeDir || homeDir.startsWith(dir.endsWith(sep) ? dir : `${dir}${sep}`);
207
+ }
208
+ /** The nearest folder at or above `dir` that holds `.git`, as spelled; undefined outside a repository. */
209
+ function findGitRepoRoot(dir) {
210
+ let currentDir = dir;
211
+ while (!existsSync(join(currentDir, ".git"))) {
212
+ const parentDir = dirname(currentDir);
213
+ if (parentDir === currentDir)
214
+ return undefined;
215
+ currentDir = parentDir;
216
+ }
217
+ return currentDir;
218
+ }
219
+ /**
220
+ * The project `.agents/skills` directories for `cwd`, nearest first: one per folder from cwd up to
221
+ * the git root (or the filesystem root), stopping below the user's home. Home and every folder
222
+ * above it are never a project; `~/.agents/skills` is the user's own global skills. The ONE walk
223
+ * both the trust check and the loader use, over the same spelling and the same git root (#3420
224
+ * review): paths stay as spelled (resources are compared as strings), and each folder's real path
225
+ * decides whether it is home or above, so a symlink can't carry the walk past home either way.
226
+ */
227
+ export function projectAgentsSkillDirs(cwd) {
228
+ const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
229
+ const startDir = resolvePath(cwd);
230
+ const gitRepoRoot = findGitRepoRoot(startDir);
231
+ const dirs = [];
232
+ let currentDir = startDir;
233
+ while (!isHomeOrAboveDir(canonicalizePath(currentDir), homeDir)) {
234
+ dirs.push(join(currentDir, ".agents", "skills"));
235
+ if (currentDir === gitRepoRoot)
236
+ break;
237
+ const parentDir = dirname(currentDir);
238
+ if (parentDir === currentDir)
239
+ break;
240
+ currentDir = parentDir;
241
+ }
242
+ return dirs;
243
+ }
204
244
  /**
205
245
  * Returns true when cwd has project-local resources that must be gated by
206
246
  * project trust: trust-requiring entries under the project config dir or the
207
- * legacy cwd/.pi dir (whose resources are also discovered), or .agents/skills in
208
- * cwd or one of its ancestors. Returns false when no such project resources
209
- * exist. The user/global ~/.agents/skills directory is always treated as a
210
- * trusted user resource and is ignored here, even when cwd is $HOME.
247
+ * legacy cwd/.pi dir (whose resources are also discovered), or a project
248
+ * `.agents/skills` (`projectAgentsSkillDirs`, the same walk the loader uses).
249
+ * The user's home and every folder above it are never a project: their config
250
+ * dirs and .agents/skills are the user's own global resources (#2465).
211
251
  */
212
252
  export function hasTrustRequiringProjectResources(cwd) {
213
253
  const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
214
- const userAgentsSkillsDir = join(homeDir, ".agents", "skills");
215
- let currentDir = canonicalizePath(resolvePath(cwd));
216
- const configDirs = [join(currentDir, CONFIG_DIR_NAME), join(currentDir, LEGACY_PROJECT_CONFIG_DIR_NAME)];
217
- if (configDirs.some((configDir) => TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry))))) {
218
- return true;
219
- }
220
- if (projectCodemodeNamesExecutable(join(currentDir, CONFIG_DIR_NAME)))
221
- return true;
222
- while (true) {
223
- const agentsSkillsDir = join(currentDir, ".agents", "skills");
224
- if (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {
254
+ const currentDir = canonicalizePath(resolvePath(cwd));
255
+ if (!isHomeOrAboveDir(currentDir, homeDir)) {
256
+ const configDirs = [join(currentDir, CONFIG_DIR_NAME), join(currentDir, LEGACY_PROJECT_CONFIG_DIR_NAME)];
257
+ if (configDirs.some((configDir) => TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry))))) {
225
258
  return true;
226
259
  }
227
- const parentDir = dirname(currentDir);
228
- if (parentDir === currentDir) {
229
- return false;
230
- }
231
- currentDir = parentDir;
260
+ if (projectCodemodeNamesExecutable(join(currentDir, CONFIG_DIR_NAME)))
261
+ return true;
232
262
  }
263
+ return projectAgentsSkillDirs(cwd).some((dir) => existsSync(dir));
233
264
  }
234
265
  export class ProjectTrustStore {
235
266
  constructor(agentDir) {
@@ -0,0 +1,92 @@
1
+ export declare const TURN_DEADLINE_ENTRY_TYPE = "deadline-stop";
2
+ export interface TurnDeadlineSettings {
3
+ readonly inactivityMs: number;
4
+ readonly maxRuntimeMs: number;
5
+ }
6
+ /** Milliseconds the abort is given to terminalize before the lease forces settlement. */
7
+ export declare const TURN_DEADLINE_GRACE_MS = 10000;
8
+ export declare const DEFAULT_TURN_INACTIVITY_MS = 3600000;
9
+ export declare const MIN_TURN_INACTIVITY_MS = 60000;
10
+ export declare const DEFAULT_TURN_MAX_RUNTIME_MS = 21600000;
11
+ export type TurnDeadlineKind = "inactivity" | "max_runtime";
12
+ export interface TurnDeadline {
13
+ readonly kind: TurnDeadlineKind;
14
+ readonly limitMs: number;
15
+ readonly elapsedMs: number;
16
+ }
17
+ /**
18
+ * The recorded form of a deadline stop, durable on the session branch so a config reload
19
+ * or session reopen keeps suppressing goal continuations until admitted user input.
20
+ */
21
+ export interface DeadlineStopData {
22
+ readonly kind: TurnDeadlineKind;
23
+ readonly limitMs: number;
24
+ readonly elapsedMs: number;
25
+ readonly at: number;
26
+ }
27
+ interface DeadlineStopEntry {
28
+ readonly type: string;
29
+ readonly customType?: string;
30
+ readonly data?: unknown;
31
+ readonly message?: {
32
+ readonly role?: string;
33
+ };
34
+ }
35
+ /**
36
+ * The latest deadline stop on the branch, or undefined when none is pending. A
37
+ * deadline-stopped session suppresses every automatic continuation until admitted user
38
+ * input starts a new turn; the entry is read fresh so the hold survives reload and reopen.
39
+ * Only a user message releases the hold: the aborted assistant message of the stopped
40
+ * turn is persisted after the stop entry and must not clear it.
41
+ */
42
+ export declare function deadlineStopPendingSinceLastTurn(entries: readonly DeadlineStopEntry[]): DeadlineStopData | undefined;
43
+ export declare function resolveTurnDeadlineSettings(configured: {
44
+ readonly turn_inactivity_ms?: unknown;
45
+ readonly turn_max_runtime_ms?: unknown;
46
+ }): TurnDeadlineSettings;
47
+ /**
48
+ * The narrow AgentSession surface the tracker needs, declared here so the tracker lives
49
+ * with the rest of the deadline contract instead of inside agent-session.ts.
50
+ */
51
+ export type TurnDeadlineWarning = "turn_deadline_record_failed" | "turn_deadline_abort_failed" | "turn_deadline_unsettled" | "turn_deadline_failed";
52
+ export interface DeadlineSessionOwner {
53
+ _recordDeadlineStop(deadline: TurnDeadline): void;
54
+ /** End the turn through the normal system abort (tools see the abort signal); resolves once the session is idle. */
55
+ abortTurnForDeadline(): Promise<void>;
56
+ /** True while a provider retry is in flight; the engine is actively recovering the turn. */
57
+ readonly isRetryInFlight: boolean;
58
+ readonly isIdle: boolean;
59
+ /** A deadline stop that could not complete (the entry write failed, or the turn outlived the grace). */
60
+ _warnDeadline(event: TurnDeadlineWarning, detail: string): void;
61
+ }
62
+ /**
63
+ * Owns the inactivity lease and the hard max-runtime cap for one turn. Armed at prompt
64
+ * accept, renewed only by tool execution progress and admitted user input, never by text
65
+ * deltas. On expiry it records the durable stop entry and ends the run through the normal
66
+ * system-abort path. This is the single implementation of senpi#3227 for every surface.
67
+ */
68
+ export declare class AgentDeadlineTracker {
69
+ private inactivityTimer;
70
+ private maxRuntimeTimer;
71
+ private deadlineValue;
72
+ private startedAtMs;
73
+ private readonly session;
74
+ private readonly settings;
75
+ private readonly now;
76
+ get current(): TurnDeadline | undefined;
77
+ constructor(session: DeadlineSessionOwner, settings: () => TurnDeadlineSettings, now?: () => number);
78
+ arm(): void;
79
+ renew(): void;
80
+ disarm(): void;
81
+ /**
82
+ * Admitted user input releases the deadline-stopped hold. Input steered into a live run counts as
83
+ * activity (the inactivity lease restarts) but never resets the max-runtime clock of that run.
84
+ */
85
+ clearHold(): void;
86
+ private clearTimers;
87
+ private fire;
88
+ private expire;
89
+ }
90
+ export declare function buildDeadlineStopNotice(deadline: TurnDeadline): string;
91
+ export {};
92
+ //# sourceMappingURL=turn-deadline.d.ts.map