marshall-steward

Project configuration wizard for planning system. Manages executor generation, health checks, build systems, and skill domains.

SKILL.md

GitHub

Marshall Steward Skill

Project configuration wizard for the planning system.

Exit-code convention for every script call

The exit-code contract for every python3 .plan/execute-script.py call in this document — of EVERY notation, not only manage-* — is stated once in tools-script-executor/standards/exit-code-convention.md; it is not restated here.

Usage

/marshall-steward                        # Interactive menu or first-run wizard
/marshall-steward --wizard               # Force first-run wizard
/marshall-steward upgrade                # Post-change reconciliation — asks before each stage
/marshall-steward upgrade integrate=true # Post-change reconciliation — runs all four stages end-to-end

Banner

At command start, emit the following banner verbatim to the user:

[ MARSHALL STEWARD ]
configure · verify · maintain

Enforcement

Execution mode: Run scripts exactly as documented; return to Main Menu after each operation.

Prohibited actions:

  • Do not invent alternative menu structures or options
  • Do not end without returning to menu (unless Quit)
  • Do not summarize what you are about to do instead of doing it
  • Do not improvise script execution; run exactly as documented

Constraints:

  • Bootstrap scripts use direct Python paths with glob
  • All other scripts use python3 .plan/execute-script.py {notation} ...
  • After any operation completes, return to Main Menu
  • Only exit when user selects "Quit"

What This Skill Provides

Wizard Mode: Sequential setup for new projects (executor generation, marshal.json init, build detection, skill domains)

Menu Mode: Interactive maintenance for returning users (regenerate executor, health check, configuration)


Scripts

Own Scripts (bootstrap-capable, run before executor exists)

Script Notation Purpose
determine_mode plan-marshall:marshall-steward:determine_mode Determine wizard vs menu mode; also exposes check-harness (active harness verification), check-working-prefixes (project.working_prefixes presence/drift), and check-staleness (health-menu executor/config staleness preflight)
configure_harness plan-marshall:marshall-steward:configure_harness Deterministic script-only local harness configuration (.plan/local/harness/{harness}.json)
gitignore_setup plan-marshall:marshall-steward:gitignore_setup Configure .gitignore for .plan/
upgrade plan-marshall:marshall-steward:upgrade Emit the four-stage upgrade verb plan (the stage plan is a pure function of (integrate, project_kind); the verb validates the serving executor against the newest plugin-cache version first, regenerating a merely-stale one only with --bootstrap true (a missing-or-broken executor is always restored), and reports a warning naming why the executor cannot serve current scripts — a skill/planner version split, with its remedy, or an observed import failure being restored from the newest cache — instead of a silently stale run); also exposes migrate-bot-lists, the idempotent one-shot auto-map of the retired enabled_bots knob onto required_bots / optional_bots driven as the Stage-2 migrate-bot-lists sub-step, and validate-bot-lists, the read-only report of configured reviewer tokens matching no registered bot kind driven as the Stage-2 validate-bot-lists sub-step. The plan's last Stage-2 sub-step, migrate-architecture-descriptors, has no subcommand here — it is driven through the manage-architecture verbs (see references/upgrade-flow.md)
cache_freshness plan-marshall:marshall-steward:cache_freshness Fail-closed three-valued plugin-cache freshness verdict (fresh|stale|unknown) driving the consumer Stage-1 cache-freshness-check sub-step
cache_retention plan-marshall:marshall-steward:cache_retention Union-keep plugin-cache retention sweep (dry run unless --apply) driving the Stage-1 cache-retention-sweep sub-step behind the cache-retention-prune nested gate
bootstrap_plugin (direct Python call) Detect plugin root, cache in .plan/local/marshall-state.toon

Delegated Scripts (require executor)

Script Notation Purpose
generate-executor plan-marshall:tools-script-executor:generate_executor Executor generation. Both surfaces (wizard Step 4 and maintenance "Regenerate Executor") detect whether they are running inside a git worktree (path under .plan/local/worktrees/) and, when so, pass --marketplace-root <worktree-absolute-path> so the generated executor's script mappings resolve against the worktree's marketplace/bundles/ instead of the main checkout or the plugin cache.
manage-config plan-marshall:manage-config:manage-config Project-level marshal.json CRUD
run_config plan-marshall:manage-run-config:run_config Clean temp, logs, archived-plans, memory
ci_health plan-marshall:tools-integration-ci:ci_health CI provider detection
permission_doctor plan-marshall:tools-permission-doctor:permission_doctor Permission analysis
permission_fix plan-marshall:tools-permission-fix:permission_fix Permission fixes
extension_discovery plan-marshall:extension-api:extension_discovery Extension config defaults
credentials plan-marshall:manage-providers:credentials External tool provider management

Prerequisites

The /marshall-steward command must locate bootstrap_plugin.py and detect the plugin root before loading this skill. bootstrap_plugin.py is the single deterministic resolver for every other bootstrap script path — locate it once, then route all post-get-root path lookups through its resolve verb instead of hand-globbing each script.

  1. Locate bootstrap_plugin.py (the one unavoidable glob). Resolve its path with the Glob tool against the layout-agnostic pattern **/*marshall-steward/scripts/bootstrap_plugin.py and capture the first match as ${BOOTSTRAP}. The pattern matches across all target layouts — Claude (skills/marshall-steward/…), Antigravity (skills/plan-marshall-marshall-steward/…), and OpenCode (skill/plan-marshall-marshall-steward/…) — without a hand-placed * version level.

  2. Detect the plugin root and cache it:

    python3 "${BOOTSTRAP}" get-root
    

    Read plugin_root from the TOON output and set ${PLUGIN_ROOT} to it. The plugin root is cached in .plan/local/marshall-state.toon for subsequent calls.

  3. Resolve every other bootstrap script path through bootstrap_plugin resolve — version-aware, layout-agnostic. For any script X.py under a bundle, run:

    python3 "${BOOTSTRAP}" resolve --bundle plan-marshall --path skills/{skill}/scripts/X.py
    

    and read resolved_path from the TOON. Never hand-glob a ${PLUGIN_ROOT}/plan-marshall/*/skills/… pattern to find a post-get-root script — resolve already iterates the version dirs deterministically.


