@blxzer/cursor-trellis 0.3.3 → 0.3.4

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/CHANGELOG.md CHANGED
@@ -11,6 +11,12 @@ SemVer: [semver.org](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.3.4] - 2026-07-04
15
+
16
+ ### Added
17
+
18
+ - **feat(cursor)**: auto-rename the **main** Agent chat tab to the cstl task **directory name** after `task.py select` or `task.py start-execution --approved` (`rename-session-for-task.py` `afterShellExecution` hook + `cstl-session-rename.mdc` rule). Uses `cursor-app-control` `rename_chat` when available; skips silently when MCP is missing. Does not rename on `task.py create`. Native and BYOK share the same path.
19
+
14
20
  ## [0.3.3] - 2026-07-04
15
21
 
16
22
  Follow-up polish after 0.3.2 coexistence release: closes known limitations from the runtime-isolation task.
package/README.zh-CN.md CHANGED
@@ -197,6 +197,7 @@ smart-search --version
197
197
  | --- | --- |
198
198
  | `pnpm build` | `tsc` + 拷贝模板 |
199
199
  | `pnpm test` | Vitest |
200
+ | `pnpm mirror-check` | Dogfood `.cursor` / `.agents` vs templates |
200
201
  | `pnpm run sync:smart-search` | 刷新 vendor |
201
202
 
202
203
  Release 与 npm 发布流程**不在**公开 README 中;见内部维护文档。
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": "0.3.2",
3
+ "description": "Reissue 0.3.1 runtime isolation with publishable core/cli pair (.cstl/)",
4
+ "breaking": true,
5
+ "recommendMigrate": true,
6
+ "changelog": "**@blxzer/cursor-trellis 0.3.2**\n\nReissues 0.3.1 content with a publishable `@blxzer/cursor-trellis-core` pair. Runtime root `.trellis/` → **`.cstl/`** for coexistence with upstream Trellis.\n\n```bash\nnpm install -g @blxzer/cursor-trellis@0.3.2\ncstl update --migrate\n```",
7
+ "migrations": [
8
+ {
9
+ "type": "rename-dir",
10
+ "from": ".trellis",
11
+ "to": ".cstl",
12
+ "description": "v0.3.2: cursor-trellis runtime dir isolation (.trellis -> .cstl)"
13
+ }
14
+ ],
15
+ "notes": "0.3.1 was withdrawn from npm; use 0.3.2+ for the .cstl/ migration."
16
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.3.3",
3
+ "description": "Coexistence polish: AGENTS CSTL block uninstall, hash scope, publish CI",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**@blxzer/cursor-trellis 0.3.3**\n\n- feat(uninstall): strip CSTL managed block from AGENTS.md while preserving upstream TRELLIS block\n- feat(hash): AGENTS.md template hash tracks CSTL block only\n- docs(spec): cursor-trellis release coexistence guide\n- ci: publish workflow on cstl-v* tags",
7
+ "migrations": [],
8
+ "notes": "Run `cstl update` to sync templates. No `--migrate` required."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.3.4",
3
+ "description": "Rename main Cursor chat to cstl task directory on select/start-execution",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**@blxzer/cursor-trellis 0.3.4**\n\n- feat(cursor): auto-rename main Agent chat tab to task directory name after `task.py select` or `start-execution --approved` (`rename-session-for-task.py` + `cstl-session-rename.mdc`). Uses `cursor-app-control` `rename_chat` when available; silent skip when MCP missing. No rename on `task.py create`.",
7
+ "migrations": [],
8
+ "notes": "Run `cstl update` to sync new hook, rule, and hooks.json wiring."
9
+ }
@@ -27,8 +27,9 @@ Whether legacy settings/config files exist in a project depends on which histori
27
27
  | `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.cstl/workflow.md` and emits the body matching the selected task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. |
28
28
  | `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. |
29
29
  | `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. |
30
+ | `rename-session-for-task.py` | After successful `task.py select` / `start-execution --approved`, prompts main-session rename to task directory name (best-effort `rename_chat`). |
30
31
 
