@pennixrv/trellis 0.7.1-beta.5 → 0.7.1-beta.7

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 (50) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/init.js +14 -6
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/commands/task-progress.d.ts +1 -0
  5. package/dist/commands/task-progress.d.ts.map +1 -1
  6. package/dist/commands/task-progress.js +67 -25
  7. package/dist/commands/task-progress.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +21 -4
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/constants/paths.d.ts +3 -10
  12. package/dist/constants/paths.d.ts.map +1 -1
  13. package/dist/constants/paths.js +4 -13
  14. package/dist/constants/paths.js.map +1 -1
  15. package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
  16. package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
  17. package/dist/templates/claude/hooks/statusline.py +9 -4
  18. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
  19. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
  20. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
  21. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
  22. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  23. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
  24. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
  25. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +2 -2
  26. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +1 -1
  27. package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
  28. package/dist/templates/opencode/lib/session-utils.js +301 -209
  29. package/dist/templates/snow/SNOW.md +1 -1
  30. package/dist/templates/trellis/agents/teammate.md +5 -5
  31. package/dist/templates/trellis/index.d.ts +2 -0
  32. package/dist/templates/trellis/index.d.ts.map +1 -1
  33. package/dist/templates/trellis/index.js +4 -0
  34. package/dist/templates/trellis/index.js.map +1 -1
  35. package/dist/templates/trellis/scripts/add_session.py +1 -1
  36. package/dist/templates/trellis/scripts/common/active_task.py +3 -47
  37. package/dist/templates/trellis/scripts/common/paths.py +9 -25
  38. package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
  39. package/dist/templates/trellis/scripts/common/task_context.py +11 -51
  40. package/dist/templates/trellis/scripts/common/task_continuity.py +53 -45
  41. package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
  42. package/dist/templates/trellis/scripts/common/task_move.py +345 -0
  43. package/dist/templates/trellis/scripts/common/task_store.py +28 -55
  44. package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
  45. package/dist/templates/trellis/scripts/common/task_workflow.py +28 -20
  46. package/dist/templates/trellis/scripts/common/tasks.py +6 -5
  47. package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
  48. package/dist/templates/trellis/scripts/task.py +41 -23
  49. package/dist/templates/trellis/workflow.md +4 -2
  50. package/package.json +2 -2
@@ -25,7 +25,7 @@ In Snow:
25
25
  2. Dispatch implement/check/research (project agents under `.snow/agents/`). Prefer prompt first line:
26
26
 
27
27
  ```text
28
- Active task: .trellis/tasks/<id>
28
+ Active task: .trellis/tasks/<task-dir>
29
29
  ```
30
30
 
31
31
  3. Optional: `/trellis-continue` / `/trellis-finish-work`, or `skill-execute` on `trellis-*` skills.
@@ -16,11 +16,11 @@ assignment,
16
16
  the paths are:
17
17
 
18
18
  ```text
19
- .trellis/tasks/<task>/teammates/<batch>/<assignment>/brief.json
20
- .trellis/tasks/<task>/teammates/<batch>/<assignment>/queue.json
21
- .trellis/tasks/<task>/teammates/<batch>/<assignment>/attempts/<attempt-id>/claim.json
22
- .trellis/tasks/<task>/teammates/<batch>/<assignment>/attempts/<attempt-id>/worklog.md
23
- .trellis/tasks/<task>/teammates/<batch>/<assignment>/attempts/<attempt-id>/report.json
19
+ .trellis/tasks/<task-dir>/teammates/<batch>/<assignment>/brief.json
20
+ .trellis/tasks/<task-dir>/teammates/<batch>/<assignment>/queue.json
21
+ .trellis/tasks/<task-dir>/teammates/<batch>/<assignment>/attempts/<attempt-id>/claim.json
22
+ .trellis/tasks/<task-dir>/teammates/<batch>/<assignment>/attempts/<attempt-id>/worklog.md
23
+ .trellis/tasks/<task-dir>/teammates/<batch>/<assignment>/attempts/<attempt-id>/report.json
24
24
  ```
25
25
 
26
26
  Read the brief and claim, append material progress to the assigned worklog,
@@ -25,6 +25,8 @@ export declare const commonDeveloper: string;
25
25
  export declare const commonGitContext: string;
26
26
  export declare const commonTaskQueue: string;
