Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions .agents/skills/gc-watchdog/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: gc-watchdog
description: Use when setting up a periodic Gas City health monitor. Trigger when the user asks for a watchdog, periodic status check, or monitoring loop for gc status. Covers the check cycle, exit conditions, and reporting format.
---

# GC Watchdog

Periodic Gas City health monitor. Checks `gc status` and `bd list` on an
interval, reports issues, and exits when there is no more open work.

## When to use

- User asks to "watch" or "monitor" Gas City while work is in progress
- User wants periodic status reports during a long-running wave
- User wants to know when the city goes idle (all work done)

## Exit conditions

The watchdog stops when ALL of these are true:

1. No open real tasks: `bd list --status=open` returns no issues (excluding
ephemeral wisp/nudge beads)
2. No in-progress workflows: no beads with `gc.kind: workflow` in
`in_progress` state
3. Mayor is awake and not suspended (clean shutdown, not a crash)

If the mayor is down or the city is suspended, the watchdog reports the
error and keeps running (does NOT exit on failure — only on success).

## Check cycle

Each tick:

1. `gc status` — extract mayor status, sessions, suspended, controller
2. `bd list --status=open` — count real (non-wisp, non-nudge) open issues
3. `bd list --status=in_progress` — count real in-progress issues
4. Report one line
5. If no open AND no in-progress real issues → report "idle" and exit 0

## Reporting format

```text
[HH:MM:SS] ✓ mayor:<status> | open:<N> in_progress:<M> | <sessions line>
[HH:MM:SS] ⚠ <issues> | open:<N> in_progress:<M> | <sessions line>
[HH:MM:SS] IDLE — no open work, no in-progress work. Watchdog exiting.
```

## Usage

### As a skill (agent-driven)

The agent reads this SKILL.md, runs the check script, and reports back.
Each tick is one subagent invocation: wait 60s, check, report.

### As a standalone script

```bash
bash .agents/skills/gc-watchdog/watchdog.sh [interval_seconds]
```

Default interval: 60 seconds. Runs until idle or interrupted.

## Files

- `watchdog.sh` — standalone shell script implementing the check cycle
- `SKILL.md` — this file (documentation for agents)
119 changes: 119 additions & 0 deletions .agents/skills/gc-watchdog/watchdog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/bin/bash
# gc-watchdog — periodic Gas City health monitor with exit condition.
#
# Checks gc status + bd ready on an interval. Reports issues. Exits when
# there is no more open real work (non-wisp, non-nudge) and no in-progress
# real work.
#
# Usage: bash watchdog.sh [interval_seconds]
# Default interval: 60 seconds.

# NOTE: intentionally NOT using set -e. gc status and bd list can fail
# transiently (session snapshot timeouts, store locks). We handle errors
# per-command and continue the loop.
set -uo pipefail
Comment thread
ThePlenkov marked this conversation as resolved.

