Conversation
Refine the /join skill after the auto-scope-from-git-org feature landed (PR #66). Three changes: 1. Generalize "#general" → "#<room>" throughout. The skill described the pre-auto-scope world where every airc join landed in #general; the actual output now prints "Auto-scoped: #useideem ...", "Found #useideem on your gh account → joining", etc. Examples updated to match. 2. Add explicit Monitor description guidance: description="airc" (short, stable). Previous skill wording encouraged agents to put "auto-#general" in the description — which is wrong post-feature AND looks identical across every event in the UI, so users couldn't tell what was happening. 3. New §2b "Narrate monitor events (critical UX)". Claude Code's UI only renders the Monitor description per event; event bodies don't reach the user unless the agent echoes them. Skill now explicitly directs: one short chat line per event, paraphrase peer messages, stay silent on routine noise, surface state changes. This was the UX regression observed during the rollout — two tabs paired in #useideem but the user saw only a wall of identical "Monitor event" lines with no content. Plus one troubleshooting bullet: after `airc canary` / `airc update`, the running monitor still uses the old binary; teardown + rejoin to pick up new code. Exactly what bit the authenticator-side Claude on Monday demo morning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New top-level section "Auto-scope — the default room" with:
* The rule (git remote owner → parent-dir → #general)
* Worked example with a generic ~/work/my-org/* + ~/work/cambriantech/* layout
* "Scoping is the default, not a wall" subsection explaining airc list
+ airc join --room <other> + AIRC_NO_AUTO_ROOM=1 for cross-room hops
- Swap the old "The Magic" bullet from a verbose inline description
to a short pitch + link into the new section.
- Update "Same gh account" setup to reference the auto-scope room
rather than saying "hosts #general" (now only true when auto-scope
falls through).
- Table row + skill descriptions + quick-start comments: rename
"Auto-#general" to "Auto-scope" throughout.
Also scrub the skill file of PII from the original rollout (per
Joel): no joelteply/useideem references, no "Joel's Mac / Brian's
Linux box" phrasings. Generic my-org / your-username examples +
cambriantech where a concrete name helps (public, self-promoted).
Pairs with the auto-scope feature landed in #66; completes the
"make the docs match the behavior" pass that the rollout demo
surfaced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mental-model table appears very early in the README (§"The mental model: IRC..."), so a stale "airc join (auto-joins #general)" row misleads first-time readers before they ever reach the Auto-scope section. Replace with a one-line description that matches the feature and anchors into #auto-scope--the-default-room for the full worked example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…larity docs+skills(join): align with auto-scope, add narrate-events guidance, scrub PII
The 12-char truncation was producing awkward stubs in the auto-derived agent name. `~/Development/ideem/authenticator` came back as `authenticato-fd63` instead of `authenticator-fd63`; the missing trailing 'r' was an immediate cognitive speed bump for anyone trying to match agent names to repos in a busy multi-tab room. Bump the cap to 20 chars so common repo names fit cleanly: authenticator → authenticator-d1f4 vHSM → vhsm-d1f4 trusted-app-service → trusted-app-service-993e merchant-browser-sim → merchant-browser-sim-XXXX airc → airc-d1f4 Also tighten the sanitization pipeline: collapse runs of dashes (`cambriantech--airc-src` → `cambriantech-airc-src`), strip leading and trailing dashes, and re-strip after the cut so a truncation that falls on a dash doesn't leave a dangling tail. Pure cosmetic — safer default, no behavior change beyond the cap. Within an auto-scoped room, every peer is from the same org by construction (see infer_repo_org from #66), so the org prefix would just be redundant noise on every name. The repo basename + 4-char hash is the right granularity for the room's audience. Existing names persist in `<scope>/.airc/identity` — derive_name fires on first-init, not on every connect. To pick up the new form in an existing scope: `airc nick <new>` for in-place rename, or `airc teardown --flush && airc join` for a fresh derive. 97/97 integration tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(derive_name): bump basename cap 12 → 20 so repo names fit
Adds structured agent persona on top of the bootstrap name from
derive_name. Five fields, all optional, all settable post-pair:
pronouns she/they/he/it for grammatical narration
role short hyphenated tag, e.g. device-link-orchestrator
bio one-line free-form, IRC-realname analog
status mutable "what I'm working on now" (Slack-like)
integrations {platform: handle} mappings (continuum, slack, …)
Stored under config.json's `identity` key (single-file scope) and
exchanged at pair-handshake time so peers cache each other's identity
locally and `airc whois` works without round-trips.
New commands:
airc identity show
airc identity set [--pronouns X] [--role Y] [--bio "…"] [--status "…"]
airc identity link <platform> <handle>
airc identity import <platform>:<id> # continuum live, others stub
airc identity push <platform> # continuum live, others stub
airc whois [<peer>] # self / host / paired peer / cross-peer-via-host
airc kick <peer> [reason] # host-only IRC-style kick
Handshake protocol additions (backward compatible — missing fields
default to {}, so a v1 host pairing with a legacy joiner just gets
empty identity):
- Joiner sends `identity` blob alongside ssh_pub/sign_pub
- Host stores it in peers/<jname>.json under `identity`
- Host returns own identity in response; joiner caches as host_identity
- Cross-peer WHOIS reads remote peer file via single SSH cat
Skill updates (skills/join/SKILL.md):
- New §2a "Identity bootstrap" — prompt agent to fill empty fields
on every /join with persistent (non-nagging) re-prompts, skip
via AIRC_NO_IDENTITY_PROMPT=1
- §2b updated — auto-WHOIS on peer-join so context loads when a
new peer joins the room
Test harness fix (incidental but load-bearing):
cleanup_dirs was silently broken on macOS — `find /tmp -maxdepth 1`
doesn't traverse the /tmp → /private/tmp symlink, so stale state from
previous suite invocations leaked forward and caused flaky
scenario_identity failures (saw "pronouns: they" survive into a
fresh run). Resolve to canonical path before walking. Linux
unaffected.
cmd_connect's host-mode + joiner-mode config writes converted from
heredoc to python json-merge so the `identity` block survives
re-pairs (teardown + airc connect), matching user expectation that
identity is sticky like nick.
Three new integration scenarios (29 new assertions, 126/126 total):
scenario_identity — local set/show/link/whois/persistence
scenario_whois — handshake exchange (host→joiner direction)
scenario_kick — host kicks joiner; joiner kick attempt refused
Deferred to follow-ups (not blocking this PR):
- airc.ps1 parity for Windows (talking to green on continuum side)
- airc ban (permanent block, complement to kick)
- Live slack/telegram/discord import-push (continuum is the v1
integration that actually works)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(identity): agent identity layer + cross-platform linking (closes #34)
- IRC-vs-airc table: add WHOIS / AWAY-style status / kick / USER-realname / cross-platform-identity rows so first-time readers see the IRC-parity story. - "The Magic" bullet: new entry covering the bootstrap-prompt moment + cross-platform integrations. - Core Commands: new "Identity" subsection listing show/set/link/ import/push/whois/kick. - New "Agent identity & WHOIS" section with the JSON shape, the field table, the bootstrap behavior, the handshake-time exchange story, the link-don't-duplicate pattern, and the kick semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(readme): cover the new identity layer (post-#70)
…ad-bearing) The Windows port has shipped (install.ps1, airc.ps1, install.sh Git Bash compat all merged), and PR #70's identity layer has surfaced the follow-up parity work for green's PS1 side directly via airc DM rather than this file. The doc was useful as a handoff-tracker during the two-tier port; it's now stale and clutters the repo root for fresh clones. Per Joel: "WINDOWS-PORT-STATUS.md can go now if it is now complete no need for a doc sitting there." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…us-doc chore: drop WINDOWS-PORT-STATUS.md (port shipped)
chore: merge main → canary (resolve README conflict for #73 promote)
There was a problem hiding this comment.
Pull request overview
Promotes canary → main by landing the new identity layer (handshake exchange + CLI), fixing derive_name truncation/sanitization, and updating docs/skills for IRC-parity + auto-scope guidance.
Changes:
- Add identity management commands (
airc identity ...),airc whois, andairc kick, including handshake-time identity exchange. - Improve
derive_name(basename cap 12 → 20, tighter dash sanitization) and preserve identity when reconnecting by mergingconfig.json. - Extend integration tests + update README and join skill docs; fix macOS
/tmpcleanup in the test harness.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
airc |
Implements identity layer, whois/kick, handshake identity caching, and derive_name update. |
test/integration.sh |
Fixes /tmp cleanup on macOS; adds new identity/whois/kick scenarios. |
skills/join/SKILL.md |
Updates join skill for auto-scope defaults + identity bootstrap + event narration guidance. |
README.md |
Documents auto-scope and the new identity/WHOIS/kick command set. |
WINDOWS-PORT-STATUS.md |
Removes obsolete Windows port tracking document. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| _identity_link() { | ||
| local platform="${1:-}" handle="${2:-}" | ||
| [ -z "$platform" ] && die "Usage: airc identity link <platform> <handle>" |
There was a problem hiding this comment.
airc identity link supports unlinking by omitting/blanking the handle, but the usage/error text still implies <handle> is required. Update the usage string (and the cmd_identity help line) to reflect that the handle is optional and that an empty handle unlinks, so users aren't misled.
| [ -z "$platform" ] && die "Usage: airc identity link <platform> <handle>" | |
| [ -z "$platform" ] && die "Usage: airc identity link <platform> [handle] (omit or pass empty handle to unlink)" |
| # Pull peer's SSH pubkey out of authorized_keys (best-effort — pubkey | ||
| # lives in $PEERS_DIR/$target.pub if recorded; otherwise we leave the | ||
| # ssh key in place and rely on peer-file deletion to break the | ||
| # bookkeeping). Each step `|| true` since set -euo pipefail is active | ||
| # and benign no-match / missing-file cases are normal here. | ||
| local peer_pub_file="$PEERS_DIR/$target.pub" | ||
| if [ -f "$peer_pub_file" ] && [ -f "$HOME/.ssh/authorized_keys" ]; then | ||
| local peer_pub; peer_pub=$(cat "$peer_pub_file" 2>/dev/null || echo "") | ||
| if [ -n "$peer_pub" ]; then | ||
| # grep -v returns 1 when every line matches (or the file is empty); | ||
| # both are fine outcomes here, so eat the exit code. | ||
| grep -vF "$peer_pub" "$HOME/.ssh/authorized_keys" > "$HOME/.ssh/authorized_keys.tmp" 2>/dev/null || true | ||
| [ -f "$HOME/.ssh/authorized_keys.tmp" ] && mv "$HOME/.ssh/authorized_keys.tmp" "$HOME/.ssh/authorized_keys" | ||
| chmod 600 "$HOME/.ssh/authorized_keys" 2>/dev/null || true | ||
| fi |
There was a problem hiding this comment.
cmd_kick attempts to remove the peer's SSH key from ~/.ssh/authorized_keys using $PEERS_DIR/$target.pub, but the host handshake writes peers/<name>.pub from sign_pub (the signing key), not ssh_pub. As a result, kick will usually leave the joiner's SSH key authorized, so the peer may still be able to SSH even after being “kicked”. Store the joiner’s ssh_pub in the peer record (e.g. in <peer>.json) and use that value for authorized_keys removal, and adjust the confirmation message accordingly.
| # ── Peer file gone ── | ||
| [ ! -f /tmp/airc-it-k-h/state/peers/kjoiner.json ] \ | ||
| && pass "kicked peer's file removed" \ | ||
| || fail "peer file still present after kick" |
There was a problem hiding this comment.
scenario_kick's header comment says kick removes the peer's pubkey from authorized_keys, but the scenario never asserts that the joiner’s SSH public key was actually removed from the host’s ~/.ssh/authorized_keys. Adding an explicit before/after grep for the joiner’s ssh_key.pub would both cover the advertised behavior and catch regressions in the key-removal implementation.
| ensure_init | ||
| local sub="${1:-show}" | ||
| shift 2>/dev/null || true | ||
| case "$sub" in | ||
| show|"") _identity_show ;; | ||
| set) _identity_set "$@" ;; | ||
| link) _identity_link "$@" ;; |
There was a problem hiding this comment.
cmd_identity shifts two arguments after reading the subcommand (shift 2), which drops the first flag for set/link/import/push (e.g., airc identity set --pronouns they becomes args=["they"] and fails parsing). Change this to shift only the subcommand (or conditionally shift based on arg count) so flags are preserved.
| # Local peer file — flat layout: $PEERS_DIR/<peer>.json | ||
| local peer_file="$PEERS_DIR/$target.json" | ||
| if [ -f "$peer_file" ]; then | ||
| local blob; blob=$(python3 -c " | ||
| import json | ||
| p = json.load(open('$peer_file')) | ||
| print(json.dumps(p.get('identity', {}) or {})) | ||
| " 2>/dev/null) | ||
| local host; host=$(python3 -c " | ||
| import json | ||
| print(json.load(open('$peer_file')).get('host', '')) | ||
| " 2>/dev/null) | ||
| _whois_pretty "$target" "$blob" "$host" | ||
| return 0 | ||
| fi | ||
|
|
||
| # Cross-peer via host (we're a joiner; query host's peer file remotely) | ||
| local host_target; host_target=$(get_config_val host_target "") | ||
| local host_airc_home; host_airc_home=$(get_config_val host_airc_home "") | ||
| if [ -n "$host_target" ] && [ -n "$host_airc_home" ]; then | ||
| local remote_blob | ||
| remote_blob=$(relay_ssh "$host_target" "cat $host_airc_home/peers/$target.json 2>/dev/null" 2>/dev/null || true) | ||
| if [ -n "$remote_blob" ]; then | ||
| local peer_id; peer_id=$(printf '%s' "$remote_blob" | python3 -c ' |
There was a problem hiding this comment.
cmd_whois uses the user-supplied target directly in local/remote file paths (e.g. peer_file="$PEERS_DIR/$target.json" and cat $host_airc_home/peers/$target.json). This allows path traversal (e.g. ../config) and, in the remote case, shell injection if target contains metacharacters. Validate target against the same safe charset as nicknames (e.g. ^[a-z0-9-]+$) and/or properly quote/escape the remote path before passing it to SSH.
| ensure_init | ||
| local target="${1:-}" | ||
| [ -z "$target" ] && die "Usage: airc kick <peer> [reason]" | ||
| shift || true | ||
| local reason="${*:-no reason given}" | ||
|
|
||
| # Joiner role check — kicking only makes sense as host. | ||
| local host_target; host_target=$(get_config_val host_target "") | ||
| if [ -n "$host_target" ]; then | ||
| die "kick: only the room host can kick. You are a joiner of $host_target — talk to the host." | ||
| fi | ||
|
|
||
| local peer_file="$PEERS_DIR/$target.json" | ||
| if [ ! -f "$peer_file" ]; then | ||
| die "kick: '$target' not in peers list (try: airc peers)" |
There was a problem hiding this comment.
cmd_kick constructs peer_file="$PEERS_DIR/$target.json" from the unsanitized CLI argument. A value like ../config would match an existing file and then be deleted by rm -f, which is an easy footgun and a path traversal vulnerability. Reject peer names that don't match the allowed nickname format (e.g. ^[a-z0-9-]+$) before using them in filesystem paths.
Five concrete fixes from Copilot's review of #73 (one false positive about a `shift 2>/dev/null` mis-read as `shift 2` left as-is): 1. **kick was removing the wrong pubkey** — `<peer>.pub` holds the signing key (sign_pub), not the SSH auth key. cmd_kick was reading that file and trying to grep it out of authorized_keys, which never matched. Result: kicked peers kept SSH access. Now host handshake stores `ssh_pub` in the peer JSON record; cmd_kick reads from there. Falls back to a clear "no SSH key recorded" warning for pre-#34 peers (whose handshake didn't carry ssh_pub). 2. **path traversal in cmd_whois / cmd_kick** — peer name was interpolated into local file paths AND remote SSH cat commands without sanitization. `airc whois ../config` would have been happily resolved against the joiner's host. New _validate_peer_name helper enforces `[a-z0-9-]+` (same charset cmd_rename already uses), no leading dash. Wired into both commands. 3. **kick test never asserted authorized_keys removal** — scenario covered the peer file removal but not the SSH key revocation, which is the actual security boundary. Now greps authorized_keys for the joiner's pubkey before/after kick. Would have caught (1) immediately. 4. **identity link usage string lied** — claimed `<handle>` was required, but the code actually unlinks when handle is omitted. Updated usage + help to reflect optional/blank handle = unlink. 5. **path-traversal regression test** — scenario_kick now also asserts that `airc whois ../config` and `airc kick ../config` both fail with "invalid peer name". Defense in depth. 130/130 integration tests pass (4 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Joel: airc is the chat substrate that came out of continuum work, and the broader claim is that collaborative agentic systems are the unlock. One italic line right under the title to set context for fresh-clone readers before they hit the install instructions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: address Copilot review on #73 (kick auth bug + path traversal) + README tagline
Three changes: 1. **cmd_rename leading-dash regression** — the leading-dash check fired BEFORE sanitization, so input like `.foo` slipped past (the case check) and emerged as `-foo` after `[^a-z0-9-]/-/g` substitution. The resulting nick was unreachable to airc whois / airc kick (both reject leading-dash via _validate_peer_name). Fix: align cmd_rename's sanitization pipeline with derive_name's — collapse dash runs, strip leading and trailing dashes after sed AND after the cap. New test in scenario_identity asserts `airc nick .dottyname` produces a non-leading-dash result. 2. **scenario_kick guard improvement** — the authorized_keys assertions were guarded by `if [ -n "$kj_ssh_pub" ] && [ -f auth ]`, which silently skipped the entire kick-revocation check if either precondition was missing. Both should always be present here (init_identity always generates ssh_key.pub; pair handshake always creates authorized_keys). Now: each precondition is its own pass/ fail, and a missing one short-circuits with cleanup_all + return so the kick assertion isn't silently elided. 3. **README title — pure shell** — add a hyphen-tagline to the H1 so the technical pitch (no compiled binary, no runtime install, just bash + PowerShell scripts) is the first thing fresh-clone readers see. Per Joel after pulling up the GitHub language breakdown: 64% Shell, 35% PowerShell, 0.1% Batchfile. 133/133 integration tests pass (3 new — 2 from scenario_kick guard expansion, 1 in scenario_identity for the dash strip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Copilot's nit on PR #76 + Joel's call: "pure shell" in the title overstated since python3 is a system dep (used for inline JSON parsing throughout airc). Pull the hyphen out of the H1 and fold the shell-scripts-only claim into the existing descriptive tagline so it lands as part of "what this is" rather than as a misleading title suffix. Title is back to the plain project name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: PR #75 Copilot follow-up + 'pure shell' title hyphen
Per Joel: the load-bearing claim isn't "no Python" (Python IS a system
dep, used for inline JSON parsing throughout airc). It's "no real
changes to your system as it is — you have/need gh and python anyway,
so the project isn't some dependency nightmare." The audience is devs
and tech enthusiasts who already have these.
So:
- Pull "shell scripts only (bash + PowerShell), no compiled binary"
out of the italic tagline (where it overstated). Tagline is back to
the cleaner thesis-only framing.
- Move the claim — explained — into the blockquote's closing line:
"The whole thing is shell scripts ... the prereqs (git, gh, python)
are things any developer's machine already has." That lands the
point ("no system pollution") without a misleading title-level
shorthand.
- While there: replace the stale "you're in #general" with "you're in
your project's room" so the blockquote matches the auto-scope
feature shipped in #66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ou-have docs(readme): leverage-what-you-have framing in the blockquote
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| local remote_blob | ||
| remote_blob=$(relay_ssh "$host_target" "cat $host_airc_home/peers/$target.json 2>/dev/null" 2>/dev/null || true) |
There was a problem hiding this comment.
The remote whois path builds an SSH command with an unquoted $host_airc_home (from config/handshake) embedded into the remote shell command. If the path contains spaces or shell metacharacters, this will fail; worse, a malicious/compromised host could inject additional shell syntax via host_airc_home. Consider robust quoting/escaping (or avoid the remote shell entirely by invoking a remote python/json reader with a safely-passed file path) before concatenating into an SSH command string.
| local remote_blob | |
| remote_blob=$(relay_ssh "$host_target" "cat $host_airc_home/peers/$target.json 2>/dev/null" 2>/dev/null || true) | |
| local remote_blob remote_peer_file remote_peer_file_q | |
| remote_peer_file="$host_airc_home/peers/$target.json" | |
| remote_peer_file_q=$(python3 -c ' | |
| import shlex, sys | |
| print(shlex.quote(sys.argv[1])) | |
| ' "$remote_peer_file" 2>/dev/null) | |
| remote_blob=$(relay_ssh "$host_target" "cat -- $remote_peer_file_q 2>/dev/null" 2>/dev/null || true) |
| ident[k] = v | ||
| ints = ident.setdefault("integrations", {}) | ||
| ints["continuum"] = src.get("name", "") | ||
| json.dump(c, open(os.environ["CONFIG"], "w"), indent=2) |
There was a problem hiding this comment.
In the continuum import merge, ints["continuum"] is set to src.get("name", "") even when JSON parsing fails (src becomes {}), which overwrites any existing continuum link with an empty string. It would be safer to only update the integrations mapping when the imported persona name is non-empty (or fall back to the requested id), otherwise keep the existing link intact.
| AIRC_HOME="$home/state" "$AIRC" nick ".dottyname" >/dev/null 2>&1 || true | ||
| local renamed; renamed=$(python3 -c "import json; print(json.load(open('$home/state/config.json')).get('name',''))" 2>/dev/null) | ||
| case "$renamed" in | ||
| -*) fail "airc nick produced leading-dash name '$renamed' — sanitization regression" ;; | ||
| "") fail "airc nick wrote empty name — sanitization regression" ;; | ||
| *) pass "airc nick strips leading dash post-sanitization (got '$renamed')" ;; | ||
| esac |
There was a problem hiding this comment.
This nick sanitization regression check can false-pass because the airc nick invocation ignores failures (|| true). If the command errors for an unrelated reason, $renamed may remain the old valid name and the test still passes. Consider asserting airc nick exits 0 and/or that the stored name actually changed to the expected sanitized value before checking the leading-dash invariant.
| AIRC_HOME="$home/state" "$AIRC" nick ".dottyname" >/dev/null 2>&1 || true | |
| local renamed; renamed=$(python3 -c "import json; print(json.load(open('$home/state/config.json')).get('name',''))" 2>/dev/null) | |
| case "$renamed" in | |
| -*) fail "airc nick produced leading-dash name '$renamed' — sanitization regression" ;; | |
| "") fail "airc nick wrote empty name — sanitization regression" ;; | |
| *) pass "airc nick strips leading dash post-sanitization (got '$renamed')" ;; | |
| esac | |
| local renamed | |
| if AIRC_HOME="$home/state" "$AIRC" nick ".dottyname" >/dev/null 2>&1; then | |
| renamed=$(python3 -c "import json; print(json.load(open('$home/state/config.json')).get('name',''))" 2>/dev/null) | |
| case "$renamed" in | |
| -*) fail "airc nick produced leading-dash name '$renamed' — sanitization regression" ;; | |
| "") fail "airc nick wrote empty name — sanitization regression" ;; | |
| "dottyname") pass "airc nick strips leading dash post-sanitization (got '$renamed')" ;; | |
| *) fail "airc nick wrote unexpected sanitized name '$renamed' (expected 'dottyname')" ;; | |
| esac | |
| else | |
| fail "airc nick .dottyname failed unexpectedly" | |
| fi |
| - **Open a new tab.** `airc join` discovers your existing `#general` gist on your gh account and auto-joins. **No string typed.** | ||
| - **Open a new machine.** Same gh account, same `airc join`, same auto-join. The mesh extends across the internet via gh. | ||
| - **`cd` into a git repo → land in the right room automatically.** `airc join` from any `useideem/*` checkout defaults to `#useideem`; any `cambriantech/*` checkout defaults to `#cambriantech`; any `joelteply/*` personal project defaults to `#joelteply`. The room name comes from the git remote's owner, so Joel's Mac and Brian's Linux box agree on the room without coordinating paths. Non-git dirs fall through to `#general` (the lobby). Override any time with `--room <name>` or `AIRC_NO_AUTO_ROOM=1`. | ||
| - **`cd` into a git repo → land in the right room automatically.** `airc join` with no flags defaults to a room named after the git remote's owner, so your work org's repos converge in one channel, your side projects converge in another, and you don't have to think about it. See **[Auto-scope — the default room](#auto-scope--the-default-room)** for the worked example. Non-git dirs fall through to `#general` (the lobby). Override any time with `--room <name>` or `AIRC_NO_AUTO_ROOM=1`, and `airc list` + `airc join --room <other>` lets any agent hop across rooms at will — scoping is the default, not a wall. |
There was a problem hiding this comment.
This bullet still claims airc join auto-joins #general, but the default behavior described immediately below is now auto-scope to the current repo org (with #general only as a fallback). Updating this line to match the new default (or explicitly qualify it as the non-git-dir fallback case) would prevent reader confusion.
| import json, os | ||
| try: | ||
| c = json.load(open(os.environ["CONFIG"])) | ||
| except Exception: | ||
| c = {} | ||
| c["name"] = os.environ["MY_NAME"] | ||
| c["host"] = os.environ["MY_HOST"] | ||
| c["host_target"] = os.environ["SSH_TARGET"] | ||
| c["created"] = os.environ["CREATED"] | ||
| json.dump(c, open(os.environ["CONFIG"], "w"), indent=2) |
There was a problem hiding this comment.
Config writes here use an in-place json.dump(..., open(CONFIG, "w")), which can transiently leave a partially-written config.json if another process reads mid-write (the integration test later adds a sleep to avoid flaking on half-written config). Consider switching to an atomic write (write to a temp file, fsync, then rename/replace) so readers either see the old or new full JSON.
| import json, os | |
| try: | |
| c = json.load(open(os.environ["CONFIG"])) | |
| except Exception: | |
| c = {} | |
| c["name"] = os.environ["MY_NAME"] | |
| c["host"] = os.environ["MY_HOST"] | |
| c["host_target"] = os.environ["SSH_TARGET"] | |
| c["created"] = os.environ["CREATED"] | |
| json.dump(c, open(os.environ["CONFIG"], "w"), indent=2) | |
| import json, os, tempfile | |
| config_path = os.environ["CONFIG"] | |
| try: | |
| c = json.load(open(config_path)) | |
| except Exception: | |
| c = {} | |
| c["name"] = os.environ["MY_NAME"] | |
| c["host"] = os.environ["MY_HOST"] | |
| c["host_target"] = os.environ["SSH_TARGET"] | |
| c["created"] = os.environ["CREATED"] | |
| config_dir = os.path.dirname(config_path) or "." | |
| fd, tmp_path = tempfile.mkstemp(dir=config_dir, prefix=".config.", suffix=".tmp") | |
| try: | |
| with os.fdopen(fd, "w") as f: | |
| json.dump(c, f, indent=2) | |
| f.flush() | |
| os.fsync(f.fileno()) | |
| os.replace(tmp_path, config_path) | |
| finally: | |
| if os.path.exists(tmp_path): | |
| os.unlink(tmp_path) |
| # Host mode: clear any leftover host_target/host_name from a prior | ||
| # joiner run in this scope (avoid mis-reading ourselves as a joiner). | ||
| for k in ("host_target", "host_name", "host_port", "host_airc_home", "host_ssh_pub", "host_identity"): | ||
| c.pop(k, None) | ||
| json.dump(c, open(os.environ["CONFIG"], "w"), indent=2) |
There was a problem hiding this comment.
Same atomicity concern as the joiner branch: this merges into config.json with an in-place write. Since other subprocesses (monitor/identity/whois) can read config.json while airc connect is running, a non-atomic write risks corrupted/empty reads. Prefer an atomic write pattern (temp file + replace) to make config updates crash-safe and race-safe.
Promote canary → main. Six commits across four PRs since the last promote (#68).
What's landing
skills(join): align with auto-scope + narrate-events guidance— fixes stale#generalreferences; adds §2a identity bootstrap; adds §2b mandatory event-narration; adds canary/update troubleshooting bullet.fix(derive_name): bump basename cap from 12 to 20 chars— closes theauthenticato-fd63truncation bug visible to fresh-clone users on main today. Sanitization tightened (collapse double-dashes, strip leading/trailing).feat(identity): agent identity layer with cross-platform linking (#34)— pronouns/role/bio/status/integrations on top of derive_name, exchanged at pair-handshake; newairc identity show/set/link/import/push,airc whois,airc kick. Backward-compat with existing peers (missing fields default to{}).docs(readme): cover the new identity layer— IRC-vs-airc table additions, Magic bullet, Core Commands subsection, full "Agent identity & WHOIS" section.chore: drop WINDOWS-PORT-STATUS.md— port shipped, tracker no longer load-bearing.Plus a load-bearing test-harness fix folded into #70:
cleanup_dirswas silently broken on macOS becausefind /tmp -maxdepth 1doesn't traverse the/tmp → /private/tmpsymlink. Fixed by resolving to canonical path before walking. Linux unaffected.Sign-off status
#useideemhost scope{}identity) so the risk is asymmetric feature-set, not breakage.Out of scope (already tracked, not blocking)
identity / whois / kickcommands (coordinating with green)airc ban(permanent block, complement to kick)🤖 Generated with Claude Code