@blxzer/cursor-trellis 0.2.9 → 0.3.2

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.
Files changed (234) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +85 -19
  3. package/README.zh-CN.md +60 -21
  4. package/bin/smart-search.js +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +46 -18
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/capability-smoke.d.ts +24 -0
  9. package/dist/commands/capability-smoke.d.ts.map +1 -0
  10. package/dist/commands/capability-smoke.js +83 -0
  11. package/dist/commands/capability-smoke.js.map +1 -0
  12. package/dist/commands/channel/agent-loader.d.ts +1 -1
  13. package/dist/commands/channel/agent-loader.js +4 -4
  14. package/dist/commands/channel/agent-loader.js.map +1 -1
  15. package/dist/commands/channel/guard.d.ts +2 -2
  16. package/dist/commands/channel/guard.js +3 -3
  17. package/dist/commands/channel/guard.js.map +1 -1
  18. package/dist/commands/channel/index.js +3 -3
  19. package/dist/commands/channel/index.js.map +1 -1
  20. package/dist/commands/channel/list.d.ts +1 -1
  21. package/dist/commands/channel/list.js +1 -1
  22. package/dist/commands/channel/rm.d.ts +1 -1
  23. package/dist/commands/channel/rm.js +1 -1
  24. package/dist/commands/channel/store/lock.d.ts +1 -1
  25. package/dist/commands/channel/store/lock.js +1 -1
  26. package/dist/commands/channel/store/paths.js +1 -1
  27. package/dist/commands/channel/store/paths.js.map +1 -1
  28. package/dist/commands/init.d.ts.map +1 -1
  29. package/dist/commands/init.js +197 -91
  30. package/dist/commands/init.js.map +1 -1
  31. package/dist/commands/uninstall.d.ts +4 -4
  32. package/dist/commands/uninstall.js +12 -12
  33. package/dist/commands/uninstall.js.map +1 -1
  34. package/dist/commands/update.d.ts +7 -2
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/update.js +161 -61
  37. package/dist/commands/update.js.map +1 -1
  38. package/dist/commands/upgrade.js +2 -2
  39. package/dist/commands/upgrade.js.map +1 -1
  40. package/dist/commands/workflow.d.ts +4 -4
  41. package/dist/commands/workflow.d.ts.map +1 -1
  42. package/dist/commands/workflow.js +10 -9
  43. package/dist/commands/workflow.js.map +1 -1
  44. package/dist/configurators/cursor.d.ts +2 -2
  45. package/dist/configurators/cursor.js +4 -4
  46. package/dist/configurators/cursor.js.map +1 -1
  47. package/dist/configurators/index.d.ts.map +1 -1
  48. package/dist/configurators/index.js +4 -4
  49. package/dist/configurators/index.js.map +1 -1
  50. package/dist/configurators/shared.d.ts +10 -10
  51. package/dist/configurators/shared.d.ts.map +1 -1
  52. package/dist/configurators/shared.js +32 -32
  53. package/dist/configurators/shared.js.map +1 -1
  54. package/dist/configurators/workflow.d.ts +6 -6
  55. package/dist/configurators/workflow.js +1 -1
  56. package/dist/constants/paths.d.ts +24 -24
  57. package/dist/constants/paths.d.ts.map +1 -1
  58. package/dist/constants/paths.js +16 -16
  59. package/dist/constants/paths.js.map +1 -1
  60. package/dist/migrations/manifests/0.2.10.json +374 -0
  61. package/dist/migrations/manifests/0.3.0.json +89 -0
  62. package/dist/migrations/manifests/0.3.1.json +32 -0
  63. package/dist/templates/common/bundled-skills/{trellis-cursor2plus-setup → cstl-cursor2plus-setup}/SKILL.md +15 -14
  64. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/SKILL.md +75 -75
  65. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/add-project-local-conventions.md +19 -19
  66. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-agents.md +8 -8
  67. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-context-loading.md +26 -26
  68. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-hooks.md +9 -9
  69. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-skills-or-commands.md +8 -8
  70. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-spec-structure.md +17 -17
  71. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-task-lifecycle.md +46 -46
  72. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-workflow.md +18 -18
  73. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/overview.md +55 -55
  74. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/context-injection.md +20 -20
  75. package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/generated-files.md +83 -0
  76. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/overview.md +15 -15
  77. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/spec-system.md +12 -12
  78. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/task-system.md +34 -34
  79. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workflow.md +8 -8
  80. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workspace-memory.md +19 -19
  81. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/agents.md +12 -12
  82. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/hooks-and-settings.md +7 -7
  83. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/overview.md +9 -9
  84. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/platform-map.md +7 -7
  85. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/skills-and-commands.md +19 -19
  86. package/dist/templates/common/bundled-skills/{trellis-micro-grill → cstl-micro-grill}/SKILL.md +62 -47
  87. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/SKILL.md +44 -44
  88. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/authoring-rules.md +97 -97
  89. package/dist/templates/common/bundled-skills/{trellis-skill-creator/references/trellis-skill-locations.md → cstl-skill-creator/references/cstl-skill-locations.md} +52 -52
  90. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/review-checklist.md +47 -47
  91. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/SKILL.md +6 -6
  92. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/repository-analysis.md +1 -1
  93. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-task-planning.md +3 -3
  94. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-writing.md +1 -1
  95. package/dist/templates/common/commands/continue.md +24 -24
  96. package/dist/templates/common/commands/finish-work.md +5 -5
  97. package/dist/templates/common/commands/start.md +20 -20
  98. package/dist/templates/common/index.d.ts +1 -1
  99. package/dist/templates/common/index.d.ts.map +1 -1
  100. package/dist/templates/common/skills/before-dev.md +4 -4
  101. package/dist/templates/common/skills/brainstorm.md +188 -164
  102. package/dist/templates/common/skills/break-loop.md +3 -3
  103. package/dist/templates/common/skills/check.md +5 -5
  104. package/dist/templates/common/skills/update-spec.md +4 -4
  105. package/dist/templates/cursor/agents/{trellis-check.md → cstl-check.md} +14 -14
  106. package/dist/templates/cursor/agents/{trellis-implement.md → cstl-implement.md} +14 -14
  107. package/dist/templates/cursor/agents/{trellis-research.md → cstl-research.md} +11 -11
  108. package/dist/templates/cursor/commands/cursor2plus-setup.md +15 -15
  109. package/dist/templates/cursor/fixtures/expected-rules.d.ts +1 -1
  110. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  111. package/dist/templates/cursor/fixtures/expected-rules.js +2 -2
  112. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  113. package/dist/templates/cursor/rules/{trellis-subagent-dispatch.mdc → cstl-subagent-dispatch.mdc} +4 -4
  114. package/dist/templates/cursor/rules/{trellis-triage.mdc → cstl-triage.mdc} +3 -3
  115. package/dist/templates/cursor/rules/retrieval-routing.mdc +4 -4
  116. package/dist/templates/extract.d.ts +1 -1
  117. package/dist/templates/extract.js +1 -1
  118. package/dist/templates/markdown/agents.md +13 -13
  119. package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +1 -1
  120. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +634 -634
  121. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +5 -5
  122. package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -1
  123. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +393 -391
  124. package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +14 -14
  125. package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +5 -5
  126. package/dist/templates/markdown/spec/guides/index.md.txt +3 -2
  127. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +10 -10
  128. package/dist/templates/markdown/workspace-index.md +3 -3
  129. package/dist/templates/markdown/worktree.yaml.txt +1 -1
  130. package/dist/templates/shared-hooks/index.d.ts +2 -2
  131. package/dist/templates/shared-hooks/index.js +2 -2
  132. package/dist/templates/shared-hooks/inject-retrieval-plan.py +3 -3
  133. package/dist/templates/shared-hooks/inject-shell-session-context.py +7 -7
  134. package/dist/templates/shared-hooks/inject-subagent-context.py +4 -4
  135. package/dist/templates/shared-hooks/inject-workflow-state.py +37 -37
  136. package/dist/templates/shared-hooks/research-end-retrieval-pack.py +2 -2
  137. package/dist/templates/shared-hooks/session-start.py +16 -16
  138. package/dist/templates/trellis/config/execution-strategy-rules.json +1 -1
  139. package/dist/templates/trellis/config.yaml +5 -25
  140. package/dist/templates/trellis/index.d.ts +2 -2
  141. package/dist/templates/trellis/index.js +2 -2
  142. package/dist/templates/trellis/local/README.md +15 -13
  143. package/dist/templates/trellis/local/patch_wpelc8.py +7 -7
  144. package/dist/templates/trellis/local/subagent-models.json.example +1 -1
  145. package/dist/templates/trellis/local/trellis-task-models.json.example +3 -3
  146. package/dist/templates/trellis/local/trellis-task-models.json5.example +5 -5
  147. package/dist/templates/trellis/local/trellis_task_models_config.py +6 -6
  148. package/dist/templates/trellis/scripts/add_session.py +20 -20
  149. package/dist/templates/trellis/scripts/common/active_task.py +2 -2
  150. package/dist/templates/trellis/scripts/common/artifact_search.py +587 -587
  151. package/dist/templates/trellis/scripts/common/cli_adapter.py +3 -3
  152. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +6 -6
  153. package/dist/templates/trellis/scripts/common/config.py +3 -3
  154. package/dist/templates/trellis/scripts/common/developer.py +2 -2
  155. package/dist/templates/trellis/scripts/common/execution_strategy.py +1 -1
  156. package/dist/templates/trellis/scripts/common/packages_context.py +23 -23
  157. package/dist/templates/trellis/scripts/common/parent_orchestration.py +702 -702
  158. package/dist/templates/trellis/scripts/common/paths.py +107 -107
  159. package/dist/templates/trellis/scripts/common/project_file_stats.py +1 -1
  160. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +1 -1
  161. package/dist/templates/trellis/scripts/common/safe_commit.py +31 -31
  162. package/dist/templates/trellis/scripts/common/session_context.py +1159 -1159
  163. package/dist/templates/trellis/scripts/common/session_memory.py +360 -360
  164. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +514 -514
  165. package/dist/templates/trellis/scripts/common/smart_search_resolve.py +1 -1
  166. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +7 -7
  167. package/dist/templates/trellis/scripts/common/task_context.py +1 -1
  168. package/dist/templates/trellis/scripts/common/task_dashboard.py +140 -140
  169. package/dist/templates/trellis/scripts/common/task_gates.py +2056 -2034
  170. package/dist/templates/trellis/scripts/common/task_store.py +1594 -1594
  171. package/dist/templates/trellis/scripts/common/task_utils.py +3 -3
  172. package/dist/templates/trellis/scripts/common/tasks.py +1 -1
  173. package/dist/templates/trellis/scripts/common/trellis_config.py +3 -3
  174. package/dist/templates/trellis/scripts/common/workflow_phase.py +1 -1
  175. package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +3 -3
  176. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +11 -11
  177. package/dist/templates/trellis/scripts/hooks/linear_sync.py +8 -8
  178. package/dist/templates/trellis/scripts/init_developer.py +2 -2
  179. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +4 -4
  180. package/dist/templates/trellis/scripts/route_codebase_retrieval.py +1 -1
  181. package/dist/templates/trellis/scripts/task.py +918 -918
  182. package/dist/templates/trellis/tasks/templates/release-readiness/implement.md +2 -2
  183. package/dist/templates/trellis/workflow.md +805 -805
  184. package/dist/types/ai-tools.d.ts +1 -1
  185. package/dist/types/ai-tools.d.ts.map +1 -1
  186. package/dist/types/ai-tools.js +1 -1
  187. package/dist/types/ai-tools.js.map +1 -1
  188. package/dist/types/migration.d.ts +1 -1
  189. package/dist/types/migration.d.ts.map +1 -1
  190. package/dist/utils/agents-md.d.ts +37 -0
  191. package/dist/utils/agents-md.d.ts.map +1 -0
  192. package/dist/utils/agents-md.js +82 -0
  193. package/dist/utils/agents-md.js.map +1 -0
  194. package/dist/utils/codebase-retrieval-router.js +5 -5
  195. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  196. package/dist/utils/manifest-prune.d.ts +2 -2
  197. package/dist/utils/manifest-prune.js +4 -4
  198. package/dist/utils/manifest-prune.js.map +1 -1
  199. package/dist/utils/mirror-check.d.ts +3 -1
  200. package/dist/utils/mirror-check.d.ts.map +1 -1
  201. package/dist/utils/mirror-check.js +22 -11
  202. package/dist/utils/mirror-check.js.map +1 -1
  203. package/dist/utils/project-capabilities.d.ts +14 -3
  204. package/dist/utils/project-capabilities.d.ts.map +1 -1
  205. package/dist/utils/project-capabilities.js +127 -17
  206. package/dist/utils/project-capabilities.js.map +1 -1
  207. package/dist/utils/readiness.d.ts +10 -0
  208. package/dist/utils/readiness.d.ts.map +1 -1
  209. package/dist/utils/readiness.js +6 -3
  210. package/dist/utils/readiness.js.map +1 -1
  211. package/dist/utils/retrieval-agent-instructions.js +1 -1
  212. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  213. package/dist/utils/retrieval-execution-telemetry.js +1 -1
  214. package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
  215. package/dist/utils/template-fetcher.js +1 -1
  216. package/dist/utils/template-fetcher.js.map +1 -1
  217. package/dist/utils/template-hash.d.ts +2 -2
  218. package/dist/utils/template-hash.d.ts.map +1 -1
  219. package/dist/utils/template-hash.js +12 -10
  220. package/dist/utils/template-hash.js.map +1 -1
  221. package/dist/utils/workflow-dir.d.ts +13 -0
  222. package/dist/utils/workflow-dir.d.ts.map +1 -0
  223. package/dist/utils/workflow-dir.js +35 -0
  224. package/dist/utils/workflow-dir.js.map +1 -0
  225. package/dist/utils/workflow-ownership.d.ts +36 -0
  226. package/dist/utils/workflow-ownership.d.ts.map +1 -0
  227. package/dist/utils/workflow-ownership.js +137 -0
  228. package/dist/utils/workflow-ownership.js.map +1 -0
  229. package/dist/utils/workflow-resolver.d.ts +1 -1
  230. package/dist/utils/workflow-resolver.js +1 -1
  231. package/package.json +3 -4
  232. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +0 -83
  233. /package/bin/{trellis.js → cstl.js} +0 -0
  234. /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/mcp-setup.md +0 -0
