@brainervirus/workit-core 8.1.0 → 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.
- package/package.json +1 -1
- package/skills/workit-ship/SKILL.md +14 -14
- package/src/core/task-store.ts +4 -2
- package/src/core/workspaces.ts +84 -38
- package/src/forge/exec.ts +72 -3
- package/src/forge/github.ts +143 -0
- package/src/forge/gitlab.ts +167 -0
- package/src/forge/pr-edit.ts +291 -0
- package/src/forge/pr-ops.ts +103 -17
- package/src/forge/report.ts +4 -3
- package/src/forge/resolve.ts +328 -78
- package/src/forge/types.ts +32 -0
- package/src/git/ops.ts +3 -0
- package/src/git/rev.ts +10 -5
- package/src/hooks/handle.ts +33 -7
- package/src/hooks/hosts/claude-code.ts +12 -2
- package/src/hooks/hosts/codex.ts +33 -8
- package/src/hooks/hosts/cursor.ts +10 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/protocol.ts +8 -1
- package/src/hooks/raw-git.ts +612 -0
- package/src/hooks/shell-words.ts +128 -0
- package/src/hooks/write-gate.ts +1 -78
- package/src/host-session.ts +53 -0
- package/src/ledger.ts +17 -3
|
@@ -135,6 +135,8 @@ type Parsed = { ok: true; event: HookEvent } | { ok: false; error: string };
|
|
|
135
135
|
|
|
136
136
|
/** Claude's shell tools: Bash everywhere, PowerShell on Windows. */
|
|
137
137
|
const SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
|
|
138
|
+
const dialectOf = (value: Record<string, unknown>) =>
|
|
139
|
+
value.tool_name === "PowerShell" ? ("powershell" as const) : ("posix" as const);
|
|
138
140
|
const toolCommand = (value: Record<string, unknown>): string | null =>
|
|
139
141
|
isRecord(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
140
142
|
|
|
@@ -163,7 +165,7 @@ const eventOf = (name: ClaudeHookEvent, value: Record<string, unknown>): Parsed
|
|
|
163
165
|
return { ok: true, event: { kind: "tool.pre", tool: value.tool_name, toolUseId } };
|
|
164
166
|
const command = toolCommand(value);
|
|
165
167
|
return command
|
|
166
|
-
? { ok: true, event: { kind: "shell.pre", command, toolUseId } }
|
|
168
|
+
? { ok: true, event: { kind: "shell.pre", command, toolUseId, dialect: dialectOf(value) } }
|
|
167
169
|
: { ok: false, error: `tool_input.command is required for ${value.tool_name}` };
|
|
168
170
|
}
|
|
169
171
|
case "PostToolUse": {
|
|
@@ -179,6 +181,7 @@ const eventOf = (name: ClaudeHookEvent, value: Record<string, unknown>): Parsed
|
|
|
179
181
|
stdout: optionalText(response.stdout) ?? "",
|
|
180
182
|
exitCode: typeof response.exit_code === "number" ? response.exit_code : null,
|
|
181
183
|
toolUseId,
|
|
184
|
+
dialect: dialectOf(value),
|
|
182
185
|
},
|
|
183
186
|
};
|
|
184
187
|
}
|
|
@@ -222,7 +225,14 @@ const eventOf = (name: ClaudeHookEvent, value: Record<string, unknown>): Parsed
|
|
|
222
225
|
}
|
|
223
226
|
};
|
|
224
227
|
|
|
225
|
-
|
|
228
|
+
/** Events whose hookSpecificOutput takes additionalContext (PreToolUse: a raw-git nudge). */
|
|
229
|
+
const CONTEXT_EVENTS = new Set([
|
|
230
|
+
"SessionStart",
|
|
231
|
+
"UserPromptSubmit",
|
|
232
|
+
"SubagentStart",
|
|
233
|
+
"PreToolUse",
|
|
234
|
+
"PostToolUse",
|
|
235
|
+
]);
|
|
226
236
|
|
|
227
237
|
/** Never emits `allow`: that would bypass the user's own permission prompt. */
|
|
228
238
|
const render = (decision: HookDecision, native: string | null) => {
|
package/src/hooks/hosts/codex.ts
CHANGED
|
@@ -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 =
|
|
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":
|
|
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: "
|
|
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
|
-
|
|
170
|
-
|
|
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 (
|
|
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
|
-
...(
|
|
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
|
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -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";
|
package/src/hooks/protocol.ts
CHANGED
|
@@ -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
|
-
| {
|
|
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
|
| {
|