@gajae-code/coding-agent 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/types/async/job-manager.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +4 -4
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
- package/dist/types/hooks/skill-state.d.ts +21 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +143 -11
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +36 -0
- package/src/cli/args.ts +9 -2
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +289 -19
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +22 -4
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +6 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
- package/src/gjc-runtime/ralplan-runtime.ts +213 -36
- package/src/gjc-runtime/state-migrations.ts +54 -7
- package/src/gjc-runtime/state-runtime.ts +461 -64
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-writer.ts +32 -1
- package/src/gjc-runtime/team-runtime.ts +177 -105
- package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
- package/src/gjc-runtime/workflow-manifest.ts +3 -1
- package/src/harness-control-plane/control-endpoint.ts +19 -8
- package/src/harness-control-plane/owner.ts +57 -10
- package/src/harness-control-plane/state-machine.ts +2 -1
- package/src/hooks/skill-state.ts +176 -26
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +61 -18
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +39 -3
- package/src/modes/controllers/input-controller.ts +97 -9
- package/src/modes/controllers/selector-controller.ts +29 -0
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +27 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/subagent.md +12 -7
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +214 -38
- package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
- package/src/skill-state/workflow-state-contract.ts +7 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +29 -5
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +257 -67
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +48 -131
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +48 -7
- package/src/tools/ask.ts +73 -33
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +237 -84
|
@@ -88,13 +88,16 @@ export class ControlServer {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export class EndpointUnreachableError extends Error {
|
|
91
|
-
constructor(
|
|
92
|
-
|
|
91
|
+
constructor(
|
|
92
|
+
readonly socketPath: string,
|
|
93
|
+
readonly reason = "unreachable",
|
|
94
|
+
) {
|
|
95
|
+
super(`endpoint_${reason}:${socketPath}`);
|
|
93
96
|
this.name = "EndpointUnreachableError";
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
/** Call the owner's control endpoint. Rejects with {@link EndpointUnreachableError} when no owner listens. */
|
|
100
|
+
/** Call the owner's control endpoint. Rejects with {@link EndpointUnreachableError} when no owner listens or responds. */
|
|
98
101
|
export function callEndpoint(socketPath: string, req: EndpointRequest, timeoutMs = 5_000): Promise<unknown> {
|
|
99
102
|
return new Promise((resolve, reject) => {
|
|
100
103
|
const socket = net.connect(socketPath);
|
|
@@ -107,7 +110,10 @@ export function callEndpoint(socketPath: string, req: EndpointRequest, timeoutMs
|
|
|
107
110
|
socket.destroy();
|
|
108
111
|
fn();
|
|
109
112
|
};
|
|
110
|
-
const timer = setTimeout(
|
|
113
|
+
const timer = setTimeout(
|
|
114
|
+
() => done(() => reject(new EndpointUnreachableError(socketPath, "timeout"))),
|
|
115
|
+
timeoutMs,
|
|
116
|
+
);
|
|
111
117
|
socket.setEncoding("utf8");
|
|
112
118
|
socket.on("connect", () => socket.write(frame(req)));
|
|
113
119
|
socket.on("data", (chunk: string) => {
|
|
@@ -118,16 +124,21 @@ export function callEndpoint(socketPath: string, req: EndpointRequest, timeoutMs
|
|
|
118
124
|
done(() => {
|
|
119
125
|
try {
|
|
120
126
|
resolve(JSON.parse(line));
|
|
121
|
-
} catch
|
|
122
|
-
reject(
|
|
127
|
+
} catch {
|
|
128
|
+
reject(new EndpointUnreachableError(socketPath, "bad_frame"));
|
|
123
129
|
}
|
|
124
130
|
});
|
|
125
131
|
}
|
|
126
132
|
});
|
|
127
133
|
socket.on("error", (error: NodeJS.ErrnoException) => {
|
|
128
134
|
done(() => {
|
|
129
|
-
if (
|
|
130
|
-
|
|
135
|
+
if (
|
|
136
|
+
error.code === "ENOENT" ||
|
|
137
|
+
error.code === "ECONNREFUSED" ||
|
|
138
|
+
error.code === "ECONNRESET" ||
|
|
139
|
+
error.code === "EPIPE"
|
|
140
|
+
) {
|
|
141
|
+
reject(new EndpointUnreachableError(socketPath, error.code.toLowerCase()));
|
|
131
142
|
} else {
|
|
132
143
|
reject(error);
|
|
133
144
|
}
|
|
@@ -52,6 +52,31 @@ import {
|
|
|
52
52
|
import type { EventEnvelope, GitDelta, Observation, PrimitiveResponse, SessionState, Severity } from "./types";
|
|
53
53
|
import { DEFAULT_RETRY_BUDGET, OBSERVED_SIGNALS } from "./types";
|
|
54
54
|
|
|
55
|
+
function isStartupLivenessBlocker(blocker: string): boolean {
|
|
56
|
+
return blocker === "detached-owner-not-live";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isOwnerVanishedBlocker(blocker: string): boolean {
|
|
60
|
+
return blocker.startsWith("owner-vanished:");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reconcileLiveOwnerState(state: SessionState): { state: SessionState; reconciled: boolean } {
|
|
64
|
+
const blockers = state.blockers.filter(blocker => !isStartupLivenessBlocker(blocker));
|
|
65
|
+
const hadLivenessBlocker = blockers.length !== state.blockers.length;
|
|
66
|
+
const lifecycle =
|
|
67
|
+
hadLivenessBlocker && state.lifecycle === "blocked" && blockers.length === 0 ? "observing" : state.lifecycle;
|
|
68
|
+
if (!hadLivenessBlocker && lifecycle === state.lifecycle) return { state, reconciled: false };
|
|
69
|
+
return {
|
|
70
|
+
state: {
|
|
71
|
+
...state,
|
|
72
|
+
lifecycle,
|
|
73
|
+
blockers,
|
|
74
|
+
updatedAt: new Date().toISOString(),
|
|
75
|
+
},
|
|
76
|
+
reconciled: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
export interface OwnerOptions {
|
|
56
81
|
root: string;
|
|
57
82
|
sessionId: string;
|
|
@@ -139,6 +164,11 @@ export class RuntimeOwner {
|
|
|
139
164
|
async #loadState(): Promise<SessionState> {
|
|
140
165
|
const state = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
141
166
|
if (!state) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
167
|
+
const reconciled = reconcileLiveOwnerState(state);
|
|
168
|
+
if (reconciled.reconciled) {
|
|
169
|
+
await writeSessionState(this.#opts.root, reconciled.state);
|
|
170
|
+
return reconciled.state;
|
|
171
|
+
}
|
|
142
172
|
return state;
|
|
143
173
|
}
|
|
144
174
|
|
|
@@ -369,17 +399,22 @@ export class RuntimeOwner {
|
|
|
369
399
|
|
|
370
400
|
async #recover(): Promise<PrimitiveResponse> {
|
|
371
401
|
const obs = await this.#observeGit();
|
|
372
|
-
const
|
|
402
|
+
const state = await this.#loadState();
|
|
403
|
+
const recoveringPriorVanish = state.blockers.some(isOwnerVanishedBlocker);
|
|
404
|
+
const recoveryObservation: Observation = recoveringPriorVanish
|
|
405
|
+
? { ...obs, ownerLive: false, risk: obs.gitDelta === "dirty" ? "vanished-dirty" : obs.risk }
|
|
406
|
+
: obs;
|
|
407
|
+
const decision = classifyRecovery({ observation: recoveryObservation, retryBudget: { ...DEFAULT_RETRY_BUDGET } });
|
|
373
408
|
let vanishReceiptId: string | null = null;
|
|
374
409
|
if (requiresVanishBeforeAction(decision.classification)) {
|
|
375
|
-
const dirty =
|
|
376
|
-
const p = dirty ? preserveDirtyWorktree(
|
|
410
|
+
const dirty = recoveryObservation.gitDelta === "dirty" || recoveryObservation.gitDelta === "unknown";
|
|
411
|
+
const p = dirty ? preserveDirtyWorktree(recoveryObservation.cwd) : null;
|
|
377
412
|
const evidence: VanishEvidence = {
|
|
378
413
|
classification: decision.classification,
|
|
379
|
-
gitDelta:
|
|
414
|
+
gitDelta: recoveryObservation.gitDelta,
|
|
380
415
|
gitStatusPorcelain: p
|
|
381
416
|
? `tracked:${p.trackedDiffSha256};untracked:${p.untrackedManifest.length}`
|
|
382
|
-
:
|
|
417
|
+
: recoveryObservation.observedSignals.join(","),
|
|
383
418
|
untrackedManifest: p?.untrackedManifest ?? [],
|
|
384
419
|
preservation: p?.stashRef ? "stash" : "snapshot",
|
|
385
420
|
stashRef: p?.stashRef ?? null,
|
|
@@ -391,15 +426,25 @@ export class RuntimeOwner {
|
|
|
391
426
|
sessionId: this.#opts.sessionId,
|
|
392
427
|
family: "vanish",
|
|
393
428
|
source: "owner",
|
|
394
|
-
subject: {
|
|
429
|
+
subject: {
|
|
430
|
+
workspace: recoveryObservation.cwd,
|
|
431
|
+
branch: recoveryObservation.branch,
|
|
432
|
+
head: null,
|
|
433
|
+
commit: null,
|
|
434
|
+
},
|
|
395
435
|
evidence,
|
|
396
436
|
});
|
|
397
437
|
await writeReceiptImmutable(this.#opts.root, this.#opts.sessionId, "vanish", receipt.receiptId, receipt);
|
|
398
438
|
vanishReceiptId = receipt.receiptId;
|
|
399
439
|
}
|
|
400
|
-
|
|
440
|
+
if (vanishReceiptId) {
|
|
441
|
+
state.blockers = state.blockers.filter(blocker => !isOwnerVanishedBlocker(blocker));
|
|
442
|
+
state.lifecycle = state.blockers.length === 0 ? "observing" : state.lifecycle;
|
|
443
|
+
state.updatedAt = new Date(this.#opts.clock ? this.#opts.clock() : Date.now()).toISOString();
|
|
444
|
+
await writeSessionState(this.#opts.root, state);
|
|
445
|
+
}
|
|
401
446
|
await this.#emit(decision.severity, "recover_classified", { classification: decision.classification });
|
|
402
|
-
return this.#response(state, { decision, observation:
|
|
447
|
+
return this.#response(state, { decision, observation: recoveryObservation, vanishReceiptId });
|
|
403
448
|
}
|
|
404
449
|
|
|
405
450
|
async #operate(input: Record<string, unknown>): Promise<PrimitiveResponse> {
|
|
@@ -475,12 +520,14 @@ export class RuntimeOwner {
|
|
|
475
520
|
|
|
476
521
|
async #submit(input: Record<string, unknown>): Promise<PrimitiveResponse> {
|
|
477
522
|
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
523
|
+
const state = await this.#loadState();
|
|
478
524
|
if (!prompt) {
|
|
479
|
-
const state = await this.#loadState();
|
|
480
525
|
return this.#response(state, { accepted: false, reason: "empty-prompt" }, false);
|
|
481
526
|
}
|
|
527
|
+
if (state.lifecycle === "blocked") {
|
|
528
|
+
return this.#response(state, { accepted: false, reason: "lifecycle-blocked" }, false);
|
|
529
|
+
}
|
|
482
530
|
const result = await singleFlightAccept(this.#opts.rpc, prompt, this.#opts.acceptanceTimeoutMs);
|
|
483
|
-
const state = await this.#loadState();
|
|
484
531
|
if (result.accepted) {
|
|
485
532
|
state.lifecycle = "observing";
|
|
486
533
|
state.updatedAt = new Date(this.#opts.clock ? this.#opts.clock() : Date.now()).toISOString();
|
|
@@ -57,8 +57,9 @@ export function nextAllowedActions(lifecycle: HarnessLifecycle, ownerLive: boole
|
|
|
57
57
|
// `start` creates a new session; never re-applicable to an existing record.
|
|
58
58
|
add("start", false, "session-already-exists");
|
|
59
59
|
|
|
60
|
-
// `submit` is owner-routed: it requires a live owner and a non-terminal lifecycle.
|
|
60
|
+
// `submit` is owner-routed: it requires a live owner and a non-blocked, non-terminal lifecycle.
|
|
61
61
|
if (terminal) add("submit", false, `lifecycle-terminal:${lifecycle}`);
|
|
62
|
+
else if (lifecycle === "blocked") add("submit", false, "lifecycle-blocked");
|
|
62
63
|
else if (!ownerLive) add("submit", false, "owner-not-live");
|
|
63
64
|
else add("submit", true);
|
|
64
65
|
|
package/src/hooks/skill-state.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
|
|
3
|
-
import {
|
|
3
|
+
import { ModeStateSchema, SkillActiveStateSchema } from "../gjc-runtime/state-schema";
|
|
4
|
+
import { writeJsonAtomic, writeWorkflowEnvelopeAtomic } from "../gjc-runtime/state-writer";
|
|
4
5
|
import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
|
|
5
6
|
import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
|
|
6
7
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
type SkillActiveEntry,
|
|
9
10
|
type SkillActiveState,
|
|
10
11
|
} from "../skill-state/active-state";
|
|
12
|
+
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
11
13
|
import {
|
|
12
14
|
compareSkillKeywordMatches,
|
|
13
15
|
GJC_SKILL_KEYWORD_DEFINITIONS,
|
|
@@ -218,14 +220,36 @@ function skillStatePath(stateDir: string, sessionId?: string): string {
|
|
|
218
220
|
return path.join(stateDir, SKILL_ACTIVE_STATE_FILE);
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
function warnInvalidState(kind: string, filePath: string, error: string): void {
|
|
224
|
+
console.warn(`gjc skill-state: invalid ${kind} at ${filePath}: ${error}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function readValidatedJsonFile<T>(
|
|
228
|
+
filePath: string,
|
|
229
|
+
kind: string,
|
|
230
|
+
schema: { safeParse: (value: unknown) => { success: true } | { success: false; error: { message: string } } },
|
|
231
|
+
): Promise<T | null> {
|
|
232
|
+
let raw: string;
|
|
222
233
|
try {
|
|
223
|
-
|
|
224
|
-
return JSON.parse(raw) as T;
|
|
234
|
+
raw = await Bun.file(filePath).text();
|
|
225
235
|
} catch (error) {
|
|
226
236
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
|
|
237
|
+
warnInvalidState(kind, filePath, `read error: ${(error as Error).message}`);
|
|
227
238
|
return null;
|
|
228
239
|
}
|
|
240
|
+
let value: T;
|
|
241
|
+
try {
|
|
242
|
+
value = JSON.parse(raw) as T;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
warnInvalidState(kind, filePath, `invalid JSON: ${(error as Error).message}`);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const parsed = schema.safeParse(value);
|
|
248
|
+
if (!parsed.success) {
|
|
249
|
+
warnInvalidState(kind, filePath, parsed.error.message);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
229
253
|
}
|
|
230
254
|
|
|
231
255
|
async function writeJsonFile(filePath: string, value: unknown, cwd: string): Promise<void> {
|
|
@@ -265,22 +289,41 @@ export async function readVisibleSkillActiveState(
|
|
|
265
289
|
if (!stateDir) return await readCanonicalVisibleSkillActiveState(cwd, sessionId);
|
|
266
290
|
const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
|
|
267
291
|
if (sessionId) {
|
|
268
|
-
const sessionState = await
|
|
292
|
+
const sessionState = await readValidatedJsonFile<SkillActiveState>(
|
|
293
|
+
skillStatePath(resolvedStateDir, sessionId),
|
|
294
|
+
"skill-active-state",
|
|
295
|
+
SkillActiveStateSchema,
|
|
296
|
+
);
|
|
269
297
|
if (sessionState) return sessionState;
|
|
270
298
|
}
|
|
271
|
-
return await
|
|
299
|
+
return await readValidatedJsonFile<SkillActiveState>(
|
|
300
|
+
skillStatePath(resolvedStateDir),
|
|
301
|
+
"skill-active-state",
|
|
302
|
+
SkillActiveStateSchema,
|
|
303
|
+
);
|
|
272
304
|
}
|
|
273
305
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
306
|
+
interface SeedSkillActivationStateInput {
|
|
307
|
+
cwd: string;
|
|
308
|
+
sessionId?: string;
|
|
309
|
+
threadId?: string;
|
|
310
|
+
turnId?: string;
|
|
311
|
+
nowIso?: string;
|
|
312
|
+
stateDir?: string;
|
|
313
|
+
}
|
|
277
314
|
|
|
315
|
+
async function seedSkillActivationState(
|
|
316
|
+
skill: GjcWorkflowSkill,
|
|
317
|
+
keyword: string,
|
|
318
|
+
source: string,
|
|
319
|
+
input: SeedSkillActivationStateInput,
|
|
320
|
+
): Promise<SkillActiveState> {
|
|
278
321
|
const resolvedStateDir = resolveGjcStateDir(input.cwd, input.stateDir);
|
|
279
322
|
const nowIso = input.nowIso ?? new Date().toISOString();
|
|
280
|
-
const phase = initialPhaseForSkill(
|
|
281
|
-
const initializedStatePath = modeStatePath(resolvedStateDir,
|
|
323
|
+
const phase = initialPhaseForSkill(skill);
|
|
324
|
+
const initializedStatePath = modeStatePath(resolvedStateDir, skill, input.sessionId);
|
|
282
325
|
const entry: SkillActiveEntry = {
|
|
283
|
-
skill
|
|
326
|
+
skill,
|
|
284
327
|
phase,
|
|
285
328
|
active: true,
|
|
286
329
|
activated_at: nowIso,
|
|
@@ -292,41 +335,102 @@ export async function recordSkillActivation(input: RecordSkillActivationInput):
|
|
|
292
335
|
const state: SkillActiveState = {
|
|
293
336
|
version: 1,
|
|
294
337
|
active: true,
|
|
295
|
-
skill
|
|
296
|
-
keyword
|
|
338
|
+
skill,
|
|
339
|
+
keyword,
|
|
297
340
|
phase,
|
|
298
341
|
activated_at: nowIso,
|
|
299
342
|
updated_at: nowIso,
|
|
300
|
-
source
|
|
343
|
+
source,
|
|
301
344
|
...(input.sessionId ? { session_id: input.sessionId } : {}),
|
|
302
345
|
...(input.threadId ? { thread_id: input.threadId } : {}),
|
|
303
346
|
...(input.turnId ? { turn_id: input.turnId } : {}),
|
|
304
|
-
initialized_mode:
|
|
347
|
+
initialized_mode: skill,
|
|
305
348
|
initialized_state_path: initializedStatePath,
|
|
306
349
|
active_skills: [entry],
|
|
307
350
|
};
|
|
308
351
|
const modeState: ModeState = {
|
|
309
352
|
active: true,
|
|
353
|
+
version: WORKFLOW_STATE_VERSION,
|
|
310
354
|
current_phase: phase,
|
|
311
|
-
skill
|
|
355
|
+
skill,
|
|
312
356
|
cwd: input.cwd,
|
|
313
357
|
updated_at: nowIso,
|
|
314
358
|
...(input.sessionId ? { session_id: input.sessionId } : {}),
|
|
315
359
|
...(input.threadId ? { thread_id: input.threadId } : {}),
|
|
316
360
|
...(input.turnId ? { turn_id: input.turnId } : {}),
|
|
317
361
|
};
|
|
318
|
-
if (
|
|
362
|
+
if (skill === "deep-interview") {
|
|
319
363
|
modeState.threshold = DEFAULT_DEEP_INTERVIEW_AMBIGUITY_THRESHOLD;
|
|
320
364
|
modeState.threshold_source = "default";
|
|
321
365
|
}
|
|
322
366
|
|
|
323
|
-
await
|
|
367
|
+
await writeWorkflowEnvelopeAtomic(initializedStatePath, modeState, {
|
|
368
|
+
cwd: input.cwd,
|
|
369
|
+
receipt: {
|
|
370
|
+
cwd: input.cwd,
|
|
371
|
+
skill,
|
|
372
|
+
owner: "gjc-hook",
|
|
373
|
+
command: source,
|
|
374
|
+
sessionId: input.sessionId,
|
|
375
|
+
},
|
|
376
|
+
audit: { category: "state", verb: "write", owner: "gjc-hook", skill },
|
|
377
|
+
});
|
|
324
378
|
await writeJsonFile(skillStatePath(resolvedStateDir, input.sessionId), state, input.cwd);
|
|
325
|
-
if (
|
|
326
|
-
|
|
379
|
+
if (input.sessionId) {
|
|
380
|
+
await writeJsonFile(skillStatePath(resolvedStateDir), state, input.cwd);
|
|
381
|
+
}
|
|
327
382
|
return state;
|
|
328
383
|
}
|
|
329
384
|
|
|
385
|
+
export async function recordSkillActivation(input: RecordSkillActivationInput): Promise<SkillActiveState | null> {
|
|
386
|
+
const match = detectPrimarySkillKeyword(input.text);
|
|
387
|
+
if (!match) return null;
|
|
388
|
+
return await seedSkillActivationState(match.skill, match.keyword, "gjc-skill-state-hook", input);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface EnsureWorkflowSkillActivationInput {
|
|
392
|
+
cwd: string;
|
|
393
|
+
skill: string;
|
|
394
|
+
sessionId?: string;
|
|
395
|
+
threadId?: string;
|
|
396
|
+
turnId?: string;
|
|
397
|
+
nowIso?: string;
|
|
398
|
+
stateDir?: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Idempotently seed `.gjc/state` for a workflow skill that was invoked directly
|
|
403
|
+
* (e.g. via `/skill:<name>`) rather than through keyword detection. This ensures
|
|
404
|
+
* the mutation guard and Stop hook engage the moment a workflow skill becomes
|
|
405
|
+
* active, instead of relying on the skill prompt to run its own state-init steps.
|
|
406
|
+
*
|
|
407
|
+
* The seed is non-destructive: if an active entry for this skill already exists
|
|
408
|
+
* (for example after a `gjc state handoff` promotion that carries
|
|
409
|
+
* `handoff_from`/`handoff_at` lineage), nothing is written so lineage is
|
|
410
|
+
* preserved. Non-workflow skills are ignored.
|
|
411
|
+
*/
|
|
412
|
+
export async function ensureWorkflowSkillActivationState(
|
|
413
|
+
input: EnsureWorkflowSkillActivationInput,
|
|
414
|
+
): Promise<SkillActiveState | null> {
|
|
415
|
+
const skill = input.skill.trim();
|
|
416
|
+
if (!isGjcWorkflowSkill(skill)) return null;
|
|
417
|
+
const existing = await readVisibleSkillActiveState(input.cwd, input.sessionId, input.stateDir);
|
|
418
|
+
const alreadyActive = listActiveSkills(existing).some(
|
|
419
|
+
entry =>
|
|
420
|
+
entry.skill === skill &&
|
|
421
|
+
(existing ? entryMatchesContext(entry, existing, input.sessionId, input.threadId) : true),
|
|
422
|
+
);
|
|
423
|
+
if (alreadyActive) return existing;
|
|
424
|
+
return await seedSkillActivationState(skill, `/skill:${skill}`, "gjc-skill-invocation", {
|
|
425
|
+
cwd: input.cwd,
|
|
426
|
+
sessionId: input.sessionId,
|
|
427
|
+
threadId: input.threadId,
|
|
428
|
+
turnId: input.turnId,
|
|
429
|
+
nowIso: input.nowIso,
|
|
430
|
+
stateDir: input.stateDir,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
330
434
|
function isTerminalModeState(state: ModeState | null): boolean {
|
|
331
435
|
if (state?.active !== true) return true;
|
|
332
436
|
const phase = String(state.current_phase ?? "")
|
|
@@ -335,6 +439,45 @@ function isTerminalModeState(state: ModeState | null): boolean {
|
|
|
335
439
|
return ["complete", "completed", "handoff", "failed", "cancelled", "canceled", "inactive"].includes(phase);
|
|
336
440
|
}
|
|
337
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Phases that genuinely finish a skill and release the Stop block. Note that
|
|
444
|
+
* "handoff" is intentionally absent: a skill sitting in the handoff phase has
|
|
445
|
+
* declared it is ready to chain but has not yet been demoted/cleared, so it
|
|
446
|
+
* must keep blocking until the chain (or an explicit clear) removes it.
|
|
447
|
+
*/
|
|
448
|
+
const STOP_RELEASING_PHASES = ["complete", "completed", "failed", "cancelled", "canceled", "inactive"] as const;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Handoff workflows must never stop silently — they always have to offer the
|
|
452
|
+
* user a next step (refine, hand off, or finish) via the ask tool. The Stop
|
|
453
|
+
* hook keeps blocking these even in the "handoff" phase until they are demoted
|
|
454
|
+
* (active:false) or cleared.
|
|
455
|
+
*/
|
|
456
|
+
function isHandoffRequiredSkill(skill: GjcWorkflowSkill): boolean {
|
|
457
|
+
return skill === "deep-interview" || skill === "ralplan";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Decide whether an active-state entry's mode-state releases the Stop block.
|
|
462
|
+
*
|
|
463
|
+
* For handoff-required skills a missing or unreadable mode-state does NOT
|
|
464
|
+
* release the block: those workflows must always end by offering the user a
|
|
465
|
+
* next step, so the `skill-active-state.json` entry stays authoritative until
|
|
466
|
+
* the skill is demoted or cleared. For other skills a missing/corrupt
|
|
467
|
+
* mode-state preserves the historical fail-open behavior so a broken state file
|
|
468
|
+
* cannot lock a session.
|
|
469
|
+
*/
|
|
470
|
+
function modeStateReleasesStop(state: ModeState | null, handoffRequired: boolean): boolean {
|
|
471
|
+
if (!state) return !handoffRequired;
|
|
472
|
+
if (state.active !== true) return true;
|
|
473
|
+
const phase = String(state.current_phase ?? "")
|
|
474
|
+
.trim()
|
|
475
|
+
.toLowerCase();
|
|
476
|
+
if ((STOP_RELEASING_PHASES as readonly string[]).includes(phase)) return true;
|
|
477
|
+
if (!handoffRequired && phase === "handoff") return true;
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
338
481
|
async function readVisibleModeState(
|
|
339
482
|
cwd: string,
|
|
340
483
|
skill: GjcWorkflowSkill,
|
|
@@ -344,11 +487,11 @@ async function readVisibleModeState(
|
|
|
344
487
|
const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
|
|
345
488
|
if (sessionId) {
|
|
346
489
|
const sessionStatePath = modeStatePath(resolvedStateDir, skill, sessionId);
|
|
347
|
-
const sessionState = await
|
|
490
|
+
const sessionState = await readValidatedJsonFile<ModeState>(sessionStatePath, "mode-state", ModeStateSchema);
|
|
348
491
|
if (sessionState) return { state: sessionState, statePath: sessionStatePath };
|
|
349
492
|
}
|
|
350
493
|
const rootStatePath = modeStatePath(resolvedStateDir, skill);
|
|
351
|
-
const rootState = await
|
|
494
|
+
const rootState = await readValidatedJsonFile<ModeState>(rootStatePath, "mode-state", ModeStateSchema);
|
|
352
495
|
if (!rootState) return null;
|
|
353
496
|
return { state: rootState, statePath: rootStatePath };
|
|
354
497
|
}
|
|
@@ -421,8 +564,13 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
|
|
|
421
564
|
if (!skillState || activeEntries.length === 0) return null;
|
|
422
565
|
|
|
423
566
|
for (const entry of activeEntries) {
|
|
424
|
-
const modeState = await
|
|
425
|
-
|
|
567
|
+
const modeState = await readValidatedJsonFile<ModeState>(
|
|
568
|
+
modeStatePath(resolvedStateDir, entry.skill, input.sessionId),
|
|
569
|
+
"mode-state",
|
|
570
|
+
ModeStateSchema,
|
|
571
|
+
);
|
|
572
|
+
const handoffRequired = isHandoffRequiredSkill(entry.skill);
|
|
573
|
+
if (modeStateReleasesStop(modeState, handoffRequired)) continue;
|
|
426
574
|
const phase = String(modeState?.current_phase ?? entry.phase ?? skillState.phase ?? "active");
|
|
427
575
|
const statePath = modeStatePath(resolvedStateDir, entry.skill, input.sessionId);
|
|
428
576
|
if (entry.skill === "ultragoal") {
|
|
@@ -450,7 +598,9 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
|
|
|
450
598
|
}
|
|
451
599
|
}
|
|
452
600
|
}
|
|
453
|
-
const systemMessage =
|
|
601
|
+
const systemMessage = handoffRequired
|
|
602
|
+
? `GJC handoff skill "${entry.skill}" must not stop without offering a next step (phase: ${phase}; state: ${statePath}). Use the ask tool to present the next handoff step — e.g. refine further, hand off to ralplan/team/ultragoal, or finish — then chain or explicitly clear the skill before stopping.`
|
|
603
|
+
: `GJC skill "${entry.skill}" is still active (phase: ${phase}; state: ${statePath}). Continue or explicitly finish/cancel the skill before stopping.`;
|
|
454
604
|
return {
|
|
455
605
|
decision: "block",
|
|
456
606
|
reason: systemMessage,
|
|
@@ -1,23 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for agent:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves agent output IDs against
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the shared context.
|
|
4
|
+
* Resolves agent output IDs only against artifacts directories explicitly
|
|
5
|
+
* authorized by the caller's ResolveContext. Parents and subagents can share
|
|
6
|
+
* outputs by passing their tree's artifacts dir at that API boundary.
|
|
8
7
|
*
|
|
9
8
|
* URL forms:
|
|
10
9
|
* - agent://<id> - Full output content
|
|
11
10
|
* - agent://<id>/<path> - JSON extraction via path form
|
|
12
11
|
* - agent://<id>?q=<query> - JSON extraction via query form
|
|
13
12
|
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
14
|
import * as fs from "node:fs/promises";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { isEnoent } from "@gajae-code/utils";
|
|
17
17
|
import { applyQuery, pathToQuery } from "./json-query";
|
|
18
|
-
import {
|
|
19
|
-
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
18
|
+
import { authorizedArtifactsDirsFromContext } from "./registry-helpers";
|
|
19
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
|
|
20
20
|
|
|
21
|
+
interface AgentOutputMetadata {
|
|
22
|
+
id: string;
|
|
23
|
+
kind: "agent-output";
|
|
24
|
+
sizeBytes: number;
|
|
25
|
+
lineCount: number;
|
|
26
|
+
sha256: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isAgentOutputMetadata(value: unknown, outputId: string): value is AgentOutputMetadata {
|
|
31
|
+
if (!value || typeof value !== "object") return false;
|
|
32
|
+
const meta = value as Record<string, unknown>;
|
|
33
|
+
return (
|
|
34
|
+
meta.id === outputId &&
|
|
35
|
+
meta.kind === "agent-output" &&
|
|
36
|
+
typeof meta.sizeBytes === "number" &&
|
|
37
|
+
typeof meta.lineCount === "number" &&
|
|
38
|
+
typeof meta.sha256 === "string" &&
|
|
39
|
+
typeof meta.createdAt === "string"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function verifyAgentOutputMetadata(outputId: string, foundPath: string, bytes: Buffer): Promise<void> {
|
|
44
|
+
const metaPath = `${foundPath}.meta.json`;
|
|
45
|
+
let metaRaw: string;
|
|
46
|
+
try {
|
|
47
|
+
metaRaw = await Bun.file(metaPath).text();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (isEnoent(err)) throw new Error(`agent://${outputId} missing metadata`);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
let parsed: unknown;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(metaRaw);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`agent://${outputId} malformed metadata`);
|
|
57
|
+
}
|
|
58
|
+
if (!isAgentOutputMetadata(parsed, outputId)) {
|
|
59
|
+
throw new Error(`agent://${outputId} malformed metadata`);
|
|
60
|
+
}
|
|
61
|
+
const stat = await fs.stat(foundPath);
|
|
62
|
+
if (stat.size !== parsed.sizeBytes || bytes.byteLength !== parsed.sizeBytes) {
|
|
63
|
+
throw new Error(`agent://${outputId} size mismatch`);
|
|
64
|
+
}
|
|
65
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
66
|
+
if (sha256 !== parsed.sha256) {
|
|
67
|
+
throw new Error(`agent://${outputId} hash mismatch`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
21
70
|
/**
|
|
22
71
|
* Handler for agent:// URLs.
|
|
23
72
|
*
|
|
@@ -28,11 +77,17 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
28
77
|
readonly scheme = "agent";
|
|
29
78
|
readonly immutable = true;
|
|
30
79
|
|
|
31
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
80
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
32
81
|
const outputId = url.rawHost || url.hostname;
|
|
33
82
|
if (!outputId) {
|
|
34
83
|
throw new Error("agent:// URL requires an output ID: agent://<id>");
|
|
35
84
|
}
|
|
85
|
+
// Output IDs address a single file inside a session artifacts dir. Reject
|
|
86
|
+
// path separators / traversal so a crafted id cannot escape the dir via
|
|
87
|
+
// path.join(dir, `${outputId}.md`).
|
|
88
|
+
if (outputId.includes("/") || outputId.includes("\\") || outputId.includes("..")) {
|
|
89
|
+
throw new Error(`agent://${outputId} invalid id: path separators are not allowed`);
|
|
90
|
+
}
|
|
36
91
|
|
|
37
92
|
const urlPath = url.pathname;
|
|
38
93
|
const queryParam = url.searchParams.get("q");
|
|
@@ -43,7 +98,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
43
98
|
throw new Error("agent:// URL cannot combine path extraction with ?q=");
|
|
44
99
|
}
|
|
45
100
|
|
|
46
|
-
const dirs =
|
|
101
|
+
const dirs = authorizedArtifactsDirsFromContext(context);
|
|
47
102
|
|
|
48
103
|
if (dirs.length === 0) {
|
|
49
104
|
throw new Error("No session - agent outputs unavailable");
|
|
@@ -51,7 +106,6 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
51
106
|
|
|
52
107
|
let foundPath: string | undefined;
|
|
53
108
|
let anyDirExists = false;
|
|
54
|
-
const availableIds = new Set<string>();
|
|
55
109
|
|
|
56
110
|
for (const dir of dirs) {
|
|
57
111
|
try {
|
|
@@ -64,18 +118,10 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
64
118
|
const candidate = path.join(dir, `${outputId}.md`);
|
|
65
119
|
try {
|
|
66
120
|
await fs.stat(candidate);
|
|
121
|
+
if (foundPath) throw new Error(`agent://${outputId} ambiguous id in authorized artifacts`);
|
|
67
122
|
foundPath = candidate;
|
|
68
|
-
break;
|
|
69
123
|
} catch (err) {
|
|
70
124
|
if (!isEnoent(err)) throw err;
|
|
71
|
-
try {
|
|
72
|
-
const files = await fs.readdir(dir);
|
|
73
|
-
for (const f of files) {
|
|
74
|
-
if (f.endsWith(".md")) availableIds.add(f.replace(/\.md$/, ""));
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
// Listing failures are non-fatal; continue searching.
|
|
78
|
-
}
|
|
79
125
|
}
|
|
80
126
|
}
|
|
81
127
|
|
|
@@ -84,11 +130,12 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
84
130
|
}
|
|
85
131
|
|
|
86
132
|
if (!foundPath) {
|
|
87
|
-
|
|
88
|
-
throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
|
|
133
|
+
throw new Error(`agent://${outputId} not found`);
|
|
89
134
|
}
|
|
90
135
|
|
|
91
|
-
const
|
|
136
|
+
const rawBytes = Buffer.from(await Bun.file(foundPath).arrayBuffer());
|
|
137
|
+
await verifyAgentOutputMetadata(outputId, foundPath, rawBytes);
|
|
138
|
+
const rawContent = rawBytes.toString("utf8");
|
|
92
139
|
const notes: string[] = [];
|
|
93
140
|
let content = rawContent;
|
|
94
141
|
let contentType: InternalResource["contentType"] = "text/markdown";
|