Step 1: Determine Mode

Determine whether to run wizard or menu based on existing files.

BOOTSTRAP: Since execute-script.py may not exist yet, use a DIRECT Python call. Resolve the script path deterministically via bootstrap_plugin resolve (${BOOTSTRAP} was located in Prerequisites) and read resolved_path from the TOON as {DETERMINE_MODE}:

python3 "${BOOTSTRAP}" resolve --bundle plan-marshall --path skills/marshall-steward/scripts/determine_mode.py

Then invoke the resolved script directly:

python3 "{DETERMINE_MODE}" mode

Output (TOON):

status	success
mode	wizard
reason	executor_missing
harness	antigravity
target_source	env
harness_configured	false
harness_reason	harness_config_missing

Mode Routing

mode reason Action
wizard executor_missing Load: Read references/wizard-flow.md → Execute wizard
wizard marshal_missing Load: Read references/wizard-flow.md → Execute wizard
menu both_exist Check Harness Preflight, then show Main Menu below

Menu Mode Preflight (Harness Configuration)

When determine_mode returns mode: menu with harness_configured: false, marshall-steward automatically configures the active harness before presenting the menu:

python3 .plan/execute-script.py plan-marshall:marshall-steward:configure_harness

Emit a one-line status confirmation:

Configured local {harness} harness state (.plan/local/harness/{harness}.json).

Then proceed to the Main Menu.

Check for --wizard Flag

If --wizard flag provided, force wizard regardless of determine_mode result:

Read references/wizard-flow.md

Execute the wizard flow from that file.

Check for upgrade Verb

If the invocation carries the upgrade verb argument (optionally with integrate=true), bypass both the mode routing and the Main Menu entirely and run the upgrade flow directly:

Read references/upgrade-flow.md

Execute the upgrade flow from that file, passing the integrate value (true when integrate=true was given, otherwise false). Follow that reference's end-of-flow behavior when it completes.


Interactive Menu (Returning User)

Display menu when both executor and marshal.json exist.

Main Menu

The Main Menu has 7 options, which exceeds the AskUserQuestion 4-option cap. It is presented as a paginated menu following the "More actions..." pattern documented in plan-marshall/workflow/planning.md (§ Action: list): each page presents at most 4 options, and every non-final page reserves its 4th slot for a "More..." continuation that triggers the next page's AskUserQuestion. Page 2 carries 4 options and needs no further continuation.

Page 1 — first 3 options plus the "More..." continuation:

AskUserQuestion:
  question: "This project is already set up, so this is the maintenance menu rather than first-run setup. What would you like to do?"
  header: "Main Menu"
  options:
    - label: "1. Maintenance"
      description: "Rebuilds the generated command runner and clears out old logs, temporary files, and archived plans"
    - label: "2. Health Check"
      description: "Checks the setup end to end and reports anything broken, missing, or out of date"
    - label: "3. Configuration"
      description: "Change how this project builds, which standards apply, and the other project settings"
    - label: "More..."
      description: "Shows the four remaining entries: Effort, Pin models, Upgrade, and Quit"
  multiSelect: false

Page 2 — shown only when the user selects "More..." on Page 1 — the remaining options:

AskUserQuestion:
  question: "These are the four entries that did not fit on the first page. What would you like to do?"
  header: "More actions"
  options:
    - label: "4. Effort"
      description: "Choose how much model capability each kind of work gets — more for planning and review, less for routine steps"
    - label: "5. Pin models"
      description: "Materialize per-level model pins from the machine-local map — which model each level provisions on this machine"
    - label: "6. Upgrade"
      description: "Brings this checkout back in line after a plan-marshall change: rebuilds the runner, reconciles settings, verifies, and lands the result"
    - label: "7. Quit"
      description: "Ends this session, offering first to commit any settings changes it made"
  multiSelect: false

Menu Routing

User Selection Action
"1. Maintenance" Load: Read references/menu-maintenance.md → Execute
"2. Health Check" Load: Read references/menu-healthcheck.md → Execute
"3. Configuration" Load: Read references/menu-configuration.md → Execute
"More..." Present Main Menu Page 2 AskUserQuestion
"4. Effort" Load: Read standards/effort-menu.md → Execute
"5. Pin models" Load: Read references/menu-pins.md → Execute
"6. Upgrade" Load: Read references/upgrade-flow.md → Execute
"7. Quit" Output "Good bye!" → STOP

After any menu option completes, return to Main Menu Page 1 (except Quit).


Deferred Loading Pattern

This skill uses progressive disclosure to minimize context usage:

  1. Core skill loads: ~150 lines (this file - routing logic only)
  2. On wizard mode: Load references/wizard-flow.md (~250 lines)
  3. On menu selection: Load only the selected reference (~100-150 lines)

How to Load a Reference

When routing indicates to load a reference:

Read references/{file}.md

Then execute the workflow described in that file. Each reference file is loaded in full when its menu path is chosen — only one reference is active at a time.


Available References

