@brainervirus/workit-core 8.1.1 → 8.3.0

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.
@@ -4,7 +4,12 @@ import type { HookDecision, HookEvent, HookEventKind, HostAdapter } from "../pro
4
4
  import { commandText, existingDirectory, isRecord, nonEmpty, optionalText } from "./fields";
5
5
 
6
6
  export type CodexHost = "codex_cli" | "codex_desktop";
7
- export type CodexHookEvent = "SessionStart" | "PreToolUse" | "SubagentStart" | "SubagentStop";
7
+ export type CodexHookEvent =
8
+ | "SessionStart"
9
+ | "PreToolUse"
10
+ | "PostToolUse"
11
+ | "SubagentStart"
12
+ | "SubagentStop";
8
13
  type SessionSource = "startup" | "resume" | "clear" | "compact";
9
14
 
10
15
  export type CodexHookInput = {
@@ -21,6 +26,8 @@ export type CodexHookInput = {
21
26
  /** The shell command as one string; argv arrays are joined. */
22
27
  command?: string;
23
28
  tool_use_id?: string;
29
+ /** PostToolUse: the model-facing tool output. */
30
+ tool_response?: string;
24
31
  agent_id?: string;
25
32
  agent_type?: string;
26
33
  agent_transcript_path?: string | null;
@@ -53,7 +60,7 @@ export const CODEX_DESCRIPTOR: HostDescriptor = {
53
60
  // Codex PreToolUse intercepts shell calls, not apply_patch edits: the
54
61
  // before-write gate stays advisory rather than half-enforced.
55
62
  "write.pre": { support: "undocumented", native: null },
56
- "shell.post": undocumented,
63
+ "shell.post": { support: "native", native: "PostToolUse" },
57
64
  "subagent.start": { support: "native", native: "SubagentStart" },
58
65
  "subagent.stop": { support: "native", native: "SubagentStop" },
59
66
  "prompt.submit": undocumented,
@@ -74,7 +81,7 @@ export const CODEX_DESCRIPTOR: HostDescriptor = {
74
81
  worktreeIsolation: "undocumented",
75
82
  maxConcurrency: "undocumented",
76
83
  },
77
- provenance: { sessionId: "native", agentIdOnTool: "partial", postToolObserve: "undocumented" },
84
+ provenance: { sessionId: "native", agentIdOnTool: "partial", postToolObserve: "native" },
78
85
  interaction: { questions: "none", writeBoundary: "partial" },
79
86
  stopControl: "undocumented",
80
87
  shellAvailable: "native",
@@ -146,6 +153,7 @@ export const codexDescriptor = (host: CodexHost): HostDescriptor => ({ ...CODEX_
146
153
  const EVENTS: Record<CodexHookEvent, HookEventKind> = {
147
154
  SessionStart: "session.start",
148
155
  PreToolUse: "shell.pre",
156
+ PostToolUse: "shell.post",
149
157
  SubagentStart: "subagent.start",
150
158
  SubagentStop: "subagent.stop",
151
159
  };
@@ -166,10 +174,10 @@ export const parseCodexHookInput = (value: unknown): CodexParseResult => {
166
174
  const event = value.hook_event_name as CodexHookEvent;
167
175
  const cwd = existingDirectory(value.cwd);
168
176
  if (!cwd) return { ok: false, error: "cwd must be an existing absolute directory" };
169
- if (event === "PreToolUse" && !nonEmpty(value.tool_name))
170
- return { ok: false, error: "tool_name is required" };
177
+ const toolEvent = event === "PreToolUse" || event === "PostToolUse";
178
+ if (toolEvent && !nonEmpty(value.tool_name)) return { ok: false, error: "tool_name is required" };
171
179
  const command = isRecord(value.tool_input) ? commandText(value.tool_input.command) : null;
172
- if (event === "PreToolUse" && isShellTool(value.tool_name) && !command)
180
+ if (toolEvent && isShellTool(value.tool_name) && !command)
173
181
  return { ok: false, error: "tool_input.command is required for shell tools" };
174
182
  if (event === "SubagentStart" && !nonEmpty(value.agent_id))
175
183
  return { ok: false, error: "agent_id is required" };
@@ -188,7 +196,10 @@ export const parseCodexHookInput = (value: unknown): CodexParseResult => {
188
196
  : {}),
189
197
  ...(nonEmpty(value.turn_id) ? { turn_id: value.turn_id } : {}),
190
198
  ...(nonEmpty(value.tool_name) ? { tool_name: value.tool_name } : {}),
191
- ...(event === "PreToolUse" ? { tool_input: value.tool_input } : {}),
199
+ ...(toolEvent ? { tool_input: value.tool_input } : {}),
200
+ ...(event === "PostToolUse" && typeof value.tool_response === "string"
201
+ ? { tool_response: value.tool_response }
202
+ : {}),
192
203
  ...(command ? { command } : {}),
193
204
  ...(nonEmpty(value.tool_use_id) ? { tool_use_id: value.tool_use_id } : {}),
194
205
  ...(nonEmpty(value.agent_id) ? { agent_id: value.agent_id } : {}),
@@ -218,6 +229,20 @@ const protocolEvent = (input: CodexHookInput): HookEvent => {
218
229
  }
219
230
  : { kind: "tool.pre", tool: input.tool_name!, toolUseId };
220
231
  }
232
+ case "PostToolUse": {
233
+ const toolUseId = input.tool_use_id ?? null;
234
+ return isShellTool(input.tool_name)
235
+ ? {
236
+ kind: "shell.post",
237
+ command: input.command ?? "",
238
+ stdout: input.tool_response ?? "",
239
+ // Codex reports the model-facing output, not an exit code.
240
+ exitCode: null,
241
+ toolUseId,
242
+ }
243
+ : // A non-shell tool result carries nothing workit observes.
244
+ { kind: "tool.pre", tool: input.tool_name!, toolUseId };
245
+ }
221
246
  case "SubagentStart":
222
247
  return {
223
248
  kind: "subagent.start",
@@ -288,5 +313,5 @@ export const codexAdapter: HostAdapter = {
288
313
  },
289
314
  render,
290
315
  addendum: (input) =>
291
- `<workit-codex-mutations>Codex MCP is read-only: unattested callers cannot mutate. Run workit verbs with the workit CLI on the shell (node_modules/.bin/workit, or npx -y @brainervirus/workit-cli): check, git branch|commit|push, pr, ci, stack, ledger, handoff; task-family mutations take --json${input.session.id ? ` --actor ${input.session.id}` : ""}. A verifier or reviewer records workit ledger verdict under a session the lead assigns (WORKIT_SESSION_ID=<lead>-v<n>), never the author's. Merge and release need a workspace grant (workit grant show); a question answer is not host permission.</workit-codex-mutations>`,
316
+ `<workit-codex-mutations>Codex MCP is read-only: unattested callers cannot mutate. Run workit verbs with the workit CLI on the shell (node_modules/.bin/workit, or npx -y @brainervirus/workit-cli): check, git branch|commit|push, pr, ci, stack, ledger, handoff; task-family mutations take --json${input.session.id ? ` --actor ${input.session.id}` : ""}. workit acts as this thread (CODEX_THREAD_ID${input.session.id ? `=${input.session.id}` : ""}) unless WORKIT_SESSION_ID is set. A verifier or reviewer records workit ledger verdict under a session the lead assigns (WORKIT_HOST=codex_cli WORKIT_SESSION_ID=<lead>-v<n>), never the author's. Merge and release need a workspace grant (workit grant show); a question answer is not host permission.</workit-codex-mutations>`,
292
317
  };
@@ -278,6 +278,10 @@ const render = (decision: HookDecision, native: string | null) => {
278
278
  json: cursorDeny(decision.reason),
279
279
  exitCode: native !== null && BLOCKING.has(native) ? 2 : 0,
280
280
  };
281
+ // beforeShellExecution has no context channel: a raw-git nudge rides on
282
+ // agent_message next to Cursor's required permission answer.
283
+ if (decision.kind === "context" && native === "beforeShellExecution")
284
+ return { json: { permission: "allow", agent_message: decision.text }, exitCode: 0 };
281
285
  if (decision.kind === "context")
282
286
  return { json: { additional_context: decision.text }, exitCode: 0 };
283
287
  if (decision.kind === "notice")
@@ -319,4 +323,10 @@ export const cursorAdapter: HostAdapter = {
319
323
  };
320
324
  },
321
325
  render,
326
+ // Cursor's shell carries no conversation id (sessionStart env reaches hooks
327
+ // only), so the agent is told the id its `workit` calls should act as.
328
+ addendum: (input) =>
329
+ input.session.id
330
+ ? `<workit-session>Cursor puts no session id in the shell: prefix workit commands that record authorship with WORKIT_SESSION_ID=${input.session.id} (e.g. \`WORKIT_SESSION_ID=${input.session.id} workit git commit -m …\`). A verifier uses its own id, never this one.</workit-session>`
331
+ : null,
322
332
  };
@@ -18,6 +18,7 @@ export {
18
18
  } from "./context";
19
19
  export { shellPolicy } from "./policy";
20
20
  export { shellWrites, writeGate } from "./write-gate";
21
+ export { callKey, noteRawCommit, rawGitPost, rawGitPre } from "./raw-git";
21
22
  export { isWriteTool, writePaths } from "./hosts/fields";
22
23
  export { dispatchHook, failureDecision, handleHook, type HookDeps } from "./handle";
23
24
  export { HOOK_ADAPTERS, runHookProcess } from "./run";
@@ -17,7 +17,13 @@ export type HookEvent =
17
17
  | { kind: "session.start"; source: SessionSource }
18
18
  /** Per-turn injection (OpenCode session context, Pi before_agent_start, Claude UserPromptSubmit). */
19
19
  | { kind: "context.turn" }
20
- | { kind: "shell.pre"; command: string; toolUseId: string | null }
20
+ | {
21
+ kind: "shell.pre";
22
+ command: string;
23
+ toolUseId: string | null;
24
+ /** The shell that runs `command`; posix when absent. */
25
+ dialect?: "posix" | "powershell";
26
+ }
21
27
  /** A pre-tool gate for a non-shell tool. Host permission policy owns these. */
22
28
  | { kind: "tool.pre"; tool: string; toolUseId: string | null }
23
29
  /** A file-writing tool (Edit, Write, apply_patch…): the before-write gate (S17). */
@@ -28,6 +34,7 @@ export type HookEvent =
28
34
  stdout: string;
29
35
  exitCode: number | null;
30
36
  toolUseId: string | null;
37
+ dialect?: "posix" | "powershell";
31
38
  }
32
39
  | { kind: "subagent.start"; agentId: string; agentType: string; task: string | null }
33
40
  | {