@aryaminus/controlkeel-opencode 0.2.25 → 0.2.26

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.
@@ -180,7 +180,18 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
180
180
  }
181
181
  }
182
182
 
183
- const resolveReviewScope = async () => {
183
+ const resolveReviewScope = async (
184
+ explicitTaskId?: string | number | null,
185
+ explicitSessionId?: string | number | null
186
+ ) => {
187
+ if (explicitTaskId || explicitSessionId) {
188
+ return {
189
+ taskId: explicitTaskId != null ? String(explicitTaskId) : null,
190
+ sessionId: explicitSessionId != null ? String(explicitSessionId) : null,
191
+ source: "explicit",
192
+ }
193
+ }
194
+
184
195
  const envTaskId = process.env.CONTROLKEEL_TASK_ID
185
196
  const envSessionId = process.env.CONTROLKEEL_SESSION_ID
186
197
 
@@ -215,26 +226,44 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
215
226
  const contextTaskId = contextPayload?.current_task?.id
216
227
  const contextSessionId = contextPayload?.session_id
217
228
 
218
- if (!contextTaskId && !contextSessionId) {
219
- throw new Error("ControlKeel context did not include a session_id or current_task.id")
229
+ if (contextTaskId || contextSessionId) {
230
+ return {
231
+ taskId: contextTaskId != null ? String(contextTaskId) : null,
232
+ sessionId: contextSessionId != null ? String(contextSessionId) : null,
233
+ source: "context",
234
+ }
220
235
  }
221
236
 
222
- return {
223
- taskId: contextTaskId != null ? String(contextTaskId) : null,
224
- sessionId: contextSessionId != null ? String(contextSessionId) : null,
225
- source: "context",
237
+ try {
238
+ const bindingPayload = parseJson(await Bun.file(`${directory}/controlkeel/project.json`).text())
239
+ const bindingSessionId = bindingPayload?.session_id
240
+
241
+ if (bindingSessionId) {
242
+ return {
243
+ taskId: null,
244
+ sessionId: String(bindingSessionId),
245
+ source: "binding",
246
+ }
247
+ }
248
+ } catch (_error) {
226
249
  }
250
+
251
+ throw new Error(
252
+ "ControlKeel could not infer review scope. Set CONTROLKEEL_TASK_ID or CONTROLKEEL_SESSION_ID, or pass task_id/session_id to submit_plan."
253
+ )
227
254
  }
228
255
 
229
256
  const submitPlan = async (
230
257
  body: string,
231
258
  submittedBy: string,
232
259
  title?: string,
233
- waitTimeoutSeconds?: number
260
+ waitTimeoutSeconds?: number,
261
+ taskId?: string | number | null,
262
+ sessionId?: string | number | null
234
263
  ) => {
235
264
  await ensurePlanSubmitSupport()
236
265
 
237
- const reviewScope = await resolveReviewScope()
266
+ const reviewScope = await resolveReviewScope(taskId, sessionId)
238
267
  const waitTimeout = Number(waitTimeoutSeconds ?? process.env.CONTROLKEEL_REVIEW_WAIT_TIMEOUT ?? 30)
239
268
  const waitTimeoutSecondsSafe = Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 30
240
269
 
@@ -292,19 +321,35 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
292
321
  const waitOut = await new Response(waitProc.stdout).text()
293
322
  const waitErr = await new Response(waitProc.stderr).text()
294
323
  const waitExit = await waitProc.exited
324
+ const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
325
+ const waitMessage = typeof waitPayload?.message === "string" ? waitPayload.message.toLowerCase() : ""
326
+ const waitError = typeof waitPayload?.error === "string" ? waitPayload.error.toLowerCase() : ""
327
+ const waitTimedOut = waitMessage.includes("timeout") || waitError.includes("timed out")
328
+ const waitPending = waitPayload?.review?.status === "pending"
295
329
 
296
330
  if (waitExit !== 0) {
331
+ if (waitTimedOut && waitPending) {
332
+ return {
333
+ reviewId,
334
+ submitPayload,
335
+ waitPayload,
336
+ browserUrl: waitPayload?.browser_url ?? submitPayload?.browser_url,
337
+ status: "pending",
338
+ feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
339
+ timedOut: true,
340
+ }
341
+ }
342
+
297
343
  throw new Error(
298
344
  `controlkeel review plan wait failed with exit code ${waitExit}${waitErr.trim() ? `: ${waitErr.trim()}` : ""}`
299
345
  )
300
346
  }
301
347
 
302
- const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
303
348
  return {
304
349
  reviewId,
305
350
  submitPayload,
306
351
  waitPayload,
307
- browserUrl: submitPayload?.browser_url,
352
+ browserUrl: waitPayload?.browser_url ?? submitPayload?.browser_url,
308
353
  status: waitPayload?.review?.status,
309
354
  feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
310
355
  }
@@ -355,13 +400,17 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
355
400
  plan: tool.schema.string().describe("Markdown plan body to submit for review."),
356
401
  title: tool.schema.string().optional(),
357
402
  wait_timeout_seconds: tool.schema.number().int().positive().optional(),
403
+ task_id: tool.schema.number().int().positive().optional(),
404
+ session_id: tool.schema.number().int().positive().optional(),
358
405
  },
359
406
  async execute(args) {
360
407
  const result = await submitPlan(
361
408
  args.plan,
362
409
  "opencode",
363
410
  args.title,
364
- args.wait_timeout_seconds
411
+ args.wait_timeout_seconds,
412
+ args.task_id,
413
+ args.session_id
365
414
  )
366
415
  return JSON.stringify(result, null, 2)
367
416
  },
package/index.js CHANGED
@@ -173,7 +173,15 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
173
173
  }
174
174
  }
175
175
 
176
- const resolveReviewScope = async () => {
176
+ const resolveReviewScope = async (explicitTaskId, explicitSessionId) => {
177
+ if (explicitTaskId || explicitSessionId) {
178
+ return {
179
+ taskId: explicitTaskId != null ? String(explicitTaskId) : null,
180
+ sessionId: explicitSessionId != null ? String(explicitSessionId) : null,
181
+ source: "explicit",
182
+ }
183
+ }
184
+
177
185
  const envTaskId = process.env.CONTROLKEEL_TASK_ID
178
186
  const envSessionId = process.env.CONTROLKEEL_SESSION_ID
179
187
 
@@ -208,21 +216,37 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
208
216
  const contextTaskId = contextPayload?.current_task?.id
209
217
  const contextSessionId = contextPayload?.session_id
210
218
 
211
- if (!contextTaskId && !contextSessionId) {
212
- throw new Error("ControlKeel context did not include a session_id or current_task.id")
219
+ if (contextTaskId || contextSessionId) {
220
+ return {
221
+ taskId: contextTaskId != null ? String(contextTaskId) : null,
222
+ sessionId: contextSessionId != null ? String(contextSessionId) : null,
223
+ source: "context",
224
+ }
213
225
  }
214
226
 
215
- return {
216
- taskId: contextTaskId != null ? String(contextTaskId) : null,
217
- sessionId: contextSessionId != null ? String(contextSessionId) : null,
218
- source: "context",
227
+ try {
228
+ const bindingPayload = parseJson(await Bun.file(`${directory}/controlkeel/project.json`).text())
229
+ const bindingSessionId = bindingPayload?.session_id
230
+
231
+ if (bindingSessionId) {
232
+ return {
233
+ taskId: null,
234
+ sessionId: String(bindingSessionId),
235
+ source: "binding",
236
+ }
237
+ }
238
+ } catch (_error) {
219
239
  }
240
+
241
+ throw new Error(
242
+ "ControlKeel could not infer review scope. Set CONTROLKEEL_TASK_ID or CONTROLKEEL_SESSION_ID, or pass task_id/session_id to submit_plan."
243
+ )
220
244
  }
221
245
 
222
- const submitPlan = async (body, submittedBy, title, waitTimeoutSeconds) => {
246
+ const submitPlan = async (body, submittedBy, title, waitTimeoutSeconds, taskId, sessionId) => {
223
247
  await ensurePlanSubmitSupport()
224
248
 
225
- const reviewScope = await resolveReviewScope()
249
+ const reviewScope = await resolveReviewScope(taskId, sessionId)
226
250
  const waitTimeout = Number(waitTimeoutSeconds ?? process.env.CONTROLKEEL_REVIEW_WAIT_TIMEOUT ?? 30)
227
251
  const waitTimeoutSecondsSafe = Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 30
228
252
 
@@ -280,19 +304,35 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
280
304
  const waitOut = await new Response(waitProc.stdout).text()
281
305
  const waitErr = await new Response(waitProc.stderr).text()
282
306
  const waitExit = await waitProc.exited
307
+ const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
308
+ const waitMessage = typeof waitPayload?.message === "string" ? waitPayload.message.toLowerCase() : ""
309
+ const waitError = typeof waitPayload?.error === "string" ? waitPayload.error.toLowerCase() : ""
310
+ const waitTimedOut = waitMessage.includes("timeout") || waitError.includes("timed out")
311
+ const waitPending = waitPayload?.review?.status === "pending"
283
312
 
284
313
  if (waitExit !== 0) {
314
+ if (waitTimedOut && waitPending) {
315
+ return {
316
+ reviewId,
317
+ submitPayload,
318
+ waitPayload,
319
+ browserUrl: waitPayload?.browser_url ?? submitPayload?.browser_url,
320
+ status: "pending",
321
+ feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
322
+ timedOut: true,
323
+ }
324
+ }
325
+
285
326
  throw new Error(
286
327
  `controlkeel review plan wait failed with exit code ${waitExit}${waitErr.trim() ? `: ${waitErr.trim()}` : ""}`
287
328
  )
288
329
  }
289
330
 
290
- const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
291
331
  return {
292
332
  reviewId,
293
333
  submitPayload,
294
334
  waitPayload,
295
- browserUrl: submitPayload?.browser_url,
335
+ browserUrl: waitPayload?.browser_url ?? submitPayload?.browser_url,
296
336
  status: waitPayload?.review?.status,
297
337
  feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
298
338
  }
@@ -343,13 +383,17 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
343
383
  plan: tool.schema.string().describe("Markdown plan body to submit for review."),
344
384
  title: tool.schema.string().optional(),
345
385
  wait_timeout_seconds: tool.schema.number().int().positive().optional(),
386
+ task_id: tool.schema.number().int().positive().optional(),
387
+ session_id: tool.schema.number().int().positive().optional(),
346
388
  },
347
389
  async execute(args) {
348
390
  const result = await submitPlan(
349
391
  args.plan,
350
392
  "opencode",
351
393
  args.title,
352
- args.wait_timeout_seconds
394
+ args.wait_timeout_seconds,
395
+ args.task_id,
396
+ args.session_id
353
397
  )
354
398
  return JSON.stringify(result, null, 2)
355
399
  },
package/package.json CHANGED
@@ -35,5 +35,5 @@
35
35
  "url": "git+https://github.com/aryaminus/controlkeel.git"
36
36
  },
37
37
  "type": "module",
38
- "version": "0.2.25"
38
+ "version": "0.2.26"
39
39
  }