llm-wb 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.agentic/00.chat/README.md +78 -0
  2. package/.agentic/00.chat/checklists/before-commit.md +195 -0
  3. package/.agentic/00.chat/checklists/llm-workbench-public-beta.md +94 -0
  4. package/.agentic/00.chat/commands/README.md +108 -0
  5. package/.agentic/00.chat/migration-plan.md +132 -0
  6. package/.agentic/00.chat/skills/session-summary.md +48 -0
  7. package/.agentic/00.chat/standards/llm-workbench-public-beta-contract.md +216 -0
  8. package/.agentic/00.chat/standards/main-refresh-conflict-types.md +358 -0
  9. package/.agentic/00.chat/workflows/README.md +40 -0
  10. package/.agentic/00.chat/workflows/bootstrap-chat-workbench-repo.md +212 -0
  11. package/.agentic/00.chat/workflows/chat-cleanup.md +102 -0
  12. package/.agentic/00.chat/workflows/chat-commit.md +56 -0
  13. package/.agentic/00.chat/workflows/chat-promote-to-main.md +169 -0
  14. package/.agentic/00.chat/workflows/chat-refresh-from-main.md +242 -0
  15. package/.agentic/00.chat/workflows/chat-reporting.md +69 -0
  16. package/.agentic/00.chat/workflows/chat-start.md +173 -0
  17. package/.agentic/00.chat/workflows/chat-upstream-reusable-lesson.md +123 -0
  18. package/.agentic/shared/standards/README.md +32 -0
  19. package/.agentic/shared/standards/upstream-repo-bootstrap.md +131 -0
  20. package/.agentic/shared/workflows/README.md +35 -0
  21. package/.agentic/shared/workflows/capability-resolution-workflow.md +189 -0
  22. package/.agentic/shared/workflows/change-shared-process.md +92 -0
  23. package/.cursor/rules/llm-workbench.mdc +17 -0
  24. package/.github/copilot-instructions.md +16 -0
  25. package/AGENTS.md +63 -0
  26. package/CLAUDE.md +16 -0
  27. package/CONTRIBUTING.md +57 -0
  28. package/LICENSE +21 -0
  29. package/LLM_WORKBENCH.md +17 -0
  30. package/README.md +98 -0
  31. package/SECURITY.md +44 -0
  32. package/bin/llm-workbench.js +672 -0
  33. package/docs/00.chat/README.md +47 -0
  34. package/docs/00.chat/llm-workbench-acceptance-matrix.md +55 -0
  35. package/docs/00.chat/script-layout.md +107 -0
  36. package/docs/adapting-to-your-repo.md +29 -0
  37. package/docs/concepts.md +38 -0
  38. package/docs/install.md +114 -0
  39. package/docs/public-beta-contract.md +45 -0
  40. package/docs/workflows.md +103 -0
  41. package/examples/minimal-repo/README.md +13 -0
  42. package/package.json +93 -0
  43. package/scripts/00.chat/README.md +46 -0
  44. package/scripts/00.chat/bootstrap/README.md +35 -0
  45. package/scripts/00.chat/bootstrap/audit-chat-bootstrap-file-set/README.md +39 -0
  46. package/scripts/00.chat/bootstrap/audit-chat-bootstrap-file-set/script.sh +213 -0
  47. package/scripts/00.chat/closeout/README.md +30 -0
  48. package/scripts/00.chat/closeout/build-closeout-prompt/README.md +35 -0
  49. package/scripts/00.chat/closeout/build-closeout-prompt/script.sh +124 -0
  50. package/scripts/00.chat/command/README.md +31 -0
  51. package/scripts/00.chat/command/close/README.md +30 -0
  52. package/scripts/00.chat/command/close/script.sh +25 -0
  53. package/scripts/00.chat/command/dispatcher/README.md +46 -0
  54. package/scripts/00.chat/command/dispatcher/script.sh +91 -0
  55. package/scripts/00.chat/command/dispatcher/smoke-test.sh +168 -0
  56. package/scripts/00.chat/command/new/README.md +32 -0
  57. package/scripts/00.chat/command/new/script.sh +28 -0
  58. package/scripts/00.chat/command/open-window/README.md +38 -0
  59. package/scripts/00.chat/command/open-window/script.sh +25 -0
  60. package/scripts/00.chat/command/package-scripts/README.md +34 -0
  61. package/scripts/00.chat/command/package-scripts/smoke-test.sh +113 -0
  62. package/scripts/00.chat/git/README.md +30 -0
  63. package/scripts/00.chat/git/cleanup-empty-chat-branches/README.md +36 -0
  64. package/scripts/00.chat/git/cleanup-empty-chat-branches/script.sh +243 -0
  65. package/scripts/00.chat/git/cleanup-empty-chat-branches/smoke-test.sh +136 -0
  66. package/scripts/00.chat/local-merge/README.md +30 -0
  67. package/scripts/00.chat/local-merge/list-active-chat-branches/README.md +29 -0
  68. package/scripts/00.chat/local-merge/list-active-chat-branches/script.sh +109 -0
  69. package/scripts/00.chat/local-merge/report-chat-branch-overlaps/README.md +29 -0
  70. package/scripts/00.chat/local-merge/report-chat-branch-overlaps/script.sh +142 -0
  71. package/scripts/00.chat/local-merge/verify-chat-ready-to-merge-local-main/README.md +33 -0
  72. package/scripts/00.chat/local-merge/verify-chat-ready-to-merge-local-main/script.sh +345 -0
  73. package/scripts/00.chat/local-merge/verify-chat-ready-to-merge-local-main/smoke-test.sh +244 -0
  74. package/scripts/00.chat/main-refresh/README.md +39 -0
  75. package/scripts/00.chat/main-refresh/apply-rehearsed-refresh/README.md +32 -0
  76. package/scripts/00.chat/main-refresh/apply-rehearsed-refresh/script.sh +198 -0
  77. package/scripts/00.chat/main-refresh/check-chat-is-current-with-main/README.md +30 -0
  78. package/scripts/00.chat/main-refresh/check-chat-is-current-with-main/script.sh +121 -0
  79. package/scripts/00.chat/main-refresh/classify-conflict/README.md +39 -0
  80. package/scripts/00.chat/main-refresh/classify-conflict/script.sh +169 -0
  81. package/scripts/00.chat/main-refresh/classify-conflict/smoke-test.sh +137 -0
  82. package/scripts/00.chat/main-refresh/classify-refresh-readiness/README.md +35 -0
  83. package/scripts/00.chat/main-refresh/classify-refresh-readiness/script.sh +171 -0
  84. package/scripts/00.chat/main-refresh/classify-refresh-readiness/smoke-test.sh +132 -0
  85. package/scripts/00.chat/main-refresh/rehearse-refresh-from-main/README.md +34 -0
  86. package/scripts/00.chat/main-refresh/rehearse-refresh-from-main/script.sh +124 -0
  87. package/scripts/00.chat/main-refresh/rehearse-refresh-from-main/smoke-test.sh +257 -0
  88. package/scripts/00.chat/main-refresh/show-main-update-status/README.md +31 -0
  89. package/scripts/00.chat/main-refresh/show-main-update-status/script.sh +73 -0
  90. package/scripts/00.chat/main-refresh/verify-conflict-audit/README.md +37 -0
  91. package/scripts/00.chat/main-refresh/verify-conflict-audit/script.sh +154 -0
  92. package/scripts/00.chat/main-refresh/verify-conflict-audit/smoke-test.sh +99 -0
  93. package/scripts/00.chat/metrics/README.md +35 -0
  94. package/scripts/00.chat/metrics/data/chat-pricing.json +107 -0
  95. package/scripts/00.chat/metrics/data/chat-pricing.schema.json +63 -0
  96. package/scripts/00.chat/metrics/estimate-chat-cost/README.md +40 -0
  97. package/scripts/00.chat/metrics/estimate-chat-cost/script.js +130 -0
  98. package/scripts/00.chat/migration/README.md +30 -0
  99. package/scripts/00.chat/migration/audit-chat-layer-migration/README.md +33 -0
  100. package/scripts/00.chat/migration/audit-chat-layer-migration/script.sh +127 -0
  101. package/scripts/00.chat/recovery/README.md +30 -0
  102. package/scripts/00.chat/recovery/import-active-paths-to-chat-worktree/README.md +76 -0
  103. package/scripts/00.chat/recovery/import-active-paths-to-chat-worktree/script.sh +212 -0
  104. package/scripts/00.chat/recovery/import-active-paths-to-chat-worktree/smoke-test.sh +162 -0
  105. package/scripts/00.chat/reporting/README.md +30 -0
  106. package/scripts/00.chat/reporting/generate-commit-log-summary/README.md +35 -0
  107. package/scripts/00.chat/reporting/generate-commit-log-summary/script.sh +299 -0
  108. package/scripts/00.chat/reporting/generate-commit-log-summary/smoke-test.sh +93 -0
  109. package/scripts/00.chat/reporting/report-chat-workspaces/README.md +32 -0
  110. package/scripts/00.chat/reporting/report-chat-workspaces/script.sh +82 -0
  111. package/scripts/00.chat/session-log/README.md +33 -0
  112. package/scripts/00.chat/session-log/check-commit-prerequisites/README.md +89 -0
  113. package/scripts/00.chat/session-log/check-commit-prerequisites/script.sh +121 -0
  114. package/scripts/00.chat/session-log/check-commit-prerequisites/smoke-test.sh +119 -0
  115. package/scripts/00.chat/session-log/check-commitlog-deletions/README.md +90 -0
  116. package/scripts/00.chat/session-log/check-commitlog-deletions/script.sh +131 -0
  117. package/scripts/00.chat/session-log/check-commitlog-deletions/smoke-test.sh +123 -0
  118. package/scripts/00.chat/session-log/checkpoint-chat-session-log/README.md +98 -0
  119. package/scripts/00.chat/session-log/checkpoint-chat-session-log/script.sh +126 -0
  120. package/scripts/00.chat/session-log/paths/README.md +38 -0
  121. package/scripts/00.chat/session-log/paths/lib.sh +133 -0
  122. package/scripts/00.chat/session-log/prepare-chat-session-before-commit/README.md +90 -0
  123. package/scripts/00.chat/session-log/prepare-chat-session-before-commit/script.sh +145 -0
  124. package/scripts/00.chat/session-log/read-current-chat-log/README.md +44 -0
  125. package/scripts/00.chat/session-log/read-current-chat-log/script.sh +92 -0
  126. package/scripts/00.chat/session-log/read-current-chat-log/smoke-test.sh +127 -0
  127. package/scripts/00.chat/session-log/record-chat-commit/README.md +133 -0
  128. package/scripts/00.chat/session-log/record-chat-commit/script.sh +394 -0
  129. package/scripts/00.chat/session-log/record-chat-commit/smoke-test.sh +227 -0
  130. package/scripts/00.chat/session-log/record-main-refresh-conflict/README.md +34 -0
  131. package/scripts/00.chat/session-log/record-main-refresh-conflict/script.sh +239 -0
  132. package/scripts/00.chat/session-log/rename-current-chat-log-folder/README.md +32 -0
  133. package/scripts/00.chat/session-log/rename-current-chat-log-folder/script.sh +112 -0
  134. package/scripts/00.chat/session-log/update-chat-log/README.md +32 -0
  135. package/scripts/00.chat/session-log/update-chat-log/script.sh +294 -0
  136. package/scripts/00.chat/startup/README.md +37 -0
  137. package/scripts/00.chat/startup/auto-start-missing-session/README.md +113 -0
  138. package/scripts/00.chat/startup/auto-start-missing-session/script.sh +54 -0
  139. package/scripts/00.chat/startup/resolve-current-chat-session/README.md +57 -0
  140. package/scripts/00.chat/startup/resolve-current-chat-session/script.sh +47 -0
  141. package/scripts/00.chat/startup/resolve-current-chat-session/smoke-test.sh +130 -0
  142. package/scripts/00.chat/startup/start-chat-session/README.md +197 -0
  143. package/scripts/00.chat/startup/start-chat-session/script.sh +330 -0
  144. package/scripts/00.chat/startup/start-chat-session/smoke-test.sh +182 -0
  145. package/scripts/00.chat/startup/start-new-chat/README.md +31 -0
  146. package/scripts/00.chat/startup/start-new-chat/script.sh +29 -0
  147. package/scripts/00.chat/transcript/README.md +36 -0
  148. package/scripts/00.chat/transcript/discover-codex-session-log/README.md +32 -0
  149. package/scripts/00.chat/transcript/discover-codex-session-log/script.sh +106 -0
  150. package/scripts/00.chat/transcript/register-codex-session-log/README.md +32 -0
  151. package/scripts/00.chat/transcript/register-codex-session-log/script.sh +115 -0
  152. package/scripts/00.chat/worktree/README.md +32 -0
  153. package/scripts/00.chat/worktree/check-write-location/README.md +87 -0
  154. package/scripts/00.chat/worktree/check-write-location/script.sh +95 -0
  155. package/scripts/00.chat/worktree/dirty-worktree-check/README.md +77 -0
  156. package/scripts/00.chat/worktree/dirty-worktree-check/script.sh +93 -0
  157. package/scripts/00.chat/worktree/ensure-chat-worktree/README.md +33 -0
  158. package/scripts/00.chat/worktree/ensure-chat-worktree/script.sh +132 -0
  159. package/scripts/00.chat/worktree/open-window/README.md +34 -0
  160. package/scripts/00.chat/worktree/open-window/script.sh +131 -0
  161. package/scripts/00.chat/worktree/paths/README.md +32 -0
  162. package/scripts/00.chat/worktree/paths/lib.sh +71 -0
  163. package/scripts/01.harness/artifact-metadata/check-headers/script.sh +522 -0
  164. package/scripts/01.harness/artifact-metadata/check-headers/smoke-test.sh +48 -0
  165. package/scripts/01.harness/check-deterministic-process-drift.sh +416 -0
  166. package/scripts/01.harness/check-governed-script-command-drift.sh +184 -0
  167. package/scripts/01.harness/run-governed-script.sh +178 -0
  168. package/scripts/install.sh +503 -0
  169. package/scripts/uninstall.sh +199 -0
  170. package/tests/smoke-test-install.sh +70 -0
