fix(skills-placement): require opt-in for inline skill delivery#249
Conversation
composeSkillsForHarness silently fell back to inline delivery —
rendering every skill BODY into the system prompt — whenever a
harness had no native skill-mount directory. Of the 14
KNOWN_HARNESSES, 7 (amp, factory-droids, hermes, forge, acp, cursor,
cli-base) hit this path, and one product reproduced a 151,882-byte
prompt this way.
An AgentProfile is a recipe that materializes into a sandbox, not a
prompt template: skill bodies belong in a file mount the agent reads
on demand, not concatenated into prompt.systemPrompt. Inline delivery
is a narrow fallback, never a co-equal mode.
composeSkillsForHarness now takes onNoSkillDir ('throw' | 'inline'),
defaulting to 'throw'. The thrown error names the harness, the skill
count, the byte size that would have been inlined, and the harnesses
that support native mounting. Callers that genuinely need inline
delivery opt in explicitly with onNoSkillDir: 'inline'.
The low-level composeSkills already required an explicit mode; this
closes the one recommended integration point that auto-fell back
around it.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 01a1d3ca
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-24T23:51:30Z
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 2 (2 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 103.9s (2 bridge agents) |
| Total | 103.9s |
💰 Value — sound-with-nits
Flips composeSkillsForHarness from silently inlining every skill body into the prompt (a 151KB-prompt footgun) to throwing by default with an actionable error — a clean, grain-aligned fail-loud change with an explicit opt-in escape hatch.
- What it does: Adds
onNoSkillDir: 'throw' | 'inline'tocomposeSkillsForHarness(src/skills-placement/index.ts:127), defaulting to'throw'. For the 7 of 14 KNOWN_HARNESSES with no native skill-mount dir (amp, factory-droids, hermes, forge, acp, cursor, cli-base), the old code silently fell back toinlinedelivery — rendering every skill BODY intosystemPrompt. Now it throws an error naming the harness, - Goals it achieves: Stop a silent prompt-bloat footgun: a product reproduced a 151,882-byte system prompt this way because switching harness invisibly converted mounted skills into prompt text with no signal. The doctrinal point (spelled out in the new JSDoc) is that an
AgentProfileis a recipe that materializes into a sandbox, not a prompt template — skill bodies belong in a file mount the agent reads on demand, a - Assessment: Strong change, squarely in the codebase's grain. The repo's dominant pattern is fail-loud-not-silent-fallback — a grep for
silently|fail.?loud|refuse|rather than silentlyreturns ~100 hits across crypto, billing, tools/registry, config, intakes, sandbox, missions, sequences, and the siblingparseSkillFrontmatter/composeSkillsin this very module. The design is coherent: (1) default-throw mat - Better / existing approach: none — this is the right approach. Searched for an existing fail-loud-on-no-skill-dir guard to extend (none —
composeSkillsrequires explicitmodebut the high-levelcomposeSkillsForHarnesswas the only auto-fallback path, and it's the one being fixed here). Considered warn-vs-throw (warn-and-continue would be ignored, defeating the point for a silent prompt-size regression; the codebase's g - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound
Converts a silent 150KB+ prompt footgun (auto-inlining skill bodies for 7 of 14 harnesses) into a loud, actionable error with a one-line opt-in escape hatch — extending the codebase's existing fail-loud grain to the harness-resolution layer.
- Integration: Reachable and correctly wired.
composeSkillsForHarnessis the documented public entry point of the/skills-placementsubpath and is named as the canonical caller byProfileChannels.skillRefs(src/profile/index.ts:84) and bycomposeSkills' own JSDoc (src/skills/index.ts:639). No internal call site exists, but that is expected — agent-app is an app-shell library consumed by external products - Fit with existing patterns: Matches the established grain exactly.
composeSkillsalready throws whenskillDiris null formountedmode (src/skills/index.ts:654-659); this PR extends that same fail-loud posture one layer up to the function that chooses the mode. The pre-existingunsupportedSkillHarnesses(src/skills-placement/index.ts:59) already enumerates unbridged harnesses, and the new private `nativeSkillMountHar - Real-world viability: Holds up past the happy path. The byte count in the error is computed from the REAL
renderInlineSkillsoutput (src/skills-placement/index.ts:134-135) — the identical call thatcomposeSkills({mode:'inline'})makes at src/skills/index.ts:649 — so the warning number is what would actually ship, not a proxy. The error names the harness, the tier-filtered skill count, the byte size, the harnesses t - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 AGENTS.md module map still advertises the silent fallback this PR removes [maintenance] ``
The PR updates doc comments in src/skills-placement/index.ts, src/skills/index.ts, docs/codemap.json, docs/api/skills-placement.md, and docs/llms-full.txt, but AGENTS.md:43 — the module map agents read as the canonical reference — still lists
composeSkillsForHarness (auto-inline fallback for harnesses with no cwd skill dir — hermes, amp, …)as a FEATURE. That now directly contradicts the change's intent and could mislead the next agent (or human) into expecting the old behavior. One-line edit
🎯 Usefulness Audit
🟡 Error message bakes in a one-off historical anecdote [ergonomics] ``
The thrown string hardcodes 'a 151,882-byte prompt shipped this way once' (src/skills-placement/index.ts:140). Runtime error messages should be timeless and self-justifying; this anecdote will read as stale/arbitrary in a year and is redundant — the message already shows the caller's OWN computed byte count two words earlier. Dropping the historical clause leaves the message purely actionable (harness + count + bytes + mountable list + opt-in) without dating it. Does not gate shipping.
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 4 non-blocking findings — 01a1d3ca
Full multi-shot audit completed 5/5 planned shots over 6 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-24T23:58:31Z · immutable trace
composeSkillsForHarnesssilently rendered every skill body into the system prompt wheneverresolveSkillDir(harness)returned null — 7 of 14 known harnesses (amp, factory-droids, hermes, forge, acp, cursor, cli-base). Switching harness therefore converted mounted skills into a giant prompt with no signal; one app reproduced a 151,882-byte prompt this way.Adds
onNoSkillDir: 'throw' | 'inline', defaulting to throw. The error names the harness, the skill count, the exact UTF-8 byte size that would have been inlined (computed from the realrenderInlineSkillsoutput, not a proxy), and which harnesses do support mounting.Verified:
tsc --noEmitclean; 193 files / 3,148 tests passing (baseline 3,143 + 5 new); 23/23 in the target suite; mutation-checked — reverting to the silent fallback fails the new tests.