@gajae-code/coding-agent 0.11.7 → 0.11.8

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 (180) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/config/keybindings.d.ts +33 -3
  4. package/dist/types/config/model-profile-activation.d.ts +4 -3
  5. package/dist/types/config/model-profile-contract.d.ts +38 -0
  6. package/dist/types/config/settings-schema.d.ts +15 -2
  7. package/dist/types/edit/modes/patch.d.ts +13 -0
  8. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  9. package/dist/types/eval/py/executor.d.ts +1 -1
  10. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  11. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  12. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  13. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  14. package/dist/types/main.d.ts +8 -0
  15. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  16. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  19. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  20. package/dist/types/modes/interactive-mode.d.ts +5 -2
  21. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  22. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  24. package/dist/types/modes/types.d.ts +3 -0
  25. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  26. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  27. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  28. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  29. package/dist/types/sdk/broker/broker.d.ts +2 -0
  30. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  31. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  32. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  33. package/dist/types/sdk/bus/config.d.ts +2 -0
  34. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  35. package/dist/types/sdk/bus/index.d.ts +25 -0
  36. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  37. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  38. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
  39. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  42. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  43. package/dist/types/sdk/host/host.d.ts +1 -1
  44. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  45. package/dist/types/sdk/index.d.ts +2 -0
  46. package/dist/types/sdk/prompt-status.d.ts +88 -0
  47. package/dist/types/sdk/session-directory.d.ts +1 -1
  48. package/dist/types/sdk/startup-capability.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +2 -0
  50. package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
  51. package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
  52. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  53. package/dist/types/session/session-storage.d.ts +18 -0
  54. package/dist/types/task/index.d.ts +3 -1
  55. package/dist/types/task/receipt.d.ts +2 -0
  56. package/dist/types/task/types.d.ts +94 -0
  57. package/dist/types/tools/ask.d.ts +454 -174
  58. package/dist/types/tools/image-gen.d.ts +47 -1
  59. package/dist/types/tools/index.d.ts +4 -0
  60. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  61. package/package.json +8 -8
  62. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  63. package/scripts/dogfood-repository-binding.ts +209 -0
  64. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  65. package/scripts/run-test-manifest.ts +4 -4
  66. package/src/cli/args.ts +2 -92
  67. package/src/cli/notify-cli.ts +5 -38
  68. package/src/cli.ts +18 -0
  69. package/src/commands/sdk.ts +2 -2
  70. package/src/config/keybindings.ts +157 -24
  71. package/src/config/model-profile-activation.ts +7 -18
  72. package/src/config/model-profile-contract.ts +161 -0
  73. package/src/config/model-registry.ts +11 -1
  74. package/src/config/settings-schema.ts +17 -2
  75. package/src/coordinator-mcp/model-preset.ts +21 -74
  76. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  77. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  78. package/src/edit/index.ts +3 -6
  79. package/src/edit/modes/patch.ts +38 -1
  80. package/src/edit/modes/replace.ts +42 -0
  81. package/src/edit/path-mutation-lock.ts +112 -0
  82. package/src/eval/py/executor.ts +30 -11
  83. package/src/eval/py/prelude.py +2 -2
  84. package/src/eval/py/tool-bridge.ts +34 -18
  85. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  86. package/src/gjc-runtime/launch-tmux.ts +1 -1
  87. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  88. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  89. package/src/gjc-runtime/repository-binding.ts +267 -0
  90. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  91. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  92. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  93. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  94. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  95. package/src/internal-urls/docs-index.generated.ts +6 -5
  96. package/src/internal-urls/local-protocol.ts +33 -16
  97. package/src/internal-urls/mcp-protocol.ts +73 -11
  98. package/src/main.ts +25 -10
  99. package/src/modes/DESIGN.md +5 -0
  100. package/src/modes/acp/acp-agent.ts +50 -16
  101. package/src/modes/components/model-selector.ts +42 -8
  102. package/src/modes/components/plan-preview-overlay.ts +1 -1
  103. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  104. package/src/modes/components/tool-status-header.ts +20 -4
  105. package/src/modes/components/welcome.ts +22 -18
  106. package/src/modes/controllers/command-controller.ts +97 -54
  107. package/src/modes/controllers/event-controller.ts +10 -0
  108. package/src/modes/controllers/input-controller.ts +7 -2
  109. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  110. package/src/modes/controllers/selector-controller.ts +46 -54
  111. package/src/modes/interactive-mode.ts +41 -27
  112. package/src/modes/prompt-action-autocomplete.ts +32 -13
  113. package/src/modes/prompt-suggestion-controller.ts +96 -0
  114. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  115. package/src/modes/types.ts +3 -0
  116. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  117. package/src/modes/utils/ui-helpers.ts +5 -3
  118. package/src/prompts/agents/architect.md +0 -1
  119. package/src/prompts/agents/critic.md +0 -1
  120. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  121. package/src/prompts/system/system-prompt.md +6 -4
  122. package/src/prompts/tools/cron.md +3 -1
  123. package/src/runtime-mcp/manager.ts +51 -53
  124. package/src/runtime-mcp/oauth-flow.ts +0 -72
  125. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  126. package/src/runtime-mcp/transports/http.ts +13 -6
  127. package/src/sdk/broker/broker.ts +34 -20
  128. package/src/sdk/broker/lifecycle.ts +185 -36
  129. package/src/sdk/bus/chat-daemon-control.ts +18 -5
  130. package/src/sdk/bus/config-commands.ts +2 -2
  131. package/src/sdk/bus/config.ts +9 -1
  132. package/src/sdk/bus/conversation-store.ts +40 -6
  133. package/src/sdk/bus/index.ts +240 -65
  134. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  135. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  136. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  137. package/src/sdk/bus/notification-orchestration.ts +12 -0
  138. package/src/sdk/bus/notification-service.ts +27 -2
  139. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  140. package/src/sdk/bus/recent-activity.ts +90 -26
  141. package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
  142. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  143. package/src/sdk/bus/telegram-daemon.ts +835 -110
  144. package/src/sdk/bus/telegram-reference.ts +20 -2
  145. package/src/sdk/cli/session-cli.ts +15 -2
  146. package/src/sdk/host/control/dispatch.ts +1 -1
  147. package/src/sdk/host/control/operations.ts +1 -1
  148. package/src/sdk/host/host.ts +2 -6
  149. package/src/sdk/host/query/handlers.ts +70 -5
  150. package/src/sdk/index.ts +12 -0
  151. package/src/sdk/prompt-status.ts +97 -0
  152. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  153. package/src/sdk/protocol/operation-registry.ts +18 -4
  154. package/src/sdk/session-directory.ts +5 -1
  155. package/src/sdk/session.ts +2 -0
  156. package/src/sdk/startup-capability.ts +9 -1
  157. package/src/session/agent-session.ts +52 -5
  158. package/src/session/blob-store.ts +7 -2
  159. package/src/session/internal/managed-session-scope.ts +854 -84
  160. package/src/session/internal/managed-session-storage.ts +251 -199
  161. package/src/session/internal/native-publish-outcome.ts +266 -0
  162. package/src/session/session-manager.ts +310 -327
  163. package/src/session/session-storage.ts +128 -11
  164. package/src/task/gjc-command.ts +2 -2
  165. package/src/task/index.ts +121 -15
  166. package/src/task/receipt.ts +3 -0
  167. package/src/task/types.ts +34 -3
  168. package/src/tools/ask.ts +198 -88
  169. package/src/tools/ast-edit.ts +2 -2
  170. package/src/tools/bash-pty-selection.ts +2 -2
  171. package/src/tools/browser/screenshot-format.ts +6 -1
  172. package/src/tools/cron.ts +1 -1
  173. package/src/tools/image-gen.ts +178 -6
  174. package/src/tools/index.ts +4 -0
  175. package/src/tools/render-utils.ts +7 -1
  176. package/src/utils/edit-mode.ts +2 -2
  177. package/src/utils/prompt-suggestion.ts +265 -0
  178. package/src/utils/sixel.ts +3 -3
  179. package/src/web/insane/url-guard.ts +3 -1
  180. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -1,60 +1,88 @@
