Skip to content

feat(cli): add post-init config command - #113

Merged
kanfil merged 17 commits into
tikalk:mainfrom
therightstuff:feat/add-post-init-config-command
Sep 11, 2026
Merged

kanfil merged 17 commits into
tikalk:mainfrom
therightstuff:feat/add-post-init-config-command

Conversation

@therightstuff

@therightstuff therightstuff commented Aug 30, 2026

Copy link
Copy Markdown

Description

Expose persisted settings and extension lifecycle under specify config, publish fork-owned documentation through the guarded Pages workflow, and add a post-initialization lifecycle for team directives.

Behavioral Safeguards

  • specify config permits only settings it can change safely. Integration, script, and skills-layout changes remain owned by specify integration; initialization metadata remains read-only.
  • Team-directives MCP entries are tracked at initialization and when a source is changed. Replacing or unsetting a source removes only unchanged entries owned by that source, preserving user-owned or user-modified entries.
  • Archive-backed directives retain the previous extracted cache until synchronization, skill installation, and MCP configuration succeed. A later failure restores that previous cache.
  • Agent context refreshes only after team-directives configuration is saved or removed, so it cannot observe uncommitted directives or retain directives that were unset.
  • Team-directives installation is not fully transactional. A failed skill copy can leave partial skill files; users must repair them before retrying because existing skill files are skipped.

Enable Fork Documentation Publishing

Follow these steps after this pull request merges into main. They require
administrator or maintainer access to tikalk/agentic-sdlc-spec-kit.

1. Enable GitHub Pages for Actions

  1. Open Pages settings.
  2. Under Build and deployment, set Source to GitHub Actions.
  3. Save the setting.

2. Run the Documentation Workflow

  1. Open Deploy Documentation to Pages.
  2. Select Run workflow.
  3. Select the main branch.
  4. Select Run workflow and wait for both build and deploy jobs to pass.

3. Validate the Published Site

Open each URL and confirm it returns the fork's content rather than an error
page:

Also open the deployment environment and confirm the latest deployment is successful.

4. Validate Future Publishing

Merge a documentation-only change that modifies a file under docs/, then
confirm the workflow starts automatically and deploys successfully.

If the workflow does not start or deploy, review its job log before retrying;
the log is the source of the failure reason and recovery guidance.

Testing

  • tests/test_config_cli.py and tests/test_init_fork.py passed in the focused local run.
  • Full test suite rerun completed with .venv/bin/pytest: 7,625 passed, 222 skipped, and 16 failed in 10m38s. The failures are in git-template, PowerShell-event, script-parity, bundler-reference, and workflow-documentation tests; the focused feature tests passed.
  • Ruff is installed in .venv; the full check reports unrelated baseline violations. Focused branch files were checked, and branch-local findings were cleaned without changing unrelated shared-file findings.
  • Manual sample-project validation.

See new section Verify Post-Initialization Configuration under docs/local-development.md

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

Code planned and implemented using superpowers and Codex, with some manual editing. Local development test steps performed without AI.

@kanfil

kanfil commented Sep 2, 2026

Copy link
Copy Markdown
Member

Posted on behalf of @kanfil by opencode (model: glm-5.2).

Reviewed src/specify_cli/commands/config.py, tests/test_config_cli.py, and the supporting modules. Verified findings against integrations/base.py, integrations/_install_commands.py, integrations/_migrate_commands.py, extensions/__init__.py, and upstream github/spec-kit. Confirmed upstream has no config command, so all of the following are fork-introduced against this fork-only feature.

Medium — config set script has no effect on installed command files

