Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/instruction-placement/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "instruction-placement",
"version": "0.11.10",
"version": "0.11.11",
"description": "Routes agent-instruction content to the surface that loads it at the right moment. The audit skill sweeps a repository's instruction layer and its ordinary markdown for content whose scope is narrower than the surface carrying it \u2014 conventions keyed to one file type or one subtree sitting in an always-loaded CLAUDE.md or AGENTS.md \u2014 and for normative conventions stranded in documentation Claude never loads at all, then classifies each against a routing rubric and proposes a destination whose `paths:` glob is machine-validated before it is ever offered. Safety-class content (irreversible actions, secrets, data integrity, external publication, compliance, agent authority) is hard-denied from demotion and reported as held back rather than proposed, because demotion trades guaranteed presence for conditional presence and deferred surfaces are invisible inside subagents and absent after compaction until re-triggered. Every accepted move regenerates an always-loaded index of deferred surfaces, which is what keeps a demoted rule reachable from a subagent that never receives its injection. The audit is read-only and emits a diffable findings artifact; realignment is a separate skill gated per item with no blanket-approve path; a deterministic check skill gates that every rule glob still resolves and the index is current; and a setup skill verifies the one thing no other gate can see \u2014 that the index target is a file Claude Code will actually read, since it reads CLAUDE.md and not AGENTS.md.",
"author": {
"name": "Melodic Software",
Expand Down
40 changes: 40 additions & 0 deletions plugins/instruction-placement/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,46 @@
All notable changes to the `instruction-placement` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.11.11]

### Fixed

- **`adherence-experiment.sh` scored its underscore criterion against the whole file.** The seeded
`Billing.cs` already declares `private readonly decimal _unitPrice`, so the check returned 1
before the model had written anything and no run could ever fail it. The check is now scoped to
the body of the class the task asks for, by a scanner that biases toward under-crediting: a
body-less primary-constructor declaration, a brace inside a string or comment, and any other
shape that leaves the body undelimited all score 0 rather than running on into a later class and
crediting a field the task never asked for. For an instrument whose numbers get published, a
false 1 is the unrecoverable error. `adherence-results.md` carries a correction noting that the
recorded run's `underscore` and `both` columns were constants, not measurements; the conclusion
is unchanged, since it rests on the `sealed` criterion, which was scored correctly.
- **Both class matches now require a non-identifier boundary after the name.** `InvoiceTotal` is a
prefix of `InvoiceTotals` and `InvoiceTotalizer`, so a trial that produced a differently named
class scored full compliance on both criteria. The boundary still admits the three shapes that
occur here: the bare name, `InvoiceTotal {`, and `InvoiceTotal(decimal seed);`.
- **The underscore criterion credits a FIELD, not any member carrying an underscore name.** The
convention is about field naming, but the match accepted an auto-property
(`private decimal _total { get; set; }`), an expression-bodied property (`private decimal _Total
=> x;`) and a method (`private decimal _GetTotal() => x;`). The name must now be followed by a
`;` or by an initializer `=` that is not the `=>` of an expression-bodied member. A
multi-declarator line credits on its first name only, per the same under-crediting bias.
- **The usage banner documents `--filler`.** The flag has always been parsed, and
`adherence-results.md` gives `--filler 120` as the re-run command, but neither the banner nor the
header comment listed it.

### Added

- **`adherence-experiment.test.sh` covers the experiment harness.** 52 cases driving the harness
through a stub CLI, so every trial's output is controlled and nothing is skipped: argument
validation and the exit-3 unmeasurable path, arm construction (identical filler and identical
seed file in both arms, the convention delivered inline in one and as a `**/*.cs` path-scoped
rule in the other), scoring asserted in both directions, against the three C# shapes that decide
whether a field from another class is credited and the three near misses that carry the right
characters in the wrong construct, per-trial reset, the ERROR row for a failed trial and its
exclusion from the arm's `n`, and fixture cleanup. The file previously mapped to no suite under
`scripts/affected-tests.sh`.

## [0.11.10]

### Changed
Expand Down
94 changes: 88 additions & 6 deletions plugins/instruction-placement/evals/adherence-experiment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
# * Order is interleaved so drift in service conditions hits both arms alike.
#
# Usage:
# adherence-experiment.sh [--trials N] [--claude <path>] [--keep]
# adherence-experiment.sh [--trials N] [--filler N] [--claude <path>] [--keep]
#
# Exit: 0 the experiment ran; 2 usage error; 3 could not measure.

