@aryaminus/controlkeel-opencode 0.4.2 → 0.4.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.
@@ -5,4 +5,4 @@ Use this command in OpenCode to reopen the latest ControlKeel review you are tra
5
5
  Suggested flow:
6
6
  1. Read the last stored review id from your notes or prior command output.
7
7
  2. Run `controlkeel review plan open --id <review_id> --json`
8
- 3. If the review is still pending, run `controlkeel review plan wait --id <review_id> --json`
8
+ 3. If the review is still pending, ask the user for approval in this conversation, then record it with `controlkeel review plan respond <review_id> --decision approved --json`.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Submit the current plan to ControlKeel browser review and wait for approval
2
+ description: Submit the current plan to ControlKeel for approval
3
3
  ---
4
4
 
5
5
  Save the current plan to a markdown file, then submit it through ControlKeel.
@@ -8,10 +8,13 @@ Recommended flow:
8
8
  1. Save the plan to `.opencode/review-plan.md`
9
9
  2. Ensure `controlkeel version` reports `>= 0.1.26`
10
10
  3. Run `controlkeel review plan submit --body-file .opencode/review-plan.md --submitted-by opencode --task-id <task_id> --json` (or use `--session-id <session_id>`)
11
- 4. Read the returned `review.id` and `browser_url`
12
- 5. If `browser_url` is available, wait with `controlkeel review plan wait --id <review_id> --timeout 30 --json`
13
- 6. If `browser_url` is missing/unreachable, the browser does not actually open, **or** wait times out while still `pending`, do **not** loop on wait; ask for explicit user approval in chat and record it with `controlkeel review plan respond --id <review_id> --decision approved --feedback-notes "User approved in chat; browser unavailable or timed out" --json` (or `ck_review_feedback`)
14
- 7. Do not execute until the review is approved
11
+ 4. Read the returned `review.id`, `browser_url`, and `auto_approved`
12
+ 5. If `auto_approved` is true, the plan was approved automatically (low risk, no blocked findings). Proceed.
13
+ 6. Otherwise, present the plan summary to the user and ask for approval in this conversation
14
+ 7. After the user approves, record it with `controlkeel review plan respond <review_id> --decision approved --feedback-notes "User approved in chat" --json` (or `ck_review_feedback`)
15
+ 8. Do not execute until the review is approved
16
+
17
+ Auto-approval: For low-risk work (small scope, no security concerns, within established patterns), the server may auto-approve. Check `auto_approved` in the response.
15
18
 
16
19
  Fallback when the `submit_plan` tool is stale in a long-running OpenCode session:
17
20
  - If the tool returns an error like `ControlKeel CLI [object Object] is too old`, run the CLI flow above directly.
@@ -68,6 +68,25 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
68
68
  }
69
69
  }
70
70
 
71
+ // CLI commands emit a `{command, data, status, version}` envelope; unwrap it so
72
+ // callers can read `review.id`, `session_id`, `browser_url` at the top level.
73
+ // Older flat payloads pass through unchanged.
74
+ const parseCliJson = (output: string) => {
75
+ const payload = parseJson(output)
76
+
77
+ if (
78
+ payload != null &&
79
+ typeof payload === "object" &&
80
+ !Array.isArray(payload) &&
81
+ payload.data != null &&
82
+ typeof payload.data === "object"
83
+ ) {
84
+ return payload.data
85
+ }
86
+
87
+ return payload
88
+ }
89
+
71
90
  const toText = async (output: unknown) => {
72
91
  if (typeof output === "string") {
73
92
  return output
@@ -242,7 +261,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
242
261
  )
243
262
  }
244
263
 
245
- const contextPayload = parseJson([contextOut, contextErr].filter(Boolean).join("\n"))
264
+ const contextPayload = parseCliJson([contextOut, contextErr].filter(Boolean).join("\n"))
246
265
  const contextTaskId = contextPayload?.current_task?.id
247
266
  const contextSessionId = contextPayload?.session_id
248
267
 
@@ -316,7 +335,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
316
335
  )
317
336
  }
318
337
 
319
- const submitPayload = parseJson([submitOut, submitErr].filter(Boolean).join("\n"))
338
+ const submitPayload = parseCliJson([submitOut, submitErr].filter(Boolean).join("\n"))
320
339
 