Reference Purpose Load When
wizard-flow.md First-run wizard steps 1-15 (bootstrap 1-4, configuration 5 onwards) mode=wizard or --wizard flag
provider-setup.md Provider discovery/activation, CI detection, credential setup (extracted from wizard-flow.md) Linked from wizard-flow.md (provider/CI/credential steps)
architecture-setup.md Extension defaults, module discovery, build commands, Maven profiles, LLM analysis + architecture_refresh tier knobs (extracted from wizard-flow.md) Linked from wizard-flow.md Step 8
build-map-setup.md Build-map seed/read workflow — build.map file-to-build contract, write-once seed, menu re-seed operation Linked from wizard-flow.md Step 8b and menu-configuration.md (Project Structure)
skill-domains-setup.md Skill-domain configuration, profile activation, execute-task/recipe registration (extracted from wizard-flow.md) Linked from wizard-flow.md Step 9
menu-maintenance.md Regenerate executor, cleanup. The cleanup operation routes to the Action: cleanup workflow — see ../plan-marshall/workflow/planning.md § "Action: cleanup", the sole authority for what that pass does. Menu option 1
menu-healthcheck.md Verify setup, diagnose issues Menu option 2
menu-configuration.md Build systems, skill domains, architecture refresh tier knobs Menu option 3
standards/effort-menu.md Per-phase effort configuration (Effort submenu) Menu option 4
references/menu-pins.md Per-level model pin materialization (Pin models flow) Menu option 5
menu-recipes.md Built-in recipes available in the wizard Linked from menu-configuration.md
menu-derivation-resolvers.md Inspect and change which module-edge derivation resolvers run in this checkout; machine-local, keyed by resolver id, unconfigured means every discovered resolver is active Linked from menu-configuration.md (Derivation Resolvers)
menu-display-timezone.md Inspect and change the IANA zone operator-facing timestamps are rendered in; machine-local, display-only (storage and comparison stay UTC), unconfigured renders UTC Linked from menu-configuration.md (Display Timezone)
menu-commit-trailer.md Inspect and change the co-author identity assistant-authored commits are recorded under; machine-local, per-half fallback, unconfigured commits under plan-marshall <noreply@cuioss.de> Linked from menu-configuration.md (Commit Trailer)
merge-queue-setup.md Idempotent probe→ask→configure provisioning of the platform merge queue (GitHub merge queue / GitLab merge train) via the ci repo merge-queue verbs Linked from wizard-flow.md Step 13.5 and menu-configuration.md (Merge Queue)
landing-cycle.md End-of-run landing cycle: detect uncommitted plan-marshall artifact diff → offer to commit → push → skip-bot-review-labelled plan-less PR → merge-queue-aware merge → switch-to-main → pull; base-branch-conditional branch selection + bot skip-label honoring matrix Linked from the "End-of-Run Landing Cycle" hook (menu-mode Quit path + wizard-flow.md end)
upgrade-flow.md Post-change upgrade verb: Step-0 executor bootstrap from the newest plugin cache, then four-stage reconciliation driven by the project-kind-aware upgrade.py plan stage plan (the meta/consumer stage matrix — meta regenerates the target tree + executor and verifies with preflight + content-drift; consumer regenerates the executor only and verifies with preflight only; both reconcile config through reconcile-marshal-json, migrate-bot-lists, validate-bot-lists and migrate-architecture-descriptors), honoring each stage's per-stage gate dispositions and sub_steps Main Menu option 6, or the upgrade early verb check
error-handling.md Error types and recovery On error conditions

Build Server Status (read-only pointer)

The Health Check may surface the machine-global marshalld build server's status by running manage-build-server status and reporting the returned running / version / registered / binary_diverges fields — plus the note, whenever the payload carries one — to the operator:

python3 .plan/execute-script.py plan-marshall:manage-build-server:manage_build_server status

⛔ running: true with a version is NOT by itself a healthy daemon. Read binary_diverges before reporting health. A true value means the live process is executing a different binary from the one a fresh start would resolve today — an older pinned copy — so the daemon is stale and owes a reconcile: report it as owing a restart against the current copy, never as healthy. The accompanying note spells the divergence out (which path is running, which one would be resolved) and is the text to relay verbatim. The daemon is healthy only when running: true and binary_diverges: false; reporting the version alone would present accumulated drift as a clean status line.

A note also appears with binary_diverges: false when the running provenance could not be read at all — that note says so explicitly, and the resolved-now path is never substituted for it.

This is a read-only pointer only. Steward carries NO daemon lifecycle logic — enrolment (register / unregister) and control (start / stop / drain / install / upgrade) live exclusively in the user-invocable manage-build-server control skill. When the operator wants to start, stop, or enrol, direct them to /manage-build-server; steward never mutates registry or daemon state.

Phase 6 Finalize Step Seeding

The wizard seeds phase-6-finalize.steps in marshal.json from the default-on built-in finalize-step set discovered via extension_discovery.find_implementors — the SOLE finalize-step discovery path. Membership, execution order, and default-seed inclusion are declared in each step doc's frontmatter (implements: ...ext-point-finalize-step, order, default_on: true), NOT a hand-maintained constant list; see extension-api/standards/ext-point-finalize-step.md. The seed intentionally covers only steps that are sensible defaults for any plan-marshall consumer (pre-push-quality-gate, finalize-step-simplify, finalize-step-security-audit, push, create-pr, ci-verify, automated-review, sonar-roundtrip, lessons-capture, finalize-step-preference-emitter, branch-cleanup, record-metrics, finalize-step-print-phase-breakdown, archive-plan), ordered by their declared order. pre-push-quality-gate is a built-in default like the rest; its activation is derived from build.map — it activates whenever the live footprint touches a glob registered in the build_map. Those globs are tree-derived from each extension's classify_globs() vocabulary (complete-by-construction over the real tree), not author-shipped static literals.

Steps that are meta-project-only — e.g. running the multi-target generator and syncing the harness installs — are NOT default-on built-ins. They live as project-local skills under project skill roots (e.g. .claude/skills/finalize-step-{name}/SKILL.md, .agents/skills/finalize-step-{name}/SKILL.md, or .opencode/skills/finalize-step-{name}/SKILL.md) in the meta-project that needs them (discovered as project:finalize-step-{name} with default_on: false), and that meta-project's marshal.json registers them explicitly. Consumer projects don't see them and don't have them seeded.

Missing-default detection. When the wizard runs against an existing project, determine_mode.py compares the existing marshal.json::plan["phase-6-finalize"]["steps"] array against the discovered default-on built-in set (via extension_discovery.find_implementors). Any built-in step missing from the project's array is surfaced as missing_default_finalize_steps so the wizard can prompt the user to add it. This protects existing projects from quietly missing newly-added consumer-applicable defaults when their marshal.json predates the additions.

First-run lane materialization. At the end of the wizard (Step 16), sync-defaults deep-merges the full default finalize step-set into marshal.json before the step-sort, so the seeded pipeline becomes fully explicit — a newly-materialized default_on: false step arrives lane: off, growing the step count while leaving the effective running set unchanged (opt-in preserved). See references/wizard-flow.md Step 16 for the materialize-then-sort sequencing.

