@gajae-code/coding-agent 0.4.3 → 0.4.4

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 (45) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/types/async/job-manager.d.ts +19 -1
  3. package/dist/types/cli/setup-cli.d.ts +14 -1
  4. package/dist/types/commands/coordinator.d.ts +19 -0
  5. package/dist/types/commands/mcp-serve.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +41 -0
  7. package/dist/types/coordinator/contract.d.ts +4 -0
  8. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  9. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  10. package/dist/types/coordinator-mcp/server.d.ts +52 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  12. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  13. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  14. package/dist/types/setup/hermes-setup.d.ts +71 -0
  15. package/dist/types/task/render.d.ts +7 -1
  16. package/dist/types/tools/subagent-render.d.ts +25 -0
  17. package/dist/types/tools/subagent.d.ts +5 -1
  18. package/package.json +7 -7
  19. package/src/async/job-manager.ts +43 -1
  20. package/src/cli/setup-cli.ts +86 -2
  21. package/src/cli.ts +2 -0
  22. package/src/commands/coordinator.ts +70 -0
  23. package/src/commands/mcp-serve.ts +62 -0
  24. package/src/commands/setup.ts +30 -1
  25. package/src/coordinator/contract.ts +20 -0
  26. package/src/coordinator-mcp/policy.ts +160 -0
  27. package/src/coordinator-mcp/safety.ts +80 -0
  28. package/src/coordinator-mcp/server.ts +1316 -0
  29. package/src/extensibility/extensions/types.ts +13 -0
  30. package/src/gjc-runtime/session-state-sidecar.ts +79 -0
  31. package/src/internal-urls/docs-index.generated.ts +3 -2
  32. package/src/modes/components/hook-selector.ts +109 -5
  33. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  34. package/src/prompts/agents/architect.md +6 -0
  35. package/src/prompts/agents/critic.md +6 -0
  36. package/src/prompts/agents/planner.md +8 -1
  37. package/src/session/agent-session.ts +6 -0
  38. package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
  39. package/src/setup/hermes-setup.ts +429 -0
  40. package/src/task/index.ts +2 -0
  41. package/src/task/render.ts +14 -0
  42. package/src/tools/ask.ts +30 -10
  43. package/src/tools/renderers.ts +2 -0
  44. package/src/tools/subagent-render.ts +160 -0
  45. package/src/tools/subagent.ts +49 -7
@@ -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
  }
@@ -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();
@@ -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>
@@ -180,6 +180,7 @@ import type { HookCommandContext } from "../extensibility/hooks/types";
180
180
  import type { Skill, SkillWarning } from "../extensibility/skills";
181
181
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
182
182
  import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
183
+ import { persistCoordinatorRuntimeStateFromEvent } from "../gjc-runtime/session-state-sidecar";
183
184
  import { writeArtifact } from "../gjc-runtime/state-writer";
184
185
  import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
185
186
  import { GoalRuntime } from "../goals/runtime";
@@ -1626,6 +1627,11 @@ export class AgentSession {
1626
1627
  }
1627
1628
 
1628
1629
  async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
1630
+ await persistCoordinatorRuntimeStateFromEvent(event, {
1631
+ sessionId: this.sessionId,
1632
+ cwd: this.sessionManager.getCwd(),
1633
+ sessionFile: this.sessionManager.getSessionFile(),
1634
+ });
1629
1635
  if (event.type === "message_update") {
1630
1636
  this.#emit(event);
1631
1637
  void this.#queueExtensionEvent(event);
@@ -0,0 +1,29 @@
1
+ # GJC Hermes operator instructions v{{TEMPLATE_VERSION}}
2
+
3
+ Server key: {{SERVER_KEY}}
4
+
5
+ These instructions teach a Hermes-style coordinator how to operate GJC through the `{{TOOL_PREFIX}}_*` MCP tools. They are setup guidance, not a GJC workflow skill.
6
+
7
+ ## Core loop
8
+
9
+ 1. Use `{{TOOL_PREFIX}}_list_sessions` to find an existing session, or `{{TOOL_PREFIX}}_start_session` when a new session is required and mutation is enabled.
10
+ 2. Send exactly one bounded task prompt with `{{TOOL_PREFIX}}_send_prompt`.
11
+ 3. Store the returned `turn_id`.
12
+ 4. Poll `{{TOOL_PREFIX}}_read_turn` or `{{TOOL_PREFIX}}_await_turn` for that `turn_id` until the turn is terminal.
13
+ 5. If GJC asks a structured question, use `{{TOOL_PREFIX}}_list_questions` and answer with `{{TOOL_PREFIX}}_submit_question_answer`.
14
+ 6. Use `{{TOOL_PREFIX}}_report_status` for coordinator-visible status and final reports.
15
+ 7. Use `{{TOOL_PREFIX}}_read_tail` only as advisory debug output when structured turn state is insufficient.
16
+
17
+ Do not report completion to the user until the GJC turn is terminal. Do not infer completion from terminal scrollback alone.
18
+
19
+ ## Model and provider policy
20
+
21
+ The Hermes bridge does not choose a model/provider. When no session command is configured, GJC uses its normal local model/provider resolution. If the operator config supplies `GJC_COORDINATOR_MCP_SESSION_COMMAND`, preserve it as explicit user intent.
22
+
23
+ Provider-specific commands are examples only, never product defaults.
24
+
25
+ ## Safety
26
+
27
+ - Mutating tools require bridge startup mutation classes and per-call consent.
28
+ - Allowed roots restrict workdir and artifact paths.
29
+ - Artifact reads are bounded and should be treated as evidence, not unlimited filesystem access.