Skip to content

The surveyor cannot resolve the default-branch CI classifier's admitted path, so rung 0 is blind on every repository #200

Closed

Description

@devantler

🤖 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 than unknown, on a checkout where the classifier is installed.
  • The guard is unchanged in strength: offline --input stays 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.

Activity

  1. added theissue type on Sep 6, 2026
  2. moved this to 📥 Backlog in 🌊 Project Boardon Sep 6, 2026
  3. devantler commented on Sep 6, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agent Improver

    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_error tool 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:

    attempts root tried
    4 the consumer worktree's .claude/scripts/
    8 ~/.claude/plugins/marketplaces/devantler-plugins/plugins/agentic-engineering/scripts/
    1 ~/.claude/plugins/cache/devantler-plugins/agentic-engineering/scripts/
    1 ~/.claude/plugins/repos/devantler-tech/agent-plugins/plugins/agentic-engineering/scripts/
    1 the libraries/agent-plugins submodule

    So 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_segment admits at position 0 only gh, 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 contains deny: 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:

    <installed plugin>/scripts/classify-default-branch-ci-runs.sh --repo OWNER/REPO --branch BRANCH --head-sha FULL_SHA

    It correctly enumerates what is forbidden (bare basename, PATH lookup, 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-placed classify-default-branch-ci-runs.sh earlier on PATH or 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.

  4. self-assigned this
    on Sep 6, 2026
  5. added a commit that references this issue on Sep 6, 2026
  6. moved this from 📥 Backlog to 🏃🏻‍♂️ In Progress in 🌊 Project Boardon Sep 6, 2026
  7. added a commit that references this issue on Sep 6, 2026
  8. moved this from 🏃🏻‍♂️ In Progress to 👀 In Review in 🌊 Project Boardon Sep 6, 2026
  9. moved this from 👀 In Review to ✅ Done in 🌊 Project Boardon Sep 6, 2026
  10. devantler commented on Sep 6, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

None yet

Effort

None yet

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions