cowork-flow 0.0.20 → 0.0.21

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.
Files changed (70) hide show
  1. package/README.md +65 -20
  2. package/package.json +1 -1
  3. package/src/cli.js +109 -2
  4. package/src/commands/init.js +61 -4
  5. package/src/commands/sync.js +10 -1
  6. package/src/lib/copy-template.js +38 -7
  7. package/src/lib/platforms.js +73 -0
  8. package/template/.agent/skills/brainstorming/SKILL.md +13 -9
  9. package/template/.agent/skills/finish-work/SKILL.md +3 -3
  10. package/template/.agent/skills/meta/SKILL.md +1 -1
  11. package/template/.agent/skills/start/SKILL.md +35 -16
  12. package/template/.agent/skills/writing-plans/SKILL.md +1 -1
  13. package/template/.claude/agents/cowork-check.md +41 -0
  14. package/template/.claude/agents/cowork-implement.md +40 -0
  15. package/template/.claude/agents/cowork-research.md +40 -0
  16. package/template/.claude/commands/cowork-check.md +27 -0
  17. package/template/.claude/commands/cowork-implement.md +27 -0
  18. package/template/.claude/commands/cowork-research.md +27 -0
  19. package/template/.claude/hooks/inject-workflow-state.py +298 -0
  20. package/template/.claude/settings.json +24 -0
  21. package/template/.claude/skills/before-dev/SKILL.md +31 -0
  22. package/template/.claude/skills/brainstorming/SKILL.md +32 -0
  23. package/template/.claude/skills/break-loop/SKILL.md +26 -0
  24. package/template/.claude/skills/check/SKILL.md +29 -0
  25. package/template/.claude/skills/continue/SKILL.md +29 -0
  26. package/template/.claude/skills/finish-work/SKILL.md +32 -0
  27. package/template/.claude/skills/meta/SKILL.md +30 -0
  28. package/template/.claude/skills/python-design/SKILL.md +27 -0
  29. package/template/.claude/skills/start/SKILL.md +78 -0
  30. package/template/.claude/skills/update-spec/SKILL.md +39 -0
  31. package/template/.claude/skills/writing-plans/SKILL.md +70 -0
  32. package/template/.codex/agents/cowork-check.toml +17 -22
  33. package/template/.codex/agents/cowork-implement.toml +18 -23
  34. package/template/.codex/agents/cowork-research.toml +14 -19
  35. package/template/.codex/agents/default.toml +27 -0
  36. package/template/.codex/agents/explorer.toml +28 -0
  37. package/template/.codex/agents/worker.toml +29 -0
  38. package/template/.codex/hooks/inject-workflow-state.py +161 -108
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
  42. package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
  43. package/template/.cowork-flow/config.yaml +4 -6
  44. package/template/.cowork-flow/scripts/add_session.py +20 -7
  45. package/template/.cowork-flow/scripts/common/active_task.py +212 -1
  46. package/template/.cowork-flow/scripts/common/config.py +0 -17
  47. package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
  48. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  49. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  50. package/template/.cowork-flow/scripts/doctor.py +418 -40
  51. package/template/.cowork-flow/scripts/project_context.py +230 -0
  52. package/template/.cowork-flow/scripts/run.py +3 -0
  53. package/template/.cowork-flow/scripts/subagent.py +152 -238
  54. package/template/.cowork-flow/scripts/task.py +330 -17
  55. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  56. package/template/.cowork-flow/spec/capabilities.md +34 -0
  57. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  58. package/template/.cowork-flow/spec/registry.json +54 -0
  59. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  60. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  61. package/template/.cowork-flow/workflow.md +158 -203
  62. package/template/.opencode/agents/cowork-check.md +46 -0
  63. package/template/.opencode/agents/cowork-implement.md +45 -0
  64. package/template/.opencode/agents/cowork-research.md +46 -0
  65. package/template/.opencode/commands/cowork-check.md +26 -0
  66. package/template/.opencode/commands/cowork-implement.md +26 -0
  67. package/template/.opencode/commands/cowork-research.md +26 -0
  68. package/template/.opencode/plugins/cowork-flow.js +317 -0
  69. package/template/CLAUDE.md +5 -0
  70. package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
@@ -5,24 +5,28 @@ description: Use when requirements are unclear, a requested change has multiple
5
5
 
6
6
  # Brainstorming
7
7
 
8
- Use this skill to turn an idea into a clear change request before code changes begin.
8
+ Use this skill as the active clarification gate for new requirements. Turn an idea into a clear change request before PRD, planning, fixed-agent dispatch, or code changes begin.
9
9
 
