@devcxl/opencode-cabbage 1.2.0 → 2.0.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.
Files changed (104) hide show
  1. package/.codex-plugin/plugin.json +36 -0
  2. package/README.md +67 -21
  3. package/README.zh.md +26 -13
  4. package/assets/agents/dev-lifecycle.md +6 -0
  5. package/assets/prompts/bootstrap.md +4 -0
  6. package/assets/skills/flow-change/SKILL.md +109 -0
  7. package/assets/skills/flow-design/SKILL.md +15 -0
  8. package/assets/skills/flow-docs/SKILL.md +112 -0
  9. package/assets/skills/flow-incident/SKILL.md +98 -0
  10. package/assets/skills/flow-research/SKILL.md +9 -0
  11. package/assets/skills/flow-setup/SKILL.md +13 -0
  12. package/codex-skills/agent-architect/SKILL.md +79 -0
  13. package/codex-skills/agent-dev-lifecycle/SKILL.md +311 -0
  14. package/codex-skills/agent-developer/SKILL.md +51 -0
  15. package/codex-skills/agent-goal-verify/SKILL.md +45 -0
  16. package/codex-skills/agent-researcher/SKILL.md +32 -0
  17. package/codex-skills/agent-reviewer/SKILL.md +109 -0
  18. package/codex-skills/flow-change/SKILL.md +109 -0
  19. package/codex-skills/flow-code/SKILL.md +71 -0
  20. package/codex-skills/flow-design/SKILL.md +161 -0
  21. package/codex-skills/flow-docs/SKILL.md +112 -0
  22. package/codex-skills/flow-incident/SKILL.md +98 -0
  23. package/codex-skills/flow-release/SKILL.md +90 -0
  24. package/codex-skills/flow-requirements/SKILL.md +90 -0
  25. package/codex-skills/flow-research/SKILL.md +86 -0
  26. package/codex-skills/flow-review/SKILL.md +110 -0
  27. package/codex-skills/flow-setup/SKILL.md +88 -0
  28. package/codex-skills/flow-tasks/SKILL.md +220 -0
  29. package/codex-skills/flow-tdd/SKILL.md +235 -0
  30. package/dist/hooks/session-start.d.ts +1 -0
  31. package/dist/hooks/session-start.js +72 -0
  32. package/dist/hooks/session-start.js.map +1 -0
  33. package/dist/index.d.ts +2 -5
  34. package/dist/index.js +1 -4
  35. package/dist/index.js.map +1 -1
  36. package/dist/plugin/agents.d.ts +20 -0
  37. package/dist/plugin/agents.js +79 -0
  38. package/dist/plugin/agents.js.map +1 -1
  39. package/dist/plugin/cabbage-cli.d.ts +28 -0
  40. package/dist/plugin/cabbage-cli.js +92 -0
  41. package/dist/plugin/cabbage-cli.js.map +1 -0
  42. package/dist/plugin/cabbage-tool.d.ts +103 -0
  43. package/dist/plugin/cabbage-tool.js +169 -0
  44. package/dist/plugin/cabbage-tool.js.map +1 -0
  45. package/dist/plugin/commands.d.ts +10 -0
  46. package/dist/plugin/commands.js +1 -1
  47. package/dist/plugin/commands.js.map +1 -1
  48. package/dist/plugin/continuation.d.ts +22 -0
  49. package/dist/plugin/continuation.js +233 -0
  50. package/dist/plugin/continuation.js.map +1 -0
  51. package/dist/plugin/goal-store.d.ts +64 -0
  52. package/dist/plugin/goal-store.js +57 -0
  53. package/dist/plugin/goal-store.js.map +1 -0
  54. package/dist/plugin/goal.d.ts +95 -38
  55. package/dist/plugin/goal.js +208 -199
  56. package/dist/plugin/goal.js.map +1 -1
  57. package/dist/plugin/install-dir.d.ts +13 -0
  58. package/dist/plugin/install-dir.js +46 -0
  59. package/dist/plugin/install-dir.js.map +1 -0
  60. package/dist/plugin/server.d.ts +4 -12
  61. package/dist/plugin/server.js +279 -423
  62. package/dist/plugin/server.js.map +1 -1
  63. package/dist/plugin/session-abort.d.ts +5 -0
  64. package/dist/plugin/session-abort.js +26 -0
  65. package/dist/plugin/session-abort.js.map +1 -0
  66. package/dist/plugin/skills.d.ts +15 -0
  67. package/dist/plugin/skills.js +47 -50
  68. package/dist/plugin/skills.js.map +1 -1
  69. package/dist/plugin.d.ts +2 -2
  70. package/dist/plugin.js +6 -2
  71. package/dist/plugin.js.map +1 -1
  72. package/dist/util/paths.d.ts +0 -6
  73. package/dist/util/paths.js +0 -15
  74. package/dist/util/paths.js.map +1 -1
  75. package/hooks/hooks.json +17 -0
  76. package/package.json +13 -8
  77. package/vendor/cabbage/MANIFEST.json +8 -0
  78. package/vendor/cabbage/cabbage_cli/__init__.py +1 -0
  79. package/vendor/cabbage/cabbage_cli/__main__.py +2 -0
  80. package/vendor/cabbage/cabbage_cli/assets/docs-site/.vitepress/config.ts +78 -0
  81. package/vendor/cabbage/cabbage_cli/assets/docs-site/README.md +31 -0
  82. package/vendor/cabbage/cabbage_cli/assets/docs-site/package.json +16 -0
  83. package/vendor/cabbage/cabbage_cli/assets/docs-site/pnpm-lock.yaml +2504 -0
  84. package/vendor/cabbage/cabbage_cli/assets/integrations/cabbage.yml +127 -0
  85. package/vendor/cabbage/cabbage_cli/assets/templates/adr.md +47 -0
  86. package/vendor/cabbage/cabbage_cli/assets/templates/api-design.md +45 -0
  87. package/vendor/cabbage/cabbage_cli/assets/templates/change-record.md +21 -0
  88. package/vendor/cabbage/cabbage_cli/assets/templates/database-design.md +37 -0
  89. package/vendor/cabbage/cabbage_cli/assets/templates/generic.md +20 -0
  90. package/vendor/cabbage/cabbage_cli/assets/templates/impact.md +41 -0
  91. package/vendor/cabbage/cabbage_cli/assets/templates/incident.md +48 -0
  92. package/vendor/cabbage/cabbage_cli/assets/templates/postmortem.md +43 -0
  93. package/vendor/cabbage/cabbage_cli/assets/templates/prd.md +71 -0
  94. package/vendor/cabbage/cabbage_cli/assets/templates/release-plan.md +45 -0
  95. package/vendor/cabbage/cabbage_cli/assets/templates/rfc.md +44 -0
  96. package/vendor/cabbage/cabbage_cli/assets/templates/security-review.md +35 -0
  97. package/vendor/cabbage/cabbage_cli/assets/templates/tasks.md +62 -0
  98. package/vendor/cabbage/cabbage_cli/assets/templates/tech-spec.md +87 -0
  99. package/vendor/cabbage/cabbage_cli/assets/templates/test-plan.md +44 -0
  100. package/vendor/cabbage/cabbage_cli/assets/workflows/change.yaml +11 -0
  101. package/vendor/cabbage/cabbage_cli/assets/workflows/incident.yaml +4 -0
  102. package/vendor/cabbage/cabbage_cli/cli.py +283 -0
  103. package/vendor/cabbage/cabbage_cli/core.py +606 -0
  104. package/vendor/cabbage/cabbage_cli/scaffold.py +313 -0
