Manifest format
The shape and contents of manifest.json — the canonical record of every SKMTC generation run.
The manifest is overwritten on every skmtc generate (and every
skmtc dev rebuild). It records every file written, every
(generator, item) outcome, parse issues, timing, and per-projection
previews. Treat it as the source of truth for what happened in
the last run.
Two JSON shapes — read carefully. SKMTC produces two structurally different JSON outputs. This document describes the on-disk manifest at
.skmtc/<project>/.settings/manifest.json. Theskmtc generate --jsonstdout output is a different shape (documented inreference/cli/generate.md). They overlap but are not identical: stdout flattensfilesto a top-level string array and does not include the per-itemresultstree. Recipes assuming this manifest shape against stdout JSON will silently producenull. When writing ajqrecipe, decide first which JSON you are querying.
Location
<root>/.skmtc/<project>/.settings/manifest.jsonWritten by the CLI host after the Worker posts its RESULT message.
The host writes the manifest to disk alongside the generated
artifacts.
Top-level shape
The type is ManifestContent (core/types/Manifest.ts, with a
companion Valibot schema manifestContent):
type ManifestContent = {
/** Unique identifier for this run */
deploymentId: string
/** Trace correlation IDs */
traceId: string
spanId: string
/** Optional region identifier (for hosted Sandbox API runs) */
region?: string
/** Unix-millisecond timestamps. (endAt - startAt) = wall-clock time */
startAt: number
endAt: number
/** Every file written by this run, keyed by destination path */
files: Record<string, {
lines: number
characters: number
destinationPath: string
}>
/** Per-Projection preview metadata for UI surfaces */
previews: Record<string, Preview>
/** Per-(generator × item) outcome — see "results" below */
results: ResultsItem
/** Parse-time diagnostics. Always present; empty array means no parse issues. */
parseIssues: ParseIssue[]
}Fields in detail
deploymentId, traceId, spanId
Unique identifiers for the run. Useful when correlating manifests
across multiple generate invocations or matching log entries to
their source run.
region
Present only when the run executed via the hosted Sandbox API. Identifies which deployment region handled the request. Absent for local Worker runs.
startAt, endAt
Unix-millisecond timestamps. endAt - startAt is the wall-clock
duration of the entire pipeline (parse + generate + render + host
I/O).
files
Every file written, keyed by the resolved destination path (after
basePath resolution). Each entry:
lines— line count of the rendered contentcharacters— character countdestinationPath— same as the key; redundant but useful for iterating values
To get the list of files this run produced:
jq '.files | keys' manifest.jsonpreviews
Per-Projection metadata for UI surfaces (dashboard, editor
previews). Each Preview pairs a module ({ name, exportPath } —
the identifier name and export path of the previewable artifact)
with a source descriptor pointing back at the operation, webhook,
or model it was generated from. Populated only by entries that
supply toPreviewModule.
This field is most useful to tooling that wraps SKMTC (the Sandbox API dashboard, for example). For everyday agent work, it can be ignored.
results
The most-consulted field for debugging. A deeply nested record keyed by trace → span → phase → generator package ID → item identifier (verified against real manifests, not extrapolated):
{
"trace-1763060002688": {
"span-1763060002688": {
"generate": {
"@skmtc/gen-express": {
"/accounts%3Aget": "success",
"/accounts%3Apost": "success",
"/deployments/{deploymentId}%3Aget": "success"
},
"@skmtc/gen-valibot": {
"ApplicantModel": "success",
"BrokenModel": "error"
}
},
"render": {
"@/accounts/routes.generated.ts": "success",
"@/deployments/routes.generated.ts": "success"
}
}
}
}Two phase keys sit under span-…:
generate— per-(generator × item) outcome from the Generate phaserender— per-output-file outcome from the Render phase, keyed byexportPath
Each leaf is a ResultType string:
| Value | Meaning |
|---|---|
success | Generator ran and produced output for this item |
warning | Output produced, with a recoverable issue logged |
error | Generator threw or returned failure; output may be missing or partial |
skipped | Item matched but deliberately skipped (via client.json filters) |
notSupported | Generator's isSupported returned false — expected for items outside the generator's scope |
error leaves do not carry the exception text
ResultType is a literal string union in @skmtc/core — 'success' | 'warning' | 'error' | 'skipped' | 'notSupported'. There is no
companion message, stack, or cause field on an errored leaf.
When you see "identifier": "error" in the tree, the manifest is
telling you that the item failed, not why.
Where the exception text lives, by failure phase:
| Phase that failed | Where the message and stack appear |
|---|---|
Bundle (skmtc bundle, or the implicit rebundle inside clone / install / dev) | .skmtc/<project>/.settings/error-logs.txt. The CLI's GenerateBundleTask writes the deno bundle subprocess stderr there on every bundle run |
Generate (skmtc generate / skmtc dev re-run) | Live stderr of the invocation only. The host does not persist generate-time worker errors to disk |
| Hosted Sandbox API run | skmtc workspaces runtime-logs <project> fetches them from the service using manifest.spanId |
Operational consequence for agentic --json workflows: --json
sends the structured result to stdout but stderr still carries the
human-readable message for any item that errored. Capture both:
skmtc generate <project> --json 2>generate.stderr.log. Parsing the
JSON tells you which identifiers errored; the stderr file tells
you what each exception said. The manifest's results tree is not
self-sufficient for this.
Item-identifier formats under generate:
- OAS operation generators:
<path>%3A<method>— URL-encoded colon separates the OpenAPI path from the lowercase HTTP method (matches theStackTrail.toString()format used throughout the engine). Examples:/accounts%3Aget,/deployments/{deploymentId}%3Aput,/users/{userId}/avatar%3Apost. - GraphQL operation generators:
<rootKind>%3A<fieldName>— e.g.,query%3AgetApplicants,mutation%3AcreateApplicant. - Model generators: the refName directly — e.g.,
UserModel,ApplicantModel.
Edge case: no matches at all
When nothing the engine generated produced a result (no generators
matched any item), the tree collapses to a flat SKIPPED marker
instead of the nested trace/span shape:
{
"results": {
"SKIPPED": "error"
}
}Recipes that walk the tree should handle both forms — for example,
checking .results.SKIPPED first before descending into trace keys.
parseIssues
Array of parse-time diagnostics. Always present in the manifest — an empty array means no parse issues, not "old core version".
ParseIssue is a discriminated union of six shapes, keyed by
(protocol, level) — two protocols ('oas', 'gql') × three
levels ('error', 'warning', 'debug'):
type ParseIssue =
| { protocol: 'oas'; level: 'error'; type: OasIssueType; location: string; message: string; cause?: unknown }
| { protocol: 'oas'; level: 'warning'; type: OasIssueType; location: string; message: string }
| { protocol: 'oas'; level: 'debug'; type: OasIssueType; location: string; message: string }
| { protocol: 'gql'; level: 'error'; type: GqlIssueType; location: string; message: string; cause?: unknown }
| { protocol: 'gql'; level: 'warning'; type: GqlIssueType; location: string; message: string }
| { protocol: 'gql'; level: 'debug'; type: GqlIssueType; location: string; message: string }Note that cause exists only on level: 'error' shapes (and
is optional there) — warnings and debug entries never carry it.
debug is informational: the parser handled the input gracefully
and is recording what it did. The type field is constrained to
the protocol's OasIssueType or GqlIssueType literal union — see
reference/error-codes.md for the full lists.
The CLI exits with code 1 when any parseIssues[].level === 'error' is present; an array containing only warnings and debug
entries (or an empty array) exits cleanly.
Diagnostic workflow against the manifest
The most common diagnostic paths:
"It generated nothing"
jq '[.. | strings] | group_by(.) | map({status: .[0], n: length})' manifest.jsonIf every leaf is notSupported, no generator's isSupported matched
any operation/model. Check that the schema has the operations
expected and that the right generators are installed.
"It generated less than expected"
jq '.results[][].generate["@skmtc/gen-X"]
| to_entries | map(select(.value != "success"))' manifest.jsonReplace @skmtc/gen-X with the generator of interest. Returns every
identifier under that generator that wasn't success, with its
status.
"A specific output is missing"
Check files first. If the expected destinationPath isn't there,
find the corresponding identifier in results. error means the
generator failed; notSupported means the engine never reached it.
"Cost / size accounting"
files has lines and characters per output. (endAt - startAt)
is wall-clock duration. To group files by output subdirectory:
jq '.files | to_entries | group_by(.value.destinationPath | split("/")[1])
| map({dir: .[0].value.destinationPath, n: length})' manifest.json"Fatal parse issues only"
jq '.parseIssues // [] | map(select(.level == "error"))' manifest.jsonThe presence of any level: "error" entry causes the CLI to exit
non-zero (typically code 1).
Update behavior
The manifest is overwritten on every generate and dev
rebuild. The previous run's contents are not preserved.
If you need to compare across runs:
- Copy the manifest before re-running:
cp .skmtc/<project>/.settings/manifest.json manifest-before.json - Then re-run, then
diff
In CI, archive the manifest as a build artifact for forensic analysis of failed runs.
Exit code derivation
The CLI exits non-zero when:
- Any
parseIssue.level === 'error'is present → exit 1 --typecheckwas passed and the typecheck failed → exit 1- Required arguments were missing in strict mode → exit 2
Exit 0 means: no fatal parse issues, no typecheck failures, and (if
typecheck was requested) the consumer's tsc --noEmit passed for
the files this run wrote.
The manifest persists regardless of exit code. Even a failed run writes the manifest — the diagnostic record survives.
Common questions
Can I get a streaming view of generation?
No. The Worker computes the full result in memory and posts it back to the host as a single message. The host then writes the manifest and the files. There's no incremental streaming.
For watch-mode-style behavior, use skmtc dev, which auto-reruns
the whole pipeline on file changes. Each rerun overwrites the
manifest.
Why does the results structure have trace/span keys?
Future-proofing for distributed traces. Today there's a single
trace/span per run, but the structure leaves room for nested spans
(e.g., if a run aggregates multiple sub-pipelines). For now, treat
manifest.results[traceId][spanId].generate as a constant prefix
and slice into the generate object.
How big does the manifest get?
For a typical run of a few hundred operations, the manifest is on
the order of 100KB-1MB. Each entry in files and results is
small (~100 bytes), but they accumulate. For very large schemas
(thousands of operations), the manifest can reach several MB. There's
no rotation or trimming; the file just grows with the schema.
Where do I see real-time progress?
In stdout/stderr from the skmtc generate invocation, not in the
manifest. The manifest is the post-hoc record. For real-time, watch
the CLI's output stream.
Cross-references
- Error handling philosophy — why the manifest is canonical
- Error codes — full list of
parseIssue.typevalues skmtc-debugskill — operational diagnostic workflowsskmtc generatereference — the command that writes the manifestskmtc-cliskill §10 (CI task card) — archiving the manifest in CI