Files
boshu2__agentops/scripts/check-workflow-drift.sh
Bo 238964fdd2 feat(cli): workflows are canonical product artifacts — workflows/ + ao workflows link (#945)
Workflows get the skills treatment (operator decision): canonical source
in the product tree, installed by a product verb, Claude-only labeled as
such.

**What moves:** all seven Claude workflow scripts + README migrate from
force-added exceptions inside the gitignored `.claude/` to a tracked
top-level `workflows/` (sibling of `skills/`) — the four existing
conveyors plus `audit-dimensions`, `verify-fixes`, `implement-wave`:
three thin, args-parameterized orchestration conveyors extracted from
this session's hand-rolled waves, contract-reviewed, and smoke-proven
through the real Workflow runtime (the smoke caught two contract gaps
static review could not: an `export default` wrapper the runtime never
invokes, and args arriving as a JSON string — both fixed, string-args
tolerance now built in).

**New verb:** `ao workflows link` / `unlink` mirror `ao skills link`
semantics — dry-run `--json`, refuse to replace real files or foreign
links, unlink only checkout-owned links — targeting the project-local
`.claude/workflows/` where Claude Code resolves named workflows
(`--into` overrides). Checkout identity reuses the skillsapp marker
discipline, fail-closed. Claude-only runtime adapter, same doctrine as
the Codex-only `skills-codex/`.

**Legacy surfaces repointed:** `install-workflows.sh` (user-global $HOME
installer), `check-workflow-drift.sh` + gate comment,
`check-bdd-foundry-markers.sh`; spine allowlist + YAML-probe excuse +
go-cli.md spine region gain the workflows group; COMMANDS.md,
cli-surface projections, and surface-count fixture regenerated; new
tests carry per-command git-env scrubbing (test-isolation ratchet back
at baseline).

**Built BY the workflow being canonized** — `implement-wave`
orchestrated its own canonization: two disjoint-ownership lanes plus a
seam-checking verifier that ran the real binary's link → resolve →
unlink cycle in the live tree (both lanes RESOLVED). The lanes correctly
*refused* to self-approve their command into the spine invariants and
handed integration three flagged edits instead.

**Expected local gate note:** `workflow.install-drift` correctly FAILS
on machines whose user-global `~/.claude/workflows` links still point at
the old location — that is the transition it exists to catch. CI stays
green (absent→skip). **Post-merge operator step:** `cd ~/dev/agentops &&
git pull && bash scripts/install-workflows.sh`.

**Verified:** full suite 63/63 pkgs; golangci-lint clean; `gate check
--full` over this range = 66/67 with only the documented install-drift
environment finding; workflows smoke-run evidence in session logs.
2026-07-20 19:39:16 -04:00

78 lines
3.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# check-workflow-drift.sh — workflow install freshness/resolution check (ag-wi9w1).
#
# Blocking set: bdd-foundry.js
# - absent entirely -> SKIP line, exit 0 (clean machines/CI stay green)
# - dangling symlink -> exit 1 naming the offending path
# - symlink -> must realpath-resolve to the repo canonical, OR (cross-
# checkout: gate run from a worktree) resolve to a target
# byte-equal to this repo's canonical; else exit 1
# - regular file -> cmp -s against the repo canonical, else exit 1
# Report-only set: every other repo-tracked workflows/*.js
# (today: bead-crank.js, operating-loop.js) — the same comparison, but divergence
# emits 'DRIFT-REPORT: <name> ...' to stdout and NEVER affects the exit code.
#
# Repo root from cwd git; installed dir from $HOME. No hardcoded user paths.
set -euo pipefail
# shellcheck disable=SC1007,SC1091
. "$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/repo-root.sh"
repo_root="$(resolve_repo_root)"
canon_dir="$repo_root/workflows"
inst_dir="$HOME/.claude/workflows"
blocking_name="bdd-foundry.js"
resolve() { python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$1"; }
# compare_install <name>: echoes a failure detail and returns 1 on drift/dangle;
# returns 0 when absent or faithful.
compare_install() {
name="$1"
inst="$inst_dir/$name"
canon="$canon_dir/$name"
if [ ! -e "$inst" ] && [ ! -L "$inst" ]; then
return 0
fi
if [ -L "$inst" ]; then
if [ ! -e "$inst" ]; then
echo "$name installed symlink is dangling: $inst -> $(readlink "$inst")"
return 1
fi
if [ "$(resolve "$inst")" != "$(resolve "$canon")" ]; then
# Cross-checkout tolerance: the home symlink targets the MAIN checkout, so a gate run
# from a worktree sees a different realpath. Byte-equality with THIS repo's canonical
# is the real invariant — fail only when the resolved content has drifted.
if ! cmp -s "$(resolve "$inst")" "$canon"; then
echo "$name installed symlink resolves to $(resolve "$inst"), not the repo canonical $canon, and its bytes differ"
return 1
fi
fi
elif ! cmp -s "$inst" "$canon"; then
echo "$name installed copy bytes differ from the repo canonical: $inst vs $canon"
return 1
fi
return 0
}
# Blocking: bdd-foundry.js
if [ ! -e "$inst_dir/$blocking_name" ] && [ ! -L "$inst_dir/$blocking_name" ]; then
echo "SKIP: $blocking_name not installed ($inst_dir/$blocking_name absent)"
else
if ! detail="$(compare_install "$blocking_name")"; then
echo "FAIL: $detail"
exit 1
fi
echo "OK: $blocking_name installed and faithful to the repo canonical"
fi
# Report-only siblings: every other repo-tracked workflow.
while IFS= read -r tracked; do
name="$(basename "$tracked")"
[ "$name" = "$blocking_name" ] && continue
if ! detail="$(compare_install "$name")"; then
echo "DRIFT-REPORT: $detail"
fi
done < <(git -C "$repo_root" ls-files 'workflows/*.js')
exit 0