Resources
2Install
npx skillscat add cuioss/plan-marshall/manage-config Install via the SkillsCat registry.
Manage Config Skill
Manages project-level infrastructure configuration in .plan/marshal.json.
Scope: hybrid means this skill manages project-level settings (marshal.json persists across plans) while also providing plan-phase-specific configuration (branching, commit strategy, verification steps).
Enforcement
Base contract: See manage-contract.md for shared enforcement rules, TOON output format, and error response patterns.
Skill-specific constraints:
- Do not bypass initialization (marshal.json must exist before queries)
- Domain configuration follows the noun-verb pattern documented in the API Reference
- Phase configuration uses the
plan {phase} {verb}pattern - Every addition, removal, relocation, or rename of a config field MUST satisfy the governance rules in standards/config-design-principles.md — ownership boundaries (Rule 1 foreign-system, Rule 2 meta-project convention), placement (Rule 5), anti-speculation (Rule 6), and the lossless field-migration mechanics (Rule 3).
Workflow: Initialize Configuration
Pattern: Script Automation
Initialize marshal.json with defaults.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config initWorkflow: Sync Defaults
Pattern: Script Automation
Non-destructively merge any keys present in get_default_config() but missing
from the live .plan/marshal.json into the file, without overwriting existing
user values. This is the canonical migration path after a default-shape change
(new schema rows added to a DEFAULT_* block): existing projects never re-runinit, so sync-defaults is how a live marshal.json picks up new defaults
while preserving every operator override.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config sync-defaultsContract (non-destructive deep merge):
- A key already present in the live config is preserved unchanged. "Present"
means "key exists" — value comparison is NOT performed, so a user-setfinal_merge_without_asking: true(nested understeps['default:branch-cleanup'])
survives even when the default isfalse. - Exception — provisioning stamps:
system.provisioned_versionandsystem.config_seed_fingerprintare the two runtime-stamped provisioning fields
(written bystamp_provisioning_fields()at bothinitandsync-defaultstime,
NOT part ofget_default_config()). Unlike every other already-present key,
neither follows the key-exists preservation rule above — but their override
conditions differ:config_seed_fingerprintis re-stamped unconditionally
on everysync-defaultsrun, whileprovisioned_versionadvances only whenread_provisioned_version()returns a real (non-empty) version; on an empty
read (an unstamped/absent executor) any pre-existingprovisioned_versionis
left intact rather than blanked, so a transient read failure never erases a
known-good stamp. A livemarshal.jsontherefore always reflects the
currently provisioned config-seed fingerprint, and the most recently confirmed
executor version, after a steward reconcile. Seemarshall-steward/SKILL.md
config-reconcile step. - Nested dicts are merged recursively, so a deeply-nested missing sub-key
(e.g. theauto_rebase_thresholdparam underplan.phase-6-finalize.steps['default:branch-cleanup']when that step's param
object exists but the param does not) is added without disturbing siblings. - Lists are atomic: a present list is kept verbatim; only an absent list key is
seeded from defaults. - The merge is idempotent — re-running immediately produces an empty
added[].
Retired-step-key migration (runs BEFORE the deep-merge):
sync-defaults first migrates any retired step key in the two keyed-map step
containers — plan.phase-5-execute.verification_steps andplan.phase-6-finalize.steps — to its canonical id via the explicitRETIRED_STEP_KEY_RENAMES table. The table is the single, extensible rename set;
its first (and currently only) entry maps both the built-in-prefixed and the bare
legacy review-step forms — default:automated-review and automated-review — to
the promoted bundle:skill canonical plan-marshall:automatic-review. Future
renames add rows to the same table.
Migration semantics (idempotent, knob-preserving):
- Rename in place when the canonical is absent: the canonical takes the retired
key's position and its nested knob block byte-identically, so both the params and
the surrounding insertion order are preserved. - Drop the retired duplicate when the canonical is already present: the
canonical's own knob block wins; no duplicate (double-review) step is produced. - A second run reports no renames (idempotent).
Running the migration before the deep-merge is load-bearing: with the canonical
already present, the deep-merge does not re-add the default canonical alongside a
surviving retired key, so a stale-key config never yields a double review step.
Output (TOON):
status: success
added[3]:
- plan.phase-5-execute.per_envelope_budget_tokens
- plan.phase-6-finalize.steps.default:branch-cleanup.auto_rebase_threshold
- project.default_base_branch
added_count: 3
renamed[1]:
- plan.phase-6-finalize.steps.default:automated-review -> plan-marshall:automatic-review
renamed_count: 1
migrated[1]:
- plan.phase-6-finalize.steps.default:branch-cleanup.run_at_all -> lane
migrated_count: 1
materialized[2]:
- plan.phase-6-finalize.steps.default:push.lane=minimal
- plan.phase-6-finalize.steps.plan-marshall:automatic-review.lane=off
materialized_count: 2added[] lists the dotted paths of every newly-added key; added_count is its
length. An empty added[] (with added_count: 0) means the live config already
carried every default. renamed[] lists each migrated retired key as a
human-readable dotted-path string (... -> {canonical} for a rename in place, or... (dropped duplicate of {canonical}) for a dropped duplicate); renamed_count
is its length, and an empty renamed[] means no retired key was present.migrated[] lists each legacy run_at_all finalize-step key rewritten to the
unified lane knob; migrated_count is its length. materialized[] lists eachplan.phase-6-finalize.steps entry whose lane was made explicit by the
materialization pass — a pre-existing lane-less step annotated with its resolved
frontmatter-class effective lane (...=minimal / ...=standard), a freshly-merged
default step annotated with ...=off (opt-in); materialized_count is its length,
and an empty materialized[] means every RESOLVABLE lane-less finalize step already
carried an explicit lane (idempotent re-run). A lane-less step whose frontmatter
lane cannot be resolved to a concrete lattice tier — an external bundle:skill step,
or one whose source doc is missing or declares no lane: block — is deliberately left
untouched and is NOT reported in materialized[]. The
config is persisted whenever added[], renamed[], or the provisioning stamps
changed.
Workflow: Sort Finalize Steps
Pattern: Script Automation
Physically re-sort the on-disk plan.phase-6-finalize.steps keyed-map into
ascending frontmatter order sequence. sync-defaults deep-merges newly-added
steps by appending them, so the operator-visible marshal.json drifts out of
frontmatter order over time; the manifest composer already corrects this inside
the plan-local manifest (_sort_steps_by_frontmatter_order) but never on disk.steps-sort closes that gap by REUSING that same composer choke-point — no order
table is duplicated.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config steps-sortContract:
- Reuse, no duplication — order comes exclusively from the composer's
_sort_steps_by_frontmatter_order/_resolve_step_orderhelpers
(manage-execution-manifest), imported via the executor's shared PYTHONPATH. - Scope fixed to
phase-6-finalize.steps—phase-5-execute.verification_steps
is NOT sorted (it is already composer-ordered and carries no per-step
frontmatter-order doc). - Values byte-identical — only key order changes; each step's nested param
object is carried over unchanged. - Idempotent — persists ONLY when the key order actually changed. A re-run on
an already-sorted map is a no-op that produces zero diff (reordered: false). - Unresolvable-order pin — a step whose
_resolve_step_orderreturnsNone
(externalbundle:skillentries, non-string keys) is pinned at its original
index — the deterministic fallback the reused helper already implements.
Output (TOON):
status: success
phase: phase-6-finalize
reordered: true
before[3]:
- default:create-pr
- default:push
- default:archive-plan
after[3]:
- default:push
- default:create-pr
- default:archive-planreordered is true when the on-disk key order changed (and was persisted),false when the map was already in ascending frontmatter order (no write).before / after are the ordered step-id lists.
Workflow: Query Skill Domains
Pattern: Read-Process-Write
Get implementation skills for a specific domain.
Get Domain Defaults
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
skill-domains get-defaults --domain java-coreOutput:
status: success
domain: java-core
defaults[1]:
- pm-dev-java:java-coreGet Domain Optionals
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
skill-domains get-optionals --domain java-implementationValidate Skill in Domain
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
skill-domains validate --domain java-core --skill pm-dev-java:java-lombokWorkflow: System Settings
Pattern: Read-Process-Write
Manage system-level infrastructure settings.
Get Retention Settings
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
system retention getSet Retention Field
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
system retention set --field logs_days --value 7Workflow: Plan Phase Configuration
Pattern: Read-Process-Write
Manage phase-specific plan configuration. Each phase has its own sub-noun.
Get Phase Configuration
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-2-refine getGet Specific Phase Field
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-2-refine get --field compatibilitySet Phase Field
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-5-execute set --field commit_and_push --value false
# Select the per-deliverable build — comma-separated list of default:verify:{canonical} step IDs (empty disables it)
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-5-execute set --field per_deliverable_build --value default:verify:compile,default:verify:module-tests
# Tune the per-envelope packing budget consumed at plan time by the bin-packer
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-5-execute set --field per_envelope_budget_tokens --value 400Kphase-5-execute build + cost-sizing fields:
| Field | Type | Default | Semantics |
|---|---|---|---|
per_deliverable_build |
list[default:verify:{canonical}] |
[default:verify:compile, default:verify:module-tests] |
Canonical-verify rungs phase-5-execute runs at each per-deliverable chain-tail point (Step 10), module-scoped to the changed module. The default runs compile + scoped module-tests; [] disables the per-deliverable build (the end-of-phase sweep is the only build). The retired enum strings (off / compile-only / compile+scoped-test / full) are rejected with a migration error. Read by phase-5-execute per-deliverable. |
cost_size_token_table |
dict[XS/S/M/L/XL/XXL → magnitude] |
{XS: 5K, S: 25K, M: 60K, L: 130K, XL: 260K, XXL: 520K} |
Size→token table mapping each T-shirt cost_size to a predicted-token magnitude. The phase-4-plan bin-packer (manage-tasks pack-envelopes) reads it to map a task's derived cost_size to its predicted_cost_tokens. Keys must be exactly XS/S/M/L/XL/XXL; each value parses via sensible_number.parse_sensible_int. Tune the magnitudes to recalibrate the cost model from observed post-return <usage>. Read via manage-config plan phase-5-execute get --field cost_size_token_table. |
per_envelope_budget_tokens |
string (sensible int) | "400K" |
Per-envelope packing budget — the token ceiling the phase-4-plan bin-packer accumulates predicted_cost_tokens against before opening a new envelope group. Consumed at PLAN time by the bin-packer, NOT a runtime comparand. The _tokens suffix names the unit; the value parses via sensible_number.parse_sensible_int. Read via manage-config plan phase-5-execute get --field per_envelope_budget_tokens. |
Symmetric auto-continuation knobs: the forward (finalize_without_asking) and reverse (loop_back_without_asking) auto-continuation knobs are flat knobs under plan.phase-6-finalize — read/written via the standard manage-config plan phase-6-finalize get/set --field <knob> access shape. (final_merge_without_asking is a step-owned param of default:branch-cleanup, read/written via the one-stop step get/step set verb — not a flat field.)
Manage Verification Steps
Step-map fields reject
set --field. The phase's keyed step-map field —verification_stepsforphase-5-executeandstepsforphase-6-finalize— serializes on disk as the keyed-map serial form (a JSON object keyed by step id), NOT a scalar. The scalarset --fieldverb rejects these two fields with a structured error (Field '{field}' is a keyed step-map and cannot be set via 'set --field'. Use: set-steps, add-step, remove-step, or step set.) and mutates nothing. Use the step verbs below (set-steps/add-step/remove-step) to manage the step map, andstep get/step setto read or write a step's nested params. The reader consumes the keyed map directly — it is the sole on-disk shape both read and written. Only genuine scalar fields (e.g.commit_and_push,max_iterations,finalize_without_asking) are settable viaset --field.
set-steps and add-step resolve each step's order from its authoritative source (frontmatter on built-in standards docs, or frontmatter on project-local SKILL.md for project: steps) and persist the steps list sorted ascending by that value. They return error: missing_order or error: order_collision when a step has no declared order or two steps share the same value — fix the offending step's authoritative source.
# Add a step — the list is re-sorted by resolved order; --position is ignored by the new flow
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-5-execute add-step --step sonar_check
# Replace all verification steps (input order is irrelevant — output is sorted by resolved order)
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-5-execute set-steps --steps "quality_check,build_verify,sonar_check"
# Remove a step
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-5-execute remove-step --step sonar_checkCoverage two-knob configuration
Coverage is a two-dial contract — thoroughness (T1–T5) × scope (change-set…overall) — orthogonal to the effort model-tier dial. A per-phase override lives under the phase entry's coverage key; the plan-wide fallback is plan.coverage (seeded inherit/inherit). The read/resolve verbs mirror the effort resolver's lookup shape, resolving each field independently from marshal.json only (the project-DEFAULT tier — no per-plan tier), and enforcing the scope↔thoroughness coupling constraint (reject thoroughness ≥ T4 ∧ scope < component) at lookup time. See `persona-plan-marshall-agent/standards/thoroughness.md` § Coupling Constraint.
coverage's consumers are the broad-pass components that implement the coverage-gathering contract — wide audits, compliance sweeps, simplification/refactor campaigns, pre-submission review. Each gathers a (thoroughness, scope) cell from the user at invocation, expands it via coverage expand, persists the identifier + expanded instruction in status.json metadata, and consumes the expanded instruction to govern its breadth/depth. coverage resolve is the project-default tier consulted when no per-invocation cell was gathered.
# Resolve the coverage cell for a phase (project default)
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
coverage read --phase phase-5-execute
# Resolve cell + coupling result (project default)
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
coverage resolve --phase phase-5-execute
# Raw plan-wide fallback
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
coverage read --default
# Expand the identifier into the contract's operational instruction block
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
coverage expand --thoroughness T3 --scope componentcoverage expand is the static identifier→instruction expander (backed by coverage_presets.py): it maps the (thoroughness, scope) identifier to the canonical operational instruction text defined by the coverage-gathering contract's expansion table. inherit/inherit expands to the behavior-preserving instruction. An incoherent cell (e.g. thoroughness: T4, scope: change-set) is rejected at lookup/expand time with error_type: coverage_coupling_violation; unconfigured fields resolve to inherit.
Resolve Skills for a Domain and Profile
# Get aggregated skills for java implementation profile
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
resolve-domain-skills --domain java --profile implementationExtension Defaults
# Set a write-once default (only if key doesn't exist)
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
ext-defaults set-default --key preferred_build_profile --value fast
# Get an extension default
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
ext-defaults get --key preferred_build_profileWorkflow: Phase-Local gate_mode Gates and Automation Knobs
Pattern: Read-Process
The three surviving lifecycle gates ride the gate_mode enum (auto|always|never, validated at set-time by validate_gate_mode) as flat knobs under their owning phase — read/written through the standard plan <phase> get/set --field <knob> verb. The automation knobs are boolean. The four finalize ceremony gates (qgate / self_review / simplify / security_audit) no longer ride a run-at-all knob — each is governed by its owning finalize step's per-element steps.<step>.lane override (off/minimal/standard), set via step set --step-id <owning-step> --param lane --value <value>. Distribution:
| Knob | Location | Read via |
|---|---|---|
deep_lane |
plan.phase-1-init (gate_mode) |
plan phase-1-init get --field deep_lane |
escalation |
plan.phase-1-init (gate_mode) |
plan phase-1-init get --field escalation |
auto_route_recipe |
plan.phase-1-init |
plan phase-1-init get --field auto_route_recipe |
auto_route_recipe_threshold |
plan.phase-1-init |
plan phase-1-init get --field auto_route_recipe_threshold |
revalidation |
plan.phase-2-refine (gate_mode) |
plan phase-2-refine get --field revalidation |
finalize_without_asking |
plan.phase-6-finalize |
plan phase-6-finalize get --field finalize_without_asking |
loop_back_without_asking |
plan.phase-6-finalize |
plan phase-6-finalize get --field loop_back_without_asking |
final_merge_without_asking |
plan.phase-6-finalize.steps['default:branch-cleanup'] (step-owned param) |
plan phase-6-finalize step get --step-id default:branch-cleanup (read final_merge_without_asking off params) |
qgate (ceremony, lane) |
plan.phase-6-finalize.steps['pre-push-quality-gate'].lane |
plan phase-6-finalize step get --step-id pre-push-quality-gate (read lane off params) |
self_review (ceremony, lane) |
plan.phase-6-finalize.steps['default:pre-submission-self-review'].lane |
plan phase-6-finalize step get --step-id default:pre-submission-self-review (read lane off params) |
simplify (ceremony, lane) |
plan.phase-6-finalize.steps['default:finalize-step-simplify'].lane |
plan phase-6-finalize step get --step-id default:finalize-step-simplify (read lane off params) |
security_audit (ceremony, lane) |
plan.phase-6-finalize.steps['default:finalize-step-security-audit'].lane |
plan phase-6-finalize step get --step-id default:finalize-step-security-audit (read lane off params) |
Read an automation knob
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-6-finalize get --field finalize_without_askingOutput (TOON):
status: success
phase: phase-6-finalize
field: finalize_without_asking
value: trueRead a gate_mode planning gate
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config \
plan phase-1-init get --field deep_laneThe get verb is read-only — it never mutates marshal.json. An unresolvable field returns error_type: field_not_found. To read a finalize ceremony gate (qgate / self_review / simplify / security_audit), read the lane param off its owning step via plan phase-6-finalize step get --step-id <owning-step>.
Workflow: Build Map
Pattern: Script Automation
The build.map block in marshal.json is the file-to-build contract: a domain-keyed inventory of {glob, role, build_class} entries that maps every changed path to the build action it requires. It lives at the top-level build.map block (its owning block, peer to build.queue) and is populated from the registered domain extensions with write-once semantics — an existing seed survives a re-seed so user corrections are preserved. The seeded globs are explicit (pattern, role) routes: each extension declares its routes directly via classify_globs() (single-* fnmatch globs for path-bearing routes, never recursive **; a bare config-file basename route like pom.xml or package.json matches the file at any tree depth, not only a root-level instance), and the script-shared route collector gathers them verbatim. A separate git-tracked completeness validator scans git ls-files and flags any tracked source file no declared route covers, so a forgotten production module surfaces rather than silently classifying to no build. There is no separate override layer; corrections are made directly to the seeded entries.
Applicability scoping. The seed includes a domain's routes only when that domain applies to the project. aggregate_build_map() consults each domain's owning extension's applies_to_module() against the discovered project modules and keeps the domain's routes only when applies_to_module() reports applicable: True for at least one discovered module — the same applicability predicate architecture enrichment uses. A Python-only project therefore never receives java / oci / javascript routes merely because those bundles are installed. Because applicability is resolved against discovered modules, the seed is post-architecture-only: when module discovery yields no modules (architecture not yet discovered) the aggregation is empty.
Seed point. The build map is not populated at init or by sync-defaults — get_default_config() does not include a build_map block, so neither the init write nor the sync-defaults deep-merge seeds it. The wizard's Step 8b (build-map seed, run after architecture discovery) is the sole authoritative seed point; the write-once guard makes that first explicit seed authoritative. Re-run build-map seed whenever a domain extension is added or updated.
Drift detection. Because the seed is write-once, a persisted build.map can grow stale relative to the live-tree derivation as extensions add or change classify_globs() routes. The read-only build-map drift verb surfaces that staleness: it diffs the persisted block against the current derivation and returns in_sync plus per-domain added_globs / removed_globs, never mutating marshal.json. The steward consumes this verb at menu-mode entry to gate an interactive re-seed (Y/N → build-map seed --force on yes / leave untouched on no), so the --force path is no longer the only way a stale map gets surfaced — see `marshall-steward/SKILL.md` § "Re-Run Remediation Pass".
Seed the Build Map
Re-seeds build.map from every applicable registered extension's classify_globs() + classify_build_class() predicates. The aggregator collects each applicable extension's explicit (pattern, role) routes verbatim; classify_build_class() then stamps each route with its canonical-named build_class (the build_class value IS the canonical command — there is no indirection map). Write-once: an existing build_map block is never clobbered — only a missing block is populated. Run build-map seed at wizard Step 8b (after architecture discovery) and again whenever a domain extension is added or updated.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map seedOutput (TOON):
status: success
action: seeded
domain_count: 1
build_map:
python: [...]action is seeded when a missing block was written, or preserved when an existing block was left untouched (write-once). domain_count is the number of applicable domains in the resulting block.
Force a clean re-derivation
build-map seed --force bypasses the write-once guard: it clears any existing build_map and re-derives a clean one from the current project state (current extensions, current applicability against the discovered modules). Use it to discard stale or hand-edited entries — for example after an extension's classify_globs() routes change, since a plain re-seed preserves the existing block and would not pick up the new routes.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map seed --forceWhen --force clears and rewrites an existing block, action is re-derived (versus seeded for a first-time write into a missing block).
Detect drift against the live derivation
build-map drift is a read-only diff: it derives the current map from the applicable extensions (the same derivation seed uses) and compares it against the persisted build.map, returning in_sync plus the per-domain added/removed-glob diff. It never mutates marshal.json. The steward's menu-mode entry consumes this verb to gate an interactive re-seed without clobbering deliberate hand-edits.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map driftOutput (TOON):
status: success
in_sync: false
drift:
python:
added_globs: [...]
removed_globs: [...]in_sync is true when the persisted map matches the derivation (the drift block is empty); false when any domain has added_globs (present in the derivation, absent from the persisted block) or removed_globs (the reverse).
Read the Effective Build Map
Returns the effective build map read from build.map. This is the map the architecture derive-verification command reads to emit a task's verification command set. The read fails closed: when build.map is absent it returns a structured error rather than an empty map.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map readOutput (TOON):
status: success
build_map:
python: [...]
domain_count: 1domain_count is the number of domain keys in the returned build_map.
Schema and semantics: See standards/data-model.md § build_map for the
{glob, role, build_class}entry schema, the tree-derived seed contract, and the closed canonical-namedbuild_classset.
Decide Whether a Build Must Run
build-decision is the CLI surface of the sole build/no-build authority. It returns a structured three-value verdict for a plan's live footprint, and every consumer site consults it instead of deciding build necessity from any other signal. The verdict is a pure function of the build.map globs and the live plan footprint — no LLM judgement:
decision: buildwhen the footprint touches at least one registered build_map glob.decision: not_necessary(always carrying a non-empty, log-friendlyreason) when the build_map registers no globs, the footprint is resolvable-and-empty, or the footprint intersects no build glob.decision: unknown(always carrying a non-emptyreason) when the footprint is unresolvable — the worktree is not yet materialised, so there is no evidence either way about what changed.
Consumers MUST branch on all three and MUST NOT collapse unknown into not_necessary. The two answer different questions: not_necessary is the positive "nothing here needs building", while unknown is "the authority could not substantiate an answer". Dropping a build gate requires the positive answer — reading an unsubstantiated verdict as a positive one is the failure `doc/adr/009` closes. A gate consuming this verdict therefore fails toward inclusion on unknown.
--command is optional and is an echo-only label: it takes no part in the predicate above, so for a fixed footprint every command yields the identical decision / reason pair. Omit it to ask the command-free plan-wide question; supply it when the caller wants the label echoed back alongside the verdict. Synthesizing a plan-wide answer by picking an arbitrary representative command is a retired anti-pattern — see `doc/adr/004` § "Amendment: build-decision is the sole build/no-build authority", which also records the empty-footprint constraint that makes a compose-time consultation unsafe.
# Command-free (plan-wide) verdict
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-decision \
--plan-id my-plan
# Same verdict, with the caller's command echoed back as a label
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-decision \
--command quality-gate --plan-id my-planOutput — build verdict, command-free (TOON):
status: success
decision: buildOutput — not_necessary verdict, with the --command label echoed (TOON):
status: success
decision: not_necessary
reason: plan footprint touches no build_map glob — only non-buildable files changed
canonical_command: quality-gateOutput — unknown verdict, command-free (TOON):
status: success
decision: unknown
reason: plan footprint unresolvable — worktree not yet materialisedThe decision logic itself lives in the build-system-owned should_execute_build helper in script-shared; build-decision is a thin wrapper exposing it through the manage-config command surface (the home that already owns the build_map seed and footprint-matching logic the decision reuses).
Workflow: CI Operations
CI operations use the provider-agnostic ci router. The router resolves the active provider by scanning providers[] in marshal.json for the entry with category == "ci" and deriving the key from its skill_name (e.g., plan-marshall:workflow-integration-github -> github), then delegates to the matching provider script.
Note: CI commands use a different notation — they route through tools-integration-ci, not manage-config. providers[] is the single source of truth for CI provider identity; manage-config does not store a separate CI provider block. Actual CI operations live in the workflow-integration-github (or workflow-integration-gitlab) and workflow-integration-git skills.
Example: View Issue
python3 .plan/execute-script.py plan-marshall:tools-integration-ci:ci issue view --issue 123Available CI Operations
pr create/pr view/pr list/pr merge/pr close/pr ready/pr editpr reviews/pr comments/pr reply/pr resolve-thread/pr thread-replypr auto-mergeci status/ci wait/ci rerun/ci logsissue create/issue view/issue close
API Reference
Full API specification: See standards/api-reference.md.
| Noun | Key Verbs |
|---|---|
skill-domains |
list, get, get-defaults, get-optionals, set, set-inclusion, add, validate, detect, configure, get-extensions, set-extensions, get-available |
resolve-domain-skills |
--domain --profile (aggregates core + profile skills) |
resolve-workflow-skill |
--phase (resolve system workflow skill) |
resolve-workflow-skill-extension |
--domain --type (outline, triage, marker-detect) |
get-workflow-skills |
Get all workflow skills from system domain |
get-skills-by-profile |
--domain (skills organized by profile) |
ext-defaults |
get, set, set-default, list, remove |
system |
retention get, retention set |
project |
get/set (default_base_branch, working_prefixes, pr_strategy, pr_compact_max_changed_files), pr-decision --changed-files N (resolve the two PR-batching knobs into a ride|split verdict) |
orchestrator |
get/set --field {parallelization_scope|auto_emit} (whitelist-guarded scalar knobs on the top-level orchestrator block, a sibling of plan: parallelization_scope int >= 1, auto_emit bool. orchestrator.effort.* is NOT handled here — see the effort row's orchestrator[...] scope forms.) |
plan |
{phase} get/set (incl. gate_mode planning gates + flat finalize automation knobs), {phase} step get/set (one-stop keyed-map step-param read/write; ceremony gates ride the step lane param), set-steps, add-step, remove-step, set-max-iterations |
effort |
read (role/phase/--default resolver; --role orchestrator|orchestrator.{analyze|decompose|reader} resolves the sibling orchestrator.effort block, clamped to orchestrator.effort.max), resolve-target (same lookup plus execution-context-{level} target-name computation), apply-preset --preset (whole-tree writer), set --scope {phase}.{role}|plan|orchestrator[.{analyze|decompose|reader}|default|max] --level (surgical per-scope writer) |
ci |
get, get-provider, get-tools, get-command, set-provider, set-tools, persist |
build-map |
seed (re-seed build.map from applicable extensions, write-once; --force clears + re-derives), read (effective map from build.map, fail-closed when absent), drift (read-only diff of persisted vs derived map: in_sync + per-domain added/removed globs) |
build-decision |
[--command] --plan-id (the sole build/no-build authority's three-value verdict: build / not_necessary / unknown; the latter two each carry a log-friendly reason, and unknown — an unresolvable footprint — must never be collapsed into not_necessary. --command is an optional echo-only label that never enters the predicate — omit it for the command-free plan-wide verdict) |
finalize-steps |
apply-preset --preset (write plan.phase-6-finalize.steps from a named preset), list-ask-lane (enumerate steps whose effective lane is still the unresolved ask), set-lane --step-id --lane [--plan-id] (persist a resolved off/auto/full lane override; --plan-id is the channel selector — absent writes project-wide marshal.json, present writes only that plan's status.metadata.finalize_step_overrides) |
init |
Initialize marshal.json (with optional --force) |
normalize-keys |
Re-write marshal.json with the canonical top-level key order (reuses the save_config key-order writer). Idempotent and byte-stable on a canonical file; returns unrecognized_keys and a warning status naming any top-level key it could not order — a key absent from CANONICAL_TOP_LEVEL_KEY_ORDER, appended out of canonical position rather than dropped |
steps-sort |
Re-sort plan.phase-6-finalize.steps into ascending frontmatter order (silent, idempotent, values byte-identical; reuses the manifest composer's _sort_steps_by_frontmatter_order choke-point; phase-5-execute.verification_steps is out of scope; unresolvable-order steps pinned at their original index) |
domain-detect |
--plan-id [--domain-override] [--affected-files CSV] (deterministic detector for phase-1-init Step 7 and phase-2-refine; walks request.md clarified narrative for explicit mentions of configured skill_domains and their bundle aliases; returns the multi-valued domains SET — the unconditional union {detector/prompt} ∪ always_on ∪ glob_matched — plus candidates (narrative matches, offered first in the multiSelect prompt), additional_candidates (the remaining configured non-system domains — neither a narrative match nor already supplied by the always_on / file_globs legs — offered as the prompt's second group so a configured-but-unmatched domain stays selectable), always_on, glob_matched, ambiguous, source, reason. ambiguous is true only on a detector multi-match, or a zero-match with an empty always_on/glob union; a zero-match resolved by the always_on/glob legs is silent (reason=inclusion_only_resolve). The optional --affected-files CSV is the file signal for the file_globs leg (refine passes the real affected files; init falls back to narrative path tokens). No LLM dispatch fallback applies.) |
recipe-match |
--request-text [--threshold 0.6] (Tier 1 recipe-match for phase-1-init; scores free-form request text against the live recipe registry via the shared recipe_scoring core; returns ranked matches[] + top_match + meets_auto_route_threshold. Heuristic-first, zero LLM call inside the script — the bounded LLM fallback is orchestrator-driven.) |
aspect-classify |
--request-text [--threshold 0.7] (request-aspect classifier for phase-1-init; scores free-form request text against fixed analysis/planning/implementation keyword tables via recipe_scoring.tokenize; returns aspect + confidence + per-aspect breakdown. A winning analysis/planning aspect is accepted only when its _overlap_score confidence clears >= --threshold (default 0.7, NO 0.6 cap) AND beats the implementation overlap; below threshold the conservative implementation fallback applies. The verb classifies request INTENT only and has no say in build necessity — that is the build-decision verdict's exclusive province. Heuristic-first, zero LLM call inside the script — the bounded LLM fallback is orchestrator-driven.) |
Data Model
marshal.json Location
.plan/marshal.json
Structure
The defaults template contains only system domain. Technical domains (java, javascript, etc.) are added during project initialization based on detection or manual configuration. Technical domains store only bundle reference and workflow_skill_extensions -- profiles are loaded at runtime from extension.py.
Example (Java project after init):
{
"skill_domains": {
"system": {
"defaults": ["plan-marshall:persona-plan-marshall-agent"],
"optionals": ["plan-marshall:persona-plan-marshall-agent"]
},
"java": {
"bundle": "pm-dev-java",
"workflow_skill_extensions": {
"triage": "pm-dev-java:ext-triage-java"
}
}
},
"system": {
"retention": {
"logs_days": 1,
"archived_plans_days": 5,
"lessons_superseded_days": 0,
"no_plan_body_days": 7,
"build_results_days": 5,
"temp_on_maintenance": true,
"plugin_cache_keep_versions": 5,
"plugin_cache_keep_days": 3
}
},
"build": {
"queue": {
"max_slots": 5,
"max_retries": 10
}
},
"plan": {
"phase-1-init": {
"branch_strategy": "feature",
"deep_lane": "auto",
"escalation": "auto",
"auto_route_recipe": true,
"auto_route_recipe_threshold": 0.6,
"lane_selection": "ask",
"lane_prune_thresholds": {
"confidence_complete": 95,
"linear_change_max_deliverables": 1
}
},
"phase-2-refine": {
"confidence_threshold": 95,
"compatibility": "breaking",
"revalidation": "auto"
},
"phase-3-outline": {
"plan_without_asking": false,
"q_gate_validation": "once"
},
"phase-5-execute": {
"commit_and_push": true,
"max_iterations": 5,
"per_deliverable_build": ["default:verify:compile", "default:verify:module-tests"],
"verification_steps": {
"default:verify:quality-gate": {},
"default:verify:module-tests": {},
"default:verify:coverage": {}
}
},
"phase-6-finalize": {
"max_iterations": 3,
"finalize_without_asking": true,
"loop_back_without_asking": false,
"qgate": "auto",
"steps": {
"default:push": {},
"default:create-pr": {},
"plan-marshall:automatic-review": { "required_bots": "", "optional_bots": "", "review_bot_buffer_seconds": 180 },
"default:sonar-roundtrip": {
"touched_file_cleanup": "new_code_only",
"do_transition": false,
"ce_wait_timeout_seconds": 600
},
"default:lessons-capture": {},
"default:branch-cleanup": {
"pr_merge_strategy": "squash",
"final_merge_without_asking": false,
"auto_rebase_threshold": "no_overlap_only"
},
"default:archive-plan": {}
}
}
}
}Phase-Local gate_mode Gates and Automation Knobs
The three surviving lifecycle gates ride the gate_mode enum as flat phase-local knobs, each owned by the phase whose decision machinery consumes it. Each takes auto|always|never (validated at set-time by validate_gate_mode): auto defers to the existing machinery (lane router / refine revalidation), always forces the gate in, never skips it. The finalize automation knobs are boolean.
gate_mode planning gates:
| Gate | Owning phase | Controls |
|---|---|---|
deep_lane |
phase-1-init |
Whether the precondition-driven deep planning lane runs (phase-1-init lane router). never forces light, but a hard escalation still ratchets unless escalation: never is also set. |
escalation |
phase-1-init |
Whether the hard-escalation safety ratchet (explosion / build-break / premise) stays live. auto keeps it live; never is the explicit full-speed-full-risk opt-in. |
revalidation |
phase-2-refine |
Whether the premise / narrative-vs-code safety check runs (light lane + deep refine). |
Finalize ceremony gates (per-element lane override, not run-at-all): the four finalize ceremony gates — qgate, self_review, simplify, security_audit — are each governed by their owning step's steps.<step>.lane override, resolved by the manifest ceremony transform: off → never, minimal → always, and every other accepted lane value (standard, full, ask, or an absent override) → auto. Set via plan phase-6-finalize step set --step-id <owning-step> --param lane --value <value>. The owning steps are pre-push-quality-gate (qgate), default:pre-submission-self-review (self_review), default:finalize-step-simplify (simplify), default:finalize-step-security-audit (security_audit).
Flat phase-1-init recipe-match knobs (under phase-1-init):
| Field | Type | Default | Meaning |
|---|---|---|---|
auto_route_recipe |
bool | true |
Whether a high-confidence Tier 1 recipe match (top confidence >= auto_route_recipe_threshold) auto-routes to the matched recipe without prompting. false proposes the ranked matches via AskUserQuestion first. |
auto_route_recipe_threshold |
float | 0.6 |
Auto-route confidence threshold for the Tier 1 recipe match. Default 0.6 because free-form requests carry no plan domain/scope, so keyword-overlap-only confidence caps at 0.6 — the same threshold the recipe-match verb's --threshold default uses. The aspect-classify verb is unrelated: it scores via _overlap_score (request-token / keyword-table overlap fraction, 0.0–1.0) with NO 0.6 cap, so it carries its own 0.7 default threshold — do not conflate the two. |
Execution-profile lane knobs (under phase-1-init):
The lane mechanism's per-element vocabulary (the closed lane.class enum, the class→default tier table, the prune-predicate names) is owned by `extension-api/standards/ext-point-lane-element.md`; these knobs carry only the project-level posture / override / threshold config the manifest composer resolves over that contract.
| Field | Type | Default | Meaning |
|---|---|---|---|
lane_selection |
enum(ask|auto) |
ask |
Whether init PROMPTS for the execution-profile posture (ask surfaces the minimal/standard/full dialogue) or silently takes the computed projection (auto). Validated by validate_lane_selection. Mirrors the deep_lane / finalize_without_asking ask/auto family. |
lane_prune_thresholds |
dict(confidence_complete, linear_change_max_deliverables) |
{confidence_complete: 95, linear_change_max_deliverables: 1} |
Tunable numeric thresholds the standard posture evaluates its prunable-element predicates against at manifest-compose time. confidence_complete (int 0–100) is the post-init confidence floor that prunes refine; linear_change_max_deliverables (int ≥ 1) is the deliverable-count ceiling that prunes the 4-plan decomposition element. The boolean predicates (no_code_delta, footprint_no_lesson_component) carry no threshold. Validated by validate_lane_prune_thresholds (exact key set; ranges enforced). |
Per-element lane override (plan.<phase>.steps.<step>.lane, value ∈ off|minimal|standard|full|ask, validated by validate_lane_override): pins any lane-participating element to a fixed posture cutoff via the same nested step-param channel finalize-step params use — off never runs it (a derived-state/core weakening additionally emits a correctness warning at compose time, but is honored), minimal force-keeps it in every posture, standard/full pin its tier, ask always surfaces it individually in the init dialogue. Absent by default — the shipped per-element default lives in each element's frontmatter lane: block, and marshal.json carries only the project / meta overrides.
Flat finalize automation knobs (boolean, under phase-6-finalize):
| Field | Default | Meaning |
|---|---|---|
finalize_without_asking |
true |
Auto-continue into finalize after execute. |
loop_back_without_asking |
false |
Auto-re-enter on a finalize loop_back outcome. |
(final_merge_without_asking is NOT flat — it is a step-owned param nested under the default:branch-cleanup step; see the step-owned param tables below.)
Step-owned params (nested under their owning step in the phase-6-finalize.steps keyed map):
default:sonar-roundtrip (the sonar_ prefix is dropped within the scoped object):
| Param | Type | Default | Meaning |
|---|---|---|---|
touched_file_cleanup |
enum(new_code_only|touched_files_zero) |
new_code_only |
Cleanup-scope for the Sonar roundtrip success criterion. new_code_only (lean default) anchors success on new-code issues == 0; touched_files_zero also sweeps pre-existing issues on touched files. Validated by validate_sonar_touched_file_cleanup. |
do_transition |
bool | false |
Gate for the server-side SonarCloud dismissal path. false routes FALSE-POSITIVE / WON'T-FIX dispositions through in-code suppression; true re-enables sonar_rest transition dismissal. Consumed by triage Step 3c as the fall-through gate. |
ce_wait_timeout_seconds |
int | 600 |
Budget (seconds) for the synchronous in-Python CE-readiness wait in sonar.py fetch_findings — sibling of the flat checks_wait_timeout_seconds; overridable by --ce-wait-timeout. |
plan-marshall:automatic-review: required_bots and optional_bots (comma-separated bot-kinds, both default EMPTY) — the review-bot participation classification. A required bot's silence is a failure and gates the completeness quorum; an optional bot's silence never gates; a bot in NEITHER list is warned about but STILL ingested (the two lists classify, they do not admit). Each entry maps one-to-one to a standards/{bot_kind}.md registry doc. Both default empty so a never-asked key stays distinguishable from an answered-empty value; the three-valued bot_lists_provenance (never_asked / migrated / answered) records which. See `standards/data-model.md` for the provenance contract and `../automatic-review/standards/bot-participation-contract.md` for the participation semantics and the failure taxonomy. review_bot_buffer_seconds (int, default 180) — max-wait ceiling for pr wait-for-comments. default:branch-cleanup: pr_merge_strategy (default squash), final_merge_without_asking (bool, default false), auto_rebase_threshold (default no_overlap_only).
Access shape. Read/write each FLAT knob through the standard plan <phase> get/set --field <knob> verb — e.g. plan phase-1-init get --field deep_lane, plan phase-6-finalize get --field finalize_without_asking. Read/write each STEP-OWNED param — including a ceremony gate's lane override — through the one-stop plan phase-6-finalize step get/set --step-id {step} [--param {k} --value {v}] verb against the marshal.json keyed-map serial form (the global-config default + wizard write target), or via the plan-local manifest snapshot manage-execution-manifest step-params get/set (the per-plan runtime read/override). See § Workflow: Phase-Local gate_mode Gates and Automation Knobs.
Default source. The Default column above is not held in any centralized constant. Each param-owning step declares its params self-describingly in the configurable: block of its body-doc frontmatter; the finalize-step defaults seed (get_default_config()) materializes them by delegating each built-in step id through the plan-marshall:extension-api:configurable_contract parser (resolve_step_defaults_optional, ownerless steps → null). The parser is the single fail-loud source of truth for a valid step-param declaration — see `extension-api` SKILL.md § Configurable step-param contract.
Build-Queue Settings
The build.queue block lives under the top-level build block in marshal.json (peer to build.map, not under plan.*) because the build queue is a project-wide, cross-plan resource — every session bounds its concurrent builds against the same shared queue. Both keys are seeded into a fresh marshal.json by init and back-filled into existing projects by sync-defaults.
| Field | Default | Meaning |
|---|---|---|
max_slots |
5 |
Number of concurrent build admissions the cross-session build queue grants before further requests are enqueued FIFO. Read by the build-queue admission primitive (plan-marshall:manage-locks:build_queue) via build.queue.max_slots; a missing block, missing key, or non-positive value falls back to 5. |
max_retries |
10 |
Number of times the build wrapper re-polls a blocked admission before giving up. |
Edit both keys directly in marshal.json — they are operator-visible JSON integers at the top level.
Standard Domains
Detailed reference: See standards/skill-domains.md for domain structure, profiles, and validation rules. See standards/skill-domains-operations.md for resolution commands and usage patterns.
System Domain
The system domain contains execute-task skills and base skills applied to all tasks.
| Field | Purpose |
|---|---|
defaults |
Base skills loaded for all tasks (plan-marshall:persona-plan-marshall-agent) |
optionals |
Optional base skills available for selection |
Technical Domains (Profile Structure)
Technical domains store bundle reference and workflow_skill_extensions in marshal.json. Profiles are loaded at runtime from extension.py.
| Profile | Phase | Purpose |
|---|---|---|
core |
all | Skills loaded for all profiles |
implementation |
execute | Production code tasks |
module_testing |
execute | Unit/module test tasks |
integration_testing |
execute | Integration test tasks |
quality |
verify | Documentation, verification |
Available Domains:
| Domain | Bundle | Extensions |
|---|---|---|
java |
pm-dev-java |
triage |
javascript |
pm-dev-frontend |
triage |
plan-marshall-plugin-dev |
pm-plugin-development |
outline, triage |
documentation |
pm-documents |
outline, triage |
Use resolve-domain-skills --domain {domain} --profile {profile} to get aggregated skills.
Scripts
| Script | Notation |
|---|---|
| manage-config | plan-marshall:manage-config |
Script characteristics:
- Uses Python stdlib only (json, argparse, pathlib, xml.etree)
- Outputs TOON to stdout
- Exit code 0 for success, 1 for errors
- Supports
--helpflag
Integration
Producers
| Client | Operation | Purpose |
|---|---|---|
marshall-steward |
init, skill-domains configure | Initialize and configure domains |
manage-architecture |
skill-domains set, ext-defaults | Set domain skills from enrichment |
Consumers
| Client | Operation | Purpose |
|---|---|---|
phase-1-init |
plan get, resolve-domain-skills | Read plan config, resolve skills |
phase-5-execute |
resolve-domain-skills | Load skills for task execution |
manage-run-config |
system retention get | Read retention settings for cleanup |
Canonical invocations
The canonical argparse surface for manage-config.py. The D4 plugin-doctor analyzer
(_analyze_manage_invocation.py) reads this section as source-of-truth for markdown
notation occurrences across the marketplace. Consuming skills xref this section by
name (e.g., "see manage-config Canonical invocations → effort resolve-target")
instead of restating the command inline.
init
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config init \
[--force]skill-domains list
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains listskill-domains get
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains get \
--domain DOMAINskill-domains get-defaults
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains get-defaults \
--domain DOMAINskill-domains get-optionals
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains get-optionals \
--domain DOMAINskill-domains set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains set \
--domain DOMAIN [--profile PROFILE] [--defaults LIST] [--optionals LIST]skill-domains set-inclusion
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains set-inclusion \
--domain DOMAIN [--always-on] [--file-globs CSV]Sets the per-domain inclusion keys always_on (bool) and file_globs (list[str], via the comma-separated --file-globs CSV) after validate_domain_inclusion. --always-on is a BooleanOptionalAction flag: pass --always-on to set it true or its --no-always-on companion to set it false; omitting the flag entirely leaves the persisted value untouched (each key is written independently). Domains carrying these keys are unioned into references.domains by domain-detect.
skill-domains get-extensions
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains get-extensions \
--domain DOMAINskill-domains set-extensions
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains set-extensions \
--domain DOMAIN --type {outline|triage|marker-detect} --skill SKILL_REFskill-domains add
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains add \
--domain DOMAIN [--defaults LIST] [--optionals LIST]skill-domains validate
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains validate \
--domain DOMAIN --skill SKILL_REFskill-domains detect
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains detectskill-domains get-available
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains get-availableskill-domains configure
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains configure \
--domains LISTskill-domains discover-project
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains discover-projectskill-domains attach-project
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains attach-project \
--domain DOMAIN --skills LISTskill-domains active-profiles set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains active-profiles set \
--profiles LIST [--domain DOMAIN]skill-domains active-profiles remove
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config skill-domains active-profiles remove \
[--domain DOMAIN]system retention get
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config system retention getsystem retention set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config system retention set \
--field FIELD --value VALUEproject get
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config project get \
--field FIELD--field working_prefixes returns the canonical closed set of allowed
working-branch prefixes (a flat JSON array of strings, default["feature/", "fix/", "chore/"]), falling back to the DEFAULT_PROJECT
default when the key is absent from marshal.json.
project set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config project set \
--field FIELD --value VALUEScalar fields (e.g. default_base_branch) take a plain value; the list-valued
field working_prefixes takes a JSON array of strings that round-trips throughget. A non-array value (or an array containing a non-string item) is rejected
with error_type: invalid_type.
The two PR-batching knobs are scalar fields: pr_strategy (enum compact|distinct,
default compact) is rejected with error_type: invalid_value when the value is
outside the enum; pr_compact_max_changed_files (int >= 0, default 150) is
rejected with error_type: invalid_value when the value is a bool, a non-int, or
negative.
project pr-decision
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config project pr-decision \
--changed-files NResolves the two PR-batching knobs — project.pr_strategy andproject.pr_compact_max_changed_files (falling back to their DEFAULT_PROJECT
defaults when absent, exactly like get) — into a ride|split verdict for a
change with N changed files. This is the documented single consult surface every
PR-opening guidance references (D2 create-pr, D3 ad-hoc full-PR-flow, D4
marshall-steward landing-cycle); consult it rather than re-deriving the
strategy/ceiling comparison in prose. Semantics: distinct ⇒ always split;compact ⇒ ride when changed_files <= max else split. --changed-files is a
required int >= 0; a negative or non-int value returns status: error.
Output TOON shape:
status: success
decision: ride | split
strategy: compact | distinct
changed_files: N
max: 150
threshold: 151max is the resolved compact ceiling (pr_compact_max_changed_files); threshold
is the first changed-file count that forces a split under the compact strategy
(max + 1).
orchestrator get
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config orchestrator get \
--field FIELDReturns the stored value plus a set boolean distinguishing a configured value from an unset one. When unset, value falls back to the canonical default from DEFAULT_ORCHESTRATOR (so auto_emit reads false and parallelization_scope reads 1; a field with no seeded default would read null). --field is checked against the scalar whitelist (parallelization_scope, auto_emit); an unknown field is rejected before the read.
orchestrator set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config orchestrator set \
--field FIELD --value VALUEWrites one scalar knob into the top-level orchestrator block (a sibling of plan), rejecting an unknown --field against the same whitelist with error_type: unknown_field. Two known scalar fields: parallelization_scope (int >= 1; bool is rejected even though it is an int subclass) pre-fills the per-epic parallelization_scope ask in plan-orchestrator init; auto_emit (bool; the orchestrator-tier autonomy knob, default false) governs the post-landing queue-fill emit. orchestrator.effort.* is NOT written here — see effort set --scope orchestrator[...] below.
plan {phase} get
Applies to every phase sub-noun: phase-1-init, phase-2-refine, phase-3-outline,phase-4-plan, phase-5-execute, phase-6-finalize.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan {phase} get \
[--field FIELD]plan {phase} set
Applies to every phase sub-noun (scalar verb).
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan {phase} set \
--field FIELD --value VALUEThe scalar set verb rejects the keyed step-map field of phase-5-execute
(--field verification_steps) and phase-6-finalize (--field steps) with a
structured error and no mutation — those fields are keyed step-maps, not
scalars. Use set-steps / add-step / remove-step to manage the step map
and step get / step set for a step's nested params.
plan phase-5-execute set-max-iterations / plan phase-6-finalize set-max-iterations
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-5-execute set-max-iterations \
--value Nplan phase-5-execute set-steps / plan phase-6-finalize set-steps
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-5-execute set-steps \
--steps LISTplan phase-5-execute add-step / plan phase-6-finalize add-step
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-5-execute add-step \
--step STEP_REFplan phase-5-execute remove-step / plan phase-6-finalize remove-step
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-5-execute remove-step \
--step STEP_REFplan phase-6-finalize step get / plan phase-5-execute step get
Returns the complete nested param object for a step in a single call against the marshal.json keyed map.
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-6-finalize step get \
--step-id STEP_IDplan phase-6-finalize step set / plan phase-5-execute step set
Writes one step-owned param into the step's nested object (value-coerced).
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config plan phase-6-finalize step set \
--step-id STEP_ID --param PARAM --value VALUEext-defaults get
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config ext-defaults get \
--key KEYext-defaults set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config ext-defaults set \
--key KEY --value VALUEext-defaults set-default
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config ext-defaults set-default \
--key KEY --value VALUEext-defaults list
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config ext-defaults listext-defaults remove
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config ext-defaults remove \
--key KEYeffort read
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config effort read \
[--role ROLE] [--phase PHASE] [--default]effort resolve-target
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config effort resolve-target \
[--role ROLE] [--phase PHASE] [--default] \
[--workflow WORKFLOW] [--plan-id PLAN_ID] [--caller CALLER]--workflow/--plan-id/--caller are the dispatch-emission context: supply --workflow and the resolver emits the [DISPATCH] work-log line and the paired decision-log resolution record from this seam, per firing (see ref-workflow-architecture dispatch-logging.md). A resolve with no --workflow is a pure query and emits nothing. --caller defaults to plan-marshall:manage-config; --plan-id none (or omitted) routes the emission to the global log.
--role orchestrator (bare) or --role orchestrator.{analyze|decompose|reader} resolves against the sibling orchestrator.effort block instead of a plan.<phase> entry: orchestrator.effort.{surface} → orchestrator.effort.default (or the bare-string shorthand) → plan.effort → inherit, then CLAMPED to orchestrator.effort.max when set. The reader surface's resolved level is what the dispatch site composes into the read-only execution-context-reader-{level} variant (untrusted-text ingestion runs under the reader variant, not the write-capable one) — resolve-target itself still returns the plain execution-context-{level} target name.
effort apply-preset
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config effort apply-preset \
--preset PRESETeffort set
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config effort set \
--scope {phase}.{role}|plan|orchestrator[.{analyze|decompose|reader}|.default|.max] --level LEVELSurgical per-scope writer. --scope {phase}.{role} (e.g. phase-6-finalize.verification-feedback) writes one nested effort scope, preserving sibling sub-keys (a pre-existing scalar effort string is normalised into an object first). --scope plan writes the plan.effort plan-wide scalar. --scope orchestrator writes the orchestrator.effort scalar shorthand (a bare level string applying to every orchestrator surface); --scope orchestrator.{analyze|decompose|reader} writes one per-surface override (normalising a pre-existing scalar orchestrator.effort into an object first, seeding its prior value into default); --scope orchestrator.default writes the in-block fallback slot; --scope orchestrator.max writes the uplift ceiling that clamps every resolved orchestrator surface level on the ordinal ladder (an unset max is a no-op). Unknown phase/role, an unwritable orchestrator.<key> sub-key, and invalid --level are all rejected.
effort identify
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config effort identifyRead-only recogniser. Reconstructs the {default, roles} payload from the on-disk effort config and classifies it against the preset ladder: match: current (equals a preset — preset names it), previous-ladder (equals a pre-respread shape no current preset matches — offer a re-apply of preset), custom, or not_configured. Returns a ready-to-print message the wizard's Current: … line displays. Recognises pre-respread preset shapes so a value re-spread never silently reclassifies a working config as custom.
coverage read
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config coverage read \
[--role ROLE] [--phase PHASE] [--default]coverage resolve
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config coverage resolve \
[--role ROLE] [--phase PHASE] [--default]coverage expand
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config coverage expand \
--thoroughness THOROUGHNESS --scope SCOPEresolve-domain-skills
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config resolve-domain-skills \
--domain DOMAIN --profile PROFILEresolve-workflow-skill-extension
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config resolve-workflow-skill-extension \
--domain DOMAIN --type {outline|triage|marker-detect}get-skills-by-profile
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config get-skills-by-profile \
--domain DOMAINlist-recipes
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config list-recipesresolve-recipe
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config resolve-recipe \
--recipe RECIPE_KEYrecipe-match
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config recipe-match \
--request-text REQUEST_TEXT [--threshold 0.6]Tier 1 recipe-match: scores free-form --request-text against the live recipe registry using the shared recipe_scoring core (the same keyword/intent-overlap matcher the lesson auto-suggest path consumes). Returns the ranked matches[] (each with key, name, skill, domain, scope, source, confidence, breakdown), a top_match, and a meets_auto_route_threshold boolean (true only when the top match's confidence is >= --threshold, default 0.6 — the keyword-only scoring ceiling for free-form requests, so a perfect keyword match exactly meets the bar). Returns status: success with empty matches when nothing clears the minimum-confidence floor.
The verb is heuristic-first: it performs no LLM call and no plan-scoped read — only the free-form request text drives scoring (no plan domain/scope is available, so keyword overlap is the sole signal). The bounded LLM fallback for ambiguous matches is orchestrator-driven (phase-1-init), not part of this script — mirroring how change-type-heuristic and planning-lane route keep the LLM out of the script body.
Output TOON shape:
status: success
request_tokens[N]: [token, ...]
recipes_evaluated: N
threshold: 0.6
matches[N]{key,name,skill,domain,scope,source,confidence,breakdown}:
...
count: N
top_match:
key: ...
confidence: ...
meets_auto_route_threshold: true | falseaspect-classify
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config aspect-classify \
--request-text REQUEST_TEXT [--threshold 0.7]Request-aspect classifier: tokenizes free-form --request-text via the shared recipe_scoring.tokenize and scores the token overlap against three fixed keyword tables — analysis, planning, and implementation. The higher of analysis/planning is the candidate aspect; it is accepted only when its _overlap_score confidence clears >= --threshold (default 0.7) AND beats the implementation overlap. Otherwise the verb returns the conservative implementation fallback. The caller (phase-1-init) persists the resolved aspect.
Scope: the verb classifies request INTENT and nothing else. It has no say in whether a change needs a build — build necessity is decided exclusively by the build-decision verdict over the build.map globs and the live footprint (see `doc/adr/004` § "Amendment: build-decision is the sole build/no-build authority").
The --threshold here is independent of the recipe-match verb's --threshold and of the auto_route_recipe_threshold config knob. Those caps at 0.6 because plan-domain/scope blending is unavailable; aspect-classify uses a pure request-token overlap fraction (_overlap_score, range 0.0–1.0) with NO 0.6 ceiling, so its 0.7 default is reachable and intentional. Do not conflate the two thresholds.
The verb is heuristic-first: it performs no LLM call and no plan-scoped read — only the free-form request text drives scoring. The bounded LLM fallback for genuinely ambiguous requests is orchestrator-driven (phase-1-init), not part of this script — mirroring change-type-heuristic's heuristic-first / conservative-default contract.
Output TOON shape:
status: success
request_tokens[N]: [token, ...]
threshold: 0.7
aspect: analysis | planning | implementation
confidence: 0.0-1.0
scores:
analysis: 0.0-1.0
planning: 0.0-1.0
implementation: 0.0-1.0
breakdown:
analysis:
score: 0.0-1.0
matched_keywords[N]: [keyword, ...]
planning:
score: 0.0-1.0
matched_keywords[N]: [keyword, ...]
implementation:
score: 0.0-1.0
matched_keywords[N]: [keyword, ...]resolve-outline-skill
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config resolve-outline-skill \
--domain DOMAINlist-finalize-steps
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config list-finalize-stepslist-verify-steps
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config list-verify-stepsfinalize-steps set-lane
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config finalize-steps set-lane \
--step-id STEP_ID --lane {off,standard,full} [--plan-id PLAN_ID]Persists a resolved per-element lane override for one finalize step. --plan-id is the channel selector and the only argument distinguishing the two declaration channels: omit it to write the project-wide marshal.json plan.phase-6-finalize.steps[<step>].lane, supply it to write only that plan's status.metadata.finalize_step_overrides[<step>].lane and leave marshal.json byte-unchanged. The composer merges both (plan-local ▸ marshal) into the one map every per-element reader consults. The --lane choices are the resolved answers an operator dialogue produces; the reader enum additionally accepts the minimal / ask seed values, which only shipped frontmatter and marshal seeding emit.
domain-detect
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config domain-detect \
--plan-id PLAN_ID [--domain-override DOMAIN] [--affected-files CSV]Returns the multi-valued domains SET (the union {detector/prompt} ∪ always_on ∪ glob_matched) plus candidates / additional_candidates / always_on / glob_matched / ambiguous / source / reason. candidates is the detector's narrative matches; additional_candidates is the remaining configured non-system domains — neither a narrative match nor already supplied by the always_on / file_globs legs — so the caller's ambiguous-branch prompt offers candidates first and additional_candidates second, keeping a configured-but-unmatched domain selectable. --affected-files (comma-separated) is the file signal for the file_globs inclusion leg — refine supplies the real affected files; init falls back to path-like tokens extracted from the narrative. Read-only: it reads config + request.md and writes nothing (no LLM dispatch).
build-map seed
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map seed \
[--force]--force clears any existing build_map and re-derives a clean one from the current project state, bypassing the write-once guard.
build-map read
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map readbuild-map drift
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-map driftRead-only diff of the persisted build.map against the live derivation. Returns in_sync plus per-domain added_globs / removed_globs; never mutates marshal.json.
normalize-keys
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config normalize-keysRe-writes marshal.json with the canonical top-level key order (reuses the save_config writer). Idempotent — an already-canonical file is left byte-stable. The result reports action: normalized plus unrecognized_keys: the top-level keys absent from CANONICAL_TOP_LEVEL_KEY_ORDER, which order_config_keys can only append after the canonical block rather than drop. When that set is non-empty the status is warning (naming the offenders); otherwise success. So a caller can no longer read action: normalized and conclude the file is canonical when a stray block remains appended.
steps-sort
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config steps-sortRe-sorts plan.phase-6-finalize.steps into ascending frontmatter order, reusing the manifest composer's _sort_steps_by_frontmatter_order choke-point (no duplicated order table). Values are preserved byte-identically; only key order changes. Silent and idempotent — persists only when the order changed, so a re-run on an already-sorted map yields reordered: false and zero diff. phase-5-execute.verification_steps is out of scope; steps whose frontmatter order is unresolvable are pinned at their original index.
build-decision
python3 .plan/execute-script.py plan-marshall:manage-config:manage-config build-decision \
[--command COMMAND] --plan-id PLAN_IDReturns a build / not_necessary / unknown verdict for PLAN_ID's live footprint. --audit-plan-id is accepted as an alias for --plan-id. Thin wrapper over extension_base.should_execute_build — a pure function of the build.map globs ∩ the live plan footprint; on not_necessary and unknown it carries a populated reason, on build no reason. unknown means the footprint could not be resolved at all (the worktree is not yet materialised) and is forwarded verbatim — the wrapper never collapses it into not_necessary.
--command is optional and is an echo-only label: it never enters the predicate, so the verdict is identical with and without it for a given footprint. Omit it for the command-free plan-wide verdict ({decision, reason} with no canonical_command key); supply it to have the label echoed back. Picking an arbitrary representative command to stand in for a plan-wide answer is a retired anti-pattern.
This is the sole build/no-build authority: every consumer site — the pre_push_quality_gate_inactive compose pre-filter, the pre-commit-verify-freshness gate, the phase-5 whole-tree verify surfaces (Step 11b Final Quality Sweep and the end-of-phase default:verify:{canonical} loop) — consults this verdict instead of deciding build necessity from any other signal. The ruling and the empty-footprint constraint that bounds where the verdict may be consulted are recorded in `doc/adr/004` § "Amendment: build-decision is the sole build/no-build authority".
Error Responses
See manage-contract.md for the standard error response format.
| Error | Cause | Resolution |
|---|---|---|
not_initialized |
marshal.json missing | Run /marshall-steward |
invalid_domain |
Domain not in skill_domains | Check domain name or run /marshall-steward |
skill_domains not configured |
No domains in marshal.json | Run /marshall-steward |
invalid_field |
Unknown field for phase/noun | Check field reference table above |
unknown_field |
project set --field X where X is not a known project field (default_base_branch, working_prefixes, pr_strategy, pr_compact_max_changed_files) |
Use one of the four known project fields; the write is refused before persisting so no dead key is written |
keyed step-map set --field rejection |
set --field verification_steps (phase-5-execute) or set --field steps (phase-6-finalize) — those fields are keyed step-maps, not scalars |
Use set-steps / add-step / remove-step, or step set for a step's nested params |
skill_not_found |
Skill not in domain defaults/optionals | Check with validate --domain --skill |
Related
- standards/config-design-principles.md — Governance rules for what belongs in
marshal.jsonand how config fields change (ownership, placement, anti-speculation, lossless migration) manage-architecture— Consumes configuration for project analysismarshall-steward— Interactive configuration wizardextension-api— Build system detection uses config