@gajae-code/coding-agent 0.3.1 → 0.4.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 +46 -0
- package/README.md +1 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +30 -0
- package/dist/types/config/model-profiles.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +25 -10
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +84 -0
- package/dist/types/config/settings-schema.d.ts +15 -0
- package/dist/types/edit/diff.d.ts +16 -0
- package/dist/types/edit/modes/replace.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
- package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
- package/dist/types/extensibility/skills.d.ts +9 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
- package/dist/types/harness-control-plane/storage.d.ts +7 -0
- package/dist/types/lsp/client.d.ts +1 -0
- package/dist/types/main.d.ts +10 -1
- package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/sdk.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/blob-store.d.ts +17 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/skill-state/active-state.d.ts +13 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/thinking.d.ts +3 -2
- package/dist/types/tools/hindsight-recall.d.ts +0 -2
- package/dist/types/tools/hindsight-reflect.d.ts +0 -2
- package/dist/types/tools/hindsight-retain.d.ts +0 -2
- package/dist/types/tools/index.d.ts +7 -4
- package/package.json +9 -7
- package/src/cli/args.ts +10 -0
- package/src/cli.ts +14 -0
- package/src/commands/harness.ts +192 -7
- package/src/commands/launch.ts +8 -0
- package/src/commands/ultragoal.ts +1 -21
- package/src/config/model-equivalence.ts +1 -1
- package/src/config/model-profile-activation.ts +157 -0
- package/src/config/model-profiles.ts +155 -0
- package/src/config/model-registry.ts +51 -5
- package/src/config/model-resolver.ts +3 -2
- package/src/config/models-config-schema.ts +42 -1
- package/src/config/settings-schema.ts +14 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +11 -1
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/discovery/claude-plugins.ts +25 -5
- package/src/edit/diff.ts +64 -1
- package/src/edit/modes/replace.ts +60 -2
- package/src/extensibility/gjc-plugins/activation.ts +87 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +114 -0
- package/src/extensibility/gjc-plugins/loader.ts +131 -0
- package/src/extensibility/gjc-plugins/paths.ts +66 -0
- package/src/extensibility/gjc-plugins/schema.ts +79 -0
- package/src/extensibility/gjc-plugins/state.ts +29 -0
- package/src/extensibility/gjc-plugins/tools.ts +47 -0
- package/src/extensibility/gjc-plugins/types.ts +97 -0
- package/src/extensibility/gjc-plugins/validation.ts +76 -0
- package/src/extensibility/skills.ts +39 -7
- package/src/gjc-runtime/state-runtime.ts +93 -2
- package/src/gjc-runtime/state-writer.ts +17 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +62 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
- package/src/gjc-runtime/workflow-manifest.ts +2 -2
- package/src/harness-control-plane/storage.ts +144 -2
- package/src/hashline/hash.ts +23 -0
- package/src/hooks/skill-state.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +8 -11
- package/src/lsp/client.ts +7 -0
- package/src/main.ts +67 -1
- package/src/modes/acp/acp-agent.ts +25 -2
- package/src/modes/bridge/bridge-mode.ts +124 -2
- package/src/modes/components/custom-provider-wizard.ts +318 -0
- package/src/modes/components/model-selector.ts +108 -18
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/controllers/input-controller.ts +14 -2
- package/src/modes/controllers/selector-controller.ts +57 -1
- package/src/modes/prompt-action-autocomplete.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +57 -3
- package/src/modes/rpc/rpc-mode.ts +67 -0
- package/src/modes/rpc/rpc-types.ts +224 -2
- package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
- package/src/modes/shared/agent-wire/command-validation.ts +25 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
- package/src/modes/shared/agent-wire/handshake.ts +43 -3
- package/src/modes/shared/agent-wire/protocol.ts +7 -0
- package/src/modes/shared/agent-wire/responses.ts +2 -2
- package/src/modes/shared/agent-wire/scopes.ts +2 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read-path.md +6 -7
- package/src/prompts/memories/unavailable.md +2 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/recall.md +1 -0
- package/src/prompts/tools/reflect.md +1 -0
- package/src/prompts/tools/retain.md +1 -0
- package/src/runtime-mcp/client.ts +7 -4
- package/src/runtime-mcp/manager.ts +45 -13
- package/src/runtime-mcp/transports/http.ts +40 -14
- package/src/runtime-mcp/transports/stdio.ts +11 -10
- package/src/sdk.ts +48 -1
- package/src/session/agent-session.ts +211 -2
- package/src/session/blob-store.ts +84 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-manager.ts +390 -33
- package/src/session/session-storage.ts +26 -0
- package/src/setup/provider-onboarding.ts +2 -2
- package/src/skill-state/active-state.ts +89 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/discovery.ts +7 -1
- package/src/task/executor.ts +18 -2
- package/src/task/index.ts +2 -0
- package/src/thinking.ts +8 -2
- package/src/tools/ask.ts +39 -9
- package/src/tools/hindsight-recall.ts +0 -2
- package/src/tools/hindsight-reflect.ts +0 -2
- package/src/tools/hindsight-retain.ts +0 -2
- package/src/tools/index.ts +7 -18
- package/src/tools/read.ts +3 -3
- package/src/tools/skill.ts +15 -3
- package/src/utils/edit-mode.ts +1 -1
package/src/commands/harness.ts
CHANGED
|
@@ -9,20 +9,28 @@
|
|
|
9
9
|
* until the RuntimeOwner (M3+) lands.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
14
|
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
14
15
|
import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
|
|
15
16
|
import { classifyRecovery } from "../harness-control-plane/classifier";
|
|
16
17
|
import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
|
|
17
|
-
import { RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
|
|
18
|
+
import { type ResolvedOwner, RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
|
|
19
|
+
import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
|
|
20
|
+
import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
|
|
18
21
|
import { GajaeCodeRpc } from "../harness-control-plane/rpc-adapter";
|
|
22
|
+
import { classifyLeaseStatus, readLease } from "../harness-control-plane/session-lease";
|
|
19
23
|
import { buildResponse, buildStateView } from "../harness-control-plane/state-machine";
|
|
20
24
|
import {
|
|
25
|
+
canonicalWorkspacePath,
|
|
21
26
|
generateSessionId,
|
|
22
27
|
readEvents,
|
|
23
28
|
readSessionState,
|
|
29
|
+
rememberHarnessSessionRoot,
|
|
24
30
|
resolveHarnessRoot,
|
|
31
|
+
resolveHarnessSessionRoot,
|
|
25
32
|
sessionPaths,
|
|
33
|
+
writeReceiptImmutable,
|
|
26
34
|
writeSessionState,
|
|
27
35
|
} from "../harness-control-plane/storage";
|
|
28
36
|
import {
|
|
@@ -31,6 +39,7 @@ import {
|
|
|
31
39
|
type GitDelta,
|
|
32
40
|
type Harness as HarnessKind,
|
|
33
41
|
type Observation,
|
|
42
|
+
type RecoveryClassification,
|
|
34
43
|
type RetryBudget,
|
|
35
44
|
SESSION_SCHEMA_VERSION,
|
|
36
45
|
type SessionHandle,
|
|
@@ -120,8 +129,12 @@ function gitOutput(workspace: string, args: string[]): string | null {
|
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
function resolveInputWorkspace(input: Record<string, unknown>): string {
|
|
133
|
+
return canonicalWorkspacePath(typeof input.workspace === "string" ? input.workspace : process.cwd());
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
function buildPreflight(input: Record<string, unknown>): HarnessPreflight {
|
|
124
|
-
const workspace =
|
|
137
|
+
const workspace = resolveInputWorkspace(input);
|
|
125
138
|
const declaredBranch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : null;
|
|
126
139
|
const blockers: string[] = [];
|
|
127
140
|
const gitRoot = gitOutput(workspace, ["rev-parse", "--show-toplevel"]);
|
|
@@ -198,6 +211,7 @@ async function buildObservation(
|
|
|
198
211
|
const observedSignals = ["SessionStart"];
|
|
199
212
|
for (const event of events.slice(-200)) {
|
|
200
213
|
pushUnique(observedSignals, (event.evidence as { signal?: unknown } | undefined)?.signal);
|
|
214
|
+
if (event.kind === "prompt_accepted") pushUnique(observedSignals, "prompt-accepted");
|
|
201
215
|
}
|
|
202
216
|
const terminalEvent = completedTerminalEvent(events);
|
|
203
217
|
const lastEventAt = events.at(-1)?.createdAt;
|
|
@@ -215,6 +229,114 @@ async function buildObservation(
|
|
|
215
229
|
completedTerminalEvent: terminalEvent,
|
|
216
230
|
};
|
|
217
231
|
}
|
|
232
|
+
interface OwnerExitEvidence {
|
|
233
|
+
reason: string;
|
|
234
|
+
leaseStatus: string;
|
|
235
|
+
pid: number | null;
|
|
236
|
+
endpointPresent: boolean;
|
|
237
|
+
heartbeatAt: string | null;
|
|
238
|
+
expiresAt: string | null;
|
|
239
|
+
lastEventKind: string | null;
|
|
240
|
+
lastEventAt: string | null;
|
|
241
|
+
lastSignal: string | null;
|
|
242
|
+
promptAcceptedSeen: boolean;
|
|
243
|
+
completedSeen: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function buildOwnerExitEvidence(root: string, state: SessionState): Promise<OwnerExitEvidence> {
|
|
247
|
+
const lease = await readLease(root, state.sessionId);
|
|
248
|
+
const leaseStatus = classifyLeaseStatus(lease);
|
|
249
|
+
const events = await readEvents(root, state.sessionId, 0);
|
|
250
|
+
const lastEvent = events.at(-1) ?? null;
|
|
251
|
+
let lastSignal: string | null = null;
|
|
252
|
+
let promptAcceptedSeen = false;
|
|
253
|
+
let completedSeen = false;
|
|
254
|
+
for (const event of events) {
|
|
255
|
+
const signal = (event.evidence as { signal?: unknown } | undefined)?.signal;
|
|
256
|
+
if (typeof signal === "string") lastSignal = signal;
|
|
257
|
+
if (event.kind === "prompt_accepted" || signal === "prompt-accepted") promptAcceptedSeen = true;
|
|
258
|
+
if (event.kind === "rpc_agent_completed" || signal === "completed") completedSeen = true;
|
|
259
|
+
}
|
|
260
|
+
let reason = "owner-not-live";
|
|
261
|
+
if (!lease) {
|
|
262
|
+
reason = promptAcceptedSeen && !completedSeen ? "owner-exited-after-prompt-acceptance" : "owner-lease-missing";
|
|
263
|
+
} else if (leaseStatus === "dead") {
|
|
264
|
+
reason = promptAcceptedSeen && !completedSeen ? "owner-exited-after-prompt-acceptance" : "owner-process-dead";
|
|
265
|
+
} else if (leaseStatus === "expiredAlive") {
|
|
266
|
+
reason = "owner-lease-expired";
|
|
267
|
+
} else if (leaseStatus === "epermAlive") {
|
|
268
|
+
reason = "owner-liveness-unknown-permission-denied";
|
|
269
|
+
} else {
|
|
270
|
+
reason = "owner-endpoint-unreachable";
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
reason,
|
|
274
|
+
leaseStatus,
|
|
275
|
+
pid: lease?.pid ?? null,
|
|
276
|
+
endpointPresent: Boolean(lease?.endpoint?.path),
|
|
277
|
+
heartbeatAt: lease?.heartbeatAt ?? null,
|
|
278
|
+
expiresAt: lease?.expiresAt ?? null,
|
|
279
|
+
lastEventKind: lastEvent?.kind ?? null,
|
|
280
|
+
lastEventAt: lastEvent?.createdAt ?? null,
|
|
281
|
+
lastSignal,
|
|
282
|
+
promptAcceptedSeen,
|
|
283
|
+
completedSeen,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function writeVanishReceiptForDecision(
|
|
288
|
+
root: string,
|
|
289
|
+
state: SessionState,
|
|
290
|
+
observation: Observation,
|
|
291
|
+
classification: RecoveryClassification,
|
|
292
|
+
): Promise<string | null> {
|
|
293
|
+
if (!requiresVanishBeforeAction(classification)) return null;
|
|
294
|
+
const dirty = observation.gitDelta === "dirty" || observation.gitDelta === "unknown";
|
|
295
|
+
const preservation = dirty ? preserveDirtyWorktree(observation.cwd) : null;
|
|
296
|
+
const evidence: VanishEvidence = {
|
|
297
|
+
classification,
|
|
298
|
+
gitDelta: observation.gitDelta,
|
|
299
|
+
gitStatusPorcelain: preservation
|
|
300
|
+
? `tracked:${preservation.trackedDiffSha256};untracked:${preservation.untrackedManifest.length}`
|
|
301
|
+
: observation.observedSignals.join(","),
|
|
302
|
+
untrackedManifest: preservation?.untrackedManifest ?? [],
|
|
303
|
+
preservation: preservation?.stashRef ? "stash" : "snapshot",
|
|
304
|
+
stashRef: preservation?.stashRef ?? null,
|
|
305
|
+
snapshotComplete: preservation?.snapshotComplete ?? true,
|
|
306
|
+
forbiddenActions: dirty ? ["restart-clean", "delete", "reset"] : [],
|
|
307
|
+
};
|
|
308
|
+
const receipt = buildReceipt<VanishEvidence>({
|
|
309
|
+
receiptId: `vanish-${Date.now()}-${randomBytes(4).toString("hex")}`,
|
|
310
|
+
sessionId: state.sessionId,
|
|
311
|
+
family: "vanish",
|
|
312
|
+
source: "cli-recover",
|
|
313
|
+
subject: {
|
|
314
|
+
workspace: observation.cwd,
|
|
315
|
+
branch: observation.branch,
|
|
316
|
+
head: null,
|
|
317
|
+
commit: null,
|
|
318
|
+
},
|
|
319
|
+
evidence,
|
|
320
|
+
});
|
|
321
|
+
await writeReceiptImmutable(root, state.sessionId, "vanish", receipt.receiptId, receipt);
|
|
322
|
+
return receipt.receiptId;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function updateStateWithRestoredOwner(state: SessionState, leasePath: string, resolved: ResolvedOwner): void {
|
|
326
|
+
state.lifecycle = "observing";
|
|
327
|
+
state.blockers = state.blockers.filter(blocker => !isOwnerLivenessBlocker(blocker));
|
|
328
|
+
state.handle.processHandle = {
|
|
329
|
+
kind: "runtime-owner",
|
|
330
|
+
ownerId: resolved.lease?.ownerId ?? null,
|
|
331
|
+
pid: resolved.lease?.pid ?? null,
|
|
332
|
+
};
|
|
333
|
+
state.handle.ownerHandle = {
|
|
334
|
+
leasePath,
|
|
335
|
+
endpoint: resolved.socketPath,
|
|
336
|
+
heartbeatAt: resolved.lease?.heartbeatAt ?? null,
|
|
337
|
+
};
|
|
338
|
+
state.updatedAt = nowIso();
|
|
339
|
+
}
|
|
218
340
|
|
|
219
341
|
function isOwnerLivenessBlocker(blocker: string): boolean {
|
|
220
342
|
return blocker === "detached-owner-not-live" || blocker.startsWith("owner-vanished:");
|
|
@@ -327,9 +449,14 @@ export default class Harness extends Command {
|
|
|
327
449
|
async run(): Promise<void> {
|
|
328
450
|
const { args, flags } = await this.parse(Harness);
|
|
329
451
|
const verb = String(args.verb);
|
|
330
|
-
|
|
452
|
+
let root = resolveHarnessRoot();
|
|
331
453
|
try {
|
|
332
454
|
const input = parseInput(flags.input);
|
|
455
|
+
const sessionId = flags.session ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
|
|
456
|
+
const expectedWorkspace = typeof input.workspace === "string" ? resolveInputWorkspace(input) : undefined;
|
|
457
|
+
if (verb !== "start" && sessionId) {
|
|
458
|
+
root = await resolveHarnessSessionRoot(root, sessionId, process.env, { expectedWorkspace });
|
|
459
|
+
}
|
|
333
460
|
switch (verb) {
|
|
334
461
|
case "start":
|
|
335
462
|
return await this.#start(root, input);
|
|
@@ -468,6 +595,9 @@ export default class Harness extends Command {
|
|
|
468
595
|
if (process.env.GJC_HARNESS_RPC_COMMAND) {
|
|
469
596
|
envAssignments.push(`GJC_HARNESS_RPC_COMMAND=${shellQuote(process.env.GJC_HARNESS_RPC_COMMAND)}`);
|
|
470
597
|
}
|
|
598
|
+
if (process.env.GJC_HARNESS_TEST_NODE_MODULES) {
|
|
599
|
+
envAssignments.push(`GJC_HARNESS_TEST_NODE_MODULES=${shellQuote(process.env.GJC_HARNESS_TEST_NODE_MODULES)}`);
|
|
600
|
+
}
|
|
471
601
|
const ownerCommand = this.#buildOwnerCommand(sessionId).map(shellQuote).join(" ");
|
|
472
602
|
const shellCommand = `exec env ${envAssignments.join(" ")} ${ownerCommand}`;
|
|
473
603
|
const created = Bun.spawnSync([tmuxCommand, "new-session", "-d", "-s", sessionName, "-c", cwd, shellCommand], {
|
|
@@ -498,7 +628,13 @@ export default class Harness extends Command {
|
|
|
498
628
|
const cmd = this.#buildOwnerCommand(sessionId);
|
|
499
629
|
const child = Bun.spawn(cmd, {
|
|
500
630
|
cwd,
|
|
501
|
-
env: {
|
|
631
|
+
env: {
|
|
632
|
+
...process.env,
|
|
633
|
+
GJC_HARNESS_STATE_ROOT: root,
|
|
634
|
+
...(process.env.GJC_HARNESS_TEST_NODE_MODULES
|
|
635
|
+
? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
|
|
636
|
+
: {}),
|
|
637
|
+
},
|
|
502
638
|
stdout: "ignore",
|
|
503
639
|
stderr: "ignore",
|
|
504
640
|
stdin: "ignore",
|
|
@@ -540,7 +676,7 @@ export default class Harness extends Command {
|
|
|
540
676
|
process.exitCode = 1;
|
|
541
677
|
return;
|
|
542
678
|
}
|
|
543
|
-
const workspace =
|
|
679
|
+
const workspace = resolveInputWorkspace(input);
|
|
544
680
|
const sessionId = typeof input.sessionId === "string" ? input.sessionId : generateSessionId();
|
|
545
681
|
const eventsPath = `${root}/sessions/${sessionId}/events.jsonl`;
|
|
546
682
|
const leasePath = `${root}/sessions/${sessionId}/lease.json`;
|
|
@@ -573,6 +709,7 @@ export default class Harness extends Command {
|
|
|
573
709
|
updatedAt: startedAt,
|
|
574
710
|
};
|
|
575
711
|
await writeSessionState(root, state);
|
|
712
|
+
await rememberHarnessSessionRoot(root, sessionId);
|
|
576
713
|
let ownerLive = false;
|
|
577
714
|
let ownerRuntime: OwnerSpawnResult["runtime"] = "manual";
|
|
578
715
|
let ownerFallbackReason: string | null = null;
|
|
@@ -676,6 +813,10 @@ export default class Harness extends Command {
|
|
|
676
813
|
state = await reconcileCompletedOwnerExited(root, state, observation, completedTerminalEvent);
|
|
677
814
|
const vanishedOwnerBlock = needsVanishedOwnerBlock(state, observation, completedTerminalEvent);
|
|
678
815
|
state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
|
|
816
|
+
const ownerExit =
|
|
817
|
+
!ownerLive && (vanishedOwnerBlock || completedTerminalEvent)
|
|
818
|
+
? await buildOwnerExitEvidence(root, state)
|
|
819
|
+
: null;
|
|
679
820
|
writeJson(
|
|
680
821
|
buildResponse(state, ownerLive, {
|
|
681
822
|
observation: { ...observation, lifecycle: state.lifecycle },
|
|
@@ -686,6 +827,7 @@ export default class Harness extends Command {
|
|
|
686
827
|
...(completedTerminalEvent && !ownerLive
|
|
687
828
|
? { completedOwnerExited: true, terminalResult: completedTerminalEvent }
|
|
688
829
|
: {}),
|
|
830
|
+
...(ownerExit ? { ownerExit } : {}),
|
|
689
831
|
}),
|
|
690
832
|
);
|
|
691
833
|
}
|
|
@@ -767,7 +909,9 @@ export default class Harness extends Command {
|
|
|
767
909
|
buildResponse(state, ownerLiveFor(state), {
|
|
768
910
|
events,
|
|
769
911
|
cursor: nextCursor,
|
|
770
|
-
note: "tail-only;
|
|
912
|
+
note: "tail-only; events are preserved after owner exit",
|
|
913
|
+
ownerLive: ownerLiveFor(state),
|
|
914
|
+
ownerExit: ownerLiveFor(state) ? null : await buildOwnerExitEvidence(root, state),
|
|
771
915
|
}),
|
|
772
916
|
);
|
|
773
917
|
}
|
|
@@ -802,21 +946,62 @@ export default class Harness extends Command {
|
|
|
802
946
|
async #recoverWithoutOwner(root: string, sessionId: string, input: Record<string, unknown>): Promise<void> {
|
|
803
947
|
const budget = resolveRetryBudget(input);
|
|
804
948
|
let state = await loadState(root, sessionId);
|
|
949
|
+
const beforeExit = await buildOwnerExitEvidence(root, state);
|
|
805
950
|
const { observation, completedTerminalEvent } = await buildObservation(root, state, false);
|
|
806
951
|
state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
|
|
807
952
|
const decision = classifyRecovery({
|
|
808
953
|
observation: { ...observation, lifecycle: state.lifecycle },
|
|
809
954
|
retryBudget: budget,
|
|
810
955
|
});
|
|
956
|
+
const vanishReceiptId = await writeVanishReceiptForDecision(root, state, observation, decision.classification);
|
|
957
|
+
const restoredOwner =
|
|
958
|
+
decision.ownerRequired && beforeExit.endpointPresent
|
|
959
|
+
? await this.#spawnDetachedOwner(root, sessionId, state.handle.workspace)
|
|
960
|
+
: null;
|
|
961
|
+
if (restoredOwner?.live) {
|
|
962
|
+
const resolved = await resolveOwner(root, sessionId);
|
|
963
|
+
if (resolved.live && resolved.socketPath) {
|
|
964
|
+
updateStateWithRestoredOwner(state, state.handle.ownerHandle.leasePath, resolved);
|
|
965
|
+
if (restoredOwner.tmuxSessionName)
|
|
966
|
+
state.handle.viewportHandle.tmuxSessionName = restoredOwner.tmuxSessionName;
|
|
967
|
+
await writeSessionState(root, state);
|
|
968
|
+
writeJson(
|
|
969
|
+
buildResponse(state, true, {
|
|
970
|
+
pending: false,
|
|
971
|
+
restoredOwner: true,
|
|
972
|
+
decision,
|
|
973
|
+
observation: { ...observation, lifecycle: state.lifecycle, ownerLive: true },
|
|
974
|
+
ownerExit: beforeExit,
|
|
975
|
+
ownerRuntime: restoredOwner.runtime,
|
|
976
|
+
...(restoredOwner.fallbackReason ? { ownerFallbackReason: restoredOwner.fallbackReason } : {}),
|
|
977
|
+
...(vanishReceiptId ? { vanishReceiptId } : {}),
|
|
978
|
+
}),
|
|
979
|
+
);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const afterExit = await buildOwnerExitEvidence(root, state);
|
|
811
984
|
writeJson(
|
|
812
985
|
buildResponse(
|
|
813
986
|
state,
|
|
814
987
|
false,
|
|
815
988
|
{
|
|
816
989
|
pending: false,
|
|
817
|
-
reason:
|
|
990
|
+
reason: afterExit.reason,
|
|
818
991
|
decision,
|
|
819
992
|
observation: { ...observation, lifecycle: state.lifecycle },
|
|
993
|
+
ownerExit: afterExit,
|
|
994
|
+
...(restoredOwner
|
|
995
|
+
? {
|
|
996
|
+
restoreAttempt: {
|
|
997
|
+
runtime: restoredOwner.runtime,
|
|
998
|
+
live: restoredOwner.live,
|
|
999
|
+
fallbackReason: restoredOwner.fallbackReason,
|
|
1000
|
+
blockerReason: restoredOwner.blockerReason,
|
|
1001
|
+
},
|
|
1002
|
+
}
|
|
1003
|
+
: {}),
|
|
1004
|
+
...(vanishReceiptId ? { vanishReceiptId } : {}),
|
|
820
1005
|
},
|
|
821
1006
|
false,
|
|
822
1007
|
),
|
package/src/commands/launch.ts
CHANGED
|
@@ -36,6 +36,12 @@ export default class Index extends Command {
|
|
|
36
36
|
plan: Flags.string({
|
|
37
37
|
description: "Plan model for architectural planning (or GJC_PLAN_MODEL env)",
|
|
38
38
|
}),
|
|
39
|
+
mpreset: Flags.string({
|
|
40
|
+
description: "Model profile preset to activate for this session",
|
|
41
|
+
}),
|
|
42
|
+
default: Flags.boolean({
|
|
43
|
+
description: "Persist --mpreset as the default model profile",
|
|
44
|
+
}),
|
|
39
45
|
provider: Flags.string({
|
|
40
46
|
description: "Provider to use (legacy; prefer --model)",
|
|
41
47
|
}),
|
|
@@ -136,6 +142,8 @@ export default class Index extends Command {
|
|
|
136
142
|
`# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
|
|
137
143
|
`# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
|
|
138
144
|
`# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
|
|
145
|
+
`# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
|
|
146
|
+
`# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
|
|
139
147
|
`# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
|
|
140
148
|
];
|
|
141
149
|
|
|
@@ -6,13 +6,7 @@ import {
|
|
|
6
6
|
writeCurrentSessionGoalModeState,
|
|
7
7
|
writePendingGoalModeRequest,
|
|
8
8
|
} from "../gjc-runtime/goal-mode-request";
|
|
9
|
-
import {
|
|
10
|
-
buildUltragoalHudSummary,
|
|
11
|
-
getUltragoalStatus,
|
|
12
|
-
readUltragoalLedger,
|
|
13
|
-
runNativeUltragoalCommand,
|
|
14
|
-
} from "../gjc-runtime/ultragoal-runtime";
|
|
15
|
-
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
9
|
+
import { runNativeUltragoalCommand } from "../gjc-runtime/ultragoal-runtime";
|
|
16
10
|
|
|
17
11
|
export default class Ultragoal extends Command {
|
|
18
12
|
static description = "Run native GJC Ultragoal workflow commands";
|
|
@@ -25,20 +19,6 @@ export default class Ultragoal extends Command {
|
|
|
25
19
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
26
20
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
27
21
|
process.exitCode = result.status;
|
|
28
|
-
try {
|
|
29
|
-
const summary = await getUltragoalStatus(process.cwd());
|
|
30
|
-
const ledger = await readUltragoalLedger(process.cwd());
|
|
31
|
-
await syncSkillActiveState({
|
|
32
|
-
cwd: process.cwd(),
|
|
33
|
-
skill: "ultragoal",
|
|
34
|
-
active: summary.exists && summary.status !== "complete",
|
|
35
|
-
phase: summary.status,
|
|
36
|
-
hud: buildUltragoalHudSummary(summary, ledger.at(-1)),
|
|
37
|
-
source: "gjc-ultragoal",
|
|
38
|
-
});
|
|
39
|
-
} catch {
|
|
40
|
-
// HUD sync is best-effort and must not change command semantics.
|
|
41
|
-
}
|
|
42
22
|
if (result.status !== 0 || !shouldActivateGoalMode) return;
|
|
43
23
|
|
|
44
24
|
const cwd = process.cwd();
|
|
@@ -42,7 +42,7 @@ interface ResolvedCanonicalModel {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const TRAILING_MARKER_PATTERN =
|
|
45
|
-
/[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4)$/i;
|
|
45
|
+
/[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|max|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4)$/i;
|
|
46
46
|
const WRAPPER_PREFIXES = ["duo-chat-"] as const;
|
|
47
47
|
|
|
48
48
|
let referenceDataCache: CanonicalReferenceData | undefined;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
|
+
import type { Api, Model } from "@gajae-code/ai";
|
|
3
|
+
import type { AgentSession } from "../session/agent-session";
|
|
4
|
+
import {
|
|
5
|
+
aggregateModelProfileRequiredProviders,
|
|
6
|
+
formatAvailableProfileNames,
|
|
7
|
+
resolveProfileBindings,
|
|
8
|
+
} from "./model-profiles";
|
|
9
|
+
import { type GjcModelAssignmentTargetId, isAuthenticated, type ModelRegistry } from "./model-registry";
|
|
10
|
+
import { resolveModelRoleValue } from "./model-resolver";
|
|
11
|
+
import type { Settings } from "./settings";
|
|
12
|
+
|
|
13
|
+
export interface PrepareModelProfileActivationOptions {
|
|
14
|
+
session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId">;
|
|
15
|
+
modelRegistry: Pick<
|
|
16
|
+
ModelRegistry,
|
|
17
|
+
| "getModelProfile"
|
|
18
|
+
| "getModelProfiles"
|
|
19
|
+
| "getAvailableModelProfileNames"
|
|
20
|
+
| "getApiKeyForProvider"
|
|
21
|
+
| "getAll"
|
|
22
|
+
| "resolveCanonicalModel"
|
|
23
|
+
| "getCanonicalVariants"
|
|
24
|
+
| "getCanonicalId"
|
|
25
|
+
>;
|
|
26
|
+
settings: Pick<Settings, "get">;
|
|
27
|
+
profileName: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PreparedModelProfileActivation {
|
|
31
|
+
profileName: string;
|
|
32
|
+
session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId" | "setModelTemporary">;
|
|
33
|
+
settings: Pick<Settings, "get" | "override" | "set" | "flush">;
|
|
34
|
+
previousModel: Model<Api> | undefined;
|
|
35
|
+
previousThinkingLevel: ThinkingLevel | undefined;
|
|
36
|
+
previousAgentModelOverrides: Record<string, string>;
|
|
37
|
+
defaultModel: Model<Api> | undefined;
|
|
38
|
+
defaultThinkingLevel: ThinkingLevel | undefined;
|
|
39
|
+
agentModelOverrides: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatModelProfileCredentialError(profileName: string, providers: readonly string[]): string {
|
|
43
|
+
return `Model profile "${profileName}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function prepareModelProfileActivation(
|
|
47
|
+
options: PrepareModelProfileActivationOptions,
|
|
48
|
+
): Promise<PreparedModelProfileActivation> {
|
|
49
|
+
const profile = options.modelRegistry.getModelProfile(options.profileName);
|
|
50
|
+
if (!profile) {
|
|
51
|
+
const available = formatAvailableProfileNames(options.modelRegistry.getModelProfiles());
|
|
52
|
+
throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const missingProviders: string[] = [];
|
|
56
|
+
for (const provider of aggregateModelProfileRequiredProviders(profile.requiredProviders, profile)) {
|
|
57
|
+
const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.session.sessionId);
|
|
58
|
+
if (!isAuthenticated(apiKey)) {
|
|
59
|
+
missingProviders.push(provider);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (missingProviders.length > 0) {
|
|
63
|
+
throw new Error(formatModelProfileCredentialError(options.profileName, missingProviders));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const availableModels = options.modelRegistry.getAll();
|
|
67
|
+
const bindings = resolveProfileBindings(profile);
|
|
68
|
+
const resolvedDefault = bindings.defaultSelector
|
|
69
|
+
? resolveModelRoleValue(bindings.defaultSelector, availableModels, {
|
|
70
|
+
settings: options.settings as Settings,
|
|
71
|
+
modelRegistry: options.modelRegistry,
|
|
72
|
+
})
|
|
73
|
+
: undefined;
|
|
74
|
+
if (bindings.defaultSelector && !resolvedDefault?.model) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Model profile "${options.profileName}" default selector did not resolve: ${bindings.defaultSelector}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const agentModelOverrides: Record<string, string> = {};
|
|
81
|
+
for (const [role, selector] of Object.entries(bindings.agentModelOverrides) as [
|
|
82
|
+
GjcModelAssignmentTargetId,
|
|
83
|
+
string,
|
|
84
|
+
][]) {
|
|
85
|
+
const resolved = resolveModelRoleValue(selector, availableModels, {
|
|
86
|
+
settings: options.settings as Settings,
|
|
87
|
+
modelRegistry: options.modelRegistry,
|
|
88
|
+
});
|
|
89
|
+
if (!resolved.model) {
|
|
90
|
+
throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
|
|
91
|
+
}
|
|
92
|
+
agentModelOverrides[role] = selector;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
profileName: options.profileName,
|
|
97
|
+
session: options.session as PreparedModelProfileActivation["session"],
|
|
98
|
+
settings: options.settings as PreparedModelProfileActivation["settings"],
|
|
99
|
+
previousModel: options.session.model,
|
|
100
|
+
previousThinkingLevel: options.session.thinkingLevel,
|
|
101
|
+
previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
|
|
102
|
+
defaultModel: resolvedDefault?.model,
|
|
103
|
+
defaultThinkingLevel: resolvedDefault?.thinkingLevel,
|
|
104
|
+
agentModelOverrides,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function applyPreparedModelProfileActivation(
|
|
109
|
+
prepared: PreparedModelProfileActivation,
|
|
110
|
+
options: { persistDefault?: boolean } = {},
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const previousModel = prepared.previousModel;
|
|
113
|
+
const previousThinkingLevel = prepared.previousThinkingLevel;
|
|
114
|
+
const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
|
|
115
|
+
const previousPersistedDefault = prepared.settings.get("modelProfile.default");
|
|
116
|
+
let modelChanged = false;
|
|
117
|
+
let overridesChanged = false;
|
|
118
|
+
let defaultChanged = false;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
if (prepared.defaultModel) {
|
|
122
|
+
await prepared.session.setModelTemporary(prepared.defaultModel, prepared.defaultThinkingLevel);
|
|
123
|
+
modelChanged = true;
|
|
124
|
+
}
|
|
125
|
+
if (Object.keys(prepared.agentModelOverrides).length > 0) {
|
|
126
|
+
prepared.settings.override("task.agentModelOverrides", {
|
|
127
|
+
...prepared.settings.get("task.agentModelOverrides"),
|
|
128
|
+
...prepared.agentModelOverrides,
|
|
129
|
+
});
|
|
130
|
+
overridesChanged = true;
|
|
131
|
+
}
|
|
132
|
+
if (options.persistDefault) {
|
|
133
|
+
prepared.settings.set("modelProfile.default", prepared.profileName);
|
|
134
|
+
defaultChanged = true;
|
|
135
|
+
await prepared.settings.flush();
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (defaultChanged) {
|
|
139
|
+
prepared.settings.set("modelProfile.default", previousPersistedDefault);
|
|
140
|
+
}
|
|
141
|
+
if (overridesChanged) {
|
|
142
|
+
prepared.settings.override("task.agentModelOverrides", previousAgentModelOverrides);
|
|
143
|
+
}
|
|
144
|
+
if (modelChanged && previousModel) {
|
|
145
|
+
await prepared.session.setModelTemporary(previousModel, previousThinkingLevel);
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function activateModelProfile(
|
|
152
|
+
options: PrepareModelProfileActivationOptions,
|
|
153
|
+
applyOptions: { persistDefault?: boolean } = {},
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const prepared = await prepareModelProfileActivation(options);
|
|
156
|
+
await applyPreparedModelProfileActivation(prepared, applyOptions);
|
|
157
|
+
}
|