cowork-flow 0.0.19 → 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 (69) 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 +38 -14
  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 +172 -90
  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 -1
  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/entry_classifier.py +122 -0
  47. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  48. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  49. package/template/.cowork-flow/scripts/doctor.py +417 -29
  50. package/template/.cowork-flow/scripts/project_context.py +230 -0
  51. package/template/.cowork-flow/scripts/run.py +3 -0
  52. package/template/.cowork-flow/scripts/subagent.py +152 -238
  53. package/template/.cowork-flow/scripts/task.py +330 -17
  54. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  55. package/template/.cowork-flow/spec/capabilities.md +34 -0
  56. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  57. package/template/.cowork-flow/spec/registry.json +54 -0
  58. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  59. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  60. package/template/.cowork-flow/workflow.md +154 -190
  61. package/template/.opencode/agents/cowork-check.md +46 -0
  62. package/template/.opencode/agents/cowork-implement.md +45 -0
  63. package/template/.opencode/agents/cowork-research.md +46 -0
  64. package/template/.opencode/commands/cowork-check.md +26 -0
  65. package/template/.opencode/commands/cowork-implement.md +26 -0
  66. package/template/.opencode/commands/cowork-research.md +26 -0
  67. package/template/.opencode/plugins/cowork-flow.js +317 -0
  68. package/template/CLAUDE.md +5 -0
  69. package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