321
340
  if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
322
341
  throw new Error(
@@ -346,7 +365,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
346
365
  const openExit = await openProc.exited
347
366
 
348
367
  if (openExit === 0) {
349
- openPayload = parseJson([openOut, openErr].filter(Boolean).join("\n"))
368
+ openPayload = parseCliJson([openOut, openErr].filter(Boolean).join("\n"))
350
369
  } else {
351
370
  openPayload = {
352
371
  error: `controlkeel review plan open failed with exit code ${openExit}${openErr.trim() ? `: ${openErr.trim()}` : ""}`,
@@ -377,6 +396,22 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
377
396
  guidance: overrides.guidance ?? null,
378
397
  })
379
398
 
399
+ // If the server auto-approved, skip the entire open/wait flow
400
+ const autoApproved = submitPayload?.auto_approved === true
401
+ const autoApproveReason = submitPayload?.auto_approve_reason ?? null
402
+
403
+ if (autoApproved) {
404
+ return buildPlanResult({
405
+ status: "approved",
406
+ waitSkipped: true,
407
+ manualApprovalRequired: false,
408
+ reason: "auto_approved",
409
+ guidance: autoApproveReason
410
+ ? `Plan auto-approved: ${autoApproveReason}`
411
+ : "Plan was auto-approved by ControlKeel (low risk, no blocked findings, within policy).",
412
+ })
413
+ }
414
+
380
415
  const openError = typeof openPayload?.open_error === "string" ? openPayload.open_error.trim() : ""
381
416
  const openFailure = typeof openPayload?.error === "string" ? openPayload.error.trim() : ""
382
417
  const browserNotOpened = openPayload?.opened !== true
@@ -387,7 +422,11 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
387
422
  browserUrl.includes("localhost") &&
388
423
  openPayload?.remote === true
389
424
 
390
- if (!browserUrl || serverUnavailable || openError || openFailure || remoteLocalhostMismatch || browserNotOpened) {
425
+ // Default: always return inline approval guidance.
426
+ const browserAvailable =
427
+ browserUrl && !serverUnavailable && !openError && !openFailure && !remoteLocalhostMismatch && !browserNotOpened
428
+
429
+ if (!browserAvailable) {
391
430
  return buildPlanResult({
392
431
  waitSkipped: true,
393
432
  manualApprovalRequired: true,
@@ -400,10 +439,29 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
400
439
  ? "browser_not_opened"
401
440
  : "browser_unreachable",
402
441
  guidance:
403
- "Browser review is unavailable, the CK review server is not reachable, or the browser did not actually open. Ask the user for explicit approval in chat, then record it with `controlkeel review plan respond --id <review_id> --decision approved --feedback-notes \"User approved in chat; browser/review server unavailable\" --json` or `ck_review_feedback`.",
442
+ "Ask the user for explicit approval in this conversation. " +
443
+ "Present the plan summary and ask: 'Do you approve this plan? (yes/no)'. " +
444
+ "After the user says yes, record it with: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json` " +
445
+ "or call `ck_review_feedback` with review_id=<review_id> decision=\"approved\".",
404
446
  })
405
447
  }
406
448
 
449
+ // Browser is available. If the caller explicitly passed wait_timeout_seconds,
450
+ // honor it (opt-in blocking). Otherwise, return immediately with inline guidance.
451
+ if (waitTimeoutSeconds == null) {
452
+ return buildPlanResult({
453
+ waitSkipped: true,
454
+ manualApprovalRequired: false,
455
+ reason: "browser_available_inline_default",
456
+ guidance:
457
+ "Browser review is available at: " + browserUrl + "\n" +
458
+ "You can: (a) ask the user to approve inline in this conversation, or (b) pass wait_timeout_seconds to block until the browser review is completed. " +
459
+ "To record inline approval: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json`.",
460
+ })
461
+ }
462
+
463
+ // Opt-in blocking: caller explicitly asked to wait for browser review.
464
+
407
465
  const waitEnv = process.env.LOGGER_LEVEL
408
466
  ? process.env
409
467
  : { ...process.env, LOGGER_LEVEL: "warning" }
@@ -416,7 +474,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
416
474
  const waitOut = await new Response(waitProc.stdout).text()
417
475
  const waitErr = await new Response(waitProc.stderr).text()
418
476
  const waitExit = await waitProc.exited
419
- const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
477
+ const waitPayload = parseCliJson([waitOut, waitErr].filter(Boolean).join("\n"))
420
478
  const waitMessage = typeof waitPayload?.message === "string" ? waitPayload.message.toLowerCase() : ""
421
479
  const waitError = typeof waitPayload?.error === "string" ? waitPayload.error.toLowerCase() : ""
422
480
  const waitTimedOut = waitMessage.includes("timeout") || waitError.includes("timed out")
@@ -433,7 +491,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
433
491
  manualApprovalRequired: true,
434
492
  reason: "review_timeout",
435
493
  guidance:
436
- "Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond --id <review_id> --decision approved --feedback-notes \"User approved in chat after timeout/browser issue\" --json` (or `ck_review_feedback`) before proceeding.",
494
+ "Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat after timeout/browser issue\" --json` (or `ck_review_feedback`) before proceeding.",
437
495
  })
