Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/workit-next/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ I checked the Claude Code facts against the local binary, v2.1.288: the `claude
| 4 | Evidence becomes "host_observed" | What we get is **CLI-observed**: the CLI saw the exit code. It is attributed to a session or agent only if a host hook attests it. Claude can attest via `PostToolUse` (its common input carries `session_id` and `agent_id` on subagent calls). Other hosts degrade to `attested:false`. Model this as `observer:"workit_cli"` + `attestation: {host, session, agentId} \| null`. |
| 5 | "Run ledger lives in `.workit/`" (D6), with implicit task per branch/worktree | Stacks, verdicts and branch tasks span worktrees (fanout implementers run in `isolation: worktree`), and they must outlive a removed worktree. `<cwd>/.workit` is per-checkout, and `git clean -fdx` deletes it (it is ignored). **Recommendation:** in git repos the store root is `$(git rev-parse --git-common-dir)/workit/`; non-git directories keep `<cwd>/.workit/`. This deviates from D6 and needs a user OK. |
| 6 | Forge = configured provider | `vcsConfig()` (`vcs-config.ts:150`) lets the workspace `vcs.provider` win over the origin host. Repos migrated from GitLab to GitHub under the `work` glob therefore misroute, which is why the `workit-github-override` skill exists. New verbs must derive the forge **from the push remote host**. If config disagrees, return `blocked` with an unblock hint. |
| 7 | Additive fields are safe | Every record schema is `.strict()`. Older hosts read the same store (OpenCode `@latest`, cached Cursor npx), and a new optional field in `evidenceSchema` makes them fail with `recovery_required`. S8 must ship reader tolerance (strip unknown keys on read, strict on write) **one release before** S9 writes new fields. Alternatively, release S8 and S9 in the same version and accept the window. |
| 7 | Additive fields are safe | Every record schema is `.strict()`. Older hosts read the same store (OpenCode `@latest`, cached Cursor npx), and a new optional field in `evidenceSchema` makes them fail with `recovery_required`. S8 must ship reader tolerance (strip unknown keys on read, strict on write) **one release before** S9 writes new fields. Alternatively, release S8 and S9 in the same version and accept the window. | **Rule (S8a):** a new record field must be safe for an older reader to ignore and to lose on rewrite, **or** the writer lists its path in the record's top-level `critical` array; a reader that would strip a critical path fails closed with the upgrade message and never rewrites the record. Recovery never writes back a record it had to strip.
| 8 | `pr status` via gh porcelain | Local gh is **2.45.0**, and its `gh pr checks` has **no `--json`**. Use `gh api graphql` / `gh api` and `glab api` only. This matches the existing code (`remoteBranchTip`, `mergedBranch`) and does not depend on the CLI version. |
| 9 | Stack land: "PR 3 is retargeted" | With squash merges, retargeting **forces a restack**: `git rebase --onto <trunk> <oldParentTip>`, then push `--force-with-lease`. That changes the head SHA and re-triggers CI. Patch-id carry-over keeps the *verdict*, not CI. Only a merge-commit strategy can retarget without a restack. |
| 10 | Codex adapter is reusable for Claude | `parseCodexHookInput` rejects **unknown keys** and denies `PreToolUse` on a parse error. A Codex release that adds a field would therefore deny every tool call (a latent bug). Claude sends extra keys (`prompt_id`, `agent_id`, `agent_type`, `mcp_server`, …). All parsers must be lenient on unknown keys and strict only on required ones. |
| 11 | Cursor hooks are fine | Every Cursor hook spawns `npx -y --prefer-online …@latest` per event, so there is a registry round-trip on each shell command. That is out of scope here, but the descriptor should flag it, and `doctor` should suggest a local install. |
| 11 | Cursor hooks are fine | Every Cursor hook spawns `npx -y --prefer-online …@latest` per event, so there is a registry round-trip on each shell command. That is out of scope here, but the descriptor should flag it, and `doctor` should suggest a local install. | **Follow-up:** the Cursor hook answers `{permission:"allow"}` for compliant shell commands and pre-tool calls (pre-existing). Verify against a live Cursor whether `allow` skips Cursor's own approval prompt; if it does, answer with no permission instead.
| 12 | Host authority suffices (D2) | A user who allowlists `Bash(workit *)` makes `workit pr merge` silent. **Grants are the real ceiling.** They must live only in user config (`~/.config/workit/workspaces.json`), never in a repo file, and must not be settable headless. Docs should recommend allowlisting read verbs only. |
| 13 | `hostSchema` | `task-contract.ts:80` has no `claude_code`. Add it in S8, not S14, so S14 does not touch the contract. |
| 14 | S2b (engine revision retry) | It is in the plan but has no branch yet. S9's `observeCheck` and the hooks must not surface `revision_conflict`, so S2b is a hard prerequisite for S9. |
Expand Down Expand Up @@ -87,7 +87,7 @@ Behavior in S8 (no model change):
|---|---|---|---|---|---|
| session.start | `SessionStart` (matcher `startup\|resume\|clear\|compact`) → `hookSpecificOutput.additionalContext`; also append `export WORKIT_HOST=claude_code WORKIT_SESSION_ID=…` to `$CLAUDE_ENV_FILE` | `SessionStart` → `additionalContext` | `sessionStart` → `additional_context` | n/a (per-turn) | `session_start` |
| context.turn | `UserPromptSubmit` → `additionalContext`, only when the task revision changed (cache in `${CLAUDE_PLUGIN_DATA}/ctx/<session>.json`; each hook is a new process, so the in-memory cache in `session-context.ts` is useless) | undocumented → none | none | `session.hook("context")` → `injectAgentContext` | `before_agent_start` → `{message}` |
| shell.pre | `PreToolUse` matcher `Bash`, `"if":"Bash(git *)"` → `permissionDecision:"deny"`, `permissionDecisionReason`. **Never emit `allow`**: it would bypass the user's permission prompt | `PreToolUse` (bash/unified-exec) → deny | `beforeShellExecution` → `{permission:"deny",agent_message}`, exit 2 (**new**) | `permission.hook("evaluate")` → `event.effect="deny"` (`v2/permissions.ts`) | `tool_call` bash → `{block,reason}` (exists) |
| shell.pre | `PreToolUse` matchers `Bash` (`"if":"Bash(git *)"`) and `PowerShell` (`"if":"PowerShell(git *)"`, Windows) → `permissionDecision:"deny"`, `permissionDecisionReason`. **Never emit `allow`**: it would bypass the user's permission prompt | `PreToolUse` (bash/unified-exec) → deny | `beforeShellExecution` → `{permission:"deny",agent_message}`, exit 2 (**new**) | `permission.hook("evaluate")` → `event.effect="deny"` (`v2/permissions.ts`) | `tool_call` bash → `{block,reason}` (exists) |
| shell.post | `PostToolUse` matcher `Bash`, `"if":"Bash(workit *)"`, input has `tool_response` + `agent_id` | undocumented | undocumented | `tool.execute.after` (bash) | `tool_result` |
| subagent.start | `SubagentStart` (`agent_id`,`agent_type`) → `additionalContext` only | `SubagentStart` | `subagentStart` (can deny) | `tool.execute.before` tool=`subagent` | n/a (supervisor) |
| subagent.stop | `SubagentStop` (`agent_transcript_path`, `last_assistant_message`) → `decision:"block"` + `reason` to continue | `SubagentStop` | `subagentStop` (no stable id) | `tool.execute.after` | worker protocol |
Expand Down Expand Up @@ -322,7 +322,8 @@ packages/workit-claude-code/
{"hooks":{
"SessionStart":[{"matcher":"startup|resume|clear|compact","hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":10}]}],
"UserPromptSubmit":[{"hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]}],
"PreToolUse":[{"matcher":"Bash","if":"Bash(git *)","hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]}],
"PreToolUse":[{"matcher":"Bash","if":"Bash(git *)","hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]},
{"matcher":"PowerShell","if":"PowerShell(git *)","hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]}],
"PostToolUse":[{"matcher":"Bash","if":"Bash(workit *)","hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]}],
"SubagentStart":[{"hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]}],
"SubagentStop":[{"hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/bin/workit-hook","timeout":5}]}],
Expand Down
Loading
Loading