@gajae-code/coding-agent 0.5.1 → 0.5.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 +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
package/src/async/job-manager.ts
CHANGED
|
@@ -7,6 +7,11 @@ const DELIVERY_RETRY_JITTER_MS = 200;
|
|
|
7
7
|
const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
|
|
8
8
|
const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
9
9
|
const MONITOR_TOMBSTONE_TTL_MS = 5 * 60_000;
|
|
10
|
+
const DEFAULT_MAX_DELIVERY_QUEUE = 100;
|
|
11
|
+
const DELIVERY_MAX_TEXT_BYTES = 64 * 1024;
|
|
12
|
+
const DELIVERY_PREVIEW_HEAD_BYTES = 32 * 1024;
|
|
13
|
+
const DELIVERY_PREVIEW_TAIL_BYTES = 32 * 1024;
|
|
14
|
+
const DELIVERY_MAX_ATTEMPTS = 3;
|
|
10
15
|
|
|
11
16
|
export interface AsyncJob {
|
|
12
17
|
id: string;
|
|
@@ -128,9 +133,16 @@ export interface AsyncJobManagerOptions {
|
|
|
128
133
|
retentionMs?: number;
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
export interface AsyncJobDisposeDiagnostics {
|
|
137
|
+
stuckJobIds: string[];
|
|
138
|
+
deliveriesDrained: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
interface AsyncJobDelivery {
|
|
132
142
|
jobId: string;
|
|
133
143
|
text: string;
|
|
144
|
+
originalBytes?: number;
|
|
145
|
+
truncated?: boolean;
|
|
134
146
|
attempt: number;
|
|
135
147
|
nextAttemptAt: number;
|
|
136
148
|
lastError?: string;
|
|
@@ -143,6 +155,7 @@ export interface AsyncJobDeliveryState {
|
|
|
143
155
|
delivering: boolean;
|
|
144
156
|
nextRetryAt?: number;
|
|
145
157
|
pendingJobIds: string[];
|
|
158
|
+
deadLettered: number;
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
export interface AsyncJobLifecycleCleanup {
|
|
@@ -198,6 +211,32 @@ function sliceTextFromUtf8ByteOffset(text: string, offsetBytes: number): string
|
|
|
198
211
|
return text.slice(codeUnitIndex);
|
|
199
212
|
}
|
|
200
213
|
|
|
214
|
+
function sliceTextAfterUtf8ByteOffset(text: string, offsetBytes: number): string {
|
|
215
|
+
if (offsetBytes <= 0) return text;
|
|
216
|
+
let consumedBytes = 0;
|
|
217
|
+
let codeUnitIndex = 0;
|
|
218
|
+
for (const char of text) {
|
|
219
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
220
|
+
consumedBytes += charBytes;
|
|
221
|
+
codeUnitIndex += char.length;
|
|
222
|
+
if (consumedBytes >= offsetBytes) break;
|
|
223
|
+
}
|
|
224
|
+
return text.slice(codeUnitIndex);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sliceTextToUtf8ByteLength(text: string, maxBytes: number): string {
|
|
228
|
+
if (maxBytes <= 0) return "";
|
|
229
|
+
let consumedBytes = 0;
|
|
230
|
+
let codeUnitIndex = 0;
|
|
231
|
+
for (const char of text) {
|
|
232
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
233
|
+
if (consumedBytes + charBytes > maxBytes) break;
|
|
234
|
+
consumedBytes += charBytes;
|
|
235
|
+
codeUnitIndex += char.length;
|
|
236
|
+
}
|
|
237
|
+
return text.slice(0, codeUnitIndex);
|
|
238
|
+
}
|
|
239
|
+
|
|
201
240
|
/**
|
|
202
241
|
* A slice of process-stream output for a background job, as recorded by
|
|
203
242
|
* `appendOutput` / read by `readOutputSince`.
|
|
@@ -277,6 +316,8 @@ export class AsyncJobManager {
|
|
|
277
316
|
#resumeSeq = 0;
|
|
278
317
|
#resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
|
|
279
318
|
readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
|
|
319
|
+
readonly #deadLetteredDeliveries = new Map<string, AsyncJobDelivery>();
|
|
320
|
+
#lastDisposeDiagnostics: AsyncJobDisposeDiagnostics = { stuckJobIds: [], deliveriesDrained: true };
|
|
280
321
|
/**
|
|
281
322
|
* Change listeners notified on any mutation that can alter the live job set
|
|
282
323
|
* (register, terminal/eviction transitions, dispose). Used by the status-line
|
|
@@ -381,7 +422,7 @@ export class AsyncJobManager {
|
|
|
381
422
|
|
|
382
423
|
if (job.status === "cancelled") {
|
|
383
424
|
job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
|
|
384
|
-
this.#runLifecycle(id, "terminal");
|
|
425
|
+
this.#runLifecycle(id, "terminal", job);
|
|
385
426
|
this.#scheduleEviction(id);
|
|
386
427
|
this.#markRecordTerminal(id, "cancelled");
|
|
387
428
|
this.#drainResumeQueue();
|
|
@@ -403,20 +444,20 @@ export class AsyncJobManager {
|
|
|
403
444
|
this.#freezeEndTime(job);
|
|
404
445
|
job.resultText = outcome.text;
|
|
405
446
|
this.#enqueueDelivery(id, outcome.text);
|
|
406
|
-
this.#runLifecycle(id, "terminal");
|
|
447
|
+
this.#runLifecycle(id, "terminal", job);
|
|
407
448
|
this.#scheduleEviction(id);
|
|
408
449
|
this.#markRecordTerminal(id, "completed");
|
|
409
450
|
this.#drainResumeQueue();
|
|
410
451
|
} catch (error) {
|
|
411
452
|
if (job.status === "cancelled") {
|
|
412
453
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
413
|
-
this.#runLifecycle(id, "terminal");
|
|
454
|
+
this.#runLifecycle(id, "terminal", job);
|
|
414
455
|
this.#scheduleEviction(id);
|
|
415
456
|
this.#markRecordTerminal(id, "cancelled");
|
|
416
457
|
this.#drainResumeQueue();
|
|
417
458
|
return;
|
|
418
459
|
}
|
|
419
|
-
this.#runLifecycle(id, "terminal");
|
|
460
|
+
this.#runLifecycle(id, "terminal", job);
|
|
420
461
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
421
462
|
job.status = "failed";
|
|
422
463
|
this.#freezeEndTime(job);
|
|
@@ -471,14 +512,14 @@ export class AsyncJobManager {
|
|
|
471
512
|
job.endTime ??= Date.now();
|
|
472
513
|
}
|
|
473
514
|
|
|
474
|
-
#runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict"): void {
|
|
515
|
+
#runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict", jobOverride?: AsyncJob): void {
|
|
516
|
+
const lifecycle = this.#lifecycles.get(jobId);
|
|
517
|
+
const job = jobOverride ?? this.#jobs.get(jobId);
|
|
518
|
+
if (!lifecycle || !job) return;
|
|
475
519
|
const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
|
|
476
520
|
if (fired.has(phase)) return;
|
|
477
521
|
fired.add(phase);
|
|
478
522
|
this.#lifecyclePhases.set(jobId, fired);
|
|
479
|
-
const lifecycle = this.#lifecycles.get(jobId);
|
|
480
|
-
const job = this.#jobs.get(jobId);
|
|
481
|
-
if (!lifecycle || !job) return;
|
|
482
523
|
try {
|
|
483
524
|
if (phase === "cancel") lifecycle.onCancel?.(job);
|
|
484
525
|
else if (phase === "terminal") lifecycle.onTerminal?.(job);
|
|
@@ -649,6 +690,16 @@ export class AsyncJobManager {
|
|
|
649
690
|
}
|
|
650
691
|
}
|
|
651
692
|
|
|
693
|
+
#purgeTerminalSubagentStateForJob(jobId: string): void {
|
|
694
|
+
const rec = this.#recordByJobId(jobId);
|
|
695
|
+
if (!rec) return;
|
|
696
|
+
if (rec.status === "paused" || rec.status === "queued") return;
|
|
697
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
698
|
+
this.#subagentProgress.delete(rec.subagentId);
|
|
699
|
+
this.#resumeDescriptors.delete(rec.subagentId);
|
|
700
|
+
this.#subagentRecords.delete(rec.subagentId);
|
|
701
|
+
}
|
|
702
|
+
|
|
652
703
|
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
|
|
653
704
|
const rec = this.#recordByJobId(jobId);
|
|
654
705
|
if (!rec) return;
|
|
@@ -967,6 +1018,10 @@ export class AsyncJobManager {
|
|
|
967
1018
|
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
968
1019
|
const deliveries = this.#filterDeliveries(filter);
|
|
969
1020
|
const inFlightDeliveries = this.#filterInFlightDeliveries(filter);
|
|
1021
|
+
const ownerId = filter?.ownerId;
|
|
1022
|
+
const deadLettered = Array.from(this.#deadLetteredDeliveries.values()).filter(
|
|
1023
|
+
delivery => !ownerId || delivery.ownerId === ownerId,
|
|
1024
|
+
).length;
|
|
970
1025
|
const nextRetryAt = deliveries.reduce<number | undefined>((next, delivery) => {
|
|
971
1026
|
if (next === undefined) return delivery.nextAttemptAt;
|
|
972
1027
|
return Math.min(next, delivery.nextAttemptAt);
|
|
@@ -977,6 +1032,7 @@ export class AsyncJobManager {
|
|
|
977
1032
|
delivering: inFlightDeliveries.length > 0 || (this.#deliveryLoop !== undefined && deliveries.length > 0),
|
|
978
1033
|
nextRetryAt,
|
|
979
1034
|
pendingJobIds: deliveries.concat(inFlightDeliveries).map(delivery => delivery.jobId),
|
|
1035
|
+
deadLettered,
|
|
980
1036
|
};
|
|
981
1037
|
}
|
|
982
1038
|
|
|
@@ -1035,6 +1091,29 @@ export class AsyncJobManager {
|
|
|
1035
1091
|
}
|
|
1036
1092
|
}
|
|
1037
1093
|
|
|
1094
|
+
getLastDisposeDiagnostics(): AsyncJobDisposeDiagnostics {
|
|
1095
|
+
return { ...this.#lastDisposeDiagnostics, stuckJobIds: [...this.#lastDisposeDiagnostics.stuckJobIds] };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async #waitForAllWithDeadline(timeoutMs: number): Promise<{ completed: boolean; stuckJobIds: string[] }> {
|
|
1099
|
+
const jobs = Array.from(this.#jobs.values());
|
|
1100
|
+
if (jobs.length === 0) return { completed: true, stuckJobIds: [] };
|
|
1101
|
+
let timedOut = false;
|
|
1102
|
+
await Promise.race([
|
|
1103
|
+
Promise.allSettled(jobs.map(job => job.promise)),
|
|
1104
|
+
Bun.sleep(Math.max(0, timeoutMs)).then(() => {
|
|
1105
|
+
timedOut = true;
|
|
1106
|
+
}),
|
|
1107
|
+
]);
|
|
1108
|
+
if (!timedOut) return { completed: true, stuckJobIds: [] };
|
|
1109
|
+
return {
|
|
1110
|
+
completed: false,
|
|
1111
|
+
stuckJobIds: Array.from(this.#jobs.values())
|
|
1112
|
+
.filter(job => job.status === "running" || job.status === "cancelled")
|
|
1113
|
+
.map(job => job.id),
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1038
1117
|
async waitForAll(): Promise<void> {
|
|
1039
1118
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
1040
1119
|
}
|
|
@@ -1102,12 +1181,18 @@ export class AsyncJobManager {
|
|
|
1102
1181
|
}
|
|
1103
1182
|
}
|
|
1104
1183
|
this.#monitorTombstones.clear();
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1184
|
+
const timeoutMs = options?.timeoutMs ?? 3_000;
|
|
1185
|
+
const waitResult = await this.#waitForAllWithDeadline(timeoutMs);
|
|
1186
|
+
const drained = waitResult.completed ? await this.drainDeliveries({ timeoutMs }) : false;
|
|
1187
|
+
this.#lastDisposeDiagnostics = { stuckJobIds: waitResult.stuckJobIds, deliveriesDrained: drained };
|
|
1188
|
+
if (waitResult.stuckJobIds.length > 0) {
|
|
1189
|
+
logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds: waitResult.stuckJobIds });
|
|
1190
|
+
}
|
|
1107
1191
|
this.#clearEvictionTimers();
|
|
1108
1192
|
this.#jobs.clear();
|
|
1109
1193
|
this.#deliveries.length = 0;
|
|
1110
1194
|
this.#inFlightDeliveries.length = 0;
|
|
1195
|
+
this.#deadLetteredDeliveries.clear();
|
|
1111
1196
|
this.#suppressedDeliveries.clear();
|
|
1112
1197
|
this.#watchedJobs.clear();
|
|
1113
1198
|
this.#outputState.clear();
|
|
@@ -1119,7 +1204,7 @@ export class AsyncJobManager {
|
|
|
1119
1204
|
this.#resumeQueue.length = 0;
|
|
1120
1205
|
this.#notifyChange();
|
|
1121
1206
|
this.#changeListeners.clear();
|
|
1122
|
-
return drained;
|
|
1207
|
+
return drained && waitResult.completed;
|
|
1123
1208
|
}
|
|
1124
1209
|
|
|
1125
1210
|
#resolveJobId(preferredId?: string): string {
|
|
@@ -1148,16 +1233,10 @@ export class AsyncJobManager {
|
|
|
1148
1233
|
}
|
|
1149
1234
|
|
|
1150
1235
|
#scheduleEviction(jobId: string): void {
|
|
1236
|
+
if (this.#disposed) return;
|
|
1151
1237
|
this.#notifyChange();
|
|
1152
1238
|
if (this.#retentionMs <= 0) {
|
|
1153
|
-
this.#
|
|
1154
|
-
this.#runLifecycle(jobId, "evict");
|
|
1155
|
-
this.#jobs.delete(jobId);
|
|
1156
|
-
this.#lifecycles.delete(jobId);
|
|
1157
|
-
this.#lifecyclePhases.delete(jobId);
|
|
1158
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
1159
|
-
this.#watchedJobs.delete(jobId);
|
|
1160
|
-
this.#outputState.delete(jobId);
|
|
1239
|
+
this.#evictJob(jobId);
|
|
1161
1240
|
return;
|
|
1162
1241
|
}
|
|
1163
1242
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -1166,20 +1245,25 @@ export class AsyncJobManager {
|
|
|
1166
1245
|
}
|
|
1167
1246
|
const timer = setTimeout(() => {
|
|
1168
1247
|
this.#evictionTimers.delete(jobId);
|
|
1169
|
-
this.#
|
|
1170
|
-
this.#runLifecycle(jobId, "evict");
|
|
1171
|
-
this.#jobs.delete(jobId);
|
|
1172
|
-
this.#lifecycles.delete(jobId);
|
|
1173
|
-
this.#lifecyclePhases.delete(jobId);
|
|
1174
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
1175
|
-
this.#watchedJobs.delete(jobId);
|
|
1176
|
-
this.#outputState.delete(jobId);
|
|
1248
|
+
this.#evictJob(jobId);
|
|
1177
1249
|
this.#notifyChange();
|
|
1178
1250
|
}, this.#retentionMs);
|
|
1179
1251
|
timer.unref();
|
|
1180
1252
|
this.#evictionTimers.set(jobId, timer);
|
|
1181
1253
|
}
|
|
1182
1254
|
|
|
1255
|
+
#evictJob(jobId: string): void {
|
|
1256
|
+
this.#recordMonitorTombstone(jobId);
|
|
1257
|
+
this.#runLifecycle(jobId, "evict");
|
|
1258
|
+
this.#purgeTerminalSubagentStateForJob(jobId);
|
|
1259
|
+
this.#jobs.delete(jobId);
|
|
1260
|
+
this.#lifecycles.delete(jobId);
|
|
1261
|
+
this.#lifecyclePhases.delete(jobId);
|
|
1262
|
+
this.#suppressedDeliveries.delete(jobId);
|
|
1263
|
+
this.#watchedJobs.delete(jobId);
|
|
1264
|
+
this.#outputState.delete(jobId);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1183
1267
|
#clearEvictionTimers(): void {
|
|
1184
1268
|
for (const timer of this.#evictionTimers.values()) {
|
|
1185
1269
|
clearTimeout(timer);
|
|
@@ -1245,17 +1329,38 @@ export class AsyncJobManager {
|
|
|
1245
1329
|
if (this.isDeliverySuppressed(jobId)) {
|
|
1246
1330
|
return;
|
|
1247
1331
|
}
|
|
1332
|
+
const deliveryText = this.#boundedDeliveryText(text);
|
|
1248
1333
|
this.#deliveries.push({
|
|
1249
1334
|
jobId,
|
|
1250
|
-
text,
|
|
1335
|
+
text: deliveryText.text,
|
|
1336
|
+
originalBytes: deliveryText.originalBytes,
|
|
1337
|
+
truncated: deliveryText.truncated,
|
|
1251
1338
|
attempt: 0,
|
|
1252
1339
|
nextAttemptAt: Date.now(),
|
|
1253
1340
|
ownerId: this.#jobs.get(jobId)?.ownerId,
|
|
1254
1341
|
});
|
|
1342
|
+
while (this.#deliveries.length > DEFAULT_MAX_DELIVERY_QUEUE) {
|
|
1343
|
+
const dropped = this.#deliveries.shift();
|
|
1344
|
+
if (dropped) this.#deadLetteredDeliveries.set(dropped.jobId, dropped);
|
|
1345
|
+
}
|
|
1255
1346
|
this.#ensureDeliveryLoop();
|
|
1256
1347
|
}
|
|
1257
1348
|
|
|
1349
|
+
#boundedDeliveryText(text: string): { text: string; originalBytes?: number; truncated?: boolean } {
|
|
1350
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
1351
|
+
if (bytes <= DELIVERY_MAX_TEXT_BYTES) return { text };
|
|
1352
|
+
const head = sliceTextToUtf8ByteLength(text, DELIVERY_PREVIEW_HEAD_BYTES);
|
|
1353
|
+
const tailStart = Math.max(0, bytes - DELIVERY_PREVIEW_TAIL_BYTES);
|
|
1354
|
+
const tail = sliceTextAfterUtf8ByteOffset(text, tailStart);
|
|
1355
|
+
return {
|
|
1356
|
+
text: `${head}\n\n[async delivery output truncated from ${bytes} bytes]\n\n${tail}`,
|
|
1357
|
+
originalBytes: bytes,
|
|
1358
|
+
truncated: true,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1258
1362
|
#ensureDeliveryLoop(): void {
|
|
1363
|
+
if (this.#disposed) return;
|
|
1259
1364
|
if (this.#deliveryLoop) {
|
|
1260
1365
|
return;
|
|
1261
1366
|
}
|
|
@@ -1266,7 +1371,7 @@ export class AsyncJobManager {
|
|
|
1266
1371
|
})
|
|
1267
1372
|
.finally(() => {
|
|
1268
1373
|
this.#deliveryLoop = undefined;
|
|
1269
|
-
if (this.#deliveries.length > 0) {
|
|
1374
|
+
if (!this.#disposed && this.#deliveries.length > 0) {
|
|
1270
1375
|
this.#ensureDeliveryLoop();
|
|
1271
1376
|
}
|
|
1272
1377
|
});
|
|
@@ -1304,20 +1409,29 @@ export class AsyncJobManager {
|
|
|
1304
1409
|
} catch (error) {
|
|
1305
1410
|
delivery.attempt += 1;
|
|
1306
1411
|
delivery.lastError = error instanceof Error ? error.message : String(error);
|
|
1307
|
-
delivery.
|
|
1308
|
-
|
|
1309
|
-
|
|
1412
|
+
if (delivery.attempt >= DELIVERY_MAX_ATTEMPTS) {
|
|
1413
|
+
this.#deadLetteredDeliveries.set(delivery.jobId, delivery);
|
|
1414
|
+
logger.warn("Async job completion delivery reached retry cap", {
|
|
1415
|
+
jobId: delivery.jobId,
|
|
1416
|
+
attempt: delivery.attempt,
|
|
1417
|
+
error: delivery.lastError,
|
|
1418
|
+
});
|
|
1419
|
+
} else {
|
|
1420
|
+
delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
|
|
1421
|
+
if (!this.isDeliverySuppressed(delivery.jobId)) {
|
|
1422
|
+
this.#deliveries.push(delivery);
|
|
1423
|
+
}
|
|
1424
|
+
logger.warn("Async job completion delivery failed", {
|
|
1425
|
+
jobId: delivery.jobId,
|
|
1426
|
+
attempt: delivery.attempt,
|
|
1427
|
+
nextRetryAt: delivery.nextAttemptAt,
|
|
1428
|
+
error: delivery.lastError,
|
|
1429
|
+
});
|
|
1310
1430
|
}
|
|
1311
|
-
logger.warn("Async job completion delivery failed", {
|
|
1312
|
-
jobId: delivery.jobId,
|
|
1313
|
-
attempt: delivery.attempt,
|
|
1314
|
-
nextRetryAt: delivery.nextAttemptAt,
|
|
1315
|
-
error: delivery.lastError,
|
|
1316
|
-
});
|
|
1317
1431
|
} finally {
|
|
1318
1432
|
const index = this.#inFlightDeliveries.indexOf(delivery);
|
|
1319
1433
|
if (index !== -1) this.#inFlightDeliveries.splice(index, 1);
|
|
1320
|
-
if (this.#deliveries.length > 0) this.#ensureDeliveryLoop();
|
|
1434
|
+
if (!this.#disposed && this.#deliveries.length > 0) this.#ensureDeliveryLoop();
|
|
1321
1435
|
}
|
|
1322
1436
|
})();
|
|
1323
1437
|
delivery.promise = promise;
|
package/src/cli/args.ts
CHANGED
|
@@ -269,6 +269,8 @@ export function getExtraHelpText(): string {
|
|
|
269
269
|
gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
|
|
270
270
|
GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
|
|
271
271
|
GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
|
|
272
|
+
GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
|
|
273
|
+
GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
|
|
272
274
|
|
|
273
275
|
For complete environment variable reference, see:
|
|
274
276
|
${chalk.dim("docs/environment-variables.md")}
|
package/src/cli/fast-help.ts
CHANGED
|
@@ -54,6 +54,8 @@ export function getExtraHelpText(): string {
|
|
|
54
54
|
gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
|
|
55
55
|
GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
|
|
56
56
|
GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
|
|
57
|
+
GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
|
|
58
|
+
GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
|
|
57
59
|
|
|
58
60
|
For complete environment variable reference, see:
|
|
59
61
|
docs/environment-variables.md
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles `gjc setup [component]` to install the normal defaults or optional feature dependencies.
|
|
5
5
|
*/
|
|
6
|
+
|
|
6
7
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { SqliteAuthCredentialStore } from "@gajae-code/ai";
|
|
10
|
+
import { $which, APP_NAME, getAgentDbPath, getPythonEnvDir } from "@gajae-code/utils";
|
|
8
11
|
import { $ } from "bun";
|
|
9
12
|
import chalk from "chalk";
|
|
10
13
|
import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
readGjcManagedCodexHooksStatus,
|
|
15
18
|
} from "../hooks/codex-native-hooks-config";
|
|
16
19
|
import { theme } from "../modes/theme/theme";
|
|
20
|
+
import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../setup/credential-import";
|
|
17
21
|
import {
|
|
18
22
|
formatHermesSetupResult,
|
|
19
23
|
type HermesSetupFlags,
|
|
@@ -27,7 +31,7 @@ import {
|
|
|
27
31
|
parseProviderCompatibility,
|
|
28
32
|
} from "../setup/provider-onboarding";
|
|
29
33
|
|
|
30
|
-
export type SetupComponent = "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
|
|
34
|
+
export type SetupComponent = "credentials" | "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
|
|
31
35
|
|
|
32
36
|
export interface SetupCommandArgs {
|
|
33
37
|
component: SetupComponent;
|
|
@@ -57,10 +61,12 @@ export interface SetupCommandArgs {
|
|
|
57
61
|
gjcCommand?: string;
|
|
58
62
|
target?: string;
|
|
59
63
|
profileDir?: string;
|
|
64
|
+
yes?: boolean;
|
|
65
|
+
dryRun?: boolean;
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
const VALID_COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
69
|
+
const VALID_COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
64
70
|
|
|
65
71
|
function hasProviderSetupFlags(flags: SetupCommandArgs["flags"]): boolean {
|
|
66
72
|
return (
|
|
@@ -113,6 +119,10 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
|
|
|
113
119
|
flags.smoke = true;
|
|
114
120
|
} else if (arg === "--install") {
|
|
115
121
|
flags.install = true;
|
|
122
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
123
|
+
flags.yes = true;
|
|
124
|
+
} else if (arg === "--dry-run") {
|
|
125
|
+
flags.dryRun = true;
|
|
116
126
|
} else if (arg === "--root") {
|
|
117
127
|
flags.root = [...(flags.root ?? []), args[++i] ?? ""];
|
|
118
128
|
} else if (arg === "--repo") {
|
|
@@ -243,6 +253,9 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
|
|
|
243
253
|
case "stt":
|
|
244
254
|
await handleSttSetup(cmd.flags);
|
|
245
255
|
break;
|
|
256
|
+
case "credentials":
|
|
257
|
+
await handleCredentialsSetup(cmd.flags);
|
|
258
|
+
break;
|
|
246
259
|
}
|
|
247
260
|
}
|
|
248
261
|
|
|
@@ -472,6 +485,122 @@ async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promi
|
|
|
472
485
|
process.exit(1);
|
|
473
486
|
}
|
|
474
487
|
}
|
|
488
|
+
async function confirmImport(count: number): Promise<boolean> {
|
|
489
|
+
if (!process.stdin.isTTY) return false;
|
|
490
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
491
|
+
try {
|
|
492
|
+
const answer = (await rl.question(`Import ${count} credential(s) into ${getAgentDbPath()}? [y/N] `))
|
|
493
|
+
.trim()
|
|
494
|
+
.toLowerCase();
|
|
495
|
+
return answer === "y" || answer === "yes";
|
|
496
|
+
} finally {
|
|
497
|
+
rl.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Discover existing Claude Code / Codex CLI credentials and import them into the
|
|
503
|
+
* gjc credential store after a redacted preview + confirmation. Falls back to
|
|
504
|
+
* manual-setup guidance when nothing importable is found.
|
|
505
|
+
*/
|
|
506
|
+
async function handleCredentialsSetup(flags: { json?: boolean; yes?: boolean; dryRun?: boolean }): Promise<void> {
|
|
507
|
+
const result = await discoverExternalCredentials();
|
|
508
|
+
const redactedPlan = {
|
|
509
|
+
importable: result.importable.map(c => ({
|
|
510
|
+
provider: c.provider,
|
|
511
|
+
kind: c.kind,
|
|
512
|
+
source: c.source,
|
|
513
|
+
identity: c.identity,
|
|
514
|
+
expiresAt: c.expiresAt,
|
|
515
|
+
redactedToken: c.redactedToken,
|
|
516
|
+
})),
|
|
517
|
+
skipped: result.skipped,
|
|
518
|
+
environment: result.environment,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (result.importable.length === 0) {
|
|
522
|
+
if (flags.json) {
|
|
523
|
+
process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
|
|
527
|
+
process.stdout.write(
|
|
528
|
+
chalk.yellow(
|
|
529
|
+
`\nNo importable Claude/Codex credentials found. Continue with manual setup:\n` +
|
|
530
|
+
` ${APP_NAME} setup provider (add an API-compatible provider)\n` +
|
|
531
|
+
` ${APP_NAME} (then /login) (interactive OAuth/subscription login)\n`,
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!flags.json) {
|
|
538
|
+
process.stdout.write(chalk.bold("Discovered credentials (redacted):\n"));
|
|
539
|
+
for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (flags.dryRun) {
|
|
543
|
+
if (flags.json) process.stdout.write(`${JSON.stringify({ ...redactedPlan, dryRun: true, imported: [] })}\n`);
|
|
544
|
+
else process.stdout.write(chalk.dim(`\nDry run — no credentials imported.\n`));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const confirmed = flags.yes || (await confirmImport(result.importable.length));
|
|
549
|
+
if (!confirmed) {
|
|
550
|
+
if (flags.json) {
|
|
551
|
+
process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
process.stdout.write(chalk.dim(`\nImport cancelled. Re-run with --yes to import non-interactively.\n`));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
|
|
559
|
+
let summary: Awaited<ReturnType<typeof importCredentials>>;
|
|
560
|
+
try {
|
|
561
|
+
summary = await importCredentials(result.importable, (provider, credential) =>
|
|
562
|
+
store.upsertAuthCredentialForProvider(provider, credential),
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
store.close();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (flags.json) {
|
|
569
|
+
process.stdout.write(
|
|
570
|
+
`${JSON.stringify({
|
|
571
|
+
...redactedPlan,
|
|
572
|
+
imported: summary.imported.map(c => ({ provider: c.provider, kind: c.kind, source: c.source })),
|
|
573
|
+
failed: summary.failed.map(f => ({
|
|
574
|
+
provider: f.credential.provider,
|
|
575
|
+
source: f.credential.source,
|
|
576
|
+
error: f.error,
|
|
577
|
+
})),
|
|
578
|
+
})}\n`,
|
|
579
|
+
);
|
|
580
|
+
if (summary.failed.length > 0) process.exitCode = 1;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const credential of summary.imported) {
|
|
585
|
+
process.stdout.write(
|
|
586
|
+
`${chalk.green(`${theme.status.success} imported`)} ${formatCredentialSummaryLine(credential)}\n`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
for (const failure of summary.failed) {
|
|
590
|
+
process.stdout.write(
|
|
591
|
+
`${chalk.red(`${theme.status.error} failed`)} ${failure.credential.provider} (${failure.credential.source}): ${failure.error}\n`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (summary.failed.length > 0) {
|
|
595
|
+
process.exitCode = 1;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
process.stdout.write(chalk.dim(`\nCredentials saved to ${getAgentDbPath()}\n`));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function formatCredentialSummaryLine(credential: { provider: string; kind: string; source: string }): string {
|
|
602
|
+
return `${credential.provider} · ${credential.kind} (from ${credential.source})`;
|
|
603
|
+
}
|
|
475
604
|
|
|
476
605
|
/**
|
|
477
606
|
* Print setup command help.
|
|
@@ -489,6 +618,7 @@ ${chalk.bold("Components:")}
|
|
|
489
618
|
provider Optional: add a preset, OpenAI-compatible, or Anthropic-compatible API provider
|
|
490
619
|
python Optional: verify a Python 3 interpreter is reachable for code execution
|
|
491
620
|
stt Optional: install speech-to-text dependencies (openai-whisper, recording tools)
|
|
621
|
+
credentials Optional: import existing Claude Code / Codex CLI credentials
|
|
492
622
|
|
|
493
623
|
|
|
494
624
|
${chalk.bold("Provider example:")}
|
|
@@ -524,6 +654,8 @@ ${chalk.bold("Options:")}
|
|
|
524
654
|
--mutation Hermes MCP mutation classes: sessions,questions,reports,all
|
|
525
655
|
--target Hermes config file target for config-only install
|
|
526
656
|
--profile-dir Hermes profile directory for full setup install
|
|
657
|
+
--dry-run Preview discovered credentials without importing (credentials)
|
|
658
|
+
-y, --yes Import discovered credentials without an interactive prompt (credentials)
|
|
527
659
|
|
|
528
660
|
${chalk.bold("Examples:")}
|
|
529
661
|
${APP_NAME} setup Install bundled GJC default workflow skills
|
|
@@ -536,5 +668,8 @@ ${chalk.bold("Examples:")}
|
|
|
536
668
|
${APP_NAME} setup stt Install speech-to-text dependencies
|
|
537
669
|
${APP_NAME} setup stt --check Check if STT dependencies are available
|
|
538
670
|
${APP_NAME} setup python --check Check if Python execution is available
|
|
671
|
+
${APP_NAME} setup credentials Discover & import existing Claude/Codex credentials
|
|
672
|
+
${APP_NAME} setup credentials --dry-run Preview importable credentials (redacted)
|
|
673
|
+
${APP_NAME} setup credentials --yes Import without an interactive prompt
|
|
539
674
|
`);
|
|
540
675
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
|
5
5
|
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
|
|
6
6
|
import { initTheme } from "../modes/theme/theme";
|
|
7
7
|
|
|
8
|
-
const COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
8
|
+
const COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
9
9
|
|
|
10
10
|
export default class Setup extends Command {
|
|
11
11
|
static description = "Install GJC defaults or optional feature dependencies";
|
|
@@ -47,6 +47,8 @@ export default class Setup extends Command {
|
|
|
47
47
|
"api-key-env": Flags.string({ description: "Read provider API key from this environment variable" }),
|
|
48
48
|
model: Flags.string({ description: "Model id to add (repeat or comma-separate)", multiple: true }),
|
|
49
49
|
"models-path": Flags.string({ description: "Override models config path" }),
|
|
50
|
+
yes: Flags.boolean({ char: "y", description: "Import discovered credentials without an interactive prompt" }),
|
|
51
|
+
"dry-run": Flags.boolean({ description: "Preview discovered credentials without importing" }),
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
async run(): Promise<void> {
|
|
@@ -79,6 +81,8 @@ export default class Setup extends Command {
|
|
|
79
81
|
gjcCommand: flags["gjc-command"],
|
|
80
82
|
target: flags.target,
|
|
81
83
|
profileDir: flags["profile-dir"],
|
|
84
|
+
yes: flags.yes,
|
|
85
|
+
dryRun: flags["dry-run"],
|
|
82
86
|
},
|
|
83
87
|
};
|
|
84
88
|
await initTheme();
|
|
@@ -16,12 +16,14 @@ export default class Ultragoal extends Command {
|
|
|
16
16
|
static delegateHelp = true;
|
|
17
17
|
|
|
18
18
|
async run(): Promise<void> {
|
|
19
|
+
const isReviewStart = this.argv.includes("review") && this.argv.includes("review-start");
|
|
19
20
|
const shouldActivateGoalMode = isUltragoalCreateGoalsInvocation(this.argv);
|
|
20
21
|
const result = await runNativeUltragoalCommand(this.argv);
|
|
21
22
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
22
23
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
23
24
|
process.exitCode = result.status;
|
|
24
|
-
if (result.status !== 0 || !shouldActivateGoalMode) return;
|
|
25
|
+
if (result.status !== 0 || (!shouldActivateGoalMode && !isReviewStart)) return;
|
|
26
|
+
if (isReviewStart && !result.createdReviewPlan && (result.reviewBlockerGoalIds?.length ?? 0) === 0) return;
|
|
25
27
|
|
|
26
28
|
const cwd = process.cwd();
|
|
27
29
|
const { objective, goalsPath } = await readUltragoalGjcObjective(cwd);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* GC adapter for config file-locks (`<file>.lock` dirs holding `{pid, timestamp}`).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { Stats } from "node:fs";
|
|
5
6
|
import * as fs from "node:fs/promises";
|
|
6
7
|
import * as path from "node:path";
|
|
7
8
|
import { getAgentDir, getConfigRootDir, isEnoent } from "@gajae-code/utils";
|
|
@@ -92,7 +93,7 @@ async function walkForLockDirs(
|
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
let stat:
|
|
96
|
+
let stat: Stats;
|
|
96
97
|
try {
|
|
97
98
|
stat = await fs.lstat(dir);
|
|
98
99
|
} catch (error) {
|
|
@@ -171,8 +172,19 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
|
|
|
171
172
|
return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
// Fail-closed owner-token guard (#606): we observed `info` (pid+timestamp)
|
|
176
|
+
// dead, but a fresh owner can reclaim a stale lock dir at this same path
|
|
177
|
+
// between the probe above and the unlink below. Pass the exact owner token
|
|
178
|
+
// so removal re-verifies the on-disk identity under the unlink and refuses
|
|
179
|
+
// to delete a recreated LIVE lock (TOCTOU).
|
|
174
180
|
try {
|
|
175
|
-
await removeFileLockDirForGc(lockDir);
|
|
181
|
+
const removal = await removeFileLockDirForGc(lockDir, info);
|
|
182
|
+
if (removal === "owner_changed") {
|
|
183
|
+
return { removed: false, skipped: "file_lock_owner_changed_before_delete" };
|
|
184
|
+
}
|
|
185
|
+
if (removal === "missing") {
|
|
186
|
+
return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
187
|
+
}
|
|
176
188
|
return { removed: true };
|
|
177
189
|
} catch (error) {
|
|
178
190
|
return { removed: false, error: errorMessage(error) };
|