@michelj/context-guard 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 +203 -0
- package/README.zh-CN.md +203 -0
- package/bin/context-guard-skill.js +185 -0
- package/hooks.json +41 -0
- package/package.json +30 -0
- package/skills/context-guard/README.md +203 -0
- package/skills/context-guard/README.zh-CN.md +203 -0
- package/skills/context-guard/SKILL.md +519 -0
- package/skills/context-guard/agents/openai.yaml +4 -0
- package/skills/context-guard/references/context-template.md +290 -0
- package/skills/context-guard/references/register-template.md +85 -0
- package/skills/context-guard/references/task-case-template.md +63 -0
- package/skills/context-guard/scripts/context_guard.py +4661 -0
- package/skills/context-guard/scripts/context_guard_hook.py +465 -0
- package/skills/context-guard/tests/BC-20260618-063.sh +116 -0
- package/skills/context-guard/tests/BC-20260618-065.sh +66 -0
- package/skills/context-guard/tests/BC-20260626-080.sh +48 -0
- package/skills/context-guard/tests/BC-20260626-081.sh +40 -0
- package/skills/context-guard/tests/BC-20260626-082.sh +32 -0
- package/skills/context-guard/tests/BC-20260626-083.sh +66 -0
- package/skills/context-guard/tests/BC-20260627-084.sh +74 -0
- package/skills/context-guard/tests/BC-20260630-086.sh +50 -0
- package/skills/context-guard/tests/BC-20260630-087.sh +103 -0
- package/skills/context-guard/tests/BC-20260630-088.sh +32 -0
- package/skills/context-guard/tests/BC-20260630-089.sh +63 -0
- package/skills/context-guard/tests/BC-20260701-090.sh +83 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lightweight lifecycle reminders for the Context Guard plugin.
|
|
3
|
+
|
|
4
|
+
The hook initializes folder-scoped context on session start and nudges Codex to
|
|
5
|
+
use the context-guard skill at the two moments where omission is most costly:
|
|
6
|
+
prompt intake and turn stop.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from context_guard import context_dir, init_context
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
WORKSPACE_KEYS = {
|
|
22
|
+
"cwd",
|
|
23
|
+
"current_working_directory",
|
|
24
|
+
"working_directory",
|
|
25
|
+
"workspace",
|
|
26
|
+
"workspace_root",
|
|
27
|
+
"workspaceFolder",
|
|
28
|
+
"workspace_folder",
|
|
29
|
+
"project",
|
|
30
|
+
"project_root",
|
|
31
|
+
"projectRoot",
|
|
32
|
+
"project_path",
|
|
33
|
+
"repository",
|
|
34
|
+
"repo",
|
|
35
|
+
"repo_path",
|
|
36
|
+
"root",
|
|
37
|
+
}
|
|
38
|
+
WORKSPACE_ENV_KEYS = [
|
|
39
|
+
"CODEX_WORKSPACE_ROOT",
|
|
40
|
+
"CODEX_PROJECT_ROOT",
|
|
41
|
+
"CODEX_CWD",
|
|
42
|
+
"WORKSPACE_ROOT",
|
|
43
|
+
"PROJECT_ROOT",
|
|
44
|
+
"PWD",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_inside(path: Path, parent: Path) -> bool:
|
|
49
|
+
try:
|
|
50
|
+
path.resolve().relative_to(parent.resolve())
|
|
51
|
+
return True
|
|
52
|
+
except ValueError:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_context_guard_skill_path(path: Path) -> bool:
|
|
57
|
+
return is_inside(path, SKILL_ROOT)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def git_root(cwd: Path) -> Path:
|
|
61
|
+
try:
|
|
62
|
+
out = subprocess.check_output(
|
|
63
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
64
|
+
cwd=str(cwd),
|
|
65
|
+
stderr=subprocess.DEVNULL,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=2,
|
|
68
|
+
).strip()
|
|
69
|
+
if out:
|
|
70
|
+
return Path(out)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return cwd
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def possible_workspace_paths(value: object) -> list[Path]:
|
|
77
|
+
paths: list[Path] = []
|
|
78
|
+
|
|
79
|
+
def add_path(candidate: object) -> None:
|
|
80
|
+
if not isinstance(candidate, str):
|
|
81
|
+
return
|
|
82
|
+
text = candidate.strip()
|
|
83
|
+
if not text or not text.startswith("/"):
|
|
84
|
+
return
|
|
85
|
+
path = Path(text).expanduser()
|
|
86
|
+
if path.exists():
|
|
87
|
+
paths.append(path)
|
|
88
|
+
|
|
89
|
+
def walk(obj: object, key_hint: str = "") -> None:
|
|
90
|
+
if isinstance(obj, dict):
|
|
91
|
+
for key, child in obj.items():
|
|
92
|
+
if key in WORKSPACE_KEYS:
|
|
93
|
+
add_path(child)
|
|
94
|
+
walk(child, key)
|
|
95
|
+
elif isinstance(obj, list):
|
|
96
|
+
for child in obj:
|
|
97
|
+
walk(child, key_hint)
|
|
98
|
+
|
|
99
|
+
walk(value)
|
|
100
|
+
return paths
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def parse_hook_payload(raw: str) -> object:
|
|
104
|
+
if not raw.strip():
|
|
105
|
+
return {}
|
|
106
|
+
try:
|
|
107
|
+
return json.loads(raw)
|
|
108
|
+
except Exception:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def event_root(raw: str, cwd: Path) -> tuple[Path, str]:
|
|
113
|
+
payload = parse_hook_payload(raw)
|
|
114
|
+
candidates: list[tuple[Path, str]] = []
|
|
115
|
+
for path in possible_workspace_paths(payload):
|
|
116
|
+
candidates.append((path, "hook payload"))
|
|
117
|
+
for key in WORKSPACE_ENV_KEYS:
|
|
118
|
+
value = os.environ.get(key, "").strip()
|
|
119
|
+
if value.startswith("/"):
|
|
120
|
+
path = Path(value).expanduser()
|
|
121
|
+
if path.exists():
|
|
122
|
+
candidates.append((path, f"${key}"))
|
|
123
|
+
candidates.append((cwd, "process cwd"))
|
|
124
|
+
|
|
125
|
+
for path, source in candidates:
|
|
126
|
+
root = git_root(path)
|
|
127
|
+
if not is_context_guard_skill_path(root):
|
|
128
|
+
return root, source
|
|
129
|
+
|
|
130
|
+
root = git_root(cwd)
|
|
131
|
+
return root, "process cwd"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def read_stdin() -> str:
|
|
135
|
+
try:
|
|
136
|
+
return sys.stdin.read()
|
|
137
|
+
except Exception:
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def hook_log(message: str) -> None:
|
|
142
|
+
print(message, file=sys.stderr)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def hook_response(**payload: object) -> int:
|
|
146
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def prompt_text(raw: str) -> str:
|
|
151
|
+
if not raw.strip():
|
|
152
|
+
return ""
|
|
153
|
+
try:
|
|
154
|
+
data = json.loads(raw)
|
|
155
|
+
except Exception:
|
|
156
|
+
return raw
|
|
157
|
+
pieces: list[str] = []
|
|
158
|
+
|
|
159
|
+
def walk(value: object) -> None:
|
|
160
|
+
if isinstance(value, str):
|
|
161
|
+
pieces.append(value)
|
|
162
|
+
elif isinstance(value, list):
|
|
163
|
+
for item in value:
|
|
164
|
+
walk(item)
|
|
165
|
+
elif isinstance(value, dict):
|
|
166
|
+
for key in ("prompt", "message", "text", "content", "input"):
|
|
167
|
+
if key in value:
|
|
168
|
+
walk(value[key])
|
|
169
|
+
|
|
170
|
+
walk(data)
|
|
171
|
+
return "\n".join(pieces)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def looks_like_bad_case(text: str) -> bool:
|
|
175
|
+
lowered = text.lower()
|
|
176
|
+
markers = [
|
|
177
|
+
"bug",
|
|
178
|
+
"bad case",
|
|
179
|
+
"regression",
|
|
180
|
+
"broken",
|
|
181
|
+
"error",
|
|
182
|
+
"failed",
|
|
183
|
+
"failing",
|
|
184
|
+
"doesn't work",
|
|
185
|
+
"not working",
|
|
186
|
+
"紧急",
|
|
187
|
+
"报错",
|
|
188
|
+
"失败",
|
|
189
|
+
"坏例",
|
|
190
|
+
"复现",
|
|
191
|
+
"回归",
|
|
192
|
+
"不对",
|
|
193
|
+
"有问题",
|
|
194
|
+
]
|
|
195
|
+
return any(marker in lowered for marker in markers)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def looks_like_task_switch(text: str) -> bool:
|
|
199
|
+
lowered = text.lower()
|
|
200
|
+
markers = [
|
|
201
|
+
"urgent",
|
|
202
|
+
"instead",
|
|
203
|
+
"switch",
|
|
204
|
+
"pause",
|
|
205
|
+
"later",
|
|
206
|
+
"different",
|
|
207
|
+
"unrelated",
|
|
208
|
+
"先",
|
|
209
|
+
"暂停",
|
|
210
|
+
"等下",
|
|
211
|
+
"换个",
|
|
212
|
+
"另一个",
|
|
213
|
+
"紧急",
|
|
214
|
+
"回头",
|
|
215
|
+
"先不",
|
|
216
|
+
]
|
|
217
|
+
return any(marker in lowered for marker in markers)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def looks_like_goal_mode(text: str) -> bool:
|
|
221
|
+
lowered = text.lower()
|
|
222
|
+
markers = [
|
|
223
|
+
"goal mode",
|
|
224
|
+
"goal模式",
|
|
225
|
+
"goal 模式",
|
|
226
|
+
"active goal",
|
|
227
|
+
"long-running",
|
|
228
|
+
"autonomous",
|
|
229
|
+
"目标模式",
|
|
230
|
+
"长期目标",
|
|
231
|
+
"持续执行",
|
|
232
|
+
"自动继续",
|
|
233
|
+
]
|
|
234
|
+
return any(marker in lowered for marker in markers)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def looks_like_remote_work(text: str) -> bool:
|
|
238
|
+
lowered = text.lower()
|
|
239
|
+
markers = [
|
|
240
|
+
"ssh",
|
|
241
|
+
"remote server",
|
|
242
|
+
"remote host",
|
|
243
|
+
"dev server",
|
|
244
|
+
"jump host",
|
|
245
|
+
"服务器",
|
|
246
|
+
"远程",
|
|
247
|
+
"开发机",
|
|
248
|
+
"跳板机",
|
|
249
|
+
"通过ssh",
|
|
250
|
+
"通过 ssh",
|
|
251
|
+
]
|
|
252
|
+
return any(marker in lowered for marker in markers)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def looks_like_test_creation(text: str) -> bool:
|
|
256
|
+
lowered = text.lower()
|
|
257
|
+
creation_markers = [
|
|
258
|
+
"create",
|
|
259
|
+
"write",
|
|
260
|
+
"generate",
|
|
261
|
+
"design",
|
|
262
|
+
"add",
|
|
263
|
+
"创建",
|
|
264
|
+
"建立",
|
|
265
|
+
"写",
|
|
266
|
+
"生成",
|
|
267
|
+
"设计",
|
|
268
|
+
"新增",
|
|
269
|
+
"加一个",
|
|
270
|
+
"做一个",
|
|
271
|
+
]
|
|
272
|
+
test_markers = [
|
|
273
|
+
"test case",
|
|
274
|
+
"task case",
|
|
275
|
+
"test task",
|
|
276
|
+
"testing task",
|
|
277
|
+
"测试case",
|
|
278
|
+
"测试 case",
|
|
279
|
+
"测试任务",
|
|
280
|
+
"测试用例",
|
|
281
|
+
"测评任务",
|
|
282
|
+
"测评case",
|
|
283
|
+
"测评 case",
|
|
284
|
+
"测试链路",
|
|
285
|
+
"测试",
|
|
286
|
+
]
|
|
287
|
+
return any(marker in lowered for marker in creation_markers) and any(marker in lowered for marker in test_markers)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def looks_like_explicit_branch(text: str) -> bool:
|
|
291
|
+
lowered = text.lower()
|
|
292
|
+
markers = [
|
|
293
|
+
"branch task",
|
|
294
|
+
"side task",
|
|
295
|
+
"side route",
|
|
296
|
+
"fork this",
|
|
297
|
+
"create a branch",
|
|
298
|
+
"new branch",
|
|
299
|
+
"as a branch",
|
|
300
|
+
"支线",
|
|
301
|
+
"分支",
|
|
302
|
+
"开一个分支",
|
|
303
|
+
"开一条支线",
|
|
304
|
+
"创建支线",
|
|
305
|
+
"创建分支",
|
|
306
|
+
"作为支线",
|
|
307
|
+
]
|
|
308
|
+
return any(marker in lowered for marker in markers)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def looks_like_route_drift(text: str) -> bool:
|
|
312
|
+
lowered = text.lower()
|
|
313
|
+
drift_markers = [
|
|
314
|
+
"significantly diverge",
|
|
315
|
+
"diverge from",
|
|
316
|
+
"different architecture",
|
|
317
|
+
"new architecture",
|
|
318
|
+
"new direction",
|
|
319
|
+
"refactor direction",
|
|
320
|
+
"偏离",
|
|
321
|
+
"显著偏离",
|
|
322
|
+
"新的架构",
|
|
323
|
+
"新方向",
|
|
324
|
+
"重构方向",
|
|
325
|
+
"主线架构",
|
|
326
|
+
]
|
|
327
|
+
return any(marker in lowered for marker in drift_markers)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def bad_case_blocks(text: str) -> list[dict[str, str]]:
|
|
331
|
+
blocks: list[dict[str, str]] = []
|
|
332
|
+
current: dict[str, str] | None = None
|
|
333
|
+
for line in text.splitlines():
|
|
334
|
+
heading = line.startswith("### BC-")
|
|
335
|
+
if heading:
|
|
336
|
+
if current:
|
|
337
|
+
blocks.append(current)
|
|
338
|
+
identifier, _, title = line.removeprefix("### ").partition(":")
|
|
339
|
+
current = {"id": identifier.strip(), "title": title.strip()}
|
|
340
|
+
continue
|
|
341
|
+
if current is None or not line.startswith("- ") or ":" not in line:
|
|
342
|
+
continue
|
|
343
|
+
key, _, value = line[2:].partition(":")
|
|
344
|
+
current[key.strip().lower()] = value.strip()
|
|
345
|
+
if current:
|
|
346
|
+
blocks.append(current)
|
|
347
|
+
return blocks
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def unresolved_bad_cases(path: Path) -> list[dict[str, str]]:
|
|
351
|
+
if not path.exists():
|
|
352
|
+
return []
|
|
353
|
+
resolved_statuses = {"resolved", "done", "superseded-by-route-change"}
|
|
354
|
+
cases = bad_case_blocks(path.read_text(encoding="utf-8"))
|
|
355
|
+
return [
|
|
356
|
+
case
|
|
357
|
+
for case in cases
|
|
358
|
+
if case.get("status", "").strip().lower() not in resolved_statuses
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def format_unresolved_bad_cases(cases: list[dict[str, str]], limit: int = 5) -> str:
|
|
363
|
+
if not cases:
|
|
364
|
+
return "none"
|
|
365
|
+
parts = []
|
|
366
|
+
for case in cases[:limit]:
|
|
367
|
+
status = case.get("status", "unknown") or "unknown"
|
|
368
|
+
title = case.get("title", "Untitled bad case")
|
|
369
|
+
phenomenon = case.get("phenomenon", "").strip()
|
|
370
|
+
summary = f"{title} ({status})"
|
|
371
|
+
if phenomenon:
|
|
372
|
+
summary += f" - {phenomenon}"
|
|
373
|
+
parts.append(summary)
|
|
374
|
+
if len(cases) > limit:
|
|
375
|
+
parts.append(f"{len(cases) - limit} more unresolved bad cases")
|
|
376
|
+
return "; ".join(parts)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def main() -> int:
|
|
380
|
+
event = sys.argv[1] if len(sys.argv) > 1 else "unknown"
|
|
381
|
+
raw = read_stdin()
|
|
382
|
+
root, root_source = event_root(raw, Path.cwd())
|
|
383
|
+
context_dir = root / ".codex" / "context"
|
|
384
|
+
index_path = context_dir / "index.md"
|
|
385
|
+
roadmap_path = context_dir / "roadmap.md"
|
|
386
|
+
bad_cases_path = context_dir / "bad-cases.md"
|
|
387
|
+
text = prompt_text(raw)
|
|
388
|
+
|
|
389
|
+
if is_context_guard_skill_path(root):
|
|
390
|
+
hook_log(
|
|
391
|
+
"[context-guard] detected Context Guard skill directory as the apparent root; "
|
|
392
|
+
"skipping project context writes. Open the target Codex folder or pass an explicit local `--root` "
|
|
393
|
+
"when showing/updating a roadmap."
|
|
394
|
+
)
|
|
395
|
+
hook_log(f"[context-guard] apparent root source: {root_source}; apparent root: {root}")
|
|
396
|
+
return hook_response()
|
|
397
|
+
|
|
398
|
+
if event == "session-start":
|
|
399
|
+
created = init_context(root)
|
|
400
|
+
if created:
|
|
401
|
+
hook_log(f"[context-guard] initialized folder context: {context_dir}")
|
|
402
|
+
else:
|
|
403
|
+
hook_log(f"[context-guard] folder context ready: {context_dir}")
|
|
404
|
+
hook_log(f"[context-guard] project root: {root} ({root_source})")
|
|
405
|
+
hook_log("[context-guard] context location rule: save project context only under `<opened local Codex project root>/.codex/context/`.")
|
|
406
|
+
hook_log("[context-guard] use .codex/context/index.md for quick scan and .codex/context/roadmap.md for route nodes.")
|
|
407
|
+
return hook_response()
|
|
408
|
+
|
|
409
|
+
if event == "user-prompt-submit":
|
|
410
|
+
hints: list[str] = []
|
|
411
|
+
if looks_like_goal_mode(text):
|
|
412
|
+
hints.append("goal mode: align active goal with current context and record roadmap/bad-case checkpoints during long-running work")
|
|
413
|
+
if looks_like_remote_work(text):
|
|
414
|
+
hints.append("remote/SSH work: keep `.codex/context` in the local Codex workspace; record remote host/path as metadata and do not initialize roadmap context on the server unless explicitly requested")
|
|
415
|
+
if looks_like_test_creation(text):
|
|
416
|
+
hints.append("explicit test creation: start the user-visible response with `测试创建识别:...`, summarize the test target from state A to state B, and only create durable tests after the user's design is clear or confirmed")
|
|
417
|
+
if looks_like_explicit_branch(text):
|
|
418
|
+
hints.append("explicit branch task: create/select a branch task by running `context_guard.py create-branch-task --title <task title> --branch <branch name> --parent-node <parent NODE id>` before implementation; verify the roadmap node has Branch: and Parent:")
|
|
419
|
+
elif looks_like_route_drift(text):
|
|
420
|
+
hints.append("possible route drift: ask whether to create a branch before moving away from the current mainline architecture")
|
|
421
|
+
if looks_like_task_switch(text):
|
|
422
|
+
hints.append("possible task switch: park current context in .codex/context/index.md before switching")
|
|
423
|
+
if looks_like_bad_case(text):
|
|
424
|
+
hints.append("possible bad case: record/update .codex/context/bad-cases.md or task-local bad-cases.md")
|
|
425
|
+
if not hints:
|
|
426
|
+
hints.append("run Context Guard intake: continue current context or note no active context")
|
|
427
|
+
hook_log("[context-guard] " + "; ".join(hints))
|
|
428
|
+
hook_log(f"[context-guard] root source: {root_source}")
|
|
429
|
+
hook_log(f"[context-guard] project root: {root}")
|
|
430
|
+
hook_log(f"[context-guard] context folder: {context_dir}")
|
|
431
|
+
hook_log(f"[context-guard] context index: {index_path}")
|
|
432
|
+
hook_log(f"[context-guard] route map: {roadmap_path}")
|
|
433
|
+
return hook_response()
|
|
434
|
+
|
|
435
|
+
if event == "stop":
|
|
436
|
+
hook_log("[context-guard] run turn-end checkpoint before finalizing or updating a goal: update index, route map nodes, parked/resume tasks, and relevant bad-case/test-chain links.")
|
|
437
|
+
hook_log("[context-guard] COMPLETION RELIABILITY GATE: use existing user screenshots/logs/reproductions as red evidence when available; implement once the cause is clear, then run the smallest real post-fix check. Default budget is one primary check plus at most two highly relevant bad-case guards.")
|
|
438
|
+
hook_log("[context-guard] BAD-CASE GUARD GATE: newly checked resolved or recurred BC entries need Guard type, Red condition, Green condition, Expected failure reason, and a red-capable Guard / verification; run `context_guard.py validate-bad-cases` only after register/schema/renderer edits, or `--strict` when intentionally migrating/checking all resolved cases.")
|
|
439
|
+
hook_log("[context-guard] GUARD SELECTION GATE: do not run every historical guard and do not manufacture new red tests when credible evidence already exists. Select guards by changed files, feature area, route branch, tags, and original user-visible symptom; skip unrelated resolved cases.")
|
|
440
|
+
hook_log("[context-guard] TEST HUB GATE: before finalizing development work, prefer `context_guard.py dev-complete --root <project>` so the hub runs every human-approved `every-dev-completion` test, cleans success artifacts, and preserves failed/blocked evidence. Do not treat ordinary bad-case guards or roadmap Test chain notes as registered tests.")
|
|
441
|
+
hook_log("[context-guard] TASK-CASE GATE: when a workflow has multiple phases, prefer one relevant task case from `.codex/context/task-cases/` with phase/checkpoint logs over many isolated bug-level tests; report the failed phase/checkpoint if it breaks.")
|
|
442
|
+
hook_log("[context-guard] TEST BLOCKER GATE: if approved tests are blocked by credentials, external service outage, permission denial, hardware/resource limits, network, destructive-risk confirmation, or user-only judgment, stop and ask/warn the user with the exact blocker and evidence path.")
|
|
443
|
+
hook_log("[context-guard] TASK-CASE DESIGN GATE: before writing a new durable task-case script for a complex workflow, ask the user to confirm a short business-facing proposal: from what state to what state, main task, and major risk; keep technical details inside the task-case file, or keep it `proposed` if unavailable.")
|
|
444
|
+
hook_log("[context-guard] GOAL-MODE TEST GATE: in goal mode, use task cases as phase gates; log current phase progress and run the smallest approved path before claiming goal completion instead of silently creating broad new tests.")
|
|
445
|
+
hook_log("[context-guard] ROADMAP CHECKPOINT GATE: assess whether this turn deserves a roadmap node. Create one only for meaningful progress, a route decision, a fix, a branch/fork, a user-visible milestone, or stale hidden checkpoints; otherwise say no roadmap node was needed and why.")
|
|
446
|
+
hook_log("[context-guard] If a node is needed, run `context_guard.py checkpoint-roadmap-node --title <short title> --branch <Main or route> --level <major|checkpoint> --outcome <one-line progress> --next-step <next>` and include linked BC/test-chain notes when relevant.")
|
|
447
|
+
hook_log("[context-guard] ROADMAP MAINTENANCE GATE: run `context_guard.py validate-roadmap-maintenance` after route updates; do not let mainline/branch overview stay stale while important work is hidden as checkpoints.")
|
|
448
|
+
hook_log("[context-guard] If frontend/UI/HTML/CSS/layout/browser behavior changed, inspect with browser/screenshot or state the exact blocker; do not claim fixed without this evidence.")
|
|
449
|
+
hook_log("[context-guard] Branch task gate: if the user explicitly asked for a branch, ensure `context_guard.py create-branch-task --title <task title> --branch <branch name> --parent-node <parent NODE id>` has created the task folder, index current entry, and Branch/Parent roadmap node; if the work significantly drifts from the mainline architecture, ask whether to create a branch before finalizing.")
|
|
450
|
+
hook_log("[context-guard] final answer must include verification evidence and must not say done/fixed/passing unless the gate above was satisfied.")
|
|
451
|
+
hook_log(f"[context-guard] root source: {root_source}")
|
|
452
|
+
hook_log(f"[context-guard] project root: {root}")
|
|
453
|
+
hook_log(f"[context-guard] context folder: {context_dir}")
|
|
454
|
+
hook_log(f"[context-guard] bad-case register: {bad_cases_path}")
|
|
455
|
+
open_cases = unresolved_bad_cases(bad_cases_path)
|
|
456
|
+
hook_log("[context-guard] final answer must include BC summary: archived/updated BC this turn, and current unresolved BC.")
|
|
457
|
+
hook_log(f"[context-guard] current unresolved BC: {format_unresolved_bad_cases(open_cases)}")
|
|
458
|
+
return hook_response()
|
|
459
|
+
|
|
460
|
+
hook_log("[context-guard] unknown hook event; use the context-guard skill if context changed.")
|
|
461
|
+
return hook_response()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
if __name__ == "__main__":
|
|
465
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/context_guard.py}"
|
|
5
|
+
|
|
6
|
+
python3 - "$SCRIPT" <<'PY'
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
script = Path(sys.argv[1])
|
|
17
|
+
root = Path(tempfile.mkdtemp(prefix="context-guard-branch-test-"))
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
subprocess.run(["python3", str(script), "init", "--root", str(root)], check=True, capture_output=True, text=True)
|
|
21
|
+
subprocess.run(["python3", str(script), "set-language", "--language", "zh", "--root", str(root)], check=True, capture_output=True, text=True)
|
|
22
|
+
|
|
23
|
+
ctx = root / ".codex" / "context"
|
|
24
|
+
(ctx / "index.md").write_text(
|
|
25
|
+
"""# Context Index
|
|
26
|
+
|
|
27
|
+
## Quick Scan
|
|
28
|
+
|
|
29
|
+
- Current: CTX-20260618-main
|
|
30
|
+
- Latest roadmap node: NODE-20260618-001
|
|
31
|
+
- Hot bad-case tags: none
|
|
32
|
+
- Resume candidate: none
|
|
33
|
+
|
|
34
|
+
## Current
|
|
35
|
+
|
|
36
|
+
- ID: CTX-20260618-main
|
|
37
|
+
- Title: 主线任务
|
|
38
|
+
- State: current
|
|
39
|
+
- Folder: `.codex/context/tasks/CTX-20260618-main/`
|
|
40
|
+
- Last updated: 2026-06-18
|
|
41
|
+
- Summary: 正在推进主线任务。
|
|
42
|
+
- Next step: 继续主线。
|
|
43
|
+
|
|
44
|
+
## Parked / Resume Candidates
|
|
45
|
+
|
|
46
|
+
None.
|
|
47
|
+
|
|
48
|
+
## Archived
|
|
49
|
+
|
|
50
|
+
None.
|
|
51
|
+
""",
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
)
|
|
54
|
+
(ctx / "roadmap.md").write_text(
|
|
55
|
+
"""# Context Roadmap
|
|
56
|
+
|
|
57
|
+
## Nodes
|
|
58
|
+
|
|
59
|
+
### NODE-20260618-001: 主线起点
|
|
60
|
+
|
|
61
|
+
- Date: 2026-06-18
|
|
62
|
+
- Status: done
|
|
63
|
+
- Level: major
|
|
64
|
+
- Branch: Main
|
|
65
|
+
- Parent: none
|
|
66
|
+
- Task: `CTX-20260618-main`
|
|
67
|
+
- Outcome: 主线已经建立。
|
|
68
|
+
- Decision / reason: 作为支线父节点。
|
|
69
|
+
- Avoid going back: 不要把支线写回主线。
|
|
70
|
+
- Next: 等待支线。
|
|
71
|
+
- Linked bad cases: none
|
|
72
|
+
- Test chain: none
|
|
73
|
+
""",
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
[
|
|
79
|
+
"python3",
|
|
80
|
+
str(script),
|
|
81
|
+
"create-branch-task",
|
|
82
|
+
"--root",
|
|
83
|
+
str(root),
|
|
84
|
+
"--title",
|
|
85
|
+
"后端状态机设计",
|
|
86
|
+
"--branch",
|
|
87
|
+
"后端状态机",
|
|
88
|
+
"--parent-node",
|
|
89
|
+
"NODE-20260618-001",
|
|
90
|
+
],
|
|
91
|
+
check=True,
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
output = result.stdout + result.stderr
|
|
97
|
+
assert "CTX-" in output, output
|
|
98
|
+
assert "NODE-" in output, output
|
|
99
|
+
|
|
100
|
+
index = (ctx / "index.md").read_text(encoding="utf-8")
|
|
101
|
+
roadmap = (ctx / "roadmap.md").read_text(encoding="utf-8")
|
|
102
|
+
exports = json.loads((ctx / "roadmap" / "roadmap.json").read_text(encoding="utf-8"))
|
|
103
|
+
|
|
104
|
+
assert "- Current: CTX-20260618-main" not in index, index
|
|
105
|
+
assert "后端状态机设计" in index, index
|
|
106
|
+
assert "主线任务" in index and "resume-candidate" in index, index
|
|
107
|
+
assert "- Branch: 后端状态机" in roadmap, roadmap
|
|
108
|
+
assert "- Parent: NODE-20260618-001" in roadmap, roadmap
|
|
109
|
+
assert "- Task: `CTX-" in roadmap, roadmap
|
|
110
|
+
assert any(route["branch"] == "后端状态机" for route in exports["routes"]), exports
|
|
111
|
+
|
|
112
|
+
task_dirs = list((ctx / "tasks").glob("CTX-*"))
|
|
113
|
+
assert any((path / "context.md").exists() and "后端状态机设计" in (path / "context.md").read_text(encoding="utf-8") for path in task_dirs), task_dirs
|
|
114
|
+
finally:
|
|
115
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
116
|
+
PY
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/context_guard.py}"
|
|
5
|
+
ROOT="$(mktemp -d "${TMPDIR:-/tmp}/context-guard-marker-test-XXXXXX")"
|
|
6
|
+
trap 'rm -rf "$ROOT"' EXIT
|
|
7
|
+
|
|
8
|
+
python3 "$SCRIPT" init --root "$ROOT" >/dev/null
|
|
9
|
+
python3 "$SCRIPT" set-language --language zh --root "$ROOT" >/dev/null
|
|
10
|
+
|
|
11
|
+
CTX="$ROOT/.codex/context"
|
|
12
|
+
cat > "$CTX/roadmap.md" <<'MD'
|
|
13
|
+
# Context Roadmap
|
|
14
|
+
|
|
15
|
+
## Nodes
|
|
16
|
+
|
|
17
|
+
### NODE-20260618-001: 标记测试节点
|
|
18
|
+
|
|
19
|
+
- Date: 2026-06-18
|
|
20
|
+
- Status: done
|
|
21
|
+
- Level: major
|
|
22
|
+
- Branch: Main
|
|
23
|
+
- Parent: none
|
|
24
|
+
- Task: `CTX-20260618-marker`
|
|
25
|
+
- Outcome: 检查问题案例标题行标记布局。
|
|
26
|
+
- Decision / reason: 标记不能单独占一行。
|
|
27
|
+
- Avoid going back: 不要把状态点放在标题前面。
|
|
28
|
+
- Next: none
|
|
29
|
+
- Linked bad cases: BC-20260618-999
|
|
30
|
+
- Test chain: none
|
|
31
|
+
MD
|
|
32
|
+
|
|
33
|
+
cat > "$CTX/bad-cases.md" <<'MD'
|
|
34
|
+
# Bad Case Register
|
|
35
|
+
|
|
36
|
+
## Active Cases
|
|
37
|
+
|
|
38
|
+
### BC-20260618-999: 这是一个很长的问题案例标题用于触发换行但标记不能单独成行
|
|
39
|
+
|
|
40
|
+
- Status: resolved
|
|
41
|
+
- First observed: 2026-06-18
|
|
42
|
+
- Last checked: 2026-06-18
|
|
43
|
+
- Scope: roadmap marker layout
|
|
44
|
+
- Context task: `CTX-20260618-marker`
|
|
45
|
+
- Roadmap nodes: NODE-20260618-001
|
|
46
|
+
- Tags: #roadmap-ux
|
|
47
|
+
- Frequency: repeated-2
|
|
48
|
+
- Phenomenon: 状态点和频率点不应该单独一行。
|
|
49
|
+
- Trigger / reproduction: 生成 roadmap 并检查 badcase-head。
|
|
50
|
+
- Root cause: 标记位于标题前,窄列时容易变成单独一行。
|
|
51
|
+
- Fix method: 将标记移到标题右侧的 inline marker 容器。
|
|
52
|
+
- Guard / verification: 本脚本检查 HTML 结构。
|
|
53
|
+
- Reusable guard path: none
|
|
54
|
+
- Test chain: none
|
|
55
|
+
MD
|
|
56
|
+
|
|
57
|
+
python3 "$SCRIPT" show-roadmap --root "$ROOT" >/dev/null
|
|
58
|
+
HTML="$CTX/roadmap/roadmap.html"
|
|
59
|
+
|
|
60
|
+
grep -q 'class="badcase-markers"' "$HTML"
|
|
61
|
+
grep -q '.badcase-head { display: grid; grid-template-columns: minmax(0, 1fr) auto;' "$HTML"
|
|
62
|
+
if grep -q '<div class="badcase-head"><span class="status-dot' "$HTML"; then
|
|
63
|
+
echo "badcase marker still starts a separate title row" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
grep -q '<a class="detail-link" href="#case-1">' "$HTML"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/context_guard.py}"
|
|
5
|
+
ROOT="$(mktemp -d "${TMPDIR:-/tmp}/context-guard-readable-map-XXXXXX")"
|
|
6
|
+
trap 'rm -rf "$ROOT"' EXIT
|
|
7
|
+
|
|
8
|
+
python3 "$SCRIPT" init --root "$ROOT" >/dev/null
|
|
9
|
+
python3 "$SCRIPT" set-language --language zh --root "$ROOT" >/dev/null
|
|
10
|
+
|
|
11
|
+
CTX="$ROOT/.codex/context"
|
|
12
|
+
cat > "$CTX/roadmap.md" <<'MD'
|
|
13
|
+
# Context Roadmap
|
|
14
|
+
|
|
15
|
+
## Nodes
|
|
16
|
+
|
|
17
|
+
### NODE-20260626-001: 主线起点
|
|
18
|
+
|
|
19
|
+
- Date: 2026-06-26
|
|
20
|
+
- Status: done
|
|
21
|
+
- Level: major
|
|
22
|
+
- Branch: Main
|
|
23
|
+
- Parent: none
|
|
24
|
+
- Outcome: 这是一段很长的主线摘要,过去会直接显示在多路线概览卡片里,让路线图看起来像卡片墙而不是路线骨架。
|
|
25
|
+
- Next: none
|
|
26
|
+
- Linked bad cases: none
|
|
27
|
+
|
|
28
|
+
### NODE-20260626-002: 支线入口
|
|
29
|
+
|
|
30
|
+
- Date: 2026-06-26
|
|
31
|
+
- Status: done
|
|
32
|
+
- Level: major
|
|
33
|
+
- Branch: 可读性支线
|
|
34
|
+
- Parent: NODE-20260626-001
|
|
35
|
+
- Outcome: 这是一段很长的支线摘要,应该留在详情里,而不是占据概览卡片的主要视觉空间。
|
|
36
|
+
- Next: none
|
|
37
|
+
- Linked bad cases: none
|
|
38
|
+
MD
|
|
39
|
+
|
|
40
|
+
python3 "$SCRIPT" show-roadmap --root "$ROOT" >/dev/null
|
|
41
|
+
HTML="$CTX/roadmap/roadmap.html"
|
|
42
|
+
|
|
43
|
+
grep -q 'class="route-stack branch-map"' "$HTML"
|
|
44
|
+
grep -q '.track-grid.route-only {' "$HTML"
|
|
45
|
+
grep -q 'grid-auto-columns: minmax(180px, 230px);' "$HTML"
|
|
46
|
+
grep -q '.branch-map .summary {' "$HTML"
|
|
47
|
+
grep -q 'display: none;' "$HTML"
|
|
48
|
+
grep -q -- '-webkit-line-clamp: 3;' "$HTML"
|