@@ -0,0 +1,522 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # agentic-artifact:
5
+ # schema: agentic-artifact/v2
6
+ # id: harness.script.artifact-metadata.check-headers
7
+ # version: 1
8
+ # status: active
9
+ # layer: 01.harness
10
+ # domain: metadata
11
+ # disciplines:
12
+ # - agentic
13
+ # kind: script
14
+ # purpose: Validate v1 and v2 artifact metadata headers for harness artifacts.
15
+ # portability:
16
+ # class: required
17
+ # targets:
18
+ # - llm-workbench
19
+ # - entity-builder
20
+ # - design-system-builder
21
+ # effects:
22
+ # - read-only
23
+ # used_by:
24
+ # - id: harness.standard.artifact-metadata
25
+ # - id: harness.checklist.before-commit
26
+ # path: .agentic/00.chat/checklists/before-commit.md
27
+
28
+ python3 - "$@" <<'PY'
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import re
33
+ import subprocess
34
+ import sys
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ try:
39
+ import yaml
40
+ except ImportError: # pragma: no cover - environment gate
41
+ print("ERROR: python3 yaml module is required for artifact metadata checks.", file=sys.stderr)
42
+ sys.exit(2)
43
+
44
+
45
+ V1_OWNERS = {"00.chat", "shared", "harness", "aws", "product", "education"}
46
+ V1_PORTABILITY = {
47
+ "llm-workbench-required",
48
+ "llm-workbench-validation",
49
+ "llm-workbench-compatibility",
50
+ "source-only",
51
+ "internal",
52
+ }
53
+ PATH_PREFIXES = (
54
+ "AGENTS.md",
55
+ ".github/workflows/",
56
+ ".agentic/",
57
+ "docs/00.chat/",
58
+ "docs/02.rag-rulebook/",
59
+ "docs/harness/",
60
+ "infra/",
61
+ "scripts/",
62
+ )
63
+ ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*$")
64
+ DOMAIN_RE = ID_RE
65
+
66
+
67
+ class CheckError(Exception):
68
+ pass
69
+
70
+
71
+ def repo_root() -> Path:
72
+ result = subprocess.run(
73
+ ["git", "rev-parse", "--show-toplevel"],
74
+ check=True,
75
+ text=True,
76
+ stdout=subprocess.PIPE,
77
+ )
78
+ return Path(result.stdout.strip())
79
+
80
+
81
+ ROOT = repo_root()
82
+
83
+
84
+ def load_taxonomy() -> dict[str, Any]:
85
+ path = ROOT / ".agentic" / "01.harness" / "artifact-metadata" / "taxonomy.yml"
86
+ if not path.exists():
87
+ return {
88
+ "layers": [
89
+ {"id": "00.chat"},
90
+ {"id": "01.harness"},
91
+ {"id": "02.rag-rulebook"},
92
+ {"id": "03.product"},
93
+ {"id": "04.deploy"},
94
+ {"id": "05.education"},
95
+ {"id": "06.shared"},
96
+ ],
97
+ "disciplines": [
98
+ "agentic",
99
+ "architecture",
100
+ "backend",
101
+ "frontend",
102
+ "requirements",
103
+ "security",
104
+ "sre",
105
+ ],
106
+ "statuses": ["draft", "active", "deprecated", "retired"],
107
+ "portability_classes": [
108
+ "required",
109
+ "reusable",
110
+ "compatible",
111
+ "source-only",
112
+ "internal",
113
+ ],
114
+ "script_effects": [
115
+ "read-only",
116
+ "writes-files",
117
+ "stages-files",
118
+ "commits",
119
+ "branches",
120
+ "worktrees",
121
+ "network",
122
+ "destructive",
123
+ ],
124
+ }
125
+ with path.open("r", encoding="utf-8") as handle:
126
+ data = yaml.safe_load(handle) or {}
127
+ return data
128
+
129
+
130
+ TAXONOMY = load_taxonomy()
131
+ LAYERS = {entry["id"] for entry in TAXONOMY.get("layers", [])}
132
+ DISCIPLINES = set(TAXONOMY.get("disciplines", []))
133
+ STATUSES = set(TAXONOMY.get("statuses", []))
134
+ PORTABILITY_CLASSES = set(TAXONOMY.get("portability_classes", []))
135
+ SCRIPT_EFFECTS = set(TAXONOMY.get("script_effects", []))
136
+
137
+
138
+ def usage() -> str:
139
+ return """Usage:
140
+ check-artifact-metadata-headers.sh --staged-added
141
+ check-artifact-metadata-headers.sh --paths <path> [path...]
142
+ check-artifact-metadata-headers.sh --all
143
+
144
+ Checks scripts, chat Markdown documents, harness Markdown documents, and harness
145
+ YAML artifacts for required agentic metadata headers. --staged-added enforces
146
+ only newly added files so existing files can be backfilled in batches.
147
+ """
148
+
149
+
150
+ def parse_args(argv: list[str]) -> argparse.Namespace:
151
+ parser = argparse.ArgumentParser(add_help=False)
152
+ parser.add_argument("--staged-added", action="store_true")
153
+ parser.add_argument("--all", action="store_true")
154
+ parser.add_argument("--paths", nargs="*")
155
+ parser.add_argument("-h", "--help", action="store_true")
156
+ args = parser.parse_args(argv)
157
+
158
+ if args.help:
159
+ print(usage(), end="")
160
+ sys.exit(0)
161
+
162
+ modes = [args.staged_added, args.all, args.paths is not None]
163
+ if sum(1 for mode in modes if mode) != 1:
164
+ print("ERROR: choose exactly one mode.", file=sys.stderr)
165
+ print(usage(), end="", file=sys.stderr)
166
+ sys.exit(2)
167
+ if args.paths == []:
168
+ print("ERROR: --paths requires at least one path.", file=sys.stderr)
169
+ sys.exit(2)
170
+ return args
171
+
172
+
173
+ def run_git(args: list[str]) -> str:
174
+ result = subprocess.run(["git", *args], check=True, text=True, stdout=subprocess.PIPE)
175
+ return result.stdout
176
+
177
+
178
+ def normalize_path(path: Path | str) -> str:
179
+ path_obj = Path(path)
180
+ if path_obj.is_absolute():
181
+ try:
182
+ return path_obj.resolve().relative_to(ROOT).as_posix()
183
+ except ValueError:
184
+ return path_obj.as_posix()
185
+ return path_obj.as_posix()
186
+
187
+
188
+ def collect_staged_added_paths() -> list[str]:
189
+ output = run_git(["diff", "--cached", "--name-status", "--diff-filter=ACR"])
190
+ paths = []
191
+ for line in output.splitlines():
192
+ parts = line.split("\t")
193
+ if not parts:
194
+ continue
195
+ status = parts[0]
196
+ if status.startswith("R") and len(parts) >= 3:
197
+ paths.append(parts[2])
198
+ elif len(parts) >= 2:
199
+ paths.append(parts[1])
200
+ return sorted(set(paths))
201
+
202
+
203
+ def collect_paths_from_args(paths: list[str]) -> list[str]:
204
+ collected: list[str] = []
205
+ for raw_path in paths:
206
+ path = Path(raw_path)
207
+ absolute = path if path.is_absolute() else ROOT / path
208
+ if absolute.is_dir():
209
+ collected.extend(normalize_path(child) for child in absolute.rglob("*") if child.is_file())
210
+ elif absolute.is_file():
211
+ collected.append(normalize_path(absolute))
212
+ else:
213
+ print(f"WARN: path does not exist, skipping: {raw_path}", file=sys.stderr)
214
+ return sorted(set(collected))
215
+
216
+
217
+ def collect_all_paths() -> list[str]:
218
+ roots = [
219
+ ROOT / "scripts",
220
+ ROOT / ".github/workflows",
221
+ ROOT / ".agentic",
222
+ ROOT / "docs/00.chat",
223
+ ROOT / "docs/02.rag-rulebook",
224
+ ROOT / "docs/harness",
225
+ ROOT / "infra",
226
+ ]
227
+ collected: list[str] = []
228
+ for root in roots:
229
+ if root.is_dir():
230
+ collected.extend(normalize_path(path) for path in root.rglob("*") if path.is_file())
231
+ return sorted(set(collected))
232
+
233
+
234
+ def is_script_artifact(path: str) -> bool:
235
+ return (
236
+ path.startswith("scripts/")
237
+ and path.endswith((".sh", ".js", ".mjs"))
238
+ ) or (
239
+ path.startswith(".agentic/")
240
+ and path.endswith((".js", ".mjs"))
241
+ )
242
+
243
+
244
+ def is_markdown_artifact(path: str) -> bool:
245
+ return path.endswith(".md") and (
246
+ path.startswith(".agentic/")
247
+ or path.startswith("docs/00.chat/")
248
+ or path.startswith("docs/02.rag-rulebook/")
249
+ or path.startswith("docs/aws/")
250
+ or path.startswith("docs/education/")
251
+ or path.startswith("docs/harness/")
252
+ or path.startswith("infra/")
253
+ or path.startswith("scripts/")
254
+ )
255
+
256
+
257
+ def is_yaml_artifact(path: str) -> bool:
258
+ return path.endswith((".yml", ".yaml")) and (
259
+ path.startswith(".agentic/")
260
+ or path.startswith(".github/workflows/")
261
+ or path.startswith("docs/02.rag-rulebook/")
262
+ or path.startswith("docs/harness/")
263
+ )
264
+
265
+
266
+ def is_relevant_path(path: str) -> bool:
267
+ return is_script_artifact(path) or is_markdown_artifact(path) or is_yaml_artifact(path)
268
+
269
+
270
+ def strip_comment(line: str) -> str:
271
+ stripped = line.lstrip()
272
+ if stripped.startswith("# "):
273
+ return stripped[2:]
274
+ if stripped.startswith("#"):
275
+ return stripped[1:]
276
+ if stripped.startswith("// "):
277
+ return stripped[3:]
278
+ if stripped.startswith("//"):
279
+ return stripped[2:]
280
+ return line
281
+
282
+
283
+ def parse_header(path: str) -> dict[str, Any]:
284
+ full_path = ROOT / path
285
+ lines = full_path.read_text(encoding="utf-8").splitlines()[:120]
286
+
287
+ for index, line in enumerate(lines):
288
+ if "agentic-artifact:" not in line and "agentic-script:" not in line:
289
+ continue
290
+
291
+ if line.lstrip().startswith("<!--"):
292
+ marker = line.replace("<!--", "", 1).strip()
293
+ body_lines = []
294
+ for following in lines[index + 1 :]:
295
+ if "-->" in following:
296
+ before_end = following.split("-->", 1)[0]
297
+ if before_end.strip():
298
+ body_lines.append(before_end)
299
+ break
300
+ body_lines.append(following)
301
+ header_lines = [marker]
302
+ header_lines.extend(f" {body_line}" if body_line.strip() else body_line for body_line in body_lines)
303
+ else:
304
+ header_lines = [strip_comment(line)]
305
+ for following in lines[index + 1 :]:
306
+ stripped = following.lstrip()
307
+ if stripped.startswith("#") or stripped.startswith("//"):
308
+ header_lines.append(strip_comment(following))
309
+ continue
310
+ if not following.strip():
311
+ break
312
+ break
313
+
314
+ try:
315
+ parsed = yaml.safe_load("\n".join(header_lines)) or {}
316
+ except yaml.YAMLError as exc:
317
+ raise CheckError(f"invalid metadata YAML: {path}: {exc}") from exc
318
+ if not isinstance(parsed, dict):
319
+ raise CheckError(f"invalid metadata header shape: {path}")
320
+ return parsed
321
+
322
+ return {}
323
+
324
+
325
+ def require_fields(path: str, metadata: dict[str, Any], fields: list[str], label: str) -> None:
326
+ for field in fields:
327
+ if field not in metadata or metadata[field] in (None, ""):
328
+ raise CheckError(f"missing {field} in {label} metadata header: {path}")
329
+
330
+
331
+ def validate_used_by_path(path: str, ref: str) -> None:
332
+ if not ref or not ref.startswith(PATH_PREFIXES):
333
+ return
334
+ if not (ROOT / ref).exists():
335
+ raise CheckError(f"{path} references missing used_by path: {ref}")
336
+
337
+
338
+ def validate_v1_used_by_paths(path: str, used_by: Any) -> None:
339
+ if not isinstance(used_by, list):
340
+ raise CheckError(f"used_by must be a list in metadata header: {path}")
341
+ for entry in used_by:
342
+ if isinstance(entry, str):
343
+ validate_used_by_path(path, entry)
344
+ elif isinstance(entry, dict) and isinstance(entry.get("path"), str):
345
+ validate_used_by_path(path, entry["path"])
346
+
347
+
348
+ def validate_v1_script(path: str, metadata: dict[str, Any]) -> None:
349
+ require_fields(path, metadata, ["owner", "purpose", "domain", "portability", "used_by", "effects"], "script")
350
+ if metadata["owner"] not in V1_OWNERS:
351
+ raise CheckError(f"invalid owner value in script metadata header: {path}")
352
+ if metadata["portability"] not in V1_PORTABILITY:
353
+ raise CheckError(f"invalid portability value in script metadata header: {path}")
354
+ validate_v1_used_by_paths(path, metadata["used_by"])
355
+
356
+
357
+ def validate_v1_artifact(path: str, metadata: dict[str, Any], yaml_artifact: bool) -> None:
358
+ label = "YAML artifact" if yaml_artifact else "artifact"
359
+ require_fields(path, metadata, ["owner", "kind", "purpose", "domain", "portability", "used_by"], label)
360
+ if metadata["owner"] not in V1_OWNERS:
361
+ raise CheckError(f"invalid owner value in {label} metadata header: {path}")
362
+ if metadata["portability"] not in V1_PORTABILITY:
363
+ raise CheckError(f"invalid portability value in {label} metadata header: {path}")
364
+ validate_v1_used_by_paths(path, metadata["used_by"])
365
+
366
+
367
+ def require_type(path: str, field: str, value: Any, expected_type: type, description: str) -> None:
368
+ if not isinstance(value, expected_type):
369
+ raise CheckError(f"{field} must be {description} in v2 metadata header: {path}")
370
+
371
+
372
+ def validate_id(path: str, field: str, value: str) -> None:
373
+ if not ID_RE.match(value):
374
+ raise CheckError(f"invalid {field} value in v2 metadata header: {path}")
375
+
376
+
377
+ def validate_v2(path: str, metadata: dict[str, Any]) -> None:
378
+ required = [
379
+ "schema",
380
+ "id",
381
+ "version",
382
+ "status",
383
+ "layer",
384
+ "domain",
385
+ "disciplines",
386
+ "kind",
387
+ "purpose",
388
+ "portability",
389
+ "used_by",
390
+ ]
391
+ require_fields(path, metadata, required, "v2 artifact")
392
+
393
+ if metadata["schema"] != "agentic-artifact/v2":
394
+ raise CheckError(f"invalid schema value in v2 metadata header: {path}")
395
+
396
+ require_type(path, "id", metadata["id"], str, "a string")
397
+ validate_id(path, "id", metadata["id"])
398
+
399
+ if not isinstance(metadata["version"], int) or metadata["version"] < 1:
400
+ raise CheckError(f"version must be an integer greater than zero in v2 metadata header: {path}")
401
+
402
+ if metadata["status"] not in STATUSES:
403
+ raise CheckError(f"invalid status value in v2 metadata header: {path}")
404
+ if metadata["layer"] not in LAYERS:
405
+ raise CheckError(f"invalid layer value in v2 metadata header: {path}")
406
+
407
+ require_type(path, "domain", metadata["domain"], str, "a string")
408
+ if not DOMAIN_RE.match(metadata["domain"]):
409
+ raise CheckError(f"invalid domain value in v2 metadata header: {path}")
410
+
411
+ disciplines = metadata["disciplines"]
412
+ if not isinstance(disciplines, list) or not disciplines:
413
+ raise CheckError(f"disciplines must be a non-empty list in v2 metadata header: {path}")
414
+ for discipline in disciplines:
415
+ if discipline not in DISCIPLINES:
416
+ raise CheckError(f"invalid discipline value in v2 metadata header: {path}: {discipline}")
417
+
418
+ require_type(path, "kind", metadata["kind"], str, "a string")
419
+ require_type(path, "purpose", metadata["purpose"], str, "a string")
420
+ if not metadata["kind"].strip() or not metadata["purpose"].strip():
421
+ raise CheckError(f"kind and purpose must be non-empty in v2 metadata header: {path}")
422
+
423
+ portability = metadata["portability"]
424
+ if not isinstance(portability, dict):
425
+ raise CheckError(f"portability must be an object in v2 metadata header: {path}")
426
+ if portability.get("class") not in PORTABILITY_CLASSES:
427
+ raise CheckError(f"invalid portability.class value in v2 metadata header: {path}")
428
+ targets = portability.get("targets")
429
+ if not isinstance(targets, list):
430
+ raise CheckError(f"portability.targets must be a list in v2 metadata header: {path}")
431
+ for target in targets:
432
+ if not isinstance(target, str) or not target:
433
+ raise CheckError(f"portability.targets entries must be strings in v2 metadata header: {path}")
434
+
435
+ used_by = metadata["used_by"]
436
+ if not isinstance(used_by, list) or not used_by:
437
+ raise CheckError(f"used_by must be a non-empty list in v2 metadata header: {path}")
438
+ for entry in used_by:
439
+ if not isinstance(entry, dict):
440
+ raise CheckError(f"used_by entries must be objects in v2 metadata header: {path}")
441
+ ref_id = entry.get("id")
442
+ if not isinstance(ref_id, str) or not ref_id:
443
+ raise CheckError(f"used_by.id is required in v2 metadata header: {path}")
444
+ validate_id(path, "used_by.id", ref_id)
445
+ ref_path = entry.get("path")
446
+ if ref_path is not None:
447
+ if not isinstance(ref_path, str):
448
+ raise CheckError(f"used_by.path must be a string in v2 metadata header: {path}")
449
+ validate_used_by_path(path, ref_path)
450
+
451
+ effects = metadata.get("effects")
452
+ if metadata["kind"] == "script" or is_script_artifact(path):
453
+ if metadata["kind"] != "script":
454
+ raise CheckError(f"script file must use kind: script in v2 metadata header: {path}")
455
+ if not isinstance(effects, list) or not effects:
456
+ raise CheckError(f"effects must be a non-empty list for v2 script metadata header: {path}")
457
+ if effects is not None:
458
+ if not isinstance(effects, list):
459
+ raise CheckError(f"effects must be a list in v2 metadata header: {path}")
460
+ for effect in effects:
461
+ if effect not in SCRIPT_EFFECTS:
462
+ raise CheckError(f"invalid effects value in v2 metadata header: {path}: {effect}")
463
+
464
+
465
+ def validate_path(path: str) -> None:
466
+ parsed = parse_header(path)
467
+ artifact = parsed.get("agentic-artifact")
468
+ script = parsed.get("agentic-script")
469
+
470
+ if is_script_artifact(path):
471
+ if isinstance(artifact, dict) and artifact.get("schema") == "agentic-artifact/v2":
472
+ validate_v2(path, artifact)
473
+ return
474
+ if isinstance(script, dict):
475
+ validate_v1_script(path, script)
476
+ return
477
+ raise CheckError(f"missing agentic-script or agentic-artifact/v2 metadata header: {path}")
478
+
479
+ if isinstance(artifact, dict):
480
+ if artifact.get("schema") == "agentic-artifact/v2":
481
+ validate_v2(path, artifact)
482
+ else:
483
+ validate_v1_artifact(path, artifact, is_yaml_artifact(path))
484
+ return
485
+
486
+ raise CheckError(f"missing agentic-artifact metadata header: {path}")
487
+
488
+
489
+ def main(argv: list[str]) -> int:
490
+ args = parse_args(argv)
491
+ if args.staged_added:
492
+ paths = collect_staged_added_paths()
493
+ elif args.all:
494
+ paths = collect_all_paths()
495
+ else:
496
+ paths = collect_paths_from_args(args.paths or [])
497
+
498
+ failures = 0
499
+ checked = 0
500
+ for path in paths:
501
+ if not path or not is_relevant_path(path):
502
+ continue
503
+ if not (ROOT / path).is_file():
504
+ continue
505
+ checked += 1
506
+ try:
507
+ validate_path(path)
508
+ except CheckError as exc:
509
+ print(f"ERROR: {exc}", file=sys.stderr)
510
+ failures += 1
511
+
512
+ if failures:
513
+ print(f"Artifact metadata header check failed: {failures} file(s).", file=sys.stderr)
514
+ return 1
515
+
516
+ print(f"Artifact metadata headers passed for {checked} file(s).")
517
+ return 0
518
+
519
+
520
+ if __name__ == "__main__":
521
+ sys.exit(main(sys.argv[1:]))
522
+ PY
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # agentic-artifact:
5
+ # schema: agentic-artifact/v2
6
+ # id: harness.script.artifact-metadata.check-headers-smoke-test
7
+ # version: 1
8
+ # status: active
9
+ # layer: 01.harness
10
+ # domain: metadata
11
+ # disciplines:
12
+ # - agentic
13
+ # kind: script
14
+ # purpose: Smoke test v1 and v2 artifact metadata header validation.
15
+ # portability:
16
+ # class: required
17
+ # targets:
18
+ # - llm-workbench
19
+ # - entity-builder
20
+ # - design-system-builder
21
+ # effects:
22
+ # - writes-files
23
+ # used_by:
24
+ # - id: harness.script.artifact-metadata.check-headers
25
+ # path: scripts/01.harness/artifact-metadata/check-headers/script.sh
26
+
27
+ repo_root="$(git rev-parse --show-toplevel)"
28
+ checker="$repo_root/scripts/01.harness/artifact-metadata/check-headers/script.sh"
29
+
30
+ bash "$checker" --paths \
31
+ .agentic/00.chat/workflows/chat-start.md \
32
+ .agentic/shared/standards/upstream-repo-bootstrap.md \
33
+ docs/00.chat/README.md \
34
+ scripts/00.chat/startup/start-chat-session/script.sh \
35
+ scripts/01.harness/artifact-metadata/check-headers/script.sh \
36
+ scripts/01.harness/artifact-metadata/check-headers/smoke-test.sh
37
+
38
+ tmp_dir="$repo_root/scripts/01.harness/artifact-metadata/check-headers/.tmp-check-headers-smoke-$$"
39
+ trap 'rm -rf "$tmp_dir"' EXIT
40
+ mkdir -p "$tmp_dir"
41
+ printf '#!/usr/bin/env bash\n' > "$tmp_dir/missing.sh"
42
+
43
+ if bash "$checker" --paths "$tmp_dir/missing.sh" >/dev/null 2>&1; then
44
+ echo "ERROR: missing-header fixture unexpectedly passed." >&2
45
+ exit 1
46
+ fi
47
+
48
+ echo "Artifact metadata header checker smoke test passed."