@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.
- package/CHANGELOG.md +2 -0
- package/dist/types/async/job-manager.d.ts +19 -1
- package/dist/types/cli/setup-cli.d.ts +14 -1
- package/dist/types/commands/coordinator.d.ts +19 -0
- package/dist/types/commands/mcp-serve.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +41 -0
- package/dist/types/coordinator/contract.d.ts +4 -0
- package/dist/types/coordinator-mcp/policy.d.ts +24 -0
- package/dist/types/coordinator-mcp/safety.d.ts +26 -0
- package/dist/types/coordinator-mcp/server.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +13 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/setup/hermes-setup.d.ts +71 -0
- package/dist/types/task/render.d.ts +7 -1
- package/dist/types/tools/subagent-render.d.ts +25 -0
- package/dist/types/tools/subagent.d.ts +5 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +43 -1
- package/src/cli/setup-cli.ts +86 -2
- package/src/cli.ts +2 -0
- package/src/commands/coordinator.ts +70 -0
- package/src/commands/mcp-serve.ts +62 -0
- package/src/commands/setup.ts +30 -1
- package/src/coordinator/contract.ts +20 -0
- package/src/coordinator-mcp/policy.ts +160 -0
- package/src/coordinator-mcp/safety.ts +80 -0
- package/src/coordinator-mcp/server.ts +1316 -0
- package/src/extensibility/extensions/types.ts +13 -0
- package/src/gjc-runtime/session-state-sidecar.ts +79 -0
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/modes/components/hook-selector.ts +109 -5
- package/src/modes/controllers/extension-ui-controller.ts +16 -1
- package/src/prompts/agents/architect.md +6 -0
- package/src/prompts/agents/critic.md +6 -0
- package/src/prompts/agents/planner.md +8 -1
- package/src/session/agent-session.ts +6 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
- package/src/setup/hermes-setup.ts +429 -0
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +14 -0
- package/src/tools/ask.ts +30 -10
- package/src/tools/renderers.ts +2 -0
- package/src/tools/subagent-render.ts +160 -0
- 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 {
|
|
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
|
-
|
|
369
|
-
this
|
|
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)
|
|
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
|
-
|
|
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.
|