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
403 changes: 80 additions & 323 deletions README.md

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions demo/homelab/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-onboard.sh"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-prompt.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash|Edit|Write|NotebookEdit|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-capture.sh"
}
]
}
],
"PreCompact": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-precompact.sh"
}
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-flush.sh"
}
]
}
]
}
}
22 changes: 22 additions & 0 deletions demo/homelab/.claude/throughline/HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# homelab-linkcheck — Handoff
**Last Updated:** 2026-08-29

## Resolved Issues
| Issue | Resolution | Date |
|---|---|---|
| `status.example.com` false FAIL every night since Aug 25 | Root cause: its load balancer 405s HEAD requests, GET-only. `check-links.sh` switched from `curl -I` to a GET-based check for all URLs. | 2026-08-27 |

## Pending Items
| Item | Priority | Tracking |
|---|---|---|
| Confirm the fix holds for a third consecutive clean run before calling this closed | Medium | logs/link-check.log |

## Current State
- Fix landed 2026-08-27 evening. Clean runs since: Aug 28, Aug 29. One more
clean night (Aug 30) closes this out; anything else means the
load-balancer theory was incomplete.
- `urls.txt` unchanged: 4 URLs, all on `example.com` / `status.example.com`.
- No other work in flight on this project.

## Recent Session Logs
1. [status.example.com false-FAIL root-caused and fixed](logs/handoff-2026-08-27-2140.md) — 2026-08-27
7 changes: 7 additions & 0 deletions demo/homelab/.claude/throughline/buffer/session-demo-0002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- `2026-08-29 08:05:12` **prompt** did last night's run pass for status.example.com?
- `2026-08-29 08:05:14` **bash** Tail last night's link-check results - `tail -4 logs/link-check.log`
- `2026-08-29 08:05:19` **grep** `status.example.com`
- `2026-08-29 08:05:31` **prompt** good, that's the second clean night in a row. one more and we can close the handoff item.

<!-- session-ended 2026-08-29 08:06:02 (exit) -->

61 changes: 61 additions & 0 deletions demo/homelab/.claude/throughline/logs/handoff-2026-08-27-2140.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Handoff: status.example.com nightly link-check false failure

## Objective
Find why `check-links.sh` reports `status.example.com` as failing every night
when the site is reachable and fine in a browser.

## What happened
Every run since at least Aug 25 logged `FAIL https://status.example.com/ (405)`
while the other three URLs passed. Loading the site in a browser works. Curling
it by hand with a plain GET also returns 200. Only the script's HEAD request
(`curl -I`) gets a 405.

## What we tried (including what failed)
- Assumed a transient outage at first - ruled out once the same URL failed
identically three nights running with nothing else changing.
- Compared the script's `curl -I "$url"` against `curl -sI "$url" -L` by hand:
`-L` alone didn't change the result, still 405.
- Checked the site's own status page (ironically, the URL under test) for a
known issue - nothing reported.
- Ran `curl -v -I https://status.example.com/` and read the actual response:
the load balancer in front of that site returns `405 Method Not Allowed`
specifically for `HEAD` requests, and only for that one host - `Allow: GET`
is in the response headers. `example.com`'s other three URLs happily answer
HEAD, this one doesn't.

## Progress
### Completed
Root cause confirmed: `status.example.com`'s load balancer rejects HEAD,
GET-only. Not a redirect issue, not a TLS issue, not our script's timeout.

### In progress
Fix: swap the script's `curl -I` for a GET request
(`curl -s -o /dev/null -w '%{http_code}' "$url"` instead of the `-I` variant),
so it never sends a method the target might reject. Verify the new script
against all four URLs, one clean run tonight (Aug 27), then watch two more
nightly runs before calling it closed.

### Not started
Nothing else queued on this project.

## Key learnings & gotchas
- `curl -I` sends `HEAD`, not `GET`. A URL that 405s a HEAD request but 200s a
GET request looks identical to "the site is down" in a HEAD-based checker.
`curl -v -I <url>` on a failing one shows the `Allow:` header naming what the
server actually accepts - check that before assuming an outage.
- Don't add `-L` as the fix here: it was a red herring, this host doesn't
redirect at all, it flat-out rejects the method.

## Current state
Fix is written (`scripts/check-links.sh` now uses a GET-based check for all
URLs, not just this one - simpler than special-casing one host). Not yet
proven: needs consecutive clean nightly runs, not just one manual test.

## Next steps
Check `logs/link-check.log` after tomorrow's (Aug 28) and the day after's
(Aug 29) 02:30 run. Two consecutive clean runs for `status.example.com` closes
this out; anything else means the load-balancer theory was incomplete.

## Files & resources
- `scripts/check-links.sh` - the fix
- `logs/link-check.log` - where the nightly results land
20 changes: 20 additions & 0 deletions demo/homelab/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Populated by ../setup.sh from the canonical copies at the repo root, so the
# demo never drifts from the real hooks/skills. Not tracked here.
.claude/hooks/
.claude/skills/

# The repo root's `.claude/throughline/` line is a repo-relative pattern (it
# has a `/` in the middle), so it only ever matches the repo root's own data
# dir - nothing here ignores this subtree by default. throughline's data is
# still per-operator working memory, not something a real project would
# track (see the root README's "Local by default"); the demo is the
# deliberate exception, since its throughline data IS the example. Only the
# scratch paths below need excluding - a user running the demo will see
# their own rewritten HANDOFF.md/session log/buffer as uncommitted changes,
# which is expected.
.claude/throughline/buffer/archive/
.claude/throughline/.capture-errors

