Add AGENTS and Definition of Done documentation; implement project standards reconciliation scripts
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Stefan Goppelt 2026-03-29 13:07:14 +02:00
parent fb0e7bb668
commit e69fd33931
5 changed files with 410 additions and 4 deletions

63
AGENTS.md Normal file
View File

@ -0,0 +1,63 @@
# AGENTS.md
## Purpose
This file defines the default working agreement for AI coding agents and contributors in this repository.
## Documentation Language
Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English.
All newly created documentation in this project must be written in English.
All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`.
## Working Principles
1. Keep changes minimal and focused on the requested task.
2. Preserve existing public APIs unless a breaking change is explicitly requested.
3. Prefer clear, maintainable code over clever shortcuts.
4. Do not modify unrelated files.
5. Never add secrets, credentials, or tokens to the repository.
## Testing Expectations
1. Add or update tests for behavior changes.
2. Keep tests deterministic and fast.
3. Prefer table-driven tests where they improve readability.
4. Run relevant tests locally before finishing changes.
## Definition of Done (DoD)
### Purpose
The Definition of Done defines the minimum quality bar for every completed change in this repository.
### Mandatory Criteria
1. Tests
- Every code change is covered by tests where applicable.
- New functionality includes new tests.
- Bug fixes include at least one regression test.
1. Functional documentation
- Implemented functionality is documented.
- Public API-relevant changes are reflected in README and/or docs.
1. Documentation standards
- Documentation is written in English.
- Documentation files are placed under `docs/`.
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
### Technical Completion Criteria
1. Build and test status
- The project builds successfully.
- Relevant test commands run successfully.
1. No unresolved critical issues
- No new blocking errors are introduced.
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
1. Documentation structure
- Links to moved or newly added docs are valid.
- Documentation structure remains consistent with project rules.
### Review Checklist (Quick)
- [ ] Change is implemented and meets acceptance criteria.
- [ ] Tests were added/updated and pass.
- [ ] Functionality is documented.
- [ ] Documentation is in English.
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
- [ ] No critical regressions found.

View File

@ -0,0 +1,44 @@
# Definition of Done (DoD)
## Purpose
This Definition of Done defines the minimum quality bar for every completed change in this repository.
## Mandatory Criteria
1. Tests
- Every code change is covered by tests where applicable.
- New functionality includes new tests.
- Bug fixes include at least one regression test.
1. Functional documentation
- Implemented functionality is documented.
- Public API-relevant changes are reflected in README and/or docs.
1. Documentation standards
- Documentation is written in English.
- Documentation files are placed under `docs/`.
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
## Technical Completion Criteria
1. Build and test status
- The project builds successfully.
- Relevant test commands run successfully.
1. No unresolved critical issues
- No new blocking errors are introduced.
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
1. Documentation links and structure
- Links to moved or newly added docs are valid.
- Documentation structure remains consistent with project rules.
## Review Checklist (Quick)
- [ ] Change is implemented and meets acceptance criteria.
- [ ] Tests were added/updated and pass.
- [ ] Functionality is documented.
- [ ] Documentation is in English.
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
- [ ] No critical regressions found.

View File

@ -0,0 +1,121 @@
[CmdletBinding()]
param(
[string]$RootPath = "C:\Users\stefan\git",
[string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards",
[string[]]$Exclude = @('project-standards'),
[switch]$CheckOnly,
[switch]$Watch,
[int]$IntervalSeconds = 60
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md'
$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md'
if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) {
throw "AGENTS template not found: $agentsTemplate"
}
if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) {
throw "DoD template not found: $dodTemplate"
}
if ($IntervalSeconds -lt 5) {
throw 'IntervalSeconds must be >= 5.'
}
function Get-ContentHashOrMissing {
param([string]$Path)
if (-not (Test-Path -Path $Path -PathType Leaf)) {
return '__MISSING__'
}
return (Get-FileHash -Path $Path -Algorithm SHA256).Hash
}
function Ensure-FileFromTemplate {
param(
[string]$Template,
[string]$Target,
[switch]$OnlyCheck
)
$templateHash = Get-ContentHashOrMissing -Path $Template
$targetHash = Get-ContentHashOrMissing -Path $Target
if ($templateHash -eq $targetHash) {
return 'ok'
}
if ($OnlyCheck) {
return 'drift'
}
$parent = Split-Path -Parent $Target
if (-not (Test-Path -Path $parent -PathType Container)) {
New-Item -ItemType Directory -Path $parent | Out-Null
}
Copy-Item -Path $Template -Destination $Target -Force
return 'updated'
}
function Invoke-ReconcileOnce {
param([switch]$OnlyCheck)
$resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path
$repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object {
$Exclude -notcontains $_.Name
}
$summary = [ordered]@{
scanned = 0
updated = 0
drift = 0
}
foreach ($repo in $repos) {
$repoPath = $repo.FullName
$agentsTarget = Join-Path $repoPath 'AGENTS.md'
$dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md'
$summary.scanned++
$agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck
$dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck
if ($agentsState -eq 'updated' -or $dodState -eq 'updated') {
$summary.updated++
Write-Host "UPDATED: $repoPath"
continue
}
if ($agentsState -eq 'drift' -or $dodState -eq 'drift') {
$summary.drift++
Write-Host "DRIFT: $repoPath"
continue
}
Write-Host "OK: $repoPath"
}
Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)"
return $summary
}
if ($Watch) {
while ($true) {
$now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Write-Host "[$now] Reconciling project standards..."
[void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly)
Start-Sleep -Seconds $IntervalSeconds
}
}
$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly
if ($CheckOnly -and $result.drift -gt 0) {
exit 1
}

View File

@ -0,0 +1,170 @@
#!/usr/bin/env sh
set -eu
ROOT_PATH="/c/Users/stefan/git"
STANDARDS_REPO="/c/Users/stefan/git/project-standards"
EXCLUDE_NAME="project-standards"
CHECK_ONLY=0
WATCH=0
INTERVAL=60
usage() {
cat <<'EOF'
Usage:
reconcile-project-standards.sh [options]
Options:
--root <path> Root folder containing repositories
--standards-repo <path> Path to project-standards repository
--exclude <name> First-level directory name to skip (default: project-standards)
--check-only Check drift only, do not update files
--watch Continuously scan and reconcile
--interval <seconds> Watch interval in seconds (default: 60)
-h, --help Show this help
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--root)
ROOT_PATH=$2
shift 2
;;
--standards-repo)
STANDARDS_REPO=$2
shift 2
;;
--exclude)
EXCLUDE_NAME=$2
shift 2
;;
--check-only)
CHECK_ONLY=1
shift
;;
--watch)
WATCH=1
shift
;;
--interval)
INTERVAL=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if [ "$INTERVAL" -lt 5 ]; then
echo "interval must be >= 5" >&2
exit 1
fi
AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md"
DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md"
if [ ! -f "$AGENTS_TEMPLATE" ]; then
echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$DOD_TEMPLATE" ]; then
echo "DoD template not found: $DOD_TEMPLATE" >&2
exit 1
fi
hash_or_missing() {
path=$1
if [ ! -f "$path" ]; then
printf "%s" "__MISSING__"
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" | awk '{print $1}'
else
shasum -a 256 "$path" | awk '{print $1}'
fi
}
ensure_file() {
template=$1
target=$2
template_hash=$(hash_or_missing "$template")
target_hash=$(hash_or_missing "$target")
if [ "$template_hash" = "$target_hash" ]; then
printf "%s" "ok"
return 0
fi
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
return 0
fi
mkdir -p "$(dirname "$target")"
cp "$template" "$target"
printf "%s" "updated"
}
run_once() {
scanned=0
updated=0
drift=0
for repo in "$ROOT_PATH"/*; do
[ -d "$repo" ] || continue
name=$(basename "$repo")
[ "$name" = "$EXCLUDE_NAME" ] && continue
scanned=$((scanned + 1))
agents_target="$repo/AGENTS.md"
dod_target="$repo/docs/DEFINITION_OF_DONE.md"
agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target")
dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target")
if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ]; then
updated=$((updated + 1))
echo "UPDATED: $repo"
continue
fi
if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ]; then
drift=$((drift + 1))
echo "DRIFT: $repo"
continue
fi
echo "OK: $repo"
done
echo "Summary -> scanned=$scanned, updated=$updated, drift=$drift"
if [ "$CHECK_ONLY" -eq 1 ] && [ "$drift" -gt 0 ]; then
return 1
fi
return 0
}
if [ "$WATCH" -eq 1 ]; then
while :; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reconciling project standards..."
run_once || true
sleep "$INTERVAL"
done
else
run_once
fi

View File

@ -1,7 +1,6 @@
package util package util
import ( import (
"fmt"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
@ -67,7 +66,7 @@ func TestIsSuperUser(t *testing.T) {
func TestGlobalConfigurationDirectoryWindows(t *testing.T) { func TestGlobalConfigurationDirectoryWindows(t *testing.T) {
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) t.Skipf("Skipping on OS %s", runtime.GOOS)
} }
appFolder := GetGlobalConfigurationDirectory("myapp") appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, filepath.Join(os.Getenv("APPDATA"), "myapp"), appFolder) assert.Equal(t, filepath.Join(os.Getenv("APPDATA"), "myapp"), appFolder)
@ -75,7 +74,7 @@ func TestGlobalConfigurationDirectoryWindows(t *testing.T) {
func TestGlobalConfigurationDirectoryLinux(t *testing.T) { func TestGlobalConfigurationDirectoryLinux(t *testing.T) {
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" {
t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) t.Skipf("Skipping on OS %s", runtime.GOOS)
} }
appFolder := GetGlobalConfigurationDirectory("myapp") appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, "/etc/myapp", appFolder) assert.Equal(t, "/etc/myapp", appFolder)
@ -83,8 +82,17 @@ func TestGlobalConfigurationDirectoryLinux(t *testing.T) {
func TestGlobalConfigurationDirectoryMacOS(t *testing.T) { func TestGlobalConfigurationDirectoryMacOS(t *testing.T) {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) t.Skipf("Skipping on OS %s", runtime.GOOS)
} }
appFolder := GetGlobalConfigurationDirectory("myapp") appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, "/etc/myapp", appFolder) assert.Equal(t, "/etc/myapp", appFolder)
} }
func TestMkDir(t *testing.T) {
path := "c:/tmp/bla/blub"
if !FileExists(path) {
err := os.MkdirAll(path, os.ModeDir)
assert.Nil(t, err)
assert.True(t, FileExists(path))
}
}