@@ -1,360 +1,360 @@
1
- #!/usr/bin/env python3
2
- """
3
- Search Trellis workspace session memory.
4
-
5
- This module parses `.trellis/workspace/<developer>/journal-*.md` files written
6
- by add_session.py and returns compact, explainable memory results.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import argparse
12
- import json
13
- import re
14
- import sys
15
- from dataclasses import dataclass, field
16
- from pathlib import Path
17
- from typing import Iterable
18
-
19
- from .paths import DIR_WORKFLOW, DIR_WORKSPACE, FILE_JOURNAL_PREFIX, get_developer, get_repo_root
20
-
21
-
22
- SECTION_NAMES = {"summary", "main changes", "git commits", "testing", "status", "next steps"}
23
- FIELD_WEIGHTS = {
24
- "title": 8,
25
- "task": 8,
26
- "summary": 6,
27
- "next steps": 6,
28
- "main changes": 4,
29
- "commits": 3,
30
- "package": 3,
31
- "branch": 3,
32
- "path": 3,
33
- }
34
-
35
-
36
- @dataclass
37
- class SessionEntry:
38
- developer: str
39
- session_number: int
40
- title: str
41
- date: str
42
- task: str
43
- package: str
44
- branch: str
45
- commits: list[str]
46
- sections: dict[str, str]
47
- path: Path
48
- rel_path: str
49
- line: int
50
-
51
-
52
- @dataclass
53
- class MemoryResult:
54
- entry: SessionEntry
55
- score: int
56
- matched_sections: set[str] = field(default_factory=set)
57
- matched_fields: set[str] = field(default_factory=set)
58
- reason_parts: list[str] = field(default_factory=list)
59
-
60
- def to_json(self) -> dict[str, object]:
61
- return {
62
- "version": 1,
63
- "source": "session-memory",
64
- "developer": self.entry.developer,
65
- "session": self.entry.session_number,
66
- "title": self.entry.title,
67
- "date": self.entry.date,
68
- "task": self.entry.task,
69
- "package": self.entry.package,
70
- "branch": self.entry.branch,
71
- "commits": self.entry.commits,
72
- "summary": normalize_space(self.entry.sections.get("summary", "")),
73
- "matchedSections": sorted(self.matched_sections),
74
- "matchedFields": sorted(self.matched_fields),
75
- "path": self.entry.rel_path,
76
- "line": self.entry.line,
77
- "score": self.score,
78
- "reason": "; ".join(self.reason_parts),
79
- }
80
-
81
-
82
- def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
83
- parser = argparse.ArgumentParser(description="Search Trellis session memory journals.")
84
- parser.add_argument("--query", "-q", default="", help="Whitespace-token OR query.")
85
- parser.add_argument(
86
- "--developer",
87
- action="append",
88
- default=[],
89
- help="Developer workspace to search. Defaults to current developer, then all.",
90
- )
91
- parser.add_argument("--task", help="Filter by task/title substring.")
92
- parser.add_argument("--package", help="Filter by package substring.")
93
- parser.add_argument("--branch", help="Filter by branch substring.")
94
- parser.add_argument("--since", help="Filter sessions on or after YYYY-MM-DD.")
95
- parser.add_argument("--until", help="Filter sessions on or before YYYY-MM-DD.")
96
- parser.add_argument("--limit", type=int, default=10, help="Maximum results. Use 0 for no limit.")
97
- parser.add_argument("--json", action="store_true", help="Emit stable JSON.")
98
- parser.add_argument("--root", help="Repository root. Defaults to nearest parent with .trellis/.")
99
- return parser.parse_args(argv)
100
-
101
-
102
- def main(argv: list[str] | None = None) -> int:
103
- args = parse_args(argv)
104
- if args.limit < 0:
105
- print("Error: --limit must be >= 0", file=sys.stderr)
106
- return 2
107
-
108
- repo_root = Path(args.root).resolve() if args.root else get_repo_root()
109
- results = search_memory(
110
- repo_root=repo_root,
111
- query=args.query,
112
- developers=args.developer,
113
- task_filter=args.task or "",
114
- package_filter=args.package or "",
115
- branch_filter=args.branch or "",
116
- since=args.since or "",
117
- until=args.until or "",
118
- limit=args.limit,
119
- )
120
- payload = {
121
- "query": args.query,
122
- "developers": args.developer,
123
- "filters": {
124
- "task": args.task or "",
125
- "package": args.package or "",
126
- "branch": args.branch or "",
127
- "since": args.since or "",
128
- "until": args.until or "",
129
- },
130
- "total": len(results),
131
- "results": [result.to_json() for result in results],
132
- }
133
- if args.json:
134
- print(json.dumps(payload, ensure_ascii=False, indent=2))
135
- else:
136
- print_human(payload)
137
- return 0
138
-
139
-
140
- def search_memory(
141
- repo_root: Path,
142
- query: str,
143
- developers: list[str],
144
- task_filter: str,
145
- package_filter: str,
146
- branch_filter: str,
147
- since: str,
148
- until: str,
149
- limit: int,
150
- ) -> list[MemoryResult]:
151
- tokens = tokenize(query)
152
- results: list[MemoryResult] = []
153
- for entry in iter_session_entries(repo_root, developers):
154
- if task_filter and task_filter.lower() not in entry.task.lower() and task_filter.lower() not in entry.title.lower():
155
- continue
156
- if package_filter and package_filter.lower() not in entry.package.lower():
157
- continue
158
- if branch_filter and branch_filter.lower() not in entry.branch.lower():
159
- continue
160
- if since and entry.date and entry.date < since:
161
- continue
162
- if until and entry.date and entry.date > until:
163
- continue
164
- result = score_entry(entry, tokens)
165
- if result is not None:
166
- results.append(result)
167
-
168
- results.sort(key=lambda item: (-item.score, -item.entry.session_number, item.entry.rel_path, item.entry.line))
169
- if limit:
170
- return results[:limit]
171
- return results
172
-
173
-
174
- def iter_session_entries(repo_root: Path, developers: list[str]) -> Iterable[SessionEntry]:
175
- workspace_root = repo_root / DIR_WORKFLOW / DIR_WORKSPACE
176
- if not workspace_root.is_dir():
177
- return
178
-
179
- selected_developers = developers or ([get_developer(repo_root)] if get_developer(repo_root) else [])
180
- developer_dirs: list[Path]
181
- if selected_developers:
182
- developer_dirs = [workspace_root / developer for developer in selected_developers if developer]
183
- else:
184
- developer_dirs = [path for path in sorted(workspace_root.iterdir()) if path.is_dir()]
185
-
186
- for developer_dir in developer_dirs:
187
- if not developer_dir.is_dir():
188
- continue
189
- developer = developer_dir.name
190
- for journal in sorted(developer_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"), key=journal_sort_key):
191
- yield from parse_journal(journal, repo_root, developer)
192
-
193
-
194
- def journal_sort_key(path: Path) -> tuple[int, str]:
195
- match = re.search(r"(\d+)$", path.stem)
196
- return (int(match.group(1)) if match else 0, path.name)
197
-
198
-
199
- def parse_journal(path: Path, repo_root: Path, developer: str) -> list[SessionEntry]:
200
- if not path.is_file():
201
- return []
202
- lines = path.read_text(encoding="utf-8").splitlines()
203
- entries: list[SessionEntry] = []
204
- current_start = 0
205
- current_header = ""
206
- current_lines: list[str] = []
207
- for index, line in enumerate(lines, start=1):
208
- if re.match(r"^## Session \d+:", line):
209
- if current_header:
210
- entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
211
- current_start = index
212
- current_header = line
213
- current_lines = []
214
- elif current_header:
215
- current_lines.append(line)
216
- if current_header:
217
- entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
218
- return entries
219
-
220
-
221
- def build_entry(
222
- path: Path,
223
- repo_root: Path,
224
- developer: str,
225
- line: int,
226
- header: str,
227
- lines: list[str],
228
- ) -> SessionEntry:
229
- match = re.match(r"^## Session (\d+):\s*(.+?)\s*$", header)
230
- session_number = int(match.group(1)) if match else 0
231
- title = match.group(2).strip() if match else header.strip("# ")
232
- fields = extract_fields(lines)
233
- sections = extract_sections(lines)
234
- commits = extract_commits(sections.get("git commits", ""))
235
- return SessionEntry(
236
- developer=developer,
237
- session_number=session_number,
238
- title=title,
239
- date=fields.get("date", ""),
240
- task=fields.get("task", title),
241
- package=fields.get("package", ""),
242
- branch=fields.get("branch", ""),
243
- commits=commits,
244
- sections=sections,
245
- path=path,
246
- rel_path=to_repo_path(path, repo_root),
247
- line=line,
248
- )
249
-
250
-
251
- def extract_fields(lines: list[str]) -> dict[str, str]:
252
- fields: dict[str, str] = {}
253
- for line in lines:
254
- match = re.match(r"^\*\*(Date|Task|Package|Branch)\*\*:\s*(.+?)\s*$", line)
255
- if not match:
256
- continue
257
- key = match.group(1).lower()
258
- value = match.group(2).strip()
259
- if key == "branch":
260
- value = value.strip("`")
261
- fields[key] = value
262
- return fields
263
-
264
-
265
- def extract_sections(lines: list[str]) -> dict[str, str]:
266
- sections: dict[str, list[str]] = {}
267
- current: str | None = None
268
- for line in lines:
269
- heading = re.match(r"^###\s+(.+?)\s*$", line)
270
- if heading:
271
- name = heading.group(1).strip().lower()
272
- current = name if name in SECTION_NAMES else name
273
- sections.setdefault(current, [])
274
- continue
275
- if current:
276
- sections[current].append(line)
277
- return {key: "\n".join(value).strip() for key, value in sections.items()}
278
-
279
-
280
- def extract_commits(content: str) -> list[str]:
281
- return sorted(set(re.findall(r"`?([a-f0-9]{7,40})`?", content, flags=re.IGNORECASE)))
282
-
283
-
284
- def score_entry(entry: SessionEntry, tokens: list[str]) -> MemoryResult | None:
285
- result = MemoryResult(entry=entry, score=min(entry.session_number, 5))
286
- fields = searchable_fields(entry)
287
- if not tokens:
288
- result.reason_parts.append("recent session memory")
289
- return result
290
-
291
- matched_any = False
292
- for token in tokens:
293
- token_matched = False
294
- for field, value in fields:
295
- if token not in value.lower():
296
- continue
297
- token_matched = True
298
- matched_any = True
299
- result.score += FIELD_WEIGHTS.get(field, 2)
300
- if field in SECTION_NAMES:
301
- result.matched_sections.add(display_section_name(field))
302
- else:
303
- result.matched_fields.add(field)
304
- if token_matched:
305
- result.reason_parts.append(f"matched '{token}'")
306
-
307
- if not matched_any:
308
- return None
309
- if not result.reason_parts:
310
- result.reason_parts.append("matched session memory")
311
- return result
312
-
313
-
314
- def searchable_fields(entry: SessionEntry) -> list[tuple[str, str]]:
315
- fields = [
316
- ("title", entry.title),
317
- ("task", entry.task),
318
- ("package", entry.package),
319
- ("branch", entry.branch),
320
- ("commits", " ".join(entry.commits)),
321
- ("path", entry.rel_path),
322
- ]
323
- for section, content in entry.sections.items():
324
- fields.append((section, content))
325
- return fields
326
-
327
-
328
- def display_section_name(name: str) -> str:
329
- return " ".join(part.capitalize() for part in name.split())
330
-
331
-
332
- def tokenize(query: str) -> list[str]:
333
- return [token.lower() for token in re.findall(r"\S+", query) if token.strip()]
334
-
335
-
336
- def normalize_space(value: str) -> str:
337
- return re.sub(r"\s+", " ", value).strip()
338
-
339
-
340
- def to_repo_path(path: Path, repo_root: Path) -> str:
341
- try:
342
- return path.resolve().relative_to(repo_root.resolve()).as_posix()
343
- except ValueError:
344
- return str(path)
345
-
346
-
347
- def print_human(payload: dict[str, object]) -> None:
348
- results = payload["results"]
349
- if not isinstance(results, list) or not results:
350
- print("No session memory results.")
351
- return
352
- for index, raw in enumerate(results, start=1):
353
- if not isinstance(raw, dict):
354
- continue
355
- print(f"{index}. {raw.get('title')} ({raw.get('date') or 'unknown date'})")
356
- print(f" Path: {raw.get('path')}:{raw.get('line')}")
357
- print(f" Score: {raw.get('score')} - {raw.get('reason')}")
358
- summary = raw.get("summary")
359
- if summary:
360
- print(f" Summary: {summary}")
1
+ #!/usr/bin/env python3
2
+ """
3
+ Search Trellis workspace session memory.
4
+
5
+ This module parses `.cstl/workspace/<developer>/journal-*.md` files written
6
+ by add_session.py and returns compact, explainable memory results.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import sys
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Iterable
18
+
19
+ from .paths import DIR_WORKFLOW, DIR_WORKSPACE, FILE_JOURNAL_PREFIX, get_developer, get_repo_root
20
+
21
+
22
+ SECTION_NAMES = {"summary", "main changes", "git commits", "testing", "status", "next steps"}
23
+ FIELD_WEIGHTS = {
24
+ "title": 8,
25
+ "task": 8,
26
+ "summary": 6,
27
+ "next steps": 6,
28
+ "main changes": 4,
29
+ "commits": 3,
30
+ "package": 3,
31
+ "branch": 3,
32
+ "path": 3,
33
+ }
34
+
35
+
36
+ @dataclass
37
+ class SessionEntry:
38
+ developer: str
39
+ session_number: int
40
+ title: str
41
+ date: str
42
+ task: str
43
+ package: str
44
+ branch: str
45
+ commits: list[str]
46
+ sections: dict[str, str]
47
+ path: Path
48
+ rel_path: str
49
+ line: int
50
+
51
+
52
+ @dataclass
53
+ class MemoryResult:
54
+ entry: SessionEntry
55
+ score: int
56
+ matched_sections: set[str] = field(default_factory=set)
57
+ matched_fields: set[str] = field(default_factory=set)
58
+ reason_parts: list[str] = field(default_factory=list)
59
+
60
+ def to_json(self) -> dict[str, object]:
61
+ return {
62
+ "version": 1,
63
+ "source": "session-memory",
64
+ "developer": self.entry.developer,
65
+ "session": self.entry.session_number,
66
+ "title": self.entry.title,
67
+ "date": self.entry.date,
68
+ "task": self.entry.task,
69
+ "package": self.entry.package,
70
+ "branch": self.entry.branch,
71
+ "commits": self.entry.commits,
72
+ "summary": normalize_space(self.entry.sections.get("summary", "")),
73
+ "matchedSections": sorted(self.matched_sections),
74
+ "matchedFields": sorted(self.matched_fields),
75
+ "path": self.entry.rel_path,
76
+ "line": self.entry.line,
77
+ "score": self.score,
78
+ "reason": "; ".join(self.reason_parts),
79
+ }
80
+
81
+
82
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
83
+ parser = argparse.ArgumentParser(description="Search Trellis session memory journals.")
84
+ parser.add_argument("--query", "-q", default="", help="Whitespace-token OR query.")
85
+ parser.add_argument(
86
+ "--developer",
87
+ action="append",
88
+ default=[],
89
+ help="Developer workspace to search. Defaults to current developer, then all.",
90
+ )
91
+ parser.add_argument("--task", help="Filter by task/title substring.")
92
+ parser.add_argument("--package", help="Filter by package substring.")
93
+ parser.add_argument("--branch", help="Filter by branch substring.")
94
+ parser.add_argument("--since", help="Filter sessions on or after YYYY-MM-DD.")
95
+ parser.add_argument("--until", help="Filter sessions on or before YYYY-MM-DD.")
96
+ parser.add_argument("--limit", type=int, default=10, help="Maximum results. Use 0 for no limit.")
97
+ parser.add_argument("--json", action="store_true", help="Emit stable JSON.")
98
+ parser.add_argument("--root", help="Repository root. Defaults to nearest parent with .cstl/.")
99
+ return parser.parse_args(argv)
100
+
101
+
102
+ def main(argv: list[str] | None = None) -> int:
103
+ args = parse_args(argv)
104
+ if args.limit < 0:
105
+ print("Error: --limit must be >= 0", file=sys.stderr)
106
+ return 2
107
+
108
+ repo_root = Path(args.root).resolve() if args.root else get_repo_root()
109
+ results = search_memory(
110
+ repo_root=repo_root,
111
+ query=args.query,
112
+ developers=args.developer,
113
+ task_filter=args.task or "",
114
+ package_filter=args.package or "",
115
+ branch_filter=args.branch or "",
116
+ since=args.since or "",
117
+ until=args.until or "",
118
+ limit=args.limit,
119
+ )
120
+ payload = {
121
+ "query": args.query,
122
+ "developers": args.developer,
123
+ "filters": {
124
+ "task": args.task or "",
125
+ "package": args.package or "",
126
+ "branch": args.branch or "",
127
+ "since": args.since or "",
128
+ "until": args.until or "",
129
+ },
130
+ "total": len(results),
131
+ "results": [result.to_json() for result in results],
132
+ }
133
+ if args.json:
134
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
135
+ else:
136
+ print_human(payload)
137
+ return 0
138
+
139
+
140
+ def search_memory(
141
+ repo_root: Path,
142
+ query: str,
143
+ developers: list[str],
144
+ task_filter: str,
145
+ package_filter: str,
146
+ branch_filter: str,
147
+ since: str,
148
+ until: str,
149
+ limit: int,
150
+ ) -> list[MemoryResult]:
151
+ tokens = tokenize(query)
152
+ results: list[MemoryResult] = []
153
+ for entry in iter_session_entries(repo_root, developers):
154
+ if task_filter and task_filter.lower() not in entry.task.lower() and task_filter.lower() not in entry.title.lower():
155
+ continue
156
+ if package_filter and package_filter.lower() not in entry.package.lower():
157
+ continue
158
+ if branch_filter and branch_filter.lower() not in entry.branch.lower():
159
+ continue
160
+ if since and entry.date and entry.date < since:
161
+ continue
162
+ if until and entry.date and entry.date > until:
163
+ continue
164
+ result = score_entry(entry, tokens)
165
+ if result is not None:
166
+ results.append(result)
167
+
168
+ results.sort(key=lambda item: (-item.score, -item.entry.session_number, item.entry.rel_path, item.entry.line))
169
+ if limit:
170
+ return results[:limit]
171
+ return results
172
+
173
+
174
+ def iter_session_entries(repo_root: Path, developers: list[str]) -> Iterable[SessionEntry]:
175
+ workspace_root = repo_root / DIR_WORKFLOW / DIR_WORKSPACE
176
+ if not workspace_root.is_dir():
177
+ return
178
+
179
+ selected_developers = developers or ([get_developer(repo_root)] if get_developer(repo_root) else [])
180
+ developer_dirs: list[Path]
181
+ if selected_developers:
182
+ developer_dirs = [workspace_root / developer for developer in selected_developers if developer]
183
+ else:
184
+ developer_dirs = [path for path in sorted(workspace_root.iterdir()) if path.is_dir()]
185
+
186
+ for developer_dir in developer_dirs:
187
+ if not developer_dir.is_dir():
188
+ continue
189
+ developer = developer_dir.name
190
+ for journal in sorted(developer_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"), key=journal_sort_key):
191
+ yield from parse_journal(journal, repo_root, developer)
192
+
193
+
194
+ def journal_sort_key(path: Path) -> tuple[int, str]:
195
+ match = re.search(r"(\d+)$", path.stem)
196
+ return (int(match.group(1)) if match else 0, path.name)
197
+
198
+
199
+ def parse_journal(path: Path, repo_root: Path, developer: str) -> list[SessionEntry]:
200
+ if not path.is_file():
201
+ return []
202
+ lines = path.read_text(encoding="utf-8").splitlines()
203
+ entries: list[SessionEntry] = []
204
+ current_start = 0
205
+ current_header = ""
206
+ current_lines: list[str] = []
207
+ for index, line in enumerate(lines, start=1):
208
+ if re.match(r"^## Session \d+:", line):
209
+ if current_header:
210
+ entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
211
+ current_start = index
212
+ current_header = line
213
+ current_lines = []
214
+ elif current_header:
215
+ current_lines.append(line)
216
+ if current_header:
217
+ entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
218
+ return entries
219
+
220
+
221
+ def build_entry(
222
+ path: Path,
223
+ repo_root: Path,
224
+ developer: str,
225
+ line: int,
226
+ header: str,
227
+ lines: list[str],
228
+ ) -> SessionEntry:
229
+ match = re.match(r"^## Session (\d+):\s*(.+?)\s*$", header)
230
+ session_number = int(match.group(1)) if match else 0
231
+ title = match.group(2).strip() if match else header.strip("# ")
232
+ fields = extract_fields(lines)
233
+ sections = extract_sections(lines)
234
+ commits = extract_commits(sections.get("git commits", ""))
235
+ return SessionEntry(
236
+ developer=developer,
237
+ session_number=session_number,
238
+ title=title,
239
+ date=fields.get("date", ""),
240
+ task=fields.get("task", title),
241
+ package=fields.get("package", ""),
242
+ branch=fields.get("branch", ""),
243
+ commits=commits,
244
+ sections=sections,
245
+ path=path,
246
+ rel_path=to_repo_path(path, repo_root),
247
+ line=line,
248
+ )
249
+
250
+
251
+ def extract_fields(lines: list[str]) -> dict[str, str]:
252
+ fields: dict[str, str] = {}
253
+ for line in lines:
254
+ match = re.match(r"^\*\*(Date|Task|Package|Branch)\*\*:\s*(.+?)\s*$", line)
255
+ if not match:
256
+ continue
257
+ key = match.group(1).lower()
258
+ value = match.group(2).strip()
259
+ if key == "branch":
260
+ value = value.strip("`")
261
+ fields[key] = value
262
+ return fields
263
+
264
+
265
+ def extract_sections(lines: list[str]) -> dict[str, str]:
266
+ sections: dict[str, list[str]] = {}
267
+ current: str | None = None
268
+ for line in lines:
269
+ heading = re.match(r"^###\s+(.+?)\s*$", line)
270
+ if heading:
271
+ name = heading.group(1).strip().lower()
272
+ current = name if name in SECTION_NAMES else name
273
+ sections.setdefault(current, [])
274
+ continue
275
+ if current:
276
+ sections[current].append(line)
277
+ return {key: "\n".join(value).strip() for key, value in sections.items()}
278
+
279
+
280
+ def extract_commits(content: str) -> list[str]:
281
+ return sorted(set(re.findall(r"`?([a-f0-9]{7,40})`?", content, flags=re.IGNORECASE)))
282
+
283
+
284
+ def score_entry(entry: SessionEntry, tokens: list[str]) -> MemoryResult | None:
285
+ result = MemoryResult(entry=entry, score=min(entry.session_number, 5))
286
+ fields = searchable_fields(entry)
287
+ if not tokens:
288
+ result.reason_parts.append("recent session memory")
289
+ return result
290
+
291
+ matched_any = False
292
+ for token in tokens:
293
+ token_matched = False
294
+ for field, value in fields:
295
+ if token not in value.lower():
296
+ continue
297
+ token_matched = True
298
+ matched_any = True
299
+ result.score += FIELD_WEIGHTS.get(field, 2)
300
+ if field in SECTION_NAMES:
301
+ result.matched_sections.add(display_section_name(field))
302
+ else:
303
+ result.matched_fields.add(field)
304
+ if token_matched:
305
+ result.reason_parts.append(f"matched '{token}'")
306
+
307
+ if not matched_any:
308
+ return None
309
+ if not result.reason_parts:
310
+ result.reason_parts.append("matched session memory")
311
+ return result
312
+
313
+
314
+ def searchable_fields(entry: SessionEntry) -> list[tuple[str, str]]:
315
+ fields = [
316
+ ("title", entry.title),
317
+ ("task", entry.task),
318
+ ("package", entry.package),
319
+ ("branch", entry.branch),
320
+ ("commits", " ".join(entry.commits)),
321
+ ("path", entry.rel_path),
322
+ ]
323
+ for section, content in entry.sections.items():
324
+ fields.append((section, content))
325
+ return fields
326
+
327
+
328
+ def display_section_name(name: str) -> str:
329
+ return " ".join(part.capitalize() for part in name.split())
330
+
331
+
332
+ def tokenize(query: str) -> list[str]:
333
+ return [token.lower() for token in re.findall(r"\S+", query) if token.strip()]
334
+
335
+
336
+ def normalize_space(value: str) -> str:
337
+ return re.sub(r"\s+", " ", value).strip()
338
+
339
+
340
+ def to_repo_path(path: Path, repo_root: Path) -> str:
341
+ try:
342
+ return path.resolve().relative_to(repo_root.resolve()).as_posix()
343
+ except ValueError:
344
+ return str(path)
345
+
346
+
347
+ def print_human(payload: dict[str, object]) -> None:
348
+ results = payload["results"]
349
+ if not isinstance(results, list) or not results:
350
+ print("No session memory results.")
351
+ return
352
+ for index, raw in enumerate(results, start=1):
353
+ if not isinstance(raw, dict):
354
+ continue
355
+ print(f"{index}. {raw.get('title')} ({raw.get('date') or 'unknown date'})")
356
+ print(f" Path: {raw.get('path')}:{raw.get('line')}")
357
+ print(f" Score: {raw.get('score')} - {raw.get('reason')}")
358
+ summary = raw.get("summary")
359
+ if summary:
360
+ print(f" Summary: {summary}")