cowork-flow 0.0.35 → 0.0.36

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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/commands/install-zcode-plugin.js +14 -0
  3. package/template/.codex/agents/cowork-check.toml +1 -1
  4. package/template/.codex/agents/cowork-implement.toml +1 -1
  5. package/template/.cowork-flow/.version +1 -1
  6. package/template/.cowork-flow/scripts/commands/task.py +29 -13
  7. package/template/.cowork-flow/spec/runtime/rules.json +17 -125
  8. package/template/.cowork-flow/spec/schemas/rules.schema.json +2 -2
  9. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  10. package/template/.zcode/hooks/inject-context.js +32 -2
  11. package/template/.zcode/scaffold/.cowork-flow/.version +1 -1
  12. package/template/.zcode/scaffold/.cowork-flow/run +23 -4
  13. package/template/.zcode/scaffold/.cowork-flow/run.cmd +27 -4
  14. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +17 -125
  15. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +2 -2
  16. package/template/skills/check/SKILL.md +2 -2
  17. package/template/skills/game-design/SKILL.md +1 -1
  18. package/template/skills/start/SKILL.md +2 -2
  19. package/template/skills/tdd/SKILL.md +2 -2
  20. package/template/skills/writing-plans/SKILL.md +4 -4
  21. package/template/.zcode/hooks/runtime/run +0 -58
  22. package/template/.zcode/hooks/runtime/run.cmd +0 -37
  23. package/template/.zcode/hooks/runtime/scripts/__init__.py +0 -1
  24. package/template/.zcode/hooks/runtime/scripts/commands/__init__.py +0 -1
  25. package/template/.zcode/hooks/runtime/scripts/commands/_bootstrap.py +0 -10
  26. package/template/.zcode/hooks/runtime/scripts/commands/add_session.py +0 -452
  27. package/template/.zcode/hooks/runtime/scripts/commands/change.py +0 -412
  28. package/template/.zcode/hooks/runtime/scripts/commands/doctor.py +0 -471
  29. package/template/.zcode/hooks/runtime/scripts/commands/get_context.py +0 -20
  30. package/template/.zcode/hooks/runtime/scripts/commands/get_developer.py +0 -30
  31. package/template/.zcode/hooks/runtime/scripts/commands/init_developer.py +0 -55
  32. package/template/.zcode/hooks/runtime/scripts/commands/party_mode_v2.py +0 -1166
  33. package/template/.zcode/hooks/runtime/scripts/commands/resume.py +0 -62
  34. package/template/.zcode/hooks/runtime/scripts/commands/subagent.py +0 -321
  35. package/template/.zcode/hooks/runtime/scripts/commands/task.py +0 -2072
  36. package/template/.zcode/hooks/runtime/scripts/common/__init__.py +0 -79
  37. package/template/.zcode/hooks/runtime/scripts/common/core/__init__.py +0 -1
  38. package/template/.zcode/hooks/runtime/scripts/common/core/config.py +0 -242
  39. package/template/.zcode/hooks/runtime/scripts/common/core/developer.py +0 -205
  40. package/template/.zcode/hooks/runtime/scripts/common/core/execution_context.py +0 -385
  41. package/template/.zcode/hooks/runtime/scripts/common/core/files.py +0 -45
  42. package/template/.zcode/hooks/runtime/scripts/common/core/paths.py +0 -239
  43. package/template/.zcode/hooks/runtime/scripts/common/gates/__init__.py +0 -1
  44. package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +0 -55
  45. package/template/.zcode/hooks/runtime/scripts/common/gates/coding_standards.py +0 -210
  46. package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +0 -71
  47. package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +0 -378
  48. package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +0 -262
  49. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +0 -342
  50. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_implementation.py +0 -273
  51. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +0 -462
  52. package/template/.zcode/hooks/runtime/scripts/common/git/__init__.py +0 -1
  53. package/template/.zcode/hooks/runtime/scripts/common/git/git_context.py +0 -765
  54. package/template/.zcode/hooks/runtime/scripts/common/git/git_snapshot.py +0 -83
  55. package/template/.zcode/hooks/runtime/scripts/common/task/__init__.py +0 -1
  56. package/template/.zcode/hooks/runtime/scripts/common/task/active_task.py +0 -403
  57. package/template/.zcode/hooks/runtime/scripts/common/task/archive_utils.py +0 -72
  58. package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +0 -267
  59. package/template/.zcode/hooks/runtime/scripts/common/task/state_machine.py +0 -56
  60. package/template/.zcode/hooks/runtime/scripts/common/task/task_utils.py +0 -187
  61. package/template/.zcode/hooks/runtime/scripts/run.py +0 -138
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "enforcement": {
69
69
  "type": "string",
70
- "enum": ["validate_rules", "validate_implementation", "tdd_evidence", "host_contract", "metadata_only"],
71
- "description": "Runtime or contract surface responsible for enforcing or carrying this rule"
70
+ "enum": ["validate_rules", "validate_implementation"],
71
+ "description": "Runtime surface responsible for enforcing this rule. Must have a corresponding validator module."
72
72
  }
