@aipper/aiws-spec 0.0.1

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 (145) hide show
  1. package/README.md +12 -0
  2. package/docs/cli-interface.md +288 -0
  3. package/docs/spec-contract.md +183 -0
  4. package/package.json +18 -0
  5. package/templates/workspace/.agents/skills/aiws-change-archive/SKILL.md +23 -0
  6. package/templates/workspace/.agents/skills/aiws-change-list/SKILL.md +18 -0
  7. package/templates/workspace/.agents/skills/aiws-change-new/SKILL.md +26 -0
  8. package/templates/workspace/.agents/skills/aiws-change-next/SKILL.md +19 -0
  9. package/templates/workspace/.agents/skills/aiws-change-start/SKILL.md +27 -0
  10. package/templates/workspace/.agents/skills/aiws-change-status/SKILL.md +19 -0
  11. package/templates/workspace/.agents/skills/aiws-change-sync/SKILL.md +19 -0
  12. package/templates/workspace/.agents/skills/aiws-change-templates-init/SKILL.md +18 -0
  13. package/templates/workspace/.agents/skills/aiws-change-templates-which/SKILL.md +18 -0
  14. package/templates/workspace/.agents/skills/aiws-change-validate/SKILL.md +23 -0
  15. package/templates/workspace/.agents/skills/aiws-hooks-install/SKILL.md +30 -0
  16. package/templates/workspace/.agents/skills/aiws-hooks-status/SKILL.md +18 -0
  17. package/templates/workspace/.agents/skills/aiws-init/SKILL.md +27 -0
  18. package/templates/workspace/.agents/skills/aiws-rollback/SKILL.md +18 -0
  19. package/templates/workspace/.agents/skills/aiws-update/SKILL.md +26 -0
  20. package/templates/workspace/.agents/skills/aiws-validate/SKILL.md +22 -0
  21. package/templates/workspace/.agents/skills/ws-analyze/SKILL.md +26 -0
  22. package/templates/workspace/.agents/skills/ws-commit/SKILL.md +50 -0
  23. package/templates/workspace/.agents/skills/ws-dev/SKILL.md +34 -0
  24. package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +54 -0
  25. package/templates/workspace/.agents/skills/ws-plan/SKILL.md +39 -0
  26. package/templates/workspace/.agents/skills/ws-preflight/SKILL.md +29 -0
  27. package/templates/workspace/.agents/skills/ws-req-change/SKILL.md +33 -0
  28. package/templates/workspace/.agents/skills/ws-req-contract-sync/SKILL.md +17 -0
  29. package/templates/workspace/.agents/skills/ws-req-contract-validate/SKILL.md +12 -0
  30. package/templates/workspace/.agents/skills/ws-req-flow-sync/SKILL.md +28 -0
  31. package/templates/workspace/.agents/skills/ws-req-review/SKILL.md +32 -0
  32. package/templates/workspace/.agents/skills/ws-review/SKILL.md +24 -0
  33. package/templates/workspace/.agents/skills/ws-rule/SKILL.md +23 -0
  34. package/templates/workspace/.aiws/manifest.json +36 -0
  35. package/templates/workspace/.claude/commands/aiws-init.md +19 -0
  36. package/templates/workspace/.claude/commands/aiws-rollback.md +12 -0
  37. package/templates/workspace/.claude/commands/aiws-update.md +18 -0
  38. package/templates/workspace/.claude/commands/aiws-validate.md +13 -0
  39. package/templates/workspace/.claude/commands/ws-analyze.md +27 -0
  40. package/templates/workspace/.claude/commands/ws-dev.md +24 -0
  41. package/templates/workspace/.claude/commands/ws-migrate.md +22 -0
  42. package/templates/workspace/.claude/commands/ws-preflight.md +27 -0
  43. package/templates/workspace/.claude/commands/ws-req-change.md +34 -0
  44. package/templates/workspace/.claude/commands/ws-req-contract-sync.md +18 -0
  45. package/templates/workspace/.claude/commands/ws-req-contract-validate.md +13 -0
  46. package/templates/workspace/.claude/commands/ws-req-flow-sync.md +20 -0
  47. package/templates/workspace/.claude/commands/ws-req-review.md +33 -0
  48. package/templates/workspace/.claude/commands/ws-review.md +25 -0
  49. package/templates/workspace/.claude/commands/ws-rule.md +24 -0
  50. package/templates/workspace/.codex/prompts/aiws-init.md +23 -0
  51. package/templates/workspace/.codex/prompts/aiws-rollback.md +16 -0
  52. package/templates/workspace/.codex/prompts/aiws-update.md +22 -0
  53. package/templates/workspace/.codex/prompts/aiws-validate.md +17 -0
  54. package/templates/workspace/.codex/prompts/ws-analyze.md +32 -0
  55. package/templates/workspace/.codex/prompts/ws-dev.md +29 -0
  56. package/templates/workspace/.codex/prompts/ws-migrate.md +27 -0
  57. package/templates/workspace/.codex/prompts/ws-preflight.md +32 -0
  58. package/templates/workspace/.codex/prompts/ws-req-change.md +39 -0
  59. package/templates/workspace/.codex/prompts/ws-req-contract-sync.md +23 -0
  60. package/templates/workspace/.codex/prompts/ws-req-contract-validate.md +18 -0
  61. package/templates/workspace/.codex/prompts/ws-req-flow-sync.md +25 -0
  62. package/templates/workspace/.codex/prompts/ws-req-review.md +38 -0
  63. package/templates/workspace/.codex/prompts/ws-review.md +30 -0
  64. package/templates/workspace/.codex/prompts/ws-rule.md +29 -0
  65. package/templates/workspace/.githooks/pre-commit +32 -0
  66. package/templates/workspace/.githooks/pre-push +32 -0
  67. package/templates/workspace/.iflow/agents/feature-reviewer.md +27 -0
  68. package/templates/workspace/.iflow/agents/requirements-analyst.md +24 -0
  69. package/templates/workspace/.iflow/agents/server-commit-manager.md +28 -0
  70. package/templates/workspace/.iflow/agents/server-fix-implementer.md +31 -0
  71. package/templates/workspace/.iflow/agents/server-test-planner.md +28 -0
  72. package/templates/workspace/.iflow/agents/server-test-triager.md +30 -0
  73. package/templates/workspace/.iflow/commands/aiws-init.toml +24 -0
  74. package/templates/workspace/.iflow/commands/aiws-rollback.toml +18 -0
  75. package/templates/workspace/.iflow/commands/aiws-update.toml +23 -0
  76. package/templates/workspace/.iflow/commands/aiws-validate.toml +18 -0
  77. package/templates/workspace/.iflow/commands/server-commit.toml +27 -0
  78. package/templates/workspace/.iflow/commands/server-drain.toml +99 -0
  79. package/templates/workspace/.iflow/commands/server-fix-and-commit.toml +27 -0
  80. package/templates/workspace/.iflow/commands/server-fix.toml +65 -0
  81. package/templates/workspace/.iflow/commands/server-test-plan.toml +62 -0
  82. package/templates/workspace/.iflow/commands/server-test.toml +58 -0
  83. package/templates/workspace/.iflow/commands/server-triage.toml +38 -0
  84. package/templates/workspace/.iflow/commands/server_test-plan.toml +12 -0
  85. package/templates/workspace/.iflow/commands/server_test.toml +12 -0
  86. package/templates/workspace/.iflow/commands/ws-analyze.toml +33 -0
  87. package/templates/workspace/.iflow/commands/ws-contract-check.toml +69 -0
  88. package/templates/workspace/.iflow/commands/ws-dev.toml +34 -0
  89. package/templates/workspace/.iflow/commands/ws-doctor.toml +141 -0
  90. package/templates/workspace/.iflow/commands/ws-env-doctor.toml +74 -0
  91. package/templates/workspace/.iflow/commands/ws-feature-deliver.toml +44 -0
  92. package/templates/workspace/.iflow/commands/ws-feature-plan.toml +47 -0
  93. package/templates/workspace/.iflow/commands/ws-init.toml +53 -0
  94. package/templates/workspace/.iflow/commands/ws-memory-bank-init.toml +100 -0
  95. package/templates/workspace/.iflow/commands/ws-migrate.toml +59 -0
  96. package/templates/workspace/.iflow/commands/ws-preflight.toml +30 -0
  97. package/templates/workspace/.iflow/commands/ws-req-change.toml +52 -0
  98. package/templates/workspace/.iflow/commands/ws-req-contract-sync.toml +25 -0
  99. package/templates/workspace/.iflow/commands/ws-req-contract-validate.toml +16 -0
  100. package/templates/workspace/.iflow/commands/ws-req-flow-sync.toml +36 -0
  101. package/templates/workspace/.iflow/commands/ws-req-review.toml +56 -0
  102. package/templates/workspace/.iflow/commands/ws-review.toml +32 -0
  103. package/templates/workspace/.iflow/commands/ws-rule.toml +43 -0
  104. package/templates/workspace/.opencode/command/aiws-init.md +19 -0
  105. package/templates/workspace/.opencode/command/aiws-rollback.md +12 -0
  106. package/templates/workspace/.opencode/command/aiws-update.md +18 -0
  107. package/templates/workspace/.opencode/command/aiws-validate.md +13 -0
  108. package/templates/workspace/.opencode/command/ws-analyze.md +27 -0
  109. package/templates/workspace/.opencode/command/ws-dev.md +24 -0
  110. package/templates/workspace/.opencode/command/ws-migrate.md +22 -0
  111. package/templates/workspace/.opencode/command/ws-preflight.md +27 -0
  112. package/templates/workspace/.opencode/command/ws-req-change.md +34 -0
  113. package/templates/workspace/.opencode/command/ws-req-contract-sync.md +18 -0
  114. package/templates/workspace/.opencode/command/ws-req-contract-validate.md +13 -0
  115. package/templates/workspace/.opencode/command/ws-req-flow-sync.md +20 -0
  116. package/templates/workspace/.opencode/command/ws-req-review.md +33 -0
  117. package/templates/workspace/.opencode/command/ws-review.md +25 -0
  118. package/templates/workspace/.opencode/command/ws-rule.md +24 -0
  119. package/templates/workspace/AGENTS.md +22 -0
  120. package/templates/workspace/AI_PROJECT.md +86 -0
  121. package/templates/workspace/AI_WORKSPACE.md +167 -0
  122. package/templates/workspace/REQUIREMENTS.md +94 -0
  123. package/templates/workspace/changes/README.md +55 -0
  124. package/templates/workspace/changes/templates/design.md +29 -0
  125. package/templates/workspace/changes/templates/proposal.md +59 -0
  126. package/templates/workspace/changes/templates/tasks.md +33 -0
  127. package/templates/workspace/issues/problem-issues.csv +2 -0
  128. package/templates/workspace/manifest.json +205 -0
  129. package/templates/workspace/memory-bank/README.md +14 -0
  130. package/templates/workspace/memory-bank/architecture.md +9 -0
  131. package/templates/workspace/memory-bank/implementation-plan.md +11 -0
  132. package/templates/workspace/memory-bank/progress.md +10 -0
  133. package/templates/workspace/memory-bank/tech-stack.md +11 -0
  134. package/templates/workspace/requirements/CHANGELOG.md +13 -0
  135. package/templates/workspace/requirements/requirements-issues.csv +2 -0
  136. package/templates/workspace/secrets/test-accounts.example.json +32 -0
  137. package/templates/workspace/tools/iflow_watchdog.sh +138 -0
  138. package/templates/workspace/tools/install_iflow_watchdog_systemd_user.sh +118 -0
  139. package/templates/workspace/tools/requirements_contract.py +285 -0
  140. package/templates/workspace/tools/requirements_contract_sync.py +290 -0
  141. package/templates/workspace/tools/requirements_flow_gen.py +250 -0
  142. package/templates/workspace/tools/server_test_runner.py +1902 -0
  143. package/templates/workspace/tools/systemd/iflow-watchdog@.service +16 -0
  144. package/templates/workspace/tools/systemd/iflow-watchdog@.timer +11 -0
  145. package/templates/workspace/tools/ws_change_check.py +323 -0
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=iFlow AI Workspace API Test Watchdog (%i)
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ # Per-instance environment file created by tools/install_iflow_watchdog_systemd_user.sh
8
+ EnvironmentFile=%h/.config/iflow-watchdog/%i.env
9
+ ExecStart=/usr/bin/env bash -lc 'cd "${WORKSPACE_DIR}" && bash tools/iflow_watchdog.sh'
10
+ Restart=always
11
+ RestartSec=30
12
+ KillMode=mixed
13
+ TimeoutStopSec=30
14
+
15
+ [Install]
16
+ WantedBy=default.target
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description=Nightly iFlow API Test Watchdog (%i)
3
+
4
+ [Timer]
5
+ OnCalendar=*-*-* 02:00:00
6
+ Persistent=true
7
+ Unit=iflow-watchdog@%i.service
8
+
9
+ [Install]
10
+ WantedBy=timers.target
11
+
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import csv
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+
17
+ CHANGE_BRANCH_RE = re.compile(r"^(change|changes|ws|ws-change)/([a-z0-9]+(?:-[a-z0-9]+)*)$")
18
+ CHANGE_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
19
+
20
+
21
+ def eprint(msg: str) -> None:
22
+ sys.stderr.write(msg + "\n")
23
+
24
+
25
+ def read_text(path: Path) -> str:
26
+ return path.read_text(encoding="utf-8", errors="replace")
27
+
28
+
29
+ def sha256(path: Path) -> str:
30
+ h = hashlib.sha256()
31
+ with path.open("rb") as f:
32
+ while True:
33
+ b = f.read(1024 * 1024)
34
+ if not b:
35
+ break
36
+ h.update(b)
37
+ return h.hexdigest()
38
+
39
+
40
+ def git_root(cwd: Path) -> Optional[Path]:
41
+ try:
42
+ root = subprocess.check_output(
43
+ ["git", "-C", str(cwd), "rev-parse", "--show-toplevel"],
44
+ text=True,
45
+ ).strip()
46
+ if root:
47
+ return Path(root).resolve()
48
+ except Exception:
49
+ return None
50
+ return None
51
+
52
+
53
+ def current_branch(root: Path) -> Optional[str]:
54
+ try:
55
+ b = subprocess.check_output(
56
+ ["git", "-C", str(root), "symbolic-ref", "--quiet", "--short", "HEAD"],
57
+ text=True,
58
+ ).strip()
59
+ return b or None
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ def infer_change_id_from_branch(branch: Optional[str]) -> Optional[str]:
65
+ if not branch:
66
+ return None
67
+ m = CHANGE_BRANCH_RE.match(branch)
68
+ if not m:
69
+ return None
70
+ return m.group(2)
71
+
72
+
73
+ def has_truth_files(root: Path) -> Tuple[bool, List[str]]:
74
+ required = ["AI_PROJECT.md", "AI_WORKSPACE.md", "REQUIREMENTS.md"]
75
+ missing = [f for f in required if not (root / f).exists()]
76
+ return (len(missing) == 0, missing)
77
+
78
+
79
+ def extract_id(label: str, text: str) -> str:
80
+ m = re.search(rf"(?m)^.*{re.escape(label)}.*?[:=]\s*(.+)$", text)
81
+ if not m:
82
+ return ""
83
+ v = m.group(1).strip()
84
+ v = re.sub(r"<!--.*?-->", "", v).strip()
85
+ v = v.strip("`").strip()
86
+ return v
87
+
88
+
89
+ def csv_has_id(path: Path, column: str, value: str) -> bool:
90
+ with path.open("r", encoding="utf-8", errors="replace", newline="") as f:
91
+ reader = csv.DictReader(f)
92
+ if not reader.fieldnames:
93
+ return False
94
+ if column in reader.fieldnames:
95
+ for row in reader:
96
+ if (row.get(column) or "").strip() == value:
97
+ return True
98
+ return False
99
+ for row in reader:
100
+ for cell in row.values():
101
+ if (cell or "").strip() == value:
102
+ return True
103
+ return False
104
+
105
+
106
+ def truth_snapshot(root: Path) -> Dict[str, Dict[str, Any]]:
107
+ truth_files = ["AI_PROJECT.md", "AI_WORKSPACE.md", "REQUIREMENTS.md"]
108
+ out: Dict[str, Dict[str, Any]] = {}
109
+ for rel in truth_files:
110
+ p = root / rel
111
+ if not p.exists():
112
+ continue
113
+ st = p.stat()
114
+ out[rel] = {"mtime": int(st.st_mtime), "sha256": sha256(p)}
115
+ return out
116
+
117
+
118
+ def validate_change(
119
+ *,
120
+ root: Path,
121
+ change_id: str,
122
+ strict: bool,
123
+ allow_truth_drift: bool,
124
+ ) -> int:
125
+ change_dir = root / "changes" / change_id
126
+ required_files = ["proposal.md", "tasks.md"]
127
+
128
+ errors: List[str] = []
129
+ warnings: List[str] = []
130
+
131
+ def file_state(rel: str) -> Optional[Path]:
132
+ p = change_dir / rel
133
+ if not p.exists():
134
+ errors.append(f"missing: {rel}")
135
+ return None
136
+ if p.stat().st_size == 0:
137
+ errors.append(f"empty: {rel}")
138
+ return None
139
+ return p
140
+
141
+ proposal_path = file_state("proposal.md")
142
+ tasks_path = file_state("tasks.md")
143
+
144
+ meta_path = change_dir / ".ws-change.json"
145
+ meta: Optional[Dict[str, Any]] = None
146
+ if not meta_path.exists():
147
+ (errors if strict else warnings).append("missing: .ws-change.json (created by aiws change new / aiws change sync)")
148
+ else:
149
+ try:
150
+ meta = json.loads(read_text(meta_path))
151
+ except Exception as e:
152
+ errors.append(f"invalid .ws-change.json: {e}")
153
+
154
+ if meta:
155
+ created_truth = meta.get("base_truth_files") or {}
156
+ synced_truth = meta.get("synced_truth_files") or {}
157
+ baseline = synced_truth if synced_truth else created_truth
158
+ baseline_label = "last sync" if synced_truth else "creation"
159
+ baseline_at = meta.get("synced_at") if synced_truth else meta.get("created_at")
160
+
161
+ for rel, base in (baseline or {}).items():
162
+ p = root / rel
163
+ if not p.exists():
164
+ (errors if strict else warnings).append(f"truth file missing now: {rel} (baseline={baseline_label})")
165
+ continue
166
+ try:
167
+ cur_sha = sha256(p)
168
+ except Exception as e:
169
+ warnings.append(f"failed to hash truth file {rel}: {e}")
170
+ continue
171
+ base_sha = (base or {}).get("sha256") if isinstance(base, dict) else None
172
+ if base_sha and cur_sha != base_sha:
173
+ msg = (
174
+ f"truth file changed since {baseline_label}: {rel} "
175
+ f"(baseline_at={baseline_at}, baseline_sha={base_sha}, current_sha={cur_sha})"
176
+ )
177
+ if strict and not allow_truth_drift:
178
+ errors.append(msg + f"; run `aiws change sync {change_id}` to acknowledge")
179
+ else:
180
+ warnings.append(msg)
181
+
182
+ def placeholder_scan(rel: str, text: str) -> None:
183
+ if "{{CHANGE_ID}}" in text or "{{TITLE}}" in text or "{{CREATED_AT}}" in text:
184
+ errors.append(f"unrendered template placeholders in {rel}")
185
+ if "WS:TODO" in text:
186
+ (errors if strict else warnings).append(f"WS:TODO markers remain in {rel}")
187
+
188
+ req_id = ""
189
+ prob_id = ""
190
+
191
+ if proposal_path:
192
+ t = read_text(proposal_path)
193
+ placeholder_scan("proposal.md", t)
194
+ if "验证" not in t:
195
+ warnings.append("proposal.md does not mention 验证 (recommended to include reproducible verification)")
196
+ if "AI_WORKSPACE.md" not in t:
197
+ warnings.append("proposal.md does not reference AI_WORKSPACE.md (recommended)")
198
+
199
+ req_id = extract_id("Req_ID", t)
200
+ prob_id = extract_id("Problem_ID", t)
201
+ if strict and not (req_id or prob_id):
202
+ errors.append("proposal.md must include a non-empty Req_ID or Problem_ID (attribution)")
203
+
204
+ req_csv = root / "requirements" / "requirements-issues.csv"
205
+ prob_csv = root / "issues" / "problem-issues.csv"
206
+
207
+ if req_id and req_csv.exists():
208
+ ok = False
209
+ try:
210
+ ok = csv_has_id(req_csv, "Req_ID", req_id)
211
+ except Exception as e:
212
+ warnings.append(f"failed to read requirements/requirements-issues.csv: {e}")
213
+ if not ok:
214
+ (errors if strict else warnings).append(f"Req_ID not found in requirements/requirements-issues.csv: {req_id}")
215
+
216
+ if prob_id and prob_csv.exists():
217
+ ok = False
218
+ try:
219
+ ok = csv_has_id(prob_csv, "Problem_ID", prob_id)
220
+ except Exception as e:
221
+ warnings.append(f"failed to read issues/problem-issues.csv: {e}")
222
+ if not ok:
223
+ (errors if strict else warnings).append(f"Problem_ID not found in issues/problem-issues.csv: {prob_id}")
224
+
225
+ if tasks_path:
226
+ t = read_text(tasks_path)
227
+ placeholder_scan("tasks.md", t)
228
+ if not re.search(r"(?m)^- \[[ xX]\]", t):
229
+ errors.append("tasks.md has no checkbox tasks ('- [ ]' or '- [x]')")
230
+
231
+ design_path = change_dir / "design.md"
232
+ if design_path.exists() and design_path.stat().st_size > 0:
233
+ placeholder_scan("design.md", read_text(design_path))
234
+
235
+ for e in errors:
236
+ eprint(f"error: {e}")
237
+ for w in warnings:
238
+ eprint(f"warn: {w}")
239
+
240
+ return 2 if errors else 0
241
+
242
+
243
+ def main(argv: Optional[List[str]] = None) -> int:
244
+ parser = argparse.ArgumentParser(description="Validate ws change artifacts for hooks/CI.")
245
+ parser.add_argument("--workspace-root", default="", help="Workspace root (defaults to git root).")
246
+ parser.add_argument("--change-id", default="", help="Change id (defaults to infer from branch).")
247
+ parser.add_argument(
248
+ "--branch",
249
+ default="",
250
+ help="Branch name to validate against (for CI detached HEAD). When set, enforces change/<id> naming.",
251
+ )
252
+ parser.add_argument("--strict", action="store_true", help="Treat WS:TODO as errors and require attribution IDs.")
253
+ parser.add_argument(
254
+ "--allow-truth-drift",
255
+ action="store_true",
256
+ help="Do not fail strict validation on truth drift (use only for emergencies).",
257
+ )
258
+ parser.add_argument(
259
+ "--allow-branches",
260
+ default="main,master",
261
+ help="Comma-separated branch names that are exempt (default: main,master).",
262
+ )
263
+ args = parser.parse_args(argv)
264
+
265
+ cwd = Path(os.getcwd())
266
+ root = Path(args.workspace_root).resolve() if args.workspace_root else (git_root(cwd) or cwd.resolve())
267
+
268
+ ok_truth, missing = has_truth_files(root)
269
+ if not ok_truth:
270
+ # Not an AI Workspace; do not block.
271
+ eprint(f"warn: skip ws-change check (missing truth files): {missing}")
272
+ return 0
273
+
274
+ change_id = args.change_id.strip()
275
+ if change_id and not CHANGE_ID_RE.match(change_id):
276
+ eprint(f"error: invalid change id (use kebab-case): {change_id}")
277
+ return 2
278
+
279
+ branch_arg = (args.branch or "").strip()
280
+ branch = branch_arg or current_branch(root)
281
+ if not change_id:
282
+ # Detached HEAD during rebase/merge; do not block unless CI passes --branch.
283
+ if not branch:
284
+ eprint("warn: skip ws-change check (detached HEAD; pass --branch in CI to enforce)")
285
+ return 0
286
+ allow = {b.strip() for b in (args.allow_branches or "").split(",") if b.strip()}
287
+ if branch in allow:
288
+ return 0
289
+ inferred = infer_change_id_from_branch(branch)
290
+ if not inferred:
291
+ eprint(f"error: branch must be change/<change-id> (current: {branch})")
292
+ eprint("hint: switch/create: git switch -c change/<change-id>")
293
+ return 2
294
+ change_id = inferred
295
+ else:
296
+ # If CI provides --branch, cross-check branch naming even when --change-id is provided.
297
+ if branch_arg:
298
+ allow = {b.strip() for b in (args.allow_branches or "").split(",") if b.strip()}
299
+ if branch_arg not in allow:
300
+ inferred = infer_change_id_from_branch(branch_arg)
301
+ if not inferred:
302
+ eprint(f"error: branch must be change/<change-id> (current: {branch_arg})")
303
+ return 2
304
+ if inferred != change_id:
305
+ eprint(f"error: change-id does not match branch (branch={branch_arg}, change_id={change_id})")
306
+ return 2
307
+
308
+ change_dir = root / "changes" / change_id
309
+ if not change_dir.exists():
310
+ eprint(f"error: missing change dir: {change_dir}")
311
+ eprint(f"hint: create: aiws change new {change_id} --no-design")
312
+ return 2
313
+
314
+ return validate_change(
315
+ root=root,
316
+ change_id=change_id,
317
+ strict=args.strict,
318
+ allow_truth_drift=args.allow_truth_drift,
319
+ )
320
+
321
+
322
+ if __name__ == "__main__":
323
+ raise SystemExit(main())