@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.
- package/README.md +12 -0
- package/docs/cli-interface.md +288 -0
- package/docs/spec-contract.md +183 -0
- package/package.json +18 -0
- package/templates/workspace/.agents/skills/aiws-change-archive/SKILL.md +23 -0
- package/templates/workspace/.agents/skills/aiws-change-list/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/aiws-change-new/SKILL.md +26 -0
- package/templates/workspace/.agents/skills/aiws-change-next/SKILL.md +19 -0
- package/templates/workspace/.agents/skills/aiws-change-start/SKILL.md +27 -0
- package/templates/workspace/.agents/skills/aiws-change-status/SKILL.md +19 -0
- package/templates/workspace/.agents/skills/aiws-change-sync/SKILL.md +19 -0
- package/templates/workspace/.agents/skills/aiws-change-templates-init/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/aiws-change-templates-which/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/aiws-change-validate/SKILL.md +23 -0
- package/templates/workspace/.agents/skills/aiws-hooks-install/SKILL.md +30 -0
- package/templates/workspace/.agents/skills/aiws-hooks-status/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/aiws-init/SKILL.md +27 -0
- package/templates/workspace/.agents/skills/aiws-rollback/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/aiws-update/SKILL.md +26 -0
- package/templates/workspace/.agents/skills/aiws-validate/SKILL.md +22 -0
- package/templates/workspace/.agents/skills/ws-analyze/SKILL.md +26 -0
- package/templates/workspace/.agents/skills/ws-commit/SKILL.md +50 -0
- package/templates/workspace/.agents/skills/ws-dev/SKILL.md +34 -0
- package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +54 -0
- package/templates/workspace/.agents/skills/ws-plan/SKILL.md +39 -0
- package/templates/workspace/.agents/skills/ws-preflight/SKILL.md +29 -0
- package/templates/workspace/.agents/skills/ws-req-change/SKILL.md +33 -0
- package/templates/workspace/.agents/skills/ws-req-contract-sync/SKILL.md +17 -0
- package/templates/workspace/.agents/skills/ws-req-contract-validate/SKILL.md +12 -0
- package/templates/workspace/.agents/skills/ws-req-flow-sync/SKILL.md +28 -0
- package/templates/workspace/.agents/skills/ws-req-review/SKILL.md +32 -0
- package/templates/workspace/.agents/skills/ws-review/SKILL.md +24 -0
- package/templates/workspace/.agents/skills/ws-rule/SKILL.md +23 -0
- package/templates/workspace/.aiws/manifest.json +36 -0
- package/templates/workspace/.claude/commands/aiws-init.md +19 -0
- package/templates/workspace/.claude/commands/aiws-rollback.md +12 -0
- package/templates/workspace/.claude/commands/aiws-update.md +18 -0
- package/templates/workspace/.claude/commands/aiws-validate.md +13 -0
- package/templates/workspace/.claude/commands/ws-analyze.md +27 -0
- package/templates/workspace/.claude/commands/ws-dev.md +24 -0
- package/templates/workspace/.claude/commands/ws-migrate.md +22 -0
- package/templates/workspace/.claude/commands/ws-preflight.md +27 -0
- package/templates/workspace/.claude/commands/ws-req-change.md +34 -0
- package/templates/workspace/.claude/commands/ws-req-contract-sync.md +18 -0
- package/templates/workspace/.claude/commands/ws-req-contract-validate.md +13 -0
- package/templates/workspace/.claude/commands/ws-req-flow-sync.md +20 -0
- package/templates/workspace/.claude/commands/ws-req-review.md +33 -0
- package/templates/workspace/.claude/commands/ws-review.md +25 -0
- package/templates/workspace/.claude/commands/ws-rule.md +24 -0
- package/templates/workspace/.codex/prompts/aiws-init.md +23 -0
- package/templates/workspace/.codex/prompts/aiws-rollback.md +16 -0
- package/templates/workspace/.codex/prompts/aiws-update.md +22 -0
- package/templates/workspace/.codex/prompts/aiws-validate.md +17 -0
- package/templates/workspace/.codex/prompts/ws-analyze.md +32 -0
- package/templates/workspace/.codex/prompts/ws-dev.md +29 -0
- package/templates/workspace/.codex/prompts/ws-migrate.md +27 -0
- package/templates/workspace/.codex/prompts/ws-preflight.md +32 -0
- package/templates/workspace/.codex/prompts/ws-req-change.md +39 -0
- package/templates/workspace/.codex/prompts/ws-req-contract-sync.md +23 -0
- package/templates/workspace/.codex/prompts/ws-req-contract-validate.md +18 -0
- package/templates/workspace/.codex/prompts/ws-req-flow-sync.md +25 -0
- package/templates/workspace/.codex/prompts/ws-req-review.md +38 -0
- package/templates/workspace/.codex/prompts/ws-review.md +30 -0
- package/templates/workspace/.codex/prompts/ws-rule.md +29 -0
- package/templates/workspace/.githooks/pre-commit +32 -0
- package/templates/workspace/.githooks/pre-push +32 -0
- package/templates/workspace/.iflow/agents/feature-reviewer.md +27 -0
- package/templates/workspace/.iflow/agents/requirements-analyst.md +24 -0
- package/templates/workspace/.iflow/agents/server-commit-manager.md +28 -0
- package/templates/workspace/.iflow/agents/server-fix-implementer.md +31 -0
- package/templates/workspace/.iflow/agents/server-test-planner.md +28 -0
- package/templates/workspace/.iflow/agents/server-test-triager.md +30 -0
- package/templates/workspace/.iflow/commands/aiws-init.toml +24 -0
- package/templates/workspace/.iflow/commands/aiws-rollback.toml +18 -0
- package/templates/workspace/.iflow/commands/aiws-update.toml +23 -0
- package/templates/workspace/.iflow/commands/aiws-validate.toml +18 -0
- package/templates/workspace/.iflow/commands/server-commit.toml +27 -0
- package/templates/workspace/.iflow/commands/server-drain.toml +99 -0
- package/templates/workspace/.iflow/commands/server-fix-and-commit.toml +27 -0
- package/templates/workspace/.iflow/commands/server-fix.toml +65 -0
- package/templates/workspace/.iflow/commands/server-test-plan.toml +62 -0
- package/templates/workspace/.iflow/commands/server-test.toml +58 -0
- package/templates/workspace/.iflow/commands/server-triage.toml +38 -0
- package/templates/workspace/.iflow/commands/server_test-plan.toml +12 -0
- package/templates/workspace/.iflow/commands/server_test.toml +12 -0
- package/templates/workspace/.iflow/commands/ws-analyze.toml +33 -0
- package/templates/workspace/.iflow/commands/ws-contract-check.toml +69 -0
- package/templates/workspace/.iflow/commands/ws-dev.toml +34 -0
- package/templates/workspace/.iflow/commands/ws-doctor.toml +141 -0
- package/templates/workspace/.iflow/commands/ws-env-doctor.toml +74 -0
- package/templates/workspace/.iflow/commands/ws-feature-deliver.toml +44 -0
- package/templates/workspace/.iflow/commands/ws-feature-plan.toml +47 -0
- package/templates/workspace/.iflow/commands/ws-init.toml +53 -0
- package/templates/workspace/.iflow/commands/ws-memory-bank-init.toml +100 -0
- package/templates/workspace/.iflow/commands/ws-migrate.toml +59 -0
- package/templates/workspace/.iflow/commands/ws-preflight.toml +30 -0
- package/templates/workspace/.iflow/commands/ws-req-change.toml +52 -0
- package/templates/workspace/.iflow/commands/ws-req-contract-sync.toml +25 -0
- package/templates/workspace/.iflow/commands/ws-req-contract-validate.toml +16 -0
- package/templates/workspace/.iflow/commands/ws-req-flow-sync.toml +36 -0
- package/templates/workspace/.iflow/commands/ws-req-review.toml +56 -0
- package/templates/workspace/.iflow/commands/ws-review.toml +32 -0
- package/templates/workspace/.iflow/commands/ws-rule.toml +43 -0
- package/templates/workspace/.opencode/command/aiws-init.md +19 -0
- package/templates/workspace/.opencode/command/aiws-rollback.md +12 -0
- package/templates/workspace/.opencode/command/aiws-update.md +18 -0
- package/templates/workspace/.opencode/command/aiws-validate.md +13 -0
- package/templates/workspace/.opencode/command/ws-analyze.md +27 -0
- package/templates/workspace/.opencode/command/ws-dev.md +24 -0
- package/templates/workspace/.opencode/command/ws-migrate.md +22 -0
- package/templates/workspace/.opencode/command/ws-preflight.md +27 -0
- package/templates/workspace/.opencode/command/ws-req-change.md +34 -0
- package/templates/workspace/.opencode/command/ws-req-contract-sync.md +18 -0
- package/templates/workspace/.opencode/command/ws-req-contract-validate.md +13 -0
- package/templates/workspace/.opencode/command/ws-req-flow-sync.md +20 -0
- package/templates/workspace/.opencode/command/ws-req-review.md +33 -0
- package/templates/workspace/.opencode/command/ws-review.md +25 -0
- package/templates/workspace/.opencode/command/ws-rule.md +24 -0
- package/templates/workspace/AGENTS.md +22 -0
- package/templates/workspace/AI_PROJECT.md +86 -0
- package/templates/workspace/AI_WORKSPACE.md +167 -0
- package/templates/workspace/REQUIREMENTS.md +94 -0
- package/templates/workspace/changes/README.md +55 -0
- package/templates/workspace/changes/templates/design.md +29 -0
- package/templates/workspace/changes/templates/proposal.md +59 -0
- package/templates/workspace/changes/templates/tasks.md +33 -0
- package/templates/workspace/issues/problem-issues.csv +2 -0
- package/templates/workspace/manifest.json +205 -0
- package/templates/workspace/memory-bank/README.md +14 -0
- package/templates/workspace/memory-bank/architecture.md +9 -0
- package/templates/workspace/memory-bank/implementation-plan.md +11 -0
- package/templates/workspace/memory-bank/progress.md +10 -0
- package/templates/workspace/memory-bank/tech-stack.md +11 -0
- package/templates/workspace/requirements/CHANGELOG.md +13 -0
- package/templates/workspace/requirements/requirements-issues.csv +2 -0
- package/templates/workspace/secrets/test-accounts.example.json +32 -0
- package/templates/workspace/tools/iflow_watchdog.sh +138 -0
- package/templates/workspace/tools/install_iflow_watchdog_systemd_user.sh +118 -0
- package/templates/workspace/tools/requirements_contract.py +285 -0
- package/templates/workspace/tools/requirements_contract_sync.py +290 -0
- package/templates/workspace/tools/requirements_flow_gen.py +250 -0
- package/templates/workspace/tools/server_test_runner.py +1902 -0
- package/templates/workspace/tools/systemd/iflow-watchdog@.service +16 -0
- package/templates/workspace/tools/systemd/iflow-watchdog@.timer +11 -0
- 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,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())
|