diff --git a/AGENTS.md b/AGENTS.md index a8410e9..20d2119 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -400,11 +400,19 @@ Standing Order #3 (vault hygiene) is **in-session, not 'later'**. Every "later" ### Overrides of Harness Defaults (non-negotiable) -These rules **counter agent harness defaults** that would otherwise silently win at runtime (e.g. a CLI whose default appends `Co-Authored-By` to commits). They are re-affirmed here because a default not explicitly overridden is the default that ships. Canonical source: `00_meta/patterns/pattern-git-workflow.md` Β§6–§7. *(Hand-maintained today; HARNESS-001 (#156) will generate this block from the pattern.)* - -* **No AI attribution.** No `Co-Authored-By` trailers, no "Generated with" / "πŸ€–" footers, no bot-style emojis β€” in commits, PR descriptions, or issue comments. All git/GitHub artifacts must appear human-authored. -* **English only** in git/GitHub artifacts: commit messages, branch names, PR/issue titles and bodies, and code comments. (Conversation with the user may be in any language; the durable record is English.) -* **No internal phase/milestone references** in branch names, commit messages, or PR titles (e.g. `phase-3.1`). Phase tracking lives in the issue tracker, not in git history. +These rules **counter agent harness defaults** that would otherwise silently win at runtime (e.g. a CLI whose default appends `Co-Authored-By` to commits). They are re-affirmed here because a default not explicitly overridden is the default that ships. Canonical source: `00_meta/patterns/pattern-git-workflow.md` Β§6–§8. *(Generated by the HARNESS engine via `scripts/compile-harness.sh` β€” edit the vault pattern, then re-run setup. Do NOT edit between the markers.)* + + +- **No AI attribution** in git history or GitHub messages (commits, PRs, issues). +- No `Co-Authored-By` trailers referencing AI agents. +- No bot-style emojis or "Generated with" footers. +- All artifacts must appear human-authored. +- **English only** in git/GitHub artifacts: commit messages, branch names, PR/issue titles and bodies, and code comments. Conversation with the user may be in any language; the durable record is English. +- **No internal phase/milestone references** in branch names, commit messages, or PR titles. + - Bad: `feat/phase-3.1-scaffold`, `chore: scaffold repo (Phase 3.1)` + - Good: `feat/scaffold-pyhydra3d`, `chore: scaffold PyHydra3D repository` +- Phase tracking belongs in the vault backlog (`11-tasks.md`), not in git history. + ### Interaction Discipline diff --git a/ai/claude/CLAUDE.md b/ai/claude/CLAUDE.md index dcdfbb0..df3b131 100644 --- a/ai/claude/CLAUDE.md +++ b/ai/claude/CLAUDE.md @@ -53,7 +53,18 @@ For the full dual-memory protocol, query `00_meta/patterns/pattern-dual-memory.m ## Claude Code Tooling Notes -* **No attribution / English-only (overrides Claude default).** Per `AGENTS.md` Β§ Operational Rules β†’ "Overrides of Harness Defaults": no `Co-Authored-By` / "Generated with" / πŸ€– footers on commits, PRs, or issues; English only in git/GitHub artifacts β€” even when the harness default suggests otherwise. +* **Overrides of harness defaults (generated).** Sourced from the vault via `scripts/compile-harness.sh` β€” edit the vault pattern + re-run setup, not here: + +- **No AI attribution** in git history or GitHub messages (commits, PRs, issues). +- No `Co-Authored-By` trailers referencing AI agents. +- No bot-style emojis or "Generated with" footers. +- All artifacts must appear human-authored. +- **English only** in git/GitHub artifacts: commit messages, branch names, PR/issue titles and bodies, and code comments. Conversation with the user may be in any language; the durable record is English. +- **No internal phase/milestone references** in branch names, commit messages, or PR titles. + - Bad: `feat/phase-3.1-scaffold`, `chore: scaffold repo (Phase 3.1)` + - Good: `feat/scaffold-pyhydra3d`, `chore: scaffold PyHydra3D repository` +- Phase tracking belongs in the vault backlog (`11-tasks.md`), not in git history. + * **Skills.** `~/.claude/skills//SKILL.md` auto-load via slash commands. Skill auto-loading is a Claude Code feature, not portable. Skill **content** is portable: `AI-012-opencode-commands-port` mechanically transforms each skill into an OpenCode command in `ai/opencode/commands/*.md`. * **TaskCreate / TaskUpdate / TaskList.** Use for non-trivial multi-step work (β‰₯3 distinct steps). Mark `in_progress` BEFORE starting; mark `completed` immediately on finish. Don't batch updates. * **AskUserQuestion.** Use for branching decisions with 2-4 mutually exclusive options. Always include "(Recommended)" on the preferred option. diff --git a/harness/enforced/english-only.md b/harness/enforced/english-only.md new file mode 100644 index 0000000..9d1ddd0 --- /dev/null +++ b/harness/enforced/english-only.md @@ -0,0 +1 @@ +- **English only** in git/GitHub artifacts: commit messages, branch names, PR/issue titles and bodies, and code comments. Conversation with the user may be in any language; the durable record is English. diff --git a/harness/enforced/no-attribution.md b/harness/enforced/no-attribution.md new file mode 100644 index 0000000..052baa6 --- /dev/null +++ b/harness/enforced/no-attribution.md @@ -0,0 +1,4 @@ +- **No AI attribution** in git history or GitHub messages (commits, PRs, issues). +- No `Co-Authored-By` trailers referencing AI agents. +- No bot-style emojis or "Generated with" footers. +- All artifacts must appear human-authored. diff --git a/harness/enforced/no-phase-references.md b/harness/enforced/no-phase-references.md new file mode 100644 index 0000000..6b5402d --- /dev/null +++ b/harness/enforced/no-phase-references.md @@ -0,0 +1,4 @@ +- **No internal phase/milestone references** in branch names, commit messages, or PR titles. + - Bad: `feat/phase-3.1-scaffold`, `chore: scaffold repo (Phase 3.1)` + - Good: `feat/scaffold-pyhydra3d`, `chore: scaffold PyHydra3D repository` +- Phase tracking belongs in the vault backlog (`11-tasks.md`), not in git history. diff --git a/harness/manifest.json b/harness/manifest.json new file mode 100644 index 0000000..07e8517 --- /dev/null +++ b/harness/manifest.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "vault_subpath": "00_meta/patterns", + "enforced": [ + { "id": "no-attribution", "source": "pattern-git-workflow.md#6-attribution-policy" }, + { "id": "english-only", "source": "pattern-git-workflow.md#8-language-policy" }, + { "id": "no-phase-references", "source": "pattern-git-workflow.md#7-message-content-policy" } + ], + "targets": [ + { + "agent": "agents", + "kind": "native", + "file": "AGENTS.md", + "inject": ["no-attribution", "english-only", "no-phase-references"] + }, + { + "agent": "claude", + "kind": "pointer", + "file": "ai/claude/CLAUDE.md", + "inject": ["no-attribution", "english-only", "no-phase-references"] + } + ] +} diff --git a/scripts/compile-harness.sh b/scripts/compile-harness.sh new file mode 100755 index 0000000..f700c31 --- /dev/null +++ b/scripts/compile-harness.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# compile-harness.sh β€” agent-artifact deploy engine (ENGINE-001 / HARNESS-001 #162). +# +# Compiles enforced vault patterns into a marker-delimited "Overrides of Harness +# Defaults" block inside the deployed agent instruction files. The SSOT is the +# vault (00_meta/patterns); the generated output is COMMITTED so CI and machines +# without the vault can still verify it (ADR-013, builds on ADR-012). +# +# Modes: +# --refresh vault section -> harness/enforced/.md (source-of-record) +# -> injected, marker-delimited, into each target. +# Requires the vault. Asserts per-file line caps. Run by setup. +# --check offline: render from harness/enforced/.md and diff against +# each target's region. NO vault access. Used by CI / healthcheck. +# --help +# +# Spec: specs/ENGINE-001-deploy-engine-core/proposal.md +set -euo pipefail + +BEGIN_PREFIX='' + +usage() { + cat <&2 + exit 1 +fi +MANIFEST="$REPO_ROOT/harness/manifest.json" +RECORD_DIR="$REPO_ROOT/harness/enforced" +VAULT_PATH="${VAULT_PATH:-$HOME/Projects/knowledge}" + +require_tools() { + command -v jq >/dev/null 2>&1 || { printf '[ERROR] jq is required\n' >&2; exit 2; } +} + +# --- helpers --- + +# Extract a markdown section body by GitHub-style heading slug. +# Args: . Prints the section body (heading excluded). +extract_section() { + local file="$1" want="$2" out + out="$(awk -v want="$want" ' + function slug(s){ s=tolower(s); gsub(/[^a-z0-9 -]/,"",s); gsub(/ +/,"-",s); gsub(/-+/,"-",s); return s } + /^#{1,6} / { + if (cap) { exit } + hdr=$0; sub(/^#{1,6} +/,"",hdr) + if (slug(hdr)==want) { cap=1; next } + } + cap { print } + ' "$file")" + if [[ -z "$out" ]]; then + printf '[ERROR] section "%s" not found (or empty) in %s\n' "$want" "$file" >&2 + return 1 + fi + printf '%s\n' "$out" +} + +# Validate a target has exactly one well-ordered BEGIN/END marker pair. +validate_markers() { + local file="$1" b e bl el + b="$(grep -c "^$BEGIN_PREFIX" "$file" 2>/dev/null || true)" + e="$(grep -cF "$END_MARKER" "$file" 2>/dev/null || true)" + if [[ "$b" != "1" || "$e" != "1" ]]; then + printf '[ERROR] %s: need exactly 1 BEGIN + 1 END HARNESS marker (found %s/%s)\n' "$file" "$b" "$e" >&2 + return 1 + fi + bl="$(grep -n "^$BEGIN_PREFIX" "$file" | head -1 | cut -d: -f1)" + el="$(grep -nF "$END_MARKER" "$file" | head -1 | cut -d: -f1)" + if [[ "$bl" -ge "$el" ]]; then + printf '[ERROR] %s: END marker precedes BEGIN marker\n' "$file" >&2 + return 1 + fi +} + +# Print the content strictly between the markers of a target file. +region_content() { + awk -v b="$BEGIN_PREFIX" -v e="$END_MARKER" ' + index($0,b)==1 {inreg=1; next} + $0==e {inreg=0; next} + inreg {print} + ' "$1" +} + +# Render the expected region for a target: concat of record files, in order. +# Args: id... . Prints to stdout. Fails if a record is missing. +render_region() { + local id + for id in "$@"; do + if [[ ! -f "$RECORD_DIR/$id.md" ]]; then + printf '[ERROR] missing source-of-record: harness/enforced/%s.md (run --refresh)\n' "$id" >&2 + return 1 + fi + cat "$RECORD_DIR/$id.md" + done +} + +# Per-file deployed line cap (0 = no cap). +cap_for() { + case "$1" in + ai/claude/CLAUDE.md) printf '100' ;; + ai/agy/AGY.md) printf '50' ;; + *) printf '0' ;; + esac +} + +# Replace a target's managed region with new content + a fresh BEGIN marker. +# Args: +replace_region() { + local file="$1" begin_marker="$2" content_file="$3" tmp rc=0 + tmp="$(mktemp)" + if awk -v beginm="$begin_marker" -v endm="$END_MARKER" -v bp="$BEGIN_PREFIX" -v cf="$content_file" ' + index($0,bp)==1 { + print beginm + while ((getline l < cf) > 0) print l + close(cf) + skip=1; next + } + $0==endm { if (skip){ print; skip=0; next } } + skip { next } + { print } + ' "$file" > "$tmp"; then rc=0; else rc=$?; fi + if [[ $rc -ne 0 ]]; then rm -f "$tmp"; return $rc; fi + mv "$tmp" "$file" +} + +sha_of() { sha256sum "$1" | cut -c1-16; } + +target_inject() { jq -r --arg f "$1" '.targets[] | select(.file==$f) | .inject[]' "$MANIFEST"; } + +# --- modes --- + +do_refresh() { + require_tools + local vsub pat_dir + vsub="$(jq -r '.vault_subpath' "$MANIFEST")" + pat_dir="$VAULT_PATH/$vsub" + if [[ ! -d "$pat_dir" ]]; then + cat >&2 < clone the vault to \$VAULT_PATH (default ~/Projects/knowledge), or set VAULT_PATH. + ( --check works offline and needs no vault. ) +EOF + exit 2 + fi + + mkdir -p "$RECORD_DIR" + + # 1. vault section -> source-of-record per enforced rule + local id source pat slug body + while IFS=$'\t' read -r id source; do + pat="${source%%#*}"; slug="${source#*#}" + body="$(extract_section "$pat_dir/$pat" "$slug")" + printf '%s\n' "$body" > "$RECORD_DIR/$id.md" + printf '[refresh] record: harness/enforced/%s.md <- %s#%s\n' "$id" "$pat" "$slug" + done < <(jq -r '.enforced[] | "\(.id)\t\(.source)"' "$MANIFEST") + + # 2. inject into each target + local file ids tmpc sha begin cap lines + while IFS= read -r file; do + mapfile -t ids < <(target_inject "$file") + validate_markers "$REPO_ROOT/$file" + tmpc="$(mktemp)" + render_region "${ids[@]}" > "$tmpc" + sha="$(sha_of "$tmpc")" + begin="$BEGIN_PREFIX (sha256:$sha) β€” SSOT: vault $vsub; edit there + re-run setup, do NOT edit between markers -->" + replace_region "$REPO_ROOT/$file" "$begin" "$tmpc" + rm -f "$tmpc" + cap="$(cap_for "$file")" + if [[ "$cap" != "0" ]]; then + lines="$(wc -l < "$REPO_ROOT/$file")" + if [[ "$lines" -gt "$cap" ]]; then + printf '[ERROR] %s is %s lines after injection (cap %s)\n' "$file" "$lines" "$cap" >&2 + exit 1 + fi + fi + printf '[refresh] injected -> %s (%s)\n' "$file" "$(wc -l < "$REPO_ROOT/$file" | tr -d ' ')L" + done < <(jq -r '.targets[].file' "$MANIFEST") + + printf '[refresh] OK\n' +} + +do_check() { + require_tools + local file ids drift=0 expected actual + while IFS= read -r file; do + if ! validate_markers "$REPO_ROOT/$file"; then drift=1; continue; fi + mapfile -t ids < <(target_inject "$file") + expected="$(mktemp)"; actual="$(mktemp)" + if ! render_region "${ids[@]}" > "$expected"; then drift=1; rm -f "$expected" "$actual"; continue; fi + region_content "$REPO_ROOT/$file" > "$actual" + if ! diff -u "$expected" "$actual" >/dev/null 2>&1; then + printf '[DRIFT] %s: managed region differs from harness/enforced/ (run --refresh)\n' "$file" >&2 + diff -u "$actual" "$expected" | sed 's/^/ /' >&2 || true + drift=1 + else + printf '[check] OK -> %s\n' "$file" + fi + rm -f "$expected" "$actual" + done < <(jq -r '.targets[].file' "$MANIFEST") + + if [[ "$drift" -ne 0 ]]; then + printf '[check] FAIL: harness drift detected\n' >&2 + exit 1 + fi + printf '[check] OK: no harness drift\n' +} + +# --- dispatch --- +case "${1:-}" in + --refresh) do_refresh ;; + --check) do_check ;; + -h|--help) usage ;; + *) usage >&2; exit 2 ;; +esac diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh index ac71597..e87847c 100755 --- a/scripts/healthcheck.sh +++ b/scripts/healthcheck.sh @@ -414,6 +414,18 @@ else skip "diff-check.sh not found at $SCRIPT_DIR/diff-check.sh" fi +# Harness override drift (ENGINE-001): generated blocks vs their source-of-record. +# Offline (no vault); run from the repo so git resolves regardless of CWD. +if [ -x "$SCRIPT_DIR/compile-harness.sh" ]; then + if ( cd "$DOTFILES_DIR" && "$SCRIPT_DIR/compile-harness.sh" --check ) >/dev/null 2>&1; then + pass "harness override blocks match their source-of-record (no drift)" + else + fail "harness override drift (run: $SCRIPT_DIR/compile-harness.sh --refresh, then re-deploy)" + fi +else + skip "compile-harness.sh not found at $SCRIPT_DIR/compile-harness.sh" +fi + # ================================================== section "13/13" "Antigravity CLI Health" diff --git a/setup-linux.sh b/setup-linux.sh index e88e129..9c578cb 100755 --- a/setup-linux.sh +++ b/setup-linux.sh @@ -478,6 +478,20 @@ fi log_success "Antigravity CLI configuration complete" +# Harness deploy engine (ENGINE-001 / HARNESS-001): re-render the generated +# "Overrides of Harness Defaults" blocks in AGENTS.md + ai/claude/CLAUDE.md from +# the vault SSOT before deploying agent configs. Committed blocks are the fallback +# when the vault is absent (fresh machine), so this only re-renders when possible. +if [ -d "${VAULT_PATH:-$HOME/Projects/knowledge}/00_meta/patterns" ]; then + if ( cd "$CURRENT_DIR" && "$CURRENT_DIR/scripts/compile-harness.sh" --refresh ) >/dev/null 2>&1; then + log_success "Harness override blocks refreshed from vault SSOT" + else + log_warning "compile-harness --refresh failed; deploying committed blocks" + fi +else + log_info "Vault absent; deploying committed harness override blocks" +fi + # Claude Code ensure_directory "$HOME/.claude" ensure_directory "$HOME/.claude/skills" diff --git a/specs/ENGINE-001-deploy-engine-core/features.json b/specs/ENGINE-001-deploy-engine-core/features.json new file mode 100644 index 0000000..97fd91f --- /dev/null +++ b/specs/ENGINE-001-deploy-engine-core/features.json @@ -0,0 +1,51 @@ +[ + { + "id": "check-detects-drift", + "behavior": "AC1 β€” compile-harness.sh --check exits 0 on a freshly-refreshed tree; exits non-zero (with a diff summary) after a deployed block is hand-edited.", + "verification": "bats tests/compile-harness.bats -f 'AC1'", + "state": "verifying", + "evidence": "" + }, + { + "id": "refresh-idempotent-sha-markers", + "behavior": "AC2 β€” Re-running --refresh is idempotent (no diff); generated blocks carry BEGIN/END HARNESS markers with source path + sha256 prefix.", + "verification": "bats tests/compile-harness.bats -f 'AC2'", + "state": "verifying", + "evidence": "" + }, + { + "id": "record-committed-offline-check", + "behavior": "AC3 β€” harness/enforced/*.md exists and is committed; --check renders from it with no vault access (VAULT_PATH at an empty dir).", + "verification": "bats tests/compile-harness.bats -f 'AC3'", + "state": "verifying", + "evidence": "" + }, + { + "id": "line-cap-breach-fails", + "behavior": "AC4 β€” Injecting past the 100-line cap on ai/claude/CLAUDE.md makes --refresh fail with a clear error.", + "verification": "bats tests/compile-harness.bats -f 'AC4'", + "state": "verifying", + "evidence": "" + }, + { + "id": "missing-end-marker-fails-loud", + "behavior": "AC5 β€” A missing END marker makes --refresh fail loudly (no silent append).", + "verification": "bats tests/compile-harness.bats -f 'AC5'", + "state": "verifying", + "evidence": "" + }, + { + "id": "healthcheck-guards-drift", + "behavior": "AC6 β€” healthcheck.sh wires the offline harness drift check (compile-harness.sh --check) so a tampered deployed block is flagged with no vault.", + "verification": "bats tests/compile-harness.bats -f 'AC6'", + "state": "verifying", + "evidence": "" + }, + { + "id": "override-text-deployed", + "behavior": "AC7 β€” The no-attribution override text appears in deployed ai/claude/CLAUDE.md + AGENTS.md between the markers.", + "verification": "grep -q 'No AI attribution' AGENTS.md && grep -q 'No AI attribution' ai/claude/CLAUDE.md", + "state": "verifying", + "evidence": "" + } +] diff --git a/specs/ENGINE-001-deploy-engine-core/proposal.md b/specs/ENGINE-001-deploy-engine-core/proposal.md new file mode 100644 index 0000000..91b9480 --- /dev/null +++ b/specs/ENGINE-001-deploy-engine-core/proposal.md @@ -0,0 +1,78 @@ +--- +id: "ENGINE-001-deploy-engine-core" +type: spec +status: draft # draft | implementing | verifying | archived +created: "2026-05-28" +tags: [spec, proposal, engine-001, harness-001, deploy-engine, cross-agent, tracer-bullet] +template_version: "1.0" +--- + +# ENGINE-001-deploy-engine-core + +> **PR-1 of the HARNESS-001 epic** (`specs/HARNESS-001-unified-cross-agent-harness/`, GH #162). The engine core, delivered as the *no-attribution tracer-bullet*: smallest payload through the whole pipeline. Consumers (SDD-008, IDEAS-007, #156, #159) build on what this ships. + +## Why + + + +The no-attribution policy regressed silently (#156) because there is no pipeline from the SSOT rules (`00_meta/patterns/`) to the instruction files agents read; harness defaults override absent rules at runtime. This PR builds the **engine core** and proves it end-to-end on the smallest possible payload β€” the no-attribution rule β€” so the full pipeline (manifest β†’ source-marker β†’ compile β†’ commit β†’ drift guard) exists and the #156 regression class cannot recur undetected. Scaling to all agents and all artifact kinds is the consumers' job; this PR ships the spine. + +## What + +Observable behaviour after this PR (Linux; Windows parity is PR-2): + +1. **`harness/manifest.json`** declares enforced rules and targets: + ```json + { "version": 1, "vault_subpath": "00_meta/patterns", + "enforced": [ { "id": "no-attribution", "source": "pattern-git-workflow.md#6-attribution-policy" }, + { "id": "english-only", "source": "pattern-git-workflow.md#8-language-policy" }, + { "id": "no-phase-references", "source": "pattern-git-workflow.md#7-message-content-policy" } ], + "targets": [ { "agent": "agents", "kind": "native", "file": "AGENTS.md", "inject": ["no-attribution","english-only","no-phase-references"] }, + { "agent": "claude", "kind": "pointer", "file": "ai/claude/CLAUDE.md", "inject": ["no-attribution","english-only","no-phase-references"] } ] } + + The `english-only` SSOT was added as `pattern-git-workflow.md` Β§8 in this PR (it was enforced in AGENTS.md but had no SSOT β€” the #156 bug class). The `ai/claude/CLAUDE.md` line cap was bumped 80β†’100 to fit the generated block. + ``` +2. **`compile-harness.sh --refresh`** (needs the vault): extracts the named section from the vault pattern, writes a **committed source-of-record** at `harness/enforced/no-attribution.md`, and injects a marker-delimited override block into each target file. +3. **`compile-harness.sh --check`** (fully offline, no vault): renders from the committed source-of-record and diffs against the deployed blocks; non-zero exit on any drift. +4. **Marker-delimited regions** are replaced idempotently; everything outside the markers stays hand-authored: + ``` + + …generated override text… + + ``` +5. **Healthcheck guard** β€” `scripts/healthcheck.sh` gains a `check_harness` assertion that fails when a deployed block β‰  render(source-of-record). Runs locally with no vault (the type-A / #156 guard; per the 2026-05-28 decision, the guard lives here, not in CI). +6. **Line-cap assertion** β€” `--refresh` fails if injecting a block pushes `ai/claude/CLAUDE.md` over 100 lines (cap bumped 80β†’100 in this PR to fit the generated block). +7. **`setup-linux.sh`** invokes `compile-harness.sh --refresh` as part of deploy, so the build IS setup. + +## Out of scope + +- **Windows `compile-harness.ps1` + Pester** β€” PR-2 (sequenced in the umbrella). PR-1 is the Linux tracer-bullet to prove the pipeline. +- **Agents beyond Claude + AGENTS** β€” agy/Copilot/OpenCode/Pi targets are PR-3. +- **Consumers** β€” skills (SDD-008), `.agent//` registry (IDEAS-007), full `enforce:true` discovery (#156), work/personal mode (#159). +- **Auto-discovery of enforced patterns** β€” PR-1 lists the one rule explicitly in the manifest; frontmatter `enforce:true` scanning is the #156 consumer. +- **Splitting patterns into per-rule files** β€” D3 decision: extract by section anchor now; the split belongs to #156. + +## Risks / open questions + +- **FM1 β€” marker tampering**: a missing/duplicated `END` marker must make `--refresh` *fail loudly*, never silently append (that reintroduces the drift class). Guarded by AC5. +- **FM2 β€” line-cap breach**: injection can push `ai/claude/CLAUDE.md` over 80 lines (opencode.bats #34). `--refresh` asserts the cap post-injection and fails. Guarded by AC4. +- **FM3 β€” nondeterministic order**: multiple injected rules must sort stably by `id`, or every recompile churns the diff. (Only one rule in PR-1, but the sort lands now.) +- **Section-anchor fragility (D3)**: if the heading named by the anchor is absent, the extractor errors clearly rather than emitting an empty block. +- **Vault dependency of `--refresh`**: reuse SDD-008's preflight β€” if the vault is absent, `--refresh` aborts with an actionable message; `--check` still works offline. + +## Acceptance criteria + +- [ ] **AC1** β€” `compile-harness.sh --check` exits 0 on a freshly-refreshed tree; exits non-zero (with a diff summary) after a deployed block is hand-edited. *(bats)* +- [ ] **AC2** β€” Re-running `--refresh` is idempotent (no diff); generated blocks carry `BEGIN/END GENERATED no-attribution` markers with source path + sha256 prefix. *(bats)* +- [ ] **AC3** β€” `harness/enforced/no-attribution.md` exists and is committed; `--check` renders from it with **no vault access** (test runs with `VAULT_PATH` pointed at an empty dir). *(bats)* +- [ ] **AC4** β€” Injecting past the 100-line cap on `ai/claude/CLAUDE.md` makes `--refresh` fail with a clear error. *(bats fixture)* +- [ ] **AC5** β€” A missing `END` marker makes `--refresh` fail loudly (no silent append). *(bats fixture)* +- [ ] **AC6** β€” `healthcheck.sh` flags a tampered deployed block (offline). *(bats / healthcheck test)* +- [ ] **AC7** β€” The no-attribution override text appears in deployed `ai/claude/CLAUDE.md` + `AGENTS.md` between the markers. *(grep test)* + +## References + +- Umbrella: `specs/HARNESS-001-unified-cross-agent-harness/` (epic AC1, AC3, AC4, AC5, partial AC7) +- ADR: `docs/adr/adr-013-agent-artifact-deploy-engine.md` (this engine); `docs/adr/adr-012-deploy-strategy-copy-with-drift-assertion.md` (copy substrate + healthcheck drift) +- SSOT rule: vault `00_meta/patterns/pattern-git-workflow.md` Β§Attribution Policy +- Tracker: GH #162 (epic), #156 (the regression this proves a guard for) diff --git a/specs/ENGINE-001-deploy-engine-core/tasks.md b/specs/ENGINE-001-deploy-engine-core/tasks.md new file mode 100644 index 0000000..c545476 --- /dev/null +++ b/specs/ENGINE-001-deploy-engine-core/tasks.md @@ -0,0 +1,45 @@ +--- +tags: [spec, tasks, engine-001, harness-001] +created: "2026-05-28" +--- + +# Tasks - ENGINE-001-deploy-engine-core + +> TDD order. One task = one focused commit. Freeze once `implementing` starts. + +## Setup + +- [x] Branch `feat/engine-001-deploy-core` off `main` +- [x] `proposal.md` complete; acceptance criteria testable +- [x] Manifest format locked: JSON (`jq` Linux / `ConvertFrom-Json` Windows) +- [x] Decisions recorded: id=ENGINE-001 Β· D2=source-of-record Β· D3=section-anchor Β· guard=healthcheck + +## Implementation (TDD) + +- [x] **(red)** `tests/compile-harness.bats`: `--check` exits non-zero when no record/blocks exist yet +- [x] **(green)** `harness/manifest.json` + `compile-harness.sh` skeleton (arg parse: `--refresh` | `--check` | `--help`) +- [x] **(red)** `--check` exits 0 on a refreshed fixture, non-zero after a deployed block is hand-edited (AC1) +- [x] **(green)** implement `--check`: render block from `harness/enforced/.md` β†’ diff vs marker region in each target file +- [x] **(red)** `--refresh` is idempotent; blocks carry BEGIN/END markers + source + sha256 (AC2, AC7) +- [x] **(green)** implement `--refresh`: extract vault section by anchor β†’ write `harness/enforced/.md` β†’ inject marker region (stable sort by id, FM3) +- [x] **(red)** AC3 β€” `--check` works with `VAULT_PATH` at an empty dir (offline render from record) +- [x] **(green)** ensure `--check` never touches the vault; `--refresh` preflights vault presence (actionable abort) +- [x] **(red)** AC4 line-cap breach fails; AC5 missing END marker fails loudly (fixtures) +- [x] **(green)** post-inject 100-line assertion + marker-integrity validation (no silent append) +- [x] **(red)** AC6 β€” `healthcheck.sh` flags a tampered deployed block (offline) +- [x] **(green)** add `check_harness` to `scripts/healthcheck.sh` (calls `compile-harness.sh --check`) +- [x] wire `setup-linux.sh` to run `compile-harness.sh --refresh` in the deploy phase +- [x] **(refactor)** extract helpers, shellcheck clean, dedupe + +## Closing + +- [x] Every AC covered by β‰₯1 test in `tests/compile-harness.bats` +- [x] `features.json` emitted; each AC β†’ executable `verification` command +- [x] `shellcheck scripts/compile-harness.sh` clean; `bash -n` + `zsh -n` pass +- [x] `bats tests/compile-harness.bats` green (targeted β€” NOT `tests/*.bats`, hangs per #167) +- [x] `verification.md` filled with evidence +- [ ] PR opened referencing this spec + the umbrella + +## Machine-readable features + +Emit `features.json` (sibling) per [[pattern-feature-list-as-primitive]]: one feature per AC with an executable `verification`. Only the harness may set `"state": "passing"` after capturing exit 0. diff --git a/specs/ENGINE-001-deploy-engine-core/verification.md b/specs/ENGINE-001-deploy-engine-core/verification.md new file mode 100644 index 0000000..1a018cd --- /dev/null +++ b/specs/ENGINE-001-deploy-engine-core/verification.md @@ -0,0 +1,52 @@ +--- +tags: [spec, verification, engine-001, harness-001] +created: "2026-05-13" +--- + +# Verification - ENGINE-001-deploy-engine-core + +> Branch `feat/engine-001-deploy-core`. Machine-readable contract: `features.json` (7 entries, one per AC). Evidence below is reproducible from the test names; the harness sets `passing` after capturing exit 0 (pass-state gating per [[pattern-feature-list-as-primitive]]). + +## Evidence + +Every acceptance criterion maps to an executable verification in `features.json` and a test/observation here. + +- [x] **AC1** (`--check` 0 on fresh tree, β‰ 0 after hand-edit) -> `bats compile-harness.bats -f 'AC1'` (2 tests green) + observed: `--check` on the real repo prints `[check] OK -> AGENTS.md` / `[check] OK -> ai/claude/CLAUDE.md`. +- [x] **AC2** (idempotent `--refresh`; BEGIN/END markers carry source + sha256) -> `bats … -f 'AC2'` (2 tests green) + observed: re-running `--refresh` against the real vault leaves `git diff` byte-identical; markers carry `sha256:82589eb0b0204879`. +- [x] **AC3** (record committed; `--check` renders offline, no vault) -> `bats … -f 'AC3'` (2 tests green) + observed: `--check` passes with `VAULT_PATH` at an empty dir. +- [x] **AC4** (100-line cap breach fails) -> `bats … -f 'AC4'` (1 test green); deployed `ai/claude/CLAUDE.md` = 90 lines (≀100). +- [x] **AC5** (missing END marker fails loud, no silent append) -> `bats … -f 'AC5'` (1 test green). +- [x] **AC6** (healthcheck wires the offline drift check) -> `bats … -f 'AC6'` (1 test green); `scripts/healthcheck.sh` calls `compile-harness.sh --check`. +- [x] **AC7** (override text in deployed AGENTS.md + CLAUDE.md) -> `grep -q 'No AI attribution' AGENTS.md && grep -q … ai/claude/CLAUDE.md` (exit 0). + +## Test status + +- Test suite: `bats tests/compile-harness.bats` -> `1..12` all `ok` (12/12). Run targeted, NOT `tests/*.bats` (hangs per #167). +- `features.json`: all 7 verification commands exit 0 (run manually); `jq` schema-valid (unique ids, all `state: verifying`, no sham verifications). +- Static: `shellcheck scripts/compile-harness.sh` clean; `bash -n` + `zsh -n` pass. +- Manual smoke: `--refresh` against the real vault renders Β§6/Β§7/Β§8 of `pattern-git-workflow.md` into both targets; `--check` on the real repo reports no drift; second `--refresh` is a no-op (idempotent). +- No regressions: `bats tests/opencode.bats -f 'pointer to AGENTS.md'` -> 2/2 green after the 80β†’100 cap bump. + +## Decisions made during implementation + +- **Single managed region per target**, not per-rule markers (the original proposal sketch). All enforced rules render concatenated under one `` block, stable-sorted by manifest order β€” collapses FM3 (nondeterministic order churn) to a single deterministic block. `proposal.md` was rewritten to this 3-rule/2-target design. +- **Marker carries `sha256:<16>` of the rendered region + SSOT pointer + "do not edit between markers"** so drift is visible in the diff and the source is self-documenting. +- **`english-only` (Β§8 Language Policy) added to the vault SSOT** `pattern-git-workflow.md` in this change β€” it was enforced in `AGENTS.md` with no SSOT, which is exactly the #156 regression class. +- **Cap bumped 80β†’100** on `ai/claude/CLAUDE.md` to fit the generated block; `opencode.bats` updated with the justification inline. +- **Drift guard lives in `healthcheck.sh` (local), not CI** β€” per the 2026-05-28 decision (ADR-013), `--check` is offline-by-design so the guard runs on any machine without the vault. + +## Promotion candidates + +> Per the knowledge-placement model (KPM-001), project-bound knowledge stays in this repo's `docs/`; only cross-project brain goes to the vault. **Flagged for user confirmation β€” promotion is a knowledge-system call.** + +- [ ] **Lesson?** Recommend **no** as a standalone vault lesson. The reusable insight (SSOTβ†’generated-blockβ†’offline drift guard closing the #156 class) is already captured by ADR-013 + this spec. Optional one-liner to `docs/lessons.md` if desired. +- [ ] **ADR-worthy?** **Already exists** β€” `docs/adr/adr-013-agent-artifact-deploy-engine.md` (engine) + `adr-012` (copy-with-drift substrate). No new ADR. +- [ ] **New `00_meta/patterns/` pattern?** **Defer.** The "compile canonical block from SSOT + sha-markered region + offline drift check" mechanism is reusable, but per the pattern's own >1-project rule its consumers (PR-2 Windows, PR-3 other agents) are still inside HARNESS-001. Revisit when a second independent project adopts the engine. + +## Archive checklist + +- [ ] `proposal.md` frontmatter set to `status: archived` +- [ ] Folder moved: `specs/ENGINE-001-deploy-engine-core/` -> `specs/archive/ENGINE-001-deploy-engine-core/` +- [ ] Backlog entry in vault `11-tasks.md` ticked with PR link +- [ ] Promotions above executed (if any) +- [ ] PR merged; worktree `dotfiles-engine` removed (`git worktree remove`) + branch deleted diff --git a/tests/compile-harness.bats b/tests/compile-harness.bats new file mode 100644 index 0000000..cb07009 --- /dev/null +++ b/tests/compile-harness.bats @@ -0,0 +1,127 @@ +#!/usr/bin/env bats +# Tests for scripts/compile-harness.sh β€” ENGINE-001 agent-artifact deploy engine. +# Each test runs the real script against an isolated temp git repo + fake vault, +# so the real AGENTS.md / CLAUDE.md are never touched. + +setup() { + SCRIPT="$BATS_TEST_DIRNAME/../scripts/compile-harness.sh" + TMP="/tmp/bats_harness_$$_${BATS_TEST_NUMBER:-0}" + mkdir -p "$TMP" + + # fake vault with one extractable section + VAULT="$TMP/vault" + mkdir -p "$VAULT/00_meta/patterns" + cat > "$VAULT/00_meta/patterns/test-pattern.md" <<'EOF' +# Test Pattern + +## 1. Demo Rule +- rule line one +- rule line two + +## 2. Next Section +- unrelated +EOF + + # repo fixture + REPO="$TMP/repo" + mkdir -p "$REPO/harness" + cd "$REPO" || exit 1 + git init -q -b main + git config user.email t@t; git config user.name t; git config commit.gpgsign false + + cat > "$REPO/harness/manifest.json" <<'EOF' +{ "version": 1, "vault_subpath": "00_meta/patterns", + "enforced": [ { "id": "demo", "source": "test-pattern.md#1-demo-rule" } ], + "targets": [ { "agent": "t", "kind": "native", "file": "TARGET.md", "inject": ["demo"] } ] } +EOF + + printf 'intro\n\n\n\n\noutro\n' > "$REPO/TARGET.md" + git add -A; git commit -q -m seed +} + +teardown() { cd / || true; rm -rf "$TMP"; } + +run_refresh() { run env VAULT_PATH="$VAULT" "$SCRIPT" --refresh; } + +@test "--help exits 0 and prints usage" { + run "$SCRIPT" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage"* ]] +} + +@test "unknown argument exits 2" { + run "$SCRIPT" --bogus + [ "$status" -eq 2 ] +} + +@test "AC2/AC7: --refresh writes source-of-record + injects content with sha marker" { + run_refresh + [ "$status" -eq 0 ] + [ -f "$REPO/harness/enforced/demo.md" ] + grep -q "rule line one" "$REPO/TARGET.md" + grep -q "rule line two" "$REPO/TARGET.md" + grep -qE "BEGIN HARNESS GENERATED \(sha256:[0-9a-f]{16}\)" "$REPO/TARGET.md" +} + +@test "AC2: --refresh is idempotent" { + run_refresh; [ "$status" -eq 0 ] + cp "$REPO/TARGET.md" "$TMP/first" + run_refresh; [ "$status" -eq 0 ] + diff "$TMP/first" "$REPO/TARGET.md" +} + +@test "AC1: --check passes on a freshly refreshed tree" { + run_refresh; [ "$status" -eq 0 ] + run "$SCRIPT" --check + [ "$status" -eq 0 ] + [[ "$output" == *"no harness drift"* ]] +} + +@test "AC1: --check fails after a deployed block is hand-edited" { + run_refresh; [ "$status" -eq 0 ] + sed -i 's/rule line one/TAMPERED/' "$REPO/TARGET.md" + run "$SCRIPT" --check + [ "$status" -ne 0 ] + [[ "$output" == *"DRIFT"* ]] +} + +@test "AC3: --check works offline (no vault) from the committed record" { + run_refresh; [ "$status" -eq 0 ] + run env VAULT_PATH="$TMP/nonexistent" "$SCRIPT" --check + [ "$status" -eq 0 ] +} + +@test "AC3: --check fails when the source-of-record is missing" { + run_refresh; [ "$status" -eq 0 ] + rm -f "$REPO/harness/enforced/demo.md" + run "$SCRIPT" --check + [ "$status" -ne 0 ] +} + +@test "AC5: missing END marker makes --refresh fail loudly (no silent append)" { + printf 'intro\n\nno end here\n' > "$REPO/TARGET.md" + run_refresh + [ "$status" -ne 0 ] + [[ "$output" == *"marker"* ]] +} + +@test "AC6: healthcheck.sh wires the offline harness drift check" { + grep -q 'compile-harness.sh" --check' "$BATS_TEST_DIRNAME/../scripts/healthcheck.sh" +} + +@test "setup-linux.sh runs compile-harness --refresh during deploy" { + grep -q 'compile-harness.sh" --refresh' "$BATS_TEST_DIRNAME/../setup-linux.sh" +} + +@test "AC4: injection past the line cap fails (ai/claude/CLAUDE.md)" { + mkdir -p "$REPO/ai/claude" + cat > "$REPO/harness/manifest.json" <<'EOF' +{ "version": 1, "vault_subpath": "00_meta/patterns", + "enforced": [ { "id": "demo", "source": "test-pattern.md#1-demo-rule" } ], + "targets": [ { "agent": "claude", "kind": "pointer", "file": "ai/claude/CLAUDE.md", "inject": ["demo"] } ] } +EOF + { printf 'line %d\n' $(seq 1 99); printf '\n\n'; } > "$REPO/ai/claude/CLAUDE.md" + run_refresh + [ "$status" -ne 0 ] + [[ "$output" == *"cap"* ]] +} diff --git a/tests/opencode.bats b/tests/opencode.bats index ff4d66f..fcd1085 100644 --- a/tests/opencode.bats +++ b/tests/opencode.bats @@ -186,11 +186,14 @@ setup() { # --- Per-agent pointer files (AI-013 fold-in) --- -@test "ai/claude/CLAUDE.md is a pointer to AGENTS.md (≀ 80 lines)" { +@test "ai/claude/CLAUDE.md is a pointer to AGENTS.md (≀ 100 lines)" { # Threshold bumped 70β†’80 in AI-019 (model-tier overlay added ~8 lines). + # Bumped 80β†’100 in ENGINE-001: the HARNESS engine injects the generated + # "Overrides of Harness Defaults" block (no-attribution + english-only + + # no-phase-refs) sourced from the vault, which needs the extra headroom. # Future per-agent extensions should justify each bump in the spec. grep -q "First, read \`AGENTS.md\`" "$DOTFILES_DIR/ai/claude/CLAUDE.md" - [[ $(wc -l < "$DOTFILES_DIR/ai/claude/CLAUDE.md") -le 80 ]] + [[ $(wc -l < "$DOTFILES_DIR/ai/claude/CLAUDE.md") -le 100 ]] } @test "ai/agy/AGY.md is a pointer to AGENTS.md (≀ 50 lines)" {