# The root .gitignore ignores *.log everywhere; this one sample log is part
# of the fixture, not scratch output, so it's tracked deliberately.
!logs/link-check.log
15 changes: 15 additions & 0 deletions demo/homelab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# homelab-linkcheck

A small nightly link checker for a personal site. `scripts/check-links.sh` reads
`urls.txt`, hits each one, and appends a result line to `logs/link-check.log`. Run
from cron at 2:30am.

This is throughline's demo project: a fictional repo with a real, populated
`.claude/throughline/` already in it, so you can see the artifacts before
generating your own. See [`../../README.md`](../../README.md) for how to run it.

**One deliberate quirk.** throughline is local-only by default and normally
gitignores the whole data dir - this demo tracks its `HANDOFF.md`, a session
log, and one live capture buffer instead, because those artifacts *are* the
example. That's why a session here prints a "not gitignored yet" warning on
`buffer/` - correct behavior, aimed at a real project, not a bug in the demo.
24 changes: 24 additions & 0 deletions demo/homelab/logs/link-check.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
2026-08-25 02:30:01 OK https://example.com/ (200)
2026-08-25 02:30:02 OK https://example.com/about (200)
2026-08-25 02:30:03 OK https://example.com/blog (200)
2026-08-25 02:30:04 FAIL https://status.example.com/ (405)
2026-08-26 02:30:01 OK https://example.com/ (200)
2026-08-26 02:30:02 OK https://example.com/about (200)
2026-08-26 02:30:03 OK https://example.com/blog (200)
2026-08-26 02:30:04 FAIL https://status.example.com/ (405)
2026-08-27 02:30:01 OK https://example.com/ (200)
2026-08-27 02:30:02 OK https://example.com/about (200)
2026-08-27 02:30:03 OK https://example.com/blog (200)
2026-08-27 02:30:04 FAIL https://status.example.com/ (405)
2026-08-28 02:30:01 OK https://example.com/ (200)
2026-08-28 02:30:02 OK https://example.com/about (200)
2026-08-28 02:30:03 OK https://example.com/blog (200)
2026-08-28 02:30:04 OK https://status.example.com/ (200)
2026-08-29 02:30:01 OK https://example.com/ (200)
2026-08-29 02:30:02 OK https://example.com/about (200)
2026-08-29 02:30:03 OK https://example.com/blog (200)
2026-08-29 02:30:04 OK https://status.example.com/ (200)
2026-08-30 02:30:01 OK https://example.com/ (200)
2026-08-30 02:30:02 OK https://example.com/about (200)
2026-08-30 02:30:03 OK https://example.com/blog (200)
2026-08-30 02:30:04 OK https://status.example.com/ (200)
23 changes: 23 additions & 0 deletions demo/homelab/scripts/check-links.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
#
# Nightly link check. Reads urls.txt, GET-requests each URL, appends one
# result line per URL to logs/link-check.log. Run from cron at 2:30am.
# GET, not HEAD: status.example.com's load balancer 405s HEAD requests
# (see .claude/throughline/HANDOFF.md).

set -uo pipefail

here=$(cd "$(dirname "$0")/.." && pwd)
urls="$here/urls.txt"
log="$here/logs/link-check.log"

while IFS= read -r url; do
[ -n "$url" ] || continue
code=$(curl -s -o /dev/null -w '%{http_code}' "$url")
ts=$(date '+%Y-%m-%d %H:%M:%S')
if [ "$code" = "200" ]; then
printf '%s OK %s (%s)\n' "$ts" "$url" "$code" >> "$log"
else
printf '%s FAIL %s (%s)\n' "$ts" "$url" "$code" >> "$log"
fi
done < "$urls"
4 changes: 4 additions & 0 deletions demo/homelab/urls.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
https://example.com/
https://example.com/about
https://example.com/blog
https://status.example.com/
41 changes: 41 additions & 0 deletions demo/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
#
# Puts the hooks and skills into demo/homelab so the demo project is a real
# install, not a mockup. They live once in the repo; these copies are
# gitignored (see demo/homelab/.gitignore) and rebuilt fresh by this script.

set -euo pipefail

here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/.." && pwd)
project="$here/homelab"

rm -rf "$project/.claude/hooks" "$project/.claude/skills"
mkdir -p "$project/.claude/hooks" "$project/.claude/skills" "$project/logs"

install -m 755 "$root"/hooks/*.sh "$project/.claude/hooks/"
for skill in onboard handoff consolidate consolidate-memory; do
mkdir -p "$project/.claude/skills/$skill"
cp -R "$root/skills/$skill/." "$project/.claude/skills/$skill/"
done

cat <<'EOF'
demo/homelab is ready. Open a session scoped to just this project:

cd demo/homelab
claude --setting-sources project,local --strict-mcp-config \
--tools Read,Glob,Grep,Bash,Skill,Write

Ask: "where were we?" - the answer comes from HANDOFF.md, injected by the
SessionStart hook before your first message, not from a file you have to open.

Then try the loop: "check logs/link-check.log for last night's run and update
the handoff if the third clean night landed." Watch the capture buffer grow at
.claude/throughline/buffer/session-<id>.md as you go, then ask for a handoff
and see it distilled into HANDOFF.md and a new session log.

Claude Code will ask you to trust this folder, because it carries a project
hook in .claude/settings.json. The hooks at .claude/hooks/*.sh are copies of
the real throughline/hooks/*.sh, placed here by this script: read them before
you accept, the same as any repo you clone.
EOF
Loading
Loading