@@ -0,0 +1,313 @@
1
+ from __future__ import annotations
2
+ import shutil
3
+ from importlib import resources
4
+ from pathlib import Path
5
+ import re
6
+ from .core import (CabbageError, dump_yaml, load_config, load_yaml, change_dir, now_iso,
7
+ project_root, workflow, condition_enabled, write_text_atomic, CABBAGE_DIR)
8
+
9
+ IMPACT_FIELD_LABELS={
10
+ "product":"Product","design":"Design","architecture":"Architecture","rfc":"RFC","api":"API","database":"Database",
11
+ "security":"Security","testing":"Testing","deployment":"Deployment","operations":"Operations",
12
+ "performance":"Performance"
13
+ }
14
+ IMPACT_FIELDS=list(IMPACT_FIELD_LABELS)
15
+
16
+ ADOPTION_IGNORED_DIRS={".git",".cabbage",".github",".idea",".vscode",".venv","venv","node_modules","__pycache__","dist","build","target","coverage","site",".cache",".temp",".vitepress",".vuepress","vendor"}
17
+
18
+ ADOPTION_CATEGORY_RULES=[
19
+ ("adr",{"adr"}),
20
+ ("rfc",{"rfc","proposal","proposals"}),
21
+ ("incident",{"incident","incidents","postmortem","postmortems","retrospective","retro"}),
22
+ ("api",{"api","apis","openapi","swagger","webhook","webhooks"}),
23
+ ("data",{"database","databases","db","schema","schemas","migration","migrations","erd","datamodel","domain","domains"}),
24
+ ("security",{"security","threat","threats"}),
25
+ ("testing",{"test","tests","testing","qa","e2e"}),
26
+ ("infrastructure",{"infrastructure","infra","kubernetes","k8s","terraform","helm","ansible"}),
27
+ ("ci-cd",{"ci","cd","pipeline","pipelines","jenkins"}),
28
+ ("release",{"release","releases","changelog","changelogs"}),
29
+ ("operations",{"runbook","runbooks","oncall","operations","ops","monitoring","alerting","observability","slo","sli"}),
30
+ ("performance",{"performance","benchmark","benchmarks","load"}),
31
+ ("product",{"prd","requirement","requirements","user-story","user-stories","product","roadmap"}),
32
+ ("architecture",{"architecture","system-design","tech-design","design","design-doc"}),
33
+ ("development",{"development","contributing","onboarding","setup","installation","getting-started"}),
34
+ ("compliance",{"compliance","policy","policies","governance"}),
35
+ ("overview",{"overview","about","introduction","readme"}),
36
+ ]
37
+
38
+ ADOPTION_TARGET_BY_CATEGORY={
39
+ "adr":"docs/03-architecture/adr","rfc":"docs/03-architecture/rfc","incident":"docs/15-incidents",
40
+ "product":"docs/01-product","architecture":"docs/03-architecture","data":"docs/04-domain","api":"docs/05-api",
41
+ "development":"docs/06-development","testing":"docs/08-testing","security":"docs/09-security",
42
+ "infrastructure":"docs/10-infrastructure","ci-cd":"docs/11-ci-cd","release":"docs/12-release",
43
+ "operations":"docs/13-operations","performance":"docs/14-performance","compliance":"docs/17-compliance",
44
+ "overview":"docs/00-overview",
45
+ }
46
+
47
+ ADOPTION_HISTORICAL={"adr","rfc","incident"}
48
+
49
+ ADOPTION_CONFORMING_AREAS={
50
+ "00":"overview","01":"product","02":"design","03":"architecture","04":"data","05":"api",
51
+ "06":"development","07":"standards","08":"testing","09":"security","10":"infrastructure",
52
+ "11":"ci-cd","12":"release","13":"operations","14":"performance","15":"incident","16":"dependencies","17":"compliance",
53
+ }
54
+
55
+ ALL_CONFORMING_DIRS = [
56
+ "00-overview", "01-product", "02-design", "03-architecture/adr", "03-architecture/rfc",
57
+ "03-architecture/system-design", "04-domain", "05-api", "06-development", "07-standards",
58
+ "08-testing", "09-security", "10-infrastructure", "11-ci-cd", "12-release",
59
+ "13-operations", "14-performance", "15-incidents", "16-dependencies", "17-compliance"
60
+ ]
61
+
62
+ def asset(path: str):
63
+ return resources.files("cabbage_cli").joinpath("assets", path)
64
+
65
+ def copy_asset_tree(src_rel: str, dst: Path, overwrite: bool=False):
66
+ src=asset(src_rel)
67
+ dst.mkdir(parents=True,exist_ok=True)
68
+ for item in src.iterdir():
69
+ target=dst/item.name
70
+ if item.is_dir(): copy_asset_tree(f"{src_rel}/{item.name}", target, overwrite)
71
+ elif overwrite or not target.exists():
72
+ target.write_bytes(item.read_bytes())
73
+
74
+ def init_project(root: Path, force: bool=False, vendor_cli: bool=True):
75
+ d=root/CABBAGE_DIR
76
+ if (d/"config.yaml").exists() and not force:
77
+ raise CabbageError(f"{CABBAGE_DIR} already initialized; use --force to refresh missing scaffold files")
78
+ d.mkdir(parents=True,exist_ok=True)
79
+ cfg={
80
+ "version":1,
81
+ "impact_fields":IMPACT_FIELDS,
82
+ "ci":{
83
+ "require_change_for_code":True,
84
+ "require_current_state_docs":True,
85
+ "exclude_prefixes":["docs/",".cabbage/",".github/","README","LICENSE"],
86
+ "current_state_rules":{
87
+ "product":["docs/01-product/"],
88
+ "architecture":["docs/03-architecture/"],
89
+ "api":["docs/05-api/"],
90
+ "database":["docs/04-domain/"],
91
+ "security":["docs/09-security/"],
92
+ "testing":["docs/08-testing/"],
93
+ "deployment":["docs/10-infrastructure/","docs/11-ci-cd/","docs/12-release/"],
94
+ "operations":["docs/13-operations/"],
95
+ "performance":["docs/14-performance/"]
96
+ }
97
+ },
98
+ "docs":{"dir":"docs"}
99
+ }
100
+ dump_yaml(d/"config.yaml",cfg)
101
+ copy_asset_tree("workflows",d/"workflows",overwrite=force)
102
+ (d/"changes").mkdir(exist_ok=True); (d/"archive").mkdir(exist_ok=True)
103
+ copy_asset_tree("docs-site",root/"docs",overwrite=False)
104
+ # current-state doc skeleton
105
+ for name in ALL_CONFORMING_DIRS:
106
+ (root/"docs"/name).mkdir(parents=True,exist_ok=True)
107
+ # CI template
108
+ gh=root/".github/workflows"; gh.mkdir(parents=True,exist_ok=True)
109
+ ci=asset("integrations/cabbage.yml").read_text(encoding="utf-8")
110
+ cip=gh/"cabbage.yml"
111
+ if not cip.exists(): cip.write_text(ci,encoding="utf-8")
112
+ gi=root/".gitignore"
113
+ ignores=["docs/node_modules/","docs/.vitepress/cache/","docs/.vitepress/dist/"]
114
+ existing=gi.read_text(encoding="utf-8") if gi.exists() else ""
115
+ add=[x for x in ignores if x not in existing.splitlines()]
116
+ if add:
117
+ with gi.open("a",encoding="utf-8") as f:
118
+ if existing and not existing.endswith("\n"): f.write("\n")
119
+ f.write("\n# cabbage / VitePress\n"+"\n".join(add)+"\n")
120
+ if vendor_cli:
121
+ sync_vendored_cli(root)
122
+
123
+ def sync_vendored_cli(root: Path):
124
+ """Regenerate the repository-local CLI from the installed source package."""
125
+ pkg = Path(__file__).resolve().parent
126
+ target = root / CABBAGE_DIR / "tooling/cabbage_cli"
127
+ if pkg == target.resolve():
128
+ raise CabbageError("cannot refresh vendored CLI from itself; use the source package")
129
+ if target.exists():
130
+ shutil.rmtree(target)
131
+ shutil.copytree(pkg, target, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
132
+
133
+ def render_template(stage: dict, change_id: str, change_type: str) -> str:
134
+ template=stage.get("template","generic.md")
135
+ txt=asset(f"templates/{template}").read_text(encoding="utf-8")
136
+ return txt.replace("{{CHANGE_ID}}",change_id).replace("{{STAGE_ID}}",stage["id"]).replace("{{CHANGE_TYPE}}",change_type)
137
+
138
+ def sync_impact_document(root: Path, change_id: str):
139
+ p=change_dir(root,change_id)/"impact.md"
140
+ if not p.exists(): return
141
+ spec=load_yaml(change_dir(root,change_id)/"change.yaml")
142
+ text=p.read_text(encoding="utf-8")
143
+ for key,label in IMPACT_FIELD_LABELS.items():
144
+ val="Yes" if spec.get("impact",{}).get(key,False) else "No"
145
+ pattern=rf"^(\|\s*{re.escape(label)}\s*\|)\s*(Yes|No)\s*(\|.*)$"
146
+ text=re.sub(pattern,rf"\1 {val} \3",text,flags=re.M|re.I)
147
+ write_text_atomic(p,text,encoding="utf-8")
148
+
149
+ def ensure_artifacts(root: Path, change_id: str):
150
+ spec=load_yaml(change_dir(root,change_id)/"change.yaml")
151
+ wf,_=workflow(root,spec["type"])
152
+ for st in wf.get("stages",[]):
153
+ if condition_enabled(st,spec) and st.get("artifact"):
154
+ p=change_dir(root,change_id)/st["artifact"]
155
+ if not p.exists():
156
+ p.parent.mkdir(parents=True,exist_ok=True)
157
+ p.write_text(render_template(st,change_id,spec["type"]),encoding="utf-8")
158
+ sync_impact_document(root,change_id)
159
+
160
+ def new_change(root: Path, change_type: str, change_id: str):
161
+ cfg=load_config(root)
162
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*",change_id):
163
+ raise CabbageError("change id must be kebab-case")
164
+ workflow(root,change_type) # raises `unknown change type` when missing
165
+ d=change_dir(root,change_id)
166
+ if d.exists(): raise CabbageError(f"change already exists: {change_id}")
167
+ d.mkdir(parents=True)
168
+ spec={"id":change_id,"type":change_type,"status":"active","impact":{k:False for k in cfg.get("impact_fields",[])}}
169
+ # Defaults declare only impacts the workflow can produce evidence for; the rest stay false for explicit opt-in.
170
+ # Legacy types keep their defaults so existing projects behave exactly as before the new-project model.
171
+ if change_type!="incident": spec["impact"]["testing"]=True
172
+ if change_type=="change" or change_type=="feature": spec["impact"]["product"]=True
173
+ if change_type=="architecture": spec["impact"]["architecture"]=True
174
+ if change_type in {"migration"}: spec["impact"]["database"]=True; spec["impact"]["deployment"]=True
175
+ if change_type=="integration": spec["impact"]["api"]=True
176
+ dump_yaml(d/"change.yaml",spec)
177
+ ensure_artifacts(root,change_id)
178
+
179
+ def iter_adoption_docs(root: Path):
180
+ docs_name=load_config(root).get("docs",{}).get("dir","docs")
181
+ for p in sorted(root.rglob("*.md")):
182
+ rel=p.relative_to(root)
183
+ parts=rel.parts
184
+ if not parts or parts[0] in ADOPTION_IGNORED_DIRS or parts[0].startswith("."):
185
+ continue
186
+ if parts[0]==docs_name:
187
+ if conforming_area(rel,docs_name):
188
+ yield rel
189
+ continue
190
+ if any(part in ADOPTION_IGNORED_DIRS for part in parts[:-1]):
191
+ continue
192
+ if p.name.upper()=="README.md" and len(parts)==1:
193
+ continue
194
+ yield rel
195
+
196
+ def classify_adoption_doc(rel: Path) -> tuple[str,str|None,str|None]:
197
+ name_parts=[p.lower() for p in rel.with_suffix("").parts]
198
+ category=next((c for c,hints in ADOPTION_CATEGORY_RULES if any(h in name_parts for h in hints)),None)
199
+ if category is None:
200
+ lowered="/".join(name_parts)
201
+ category=next((c for c,hints in ADOPTION_CATEGORY_RULES
202
+ if any(re.search(rf"(^|[-_/]){re.escape(h)}([-_/]|$)",lowered) for h in hints)),None)
203
+ if category is None:
204
+ return "review",None,None
205
+ action="import" if category in ADOPTION_HISTORICAL else "migrate"
206
+ return action,category,ADOPTION_TARGET_BY_CATEGORY[category]
207
+
208
+ def conforming_area(rel: Path, docs_name: str|None=None) -> str|None:
209
+ parts=rel.parts
210
+ if docs_name and parts and parts[0]==docs_name:
211
+ parts=parts[1:]
212
+ first=parts[0] if parts else ""
213
+ m=re.match(r"^(\d{2})-",first)
214
+ if m and m.group(1) in ADOPTION_CONFORMING_AREAS:
215
+ return ADOPTION_CONFORMING_AREAS[m.group(1)]
216
+ return None
217
+
218
+ def discard_change(root: Path, change_id: str) -> None:
219
+ d = change_dir(root, change_id)
220
+ if not d.exists():
221
+ raise CabbageError(f"change `{change_id}` does not exist")
222
+ shutil.rmtree(d)
223
+
224
+ def adopt_project(root: Path, apply: bool = False) -> dict:
225
+ project_root(root)
226
+ load_config(root)
227
+ rows=[]; counts={"migrate":0,"import":0,"keep":0,"review":0}
228
+ docs_name=load_config(root).get("docs",{}).get("dir","docs")
229
+ for rel in iter_adoption_docs(root):
230
+ area=conforming_area(rel,docs_name)
231
+ if area:
232
+ action,category,target="keep",area,None
233
+ else:
234
+ action,category,target=classify_adoption_doc(rel)
235
+ counts[action]+=1
236
+ rows.append({"path":str(rel).replace("\\","/"),"action":action,"category":category,"target":target})
237
+
238
+ applied = []
239
+ warnings = []
240
+ if apply:
241
+ for r in rows:
242
+ if r["action"] in {"migrate", "import"} and r["target"]:
243
+ src_path = root / r["path"]
244
+ dst_dir = root / r["target"]
245
+ dst_dir.mkdir(parents=True, exist_ok=True)
246
+ dst_path = dst_dir / src_path.name
247
+ if not dst_path.exists():
248
+ shutil.move(str(src_path), str(dst_path))
249
+ if r["action"] == "migrate":
250
+ try:
251
+ text = dst_path.read_text(encoding="utf-8")
252
+ if not text.startswith("---\n"):
253
+ fm = f"---\ncategory: {r['category']}\nadopted_at: {now_iso()}\n---\n\n"
254
+ dst_path.write_text(fm + text, encoding="utf-8")
255
+ except (OSError, UnicodeDecodeError) as e:
256
+ warnings.append(f"{dst_path.relative_to(root)}: frontmatter not added ({e})")
257
+ applied.append({"from": r["path"], "to": str(dst_path.relative_to(root)).replace("\\", "/")})
258
+
259
+ if applied:
260
+ # Re-scan after applying
261
+ res = adopt_project(root, apply=False)
262
+ res["applied"] = applied
263
+ res["warnings"] = warnings + res.get("warnings", [])
264
+ return res
265
+
266
+ report=render_adoption_report(root,rows)
267
+ out=root/CABBAGE_DIR/"adoption-report.md"
268
+ out.parent.mkdir(parents=True,exist_ok=True)
269
+ out.write_text(report,encoding="utf-8")
270
+ return {"report":str(out.relative_to(root)),"counts":counts,"documents":rows, "applied": applied, "warnings": warnings}
271
+
272
+ def render_adoption_report(root: Path, rows: list[dict]) -> str:
273
+ docs_name=load_config(root).get("docs",{}).get("dir","docs")
274
+ lines=[
275
+ "---",
276
+ "cabbage_stage: adoption",
277
+ f"generated_at: {now_iso()}",
278
+ "---",
279
+ "",
280
+ "# Adoption report",
281
+ "",
282
+ "Inventory of existing documentation. This report is advisory only; no files were moved.",
283
+ "",
284
+ "## Action legend",
285
+ "",
286
+ "| Action | Meaning |",
287
+ "|---|---|",
288
+ "| keep | Already inside the standard current-state tree; no move needed |",
289
+ "| migrate | Current-state document; move into the standard tree during adoption |",
290
+ "| import | Historical record (ADR/RFC/incident); archive as-is under the standard tree |",
291
+ "| review | Unclassified; a human decides whether to migrate, import, or leave it |",
292
+ "",
293
+ "## Documents",
294
+ "",
295
+ "| Path | Action | Category | Suggested target |",
296
+ "|---|---|---|---|",
297
+ ]
298
+ for r in rows:
299
+ lines.append(f"| `{r['path']}` | {r['action']} | {r['category'] or '-'} | `{r['target'] or '-'}` |")
300
+ lines+=[
301
+ "",
302
+ "## Adoption steps",
303
+ "",
304
+ "1. Resolve each `review` row by hand, then re-run this command.",
305
+ "2. Move `migrate` rows into their suggested target and fix intra-project links.",
306
+ "3. Import `import` rows as immutable history: do not rewrite their content.",
307
+ "4. Record completed moves in a change record (`cabbage new change adopt-existing-docs`).",
308
+ "5. Verify the current-state site (`cabbage docs build`), then enable CI gates.",
309
+ "",
310
+ f"The standard current-state tree is `{docs_name}/`; see the cabbage-docs skill for the layout.",
311
+ "",
312
+ ]
313
+ return "\n".join(lines)