context-brake 1.0.0 → 1.1.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 (195) hide show
  1. package/README.md +31 -41
  2. package/dist/assets/runtime/antigravity-cli-hook.mjs +68 -23
  3. package/dist/assets/runtime/claude-code-hook.mjs +133 -35
  4. package/dist/assets/runtime/claude-code-mod.mjs +165 -59
  5. package/dist/assets/runtime/claude-code-statusline.mjs +41 -9
  6. package/dist/assets/runtime/codex-cli-hook.mjs +71 -23
  7. package/dist/assets/runtime/cursor-hook.mjs +71 -23
  8. package/dist/assets/runtime/github-copilot-cli-hook.mjs +71 -23
  9. package/dist/assets/runtime/omp-extension.js +70 -23
  10. package/dist/assets/runtime/omp-restart.js +41 -9
  11. package/dist/assets/runtime/opencode-plugin.js +70 -23
  12. package/dist/assets/runtime/pi-extension.js +73 -23
  13. package/dist/assets/runtime/pi-restart.js +41 -9
  14. package/dist/src/cli/assistant/assistant-context.js +7 -0
  15. package/dist/src/cli/assistant/assistant-questions.js +5 -4
  16. package/dist/src/cli/assistant/assistant-session.js +3 -1
  17. package/dist/src/cli/assistant/invocation-facts.d.ts +5 -0
  18. package/dist/src/cli/assistant/invocation-facts.js +6 -0
  19. package/dist/src/cli/assistant/questions-misc.d.ts +1 -0
  20. package/dist/src/cli/assistant/questions-misc.js +3 -2
  21. package/dist/src/cli/assistant/questions-skill.d.ts +21 -0
  22. package/dist/src/cli/assistant/questions-skill.js +45 -0
  23. package/dist/src/cli/assistant/questions-snapshot.d.ts +3 -1
  24. package/dist/src/cli/assistant/questions-snapshot.js +21 -26
  25. package/dist/src/cli/assistant/questions-trigger.d.ts +6 -0
  26. package/dist/src/cli/assistant/questions-trigger.js +56 -0
  27. package/dist/src/cli/assistant/skill-options.d.ts +20 -0
  28. package/dist/src/cli/assistant/skill-options.js +40 -0
  29. package/dist/src/cli/assistant/snapshot-specs.d.ts +7 -1
  30. package/dist/src/cli/assistant/snapshot-specs.js +17 -7
  31. package/dist/src/cli/assistant/summary-rows.d.ts +5 -0
  32. package/dist/src/cli/assistant/summary-rows.js +47 -0
  33. package/dist/src/cli/assistant/summary.d.ts +2 -1
  34. package/dist/src/cli/assistant/summary.js +21 -30
  35. package/dist/src/cli/assistant/types.d.ts +15 -0
  36. package/dist/src/cli/commands/doctor.js +2 -1
  37. package/dist/src/cli/commands/remove.js +1 -1
  38. package/dist/src/cli/init-arguments.js +5 -4
  39. package/dist/src/cli/init-flow.js +1 -1
  40. package/dist/src/cli/init-option-rules.d.ts +5 -0
  41. package/dist/src/cli/init-option-rules.js +16 -0
  42. package/dist/src/cli/output/card.d.ts +8 -0
  43. package/dist/src/cli/output/card.js +30 -0
  44. package/dist/src/cli/output/doctor-mode-text.js +8 -2
  45. package/dist/src/cli/output/install-rows.d.ts +6 -0
  46. package/dist/src/cli/output/install-rows.js +31 -0
  47. package/dist/src/cli/output/install-text.d.ts +4 -0
  48. package/dist/src/cli/output/install-text.js +26 -0
  49. package/dist/src/cli/output/style.d.ts +22 -0
  50. package/dist/src/cli/output/style.js +27 -0
  51. package/dist/src/cli/output/text.d.ts +1 -2
  52. package/dist/src/cli/output/text.js +1 -28
  53. package/dist/src/core/contracts/adapter.d.ts +2 -1
  54. package/dist/src/core/contracts/configuration.d.ts +2 -1
  55. package/dist/src/core/contracts/configuration.js +2 -10
  56. package/dist/src/core/contracts/context-reading.d.ts +16 -0
  57. package/dist/src/core/contracts/context-reading.js +20 -0
  58. package/dist/src/core/contracts/context-window-report.d.ts +8 -1
  59. package/dist/src/core/contracts/context-window-report.js +3 -1
  60. package/dist/src/core/contracts/diagnostics.d.ts +21 -0
  61. package/dist/src/core/contracts/diagnostics.js +2 -1
  62. package/dist/src/core/contracts/manifest.d.ts +16 -0
  63. package/dist/src/core/contracts/manifest.js +6 -0
  64. package/dist/src/core/contracts/runtime.d.ts +2 -0
  65. package/dist/src/core/contracts/skill-catalog.d.ts +11 -0
  66. package/dist/src/core/contracts/skill-catalog.js +1 -0
  67. package/dist/src/core/contracts/skill-invocation.d.ts +3 -0
  68. package/dist/src/core/contracts/skill-invocation.js +1 -0
  69. package/dist/src/core/contracts/skill-name.d.ts +5 -0
  70. package/dist/src/core/contracts/skill-name.js +31 -0
  71. package/dist/src/core/services/brake-engine.js +4 -3
  72. package/dist/src/core/services/doctor-report-extras.d.ts +2 -0
  73. package/dist/src/core/services/doctor-report-extras.js +22 -5
  74. package/dist/src/core/services/doctor-service.d.ts +2 -0
  75. package/dist/src/core/services/doctor-service.js +1 -1
  76. package/dist/src/core/services/failure-policy.js +1 -1
  77. package/dist/src/core/services/injection-policy.d.ts +1 -0
  78. package/dist/src/core/services/injection-policy.js +1 -1
  79. package/dist/src/core/services/installation-adapters.d.ts +2 -1
  80. package/dist/src/core/services/installation-adapters.js +3 -1
  81. package/dist/src/core/services/installation-builder.js +3 -2
  82. package/dist/src/core/services/installation-service.js +1 -1
  83. package/dist/src/core/services/manifest-change.d.ts +2 -1
  84. package/dist/src/core/services/manifest-change.js +2 -1
  85. package/dist/src/core/services/session-reset-handler.js +1 -1
  86. package/dist/src/core/services/skill-candidates.d.ts +16 -0
  87. package/dist/src/core/services/skill-candidates.js +14 -0
  88. package/dist/src/core/services/skill-invocation.d.ts +4 -0
  89. package/dist/src/core/services/skill-invocation.js +12 -0
  90. package/dist/src/core/services/skill-ranking.d.ts +7 -0
  91. package/dist/src/core/services/skill-ranking.js +25 -0
  92. package/dist/src/core/services/snapshot-merge.d.ts +1 -0
  93. package/dist/src/core/services/snapshot-merge.js +12 -5
  94. package/dist/src/core/services/trigger-text.d.ts +5 -0
  95. package/dist/src/core/services/trigger-text.js +4 -0
  96. package/dist/src/core/services/zone-guidance.d.ts +13 -2
  97. package/dist/src/core/services/zone-guidance.js +14 -8
  98. package/dist/src/infrastructure/harnesses/antigravity-cli/runtime.js +2 -1
  99. package/dist/src/infrastructure/harnesses/antigravity-cli/skill-folders.d.ts +1 -0
  100. package/dist/src/infrastructure/harnesses/antigravity-cli/skill-folders.js +1 -0
  101. package/dist/src/infrastructure/harnesses/antigravity-cli/skill-invocation.d.ts +2 -0
  102. package/dist/src/infrastructure/harnesses/antigravity-cli/skill-invocation.js +2 -0
  103. package/dist/src/infrastructure/harnesses/claude-code/adapter.js +6 -4
  104. package/dist/src/infrastructure/harnesses/claude-code/auto-restart-diagnostics.d.ts +1 -0
  105. package/dist/src/infrastructure/harnesses/claude-code/auto-restart-diagnostics.js +2 -2
  106. package/dist/src/infrastructure/harnesses/claude-code/auto-restart-planner.d.ts +5 -1
  107. package/dist/src/infrastructure/harnesses/claude-code/auto-restart-planner.js +6 -6
  108. package/dist/src/infrastructure/harnesses/claude-code/capabilities.js +1 -1
  109. package/dist/src/infrastructure/harnesses/claude-code/context-readings-scan.d.ts +2 -0
  110. package/dist/src/infrastructure/harnesses/claude-code/context-readings-scan.js +21 -0
  111. package/dist/src/infrastructure/harnesses/claude-code/context-source.d.ts +10 -0
  112. package/dist/src/infrastructure/harnesses/claude-code/context-source.js +16 -0
  113. package/dist/src/infrastructure/harnesses/claude-code/mod/context-reading.d.ts +2 -0
  114. package/dist/src/infrastructure/harnesses/claude-code/mod/context-reading.js +67 -0
  115. package/dist/src/infrastructure/harnesses/claude-code/mod/host.d.ts +9 -0
  116. package/dist/src/infrastructure/harnesses/claude-code/mod/mod-info.d.ts +1 -1
  117. package/dist/src/infrastructure/harnesses/claude-code/mod/mod-info.js +1 -1
  118. package/dist/src/infrastructure/harnesses/claude-code/mod/mod-log.d.ts +1 -0
  119. package/dist/src/infrastructure/harnesses/claude-code/mod/mod-log.js +3 -0
  120. package/dist/src/infrastructure/harnesses/claude-code/mod/register.js +6 -1
  121. package/dist/src/infrastructure/harnesses/claude-code/mod-reading.d.ts +6 -0
  122. package/dist/src/infrastructure/harnesses/claude-code/mod-reading.js +37 -0
  123. package/dist/src/infrastructure/harnesses/claude-code/planner.d.ts +2 -1
  124. package/dist/src/infrastructure/harnesses/claude-code/planner.js +4 -4
  125. package/dist/src/infrastructure/harnesses/claude-code/project-root.d.ts +2 -0
  126. package/dist/src/infrastructure/harnesses/claude-code/project-root.js +5 -0
  127. package/dist/src/infrastructure/harnesses/claude-code/runtime.js +12 -7
  128. package/dist/src/infrastructure/harnesses/claude-code/skill-folders.d.ts +1 -0
  129. package/dist/src/infrastructure/harnesses/claude-code/skill-folders.js +1 -0
  130. package/dist/src/infrastructure/harnesses/claude-code/skill-invocation.d.ts +2 -0
  131. package/dist/src/infrastructure/harnesses/claude-code/skill-invocation.js +2 -0
  132. package/dist/src/infrastructure/harnesses/claude-code/statusline-context-window.js +15 -2
  133. package/dist/src/infrastructure/harnesses/claude-code/statusline-diagnostics.js +3 -3
  134. package/dist/src/infrastructure/harnesses/claude-code/statusline-migration.d.ts +3 -0
  135. package/dist/src/infrastructure/harnesses/claude-code/statusline-migration.js +23 -0
  136. package/dist/src/infrastructure/harnesses/claude-code/statusline-planner.d.ts +2 -1
  137. package/dist/src/infrastructure/harnesses/claude-code/statusline-planner.js +7 -4
  138. package/dist/src/infrastructure/harnesses/claude-code/user-settings.d.ts +2 -0
  139. package/dist/src/infrastructure/harnesses/claude-code/user-settings.js +11 -0
  140. package/dist/src/infrastructure/harnesses/codex-cli/adapter.js +5 -4
  141. package/dist/src/infrastructure/harnesses/codex-cli/runtime.js +2 -1
  142. package/dist/src/infrastructure/harnesses/codex-cli/skill-folders.d.ts +1 -0
  143. package/dist/src/infrastructure/harnesses/codex-cli/skill-folders.js +1 -0
  144. package/dist/src/infrastructure/harnesses/codex-cli/skill-invocation.d.ts +2 -0
  145. package/dist/src/infrastructure/harnesses/codex-cli/skill-invocation.js +2 -0
  146. package/dist/src/infrastructure/harnesses/common/config-origin.d.ts +10 -0
  147. package/dist/src/infrastructure/harnesses/common/config-origin.js +52 -0
  148. package/dist/src/infrastructure/harnesses/cursor/adapter.js +5 -4
  149. package/dist/src/infrastructure/harnesses/cursor/runtime.js +2 -1
  150. package/dist/src/infrastructure/harnesses/cursor/skill-folders.d.ts +1 -0
  151. package/dist/src/infrastructure/harnesses/cursor/skill-folders.js +1 -0
  152. package/dist/src/infrastructure/harnesses/cursor/skill-invocation.d.ts +2 -0
  153. package/dist/src/infrastructure/harnesses/cursor/skill-invocation.js +2 -0
  154. package/dist/src/infrastructure/harnesses/github-copilot-cli/runtime.js +2 -1
  155. package/dist/src/infrastructure/harnesses/github-copilot-cli/skill-folders.d.ts +1 -0
  156. package/dist/src/infrastructure/harnesses/github-copilot-cli/skill-folders.js +1 -0
  157. package/dist/src/infrastructure/harnesses/github-copilot-cli/skill-invocation.d.ts +2 -0
  158. package/dist/src/infrastructure/harnesses/github-copilot-cli/skill-invocation.js +2 -0
  159. package/dist/src/infrastructure/harnesses/oh-my-pi/runtime.js +2 -1
  160. package/dist/src/infrastructure/harnesses/oh-my-pi/skill-folders.d.ts +1 -0
  161. package/dist/src/infrastructure/harnesses/oh-my-pi/skill-folders.js +1 -0
  162. package/dist/src/infrastructure/harnesses/oh-my-pi/skill-invocation.d.ts +2 -0
  163. package/dist/src/infrastructure/harnesses/oh-my-pi/skill-invocation.js +2 -0
  164. package/dist/src/infrastructure/harnesses/opencode/runtime.js +2 -1
  165. package/dist/src/infrastructure/harnesses/opencode/skill-folders.d.ts +1 -0
  166. package/dist/src/infrastructure/harnesses/opencode/skill-folders.js +1 -0
  167. package/dist/src/infrastructure/harnesses/opencode/skill-invocation.d.ts +2 -0
  168. package/dist/src/infrastructure/harnesses/opencode/skill-invocation.js +2 -0
  169. package/dist/src/infrastructure/harnesses/pi/runtime.js +2 -1
  170. package/dist/src/infrastructure/harnesses/pi/skill-folders.d.ts +1 -0
  171. package/dist/src/infrastructure/harnesses/pi/skill-folders.js +1 -0
  172. package/dist/src/infrastructure/harnesses/pi/skill-invocation.d.ts +2 -0
  173. package/dist/src/infrastructure/harnesses/pi/skill-invocation.js +2 -0
  174. package/dist/src/infrastructure/harnesses/skill-folders.d.ts +3 -0
  175. package/dist/src/infrastructure/harnesses/skill-folders.js +21 -0
  176. package/dist/src/infrastructure/harnesses/skill-invocations.d.ts +4 -0
  177. package/dist/src/infrastructure/harnesses/skill-invocations.js +21 -0
  178. package/dist/src/infrastructure/runtime/runtime-paths.js +2 -1
  179. package/dist/src/infrastructure/skills/frontmatter.d.ts +5 -0
  180. package/dist/src/infrastructure/skills/frontmatter.js +25 -0
  181. package/dist/src/infrastructure/skills/skill-catalog-reader.d.ts +4 -0
  182. package/dist/src/infrastructure/skills/skill-catalog-reader.js +78 -0
  183. package/dist/src/infrastructure/skills/skill-catalog.d.ts +2 -0
  184. package/dist/src/infrastructure/skills/skill-catalog.js +5 -0
  185. package/dist/src/infrastructure/storage/change-applier.d.ts +1 -0
  186. package/dist/src/infrastructure/storage/change-applier.js +2 -1
  187. package/dist/src/infrastructure/storage/json-document-editor.js +2 -1
  188. package/dist/src/infrastructure/storage/json-node-removal.d.ts +2 -0
  189. package/dist/src/infrastructure/storage/json-node-removal.js +70 -0
  190. package/dist/src/infrastructure/storage/json-span-utils.d.ts +1 -1
  191. package/dist/src/infrastructure/storage/json-span-utils.js +1 -48
  192. package/docs/telemetry-block.md +3 -1
  193. package/package.json +1 -1
  194. package/schemas/context-brake.config.schema.json +9 -4
  195. package/schemas/doctor-report.schema.json +51 -0