27
27
  export declare const commonTaskUtils: string;
28
+ export declare const commonTaskLayout: string;
29
+ export declare const commonTaskMove: string;
28
30
  export declare const commonActiveTask: string;
29
31
  export declare const commonCliAdapter: string;
30
32
  export declare const commonConfig: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,kBAAkB,QAE9B,CAAC;AACF,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,uBAAuB,QAEnC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAA+C,CAAC;AAG7E,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAC/D,eAAO,MAAM,qBAAqB,QAAoC,CAAC;AAGvE,eAAO,MAAM,qBAAqB,QAAqC,CAAC;AACxE,eAAO,MAAM,8BAA8B,QAE1C,CAAC;AACF,eAAO,MAAM,wBAAwB,QAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA2CnD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASlD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,cAAc,QAA8C,CAAC;AAC1E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,kBAAkB,QAE9B,CAAC;AACF,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,uBAAuB,QAEnC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAA+C,CAAC;AAG7E,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAC/D,eAAO,MAAM,qBAAqB,QAAoC,CAAC;AAGvE,eAAO,MAAM,qBAAqB,QAAqC,CAAC;AACxE,eAAO,MAAM,8BAA8B,QAE1C,CAAC;AACF,eAAO,MAAM,wBAAwB,QAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA6CnD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASlD"}
@@ -35,6 +35,8 @@ export const commonDeveloper = readTemplate("scripts/common/developer.py");
35
35
  export const commonGitContext = readTemplate("scripts/common/git_context.py");
36
36
  export const commonTaskQueue = readTemplate("scripts/common/task_queue.py");
37
37
  export const commonTaskUtils = readTemplate("scripts/common/task_utils.py");
38
+ export const commonTaskLayout = readTemplate("scripts/common/task_layout.py");
39
+ export const commonTaskMove = readTemplate("scripts/common/task_move.py");
38
40
  export const commonActiveTask = readTemplate("scripts/common/active_task.py");
39
41
  export const commonCliAdapter = readTemplate("scripts/common/cli_adapter.py");
40
42
  export const commonConfig = readTemplate("scripts/common/config.py");