438
496
  }
439
497
 
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: compliance-audit
3
- description: "Run a structured compliance audit against active ControlKeel policy packs and domain controls. Use this before shipping regulated data flows, external integrations, or document exports."
4
- when_to_use: "Activate before exporting data, integrating with external services, or when the user asks about compliance, regulatory requirements, GDPR, SOC2, HIPAA, or policy packs."
5
- argument-hint: "[data flow, integration, or domain to audit]"
3
+ description: "DEPRECATED use `security-review` instead (it now covers all of this — regulated flows, policy packs, domain controls). Kept as a thin alias for search (compliance/GDPR/SOC2/HIPAA) so existing `ck_skill_list` queries still surface `security-review`."
4
+ when_to_use: "Do NOT invoke this skill directly invoke `security-review`, which runs the compliance audit as its policy/domain-pack layer. This file exists only so keyword search for compliance, GDPR, SOC2, HIPAA routes to the consolidated `security-review` path."
5
+ argument-hint: "[use security-review instead]"
6
6
  license: Apache-2.0
7
+ redirect_to: security-review
7
8
  compatibility:
8
9
  - codex
9
10
  - claude-standalone
@@ -40,18 +41,13 @@ metadata:
40
41
  - ck_context
41
42
  - ck_validate
42
43
  - ck_finding
44
+ deprecated: true
45
+ superseded_by: security-review
43
46
  ---
44
47
 
45
- # Compliance Audit Skill
48
+ # Compliance Audit Skill — alias
46
49
 
47
- ## Audit flow
48
-
49
- 1. Call `ck_context` and confirm the active compliance profile for the session.
50
- 2. Review only the pack sections that match the active domain pack and data flows.
51
- 3. Use `ck_validate` for concrete snippets and configs when the checklist points to code.
52
- 4. Persist each failing control with `ck_finding`.
53
- 5. End with packs checked, controls reviewed, blockers, and required approvals.
54
-
55
- ## Additional resources
56
-
57
- - For the full domain-by-domain checklist, see [references/control-matrix.md](references/control-matrix.md)
50
+ > **This skill has moved.** Use `security-review` — it runs the same compliance audit
51
+ > as its policy/domain-pack layer (active compliance profile → pack sections →
52
+ > `ck_validate` `ck_finding`). This file is a thin keyword alias so search for
53
+ > `compliance`, `GDPR`, `SOC2`, `HIPAA` still surfaces the consolidated path.
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: domain-audit
3
- description: "Audit a session against its domain pack, especially the regulated and operations-heavy packs such as HR, legal, marketing, sales, real-estate, government, insurance, logistics, manufacturing, e-commerce, and nonprofit. Use this when domain-specific policy needs a manual pass."
4
- when_to_use: "Activate before handling data in regulated or sensitive domains, or when the user asks about HR policy, legal compliance, GDPR, healthcare, finance, or any industry-specific governance requirements."
5
- argument-hint: "[domain or data type to audit]"
3
+ description: "DEPRECATED use `security-review` instead (it now covers all domain-pack audits HR, legal, marketing, sales, real-estate, government, insurance, logistics, manufacturing, e-commerce, nonprofit). Kept as a thin alias so `ck_skill_list` for those domains still surfaces `security-review`."
4
+ when_to_use: "Do NOT invoke this skill directly invoke `security-review`, which runs the domain-pack audit as its policy/domain-pack layer. This file exists only so keyword search for HR, legal, marketing, sales, government, etc. routes to the consolidated `security-review` path."
5
+ argument-hint: "[use security-review instead]"
6
6
  license: Apache-2.0