Expand All @@ -53,9 +53,12 @@ usage() {
adherence-experiment.sh — measure whether path-scoping improves adherence.

Usage:
adherence-experiment.sh [--trials N] [--claude <path>] [--keep]
adherence-experiment.sh [--trials N] [--filler N] [--claude <path>] [--keep]

--trials N runs per arm (default 6; 2 model calls per trial pair)
--filler N filler sections per half of the control file (default 24,
giving a ~250-line AGENTS.md); raise it to test for an
adherence effect that appears only at much greater bloat
--claude PATH Claude Code CLI to drive
--keep leave the fixture and transcripts on disk for inspection

Expand Down Expand Up @@ -216,11 +219,90 @@ TASK='Add a public class named InvoiceTotal to src/Billing.cs. It needs a privat
# Compliance, defined before any run.
# sealed: the produced class is declared sealed
# underscore: the new private field uses a leading-underscore name
#
# The underscore check is SCOPED to the InvoiceTotal body. The seeded file
# already declares `private readonly decimal _unitPrice`, so a whole-file search
# scores 1 before the model has written anything: the criterion becomes a
# constant, and a run reports a number that is not a measurement.
#
# The scanner below biases toward UNDER-crediting. Every way of failing to
# delimit the body ends at the declaration line alone, scoring 0, rather than
# running on into a later class and crediting a field that is not the one the
# task asked for. For an instrument whose numbers get published, a false 1 is
# the unrecoverable error and a false 0 is a visible one.
#
# Three shapes a model can plausibly produce, and what happens to each:
# `class InvoiceTotal(decimal x);` a body-less primary constructor. No brace
# body opens before the next declaration, so
# the region stops at the declaration line.
# `"unbalanced { brace"` a brace inside a string, char literal or
# line comment. Stripped before counting, so
# the depth counter stays in sync.
# anything else that desyncs a block comment holding a lone brace, say.
# Depth never returns to zero, the body is
# treated as undelimited, and the region
# stops at the declaration line.
#
# Both class matches are ANCHORED on a non-identifier boundary. `InvoiceTotal`
# is a prefix of `InvoiceTotals` and of `InvoiceTotalizer`, so an unanchored
# match scores full compliance for a trial that produced the wrong class. The
# boundary is `[^A-Za-z0-9_]` or end of line, which still admits the three
# shapes that occur here: `InvoiceTotal` alone, `InvoiceTotal {`, and
# `InvoiceTotal(decimal seed);`.
score_trial() {
local file="$1" sealed=0 underscore=0
grep -qE 'sealed[[:space:]]+class[[:space:]]+InvoiceTotal' "$file" 2>/dev/null && sealed=1
# A private field declaration in the new class whose name starts with `_`.
grep -qE 'private[^;]*[[:space:]]_[A-Za-z][A-Za-z0-9]*' "$file" 2>/dev/null && underscore=1
local file="$1" sealed=0 underscore=0 body
grep -qE 'sealed[[:space:]]+class[[:space:]]+InvoiceTotal([^A-Za-z0-9_]|$)' \
"$file" 2>/dev/null && sealed=1
# The new class, from its declaration to its matching closing brace.
body="$(awk '
# Brace characters that are data, not structure.
function scrub(s, q) {
q = sprintf("%c", 39)
gsub(/"[^"]*"/, "", s)
gsub(q "[^" q "]*" q, "", s)
sub(/\/\/.*/, "", s)
return s
}
# A line that declares some class. Rejects comment lines, which cannot.
function declares_class(s) {
return (s ~ /^[[:space:]]*[A-Za-z]/ &&
s ~ /(^|[[:space:]])class[[:space:]]+[A-Za-z_]/)
}
!seen && /class[[:space:]]+InvoiceTotal([^[:alnum:]_]|$)/ { seen = 1; decl = NR }
seen && !finished {
# A second declaration before any brace opened means InvoiceTotal has no
# brace body of its own, and the lines that follow belong to that class.
if (!entered && NR > decl && declares_class($0)) { finished = 1; next }
buf[++n] = $0
code = scrub($0)
opens = gsub(/\{/, "{", code)
closes = gsub(/\}/, "}", code)
depth += opens - closes
if (opens > 0) entered = 1
if (entered && depth <= 0) { finished = 1; closed = 1 }
}
END {
if (!seen) exit
if (!closed) n = 1
for (i = 1; i <= n; i++) print buf[i]
}
' "$file" 2>/dev/null)"
# A private FIELD declaration in the new class whose name starts with `_`.
#
# "Field" is the load-bearing word. The convention is about field naming, so
# a member that merely carries an underscore name does not satisfy it: an
# auto-property (`private decimal _total { get; set; }`), an expression-bodied
# property (`private decimal _Total => x;`) and a method
# (`private decimal _GetTotal() => x;`) all have to score 0. The trailing
# group is what separates them: a field declaration ends at `;` or continues
# into an initializer `=`, and `=[^>]` rejects the `=>` of an
# expression-bodied member. The leading `[^;{(=]*` cannot cross a `{`, a `(`
# or an `=`, so no later token on the line can be reached to stand in for the
# field. A multi-declarator line (`private decimal _a, _b;`) credits on its
# first name only, which is the documented under-crediting bias.
printf '%s\n' "$body" |
grep -qE 'private[^;{(=]*[[:space:]]_[A-Za-z][A-Za-z0-9]*[[:space:]]*(;|=[^>])' &&
underscore=1
printf '%d\t%d' "$sealed" "$underscore"
}

Expand Down
Loading