@aryaminus/controlkeel-opencode 0.1.26 → 0.1.28

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.
@@ -11,7 +11,7 @@ and validate them against the project's security, budget, and compliance policie
11
11
 
12
12
  ## Instructions
13
13
 
14
- 1. Use the `ck-validate` tool to run governance checks before providing feedback.
14
+ 1. Use `ck_context` first, then `ck_validate` before providing feedback.
15
15
  2. Report findings by severity: critical > high > medium > low.
16
16
  3. Never approve changes that have unresolved critical or high findings.
17
17
  4. Reference specific policy rules when flagging issues.
@@ -19,7 +19,10 @@ and validate them against the project's security, budget, and compliance policie
19
19
 
20
20
  ## Available MCP Tools
21
21
 
22
+ - `ck_context` — Load mission, findings, budget, and proof context
22
23
  - `ck_validate` — Run full governance validation
24
+ - `ck_finding` — Record a governed finding when you detect a missed issue
25
+ - `ck_review_submit` — Submit review material for human approval
26
+ - `ck_review_status` — Check review status before execution
23
27
  - `ck_budget` — Check remaining budget and spend history
24
- - `ck_findings` — List open findings for the current session
25
- - `ck_approve` — Approve a finding (requires operator confirmation)
28
+ - `ck_route` — Ask ControlKeel for the recommended specialist route
@@ -3,7 +3,7 @@ description: Run ControlKeel governance review on the current project
3
3
  agent: controlkeel-operator
4
4
  ---
5
5
 
6
- Review the current project for governance compliance. Run `ck-validate` to check
6
+ Review the current project for governance compliance. Run `ck_validate` to check
7
7
  for security findings, budget status, and proof readiness. Summarize the results
8
8
  and highlight any blockers that need attention before shipping.
9
9
 
@@ -6,7 +6,8 @@ Save the current plan to a markdown file, then submit it through ControlKeel.
6
6
 
7
7
  Recommended flow:
8
8
  1. Save the plan to `.opencode/review-plan.md`
9
- 2. Run `controlkeel review plan submit --body-file .opencode/review-plan.md --submitted-by opencode --json`
10
- 3. Read the returned `review.id` and `browser_url`
11
- 4. Wait with `controlkeel review plan wait --id <review_id> --json`
12
- 4. Do not execute until the review is approved
9
+ 2. Ensure `controlkeel version` reports `>= 0.1.26`
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. Wait with `controlkeel review plan wait --id <review_id> --timeout 30 --json`
13
+ 6. Do not execute until the review is approved
@@ -1,12 +1,13 @@
1
1
  {
2
- "mcpServers": {
2
+ "mcp": {
3
3
  "controlkeel": {
4
- "args": [
4
+ "command": [
5
+ "controlkeel",
5
6
  "mcp",
6
7
  "--project-root",
7
8
  "."
8
9
  ],
9
- "command": "controlkeel"
10
+ "type": "local"
10
11
  }
11
12
  }
12
13
  }
@@ -19,29 +19,151 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
19
19
  }
20
20
  }
21
21
 
