@ikon85/agent-workflow-kit 0.10.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 (273) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +139 -0
  2. package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  3. package/.agents/skills/audit-skills/SKILL.md +145 -0
  4. package/.agents/skills/board-to-waves/SKILL.md +135 -0
  5. package/.agents/skills/code-review/SKILL.md +64 -0
  6. package/.agents/skills/codebase-design/DEEPENING.md +37 -0
  7. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  8. package/.agents/skills/codebase-design/SKILL.md +114 -0
  9. package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  10. package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
  11. package/.agents/skills/decision-gate/SKILL.md +67 -0
  12. package/.agents/skills/diagnose/SKILL.md +136 -0
  13. package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  14. package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  15. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  16. package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  17. package/.agents/skills/domain-modeling/SKILL.md +76 -0
  18. package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  19. package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
  20. package/.agents/skills/grill-me/SKILL.md +54 -0
  21. package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  22. package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  23. package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  24. package/.agents/skills/grill-with-docs/SKILL.md +137 -0
  25. package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  26. package/.agents/skills/implement/SKILL.md +15 -0
  27. package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  28. package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  29. package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  30. package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  31. package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  32. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  33. package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  34. package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
  35. package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  36. package/.agents/skills/kit-release/SKILL.md +56 -0
  37. package/.agents/skills/local-ci/SKILL.md +94 -0
  38. package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
  39. package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
  40. package/.agents/skills/prototype/LOGIC.md +79 -0
  41. package/.agents/skills/prototype/SKILL.md +30 -0
  42. package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  43. package/.agents/skills/prototype/UI.md +112 -0
  44. package/.agents/skills/research/SKILL.md +12 -0
  45. package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  46. package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
  47. package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  48. package/.agents/skills/retro/SKILL.md +209 -0
  49. package/.agents/skills/scale-check/SKILL.md +141 -0
  50. package/.agents/skills/security-audit/SKILL.md +67 -0
  51. package/.agents/skills/setup-workflow/SKILL.md +187 -0
  52. package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  53. package/.agents/skills/setup-workflow/board-sync.md +111 -0
  54. package/.agents/skills/setup-workflow/code-review.md +26 -0
  55. package/.agents/skills/setup-workflow/domain.md +51 -0
  56. package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
  57. package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  58. package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
  59. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  60. package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
  61. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  62. package/.agents/skills/setup-workflow/triage-labels.md +18 -0
  63. package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
  64. package/.agents/skills/spec-self-critique/SKILL.md +152 -0
  65. package/.agents/skills/spec-self-critique/scenarios.md +82 -0
  66. package/.agents/skills/tdd/SKILL.md +120 -0
  67. package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  68. package/.agents/skills/tdd/deep-modules.md +33 -0
  69. package/.agents/skills/tdd/interface-design.md +31 -0
  70. package/.agents/skills/tdd/mocking.md +59 -0
  71. package/.agents/skills/tdd/refactoring.md +10 -0
  72. package/.agents/skills/tdd/tests.md +61 -0
  73. package/.agents/skills/to-issues/SKILL.md +325 -0
  74. package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  75. package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  76. package/.agents/skills/to-prd/SKILL.md +180 -0
  77. package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  78. package/.agents/skills/to-waves/SKILL.md +260 -0
  79. package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  80. package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
  81. package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
  82. package/.agents/skills/triage/SKILL.md +122 -0
  83. package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  84. package/.agents/skills/verify-spike/SKILL.md +39 -0
  85. package/.agents/skills/wayfinder/SKILL.md +127 -0
  86. package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  87. package/.agents/skills/wrapup/SKILL.md +71 -0
  88. package/.agents/skills/wrapup/SOURCES.txt +6 -0
  89. package/.agents/skills/zoom-out/SKILL.md +7 -0
  90. package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  91. package/.claude/hooks/_hook_utils.py +184 -0
  92. package/.claude/hooks/drift-guard.py +176 -0
  93. package/.claude/hooks/skill-drift-hint.py +122 -0
  94. package/.claude/hooks/sync-board-status.py +191 -0
  95. package/.claude/skills/ask-matt/SKILL.md +139 -0
  96. package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  97. package/.claude/skills/audit-skills/SKILL.md +145 -0
  98. package/.claude/skills/board-to-waves/SKILL.md +148 -0
  99. package/.claude/skills/code-review/SKILL.md +64 -0
  100. package/.claude/skills/codebase-design/DEEPENING.md +37 -0
  101. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  102. package/.claude/skills/codebase-design/SKILL.md +114 -0
  103. package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  104. package/.claude/skills/codex-build/SKILL.md +126 -0
  105. package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
  106. package/.claude/skills/codex-review/SKILL.md +145 -0
  107. package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
  108. package/.claude/skills/decision-gate/SKILL.md +67 -0
  109. package/.claude/skills/diagnose/SKILL.md +136 -0
  110. package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  111. package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  112. package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
  113. package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  114. package/.claude/skills/domain-modeling/SKILL.md +76 -0
  115. package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  116. package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
  117. package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
  118. package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
  119. package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
  120. package/.claude/skills/grill-me/SKILL.md +54 -0
  121. package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  122. package/.claude/skills/grill-me-codex/SKILL.md +158 -0
  123. package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
  124. package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  125. package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  126. package/.claude/skills/grill-with-docs/SKILL.md +137 -0
  127. package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  128. package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
  129. package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
  130. package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
  131. package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
  132. package/.claude/skills/implement/SKILL.md +15 -0
  133. package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  134. package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  135. package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  136. package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  137. package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  138. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  139. package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  140. package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
  141. package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  142. package/.claude/skills/kit-release/SKILL.md +56 -0
  143. package/.claude/skills/local-ci/SKILL.md +94 -0
  144. package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
  145. package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
  146. package/.claude/skills/prototype/LOGIC.md +79 -0
  147. package/.claude/skills/prototype/SKILL.md +30 -0
  148. package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  149. package/.claude/skills/prototype/UI.md +112 -0
  150. package/.claude/skills/research/SKILL.md +12 -0
  151. package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  152. package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
  153. package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  154. package/.claude/skills/retro/SKILL.md +209 -0
  155. package/.claude/skills/scale-check/SKILL.md +141 -0
  156. package/.claude/skills/security-audit/SKILL.md +67 -0
  157. package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
  158. package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
  159. package/.claude/skills/setup-workflow/SKILL.md +187 -0
  160. package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  161. package/.claude/skills/setup-workflow/board-sync.md +111 -0
  162. package/.claude/skills/setup-workflow/code-review.md +26 -0
  163. package/.claude/skills/setup-workflow/domain.md +51 -0
  164. package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
  165. package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  166. package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
  167. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  168. package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
  169. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  170. package/.claude/skills/setup-workflow/triage-labels.md +18 -0
  171. package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
  172. package/.claude/skills/skill-manifest.json +389 -0
  173. package/.claude/skills/spec-self-critique/SKILL.md +152 -0
  174. package/.claude/skills/spec-self-critique/scenarios.md +82 -0
  175. package/.claude/skills/tdd/SKILL.md +120 -0
  176. package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  177. package/.claude/skills/tdd/deep-modules.md +33 -0
  178. package/.claude/skills/tdd/interface-design.md +31 -0
  179. package/.claude/skills/tdd/mocking.md +59 -0
  180. package/.claude/skills/tdd/refactoring.md +10 -0
  181. package/.claude/skills/tdd/tests.md +61 -0
  182. package/.claude/skills/to-issues/SKILL.md +325 -0
  183. package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  184. package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  185. package/.claude/skills/to-prd/SKILL.md +180 -0
  186. package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  187. package/.claude/skills/to-waves/SKILL.md +260 -0
  188. package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  189. package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
  190. package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
  191. package/.claude/skills/triage/SKILL.md +122 -0
  192. package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  193. package/.claude/skills/verify-spike/SKILL.md +39 -0
  194. package/.claude/skills/wayfinder/SKILL.md +127 -0
  195. package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  196. package/.claude/skills/wrapup/SKILL.md +81 -0
  197. package/.claude/skills/wrapup/SOURCES.txt +6 -0
  198. package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
  199. package/.claude/skills/write-a-skill/SKILL.md +88 -0
  200. package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
  201. package/.claude/skills/zoom-out/SKILL.md +7 -0
  202. package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  203. package/LICENSE +28 -0
  204. package/PROVENANCE.md +39 -0
  205. package/README.md +619 -0
  206. package/agent-workflow-kit.package.json +1939 -0
  207. package/docs/agents/board-sync.md +71 -0
  208. package/docs/agents/code-review.md +14 -0
  209. package/docs/agents/domain.md +7 -0
  210. package/docs/agents/issue-tracker.md +12 -0
  211. package/docs/agents/security-audit-runbook-template.md +205 -0
  212. package/docs/agents/skills/orchestrate-wave.md +13 -0
  213. package/docs/agents/skills/spec-self-critique.md +18 -0
  214. package/docs/agents/triage-labels.md +10 -0
  215. package/docs/agents/wave-anchor-template.md +88 -0
  216. package/docs/conventions/spec-completeness.md +23 -0
  217. package/docs/index.html +56 -0
  218. package/docs/methodology.html +488 -0
  219. package/docs/methodology.svg +93 -0
  220. package/docs/workflow.png +0 -0
  221. package/package.json +40 -0
  222. package/scripts/anchor_table.py +343 -0
  223. package/scripts/board-sync.py +1232 -0
  224. package/scripts/board_config.py +147 -0
  225. package/scripts/board_fields.py +192 -0
  226. package/scripts/build-kit.mjs +116 -0
  227. package/scripts/build-kit.test.mjs +59 -0
  228. package/scripts/check-kit-staleness.mjs +45 -0
  229. package/scripts/check-kit-staleness.test.mjs +19 -0
  230. package/scripts/execute-ready-check.py +516 -0
  231. package/scripts/issue_deps.py +79 -0
  232. package/scripts/kit-release.mjs +100 -0
  233. package/scripts/kit-release.test.mjs +67 -0
  234. package/scripts/lib/audit-refs.mjs +146 -0
  235. package/scripts/lib/scrub.mjs +113 -0
  236. package/scripts/lib/scrub.test.mjs +187 -0
  237. package/scripts/loc_offender_core.py +196 -0
  238. package/scripts/loc_offender_gate.py +182 -0
  239. package/scripts/node_kind.py +68 -0
  240. package/scripts/portability_profile_scan.py +53 -0
  241. package/scripts/pr-body-check.py +263 -0
  242. package/scripts/program_graph.py +125 -0
  243. package/scripts/program_graph_parse.py +275 -0
  244. package/scripts/program_graph_validate.py +256 -0
  245. package/scripts/program_sync.py +160 -0
  246. package/scripts/release-delta-guard.mjs +96 -0
  247. package/scripts/release-delta-guard.test.mjs +53 -0
  248. package/scripts/test_board_sync_create_idempotency.py +104 -0
  249. package/scripts/test_dist_kit_smoke.py +199 -0
  250. package/scripts/test_kit_docs_language_census.py +178 -0
  251. package/scripts/test_skill_code_review_seed.py +89 -0
  252. package/scripts/test_skill_frontmatter_lint.py +226 -0
  253. package/scripts/test_skill_gh_lint.py +124 -0
  254. package/scripts/test_skill_language_census.py +254 -0
  255. package/scripts/test_skill_portability_lint.py +773 -0
  256. package/scripts/test_skill_publish_audit.py +300 -0
  257. package/scripts/test_skill_selfcontainment_lint.py +255 -0
  258. package/scripts/test_skill_setup_workflow_seeds.py +200 -0
  259. package/scripts/test_skill_stale_name_lint.py +126 -0
  260. package/scripts/test_skill_surface_refs.py +300 -0
  261. package/scripts/test_skill_trailing_artifact_lint.py +71 -0
  262. package/scripts/wrapup-land.py +573 -0
  263. package/src/cli.mjs +70 -0
  264. package/src/commands/diff.mjs +6 -0
  265. package/src/commands/init.mjs +59 -0
  266. package/src/commands/uninstall.mjs +43 -0
  267. package/src/commands/update.mjs +95 -0
  268. package/src/lib/atomicWrite.mjs +49 -0
  269. package/src/lib/bundle.mjs +133 -0
  270. package/src/lib/hash.mjs +13 -0
  271. package/src/lib/manifest.mjs +58 -0
  272. package/src/lib/sentinel.mjs +20 -0
  273. package/src/lib/settings.mjs +23 -0
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """board_config.py — shared loader for the board-sync profile.
3
+
4
+ The planning scripts (board-sync.py / execute-ready-check.py / pr-body-check.py)
5
+ carry NO inline board IDs. They read every board-specific value — field IDs,
6
+ status option names, labels, branch prefixes, PR-body markers, headings — from a
7
+ single machine-readable profile embedded in `docs/agents/board-sync.md`:
8
+
9
+ <!-- board-sync:profile -->
10
+ ```json
11
+ { "repo": "...", "project": {...}, "fields": {...}, ... }
12
+ ```
13
+
14
+ A fenced ```json block is the SSOT (board discovery via
15
+ `gh project field-list --format json` already emits JSON, so a fill is a paste,
16
+ and `json` is stdlib). The human field-catalog table in the doc is documentation
17
+ only; the IDs live solely in this block, so the two can't drift.
18
+
19
+ `/setup-workflow` seeds + fills this block per consumer. Override the path with
20
+ the `BOARD_SYNC_PROFILE` env var (tests / non-default layouts).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import re
27
+ from pathlib import Path
28
+
29
+ # Default: <repo-root>/docs/agents/board-sync.md, resolved relative to THIS file
30
+ # (scripts/board_config.py → parent.parent == repo root) so it works regardless
31
+ # of the caller's cwd, in this repo and in a consumer install alike.
32
+ _DEFAULT_PROFILE = Path(__file__).resolve().parent.parent / "docs" / "agents" / "board-sync.md"
33
+
34
+ _MARKER_RE = re.compile(r"<!--\s*board-sync:profile\s*-->")
35
+ _FENCE_RE = re.compile(r"```json\s*\n(.*?)\n```", re.DOTALL)
36
+
37
+ # Required dotted paths — every value a planning script dereferences. A consumer
38
+ # who mis-fills the profile gets a clear "missing key X" at startup, not a deep
39
+ # KeyError mid-command.
40
+ _REQUIRED_PATHS = (
41
+ ("repo",),
42
+ ("project", "number"), ("project", "owner"), ("project", "nodeId"),
43
+ ("fields", "status", "id"), ("fields", "status", "options"),
44
+ ("fields", "wave"), ("fields", "cluster"),
45
+ ("fields", "specPath"), ("fields", "planPath"),
46
+ ("labels", "readyForAgent"), ("labels", "typePrefix"), ("labels", "clusterType"),
47
+ ("labels", "waveStub"),
48
+ ("branchPrefixes",),
49
+ ("prMarkers", "partOf"), ("prMarkers", "retroMarker"), ("prMarkers", "retroValues"),
50
+ ("headings", "vorBau"),
51
+ )
52
+
53
+
54
+ class ConfigError(RuntimeError):
55
+ """Raised when the board profile is absent, malformed, or incomplete."""
56
+
57
+
58
+ def _profile_path(path=None) -> Path:
59
+ if path is not None:
60
+ return Path(path)
61
+ env = os.environ.get("BOARD_SYNC_PROFILE")
62
+ return Path(env) if env else _DEFAULT_PROFILE
63
+
64
+
65
+ def _extract_block(text: str, src: str) -> str:
66
+ marker = _MARKER_RE.search(text)
67
+ if not marker:
68
+ raise ConfigError(
69
+ f"{src}: no `<!-- board-sync:profile -->` marker found — "
70
+ "run /setup-workflow to seed the board profile.")
71
+ fence = _FENCE_RE.search(text, marker.end())
72
+ if not fence:
73
+ raise ConfigError(
74
+ f"{src}: the `board-sync:profile` marker is not followed by a "
75
+ "```json block.")
76
+ return fence.group(1)
77
+
78
+
79
+ def _check_required(cfg, src: str) -> None:
80
+ for path in _REQUIRED_PATHS:
81
+ node = cfg
82
+ for key in path:
83
+ if not isinstance(node, dict) or key not in node:
84
+ raise ConfigError(
85
+ f"{src}: profile is missing required key "
86
+ f"'{'.'.join(path)}'.")
87
+ node = node[key]
88
+
89
+
90
+ def load_board_config(path=None) -> dict:
91
+ """Parse + validate the board profile. Raises ConfigError on any problem."""
92
+ p = _profile_path(path)
93
+ if not p.exists():
94
+ raise ConfigError(
95
+ f"board profile not found at {p} — run /setup-workflow to seed "
96
+ "docs/agents/board-sync.md.")
97
+ raw = _extract_block(p.read_text(encoding="utf-8"), str(p))
98
+ try:
99
+ cfg = json.loads(raw)
100
+ except json.JSONDecodeError as exc:
101
+ raise ConfigError(
102
+ f"{p}: the board-sync:profile block is not valid JSON: {exc}") from exc
103
+ if not isinstance(cfg, dict):
104
+ raise ConfigError(f"{p}: the board-sync:profile block must be a JSON object.")
105
+ _check_required(cfg, str(p))
106
+ return cfg
107
+
108
+
109
+ # --- optional Programm-Flughöhe keys ------------------------------------------
110
+ # `fields.phase` and `labels.programType` are deliberately NOT in _REQUIRED_PATHS
111
+ # — Phasen are optional per the program-altitude design (§2) and a Program-PRD's
112
+ # type label has a literal default, so an existing consumer profile without
113
+ # either key keeps loading unchanged (no migration forced by this slice).
114
+ def phase_field_id(cfg: dict):
115
+ """The `fields.phase.id` (a Projects-v2 single-select field id), or None when
116
+ the profile hasn't configured a Phase field yet. Callers must degrade to a
117
+ visible skip/hint on None, never a KeyError (mirrors `validate-graph`'s
118
+ existing defensive read of this same optional key)."""
119
+ return (cfg.get("fields", {}).get("phase") or {}).get("id")
120
+
121
+
122
+ def program_type_label(cfg: dict) -> str:
123
+ """The `labels.programType` label, or the literal default `"type:program"`
124
+ when the profile hasn't set one — a Program-PRD's board-filterable type
125
+ label (the `<!-- prd: program -->` body marker is not filterable)."""
126
+ return cfg.get("labels", {}).get("programType", "type:program")
127
+
128
+
129
+ # The semantic role keys of the workflow — schema, not
130
+ # vocabulary; stable once shipped, extend additively, never rename. Single
131
+ # home so CLI `choices`, docs, and future consumers reference one list.
132
+ STATUS_ROLE_KEYS = ("idea", "triaged", "spec", "inProgress", "review", "done")
133
+
134
+
135
+ def status_roles(cfg: dict) -> dict:
136
+ """The `fields.status.roles` map: semantic role keys (`idea`,
137
+ `triaged`, `spec`, `inProgress`, `review`, `done`) → this board's own
138
+ status option NAMES. This is where the workflow's status vocabulary lives —
139
+ scripts read roles, never option-name literals, so any board language works.
140
+
141
+ Optional (not in _REQUIRED_PATHS, like `fields.phase`): a profile without
142
+ the key returns an EMPTY DICT — deliberately no language default in code
143
+ (a German default would leak vocabulary into consumer installs, an English
144
+ one would be silently wrong for German boards). Callers degrade visibly:
145
+ passive paths (SessionStart hook, status-token sync) log a skip/hint;
146
+ explicit `--status-role` CLI calls fail loud with a migration snippet."""
147
+ return (cfg.get("fields", {}).get("status") or {}).get("roles") or {}
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ """board_fields.py — batched field-writes + a field-value read for Projects-v2
3
+ items.
4
+
5
+ Three mechanics `board-sync.py` lacked before this slice:
6
+
7
+ - `stamp-batch` — alias-batched `updateProjectV2ItemFieldValue` mutations
8
+ (~30 aliases/request, chunked) instead of one `gh` call per field per
9
+ item. `to-waves` stamps Wave+Phase on 50+ items at publish time;
10
+ the existing sequential `stamp_arg_list` path (still the create/add/promote
11
+ fallback for a single item) does not scale to that.
12
+ - `field-value` — read a project item's current field value for an issue.
13
+ Nothing in board-sync.py read a field back before this slice; `promote`'s
14
+ Mismatch-Guard is the first consumer.
15
+ - the `promote` guards themselves (pure decision functions) — Wave-Mismatch
16
+ and Program-PRD-Refusal — kept here alongside field-value since a guard IS
17
+ the read's only consumer today.
18
+
19
+ PURE — no gh / no I/O. The `_gh`/`_gh_json` seam calls and CLI wiring live in
20
+ board-sync.py (mirrors anchor_table.py's / program_graph.py's split).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from dataclasses import dataclass
26
+ from typing import Optional
27
+
28
+ # GraphQL mutations tolerate far more than this per request (GitHub's Secondary
29
+ # Rate Limit budgets ~5 "points"), but a hard chunk cap keeps one failed/timed-out
30
+ # request's blast radius small and the per-alias error report readable.
31
+ CHUNK_SIZE = 30
32
+
33
+
34
+ @dataclass
35
+ class FieldStamp:
36
+ """One (item, field) write for the batched mutation — also the unit the
37
+ per-alias error report and the repair command are built from."""
38
+ item_id: str
39
+ field_id: str
40
+ kind: str # "number" | "single_select"
41
+ value: object # the GraphQL literal value: int, or an option-id str
42
+ issue: int # for the report + repair command
43
+ field_name: str # "wave" | "phase" — doubles as the repair CLI flag name
44
+ display_value: object # human-readable value for the repair command
45
+ # (e.g. the Phase NAME, not its resolved option-id)
46
+
47
+
48
+ def build_stamps(items: list[dict], *, wave_field_id: str,
49
+ phase_cfg: Optional[dict]) -> tuple[list[FieldStamp], int]:
50
+ """FieldStamp list + a skipped-phase-stamp count from raw item dicts
51
+ (`{issue, item_id, wave?, phase?}`). A requested `phase` stamp when
52
+ `phase_cfg` is None (profile lacks `fields.phase`) is SKIPPED and counted —
53
+ never silently dropped without a trace (AC2)."""
54
+ phase_field_id = (phase_cfg or {}).get("id")
55
+ phase_options = (phase_cfg or {}).get("options", {})
56
+ stamps: list[FieldStamp] = []
57
+ skipped_phase = 0
58
+ for it in items:
59
+ issue = it["issue"]
60
+ item_id = it["item_id"]
61
+ wave = it.get("wave")
62
+ phase = it.get("phase")
63
+ if wave is not None:
64
+ stamps.append(FieldStamp(item_id, wave_field_id, "number", wave,
65
+ issue, "wave", wave))
66
+ if phase is not None:
67
+ if phase_field_id is None:
68
+ skipped_phase += 1
69
+ continue
70
+ if phase not in phase_options:
71
+ raise ValueError(
72
+ f"unknown phase {phase!r}; valid: {', '.join(phase_options) or '(none configured)'}")
73
+ stamps.append(FieldStamp(item_id, phase_field_id, "single_select",
74
+ phase_options[phase], issue, "phase", phase))
75
+ return stamps, skipped_phase
76
+
77
+
78
+ def chunk_stamps(stamps: list[FieldStamp], size: int = CHUNK_SIZE) -> list[list[FieldStamp]]:
79
+ """`stamps` split into ≤`size`-alias chunks, in order — one `gh api graphql`
80
+ request per chunk."""
81
+ return [stamps[i:i + size] for i in range(0, len(stamps), size)]
82
+
83
+
84
+ def _value_fragment(kind: str, value) -> str:
85
+ if kind == "number":
86
+ return f"number:{int(value)}"
87
+ if kind == "single_select":
88
+ return f"singleSelectOptionId:{json.dumps(str(value))}"
89
+ raise ValueError(f"unsupported field kind {kind!r}")
90
+
91
+
92
+ def build_stamp_mutation(stamps: list[FieldStamp], project_id: str) -> tuple[str, dict[str, FieldStamp]]:
93
+ """One aliased `updateProjectV2ItemFieldValue` mutation body for ≤CHUNK_SIZE
94
+ stamps (GraphQL alias-batching — one `gh api graphql` request instead of N).
95
+ Returns (mutation string, {alias: FieldStamp}) so a caller can map a
96
+ GraphQL error's `path` back to the failing (issue, field)."""
97
+ pid = json.dumps(project_id)
98
+ parts = []
99
+ alias_map: dict[str, FieldStamp] = {}
100
+ for i, s in enumerate(stamps):
101
+ alias = f"s{i}"
102
+ alias_map[alias] = s
103
+ parts.append(
104
+ f"{alias}: updateProjectV2ItemFieldValue(input:{{"
105
+ f"projectId:{pid}, itemId:{json.dumps(s.item_id)}, "
106
+ f"fieldId:{json.dumps(s.field_id)}, "
107
+ f"value:{{{_value_fragment(s.kind, s.value)}}}}}) "
108
+ f"{{ projectV2Item {{ id }} }}"
109
+ )
110
+ return "mutation{" + " ".join(parts) + "}", alias_map
111
+
112
+
113
+ def parse_batch_response(response: dict, alias_map: dict[str, FieldStamp]
114
+ ) -> tuple[list[FieldStamp], list[tuple[FieldStamp, str]]]:
115
+ """(succeeded, failed) for one chunk, read off the GraphQL response's
116
+ `data`/`errors` — the counted report's ONLY source (deliberately no extra
117
+ post-write read-back pass AC). A GraphQL partial-failure response
118
+ carries BOTH `data` (for the aliases that succeeded) and `errors[].path`
119
+ (the alias of each one that didn't); an alias with neither an error nor
120
+ data is counted as failed defensively, never silently dropped."""
121
+ errors_by_alias: dict[str, str] = {}
122
+ for err in (response.get("errors") or []):
123
+ path = err.get("path") or []
124
+ if path and path[0] in alias_map:
125
+ errors_by_alias[path[0]] = err.get("message", "unknown error")
126
+ data = response.get("data") or {}
127
+ succeeded: list[FieldStamp] = []
128
+ failed: list[tuple[FieldStamp, str]] = []
129
+ for alias, stamp in alias_map.items():
130
+ if alias in errors_by_alias:
131
+ failed.append((stamp, errors_by_alias[alias]))
132
+ elif data.get(alias):
133
+ succeeded.append(stamp)
134
+ else:
135
+ failed.append((stamp, "no data returned for this alias"))
136
+ return succeeded, failed
137
+
138
+
139
+ def repair_command(stamp: FieldStamp) -> str:
140
+ """Idempotent single-item retry for one failed alias — a GraphQL field-set
141
+ is idempotent, so re-running just this stamp is always safe, no matter how
142
+ many times."""
143
+ return (f"python3 scripts/board-sync.py stamp-batch --issue {stamp.issue} "
144
+ f"--item-id {stamp.item_id} --{stamp.field_name} {stamp.display_value}")
145
+
146
+
147
+ # --- field-value: read a project item's current field value ------------------
148
+ def extract_field_value(data: dict, project_id: str, field_id: str) -> Optional[dict]:
149
+ """The current value of `field_id` on the issue's item in `project_id`
150
+ (`{"number": n}` / `{"name": ..., "optionId": ...}` / `{"text": ...}`), or
151
+ None if the issue isn't on that project / the field was never set. Matches
152
+ by field id (not name) — the profile only carries opaque field ids, never
153
+ the human-facing field NAME, so a name-keyed GraphQL lookup would need a
154
+ new required key; this stays ID-driven like the rest of the profile."""
155
+ issue = (data.get("data") or {}).get("repository", {}).get("issue")
156
+ items = ((issue or {}).get("projectItems") or {}).get("nodes") or []
157
+ for item in items:
158
+ if (item.get("project") or {}).get("id") != project_id:
159
+ continue
160
+ for fv in ((item.get("fieldValues") or {}).get("nodes") or []):
161
+ if (fv.get("field") or {}).get("id") != field_id:
162
+ continue
163
+ if "number" in fv:
164
+ return {"number": fv["number"]}
165
+ if "optionId" in fv:
166
+ return {"name": fv.get("name"), "optionId": fv["optionId"]}
167
+ if "text" in fv:
168
+ return {"text": fv["text"]}
169
+ return None
170
+
171
+
172
+ # --- promote guards (pure decisions; board-sync.py wires the reads) ----------
173
+ def wave_mismatch_guard(current_wave: Optional[int], target_wave: int) -> Optional[str]:
174
+ """None when `promote` may proceed (Wave field empty, or already == target —
175
+ an idempotent re-promote); an abort message otherwise (the field carries a
176
+ DIFFERENT wave — never silently overwritten AC3)."""
177
+ if current_wave is None or current_wave == target_wave:
178
+ return None
179
+ return (f"stub already carries Wave={current_wave} — refusing to overwrite with "
180
+ f"--wave {target_wave} (mismatch; re-check which wave this stub belongs "
181
+ "to, or pass the already-stamped wave to proceed idempotently)")
182
+
183
+
184
+ def program_prd_refusal(is_program: bool, issue: int) -> Optional[str]:
185
+ """None when `promote` may proceed; a refusal message when the target is a
186
+ Program-PRD (never a promote target — it is the native anchor OVER Wellen,
187
+ not itself a Welle AC3)."""
188
+ if not is_program:
189
+ return None
190
+ return (f"#{issue} is a Program-PRD (`<!-- prd: program -->` marker or the "
191
+ "programType label) — refusing to promote it; a Program-PRD is a "
192
+ "native anchor over Wellen, never itself a promote target.")
@@ -0,0 +1,116 @@
1
+ /** Build the distributable kit from this public repository's current SSOT. */
2
+ import { chmod, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { collectBundle } from '../src/lib/bundle.mjs';
6
+ import { sha256 } from '../src/lib/hash.mjs';
7
+ import { scrub } from './lib/scrub.mjs';
8
+ import { auditExecRefs, auditModuleImports, auditSkillNameRefs } from './lib/audit-refs.mjs';
9
+
10
+ const ROOT_FILES = ['LICENSE', 'README.md', 'PROVENANCE.md'];
11
+ const DOC_ASSETS = ['index.html', 'methodology.html', 'methodology.svg', 'workflow.png'];
12
+
13
+ const isBinary = (buf) => buf.includes(0);
14
+
15
+ async function walk(dir) {
16
+ const files = [];
17
+ let entries;
18
+ try { entries = await readdir(dir, { withFileTypes: true }); }
19
+ catch (error) { if (error.code === 'ENOENT') return files; throw error; }
20
+ for (const entry of entries) {
21
+ const path = join(dir, entry.name);
22
+ if (entry.isDirectory()) files.push(...await walk(path));
23
+ else if (entry.isFile()) files.push(path);
24
+ }
25
+ return files.sort();
26
+ }
27
+
28
+ async function writeOut(path, content, mode = 0o644) {
29
+ await mkdir(dirname(path), { recursive: true });
30
+ await writeFile(path, content);
31
+ await chmod(path, mode);
32
+ }
33
+
34
+ async function copySource(repoRoot, distDir, source, destination, mode = 0o644, shouldScrub = true) {
35
+ const raw = await readFile(join(repoRoot, source));
36
+ const content = shouldScrub && !isBinary(raw)
37
+ ? Buffer.from(scrub(raw.toString('utf8')), 'utf8') : raw;
38
+ await writeOut(join(distDir, destination), content, mode);
39
+ return content;
40
+ }
41
+
42
+ async function shipBundle(repoRoot, distDir, bundleFiles) {
43
+ const entries = [];
44
+ for (const file of bundleFiles) {
45
+ const content = await copySource(repoRoot, distDir, file.src, file.dest, file.mode);
46
+ entries.push({
47
+ path: file.dest, kind: file.kind, ownerSkill: file.ownerSkill,
48
+ surface: file.surface, sha256: sha256(content), mode: file.mode, origin: 'kit',
49
+ });
50
+ }
51
+ return entries.sort((a, b) => a.path.localeCompare(b.path));
52
+ }
53
+
54
+ async function shipRepositoryFiles(repoRoot, distDir, packageJson) {
55
+ for (const file of ROOT_FILES) await copySource(repoRoot, distDir, file, file, 0o644, false);
56
+ for (const file of DOC_ASSETS) {
57
+ await copySource(repoRoot, distDir, `docs/${file}`, `docs/${file}`);
58
+ }
59
+ for (const absolute of await walk(join(repoRoot, 'src'))) {
60
+ const path = `src/${relative(join(repoRoot, 'src'), absolute)}`;
61
+ await copySource(repoRoot, distDir, path, path);
62
+ }
63
+ const distributable = {
64
+ ...packageJson,
65
+ repository: {
66
+ type: 'git', url: 'git+https://github.com/iKon85/agent-workflow-kit.git',
67
+ },
68
+ };
69
+ await writeOut(join(distDir, 'package.json'), Buffer.from(`${JSON.stringify(distributable, null, 2)}\n`));
70
+ }
71
+
72
+ async function auditBuild(repoRoot, distDir, manifest, entries) {
73
+ const bodies = new Map();
74
+ for (const entry of entries) {
75
+ if (entry.path.endsWith('.md') || entry.path.endsWith('.py')) {
76
+ bodies.set(entry.path, await readFile(join(distDir, entry.path), 'utf8'));
77
+ }
78
+ }
79
+ const localModules = (await Promise.all(
80
+ ['scripts', '.claude/hooks'].map(async (dir) => {
81
+ try { return await readdir(join(repoRoot, dir)); } catch { return []; }
82
+ }),
83
+ )).flat().filter((name) => name.endsWith('.py')).map((name) => name.slice(0, -3));
84
+ const known = Object.keys(manifest.skills);
85
+ const published = known.filter((name) => manifest.skills[name].publish);
86
+ const read = (path) => bodies.get(path) ?? null;
87
+ const failures = [
88
+ ...auditExecRefs(entries, read).map((v) => `${v.file} -> ${v.ref}`),
89
+ ...auditModuleImports(entries, read, localModules).map((v) => `${v.file} -> ${v.module}`),
90
+ ...auditSkillNameRefs(entries, read, { known, published }).map((v) => `${v.file} -> ${v.skill}`),
91
+ ];
92
+ if (failures.length) throw new Error(`build-kit: dangling dependencies:\n${failures.join('\n')}`);
93
+ }
94
+
95
+ export async function buildKit({ repoRoot, distDir } = {}) {
96
+ repoRoot ??= join(dirname(fileURLToPath(import.meta.url)), '..');
97
+ distDir ??= join(repoRoot, 'dist-kit');
98
+ const manifest = JSON.parse(await readFile(join(repoRoot, '.claude/skills/skill-manifest.json'), 'utf8'));
99
+ const packageJson = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
100
+ await rm(distDir, { recursive: true, force: true });
101
+ await mkdir(distDir, { recursive: true });
102
+ const { files } = await collectBundle(repoRoot, manifest);
103
+ const entries = await shipBundle(repoRoot, distDir, files);
104
+ await shipRepositoryFiles(repoRoot, distDir, packageJson);
105
+ await writeOut(join(distDir, 'agent-workflow-kit.package.json'), Buffer.from(
106
+ `${JSON.stringify({ kitVersion: packageJson.version, files: entries }, null, 2)}\n`,
107
+ ));
108
+ await auditBuild(repoRoot, distDir, manifest, entries);
109
+ return { distDir, kitVersion: packageJson.version, fileCount: entries.length };
110
+ }
111
+
112
+ if (import.meta.url === `file://${process.argv[1]}`) {
113
+ buildKit().then(({ distDir, kitVersion, fileCount }) => {
114
+ console.log(`built ${distDir} · kitVersion ${kitVersion} · ${fileCount} files`);
115
+ }).catch((error) => { console.error(error.message); process.exitCode = 1; });
116
+ }
@@ -0,0 +1,59 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createHash } from 'node:crypto';
8
+ import { execFileSync } from 'node:child_process';
9
+ import { buildKit } from './build-kit.mjs';
10
+
11
+ const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
12
+ async function withBuild(fn) {
13
+ const dist = await mkdtemp(join(tmpdir(), 'awkit-build-'));
14
+ try { return await fn(dist, await buildKit({ repoRoot: REPO, distDir: dist })); }
15
+ finally { await rm(dist, { recursive: true, force: true }); }
16
+ }
17
+
18
+ test('historical v0.9.0 manifest remains an immutable golden fixture', async () => {
19
+ const fixture = await readFile(join(REPO, 'test/fixtures/v0.9.0-agent-workflow-kit.package.json'));
20
+ assert.equal(createHash('sha256').update(fixture).digest('hex'),
21
+ 'a209e14b1a2e5b3bba63c16ee6a7d713b6acd9a1d11379bb07fdd1e3e4ca8b6c');
22
+ const manifest = JSON.parse(fixture);
23
+ assert.equal(manifest.kitVersion, '0.9.0');
24
+ assert.equal(manifest.files.length, 215);
25
+ });
26
+
27
+ test('current public SSOT builds deterministically', async () => {
28
+ const first = await withBuild(async (dist) => readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
29
+ const second = await withBuild(async (dist) => readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
30
+ assert.equal(first, second);
31
+ });
32
+
33
+ test('current build contains post-tag public files and repository metadata', async () => {
34
+ await withBuild(async (dist) => {
35
+ await readFile(join(dist, '.agents/skills/codex-adapter-sync/SKILL.md'));
36
+ await readFile(join(dist, 'scripts/program_graph.py'));
37
+ const pkg = JSON.parse(await readFile(join(dist, 'package.json'), 'utf8'));
38
+ assert.equal(pkg.repository.url, 'git+https://github.com/iKon85/agent-workflow-kit.git');
39
+ });
40
+ });
41
+
42
+ test('current build is self-contained and never reaches into a consumer checkout', async () => {
43
+ const source = await readFile(join(REPO, 'scripts/build-kit.mjs'), 'utf8');
44
+ assert.doesNotMatch(source, /testreporter|tools\/agent-workflow-kit/);
45
+ });
46
+
47
+ test('npm pack keeps the complete scripts tree but excludes Python caches', () => {
48
+ const output = execFileSync('npm', ['pack', '--dry-run', '--json'], {
49
+ cwd: REPO, encoding: 'utf8',
50
+ });
51
+ const files = JSON.parse(output)[0].files.map((file) => file.path);
52
+ assert.ok(files.includes('scripts/build-kit.mjs'));
53
+ assert.ok(files.includes('scripts/board-sync.py'));
54
+ assert.ok(files.every((path) => !path.includes('__pycache__') && !path.endsWith('.pyc')));
55
+ const pkg = JSON.parse(execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], {
56
+ cwd: REPO, encoding: 'utf8',
57
+ }));
58
+ assert.ok(pkg.files.includes('scripts/'), 'package must ship future script file types');
59
+ });
@@ -0,0 +1,45 @@
1
+ /** Verify that the checked-in install manifest matches a fresh public-SSOT build. */
2
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { buildKit } from './build-kit.mjs';
7
+
8
+ const index = (manifest) => new Map(manifest.files.map((file) => [file.path, file.sha256]));
9
+
10
+ export function diffManifests(checked, built) {
11
+ const before = index(checked);
12
+ const after = index(built);
13
+ const added = [...after.keys()].filter((path) => !before.has(path)).sort();
14
+ const removed = [...before.keys()].filter((path) => !after.has(path)).sort();
15
+ const changed = [...after.keys()].filter(
16
+ (path) => before.has(path) && before.get(path) !== after.get(path),
17
+ ).sort();
18
+ return { added, removed, changed };
19
+ }
20
+
21
+ export async function checkKitStaleness({ repoRoot } = {}) {
22
+ repoRoot ??= join(dirname(fileURLToPath(import.meta.url)), '..');
23
+ const distDir = await mkdtemp(join(tmpdir(), 'awkit-staleness-'));
24
+ try {
25
+ await buildKit({ repoRoot, distDir });
26
+ const checked = JSON.parse(await readFile(join(repoRoot, 'agent-workflow-kit.package.json'), 'utf8'));
27
+ const built = JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8'));
28
+ const diff = diffManifests(checked, built);
29
+ const drift = Object.values(diff).some((paths) => paths.length);
30
+ return { status: drift ? 'drift' : 'ok', ...diff };
31
+ } finally {
32
+ await rm(distDir, { recursive: true, force: true });
33
+ }
34
+ }
35
+
36
+ if (import.meta.url === `file://${process.argv[1]}`) {
37
+ checkKitStaleness().then((result) => {
38
+ if (result.status === 'ok') return console.log('kit:staleness — OK');
39
+ console.error('kit:staleness — DRIFT');
40
+ for (const key of ['added', 'removed', 'changed']) {
41
+ if (result[key].length) console.error(` ${key}: ${result[key].join(', ')}`);
42
+ }
43
+ process.exitCode = 1;
44
+ }).catch((error) => { console.error(error.message); process.exitCode = 1; });
45
+ }
@@ -0,0 +1,19 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { diffManifests } from './check-kit-staleness.mjs';
7
+
8
+ test('manifest parity reports no drift for identical path hashes', () => {
9
+ const manifest = { files: [{ path: 'a', sha256: 'one' }] };
10
+ assert.deepEqual(diffManifests(manifest, manifest), { added: [], removed: [], changed: [] });
11
+ });
12
+
13
+ test('manifest parity counts added, removed, and changed files', () => {
14
+ const checked = { files: [{ path: 'gone', sha256: '1' }, { path: 'same', sha256: '1' }, { path: 'changed', sha256: '1' }] };
15
+ const built = { files: [{ path: 'new', sha256: '1' }, { path: 'same', sha256: '1' }, { path: 'changed', sha256: '2' }] };
16
+ assert.deepEqual(diffManifests(checked, built), {
17
+ added: ['new'], removed: ['gone'], changed: ['changed'],
18
+ });
19
+ });