10
10
  ## Flow
11
11
 
12
12
  1. Read only the context needed to understand the project area: `AGENTS.md`, `.cowork-flow/workflow.md`, and relevant spec indexes.
13
- 2. State assumptions, success criteria, and the smallest useful scope.
14
- 3. Ask one high-value question only when the answer cannot be inferred safely.
15
- 4. Present 2-3 viable approaches when trade-offs matter, with a concrete recommendation.
16
- 5. For L1/L2 work, create or update the change/task artifacts required by `.cowork-flow/workflow.md`.
17
- 6. Hand off to `writing-plans` when the requested behavior and acceptance criteria are clear.
13
+ 2. State the goal, non-goals, assumptions, scope boundary, success criteria, and the smallest useful scope.
14
+ 3. Ask one high-value question only when the answer cannot be inferred safely; otherwise proceed with explicit assumptions.
15
+ 4. Present 2-3 viable approaches when trade-offs matter, with a concrete recommended direction.
16
+ 5. Do not write PRD, planning, or fixed-agent dispatch input until the direction and acceptance criteria are clear.
17
+ 6. For L1/L2 work, create or update the change/task artifacts required by `.cowork-flow/workflow.md` after the clarification output is stable.
18
+ 7. Hand off to `writing-plans` when the requested behavior and acceptance criteria are clear.
18
19
 
19
20
  ## Output
20
21
 
21
22
  Keep the result practical:
22
23
 
23
- - Goal and non-goals.
24
- - Chosen approach and rejected alternatives.
24
+ - Goal.
25
+ - Non-goals.
26
+ - Key assumptions.
27
+ - Scope boundary, including in-scope and out-of-scope work.
28
+ - Recommended direction and rejected alternatives.
25
29
  - Acceptance criteria.
26
- - Open risks or blockers.
30
+ - Open questions, risks, or blockers, with their impact on the next step.
27
31
 
28
32
  Do not implement during brainstorming unless the user explicitly changes the request to a direct edit.
@@ -25,8 +25,8 @@ Before claiming completion, verify:
25
25
  2. Run `git diff --check`.
26
26
  3. Run focused tests and then broader project verification when appropriate.
27
27
  4. Update the plan/task status.
28
- 5. If commit policy allows, stage only expected files and commit.
29
- 6. Archive completed task/change artifacts when requested or required by workflow.
30
- 7. Record the session with `.cowork-flow/run add-session` after code policy is satisfied.
28
+ 5. Archive completed task/change artifacts when requested or required by workflow.
29
+ 6. Record the session with `.cowork-flow/run add-session --commit "-"` unless a prior commit hash is intentionally being recorded.
30
+ 7. If commit policy allows, stage only expected files and commit once.
31
31
 
32
32
  If the user asks not to commit, stop before staging and report the verified state.
@@ -18,7 +18,7 @@ Keep those copies aligned unless there is a documented reason not to.
18
18
 
19
19
  ## Rules
20
20
 
21
- - Prefer the current fixed-agent model: main session coordinates, child agents execute bounded tasks, `fork_turns="none"`.
21
+ - Prefer the current fixed-agent model: main session coordinates through the active Host Adapter, child agents execute bounded tasks with fresh context.
22
22
  - Do not add compatibility fallback paths for removed workflow models unless the user explicitly asks.
23
23
  - Delete stale skills, prompts, tests, and docs when their behavior is no longer valid.
24
24
  - Update tests that guard template contents, packaging, hooks, and task context generation.
@@ -5,15 +5,19 @@ description: Use when starting or resuming main-session work in a cowork-flow pr
5
5
 
6
6
  # Start
7
7
 
8
- This skill is for the main session. A bounded delegated task should use `entry-boundary` and then execute the delegated prompt directly.
8
+ This skill is for the main session only. Formal subagents are accepted only after runtime context binding is recorded and must not load this startup flow.
9
9
  Main repository changes follow `Plan -> Implement -> Check -> Finish`.
10
- Before loading state, classify the actual prompt. If it is a bounded delegated task, stop using this skill and execute that delegated prompt. A `任务:` / `约束:` / `输出:` structure is a strong delegated-task signal. Advisory/default subagent prompts should start with a natural-language delegated-task sentence rather than relying on repo bootstrap to infer intent. Keep project rules as constraints only.
10
+ Before loading state, classify only whether the current request is a
11
+ main-session request, read-only question, command-only wrapper, or unclear
12
+ input. Do not infer subagent identity from prompt shape. If a child has
13
+ `cowork_runtime_context_id`, hook/plugin binding may run early; otherwise the
14
+ child must run the explicit shim bind before formal work.
11
15
 