@@ -440,8 +440,23 @@ export declare const doctorReportSchema: z.ZodObject<{
440
440
  YELLOW: "YELLOW";
441
441
  RED: "RED";
442
442
  }>;
443
+ triggerPercentage: z.ZodOptional<z.ZodNumber>;
443
444
  command: z.ZodNullable<z.ZodString>;
444
445
  resumeCommand: z.ZodNullable<z.ZodString>;
446
+ invocations: z.ZodOptional<z.ZodArray<z.ZodObject<{
447
+ harness: z.ZodEnum<{
448
+ cursor: "cursor";
449
+ "claude-code": "claude-code";
450
+ "codex-cli": "codex-cli";
451
+ "github-copilot-cli": "github-copilot-cli";
452
+ opencode: "opencode";
453
+ pi: "pi";
454
+ "oh-my-pi": "oh-my-pi";
455
+ "antigravity-cli": "antigravity-cli";
456
+ }>;
457
+ command: z.ZodString;
458
+ resumeCommand: z.ZodNullable<z.ZodString>;
459
+ }, z.core.$strict>>>;
445
460
  }, z.core.$strict>>;
446
461
  contextWindow: z.ZodOptional<z.ZodObject<{
447
462
  bridge: z.ZodEnum<{
@@ -449,8 +464,14 @@ export declare const doctorReportSchema: z.ZodObject<{
449
464
  installed: "installed";
450
465
  absent: "absent";
451
466
  }>;
467
+ mod: z.ZodEnum<{
468
+ installed: "installed";
469
+ absent: "absent";
470
+ reporting: "reporting";
471
+ }>;
452
472
  source: z.ZodEnum<{
453
473
  contextWindowCeiling: "contextWindowCeiling";
474
+ mod: "mod";
454
475
  statusline: "statusline";
455
476
  }>;
456
477
  lastWindowTokens: z.ZodNullable<z.ZodNumber>;
@@ -20,7 +20,8 @@ const harnessPlan = z.object({ harness: z.enum(HARNESS_IDS), outcome: z.enum(['p
20
20
  const outcome = z.object({ path: z.string(), status: z.enum(['planned', 'applied', 'unchanged', 'skipped', 'failed']), detail: z.string().nullable() }).strict();
21
21
  const plan = z.object({ schemaVersion: z.literal(1), projectRoot: z.string(), changes: z.array(fileChange), conflicts: z.array(conflict), harnesses: z.array(harnessPlan), requiresConfirmation: z.boolean() }).strict();
22
22
  export const installReportSchema = z.object({ schemaVersion: z.literal(1), command: z.enum(['init', 'remove']), mode: z.enum(['dry_run', 'applied']), status: z.enum(['success', 'warnings', 'errors']), exitCode: z.union([z.literal(0), z.literal(1), z.literal(2)]), detections: z.array(detection), plan, outcomes: z.array(outcome), findings: z.array(finding) }).strict();
23
- const snapshotReport = z.object({ triggerZone: z.enum(SNAPSHOT_TRIGGER_ZONES), command: z.string().nullable(), resumeCommand: z.string().nullable() }).strict();
23
+ const invocationReport = z.object({ harness: z.enum(HARNESS_IDS), command: z.string(), resumeCommand: z.string().nullable() }).strict();
24
+ const snapshotReport = z.object({ triggerZone: z.enum(SNAPSHOT_TRIGGER_ZONES), triggerPercentage: z.optional(z.number().int().min(1).max(100)), command: z.string().nullable(), resumeCommand: z.string().nullable(), invocations: z.optional(z.array(invocationReport)) }).strict();
24
25
  const sessionUsage = z.object({ percentage: z.number().int().nonnegative(), usedTokens: z.number().int().nonnegative(), windowTokens: z.number().int().positive(), zone: z.enum(ZONES), source: z.enum(USAGE_SOURCES), at: z.string() }).strict();
25
26
  const activeSession = z.object({ harness: z.enum(HARNESS_IDS), sessionId: z.string().nullable(), lastActivityAt: z.string(), usage: sessionUsage.nullable() }).strict();
26
27
  export const doctorReportSchema = z.object({ schemaVersion: z.literal(1), command: z.literal('doctor'), status: z.enum(['healthy', 'warnings', 'errors']), exitCode: z.union([z.literal(0), z.literal(1), z.literal(2)]), detections: z.array(detection), integrations: z.array(integration), findings: z.array(finding), snapshot: snapshotReport.optional(), contextWindow: contextWindowReportSchema.optional(), activeSessions: z.array(activeSession).optional(), debugMode: z.optional(z.literal(true)) }).strict();
@@ -13,11 +13,17 @@ export type ManagedEntry = {
13
13
  path: string;
14
14
  identity: string;
15
15
  };
16
+ export type ConfigOrigin = {
17
+ path: string;
18
+ created: boolean;
19
+ addedKeys: readonly string[];
20
+ };
16
21
  export type InstallationManifest = {
17
22
  schemaVersion: 1;
18
23
  packageVersion: string;
19
24
  assets: readonly ManagedAsset[];
20
25
  entries: readonly ManagedEntry[];
26
+ configOrigins?: readonly ConfigOrigin[] | undefined;
21
27
  };
22
28
  export interface ManifestStore {
23
29
  load(): Promise<InstallationManifest | null>;
@@ -44,6 +50,11 @@ export declare const managedEntrySchema: z.ZodObject<{
44
50
  path: z.ZodString;
45
51
  identity: z.ZodString;
46
52
  }, z.core.$strict>;
53
+ export declare const configOriginSchema: z.ZodObject<{
54
+ path: z.ZodString;
55
+ created: z.ZodBoolean;
56
+ addedKeys: z.ZodArray<z.ZodString>;
57
+ }, z.core.$strict>;
47
58
  export declare const installationManifestSchema: z.ZodObject<{
48
59
  schemaVersion: z.ZodLiteral<1>;
49
60
  packageVersion: z.ZodString;
@@ -66,4 +77,9 @@ export declare const installationManifestSchema: z.ZodObject<{
66
77
  path: z.ZodString;
67
78
  identity: z.ZodString;
68
79
  }, z.core.$strict>>;
80
+ configOrigins: z.ZodOptional<z.ZodArray<z.ZodObject<{
81
+ path: z.ZodString;
82
+ created: z.ZodBoolean;
83
+ addedKeys: z.ZodArray<z.ZodString>;
84
+ }, z.core.$strict>>>;
69
85
  }, z.core.$strict>;
@@ -12,9 +12,15 @@ export const managedEntrySchema = z.object({
12
12
  path: z.string(),
13
13
  identity: z.string().min(1),
14
14
  }).strict();
15
+ export const configOriginSchema = z.object({
16
+ path: z.string(),
17
+ created: z.boolean(),
18
+ addedKeys: z.array(z.string().min(1)),
19
+ }).strict();
15
20
  export const installationManifestSchema = z.object({
16
21
  schemaVersion: z.literal(MANIFEST_SCHEMA_VERSION),
17
22
  packageVersion: z.string().min(1),
18
23
  assets: z.array(managedAssetSchema),
19
24
  entries: z.array(managedEntrySchema),
25
+ configOrigins: z.array(configOriginSchema).optional(),
20
26
  }).strict();
@@ -1,4 +1,5 @@
1
1
  import type { CapabilityDefinition, HarnessId } from './harness.js';
2
+ import type { SkillInvocationFormat } from './skill-invocation.js';
2
3
  export type SessionKey = {
3
4
  readonly harness: HarnessId;
4
5
  readonly sessionId: string;
@@ -47,4 +48,5 @@ export type RuntimeDescriptor = {
47
48
  readonly capabilities: readonly CapabilityDefinition[];
48
49
  readonly estimation: EstimationConstants;
49
50
  readonly newSessionCommand: string | null;
51
+ readonly skillInvocation: SkillInvocationFormat;
50
52
  };
@@ -0,0 +1,11 @@
1
+ import type { HarnessId } from './harness.js';
2
+ export type CatalogSkill = {
3
+ readonly name: string;
4
+ readonly description: string;
5
+ readonly folder: string;
6
+ };
7
+ export type SkillCatalog = {
8
+ readonly skills: readonly CatalogSkill[];
9
+ readonly warnings: readonly string[];
10
+ };
11
+ export type SkillCatalogReader = (harness: HarnessId, projectRoot: string) => Promise<SkillCatalog>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { HarnessId } from './harness.js';
2
+ export type SkillInvocationFormat = (skill: string) => string;
3
+ export type SkillInvocationResolver = (harness: HarnessId) => SkillInvocationFormat;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { z } from 'zod/mini';
2
+ export declare const MAX_SKILL_NAME_LENGTH = 64;
3
+ export declare function isSkillName(value: string): boolean;
4
+ export declare function skillNameProblem(value: string): string | null;
5
+ export declare const skillNameSchema: z.ZodMiniString<string>;
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod/mini';
2
+ export const MAX_SKILL_NAME_LENGTH = 64;
3
+ const NAME_SEGMENT = '[a-z0-9]+(?:-[a-z0-9]+)*';
4
+ const NAMESPACE_SEPARATOR = ':';
5
+ const INVOCATION_CHARACTERS = ['/', '$', '@'];
6
+ const MAX_QUALIFIED_LENGTH = MAX_SKILL_NAME_LENGTH * 2 + NAMESPACE_SEPARATOR.length;
7
+ function segmentFits(end) {
8
+ return `(?=[^${NAMESPACE_SEPARATOR}]{1,${MAX_SKILL_NAME_LENGTH}}${end})`;
9
+ }
10
+ const PREFIX = `(?:${segmentFits(NAMESPACE_SEPARATOR)}${NAME_SEGMENT}${NAMESPACE_SEPARATOR})?`;
11
+ const SKILL_NAME_PATTERN = new RegExp(`^${PREFIX}${segmentFits('$')}${NAME_SEGMENT}$`);
12
+ const SHAPE_RULE = `lowercase letters, digits, and single hyphens, up to ${MAX_SKILL_NAME_LENGTH} characters, optionally prefixed by a plugin name and a colon`;
13
+ export function isSkillName(value) {
14
+ return SKILL_NAME_PATTERN.test(value);
15
+ }
16
+ export function skillNameProblem(value) {
17
+ if (value === '')
18
+ return 'must not be empty';
19
+ if (isSkillName(value))
20
+ return null;
21
+ const bare = INVOCATION_CHARACTERS.includes(value.charAt(0)) ? value.slice(1) : null;
22
+ if (bare !== null && isSkillName(bare))
23
+ return `"${value}" is not a skill name; use the bare name ${bare}`;
24
+ return `"${value}" is not a skill name; use ${SHAPE_RULE}`;
25
+ }
26
+ export const skillNameSchema = z.string().check((ctx) => {
27
+ const message = skillNameProblem(ctx.value);
28
+ if (message !== null)
29
+ ctx.issues.push({ code: 'custom', input: ctx.value, message });
30
+ });
31
+ z.globalRegistry.add(skillNameSchema, { pattern: SKILL_NAME_PATTERN.source, maxLength: MAX_QUALIFIED_LENGTH });
@@ -7,7 +7,7 @@ import { nextTurn, summarizeLedger } from './session-counters.js';
7
7
  import { readZone } from './session-zone.js';
8
8
  import { renderTelemetryBlock } from './telemetry-block.js';
9
9
  import { redStartTurn } from './zone-classifier.js';
10
- import { zoneAction } from './zone-guidance.js';
10
+ import { isResetDue, zoneAction } from './zone-guidance.js';
11
11
  import { handleSessionReset } from './session-reset-handler.js';
12
12
  const NEUTRAL = { kind: 'neutral' };
13
13
  export function createBrakeEngine(options) { return { handle: (event, input) => handleEvent(options, event, input ?? {}) }; }
@@ -36,9 +36,10 @@ async function handlePreInvocation(options, event, input) {
36
36
  return telemetryDecision(options, summary.turns, readZone(options, { summary, turns: summary.turns, observedCharacters: 0, measured: input.measured }));
37
37
  }
38
38
  async function telemetryDecision(options, turn, view) {
39
- if (!decideInjection({ telemetry: options.config.telemetry, zone: view.zone, usagePercentage: view.percentage, debug: isDebugModeInEffect(options.config) }))
39
+ const resetDue = isResetDue({ zone: view.zone, usagePercentage: view.percentage }, options.config.snapshot);
40
+ if (!decideInjection({ telemetry: options.config.telemetry, zone: view.zone, usagePercentage: view.percentage, debug: isDebugModeInEffect(options.config), resetDue }))
40
41
  return NEUTRAL;
41
- const action = zoneAction(view.zone, options.config.snapshot, restartMode(options.config));
42
+ const action = zoneAction(view.zone, options.config.snapshot, { mode: restartMode(options.config), invoke: options.descriptor.skillInvocation, usagePercentage: view.percentage });
42
43
  return { kind: 'context', block: renderTelemetryBlock({ turn, turnCeiling: redStartTurn(options.config.telemetry.zones), usagePercentage: view.percentage, usage: view.reading, zone: view.zone, action, debug: isDebugModeInEffect(options.config) }) };
43
44
  }
44
45
  async function handleResponseEnd(options, event) {
@@ -1,11 +1,13 @@
1
1
  import type { ContextBrakeConfig } from '../contracts/configuration.js';
2
2
  import type { ContextWindowReport } from '../contracts/context-window-report.js';
3
3
  import type { DiagnosticFinding, DoctorReport } from '../contracts/diagnostics.js';
4
+ import type { SkillInvocationResolver } from '../contracts/skill-invocation.js';
4
5
  import type { BuildDoctorReportInput } from './report-service.js';
5
6
  export type DoctorExtrasInput = {
6
7
  readonly config: ContextBrakeConfig | null;
7
8
  readonly planPresent: boolean;
8
9
  readonly contextWindow: ContextWindowReport | undefined;
9
10
  readonly sessions: DoctorReport['activeSessions'];
11
+ readonly invocations?: SkillInvocationResolver | undefined;
10
12
  };
11
13
  export declare function doctorReportExtras(input: DoctorExtrasInput, findings: readonly DiagnosticFinding[]): Omit<BuildDoctorReportInput, 'detections' | 'integrations'>;
@@ -1,18 +1,23 @@
1
1
  import { isDebugModeInEffect } from './debug-mode-merge.js';
2
2
  export function doctorReportExtras(input, findings) {
3
3
  return {
4
- findings: [...findings, ...bridgeAbsentFindings(input.contextWindow)],
5
- snapshot: snapshotReport(input.config), contextWindow: input.contextWindow, activeSessions: input.sessions,
4
+ findings: [...findings, ...modFindings(input.contextWindow), ...bridgeAbsentFindings(input.contextWindow)],
5
+ snapshot: snapshotReport(input.config, input.invocations), contextWindow: input.contextWindow, activeSessions: input.sessions,
6
6
  debugMode: isDebugModeInEffect(input.config),
7
7
  };
8
8
  }
9
- function snapshotReport(config) {
9
+ function snapshotReport(config, resolve) {
10
10
  if (config === null)
11
11
  return undefined;
12
- return { triggerZone: config.snapshot.triggerZone, command: config.snapshot.command ?? null, resumeCommand: config.snapshot.resumeCommand ?? null };
12
+ const { triggerZone, triggerPercentage, command, resumeCommand } = config.snapshot;
13
+ const trigger = triggerPercentage === undefined ? {} : { triggerPercentage };
14
+ const report = { triggerZone, ...trigger, command: command ?? null, resumeCommand: resumeCommand ?? null };
15
+ if (command === undefined || resolve === undefined)
16
+ return report;
17
+ return { ...report, invocations: config.activeHarnesses.map((harness) => ({ harness, command: resolve(harness)(command), resumeCommand: resumeCommand === undefined ? null : resolve(harness)(resumeCommand) })) };
13
18
  }
14
19
  function bridgeAbsentFindings(contextWindow) {
15
- if (contextWindow?.bridge !== 'absent')
20
+ if (contextWindow?.bridge !== 'absent' || contextWindow.mod !== 'absent')
16
21
  return [];
17
22
  return [{
18
23
  code: 'STATUSLINE_BRIDGE_ABSENT', severity: 'warning', scope: 'harness', harness: 'claude-code', path: null,
@@ -21,3 +26,15 @@ function bridgeAbsentFindings(contextWindow) {
21
26
  remediation: 'Run context-brake init. After --no-statusline-bridge, run context-brake init --statusline-bridge.',
22
27
  }];
23
28
  }
29
+ function modFindings(contextWindow) {
30
+ if (contextWindow?.mod === 'reporting')
31
+ return [{ code: 'CONTEXT_MOD_READY', severity: 'ok', scope: 'harness', harness: 'claude-code', path: null, message: 'The Claude Code mod reports the context window.', impact: null, remediation: null }];
32
+ if (contextWindow?.mod !== 'installed' || contextWindow.bridge === 'installed')
33
+ return [];
34
+ return [{
35
+ code: 'CONTEXT_MOD_SILENT', severity: 'warning', scope: 'harness', harness: 'claude-code', path: null,
36
+ message: 'The ContextBrake mod is installed, but no Claude Code session has reported the context yet.',
37
+ impact: 'The context window falls back to contextWindowCeiling and usage to the transcript or an estimate.',
38
+ remediation: 'Start Claude Code in this project and trust the folder. Mods stay off under disableAllHooks, --safe-mode, --bare, an organization managed policy, and in Desktop WSL sessions. Claude Code also keeps one marketplace named context-brake-local per user, so the mod of another repository can be the one that loads; run context-brake init in that repository too, or use the status line bridge with context-brake init --statusline-bridge.',
39
+ }];
40
+ }
@@ -5,6 +5,7 @@ import type { FileSnapshot } from '../contracts/changes.js';
5
5
  import { type DetectionSources, type HarnessId } from '../contracts/harness.js';
6
6
  import type { InstallationManifest } from '../contracts/manifest.js';
7
7
  import { type RuntimeStateReading } from './runtime-error-checks.js';
8
+ import type { SkillInvocationResolver } from '../contracts/skill-invocation.js';
8
9
  export type DoctorInput = {
9
10
  projectRoot: string;
10
11
  config: ContextBrakeConfig | null;
@@ -20,5 +21,6 @@ export type DoctorInput = {
20
21
  runtimeState?: RuntimeStateReading | null;
21
22
  contextWindow?: DoctorReport['contextWindow'];
22
23
  now?: Date | undefined;
24
+ invocations?: SkillInvocationResolver;
23
25
  };
24
26
  export declare function diagnoseProject(input: DoctorInput): Promise<DoctorReport>;
@@ -78,5 +78,5 @@ export async function diagnoseProject(input) {
78
78
  if (input.runtimeState)
79
79
  allFindings.push(...runtimeErrorFindings(input.runtimeState));
80
80
  const sessions = input.now === undefined ? [] : activeSessions(input.runtimeState?.ledgers ?? [], { now: input.now, zones: effective.telemetry.zones });
81
- return buildDoctorReport({ detections, integrations, ...doctorReportExtras({ config: input.config, planPresent: false, contextWindow: targetIds.has('claude-code') ? input.contextWindow : undefined, sessions }, allFindings) });
81
+ return buildDoctorReport({ detections, integrations, ...doctorReportExtras({ config: input.config, planPresent: false, contextWindow: targetIds.has('claude-code') ? input.contextWindow : undefined, sessions, invocations: input.invocations }, allFindings) });
82
82
  }
@@ -47,7 +47,7 @@ export async function resolveFailure(input) {
47
47
  return { kind: 'neutral' };
48
48
  }
49
49
  function deadlineBootDecision(input) {
50
- const text = resumeText((input.config ?? DEFAULT_CONFIG).snapshot);
50
+ const text = resumeText((input.config ?? DEFAULT_CONFIG).snapshot, input.descriptor?.skillInvocation);
51
51
  return text === null ? { kind: 'neutral' } : { kind: 'context', block: text };
52
52
  }
53
53
  export async function recordRuntimeFailure(errors, record) {
@@ -5,5 +5,6 @@ export type InjectionInput = {
5
5
  readonly zone: Zone;
6
6
  readonly usagePercentage: number;
7
7
  readonly debug: boolean;
8
+ readonly resetDue?: boolean | undefined;
8
9
  };
9
10
  export declare function decideInjection(input: InjectionInput): boolean;
@@ -1,5 +1,5 @@
1
1
  export function decideInjection(input) {
2
- if (input.debug || input.telemetry.injectionMode === 'always')
2
+ if (input.debug || input.resetDue === true || input.telemetry.injectionMode === 'always')
3
3
  return true;
4
4
  return input.zone !== 'GREEN' || input.usagePercentage >= input.telemetry.activationThresholdPercentage;
5
5
  }
@@ -1,12 +1,13 @@
1
1
  import type { HarnessAdapter, HarnessContext } from '../contracts/adapter.js';
2
2
  import type { HarnessInstallPlan, PlannedChange, PlanConflict } from '../contracts/changes.js';
3
3
  import type { HarnessDetection } from '../contracts/harness.js';
4
- import type { ManagedAsset, ManagedEntry } from '../contracts/manifest.js';
4
+ import type { ConfigOrigin, ManagedAsset, ManagedEntry } from '../contracts/manifest.js';
5
5
  export declare function planAdapters(adapters: readonly HarnessAdapter[], active: readonly HarnessDetection[], ctx: HarnessContext): Promise<{
6
6
  changes: PlannedChange[];
7
7
  conflicts: PlanConflict[];
8
8
  entries: ManagedEntry[];
9
9
  assets: ManagedAsset[];
10
+ configOrigins: ConfigOrigin[];
10
11
  harnesses: HarnessInstallPlan[];
11
12
  findings: {
12
13
  code: string;
@@ -4,6 +4,7 @@ export async function planAdapters(adapters, active, ctx) {
4
4
  const conflicts = [];
5
5
  const entries = [];
6
6
  const assets = [];
7
+ const configOrigins = [];
7
8
  const harnesses = [];
8
9
  const findings = [];
9
10
  for (const d of active) {
@@ -15,6 +16,7 @@ export async function planAdapters(adapters, active, ctx) {
15
16
  conflicts.push(...aPlan.conflicts);
16
17
  findings.push(...(aPlan.findings ?? []));
17
18
  entries.push(...aPlan.entries);
19
+ configOrigins.push(...(aPlan.configOrigins ?? []));
18
20
  for (const c of aPlan.changes)
19
21
  if (c.owner === 'runtime_asset' && c.content)
20
22
  assets.push({ path: c.path, kind: 'runtime_asset', sha256: hashString(c.content) });
@@ -24,5 +26,5 @@ export async function planAdapters(adapters, active, ctx) {
24
26
  const profile = adapter.capabilityProfile();
25
27
  harnesses.push({ harness: d.harness, outcome, supportLevel: profile.supportLevel, limitations: [...profile.limitations] });
26
28
  }
27
- return { changes, conflicts, entries, assets, harnesses, findings };
29
+ return { changes, conflicts, entries, assets, configOrigins, harnesses, findings };
28
30
  }
@@ -6,6 +6,7 @@ import { applyDebugMode } from './debug-mode-merge.js';
6
6
  import { applyGitIgnore } from './gitignore-merge.js';
7
7
  import { normalizeTurnLimits } from './config-legacy-checks.js';
8
8
  import { applyExclusion } from './harness-exclusion.js';
9
+ import { describeTrigger } from './trigger-text.js';
9
10
  const KEEP = { kind: 'keep' };
10
11
  const CONFIG_KEY_ORDER = Object.keys(configurationSchema.shape);
11
12
  const DEBUG_SUMMARY = { set: 'set the debug mode (agent prints context usage)', remove: 'remove the debug mode' };
@@ -60,7 +61,7 @@ function configSummary(updates) {
60
61
  }
61
62
  function snapshotSummary(section) {
62
63
  if (section.command === undefined)
63
- return `no snapshot command, so only zone headers will be injected (trigger: ${section.triggerZone})`;
64
+ return `no snapshot command, so only zone headers will be injected (trigger: ${describeTrigger(section)})`;
64
65
  const resume = section.resumeCommand === undefined ? '' : `, resume command ${section.resumeCommand}`;
65
- return `snapshot command ${section.command} at ${section.triggerZone}${resume}`;
66
+ return `snapshot command ${section.command} at ${describeTrigger(section)}${resume}`;
66
67
  }
@@ -39,7 +39,7 @@ export async function planInstallation(input) {
39
39
  const preservedAssets = (input.previousManifest?.assets ?? []).filter((a) => modifiedPaths.has(a.path));
40
40
  const adapterAssets = [...ap.assets.filter((a) => !modifiedPaths.has(a.path)), ...preservedAssets, ...extras.assets];
41
41
  const allAssets = buildManagedAssets(cfg.change.content ?? '', adapterAssets);
42
- const manifestChanges = active.length === 0 && !input.previousManifest ? [] : [planManifestChange({ root: input.projectRoot, assets: allAssets, entries: ap.entries, prev: input.previousManifest ?? null, pkgVer: input.packageVersion, snapshot: input.allSnapshots.find((s) => s.path === MANIFEST_RELATIVE_PATH) })];
42
+ const manifestChanges = active.length === 0 && !input.previousManifest ? [] : [planManifestChange({ root: input.projectRoot, assets: allAssets, entries: ap.entries, configOrigins: ap.configOrigins, prev: input.previousManifest ?? null, pkgVer: input.packageVersion, snapshot: input.allSnapshots.find((s) => s.path === MANIFEST_RELATIVE_PATH) })];
43
43
  const plannedChanges = [cfg.change, ...manifestChanges, ...protection.changes, ...extras.changes, ...removals.changes];
44
44
  const ignore = planGitIgnoreForInstall({ root: input.projectRoot, enabled: cfg.config.gitIgnore !== false, insideGit: input.insideGit ?? false, hasInstall: manifestChanges.length > 0, assetPaths: allAssets.map((asset) => asset.path), changes: plannedChanges, snapshots: input.allSnapshots });
45
45
  const conflicts = [...ap.conflicts, ...protection.conflicts, ...ignore.conflicts];
@@ -1,9 +1,10 @@
1
1
  import type { FileSnapshot, PlannedChange } from '../contracts/changes.js';
2
- import { type InstallationManifest, type ManagedAsset, type ManagedEntry } from '../contracts/manifest.js';
2
+ import { type ConfigOrigin, type InstallationManifest, type ManagedAsset, type ManagedEntry } from '../contracts/manifest.js';
3
3
  export type ManifestChangeInput = {
4
4
  root: string;
5
5
  assets: readonly ManagedAsset[];
6
6
  entries: readonly ManagedEntry[];
7
+ configOrigins?: readonly ConfigOrigin[] | undefined;
7
8
  prev: InstallationManifest | null;
8
9
  pkgVer: string;
9
10
  snapshot?: FileSnapshot | null | undefined;
@@ -1,7 +1,8 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { MANIFEST_RELATIVE_PATH } from '../contracts/manifest.js';
3
3
  export function planManifestChange(input) {
4
- const manifest = { schemaVersion: 1, packageVersion: input.pkgVer, assets: input.assets, entries: input.entries };
4
+ const origins = input.configOrigins ?? [];
5
+ const manifest = { schemaVersion: 1, packageVersion: input.pkgVer, assets: input.assets, entries: input.entries, ...(origins.length > 0 ? { configOrigins: origins } : {}) };
5
6
  const content = `${JSON.stringify(manifest, null, 2)}\n`;
6
7
  const defaultPath = resolve(input.root, MANIFEST_RELATIVE_PATH).replace(/\\/g, '/');
7
8
  const realPath = (input.snapshot?.realPath ?? defaultPath).replace(/\\/g, '/');
@@ -13,7 +13,7 @@ export async function handleSessionReset(options, event, hooks = {}) {
13
13
  if (event.reason === 'compact' && !COMPACTION_BOOT_HARNESSES.includes(options.descriptor.harness))
14
14
  return NEUTRAL;
15
15
  hooks.onPhase?.('guidance');
16
- const text = resumeText(options.config.snapshot) ?? await claimHandoff(options, event.reason, hooks.deadline);
16
+ const text = resumeText(options.config.snapshot, options.descriptor.skillInvocation) ?? await claimHandoff(options, event.reason, hooks.deadline);
17
17
  return text === null ? NEUTRAL : { kind: 'context', block: text };
18
18
  }
19
19
  async function claimHandoff(options, reason, deadline) {
@@ -0,0 +1,16 @@
1
+ import type { HarnessId } from '../contracts/harness.js';
2
+ import type { CatalogSkill } from '../contracts/skill-catalog.js';
3
+ export type SkillOrigin = {
4
+ readonly harness: HarnessId;
5
+ readonly folder: string;
6
+ };
7
+ export type SkillCandidate = {
8
+ readonly name: string;
9
+ readonly description: string;
10
+ readonly origins: readonly SkillOrigin[];
11
+ };
12
+ export type HarnessCatalog = {
13
+ readonly harness: HarnessId;
14
+ readonly skills: readonly CatalogSkill[];
15
+ };
16
+ export declare function mergeCatalogs(catalogs: readonly HarnessCatalog[]): SkillCandidate[];
@@ -0,0 +1,14 @@
1
+ function byHarness(left, right) {
2
+ return left.harness.localeCompare(right.harness) || left.folder.localeCompare(right.folder);
3
+ }
4
+ export function mergeCatalogs(catalogs) {
5
+ const byName = new Map();
6
+ for (const { harness, skills } of catalogs) {
7
+ for (const skill of skills) {
8
+ const known = byName.get(skill.name) ?? { description: skill.description, origins: [] };
9
+ known.origins.push({ harness, folder: skill.folder });
10
+ byName.set(skill.name, known);
11
+ }
12
+ }
13
+ return [...byName].map(([name, { description, origins }]) => ({ name, description, origins: [...origins].sort(byHarness) }));
14
+ }
@@ -0,0 +1,4 @@
1
+ export declare function slashInvocation(skill: string): string;
2
+ export declare function dollarInvocation(skill: string): string;
3
+ export declare function skillSlashInvocation(skill: string): string;
4
+ export declare function sentenceInvocation(skill: string): string;
@@ -0,0 +1,12 @@
1
+ export function slashInvocation(skill) {
2
+ return `/${skill}`;
3
+ }
4
+ export function dollarInvocation(skill) {
5
+ return `$${skill}`;
6
+ }
7
+ export function skillSlashInvocation(skill) {
8
+ return `/skill:${skill}`;
9
+ }
10
+ export function sentenceInvocation(skill) {
11
+ return `use the ${skill} skill`;
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { SkillCandidate } from './skill-candidates.js';
2
+ export type SkillRole = 'snapshot' | 'resume';
3
+ export type RankOptions = {
4
+ readonly exclude?: string | undefined;
5
+ };
6
+ export declare function scoreSkill(role: SkillRole, candidate: SkillCandidate): number;
7
+ export declare function rankSkills(role: SkillRole, candidates: readonly SkillCandidate[], options?: RankOptions): SkillCandidate[];
@@ -0,0 +1,25 @@
1
+ const ROLE_TERMS = {
2
+ snapshot: ['snapshot', 'handoff', 'checkpoint'],
3
+ resume: ['resume', 'continue', 'retomar', 'retomada', 'continuar', 'orchestrate', 'flow'],
4
+ };
5
+ const NAME_SCORE = 100;
6
+ const DESCRIPTION_SCORE = 10;
7
+ const DESCRIPTION_WINDOW = 300;
8
+ function mentions(text, terms) {
9
+ const lowered = text.toLowerCase();
10
+ return terms.some((term) => lowered.includes(term));
11
+ }
12
+ export function scoreSkill(role, candidate) {
13
+ const terms = ROLE_TERMS[role];
14
+ if (mentions(candidate.name, terms))
15
+ return NAME_SCORE;
16
+ return mentions(candidate.description.slice(0, DESCRIPTION_WINDOW), terms) ? DESCRIPTION_SCORE : 0;
17
+ }
18
+ export function rankSkills(role, candidates, options = {}) {
19
+ return candidates
20
+ .filter((candidate) => candidate.name !== options.exclude)
21
+ .map((candidate) => ({ candidate, score: scoreSkill(role, candidate) }))
22
+ .filter(({ score }) => score > 0)
23
+ .sort((left, right) => right.score - left.score || right.candidate.origins.length - left.candidate.origins.length || left.candidate.name.localeCompare(right.candidate.name))
24
+ .map(({ candidate }) => candidate);
25
+ }
@@ -2,6 +2,7 @@ import { type SnapshotConfig } from '../contracts/configuration.js';
2
2
  export type SnapshotFlags = {
3
3
  readonly command?: string | undefined;
4
4
  readonly triggerZone?: string | undefined;
5
+ readonly triggerPercentage?: number | undefined;
5
6
  readonly resumeCommand?: string | undefined;
6
7
  readonly clearCommand: boolean;
7
8
  };
@@ -1,24 +1,31 @@
1
1
  import { snapshotSchema } from '../contracts/configuration.js';
2
2
  const SECTION_PATH = 'snapshot';
3
+ const CONFIG_FILE = 'context-brake.config.json';
4
+ const FLAG_BY_FIELD = { command: '--snapshot-command <skill>', resumeCommand: '--resume-command <skill>', triggerZone: '--snapshot-trigger <YELLOW|RED>', triggerPercentage: '--snapshot-trigger-percentage <1-100>' };
3
5
  const CLEAR_CONFLICT = '--no-snapshot-command cannot be combined with --snapshot-command or --resume-command.';
4
6
  export function mergeSnapshot(current, flags) {
5
7
  if (flags.clearCommand && (flags.command !== undefined || flags.resumeCommand !== undefined))
6
8
  return { error: CLEAR_CONFLICT };
7
- if (!flags.clearCommand && [flags.command, flags.triggerZone, flags.resumeCommand].every((value) => value === undefined))
9
+ if (!flags.clearCommand && [flags.command, flags.triggerZone, flags.triggerPercentage, flags.resumeCommand].every((value) => value === undefined))
8
10
  return { update: { kind: 'keep' } };
9
- const base = flags.clearCommand ? { triggerZone: current?.triggerZone } : { ...current };
10
- const result = snapshotSchema.safeParse(withoutUndefined({ ...base, ...definedFields(flags) }));
11
+ const result = snapshotSchema.safeParse(withoutUndefined({ ...baseSection(current, flags), ...definedFields(flags) }));
11
12
  if (result.success)
12
13
  return { update: { kind: 'set', section: result.data } };
13
14
  const issue = result.error.issues[0];
14
15
  const path = [SECTION_PATH, ...(issue?.path ?? [])].join('.');
15
- return { error: `Invalid snapshot option: ${path} ${issue?.message ?? 'is invalid'}.` };
16
+ const flag = FLAG_BY_FIELD[String(issue?.path[0])];
17
+ const where = flag === undefined ? '' : ` Set it with ${flag} or in ${CONFIG_FILE}.`;
18
+ return { error: `Invalid snapshot option: ${path} ${issue?.message ?? 'is invalid'}.${where}` };
16
19
  }
17
20
  export function applySnapshot(config, update) {
18
21
  return update.kind === 'keep' ? config : { ...config, snapshot: update.section };
19
22
  }
23
+ function baseSection(current, flags) {
24
+ const trigger = { triggerZone: current?.triggerZone, triggerPercentage: flags.triggerZone === undefined ? current?.triggerPercentage : undefined };
25
+ return flags.clearCommand ? trigger : { ...current, ...trigger };
26
+ }
20
27
  function definedFields(flags) {
21
- return withoutUndefined({ command: flags.command, triggerZone: flags.triggerZone, resumeCommand: flags.resumeCommand });
28
+ return withoutUndefined({ command: flags.command, triggerZone: flags.triggerZone, triggerPercentage: flags.triggerPercentage, resumeCommand: flags.resumeCommand });
22
29
  }
23
30
  function withoutUndefined(values) {
24
31
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
@@ -0,0 +1,5 @@
1
+ export type TriggerSetting = {
2
+ readonly triggerZone: string;
3
+ readonly triggerPercentage?: number | null | undefined;
4
+ };
5
+ export declare function describeTrigger(setting: TriggerSetting): string;
@@ -0,0 +1,4 @@
1
+ export function describeTrigger(setting) {
2
+ const percentage = setting.triggerPercentage ?? null;
3
+ return percentage === null ? setting.triggerZone : `${percentage}% of the context window`;
4
+ }
@@ -1,6 +1,17 @@
1
1
  import type { SnapshotConfig } from '../contracts/configuration.js';
2
+ import type { SkillInvocationFormat } from '../contracts/skill-invocation.js';
2
3
  import { type Zone } from '../contracts/zones.js';
3
4
  import type { RestartMode } from './restart-mode.js';
4
- export declare function zoneAction(zone: Zone, snapshot: SnapshotConfig, mode?: RestartMode): string;
5
+ export type GuidanceOptions = {
6
+ readonly mode?: RestartMode | undefined;
7
+ readonly invoke?: SkillInvocationFormat | undefined;
8
+ readonly usagePercentage?: number | undefined;
9
+ };
10
+ export type ZoneUsage = {
11
+ readonly zone: Zone;
12
+ readonly usagePercentage: number;
13
+ };
14
+ export declare function isResetDue(reading: ZoneUsage, snapshot: SnapshotConfig): boolean;
15
+ export declare function zoneAction(zone: Zone, snapshot: SnapshotConfig, options?: GuidanceOptions): string;
5
16
  export declare function handoffResumeText(archivedPath: string): string;
6
- export declare function resumeText(snapshot: SnapshotConfig): string | null;
17
+ export declare function resumeText(snapshot: SnapshotConfig, invoke?: SkillInvocationFormat): string | null;