22
- const submitPlan = async (body: string, submittedBy: string) => {
23
- const result = await $`controlkeel review plan submit --stdin --submitted-by ${submittedBy} --json`
24
- .text(body)
25
- const submitPayload = parseJson(result)
26
- const reviewId = submitPayload?.review?.id
27
- if (!reviewId) {
28
- throw new Error("ControlKeel did not return a review id")
22
+ const toText = async (output: unknown) => {
23
+ if (typeof output === "string") {
24
+ return output
29
25
  }
30
- const waitPayload = parseJson(await $`controlkeel review plan wait --id ${reviewId} --json`)
26
+
27
+ if (output == null) {
28
+ return ""
29
+ }
30
+
31
+ if (typeof output === "object") {
32
+ const stdout = (output as { stdout?: unknown }).stdout
33
+ if (typeof stdout === "string") {
34
+ return stdout
35
+ }
36
+
37
+ if (stdout && typeof (stdout as { text?: unknown }).text === "function") {
38
+ try {
39
+ const streamed = await (stdout as { text: () => Promise<string> }).text()
40
+ if (typeof streamed === "string") {
41
+ return streamed
42
+ }
43
+ } catch (_error) {
44
+ }
45
+ }
46
+ }
47
+
48
+ return String(output)
49
+ }
50
+
51
+ const parseVersion = (output: string) => {
52
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/)
53
+ if (!match) {
54
+ return null
55
+ }
56
+
31
57
  return {
32
- reviewId,
33
- submitPayload,
34
- waitPayload,
35
- browserUrl: submitPayload?.browser_url,
36
- status: waitPayload?.review?.status,
37
- feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
58
+ major: Number(match[1]),
59
+ minor: Number(match[2]),
60
+ patch: Number(match[3]),
61
+ }
62
+ }
63
+
64
+ const versionAtLeast = (
65
+ current: { major: number; minor: number; patch: number },
66
+ required: { major: number; minor: number; patch: number }
67
+ ) => {
68
+ if (current.major !== required.major) {
69
+ return current.major > required.major
70
+ }
71
+
72
+ if (current.minor !== required.minor) {
73
+ return current.minor > required.minor
74
+ }
75
+
76
+ return current.patch >= required.patch
77
+ }
78
+
79
+ const ensurePlanSubmitSupport = async () => {
80
+ let versionOutput = ""
81
+
82
+ try {
83
+ versionOutput = await toText(await $`controlkeel version`)
84
+ } catch (error) {
85
+ throw new Error(
86
+ "Failed to run `controlkeel version`. Install ControlKeel >= 0.1.26 and ensure `controlkeel` is on PATH."
87
+ )
88
+ }
89
+
90
+ const parsed = parseVersion(versionOutput)
91
+ const required = { major: 0, minor: 1, patch: 26 }
92
+
93
+ if (!parsed || !versionAtLeast(parsed, required)) {
94
+ throw new Error(
95
+ `ControlKeel CLI ${versionOutput.trim() || "unknown"} is too old for plan-review submit. Install >= 0.1.26.`
96
+ )
97
+ }
98
+ }
99
+
100
+ const submitPlan = async (
101
+ body: string,
102
+ submittedBy: string,
103
+ title?: string,
104
+ waitTimeoutSeconds?: number
105
+ ) => {
106
+ await ensurePlanSubmitSupport()
107
+
108
+ const envTaskId = process.env.CONTROLKEEL_TASK_ID
109
+ const envSessionId = process.env.CONTROLKEEL_SESSION_ID
110
+ const waitTimeout = Number(waitTimeoutSeconds ?? process.env.CONTROLKEEL_REVIEW_WAIT_TIMEOUT ?? 30)
111
+ const waitTimeoutSecondsSafe = Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 30
112
+
113
+ // Write body to temp file to avoid stdin piping issues
114
+ const tmpFile = `${directory}/.opencode/review-plan-${Date.now()}.md`
115
+ await Bun.write(tmpFile, body)
116
+
117
+ try {
118
+ const submitArgs = ["controlkeel", "review", "plan", "submit", "--body-file", tmpFile, "--submitted-by", submittedBy, "--json"]
119
+ if (title) submitArgs.push("--title", title)
120
+ if (envTaskId) submitArgs.push("--task-id", envTaskId)
121
+ else if (envSessionId) submitArgs.push("--session-id", envSessionId)
122
+
123
+ const submitProc = Bun.spawn(submitArgs, { stdout: "pipe", stderr: "pipe" })
124
+ const submitOut = await new Response(submitProc.stdout).text()
125
+ await submitProc.exited
126
+
127
+ const submitPayload = parseJson(submitOut)
128
+
129
+ if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
130
+ throw new Error(
131
+ "ControlKeel plan submission requires review context. Set CONTROLKEEL_TASK_ID (preferred) or CONTROLKEEL_SESSION_ID, or pass --task-id/--session-id manually."
132
+ )
133
+ }
134
+
135
+ const reviewId = submitPayload?.review?.id
136
+ if (!reviewId) {
137
+ throw new Error("ControlKeel did not return a review id")
138
+ }
139
+
140
+ const waitProc = Bun.spawn(["controlkeel", "review", "plan", "wait", "--id", String(reviewId), "--timeout", String(waitTimeoutSecondsSafe), "--json"], { stdout: "pipe", stderr: "pipe" })
141
+ const waitOut = await new Response(waitProc.stdout).text()
142
+ await waitProc.exited
143
+
144
+ const waitPayload = parseJson(waitOut)
145
+ return {
146
+ reviewId,
147
+ submitPayload,
148
+ waitPayload,
149
+ browserUrl: submitPayload?.browser_url,
150
+ status: waitPayload?.review?.status,
151
+ feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
152
+ }
153
+ } finally {
154
+ // Clean up temp file
155
+ try { await Bun.file(tmpFile).unlink?.() ?? (await $`rm -f ${tmpFile}`.quiet()) } catch {}
38
156
  }
39
157
  }