7
+ redirect_to: security-review
7
8
  compatibility:
8
9
  - codex
9
10
  - claude-standalone
@@ -39,26 +40,14 @@ metadata:
39
40
  ck_mcp_tools:
40
41
  - ck_context
41
42
  - ck_finding
43
+ deprecated: true
44
+ superseded_by: security-review
42
45
  ---
43
46
 
44
- # Domain Audit Skill
47
+ # Domain Audit Skill — alias
45
48
 
46
- Use this skill when the session’s domain pack drives the real risk more than generic software checks.
47
-
48
- ## Focus areas
49
-
50
- - HR: bias, candidate data, compensation visibility
51
- - Legal: privilege, retention, document handling
52
- - Marketing: consent, unsubscribe, analytics PII
53
- - Sales: CRM data, contact deletion, revenue visibility
54
- - Real estate: fair-housing logic, tenant PII, retention
55
- - Government: records retention, constituent data, approval chains
56
- - Insurance: claims fairness, medical-adjacent privacy, denial review
57
- - E-commerce: card scope, refunds, fraud controls
58
- - Logistics: shipment custody, dispatch safety, carrier data
59
- - Manufacturing: QA holds, traceability, plant safety
60
- - Nonprofit: donor privacy, grant restrictions, beneficiary exports
61
-
62
- ## Additional resources
63
-
64
- - [Domain review matrix](references/domain-review-matrix.md)
49
+ > **This skill has moved.** Use `security-review` it runs the same domain-pack
50
+ > audit as its policy/domain-pack layer. This file is a thin keyword alias so
51
+ > search for `HR`, `legal`, `marketing`, `sales`, `government`, `insurance`,
52
+ > `logistics`, `manufacturing`, `e-commerce`, or `nonprofit` still surfaces
53
+ > the consolidated path.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: end-of-shift
3
- description: "Close a governed work session with validation, proof, findings, budget, digest, learning, and handoff checks. Use when the user asks to wrap up, stop for the day, or leave work ready for another agent."
4
- when_to_use: "Activate only at an explicit session stop point or when the user asks for end-of-shift validation. Do not run expensive full-suite or benchmark work unless project policy or the approved plan requires it."
3
+ description: "Close a governed work session the unified session-close path (validation + proof + findings + budget + digest + learning) that ends with the `handoff` delegation step when work remains. Prefer this over bare `handoff` for any end-of-shift wrap-up. Use when the user asks to wrap up, stop for the day, or leave work ready for another agent."
4
+ when_to_use: "Activate at any explicit session stop point (wrap up, stop for the day, leave work for another agent). It subsumes the bare `handoff` flow — `handoff` is just this skill's final delegation step, kept callable alone for mid-session delegation. Activate only at for end-of-shift validation. Do not run expensive full-suite or benchmark work unless project policy or the approved plan requires it."
5
5
  argument-hint: "[completed work or remaining task]"
6
6
  disable-model-invocation: true
7
7
  license: Apache-2.0
@@ -27,8 +27,10 @@ result-schema:
27
27
  handoff_reference: {type: [string, "null"]}
28
28
  metadata:
29
29
  author: controlkeel
30
- version: "1.0"
30
+ version: "1.1"
31
31
  category: execution
32
+ supersedes: handoff
33
+ subsumes: handoff
32
34
  ck_mcp_tools:
33
35
  - ck_context
34
36
  - ck_git_status
@@ -42,7 +44,9 @@ metadata:
42
44
  - ck_checkpoint_create
43
45
  ---
44
46
 
45
- # End of Shift
47
+ # End of Shift — unified session-close
48
+
49
+ > `handoff` is step 7 of this skill (mid-session delegation without the earlier validation/proof/digest/learning gates). Use bare `handoff` only for mid-session delegation; this skill is the full close — it validates, finalizes proof, checks budget, synthesizes learnings, then hands off if work remains.
46
50
 
