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.
- package/package.json +1 -1
- package/src/commands/install-zcode-plugin.js +14 -0
- package/template/.codex/agents/cowork-check.toml +1 -1
- package/template/.codex/agents/cowork-implement.toml +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/commands/task.py +29 -13
- package/template/.cowork-flow/spec/runtime/rules.json +17 -125
- package/template/.cowork-flow/spec/schemas/rules.schema.json +2 -2
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +32 -2
- package/template/.zcode/scaffold/.cowork-flow/.version +1 -1
- package/template/.zcode/scaffold/.cowork-flow/run +23 -4
- package/template/.zcode/scaffold/.cowork-flow/run.cmd +27 -4
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +17 -125
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +2 -2
- package/template/skills/check/SKILL.md +2 -2
- package/template/skills/game-design/SKILL.md +1 -1
- package/template/skills/start/SKILL.md +2 -2
- package/template/skills/tdd/SKILL.md +2 -2
- package/template/skills/writing-plans/SKILL.md +4 -4
- package/template/.zcode/hooks/runtime/run +0 -58
- package/template/.zcode/hooks/runtime/run.cmd +0 -37
- package/template/.zcode/hooks/runtime/scripts/__init__.py +0 -1
- package/template/.zcode/hooks/runtime/scripts/commands/__init__.py +0 -1
- package/template/.zcode/hooks/runtime/scripts/commands/_bootstrap.py +0 -10
- package/template/.zcode/hooks/runtime/scripts/commands/add_session.py +0 -452
- package/template/.zcode/hooks/runtime/scripts/commands/change.py +0 -412
- package/template/.zcode/hooks/runtime/scripts/commands/doctor.py +0 -471
- package/template/.zcode/hooks/runtime/scripts/commands/get_context.py +0 -20
- package/template/.zcode/hooks/runtime/scripts/commands/get_developer.py +0 -30
- package/template/.zcode/hooks/runtime/scripts/commands/init_developer.py +0 -55
- package/template/.zcode/hooks/runtime/scripts/commands/party_mode_v2.py +0 -1166
- package/template/.zcode/hooks/runtime/scripts/commands/resume.py +0 -62
- package/template/.zcode/hooks/runtime/scripts/commands/subagent.py +0 -321
- package/template/.zcode/hooks/runtime/scripts/commands/task.py +0 -2072
- package/template/.zcode/hooks/runtime/scripts/common/__init__.py +0 -79
- package/template/.zcode/hooks/runtime/scripts/common/core/__init__.py +0 -1
- package/template/.zcode/hooks/runtime/scripts/common/core/config.py +0 -242
- package/template/.zcode/hooks/runtime/scripts/common/core/developer.py +0 -205
- package/template/.zcode/hooks/runtime/scripts/common/core/execution_context.py +0 -385
- package/template/.zcode/hooks/runtime/scripts/common/core/files.py +0 -45
- package/template/.zcode/hooks/runtime/scripts/common/core/paths.py +0 -239
- package/template/.zcode/hooks/runtime/scripts/common/gates/__init__.py +0 -1
- package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +0 -55
- package/template/.zcode/hooks/runtime/scripts/common/gates/coding_standards.py +0 -210
- package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +0 -71
- package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +0 -378
- package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +0 -262
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +0 -342
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_implementation.py +0 -273
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +0 -462
- package/template/.zcode/hooks/runtime/scripts/common/git/__init__.py +0 -1
- package/template/.zcode/hooks/runtime/scripts/common/git/git_context.py +0 -765
- package/template/.zcode/hooks/runtime/scripts/common/git/git_snapshot.py +0 -83
- package/template/.zcode/hooks/runtime/scripts/common/task/__init__.py +0 -1
- package/template/.zcode/hooks/runtime/scripts/common/task/active_task.py +0 -403
- package/template/.zcode/hooks/runtime/scripts/common/task/archive_utils.py +0 -72
- package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +0 -267
- package/template/.zcode/hooks/runtime/scripts/common/task/state_machine.py +0 -56
- package/template/.zcode/hooks/runtime/scripts/common/task/task_utils.py +0 -187
- 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"
|
|
71
|
-
"description": "Runtime
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 直接开始反正有了
|
|
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
|
|
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())
|