cowork-flow 0.0.47 → 0.0.49

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 (90) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +102 -23
  3. package/package.json +8 -3
  4. package/src/cli.js +6 -0
  5. package/src/commands/init.js +0 -83
  6. package/src/commands/source-refresh.js +40 -0
  7. package/src/commands/sync.js +6 -0
  8. package/src/commands/update.js +59 -6
  9. package/src/lib/copy-template.js +200 -50
  10. package/src/lib/host-assets.js +367 -64
  11. package/src/lib/plan-applier.js +47 -0
  12. package/template/.claude/hooks/inject-workflow-state.py +7 -0
  13. package/template/.codex/hooks/inject-workflow-state.py +7 -0
  14. package/template/.codex/hooks.json +1 -1
  15. package/template/.cowork-flow/.version +1 -1
  16. package/template/.cowork-flow/config.yaml +0 -11
  17. package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
  18. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
  19. package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
  20. package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
  21. package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
  22. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
  23. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
  24. package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
  25. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
  26. package/template/.cowork-flow/scripts/infra/config.py +0 -17
  27. package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
  28. package/template/.cowork-flow/scripts/infra/paths.py +0 -79
  29. package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
  30. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
  31. package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
  32. package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
  33. package/template/.cowork-flow/scripts/run.py +0 -5
  34. package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
  35. package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
  36. package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
  37. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
  38. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
  39. package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
  40. package/template/.cowork-flow/scripts/services/readiness.py +3 -170
  41. package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
  42. package/template/.cowork-flow/scripts/services/task_context.py +103 -365
  43. package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
  44. package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
  45. package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
  46. package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
  47. package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
  48. package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
  49. package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
  50. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
  51. package/template/.cowork-flow/spec/contracts/index.md +1 -0
  52. package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
  53. package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
  54. package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
  55. package/template/.cowork-flow/spec/guides/index.md +3 -1
  56. package/template/.cowork-flow/spec/index.md +5 -4
  57. package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
  58. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  59. package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
  60. package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
  61. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  62. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
  63. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  64. package/template/skills/batch-execution/SKILL.md +37 -3
  65. package/template/skills/batch-execution/scripts/batch_action.py +4 -0
  66. package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
  67. package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
  68. package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
  69. package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
  70. package/template/skills/brainstorming/SKILL.md +8 -7
  71. package/template/skills/cowork-flow/SKILL.md +3 -0
  72. package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
  73. package/template/skills/party-mode/scripts/party_actions.py +95 -0
  74. package/template/skills/party-mode/scripts/party_board_store.py +148 -0
  75. package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
  76. package/template/skills/party-mode/scripts/party_reports.py +177 -0
  77. package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
  78. package/template/skills/runtime-health/SKILL.md +14 -2
  79. package/template/skills/runtime-health/scripts/doctor.py +278 -41
  80. package/template/skills/task-planning/SKILL.md +105 -61
  81. package/template/skills/task-review/SKILL.md +4 -4
  82. package/template/skills/task-review/scripts/review_check.py +277 -20
  83. package/template/skills/test-first/SKILL.md +18 -3
  84. package/template/skills/test-first/manifest.json +8 -4
  85. package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
  86. package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
  87. package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
  88. package/template/.cowork-flow/workspace/index.md +0 -102
  89. package/template/.zcode/scaffold/AGENTS.md +0 -145
  90. package/template/.zcode/scaffold/CLAUDE.md +0 -5
@@ -1,465 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Add a new session to journal file and update index.md.
5
-
6
- Usage:
7
- ./.cowork-flow/run add-session --title "Title" --commit "hash" --summary "Summary"
8
- echo "content" | ./.cowork-flow/run add-session --title "Title" --commit "hash"
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import argparse
14
- import re
15
- import subprocess
16
- import sys
17
- from datetime import datetime
18
- from pathlib import Path
19
-
20
- if __package__:
21
- from . import _bootstrap as _bootstrap # noqa: F401
22
- else:
23
- import _bootstrap # noqa: F401
24
- from infra.paths import (
25
- DIR_TASKS,
26
- DIR_WORKFLOW,
27
- DIR_WORKSPACE,
28
- FILE_JOURNAL_PREFIX,
29
- count_lines,
30
- get_repo_root,
31
- get_developer,
32
- get_workspace_dir,
33
- )
34
- from infra.developer_profile import ensure_developer
35
- from infra.config import get_session_commit_message, get_max_journal_lines
36
-
37
- MARKER_CURRENT_STATUS_START = "@@@auto:current-status"
38
- MARKER_CURRENT_STATUS_END = "@@@/auto:current-status"
39
- MARKER_ACTIVE_DOCUMENTS_START = "@@@auto:active-documents"
40
- MARKER_ACTIVE_DOCUMENTS_END = "@@@/auto:active-documents"
41
- MARKER_SESSION_HISTORY_START = "@@@auto:session-history"
42
- MARKER_SESSION_HISTORY_END = "@@@/auto:session-history"
43
-
44
-
45
- # =============================================================================
46
- # Helper Functions
47
- # =============================================================================
48
-
49
- def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]:
50
- """Get latest journal file info.
51
-
52
- Returns:
53
- Tuple of (file_path, file_number, line_count).
54
- """
55
- latest_file: Path | None = None
56
- latest_num = -1
57
-
58
- for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
59
- if not f.is_file():
60
- continue
61
-
62
- match = re.search(r"(\d+)$", f.stem)
63
- if match:
64
- num = int(match.group(1))
65
- if num > latest_num:
66
- latest_num = num
67
- latest_file = f
68
-
69
- if latest_file:
70
- lines = count_lines(latest_file)
71
- return latest_file, latest_num, lines
72
-
73
- return None, 0, 0
74
-
75
-
76
- def get_current_session(index_file: Path) -> int:
77
- """Get current session number from index.md."""
78
- if not index_file.is_file():
79
- return 0
80
-
81
- content = index_file.read_text(encoding="utf-8")
82
- for line in content.splitlines():
83
- if "Total Sessions" in line or "\u603b\u4f1a\u8bdd\u6570" in line:
84
- match = re.search(r"[:\uFF1A]\s*(\d+)", line)
85
- if match:
86
- return int(match.group(1))
87
- return 0
88
-
89
-
90
- def _extract_journal_num(filename: str) -> int:
91
- """Extract journal number from filename for sorting."""
92
- match = re.search(r"(\d+)", filename)
93
- return int(match.group(1)) if match else 0
94
-
95
-
96
- def _format_commit_display(commit: str) -> str:
97
- """Format commit hashes for display in index.md."""
98
- if not commit or commit == "-":
99
- return "-"
100
- return re.sub(r"([a-f0-9]{7,})", r"`\1`", commit.replace(",", ", "))
101
-
102
-
103
- def _format_commit_table(commit: str) -> str:
104
- """Format commit hashes for a session entry."""
105
- if not commit or commit == "-":
106
- return "(no code commit; planning or sync session)"
107
-
108
- commit_table = """| Hash | Note |
109
- |------|------|"""
110
- for c in commit.split(","):
111
- c = c.strip()
112
- commit_table += f"\n| `{c}` | See git log |"
113
- return commit_table
114
-
115
-
116
- def count_journal_files(dev_dir: Path, active_num: int) -> str:
117
- """Count journal files and return table rows."""
118
- active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md"
119
- result_lines = []
120
-
121
- files = sorted(
122
- [f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()],
123
- key=lambda f: _extract_journal_num(f.stem),
124
- reverse=True
125
- )
126
-
127
- for f in files:
128
- filename = f.name
129
- lines = count_lines(f)
130
- status = "Current" if filename == active_file else "Archived"
131
- result_lines.append(f"| `{filename}` | ~{lines} | {status} |")
132
-
133
- return "\n".join(result_lines)
134
-
135
-
136
- def create_new_journal_file(
137
- dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
138
- ) -> Path:
139
- """Create a new journal file."""
140
- prev_num = num - 1
141
- new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
142
-
143
- content = f"""# Development Journal - {developer} (Part {num})
144
-
145
- > Continued from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived after about {max_lines} lines)
146
- > Start date: {today}
147
-
148
- ---
149
-
150
- """
151
- new_file.write_text(content, encoding="utf-8")
152
- return new_file
153
-
154
-
155
- def generate_session_content(
156
- session_num: int,
157
- title: str,
158
- commit: str,
159
- summary: str,
160
- extra_content: str,
161
- today: str
162
- ) -> str:
163
- """Generate session content."""
164
- commit_table = _format_commit_table(commit)
165
-
166
- return f"""
167
-
168
- ## Session {session_num}: {title}
169
-
170
- **Date**: {today}
171
- **Task**: {title}
172
-
173
- ### Summary
174
-
175
- {summary}
176
-
177
- ### Main Changes
178
-
179
- {extra_content}
180
-
181
- ### Git Commit
182
-
183
- {commit_table}
184
-
185
- ### Verification
186
-
187
- - [OK] (add verification results)
188
-
189
- ### Status
190
-
191
- [OK] **Completed**
192
-
193
- ### Follow-up Actions
194
-
195
- - None, active task is complete
196
- """
197
-
198
-
199
- def _replace_index_section(
200
- lines: list[str],
201
- start_marker: str,
202
- end_marker: str,
203
- replacement: list[str],
204
- ) -> list[str]:
205
- rendered: list[str] = []
206
- inside = False
207
- for line in lines:
208
- if start_marker in line:
209
- rendered.extend((line, *replacement))
210
- inside = True
211
- elif end_marker in line:
212
- rendered.append(line)
213
- inside = False
214
- elif not inside:
215
- rendered.append(line)
216
- return rendered
217
-
218
-
219
- def _insert_session_history(lines: list[str], row: str) -> list[str]:
220
- rendered: list[str] = []
221
- inside = False
222
- inserted = False
223
- for line in lines:
224
- if MARKER_SESSION_HISTORY_START in line:
225
- inside = True
226
- elif MARKER_SESSION_HISTORY_END in line:
227
- inside = False
228
- rendered.append(line)
229
- if inside and re.match(r"^\|\s*-", line) and not inserted:
230
- rendered.append(row)
231
- inserted = True
232
- return rendered
233
-
234
-
235
- def update_index(
236
- index_file: Path,
237
- dev_dir: Path,
238
- title: str,
239
- commit: str,
240
- new_session: int,
241
- active_file: str,
242
- today: str
243
- ) -> bool:
244
- """Update index.md with new session info."""
245
- commit_display = _format_commit_display(commit)
246
- match = re.search(r"(\d+)", active_file)
247
- active_num = int(match.group(1)) if match else 0
248
- files_table = count_journal_files(dev_dir, active_num)
249
-
250
- print(f"Updating index.md (session {new_session})...", file=sys.stderr)
251
- print(f" Title: {title}", file=sys.stderr)
252
- print(f" Commit: {commit_display}", file=sys.stderr)
253
- print(f" Current file: {active_file}", file=sys.stderr)
254
- print("", file=sys.stderr)
255
-
256
- content = index_file.read_text(encoding="utf-8")
257
-
258
- if MARKER_CURRENT_STATUS_START not in content:
259
- print("Error: index.md is missing auto-update markers. Please restore the template first.", file=sys.stderr)
260
- return False
261
-
262
- lines = content.splitlines()
263
- lines = _replace_index_section(
264
- lines,
265
- MARKER_CURRENT_STATUS_START,
266
- MARKER_CURRENT_STATUS_END,
267
- [
268
- f"- **Current file**: `{active_file}`",
269
- f"- **Total Sessions**: {new_session}",
270
- f"- **Last Active**: {today}",
271
- ],
272
- )
273
- lines = _replace_index_section(
274
- lines,
275
- MARKER_ACTIVE_DOCUMENTS_START,
276
- MARKER_ACTIVE_DOCUMENTS_END,
277
- ["| File | Lines | Status |", "|------|------|------|", files_table],
278
- )
279
- row = f"| {new_session} | {today} | {title} | {commit_display} |"
280
- lines = _insert_session_history(lines, row)
281
- index_file.write_text("\n".join(lines), encoding="utf-8")
282
- print("[OK] index.md updated.", file=sys.stderr)
283
- return True
284
-
285
-
286
- # =============================================================================
287
- # Main Function
288
- # =============================================================================
289
-
290
- def _auto_commit_workspace(repo_root: Path) -> None:
291
- """Stage .cowork-flow/workspace and .cowork-flow/tasks, then commit with a configured message."""
292
- commit_msg = get_session_commit_message(repo_root)
293
- workspace_rel = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}"
294
- tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
295
- subprocess.run(
296
- ["git", "add", "-A", workspace_rel, tasks_rel],
297
- cwd=repo_root,
298
- capture_output=True,
299
- )
300
- # Check if there are staged changes
301
- result = subprocess.run(
302
- ["git", "diff", "--cached", "--quiet", "--", workspace_rel, tasks_rel],
303
- cwd=repo_root,
304
- )
305
- if result.returncode == 0:
306
- print("[OK] No workspace metadata changes to commit.", file=sys.stderr)
307
- return
308
- commit_result = subprocess.run(
309
- ["git", "commit", "-m", commit_msg],
310
- cwd=repo_root,
311
- capture_output=True,
312
- text=True,
313
- )
314
- if commit_result.returncode == 0:
315
- print(f"[OK] Metadata auto-committed: {commit_msg}", file=sys.stderr)
316
- else:
317
- print(f"[WARN] Metadata auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
318
-
319
-
320
- def _session_workspace(repo_root: Path):
321
- developer = get_developer(repo_root)
322
- if not developer:
323
- print("Error: developer identity has not been initialized.", file=sys.stderr)
324
- return None
325
- dev_dir = get_workspace_dir(repo_root)
326
- if dev_dir is None or not dev_dir.is_dir():
327
- print("Error: workspace directory not found.", file=sys.stderr)
328
- return None
329
- index_file = dev_dir / "index.md"
330
- journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
331
- if not index_file.is_file():
332
- print(f"Error: workspace index is missing: {index_file}", file=sys.stderr)
333
- return None
334
- if journal_file is None:
335
- print(f"Error: workspace journal is missing: {dev_dir}", file=sys.stderr)
336
- return None
337
- return developer, dev_dir, index_file, journal_file, current_num, current_lines
338
-
339
-
340
- def _print_session_preview(
341
- new_session: int,
342
- title: str,
343
- commit: str,
344
- current_num: int,
345
- current_lines: int,
346
- content_lines: int,
347
- ) -> None:
348
- print("=" * 40, "Add Session Record", "=" * 40, sep="\n", file=sys.stderr)
349
- print(f"\nSession: {new_session}", file=sys.stderr)
350
- print(f"Title: {title}", file=sys.stderr)
351
- print(f"Commit: {commit}\n", file=sys.stderr)
352
- print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
353
- print(f"Current lines: {current_lines}", file=sys.stderr)
354
- print(f"New content lines: {content_lines}", file=sys.stderr)
355
- print(f"Total lines after append: {current_lines + content_lines}\n", file=sys.stderr)
356
-
357
-
358
- def _session_target(
359
- journal_file: Path,
360
- current_num: int,
361
- current_lines: int,
362
- content_lines: int,
363
- max_lines: int,
364
- dev_dir: Path,
365
- developer: str,
366
- today: str,
367
- ) -> tuple[Path, int]:
368
- if current_lines + content_lines <= max_lines:
369
- return journal_file, current_num
370
- target_num = current_num + 1
371
- print(f"[!] Over {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
372
- target_file = create_new_journal_file(
373
- dev_dir, target_num, developer, today, max_lines
374
- )
375
- print(f"Created: {target_file}", file=sys.stderr)
376
- return target_file, target_num
377
-
378
-
379
- def _print_session_success(new_session: int, target_file: Path) -> None:
380
- print("\n" + "=" * 40, file=sys.stderr)
381
- print(f"[OK] Session {new_session} recorded successfully.", file=sys.stderr)
382
- print("=" * 40 + "\n", file=sys.stderr)
383
- print("Updated files:", file=sys.stderr)
384
- print(f" - {target_file.name}", file=sys.stderr)
385
- print(" - index.md", file=sys.stderr)
386
-
387
-
388
- def add_session(
389
- title: str,
390
- commit: str = "-",
391
- summary: str = "(add summary)",
392
- extra_content: str = "(add details)",
393
- auto_commit: bool = False,
394
- ) -> int:
395
- """Add a new session."""
396
- repo_root = get_repo_root()
397
- ensure_developer(repo_root)
398
- workspace = _session_workspace(repo_root)
399
- if workspace is None:
400
- return 1
401
- developer, dev_dir, index_file, journal_file, current_num, current_lines = workspace
402
- max_lines = get_max_journal_lines(repo_root)
403
- today = datetime.now().strftime("%Y-%m-%d")
404
- new_session = get_current_session(index_file) + 1
405
- session_content = generate_session_content(
406
- new_session, title, commit, summary, extra_content, today
407
- )
408
- content_lines = len(session_content.splitlines())
409
- _print_session_preview(
410
- new_session, title, commit, current_num, current_lines, content_lines
411
- )
412
- target_file, target_num = _session_target(
413
- journal_file, current_num, current_lines, content_lines,
414
- max_lines, dev_dir, developer, today,
415
- )
416
- with target_file.open("a", encoding="utf-8") as stream:
417
- stream.write(session_content)
418
- print(f"[OK] Appended session to {target_file.name}\n", file=sys.stderr)
419
- active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
420
- if not update_index(index_file, dev_dir, title, commit, new_session, active_file, today):
421
- return 1
422
- _print_session_success(new_session, target_file)
423
- if auto_commit:
424
- print("", file=sys.stderr)
425
- _auto_commit_workspace(repo_root)
426
-
427
- return 0
428
-
429
-
430
- # =============================================================================
431
- # Main Entry
432
- # =============================================================================
433
-
434
- def main() -> int:
435
- """CLI entry point."""
436
- parser = argparse.ArgumentParser(
437
- description="Append a session record to the journal and update index.md"
438
- )
439
- parser.add_argument("--title", required=True, help="Session title")
440
- parser.add_argument("--commit", default="-", help="Comma-separated commit hashes")
441
- parser.add_argument("--summary", default="(add summary)", help="Brief summary")
442
- parser.add_argument("--content-file", help="Path to a file containing detailed content")
443
- parser.add_argument(
444
- "--auto-commit",
445
- action="store_true",
446
- help="Auto git commit after recording workspace metadata",
447
- )
448
- args = parser.parse_args()
449
-
450
- extra_content = "(add details)"
451
- if args.content_file:
452
- content_path = Path(args.content_file)
453
- if content_path.is_file():
454
- extra_content = content_path.read_text(encoding="utf-8")
455
- elif not sys.stdin.isatty():
456
- extra_content = sys.stdin.read()
457
-
458
- return add_session(
459
- args.title, args.commit, args.summary, extra_content,
460
- auto_commit=args.auto_commit,
461
- )
462
-
463
-
464
- if __name__ == "__main__":
465
- sys.exit(main())