47
51
  Close work with enough verified state that the next human or agent can continue without reconstructing the session from chat.
48
52
 
@@ -1,10 +1,11 @@
1
1
  ---
2
2
  name: handoff
3
- description: "Persist session state and hand off in-progress work to a background agent or delegated execution. Use when work outgrows the current session, context is near limit, or a task needs to continue unattended."
4
- when_to_use: "Activate when the user says 'hand off', 'delegate this', 'continue in background', 'pass this off', or when context pressure is high and significant work remains. Also activate when ck_route recommends a different agent for the remaining work."
3
+ description: "Persist session state and hand off in-progress work to a background agent the *delegation step* of `end-of-shift`. Use when work outgrows the current session or must continue unattended; for a full session close (validation + proof + findings + budget + digest + learning before handoff) prefer `end-of-shift`."
4
+ when_to_use: "Activate ONLY for mid-session delegation (context near limit, `ck_route` recommends a different agent/runtime, user says hand off/delegate/pass off). For an end-of-shift wrap-up (validate remaining work, finalize proofs, synthesize learnings), use `end-of-shift` instead `handoff` is that skill's final step."
5
5
  argument-hint: "[optional: specific task or remaining work to hand off]"
6
6
  disable-model-invocation: true
7
7
  license: Apache-2.0
8
+ redirect_to: end-of-shift
8
9
  compatibility:
9
10
  - codex
10
11
  - claude-standalone
@@ -35,8 +36,9 @@ compatibility:
35
36
  - forge-acp
36
37
  metadata:
37
38
  author: controlkeel
38
- version: "1.0"
39
+ version: "1.1"
39
40
  category: execution
41
+ superseded_by: end-of-shift
40
42
  ck_mcp_tools:
41
43
  - ck_context
42
44
  - ck_memory_record
@@ -46,7 +48,12 @@ metadata:
46
48
  - ck_goal
47
49
  ---
48
50
 
49
- # Handoff Skill
51
+ # Handoff Skill — delegation step
52
+
53
+ > This skill is the **delegation sub-step** of `end-of-shift`. A full end-of-shift
54
+ > run validates remaining work, finalizes proofs, records digests/learnings, and THEN
55
+ > hands off. Use this skill alone only for mid-session delegation; otherwise prefer
56
+ > `end-of-shift`.
50
57
 
51
58
  Transfer in-progress work to another agent or execution context with full state preservation, so work continues seamlessly after the current session ends or context runs out.
52
59
 
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  name: parallel-review
3
- description: "Run security + code quality reviews concurrently, synthesize deduplicated findings. Trigger: 'full review', 'parallel review', 'both reviews', comprehensive pre-merge check."
4
- when_to_use: "Activate ONLY when explicitly asked for comprehensive security+quality review. Do NOT use when only one review type is needed."
3
+ description: "DEPRECATED for inline use this is now a CLI composite (`controlkeel review --parallel`). Kept callable only for explicitly requested comprehensive security+quality orchestration; otherwise invoke security-review + deep-code-quality-review separately or via that CLI."
4
+ when_to_use: "Do NOT invoke via skills when possible use `controlkeel review --parallel` (CLI). Invoke this skill only when explicitly asked for comprehensive security+quality orchestration."
5
+
5
6
  argument-hint: "[PR, branch, or diff]"
6
7
  disable-model-invocation: true
8
+ deprecated: true
9
+ prefer_cli: "controlkeel review --parallel"
7
10
  license: Apache-2.0
8
11
  compatibility:
