Implementer Quickstart — CLI Widen Mode & Decision Write-Back
Mission: cli-widen-mode-and-write-back-01KPXFGJ For: Implementing engineer (WP implementers)
This walkthrough covers the full development loop: environment setup, happy path verification, each test branch, and the release checklist.
1. Setup
1.1 Prerequisites
``bash cd /path/to/spec-kitty pip install -e ".[dev]" ``
- Python 3.11+ virtual environment with
spec-kitty-cliinstalled in editable mode: mypy,ruff,pytest,respx(httpx mock) installed in dev extras.spec-kitty-saasrunning locally or a staging deployment accessible.
1.2 Environment variables
# Required for widen mode (all three must be set for prereqs to pass)
export SPEC_KITTY_SAAS_URL="http://localhost:8000" # or staging URL
export SPEC_KITTY_SAAS_TOKEN="your-session-token" # from spec-kitty login or staging
# Optional: suppress LLM summarization timeout for fast manual testing
export SPEC_KITTY_WIDEN_SUMMARIZE_TIMEOUT="300" # 300s for interactive dev
If SPEC_KITTY_SAAS_TOKEN is not set, check_prereqs() returns saas_reachable=False and [w] is silently suppressed. The rest of the interview works normally (C-007).
1.3 Slack integration on staging team
For end-to-end Slack testing, ensure your staging Teamspace has:
- A Slack workspace connected (via
GET /api/v1/teams/{slug}/integrationsreturning["slack"]). - The bot token configured in the SaaS staging env.
For unit/integration tests, Slack is always mocked — no real Slack connection needed.
1.4 Verify prereqs with a doctor check
# (Out of scope for V1, but useful during dev)
spec-kitty doctor widen # If implemented in a future mission
During V1 development, verify prereqs manually:
python -c "
from specify_cli.widen.prereq import check_prereqs
from specify_cli.saas_client import SaasClient
client = SaasClient.from_env()
state = check_prereqs(client, team_slug='your-team-slug')
print(state)
"
2. Run charter interview and verify [w] appears
spec-kitty charter interview --mission-slug cli-widen-mode-and-write-back-01KPXFGJ
At the first question, you should see:
What is the project name? [my-project]:
[enter]=accept default | [text]=type answer | [w]iden | [d]efer | [!cancel]
If [w]iden is absent, check that all three prereqs are satisfied:
- Is
SPEC_KITTY_SAAS_TOKENset and valid? - Is
SPEC_KITTY_SAAS_URLpointing to a live instance? - Does your token's team have Slack integration? (
GET /api/v1/teams/{slug}/integrations)
3. Press w — audience review → trim → confirm → SaaS widen called
At any question prompt, type w and press Enter.
Expected flow: 1. CLI calls GET /api/v1/missions/{id}/audience-default and renders: `` ╭─ Widen: What is the project name? ────────────────────────────────╮ │ Default audience for this decision: │ │ Alice Johnson, Bob Smith, Carol Lee, Dana Park │ │ ... │ ` 2. Press Enter to accept all, or type Alice Johnson, Carol Lee to trim. 3. CLI calls POST /api/v1/decision-points/{id}/widen with {"invited": ["Alice Johnson", "Carol Lee"]}`. 4. Success banner appears with Slack thread URL.
To verify with a local mock instead of staging:
# In a separate terminal, start the httpx mock server (or use respx in tests)
# See tests/specify_cli/saas_client/test_client.py for mock patterns
In tests, use the CliRunner pattern:
from typer.testing import CliRunner
from specify_cli.cli.app import app
runner = CliRunner()
result = runner.invoke(app, ["charter", "interview", "--mission-slug", "test-mission"],
input="w\n\nAlice Johnson, Carol Lee\n\n")
assert "Slack thread created" in result.output
4. Test both [b] block and [c] continue paths
4.1 Block path
After widen succeeds and the [b/c] prompt appears, press Enter (default b):
Block here or continue with other questions? [b/c] (default: b):
Expected: interview pauses. You see:
╭─ Waiting for widened discussion ──────────────────────────────────╮
│ Question: What is the project name? │
│ Participants: Alice Johnson, Carol Lee │
│ Slack thread: https://... │
╰───────────────────────────────────────────────────────────────────╯
Waiting >
Verify the blocked prompt accepts f, plain text, and d.
4.2 Continue path
After widen succeeds, type c at the [b/c] prompt:
Block here or continue with other questions? [b/c] (default: b): c
Question parked as pending. You'll be prompted to resolve it at end of interview.
Expected: interview advances to the next question. The pending entry appears in kitty-specs/<slug>/widen-pending.jsonl.
cat kitty-specs/cli-widen-mode-and-write-back-01KPXFGJ/widen-pending.jsonl
# Should show one JSON line with the parked question's decision_id
At end of interview:
╭─ Pending Widened Questions ────────────────────────────────────────╮
│ 1 widened question is still pending... │
╰────────────────────────────────────────────────────────────────────╯
5. Test local-answer-at-blocked-prompt → SaaS observes terminal state
From the blocked prompt (Waiting >), type a plain-text answer:
Waiting > PostgreSQL with migration path planned from day 1
Expected: 1. CLI calls decision.resolve(final_answer="PostgreSQL...", summary_json={"source": "manual", "text": ""}). 2. CLI prints: Resolved locally. SaaS will close the Slack thread shortly. 3. Interview resumes at next question.
Verify in SaaS: GET /api/v1/decision-points/{id} should show status=resolved. Verify in Slack (staging): the thread should receive a closure message from #111 within ~30 seconds (SC-002).
6. Test review prompt [a/e/d] with mocked discussion fetch
From the blocked prompt, type f to fetch & review:
Waiting > f
Fetching discussion...
The CLI renders the LLM summarization request (contracts §5) and waits for the LLM response. In a test harness, inject the mock response:
# Simulate LLM producing a candidate block
mock_llm_response = '{"candidate_summary": "Team agrees on PostgreSQL.", "candidate_answer": "PostgreSQL.", "source_hint": "slack_extraction"}'
runner.invoke(app, [...], input=f"f\n{mock_llm_response}\na\n")
Verify [a]ccept:
decisions/index.jsonentry hasstatus=resolved,final_answer="PostgreSQL.".summary_json.source = "slack_extraction".
Verify [e]dit with material change:
runner.invoke(app, [...], input=f"f\n{mock_llm_response}\ne\n")
# Editor opens pre-filled with "PostgreSQL."
# User saves with "PostgreSQL with read replicas."
# CLI detects material change → prompts for optional rationale
summary_json.source = "mission_owner_override".
Verify [d]efer:
runner.invoke(app, [...], input=f"f\n{mock_llm_response}\nd\nNot enough context yet\n")
decisions/index.jsonentry hasstatus=deferred,rationale="Not enough context yet".
7. Test provenance tagging
slack_extraction
- Accept candidate unchanged:
[a]. - Minor edit (fix a typo in candidate):
[e], save with <30% distance change. - Verify:
summary_json.source == "slack_extraction".
mission_owner_override
- Material edit:
[e], save with significantly different text. - Verify:
summary_json.source == "mission_owner_override".
manual
- LLM timeout: set
SPEC_KITTY_WIDEN_SUMMARIZE_TIMEOUT=0(force timeout), then[f]etch & review. - Verify: editor opens blank, owner types fresh answer.
- Verify:
summary_json.source == "manual". - Also: plain-text answer at blocked prompt →
source == "manual".
SPEC_KITTY_WIDEN_SUMMARIZE_TIMEOUT=0 spec-kitty charter interview --mission-slug test
8. Test prereq suppression
User without Teamspace
# Use a token that belongs to no Teamspace (or unset the token)
unset SPEC_KITTY_SAAS_TOKEN
spec-kitty charter interview --mission-slug test
At every question, [w]iden must NOT appear in the prompt. The interview must complete normally using the existing [d]efer | [!cancel] options (SC-004, C-007, C-009).
Verify:
- No
[w]idenin any prompt text. - No error banners or noisy warnings about missing Teamspace.
answers.yamlwritten correctly at the end.
Slack integration not configured
Mock GET /api/v1/teams/{slug}/integrations to return [] (empty list):
# In test:
with respx.mock:
respx.get(".../integrations").respond(200, json=[])
result = runner.invoke(...)
assert "[w]iden" not in result.output
SaaS unreachable
export SPEC_KITTY_SAAS_URL="http://localhost:9999" # nothing listening
spec-kitty charter interview --mission-slug test
[w]iden suppressed. Interview completes locally (C-007).
9. Test suite commands
# Run all widen-related tests
cd /path/to/spec-kitty
pytest tests/specify_cli/widen/ -v
# Run charter interview integration tests (includes widen paths)
pytest tests/specify_cli/cli/commands/test_charter_widen.py -v
pytest tests/specify_cli/cli/commands/test_charter_prereq_suppression.py -v
# Run saas_client contract tests
pytest tests/specify_cli/saas_client/ -v
# Full suite (verify NFR-005: ≤90s added to baseline)
time pytest tests/ -x -q
# Type checking
mypy src/specify_cli/widen/ src/specify_cli/saas_client/
# Lint
ruff check src/specify_cli/widen/ src/specify_cli/saas_client/
Expected: all tests pass, mypy clean, ruff clean (NFR-006).
10. Release checklist
Before marking the implementation WP as ready for review:
- □ All new test files pass:
pytest tests/specify_cli/widen/ tests/specify_cli/saas_client/ - □ Charter interview integration tests pass (widen happy path, both
[b]and[c]branches, prereq suppression) - □
mypy src/specify_cli/widen/ src/specify_cli/saas_client/exits 0 - □
ruff check src/specify_cli/widen/ src/specify_cli/saas_client/exits 0 - □
WidenPendingStoreJSONL round-trip test passes (write + read + remove) - □
summary_json.sourceprovenance verified for all three values in tests - □
NFR-001: prompt render time (with mocked prereq check) < 300ms verified in test assertions - □
NFR-004: inactivity reminder fires at 60m (unit test with time mock) - □ Internal
spec-kitty agent decision widen --dry-runsubcommand works - □
[w]is NOT shown in prompt whenSPEC_KITTY_SAAS_TOKENis unset (SC-004) - □
answers.yamlis written correctly at interview completion with and without widen - □ No direct Slack API calls anywhere in
specify_clicodebase (grep confirms C-004) - □
contracts/widen-state.schema.jsonvalidates against a realwiden-pending.jsonlentry usingjsonschema - □ Full test suite wall-clock delta confirmed ≤ 90s (NFR-005)