Skip to content

feat(autonomy): deterministic prerequisite resolver (#2724) - #2809

Merged
kyle-sexton merged 3 commits into
mainfrom
cursor/2724-prereq-resolver-14fb
Aug 16, 2026
Merged

feat(autonomy): deterministic prerequisite resolver (#2724)#2809
kyle-sexton merged 3 commits into
mainfrom
cursor/2724-prereq-resolver-14fb

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #2724

Summary

Replacement for #2796 (auto-closed when stack base was deleted after #2793 squash-merge). Lands the deterministic prerequisite resolver on main after #2793.

Closes #2724

Summary

Phase 4 of the routine-capability-detection plan (ADR 0011): deterministic prerequisite resolver that reads the Phase 3 emission and emits per-identity verdicts with provenance on a named scheduling surface.

Fix

  • Resolver skills/setup/scripts/resolve-prerequisites.mjs — repo-file + harness-context probes; composes binding declarations, ecosystems (resolved), tracker seam, .mcp.json presence/enablement.
  • Precedence — declaration narrows; ran-negative probe caps declarations (finding); unprobeable ≠ absent.
  • Seven fixtures under scripts/fixtures/prerequisite-resolution/ + co-located test/manifest (bare-repo, fail-closed, declared-absent-narrows, probe-negative-caps, probe-could-not-run, posture-divergence, positive-verdict).
  • Liveness — engine health-check taxonomy row; fail-loud; wall-clock-free (byte-identical consecutive runs).
  • Version bump to 0.21.0.

Stacked on #2793 (cursor/2723-prereq-emit-8f91).

Verification

$ bash plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.sh
$ bash scripts/check-changelog-parity.sh --check-bump origin/main
$ bash scripts/check-orphaned-fixtures.sh --check
$ bash scripts/check-changed-skills.sh origin/main

Related

Related

@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 41s —— View job


Security review (/review:security-review)

  • Gather context (PR diff, changed files, custom instructions)
  • Invoke /review:security-review skill and follow its instructions
  • Post findings / summary

Scope: Reviewed only the files changed in this PR against origin/main. The
diff is almost entirely new: a 927-line deterministic resolver
(resolve-prerequisites.mjs), its fixtures/tests, and version/changelog bumps.
The resolver is a pure, read-only, wall-clock-free CLI script — no network I/O,
no eval, no process spawning in production code, no config writes.

Findings: 1 SUGGESTION, no CRITICAL/IMPORTANT issues.

Severity File Summary
SUGGESTION resolve-prerequisites.mjs#L220-L242 probeTracker joins the repo-controlled provider field (from .work-item-tracker.json) into a filesystem path without validating it, so a provider value containing ../ segments can make capsPath resolve outside repoRoot (path.join collapses ..). Impact is limited — it's only a boolean existence probe (statSync, no content is read/returned) and requires the invoker to already control repo content — but since this resolver composes signals for a fail-closed autonomy/guardrail verdict (ADR 0011), constraining provider to a safe identifier pattern before use is worth doing. Posted inline with a suggested fix.

Reviewed and found sound (no issues):

  • Declaration precedence logic (resolveNeed / aggregateVerdict, resolve-prerequisites.mjs#L747-L833): a binding.json declaration alone cannot force a "supported" verdict — a positive declaration only reaches "present" when an independent probe also ran and agreed; otherwise it degrades to "conditional". Negative/disabled declarations narrow but never widen. This matches the "declaration narrows; probe caps declarations" precedence the PR describes, and correctly stays fail-closed.
  • No command execution, eval, template/SQL construction, or credential handling anywhere in the diff.
  • Test harness (resolve-prerequisites.fixtures.test.mjs) uses spawnSync with an argument array (no shell), so no injection risk there.
  • Version bump / CHANGELOG / README changes are documentation-only.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26d9bce085

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs
Comment thread plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs
Comment thread plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs
Comment thread plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs
Comment thread plugins/autonomy/skills/setup/scripts/resolve-prerequisites.mjs
@github-actions

Copy link
Copy Markdown

Last security-reviewed head: 26d9bce0856e8244bd10aaaa250960b0c3f9d8ff. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

cursoragent and others added 3 commits August 16, 2026 01:12
Closes #2724

Phase 4 of the routine-capability-detection plan (ADR 0011): resolve
per-identity verdicts on a named surface from the generated emission,
with precedence fixtures and wall-clock-free output.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/2724-prereq-resolver-14fb branch from 26d9bce to 42f99f7 Compare August 16, 2026 01:13
@kyle-sexton
kyle-sexton merged commit e37130a into main Aug 16, 2026
44 checks passed
@kyle-sexton
kyle-sexton deleted the cursor/2724-prereq-resolver-14fb branch August 16, 2026 01:20
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 6m 43s —— View job


Code Review (/review:code-review)

  • Gather context (PR diff, changed files, custom instructions)
  • Invoke /review:code-review skill and follow its instructions
  • Post findings / summary

Scope: CI code-review lane — correctness and maintainability only. Security is
scoped to the dedicated /review:security-review lane, which already ran on this
PR and flagged one path-traversal SUGGESTION in probeTracker (unrelated to the
findings below, left to that lane). Reviewed the full diff against origin/main
(31 files: the 927-line resolve-prerequisites.mjs, its fixtures/tests,
CHANGELOG/README/version-bump housekeeping).

I confirmed the resolver's blob content at this head (a0954ce9…, per the diff
index line) is byte-identical to what the existing Codex review already
examined, so I independently re-verified each of Codex's four correctness
findings against the code and the generated/identity-prerequisites.json
emission rather than taking them at face value. All four hold up; I posted them
as inline comments (corroborating, not duplicating, the existing threads) since
this is the code-review lane's first pass on this head:

Line Finding
resolve-prerequisites.mjs#L839 resolveIdentity never reads record.isolation_floor from the emission (verified present on every record, e.g. L2/L3) — verdicts are computed purely from needs, so an identity can resolve supported without the resolver ever checking whether the bound surface established the required isolation floor.
resolve-prerequisites.mjs#L150 loadBinding collapses readJson's "unreadable" (malformed/corrupt binding.json) and "absent" (missing file) into the same no-binding fallback, silently dropping narrowing declarations on a parse failure rather than fail-loud/unresolvable.
resolve-prerequisites.mjs#L673 probeMergePath's mergeCapable check accepts recorded.class === "temporal" on its own, but temporal denotes scheduling transport (per reference/trigger-dispatch.md), not merge capability — an advisory-only temporal surface satisfies merge_path regardless of merge_policy_capable.
resolve-prerequisites.mjs#L453 probeSourceTree's fallback excludes only 3 doc filenames, so package.json/lockfiles/other manifests (already enumerated in this file's own DEPENDENCY_MANIFESTS) count as a source tree — verified tech-debt-sweep depends on source_tree as a need distinct from dependency_manifests/ecosystems, so this is a real false-positive path.

All four were already raised by the Codex review and marked "Deferred follow-up.
Resolving to unblock merge." by @kyle-sexton — I'm not asking to block on them,
just confirming (with independent verification against the emission schema and
trigger-dispatch.md) that they remain live in the code and worth tracking to
resolution given this resolver composes fail-closed guardrail signals.

Reviewed and found sound, no new findings:

  • Declaration-vs-probe precedence (resolveNeed/aggregateVerdict) — matches
    the documented "declaration narrows; probe caps declarations;
    unresolvable ≠ absent" contract across all seven fixtures.
  • Fixture/test harness (self-policing manifest coverage, byte-identical
    reproducibility check, spawnSync with an argument array) is correct and
    matches the stated wall-clock-free guarantee.
  • Version bump / CHANGELOG / README changes are consistent and accurate.

kyle-sexton added a commit that referenced this pull request Aug 16, 2026
) (#2813)

Closes #2725

## Summary

Re-lands the autonomy setup-skill prerequisite-resolution slice (0.22.0)
on main after #2809. Replaces #2804, which was auto-closed when its
stack base was deleted on squash-merge.

## Related

- Completes the autonomy stack after #2793 / #2809.
- Supersedes closed #2804.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
const signals = [];
const findings = [];
for (const need of record.needs) {
const resolved = resolveNeed(need, record.identity, ctx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdicts aren't gated on the emitted isolation_floor.

resolveIdentity only iterates record.needs; it never reads record.isolation_floor (present on every emission record, e.g. L2/L3 in generated/identity-prerequisites.json) or checks it against anything the bound surface actually established. So an identity can resolve supported purely from its needs list even when the scheduling surface never established the isolation floor the identity's class requires — e.g. the positive-verdict fixture resolves dependency-update-wave/mechanical as supported without the resolver ever consulting isolation posture.

If isolation-floor conformance is fully re-derived from the needs list per posture (so this is redundant rather than missing), a one-line comment saying so would help the next reader; otherwise this is a real fail-closed gap for a resolver whose stated purpose (ADR 0011) is composing guardrail-relevant signals.

This corroborates the existing (deferred) finding from the Codex review on this same line.

[Fix this →](https://claude.ai/code?q=In%20plugins%2Fautonomy%2Fskills%2Fsetup%2Fscripts%2Fresolve-prerequisites.mjs%2C%20resolveIdentity()%20(around%20line%20831-843)%20only%20evaluates%20record.needs%20and%20never%20consults%20record.isolation_floor%20from%20the%20Phase%203%20emission%2C%20nor%20the%20bound%20surface%27s%20established%20isolation%20posture.%20Add%20a%20check%20that%20compares%20the%20identity%27s%20required%20isolation_floor%20against%20what%20the%20surface%27s%20binding%20actually%20establishes%2C%20and%20cap%20the%20verdict%20(e.g.%20to%20unsupported%2Funknown)%20when%20it%20is%20not%20met%2C%20or%20document%20why%20this%20is%20intentionally%20out%20of%20scope%20for%20this%20resolver.&repo=melodic-software/claude-code-plugins

const path = join(repoRoot, ".claude", "autonomy", "binding.json");
const result = readJson(path);
if (!result.ok) {
return {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unreadable binding.json is silently treated the same as an absent one.

readJson distinguishes reason: "absent" (ENOENT) from reason: "unreadable" (parse/read failure), but loadBinding collapses both into the same present: false, declarations: [] shape — the caller never sees result.reason. A binding.json that exists but is malformed (truncated write, merge-conflict markers, hand-edit typo) therefore silently loses every declaration it carries, including narrowing absent/disabled declarations, and probes can independently swing the verdict to supported.

For a resolver whose whole premise is fail-closed composition of trust-relevant signals (ADR 0011), a corrupt binding should fail loudly or resolve to unknown/unsupported — not silently degrade to "no binding at all," which is the more permissive of the two states.

This corroborates the existing (deferred) finding from the Codex review on this same line.

[Fix this →](https://claude.ai/code?q=In%20plugins%2Fautonomy%2Fskills%2Fsetup%2Fscripts%2Fresolve-prerequisites.mjs%2C%20loadBinding()%20(around%20line%20142-156)%20treats%20readJson%27s%20reason%3A%3D%3D%3D%22unreadable%22%20(binding.json%20exists%20but%20is%20malformed%2Funreadable)%20identically%20to%20reason%3D%3D%3D%22absent%22%20(file%20missing)%2C%20discarding%20result.reason.%20Distinguish%20the%20two%3A%20either%20throw%2Ffail-loud%20on%20an%20unreadable%20binding%2C%20or%20propagate%20the%20unreadable%20state%20so%20downstream%20verdicts%20resolve%20to%20unknown%2Funsupported%20rather%20than%20silently%20falling%20back%20to%20the%20no-binding-present%20path.&repo=melodic-software/claude-code-plugins

// the surface is the repo-local half of the question.
const mergeCapable =
recorded.merge_policy_capable === true ||
recorded.class === "temporal";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge_path is satisfied by class === "temporal" alone, independent of merge_policy_capable.

const mergeCapable =
  recorded.merge_policy_capable === true ||
  recorded.class === "temporal";

Per reference/trigger-dispatch.md, temporal denotes the scheduling transport ("schedules and poll-fallback detectors for push-less surfaces"), not merge capability — an ordinary advisory-only/enqueue-only surface can just as validly be class: "temporal". Since merge_policy_capable already exists as the explicit signal for this need, the || recorded.class === "temporal" disjunct makes the check pass for any temporal surface regardless of that flag, which defeats the purpose of having it: a repo with a temporal surface bound for advisory-only dispatch would resolve merge_path as present for identities that require an actual merge-capable disposition.

This corroborates the existing (deferred) finding from the Codex review on this same line.

[Fix this →](https://claude.ai/code?q=In%20plugins%2Fautonomy%2Fskills%2Fsetup%2Fscripts%2Fresolve-prerequisites.mjs%2C%20probeMergePath()%20(around%20line%20666-674)%2C%20the%20mergeCapable%20check%20is%20%60recorded.merge_policy_capable%20%3D%3D%3D%20true%20%7C%7C%20recorded.class%20%3D%3D%3D%20%22temporal%22%60.%20Since%20class%3A%20%22temporal%22%20describes%20scheduling%20transport%20(poll%2Fcron)%20and%20not%20merge%20capability%2C%20drop%20the%20%60recorded.class%20%3D%3D%3D%20%22temporal%22%60%20disjunct%20and%20gate%20solely%20on%20merge_policy_capable%20(or%20another%20explicit%20merge-capable%20signal)%2C%20so%20advisory-only%20temporal%20surfaces%20don%27t%20falsely%20satisfy%20merge_path.&repo=melodic-software/claude-code-plugins

(name) =>
!name.startsWith(".") &&
!["README.md", "LICENSE", "CHANGELOG.md"].includes(name),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source_tree fallback treats build manifests as source code.

const codeish = entries.filter(
  (name) =>
    !name.startsWith(".") &&
    !["README.md", "LICENSE", "CHANGELOG.md"].includes(name),
);

This is meant to catch repos without one of the src/lib/app/pkg/cmd convention directories, but the exclusion list only names three docs — package.json, lockfiles (package-lock.json, pnpm-lock.yaml, ...), Cargo.toml, pom.xml, etc. all count as "codeish" here. A manifest-only repo root (package.json + CI + tracker config, no actual source) resolves source_tree as present, which is exactly the false-positive tech-debt-sweep guards against by requiring this need separately from dependency_manifests/ecosystems.

Reusing the DEPENDENCY_MANIFESTS list already defined at the top of this file to extend the exclusion set would close this gap without adding a new dependency.

This corroborates the existing (deferred) finding from the Codex review on this same line.

[Fix this →](https://claude.ai/code?q=In%20plugins%2Fautonomy%2Fskills%2Fsetup%2Fscripts%2Fresolve-prerequisites.mjs%2C%20probeSourceTree()%20(around%20line%20437-453)%2C%20the%20fallback%20%22codeish%22%20filter%20only%20excludes%20README.md%2C%20LICENSE%2C%20and%20CHANGELOG.md%2C%20so%20build%2Fdependency%20manifests%20(package.json%2C%20lockfiles%2C%20Cargo.toml%2C%20pom.xml%2C%20etc%2C%20already%20listed%20in%20the%20file%27s%20DEPENDENCY_MANIFESTS%20constant)%20count%20as%20a%20source%20tree.%20Extend%20the%20exclusion%20set%20to%20also%20exclude%20DEPENDENCY_MANIFESTS%20entries%20(and%20other%20known%20non-source%20root%20files)%20so%20a%20manifest-only%20repo%20doesn%27t%20falsely%20satisfy%20source_tree.&repo=melodic-software/claude-code-plugins

@github-actions

Copy link
Copy Markdown

Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count.

kyle-sexton added a commit that referenced this pull request Aug 16, 2026
…gger (#2822)

## Summary

Reconciles the #2717 naming ruling with the already-shipped
prerequisite-resolution chain. The ruling was posted 2026-08-16T01:54Z —
about three hours **after** PR #2772 merged the contract document
(2026-08-15T22:47Z) and after the downstream phases (#2783, #2809,
#2813) had shipped a working vocabulary. Its premise ("the contract
document may be authored against these") was stale when written, so the
reconciliation decision re-derived the outcome:

- **Verdict tokens ratified as shipped.** `supported` / `conditional` /
`unsupported` / `unknown` clears every constraint that binds — no
security-binding reading, no barred health word, no collision with the
five named incumbents. The ruling's `met` family was a precision
preference, not a defect cure; migrating ~60 sites across four merged
PRs, including machine-emitted resolver tokens, buys no correctness.
**No token changes.**
- **The deferred marker gains its mandatory trigger: `deferred-class` →
`deferred(<trigger>)`.** This half of the ruling names a genuine defect
— a bare marker records that resolution is postponed while discarding
the condition under which the deferral is revisited, the half that makes
it auditable. The trigger is the `join:` row's own catalog Status
trigger. 6 sites, 4 files: the contract document, the README bullet, ADR
0011 (a dated appended amendment — the original decision text is
byte-identical, and the released CHANGELOG entries stay untouched as
historical record), and a new 0.22.1 changelog entry with the version
bump.

A reader who finds the ruling comment and the shipped vocabulary
disagreeing now finds the reconciliation on the record in ADR 0011's
amendment.

## Test plan

- `scripts/check-changelog-parity.sh --check-bump origin/main` exits 0;
`plugin.json` bumped 0.22.0 → 0.22.1 with a matching `## [0.22.1]` entry
- `markdownlint-cli2` and `typos` clean over all five changed files
- `scripts/validate-plugins.sh` passes; `generate-catalog.mjs` /
`generate-cheatsheet.mjs` report no drift
- Repo-wide `git grep deferred-class` at the head commit hits only
historical released changelog text and ADR 0011's original pre-amendment
lines
- Independently verified by a fresh-context auditor reading blobs at the
pushed SHA with authoring rationale withheld: all nine checks PASS (its
one finding — a changelog sentence misattributing the ratification to
the ruling — is fixed in the head commit)

## Related

Closes #2717

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(autonomy): deterministic prerequisite resolver

2 participants