@gajae-code/coding-agent 0.8.2 → 0.9.1

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 (214) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/fixture-report.d.ts +20 -0
  5. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  6. package/dist/types/cli/nofile-limit.d.ts +11 -0
  7. package/dist/types/cli/update-cli.d.ts +22 -0
  8. package/dist/types/config/settings-schema.d.ts +119 -1
  9. package/dist/types/discovery/index.d.ts +1 -0
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/export/ttsr.d.ts +14 -3
  12. package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  15. package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
  16. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  17. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
  19. package/dist/types/goals/runtime.d.ts +1 -0
  20. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  21. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  22. package/dist/types/modes/components/status-line.d.ts +9 -0
  23. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  26. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  27. package/dist/types/modes/interactive-mode.d.ts +4 -0
  28. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  29. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  30. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  31. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  32. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  33. package/dist/types/modes/types.d.ts +3 -0
  34. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  36. package/dist/types/notifications/config-commands.d.ts +9 -0
  37. package/dist/types/notifications/config.d.ts +22 -0
  38. package/dist/types/notifications/index.d.ts +2 -0
  39. package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  41. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  42. package/dist/types/notifications/rich-draft.d.ts +68 -0
  43. package/dist/types/notifications/rich-render.d.ts +90 -0
  44. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  45. package/dist/types/notifications/telegram-daemon.d.ts +30 -0
  46. package/dist/types/notifications/telegram-reference.d.ts +7 -0
  47. package/dist/types/notifications/threaded-render.d.ts +14 -0
  48. package/dist/types/sdk.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +43 -1
  50. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  51. package/dist/types/session/messages.d.ts +4 -0
  52. package/dist/types/session/session-manager.d.ts +17 -1
  53. package/dist/types/skill-state/active-state.d.ts +18 -1
  54. package/dist/types/system-prompt.d.ts +18 -2
  55. package/dist/types/task/executor.d.ts +1 -0
  56. package/dist/types/task/fork-context-budget.d.ts +7 -0
  57. package/dist/types/task/token-log.d.ts +30 -0
  58. package/dist/types/task/token-log.test.d.ts +1 -0
  59. package/dist/types/tools/bisect.d.ts +81 -0
  60. package/dist/types/tools/index.d.ts +1 -0
  61. package/dist/types/tools/search.d.ts +2 -2
  62. package/dist/types/utils/file-mentions.d.ts +14 -0
  63. package/dist/types/utils/git.d.ts +32 -0
  64. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  65. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  66. package/package.json +8 -11
  67. package/src/append-only-mode.ts +58 -0
  68. package/src/autoresearch/prompt.md +1 -1
  69. package/src/autoresearch/tools/log-experiment.ts +1 -1
  70. package/src/capability/rule.ts +9 -0
  71. package/src/cli/config-cli.ts +27 -3
  72. package/src/cli/fixture-report.test.ts +230 -0
  73. package/src/cli/fixture-report.ts +171 -0
  74. package/src/cli/nofile-limit.ts +48 -0
  75. package/src/cli/notify-cli.ts +11 -9
  76. package/src/cli/update-cli.ts +85 -9
  77. package/src/cli.ts +26 -0
  78. package/src/commands/contribution-prep.ts +1 -1
  79. package/src/config/keybindings.ts +1 -1
  80. package/src/config/settings-schema.ts +76 -1
  81. package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
  82. package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
  83. package/src/defaults/gjc/skills/team/SKILL.md +16 -13
  84. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  85. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  86. package/src/defaults/gjc-defaults.ts +9 -0
  87. package/src/discovery/helpers.ts +10 -0
  88. package/src/discovery/index.ts +1 -0
  89. package/src/export/html/template.generated.ts +1 -1
  90. package/src/export/html/template.js +60 -2
  91. package/src/export/ttsr.ts +96 -18
  92. package/src/extensibility/extensions/runner.ts +181 -219
  93. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  94. package/src/gjc-runtime/deep-interview-state.ts +31 -2
  95. package/src/gjc-runtime/launch-tmux.ts +6 -0
  96. package/src/gjc-runtime/session-state-sidecar.ts +208 -13
  97. package/src/gjc-runtime/state-writer.ts +15 -0
  98. package/src/gjc-runtime/team-runtime.ts +175 -19
  99. package/src/gjc-runtime/tmux-common.ts +1 -0
  100. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  101. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  102. package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
  103. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  104. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  105. package/src/goals/runtime.ts +10 -2
  106. package/src/hindsight/backend.ts +1 -3
  107. package/src/hooks/native-skill-hook.ts +95 -4
  108. package/src/internal-urls/docs-index.generated.ts +18 -9
  109. package/src/lsp/client.ts +1 -0
  110. package/src/main.ts +43 -2
  111. package/src/memories/index.ts +12 -1
  112. package/src/modes/components/assistant-message.ts +241 -82
  113. package/src/modes/components/custom-editor.ts +1 -0
  114. package/src/modes/components/footer.ts +7 -2
  115. package/src/modes/components/status-line.ts +167 -54
  116. package/src/modes/components/thinking-selector.ts +10 -8
  117. package/src/modes/components/tool-execution.ts +44 -19
  118. package/src/modes/controllers/command-controller.ts +39 -2
  119. package/src/modes/controllers/event-controller.ts +77 -49
  120. package/src/modes/controllers/extension-ui-controller.ts +8 -1
  121. package/src/modes/controllers/input-controller.ts +13 -18
  122. package/src/modes/controllers/selector-controller.ts +41 -0
  123. package/src/modes/interactive-mode.ts +14 -1
  124. package/src/modes/rpc/rpc-mode.ts +32 -12
  125. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  126. package/src/modes/rpc/rpc-types.ts +9 -0
  127. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  128. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  129. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  130. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  131. package/src/modes/types.ts +3 -0
  132. package/src/modes/utils/injected-user-submission.ts +94 -0
  133. package/src/modes/utils/ui-helpers.ts +17 -4
  134. package/src/notifications/config-commands.ts +20 -0
  135. package/src/notifications/config.ts +43 -4
  136. package/src/notifications/index.ts +105 -9
  137. package/src/notifications/lifecycle-commands.ts +37 -16
  138. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  139. package/src/notifications/reply-sent-store.ts +134 -0
  140. package/src/notifications/rich-draft.ts +107 -0
  141. package/src/notifications/rich-render.ts +142 -0
  142. package/src/notifications/telegram-daemon-cli.ts +54 -7
  143. package/src/notifications/telegram-daemon-control.ts +2 -2
  144. package/src/notifications/telegram-daemon.ts +390 -56
  145. package/src/notifications/telegram-reference.ts +17 -0
  146. package/src/notifications/threaded-render.ts +45 -2
  147. package/src/prompts/agents/architect.md +19 -10
  148. package/src/prompts/agents/critic.md +16 -18
  149. package/src/prompts/agents/executor.md +14 -11
  150. package/src/prompts/agents/planner.md +18 -13
  151. package/src/prompts/ci-green-request.md +3 -8
  152. package/src/prompts/goals/goal-continuation.md +0 -5
  153. package/src/prompts/goals/goal-mode-active.md +0 -5
  154. package/src/prompts/system/agent-creation-architect.md +2 -2
  155. package/src/prompts/system/plan-mode-active.md +1 -1
  156. package/src/prompts/system/plan-mode-subagent.md +3 -3
  157. package/src/prompts/system/project-prompt.md +0 -13
  158. package/src/prompts/system/subagent-system-prompt.md +3 -2
  159. package/src/prompts/system/system-prompt.md +26 -19
  160. package/src/prompts/system/volatile-project-context.md +10 -0
  161. package/src/prompts/system/web-search.md +4 -7
  162. package/src/prompts/tools/apply-patch.md +6 -0
  163. package/src/prompts/tools/ast-edit.md +1 -0
  164. package/src/prompts/tools/bash.md +1 -1
  165. package/src/prompts/tools/bisect.md +22 -0
  166. package/src/prompts/tools/browser.md +1 -1
  167. package/src/prompts/tools/cron.md +0 -2
  168. package/src/prompts/tools/eval.md +2 -2
  169. package/src/prompts/tools/find.md +1 -1
  170. package/src/prompts/tools/github.md +6 -6
  171. package/src/prompts/tools/goal.md +1 -3
  172. package/src/prompts/tools/image-gen.md +12 -5
  173. package/src/prompts/tools/irc.md +5 -4
  174. package/src/prompts/tools/job.md +2 -2
  175. package/src/prompts/tools/lsp.md +1 -1
  176. package/src/prompts/tools/monitor.md +2 -2
  177. package/src/prompts/tools/patch.md +2 -2
  178. package/src/prompts/tools/replace.md +2 -21
  179. package/src/prompts/tools/rewind.md +1 -1
  180. package/src/prompts/tools/search-tool-bm25.md +6 -11
  181. package/src/prompts/tools/search.md +1 -1
  182. package/src/prompts/tools/skill.md +3 -2
  183. package/src/prompts/tools/ssh.md +2 -1
  184. package/src/prompts/tools/subagent.md +2 -2
  185. package/src/prompts/tools/task.md +4 -5
  186. package/src/prompts/tools/todo-write.md +1 -1
  187. package/src/prompts/tools/vim.md +1 -0
  188. package/src/prompts/tools/web-search.md +2 -0
  189. package/src/prompts/tools/write.md +8 -2
  190. package/src/sdk.ts +68 -30
  191. package/src/session/agent-session.ts +734 -107
  192. package/src/session/file-mention-pruning.ts +64 -0
  193. package/src/session/messages.ts +4 -0
  194. package/src/session/session-manager.ts +59 -2
  195. package/src/skill-state/active-state.ts +141 -5
  196. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  197. package/src/slash-commands/builtin-registry.ts +143 -1
  198. package/src/system-prompt.ts +57 -16
  199. package/src/task/executor.ts +125 -20
  200. package/src/task/fork-context-advisory.ts +2 -7
  201. package/src/task/fork-context-budget.ts +9 -0
  202. package/src/task/index.ts +4 -3
  203. package/src/task/output-manager.ts +4 -2
  204. package/src/task/token-log.test.ts +188 -0
  205. package/src/task/token-log.ts +179 -0
  206. package/src/tools/bisect.ts +367 -0
  207. package/src/tools/index.ts +3 -0
  208. package/src/tools/output-meta.ts +116 -4
  209. package/src/tools/search.ts +8 -2
  210. package/src/tools/skill.ts +15 -0
  211. package/src/utils/file-mentions.ts +39 -7
  212. package/src/utils/git.ts +73 -0
  213. package/src/utils/pasted-image-path.ts +170 -0
  214. package/src/web/scrapers/youtube.ts +24 -12
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Staleness pruning for auto-read file-mention entries.
3
+ *
4
+ * A `@path` mention is superseded once a newer read or mention of the same
5
+ * resolved path appears later in the branch. Stale bodies are replaced with an
6
+ * explicit digest notice (never silently deleted) so context shrinks without
7
+ * losing the reference.
8
+ */
9
+ import type { FileMentionMessage } from "./messages";
10
+ import type { SessionEntry, SessionMessageEntry } from "./session-manager";
11
+
12
+ export interface FileMentionPruneResult {
13
+ changed: SessionMessageEntry[];
14
+ bytesSaved: number;
15
+ }
16
+
17
+ /** Notice injected in place of a superseded file-mention body. */
18
+ export function stalefileMentionNotice(path: string): string {
19
+ return `(pruned: a newer view of \`${path}\` appears later in the conversation; use the read tool to fetch it again)`;
20
+ }
21
+
22
+ export function pruneStaleFileMentions(
23
+ entries: readonly SessionEntry[],
24
+ resolveAbs: (path: string) => string,
25
+ ): FileMentionPruneResult {
26
+ // Latest branch index at which each absolute path was shown (read or mention).
27
+ const latestShownIndex = new Map<string, number>();
28
+ entries.forEach((entry, i) => {
29
+ if (entry.type !== "message") return;
30
+ const msg = entry.message;
31
+ if (msg.role === "fileMention") {
32
+ for (const file of msg.files) {
33
+ if (file.duplicate || file.pruned) continue;
34
+ latestShownIndex.set(resolveAbs(file.path), i);
35
+ }
36
+ } else if (msg.role === "toolResult") {
37
+ const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
38
+ if (typeof resolved === "string" && resolved) latestShownIndex.set(resolveAbs(resolved), i);
39
+ }
40
+ });
41
+
42
+ const changed: SessionMessageEntry[] = [];
43
+ let bytesSaved = 0;
44
+ entries.forEach((entry, i) => {
45
+ if (entry.type !== "message" || entry.message.role !== "fileMention") return;
46
+ const msg = entry.message as FileMentionMessage;
47
+ let mutated = false;
48
+ for (const file of msg.files) {
49
+ if (file.duplicate || file.pruned || !file.content) continue;
50
+ const latest = latestShownIndex.get(resolveAbs(file.path));
51
+ if (latest !== undefined && latest > i) {
52
+ const notice = stalefileMentionNotice(file.path);
53
+ bytesSaved += Math.max(0, Buffer.byteLength(file.content, "utf-8") - Buffer.byteLength(notice, "utf-8"));
54
+ file.content = notice;
55
+ file.pruned = true;
56
+ file.image = undefined;
57
+ file.lineCount = undefined;
58
+ mutated = true;
59
+ }
60
+ }
61
+ if (mutated) changed.push(entry as SessionMessageEntry);
62
+ });
63
+ return { changed, bytesSaved };
64
+ }
@@ -192,6 +192,10 @@ export interface FileMentionMessage {
192
192
  /** Why the file contents were omitted from auto-read. */
193
193
  skippedReason?: "tooLarge";
194
194
  image?: ImageContent;
195
+ /** Set when this mention duplicated a recently shown path (compact note, no full body). */
196
+ duplicate?: boolean;
197
+ /** Set when a stale entry was pruned to a digest notice (never silently deleted). */
198
+ pruned?: boolean;
195
199
  }>;
