@mindfoldhq/trellis 0.5.0-rc.4 → 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.5",
3
+ "description": "Codex template enables multi_agent_v2 by default with an 8-minute wait floor. AGENTS.md adds wait tool rules. No new migrations.",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**Enhancements:**\n- feat(codex): `.codex/config.toml` template now writes `[features.multi_agent_v2] { enabled = true, max_concurrent_threads_per_session = 6, min_wait_timeout_ms = 480000 }`. `min_wait_timeout_ms = 480000` (8 min) is the `wait()` timeout floor, up from Codex's `10000` (10 s) default; forces the parent to wait through subagent runtime instead of cancelling. `enabled = true` is required inside the table — the table form alone does not enable the feature. Project-level `[features]` activates only when the project is trusted in `~/.codex/config.toml`.\n- fix(codex): drop the `[features].codex_hooks = true` line — `CodexHooks` is now `Stage::Stable` with `default_enabled: true` in Codex's feature registry, so hooks load automatically once the project is trusted. The legacy `codex_hooks` alias was redundant.\n- feat(agents): `AGENTS.md` Subagents section names Codex's `wait` tool and bans cancel/re-spawn before `wait` returns. Raise the timeout (default 30 s, max 1 h) before judging a subagent stuck.",
7
+ "migrations": [],
8
+ "notes": "RC install: `npm install -g @mindfoldhq/trellis@rc`. Codex users: trust this project (`[projects.\"<abs path>\"].trust_level = \"trusted\"` in `~/.codex/config.toml`) so hooks and the project-level [features] block activate."
9
+ }
@@ -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
+ }
@@ -1,15 +1,32 @@
1
1
  # Project-scoped Codex defaults for Trellis workflows.
2
- # Codex loads this after ~/.codex/config.toml when you work in this project.
2
+ # Codex merges this layer after the user-level config when the project
3
+ # is marked as a trusted project. To trust this project, add it under
4
+ # `[projects]` in ~/.codex/config.toml, e.g.:
5
+ #
6
+ # [projects."/abs/path/to/this/repo"]
7
+ # trust_level = "trusted"
8
+ #
9
+ # Without trust, the [features] block below is loaded but disabled.
3
10
 
4
11
  # Keep AGENTS.md as the primary project instruction file.
5
12
  project_doc_fallback_filenames = ["AGENTS.md"]
6
13
 
7
- # NOTE: Trellis's SessionStart + UserPromptSubmit hooks require opt-in.
8
- # Add the following to your USER-level config at ~/.codex/config.toml
9
- # (not this project file features.* must be enabled globally):
10
- #
11
- # [features]
12
- # codex_hooks = true
13
- #
14
- # Without this flag, hooks.json is ignored and Trellis context won't
15
- # be injected into Codex sessions.
14
+ # Codex hooks (`hooks.json` in this directory) load automatically once
15
+ # the project is trusted no feature flag needed. CodexHooks is Stable
16
+ # and default_enabled: true in codex's feature registry; the legacy
17
+ # `[features].codex_hooks = true` flag is no longer required.
18
+
19
+ # multi_agent_v2 forces structured subagent orchestration with the
20
+ # `wait` tool — parent must block on terminal status before acting,
21
+ # instead of cancelling / re-spawning. Incompatible with
22
+ # [agents].max_threads (codex will reject the combination).
23
+ # `enabled = true` is required inside the table — the table form does
24
+ # NOT auto-enable the feature without it.
25
+ # - max_concurrent_threads_per_session: bumps default 4 → 6.
26
+ # - min_wait_timeout_ms: 480000 ms = 8 min. Codex default is 10 s, too
27
+ # short for Trellis subagents that routinely take 2-10 min. Hard
28
+ # ceiling is 3,600,000 (1 h).
29
+ [features.multi_agent_v2]
30
+ enabled = true
31
+ max_concurrent_threads_per_session = 6
32
+ min_wait_timeout_ms = 480000
@@ -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,10 +18,13 @@ If you're using Codex or another agent-capable tool, additional project-scoped h
18
18
 
19
19
  ## Subagents
20
20
 
21
- - ALWAYS wait for all subagents to complete before yielding.
21
+ - ALWAYS wait for every spawned subagent to reach a terminal status before yielding, acting on partial results, or spawning followups.
22
+ - On Codex, this means calling the `wait` tool with the subagent's thread id (requires `multi_agent_v2`). Do NOT infer completion from elapsed time.
23
+ - On Claude Code / OpenCode, this means awaiting the Task/agent tool result before continuing.
24
+ - NEVER cancel or re-spawn a subagent that hasn't finished. If a subagent appears stuck, raise the wait timeout (Codex default 30s, max 1h) before judging it broken.
22
25
  - Spawn subagents automatically when:
23
26
  - Parallelizable work (e.g., install + verify, npm test + typecheck, multiple tasks from plan)
24
- - Long-running or blocking tasks where a worker can run independently.
27
+ - Long-running or blocking tasks where a worker can run independently
25
28
  - Isolation for risky changes or checks
26
29
 
27
30
  Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`.
@@ -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.4",
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",