Repository navigation
The surveyor cannot resolve the default-branch CI classifier's admitted path, so rung 0 is blind on every repository #200
Description
🤖 Generated by the Agentic Engineer
Evidence
The surveyor's rung-0 step tells it to run the shipped default-branch classifier and forbids
reimplementing it:
Then invoke the shipped
../scripts/classify-default-branch-ci-runs.sh
with the repository, default-branch name, and that exact sha, resolving it from the installed,
reviewed plugin path. Do not reimplement the helper inline.
—agents/portfolio-surveyor.agent.md§4, at 4.4.22
The guard admits exactly one spelling of that call. forge-readonly-guard.sh sets
GUARD_DIR from its own location and matches only ${GUARD_DIR}/classify-default-branch-ci-runs.sh,
with the comment "Match the exact sibling of this guard, never a basename or caller-supplied path".
The consuming hook invokes the adapter with SURVEYOR_FORGE_READONLY_GUARD="${install_path}/scripts/forge-readonly-guard.sh",
so the only admitted path is ${install_path}/scripts/classify-default-branch-ci-runs.sh, where
install_path comes from the runtime registry (~/.claude/plugins/installed_plugins.json).
Nothing tells the surveyor what install_path is. The single pointer it has is a relative
markdown link, which resolves against the agent file inside the plugin — a location the surveyor
does not know as an absolute path. The runtime install root is version-numbered
(.../agentic-engineering/4.4.22) and changes on every plugin update, so it cannot be learned once
either.
Measured on a survey run 2026-09-06: every path the surveyor could construct was denied
a read must begin with a forge command — the consumer's .claude/scripts/, the marketplace
checkout, the plugin cache, the repos directory, and the libraries/agent-plugins submodule. The
one path that would have been admitted is the registry's installPath, which the surveyor never
derives. All three siblings do exist together under that root, so the guard's sibling rule is
satisfied there; the gap is purely that the caller cannot name it.
Impact
Rung 0 of the work-selection ladder is blind. The survey reported default-branch CI health as
unknown for all 19 active repositories, and nothing_on_fire: false follows from the unknown
rather than from any observed failure. Since a run works the ladder top-down, live breakage on a
default branch is not detected by the survey at all; it is found only if some other step happens to
trip over it.
This fails closed, which is the safe direction, but the cost is not zero: the top rung produces no
signal on any run, and the instruction's own prohibition on reimplementing the helper means the
surveyor cannot substitute anything.
This is the same class as monorepo#3123 (the programmed-bot exemption classifier is unreachable
under the same guard) but a different script, a different admission path, and a wider blast radius:
#3123 costs one PR classification per release, this costs rung-0 coverage on every repository on
every run.
Expected behaviour
The surveyor can invoke the mandated classifier at a path the guard admits, without relaxing the
guard and without reimplementing the helper.
Acceptance criteria
- The surveyor has a documented, runtime-independent way to reach the admitted classifier — the
resolution step spelled out where the hook already performs it, an environment variable the
adapter exports, or a wrapper the guard admits by shape. The plugin version in the path must
not have to be known in advance. - A survey run reports a real per-repository default-branch verdict (
green/ named red
identities) rather thanunknown, on a checkout where the classifier is installed. - The guard is unchanged in strength: offline
--inputstays denied, and only the remote-mode
argument shape is admitted. - Failure still fails closed — an unresolvable classifier reports
unknown, never green.
Rough size: small-to-medium. The mechanism is understood and the fix is a resolution step rather
than new policy.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Projects
- StatusShow more project fields✅ Done
Activity
devantler commentedon Sep 6, 2026
Independent corroboration from the observation plane, with two facts this issue does not yet carry
Mined from the Claude lane's own transcripts (24h to 2026-09-06T14:00Z; 79 files, 296
is_errortool results). This is a separate observation of the condition, not a re-reading of the issue.1. The caller does not fail once — it hunts, and every root is denied
One surveyor dispatch (02:27:52Z → 02:36:07Z) issued 15 invocations of the classifier across 5 distinct roots, and all 15 were denied
a read must begin with a forge command:.claude/scripts/~/.claude/plugins/marketplaces/devantler-plugins/plugins/agentic-engineering/scripts/~/.claude/plugins/cache/devantler-plugins/agentic-engineering/scripts/~/.claude/plugins/repos/devantler-tech/agent-plugins/plugins/agentic-engineering/scripts/libraries/agent-pluginssubmoduleSo the cost is not one denial per dispatch — it is a ~8-minute search that ends in no signal. Note the third row: it reached the right cache tree and failed only on the missing version segment, which is the part that cannot be written as a stable literal.
2. The admitted path is undiscoverable through the guard, which is what makes this a trap rather than a gap
classify_segmentadmits at position 0 onlygh,git, or the literal$DEFAULT_BRANCH_CLASSIFIER. Every command that could locate the script is therefore denied by the same guard — the same 24h window containsdeny: a read must begin with a forge command, not 'find'and... not 'grep'. The surveyor cannot be told to look it up, because looking it up is not an admitted read.This is why prose alone cannot close it: any instruction that ends in "resolve the installed path" is unsatisfiable from inside the guard.
3. A prescription added since this issue was filed makes the placeholder explicit
4.4.29§4 now reads:It correctly enumerates what is forbidden (bare basename,
PATHlookup, relative../scripts/) while the one admitted spelling still contains<installed plugin>, a value the surveyor is never given. The agent file's own markdown link is the relative../scripts/form the same paragraph rules out. That change fixed a real, separate defect (positional argv) and did not, and could not, fix this one.Design constraint worth pinning in the acceptance criteria
A bare basename or
PATH-resolved form must stay denied — it would let an attacker-placedclassify-default-branch-ci-runs.shearlier onPATHor in the cwd execute under the surveyor's credentials. That exact widening was proposed and rejected in review on agent-plugins#197, so the fix needs to make the real path reachable rather than make more spellings admissible.No change proposed here; this issue already owns the fix. Recording the measurement so its size and root cause are on the record.
chore: agent-claim #200 nonce=5dea1a074a3c8febe764660233c7ba5b
chore: agent-claim #200 nonce=8de2093b1349d60f6ee1b61657827fae
devantler commentedon Sep 6, 2026
Delivered in #205, merged as 6c62e1f. The merged tree exactly matches reviewed head 966c942.
A denied bare classifier probe now returns a JSON-encoded path to the guard's executable sibling. The surveyor decodes it as data and retries through the same guard; missing or unusable discovery remains QUERY-UNKNOWN. Offline input and arbitrary executable paths remain denied.
Validation passed 393 guard cases, 35 adapter cases, 18 classifier cases, 202 manifest cases, ShellCheck, desired-state digests, version parity, and bundled-skill provenance. Live evaluation used the real adapter, guard, and classifier to read this repository's CI successfully. Codex completed a clean review of the merged source head after CodeRabbit's current-head request was rate limited.
The plugin manifests now carry agentic-engineering 5.0.1. Consumer pins and runtime installs have their own rollout lifecycle; this change does not report them as upgraded.
Post-merge verification: CI run 34046952040 completed successfully on the exact merged commit. The issue is closed and its board item is Done.