@@ -85,6 +87,8 @@ export function getAllScripts() {
85
87
  scripts.set("common/git_context.py", commonGitContext);
86
88
  scripts.set("common/task_queue.py", commonTaskQueue);
87
89
  scripts.set("common/task_utils.py", commonTaskUtils);
90
+ scripts.set("common/task_layout.py", commonTaskLayout);
91
+ scripts.set("common/task_move.py", commonTaskMove);
88
92
  scripts.set("common/active_task.py", commonActiveTask);
89
93
  scripts.set("common/cli_adapter.py", commonCliAdapter);
90
94
  scripts.set("common/config.py", commonConfig);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
@@ -1107,7 +1107,7 @@ def _auto_commit_workspace(repo_root: Path) -> str:
1107
1107
 
1108
1108
  commit_msg = get_session_commit_message(repo_root)
1109
1109
  # Resolve the current task so staging is scoped to its dir only. The ref
1110
- # is ``.trellis/tasks/<name>`` — pass the bare task name.
1110
+ # is ``.trellis/tasks/<task-dir>`` — pass the bare task name.
1111
1111
  current = get_current_task(repo_root)
1112
1112
  if current:
1113
1113
  task_name = Path(current).name
@@ -201,54 +201,10 @@ def normalize_task_ref(task_ref: str) -> str:
201
201
 
202
202
 
203
203
  def resolve_task_ref(task_ref: str, repo_root: Path) -> Path | None:
204
- """Resolve a task ref to an absolute task directory inside the repo.
204
+ """Use the canonical status-layout resolver shared by all task readers."""
205
+ from .paths import resolve_task_ref as resolve
205
206
 
206
- Mirrors `paths.resolve_task_ref` (same containment check). Duplicated
207
- rather than imported because this module is loaded standalone — hooks add
208
- it to `sys.path` directly — so it stays zero-relative-import on purpose.
209
- """
210
- normalized = normalize_task_ref(task_ref)
211
- if not normalized:
212
- return None
213
-
214
- try:
215
- root = repo_root.resolve()
216
- except OSError:
217
- return None
218
-
219
- path_obj = Path(normalized)
220
- if path_obj.is_absolute():
221
- candidate = path_obj
222
- elif normalized.startswith(f"{DIR_WORKFLOW}/"):
223
- candidate = root / path_obj
224
- else:
225
- candidate = root / DIR_WORKFLOW / DIR_TASKS / path_obj
226
-
227
- # Both sides are resolved because repo_root itself may sit behind a
228
- # symlink (/tmp on macOS does), and resolve() is what collapses `..`
229
- # instead of leaving it for a lexical relative_to() to wave through.
230
- try:
231
- resolved = candidate.resolve()
232
- workflow_real = (root / DIR_WORKFLOW).resolve()
233
- except OSError:
234
- return None
235
-
236
- try:
237
- resolved.relative_to(root)
238
- return resolved
239
- except ValueError:
240
- pass
241
-
242
- # `.trellis` may itself be a symlink into a store outside the repo (#567).
243
- # The workflow dir's own real location is then a second legitimate
244
- # containment base; a ref that escapes BOTH bases is still refused. Map
245
- # back to the in-repo (lexical) form so callers store a repo-relative ref.
246
- try:
247
- rel = resolved.relative_to(workflow_real)
248
- except ValueError:
249
- return None
250
-
251
- return root / DIR_WORKFLOW / rel
207
+ return resolve(task_ref, repo_root)
252
208
 
253
209
 
254
210
  def _runtime_sessions_dir(repo_root: Path) -> Path:
@@ -344,35 +344,19 @@ def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | Non
344
344
  else:
345
345
  candidate = root / DIR_WORKFLOW / DIR_TASKS / path_obj
346
346
 
347
- # resolve() collapses `..` and follows symlinks, so a task directory that
348
- # links outside the repo is refused too. Both sides are resolved because
349
- # repo_root itself may sit behind a symlink (/tmp on macOS does).
350
- try:
351
- resolved = candidate.resolve()
352
- workflow_real = (root / DIR_WORKFLOW).resolve()
353
- except OSError:
354
- return None
347
+ from .task_layout import task_status_dir
348
+ from .task_move import require_settled_layout
355
349
 
356
350
  try:
357
- resolved.relative_to(root)
358
- return resolved
359
- except ValueError:
360
- pass
361
-
362
- # `.trellis` may itself be a symlink into a store outside the repo (#567).
363
- # The workflow dir's own real location is then a second legitimate
364
- # containment base: a ref through that link never left the workflow tree.
365
- # A ref that escapes BOTH bases (traversal, absolute path elsewhere, a
366
- # task dir symlinked out of the tree) is still refused.
367
- try:
368
- rel = resolved.relative_to(workflow_real)
369
- except ValueError:
351
+ require_settled_layout(root)
352
+ tasks = root / DIR_WORKFLOW / DIR_TASKS
353
+ if task_status_dir(candidate, tasks) is None:
354
+ return None
355
+ relative = candidate.resolve().relative_to(tasks.resolve())
356
+ return tasks / relative
357
+ except (ValueError, OSError, RuntimeError):
370
358
  return None
371
359
 
372
- # Map back to the in-repo (lexical) form so callers store the same
373
- # repo-relative ref as in the non-symlinked layout.
374
- return root / DIR_WORKFLOW / rel
375
-
376
360
 
377
361
  def get_current_task(
378
362
  repo_root: Path | None = None,
@@ -70,7 +70,7 @@ def safe_trellis_paths_to_add(
70
70
  Included:
71
71
  - .trellis/workspace/<developer>/journal-*.md
72
72
  - .trellis/workspace/<developer>/index.md
73
- - .trellis/tasks/<task_name>/ (ONLY the current task dir when
73
+ - .trellis/tasks/<task-dir>/ (ONLY the current task dir when
74
74
  ``task_name`` is passed)
75
75
 
76
76
  Excluded (intentionally — these must not be staged):
@@ -112,16 +112,16 @@ def safe_trellis_paths_to_add(
112
112
  # Narrow scope — ONLY the current task directory.
113
113
  # Never iterdir() all tasks: parallel-window dirty task dirs must not
114
114
  # leak into the session auto-commit.
115
- active_task = tasks_dir / task_name
116
- if active_task.is_dir():
117
- paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}")
115
+ from .task_utils import find_task_by_name
116
+ active_task = find_task_by_name(task_name, tasks_dir)
117
+ if active_task is not None and active_task.is_dir():
118
+ paths.append(active_task.relative_to(repo_root).as_posix())
118
119
  return paths
119
120
 
120
121
  # Wide scope (no task_name): each direct task directory.
121
- for child in sorted(tasks_dir.iterdir()):
122
- if not child.is_dir():
123
- continue
124
- paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}")
122
+ from .task_layout import iter_task_dirs
123
+ for child in iter_task_dirs(tasks_dir):
124
+ paths.append(child.relative_to(repo_root).as_posix())
125
125
 
126
126
  return paths
127
127
 
@@ -29,7 +29,7 @@ from .config import get_context_injection_limits
29
29
  from .git import branch_exists_locally
30
30
  from .io import read_json
31
31
  from .log import Colors, colored
32
- from .paths import DIR_TASKS, DIR_WORKFLOW, FILE_TASK_JSON, get_repo_root
32
+ from .paths import FILE_TASK_JSON, get_repo_root
33
33
  from .task_utils import resolve_task_dir
34
34
 
35
35
  # Extensions that look like code rather than spec/research docs. Entries with
@@ -195,54 +195,6 @@ def _is_exempt_from_code_file_warning(file_path: str, task_rel: str) -> bool:
195
195
  return False
196
196
 
197
197
 
198
- def _resolve_context_entry_path(
199
- file_path: str, repo_root: Path, task_dir: Path | None
200
- ) -> Path | None:
201
- """Bind historical self-references to their archive copy without fallback."""
202
- repo_path = repo_root / file_path
203
- if task_dir is None:
204
- return repo_path
205
-
206
- try:
207
- task_parts = task_dir.resolve().relative_to(repo_root.resolve()).parts
208
- except ValueError:
209
- return repo_path
210
-
211
- archive_prefix = (DIR_WORKFLOW, DIR_TASKS, "archive")
212
- if len(task_parts) != 5 or task_parts[:3] != archive_prefix:
213
- return repo_path
214
-
215
- year_month = task_parts[3]
216
- if (
217
- len(year_month) != 7
218
- or year_month[4] != "-"
219
- or not year_month[:4].isdigit()
220
- or not year_month[5:].isdigit()
221
- ):
222
- return repo_path
223
-
224
- historical_root = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_dir.name}"
225
- posix_path = file_path.replace("\\", "/")
226
- if posix_path == historical_root:
227
- relative_parts: tuple[str, ...] = ()
228
- elif posix_path.startswith(f"{historical_root}/"):
229
- relative_path = posix_path[len(historical_root) + 1 :]
230
- if relative_path.endswith("/"):
231
- relative_path = relative_path[:-1]
232
- relative_parts = tuple(relative_path.split("/")) if relative_path else ()
233
- if any(part in ("", ".", "..") for part in relative_parts):
234
- return None
235
- else:
236
- return repo_path
237
-
238
- try:
239
- archive_root = task_dir.resolve()
240
- resolved_path = task_dir.joinpath(*relative_parts).resolve()
241
- resolved_path.relative_to(archive_root)
242
- except (OSError, RuntimeError, ValueError):
243
- return None
244
- return resolved_path
245
-
246
198
 
247
199
  def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = None) -> int:
248
200
  """Validate a single JSONL file.
@@ -268,7 +220,7 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = N
268
220
  task_rel = ""
269
221
  if task_dir is not None:
270
222
  try:
271
- task_rel = task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
223
+ task_rel = task_dir.absolute().relative_to(repo_root.absolute()).as_posix()
272
224
  except ValueError:
273
225
  task_rel = ""
274
226
 
@@ -323,7 +275,15 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = N
323
275
  continue
324
276
 
325
277
  real_entries += 1
326
- full_path = _resolve_context_entry_path(file_path, repo_root, task_dir)
278
+ full_path = repo_root / file_path
279
+ normalized = file_path.replace("\\", "/")
280
+ if task_dir is not None and task_rel and (normalized == task_rel or normalized.startswith(task_rel + "/")):
281
+ try:
282
+ if ".." in Path(normalized).parts:
283
+ raise ValueError("Traversal in task reference")
284
+ full_path.resolve().relative_to(task_dir.resolve())
285
+ except (ValueError, OSError, RuntimeError):
286
+ full_path = None
327
287
  if entry_type == "directory":
328
288
  if full_path is None or not full_path.is_dir():
329
289
  print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
@@ -524,35 +524,39 @@ def clear_task_runtime_records(repo_root: Path, task_path: str) -> bool:
524
524
  @contextlib.contextmanager
525
525
  def active_task_change_guard(repo_root: Path, context_key: str, task_path: str, *, clearing: bool):
526
526
  """Serialize pointer writes with ownership changes for the same task."""
527
- directory = resolve_task_dir(task_path, repo_root)
528
- if directory is None:
529
- yield False
530
- return
531
- task_path = directory.relative_to(repo_root).as_posix()
532
- path = _ownership_path(repo_root, task_path)
533
- with _record_lock(path):
534
- try:
535
- record = _load_owner(path)
536
- except ContinuityError:
537
- yield False
538
- return
539
- if record is None:
540
- yield True
541
- return
542
- data, _ = read_json_checked(directory / FILE_TASK_JSON)
543
- task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
544
- if task_id != record["task_id"] or record["task_path"] != task_path:
527
+ from .task_move import layout_lock, require_settled_layout
528
+
529
+ with layout_lock(repo_root):
530
+ require_settled_layout(repo_root)
531
+ directory = resolve_task_dir(task_path, repo_root)
532
+ if directory is None:
545
533
  yield False
546
534
  return
547
- if clearing:
548
- allowed = record["status"] == "consumed" and record["target_session_id"] == context_key
549
- elif record["status"] == "ready":
550
- allowed = context_key != record["source_session_id"] and task_path == record["task_path"]
551
- elif record["status"] == "consumed":
552
- allowed = context_key == record["target_session_id"] and task_path == record["task_path"]
553
- else:
554
- allowed = record["status"] in {"claiming", "claimed"} and context_key == record["target_session_id"] and task_path == record["task_path"]
555
- yield allowed
535
+ task_path = directory.relative_to(repo_root).as_posix()
536
+ path = _ownership_path(repo_root, task_path)
537
+ with _record_lock(path):
538
+ try:
539
+ record = _load_owner(path)
540
+ except ContinuityError:
541
+ yield False
542
+ return
543
+ if record is None:
544
+ yield True
545
+ return
546
+ data, _ = read_json_checked(directory / FILE_TASK_JSON)
547
+ task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
548
+ if task_id != record["task_id"] or record["task_path"] != task_path:
549
+ yield False
550
+ return
551
+ if clearing:
552
+ allowed = record["status"] == "consumed" and record["target_session_id"] == context_key
553
+ elif record["status"] == "ready":
554
+ allowed = context_key != record["source_session_id"] and task_path == record["task_path"]
555
+ elif record["status"] == "consumed":
556
+ allowed = context_key == record["target_session_id"] and task_path == record["task_path"]
557
+ else:
558
+ allowed = record["status"] in {"claiming", "claimed"} and context_key == record["target_session_id"] and task_path == record["task_path"]
559
+ yield allowed
556
560
 
557
561
 
558
562
  def assert_task_mutation_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
@@ -578,21 +582,25 @@ def assert_task_identity_change_allowed(repo_root: Path, task_dir: Path, data: d
578
582
  @contextlib.contextmanager
579
583
  def task_mutation_guard(repo_root: Path, task_dir: Path, data: dict):
580
584
  """Hold the ownership lock across a single task mutation."""
581
- task_path = task_dir.relative_to(repo_root).as_posix()
582
- path = _ownership_path(repo_root, task_path)
583
- with _record_lock(path):
584
- try:
585
- record = _load_owner(path)
586
- except ContinuityError:
587
- yield False
588
- return
589
- if record is None:
590
- yield True
591
- return
592
- task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
593
- if task_id != record["task_id"] or record["task_path"] != task_path:
594
- yield False
595
- return
596
- active = resolve_active_task(repo_root)
597
- allowed = record["status"] == "consumed" and active.context_key == record["target_session_id"] and active.task_path == task_path
598
- yield allowed
585
+ from .task_move import layout_lock, require_settled_layout
586
+
587
+ with layout_lock(repo_root):
588
+ require_settled_layout(repo_root)
589
+ task_path = task_dir.relative_to(repo_root).as_posix()
590
+ path = _ownership_path(repo_root, task_path)
591
+ with _record_lock(path):
592
+ try:
593
+ record = _load_owner(path)
594
+ except ContinuityError:
595
+ yield False
596
+ return
597
+ if record is None:
598
+ yield True
599
+ return
600
+ task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
601
+ if task_id != record["task_id"] or record["task_path"] != task_path:
602
+ yield False
603
+ return
604
+ active = resolve_active_task(repo_root)
605
+ allowed = record["status"] == "consumed" and active.context_key == record["target_session_id"] and active.task_path == task_path
606
+ yield allowed
@@ -0,0 +1,80 @@
1
+ """Task status directories and bounded discovery (never recurse into task content)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterator
7
+ from datetime import date
8
+ from pathlib import Path
9
+
10
+ STATUSES = ("planning", "in_progress", "completed", "cancelled")
11
+ MONTH_RE = re.compile(r"^\d{4}-(?:0[1-9]|1[0-2])$")
12
+
13
+
14
+ def task_location(tasks_dir: Path, name: str, status: str, month: str | None = None) -> Path:
15
+ if not name or name in {".", "..", "archive", *STATUSES} or "/" in name or "\\" in name:
16
+ raise ValueError(f"Invalid task directory name: {name}")
17
+ if status not in STATUSES:
18
+ raise ValueError(f"Invalid task status: {status}")
19
+ if status == "completed":
20
+ month = month or date.today().strftime("%Y-%m")
21
+ if not MONTH_RE.fullmatch(month):
22
+ raise ValueError(f"Invalid completion month: {month}")
23
+ return tasks_dir / status / month / name
24
+ return tasks_dir / status / name
25
+
26
+
27
+ def task_status_dir(task_dir: Path, tasks_dir: Path) -> str | None:
28
+ """Validate exact depth, containment and nested symlinks; allow a shared tasks root."""
29
+ try:
30
+ relative = task_dir.absolute().relative_to(tasks_dir.absolute())
31
+ parts = relative.parts
32
+ valid = (len(parts) == 2 and parts[0] in {"planning", "in_progress", "cancelled"}) or (
33
+ len(parts) == 3 and parts[0] == "completed" and MONTH_RE.fullmatch(parts[1])
34
+ )
35
+ if not valid:
36
+ return None
37
+ current = tasks_dir
38
+ for part in parts:
39
+ current /= part
40
+ if current.is_symlink():
41
+ return None
42
+ task_dir.resolve().relative_to(tasks_dir.resolve())
43
+ return parts[0]
44
+ except (ValueError, OSError, RuntimeError):
45
+ return None
46
+
47
+
48
+ def iter_task_dirs(tasks_dir: Path, *, legacy: bool = False) -> Iterator[Path]:
49
+ """Enumerate only the prescribed levels; old layout is migration input only."""
50
+ if not tasks_dir.is_dir():
51
+ return
52
+ candidates: list[Path] = []
53
+ for entry in sorted(tasks_dir.iterdir()):
54
+ if entry.is_symlink():
55
+ raise ValueError(f"Symlink in task layout: {entry}")
56
+ if not entry.is_dir():
57
+ continue
58
+ if entry.name in STATUSES:
59
+ if entry.name == "completed":
60
+ for month in sorted(entry.iterdir()):
61
+ if month.is_symlink() or not month.is_dir() or not MONTH_RE.fullmatch(month.name):
62
+ raise ValueError(f"Invalid completion month directory: {month}")
63
+ candidates.extend(month.iterdir())
64
+ else:
65
+ candidates.extend(entry.iterdir())
66
+ elif legacy and entry.name == "archive":
67
+ for month in sorted(entry.iterdir()):
68
+ if month.is_symlink() or not month.is_dir() or not MONTH_RE.fullmatch(month.name):
69
+ raise ValueError(f"Invalid archive month directory: {month}")
70
+ candidates.extend(month.iterdir())
71
+ elif legacy:
72
+ candidates.append(entry)
73
+ else:
74
+ raise ValueError(f"Old task layout at {entry}; run task.py migrate-layout")
75
+ for candidate in sorted(candidates, key=lambda p: (p.name, str(p))):
76
+ if candidate.is_symlink():
77
+ raise ValueError(f"Symlink in task layout: {candidate}")
78
+ if not candidate.is_dir():
79
+ raise ValueError(f"Invalid task directory: {candidate}")
80
+ yield candidate