diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2ab08d..41d9583 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,16 +10,22 @@ jobs: smoke: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: skills/video-transcript-downloader/package-lock.json + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.0 + - name: Check shell helper syntax run: | + bash -n scripts/sync-skills + bash -n scripts/test-sync-skills find skills -path '*/scripts/*' -type f -print0 | while IFS= read -r -d '' script; do if head -n 1 "$script" | grep -q 'bash'; then @@ -27,9 +33,49 @@ jobs: fi done + - name: Test skill link sync and audit + run: | + sudo apt-get update + sudo apt-get install -y ripgrep + /bin/bash scripts/test-sync-skills + - name: Test ClawSweeper status helper run: skills/clawsweeper-status/scripts/clawsweeper-status.test.sh + - name: Test Xcode CLI smoke and rollback (synthetic) + run: /bin/bash skills/xcode-sync/scripts/xcode-post-install-smoke.test.sh + + - name: Test npm helpers (offline, synthetic) + run: | + test_home="$(mktemp -d)" + trap 'rm -rf "$test_home"' EXIT + timeout 60s env -i HOME="$test_home" PATH="$PATH" node --test skills/npm/scripts/*.test.mjs .git`. 3rd-party/OSS: `~/Projects/oss`. -- `../agent-skills` = `https://github.com/openclaw/agent-skills`, not `steipete/agent-skills`; canonical public OpenClaw shared skills. -- "Make a note" here => terse `AGENTS.MD` edit. No separate `CLAUDE.md` here. -- `ship` => changelog, commit in groups, push, pull. -- Version bumps only on explicit release request. Release = GitHub Release; npm publish too when package applies. -- Release closeout: after verified release, bump changelog to next patch `Unreleased`, commit. -- Release verify: confirm release docs/notes contain changelog; if missing/stale, fix before closeout. -- npm release verify: `npm view @` shows version, dist-tag, tarball, integrity, publish time; GitHub tag + Release exist; Release body links npm version page, registry tarball, integrity, CI/proof. -- Changelogs: match file style; prefer one bullet per entry on one line. Do not hard-wrap changelog bullets just because prose is long. -- Skills are canonical for tool workflows. Keep this file to hard rules only. -- Editing here/skills: token-efficient, relaxed grammar, terse descriptions. -- Skill descriptions: short generic trigger phrase, not summary; no personal names, long paths, or workflow narration unless needed for routing. -- Skill frontmatter: quote `description`; after SKILL.md edits, YAML-parse frontmatter before commit. -- Read `~/Projects/agent-scripts/tools.md` when the tool catalog matters. -- Internal handling: private agent conversations and authenticated organization-approved systems are internal contexts. Use task-relevant non-public information there, including internal names, links, systems, processes, and personnel; answering the authorized user is not public exposure. -- External disclosure: never publish, upload, or send non-public organizational information to public audiences, external recipients, or unapproved services without explicit user approval for both content and destination. -- Secrets: never reveal credential or secret values, even internally. Use approved secret tooling and redact outputs. -- Unclear audience/destination: ask before external transmission; confidentiality alone must not block internal research or answers. -- Image uploads: verify destination approval before uploading any image or screenshot. Personal devices: user-requested destinations allowed, subject to external-disclosure rules. Work-managed devices: external uploads default-deny; proceed only when both content and destination are explicitly approved for the device and data classification. Never upload potentially confidential/internal images to social media, public image hosts, or unapproved AI/vision services. Unclear device context, sensitivity, or approval: stop and ask. Local-only processing allowed. -- Personal GitHub repos: use Peter's personal GitHub authorization (`steipete`), not `steipete-oai`, for pushes/writes. -- Peter Macs: expect both GitHub auth contexts to exist on each Mac: `steipete` and `steipete-oai`. +- Create or enter a plan only when the user explicitly asks for one; otherwise proceed directly. +- Workspace: `~/Projects`. Missing steipete repo: clone `https://github.com/steipete/.git`. Other OSS: `~/Projects/oss`. +- `../agent-skills` = canonical public OpenClaw skills, `https://github.com/openclaw/agent-skills`; never `steipete/agent-skills`. +- "Make a note" here = terse `AGENTS.MD` edit. No separate `CLAUDE.md`. +- `ship` = grouped commits, push, pull. "Shipped" = pushed to GitHub. +- Version/artifact publication needs explicit `release`/`publish` ask. Release = GitHub Release; npm publish when applicable. Tag/push alone != released. +- Verified release done: verify the finalized changelog matches the published release; follow the repository's post-release `Unreleased` convention. Only `openclaw/openclaw`: do not prefill the next release. +- Release verify: docs/notes contain current changelog. Missing/stale: fix before closeout. +- npm release verify: `npm view @` proves version, dist-tag, tarball, integrity, publish time. GitHub tag + Release exist. Release body links npm version page, registry tarball, integrity, CI/proof. +- Changelogs: every repository except `openclaw/openclaw` maintains entries for user-visible fixes and features as work lands or ships, normally under `Unreleased`. Only `openclaw/openclaw` generates or edits changelogs at release time; keep its pending release-note context in PR bodies or commits. This exception is for that exact repository, not the `openclaw` organization: `openclaw/gogcli` and all other projects retain normal changelog maintenance. Match house style; prefer one-line bullets without prose-length hard-wrap. +- Skills own tool workflows. This file: hard rules only. +- Agent transcripts: omit by default and never ask, even if repo/skill guidance offers one; include only on explicit request. +- Private agent chat + authenticated org-approved systems = internal. Use task-needed non-public names, links, systems, processes, people. Answering authorized user != public disclosure. +- External disclosure: no non-public org info to public audience, external recipient, or unapproved service without explicit approval of both content + destination. +- Public model naming: never expose internal, prerelease, routing, or codename model identifiers in source code, commits, PRs, issues, comments, release notes, logs, or proof commands. Use the stable public model ID (for example `gpt-5.6-sol`) when known; otherwise say only `Codex` or omit the model. Sanitize `--model` flags and copied transcripts before posting. +- Secrets: never reveal values, even internal. Approved secret tools; redact output. +- Audience/destination unclear: ask before external send. Confidentiality alone no block on internal research/answers. +- Synthetic proof screenshots/recordings: pre-approved for the task's already-authorized PR/issue or explicitly requested destination, on any host. Inspect the full capture for incidental secrets, real/private data, internal identifiers, or unrelated desktop content. Verified synthetic-only captures need no device-classification or repeat upload-approval question. Real/mixed/uncertain content and unrelated destinations are not covered. +- Other image/screenshot uploads: first verify destination approval. Personal device: user-requested destination okay, external-disclosure rules still apply. Work device: external upload default deny; need explicit content + destination approval for device/data class. Never send possibly confidential/internal image to social media, public image host, or unapproved AI/vision service. Device/sensitivity/approval unclear: stop + ask. Local-only processing okay. +- Personal GitHub repos: push/write as `steipete`, never `steipete-oai`. +- Peter Macs: expect both GitHub auths: `steipete`, `steipete-oai`. +- Mac Studio identity: `steipete-macstudio.local` / `peters-mac-studio-1` and `steipete-studio-sf-worker` / `steipete-studio-sf-worker.local` = personal (Peter-confirmed). Do not ask again for these known hosts. A separate corporate Mac Studio exists; verify only genuinely unknown hosts, never infer class from a similar name. +- `manager`, `conferences`, `agent-scripts` = internal ops/note repos. After task change: validate, commit, push, pull/verify, leave clean. No push approval needed. ## Routing -- Screenshots/assets: newest PNG in `~/Desktop` or `~/Downloads`; verify UI before replacing. -- Screenshot/live UI bugs: verify with `$browser-use` against the existing Chrome profile. `curl`/source proof is supporting only; no Playwright/Puppeteer/in-app browser for login/profile-dependent pages unless explicitly requested. -- Private/history: local archives first; verify freshness for current questions. -- Secrets/API keys/live creds: use `$one-password`; env only if already exported; `op` is skill/tmux-only, no broad enumeration/secret output. -- 1Password interactive timeout: use `sag` to call Peter aloud; keep tmux/session alive, retry after unlock. Do not silently abandon. -- New API keys: immediately store in 1Password service account via `$one-password`; temp files/env only for current task. -- User-owned Gmail service logins: pre-approved; use saved credentials without asking. Account creation, keys, permissions, and other persistent access remain separate actions. -- Email/Google account for testing: use `gog+clawdbot@gmail.com`. -- OpenClaw deployments: use `service@openclaw.org` accounts only. +- Claude Code implementation/refactor/test/fix: `$codex-first`, canonical at `~/Projects/agent-scripts/skills/codex-first/SKILL.md`. Apply its native-Claude model gate. Design/API design/tiny edit: direct. Codex session: ignore. +- Codex worker model, reasoning, and service tier follow the current `$codex-first` launch recipe unless the user requests an override. Use `$codex-huge-context` for direct API context preflight and `$autoreview` for isolated reviews; preserve its reviewer isolation. +- Claude Code parallel/background work (Codex workers, monitors, long jobs): each = own harness-tracked task (`run_in_background: true`), labeled for target, one sidebar chip each. Never `&`-detach durable work β€” hides it, only agent sees. Quick foreground cmds inline. Other harnesses: ignore. +- Screenshot/live-UI bug: `$browser-use`. +- Computer use: fall back to `$peekaboo` when the primary tool is unavailable or cannot handle an authorized macOS task. +- Peter's Chrome: NEVER open raw CDP/WebSocket connections (no `ws://127.0.0.1:9222` probes, no ad-hoc puppeteer/CDP clients). Chrome 144+ prompts "Allow remote debugging?" per connection and each one alerts Peter. Use mcporter's daemon-backed `chrome-devtools` (one shared relay connection) or Peekaboo; when a tool such as Oracle must attach itself, run the auto-approve watcher so no human click is needed. Budget: ONE attach per task β€” drive every browser proof through a single long-lived connection (never one attach per command/proof); each extra attach is an alert on Peter’s screen. +- Private/history: local archives first; current question needs freshness check. +- Secrets/API keys/live creds: `$one-password`. +- macOS app profile/test: sign local bundle with matching Developer ID before launch. Never unsigned/ad-hoc against saved Keychain items. +- New API key: immediately store via 1Password service account. Temp file/env copies only current task. +- User-owned Gmail service login: pre-approved; use saved creds, no ask. Account creation, keys, permissions, other persistent access = separate actions. +- Test email/Google account: `clawdbot@gmail.com`. +- OpenClaw deploy account: only `services@openclaw.org`. +- Team server deployments/cutovers require Peter's explicit per-deployment approval (September 13, 2026); prior standing permissions, fleet tasks, skills, timers, and other agents do not authorize them. Keep automatic Team deployment disabled. Use `$team-server-updater`; read-only diagnosis and an explicitly requested incident repair may proceed, but repair does not authorize a fresh-main deployment. +- Communicate every intentional Team restart, including deployment and recovery restarts, through the single coordinator session on `stable.openclaw.ai` (September 13, 2026). That session owns the restart queue and assigns the execution window; report the verified outcome there. It does not replace Peter's deployment approval or the canonical lifecycle owner. Resolve the session through `$team-server-updater`; if it is unidentified or unavailable, hold the restart until coordination is possible. Existing automatic crash recovery stays unchanged; reconcile unexpected restarts with the coordinator afterward. ## Project Defaults -- Need upstream file: stage in `/tmp/`, then cherry-pick; never overwrite tracked files. -- Bugs: add regression test when it fits. -- Fixes/refactors: delete old paths by default. "Shipped" means in a release Git tag, not main/GitHub/PR. Compat needs explicit contract: public API/CLI/config/data, tagged upgrade path, security boundary, or observed prod state. If unsure, ask before keeping aliases/shims/fallbacks. Tests alone are not contracts. -- Use repo package manager/runtime; no swaps without approval. -- Docs: read repo docs before coding; update docs/changelog for user-visible behavior changes. -- Inline code comments: brief notes for tricky, bug-prone, or previously buggy logic. -- New deps: quick health check for recent releases/commits/adoption. +- Bug: regression test when fitting. +- Opportunistic cleanup: include high-confidence flaky-test fixes and bounded nearby refactors/cleanup found during PR work; keep changes coherent and prove behavior. +- Fix/refactor: delete old path by default. Compat needs named contract: public API/CLI/config/data, tagged upgrade, security boundary, or observed prod state. Unsure: ask before alias/shim/fallback. Tests alone != contract. +- Use repo package manager/runtime. Swap needs approval. +- Docs: read repo docs before code. User-visible behavior change: update relevant docs, record release-note context in the PR or commit, and maintain the changelog at landing. Only `openclaw/openclaw` defers changelog edits to release generation. +- Inline comment: brief; only tricky, bug-prone, or formerly buggy logic. +- New dependency: quick health checkβ€”recent release, commits, adoption. ## PR / CI -- GitHub broad reads: prefer shimmed `gh` / `gitcrawl gh` first. Raw `gh api search/* -f ...` needs `--method GET`; gitcrawl shim sanitizes this. -- Pasted GitHub issue/PR: first `git status -sb`; if dirty, yell; then `git push` + `git pull --ff-only`. -- PR refs: use `gh pr view/diff`, not web search. -- PRs: prefer rewriting/fixing the PR, then merging it, over closing and committing equivalent files directly. -- PR quality: assume generated code may come from weaker AI models. Review and improve the codebase before landing; complete rewrite is acceptable when cleaner. -- Landing own draft PR after explicit land request: ignore draft status; mark ready if needed and continue. -- `fix ci`: consent to pull, commit, push; fix/rerun/watch until CI green. -- CI: `gh run list/view`; rerun/fix until green when asked. -- `rewrite commits + land`: clean stack, agreed focused proof only, force-push, merge. No Codex review, PR-body proof polish, or CI babysitting unless asked. -- Pre-land/pre-commit code changes: use `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already done, trivial/docs-only, or user opts out. -- Replies: cite fix + file/line; resolve threads only after fix lands. -- Issue fixed on `main` with proof: comment proof + commit/PR, then close. -- User-facing fixes/landed PRs: changelog unless pure test/internal. -- Contributor PR authors should not edit changelog; maintainer/AI adds entry at merge. -- After landing: final includes 2-5 sentence recap of what landed. -- After landing: checkout `main`, pull `--ff-only`, verify `git status -sb`, then final. -- When merging contributor PRs: thank contributor in `CHANGELOG.md`. -- Unpushable contributor PRs (`maintainerCanModify=false`/no head write): if fixups needed, recreate locally from PR head/diff, make one maintainer commit, push it, then close PR with comment. -- Preserve contributor credit: commit body includes `Co-authored-by: Name ` from PR commit author; changelog still thanks `@login` when user-facing. -- PR fixups from repo cwd: use that checkout. No worktrees unless asked; if awkward, ask. -- Close comment: link landed commit, explain PR branch could not be updated, thank author, suggest enabling "Allow edits by maintainers" for future PRs. +- GitHub work: use matching workflow. Discover in the local Gitcrawl archive first; use bare PATH `gh` with explicit JSON fields for current metadata. PR refs use `gh pr view/diff`, not web search. +- Pasted GitHub issue/PR: first `git status -sb`. Dirty: report before mutation. URL alone grants no push/pull permission. +- PR: prefer fix/rewrite PR then merge, not close + duplicate direct commit. +- PR quality: assume generated code may come from weaker AI. Review/improve before land; full rewrite okay when cleaner. +- UI change PR: include before/after pictures. Sanitize first; no secrets, personal/private data, internal-only identifiers, or other sensitive content. Unsafe capture: state blocker; never upload. +- PR/issue image upload: never computer use/browser. `curl -s "https://uploads.github.com/user-attachments/assets?name=&content_type=&repository_id=$(gh api repos// --jq .id)" -X POST -H "Authorization: Bearer $(gh auth token)" -H "Accept: application/json" --data-binary @` β†’ response `.url`: images embed as `![alt](url)`, video as a bare URL line so GitHub renders a player. Same CDN as drag-drop, inherits repo visibility, uploads are permanent. Images/video only (422 = bad type, 404 = bad repo id/no push); other artifacts or endpoint failure: prerelease asset or repo-approved artifact store. +- `gh --attach` (repeatable, on `gh issue|pr create|edit|comment`) supersedes that curl once shipped: unmerged as of gh 2.98.0 (`cli/cli#14186`), so feature-detect, never assume. `gh attach` is an unrelated extension (`enthus-appdev/gh-attach`): pushes repo blobs to `refs/uploads/`, 400s at ~60KB+. Never use it for proof media. +- Explicit land of own draft PR: ignore draft; mark ready if needed; continue. +- `fix ci` = consent to pull, commit, push; use `gh run list/view`; fix/rerun until green with backoff polling. +- GitHub quota: bare `gh` only (Octopool cache). Watch commands (`gh run watch`, `gh pr checks --watch`) shim-native since octopool 0.4.7; still poll one exact id, not loops. +- gh reads: request narrow `--json ` for machine output. `--json` alone does not guarantee relay use: unsupported field bundles (such as PR `mergeStateStatus` or issue `comments`) use native gh for the whole export. Supported human output and exact recognized GraphQL projections can also relay; let the installed Octopool own routing. +- Supported `gh api --paginate`/`--slurp` and `--hostname github.com` reads can relay. `--include` selects native caller credentials, not just fresher cache data. Use `OCTOPOOL_FRESH=1` or `Cache-Control: max-age=0` for relay freshness; preserve required decision fields and verify CI by exact run/head, not the newest broad-list row. +- CI logs: fetch once per failed run; reuse printed output. One `gh search`/`list --json` over per-item view loops; narrow fields, exact refs. +- `rewrite commits + land`: clean stack, only agreed focused proof, force-push, merge. No PR-body proof polish or CI babysit unless asked. +- Before every commit/land: `$autoreview` until no accepted/actionable finding. Always prefer Codex for autoreview, independent of environment. +- Routine `$autoreview` is pre-approved, including sending task-scoped unpublished diffs to the configured authenticated Codex review service; never ask Peter for autoreview approval. Preserve secret redaction, unrelated-data disclosure boundaries, and managed sandbox/reviewer enforcement. +- Issue fixed on `main` with proof: comment proof + commit/PR; close. +- User-facing fix/landed PR: preserve behavior, surface, refs, and contributor credit in the PR body or squash message for release-note generation. +- Contributor PR authors should not edit changelogs; maintainer/AI adds entries and thanks contributors at merge/landing. Only `openclaw/openclaw` defers these changes to release generation. +- Explicit land/ship authorizes needed branch changes and push. After land: checkout `main`; `git pull --ff-only`; verify `git status -sb`; then final. +- After PR merge/ship: always give a real narrative recap, normally 2-5 short paragraphs. Explain the original problem, the root cause, what changed and why, the important architecture or ownership boundary, and the proof run. Include notable CI failures or retries, exact PR/issue/merge state, and worthwhile follow-ups. Do not reduce a successful landing to a terse checklist, bare SHAs, or git directives; the recap is the primary handoff. +- Preserve contributor credit: commit body `Co-authored-by: Name ` from PR commit author. Changelog entries thank `@login` for user-visible work when added: at landing by default, at release generation only for `openclaw/openclaw`. ## Runtime Safety -- zsh: don't use `status` as a variable. -- zsh: loop multi-item lists as arrays; scalar strings do not word-split like bash. -- Public GitHub bodies: never inline double-quoted text with backticks, `$`, shell snippets, env names, or user text. Use temp file + `cat <<'EOF'` + inspect + `--body-file`. -- PR/issue body edits: fetch via REST + `jq -r`, never `gh pr/issue view --json body --jq .body`. Example: `gh api repos/OWNER/REPO/pulls/NUM | jq -r '.body // ""' > /tmp/body.md`; inspect before `--body-file`; stop if it starts with `"` or shows literal `\n`. -- Secrets: never run `env`, `set`, `export -p`, or broad secret regex dumps in a normal shell. Query exact names only; redact values. -- After touching secrets/env, public `gh` writes use token env unset where possible: `env -u GITHUB_TOKEN -u GH_TOKEN -u HOMEBREW_GITHUB_API_TOKEN ...`. +- Routine task-needed Crabbox/Testbox lease creation, reuse, use, and cleanup are pre-approved; never ask for confirmation to use them. Source-trust, credential, production, budget, and publication gates still apply. +- Low disk space: empty Trash and/or delete old caches, then push and sync across Peter's Macs and continue. +- zsh: never variable `status`. +- zsh multi-item loop: array. Scalar string does not word-split like bash. +- Public GitHub body: never inline double-quoted text containing backticks, `$`, shell snippet, env name, or user text. Temp file + `cat <<'EOF'` + inspect + `--body-file`. +- Secrets: never normal-shell `env`, `set`, `export -p`, broad secret regex dump. Query exact name only; redact value. +- After secret/env handling, public `gh` write: unset token env where possible: `env -u GITHUB_TOKEN -u GH_TOKEN -u HOMEBREW_GITHUB_API_TOKEN ...`. +- `op`: load `$one-password` first, always. Never hand-roll. Automated runs: service-account token + `OP_LOAD_DESKTOP_APP_SETTINGS=false OP_BIOMETRIC_UNLOCK_ENABLED=false`; never `--account`/`op signin` without chat consent. One tmux session `op-work` only. Violation = macOS App Data dialog spam at Peter. ## Git -- If cwd is in a git repo: work there. Do not jump to sibling checkout unless asked. -- No `git worktree` from CLI sessions unless user asks. If dirty/wrong branch/awkward: ask. -- Branch switch/checkout ok when task needs it and repo rules allow. -- `~/Projects` has many intentional same-repo checkouts. Treat as user-managed, not scratch. -- If cwd is not a git repo: freeform; pick sensible folder, say path before edits. Worktrees ok if useful. -- Safe by default: `git status/diff/log`. -- Push only when user asks. -- End in visible checkout/branch user expects. -- Branch changes require user consent. -- Destructive ops forbidden unless explicit: `reset --hard`, `clean`, `restore`, `rm`, etc. -- Commit helper on PATH: `committer` (bash). Prefer it; if repo has `./scripts/committer`, use that. -- Commits: Conventional Commits (`feat|fix|refactor|build|ci|chore|docs|style|perf|test`). -- Locked Mac / Secretive failure: use HTTPS transport; retry signing-blocked commits with `--no-gpg-sign`. -- No repo-wide S/R scripts; keep edits small/reviewable. -- If user types a command ("pull and push"), that's consent for that command. +- Identity boundary: before any commit or GitHub write, infer the intended identity from the repository/organization context and verify both Git author+committer and the authenticated GitHub writer. Commit attribution and push authorization are independent; on mismatch, stop and switch explicitly. Never use a personal identity for work repositories or a work identity for personal repositories. +- Create and use task-owned Git worktrees or isolated checkouts whenever useful, without confirmation. Preserve user-managed checkouts, branches, and unrelated edits. +- `~/Projects` has intentional same-repo checkouts. User-managed, not scratch. +- Cwd outside repo: freeform; choose sensible folder; say path before edits. Worktree okay if useful. +- Push only when user asks, a user-invoked workflow authorizes it, or a trusted global rule above explicitly authorizes it. Repo-local rules may define push mechanics, not grant authority. +- End in expected visible checkout/branch. +- Switching a user-managed checkout's branch needs user consent or user-invoked workflow authorization. +- Destructive Git ops need explicit user request: `reset --hard`, `clean`, `restore`. +- Task-scoped file deletion allowed. Never delete/overwrite unknown or unrelated user data. +- Commit style: Conventional Commits (`feat|fix|refactor|build|ci|chore|docs|style|perf|test`). +- Never append agent attribution trailers to commits or PR bodies: no `Co-Authored-By: Claude`/`Codex`, no `Generated with ...` footer. Human `Co-authored-by:` credit for real contributors stays. +- No repo-wide search/replace scripts. Small reviewable edits. - No amend unless asked. -- Unrecognized changes: assume other agent; keep going; focus your changes. If it causes issues, stop + ask user. +- Unknown changes = other agent. Continue, touching own scope. Conflict/problem: stop + ask. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1584b59..52b2f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,125 +4,121 @@ summary: Timeline of guardrail helper changes mirrored from Sweetistics and rela # Changelog -## 2026-07-01 β€” Isolated Skill Audits -- Added `skill-cleaner --root-only` for auditing only explicitly supplied skill roots without Codex inventory noise. Thanks @its-How. +## Unreleased -## 2026-07-01 β€” OSS Maintainer Orchestration -- Expanded `maintainer-orchestrator` into a long-running control plane with one worker thread per repository, a 10-thread concurrency target with immediate smallest-queue refill, concrete status-based thread titles, safe repository synchronization, forgotten-work preservation, PR rewrite/deduplication, decision-ready risk and diff summaries, durable `VISION.md` policy capture, dependency audits, release proposals with strongest-first highlights, and a persistent daily log plus heartbeat. +- Upload macOS release archives before publishing, preserve release/tag/commit state on failures, retry transient enclosure downloads, and retain changelog section spacing in release notes. -## 2026-06-27 β€” Internal Handling Boundary -- Clarified that task-relevant confidential information may be used in authorized internal contexts while external disclosure still requires explicit content and destination approval. +- Correct Octopool calling guidance for native JSON bundles, pagination, caller-owned headers, exact-run CI verification, and sandbox coverage. -## 2026-06-23 β€” Remote Mac Network Boundaries -- Updated `remote-mac` discovery to use live Tailscale addresses, honor network-boundary constraints, and avoid stale IPs or unreachable relays. +- Guard Xcode installs with bounded Git/Python smoke checks and selected-bundle rollback; require a Git host preflight before Codex workers and document the stable-Xcode workaround. -## 2026-06-22 β€” Release Gate Scope -- Updated `maintainer-orchestrator` so only target-release work and demonstrated candidate regressions block releases; unrelated open issues and PRs remain backlog without exemption requests. +- Fixed false missing-asset errors when verifying macOS releases with long asset lists. -## 2026-06-19 β€” Gmail Login Pre-Approval -- Added global guidance that user-owned Gmail service logins may use saved credentials without asking, while persistent access changes remain separately gated. +- Route intentional Team restarts through one coordinator session on Stable, preserving explicit deployment approval and holding restarts while the coordinator is unidentified or unavailable. -## 2026-06-19 β€” ClawSweeper Status Payload Bounds -- Reworked `clawsweeper-status` around field-bounded workflow and PR queries plus small activity windows, avoiding GitHub relay response caps while preserving worker and repository activity reporting. +- Require Peter's explicit approval for each Team server deployment and keep automatic deployment disabled, preserving scoped incident repair without an automatic follow-up upgrade. -## 2026-06-16 β€” Image Upload Guardrail -- Added device-aware image upload guidance that defaults work-managed devices to approved content and destinations only. +- Correct the OpenClaw deployment account to `services@openclaw.org`. -## 2026-06-15 β€” Locked-Mac Git Fallback -- Added AGENTS guidance to use HTTPS transport and unsigned commits when Secretive signing is unavailable on a locked Mac. +- Corrected browser relay timeout recovery to persist canonical controls, distinguish per-step and total startup deadlines, require HTTP base URLs for explicit overrides, explain the 0.13.10 discovery fallback, and verify saved relay-only policy across daemon respawns. -## 2026-06-15 β€” PR Quality Guardrail -- Added global guidance to improve AI-generated PR code before landing and permit complete rewrites when cleaner. +- Pin macOS release credential runners to system Bash so the shared tmux server's PATH cannot select an incompatible shell. -## 2026-06-14 β€” Skill Budget Audit -- Updated `skill-cleaner` to audit the live Codex skill inventory, aliased paths, prompt budget, and actual usage traces while retaining broad duplicate diagnostics. -- Compacted verbose personal skill descriptions without weakening their routing triggers. +- Fixed false macOS signing-canary failures on long signature reports while preserving Apple trust and Developer ID authority checks. -## 2026-06-11 β€” Confidentiality Guardrail -- Added strong global guidance against exposing non-public organizational information, including internal project and model names. +- Run the Codex direct-route preflight regression suite in CI with a clean environment and temporary HOME, covering private-home auth delivery and secret-safe failures without live credentials. -## 2026-06-11 β€” Maintainer Decision Briefs -- Updated `maintainer-orchestrator` to refresh state before owner questions and explain each decision with the change, proof, risks, recommendation, and exact choices instead of bare land/delete prompts. +- Make Codex Keychain helper guidance independent of private reviewer HOME paths and add a secret-safe `--private-home` delivery diagnostic without changing reviewer isolation or provider selection. -## 2026-06-06 β€” GitHub Triage Autonomy -- Updated `github-project-triage` so `triage` scans issues and PRs, surfaces autonomous candidates versus Peter-blocked items, treats Peter/owner comments as authoritative, requires independent feasibility checks where available, and posts exact verification proof after every landed PR. +- Align shared Codex routing rules with the canonical `codex-first` skill and its launch recipe instead of stale saved-defaults guidance; preserve the model gate and isolated review workflow. -## 2026-06-06 β€” Chrome Plugin Routing -- Updated `browser-use` to prefer the callable Codex Chrome plugin and fall back to the existing-profile mcporter route when unavailable. +- Added vm-lab bootstrap diagnostics for empty Apple-VZ clone disks and pre-output Bash heredoc stalls, with immutable-source safeguards and invocation-local shell fallback. -## 2026-05-28 β€” 1Password Timeout Escalation -- Added AGENTS guidance to use `sag` to call Peter aloud when interactive 1Password unlock/sign-in times out, keeping the tmux session alive for retry. +- Simplified requested agent transcripts and require scope trimming before previews or publication; helpers that rerender sessions cannot reuse approval of edited Markdown. -## 2026-05-26 β€” Browser Network Capture -- Added `network` to `scripts/browser-tools.ts` for tailing DevTools request, response, and failure events from the active tab, with resource-type filters, color control, one-shot captures, and follow mode. Thanks @mvanhorn. +- Set `codex-first` workers and Codex-backed reviews to GPT-6 Astra with high reasoning and Fast service, including fresh, resumed, and watchdog launches. -## 2026-05-25 β€” Agent Skills Origin -- Added `wrangler` skill for Cloudflare account routing, current Wrangler flags, KV/tail pitfalls, and serial command hygiene. -- Updated `skill-cleaner` to realpath-dedupe roots, keep Dropbox archives opt-in, print Codex-rule GPT-5.5 2% budget usage, scope disabled-plugin parsing correctly, and rank duplicate delete suggestions by body similarity with Codex/system copies preferred. -- Added `skill-cleaner` for auditing loaded Codex/OpenClaw skills, duplicate copies, recent usage, and prompt-budget description candidates. -- Renamed release workflow skills to the `release-*` convention and moved product-specific release skills into their owning repos. -- Added AGENTS guidance that `../agent-skills` means `openclaw/agent-skills`, plus local `handoff` skill routing. +- Removed the retired private launch-skill dependency from shared instructions and Claude routing; Codex workers now inherit saved model, reasoning, and service-tier defaults. -## 2026-05-24 β€” 1Password Item Lookup -- Updated `one-password` to allow explicit vault-scoped metadata search for fuzzy/screenshot-driven item lookup before exact field reads. +- Scoped release-time-only changelog generation to `openclaw/openclaw`; restored changelog updates at landing and post-release `Unreleased` sections for every other repository. +- Fixed Bash 3.2 escaping of captured macOS release direct-reference values in the private environment handoff. +- Prevented macOS release credential diagnostics from replaying provider stderr or secret-bearing JSON/parser errors; added synthetic regression coverage. +- Fixed skill sync nesting links inside locally owned directories; added bounded allowlisted self-link repair and read-only audit detection. +- Removed the machine-specific 1Password skill from the public skill set; local discovery now uses its private owner. +- Fixed `clawsweeper-status` aborting before activity sections on large workflow snapshots while preserving row caps and upstream errors. +- Fixed `clawsweeper-status` public queue parsing, preserved optional health fields without shifted columns, and added a separate publication-tail summary. +- Added headless Sparkle signing through scoped 1Password references, with public-key validation, mode-0600 temporary files, and cleanup on success or failure. +- Corrected GitHub secret provisioning to omit `--body` for stdin and added a skill validation guard against the literal-dash trap. +- Taught the `clawsweeper-status` snapshot to report queue handoff health, the ready/admissible split, backoff and parked reasons, and shed-since-reset, so exact-review items parked on retry exhaustion are no longer invisible behind a `healthy` verdict. +- Added the `project-structure` skill: a TypeScript symbol-map generator that compresses a repository into one context-loadable file with dense/skeleton/exports tiers, plugin-boundary listings, and measured token budgets. +- Removed the obsolete scoped-commit helper and returned commit recipes to standard Git now that agent work uses isolated worktrees. +- Made the million-token Codex provider and context settings an explicit atomic invariant, with a fatal preflight diagnostic for the unrecoverable `openai` plus 922K/700K split configuration. +- Added a fleet audit and repair action that disables Claude commit, pull-request, and session-link attribution while preserving unrelated settings and detecting higher-precedence overrides. +- Added narrow Homebrew 6 trust handling for exact third-party formulae already declared in a fleet profile. +- Made Apple-classified outdated/unusable Xcode runtimes and unavailable simulator devices required fleet drift, with a booted-device-safe audit and repair action. +- Established the SF Mini's classic OpenSSH fleet path, documented symmetric tailnet TCP 22 policy and proof rules, and recorded the MiniClaw duplicate-daemon regression and public-SSH fallback. +- Added the separately owned SF Mac Mini and distinguished its local/Tailscale names from FoundationClaw while its trusted SSH path remains pending. +- Verified FoundationClaw's provider identity, installed Tailscale and Jump Desktop Connect v10, and documented its data-preserving credential-reset escalation before GUI activation. +- Added ClawMac provider-outage triage that escalates console, NIC-link, and switch-port inspection without repeated power cycles or data-affecting recovery. +- Restored MiniClaw's canonical Homebrew Tailscale node and removed its duplicate GUI identity from fleet guidance. +- Removed obsolete Mac identities from remote fleet discovery guidance after pruning them from Tailscale. +- Reconciled the remote-Mac topology with provider purchases, including FoundationClaw's MacStadium identity and MiniClaw's canonical Tailscale identity. +- Reserved GPT-5.6's maximum output budget in `codex-huge-context` and moved fleet compaction to a verified 922K input window with a 700K safety threshold. +- Made `codex-first` treat the Gorilla-backed Clawdex endpoint as already model-routed, preventing recursive Codex delegation after the fleet proxy migration. +- Added a secret-safe Codex direct-API preflight so million-token launches fail before an unauthenticated Responses request when a machine is missing its Keychain delivery copy. +- Generalized interactive 1Password routing to select the active approval workstation within the matching personal or work-managed environment while preserving service-account isolation and safe offline fallback. -## 2026-05-23 β€” Skill Description Budget -- Shortened skill frontmatter descriptions to terse trigger phrases so the skills prompt budget keeps useful routing hints without filler prose. -- Updated `gog` auth guidance to preserve broad user OAuth scopes during reauth and rely on command guards for scoped execution. +## 2026-07-17 β€” 0.12.0 -## 2026-05-22 β€” Browser UI Verification -- Added hard guidance to verify screenshot/live UI bugs through the existing Chrome `$browser-use` path, including one-shot Peekaboo acceptance for visible Chrome attach alerts and no silent Playwright fallback for login/profile-dependent pages. +### Highlights +- Turned `maintainer-orchestrator` into a long-running control plane for autonomous queue triage, proof-driven changes, dependency maintenance, and release proposals across Peter's repositories. +- Added fleet maintenance, safe repository synchronization, package ownership audits, and Xcode fleet management for Peter's Macs. +- Replaced the old Codex review path with isolated structured autoreview and added Claude Code-only `codex-first` delegation for implementation-heavy work. +- Added `scripts/sync-skills` so Codex and Claude share one canonical skill and instruction mirror across agent-scripts, manager, and repo-owned skills. +- Hardened 1Password, npm, and macOS release workflows around scoped service access, stable tool identities, noninteractive signing, and verified publication boundaries. -## 2026-05-22 β€” npm Release Auth -- Updated `npm` to treat explicit release/publish requests as consent for the expected desktop 1Password npm auth prompt when service-account access cannot read `npmjs`, while still stopping on missing or ambiguous credentials. +### Maintainer Orchestration +- Expanded `maintainer-orchestrator` with one tracked Codex thread per repository, 30-lane scheduling, durable status, forgotten-work preservation, exact-head landing, dependency sweeps, VISION capture, and release-readiness proposals. +- Added a dedicated OpenClaw mode with root-owned discovery, qualified execution lanes, contributor routing, live permission checks, serialized landing, and OpenClaw-specific proof and changelog rules. +- Added autonomous GitHub queue triage with URL-first item briefs, maintainer-comment routing, author context, live proof requirements, safe spam closure, and explicit Peter decision briefs. +- Added the non-majority repository ledger, owner-maintained crawl-family overrides, and clearer root ownership for orchestration policy and worker titles. +- Made dependency updates, internal operating repositories, bounded cleanup, safe dirty fast-forwards, and candidate-scoped release blockers autonomous. +- Improved ClawSweeper status reporting for worker capacity, exact-review occupancy, workflow waiters, bounded API reads, and accurate failure and closure counts. +- Tightened shared agent policy around exact-head proof, contributor credit, screenshot safety, post-merge recaps, background-task visibility, public mutation, and release authority. -## 2026-05-22 β€” Auto Review Skill -- Replaced the old `codex-review` skill with `autoreview`, keeping Codex as the default/recommended review engine while adding structured findings, prompt/dataset inputs, tool/web-search review context, and security-aware checks. +### Review and Agent Workflows +- Replaced `codex-review` with structured `autoreview`, adding isolated Codex, Claude, and Pi review, safe bundle validation, regression provenance, security checks, parallel tests, and bounded multi-pass review. +- Added Claude Code-only `codex-first` routing for implementation, fixing, exploration, rebasing, and landing mechanics, including safe use of the ChatGPT app-bundled Codex CLI. Thanks @notorious-d-e-v. +- Added current model, effort, fast-mode, liveness, deterministic resume, and self-delegation guardrails to the Codex workflow. +- Made GitHub deep review and project triage prefer current source, real behavior reproduction, exact PR heads, and factual contributor trust signals. +- Routed screenshot and login-dependent browser work through existing Chrome state, with safe attach recovery and no silent isolated-browser fallback. +- Added explicit compatibility contracts, clean bounded-refactor guidance, generated-code skepticism, and scoped opportunistic cleanup rules. -## 2026-05-21 β€” Mac App Release Skill -- Added `release-mac-app` skill and `mac-release` helper so Sparkle appcast, key validation, GitHub release asset checks, and release closeout are shared while app metadata stays in each repo’s `.mac-release.env`. +### Fleet, Release, and Remote Operations +- Added `fleet-maintenance` for host health, package updates, repository synchronization, ownership collisions, disk cleanup, and service-impact reporting. +- Added `xcode-sync` for signed Xcode inventory, stable and prerelease slot management, build-identity checks, platform compatibility, and verified installation. +- Added dependency-light fleet repo audit and update helpers with batched snapshots, clean fast paths, dirty-work preservation, collision checks, and safe fast-forwards. +- Added `release-mac-app` and shared macOS release helpers for changelog notes, Sparkle appcasts, signing, notarization, GitHub assets, and post-release verification. +- Hardened macOS releases for passwordless isolated keychains, preloaded secrets, modern distribution validation, Bash 3.2, lightweight tags, bracketed changelog versions, and archive version parsing. +- Refreshed remote-Mac topology and network-boundary guidance, signed local app testing, locked-Mac Git fallback, and Cloudflare-only ClickClack deployment. +- Kept GitHub reads on the Octopool shim and added cache-health recovery before live GitHub fallback. -## 2026-05-20 β€” Browser Login Automation -- Updated `browser-use` to prefer existing Chrome for login-heavy sites because isolated profiles trigger captcha/device checks. +### Credentials and Safety +- Unified 1Password work on one tracked tmux session with scoped service-account access first, consent-gated desktop fallback, exact-field reads, known-item routing, and TCC-safe `~/bin/op` updates. +- Unified npm authentication around reusable service sessions, safe field selection, token caching, login fallback, package reservation, publication verification, and a generic authenticated command wrapper. +- Added internal-information, confidentiality, device-aware image upload, API-key storage, and approved-destination guardrails without blocking authorized private research. +- Standardized the canonical test Gmail account, OpenClaw deployment account, personal versus corporate Mac routing, and pre-approved Gmail service login behavior. -## 2026-05-20 β€” OpenClaw Deployment Account -- Added AGENTS routing to require `service@openclaw.org` accounts for OpenClaw deployments. - -## 2026-05-20 β€” Things Todo Skill -- Added `things-todo` skill for Things 3 todo CRUD through the `things` CLI with auth-token handling, JSON/read-back verification, and no direct DB-write guidance. - -## 2026-05-20 β€” Reminders Skill -- Added `reminders` skill for Apple Reminders CRUD through the `rem` CLI with JSON/read-back verification and macOS permission notes. - -## 2026-05-20 β€” GitHub Triage Skill Detail -- Updated `github-project-triage` to summarize each issue/PR with fit, risk, proof, blockers, next action, and contributor trust signals. -- Added a bundled `github-activity.sh` helper for repo/global GitHub author activity checks during triage. - -## 2026-05-20 β€” Codex Review Autoreview Trigger -- Updated `codex-review` skill description to include `autoreview` for routing/search. - -## 2026-05-18 β€” 1Password Exact Field Reads -- Updated `one-password` to avoid tmux window-index assumptions and document exact-label JSON extraction when `op --field` resolves an ambiguous concealed field. - -## 2026-05-18 β€” SSH Doctor Skill -- Added `ssh-doctor` for Remote Login diagnosis, launchd sshd pre-auth closes, stale `sshd-session` cleanup, and safe OP profile token block checks. - -## 2026-05-18 β€” 1Password Service Account Priority -- Updated `one-password` to prefer scoped service-account access before interactive desktop-app sign-in and to ask before fallback when scoped access is missing. - -## 2026-05-18 β€” Browser Reattach Defaults -- Updated `browser-use` to call the default mcporter `chrome-devtools` reattach target without a temporary config file. -- Added browser-use mcporter config notes for diagnosing blank/isolated Chrome attachments and restoring the reattach config. - -## 2026-05-18 β€” Lean Fix Guidance -- Added AGENTS guidance to prefer clean bounded refactors over tiny shims and avoid compat/edge-case scaffolding except for real public/API, upgrade, security, or production states. - -## 2026-05-16 β€” Codex Review Gitcrawl Repair -- Extended `codex-review` Gitcrawl recovery guidance to inspect portable manifest, source/runtime DB health, and portable-store status before live fallback. -- Updated `codex-review` to run `gitcrawl doctor --json` for malformed local Gitcrawl DB errors before falling back to live GitHub reads. - -## 2026-05-16 β€” GitHub Project Triage Scope -- Updated `github-project-triage` to default broad queue scans to `steipete` and `openclaw`, sort PR triage by PR count, and preserve RepoBar order when summarizing. +### Skills and Tools +- Added `scripts/sync-skills` to build Codex whole-root links, Claude's flat skill mirror, shared instruction pointers, deterministic collision handling, and stale-link pruning. +- Added skills for fleet maintenance, Xcode sync, Codex delegation, Twilio SMS, Wrangler, Things, Reminders, SSH diagnosis, agent transcripts, and shared macOS releases. +- Added `skill-cleaner` inventory, duplicate, usage, and prompt-budget audits plus isolated `--root-only` scans. Thanks @its-How. +- Added browser-tools network capture with filtering and follow mode. Thanks @mvanhorn. +- Hardened browser-tools startup, profile copying, symlink handling, and console flags. Thanks @ShiroKSH. +- Fixed xurl's OpenClaw npm installer metadata. Thanks @not-stbenjam. +- Made skill validation explicitly UTF-8-safe under C locales. Thanks @chaochaoweb3. +- Added skills.sh grouping metadata for shared skills. Thanks @vyctorbrzezowski. +- Exposed shared behavior validation, session viewing, crabbox, and crawl-family skills through the canonical mirror while removing duplicated bundled copies. ## 2026-05-14 β€” Video Transcript Dependency Update - Updated `video-transcript-downloader` to `youtube-transcript-plus` 2.0.0. @@ -200,7 +196,7 @@ summary: Timeline of guardrail helper changes mirrored from Sweetistics and rela ## 2025-11-08 β€” Guardrail Sync & Docs Hardening - Synced guardrail helpers with Sweetistics so downstream repos share the same runner, docs-list helper, and supporting scripts. - Expanded README guidance around runner usage, portability, and multi-repo sync expectations. -- Added committer lock cleanup, tightened path ignores, and refreshed misc. helper utilities (e.g., `toArray`) to reduce drift across repos. +- Tightened path ignores and refreshed misc. helper utilities (e.g., `toArray`) to reduce drift across repos. ## 2025-11-08 β€” Initial Toolkit Import - Established the repo with the Sweetistics guardrail toolkit (runner, git policy enforcement, docs-list helper, etc.). diff --git a/README.md b/README.md index 4eeccad..58544b0 100644 --- a/README.md +++ b/README.md @@ -26,24 +26,43 @@ Rules: - Validate after edits: `scripts/validate-skills`. - Quote `description` in front matter. -Global discovery usually points here: -- `~/.codex/skills -> ~/Projects/agent-scripts/skills` -- `~/.claude/skills -> ~/Projects/agent-scripts/skills` +Global discovery is built by `scripts/sync-skills` (idempotent; run on every Mac after cloning or adding skills): +- Codex scans nested dirs, so it gets whole-root links: `~/.codex/skills/agent-scripts -> ~/Projects/agent-scripts/skills`, `~/.codex/skills/manager -> ~/Projects/manager/skills`. +- Claude Code loads only `~/.claude/skills//SKILL.md` (exactly one level deep; per-entry symlinks are followed, category subfolders are not scanned β€” verified on 2.1.197). It gets a flat per-skill link mirror covering both repos plus machine-local `~/.codex/skills/` extras. +- Name collisions resolve agent-scripts > manager > codex-local; the script prints skipped duplicates and prunes broken/stale managed links. +- Real destination files and directories are preserved. A real Claude skill directory with a Codex backlink to that same directory satisfies local ownership; other real destination conflicts are reported and make sync fail. + +For the specific legacy topology `~/.claude/skills/NAME/NAME -> ~/.codex/skills/NAME -> ~/.claude/skills/NAME`, invoke the sync owner directly with an explicit allowlist: + +```bash +/absolute/path/to/agent-scripts/scripts/sync-skills --repair-nested-self-links --dry-run -- boxd-cli boxd-setup-deploy +``` + +```bash +/absolute/path/to/agent-scripts/scripts/sync-skills --repair-nested-self-links -- boxd-cli boxd-setup-deploy +``` + +This mode validates every candidate before unlinking only the extra nested leaves. It preserves the real skill directories, assets, and valid Codex backlinks, and exits before creating roots, building mirrors, pruning, or touching instruction pointers. Names must start with an ASCII letter or digit and contain only letters, digits, `.`, `_`, or `-`; duplicates, missing names, and unknown arguments are rejected. A missing nested leaf is a no-op only with the expected surrounding topology. Redirected/inaccessible roots, unexpected objects or literal targets, and changed directory/link identities cause refusal. Rechecks before each unlink are not atomic concurrency protection; a later error stops the batch and reports removals already completed, without rollback. + +The read-only `skills/fleet-maintenance/scripts/agent-skill-links-audit.sh` reports same-name nested ancestor loops as `reason=nested-self-link`. This is narrow detection, not an exhaustive graph validator. Its `--repair` remains a broad sync through `~/Projects/agent-scripts/scripts/sync-skills`; it is not the scoped repair above. Run `scripts/test-sync-skills` for isolated fixture coverage; `scripts/test-sync-skills --recurrence-only /absolute/path/to/old-sync-skills` runs the unchanged recurrence assertion against an original helper. Shared personal skills live as real folders in `skills/`. Public OpenClaw shared skills live in `../agent-skills` and are exposed here with tracked relative symlinks. Repo-owned skills stay canonical in their repo and are exposed here the same way, for example: ```text skills/autoreview -> ../../agent-skills/skills/autoreview skills/discrawl -> ../../discrawl/.agents/skills/discrawl +skills/peekaboo -> ../../peekaboo/skills/peekaboo ``` -Current symlinked repo-owned skills include `birdclaw`, `discrawl`, `gog`, `imsg`, `slacrawl`, `wacli`, and `wacrawl`. +Current symlinked repo-owned skills include `birdclaw`, `discrawl`, `gog`, `imsg`, `peekaboo`, `slacrawl`, `wacli`, and `wacrawl`. + +Keep `~/Projects/peekaboo` cloned and current before syncing the Peekaboo skill. Its `skills/peekaboo/SKILL.md` owns command, permission, capture, and interaction guidance; edit that source instead of adding another copy here. After updating both repositories, run `scripts/sync-skills` and verify the Peekaboo skill resolves through the Codex and Claude mirrors. ## Agent Instructions Shared hard rules live in `AGENTS.MD`. -Global setup: +Global setup (also maintained by `scripts/sync-skills`; Claude Code reads `CLAUDE.md` only, so it links to the shared `AGENTS.MD`): - `~/.codex/AGENTS.md -> ~/Projects/agent-scripts/AGENTS.MD` - `~/.claude/CLAUDE.md -> ~/Projects/agent-scripts/AGENTS.MD` - `~/.claude/AGENTS.md -> ~/Projects/agent-scripts/AGENTS.MD` @@ -58,10 +77,10 @@ Repo-specific rules go below that pointer. Do not copy the shared blocks into do ## Helpers -`scripts/committer` -- Stages exactly the listed files. -- Enforces a non-empty commit message. -- Runs skill validation before committing. +`scripts/sync-skills` +- Builds the per-machine skill mirror: Codex whole-root links, Claude flat per-skill links, shared `AGENTS.MD` pointers. +- Idempotent; prints changes only, prunes broken/stale managed links, never clobbers real files. +- No arguments runs ordinary sync; `--help` prints usage. Only the scoped `--repair-nested-self-links` mode accepts `--dry-run`. `scripts/validate-skills` - Checks every `skills/*/SKILL.md`. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..28c9556 --- /dev/null +++ b/bun.lock @@ -0,0 +1,66 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "agent-scripts-tools", + "dependencies": { + "commander": "^15.0.0", + "puppeteer-core": "^25.9.0", + }, + }, + }, + "packages": { + "@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="], + + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "chromium-bidi": ["chromium-bidi@17.0.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + + "devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="], + + "puppeteer-core": ["puppeteer-core@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.3" } }, "sha512-U61rCwSMha62CA/Opy6tCx2Fx+ck7ouiKnbpEApzSoLYMoEu9F71nuFpHL55vmIt33/GYm6eKZVhH2ev0nAIeg=="], + + "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], + + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + } +} diff --git a/docs/RELEASING-MAC.md b/docs/RELEASING-MAC.md index 6470d24..467f366 100644 --- a/docs/RELEASING-MAC.md +++ b/docs/RELEASING-MAC.md @@ -72,7 +72,7 @@ for v in /Volumes/*; do [[ $v == */* ]] && hdiutil detach "$v" -force; done - Notarization via `notarytool` with the exported API key; staple after success. - Before zipping, strip resource forks/extended attributes from the app (`xattr -cr .app && find .app -name '._*' -delete`) and zip with `ditto --norsrc -c -k --keepParent …` to avoid AppleDouble files that invalidate signatures. - Avoid `unzip` when testing locally; use `ditto -x -k /Applications` to prevent `._*` files that break signatures. -- The shared release helpers now always download the enclosure, verify the ed25519 signature, and run `codesign --verify` + `spctl` on the extracted app before publishingβ€”no opt-in flag needed. +- The shared release helpers upload the app and dSYM to a draft before publishing. They then download the public enclosure with bounded retries for transient HTTP/transport failures, verify its ed25519 signature and length, and run `codesign --verify` plus the system distribution policy check before pushing the appcast. They prefer `syspolicy_check distribution` and fall back to `spctl` on older macOS versions. ### Shared release skill - Canonical entry point: `~/Projects/agent-scripts/skills/release-mac-app/scripts/mac-release`. @@ -103,8 +103,8 @@ sign_update -f "$SPARKLE_PRIVATE_KEY_FILE" path/to/-.dmg --account "${ ## GitHub Release & Tag 1) Tag the release after artifacts are ready: `git tag v` (or let the release script tag). -2) Create the GitHub release (pre-release for betas), title ` `, body = changelog section for that version. -3) Upload artifacts: DMG/ZIP **and the dSYM archive** (zip it and attach alongside the main artifact for symbolicated crash debugging). Upload the appcast if it is served via Releases. Ensure enclosure URLs in the appcast point to the uploaded assets and return 200/OK. The shared helpers already re-download the enclosure and run codesign/spctl; if the repo ships a release check script (e.g., `Scripts/check-release-assets.sh`), run it after publishing to verify both zip and dSYM are present. +2) Create a draft GitHub release (pre-release for betas), title ` `, body = changelog section for that version, preserving its section spacing. +3) Upload artifacts: DMG/ZIP **and the dSYM archive** (zip it and attach alongside the main artifact for symbolicated crash debugging), then publish the draft. Upload the appcast if it is served via Releases. Ensure enclosure URLs in the appcast point to the uploaded assets and return 200/OK. The shared helpers already re-download the enclosure and run codesign/spctl; if the repo ships a release check script (e.g., `Scripts/check-release-assets.sh`), run it after publishing to verify both zip and dSYM are present. Workflows triggered by `release.published` can attach additional platform assets after publication; wait for those before pushing the appcast. 4) Release notes correctness: - Header **must be exactly** ` ` β€” no prefixes/suffixes. - Body must be a copy of the curated changelog for that version (user-facing items only, same order). @@ -115,7 +115,7 @@ sign_update -f "$SPARKLE_PRIVATE_KEY_FILE" path/to/-.dmg --account "${ ## Verification (Definition of Done) - Download the published artifact, install via `ditto`, launch, and verify: - - `spctl --assess --type execute --verbose .app` + - `syspolicy_check distribution .app` (or `spctl --assess --type execute --verbose .app` on older macOS versions) - `codesign --verify --deep --strict --verbose .app` - `stapler validate .app` - Check Sparkle update path from the previous installed build (stable and prerelease if applicable). @@ -129,10 +129,11 @@ sign_update -f "$SPARKLE_PRIVATE_KEY_FILE" path/to/-.dmg --account "${ ## Final Sign-Off (agent handoff) - Re-read the GitHub release page: title exactly ` `; body matches the curated changelog (no missing/extra bullets). - Re-open the appcast in a browser/`curl` to confirm the new entry, signature, and enclosure URL are present and 200/OK. -- Confirm local checks: `spctl`, `codesign`, `stapler` on the downloaded artifact; app launches; update path works from previous build. +- Confirm local checks: system distribution policy, `codesign`, and `stapler` on the downloaded artifact; app launches; update path works from previous build. - Log any deviations or manual fixes (e.g., regenerated signature) in the task notes before handing off. ## Recovery / Resume +- The shared `mac-release release` helper preserves created releases, tags, appcast commits, and working-tree edits when a later step fails. It never automatically deletes a release/tag or resets Git. A failed publication response may still have published successfully: inspect the existing release and resume the failed upload, verification, or push step without retagging or recreating the release. The monotonic appcast check prevents blindly rerunning the whole release after its appcast commit. - If a scripted release stops mid-flight, rerun with `--resume` or consult any `.release-state` the script writes. - After notarization success but before publish, you can recover manually: 1) Create DMG/ZIP if missing (`./scripts/create-dmg.sh` or `./Scripts/package_app.sh`). @@ -151,7 +152,7 @@ sign_update -f "$SPARKLE_PRIVATE_KEY_FILE" path/to/-.dmg --account "${ - [ ] Verify the published enclosure matches the appcast entry: `curl -L -o /tmp/update.zip && sign_update --verify /tmp/update.zip -f "$SPARKLE_PRIVATE_KEY_FILE"` (fails if the wrong key/signature is used). - [ ] Tag + GitHub release created; assets uploaded; URLs in appcast resolve (200/OK). - [ ] After publishing, bump `CHANGELOG.md`: move the shipped notes under the released version, increment its patch number, and start a new `Unreleased` section for the next patch. -- [ ] Downloaded artifact passes `spctl`, `codesign`, `stapler`; no `._*` files. +- [ ] Downloaded artifact passes system distribution policy, `codesign`, and `stapler`; no `._*` files. - [ ] Update flow validated from a previous version (if appcast was edited, re-run a live update after the change to confirm the new signature is accepted; clear `~/Library/Caches/com.` if Sparkle cached a bad download). - [ ] Appcast shows the correct notes (single-version chunk), and artifact size looks sane. - [ ] GitHub release notes header is ` ` and body matches changelog bullets exactly. diff --git a/docs/npm-publish-with-1password.md b/docs/npm-publish-with-1password.md index 55423ce..7dbcfcf 100644 --- a/docs/npm-publish-with-1password.md +++ b/docs/npm-publish-with-1password.md @@ -1,132 +1,63 @@ --- -summary: "Publish to npm via tmux + 1Password CLI (op)" +summary: "npm publishing: hand off to the canonical npm and 1Password skills" read_when: - "Need npm publish without copy/paste secrets." - "Need npm OTP/TOTP from 1Password." --- -# npm publish via tmux + op +# npm publishing with 1Password -Goal: publish to npm without pasting tokens/passwords into terminal logs. +This document is a routing guide, not a standalone auth or publishing recipe. +Load `one-password` first (`$one-password`, owned by +`~/Projects/manager/skills/one-password/SKILL.md`), then +[npm](../skills/npm/SKILL.md) (`$npm`). Those skills own the current commands, +credential selection, consent rules, retries, verification, and cleanup. If a +skill is unavailable, stop rather than reconstructing its workflow here. +Publishing still requires an explicit release/publish request. -Hard rule: do not run `op whoami`, `op item list`, `op item get`, `op read`, or any other -1Password CLI command directly in a normal shell for this workflow. Use the tmux -session below first, and send every `op` command through that tmux session. Direct -`op` commands can trigger repeated 1Password desktop alerts. +## Auth and session boundaries -## Prereqs +- **Service account first.** Follow `one-password` for `OP_SERVICE_ACCOUNT_TOKEN` + access to `Molty`, with both `OP_LOAD_DESKTOP_APP_SETTINGS=false` and + `OP_BIOMETRIC_UNLOCK_ENABLED=false`. No desktop unlock or sign-in is required + on this path. Do not add `--account` or run `op signin` on the service-account + path. Missing or expired access means stop and ask, not silent desktop fallback. +- **Desktop access requires consent.** Follow the owning skills' explicit + desktop-consent rules, including the npm skill's release/publish consent rule. + An unlocked desktop app is not a prerequisite for the default workflow. +- **One shared session, one task window.** Use the `one-password` bootstrap for + `clawdbot-op.sock` / `op-work`. Open exactly one window for the npm task, + target it by window ID, and reuse it for retries and follow-ups. Never create + an npm-specific socket, server, or session, send work to the permanent `shell` + keeper window, or interfere with another task's window. Never run `op` + directly in the normal shell tool. -- 1Password desktop app unlocked + CLI integration enabled. -- `op` installed. -- `tmux` installed. +## Use the npm helpers -## tmux session (required) +Inside that same task window, follow the npm skill to choose the entrypoint: -Use a persistent tmux session so `op` auth survives across commands. +- Local package publishing: `skills/npm/scripts/publish-package.sh`, from the + package root. +- Ad-hoc authenticated registry commands: `skills/npm/scripts/npm-service.sh` + with `-- `. +- Package reservation: `skills/npm/scripts/reserve-packages.sh`. -```bash -SOCKET_DIR="${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/clawdbot-tmux-sockets}" -mkdir -p "$SOCKET_DIR" -SOCKET="$SOCKET_DIR/op-auth.sock" -SESSION="op-auth-$(date +%Y%m%d-%H%M%S)" +These paths are relative to the agent-scripts checkout; use the invocation paths +in the npm skill. The shared auth helper owns credential extraction, stored +registry-session reuse, login/OTP fallback, cache writes, and temporary npmrc +cleanup. Do not copy legacy `Private/Npmjs` field reads, buffer-based login, +manual JSON extraction, or an OTP-only publish shortcut into a separate recipe. +Keep secret values out of logs, chat, and pane output. -tmux -S "$SOCKET" new -d -s "$SESSION" -n shell -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op signin" Enter -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op whoami" Enter -``` - -All commands below assume that same tmux socket/session. If you need to discover -the item name or fields, do it inside tmux too, for example: - -```bash -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op item list" Enter -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op item get '' --vault ''" Enter -``` - -## Preferred: granular automation token (+ optional OTP) - -Store a granular npm token in 1Password (item field `token`), plus TOTP if required. -For Peter's npm account, the 1Password item is `op://Private/Npmjs`. - -```bash -TOKEN_REF='op://Private/Npmjs/token' -OTP_REF='op://Private/Npmjs/one-time password?attribute=otp' - -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "NODE_AUTH_TOKEN=\"\$(op read \"$TOKEN_REF\" | tr -d \"\\n\")\" npm publish --otp \"\$(op read \"$OTP_REF\" | tr -d \"\\n\")\"" Enter -``` - -Notes: -- `tr -d "\n"` avoids accidental extra submits when pasting/reading. -- Avoid printing token/OTP (no `echo`, no `set -x`, no pane capture right after OTP). - -## If you’re already logged in: OTP-only publish - -If `npm whoami` works, you usually only need OTP for publish: - -```bash -OTP_REF='op:////one-time password?attribute=otp' -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "npm publish --otp \"\$(op read \"$OTP_REF\" | tr -d \"\\n\")\"" Enter -``` - -Tip: unset CI tokens so you don’t accidentally override your local login: - -```bash -env -u NPM_TOKEN -u NODE_AUTH_TOKEN npm whoami -``` - -## Fallback: `npm login` using op buffers (no echo) - -When password auth is unavoidable, avoid typing secrets by piping into tmux -buffers and pasting. Peter's npm account is stored in the 1Password item -`Npmjs` in the `Private` vault. - -```bash -USER_REF='op://Private/Npmjs/name' -PASS_REF='op://Private/Npmjs/password' -EMAIL_REF='op://Private/Npmjs/email' -OTP_REF='op://Private/Npmjs/one-time password?attribute=otp' - -# load buffers (strip trailing newline) -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op read \"$USER_REF\" | tr -d \"\\n\" | tmux -S \"$SOCKET\" load-buffer -b npm_user -" Enter -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op read \"$PASS_REF\" | tr -d \"\\n\" | tmux -S \"$SOCKET\" load-buffer -b npm_pass -" Enter -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op read \"$EMAIL_REF\" | tr -d \"\\n\" | tmux -S \"$SOCKET\" load-buffer -b npm_email -" Enter - -# run login; paste at prompts (repeat pattern for Email/OTP) -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "npm login --auth-type=legacy" Enter -tmux -S "$SOCKET" paste-buffer -t "$SESSION":1.1 -b npm_user -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- Enter -tmux -S "$SOCKET" paste-buffer -t "$SESSION":1.1 -b npm_pass -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- Enter -tmux -S "$SOCKET" paste-buffer -t "$SESSION":1.1 -b npm_email -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- Enter -tmux -S "$SOCKET" paste-buffer -t "$SESSION":1.1 -b npm_otp -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- Enter -``` - -If the item has duplicate labels, extract by field purpose/type from JSON inside -tmux instead of reading by label: - -```bash -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op item get Npmjs --vault Private --format json | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const item=JSON.parse(s);const fields=item.fields||[];const pick=(fn)=>fields.find(fn)?.value||'';process.stdout.write(pick(f=>f.purpose==='USERNAME'))})\" | tmux -S \"$SOCKET\" load-buffer -b npm_user -" Enter -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op item get Npmjs --vault Private --format json | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const item=JSON.parse(s);const fields=item.fields||[];const pick=(fn)=>fields.find(fn)?.value||'';process.stdout.write(pick(f=>f.purpose==='PASSWORD'&&f.type==='CONCEALED'))})\" | tmux -S \"$SOCKET\" load-buffer -b npm_pass -" Enter -tmux -S "$SOCKET" send-keys -t "$SESSION":1.1 -- "op item get Npmjs --vault Private --format json | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const item=JSON.parse(s);const fields=item.fields||[];const pick=(fn)=>fields.find(fn)?.value||'';process.stdout.write(pick(f=>f.label==='email'))})\" | tmux -S \"$SOCKET\" load-buffer -b npm_email -" Enter -``` - -Gotchas: -- If npm says β€œIncorrect or missing password”, the 1Password password is stale or the paste didn’t reach the prompt. -- Don’t run `tmux capture-pane` after pasting OTP (it may echo); wait 30–60s if you must debug. -- Repeated reads of the password field can trigger multiple 1Password β€œpassword used/copied” alerts; OTP-only flow avoids that entirely. - -## Verify - -```bash -npm whoami -npm view version -``` +Use the owning skill's identity and registry checks. If credentials are missing +or ambiguous, auth fails, package access is denied, or the package/version does +not match the release target, stop and ask; do not probe other items or open a +second auth window. ## Cleanup -```bash -tmux -S "$SOCKET" kill-session -t "$SESSION" -rm -f "$SOCKET" -``` +On success or failure, ensure the helper's temporary auth files are cleaned up, +then close only this task's window using the window ID and cleanup procedure +from `one-password`. Leave `op-work`, its `shell` keeper, the shared socket, and +all other task windows intact. Never kill the shared session/server or remove +its socket as npm-task cleanup. diff --git a/docs/slash-commands/landpr.md b/docs/slash-commands/landpr.md index 25f6dcb..96b0c03 100644 --- a/docs/slash-commands/landpr.md +++ b/docs/slash-commands/landpr.md @@ -53,10 +53,11 @@ git rebase "temp/landpr-$prnum" 5) Gate (before commit) - Run full repo gate (lint/typecheck/tests/docs). Example: `pnpm lint && pnpm build && pnpm test`. -6) Commit (via `committer`) +6) Commit ```sh -committer "fix: (#$prnum) (thanks @$contrib)" CHANGELOG.md +git add CHANGELOG.md +git commit -m "fix: (#$prnum) (thanks @$contrib)" land_sha=$(git rev-parse HEAD) ``` diff --git a/docs/slash-commands/raise.md b/docs/slash-commands/raise.md index fc216d7..35f949e 100644 --- a/docs/slash-commands/raise.md +++ b/docs/slash-commands/raise.md @@ -23,7 +23,7 @@ Goal: If `CHANGELOG.md` top release is dated (not `Unreleased`), create a new to - Do not touch any other release sections. 3) Commit + push -- `committer "docs(changelog): start X.Y.(Z+1) cycle" CHANGELOG.md` +- `git add CHANGELOG.md && git commit -m "docs(changelog): start X.Y.(Z+1) cycle"` - `git push` 4) Verify CI diff --git a/package.json b/package.json new file mode 100644 index 0000000..15678d8 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "agent-scripts-tools", + "private": true, + "type": "module", + "dependencies": { + "commander": "^15.0.0", + "puppeteer-core": "^25.9.0" + } +} diff --git a/scripts/browser-tools-profile.test.ts b/scripts/browser-tools-profile.test.ts new file mode 100644 index 0000000..69e95d6 --- /dev/null +++ b/scripts/browser-tools-profile.test.ts @@ -0,0 +1,94 @@ +import { lstatSync, mkdtempSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { describe, expect, test } from 'bun:test'; +import { copyChromeProfile, isMainModule } from './browser-tools'; + +describe('copyChromeProfile', () => { + test('preserves relative symlink targets', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'browser-tools-profile-')); + const source = path.join(root, 'source'); + const sourceLink = path.join(root, 'source-link'); + const destination = path.join(root, 'destination'); + mkdirSync(source); + writeFileSync(path.join(source, 'target'), 'profile state'); + symlinkSync('target', path.join(source, 'relative-link')); + symlinkSync('source', sourceLink); + + copyChromeProfile(sourceLink, destination); + + expect(readlinkSync(path.join(destination, 'relative-link'))).toBe('target'); + }); + + test('rejects overlapping source and destination paths', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'browser-tools-profile-overlap-')); + const source = path.join(root, 'source'); + mkdirSync(source); + writeFileSync(path.join(source, 'profile-state'), 'keep me'); + + expect(() => copyChromeProfile(source, source)).toThrow('must not overlap'); + expect(() => copyChromeProfile(source, path.join(source, 'nested'))).toThrow('must not overlap'); + expect(() => copyChromeProfile(source, root)).toThrow('must not overlap'); + expect(readFileSync(path.join(source, 'profile-state'), 'utf8')).toBe('keep me'); + }); + + test('validates the source before changing the destination', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'browser-tools-profile-missing-source-')); + const destination = path.join(root, 'destination'); + mkdirSync(destination); + writeFileSync(path.join(destination, 'profile-state'), 'keep me'); + + expect(() => copyChromeProfile(path.join(root, 'missing'), destination)).toThrow(); + expect(readFileSync(path.join(destination, 'profile-state'), 'utf8')).toBe('keep me'); + }); + + test('preserves a symlinked destination directory', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'browser-tools-profile-destination-link-')); + const source = path.join(root, 'source'); + const destinationTarget = path.join(root, 'destination-target'); + const destinationLink = path.join(root, 'destination-link'); + mkdirSync(source); + mkdirSync(destinationTarget); + writeFileSync(path.join(source, 'new-state'), 'new'); + writeFileSync(path.join(destinationTarget, 'old-state'), 'old'); + symlinkSync('destination-target', destinationLink); + + copyChromeProfile(source, destinationLink); + + expect(lstatSync(destinationLink).isSymbolicLink()).toBe(true); + expect(readlinkSync(destinationLink)).toBe('destination-target'); + expect(readFileSync(path.join(destinationTarget, 'new-state'), 'utf8')).toBe('new'); + expect(() => readFileSync(path.join(destinationTarget, 'old-state'), 'utf8')).toThrow(); + }); + + test('replaces a symlink to a non-directory without changing its target', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'browser-tools-profile-destination-file-link-')); + const source = path.join(root, 'source'); + const destinationTarget = path.join(root, 'destination-target'); + const destinationLink = path.join(root, 'destination-link'); + mkdirSync(source); + writeFileSync(path.join(source, 'new-state'), 'new'); + writeFileSync(destinationTarget, 'keep target'); + symlinkSync('destination-target', destinationLink); + + copyChromeProfile(source, destinationLink); + + expect(lstatSync(destinationLink).isDirectory()).toBe(true); + expect(readFileSync(destinationTarget, 'utf8')).toBe('keep target'); + expect(readFileSync(path.join(destinationLink, 'new-state'), 'utf8')).toBe('new'); + }); +}); + +describe('isMainModule', () => { + test('falls back to canonical paths when import.meta.main is unavailable', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'browser-tools-main-module-')); + const modulePath = path.join(root, 'browser-tools.ts'); + const launcherPath = path.join(root, 'browser-tools'); + writeFileSync(modulePath, 'fixture'); + symlinkSync('browser-tools.ts', launcherPath); + + expect(isMainModule(null, launcherPath, pathToFileURL(modulePath).href)).toBe(true); + expect(isMainModule(null, path.join(root, 'other'), pathToFileURL(modulePath).href)).toBe(false); + }); +}); diff --git a/scripts/browser-tools.ts b/scripts/browser-tools.ts index 3c8ffb1..f85443b 100644 --- a/scripts/browser-tools.ts +++ b/scripts/browser-tools.ts @@ -8,13 +8,15 @@ * directly via the DevTools protocol without pulling in a large MCP server. */ import { Command } from 'commander'; -import { execSync, spawn } from 'node:child_process'; +import { execFileSync, execSync, spawn } from 'node:child_process'; +import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import http from 'node:http'; import os from 'node:os'; import path from 'node:path'; import readline from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; +import { fileURLToPath } from 'node:url'; import { inspect } from 'node:util'; import puppeteer, { type HTTPRequest } from 'puppeteer-core'; @@ -33,6 +35,60 @@ async function connectBrowser(port: number) { return puppeteer.connect({ browserURL: browserURL(port), defaultViewport: null }); } +function resolveComparablePath(inputPath: string): string { + let existingPath = path.resolve(inputPath); + const missingSegments: string[] = []; + while (!existsSync(existingPath)) { + const parent = path.dirname(existingPath); + if (parent === existingPath) break; + missingSegments.unshift(path.basename(existingPath)); + existingPath = parent; + } + return path.join(realpathSync(existingPath), ...missingSegments); +} + +function pathsOverlap(first: string, second: string): boolean { + const relative = path.relative(first, second); + return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)); +} + +export function copyChromeProfile(sourceDir: string, profileDir: string): void { + const source = realpathSync(sourceDir); + if (!statSync(source).isDirectory()) { + throw new Error('Chrome profile source must be a directory'); + } + + let destinationLinkExists = false; + try { + destinationLinkExists = lstatSync(profileDir).isSymbolicLink(); + } catch { + // Missing destinations are created below. + } + if (destinationLinkExists && !existsSync(profileDir)) { + throw new Error('Chrome profile destination symlink target does not exist'); + } + + let destination = resolveComparablePath(profileDir); + if (pathsOverlap(source, destination) || pathsOverlap(destination, source)) { + throw new Error('Chrome profile source and destination must not overlap'); + } + + if (existsSync(profileDir) && statSync(profileDir).isDirectory()) { + for (const entry of readdirSync(destination)) { + rmSync(path.join(destination, entry), { recursive: true, force: true }); + } + } else { + rmSync(profileDir, { recursive: true, force: true }); + mkdirSync(profileDir, { recursive: true }); + destination = resolveComparablePath(profileDir); + } + cpSync(source, destination, { + recursive: true, + force: true, + verbatimSymlinks: true, + }); +} + async function getActivePage(port: number) { const browser = await connectBrowser(port); const pages = await browser.pages(); @@ -70,16 +126,16 @@ program if (killExisting) { try { - execSync("killall 'Google Chrome'", { stdio: 'ignore' }); + execFileSync('killall', ['Google Chrome'], { stdio: 'ignore' }); } catch { // ignore missing processes } await new Promise((resolve) => setTimeout(resolve, 1000)); } - execSync(`mkdir -p "${profileDir}"`); + mkdirSync(profileDir, { recursive: true }); if (profile) { - const source = `${path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome')}/`; - execSync(`rsync -a --delete "${source}" "${profileDir}/"`, { stdio: 'ignore' }); + const source = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome'); + copyChromeProfile(source, profileDir); } spawn(chromePath, [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, '--no-first-run', '--disable-popup-blocking'], { @@ -416,14 +472,13 @@ program .option('--timeout ', 'Capture duration in seconds (default: 5 for one-shot, infinite for --follow)', (value) => Number.parseInt(value, 10)) .option('--color', 'Force color output') .option('--no-color', 'Disable color output') - .option('--no-serialize', 'Disable object serialization (show raw text only)', false) + .option('--no-serialize', 'Disable object serialization (show raw text only)') .action(async (options) => { const port = options.port as number; const follow = options.follow as boolean; const timeout = options.timeout as number | undefined; const typesFilter = options.types as string | undefined; - const noSerialize = options.noSerialize as boolean; - const serialize = !noSerialize; + const serialize = options.serialize !== false; // Track explicit color flags by looking at argv to avoid Commander defaults overriding TTY detection. const argv = process.argv.slice(2); @@ -951,8 +1006,8 @@ async function ensureReadability(page: any) { // ignore } const scripts = [ - 'https://unpkg.com/@mozilla/readability@0.4.4/Readability.js', - 'https://unpkg.com/turndown@7.1.2/dist/turndown.js', + 'https://unpkg.com/@mozilla/readability@0.6.0/Readability.js', + 'https://unpkg.com/turndown@7.2.4/dist/turndown.js', 'https://unpkg.com/turndown-plugin-gfm@1.0.2/dist/turndown-plugin-gfm.js', ]; for (const src of scripts) { @@ -1164,4 +1219,22 @@ function fetchJson(url: string, timeoutMs = 2000): Promise { }); } -program.parseAsync(process.argv); +export function isMainModule( + metaMain: boolean | null | undefined = import.meta.main, + argvPath: string | undefined = process.argv[1], + moduleUrl: string = import.meta.url, +): boolean { + if (typeof metaMain === 'boolean') return metaMain; + if (!argvPath) return false; + + const modulePath = fileURLToPath(moduleUrl); + try { + return realpathSync(argvPath) === realpathSync(modulePath); + } catch { + return path.resolve(argvPath) === modulePath; + } +} + +if (isMainModule()) { + void program.parseAsync(process.argv); +} diff --git a/scripts/committer b/scripts/committer deleted file mode 100755 index 0f4a5ad..0000000 --- a/scripts/committer +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -# Disable glob expansion to handle brackets in file paths -set -f -usage() { - printf 'Usage: %s [--force] "commit message" "file" ["file" ...]\n' "$(basename "$0")" >&2 - exit 2 -} - -if [ "$#" -lt 2 ]; then - usage -fi - -force_delete_lock=false -if [ "${1:-}" = "--force" ]; then - force_delete_lock=true - shift -fi - -if [ "$#" -lt 2 ]; then - usage -fi - -commit_message=$1 -shift - -if [[ "$commit_message" != *[![:space:]]* ]]; then - printf 'Error: commit message must not be empty\n' >&2 - exit 1 -fi - -if [ -e "$commit_message" ]; then - printf 'Error: first argument looks like a file path ("%s"); provide the commit message first\n' "$commit_message" >&2 - exit 1 -fi - -if [ "$#" -eq 0 ]; then - usage -fi - -files=("$@") - -# Disallow "." because it stages the entire repository and defeats the helper's safety guardrails. -for file in "${files[@]}"; do - if [ "$file" = "." ]; then - printf 'Error: "." is not allowed; list specific paths instead\n' >&2 - exit 1 - fi -done - -last_commit_error='' - -run_git_commit() { - local stderr_log - stderr_log=$(mktemp) - if git commit -m "$commit_message" -- "${files[@]}" 2> >(tee "$stderr_log" >&2); then - rm -f "$stderr_log" - last_commit_error='' - return 0 - fi - - last_commit_error=$(cat "$stderr_log") - rm -f "$stderr_log" - return 1 -} - -for file in "${files[@]}"; do - if [ ! -e "$file" ]; then - # Allow staging deletions: when a file is removed from disk, it may no longer exist in the index - # (once staged for deletion), but it can still exist in HEAD. - if ! git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then - if ! git cat-file -e "HEAD:$file" >/dev/null 2>&1; then - printf 'Error: file not found: %s\n' "$file" >&2 - exit 1 - fi - fi - fi -done - -git restore --staged :/ -git add -A -- "${files[@]}" - -if git diff --staged --quiet; then - printf 'Warning: no staged changes detected for: %s\n' "${files[*]}" >&2 - exit 1 -fi - -committed=false -if run_git_commit; then - committed=true -elif [ "$force_delete_lock" = true ]; then - lock_path=$( - printf '%s\n' "$last_commit_error" | - awk -F"'" '/Unable to create .*\.git\/index\.lock/ { print $2; exit }' - ) - - if [ -n "$lock_path" ] && [ -e "$lock_path" ]; then - rm -f "$lock_path" - printf 'Removed stale git lock: %s\n' "$lock_path" >&2 - if run_git_commit; then - committed=true - fi - fi -fi - -if [ "$committed" = false ]; then - exit 1 -fi - -printf 'Committed "%s" with %d files\n' "$commit_message" "${#files[@]}" diff --git a/scripts/sync-skills b/scripts/sync-skills new file mode 100755 index 0000000..957588e --- /dev/null +++ b/scripts/sync-skills @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# sync-skills: build/refresh the per-machine agent skill mirror. Idempotent. +# +# Codex scans ~/.codex/skills recursively, so it gets whole-root symlinks. +# Claude Code loads only ~/.claude/skills//SKILL.md (one level deep, +# per-entry symlinks followed, no recursive scan; verified live on 2.1.197), +# so it gets a flat per-skill symlink mirror instead. +# Collision priority: agent-scripts > manager > codex-local extras. +set -eo pipefail + +usage() { + printf 'usage: %s [--help | --repair-nested-self-links [--dry-run] -- NAME...]\n' "$0" +} + +repair=0 +dry_run=0 +if [ "$#" -gt 0 ]; then + case "$1" in + --help) [ "$#" -eq 1 ] || { usage >&2; exit 2; }; usage; exit 0 ;; + --repair-nested-self-links) + repair=1 + shift + if [ "${1:-}" = --dry-run ]; then dry_run=1; shift; fi + [ "${1:-}" = -- ] || { usage >&2; exit 2; } + shift + [ "$#" -gt 0 ] || { usage >&2; exit 2; } + repair_names=() + for name in "$@"; do + case "$name" in + ''|[!a-zA-Z0-9]*|*[!a-zA-Z0-9._-]*) + printf 'invalid skill basename: %s\n' "$name" >&2; exit 2 ;; + esac + for previous in "${repair_names[@]}"; do + [ "$name" != "$previous" ] || { printf 'duplicate skill name: %s\n' "$name" >&2; exit 2; } + done + repair_names+=("$name") + done + ;; + *) usage >&2; exit 2 ;; + esac +fi + +AGENT_SKILLS="$HOME/Projects/agent-scripts/skills" +MANAGER_SKILLS="$HOME/Projects/manager/skills" +CODEX_ROOT="$HOME/.codex/skills" +CLAUDE_ROOT="$HOME/.claude/skills" +AGENTS_MD="$HOME/Projects/agent-scripts/AGENTS.MD" + +# This mode exits before any ordinary mirror or instruction-pointer work. +if [ "$repair" -eq 1 ]; then + removed=0 + absent=0 + refuse() { + printf 'nested-self-links: refused: %s (removed=%s already-absent=%s)\n' "$*" "$removed" "$absent" >&2 + exit 1 + } + # BSD and GNU stat both default to the link itself, not its target. + if stat -c '%d:%i' "$HOME" >/dev/null 2>&1; then + stat_args=(-c '%d:%i') + else + stat_args=(-f '%d:%i') + fi + identity() { stat "${stat_args[@]}" "$1"; } + roots=("$HOME" "$HOME/.claude" "$HOME/.codex" "$CLAUDE_ROOT" "$CODEX_ROOT") + root_ids=() + check_roots() { + local i root physical current + for i in "${!roots[@]}"; do + root=${roots[$i]} + [ ! -L "$root" ] && [ -d "$root" ] && [ -r "$root" ] && [ -x "$root" ] || refuse "not an accessible real root: $root" + physical=$(cd "$root" && pwd -P) || refuse "cannot resolve root: $root" + [ "$physical" = "$root" ] || refuse "redirected root: $root" + current=$(identity "$root") || refuse "cannot identify root: $root" + [ "$current" = "${root_ids[$i]:-$current}" ] || refuse "root changed: $root" + root_ids[$i]=$current + done + } + check_candidate() { + local parent="$CLAUDE_ROOT/$1" backlink="$CODEX_ROOT/$1" nested="$CLAUDE_ROOT/$1/$1" target + [ ! -L "$parent" ] && [ -d "$parent" ] && [ -r "$parent" ] && [ -x "$parent" ] || refuse "not an accessible real skill directory: $parent" + [ -f "$parent/SKILL.md" ] && [ -r "$parent/SKILL.md" ] || refuse "missing or unreadable SKILL.md: $parent" + [ -L "$backlink" ] || refuse "not a Codex backlink: $backlink" + target=$(readlink "$backlink") || refuse "cannot read backlink: $backlink" + [ "$target" = "$parent" ] && [ "$parent" -ef "$backlink" ] || refuse "unexpected Codex backlink: $backlink" + candidate_state=absent + candidate_leaf_id=absent + if [ -L "$nested" ]; then + target=$(readlink "$nested") || refuse "cannot read nested link: $nested" + [ "$target" = "$backlink" ] && [ "$parent" -ef "$nested" ] && [ "$backlink" -ef "$nested" ] || refuse "unexpected nested link: $nested" + candidate_state=present + candidate_leaf_id=$(identity "$nested") || refuse "cannot identify nested link: $nested" + elif [ -e "$nested" ]; then + refuse "nested entry is not a symlink: $nested" + fi + candidate_parent_id=$(identity "$parent") || refuse "cannot identify skill directory: $parent" + candidate_backlink_id=$(identity "$backlink") || refuse "cannot identify backlink: $backlink" + } + check_roots + parent_ids=(); backlink_ids=(); leaf_ids=(); states=() + for i in "${!repair_names[@]}"; do + check_candidate "${repair_names[$i]}" + parent_ids[$i]=$candidate_parent_id + backlink_ids[$i]=$candidate_backlink_id + leaf_ids[$i]=$candidate_leaf_id + states[$i]=$candidate_state + done + for i in "${!repair_names[@]}"; do + name=${repair_names[$i]} + nested="$CLAUDE_ROOT/$name/$name" + # Recheck the roots and this candidate immediately before unlinking. These + # shell checks detect drift but cannot make pathname-based unlink atomic. + check_roots + check_candidate "$name" + [ "$candidate_parent_id" = "${parent_ids[$i]}" ] && + [ "$candidate_backlink_id" = "${backlink_ids[$i]}" ] && + [ "$candidate_leaf_id" = "${leaf_ids[$i]}" ] && + [ "$candidate_state" = "${states[$i]}" ] || refuse "topology changed: $name" + if [ "$candidate_state" = absent ]; then + absent=$((absent + 1)) + printf 'already-absent %s\n' "$nested" + elif [ "$dry_run" -eq 1 ]; then + printf 'would-remove %s\n' "$nested" + else + unlink "$nested" || refuse "unlink failed: $nested" + removed=$((removed + 1)) + printf 'removed %s\n' "$nested" + fi + done + [ "$dry_run" -eq 1 ] || printf 'nested-self-links: removed=%s already-absent=%s\n' "$removed" "$absent" + exit 0 +fi + +changed=0 +conflicts=0 +note() { printf '%s\n' "$*"; changed=1; } + +# link : create/retarget symlink, quiet when already right. +link() { + if [ ! -L "$2" ] && [ -e "$2" ]; then + [ -d "$1" ] && [ -d "$2" ] && [ "$1" -ef "$2" ] && return 0 + printf 'WARN: real destination preserved: %s (expected -> %s)\n' "$2" "$1" >&2 + conflicts=$((conflicts + 1)) + return 0 + fi + [ "$(readlink "$2" 2>/dev/null)" = "$1" ] && return 0 + ln -sfn "$1" "$2" + note "link $2 -> $1" +} + +# --- Claude root must be a real dir; the old layout symlinked the whole dir, +# which hid manager + codex-local skills. +if [ -L "$CLAUDE_ROOT" ]; then + rm "$CLAUDE_ROOT" + note "replaced legacy whole-dir symlink $CLAUDE_ROOT with real dir" +fi +mkdir -p "$CLAUDE_ROOT" "$CODEX_ROOT" + +# --- Codex: whole-root links. +[ -d "$AGENT_SKILLS" ] && link "$AGENT_SKILLS" "$CODEX_ROOT/agent-scripts" +[ -d "$MANAGER_SKILLS" ] && link "$MANAGER_SKILLS" "$CODEX_ROOT/manager" + +# --- Claude: collect desired name -> target (bash 3.2: parallel arrays). +names=() +targets=() +lookup() { + local i + for i in "${!names[@]}"; do + [ "${names[$i]}" = "$1" ] && { printf '%s' "${targets[$i]}"; return 0; } + done + return 1 +} +claim() { # claim