1
+ import type { Keybinding } from "@gajae-code/tui";
1
2
  import type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
2
3
 
3
4
  export interface HotkeysMarkdownBindings {
4
- keybindings: Pick<KeybindingsManager, "getDisplayString">;
5
+ keybindings: Pick<KeybindingsManager, "formatAccessibleKeyHint" | "getAccessibleDisplayString">;
6
+ }
7
+
8
+ function key(bindings: HotkeysMarkdownBindings, action: Keybinding): string {
9
+ return bindings.keybindings.getAccessibleDisplayString(action) || "Disabled";
5
10
  }
6
11
 
7
12
  function appKey(bindings: HotkeysMarkdownBindings, action: AppKeybinding): string {
8
- return bindings.keybindings.getDisplayString(action) || "Disabled";
13
+ return key(bindings, action);
14
+ }
15
+
16
+ export function formatHotkeyMarkdownCode(label: string, escapeTablePipes = true): string {
17
+ const escaped = escapeTablePipes ? label.replace(/\|/g, "\\|") : label;
18
+ let longestBacktickRun = 0;
19
+ for (const match of escaped.matchAll(/`+/g)) {
20
+ longestBacktickRun = Math.max(longestBacktickRun, match[0].length);
21
+ }
22
+ const fence = "`".repeat(longestBacktickRun + 1);
23
+ const padding = escaped.startsWith("`") || escaped.endsWith("`") ? " " : "";
24
+ return `${fence}${padding}${escaped}${padding}${fence}`;
9
25
  }
10
26
 
11
27
  export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
28
+ const displayKey = (action: Keybinding): string => formatHotkeyMarkdownCode(key(bindings, action));
29
+ const displayAppKey = (action: AppKeybinding): string => formatHotkeyMarkdownCode(appKey(bindings, action));
30
+ const displayKeys = (...labels: string[]): string => formatHotkeyMarkdownCode(labels.join("/"));
31
+ const formatFixedKey = (key: string): string => bindings.keybindings.formatAccessibleKeyHint(key);
32
+ const displayKeyWithFixed = (action: Keybinding, fixedKey: string): string => {
33
+ const configured = key(bindings, action);
34
+ const fixed = formatFixedKey(fixedKey);
35
+ const labels = configured.split("/");
36
+ if (!labels.includes(fixed)) labels.push(fixed);
37
+ return displayKeys(...labels);
38
+ };
39
+
12
40
  return [
13
41
  "**Navigation**",
14
42
  "| Key | Action |",
15
43
  "|-----|--------|",
16
- "| `Arrow keys` | Move cursor / browse history (Up when empty) |",
17
- "| `Option+Left/Right` | Move by word |",
18
- "| `Ctrl+A` / `Home` / `Cmd+Left` | Start of line |",
19
- "| `Ctrl+E` / `End` / `Cmd+Right` | End of line |",
44
+ `| ${displayKeys(key(bindings, "tui.editor.cursorUp"), key(bindings, "tui.editor.cursorDown"))} | Move cursor / browse history (Up when empty) |`,
45
+ `| ${displayKeys(key(bindings, "tui.editor.cursorWordLeft"), key(bindings, "tui.editor.cursorWordRight"))} | Move by word |`,
46
+ `| ${displayKeyWithFixed("tui.editor.cursorLineStart", "ctrl+a")} | Start of line |`,
47
+ `| ${displayKeyWithFixed("tui.editor.cursorLineEnd", "ctrl+e")} | End of line |`,
20
48
  "",
21
49
  "**Editing**",
22
50
  "| Key | Action |",
23
51
  "|-----|--------|",
24
- "| `Enter` | Send / queue while busy |",
25
- `| \`${appKey(bindings, "app.message.queue")}\` | Queue message for next turn |`,
26
- `| \`${appKey(bindings, "app.message.dequeue")}\` | Select queued message to edit |`,
27
- "| `Shift+Enter` / `Ctrl+J` | New line |",
28
- "| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
29
- "| `Ctrl+U` | Delete to start of line |",
30
- "| `Ctrl+K` | Delete to end of line |",
31
- `| \`${appKey(bindings, "app.clipboard.copyLine")}\` | Copy current line |`,
32
- `| \`${appKey(bindings, "app.clipboard.copyPrompt")}\` | Copy whole prompt |`,
52
+ `| ${displayKey("tui.input.submit")} | Send / queue while busy |`,
53
+ `| ${displayAppKey("app.message.queue")} | Queue message for next turn |`,
54
+ `| ${displayAppKey("app.message.dequeue")} | Select queued message to edit |`,
55
+ `| ${displayKeys(key(bindings, "tui.input.newLine"), formatFixedKey("ctrl+j"))} | New line |`,
56
+ `| ${displayKeys(formatFixedKey("ctrl+w"), formatFixedKey("alt+backspace"))} | Delete word backwards |`,
57
+ `| ${displayKeys(formatFixedKey("ctrl+u"))} | Delete to start of line |`,
58
+ `| ${displayKeys(formatFixedKey("ctrl+k"))} | Delete to end of line |`,
59
+ `| ${displayAppKey("app.clipboard.copyLine")} | Copy current line |`,
60
+ `| ${displayAppKey("app.clipboard.copyPrompt")} | Copy whole prompt |`,
33
61
  "",
34
62
  "**Other**",
35
63
  "| Key | Action |",
36
64
  "|-----|--------|",
37
- "| `Tab` | Path completion / accept autocomplete |",
38
- `| \`${appKey(bindings, "app.interrupt")}\` | Cancel autocomplete / interrupt active work |`,
39
- `| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
40
- `| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
41
- `| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
42
- `| \`${appKey(bindings, "app.commandPalette.open")}\` | Open command palette |`,
43
- `| \`${appKey(bindings, "app.session.new")}\` | Start a fresh session |`,
44
- `| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
45
- `| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle configured model roles |`,
46
- `| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle configured model roles temporarily |`,
47
- `| \`${appKey(bindings, "app.model.selectTemporary")}\` | Select model (temporary) |`,
48
- `| \`${appKey(bindings, "app.model.select")}\` | Select default model |`,
49
- `| \`${appKey(bindings, "app.plan.toggle")}\` | Toggle plan mode |`,
50
- `| \`${appKey(bindings, "app.history.search")}\` | Search prompt history |`,
51
- `| \`${appKey(bindings, "app.tools.expand")}\` | Toggle tool output expansion |`,
52
- `| \`${appKey(bindings, "app.tool.backgroundFold")}\` twice | Fold supported foreground bash into a background job |`,
53
- `| \`${appKey(bindings, "app.thinking.toggle")}\` | Toggle thinking block visibility |`,
54
- `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
55
- `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
56
- `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
57
- `| \`${appKey(bindings, "app.irc.sidebar.toggle")}\` | Toggle IRC sidebar |`,
65
+ `| ${displayKey("tui.input.tab")} | Path completion / accept autocomplete |`,
66
+ `| ${displayAppKey("app.interrupt")} | Cancel autocomplete / interrupt active work |`,
67
+ `| ${displayAppKey("app.clear")} | Clear editor (first) / exit (second) |`,
68
+ `| ${displayAppKey("app.exit")} | Exit (when editor is empty) |`,
69
+ `| ${displayAppKey("app.suspend")} | Suspend to background |`,
70
+ `| ${displayAppKey("app.commandPalette.open")} | Open command palette |`,
71
+ `| ${displayAppKey("app.session.new")} | Start a fresh session |`,
72
+ `| ${displayAppKey("app.thinking.cycle")} | Cycle thinking level |`,
73
+ `| ${displayAppKey("app.model.cycleForward")} | Cycle configured model roles |`,
74
+ `| ${displayAppKey("app.model.cycleBackward")} | Cycle configured model roles temporarily |`,
75
+ `| ${displayAppKey("app.model.selectTemporary")} | Select model (temporary) |`,
76
+ `| ${displayAppKey("app.model.select")} | Select default model |`,
77
+ `| ${displayAppKey("app.plan.toggle")} | Toggle plan mode |`,
78
+ `| ${displayAppKey("app.history.search")} | Search prompt history |`,
79
+ `| ${displayAppKey("app.tools.expand")} | Toggle tool output expansion |`,
80
+ `| ${displayAppKey("app.tool.backgroundFold")} twice | Fold supported foreground bash into a background job |`,
81
+ `| ${displayAppKey("app.thinking.toggle")} | Toggle thinking block visibility |`,
82
+ `| ${displayAppKey("app.editor.external")} | Edit message in external editor |`,
83
+ `| ${displayAppKey("app.clipboard.pasteImage")} | Paste image from clipboard |`,
84
+ `| ${displayAppKey("app.stt.toggle")} | Toggle speech-to-text recording |`,
85
+ `| ${displayAppKey("app.irc.sidebar.toggle")} | Toggle IRC sidebar |`,
58
86
  "| `?` | Show help / shortcuts |",
59
87
  "| `#` | Prompt actions (command-palette style actions) |",
60
88
  "| `/` | Slash commands (try `/help` or `/new`) |",
@@ -916,9 +916,11 @@ export class UiHelpers {
916
916
  const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
917
917
  this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
918
918
  }
919
- const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue") || "Alt+Up/Alt+Down";
920
- const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
921
- this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
919
+ const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue");
920
+ if (dequeueKey) {
921
+ const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
922
+ this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
923
+ }
922
924
  }
923
925
  }
924
926
 
@@ -33,7 +33,6 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
33
33
  - Never approve CRITICAL or HIGH severity issues.
34
34
  - Do not skip spec compliance to jump to style nitpicks.
35
35
  - Be constructive: explain why an issue matters and how to fix it or strengthen the design.
36
- - Practical sufficiency beats endless logical completeness: treat preemptive logic for edge cases with no observed or evidenced trigger as over-engineering, but never downgrade genuine CRITICAL/HIGH architecture, extensibility, or robustness risks that have observability-based evidence.
37
36
  </constraints>
38
37
 
39
38
  <review_stages>
@@ -23,7 +23,6 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
23
23
  - Do not invent problems; report no issues found when the plan passes.
24
24
  - Escalate routing needs upward: planner for plan revision, the deep-interview skill for requirements gathering, architect for code analysis.
25
25
  - For consensus planning, reject shallow alternatives, driver contradictions, vague risks, weak verification, missing acceptance criteria, or under-specified areas needing expansion before execution.
26
- - Prefer practical sufficiency over endless logical completeness: do not demand preemptive logic for edge cases that are not actually observed or evidenced, yet still require extensibility and robustness when observability-based evidence shows the investment is worth the token and time.
27
26
  </constraints>
28
27
 
29
28
  <execution_loop>
@@ -0,0 +1,32 @@
1
+ [SUGGESTION MODE: Suggest what the user might naturally type next into the coding agent.]
2
+
3
+ FIRST: Look at the user's recent messages and original request.
4
+
5
+ Your job is to predict what THEY would type - not what you think they should do.
6
+
7
+ THE TEST: Would they think "I was just about to type that"?
8
+
9
+ EXAMPLES:
10
+ User asked "fix the bug and run tests", bug is fixed → "run the tests"
11
+ After code written → "try it out"
12
+ Agent offers options → suggest the one the user would likely pick, based on conversation
13
+ Agent asks to continue → "yes" or "go ahead"
14
+ Task complete, obvious follow-up → "commit this" or "push it"
15
+ After error or misunderstanding → silence (let them assess/correct)
16
+
17
+ Be specific: "run the tests" beats "continue".
18
+
19
+ NEVER SUGGEST:
20
+ - Evaluative ("looks good", "thanks")
21
+ - Questions ("what about…?")
22
+ - Agent-voice ("Let me…", "I'll…", "Here's…")
23
+ - New ideas they didn't ask about
24
+ - Multiple sentences
25
+
26
+ Stay silent if the next step isn't obvious from what the user said.
27
+
28
+ Stay silent if a suggestion could be unsafe or inappropriate — including any sensitive topic (security incidents, credentials, harm, private data). Even when the user is doing legitimate security or cybersecurity work, do not predict potentially unsafe actions.
29
+
30
+ Format: 2-12 words, match the user's style. Or nothing.
31
+
32
+ Reply with ONLY the suggestion, no quotes or explanation.
@@ -19,12 +19,15 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
19
19
  {{#unless subagent}}
20
20
  <gjc-runtime>
21
21
  <routing>
22
- - Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony.
22
+ - Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony. Small verification needs do not turn a clear request into a planning workflow.
23
+ - Ambiguous implementation asks with a missing target, scope, acceptance criteria, or safety boundary require clarification or the appropriate planning workflow before mutation.
23
24
  - Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
24
- - Vague requirements use `/skill:deep-interview`; clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
25
+ - Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
26
+ - Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
25
27
  - Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
26
28
  - Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
27
- - Active skills are authoritative: read and follow them; planning and read-only skills do not mutate before approval.
29
+ - Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
30
+ - Before explicit execution approval, planning and interview workflows NEVER edit product source, run mutating shell commands, commit, push, open PRs, or delegate implementation.
28
31
  </routing>
29
32
  </gjc-runtime>
30
33
  {{/unless}}
@@ -206,7 +209,6 @@ Guardrails? None. Restrictions? None. Freedom? Unlimited.
206
209
  - **Do your homework first** - Read the files, search, and understand the context before asking
207
210
  - **Don’t Stop at Analysis** - Once you’ve assessed the situation, take the next step (session, comment, merge, fix, cleanup) on your own
208
211
  - **No Inflating Operational/Meta Issues** - Just because the backlog is empty doesn’t mean you should force operational follow-ups, verifiability checks, or meta issues. Only raise issues for actual repository problems; otherwise, stop.
209
- - **Practical sufficiency over completeness** - Don’t add logic that preemptively handles edge cases you haven’t actually observed; endless logical completeness loses to practical sufficiency. Extensibility and robustness matter, but they must be observability-based and evidence-based—any engineering without underlying evidence that it’s worth the token and time is over-engineering.
210
212
  - **Always maintain execution** - Do not end your turn with just reporting, summarizing, or analyzing. If an action is possible, execute it immediately during the same turn.
211
213
  - **Always be in action mode** - The default stance is intervention, not observation. If you see something that needs cleaning up, clean it up; if you see a session that needs opening, open it; if you see a PR that needs blocking, block it; if you see a PR that needs merging, review it and merge it.
212
214
  - **No begging for permission** - For obvious follow-up actions (cleaning up sessions, marking items as stale, fixing routing, updating documentation to reflect Doctrine), don’t ask—just do it.
@@ -1,4 +1,6 @@
1
- Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks let you re-run a prompt automatically on an interval — poll a deployment, babysit a PR, check back on a long-running build, or remind yourself to do something later in the session.
1
+ Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks re-run an agent prompt automatically on an interval when every firing is allowed to produce a normal assistant response, such as a reminder or a scheduled status report.
2
+
3
+ Cron is not a silent polling primitive. Every firing starts an agent turn, and hiding the injected cron message does not hide that turn's assistant response. For ongoing logs, file watching, or PR/CI polling that should report only meaningful state changes, use `monitor` with a script that writes a line only when there is an event to process, and set `persistent: true` so the monitor survives the first emitted event. Do not schedule a cron prompt that asks the agent to suppress routine polls; prompt wording cannot make a cron-triggered assistant turn reliably silent.
2
4
 
3
5
  Use a single `op` field to select the operation:
4
6
 
@@ -6,12 +6,12 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import * as url from "node:url";
9
- import type { TSchema } from "@gajae-code/ai";
9
+ import { isCanonicalMCPOAuthBinding, resolveMCPOAuthResourceOrigin, type TSchema } from "@gajae-code/ai";
10
10
  import { logger } from "@gajae-code/utils";
11
11
  import type { SourceMeta } from "../capability/types";
12
12
  import * as configValue from "../config/resolve-config-value";
13
13
  import type { CustomTool } from "../extensibility/custom-tools/types";
14
- import type { AuthStorage } from "../session/auth-storage";
14
+ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
15
15
  import {
16
16
  connectToServer,
17
17
  disconnectServer,
@@ -27,7 +27,6 @@ import {
27
27
  unsubscribeFromResources,
28
28
  } from "./client";
29
29
  import { loadAllMCPConfigs, validateServerConfig } from "./config";
30
- import { refreshMCPOAuthToken } from "./oauth-flow";
31
30
  import type { MCPToolDetails } from "./tool-bridge";
32
31
  import { DeferredMCPTool, MCPTool } from "./tool-bridge";
33
32
  import type { MCPToolCache } from "./tool-cache";
@@ -1469,64 +1468,63 @@ export class MCPManager {
1469
1468
  let resolved: MCPServerConfig = { ...config };
1470
1469
 
1471
1470
  const auth = config.auth;
1472
- if (auth?.type === "oauth" && auth.credentialId && this.#authStorage) {
1471
+ if (auth?.type === "oauth") {
1472
+ if (!auth.credentialId || !this.#authStorage || (config.type !== "http" && config.type !== "sse")) {
1473
+ throw new MCPExpectedFailure();
1474
+ }
1473
1475
  const credentialId = auth.credentialId;
1474
1476
  let credential = this.#authStorage.get(credentialId);
1475
- if (credential?.type === "oauth") {
1476
- // Proactive refresh: 5-minute buffer before expiry
1477
- // Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
1478
- const REFRESH_BUFFER_MS = 5 * 60_000;
1479
- const shouldRefresh =
1480
- forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1481
- let refreshedCredential:
1482
- | {
1483
- type: "oauth";
1484
- access: string;
1485
- refresh: string;
1486
- expires: number;
1487
- }
1488
- | undefined;
1489
- if (shouldRefresh && credential.refresh && auth.tokenUrl) {
1490
- try {
1491
- const refreshed = await refreshMCPOAuthToken(
1492
- auth.tokenUrl,
1493
- credential.refresh,
1494
- auth.clientId,
1495
- auth.clientSecret,
1496
- );
1497
- refreshedCredential = { type: "oauth", ...refreshed };
1498
- } catch (error) {
1499
- if (!(error instanceof MCPExpectedFailure)) throw error;
1500
- if (this.#toolsOnly) {
1501
- logger.debug("MCP OAuth refresh failed");
1502
- } else {
1503
- logger.warn("MCP OAuth refresh failed, using existing token");
1504
- }
1477
+ const binding = credential?.type === "oauth" ? credential.mcpBinding : undefined;
1478
+ const endpointOrigin = resolveMCPOAuthResourceOrigin(config.url);
1479
+ if (
1480
+ credential?.type !== "oauth" ||
1481
+ !binding ||
1482
+ !isCanonicalMCPOAuthBinding(binding) ||
1483
+ binding.resourceOrigin !== endpointOrigin ||
1484
+ (auth.tokenUrl !== undefined && auth.tokenUrl !== binding.tokenEndpoint)
1485
+ ) {
1486
+ throw new MCPExpectedFailure();
1487
+ }
1488
+
1489
+ // Proactive refresh: 5-minute buffer before expiry
1490
+ // Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
1491
+ const REFRESH_BUFFER_MS = 5 * 60_000;
1492
+ const shouldRefresh =
1493
+ forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1494
+ if (shouldRefresh && credential.refresh) {
1495
+ let refreshedCredential: OAuthCredential | undefined;
1496
+ try {
1497
+ refreshedCredential = await this.#authStorage.forceRefreshOAuthCredential(credentialId, credential, {
1498
+ clientId: auth.clientId,
1499
+ clientSecret: auth.clientSecret,
1500
+ });
1501
+ } catch {
1502
+ if (forceRefresh) throw new MCPExpectedFailure();
1503
+ if (this.#toolsOnly) {
1504
+ logger.debug("MCP OAuth refresh failed");
1505
+ } else {
1506
+ logger.warn("MCP OAuth refresh failed, using existing token");
1505
1507
  }
1506
1508
  }
1507
1509
  if (refreshedCredential) {
1508
- await this.#authStorage.set(credentialId, refreshedCredential);
1510
+ const refreshedBinding = refreshedCredential.mcpBinding;
1511
+ if (
1512
+ !refreshedBinding ||
1513
+ refreshedBinding.resourceOrigin !== binding.resourceOrigin ||
1514
+ refreshedBinding.tokenEndpoint !== binding.tokenEndpoint
1515
+ ) {
1516
+ throw new MCPExpectedFailure();
1517
+ }
1509
1518
  credential = refreshedCredential;
1510
1519
  }
1511
-
1512
- if (resolved.type === "http" || resolved.type === "sse") {
1513
- resolved = {
1514
- ...resolved,
1515
- headers: {
1516
- ...resolved.headers,
1517
- Authorization: `Bearer ${credential.access}`,
1518
- },
1519
- };
1520
- } else {
1521
- resolved = {
1522
- ...resolved,
1523
- env: {
1524
- ...resolved.env,
1525
- OAUTH_ACCESS_TOKEN: credential.access,
1526
- },
1527
- };
1528
- }
1529
1520
  }
1521
+ resolved = {
1522
+ ...config,
1523
+ headers: {
1524
+ ...config.headers,
1525
+ Authorization: `Bearer ${credential.access}`,
1526
+ },
1527
+ };
1530
1528
  }
1531
1529
  if (this.#toolsOnly && resolved.type === "stdio") {
1532
1530
  resolved = { ...resolved, noInheritEnv: true };
@@ -8,29 +8,10 @@
8
8
  import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callback-server";
9
9
  import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
10
10
  import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
11
- import { MCPExpectedFailure } from "./types";
12
11
 
13
12
  const DEFAULT_PORT = 3000;
14
13
  const CALLBACK_PATH = "/callback";
15
14
  const CALLBACK_BIND_HOSTNAME = "127.0.0.1";
16
- type RefreshTokenResponse = {
17
- access_token: string;
18
- refresh_token?: string;
19
- expires_in?: number;
20
- };
21
-
22
- function isRefreshTokenResponse(value: unknown): value is RefreshTokenResponse {
23
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
24
- const payload = value as Record<string, unknown>;
25
- return (
26
- typeof payload.access_token === "string" &&
27
- payload.access_token.length > 0 &&
28
- (payload.refresh_token === undefined || typeof payload.refresh_token === "string") &&
29
- (payload.expires_in === undefined ||
30
- (typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in >= 0))
31
- );
32
- }
33
-
34
15
  function isLoopbackHostname(hostname: string): boolean {
35
16
  return hostname === "localhost" || hostname === "127.0.0.1";
36
17
  }
@@ -384,56 +365,3 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
384
365
  }
385
366
  }
386
367
  }
387
-
388
- /**
389
- * Refresh an MCP OAuth token using the standard refresh_token grant.
390
- * Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
391
- */
392
- export async function refreshMCPOAuthToken(
393
- tokenUrl: string,
394
- refreshToken: string,
395
- clientId?: string,
396
- clientSecret?: string,
397
- ): Promise<OAuthCredentials> {
398
- const params = new URLSearchParams({
399
- grant_type: "refresh_token",
400
- refresh_token: refreshToken,
401
- });
402
- if (clientId) params.set("client_id", clientId);
403
- if (clientSecret) params.set("client_secret", clientSecret);
404
-
405
- let response: Response;
406
- try {
407
- response = await fetch(tokenUrl, {
408
- method: "POST",
409
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
410
- body: params.toString(),
411
- });
412
- } catch (error) {
413
- throw new MCPExpectedFailure(error);
414
- }
415
-
416
- if (!response.ok) {
417
- let text: string;
418
- try {
419
- text = await response.text();
420
- } catch (error) {
421
- throw new MCPExpectedFailure(error);
422
- }
423
- throw new MCPExpectedFailure(new Error(`MCP OAuth refresh failed: ${response.status} ${text}`));
424
- }
425
-
426
- let data: unknown;
427
- try {
428
- data = await response.json();
429
- } catch {
430
- throw new MCPExpectedFailure();
431
- }
432
- if (!isRefreshTokenResponse(data)) throw new MCPExpectedFailure();
433
- const expiresIn = data.expires_in ?? 3600;
434
- return {
435
- access: data.access_token,
436
- refresh: data.refresh_token ?? refreshToken,
437
- expires: Date.now() + expiresIn * 1000,
438
- };
439
- }
@@ -0,0 +1,101 @@
1
+ import { type AddressResolver, guardedPublicFetch } from "../web/insane/url-guard";
2
+ import { cancelMCPStream } from "./content-limits";
3
+ import type { MCPHttpServerConfig, MCPSseServerConfig } from "./types";
4
+
5
+ const PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY: unique symbol = Symbol("plugin-mcp-public-network-boundary");
6
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
7
+ const MAX_REDIRECTS = 20;
8
+ const REQUEST_BODY_HEADERS = [
9
+ "content-encoding",
10
+ "content-language",
11
+ "content-length",
12
+ "content-location",
13
+ "content-type",
14
+ ] as const;
15
+
16
+ /** Mark an in-memory plugin-bundle remote MCP config for connection-bound public-network enforcement. */
17
+ export function bindPluginMcpToPublicNetwork<T extends MCPHttpServerConfig | MCPSseServerConfig>(config: T): T {
18
+ return Object.assign(config, { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]: true });
19
+ }
20
+
21
+ /** The marker is an internal symbol, so persisted user MCP configs cannot opt into or forge this path. */
22
+ export function isPluginMcpPublicNetworkBound(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
23
+ return (
24
+ (config as MCPHttpServerConfig & { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]?: boolean })[
25
+ PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY
26
+ ] === true
27
+ );
28
+ }
29
+
30
+ function blocked(reason: string): Error {
31
+ return new Error(`Plugin MCP network request blocked: ${reason}`);
32
+ }
33
+
34
+ function parseHttpsUrl(rawUrl: string): URL {
35
+ let url: URL;
36
+ try {
37
+ url = new URL(rawUrl);
38
+ } catch {
39
+ throw blocked("invalid URL");
40
+ }
41
+ if (url.protocol !== "https:") throw blocked("HTTPS is required");
42
+ return url;
43
+ }
44
+
45
+ function rewritesToGet(status: number, method: string): boolean {
46
+ return (
47
+ ((status === 301 || status === 302) && method === "POST") ||
48
+ (status === 303 && method !== "GET" && method !== "HEAD")
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Fetch a plugin-bundle MCP request through a DNS-pinned public address. Every
54
+ * redirect is handled manually and revalidated before the next connection.
55
+ */
56
+ export async function fetchPluginMcpRequest(
57
+ rawUrl: string,
58
+ init: BunFetchRequestInit,
59
+ options: { resolver?: AddressResolver; maxRedirects?: number } = {},
60
+ ): Promise<Response> {
61
+ let currentUrl = parseHttpsUrl(rawUrl);
62
+ let currentInit: BunFetchRequestInit = { ...init };
63
+ const maxRedirects = options.maxRedirects ?? MAX_REDIRECTS;
64
+
65
+ for (let redirectCount = 0; ; redirectCount++) {
66
+ const dial = await guardedPublicFetch(currentUrl.toString(), currentInit, { resolver: options.resolver });
67
+ if (!dial.ok) throw blocked(dial.reason);
68
+ const { response, logicalUrl } = dial;
69
+ if (!REDIRECT_STATUSES.has(response.status)) return response;
70
+
71
+ const location = response.headers.get("location");
72
+ if (!location) return response;
73
+ if (redirectCount >= maxRedirects) {
74
+ cancelMCPStream(response.body);
75
+ throw blocked("redirect limit exceeded");
76
+ }
77
+
78
+ let nextUrl: URL;
79
+ try {
80
+ nextUrl = parseHttpsUrl(new URL(location, logicalUrl).toString());
81
+ } catch (error) {
82
+ cancelMCPStream(response.body);
83
+ throw error;
84
+ }
85
+ if (logicalUrl.origin !== nextUrl.origin) {
86
+ cancelMCPStream(response.body);
87
+ throw blocked("cross-origin redirects are not allowed");
88
+ }
89
+
90
+ const headers = new Headers(currentInit.headers);
91
+ const method = (currentInit.method ?? "GET").toUpperCase();
92
+ if (rewritesToGet(response.status, method)) {
93
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
94
+ currentInit = { ...currentInit, method: "GET", headers, body: undefined };
95
+ } else {
96
+ currentInit = { ...currentInit, headers };
97
+ }
98
+ currentUrl = nextUrl;
99
+ cancelMCPStream(response.body);
100
+ }
101
+ }
@@ -24,6 +24,7 @@ import {
24
24
  MCP_MAX_SSE_REQUEST_MESSAGES,
25
25
  readMCPResponseText,
26
26
  } from "../content-limits";
27
+ import { fetchPluginMcpRequest, isPluginMcpPublicNetworkBound } from "../plugin-network-boundary";
27
28
 
28
29
  /**
29
30
  * HTTP transport for MCP servers.
@@ -62,6 +63,12 @@ export class HttpTransport implements MCPTransport {
62
63
  this.#connected = true;
63
64
  }
64
65
 
66
+ #fetch(init: BunFetchRequestInit): Promise<Response> {
67
+ return isPluginMcpPublicNetworkBound(this.config)
68
+ ? fetchPluginMcpRequest(this.config.url, init)
69
+ : fetch(this.config.url, init);
70
+ }
71
+
65
72
  #trackReader(promise: Promise<void>, controller?: AbortController): void {
66
73
  if (controller) this.#streamControllers.add(controller);
67
74
  this.#streamReaders.add(promise);
@@ -96,7 +103,7 @@ export class HttpTransport implements MCPTransport {
96
103
 
97
104
  let response: Response;
98
105
  try {
99
- response = await fetch(this.config.url, {
106
+ response = await this.#fetch({
100
107
  method: "GET",
101
108
  headers,
102
109
  signal: AbortSignal.any([sseConnection.signal, headerController.signal]),
@@ -227,7 +234,7 @@ export class HttpTransport implements MCPTransport {
227
234
  : abortController.signal;
228
235
 
229
236
  try {
230
- const response = await fetch(this.config.url, {
237
+ const response = await this.#fetch({
231
238
  method: "POST",
232
239
  headers,
233
240
  body: JSON.stringify(body),
@@ -423,7 +430,7 @@ export class HttpTransport implements MCPTransport {
423
430
  headers["Mcp-Session-Id"] = this.#sessionId;
424
431
  }
425
432
  try {
426
- const resp = await fetch(this.config.url, {
433
+ const resp = await this.#fetch({
427
434
  method: "POST",
428
435
  headers,
429
436
  body: JSON.stringify(body),
@@ -437,7 +444,7 @@ export class HttpTransport implements MCPTransport {
437
444
  this.config.headers ??= {};
438
445
  Object.assign(this.config.headers, newHeaders);
439
446
  Object.assign(headers, newHeaders);
440
- const retry = await fetch(this.config.url, {
447
+ const retry = await this.#fetch({
441
448
  method: "POST",
442
449
  headers,
443
450
  body: JSON.stringify(body),
@@ -480,7 +487,7 @@ export class HttpTransport implements MCPTransport {
480
487
  const timeoutId = setTimeout(() => abortController.abort(), timeout);
481
488
 
482
489
  try {
483
- const response = await fetch(this.config.url, {
490
+ const response = await this.#fetch({
484
491
  method: "POST",
485
492
  headers,
486
493
  body: JSON.stringify(body),
@@ -542,7 +549,7 @@ export class HttpTransport implements MCPTransport {
542
549
  "Mcp-Session-Id": this.#sessionId,
543
550
  };
544
551
 
545
- await fetch(this.config.url, {
552
+ await this.#fetch({
546
553
  method: "DELETE",
547
554
  headers,
548
555
  signal: AbortSignal.timeout(timeout),