Imported from pabloimrik17/monolab (
claude-plugins/experiments/skills/commander-update-orchestrator/SKILL.md). Install upstream withnpx skills add pabloimrik17/monolab --skill commander-update-orchestrator. Copyright stays with the author.
commander-update-orchestrator
Cross-project npm-update orchestration. Parameterized by level / target, so every commander:update-* command (and the deep variants) drops into the same plumbing without re-implementing fan-out / fan-in.
When to use
- Invoked by
/experiments:commander-update-patchwithlevel=patch,target=patch(shallow mode, default). - Invoked by
/experiments:commander-update-deep-patchwithlevel=patch,target=patch,mode=deep(deep mode, MON-199). commander-update-{minor,major,engines}andcommander-update-deep-{minor,major,engines}wire the matchinglevel/target/modetrio. Atlevel=enginesthe scan/apply steps route to the engine toolchain skills — see "Level-conditional routing:level=engines".- Composes with
parallel-research-workflow(in cross-project mode) for the deep-mode research insertion (Step 6.5).
Never invoke directly from the user side. The skill is meant for command-layer composition.
Artifact glossary + main-window context diet. dossier.md is the global research document (formerly plan.md; no artifact is named plan.md), changeset.md is the per-project concrete apply plan written by an apply teammate, and Claude Code plan mode is the harness feature (used only as the changeset gate's review UI — see Step 10b). The orchestrator's main conversation holds only paths and small status digests (target ≤ ~30 lines each; structured tables such as the bump set may exceed the target but stay bounded digests, never full artifact bodies). It SHALL NOT load changelog bodies, per-group research files, or the dossier body into its own context, and ncu/install output SHALL NOT stream verbatim into the conversation (on-disk logs + digest + bounded tail-on-failure only, per apply-npm-updates).
Inputs
| Field | Type | Required | Notes |
|---|---|---|---|
level |
string |
yes | One of patch, minor, major, engines. Passed verbatim to experiments:scan-npm-updates. |
target |
string |
yes | One of patch, minor, major, engines. Passed verbatim to ncu --target. Matches level for the four shipped shallow commands and four future deep commands. |
mode |
string |
no | One of shallow, deep. Default shallow. Selects the deep-research path (cross-project changelog research inserted at Step 6.5 + per-project changeset gate round at apply time). The shallow path is byte-equivalent across mode: "shallow" and an absent mode input. See "Mode-conditional behavior" below. |
overrideRegistryPath |
string |
no | Repo-relative path to a pkg-upgrade-overrides.yaml file. Default: claude-plugins/experiments/skills/scan-npm-updates/data/pkg-upgrade-overrides.yaml. Identical default in both modes. |
projectsFilter |
string[] |
no | Project names to operate on. When set, the project picker is skipped. When unset, the multi-select picker is raised (Step 3). |
Input validation
Reject before any side effect:
- Unknown
level: abort withError: invalid level "<value>". Expected patch|minor|major|engines. - Unknown
target: abort withError: invalid target "<value>". Expected patch|minor|major|engines. - Unknown
mode: abort withError: invalid mode "<value>". Expected shallow|deep.
Mode-conditional behavior
This skill ships two execution paths selected by the mode input. The shallow path is byte-equivalent to MON-194's shipped contract; the deep path layers research + a per-project changeset gate round on top.
| Step / Concern | mode === "shallow" (default) |
mode === "deep" |
|---|---|---|
| Steps 1, 2, 3, 4, 5, 6, 8 | Identical (shared plumbing). | Identical (shared plumbing). |
| Step 6.5 (cross-project research) | SHALL NOT execute. No plan-dir is created. | Fires. Composes group-packages-for-research + parallel-research-workflow (cross-project mode). Produces <plan-dir>/dossier.md (synthesized by the workflow's teammate + checks). |
| Step 7 (gate rendering) | Generates the bump-set table inline from CrossProjectPlan. |
Dossier gate rendering: references <plan-dir>/dossier.md by path + a bounded digest; appends orchestrator-owned drift sections (Warnings, scan-failed, path-missing). |
| Step 9 (gate) | Three options: apply-all, pick-subset, cancel. |
Four options: apply-all, apply-bumps-only, pick-subset, cancel. pick-subset accepts both package names AND improvement-bullet titles. |
| Step 10 (apply) | Single per-project bumps loop (10.1–10.6). | Splits into Step 10a (bumps loop, identical mechanics; failure pauses for a stop/continue decision), Step 10b (per-project changeset gate round), Step 10c (end-of-flow cleanup). |
| Step 11 (summary) H1 | ## commander-update-<level> summary |
## commander-update-deep-<level> summary |
| Step 11 (summary) sections | Shallow set (Applied / Failed / Pending / Skipped-by-{path,scan,user,policy,override} / Warnings). | Shallow set PLUS Applied improvements, Skipped improvements, Inapplicable improvements, Skipped or unavailable groups. Conditional Review dossier.md bullet on keep-plan. |
The shallow path SHALL NOT execute Step 6.5, SHALL NOT run any changeset gate round at apply time, SHALL NOT invoke the workflow's end-of-flow cleanup, and SHALL NOT render deep-mode summary sections. The deep-mode insertions are local to Steps 6.5, 7, 9, 10a/10b/10c, and 11 — Steps 1, 2, 3, 4, 5, 6, and 8 behave identically across modes (in particular, Step 8 override registry consultation is shared verbatim — Decision 5 in design.md).
Level-conditional routing: level=engines (toolchain bump)
engines is not an ncu dependency level. When level === "engines" (and target === "engines"), the orchestrator swaps the per-project scan and apply steps to the engine toolchain skills and aligns cross-project on the engine version, while reusing the rest of the cross-project skeleton unchanged (project resolution Steps 1–3, subset selection, plan rendering Step 7, sequential apply with stop-on-fail Step 10, aggregated summary Step 11). For level ∈ {patch, minor, major} this entire section is inert — those levels use scan-npm-updates / apply-npm-updates exactly as before (design D6, additive delta).
The engines branch overrides exactly these surfaces:
- Step 4 — scan. The per-project scan agent invokes
detect-toolchain-surfaces(capabilityengine-surface-scanning) instead ofscan-npm-updates. Each agent returns that skill'sEngineSurfaceInventoryJSON verbatim (or{"_error": "<string>"}on a precondition abort). The CWD + parse-and-tag plumbing of Step 4 is unchanged; only the skill name in the agent prompt changes. - Steps 5–6 — aggregation + alignment. Aggregate per-engine across projects instead of per-package. Cross-project alignment is on the resolved engine version: resolve one target per engine once (Node → latest LTS; pnpm/npm/yarn/bun/deno → latest — via
apply-engine-bumps's resolution) and reuse it for every project. There is no per-package max-wins computation and no range-admission conflict prompt. A project already pinned above the resolved target is surfaced and left higher unless the user opts to converge. Intra-repo misalignment reported bydetect-toolchain-surfacesis surfaced and converged to the resolved target (runtime loci only). - Step 8 — override registry consultation is SKIPPED. Package-name overrides (Storybook-style families) have no meaning for runtime/PM surfaces.
OVERRIDE_RUN/OVERRIDE_SKIPare empty; every eligible runtime surface is in the generic apply set. - Step 10.3 — apply. The per-project apply invokes
apply-engine-bumps(capabilityengine-update-apply) instead ofapply-npm-updates. Noncuis invoked at engines level. The orchestrator passes the project's inventory + the resolved per-engine targets (withconfirmed: true— the user already gated at Step 9, and anyambiguousloci were resolved by the command/orchestrator before apply) and folds the returned{ resolvedTargets, applied, skipped, droppedHashes, failure? }fragment into the project's summary entry.supportandunknownSurfacesloci are never touched; publishable-libengines.<engine>support ranges are preserved across every project. - Step 6.5 / 7.D — deep mode. When
mode === "deep", Step 6.5 invokesparallel-research-workflowwithlevel=engines(research targets engine release notes, deduplicated once per engine/version) and Step 7.D surfaces the resultingdossier.md— including the presence of its## Breaking changes & migration+## Changelogssections — by path + bounded digest (never verbatim bodies). No## PR plan/partition-breaking-changesapplies (Step 7.D point 3 runs only forlevel === "major"): an engine bump is a single coordinated co-upgrade (Node + its PM, moved together), so the PR-partition is meaningless — one bucket. Isolation, when chosen (Step 9.5), wraps the whole engine bump as one workspace per project.
Everything else (the project picker, the gate's apply-all/pick-subset/cancel shape, stop-on-fail, the summary skeleton, the hard rules, registry read-only) is reused verbatim. Where the steps below describe ncu/package mechanics, read them through this section's overrides when level === "engines".
Registry contract (read-only excerpt)
This skill reads the user-scoped Commander registry. The full contract is in commander:add. Relevant invariants repeated here so this file is self-contained — no shared sidecar yet (extraction deferred until the third commander consumer requires it).
Path
<HOME>/.claude/commander/projects.json — <HOME> resolves to $HOME on POSIX and %USERPROFILE% on Windows.
Lazy create — read MUST NOT touch disk
A missing file MUST be treated as an empty registry. The skill MUST NOT create the directory or the file. The on-disk state is byte-identical before and after every run (verifiable via shasum).
Schema (v2)
{
"version": 2,
"projects": {
"<name>": {
"name": "...",
"path": "...",
"keywords": ["..."],
"description": "...",
"createdAt": "<ISO-8601 UTC>",
"updatedAt": "<ISO-8601 UTC>",
"repoType": "single-repo | monorepo | multi-monorepo",
"specialRules": ["..."],
"monorepoRoot": "..."
}
}
}
repoType, specialRules, and monorepoRoot MAY be absent on legacy v1 records. The skill consumes only name and path for routing — repoType is informational.
Version gate
version <= 2→ read normally.version > 2→ abort withunsupported registry version: <n>and exit non-zero. Do NOT touch the file.
Step 1 — Validate inputs
Apply the input validation listed above. If validation fails, abort with the exact error string and perform no scan or apply.
Step 2 — Resolve projects from the registry
- Resolve
REGISTRY_PATH=<HOME>/.claude/commander/projects.json. - Probe with
Bash test -f "<REGISTRY_PATH>".- If missing: print exactly
No projects registered. Use /commander:add to register one.and exit0. Do NOT create the directory or the file.
- If missing: print exactly
Readthe file and JSON-parse the contents.- On parse failure: print
registry file is not valid JSON, exit non-zero, do NOT touch the file.
- On parse failure: print
- Inspect
version:- If
version > 2: printunsupported registry version: <n>, exit non-zero, do NOT touch the file.
- If
- Inspect
projects:- If absent,
null, or an empty object: printNo projects registered. Use /commander:add to register one.and exit0.
- If absent,
- Iterate
projectsin JSON insertion order. Build the candidate listRESOLVED = [{ name, path, repoType? }, ...]preserving that order.
2.1 Apply projectsFilter (when provided)
- Compute
MATCHED = RESOLVED ∩ projectsFilterbyname. - For each name in
projectsFilternot present inRESOLVED, print one line:Filter name not found: <name>. - Replace
RESOLVED ← MATCHED. - If
RESOLVEDis empty after the filter: printNo projects matched the filter.and exit0.
2.2 Detect missing-path drift
For each record retained, run Bash test -d "<record.path>". If exit is non-zero:
- Append
{ name, path }to apathMissing[]buffer. - Drop the record from
RESOLVED.
The skill SHALL NOT abort on missing paths. The aggregated summary lists every dropped record under Skipped (path missing) (<N>):.
2.3 Pass-through legacy v1 records
Records lacking repoType are kept as-is. Do NOT synthesize the field, do NOT abort, do NOT mutate the registry.
2.4 Empty-after-resolution exit
If RESOLVED is empty after Step 2.2 (every retained record has a missing path), print:
No projects registered. Use /commander:add to register one.— when the registry was empty initially (the empty-registry / empty-projects-object branches in Step 2 already exited there; this branch is effectively dead unless Step 2.2 emptied a non-empty list).- Otherwise, print
No selectable projects after path-drift filtering.followed by everypathMissing[]entry as a- <name> — <path>bullet, and exit0.
Step 3 — Project subset selection (skipped when projectsFilter is set)
When projectsFilter is provided, skip this step entirely — the resolved set is already final.
When projectsFilter is unset and RESOLVED is non-empty, the interface depends on N = RESOLVED.length — AskUserQuestion caps a question at 4 options, so the project options + all only fit when N ≤ 3:
N ≤ 3— raise exactly oneAskUserQuestioncall configured as:multiSelect: true- One option per project, label =
<name> — <path>, description = a short hint (e.g.,repoType: <value>when present, elselegacy v1 record). - A final
alloption, label =All registered projects (<N>).
N ≥ 4— SHALL NOT attempt theAskUserQuestion(its option count would exceed the tool cap and the call fails with invalid parameters). Instead, use a free-form selection message: print the selectable projects as a numbered<name> — <path>list and ask for a comma-separated list of names, orall. Unknown names → print the valid list and re-prompt. Empty response → treat as zero selection.
Selection handling (both interfaces)
- Selecting
allis equivalent to selecting every individual project. - Selecting zero options → print
No projects selected. Cancelled.and exit0with no scan or apply. - Otherwise, set
RESOLVED ← <selected subset>(preserving registry insertion order, NOT the order the user clicked).
Step 4 — Parallel scan dispatch
level=engines: substitutedetect-toolchain-surfacesforexperiments:scan-npm-updatesin the agent prompt below (the agent returns anEngineSurfaceInventoryJSON, or{"_error": …}on abort). The CWD config (4.1), the JSON parse + project-tag plumbing (4.2), and the per-project map (4.3) are unchanged — anEngineSurfaceInventoryis tagged and stored exactly like aScanResult. See "Level-conditional routing:level=engines".
For the resolved project set, send a single message containing N Agent tool-uses (one per project). Each agent call:
-
No model override — the agent inherits the session model. Do NOT force a latency-optimized tier: the agent executes the full
scan-npm-updatesskill (not a JSON echo); a weaker tier returned a fabricated emptyScanResultin dry-run 2026-07-12, silently dropping every update for a project. -
subagent_type: "general-purpose". -
description: short, e.g.commander scan: <name>. -
prompt(verbatim, substituting<level>and<path>):You are scanning a single project for npm dependency updates as part of a cross-project Commander run. PROJECT_PATH: <absolute path from the resolved record> Tasks: 1. Change working directory to PROJECT_PATH. 2. Invoke the `experiments:scan-npm-updates` skill with `level: <level>`. 3. Return the resulting `ScanResult` JSON verbatim. CRITICAL OUTPUT FORMAT: your entire response MUST be a single JSON object (the `ScanResult`). No markdown fences (no ```json). No prose before or after. No explanations. The first character of your response MUST be `{` and the last character MUST be `}`. Nothing else. If the skill aborts on a precondition error (no package manager detected, no manifest, etc.), your entire response MUST be a single JSON object of the shape: {"_error": "<the precondition error string>"} Do NOT attempt to recover. Do NOT modify any file. Do NOT run any package-manager command outside what `scan-npm-updates` itself runs.
4.1 Per-agent CWD
The Agent tool-call MUST configure the agent's working directory at <record.path> so scan-npm-updates detects the project's local package manager (pnpm/npm/yarn/bun/deno) without drift.
4.2 Parse responses
Collect each agent's response and parse it as JSON:
- If parse succeeds and the JSON has shape
{ packageManager, repoType, updates, warnings }: this is aScanResult. Tag with the originating project'sname/path.- Empty-scan cross-check (
level ∈ {patch, minor, major}only; notengines): whenupdatesis empty, cross-check with one direct read-onlyncuinvocation (--jsonUpgraded, no--upgrade, no file writes) in that project before accepting the empty result. On mismatch, re-dispatch that project's scan agent once; if it still mismatches, mark the projectscan-failedwith reasonscan disagreed with ncu cross-check. An empty result the cross-check confirms is accepted normally.
- Empty-scan cross-check (
- If parse succeeds and the JSON has shape
{ "_error": "<string>" }: mark the project asscan-failed, store the error string for the summary, and exclude the project from aggregation and apply. - If parse fails (non-JSON or invalid JSON): same as
_errorabove, with a synthesized error stringAgent response was not valid JSON.
4.3 Build the per-project map
Combine successful results into ScanResultByProject = { [projectName]: ScanResult }. The scanFailed[] buffer holds { name, error } for every project that failed.
The skill SHALL continue processing other projects' results normally; a failure in one project never blocks the rest.
Step 5 — Cross-project aggregation
Aggregate ScanResultByProject into a CrossProjectPlan:
interface CrossProjectPlan {
packages: Array<{
name: string;
occurrences: Array<{
projectName: string;
currentVersion: string;
targetVersion: string;
location: string;
sourceFile: string;
skippedByReleaseAge?: boolean;
}>;
proposedTarget: string; // see Step 6
conflict: boolean; // see Step 6
}>;
warnings: string[]; // per-project warnings, prefixed with `<projectName>: `
scanFailed: Array<{ name: string; error: string }>;
pathMissing: Array<{ name: string; path: string }>;
}
Aggregation rules:
- Group every
updateacross every project by packagename(case-sensitive npm name). - Preserve insertion order: packages appear in the order of their first occurrence across the iteration of projects (same as registry insertion order).
- Each
occurrencecarries the full per-project context (projectName,currentVersion,targetVersion,location,sourceFile,skippedByReleaseAge). - Concatenate
warnings[]across every project'sScanResult, prefixing each with<projectName>:.
Step 6 — Version alignment
For each aggregated package, compute:
proposedTarget = max(occurrences[].targetVersion)(semver max). Strip leading^/~/=for comparison; preserve the prefix of the highest-versioned occurrence on output.conflict = truewhen at least one occurrence's declared range (currentVersioninterpreted as a range) does NOT admitproposedTarget. Range admission: standard semversatisfies(proposedTarget, currentVersion).
If any package.conflict === true, raise exactly one AskUserQuestion (regardless of how many packages conflict):
-
Question copy (verbatim, substitute the package list):
Cross-project version conflict for: <comma-separated conflicting package names>. At least one project's declared range does not admit the proposed maximum target. How should the run resolve every conflicting package? -
multiSelect: false -
Options:
use-max-where-possible— ApplyproposedTargetonly to occurrences whose range admits it; non-admitting occurrences keep their per-projecttargetVersion.per-project— Every occurrence retains its per-projecttargetVersion; no max-alignment for the conflicting packages.skip-package— Drop every conflicting package from the run entirely (their occurrences are removed from the plan).
The chosen policy applies to every conflicting package in the run. Do NOT prompt per-package.
Materialize the post-policy plan
Walk every package, applying the chosen policy to the conflicting subset:
use-max-where-possible: per occurrence, seteffectiveTarget = proposedTargetwhen admissible, elseeffectiveTarget = occurrence.targetVersion.per-project: per occurrence,effectiveTarget = occurrence.targetVersion.skip-package: drop the package; record names underSkipped by conflict policyfor the summary.
Non-conflicting packages always set effectiveTarget = proposedTarget for every occurrence (range admission already proven).
Step 6.5 — Cross-project research (deep mode only)
Fires only when mode === "deep". Inserted between Step 6 (version alignment) and Step 7 (dossier gate rendering). The shallow path SHALL skip this entire section.
This step composes three already-shipped pieces: experiments:group-packages-for-research, experiments:parallel-research-workflow (in cross-project mode), and — through the workflow's subagents — the fetch-changelog plugin executable. The orchestrator SHALL NOT advance the workflow's phases on its behalf; the workflow owns its own phase machine (0 → init → 1 → 2 → 3 → 4, through dossier synthesis).
6.5.1 Build the deduplicated package set
Convert the post-policy CrossProjectPlan.packages (output of Step 6) into a ScanResult.updates[]-compatible array — one record per unique package, NOT one per occurrence:
const dedupedUpdates = postPolicyPlan.packages.map((pkg) => ({
name: pkg.name,
targetVersion: pkg.effectiveTarget, // post-policy resolution from Step 6
currentVersion: mostCommonCurrentVersion(pkg.occurrences), // by occurrence count; ties broken by first-occurrence order
location: "cross-project", // synthetic; cross-project has no single location
sourceFile: "cross-project", // synthetic; cross-project has no single source file
}));
Sort the deduplicated set alphabetically by name (stable) — matches the Step 7 ordering shipped for the shallow rendering.
The deduplication is package-level. A package that appears in N projects produces exactly one updates[] record, not N. This is the deduplication contract MON-199 mandates: research keys off the unique package set, not off the (package, project) matrix.
Packages dropped by Step 6's skip-package conflict policy are NOT included in dedupedUpdates (they were already removed from postPolicyPlan.packages).
6.5.2 Group the deduplicated set
Invoke experiments:group-packages-for-research with { updates: dedupedUpdates } (and maxPerGroup only if the caller overrode it). Capture:
groups: Array<{ groupId, bucketKey, packages: [...] }>— input to the workflow.warnings: string[]— append each entry to the orchestrator's runningwarnings[]list for the Step 11 summary.
6.5.3 Synthesize a cross-project scanResult
parallel-research-workflow requires a scanResult input shaped like a ScanResult. Build one from the cross-project plan:
const crossProjectScan = {
packageManager: unionPackageManager(allProjects), // see below
repoType: "workspace", // cross-project is workspace-shaped by construction
updates: dedupedUpdates,
warnings: orchestratorWarnings, // the running list at this point
};
unionPackageManager rule: when every project in the resolved set shares the same package manager (every ScanResult.packageManager value identical across ScanResultByProject), use that value verbatim. Otherwise, use the literal string "mixed".
When the result is "mixed", append the warning Mixed package managers across selected projects: <comma-separated unique pm list, alphabetical>. to the orchestrator's running warnings[] list (surfaces in Step 11 and is persisted in crossProjectScan.warnings).
The workflow does NOT consume scanResult.packageManager for routing in cross-project mode — this field is informational only. It propagates into <plan-dir>/scan-by-project.json (which the orchestrator writes — see 6.5.5) for user inspection.
6.5.4 Invoke the workflow
Call experiments:parallel-research-workflow with:
{
groups: groups, // from Step 6.5.2
level: skillInput.level, // e.g. "patch"
scanResult: crossProjectScan, // from Step 6.5.3
mode: "cross-project", // mandatory for cross-project research contract
slugOverride: `commander-deep-${level}`, // e.g. "commander-deep-patch" — plan-dir slug
}
Capture the absolute <plan-dir> path the workflow returns. Inside this single invocation the workflow runs (the orchestrator does not advance any phase on the workflow's behalf):
- Phase 0 — stale-plan cleanup (pattern
^[a-z0-9-]+-(patch|minor|major|engines)-\d+(-\d+)?$matches cross-project plan-dirs, e.g.commander-deep-patch-1715693231). - Phase init — plan-dir creation under
~/.claude/experiments/plans/commander-deep-<level>-<unix-ts>[-N]/. - Phase 1 — batched parallel changelog fetch via the
fetch-changelogexecutable (sequential batches ofmaxConcurrent, parallel within a batch; hard-wall fallback prompt on dispatch denial). - Phase 2 — parallel research with the cross-project subagent prompt template (universal findings only, no codebase cross-reference).
- Phase 3 — mandatory integrity gate (
retry-failed/continue-without/abortif any group is non-healthy). - Phase 4 — dossier synthesis: the workflow's named synthesizer teammate writes
<plan-dir>/dossier.mdwith the cross-project template (chronology assembled by script) and the two-layer compliance check runs before the dossier is surfaced.
6.5.5 Persist per-project scans alongside the cross-project plan
After Step 6.5.4 returns (i.e. dossier.md exists at <plan-dir>/dossier.md), the orchestrator SHALL write two additional artifacts under the plan-dir (the workflow's contract delegates these two files to the cross-project caller):
<plan-dir>/scan-by-project.json— JSON object mappingprojectName→ the verbatim per-projectScanResultfrom Step 4 (ScanResultByProject).<plan-dir>/cross-project-plan.json— JSON object capturing the post-Step-6CrossProjectPlan(deduplicated package list with per-occurrence projection —projectName,currentVersion,targetVersion,location,sourceFile, plusproposedTarget,effectiveTargetper occurrence, and the resolved conflict-policy outcome).
Both files are pretty-printed (2-space indent). The workflow itself does NOT require these files for dossier synthesis (it consumes groups[] and the synthesized scanResult directly through its inputs); they exist for the user's post-hoc inspection and as the data source for dossier.md's affects projects: rendering plus the chronology script's cross-project representative versions. The orchestrator MAY write them before invoking the workflow (Step 6.5.4) or after — implementations SHALL write them by end of Step 6.5 regardless. Writing before the workflow runs is preferred because the workflow's phase 4 synthesis reads <plan-dir>/cross-project-plan.json for the bump-set table's projects (locations) cell and the chronology headers. When aligning versions deterministically, the orchestrator MAY use the plugin script node ${CLAUDE_PLUGIN_ROOT}/scripts/semver-max-wins.mjs --scan-by-project <plan-dir>/scan-by-project.json (max-wins effectiveTarget + most-common representative currentVersion per package).
6.5.6 Workflow early-exit handling
The workflow can return one of three abort signals. The orchestrator SHALL handle each before advancing to Step 7:
- Phase 0
cancel(Cancelled by stale-cleanup): print exactlyCancelled. No files modified.and exit0. Steps 7–11 SHALL NOT execute. No plan-dir is created for this run (phase 0'scancelshort-circuits before plan-dir creation). - Phase 1 hard-wall
abort: surface the workflow's abort message verbatim. Skip Steps 7–11 (no override prompts, no gate, no apply, no Step 10c cleanup invocation). The plan-dir IS preserved on disk per the workflow's contract; the orchestrator SHALL NOT re-invoke the workflow for cleanup on this path. - Phase 3 integrity-gate
abort: same as Phase 1 hard-wall — surface message verbatim, skip Steps 7–11, plan-dir preserved on disk.
For Phase 1 degrade-to-direct-synthesis (a non-abort outcome of the hard-wall prompt), the workflow proceeds to phase 4 and emits dossier.md with the degraded banner (research consolidated from the changelog cache). The orchestrator continues normally to Step 7 — the degraded path is NOT an early exit.
Step 7 — Render the cross-project bump set / dossier gate
Rendering branches on mode. Shallow mode generates the bump-set table inline from CrossProjectPlan. Deep mode references the workflow-produced dossier.md by path plus a bounded digest and appends the orchestrator-owned drift sections — it does NOT ingest the full dossier.
7.S — Shallow mode (mode === "shallow" or absent)
Render a single markdown table:
| package | proposed target | projects | locations |
| ------- | --------------- | -------------- | -------------------------- |
| lodash | ^4.17.22 | proj-A, proj-B | root, workspace:@scope/foo |
| ... | ... | ... | ... |
- Sort rows alphabetically by
name(stable). proposed targetreflects the post-policyeffectiveTarget. Whenuse-max-where-possibleproduced a split, render the per-project values with a slash separator (e.g.,^4.17.22 / ~4.17.21 (proj-B)) and append a footnote* per-project under conflict policy.projectslists project names where the package will be applied (deduplicated, comma-separated, registry insertion order).locationslists the uniquelocationstrings across those projects (deduplicated, comma-separated).- Append
Warnings:heading with each warning as a-bullet, whenwarnings[]is non-empty. - Append
Skipped (scan-failed) (<N>):heading listing project names + error, whenscanFailed[]is non-empty. - Append
Skipped (path missing) (<N>):heading listing<name> — <path>bullets, whenpathMissing[]is non-empty.
7.D — Deep mode (mode === "deep"): dossier gate rendering
When the workflow returned successfully in Step 6.5 (i.e. <plan-dir>/dossier.md exists at the plan-dir root):
-
Reference
<plan-dir>/dossier.mdby absolute path (so the user can open it) and surface a bounded digest of the dossier — NOT its full content. The digest comprises:- the
Cross-project bump settable (a bounded structured table; the workflow produced it from<plan-dir>/cross-project-plan.json— the orchestrator SHALL NOT regenerate it inline), - the improvement / workaround bullet titles with their
affects projects:tags, - the
## Skipped or unavailableentries, - section presence counts (e.g.
Breaking changes & migration: 4 items; Changelogs: 12 package blocks), plus any residual violations escalated by the workflow's two-layer compliance check.
The digest SHALL NOT include the
## Changelogsbodies or any full research content; the main conversation SHALL NOT ingest the full dossier (main-window context diet). The dossier's five H2 sections (in order:Improvements (universal — applicability checked per project at apply time),Workarounds resolved,Skipped or unavailable,Cross-project bump set,Changelogs;## Breaking changes & migrationfirst atlevel ∈ {major, engines}) live on disk for the user to open. - the
-
Append the orchestrator-owned drift sections after the digest, in this exact order. Each section is omitted when its count is zero:
**Warnings:**heading with each warning as a-bullet, when the orchestrator'swarnings[]list (the running list across Step 5, Step 6.5.2 grouping-skill warnings, Step 6.5.3 mixed-pm warning, and any later source) is non-empty.**Skipped (scan-failed) (<N>):**heading with<name>: <error>bullets, whenscanFailed[]is non-empty.**Skipped (path missing) (<N>):**heading with<name> — <path>bullets, whenpathMissing[]is non-empty.
These three sections are orchestrator-owned (they originate at Steps 2.2, 4.2, 5, and 6.5.3) — the workflow does NOT know about per-project scan failures or path-missing drift, so it cannot emit them in
dossier.md. The orchestrator MUST append them at Step 7 rendering time. -
(level=major only) Append the
## PR plan. Whenlevel === "major", invoke thepartition-breaking-changesskill and append its## PR plansection (ordered buckets + count-by-policy summary; the section name## PR planis a retained legacy name — see the deep-update artifact glossary carve-outs) after the drift sections. Build its inputs from already-available data:bumpSet= the rows of the dossier's## Cross-project bump set;breakingFindings= the per-package items under the dossier's## Breaking changes & migration;depGraph= a per-projectpeerDependencies+ import-site read (reuseScanResultByProject);overrideFamilies= the shipped registry families. The## PR planis advisory cross-project — v1 isolation is one worktree per project (Step 9.5), NOT per bucket. Forlevel ∈ {patch, minor, engines}this step SHALL NOT run (no## PR planis appended — output unchanged).
7.1 Empty-plan early exit
7.1.S — Shallow mode
If the post-policy plan has no apply-able packages (every package was scan-failed, path-missing, or skip-package-dropped):
- Print any warnings and the literal line
No <level> updates available across selected projects. - Exit
0with no apply, no install, no override execution.
7.1.D — Deep mode
If the workflow's dossier.md reports zero bumps (the Cross-project bump set table has no data rows) AND zero improvements (Improvements (...) section body is the _no improvements identified_ sentinel) AND zero workarounds (Workarounds resolved section body is the _no workarounds resolved_ sentinel):
- Print any orchestrator warnings (per the rules in Step 7.D, point 2 above).
- Print exactly
No <level> updates available across selected projects. - Exit
0without invoking Step 8, Step 9, Step 10a/10b/10c, or Step 11.
The plan-dir is preserved on disk; the workflow's end-of-flow cleanup runs separately when the next deep-mode invocation hits phase 0 stale-cleanup (>10 days). The orchestrator SHALL NOT delete the plan-dir on the empty-plan exit path.
Step 8 — Override registry consultation
level=engines: this entire step is SKIPPED (package-name overrides have no meaning for runtime/PM surfaces). TreatOVERRIDE_RUNandOVERRIDE_SKIPas empty and proceed to Step 9. See "Level-conditional routing:level=engines".
Mode-independent. Step 8 runs identically in both shallow and deep modes — same registry path default, same first-win matching, same run-override / skip-matched / force-generic prompt, same OVERRIDE_RUN / OVERRIDE_SKIP / GENERIC partitioning. This is Decision 5 in design.md: cross-project deep mode IS consulted for overrides (explicit divergence from single-project npm-update-deep-patch, which deliberately skips overrides). Rationale: in cross-project context, Storybook-style families spanning multiple projects need the same coordinated handling shallow already provides; degrading to "run shallow first, then deep" would defeat the one-command UX.
Resolve overrides using the apply-npm-updates override-resolution procedure (R1–R3) for registry load, first-win matching, and {version} resolution — the shared procedure, NOT an inline copy of the algorithm. The cross-project prompt (8.4) and the cross-project resolution scope stay owned by this skill.
8.1 Load + match + resolve (procedure R1–R3)
Invoke the procedure with the registry path from overrideRegistryPath (default claude-plugins/experiments/skills/scan-npm-updates/data/pkg-upgrade-overrides.yaml) and the resolution source set to the cross-project aggregated proposedTarget set (NOT per-project):
- R1 (load) — on a missing/unparseable registry the procedure prints
Override registry unavailable: <reason>. Proceeding without overrides.and treats it as empty. Do NOT abort. - R2 (first-win glob match) — over the post-policy plan's package names. Build
MATCHED_BY_ENTRY = { entry.id → [packages bound to this entry] }. - R3 (resolve + interpolate) — resolve
{version}against the cross-projectproposedTargetset:target-of:<name>→ theproposedTargetof the package whosename == <name>in the cross-project plan (prefix-stripped);max-target-of:<glob>→ the max semver acrossproposedTargetof packages whosenamematches<glob>(prefix-stripped);latest→ the literallatest; withfallbackVersionSourcefallback.{version}resolution SHALL run against the cross-project aggregate, never per-project. On an unresolvable entry the procedure warns, drops the entry, and its matched packages rejoin the generic flow. Otherwise the resolved version is interpolated intocommand.
8.4 Prompt once per matched entry across the run
For each remaining entry, raise exactly one AskUserQuestion:
-
Question copy (verbatim, substitute fields):
Override detected for {entry.id}. {entry.notes} Matched packages (across all projects): {comma-separated names}. Affected projects: {comma-separated project names where any matched package occurs}. Suggested command: {interpolated command}. {entry.reference ? "Reference: <url>" : ""} How do you want to handle this family across every affected project? -
multiSelect: false -
Options:
run-override— Execute the command once per affected project; skip generic ncu bump for these packages.skip-matched— Leave these packages untouched in every project; do not run the override and do not bump generically.force-generic— Ignore the override and bump these packages with the generic ncu flow in every affected project.
Record the chosen action per entry into OVERRIDE_ACTIONS: Map<entry.id, "run-override"|"skip-matched"|"force-generic"> along with the interpolated command.
8.5 Partition for apply
Compute three disjoint subsets per package:
OVERRIDE_RUN— packages bound to arun-overrideentry. The override command runs once per project that has at least one matched occurrence.OVERRIDE_SKIP— packages bound to askip-matchedentry. Excluded from everything.GENERIC— packages not bound to any entry, plus packages bound to aforce-genericentry.
If every package in the post-policy plan is in OVERRIDE_SKIP and OVERRIDE_RUN is empty, print All accepted updates were skipped by override policy. Nothing to apply. and exit 0 without touching files. (ACCEPTED is defined in Step 9; this short-circuit happens before the user confirmation gate.)
Step 9 — User confirmation gate
Raise exactly one AskUserQuestion. The option set depends on mode.
9.S — Shallow-mode options (three)
- Question copy:
Apply <level> updates across <N> project(s)? multiSelect: false- Options (in this exact order):
apply-all— Proceed with the entire (post-policy, post-override) plan.pick-subset— Accept a free-form package-name list to exclude before apply.cancel— Exit without modifying any file.
9.D — Deep-mode options (four)
- Question copy:
Apply <level> updates across <N> project(s)?(same as shallow) multiSelect: false- Options (in this exact order):
apply-all— Proceed with the entire (post-policy, post-override) plan, INCLUDING the post-bumps per-project changeset gate round (Step 10b).apply-bumps-only— Apply bumps + overrides + installs sequentially per project (Step 10a), but SKIP the changeset gate round (Step 10b) entirely. The Step 11 summary'sApplied improvementssection is omitted (zero items). Allrun-overridedecisions resolved in Step 8 still execute on this path because they were resolved before the gate.pick-subset— Accept a free-form selection of the items to APPLY, combining improvement-bullet titles AND package names. Substring match (case-insensitive) for improvements; exact match for bumps. Selected improvements are the only bullets in scope for the changeset gate round (Step 10b); selected packages are the only bumps applied in Step 10a; unlisted items are skipped (9.2.D).cancel— Exit without modifying any file. In deep mode the plan-dir IS preserved on disk and the orchestrator invokes Step 10c (end-of-flow cleanup) before exiting; in shallow mode there is no plan-dir.
9.1 apply-all
Let ACCEPTED = post-policy ∖ OVERRIDE_SKIP. Proceed to Step 10 (shallow) or Step 10a (deep).
9.2 pick-subset
9.2.S — Shallow pick-subset (package names only)
-
Compute
VALID_NAMES = unique names in the post-policy plan(post Step 6 conflict policy), then remove names inOVERRIDE_SKIP(post Step 8.5) — the resulting set is what the user can validly exclude. -
Ask the user (free-form message, no AskUserQuestion):
Enter package names to exclude (comma-separated or one per line). Empty response means exclude none. Valid names: {comma-separated VALID_NAMES} -
Parse the response by splitting on commas and newlines, trimming whitespace, removing empty tokens. Result:
EXCLUDED. -
If
EXCLUDED === []→ treat asapply-all. -
Validate every name in
EXCLUDEDis inVALID_NAMES. On any invalid:- Print
Unknown package name(s): {invalid names}. Valid names: {VALID_NAMES}. - Re-prompt step 9.2.S.2.
- Print
-
Let
ACCEPTED = post-policy \ {names in EXCLUDED} \ {names in OVERRIDE_SKIP}(set difference: drop both excluded and override-skipped packages —OVERRIDE_SKIPMUST NOT be re-included bypick-subset). LetSKIPPED_BY_USER = EXCLUDED. -
If
ACCEPTEDis empty after exclusion → printAll updates excluded; nothing to apply.and exit0without touching files. -
Otherwise proceed to Step 10.
9.2.D — Deep pick-subset (package names AND improvement titles)
Free-form selection over both improvement bullets and package bump names (mirrors single-project npm-update-deep-patch.md Step 6c).
-
Compute
VALID_BUMP_NAMES= unique names in the post-policy plan (post Step 6 conflict policy), then remove names inOVERRIDE_SKIP— same as shallowVALID_NAMES. -
Compute
VALID_IMPROVEMENT_TITLES= the leading title text of each-bullet under the## Improvements (universal — applicability checked per project at apply time)heading in the workflow'sdossier.md(a bounded titles-only read — no research bodies). The title is the prefix before the(group: ...; affects projects: ...)parenthetical — typically formatted as{package}: {opportunity description}or[{priority}] {package} — {opportunity}. -
Ask the user (free-form message, no AskUserQuestion):
Enter the IDs to apply (comma-separated or one per line). Use plan-line excerpts for improvements (case-insensitive substring match), package names for bumps. Empty response cancels. Improvements: {comma-separated VALID_IMPROVEMENT_TITLES} Bumps: {comma-separated VALID_BUMP_NAMES} -
Parse the response: split on commas and newlines, trim whitespace, drop empty tokens. Result:
SELECTIONS. -
Empty
SELECTIONS→ equivalent tocancel(Step 9.3). PrintCancelled. No files modified.and proceed to Step 10c cleanup + Step 11 summary (with the cancel section). -
For each token in
SELECTIONS, classify it as:- An improvement if it matches an entry in
VALID_IMPROVEMENT_TITLESvia case-insensitive substring (the token is a substring of a valid title). - A bump if it matches an entry in
VALID_BUMP_NAMESexactly (case-sensitive). - Unknown if it matches neither.
Classification MAY be delegated to the deterministic plugin script
node ${CLAUDE_PLUGIN_ROOT}/scripts/validate-subset.mjs(input{ selection, bumpNames, improvementTitles }; output{ bumpMatches, improvementMatches, unmatched }— matched tokens are the items to APPLY) — same semantics, no prose drift. - An improvement if it matches an entry in
-
On any unknown tokens:
- Print
Unknown selection(s): {invalid items}. Valid improvements: {VALID_IMPROVEMENT_TITLES}. Valid bumps: {VALID_BUMP_NAMES}. - Re-prompt step 9.2.D.3.
- Print
-
Compute:
ACCEPTED_BUMPS = post-policy ∖ OVERRIDE_SKIP, restricted to names in the bump-classified selections.ACCEPTED_IMPROVEMENTS = improvement bullets whose title text matches at least one improvement-classified selection (case-insensitive substring).SKIPPED_BY_USER = VALID_BUMP_NAMES \ ACCEPTED_BUMPS(package names the user chose to exclude).SKIPPED_IMPROVEMENTS_BY_USER = VALID_IMPROVEMENT_TITLES \ ACCEPTED_IMPROVEMENTS(improvement bullets the user chose to exclude — appear in Step 11Skipped improvementswith(excluded via pick-subset)).
-
If both
ACCEPTED_BUMPSis empty ANDACCEPTED_IMPROVEMENTSis empty → treat ascancel(Step 9.3). PrintCancelled. No files modified.and proceed to Step 10c + Step 11. -
Otherwise proceed to Step 10a with
ACCEPTED = ACCEPTED_BUMPS(Step 10a filters by name) and Step 10b withACCEPTED_IMPROVEMENTSas the in-scope bullets only.
9.3 cancel
Print exactly:
Cancelled. No files modified.
In shallow mode: exit 0 without touching files. Do NOT run any apply, install, or override command.
In deep mode: the plan-dir exists (Step 6.5 created it). Do NOT run any apply, install, or override command, but DO invoke Step 10c (end-of-flow cleanup) before rendering the Step 11 summary. The summary's H1 SHALL be the deep H1 (## commander-update-deep-<level> summary) and the summary SHALL contain a single body line Cancelled. No files modified. plus the always-rendered Suggested next steps section.
Step 9.5 — Optional isolation gate (default none, both modes)
After the Step 9 gate resolves to an apply path (apply-all / apply-bumps-only / pick-subset with a non-empty accepted set) and before Step 10, raise exactly one AskUserQuestion offering branch/worktree isolation. On Step 9 cancel, this step SHALL NOT run.
- Question copy:
Isolate these updates before applying? multiSelect: false- Options (in this exact order):
none— Apply each project in its current working tree (default; no VCS action — byte-equivalent to pre-isolation behavior).worktree— For each resolved project, create a branch + worktree viaupdate-isolation(worktrunk-preferred) and apply there; the project's current checkout stays untouched.branch— For each resolved project, create a branch in place viaupdate-isolationand apply on it.
Build ISOLATION_BY_PROJECT (consumed by Step 10.2/10.3):
none→ for every project,workdir = <record.path>, no VCS action.worktree/branch→ for each resolved project, invoke theupdate-isolationskill once with{ projectPath: <record.path>, branchName: "deps/<level>-<YYYY-MM-DD>", strategy: <worktree → "auto"; branch → "branch"> }; record the returnedworkdirandinstallAlreadyRan. v1 cross-project isolation is one worktree per project — the deep-major## PR planbuckets stay advisory; per-(project, bucket) worktrees are deferred.
update-isolation creates a branch/worktree only — it never commits, pushes, or opens a PR. On any update-isolation failure it degrades to none for that project (apply in place) with a surfaced note, never aborting the run.
Step 10 — Sequential apply (one project at a time, stop-on-fail)
The apply step splits by mode.
-
Shallow mode (
mode === "shallow"or absent): a single per-project bumps loop with no changeset gate round and no end-of-flow cleanup invocation. The existing 10.1–10.6 sub-steps apply unchanged. The orchestrator returns after Step 10.6 (or on stop-on-fail) and renders Step 11. -
Deep mode (
mode === "deep"): split into three phases:- Step 10a — Bumps loop (identical to shallow Step 10.1–10.6, except failure handling: a per-project failure pauses the run at the per-project failure gate — see 10.6.D).
- Step 10b — Per-project changeset gate round for improvements (conditional — see 10b's gating below).
- Step 10c — End-of-flow cleanup invocation (runs on every deep path except workflow-abort paths).
A Step 10a failure removes the failed and unattempted (
pending) projects from Step 10b; projects that DID apply bumps successfully still get their changeset gate round (see 10b's gating). Step 10c always runs (the plan-dir exists and the user deserves a cleanup decision).
Step 10a — Bumps loop (both modes; renamed from "Step 10" for shallow)
Iterate the resolved project set in registry insertion order (already preserved through Steps 2–9). For each project:
10.1 Compute the per-project subset
Collect occurrences in ACCEPTED whose projectName matches this project. Apply:
- The chosen conflict policy (Step 6) — drop occurrences for
skip-package-dropped packages; preserve per-projecteffectiveTargetunderper-project; honor partition underuse-max-where-possible. - The override partition (Step 8.5) — drop occurrences in
OVERRIDE_SKIP; routeOVERRIDE_RUNpackages to the apply spec'soverrideCommands; route everything else (GENERIC) to the apply spec'smanifestBumps/catalogEdits(built in Step 10.3). - The user exclusion (Step 9.2) — already excluded from
ACCEPTED.
If the per-project subset is empty (no generic occurrences AND no override entries touch this project), skip apply AND install for this project. Continue to the next.
10.2 Set the working directory
Resolve this project's apply directory from ISOLATION_BY_PROJECT (Step 9.5): WORKDIR = ISOLATION_BY_PROJECT[project].workdir — which is <record.path> under none, or the isolation branch/worktree path otherwise. For every Bash invocation in the apply for this project, prepend cd "<WORKDIR>" && (or use absolute paths for ncu's --packageFile). The skill SHALL NOT mutate the user's shell state across iterations.
10.3 Build the per-project apply spec and invoke apply-npm-updates
level=engines: invokeapply-engine-bumps(capabilityengine-update-apply) for this project instead ofapply-npm-updates. Pass{ cwd: WORKDIR, inventory: <this project's EngineSurfaceInventory from Step 4>, resolvedTargets: <the per-engine targets resolved once in Step 6>, ambiguousResolutions: <any ambiguous loci the user resolved before the gate>, confirmed: true }. Noncu, nomanifestBumps/catalogEdits/overrideCommandsspec. Fold the returned{ resolvedTargets, applied, skipped, droppedHashes, failure? }fragment into this project's summary entry; on a non-nullfailureapply Step 10.4/10.6 stop-on-fail exactly as below (thefailure.stepisresolve/writerather thanncu/catalog/override/install— surface itsdetailin the cross-project abort copy). The rest of 10.1–10.6 (subset, workdir, stop-on-fail) is unchanged. See "Level-conditional routing:level=engines".
The apply-npm-updates skill is the single source of truth for the per-project mechanical apply (generic ncu package.json bumps, catalog source edits — pnpm-workspace.yaml for pnpm, the root package.json for Bun — override commands, single install). The orchestrator builds the resolved spec for this project and invokes the skill once; it SHALL NOT restate the ncu / catalog / install recipe inline.
Build the spec from this project's subset (Step 10.1):
packageManager= this project'sScanResult.packageManager.cwd=WORKDIR(Step 10.2 —<record.path>undernone, else the isolation branch/worktree).target= the orchestrator'stargetinput (passed unchanged — thetarget → ncuTargetmapping,major→latest, and the exact-pin--removeRangewrite are owned byapply-npm-updates, the single source of truth).cooldown= the valuescan-npm-updatesresolved for this project (omit forpnpm). (This step runs only fortarget ∈ {patch, minor, major};target=enginesroutes toapply-engine-bumpsper the note above and never reaches this spec.)manifestBumps— one element per distinctGENERICpackage.jsonsourceFile:{ sourceFile, names: <GENERIC names for this file, space-separated>, includeFilter }. SetincludeFilter: truewhen any of: the user pickedpick-subsetand excluded ≥1 package for this project; any update for the file was removed byOVERRIDE_RUN/OVERRIDE_SKIP; or the conflict policy isuse-max-where-possibleand ncu's full set ≠ this project's effective subset. Otherwisefalse(ncu's own set equals the target set for this file). Additionally, whentargetismajor(it maps toncu --target latest),includeFilterSHALL ALWAYS betruefor every element regardless of the above — the per-projectnameslist is authoritative, preventing over-bumping dependencies thatscan-npm-updatesdeliberately excluded. (apply-npm-updatesalso forces the filter forlatest-mapped targets; this explicit set is defense-in-depth and keeps the spec readable. Thepatch/minorbranch is unchanged.)catalogEdits— one element perGENERICoccurrence whoselocationiscatalog:default/catalog:<name>(pnpmpnpm-workspace.yamlor Bun rootpackage.json):{ name, targetVersion: <effectiveTarget>, catalogSource: <the scan record's catalogSource> }. ThreadingcatalogSourceletsapply-npm-updatestarget the exact source node; omitting it falls back to the legacy pnpm default.overrideCommands— theOVERRIDE_RUNentries that touch this project, as{ id, command: <interpolated command> }, in declaration order (run once per affected project).skipInstall—truewhen every accepted package for this project went throughrun-overrideAND no generic ncu bump ran AND no catalog edit happened for this project (every override handles its own install); alsotruewhen Step 9.5'supdate-isolationreportedinstallAlreadyRanfor this project's worktree (a worktrunk hook already installed); otherwisefalse.
Invoke apply-npm-updates once with this spec and cwd: <record.path>, passing runDir: <plan-dir> in deep mode (shallow mode omits it; the skill logs to a temporary path). The skill redirects ncu / install / override stdout/stderr to an on-disk run log — one-line digests to the conversation, a bounded tail (≤ ~40 lines) on failure only, never verbatim streaming — and returns { appliedGeneric, appliedOverrides, installRan, logPath, failure }. Fold the returned fragment into this project's entry of the cross-project summary (Step 11).
10.4 On structured failure — format the cross-project failure copy
If apply-npm-updates returns a non-null failure, print the orchestrator-owned cross-project failure copy for the failing step (the skill never prints this copy — it only surfaces the bounded log tail), then apply the mode's failure handling (Step 10.6). In deep mode, replace each template's closing line Stopping the run. Subsequent projects not attempted. with the per-project failure gate (10.6.D) — whether subsequent projects are attempted is the user's
Truncated - read the full file at https://github.com/pabloimrik17/monolab/blob/0145297bd1f6b1dc2833d8e357e17b4d46f9351b/claude-plugins/experiments/skills/commander-update-orchestrator/SKILL.md.