yadflow 2.5.0 → 2.7.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 (124) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/README.md +65 -22
  3. package/bin/yad.mjs +27 -1
  4. package/cli/docs.mjs +298 -0
  5. package/cli/doctor.mjs +1 -0
  6. package/cli/manifest.mjs +23 -2
  7. package/cli/ship.mjs +37 -0
  8. package/docs/index.html +44 -13
  9. package/package.json +2 -2
  10. package/skills/sdlc/config.yaml +26 -2
  11. package/skills/sdlc/install.sh +1 -1
  12. package/skills/sdlc/module-help.csv +11 -4
  13. package/skills/yad-checks/references/check-gates.md +58 -2
  14. package/skills/yad-checks/templates/checks/commit-message.sh +82 -0
  15. package/skills/yad-checks/templates/github/yad-checks.yml +27 -0
  16. package/skills/yad-checks/templates/github/yad-hub-checks.yml +36 -0
  17. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +20 -0
  18. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +39 -0
  19. package/skills/yad-commit/SKILL.md +66 -0
  20. package/skills/yad-connect-docs/SKILL.md +132 -0
  21. package/skills/yad-connect-docs/references/docs-registry.md +74 -0
  22. package/skills/yad-docs/SKILL.md +159 -0
  23. package/skills/yad-docs/references/data-mapping.md +75 -0
  24. package/skills/yad-docs/references/theme-map.md +69 -0
  25. package/skills/yad-docs/templates/app/README.md +31 -0
  26. package/skills/yad-docs/templates/app/eslint.config.js +23 -0
  27. package/skills/yad-docs/templates/app/index.html +17 -0
  28. package/skills/yad-docs/templates/app/package-lock.json +4030 -0
  29. package/skills/yad-docs/templates/app/package.json +35 -0
  30. package/skills/yad-docs/templates/app/public/favicon.svg +28 -0
  31. package/skills/yad-docs/templates/app/public/logo.svg +39 -0
  32. package/skills/yad-docs/templates/app/public/vite.svg +1 -0
  33. package/skills/yad-docs/templates/app/src/App.tsx +98 -0
  34. package/skills/yad-docs/templates/app/src/components/Auth/LoginPage.tsx +101 -0
  35. package/skills/yad-docs/templates/app/src/components/Canvas/AnimatedMessage.tsx +101 -0
  36. package/skills/yad-docs/templates/app/src/components/Canvas/ConnectionLine.tsx +90 -0
  37. package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +216 -0
  38. package/skills/yad-docs/templates/app/src/components/Canvas/SystemComponent.tsx +153 -0
  39. package/skills/yad-docs/templates/app/src/components/Controls/PlaybackBar.tsx +284 -0
  40. package/skills/yad-docs/templates/app/src/components/Controls/StepDetail.tsx +167 -0
  41. package/skills/yad-docs/templates/app/src/components/DetailPanel/HandlerLogicSnippet.tsx +41 -0
  42. package/skills/yad-docs/templates/app/src/components/DetailPanel/RequestPayloadPreview.tsx +46 -0
  43. package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +88 -0
  44. package/skills/yad-docs/templates/app/src/components/DetailPanel/StatusCard.tsx +76 -0
  45. package/skills/yad-docs/templates/app/src/components/DetailPanel/TriggerEventCard.tsx +45 -0
  46. package/skills/yad-docs/templates/app/src/components/DocLayout/DocPageShell.tsx +80 -0
  47. package/skills/yad-docs/templates/app/src/components/DocLayout/DocSectionCard.tsx +55 -0
  48. package/skills/yad-docs/templates/app/src/components/DocLayout/DocTableOfContents.tsx +79 -0
  49. package/skills/yad-docs/templates/app/src/components/DocLayout/RoleCard.tsx +67 -0
  50. package/skills/yad-docs/templates/app/src/components/DocSections/ApiReferenceSection.tsx +108 -0
  51. package/skills/yad-docs/templates/app/src/components/DocSections/CancelabilitySection.tsx +73 -0
  52. package/skills/yad-docs/templates/app/src/components/DocSections/CriticalRunbookSection.tsx +177 -0
  53. package/skills/yad-docs/templates/app/src/components/DocSections/DataMigrationSection.tsx +102 -0
  54. package/skills/yad-docs/templates/app/src/components/DocSections/DbSchemaSection.tsx +98 -0
  55. package/skills/yad-docs/templates/app/src/components/DocSections/DeploymentGuideSection.tsx +104 -0
  56. package/skills/yad-docs/templates/app/src/components/DocSections/DriverIntegrationSection.tsx +127 -0
  57. package/skills/yad-docs/templates/app/src/components/DocSections/ExecutiveSummarySection.tsx +69 -0
  58. package/skills/yad-docs/templates/app/src/components/DocSections/FlowOverviewSection.tsx +73 -0
  59. package/skills/yad-docs/templates/app/src/components/DocSections/FlowPathsChecklistSection.tsx +96 -0
  60. package/skills/yad-docs/templates/app/src/components/DocSections/MiddlewareChainSection.tsx +107 -0
  61. package/skills/yad-docs/templates/app/src/components/DocSections/MonitoringAlertingSection.tsx +106 -0
  62. package/skills/yad-docs/templates/app/src/components/DocSections/NotificationLocalizationSection.tsx +102 -0
  63. package/skills/yad-docs/templates/app/src/components/DocSections/PMRoadmapSection.tsx +133 -0
  64. package/skills/yad-docs/templates/app/src/components/DocSections/PerformanceTestingSection.tsx +91 -0
  65. package/skills/yad-docs/templates/app/src/components/DocSections/RiderIntegrationSection.tsx +99 -0
  66. package/skills/yad-docs/templates/app/src/components/DocSections/SecuritySection.tsx +74 -0
  67. package/skills/yad-docs/templates/app/src/components/DocSections/StatusMachineSection.tsx +90 -0
  68. package/skills/yad-docs/templates/app/src/components/DocSections/TestPlanSection.tsx +163 -0
  69. package/skills/yad-docs/templates/app/src/components/Logs/SystemLogsTerminal.tsx +126 -0
  70. package/skills/yad-docs/templates/app/src/components/Navigation/TopNavBar.tsx +90 -0
  71. package/skills/yad-docs/templates/app/src/components/Reference/BullMQJobsList.tsx +60 -0
  72. package/skills/yad-docs/templates/app/src/components/Reference/DecisionTreeView.tsx +49 -0
  73. package/skills/yad-docs/templates/app/src/components/Reference/DeeplinkActionsChips.tsx +69 -0
  74. package/skills/yad-docs/templates/app/src/components/Reference/DriverUIStatesTable.tsx +61 -0
  75. package/skills/yad-docs/templates/app/src/components/Reference/FeatureFlagMatrix.tsx +73 -0
  76. package/skills/yad-docs/templates/app/src/components/Reference/RiderUIStatesTable.tsx +61 -0
  77. package/skills/yad-docs/templates/app/src/components/Reference/RulesLegendPanel.tsx +217 -0
  78. package/skills/yad-docs/templates/app/src/components/Reference/StakeholderToggle.tsx +41 -0
  79. package/skills/yad-docs/templates/app/src/components/Reference/TroubleshootingSection.tsx +93 -0
  80. package/skills/yad-docs/templates/app/src/components/Sidebar/PathSelector.tsx +148 -0
  81. package/skills/yad-docs/templates/app/src/components/Sidebar/SidebarFooter.tsx +40 -0
  82. package/skills/yad-docs/templates/app/src/components/Sidebar/StepList.tsx +234 -0
  83. package/skills/yad-docs/templates/app/src/components/shared/Badge.tsx +28 -0
  84. package/skills/yad-docs/templates/app/src/components/shared/CommandPalette.tsx +213 -0
  85. package/skills/yad-docs/templates/app/src/components/shared/Icon.tsx +21 -0
  86. package/skills/yad-docs/templates/app/src/components/shared/Tooltip.tsx +42 -0
  87. package/skills/yad-docs/templates/app/src/data/components.ts +74 -0
  88. package/skills/yad-docs/templates/app/src/data/docSections.ts +231 -0
  89. package/skills/yad-docs/templates/app/src/data/paths.ts +2319 -0
  90. package/skills/yad-docs/templates/app/src/data/referenceData.ts +392 -0
  91. package/skills/yad-docs/templates/app/src/data/roles.ts +145 -0
  92. package/skills/yad-docs/templates/app/src/data/types.ts +79 -0
  93. package/skills/yad-docs/templates/app/src/hooks/useAnimationQueue.ts +41 -0
  94. package/skills/yad-docs/templates/app/src/hooks/usePlayback.ts +100 -0
  95. package/skills/yad-docs/templates/app/src/hooks/useStakeholderFilter.ts +10 -0
  96. package/skills/yad-docs/templates/app/src/index.css +121 -0
  97. package/skills/yad-docs/templates/app/src/main.tsx +13 -0
  98. package/skills/yad-docs/templates/app/src/pages/RoleSelectPage.tsx +34 -0
  99. package/skills/yad-docs/templates/app/src/pages/StakeholderDocPage.tsx +98 -0
  100. package/skills/yad-docs/templates/app/src/pages/SubPathDetailPage.tsx +282 -0
  101. package/skills/yad-docs/templates/app/src/store/useAuthStore.ts +42 -0
  102. package/skills/yad-docs/templates/app/src/store/useFlowStore.ts +197 -0
  103. package/skills/yad-docs/templates/app/src/utils/iconMap.ts +46 -0
  104. package/skills/yad-docs/templates/app/tsconfig.app.json +28 -0
  105. package/skills/yad-docs/templates/app/tsconfig.json +7 -0
  106. package/skills/yad-docs/templates/app/tsconfig.node.json +26 -0
  107. package/skills/yad-docs/templates/app/vite.config.ts +10 -0
  108. package/skills/yad-docs-overview/SKILL.md +129 -0
  109. package/skills/yad-docs-overview/references/pipeline-model.md +102 -0
  110. package/skills/yad-docs-sync/SKILL.md +99 -0
  111. package/skills/yad-docs-sync/references/staleness.md +81 -0
  112. package/skills/yad-engineer-review/SKILL.md +86 -0
  113. package/skills/{yad-ship → yad-engineer-review}/references/ship-and-record.md +2 -2
  114. package/skills/{yad-ship → yad-engineer-review}/templates/.coderabbit.yaml +1 -1
  115. package/skills/yad-epic/references/state-schema.md +1 -1
  116. package/skills/yad-implement/SKILL.md +1 -1
  117. package/skills/yad-implement/references/implement-conventions.md +1 -1
  118. package/skills/yad-open-pr/SKILL.md +72 -0
  119. package/skills/yad-pr-template/templates/checks/pr-template.sh +62 -0
  120. package/skills/yad-pr-template/templates/checks/pr-title.sh +51 -0
  121. package/skills/yad-run/SKILL.md +2 -2
  122. package/skills/yad-run/references/run-loop.md +4 -4
  123. package/skills/yad-ship/SKILL.md +44 -66
  124. package/skills/yad-spec/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -1,9 +1,15 @@
