Scenario Authoring CLI
A small developer-facing CLI that lets you scaffold, validate, dry-run, and lint scenario packs without booting the HTTP server. Internal authoring tool — not exposed over HTTP.
Authoring format reference:
scenario-authoring.md.
Quickstart
From nothing to a validated, dry-run-proven scenario in three commands:
npm run scenario:new -- demo my-sample
$EDITOR packs/demo/scenarios/my-sample/1.0.0/scenario.yaml
echo '{"sampleField":"hello"}' > /tmp/inputs.json
npm run scenario:validate -- packs/demo/scenarios/my-sample/1.0.0/scenario.yaml
npm run scenario:dry-run -- 'demo/my-sample@1.0.0' --inputs /tmp/inputs.json
Note the
--after the script name. npm passes everything after it through to the underlying script verbatim.
Commands
scenario:validate
npm run scenario:validate -- <path-to-scenario.yaml> [--packs-root <dir>]
Loads a scenario file (resolving any !include tags), checks structure, compiles inputs.schema with the same AJV instance the HTTP path uses, validates every fixture under the scenario's fixtures/ directory, walks every {{ inputs.* }} placeholder to confirm it's reachable from the schema or a fixture, and checks the shape of launch.extensions (scenario and template overrides — a mapping of base64 string values, RFC-MACP-0001 §10.3), and cross-checks every participants[].agentRef against the example-agent catalog. Template defaults that fail the input schema (checked for templates that override launch), a template kind other than ScenarioTemplate, any template that overrides commitments, and a commitment with no description are reported as warnings.
| Exit | Meaning |
|---|---|
0 | Pass (may include warnings). |
1 | At least one error. |
Example pass output:
scenario:validate packs/fraud/scenarios/high-value-new-device/1.0.0/scenario.yaml
OK
Example failure:
FAIL participant risk-agent agentRef "missing-agent" is not in the example-agent catalog
FAIL placeholder {{ inputs.unknown }} is not satisfied by schema defaults or any fixture
FAILED (2 error(s), 0 warning(s))
scenario:dry-run
npm run scenario:dry-run -- <scenarioRef> --inputs <file.json> [--template <slug>] [--mode live|sandbox] [--packs-root <dir>]
Runs CompilerService.compile() offline against <scenarioRef> and <file.json> and prints the resulting CompileLaunchResult (sessionId, mode, initiator, runDescriptor, scenarioMeta, display, participantBindings — see api-reference.md § POST /launch/compile) as pretty JSON. This is the same code path as POST /launch/compile, so the output has the same shape and content — except sessionId, which is a fresh UUID v4 on every compile.
| Exit | Meaning |
|---|---|
0 | Compile succeeded; CompileLaunchResult printed to stdout. |
1 | Validation failure, missing scenario, or other compile error. Error code printed to stderr. |
scenario:new
npm run scenario:new -- <pack> <scenario> [--version 1.0.0] [--from <scenarioRef>] [--packs-root <dir>]
Scaffolds packs/<pack>/scenarios/<scenario>/<version>/ with a starter scenario.yaml, templates/default.yaml, and a fixtures/sample.json. Auto-creates packs/<pack>/pack.yaml if the pack doesn't exist yet.
With --from fraud/high-value-new-device@1.0.0, copies an existing scenario directory and rewrites the pack / scenario / version metadata. Useful for forking a scenario for a variant.
Refuses to overwrite an existing version directory. Slugs must be kebab-case ([a-z0-9][a-z0-9-]*).
--versionnow actually works. From the CLI's first commit until September 2026 it did not: the program-level-V, --versionflag shadowed this option, so--version 1.2.3printed the CLI's own version (0.2.0), exited 0, and scaffolded nothing — while this page documented it as working. The program's flag is now-V, --cli-version, which frees--versionfor this subcommand. If you scripted around the bug by omitting--versionand renaming the1.0.0directory afterwards, that workaround is no longer needed.
Printing the CLI's own version
npm run scenario -- --cli-version # -> 0.2.0
Deliberately not --version: that spelling belongs to scenario:new's --version <semver>
option (see above), and commander resolves a program/subcommand collision in favour of the program.
A bare npm run scenario -- --version is now an unknown-option error rather than a silent no-op.
scenario:lint
npm run scenario:lint -- <target> [--packs-root <dir>]
Static checks across one or more packs. Pass either a single pack directory (packs/fraud) or the packs root (packs).
Errors:
pack.yamlmust parse to a YAML mapping (an empty, comment-only,----only, sequence or scalar file is reported, not crashed on).- Pack and scenario slugs are kebab-case.
- Every commitment has a non-empty string
description. launch.extensions(scenario and each template'soverrides.launch.extensions) is a mapping of string values (base64 per RFC-MACP-0001 §10.3).- The policy named by
policyVersionpasses rules-schema validation (checked once per policy across the run,policy.defaultincluded) — see policy-authoring.md. - Every
participants[].agentRefexists in the example-agent catalog. - Any
scenario.yamlor template that fails to load.
Warnings:
policyVersionthat is neitherpolicy.defaultnor a file underpolicies/.- Templates whose
overrides.launch.commitmentsarray is shorter than the scenario's commitments (arrays REPLACE entirely; partial = probably a mistake). - Files under
data/that aren't referenced by any!include(orphans).
| Exit | Meaning |
|---|---|
0 | No errors. Warnings may still print. |
1 | At least one error. |
CI integration
This repo's CI does not currently run either command. A suggested step, if you want it to:
- name: Lint scenario packs
run: npm run scenario:lint -- packs
- name: Validate every scenario
run: |
set -euo pipefail
find packs -mindepth 4 -maxdepth 4 -name scenario.yaml | while read scenario; do
npm run scenario:validate -- "$scenario"
done
Troubleshooting
!include path escapes PACKS_DIR — your relative path resolved outside the packs root. Adjust the ..-segments. The error message shows which file contained the bad include.
!include cycle detected — A → B → A loop. Refactor so one of the two stops referencing the other.
!include target not found — typo or wrong relative path. Remember: paths are resolved relative to the file containing the !include, not relative to the scenario root.
Dry-run output looks polluted — the dry-run command silences Nest's logger to keep stdout clean JSON. If you're seeing log lines mixed in, you've probably wired a custom logger that bypasses Logger.overrideLogger(false) in scripts/scenario/dry-run.ts.
participant X agentRef "..." is not in the example-agent catalog — agentRef must match an entry in src/example-agents/example-agent-catalog.service.ts. Either fix the typo or add the agent to the catalog.
Implementation notes
- The CLI re-uses
FileRegistryLoader,RegistryIndexService,CompilerService, the AJV factory atsrc/compiler/ajv-factory.ts, andExampleAgentCatalogServicefrom the main service. There is no parallel implementation — drift between CLI and HTTP outputs is impossible by construction. - All four subcommands lazy-load their handlers, so
scenario:dry-rundoesn't pay the cost of importing the lint/validate machinery. - Source:
scripts/scenario.ts(dispatcher) andscripts/scenario/{validate,dry-run,new,lint}.ts.