12
16
  ## Load State
13
17
 
14
18
  1. Read `AGENTS.md`.
15
19
  2. Read `.cowork-flow/workflow.md`.
16
- 3. Read `.cowork-flow/config.yaml` for `codex.post_ack_execution_grace_ms`.
20
+ 3. Read `.cowork-flow/config.yaml` for Codex dispatch hints and lifecycle hooks.
17
21
  4. Run `.cowork-flow/run resume` or `.\.cowork-flow\run.cmd resume` on Windows.
18
22
  5. Read the active task PRD and JSONL indexes only when a task is active.
19
23
  6. Read relevant `.cowork-flow/spec/*/index.md` files before code changes.
@@ -22,11 +26,17 @@ Report active task, workflow state, blockers, and the next phase.
22
26
 
23
27
  ## Route
24
28
 
25
- Route in stages. Before state is loaded, only true question-only requests and bounded delegated prompts bypass Load State. Repository-changing main-session requests load state first. After state is loaded, route to the next workflow phase; clear multi-step implementation uses `writing-plans` before fixed-agent dispatch.
29
+ Route in stages. Before state is loaded, true question-only requests may bypass
30
+ Load State. Repository-changing main-session requests load state first. After
31
+ state is loaded, apply the requirement clarification gate from
32
+ `.cowork-flow/workflow.md`, then route to the next workflow phase; clear
33
+ multi-step implementation uses `writing-plans` before fixed-agent dispatch.
34
+
35
+ New requirements that are unclear, boundary-unclear, multi-approach, behavior-changing, or missing acceptance criteria use `brainstorming` before PRD, planning, or fixed-agent dispatch. Small repository changes proceed directly only when the goal, scope boundary, and acceptance criteria are already clear.
26
36
 
27
37
  - Question-only work: answer directly.
28
- - Small repository change: classify by `.cowork-flow/workflow.md`, create/start a task if required, then proceed.
29
- - Unclear or multi-approach work: use `brainstorming`.
38
+ - Small repository change with clear goal/scope/acceptance: classify by `.cowork-flow/workflow.md`, create/start a task if required, then proceed.
39
+ - Unclear, boundary-unclear, behavior-changing, or multi-approach work: use `brainstorming`.
30
40
  - Multi-step implementation: use `writing-plans`, then dispatch fixed agents where appropriate.
31
41
  - Before coding: use `before-dev`.
32
42
  - After implementation: use `check`, then `finish-work`.
@@ -42,18 +52,27 @@ Route in stages. Before state is loaded, only true question-only requests and bo
42
52
 
43
53
  The main session owns coordination:
44
54
 
45
- - Research: `spawn_agent(agent_type="cowork-research", fork_turns="none")`
46
- - Implementation: `spawn_agent(agent_type="cowork-implement", fork_turns="none")`
47
- - Verification: `spawn_agent(agent_type="cowork-check", fork_turns="none")`
55
+ - Research: dispatch `cowork-research` through the active Host Adapter.
56
+ - Implementation: dispatch `cowork-implement` through the active Host Adapter.
57
+ - Verification: dispatch `cowork-check` through the active Host Adapter.
48
58
 
49
- Every dispatch prompt starts with:
59
+ Every formal dispatch uses runtime-context dispatch:
50
60
 
51
61
  ```text
52
- Active task: <task-dir>
62
+ cowork_runtime_context_id: <runtime_context_id>
63
+ cowork_host_context_key: <host_context_key>
53
64
  ```
54
65
 
