@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.
@@ -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 — working tree must be clean
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
- If anything else is dirty (any path outside those two prefixes), **stop and bail out** with:
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
- > "Working tree has uncommitted code changes. Return to workflow Phase 3.4 to commit them before running `$finish-work`."
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
- 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.
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 — working tree must be clean
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
- If anything else is dirty (any path outside those two prefixes), **stop and bail out** with:
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
- > "Working tree has uncommitted code changes. Return to workflow Phase 3.4 to commit them before running `{{CMD_REF:finish-work}}`."
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
- 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.
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 — working tree must be clean
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
- If anything else is dirty (any path outside those two prefixes), **stop and bail out** with:
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
- > "Working tree has uncommitted code changes. Return to workflow Phase 3.4 to commit them before running `/finish-work`."
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
- 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindfoldhq/trellis",
3
- "version": "0.5.0-rc.5",
3
+ "version": "0.5.0-rc.6",
4
4
  "description": "AI capabilities grow like ivy — Trellis provides the structure to guide them along a disciplined path",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",