196
200
  timestamp: number;
197
201
  }
@@ -27,7 +27,9 @@ import {
27
27
  Snowflake,
28
28
  toError,
29
29
  } from "@gajae-code/utils";
30
+ import type { TtsrInjectionRecord } from "../export/ttsr";
30
31
  import { writeTextAtomic } from "../gjc-runtime/state-writer";
32
+
31
33
  import * as git from "../utils/git";
32
34
  import { ArtifactManager } from "./artifacts";
33
35
  import {
@@ -221,6 +223,10 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
221
223
  type: "ttsr_injection";
222
224
  /** Names of rules that were injected */
223
225
  injectedRules: string[];
226
+ /** Rich rule injection records with repeat state. */
227
+ injectedRuleRecords?: TtsrInjectionRecord[];
228
+ /** TTSR manager message count when this injection was recorded. */
229
+ ttsrMessageCount?: number;
224
230
  }
225
231
 
226
232
  /** Persisted MCP discovery selection state for a session branch. */
@@ -313,6 +319,11 @@ export interface SessionContext {
313
319
  models: Record<string, string>;
314
320
  /** Names of TTSR rules that have been injected this session */
315
321
  injectedTtsrRules: string[];
322
+ /** Rich TTSR rule injection records for repeat resume. */
323
+ injectedTtsrRuleRecords?: TtsrInjectionRecord[];
324
+ /** TTSR manager message count for repeat resume. */
325
+ ttsrMessageCount?: number;
326
+
316
327
  /** MCP tool names selected through discovery for this session branch. */
317
328
  selectedMCPToolNames: string[];
318
329
  /** Whether this branch contains an explicit persisted MCP selection entry. */
@@ -612,6 +623,9 @@ export function buildSessionContext(
612
623
  serviceTier: undefined,
613
624
  models: {},
614
625
  injectedTtsrRules: [],
626
+ injectedTtsrRuleRecords: [],
627
+ ttsrMessageCount: 0,
628
+
615
629
  selectedMCPToolNames: [],
616
630
  hasPersistedMCPToolSelection: false,
617
631
  mode: "none",
@@ -632,6 +646,8 @@ export function buildSessionContext(
632
646
  serviceTier: undefined,
633
647
  models: {},
634
648
  injectedTtsrRules: [],
649
+ injectedTtsrRuleRecords: [],
650
+ ttsrMessageCount: 0,
635
651
  selectedMCPToolNames: [],
636
652
  hasPersistedMCPToolSelection: false,
637
653
  mode: "none",
@@ -656,6 +672,9 @@ export function buildSessionContext(
656
672
  const models: Record<string, string> = {};
657
673
  let compaction: CompactionEntry | null = null;
658
674
  const injectedTtsrRulesSet = new Set<string>();
675
+ const injectedTtsrRuleRecords = new Map<string, TtsrInjectionRecord>();
676
+ let ttsrMessageCount = 0;
677
+
659
678
  let selectedMCPToolNames: string[] = [];
660
679
  let hasPersistedMCPToolSelection = false;
661
680
  let mode = "none";
@@ -695,9 +714,19 @@ export function buildSessionContext(
695
714
  } else if (entry.type === "compaction") {
696
715
  compaction = entry;
697
716
  } else if (entry.type === "ttsr_injection") {
698
- // Collect injected TTSR rule names
717
+ // Collect injected TTSR rule names and richer records when present.
699
718
  for (const ruleName of entry.injectedRules) {
700
719
  injectedTtsrRulesSet.add(ruleName);
720
+ if (!injectedTtsrRuleRecords.has(ruleName)) {
721
+ injectedTtsrRuleRecords.set(ruleName, { name: ruleName, lastInjectedAt: 0 });
722
+ }
723
+ }
724
+ for (const record of entry.injectedRuleRecords ?? []) {
725
+ injectedTtsrRulesSet.add(record.name);
726
+ injectedTtsrRuleRecords.set(record.name, record);
727
+ }
728
+ if (typeof entry.ttsrMessageCount === "number" && Number.isFinite(entry.ttsrMessageCount)) {
729
+ ttsrMessageCount = entry.ttsrMessageCount;
701
730
  }
702
731
  } else if (entry.type === "mcp_tool_selection") {
703
732
  selectedMCPToolNames = [...entry.selectedToolNames];
@@ -709,6 +738,7 @@ export function buildSessionContext(
709
738
  }
710
739
 
711
740
  const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
741
+ const injectedTtsrRuleRecordsArray = Array.from(injectedTtsrRuleRecords.values());
712
742
 
713
743
  // Build messages and collect corresponding entries
714
744
  // When there's a compaction, we need to:
@@ -797,6 +827,9 @@ export function buildSessionContext(
797
827
  serviceTier,
798
828
  models,
799
829
  injectedTtsrRules,
830
+ injectedTtsrRuleRecords: injectedTtsrRuleRecordsArray,
831
+ ttsrMessageCount,
832
+
800
833
  selectedMCPToolNames,
801
834
  hasPersistedMCPToolSelection,
802
835
  mode,
@@ -810,6 +843,9 @@ function cloneSessionContext(context: SessionContext): SessionContext {
810
843
  messages: cloneJsonSemantic(context.messages),
811
844
  models: { ...context.models },
812
845
  injectedTtsrRules: [...context.injectedTtsrRules],
846
+ injectedTtsrRuleRecords: context.injectedTtsrRuleRecords?.map(record => ({ ...record })),
847
+ ttsrMessageCount: context.ttsrMessageCount,
848
+
813
849
  selectedMCPToolNames: [...context.selectedMCPToolNames],
814
850
  modeData: cloneJsonSemantic(context.modeData),
815
851
  };
@@ -3957,6 +3993,25 @@ export class SessionManager {
3957
3993
  return entry.id;
3958
3994
  }
3959
3995
 
3996
+ /**
3997
+ * Append a root marker that starts a fresh active branch without changing the
3998
+ * session id or deleting earlier durable entries. Subsequent messages descend
3999
+ * from this marker, so provider context is clear while history remains
4000
+ * available for diagnostics/export.
4001
+ */
4002
+ appendContextClearEntry(data?: Record<string, unknown>): string {
4003
+ const entry: CustomEntry = {
4004
+ type: "custom",
4005
+ customType: "context_clear",
4006
+ data,
4007
+ id: generateId(this.#byId),
4008
+ parentId: null,
4009
+ timestamp: new Date().toISOString(),
4010
+ };
4011
+ this.#appendEntry(entry);
4012
+ return entry.id;
4013
+ }
4014
+
3960
4015
  /**
3961
4016
  * Write mutated message entries back into the canonical entry store by id.
3962
4017
  *
@@ -4048,13 +4103,15 @@ export class SessionManager {
4048
4103
  * @param ruleNames Names of rules that were injected
4049
4104
  * @returns Entry id
4050
4105
  */
4051
- appendTtsrInjection(ruleNames: string[]): string {
4106
+ appendTtsrInjection(ruleNames: string[], records?: TtsrInjectionRecord[], ttsrMessageCount?: number): string {
4052
4107
  const entry: TtsrInjectionEntry = {
4053
4108
  type: "ttsr_injection",
4054
4109
  id: generateId(this.#byId),
4055
4110
  parentId: this.#leafId,
4056
4111
  timestamp: new Date().toISOString(),
4057
4112
  injectedRules: ruleNames,
4113
+ injectedRuleRecords: records,
4114
+ ttsrMessageCount,
4058
4115
  };
4059
4116
  this.#appendEntry(entry);
4060
4117
  return entry.id;
@@ -1,11 +1,19 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
1
3
  import * as logger from "@gajae-code/utils/logger";
2
- import { activeSnapshotPath, assertNonEmptyGjcSessionId, modeStatePath } from "../gjc-runtime/session-layout";
4
+ import {
5
+ activeSnapshotPath,
6
+ activeStateDir,
7
+ assertNonEmptyGjcSessionId,
8
+ modeStatePath,
9
+ } from "../gjc-runtime/session-layout";
3
10
  import { resolveGjcSessionForRead, SessionResolutionError } from "../gjc-runtime/session-resolution";
4
11
  import {
5
12
  type ActiveSessionScope,
6
13
  readActiveEntries,
7
14
  rebuildActiveSnapshot,
8
15
  removeActiveEntry,
16
+ setActiveStateCacheInvalidator,
9
17
  writeActiveEntry,
10
18
  } from "../gjc-runtime/state-writer";
11
19
  import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } from "./canonical-skills";
@@ -614,14 +622,108 @@ async function mergeVisibleEntries(
614
622
  return collapsePlanningPipeline(visibleEntries).toSorted(comparePipelineEntry);
615
623
  }
616
624
 
617
- export async function readVisibleSkillActiveState(cwd: string, sessionId?: string): Promise<SkillActiveState | null> {
618
- let resolvedSessionId: string;
625
+ export type VisibleSkillActiveStateCacheTier = "security" | "hud";
626
+ export interface ReadVisibleSkillActiveStateOptions {
627
+ tier?: VisibleSkillActiveStateCacheTier;
628
+ }
629
+
630
+ interface ActiveStateStatSignature {
631
+ path: string;
632
+ mtimeMs: number;
633
+ size: number;
634
+ }
635
+
636
+ interface ActiveStateSignature {
637
+ sessionPath: ActiveStateStatSignature | null;
638
+ activeDir: ActiveStateStatSignature | null;
639
+ activeEntries: ActiveStateStatSignature[];
640
+ ralplanModeState: ActiveStateStatSignature | null;
641
+ }
642
+
643
+ interface VisibleSkillActiveStateCacheEntry {
644
+ signature: ActiveStateSignature;
645
+ value: SkillActiveState | null;
646
+ cachedAtMs: number;
647
+ }
648
+
649
+ const HUD_VISIBLE_STATE_TTL_MS = 500;
650
+ const SECURITY_VISIBLE_STATE_TTL_MS = 30_000;
651
+ const visibleSkillActiveStateCache = new Map<string, VisibleSkillActiveStateCacheEntry>();
652
+
653
+ async function statSignature(filePath: string): Promise<ActiveStateStatSignature | null> {
619
654
  try {
620
- resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
655
+ const stat = await fs.stat(filePath);
656
+ return { path: filePath, mtimeMs: stat.mtimeMs, size: stat.size };
621
657
  } catch (error) {
622
- if (error instanceof SessionResolutionError && error.code === "no_session") return null;
658
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
623
659
  throw error;
624
660
  }
661
+ }
662
+
663
+ function signaturesEqual(a: ActiveStateSignature, b: ActiveStateSignature): boolean {
664
+ const statEqual = (left: ActiveStateStatSignature | null, right: ActiveStateStatSignature | null): boolean =>
665
+ left === right ||
666
+ (Boolean(left) &&
667
+ Boolean(right) &&
668
+ left?.path === right?.path &&
669
+ left?.mtimeMs === right?.mtimeMs &&
670
+ left?.size === right?.size);
671
+ if (!statEqual(a.sessionPath, b.sessionPath)) return false;
672
+ if (!statEqual(a.activeDir, b.activeDir)) return false;
673
+ if (!statEqual(a.ralplanModeState, b.ralplanModeState)) return false;
674
+ if (a.activeEntries.length !== b.activeEntries.length) return false;
675
+ return a.activeEntries.every((entry, index) => statEqual(entry, b.activeEntries[index] ?? null));
676
+ }
677
+
678
+ export async function computeActiveStateSignature(cwd: string, sessionId: string): Promise<ActiveStateSignature> {
679
+ const resolvedCwd = path.resolve(cwd);
680
+ const { sessionPath } = getSkillActiveStatePaths(resolvedCwd, sessionId);
681
+ const activeDirPath = activeStateDir(resolvedCwd, sessionId);
682
+ const activeDir = await statSignature(activeDirPath);
683
+ let activeEntries: ActiveStateStatSignature[] = [];
684
+ try {
685
+ const names = await fs.readdir(activeDirPath);
686
+ activeEntries = (
687
+ await Promise.all(
688
+ names
689
+ .filter(name => name.endsWith(".json"))
690
+ .sort()
691
+ .map(name => statSignature(path.join(activeDirPath, name))),
692
+ )
693
+ ).filter((entry): entry is ActiveStateStatSignature => entry !== null);
694
+ } catch (error) {
695
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
696
+ }
697
+ return {
698
+ sessionPath: await statSignature(sessionPath),
699
+ activeDir,
700
+ activeEntries,
701
+ ralplanModeState: await statSignature(modeStatePath(resolvedCwd, sessionId, "ralplan")),
702
+ };
703
+ }
704
+
705
+ function visibleActiveStateCacheKey(cwd: string, sessionId: string): string {
706
+ return `${path.resolve(cwd)}\0${sessionId}`;
707
+ }
708
+
709
+ export function invalidateVisibleSkillActiveStateCache(cwd?: string, sessionId?: string): void {
710
+ if (!cwd && !sessionId) {
711
+ visibleSkillActiveStateCache.clear();
712
+ return;
713
+ }
714
+ const resolvedCwd = cwd ? path.resolve(cwd) : undefined;
715
+ const suffix = sessionId ? `\0${sessionId}` : undefined;
716
+ for (const key of [...visibleSkillActiveStateCache.keys()]) {
717
+ if (resolvedCwd && !key.startsWith(`${resolvedCwd}\0`)) continue;
718
+ if (suffix && !key.endsWith(suffix)) continue;
719
+ visibleSkillActiveStateCache.delete(key);
720
+ }
721
+ }
722
+
723
+ async function readVisibleSkillActiveStateUncached(
724
+ cwd: string,
725
+ resolvedSessionId: string,
726
+ ): Promise<SkillActiveState | null> {
625
727
  const { sessionPath } = getSkillActiveStatePaths(cwd, resolvedSessionId);
626
728
  const sessionState = await readRawActiveStateForHandoff(sessionPath, false);
627
729
  const activeSkills = await mergeVisibleEntries(cwd, sessionState, resolvedSessionId);
@@ -639,6 +741,40 @@ export async function readVisibleSkillActiveState(cwd: string, sessionId?: strin
639
741
  };
640
742
  }
641
743
 
744
+ setActiveStateCacheInvalidator(invalidateVisibleSkillActiveStateCache);
745
+
746
+ export async function readVisibleSkillActiveState(
747
+ cwd: string,
748
+ sessionId?: string,
749
+ opts?: ReadVisibleSkillActiveStateOptions,
750
+ ): Promise<SkillActiveState | null> {
751
+ let resolvedSessionId: string;
752
+ try {
753
+ resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
754
+ } catch (error) {
755
+ if (error instanceof SessionResolutionError && error.code === "no_session") return null;
756
+ throw error;
757
+ }
758
+ const resolvedCwd = path.resolve(cwd);
759
+ const cacheKey = visibleActiveStateCacheKey(resolvedCwd, resolvedSessionId);
760
+ const tier = opts?.tier ?? "security";
761
+ const now = Date.now();
762
+ const cached = visibleSkillActiveStateCache.get(cacheKey);
763
+ if (tier === "hud" && cached && now - cached.cachedAtMs < HUD_VISIBLE_STATE_TTL_MS) return cached.value;
764
+
765
+ const signature = await computeActiveStateSignature(resolvedCwd, resolvedSessionId);
766
+ if (
767
+ cached &&
768
+ signaturesEqual(cached.signature, signature) &&
769
+ (tier === "hud" || now - cached.cachedAtMs < SECURITY_VISIBLE_STATE_TTL_MS)
770
+ ) {
771
+ return cached.value;
772
+ }
773
+ const value = await readVisibleSkillActiveStateUncached(resolvedCwd, resolvedSessionId);
774
+ visibleSkillActiveStateCache.set(cacheKey, { signature, value, cachedAtMs: now });
775
+ return value;
776
+ }
777
+
642
778
  function activeStateWriterAudit(verb: string, sessionScope?: ActiveSessionScope | string) {
643
779
  const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
644
780
  return { category: "state" as const, verb, owner: "gjc-runtime" as const, ...(sessionId ? { sessionId } : {}) };
@@ -33,10 +33,18 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
33
33
  return DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE;
34
34
  }
35
35
 
36
- const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
36
+ const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
37
37
  const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
38
38
  const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
39
39
  const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
40
+ const BASH_MUTATION_COMMAND_RE =
41
+ /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
42
+ const BASH_IN_PLACE_MUTATION_COMMAND_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:sed|perl)\b([^;&|\n]*)/gi;
43
+ const BASH_OPAQUE_INTERPRETER_WRITE_RE =
44
+ /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:-c|-e)\b[^;&|\n]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
45
+ const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
46
+ /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
47
+ const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?dd\b([^;&|\n]*)/gi;
40
48
 
41
49
  type ToolWithEditMode = AgentTool & {
42
50
  mode?: unknown;
@@ -214,7 +222,7 @@ async function getActivePlanningSkill(
214
222
  ): Promise<ActivePlanningSkill | null> {
215
223
  const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
216
224
  if (!resolvedSessionId) return null;
217
- const skillState = await readVisibleSkillActiveState(cwd, resolvedSessionId);
225
+ const skillState = await readVisibleSkillActiveState(cwd, resolvedSessionId, { tier: "security" });
218
226
  if (!skillState) return null;
219
227
  const activeEntries = listActiveSkills(skillState).filter(entry =>
220
228
  entryMatchesContext(entry, resolvedSessionId, threadId),
@@ -329,11 +337,66 @@ function extractEditTargets(args: unknown, tool: ToolWithEditMode): ExtractedTar
329
337
  if (targets.paths.length === 0) targets.unknown = true;
330
338
  return targets;
331
339
  }
340
+ function shellWords(argsText: string): string[] {
341
+ return argsText.match(/(?:[^\s'"\\]+|'[^']*'|"[^"]*")+/g) ?? [];
342
+ }
343
+
344
+ function cleanShellWord(value: string): string {
345
+ return value.replace(/^['"]|['"]$/g, "");
346
+ }
332
347
 
348
+ function extractBashTargets(args: unknown): ExtractedTargets {
349
+ const record = getRecord(args);
350
+ const command = safeString(record?.command);
351
+ const targets: ExtractedTargets = { paths: [], unknown: false };
352
+ if (!command.trim()) {
353
+ targets.unknown = true;
354
+ return targets;
355
+ }
356
+ if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
357
+ targets.unknown = true;
358
+ }
359
+ for (const match of command.matchAll(BASH_DD_OUTPUT_RE)) {
360
+ const parts = shellWords(match[1] ?? "").map(cleanShellWord);
361
+ const output = parts.find(part => part.startsWith("of="));
362
+ if (output) addPath(targets, output.slice(3));
363
+ else targets.unknown = true;
364
+ }
365
+ for (const match of command.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
366
+ const parts = shellWords(match[1] ?? "").map(cleanShellWord);
367
+ const hasInPlaceFlag = parts.some(part => /^-.*i/.test(part));
368
+ if (!hasInPlaceFlag) continue;
369
+ const target = [...parts].reverse().find(part => part && !part.startsWith("-"));
370
+ if (target) addPath(targets, target);
371
+ else targets.unknown = true;
372
+ }
373
+ for (const match of command.matchAll(BASH_MUTATION_COMMAND_RE)) {
374
+ const redirected = match[2]?.trim();
375
+ if (redirected) {
376
+ addPath(targets, cleanShellWord(redirected));
377
+ continue;
378
+ }
379
+ const parts = shellWords(match[1] ?? "");
380
+ const commandName = match[0]
381
+ ?.match(/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i)?.[1]
382
+ ?.toLowerCase();
383
+ const targetParts =
384
+ commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "truncate"
385
+ ? parts.slice(-1)
386
+ : parts;
387
+ for (const part of targetParts) {
388
+ const cleaned = cleanShellWord(part);
389
+ if (!cleaned || cleaned.startsWith("-")) continue;
390
+ addPath(targets, cleaned);
391
+ }
392
+ }
393
+ return targets;
394
+ }
333
395
  function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
334
396
  if (tool.name === "write") return extractWriteTargets(args);
335
397
  if (tool.name === "ast_edit") return extractAstEditTargets(args);
336
398
  if (tool.name === "edit") return extractEditTargets(args, tool);
399
+ if (tool.name === "bash") return extractBashTargets(args);
337
400
  return { paths: [], unknown: true };
338
401
  }
339
402
 
@@ -490,6 +553,7 @@ async function isNeutralTempPath(cwd: string, rawPath: string): Promise<boolean>
490
553
  async function planningBlockedTargets(cwd: string, targets: ExtractedTargets): Promise<string[]> {
491
554
  const blocked: string[] = [];
492
555
  for (const rawPath of targets.paths) {
556
+ if (isAllowlistedPath(cwd, rawPath)) continue;
493
557
  if (!(await isNeutralTempPath(cwd, rawPath))) blocked.push(rawPath);
494
558
  }
495
559
  return blocked;
@@ -523,7 +587,7 @@ export async function getDeepInterviewMutationDecision(
523
587
  ): Promise<DeepInterviewMutationDecision> {
524
588
  if (!BLOCKED_TOOL_NAMES.has(input.tool.name)) return { blocked: false, targets: [] };
525
589
  const targets = extractTargets(input.tool, input.args);
526
- if (input.enforceWorkflowState !== false && hasBlockedGjcTarget(input.cwd, targets)) {
590
+ if (input.tool.name !== "bash" && input.enforceWorkflowState !== false && hasBlockedGjcTarget(input.cwd, targets)) {
527
591
  const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
528
592
  const command = stateSkill ? sanctionedWorkflowStateCommand(stateSkill) : "gjc <workflow-command>";
529
593
  return {