@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.
Files changed (95) hide show
  1. package/dist/main.js +425 -51
  2. package/dist/main.js.map +1 -1
  3. package/dist/postinstall.d.ts +9 -2
  4. package/dist/postinstall.js +812 -6
  5. package/dist/postinstall.js.map +1 -1
  6. package/framework/.claude/agents/architect.md +15 -15
  7. package/framework/.claude/agents/default.md +118 -104
  8. package/framework/.claude/agents/designer.md +17 -17
  9. package/framework/.claude/agents/engineer.md +16 -25
  10. package/framework/.claude/agents/external/NOTICES.md +1 -1
  11. package/framework/.claude/agents/external/everything-claude-code/agents/ecc-tdd-guide.md +108 -0
  12. package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +1 -1
  13. package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-architect.md +134 -0
  14. package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-code-reviewer.md +248 -0
  15. package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-designer.md +122 -0
  16. package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-qa-tester.md +106 -0
  17. package/framework/.claude/agents/external/wshobson-agents/agents/wshobson-frontend-developer.md +173 -0
  18. package/framework/.claude/agents/frontend-engineer.md +15 -16
  19. package/framework/.claude/agents/knowledge-curator.md +18 -15
  20. package/framework/.claude/agents/pm.md +17 -23
  21. package/framework/.claude/agents/qa.md +17 -13
  22. package/framework/.claude/agents/release-coordinator.md +18 -12
  23. package/framework/.claude/agents/researcher.md +16 -26
  24. package/framework/.claude/harnesses/command-map.yaml +12 -5
  25. package/framework/.claude/hooks/on-session-start.sh +0 -1
  26. package/framework/.claude/hooks/pre-tool-use.sh +3 -3
  27. package/framework/.claude/schemas/agent-definition.schema.json +6 -1
  28. package/framework/.claude/schemas/stage-definition.schema.json +49 -4
  29. package/framework/.claude/skills/build-product-prototype/SKILL.md +1 -1
  30. package/framework/.claude/skills/build-product-prototype/prompts/main.md +89 -26
  31. package/framework/.claude/skills/clarify-requirements/prompts/main.md +80 -14
  32. package/framework/.claude/skills/dependencies.yaml +6 -0
  33. package/framework/.claude/skills/draft-product-design/SKILL.md +2 -0
  34. package/framework/.claude/skills/draft-product-design/prompts/main.md +30 -17
  35. package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +2 -0
  36. package/framework/.claude/skills/lc-explore-opensource/SKILL.md +2 -0
  37. package/framework/.claude/skills/lc-market-research/SKILL.md +1 -1
  38. package/framework/.claude/skills/lc-one-id/SKILL.md +81 -0
  39. package/framework/.claude/skills/lc-one-id/prompts/main.md +35 -0
  40. package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +1 -1
  41. package/framework/.claude/skills/lc-workflow/validators/validate.py +2 -0
  42. package/framework/.claude/skills/lc-workflow-router/SKILL.md +2 -0
  43. package/framework/.claude/stages/build-codebase-knowledge.yaml +10 -10
  44. package/framework/.claude/stages/clarify.yaml +22 -16
  45. package/framework/.claude/stages/explore-opensource.yaml +13 -10
  46. package/framework/.claude/stages/implement.yaml +11 -11
  47. package/framework/.claude/stages/ingest.yaml +13 -10
  48. package/framework/.claude/stages/lc-analyze-frameworks.yaml +13 -10
  49. package/framework/.claude/stages/lc-collect-intelligence.yaml +13 -10
  50. package/framework/.claude/stages/lc-competitive-analysis.yaml +13 -10
  51. package/framework/.claude/stages/lc-first-principles.yaml +13 -10
  52. package/framework/.claude/stages/lc-market-research.yaml +13 -10
  53. package/framework/.claude/stages/lc-product-research.yaml +13 -10
  54. package/framework/.claude/stages/lc-research-report.yaml +13 -10
  55. package/framework/.claude/stages/lc-research-scope.yaml +13 -10
  56. package/framework/.claude/stages/lc-stakeholder-research.yaml +13 -10
  57. package/framework/.claude/stages/lc-storytelling.yaml +13 -10
  58. package/framework/.claude/stages/product-design-docs.yaml +33 -19
  59. package/framework/.claude/stages/product-prototype.yaml +22 -18
  60. package/framework/.claude/stages/propose.yaml +16 -9
  61. package/framework/.claude/stages/split.yaml +13 -9
  62. package/framework/.claude/stages/sync-knowledge.yaml +20 -9
  63. package/framework/.claude/stages/tdd-development-plan.yaml +17 -9
  64. package/framework/.claude/stages/workflow-router.yaml +17 -13
  65. package/framework/.claude/workflows/README.md +25 -3
  66. package/framework/.claude/workflows/bug-fix.yaml +1 -0
  67. package/framework/.claude/workflows/existing-project-iteration.yaml +1 -0
  68. package/framework/.claude/workflows/interview-to-knowledge.yaml +4 -2
  69. package/framework/.claude/workflows/oss-first-design.yaml +1 -0
  70. package/framework/.claude-plugin/marketplace.json +2 -2
  71. package/framework/.claude-plugin/plugin.json +2 -1
  72. package/framework/.version-bump.json +1 -1
  73. package/framework/README.en.md +15 -2
  74. package/framework/README.md +16 -3
  75. package/framework/RELEASE.md +32 -0
  76. package/framework/manifest.json +128 -88
  77. package/framework/scripts/check-prompt-drift.py +657 -0
  78. package/framework/scripts/extract_release_notes.py +79 -0
  79. package/framework/scripts/init-lincoln-branch.sh +6 -0
  80. package/framework/scripts/lincoln_command_map.py +1 -1
  81. package/framework/scripts/lincoln_id.py +333 -0
  82. package/framework/scripts/lincoln_index.py +10 -0
  83. package/framework/scripts/lincoln_prd.py +16 -17
  84. package/framework/scripts/lincoln_render.py +657 -0
  85. package/framework/scripts/run-infrastructure-tests.py +2 -0
  86. package/framework/scripts/stage_loader.py +149 -2
  87. package/framework/scripts/static-check.sh +40 -1
  88. package/framework/scripts/sync-external-agents.sh +2 -0
  89. package/framework/scripts/update_readme_version.py +149 -0
  90. package/framework/scripts/validate_stage.py +80 -1
  91. package/framework/tools/lincoln/package-lock.json +2 -2
  92. package/framework/tools/lincoln/package.json +1 -1
  93. package/framework/tools/lincoln-installer/package.json +1 -1
  94. package/package.json +2 -2
  95. 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: interpolate(value, variables)
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", "templates", "prerequisite_stage", "next_stage", "human_gate", "agent", "skills", "artifacts", "context", "gates"]:
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=$?
@@ -19,6 +19,8 @@ AGENTS_DIR="$ROOT/.claude/agents/external"
19
19
  mkdir -p "$AGENTS_DIR"
20
20
 
21
21
  "$ROOT/.venv/bin/python3" - "$AGENTS_DIR" "$DRY_RUN" <<'PY'
22
+ from __future__ import annotations
23
+
22
24
  import sys
23
25
  from pathlib import Path
24
26
  import re
@@ -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 VERSION_COMMENT_RE, extract_html_markdown
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.5.4",
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.5.4",
9
+ "version": "1.6.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "ink": "^5.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sushanglewis/lincoln-recorder",
3
- "version": "1.5.4",
3
+ "version": "1.6.1",
4
4
  "description": "Cross-tool terminal UI for recording Lincoln interviews",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lincoln-install",
3
- "version": "1.5.4",
3
+ "version": "1.6.1",
4
4
  "description": "Terminal TUI installer for the Lincoln AI-Native R&D workflow system",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sushanglewis/lincoln",
3
- "version": "1.5.4",
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"