OpenCode Enforcement Apply (wizard Step 14b)

On opencode-target projects the wizard applies enforcement once per project: the two-tier permission block (D1) into the project's opencode.json and the guard plugin (D2) under .opencode/plugin/. The step is gated on runtime.target == opencode and applies at most once — a re-entry that finds both artifacts in place skips silently with a STEWARD audit entry and never re-prompts. Step position, gate, and singular-apply semantics live in references/wizard-flow.md Step 14b; upgrade.py stays a pure four-stage emitter over (integrate, project_kind) with no enforcement entry point.

Review-Bot Participation (required_bots / optional_bots)

The plan-marshall:automatic-review step classifies review bots into two lists — required_bots (silence is a failure; gates the completeness quorum) and optional_bots (silence is tolerable; never gates). A bot in NEITHER list is warned about but still ingested. The semantics, the ask posture, and the failure taxonomy are owned by ../automatic-review/standards/bot-participation-contract.md; the knob storage shape and the three-valued provenance are documented in ../manage-config/standards/data-model.md.

Wizard question. Both lists ship EMPTY, so the wizard MUST ask rather than assume. Build the option set from the live registry — bot_registry.bot_kinds() — never a hardcoded bot list, so a newly-registered bot appears automatically. Ask in two passes (required first, then optional over the bots not already chosen as required):

AskUserQuestion:
  question: "No automated reviewers are configured for this project yet, so nothing is currently required to look at a pull request before it merges. Which reviewers must always weigh in?"
  header: "Must review"
  options:                        # one per bot_registry.bot_kinds(), plus the none escape
    - label: "{bot_kind}"
      description: "Your pull request waits for {bot_kind}, and cannot merge until it has reviewed"
    - label: "None"
      description: "No reviewer is required; a merge is never held up waiting for an automated review"
  multiSelect: true
AskUserQuestion:
  question: "Those reviewers will now block a merge until they answer. Which of the remaining ones should still review, without ever holding a merge up?"
  header: "May review"
  options:                        # bot_registry.bot_kinds() minus the required selections
    - label: "{bot_kind}"
      description: "{bot_kind}'s comments are collected and shown to you, but a merge never waits for them"
    - label: "None"
      description: "Only the reviewers you just marked as required run; no other review comments are collected"
  multiSelect: true

Record an explicit answer even when the operator selects "None". Write the (possibly empty) value AND set bot_lists_provenance to answered:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-6-finalize step set \
  --step-id plan-marshall:automatic-review --param required_bots --value "{csv_or_empty}"
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-6-finalize step set \
  --step-id plan-marshall:automatic-review --param bot_lists_provenance --value answered

Recording answered is load-bearing: it is what keeps an operator's deliberate "no required bots" distinguishable from a never_asked key the wizard has simply not reached yet. Never collapse the two — a never-asked key is re-asked, an answered-empty one is not.

migrate-bot-lists (upgrade Stage 2). A project whose marshal.json still carries the retired enabled_bots key is auto-mapped by the migrate-bot-lists sub_step of Stage 2 (reconcile-config) in the upgrade flow:

python3 .plan/execute-script.py plan-marshall:marshall-steward:upgrade migrate-bot-lists

It seeds the standard roster (required_bots: cuioss-review-bot,coderabbit, optional_bots: sourcery), removes enabled_bots, and records provenance migrated. It does NOT copy the legacy value verbatim: each discarded bot is named in the discarded_legacy list so no name disappears silently, and any unknown name is surfaced by validate-bot-lists rather than copied. (Its sibling Stage-2 sub-step reconcile-marshal-json seeds the same standard roster through sync-defaults --backfill-bot-roster when the block was never answered — both lists otherwise have one default, EMPTY.) When the operator has ALREADY answered either new key, their answer WINS — only the stale legacy key is removed and both values are reported. The verb is idempotent and self-disarming: once the legacy key is gone it is a no-op success, so re-running upgrade is always safe.

When the wizard asks the question above on a project that was just migrated, pre-fill the migrated value as the default selection so the operator confirms or adjusts a real starting point rather than re-deriving it. Read the current value first:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-6-finalize step get \
  --step-id plan-marshall:automatic-review

validate-bot-lists (upgrade Stage 2). A configured token that matches no registered reviewer is its own failure — distinct from a registered reviewer that stayed silent, and fixed by editing the name rather than by chasing the reviewer. The validate-bot-lists sub_step of Stage 2 (reconcile-config) reaches that token at config-read time, before any pull request exists:

python3 .plan/execute-script.py plan-marshall:marshall-steward:upgrade validate-bot-lists

It reports and never rewrites. No token is dropped and no list is rejected — the fail-closed participation barrier already blocks on an unregistered name (see ../automatic-review/standards/bot-participation-contract.md for that taxonomy, which this skill does not restate), so refusing here would turn a one-token typo into an unstartable finalize while catching nothing the barrier misses. Its output names the live kind set the tokens were checked against — the set the corrected token must be chosen from — and the size of the population it actually checked, so a clean verdict over three configured tokens stays distinguishable from a clean verdict over none. See the Canonical invocations (upgrade validate-bot-lists) for the emitted fields and the noop states.

Architecture Descriptor Migration (upgrade Stage 2)

The migrate-architecture-descriptors sub_step of Stage 2 (reconcile-config) is the sanctioned home of the architecture-descriptor tool migration, for both project kinds. A plan's architecture-refresh finalize step commits only the plan-attributable part of a discover --force rewrite and leaves every migration class unwritten; this sub-step writes exactly the migration part (architecture discover --force --apply migration), gates it on descriptor-regression-check --pre-ref HEAD, and leaves it in the working tree so Stage 4 lands it on the plan-less steward PR with no commit of its own. It skips itself when .plan/project-architecture already carries uncommitted edits. The procedure and its per-verdict reporting are documented in references/upgrade-flow.md § "Sub-step migrate-architecture-descriptors"; the delta classes and verdicts it consumes are published in ../manage-architecture/standards/manage-api.md § discover.