73
73
  },
74
74
  "additionalProperties": false
@@ -20,7 +20,7 @@ Use this after implementation and before `finish-work`.
20
20
 
21
21
  ## Steps
22
22
 
23
- 1. Read active task PRD, plan, and `check.jsonl`.
23
+ 1. Read active task decision-anchor.md, plan, and `check.jsonl`.
24
24
  2. Review `git diff --name-only` and `git diff`.
25
25
  3. Check contracts across caller/callee, command output, persisted state, templates, and docs.
26
26
  4. Verify spec compliance:
@@ -31,7 +31,7 @@ Use this after implementation and before `finish-work`.
31
31
  6. Review test intent: reject shallow tests that do not fail for meaningful behavior breaks.
32
32
  7. Run focused tests that would fail if the changed behavior broke.
33
33
  8. Run broader validation when the change touches shared runtime, templates, packaging, or public workflow.
34
- 9. Report `test_intent_review` with the key tests that defend PRD acceptance behavior.
34
+ 9. Report `test_intent_review` with the key tests that defend decision-anchor.md acceptance behavior.
35
35
  10. Report spec compliance: for each spec/ file checked, state pass/fail with evidence from the diff.
36
36
 
37
37
  ## Report
@@ -29,7 +29,7 @@ The following guidelines live in `.cowork-flow/spec/game/` and should be read be
29
29
  ## Usage
30
30
 
31
31
  1. Read the relevant spec/game/ guide(s) first.
32
- 2. Read the task PRD and implement.jsonl context.
32
+ 2. Read the task decision-anchor.md and implement.jsonl context.
33
33
  3. Proceed with implementation through `cowork-implement` or directly (depending on scope).
34
34
 
35
35
  ## When NOT to use
@@ -19,7 +19,7 @@ child must run the explicit shim bind before formal work.
19
19
  2. Read `.cowork-flow/workflow.md`.
20
20
  3. Read `.cowork-flow/config.yaml` for Codex dispatch hints and lifecycle hooks.
21
21
  4. Run `.cowork-flow/run resume` or `.\.cowork-flow\run.cmd resume` on Windows.
22
- 5. Read the active task PRD and JSONL indexes only when a task is active.
22
+ 5. Read the active task decision-anchor.md and JSONL indexes only when a task is active.
23
23
  6. Read relevant `.cowork-flow/spec/*/index.md` files before code changes.
24
24
 
25
25
  Report active task, workflow state, blockers, and the next phase.
@@ -32,7 +32,7 @@ state is loaded, apply the requirement clarification gate from
32
32
  `.cowork-flow/workflow.md`, then route to the next workflow phase; clear
33
33
  multi-step implementation uses `writing-plans` before fixed-agent dispatch.
34
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.
35
+ New requirements that are unclear, boundary-unclear, multi-approach, behavior-changing, or missing acceptance criteria use `brainstorming` before writing decision-anchor.md, planning, or fixed-agent dispatch. Small repository changes proceed directly only when the goal, scope boundary, and acceptance criteria are already clear.
36
36
 
37
37
  - Question-only work: answer directly.
38
38
  - Small repository change with clear goal/scope/acceptance: classify by `.cowork-flow/workflow.md`, create/start a task if required, then proceed.
@@ -9,7 +9,7 @@ Use this before editing behavior-changing code.
9
9
 
10
10
  ## Red-Green-Refactor
11
11
 
12
- 1. Map the task PRD acceptance criteria to stable IDs such as `AC-001`.
12
+ 1. Map the task decision-anchor.md acceptance criteria to stable IDs such as `AC-001`.
13
13
  2. Write the smallest meaningful failing test first.
14
14
  3. Run the red command and confirm it fails because the target behavior is missing or wrong.
