Files
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

68 lines
2.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# install-workflows.sh — install repo-canonical Claude workflows (workflows/)
# into $HOME/.claude/workflows as symlinks (ag-wi9w1). Project-local installs
# use `ao workflows link` instead.
#
# Usage: bash scripts/install-workflows.sh [name.js ...]
# No args: install every repo-tracked workflows/*.js.
# With args: install only the named workflows (arg-scoped).
#
# Semantics (per the canonicalize-bdd-foundry-workflow spec, C2):
# - repo root resolved from cwd git; dest dir $HOME/.claude/workflows (mkdir -p)
# - dest is a symlink (incl. dangling) -> ln -sfn to the repo canonical (idempotent)
# - dest is a byte-equal regular file -> replace with the symlink
# - dest is a divergent regular file -> cp -p backup to
# <name>.pre-canonicalize-<UTC-ts> (path printed), then ln -sfn
# - dest absent -> ln -sfn
# Touches ONLY $HOME; never writes into the repo. Exits non-zero on any real failure.
set -euo pipefail
shopt -s lastpipe 2>/dev/null || true
umask 022
# shellcheck disable=SC1007,SC1091
. "$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/repo-root.sh"
repo_root="$(resolve_repo_root)"
src_dir="$repo_root/workflows"
dest_dir="$HOME/.claude/workflows"
mkdir -p "$dest_dir"
if [ "$#" -eq 0 ]; then
for f in "$src_dir"/*.js; do
[ -e "$f" ] || continue
set -- "$@" "$(basename "$f")"
done
fi
status=0
for name in "$@"; do
src="$src_dir/$name"
dest="$dest_dir/$name"
if [ ! -f "$src" ]; then
echo "ERROR: no such workflow in repo: $name ($src)" >&2
status=1
continue
fi
if [ -L "$dest" ]; then
ln -sfn "$src" "$dest"
echo "installed (symlink refreshed): $dest -> $src"
elif [ -f "$dest" ]; then
if cmp -s "$dest" "$src"; then
ln -sfn "$src" "$dest"
echo "installed (byte-equal copy replaced with symlink): $dest -> $src"
else
backup="$dest.pre-canonicalize-$(date -u +%Y%m%dT%H%M%SZ)"
cp -p "$dest" "$backup"
echo "backup of divergent local file: $backup"
ln -sfn "$src" "$dest"
echo "installed (divergent copy backed up, then symlinked): $dest -> $src"
fi
elif [ -e "$dest" ]; then
echo "ERROR: $dest exists and is neither a regular file nor a symlink; refusing" >&2
status=1
else
ln -sfn "$src" "$dest"
echo "installed (new symlink): $dest -> $src"
fi
done
exit "$status"