@gajae-code/coding-agent 0.2.5 → 0.3.1
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 +28 -0
- package/dist/types/async/job-manager.d.ts +91 -2
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +10 -4
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +23 -29
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +198 -14
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +26 -1
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +334 -6
- package/src/cli/args.ts +9 -2
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +862 -0
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +54 -39
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +13 -3
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +372 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +235 -43
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +179 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +1155 -46
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +749 -0
- package/src/gjc-runtime/team-runtime.ts +1255 -189
- package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
- package/src/gjc-runtime/workflow-manifest.ts +427 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +148 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +600 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +98 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +197 -64
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +133 -20
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +43 -1
- package/src/modes/controllers/input-controller.ts +105 -9
- package/src/modes/controllers/selector-controller.ts +31 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +28 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +39 -4
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +8 -0
- package/src/session/agent-session.ts +445 -71
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +33 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +79 -13
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +376 -74
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +54 -134
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +104 -10
- package/src/tools/ask.ts +88 -27
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +423 -79
package/src/async/job-manager.ts
CHANGED
|
@@ -10,7 +10,7 @@ const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
|
10
10
|
export interface AsyncJob {
|
|
11
11
|
id: string;
|
|
12
12
|
type: "bash" | "task";
|
|
13
|
-
status: "running" | "completed" | "failed" | "cancelled";
|
|
13
|
+
status: "running" | "completed" | "failed" | "cancelled" | "paused";
|
|
14
14
|
startTime: number;
|
|
15
15
|
label: string;
|
|
16
16
|
abortController: AbortController;
|
|
@@ -35,6 +35,66 @@ export interface AsyncJobMetadata {
|
|
|
35
35
|
description?: string;
|
|
36
36
|
assignment?: string;
|
|
37
37
|
};
|
|
38
|
+
/** True when this bash job was started by the `monitor` tool (vs plain async bash). */
|
|
39
|
+
monitor?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Typed outcome a subagent task run may produce. A `paused` outcome is
|
|
44
|
+
* non-terminal and non-delivering: the run suspended at a safe boundary and the
|
|
45
|
+
* subagent can be resumed from its persisted sessionFile. `completed` always
|
|
46
|
+
* wins a race with a late pause because the run returns it once it has actually
|
|
47
|
+
* finished.
|
|
48
|
+
*/
|
|
49
|
+
export type SubagentRunOutcome = { kind: "completed"; text: string } | { kind: "paused"; note?: string };
|
|
50
|
+
|
|
51
|
+
/** Canonical lifecycle of a subagent across pause/resume cycles. */
|
|
52
|
+
export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Live, executor-owned control handle for a RUNNING subagent. Registered when a
|
|
56
|
+
* subagent run starts and removed on pause/terminal so a paused subagent retains
|
|
57
|
+
* no live `AgentSession` reference (leak-free).
|
|
58
|
+
*/
|
|
59
|
+
export interface SubagentLiveHandle {
|
|
60
|
+
/** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */
|
|
61
|
+
requestPause(): void;
|
|
62
|
+
/** Inject a steering message into the live session. */
|
|
63
|
+
injectMessage(content: string, deliverAs: "steer" | "followUp" | "nextTurn"): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Canonical, stable-id-keyed record for a subagent. Survives `AsyncJob`
|
|
68
|
+
* eviction so resume stays addressable by subagent id, and is the single source
|
|
69
|
+
* of truth for control-plane status and identity.
|
|
70
|
+
*/
|
|
71
|
+
export interface SubagentRecord {
|
|
72
|
+
subagentId: string;
|
|
73
|
+
ownerId?: string;
|
|
74
|
+
/** Current live/last AsyncJob id; null while queued with no active job. */
|
|
75
|
+
currentJobId: string | null;
|
|
76
|
+
historicalJobIds: string[];
|
|
77
|
+
status: SubagentLifecycle;
|
|
78
|
+
sessionFile: string | null;
|
|
79
|
+
/** False for ephemeral sessions (no persistent artifacts dir). */
|
|
80
|
+
resumable: boolean;
|
|
81
|
+
queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
|
|
85
|
+
export interface ResumeDescriptor {
|
|
86
|
+
subagentId: string;
|
|
87
|
+
ownerId?: string;
|
|
88
|
+
data: unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A pending resume awaiting a free concurrency slot. */
|
|
92
|
+
interface ResumeQueueEntry {
|
|
93
|
+
subagentId: string;
|
|
94
|
+
ownerId?: string;
|
|
95
|
+
seq: number;
|
|
96
|
+
message?: string;
|
|
97
|
+
createdAt: number;
|
|
38
98
|
}
|
|
39
99
|
|
|
40
100
|
export interface AsyncJobManagerOptions {
|
|
@@ -160,6 +220,18 @@ export class AsyncJobManager {
|
|
|
160
220
|
readonly #retentionMs: number;
|
|
161
221
|
#deliveryLoop: Promise<void> | undefined;
|
|
162
222
|
#disposed = false;
|
|
223
|
+
readonly #subagentRecords = new Map<string, SubagentRecord>();
|
|
224
|
+
readonly #liveHandles = new Map<string, SubagentLiveHandle>();
|
|
225
|
+
readonly #resumeQueue: ResumeQueueEntry[] = [];
|
|
226
|
+
#resumeSeq = 0;
|
|
227
|
+
#resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
|
|
228
|
+
readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
|
|
229
|
+
/**
|
|
230
|
+
* Change listeners notified on any mutation that can alter the live job set
|
|
231
|
+
* (register, terminal/eviction transitions, dispose). Used by the status-line
|
|
232
|
+
* jobs widget / overlay to refresh event-driven without polling.
|
|
233
|
+
*/
|
|
234
|
+
readonly #changeListeners = new Set<() => void>();
|
|
163
235
|
|
|
164
236
|
#filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
|
|
165
237
|
const ownerId = filter?.ownerId;
|
|
@@ -177,6 +249,29 @@ export class AsyncJobManager {
|
|
|
177
249
|
this.#retentionMs = Math.max(0, Math.floor(options.retentionMs ?? DEFAULT_RETENTION_MS));
|
|
178
250
|
}
|
|
179
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Subscribe to live-job-set change events. Returns an unsubscribe function.
|
|
254
|
+
* Listener errors are isolated so one bad subscriber cannot break others.
|
|
255
|
+
*/
|
|
256
|
+
onChange(cb: () => void): () => void {
|
|
257
|
+
this.#changeListeners.add(cb);
|
|
258
|
+
return () => {
|
|
259
|
+
this.#changeListeners.delete(cb);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#notifyChange(): void {
|
|
264
|
+
for (const cb of this.#changeListeners) {
|
|
265
|
+
try {
|
|
266
|
+
cb();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logger.warn("Async job change listener failed", {
|
|
269
|
+
error: error instanceof Error ? error.message : String(error),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
180
275
|
register(
|
|
181
276
|
type: "bash" | "task",
|
|
182
277
|
label: string,
|
|
@@ -184,7 +279,7 @@ export class AsyncJobManager {
|
|
|
184
279
|
jobId: string;
|
|
185
280
|
signal: AbortSignal;
|
|
186
281
|
reportProgress: (text: string, details?: Record<string, unknown>) => Promise<void>;
|
|
187
|
-
}) => Promise<string>,
|
|
282
|
+
}) => Promise<string | SubagentRunOutcome>,
|
|
188
283
|
options?: AsyncJobRegisterOptions,
|
|
189
284
|
): string {
|
|
190
285
|
if (this.#disposed) {
|
|
@@ -227,20 +322,38 @@ export class AsyncJobManager {
|
|
|
227
322
|
};
|
|
228
323
|
job.promise = (async () => {
|
|
229
324
|
try {
|
|
230
|
-
const
|
|
325
|
+
const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
|
|
326
|
+
const outcome: SubagentRunOutcome =
|
|
327
|
+
typeof result === "string" ? { kind: "completed", text: result } : result;
|
|
231
328
|
if (job.status === "cancelled") {
|
|
232
|
-
job.resultText = text;
|
|
329
|
+
job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
|
|
233
330
|
this.#scheduleEviction(id);
|
|
331
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
332
|
+
this.#drainResumeQueue();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (outcome.kind === "paused") {
|
|
336
|
+
// Sole canonical writer of the running -> paused transition. No
|
|
337
|
+
// delivery and no eviction scheduling: a paused subagent stays
|
|
338
|
+
// listed and resumable from its sessionFile.
|
|
339
|
+
job.status = "paused";
|
|
340
|
+
if (outcome.note) job.resultText = outcome.note;
|
|
341
|
+
this.#markRecordPaused(id);
|
|
342
|
+
this.#drainResumeQueue();
|
|
234
343
|
return;
|
|
235
344
|
}
|
|
236
345
|
job.status = "completed";
|
|
237
|
-
job.resultText = text;
|
|
238
|
-
this.#enqueueDelivery(id, text);
|
|
346
|
+
job.resultText = outcome.text;
|
|
347
|
+
this.#enqueueDelivery(id, outcome.text);
|
|
239
348
|
this.#scheduleEviction(id);
|
|
349
|
+
this.#markRecordTerminal(id, "completed");
|
|
350
|
+
this.#drainResumeQueue();
|
|
240
351
|
} catch (error) {
|
|
241
352
|
if (job.status === "cancelled") {
|
|
242
353
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
243
354
|
this.#scheduleEviction(id);
|
|
355
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
356
|
+
this.#drainResumeQueue();
|
|
244
357
|
return;
|
|
245
358
|
}
|
|
246
359
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
@@ -248,10 +361,13 @@ export class AsyncJobManager {
|
|
|
248
361
|
job.errorText = errorText;
|
|
249
362
|
this.#enqueueDelivery(id, errorText);
|
|
250
363
|
this.#scheduleEviction(id);
|
|
364
|
+
this.#markRecordTerminal(id, "failed");
|
|
365
|
+
this.#drainResumeQueue();
|
|
251
366
|
}
|
|
252
367
|
})();
|
|
253
368
|
|
|
254
369
|
this.#jobs.set(id, job);
|
|
370
|
+
this.#notifyChange();
|
|
255
371
|
return id;
|
|
256
372
|
}
|
|
257
373
|
|
|
@@ -264,6 +380,15 @@ export class AsyncJobManager {
|
|
|
264
380
|
const job = this.#jobs.get(id);
|
|
265
381
|
if (!job) return false;
|
|
266
382
|
if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
|
|
383
|
+
if (job.status === "paused") {
|
|
384
|
+
// Paused jobs have no running promise to abort; transition directly.
|
|
385
|
+
// The session file is kept, so the record stays resumable by id.
|
|
386
|
+
job.status = "cancelled";
|
|
387
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
388
|
+
this.#scheduleEviction(id);
|
|
389
|
+
this.#drainResumeQueue();
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
267
392
|
if (job.status !== "running") return false;
|
|
268
393
|
job.status = "cancelled";
|
|
269
394
|
job.abortController.abort();
|
|
@@ -271,6 +396,200 @@ export class AsyncJobManager {
|
|
|
271
396
|
return true;
|
|
272
397
|
}
|
|
273
398
|
|
|
399
|
+
// ── Subagent control plane (pause / resume / steer support) ──────────
|
|
400
|
+
|
|
401
|
+
/** Register or replace the canonical record for a subagent. */
|
|
402
|
+
registerSubagentRecord(record: SubagentRecord): void {
|
|
403
|
+
this.#subagentRecords.set(record.subagentId, record);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
|
|
407
|
+
const rec = this.#subagentRecords.get(subagentId.trim());
|
|
408
|
+
if (!rec) return undefined;
|
|
409
|
+
if (filter?.ownerId && rec.ownerId !== filter.ownerId) return undefined;
|
|
410
|
+
return rec;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[] {
|
|
414
|
+
const ownerId = filter?.ownerId;
|
|
415
|
+
const out: SubagentRecord[] = [];
|
|
416
|
+
for (const rec of this.#subagentRecords.values()) {
|
|
417
|
+
if (ownerId && rec.ownerId !== ownerId) continue;
|
|
418
|
+
out.push(rec);
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
registerLiveHandle(subagentId: string, handle: SubagentLiveHandle): void {
|
|
424
|
+
this.#liveHandles.set(subagentId, handle);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
getLiveHandle(subagentId: string): SubagentLiveHandle | undefined {
|
|
428
|
+
return this.#liveHandles.get(subagentId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
removeLiveHandle(subagentId: string): void {
|
|
432
|
+
this.#liveHandles.delete(subagentId);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */
|
|
436
|
+
setResumeRunner(
|
|
437
|
+
runner: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined,
|
|
438
|
+
): void {
|
|
439
|
+
this.#resumeRunner = runner;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
registerResumeDescriptor(descriptor: ResumeDescriptor): void {
|
|
443
|
+
this.#resumeDescriptors.set(descriptor.subagentId, descriptor);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined {
|
|
447
|
+
const descriptor = this.#resumeDescriptors.get(subagentId.trim());
|
|
448
|
+
if (!descriptor) return undefined;
|
|
449
|
+
if (filter?.ownerId && descriptor.ownerId !== filter.ownerId) return undefined;
|
|
450
|
+
return descriptor;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
#recordByJobId(jobId: string): SubagentRecord | undefined {
|
|
454
|
+
for (const rec of this.#subagentRecords.values()) {
|
|
455
|
+
if (rec.currentJobId === jobId) return rec;
|
|
456
|
+
}
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
#markRecordPaused(jobId: string): void {
|
|
461
|
+
const rec = this.#recordByJobId(jobId);
|
|
462
|
+
if (rec) {
|
|
463
|
+
rec.status = "paused";
|
|
464
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
|
|
469
|
+
const rec = this.#recordByJobId(jobId);
|
|
470
|
+
if (!rec) return;
|
|
471
|
+
rec.status = status;
|
|
472
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Request a graceful safe-boundary pause of a running subagent. */
|
|
476
|
+
pauseSubagent(
|
|
477
|
+
subagentId: string,
|
|
478
|
+
filter?: AsyncJobFilter,
|
|
479
|
+
): { ok: boolean; status?: SubagentLifecycle; reason?: string } {
|
|
480
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
481
|
+
if (!rec) return { ok: false, reason: "not_found" };
|
|
482
|
+
if (rec.status !== "running") return { ok: false, status: rec.status, reason: "not_running" };
|
|
483
|
+
const handle = this.#liveHandles.get(rec.subagentId);
|
|
484
|
+
if (!handle) return { ok: false, status: rec.status, reason: "no_live_handle" };
|
|
485
|
+
handle.requestPause();
|
|
486
|
+
return { ok: true, status: rec.status };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Resume a non-running subagent from its sessionFile, optionally injecting a message first. */
|
|
490
|
+
resumeSubagent(
|
|
491
|
+
subagentId: string,
|
|
492
|
+
filter?: AsyncJobFilter,
|
|
493
|
+
message?: string,
|
|
494
|
+
): { ok: boolean; status?: SubagentLifecycle; jobId?: string; queued?: boolean; reason?: string } {
|
|
495
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
496
|
+
if (!rec) return { ok: false, reason: "not_found" };
|
|
497
|
+
if (rec.status === "running") return { ok: false, status: "running", reason: "already_running" };
|
|
498
|
+
if (rec.status === "queued") {
|
|
499
|
+
if (message !== undefined && rec.queued) {
|
|
500
|
+
rec.queued.message = message;
|
|
501
|
+
const queued = this.#resumeQueue.find(entry => entry.subagentId === rec.subagentId);
|
|
502
|
+
if (queued) queued.message = message;
|
|
503
|
+
return { ok: true, queued: true, status: "queued" };
|
|
504
|
+
}
|
|
505
|
+
return { ok: false, status: "queued", reason: "already_queued" };
|
|
506
|
+
}
|
|
507
|
+
if (!rec.resumable || !rec.sessionFile) return { ok: false, reason: "context_unavailable" };
|
|
508
|
+
if (!this.#resumeRunner) return { ok: false, reason: "no_runner" };
|
|
509
|
+
if (this.getRunningJobs().length >= this.#maxRunningJobs) {
|
|
510
|
+
const seq = ++this.#resumeSeq;
|
|
511
|
+
rec.status = "queued";
|
|
512
|
+
rec.queued = { ownerId: rec.ownerId, seq, message, createdAt: Date.now() };
|
|
513
|
+
this.#resumeQueue.push({
|
|
514
|
+
subagentId: rec.subagentId,
|
|
515
|
+
ownerId: rec.ownerId,
|
|
516
|
+
seq,
|
|
517
|
+
message,
|
|
518
|
+
createdAt: rec.queued.createdAt,
|
|
519
|
+
});
|
|
520
|
+
return { ok: true, queued: true, status: "queued" };
|
|
521
|
+
}
|
|
522
|
+
return this.#startResume(rec, message);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#startResume(
|
|
526
|
+
rec: SubagentRecord,
|
|
527
|
+
message?: string,
|
|
528
|
+
): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
|
|
529
|
+
const prevJobId = rec.currentJobId;
|
|
530
|
+
const newJobId = this.#resumeRunner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
|
|
531
|
+
if (!newJobId) return { ok: false, reason: "resume_failed" };
|
|
532
|
+
if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
|
|
533
|
+
rec.currentJobId = newJobId;
|
|
534
|
+
rec.status = this.#jobs.get(newJobId)?.status ?? "running";
|
|
535
|
+
rec.queued = undefined;
|
|
536
|
+
return { ok: true, status: rec.status, jobId: newJobId };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Drain queued resumes (FIFO by seq) while concurrency slots are available. */
|
|
540
|
+
#drainResumeQueue(): void {
|
|
541
|
+
if (this.#resumeQueue.length === 0) return;
|
|
542
|
+
this.#resumeQueue.sort((a, b) => a.seq - b.seq);
|
|
543
|
+
while (this.#resumeQueue.length > 0 && this.getRunningJobs().length < this.#maxRunningJobs) {
|
|
544
|
+
const entry = this.#resumeQueue.shift();
|
|
545
|
+
if (!entry) return;
|
|
546
|
+
const rec = this.#subagentRecords.get(entry.subagentId);
|
|
547
|
+
if (rec?.status !== "queued") continue;
|
|
548
|
+
this.#startResume(rec, entry.message);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Cancel a subagent by stable id across running/paused/queued states (keeps the session file). */
|
|
553
|
+
cancelSubagent(subagentId: string, filter?: AsyncJobFilter): boolean {
|
|
554
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
555
|
+
if (!rec) return false;
|
|
556
|
+
if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
|
|
557
|
+
if (rec.status === "paused") {
|
|
558
|
+
if (rec.currentJobId) {
|
|
559
|
+
const job = this.#jobs.get(rec.currentJobId);
|
|
560
|
+
if (job && job.status === "paused") {
|
|
561
|
+
job.status = "cancelled";
|
|
562
|
+
this.#scheduleEviction(rec.currentJobId);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
rec.status = "cancelled";
|
|
566
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
567
|
+
this.#drainResumeQueue();
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
if (rec.status === "queued") {
|
|
571
|
+
const idx = this.#resumeQueue.findIndex(e => e.subagentId === rec.subagentId);
|
|
572
|
+
if (idx !== -1) this.#resumeQueue.splice(idx, 1);
|
|
573
|
+
rec.status = "cancelled";
|
|
574
|
+
rec.queued = undefined;
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#purgeOwnerSubagentState(ownerId?: string): void {
|
|
581
|
+
for (let i = this.#resumeQueue.length - 1; i >= 0; i--) {
|
|
582
|
+
if (!ownerId || this.#resumeQueue[i].ownerId === ownerId) this.#resumeQueue.splice(i, 1);
|
|
583
|
+
}
|
|
584
|
+
for (const [sid, rec] of this.#subagentRecords) {
|
|
585
|
+
if (!ownerId || rec.ownerId === ownerId) {
|
|
586
|
+
this.#liveHandles.delete(sid);
|
|
587
|
+
this.#resumeDescriptors.delete(sid);
|
|
588
|
+
this.#subagentRecords.delete(sid);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
274
593
|
getJob(id: string): AsyncJob | undefined {
|
|
275
594
|
return this.#jobs.get(id);
|
|
276
595
|
}
|
|
@@ -451,6 +770,7 @@ export class AsyncJobManager {
|
|
|
451
770
|
}
|
|
452
771
|
}
|
|
453
772
|
}
|
|
773
|
+
this.#purgeOwnerSubagentState(ownerId);
|
|
454
774
|
}
|
|
455
775
|
|
|
456
776
|
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
@@ -588,6 +908,12 @@ export class AsyncJobManager {
|
|
|
588
908
|
this.#watchedJobs.clear();
|
|
589
909
|
this.#outputState.clear();
|
|
590
910
|
this.#ownerCleanups.clear();
|
|
911
|
+
this.#subagentRecords.clear();
|
|
912
|
+
this.#liveHandles.clear();
|
|
913
|
+
this.#resumeDescriptors.clear();
|
|
914
|
+
this.#resumeQueue.length = 0;
|
|
915
|
+
this.#notifyChange();
|
|
916
|
+
this.#changeListeners.clear();
|
|
591
917
|
return drained;
|
|
592
918
|
}
|
|
593
919
|
|
|
@@ -617,6 +943,7 @@ export class AsyncJobManager {
|
|
|
617
943
|
}
|
|
618
944
|
|
|
619
945
|
#scheduleEviction(jobId: string): void {
|
|
946
|
+
this.#notifyChange();
|
|
620
947
|
if (this.#retentionMs <= 0) {
|
|
621
948
|
this.#jobs.delete(jobId);
|
|
622
949
|
this.#suppressedDeliveries.delete(jobId);
|
|
@@ -634,6 +961,7 @@ export class AsyncJobManager {
|
|
|
634
961
|
this.#suppressedDeliveries.delete(jobId);
|
|
635
962
|
this.#watchedJobs.delete(jobId);
|
|
636
963
|
this.#outputState.delete(jobId);
|
|
964
|
+
this.#notifyChange();
|
|
637
965
|
}, this.#retentionMs);
|
|
638
966
|
timer.unref();
|
|
639
967
|
this.#evictionTimers.set(jobId, timer);
|
package/src/cli/args.ts
CHANGED
|
@@ -7,7 +7,7 @@ import chalk from "chalk";
|
|
|
7
7
|
import { parseEffort } from "../thinking";
|
|
8
8
|
import { BUILTIN_TOOLS } from "../tools";
|
|
9
9
|
|
|
10
|
-
export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
|
|
10
|
+
export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui" | "bridge";
|
|
11
11
|
|
|
12
12
|
export interface Args {
|
|
13
13
|
cwd?: string;
|
|
@@ -96,7 +96,14 @@ export function parseArgs(args: string[]): Args {
|
|
|
96
96
|
result.allowHome = true;
|
|
97
97
|
} else if (arg === "--mode" && i + 1 < args.length) {
|
|
98
98
|
const mode = args[++i];
|
|
99
|
-
if (
|
|
99
|
+
if (
|
|
100
|
+
mode === "text" ||
|
|
101
|
+
mode === "json" ||
|
|
102
|
+
mode === "rpc" ||
|
|
103
|
+
mode === "acp" ||
|
|
104
|
+
mode === "rpc-ui" ||
|
|
105
|
+
mode === "bridge"
|
|
106
|
+
) {
|
|
100
107
|
result.mode = mode;
|
|
101
108
|
}
|
|
102
109
|
} else if (arg === "--continue" || arg === "-c") {
|
package/src/cli/config-cli.ts
CHANGED
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
getEnumValues,
|
|
13
13
|
getType,
|
|
14
14
|
getUi,
|
|
15
|
+
SETTINGS_SCHEMA,
|
|
15
16
|
type SettingPath,
|
|
16
17
|
Settings,
|
|
17
18
|
type SettingValue,
|
|
18
19
|
settings,
|
|
19
20
|
} from "../config/settings";
|
|
20
|
-
import { SETTINGS_SCHEMA } from "../config/settings-schema";
|
|
21
21
|
import { theme } from "../modes/theme/theme";
|
|
22
22
|
import { initXdg } from "./commands/init-xdg";
|
|
23
23
|
|
|
@@ -183,10 +183,18 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
|
|
|
183
183
|
else throw new Error(`Invalid boolean value: ${rawValue}. Use true/false, yes/no, on/off, or 1/0`);
|
|
184
184
|
break;
|
|
185
185
|
}
|
|
186
|
-
case "number":
|
|
186
|
+
case "number": {
|
|
187
187
|
parsedValue = Number(trimmed);
|
|
188
188
|
if (!Number.isFinite(parsedValue)) throw new Error(`Invalid number: ${rawValue}`);
|
|
189
|
+
const validate =
|
|
190
|
+
"validate" in SETTINGS_SCHEMA[path]
|
|
191
|
+
? (SETTINGS_SCHEMA[path].validate as ((value: number) => boolean) | undefined)
|
|
192
|
+
: undefined;
|
|
193
|
+
if (validate?.(parsedValue as number) === false) {
|
|
194
|
+
throw new Error(`Invalid number for ${path}: ${rawValue}`);
|
|
195
|
+
}
|
|
189
196
|
break;
|
|
197
|
+
}
|
|
190
198
|
case "enum": {
|
|
191
199
|
const valid = getEnumValues(path);
|
|
192
200
|
if (valid && !valid.includes(trimmed)) {
|
package/src/cli.ts
CHANGED
|
@@ -35,9 +35,11 @@ const commands: CommandEntry[] = [
|
|
|
35
35
|
{ name: "setup", load: () => import("./commands/setup").then(m => m.default) },
|
|
36
36
|
{ name: "skills", load: () => import("./commands/skills").then(m => m.default) },
|
|
37
37
|
{ name: "session", load: () => import("./commands/session").then(m => m.default) },
|
|
38
|
+
{ name: "harness", load: () => import("./commands/harness").then(m => m.default) },
|
|
38
39
|
{ name: "team", load: () => import("./commands/team").then(m => m.default) },
|
|
39
40
|
{ name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
|
|
40
41
|
{ name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
|
|
42
|
+
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
41
43
|
{
|
|
42
44
|
name: "contribute-pr",
|
|
43
45
|
aliases: ["contribution-prep"],
|
|
@@ -21,6 +21,7 @@ export default class DeepInterview extends Command {
|
|
|
21
21
|
deliberate: Flags.boolean({
|
|
22
22
|
description: "Shortcut for --write handoff to ralplan in deliberate consensus mode",
|
|
23
23
|
}),
|
|
24
|
+
force: Flags.boolean({ description: "Overwrite corrupt existing deep-interview state during --write" }),
|
|
24
25
|
json: Flags.boolean({ description: "Output JSON" }),
|
|
25
26
|
};
|
|
26
27
|
static examples = [
|