@xluos/dev-assets-cli 0.3.0 → 0.4.0
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/README.md +12 -0
- package/lib/dev_asset_common.py +75 -1
- package/package.json +6 -1
- package/scripts/hooks/_common.py +307 -0
- package/scripts/hooks/pre_compact.py +34 -0
- package/scripts/hooks/session_end.py +31 -0
- package/scripts/hooks/session_start.py +51 -0
- package/scripts/hooks/stop.py +31 -0
- package/skills/dev-assets-context/SKILL.md +2 -0
- package/skills/dev-assets-context/scripts/dev_asset_common.py +1 -1
- package/skills/dev-assets-setup/SKILL.md +2 -0
- package/skills/dev-assets-setup/scripts/dev_asset_common.py +1 -1
- package/skills/dev-assets-sync/SKILL.md +2 -0
- package/skills/dev-assets-sync/scripts/dev_asset_common.py +1 -1
- package/skills/dev-assets-update/SKILL.md +2 -0
- package/skills/dev-assets-update/scripts/dev_asset_common.py +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,18 @@ This repository packages a small skill suite for one job: keep development memor
|
|
|
13
13
|
Detailed guide:
|
|
14
14
|
|
|
15
15
|
- [docs/dev-asset-skill-suite-guide.md](docs/dev-asset-skill-suite-guide.md)
|
|
16
|
+
- [docs/workspace-mode.md](docs/workspace-mode.md) — multi-repo workspace mode (new)
|
|
17
|
+
|
|
18
|
+
## Workspace Mode
|
|
19
|
+
|
|
20
|
+
When cwd is not itself a git repo but its first-level subdirectories are (a "workspace" layout hosting multiple cloned repos), the suite switches to **workspace mode** automatically:
|
|
21
|
+
|
|
22
|
+
- `SessionStart`: injects full memory for the **primary repo** (from env `DEV_ASSETS_PRIMARY_REPO`, basename) plus a brief overview of the other repos; when no primary is set, all repos get the brief form.
|
|
23
|
+
- `Stop` / `SessionEnd`: records HEAD for every repo in the workspace (idempotent).
|
|
24
|
+
- `PreCompact`: refreshes working-tree navigation for every repo (Claude only).
|
|
25
|
+
- `dev-assets-sync` / `-context` / `-update`: pass `--repo <basename>` explicitly, or rely on `DEV_ASSETS_PRIMARY_REPO` for the default target.
|
|
26
|
+
|
|
27
|
+
Single-repo cwd behavior is unchanged. See [docs/workspace-mode.md](docs/workspace-mode.md) for the full design.
|
|
16
28
|
|
|
17
29
|
## Install with `npx skills`
|
|
18
30
|
|
package/lib/dev_asset_common.py
CHANGED
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -162,7 +162,34 @@ def detect_repo_identity(repo_root):
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
def _resolve_workspace_repo(repo):
|
|
166
|
+
"""If `repo` points to a workspace root (cwd is not a git repo, but first-level
|
|
167
|
+
subdirs are), redirect to the primary repo via `DEV_ASSETS_PRIMARY_REPO` env.
|
|
168
|
+
Single-repo case returns `repo` unchanged. Raises in workspace mode when
|
|
169
|
+
primary is unset or does not match an existing subdir, so callers see a
|
|
170
|
+
clear error instead of git failing later.
|
|
171
|
+
"""
|
|
172
|
+
if not detect_workspace_mode(repo):
|
|
173
|
+
return repo
|
|
174
|
+
primary = os.environ.get("DEV_ASSETS_PRIMARY_REPO", "").strip()
|
|
175
|
+
repos_in_ws = list_repos_in_workspace(repo)
|
|
176
|
+
names = [p.name for p in repos_in_ws]
|
|
177
|
+
if not primary:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
f"workspace mode detected at '{repo}': pass --repo <basename> explicitly "
|
|
180
|
+
f"(one of: {names}) or set DEV_ASSETS_PRIMARY_REPO env."
|
|
181
|
+
)
|
|
182
|
+
match = next((p for p in repos_in_ws if p.name == primary), None)
|
|
183
|
+
if match is None:
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
f"workspace mode: DEV_ASSETS_PRIMARY_REPO='{primary}' not found in '{repo}'. "
|
|
186
|
+
f"Available: {names}."
|
|
187
|
+
)
|
|
188
|
+
return str(match)
|
|
189
|
+
|
|
190
|
+
|
|
165
191
|
def get_branch_paths(repo, context_dir=None, branch=None):
|
|
192
|
+
repo = _resolve_workspace_repo(repo)
|
|
166
193
|
repo_root = detect_repo_root(repo)
|
|
167
194
|
branch_name = branch or detect_branch(repo_root)
|
|
168
195
|
branch_key = sanitize_branch_name(branch_name)
|
|
@@ -173,6 +200,53 @@ def get_branch_paths(repo, context_dir=None, branch=None):
|
|
|
173
200
|
return repo_root, branch_name, branch_key, storage_root, identity["repo_key"], repo_dir, branch_dir
|
|
174
201
|
|
|
175
202
|
|
|
203
|
+
def detect_workspace_mode(cwd=None):
|
|
204
|
+
"""Return True iff cwd is not inside any git repo yet has first-level
|
|
205
|
+
subdirectories that are git repos. Purely additive — existing single-repo
|
|
206
|
+
behavior (cwd inside a git repo) returns False.
|
|
207
|
+
"""
|
|
208
|
+
base = Path(cwd or ".").resolve()
|
|
209
|
+
if not base.exists() or not base.is_dir():
|
|
210
|
+
return False
|
|
211
|
+
probe = run_git(["rev-parse", "--show-toplevel"], cwd=base, check=False)
|
|
212
|
+
if probe.returncode == 0 and probe.stdout.strip():
|
|
213
|
+
return False
|
|
214
|
+
return bool(list_repos_in_workspace(base))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def list_repos_in_workspace(cwd=None):
|
|
218
|
+
"""First-level subdirectories of cwd that are git repos. Sorted by name.
|
|
219
|
+
Returns [] when cwd has none or isn't readable. `.git` may be a dir or a
|
|
220
|
+
file (worktree pointer); both count.
|
|
221
|
+
"""
|
|
222
|
+
base = Path(cwd or ".").resolve()
|
|
223
|
+
repos = []
|
|
224
|
+
try:
|
|
225
|
+
entries = sorted(base.iterdir(), key=lambda p: p.name)
|
|
226
|
+
except (OSError, PermissionError):
|
|
227
|
+
return []
|
|
228
|
+
for entry in entries:
|
|
229
|
+
if not entry.is_dir():
|
|
230
|
+
continue
|
|
231
|
+
if (entry / ".git").exists():
|
|
232
|
+
repos.append(entry)
|
|
233
|
+
return repos
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_all_branch_paths(cwd=None, context_dir=None):
|
|
237
|
+
"""Batch variant of get_branch_paths() for every repo under a workspace cwd.
|
|
238
|
+
Repos with detached HEAD or other resolution errors are skipped silently.
|
|
239
|
+
Returns [] when not in workspace mode.
|
|
240
|
+
"""
|
|
241
|
+
result = []
|
|
242
|
+
for repo_path in list_repos_in_workspace(cwd):
|
|
243
|
+
try:
|
|
244
|
+
result.append(get_branch_paths(str(repo_path), context_dir=context_dir))
|
|
245
|
+
except Exception:
|
|
246
|
+
continue
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
|
|
176
250
|
def asset_paths(repo_dir, branch_dir):
|
|
177
251
|
repo_memory_dir = repo_dir / "repo"
|
|
178
252
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xluos/dev-assets-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for dev-assets hooks and repo-local setup",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"hooks/*.json",
|
|
20
20
|
"hooks/README.md",
|
|
21
21
|
"lib/*.py",
|
|
22
|
+
"scripts/hooks/_common.py",
|
|
23
|
+
"scripts/hooks/session_start.py",
|
|
24
|
+
"scripts/hooks/pre_compact.py",
|
|
25
|
+
"scripts/hooks/stop.py",
|
|
26
|
+
"scripts/hooks/session_end.py",
|
|
22
27
|
"skills/dev-assets-context/SKILL.md",
|
|
23
28
|
"skills/dev-assets-context/agents/*.yaml",
|
|
24
29
|
"skills/dev-assets-context/references/*",
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
REPO_ROOT = Path(os.environ.get("DEV_ASSETS_HOOK_REPO_ROOT", ".")).expanduser().resolve()
|
|
13
|
+
LIB_ROOT = PACKAGE_ROOT / "lib"
|
|
14
|
+
if str(LIB_ROOT) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(LIB_ROOT))
|
|
16
|
+
|
|
17
|
+
from dev_asset_common import (
|
|
18
|
+
AUTO_END,
|
|
19
|
+
AUTO_START,
|
|
20
|
+
PLACEHOLDER_MARKERS,
|
|
21
|
+
asset_paths,
|
|
22
|
+
detect_workspace_mode,
|
|
23
|
+
get_branch_paths,
|
|
24
|
+
list_repos_in_workspace,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
CONTEXT_SCRIPT = PACKAGE_ROOT / "skills" / "dev-assets-context" / "scripts" / "dev_asset_context.py"
|
|
29
|
+
SYNC_SCRIPT = PACKAGE_ROOT / "skills" / "dev-assets-sync" / "scripts" / "dev_asset_sync.py"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_python(script_path, *args, cwd=None):
|
|
33
|
+
work_cwd = cwd if cwd is not None else REPO_ROOT
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["python3", str(script_path), *args],
|
|
36
|
+
cwd=work_cwd,
|
|
37
|
+
check=False,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
)
|
|
41
|
+
if result.returncode != 0:
|
|
42
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"command failed: {script_path}")
|
|
43
|
+
return result.stdout.strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def log(message):
|
|
47
|
+
print(message, file=sys.stderr)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_assets_for(repo_root):
|
|
51
|
+
"""Resolve asset paths for an explicit repo root (workspace-mode friendly)."""
|
|
52
|
+
repo_root_str = str(repo_root)
|
|
53
|
+
root, branch_name, branch_key, storage_root, repo_key, repo_dir, branch_dir = get_branch_paths(repo_root_str)
|
|
54
|
+
return {
|
|
55
|
+
"repo_root": root,
|
|
56
|
+
"branch_name": branch_name,
|
|
57
|
+
"branch_key": branch_key,
|
|
58
|
+
"storage_root": storage_root,
|
|
59
|
+
"repo_key": repo_key,
|
|
60
|
+
"repo_dir": repo_dir,
|
|
61
|
+
"branch_dir": branch_dir,
|
|
62
|
+
"paths": asset_paths(repo_dir, branch_dir),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_assets():
|
|
67
|
+
return resolve_assets_for(REPO_ROOT)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_workspace_mode():
|
|
71
|
+
return detect_workspace_mode(str(REPO_ROOT))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_workspace_repos():
|
|
75
|
+
return list_repos_in_workspace(str(REPO_ROOT))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def primary_repo_name():
|
|
79
|
+
"""Basename of the focus repo from env; None if unset."""
|
|
80
|
+
value = os.environ.get("DEV_ASSETS_PRIMARY_REPO", "").strip()
|
|
81
|
+
return value or None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def strip_managed_markers(text):
|
|
85
|
+
return text.replace(AUTO_START, "").replace(AUTO_END, "").replace("_尚未同步_", "").strip()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_placeholder(text):
|
|
89
|
+
stripped = strip_managed_markers(text)
|
|
90
|
+
if not stripped:
|
|
91
|
+
return True
|
|
92
|
+
return any(marker in stripped for marker in PLACEHOLDER_MARKERS)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_section(path, title):
|
|
96
|
+
if not path.exists():
|
|
97
|
+
return None
|
|
98
|
+
content = path.read_text(encoding="utf-8")
|
|
99
|
+
match = re.search(rf"^## {re.escape(title)}\n\n(.*?)(?=^## |\Z)", content, flags=re.MULTILINE | re.DOTALL)
|
|
100
|
+
if not match:
|
|
101
|
+
return None
|
|
102
|
+
body = strip_managed_markers(match.group(1)).strip()
|
|
103
|
+
return None if is_placeholder(body) else body
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def compact_body(text, max_lines=8, max_chars=700):
|
|
107
|
+
normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip()
|
|
108
|
+
lines = [line for line in normalized.splitlines() if line.strip()]
|
|
109
|
+
if len(lines) > max_lines:
|
|
110
|
+
lines = lines[:max_lines]
|
|
111
|
+
if not lines[-1].endswith("..."):
|
|
112
|
+
lines.append("...")
|
|
113
|
+
compacted = "\n".join(lines)
|
|
114
|
+
if len(compacted) > max_chars:
|
|
115
|
+
compacted = compacted[: max_chars - 3].rstrip() + "..."
|
|
116
|
+
return compacted
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def sync_context_for(repo_root):
|
|
120
|
+
return json.loads(
|
|
121
|
+
run_python(CONTEXT_SCRIPT, "sync", "--repo", str(repo_root), cwd=str(repo_root))
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def sync_working_tree_for(repo_root):
|
|
126
|
+
return json.loads(
|
|
127
|
+
run_python(SYNC_SCRIPT, "sync-working-tree", "--repo", str(repo_root), cwd=str(repo_root))
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def record_head_for(repo_root):
|
|
132
|
+
return json.loads(
|
|
133
|
+
run_python(SYNC_SCRIPT, "record-head", "--repo", str(repo_root), cwd=str(repo_root))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def maybe_sync_context():
|
|
138
|
+
return sync_context_for(REPO_ROOT)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def maybe_sync_working_tree():
|
|
142
|
+
return sync_working_tree_for(REPO_ROOT)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def maybe_record_head():
|
|
146
|
+
return record_head_for(REPO_ROOT)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_FULL_SECTION_KEYS = (
|
|
150
|
+
("overview", "当前目标"),
|
|
151
|
+
("overview", "范围边界"),
|
|
152
|
+
("overview", "当前阶段"),
|
|
153
|
+
("overview", "关键约束"),
|
|
154
|
+
("development", "建议优先查看的目录"),
|
|
155
|
+
("development", "当前进展"),
|
|
156
|
+
("development", "阻塞与注意点"),
|
|
157
|
+
("development", "下一步"),
|
|
158
|
+
("context", "当前有效上下文"),
|
|
159
|
+
("context", "关键决策与原因"),
|
|
160
|
+
("context", "后续继续前要注意"),
|
|
161
|
+
("repo_overview", "长期目标与边界"),
|
|
162
|
+
("repo_overview", "仓库级关键约束"),
|
|
163
|
+
("repo_sources", "共享入口"),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
_BRIEF_SECTION_KEYS = (
|
|
167
|
+
("overview", "当前目标"),
|
|
168
|
+
("overview", "当前阶段"),
|
|
169
|
+
("development", "当前进展"),
|
|
170
|
+
("development", "下一步"),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _extract_sections(paths, keys):
|
|
175
|
+
out = []
|
|
176
|
+
for file_key, title in keys:
|
|
177
|
+
body = extract_section(paths[file_key], title)
|
|
178
|
+
out.append((title, body))
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _build_context_from_assets(assets, *, full=True, heading=None):
|
|
183
|
+
if not assets["branch_dir"].exists():
|
|
184
|
+
if heading is None:
|
|
185
|
+
return (
|
|
186
|
+
"当前仓库尚未初始化 dev-assets 分支记忆。\n"
|
|
187
|
+
"如果这是需要跨会话继续的开发线,请先使用 `dev-assets-setup` 建立 repo+branch 存储。"
|
|
188
|
+
)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
paths = assets["paths"]
|
|
192
|
+
keys = _FULL_SECTION_KEYS if full else _BRIEF_SECTION_KEYS
|
|
193
|
+
sections = _extract_sections(paths, keys)
|
|
194
|
+
max_lines, max_chars = (8, 700) if full else (3, 200)
|
|
195
|
+
|
|
196
|
+
parts = []
|
|
197
|
+
if heading is None:
|
|
198
|
+
parts.append(
|
|
199
|
+
f"已加载 dev-assets:repo `{assets['repo_key']}`,branch `{assets['branch_name']}`。"
|
|
200
|
+
)
|
|
201
|
+
parts.append(f"主存储目录:`{assets['branch_dir']}`")
|
|
202
|
+
else:
|
|
203
|
+
parts.append(heading)
|
|
204
|
+
for title, body in sections:
|
|
205
|
+
if body:
|
|
206
|
+
parts.append(f"{title}:\n{compact_body(body, max_lines=max_lines, max_chars=max_chars)}")
|
|
207
|
+
return "\n\n".join(parts)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_session_start_context():
|
|
211
|
+
assets = resolve_assets()
|
|
212
|
+
try:
|
|
213
|
+
maybe_sync_context()
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
log(f"[dev-assets][SessionStart] refresh skipped: {exc}")
|
|
216
|
+
return _build_context_from_assets(assets, full=True)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_context_for_repo(repo_path, *, full=True, is_primary=False):
|
|
220
|
+
"""Build a per-repo context block for workspace-mode SessionStart injection.
|
|
221
|
+
Returns None when the repo has no initialized branch memory or resolution fails.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
assets = resolve_assets_for(repo_path)
|
|
225
|
+
except Exception as exc:
|
|
226
|
+
log(f"[dev-assets] resolve failed for {Path(repo_path).name}: {exc}")
|
|
227
|
+
return None
|
|
228
|
+
try:
|
|
229
|
+
sync_context_for(repo_path)
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
log(f"[dev-assets] context sync skipped for {Path(repo_path).name}: {exc}")
|
|
232
|
+
tag = "[PRIMARY] " if is_primary else ""
|
|
233
|
+
heading = (
|
|
234
|
+
f"## {tag}`{Path(repo_path).name}` — repo `{assets['repo_key']}`, branch `{assets['branch_name']}`"
|
|
235
|
+
)
|
|
236
|
+
return _build_context_from_assets(assets, full=full, heading=heading)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def build_workspace_start_context():
|
|
240
|
+
"""SessionStart context for workspace mode. Primary repo gets full memory;
|
|
241
|
+
others get a brief overview only. Returns None if no initialized repos.
|
|
242
|
+
"""
|
|
243
|
+
repos = list_workspace_repos()
|
|
244
|
+
if not repos:
|
|
245
|
+
return None
|
|
246
|
+
primary = primary_repo_name()
|
|
247
|
+
primary_hit = False
|
|
248
|
+
sections = []
|
|
249
|
+
for repo_path in repos:
|
|
250
|
+
is_primary = (primary is None) or (repo_path.name == primary)
|
|
251
|
+
if is_primary:
|
|
252
|
+
primary_hit = True
|
|
253
|
+
ctx = build_context_for_repo(repo_path, full=is_primary, is_primary=is_primary)
|
|
254
|
+
if ctx:
|
|
255
|
+
sections.append(ctx)
|
|
256
|
+
if not sections:
|
|
257
|
+
return None
|
|
258
|
+
header_parts = [
|
|
259
|
+
f"已加载 dev-assets workspace 模式:共 {len(repos)} 个仓库 @ `{REPO_ROOT}`"
|
|
260
|
+
]
|
|
261
|
+
if primary:
|
|
262
|
+
status = "命中" if primary_hit else "未在 workspace 中找到,全部按完整模式注入"
|
|
263
|
+
header_parts.append(f"Primary 仓库提示:`{primary}` ({status})")
|
|
264
|
+
header = "\n".join(header_parts)
|
|
265
|
+
return header + "\n\n---\n\n" + "\n\n---\n\n".join(sections)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def record_head_all_repos():
|
|
269
|
+
"""Stop/SessionEnd hook helper for workspace mode. Iterates all repos; logs per-repo
|
|
270
|
+
outcome; swallows per-repo failures.
|
|
271
|
+
"""
|
|
272
|
+
results = []
|
|
273
|
+
for repo_path in list_workspace_repos():
|
|
274
|
+
try:
|
|
275
|
+
assets = resolve_assets_for(repo_path)
|
|
276
|
+
if not assets["branch_dir"].exists():
|
|
277
|
+
log(f"[dev-assets] {repo_path.name}: branch memory not initialized, skip")
|
|
278
|
+
continue
|
|
279
|
+
payload = record_head_for(repo_path)
|
|
280
|
+
log(
|
|
281
|
+
f"[dev-assets] {repo_path.name}: recorded HEAD "
|
|
282
|
+
f"{payload.get('last_seen_head')} for {payload.get('branch')}"
|
|
283
|
+
)
|
|
284
|
+
results.append((repo_path.name, payload))
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
log(f"[dev-assets] {repo_path.name}: record-head skipped: {exc}")
|
|
287
|
+
return results
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def sync_working_tree_all_repos():
|
|
291
|
+
"""PreCompact hook helper for workspace mode. Iterates all repos."""
|
|
292
|
+
results = []
|
|
293
|
+
for repo_path in list_workspace_repos():
|
|
294
|
+
try:
|
|
295
|
+
assets = resolve_assets_for(repo_path)
|
|
296
|
+
if not assets["branch_dir"].exists():
|
|
297
|
+
log(f"[dev-assets] {repo_path.name}: branch memory not initialized, skip")
|
|
298
|
+
continue
|
|
299
|
+
payload = sync_working_tree_for(repo_path)
|
|
300
|
+
log(
|
|
301
|
+
f"[dev-assets] {repo_path.name}: refreshed working-tree navigation for "
|
|
302
|
+
f"{payload.get('branch')} ({payload.get('files_considered')} files)"
|
|
303
|
+
)
|
|
304
|
+
results.append((repo_path.name, payload))
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
log(f"[dev-assets] {repo_path.name}: working-tree sync skipped: {exc}")
|
|
307
|
+
return results
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from _common import (
|
|
4
|
+
is_workspace_mode,
|
|
5
|
+
log,
|
|
6
|
+
maybe_sync_working_tree,
|
|
7
|
+
resolve_assets,
|
|
8
|
+
sync_working_tree_all_repos,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
try:
|
|
14
|
+
if is_workspace_mode():
|
|
15
|
+
results = sync_working_tree_all_repos()
|
|
16
|
+
if not results:
|
|
17
|
+
log("[dev-assets][PreCompact] workspace mode: no initialized repos refreshed")
|
|
18
|
+
return 0
|
|
19
|
+
assets = resolve_assets()
|
|
20
|
+
if not assets["branch_dir"].exists():
|
|
21
|
+
log("[dev-assets][PreCompact] branch memory not initialized, skip")
|
|
22
|
+
return 0
|
|
23
|
+
payload = maybe_sync_working_tree()
|
|
24
|
+
log(
|
|
25
|
+
"[dev-assets][PreCompact] refreshed working-tree navigation for "
|
|
26
|
+
f"{payload['branch']} ({payload['files_considered']} files)"
|
|
27
|
+
)
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
log(f"[dev-assets][PreCompact] skipped: {exc}")
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from _common import (
|
|
4
|
+
is_workspace_mode,
|
|
5
|
+
log,
|
|
6
|
+
maybe_record_head,
|
|
7
|
+
record_head_all_repos,
|
|
8
|
+
resolve_assets,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
try:
|
|
14
|
+
if is_workspace_mode():
|
|
15
|
+
results = record_head_all_repos()
|
|
16
|
+
if not results:
|
|
17
|
+
log("[dev-assets][SessionEnd] workspace mode: no initialized repos finalized")
|
|
18
|
+
return 0
|
|
19
|
+
assets = resolve_assets()
|
|
20
|
+
if not assets["branch_dir"].exists():
|
|
21
|
+
log("[dev-assets][SessionEnd] branch memory not initialized, skip")
|
|
22
|
+
return 0
|
|
23
|
+
payload = maybe_record_head()
|
|
24
|
+
log(f"[dev-assets][SessionEnd] finalized HEAD marker {payload['last_seen_head']} for {payload['branch']}")
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
log(f"[dev-assets][SessionEnd] skipped: {exc}")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from _common import (
|
|
7
|
+
build_session_start_context,
|
|
8
|
+
build_workspace_start_context,
|
|
9
|
+
is_workspace_mode,
|
|
10
|
+
log,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _resolve_context():
|
|
15
|
+
if is_workspace_mode():
|
|
16
|
+
ctx = build_workspace_start_context()
|
|
17
|
+
if ctx:
|
|
18
|
+
return ctx
|
|
19
|
+
return "dev-assets workspace 模式:当前 workspace 下未发现已初始化的仓库记忆。"
|
|
20
|
+
return build_session_start_context()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
try:
|
|
25
|
+
additional_context = _resolve_context()
|
|
26
|
+
payload = {
|
|
27
|
+
"hookSpecificOutput": {
|
|
28
|
+
"hookEventName": "SessionStart",
|
|
29
|
+
"additionalContext": additional_context,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
33
|
+
return 0
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
log(f"[dev-assets][SessionStart] skipped: {exc}")
|
|
36
|
+
print(
|
|
37
|
+
json.dumps(
|
|
38
|
+
{
|
|
39
|
+
"hookSpecificOutput": {
|
|
40
|
+
"hookEventName": "SessionStart",
|
|
41
|
+
"additionalContext": "dev-assets SessionStart hook 未能加载上下文,本轮按普通会话继续。",
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
ensure_ascii=False,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from _common import (
|
|
4
|
+
is_workspace_mode,
|
|
5
|
+
log,
|
|
6
|
+
maybe_record_head,
|
|
7
|
+
record_head_all_repos,
|
|
8
|
+
resolve_assets,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
try:
|
|
14
|
+
if is_workspace_mode():
|
|
15
|
+
results = record_head_all_repos()
|
|
16
|
+
if not results:
|
|
17
|
+
log("[dev-assets][Stop] workspace mode: no initialized repos recorded")
|
|
18
|
+
return 0
|
|
19
|
+
assets = resolve_assets()
|
|
20
|
+
if not assets["branch_dir"].exists():
|
|
21
|
+
log("[dev-assets][Stop] branch memory not initialized, skip")
|
|
22
|
+
return 0
|
|
23
|
+
payload = maybe_record_head()
|
|
24
|
+
log(f"[dev-assets][Stop] recorded HEAD {payload['last_seen_head']} for {payload['branch']}")
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
log(f"[dev-assets][Stop] skipped: {exc}")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
raise SystemExit(main())
|
|
@@ -7,6 +7,8 @@ description: Use when starting work in any Git repository conversation on an exi
|
|
|
7
7
|
|
|
8
8
|
把当前仓库的 branch 记忆作为默认上下文入口恢复出来;repo 共享层只在需要时补读。
|
|
9
9
|
|
|
10
|
+
**Workspace mode:** cwd 是多 repo workspace 时,SessionStart 已自动注入 primary 仓库的完整记忆 + 其他仓库的简短概览。当需要切换焦点到非 primary 仓库补读完整记忆时,向脚本传递 `--repo <basename>` 明确指定。
|
|
11
|
+
|
|
10
12
|
**Announce at start:** 用一句简短的话说明将先恢复当前 branch 记忆,再按需补读 repo 共享记忆。
|
|
11
13
|
|
|
12
14
|
## Workflow
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -7,6 +7,8 @@ description: Use when a branch starts a new requirement stream or when the curre
|
|
|
7
7
|
|
|
8
8
|
为当前 Git 仓库初始化用户目录下的 repo+branch 开发记忆骨架,并在初始化后主动向用户收集最小但关键的资料。
|
|
9
9
|
|
|
10
|
+
**Workspace mode:** 初始化始终针对单个仓库。cwd 是多 repo workspace 时,必须通过 `--repo <basename>` 明确指定目标仓库,每个新仓库分别调用一次;绝不做批量自动初始化,避免用户意外污染不相关的仓库。
|
|
11
|
+
|
|
10
12
|
**Announce at start:** 用一句简短的话说明将先初始化当前仓库的 repo+branch 记忆目录。
|
|
11
13
|
|
|
12
14
|
## Workflow
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -7,6 +7,8 @@ description: Use when the current conversation reaches a commit-related checkpoi
|
|
|
7
7
|
|
|
8
8
|
在提交相关检查点,或在当前对话已经形成需要跨会话保留的稳定结论时,把这次会话结束后仍然有价值的信息同步到当前 branch 记忆,并顺带刷新 repo 共享层的轻量元信息。
|
|
9
9
|
|
|
10
|
+
**Workspace mode:** 当 cwd 是 workspace 根(不是 git repo,但一级子目录中有多个 git repo)时:向脚本传递 `--repo <basename>` 明确指定目标仓库;若未指定且 `DEV_ASSETS_PRIMARY_REPO` env 已设置,会默认落到 primary 仓库。跨仓库 sync 需要为每个仓库各调用一次。
|
|
11
|
+
|
|
10
12
|
**Announce at start:** 用一句简短的话说明将先沉淀本次检查点留下的关键信息。
|
|
11
13
|
|
|
12
14
|
## Workflow
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -7,6 +7,8 @@ description: Use when current development memory needs to be corrected, rewritte
|
|
|
7
7
|
|
|
8
8
|
把当前对话中已经形成且需要保留的新理解,改写到 branch 或 repo 共享层的开发记忆里,而不是只留在对话中。
|
|
9
9
|
|
|
10
|
+
**Workspace mode:** cwd 是多 repo workspace 时,改写需要通过 `--repo <basename>` 明确目标仓库;未指定且 `DEV_ASSETS_PRIMARY_REPO` env 已设置则落到 primary 仓库。改写不跨仓库合并。
|
|
11
|
+
|
|
10
12
|
**Announce at start:** 用一句简短的话说明将先重写相关 section,而不是继续追加历史。
|
|
11
13
|
|
|
12
14
|
## Trigger Hints
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|