gsd-pi 2.8.0 → 2.8.2

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 (143) hide show
  1. package/dist/loader.js +5 -0
  2. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
  3. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
  4. package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
  5. package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +32 -22
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
  33. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  34. package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
  35. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  36. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  37. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  38. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  39. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  41. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
  44. package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  45. package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
  46. package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
  47. package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
  48. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +35 -22
  49. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  50. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  51. package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
  52. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  53. package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
  54. package/package.json +6 -1
  55. package/packages/pi-coding-agent/dist/config.d.ts +2 -0
  56. package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/config.js +4 -0
  58. package/packages/pi-coding-agent/dist/config.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  60. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  62. package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
  64. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  66. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  67. package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
  68. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  69. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  70. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
  72. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  74. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/tools/bash.js +32 -22
  76. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  78. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  80. package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  84. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/index.d.ts +3 -1
  86. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/index.js +4 -1
  88. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  91. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  93. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
  95. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  96. package/packages/pi-coding-agent/src/config.ts +5 -0
  97. package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  98. package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
  99. package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
  100. package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
  101. package/packages/pi-coding-agent/src/core/tools/bash.ts +35 -22
  102. package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  103. package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  104. package/packages/pi-coding-agent/src/index.ts +4 -1
  105. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  106. package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
  107. package/src/resources/extensions/bg-shell/index.ts +2 -1
  108. package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
  109. package/src/resources/extensions/gsd/auto.ts +92 -49
  110. package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
  111. package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
  112. package/src/resources/extensions/gsd/exit-command.ts +18 -0
  113. package/src/resources/extensions/gsd/files.ts +9 -40
  114. package/src/resources/extensions/gsd/git-service.ts +62 -17
  115. package/src/resources/extensions/gsd/gitignore.ts +28 -0
  116. package/src/resources/extensions/gsd/guided-flow.ts +49 -11
  117. package/src/resources/extensions/gsd/index.ts +111 -16
  118. package/src/resources/extensions/gsd/preferences.ts +8 -0
  119. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  120. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
  121. package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
  122. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  123. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
  124. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  125. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
  126. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
  127. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  128. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
  131. package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
  132. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
  133. package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  134. package/src/resources/extensions/gsd/tests/git-service.test.ts +116 -39
  135. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
  136. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
  137. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
  138. package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
  139. package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  140. package/src/resources/extensions/ttsr/index.ts +163 -0
  141. package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
  142. package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  143. package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Time Traveling Stream Rules (TTSR) Manager