31
- Cursor exposes sessionStart, preToolUse on Task/Subagent, beforeShellExecution, and stop events. Confirm against the current Cursor release before wiring a new event name; the available event surface has evolved over time and historical platform documentation may be stale.
32
+ Cursor exposes sessionStart, preToolUse on Task/Subagent, beforeShellExecution, afterShellExecution, and stop events. Confirm against the current Cursor release before wiring a new event name; the available event surface has evolved over time and historical platform documentation may be stale.
32
33
 
33
34
  ## Local Change Scenarios
34
35
 
@@ -1 +1 @@
1
- {"version":3,"file":"expected-rules.d.ts","sourceRoot":"","sources":["../../../../src/templates/cursor/fixtures/expected-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,aAAa,EAAE,YAAY,EAoBvC,CAAC"}
1
+ {"version":3,"file":"expected-rules.d.ts","sourceRoot":"","sources":["../../../../src/templates/cursor/fixtures/expected-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,aAAa,EAAE,YAAY,EAyBvC,CAAC"}
@@ -22,5 +22,10 @@ export const expectedRules = [
22
22
  requiredSections: ["Native", "BYOK", "codegraph", "smart-search"],
23
23
  minBytes: 600,
24
24
  },
25
+ {
26
+ filename: "cstl-session-rename.mdc",
27
+ requiredSections: ["rename_chat", "select", "start-execution", "skip silently"],
28
+ minBytes: 300,
29
+ },
25
30
  ];
26
31
  //# sourceMappingURL=expected-rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"expected-rules.js","sourceRoot":"","sources":["../../../../src/templates/cursor/fixtures/expected-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C;QACE,QAAQ,EAAE,iBAAiB;QAC3B,gBAAgB,EAAE;YAChB,eAAe;YACf,qBAAqB;YACrB,cAAc;SACf;QACD,QAAQ,EAAE,GAAG;KACd;IACD;QACE,QAAQ,EAAE,4BAA4B;QACtC,gBAAgB,EAAE,CAAC,SAAS,EAAE,0BAA0B,EAAE,KAAK,CAAC;QAChE,QAAQ,EAAE,GAAG;KACd;IACD;QACE,QAAQ,EAAE,uBAAuB;QACjC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,CAAC;QACjE,QAAQ,EAAE,GAAG;KACd;CACF,CAAC"}
1
+ {"version":3,"file":"expected-rules.js","sourceRoot":"","sources":["../../../../src/templates/cursor/fixtures/expected-rules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C;QACE,QAAQ,EAAE,iBAAiB;QAC3B,gBAAgB,EAAE;YAChB,eAAe;YACf,qBAAqB;YACrB,cAAc;SACf;QACD,QAAQ,EAAE,GAAG;KACd;IACD;QACE,QAAQ,EAAE,4BAA4B;QACtC,gBAAgB,EAAE,CAAC,SAAS,EAAE,0BAA0B,EAAE,KAAK,CAAC;QAChE,QAAQ,EAAE,GAAG;KACd;IACD;QACE,QAAQ,EAAE,uBAAuB;QACjC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,CAAC;QACjE,QAAQ,EAAE,GAAG;KACd;IACD;QACE,QAAQ,EAAE,yBAAyB;QACnC,gBAAgB,EAAE,CAAC,aAAa,EAAE,QAAQ,EAAE,iBAAiB,EAAE,eAAe,CAAC;QAC/E,QAAQ,EAAE,GAAG;KACd;CACF,CAAC"}
@@ -26,6 +26,13 @@
26
26
  "timeout": 5
27
27
  }
28
28
  ],