15
15
  4. Implement the smallest change that makes the behavior pass.
@@ -38,7 +38,7 @@ Record TDD proof in `<task>/tdd.jsonl`. Each evidence line is one JSON object:
38
38
  }
39
39
  ```
40
40
 
41
- Every evidence record must map to a PRD `acceptanceId`. The red failure must be about the target behavior, not syntax, import, environment, fixture, or setup failure.
41
+ Every evidence record must map to a decision-anchor.md `acceptanceId`. The red failure must be about the target behavior, not syntax, import, environment, fixture, or setup failure.
42
42
 
43
43
  `testName` must resolve to the exact behavior test in `testFile`. Use one of:
44
44
  `test_method`, `ClassName.test_method`, or `module.ClassName.test_method`.
@@ -13,7 +13,7 @@ Before writing a plan, confirm the request has an executable scope, acceptance c
13
13
 
14
14
  Read:
15
15
 
16
- - Active task PRD.
16
+ - Active task decision-anchor.md.
17
17
  - Relevant change spec/design files.
18
18
  - Relevant `.cowork-flow/spec/` indexes and target specs.
19
19
  - Files that define the contracts being changed.
@@ -64,7 +64,7 @@ Start with:
64
64
  - Include a failing test before implementation when behavior can be tested.
65
65
  - Do not add shallow tests just to satisfy process. Avoid tests that only assert existence, mirror implementation details, count mocks without behavior, or snapshot empty structure.
66
66
  - For complex problems, test depth first: cover invariants, cross-layer contracts, state transitions, error boundaries, and real regression paths before narrow unit cases.
67
- - Map each behavior-changing test to a stable PRD acceptance ID and note where `tdd.jsonl` will record the red-green evidence.
67
+ - Map each behavior-changing test to a stable decision-anchor.md acceptance ID and note where `tdd.jsonl` will record the red-green evidence.
68
68
  - Avoid placeholders such as TODO, TBD, "handle edge cases", or "write tests".
69
69
  - Keep root/template parity explicit when both copies exist.
70
70
 
@@ -74,7 +74,7 @@ Start with:
74
74
  |---|---|
75
75
  | "这个 task 太小了,可以合并" | task 粒度由 acceptance criteria 决定,不由行数决定。AC 不可拆分时应合并,否则独立。 |
76
76
  | "先写个粗 plan,实现时再细化" | 粗 plan = 实现时没有约束。没有约束的实现 = 范围蔓延或返工。 |
77
- | "跳过 plan 直接开始反正有了 PRD" | PRD 定义"做什么",plan 定义"怎么做和按什么顺序做"。缺少 plan 的任务在 midway 发现依赖错误时返工成本翻倍。 |
77
+ | "跳过 plan 直接开始反正有了 decision-anchor" | decision-anchor.md 定义"做什么",plan 定义"怎么做和按什么顺序做"。缺少 plan 的任务在 midway 发现依赖错误时返工成本翻倍。 |
78
78
  | "这个 task 不需要 verify 命令" | 没有 verify = 没有完成信号。每个 task 必须有可运行的验证命令。 |
79
79
 
80
80
  ## Plan Approval Gate
@@ -110,7 +110,7 @@ Execution strategy guide:
110
110
 
111
111
  Before handoff:
112
112
 
113
- 1. Confirm every PRD acceptance criterion maps to a plan step.
113
+ 1. Confirm every decision-anchor.md acceptance criterion maps to a plan step.
114
114
  2. Search the plan for placeholders.
115
115
  3. Check names, paths, command syntax, and expected outputs.
116
116
  4. Record remaining risks or blockers.
@@ -1,58 +0,0 @@
1
- #!/bin/sh
2
- # cowork-flow run launcher (ZCode plugin edition)
3
- # Runs commands using the plugin's bundled Python runtime.
4
- set -u
5
-
6
- MIN_PYTHON_LABEL="Python 3.9+"
7
- VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)'
8
- WORKFLOW_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
9
-
10
- # Plugin cache runtime (resolved relative to this script)
11
- # Path: hooks/runtime/run -> hooks/runtime/scripts/run.py
12
- RUNTIME_SCRIPTS="$WORKFLOW_DIR/scripts"
13
-
14
- if [ ! -d "$RUNTIME_SCRIPTS" ]; then
15
- echo "Error: cowork-flow runtime not found. Expected at: $RUNTIME_SCRIPTS" >&2
16
- exit 127
17
- fi
18
-
19
- RUNNER_SCRIPT="$RUNTIME_SCRIPTS/run.py"
20
- if [ ! -f "$RUNNER_SCRIPT" ]; then
21
- echo "Error: run.py not found in runtime: $RUNTIME_SCRIPTS" >&2
22
- exit 127
23
- fi
24
-
25
- candidate_is_valid() {
26
- command -v "$1" >/dev/null 2>&1 || return 1
27
- "$1" -c "$VERSION_CHECK" >/dev/null 2>&1
28
- }
29
-
30
- select_python() {
31
- if candidate_is_valid "python3"; then
32
- SELECTED_PYTHON=python3
33
- return 0
34
- fi
35
- if candidate_is_valid "python"; then
36
- SELECTED_PYTHON=python
37
- return 0
38
- fi
39
- if candidate_is_valid "py"; then
40
- SELECTED_PYTHON=py
41
- SELECTED_PYTHON_ARG=-3
42
- return 0
43
- fi
44
- echo "Error: $MIN_PYTHON_LABEL required but not found." >&2
45
- exit 127
46
- }
47
-
48
- select_python
49
-
50
- SELECTED_PYTHON_ARG=${SELECTED_PYTHON_ARG:-}
51
-
52
- if [ -n "$SELECTED_PYTHON_ARG" ]; then
53
- export PYTHONPATH="$RUNTIME_SCRIPTS${PYTHONPATH:+:$PYTHONPATH}"
54
- exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$RUNNER_SCRIPT" "$@"
55
- fi
56
-
57
- export PYTHONPATH="$RUNTIME_SCRIPTS${PYTHONPATH:+:$PYTHONPATH}"
58
- exec "$SELECTED_PYTHON" "$RUNNER_SCRIPT" "$@"
@@ -1,37 +0,0 @@
1
- @echo off
2
- setlocal EnableExtensions
3
-
4
- set "MIN_PYTHON_LABEL=Python 3.9+"
5
- set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)"
6
- set "WORKFLOW_DIR=%~dp0"
7
-
8
- set "RUNTIME_SCRIPTS=%WORKFLOW_DIR%scripts"
9
-
10
- if not exist "%RUNTIME_SCRIPTS%\run.py" (
11
- echo Error: cowork-flow runtime not found. >&2
12
- exit /b 127
13
- )
14
-
15
- where python3 >nul 2>nul
16
- if not errorlevel 1 (
17
- set "PYTHONPATH=%RUNTIME_SCRIPTS%;%PYTHONPATH%"
18
- python3 "%RUNTIME_SCRIPTS%\run.py" %*
19
- exit /b %ERRORLEVEL%
20
- )
21
-
22
- where python >nul 2>nul
23
- if not errorlevel 1 (
24
- set "PYTHONPATH=%RUNTIME_SCRIPTS%;%PYTHONPATH%"
25
- python "%RUNTIME_SCRIPTS%\run.py" %*
26
- exit /b %ERRORLEVEL%
27
- )
28
-
29
- where py >nul 2>nul
30
- if not errorlevel 1 (
31
- set "PYTHONPATH=%RUNTIME_SCRIPTS%;%PYTHONPATH%"
32
- py -3 "%RUNTIME_SCRIPTS%\run.py" %*
33
- exit /b %ERRORLEVEL%
34
- )
35
-
36
- echo Error: %MIN_PYTHON_LABEL% required but not found. >&2
37
- exit /b 127
@@ -1 +0,0 @@
1
- """Cowork Flow zcode plugin runtime scripts."""
@@ -1 +0,0 @@
1
- """Command entrypoints for cowork-flow."""
@@ -1,10 +0,0 @@
1
- """Allow command scripts to import the sibling common package when run directly."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from pathlib import Path
7
-
8
- SCRIPTS_DIR = Path(__file__).resolve().parents[1]
9
- if str(SCRIPTS_DIR) not in sys.path:
10
- sys.path.insert(0, str(SCRIPTS_DIR))
@@ -1,452 +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 common.core.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 common.core.developer import ensure_developer
35
- from common.core.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 update_index(
200
- index_file: Path,
201
- dev_dir: Path,
202
- title: str,
203
- commit: str,
204
- new_session: int,
205
- active_file: str,
206
- today: str
207
- ) -> bool:
208
- """Update index.md with new session info."""
209
- commit_display = _format_commit_display(commit)
210
-
211
- # Get file number from active_file name
212
- match = re.search(r"(\d+)", active_file)
213
- active_num = int(match.group(1)) if match else 0
214
- files_table = count_journal_files(dev_dir, active_num)
215
-
216
- print(f"Updating index.md (session {new_session})...", file=sys.stderr)
217
- print(f" Title: {title}", file=sys.stderr)
218
- print(f" Commit: {commit_display}", file=sys.stderr)
219
- print(f" Current file: {active_file}", file=sys.stderr)
220
- print("", file=sys.stderr)
221
-
222
- content = index_file.read_text(encoding="utf-8")
223
-
224
- if MARKER_CURRENT_STATUS_START not in content:
225
- print("Error: index.md is missing auto-update markers. Please restore the template first.", file=sys.stderr)
226
- return False
227
-
228
- # Process sections
229
- lines = content.splitlines()
230
- new_lines = []
231
-
232
- in_current_status = False
233
- in_active_documents = False
234
- in_session_history = False
235
- header_written = False
236
-
237
- for line in lines:
238
- if MARKER_CURRENT_STATUS_START in line:
239
- new_lines.append(line)
240
- in_current_status = True
241
- new_lines.append(f"- **Current file**: `{active_file}`")
242
- new_lines.append(f"- **Total Sessions**: {new_session}")
243
- new_lines.append(f"- **Last Active**: {today}")
244
- continue
245
-
246
- if MARKER_CURRENT_STATUS_END in line:
247
- in_current_status = False
248
- new_lines.append(line)
249
- continue
250
-
251
- if MARKER_ACTIVE_DOCUMENTS_START in line:
252
- new_lines.append(line)
253
- in_active_documents = True
254
- new_lines.append("| File | Lines | Status |")
255
- new_lines.append("|------|------|------|")
256
- new_lines.append(files_table)
257
- continue
258
-
259
- if MARKER_ACTIVE_DOCUMENTS_END in line:
260
- in_active_documents = False
261
- new_lines.append(line)
262
- continue
263
-
264
- if MARKER_SESSION_HISTORY_START in line:
265
- new_lines.append(line)
266
- in_session_history = True
267
- header_written = False
268
- continue
269
-
270
- if MARKER_SESSION_HISTORY_END in line:
271
- in_session_history = False
272
- new_lines.append(line)
273
- continue
274
-
275
- if in_current_status:
276
- continue
277
-
278
- if in_active_documents:
279
- continue
280
-
281
- if in_session_history:
282
- new_lines.append(line)
283
- if re.match(r"^\|\s*-", line) and not header_written:
284
- new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} |")
285
- header_written = True
286
- continue
287
-
288
- new_lines.append(line)
289
-
290
- index_file.write_text("\n".join(new_lines), encoding="utf-8")
291
- print("[OK] index.md updated.", file=sys.stderr)
292
- return True
293
-
294
-
295
- # =============================================================================
296
- # Main Function
297
- # =============================================================================
298
-
299
- def _auto_commit_workspace(repo_root: Path) -> None:
300
- """Stage .cowork-flow/workspace and .cowork-flow/tasks, then commit with a configured message."""
301
- commit_msg = get_session_commit_message(repo_root)
302
- workspace_rel = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}"
303
- tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
304
- subprocess.run(
305
- ["git", "add", "-A", workspace_rel, tasks_rel],
306
- cwd=repo_root,
307
- capture_output=True,
308
- )
309
- # Check if there are staged changes
310
- result = subprocess.run(
311
- ["git", "diff", "--cached", "--quiet", "--", workspace_rel, tasks_rel],
312
- cwd=repo_root,
313
- )
314
- if result.returncode == 0:
315
- print("[OK] No workspace metadata changes to commit.", file=sys.stderr)
316
- return
317
- commit_result = subprocess.run(
318
- ["git", "commit", "-m", commit_msg],
319
- cwd=repo_root,
320
- capture_output=True,
321
- text=True,
322
- )
323
- if commit_result.returncode == 0:
324
- print(f"[OK] Metadata auto-committed: {commit_msg}", file=sys.stderr)
325
- else:
326
- print(f"[WARN] Metadata auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
327
-
328
-
329
- def add_session(
330
- title: str,
331
- commit: str = "-",
332
- summary: str = "(add summary)",
333
- extra_content: str = "(add details)",
334
- auto_commit: bool = False,
335
- ) -> int:
336
- """Add a new session."""
337
- repo_root = get_repo_root()
338
- ensure_developer(repo_root)
339
-
340
- developer = get_developer(repo_root)
341
- if not developer:
342
- print("Error: developer identity has not been initialized.", file=sys.stderr)
343
- return 1
344
-
345
- dev_dir = get_workspace_dir(repo_root)
346
- if not dev_dir:
347
- print("Error: workspace directory not found.", file=sys.stderr)
348
- return 1
349
-
350
- max_lines = get_max_journal_lines(repo_root)
351
-
352
- index_file = dev_dir / "index.md"
353
- today = datetime.now().strftime("%Y-%m-%d")
354
-
355
- journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
356
- current_session = get_current_session(index_file)
357
- new_session = current_session + 1
358
-
359
- session_content = generate_session_content(
360
- new_session, title, commit, summary, extra_content, today
361
- )
362
- content_lines = len(session_content.splitlines())
363
-
364
- print("========================================", file=sys.stderr)
365
- print("Add Session Record", file=sys.stderr)
366
- print("========================================", file=sys.stderr)
367
- print("", file=sys.stderr)
368
- print(f"Session: {new_session}", file=sys.stderr)
369
- print(f"Title: {title}", file=sys.stderr)
370
- print(f"Commit: {commit}", file=sys.stderr)
371
- print("", file=sys.stderr)
372
- print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
373
- print(f"Current lines: {current_lines}", file=sys.stderr)
374
- print(f"New content lines: {content_lines}", file=sys.stderr)
375
- print(f"Total lines after append: {current_lines + content_lines}", file=sys.stderr)
376
- print("", file=sys.stderr)
377
-
378
- target_file = journal_file
379
- target_num = current_num
380
-
381
- if current_lines + content_lines > max_lines:
382
- target_num = current_num + 1
383
- print(f"[!] Over {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
384
- target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines)
385
- print(f"Created: {target_file}", file=sys.stderr)
386
-
387
- # Append session content
388
- if target_file:
389
- with target_file.open("a", encoding="utf-8") as f:
390
- f.write(session_content)
391
- print(f"[OK] Appended session to {target_file.name}", file=sys.stderr)
392
-
393
- print("", file=sys.stderr)
394
-
395
- # Update index.md
396
- active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
397
- if not update_index(index_file, dev_dir, title, commit, new_session, active_file, today):
398
- return 1
399
-
400
- print("", file=sys.stderr)
401
- print("========================================", file=sys.stderr)
402
- print(f"[OK] Session {new_session} recorded successfully.", file=sys.stderr)
403
- print("========================================", file=sys.stderr)
404
- print("", file=sys.stderr)
405
- print("Updated files:", file=sys.stderr)
406
- print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
407
- print(" - index.md", file=sys.stderr)
408
-
409
- # Auto-commit workspace changes only when explicitly requested.
410
- if auto_commit:
411
- print("", file=sys.stderr)
412
- _auto_commit_workspace(repo_root)
413
-
414
- return 0
415
-
416
-
417
- # =============================================================================
418
- # Main Entry
419
- # =============================================================================
420
-
421
- def main() -> int:
422
- """CLI entry point."""
423
- parser = argparse.ArgumentParser(
424
- description="Append a session record to the journal and update index.md"
425
- )
426
- parser.add_argument("--title", required=True, help="Session title")
427
- parser.add_argument("--commit", default="-", help="Comma-separated commit hashes")
428
- parser.add_argument("--summary", default="(add summary)", help="Brief summary")
429
- parser.add_argument("--content-file", help="Path to a file containing detailed content")
430
- parser.add_argument(
431
- "--auto-commit",
432
- action="store_true",
433
- help="Auto git commit after recording workspace metadata",
434
- )
435
- args = parser.parse_args()
436
-
437
- extra_content = "(add details)"
438
- if args.content_file:
439
- content_path = Path(args.content_file)
440
- if content_path.is_file():
441
- extra_content = content_path.read_text(encoding="utf-8")
442
- elif not sys.stdin.isatty():
443
- extra_content = sys.stdin.read()
444
-
445
- return add_session(
446
- args.title, args.commit, args.summary, extra_content,
447
- auto_commit=args.auto_commit,
448
- )
449
-
450
-
451
- if __name__ == "__main__":
452
- sys.exit(main())