@gajae-code/coding-agent 0.4.3 → 0.4.5

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 (92) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/async/job-manager.d.ts +19 -1
  3. package/dist/types/cli/fast-help.d.ts +1 -0
  4. package/dist/types/cli/setup-cli.d.ts +16 -1
  5. package/dist/types/commands/coordinator.d.ts +19 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/commands/mcp-serve.d.ts +24 -0
  8. package/dist/types/commands/setup.d.ts +47 -0
  9. package/dist/types/config/model-registry.d.ts +3 -0
  10. package/dist/types/config/models-config-schema.d.ts +5 -0
  11. package/dist/types/coordinator/contract.d.ts +4 -0
  12. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  13. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +58 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  17. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  21. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  22. package/dist/types/harness-control-plane/types.d.ts +9 -1
  23. package/dist/types/main.d.ts +2 -2
  24. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  25. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  26. package/dist/types/session/session-manager.d.ts +8 -0
  27. package/dist/types/setup/hermes-setup.d.ts +78 -0
  28. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  29. package/dist/types/task/receipt.d.ts +1 -0
  30. package/dist/types/task/render.d.ts +7 -1
  31. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  32. package/dist/types/task/types.d.ts +10 -0
  33. package/dist/types/tools/subagent-render.d.ts +25 -0
  34. package/dist/types/tools/subagent.d.ts +5 -1
  35. package/package.json +8 -7
  36. package/scripts/build-binary.ts +4 -0
  37. package/src/async/job-manager.ts +43 -1
  38. package/src/cli/fast-help.ts +80 -0
  39. package/src/cli/setup-cli.ts +95 -2
  40. package/src/cli.ts +109 -16
  41. package/src/commands/coordinator.ts +113 -0
  42. package/src/commands/harness.ts +92 -9
  43. package/src/commands/mcp-serve.ts +63 -0
  44. package/src/commands/setup.ts +34 -1
  45. package/src/config/models-config-schema.ts +1 -0
  46. package/src/coordinator/contract.ts +21 -0
  47. package/src/coordinator-mcp/policy.ts +160 -0
  48. package/src/coordinator-mcp/safety.ts +80 -0
  49. package/src/coordinator-mcp/server.ts +1519 -0
  50. package/src/cursor.ts +30 -2
  51. package/src/extensibility/extensions/types.ts +13 -0
  52. package/src/gjc-runtime/launch-worktree.ts +12 -1
  53. package/src/gjc-runtime/session-state-sidecar.ts +117 -0
  54. package/src/harness-control-plane/finalize.ts +39 -5
  55. package/src/harness-control-plane/owner.ts +9 -1
  56. package/src/harness-control-plane/phase-rollup.ts +96 -0
  57. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  58. package/src/harness-control-plane/receipts.ts +229 -1
  59. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  60. package/src/harness-control-plane/types.ts +29 -1
  61. package/src/internal-urls/docs-index.generated.ts +6 -4
  62. package/src/main.ts +7 -3
  63. package/src/modes/components/hook-selector.ts +109 -5
  64. package/src/modes/components/status-line.ts +6 -6
  65. package/src/modes/controllers/event-controller.ts +5 -4
  66. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  67. package/src/modes/interactive-mode.ts +4 -5
  68. package/src/modes/print-mode.ts +1 -1
  69. package/src/modes/theme/theme.ts +2 -2
  70. package/src/modes/utils/abort-message.ts +41 -0
  71. package/src/modes/utils/context-usage.ts +15 -8
  72. package/src/modes/utils/ui-helpers.ts +5 -6
  73. package/src/prompts/agents/architect.md +6 -0
  74. package/src/prompts/agents/critic.md +6 -0
  75. package/src/prompts/agents/planner.md +8 -1
  76. package/src/sdk.ts +9 -4
  77. package/src/session/agent-session.ts +22 -5
  78. package/src/session/session-manager.ts +20 -0
  79. package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
  80. package/src/setup/hermes-setup.ts +484 -0
  81. package/src/task/fork-context-advisory.ts +99 -0
  82. package/src/task/index.ts +33 -2
  83. package/src/task/receipt.ts +2 -0
  84. package/src/task/render.ts +14 -0
  85. package/src/task/roi-reconciliation.ts +90 -0
  86. package/src/task/types.ts +7 -0
  87. package/src/tools/ask.ts +30 -10
  88. package/src/tools/index.ts +2 -2
  89. package/src/tools/renderers.ts +2 -0
  90. package/src/tools/subagent-render.ts +169 -0
  91. package/src/tools/subagent.ts +49 -7
  92. package/src/utils/title-generator.ts +16 -2
