Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7bb730e
chore(sandcastle): pin hub-version to current hub SHA
arndvs Aug 20, 2026
4084550
docs(plan): architect hub-model cleanup — hub templates, QA gate, too…
arndvs Aug 20, 2026
7ca0929
feat(sandcastle): 3-way tooling merge + hub template sync tool
arndvs Aug 20, 2026
a28c9ca
docs(plan): architect workspace prime + ownership scaffold
arndvs Aug 20, 2026
6ba03a2
docs(plan): architect drift remediation — engine cleanup + sync pullback
arndvs Aug 20, 2026
67b9692
docs(cleanup): archive completed plans, fix stale docs, trim README
arndvs Aug 20, 2026
8e3cc5f
chore(sandcastle): pin hub to a02f85a
arndvs Aug 21, 2026
4a7e206
fix(producer): update model from claude-opus-4-6 to claude-opus-4-7
arndvs Sep 1, 2026
08b7c1f
chore(repo): sandcastle-hub → ctrlshft-hub (tooling, templates, ADR)
arndvs Sep 1, 2026
b8cf5a1
chore(repo): update last sandcastle-hub refs in bin/ctrl
arndvs Sep 1, 2026
3123f0e
chore(sandcastle): pin hub to 8a0a288
arndvs Sep 2, 2026
2f09881
Merge pull request #343 from arndvs/sandcastle/hub-review-20260901
arndvs Sep 2, 2026
e09f1cc
Merge remote-tracking branch 'origin/main' into dev
arndvs Sep 2, 2026
1f5ca2d
chore(templates): sync sandcastle-drift template from hub
arndvs Sep 2, 2026
5866c17
chore(templates): sync sandcastle-drift detectedAt fix from hub
arndvs Sep 2, 2026
f776497
chore(templates): sync sandcastle-drift branch-from-default fix from hub
arndvs Sep 25, 2026
21b6d5a
fix(sandcastle): re-sync installed drift workflow from hub template
arndvs Sep 25, 2026
55536db
chore(templates): sync code-health 90m timeout from hub
arndvs Sep 25, 2026
e416081
fix(guard): allow sandcastle/* pin-review PRs to main
arndvs Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/main-pr-source-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ jobs:

case "$GITHUB_BASE_REF" in
main|master)
# The drift workflow (sandcastle-drift.yml) opens sandcastle/*
# PRs that only touch .sandcastle/hub-version.json — a bot
# pin-review, not a code promotion. Allow those through; every
# other PR to main must come from dev.
if [[ "$GITHUB_HEAD_REF" == sandcastle/* ]]; then
echo "PR source accepted (sandcastle pin-review): $GITHUB_HEAD_REF -> $GITHUB_BASE_REF"
exit 0
fi
if [[ "$GITHUB_HEAD_REF" != "dev" ]]; then
echo "PRs targeting $GITHUB_BASE_REF must come from dev, not $GITHUB_HEAD_REF." >&2
echo "Use the promotion path: feature branch -> dev, then dev -> $GITHUB_BASE_REF." >&2
Expand Down
76 changes: 16 additions & 60 deletions .github/workflows/sandcastle-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,63 +40,6 @@ jobs:
echo "drifted=false" >> "$GITHUB_OUTPUT"
fi

# Hub-synced workflows must match the hub templates byte-for-byte (modulo
# render-time substitution). If a syncable workflow has drifted from the
# hub, re-sync it. Prevents silent divergence (e.g. the proxy-canary
# probe-ordering bug that made every canary run fail).
- name: Compare hub-synced workflows vs hub templates
id: wf-drift
run: |
set -euo pipefail
# Mapping: local path -> hub template path
declare -A checks=(
[".github/workflows/proxy-canary.yml"]="templates/workflows-proxy/proxy-canary.yml"
[".github/workflows/check-attribution.yml"]="templates/workflows/check-attribution.yml"
)
drifted=0
for local in "${!checks[@]}"; do
if [ ! -f "$local" ]; then
echo " MISSING $local (no local file)" | tee -a /tmp/wf-drift-list.txt
drifted=1
continue
fi
tmpl="${checks[$local]}"
curl -fsSL "https://raw.githubusercontent.com/arndvs/ctrlshft-hub/main/$tmpl" -o /tmp/hub-template 2>/dev/null || { echo " ? cannot fetch hub template $tmpl" >&2; continue; }
if ! diff -q "$local" /tmp/hub-template >/dev/null 2>&1; then
echo " DRIFTED: $local vs hub $tmpl" >> ./wf-drift-report.txt
drifted=1
else
echo " OK: $local matches hub template"
fi
done
if [ "$drifted" = "1" ]; then
echo "wf_drifted=true" >> "$GITHUB_OUTPUT"
else
echo "wf_drifted=false" >> "$GITHUB_OUTPUT"
fi

- name: Open review PR on workflow drift
if: steps.wf-drift.outputs.wf_drifted == 'true'
run: |
set -euo pipefail
branch="sandcastle/sync-workflows-$(date +%Y%m%d)"
git config user.name "claude-code[bot]"
git config user.email "claude-code[bot]@users.noreply.github.com"
git checkout -b "$branch"
for entry in ".github/workflows/proxy-canary.yml templates/workflows-proxy/proxy-canary.yml" ".github/workflows/check-attribution.yml templates/workflows/check-attribution.yml"; do
set -- $entry
local="$1"; tmpl="$2"
curl -fsSL "https://raw.githubusercontent.com/arndvs/ctrlshft-hub/main/$tmpl" -o "$local"
done
git add .github/workflows/proxy-canary.yml .github/workflows/check-attribution.yml
git commit -m "chore(sandcastle): re-sync hub-synced workflows from hub templates" || true
git push origin "$branch" || true
gh pr create -R ${{ github.repository }} \
--base main \
--head "$branch" \
--title "chore(sandcastle): re-sync hub-synced workflows" \
--body "Detected drift in hub-synced workflows. Re-synced from \`arndvs/ctrlshft-hub\` templates." || true

- name: Open review PR on drift
if: steps.drift.outputs.drifted == 'true'
env:
Expand All @@ -106,11 +49,24 @@ jobs:
branch="sandcastle/hub-review-$(date +%Y%m%d)"
git config user.name "claude-code[bot]"
git config user.email "claude-code[bot]@users.noreply.github.com"
git checkout -b "$branch"
jq --arg sha "$LATEST_SHA" '.lastPinnedSha = $sha' .sandcastle/hub-version.json > tmp.json && mv tmp.json .sandcastle/hub-version.json
# Branch from the default branch explicitly. The workflow-drift step
# (when present in a consumer copy) may have left the working tree on
# a dirty sync-workflows branch; branching from HEAD would inherit
# its unmerged workflow-file changes and fail the push with a
# workflow-scope rejection.
git checkout -b "$branch" main
# detectedAt = when the bot detected the drift (branch creation).
# reviewedAt is intentionally NOT written here — it is derived from
# the merge-commit date of this PR by scan-consumers.sh, so it can
# never claim a human signed off before the merge happened.
jq --arg sha "$LATEST_SHA" --arg now "$(date +%Y-%m-%d)" \
'.lastPinnedSha = $sha | .detectedAt = $now' \
.sandcastle/hub-version.json > tmp.json && mv tmp.json .sandcastle/hub-version.json
git add .sandcastle/hub-version.json
git commit -m "chore(sandcastle): pin hub to $LATEST_SHA"
git push origin "$branch"
# Tolerate push failures (e.g. PAT without workflow scope) so a
# scope issue degrades to a warning, not a red job.
git push origin "$branch" || true
gh pr create -R ${{ github.repository }} \
--base main \
--head "$branch" \
Expand Down
2 changes: 1 addition & 1 deletion .sandcastle/hub-version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"ref": "main",
"lastPinnedSha": "2acfac4",
"lastPinnedSha": "8a0a288",
"reviewedAt": "2026-09-01"
}
8 changes: 8 additions & 0 deletions bin/validate-main-pr-source.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ fi

case "$base_ref" in
main|master)
# The drift workflow (sandcastle-drift.yml) opens sandcastle/* PRs
# that only touch .sandcastle/hub-version.json — a bot pin-review,
# not a code promotion. Allow those through; every other PR to main
# must come from dev.
if [[ "$head_ref" == sandcastle/* ]]; then
echo "PR source accepted (sandcastle pin-review): $head_ref -> $base_ref"
exit 0
fi
if [[ "$head_ref" != "dev" ]]; then
echo "PRs targeting $base_ref must come from dev, not $head_ref." >&2
echo "Use the promotion path: feature branch -> dev, then dev -> $base_ref." >&2
Expand Down
4 changes: 3 additions & 1 deletion shft/templates/workflows/agent-code-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ permissions:
jobs:
code-health-audit:
runs-on: ubuntu-latest
timeout-minutes: 60
# 90m: the audit runs multiple lenses and the Sep 2026 hub run hit 59m15s
# against the old 60m cap, timing out with the session-resume retry loop.
timeout-minutes: 90
concurrency:
group: agent-code-health-audit
cancel-in-progress: false
Expand Down
23 changes: 20 additions & 3 deletions shft/templates/workflows/sandcastle-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
# Pass AGENT_PAT as the checkout token so the persisted git credential
# (used by `git push` later in this job) can write. The default
# GITHUB_TOKEN is contents: read here and cannot push branches.
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 1
token: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }}

- name: Compare pinned SHA vs hub latest
id: drift
Expand All @@ -45,11 +49,24 @@ jobs:
branch="sandcastle/hub-review-$(date +%Y%m%d)"
git config user.name "claude-code[bot]"
git config user.email "claude-code[bot]@users.noreply.github.com"
git checkout -b "$branch"
jq --arg sha "$LATEST_SHA" '.lastPinnedSha = $sha' .sandcastle/hub-version.json > tmp.json && mv tmp.json .sandcastle/hub-version.json
# Branch from the default branch explicitly. The workflow-drift step
# (when present in a consumer copy) may have left the working tree on
# a dirty sync-workflows branch; branching from HEAD would inherit
# its unmerged workflow-file changes and fail the push with a
# workflow-scope rejection.
git checkout -b "$branch" {{DEFAULT_BRANCH}}
# detectedAt = when the bot detected the drift (branch creation).
# reviewedAt is intentionally NOT written here — it is derived from
# the merge-commit date of this PR by scan-consumers.sh, so it can
# never claim a human signed off before the merge happened.
jq --arg sha "$LATEST_SHA" --arg now "$(date +%Y-%m-%d)" \
'.lastPinnedSha = $sha | .detectedAt = $now' \
.sandcastle/hub-version.json > tmp.json && mv tmp.json .sandcastle/hub-version.json
git add .sandcastle/hub-version.json
git commit -m "chore(sandcastle): pin hub to $LATEST_SHA"
git push origin "$branch"
# Tolerate push failures (e.g. PAT without workflow scope) so a
# scope issue degrades to a warning, not a red job.
git push origin "$branch" || true
gh pr create -R ${{ github.repository }} \
--base {{DEFAULT_BRANCH}} \
--head "$branch" \
Expand Down
2 changes: 2 additions & 0 deletions test/main-pr-source-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ if [[ -x "$GUARD" ]]; then
run_case "feature branch may target dev" pass dev ai/fix/example
run_case "dev may target master if present" pass master dev
run_case "feature branch may not target master" fail master ai/fix/example
run_case "sandcastle pin-review may target main" pass main sandcastle/hub-review-20260926
run_case "sandcastle pin-review may target master" pass master sandcastle/hub-review-20260926
fi

printf "\n \033[32m%d passed\033[0m \033[31m%d failed\033[0m\n" "$PASS" "$FAIL"
Expand Down
Loading