Agent capabilities
Maintain Pulp's installed design-time agent capability manifest and public-surface ledger. Use when adding, removing, renaming, or materially changing public audio, MIDI, signal, timebase, or sequence APIs; registering a new algorithm for generators; changing capability support or deprecation state; or repairing agent-capabilities freshness, schema, fingerprint, tombstone, or installed-SDK tests.From its SKILL.md
npx -y skills add Generous-Corp/pulp --skill agent-capabilitiesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 14 stars14 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
29.0 KB, as published. Nobody here has run it
Agent Capabilities
Maintain three related artifacts:
agent-capabilities.jsonis the installed consumer contract: curated keys, versions, digests, status, evolution, typed C++ bindings, and partial-coverage semantics.agent-capability-surface.jsonis the maintenance ledger: every public header in the covered roots, its byte fingerprint, and its reviewed disposition.tools/agent-capabilities/contract-history.jsonis the repository-only, append-only evolution history checked against the protected Git tip. Shallow GitHub Actions checkouts fetch the immutable event base SHA when necessary.
The consumer manifest, its schema, and the handoff schema install into the SDK.
Official release packaging stamps agent-capability-handoff.json only after
installation; it binds the exact SDK source SHA and platform to the installed
importer's SHA-256 plus the installed manifest's exact content and byte hash.
The release archive verifier must require and revalidate that identity at the
configured capability-handoff floor. The surface ledger, surface schema, legacy
baseline, and contract history are maintenance artifacts and must not be
installed.
When adding an installed SDK library in PulpInstallRules.cmake, register its
archive stem in release_product_matrix.json and classify every newly covered
public header here in the same change. A successful CMake export alone does not
prove the release archive or installed agent-capability contract is complete.
Keep both separate from the unified runtime control platform. This contract may describe what an SDK can design or generate; it must never contain runtime operations, grants, policy, risk decisions, instances, activation, sessions, revocation, or receipts.
The installed SDK also ships canonical runtime-control headers, CMake helpers,
and control-authoring examples. Their presence in the same install tree does
not make them agent-capability rows: keep runtime-control operations and policy
out of agent-capabilities.json, and keep the capability surface ledger focused
on the design-time public-header contract.
Classify the change
For a new public header or symbol:
- Decide whether it is a generator-facing capability. Do not infer this from a plausible class name.
- If yes, add an explicit row to
EXPORTSin the domain-appropriatetools/scripts/agent_capability_catalog_*.pymodule, add typed bindings for every advertised entrypoint/operation, and record the current header fingerprint. Add a nonempty_link_probethat constructs or invokes the real API rather than merely takingsizeof, and start a new key at contract version1.0.agent_capability_manifest.pyassembles those catalogs; do not put capability rows back into that orchestrator. - If no, add a reviewed header classification to
tools/scripts/agent_capability_registry.pyinstead:capability_support,infrastructure, orunsupported_capability. Give a durable rationale. Never grow the frozenlegacy_unreviewedbaseline. - Increase
SURFACE_INVENTORY_VERSIONfor any ledger change. IncreaseMANIFEST_REVISIONwhenever the installed manifest changes.
For overloaded C++ free functions, keep the public qualified_name as the real
API name and give each overload a distinct binding role. Use private generator
metadata for an explicit static_cast address expression so the generated
compile fixture proves the intended signature without leaking fixture syntax
into the installed contract. Each overload still needs its own operational
probe with arguments that select and invoke that overload.
If generation reports that a new public header is unclassified or has no
covered public target owner, do not retry --write or add a blanket exception.
Add the curated capability or reviewed disposition first, register the exact
minimal owner in REVIEWED_MINIMAL_TARGETS, and then regenerate. A capability
binding alone cannot establish which installed CMake target owns its header.
For a non-static member-function binding, keep the public qualified_name as
the real class-qualified method, provide an exact pointer-to-member
address_expression, and use an explicit-object member_function_call probe.
The generator must retain address references with auto volatile; auto *
cannot represent a pointer-to-member. The installed-SDK suite runs the matching
operational probe independently for every binding, not only in the aggregate
capability consumer.
A new TSP algorithm is therefore detected automatically but not advertised by guesswork: the new/changed public header fails the ledger gate until its owner makes the explicit registration or non-capability classification.
For a fixed-capacity record algebra such as music.pattern-development, bind
the stable record, error, configuration, and result types as well as every
advertised free function. Each free function needs its own operational probe;
a type-only row or one aggregate probe cannot establish that installed
consumers can execute density, fill, set-algebra, ID, and morph operations.
Keep scheduling, clocks, note ownership, and publication outside this manifest.
For bounded sampled-target FIR design, register the public
pulp/signal/fir_design.hpp entry point as signal.fir-design, keep it
offline-only, and have the generated compile fixture invoke an empty-target
request in addition to taking the exact function pointer. This preserves the
contract's proof that the published binding is operational rather than merely
type-visible.
For an existing capability change:
- Update the reviewed header fingerprint for every public-header byte change, even when the consumer contract is unchanged. Increase the surface inventory version.
- Increase the capability minor version for compatible additive contract changes.
- Increase the capability major version when a binding is removed, renamed, or replaced, or when lifecycle, RT, state, seed, domain, units, latency, tail, or scheduling semantics change incompatibly.
- Keep
seed_modelanddeterminismseparate. Whendeterminism-contract-v1is required, every live row must declare repeatability, block-partition behavior, platform scope, and whether transport history is an input. Strengthening a determinism promise is additive; weakening or removing one requires a major increase or a new successor key. - Treat a minor-0 row with no
determinismas unspecified. Consumers that require determinism must reject it and must reject unknown required features. - Leave the capability version unchanged for summary-only wording. The generated digest excludes the summary but covers the material contract.
- Keep numeric parameter ranges/defaults/choices in
forge-catalog.json; use aforge_descriptorreference instead of copying them.
For removal:
- First publish the live capability as
status: deprecatedwith a matching deprecated evolution state and ordered lifecycle versions. A capability that is active in the protected base may not be removed in the current change. - In a later published revision, add a
status: removedcapability tombstone with its introduction/deprecation versions, last version, and digest before deleting the row. Never reuse a tombstoned key. - Add a surface tombstone with the immediately prior reviewed header fingerprint before deleting a covered public header.
- Increase the manifest and surface inventory revisions as applicable.
- Preserve tombstones permanently; do not rewrite history. Replacement keys must exist, may not reference themselves, and may not form cycles.
Interpret support honestly
The published capability table now includes bounded, versioned registered clip content and trusted note-renderer hooks. Keep that row aligned with the compile contract: note output/reset state only, a 4096-note fragment cap, and explicit refusal of trimmed nesting and nondefault-production wire serialization.
- A live capability row's
statusis the explicit support claim. Use onlystable,usable,experimental,partial,unsupported, ordeprecated; never publish planned work. - A header classified
unsupported_capabilityis an explicit negative claim. legacy_unreviewedmeans only that no machine-readable claim has been reviewed yet.- Coverage is currently
partial, so an absent key means unknown, not unsupported.
Reuse the spectral-mask processor
signal.spectral-mask-processor is the shared streaming STFT/WOLA layer for
products that apply authored spectral gain tables. Installed-SDK consumers
include <pulp/signal/spectral_mask_processor.hpp> and link Pulp::signal.
Prepare it off the audio thread, publish layouts or compiled tables from a
control thread, and call process() or process_frame() on the audio thread.
The processor owns frame-boundary table adoption, gain interpolation,
overlap-add reconstruction, latency reporting, and latency-aligned dry/wet.
Use categorical mask entries for true mute: a muted bin is multiplied by exact zero, not represented by a finite decibel floor. Reuse this processor for zoomable filter banks, spectral gates, freezes, morphing, and related products instead of rebuilding an application-local STFT lifecycle or publication protocol. Analyzer snapshots and captured-frame storage are separate layers; do not infer them from this capability or duplicate them inside the processor.
Before presenting every authored band as independently controllable, call
analyze_spectral_band_resolution() with the product's layout, sample rate,
and FFT size. Its fixed-capacity report counts directly owned viewport bins per
band and excludes exterior edge-band extension. fully_represented() == false
means the UI or profile selector must disclose the resolution limit, select a
higher supported geometry, or use a different filter architecture; zoom alone
cannot create additional FFT bins.
Reuse the realtime visualization bridge
pulp::view::VisualizationBridge is the shared realtime-safe audio-to-UI tap
for spectrum, waveform, and meter consumers. Configure it while fully
quiescent, call process() from the audio callback, and give exactly one UI
thread ownership of poll() plus the snapshot reads. The callback path only
meters and copies into fixed SPSC storage; FFT and waveform assembly happen in
the bounded, non-realtime poll() call.
read_spectrum() and read_waveform() remain cheap snapshot reads for source
compatibility. They do not analyze newly captured audio. A consumer that needs
fresh data must schedule poll() first, then read or use the explicit
peek_spectrum() / peek_waveform() aliases. Treat capture overflow, rejected
channel topology, and positive-length missing-channel callbacks as continuity
breaks: the bridge advances its epoch and never joins audio across the gap.
Keep configure() and reset() quiescent; neither is concurrent with the
audio producer or UI consumer.
Regenerate and validate
Do not use the bootstrap or unpublished-migration switches during normal work.
When the base branch has already advanced either counter, recompute the next
MANIFEST_REVISION and SURFACE_INVENTORY_VERSION from that exact base before
running --write; replaying stale projection counters can silently reuse an
already-published contract identity.
Use Python 3.10 or newer: transactional generation uses modern standard-library
APIs such as zip(..., strict=True), so an older system python3 can fail before
validating the contract. Run installed-SDK capability tests from a Release build.
A Debug/coverage build is an invalid positive control because the installed SDK
guard intentionally refuses unacknowledged Debug SDK consumers. Also keep the
build tree free of stale nested SDK install prefixes: archive-mutation checks
require one owning build-tree library per target, and an old consumer-smoke
prefix/lib can create a false duplicate-owner failure. Move such generated
fixtures aside and rerun the same test before changing capability code or
weakening the archive check.
Run:
python3 tools/scripts/agent_capability_manifest.py --write
python3 tools/scripts/agent_capability_manifest.py --check
python3 tools/scripts/test_agent_capability_manifest.py
cmake --build build --config Release --target pulp-test-agent-capability-compile
ctest --test-dir build -C Release -R '^agent-capability-' --output-on-failure
The installed-SDK test must install to an isolated prefix, verify maintenance artifacts are absent, read the installed schema and manifest, and independently compile/link/run every capability and every typed binding against only its declared minimal target. It must reject wrong-target declarations and checkout- path leakage, and use configuration-aware build/install and executable paths. When checking CMake File API include paths, permit paths outside the install prefix only when CMake marks them as system includes; transitive platform and third-party headers may be legitimate, but non-system source/build leakage is still a failure. When finding build-tree archives for mutation controls, exclude the staged install prefix because retrying the test leaves installed archives under the build directory. The official-SDK handoff self-test separately covers exact identity plus wrong source SHA, importer hash, capability hash, and schema-invalid documents.
test/cmake/quality_tests.cmake is also the registry for capability-manifest
and adjacent policy self-tests. When adding a new Python policy test there,
register the test explicitly in the same change; merely creating a
tools/ci/test_*.py file does not make CTest execute it.
The surface fingerprint is intentionally conservative SHA-256 over full header bytes. Do not weaken it with regex symbol extraction. A future pinned-Clang AST inventory may reduce comment/private-detail churn only if its version and toolchain are pinned and mutation tests retain add/remove/change detection.
Because the fingerprint covers full bytes, editing only comments in a
capability header is a surface change and --write will refuse it twice
before it succeeds. The declared fingerprint lives in the catalog source, not
just the generated JSON, so the order is: edit the header, then replace every
occurrence of the old digest in the owning agent_capability_catalog_*.py
(one per binding, so a single header can hold a dozen copies), then raise
MANIFEST_REVISION and SURFACE_INVENTORY_VERSION, then run --write once.
Derive both counters from the CURRENT protected base every time. A capability transaction can land while yours waits in the merge queue, which takes the numbers you reserved and leaves your branch conflicting on exactly those two constant lines. Re-read them from main and regenerate rather than resolving that conflict by hand.
Do that with the tool, not by hand:
python3 tools/scripts/agent_capability_rederive.py --print # read-only: what would change
python3 tools/scripts/agent_capability_rederive.py # rewrite both, then --write
It resolves the same protected tip --check uses, compares this tree's
generated material against the base's with the counter excluded, and moves each
counter only when its own material actually moved. That asymmetry is the part
worth not hand-rolling: over-bumping is not the safe direction. Advancing a
counter whose material is identical fails the opposite rule,
... changed without a manifest change, so "bump both to be safe" trades one
red gate for another.
It refuses rather than guesses when the surface has unresolved problems — a changed header with a stale fingerprint has no stable material to derive from, and its fingerprints must be refreshed first. Counters are decided LAST.
Re-running it is idempotent: the counter a tree currently holds is not evidence of anything, so a tree already carrying a stale reservation is derived back down to what its material justifies.
Commit the merge before you run it. During an uncommitted merge the incoming
tip is not yet an ancestor of HEAD, so base resolution steps back to the merge
base — a commit that predates both sides — and the counter derived from it is
stale while looking entirely plausible. Observed live: it read base 28/45 while
main already held 30/47. The tool now refuses in that state rather than
answering the wrong question, so the sequence when resolving a conflict is:
resolve → commit the merge → refresh fingerprints → re-derive → --write.
Regenerate exactly once from final header bytes. Each --write appends a full
entry to contract-history.json, so editing the header again after a
successful --write leaves two entries for one logical change.
--write is not idempotent, so never use it to verify a transaction. A
second --write on an unchanged tree still appends a second history entry, and
it reports the same cheerful wrote ... line either way — so the check reads as
confirmation while it is the thing creating the defect. The reviewer then sees
capability history is not append-only relative to the protected base for a
transaction that was correct until it was checked. Verify with --check, which
writes nothing and answers the same question (fresh; N keys and M public headers checked). If a verification --write already ran, do not try to prune
the entry by hand: reset the four generated artifacts to the protected base and
regenerate once.
Prove the append is single rather than assuming it. git diff --numstat showing
zero deletions is necessary but not sufficient — two appends are also
deletion-free. Compare entry counts:
python3 -c "
import json,subprocess
c=json.load(open('tools/agent-capabilities/contract-history.json'))['entries']
b=json.loads(subprocess.run(['git','show','origin/main:tools/agent-capabilities/contract-history.json'],
capture_output=True,text=True,check=True).stdout)['entries']
print('delta', len(c)-len(b), 'prefix-identical', c[:len(b)]==b)"
--check's diagnostics go to stderr, not stdout. A script that scrapes the
expected/got fingerprint pairs from captured stdout alone silently sees nothing
and reports success.
Publishing a capability on a NEW public header takes four coordinated edits
--write validates one precondition at a time and stops at the first failure,
so a new-header capability surfaces as four unrelated-looking errors in
sequence rather than one checklist. Make all four edits before running it:
- Catalog entry — a
capability(...)block inagent_capability_catalog_<domain>.py. - Registry header ownership —
"pulp/<domain>/<new>.hpp": "Pulp::<domain>",inagent_capability_registry.py. Without it the error isbindings[N] include has no covered public target owner, which names the binding rather than the missing map entry — the message points away from the fix. - Umbrella fingerprint — adding the include to
<domain>.hppchanges that umbrella's digest too. Update the declared value to thegot sha256:the error reports. The stale digest also appears incontract-history.json; do not edit those — history is append-only. - Both counters —
MANIFEST_REVISIONandSURFACE_INVENTORY_VERSION, reported as two separate errors.
header_fingerprint is not the SHA-256 of the header file
The declared value and sha256sum <header> legitimately differ. Do not
"reconcile" them by hashing the file — take the got sha256: the generator
reports.
Adding a function to an existing capability header costs NO contract bump
A binding's identity is (role, kind, include, qualified_name, target, availability). header_fingerprint is not a component, and it does not
appear in the capability row at all — it lives only in the surface document,
versioned on its own SURFACE_INVENTORY_VERSION axis. So a pure header-bytes
change is a surface-axis event that is invisible to every capability contract
payload.
Two agents independently reasoned "fingerprint ∈ bindings ∈ contract_payload,
therefore this needs a version bump," each having verified _binding_identity
(which legitimately excludes the fingerprint) and let that stand in for
checking the snapshot's actual binding shape one level down. The generator
rejected it with contract_version changed without a contract change. If you
are adding a function to a header that already backs a capability, expect the
existing key to stay at its current version and the change to be absorbed by
the two counters.
A STALE verdict on a tree you did not touch is a base problem, not a you problem
If --check reports these on a clean checkout whose diff touches no capability
file, stop and look at the base before touching anything:
agent-capabilities: STALE: capability history is not append-only relative to the protected base
agent-capabilities: STALE: manifest changed without a manifest_revision increase
agent-capabilities: STALE: public surface changed without an inventory_version increase
The check is not wrong — it is answering correctly against the wrong reference. The protected base must be an ancestor of the commit under validation, or "append-only relative to the base" is ill-posed: measured against a tip that carries commits your branch does not have, every correct branch looks non-append-only.
Naming a moving ref makes that routine. The build hosts run ~134 worktrees off
one shared .git, so a git fetch in any sibling advances origin/main
for all of them — mid-validation included, with your session issuing no fetch.
_resolve_local_base therefore checks ancestry and steps back to the merge-base
when the ref has moved past you. The merge-base does not move when the tip
advances, which is what makes the verdict reproducible.
Two consequences worth knowing:
- Re-running does not help. The tip keeps moving, so the failure reproduces and reads like a real defect. It is the failure mode most likely to send you editing correct generated files.
- "Don't fetch during a validation" cannot fix it and is not the rule. The
ref was observed moving with the validating session issuing no fetch at all;
discipline is blind to peers sharing the
.git.
To pin the base explicitly — for a bisect, or to reproduce a CI verdict exactly:
PULP_AGENT_CAPABILITY_BASE_REF=<sha> python3 tools/scripts/agent_capability_manifest.py --check
That path is deliberately literal: an explicit ref is used as given, without the ancestry fallback.
gates.sh does NOT run the capability check — adding a public header passes pre-push and fails in CI
The pre-push gates cover skill-sync, version-bump, compat, deps and friends. They do not
run agent_capability_manifest.py --check. So a change that adds a header under a covered
root, or edits an existing one, sails through gates.sh: all gates pass and then fails CI on
agent-capability-manifest-check / -selftest.
Adding one new DSP header produces two failures, not one:
unclassified public header: pulp/signal/<new>.hpp
public header fingerprint changed: pulp/signal/signal.hpp <- the umbrella include
The umbrella one is the easiest to miss: adding #include <pulp/signal/foo.hpp> to
signal.hpp changes that header's bytes too.
Run python3 tools/scripts/agent_capability_manifest.py --check yourself before pushing any
change under core/*/include/. Gates passing is not evidence here.
Classify honestly: a reusable bounded surface that is not an advertised generator claim takes
infrastructure with empty capability_keys and a durable rationale. Do not manufacture a
capability row to clear the gate; a capability row is a consumer contract with typed bindings
and operational probes.
A header in the frozen legacy baseline does NOT require unfreezing anything.
tools/agent-capabilities/legacy-unreviewed-baseline.json snapshots the public headers that
predate classification, guarded by FROZEN_LEGACY_DIGEST and FROZEN_LEGACY_COUNT in
tools/scripts/agent_capability_surface.py. Changing one fails with public header fingerprint changed, which reads like it demands editing those pinned constants — it does not, and editing
them to make one PR pass would be removing a deliberate guard. Declaring the header in
agent_capability_registry.py, as a capability or as an infrastructure disposition, satisfies
the fingerprint check on its own; the baseline file, its entry count, and its digest all stay
untouched. Confirm afterwards that the baseline entry count is unchanged and the manifest
self-tests still pass.
Ask the prior question first: does your capability actually need that header to change? Both this section and the recorded precedents jump straight to how to classify a frozen header, which quietly assumes the edit to it is load-bearing. Often it is not. A cell that adds a new processor and, while it is in there, refactors three existing processors onto a shared kernel will trip this gate on headers its capability never touches — and the whole gate disappears if the refactor is dropped.
So triage the edit before classifying the header:
- Incidental to the capability (a dedup, a comment, a rename, an ordering tidy) — revert it and shrink the cell. Nothing about the new capability depends on it, the frozen baseline stays out of the change entirely, and the diff shrinks to what a reviewer can actually check.
- Required by the capability — declare the header. The recorded example is
frequency_response.hpptemplated overSampleTypeso the_64variants could compute a response without narrowing: reverting it would have shipped those variants with no response inspection, so there was no smaller correct slice.
The asymmetry is what makes this worth doing in that order. Reverting an incidental edit costs
one git checkout origin/main -- <header>. Declaring a header is permanent: it converts an
unreviewed legacy header into a reviewed contract the repo then owns, decided as a side effect
of a cleanup rather than on its own merits. If the dedup is worth having, it is worth its own
change, where the classification is the subject of review instead of collateral.
When you do revert, prune whatever the reverted call sites were the only users of. A shared helper introduced for three call sites that no longer exist is dead public surface, and it enlarges the very fingerprint you are trying to keep small.
test_signal_no_exceptions.cpp is a shared ledger — three hazards, not two
Nearly every signal capability appends to it, and it assigns a unique non-zero exit code per checked capability so each failure is identifiable from the status alone. Cells prepared in parallel therefore collide in it constantly. Two hazards are well known; the third is not, and it defeats the checks for the other two.
-
Duplicate exit codes. Two cells both append above the same remembered maximum and take the same numbers. Compiles fine, both pass in isolation, and two distinct failures become indistinguishable. Only a duplicate-code scan finds it.
-
Duplicate capability blocks. Rebuilding the file from main's canonical version and appending your block is the right resolution, but a cell with more than one commit touching the file re-adds what the resolution already appended. This one is usually loud (
redefinition of ...). -
Two statements sharing a line. A resolution can leave a declaration on the same line as the preceding
return:if (!(formants.configure(recipe) == FormantConfigureStatus::configured)) return 29; pulp::signal::ParallelDynamicsMixer parallel_dynamics; if (!parallel_dynamics.prepare(8u, 16u)) return 31;This compiles, and it is reachable — the
ifbody is only the return, so the declaration still executes — so no test can see it and neither the duplicate-code nor the duplicate-block check fires. Both of those are line-oriented, and two statements on one line is precisely the shape that slips past them. Observed on a published PR in 2026-08.It is not a correctness bug today, which is why it survives review; it is a latent one. The same shape with a guarded early return (
if (cond) return N; Type x;inside a block that can be taken) silently skips the declaration and every check after it, and the capability's proof quietly stops running while the binary still exits 0.
So do not hand-resolve this file. Rebuild it from git show origin/main: and re-append your
block programmatically, which removes all three by construction — that is how the line-sharing
defect above was removed, as a side effect of the correct procedure rather than by spotting it.
Then verify all three: no non-zero code repeats, each capability block appears exactly once, and
no line carries two statements.
Extract codes from every return form, including ternaries (return c ? 0 : N;) — a naive
grep -oE "return [0-9]+;" misses those and manufactures phantom collisions.