config.py:140-144 updates options["script"] and persists it, but script is baked into agent command files at install time via {SCRIPT} replacement (base.py:1094-1103, sourced from opts["script_type"] at base.py:1394). After config set script py, config get script reports py while every installed command file still invokes scripts/bash/*.sh. This is inconsistent with the other two mutable keys: feature-numbering is read at runtime by agent templates (templates/commands/specify.md:87, extensions/git/commands/speckit.git.feature.md:34), and team-ai-directives re-runs sync_team_ai_directives + _install_skills_from_path on set. docs/reference/configuration.md presents all three uniformly as "safely change supported settings" with no regeneration note. The test test_config_set_script_persists_valid_value only asserts the persisted JSON, so this gap is uncovered.

Suggested fix (preferred): reject script from config set the same way ai/integration/ai-skills are rejected, pointing to the existing owner command specify integration upgrade <key> --script <type> (_migrate_commands.py:597, which is diff/manifest-aware and already shared with upstream). This matches the PR's own Global Constraint that integration lifecycle stays with specify integration *. Alternatively, document the JSON-only limitation, or regenerate on set by calling the integration upgrade --script logic internally.

Low — misleading guidance for config set ai-skills

config.py:152-155 rejects ai, integration, and ai-skills with f"{normalized_key} is managed by specify integration use {value}". specify integration use (_query_commands.py:224) takes an integration key and has no skills-toggle flag, so config set ai-skills true yields "ai-skills is managed by specify integration use true", which is not a valid command. The test only exercises integration, so this isn't caught. The message should be key-specific and point at the real owner command for each rejected key.

Low — "Unknown configuration key" for known-but-unsettable keys

config.py:172-173's else branch raises Unknown configuration key: {key}. here and speckit-version are in _INIT_OPTION_KEYS (so config get here / config get speckit-version work), but config set here … falls through to else and reports them as unknown. The message should distinguish "unknown" from "known but read-only".

Low — config unset ignores ExtensionManager.remove return value

config.py:188 calls ExtensionManager(project_root).remove("team-ai-directives"), which returns False (does not raise) when the extension is not installed (extensions/__init__.py:2971-2972). The command unconditionally prints "Removed team-ai-directives configuration." regardless of the return value, so the user is told removal succeeded for a no-op when the extension was never installed. Worth checking the return value and printing "team-ai-directives is not installed" when False.

Low — partial state on skill-install failure

In the team-ai-directives branch (config.py:164-171), if _install_skills_from_path raises after sync_team_ai_directives succeeds, the governance extension is already installed but save_init_options (line 175) is never reached, so team_ai_directives is not persisted and the user sees a raw traceback. Retry is self-healing (sync is idempotent when the extension is already installed, _init_fork.py:568-569), so impact is limited, but wrapping the install step and either persisting the resolved source before the install or surfacing a clean typer.BadParameter would be more robust.

Notes (not issues)

  • Registering the same extension_app singleton under both specify extension and specify config extension works — Typer.add_typer builds independent Click groups per parent, and test_config_extension_list_reuses_extension_commands exercises the path.
  • The try/except ImportError fallbacks for _init_fork are correct for the fork (all three symbols exist there); the upstream fallback paths are reasonable.
  • feature-numbering mutation is sound — confirmed the value is read at runtime by agent templates, not baked at install time.

Yash-Chindam and others added 14 commits September 3, 2026 09:27
…thub#4397)

* fix(scripts): name setup-plan's feature directory key FEATURE_DIR

setup-plan emitted a key called SPECS_DIR holding $FEATURE_DIR -- the
per-feature subdirectory, not the specs root. The name is already taken
elsewhere with the other meaning: create-new-feature.sh sets
SPECS_DIR="$REPO_ROOT/specs" and derives FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME".

setup-plan was also the only script in the suite using it. setup-tasks and
both check-prerequisites payloads already emit FEATURE_DIR for exactly this
value, so this brings setup-plan in line rather than inventing a convention.

Renamed in all three ports so the payloads stay identical, and in
templates/commands/plan.md, which is the only consumer -- it parses the key
by name, so it has to move in the same commit.

Verified the bash, PowerShell, and Python variants all emit
['BRANCH','FEATURE_DIR','FEATURE_SPEC','IMPL_PLAN'].

Fixes github#4017

* test(scripts): pin setup-plan's FEATURE_DIR output contract

Addresses review feedback. The existing setup-plan tests compare the ports
against each other, so all three could regress to SPECS_DIR together and
still pass. This asserts the contract absolutely, in JSON and text mode and
across bash/Python/PowerShell: the key is FEATURE_DIR, it carries the
feature directory rather than the specs root, and SPECS_DIR is absent.

The value is matched by suffix rather than full path because the ports
legitimately differ in path flavour -- under MSYS bash reports /tmp/... where
the Python and PowerShell ports report C:\... . The suffix still separates
specs/001-my-feature from a bare specs, which is the regression being
guarded; verified it rejects both /tmp/proj/specs and C:\proj\specs.
…ithub#4424)

docs/reference/workflows.md introduces its YAML block as the workflow that
ships with Spec Kit, so a reader is entitled to treat it as the real
definition. It had drifted on four points:

  version           1.0.0                      -> 1.0.1
  speckit_version   >=0.7.2                    -> >=0.8.5
  integrations.any  copilot, claude, gemini    -> also alquimia, opencode
  integration       default "copilot"          -> default "auto"

The last is the most user-visible: the guide stated the default integration
was copilot, when it is auto, resolved from the project's initialized
integration. Someone reading the guide to learn what they get by default was
being told the wrong thing.

Adds a guard so this cannot drift again. It compares parsed YAML rather than
text, so the guide stays free to format lists however reads best and only the
content has to agree. Verified it fails against the pre-sync copy, reporting
all four differences, and passes after.

Follow-up to github#4384 / github#4398, at the maintainer's suggestion.
…4320)

* fix(presets): reject falsy non-mapping catalog config shapes

`PresetCatalog._load_catalog_config` had two "shape check runs after
an emptiness check" bugs, both masking a corrupted preset-catalogs.yml
as an empty/no-op config instead of raising:

- Top level: `yaml.safe_load(...) or {}` coerced a FALSY non-mapping
  document (`[]`, `false`, `0`, `''`) to `{}` before the
  `isinstance(data, dict)` guard ran, so it was silently treated as
  "no config" — while a TRUTHY non-mapping (a bare string) already
  raised "expected a mapping at root".
- One level down: `catalogs_data = data.get("catalogs", [])` followed
  by `if not catalogs_data: return None` ran the emptiness check
  *before* the `isinstance(catalogs_data, list)` check, so a FALSY
  non-list `catalogs:` value (`{}`, `''`, `0`, `false`) was silently
  swallowed as "no catalogs" — while a TRUTHY non-list
  (`catalogs: "not-a-list"`) already raised "must be a list".

`WorkflowCatalog._load_catalog_config` and
`StepCatalog._load_catalog_config` (workflows/catalog.py) already
guard against both cases correctly, with the same explanatory
comments reused here. This preset sibling was missed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FW9fAYsCBCAgdKWovtSyqt

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix indentation in test for catalog config loading

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Expose persisted settings and extension lifecycle under `specify config`, and publish fork-owned documentation through the guarded Pages workflow.
@therightstuff
therightstuff force-pushed the feat/add-post-init-config-command branch from e1a94b5 to 8cbf12f Compare September 11, 2026 05:39
@kanfil

kanfil commented Sep 11, 2026

Copy link
Copy Markdown
Member

Code Review — feat(cli): add post-init config command

Bug — Stale SPECS_DIR reference in fork's default preset plan command

File: presets/agentic-sdlc/commands/adlc.spec.plan.md:65

This PR renames the SPECS_DIR output key to FEATURE_DIR across all three script variants (setup-plan.sh, .ps1, .py) and the core template templates/commands/plan.md. However, the fork's agentic-sdlc preset ships a command override (adlc.spec.plan.md) that replaces: "speckit.plan" (per preset.yml), and it was not updated — it still instructs the agent to parse SPECS_DIR:

1. **Setup**: Run {SCRIPT} from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH.

The core template now says FEATURE_DIR, the scripts now emit FEATURE_DIR, but the preset override still says SPECS_DIR.

Why this matters: The agentic-sdlc preset is bundled: true, preinstall: true in presets/catalog.json — it is installed by default on every fork project. When active, adlc.spec.plan.md replaces the core plan.md, making the FEATURE_DIR update to the core template effectively invisible. Every fork project running the plan command would have its agent look for a SPECS_DIR key that no longer exists in the JSON output, leaving it without the feature directory path.

The test doesn't catch this: The new test test_script_upgrade_regenerates_commands_or_preserves_customizations explicitly removes the agentic-sdlc, agentic-change, and agentic-quick presets before testing, so the mismatch is never exercised.

Fix: Update presets/agentic-sdlc/commands/adlc.spec.plan.md:65 to reference FEATURE_DIR instead of SPECS_DIR, matching the core template and scripts.


Behavioral change — Preset catalog config now rejects falsy non-list values

File: src/specify_cli/presets/__init__.py (_load_catalog_config)

This is a correctness fix: catalogs: {}, catalogs: "", catalogs: 0, catalogs: false previously silently returned None (treated as "no catalogs") instead of raising PresetValidationError. After this change, those values correctly raise.

This is the right behavior (a non-list catalogs value is always a config error), but it is a behavioral change. Projects with a malformed catalogs: value that previously worked silently will now see an error at load time. The test test_load_catalog_config_rejects_falsy_non_list_catalogs covers it. Low risk, but worth calling out in the changelog.


Minor — Orphaned MCP metadata on re-init

File: src/specify_cli/commands/init.py:857-870

When re-initializing without --team-ai-directives, the code carries over team_ai_directives_mcp from the previous init options even though the flag wasn't passed. This means config get team-ai-directives reports "not set" while MCP entries remain tracked in init-options.json. Not a functional bug (it self-heals on config unset/config set), but creates a transient inconsistency. Consider only carrying over team_ai_directives_mcp when team_ai_directives is also being set.


Posted by opencode (model: glm-5.2) on behalf of @lior

Retain MCP ownership only while its directives source is configured.
@kanfil
kanfil self-requested a review September 11, 2026 08:19
@kanfil

kanfil commented Sep 11, 2026

Copy link
Copy Markdown
Member

@therightstuff you should also suggest this to spec-kit and then we will delete the change here

@kanfil
kanfil merged commit 12e74a5 into tikalk:main Sep 11, 2026
@therightstuff
therightstuff deleted the feat/add-post-init-config-command branch September 11, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants