@michelj/context-guard 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +113 -150
  2. package/README.zh-CN.md +113 -150
  3. package/SKILL.md +46 -0
  4. package/agents/openai.yaml +4 -0
  5. package/bin/context-guard-skill.js +234 -46
  6. package/bin/postinstall.js +1 -1
  7. package/hooks.json +29 -5
  8. package/package.json +12 -5
  9. package/prototype/workbench.html +4933 -0
  10. package/references/bug-record-template.md +37 -0
  11. package/references/context-template.md +19 -0
  12. package/scripts/context_guard.py +806 -0
  13. package/scripts/context_guard_hook.py +302 -0
  14. package/scripts/map_owns.py +769 -0
  15. package/skills/context-guard/README.md +0 -234
  16. package/skills/context-guard/README.zh-CN.md +0 -234
  17. package/skills/context-guard/SKILL.md +0 -558
  18. package/skills/context-guard/agents/openai.yaml +0 -4
  19. package/skills/context-guard/references/context-template.md +0 -290
  20. package/skills/context-guard/references/register-template.md +0 -85
  21. package/skills/context-guard/references/task-case-template.md +0 -63
  22. package/skills/context-guard/scripts/context_guard.py +0 -4886
  23. package/skills/context-guard/scripts/context_guard_hook.py +0 -522
  24. package/skills/context-guard/tests/BC-20260618-063.sh +0 -116
  25. package/skills/context-guard/tests/BC-20260618-065.sh +0 -66
  26. package/skills/context-guard/tests/BC-20260626-080.sh +0 -48
  27. package/skills/context-guard/tests/BC-20260626-081.sh +0 -40
  28. package/skills/context-guard/tests/BC-20260626-082.sh +0 -32
  29. package/skills/context-guard/tests/BC-20260626-083.sh +0 -66
  30. package/skills/context-guard/tests/BC-20260627-084.sh +0 -74
  31. package/skills/context-guard/tests/BC-20260630-086.sh +0 -50
  32. package/skills/context-guard/tests/BC-20260630-087.sh +0 -103
  33. package/skills/context-guard/tests/BC-20260630-088.sh +0 -32
  34. package/skills/context-guard/tests/BC-20260630-089.sh +0 -63
  35. package/skills/context-guard/tests/BC-20260701-090.sh +0 -83
  36. package/skills/context-guard/tests/BC-20260702-096.sh +0 -45
