Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
75254bf
build(agents): scaffold reproducible session bootstrap
Sir-NoChill Jul 22, 2026
223c45d
build(agents)!: drop identity prescription and generated artifacts
Sir-NoChill Aug 9, 2026
40206c1
build(agents): bundle spec-review and grammar-consistency skills
Sir-NoChill Aug 9, 2026
1fe0a7a
build(agents): bundle CI-parity check script in spec-review skill
Sir-NoChill Aug 9, 2026
6e2bbba
chore(agents): point .agents/ at the DocsDev image and rewrite skills
Sir-NoChill Aug 9, 2026
136258b
docs(readme): point docs-dev link at ci-utils repo root
Sir-NoChill Aug 10, 2026
66ae45f
build(agents): commit the uv lockfile
Sir-NoChill Aug 15, 2026
e4e0938
fix(gazprea): replace undefined syntax in examples
Sir-NoChill Jul 22, 2026
4659c65
fix(gazprea): use .len() on strings and rename shadowy struct fields
Sir-NoChill Aug 9, 2026
a00d64f
fix(gazprea): disambiguate the Another struct example field names
Sir-NoChill Aug 9, 2026
78f70b5
fix(gazprea): state the vector element-type set
Sir-NoChill Jul 22, 2026
30b53d6
fix(gazprea): align vector element-type list with the rest of the spec
Sir-NoChill Aug 9, 2026
e3fac8a
fix(gazprea): scope the vector-array equivalence claim
Sir-NoChill Jul 22, 2026
a8abfb8
fix(gazprea): keep vector/array claims consistent between intro and body
Sir-NoChill Aug 9, 2026
3f60040
fix(gazprea): leave headroom in the vector/array differences enumeration
Sir-NoChill Aug 9, 2026
8ecb506
fix(gazprea): distinguish range slices from array-valued indices
Sir-NoChill Jul 22, 2026
d1141e9
fix(gazprea): reconcile the array indexing rule with the slice form
Sir-NoChill Aug 9, 2026
2bb4028
fix(gazprea): scope the array-value index rule to any array-valued ex…
Sir-NoChill Aug 9, 2026
9ee8d75
fix(gazprea): scope the promotion-implies-cast claim
Sir-NoChill Jul 22, 2026
9939f81
fix(gazprea): drop stdlib "shape" noun and trim redundant clause
Sir-NoChill Aug 9, 2026
359877f
fix(gazprea): label the Character-Array/String section and reference it
Sir-NoChill Aug 9, 2026
1419e94
refactor(gazprea): single home for operator precedence
Sir-NoChill Jul 22, 2026
a402394
refactor(gazprea): flatten the precedence indirection and restore the…
Sir-NoChill Aug 9, 2026
479ffd5
refactor(gazprea): single home for stream_state semantics
Sir-NoChill Jul 22, 2026
43ce124
refactor(gazprea): single home for const-by-default rule
Sir-NoChill Jul 22, 2026
f4364ac
refactor(gazprea): state the "both spellings legal" sub-rule and clos…
Sir-NoChill Aug 9, 2026
c34d3bc
fix(gazprea): name error classes at anonymous error sites
Sir-NoChill Jul 22, 2026
7b85dad
fix(gazprea): add missing sec:errors cross-refs at first-mentions
Sir-NoChill Aug 8, 2026
5fbbb07
fix(gazprea): fill omitted error sites and unify on "must emit"
Sir-NoChill Aug 9, 2026
be54499
fix(gazprea): tighten "must emit" prose per human-review pass
Sir-NoChill Aug 9, 2026
868c3ad
fix(gazprea): specify combined struct-plus-instance declaration
Sir-NoChill Jul 22, 2026
f842a18
fix(gazprea): state the mixed concatenation result type
Sir-NoChill Jul 22, 2026
70a16b0
fix(gazprea): tuple member conversions cover array members
Sir-NoChill Jul 22, 2026
3c2d935
fix(gazprea): close two silent contract gaps
Sir-NoChill Jul 22, 2026
c8be9f9
fix(gazprea): unify slice bound rule in one section
Sir-NoChill Jul 22, 2026
a680b7a
fix(gazprea): remove struct field forbidden by struct's own rule
Sir-NoChill Jul 22, 2026
20fab59
fix(gazprea): reconcile procedure call-site restatement
Sir-NoChill Jul 22, 2026
ffde84f
feat(gazprea): specify the method-call surface
Sir-NoChill Jul 22, 2026
14ebb1c
fix(gazprea): correct misleading examples (batched)
Sir-NoChill Jul 22, 2026
f21f38c
spec(gazprea): permit nested aggregates and n-d arrays
Sir-NoChill Aug 21, 2026
07a417f
spec(gazprea): add initialization-time sizing, drop the by operator
Sir-NoChill Aug 21, 2026
b5bf29b
spec(gazprea): reword implicit casts, add array/vector casting
Sir-NoChill Aug 21, 2026
54d22f5
spec(gazprea): make string a vector typealias, methods are procedures
Sir-NoChill Aug 21, 2026
0105a03
spec(gazprea): thread initialization-time sizing through core chapters
Sir-NoChill Aug 21, 2026
3fe3b1b
spec(gazprea): specify array/vector parameter sizing and call positions
Sir-NoChill Aug 21, 2026
9d50acb
spec(gazprea): struct qualifiers, drop StrideError, fix zero values
Sir-NoChill Aug 21, 2026
bb0c484
spec(gazprea): apply review pass 1 findings
Sir-NoChill Aug 21, 2026
eaf3f05
spec(gazprea): resolve design questions and pass-2 findings
Sir-NoChill Aug 22, 2026
99fd943
spec(gazprea): final convergence-review fixes
Sir-NoChill Aug 22, 2026
cba814f
spec(gazprea): rename promotion/typedef chapters and fix casts
Sir-NoChill Aug 23, 2026
05a2f74
spec(gazprea): half-open slice views and ** by rank
Sir-NoChill Aug 23, 2026
38a465d
spec(gazprea): vector methods, string alias, struct/tuple rules
Sir-NoChill Aug 23, 2026
9722b83
spec(gazprea): scalar types and casting semantics
Sir-NoChill Aug 23, 2026
603f543
spec(gazprea): functions, procedures, statements, expressions
Sir-NoChill Aug 23, 2026
0ae185f
spec(gazprea): declarations, constexpr, globals, namespaces
Sir-NoChill Aug 23, 2026
4244a1b
spec(gazprea): streams, built-ins, glossary consistency
Sir-NoChill Aug 23, 2026
fa0ecb3
style(gazprea): reflow prose to 79 columns
Sir-NoChill Aug 23, 2026
14477cb
style(gazprea): multi-line term
Sir-NoChill Aug 23, 2026
2529864
style(gazprea): clarify contiguous blob for n-d array
Sir-NoChill Aug 23, 2026
3ad7dfb
style(gazprea): clarify -ffast-math behaviour
Sir-NoChill Aug 23, 2026
ce82fc9
style(gazprea): multi-line term
Sir-NoChill Aug 23, 2026
c3192f0
docs(gazprea): resolve consolidated-review spec ambiguities
Sir-NoChill Aug 24, 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
112 changes: 112 additions & 0 deletions .agents/bootstrap.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# {{ project }} session bootstrap -- rendered from bootstrap.sh.tmpl.
# Edit .agents/manifest.yaml and re-render; do not edit this file directly.
set -euo pipefail

AGENTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATE_DIR="${AGENTS_DIR}/state"
mkdir -p "${STATE_DIR}"
cd "${AGENTS_DIR}/.."

log() { printf '[bootstrap] %s\n' "$*"; }
fail() { printf '[bootstrap] ERROR: %s\n' "$*" >&2; exit 1; }

# ---------------------------------------------------------------- apt packages
export DEBIAN_FRONTEND=noninteractive

APT_PACKAGES=(
{% for pkg in apt_packages %}
"{{ pkg.name }}{% if pkg.version %}={{ pkg.version }}{% endif %}"
{% endfor %}
)

SUDO=""
if [[ "$(id -u)" -ne 0 ]]; then
command -v sudo >/dev/null || fail "not root and sudo unavailable"
SUDO="sudo"
fi

missing=()
for spec in "${APT_PACKAGES[@]}"; do
name="${spec%%=*}"
if ! dpkg-query -W -f='${Status}' "${name}" 2>/dev/null | grep -q 'install ok installed'; then
missing+=("${spec}")
fi
done

if ((${#missing[@]})); then
log "installing: ${missing[*]}"
${SUDO} apt-get update -q || log "apt-get update warning; attempting install anyway"
${SUDO} apt-get install -q -y --no-install-recommends "${missing[@]}"
else
log "apt dependencies already satisfied"
fi

# ------------------------------------------------------------------------ uv
if ! command -v uv >/dev/null; then
log "installing uv"
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="${HOME}/.local/bin:${PATH}"
command -v uv >/dev/null || fail "uv install completed but binary not on PATH"
fi
log "uv $(uv --version | awk '{print $2}')"

(cd "${AGENTS_DIR}/.." && uv sync)

# ------------------------------------------------------- optional signing key
# Only mints a key when the manifest names an identity. Otherwise signing is
# left to whatever the user has configured in git.
AGENT_GIT_NAME="{{ agent.name or '' }}"
AGENT_GIT_EMAIL="{{ agent.email or '' }}"

if [[ -n "${AGENT_GIT_NAME}" && -n "${AGENT_GIT_EMAIL}" ]]; then
AGENT_GNUPGHOME="${AGENTS_DIR}/gnupg"
AGENT_UID="${AGENT_GIT_NAME} <${AGENT_GIT_EMAIL}>"

live_fpr() {
gpg --homedir "${AGENT_GNUPGHOME}" --list-secret-keys --with-colons "${AGENT_GIT_EMAIL}" 2>/dev/null \
| awk -F: '$1=="sec" && $2!="e" && $2!="r" {take=1; next}
take && $1=="fpr" {print $10; exit}'
}

fpr="$(live_fpr || true)"
if [[ -z "${fpr}" ]]; then
log "generating agent key for ${AGENT_UID}"
rm -rf "${AGENT_GNUPGHOME}"
mkdir -p "${AGENT_GNUPGHOME}" && chmod 700 "${AGENT_GNUPGHOME}"
gpg --homedir "${AGENT_GNUPGHOME}" --batch --pinentry-mode loopback --passphrase '' \
--quick-generate-key "${AGENT_UID}" ed25519 sign "{{ agent.key_expiry or '1w' }}"
fpr="$(live_fpr)"
[[ -n "${fpr}" ]] || fail "key generation produced no usable secret key"
else
log "reusing agent key ${fpr}"
fi

cat > "${AGENTS_DIR}/agent-env.sh" <<EOF
export AGENT_GIT_NAME="${AGENT_GIT_NAME}"
export AGENT_GIT_EMAIL="${AGENT_GIT_EMAIL}"
export AGENT_SIGNING_KEY="${fpr}"
export AGENT_GNUPGHOME="${AGENT_GNUPGHOME}"
EOF
chmod 600 "${AGENTS_DIR}/agent-env.sh"
log "agent identity ready; source ${AGENTS_DIR}/agent-env.sh"
else
log "no agent.name/email in manifest; skipping GPG key generation"
rm -f "${AGENTS_DIR}/agent-env.sh"
fi

# ------------------------------------------------------------------ baselines
sha256sum "${AGENTS_DIR}/manifest.yaml" | awk '{print $1}' > "${STATE_DIR}/manifest.sha256"

: > "${STATE_DIR}/dpkg-versions.txt"
for spec in "${APT_PACKAGES[@]}"; do
dpkg-query -W -f='${Package} ${Version}\n' "${spec%%=*}" >> "${STATE_DIR}/dpkg-versions.txt"
done

{
{% for s in sentinels %}
sha256sum "{{ s }}"
{% endfor %}
} > "${STATE_DIR}/sentinels.sha256"

log "bootstrap complete"
52 changes: 52 additions & 0 deletions .agents/check.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# {{ project }} dependency validation -- rendered from check.sh.tmpl.
# Edit .agents/manifest.yaml and re-render; do not edit this file directly.
set -euo pipefail

AGENTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
failures=0
flag() { printf '[check] FAIL: %s\n' "$*" >&2; failures=$((failures + 1)); }
ok() { printf '[check] ok: %s\n' "$*"; }

# dpkg state proves the package installed; a runnable command on PATH proves
# it usable -- either alone gives false confidence [arslan2019]
{% for pkg in apt_packages %}
if dpkg-query -W -f='${Status}' "{{ pkg.name }}" 2>/dev/null | grep -q 'install ok installed'; then
ok "package {{ pkg.name }}"
else
flag "package {{ pkg.name }} not installed"
fi
{% endfor %}

{% for c in commands %}
if command -v "{{ c.cmd }}" >/dev/null; then
ok "command {{ c.cmd }} ($("{{ c.cmd }}" {{ c.version_flag }} 2>&1 | head -n 1))"
else
flag "command {{ c.cmd }} missing from PATH (expected via {{ c.package }})"
fi
{% endfor %}

# Signing identity is optional. Only enforce agent-env.sh when the manifest
# named an agent identity for this repo.
{% if agent.name and agent.email %}
if [[ -r "${AGENTS_DIR}/agent-env.sh" ]]; then
# shellcheck source=/dev/null
source "${AGENTS_DIR}/agent-env.sh"
if gpg --homedir "${AGENT_GNUPGHOME}" --list-secret-keys --with-colons "${AGENT_SIGNING_KEY}" 2>/dev/null \
| awk -F: '$1=="sec" && $2!="e" && $2!="r" {found=1} END {exit !found}'; then
ok "agent signing key ${AGENT_SIGNING_KEY}"
else
flag "agent signing key ${AGENT_SIGNING_KEY} missing, expired, or revoked"
fi
else
flag "agent-env.sh missing; run .agents/bootstrap.sh"
fi
{% else %}
ok "no agent identity configured; signing check skipped"
{% endif %}

if ((failures)); then
printf '[check] %d failure(s); run .agents/bootstrap.sh or fix the manifest\n' "${failures}" >&2
exit 1
fi
printf '[check] all dependencies validated\n'
89 changes: 89 additions & 0 deletions .agents/healthcheck.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# {{ project }} environment healthcheck -- rendered from healthcheck.sh.tmpl.
# Edit .agents/manifest.yaml and re-render; do not edit this file directly.
#
# Compares the live environment against the baselines bootstrap.sh recorded,
# the IaC drift-detection pattern: desired state is code, drift is divergence
# from it [hashicorp-drift]. Verdict contract (machine-readable last line):
# HEALTHCHECK: ok exit 0 environment matches baselines
# HEALTHCHECK: drift exit 1 recoverable divergence; re-render/re-bootstrap
# HEALTHCHECK: corrupt exit 2 integrity violation; stop and tell the user
set -uo pipefail

AGENTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATE_DIR="${AGENTS_DIR}/state"
cd "${AGENTS_DIR}/.." || { echo "HEALTHCHECK: corrupt"; exit 2; }

verdict=0
note() { printf '[healthcheck] %s\n' "$*"; }
drift() { note "DRIFT: $*"; ((verdict < 1)) && verdict=1; }
corrupt() { note "CORRUPT: $*"; verdict=2; }

if [[ ! -d "${STATE_DIR}" ]]; then
corrupt "no recorded baselines; .agents/bootstrap.sh has never completed here"
fi

# Manifest changed since the scripts were rendered => scripts are stale
if [[ -f "${STATE_DIR}/manifest.sha256" ]]; then
current="$(sha256sum "${AGENTS_DIR}/manifest.yaml" | awk '{print $1}')"
recorded="$(cat "${STATE_DIR}/manifest.sha256")"
[[ "${current}" == "${recorded}" ]] \
|| drift "manifest.yaml changed after last bootstrap; re-render *.tmpl and re-run bootstrap"
fi

# Package set drift: removed packages are corruption, version churn is drift
{% for pkg in apt_packages %}
if ! dpkg-query -W -f='${Status}' "{{ pkg.name }}" 2>/dev/null | grep -q 'install ok installed'; then
corrupt "required package {{ pkg.name }} is no longer installed"
fi
{% endfor %}
if [[ -f "${STATE_DIR}/dpkg-versions.txt" ]]; then
while read -r name recorded_ver; do
live_ver="$(dpkg-query -W -f='${Version}' "${name}" 2>/dev/null || true)"
[[ "${live_ver}" == "${recorded_ver}" ]] \
|| drift "package ${name} moved ${recorded_ver} -> ${live_ver:-<absent>} since bootstrap"
done < "${STATE_DIR}/dpkg-versions.txt"
fi

# Sentinel files: silent mutation of the environment's own machinery
if [[ -f "${STATE_DIR}/sentinels.sha256" ]]; then
if ! sha256sum --check --quiet "${STATE_DIR}/sentinels.sha256" 2>/dev/null; then
corrupt "sentinel file checksum mismatch (see: sha256sum --check ${STATE_DIR}/sentinels.sha256)"
fi
fi

# Bundled skills: each listed name must exist under .agents/skills/ with a
# non-empty SKILL.md. These are checked-in review helpers, not workspace
# copies of external skill libraries.
{% for s in skills %}
if [[ ! -s "${AGENTS_DIR}/skills/{{ s }}/SKILL.md" ]]; then
drift "bundled skill '{{ s }}' missing or empty at .agents/skills/{{ s }}/SKILL.md"
fi
{% endfor %}

# Agent key: only enforced when the manifest configured an identity
{% if agent.name and agent.email %}
if [[ -r "${AGENTS_DIR}/agent-env.sh" ]]; then
# shellcheck source=/dev/null
source "${AGENTS_DIR}/agent-env.sh"
gpg --homedir "${AGENT_GNUPGHOME}" --list-secret-keys --with-colons "${AGENT_SIGNING_KEY}" 2>/dev/null \
| awk -F: '$1=="sec" && $2!="e" && $2!="r" {found=1} END {exit !found}' \
|| drift "agent signing key unusable (likely expired); re-run bootstrap to mint a fresh one"
else
drift "agent-env.sh missing; re-run bootstrap"
fi
{% endif %}

# Project-specific indicators from the manifest
{% for c in extra_checks %}
if ! bash -c '{{ c.cmd }}' >/dev/null 2>&1; then
drift "extra check failed: {{ c.name }}"
fi
{% endfor %}

case "${verdict}" in
0) echo "HEALTHCHECK: ok" ;;
1) echo "HEALTHCHECK: drift" ;;
*) echo "HEALTHCHECK: corrupt" ;;
esac
exit "${verdict}"
66 changes: 66 additions & 0 deletions .agents/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Render context for .agents/*.tmpl and desired state for healthcheck.sh.
# Committed with a blank agent identity; users fill it in locally (or leave
# it blank to skip signing entirely). Identity is not prescribed by this
# repo.

project: 415-docs

# Preferred way to run a review session: pull the DocsDev image maintained
# in cmput415/ci-utils. It bundles sphinx, latexmk, texlive, lychee, act,
# uv, graphviz, and gnupg, so the apt_packages/commands blocks below are
# only exercised when running natively without the image.
docker:
image: ghcr.io/cmput415/docs-dev:latest
source: https://github.com/cmput415/ci-utils/tree/main/DocsDev

# apt packages the bootstrap installs when running natively. Python
# packages are managed separately by uv (see `pyproject.toml`).
apt_packages:
- name: gnupg
version: null
- name: graphviz
version: null
- name: curl
version: null

# Runnable-on-PATH checks. `uv` is bootstrapped by the install script when
# missing, so it belongs in commands rather than apt_packages.
commands:
- cmd: gpg
package: gnupg
version_flag: --version
- cmd: dot
package: graphviz
version_flag: -V
- cmd: uv
package: uv
version_flag: --version

# Signing identity for `git agent-commit`. Blank by default -- fill in
# locally to opt in; leave blank to skip GPG entirely. Not prescribed.
agent:
name: null
email: null
key_expiry: 1w

# Skills bundled with this repo for review/consistency work over the spec.
# Each entry names a directory under `.agents/skills/` that contains a
# SKILL.md file. The healthcheck asserts these directories still exist.
skills:
- spec-review
- grammar-consistency

sentinels:
- .agents/bootstrap.sh.tmpl
- .agents/check.sh.tmpl
- .agents/healthcheck.sh.tmpl
- .agents/render.py
- .agents/manifest.yaml
- pyproject.toml

# Project-specific readiness probes evaluated by healthcheck.
extra_checks:
- name: inside the 415-docs repo
cmd: git rev-parse --is-inside-work-tree
- name: sphinx importable from the uv-managed venv
cmd: uv run python -c "import sphinx, sys; sys.exit(0 if sphinx.__version__.startswith(\"6.2\") else 1)"
44 changes: 44 additions & 0 deletions .agents/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Render a Jinja2 template against a YAML manifest.

Usage: render.py MANIFEST.yaml TEMPLATE.tmpl > OUTPUT

Dependencies (`jinja2`, `PyYAML`) are declared in the repo's pyproject.toml
and provisioned by `uv sync`; run this script under `uv run` from a fresh
checkout so those are guaranteed available.
"""
from __future__ import annotations

import sys
from pathlib import Path

import jinja2
import yaml


def render(manifest_path: Path, template_path: Path) -> str:
scope = yaml.safe_load(manifest_path.read_text())
if not isinstance(scope, dict):
raise ValueError("manifest must be a mapping at the top level")
# Comment delimiter is remapped away from `{# #}` because the default
# collides with bash array-length syntax `${#name[@]}` in shell templates.
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(template_path.parent)),
undefined=jinja2.StrictUndefined,
keep_trailing_newline=True,
comment_start_string="{##",
comment_end_string="##}",
)
return env.get_template(template_path.name).render(scope)


def main(argv: list[str]) -> int:
if len(argv) != 3:
print(__doc__, file=sys.stderr)
return 2
sys.stdout.write(render(Path(argv[1]), Path(argv[2])))
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Loading
Loading