@sushanglewis/lincoln 1.5.4 → 1.6.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/dist/main.js +425 -51
- package/dist/main.js.map +1 -1
- package/dist/postinstall.d.ts +9 -2
- package/dist/postinstall.js +812 -6
- package/dist/postinstall.js.map +1 -1
- package/framework/.claude/agents/architect.md +15 -15
- package/framework/.claude/agents/default.md +118 -104
- package/framework/.claude/agents/designer.md +17 -17
- package/framework/.claude/agents/engineer.md +16 -25
- package/framework/.claude/agents/external/NOTICES.md +1 -1
- package/framework/.claude/agents/external/everything-claude-code/agents/ecc-tdd-guide.md +108 -0
- package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +1 -1
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-architect.md +134 -0
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-code-reviewer.md +248 -0
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-designer.md +122 -0
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-qa-tester.md +106 -0
- package/framework/.claude/agents/external/wshobson-agents/agents/wshobson-frontend-developer.md +173 -0
- package/framework/.claude/agents/frontend-engineer.md +15 -16
- package/framework/.claude/agents/knowledge-curator.md +18 -15
- package/framework/.claude/agents/pm.md +17 -23
- package/framework/.claude/agents/qa.md +17 -13
- package/framework/.claude/agents/release-coordinator.md +18 -12
- package/framework/.claude/agents/researcher.md +16 -26
- package/framework/.claude/harnesses/command-map.yaml +12 -5
- package/framework/.claude/hooks/on-session-start.sh +0 -1
- package/framework/.claude/hooks/pre-tool-use.sh +3 -3
- package/framework/.claude/schemas/agent-definition.schema.json +6 -1
- package/framework/.claude/schemas/stage-definition.schema.json +49 -4
- package/framework/.claude/skills/build-product-prototype/SKILL.md +1 -1
- package/framework/.claude/skills/build-product-prototype/prompts/main.md +89 -26
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +80 -14
- package/framework/.claude/skills/dependencies.yaml +6 -0
- package/framework/.claude/skills/draft-product-design/SKILL.md +2 -0
- package/framework/.claude/skills/draft-product-design/prompts/main.md +30 -17
- package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +2 -0
- package/framework/.claude/skills/lc-explore-opensource/SKILL.md +2 -0
- package/framework/.claude/skills/lc-market-research/SKILL.md +1 -1
- package/framework/.claude/skills/lc-one-id/SKILL.md +81 -0
- package/framework/.claude/skills/lc-one-id/prompts/main.md +35 -0
- package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +1 -1
- package/framework/.claude/skills/lc-workflow/validators/validate.py +2 -0
- package/framework/.claude/skills/lc-workflow-router/SKILL.md +2 -0
- package/framework/.claude/stages/build-codebase-knowledge.yaml +10 -10
- package/framework/.claude/stages/clarify.yaml +22 -16
- package/framework/.claude/stages/explore-opensource.yaml +13 -10
- package/framework/.claude/stages/implement.yaml +11 -11
- package/framework/.claude/stages/ingest.yaml +13 -10
- package/framework/.claude/stages/lc-analyze-frameworks.yaml +13 -10
- package/framework/.claude/stages/lc-collect-intelligence.yaml +13 -10
- package/framework/.claude/stages/lc-competitive-analysis.yaml +13 -10
- package/framework/.claude/stages/lc-first-principles.yaml +13 -10
- package/framework/.claude/stages/lc-market-research.yaml +13 -10
- package/framework/.claude/stages/lc-product-research.yaml +13 -10
- package/framework/.claude/stages/lc-research-report.yaml +13 -10
- package/framework/.claude/stages/lc-research-scope.yaml +13 -10
- package/framework/.claude/stages/lc-stakeholder-research.yaml +13 -10
- package/framework/.claude/stages/lc-storytelling.yaml +13 -10
- package/framework/.claude/stages/product-design-docs.yaml +33 -19
- package/framework/.claude/stages/product-prototype.yaml +22 -18
- package/framework/.claude/stages/propose.yaml +16 -9
- package/framework/.claude/stages/split.yaml +13 -9
- package/framework/.claude/stages/sync-knowledge.yaml +20 -9
- package/framework/.claude/stages/tdd-development-plan.yaml +17 -9
- package/framework/.claude/stages/workflow-router.yaml +17 -13
- package/framework/.claude/workflows/README.md +25 -3
- package/framework/.claude/workflows/bug-fix.yaml +1 -0
- package/framework/.claude/workflows/existing-project-iteration.yaml +1 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +4 -2
- package/framework/.claude/workflows/oss-first-design.yaml +1 -0
- package/framework/.claude-plugin/marketplace.json +2 -2
- package/framework/.claude-plugin/plugin.json +2 -1
- package/framework/.version-bump.json +1 -1
- package/framework/README.en.md +15 -2
- package/framework/README.md +16 -3
- package/framework/RELEASE.md +32 -0
- package/framework/manifest.json +128 -88
- package/framework/scripts/check-prompt-drift.py +657 -0
- package/framework/scripts/extract_release_notes.py +79 -0
- package/framework/scripts/init-lincoln-branch.sh +6 -0
- package/framework/scripts/lincoln_command_map.py +1 -1
- package/framework/scripts/lincoln_id.py +333 -0
- package/framework/scripts/lincoln_index.py +10 -0
- package/framework/scripts/lincoln_prd.py +16 -17
- package/framework/scripts/lincoln_render.py +657 -0
- package/framework/scripts/run-infrastructure-tests.py +2 -0
- package/framework/scripts/stage_loader.py +149 -2
- package/framework/scripts/static-check.sh +40 -1
- package/framework/scripts/sync-external-agents.sh +2 -0
- package/framework/scripts/update_readme_version.py +149 -0
- package/framework/scripts/validate_stage.py +80 -1
- package/framework/tools/lincoln/package-lock.json +2 -2
- package/framework/tools/lincoln/package.json +1 -1
- package/framework/tools/lincoln-installer/package.json +1 -1
- package/package.json +2 -2
- package/framework/.claude/agents/_contract.md +0 -79
|
@@ -37,6 +37,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
|
|
37
37
|
import yaml
|
|
38
38
|
|
|
39
39
|
from scripts import lincoln_trace
|
|
40
|
+
from scripts import lincoln_render
|
|
40
41
|
from scripts.lincoln_documents import write_documents_index
|
|
41
42
|
from scripts.lincoln_index import write_package_index
|
|
42
43
|
from scripts.lincoln_paths import (
|
|
@@ -192,6 +193,21 @@ def interpolate(value: str, variables: dict[str, Any]) -> str:
|
|
|
192
193
|
return re.sub(r"\{(\w+)\}", replacer, value)
|
|
193
194
|
|
|
194
195
|
|
|
196
|
+
def interpolate_value(value: Any, variables: dict[str, Any]) -> Any:
|
|
197
|
+
"""Recursively interpolate {var} placeholders in nested str/dict/list values.
|
|
198
|
+
|
|
199
|
+
Non-string scalars are returned unchanged so new optional structured
|
|
200
|
+
stage fields (e.g. context.execution, references) pass through gracefully.
|
|
201
|
+
"""
|
|
202
|
+
if isinstance(value, str):
|
|
203
|
+
return interpolate(value, variables)
|
|
204
|
+
if isinstance(value, dict):
|
|
205
|
+
return {key: interpolate_value(val, variables) for key, val in value.items()}
|
|
206
|
+
if isinstance(value, list):
|
|
207
|
+
return [interpolate_value(item, variables) for item in value]
|
|
208
|
+
return value
|
|
209
|
+
|
|
210
|
+
|
|
195
211
|
def interpolate_artifact(value: str, state: dict[str, Any], state_file: Path | None = None) -> str:
|
|
196
212
|
value = interpolate(value, get_variables(state))
|
|
197
213
|
return interpolate_process_path(value, state, resolve_state_path(state_file))
|
|
@@ -328,9 +344,8 @@ def resolve_stage_context(
|
|
|
328
344
|
|
|
329
345
|
context = stage.get("context", {})
|
|
330
346
|
interpolated_context = {
|
|
331
|
-
key:
|
|
347
|
+
key: interpolate_value(value, variables)
|
|
332
348
|
for key, value in context.items()
|
|
333
|
-
if isinstance(value, str)
|
|
334
349
|
}
|
|
335
350
|
|
|
336
351
|
artifacts = stage.get("artifacts", {})
|
|
@@ -343,6 +358,20 @@ def resolve_stage_context(
|
|
|
343
358
|
for art in artifacts.get("optional", [])
|
|
344
359
|
]
|
|
345
360
|
|
|
361
|
+
# Schema 3.0.0 optional structured fields; emitted with defaults when absent.
|
|
362
|
+
raw_artifact_templates = stage.get("artifact_templates", {}) or {}
|
|
363
|
+
artifact_templates = {
|
|
364
|
+
interpolate_artifact(str(art_path), state, state_file): interpolate(str(tpl), variables)
|
|
365
|
+
for art_path, tpl in raw_artifact_templates.items()
|
|
366
|
+
}
|
|
367
|
+
stage_variables = list(stage.get("variables", []) or [])
|
|
368
|
+
execution_mode = stage.get("execution_mode", "both")
|
|
369
|
+
primary_action = stage.get("primary_action")
|
|
370
|
+
references = {
|
|
371
|
+
str(key): interpolate(str(val), variables)
|
|
372
|
+
for key, val in (stage.get("references", {}) or {}).items()
|
|
373
|
+
}
|
|
374
|
+
|
|
346
375
|
agent = stage.get("agent", {})
|
|
347
376
|
skills = stage.get("skills", {})
|
|
348
377
|
|
|
@@ -359,6 +388,11 @@ def resolve_stage_context(
|
|
|
359
388
|
"optional_skills": skills.get("optional", []),
|
|
360
389
|
"required_artifacts": required_artifacts,
|
|
361
390
|
"optional_artifacts": optional_artifacts,
|
|
391
|
+
"artifact_templates": artifact_templates,
|
|
392
|
+
"variables": stage_variables,
|
|
393
|
+
"execution_mode": execution_mode,
|
|
394
|
+
"primary_action": primary_action,
|
|
395
|
+
"references": references,
|
|
362
396
|
"context": interpolated_context,
|
|
363
397
|
"context_path": str((STAGES_DIR / f"{stage_id}.yaml").relative_to(PROJECT_ROOT)),
|
|
364
398
|
"workflow_template": template_name,
|
|
@@ -389,6 +423,11 @@ def action_load(stage_id: str, state: dict[str, Any], state_file: Path | None =
|
|
|
389
423
|
"context_paths": [context["context_path"]],
|
|
390
424
|
"required_skills": context["required_skills"],
|
|
391
425
|
"optional_skills": context["optional_skills"],
|
|
426
|
+
"artifact_templates": context["artifact_templates"],
|
|
427
|
+
"variables": context["variables"],
|
|
428
|
+
"execution_mode": context["execution_mode"],
|
|
429
|
+
"primary_action": context["primary_action"],
|
|
430
|
+
"references": context["references"],
|
|
392
431
|
}
|
|
393
432
|
|
|
394
433
|
|
|
@@ -935,6 +974,72 @@ def action_update_last_updated(state_file: Path) -> None:
|
|
|
935
974
|
save_state(state, state_file)
|
|
936
975
|
|
|
937
976
|
|
|
977
|
+
def action_render_page(
|
|
978
|
+
stage_id: str,
|
|
979
|
+
target: str,
|
|
980
|
+
state: dict[str, Any],
|
|
981
|
+
state_file: Path,
|
|
982
|
+
*,
|
|
983
|
+
title: str = "",
|
|
984
|
+
nav_label: str = "",
|
|
985
|
+
nav_group: str = "Docs",
|
|
986
|
+
version: str = "",
|
|
987
|
+
uid: str = "",
|
|
988
|
+
stage_mark: str = "",
|
|
989
|
+
markdown_path: str = "",
|
|
990
|
+
extra_variables: dict[str, str] | None = None,
|
|
991
|
+
) -> Path:
|
|
992
|
+
"""Render a single HTML page using lincoln_render and record it as an artifact."""
|
|
993
|
+
markdown_source = ""
|
|
994
|
+
if markdown_path:
|
|
995
|
+
md_path = PROJECT_ROOT / markdown_path
|
|
996
|
+
if not md_path.exists():
|
|
997
|
+
raise FileNotFoundError(f"Markdown source not found: {md_path}")
|
|
998
|
+
markdown_source = md_path.read_text(encoding="utf-8")
|
|
999
|
+
|
|
1000
|
+
html = lincoln_render.render_page(
|
|
1001
|
+
stage_id=stage_id,
|
|
1002
|
+
target=target,
|
|
1003
|
+
title=title,
|
|
1004
|
+
nav_label=nav_label,
|
|
1005
|
+
nav_group=nav_group,
|
|
1006
|
+
version=version,
|
|
1007
|
+
uid=uid,
|
|
1008
|
+
stage_mark=stage_mark or stage_id,
|
|
1009
|
+
markdown_source=markdown_source,
|
|
1010
|
+
extra_variables=extra_variables,
|
|
1011
|
+
state_file=state_file,
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
target_path = PROJECT_ROOT / target
|
|
1015
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1016
|
+
lincoln_render.atomic_write_text(target_path, html)
|
|
1017
|
+
|
|
1018
|
+
# Record the rendered artifact under the current stage node.
|
|
1019
|
+
latest_node = get_latest_node_for_stage(state, stage_id)
|
|
1020
|
+
if latest_node is None:
|
|
1021
|
+
latest_node = {
|
|
1022
|
+
"stage_id": stage_id,
|
|
1023
|
+
"node_id": f"{stage_id}-{now_iso()}",
|
|
1024
|
+
"status": "in_progress",
|
|
1025
|
+
"started_at": now_iso(),
|
|
1026
|
+
"completed_at": None,
|
|
1027
|
+
"gate_passed": False,
|
|
1028
|
+
"approved_by": None,
|
|
1029
|
+
"artifacts": [],
|
|
1030
|
+
"handoff_file": None,
|
|
1031
|
+
}
|
|
1032
|
+
state.setdefault("nodes", []).append(latest_node)
|
|
1033
|
+
|
|
1034
|
+
existing = set(latest_node.get("artifacts", []))
|
|
1035
|
+
existing.add(str(target_path.relative_to(PROJECT_ROOT)))
|
|
1036
|
+
latest_node["artifacts"] = sorted(existing)
|
|
1037
|
+
|
|
1038
|
+
save_state(state, state_file)
|
|
1039
|
+
print(f"Rendered {target_path.relative_to(PROJECT_ROOT)}")
|
|
1040
|
+
return target_path
|
|
1041
|
+
|
|
1042
|
+
|
|
938
1043
|
def action_benchmark_report(state_file: Path, trigger: str) -> dict[str, str] | None:
|
|
939
1044
|
"""Generate a benchmark report for the current process state."""
|
|
940
1045
|
from scripts import lincoln_benchmark
|
|
@@ -976,6 +1081,7 @@ def main() -> int:
|
|
|
976
1081
|
"recover",
|
|
977
1082
|
"status",
|
|
978
1083
|
"handoff-report",
|
|
1084
|
+
"render-page",
|
|
979
1085
|
"benchmark-report",
|
|
980
1086
|
"update-last-updated",
|
|
981
1087
|
],
|
|
@@ -988,6 +1094,16 @@ def main() -> int:
|
|
|
988
1094
|
parser.add_argument("--handoff-file", help="Handoff file path for append-node")
|
|
989
1095
|
parser.add_argument("--artifacts", help="Comma-separated artifact paths for append-node")
|
|
990
1096
|
parser.add_argument("--trigger", default="manual", help="Trigger type for benchmark-report")
|
|
1097
|
+
# render-page arguments
|
|
1098
|
+
parser.add_argument("--target", help="Repo-relative target artifact path for render-page")
|
|
1099
|
+
parser.add_argument("--title", default="", help="Page title for render-page")
|
|
1100
|
+
parser.add_argument("--nav-label", default="", help="Navigation label for render-page")
|
|
1101
|
+
parser.add_argument("--nav-group", default="Docs", help="Navigation group for render-page")
|
|
1102
|
+
parser.add_argument("--version", default="", help="Version marker for render-page")
|
|
1103
|
+
parser.add_argument("--uid", default="", help="Stable page/doc UID for render-page")
|
|
1104
|
+
parser.add_argument("--stage-mark", default="", help="Stage label for render-page")
|
|
1105
|
+
parser.add_argument("--markdown", default="", help="Markdown source path for render-page")
|
|
1106
|
+
parser.add_argument("--set", action="append", default=[], help="Extra variable key=value for render-page")
|
|
991
1107
|
args = parser.parse_args()
|
|
992
1108
|
|
|
993
1109
|
state_file = resolve_state_path(args.state_file)
|
|
@@ -1056,6 +1172,37 @@ def main() -> int:
|
|
|
1056
1172
|
action_update_last_updated(state_file)
|
|
1057
1173
|
return 0
|
|
1058
1174
|
|
|
1175
|
+
if args.action == "render-page":
|
|
1176
|
+
if not args.stage:
|
|
1177
|
+
parser.error("--stage is required for render-page")
|
|
1178
|
+
if not args.target:
|
|
1179
|
+
parser.error("--target is required for render-page")
|
|
1180
|
+
extra: dict[str, str] = {}
|
|
1181
|
+
for item in args.set:
|
|
1182
|
+
if "=" not in item:
|
|
1183
|
+
parser.error(f"--set value must be key=value: {item}")
|
|
1184
|
+
key, value = item.split("=", 1)
|
|
1185
|
+
extra[key] = value
|
|
1186
|
+
try:
|
|
1187
|
+
action_render_page(
|
|
1188
|
+
stage_id=args.stage,
|
|
1189
|
+
target=args.target,
|
|
1190
|
+
state=state,
|
|
1191
|
+
state_file=state_file,
|
|
1192
|
+
title=args.title,
|
|
1193
|
+
nav_label=args.nav_label,
|
|
1194
|
+
nav_group=args.nav_group,
|
|
1195
|
+
version=args.version,
|
|
1196
|
+
uid=args.uid,
|
|
1197
|
+
stage_mark=args.stage_mark,
|
|
1198
|
+
markdown_path=args.markdown,
|
|
1199
|
+
extra_variables=extra,
|
|
1200
|
+
)
|
|
1201
|
+
except Exception as exc:
|
|
1202
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1203
|
+
return 1
|
|
1204
|
+
return 0
|
|
1205
|
+
|
|
1059
1206
|
if args.action == "benchmark-report":
|
|
1060
1207
|
result = action_benchmark_report(state_file, args.trigger)
|
|
1061
1208
|
if result is None:
|
|
@@ -163,6 +163,42 @@ if errors:
|
|
|
163
163
|
print("All agent frontmatters valid.")
|
|
164
164
|
PY
|
|
165
165
|
|
|
166
|
+
echo "==> Validate imported external agents are in sync with manifests"
|
|
167
|
+
"$PYTHON" - "$ROOT" <<'PY'
|
|
168
|
+
import sys, yaml
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
|
|
171
|
+
root = Path(sys.argv[1])
|
|
172
|
+
external_dir = root / ".claude" / "agents" / "external"
|
|
173
|
+
errors = []
|
|
174
|
+
|
|
175
|
+
for manifest_path in sorted(external_dir.glob("*.manifest.yaml")):
|
|
176
|
+
docs = list(yaml.safe_load_all(manifest_path.read_text(encoding="utf-8")))
|
|
177
|
+
data = docs[0] if docs else {}
|
|
178
|
+
if not isinstance(data, dict):
|
|
179
|
+
continue
|
|
180
|
+
framework = manifest_path.stem.replace(".manifest", "")
|
|
181
|
+
prefix = data.get("prefix", framework)
|
|
182
|
+
imported = data.get("imported_agents", []) or []
|
|
183
|
+
target_dir = external_dir / framework / "agents"
|
|
184
|
+
expected_names = {f"{prefix}-{Path(agent.replace('.md', '')).name}.md" for agent in imported}
|
|
185
|
+
actual_names = {p.name for p in target_dir.glob("*.md")} if target_dir.exists() else set()
|
|
186
|
+
|
|
187
|
+
missing = expected_names - actual_names
|
|
188
|
+
extra = actual_names - expected_names
|
|
189
|
+
if missing:
|
|
190
|
+
errors.append(f"{manifest_path.name}: missing imported agents: {sorted(missing)}")
|
|
191
|
+
if extra:
|
|
192
|
+
errors.append(f"{manifest_path.name}: unexpected agents on disk: {sorted(extra)}")
|
|
193
|
+
|
|
194
|
+
if errors:
|
|
195
|
+
print("External agent sync issues:")
|
|
196
|
+
for e in errors:
|
|
197
|
+
print(f" - {e}")
|
|
198
|
+
sys.exit(1)
|
|
199
|
+
print("All imported external agents are present.")
|
|
200
|
+
PY
|
|
201
|
+
|
|
166
202
|
echo "==> Validate Python syntax"
|
|
167
203
|
"$PYTHON" -m py_compile scripts/validate_stage.py
|
|
168
204
|
|
|
@@ -245,7 +281,7 @@ for stage_yaml in sorted((root / ".claude" / "stages").glob("*.yaml")):
|
|
|
245
281
|
if not isinstance(data, dict):
|
|
246
282
|
errors.append(f"{stage_yaml.name}: root is not a mapping")
|
|
247
283
|
continue
|
|
248
|
-
for key in ["schema_version", "id", "name", "description", "
|
|
284
|
+
for key in ["schema_version", "id", "name", "description", "workflows", "prerequisite_stage", "next_stage", "human_gate", "agent", "skills", "artifacts", "context", "gates"]:
|
|
249
285
|
if key not in data:
|
|
250
286
|
errors.append(f"{stage_yaml.name}: missing key '{key}'")
|
|
251
287
|
|
|
@@ -341,6 +377,9 @@ echo "==> Check plugin package manifest"
|
|
|
341
377
|
find .claude scripts tools -type d -name __pycache__ -prune -exec rm -rf {} + 2>/dev/null || true
|
|
342
378
|
"$PYTHON" scripts/package-lincoln-plugin.py check
|
|
343
379
|
|
|
380
|
+
echo "==> Check prompt drift (PM stages only; non-PM drift reported separately)"
|
|
381
|
+
"$PYTHON" scripts/check-prompt-drift.py --strict --focus pm
|
|
382
|
+
|
|
344
383
|
echo "==> Run pytest"
|
|
345
384
|
"$PYTHON" -m pytest tests/ -v
|
|
346
385
|
pytest_exit=$?
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""README 版本引用锁步工具(配合 bump_version.py 的事实源 `.version-bump.json`)。
|
|
3
|
+
|
|
4
|
+
README.md / README.en.md 中的 release 徽章与版本公告行不属于 JSON manifest,
|
|
5
|
+
无法被 bump 的 JSON Pointer 覆盖,由本脚本按正则锁步。用法:
|
|
6
|
+
|
|
7
|
+
python3 scripts/update_readme_version.py # 锁步更新两份 README
|
|
8
|
+
python3 scripts/update_readme_version.py --check # CI:验证 README 与事实源一致
|
|
9
|
+
python3 scripts/update_readme_version.py --root /path/repo # 指定仓库根(默认脚本上一级)
|
|
10
|
+
python3 scripts/update_readme_version.py --allow-downgrade # 允许把 README 版本号改小
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Iterable
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
SEMVER = re.compile(r"^\d+\.\d+\.\d+$")
|
|
24
|
+
VERSION_EXTRACT = re.compile(r"v(\d+\.\d+\.\d+)")
|
|
25
|
+
|
|
26
|
+
BADGE = re.compile(r"(https://img\.shields\.io/badge/release-v)\d+\.\d+\.\d+(-blue)")
|
|
27
|
+
CALLOUT_ZH = re.compile(r"(\*\*v)\d+\.\d+\.\d+(\*\* 已发布)")
|
|
28
|
+
CALLOUT_EN = re.compile(r"(\*\*v)\d+\.\d+\.\d+(\*\* is released)")
|
|
29
|
+
|
|
30
|
+
READMES = (
|
|
31
|
+
("README.md", CALLOUT_ZH),
|
|
32
|
+
("README.en.md", CALLOUT_EN),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_semver(version: str) -> tuple[int, int, int]:
|
|
37
|
+
return tuple(int(part) for part in version.split("."))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_newer(left: str, right: str) -> bool:
|
|
41
|
+
return parse_semver(left) > parse_semver(right)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_version(root: Path) -> str:
|
|
45
|
+
source = root / ".version-bump.json"
|
|
46
|
+
if not source.exists():
|
|
47
|
+
sys.exit("missing version source: .version-bump.json")
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(source.read_text(encoding="utf-8"))
|
|
50
|
+
except json.JSONDecodeError as exc:
|
|
51
|
+
sys.exit(f"malformed .version-bump.json: {exc}")
|
|
52
|
+
version = data.get("version", "")
|
|
53
|
+
if not SEMVER.match(version):
|
|
54
|
+
sys.exit(f".version-bump.json has invalid version: {version!r}")
|
|
55
|
+
return version
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sync_text(text: str, version: str, patterns: Iterable[re.Pattern[str]]) -> str:
|
|
59
|
+
"""把 text 中所有 pattern 命中的版本号替换为 version,返回新文本(不原地修改)。"""
|
|
60
|
+
for pattern in patterns:
|
|
61
|
+
text = pattern.sub(lambda m: m.group(1) + version + m.group(2), text)
|
|
62
|
+
return text
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_stale(text: str, version: str, patterns: Iterable[re.Pattern[str]]) -> str | None:
|
|
66
|
+
"""返回第一个与事实源不一致的命中片段;无命中或已一致返回 None。"""
|
|
67
|
+
for pattern in patterns:
|
|
68
|
+
for match in pattern.finditer(text):
|
|
69
|
+
if match.group(0) != match.group(1) + version + match.group(2):
|
|
70
|
+
return match.group(0)
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def readme_version(text: str) -> str | None:
|
|
75
|
+
"""从 README 文本中提取第一个 vX.Y.Z 版本号(徽章或公告行)。"""
|
|
76
|
+
match = VERSION_EXTRACT.search(text)
|
|
77
|
+
return match.group(1) if match else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_update(root: Path, check: bool, allow_downgrade: bool) -> int:
|
|
81
|
+
version = load_version(root)
|
|
82
|
+
drifted: list[tuple[str, str]] = []
|
|
83
|
+
skipped: list[tuple[str, str]] = []
|
|
84
|
+
|
|
85
|
+
for name, callout in READMES:
|
|
86
|
+
path = root / name
|
|
87
|
+
if not path.exists():
|
|
88
|
+
sys.exit(f"missing README: {name}")
|
|
89
|
+
original = path.read_text(encoding="utf-8")
|
|
90
|
+
stale = find_stale(original, version, (BADGE, callout))
|
|
91
|
+
if stale is None:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
current = readme_version(original)
|
|
95
|
+
if current is not None and is_newer(current, version) and not allow_downgrade:
|
|
96
|
+
skipped.append((name, current))
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
drifted.append((name, stale))
|
|
100
|
+
if not check:
|
|
101
|
+
path.write_text(
|
|
102
|
+
sync_text(original, version, (BADGE, callout)), encoding="utf-8"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if skipped:
|
|
106
|
+
print(
|
|
107
|
+
f"README version(s) newer than source v{version}; skipping to avoid downgrade. "
|
|
108
|
+
"Use --allow-downgrade to force.",
|
|
109
|
+
file=sys.stderr,
|
|
110
|
+
)
|
|
111
|
+
for name, current in skipped:
|
|
112
|
+
print(f" {name}: v{current} -> v{version} skipped", file=sys.stderr)
|
|
113
|
+
|
|
114
|
+
if check:
|
|
115
|
+
if drifted:
|
|
116
|
+
print(f"README version drift detected (source: v{version}):", file=sys.stderr)
|
|
117
|
+
for name, stale in drifted:
|
|
118
|
+
print(f" {name}: {stale!r} != v{version}", file=sys.stderr)
|
|
119
|
+
print("fix: python3 scripts/update_readme_version.py", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
print(f"README version lockstep OK: {version} ({len(READMES)} files)")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
for name, _ in drifted:
|
|
125
|
+
print(f" updated {name} -> v{version}")
|
|
126
|
+
if drifted:
|
|
127
|
+
print(f"updated {len(drifted)} README(s) to v{version}")
|
|
128
|
+
elif not skipped:
|
|
129
|
+
print(f"READMEs already at v{version}; nothing to do")
|
|
130
|
+
return 0 if not skipped else 1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(argv: list[str], root: Path = ROOT) -> int:
|
|
134
|
+
parser = argparse.ArgumentParser(
|
|
135
|
+
description="Sync README version badges and callouts with .version-bump.json."
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument("--check", action="store_true", help="verify lockstep without writing")
|
|
138
|
+
parser.add_argument("--root", type=Path, default=root, help="repository root")
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--allow-downgrade",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="allow rewriting README version to an older version",
|
|
143
|
+
)
|
|
144
|
+
args = parser.parse_args(argv[1:])
|
|
145
|
+
return cmd_update(args.root, check=args.check, allow_downgrade=args.allow_downgrade)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
sys.exit(main(sys.argv))
|
|
@@ -23,7 +23,11 @@ if str(PROJECT_ROOT) not in sys.path:
|
|
|
23
23
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
24
24
|
|
|
25
25
|
from scripts.lincoln_documents import extract_markdown_version
|
|
26
|
-
from scripts.lincoln_index import
|
|
26
|
+
from scripts.lincoln_index import (
|
|
27
|
+
VERSION_COMMENT_RE,
|
|
28
|
+
extract_html_markdown,
|
|
29
|
+
extract_html_meta,
|
|
30
|
+
)
|
|
27
31
|
from scripts.lincoln_paths import get_process_slug, load_yaml, resolve_state_path
|
|
28
32
|
|
|
29
33
|
|
|
@@ -246,6 +250,75 @@ def check_portal_index_exists() -> None:
|
|
|
246
250
|
pass_check(f"portal index exists: {target}")
|
|
247
251
|
|
|
248
252
|
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Schema 3.0.0 placeholder checks (registered but not yet implemented)
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def check_artifact_meta_complete() -> None:
|
|
259
|
+
"""Verify that declared HTML artifacts carry the meta tags the portal needs."""
|
|
260
|
+
state_file = resolve_state_path(None, PROJECT_ROOT)
|
|
261
|
+
if not state_file or not state_file.exists():
|
|
262
|
+
fail("No state file found")
|
|
263
|
+
state = load_yaml(state_file)
|
|
264
|
+
stage_id = state.get("current_run", {}).get("current_stage")
|
|
265
|
+
if not stage_id:
|
|
266
|
+
fail("No current stage in state")
|
|
267
|
+
|
|
268
|
+
stage_path = PROJECT_ROOT / ".claude" / "stages" / f"{stage_id}.yaml"
|
|
269
|
+
if not stage_path.exists():
|
|
270
|
+
fail(f"Stage YAML not found: {stage_path}")
|
|
271
|
+
stage = load_yaml(stage_path)
|
|
272
|
+
|
|
273
|
+
variables = state.get("current_run", {}).get("variables", {})
|
|
274
|
+
process_slug = str(variables.get("process_slug") or get_process_slug(state, state_file))
|
|
275
|
+
|
|
276
|
+
artifacts = stage.get("artifacts", {})
|
|
277
|
+
required = artifacts.get("required", [])
|
|
278
|
+
html_artifacts = [str(art) for art in required if str(art).endswith(".html")]
|
|
279
|
+
if not html_artifacts:
|
|
280
|
+
pass_check("no HTML artifacts to check")
|
|
281
|
+
|
|
282
|
+
missing: list[str] = []
|
|
283
|
+
for art in html_artifacts:
|
|
284
|
+
path = art.replace("{process_slug}", process_slug)
|
|
285
|
+
for key, value in variables.items():
|
|
286
|
+
path = path.replace(f"{{{key}}}", str(value))
|
|
287
|
+
target = PROJECT_ROOT / path
|
|
288
|
+
if "*" in path:
|
|
289
|
+
# Glob artifact: check all present matches.
|
|
290
|
+
for match in PROJECT_ROOT.glob(path):
|
|
291
|
+
meta = extract_html_meta(match)
|
|
292
|
+
rel = str(match.relative_to(PROJECT_ROOT))
|
|
293
|
+
if not meta.get("nav_label"):
|
|
294
|
+
missing.append(f"{rel}: missing nav-label")
|
|
295
|
+
if not meta.get("uid"):
|
|
296
|
+
missing.append(f"{rel}: missing doc-uid/page-uid")
|
|
297
|
+
continue
|
|
298
|
+
if not target.exists():
|
|
299
|
+
missing.append(f"{path}: file not found")
|
|
300
|
+
continue
|
|
301
|
+
meta = extract_html_meta(target)
|
|
302
|
+
if not meta.get("nav_label"):
|
|
303
|
+
missing.append(f"{path}: missing nav-label")
|
|
304
|
+
if not meta.get("uid"):
|
|
305
|
+
missing.append(f"{path}: missing doc-uid/page-uid")
|
|
306
|
+
|
|
307
|
+
if missing:
|
|
308
|
+
fail("Artifact meta incomplete:\n - " + "\n - ".join(missing))
|
|
309
|
+
pass_check("all HTML artifacts have required meta tags")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def check_ids_valid() -> None:
|
|
313
|
+
"""Placeholder: verify referenced IDs (session/design/change/etc.) are valid. Not yet implemented."""
|
|
314
|
+
pass_check("not implemented")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def check_portal_index_fresh() -> None:
|
|
318
|
+
"""Placeholder: verify portal index is up to date with artifacts. Not yet implemented."""
|
|
319
|
+
pass_check("not implemented")
|
|
320
|
+
|
|
321
|
+
|
|
249
322
|
# ---------------------------------------------------------------------------
|
|
250
323
|
# PRD checks
|
|
251
324
|
# ---------------------------------------------------------------------------
|
|
@@ -310,6 +383,9 @@ ENTRY_CHECKS = {
|
|
|
310
383
|
"artifact_exists": check_artifact_exists,
|
|
311
384
|
"audio_format_supported": check_audio_format_supported,
|
|
312
385
|
"previous_stage_completed": check_previous_stage_completed,
|
|
386
|
+
"artifact_meta_complete": check_artifact_meta_complete,
|
|
387
|
+
"ids_valid": check_ids_valid,
|
|
388
|
+
"portal_index_fresh": check_portal_index_fresh,
|
|
313
389
|
}
|
|
314
390
|
|
|
315
391
|
EXIT_CHECKS = {
|
|
@@ -322,6 +398,9 @@ EXIT_CHECKS = {
|
|
|
322
398
|
"prd_has_required_sections": check_prd_has_required_sections,
|
|
323
399
|
"prd_snapshot_present": check_prd_snapshot_present,
|
|
324
400
|
"portal_index_exists": check_portal_index_exists,
|
|
401
|
+
"artifact_meta_complete": check_artifact_meta_complete,
|
|
402
|
+
"ids_valid": check_ids_valid,
|
|
403
|
+
"portal_index_fresh": check_portal_index_fresh,
|
|
325
404
|
}
|
|
326
405
|
|
|
327
406
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sushanglewis/lincoln",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@sushanglewis/lincoln",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.6.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ink": "^5.2.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sushanglewis/lincoln",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Global Lincoln agent harness plugin CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"typecheck:tests": "tsc -p tsconfig.tests.json",
|
|
25
25
|
"lint": "echo 'lint not configured'",
|
|
26
26
|
"prepack": "npm run build && node ../../scripts/sync-framework-package.mjs",
|
|
27
|
-
"postinstall": "node dist/postinstall.js"
|
|
27
|
+
"postinstall": "node -e \"const fs=require('fs'); if(fs.existsSync('dist/postinstall.js')){import('./dist/postinstall.js').then(m=>m.runPostinstall()).then(c=>process.exit(c)).catch(()=>process.exit(0))}\""
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"js-yaml": "^4.1.0"
|