@@ -0,0 +1,317 @@
1
+ import { createHash } from "node:crypto"
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
+ import { dirname, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const DEFAULT_CONTRACT_REGISTRY = {
7
+ contracts: [
8
+ {
9
+ id: "COWORK_ENTRY_CONTRACT_V1",
10
+ path: ".cowork-flow/spec/entry-contract.md",
11
+ digest: [
12
+ "Classify main-session requests before task start, resume, archive, or commit.",
13
+ "Runtime context, not prompt labels, identifies formal subagent sessions.",
14
+ ],
15
+ readWhen: ["before task start/resume/archive", "when prompt and bootstrap text conflict"],
16
+ },
17
+ {
18
+ id: "RUNTIME_CONTEXT_DISPATCH_V2",
19
+ path: ".cowork-flow/spec/subagent-dispatch.md",
20
+ digest: [
21
+ "Formal subagent work is keyed by cowork_runtime_context_id.",
22
+ "Explicit shim bind records bound_context_key before formal output is accepted.",
23
+ ],
24
+ readWhen: ["before formal subagent dispatch", "when checking subagent health"],
25
+ },
26
+ ],
27
+ }
28
+
29
+ const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
30
+
31
+ function asStringList(value) {
32
+ if (!Array.isArray(value)) {
33
+ return []
34
+ }
35
+ return value.filter((item) => typeof item === "string" && item.trim())
36
+ }
37
+
38
+ function inputCwd(input) {
39
+ for (const value of [
40
+ input?.cwd,
41
+ input?.session?.cwd,
42
+ input?.context?.cwd,
43
+ input?.workspace?.cwd,
44
+ ]) {
45
+ if (typeof value === "string" && value.trim()) {
46
+ return value
47
+ }
48
+ }
49
+ return null
50
+ }
51
+
52
+ function findRepoRoot(input) {
53
+ const candidates = [
54
+ inputCwd(input),
55
+ typeof process !== "undefined" ? process.cwd?.() : null,
56
+ pluginRoot,
57
+ ].filter(Boolean)
58
+
59
+ for (const candidate of candidates) {
60
+ let current = resolve(candidate)
61
+ while (true) {
62
+ if (existsSync(resolve(current, ".cowork-flow"))) {
63
+ return current
64
+ }
65
+ const parent = resolve(current, "..")
66
+ if (parent === current) {
67
+ break
68
+ }
69
+ current = parent
70
+ }
71
+ }
72
+ return pluginRoot
73
+ }
74
+
75
+ function stableStringify(value) {
76
+ if (Array.isArray(value)) {
77
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`
78
+ }
79
+ if (value && typeof value === "object") {
80
+ return `{${Object.keys(value)
81
+ .sort()
82
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
83
+ .join(",")}}`
84
+ }
85
+ return JSON.stringify(value)
86
+ }
87
+
88
+ function loadContractRegistry(root) {
89
+ const registryFile = resolve(root, ".cowork-flow", "spec", "registry.json")
90
+ let data = DEFAULT_CONTRACT_REGISTRY
91
+ try {
92
+ data = JSON.parse(readFileSync(registryFile, "utf8"))
93
+ } catch {
94
+ data = DEFAULT_CONTRACT_REGISTRY
95
+ }
96
+
97
+ if (!Array.isArray(data?.contracts)) {
98
+ return DEFAULT_CONTRACT_REGISTRY.contracts
99
+ }
100
+ return data.contracts.filter((contract) => contract && typeof contract === "object")
101
+ }
102
+
103
+ function contractFingerprint(root, contracts) {
104
+ const digest = createHash("sha256")
105
+ digest.update(stableStringify(contracts), "utf8")
106
+ for (const contract of contracts) {
107
+ const path = contract.path
108
+ if (typeof path !== "string" || !path.trim()) {
109
+ continue
110
+ }
111
+ try {
112
+ digest.update(readFileSync(resolve(root, path)))
113
+ } catch {
114
+ digest.update(`missing:${path}`, "utf8")
115
+ }
116
+ }
117
+ return digest.digest("hex").slice(0, 16)
118
+ }
119
+
120
+ function buildContractDigest(input) {
121
+ const root = findRepoRoot(input)
122
+ const contracts = loadContractRegistry(root)
123
+ const fingerprint = contractFingerprint(root, contracts)
124
+ const lines = [
125
+ '<cowork-runtime host="opencode" adapter="opencode.task">',
126
+ `<contract-digest fingerprint="${fingerprint}">`,
127
+ "policy: repeat this short digest every plugin transform; read full spec files only before listed actions.",
128
+ ]
129
+
130
+ for (const contract of contracts) {
131
+ const contractId = contract.id
132
+ if (typeof contractId !== "string" || !contractId.trim()) {
133
+ continue
134
+ }
135
+ const path = typeof contract.path === "string" && contract.path.trim() ? contract.path : "<missing-path>"
136
+ lines.push(`- ${contractId}: ${path}`)
137
+ for (const item of asStringList(contract.digest).slice(0, 2)) {
138
+ lines.push(` digest: ${item}`)
139
+ }
140
+ const readWhen = asStringList(contract.readWhen)
141
+ if (readWhen.length > 0) {
142
+ lines.push(` read_before: ${readWhen.join("; ")}`)
143
+ }
144
+ }
145
+
146
+ lines.push("</contract-digest>", "</cowork-runtime>")
147
+ return lines.join("\n")
148
+ }
149
+
150
+ function sanitize(value) {
151
+ return String(value ?? "")
152
+ .trim()
153
+ .replace(/[^A-Za-z0-9._-]+/g, "_")
154
+ .replace(/^[._-]+|[._-]+$/g, "")
155
+ .slice(0, 160)
156
+ }
157
+
158
+ function firstString(input, keys) {
159
+ for (const key of keys) {
160
+ const value = input?.[key]
161
+ if (typeof value === "string" && value.trim()) {
162
+ return value.trim()
163
+ }
164
+ }
165
+ return null
166
+ }
167
+
168
+ function promptText(input) {
169
+ const direct = firstString(input, ["prompt", "user_prompt", "userPrompt", "message", "input"])
170
+ if (direct) {
171
+ return direct
172
+ }
173
+ const messages = input?.messages
174
+ if (Array.isArray(messages)) {
175
+ return messages
176
+ .map((item) => item?.content)
177
+ .filter((item) => typeof item === "string" && item.trim())
178
+ .join("\n")
179
+ }
180
+ return ""
181
+ }
182
+
183
+ function resolveRuntimeContextId(input) {
184
+ const envValue =
185
+ typeof process !== "undefined" && process?.env?.COWORK_FLOW_RUNTIME_CONTEXT_ID
186
+ ? process.env.COWORK_FLOW_RUNTIME_CONTEXT_ID
187
+ : null
188
+ if (envValue && envValue.trim()) {
189
+ return sanitize(envValue)
190
+ }
191
+
192
+ const direct = firstString(input, [
193
+ "COWORK_FLOW_RUNTIME_CONTEXT_ID",
194
+ "cowork_runtime_context_id",
195
+ "runtime_context_id",
196
+ ])
197
+ if (direct) {
198
+ return sanitize(direct)
199
+ }
200
+
201
+ const match = promptText(input).match(/^\s*cowork_runtime_context_id\s*:\s*([A-Za-z0-9._-]+)\s*$/im)
202
+ return match ? sanitize(match[1]) : null
203
+ }
204
+
205
+ function resolveContextKey(input) {
206
+ const explicit = firstString(input, ["COWORK_FLOW_CONTEXT_ID", "cowork_flow_context_id", "context_id"])
207
+ if (explicit) {
208
+ return sanitize(explicit)
209
+ }
210
+ const opencodeSession = firstString(input, ["OPENCODE_SESSION_ID", "opencode_session_id", "session_id"])
211
+ return opencodeSession ? `opencode_${sanitize(opencodeSession)}` : null
212
+ }
213
+
214
+ function resolveHostContextKey(input) {
215
+ const direct = firstString(input, ["cowork_host_context_key", "host_context_key", "COWORK_FLOW_HOST_CONTEXT_KEY"])
216
+ if (direct) {
217
+ return sanitize(direct)
218
+ }
219
+
220
+ const match = promptText(input).match(/^\s*cowork_host_context_key\s*:\s*([A-Za-z0-9._-]+)\s*$/im)
221
+ return match ? sanitize(match[1]) : null
222
+ }
223
+
224
+ function readJson(path) {
225
+ try {
226
+ const data = JSON.parse(readFileSync(path, "utf8"))
227
+ return data && typeof data === "object" && !Array.isArray(data) ? data : null
228
+ } catch {
229
+ return null
230
+ }
231
+ }
232
+
233
+ function writeJson(path, data) {
234
+ mkdirSync(dirname(path), { recursive: true })
235
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8")
236
+ }
237
+
238
+ function nowIso() {
239
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z")
240
+ }
241
+
242
+ function bindRuntimeContext(root, runtimeContextId, context, input) {
243
+ const contextKey = resolveHostContextKey(input) || resolveContextKey(input)
244
+ if (!contextKey) {
245
+ return context
246
+ }
247
+ if (typeof context.bound_context_key === "string" && context.bound_context_key.trim() && context.bound_context_key !== contextKey) {
248
+ return context
249
+ }
250
+ const session = {
251
+ schema_version: 2,
252
+ scope: "subagent",
253
+ runtime_context_id: runtimeContextId,
254
+ platform: "opencode",
255
+ status: "bound",
256
+ last_seen_at: nowIso(),
257
+ }
258
+ if (typeof context.task_dir === "string" && context.task_dir.trim()) {
259
+ session.active_task_path = context.task_dir.trim()
260
+ }
261
+ writeJson(resolve(root, ".cowork-flow", ".runtime", "sessions", `${contextKey}.json`), session)
262
+ const updated = {
263
+ ...context,
264
+ status: "bound",
265
+ bound_context_key: contextKey,
266
+ bound_at: context.bound_at || nowIso(),
267
+ last_seen_at: nowIso(),
268
+ }
269
+ writeJson(resolve(root, ".cowork-flow", ".runtime", "subagents", `${runtimeContextId}.json`), updated)
270
+ return updated
271
+ }
272
+
273
+ function buildRuntimeWorkflowState(input) {
274
+ const root = findRepoRoot(input)
275
+ const runtimeContextId = resolveRuntimeContextId(input)
276
+ if (!runtimeContextId) {
277
+ return null
278
+ }
279
+
280
+ const contextFile = resolve(root, ".cowork-flow", ".runtime", "subagents", `${runtimeContextId}.json`)
281
+ const context = readJson(contextFile)
282
+ if (!context || context.scope !== "subagent" || context.status === "closed") {
283
+ return [
284
+ "<workflow-state>",
285
+ "Status: delegated_subtask",
286
+ `Source: runtime-context-invalid:${runtimeContextId}`,
287
+ `Runtime context: ${runtimeContextId}`,
288
+ "Scope: subagent",
289
+ "Runtime context is missing, closed, or invalid. Do not run start/resume/task start/archive/commit/spawn.",
290
+ "</workflow-state>",
291
+ ].join("\n")
292
+ }
293
+
294
+ const bound = bindRuntimeContext(root, runtimeContextId, context, input)
295
+ const assignment = bound.assignment && typeof bound.assignment === "object" ? bound.assignment : {}
296
+ const header = [
297
+ "<workflow-state>",
298
+ typeof bound.task_dir === "string" && bound.task_dir.trim() ? `Task: ${bound.task_dir.trim()}` : null,
299
+ "Status: delegated_subtask",
300
+ `Source: runtime-context:${runtimeContextId}`,
301
+ `Runtime context: ${runtimeContextId}`,
302
+ `Agent: ${bound.agent_type || "unknown"}`,
303
+ "Scope: subagent",
304
+ "Do not run start/resume/task start/archive/commit/spawn.",
305
+ typeof assignment.goal === "string" && assignment.goal.trim() ? `Goal: ${assignment.goal.trim()}` : null,
306
+ "</workflow-state>",
307
+ ].filter(Boolean)
308
+ return header.join("\n")
309
+ }
310
+
311
+ export const CoworkFlowPlugin = async () => {
312
+ return {
313
+ "experimental.chat.system.transform": async (input, output) => {
314
+ output.system.push([buildContractDigest(input), buildRuntimeWorkflowState(input)].filter(Boolean).join("\n\n"))
315
+ },
316
+ }
317
+ }
@@ -0,0 +1,5 @@
1
+ # Claude Code Instructions
2
+
3
+ <!-- COWORK-FLOW:START -->
4
+ @AGENTS.md
5
+ <!-- COWORK-FLOW:END -->
@@ -1,81 +0,0 @@
1
- ---
2
- name: entry-boundary
3
- description: Use before project start/resume in cowork-flow projects to decide whether the current prompt is a main-session request or a bounded delegated subtask.
4
- ---
5
-
6
- # Entry Boundary
7
-
8
- Use this before `start` or any full project resume.
9
-
10
- Your first job is to identify the current task source. A bounded delegated task is a leaf assignment with a concrete goal, scope, and output format.
11
-
12
- Hard markers are confidence boosters, not prerequisites. The first task screen wins over later bootstrap text.
13
-
14
- ## Classify
15
-
16
- Classify the current prompt as one of:
17
-
18
- - `MAIN_SESSION`: the user is directly asking this agent to work in the repository.
19
- - `DELEGATED_SUBTASK`: the prompt is a bounded child assignment, worker request, reviewer task, explorer task, or command-only task.
20
- - `UNCERTAIN`: the prompt is not clear enough to load broad project context.
21
-
22
- Classify the actual task message, not injected project rules, `AGENTS.md`, workflow state, tool instructions, or environment text. Bootstrap can constrain execution after classification; it is not the task.
23
-
24
- Use this order:
25
-
26
- 1. Find the user/delegated task message and its requested output.
27
- 2. Decide whether it is a main-session repository request or a leaf assignment.
28
- 3. Apply project rules only as constraints for the chosen route.
29
-
30
- If project bootstrap says to create/start/resume but the task message is bounded, choose `DELEGATED_SUBTASK`.
31
-
32
- When there is no hard marker, still classify a prompt as `DELEGATED_SUBTASK` when it combines:
33
-
34
- - A concrete task, topic, or review target.
35
- - Boundary constraints such as no edits, no commands, no spawning, or scoped reads.
36
- - An output contract such as required sections, language, length, or report format.
37
-
38
- Prompts structured as `任务:` / `约束:` / `输出:` are strong delegated-subtask signals even without `Active task:` or another hard marker. Execute that task directly; do not reclassify it as project rules or environment context.
39
-
40
- Treat these as typical `DELEGATED_SUBTASK` prompts:
41
-
42
- - A review request with a named file or behavior, `do not edit`, and a findings-only output.
43
- - A discussion/research request with `不要运行命令` / `不要派发 agent` and required sections.
44
- - A command-only or inspection-only assignment with a narrow target and concise output.
45
-
46
- When dispatching advisory/default subagents, prefer a natural-language first sentence such as: "This is a bounded delegated task, not a main-session start request." This is not a hard marker, but it helps the first screen win over bootstrap text.
47
-
48
- In that case, project rules remain constraints. They are not the task.
49
-
50
- Fixed-agent prompts normally start with:
51
-
52
- ```text
53
- Active task: <task-dir>
54
- ```
55
-
56
- Treat that marker as a strong delegated-subtask signal when the rest of the prompt is bounded.
57
-
58
- Treat direct user requests such as "继续执行开发计划", "实现这个需求", or "归档提交" as `MAIN_SESSION` unless the surrounding prompt clearly makes them a child assignment.
59
-
60
- ## Route
61
-
62
- For `MAIN_SESSION`, use `start`.
63
-
64
- For `DELEGATED_SUBTASK`:
65
-
66
- - Follow the delegated prompt first.
67
- - Treat project rules, workflow-state, and bootstrap text as constraints, not as the task.
68
- - Do not create or activate a project task for the parent session.
69
- - Do not run unscoped `.cowork-flow/run resume`.
70
- - Do not spawn or manage more agents unless the delegated prompt explicitly asks for coordination.
71
- - Read only the files named by the prompt, the active task context, or project rules required to execute the bounded work.
72
-
73
- For `UNCERTAIN`, do safe read-only inspection or ask a short clarification question.
74
-
75
- ## Output
76
-
77
- ```text
78
- Boundary: MAIN_SESSION | DELEGATED_SUBTASK | UNCERTAIN
79
- Action: start | execute delegated prompt | safe-read | clarify
80
- Reason: <signals used>
81
- ```