@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +183 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# daemons/ — Watchdog daemon
|
|
2
|
+
|
|
3
|
+
**Purpose**: Long-running backup and secret-scan daemon for fleet-wide repo safety.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- **run-watchdog.sh** (1.7K): Interactive daemon supervisor; spawns backup-fleet.sh every 150s, maintains heartbeat guard (200s dedupe window), logs to FLEET-BACKUP.log. Calls tools/alert_bridge.py --scan after each cycle to post HIGH/CRITICAL alerts to Slack/Discord (opt-in). Traps INT/TERM cleanly.
|
|
8
|
+
- **backup-fleet.sh** (5K): Core backup worker; discovers repos (~/.*, ~/*, ~/dev/*), stashes uncommitted work to backup/* branches, pushes unpushed commits, scans tracked files for secrets. Blocks push if secret-scan fails (marks BLOCKED state).
|
|
9
|
+
|
|
10
|
+
## Contracts
|
|
11
|
+
|
|
12
|
+
**Env vars consumed**: `AESOP_ROOT` (defaults to `.`) — root of project; prepends to all state/ and tools/ paths.
|
|
13
|
+
|
|
14
|
+
**State files written** (git-ignored):
|
|
15
|
+
- `$AESOP_ROOT/state/.watchdog-heartbeat` — epoch seconds; dedupe guard (200s window).
|
|
16
|
+
- `$AESOP_ROOT/state/.watchdog-repos.json` — per-cycle snapshot: [{repo, state: CLEAN|PUSHED|SNAPSHOTTED|BLOCKED, age}].
|
|
17
|
+
- `$AESOP_ROOT/state/FLEET-BACKUP.log` — append-only; cycle start/end, repo statuses, secret-scan blocks.
|
|
18
|
+
- `$AESOP_ROOT/state/SECURITY-ALERTS.log` — append-only; security alerts (read by alert_bridge.py to post to webhooks).
|
|
19
|
+
- `$AESOP_ROOT/state/.alert-bridge-cursor` — line number of last sent alert (ensures idempotent alert dispatch).
|
|
20
|
+
|
|
21
|
+
**Exit behaviors**: run-watchdog exits 0 always (trap handles clean shutdown). backup-fleet exits 0 even on secret-scan block (marks repo BLOCKED, continues).
|
|
22
|
+
|
|
23
|
+
**Cadence**: 150s cycle; heartbeat dedupe within 200s.
|
|
24
|
+
|
|
25
|
+
## Invariants & Gotchas
|
|
26
|
+
|
|
27
|
+
1. **Single-instance guard (atomic lock)**: run-watchdog.sh uses atomic mkdir-based lockfile (`.watchdog-lock/`) to prevent concurrent daemons.
|
|
28
|
+
- Atomic acquire: `mkdir $LOCK_DIR` is guaranteed atomic on POSIX systems (returns 0 only to first caller).
|
|
29
|
+
- Stale-lock recovery: Crashed holder won't wedge daemon forever; lock older than 300s is reclaimed atomically.
|
|
30
|
+
- Guards both daemon mode and `--once` mode (no bypass).
|
|
31
|
+
2. **Testing override**: Set `AESOP_WATCHDOG_CYCLE_CMD` env var to replace backup-fleet.sh invocation (allows tests to use mock cycle without running real backup).
|
|
32
|
+
3. **CRLF-safe, no line continuations**: Use POSIX-safe heredocs; never add `\` for line wrap.
|
|
33
|
+
4. **Secret-scan gate**: `scan_tracked_files()` calls `$AESOP_ROOT/tools/secret_scan.py` on staged/modified files; non-0 exit blocks push, marks repo BLOCKED.
|
|
34
|
+
5. **Append-only logs**: FLEET-BACKUP.log only grows; rotate via tools/rotate_logs.py.
|
|
35
|
+
6. **Path dedup via realpath**: Avoids processing symlinked repos or dot-dir aliases twice.
|
|
36
|
+
7. **Alert Bridge integration** (NEW wave-14): After each backup-fleet.sh cycle, run-watchdog.sh calls `python $AESOP_ROOT/tools/alert_bridge.py --scan || true` to post HIGH/CRITICAL security alerts and heartbeat staleness to Slack/Discord. No-op if webhook_url null/absent in aesop.config.json (feature opt-in). Uses cursor file for idempotent dispatch (no re-sends). See tools/alert_bridge.py for details.
|
package/daemons/backup-fleet.sh
CHANGED
|
@@ -3,16 +3,49 @@ set -uo pipefail
|
|
|
3
3
|
# Backup fleet repos: stash uncommitted work, push unpushed commits to backup branches.
|
|
4
4
|
# Runs every 150s from run-watchdog.sh. Abort on secret-scan failures.
|
|
5
5
|
# Improvements: dot-directory discovery, path dedup, tracked-files-only secret scanning.
|
|
6
|
+
# P2 FIX: JSON escaping for repo names and NUL-delimited internal protocol.
|
|
6
7
|
|
|
7
8
|
AESOP_ROOT="${AESOP_ROOT:-.}"
|
|
8
9
|
HEARTBEAT="$AESOP_ROOT/state/.watchdog-heartbeat"
|
|
9
10
|
LOG="$AESOP_ROOT/state/FLEET-BACKUP.log"
|
|
10
11
|
REPOS_STATUS="$AESOP_ROOT/state/.watchdog-repos.json"
|
|
11
12
|
|
|
13
|
+
# P2 FIX: script-global temp file handles (cycle-status JSON build + per-repo
|
|
14
|
+
# NUL-protocol scratch file), cleaned up unconditionally via trap below so an
|
|
15
|
+
# interrupted cycle (INT/TERM) or unexpected error never leaks mktemp files
|
|
16
|
+
# over the daemon's long-running lifecycle. Initialized empty so `rm -f`
|
|
17
|
+
# under `set -u` is always safe even if the trap fires before mktemp runs.
|
|
18
|
+
temp_json=""
|
|
19
|
+
temp_result=""
|
|
20
|
+
cleanup_temp_files() {
|
|
21
|
+
rm -f "$temp_json" "$temp_result"
|
|
22
|
+
}
|
|
23
|
+
trap cleanup_temp_files EXIT INT TERM
|
|
24
|
+
|
|
12
25
|
date +%s > "$HEARTBEAT" 2>/dev/null
|
|
13
26
|
ts() { date '+%Y-%m-%d %H:%M:%S'; }
|
|
14
27
|
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
|
|
15
28
|
|
|
29
|
+
# P2 FIX: JSON escape function for safe string interpolation in JSON
|
|
30
|
+
# Escapes backslash, quote, and control characters (newline, tab, carriage return, etc.)
|
|
31
|
+
json_escape() {
|
|
32
|
+
local s="$1"
|
|
33
|
+
# Escape backslash first (must be first to avoid double-escaping)
|
|
34
|
+
s="${s//\\/\\\\}"
|
|
35
|
+
# Escape double quote
|
|
36
|
+
s="${s//\"/\\\"}"
|
|
37
|
+
# Escape newline
|
|
38
|
+
s="${s//$'\n'/\\n}"
|
|
39
|
+
# Escape carriage return
|
|
40
|
+
s="${s//$'\r'/\\r}"
|
|
41
|
+
# Escape tab
|
|
42
|
+
s="${s//$'\t'/\\t}"
|
|
43
|
+
# Escape backspace, form feed, and other C0 controls
|
|
44
|
+
s="${s//$'\b'/\\b}"
|
|
45
|
+
s="${s//$'\f'/\\f}"
|
|
46
|
+
printf '%s' "$s"
|
|
47
|
+
}
|
|
48
|
+
|
|
16
49
|
is_touched() {
|
|
17
50
|
local repo="$1"
|
|
18
51
|
[ ! -d "$repo/.git" ] && return 1
|
|
@@ -33,31 +66,60 @@ get_tracked_modifications() {
|
|
|
33
66
|
) | sort -u
|
|
34
67
|
}
|
|
35
68
|
|
|
69
|
+
get_untracked_files() {
|
|
70
|
+
local repo="$1"
|
|
71
|
+
(
|
|
72
|
+
cd "$repo" || return 1
|
|
73
|
+
git ls-files --others --exclude-standard 2>/dev/null
|
|
74
|
+
) | sort -u
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
scan_tracked_files() {
|
|
37
78
|
local repo="$1"
|
|
38
79
|
local tracked_files
|
|
39
|
-
local
|
|
80
|
+
local untracked_files
|
|
81
|
+
local -a file_paths=()
|
|
82
|
+
local repo_win
|
|
83
|
+
|
|
84
|
+
# Convert repo path to Windows format for Python compatibility (Git Bash on Windows)
|
|
85
|
+
repo_win=$(cd "$repo" 2>/dev/null && pwd -W 2>/dev/null || echo "$repo")
|
|
86
|
+
|
|
40
87
|
tracked_files=$(get_tracked_modifications "$repo")
|
|
88
|
+
untracked_files=$(get_untracked_files "$repo")
|
|
41
89
|
|
|
42
|
-
|
|
43
|
-
|
|
90
|
+
# Collect tracked modifications
|
|
91
|
+
if [ -n "$tracked_files" ]; then
|
|
92
|
+
while IFS= read -r file; do
|
|
93
|
+
if [ -n "$file" ] && [ -f "$repo/$file" ]; then
|
|
94
|
+
file_paths+=("$repo_win/$file")
|
|
95
|
+
fi
|
|
96
|
+
done <<EOF
|
|
97
|
+
$tracked_files
|
|
98
|
+
EOF
|
|
44
99
|
fi
|
|
45
100
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
# Collect untracked files (ITEM 1 fix)
|
|
102
|
+
if [ -n "$untracked_files" ]; then
|
|
103
|
+
while IFS= read -r file; do
|
|
104
|
+
if [ -n "$file" ] && [ -f "$repo/$file" ]; then
|
|
105
|
+
file_paths+=("$repo_win/$file")
|
|
106
|
+
fi
|
|
107
|
+
done <<EOF
|
|
108
|
+
$untracked_files
|
|
52
109
|
EOF
|
|
110
|
+
fi
|
|
53
111
|
|
|
54
|
-
if [
|
|
112
|
+
if [ ${#file_paths[@]} -eq 0 ]; then
|
|
55
113
|
return 0
|
|
56
114
|
fi
|
|
57
115
|
|
|
58
116
|
if [ -f "$AESOP_ROOT/tools/secret_scan.py" ]; then
|
|
59
|
-
|
|
117
|
+
# ITEM 2 fix: use "${file_paths[@]}" to properly quote array elements
|
|
118
|
+
python "$AESOP_ROOT/tools/secret_scan.py" "${file_paths[@]}" >/dev/null 2>&1
|
|
119
|
+
return $?
|
|
60
120
|
fi
|
|
121
|
+
|
|
122
|
+
return 0
|
|
61
123
|
}
|
|
62
124
|
|
|
63
125
|
get_default_branch() {
|
|
@@ -88,11 +150,11 @@ process_repo() {
|
|
|
88
150
|
WIPREF="backup/wip-$(date +%Y%m%d)"
|
|
89
151
|
if scan_tracked_files "$repo"; then
|
|
90
152
|
if git push -qf origin "$COMMIT:refs/heads/$WIPREF" 2>/dev/null; then
|
|
91
|
-
printf 'SNAPSHOTTED
|
|
153
|
+
printf 'SNAPSHOTTED\0%s\n' "$name"
|
|
92
154
|
exit 0
|
|
93
155
|
fi
|
|
94
156
|
else
|
|
95
|
-
printf 'BLOCKED
|
|
157
|
+
printf 'BLOCKED\0%s\n' "$name"
|
|
96
158
|
exit 0
|
|
97
159
|
fi
|
|
98
160
|
fi
|
|
@@ -107,27 +169,27 @@ process_repo() {
|
|
|
107
169
|
WIPREF="backup/master-wip-$(date +%Y%m%d)"
|
|
108
170
|
if scan_tracked_files "$repo"; then
|
|
109
171
|
if git push -qf origin "HEAD:refs/heads/$WIPREF" 2>/dev/null; then
|
|
110
|
-
printf 'SNAPSHOTTED
|
|
172
|
+
printf 'SNAPSHOTTED\0%s\n' "$name"
|
|
111
173
|
exit 0
|
|
112
174
|
fi
|
|
113
175
|
else
|
|
114
|
-
printf 'BLOCKED
|
|
176
|
+
printf 'BLOCKED\0%s\n' "$name"
|
|
115
177
|
exit 0
|
|
116
178
|
fi
|
|
117
179
|
else
|
|
118
180
|
if scan_tracked_files "$repo"; then
|
|
119
181
|
if git push -q origin "$branch" 2>/dev/null; then
|
|
120
|
-
printf 'PUSHED
|
|
182
|
+
printf 'PUSHED\0%s\n' "$name"
|
|
121
183
|
exit 0
|
|
122
184
|
fi
|
|
123
185
|
else
|
|
124
|
-
printf 'BLOCKED
|
|
186
|
+
printf 'BLOCKED\0%s\n' "$name"
|
|
125
187
|
exit 0
|
|
126
188
|
fi
|
|
127
189
|
fi
|
|
128
190
|
fi
|
|
129
191
|
|
|
130
|
-
printf 'CLEAN
|
|
192
|
+
printf 'CLEAN\0%s\n' "$name"
|
|
131
193
|
)
|
|
132
194
|
}
|
|
133
195
|
|
|
@@ -154,15 +216,24 @@ for dir in ~/.* ~/* ~/dev/*; do
|
|
|
154
216
|
$real_dir"
|
|
155
217
|
|
|
156
218
|
if is_touched "$dir"; then
|
|
157
|
-
|
|
158
|
-
|
|
219
|
+
# P0 FIX: Eliminate $() command substitution to preserve NUL bytes in protocol
|
|
220
|
+
# Direct redirect maintains NUL delimiters (process_repo emits STATE\0name format)
|
|
221
|
+
temp_result=$(mktemp)
|
|
222
|
+
process_repo "$dir" > "$temp_result"
|
|
223
|
+
# Read first field (state) and second field (name) using NUL delimiter
|
|
224
|
+
# Portable solution: use awk with RS="\0" instead of GNU sed \x0 (BSD/macOS compatible)
|
|
225
|
+
state=$(awk 'BEGIN{RS="\0"} NR==1 {print}' "$temp_result")
|
|
226
|
+
name=$(awk 'BEGIN{RS="\0"} NR==2 {print}' "$temp_result" | tr -d '\n')
|
|
227
|
+
rm -f "$temp_result"
|
|
159
228
|
[ -z "$state" ] && continue
|
|
160
229
|
if [ "$first" = 1 ]; then
|
|
161
230
|
first=0
|
|
162
231
|
else
|
|
163
232
|
echo "," >> "$temp_json"
|
|
164
233
|
fi
|
|
165
|
-
|
|
234
|
+
# P2 FIX: JSON-escape all interpolated values before emitting
|
|
235
|
+
# P2 FIX: Portable date format (not GNU-only date -Iseconds)
|
|
236
|
+
printf '{"repo":"%s","state":"%s","age":"%s"}' "$(json_escape "$name")" "$(json_escape "$state")" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$temp_json"
|
|
166
237
|
log "$state: $name"
|
|
167
238
|
fi
|
|
168
239
|
done
|
package/daemons/run-watchdog.sh
CHANGED
|
@@ -3,16 +3,71 @@
|
|
|
3
3
|
# Backs up committed + uncommitted fleet work and scans for security issues every 150s.
|
|
4
4
|
# Usage: run-watchdog.sh [--once]
|
|
5
5
|
#
|
|
6
|
-
# Configuration: export AESOP_ROOT=/path/to/aesop before running
|
|
6
|
+
# Configuration: export AESOP_ROOT=/path/to/aesop before running (defaults to script directory's parent).
|
|
7
|
+
# Testing: export AESOP_WATCHDOG_CYCLE_CMD to override backup-fleet.sh invocation
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
AESOP_ROOT="${AESOP_ROOT:-$(dirname "$SCRIPT_DIR")}"
|
|
9
11
|
MODE="${1:-daemon}"
|
|
12
|
+
LOCK_DIR="$AESOP_ROOT/state/.watchdog-lock"
|
|
13
|
+
LOCK_STALE_THRESHOLD=300
|
|
10
14
|
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
# Atomic lock acquire: mkdir is atomic (POSIX guarantees)
|
|
16
|
+
# Returns 0 if lock acquired, 1 if held by another process, 2 if stale lock reclaimed
|
|
17
|
+
acquire_lock() {
|
|
18
|
+
local lock_dir="$1"
|
|
19
|
+
local stale_threshold="$2"
|
|
20
|
+
|
|
21
|
+
# Ensure parent directory exists
|
|
22
|
+
mkdir -p "$(dirname "$lock_dir")" 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
# Try to create lock directory atomically (this is atomic on all POSIX systems)
|
|
25
|
+
if mkdir "$lock_dir" 2>/dev/null; then
|
|
26
|
+
# We created it; write timestamp and PID
|
|
27
|
+
date +%s > "$lock_dir/timestamp" 2>/dev/null
|
|
28
|
+
echo $$ > "$lock_dir/pid" 2>/dev/null
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Lock directory already exists; check if it's stale
|
|
33
|
+
if [ -d "$lock_dir" ] && [ -f "$lock_dir/timestamp" ]; then
|
|
34
|
+
local lock_mtime=$(cat "$lock_dir/timestamp" 2>/dev/null || echo 0)
|
|
35
|
+
local now=$(date +%s)
|
|
36
|
+
local lock_age=$((now - lock_mtime))
|
|
37
|
+
|
|
38
|
+
if [ "$lock_age" -gt "$stale_threshold" ]; then
|
|
39
|
+
# Lock is stale; try to reclaim it
|
|
40
|
+
rm -rf "$lock_dir" 2>/dev/null || true
|
|
41
|
+
if mkdir "$lock_dir" 2>/dev/null; then
|
|
42
|
+
date +%s > "$lock_dir/timestamp" 2>/dev/null
|
|
43
|
+
echo $$ > "$lock_dir/pid" 2>/dev/null
|
|
44
|
+
echo "watchdog lock was stale (${lock_age}s) — reclaimed." >&2
|
|
45
|
+
return 2
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Lock is held by another process
|
|
51
|
+
return 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Release lock: verify ownership before removing (P0 fix)
|
|
55
|
+
release_lock() {
|
|
56
|
+
local lock_dir="$1"
|
|
57
|
+
if [ -f "$lock_dir/pid" ]; then
|
|
58
|
+
local lock_pid=$(cat "$lock_dir/pid" 2>/dev/null || echo "")
|
|
59
|
+
if [ "$lock_pid" = "$$" ]; then
|
|
60
|
+
rm -rf "$lock_dir" 2>/dev/null
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Try to acquire lock (applies to both --once and daemon modes)
|
|
66
|
+
acquire_lock "$LOCK_DIR" "$LOCK_STALE_THRESHOLD"
|
|
67
|
+
lock_result=$?
|
|
68
|
+
|
|
69
|
+
if [ $lock_result -eq 1 ]; then
|
|
70
|
+
echo "watchdog already running — not starting a duplicate."
|
|
16
71
|
exit 0
|
|
17
72
|
fi
|
|
18
73
|
|
|
@@ -21,17 +76,45 @@ echo " FLEET WATCHDOG DAEMON · backup + ensure-push + scan / 150s"
|
|
|
21
76
|
echo " logs: $AESOP_ROOT/state/FLEET-BACKUP.log · Ctrl-C to stop"
|
|
22
77
|
echo "==================================================================="
|
|
23
78
|
echo "[$(date '+%F %T')] === watchdog daemon (shell) STARTED ===" >> "$AESOP_ROOT/state/FLEET-BACKUP.log"
|
|
24
|
-
trap
|
|
79
|
+
trap "release_lock \"$LOCK_DIR\"; echo \"[$(date '+%F %T')] === watchdog daemon (shell) STOPPED ===\" >> \"$AESOP_ROOT/state/FLEET-BACKUP.log\"; echo \"stopped.\"; exit 0" INT TERM
|
|
80
|
+
|
|
81
|
+
# Allow override of backup cycle command (for testing)
|
|
82
|
+
# Use array to safely handle paths with spaces (P1 fix)
|
|
83
|
+
if [ -n "$AESOP_WATCHDOG_CYCLE_CMD" ]; then
|
|
84
|
+
# Override: run as-is through bash -c
|
|
85
|
+
CYCLE_CMD_ARRAY=("bash" "-c" "$AESOP_WATCHDOG_CYCLE_CMD")
|
|
86
|
+
else
|
|
87
|
+
# Default: array form for proper quoting
|
|
88
|
+
CYCLE_CMD_ARRAY=("bash" "$AESOP_ROOT/daemons/backup-fleet.sh")
|
|
89
|
+
fi
|
|
25
90
|
|
|
26
91
|
if [ "$MODE" = "--once" ]; then
|
|
27
|
-
|
|
28
|
-
|
|
92
|
+
full_out=$("${CYCLE_CMD_ARRAY[@]}" 2>&1)
|
|
93
|
+
cmd_exit=$?
|
|
94
|
+
echo "$full_out"
|
|
95
|
+
if [ $cmd_exit -ne 0 ]; then
|
|
96
|
+
err_msg="[$(date '+%F %T')] ERROR: cycle #1 failed with exit code $cmd_exit"
|
|
97
|
+
echo "$err_msg" >> "$AESOP_ROOT/state/FLEET-BACKUP.log"
|
|
98
|
+
echo "[ERROR: exit $cmd_exit]" >&2
|
|
99
|
+
fi
|
|
100
|
+
python "$AESOP_ROOT/tools/alert_bridge.py" --scan || true
|
|
101
|
+
release_lock "$LOCK_DIR"
|
|
102
|
+
exit $cmd_exit
|
|
29
103
|
fi
|
|
30
104
|
|
|
31
105
|
n=0
|
|
32
106
|
while true; do
|
|
33
107
|
n=$((n+1))
|
|
34
|
-
|
|
35
|
-
|
|
108
|
+
full_out=$("${CYCLE_CMD_ARRAY[@]}" 2>&1)
|
|
109
|
+
cmd_exit=$?
|
|
110
|
+
if [ $cmd_exit -eq 0 ]; then
|
|
111
|
+
out=$(echo "$full_out" | tail -2)
|
|
112
|
+
printf '%s cycle #%d\n%s\n' "$(date '+%H:%M:%S')" "$n" "$out"
|
|
113
|
+
else
|
|
114
|
+
echo "[$(date '+%F %T')] ERROR: cycle #$n failed with exit code $cmd_exit" >> "$AESOP_ROOT/state/FLEET-BACKUP.log"
|
|
115
|
+
out=$(echo "$full_out" | tail -2)
|
|
116
|
+
printf '%s cycle #%d [ERROR: exit %d]\n%s\n' "$(date '+%H:%M:%S')" "$n" "$cmd_exit" "$out"
|
|
117
|
+
fi
|
|
118
|
+
python "$AESOP_ROOT/tools/alert_bridge.py" --scan || true
|
|
36
119
|
sleep 150
|
|
37
120
|
done
|
package/dash/CLAUDE.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# dash/ — Dashboard TUI
|
|
2
|
+
|
|
3
|
+
**Purpose**: Real-time TUI fleet monitoring — watchdog daemon + agent activity display.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- **watchdog-gui.sh** — Read-only terminal UI (4s refresh, double-buffered, no flicker). Displays daemon status, backed-up repos, heartbeats, recent events. Launch in own window; never inside tool shell. CRLF-safe (no line continuations).
|
|
8
|
+
- **dash-extra.mjs** — Node.js agent activity detector. Scans AESOP_TRANSCRIPTS_ROOT for agent-*.jsonl files (last 12 min). TUI output by default; --json for web endpoints. Requires node on PATH (unavailable fallback if missing).
|
|
9
|
+
|
|
10
|
+
## State contracts
|
|
11
|
+
|
|
12
|
+
**watchdog-gui.sh reads** (heartbeat staleness thresholds applied):
|
|
13
|
+
- `state/.watchdog-heartbeat` (epoch, threshold: 300s for watchdog)
|
|
14
|
+
- `state/FLEET-BACKUP.log` (append-only, tail 3 lines)
|
|
15
|
+
- `state/SECURITY-ALERTS.log` (counts HIGH/MED, grep filtering RESOLVED-FP)
|
|
16
|
+
- `state/.watchdog-repos.json` (jq-parsed, status per repo)
|
|
17
|
+
- `state/.heartbeats/*` (epoch, per-agent thresholds: 3600s monitor, 300s watchdog, 1800s default)
|
|
18
|
+
|
|
19
|
+
**dash-extra.mjs reads**:
|
|
20
|
+
- `~/.claude/projects/**/ agent-*.jsonl` (modified time, <12min filter, top 8 recent)
|
|
21
|
+
- `state/SECURITY-ALERTS.log` (severity classification: HIGH/MED/DRIFT)
|
|
22
|
+
|
|
23
|
+
**Refresh cadence**:
|
|
24
|
+
- watchdog-gui.sh: 4s loop (infinite, Ctrl-C to exit)
|
|
25
|
+
- dash-extra.mjs: called on-demand (no built-in loop)
|
|
26
|
+
|
|
27
|
+
## Invariants & TUI conventions
|
|
28
|
+
|
|
29
|
+
- **Never modify fleet state** — both tools are read-only dashboards.
|
|
30
|
+
- **Infinite loop design** — watchdog-gui.sh runs forever; launch in dedicated terminal window, never inside tool shells.
|
|
31
|
+
- **CRLF-safe** — no bash line continuations; portable POSIX.
|
|
32
|
+
- **Node optional** — dash-extra.mjs gracefully prints "unavailable" if node missing; doesn't fail watchdog-gui.sh.
|