Blocking-Finding Classification (fixed rule — no wizard seed)

The blocking-finding gate is governed by a fixed, hardcoded actionable-vs-knowledge rule in plan-marshall/scripts/_invariants.py — there is no per-phase configuration partition, no marshal.json key, and no wizard seed step. ACTIONABLE types (build-error, test-failure, lint-issue, sonar-issue, qgate, pr-comment) block when pending at a guarded boundary; KNOWLEDGE types (insight, tip, best-practice, improvement) never block. The wizard does not write any blocking-finding configuration. See plan-marshall:plan-marshall/references/phase-handshake.md § pending_findings_blocking_count resolution for the full rule.

Branch-Naming Surfacing (project.working_prefixes)

project.working_prefixes holds the canonical closed set of allowed working-branch prefixes as the transparent, operator-editable source of truth in marshal.json. It is seeded from DEFAULT_PROJECT['working_prefixes'] (defined in manage-config/scripts/_config_defaults.py) on init and back-filled into an existing marshal.json by sync-defaults. The default value is:

Key Default
working_prefixes ["feature/", "fix/", "chore/"]

The docs/ prefix is explicitly retired and must not be re-admitted — a docs/-prefixed branch gets no push-triggered verification run (the push: allowlist in .github/workflows/python-verify.yml excludes it), so its commits are never verified on push; CI sees the branch only through the PR / merge-queue path (which filters on the base branch, not the head). See the "Branch Naming" rule, owned by the CI push-trigger allowlist. The allowlist is owned by .github/workflows/python-verify.yml (not mirrored here); a structural test (test_branch_prefix_allowlist.py) asserts every working_prefix is covered by a workflow push trigger.

Missing-default / drift detection. When the wizard runs against an existing project, determine_mode.py check-working-prefixes compares the live marshal.json::project["working_prefixes"] list against DEFAULT_PROJECT['working_prefixes']. It surfaces missing when the key is entirely absent, or a drift signal when a default entry is missing, so the wizard can prompt the user to add or update it. This protects projects whose marshal.json predates the key, since sync-defaults is not auto-run in the interactive menu flow.

Idempotent and non-clobbering. The detection performs no writes. An operator's customized list — including a superset that adds prefixes beyond the defaults — is returned as ok and never flagged or overwritten; only genuine absence or a missing default entry is surfaced.

Wizard step (runs against an existing project to surface presence/drift):

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode \
  check-working-prefixes

Output (TOON) when the list is present and current (or operator-customized):

status	ok

When the working_prefixes key is absent:

status	missing
detail	absent
missing_keys	working_prefixes

When the key is present but a default entry has drifted out (e.g. chore/ dropped):

status	missing
detail	drift
missing_keys	working_prefixes

Re-Run Remediation Pass

When the steward runs in menu mode (both the executor and marshal.json already exist — an already-initialized project re-run/upgrade), it performs a six-step remediation pass at menu-mode entry, before the Main Menu — a sibling entry-time surface to "Branch-Naming Surfacing" and the missing-default finalize-step detection above. The pass repairs config drift that accumulated in projects initialized before the relevant fixes landed. The pass is not gated by any version check; it runs unconditionally on every menu-mode entry and is idempotent (an already-clean project is left byte-stable).

Steps (a), (b), (d), (e), and (f) are deterministic script calls that are silent by default — they run unconditionally and leave an already-normalized project unchanged. They surface nothing to the user EXCEPT for the documented warning conditions of steps (a), (d), and (e): step (a)'s unrecognized_keys warning when normalize-keys finds a top-level key it cannot order, step (d)'s session-restart warning when it regenerates the executor, and step (e)'s detect/warn advisory when the reconcile could not see the current config seed. Step (c) is an LLM-driven Y/N AskUserQuestion gate that consumes a deterministic diff, mirroring the existing entry-time check-working-prefixes / missing-default surfacing.

(a) Normalize marshal.json top-level key order (silent on a canonical file, unconditional). Re-write marshal.json with the canonical save_config key order. Pre-fix projects accumulated a non-canonical top-level key order; this re-orders them without touching values:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config normalize-keys

See the manage-config Canonical invocations (normalize-keys) for the verb shape. Its position in this pass is immaterial — every marshal.json write from the three reconcile verbs of this pass ((a) normalize-keys, (e) sync-defaults, (f) steps-sort) canonicalizes through save_config, so running it before or after steps (e) and (f) yields the same key order; it is sequenced first only for readability. The call is idempotent — an already-canonical file is left byte-stable. When it returns status: warning with a non-empty unrecognized_keys, surface that list to the operator: those top-level keys are absent from the canonical order and were preserved but appended out of position (a stray or consumer-added block), which normalize-keys cannot place and does not drop.

(b) Consolidate duplicate managed .gitignore blocks (silent, unconditional). Run the .gitignore setup script via the executor/bootstrap. Pre-fix projects accumulated multiple # Planning system (managed by /marshall-steward) managed-block headers (one per re-run); the consolidation pass merges them into a single managed block preserving the union of rules, and leaves an already-single-block file unchanged (status: unchanged):

python3 .plan/execute-script.py plan-marshall:marshall-steward:gitignore_setup

See the gitignore_setup Canonical invocations entry for the verb shape. The consolidation runs on every invocation; a clean file is byte-stable.

(c) build.map drift gate (read-only diff + interactive Y/N gate). Compute the drift between the persisted build.map and the live-tree derivation, then gate any re-seed behind an AskUserQuestion so deliberate hand-edits are never clobbered:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map drift