3
+ *
4
+ * Manages rules that get injected mid-stream when their condition pattern matches
5
+ * the agent's output. When a match occurs, the stream is aborted, the rule is
6
+ * injected as a system reminder, and the request is retried.
7
+ */
8
+ import picomatch from "picomatch";
9
+
10
+ export type TtsrMatchSource = "text" | "thinking" | "tool";
11
+
12
+ /** Context about the stream content currently being checked against TTSR rules. */
13
+ export interface TtsrMatchContext {
14
+ source: TtsrMatchSource;
15
+ /** Tool name for tool argument deltas, e.g. "edit" or "write". */
16
+ toolName?: string;
17
+ /** Candidate file paths associated with the current stream chunk. */
18
+ filePaths?: string[];
19
+ /** Stable key to isolate buffering (for example a tool call ID). */
20
+ streamKey?: string;
21
+ }
22
+
23
+ export interface Rule {
24
+ name: string;
25
+ path: string;
26
+ content: string;
27
+ condition: string[];
28
+ scope?: string[];
29
+ globs?: string[];
30
+ }
31
+
32
+ export interface TtsrSettings {
33
+ enabled?: boolean;
34
+ contextMode?: "discard" | "keep";
35
+ interruptMode?: "always" | "first";
36
+ repeatMode?: "once" | "gap";
37
+ repeatGap?: number;
38
+ }
39
+
40
+ interface ToolScope {
41
+ toolName?: string;
42
+ pathMatcher?: picomatch.Matcher;
43
+ pathPattern?: string;
44
+ }
45
+
46
+ interface TtsrScope {
47
+ allowText: boolean;
48
+ allowThinking: boolean;
49
+ allowAnyTool: boolean;
50
+ toolScopes: ToolScope[];
51
+ }
52
+
53
+ interface TtsrEntry {
54
+ rule: Rule;
55
+ conditions: RegExp[];
56
+ scope: TtsrScope;
57
+ globalPathMatchers?: picomatch.Matcher[];
58
+ }
59
+
60
+ /** Tracks when a rule was last injected (for repeat gating). */
61
+ interface InjectionRecord {
62
+ lastInjectedAt: number;
63
+ }
64
+
65
+ const DEFAULT_SETTINGS: Required<TtsrSettings> = {
66
+ enabled: true,
67
+ contextMode: "discard",
68
+ interruptMode: "always",
69
+ repeatMode: "once",
70
+ repeatGap: 10,
71
+ };
72
+
73
+ /** Cap per-stream buffer at 512KB to prevent unbounded memory growth. */
74
+ const MAX_BUFFER_BYTES = 512 * 1024;
75
+
76
+ const DEFAULT_SCOPE: TtsrScope = {
77
+ allowText: true,
78
+ allowThinking: false,
79
+ allowAnyTool: true,
80
+ toolScopes: [],
81
+ };
82
+
83
+ export class TtsrManager {
84
+ readonly #settings: Required<TtsrSettings>;
85
+ readonly #rules = new Map<string, TtsrEntry>();
86
+ readonly #injectionRecords = new Map<string, InjectionRecord>();
87
+ readonly #buffers = new Map<string, string>();
88
+ #messageCount = 0;
89
+
90
+ constructor(settings?: TtsrSettings) {
91
+ this.#settings = { ...DEFAULT_SETTINGS, ...settings };
92
+ }
93
+
94
+ #canTrigger(ruleName: string): boolean {
95
+ const record = this.#injectionRecords.get(ruleName);
96
+ if (!record) return true;
97
+ if (this.#settings.repeatMode === "once") return false;
98
+ const gap = this.#messageCount - record.lastInjectedAt;
99
+ return gap >= this.#settings.repeatGap;
100
+ }
101
+
102
+ #compileConditions(rule: Rule): RegExp[] {
103
+ const compiled: RegExp[] = [];
104
+ for (const pattern of rule.condition ?? []) {
105
+ try {
106
+ compiled.push(new RegExp(pattern));
107
+ } catch (err) {
108
+ console.warn(`[ttsr] Rule "${rule.name}": invalid regex "${pattern}" — ${(err as Error).message}`);
109
+ }
110
+ }
111
+ return compiled;
112
+ }
113
+
114
+ #compileGlobalPathMatchers(globs: Rule["globs"]): picomatch.Matcher[] | undefined {
115
+ if (!globs || globs.length === 0) return undefined;
116
+ const matchers = globs
117
+ .map((g) => g.trim())
118
+ .filter((g) => g.length > 0)
119
+ .map((g) => picomatch(g));
120
+ return matchers.length > 0 ? matchers : undefined;
121
+ }
122
+
123
+ #parseToolScopeToken(token: string): ToolScope | undefined {
124
+ const match =
125
+ /^(?:(?<prefix>tool)(?::(?<tool>[a-z0-9_-]+))?|(?<bare>[a-z0-9_-]+))(?:\((?<path>[^)]+)\))?$/i.exec(token);
126
+ if (!match) return undefined;
127
+
128
+ const groups = match.groups;
129
+ const hasToolPrefix = groups?.prefix !== undefined;
130
+ const toolName = (groups?.tool ?? (hasToolPrefix ? undefined : groups?.bare))?.trim().toLowerCase();
131
+ const pathPattern = groups?.path?.trim();
132
+
133
+ if (!pathPattern) return { toolName };
134
+
135
+ return {
136
+ toolName,
137
+ pathPattern,
138
+ pathMatcher: picomatch(pathPattern),
139
+ };
140
+ }
141
+
142
+ #buildScope(rule: Rule): TtsrScope {
143
+ if (!rule.scope || rule.scope.length === 0) {
144
+ return {
145
+ allowText: DEFAULT_SCOPE.allowText,
146
+ allowThinking: DEFAULT_SCOPE.allowThinking,
147
+ allowAnyTool: DEFAULT_SCOPE.allowAnyTool,
148
+ toolScopes: [...DEFAULT_SCOPE.toolScopes],
149
+ };
150
+ }
151
+
152
+ const scope: TtsrScope = {
153
+ allowText: false,
154
+ allowThinking: false,
155
+ allowAnyTool: false,
156
+ toolScopes: [],
157
+ };
158
+
159
+ for (const rawToken of rule.scope) {
160
+ const token = rawToken.trim();
161
+ const normalized = token.toLowerCase();
162
+ if (token.length === 0) continue;
163
+
164
+ if (normalized === "text") {
165
+ scope.allowText = true;
166
+ continue;
167
+ }
168
+ if (normalized === "thinking") {
169
+ scope.allowThinking = true;
170
+ continue;
171
+ }
172
+ if (normalized === "tool" || normalized === "toolcall") {
173
+ scope.allowAnyTool = true;
174
+ continue;
175
+ }
176
+
177
+ const toolScope = this.#parseToolScopeToken(token);
178
+ if (!toolScope) continue;
179
+
180
+ if (!toolScope.toolName && !toolScope.pathMatcher) {
181
+ scope.allowAnyTool = true;
182
+ continue;
183
+ }
184
+
185
+ scope.toolScopes.push(toolScope);
186
+ }
187
+
188
+ return scope;
189
+ }
190
+
191
+ #hasReachableScope(scope: TtsrScope): boolean {
192
+ return scope.allowText || scope.allowThinking || scope.allowAnyTool || scope.toolScopes.length > 0;
193
+ }
194
+
195
+ #bufferKey(context: TtsrMatchContext): string {
196
+ if (context.streamKey && context.streamKey.trim().length > 0) return context.streamKey;
197
+ if (context.source !== "tool") return context.source;
198
+ const toolName = context.toolName?.trim().toLowerCase();
199
+ return toolName ? `tool:${toolName}` : "tool";
200
+ }
201
+
202
+ #normalizePath(pathValue: string): string {
203
+ return pathValue.replaceAll("\\", "/");
204
+ }
205
+
206
+ #matchesGlob(matcher: picomatch.Matcher, filePaths: string[] | undefined): boolean {
207
+ if (!filePaths || filePaths.length === 0) return false;
208
+ for (const filePath of filePaths) {
209
+ const normalized = this.#normalizePath(filePath);
210
+ if (matcher(normalized)) return true;
211
+ const slashIndex = normalized.lastIndexOf("/");
212
+ const basename = slashIndex === -1 ? normalized : normalized.slice(slashIndex + 1);
213
+ if (basename !== normalized && matcher(basename)) return true;
214
+ }
215
+ return false;
216
+ }
217
+
218
+ #matchesGlobalPaths(entry: TtsrEntry, context: TtsrMatchContext): boolean {
219
+ if (!entry.globalPathMatchers || entry.globalPathMatchers.length === 0) return true;
220
+ for (const matcher of entry.globalPathMatchers) {
221
+ if (this.#matchesGlob(matcher, context.filePaths)) return true;
222
+ }
223
+ return false;
224
+ }
225
+
226
+ #matchesScope(entry: TtsrEntry, context: TtsrMatchContext): boolean {
227
+ if (context.source === "text") return entry.scope.allowText;
228
+ if (context.source === "thinking") return entry.scope.allowThinking;
229
+ if (entry.scope.allowAnyTool) return true;
230
+
231
+ const toolName = context.toolName?.trim().toLowerCase();
232
+ for (const toolScope of entry.scope.toolScopes) {
233
+ if (toolScope.toolName && toolScope.toolName !== toolName) continue;
234
+ if (toolScope.pathMatcher && !this.#matchesGlob(toolScope.pathMatcher, context.filePaths)) continue;
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+
240
+ #matchesCondition(entry: TtsrEntry, streamBuffer: string): boolean {
241
+ for (const condition of entry.conditions) {
242
+ condition.lastIndex = 0;
243
+ if (condition.test(streamBuffer)) return true;
244
+ }
245
+ return false;
246
+ }
247
+
248
+ /** Add a TTSR rule to be monitored. */
249
+ addRule(rule: Rule): boolean {
250
+ if (this.#rules.has(rule.name)) return false;
251
+
252
+ const conditions = this.#compileConditions(rule);
253
+ if (conditions.length === 0) return false;
254
+
255
+ const scope = this.#buildScope(rule);
256
+ if (!this.#hasReachableScope(scope)) return false;
257
+
258
+ const globalPathMatchers = this.#compileGlobalPathMatchers(rule.globs);
259
+ this.#rules.set(rule.name, { rule, conditions, scope, globalPathMatchers });
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * Add a stream chunk to its scoped buffer and return matching rules.
265
+ *
266
+ * Buffers are isolated by source/tool key so matches don't bleed across
267
+ * assistant prose, thinking text, and unrelated tool argument streams.
268
+ */
269
+ checkDelta(delta: string, context: TtsrMatchContext): Rule[] {
270
+ const bufferKey = this.#bufferKey(context);
271
+ let nextBuffer = `${this.#buffers.get(bufferKey) ?? ""}${delta}`;
272
+ // Cap buffer size — keep the tail so patterns still match recent output
273
+ if (nextBuffer.length > MAX_BUFFER_BYTES) {
274
+ nextBuffer = nextBuffer.slice(-MAX_BUFFER_BYTES);
275
+ }
276
+ this.#buffers.set(bufferKey, nextBuffer);
277
+
278
+ const matches: Rule[] = [];
279
+ for (const [name, entry] of this.#rules) {
280
+ if (!this.#canTrigger(name)) continue;
281
+ if (!this.#matchesScope(entry, context)) continue;
282
+ if (!this.#matchesGlobalPaths(entry, context)) continue;
283
+ if (!this.#matchesCondition(entry, nextBuffer)) continue;
284
+ matches.push(entry.rule);
285
+ }
286
+ return matches;
287
+ }
288
+
289
+ /** Mark rules as injected (won't trigger again until conditions allow). */
290
+ markInjected(rulesToMark: Rule[]): void {
291
+ this.markInjectedByNames(rulesToMark.map((r) => r.name));
292
+ }
293
+
294
+ /** Mark rule names as injected. */
295
+ markInjectedByNames(ruleNames: string[]): void {
296
+ for (const rawName of ruleNames) {
297
+ const ruleName = rawName.trim();
298
+ if (ruleName.length === 0) continue;
299
+ const record = this.#injectionRecords.get(ruleName);
300
+ if (!record) {
301
+ this.#injectionRecords.set(ruleName, { lastInjectedAt: this.#messageCount });
302
+ } else {
303
+ record.lastInjectedAt = this.#messageCount;
304
+ }
305
+ }
306
+ }
307
+
308
+ /** Get names of all injected rules (for persistence). */
309
+ getInjectedRuleNames(): string[] {
310
+ return Array.from(this.#injectionRecords.keys());
311
+ }
312
+
313
+ /** Restore injected state from a list of rule names. */
314
+ restoreInjected(ruleNames: string[]): void {
315
+ for (const name of ruleNames) {
316
+ this.#injectionRecords.set(name, { lastInjectedAt: 0 });
317
+ }
318
+ }
319
+
320
+ /** Reset stream buffers (called on new turn). */
321
+ resetBuffer(): void {
322
+ this.#buffers.clear();
323
+ }
324
+
325
+ /** Check if any TTSR rules are registered. */
326
+ hasRules(): boolean {
327
+ return this.#rules.size > 0;
328
+ }
329
+
330
+ /** Increment message counter (call after each turn). */
331
+ incrementMessageCount(): void {
332
+ this.#messageCount++;
333
+ }
334
+
335
+ /** Get current message count. */
336
+ getMessageCount(): number {
337
+ return this.#messageCount;
338
+ }
339
+
340
+ /** Get settings. */
341
+ getSettings(): Required<TtsrSettings> {
342
+ return this.#settings;
343
+ }
344
+ }