@@ -1,522 +0,0 @@
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 approved_dev_completion_tests, 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 run_test_hub_completion(root: Path) -> tuple[int, str]:
151
- ctx = context_dir(root)
152
- tests = approved_dev_completion_tests(ctx)
153
- if not tests:
154
- return 0, "[context-guard] test hub: no approved every-dev-completion tests."
155
-
156
- hook_log(f"[context-guard] test hub: running {len(tests)} approved every-dev-completion test(s).")
157
- completed = subprocess.run(
158
- [
159
- sys.executable,
160
- str(SKILL_ROOT / "scripts" / "context_guard.py"),
161
- "dev-complete",
162
- "--root",
163
- str(root),
164
- ],
165
- text=True,
166
- capture_output=True,
167
- timeout=900,
168
- )
169
- output = "\n".join(part for part in [completed.stdout.strip(), completed.stderr.strip()] if part)
170
- return completed.returncode, output
171
-
172
-
173
- def completion_test_summary(output: str, code: int) -> str:
174
- if "no approved every-dev-completion tests" in output:
175
- return "no approved every-dev-completion tests"
176
- for line in output.splitlines():
177
- if "[context-guard] test hub:" in line and " passed," in line and " failed," in line and " blocked" in line:
178
- summary = line.split("test hub:", 1)[1].strip().rstrip(".")
179
- if code == 0:
180
- return f"all approved tests passed ({summary})"
181
- return f"approved tests are not all passing ({summary})"
182
- return "test hub status unknown; inspect `.codex/context/test-hub/last-run.json`"
183
-
184
-
185
- def prompt_text(raw: str) -> str:
186
- if not raw.strip():
187
- return ""
188
- try:
189
- data = json.loads(raw)
190
- except Exception:
191
- return raw
192
- pieces: list[str] = []
193
-
194
- def walk(value: object) -> None:
195
- if isinstance(value, str):
196
- pieces.append(value)
197
- elif isinstance(value, list):
198
- for item in value:
199
- walk(item)
200
- elif isinstance(value, dict):
201
- for key in ("prompt", "message", "text", "content", "input"):
202
- if key in value:
203
- walk(value[key])
204
-
205
- walk(data)
206
- return "\n".join(pieces)
207
-
208
-
209
- def looks_like_bad_case(text: str) -> bool:
210
- lowered = text.lower()
211
- markers = [
212
- "bug",
213
- "bad case",
214
- "regression",
215
- "broken",
216
- "error",
217
- "failed",
218
- "failing",
219
- "doesn't work",
220
- "not working",
221
- "紧急",
222
- "报错",
223
- "失败",
224
- "坏例",
225
- "复现",
226
- "回归",
227
- "不对",
228
- "有问题",
229
- ]
230
- return any(marker in lowered for marker in markers)
231
-
232
-
233
- def looks_like_task_switch(text: str) -> bool:
234
- lowered = text.lower()
235
- markers = [
236
- "urgent",
237
- "instead",
238
- "switch",
239
- "pause",
240
- "later",
241
- "different",
242
- "unrelated",
243
- "先",
244
- "暂停",
245
- "等下",
246
- "换个",
247
- "另一个",
248
- "紧急",
249
- "回头",
250
- "先不",
251
- ]
252
- return any(marker in lowered for marker in markers)
253
-
254
-
255
- def looks_like_goal_mode(text: str) -> bool:
256
- lowered = text.lower()
257
- markers = [
258
- "goal mode",
259
- "goal模式",
260
- "goal 模式",
261
- "active goal",
262
- "long-running",
263
- "autonomous",
264
- "目标模式",
265
- "长期目标",
266
- "持续执行",
267
- "自动继续",
268
- ]
269
- return any(marker in lowered for marker in markers)
270
-
271
-
272
- def looks_like_remote_work(text: str) -> bool:
273
- lowered = text.lower()
274
- markers = [
275
- "ssh",
276
- "remote server",
277
- "remote host",
278
- "dev server",
279
- "jump host",
280
- "服务器",
281
- "远程",
282
- "开发机",
283
- "跳板机",
284
- "通过ssh",
285
- "通过 ssh",
286
- ]
287
- return any(marker in lowered for marker in markers)
288
-
289
-
290
- def looks_like_test_creation(text: str) -> bool:
291
- lowered = text.lower()
292
- creation_markers = [
293
- "create",
294
- "write",
295
- "generate",
296
- "design",
297
- "add",
298
- "创建",
299
- "建立",
300
- "写",
301
- "生成",
302
- "设计",
303
- "新增",
304
- "加一个",
305
- "做一个",
306
- ]
307
- test_markers = [
308
- "test case",
309
- "task case",
310
- "test task",
311
- "testing task",
312
- "测试case",
313
- "测试 case",
314
- "测试任务",
315
- "测试用例",
316
- "测评任务",
317
- "测评case",
318
- "测评 case",
319
- "测试链路",
320
- "测试",
321
- ]
322
- return any(marker in lowered for marker in creation_markers) and any(marker in lowered for marker in test_markers)
323
-
324
-
325
- def looks_like_explicit_branch(text: str) -> bool:
326
- lowered = text.lower()
327
- markers = [
328
- "branch task",
329
- "side task",
330
- "side route",
331
- "fork this",
332
- "create a branch",
333
- "new branch",
334
- "as a branch",
335
- "支线",
336
- "分支",
337
- "开一个分支",
338
- "开一条支线",
339
- "创建支线",
340
- "创建分支",
341
- "作为支线",
342
- ]
343
- return any(marker in lowered for marker in markers)
344
-
345
-
346
- def looks_like_route_drift(text: str) -> bool:
347
- lowered = text.lower()
348
- drift_markers = [
349
- "significantly diverge",
350
- "diverge from",
351
- "different architecture",
352
- "new architecture",
353
- "new direction",
354
- "refactor direction",
355
- "偏离",
356
- "显著偏离",
357
- "新的架构",
358
- "新方向",
359
- "重构方向",
360
- "主线架构",
361
- ]
362
- return any(marker in lowered for marker in drift_markers)
363
-
364
-
365
- def bad_case_blocks(text: str) -> list[dict[str, str]]:
366
- blocks: list[dict[str, str]] = []
367
- current: dict[str, str] | None = None
368
- for line in text.splitlines():
369
- heading = line.startswith("### BC-")
370
- if heading:
371
- if current:
372
- blocks.append(current)
373
- identifier, _, title = line.removeprefix("### ").partition(":")
374
- current = {"id": identifier.strip(), "title": title.strip()}
375
- continue
376
- if current is None or not line.startswith("- ") or ":" not in line:
377
- continue
378
- key, _, value = line[2:].partition(":")
379
- current[key.strip().lower()] = value.strip()
380
- if current:
381
- blocks.append(current)
382
- return blocks
383
-
384
-
385
- def unresolved_bad_cases(path: Path) -> list[dict[str, str]]:
386
- if not path.exists():
387
- return []
388
- resolved_statuses = {"resolved", "done", "superseded-by-route-change"}
389
- cases = bad_case_blocks(path.read_text(encoding="utf-8"))
390
- return [
391
- case
392
- for case in cases
393
- if case.get("status", "").strip().lower() not in resolved_statuses
394
- ]
395
-
396
-
397
- def format_unresolved_bad_cases(cases: list[dict[str, str]], limit: int = 5) -> str:
398
- if not cases:
399
- return "none"
400
- parts = []
401
- for case in cases[:limit]:
402
- status = case.get("status", "unknown") or "unknown"
403
- title = case.get("title", "Untitled bad case")
404
- phenomenon = case.get("phenomenon", "").strip()
405
- summary = f"{title} ({status})"
406
- if phenomenon:
407
- summary += f" - {phenomenon}"
408
- parts.append(summary)
409
- if len(cases) > limit:
410
- parts.append(f"{len(cases) - limit} more unresolved bad cases")
411
- return "; ".join(parts)
412
-
413
-
414
- def main() -> int:
415
- event = sys.argv[1] if len(sys.argv) > 1 else "unknown"
416
- raw = read_stdin()
417
- root, root_source = event_root(raw, Path.cwd())
418
- context_dir = root / ".codex" / "context"
419
- index_path = context_dir / "index.md"
420
- roadmap_path = context_dir / "roadmap.md"
421
- bad_cases_path = context_dir / "bad-cases.md"
422
- text = prompt_text(raw)
423
-
424
- if is_context_guard_skill_path(root):
425
- hook_log(
426
- "[context-guard] detected Context Guard skill directory as the apparent root; "
427
- "skipping project context writes. Open the target Codex folder or pass an explicit local `--root` "
428
- "when showing/updating a roadmap."
429
- )
430
- hook_log(f"[context-guard] apparent root source: {root_source}; apparent root: {root}")
431
- return hook_response()
432
-
433
- if event == "session-start":
434
- created = init_context(root)
435
- if created:
436
- hook_log(f"[context-guard] initialized folder context: {context_dir}")
437
- else:
438
- hook_log(f"[context-guard] folder context ready: {context_dir}")
439
- hook_log(f"[context-guard] project root: {root} ({root_source})")
440
- hook_log("[context-guard] context location rule: save project context only under `<opened local Codex project root>/.codex/context/`.")
441
- hook_log("[context-guard] use .codex/context/index.md for quick scan and .codex/context/roadmap.md for route nodes.")
442
- return hook_response()
443
-
444
- if event == "user-prompt-submit":
445
- hints: list[str] = []
446
- if looks_like_goal_mode(text):
447
- hints.append("goal mode: align active goal with current context and record roadmap/bad-case checkpoints during long-running work")
448
- if looks_like_remote_work(text):
449
- 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")
450
- if looks_like_test_creation(text):
451
- 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")
452
- if looks_like_explicit_branch(text):
453
- 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:")
454
- elif looks_like_route_drift(text):
455
- hints.append("possible route drift: ask whether to create a branch before moving away from the current mainline architecture")
456
- if looks_like_task_switch(text):
457
- hints.append("possible task switch: park current context in .codex/context/index.md before switching")
458
- if looks_like_bad_case(text):
459
- hints.append("possible bad case: record/update .codex/context/bad-cases.md or task-local bad-cases.md")
460
- if not hints:
461
- hints.append("run Context Guard intake: continue current context or note no active context")
462
- hook_log("[context-guard] " + "; ".join(hints))
463
- hook_log(f"[context-guard] root source: {root_source}")
464
- hook_log(f"[context-guard] project root: {root}")
465
- hook_log(f"[context-guard] context folder: {context_dir}")
466
- hook_log(f"[context-guard] context index: {index_path}")
467
- hook_log(f"[context-guard] route map: {roadmap_path}")
468
- return hook_response()
469
-
470
- if event == "stop":
471
- 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.")
472
- 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.")
473
- 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.")
474
- 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.")
475
- hook_log("[context-guard] TEST HUB GATE: Stop hook runs `context_guard.py dev-complete --root <project>` so the hub executes 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.")
476
- 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.")
477
- 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.")
478
- 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.")
479
- 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.")
480
- 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.")
481
- 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.")
482
- 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.")
483
- 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.")
484
- 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.")
485
- hook_log("[context-guard] final answer must include verification evidence and must not say done/fixed/passing unless the gate above was satisfied.")
486
- hook_log(f"[context-guard] root source: {root_source}")
487
- hook_log(f"[context-guard] project root: {root}")
488
- hook_log(f"[context-guard] context folder: {context_dir}")
489
- hook_log(f"[context-guard] bad-case register: {bad_cases_path}")
490
- try:
491
- test_code, test_output = run_test_hub_completion(root)
492
- for line in test_output.splitlines():
493
- hook_log(line)
494
- hook_log(
495
- "[context-guard] final answer must include Test Hub summary: "
496
- + completion_test_summary(test_output, test_code)
497
- )
498
- if test_code != 0:
499
- reason = (
500
- "Context Guard Test Hub found failed or blocked approved tests. "
501
- "Read `.codex/context/test-hub/last-run.json` and preserved run evidence, "
502
- "then fix or report the blocker before finalizing."
503
- )
504
- hook_log(f"[context-guard] TEST HUB BLOCKER: {reason}")
505
- return hook_response(decision="block", reason=reason)
506
- except subprocess.TimeoutExpired:
507
- reason = "Context Guard Test Hub timed out after 900s; report this blocker and evidence before finalizing."
508
- hook_log(f"[context-guard] TEST HUB BLOCKER: {reason}")
509
- return hook_response(decision="block", reason=reason)
510
- except Exception as exc:
511
- hook_log(f"[context-guard] test hub hook warning: {exc}")
512
- open_cases = unresolved_bad_cases(bad_cases_path)
513
- hook_log("[context-guard] final answer must include BC summary: archived/updated BC this turn, and current unresolved BC.")
514
- hook_log(f"[context-guard] current unresolved BC: {format_unresolved_bad_cases(open_cases)}")
515
- return hook_response()
516
-
517
- hook_log("[context-guard] unknown hook event; use the context-guard skill if context changed.")
518
- return hook_response()
519
-
520
-
521
- if __name__ == "__main__":
522
- raise SystemExit(main())
@@ -1,116 +0,0 @@
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