@gajae-code/coding-agent 0.2.5 → 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 +28 -0
- package/dist/types/async/job-manager.d.ts +91 -2
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +10 -4
- package/dist/types/config/settings.d.ts +2 -0
- 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 +6 -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/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/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-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +23 -29
- 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/hook-selector.d.ts +1 -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 +2 -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 +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.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 +198 -14
- 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 +26 -1
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +334 -6
- package/src/cli/args.ts +9 -2
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +862 -0
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +54 -39
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +13 -3
- package/src/config/settings.ts +5 -0
- 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 +372 -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/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
- 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/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +235 -43
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +179 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +1155 -46
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +749 -0
- package/src/gjc-runtime/team-runtime.ts +1255 -189
- package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
- package/src/gjc-runtime/workflow-manifest.ts +427 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +148 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +600 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +98 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +197 -64
- 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/assistant-message.ts +5 -1
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +133 -20
- 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/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +43 -1
- package/src/modes/controllers/input-controller.ts +105 -9
- package/src/modes/controllers/selector-controller.ts +31 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +28 -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 +2 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +39 -4
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +8 -0
- package/src/session/agent-session.ts +445 -71
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +33 -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 +79 -13
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +376 -74
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +54 -134
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +104 -10
- package/src/tools/ask.ts +88 -27
- 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 +423 -79
package/src/hooks/skill-state.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
|
|
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
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
readVisibleSkillActiveState as readCanonicalVisibleSkillActiveState,
|
|
9
|
+
type SkillActiveEntry,
|
|
10
|
+
type SkillActiveState,
|
|
11
|
+
} from "../skill-state/active-state";
|
|
12
|
+
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
13
|
import {
|
|
8
14
|
compareSkillKeywordMatches,
|
|
9
15
|
GJC_SKILL_KEYWORD_DEFINITIONS,
|
|
@@ -74,35 +80,7 @@ export interface SkillKeywordMatch {
|
|
|
74
80
|
priority: number;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
export
|
|
78
|
-
skill: GjcWorkflowSkill;
|
|
79
|
-
phase?: string;
|
|
80
|
-
active?: boolean;
|
|
81
|
-
activated_at?: string;
|
|
82
|
-
updated_at?: string;
|
|
83
|
-
session_id?: string;
|
|
84
|
-
thread_id?: string;
|
|
85
|
-
turn_id?: string;
|
|
86
|
-
hud?: WorkflowHudSummary;
|
|
87
|
-
stale?: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface SkillActiveState {
|
|
91
|
-
version: number;
|
|
92
|
-
active: boolean;
|
|
93
|
-
skill: GjcWorkflowSkill;
|
|
94
|
-
keyword: string;
|
|
95
|
-
phase: string;
|
|
96
|
-
activated_at: string;
|
|
97
|
-
updated_at: string;
|
|
98
|
-
source: "gjc-skill-state-hook";
|
|
99
|
-
session_id?: string;
|
|
100
|
-
thread_id?: string;
|
|
101
|
-
turn_id?: string;
|
|
102
|
-
initialized_mode?: GjcWorkflowSkill;
|
|
103
|
-
initialized_state_path?: string;
|
|
104
|
-
active_skills: SkillActiveEntry[];
|
|
105
|
-
}
|
|
83
|
+
export type { SkillActiveEntry, SkillActiveState } from "../skill-state/active-state";
|
|
106
84
|
|
|
107
85
|
export interface ModeState {
|
|
108
86
|
active?: boolean;
|
|
@@ -242,19 +220,43 @@ function skillStatePath(stateDir: string, sessionId?: string): string {
|
|
|
242
220
|
return path.join(stateDir, SKILL_ACTIVE_STATE_FILE);
|
|
243
221
|
}
|
|
244
222
|
|
|
245
|
-
|
|
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;
|
|
246
233
|
try {
|
|
247
|
-
|
|
248
|
-
return JSON.parse(raw) as T;
|
|
234
|
+
raw = await Bun.file(filePath).text();
|
|
249
235
|
} catch (error) {
|
|
250
236
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
|
|
237
|
+
warnInvalidState(kind, filePath, `read error: ${(error as Error).message}`);
|
|
238
|
+
return null;
|
|
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);
|
|
251
250
|
return null;
|
|
252
251
|
}
|
|
252
|
+
return value;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
256
|
-
await
|
|
257
|
-
|
|
255
|
+
async function writeJsonFile(filePath: string, value: unknown, cwd: string): Promise<void> {
|
|
256
|
+
await writeJsonAtomic(filePath, value, {
|
|
257
|
+
cwd,
|
|
258
|
+
audit: { category: "state", verb: "write", owner: "gjc-hook" },
|
|
259
|
+
});
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
function entryMatchesContext(
|
|
@@ -272,7 +274,11 @@ function entryMatchesContext(
|
|
|
272
274
|
|
|
273
275
|
function listActiveSkills(state: SkillActiveState | null): SkillActiveEntry[] {
|
|
274
276
|
if (!state?.active) return [];
|
|
275
|
-
return state.active_skills.filter(entry => entry.active !== false);
|
|
277
|
+
return (state.active_skills ?? []).filter(entry => entry.active !== false);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isWorkflowActiveEntry(entry: SkillActiveEntry): entry is SkillActiveEntry & { skill: GjcWorkflowSkill } {
|
|
281
|
+
return isGjcWorkflowSkill(entry.skill);
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
export async function readVisibleSkillActiveState(
|
|
@@ -280,24 +286,44 @@ export async function readVisibleSkillActiveState(
|
|
|
280
286
|
sessionId?: string,
|
|
281
287
|
stateDir?: string,
|
|
282
288
|
): Promise<SkillActiveState | null> {
|
|
289
|
+
if (!stateDir) return await readCanonicalVisibleSkillActiveState(cwd, sessionId);
|
|
283
290
|
const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
|
|
284
291
|
if (sessionId) {
|
|
285
|
-
const sessionState = await
|
|
292
|
+
const sessionState = await readValidatedJsonFile<SkillActiveState>(
|
|
293
|
+
skillStatePath(resolvedStateDir, sessionId),
|
|
294
|
+
"skill-active-state",
|
|
295
|
+
SkillActiveStateSchema,
|
|
296
|
+
);
|
|
286
297
|
if (sessionState) return sessionState;
|
|
287
298
|
}
|
|
288
|
-
return await
|
|
299
|
+
return await readValidatedJsonFile<SkillActiveState>(
|
|
300
|
+
skillStatePath(resolvedStateDir),
|
|
301
|
+
"skill-active-state",
|
|
302
|
+
SkillActiveStateSchema,
|
|
303
|
+
);
|
|
289
304
|
}
|
|
290
305
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
306
|
+
interface SeedSkillActivationStateInput {
|
|
307
|
+
cwd: string;
|
|
308
|
+
sessionId?: string;
|
|
309
|
+
threadId?: string;
|
|
310
|
+
turnId?: string;
|
|
311
|
+
nowIso?: string;
|
|
312
|
+
stateDir?: string;
|
|
313
|
+
}
|
|
294
314
|
|
|
315
|
+
async function seedSkillActivationState(
|
|
316
|
+
skill: GjcWorkflowSkill,
|
|
317
|
+
keyword: string,
|
|
318
|
+
source: string,
|
|
319
|
+
input: SeedSkillActivationStateInput,
|
|
320
|
+
): Promise<SkillActiveState> {
|
|
295
321
|
const resolvedStateDir = resolveGjcStateDir(input.cwd, input.stateDir);
|
|
296
322
|
const nowIso = input.nowIso ?? new Date().toISOString();
|
|
297
|
-
const phase = initialPhaseForSkill(
|
|
298
|
-
const initializedStatePath = modeStatePath(resolvedStateDir,
|
|
323
|
+
const phase = initialPhaseForSkill(skill);
|
|
324
|
+
const initializedStatePath = modeStatePath(resolvedStateDir, skill, input.sessionId);
|
|
299
325
|
const entry: SkillActiveEntry = {
|
|
300
|
-
skill
|
|
326
|
+
skill,
|
|
301
327
|
phase,
|
|
302
328
|
active: true,
|
|
303
329
|
activated_at: nowIso,
|
|
@@ -309,41 +335,102 @@ export async function recordSkillActivation(input: RecordSkillActivationInput):
|
|
|
309
335
|
const state: SkillActiveState = {
|
|
310
336
|
version: 1,
|
|
311
337
|
active: true,
|
|
312
|
-
skill
|
|
313
|
-
keyword
|
|
338
|
+
skill,
|
|
339
|
+
keyword,
|
|
314
340
|
phase,
|
|
315
341
|
activated_at: nowIso,
|
|
316
342
|
updated_at: nowIso,
|
|
317
|
-
source
|
|
343
|
+
source,
|
|
318
344
|
...(input.sessionId ? { session_id: input.sessionId } : {}),
|
|
319
345
|
...(input.threadId ? { thread_id: input.threadId } : {}),
|
|
320
346
|
...(input.turnId ? { turn_id: input.turnId } : {}),
|
|
321
|
-
initialized_mode:
|
|
347
|
+
initialized_mode: skill,
|
|
322
348
|
initialized_state_path: initializedStatePath,
|
|
323
349
|
active_skills: [entry],
|
|
324
350
|
};
|
|
325
351
|
const modeState: ModeState = {
|
|
326
352
|
active: true,
|
|
353
|
+
version: WORKFLOW_STATE_VERSION,
|
|
327
354
|
current_phase: phase,
|
|
328
|
-
skill
|
|
355
|
+
skill,
|
|
329
356
|
cwd: input.cwd,
|
|
330
357
|
updated_at: nowIso,
|
|
331
358
|
...(input.sessionId ? { session_id: input.sessionId } : {}),
|
|
332
359
|
...(input.threadId ? { thread_id: input.threadId } : {}),
|
|
333
360
|
...(input.turnId ? { turn_id: input.turnId } : {}),
|
|
334
361
|
};
|
|
335
|
-
if (
|
|
362
|
+
if (skill === "deep-interview") {
|
|
336
363
|
modeState.threshold = DEFAULT_DEEP_INTERVIEW_AMBIGUITY_THRESHOLD;
|
|
337
364
|
modeState.threshold_source = "default";
|
|
338
365
|
}
|
|
339
366
|
|
|
340
|
-
await
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
});
|
|
378
|
+
await writeJsonFile(skillStatePath(resolvedStateDir, input.sessionId), state, input.cwd);
|
|
379
|
+
if (input.sessionId) {
|
|
380
|
+
await writeJsonFile(skillStatePath(resolvedStateDir), state, input.cwd);
|
|
381
|
+
}
|
|
344
382
|
return state;
|
|
345
383
|
}
|
|
346
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
|
+
|
|
347
434
|
function isTerminalModeState(state: ModeState | null): boolean {
|
|
348
435
|
if (state?.active !== true) return true;
|
|
349
436
|
const phase = String(state.current_phase ?? "")
|
|
@@ -352,6 +439,45 @@ function isTerminalModeState(state: ModeState | null): boolean {
|
|
|
352
439
|
return ["complete", "completed", "handoff", "failed", "cancelled", "canceled", "inactive"].includes(phase);
|
|
353
440
|
}
|
|
354
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
|
+
|
|
355
481
|
async function readVisibleModeState(
|
|
356
482
|
cwd: string,
|
|
357
483
|
skill: GjcWorkflowSkill,
|
|
@@ -361,11 +487,11 @@ async function readVisibleModeState(
|
|
|
361
487
|
const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
|
|
362
488
|
if (sessionId) {
|
|
363
489
|
const sessionStatePath = modeStatePath(resolvedStateDir, skill, sessionId);
|
|
364
|
-
const sessionState = await
|
|
490
|
+
const sessionState = await readValidatedJsonFile<ModeState>(sessionStatePath, "mode-state", ModeStateSchema);
|
|
365
491
|
if (sessionState) return { state: sessionState, statePath: sessionStatePath };
|
|
366
492
|
}
|
|
367
493
|
const rootStatePath = modeStatePath(resolvedStateDir, skill);
|
|
368
|
-
const rootState = await
|
|
494
|
+
const rootState = await readValidatedJsonFile<ModeState>(rootStatePath, "mode-state", ModeStateSchema);
|
|
369
495
|
if (!rootState) return null;
|
|
370
496
|
return { state: rootState, statePath: rootStatePath };
|
|
371
497
|
}
|
|
@@ -432,14 +558,19 @@ export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitS
|
|
|
432
558
|
export async function buildSkillStopOutput(input: StopHookInput): Promise<Record<string, unknown> | null> {
|
|
433
559
|
const resolvedStateDir = resolveGjcStateDir(input.cwd, input.stateDir);
|
|
434
560
|
const skillState = await readVisibleSkillActiveState(input.cwd, input.sessionId, input.stateDir);
|
|
435
|
-
const activeEntries = listActiveSkills(skillState)
|
|
436
|
-
|
|
437
|
-
|
|
561
|
+
const activeEntries = listActiveSkills(skillState)
|
|
562
|
+
.filter(isWorkflowActiveEntry)
|
|
563
|
+
.filter(entry => (skillState ? entryMatchesContext(entry, skillState, input.sessionId, input.threadId) : false));
|
|
438
564
|
if (!skillState || activeEntries.length === 0) return null;
|
|
439
565
|
|
|
440
566
|
for (const entry of activeEntries) {
|
|
441
|
-
const modeState = await
|
|
442
|
-
|
|
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;
|
|
443
574
|
const phase = String(modeState?.current_phase ?? entry.phase ?? skillState.phase ?? "active");
|
|
444
575
|
const statePath = modeStatePath(resolvedStateDir, entry.skill, input.sessionId);
|
|
445
576
|
if (entry.skill === "ultragoal") {
|
|
@@ -467,7 +598,9 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
|
|
|
467
598
|
}
|
|
468
599
|
}
|
|
469
600
|
}
|
|
470
|
-
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.`;
|
|
471
604
|
return {
|
|
472
605
|
decision: "block",
|
|
473
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";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for artifact:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves artifact IDs against
|
|
5
|
-
*
|
|
4
|
+
* Resolves artifact IDs only against artifacts directories explicitly authorized
|
|
5
|
+
* by the caller's ResolveContext. Unlike agent://, artifacts are raw text.
|
|
6
6
|
*
|
|
7
7
|
* URL form:
|
|
8
8
|
* - artifact://<id> - Full artifact content
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { isEnoent } from "@gajae-code/utils";
|
|
15
|
-
import {
|
|
16
|
-
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
15
|
+
import { authorizedArtifactsDirsFromContext } from "./registry-helpers";
|
|
16
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
|
|
17
17
|
|
|
18
18
|
export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
19
19
|
readonly scheme = "artifact";
|
|
20
20
|
readonly immutable = true;
|
|
21
21
|
|
|
22
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
22
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
23
23
|
const id = url.rawHost || url.hostname;
|
|
24
24
|
if (!id) {
|
|
25
25
|
throw new Error("artifact:// URL requires a numeric ID: artifact://0");
|
|
@@ -28,7 +28,7 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
|
28
28
|
throw new Error(`artifact:// ID must be numeric, got: ${id}`);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const dirs =
|
|
31
|
+
const dirs = authorizedArtifactsDirsFromContext(context);
|
|
32
32
|
|
|
33
33
|
if (dirs.length === 0) {
|
|
34
34
|
throw new Error("No session - artifacts unavailable");
|
|
@@ -36,7 +36,6 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
|
36
36
|
|
|
37
37
|
let foundPath: string | undefined;
|
|
38
38
|
let anyDirExists = false;
|
|
39
|
-
const availableIds = new Set<string>();
|
|
40
39
|
|
|
41
40
|
for (const dir of dirs) {
|
|
42
41
|
let files: string[];
|
|
@@ -47,14 +46,12 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
|
47
46
|
if (isEnoent(err)) continue;
|
|
48
47
|
throw err;
|
|
49
48
|
}
|
|
50
|
-
const match = files.find(f => f.startsWith(`${id}.`));
|
|
51
|
-
if (match) {
|
|
52
|
-
foundPath = path.join(dir, match);
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
49
|
for (const f of files) {
|
|
56
|
-
|
|
57
|
-
if (
|
|
50
|
+
if (f.endsWith(".meta.json")) continue;
|
|
51
|
+
if (f.startsWith(`${id}.`)) {
|
|
52
|
+
if (foundPath) throw new Error(`artifact://${id} ambiguous id in authorized artifacts`);
|
|
53
|
+
foundPath = path.join(dir, f);
|
|
54
|
+
}
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
57
|
|
|
@@ -63,9 +60,7 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
if (!foundPath) {
|
|
66
|
-
|
|
67
|
-
const availableStr = sorted.length > 0 ? sorted.join(", ") : "none";
|
|
68
|
-
throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
|
|
63
|
+
throw new Error(`artifact://${id} not found`);
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
const content = await Bun.file(foundPath).text();
|