cowork-flow 0.0.39 → 0.0.40
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/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/task_archive.py +148 -21
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +3 -1
- package/template/.cowork-flow/scripts/commands/add_session.py +137 -124
- package/template/.cowork-flow/scripts/commands/task.py +1 -0
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +135 -49
- package/template/.cowork-flow/scripts/commands/task_navigation.py +145 -81
- package/template/.cowork-flow/scripts/commands/task_parser.py +1 -0
- package/template/.cowork-flow/scripts/common/core/paths.py +4 -1
- package/template/.cowork-flow/scripts/common/gates/gates.py +114 -108
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +50 -28
- package/template/.cowork-flow/scripts/common/git/git_snapshot.py +14 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +1 -1
- package/template/.cowork-flow/workflow.md +6 -2
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +6 -2
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.40
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from datetime import datetime
|
|
8
10
|
from pathlib import Path
|
|
@@ -15,6 +17,7 @@ from common.task.task_utils import find_task_by_name
|
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
DONE_STATUSES = ("completed", "done")
|
|
20
|
+
ArchiveFinalizer = Callable[[], bool]
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
class TaskArchiveError(RuntimeError):
|
|
@@ -53,7 +56,38 @@ class TaskArchiveService:
|
|
|
53
56
|
task: str | Path,
|
|
54
57
|
*,
|
|
55
58
|
archived_at: str | None = None,
|
|
59
|
+
finalize: ArchiveFinalizer | None = None,
|
|
56
60
|
) -> TaskArchiveResult:
|
|
61
|
+
task_dir, task_data = self._load_completed_task(task)
|
|
62
|
+
archive_date = archived_at or datetime.now().strftime("%Y-%m-%d")
|
|
63
|
+
destination = self._archive_destination(task_dir, archive_date)
|
|
64
|
+
relationship_updates = self._relationship_updates(
|
|
65
|
+
task_dir.name,
|
|
66
|
+
task_data,
|
|
67
|
+
)
|
|
68
|
+
context_snapshots = self._context_snapshots(task_dir)
|
|
69
|
+
self._move_task(task_dir, destination)
|
|
70
|
+
self._apply_archive_state(
|
|
71
|
+
task_dir,
|
|
72
|
+
destination,
|
|
73
|
+
task_data,
|
|
74
|
+
relationship_updates,
|
|
75
|
+
context_snapshots,
|
|
76
|
+
archive_date,
|
|
77
|
+
finalize,
|
|
78
|
+
)
|
|
79
|
+
clear_task_from_sessions(
|
|
80
|
+
self.repo_root,
|
|
81
|
+
task_dir.relative_to(self.repo_root).as_posix(),
|
|
82
|
+
)
|
|
83
|
+
return TaskArchiveResult(
|
|
84
|
+
source=task_dir,
|
|
85
|
+
destination=destination,
|
|
86
|
+
task_name=task_dir.name,
|
|
87
|
+
archived_at=archive_date,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _load_completed_task(self, task: str | Path) -> tuple[Path, dict]:
|
|
57
91
|
task_dir = self.repository.resolve(task)
|
|
58
92
|
if not task_dir.is_dir():
|
|
59
93
|
raise TaskArchiveError(
|
|
@@ -78,19 +112,18 @@ class TaskArchiveService:
|
|
|
78
112
|
task_dir,
|
|
79
113
|
f"task status is {status}",
|
|
80
114
|
)
|
|
115
|
+
return task_dir, task_data
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
|
|
117
|
+
def _archive_destination(self, task_dir: Path, archive_date: str) -> Path:
|
|
118
|
+
return (
|
|
84
119
|
self.tasks_dir
|
|
85
120
|
/ DIR_ARCHIVE
|
|
86
121
|
/ archive_date[:7]
|
|
87
122
|
/ task_dir.name
|
|
88
123
|
)
|
|
89
|
-
relationship_updates = self._relationship_updates(
|
|
90
|
-
task_dir.name,
|
|
91
|
-
task_data,
|
|
92
|
-
)
|
|
93
124
|
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _move_task(task_dir: Path, destination: Path) -> None:
|
|
94
127
|
move_result = archive_directory_resumable(task_dir, destination)
|
|
95
128
|
if not move_result.ok:
|
|
96
129
|
raise TaskArchiveError(
|
|
@@ -99,6 +132,16 @@ class TaskArchiveService:
|
|
|
99
132
|
move_result.message,
|
|
100
133
|
)
|
|
101
134
|
|
|
135
|
+
def _apply_archive_state(
|
|
136
|
+
self,
|
|
137
|
+
task_dir: Path,
|
|
138
|
+
destination: Path,
|
|
139
|
+
task_data: dict,
|
|
140
|
+
relationship_updates: list[tuple[Path, dict, dict]],
|
|
141
|
+
context_snapshots: dict[str, bytes],
|
|
142
|
+
archive_date: str,
|
|
143
|
+
finalize: ArchiveFinalizer | None,
|
|
144
|
+
) -> None:
|
|
102
145
|
try:
|
|
103
146
|
self.repository.save(
|
|
104
147
|
destination,
|
|
@@ -108,32 +151,35 @@ class TaskArchiveService:
|
|
|
108
151
|
},
|
|
109
152
|
)
|
|
110
153
|
self._apply_relationship_updates(relationship_updates)
|
|
111
|
-
|
|
154
|
+
self._normalize_context_paths(task_dir, destination)
|
|
155
|
+
if finalize is not None and not finalize():
|
|
156
|
+
raise TaskArchiveError(
|
|
157
|
+
"TASK-ARCHIVE-FINALIZE-001",
|
|
158
|
+
destination,
|
|
159
|
+
"linked change archive failed",
|
|
160
|
+
)
|
|
161
|
+
except Exception as error:
|
|
112
162
|
self._rollback(
|
|
113
163
|
task_dir,
|
|
114
164
|
destination,
|
|
115
165
|
task_data,
|
|
116
166
|
relationship_updates,
|
|
167
|
+
context_snapshots,
|
|
117
168
|
)
|
|
118
169
|
if isinstance(error, TaskArchiveError):
|
|
119
170
|
raise
|
|
171
|
+
if isinstance(error, TaskRepositoryError):
|
|
172
|
+
raise TaskArchiveError(
|
|
173
|
+
"TASK-ARCHIVE-WRITE-001",
|
|
174
|
+
error.path,
|
|
175
|
+
error.detail,
|
|
176
|
+
) from error
|
|
120
177
|
raise TaskArchiveError(
|
|
121
|
-
"TASK-ARCHIVE-
|
|
122
|
-
|
|
123
|
-
error
|
|
178
|
+
"TASK-ARCHIVE-FINALIZE-001",
|
|
179
|
+
destination,
|
|
180
|
+
f"archive finalizer failed: {error}",
|
|
124
181
|
) from error
|
|
125
182
|
|
|
126
|
-
clear_task_from_sessions(
|
|
127
|
-
self.repo_root,
|
|
128
|
-
task_dir.relative_to(self.repo_root).as_posix(),
|
|
129
|
-
)
|
|
130
|
-
return TaskArchiveResult(
|
|
131
|
-
source=task_dir,
|
|
132
|
-
destination=destination,
|
|
133
|
-
task_name=task_dir.name,
|
|
134
|
-
archived_at=archive_date,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
183
|
def _relationship_updates(
|
|
138
184
|
self,
|
|
139
185
|
task_name: str,
|
|
@@ -177,6 +223,81 @@ class TaskArchiveService:
|
|
|
177
223
|
)
|
|
178
224
|
return updates
|
|
179
225
|
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _context_snapshots(task_dir: Path) -> dict[str, bytes]:
|
|
228
|
+
try:
|
|
229
|
+
return {
|
|
230
|
+
path.name: path.read_bytes()
|
|
231
|
+
for path in task_dir.glob("*.jsonl")
|
|
232
|
+
if path.is_file()
|
|
233
|
+
}
|
|
234
|
+
except OSError as error:
|
|
235
|
+
raise TaskArchiveError(
|
|
236
|
+
"TASK-ARCHIVE-CONTEXT-001",
|
|
237
|
+
task_dir,
|
|
238
|
+
f"failed to snapshot task context: {error}",
|
|
239
|
+
) from error
|
|
240
|
+
|
|
241
|
+
def _normalize_context_paths(
|
|
242
|
+
self,
|
|
243
|
+
source: Path,
|
|
244
|
+
destination: Path,
|
|
245
|
+
) -> None:
|
|
246
|
+
source_path = source.relative_to(self.repo_root).as_posix()
|
|
247
|
+
destination_path = destination.relative_to(self.repo_root).as_posix()
|
|
248
|
+
for context_file in sorted(destination.glob("*.jsonl")):
|
|
249
|
+
self._normalize_context_file(
|
|
250
|
+
context_file,
|
|
251
|
+
source_path,
|
|
252
|
+
destination_path,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _normalize_context_file(
|
|
257
|
+
context_file: Path,
|
|
258
|
+
source_path: str,
|
|
259
|
+
destination_path: str,
|
|
260
|
+
) -> None:
|
|
261
|
+
original = context_file.read_text(encoding="utf-8")
|
|
262
|
+
rendered: list[str] = []
|
|
263
|
+
changed = False
|
|
264
|
+
for line in original.splitlines(keepends=True):
|
|
265
|
+
content = line.rstrip("\r\n")
|
|
266
|
+
ending = line[len(content):]
|
|
267
|
+
try:
|
|
268
|
+
entry = json.loads(content)
|
|
269
|
+
except json.JSONDecodeError:
|
|
270
|
+
rendered.append(line)
|
|
271
|
+
continue
|
|
272
|
+
file_path = entry.get("file") if isinstance(entry, dict) else None
|
|
273
|
+
normalized = TaskArchiveService._archived_context_path(
|
|
274
|
+
file_path,
|
|
275
|
+
source_path,
|
|
276
|
+
destination_path,
|
|
277
|
+
)
|
|
278
|
+
if normalized == file_path:
|
|
279
|
+
rendered.append(line)
|
|
280
|
+
continue
|
|
281
|
+
entry["file"] = normalized
|
|
282
|
+
rendered.append(
|
|
283
|
+
json.dumps(entry, ensure_ascii=False) + ending
|
|
284
|
+
)
|
|
285
|
+
changed = True
|
|
286
|
+
if changed:
|
|
287
|
+
context_file.write_text("".join(rendered), encoding="utf-8")
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def _archived_context_path(
|
|
291
|
+
file_path: object,
|
|
292
|
+
source_path: str,
|
|
293
|
+
destination_path: str,
|
|
294
|
+
) -> object:
|
|
295
|
+
if file_path == source_path:
|
|
296
|
+
return destination_path
|
|
297
|
+
if isinstance(file_path, str) and file_path.startswith(f"{source_path}/"):
|
|
298
|
+
return destination_path + file_path[len(source_path):]
|
|
299
|
+
return file_path
|
|
300
|
+
|
|
180
301
|
def _apply_relationship_updates(
|
|
181
302
|
self,
|
|
182
303
|
updates: list[tuple[Path, dict, dict]],
|
|
@@ -204,6 +325,7 @@ class TaskArchiveService:
|
|
|
204
325
|
destination: Path,
|
|
205
326
|
task_data: dict,
|
|
206
327
|
relationship_updates: list[tuple[Path, dict, dict]],
|
|
328
|
+
context_snapshots: dict[str, bytes],
|
|
207
329
|
) -> None:
|
|
208
330
|
for task_dir, original, _ in relationship_updates:
|
|
209
331
|
try:
|
|
@@ -214,6 +336,11 @@ class TaskArchiveService:
|
|
|
214
336
|
if destination.is_dir() and not source.exists():
|
|
215
337
|
archive_directory_resumable(destination, source)
|
|
216
338
|
if source.is_dir():
|
|
339
|
+
for name, content in context_snapshots.items():
|
|
340
|
+
try:
|
|
341
|
+
(source / name).write_bytes(content)
|
|
342
|
+
except OSError:
|
|
343
|
+
pass
|
|
217
344
|
try:
|
|
218
345
|
self.repository.replace(source, task_data)
|
|
219
346
|
except TaskRepositoryError:
|
|
@@ -124,11 +124,13 @@ class TaskLifecycleService:
|
|
|
124
124
|
task: str | Path,
|
|
125
125
|
*,
|
|
126
126
|
completed_at: str | None = None,
|
|
127
|
+
allow_spec_file_modifications: bool = False,
|
|
127
128
|
) -> LifecycleResult:
|
|
128
129
|
return self.execute(
|
|
129
130
|
COMPLETE_STAGE,
|
|
130
131
|
task,
|
|
131
132
|
completed_at=completed_at,
|
|
133
|
+
allow_spec_file_modifications=allow_spec_file_modifications,
|
|
132
134
|
)
|
|
133
135
|
|
|
134
136
|
def execute(
|
|
@@ -308,7 +310,7 @@ class TaskLifecycleService:
|
|
|
308
310
|
task_dir: Path,
|
|
309
311
|
already_at_target: bool,
|
|
310
312
|
) -> LifecycleResult | None:
|
|
311
|
-
if not already_at_target or stage.name
|
|
313
|
+
if not already_at_target or stage.name != START_STAGE.name:
|
|
312
314
|
return None
|
|
313
315
|
return LifecycleResult(
|
|
314
316
|
ok=True,
|
|
@@ -196,6 +196,42 @@ def generate_session_content(
|
|
|
196
196
|
"""
|
|
197
197
|
|
|
198
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
|
+
|
|
199
235
|
def update_index(
|
|
200
236
|
index_file: Path,
|
|
201
237
|
dev_dir: Path,
|
|
@@ -207,8 +243,6 @@ def update_index(
|
|
|
207
243
|
) -> bool:
|
|
208
244
|
"""Update index.md with new session info."""
|
|
209
245
|
commit_display = _format_commit_display(commit)
|
|
210
|
-
|
|
211
|
-
# Get file number from active_file name
|
|
212
246
|
match = re.search(r"(\d+)", active_file)
|
|
213
247
|
active_num = int(match.group(1)) if match else 0
|
|
214
248
|
files_table = count_journal_files(dev_dir, active_num)
|
|
@@ -225,69 +259,26 @@ def update_index(
|
|
|
225
259
|
print("Error: index.md is missing auto-update markers. Please restore the template first.", file=sys.stderr)
|
|
226
260
|
return False
|
|
227
261
|
|
|
228
|
-
# Process sections
|
|
229
262
|
lines = content.splitlines()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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")
|
|
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")
|
|
291
282
|
print("[OK] index.md updated.", file=sys.stderr)
|
|
292
283
|
return True
|
|
293
284
|
|
|
@@ -326,87 +317,109 @@ def _auto_commit_workspace(repo_root: Path) -> None:
|
|
|
326
317
|
print(f"[WARN] Metadata auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
|
|
327
318
|
|
|
328
319
|
|
|
329
|
-
def
|
|
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
|
-
|
|
320
|
+
def _session_workspace(repo_root: Path):
|
|
340
321
|
developer = get_developer(repo_root)
|
|
341
322
|
if not developer:
|
|
342
323
|
print("Error: developer identity has not been initialized.", file=sys.stderr)
|
|
343
|
-
return
|
|
344
|
-
|
|
324
|
+
return None
|
|
345
325
|
dev_dir = get_workspace_dir(repo_root)
|
|
346
|
-
if not dev_dir:
|
|
326
|
+
if dev_dir is None or not dev_dir.is_dir():
|
|
347
327
|
print("Error: workspace directory not found.", file=sys.stderr)
|
|
348
|
-
return
|
|
349
|
-
|
|
350
|
-
max_lines = get_max_journal_lines(repo_root)
|
|
351
|
-
|
|
328
|
+
return None
|
|
352
329
|
index_file = dev_dir / "index.md"
|
|
353
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
354
|
-
|
|
355
330
|
journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
358
338
|
|
|
359
|
-
session_content = generate_session_content(
|
|
360
|
-
new_session, title, commit, summary, extra_content, today
|
|
361
|
-
)
|
|
362
|
-
content_lines = len(session_content.splitlines())
|
|
363
339
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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)
|
|
369
350
|
print(f"Title: {title}", file=sys.stderr)
|
|
370
|
-
print(f"Commit: {commit}", file=sys.stderr)
|
|
371
|
-
print("", file=sys.stderr)
|
|
351
|
+
print(f"Commit: {commit}\n", file=sys.stderr)
|
|
372
352
|
print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
|
|
373
353
|
print(f"Current lines: {current_lines}", file=sys.stderr)
|
|
374
354
|
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
|
|
355
|
+
print(f"Total lines after append: {current_lines + content_lines}\n", file=sys.stderr)
|
|
380
356
|
|
|
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
357
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
394
377
|
|
|
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
378
|
|
|
400
|
-
|
|
401
|
-
print("
|
|
379
|
+
def _print_session_success(new_session: int, target_file: Path) -> None:
|
|
380
|
+
print("\n" + "=" * 40, file=sys.stderr)
|
|
402
381
|
print(f"[OK] Session {new_session} recorded successfully.", file=sys.stderr)
|
|
403
|
-
print("
|
|
404
|
-
print("", file=sys.stderr)
|
|
382
|
+
print("=" * 40 + "\n", file=sys.stderr)
|
|
405
383
|
print("Updated files:", file=sys.stderr)
|
|
406
|
-
print(f" - {target_file.name
|
|
384
|
+
print(f" - {target_file.name}", file=sys.stderr)
|
|
407
385
|
print(" - index.md", file=sys.stderr)
|
|
408
386
|
|
|
409
|
-
|
|
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)
|
|
410
423
|
if auto_commit:
|
|
411
424
|
print("", file=sys.stderr)
|
|
412
425
|
_auto_commit_workspace(repo_root)
|
|
@@ -561,6 +561,7 @@ def cmd_complete(args: argparse.Namespace) -> int:
|
|
|
561
561
|
result = service.complete(
|
|
562
562
|
task_dir,
|
|
563
563
|
completed_at=datetime.now().strftime("%Y-%m-%d"),
|
|
564
|
+
allow_spec_file_modifications=is_main_session(repo_root),
|
|
564
565
|
)
|
|
565
566
|
if not result.ok:
|
|
566
567
|
if result.code == "LIFECYCLE-TRANSITION-001":
|