9
12
  - opencode-native
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: security-review
3
- description: "Run a structured security review before marking a task done. Use this for code, config, architecture, or release reviews that need OWASP, baseline pack, and domain-pack coverage."
4
- when_to_use: "Use before merging, deploying, or signing off on code, config, or architecture changes. Activate when reviewing auth, input handling, secrets, or third-party dependencies."
5
- argument-hint: "[file, PR, or area to review]"
3
+ description: "Run a structured security review before marking a task done. Services as the single audit skill for OWASP, baseline pack, domain-pack (HR/legal/marketing/sales/real-estate/government/insurance/logistics/manufacturing/e-commerce/nonprofit), and compliance (GDPR/SOC2/HIPAA/policy packs) — subsumes the deprecated `compliance-audit` and `domain-audit` aliases. Use this for code, config, architecture, or release reviews; the domain/compliance layer is triggered by the session's active domain pack and data flows."
4
+ when_to_use: "Use before merging, deploying, or signing off on code, config, or architecture changes (including regulated data flows, external integrations, exports, and any session whose domain pack is HR/legal/marketing/sales/real-estate/government/insurance/logistics/manufacturing/e-commerce/nonprofit). Activates for auth/input/secrets/deps and for domain/compliance coverage — the policy/domain-pack layer is driven by `ck_context`'s active compliance profile."
5
+ argument-hint: "[file, PR, area, or domain-pack scope to review]"
6
6
  license: Apache-2.0
7
+ redirect_to: security-review
7
8
  compatibility:
8
9
  - codex
9
10
  - claude-standalone
@@ -36,6 +37,10 @@ metadata:
36
37
  author: controlkeel
37
38
  version: "2.1"
38
39
  category: security
40
+ subsumes:
41
+ - compliance-audit
42
+ - domain-audit
43
+ - agent-pattern-verification
39
44
  ck_mcp_tools:
40
45
  - ck_validate
41
46
  - ck_context
package/index.js CHANGED
@@ -64,6 +64,25 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
64
64
  }
65
65
  }
66
66
 
67
+ // CLI commands emit a `{command, data, status, version}` envelope; unwrap it so
68
+ // callers can read `review.id`, `session_id`, `browser_url` at the top level.
69
+ // Older flat payloads pass through unchanged.
70
+ const parseCliJson = (output) => {
71
+ const payload = parseJson(output)
72
+
73
+ if (
74
+ payload != null &&
75
+ typeof payload === "object" &&
76
+ !Array.isArray(payload) &&
77
+ payload.data != null &&
78
+ typeof payload.data === "object"
79
+ ) {
80
+ return payload.data
81
+ }
82
+
83
+ return payload
84
+ }
85
+
67
86
  const toText = async (output) => {
68
87
  if (typeof output === "string") {
69
88
  return output
@@ -232,7 +251,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
232
251
  )
233
252
  }
234
253
 
235
- const contextPayload = parseJson([contextOut, contextErr].filter(Boolean).join("\n"))
254
+ const contextPayload = parseCliJson([contextOut, contextErr].filter(Boolean).join("\n"))
236
255
  const contextTaskId = contextPayload?.current_task?.id
237
256
  const contextSessionId = contextPayload?.session_id
238
257
 
@@ -299,7 +318,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
299
318
  )
300
319
  }
301
320
 
302
- const submitPayload = parseJson([submitOut, submitErr].filter(Boolean).join("\n"))
321
+ const submitPayload = parseCliJson([submitOut, submitErr].filter(Boolean).join("\n"))
303
322
 
304
323
  if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
305
324
  throw new Error(
@@ -329,7 +348,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
329
348
  const openExit = await openProc.exited
330
349
 
331
350
  if (openExit === 0) {
332
- openPayload = parseJson([openOut, openErr].filter(Boolean).join("\n"))
351
+ openPayload = parseCliJson([openOut, openErr].filter(Boolean).join("\n"))
333
352
  } else {
334
353
  openPayload = {
335
354
  error: `controlkeel review plan open failed with exit code ${openExit}${openErr.trim() ? `: ${openErr.trim()}` : ""}`,
@@ -360,6 +379,22 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
360
379
  guidance: overrides.guidance ?? null,
361
380
  })
362
381
 
382
+ // If the server auto-approved, skip the entire open/wait flow
383
+ const autoApproved = submitPayload?.auto_approved === true
384
+ const autoApproveReason = submitPayload?.auto_approve_reason ?? null
385
+
386
+ if (autoApproved) {
387
+ return buildPlanResult({
388
+ status: "approved",
389
+ waitSkipped: true,
390
+ manualApprovalRequired: false,
391
+ reason: "auto_approved",
392
+ guidance: autoApproveReason
393
+ ? `Plan auto-approved: ${autoApproveReason}`
394
+ : "Plan was auto-approved by ControlKeel (low risk, no blocked findings, within policy).",
395
+ })
396
+ }
397
+
363
398
  const openError = typeof openPayload?.open_error === "string" ? openPayload.open_error.trim() : ""
364
399
  const openFailure = typeof openPayload?.error === "string" ? openPayload.error.trim() : ""
365
400
  const browserNotOpened = openPayload?.opened !== true
@@ -370,7 +405,11 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
370
405
  browserUrl.includes("localhost") &&
371
406
  openPayload?.remote === true
372
407
 
373
- if (!browserUrl || serverUnavailable || openError || openFailure || remoteLocalhostMismatch || browserNotOpened) {
408
+ // Default: always return inline approval guidance.
409
+ const browserAvailable =
410
+ browserUrl && !serverUnavailable && !openError && !openFailure && !remoteLocalhostMismatch && !browserNotOpened
411
+
412
+ if (!browserAvailable) {
374
413
  return buildPlanResult({
375
414
  waitSkipped: true,
376
415
  manualApprovalRequired: true,
@@ -383,10 +422,29 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
383
422
  ? "browser_not_opened"
384
423
  : "browser_unreachable",
385
424
  guidance:
386
- "Browser review is unavailable, the CK review server is not reachable, or the browser did not actually open. Ask the user for explicit approval in chat, then record it with `controlkeel review plan respond --id <review_id> --decision approved --feedback-notes \"User approved in chat; browser/review server unavailable\" --json` or `ck_review_feedback`.",
425
+ "Ask the user for explicit approval in this conversation. " +
426
+ "Present the plan summary and ask: 'Do you approve this plan? (yes/no)'. " +
427
+ "After the user says yes, record it with: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json` " +
428
+ "or call `ck_review_feedback` with review_id=<review_id> decision=\"approved\".",
387
429
  })
388
430
  }
389
431
 
432
+ // Browser is available. If the caller explicitly passed wait_timeout_seconds,
433
+ // honor it (opt-in blocking). Otherwise, return immediately with inline guidance.
434
+ if (waitTimeoutSeconds == null) {
435
+ return buildPlanResult({
436
+ waitSkipped: true,
437
+ manualApprovalRequired: false,
438
+ reason: "browser_available_inline_default",
439
+ guidance:
440
+ "Browser review is available at: " + browserUrl + "\n" +
441
+ "You can: (a) ask the user to approve inline in this conversation, or (b) pass wait_timeout_seconds to block until the browser review is completed. " +
442
+ "To record inline approval: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json`.",
443
+ })
444
+ }
445
+
446
+ // Opt-in blocking: caller explicitly asked to wait for browser review.
447
+
390
448
  const waitEnv = process.env.LOGGER_LEVEL
391
449
  ? process.env
392
450
  : { ...process.env, LOGGER_LEVEL: "warning" }
@@ -399,7 +457,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
399
457
  const waitOut = await new Response(waitProc.stdout).text()
400
458
  const waitErr = await new Response(waitProc.stderr).text()
401
459
  const waitExit = await waitProc.exited
402
- const waitPayload = parseJson([waitOut, waitErr].filter(Boolean).join("\n"))
460
+ const waitPayload = parseCliJson([waitOut, waitErr].filter(Boolean).join("\n"))
403
461
  const waitMessage = typeof waitPayload?.message === "string" ? waitPayload.message.toLowerCase() : ""
404
462
  const waitError = typeof waitPayload?.error === "string" ? waitPayload.error.toLowerCase() : ""
405
463
  const waitTimedOut = waitMessage.includes("timeout") || waitError.includes("timed out")
@@ -416,7 +474,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
416
474
  manualApprovalRequired: true,
417
475
  reason: "review_timeout",
418
476
  guidance:
419
- "Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond --id <review_id> --decision approved --feedback-notes \"User approved in chat after timeout/browser issue\" --json` (or `ck_review_feedback`) before proceeding.",
477
+ "Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat after timeout/browser issue\" --json` (or `ck_review_feedback`) before proceeding.",
420
478
  })
421
479
  }
422
480
 
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.4.2"
38
+ "version": "0.4.4"
39
39
  }