diff --git a/.github/docs/WORKFLOWS.md b/.github/docs/WORKFLOWS.md index bbb6fb5..973283e 100644 --- a/.github/docs/WORKFLOWS.md +++ b/.github/docs/WORKFLOWS.md @@ -143,11 +143,12 @@ Extracts the PR's size label and maps to emoji for the workflow summary. | Step | Name | What it does | |------|------|-------------| | 0-0a | Cache | pnpm store, keyed on `pnpm-lock.yaml` | -| 2 | Setup GitHub Config | Git config | -| 3 | Release notes | Runs `scripts/get_github_release_log.sh` | -| 4 | Prepare release | `corepack pnpm install --frozen-lockfile && corepack pnpm run release` + push tags to main | -| 5 | Get release tag | Extract latest tag | -| 6 | Publish release | `ncipollo/release-action@v1` | +| 1 | Setup GitHub Config | Git config | +| 1a | **Dogfood — refresh vendored agent skills** | Resolves the latest tag of `DailybotHQ/deepworkplan-skill` and `DailybotHQ/agent-skill` via `gh release view`, and if either is newer than the vendored copy under `.agents/skills/`, runs `npx --yes skills add @ --skill --force -y`, asserts the version invariant, and commits `chore: dogfood vendored skills to (…)`. **Runs before Step 2** so the dogfood commit lands in the release notes. See "Dogfood step" below for details. | +| 2 | Release notes | Runs `scripts/get_github_release_log.sh` — includes the dogfood commit from Step 1a if it exists | +| 3 | Prepare release | `corepack pnpm install --frozen-lockfile && corepack pnpm run release` + push tags to main (pushes the dogfood commit alongside the version-bump commit + tag in one atomic push) | +| 4 | Get release tag | Extract latest tag | +| 5 | Publish release | `ncipollo/release-action@v1` | **Helper script:** `scripts/get_github_release_log.sh` - Reads `git log --pretty=oneline` @@ -156,6 +157,30 @@ Extracts the PR's size label and maps to emoji for the workflow summary. - Prefixes each entry with `🚩` - Creates `git_logs_output.txt` as release body +#### Dogfood step (Step 1a) + +**Purpose.** Every website release should ship with a current snapshot of the two agent skills it documents. Rather than a scheduled/autonomous refresh, the update happens **exactly when the maintainer decides to cut a release** — no background cron, no drift between "when a skill dropped" and "when someone releases the site". + +**What runs.** + +1. `gh release view --repo --json tagName -q .tagName` for each of `DailybotHQ/deepworkplan-skill` and `DailybotHQ/agent-skill` to resolve the latest tag. +2. Reads the currently vendored versions from `.agents/skills/{deepworkplan,dailybot}/SKILL.md` frontmatter. +3. Per-skill "moved" flag → only installs the skills that actually changed. Commits and PR titles list only the moved skills (no misleading "dogfood to deepworkplan vX, dailybot vY" when only one moved). +4. `npx --yes skills add @ --skill --force -y` — exact same command any downstream consumer would run. Serves as a live smoke test that the upstream release is installable. +5. Version invariant: the installed `SKILL.md` frontmatter `version:` MUST equal the requested tag. Fails the release if not — refuses to publish a website tag that misrepresents its vendored skills. +6. If any files changed, commits `chore: dogfood vendored skills to ` locally. Step 3's `git push --follow-tags` sends this commit alongside the version-bump commit and the tag in a single atomic push. + +**Failure semantics.** + +| Failure | Behavior | +|---------|----------| +| `gh release view` fails for one upstream repo (rate limit, transient outage) | Warns; skips that skill. The other skill (and the release) proceed. | +| `npx skills add` fails | **Fails the release** — real upstream breakage, must be surfaced. | +| Version invariant mismatch after install | **Fails the release** — refuses to commit a misrepresented dogfood snapshot. | +| Both skills already at latest | Clean no-op — no dogfood commit, release proceeds. | + +**Manual refresh outside of a release.** Not supported by design. If a maintainer needs to bump a vendored skill without cutting a full release, do it locally (`npx skills add @`) on a PR branch and merge it as a normal `chore:` change — that will trigger a release naturally. + ### Job 3: `cleanup_caches` (depends on: Job 2) See [DEPLOYMENT.md](./DEPLOYMENT.md) for Cloudflare Pages setup. diff --git a/.github/workflows/release_and_publish.yml b/.github/workflows/release_and_publish.yml index 3b885cb..bce21a3 100644 --- a/.github/workflows/release_and_publish.yml +++ b/.github/workflows/release_and_publish.yml @@ -97,6 +97,148 @@ jobs: git config user.name "deepworkplan-bot" git config user.email "bot@deepworkplan.com" git config advice.skippedCherryPicks false + - name: Step 1a - 🐕 Dogfood — refresh vendored agent skills to latest upstream + # Before every website release, bring the two vendored skills under + # `.agents/skills/` up to their latest published upstream tags — + # so `vX.Y.Z` of this site always ships with a current snapshot of + # `deepworkplan` and `dailybot`. Mirrors the "Dogfood" step in + # DailybotHQ/deepworkplan-skill's own `auto-release.yml`, adapted + # to install third-party upstream tags instead of this repo's own. + # + # This step runs BEFORE Step 2 on purpose: `get_github_release_log.sh` + # in Step 2 collects commit subjects from the previous release + # marker up to HEAD; committing the refresh here ensures the + # dogfood commit lands in the GitHub Release notes alongside the + # PR that triggered this run. + # + # Failure semantics: + # * Upstream API blip while resolving a tag → warn + skip THAT + # skill (a temporary hiccup with `gh release view` should NOT + # block a website release that was going to happen anyway). + # * `npx skills add` failure OR version-invariant mismatch → + # FAIL the release (we should not publish a website tag that + # misrepresents which skill versions it vendors). + # * Both skills already at latest → clean no-op, release proceeds + # normally without a dogfood commit. + env: + GH_TOKEN: ${{ secrets.AUTOMATION_GITHUB_TOKEN }} + run: | + set -euo pipefail + + # Resolve one upstream skill to a concrete tag. Prints "" on + # failure so callers can skip cleanly. + # + # CRITICAL: all logging goes to stderr (>&2). The caller uses + # `X=$(resolve ...)` — any `::notice::`/`::warning::` on stdout + # would be captured into the variable and poison the equality + # checks below. GitHub Actions still renders workflow commands + # emitted to stderr. + resolve() { + local repo="$1" name="$2" + local tag + tag=$(gh release view --repo "$repo" --json tagName -q .tagName 2>/dev/null || true) + if [ -z "$tag" ]; then + echo "::warning::${name}: could not resolve latest release for ${repo} — skipping this skill this release" >&2 + printf '\n' + return 0 + fi + echo "::notice::${name}: latest published release is ${tag}" >&2 + printf '%s\n' "$tag" + } + + read_version() { + sed -nE 's/^version:[[:space:]]*"([^"]+)".*/\1/p' "$1" | head -n1 + } + + install_and_verify() { + local repo="$1" skill_name="$2" skill_dir="$3" tag="$4" + local new_version="${tag#v}" + # Two `-y` / `--yes` flags below — both required in a non-TTY + # environment: `npx --yes` skips the "install the package?" + # prompt, `skills add ... -y` skips the interactive agent picker. + echo "::group::${skill_name}: npx skills add ${repo}@${tag}" + npx --yes skills add "${repo}@${tag}" --skill "$skill_name" --force -y + echo "::endgroup::" + # Invariant: what got installed MUST equal what we asked for. + local vendored + vendored=$(read_version "${skill_dir}/SKILL.md") + if [ "$vendored" != "$new_version" ]; then + echo "::error::${skill_name}: installed version '${vendored}' does not match requested tag '${tag}' (expected '${new_version}')" + exit 1 + fi + } + + DWP_TAG=$(resolve DailybotHQ/deepworkplan-skill deepworkplan) + DB_TAG=$(resolve DailybotHQ/agent-skill dailybot) + + DWP_CUR=$(read_version .agents/skills/deepworkplan/SKILL.md) + DB_CUR=$(read_version .agents/skills/dailybot/SKILL.md) + + # Only install skills that would actually change — no wasted work + # and no misleading dogfood commit when one skill is at latest + # and the other is not. + DWP_MOVED=false + DB_MOVED=false + [ -n "$DWP_TAG" ] && [ "$DWP_CUR" != "${DWP_TAG#v}" ] && DWP_MOVED=true + [ -n "$DB_TAG" ] && [ "$DB_CUR" != "${DB_TAG#v}" ] && DB_MOVED=true + + [ "$DWP_MOVED" = "true" ] && install_and_verify DailybotHQ/deepworkplan-skill deepworkplan .agents/skills/deepworkplan "$DWP_TAG" + [ "$DB_MOVED" = "true" ] && install_and_verify DailybotHQ/agent-skill dailybot .agents/skills/dailybot "$DB_TAG" + + # Stage only the paths the installer is allowed to touch. + git add \ + .agents/skills/deepworkplan \ + .agents/skills/dailybot \ + skills-lock.json 2>/dev/null || true + + if git diff --cached --quiet; then + echo "Vendored skills already at latest — no dogfood commit needed." + { + echo "## Vendored skills" + echo "" + echo "| Skill | Vendored | Latest upstream | Moved? |" + echo "| --- | --- | --- | --- |" + echo "| deepworkplan | v${DWP_CUR} | ${DWP_TAG:-_(unresolved)_} | ${DWP_MOVED} |" + echo "| dailybot | v${DB_CUR} | ${DB_TAG:-_(unresolved)_} | ${DB_MOVED} |" + echo "" + echo "**No dogfood commit** — vendored copies already match the latest upstream releases." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # Build a commit subject that lists ONLY the skills that moved. + # `${array[*]}` only uses the first character of IFS when joining, + # so build the joined string by hand to preserve "a, b" (with the + # space). + parts=() + [ "$DWP_MOVED" = "true" ] && parts+=("deepworkplan ${DWP_TAG}") + [ "$DB_MOVED" = "true" ] && parts+=("dailybot ${DB_TAG}") + MSG="" + for p in "${parts[@]}"; do + if [ -z "$MSG" ]; then MSG="$p"; else MSG="${MSG}, ${p}"; fi + done + + # Commit locally. Step 3 ('Prepare release') runs right after + # and does `pnpm run release && git push --follow-tags origin + # main`, which pushes THIS commit alongside the version-bump + # commit + tag in a single atomic push. The commit shows up in + # the release body because Step 2 (get_github_release_log.sh) + # runs after this step and walks the commit range from the + # previous release marker up to HEAD. + git commit -m "chore: dogfood vendored skills to ${MSG}" + + { + echo "## Vendored skills" + echo "" + echo "| Skill | Vendored | Latest upstream | Moved? |" + echo "| --- | --- | --- | --- |" + echo "| deepworkplan | v${DWP_CUR} | ${DWP_TAG:-_(unresolved)_} | ${DWP_MOVED} |" + echo "| dailybot | v${DB_CUR} | ${DB_TAG:-_(unresolved)_} | ${DB_MOVED} |" + echo "" + echo "**Dogfood commit:** \`chore: dogfood vendored skills to ${MSG}\`" + echo "" + echo "This commit is pushed alongside the version-bump commit in Step 3." + } >> "$GITHUB_STEP_SUMMARY" - name: Step 2 - 📄 Set GitHub release content "BODY" env var run: | bash .github/scripts/get_github_release_log.sh diff --git a/AGENTS.md b/AGENTS.md index f60262e..dcd582f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -261,6 +261,27 @@ This repo has the DWP **Dailybot addon** wired: the `dailybot` skill is installe **Deterministic hook enforcement (Claude Code):** `.agents/settings.json` wires the Dailybot lifecycle hooks (`dailybot hook session-start | activity | stop`, CLI >= 1.12.0) so the harness itself detects unreported work and reminds the agent at end of turn — no reliance on the model remembering. When a reminder fires: send a report if a meaningful unit of work is done, or run `dailybot hook dismiss` if not — never ignore it silently, and never let reporting block work. The hooks are local-only, always exit 0, and respect `.dailybot/disabled`. +### Vendored agent skills — refreshed on every website release + +`.agents/skills/deepworkplan/` and `.agents/skills/dailybot/` are **vendored copies** of the upstream skill repos (`DailybotHQ/deepworkplan-skill` and `DailybotHQ/agent-skill`), tracked in git and pinned via `skills-lock.json`. They are refreshed **automatically as part of every website release**, so `vX.Y.Z` of the site always ships with a current snapshot of both skills. + +**How it works.** [`release_and_publish.yml`](.github/workflows/release_and_publish.yml) (which fires on every merge to `main`) has a dogfood step (Step 1a) that runs **before** the version bump: + +1. Resolves the latest tag of each upstream skill via `gh release view --repo `. +2. Compares against the vendored `SKILL.md` `version:` field. Only installs skills that actually moved. +3. Runs `npx --yes skills add @ --skill --force -y` — the exact command any downstream consumer would run, so this doubles as a live smoke test. +4. Asserts the invariant: installed `SKILL.md` version equals the requested tag. Refuses to proceed with the release if not. +5. If any files changed, commits `chore: dogfood vendored skills to (…)` locally. Step 3's `git push --follow-tags` sends this commit alongside the version-bump commit and the tag in a single atomic push, and the dogfood commit appears in the auto-generated GitHub Release notes. + +**Semantics.** Skill refresh is **release-driven**, not autonomous — no scheduled/cron refresh runs in the background. The vendored copies advance only when a maintainer merges a PR to `main`, which is the same moment `release_and_publish.yml` cuts a new website release. The intent is that the maintainer controls exactly when the site adopts a new skill version. + +**Failure semantics.** +- `npx skills add` failure OR version-invariant mismatch → **fails the release** (a broken upstream tag must never quietly ship inside a website version). +- Transient `gh release view` blip (rate limit, temporary outage) → skips only that skill for this release; the release itself proceeds. +- Both skills already at latest → clean no-op, no dogfood commit, release proceeds normally. + +**Do not edit files under `.agents/skills/deepworkplan/` or `.agents/skills/dailybot/` by hand** — the next release will overwrite hand edits. Contribute upstream, then merge any PR to trigger a website release that picks up the new upstream tag. + ## Quick Commands ```bash