INTERVAL="${1:-60}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Validate INTERVAL is a positive integer to prevent injection.
if ! [[ "$INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: interval must be a positive integer, got: $INTERVAL" >&2
exit 2
fi
ITER=0
Comment thread
ThePlenkov marked this conversation as resolved.

# Filter out ephemeral wisp/nudge beads from bd output.
# Real issues have IDs like sv-XXXX (4+ chars after sv-).
# Wisp/nudge beads have IDs like sv-wisp-XXXX or sv-nudge-XXXX.
# Status symbols: ○ = open, ◐ = in_progress, ● = blocked, ✓ = closed
# Returns -1 on bd failure (distinct from a legitimate 0 count) so the
# caller can distinguish "no work" from "lookup failed". Adds a timeout
# to bd list so a blocked store cannot stall the watchdog.
count_real_issues() {
local status="$1"
local raw
if ! raw=$(timeout 10 bd list --status="$status" 2>/dev/null); then
printf -- '-1\n'
return 1
Comment thread
ThePlenkov marked this conversation as resolved.
fi
# grep returns 1 when no matches; with pipefail that would fail the
# pipeline. Use `|| true` to swallow grep's no-match exit code so
# an empty result yields 0, not -1.
printf '%s\n' "$raw" \
| { grep -E '^\s*[○◐●] sv-[a-z0-9]{4,}($| )' || true; } \
| { grep -vE '^\s*[○◐●] sv-(wisp|nudge)' || true; } \
Comment thread
ThePlenkov marked this conversation as resolved.
| wc -l
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

while true; do
ITER=$((ITER + 1))
TS=$(date '+%H:%M:%S')

# 1. gc status
STATUS=$(timeout 15 gc status 2>/dev/null || true)

if [ -z "$STATUS" ]; then
echo "[$TS] #$ITER ⚠ gc status timeout/error"
sleep "$INTERVAL"
continue
fi
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

MAYOR=$(echo "$STATUS" | grep "harness.mayor" | awk '{print $2}')
Comment thread
ThePlenkov marked this conversation as resolved.
SESSIONS=$(echo "$STATUS" | grep "Sessions:" | head -1 | sed 's/^ *//')
SUSPENDED=$(echo "$STATUS" | grep "Suspended:" | awk '{print $2}')
Comment thread
ThePlenkov marked this conversation as resolved.
CONTROLLER=$(echo "$STATUS" | grep "Controller:" | grep -o "supervisor-managed\|stopped\|error" | head -1 || true)

# Handle transient lookup errors (mayor shows "lookup" instead of "awake")
if echo "$MAYOR" | grep -q "lookup"; then
MAYOR="lookup-error"
fi

# 2. bd work counts
OPEN_COUNT=$(count_real_issues "open")
OPEN_FAILED=$?
INPROG_COUNT=$(count_real_issues "in_progress")
INPROG_FAILED=$?

# 3. Check for issues
ISSUES=""
# bd lookup failures are hard issues — don't allow idle exit when store is unreachable
if [ "$OPEN_FAILED" -ne 0 ] || [ "$INPROG_FAILED" -ne 0 ]; then
ISSUES="$ISSUES BD_LOOKUP_FAILED"
fi
[ -z "$MAYOR" ] && ISSUES="$ISSUES MAYOR_MISSING"
if [ -n "$MAYOR" ] && [ "$MAYOR" != "awake" ] && [ "$MAYOR" != "running" ] && [ "$MAYOR" != "active" ] && [ "$MAYOR" != "lookup-error" ]; then
ISSUES="$ISSUES MAYOR($MAYOR)"
fi
# lookup-error is a transient issue, not a hard failure — report as ⚠ but don't treat as fatal
if [ "$MAYOR" = "lookup-error" ]; then
ISSUES="$ISSUES MAYOR_LOOKUP_TIMEOUT"
fi
# Treat missing status fields as unknown, not healthy defaults
if [ -z "${SUSPENDED:-}" ]; then
ISSUES="$ISSUES SUSPENDED_MISSING"
elif [ "$SUSPENDED" != "no" ]; then
ISSUES="$ISSUES SUSPENDED"
fi
if [ -z "${CONTROLLER:-}" ]; then
ISSUES="$ISSUES CONTROLLER_MISSING"
elif [ "$CONTROLLER" != "supervisor-managed" ]; then
ISSUES="$ISSUES CONTROLLER($CONTROLLER)"
fi

# 4. Report
if [ -n "$ISSUES" ]; then
echo "[$TS] #$ITER ⚠$ISSUES | open:$OPEN_COUNT in_progress:$INPROG_COUNT | ${SESSIONS:-no-sessions}"
else
echo "[$TS] #$ITER ✓ mayor:$MAYOR | open:$OPEN_COUNT in_progress:$INPROG_COUNT | ${SESSIONS:-no-sessions}"
fi

# 5. Exit condition: no open AND no in-progress real work, mayor healthy,
# and bd lookups succeeded. Soft signals (MAYOR_LOOKUP_TIMEOUT) are
# stripped; hard signals (BD_LOOKUP_FAILED) prevent idle exit.
HARD_ISSUES="${ISSUES// MAYOR_LOOKUP_TIMEOUT/}"
if [ "$OPEN_COUNT" -eq 0 ] && [ "$INPROG_COUNT" -eq 0 ] && [ -z "$HARD_ISSUES" ]; then
echo "[$TS] #$ITER IDLE — no open work, no in-progress work. Watchdog exiting."
exit 0
fi

sleep "$INTERVAL"
done
1 change: 1 addition & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ exclude_paths:
- "template-fragments/**"
- "dist/**"
- "node_modules/**"
- "scripts/**"

# Configure tools
engines:
Expand Down
20 changes: 20 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: "Sverka CodeQL Config"

# Exclude non-source paths from CodeQL analysis
paths:
- packages
paths-ignore:
- packages/*/dist/**
- "**/node_modules/**"
- "**/__tests__/**"
- "**/*.test.ts"
- pack/**
- .agents/**
- .gc/**
- website/**
- engdocs/**
- specs/**

# Query suite: security-extended (set in workflow)
queries:
- uses: security-extended
83 changes: 83 additions & 0 deletions REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Review Policy

## Two-Axis Review

Every change is reviewed along two independent axes:

1. **Standards** — does the code follow the repo's documented coding
standards (AGENTS.md) plus a Fowler smell baseline?
2. **Spec** — does the code faithfully implement the originating spec
in `specs/`?

Both axes must pass. A change that is clean code but implements the wrong
thing is rejected. A change that implements the right thing with messy code
is rejected.

## Verification Bar

The reviewer runs all checks themselves. No trusting builder claims.

```bash
bun run test # vitest via nx (NOT `bun test` — that runs Bun's runner)
bun run typecheck # tsc via nx
bun run lint # eslint via nx
bun run build # tsdown via nx
```

Run fresh with `--skip-nx-cache` to avoid cached results.

## Finding Classification

| Class | Meaning | Action |
| ------- | ------------------------------ | --------------- |
| BLOCKING | Spec violation or broken gate | Must fix |
| NIT | Non-blocking style or edge case | Note, don't block |
| DECLINE | Reviewer disagrees with suggestion | Explain why |

## Minimalism Audit

If the builder wrote more code than the spec requires, that's a rejection
for over-engineering. Less code = fewer bugs. The reviewer audits for:

- Unnecessary abstractions
- Speculative API (exports not used by spec)
- Dead code
- Premature generalization

## Spec Compliance

Every interface, every type, every error code in the spec must be present
in the implementation. No "close enough." No "it's basically the same."

Exports must match spec 1:1. No extra exports beyond what the spec defines
(testability seams excepted — note them as NITs).

## Error Handling

- Custom error classes must use `override` on `cause` (noImplicitOverride)
- No `any` types — use `unknown` and narrow
- Error codes as string unions, not enums

## Scaffolding Checks

- `package.json`: dist outputs as `.mjs`/`.d.mts` (not `.js`/`.d.ts`)
- `project.json`: lint target without `--ext .ts` (ESLint 9 flat config)
- `project.json`: test target with `--passWithNoTests`

## Commit Hygiene

Before committing a wave, verify:

- `git status --short` — every impl + test file is at least staged
- No untracked impl files (recurrence of untracked-test-helpers drill finding)
- Exclude: `city.toml`, `agents/`, `.devin/`, `.gc/`, `.beads/`, `.evidence/`,
`.opencode/`, `formulas/` (process improvement files)
- Stage only: `packages/<package>/**`, `specs/NN-<name>/`, `engdocs/`, `bun.lock`

## Process

1. Reviewer runs all gates fresh (not cached, not trusted from builder)
2. Reviewer reads the diff and spec
3. Reviewer classifies findings
4. APPROVE or REJECT with specific, actionable feedback
5. On REJECT: builder fixes, reviewer re-reviews
59 changes: 59 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Security Policy

## Reporting Vulnerabilities

Report security vulnerabilities privately. Do NOT open a public GitHub issue.

- Email: security@sverka.dev
- Response time: 48 hours
- Disclosure: coordinated, after fix is released

## Secrets

- NEVER commit secrets, API keys, tokens, or passwords to the repository
- Use environment variables or secret managers
- If a secret is accidentally committed: rotate it immediately, then use
`git filter-repo` or BFG Repo-Cleaner to purge it from history, then
force-push the rewritten branch. A force-push alone does NOT remove the
secret from retained history, forks, logs, or artifacts — rotation comes first
- Reviewer must check for hardcoded secrets in every PR

## Dependencies

- Prefer dependencies published at least 7 days ago (supply chain safety)
- Avoid floating ranges (`latest`, `*`, unbounded `>=`) that auto-resolve to
brand-new releases
- Run `bun audit` periodically
- Pin lockfile (`bun.lock`) — do not commit without it

## Code Security

- No `any` types (use `unknown` and narrow) — prevents type confusion bugs
- Validate all external input before use
- No `eval()`, no `new Function()`, no dynamic code execution
- No `child_process.exec` with user input — use `execFile` with arg arrays
- Sanitize file paths — no path traversal via `..` in user input
- Use `spawnSync` with timeout for subprocess calls (prevent hangs)

## CI/CD Security

- Never modify repository security policies or compliance controls to work
around CI failures
- Never disable branch protection, even temporarily
- Escalate CI/auth failures to the user instead of working around them

## Agent Security

- Agents must not expose or log secrets
- Agents must not commit secrets
- Agents must not modify security policies
- `dangerous` permission mode is for local dev only, never for CI

## Runtime Security

Sverka executes commands in Docker containers and host processes:

- Docker executor: verify image digests, no untrusted images
- Host executor: validate commands before execution, no shell injection
- Runtime args: sanitize all user-provided paths and arguments
- Credentials: pass via env vars, never log them
Loading
Loading