@mindfoldhq/trellis 0.5.0-rc.5 → 0.5.0-rc.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/migrations/manifests/0.5.0-rc.6.json +9 -0
- package/dist/templates/codex/hooks/session-start.py +47 -1
- package/dist/templates/codex/skills/finish-work/SKILL.md +14 -4
- package/dist/templates/common/commands/finish-work.md +14 -4
- package/dist/templates/copilot/hooks/session-start.py +47 -1
- package/dist/templates/copilot/prompts/finish-work.prompt.md +14 -4
- package/dist/templates/shared-hooks/session-start.py +48 -2
- package/package.json +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.5.0-rc.6",
|
|
3
|
+
"description": "Windows session-start.py normalizes MSYS/Cygwin/WSL paths. finish-work Step 2 classifies dirty paths instead of aborting unconditionally. No new migrations.",
|
|
4
|
+
"breaking": false,
|
|
5
|
+
"recommendMigrate": false,
|
|
6
|
+
"changelog": "**Bug Fixes:**\n- fix(hooks): Windows `session-start.py` normalizes MSYS `/d/...`, Cygwin `/cygdrive/d/...`, WSL `/mnt/d/...` to `D:\\...` before `Path.resolve()`. Synced to 6 copies (`.claude/`, `.codex/`, `.cursor/` + templates `shared-hooks/`, `codex/`, `copilot/`). Fixes #226.\n\n**Enhancements:**\n- feat(finish-work): Step 2 classifies dirty paths into current-task / other-window / indeterminate. Only current-task paths abort; other-window paths are reported and skipped; indeterminate paths prompt the user. Synced to 8 copies.",
|
|
7
|
+
"migrations": [],
|
|
8
|
+
"notes": "RC install: `npm install -g @mindfoldhq/trellis@rc`. Windows + Git Bash users should upgrade to restore Trellis context injection."
|
|
9
|
+
}
|
|
@@ -18,6 +18,52 @@ import warnings
|
|
|
18
18
|
from io import StringIO
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
def _normalize_windows_shell_path(path_str: str) -> str:
|
|
23
|
+
"""Normalize Unix-style shell paths to real Windows paths.
|
|
24
|
+
|
|
25
|
+
On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like
|
|
26
|
+
`/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret
|
|
27
|
+
these as `D:/d/Users...` on drive D: (or similar), breaking repo root
|
|
28
|
+
detection.
|
|
29
|
+
|
|
30
|
+
This function is intentionally conservative: it only rewrites patterns that
|
|
31
|
+
unambiguously represent a drive letter mount.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(path_str, str) or not path_str:
|
|
34
|
+
return path_str
|
|
35
|
+
|
|
36
|
+
# Only relevant on Windows; keep other platforms untouched.
|
|
37
|
+
if not sys.platform.startswith("win"):
|
|
38
|
+
return path_str
|
|
39
|
+
|
|
40
|
+
p = path_str.strip()
|
|
41
|
+
|
|
42
|
+
# Already a Windows drive path (C:\... or C:/...)
|
|
43
|
+
if re.match(r"^[A-Za-z]:[\/]", p):
|
|
44
|
+
return p
|
|
45
|
+
|
|
46
|
+
# MSYS/Git-Bash style: /c/Users/... or /d/Work/...
|
|
47
|
+
m = re.match(r"^/([A-Za-z])/(.*)", p)
|
|
48
|
+
if m:
|
|
49
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
50
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
51
|
+
|
|
52
|
+
# Cygwin style: /cygdrive/c/Users/...
|
|
53
|
+
m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p)
|
|
54
|
+
if m:
|
|
55
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
56
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
57
|
+
|
|
58
|
+
# WSL mounted drive (sometimes leaked into env): /mnt/c/Users/...
|
|
59
|
+
m = re.match(r"^/mnt/([A-Za-z])/(.*)", p)
|
|
60
|
+
if m:
|
|
61
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
62
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
63
|
+
|
|
64
|
+
return path_str
|
|
65
|
+
|
|
66
|
+
|
|
21
67
|
warnings.filterwarnings("ignore")
|
|
22
68
|
|
|
23
69
|
FIRST_REPLY_NOTICE = """<first-reply-notice>
|
|
@@ -271,7 +317,7 @@ def main() -> None:
|
|
|
271
317
|
hook_input = json.loads(sys.stdin.read())
|
|
272
318
|
if not isinstance(hook_input, dict):
|
|
273
319
|
hook_input = {}
|
|
274
|
-
project_dir = Path(hook_input.get("cwd", ".")).resolve()
|
|
320
|
+
project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve()
|
|
275
321
|
except (json.JSONDecodeError, KeyError):
|
|
276
322
|
hook_input = {}
|
|
277
323
|
project_dir = Path(".").resolve()
|
|
@@ -21,7 +21,7 @@ This prints:
|
|
|
21
21
|
|
|
22
22
|
If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless.
|
|
23
23
|
|
|
24
|
-
## Step 2: Sanity check —
|
|
24
|
+
## Step 2: Sanity check — classify dirty paths
|
|
25
25
|
|
|
26
26
|
Run:
|
|
27
27
|
|
|
@@ -31,11 +31,21 @@ git status --porcelain
|
|
|
31
31
|
|
|
32
32
|
Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics:
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task
|
|
37
|
+
- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task
|
|
38
|
+
- Paths in unrelated areas you have no recollection of touching this session → other parallel work
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Then route:
|
|
41
|
+
|
|
42
|
+
- **Any remaining path looks like current-task work** — bail out with:
|
|
43
|
+
> "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `$finish-work`."
|
|
44
|
+
|
|
45
|
+
Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there.
|
|
46
|
+
- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3:
|
|
47
|
+
> "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`."
|
|
48
|
+
- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer.
|
|
39
49
|
|
|
40
50
|
## Step 3: Archive task(s)
|
|
41
51
|
|
|
@@ -16,7 +16,7 @@ This prints:
|
|
|
16
16
|
|
|
17
17
|
If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless.
|
|
18
18
|
|
|
19
|
-
## Step 2: Sanity check —
|
|
19
|
+
## Step 2: Sanity check — classify dirty paths
|
|
20
20
|
|
|
21
21
|
Run:
|
|
22
22
|
|
|
@@ -26,11 +26,21 @@ git status --porcelain
|
|
|
26
26
|
|
|
27
27
|
Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics:
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task
|
|
32
|
+
- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task
|
|
33
|
+
- Paths in unrelated areas you have no recollection of touching this session → other parallel work
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Then route:
|
|
36
|
+
|
|
37
|
+
- **Any remaining path looks like current-task work** — bail out with:
|
|
38
|
+
> "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `{{CMD_REF:finish-work}}`."
|
|
39
|
+
|
|
40
|
+
Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there.
|
|
41
|
+
- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3:
|
|
42
|
+
> "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`."
|
|
43
|
+
- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer.
|
|
34
44
|
|
|
35
45
|
## Step 3: Archive task(s)
|
|
36
46
|
|
|
@@ -21,6 +21,52 @@ import warnings
|
|
|
21
21
|
from io import StringIO
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
def _normalize_windows_shell_path(path_str: str) -> str:
|
|
26
|
+
"""Normalize Unix-style shell paths to real Windows paths.
|
|
27
|
+
|
|
28
|
+
On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like
|
|
29
|
+
`/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret
|
|
30
|
+
these as `D:/d/Users...` on drive D: (or similar), breaking repo root
|
|
31
|
+
detection.
|
|
32
|
+
|
|
33
|
+
This function is intentionally conservative: it only rewrites patterns that
|
|
34
|
+
unambiguously represent a drive letter mount.
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(path_str, str) or not path_str:
|
|
37
|
+
return path_str
|
|
38
|
+
|
|
39
|
+
# Only relevant on Windows; keep other platforms untouched.
|
|
40
|
+
if not sys.platform.startswith("win"):
|
|
41
|
+
return path_str
|
|
42
|
+
|
|
43
|
+
p = path_str.strip()
|
|
44
|
+
|
|
45
|
+
# Already a Windows drive path (C:\... or C:/...)
|
|
46
|
+
if re.match(r"^[A-Za-z]:[\/]", p):
|
|
47
|
+
return p
|
|
48
|
+
|
|
49
|
+
# MSYS/Git-Bash style: /c/Users/... or /d/Work/...
|
|
50
|
+
m = re.match(r"^/([A-Za-z])/(.*)", p)
|
|
51
|
+
if m:
|
|
52
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
53
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
54
|
+
|
|
55
|
+
# Cygwin style: /cygdrive/c/Users/...
|
|
56
|
+
m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p)
|
|
57
|
+
if m:
|
|
58
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
59
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
60
|
+
|
|
61
|
+
# WSL mounted drive (sometimes leaked into env): /mnt/c/Users/...
|
|
62
|
+
m = re.match(r"^/mnt/([A-Za-z])/(.*)", p)
|
|
63
|
+
if m:
|
|
64
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
65
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
66
|
+
|
|
67
|
+
return path_str
|
|
68
|
+
|
|
69
|
+
|
|
24
70
|
warnings.filterwarnings("ignore")
|
|
25
71
|
|
|
26
72
|
|
|
@@ -267,7 +313,7 @@ def main() -> None:
|
|
|
267
313
|
hook_input = json.loads(sys.stdin.read())
|
|
268
314
|
if not isinstance(hook_input, dict):
|
|
269
315
|
hook_input = {}
|
|
270
|
-
project_dir = Path(hook_input.get("cwd", ".")).resolve()
|
|
316
|
+
project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve()
|
|
271
317
|
except (json.JSONDecodeError, KeyError):
|
|
272
318
|
hook_input = {}
|
|
273
319
|
project_dir = Path(".").resolve()
|
|
@@ -24,7 +24,7 @@ This prints:
|
|
|
24
24
|
|
|
25
25
|
If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless.
|
|
26
26
|
|
|
27
|
-
## Step 2: Sanity check —
|
|
27
|
+
## Step 2: Sanity check — classify dirty paths
|
|
28
28
|
|
|
29
29
|
Run:
|
|
30
30
|
|
|
@@ -34,11 +34,21 @@ git status --porcelain
|
|
|
34
34
|
|
|
35
35
|
Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this prompt's own work.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics:
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task
|
|
40
|
+
- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task
|
|
41
|
+
- Paths in unrelated areas you have no recollection of touching this session → other parallel work
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Then route:
|
|
44
|
+
|
|
45
|
+
- **Any remaining path looks like current-task work** — bail out with:
|
|
46
|
+
> "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/finish-work`."
|
|
47
|
+
|
|
48
|
+
Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there.
|
|
49
|
+
- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3:
|
|
50
|
+
> "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`."
|
|
51
|
+
- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer.
|
|
42
52
|
|
|
43
53
|
## Step 3: Archive task(s)
|
|
44
54
|
|
|
@@ -18,6 +18,52 @@ import sys
|
|
|
18
18
|
from io import StringIO
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
def _normalize_windows_shell_path(path_str: str) -> str:
|
|
23
|
+
"""Normalize Unix-style shell paths to real Windows paths.
|
|
24
|
+
|
|
25
|
+
On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like
|
|
26
|
+
`/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret
|
|
27
|
+
these as `D:/d/Users...` on drive D: (or similar), breaking repo root
|
|
28
|
+
detection.
|
|
29
|
+
|
|
30
|
+
This function is intentionally conservative: it only rewrites patterns that
|
|
31
|
+
unambiguously represent a drive letter mount.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(path_str, str) or not path_str:
|
|
34
|
+
return path_str
|
|
35
|
+
|
|
36
|
+
# Only relevant on Windows; keep other platforms untouched.
|
|
37
|
+
if not sys.platform.startswith("win"):
|
|
38
|
+
return path_str
|
|
39
|
+
|
|
40
|
+
p = path_str.strip()
|
|
41
|
+
|
|
42
|
+
# Already a Windows drive path (C:\... or C:/...)
|
|
43
|
+
if re.match(r"^[A-Za-z]:[\/]", p):
|
|
44
|
+
return p
|
|
45
|
+
|
|
46
|
+
# MSYS/Git-Bash style: /c/Users/... or /d/Work/...
|
|
47
|
+
m = re.match(r"^/([A-Za-z])/(.*)", p)
|
|
48
|
+
if m:
|
|
49
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
50
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
51
|
+
|
|
52
|
+
# Cygwin style: /cygdrive/c/Users/...
|
|
53
|
+
m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p)
|
|
54
|
+
if m:
|
|
55
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
56
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
57
|
+
|
|
58
|
+
# WSL mounted drive (sometimes leaked into env): /mnt/c/Users/...
|
|
59
|
+
m = re.match(r"^/mnt/([A-Za-z])/(.*)", p)
|
|
60
|
+
if m:
|
|
61
|
+
drive, rest = m.group(1).upper(), m.group(2)
|
|
62
|
+
return f"{drive}:\\{rest.replace('/', '\\')}"
|
|
63
|
+
|
|
64
|
+
return path_str
|
|
65
|
+
|
|
66
|
+
|
|
21
67
|
FIRST_REPLY_NOTICE = """<first-reply-notice>
|
|
22
68
|
On the first visible assistant reply in this session, begin with exactly one short Chinese sentence:
|
|
23
69
|
Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。
|
|
@@ -594,10 +640,10 @@ def main():
|
|
|
594
640
|
for var in project_dir_env_vars:
|
|
595
641
|
val = os.environ.get(var)
|
|
596
642
|
if val:
|
|
597
|
-
project_dir = Path(val).resolve()
|
|
643
|
+
project_dir = Path(_normalize_windows_shell_path(val)).resolve()
|
|
598
644
|
break
|
|
599
645
|
if project_dir is None:
|
|
600
|
-
project_dir = Path(hook_input.get("cwd", ".")).resolve()
|
|
646
|
+
project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve()
|
|
601
647
|
|
|
602
648
|
trellis_dir = project_dir / ".trellis"
|
|
603
649
|
context_key = _resolve_context_key(trellis_dir, hook_input)
|
package/package.json
CHANGED