jorgex-stack 1.0.3 → 1.0.4

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.
@@ -0,0 +1,272 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { GoalStore } from "./types.js";
3
+ import { createGoalCommandHandlers } from "./command.js";
4
+ import {
5
+ GOAL_MODE_MARKER_END,
6
+ GOAL_MODE_MARKER_START,
7
+ createGoalSupervisor,
8
+ } from "./supervisor.js";
9
+
10
+ type HookOutput = Record<string, unknown>;
11
+
12
+ interface GoalSessionClient {
13
+ promptAsync?: (input: { prompt: string; sessionID?: string }) => Promise<unknown> | unknown;
14
+ }
15
+
16
+ interface GoalLogger {
17
+ warn?: (message: string, details?: unknown) => void;
18
+ error?: (message: string, details?: unknown) => void;
19
+ }
20
+
21
+ export interface OpenCodeGoalHooksDeps {
22
+ store: GoalStore;
23
+ project: string;
24
+ artifactsRootDir?: string;
25
+ sessionClient?: GoalSessionClient;
26
+ logger?: GoalLogger;
27
+ }
28
+
29
+ export interface OpenCodeGoalHooks {
30
+ event?: (input: { event: { type: string; properties?: unknown } }) => Promise<void>;
31
+ "command.execute.before"?: (input: unknown, output: HookOutput) => Promise<void>;
32
+ "experimental.chat.system.transform"?: (input: unknown, output: { system: string[] }) => Promise<void>;
33
+ "experimental.session.compacting"?: (input: { sessionID?: string }, output: { context: string[] }) => Promise<void>;
34
+ }
35
+
36
+ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGoalHooks {
37
+ const commands = createGoalCommandHandlers({
38
+ store: deps.store,
39
+ project: deps.project,
40
+ artifactsRootDir: deps.artifactsRootDir,
41
+ });
42
+ const supervisor = createGoalSupervisor({
43
+ store: deps.store,
44
+ project: deps.project,
45
+ });
46
+
47
+ return {
48
+ "command.execute.before": async (input, output) => {
49
+ const command = extractCommandName(input);
50
+ if (command !== "goal") return;
51
+
52
+ const response = commands.handleGoalCommand(extractCommandArguments(input));
53
+ appendHookText(input, output, response.message);
54
+ },
55
+
56
+ "experimental.chat.system.transform": async (_input, output) => {
57
+ const block = supervisor.renderSystemContext();
58
+ if (!block) return;
59
+
60
+ if (output.system.length === 0) {
61
+ output.system.push(block);
62
+ return;
63
+ }
64
+
65
+ const lastIndex = output.system.length - 1;
66
+ output.system[lastIndex] = upsertMarkedBlock(output.system[lastIndex]!, block);
67
+ },
68
+
69
+ "experimental.session.compacting": async (_input, output) => {
70
+ const block = supervisor.renderSystemContext();
71
+ if (!block) return;
72
+
73
+ if (!output.context.some((entry) => entry.includes(GOAL_MODE_MARKER_START))) {
74
+ output.context.push(block);
75
+ }
76
+ },
77
+
78
+ event: async ({ event }) => {
79
+ if (event.type !== "session.idle") return;
80
+
81
+ const decision = supervisor.decide();
82
+ if (!decision || decision.type === "pause_for_merge") return;
83
+ if (decision.state.goal.status !== "active") return;
84
+ const sessionID = extractSessionID(event.properties);
85
+ const stateSequence = latestNonAutoContinueSequence(decision.state.events);
86
+ if (hasAutoContinueEventForState(decision.state.events, "goal.auto_continue_requested", stateSequence)) {
87
+ if (!hasAutoContinueEventForState(decision.state.events, "goal.auto_continue_deduped", stateSequence)) {
88
+ deps.store.appendEvent(decision.state.goal.id, {
89
+ type: "goal.auto_continue_deduped",
90
+ message: "Auto-continue skipped because this goal state already requested a continuation.",
91
+ data: { sessionID, stateSequence },
92
+ });
93
+ }
94
+ return;
95
+ }
96
+ if (!deps.sessionClient?.promptAsync) {
97
+ if (!hasAutoContinueEventForState(decision.state.events, "goal.auto_continue_unavailable", stateSequence)) {
98
+ deps.store.appendEvent(decision.state.goal.id, {
99
+ type: "goal.auto_continue_unavailable",
100
+ message: "Auto-continue unavailable: session prompt client missing.",
101
+ data: { sessionID, stateSequence },
102
+ });
103
+ deps.logger?.warn?.("Goal Mode auto-continue unavailable", {
104
+ goalId: decision.state.goal.id,
105
+ sessionID,
106
+ stateSequence,
107
+ });
108
+ }
109
+ return;
110
+ }
111
+ const prompt = supervisor.renderContinuationPrompt(decision.state.goal.id);
112
+ if (!prompt?.trim()) {
113
+ deps.store.appendEvent(decision.state.goal.id, {
114
+ type: "goal.auto_continue_skipped",
115
+ message: "Auto-continue skipped because the continuation prompt was empty.",
116
+ });
117
+ return;
118
+ }
119
+ const dedupeKey = `${decision.state.goal.id}:${sessionID ?? "unknown"}:${stateSequence}`;
120
+ if (autoContinueInFlight.has(dedupeKey)) {
121
+ deps.store.appendEvent(decision.state.goal.id, {
122
+ type: "goal.auto_continue_deduped",
123
+ message: "Auto-continue skipped because a continuation is already in flight.",
124
+ data: { sessionID, stateSequence },
125
+ });
126
+ return;
127
+ }
128
+ autoContinueInFlight.add(dedupeKey);
129
+
130
+ try {
131
+ deps.store.appendEvent(decision.state.goal.id, {
132
+ type: "goal.auto_continue_requested",
133
+ message: `Auto-continue requested for session ${sessionID ?? "unknown"}.`,
134
+ data: { sessionID, stateSequence },
135
+ });
136
+ await deps.sessionClient.promptAsync({
137
+ sessionID,
138
+ prompt,
139
+ });
140
+ } catch (error) {
141
+ deps.store.appendEvent(decision.state.goal.id, {
142
+ type: "goal.auto_continue_failed",
143
+ message: `Auto-continue failed for session ${sessionID ?? "unknown"}.`,
144
+ data: { error: error instanceof Error ? error.message : String(error), sessionID, stateSequence },
145
+ });
146
+ deps.logger?.error?.("Goal Mode auto-continue failed", error);
147
+ } finally {
148
+ autoContinueInFlight.delete(dedupeKey);
149
+ }
150
+ },
151
+ };
152
+ }
153
+
154
+ const autoContinueInFlight = new Set<string>();
155
+
156
+ function extractCommandName(input: unknown): string {
157
+ if (!isRecord(input)) return "";
158
+ const command = input.command ?? input.name;
159
+ return typeof command === "string" ? command.trim().toLowerCase() : "";
160
+ }
161
+
162
+ function extractCommandArguments(input: unknown): string {
163
+ if (!isRecord(input)) return "";
164
+ const direct = input.arguments ?? input.argument ?? input.input;
165
+ if (typeof direct === "string") return direct;
166
+ const args = input.args;
167
+ if (!isRecord(args)) return "";
168
+ const nested = args.arguments ?? args.argument ?? args.input;
169
+ return typeof nested === "string" ? nested : "";
170
+ }
171
+
172
+ function appendHookText(input: unknown, output: HookOutput, text: string): void {
173
+ if (Array.isArray(output.parts)) {
174
+ output.parts.push({
175
+ id: `part_${randomUUID()}`,
176
+ sessionID: extractHookSessionID(input, output),
177
+ messageID: extractHookMessageID(input, output),
178
+ type: "text",
179
+ text,
180
+ synthetic: true,
181
+ });
182
+ return;
183
+ }
184
+ if (typeof output.message === "string") {
185
+ output.message = output.message ? `${output.message}\n\n${text}` : text;
186
+ return;
187
+ }
188
+ if (typeof output.output === "string") {
189
+ output.output = output.output ? `${output.output}\n\n${text}` : text;
190
+ return;
191
+ }
192
+ if (Array.isArray(output.content)) {
193
+ output.content.push({ type: "text", text });
194
+ return;
195
+ }
196
+ throw new Error("Unsupported OpenCode command output contract for Goal Mode.");
197
+ }
198
+
199
+ function extractHookSessionID(input: unknown, output: HookOutput): string {
200
+ const inputRecord = isRecord(input) ? input : undefined;
201
+ const outputRecord = output;
202
+ const direct = inputRecord?.sessionID ?? inputRecord?.sessionId ?? outputRecord.sessionID ?? outputRecord.sessionId;
203
+ if (typeof direct === "string" && direct.trim()) return direct;
204
+
205
+ const message = outputRecord.message ?? outputRecord.info;
206
+ if (isRecord(message)) {
207
+ const sessionID = message.sessionID ?? message.sessionId;
208
+ if (typeof sessionID === "string" && sessionID.trim()) return sessionID;
209
+ }
210
+
211
+ return `session_${randomUUID()}`;
212
+ }
213
+
214
+ function extractHookMessageID(input: unknown, output: HookOutput): string {
215
+ const inputRecord = isRecord(input) ? input : undefined;
216
+ const outputRecord = output;
217
+ const direct = inputRecord?.messageID ?? inputRecord?.messageId ?? outputRecord.messageID ?? outputRecord.messageId;
218
+ if (typeof direct === "string" && direct.trim()) return direct;
219
+
220
+ const message = outputRecord.message ?? outputRecord.info;
221
+ if (isRecord(message)) {
222
+ const id = message.id ?? message.messageID ?? message.messageId;
223
+ if (typeof id === "string" && id.trim()) return id;
224
+ }
225
+
226
+ return `msg_${randomUUID()}`;
227
+ }
228
+
229
+ function upsertMarkedBlock(text: string, block: string): string {
230
+ const start = text.indexOf(GOAL_MODE_MARKER_START);
231
+ const end = text.indexOf(GOAL_MODE_MARKER_END);
232
+ if (start !== -1 && end !== -1 && end > start) {
233
+ return `${text.slice(0, start).trimEnd()}\n\n${block}${text.slice(end + GOAL_MODE_MARKER_END.length)}`;
234
+ }
235
+ return `${text.trimEnd()}\n\n${block}`;
236
+ }
237
+
238
+ function extractSessionID(properties: unknown): string | undefined {
239
+ if (!isRecord(properties)) return undefined;
240
+ if (typeof properties.sessionID === "string") return properties.sessionID;
241
+ const info = properties.info;
242
+ if (isRecord(info) && typeof info.id === "string") return info.id;
243
+ return undefined;
244
+ }
245
+
246
+ const AUTO_CONTINUE_EVENT_PREFIX = "goal.auto_continue_";
247
+
248
+ function latestNonAutoContinueSequence(events: Array<{ type: string; sequence: number }>): number {
249
+ return Math.max(
250
+ 0,
251
+ ...events
252
+ .filter((event) => !event.type.startsWith(AUTO_CONTINUE_EVENT_PREFIX))
253
+ .map((event) => event.sequence),
254
+ );
255
+ }
256
+
257
+ function hasAutoContinueEventForState(
258
+ events: Array<{ type: string; data?: unknown }>,
259
+ type: string,
260
+ stateSequence: number,
261
+ ): boolean {
262
+ return events.some((event) => event.type === type && readEventStateSequence(event.data) === stateSequence);
263
+ }
264
+
265
+ function readEventStateSequence(data: unknown): number | undefined {
266
+ if (!isRecord(data)) return undefined;
267
+ return typeof data.stateSequence === "number" ? data.stateSequence : undefined;
268
+ }
269
+
270
+ function isRecord(value: unknown): value is Record<string, unknown> {
271
+ return typeof value === "object" && value !== null && !Array.isArray(value);
272
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ GOAL_STORE_SCHEMA_VERSION,
3
+ type GoalStatus,
4
+ type GoalStoreSnapshot,
5
+ } from "./types.js";
6
+
7
+ export const GOAL_STATUSES: readonly GoalStatus[] = [
8
+ "active",
9
+ "paused",
10
+ "blocked",
11
+ "waiting_for_merge",
12
+ "budget_limited",
13
+ "failed",
14
+ "complete",
15
+ "cancelled",
16
+ ];
17
+
18
+ export const TERMINAL_GOAL_STATUSES = new Set<GoalStatus>([
19
+ "failed",
20
+ "complete",
21
+ "cancelled",
22
+ ]);
23
+
24
+ const ALLOWED_TRANSITIONS: Record<GoalStatus, readonly GoalStatus[]> = {
25
+ active: [
26
+ "paused",
27
+ "blocked",
28
+ "waiting_for_merge",
29
+ "budget_limited",
30
+ "failed",
31
+ "complete",
32
+ "cancelled",
33
+ ],
34
+ paused: [
35
+ "active",
36
+ "blocked",
37
+ "waiting_for_merge",
38
+ "budget_limited",
39
+ "failed",
40
+ "cancelled",
41
+ ],
42
+ blocked: [
43
+ "active",
44
+ "paused",
45
+ "waiting_for_merge",
46
+ "budget_limited",
47
+ "failed",
48
+ "complete",
49
+ "cancelled",
50
+ ],
51
+ waiting_for_merge: ["active", "blocked", "budget_limited", "failed", "cancelled"],
52
+ budget_limited: ["active", "blocked", "waiting_for_merge", "failed", "cancelled"],
53
+ failed: [],
54
+ complete: [],
55
+ cancelled: [],
56
+ };
57
+
58
+ export function createEmptyGoalStoreSnapshot(): GoalStoreSnapshot {
59
+ return {
60
+ schemaVersion: GOAL_STORE_SCHEMA_VERSION,
61
+ nextEventSequence: 1,
62
+ goals: [],
63
+ events: [],
64
+ phases: [],
65
+ worktrees: [],
66
+ pullRequests: [],
67
+ };
68
+ }
69
+
70
+ export function isTerminalGoalStatus(status: GoalStatus): boolean {
71
+ return TERMINAL_GOAL_STATUSES.has(status);
72
+ }
73
+
74
+ export function assertGoalTransition(from: GoalStatus, to: GoalStatus): void {
75
+ if (from === to) return;
76
+
77
+ if (isTerminalGoalStatus(from)) {
78
+ throw new Error(`Cannot transition terminal goal from ${from} to ${to}.`);
79
+ }
80
+
81
+ const allowed = ALLOWED_TRANSITIONS[from];
82
+ if (!allowed.includes(to)) {
83
+ throw new Error(`Invalid transition from ${from} to ${to}.`);
84
+ }
85
+ }