Commit 98156ac
authored
fix(cli): survive an unwritable .codex/, inline skills when it happens, and ship the doctrine where the CLI can find it (#745)
* fix(cli): survive an unwritable .codex/, and inline skills when it happens
Codex sandboxes deny writes under `.codex/`. `installSkills` created its
destination with an unguarded `mkdirSync` sitting directly ABOVE a
per-skill copy loop that was already guarded — so the failure threw past
that fallback and past the caller, aborting the whole handoff step.
Because the skills install runs first, nothing after it ran either: no
AGENTS.md, no .cipherstash/context.json, no .cipherstash/setup-prompt.md.
All five Codex runs of the rc.3 skilltester matrix landed here, and that
report named it the primary driver of the Claude→Codex quality gap (#736).
Two changes.
`installSkills` never throws. Every filesystem step degrades to a warning
and a shorter return, so the caller decides what an empty result means
rather than being skipped entirely.
The Codex handoff falls back to inlining. When the skills cannot be
written, their bodies go into AGENTS.md — project root, writable —
through the same `doctrine-plus-skills` path the editor-agent handoff
already uses for Cursor / Windsurf / Cline. No new mechanism; the
existing one just was not reachable from this branch. The launch prompt
points at wherever the guidance actually ended up.
The fallback is only claimed when there is something to inline. A
stripped build ships no skills at all, so it stays doctrine-only and says
nothing — reporting a fallback that did not happen would be the same
false-success bug #714 and #687 removed elsewhere in init. That is why
`availableSkills` is split out: an empty install result alone cannot
distinguish "could not write" from "nothing to write".
@cipherstash/wizard carries its own copy of `installSkills` with the
identical unguarded `mkdirSync` above the identical guarded copy loop. It
targets `.claude/skills`, so the Codex sandbox case does not apply, but an
unwritable destination crashed it the same way — guarded to match.
Tests: 2 for the mkdirSync guard (including that the caller can still
distinguish unwritable from empty), 6 for the handoff across all three
states — copied, unwritable-so-inlined, and nothing-to-ship. 645 unit +
68 pty E2E pass; typecheck and build clean; 0 biome errors.
Checked per packages/cli/AGENTS.md: no E2E asserts on these strings (the
only codex assertion is the target list, unchanged), and no skill
documents the install location, so neither needed updating.
* fix(cli): make the Codex skills fallback deliver what it claims
Address the 15 findings from the PR #745 review.
The structural change: installSkills returns { copied, failed } instead
of a flat list, so callers distinguish unwritable / stripped / partial
outcomes without a second probe. The Codex handoff inlines exactly the
failed skills — a partial copy inlines the missing ones and the launch
prompt points at both locations.
The production-blocking find: the bundled AGENTS.md doctrine shipped at
dist/commands/init/doctrine, but findBundledDir probes ancestors of the
compiled chunk in dist/bin/ — so every published build wrote the minimal
AGENTS.md stub, and the inline fallback would have inlined nothing while
claiming otherwise. The doctrine now lands at dist/doctrine, like the
skills bundle, and buildAgentsMdBody honours doctrine-plus-skills even
when the doctrine fragment is missing.
Honesty and hardening around the fallback:
- writeArtifacts takes a SkillsDelivery (installed/inlined/failed);
context.json gains inlinedSkills and setup-prompt.md stops
mislabelling an unwritable destination as a "stripped build" — on the
Claude and AGENTS.md handoffs too
- availableSkills filters on SKILL.md, the predicate the inliner
actually uses, so "inlining N skills" never overstates
- the AGENTS.md upsert (which refuses malformed sentinel pairs) and the
bundled-file reads degrade to warnings instead of aborting the step
before .cipherstash/ is written; skills only count as inlined when
the write landed
- the fallback warning announces the observed recovery instead of
asserting an unverified cause, and flags a stale .codex/skills/ it
could not refresh
- a confirmed-then-failed wizard install is recorded in the wizard
changelog instead of vanishing with the terminal output
Tests pin the degrade-to-warning contract in both packages (the wizard
suite is new), cover the partial-copy split end to end, and replace the
vacuously-true warning assertions. The doctrine's "Where to read more",
the build-agents-md docblock, and root AGENTS.md now describe the real
mode consumers.
* test(cli): make the partial-copy fixture deterministic across platforms
The read-only-directory trigger was platform-dependent: on macOS cpSync
unlinks the destination file first (EACCES from the 0o555 parent), but
on Linux it overwrites the writable SKILL.md in place and the copy
succeeds — so the test failed on CI while passing locally.
Block the skill with a FILE where its directory must go instead:
cpSync then throws ERR_FS_CP_DIR_TO_NON_DIR, a type-mismatch error
raised before any permission-dependent operation, so it fires on every
platform and as root. Same technique the neighbouring mkdir test
already uses. Drops the skipIf guard the chmod approach needed.
* style(cli): format the partial-copy fixture line1 parent 508f1d5 commit 98156ac
22 files changed
Lines changed: 830 additions & 169 deletions
File tree
- .changeset
- packages
- cli
- src/commands
- impl/steps
- __tests__
- init
- doctrine
- lib
- __tests__
- status/__tests__
- wizard/src
- __tests__
- lib
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
107 | | - | |
108 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
109 | 111 | | |
110 | 112 | | |
111 | 113 | | |
| |||
Lines changed: 23 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
31 | | - | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
36 | 38 | | |
37 | | - | |
| 39 | + | |
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
Lines changed: 144 additions & 23 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
10 | 9 | | |
11 | 10 | | |
| 11 | + | |
12 | 12 | | |
| 13 | + | |
| 14 | + | |
13 | 15 | | |
14 | 16 | | |
15 | 17 | | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
| 18 | + | |
| 19 | + | |
22 | 20 | | |
23 | 21 | | |
| 22 | + | |
24 | 23 | | |
25 | 24 | | |
26 | 25 | | |
| |||
30 | 29 | | |
31 | 30 | | |
32 | 31 | | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
40 | 44 | | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
51 | | - | |
| 50 | + | |
| 51 | + | |
52 | 52 | | |
53 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
54 | 175 | | |
Lines changed: 17 additions & 17 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
3 | 1 | | |
4 | 2 | | |
5 | | - | |
6 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
13 | | - | |
14 | | - | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
44 | 40 | | |
45 | | - | |
| 41 | + | |
46 | 42 | | |
47 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
29 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
30 | 34 | | |
31 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
32 | 44 | | |
33 | 45 | | |
34 | 46 | | |
35 | | - | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
36 | 52 | | |
37 | 53 | | |
38 | 54 | | |
39 | | - | |
40 | | - | |
| 55 | + | |
| 56 | + | |
41 | 57 | | |
42 | | - | |
| 58 | + | |
43 | 59 | | |
44 | 60 | | |
45 | 61 | | |
| |||
0 commit comments