@gajae-code/coding-agent 0.4.1 → 0.4.3
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 +22 -0
- package/dist/types/async/job-manager.d.ts +25 -0
- package/dist/types/commands/ultragoal.d.ts +1 -0
- package/dist/types/commit/model-selection.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -19
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +26 -4
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/finalize.d.ts +8 -0
- package/dist/types/harness-control-plane/receipts.d.ts +16 -1
- package/dist/types/harness-control-plane/types.d.ts +16 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
- package/dist/types/reminders/star-reminder.d.ts +115 -0
- package/dist/types/session/agent-session.d.ts +30 -1
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/actions.d.ts +54 -0
- package/dist/types/tools/browser.d.ts +80 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/examples/extensions/README.md +20 -41
- package/package.json +7 -7
- package/src/async/job-manager.ts +120 -1
- package/src/cli/grep-cli.ts +1 -1
- package/src/commands/harness.ts +42 -3
- package/src/commands/ultragoal.ts +8 -1
- package/src/commit/agentic/index.ts +2 -2
- package/src/commit/model-selection.ts +7 -22
- package/src/commit/pipeline.ts +2 -2
- package/src/config/model-registry.ts +17 -9
- package/src/config/model-resolver.ts +14 -84
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +27 -4
- package/src/defaults/gjc/skills/team/SKILL.md +10 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +3 -2
- package/src/gjc-runtime/goal-mode-request.ts +21 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -2
- package/src/gjc-runtime/team-runtime.ts +78 -3
- package/src/gjc-runtime/ultragoal-guard.ts +18 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +240 -30
- package/src/harness-control-plane/finalize.ts +84 -0
- package/src/harness-control-plane/owner.ts +16 -3
- package/src/harness-control-plane/receipts.ts +39 -1
- package/src/harness-control-plane/rpc-adapter.ts +7 -1
- package/src/harness-control-plane/types.ts +33 -12
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +17 -9
- package/src/modes/acp/acp-event-mapper.ts +33 -1
- package/src/modes/components/custom-editor.ts +19 -3
- package/src/modes/controllers/input-controller.ts +27 -7
- package/src/modes/controllers/selector-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/rpc/rpc-client.ts +16 -3
- package/src/modes/rpc/rpc-mode.ts +5 -2
- package/src/modes/shared/agent-wire/command-contract.ts +18 -0
- package/src/modes/shared/agent-wire/event-contract.ts +147 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
- package/src/modes/shared/agent-wire/event-observation.ts +397 -0
- package/src/modes/shared/agent-wire/protocol.ts +24 -81
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/tools/browser.md +3 -2
- package/src/reminders/star-reminder.ts +422 -0
- package/src/runtime-mcp/manager.ts +15 -2
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +139 -17
- package/src/session/session-manager.ts +1 -1
- package/src/task/agents.ts +1 -1
- package/src/tools/bash.ts +6 -1
- package/src/tools/browser/actions.ts +189 -0
- package/src/tools/browser.ts +91 -1
- package/src/tools/image-gen.ts +42 -15
- package/src/tools/index.ts +7 -1
- package/src/tools/inspect-image.ts +10 -8
- package/src/tools/job.ts +12 -2
- package/src/tools/monitor.ts +98 -17
- package/src/utils/commit-message-generator.ts +6 -13
- package/src/utils/title-generator.ts +1 -1
- package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
- package/src/harness-control-plane/frame-mapper.ts +0 -286
- package/src/priority.json +0 -37
package/src/async/job-manager.ts
CHANGED
|
@@ -6,6 +6,7 @@ const DELIVERY_RETRY_MAX_MS = 30_000;
|
|
|
6
6
|
const DELIVERY_RETRY_JITTER_MS = 200;
|
|
7
7
|
const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
|
|
8
8
|
const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
9
|
+
const MONITOR_TOMBSTONE_TTL_MS = 5 * 60_000;
|
|
9
10
|
|
|
10
11
|
export interface AsyncJob {
|
|
11
12
|
id: string;
|
|
@@ -120,6 +121,27 @@ export interface AsyncJobDeliveryState {
|
|
|
120
121
|
pendingJobIds: string[];
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
export interface AsyncJobLifecycleCleanup {
|
|
125
|
+
onCancel?: (job: AsyncJob) => void;
|
|
126
|
+
onTerminal?: (job: AsyncJob) => void;
|
|
127
|
+
onEvict?: (job: AsyncJob) => void;
|
|
128
|
+
/**
|
|
129
|
+
* Idempotent residual cleanup invoked by a post-eviction tombstone purge
|
|
130
|
+
* (e.g. a late `job cancel` after the job left the registry). Kept distinct
|
|
131
|
+
* from the at-most-once lifecycle phases so a tombstone purge never has to
|
|
132
|
+
* re-invoke a phase hook. Must be safe to call repeatedly.
|
|
133
|
+
*/
|
|
134
|
+
onTombstonePurge?: (job: AsyncJob) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface MonitorTombstone {
|
|
138
|
+
jobId: string;
|
|
139
|
+
ownerId?: string;
|
|
140
|
+
status: AsyncJob["status"];
|
|
141
|
+
expiresAt: number;
|
|
142
|
+
purge: () => unknown;
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
export interface AsyncJobRegisterOptions {
|
|
124
146
|
id?: string;
|
|
125
147
|
/** Registry id of the agent that owns this job; used to scope cancelAll. */
|
|
@@ -127,6 +149,7 @@ export interface AsyncJobRegisterOptions {
|
|
|
127
149
|
/** Structured metadata for tool-specific control surfaces. */
|
|
128
150
|
metadata?: AsyncJobMetadata;
|
|
129
151
|
onProgress?: (text: string, details?: Record<string, unknown>) => void | Promise<void>;
|
|
152
|
+
lifecycle?: AsyncJobLifecycleCleanup;
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
/**
|
|
@@ -214,6 +237,9 @@ export class AsyncJobManager {
|
|
|
214
237
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
215
238
|
readonly #outputState = new Map<string, AsyncJobOutputState>();
|
|
216
239
|
readonly #ownerCleanups = new Map<string, Set<() => void>>();
|
|
240
|
+
readonly #lifecycles = new Map<string, AsyncJobLifecycleCleanup>();
|
|
241
|
+
readonly #lifecyclePhases = new Map<string, Set<"cancel" | "terminal" | "evict">>();
|
|
242
|
+
readonly #monitorTombstones = new Map<string, MonitorTombstone>();
|
|
217
243
|
readonly #outputRetentionBytes = DEFAULT_JOB_OUTPUT_RETENTION_BYTES;
|
|
218
244
|
readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
|
|
219
245
|
readonly #maxRunningJobs: number;
|
|
@@ -292,6 +318,7 @@ export class AsyncJobManager {
|
|
|
292
318
|
);
|
|
293
319
|
}
|
|
294
320
|
|
|
321
|
+
this.#expireMonitorTombstones();
|
|
295
322
|
const id = this.#resolveJobId(options?.id);
|
|
296
323
|
this.#suppressedDeliveries.delete(id);
|
|
297
324
|
const abortController = new AbortController();
|
|
@@ -308,6 +335,7 @@ export class AsyncJobManager {
|
|
|
308
335
|
ownerId: options?.ownerId,
|
|
309
336
|
metadata: options?.metadata,
|
|
310
337
|
};
|
|
338
|
+
if (options?.lifecycle) this.#lifecycles.set(id, options.lifecycle);
|
|
311
339
|
|
|
312
340
|
const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
|
|
313
341
|
if (!options?.onProgress) return;
|
|
@@ -325,8 +353,10 @@ export class AsyncJobManager {
|
|
|
325
353
|
const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
|
|
326
354
|
const outcome: SubagentRunOutcome =
|
|
327
355
|
typeof result === "string" ? { kind: "completed", text: result } : result;
|
|
356
|
+
|
|
328
357
|
if (job.status === "cancelled") {
|
|
329
358
|
job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
|
|
359
|
+
this.#runLifecycle(id, "terminal");
|
|
330
360
|
this.#scheduleEviction(id);
|
|
331
361
|
this.#markRecordTerminal(id, "cancelled");
|
|
332
362
|
this.#drainResumeQueue();
|
|
@@ -342,20 +372,24 @@ export class AsyncJobManager {
|
|
|
342
372
|
this.#drainResumeQueue();
|
|
343
373
|
return;
|
|
344
374
|
}
|
|
375
|
+
|
|
345
376
|
job.status = "completed";
|
|
346
377
|
job.resultText = outcome.text;
|
|
347
378
|
this.#enqueueDelivery(id, outcome.text);
|
|
379
|
+
this.#runLifecycle(id, "terminal");
|
|
348
380
|
this.#scheduleEviction(id);
|
|
349
381
|
this.#markRecordTerminal(id, "completed");
|
|
350
382
|
this.#drainResumeQueue();
|
|
351
383
|
} catch (error) {
|
|
352
384
|
if (job.status === "cancelled") {
|
|
353
385
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
386
|
+
this.#runLifecycle(id, "terminal");
|
|
354
387
|
this.#scheduleEviction(id);
|
|
355
388
|
this.#markRecordTerminal(id, "cancelled");
|
|
356
389
|
this.#drainResumeQueue();
|
|
357
390
|
return;
|
|
358
391
|
}
|
|
392
|
+
this.#runLifecycle(id, "terminal");
|
|
359
393
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
360
394
|
job.status = "failed";
|
|
361
395
|
job.errorText = errorText;
|
|
@@ -381,6 +415,7 @@ export class AsyncJobManager {
|
|
|
381
415
|
if (!job) return false;
|
|
382
416
|
if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
|
|
383
417
|
if (job.status === "paused") {
|
|
418
|
+
this.#runLifecycle(id, "cancel");
|
|
384
419
|
// Paused jobs have no running promise to abort; transition directly.
|
|
385
420
|
// The session file is kept, so the record stays resumable by id.
|
|
386
421
|
job.status = "cancelled";
|
|
@@ -390,12 +425,76 @@ export class AsyncJobManager {
|
|
|
390
425
|
return true;
|
|
391
426
|
}
|
|
392
427
|
if (job.status !== "running") return false;
|
|
428
|
+
this.#runLifecycle(id, "cancel");
|
|
393
429
|
job.status = "cancelled";
|
|
394
430
|
job.abortController.abort();
|
|
395
|
-
this.#scheduleEviction(id);
|
|
396
431
|
return true;
|
|
397
432
|
}
|
|
398
433
|
|
|
434
|
+
#runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict"): void {
|
|
435
|
+
const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
|
|
436
|
+
if (fired.has(phase)) return;
|
|
437
|
+
fired.add(phase);
|
|
438
|
+
this.#lifecyclePhases.set(jobId, fired);
|
|
439
|
+
const lifecycle = this.#lifecycles.get(jobId);
|
|
440
|
+
const job = this.#jobs.get(jobId);
|
|
441
|
+
if (!lifecycle || !job) return;
|
|
442
|
+
try {
|
|
443
|
+
if (phase === "cancel") lifecycle.onCancel?.(job);
|
|
444
|
+
else if (phase === "terminal") lifecycle.onTerminal?.(job);
|
|
445
|
+
else lifecycle.onEvict?.(job);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
logger.warn("Async job lifecycle cleanup failed", {
|
|
448
|
+
jobId,
|
|
449
|
+
phase,
|
|
450
|
+
error: error instanceof Error ? error.message : String(error),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#expireMonitorTombstones(): void {
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
for (const [jobId, tombstone] of this.#monitorTombstones) {
|
|
458
|
+
if (tombstone.expiresAt <= now) this.#monitorTombstones.delete(jobId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#recordMonitorTombstone(jobId: string): void {
|
|
463
|
+
const job = this.#jobs.get(jobId);
|
|
464
|
+
if (!job?.metadata?.monitor) return;
|
|
465
|
+
const lifecycle = this.#lifecycles.get(jobId);
|
|
466
|
+
this.#monitorTombstones.set(jobId, {
|
|
467
|
+
jobId,
|
|
468
|
+
ownerId: job.ownerId,
|
|
469
|
+
status: job.status,
|
|
470
|
+
expiresAt: Date.now() + MONITOR_TOMBSTONE_TTL_MS,
|
|
471
|
+
purge: () => (lifecycle?.onTombstonePurge ?? lifecycle?.onEvict)?.(job),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
getMonitorTombstone(jobId: string, filter?: AsyncJobFilter): MonitorTombstone | undefined {
|
|
476
|
+
this.#expireMonitorTombstones();
|
|
477
|
+
const tombstone = this.#monitorTombstones.get(jobId);
|
|
478
|
+
if (!tombstone) return undefined;
|
|
479
|
+
if (filter?.ownerId && tombstone.ownerId !== filter.ownerId) return undefined;
|
|
480
|
+
return tombstone;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
purgeMonitorTombstone(jobId: string, filter?: AsyncJobFilter): { found: boolean; status?: AsyncJob["status"] } {
|
|
484
|
+
const tombstone = this.getMonitorTombstone(jobId, filter);
|
|
485
|
+
if (!tombstone) return { found: false };
|
|
486
|
+
this.#monitorTombstones.delete(jobId);
|
|
487
|
+
try {
|
|
488
|
+
tombstone.purge();
|
|
489
|
+
} catch (error) {
|
|
490
|
+
logger.warn("Monitor tombstone purge failed", {
|
|
491
|
+
jobId,
|
|
492
|
+
error: error instanceof Error ? error.message : String(error),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return { found: true, status: tombstone.status };
|
|
496
|
+
}
|
|
497
|
+
|
|
399
498
|
// ── Subagent control plane (pause / resume / steer support) ──────────
|
|
400
499
|
|
|
401
500
|
/** Register or replace the canonical record for a subagent. */
|
|
@@ -836,6 +935,7 @@ export class AsyncJobManager {
|
|
|
836
935
|
*/
|
|
837
936
|
cancelAll(filter?: AsyncJobFilter): void {
|
|
838
937
|
for (const job of this.getRunningJobs(filter)) {
|
|
938
|
+
this.#runLifecycle(job.id, "cancel");
|
|
839
939
|
job.status = "cancelled";
|
|
840
940
|
job.abortController.abort();
|
|
841
941
|
this.#scheduleEviction(job.id);
|
|
@@ -898,6 +998,17 @@ export class AsyncJobManager {
|
|
|
898
998
|
// manager. Errors in cleanup callbacks are logged but never escalated.
|
|
899
999
|
this.runOwnerCleanups();
|
|
900
1000
|
this.cancelAll();
|
|
1001
|
+
for (const tombstone of this.#monitorTombstones.values()) {
|
|
1002
|
+
try {
|
|
1003
|
+
tombstone.purge();
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
logger.warn("Monitor tombstone purge failed during dispose", {
|
|
1006
|
+
jobId: tombstone.jobId,
|
|
1007
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
this.#monitorTombstones.clear();
|
|
901
1012
|
await this.waitForAll();
|
|
902
1013
|
const drained = await this.drainDeliveries({ timeoutMs: options?.timeoutMs ?? 3_000 });
|
|
903
1014
|
this.#clearEvictionTimers();
|
|
@@ -945,7 +1056,11 @@ export class AsyncJobManager {
|
|
|
945
1056
|
#scheduleEviction(jobId: string): void {
|
|
946
1057
|
this.#notifyChange();
|
|
947
1058
|
if (this.#retentionMs <= 0) {
|
|
1059
|
+
this.#recordMonitorTombstone(jobId);
|
|
1060
|
+
this.#runLifecycle(jobId, "evict");
|
|
948
1061
|
this.#jobs.delete(jobId);
|
|
1062
|
+
this.#lifecycles.delete(jobId);
|
|
1063
|
+
this.#lifecyclePhases.delete(jobId);
|
|
949
1064
|
this.#suppressedDeliveries.delete(jobId);
|
|
950
1065
|
this.#watchedJobs.delete(jobId);
|
|
951
1066
|
this.#outputState.delete(jobId);
|
|
@@ -957,7 +1072,11 @@ export class AsyncJobManager {
|
|
|
957
1072
|
}
|
|
958
1073
|
const timer = setTimeout(() => {
|
|
959
1074
|
this.#evictionTimers.delete(jobId);
|
|
1075
|
+
this.#recordMonitorTombstone(jobId);
|
|
1076
|
+
this.#runLifecycle(jobId, "evict");
|
|
960
1077
|
this.#jobs.delete(jobId);
|
|
1078
|
+
this.#lifecycles.delete(jobId);
|
|
1079
|
+
this.#lifecyclePhases.delete(jobId);
|
|
961
1080
|
this.#suppressedDeliveries.delete(jobId);
|
|
962
1081
|
this.#watchedJobs.delete(jobId);
|
|
963
1082
|
this.#outputState.delete(jobId);
|
package/src/cli/grep-cli.ts
CHANGED
|
@@ -98,7 +98,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
|
|
|
98
98
|
console.log(chalk.green(`Files with matches: ${result.filesWithMatches}`));
|
|
99
99
|
console.log(chalk.green(`Files searched: ${result.filesSearched}`));
|
|
100
100
|
if (result.limitReached) {
|
|
101
|
-
console.log(chalk.yellow(`
|
|
101
|
+
console.log(chalk.yellow(`Result limit reached (${cmd.limit} matches). Use --limit to show more.`));
|
|
102
102
|
}
|
|
103
103
|
console.log("");
|
|
104
104
|
|
package/src/commands/harness.ts
CHANGED
|
@@ -241,6 +241,12 @@ interface OwnerExitEvidence {
|
|
|
241
241
|
lastSignal: string | null;
|
|
242
242
|
promptAcceptedSeen: boolean;
|
|
243
243
|
completedSeen: boolean;
|
|
244
|
+
/** True only when the owner is genuinely gone (lease missing or process dead). */
|
|
245
|
+
terminal: boolean;
|
|
246
|
+
/** True when the owner process is provably alive (live lease + fresh heartbeat) but the endpoint did not route. */
|
|
247
|
+
transient: boolean;
|
|
248
|
+
/** ISO timestamp of the most recent non-terminal RPC-derived owner event, if any (observability only). */
|
|
249
|
+
lastRpcActivityAt: string | null;
|
|
244
250
|
}
|
|
245
251
|
|
|
246
252
|
async function buildOwnerExitEvidence(root: string, state: SessionState): Promise<OwnerExitEvidence> {
|
|
@@ -251,12 +257,23 @@ async function buildOwnerExitEvidence(root: string, state: SessionState): Promis
|
|
|
251
257
|
let lastSignal: string | null = null;
|
|
252
258
|
let promptAcceptedSeen = false;
|
|
253
259
|
let completedSeen = false;
|
|
260
|
+
let lastRpcActivityAt: string | null = null;
|
|
254
261
|
for (const event of events) {
|
|
255
262
|
const signal = (event.evidence as { signal?: unknown } | undefined)?.signal;
|
|
256
263
|
if (typeof signal === "string") lastSignal = signal;
|
|
257
264
|
if (event.kind === "prompt_accepted" || signal === "prompt-accepted") promptAcceptedSeen = true;
|
|
258
265
|
if (event.kind === "rpc_agent_completed" || signal === "completed") completedSeen = true;
|
|
266
|
+
// Terminal completion/failure frames are NOT owner liveness — exclude them from activity.
|
|
267
|
+
if (event.kind.startsWith("rpc_") && event.kind !== "rpc_agent_completed" && event.kind !== "rpc_agent_failed") {
|
|
268
|
+
lastRpcActivityAt = event.createdAt;
|
|
269
|
+
}
|
|
259
270
|
}
|
|
271
|
+
// Owner liveness is the lease heartbeat, never RPC frames: a "live" lease means the owner process
|
|
272
|
+
// is alive and heartbeating within TTL, so a failed endpoint call is a transient observation gap.
|
|
273
|
+
// Real owner loss (missing/dead lease) stays terminal and keeps its original reason string so
|
|
274
|
+
// existing consumers that match on the reason continue to escalate.
|
|
275
|
+
const terminal = !lease || leaseStatus === "dead";
|
|
276
|
+
const transient = leaseStatus === "live";
|
|
260
277
|
let reason = "owner-not-live";
|
|
261
278
|
if (!lease) {
|
|
262
279
|
reason = promptAcceptedSeen && !completedSeen ? "owner-exited-after-prompt-acceptance" : "owner-lease-missing";
|
|
@@ -281,6 +298,9 @@ async function buildOwnerExitEvidence(root: string, state: SessionState): Promis
|
|
|
281
298
|
lastSignal,
|
|
282
299
|
promptAcceptedSeen,
|
|
283
300
|
completedSeen,
|
|
301
|
+
terminal,
|
|
302
|
+
transient,
|
|
303
|
+
lastRpcActivityAt,
|
|
284
304
|
};
|
|
285
305
|
}
|
|
286
306
|
|
|
@@ -684,6 +704,7 @@ export default class Harness extends Command {
|
|
|
684
704
|
const handle: SessionHandle = {
|
|
685
705
|
sessionId,
|
|
686
706
|
harness,
|
|
707
|
+
mode: input.mode === "review" || input.reviewOnly === true ? "review" : "implement",
|
|
687
708
|
repo: typeof input.repo === "string" ? input.repo : null,
|
|
688
709
|
workspace,
|
|
689
710
|
branch: preflight.declaredBranch ?? preflight.actualBranch,
|
|
@@ -953,9 +974,27 @@ export default class Harness extends Command {
|
|
|
953
974
|
observation: { ...observation, lifecycle: state.lifecycle },
|
|
954
975
|
retryBudget: budget,
|
|
955
976
|
});
|
|
956
|
-
|
|
977
|
+
// A session persisted as `started` whose owner was never spawned (no lease,
|
|
978
|
+
// no endpoint, no owner-run evidence) is not a vanish — it simply never had
|
|
979
|
+
// an owner. Bootstrap a fresh owner instead of deadlocking on the missing
|
|
980
|
+
// prior endpoint (which `start` without `--detach` never records).
|
|
981
|
+
const ownerNeverStarted =
|
|
982
|
+
state.lifecycle === "started" &&
|
|
983
|
+
!beforeExit.endpointPresent &&
|
|
984
|
+
!beforeExit.promptAcceptedSeen &&
|
|
985
|
+
!beforeExit.completedSeen &&
|
|
986
|
+
beforeExit.lastEventKind === null &&
|
|
987
|
+
observation.risk !== "deleted-worktree";
|
|
988
|
+
// Bootstrapping a never-started owner is not a vanish, so it needs no vanish receipt.
|
|
989
|
+
const vanishReceiptId = ownerNeverStarted
|
|
990
|
+
? null
|
|
991
|
+
: await writeVanishReceiptForDecision(root, state, observation, decision.classification);
|
|
992
|
+
// A never-started owner has no in-flight work to preserve, so bootstrapping it does not
|
|
993
|
+
// depend on the vanish classifier's `ownerRequired` verdict — that gate exists to protect a
|
|
994
|
+
// vanished owner's worktree. Without this, a session started in a non-git workspace (git
|
|
995
|
+
// delta `unknown` → classifier `human-check` with `ownerRequired: false`) would stay stuck.
|
|
957
996
|
const restoredOwner =
|
|
958
|
-
decision.ownerRequired && beforeExit.endpointPresent
|
|
997
|
+
ownerNeverStarted || (decision.ownerRequired && beforeExit.endpointPresent)
|
|
959
998
|
? await this.#spawnDetachedOwner(root, sessionId, state.handle.workspace)
|
|
960
999
|
: null;
|
|
961
1000
|
if (restoredOwner?.live) {
|
|
@@ -968,7 +1007,7 @@ export default class Harness extends Command {
|
|
|
968
1007
|
writeJson(
|
|
969
1008
|
buildResponse(state, true, {
|
|
970
1009
|
pending: false,
|
|
971
|
-
restoredOwner: true,
|
|
1010
|
+
...(ownerNeverStarted ? { bootstrappedOwner: true } : { restoredOwner: true }),
|
|
972
1011
|
decision,
|
|
973
1012
|
observation: { ...observation, lifecycle: state.lifecycle, ownerLive: true },
|
|
974
1013
|
ownerExit: beforeExit,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "@gajae-code/utils/cli";
|
|
2
2
|
import {
|
|
3
3
|
GJC_SESSION_FILE_ENV,
|
|
4
|
+
GJC_SESSION_ID_ENV,
|
|
4
5
|
isUltragoalCreateGoalsInvocation,
|
|
5
6
|
readUltragoalGjcObjective,
|
|
6
7
|
writeCurrentSessionGoalModeState,
|
|
@@ -12,6 +13,7 @@ export default class Ultragoal extends Command {
|
|
|
12
13
|
static description = "Run native GJC Ultragoal workflow commands";
|
|
13
14
|
static strict = false;
|
|
14
15
|
static examples = ["$ gjc ultragoal status --json"];
|
|
16
|
+
static delegateHelp = true;
|
|
15
17
|
|
|
16
18
|
async run(): Promise<void> {
|
|
17
19
|
const shouldActivateGoalMode = isUltragoalCreateGoalsInvocation(this.argv);
|
|
@@ -27,6 +29,11 @@ export default class Ultragoal extends Command {
|
|
|
27
29
|
sessionFile: process.env[GJC_SESSION_FILE_ENV],
|
|
28
30
|
objective,
|
|
29
31
|
});
|
|
30
|
-
await writePendingGoalModeRequest({
|
|
32
|
+
await writePendingGoalModeRequest({
|
|
33
|
+
cwd,
|
|
34
|
+
objective,
|
|
35
|
+
goalsPath,
|
|
36
|
+
sessionId: process.env[GJC_SESSION_ID_ENV],
|
|
37
|
+
});
|
|
31
38
|
}
|
|
32
39
|
}
|
|
@@ -5,7 +5,7 @@ import { applyChangelogProposals } from "../../commit/changelog";
|
|
|
5
5
|
import { detectChangelogBoundaries } from "../../commit/changelog/detect";
|
|
6
6
|
import { parseUnreleasedSection } from "../../commit/changelog/parse";
|
|
7
7
|
import { formatCommitMessage } from "../../commit/message";
|
|
8
|
-
import { resolvePrimaryModel,
|
|
8
|
+
import { resolvePrimaryModel, resolveSecondaryCommitModel } from "../../commit/model-selection";
|
|
9
9
|
import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
|
|
10
10
|
import { ModelRegistry } from "../../config/model-registry";
|
|
11
11
|
import { Settings } from "../../config/settings";
|
|
@@ -47,7 +47,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
47
47
|
const { model: primaryModel, apiKey: primaryApiKey } = primaryModelResult;
|
|
48
48
|
process.stdout.write(` └─ ${primaryModel.name}\n`);
|
|
49
49
|
|
|
50
|
-
const { model: agentModel, thinkingLevel: agentThinkingLevel } = await
|
|
50
|
+
const { model: agentModel, thinkingLevel: agentThinkingLevel } = await resolveSecondaryCommitModel(
|
|
51
51
|
settings,
|
|
52
52
|
modelRegistry,
|
|
53
53
|
primaryModel,
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Api, Model } from "@gajae-code/ai";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
type ModelLookupRegistry,
|
|
6
|
-
parseModelPattern,
|
|
7
|
-
resolveModelRoleValue,
|
|
8
|
-
resolveRoleSelection,
|
|
9
|
-
} from "../config/model-resolver";
|
|
3
|
+
import { type ModelLookupRegistry, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver";
|
|
10
4
|
import type { Settings } from "../config/settings";
|
|
11
|
-
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
12
5
|
|
|
13
6
|
export interface ResolvedCommitModel {
|
|
14
7
|
model: Model<Api>;
|
|
@@ -29,7 +22,7 @@ export async function resolvePrimaryModel(
|
|
|
29
22
|
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
30
23
|
const resolved = override
|
|
31
24
|
? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
|
|
32
|
-
: resolveRoleSelection(["
|
|
25
|
+
: resolveRoleSelection(["default"], settings, available, modelRegistry);
|
|
33
26
|
const model = resolved?.model;
|
|
34
27
|
if (!model) {
|
|
35
28
|
throw new Error("No model available for commit generation");
|
|
@@ -41,25 +34,17 @@ export async function resolvePrimaryModel(
|
|
|
41
34
|
return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
export async function
|
|
37
|
+
export async function resolveSecondaryCommitModel(
|
|
45
38
|
settings: Settings,
|
|
46
39
|
modelRegistry: CommitModelRegistry,
|
|
47
40
|
fallbackModel: Model<Api>,
|
|
48
41
|
fallbackApiKey: string,
|
|
49
42
|
): Promise<ResolvedCommitModel> {
|
|
50
43
|
const available = modelRegistry.getAvailable();
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
const apiKey = await modelRegistry.getApiKey(
|
|
54
|
-
if (apiKey) return { model:
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
58
|
-
for (const pattern of MODEL_PRIO.smol) {
|
|
59
|
-
const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
|
|
60
|
-
if (!candidate) continue;
|
|
61
|
-
const apiKey = await modelRegistry.getApiKey(candidate);
|
|
62
|
-
if (apiKey) return { model: candidate, apiKey };
|
|
44
|
+
const resolved = resolveRoleSelection(["default"], settings, available, modelRegistry);
|
|
45
|
+
if (resolved?.model) {
|
|
46
|
+
const apiKey = await modelRegistry.getApiKey(resolved.model);
|
|
47
|
+
if (apiKey) return { model: resolved.model, apiKey, thinkingLevel: resolved.thinkingLevel };
|
|
63
48
|
}
|
|
64
49
|
|
|
65
50
|
return { model: fallbackModel, apiKey: fallbackApiKey };
|
package/src/commit/pipeline.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { runChangelogFlow } from "./changelog";
|
|
19
19
|
import { runMapReduceAnalysis, shouldUseMapReduce } from "./map-reduce";
|
|
20
20
|
import { formatCommitMessage } from "./message";
|
|
21
|
-
import { resolvePrimaryModel,
|
|
21
|
+
import { resolvePrimaryModel, resolveSecondaryCommitModel } from "./model-selection";
|
|
22
22
|
import summaryRetryPrompt from "./prompts/summary-retry.md" with { type: "text" };
|
|
23
23
|
import typesDescriptionPrompt from "./prompts/types-description.md" with { type: "text" };
|
|
24
24
|
import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
|
|
@@ -56,7 +56,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
|
56
56
|
model: smolModel,
|
|
57
57
|
apiKey: smolApiKey,
|
|
58
58
|
thinkingLevel: smolThinkingLevel,
|
|
59
|
-
} = await
|
|
59
|
+
} = await resolveSecondaryCommitModel(settings, modelRegistry, primaryModel, primaryApiKey);
|
|
60
60
|
|
|
61
61
|
let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
|
|
62
62
|
if (stagedFiles.length === 0) {
|
|
@@ -62,7 +62,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
|
|
|
62
62
|
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export type ModelRole = "default"
|
|
65
|
+
export type ModelRole = "default";
|
|
66
66
|
|
|
67
67
|
export interface ModelRoleInfo {
|
|
68
68
|
tag?: string;
|
|
@@ -72,16 +72,9 @@ export interface ModelRoleInfo {
|
|
|
72
72
|
|
|
73
73
|
export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
74
74
|
default: { tag: "DEFAULT", name: "Default", color: "success" },
|
|
75
|
-
smol: { tag: "SMOL", name: "Fast", color: "warning" },
|
|
76
|
-
slow: { tag: "SLOW", name: "Thinking", color: "accent" },
|
|
77
|
-
vision: { tag: "VISION", name: "Vision", color: "error" },
|
|
78
|
-
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
79
|
-
designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
|
|
80
|
-
commit: { tag: "COMMIT", name: "Commit", color: "dim" },
|
|
81
|
-
task: { tag: "TASK", name: "Subtask", color: "muted" },
|
|
82
75
|
};
|
|
83
76
|
|
|
84
|
-
export const MODEL_ROLE_IDS: ModelRole[] = ["default"
|
|
77
|
+
export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
|
|
85
78
|
|
|
86
79
|
export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
|
|
87
80
|
|
|
@@ -688,6 +681,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
688
681
|
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
|
|
689
682
|
if (override.thinking !== undefined) result.thinking = override.thinking as ThinkingConfig;
|
|
690
683
|
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
|
684
|
+
if (override.output !== undefined) result.output = override.output as ("text" | "image")[];
|
|
691
685
|
if (override.cacheRetention !== undefined) result.cacheRetention = override.cacheRetention;
|
|
692
686
|
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
|
693
687
|
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
@@ -718,6 +712,7 @@ interface CustomModelDefinitionLike {
|
|
|
718
712
|
reasoning?: boolean;
|
|
719
713
|
thinking?: ThinkingConfig;
|
|
720
714
|
input?: ("text" | "image")[];
|
|
715
|
+
output?: ("text" | "image")[];
|
|
721
716
|
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
722
717
|
contextWindow?: number;
|
|
723
718
|
maxTokens?: number;
|
|
@@ -743,6 +738,7 @@ type CustomModelOverlay = {
|
|
|
743
738
|
reasoning?: boolean;
|
|
744
739
|
thinking?: ThinkingConfig;
|
|
745
740
|
input?: ("text" | "image")[];
|
|
741
|
+
output?: ("text" | "image")[];
|
|
746
742
|
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
747
743
|
contextWindow?: number;
|
|
748
744
|
maxTokens?: number;
|
|
@@ -818,6 +814,7 @@ function buildCustomModelOverlay(
|
|
|
818
814
|
reasoning: modelDef.reasoning,
|
|
819
815
|
thinking: modelDef.thinking as ThinkingConfig | undefined,
|
|
820
816
|
input: modelDef.input as ("text" | "image")[] | undefined,
|
|
817
|
+
output: modelDef.output as ("text" | "image")[] | undefined,
|
|
821
818
|
cost: modelDef.cost,
|
|
822
819
|
contextWindow: modelDef.contextWindow,
|
|
823
820
|
maxTokens: modelDef.maxTokens,
|
|
@@ -910,6 +907,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
910
907
|
reference?.cost ??
|
|
911
908
|
(options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
912
909
|
const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
|
|
910
|
+
const output = resolvedModel.output ?? reference?.output;
|
|
913
911
|
return enrichModelThinking({
|
|
914
912
|
id: resolvedModel.id,
|
|
915
913
|
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
@@ -919,6 +917,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
919
917
|
reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
|
|
920
918
|
thinking: resolvedModel.thinking ?? reference?.thinking,
|
|
921
919
|
input: input as ("text" | "image")[],
|
|
920
|
+
output: output as ("text" | "image")[] | undefined,
|
|
922
921
|
cost,
|
|
923
922
|
contextWindow:
|
|
924
923
|
resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : undefined),
|
|
@@ -1213,6 +1212,7 @@ export class ModelRegistry {
|
|
|
1213
1212
|
reasoning: customModel.reasoning ?? existingModel.reasoning,
|
|
1214
1213
|
thinking: customModel.thinking ?? existingModel.thinking,
|
|
1215
1214
|
input: customModel.input ?? existingModel.input,
|
|
1215
|
+
output: customModel.output ?? existingModel.output,
|
|
1216
1216
|
cost: customModel.cost ?? existingModel.cost,
|
|
1217
1217
|
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
1218
1218
|
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
@@ -2325,6 +2325,14 @@ export class ModelRegistry {
|
|
|
2325
2325
|
fallback: 3,
|
|
2326
2326
|
};
|
|
2327
2327
|
return [...variants].sort((left, right) => {
|
|
2328
|
+
// Prefer vision-capable variants over configured provider order so an
|
|
2329
|
+
// ambiguous canonical id never resolves to a text-only namesake when a
|
|
2330
|
+
// vision-capable variant of the same id is available.
|
|
2331
|
+
const leftVision = left.model.input.includes("image") ? 0 : 1;
|
|
2332
|
+
const rightVision = right.model.input.includes("image") ? 0 : 1;
|
|
2333
|
+
if (leftVision !== rightVision) {
|
|
2334
|
+
return leftVision - rightVision;
|
|
2335
|
+
}
|
|
2328
2336
|
const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2329
2337
|
const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2330
2338
|
if (leftProviderRank !== rightProviderRank) {
|