1
- # [2.5.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.4.2...v2.5.0) (2026-06-14)
1
+ # [2.7.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.6.0...v2.7.0) (2026-06-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * drop unused today param from runDocs (lint) ([1c255f1](https://github.com/abdelrahmannasr/yadflow/commit/1c255f1c848b9571502e29b142a07366612d638c))
7
+ * publish per-epic docs sites in CI + check shell-version staleness ([9862646](https://github.com/abdelrahmannasr/yadflow/commit/9862646fbe910de8ad8248a5f1bb586a5604b18f))
2
8
 
3
9
 
4
10
  ### Features
5
11
 
6
- * per-scope roster roles + auto assignee/reviewer on PRs ([5ff066b](https://github.com/abdelrahmannasr/yadflow/commit/5ff066b2a83f63ddf25353ddaf0a088b91a6adb0))
12
+ * add interactive documentation skills + yad docs CLI ([4bf7a25](https://github.com/abdelrahmannasr/yadflow/commit/4bf7a25c38e28ef47704a8e2f5acec2f724e4e29))
7
13
 
8
14
  # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
9
15
 
package/README.md CHANGED
@@ -60,7 +60,10 @@ human**. Detailed walkthroughs for each phase follow below.
60
60
  | `skills/yad-pr-template/` | Build Step D: install the platform PR/MR template + risk routing (code repos **and** the hub). |
61
61
  | `skills/yad-review-comments/` | Install platform-matched PR/MR review-comment scaffolds (code repos and the hub). |
62
62
  | `skills/yad-hub-bridge/` | The templated PR/MR **review bridge**: open a review PR/MR on the hub and sync platform approvals/comments into the file ledger. |
63
- | `skills/yad-ship/` | Build Step E: AI review (advisory) engineer review ship + record in the build log. |
63
+ | `skills/yad-commit/` | Build helper: commit ONE staged atomic change by the conventions (Conventional subject, trailers, `--ai` footer, ≤3-file guard). |
64
+ | `skills/yad-open-pr/` | Build helper: open a code-repo task PR/MR from the committed template (push, prefill, roster auto-assign). |
65
+ | `skills/yad-ship/` | Build helper: commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`). |
66
+ | `skills/yad-engineer-review/` | Build Step E: AI review (advisory) → engineer review → merge + record in the build log. |
64
67
  | `skills/yad-backfill/` | Generate a human-verified spec for already-built code (Repomix), gated per touched feature. |
65
68
  | `skills/yad-run/` | Phase 4 orchestrator: drive a story's back half on the `automation` dial; kill switch. |
66
69
  | `skills/yad-status/` | Read-only view: front chain, build-half dials, trust record, fleet roll-up. |
@@ -94,13 +97,15 @@ with `npx` from your **product hub** repo — no clone needed.
94
97
  | `yad gate ci [--branch <head>] [--pr <n>]` | The CI entry the hub workflow calls on review/merge events: derive the epic/artifact from the `review/EP-*` branch, run the same sync, and commit **only the ledger** to the hub default branch (sweep every open review PR when no `--branch`). |
95
98
  | `yad commit --type <t> -m <subject>` | Commit by the SDLC convention — Conventional subject, `Task`/`Contract-Change`/`Co-Authored-By` trailers, atomic-file guard. |
96
99
  | `yad open-pr [--repo <name>]` | Open a code-repo **task** PR/MR from the repo's platform template (build half). |
100
+ | `yad ship --type <t> -m <subject>` | Commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`). |
97
101
  | `yad repo list` / `yad repo refresh [name]` | List connected repos as **fresh / stale**, and re-pack a stale one — staleness is now an explicit human decision, never an automatic skill side-effect. |
98
102
  | `npx yadflow --version` | Print the installed CLI version. |
99
103
 
100
104
  Flags: `--dir <path>` targets a project other than the cwd; `--force` re-copies unchanged files (or
101
105
  bypasses the commit atomic guard). Commit flags: `--type`, `-m/--message`, `--task`, `--ai
102
106
  <claude\|copilot\|cursor\|coderabbit\|none>`, `--contract-change`, `--dry-run`. `open-pr` flags:
103
- `--repo`, `--risk <low\|medium\|high>`, `--contract-change`.
107
+ `--repo`, `--risk <low\|medium\|high>`, `--contract-change`. `ship` takes the union of the `commit`
108
+ and `open-pr` flags (it runs `open-pr` only if the commit lands).
104
109
 
105
110
  ### The PR-driven review gate
106
111
 
@@ -128,7 +133,7 @@ simultaneous advancements can be lost; the next event or scheduled sweep re-sync
128
133
  ### What `setup` walks you through (10 steps)
129
134
 
130
135
  1. **Preflight** — confirm the hub is a git repo (offers `git init`); check `git`/`node`/`npx`.
131
- 2. **Install the module** — copy all 22 `yad-*` skills into the IDE skill dirs you pick
136
+ 2. **Install the module** — copy all 29 `yad-*` skills into the IDE skill dirs you pick
132
137
  (`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
133
138
  3. **Hub platform & roster** — detect GitHub/GitLab from the remote; record reviewers → `.sdlc/hub.json`.
134
139
  4. **Connect a design tool** — record the design tool (Figma / pencil / none) → `.sdlc/design.json` so
@@ -184,7 +189,7 @@ with a fix-it hint per finding. Failures carry stable, greppable codes, also pri
184
189
 
185
190
  Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
186
191
 
187
- ## Agent skills (all 22)
192
+ ## Agent skills (all 29)
188
193
 
189
194
  The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
190
195
  AI IDE (e.g. *“run `yad-epic`”*) to actually do the work. State lives in files you can also edit
@@ -213,6 +218,27 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
213
218
  tokens), detecting the **DeepTutor CLI on PATH** (a subprocess like Repomix — DeepTutor ships no MCP)
214
219
  and degrading to **harness-native** tutoring when absent. Idempotent and refreshable; one connection
215
220
  per project.
221
+ - **`yad-connect-docs`** — Connects a docs/Pages target (GitHub Pages / GitLab Pages, auto-detected from
222
+ `hub.json`) so the generated documentation sites can deploy. Records the target + scope + base path in
223
+ `.sdlc/docs.json` (local-user auth, no stored tokens), degrading to **build-only** when no Pages host /
224
+ CLI is present. Idempotent and refreshable; one connection per project.
225
+
226
+ ### Living documentation (generated, themed, auto-kept-fresh)
227
+
228
+ - **`yad-docs`** — Generates an **interactive documentation site** for an epic (a React + Vite + Tailwind
229
+ SPA: an animated front-stage flow canvas + role-based stakeholder doc pages) from the authored
230
+ artifacts — `epic.md`, `architecture.md`, the locked `contract.md`, `ui-design.md`, the stories — into
231
+ `epics/EP-<slug>/docs-site/`, themed by the **connected design system** (`DESIGN.md` / `design.json`
232
+ tokens → the site's CSS). The content lives in generated `src/data/*.ts`; the shell is a vendored
233
+ template. An **output enrichment, never a gate** — it never touches epic state, approvals, or the
234
+ contract lock. `generate` / `refresh` / `deploy`.
235
+ - **`yad-docs-overview`** — Generates the project **SDLC-overview site** (`docs/sdlc-site/`) — every
236
+ stage from setup → ship as flow paths / system components / stakeholder roles, reusing the same shell —
237
+ superseding the hand-maintained `docs/index.html`.
238
+ - **`yad-docs-sync`** — Keeps the sites fresh: detects staleness (a content hash of the authored
239
+ artifacts + the connected repos' HEAD shas vs each site's build manifest), regenerates + redeploys, and
240
+ can wire a CI job that rebuilds on push. Generalizes the rule that feature work must hand-update the
241
+ docs — the overview now regenerates whenever the skill set / pipeline changes.
216
242
 
217
243
  ### The learning layer (cross-cutting — any member, any stage)
218
244
 
@@ -276,15 +302,25 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
276
302
  (≤3 files) on its own branch. The diff stays inside the files the task declared (flag and STOP if it
277
303
  would grow). Commit ends with the task ID; `Contract-Change: yes` only if it touches the locked
278
304
  contract surface.
279
- - **`yad-checks`** — Step C, the production-safety gates. Wire and run three CI gates: **spec-link**
305
+ - **`yad-checks`** — Step C, the production-safety gates. Wire and run the CI gates: **spec-link**
280
306
  (every change links a real story/spec), **contract-check** (a contract-surface diff without a
281
- re-locked contract FAILS), and **build/test/lint**. CI-agnostic bash for GitHub Actions and GitLab CI.
307
+ re-locked contract FAILS), **build/test/lint**, **verified-commits** (signed + roster-known authors),
308
+ and the **pattern gates** — **commit-message** (Conventional subject + trailer order), **pr-title**,
309
+ and **pr-template** (the PR/MR body uses the template). Profile-aware (`code`|`hub`), so they run on
310
+ both code repos and the product hub. CI-agnostic bash for GitHub Actions and GitLab CI.
282
311
  - **`yad-pr-template`** — Step D. Detect the repo's platform and commit the matching PR/MR template with
283
312
  an Impact & Risk block; high risk (or a contract/auth/payments surface) routes the review to domain
284
- owners. Includes `risk-route.sh`.
285
- - **`yad-ship`** — Step E. AI review (CodeRabbit, advisory) engineer review (the human gate, owner +
286
- 1 reviewer with the same escalation) on merge, record the ship in the epic build-log and update the
287
- story state so the epic → story → task → PR chain stays traceable.
313
+ owners. Includes `risk-route.sh` plus the `pr-title.sh` / `pr-template.sh` gate scripts.
314
+ - **`yad-commit`** — build helper. Commit ONE staged atomic change by the conventions (Conventional
315
+ subject, `Task Contract-ChangeCo-Authored-By` trailers, the `--ai` co-author footer, the ≤3-file
316
+ atomic guard). Drives `yad commit`.
317
+ - **`yad-open-pr`** — build helper. Open a code-repo task PR/MR from the committed template: push the
318
+ branch, prefill the body, auto-assign the repo-scoped roster. Drives `yad open-pr`.
319
+ - **`yad-ship`** — build helper. Commit **and** open the task PR/MR in one step (`yad commit` then
320
+ `yad open-pr`; the PR step runs only if the commit lands). Drives `yad ship`.
321
+ - **`yad-engineer-review`** — Step E. AI review (CodeRabbit, advisory) → engineer review (the human gate,
322
+ owner + 1 reviewer with the same escalation) → on merge, record the ship in the epic build-log and
323
+ update the story state so the epic → story → task → PR chain stays traceable.
288
324
  - **`yad-backfill`** — Step G. Generate specs for already-built features in an existing repo so new work
289
325
  doesn't break them: pack one feature at a time with Repomix, write a DRAFT spec, require human approval
290
326
  before it counts. A change is blocked only until the features it touches have approved specs.
@@ -382,12 +418,16 @@ build half by hand”** below.
382
418
  11. `yad-implement story:<id> repo:<repo> task:<T0N>` → one atomic task = one branch = one commit
383
419
  (repeat per task). Commit by convention with **`yad commit --type <t> -m <subject> [--ai <tool>]`**
384
420
  (Task/Contract-Change/Co-Authored-By trailers, atomic-file guard).
385
- 12. `yad-checks repo:<repo> action: run` → spec-link, contract-check, build/test/lint, and
386
- verified-commits (platform-Verified signature + roster-allowlisted author) must pass.
387
- 13. Open the PR/MR from the wired template with **`yad open-pr --repo <repo> [--risk <level>]`**;
388
- `yad-pr-template repo:<repo> action: route` prints the required reviewers from the Impact & Risk block.
389
- 14. `yad-ship` `ai-review` (advisory) `approve` (the human engineer gate) `ship` (merge, record
390
- in `build-log.json`, update story status to `in-build`/`shipped`).
421
+ 12. `yad-checks repo:<repo> action: run` → spec-link, contract-check, build/test/lint, verified-commits
422
+ (platform-Verified signature + roster-allowlisted author), and commit-message must pass. (The
423
+ `pr-title` / `pr-template` gates need the PR title + body, so they run in CI once the PR exists —
424
+ step 13.)
425
+ 13. Open the PR/MR from the wired template with **`yad open-pr --repo <repo> [--risk <level>]`** (or do
426
+ 12+13 in one step with **`yad ship --type <t> -m <subject> --repo <repo>`**). The PR's CI now also
427
+ runs the `pr-title` and `pr-template` gates; `yad-pr-template repo:<repo> action: route` prints the
428
+ required reviewers from the Impact & Risk block.
429
+ 14. `yad-engineer-review` → `ai-review` (advisory) → `approve` (the human engineer gate) → `ship` (merge,
430
+ record in `build-log.json`, update story status to `in-build`/`shipped`).
391
431
  - **Multi-repo:** repeat 10–14 in each repo, all from the **one** locked contract.
392
432
  - **Existing code:** `yad-backfill` first, to produce a human-verified spec for a built feature.
393
433
 
@@ -514,16 +554,19 @@ the product repo. Code repos are **separate git repos** under `demo-repos/<repo>
514
554
  (`feat/<story>-<task>-…`) = one PR. The diff stays inside the files the task declared. Commit with
515
555
  **`yad commit`** — it builds the conventional subject, derives the `Task:` trailer from the branch
516
556
  (add `--contract-change` only if the locked surface is touched), appends an optional `--ai` co-author,
517
- and refuses a non-atomic stage. Open the PR with **`yad open-pr --repo <repo>`** (template prefilled).
518
- 3. **Check gates** `yad-checks` wires three CI gates (GitHub + GitLab) that must pass before merge:
557
+ and refuses a non-atomic stage. Open the PR with **`yad open-pr --repo <repo>`** (template prefilled),
558
+ or do both in one step with **`yad ship`** (commit then open-pr).
559
+ 3. **Check gates** — `yad-checks` wires the CI gates (GitHub + GitLab) that must pass before merge:
519
560
  **spec-link** (links a real story/spec), **contract-check** (a contract-surface change without
520
561
  `Contract-Change` + a re-locked contract FAILS, routing back to the architecture gate),
521
- **build/test/lint**. They fail closed on a bad base ref.
562
+ **build/test/lint**, **verified-commits**, and the **pattern gates** **commit-message** / **pr-title**
563
+ / **pr-template** (profile-aware `code`|`hub`, so they also run on the product hub). They fail closed
564
+ on a bad base ref.
522
565
  4. **PR/MR template + risk routing** — `yad-pr-template` drops the platform-matched template with an
523
566
  Impact & Risk block; `high` risk (or a contract/auth/payments surface) routes the review to domain
524
567
  owners (`risk-route.sh`), the same escalation as the gate.
525
- 5. **AI review → engineer review → ship** — `yad-ship`: CodeRabbit is an advisory first pass (never
526
- the authority); a human engineer approves (owner + 1 reviewer, escalating to domain owners); on
568
+ 5. **AI review → engineer review → merge** — `yad-engineer-review`: CodeRabbit is an advisory first pass
569
+ (never the authority); a human engineer approves (owner + 1 reviewer, escalating to domain owners); on
527
570
  merge the ship is recorded in `.sdlc/build-log.json` and the story state becomes `in-build` →
528
571
  `shipped`. The epic → story → task → PR → mergeCommit chain is traceable both ways.
529
572
 
@@ -551,7 +594,7 @@ with their dials, per repo) and `trust-log.json` (every run's verdict). See
551
594
  - **Drive a story's back half:** `yad-run {story} {repo}` walks `spec → tasks → implement → checks`,
552
595
  reading each step's dial. On `machine_advance` it advances on its own; on `human_approve` it stops
553
596
  for a human; on any FAIL, scope overrun, or contract-surface touch it **halts and pulls in a human**.
554
- It always stops at the engineer review (`yad-ship`), which is never automated.
597
+ It always stops at the engineer review (`yad-engineer-review`), which is never automated.
555
598
  - **Read the trust log:** `yad-status {epic}` shows each back step's dial, status, and trust record —
556
599
  runs, % `approved-unchanged`, and whether that clears the threshold (`automation.trust_threshold` in
557
600
  `config.yaml`, default ≥5 runs and ≥80% unchanged). The engineer review records each run's verdict
package/bin/yad.mjs CHANGED
@@ -8,7 +8,9 @@ import { gateOpen, gateSync, gateComments, gateStatus, gateCi } from '../cli/gat
8
8
  import { isValidEpicId } from '../cli/epic-state.mjs';
9
9
  import { runCommit } from '../cli/commit.mjs';
10
10
  import { runOpenPr } from '../cli/openpr.mjs';
11
+ import { runShip } from '../cli/ship.mjs';
11
12
  import { runRepo } from '../cli/repo.mjs';
13
+ import { runDocs } from '../cli/docs.mjs';
12
14
  import { runDoctor } from '../cli/doctor.mjs';
13
15
 
14
16
  const HELP = `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
@@ -34,9 +36,16 @@ ${c.bold('Review gate (front half)')}
34
36
  ${c.bold('Build helpers')}
35
37
  yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
36
38
  yad open-pr [--repo <name>] Open a code-repo task PR/MR from the template
39
+ yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step
37
40
  yad repo list Show connected repos (fresh / stale)
38
41
  yad repo refresh [name] Re-pack a stale repo (a human decision)
39
42
 
43
+ ${c.bold('Interactive docs (generated sites)')}
44
+ yad docs list Show the docs target + per-site freshness
45
+ yad docs build [--epic <id>|--overview] npm-build a generated doc site
46
+ yad docs deploy [--epic <id>|--overview] Build + report the Pages deploy
47
+ yad docs sync [--check|--refresh|--wire] Staleness sweep; --wire installs the Pages CI
48
+
40
49
  ${c.bold('Options')}
41
50
  --dir <path> Target project root (default: cwd)
42
51
  --type <t> commit: feat|fix|docs|refactor|test|perf|build|ci|chore|revert
@@ -46,6 +55,9 @@ ${c.bold('Options')}
46
55
  --contract-change commit/open-pr: mark the contract surface touched
47
56
  --risk <level> open-pr: low|medium|high (default low)
48
57
  --repo <name> open-pr: target a registered repo by name
58
+ --epic <id> docs: target one epic's site (EP-<slug>)
59
+ --overview docs: target the project SDLC-overview site
60
+ --check/--refresh/--wire docs sync: report stale / rebuild / install Pages CI
49
61
  --dry-run commit: print the message, do not commit
50
62
  --force commit: bypass the atomic-file guard / re-copy unchanged files
51
63
  --branch <head> gate ci: the review PR/MR head branch (review/EP-<slug>/<artifact>)
@@ -54,7 +66,7 @@ ${c.bold('Options')}
54
66
  -h, --help Show this help
55
67
  -v, --version Print version`;
56
68
 
57
- const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr']);
69
+ const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic']);
58
70
 
59
71
  function parseArgs(argv) {
60
72
  const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
@@ -64,6 +76,10 @@ function parseArgs(argv) {
64
76
  else if (a === '--force') o.force = true;
65
77
  else if (a === '--contract-change') o.contractChange = true;
66
78
  else if (a === '--no-push') o.noPush = true;
79
+ else if (a === '--overview') o.overview = true;
80
+ else if (a === '--check') o.check = true;
81
+ else if (a === '--refresh') o.refresh = true;
82
+ else if (a === '--wire') o.wire = true;
67
83
  else if (a === '--dry-run') o.dryRun = true;
68
84
  else if (a === '--json') o.json = true;
69
85
  else if (a === '-h' || a === '--help') o.help = true;
@@ -123,11 +139,21 @@ async function main() {
123
139
  case 'open-pr':
124
140
  await runOpenPr(o.dir, { repo: o.repo, platform: o.platform, base: o.base, title: o.title || o.message, task: o.task, risk: o.risk, contractChange: o.contractChange });
125
141
  break;
142
+ case 'ship':
143
+ await runShip(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force, repo: o.repo, platform: o.platform, base: o.base, title: o.title, risk: o.risk });
144
+ break;
126
145
  case 'repo': {
127
146
  const [, action, name] = o._;
128
147
  await runRepo(o.dir, { action: action || 'list', name, today });
129
148
  break;
130
149
  }
150
+ case 'docs': {
151
+ const [, action] = o._;
152
+ if (o.epic && !isValidEpicId(o.epic)) { log(c.red(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
153
+ const sync = o.wire ? 'wire' : o.refresh ? 'refresh' : 'check';
154
+ await runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
155
+ break;
156
+ }
131
157
  default:
132
158
  log(c.red(`unknown command: ${cmd}`));
133
159
  log(HELP);
package/cli/docs.mjs ADDED
@@ -0,0 +1,298 @@
1
+ // `yad docs list|build|deploy|sync` — the build/deploy/staleness mechanics for the interactive
2
+ // documentation sites generated by the yad-docs / yad-docs-overview skills. The CONTENT generation
3
+ // (reading epic/architecture/contract/ui/stories → writing src/data/*.ts and theming index.css) is
4
+ // the AI step inside those skills; this module only does the mechanical parts: npm build (a
5
+ // subprocess, like yad-spec shelling npx repomix), the platform Pages wiring (reusing platform.mjs),
6
+ // and the manifest-hash staleness check (reusing the head-sha idea from repo.mjs). It NEVER touches
7
+ // epic state, approvals, or the contract lock — docs are an output enrichment, not a gate.
8
+ //
9
+ // Pure mapping fns (deployTargetFromHub / siteBasePath / docsArtifactHash / docsStale / pagesWorkflow)
10
+ // are exported for unit tests; the side-effecting runDocs orchestrates them.
11
+ import path from 'node:path';
12
+ import fs from 'node:fs';
13
+ import { createHash } from 'node:crypto';
14
+ import { c, log, ok, info, warn, hand, fail, readJSON, run, has, exists } from './lib.mjs';
15
+ import { PROJECT_FILES, VERSION } from './manifest.mjs';
16
+ import { detectPlatform, platformReady } from './platform.mjs';
17
+ import { gitHead } from './setup.mjs';
18
+ import { contractSurfaceHash } from './epic-state.mjs';
19
+
20
+ // ---- registry + site locations ------------------------------------------------------------------
21
+ function loadDocs(root) {
22
+ const regPath = path.join(root, PROJECT_FILES.docsConfig);
23
+ return { regPath, docs: readJSON(regPath, null) };
24
+ }
25
+
26
+ // Per-epic site lives beside the epic artifacts; the overview is project-level under docs/.
27
+ export function siteDir(root, { epic, overview } = {}) {
28
+ if (overview) return path.join(root, 'docs/sdlc-site');
29
+ return path.join(root, 'epics', epic, 'docs-site');
30
+ }
31
+ export function manifestPath(root, { epic, overview } = {}) {
32
+ if (overview) return path.join(siteDir(root, { overview }), '.docs-build.json');
33
+ return path.join(root, 'epics', epic, '.sdlc/docs-build.json');
34
+ }
35
+
36
+ // ---- pure: platform/target + base path ----------------------------------------------------------
37
+ // hub.json platform -> the default Pages target (github-pages | gitlab-pages | none/build-only).
38
+ export function deployTargetFromHub(hub = {}) {
39
+ const platform = hub?.platform || detectPlatform(hub?.git_url || '');
40
+ if (platform === 'github') return 'github-pages';
41
+ if (platform === 'gitlab') return 'gitlab-pages';
42
+ return 'none';
43
+ }
44
+
45
+ // The Vite `base` for one site: join the project base path (from docs.json) with the per-site
46
+ // subpath. Per-epic sites nest under epics/<id>/; the overview is the Pages root. Always a
47
+ // leading+trailing slash so it works as a Vite base and a router basename.
48
+ export function siteBasePath(docs = {}, { epic, overview } = {}) {
49
+ const root = (docs.basePath || '/').replace(/\/+$/, '') || '';
50
+ const sub = overview ? '' : epic ? `/epics/${epic}` : '';
51
+ const joined = `${root}${sub}`.replace(/\/+/g, '/');
52
+ return joined ? `${joined.replace(/\/$/, '')}/` : '/';
53
+ }
54
+
55
+ // ---- pure: staleness ----------------------------------------------------------------------------
56
+ // The artifacts whose content a per-epic site is generated from (any that exist contribute).
57
+ // contract.md is deliberately EXCLUDED here: it is folded in via its CONTRACT-SURFACE block hash
58
+ // (docsArtifactHash's `extra`), so docs-staleness tracks the same locked surface as the contract
59
+ // lock — non-surface prose edits to contract.md don't needlessly mark the docs stale.
60
+ export function docsArtifactFiles(root, epic) {
61
+ const epicRoot = path.join(root, 'epics', epic);
62
+ const flat = ['epic.md', 'architecture.md', 'ui-design.md', 'DESIGN.md', 'test-cases.md']
63
+ .map((f) => path.join(epicRoot, f));
64
+ const storiesDir = path.join(epicRoot, 'stories');
65
+ const stories = exists(storiesDir)
66
+ ? fs.readdirSync(storiesDir).filter((f) => f.endsWith('.md')).sort().map((f) => path.join(storiesDir, f))
67
+ : [];
68
+ return [...flat, ...stories].filter(exists).sort();
69
+ }
70
+
71
+ // sha256 over the (sorted) artifact files' bytes plus an optional `extra` string (the contract
72
+ // surface hash) — the content baseline a site was built from. Deterministic: the file list is
73
+ // sorted and each file is length-prefixed so concatenation is unambiguous (a + bc never collides
74
+ // with ab + c).
75
+ export function docsArtifactHash(files = [], extra = '') {
76
+ const h = createHash('sha256');
77
+ for (const f of [...files].sort()) {
78
+ const buf = fs.readFileSync(f);
79
+ h.update(`${path.basename(f)}:${buf.length}\n`);
80
+ h.update(buf);
81
+ }
82
+ if (extra) h.update(`\ncontract-surface:${extra}`);
83
+ return 'sha256:' + h.digest('hex');
84
+ }
85
+
86
+ // Current HEAD sha per repo the epic touches (mirrors repo.mjs head-sha staleness).
87
+ export function repoHeadsFor(root, repos = [], registry = { repos: [] }) {
88
+ const out = {};
89
+ for (const name of repos) {
90
+ const entry = (registry.repos || []).find((r) => r.name === name);
91
+ if (!entry) continue;
92
+ out[name] = gitHead(path.resolve(root, entry.path)) || null;
93
+ }
94
+ return out;
95
+ }
96
+
97
+ // Compare a build manifest to the current world; list the concrete reasons it is stale.
98
+ export function docsStale(manifest, { artifactHash, repoHeads = {}, templateVersion } = {}) {
99
+ const reasons = [];
100
+ if (!manifest) return { stale: true, reasons: ['never built'] };
101
+ if (artifactHash && manifest.artifactHash && artifactHash !== manifest.artifactHash) {
102
+ reasons.push('authored artifacts changed');
103
+ }
104
+ for (const [repo, head] of Object.entries(repoHeads)) {
105
+ const was = (manifest.repoHeads || {})[repo];
106
+ if (head && was && head !== was) reasons.push(`repo ${repo} HEAD advanced`);
107
+ }
108
+ if (templateVersion && manifest.templateVersion && templateVersion !== manifest.templateVersion) {
109
+ reasons.push(`doc shell upgraded (${manifest.templateVersion} → ${templateVersion})`);
110
+ }
111
+ return { stale: reasons.length > 0, reasons };
112
+ }
113
+
114
+ // ---- pure: the Pages CI workflow (committed by `yad docs sync --wire`) ---------------------------
115
+ // GitHub Actions deploy-pages, or a GitLab `pages` job. Both assemble a single `public/` tree: the
116
+ // overview at the root and every per-epic site under `epics/<id>/` — matching siteBasePath's nesting
117
+ // (overview at `<base>/`, epics at `<base>/epics/EP-<slug>/`). A concurrency group prevents the
118
+ // deploy from retriggering. The shared shell script keeps the two platforms byte-for-byte aligned.
119
+ const BUILD_PUBLIC = [
120
+ 'mkdir -p public',
121
+ 'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && cp -r docs/sdlc-site/dist/. public/; fi',
122
+ 'for d in epics/*/docs-site; do [ -d "$d" ] || continue; id=$(basename "$(dirname "$d")"); (cd "$d" && npm ci && npm run build) && mkdir -p "public/epics/$id" && cp -r "$d/dist/." "public/epics/$id/"; done',
123
+ ];
124
+ export function pagesWorkflow(platform) {
125
+ if (platform === 'gitlab') {
126
+ return `# yad-managed — built by \`yad docs sync --wire\`. include it from .gitlab-ci.yml:
127
+ # include: { local: .gitlab/ci/yad-docs.yml }
128
+ # Edit the skill, not this file.
129
+ pages:
130
+ stage: deploy
131
+ image: node:20
132
+ rules:
133
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
134
+ script:
135
+ ${BUILD_PUBLIC.map((l) => ` - '${l.replace(/'/g, "'\\''")}'`).join('\n')}
136
+ artifacts:
137
+ paths: [public]
138
+ resource_group: pages
139
+ `;
140
+ }
141
+ return `# yad-managed — built by \`yad docs sync --wire\`. Edit the skill, not this file.
142
+ name: yad-docs
143
+ on:
144
+ push:
145
+ branches: [main, master]
146
+ workflow_dispatch:
147
+ permissions:
148
+ contents: read
149
+ pages: write
150
+ id-token: write
151
+ concurrency:
152
+ group: yad-docs-pages
153
+ cancel-in-progress: true
154
+ jobs:
155
+ build-deploy:
156
+ runs-on: ubuntu-latest
157
+ environment:
158
+ name: github-pages
159
+ url: \${{ steps.deployment.outputs.page_url }}
160
+ steps:
161
+ - uses: actions/checkout@v4
162
+ - uses: actions/setup-node@v4
163
+ with:
164
+ node-version: 20
165
+ - name: Build the overview + per-epic sites into ./public
166
+ run: |
167
+ ${BUILD_PUBLIC.map((l) => ` ${l}`).join('\n')}
168
+ - uses: actions/configure-pages@v5
169
+ - uses: actions/upload-pages-artifact@v3
170
+ with:
171
+ path: public
172
+ - id: deployment
173
+ uses: actions/deploy-pages@v4
174
+ `;
175
+ }
176
+ export function pagesWorkflowPath(platform) {
177
+ return platform === 'gitlab' ? '.gitlab/ci/yad-docs.yml' : '.github/workflows/yad-docs.yml';
178
+ }
179
+
180
+ // ---- build (subprocess) -------------------------------------------------------------------------
181
+ function buildSite(dir) {
182
+ if (!exists(dir)) { warn(`no generated site at ${path.relative(process.cwd(), dir)} — run the yad-docs skill first`); return { ok: false, missing: true }; }
183
+ if (!has('npm')) { warn('npm not on PATH — cannot build; the CI workflow will build on push'); return { ok: false, noNpm: true }; }
184
+ const install = exists(path.join(dir, 'package-lock.json')) ? ['ci'] : ['install'];
185
+ log(` ${c.dim('$')} npm ${install[0]} ${c.dim(`(${path.relative(process.cwd(), dir)})`)}`);
186
+ const i = run('npm', install, { cwd: dir, stdio: 'inherit' });
187
+ if (!i.ok) { fail('npm install failed'); return { ok: false }; }
188
+ const b = run('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' });
189
+ if (!b.ok) { fail('npm run build failed'); return { ok: false }; }
190
+ return { ok: true, dist: path.join(dir, 'dist') };
191
+ }
192
+
193
+ // ---- orchestration ------------------------------------------------------------------------------
194
+ export async function runDocs(root, { action = 'list', epic, overview, sync } = {}) {
195
+ const { docs } = loadDocs(root);
196
+ const targets = overview ? [{ overview: true }] : epic ? [{ epic }] : enumerateSites(root);
197
+
198
+ if (action === 'list') {
199
+ if (!docs) { warn('no docs target connected (.sdlc/docs.json) — run yad-connect-docs'); }
200
+ else {
201
+ log(c.bold('\ndocs target'));
202
+ info(`target ${c.cyan(docs.target)} scope ${docs.scope} base ${docs.basePath} ${docs.source === 'unavailable' ? c.yellow('(build-only)') : ''}`);
203
+ }
204
+ log(c.bold('\ngenerated sites'));
205
+ if (!targets.length) { info('none generated yet'); return { sites: 0 }; }
206
+ for (const t of targets) reportFreshness(root, t);
207
+ return { sites: targets.length };
208
+ }
209
+
210
+ if (action === 'build' || action === 'deploy') {
211
+ let built = 0;
212
+ for (const t of targets) {
213
+ const r = buildSite(siteDir(root, t));
214
+ if (r.ok) { built++; ok(`built ${label(t)} ${c.dim('→ ' + path.relative(root, r.dist))}`); }
215
+ }
216
+ if (action === 'deploy') {
217
+ const platform = docs ? (docs.target === 'gitlab-pages' ? 'gitlab' : docs.target === 'github-pages' ? 'github' : null) : null;
218
+ if (!platform || !platformReady(platform)) {
219
+ hand('no Pages platform/CLI — built locally only; commit + push so the CI workflow can publish (yad docs sync --wire)');
220
+ } else {
221
+ ok(`deploy via the ${platform} Pages workflow on push (yad docs sync --wire installs it)`);
222
+ if (docs?.basePath) info(`will publish under ${docs.basePath}`);
223
+ }
224
+ }
225
+ return { built };
226
+ }
227
+
228
+ if (action === 'sync') {
229
+ if (sync === 'wire') return wirePages(root, docs);
230
+ // check (default) + refresh both compute staleness; refresh additionally rebuilds.
231
+ const registry = readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] });
232
+ let stale = 0;
233
+ for (const t of targets) {
234
+ const s = freshness(root, t, registry);
235
+ if (s.stale) {
236
+ stale++;
237
+ warn(`${label(t)} — ${c.yellow('stale')}: ${s.reasons.join('; ')}`);
238
+ if (sync === 'refresh') buildSite(siteDir(root, t));
239
+ } else ok(`${label(t)} ${c.dim('— fresh')}`);
240
+ }
241
+ if (stale && sync !== 'refresh') hand('regenerate content with the yad-docs / yad-docs-overview skill (the AI step), then `yad docs deploy`');
242
+ return { stale };
243
+ }
244
+
245
+ fail(`unknown docs action: ${action} (list | build | deploy | sync)`);
246
+ process.exitCode = 1;
247
+ return {};
248
+ }
249
+
250
+ // ---- helpers ------------------------------------------------------------------------------------
251
+ function enumerateSites(root) {
252
+ const out = [];
253
+ if (exists(siteDir(root, { overview: true }))) out.push({ overview: true });
254
+ const epicsDir = path.join(root, 'epics');
255
+ if (exists(epicsDir)) {
256
+ for (const e of fs.readdirSync(epicsDir).sort()) {
257
+ if (exists(path.join(epicsDir, e, 'docs-site'))) out.push({ epic: e });
258
+ }
259
+ }
260
+ return out;
261
+ }
262
+ function label(t) { return t.overview ? 'overview (docs/sdlc-site)' : `epic ${t.epic}`; }
263
+
264
+ function freshness(root, t, registry) {
265
+ const manifest = readJSON(manifestPath(root, t), null);
266
+ if (t.overview) {
267
+ // The overview is generated from the project pipeline definition, not an epic's artifacts.
268
+ const files = ['skills/sdlc/config.yaml', 'skills/sdlc/module-help.csv', 'docs/diagrams/sdlc-overview.mmd']
269
+ .map((f) => path.join(root, f)).filter(exists);
270
+ return docsStale(manifest, { artifactHash: docsArtifactHash(files), templateVersion: VERSION });
271
+ }
272
+ const epicMeta = readJSON(path.join(root, 'epics', t.epic, '.sdlc/state.json'), {});
273
+ const repos = epicMeta.repos || [];
274
+ const surface = contractSurfaceHash(path.join(root, 'epics', t.epic)); // null when no locked surface
275
+ return docsStale(manifest, {
276
+ artifactHash: docsArtifactHash(docsArtifactFiles(root, t.epic), surface || ''),
277
+ repoHeads: repoHeadsFor(root, repos, registry),
278
+ templateVersion: VERSION,
279
+ });
280
+ }
281
+ function reportFreshness(root, t) {
282
+ const registry = readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] });
283
+ const s = freshness(root, t, registry);
284
+ if (s.stale) warn(`${label(t)} — ${c.yellow('stale')}: ${s.reasons.join('; ')}`);
285
+ else ok(`${label(t)} ${c.dim('— fresh')}`);
286
+ }
287
+
288
+ function wirePages(root, docs) {
289
+ const platform = docs?.target === 'gitlab-pages' ? 'gitlab' : 'github';
290
+ const rel = pagesWorkflowPath(platform);
291
+ const dest = path.join(root, rel);
292
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
293
+ fs.writeFileSync(dest, pagesWorkflow(platform));
294
+ ok(`wired ${rel} ${c.dim(`(${platform} Pages)`)}`);
295
+ if (platform === 'gitlab') hand(`include it from .gitlab-ci.yml: include: { local: ${rel} }`);
296
+ else hand('commit it; set the repo Pages source to "GitHub Actions" so it publishes on push');
297
+ return { wired: rel };
298
+ }
package/cli/doctor.mjs CHANGED
@@ -195,6 +195,7 @@ export function ciTagsChecks(checks, root, hub, registry) {
195
195
  fragments.push(
196
196
  { scope: 'hub', file: '.gitlab/ci/yad-gate-sync.yml', path: path.join(root, '.gitlab/ci/yad-gate-sync.yml') },
197
197
  { scope: 'hub', file: '.gitlab/ci/yad-verified-commits.yml', path: path.join(root, '.gitlab/ci/yad-verified-commits.yml') },
198
+ { scope: 'hub', file: '.gitlab/ci/yad-hub-checks.yml', path: path.join(root, '.gitlab/ci/yad-hub-checks.yml') },
198
199
  );
199
200
  }
200
201
  for (const repo of registry?.repos || []) {