See the manage-config Canonical invocations (build-map drift) for the verb shape. The verb is read-only — it never mutates marshal.json. It returns in_sync plus the per-domain added_globs / removed_globs diff.

  • in_sync: true → no drift; continue to the Main Menu silently (no prompt).

  • in_sync: false → display the added/removed-glob diff to the user, then prompt:

    AskUserQuestion:
      question: "The record of which files belong to which build no longer matches what this source tree actually contains — the differences are listed above. Should that record be rebuilt from the tree?"
      header: "Build map"
      options:
        - label: "Yes, rebuild it (recommended)"
          description: "Replaces the record with one read straight from the current tree, so builds are chosen from what is really here"
        - label: "No, leave it alone"
          description: "Keeps the record exactly as it is — choose this if you edited it by hand and want those edits kept"
      multiSelect: false
    
    • Yes → re-seed via the force path:

      python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map seed --force
      
    • No → leave the persisted build.map untouched.

(d) Refresh the generated executor via generate_executor preflight (silent, unconditional). Run the executor-freshness preflight FIRST, before the sync-defaults reconcile below. When the executor's embedded MARSHALL_VERSION is older than the installed manifest's executor_changed_at_version, this regenerates the executor in place; otherwise it is a no-op reporting executor_action: fresh. Sequencing it ahead of sync-defaults is load-bearing: a version-stale executor resolves manage-config sync-defaults to a stale _cmd_sync_defaults.py, making the reconcile a silent no-op that never sees the current config seed. Regenerating the executor first guarantees the subsequent sync-defaults call — a fresh subprocess through .plan/execute-script.py — resolves through the current-version script:

python3 .plan/execute-script.py plan-marshall:tools-script-executor:generate_executor preflight

Safety follows the same generate_executor preflight rules described in "Executor & Config Staleness Signaling" below (ADR-002: the executor is per-tree derived state, never a user decision). When preflight reports executor_action: regenerated, the "Session Reload Directive After Executor / Agent Changes" guardrail below applies — surface the target-resolved session- reload directive because the emitted agent set may have changed.

(e) Refresh provisioning stamps via sync-defaults (silent, unconditional). Run the config deep-merge reconcile. It back-fills any missing default keys AND re-stamps the system.provisioned_version / system.config_seed_fingerprint provisioning fields, so a config-seed change made after the project was initialized is reflected in marshal.json. This is the config-reconcile step the check-staleness preflight advises the user to run when it reports marshal_status: stale:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config sync-defaults

See the manage-config sync-defaults command for the deep-merge + re-stamp contract. The call is idempotent — an already-current config is left byte-stable (it persists only when the merge added a key or a stamp changed). The refreshed system.provisioned_version is what determine_mode check-staleness compares against the installed dist-manifest.json's config_changed_at_version.

Detect/warn after (e) — because step (d) already guaranteed a current-version executor, a sync-defaults that reports added_count: 0 while the config is still stale is now anomalous rather than expected. Immediately after step (e), compare its added_count against a fresh determine_mode check-staleness marshal_status.

First gate on call success: the comparison below is evaluated ONLY when BOTH the sync-defaults call AND the fresh check-staleness call return status: success. A non-success status from either call must surface the failure to the user and skip the clean-pass path entirely — never infer success from marshal_status: fresh alone, because an error path does not guarantee a well-formed added_count field. Only once both calls have returned status: success do the three cases below apply, evaluated in order — the marshal_status: unknown gate is checked FIRST and short-circuits, so an unresolvable-manifest verdict is never swept into the marshal_status: fresh → continue silently branch even when added_count > 0:

  • marshal_status: unknown (evaluated first, regardless of added_count) → the installed dist-manifest.json could not be resolved, so version-based staleness cannot be determined — the preflight failed CLOSED. Surface the cannot-determine warning (echo the preflight warning field) telling the user freshness could not be substantiated, and advise verifying the install / a manual executor regeneration (Maintenance → Regenerate Executor) followed by a fresh /marshall-steward menu-mode entry. Do NOT report a clean silent pass in this case — an unknown verdict is not a fresh verdict.
  • added_count: 0 AND marshal_status: stale → surface a warning telling the user the reconcile could not see the current config seed even after the executor-freshness preflight, and advise a manual executor regeneration (Maintenance → Regenerate Executor) followed by a fresh /marshall-steward menu-mode entry. Do NOT report a clean pass in this case.
  • added_count > 0, OR marshal_status: fresh → the normal path; continue silently to the Main Menu, preserving the idempotent silent-on-clean behavior of steps (a)–(e).

(f) Sort phase-6-finalize.steps into frontmatter order (silent, unconditional). Re-sort the on-disk plan.phase-6-finalize.steps keyed-map into ascending frontmatter order. sync-defaults (step (e)) deep-merges any newly-added finalize step by appending it, so the operator-visible marshal.json drifts out of frontmatter order over time; this step restores the canonical order on disk, reusing the manifest composer's sort choke-point (no duplicated order table). It is sequenced LAST — after step (e)'s sync-defaults may have appended a step — so it corrects any freshly-appended step, and its potential reorder diff is picked up by the End-of-Run Landing Cycle's uncommitted-artifact detection:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config steps-sort

See the manage-config Canonical invocations (steps-sort) for the verb shape. The call is idempotent — an already-sorted map is left byte-stable (it persists only when the key order actually changed), and per-step values are preserved byte-identically. phase-5-execute.verification_steps is out of scope.

After the six steps settle, proceed to the Main Menu.

End-of-Run Landing Cycle

A steward run can leave uncommitted changes to plan-marshall artifacts — the Re-Run Remediation Pass alone may rewrite marshal.json (steps (a) normalize-keys, (e) sync-defaults, (f) steps-sort), and interactive configuration edits touch it too. The End-of-Run Landing Cycle is a single, uniform end-of-run hook that offers to land those changes so a steward pass does not silently leave the working tree dirty.

Uniform firing point. The hook fires at the natural END of every steward mode:

  • Menu mode — on the "Quit" path (Main Menu option 7), AFTER "Good bye!" is emitted and BEFORE the skill stops.
  • Wizard mode — at the end of the wizard flow (see references/wizard-flow.md), after the final configuration step completes.

