Files
Termind/Frameworks/TermindApplication/AgentSkillLibrary.swift
T

242 lines
11 KiB
Swift

import AgentKit
import Foundation
/// The skills Termind ships with.
///
/// They exist so the feature is something the user *edits* rather than
/// something they have to invent. A library that starts empty asks a new user to
/// guess the format, the register, and the useful level of detail before they
/// have seen one work; three real runbooks answer all three, and the fastest way
/// to write a fourth is to open one of these and change it.
///
/// Ordinary skills in every other respect. They are copied into the library on
/// first launch and are the user's from that moment: editable, disableable, and
/// deletable, with nothing putting them back. The generation mark is what makes
/// that stick — seeding installs only what a later release added, so a skill
/// deleted today does not return tomorrow.
///
/// English, in every locale, and deliberately not in the string catalog. A
/// skill's body is model-facing prose that happens to be readable — the same
/// register as `AgentPlanContractInjection.contract` — and these are records the
/// user owns and rewrites from the moment they arrive. Translating them would
/// mean either rewriting someone's edited copy when they change languages, or
/// carrying two versions of six kilobytes of shell commentary that must not
/// drift apart. The user's own skills are written in whatever language they
/// write in; these are a starting point they are free to translate or replace.
///
/// The content is deliberately about *this* app's shape of work. Two are
/// investigations that stay read-only until they have something to say, and one
/// teaches the pull/edit/push loop that keeps a remote file's contents out of
/// the conversation entirely.
public nonisolated enum AgentSkillLibrary {
/// Bumped when a release adds starter skills, never for edits to existing
/// ones — a user's copy is theirs, and rewriting it under them would throw
/// away whatever they changed.
public static let currentGeneration = 1
public static func builtIns(after generation: Int) -> [AgentSkill] {
all.filter { $0.generation > generation }.map(\.skill)
}
private static var all: [(generation: Int, skill: AgentSkill)] {
[
(1, diskFullTriage), (1, failingService), (1, safeConfigEdit),
]
}
private static var diskFullTriage: AgentSkill {
AgentSkill(
id: UUID(uuidString: "9C362C57-BCCF-4B44-A1C4-000000000001")!,
name: "disk-full-triage",
title: "Disk Full Triage",
summary: """
Find what filled a disk and decide what is safe to reclaim. Use when a filesystem \
is at or near 100%, when writes are failing with "No space left on device", or \
when someone asks why a host is out of disk.
""",
body: """
# Disk Full Triage
Work out *what* grew before proposing to delete anything. A full disk is usually one
directory, and the fix depends on which.
## 1. Find the full filesystem
```
df -h
df -i
```
Check both. A filesystem at 100% inodes with free bytes is a different problem —
millions of small files, usually a session or cache directory — and `du` will not
show it as large.
## 2. Find what is using it
Descend from the mount point that is full, one level at a time:
```
du -x -h -d 1 /var 2>/dev/null | sort -h | tail -20
```
`-x` matters: without it you walk into other filesystems and misattribute their
size. Repeat into the largest child until a single directory explains the shortfall.
## 3. Check for deleted-but-open files
If `du` totals are far below what `df` reports, something is holding a deleted file
open — a rotated log a process never reopened:
```
lsof -nP +L1 2>/dev/null | head -40
```
That space returns when the process is restarted, not when the file is removed. Say
which process, and let the user decide about restarting it.
## 4. The usual culprits
- `journalctl --disk-usage`, capped with `journalctl --vacuum-size=…`
- `/var/log` with rotation misconfigured, or `*.log.1` never compressed
- Docker: `docker system df` before `docker system prune`
- Package caches: `/var/cache/apt`, `/var/cache/yum`
- A core dump or a database backup somebody left in place
## 5. Report before removing
Say what filled the disk, how much each candidate would return, and what the risk of
each is. Removing files is the user's decision — propose it and let the approval
prompt be where it happens. Never `rm -rf` a directory you have not listed first.
"""
)
}
private static var failingService: AgentSkill {
AgentSkill(
id: UUID(uuidString: "9C362C57-BCCF-4B44-A1C4-000000000002")!,
name: "failing-service-triage",
title: "Failing Service Triage",
summary: """
Diagnose a systemd unit that will not start or keeps restarting. Use when a service \
is down, flapping, reported as failed or inactive, or when a site or API on a host \
has stopped responding.
""",
body: """
# Failing Service Triage
Read the failure before touching the service. A restart that "fixes" it without a
cause found is the same outage again in an hour.
## 1. What the unit says
```
systemctl status <unit> --no-pager -l
systemctl show <unit> -p Result -p ExecMainStatus -p NRestarts
```
`Result` and the exit status usually name the class of failure on their own:
`exit-code` is the program's own error, `signal` is a crash or an OOM kill, `timeout`
is a unit that never signalled readiness.
## 2. What the logs say
```
journalctl -u <unit> -n 200 --no-pager
journalctl -u <unit> --since "1 hour ago" -p err --no-pager
```
Read from the *first* error of the most recent start, not the last line. The last
line is usually the supervisor giving up; the first is the reason.
## 3. Rule out the ordinary causes
- **Config**: validate before restarting — `nginx -t`, `sshd -t`,
`postgres --check`, `caddy validate`. A syntax error found here costs nothing.
- **Port already bound**: `ss -lntp | grep <port>`
- **Permissions**: the unit's `User=` against the paths in its `ExecStart=`
- **Out of memory**: `journalctl -k | grep -i "killed process"`
- **Dependency**: `systemctl list-dependencies <unit> --failed`
- **Disk**: a full filesystem breaks services in unrelated-looking ways
## 4. Then act
Say what failed and why, and propose the specific change. A restart is worth
proposing only once you can say what it will fix; when you cannot, say that instead —
an unexplained flap is worth more investigation, not a restart loop.
"""
)
}
private static var safeConfigEdit: AgentSkill {
AgentSkill(
id: UUID(uuidString: "9C362C57-BCCF-4B44-A1C4-000000000003")!,
name: "safe-config-edit",
title: "Safe Remote Config Edit",
summary: """
Change a configuration file on a host without risking a broken service or an \
unrecoverable state. Use when editing nginx, sshd, systemd units, database or \
application config on a remote machine.
""",
body: """
# Safe Remote Config Edit
The order matters more than the edit. Every step below exists because skipping it is
how a config change becomes an outage you cannot log in to fix.
## 1. Pull it, do not read it into the chat
```
scratch_pull → stage the file locally
scratch_copy → keep the pristine copy to diff against
scratch_replace → change the lines you mean to change
scratch_push → send it back
```
The contents never enter the conversation, revisions cost a diff instead of another
copy of the file, and the only step the user approves is the one that reaches their
host. Use `scratch_read` on the staged copy when you need to see it, and
`scratch_diff` against the pristine copy to check what you actually changed.
## 2. Keep a way back
Before pushing, copy the original beside itself:
```
cp -a /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%Y%m%d%H%M%S)
```
`-a` preserves owner, group and mode. A backup that root can read and the service
user cannot is not a backup.
## 3. Validate before reloading
```
nginx -t
sshd -t
systemd-analyze verify /etc/systemd/system/<unit>.service
visudo -c -f <file>
```
If the tool has a check mode, it runs before the reload, every time.
## 4. Reload rather than restart, where that exists
`systemctl reload nginx` keeps connections; `restart` drops them. Reload only after
validation passed.
## 5. Never lock yourself out
Editing `sshd_config`, a firewall, or anything else in the path you are connected
over: keep the current session open, validate, reload, and prove a *new* connection
works before closing anything. Say this to the user explicitly — they are the one who
has to keep the window open.
## 6. Say what changed
Report the diff, the backup path, the validation output, and how to roll back. A
change nobody can undo in one command is not finished.
"""
)
}
}