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.
- package/.agentic/00.chat/README.md +78 -0
- package/.agentic/00.chat/checklists/before-commit.md +195 -0
- package/.agentic/00.chat/checklists/llm-workbench-public-beta.md +94 -0
- package/.agentic/00.chat/commands/README.md +108 -0
- package/.agentic/00.chat/migration-plan.md +132 -0
- package/.agentic/00.chat/skills/session-summary.md +48 -0
- package/.agentic/00.chat/standards/llm-workbench-public-beta-contract.md +216 -0
- package/.agentic/00.chat/standards/main-refresh-conflict-types.md +358 -0
- package/.agentic/00.chat/workflows/README.md +40 -0
- package/.agentic/00.chat/workflows/bootstrap-chat-workbench-repo.md +212 -0
- package/.agentic/00.chat/workflows/chat-cleanup.md +102 -0
- package/.agentic/00.chat/workflows/chat-commit.md +56 -0
- package/.agentic/00.chat/workflows/chat-promote-to-main.md +169 -0
- package/.agentic/00.chat/workflows/chat-refresh-from-main.md +242 -0
- package/.agentic/00.chat/workflows/chat-reporting.md +69 -0
- package/.agentic/00.chat/workflows/chat-start.md +173 -0
- package/.agentic/00.chat/workflows/chat-upstream-reusable-lesson.md +123 -0
- package/.agentic/shared/standards/README.md +32 -0
- package/.agentic/shared/standards/upstream-repo-bootstrap.md +131 -0
- package/.agentic/shared/workflows/README.md +35 -0
- package/.agentic/shared/workflows/capability-resolution-workflow.md +189 -0
- package/.agentic/shared/workflows/change-shared-process.md +92 -0
- package/.cursor/rules/llm-workbench.mdc +17 -0
- package/.github/copilot-instructions.md +16 -0
- package/AGENTS.md +63 -0
- package/CLAUDE.md +16 -0
- package/CONTRIBUTING.md +57 -0
- package/LICENSE +21 -0
- package/LLM_WORKBENCH.md +17 -0
- package/README.md +98 -0
- package/SECURITY.md +44 -0
- package/bin/llm-workbench.js +672 -0
- package/docs/00.chat/README.md +47 -0
- package/docs/00.chat/llm-workbench-acceptance-matrix.md +55 -0
- package/docs/00.chat/script-layout.md +107 -0
- package/docs/adapting-to-your-repo.md +29 -0
- package/docs/concepts.md +38 -0
- package/docs/install.md +114 -0
- package/docs/public-beta-contract.md +45 -0
- package/docs/workflows.md +103 -0
- package/examples/minimal-repo/README.md +13 -0
- package/package.json +93 -0
- package/scripts/00.chat/README.md +46 -0
- package/scripts/00.chat/bootstrap/README.md +35 -0
- package/scripts/00.chat/bootstrap/audit-chat-bootstrap-file-set/README.md +39 -0
- package/scripts/00.chat/bootstrap/audit-chat-bootstrap-file-set/script.sh +213 -0
- package/scripts/00.chat/closeout/README.md +30 -0
- package/scripts/00.chat/closeout/build-closeout-prompt/README.md +35 -0
- package/scripts/00.chat/closeout/build-closeout-prompt/script.sh +124 -0
- package/scripts/00.chat/command/README.md +31 -0
- package/scripts/00.chat/command/close/README.md +30 -0
- package/scripts/00.chat/command/close/script.sh +25 -0
- package/scripts/00.chat/command/dispatcher/README.md +46 -0
- package/scripts/00.chat/command/dispatcher/script.sh +91 -0
- package/scripts/00.chat/command/dispatcher/smoke-test.sh +168 -0
- package/scripts/00.chat/command/new/README.md +32 -0
- package/scripts/00.chat/command/new/script.sh +28 -0
- package/scripts/00.chat/command/open-window/README.md +38 -0
- package/scripts/00.chat/command/open-window/script.sh +25 -0
- package/scripts/00.chat/command/package-scripts/README.md +34 -0
- package/scripts/00.chat/command/package-scripts/smoke-test.sh +113 -0
- package/scripts/00.chat/git/README.md +30 -0
- package/scripts/00.chat/git/cleanup-empty-chat-branches/README.md +36 -0
- package/scripts/00.chat/git/cleanup-empty-chat-branches/script.sh +243 -0
- package/scripts/00.chat/git/cleanup-empty-chat-branches/smoke-test.sh +136 -0
- package/scripts/00.chat/local-merge/README.md +30 -0
- package/scripts/00.chat/local-merge/list-active-chat-branches/README.md +29 -0
- package/scripts/00.chat/local-merge/list-active-chat-branches/script.sh +109 -0
- package/scripts/00.chat/local-merge/report-chat-branch-overlaps/README.md +29 -0
- package/scripts/00.chat/local-merge/report-chat-branch-overlaps/script.sh +142 -0
- package/scripts/00.chat/local-merge/verify-chat-ready-to-merge-local-main/README.md +33 -0
- package/scripts/00.chat/local-merge/verify-chat-ready-to-merge-local-main/script.sh +345 -0
- package/scripts/00.chat/local-merge/verify-chat-ready-to-merge-local-main/smoke-test.sh +244 -0
- package/scripts/00.chat/main-refresh/README.md +39 -0
- package/scripts/00.chat/main-refresh/apply-rehearsed-refresh/README.md +32 -0
- package/scripts/00.chat/main-refresh/apply-rehearsed-refresh/script.sh +198 -0
- package/scripts/00.chat/main-refresh/check-chat-is-current-with-main/README.md +30 -0
- package/scripts/00.chat/main-refresh/check-chat-is-current-with-main/script.sh +121 -0
- package/scripts/00.chat/main-refresh/classify-conflict/README.md +39 -0
- package/scripts/00.chat/main-refresh/classify-conflict/script.sh +169 -0
- package/scripts/00.chat/main-refresh/classify-conflict/smoke-test.sh +137 -0
- package/scripts/00.chat/main-refresh/classify-refresh-readiness/README.md +35 -0
- package/scripts/00.chat/main-refresh/classify-refresh-readiness/script.sh +171 -0
- package/scripts/00.chat/main-refresh/classify-refresh-readiness/smoke-test.sh +132 -0
- package/scripts/00.chat/main-refresh/rehearse-refresh-from-main/README.md +34 -0
- package/scripts/00.chat/main-refresh/rehearse-refresh-from-main/script.sh +124 -0
- package/scripts/00.chat/main-refresh/rehearse-refresh-from-main/smoke-test.sh +257 -0
- package/scripts/00.chat/main-refresh/show-main-update-status/README.md +31 -0
- package/scripts/00.chat/main-refresh/show-main-update-status/script.sh +73 -0
- package/scripts/00.chat/main-refresh/verify-conflict-audit/README.md +37 -0
- package/scripts/00.chat/main-refresh/verify-conflict-audit/script.sh +154 -0
- package/scripts/00.chat/main-refresh/verify-conflict-audit/smoke-test.sh +99 -0
- package/scripts/00.chat/metrics/README.md +35 -0
- package/scripts/00.chat/metrics/data/chat-pricing.json +107 -0
- package/scripts/00.chat/metrics/data/chat-pricing.schema.json +63 -0
- package/scripts/00.chat/metrics/estimate-chat-cost/README.md +40 -0
- package/scripts/00.chat/metrics/estimate-chat-cost/script.js +130 -0
- package/scripts/00.chat/migration/README.md +30 -0
- package/scripts/00.chat/migration/audit-chat-layer-migration/README.md +33 -0
- package/scripts/00.chat/migration/audit-chat-layer-migration/script.sh +127 -0
- package/scripts/00.chat/recovery/README.md +30 -0
- package/scripts/00.chat/recovery/import-active-paths-to-chat-worktree/README.md +76 -0
- package/scripts/00.chat/recovery/import-active-paths-to-chat-worktree/script.sh +212 -0
- package/scripts/00.chat/recovery/import-active-paths-to-chat-worktree/smoke-test.sh +162 -0
- package/scripts/00.chat/reporting/README.md +30 -0
- package/scripts/00.chat/reporting/generate-commit-log-summary/README.md +35 -0
- package/scripts/00.chat/reporting/generate-commit-log-summary/script.sh +299 -0
- package/scripts/00.chat/reporting/generate-commit-log-summary/smoke-test.sh +93 -0
- package/scripts/00.chat/reporting/report-chat-workspaces/README.md +32 -0
- package/scripts/00.chat/reporting/report-chat-workspaces/script.sh +82 -0
- package/scripts/00.chat/session-log/README.md +33 -0
- package/scripts/00.chat/session-log/check-commit-prerequisites/README.md +89 -0
- package/scripts/00.chat/session-log/check-commit-prerequisites/script.sh +121 -0
- package/scripts/00.chat/session-log/check-commit-prerequisites/smoke-test.sh +119 -0
- package/scripts/00.chat/session-log/check-commitlog-deletions/README.md +90 -0
- package/scripts/00.chat/session-log/check-commitlog-deletions/script.sh +131 -0
- package/scripts/00.chat/session-log/check-commitlog-deletions/smoke-test.sh +123 -0
- package/scripts/00.chat/session-log/checkpoint-chat-session-log/README.md +98 -0
- package/scripts/00.chat/session-log/checkpoint-chat-session-log/script.sh +126 -0
- package/scripts/00.chat/session-log/paths/README.md +38 -0
- package/scripts/00.chat/session-log/paths/lib.sh +133 -0
- package/scripts/00.chat/session-log/prepare-chat-session-before-commit/README.md +90 -0
- package/scripts/00.chat/session-log/prepare-chat-session-before-commit/script.sh +145 -0
- package/scripts/00.chat/session-log/read-current-chat-log/README.md +44 -0
- package/scripts/00.chat/session-log/read-current-chat-log/script.sh +92 -0
- package/scripts/00.chat/session-log/read-current-chat-log/smoke-test.sh +127 -0
- package/scripts/00.chat/session-log/record-chat-commit/README.md +133 -0
- package/scripts/00.chat/session-log/record-chat-commit/script.sh +394 -0
- package/scripts/00.chat/session-log/record-chat-commit/smoke-test.sh +227 -0
- package/scripts/00.chat/session-log/record-main-refresh-conflict/README.md +34 -0
- package/scripts/00.chat/session-log/record-main-refresh-conflict/script.sh +239 -0
- package/scripts/00.chat/session-log/rename-current-chat-log-folder/README.md +32 -0
- package/scripts/00.chat/session-log/rename-current-chat-log-folder/script.sh +112 -0
- package/scripts/00.chat/session-log/update-chat-log/README.md +32 -0
- package/scripts/00.chat/session-log/update-chat-log/script.sh +294 -0
- package/scripts/00.chat/startup/README.md +37 -0
- package/scripts/00.chat/startup/auto-start-missing-session/README.md +113 -0
- package/scripts/00.chat/startup/auto-start-missing-session/script.sh +54 -0
- package/scripts/00.chat/startup/resolve-current-chat-session/README.md +57 -0
- package/scripts/00.chat/startup/resolve-current-chat-session/script.sh +47 -0
- package/scripts/00.chat/startup/resolve-current-chat-session/smoke-test.sh +130 -0
- package/scripts/00.chat/startup/start-chat-session/README.md +197 -0
- package/scripts/00.chat/startup/start-chat-session/script.sh +330 -0
- package/scripts/00.chat/startup/start-chat-session/smoke-test.sh +182 -0
- package/scripts/00.chat/startup/start-new-chat/README.md +31 -0
- package/scripts/00.chat/startup/start-new-chat/script.sh +29 -0
- package/scripts/00.chat/transcript/README.md +36 -0
- package/scripts/00.chat/transcript/discover-codex-session-log/README.md +32 -0
- package/scripts/00.chat/transcript/discover-codex-session-log/script.sh +106 -0
- package/scripts/00.chat/transcript/register-codex-session-log/README.md +32 -0
- package/scripts/00.chat/transcript/register-codex-session-log/script.sh +115 -0
- package/scripts/00.chat/worktree/README.md +32 -0
- package/scripts/00.chat/worktree/check-write-location/README.md +87 -0
- package/scripts/00.chat/worktree/check-write-location/script.sh +95 -0
- package/scripts/00.chat/worktree/dirty-worktree-check/README.md +77 -0
- package/scripts/00.chat/worktree/dirty-worktree-check/script.sh +93 -0
- package/scripts/00.chat/worktree/ensure-chat-worktree/README.md +33 -0
- package/scripts/00.chat/worktree/ensure-chat-worktree/script.sh +132 -0
- package/scripts/00.chat/worktree/open-window/README.md +34 -0
- package/scripts/00.chat/worktree/open-window/script.sh +131 -0
- package/scripts/00.chat/worktree/paths/README.md +32 -0
- package/scripts/00.chat/worktree/paths/lib.sh +71 -0
- package/scripts/01.harness/artifact-metadata/check-headers/script.sh +522 -0
- package/scripts/01.harness/artifact-metadata/check-headers/smoke-test.sh +48 -0
- package/scripts/01.harness/check-deterministic-process-drift.sh +416 -0
- package/scripts/01.harness/check-governed-script-command-drift.sh +184 -0
- package/scripts/01.harness/run-governed-script.sh +178 -0
- package/scripts/install.sh +503 -0
- package/scripts/uninstall.sh +199 -0
- 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."
|