Repository navigation
Skills installer: do not delete content the pinned CLI did not write (F3), and align the status check - #1035
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 295ada5e8b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… add over local work in a locked project; keep the push gate's read inventory unchanged The first reads of #1035 (Astra at 295ada5 and the four open Codex review threads) found two ways the guard still lost local work and one regression. One commit: - A regular file or a link named `.venv`, `node_modules`, `__pycache__` and the like was taken for a regenerable directory (the name was excluded before the entry's type was looked at), and the add deleted it. git_tree_sha now skips such a name only for a real directory and does not follow a link. The SKILL.md-alone shortcut, which had the same flaw and also followed a SKILL.md link, is removed: a lone SKILL.md is hashed like any other folder. - A project locked at the pin whose Claude target is missing returned `install` before the content check, so the repairing add deleted local files. The check now runs before that repair in a project as it does in the global scope. - The shortcut's os.listdir was a sixth unclassified read site of the push gate's monitoring baseline (342 -> 343; tests.test_runtime_worker_openhands_push_gate failed 1 of 125 and repeated). With the shortcut gone the installer has its five reads again: 125 of 125 pass at this head. - The status check rejected a canonical-leaf link for a Codex-only entry that the installer accepts; both now apply the rule to entries installed for Claude Code only. The moved link to budget_report in the 2026-10-10 record points at its current line. tests/test_install_skills.py::PinnedCliForeignContentTests repeats the two measurements offline with the pinned skills 1.7.0 CLI where it is installed (a local source folder, a scratch home and project, telemetry off): the classifier must say local-modified before any add, and the same add on a copy must delete the entry (the control). Where the CLI is not installed it is skipped; the fake-CLI tests cover the same states. The record and the lifecycle are updated; the evidence rows are re-registered at their final bytes. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a77a96e80
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… add over local work in a locked project; keep the push gate's read inventory unchanged The first reads of #1035 (Astra at 295ada5 and the four open Codex review threads) found two ways the guard still lost local work and one regression. One commit: - A regular file or a link named `.venv`, `node_modules`, `__pycache__` and the like was taken for a regenerable directory (the name was excluded before the entry's type was looked at), and the add deleted it. git_tree_sha now skips such a name only for a real directory and does not follow a link. The SKILL.md-alone shortcut, which had the same flaw and also followed a SKILL.md link, is removed: a lone SKILL.md is hashed like any other folder. - A project locked at the pin whose Claude target is missing returned `install` before the content check, so the repairing add deleted local files. The check now runs before that repair in a project as it does in the global scope. - The shortcut's os.listdir was a sixth unclassified read site of the push gate's monitoring baseline (342 -> 343; tests.test_runtime_worker_openhands_push_gate failed 1 of 125 and repeated). With the shortcut gone the installer has its five reads again: 125 of 125 pass at this head. - The status check rejected a canonical-leaf link for a Codex-only entry that the installer accepts; both now apply the rule to entries installed for Claude Code only. The moved link to budget_report in the 2026-10-10 record points at its current line. tests/test_install_skills.py::PinnedCliForeignContentTests repeats the two measurements offline with the pinned skills 1.7.0 CLI where it is installed (a local source folder, a scratch home and project, telemetry off): the classifier must say local-modified before any add, and the same add on a copy must delete the entry (the control). Where the CLI is not installed it is skipped; the fake-CLI tests cover the same states. The record and the lifecycle are updated; the evidence rows are re-registered at their final bytes. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…sed on entries a git tree cannot hold, and judge runtime-named files the same way in the status check Fix wave 2 of #1035, on 9a77a96: the second GPT read (blockers 1 and 2, follow-up 3) and the open Codex thread (P2, "Fail closed on entries omitted from the tree hash"). - A lock at the requested pin plus an existing canonical folder is not an upgrade, and the add recreates the folder from nothing (vercel-labs/skills@7407f389 src/installer.ts L195, called at L389). classify_skill returned "install" when SKILL.md was not the pinned one, before the content check ran, so an edited SKILL.md plus a local file were deleted without --force. Now, when the lock names the pin (a project: source, ref, path and the pinned tree; a global entry: skillFolderHash), the folder is compared whatever made the add necessary (a missing Claude target, a missing link, an edited or missing SKILL.md); a folder that is gone is a clean repair; a lock that names another tree stays an upgrade over an older pin (the documented boundary). - git_tree_sha(strict=True) raises UnrepresentedEntry for an entry a git tree cannot hold (an empty directory, a FIFO, a socket, a device), which the add deletes all the same; the installer refuses it with a fixed text and never prints a tree for it. The status check stays a git-tree comparison. - check_folder_tree skipped runtime-artifact names by name at every level; it now skips only real directories (skip_dirs_only), as the installer does, so a regular file or a link named .venv is drift. - The two live line locators in the daily-record decision now point at budget_report (571) and claude_skills_dir (260). Fail-first: the final tests of tests.test_install_skills and tests.test_skills_status (194) give 23 failures and 6 errors against the implementation of 9a77a96 (9 tests: global and project edited-SKILL.md through the full installer path, the real pinned CLI in both, unrepresented entries, strict mode, the status check against the installer) and pass here; 20 of 20 installer mutants and 13 of 13 status mutants killed. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
9a77a96 to
5ace90c
Compare
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…n, and compare the folder in every other lock shape Fix wave 3 of #1035, on 5ace90c: Astra's delta read (~/.cache/ns2604-gpt-reads/fix2-pr1035-5ace90c12/VERDICT.md: one P1, one P2) and the command center's Claude full read (pr1035-claude-read-5ace90c1.md: PASS_WITH_P3, no P1; F3 here, F4 and F5 are claims of the PR body). - P1 (Astra): classify_skill returned "install" before the content check when a project lock entry lacked the optional `skillPath` (a lock written before the field existed), or its `computedHash` was not 64 hex digits, so the add recreated the folder from nothing (vercel-labs/skills@7407f389 src/installer.ts L195, called at L389) and deleted local work without --force. The pinned CLI makes these fields optional or does not need them whole: src/local-lock.ts L16-L37 (`ref?` and `skillPath?` are "optional for backward compatibility with lock files written before this field existed" and "omitted for non-repo sources"; `computedHash` is the CLI's own hash of the folder, not a revision), and readLocalLock and readSkillLock return an empty lock for a file that is too old or does not parse (src/local-lock.ts L83-L98; src/skill-lock.ts L89-L101). An entry that lacks them says nothing about which revision wrote the folder. The same early return took a project entry without `ref`, one that names another source and the entry the CLI writes for a local folder (`sourceType: local`, no `ref`, no `skillPath`; measured with the real CLI), and a global entry whose `skillFolderHash` is missing, empty or not text. The rule: only an entry that positively names another revision of the manifest's source (a project `ref` that is non-empty text and is not the pin's; a global `skillFolderHash` that is non-empty text and is not the pin's tree) is an upgrade over an older pin and runs as before. Every other entry is read like no lock: the folder must be the pinned tree, a folder that is gone is a clean repair, and `ok` needs the whole entry (source, type, `ref`, `skillPath`, a 64-digit `computedHash`; the global tree) with the pinned SKILL.md and the link. - F3 (command center): the module docstring (printed by --help) now gives the scope of the rule as it is: the edited SKILL.md, the lock shapes, the entries a git tree cannot hold. - P2 (Astra), the earlier review's locator premise: the previous read found the two live line locators of the daily-record decision "two lines short" (250 and 558 against 252 and 560). That was a false positive: lines are counted by LF (editors, grep -n, GitHub), and str.splitlines() also breaks at U+2028 and U+2029, one of each in skills_status.py, which shifted its count by two. The links of 5ace90c (260 and 571) are updates for this change's own line movement and stay. Fail-first: the tests of tests.test_install_skills and tests.test_skills_status (202) give FAILED (failures=34) in 5 tests on 5ace90c (the global lock shapes, the project lock shapes for both agents, and the real pinned CLI for a lock without skillPath, for the lock the CLI writes for a local folder and for a global lock without a tree) and pass here. Mutants: 15 of the new lock logic, 11 retained of the installer and 13 of the status check, all killed; nine earlier mutants of branches that this commit rewrites are replaced by the 15. Not taken, listed in the PR body: F1 (a project repair is refused, fail-closed, for a folder whose pinned tree holds a symlink or metadata.json, which the CLI's copy dereferences or leaves out), F2, F6. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s: an entry names an older pin only as a git tree id or a commit id of the manifest's source Fix wave 4 of #1035, on 453ac68, by the command center's adjudication (adjudication-pr1035-453ac683-20261012.md: the lock-shape class recurred, so the fix starts from the upstream definition, enumerates every lock-entry form the pinned CLI writes or leaves, project and global, builds the test table from it and fails closed for anything outside it). Inputs: Astra's delta read (~/.cache/ns2604-gpt-reads/fix3-pr1035-453ac6834/VERDICT.md: one P1, one P2, seven P3) and the command center's Claude delta (pr1035-claude-micro-453ac683.md: B1 at P1, one P2, five P3). - P1, both files: classify_skill returned "install" before the content check for a global entry whose `skillFolderHash` is any non-empty text that is not the pin's tree (Astra: "not-a-tree", 39 hex digits, one space), for the CLI's own sha256 digest of the folder (the CC: what src/add.ts L2108-L2111 writes where the GitHub tree lookup returns nothing, and for every gitlab, git and local source), and for an entry of another source or type (src/add.ts L2114-L2122 records `source` and `sourceType` for every global install); the add then recreated the folder (src/installer.ts L195, called at L389) and deleted local work without --force. Reproduced with the real pinned CLI for a generic git source, global and project (the entry it writes holds the digest, a URL and no `ref`). - The reading is now one function, lock_reading, built from the table: an entry names another revision only through a field the CLI writes as one, in that form (a global `skillFolderHash` that is a git tree id, 40 hex digits matched case-insensitively as src/update.ts L638 does; a project `ref` that is a full commit id), and only for the manifest's GitHub source; a tree id equal to the pin's is a content address and reads "same" whatever source wrote it. Everything else is "unidentified" and the folder is compared like an unlocked one: a digest, a URL source, a branch, a tag or an abbreviated id as `ref` (src/local-lock.ts L19-L20: what the user typed), an empty, missing or malformed field. A lock file the CLI reads as empty (no numeric `version`, a version below 3 global or 1 project, no `skills` table; for a project any entry that is not an object: src/skill-lock.ts L89-L101, src/local-lock.ts L83-L98) holds no entry (lock_skills). `ok` needs the whole entry and the pinned SKILL.md and link. - The table: 53 entry forms (26 global, 27 project) and 20 lock-file forms, each with its upstream citation, in the "Lock entries" section of the decision record and in LOCK_ENTRY_TABLE and LOCK_FILE_TABLE of tests.test_install_skills, run for the global scope and for a project under both agents in three folder states (pristine, a local file beside the pinned ones, folder gone), through the classifier and through the full installer path with the fake CLI, plus the real pinned CLI for the generic git source. - The command center's P2 (body line 107, F3 listed as open), P3-1 (the test count), P3-2 (a ref that is a branch or a tag is compared: it can name the pin's own commit), P3-3 (the lock version rule), P3-4 (the fixtures write the numeric `version` and the source the CLI writes), P3-5 (the quotation belongs to `skillPath`; the pinned CLI writes no global lock for a local folder): fixed here or in the body. F1 (a project repair is refused for a folder whose pinned tree holds a symlink or metadata.json), F2, F6 and the P3s of both reads stay in the PR body's Follow-ups. Fail-first: tests.test_install_skills and tests.test_skills_status (208 tests) give FAILED (failures=41, errors=85) in 5 tests on 453ac68 (the lock-entry table, the lock-file table, lock_skills, the installer path and the real-CLI generic git source) and pass here. Mutants: 28 of the lock reading (the version rule and its minimums, the boolean and text versions, the null-entry rule, what counts as an id, its case and length, the pin comparison, the source and its type, the project and global field, every branch that sends an entry that identifies nothing to an install or to ok, the whole-entry test, the tree oracle, a gone folder), 11 retained of the installer and 13 of the status check: all killed (survivors: none); nine earlier installer mutants named branches of classify_skill that this commit rewrites, and the 28 replace them. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
… (F3 of #1024) Where an add is about to run over an existing folder whose SKILL.md is the pinned one (an unlocked folder, or one locked at the pin whose Claude link is missing), the folder must hold SKILL.md alone or be the pinned tree: the git tree SHA that scripts/skills_status.py already computes, with the regenerable runtime-artifact directories left out, equals the manifest's tree_sha. Anything else is local-modified, refused unless --force, for the global canonical folder, a Claude-Code-only copy and a project folder. Measured with the real pinned CLI: a file added to an unlocked gh-fix-ci folder was deleted at main 83cec12 and survives now. The canonical-link rule stays literal; an upgrade over an older lock and an ok install are not covered. Also the --force help text (P3-2), the Codex-only scope of the link rule in the lifecycle record (P3-3) and a missing space in a test (P3-5). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… parent aliases install_skills.py refuses a link at the canonical skill path and accepts a link on a parent folder (createSymlink L240-250); scripts/skills_status.py followed the first and failed the second. It now reports canonical_link for a link at the skill path (the reverse alias of #1024 included) and accepts a real folder that a parent link makes the canonical folder itself (kind parent_link), for a global entry only: the Claude-Code-only copy check is unchanged. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Last commit of the change: the rows of every file it adds or changes, at their final bytes. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… add over local work in a locked project; keep the push gate's read inventory unchanged The first reads of #1035 (Astra at 295ada5 and the four open Codex review threads) found two ways the guard still lost local work and one regression. One commit: - A regular file or a link named `.venv`, `node_modules`, `__pycache__` and the like was taken for a regenerable directory (the name was excluded before the entry's type was looked at), and the add deleted it. git_tree_sha now skips such a name only for a real directory and does not follow a link. The SKILL.md-alone shortcut, which had the same flaw and also followed a SKILL.md link, is removed: a lone SKILL.md is hashed like any other folder. - A project locked at the pin whose Claude target is missing returned `install` before the content check, so the repairing add deleted local files. The check now runs before that repair in a project as it does in the global scope. - The shortcut's os.listdir was a sixth unclassified read site of the push gate's monitoring baseline (342 -> 343; tests.test_runtime_worker_openhands_push_gate failed 1 of 125 and repeated). With the shortcut gone the installer has its five reads again: 125 of 125 pass at this head. - The status check rejected a canonical-leaf link for a Codex-only entry that the installer accepts; both now apply the rule to entries installed for Claude Code only. The moved link to budget_report in the 2026-10-10 record points at its current line. tests/test_install_skills.py::PinnedCliForeignContentTests repeats the two measurements offline with the pinned skills 1.7.0 CLI where it is installed (a local source folder, a scratch home and project, telemetry off): the classifier must say local-modified before any add, and the same add on a copy must delete the entry (the control). Where the CLI is not installed it is skipped; the fake-CLI tests cover the same states. The record and the lifecycle are updated; the evidence rows are re-registered at their final bytes. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…sed on entries a git tree cannot hold, and judge runtime-named files the same way in the status check Fix wave 2 of #1035, on 9a77a96: the second GPT read (blockers 1 and 2, follow-up 3) and the open Codex thread (P2, "Fail closed on entries omitted from the tree hash"). - A lock at the requested pin plus an existing canonical folder is not an upgrade, and the add recreates the folder from nothing (vercel-labs/skills@7407f389 src/installer.ts L195, called at L389). classify_skill returned "install" when SKILL.md was not the pinned one, before the content check ran, so an edited SKILL.md plus a local file were deleted without --force. Now, when the lock names the pin (a project: source, ref, path and the pinned tree; a global entry: skillFolderHash), the folder is compared whatever made the add necessary (a missing Claude target, a missing link, an edited or missing SKILL.md); a folder that is gone is a clean repair; a lock that names another tree stays an upgrade over an older pin (the documented boundary). - git_tree_sha(strict=True) raises UnrepresentedEntry for an entry a git tree cannot hold (an empty directory, a FIFO, a socket, a device), which the add deletes all the same; the installer refuses it with a fixed text and never prints a tree for it. The status check stays a git-tree comparison. - check_folder_tree skipped runtime-artifact names by name at every level; it now skips only real directories (skip_dirs_only), as the installer does, so a regular file or a link named .venv is drift. - The two live line locators in the daily-record decision now point at budget_report (571) and claude_skills_dir (260). Fail-first: the final tests of tests.test_install_skills and tests.test_skills_status (194) give 23 failures and 6 errors against the implementation of 9a77a96 (9 tests: global and project edited-SKILL.md through the full installer path, the real pinned CLI in both, unrepresented entries, strict mode, the status check against the installer) and pass here; 20 of 20 installer mutants and 13 of 13 status mutants killed. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…n, and compare the folder in every other lock shape Fix wave 3 of #1035, on 5ace90c: Astra's delta read (~/.cache/ns2604-gpt-reads/fix2-pr1035-5ace90c12/VERDICT.md: one P1, one P2) and the command center's Claude full read (pr1035-claude-read-5ace90c1.md: PASS_WITH_P3, no P1; F3 here, F4 and F5 are claims of the PR body). - P1 (Astra): classify_skill returned "install" before the content check when a project lock entry lacked the optional `skillPath` (a lock written before the field existed), or its `computedHash` was not 64 hex digits, so the add recreated the folder from nothing (vercel-labs/skills@7407f389 src/installer.ts L195, called at L389) and deleted local work without --force. The pinned CLI makes these fields optional or does not need them whole: src/local-lock.ts L16-L37 (`ref?` and `skillPath?` are "optional for backward compatibility with lock files written before this field existed" and "omitted for non-repo sources"; `computedHash` is the CLI's own hash of the folder, not a revision), and readLocalLock and readSkillLock return an empty lock for a file that is too old or does not parse (src/local-lock.ts L83-L98; src/skill-lock.ts L89-L101). An entry that lacks them says nothing about which revision wrote the folder. The same early return took a project entry without `ref`, one that names another source and the entry the CLI writes for a local folder (`sourceType: local`, no `ref`, no `skillPath`; measured with the real CLI), and a global entry whose `skillFolderHash` is missing, empty or not text. The rule: only an entry that positively names another revision of the manifest's source (a project `ref` that is non-empty text and is not the pin's; a global `skillFolderHash` that is non-empty text and is not the pin's tree) is an upgrade over an older pin and runs as before. Every other entry is read like no lock: the folder must be the pinned tree, a folder that is gone is a clean repair, and `ok` needs the whole entry (source, type, `ref`, `skillPath`, a 64-digit `computedHash`; the global tree) with the pinned SKILL.md and the link. - F3 (command center): the module docstring (printed by --help) now gives the scope of the rule as it is: the edited SKILL.md, the lock shapes, the entries a git tree cannot hold. - P2 (Astra), the earlier review's locator premise: the previous read found the two live line locators of the daily-record decision "two lines short" (250 and 558 against 252 and 560). That was a false positive: lines are counted by LF (editors, grep -n, GitHub), and str.splitlines() also breaks at U+2028 and U+2029, one of each in skills_status.py, which shifted its count by two. The links of 5ace90c (260 and 571) are updates for this change's own line movement and stay. Fail-first: the tests of tests.test_install_skills and tests.test_skills_status (202) give FAILED (failures=34) in 5 tests on 5ace90c (the global lock shapes, the project lock shapes for both agents, and the real pinned CLI for a lock without skillPath, for the lock the CLI writes for a local folder and for a global lock without a tree) and pass here. Mutants: 15 of the new lock logic, 11 retained of the installer and 13 of the status check, all killed; nine earlier mutants of branches that this commit rewrites are replaced by the 15. Not taken, listed in the PR body: F1 (a project repair is refused, fail-closed, for a folder whose pinned tree holds a symlink or metadata.json, which the CLI's copy dereferences or leaves out), F2, F6. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s: an entry names an older pin only as a git tree id or a commit id of the manifest's source Fix wave 4 of #1035, on 453ac68, by the command center's adjudication (adjudication-pr1035-453ac683-20261012.md: the lock-shape class recurred, so the fix starts from the upstream definition, enumerates every lock-entry form the pinned CLI writes or leaves, project and global, builds the test table from it and fails closed for anything outside it). Inputs: Astra's delta read (~/.cache/ns2604-gpt-reads/fix3-pr1035-453ac6834/VERDICT.md: one P1, one P2, seven P3) and the command center's Claude delta (pr1035-claude-micro-453ac683.md: B1 at P1, one P2, five P3). - P1, both files: classify_skill returned "install" before the content check for a global entry whose `skillFolderHash` is any non-empty text that is not the pin's tree (Astra: "not-a-tree", 39 hex digits, one space), for the CLI's own sha256 digest of the folder (the CC: what src/add.ts L2108-L2111 writes where the GitHub tree lookup returns nothing, and for every gitlab, git and local source), and for an entry of another source or type (src/add.ts L2114-L2122 records `source` and `sourceType` for every global install); the add then recreated the folder (src/installer.ts L195, called at L389) and deleted local work without --force. Reproduced with the real pinned CLI for a generic git source, global and project (the entry it writes holds the digest, a URL and no `ref`). - The reading is now one function, lock_reading, built from the table: an entry names another revision only through a field the CLI writes as one, in that form (a global `skillFolderHash` that is a git tree id, 40 hex digits matched case-insensitively as src/update.ts L638 does; a project `ref` that is a full commit id), and only for the manifest's GitHub source; a tree id equal to the pin's is a content address and reads "same" whatever source wrote it. Everything else is "unidentified" and the folder is compared like an unlocked one: a digest, a URL source, a branch, a tag or an abbreviated id as `ref` (src/local-lock.ts L19-L20: what the user typed), an empty, missing or malformed field. A lock file the CLI reads as empty (no numeric `version`, a version below 3 global or 1 project, no `skills` table; for a project any entry that is not an object: src/skill-lock.ts L89-L101, src/local-lock.ts L83-L98) holds no entry (lock_skills). `ok` needs the whole entry and the pinned SKILL.md and link. - The table: 53 entry forms (26 global, 27 project) and 20 lock-file forms, each with its upstream citation, in the "Lock entries" section of the decision record and in LOCK_ENTRY_TABLE and LOCK_FILE_TABLE of tests.test_install_skills, run for the global scope and for a project under both agents in three folder states (pristine, a local file beside the pinned ones, folder gone), through the classifier and through the full installer path with the fake CLI, plus the real pinned CLI for the generic git source. - The command center's P2 (body line 107, F3 listed as open), P3-1 (the test count), P3-2 (a ref that is a branch or a tag is compared: it can name the pin's own commit), P3-3 (the lock version rule), P3-4 (the fixtures write the numeric `version` and the source the CLI writes), P3-5 (the quotation belongs to `skillPath`; the pinned CLI writes no global lock for a local folder): fixed here or in the body. F1 (a project repair is refused for a folder whose pinned tree holds a symlink or metadata.json), F2, F6 and the P3s of both reads stay in the PR body's Follow-ups. Fail-first: tests.test_install_skills and tests.test_skills_status (208 tests) give FAILED (failures=41, errors=85) in 5 tests on 453ac68 (the lock-entry table, the lock-file table, lock_skills, the installer path and the real-CLI generic git source) and pass here. Mutants: 28 of the lock reading (the version rule and its minimums, the boolean and text versions, the null-entry rule, what counts as an id, its case and length, the pin comparison, the source and its type, the project and global field, every branch that sends an entry that identifies nothing to an install or to ok, the whole-entry test, the tree oracle, a gone folder), 11 retained of the installer and 13 of the status check: all killed (survivors: none); nine earlier installer mutants named branches of classify_skill that this commit rewrites, and the 28 replace them. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
23e2dcb to
5f23fc3
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f23fc358a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…JSON constants and a local source that is not text Fix wave 5 of #1035 on 5f23fc3 (23e2dcb rebased onto main 2f59d74), released by the command center. Inputs: Astra's delta read of 23e2dcb (GPT-VERDICT-23e2dcbe9.md, 30,112 bytes, sha256 2ec782f8: three P1, one P2 carried under the scope lock, seven P3) and the command center's tests-only Claude check, which passed. - P1, global and project: load_lock took the tokens NaN, Infinity and -Infinity, which json.loads accepts and JSON.parse rejects (skills@7407f389 src/skill-lock.ts L85, src/local-lock.ts L80); both pinned readers return an empty lock when it throws (L99-L101, L97-L98), so the entry in such a lock excused nothing, but the installer read it as an older pin and let the add recreate the folder over local work. load_lock now passes parse_constant=reject_constant (CPython's hook for invalid JSON numbers, Doc/library/json.rst L324-L329 at python/[email protected]): a lock with a constant anywhere is unreadable and the folder is compared like an unlocked one. 1e999 is a valid number (both parse it as Infinity) and stays readable. - P1, project: lock_skills read every entry that is an object, but the pinned reader's loop throws, and its catch returns an empty lock, for a null entry and for a local entry whose source is missing or is not text (src/local-lock.ts L90-L98: isAbsolute(entry.source)); another entry of that kind excused the deletion for the managed entry at another commit. lock_skills now reads such a project lock as empty, for the managed entry or any other. The source description is corrected: the reader does not throw on an entry that is a string, a number, true or a list (measured by executing the pinned reader bodies on Node 24.21.0, reader-measure-20261012.txt); this installer still reads such a lock as empty, which is stricter than the reader and is now documented as such. - P1, privacy: the Git fixture of the native generic-source test gave author and committer an email-shaped literal; both are now the placeholder no-address, which is not an email (git takes it). - The fake CLI of the tests read a lock more leniently than the pinned readers, so a damaged lock survived its own add; its load_lock now mirrors them (constants, version, falsy skills, the project null and local-source rules), which lets the clean-install controls run on the complete installer path. Tests: LOCK_FILE_TABLE gained 19 forms (GL11-GL16, PL10-PL22; 39 in all) and LOCK_FILE_STRICTER_TABLE holds the four forms the reader keeps and this installer does not; every form runs through the classifier and through the whole installer path (refused with no add and the folder untouched where local work sits; installed where nothing is at risk), plus a unit test of load_lock on text, cases of lock_skills, and two controls with the real pinned CLI (the native add deletes the file the classifier protects). Fail-first: tests.test_install_skills and tests.test_skills_status (212 tests) give FAILED (failures=76, errors=1) in 6 tests on 5f23fc3 and pass here (136 + 76). 10 mutants of the new reader rules and 4 of the fake CLI's reader killed. Astra's P2 and the P3s go to the PR body's Follow-ups. The evidence rows of the changed files are updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ffcf9af52
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Scope
--force. Wherever an add is about to run over an existing folder (an unlocked canonical folder; a folder locked at the pin whose Claude link, or in a project whose Claude target, is missing or whoseSKILL.mdwas edited; a folder whose lock does not name another revision of the manifest's source in the form the CLI writes one (a globalskillFolderHashthat is a git tree id, a projectrefthat is a full commit id): every other form the CLI writes or leaves, such as its own digest of the folder, a URL source, a branch or a tag asref, an optional field missing, a lock file it reads as empty, is read as naming nothing; a Claude-only copy; a project) the folder must be the pinned tree: an extra file, an edited or missing file, a regular file or link that merely bears the name of a runtime directory (.venv,node_modules,__pycache__), and an entry a git tree cannot hold (an empty directory, a FIFO, a socket, a device) are refused aslocal-modified. The status check agrees with the installer on the layouts they disagreed on (a link at the canonical skill path for an entry installed for Claude Code, a link on a parent folder, a file or link named like a runtime directory), and three text items from the reads of Require the Claude Code link in the installer's ok state for a global skill #1024 (P3-2, P3-3, P3-5) are fixed. The narrower canonical-link rule that Require the Claude Code link in the installer's ok state for a global skill #1024's ruling left to this change is not taken (decision record).2f59d7448c7e4a51cf592f78818838353ba2dca9(main after Reconcile the MCP templates with the live registrations: code graph key, promptfoo and a drift report #1034, the target of the second rebase of 2026-10-12 below; the first rebase of the day targeted66584736c076873ca7e5898e3bd8111cca1a1447, main after Validate dated grant counts and the newest advisory delta #1030). The branch was cut at83cec12eec323cf3fc3d62177e38fe4163dbd3bb(main after Add fenced Claude usage sampling and Grafana panels #954), which stays the historical base of the first reads.lane:foundationtools/adoption/install_skills.py,scripts/skills_status.py,adoption/skills/lifecycle.md,docs/decisions/2026-10-12-skills-installer-foreign-content.md,docs/decisions/2026-10-10-daily-skill-invoke-rate.md(two links to moved lines),tests/test_install_skills.py,tests/test_skills_status.py,manifests/evidence.json.--forcehelp text and the lifecycle paragraph; (2) the status check's two layouts with their tests; (3) the evidence registry; (4) the fix wave for the first reads of295ada5e; (5) the fix wave for the second reads of9a77a96e; (6) the fix wave for the third reads of5ace90c1; (7) the fix wave for the fourth reads of453ac683, by the command center's adjudication; (8) the fix wave for the fifth reads of23e2dcbe, released by the command center (all below). Each fix wave is one commit that re-registers its own files.66584736c(the PR was DIRTY: a conflict inmanifests/evidence.jsononly). It is a pure rebase: each commit's diff without the registry has the same patch-id, so has the whole branch diff, and the registry rows of the 7 files the branch registers or changes are identical to those of the old head except the row ofdocs/decisions/2026-10-10-daily-skill-invoke-rate.md, which main changed in Validate dated grant counts and the newest advisory delta #1030 and which now hashes main's version with this branch's two link edits; the registry conflicts were resolved by writing each commit's rows again withscripts/host_receipts.py(register_file) at the rebased bytes. The commit names in this description are the heads the reads were made on:7749e5181is now4f7c57c1f,2f9bdde88is1b2331db8,295ada5e8isdc01ad015,9a77a96e8is2c289e529and the fix wave of commit 5 is5ace90c12.2f59d7448(Reconcile the MCP templates with the live registrations: code graph key, promptfoo and a drift report #1034 landed; the PR was DIRTY again, inmanifests/evidence.jsononly), at the command center's request, as a pure rebase with the same method: each commit's diff without the registry has the same patch-id (7 of 7), so has the whole branch diff, and the registry rows of the 7 files the branch registers or changes are identical to those of the old head (7 of 7); the registry conflict in the commit that registered the F3 files was resolved by writing each commit's rows again withregister_fileat the rebased bytes. The heads of the reads are now:4f7c57c1fisbe47e5c19,1b2331db8isbbfa8ac96,dc01ad015isc6565ef95,2c289e529iscabd3c89a,5ace90c12is091848186,453ac6834is59f476caaand23e2dcbe9is5f23fc358. Astra's delta read of23e2dcbe9was made before this rebase; the command center's tests-only check carries through it.Fix wave 5 (commit 8), for the fifth reads at
23e2dcbeReleased by the command center for ONE commit on
5f23fc358(its tests-only Claude check passed at23e2dcbe9and carries through the pure rebase:pr1035-claude-verify-23e2dcbe.md). Input: Astra's delta read of23e2dcbe9(CHANGES_REQUESTED: three P1, one P2 carried under the scope lock, seven P3;GPT-VERDICT-23e2dcbe9.md, 30,112 bytes, sha2562ec782f8124d7c931a45609228508cf0bd2c98aa80d45ffb5d9686eb925fd958). Fail-first: the finaltests.test_install_skillsandtests.test_skills_status(212 tests) giveFAILED (failures=76, errors=1)on5f23fc358(fail-before-5f23fc358.txt) and pass here (136 + 76).NaN,Infinityor-Infinitystill lets the add run over local work:json.loadsaccepts the tokens,JSON.parsethrows (src/skill-lock.tsL85,src/local-lock.tsL80) and both readers return an empty lock (L99-L101, L97-L98). Reproduced on the complete installer path and with the real pinned CLIload_lockpassesparse_constant=reject_constant, CPython's hook for invalid JSON numbers (Doc/library/json.rstL324-L329,Lib/json/decoder.pyL46-L49 atpython/[email protected]): a lock with a constant anywhere is unreadable and the folder is compared like an unlocked one.1e999is a valid number, parsed asInfinityby both, and stays readable. Measured: the pinned reader bodies executed on Node 24.21.0 (reader-measure-20261012.txt)LOCK_FILE_TABLEGL11-GL16andPL10-PL15(the version, another entry, the entry itself, a field outsideskills) through the classifier and the whole installer path (refused, no add, folder untouched; the pristine and absent-folder controls install, but for the six forms ofNO_CLEAN_INSTALL_CONTROL, whose reasons are in the test); the unit test ofload_lockon text; the real-CLI control for the global scope and the three tokenslock_skillsreads a project lock as empty for anullentry and for a local entry whosesourceis missing or is not text, in the managed entry or in any other (src/local-lock.tsL90-L98:isAbsolute(entry.source)throws). The description is corrected: the reader does not throw on an entry that is a string, a number,trueor a list (measured), and this installer still reads such a lock as empty, which is stricter and now documented as suchPL16-PL22andLOCK_FILE_STRICTER_TABLEPS1-PS4through the classifier and the installer path; the cases oflock_skills(text, empty text,Localin capitals, a non-local entry with a null source, the global reader); the real-CLI control for the project scope (null, number and list sources)no-address, which is not an email (git takes it); no email-shaped literal is left in the added linesload_lockmirrors the readers (a constant, a version that is not a number or is below the first, a falsyskills, and in a project a null entry or a local entry whose source is not text)Mutation check of the change: 10 mutants of the new reader rules (the hook missing, rejecting
NaNonly or all but-Infinity; the local-source rule missing, fornullonly, for every entry or for a missing source accepted, its test inverted; entries that are not objects kept; the per-entry rule applied to the global reader) and 4 of the fake's reader: all killed (survivors: none).Fix wave 4 (commit 7), for the fourth reads at
453ac683By the command center's adjudication (
adjudication-pr1035-453ac683-20261012.md: the lock-shape class recurred, so the fix starts from the upstream definition, enumerates every lock-entry form the pinned CLI writes or leaves, builds the test table from it and fails closed for anything outside it). Inputs: Astra's delta read (CHANGES_REQUESTED: 1 P1, 1 P2, 7 P3) and the command center's Claude delta (CHANGES_REQUESTED: B1 at P1, one P2, five P3). Fail-first: the finaltests.test_install_skillsandtests.test_skills_status(208 tests) giveFAILED (failures=41, errors=85)in 5 tests on453ac683(fail-before-453ac6834-final.txt) and pass here (132 + 76).skillFolderHashis any non-empty text that is not the pin's tree was read as an older pin and sent toinstallbefore the content check: Astra'snot-a-tree, 39 hex digits and one space; the CLI's own sha256 digest of the folder, whichsrc/add.tsL2108-L2111 writes where the GitHub tree lookup returns nothing and for every gitlab, git and local source; an entry of another source or type (src/add.tsL2114-L2122 records both for every global install). The add recreated the folder (src/installer.tsL195, called at L389) and deleted local work without--forcelock_reading, built from the table below: an entry names another revision only through a field the CLI writes as one, in that form (a globalskillFolderHashthat is a git tree id, 40 hex digits matched case-insensitively assrc/update.tsL638 does; a projectrefthat is a full commit id) and only for the manifest's GitHub source. A tree id equal to the pin's is a content address and reads "same" whatever source wrote it. Everything else is unidentified and the folder is compared like an unlocked one.okneeds the whole entryref; the native add deletes the extra file), global and projectsrc/add.ts,src/skill-lock.ts,src/local-lock.ts,src/update.ts,src/blob.tsorsrc/sync.tslines at the pin.LOCK_ENTRY_TABLE(53 forms: 26 global, 27 project) andLOCK_FILE_TABLE(20 forms) oftests/test_install_skills.pyare built from it. Anything outside the table is unidentified and compared (fail closed)test_the_lock_entry_table_gives_the_classifier_its_reading_for_every_form(every form x global, project/universal, project/claude-code x pristine, local file, folder gone),test_a_lock_file_the_pinned_cli_reads_as_empty_has_no_entries,test_the_installer_path_follows_the_table_in_the_state_that_destroys(the full installer path with the fake CLI,--check-onlyand an ordinary run,--forcefor the representative forms),test_lock_skills_holds_only_what_the_pinned_cli_reads,test_the_source_tree_oracle_decides_a_project_entry_that_says_the_pinrefthat is a branch or a tag counted as another revision (src/local-lock.tsL19-L20: what the user typed)refis comparedsrc/skill-lock.tsL89-L101,src/local-lock.tsL83-L98)lock_skills: no numericversion, a version below 3 (global) or 1 (project), noskillstable, a project lock with an entry that is not an object, a boolean version: no entryLOCK_FILE_TABLE,test_lock_skills_holds_only_what_the_pinned_cli_readsversionand no source of the manifest; the quotation ofsrc/local-lock.tsbelongs toskillPathonly; the pinned CLI writes no global lock for a local folderglobal_entry, the fake CLI's lock); the quotation and the local-folder test are correctedMutation check of the change: 28 of the lock reading, 11 retained of the installer and 13 of the status check: all killed (survivors: none); nine earlier installer mutants named branches of
classify_skillthat this commit rewrites, and the 28 replace them.Fix wave 3 (commit 6), for the third reads at
5ace90c1Inputs: Astra's delta read (CHANGES_REQUESTED: 1 P1, 1 P2, 4 P3) and the command center's Claude full read (PASS_WITH_P3: no P1, one P2 and five P3). Fail-first: the final
tests.test_install_skillsandtests.test_skills_status(202 tests) giveFAILED (failures=34)in 5 tests on5ace90c1(fail-before-5ace90c12-final.txt) and pass here (126 + 76).skillPathexisted (matching source, type andref), or with acomputedHashthat is not 64 hex digits, returnedinstallbefore the content check; the add recreated the folder (src/installer.tsL195, called at L389) and deleted local work without--force(real pinned CLI, the exact pinnedfind-skillssource). The one-step-wider check, as the command center asked ("every lock shape the pinned CLI supports"), found the same early return for a project entry withoutref, one that names another source, the entry the CLI itself writes for a local folder (sourceType: local, noref, noskillPath), and a global entry whoseskillFolderHashis missing, empty or not textrefthat is non-empty text and is not the pin's; a globalskillFolderHashthat is non-empty text and is not the pin's tree) is an upgrade over an older pin and runs as before. Every other entry is read like no lock: the folder must be the pinned tree, a folder that is gone is a clean repair, andokneeds the whole entry (source, type,ref,skillPath, a 64-digitcomputedHash; the global tree) with the pinnedSKILL.mdand the link. Cited:src/local-lock.tsL16-L37 (ref?andskillPath?"optional for backward compatibility with lock files written before this field existed", "omitted for non-repo sources";computedHashis the CLI's own folder hash) and L83-L98,src/skill-lock.tsL89-L101 (an unreadable or too old lock reads as empty)test_a_global_lock_that_names_no_tree_is_not_an_older_pin(5 shapes),test_a_project_lock_that_identifies_no_older_pin_is_compared_like_an_unlocked_folder(13 shapes x 2 agents, each with the extra file refused, the pinned folder alone repaired, a folder that is gone repaired), the two upgrade controls (anotherref, another global tree), the whole-entryokcontrol, and the real pinned CLI: a lock withoutskillPath, the lock the CLI writes for a local folder, a global lock without a tree (the native add deletes the extra file in each)66584736c076873ca7e5898e3bd8111cca1a1447. Dated correction of the previous review (2026-10-12): its finding was a false positive. Lines are counted by LF (editors,grep -n, GitHub);str.splitlines()also breaks at U+2028 and U+2029, one of each inskills_status.py, which shifted its count by two (252 and 560 against the LF 250 and 558). The links toclaude_skills_dirandbudget_reportwere correct, and the edits of5ace90c1(now 260 and 571) are updates for this change's own line movement--helpprints, still gave the round-1 scopeSKILL.md, the lock shapes, the entries a git tree cannot holdMutation check of the change: 15 mutants of the new lock logic (another revision not recognised, an empty
refor an empty global hash counted as another revision, another revision of any source counted, a lock that names no revision sent toinstallagain, an incomplete lockok, a malformedcomputedHashor a missingskillPathwhole, the guard skipped for an incomplete lock, a global entry without a tree another tree orok, the global guard skipped, a folder that is gone judged, a lock from another source type or name taken for the manifest's), with 11 retained of the installer and 13 of the status check: all killed (survivors: none). Nine earlier installer mutants named branches ofclassify_skillthat this commit rewrites; the 15 replace them.Fix wave 2 (commit 5), for the second reads at
9a77a96eAstra's second read (CHANGES_REQUESTED: 1 P1, 1 P2, 3 P3) and the open review thread of the Codex reviewer (P2). Each is fail-first: the final
tests.test_install_skillsandtests.test_skills_status(194 tests) fail on9a77a96e(FAILED (failures=23, errors=6), 9 of the tests,fail-before-9a77a96e8.txt) and pass here.SKILL.md(md_matchesfalse) returnedinstallbefore the content check, so the add recreated the folder from nothing (src/installer.tsL195, called at L389) and deleted the edit and every extra file without--force; a project with a matching source, ref, path and pinned tree and a missing Claude target took the same early returnskillFolderHash), the folder is compared whatever made the add necessary (a missing target, a missing link, an edited or missingSKILL.md); a folder that is gone is a clean repair; a lock that names another tree stays an upgrade over an older pin (the documented boundary)SKILL.mdwith a local file, with and without the link; a folder that is gone is repaired without--force(both scopes); the older-pin upgrade stays out of scope (control);PinnedCliForeignContentTestswith the real pinned CLI in both scopes (the native add deleted both the edit and the file; 2 of 2 fail on9a77a96e)check_folder_treeskipped runtime-artifact names by name at every level, so a regular.venv,scripts/.venvor a.venvlink wasruntime_artifactsfor the status check andlocal-modifiedfor the installer; the new body claim ("skips only real directories") was false at that headcheck_folder_treepassesskip_dirs_only=True, as the installer doesdrift, real directories keepruntime_artifacts;test_the_status_check_judges_content_as_the_installer_does(the status check's state for each entry the installer refuses; the installer's side is theForeignContentTestscases of the same entries)git_tree_sha): an entry a git tree cannot hold (an empty directory, a FIFO, a socket, a device) is left out of the hash, so a folder holding one equals the pinned tree while the add deletes itgit_tree_sha(strict=True)raisesUnrepresentedEntry; the installer asks for it, refuses with a fixed text that names the folder and prints no tree for it; the status check stays a git-tree comparison and its docstring says soreferences, or holding only a regenerated directory) and a named pipe givelocal-modifiedwith no add, while an empty regenerated.venvstays a clean install; strict mode ofgit_tree_shaon both kinds; the real pinned CLI deletes an empty directory and a named pipe (the extra-entry test, 2 subtests that fail on9a77a96e)budget_report(571) andclaude_skills_dir(260) at this headMutation check: 20 mutants of the installer change (the check disabled, each branch of
classify_skillnot asking or asking in the wrong place, a gone folder judged, the older-pin upgrade judged, runtime directories not skipped or skipped by name, a read error or an unrepresented entry counted as a pass, strict mode not asked for, the comparison inverted, the project scope not passed, anokinstall judged, the refusal text without the folder or the trees) and 13 of the status change (the canonical-link and parent-alias rules, the copy check,skip_dirs_onlyandstrictnot passed down, an empty directory or a special file accepted in strict mode): all killed (survivors: none).SOTA sources
src/installer.ts(the pinned CLI, read from the local mirror): L193-L200cleanAndCreateDirectoryremoves the directory recursively and L389-L390 writes the canonical folder anew, so everything the CLI did not write is lost with the old folder; L240-L250createSymlinkreturns without touching anything when the link path and the canonical folder are one real path (the parent-link layout); L255-L263 replaces whatever sits at the link path; L458-L466 lists what a copy leaves out (metadata.json,.git,__pycache__,__pypackages__) and makes the directory exclusions depend on the entry being a directory. The real pinned CLI reproduces the loss (below).scripts/skills_status.pygit_tree_shaandRUNTIME_ARTIFACT_DIRS(thefolder_treestate): the installer imports this existing predicate instead of a second one. Git's object format for trees and blobs is whatgit_tree_shaimplements; a git tree holds files, links and non-empty directories, which is whystrictrefuses the rest.research-queue.tsv, trigger#1024 LANDED, fired 06:55:24Z) and the command center's rulings on Require the Claude Code link in the installer's ok state for a global skill #1024 (04:06Z, 04:41Z).Evidence-class table
gh-fix-ci, a file added to an unlocked canonical folder is deleted by the installer at main83cec12eeand survives this changeinstalled, exit 0, the folder back to its 6 pinned files; afterlocal-modified, exit 1, 7 files; receipts retained locally (real-cli-before.jsonl,real-cli-after.jsonl).venv, a locked project with its Claude link missing, a locked install, global or project, with an editedSKILL.md, an empty directory) are deleted by the native add at the earlier heads and refused hererepro-f3-pushed.jsonlandrepro-f3.jsonl(the first read's states at both heads), andtests/test_install_skills.pyPinnedCliForeignContentTests(4 tests; run where the pinned CLI is installed, skipped elsewhere; two fail on295ada5e, three on9a77a96e)tree_sha(25 exactly, 3 with__pycache__left out), so the comparison needs no network and no per-file listpython3 scripts/skills_status.py --json,folder_trees; and a file-by-file blob SHA-1 comparison against the pinned vendor listing, 28 of 28SKILL.mdedited, a lock that does not name another revision of the manifest's source in the form the CLI writes one, in either scope, Claude-only copy, project, a name that only looks like a runtime directory, an entry a git tree cannot hold), and leaves an upgrade that a lock names as a git tree id or a commit id of the manifest's GitHub source, a missing folder and an ok install alonetests/test_install_skills.py(136 tests),ForeignContentTests(35) andPinnedCliForeignContentTests(10); the table of 53 lock-entry forms and 39 lock-file forms (and 4 the installer reads more strictly than the CLI); 28 of the lock reading, 11 retained of the installer, 10 of the fifth reads and 4 of the fake CLI's reader, and 13 of the status checktests/test_skills_status.py(76 tests); 13 mutants, all killedtests.test_runtime_worker_openhands_push_gate: 127 of 127, run in the module batch (the checker of #1030 reads the committed tree); no dated delta entry is neededinstall_skills.py --check-onlyreports 28ok;skills_status.pyreportsokhost-check-only.json,host-skills-status.json)NaN,Infinityor-Infinity, or in a project anullentry or a local entry whose source is not text, is read as empty by the pinned CLI, and the add recreates the folder; the installer now reads such a lock as empty and refuses the add over local workreader-measure-20261012.txt: 24 forms, empty or kept as the table says); the real pinned CLI (PinnedCliForeignContentTests, the two new controls); Astra's reproductions inGPT-VERDICT-23e2dcbe9.mdtests.test_install_skills.ForeignContentTests(the fake CLI,--check-onlyand an ordinary run, 39 forms + 4, both agents in a project)Local commands run
Decision record
docs/decisions/2026-10-12-skills-installer-foreign-content.mdcarries the measurement (the first and the second, from the reads), the decision, the alternatives (a GitHub listing at check time, a per-file list in the manifest, a marker file, refusing every unlocked folder, the narrower canonical-link rule), what is not covered and the conditions that overturn it.Host evidence
Only relevant when this PR adds or changes files under
evidence/hosts/. This PR changes none.Checklist
permissions: {}and grant each job only what it needs. None changed.Follow-ups
okinstall (no add runs). A pinned vendor folder that itself contains a file the CLI leaves out of a copy would count as local content; no managed row has one, and the status check reports the same folder asdrift..claude/skills/claude-native-practice/reference/slots.jsonL987 namesadoption/skills/lifecycle.md:167for/reload-plugins; the text is at L212 of this head (it moves with the paragraph above it) (the locator was stale before this PR; bind the historical observation to a revision or update it with the slot's provenance label).install_skills.pychecksSKILL.md, the recorded tree and the link after an add; a simulated incomplete add that omits support files returnsinstalledwhilecheck_folder_treereportsdrift): a separate post-add content verification with the same predicate and a recovery test; (b) the machine name inmanifests/evidence.json(21 occurrences on 19 lines in files this change touches, none added here) and in files of Require the Claude Code link in the installer's ok state for a global skill #1024 is the separate hygiene item of those reads; (c) the original native receipts are retained locally and named above (real-cli-before.jsonl,real-cli-after.jsonl,repro-f3-pushed.jsonl,repro-f3.jsonl, the fail-before and mutation outputs) for the next packet; the all-28 host comparison isskills_status.py --jsonand the file-by-file comparison named above.pr1035-claude-read-5ace90c1.md, PASS_WITH_P3; no P1), for a later change unless the command center rules otherwise: (F1, P2) a project-scope repair is refused, fail-closed and with nothing deleted, for a folder the pinned CLI itself wrote when the pinned tree holds a symlink or ametadata.json(the copy dereferences symlinks,src/installer.tsL525-L534, and leavesmetadata.jsonout, L458-L466, so the folder never hashes to the pinnedtree_sha; the runtime-worker manifest has 62 such rows with symlinks and 3 withmetadata.json): compare a project row with the tree the CLI writes, or record a per-row expected tree, with a regression whose pinned tree holds a symlink; the 28 host rows are not affected (the decision's measurement is scoped to them); (F2, P3) a project canonical leaf that is a link is replaced by the add (claude_link_problemis global-only): apply the canonical-link rule in project scope or record the boundary; (F6) an emptied canonical folder is refused with "git tree None" in the text (cosmetic).metadata.json: compare against the tree the CLI writes, or record a per-row expected tree); F2 and Astra's P3 3 (a project canonical leaf that is a link is replaced); F6 and Astra's P3 5 (an emptied folder is refused withgit tree Nonein the text); post-add verification of support files (Astra's P3 4); the hostname hygiene (21 occurrences on 19 lines, none in the patch or the body); the historical captures (Astra's P3 8); Astra's P3 9 (on the merged tree with the live base,convert.pycallsscripts/host_name_scan.py, which needs the host identity: 441 landscape subtests and 51 saturation-ledger failures in her restricted runner, none from this PR's paths; the complete merged suite belongs in the intended host environment).23e2dcbe9) that stay follow-ups: the P2 carried under the scope lock (pristine vendor-copy transformations are still over-refused: compare with the vendor's installed representation or record a per-row expected tree, the F1 item above); P3 5 (a project canonical leaf link is replaced), 6 (post-add verification does not attest support files), 7 (an emptied folder is refused withgit tree Nonein the text), 8 (the locator inslots.jsonL987), 9 (the host name in unchanged content, 21 occurrences on 19 lines), 10 (the cited captures are not attached; the packet's hash manifest for them is a packaging item); and 11, new here: the native Git-source test andcli()build a child environment withoutTMPDIR, so a runner whose/tmpis read-only fails two subtests before the assertion (passTMPDIRthrough; not a production defect).claude-native-practiceatc59655c7): the no-excluded-members claim is checked for the other 25 rows (exact pinned trees) and by the host comparison for all 28.🤖 Generated with Claude Code
Follow-ups added at landing (command center, 2026-10-12)