package/src/main.ts CHANGED
@@ -33,7 +33,7 @@ import { getDefault, type SettingPath, Settings, settings } from "./config/setti
33
33
  import { initializeWithSettings } from "./discovery";
34
34
  import { exportFromFile } from "./export/html";
35
35
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
36
- import { InteractiveMode, runAcpMode, runBridgeMode, runPrintMode, runRpcMode } from "./modes";
36
+ import type { InteractiveMode } from "./modes/interactive-mode";
37
37
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
38
38
  import type { SubmittedUserInput } from "./modes/types";
39
39
  import type { MCPManager } from "./runtime-mcp";
@@ -304,6 +304,7 @@ async function runInteractiveMode(
304
304
  initialMessage?: string,
305
305
  initialImages?: ImageContent[],
306
306
  ): Promise<void> {
307
+ const { InteractiveMode } = await import("./modes/interactive-mode");
307
308
  const mode = new InteractiveMode(
308
309
  session,
309
310
  version,
@@ -706,7 +707,7 @@ async function buildSessionOptions(
706
707
  interface RunRootCommandDependencies {
707
708
  createAgentSession?: typeof createAgentSession;
708
709
  discoverAuthStorage?: typeof discoverAuthStorage;
709
- runAcpMode?: typeof runAcpMode;
710
+ runAcpMode?: (createSession: AcpSessionFactory) => Promise<void>;
710
711
  settings?: Settings;
711
712
  }
712
713
 
@@ -927,7 +928,7 @@ export async function runRootCommand(
927
928
  rawArgs,
928
929
  createSession,
929
930
  });
930
- await (deps.runAcpMode ?? runAcpMode)(createAcpSession);
931
+ await (deps.runAcpMode ?? (await import("./modes/acp")).runAcpMode)(createAcpSession);
931
932
  } else {
932
933
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } =
933
934
  await createSession(sessionOptions);
@@ -973,8 +974,10 @@ export async function runRootCommand(
973
974
  }
974
975
 
975
976
  if (mode === "rpc" || mode === "rpc-ui") {
977
+ const { runRpcMode } = await import("./modes/rpc/rpc-mode");
976
978
  await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
977
979
  } else if (mode === "bridge") {
980
+ const { runBridgeMode } = await import("./modes/bridge/bridge-mode");
978
981
  await runBridgeMode(session, setToolUIContext);
979
982
  } else if (isInteractive) {
980
983
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
@@ -1014,6 +1017,7 @@ export async function runRootCommand(
1014
1017
  initialImages,
1015
1018
  );
1016
1019
  } else {
1020
+ const { runPrintMode } = await import("./modes/print-mode");
1017
1021
  await runPrintMode(session, {
1018
1022
  mode,
1019
1023
  messages: parsedArgs.messages,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import {
6
6
  Container,
7
+ Editor,
7
8
  Markdown,
8
9
  matchesKey,
9
10
  padding,
@@ -16,8 +17,13 @@ import {
16
17
  visibleWidth,
17
18
  wrapTextWithAnsi,
18
19
  } from "@gajae-code/tui";
19
- import { getMarkdownTheme, theme } from "../../modes/theme/theme";
20
- import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
20
+ import { getEditorTheme, getMarkdownTheme, theme } from "../../modes/theme/theme";
21
+ import {
22
+ matchesAppExternalEditor,
23
+ matchesAppInterrupt,
24
+ matchesSelectCancel,
25
+ } from "../../modes/utils/keybinding-matchers";
26
+ import { getEditorCommand, openInEditor } from "../../utils/external-editor";
21
27
  import { CountdownTimer } from "./countdown-timer";
22
28
  import { DynamicBorder } from "./dynamic-border";
23
29
 
@@ -56,6 +62,17 @@ export interface HookSelectorOptions {
56
62
  */
57
63
  wrapFocused?: boolean;
58
64
  scrollTitleRows?: number;
65
+ /**
66
+ * Inline free-text entry for the option with this label (e.g. the ask
67
+ * tool's "Other (type your own)"). Selecting it keeps the title and option
68
+ * list on screen and opens a prompt-style editor below the list instead of
69
+ * replacing the whole selector. Enter submits via `onSubmit`; Escape
70
+ * returns to option selection.
71
+ */
72
+ customInput?: {
73
+ optionLabel: string;
74
+ onSubmit: (text: string) => void;
75
+ };
59
76
  }
60
77
 
61
78
  class OutlinedList extends Container {
@@ -297,6 +314,12 @@ export class HookSelectorComponent extends Container {
297
314
  #wrapFocused: boolean;
298
315
  #outline: boolean;
299
316
  #scrollTitleRows: number | undefined;
317
+ #customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
318
+ #inputArea: Container;
319
+ #inlineEditor: Editor | undefined;
320
+ #helpTextComponent: Text;
321
+ #baseHelpText: string;
322
+ #tui: TUI | undefined;
300
323
  constructor(
301
324
  title: string,
302
325
  options: string[],
@@ -317,6 +340,8 @@ export class HookSelectorComponent extends Container {
317
340
  this.#onExternalEditorCallback = opts?.onExternalEditor;
318
341
  this.#wrapFocused = opts?.wrapFocused === true;
319
342
  this.#outline = opts?.outline === true;
343
+ this.#customInput = opts?.customInput;
344
+ this.#tui = opts?.tui;
320
345
 
321
346
  this.addChild(new DynamicBorder());
322
347
  this.addChild(new Spacer(1));
@@ -364,9 +389,12 @@ export class HookSelectorComponent extends Container {
364
389
  this.#listContainer = new Container();
365
390
  this.addChild(this.#listContainer);
366
391
  }
392
+ this.#inputArea = new Container();
393
+ this.addChild(this.#inputArea);
367
394
  this.addChild(new Spacer(1));
368
- const controlsHint = opts?.helpText ?? "up/down navigate enter select esc cancel";
369
- this.addChild(new Text(theme.fg("dim", controlsHint), 1, 0));
395
+ this.#baseHelpText = opts?.helpText ?? "up/down navigate enter select esc cancel";
396
+ this.#helpTextComponent = new Text(theme.fg("dim", this.#baseHelpText), 1, 0);
397
+ this.addChild(this.#helpTextComponent);
370
398
  this.addChild(new Spacer(1));
371
399
  this.addChild(new DynamicBorder());
372
400
 
@@ -432,6 +460,10 @@ export class HookSelectorComponent extends Container {
432
460
  this.#scrollableTitle?.scrollBy(this.#scrollTitleRows);
433
461
  return;
434
462
  }
463
+ if (this.#inlineEditor) {
464
+ this.#handleInputModeKey(keyData, this.#inlineEditor);
465
+ return;
466
+ }
435
467
  if (matchesKey(keyData, "up") || keyData === "k") {
436
468
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
437
469
  this.#updateList();
@@ -440,7 +472,12 @@ export class HookSelectorComponent extends Container {
440
472
  this.#updateList();
441
473
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
442
474
  const selected = this.#options[this.#selectedIndex];
443
- if (selected) this.#onSelectCallback(selected);
475
+ if (!selected) return;
476
+ if (this.#customInput && selected === this.#customInput.optionLabel) {
477
+ this.#enterInputMode();
478
+ return;
479
+ }
480
+ this.#onSelectCallback(selected);
444
481
  } else if (matchesKey(keyData, "left")) {
445
482
  this.#onLeftCallback?.();
446
483
  } else if (matchesKey(keyData, "right")) {
@@ -452,6 +489,73 @@ export class HookSelectorComponent extends Container {
452
489
  }
453
490
  }
454
491
 
492
+ /** Keys while the inline custom-input editor is open below the option list. */
493
+ #handleInputModeKey(keyData: string, editor: Editor): void {
494
+ // Escape backs out to option selection instead of cancelling the dialog,
495
+ // so a stray Esc never throws away the question context.
496
+ if (matchesKey(keyData, "escape") || matchesAppInterrupt(keyData)) {
497
+ this.#exitInputMode();
498
+ return;
499
+ }
500
+ if (matchesAppExternalEditor(keyData)) {
501
+ void this.#openExternalEditor(editor);
502
+ return;
503
+ }
504
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
505
+ this.#customInput?.onSubmit(editor.getText());
506
+ return;
507
+ }
508
+ editor.handleInput(keyData);
509
+ }
510
+
511
+ #enterInputMode(): void {
512
+ if (this.#inlineEditor) return;
513
+ // Stop the auto-select countdown for good: the user is actively typing,
514
+ // matching the old behavior where the separate editor had no timeout.
515
+ if (this.#countdown) {
516
+ this.#countdown.dispose();
517
+ this.#countdown = undefined;
518
+ this.#titleComponent.setText(this.#baseTitle);
519
+ }
520
+ const editor = new Editor(getEditorTheme());
521
+ editor.setBorderVisible(false);
522
+ editor.setPromptGutter("> ");
523
+ editor.disableSubmit = true;
524
+ this.#inlineEditor = editor;
525
+ this.#inputArea.addChild(new Spacer(1));
526
+ this.#inputArea.addChild(editor);
527
+ const scrollHint = this.#scrollTitleRows === undefined ? "" : " wheel/PgUp/PgDn scroll question";
528
+ this.#helpTextComponent.setText(
529
+ theme.fg("dim", `enter submit esc back to options ctrl+g external editor${scrollHint}`),
530
+ );
531
+ this.invalidate();
532
+ }
533
+
534
+ #exitInputMode(): void {
535
+ if (!this.#inlineEditor) return;
536
+ this.#inlineEditor = undefined;
537
+ this.#inputArea.clear();
538
+ this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
539
+ this.invalidate();
540
+ }
541
+
542
+ async #openExternalEditor(editor: Editor): Promise<void> {
543
+ const editorCmd = getEditorCommand();
544
+ if (!editorCmd || !this.#tui) return;
545
+
546
+ const currentText = editor.getExpandedText();
547
+ try {
548
+ this.#tui.stop();
549
+ const result = await openInEditor(editorCmd, currentText);
550
+ if (result !== null) {
551
+ editor.setText(result);
552
+ }
553
+ } finally {
554
+ this.#tui.start();
555
+ this.#tui.requestRender(true);
556
+ }
557
+ }
558
+
455
559
  dispose(): void {
456
560
  this.#countdown?.dispose();
457
561
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { AgentMessage } from "@gajae-code/agent-core";
3
- import { estimateTokens } from "@gajae-code/agent-core/compaction";
3
+ import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
4
4
  import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
5
5
  import { formatCount, getProjectDir } from "@gajae-code/utils";
6
6
  import { $ } from "bun";
@@ -50,7 +50,7 @@ export interface StatusLineSettings {
50
50
 
51
51
  /**
52
52
  * Symbol-keyed sidecar tagged onto each `AgentMessage` to memoize its
53
- * `estimateTokens` result. Keyed by message identity (the object itself);
53
+ * `estimateMessageTokensHeuristic` result. Keyed by message identity (the object itself);
54
54
  * a cheap content fingerprint detects in-place mutations (post-hoc error
55
55
  * attachment, retry-truncated branch rebuild, etc.) and forces recompute.
56
56
  *
@@ -64,11 +64,11 @@ interface TaggedMessage {
64
64
  }
65
65
 
66
66
  /**
67
- * Cheap structural fingerprint mirroring `estimateTokens`'s content walk.
67
+ * Cheap structural fingerprint mirroring `estimateMessageTokensHeuristic`'s content walk.
68
68
  * O(blocks) — only reads string `.length` and primitives, never copies or
69
69
  * serializes content. Any in-place mutation that alters total tokenized
70
70
  * content also alters one of the byte-length sums or block counts captured
71
- * here, forcing the cached `estimateTokens` value to be recomputed.
71
+ * here, forcing the cached heuristic token value to be recomputed.
72
72
  */
73
73
  function messageFingerprint(msg: AgentMessage): string {
74
74
  const role = (msg as { role?: string }).role ?? "";
@@ -136,7 +136,7 @@ function tokensForMessage(msg: AgentMessage): number {
136
136
  const tagged = msg as TaggedMessage;
137
137
  const cached = tagged[kTokenCache];
138
138
  if (cached && cached.fingerprint === fp) return cached.tokens;
139
- const tokens = estimateTokens(msg);
139
+ const tokens = estimateMessageTokensHeuristic(msg);
140
140
  tagged[kTokenCache] = { fingerprint: fp, tokens };
141
141
  return tokens;
142
142
  }
@@ -560,7 +560,7 @@ export class StatusLineComponent implements Component {
560
560
  let messagesTokens = 0;
561
561
  const lastIdx = messages.length - 1;
562
562
  for (let i = 0; i < messages.length; i++) {
563
- messagesTokens += i === lastIdx ? estimateTokens(messages[i]) : tokensForMessage(messages[i]);
563
+ messagesTokens += i === lastIdx ? estimateMessageTokensHeuristic(messages[i]) : tokensForMessage(messages[i]);
564
564
  }
565
565
 
566
566
  const usedTokens = this.#nonMessageTokensCache + messagesTokens;
@@ -20,6 +20,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
20
20
  import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
21
21
  import type { ResolveToolDetails } from "../../tools/resolve";
22
22
  import { interruptHint } from "../shared";
23
+ import { buildAbortDisplayMessage } from "../utils/abort-message";
23
24
 
24
25
  type AgentSessionEventKind = AgentSessionEvent["type"];
25
26
 
@@ -419,10 +420,10 @@ export class EventController {
419
420
  // controller ran, so reaching this branch implies the abort was NOT a
420
421
  // silent internal transition.
421
422
  const retryAttempt = this.ctx.session.retryAttempt;
422
- errorMessage =
423
- retryAttempt > 0
424
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
425
- : "Operation aborted";
423
+ errorMessage = buildAbortDisplayMessage({
424
+ errorMessage: this.ctx.streamingMessage.errorMessage,
425
+ retryAttempt,
426
+ });
426
427
  this.ctx.streamingMessage.errorMessage = errorMessage;
427
428
  }
428
429
  if (silentlyAborted || ttsrSilenced) {
@@ -29,6 +29,7 @@ const HOOK_SELECTOR_MOUSE_REPORTING_ENABLE = "\x1b[?1006h\x1b[?1000h";
29
29
  const HOOK_SELECTOR_MOUSE_REPORTING_DISABLE = "\x1b[?1000l\x1b[?1006l";
30
30
  const HOOK_SELECTOR_CHROME_ROWS = 7;
31
31
  const HOOK_SELECTOR_OUTLINE_ROWS = 2;
32
+ const HOOK_SELECTOR_INLINE_INPUT_ROWS = 2;
32
33
 
33
34
  export class ExtensionUiController {
34
35
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
@@ -601,8 +602,11 @@ export class ExtensionUiController {
601
602
  const maxVisible =
602
603
  requestedTitleRows === undefined ? baseMaxVisible : Math.min(15, Math.max(3, scrollOptionRows + 1));
603
604
  const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
605
+ // Reserve rows for the inline custom-input editor so opening it doesn't
606
+ // push the scrollable title past the viewport into terminal scrollback.
607
+ const inlineInputRows = dialogOptions?.customInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
604
608
  const availableTitleRows =
605
- this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - HOOK_SELECTOR_CHROME_ROWS;
609
+ this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
606
610
  const scrollTitleRows =
607
611
  requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
608
612
  if (scrollTitleRows !== undefined) {
@@ -645,6 +649,17 @@ export class ExtensionUiController {
645
649
  wrapFocused: dialogOptions?.wrapFocused,
646
650
  scrollTitleRows,
647
651
  maxVisible,
652
+ customInput: dialogOptions?.customInput
653
+ ? {
654
+ optionLabel: dialogOptions.customInput.optionLabel,
655
+ onSubmit: text => {
656
+ const optionLabel = dialogOptions.customInput?.optionLabel;
657
+ this.hideHookSelector();
658
+ dialogOptions.customInput?.onSubmit(text);
659
+ finish(optionLabel);
660
+ },
661
+ }
662
+ : undefined,
648
663
  },
649
664
  );
650
665
  this.ctx.editorContainer.clear();
@@ -1057,7 +1057,7 @@ export class InteractiveMode implements InteractiveModeContext {
1057
1057
  return;
1058
1058
  }
1059
1059
  if (event.state?.enabled === true && !this.#goalModePreviousTools) {
1060
- this.#goalModePreviousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1060
+ this.#goalModePreviousTools = this.session.getActiveToolNames();
1061
1061
  }
1062
1062
  this.goalModeEnabled = event.state?.enabled === true;
1063
1063
  this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
@@ -1146,10 +1146,9 @@ export class InteractiveMode implements InteractiveModeContext {
1146
1146
  const restored = await this.session.goalRuntime.onThreadResumed();
1147
1147
  this.goalModeEnabled = restored?.enabled === true;
1148
1148
  this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
1149
- // sdk.ts excludes "goal" from the initial active tool set unconditionally.
1150
- // Re-add it now so the agent can call resume, complete, or drop on this goal.
1149
+ // Keep `goal` armed on resumed threads; it is part of the default active tool set.
1151
1150
  if (restored?.goal) {
1152
- const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1151
+ const previousTools = this.session.getActiveToolNames();
1153
1152
  this.#goalModePreviousTools = previousTools;
1154
1153
  await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
1155
1154
  }
@@ -1318,7 +1317,7 @@ export class InteractiveMode implements InteractiveModeContext {
1318
1317
  this.showWarning("Exit plan mode first.");
1319
1318
  return;
1320
1319
  }
1321
- const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1320
+ const previousTools = this.session.getActiveToolNames();
1322
1321
  const goalTools = [...new Set([...previousTools, "goal"])];
1323
1322
  this.#goalModePreviousTools = previousTools;
1324
1323
  this.goalModePaused = false;
@@ -72,7 +72,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
72
72
  // In text mode, output final response
73
73
  if (mode === "text") {
74
74
  const state = session.state;
75
- const lastMessage = state.messages[state.messages.length - 1];
75
+ const lastMessage = state.messages.findLast(message => message.role === "assistant");
76
76
 
77
77
  if (lastMessage?.role === "assistant") {
78
78
  const assistantMsg = lastMessage as AssistantMessage;
@@ -264,7 +264,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
264
264
  "icon.context": "◫",
265
265
  "icon.cost": "💲",
266
266
  "icon.time": "⏱",
267
- "icon.pi": "π",
267
+ "icon.pi": "🦞",
268
268
  "icon.agents": "👥",
269
269
  "icon.cache": "💾",
270
270
  "icon.input": "⤵",
@@ -686,7 +686,7 @@ const ASCII_SYMBOLS: SymbolMap = {
686
686
  "icon.context": "ctx:",
687
687
  "icon.cost": "$",
688
688
  "icon.time": "t:",
689
- "icon.pi": "pi",
689
+ "icon.pi": "GJC",
690
690
  "icon.agents": "AG",
691
691
  "icon.cache": "cache",
692
692
  "icon.input": "in:",
@@ -0,0 +1,41 @@
1
+ const STREAM_IDLE_TIMEOUT_PATTERN = /\bstream stalled while waiting for the next event\b/i;
2
+ const GENERIC_ABORT_PATTERN = /^Request was aborted\.?$/i;
3
+ const ABORT_DISPLAY_LABEL_PATTERN = /^(?:Operation aborted|Aborted after \d+ retry attempts?)(?::|$)/;
4
+
5
+ export function buildAbortDisplayMessage({
6
+ errorMessage,
7
+ retryAttempt,
8
+ }: {
9
+ errorMessage?: string;
10
+ retryAttempt: number;
11
+ }): string {
12
+ const existingDisplayMessage = normalizeExistingAbortDisplayMessage(errorMessage);
13
+ if (existingDisplayMessage) return existingDisplayMessage;
14
+
15
+ const baseMessage =
16
+ retryAttempt > 0
17
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
18
+ : "Operation aborted";
19
+ const cause = normalizeAbortCause(errorMessage);
20
+ if (!cause) return baseMessage;
21
+
22
+ return `${baseMessage}: ${cause}${streamIdleTimeoutHint(cause)}`;
23
+ }
24
+
25
+ function normalizeExistingAbortDisplayMessage(errorMessage: string | undefined): string {
26
+ const trimmed = errorMessage?.trim();
27
+ if (!trimmed || !ABORT_DISPLAY_LABEL_PATTERN.test(trimmed)) return "";
28
+ return trimmed;
29
+ }
30
+
31
+ function normalizeAbortCause(errorMessage: string | undefined): string {
32
+ const trimmed = errorMessage?.trim();
33
+ if (!trimmed || GENERIC_ABORT_PATTERN.test(trimmed)) return "";
34
+ return trimmed;
35
+ }
36
+
37
+ function streamIdleTimeoutHint(cause: string): string {
38
+ if (!STREAM_IDLE_TIMEOUT_PATTERN.test(cause)) return "";
39
+ const separator = /[.!?]$/.test(cause) ? " " : ". ";
40
+ return `${separator}Hint: set PI_STREAM_IDLE_TIMEOUT_MS=300000 for slow reasoning/proxy streams, or PI_STREAM_IDLE_TIMEOUT_MS=0 to disable the watchdog.`;
41
+ }
@@ -1,8 +1,12 @@
1
1
  import type { AgentMessage } from "@gajae-code/agent-core";
2
2
  import type { CompactionSettings } from "@gajae-code/agent-core/compaction";
3
- import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "@gajae-code/agent-core/compaction";
3
+ import {
4
+ effectiveReserveTokens,
5
+ estimateMessageTokensHeuristic,
6
+ estimateTextTokensHeuristic,
7
+ resolveThresholdTokens,
8
+ } from "@gajae-code/agent-core/compaction";
4
9
  import type { Model } from "@gajae-code/ai";
5
- import { countTokens } from "@gajae-code/natives";
6
10
  import { formatNumber } from "@gajae-code/utils";
7
11
  import type { Skill } from "../../extensibility/skills";
8
12
  import type { AgentSession } from "../../session/agent-session";
@@ -46,7 +50,7 @@ export function estimateSkillsTokens(skills: readonly Skill[]): number {
46
50
  // concatenated form, so encode each piece separately and sum.
47
51
  fragments.push(skill.name, skill.description);
48
52
  }
49
- return countTokens(fragments);
53
+ return estimateTextTokensHeuristic(fragments);
50
54
  }
51
55
 
52
56
  export function estimateToolSchemaTokens(
@@ -61,7 +65,7 @@ export function estimateToolSchemaTokens(
61
65
  // Schema may contain functions or cycles; ignore.
62
66
  }
63
67
  }
64
- return countTokens(fragments);
68
+ return estimateTextTokensHeuristic(fragments);
65
69
  }
66
70
 
67
71
  /**
@@ -100,8 +104,11 @@ function computeNonMessageBreakdown(session: AgentSession): {
100
104
  const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
101
105
  const systemPromptParts = session.systemPrompt ?? [];
102
106
  const rulesTokens = estimateRulesTokens(systemPromptParts);
103
- const systemContextTokens = countTokens(systemPromptParts.slice(1));
104
- const systemPromptTokens = Math.max(0, countTokens(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens);
107
+ const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
108
+ const systemPromptTokens = Math.max(
109
+ 0,
110
+ estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
111
+ );
105
112
  return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
106
113
  }
107
114
 
@@ -112,7 +119,7 @@ function estimateRulesTokens(systemPromptParts: readonly string[]): number {
112
119
  fragments.push(match[0]);
113
120
  }
114
121
  }
115
- return fragments.length === 0 ? 0 : countTokens(fragments);
122
+ return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
116
123
  }
117
124
 
118
125
  function splitLastUserTurn(messages: readonly AgentMessage[]): {
@@ -130,7 +137,7 @@ function splitLastUserTurn(messages: readonly AgentMessage[]): {
130
137
  let regularMessagesTokens = 0;
131
138
  let lastUserTurnTokens = 0;
132
139
  for (let i = 0; i < messages.length; i++) {
133
- const tokens = estimateTokens(messages[i]);
140
+ const tokens = estimateMessageTokensHeuristic(messages[i]);
134
141
  if (i === lastUserIndex) {
135
142
  lastUserTurnTokens = tokens;
136
143
  } else {
@@ -27,6 +27,7 @@ import {
27
27
  } from "../../session/messages";
28
28
  import type { SessionContext } from "../../session/session-manager";
29
29
  import { formatBytes, formatDuration } from "../../tools/render-utils";
30
+ import { buildAbortDisplayMessage } from "./abort-message";
30
31
 
31
32
  type TextBlock = { type: "text"; text: string };
32
33
  interface RenderInitialMessagesOptions {
@@ -319,12 +320,10 @@ export class UiHelpers {
319
320
  !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
320
321
  const errorMessage = hasErrorStop
321
322
  ? message.stopReason === "aborted"
322
- ? (() => {
323
- const retryAttempt = this.ctx.session.retryAttempt;
324
- return retryAttempt > 0
325
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
326
- : "Operation aborted";
327
- })()
323
+ ? buildAbortDisplayMessage({
324
+ errorMessage: message.errorMessage,
325
+ retryAttempt: this.ctx.session.retryAttempt,
326
+ })
328
327
  : message.errorMessage || "Error"
329
328
  : null;
330
329
 
@@ -83,4 +83,10 @@ Prioritized concrete actions.
83
83
 
84
84
  ## Trade-offs
85
85
  Table or bullets comparing viable options when relevant.
86
+
87
+ Persist this full review as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
88
+
89
+ gjc ralplan --write --stage architect --stage_n <N> --artifact "<full review markdown>" --json
90
+
91
+ Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (Architectural Status + Code Review Recommendation). Never paste the full review body back into your response — the caller reads the persisted artifact when it needs the full text.
86
92
  </output_contract>
@@ -56,4 +56,10 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
56
56
  - Risk/Verification Rigor
57
57
 
58
58
  If not OKAY, list concrete required fixes.
59
+
60
+ Persist this full evaluation as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
61
+
62
+ gjc ralplan --write --stage critic --stage_n <N> --artifact "<full evaluation markdown>" --json
63
+
64
+ Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (OKAY / ITERATE / REJECT). Never paste the full evaluation body back into your response — the caller reads the persisted artifact when it needs the full text.
59
65
  </output_contract>
@@ -18,6 +18,7 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
18
18
  <constraints>
19
19
  - Read-only: never write, edit, format, commit, push, or mutate files.
20
20
  - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the plan markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
21
+ - Persist durable plans only through `gjc ralplan --write`. Never write plan files to `/tmp`, the repository, or any other path, and never rely on a file the caller must read back. The CLI is your only persistence channel.
21
22
  - Inspect the repository before asking about code facts.
22
23
  - Ask only about priorities, tradeoffs, scope decisions, timelines, or preferences that repository inspection cannot resolve.
23
24
  - Right-size the step count to the task; do not default to a fixed number of steps.
@@ -42,7 +43,7 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
42
43
  </success_criteria>
43
44
 
44
45
  <output_contract>
45
- Return:
46
+ Build the full plan as a single markdown document containing:
46
47
  - Summary
47
48
  - In scope / out of scope
48
49
  - File-level changes
@@ -50,4 +51,10 @@ Return:
50
51
  - Acceptance criteria
51
52
  - Verification
52
53
  - Risks and mitigations
54
+
55
+ Persist that markdown as the durable artifact via the restricted bash CLI, passing the plan inline (never a file path, never `/tmp`):
56
+
57
+ gjc ralplan --write --stage planner --stage_n <N> --artifact "<full plan markdown>" --json
58
+
59
+ Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus a compact plan summary (<=10 lines). Never paste the full plan body back into your response — the caller reads the persisted artifact when it needs the full text.
53
60
  </output_contract>
package/src/sdk.ts CHANGED
@@ -1622,9 +1622,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1622
1622
  };
1623
1623
 
1624
1624
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
1625
- const requestedToolNames =
1626
- (options.toolNames ? [...new Set(options.toolNames.map(name => name.toLowerCase()))] : undefined) ??
1627
- toolNamesFromRegistry;
1625
+ const requestedToolNames = options.toolNames
1626
+ ? [
1627
+ ...new Set([
1628
+ ...options.toolNames.map(name => name.toLowerCase()),
1629
+ ...(settings.get("goal.enabled") ? ["goal"] : []),
1630
+ ]),
1631
+ ]
1632
+ : toolNamesFromRegistry;
1628
1633
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1629
1634
  const requestedToolNameSet = new Set(normalizedRequested);
1630
1635
  // Effective discovery mode only covers built-in tools; MCP tool discovery
@@ -1635,7 +1640,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1635
1640
  const defaultInactiveToolNames = new Set(
1636
1641
  registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
1637
1642
  );
1638
- const requestedActiveToolNames = normalizedRequested.filter(name => name !== "goal");
1643
+ const requestedActiveToolNames = normalizedRequested;
1639
1644
  const initialRequestedActiveToolNames = options.toolNames
1640
1645
  ? requestedActiveToolNames
1641
1646
  : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));