29
+ "afterShellExecution": [
30
+ {
31
+ "command": "{{PYTHON_CMD}} .cursor/hooks/rename-session-for-task.py",
32
+ "matcher": "task\\.py.*(select|start-execution)",
33
+ "timeout": 5
34
+ }
35
+ ],
29
36
  "stop": [
30
37
  {
31
38
  "command": "{{PYTHON_CMD}} .cursor/hooks/research-end-retrieval-pack.py",
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: "Rename Cursor chat to cstl task directory name after select or start-execution (cursor-app-control rename_chat)"
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Trellis session rename (Cursor main session)
7
+
8
+ Encourage **one task per main Agent chat**. Subagent / Task child windows are out of scope.
9
+
10
+ ## When to rename
11
+
12
+ After **you** successfully run either command in the **main** session (same turn or immediately after shell output):
13
+
14
+ - `python ./.cstl/scripts/task.py select <task-dir>`
15
+ - `python ./.cstl/scripts/task.py start-execution <task-dir> --approved`
16
+
17
+ Do **not** rename for `task.py create` alone (create does not bind the session).
18
+
19
+ ## Title
20
+
21
+ Use the task **directory name** only (e.g. `07-04-cstl-session-rename`), not `task.json` `title`.
22
+
23
+ ## How
24
+
25
+ 1. If `cursor-app-control` MCP `rename_chat` is available, call it once with `title` = directory name.
26
+ 2. If the tool is **not** available (BYOK or other environments), **skip silently** — no user prompt to rename manually.
27
+ 3. Do not rename repeatedly for the same task in the same session unless the user switched to a different task directory.
28
+
29
+ ## Native vs BYOK
30
+
31
+ Same rule for both. BYOK availability of `rename_chat` is environment-dependent; never fail the task workflow when rename is unavailable.
@@ -11,7 +11,7 @@ export interface HookScript {
11
11
  /** Script content — no placeholders, ready to write directly */
12
12
  content: string;
13
13
  }
14
- export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-retrieval-plan.py" | "inject-workflow-state.py" | "inject-subagent-context.py" | "research-end-retrieval-pack.py";
14
+ export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "rename-session-for-task.py" | "inject-retrieval-plan.py" | "inject-workflow-state.py" | "inject-subagent-context.py" | "research-end-retrieval-pack.py";
15
15
  export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro";
16
16
  /**
17
17
  * Which shared hooks each platform actually invokes. Single source of truth
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,0BAA0B,GAC1B,4BAA4B,GAC5B,gCAAgC,CAAC;AAErC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CAgC1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,4BAA4B,GAC5B,0BAA0B,GAC1B,0BAA0B,GAC1B,4BAA4B,GAC5B,gCAAgC,CAAC;AAErC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CAiC1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
@@ -45,6 +45,7 @@ export const SHARED_HOOKS_BY_PLATFORM = {
45
45
  cursor: [
46
46
  "session-start.py",
47
47
  "inject-shell-session-context.py",
48
+ "rename-session-for-task.py",
48
49
  "inject-retrieval-plan.py",
49
50
  "inject-subagent-context.py",
50
51
  "research-end-retrieval-pack.py",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA4BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,KAAK,EAAE,CAAC,0BAA0B,CAAC;IACnC,MAAM,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACxD,KAAK,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACvD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,IAAI,EAAE,CAAC,4BAA4B,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA6BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,4BAA4B;QAC5B,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,KAAK,EAAE,CAAC,0BAA0B,CAAC;IACnC,MAAM,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACxD,KAAK,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACvD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gCAAgC;KACjC;IACD,IAAI,EAAE,CAAC,4BAA4B,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env python3
2
+ """Cursor afterShellExecution hook: prompt session rename after task bind/start.
3
+
4
+ When the agent successfully runs `task.py select` or
5
+ `task.py start-execution <task> --approved`, emit an agent_message (best-effort)
6
+ so the main session calls cursor-app-control `rename_chat` with the task
7
+ directory name. MCP unavailable → agent skips silently per cstl-session-rename rule.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import re
15
+ import shlex
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ DIR_WORKFLOW = ".cstl"
21
+ DIR_RUNTIME = ".runtime"
22
+ DIR_SESSION_RENAME = "session-rename"
23
+ ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
24
+ CONTEXT_IDENTITY_KEYS = (
25
+ "session_id",
26
+ "sessionId",
27
+ "sessionID",
28
+ "conversation_id",
29
+ "conversationId",
30
+ "conversationID",
31
+ "transcript_path",
32
+ "transcriptPath",
33
+ "transcript",
34
+ )
35
+
36
+
37
+ def _string_value(value: Any) -> str | None:
38
+ if isinstance(value, str):
39
+ stripped = value.strip()
40
+ return stripped or None
41
+ return None
42
+
43
+
44
+ def _strip_ansi(text: str) -> str:
45
+ return ANSI_ESCAPE.sub("", text)
46
+
47
+
48
+ def _find_trellis_root(start: Path) -> Path | None:
49
+ current = start.resolve()
50
+ while True:
51
+ if (current / DIR_WORKFLOW).is_dir():
52
+ return current
53
+ if current == current.parent:
54
+ return None
55
+ current = current.parent
56
+
57
+
58
+ def _task_py_token_index(tokens: list[str]) -> int | None:
59
+ for index, token in enumerate(tokens):
60
+ if Path(token.strip("\"'")).name == "task.py":
61
+ return index
62
+ return None
63
+
64
+
65
+ def parse_rename_intent(command: str) -> dict[str, str] | None:
66
+ """Return subcommand + task_ref when command should trigger rename."""
67
+ try:
68
+ tokens = shlex.split(command, posix=os.name != "nt")
69
+ except ValueError:
70
+ return None
71
+
72
+ task_index = _task_py_token_index(tokens)
73
+ if task_index is None or task_index + 1 >= len(tokens):
74
+ return None
75
+
76
+ subcommand = tokens[task_index + 1]
77
+ if subcommand == "select":
78
+ if task_index + 2 >= len(tokens):
79
+ return None
80
+ return {"subcommand": subcommand, "task_ref": tokens[task_index + 2]}
81
+ if subcommand == "start-execution":
82
+ if "--approved" not in tokens:
83
+ return None
84
+ if task_index + 2 >= len(tokens):
85
+ return None
86
+ task_ref = tokens[task_index + 2]
87
+ if task_ref.startswith("-"):
88
+ return None
89
+ return {"subcommand": subcommand, "task_ref": task_ref}
90
+ return None
91
+
92
+
93
+ def task_directory_name(task_ref: str) -> str:
94
+ """Use task folder basename as the chat title."""
95
+ cleaned = task_ref.strip().strip("\"'")
96
+ return Path(cleaned.replace("\\", "/")).name
97
+
98
+
99
+ def shell_output_succeeded(subcommand: str, output: str) -> bool:
100
+ text = _strip_ansi(output or "")
101
+ if "Error:" in text or "error:" in text.lower():
102
+ return False
103
+ if subcommand == "select":
104
+ return "Selected task:" in text or "✓ Selected task" in text
105
+ if subcommand == "start-execution":
106
+ return "Execution approved" in text or "✓ Execution approved" in text
107
+ return False
108
+
109
+
110
+ def _safe_context_filename(context_key: str) -> str:
111
+ digest = hashlib.sha256(context_key.encode("utf-8")).hexdigest()[:24]
112
+ return f"{digest}.json"
113
+
114
+
115
+ def _rename_state_dir(root: Path) -> Path:
116
+ return root / DIR_WORKFLOW / DIR_RUNTIME / DIR_SESSION_RENAME
117
+
118
+
119
+ def _load_rename_state(path: Path) -> dict[str, Any]:
120
+ if not path.is_file():
121
+ return {}
122
+ try:
123
+ data = json.loads(path.read_text(encoding="utf-8"))
124
+ except (json.JSONDecodeError, OSError):
125
+ return {}
126
+ return data if isinstance(data, dict) else {}
127
+
128
+
129
+ def _should_emit_rename(state: dict[str, Any], title: str) -> bool:
130
+ return state.get("last_title") != title
131
+
132
+
133
+ def _write_rename_state(path: Path, context_key: str, title: str) -> None:
134
+ path.parent.mkdir(parents=True, exist_ok=True)
135
+ payload = {
136
+ "context_key": context_key,
137
+ "last_title": title,
138
+ }
139
+ path.write_text(
140
+ json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
141
+ encoding="utf-8",
142
+ )
143
+
144
+
145
+ def _agent_rename_message(title: str) -> str:
146
+ return (
147
+ "Trellis session rename: call cursor-app-control MCP `rename_chat` once "
148
+ f'with title "{title}" (task directory name). '
149
+ "If rename_chat is not in your tool list, skip silently — do not ask the user "
150
+ "to rename manually."
151
+ )
152
+
153
+
154
+ def _load_context_key(root: Path, hook_input: dict[str, Any]) -> str | None:
155
+ scripts_dir = root / DIR_WORKFLOW / "scripts"
156
+ if str(scripts_dir) not in sys.path:
157
+ sys.path.insert(0, str(scripts_dir))
158
+ from common.active_task import resolve_context_key # type: ignore[import-not-found]
159
+
160
+ if not any(_string_value(hook_input.get(key)) for key in CONTEXT_IDENTITY_KEYS):
161
+ return None
162
+ return resolve_context_key(hook_input, platform="cursor")
163
+
164
+
165
+ def main() -> int:
166
+ if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
167
+ print("{}")
168
+ return 0
169
+
170
+ try:
171
+ hook_input = json.loads(sys.stdin.read())
172
+ except (json.JSONDecodeError, ValueError):
173
+ hook_input = {}
174
+ if not isinstance(hook_input, dict):
175
+ hook_input = {}
176
+
177
+ event = _string_value(hook_input.get("hook_event_name")) or "afterShellExecution"
178
+ if event != "afterShellExecution":
179
+ print("{}")
180
+ return 0
181
+
182
+ command = _string_value(hook_input.get("command")) or ""
183
+ intent = parse_rename_intent(command)
184
+ if intent is None:
185
+ print("{}")
186
+ return 0
187
+
188
+ output = _string_value(hook_input.get("output")) or ""
189
+ if not shell_output_succeeded(intent["subcommand"], output):
190
+ print("{}")
191
+ return 0
192
+
193
+ title = task_directory_name(intent["task_ref"])
194
+ if not title:
195
+ print("{}")
196
+ return 0
197
+
198
+ cwd = Path(_string_value(hook_input.get("cwd")) or os.getcwd())
199
+ root = _find_trellis_root(cwd)
200
+ if root is None:
201
+ print("{}")
202
+ return 0
203
+
204
+ context_key = _load_context_key(root, hook_input)
205
+ if not context_key:
206
+ print("{}")
207
+ return 0
208
+
209
+ state_path = _rename_state_dir(root) / _safe_context_filename(context_key)
210
+ state = _load_rename_state(state_path)
211
+ if not _should_emit_rename(state, title):
212
+ print("{}")
213
+ return 0
214
+
215
+ try:
216
+ _write_rename_state(state_path, context_key, title)
217
+ except OSError:
218
+ print("{}")
219
+ return 0
220
+
221
+ print(
222
+ json.dumps(
223
+ {"agent_message": _agent_rename_message(title)},
224
+ ensure_ascii=False,
225
+ ),
226
+ )
227
+ return 0
228
+
229
+
230
+ if __name__ == "__main__":
231
+ sys.exit(main())
@@ -1,61 +1,61 @@
1
- # Design — Release execution
2
-
3
- ## Boundary
4
-
5
- This task **may** mutate remotes **only** after explicit user approval captured in `verify.md` (quote or paraphrase the approval request/response).
6
-
7
- ## Release status model (this task)
8
-
9
- | Status | When to use |
10
- | --- | --- |
11
- | **published** | npm and/or git remote actions completed for the approved version |
12
- | **not published** | Execution aborted or only dry-run/preflight completed |
13
- | **blocked** | Preflight failed; do not publish until fixed |
14
- | **waived** | User approved proceeding despite a listed risk |
15
- | **deferred** | User postponed publish after readiness |
16
-
17
- Do not mark **ready to publish** here — that remains the readiness task outcome.
18
-
19
- ## Approval gate (mandatory)
20
-
21
- Before any of the following, stop and obtain explicit user approval:
22
-
23
- - `node scripts/release.js …` (bundles bump, commit, tag, push)
24
- - `npm publish` / CI publish workflow dispatch
25
- - `git tag` / `git push` (including `--tags`)
26
- - GitHub **Create release** or promotion workflows
27
-
28
- Document in `verify.md`:
29
-
30
- ```markdown
31
- ## Publish approval evidence
32
-
33
- User explicitly approved: yes/no
34
- Approved version: …
35
- Approved dist-tag: …
36
- Approved remote actions: tag / push / npm publish / …
37
- ```
38
-
39
- ## Evidence contract (`verify.md`)
40
-
41
- 1. **Readiness link** — path to readiness `handoff.md`.
42
- 2. **Publish approval evidence** — as above.
43
- 3. **Preflight results** — `check-versions`, `publish-plan`, `verify-packed-cli`.
44
- 4. **Execution log** — commands run (redact tokens); pass/fail.
45
- 5. **Post-publish smoke** — registry check, `trellis --version`, dogfood smoke commands.
46
- 6. **Validation evidence** — tests run after publish if applicable.
47
-
48
- ## Trellis CLI reference sequence (after approval)
49
-
50
- Typical human/CI sequence (do not automate without approval):
51
-
52
- ```bash
53
- cd packages/cli
54
- node scripts/release-preflight.js check-versions
55
- node scripts/release-preflight.js publish-plan
56
- # After approval:
57
- pnpm --filter @blxzer/cursor-trellis release:beta # or patch/minor/major per policy
58
- node scripts/release-preflight.js verify-npm --package all
59
- ```
60
-
1
+ # Design — Release execution
2
+
3
+ ## Boundary
4
+
5
+ This task **may** mutate remotes **only** after explicit user approval captured in `verify.md` (quote or paraphrase the approval request/response).
6
+
7
+ ## Release status model (this task)
8
+
9
+ | Status | When to use |
10
+ | --- | --- |
11
+ | **published** | npm and/or git remote actions completed for the approved version |
12
+ | **not published** | Execution aborted or only dry-run/preflight completed |
13
+ | **blocked** | Preflight failed; do not publish until fixed |
14
+ | **waived** | User approved proceeding despite a listed risk |
15
+ | **deferred** | User postponed publish after readiness |
16
+
17
+ Do not mark **ready to publish** here — that remains the readiness task outcome.
18
+
19
+ ## Approval gate (mandatory)
20
+
21
+ Before any of the following, stop and obtain explicit user approval:
22
+
23
+ - `node scripts/release.js …` (bundles bump, commit, tag, push)
24
+ - `npm publish` / CI publish workflow dispatch
25
+ - `git tag` / `git push` (including `--tags`)
26
+ - GitHub **Create release** or promotion workflows
27
+
28
+ Document in `verify.md`:
29
+
30
+ ```markdown
31
+ ## Publish approval evidence
32
+
33
+ User explicitly approved: yes/no
34
+ Approved version: …
35
+ Approved dist-tag: …
36
+ Approved remote actions: tag / push / npm publish / …
37
+ ```
38
+
39
+ ## Evidence contract (`verify.md`)
40
+
41
+ 1. **Readiness link** — path to readiness `handoff.md`.
42
+ 2. **Publish approval evidence** — as above.
43
+ 3. **Preflight results** — `check-versions`, `publish-plan`, `verify-packed-cli`.
44
+ 4. **Execution log** — commands run (redact tokens); pass/fail.
45
+ 5. **Post-publish smoke** — registry check, `cstl --version`, dogfood smoke commands.
46
+ 6. **Validation evidence** — tests run after publish if applicable.
47
+
48
+ ## Trellis CLI reference sequence (after approval)
49
+
50
+ Typical human/CI sequence (do not automate without approval):
51
+
52
+ ```bash
53
+ cd packages/cli
54
+ node scripts/release-preflight.js check-versions
55
+ node scripts/release-preflight.js publish-plan
56
+ # After approval:
57
+ pnpm --filter @blxzer/cursor-trellis release:beta # or patch/minor/major per policy
58
+ node scripts/release-preflight.js verify-npm --package all
59
+ ```
60
+
61
61
  `release.js` performs commit, tag, and push — treat it as a single approved bundle or decompose manually per policy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blxzer/cursor-trellis",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",
@@ -35,7 +35,7 @@
35
35
  "inquirer": "^9.3.7",
36
36
  "undici": "^6.21.0",
37
37
  "zod": "^4.4.2",
38
- "@blxzer/cursor-trellis-core": "0.3.3"
38
+ "@blxzer/cursor-trellis-core": "0.3.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@eslint/js": "^9.18.0",