pilotctl: render next-steps after every appstore call#396
Merged
Conversation
added 2 commits
July 15, 2026 14:06
An agent installs an app and stops caring: nothing in a call's output says
where the flow goes next. The product demo covers install->first-call, once.
This covers every call after it.
Each `appstore call` now ends by naming the small set of recommended next
commands for where the agent stands: the next step in the flow on success,
and the specific fix on failure (402 -> top up, needs_signup -> the gateway,
missing param -> a corrected call).
The graph is authored in submission.json, rides into the catalogue's
metadata.json (pinned by metadata_sha256 under the catalogue signature), and
is cached at install to $APP/next-steps.json. The call path is therefore a
local file read: no fetch, no DNS, no added latency, works offline.
Matching is over the outcome PAYLOAD, not just the outcome: the error text on
failure, the result body on success. That is load-bearing. The scaffold's
requireKey wrapper soft-fails an unauthenticated call with exit 0 and
{"needs_signup":true,...}, so "you must sign up first" is not an error at all
-- an outcome-only matcher would stay silent for precisely the cold agent who
most needs the gateway named.
Output goes to stderr, never stdout: stdout is the JSON result agents pipe
into jq, and one line of prose there breaks the workflow this exists to
encourage. --json callers get a structured next_steps object instead.
Additive and fail-silent throughout: absent, malformed, future-schema or
unmatched means the call behaves exactly as before. A hint is a nicety; a
call is not.
- appstore_nextsteps.go: mirrored wire contract, resolver, renderer, cache
- appstore.go: hooks on both outcomes; success resolves before the review
prompt can replace the body
- main.go: fatalHint drains exitNextSteps so steps land after the error they
fix (same pattern motd's importantUpdate already uses)
- PILOT_NEXT_STEPS=off opts out
Ports the precedence fix from app-template's nextsteps.Resolve, which validates the graphs this resolves. The two had drifted: this side still ranked from-exact above a match, so for any method with both a flow edge and a soft-failing gateway, the flow edge won — a cold agent calling primitive.send_email was told to read its inbox instead of to sign up. The parity test passed through the drift because it never covered exact-bare vs wildcard-match, which is precisely the shadowing case. Added it; verified by mutation that re-introducing the old scoring now fails the suite. Proven end-to-end against the real app with the real authored graph: a cold send_email now renders the signup gateway, marked (required first).
gosec flagged both ends as G703 path traversal, correctly. appID arrives straight from argv, so filepath.Join alone is not containment: `pilotctl appstore call ../../etc/x ...` resolves outside the install root and we would read a file there and print pieces of it back as "next steps". Use resolveUnder on both the read and the write — the same guard install and the supervisor already rely on, whose own comment says filepath.Join does NOT block traversal. The regression test needed a second pass. Its first draft passed even with the naive join restored: the graph's app-id-mismatch check rejected the planted file first, so the test was green for the wrong reason and proved nothing. The planted graph's `app` now equals the traversing id, leaving containment as the only thing that can stop the load — verified by mutation that removing the guard fails the suite.
Alexgodoroja
pushed a commit
that referenced
this pull request
Jul 16, 2026
…consolidated catalogue aegis 0.1.3's live bundle has the cli.args argv bug that killed 6 of 7 methods. Adopt the rev2 rebuild: bundle_url -> .../0.1.3/rev2/..., new bundle_sha256/size and per-platform bundles map, metadata size updated, aegis metadata_sha256 recomputed, catalogue re-signed. Verified all 3 rev2 platform bundles resolve (HTTP 200) and their sha256 match. The aegis product_demo (already present) is byte-identical to #389/#396. Consolidates the aegis catalogue content of #389 (demo + go-live) and #396 (rev2 fix).
Alexgodoroja
force-pushed
the
feat/next-steps-graph
branch
from
July 16, 2026 19:43
4ec90ce to
dfc4e76
Compare
Alexgodoroja
added a commit
that referenced
this pull request
Jul 16, 2026
* catalogue: backfill product demos into all 19 apps' metadata (signed) Inject the example-driven `product_demo` (authored in the app-template submissions, app-template#88) into each app's store-page metadata.json, so that `pilotctl appstore install` (pilotprotocol#395) prints the usage banner and the website renders the "Full usage demo". This is the DATA half of the feature — #395 is the renderer and is inert until this lands. - 18 apps: product_demo appended to the existing catalogue/apps/<id>/metadata.json (byte-preserving append; only the new key is added). - io.telepat.ideon-free had no rich metadata page (no metadata_url); created one from its submission via BuildMetadata, and added metadata_url + metadata_sha256 to its catalogue entry. - io.pilot.sixtyfour's demo is injected here at the metadata layer (its third-party-owned app-template submission is under the update gate and could not carry the demo). - catalogue.json: every touched app's metadata_sha256 recomputed; catalogue.json re-signed (catalogue.json.sig) with the catalogue key. Verified: every metadata_url entry's sha matches its file and the signature validates. Additive + non-breaking: product_demo is an optional metadata field; clients that don't render it ignore it. Merging deploys instantly (metadata_url is raw main). * catalogue: consolidate — demos for all 23 installable apps + smol tombstone (signed once) Fold the six sibling catalogue PRs into this one so the catalogue is signed and deployed once instead of seven times: - Product demos for the 4 apps that still lacked one — cosift, primitive, wallet, slipstream — authored against each app's real method set (validated by internal/demo). All 23 installable apps now carry a product_demo. - Consistency fixes: wallet's metadata had NO methods array (now its 16 methods); slipstream had NO metadata page at all (created one). Every installable entry is now schema_version 1 with methods + a demo whose skill == the app id. - Demos for agentphone / bowmark / didit / docker / duckdb (PRs #390–#394) are already present here — byte-identical — with main's richer entry metadata kept (the sibling PRs carried unenriched entries that would have regressed it). - Smol rename collapse (from #360): io.pilot.smolmachines replaced with a tombstone (renamed_to io.pilot.smol, hidden), its detail page removed, and the "Renaming an app" convention documented in catalogue/README.md. - Every metadata_sha256 recomputed; catalogue.json re-signed once. Verified: all 23 metadata_url entries' shas match, the tombstone is well-formed, and the signature validates against the catalogue trust anchor. * catalogue: fold aegis rev2 bundle (argv fix) from #396/#389 into the consolidated catalogue aegis 0.1.3's live bundle has the cli.args argv bug that killed 6 of 7 methods. Adopt the rev2 rebuild: bundle_url -> .../0.1.3/rev2/..., new bundle_sha256/size and per-platform bundles map, metadata size updated, aegis metadata_sha256 recomputed, catalogue re-signed. Verified all 3 rev2 platform bundles resolve (HTTP 200) and their sha256 match. The aegis product_demo (already present) is byte-identical to #389/#396. Consolidates the aegis catalogue content of #389 (demo + go-live) and #396 (rev2 fix). * catalogue: fix ground-truth mistakes in cosift/wallet/slipstream metadata Reviewed the 4 hand-authored demos against each app's real API. Fixes: - cosift: params were wrong — search/answer/research take "q" (not "query") and contents takes "url"/"urls" (not "id"). Verified against the cosift-app adapter (cmd/cosift-app/main.go methodTable) + cosift docs/API.md. - wallet: it exposes NO wallet.help method — removed the phantom method from the metadata list (15 real methods) and from the demo's next. wallet.balance takes {"asset":"USDC"}. Method summaries corrected against pkg/walletipc source. - slipstream: replaced hand-written method summaries with the authoritative ones from the live slipstream.help (installed + queried the app); demo args ({}) confirmed valid (all list params optional). - primitive: verified clean (all 5 demo methods exist, take no required params). Recomputed the 3 metadata_sha256 and re-signed the catalogue. * catalogue: add next_steps dependency graphs to all 19 apps (re-signed) Folds the dynamic-context dependency graphs into this consolidated catalogue PR so the whole rollout is one signature, not a second re-sign after a separate merge. Each app's next_steps is copied verbatim from its app-template submission (the same object BuildMetadata would emit) into catalogue/apps/<id>/metadata.json, its metadata_sha256 re-pinned, and catalogue.json re-signed. 18 apps gain next_steps here; io.telepat.ideon-free already carried a matching graph. Purely additive to each metadata.json (+next_steps, nothing else touched); aegis's rev2 argv-fix bundle pin is unchanged. Verified: every metadata_sha256 pin matches its file bytes, every graph equals its submission source, and the re-signed catalogue.json.sig verifies against the embedded catalogue public key (and rejects tampering). * catalogue: add next_steps graphs to the 5 first-party apps (re-signed) Completes dynamic-context coverage: cosift, wallet, sixtyfour and slipstream gain a next_steps graph here (authored from each app's own source + live testing where free — cosift's flow was exercised end-to-end; sixtyfour used help only, no metered calls; wallet/slipstream from their repos/metadata). Every step is a full `pilotctl appstore call ...` command. io.pilot.smolmachines is intentionally NOT covered: it is a hidden, non-installable tombstone (renamed_to io.pilot.smol) with no metadata.json, so there is nothing to attach a graph to and nothing can call it. That leaves 23/24 catalogue entries with a graph — every real, installable app. Each metadata.json gains only next_steps; metadata_sha256 re-pinned and catalogue.json re-signed. Verified: all pins match, signature valid. * catalogue: fix io.pilot.postgres links (object -> array) so `appstore view` parses Pre-existing data bug, surfaced while verifying that the added product_demo/next_steps fields don't break parsers: postgres's `links` was a JSON object {"documentation":...,"psql_reference":...} but appMetadata.Links is []{label,url} (the shape every other app uses). `pilotctl appstore view io.pilot.postgres` failed to unmarshal the detail metadata on BOTH the released client and this one — unrelated to next_steps. Converted to the array form; re-pinned + re-signed. Authoritative typed-parse over all 23 metadata files now passes; product_demo and next_steps parse cleanly everywhere (Go ignores unknown fields — no client strictness). --------- Co-authored-by: Alexgodoroja <alex@vulturelabs.io>
# Conflicts: # cmd/pilotctl/appstore_metadata.go
TeoSlayer
approved these changes
Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Autonomous agents install an app and stop caring, because nothing in a call's output tells them where the flow goes. The product demo covers install→first-call, once. This covers every call after it: each
pilotctl appstore callnow ends by naming the small set of recommended next commands for where the agent stands — the next step on success, and the specific fix on failure (needs_signup→ the gateway, 402 → balance, missing param → a corrected call).This is the client half. The authoring half (schema, CI gates, 19 backfilled graphs) is app-template#89 (merged). This PR is inert until an app's
metadata.jsoncarries anext_steps— nothing regresses when the field is absent, which is every app today.This PR is pilotctl code only — no catalogue changes. (The aegis catalogue republish that briefly rode here has been removed; it now lives in #398. This branch touches only
cmd/pilotctl/.)Deploy note: client code ships when a
v*tag is cut andpilot-updaterrolls it out — not on merge.Design
$APP/next-steps.jsonat install from the already-sha-verifiedmetadata.json; every call is a local read (path-confined viaresolveUnderafter gosec flagged the raw join as G703).jq;--jsoncallers get a structurednext_stepsobject.requireKeysoft-fail is exit 0 with{"needs_signup":true}, so an outcome-only matcher would stay silent for the cold agent who most needs the gateway named.TestResolveMatchesAppTemplateSemantics; drift caught by a mutation test.recover()-guarded,PILOT_NEXT_STEPS=offopt-out.Verification
go test ./cmd/pilotctl/green;gofmt/go vet/gosecclean.primitive.send_emailrenders the signup gateway marked(required first); a realsqlite.querymissing-param failure renders the fix, and the recommended command then works.(a+)+$, RE2-linear no hang)/2000-edge inputs all fail safe; stdout stays pure JSON; latency overhead negligible.