40
158
 
41
159
  return {
42
- "shell.env": async (_input, output) => {
160
+ "shell.env": async (input, output) => {
43
161
  output.env.CONTROLKEEL_PROJECT_ROOT = directory
44
162
  output.env.CONTROLKEEL_AGENT_ID = "opencode"
163
+
164
+ if (input.sessionID) {
165
+ output.env.CONTROLKEEL_THREAD_ID = input.sessionID
166
+ }
45
167
  },
46
168
 
47
169
  config: async (config) => {
@@ -74,9 +196,15 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
74
196
  args: {
75
197
  plan: tool.schema.string().describe("Markdown plan body to submit for review."),
76
198
  title: tool.schema.string().optional(),
199
+ wait_timeout_seconds: tool.schema.number().int().positive().optional(),
77
200
  },
78
201
  async execute(args) {
79
- const result = await submitPlan(args.plan, "opencode")
202
+ const result = await submitPlan(
203
+ args.plan,
204
+ "opencode",
205
+ args.title,
206
+ args.wait_timeout_seconds
207
+ )
80
208
  return JSON.stringify(result, null, 2)
81
209
  },
82
210
  }),
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+
1
3
  /**
2
4
  * Published OpenCode package entrypoint for ControlKeel.
3
5
  *
@@ -13,29 +15,143 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
13
15
  }
14
16
  }
15
17
 
16
- const submitPlan = async (body, submittedBy) => {
17
- const result = await $`controlkeel review plan submit --stdin --submitted-by ${submittedBy} --json`
18
- .text(body)
19
- const submitPayload = parseJson(result)
20
- const reviewId = submitPayload?.review?.id
21
- if (!reviewId) {
22
- throw new Error("ControlKeel did not return a review id")
18
+ const toText = async (output) => {
19
+ if (typeof output === "string") {
20
+ return output
21
+ }
22
+
23
+ if (output == null) {
24
+ return ""
25
+ }
26
+
27
+ if (typeof output === "object") {
28
+ const stdout = output.stdout
29
+ if (typeof stdout === "string") {
30
+ return stdout
31
+ }
32
+
33
+ if (stdout && typeof stdout.text === "function") {
34
+ try {
35
+ const streamed = await stdout.text()
36
+ if (typeof streamed === "string") {
37
+ return streamed
38
+ }
39
+ } catch (_error) {
40
+ }
41
+ }
42
+ }
43
+
44
+ return String(output)
45
+ }
46
+
47
+ const parseVersion = (output) => {
48
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/)
49
+ if (!match) {
50
+ return null
23
51
  }
24
- const waitPayload = parseJson(await $`controlkeel review plan wait --id ${reviewId} --json`)
52
+
25
53
  return {
26
- reviewId,
27
- submitPayload,
28
- waitPayload,
29
- browserUrl: submitPayload?.browser_url,
30
- status: waitPayload?.review?.status,
31
- feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
54
+ major: Number(match[1]),
55
+ minor: Number(match[2]),
56
+ patch: Number(match[3]),
57
+ }
58
+ }
59
+
60
+ const versionAtLeast = (current, required) => {
61
+ if (current.major !== required.major) {
62
+ return current.major > required.major
63
+ }
64
+
65
+ if (current.minor !== required.minor) {
66
+ return current.minor > required.minor
67
+ }
68
+
69
+ return current.patch >= required.patch
70
+ }
71
+
72
+ const ensurePlanSubmitSupport = async () => {
73
+ let versionOutput = ""
74
+
75
+ try {
76
+ versionOutput = await toText(await $`controlkeel version`)
77
+ } catch (_error) {
78
+ throw new Error(
79
+ "Failed to run `controlkeel version`. Install ControlKeel >= 0.1.26 and ensure `controlkeel` is on PATH."
80
+ )
81
+ }
82
+
83
+ const parsed = parseVersion(versionOutput)
84
+ const required = { major: 0, minor: 1, patch: 26 }
85
+
86
+ if (!parsed || !versionAtLeast(parsed, required)) {
87
+ throw new Error(
88
+ `ControlKeel CLI ${versionOutput.trim() || "unknown"} is too old for plan-review submit. Install >= 0.1.26.`
89
+ )
90
+ }
91
+ }
92
+
93
+ const submitPlan = async (body, submittedBy, title, waitTimeoutSeconds) => {
94
+ await ensurePlanSubmitSupport()
95
+
96
+ const envTaskId = process.env.CONTROLKEEL_TASK_ID
97
+ const envSessionId = process.env.CONTROLKEEL_SESSION_ID
98
+ const waitTimeout = Number(waitTimeoutSeconds ?? process.env.CONTROLKEEL_REVIEW_WAIT_TIMEOUT ?? 30)
99
+ const waitTimeoutSecondsSafe = Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 30
100
+
101
+ // Write body to temp file to avoid stdin piping issues
102
+ const tmpFile = `${directory}/.opencode/review-plan-${Date.now()}.md`
103
+ await Bun.write(tmpFile, body)
104
+
105
+ try {
106
+ const submitArgs = ["controlkeel", "review", "plan", "submit", "--body-file", tmpFile, "--submitted-by", submittedBy, "--json"]
107
+ if (title) submitArgs.push("--title", title)
108
+ if (envTaskId) submitArgs.push("--task-id", envTaskId)
109
+ else if (envSessionId) submitArgs.push("--session-id", envSessionId)
110
+
111
+ const submitProc = Bun.spawn(submitArgs, { stdout: "pipe", stderr: "pipe" })
112
+ const submitOut = await new Response(submitProc.stdout).text()
113
+ await submitProc.exited
114
+
115
+ const submitPayload = parseJson(submitOut)
116
+
117
+ if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
118
+ throw new Error(
119
+ "ControlKeel plan submission requires review context. Set CONTROLKEEL_TASK_ID (preferred) or CONTROLKEEL_SESSION_ID, or pass --task-id/--session-id manually."
120
+ )
121
+ }
122
+
123
+ const reviewId = submitPayload?.review?.id
124
+ if (!reviewId) {
125
+ throw new Error("ControlKeel did not return a review id")
126
+ }
127
+
128
+ const waitProc = Bun.spawn(["controlkeel", "review", "plan", "wait", "--id", String(reviewId), "--timeout", String(waitTimeoutSecondsSafe), "--json"], { stdout: "pipe", stderr: "pipe" })
129
+ const waitOut = await new Response(waitProc.stdout).text()
130
+ await waitProc.exited
131
+
132
+ const waitPayload = parseJson(waitOut)
133
+ return {
134
+ reviewId,
135
+ submitPayload,
136
+ waitPayload,
137
+ browserUrl: submitPayload?.browser_url,
138
+ status: waitPayload?.review?.status,
139
+ feedbackNotes: waitPayload?.review?.feedback_notes ?? null,
140
+ }
141
+ } finally {
142
+ // Clean up temp file
143
+ try { await Bun.file(tmpFile).unlink?.() ?? (await $`rm -f ${tmpFile}`.quiet()) } catch {}
32
144
  }
33
145
  }
34
146
 
35
147
  return {
36
- "shell.env": async (_input, output) => {
148
+ "shell.env": async (input, output) => {
37
149
  output.env.CONTROLKEEL_PROJECT_ROOT = directory
38
150
  output.env.CONTROLKEEL_AGENT_ID = "opencode"
151
+
152
+ if (input.sessionID) {
153
+ output.env.CONTROLKEEL_THREAD_ID = input.sessionID
154
+ }
39
155
  },
40
156
 
41
157
  config: async (config) => {
@@ -55,37 +171,32 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
55
171
  }
56
172
  },
57
173
 
58
- "tool.execute": async (input, output) => {
59
- if (input.toolID !== "submit_plan") {
60
- return
61
- }
62
-
63
- const planBody = typeof input.args?.plan === "string" ? input.args.plan : ""
64
- const review = await submitPlan(planBody, "opencode")
65
- output.metadata = {
66
- reviewId: review.reviewId,
67
- browserUrl: review.browserUrl,
68
- status: review.status,
69
- feedbackNotes: review.feedbackNotes,
70
- }
71
- output.result = JSON.stringify(output.metadata, null, 2)
174
+ "experimental.chat.system.transform": async (_input, output) => {
175
+ output.system.push(
176
+ "Use submit_plan when you are ready for human review. Do not proceed with implementation until ControlKeel approves the plan."
177
+ )
72
178
  },
73
179
 
74
- tools: async () => ({
75
- submit_plan: {
76
- description: "Submit the current plan to ControlKeel and wait for browser review approval.",
77
- parameters: {
78
- type: "object",
79
- properties: {
80
- plan: {
81
- type: "string",
82
- description: "Markdown plan body to submit to ControlKeel review.",
83
- },
84
- },
85
- required: ["plan"],
180
+ tool: {
181
+ "submit_plan": tool({
182
+ description:
183
+ "Submit a plan to ControlKeel for browser review. The tool waits for approval before execution continues.",
184
+ args: {
185
+ plan: tool.schema.string().describe("Markdown plan body to submit for review."),
186
+ title: tool.schema.string().optional(),
187
+ wait_timeout_seconds: tool.schema.number().int().positive().optional(),
188
+ },
189
+ async execute(args) {
190
+ const result = await submitPlan(
191
+ args.plan,
192
+ "opencode",
193
+ args.title,
194
+ args.wait_timeout_seconds
195
+ )
196
+ return JSON.stringify(result, null, 2)
86
197
  },
87
- },
88
- }),
198
+ }),
199
+ },
89
200
  }
90
201
  }
91
202
 
package/package.json CHANGED
@@ -2,6 +2,9 @@
2
2
  "bugs": {
3
3
  "url": "https://github.com/aryaminus/controlkeel/issues"
4
4
  },
5
+ "dependencies": {
6
+ "@opencode-ai/plugin": "1.3.13"
7
+ },
5
8
  "description": "ControlKeel OpenCode adapter bundle",
6
9
  "exports": {
7
10
  ".": "./index.js",
@@ -32,5 +35,5 @@
32
35
  "url": "git+https://github.com/aryaminus/controlkeel.git"
33
36
  },
34
37
  "type": "module",
35
- "version": "0.1.26"
38
+ "version": "0.1.28"
36
39
  }