55
- Prefer the `COWORK_DISPATCH_V1` envelope for `cowork-*` subagents, then wait for `COWORK_ACK <dispatch_id> <ack_token>` before sending `EXECUTE <dispatch_id>`.
56
-
57
- Use per-dispatch post-ACK execution grace after sending `EXECUTE`. The default duration is `300000` ms and can be configured with `codex.post_ack_execution_grace_ms` in `.cowork-flow/config.yaml`. Record `execute_sent_at[dispatch_id]` and compute `deadline[dispatch_id] = execute_sent_at[dispatch_id] + codex.post_ack_execution_grace_ms`; do not use a shared/global deadline across children. After `EXECUTE`, missing output or missing compass/status file is not proof that the child is stuck. Do not close a child only because it is still reading startup rules, AGENTS.md, workflow docs, specs, or task context. If `list_agents` still shows it running, continue waiting through grace. Grace expiration for one `dispatch_id` is a review checkpoint for that child only, not a close trigger and not evidence about other children. If progress, compass, or status files exist, keep waiting. Only close after wrong dispatch evidence, child completion, or user cancellation.
58
-
59
- After execution, use `wait_agent`, review the output, inspect `list_agents`, and `close_agent`.
66
+ Before spawning a formal child, create a runtime context with
67
+ `.cowork-flow/run subagent init` and pass the returned prompt transport through
68
+ the active Host Adapter. The child's first step is
69
+ `.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>`; if a
70
+ hook/plugin already bound the same key, this command is idempotent. The parent
71
+ must verify `status=bound` and `bound_context_key=<host_context_key>` before
72
+ accepting output. If binding is missing, closed, invalid, or mismatched, the
73
+ child fails closed and must not run start/resume/task activation/archive/commit
74
+ or coordinate other agents.
75
+
76
+ After dispatch, use adapter wait/list/cancel primitives, review the output,
77
+ verify deliverables, and close the runtime context with
78
+ `.cowork-flow/run subagent close <runtime_context_id>`.
@@ -27,7 +27,7 @@ Start with:
27
27
  ```markdown
28
28
  # <Feature> Implementation Plan
29
29
 
30
- > For agentic workers: use `spawn_agent(agent_type="cowork-implement", fork_turns="none")` for implementation and `spawn_agent(agent_type="cowork-check", fork_turns="none")` for verification. Every dispatch prompt starts with `Active task: <task-dir>`.
30
+ > For formal fixed-agent work: create a runtime context with `.cowork-flow/run subagent init`, pass `cowork_runtime_context_id: <runtime_context_id>` through the active Host Adapter, then dispatch `cowork-implement` or `cowork-check`. Close the runtime context after verification.
31
31
 
32
32
  **Goal:** <one sentence>
33
33
  **Architecture:** <2-3 sentences>
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: cowork-check
3
+ description: Cowork-flow check fixed subagent.
4
+ tools: Read, Write, Edit, MultiEdit, Grep, Glob, LS, Bash
5
+ ---
6
+
7
+ You are the `cowork-check` fixed subagent for Claude Code.
8
+ You are a leaf executor and must not invoke other agents.
9
+
10
+ Formal `cowork-check` work requires a bound runtime context. The prompt, host
11
+ metadata, or environment must provide:
12
+
13
+ ```text
14
+ cowork_runtime_context_id: <runtime_context_id>
15
+ cowork_host_context_key: <host_context_key>
16
+ ```
17
+
18
+ The hook may bind that id to
19
+ `.cowork-flow/.runtime/subagents/<runtime_context_id>.json` before workflow
20
+ state is injected. The first child step must still run:
21
+
22
+ ```bash
23
+ ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>
24
+ ```
25
+
26
+ If the explicit bind fails, or if the bound context is missing, closed, invalid,
27
+ or names another agent type, report `needs_context` and stop. Do not use
28
+ `COWORK_ENTRY_CONTRACT_V1` to infer subagent identity; that contract classifies
29
+ main-session prompts only.
30
+
31
+ Rules:
32
+
33
+ - Read the task directory from the bound runtime context.
34
+ - Read `<task>/prd.md`, `<task>/check.jsonl`, `git diff`, and each JSONL `file`
35
+ entry.
36
+ - Check acceptance criteria, tests, scope, and spec sync.
37
+ - Fix in-scope issues directly when small and clear.
38
+ - Report findings, fixes, and exact verification commands.
39
+ - Do not use the Task tool or invoke subagents.
40
+ - Do not run task start, task finish, task archive, unscoped resume, commit, or
41
+ push.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: cowork-implement
3
+ description: Cowork-flow implementation fixed subagent.
4
+ tools: Read, Write, Edit, MultiEdit, Grep, Glob, LS, Bash
5
+ ---
6
+
7
+ You are the `cowork-implement` fixed subagent for Claude Code.
8
+ You are a leaf executor and must not invoke other agents.
9
+
10
+ Formal `cowork-implement` work requires a bound runtime context. The prompt,
11
+ host metadata, or environment must provide:
12
+
13
+ ```text
14
+ cowork_runtime_context_id: <runtime_context_id>
15
+ cowork_host_context_key: <host_context_key>
16
+ ```
17
+
18
+ The hook may bind that id to
19
+ `.cowork-flow/.runtime/subagents/<runtime_context_id>.json` before workflow
20
+ state is injected. The first child step must still run:
21
+
22
+ ```bash
23
+ ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>
24
+ ```
25
+
26
+ If the explicit bind fails, or if the bound context is missing, closed, invalid,
27
+ or names another agent type, report `needs_context` and stop. Do not use
28
+ `COWORK_ENTRY_CONTRACT_V1` to infer subagent identity; that contract classifies
29
+ main-session prompts only.
30
+
31
+ Rules:
32
+
33
+ - Read the task directory from the bound runtime context.
34
+ - Read `<task>/prd.md`, `<task>/info.md` if present, `<task>/implement.jsonl`,
35
+ and each JSONL `file` entry.
36
+ - Keep edits inside the assigned scope.
37
+ - Report changed files and exact verification commands.
38
+ - Do not use the Task tool or invoke subagents.
39
+ - Do not run task start, task finish, task archive, unscoped resume, commit, or
40
+ push.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: cowork-research
3
+ description: Cowork-flow research fixed subagent.
4
+ tools: Read, Grep, Glob, LS
5
+ ---
6
+
7
+ You are the `cowork-research` fixed subagent for Claude Code.
8
+ You are a leaf executor and must not invoke other agents.
9
+
10
+ Formal `cowork-research` work requires a bound runtime context. The prompt,
11
+ host metadata, or environment must provide:
12
+
13
+ ```text
14
+ cowork_runtime_context_id: <runtime_context_id>
15
+ cowork_host_context_key: <host_context_key>
16
+ ```
17
+
18
+ The hook may bind that id to
19
+ `.cowork-flow/.runtime/subagents/<runtime_context_id>.json` before workflow
20
+ state is injected. The first child step must still run:
21
+
22
+ ```bash
23
+ ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>
24
+ ```
25
+
26
+ If the explicit bind fails, or if the bound context is missing, closed, invalid,
27
+ or names another agent type, report `needs_context` and stop. Do not use
28
+ `COWORK_ENTRY_CONTRACT_V1` to infer subagent identity; that contract classifies
29
+ main-session prompts only.
30
+
31
+ Rules:
32
+
33
+ - Read the task directory and assignment from the bound runtime context.
34
+ - Read task context and prompt-named files only.
35
+ - Write research notes only under the assigned task `research/` directory when
36
+ explicitly asked.
37
+ - Report findings with file and line evidence.
38
+ - Do not use the Task tool or invoke subagents.
39
+ - Do not run task start, task finish, task archive, unscoped resume, commit, or
40
+ push.
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Dispatch cowork-flow check fixed subagent.
3
+ argument-hint: "<task-dir>"
4
+ ---
5
+
6
+ Use the `cowork-check` agent for `$ARGUMENTS`.
7
+
8
+ Before dispatch, create a runtime context:
9
+
10
+ ```bash
11
+ ./.cowork-flow/run subagent init --role check --agent-type cowork-check --execution-task-dir "$ARGUMENTS" --title "Check $ARGUMENTS" --host claude-code --adapter claude-code.subagent
12
+ ```
13
+
14
+ Pass the returned prompt transport to the subagent:
15
+
16
+ ```text
17
+ cowork_runtime_context_id: <runtime_context_id>
18
+ cowork_host_context_key: <host_context_key>
19
+ ```
20
+
21
+ The child must first bind the runtime context before role work:
22
+
23
+ ```bash
24
+ ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>
25
+ ```
26
+
27
+ Missing, closed, invalid, mismatched, or unbound context is `needs_context`.
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Dispatch cowork-flow implementation fixed subagent.
3
+ argument-hint: "<task-dir>"
4
+ ---
5
+
6
+ Use the `cowork-implement` agent for `$ARGUMENTS`.
7
+
8
+ Before dispatch, create a runtime context:
9
+
10
+ ```bash
11
+ ./.cowork-flow/run subagent init --role implement --agent-type cowork-implement --execution-task-dir "$ARGUMENTS" --title "Implement $ARGUMENTS" --host claude-code --adapter claude-code.subagent
12
+ ```
13
+
14
+ Pass the returned prompt transport to the subagent:
15
+
16
+ ```text
17
+ cowork_runtime_context_id: <runtime_context_id>
18
+ cowork_host_context_key: <host_context_key>
19
+ ```
20
+
21
+ The child must first bind the runtime context before role work:
22
+
23
+ ```bash
24
+ ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>
25
+ ```
26
+
27
+ Missing, closed, invalid, mismatched, or unbound context is `needs_context`.
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Dispatch cowork-flow research fixed subagent.
3
+ argument-hint: "<task-dir>"
4
+ ---
5
+
6
+ Use the `cowork-research` agent for `$ARGUMENTS`.
7
+
8
+ Before dispatch, create a runtime context:
9
+
10
+ ```bash
11
+ ./.cowork-flow/run subagent init --role research --agent-type cowork-research --execution-task-dir "$ARGUMENTS" --title "Research $ARGUMENTS" --host claude-code --adapter claude-code.subagent
12
+ ```
13
+
14
+ Pass the returned prompt transport to the subagent:
15
+
16
+ ```text
17
+ cowork_runtime_context_id: <runtime_context_id>
18
+ cowork_host_context_key: <host_context_key>
19
+ ```
20
+
21
+ The child must first bind the runtime context before role work:
22
+
23
+ ```bash
24
+ ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>
25
+ ```
26
+
27
+ Missing, closed, invalid, mismatched, or unbound context is `needs_context`.
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env python3
2
+ """Emit cowork-flow workflow context for Claude Code hooks."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ TAG_RE = re.compile(
15
+ r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]",
16
+ re.DOTALL,
17
+ )
18
+ DEFAULT_CONTRACT_REGISTRY = {
19
+ "contracts": [
20
+ {
21
+ "id": "COWORK_ENTRY_CONTRACT_V1",
22
+ "path": ".cowork-flow/spec/entry-contract.md",
23
+ "digest": [
24
+ "Classify main-session requests before task start, resume, archive, or commit.",
25
+ "Runtime context, not prompt labels, identifies formal subagent sessions.",
26
+ ],
27
+ "readWhen": ["before task start/resume/archive", "when prompt and bootstrap text conflict"],
28
+ },
29
+ {
30
+ "id": "RUNTIME_CONTEXT_DISPATCH_V2",
31
+ "path": ".cowork-flow/spec/subagent-dispatch.md",
32
+ "digest": [
33
+ "Formal subagent work is keyed by cowork_runtime_context_id.",
34
+ "Explicit shim bind records bound_context_key before formal output is accepted.",
35
+ ],
36
+ "readWhen": ["before formal subagent dispatch", "when checking subagent health"],
37
+ },
38
+ ]
39
+ }
40
+
41
+
42
+ def _find_repo_root(start: Path) -> Path | None:
43
+ current = start.resolve()
44
+ while True:
45
+ if (current / ".cowork-flow").is_dir():
46
+ return current
47
+ if current == current.parent:
48
+ return None
49
+ current = current.parent
50
+
51
+
52
+ def _read_hook_input() -> dict[str, Any]:
53
+ try:
54
+ raw = sys.stdin.buffer.read()
55
+ except AttributeError:
56
+ text = sys.stdin.read()
57
+ else:
58
+ if not raw.strip():
59
+ return {}
60
+ try:
61
+ text = raw.decode("utf-8-sig")
62
+ except UnicodeDecodeError:
63
+ encoding = getattr(sys.stdin, "encoding", None) or "utf-8"
64
+ text = raw.decode(encoding, errors="replace")
65
+ if not text.strip():
66
+ return {}
67
+ try:
68
+ data = json.loads(text)
69
+ except (json.JSONDecodeError, ValueError):
70
+ return {}
71
+ return data if isinstance(data, dict) else {}
72
+
73
+
74
+ def _hook_event_name(hook_input: dict[str, Any]) -> str:
75
+ value = hook_input.get("hook_event_name") or hook_input.get("hookEventName")
76
+ if isinstance(value, str) and value.strip():
77
+ return value.strip()
78
+ return "UserPromptSubmit"
79
+
80
+
81
+ def _normalize_hook_input(hook_input: dict[str, Any]) -> dict[str, Any]:
82
+ normalized = dict(hook_input)
83
+ if "claude_session_id" not in normalized:
84
+ session_id = normalized.get("session_id")
85
+ if isinstance(session_id, str) and session_id.strip():
86
+ normalized["claude_session_id"] = session_id
87
+ return normalized
88
+
89
+
90
+ def _load_breadcrumbs(root: Path) -> dict[str, str]:
91
+ workflow = root / ".cowork-flow" / "spec" / "workflow-state-templates.md"
92
+ try:
93
+ text = workflow.read_text(encoding="utf-8")
94
+ except OSError:
95
+ return {}
96
+ return {match.group(1): match.group(2).strip() for match in TAG_RE.finditer(text)}
97
+
98
+
99
+ def _as_string_list(value: Any) -> list[str]:
100
+ if not isinstance(value, list):
101
+ return []
102
+ return [item for item in value if isinstance(item, str) and item.strip()]
103
+
104
+
105
+ def _load_contract_registry(root: Path) -> list[dict[str, Any]]:
106
+ registry_file = root / ".cowork-flow" / "spec" / "registry.json"
107
+ try:
108
+ data = json.loads(registry_file.read_text(encoding="utf-8"))
109
+ except (OSError, json.JSONDecodeError):
110
+ data = DEFAULT_CONTRACT_REGISTRY
111
+ contracts = data.get("contracts") if isinstance(data, dict) else None
112
+ if not isinstance(contracts, list):
113
+ contracts = DEFAULT_CONTRACT_REGISTRY["contracts"]
114
+ return [contract for contract in contracts if isinstance(contract, dict)]
115
+
116
+
117
+ def _contract_fingerprint(root: Path, contracts: list[dict[str, Any]]) -> str:
118
+ digest = hashlib.sha256()
119
+ digest.update(json.dumps(contracts, ensure_ascii=False, sort_keys=True).encode("utf-8"))
120
+ for contract in contracts:
121
+ path = contract.get("path")
122
+ if not isinstance(path, str) or not path.strip():
123
+ continue
124
+ try:
125
+ digest.update((root / path).read_bytes())
126
+ except OSError:
127
+ digest.update(f"missing:{path}".encode("utf-8"))
128
+ return digest.hexdigest()[:16]
129
+
130
+
131
+ def _build_contract_digest(root: Path) -> str:
132
+ contracts = _load_contract_registry(root)
133
+ fingerprint = _contract_fingerprint(root, contracts)
134
+ lines = [
135
+ '<cowork-runtime host="claude-code" adapter="claude-code.hooks">',
136
+ f'<contract-digest fingerprint="{fingerprint}">',
137
+ "policy: repeat this short digest every hook; read full spec files only before listed actions.",
138
+ ]
139
+ for contract in contracts:
140
+ contract_id = contract.get("id")
141
+ path = contract.get("path")
142
+ if not isinstance(contract_id, str) or not contract_id.strip():
143
+ continue
144
+ path_text = path if isinstance(path, str) and path.strip() else "<missing-path>"
145
+ lines.append(f"- {contract_id}: {path_text}")
146
+ for item in _as_string_list(contract.get("digest"))[:2]:
147
+ lines.append(f" digest: {item}")
148
+ read_when = _as_string_list(contract.get("readWhen"))
149
+ if read_when:
150
+ lines.append(f" read_before: {'; '.join(read_when)}")
151
+ lines.extend(["</contract-digest>", "</cowork-runtime>"])
152
+ return "\n".join(lines)
153
+
154
+
155
+ def _load_common(root: Path) -> None:
156
+ scripts_dir = root / ".cowork-flow" / "scripts"
157
+ if str(scripts_dir) not in sys.path:
158
+ sys.path.insert(0, str(scripts_dir))
159
+
160
+
161
+ def _get_active_task(root: Path, hook_input: dict[str, Any]) -> tuple[str | None, str, str]:
162
+ _load_common(root)
163
+ try:
164
+ from common.active_task import get_active_task # type: ignore[import-not-found]
165
+ except Exception:
166
+ return None, "no_task", "unavailable"
167
+
168
+ active = get_active_task(root, hook_input)
169
+ if not active.task_path:
170
+ return None, "no_task", active.source
171
+
172
+ task_dir = root / active.task_path
173
+ task_json = task_dir / "task.json"
174
+ try:
175
+ data = json.loads(task_json.read_text(encoding="utf-8"))
176
+ except (OSError, json.JSONDecodeError):
177
+ return active.task_path, "stale", active.source
178
+
179
+ status = data.get("status")
180
+ if not isinstance(status, str) or not status.strip():
181
+ status = "unknown"
182
+ return active.task_path, status.strip(), active.source
183
+
184
+
185
+ def _resolve_runtime_context(root: Path, hook_input: dict[str, Any]) -> tuple[dict[str, Any] | None, str | None]:
186
+ _load_common(root)
187
+ try:
188
+ from common.active_task import ( # type: ignore[import-not-found]
189
+ bind_runtime_context,
190
+ read_runtime_context,
191
+ resolve_runtime_context_id,
192
+ )
193
+ except Exception:
194
+ return None, None
195
+
196
+ runtime_context_id = resolve_runtime_context_id(hook_input)
197
+ if not runtime_context_id:
198
+ return None, None
199
+
200
+ context = read_runtime_context(root, runtime_context_id)
201
+ if not context or context.get("scope") != "subagent" or context.get("status") == "closed":
202
+ return None, runtime_context_id
203
+
204
+ bound = bind_runtime_context(root, runtime_context_id, values=hook_input)
205
+ return bound or context, runtime_context_id
206
+
207
+
208
+ def _subagent_runtime_lines(context: dict[str, Any]) -> list[str]:
209
+ assignment = context.get("assignment") if isinstance(context.get("assignment"), dict) else {}
210
+ lines = [
211
+ f"Runtime context: {context.get('runtime_context_id')}",
212
+ f"Agent: {context.get('agent_type') or 'unknown'}",
213
+ "Scope: subagent",
214
+ "Do not run start/resume/task start/archive/commit/spawn.",
215
+ ]
216
+ goal = assignment.get("goal") if isinstance(assignment, dict) else None
217
+ if isinstance(goal, str) and goal.strip():
218
+ lines.append(f"Goal: {goal.strip()}")
219
+ return lines
220
+
221
+
222
+ def _build_context(
223
+ root: Path,
224
+ task_path: str | None,
225
+ status: str,
226
+ source: str,
227
+ breadcrumbs: dict[str, str],
228
+ extra_lines: list[str] | None = None,
229
+ ) -> str:
230
+ body = breadcrumbs.get(status) or "Refer to .cowork-flow/workflow.md for the current step."
231
+ if extra_lines:
232
+ body = "\n".join([body, *extra_lines])
233
+ if task_path is None:
234
+ header = f"Status: {status}\nSource: {source}"
235
+ else:
236
+ header = f"Task: {task_path}\nStatus: {status}\nSource: {source}"
237
+
238
+ return "\n\n".join(
239
+ [
240
+ "<claude-code-runtime>\nhooks: UserPromptSubmit, SessionStart\n</claude-code-runtime>",
241
+ _build_contract_digest(root),
242
+ f"<workflow-state>\n{header}\n{body}\n</workflow-state>",
243
+ ]
244
+ )
245
+
246
+
247
+ def main() -> int:
248
+ if os.environ.get("COWORK_FLOW_HOOKS") == "0" or os.environ.get("COWORK_FLOW_DISABLE_HOOKS") == "1":
249
+ return 0
250
+
251
+ hook_input = _normalize_hook_input(_read_hook_input())
252
+ cwd_value = hook_input.get("cwd")
253
+ cwd = Path(cwd_value) if isinstance(cwd_value, str) and cwd_value.strip() else Path.cwd()
254
+ root = _find_repo_root(cwd)
255
+ if root is None:
256
+ return 0
257
+
258
+ event_name = _hook_event_name(hook_input)
259
+ breadcrumbs = _load_breadcrumbs(root)
260
+ runtime_context, runtime_context_id = (
261
+ _resolve_runtime_context(root, hook_input) if event_name == "UserPromptSubmit" else (None, None)
262
+ )
263
+ extra_lines: list[str] | None = None
264
+ if runtime_context is not None:
265
+ task_dir = runtime_context.get("task_dir")
266
+ task_path = task_dir.strip() if isinstance(task_dir, str) and task_dir.strip() else None
267
+ status = "delegated_subtask"
268
+ source = f"runtime-context:{runtime_context.get('runtime_context_id')}"
269
+ extra_lines = _subagent_runtime_lines(runtime_context)
270
+ elif runtime_context_id:
271
+ task_path = None
272
+ status = "delegated_subtask"
273
+ source = f"runtime-context-invalid:{runtime_context_id}"
274
+ extra_lines = [
275
+ f"Runtime context: {runtime_context_id}",
276
+ "Scope: subagent",
277
+ "Runtime context is missing, closed, or invalid. Do not run start/resume/task start/archive/commit/spawn.",
278
+ ]
279
+ else:
280
+ task_path, status, source = _get_active_task(root, hook_input)
281
+ additional_context = _build_context(root, task_path, status, source, breadcrumbs, extra_lines)
282
+
283
+ print(
284
+ json.dumps(
285
+ {
286
+ "hookSpecificOutput": {
287
+ "hookEventName": event_name,
288
+ "additionalContext": additional_context,
289
+ }
290
+ },
291
+ ensure_ascii=False,
292
+ )
293
+ )
294
+ return 0
295
+
296
+
297
+ if __name__ == "__main__":
298
+ raise SystemExit(main())