@gajae-code/coding-agent 0.2.5 → 0.3.0
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 +10 -0
- package/dist/types/async/job-manager.d.ts +84 -2
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/deep-interview/render-middleware.d.ts +5 -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/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +24 -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-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- 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 +30 -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 +2 -29
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- 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 +24 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/types.d.ts +55 -3
- package/dist/types/tools/subagent.d.ts +11 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +298 -6
- 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/harness.ts +592 -0
- package/src/commands/team.ts +36 -39
- package/src/config/settings-schema.ts +7 -0
- package/src/config/settings.ts +5 -0
- package/src/deep-interview/render-middleware.ts +366 -0
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
- 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/deep-interview-runtime.ts +40 -21
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +25 -10
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +132 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +733 -21
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +718 -0
- package/src/gjc-runtime/team-runtime.ts +1083 -89
- package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
- package/src/gjc-runtime/workflow-manifest.ts +425 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +137 -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 +553 -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 +97 -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 +24 -41
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/hook-selector.ts +72 -2
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +9 -1
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +33 -3
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +231 -33
- 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 +91 -13
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +26 -0
- package/src/task/executor.ts +50 -8
- package/src/task/index.ts +120 -8
- package/src/task/render.ts +6 -3
- package/src/task/types.ts +56 -3
- package/src/tools/ask.ts +28 -7
- package/src/tools/subagent.ts +255 -64
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;
|
|
@@ -37,6 +37,64 @@ export interface AsyncJobMetadata {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Typed outcome a subagent task run may produce. A `paused` outcome is
|
|
42
|
+
* non-terminal and non-delivering: the run suspended at a safe boundary and the
|
|
43
|
+
* subagent can be resumed from its persisted sessionFile. `completed` always
|
|
44
|
+
* wins a race with a late pause because the run returns it once it has actually
|
|
45
|
+
* finished.
|
|
46
|
+
*/
|
|
47
|
+
export type SubagentRunOutcome = { kind: "completed"; text: string } | { kind: "paused"; note?: string };
|
|
48
|
+
|
|
49
|
+
/** Canonical lifecycle of a subagent across pause/resume cycles. */
|
|
50
|
+
export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Live, executor-owned control handle for a RUNNING subagent. Registered when a
|
|
54
|
+
* subagent run starts and removed on pause/terminal so a paused subagent retains
|
|
55
|
+
* no live `AgentSession` reference (leak-free).
|
|
56
|
+
*/
|
|
57
|
+
export interface SubagentLiveHandle {
|
|
58
|
+
/** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */
|
|
59
|
+
requestPause(): void;
|
|
60
|
+
/** Inject a steering message into the live session. */
|
|
61
|
+
injectMessage(content: string, deliverAs: "steer" | "followUp" | "nextTurn"): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Canonical, stable-id-keyed record for a subagent. Survives `AsyncJob`
|
|
66
|
+
* eviction so resume stays addressable by subagent id, and is the single source
|
|
67
|
+
* of truth for control-plane status and identity.
|
|
68
|
+
*/
|
|
69
|
+
export interface SubagentRecord {
|
|
70
|
+
subagentId: string;
|
|
71
|
+
ownerId?: string;
|
|
72
|
+
/** Current live/last AsyncJob id; null while queued with no active job. */
|
|
73
|
+
currentJobId: string | null;
|
|
74
|
+
historicalJobIds: string[];
|
|
75
|
+
status: SubagentLifecycle;
|
|
76
|
+
sessionFile: string | null;
|
|
77
|
+
/** False for ephemeral sessions (no persistent artifacts dir). */
|
|
78
|
+
resumable: boolean;
|
|
79
|
+
queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
|
|
83
|
+
export interface ResumeDescriptor {
|
|
84
|
+
subagentId: string;
|
|
85
|
+
ownerId?: string;
|
|
86
|
+
data: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A pending resume awaiting a free concurrency slot. */
|
|
90
|
+
interface ResumeQueueEntry {
|
|
91
|
+
subagentId: string;
|
|
92
|
+
ownerId?: string;
|
|
93
|
+
seq: number;
|
|
94
|
+
message?: string;
|
|
95
|
+
createdAt: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
export interface AsyncJobManagerOptions {
|
|
41
99
|
onJobComplete: (jobId: string, text: string, job?: AsyncJob) => void | Promise<void>;
|
|
42
100
|
maxRunningJobs?: number;
|
|
@@ -160,6 +218,12 @@ export class AsyncJobManager {
|
|
|
160
218
|
readonly #retentionMs: number;
|
|
161
219
|
#deliveryLoop: Promise<void> | undefined;
|
|
162
220
|
#disposed = false;
|
|
221
|
+
readonly #subagentRecords = new Map<string, SubagentRecord>();
|
|
222
|
+
readonly #liveHandles = new Map<string, SubagentLiveHandle>();
|
|
223
|
+
readonly #resumeQueue: ResumeQueueEntry[] = [];
|
|
224
|
+
#resumeSeq = 0;
|
|
225
|
+
#resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
|
|
226
|
+
readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
|
|
163
227
|
|
|
164
228
|
#filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
|
|
165
229
|
const ownerId = filter?.ownerId;
|
|
@@ -184,7 +248,7 @@ export class AsyncJobManager {
|
|
|
184
248
|
jobId: string;
|
|
185
249
|
signal: AbortSignal;
|
|
186
250
|
reportProgress: (text: string, details?: Record<string, unknown>) => Promise<void>;
|
|
187
|
-
}) => Promise<string>,
|
|
251
|
+
}) => Promise<string | SubagentRunOutcome>,
|
|
188
252
|
options?: AsyncJobRegisterOptions,
|
|
189
253
|
): string {
|
|
190
254
|
if (this.#disposed) {
|
|
@@ -227,20 +291,38 @@ export class AsyncJobManager {
|
|
|
227
291
|
};
|
|
228
292
|
job.promise = (async () => {
|
|
229
293
|
try {
|
|
230
|
-
const
|
|
294
|
+
const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
|
|
295
|
+
const outcome: SubagentRunOutcome =
|
|
296
|
+
typeof result === "string" ? { kind: "completed", text: result } : result;
|
|
231
297
|
if (job.status === "cancelled") {
|
|
232
|
-
job.resultText = text;
|
|
298
|
+
job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
|
|
233
299
|
this.#scheduleEviction(id);
|
|
300
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
301
|
+
this.#drainResumeQueue();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (outcome.kind === "paused") {
|
|
305
|
+
// Sole canonical writer of the running -> paused transition. No
|
|
306
|
+
// delivery and no eviction scheduling: a paused subagent stays
|
|
307
|
+
// listed and resumable from its sessionFile.
|
|
308
|
+
job.status = "paused";
|
|
309
|
+
if (outcome.note) job.resultText = outcome.note;
|
|
310
|
+
this.#markRecordPaused(id);
|
|
311
|
+
this.#drainResumeQueue();
|
|
234
312
|
return;
|
|
235
313
|
}
|
|
236
314
|
job.status = "completed";
|
|
237
|
-
job.resultText = text;
|
|
238
|
-
this.#enqueueDelivery(id, text);
|
|
315
|
+
job.resultText = outcome.text;
|
|
316
|
+
this.#enqueueDelivery(id, outcome.text);
|
|
239
317
|
this.#scheduleEviction(id);
|
|
318
|
+
this.#markRecordTerminal(id, "completed");
|
|
319
|
+
this.#drainResumeQueue();
|
|
240
320
|
} catch (error) {
|
|
241
321
|
if (job.status === "cancelled") {
|
|
242
322
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
243
323
|
this.#scheduleEviction(id);
|
|
324
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
325
|
+
this.#drainResumeQueue();
|
|
244
326
|
return;
|
|
245
327
|
}
|
|
246
328
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
@@ -248,6 +330,8 @@ export class AsyncJobManager {
|
|
|
248
330
|
job.errorText = errorText;
|
|
249
331
|
this.#enqueueDelivery(id, errorText);
|
|
250
332
|
this.#scheduleEviction(id);
|
|
333
|
+
this.#markRecordTerminal(id, "failed");
|
|
334
|
+
this.#drainResumeQueue();
|
|
251
335
|
}
|
|
252
336
|
})();
|
|
253
337
|
|
|
@@ -264,6 +348,15 @@ export class AsyncJobManager {
|
|
|
264
348
|
const job = this.#jobs.get(id);
|
|
265
349
|
if (!job) return false;
|
|
266
350
|
if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
|
|
351
|
+
if (job.status === "paused") {
|
|
352
|
+
// Paused jobs have no running promise to abort; transition directly.
|
|
353
|
+
// The session file is kept, so the record stays resumable by id.
|
|
354
|
+
job.status = "cancelled";
|
|
355
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
356
|
+
this.#scheduleEviction(id);
|
|
357
|
+
this.#drainResumeQueue();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
267
360
|
if (job.status !== "running") return false;
|
|
268
361
|
job.status = "cancelled";
|
|
269
362
|
job.abortController.abort();
|
|
@@ -271,6 +364,200 @@ export class AsyncJobManager {
|
|
|
271
364
|
return true;
|
|
272
365
|
}
|
|
273
366
|
|
|
367
|
+
// ── Subagent control plane (pause / resume / steer support) ──────────
|
|
368
|
+
|
|
369
|
+
/** Register or replace the canonical record for a subagent. */
|
|
370
|
+
registerSubagentRecord(record: SubagentRecord): void {
|
|
371
|
+
this.#subagentRecords.set(record.subagentId, record);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
|
|
375
|
+
const rec = this.#subagentRecords.get(subagentId.trim());
|
|
376
|
+
if (!rec) return undefined;
|
|
377
|
+
if (filter?.ownerId && rec.ownerId !== filter.ownerId) return undefined;
|
|
378
|
+
return rec;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[] {
|
|
382
|
+
const ownerId = filter?.ownerId;
|
|
383
|
+
const out: SubagentRecord[] = [];
|
|
384
|
+
for (const rec of this.#subagentRecords.values()) {
|
|
385
|
+
if (ownerId && rec.ownerId !== ownerId) continue;
|
|
386
|
+
out.push(rec);
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
registerLiveHandle(subagentId: string, handle: SubagentLiveHandle): void {
|
|
392
|
+
this.#liveHandles.set(subagentId, handle);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
getLiveHandle(subagentId: string): SubagentLiveHandle | undefined {
|
|
396
|
+
return this.#liveHandles.get(subagentId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
removeLiveHandle(subagentId: string): void {
|
|
400
|
+
this.#liveHandles.delete(subagentId);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */
|
|
404
|
+
setResumeRunner(
|
|
405
|
+
runner: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined,
|
|
406
|
+
): void {
|
|
407
|
+
this.#resumeRunner = runner;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
registerResumeDescriptor(descriptor: ResumeDescriptor): void {
|
|
411
|
+
this.#resumeDescriptors.set(descriptor.subagentId, descriptor);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined {
|
|
415
|
+
const descriptor = this.#resumeDescriptors.get(subagentId.trim());
|
|
416
|
+
if (!descriptor) return undefined;
|
|
417
|
+
if (filter?.ownerId && descriptor.ownerId !== filter.ownerId) return undefined;
|
|
418
|
+
return descriptor;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#recordByJobId(jobId: string): SubagentRecord | undefined {
|
|
422
|
+
for (const rec of this.#subagentRecords.values()) {
|
|
423
|
+
if (rec.currentJobId === jobId) return rec;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#markRecordPaused(jobId: string): void {
|
|
429
|
+
const rec = this.#recordByJobId(jobId);
|
|
430
|
+
if (rec) {
|
|
431
|
+
rec.status = "paused";
|
|
432
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
|
|
437
|
+
const rec = this.#recordByJobId(jobId);
|
|
438
|
+
if (!rec) return;
|
|
439
|
+
rec.status = status;
|
|
440
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Request a graceful safe-boundary pause of a running subagent. */
|
|
444
|
+
pauseSubagent(
|
|
445
|
+
subagentId: string,
|
|
446
|
+
filter?: AsyncJobFilter,
|
|
447
|
+
): { ok: boolean; status?: SubagentLifecycle; reason?: string } {
|
|
448
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
449
|
+
if (!rec) return { ok: false, reason: "not_found" };
|
|
450
|
+
if (rec.status !== "running") return { ok: false, status: rec.status, reason: "not_running" };
|
|
451
|
+
const handle = this.#liveHandles.get(rec.subagentId);
|
|
452
|
+
if (!handle) return { ok: false, status: rec.status, reason: "no_live_handle" };
|
|
453
|
+
handle.requestPause();
|
|
454
|
+
return { ok: true, status: rec.status };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Resume a non-running subagent from its sessionFile, optionally injecting a message first. */
|
|
458
|
+
resumeSubagent(
|
|
459
|
+
subagentId: string,
|
|
460
|
+
filter?: AsyncJobFilter,
|
|
461
|
+
message?: string,
|
|
462
|
+
): { ok: boolean; status?: SubagentLifecycle; jobId?: string; queued?: boolean; reason?: string } {
|
|
463
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
464
|
+
if (!rec) return { ok: false, reason: "not_found" };
|
|
465
|
+
if (rec.status === "running") return { ok: false, status: "running", reason: "already_running" };
|
|
466
|
+
if (rec.status === "queued") {
|
|
467
|
+
if (message !== undefined && rec.queued) {
|
|
468
|
+
rec.queued.message = message;
|
|
469
|
+
const queued = this.#resumeQueue.find(entry => entry.subagentId === rec.subagentId);
|
|
470
|
+
if (queued) queued.message = message;
|
|
471
|
+
return { ok: true, queued: true, status: "queued" };
|
|
472
|
+
}
|
|
473
|
+
return { ok: false, status: "queued", reason: "already_queued" };
|
|
474
|
+
}
|
|
475
|
+
if (!rec.resumable || !rec.sessionFile) return { ok: false, reason: "context_unavailable" };
|
|
476
|
+
if (!this.#resumeRunner) return { ok: false, reason: "no_runner" };
|
|
477
|
+
if (this.getRunningJobs().length >= this.#maxRunningJobs) {
|
|
478
|
+
const seq = ++this.#resumeSeq;
|
|
479
|
+
rec.status = "queued";
|
|
480
|
+
rec.queued = { ownerId: rec.ownerId, seq, message, createdAt: Date.now() };
|
|
481
|
+
this.#resumeQueue.push({
|
|
482
|
+
subagentId: rec.subagentId,
|
|
483
|
+
ownerId: rec.ownerId,
|
|
484
|
+
seq,
|
|
485
|
+
message,
|
|
486
|
+
createdAt: rec.queued.createdAt,
|
|
487
|
+
});
|
|
488
|
+
return { ok: true, queued: true, status: "queued" };
|
|
489
|
+
}
|
|
490
|
+
return this.#startResume(rec, message);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
#startResume(
|
|
494
|
+
rec: SubagentRecord,
|
|
495
|
+
message?: string,
|
|
496
|
+
): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
|
|
497
|
+
const prevJobId = rec.currentJobId;
|
|
498
|
+
const newJobId = this.#resumeRunner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
|
|
499
|
+
if (!newJobId) return { ok: false, reason: "resume_failed" };
|
|
500
|
+
if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
|
|
501
|
+
rec.currentJobId = newJobId;
|
|
502
|
+
rec.status = this.#jobs.get(newJobId)?.status ?? "running";
|
|
503
|
+
rec.queued = undefined;
|
|
504
|
+
return { ok: true, status: rec.status, jobId: newJobId };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Drain queued resumes (FIFO by seq) while concurrency slots are available. */
|
|
508
|
+
#drainResumeQueue(): void {
|
|
509
|
+
if (this.#resumeQueue.length === 0) return;
|
|
510
|
+
this.#resumeQueue.sort((a, b) => a.seq - b.seq);
|
|
511
|
+
while (this.#resumeQueue.length > 0 && this.getRunningJobs().length < this.#maxRunningJobs) {
|
|
512
|
+
const entry = this.#resumeQueue.shift();
|
|
513
|
+
if (!entry) return;
|
|
514
|
+
const rec = this.#subagentRecords.get(entry.subagentId);
|
|
515
|
+
if (rec?.status !== "queued") continue;
|
|
516
|
+
this.#startResume(rec, entry.message);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Cancel a subagent by stable id across running/paused/queued states (keeps the session file). */
|
|
521
|
+
cancelSubagent(subagentId: string, filter?: AsyncJobFilter): boolean {
|
|
522
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
523
|
+
if (!rec) return false;
|
|
524
|
+
if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
|
|
525
|
+
if (rec.status === "paused") {
|
|
526
|
+
if (rec.currentJobId) {
|
|
527
|
+
const job = this.#jobs.get(rec.currentJobId);
|
|
528
|
+
if (job && job.status === "paused") {
|
|
529
|
+
job.status = "cancelled";
|
|
530
|
+
this.#scheduleEviction(rec.currentJobId);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
rec.status = "cancelled";
|
|
534
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
535
|
+
this.#drainResumeQueue();
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (rec.status === "queued") {
|
|
539
|
+
const idx = this.#resumeQueue.findIndex(e => e.subagentId === rec.subagentId);
|
|
540
|
+
if (idx !== -1) this.#resumeQueue.splice(idx, 1);
|
|
541
|
+
rec.status = "cancelled";
|
|
542
|
+
rec.queued = undefined;
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#purgeOwnerSubagentState(ownerId?: string): void {
|
|
549
|
+
for (let i = this.#resumeQueue.length - 1; i >= 0; i--) {
|
|
550
|
+
if (!ownerId || this.#resumeQueue[i].ownerId === ownerId) this.#resumeQueue.splice(i, 1);
|
|
551
|
+
}
|
|
552
|
+
for (const [sid, rec] of this.#subagentRecords) {
|
|
553
|
+
if (!ownerId || rec.ownerId === ownerId) {
|
|
554
|
+
this.#liveHandles.delete(sid);
|
|
555
|
+
this.#resumeDescriptors.delete(sid);
|
|
556
|
+
this.#subagentRecords.delete(sid);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
274
561
|
getJob(id: string): AsyncJob | undefined {
|
|
275
562
|
return this.#jobs.get(id);
|
|
276
563
|
}
|
|
@@ -451,6 +738,7 @@ export class AsyncJobManager {
|
|
|
451
738
|
}
|
|
452
739
|
}
|
|
453
740
|
}
|
|
741
|
+
this.#purgeOwnerSubagentState(ownerId);
|
|
454
742
|
}
|
|
455
743
|
|
|
456
744
|
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
@@ -588,6 +876,10 @@ export class AsyncJobManager {
|
|
|
588
876
|
this.#watchedJobs.clear();
|
|
589
877
|
this.#outputState.clear();
|
|
590
878
|
this.#ownerCleanups.clear();
|
|
879
|
+
this.#subagentRecords.clear();
|
|
880
|
+
this.#liveHandles.clear();
|
|
881
|
+
this.#resumeDescriptors.clear();
|
|
882
|
+
this.#resumeQueue.length = 0;
|
|
591
883
|
return drained;
|
|
592
884
|
}
|
|
593
885
|
|
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"],
|