Project configuration wizard for planning system. Manages executor generation, health checks, build systems, and skill domains.
Project configuration wizard for the planning system.
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.
/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
At command start, emit the following banner verbatim to the user:
[ MARSHALL STEWARD ]
configure Β· verify Β· maintain
Execution mode: Run scripts exactly as documented; return to Main Menu after each operation.
Prohibited actions:
Constraints:
python3 .plan/execute-script.py {notation} ...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)
| Script | Notation | Purpose |
|---|---|---|
| determine_mode | plan-marshall:marshall-steward:determine_mode |
Determine wizard vs menu mode; also exposes check-working-prefixes (project.working_prefixes presence/drift) and check-staleness (health-menu executor/config staleness preflight) |
| 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 (pure function of (integrate, project_kind)); 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 |
| 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 |
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.
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.
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.
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.
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):
mode wizard
reason executor_missing
| 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 |
Show Main Menu below |
--wizard FlagIf --wizard flag provided, force wizard regardless of determine_mode result:
Read references/wizard-flow.md
Execute the wizard flow from that file.
upgrade VerbIf 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.
Display menu when both executor and marshal.json exist.
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
| 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).
This skill uses progressive disclosure to minimize context usage:
references/wizard-flow.md (~250 lines)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.
| 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: 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 |
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.
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 pushing the host plugin cache β 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.
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.
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 required_bots from the legacy value VERBATIM (every bot on the old list was awaited, and
awaiting is exactly what required means), leaves optional_bots empty, removes enabled_bots,
and records provenance migrated. 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.
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.
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.
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
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.
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:
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.
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:
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.
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
/sync-plugin-cacherun that emits newexecution-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 withAgent type 'plan-marshall:execution-context-{level}' not foundeven 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-directiveOn 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 ano-op(Antigravity automatically discovers updated plugins in~/.gemini/config/plugins/upon/sync-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-plugin-cache,variant_emitter.py, andext-point-dynamic-level-executor.mdβ and MUST stay convergent across all four surfaces.
When a plan's deliverables touch marshall-steward-owned artifacts β executor regeneration (.plan/execute-script.py), marshal.json migrations, or the plugin-cache 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".
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.
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.mdcontent has been inlined intowizard-flow.mdandmenu-maintenance.md. For TOON output format, seeplan-marshall:ref-toon-format.
If an error occurs during execution:
Read references/error-handling.md
Apply the recovery guidance for the specific error type.
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".
python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode mode [--plan-dir PLAN_DIR]
python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-docs [--project-root PROJECT_ROOT]
python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode fix-docs [--project-root PROJECT_ROOT]
python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-structure [--plan-dir PLAN_DIR]
python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-missing-finalize-steps [--plan-dir PLAN_DIR]
python3 .plan/execute-script.py plan-marshall:marshall-steward:determine_mode check-staleness
python3 .plan/execute-script.py plan-marshall:marshall-steward:bootstrap_plugin get-root [--refresh]
python3 .plan/execute-script.py plan-marshall:marshall-steward:bootstrap_plugin resolve --bundle BUNDLE --path PATH
python3 .plan/execute-script.py plan-marshall:marshall-steward:gitignore_setup [--project-root PROJECT_ROOT] [--dry-run]
python3 .plan/execute-script.py plan-marshall:marshall-steward:upgrade plan [--integrate {true|false}] [--project-kind {auto|meta|consumer}]
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.
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.
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.
python3 .plan/execute-script.py plan-marshall:marshall-steward:cache_retention sweep [--apply] [--cache-root CACHE_ROOT] [--project-root PROJECT_ROOT]