Work Packages: State Architecture Cleanup Phase 2
Inputs: Design documents from kitty-specs/054-state-architecture-cleanup-phase-2/ Prerequisites: plan.md (required), spec.md (user stories), research.md, data-model.md, quickstart.md
Tests: Targeted tests included per cleanup area (required by spec).
Organization: Fine-grained subtasks (Txxx) roll up into work packages (WPxx). Each work package is independently deliverable and testable.
Prompt Files: Each work package references a matching prompt file in tasks/.
Work Package WP01: Shared Atomic Write Utility (Priority: P0)
Goal: Extract the atomic-write pattern from feature_metadata.py into a shared utility at src/specify_cli/core/atomic.py so all 9 stateful write paths can reuse it. Independent Test: pytest tests/specify_cli/test_atomic_write.py -v passes. The feature_metadata.py module still works after refactor. Prompt: tasks/WP01-shared-atomic-write-utility.md Requirement Refs: FR-008, FR-009, FR-010, FR-011, FR-012, FR-013, FR-014, FR-015, FR-016, NFR-001, C-004
Included Subtasks
- □ T001 Create
src/specify_cli/core/atomic.pywith publicatomic_write()function - □ T002 Refactor
src/specify_cli/feature_metadata.pyto import fromcore.atomicinstead of private_atomic_write() - □ T003 Create
tests/specify_cli/test_atomic_write.pywith success, interrupt, mkdir, bytes/str, and cleanup tests
Implementation Notes
- Extract the exact pattern from
feature_metadata.py:_atomic_write()(lines 84-108) - Add
mkdir: bool = Falseparameter for callers that needparent.mkdir(parents=True, exist_ok=True) - Support both
str(encoded to UTF-8) andbytes(raw) content - Use
.atomic-prefix (not.meta-) to distinguish from legacy usage - Ensure
BaseExceptioncatch for cleanup (handles KeyboardInterrupt)
Parallel Opportunities
- WP01 can run in parallel with WP02, WP06, WP07, WP08.
Dependencies
- None (foundation package).
Risks & Mitigations
- Permission preservation:
os.replace()may not preserve file permissions on some platforms → test withstatchecks.
Work Package WP02: Active-Mission Fallback Removal (Priority: P1)
Goal: Remove project-level .kittify/active-mission fallback from manifest, verify, and diagnostics so mission resolution uses feature-level meta.json. Independent Test: Create a project with a research-mission feature and no .kittify/active-mission; confirm verify/diagnostics resolve research. Prompt: tasks/WP02-active-mission-fallback-removal.md Requirement Refs: FR-001, FR-002, FR-003, FR-004
Included Subtasks
- □ T004 Remove
_detect_active_mission()fromsrc/specify_cli/manifest.pyand refactorFileManifestto not carryactive_missionproperty - □ T005 Update
src/specify_cli/verify_enhanced.pyto acceptfeature_dir: Path | Noneparameter and resolve mission frommeta.json - □ T006 Update
src/specify_cli/dashboard/diagnostics.pyto acceptfeature_dir: Path | Noneand resolve mission per-feature - □ T007 Update
src/specify_cli/cli/commands/mission.pycurrent_cmd()to show explicit "no active feature detected" instead of project-level fallback - □ T008 Add/update tests: manifest without active_mission, verify with feature context, diagnostics with feature context
Implementation Notes
FileManifest.__init__()currently setsself.active_mission = self._detect_active_mission()and derivesself.mission_dirfrom it. After removing, callers that need mission context must resolve it from featuremeta.jsonviaget_mission_for_feature().verify_enhanced.py::run_enhanced_verify()usesmanifest.active_missionfor file integrity checks. Refactor to acceptfeature_dirand callget_mission_for_feature(feature_dir, project_root).- The CLI
current_cmd()(lines 186-231) has two branches: feature-detected (usesget_mission_for_feature) and no-feature (usesget_active_mission). Change the no-feature branch to report "no feature context" instead.
Parallel Opportunities
- T004, T005, T006, T007 touch different files and can be developed concurrently by the same agent.
Dependencies
- None.
Risks & Mitigations
- Breaking callers of
FileManifest.active_missionorFileManifest.mission_dir→ grep all usages before removing.
Work Package WP03: Dead Mission Code Deletion (Priority: P1)
Goal: Delete deprecated set_active_mission(), unused get_active_mission_key(), and update the state contract to reflect removal. Independent Test: Grep for set_active_mission, get_active_mission_key in production code returns zero hits. Prompt: tasks/WP03-dead-mission-code-deletion.md Requirement Refs: FR-005, FR-006, FR-007
Included Subtasks
- □ T009 Delete
set_active_mission()fromsrc/specify_cli/mission.py - □ T010 Delete
get_active_mission_key()fromsrc/specify_cli/core/project_resolver.pyand remove fromsrc/specify_cli/core/__init__.pyexports - □ T011 Update
src/specify_cli/state_contract.py— removeactive_mission_markerentry entirely - □ T012 Add
.kittify/active-missionto.gitignore(prevent accidental recommit of legacy markers) - □ T013 Update/remove tests referencing deleted functions (
tests/runtime/test_project_resolver.py, any test callingset_active_mission)
Implementation Notes
set_active_mission()is at mission.py:523-566. It's deprecated since v0.8.0 with no production callers.get_active_mission_key()is at project_resolver.py:107-134. Only tests reference it.- When removing from state_contract, also remove the import of
DEFAULT_MISSION_KEYif it was only used for this entry. - Check if any migration references
set_active_mission()— them_0_8_0_remove_active_mission.pymigration may import it. If so, keep a minimal stub or inline the logic in the migration.
Parallel Opportunities
- T009, T010, T011, T012 touch different files and can proceed concurrently.
Dependencies
- Depends on WP02. Active-mission reads must be removed before deleting the code they call.
Risks & Mitigations
- Migration breakage:
m_0_8_0_remove_active_mission.pymay import the function being deleted → check and handle.
Work Package WP04: Atomic Write Conversion — Local State Files (Priority: P2)
Goal: Convert 5 local-state write paths to use the shared atomic_write() utility. Independent Test: For each converted path, mock os.replace to raise and confirm original file is untouched. Prompt: tasks/WP04-atomic-write-local-state.md Requirement Refs: FR-008, FR-009, FR-010, FR-011, FR-016, NFR-001
Included Subtasks
- □ T014 [P] Convert
src/specify_cli/next/runtime_bridge.py—_save_feature_runs(): replacepath.write_text(json.dumps(...))withatomic_write(path, content, mkdir=True) - □ T015 [P] Convert
src/specify_cli/workspace_context.py—save_context(): replacecontext_path.write_text(json.dumps(...))withatomic_write(context_path, content) - □ T016 [P] Convert
src/specify_cli/constitution/context.py—_write_state(): replacepath.write_text(json.dumps(...))withatomic_write(path, content, mkdir=True) - □ T017 [P] Convert
src/specify_cli/dashboard/lifecycle.py—_write_dashboard_file(): replacedashboard_file.write_text(...)withatomic_write(dashboard_file, content, mkdir=True) - □ T018 [P] Convert
src/specify_cli/upgrade/metadata.py—ProjectMetadata.save(): replaceopen() + yaml.dump()with serialize-to-string thenatomic_write(path, content, mkdir=True)
Implementation Notes
- Each conversion follows the same pattern: serialize content to string first, then call
atomic_write(path, content, mkdir=True). - For
metadata.py(T018): the header comment + yaml.dump must be serialized to a string buffer first (useio.StringIO), then passed toatomic_write. - For
dashboard/lifecycle.py(T017): the multi-line format (url, port, token, pid) is already assembled as a string beforewrite_text. - All 5 files currently call
path.parent.mkdir(parents=True, exist_ok=True)before writing — themkdir=Trueparameter onatomic_writehandles this.
Parallel Opportunities
- All 5 subtasks touch independent files — fully parallelizable.
Dependencies
- Depends on WP01 (shared
atomic_write()utility must exist).
Risks & Mitigations
- YAML serialization to string:
yaml.dump()withStringIOdestination may differ slightly from direct file dump → test output matches.
Work Package WP05: Atomic Write Conversion — Sync and Config (Priority: P2)
Goal: Convert 4 sync/config write paths to use the shared atomic_write() utility, handling special cases (file locks, existing atomic impl, TOML/YAML serialization). Independent Test: For each converted path, verify atomic semantics (interrupt leaves original intact). Prompt: tasks/WP05-atomic-write-sync-config.md Requirement Refs: FR-012, FR-013, FR-014, FR-015, NFR-001
Included Subtasks
- □ T019 Convert
src/specify_cli/sync/clock.py—LamportClock.save(): replace inlinetempfile.mkstemp + os.replacewithatomic_write(self._storage_path, content, mkdir=True) - □ T020 Convert
src/specify_cli/sync/auth.py—CredentialStore.save(): addatomic_write()inside the existingself._acquire_lock()context; keep file lock + 600 permissions - □ T021 [P] Convert
src/specify_cli/sync/config.py—set_server_url(): replaceopen() + toml.dump()with serialize-to-string thenatomic_write(path, content, mkdir=True) - □ T022 [P] Convert
src/specify_cli/tracker/config.py—save_tracker_config(): replaceopen() + YAML.dump()with serialize-to-string thenatomic_write(path, content, mkdir=True)
Implementation Notes
- clock.py (T019): Already has atomic write logic (lines 62-87). Replace with the shared utility. The
json.dump(data, f, indent=2)becomesjson.dumps(data, indent=2)→atomic_write(path, content). - auth.py (T020): Uses
filelockfor concurrent access. The atomic write goes INSIDE the lock context. Afteratomic_write, applyos.chmod(path, 0o600)on non-Windows. - config.py (T021):
toml.dump(config, f)→toml.dumps(config)→atomic_write(path, content, mkdir=True). - tracker/config.py (T022): Uses
ruamel.yaml.YAML.dump(). Serialize toio.StringIOfirst, thenatomic_write(path, stream.getvalue(), mkdir=True).
Parallel Opportunities
- T021 and T022 are fully independent. T019 and T020 also independent but both in
sync/.
Dependencies
- Depends on WP01 (shared
atomic_write()utility must exist).
Risks & Mitigations
auth.pypermission preservation:os.replace()doesn't preserve permissions → applyos.chmodafter replace.toml.dumps()availability: Verify thetomllibrary supportsdumps()(it does intomli_wandtoml).
Work Package WP06: Constitution Git Policy Enforcement (Priority: P2)
Goal: Enforce the hybrid Git policy: commit answers.yaml + library/*.md (shared team knowledge), ignore references.yaml (local machine state). Align .gitignore, state_contract.py, and code. Independent Test: git check-ignore .kittify/constitution/references.yaml returns the path; git check-ignore .kittify/constitution/interview/answers.yaml returns nothing. Prompt: tasks/WP06-constitution-git-policy.md Requirement Refs: FR-017, FR-018, FR-019, NFR-004
Included Subtasks
- □ T023 Add
.kittify/constitution/references.yamlto.gitignore(scope tightly — only this specific file, not wildcards) - □ T024 Update
src/specify_cli/state_contract.py:constitution_references→LOCAL_RUNTIME/IGNORED;constitution_library→AUTHORITATIVE/TRACKED;constitution_interview_answers→AUTHORITATIVE/TRACKED - □ T025 Remove "Git boundary decision deferred to constitution cleanup sprint" notes from state contract entries
- □ T026 Add test to
tests/specify_cli/test_state_contract.pyvalidating new classifications match actual.gitignoreand Git status
Implementation Notes
.gitignoreentry must be scoped to exactly.kittify/constitution/references.yaml— notreferences.*or broader patterns that could catch other files.state_contract.pycurrently has these entries around lines 237-311. Updateauthorityandgit_classfields.- The "deferred" notes appear in the
notes=field of each StateSurface entry. Replace with a note referencing this feature (e.g., "Policy enforced in 054"). - No migration needed —
.gitignorechanges take effect immediately.
Parallel Opportunities
- All subtasks are in different files (except T024/T025 both in state_contract.py).
Dependencies
- None.
Risks & Mitigations
references.yamlalready tracked in Git history: Adding to.gitignorestops future tracking but doesn't remove from history. This is intentional (C-005).
Work Package WP07: Acceptance Implementation Deduplication (Priority: P2)
Goal: Consolidate acceptance logic into acceptance.py as the single canonical implementation. Reduce acceptance_support.py to a pure re-export wrapper. Independent Test: Modify a validation rule in acceptance.py; confirm both CLI and standalone tasks_cli.py paths reflect the change. Prompt: tasks/WP07-acceptance-deduplication.md Requirement Refs: FR-020, NFR-002
Included Subtasks
- □ T027 Move
ArtifactEncodingErrorexception class fromacceptance_support.pytoacceptance.py - □ T028 Move
normalize_feature_encoding()fromacceptance_support.pytoacceptance.py - □ T029 Move
_read_text_strict()fromacceptance_support.pytoacceptance.py - □ T030 Align
AcceptanceSummary— ensurepath_violationsfield is consistent (present in canonical, was missing in standalone) - □ T031 Rewrite
acceptance_support.pyas pure re-export wrapper (~25 lines: imports +__all__) - □ T032 Update
tests/specify_cli/test_acceptance_regressions.py— parity test should now validate re-exports match canonical__all__
Implementation Notes
ArtifactEncodingErroris at acceptance_support.py:50-62. Custom exception with UTF-8 diagnostics.normalize_feature_encoding()is at acceptance_support.py:346-420. Converts Windows-1252/Latin-1 to UTF-8, maps smart quotes/dashes to ASCII._read_text_strict()is at acceptance_support.py:305-309. RaisesArtifactEncodingErroron decode failure.detect_feature_slug()diverges: standalone lacksannounce_fallbackparam. Merge into canonical withannounce_fallback: bool = True(backward compatible).- The parity test at test_acceptance_regressions.py:321-355 checks
__all__subset and signature parity. After dedup,acceptance_support.__all__should equalacceptance.__all__.
Parallel Opportunities
- T027, T028, T029 can be done together (moving functions). T030-T032 follow after.
Dependencies
- None.
Risks & Mitigations
- Import path breakage:
scripts/tasks/acceptance_support.pymust remain importable with the same names → re-export wrapper ensures this. - Signature divergence in
detect_feature_slug: use optionalannounce_fallbackparam with default → backward compatible.
Work Package WP08: Legacy Bridge Import Hardening (Priority: P3)
Goal: Make legacy_bridge a hard import in emit.py so missing module raises immediately instead of being silently swallowed. Remove stale WP06 transitional comment. Independent Test: Patch legacy_bridge import to raise ImportError; confirm emit_status_transition() raises (not silently succeeds). Prompt: tasks/WP08-legacy-bridge-hardening.md Requirement Refs: FR-021, FR-022, NFR-002
Included Subtasks
- □ T033 Move
from specify_cli.status.legacy_bridge import update_all_viewsto top-level import insrc/specify_cli/status/emit.py - □ T034 Remove the
except ImportError: passblock and the# WP06 not yet availablecomment - □ T035 Add test to
tests/status/test_emit.pythat patches the import to raiseImportErrorand asserts it propagates - □ T036 Update
test_legacy_bridge_import_error_handledtest — it currently asserts silent handling; change to assert the error is NOT silently handled
Implementation Notes
``python try: from specify_cli.status.legacy_bridge import update_all_views update_all_views(feature_dir, snapshot) except ImportError: pass # WP06 not yet available except Exception: logger.warning(...) ``
- Current code (emit.py:288-301):
- After change: top-level import + only the
except Exceptioncatch remains around theupdate_all_views()call. - The broad
except Exceptionfor bridge UPDATE failures is intentional and stays — canonical state is already persisted at that point (Step 5 before Step 7).
Parallel Opportunities
- T033/T034 are in the same file (do together). T035/T036 are test changes (do together).
Dependencies
- None.
Risks & Mitigations
- If any downstream packaging accidentally excludes
legacy_bridge.py, the hard import will fail at module load time. This is intentional — it surfaces the regression immediately.
Work Package WP09: Vault Notes Update and Final Validation (Priority: P3)
Goal: Update the Obsidian evidence vault with implementation outcomes, new evidence, and test results. Run full test suite and record results. Independent Test: Read vault notes; confirm each cleanup area has an implementation outcome entry. Prompt: tasks/WP09-vault-notes-update.md Requirement Refs: FR-023, NFR-002, NFR-003
Included Subtasks
- □ T037 Update
07-2026-03-20-refresh-findings.mdwith implementation outcomes for all 7 areas - □ T038 Add new entries to
08-evidence-log-2026-03-20.mdreferencing implementation commits and test results - □ T039 Run full test suite (
PWHEADLESS=1 pytest tests/ -q) and record results - □ T040 Create
09-implementation-outcome-054.mdwith summary of what changed, what was deferred, and what was intentionally left
Implementation Notes
- Vault absolute path:
/Users/robert/ClaudeCowork/Spec-Kitty-Cowork/spec-kitty-planning/research/007-spec-kitty-2x-state-architecture-audit/ - For each of the 7 cleanup areas, record: what was changed, which commits, test results.
- Mark areas in the "Still unresolved" section that are now resolved.
- Add any new findings discovered during implementation.
Parallel Opportunities
- T037 and T038 can be drafted in parallel. T039 must run after all code changes.
Dependencies
- Depends on WP01–WP08 (all implementation must be complete before recording outcomes).
Risks & Mitigations
- Stale references if implementation commits are amended → record commit hashes after final push.
Dependency & Execution Summary
- Wave 1 (parallel): WP01, WP02, WP06, WP07, WP08
- Wave 2 (parallel): WP03 (after WP02), WP04 (after WP01), WP05 (after WP01)
- Wave 3: WP09 (after WP01–WP08)
- MVP Scope: WP01 + WP02 + WP03 (fixes the highest-impact bug: mission mismatch)
- Parallelization: 5 WPs can start immediately; 3 more as soon as their single dependency completes.
Requirements Coverage Summary
| Requirement ID | Covered By Work Package(s) |
|---|---|
| FR-001 | WP02 |
| FR-002 | WP02 |
| FR-003 | WP02 |
| FR-004 | WP02 |
| FR-005 | WP03 |
| FR-006 | WP03 |
| FR-007 | WP03 |
| FR-008 | WP01, WP04 |
| FR-009 | WP01, WP04 |
| FR-010 | WP01, WP04 |
| FR-011 | WP01, WP04 |
| FR-012 | WP01, WP05 |
| FR-013 | WP01, WP05 |
| FR-014 | WP01, WP05 |
| FR-015 | WP01, WP05 |
| FR-016 | WP01, WP04 |
| FR-017 | WP06 |
| FR-018 | WP06 |
| FR-019 | WP06 |
| FR-020 | WP07 |
| FR-021 | WP08 |
| FR-022 | WP08 |
| FR-023 | WP09 |
| NFR-001 | WP01, WP04, WP05 |
| NFR-002 | WP07, WP08, WP09 |
| NFR-003 | WP09 |
| NFR-004 | WP06 |
| C-001 | All WPs |
| C-002 | WP02, WP03 |
| C-003 | All WPs |
| C-004 | WP01, WP04, WP05 |
| C-005 | WP06 |
| C-006 | WP09 |
Subtask Index (Reference)
| Subtask ID | Summary | Work Package | Priority | Parallel? |
|---|---|---|---|---|
| T001 | Create core/atomic.py with atomic_write() | WP01 | P0 | No |
| T002 | Refactor feature_metadata.py to use shared utility | WP01 | P0 | No |
| T003 | Tests for atomic_write | WP01 | P0 | No |
| T004 | Remove _detect_active_mission() from manifest | WP02 | P1 | Yes |
| T005 | Update verify_enhanced for feature-level mission | WP02 | P1 | Yes |
| T006 | Update diagnostics for feature-level mission | WP02 | P1 | Yes |
| T007 | Update mission CLI for no-feature-context | WP02 | P1 | Yes |
| T008 | Tests for mission resolution changes | WP02 | P1 | No |
| T009 | Delete set_active_mission() | WP03 | P1 | Yes |
| T010 | Delete get_active_mission_key() + exports | WP03 | P1 | Yes |
| T011 | Remove active_mission_marker from state contract | WP03 | P1 | Yes |
| T012 | Add active-mission to .gitignore | WP03 | P1 | Yes |
| T013 | Update/remove tests for deleted functions | WP03 | P1 | No |
| T014 | Convert runtime_bridge.py to atomic_write | WP04 | P2 | Yes |
| T015 | Convert workspace_context.py to atomic_write | WP04 | P2 | Yes |
| T016 | Convert constitution/context.py to atomic_write | WP04 | P2 | Yes |
| T017 | Convert dashboard/lifecycle.py to atomic_write | WP04 | P2 | Yes |
| T018 | Convert upgrade/metadata.py to atomic_write | WP04 | P2 | Yes |
| T019 | Convert sync/clock.py to shared atomic_write | WP05 | P2 | Yes |
| T020 | Convert sync/auth.py (keep lock + permissions) | WP05 | P2 | Yes |
| T021 | Convert sync/config.py to atomic_write | WP05 | P2 | Yes |
| T022 | Convert tracker/config.py to atomic_write | WP05 | P2 | Yes |
| T023 | Add references.yaml to .gitignore | WP06 | P2 | Yes |
| T024 | Update state_contract constitution entries | WP06 | P2 | No |
| T025 | Remove "deferred" notes from state contract | WP06 | P2 | No |
| T026 | Test new constitution classifications | WP06 | P2 | No |
| T027 | Move ArtifactEncodingError to acceptance.py | WP07 | P2 | Yes |
| T028 | Move normalize_feature_encoding() to acceptance.py | WP07 | P2 | Yes |
| T029 | Move _read_text_strict() to acceptance.py | WP07 | P2 | Yes |
| T030 | Align AcceptanceSummary path_violations | WP07 | P2 | No |
| T031 | Rewrite acceptance_support.py as re-export wrapper | WP07 | P2 | No |
| T032 | Update acceptance regression tests | WP07 | P2 | No |
| T033 | Move legacy_bridge to top-level import | WP08 | P3 | No |
| T034 | Remove ImportError catch + WP06 comment | WP08 | P3 | No |
| T035 | Add test for ImportError propagation | WP08 | P3 | No |
| T036 | Update existing test for new behavior | WP08 | P3 | No |
| T037 | Update refresh findings vault note | WP09 | P3 | Yes |
| T038 | Add evidence log entries | WP09 | P3 | Yes |
| T039 | Run full test suite and record results | WP09 | P3 | No |
| T040 | Create implementation outcome note | WP09 | P3 | No |