@pennixrv/trellis 0.7.1-beta.4 → 0.7.1-beta.6
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/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/task-progress.d.ts +1 -0
- package/dist/commands/task-progress.d.ts.map +1 -1
- package/dist/commands/task-progress.js +67 -25
- package/dist/commands/task-progress.js.map +1 -1
- package/dist/commands/update.js +2 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/constants/paths.d.ts +3 -10
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -13
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
- package/dist/templates/claude/hooks/statusline.py +9 -4
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +1 -1
- package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
- package/dist/templates/opencode/lib/session-utils.js +301 -209
- package/dist/templates/snow/SNOW.md +1 -1
- package/dist/templates/trellis/agents/teammate.md +5 -5
- package/dist/templates/trellis/index.d.ts +3 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +6 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +1 -1
- package/dist/templates/trellis/scripts/common/active_task.py +3 -47
- package/dist/templates/trellis/scripts/common/paths.py +9 -25
- package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
- package/dist/templates/trellis/scripts/common/task_context.py +11 -51
- package/dist/templates/trellis/scripts/common/task_continuity.py +53 -45
- package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
- package/dist/templates/trellis/scripts/common/task_move.py +345 -0
- package/dist/templates/trellis/scripts/common/task_store.py +28 -55
- package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
- package/dist/templates/trellis/scripts/common/task_workflow.py +28 -20
- package/dist/templates/trellis/scripts/common/tasks.py +6 -5
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
- package/dist/templates/trellis/scripts/task.py +41 -23
- package/dist/templates/trellis/workflow.md +4 -2
- 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/<
|
|
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;
|
|
@@ -50,6 +52,7 @@ export declare const initDeveloperScript: string;
|
|
|
50
52
|
export declare const taskScript: string;
|
|
51
53
|
export declare const getContextScript: string;
|
|
52
54
|
export declare const addSessionScript: string;
|
|
55
|
+
export declare const linearSyncScript: string;
|
|
53
56
|
export declare const workflowMdTemplate: string;
|
|
54
57
|
export declare const configYamlTemplate: string;
|
|
55
58
|
export declare const gitignoreTemplate: 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;
|
|
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");
|
|
@@ -61,6 +63,7 @@ export const initDeveloperScript = readTemplate("scripts/init_developer.py");
|
|
|
61
63
|
export const taskScript = readTemplate("scripts/task.py");
|
|
62
64
|
export const getContextScript = readTemplate("scripts/get_context.py");
|
|
63
65
|
export const addSessionScript = readTemplate("scripts/add_session.py");
|
|
66
|
+
export const linearSyncScript = readTemplate("scripts/hooks/linear_sync.py");
|
|
64
67
|
// Configuration files
|
|
65
68
|
export const workflowMdTemplate = readTemplate("workflow.md");
|
|
66
69
|
export const configYamlTemplate = readTemplate("config.yaml");
|
|
@@ -84,6 +87,8 @@ export function getAllScripts() {
|
|
|
84
87
|
scripts.set("common/git_context.py", commonGitContext);
|
|
85
88
|
scripts.set("common/task_queue.py", commonTaskQueue);
|
|
86
89
|
scripts.set("common/task_utils.py", commonTaskUtils);
|
|
90
|
+
scripts.set("common/task_layout.py", commonTaskLayout);
|
|
91
|
+
scripts.set("common/task_move.py", commonTaskMove);
|
|
87
92
|
scripts.set("common/active_task.py", commonActiveTask);
|
|
88
93
|
scripts.set("common/cli_adapter.py", commonCliAdapter);
|
|
89
94
|
scripts.set("common/config.py", commonConfig);
|
|
@@ -110,6 +115,7 @@ export function getAllScripts() {
|
|
|
110
115
|
scripts.set("task.py", taskScript);
|
|
111
116
|
scripts.set("get_context.py", getContextScript);
|
|
112
117
|
scripts.set("add_session.py", addSessionScript);
|
|
118
|
+
scripts.set("hooks/linear_sync.py", linearSyncScript);
|
|
113
119
|
return scripts;
|
|
114
120
|
}
|
|
115
121
|
/**
|
|
@@ -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/<
|
|
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
|
-
"""
|
|
204
|
+
"""Use the canonical status-layout resolver shared by all task readers."""
|
|
205
|
+
from .paths import resolve_task_ref as resolve
|
|
205
206
|
|
|
206
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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/<
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
with
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|