src/autoskillit/skills/sous-chef/SKILL.md
<!-- Internal bootstrap document — not a user-invocable skill. Injected by open_kitchen() into every orchestrator session. --> # Sous Chef: Global Orchestration Rules These rules apply to ALL orchestration sessions, whether following a recipe or operating ad-hoc. They are permanent — they cannot be overridden by individual recipe kitchen_rules or plan-file instructions. --- ## MULTI-PART PLAN SEQUENCING — MANDATORY When `plan_parts` contains more than one file (Part A, Part B, …): 1.
npx skillsauth add talont-org/autoskillit src/autoskillit/skills/sous-chefInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
These rules apply to ALL orchestration sessions, whether following a recipe or operating ad-hoc. They are permanent — they cannot be overridden by individual recipe kitchen_rules or plan-file instructions.
When plan_parts contains more than one file (Part A, Part B, …):
test_check) and merge it
(merge_worktree) into the base branch before implementing the next part.implement-worktree-no-merge for Part N+1 while Part N's
worktree is unmerged.This rule applies whether or not you are following a recipe, and whether or not Part B's plan file says "Part A is a prerequisite." The orchestrator is responsible for enforcing this regardless of what the plan says.
When calling run_skill, the skill_command argument MUST be a space-separated token
string — never a structured document or markdown section list.
${{ context.* }} and ${{ inputs.* }} placeholders with their resolved
values and pass the result VERBATIM to run_skill.##), labels, notes, or explanatory prose to
skill_command. It is not a document — it is a command string./path/to/file.md — not a labeled section.note: are appended as space-separated tokens.Wrong: /autoskillit:implement-worktree-no-merge\n\n## Plan Path\n/path/plan.md\n\n## Branch\nimpl-926
Right: /autoskillit:implement-worktree-no-merge /path/plan.md impl-926
This applies to ALL skills, including bare-placeholder steps where you supply values
at runtime (/autoskillit:arch-lens-{slug} {context_path} → substitute, then pass verbatim).
When run_skill returns needs_retry=true for any step:
retry_reason: resume AND subtype: stale → re-execute the same step (decrement the
retries counter). A stale session was killed by the hung-process watchdog — this is NOT a
context limit. Do NOT follow on_context_limit. If retries are exhausted, follow on_exhausted.retry_reason: resume AND subtype≠stale AND the step defines on_context_limit → follow on_context_limit.
The worktree or partial state is on disk; route to the designated recovery step
(typically test or retry_worktree) to check whether partial work was sufficient.
API infrastructure errors (overload, 529, ECONNRESET) also produce retry_reason=resume
with infra_exit_category="api_error" — route them identically to context exhaustion.
The infra_exit_category field is informational only
("completed", "context_exhausted", "api_error", "process_killed").retry_reason: resume AND subtype≠stale AND the step has no on_context_limit → fall through to on_failure.retry_reason: drain_race AND the step defines on_context_limit → follow on_context_limit.
The channel signal confirmed session completion; stdout was not fully flushed before kill.
Partial progress is confirmed — treat identically to resume for routing purposes.retry_reason: drain_race AND the step has no on_context_limit → fall through to on_failure.retry_reason: completed_no_flush AND the step defines on_context_limit → follow on_context_limit.
The session exited with empty stdout but write evidence confirms files were written to the worktree.
Partial progress is confirmed — treat identically to resume for routing purposes.retry_reason: completed_no_flush AND the step has no on_context_limit → fall through to on_failure.retry_reason: empty_output → fall through to on_failure. The session produced no
output AND no write evidence (no Write/Edit calls, no filesystem writes). Do NOT route to on_context_limit even if defined.retry_reason: path_contamination → fall through to on_failure. The session wrote
files outside its working directory. This is a CWD boundary violation, not a context limit.
Do NOT route to on_context_limit even if defined.retry_reason: thinking_stall AND lifespan_started is true AND the step defines
on_context_limit → follow on_context_limit. The model consumed tokens (thinking
blocks) but produced no final output. Prior tool calls suggest partial progress on disk.retry_reason: thinking_stall AND lifespan_started is false → fall through to on_failure.
No progress was made.retry_reason: idle_stall AND lifespan_started is true AND the step defines
on_context_limit → follow on_context_limit. The idle watchdog killed the session,
but prior tool calls suggest partial progress on disk.retry_reason: idle_stall AND lifespan_started is false → fall through to on_failure.
No progress was made.retry_reason: early_stop AND has_progress_evidence is true in the result AND the step
defines on_context_limit → follow on_context_limit. The model made progress (wrote files
or created a worktree) but stopped before emitting the completion marker. Partial progress
exists on disk.retry_reason: early_stop AND has_progress_evidence is false → fall through to on_failure.retry_reason: zero_writes AND has_progress_evidence is true in the result AND the step
defines on_context_limit → follow on_context_limit. The model made filesystem contact
but made no Write/Edit tool calls. Partial progress may exist on disk.retry_reason: zero_writes AND has_progress_evidence is false → fall through to on_failure.retry_reason: stale → decrement the retries counter for this step.
Re-execute the same step if retries remain. If retries are exhausted, fall through
to on_failure. Do NOT route to on_context_limit — stale is a transient failure,
not a context limit. No partial progress is assumed.Worktree-stale carve-out: When a step that invokes a worktree-creating skill
(implement-worktree-no-merge, implement-worktree, implement-experiment) returns
retry_reason: stale (or retry_reason: resume with subtype: stale), re-execute the
step without consuming the retries budget. Stale means the session produced nothing
useful — the worktree orphan concern that motivates retries: 0 does not apply.
This is a one-shot retry: if the retry also goes stale, fall through to on_failure.
Before re-executing, if the stale result captured worktree_path, remove the empty
worktree (git worktree remove --force <path>) to prevent orphaned worktrees.
For implement-worktree-no-merge specifically:
on_context_limit routes to retry_worktree in standard recipes./autoskillit:retry-worktree — pass the existing worktree_path from the
partial session's output. The worktree is on disk with all commits made so far.implement-worktree-no-merge again. A new call creates a fresh
timestamped worktree, discarding all partial progress.When a completed worktree implementation needs to be redone (e.g., after a plan revision):
implement-worktree-no-merge on the revised plan (creates a fresh worktree).Summary: needs_retry=true + retry_reason=resume + subtype=stale → re-execute step (decrement retries; on_exhausted when budget gone).
needs_retry=true + retry_reason=resume + subtype≠stale + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=resume + subtype≠stale + no on_context_limit → on_failure.
needs_retry=true + retry_reason=drain_race + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=drain_race + no on_context_limit → on_failure.
needs_retry=true + retry_reason=completed_no_flush + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=completed_no_flush + no on_context_limit → on_failure.
needs_retry=true + retry_reason=empty_output → on_failure.
needs_retry=true + retry_reason=path_contamination → on_failure.
needs_retry=true + retry_reason=thinking_stall + lifespan_started=true + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=thinking_stall + lifespan_started=false → on_failure.
needs_retry=true + retry_reason=idle_stall + lifespan_started=true + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=idle_stall + lifespan_started=false → on_failure.
needs_retry=true + retry_reason=early_stop + has_progress_evidence=true + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=early_stop + has_progress_evidence=false → on_failure.
needs_retry=true + retry_reason=zero_writes + has_progress_evidence=true + step has on_context_limit → follow on_context_limit.
needs_retry=true + retry_reason=zero_writes + has_progress_evidence=false → on_failure.
needs_retry=true + retry_reason=stale → decrement retries counter → on_failure when exhausted (no partial progress, not a context limit).
needs_retry=true + retry_reason=stale + worktree-creating step → one-shot re-execute (bypasses retries budget; on_failure if repeated stale).
audit-impl uses a SHA-based diff: it compares the worktree HEAD against the
merge-base with the base branch, scoping the diff to exactly that group's changes.
Rules:
audit-impl before merging — it inspects the unmerged worktree diff.audit-impl will correctly see only
that group's diff against the now-updated base branch.audit-impl call against multiple merged groups — the diff scope
will be too broad and the audit will be inaccurate.plan_parts= OUTPUTmake-plan emits plan_parts= as a flat newline-delimited ordered list of
absolute paths:
plan_parts = /abs/path/to/plan_part_a_....md
/abs/path/to/plan_part_b_....md
Act on this list as follows:
merge_worktree) before moving to the next.When the user provides more than one issue or task in a single request:
If the user says "parallel" (or "run in parallel", "simultaneously", "at the same time", "concurrently"):
a. Build execution map first. Call run_skill with /autoskillit:build-execution-map
passing all issue numbers. This produces an execution_map JSON artifact at the
emitted path.
b. Read the execution map. Parse the JSON to extract groups and merge_order.
c. Dispatch groups in order. For each group in ascending group number:
parallel: true → launch all issues in the group as independent pipeline
sessions simultaneously, using the wavefront scheduling rule (defined in the section below).parallel: false → run the group's issues one at a time in sequence.d. Merge-wait between groups. Group N+1 must NOT begin cloning until ALL of
Group N's PRs have merged to the base branch. This ensures every group's clones
capture a base SHA that includes all prior groups' changes. Use the MERGE PHASE
rules to merge each group's PRs, following the merge_order from the map for
intra-group merge sequencing.
e. Fallback. If build-execution-map fails or returns an error, fall back to
launching all N pipelines immediately (current behavior). Do not block dispatch
on map failure.
If the user says "sequential" (or "one at a time", "in order", "one by one") → run them one at a time without asking.
If the user does not specify → ask exactly one question using AskUserQuestion:
"Do you want to run these sequentially (one at a time) or in parallel (all at once)?" Present exactly two options. Nothing else.
NEVER:
implementation-groups — that recipe is for coordinated
multi-issue planning with a shared plan, not independent parallel execution.This rule applies whenever you are running multiple pipelines in parallel (run_mode=parallel or user says "parallel"). Within each batched round, pipeline steps have two speeds:
Fast steps — MCP tool calls that complete in seconds:
run_cmd, clone_repo, create_unique_branch, fetch_github_issue,
claim_issue, merge_worktree, test_check, reset_test_dir, classify_fix,
push_to_remote
Slow steps — headless sessions that take minutes:
Any run_skill invocation (investigate, implement, audit, review, etc.)
Complete all fast steps for ALL pipelines first. Before launching any slow step, advance every pipeline through its pending fast steps. Continue re-inspecting after each fast-step batch until no pipeline has a fast step pending.
Launch all slow steps together in one parallel batch. Once all pipelines are aligned
at a slow step boundary (every pipeline's next pending step is a run_skill), launch
all of them simultaneously so they overlap in wall-clock time.
Never launch a slow step for one pipeline while another pipeline still has fast steps pending. This is the most critical rule: a batched round waits for the slowest step in the batch. A fast step launched alongside a slow step completes instantly but sits idle until the slow step finishes — wasting wall-clock time and blocking re-inspection.
Advance every active pipeline in every round. A pipeline is "active" if it has not
reached done or escalate_stop. In every batched round, every active pipeline MUST
receive at least one step — either a fast step is drained or a slow step is launched.
Never leave an active pipeline idle for an entire round while sibling pipelines are
progressing. If a pipeline has completed all its plan_parts and only has finalization
steps remaining (push, merge, close), it is still active and must be advanced.
Batched rounds wait for the slowest step in the batch. If a slow run_skill is launched
alongside a fast run_cmd, the fast step completes instantly but cannot trigger the next
fast step for its pipeline until the entire batch (including the slow session) finishes.
Draining all fast steps first ensures every pipeline arrives at the slow-step boundary
simultaneously, after which all slow steps run in parallel and their wall-clock time overlaps.
When dispatching from an execution map:
Group iteration is outer loop. The group number (1, 2, 3, ...) is the primary ordering. Within each group, the wavefront scheduling rule governs step interleaving.
Merge-wait is mandatory between groups. After all pipelines in Group N complete (including their merge phase), verify all Group N PRs have merged to the base branch before starting Group N+1. This prevents Group N+1 from cloning a stale base.
merge_order governs intra-group PR merge sequencing. Within a parallel group,
merge PRs in the order specified by merge_order (not by completion time). This
minimizes merge conflicts by merging simpler changes first.
Single-issue groups skip wavefront. If a group has parallel: false or contains
only one issue, run it as a single pipeline — no wavefront scheduling needed.
Do not pause for confirmation between groups. Once merge-wait verifies all Group N PRs have merged, dispatch Group N+1 immediately. NEVER use AskUserQuestion to ask whether to proceed to the next group.
Handle deferred issues before dispatching any group.
After reading the execution map, check for has_deferred: true.
If has_deferred is false (no deferrals): proceed directly to Group 1 dispatch.
If has_deferred is true:
6a. Pre-dispatch freshness check. Before presenting any escalation question,
re-query the current label state of ALL unique blocker issue numbers across all
deferred_issues entries in a single batched GraphQL request using aliases:
query {
i887: repository(owner:"<OWNER>", name:"<REPO>") {
issue(number:887) { labels(first:20) { nodes { name } } }
}
i912: repository(owner:"<OWNER>", name:"<REPO>") {
issue(number:912) { labels(first:20) { nodes { name } } }
}
}
For each blocker, check whether the in-progress label is still present. If a
blocker's label has been removed since the map was built, remove it from that deferred
issue's blocked_by list. If all blockers for a given deferred issue have cleared,
that issue is no longer deferred — collect it in an "auto-cleared" list for the
supplementary map (step 6e). Never issue individual gh issue view calls per blocker
— always batch into a single GraphQL request.
6b. Present AskUserQuestion for each still-deferred issue. For each deferred issue
where at least one blocker's in-progress label is still present, call
AskUserQuestion:
"Issue #N (title) cannot be dispatched safely. It conflicts with in-progress issue(s): #M1 (title1), #M2 (title2). Conflict: {reasoning}
Choose:
- Wait — Hold #N; retry after the blocking issues complete
- Proceed anyway — Dispatch #N now, accepting conflict risk
- Drop — Remove #N from this session entirely"
Headless-mode rule (MANDATORY): When AskUserQuestion is denied by the hook
(the deny message says "proceed without user confirmation" — this refers to general
tool behavior, NOT this decision), treat the response as Wait. Do NOT interpret
the deny message as permission to proceed. This is the explicit safe default for
unattended sessions.
6c. Route based on user answer (or Wait default):
Wait: Hold the issue. At each group-completion barrier (after all run_skill
calls in a group return — the natural inter-group barrier in both queue-mode and
classic-mode), re-check ALL outstanding Wait-path blockers via a single batched
GraphQL aliases query. When all blockers for a Wait issue have cleared, move it to
the auto-cleared list for step 6e. After the final group completes, if any Wait
issues remain uncleared, report them as skipped in the session result.
Proceed: Add the issue to a new sequential group inserted at the end of the
dispatch sequence (after all other groups). This is transparent in both merge modes:
in queue-mode (#1268), the pipeline self-merges; in classic mode, merge-prs handles
it. Do not create one group per Proceed issue — batch all Proceed issues into the
single final group together.
Drop: If the issue has already been claimed (its in-progress label is set by
this session), call release_issue to remove it. Then exclude the issue from all
dispatch, merge, and reporting. Do not mark it as failed — it is not a failure.
6d. Zero-non-deferred-groups edge case. When all target issues are deferred and no dispatch groups exist (group_count is 0 after removing deferred issues), skip group dispatch entirely. Enter an explicit poll loop:
github.deferred_poll_interval_seconds (default: 60 seconds between checks).github.deferred_poll_timeout_seconds (default: 1800 seconds / 30 minutes).success: false with failure_reason: "All target issues deferred due to in-progress conflicts — human decision required". Exit.If in headless mode and all issues default to Wait with zero dispatch groups:
skip the poll loop and immediately set success: false with the above
failure_reason. Do not poll indefinitely in unattended sessions.
6e. Supplementary map for auto-cleared and freshness-cleared issues. When one or more Wait/freshness-cleared issues become eligible:
build-execution-map for the newly eligible issues only — pass their
issue numbers as arguments. This is a full analysis (pairwise + cross-assessment),
not a passthrough. The supplementary map may itself produce new deferrals if
remaining in-progress issues conflict with the eligible set.has_deferred=true, apply steps 6b and 6c to
the newly deferred issues before dispatching the supplementary groups. In headless
mode, treat all new deferrals as Wait (same rule as 6b). Issues that remain deferred
after this re-entry are skipped and reported in the session result.The step_name passed to run_skill (and all other recipe-step tools that accept
step_name) must be the exact value from the recipe YAML with: block.
NEVER append clone numbers, instance indices, retry counts, or any other disambiguation strings. The telemetry layer aggregates all invocations of the same logical step automatically — suffixing produces garbage rows in token and timing tables.
Correct:
with:
step_name: implement
Wrong (produces garbage):
with:
step_name: implement-30 # ← NEVER DO THIS
This rule applies whether running sequential or parallel pipelines. Each clone or parallel run of the same step reports under the same canonical step name.
MODEL PROPAGATION — When the user specifies a model (e.g. "use opus"), apply it to the model parameter of ALL run_skill calls for steps that declare a model: field — including follow-on steps (retry_worktree, fix, resolve_review, resolve_ci, conflict resolution). All run_skill steps in orchestrated recipes must declare a model: field; steps that omit it are ineligible for propagation and silently bypass user model selection.
This rule applies whenever the orchestrator must merge one or more open PRs, whether produced by a single pipeline or by N parallel pipelines.
Before initiating any merge, run the following detection step via run_cmd (not a
headless session):
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) &&
OWNER=${REPO%%/*} && REPO_NAME=${REPO##*/} &&
BRANCH="<base_branch>" && # substitute the PR's target branch (e.g. "main", "develop")
gh api graphql -f query="query {
repository(owner:\"$OWNER\", name:\"$REPO_NAME\") {
mergeQueue(branch:\"$BRANCH\") { id }
}
}" | jq -r 'if .data.repository.mergeQueue != null then "true" else "false" end' || echo false
Capture the result as queue_available. If gh api graphql fails (auth error, network
error), the || echo false fallback ensures queue_available defaults to "false",
routing to the safe sequential (non-queue) path rather than leaving the variable unset.
Run this once per orchestration run, not per-PR.
After detecting queue availability, also detect auto-merge availability:
gh api graphql -f query="query {
repository(owner:\"$OWNER\", name:\"$REPO_NAME\") {
autoMergeAllowed
}
}" | jq -r '.data.repository.autoMergeAllowed // false' || echo false
Capture the result as auto_merge_available. If detection fails, default to "false".
Note: All three recipes (implementation, implementation-groups, remediation)
perform both detections automatically via check_merge_queue + check_auto_merge —
do not repeat them manually when following a recipe.
The recipes route on the four-cell matrix queue_available × auto_merge_available:
| queue_available | auto_merge_available | Recipe path |
|-------------------|------------------------|------------------------------------------------|
| true | true | enable_auto_merge → wait_for_queue |
| true | false | queue_enqueue_no_auto → wait_for_queue |
| false | true | direct_merge → wait_for_direct_merge |
| false | false | immediate_merge → wait_for_immediate_merge |
When queue_available == true: GitHub's merge queue intercepts every merge
request on the branch regardless of the --auto flag. Both queue cells route
through wait_for_queue (the merge-queue-aware waiter). The
enable_auto_merge cell uses --auto so the queue serializes via GitHub
auto-merge; the queue_enqueue_no_auto cell (condition:
queue_available == true and auto_merge_available == false) uses plain --squash
because the repository's autoMergeAllowed=false setting causes --auto to be
rejected by the API auto-merge gate before the queue interception.
When queue_available == false: there is no queue, so behaviour matches
the historical paths — direct_merge waits via auto-merge, immediate_merge
executes synchronously.
route_queue_mode selects the correct cell
automatically from context.queue_available and context.auto_merge_available.gh pr merge --squash --auto when auto_merge_available == false,
regardless of queue_available. The --auto flag is rejected by GitHub's API
auto-merge gate before the queue intercepts. Use plain gh pr merge --squash;
if a queue exists on the branch the queue still enqueues the call.wait_for_immediate_merge
— its 5-minute poll is too short for a busy queue and on timeout the recipe
reports merge unconfirmed even though the PR will eventually merge.For ad-hoc (off-recipe) merges:
queue_available=true (and sequential_queue is not "true"): each pipeline's
implementation recipe handles its own enqueue via route_queue_mode →
enqueue_to_queue → wait_for_queue. Do NOT invoke merge-prs. The orchestrator's
natural parallel-batch join (waiting for all Group N run_skill invocations to
return) serves as the inter-group barrier — each pipeline only returns after
wait_for_queue reports merged, confirming its PR is in the base branch. Advance
to Group N+1 only after all Group N pipelines complete.queue_available=false OR sequential_queue == "true": route through the
merge-prs recipe for batch sequential merging (unchanged behavior).Hidden ingredient — sequential_queue (default "false"): Force the centralized
merge-prs path even when queue_available=true. Use when batch-level review via an
integration PR is required, or when cross-PR audit_impl is needed as a quality gate.
Each pipeline's per-PR review step before enqueue provides per-PR review coverage; the
merge queue itself provides conflict safety. sequential_queue is only needed when
consolidated batch-level review is explicitly required.
NEVER use run_cmd with gh pr merge to merge a PR outside of a named recipe
step. All PR merges must flow through the recipe's merge_pr, direct_merge,
immediate_merge, enable_auto_merge or queue_enqueue_no_auto steps. Bypassing these steps skips CI
enforcement, conflict detection, and conflict routing.
When wait_for_direct_merge or wait_for_immediate_merge returns closed (PR was
closed due to a stale base):
direct_merge_conflict_fix or
immediate_merge_conflict_fix handles rebase-and-retry automatically.run_cmd for git investigation (git rebase, git log, git reset,
git merge). The resolve-merge-conflicts skill run by direct_merge_conflict_fix
and immediate_merge_conflict_fix has full diagnostic access.When quota-related events occur during pipeline execution:
When a run_skill call is DENIED by the quota guard hook:
run_cmd sleep command with the required duration.run_cmd immediately.run_skill call with
identical arguments (skill_command, cwd, model, step_name).on_failure. Do NOT report to the user. Do NOT skip the step.When run_skill output contains --- QUOTA WARNING ---:
run_cmd sleep command.run_cmd BEFORE calling the next
run_skill (whether it is the next pipeline step or a retry).When a run_skill call is DENIED with "QUOTA BUDGET EXCEEDED":
"success": false"reason": "fleet_quota_exhausted""wait_seconds": <seconds_until_reset>"summary": "Quota exceeded; session budget insufficient for sleep. Resume after window resets."on_failure. Do NOT report to the user.run_skill has zero side effects — no partial state, no worktree changes.
Retrying with the same arguments is always safe.AskUserQuestion for quota events — they are fully automated.You MUST execute every step the pipeline routes you to. The recipe step graph is the sole authority on what executes and in what order.
Context management is handled by the system via on_context_limit routing. Execute every step at full fidelity regardless of session length.
skip_when_false evaluating to false.
When skip_when_false evaluates to true (or is absent), the step MUST execute.run_cmd
with gh pr create, gh pr review, or gh api to substitute for recipe steps.prepare_pr, run_arch_lenses, compose_pr, annotate_pr_diff, review_pr).
Bypassing these steps skips diff annotation, architectural lens analysis, and
automated code review.optional: true on a recipe step does NOT mean the step is discretionary. It means:
skip_when_false ingredient evaluates to false.success: false MUST follow on_failure.Do NOT output prose status text, phase announcements, or progress summaries between tool calls. Every non-final assistant turn MUST invoke at least one tool.
The only permitted text-only turn is a final response containing structured output
tokens (plan_path = ..., worktree_path = ..., etc.).
This applies to all skills invoked interactively within a cook session.
development
Generate YAML recipes for .autoskillit/recipes/. Use when user says "make script skill", "generate script", "script a workflow", "write a script", "create a script", "new recipe", "write a pipeline", or when loaded by other skills for script formatting.
data-ai
Create Uncertainty Representation visualization planning spec showing error bar definitions, distribution-aware alternatives, and multi-seed variance protocols. Statistical lens answering "How is uncertainty honestly represented?"
data-ai
Create Temporal Dynamics visualization planning spec showing axis scaling (linear vs log), smoothing disclosure, epoch/step alignment, run aggregation (mean + variance bands), early-stopping markers, and wall-clock vs step-count x-axis. Temporal lens answering "Are training dynamics shown clearly and honestly?"
data-ai
Create Narrative Story Arc visualization planning spec showing visual consistency across the report (same color = same model everywhere), logical figure progression, redundant figure detection, and narrative dependency between figures. Narrative lens answering "Do the figures tell a coherent story across the report?"