diff --git a/.agents/skills/dogfood b/.agents/skills/dogfood new file mode 120000 index 0000000..c705a2f --- /dev/null +++ b/.agents/skills/dogfood @@ -0,0 +1 @@ +../../.claude/skills/dogfood \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4f21818 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "skiphow@skiphow": false + } +} diff --git a/.claude/skills/dogfood/SKILL.md b/.claude/skills/dogfood/SKILL.md index c37fa64..ebefa82 100644 --- a/.claude/skills/dogfood/SKILL.md +++ b/.claude/skills/dogfood/SKILL.md @@ -36,9 +36,13 @@ Aim at whatever the question is about. Usually some of: - **Which package version ran**, and what its text said at the time. Compare against that version from git history, never against the current tree. -- **What reached the agent's context.** This one inverts conclusions when you get it wrong: a file path - appearing in a command is not proof the file's text reached the agent. Searching a file puts matching lines - in context, not the rule. Look for the wording itself. +- **What reached the agent's context.** This one inverts conclusions when you get it wrong, in both + directions. A file path appearing in a command is not proof the file's text reached the agent, and searching + a file puts matching lines in context rather than the rule. The other direction is easier to miss: a path + pattern under-counts, because `cd .../references && cat tracked-work.md` never contains the string + `references/tracked-work.md`. Search for the file's own opening sentence instead. A path-based scan once + reported zero loads where there were several, and the wrong number was already in front of the owner before + it was caught. - **What the owner actually asked**, in their own words. Owner input arrives through more than one channel, so do not read only the obvious one or you will miss turns, including the moment permission widened. - **What it cost.** Elapsed time, tokens, and how much of both went to subagents. Transcripts carry per-message @@ -59,6 +63,28 @@ honest default, not a failure. Count honestly and in whole sessions, and pool only sessions where the same text governed. Two deviations in one session are one observation. A finding the run noticed and silently dropped leaves no trace, so anything that looks like conformance is an upper bound. +## Reproduce before naming a cause + +A pattern across installed sessions is an observation, not a cause. Before saying which sentence produced it, +run the failing case with everything held fixed but the package, and run it on the unchanged package too. It +may not reproduce: a field failure seen in eighteen long sessions loaded fine in every isolated run of the same +package, which located the cause somewhere in what those sessions carry and not in the wording that was about +to be changed. That is a result worth having and it is cheaper than shipping the wrong fix. + +Two mechanics that are easy to get wrong and quietly invalidate the run: + +- **Prove the candidate is the package under test, from the transcript.** Do not take the model's own + inventory of what it loaded: asked, it went and read the disk and named the installed plugin path, which + says nothing about what was in its context. The base directory the skill itself reports is the evidence, + and it must point at the candidate rather than the host's plugin cache. +- **Isolate the other host before asking it to review.** Pointing only its own home at a scratch directory is + not enough; it also reads a host-agnostic user skill directory, so it will load the maintainer's personal + skills and the installed package it is supposed to be judging. Point the operating system home there as + well, copy in only the credentials, and check the session header before trusting the output. + +A cross-host review round converges when it is told what earlier rounds settled and what was refused, and told +not to raise those again. Without that it re-proposes them, and the rounds do not end. + ## Design the fix Only when the evidence names a defect in the package's own wording. A verdict of `UNVERIFIED`, a run that @@ -89,6 +115,27 @@ in something the kernel reserves for them. One session can prove wording is broken. It cannot prove that agents in general need a new procedure. Resist adding steps, gates, or ceremony on that basis, here or in the package. +## Review it on the other host + +A change to the shipped instructions gets a review from Codex before it is finished. The mechanics are +settled; do not re-derive them each time. + + codex exec --sandbox read-only -c model_reasoning_effort=high "$(cat prompt.md)" out.log 2>&1 + +The `"` tells you which release a sentence entered, which is how you find out +that the run you are blaming ran on text that did not exist yet. Record what was confirmed and what was +refused, with the reason, in the release notes. + ## Report Say what you found, what the evidence supports, and what stays uncertain. Where there was a fix to design, diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..cd30cc3 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,2 @@ +[plugins."skiphow@skiphow"] +enabled = false diff --git a/scripts/check_hosts.py b/scripts/check_hosts.py index 6cdcf8a..52b0e26 100755 --- a/scripts/check_hosts.py +++ b/scripts/check_hosts.py @@ -129,12 +129,12 @@ def _marketplace_manifest(host: str) -> tuple[str, Path]: def _plain_marketplace(destination: Path, host: str) -> Path: - _marketplace_manifest(host) - metadata = ".agents" if host == "codex" else ".claude-plugin" - _payload(ROOT / metadata) + manifest, candidate_manifest = _marketplace_manifest(host) _payload(PLUGIN_ROOT) destination.mkdir(parents=True, exist_ok=False) - shutil.copytree(ROOT / metadata, destination / metadata, symlinks=True) + destination_manifest = destination / manifest + destination_manifest.parent.mkdir(parents=True, exist_ok=False) + shutil.copy2(candidate_manifest, destination_manifest) shutil.copytree(PLUGIN_ROOT, destination / "plugins/skiphow", symlinks=True) _payload(destination) return destination diff --git a/tests/test_hosts.py b/tests/test_hosts.py index 97b7477..5481154 100644 --- a/tests/test_hosts.py +++ b/tests/test_hosts.py @@ -89,6 +89,7 @@ def test_plain_marketplace_matches_exact_candidate_and_rejects_repositories( tmp_path: Path, ) -> None: source = hosts._plain_marketplace(tmp_path / "plain", "codex") + assert not (source / ".agents/skills").exists() assert hosts.verify_plain_marketplace_source(str(source), "codex")[0] (source / ".git").mkdir() passed, output = hosts.verify_plain_marketplace_source(str(source), "codex") diff --git a/tests/test_package.py b/tests/test_package.py index 753b8d0..98155bc 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -76,6 +76,17 @@ def test_marketplaces_publish_only_the_plugin_directory() -> None: assert claude["plugins"][0]["source"] == "./plugins/skiphow" +def test_dogfood_skill_is_repo_scoped_and_shared_by_codex_and_claude() -> None: + claude = ROOT / ".claude/skills/dogfood" + codex = ROOT / ".agents/skills/dogfood" + + assert claude.is_dir() and not claude.is_symlink() + assert codex.is_symlink() + assert codex.readlink() == Path("../../.claude/skills/dogfood") + assert codex.resolve(strict=True) == claude.resolve(strict=True) + assert frontmatter(codex / "SKILL.md")["name"] == "dogfood" + + def test_release_metadata_uses_one_version() -> None: release = read("VERSION").strip() codex = json_object("plugins/skiphow/.codex-plugin/plugin.json")