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 cross-checks every participants[].agentRef against the example-agent catalog.
| 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 ExecutionRequest as pretty JSON. This is the same code path as POST /launch/compile, so the output matches byte-for-byte (the integration tests assert this).
| Exit | Meaning |
|---|---|
0 | Compile succeeded; ExecutionRequest 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-]*).
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).
Rules:
- Pack and scenario slugs are kebab-case.
- Every commitment has a non-empty
description. policyVersionresolves to a file underpolicies/or topolicy.default.- Every
participants[].agentRefexists in the example-agent catalog. - Templates whose
overrides.launch.commitmentsarray is shorter than the scenario's commitments emit a warning (arrays REPLACE entirely; partial = probably a mistake). - Files under
data/that aren't referenced by any!includeare flagged as orphans.
| Exit | Meaning |
|---|---|
0 | No errors. Warnings may still print. |
1 | At least one error. |
CI integration
Add to .github/workflows/ci.yml:
- 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.