Trigger. The hook runs the landing-cycle procedure only when the working tree carries an uncommitted diff — the Step 1 check is a plain whole-tree git -C {repo_root} status --porcelain that is NOT path-filtered; with no diff it is a silent no-op and the run ends normally. In practice the changes a steward run leaves uncommitted are always to tracked plan-marshall artifacts, but the check itself is unscoped over the whole working tree. The full procedure — diff detection, the land/leave AskUserQuestion gate, base-branch-conditional branch selection (create chore/{slug} on a base branch; confirm reuse of a non-base working branch), commit → push → skip-bot-review-labelled plan-less PR → merge-queue-aware merge → switch to the base branch → pull, and the bot skip-label honoring matrix — is documented in references/landing-cycle.md. Load and execute that reference when the hook fires.

Executor & Config Staleness Signaling

The steward surfaces executor/config staleness against the installed dist-manifest.json (emitted by the target generator) through the deterministic generate_executor preflight verb, wrapped by determine_mode check-staleness so the Health Check menu can run it as one of its checks. The verb applies two asymmetric ownership rules:

  • The executor is safe derived state (ADR-002). When the executor's embedded MARSHALL_VERSION is older than the manifest's executor_changed_at_version, the verb regenerates the executor in place and reports executor_action: regenerated; otherwise executor_action: fresh. Regeneration is safe because the executor is per-tree derived state, never a user decision.
  • marshal.json holds user decisions and is never auto-mutated. Config-seed staleness (system.provisioned_version older than the manifest's config_changed_at_version) is reported advisory-only as marshal_status: stale; the steward routes the user to a config reconcile rather than silently rewriting their config.

A fresh install with no manifest resolves both changed_at values to the empty sentinel, so nothing is stale and the verb is a no-op reporting fresh.

Health-menu entry. The Health Check menu runs the staleness preflight via:

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-staleness

Output (TOON) when the executor and config are both current:

status	success
executor_action	fresh
marshal_status	fresh
installed_version	<version>
executor_version	<version>
marshal_version	<version>

When marshal_status is stale, advise the user to run a steward config reconcile — the Re-Run Remediation Pass steps (d)-(e) above refresh the executor and provisioning stamps (see those steps and their detect/warn conditional for the sequencing rationale and warning mechanics), so re-entering /marshall-steward in menu mode normally clears the advisory. The exception is the detect/warn path (added_count: 0 AND marshal_status: stale), where the reconcile could not see the current config seed even after the executor-freshness preflight — there, a manual executor regeneration (Maintenance → Regenerate Executor) is required before a fresh menu-mode entry clears it. When executor_action is regenerated, surface the session-reload directive (see "Session Reload Directive After Executor / Agent Changes" below) because the emitted agent set may have changed.

Session Reload Directive After Executor / Agent Changes

CRITICAL — Reload the session's plugin set before dispatching against newly-emitted agents or notations. Claude Code's agent registry is session-pinned at session start: it scans the plugin cache exactly once when the session boots and never re-scans mid-session. Any steward operation that materially alters the agent set — executor regeneration that adds new notations, a run of the project-local, meta-repo-only /sync-harnesses command that emits new execution-context-{level} variants from the dynamic-level executor extension point — produces files the already-running session cannot see. Dispatching against a freshly emitted variant from the same session fails with Agent type 'plan-marshall:execution-context-{level}' not found even though the file exists on disk in the cache.

Operational guardrail: after running steward operations from the Maintenance menu (Regenerate Executor) or the wizard's executor- generation step that regenerated the executor OR changed the agent set, resolve the harness-appropriate reload directive through the platform-runtime seam and surface it verbatim to the user:

python3 .plan/execute-script.py plan-marshall:platform-runtime:platform_runtime session reload-directive

On Claude the directive is /reload-plugins, which refreshes the session-pinned registry live — only registered monitors force a full session restart, and plan-marshall registers none. On Antigravity or OpenCode the seam returns a no-op (Antigravity automatically discovers updated plugins in ~/.gemini/config/plugins/ upon /sync-harnesses --target antigravity, while OpenCode requires a session restart). The WHY rationale (registry is session-pinned at startup) is unchanged and is documented at the sister surfaces — /sync-harnesses, variant_emitter.py, and ext-point-dynamic-level-executor.md — and MUST stay convergent across all four surfaces.

Artifact Landing Cycle

When a plan's deliverables touch marshall-steward-owned artifacts — executor regeneration (.plan/execute-script.py), marshal.json migrations, or the harness sync — those changes already commit to the governing plan's feature branch and ship as part of its normal phase-6-finalize PR. Whether they ride that PR or split into their own is decided by the same project-wide pr_strategy policy every PR-opening surface consults. Call the decision verb with the landing cycle's changed-file count and branch on its verdict:

python3 .plan/execute-script.py plan-marshall:manage-config:manage-config project pr-decision \
  --changed-files N

Ride the plan's finalize PR on decision: ride; split into a separate PR on decision: split. Reference the verb by its canonical invocation (see manage-config Canonical invocations → project pr-decision) rather than restating the ceiling comparison. This documents already-implicit behaviour — steward artifacts already land on the plan's feature branch — now decided through the verb. For the ad-hoc (non-plan) counterpart of this rule, see persona-plan-marshall-agent agent-behavior-rules.md § "Ad-hoc changes still get the full PR flow".

Architecture Refresh Tier Knobs

The wizard and the maintenance Configuration submenu both expose two architecture_refresh tier knobs that drive the phase-6-finalize architecture-refresh step. The canonical schema, defaults, and value contract are owned by plan-marshall:manage-run-config (see manage-run-config/standards/run-config-standard.md and the architecture-refresh get-tier-0/get-tier-1/set-tier-0/set-tier-1 subcommands documented in manage-run-config/SKILL.md).

Knob Subcommand Default Allowed values
architecture_refresh.tier_0 manage-run-config architecture-refresh set-tier-0 --value {value} enabled enabled, disabled
architecture_refresh.tier_1 manage-run-config architecture-refresh set-tier-1 --value {value} prompt prompt, auto, disabled

Surfaces inside this skill:

Surface Reference Section
First-run wizard references/architecture-setup.md Architecture Refresh Tier Knobs (reached via wizard-flow.md Step 8)
Maintenance menu (returning users) references/architecture-setup.md Architecture Refresh Tier Knobs (reached via Configuration → Full Reconfigure, which re-runs the wizard from Step 5 onwards)

Both surfaces share the same architecture-setup.md tier-knob question set, and both delegate persistence to the manage-run-config architecture-refresh set-tier-* subcommands — this skill never edits run-config.json directly.


Built-In Recipes

The steward exposes the following built-in recipes (registered via provides_recipes() in plan-marshall-plugin/extension.py). Recipes are loaded by phase-3-outline when a plan's status metadata sets plan_source=recipe and recipe_key=<key>.

Recipe key Recipe skill Default change_type Scope
refactor-to-profile-standards plan-marshall:recipe-refactor-to-profile-standards tech_debt codebase_wide
lesson_cleanup plan-marshall:recipe-lesson-cleanup derived from lesson kind (see below) single_lesson

lesson_cleanup derived change_type:

Lesson kind change_type
bug bug_fix
improvement enhancement
anti-pattern tech_debt

The lesson_cleanup recipe is auto-suggested by phase-1-init Step 5c when source == lesson and the lesson body is doc-shaped (no code-touching fences, no code-action verbs as primary subject). See references/menu-recipes.md for the wizard-facing description and marketplace/bundles/plan-marshall/skills/recipe-lesson-cleanup/SKILL.md for the recipe contract.

Note: shared-doc-check.md content has been inlined into wizard-flow.md and menu-maintenance.md. For TOON output format, see plan-marshall:ref-toon-format.


Error Handling

If an error occurs during execution:

Read references/error-handling.md

Apply the recovery guidance for the specific error type.

Canonical invocations

The canonical argparse surface for the six entry-point scripts this skill registers: determine_mode.py, bootstrap_plugin.py, gitignore_setup.py, upgrade.py, cache_freshness.py, and cache_retention.py. The plugin-doctor missing-canonical-block rule checks that this section is PRESENT, matching its heading only — the body is never read; manage-invocation-invalid derives its accept-set from a live --help walk rather than from this section. Consuming docs xref this section by name instead of restating the command inline. See pm-plugin-development:plugin-script-architecture cross-skill-integration.md § "Script invocation in documentation".

determine_mode — mode

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode mode [--plan-dir PLAN_DIR]

determine_mode — check-docs

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-docs [--project-root PROJECT_ROOT]

determine_mode — fix-docs

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode fix-docs [--project-root PROJECT_ROOT]

determine_mode — check-structure

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-structure [--plan-dir PLAN_DIR]

determine_mode — check-missing-finalize-steps

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-missing-finalize-steps [--plan-dir PLAN_DIR]

determine_mode — check-staleness

python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-staleness

bootstrap_plugin — get-root

python3 .plan/execute-script.py plan-marshall:marshall-steward:bootstrap_plugin get-root [--refresh]

bootstrap_plugin — resolve

python3 .plan/execute-script.py plan-marshall:marshall-steward:bootstrap_plugin resolve --bundle BUNDLE --path PATH

gitignore_setup

python3 .plan/execute-script.py plan-marshall:marshall-steward:gitignore_setup [--project-root PROJECT_ROOT] [--dry-run]

upgrade — plan

python3 .plan/execute-script.py plan-marshall:marshall-steward:upgrade plan [--integrate {true|false}] [--project-kind {auto|meta|consumer}] [--bootstrap {true|false}]

Validates the serving executor against the newest plugin-cache version and reports its currency BEFORE emitting the stage plan, so a stale executor is caught before it resolves every downstream stage to stale scripts. Whether it may REWRITE a merely-stale executor is gated on --bootstrap, which defaults to false: a plain run reports action: regenerate_required and leaves the executor untouched, so integrate=false never rewrites it unasked; pass --bootstrap true to authorise the in-place regeneration. A missing-or-broken executor is always restored from the newest cache (see references/upgrade-flow.md Step 0). Emits the bootstrap block (action, executor_version, newest_cache_version, warning) alongside the stage plan: a non-empty warning names why the executor cannot serve current scripts — a skill/planner version split, with its remedy (re-sync the plugin cache, reload the plugin set, re-request the plan), or an observed import failure being restored from the newest cache. A warning MUST be surfaced before driving any stage, and a split's remedy MUST be followed.

upgrade — migrate-bot-lists

python3 .plan/execute-script.py plan-marshall:marshall-steward:upgrade migrate-bot-lists

Takes no arguments and operates on the live marshal.json. Driven as the migrate-bot-lists sub-step of upgrade Stage 2; idempotent and self-disarming, so a re-run after the legacy enabled_bots key is gone is a no-op success.

upgrade — validate-bot-lists

python3 .plan/execute-script.py plan-marshall:marshall-steward:upgrade validate-bot-lists

Takes no arguments and READS the live marshal.json — it reports and never rewrites, so it is safe to run at any point. Driven as the validate-bot-lists sub-step of upgrade Stage 2. Emits state (clean | unknown_tokens | noop), unknown_tokens (the configured names no registered reviewer answers to, de-duplicated across both lists), known_bot_kinds (the live registry kind set the tokens were checked against — the remedy for an unknown token, not decoration) and checked_count (the size of the population actually checked).

⛔ Read state first. The two noop states — no marshal.json, and no plan-marshall:automatic-review step — carry NO checked_count, and that absence is the contract: a 0 published by a run that never looked is indistinguishable from a genuine "checked, nothing configured", so a caller branching on the count finds no key rather than a false zero.

cache_freshness — check

python3 .plan/execute-script.py plan-marshall:marshall-steward:cache_freshness check [--cache-root CACHE_ROOT]

Emits freshness (fresh | stale | unknown), refuses_upgrade, cache_version, manifest_version, compared_against, cache_root, manifest_path, remediation and warning. compared_against names the comparison scope the verdict rests on — this verb has no upstream leg, so report a fresh verdict WITH that scope attached rather than as an unqualified currency claim.

cache_retention — sweep

python3 .plan/execute-script.py plan-marshall:marshall-steward:cache_retention sweep [--apply] [--cache-root CACHE_ROOT] [--project-root PROJECT_ROOT]