@gajae-code/coding-agent 0.12.11 → 0.12.13
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 +68 -0
- package/dist/types/cli/marketplace-hint.d.ts +28 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock-gc.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/models-config-schema.d.ts +99 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/acp/acp-agent.d.ts +24 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/html-format.d.ts +10 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
- package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +40 -6
- package/dist/types/tools/index.d.ts +15 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/marketplace-hint.ts +39 -0
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/plugin-cli.ts +10 -0
- package/src/cli/update-cli.ts +90 -23
- package/src/cli.ts +23 -0
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +65 -26
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +46 -30
- package/src/config/model-registry.ts +17 -5
- package/src/config/models-config-schema.ts +9 -4
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
- package/src/gjc-runtime/deep-interview-state.ts +27 -3
- package/src/gjc-runtime/gc-render.ts +8 -1
- package/src/gjc-runtime/gc-runtime.ts +15 -1
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/main.ts +214 -70
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/acp/acp-agent.ts +123 -7
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/model-selector.ts +52 -5
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/print-mode.ts +10 -0
- package/src/modes/utils/host-status.ts +38 -0
- package/src/prompts/system/project-prompt.md +19 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/html-format.ts +19 -3
- package/src/sdk/bus/index.ts +91 -12
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
- package/src/sdk/bus/telegram-daemon.ts +2007 -509
- package/src/sdk/bus/telegram-reference.ts +114 -15
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +144 -29
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -13
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +228 -7
- package/src/session/session-manager.ts +225 -60
- package/src/setup/provider-presets.json +3 -3
- package/src/task/index.ts +92 -44
- package/src/thinking-metadata.ts +2 -1
- package/src/tools/ask.ts +10 -0
- package/src/tools/index.ts +15 -0
- package/src/tools/todo-write.ts +4 -4
- package/src/utils/npm-registry.ts +660 -0
|
@@ -1721,9 +1721,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
1721
1721
|
: choice.role === null
|
|
1722
1722
|
? "temporary model"
|
|
1723
1723
|
: GJC_MODEL_ASSIGNMENT_TARGETS[choice.role].name;
|
|
1724
|
+
// Show the highlighted reasoning level — never the model id (that mislabel
|
|
1725
|
+
// made "Reasoning for Default: gpt-5.6-luna" look like a level).
|
|
1726
|
+
const selectedLevel = choice.levels[this.#selectedThinkingIndex];
|
|
1727
|
+
const selectedLevelLabel = selectedLevel ? getThinkingLevelMetadata(selectedLevel).label : "—";
|
|
1724
1728
|
this.#listContainer.addChild(new Spacer(1));
|
|
1725
1729
|
this.#listContainer.addChild(
|
|
1726
|
-
new Text(theme.fg("muted", ` Reasoning for ${targetLabel}: ${
|
|
1730
|
+
new Text(theme.fg("muted", ` Reasoning for ${targetLabel}: ${selectedLevelLabel}`), 0, 0),
|
|
1727
1731
|
);
|
|
1728
1732
|
this.#listContainer.addChild(new Spacer(1));
|
|
1729
1733
|
for (let i = 0; i < choice.levels.length; i++) {
|
|
@@ -2079,8 +2083,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
2079
2083
|
this.#updateList();
|
|
2080
2084
|
}
|
|
2081
2085
|
}
|
|
2082
|
-
#getInitialThinkingChoiceIndex(
|
|
2083
|
-
|
|
2086
|
+
#getInitialThinkingChoiceIndex(
|
|
2087
|
+
item: ModelItem | CanonicalModelItem,
|
|
2088
|
+
levels: ThinkingLevel[],
|
|
2089
|
+
role: GjcModelAssignmentTargetId | null = null,
|
|
2090
|
+
roles?: readonly GjcModelAssignmentTargetId[],
|
|
2091
|
+
): number {
|
|
2092
|
+
const preferred = this.#getPreferredThinkingLevel(item, role, roles);
|
|
2084
2093
|
if (preferred && preferred !== ThinkingLevel.Inherit) {
|
|
2085
2094
|
const index = levels.indexOf(preferred);
|
|
2086
2095
|
if (index !== -1) return index;
|
|
@@ -2088,13 +2097,51 @@ export class ModelSelectorComponent extends Container {
|
|
|
2088
2097
|
return 0;
|
|
2089
2098
|
}
|
|
2090
2099
|
|
|
2091
|
-
|
|
2100
|
+
/**
|
|
2101
|
+
* Prefer the role binding the user is editing when it already points at this
|
|
2102
|
+
* model so the reasoning menu opens on the level role badges already show
|
|
2103
|
+
* (e.g. EXECUTOR (xhigh) must not leave the cursor on "off").
|
|
2104
|
+
*/
|
|
2105
|
+
#getPreferredThinkingLevel(
|
|
2106
|
+
item: ModelItem | CanonicalModelItem,
|
|
2107
|
+
role: GjcModelAssignmentTargetId | null = null,
|
|
2108
|
+
roles?: readonly GjcModelAssignmentTargetId[],
|
|
2109
|
+
): ThinkingLevel | undefined {
|
|
2110
|
+
const roleThinking = this.#getAssignedThinkingLevelForModel(item.model, role, roles);
|
|
2111
|
+
if (roleThinking && roleThinking !== ThinkingLevel.Inherit) {
|
|
2112
|
+
return roleThinking;
|
|
2113
|
+
}
|
|
2092
2114
|
if (item.thinkingLevel && item.thinkingLevel !== ThinkingLevel.Inherit) {
|
|
2093
2115
|
return item.thinkingLevel;
|
|
2094
2116
|
}
|
|
2095
2117
|
return undefined;
|
|
2096
2118
|
}
|
|
2097
2119
|
|
|
2120
|
+
#getAssignedThinkingLevelForModel(
|
|
2121
|
+
model: Model,
|
|
2122
|
+
role: GjcModelAssignmentTargetId | null,
|
|
2123
|
+
roles?: readonly GjcModelAssignmentTargetId[],
|
|
2124
|
+
): ThinkingLevel | undefined {
|
|
2125
|
+
if (roles && roles.length > 0) {
|
|
2126
|
+
const assignedLevels = roles
|
|
2127
|
+
.map(targetRole => this.#roles[targetRole])
|
|
2128
|
+
.filter(
|
|
2129
|
+
(assigned): assigned is RoleAssignment =>
|
|
2130
|
+
assigned !== undefined &&
|
|
2131
|
+
modelsAreEqual(assigned.model, model) &&
|
|
2132
|
+
assigned.thinkingLevel !== ThinkingLevel.Inherit,
|
|
2133
|
+
)
|
|
2134
|
+
.map(assigned => assigned.thinkingLevel);
|
|
2135
|
+
if (assignedLevels.length === 0) return undefined;
|
|
2136
|
+
const first = assignedLevels[0];
|
|
2137
|
+
return assignedLevels.every(level => level === first) ? first : undefined;
|
|
2138
|
+
}
|
|
2139
|
+
if (role === null) return undefined;
|
|
2140
|
+
const assigned = this.#roles[role];
|
|
2141
|
+
if (!assigned || !modelsAreEqual(assigned.model, model)) return undefined;
|
|
2142
|
+
return assigned.thinkingLevel;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2098
2145
|
#handleSelect(
|
|
2099
2146
|
item: ModelItem | CanonicalModelItem,
|
|
2100
2147
|
role: GjcModelAssignmentTargetId | null,
|
|
@@ -2109,7 +2156,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
2109
2156
|
if (!hasExplicitThinkingChoice && needsExplicitThinkingChoice) {
|
|
2110
2157
|
const levels = getSelectableThinkingLevels(item.model);
|
|
2111
2158
|
this.#pendingThinkingChoice = { item, role, roles, levels };
|
|
2112
|
-
this.#selectedThinkingIndex = this.#getInitialThinkingChoiceIndex(item, levels);
|
|
2159
|
+
this.#selectedThinkingIndex = this.#getInitialThinkingChoiceIndex(item, levels, role, roles);
|
|
2113
2160
|
this.#updateList();
|
|
2114
2161
|
return;
|
|
2115
2162
|
}
|
|
@@ -197,7 +197,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
197
197
|
|
|
198
198
|
constructor(
|
|
199
199
|
sessions: SessionInfo[],
|
|
200
|
-
private readonly onSelect: (sessionPath: string) => void
|
|
200
|
+
private readonly onSelect: (sessionPath: string) => void | Promise<void>,
|
|
201
201
|
private readonly onCancel: () => void,
|
|
202
202
|
private readonly onExit: () => void,
|
|
203
203
|
private readonly onDelete?: (session: SessionInfo) => Promise<boolean>,
|
|
@@ -214,7 +214,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
214
214
|
this.#sessionList = new SessionList(sessions);
|
|
215
215
|
this.#sessionList.onSelect = session => {
|
|
216
216
|
if (this.inspector) void this.#inspect(session);
|
|
217
|
-
else this
|
|
217
|
+
else this.#dispatchSelect(session.path);
|
|
218
218
|
};
|
|
219
219
|
this.#sessionList.onCancel = () => this.#cancel();
|
|
220
220
|
this.#sessionList.onExit = () => this.#exit();
|
|
@@ -241,6 +241,29 @@ export class SessionSelectorComponent extends Container {
|
|
|
241
241
|
this.#onRequestRender?.();
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Observe async `onSelect` handlers. Callers (e.g. resume) may return a
|
|
246
|
+
* Promise; leaving it unobserved turns preparation/switch failures into
|
|
247
|
+
* process-killing unhandled rejections. Inline recovery keeps the picker
|
|
248
|
+
* usable when the host has not already closed it.
|
|
249
|
+
*/
|
|
250
|
+
#dispatchSelect(sessionPath: string): void {
|
|
251
|
+
void Promise.resolve(this.onSelect(sessionPath)).catch((error: unknown) => {
|
|
252
|
+
if (this.#state.kind === "settled") {
|
|
253
|
+
// Host already closed the picker (interactive resume path). Surface the
|
|
254
|
+
// message only if we still have a render surface; controller catch is
|
|
255
|
+
// the primary recovery path after `done()`.
|
|
256
|
+
this.#showError(error instanceof Error ? error.message : String(error));
|
|
257
|
+
this.#requestRender();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.#state = { kind: "browsing" };
|
|
261
|
+
this.#sessionList.setInputFrozen(false);
|
|
262
|
+
this.#showError(error instanceof Error ? error.message : String(error));
|
|
263
|
+
this.#requestRender();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
244
267
|
#settle(selection: SessionSelectionResult): void {
|
|
245
268
|
if (this.#state.kind === "settled") return;
|
|
246
269
|
this.#state = { kind: "settled" };
|
|
@@ -249,7 +272,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
249
272
|
this.#confirmationDialog = null;
|
|
250
273
|
if (dialog) this.removeChild(dialog);
|
|
251
274
|
if (this.onSelection) this.onSelection(selection);
|
|
252
|
-
else if (selection.kind === "selected") this
|
|
275
|
+
else if (selection.kind === "selected") this.#dispatchSelect(selection.path);
|
|
253
276
|
else this.onCancel();
|
|
254
277
|
}
|
|
255
278
|
#cancel(): void {
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { type RunGh, runGhDefault } from "../../../utils/gh";
|
|
2
2
|
|
|
3
3
|
const STATUS_LINE_GH_TIMEOUT_MS = 5_000;
|
|
4
|
+
const C0_C1_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
|
+
|
|
6
|
+
function canonicalPrUrl(value: unknown, number: number): string | null {
|
|
7
|
+
if (typeof value !== "string" || C0_C1_CONTROL_CHARACTERS.test(value)) return null;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(value);
|
|
11
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
12
|
+
if (!url.hostname || url.username || url.password || url.search || url.hash) return null;
|
|
13
|
+
|
|
14
|
+
const suffix = `/pull/${number}`;
|
|
15
|
+
if (!url.pathname.endsWith(suffix)) return null;
|
|
16
|
+
const repositoryPath = url.pathname.slice(1, -suffix.length).split("/");
|
|
17
|
+
if (repositoryPath.length !== 2 || repositoryPath.some(component => component === "")) return null;
|
|
18
|
+
|
|
19
|
+
return url.href;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export async function lookupCurrentPr(runGh: RunGh = runGhDefault): Promise<{ number: number; url: string } | null> {
|
|
6
26
|
try {
|
|
@@ -8,8 +28,9 @@ export async function lookupCurrentPr(runGh: RunGh = runGhDefault): Promise<{ nu
|
|
|
8
28
|
if (result.exitCode !== 0 || result.timedOut) return null;
|
|
9
29
|
|
|
10
30
|
const pr = JSON.parse(result.stdout) as { number?: unknown; url?: unknown };
|
|
11
|
-
if (typeof pr.number !== "number" ||
|
|
12
|
-
|
|
31
|
+
if (typeof pr.number !== "number" || !Number.isSafeInteger(pr.number) || pr.number <= 0) return null;
|
|
32
|
+
const url = canonicalPrUrl(pr.url, pr.number);
|
|
33
|
+
return url ? { number: pr.number, url } : null;
|
|
13
34
|
} catch {
|
|
14
35
|
return null;
|
|
15
36
|
}
|
|
@@ -32,6 +32,7 @@ import { computeIrcSplitWidths, getIrcSidebarSemanticToken } from "../components
|
|
|
32
32
|
import type { IrcObservationRecord } from "../irc-observation-ledger";
|
|
33
33
|
import { interruptHint } from "../shared";
|
|
34
34
|
import { buildAbortDisplayMessage } from "../utils/abort-message";
|
|
35
|
+
import { emitHostStatus } from "../utils/host-status";
|
|
35
36
|
import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
|
|
36
37
|
import { parseIrcMessage } from "../utils/irc-message";
|
|
37
38
|
import { ringTerminalBell } from "../utils/terminal-bell";
|
|
@@ -298,6 +299,7 @@ export class EventController {
|
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
302
|
+
emitHostStatus("working");
|
|
301
303
|
this.ctx.promptSuggestion?.onAgentStart();
|
|
302
304
|
this.#lastIntent = undefined;
|
|
303
305
|
this.#readToolCallArgs.clear();
|
|
@@ -924,6 +926,7 @@ export class EventController {
|
|
|
924
926
|
this.ctx.updateEditorBorderColor();
|
|
925
927
|
this.ctx.ui.requestRender();
|
|
926
928
|
this.#scheduleIdleCompaction();
|
|
929
|
+
emitHostStatus("finished");
|
|
927
930
|
this.sendCompletionNotification();
|
|
928
931
|
this.ctx.promptSuggestion?.onAgentEnd();
|
|
929
932
|
}
|
|
@@ -40,6 +40,7 @@ import { createReadonlySessionManager } from "../../session/session-manager";
|
|
|
40
40
|
import { parseThinkingLevel } from "../../thinking";
|
|
41
41
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
42
42
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
43
|
+
import { emitHostStatus } from "../utils/host-status";
|
|
43
44
|
import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
|
|
44
45
|
import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
|
|
45
46
|
import { prepareTranscriptRebuild } from "../utils/ui-helpers";
|
|
@@ -1258,6 +1259,7 @@ export class ExtensionUiController {
|
|
|
1258
1259
|
computeBudget();
|
|
1259
1260
|
|
|
1260
1261
|
ringTerminalBell(classifyHookSelectorBellEvent(title));
|
|
1262
|
+
emitHostStatus("attention");
|
|
1261
1263
|
|
|
1262
1264
|
this.ctx.hookSelector = new HookSelectorComponent(
|
|
1263
1265
|
title,
|
|
@@ -822,6 +822,17 @@ export class InputController {
|
|
|
822
822
|
if (runner?.hasHandlers("input")) {
|
|
823
823
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
824
824
|
if (result?.handled) {
|
|
825
|
+
const handledText = result.text === undefined ? "" : sanitizeText(result.text).trim();
|
|
826
|
+
if (handledText) {
|
|
827
|
+
this.ctx.addMessageToChat({
|
|
828
|
+
role: "custom",
|
|
829
|
+
customType: "extension-input-result",
|
|
830
|
+
content: handledText,
|
|
831
|
+
display: true,
|
|
832
|
+
timestamp: Date.now(),
|
|
833
|
+
});
|
|
834
|
+
this.ctx.ui.requestRender();
|
|
835
|
+
}
|
|
825
836
|
if (this.#canModifyComposer(composer)) {
|
|
826
837
|
this.ctx.editor.setText("");
|
|
827
838
|
}
|
|
@@ -2638,9 +2638,15 @@ export class SelectorController {
|
|
|
2638
2638
|
this.showSelector(done => {
|
|
2639
2639
|
const selector = new SessionSelectorComponent(
|
|
2640
2640
|
sessions,
|
|
2641
|
-
|
|
2641
|
+
sessionPath => {
|
|
2642
|
+
// `onSelect` is a void dispatch boundary: close the picker first, then
|
|
2643
|
+
// observe resume failures so a managed-candidate race (or any other
|
|
2644
|
+
// preparation/switch rejection) surfaces as UI error instead of an
|
|
2645
|
+
// unhandled rejection that can kill the process. Do not auto-retry.
|
|
2642
2646
|
done();
|
|
2643
|
-
|
|
2647
|
+
void this.handleResumeSession(sessionPath).catch(error => {
|
|
2648
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
2649
|
+
});
|
|
2644
2650
|
},
|
|
2645
2651
|
() => {
|
|
2646
2652
|
done();
|
|
@@ -1036,9 +1036,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1036
1036
|
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
1037
1037
|
await this.#goalModeController.beforeGetUserInput();
|
|
1038
1038
|
}
|
|
1039
|
-
const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
|
|
1039
|
+
const { promise, resolve, reject } = Promise.withResolvers<SubmittedUserInput>();
|
|
1040
|
+
let unsubscribeStop = () => {};
|
|
1041
|
+
unsubscribeStop = this.onStop(() => {
|
|
1042
|
+
this.onInputCallback = undefined;
|
|
1043
|
+
reject(Object.assign(new Error("Interactive mode stopped"), { code: "cancelled" }));
|
|
1044
|
+
});
|
|
1040
1045
|
this.onInputCallback = input => {
|
|
1041
1046
|
this.onInputCallback = undefined;
|
|
1047
|
+
unsubscribeStop();
|
|
1042
1048
|
resolve(input);
|
|
1043
1049
|
};
|
|
1044
1050
|
this.#goalModeController.scheduleContinuation();
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type AssistantMessage, type ImageContent, isContextOverflow } from "@gajae-code/ai";
|
|
9
9
|
import { isKnownSinkPeerClosedError, logger, sanitizeText } from "@gajae-code/utils";
|
|
10
|
+
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
10
11
|
import type { AgentSession } from "../session/agent-session";
|
|
11
12
|
import { isSilentAbort } from "../session/messages";
|
|
12
13
|
import { initializeExtensions } from "./runtime-init";
|
|
@@ -207,6 +208,15 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
207
208
|
if (header) stdout.write(`${JSON.stringify(header)}\n`);
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
// Bundled and file-based slash commands (`/init`, project `commands/*.md`, …) expand
|
|
212
|
+
// inside AgentSession#prompt from the list handed over by setSlashCommands. Interactive
|
|
213
|
+
// mode loads it while building its autocomplete; print mode has no autocomplete, so
|
|
214
|
+
// without this the list stays empty and a leading-slash prompt reaches the model as
|
|
215
|
+
// literal prose — no expansion, no error, and an answer that looks like the command ran.
|
|
216
|
+
await logger.time("print:slash-commands", async () => {
|
|
217
|
+
session.setSlashCommands(await loadSlashCommands({ cwd: session.sessionManager.getCwd() }));
|
|
218
|
+
});
|
|
219
|
+
|
|
210
220
|
// Set up extensions for print mode (no UI, no command context).
|
|
211
221
|
await initializeExtensions(session, {
|
|
212
222
|
reportSendError: (action, err) => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-terminal agent status markers.
|
|
3
|
+
*
|
|
4
|
+
* Interactive turns announce their state as an OSC 777 sequence:
|
|
5
|
+
*
|
|
6
|
+
* ESC ] 777 ; notify;Terax;gjc;<event> BEL
|
|
7
|
+
*
|
|
8
|
+
* A terminal that understands the sequence (Terax reads it to drive its per-tab
|
|
9
|
+
* agent status and header bell) reacts; every other terminal discards the OSC
|
|
10
|
+
* as unknown, exactly like the OSC 133 prompt marks and OSC 7 cwd reports that
|
|
11
|
+
* shells emit unconditionally. So there is no host sniffing and no env gate:
|
|
12
|
+
* any host that wants agent status can parse the stream without gjc knowing it
|
|
13
|
+
* exists.
|
|
14
|
+
*
|
|
15
|
+
* `notify;Terax;` is the wire identifier of the only protocol that currently
|
|
16
|
+
* carries per-turn agent state, not a host check - the payload names gjc as the
|
|
17
|
+
* reporting agent.
|
|
18
|
+
*
|
|
19
|
+
* Interactive TUI only. Print and RPC mode stdout stays byte-for-byte parseable
|
|
20
|
+
* because they never reach these controllers.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type HostStatusEvent = "working" | "attention" | "finished";
|
|
24
|
+
|
|
25
|
+
function marker(event: HostStatusEvent): string {
|
|
26
|
+
return `\x1b]777;notify;Terax;gjc;${event}\x07`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function emitHostStatus(
|
|
30
|
+
event: HostStatusEvent,
|
|
31
|
+
output: Pick<NodeJS.WriteStream, "write"> = process.stdout,
|
|
32
|
+
): void {
|
|
33
|
+
try {
|
|
34
|
+
output.write(marker(event));
|
|
35
|
+
} catch {
|
|
36
|
+
// Best-effort host integration only.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -21,6 +21,25 @@ MUST read before making changes within:
|
|
|
21
21
|
{{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
|
|
22
22
|
</dir-context>
|
|
23
23
|
{{/if}}
|
|
24
|
+
|
|
25
|
+
{{#if alwaysApplyRules.length}}
|
|
26
|
+
{{#each alwaysApplyRules}}
|
|
27
|
+
{{content}}
|
|
28
|
+
{{/each}}
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{#if rules.length}}
|
|
31
|
+
Rules are local constraints. You MUST read `rule://<name>` when working in that domain.
|
|
32
|
+
<rules>
|
|
33
|
+
{{#list rules join="\n"}}
|
|
34
|
+
<rule name="{{name}}">
|
|
35
|
+
{{description}}
|
|
36
|
+
{{#if globs.length}}
|
|
37
|
+
{{#list globs join="\n"}}<glob>{{this}}</glob>{{/list}}
|
|
38
|
+
{{/if}}
|
|
39
|
+
</rule>
|
|
40
|
+
{{/list}}
|
|
41
|
+
</rules>
|
|
42
|
+
{{/if}}
|
|
24
43
|
<critical>
|
|
25
44
|
- Each response MUST advance the task. There is no stopping condition other than completion.
|
|
26
45
|
- You MUST default to informed action; do not ask for confirmation when tools or repo context can answer.
|
package/src/runtime-mcp/types.ts
CHANGED
|
@@ -217,6 +217,8 @@ export type MCPContent = MCPTextContent | MCPImageContent | MCPResourceContent;
|
|
|
217
217
|
export interface MCPToolCallResult {
|
|
218
218
|
content: MCPContent[];
|
|
219
219
|
isError?: boolean;
|
|
220
|
+
/** MCP result metadata, encoded as `_meta` on the wire. */
|
|
221
|
+
_meta?: Record<string, unknown>;
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
// =============================================================================
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../../session/session-storage";
|
|
32
32
|
import type { SessionLifecycleMcpServer } from "../acp/mcp";
|
|
33
33
|
import { SdkClient, SdkClientError } from "../client/client";
|
|
34
|
+
import { SESSION_PREPARED_EVENT } from "../host/host";
|
|
34
35
|
import {
|
|
35
36
|
type LogicalSessionCandidate,
|
|
36
37
|
listManagedSessionCandidates,
|
|
@@ -202,6 +203,16 @@ export interface SessionLifecycleTranscriptIdentity {
|
|
|
202
203
|
sha256: string;
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/**
|
|
207
|
+
* When a lifecycle-managed session publishes its replayable readiness signal.
|
|
208
|
+
*
|
|
209
|
+
* `immediate` is the stock contract. `deferred` prepares the session instead:
|
|
210
|
+
* the child publishes a distinct prepared signal, keeps `session_ready`
|
|
211
|
+
* withheld, and stays unusable for input until it is explicitly activated. It
|
|
212
|
+
* is broker-issued and session-scoped precisely so a prepared session can never
|
|
213
|
+
* be produced by an inherited process-global flag.
|
|
214
|
+
*/
|
|
215
|
+
export type SessionLifecycleReadiness = "immediate" | "deferred";
|
|
205
216
|
export interface SessionLifecycleLaunchRequest {
|
|
206
217
|
operation: "session.create" | "session.fork" | "session.resume";
|
|
207
218
|
sessionId: string;
|
|
@@ -218,6 +229,8 @@ export interface SessionLifecycleLaunchRequest {
|
|
|
218
229
|
modelPreset?: string;
|
|
219
230
|
mcpServers?: SessionLifecycleMcpServer[];
|
|
220
231
|
worktree?: SessionLifecycleWorktreeTarget;
|
|
232
|
+
/** Absent means the stock immediate contract; `deferred` prepares the session. */
|
|
233
|
+
readiness?: SessionLifecycleReadiness;
|
|
221
234
|
receivedAt: number;
|
|
222
235
|
requestedReadinessTimeoutMs: number;
|
|
223
236
|
semanticReadyDeadlineAt: number;
|
|
@@ -363,6 +376,8 @@ export function readSessionLifecycleLaunchRequest(
|
|
|
363
376
|
now,
|
|
364
377
|
) ||
|
|
365
378
|
(request.worktree !== undefined && !isLifecycleWorktreeTarget(request.worktree)) ||
|
|
379
|
+
(request.readiness !== undefined && request.readiness !== "immediate" && request.readiness !== "deferred") ||
|
|
380
|
+
(request.readiness === "deferred" && request.operation !== "session.create") ||
|
|
366
381
|
(request.operation === "session.resume" &&
|
|
367
382
|
!hasValidTranscriptAuthority(request.sessionPath, request.sessionIdentity)) ||
|
|
368
383
|
(request.operation === "session.fork" &&
|
|
@@ -386,6 +401,7 @@ type SessionLaunch = {
|
|
|
386
401
|
modelPreset?: string;
|
|
387
402
|
mcpServers?: SessionLifecycleMcpServer[];
|
|
388
403
|
worktree?: SessionLifecycleWorktreeTarget;
|
|
404
|
+
readiness?: SessionLifecycleReadiness;
|
|
389
405
|
worktreePlan?: GjcLaunchWorktreePlan;
|
|
390
406
|
};
|
|
391
407
|
|
|
@@ -2294,6 +2310,16 @@ function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolea
|
|
|
2294
2310
|
);
|
|
2295
2311
|
}
|
|
2296
2312
|
|
|
2313
|
+
/**
|
|
2314
|
+
* Wait for the child's semantic completion signal at exactly this endpoint.
|
|
2315
|
+
*
|
|
2316
|
+
* `session_ready` is the stock signal. A deferred launch waits on
|
|
2317
|
+
* `session_prepared` instead: it is the same authenticated, replayable proof
|
|
2318
|
+
* that the child finished initializing and owns its endpoint, minus the
|
|
2319
|
+
* readiness no consumer may act on yet. Both are additionally bound to the
|
|
2320
|
+
* owner-proved lifecycle receipt through `currentReadyAuthority`, so an
|
|
2321
|
+
* endpoint file appearing on its own never satisfies either wait.
|
|
2322
|
+
*/
|
|
2297
2323
|
async function waitForReady(
|
|
2298
2324
|
broker: Broker,
|
|
2299
2325
|
id: string,
|
|
@@ -2301,6 +2327,7 @@ async function waitForReady(
|
|
|
2301
2327
|
deadline: number,
|
|
2302
2328
|
expected: EffectMarker,
|
|
2303
2329
|
timing: LifecycleTiming,
|
|
2330
|
+
signal: "session_ready" | typeof SESSION_PREPARED_EVENT = "session_ready",
|
|
2304
2331
|
): Promise<ReadinessResult> {
|
|
2305
2332
|
while (timing.now() < deadline) {
|
|
2306
2333
|
const startupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
@@ -2349,7 +2376,7 @@ async function waitForReady(
|
|
|
2349
2376
|
const frame = event as Record<string, unknown>;
|
|
2350
2377
|
return (
|
|
2351
2378
|
frame.type === "event" &&
|
|
2352
|
-
frame.name ===
|
|
2379
|
+
frame.name === signal &&
|
|
2353
2380
|
frame.sessionId === id &&
|
|
2354
2381
|
frame.generation === authority.endpointGeneration
|
|
2355
2382
|
);
|
|
@@ -2446,8 +2473,28 @@ async function launchInput(
|
|
|
2446
2473
|
return fail("invalid_input", "mcpServers must contain unique valid stdio, HTTP, or SSE server definitions.");
|
|
2447
2474
|
const mcpServers = input.mcpServers as SessionLifecycleMcpServer[] | undefined;
|
|
2448
2475
|
|
|
2476
|
+
/**
|
|
2477
|
+
* Prepared readiness is an explicit creation-only intent. Only the two exact
|
|
2478
|
+
* enum values are admissible, and a foreign value is refused rather than
|
|
2479
|
+
* collapsed into the stock immediate contract.
|
|
2480
|
+
*/
|
|
2481
|
+
if (input.readiness !== undefined && input.readiness !== "immediate" && input.readiness !== "deferred")
|
|
2482
|
+
return fail("invalid_input", "readiness must be either immediate or deferred.");
|
|
2483
|
+
const readiness = input.readiness as SessionLifecycleReadiness | undefined;
|
|
2484
|
+
if (readiness === "deferred" && operation !== "session.create")
|
|
2485
|
+
return fail("invalid_input", "readiness deferred is only supported for session.create.");
|
|
2486
|
+
|
|
2449
2487
|
if (operation === "session.create")
|
|
2450
|
-
return {
|
|
2488
|
+
return {
|
|
2489
|
+
id: randomUUID(),
|
|
2490
|
+
cwd,
|
|
2491
|
+
root: resolvedRoot,
|
|
2492
|
+
modelPreset,
|
|
2493
|
+
mcpServers,
|
|
2494
|
+
worktree,
|
|
2495
|
+
worktreePlan,
|
|
2496
|
+
...(readiness ? { readiness } : {}),
|
|
2497
|
+
};
|
|
2451
2498
|
if (operation === "session.resume") {
|
|
2452
2499
|
if (!requested) return fail("invalid_input", "sessionId is required to resume a saved session.");
|
|
2453
2500
|
const savedPath = text(input.sessionPath);
|
|
@@ -3107,6 +3154,7 @@ async function executeLifecycleResponse(
|
|
|
3107
3154
|
...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
|
|
3108
3155
|
...(launch.mcpServers ? { mcpServers: launch.mcpServers } : {}),
|
|
3109
3156
|
...(launch.worktree ? { worktree: launch.worktree } : {}),
|
|
3157
|
+
...(launch.readiness ? { readiness: launch.readiness } : {}),
|
|
3110
3158
|
};
|
|
3111
3159
|
let child: ChildProcess | undefined;
|
|
3112
3160
|
let spawnedAuthority: EffectMarker | undefined;
|
|
@@ -3161,7 +3209,15 @@ async function executeLifecycleResponse(
|
|
|
3161
3209
|
if (!child || !spawnedAuthority)
|
|
3162
3210
|
return fail("spawn_failed", "Unable to retain the spawned session process identity.");
|
|
3163
3211
|
await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
|
|
3164
|
-
const readiness = await waitForReady(
|
|
3212
|
+
const readiness = await waitForReady(
|
|
3213
|
+
broker,
|
|
3214
|
+
launch.id,
|
|
3215
|
+
launch.root,
|
|
3216
|
+
readinessDeadline,
|
|
3217
|
+
spawnedAuthority,
|
|
3218
|
+
timing,
|
|
3219
|
+
launch.readiness === "deferred" ? SESSION_PREPARED_EVENT : "session_ready",
|
|
3220
|
+
);
|
|
3165
3221
|
|
|
3166
3222
|
if (readiness.kind !== "ready") {
|
|
3167
3223
|
const terminated = await terminateSpawnedChild(
|
|
@@ -3220,6 +3276,7 @@ async function executeLifecycleResponse(
|
|
|
3220
3276
|
sessionId: launch.id,
|
|
3221
3277
|
cwd: launch.cwd,
|
|
3222
3278
|
endpoint: verified.endpoint,
|
|
3279
|
+
...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
|
|
3223
3280
|
...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
|
|
3224
3281
|
},
|
|
3225
3282
|
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type ChatDaemonCommandBindInput,
|
|
5
|
+
type ChatDaemonCommandOutcome,
|
|
6
|
+
serveChatDaemonCommandsOnce,
|
|
7
|
+
} from "./chat-daemon-command-channel";
|
|
3
8
|
import {
|
|
4
9
|
acquireChatDaemonOwnership,
|
|
5
10
|
type ChatDaemonKind,
|
|
11
|
+
chatDaemonGeneration,
|
|
6
12
|
clearChatDaemonControlRequest,
|
|
13
|
+
hasSafeChatDaemonStateShape,
|
|
7
14
|
readChatDaemonControlRequest,
|
|
8
15
|
readChatDaemonState,
|
|
9
16
|
releaseChatDaemonOwnership,
|
|
@@ -22,6 +29,8 @@ export interface ChatDaemonRuntimeHandle {
|
|
|
22
29
|
start(): Promise<void>;
|
|
23
30
|
stop(): Promise<void>;
|
|
24
31
|
transportHealthy?(): boolean;
|
|
32
|
+
/** Executes operator commands that must run inside the owning daemon. */
|
|
33
|
+
bindExistingRoot?(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export interface RunChatDaemonInternalDeps {
|
|
@@ -212,12 +221,45 @@ export async function runChatDaemonInternal(
|
|
|
212
221
|
}
|
|
213
222
|
})();
|
|
214
223
|
}, 5_000);
|
|
224
|
+
const ownedIncarnation = incarnation;
|
|
225
|
+
const stillOwner = async (): Promise<boolean> => {
|
|
226
|
+
const current = await readChatDaemonState(agentDir, kind);
|
|
227
|
+
return (
|
|
228
|
+
hasSafeChatDaemonStateShape(current) &&
|
|
229
|
+
current.kind === kind &&
|
|
230
|
+
current.ownerId === ownerId &&
|
|
231
|
+
current.pid === daemonPid &&
|
|
232
|
+
current.incarnation === ownedIncarnation &&
|
|
233
|
+
current.generation === chatDaemonGeneration(kind) &&
|
|
234
|
+
current.stoppedAt === undefined
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
const bindExistingRoot = activeRuntime.bindExistingRoot?.bind(activeRuntime);
|
|
215
238
|
while (!stopping) {
|
|
216
239
|
const request = await readChatDaemonControlRequest(agentDir, kind);
|
|
217
240
|
if (request?.ownerId === ownerId && request.incarnation === incarnation) {
|
|
218
241
|
await clearChatDaemonControlRequest(agentDir, kind, request.requestId);
|
|
219
242
|
break;
|
|
220
243
|
}
|
|
244
|
+
// Operator commands are served in place: unlike a lifecycle request they
|
|
245
|
+
// must never end this loop. A serving failure degrades to the caller's
|
|
246
|
+
// timeout rather than terminating a healthy transport.
|
|
247
|
+
if (bindExistingRoot) {
|
|
248
|
+
try {
|
|
249
|
+
await serveChatDaemonCommandsOnce({
|
|
250
|
+
agentDir,
|
|
251
|
+
kind,
|
|
252
|
+
ownerId,
|
|
253
|
+
pid: daemonPid,
|
|
254
|
+
incarnation: ownedIncarnation,
|
|
255
|
+
generation: chatDaemonGeneration(kind),
|
|
256
|
+
handler: { bindExistingRoot },
|
|
257
|
+
verifyOwnership: stillOwner,
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
// Retried on the next poll; the submitter observes a timeout.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
221
263
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
222
264
|
}
|
|
223
265
|
} finally {
|