Research: Planning Artifact and Query Consistency
Mission: 078-planning-artifact-and-query-consistency Date: 2026-04-08 Method: Codebase exploration plus planning decisions confirmed during /spec-kitty.plan
Research Summary
The existing codebase already contains the right building blocks for this mission, but they are split across incompatible call paths.
src/specify_cli/core/worktree.pyalready treatsplanning_artifactwork as repo-root work.src/specify_cli/workspace_context.pyand the implement/workflow/status callers still assume every WP must resolve throughlanes.json.src/specify_cli/cli/commands/agent/mission.pyalready demonstrates the correct compatibility pattern: infer missing metadata once, keep it in memory, and pass normalized objects downstream.src/specify_cli/next/runtime_bridge.pyandsrc/specify_cli/cli/commands/next_cmd.pyalready expose query mode, but they still encode fresh-run state asunknownand make--agentmandatory for a read-only call.src/specify_cli/core/worktree_topology.pyandsrc/specify_cli/cli/commands/agent/tasks.pystill contain lane-only assumptions that would leave planning-artifact WPs half-supported if not included in scope.
The implementation plan therefore focuses on wiring, normalization, and contract cleanup, not on inventing a new runtime model.
R1: Compatibility Seam For Missing execution_mode
Decision
Normalize missing execution_mode once per command/session in memory, then pass only normalized metadata to downstream consumers.
Rationale
This creates one compatibility seam instead of repeating inference in status, implement, stale detection, and query call sites.
Validated findings
src/specify_cli/cli/commands/agent/mission.py:1468-1629already keeps_inmemory_frontmatterand_inmemory_bodiesso validate-only flows can rely on normalized metadata without writing disk state.src/specify_cli/ownership/inference.py:62-91exposesinfer_execution_mode(wp_content, wp_files)as a deterministic helper.src/specify_cli/status/wp_metadata.py:214-220provides immutable update semantics throughWPMetadata.update(...).src/specify_cli/ownership/models.py:80-93hard-fails ifOwnershipManifest.from_frontmatter()seesexecution_mode=None, which is exactly why repeated ad hoc inference is unsafe.
Alternatives considered
- Infer at every lookup site -> rejected because it duplicates logic and invites drift.
- Fail immediately when
execution_modeis missing -> rejected because the mission spec promises zero-migration support for supported historical missions.
R2: Canonical Workspace Resolution
Decision
Make src/specify_cli/workspace_context.py the single authoritative runtime resolver and extend it to return both lane-backed workspaces and repo-root planning work.
Rationale
The codebase already has one correct repo-root planning router, but it is currently dead for production flows. The fix is to make one resolver authoritative, not to invent placeholder lanes.
Validated findings
src/specify_cli/core/worktree.py:90-160implementscreate_wp_workspace()and already routesplanning_artifactWPs torepo_root.src/specify_cli/workspace_context.py:270-316resolves workspaces only through context files andlanes.json; it raisesValueErrorwhenlane_for_wp()returnsNone.src/specify_cli/cli/commands/implement.py:439-442hard-fails if the target WP is not assigned to a lane.src/specify_cli/lanes/implement_support.py:33-134is explicitly lane-only and creates workspace context files only for lane worktrees.src/specify_cli/cli/commands/agent/workflow.py,src/specify_cli/core/execution_context.py,src/specify_cli/next/prompt_builder.py,src/specify_cli/core/stale_detection.py, andsrc/specify_cli/core/worktree_topology.pyall callresolve_workspace_for_wp(...)directly.src/specify_cli/core/worktree_topology.py:112-135iterates every WP from the dependency graph, callslane_for_wp(), and raises when a planning-artifact WP is outsidelanes.json.src/specify_cli/cli/commands/agent/workflow.py:737-746currently swallows topology failures behindexcept Exception: pass, which means topology silently disappears for mixed missions today.
Alternatives considered
- Make planning-artifact WPs fake lane members -> rejected because it encodes repo-root work as worktree work.
- Add planning-artifact special cases in every caller -> rejected because it recreates the split-brain design this mission is fixing.
R3: Planning-Artifact Lifecycle Completion
Decision
Keep the existing lifecycle statuses, but interpret them in artifact terms for planning-artifact WPs:
for_review= repository-root planning artifacts are ready for reviewapproved= review passed and downstream dependents can startdone= artifacts have been accepted as complete, with no lane-merge precondition
Rationale
This preserves the canonical status-lane model while removing the incorrect assumption that completion must always be tied to branch or worktree merge semantics.
Validated findings
src/specify_cli/cli/commands/agent/tasks.py:2427-2648already treats lifecycle lanes as mission-wide status reporting, not as execution-lane topology.src/specify_cli/status/models.pyand downstream reducers already distinguish lifecycle lanes from execution lanes.- The spec now requires planning-artifact WPs to reach completion by accepted repository-root artifacts, not by merge of a lane branch.
src/specify_cli/cli/commands/agent/tasks.py:751-792andsrc/specify_cli/cli/commands/agent/tasks.py:1014-1043still gate--to doneon branch merge ancestry, which is correct forcode_changebut wrong for planning-artifact WPs under the new lifecycle contract.
Alternatives considered
- Skip
approvedfor planning-artifact WPs -> rejected because it would create a second lifecycle model. - Wait for mission merge to mark planning-artifact WPs done -> rejected because it would preserve the merge dependency the spec explicitly rejects.
R4: Stale Detection Contract
Decision
Expose a structured stale object everywhere this mission touches stale reporting. For planning-artifact WPs in repo root, use:
{
"status": "not_applicable",
"reason": "planning_artifact_repo_root_shared_workspace"
}
Rationale
Repo-root planning work shares one workspace. Any repo activity can make every planning-artifact WP look fresh, so commit-time freshness is intentionally not a meaningful concept there.
Validated findings
src/specify_cli/core/stale_detection.py:178-318treats a WP as stale based on commit activity in the resolved workspace path.src/specify_cli/core/stale_detection.py:271-275currently trusts whateverresolve_workspace_for_wp(...)returns and then assumes the path is a worktree.src/specify_cli/cli/commands/agent/tasks.py:2390-2459serializes stale data as flat booleans and minute counts, which cannot expressnot_applicablesafely.src/specify_cli/cli/commands/agent/tasks.py:2406-2408currently emitsis_stale,minutes_since_commit, andworktree_existsdirectly in JSON output, so the nestedstaleobject needs an explicit transition strategy.
Alternatives considered
- Omit stale fields for planning-artifact WPs -> rejected because omission is ambiguous for machine consumers.
- Report
unknown-> rejected becauseunknownmeans the system tried and failed, not that stale detection is intentionally invalid here. - Use repo-wide git heartbeat -> rejected because unrelated repo activity is not a WP-scoped freshness signal.
R5: Query Mode Contract
Decision
Make query mode agent-optional, return mission_state: "not_started" plus preview_step for fresh runs, and raise an actionable validation error when a mission has no issuable first step.
Rationale
Read-only query mode should not require an actor identity, and fresh-run state should be modeled explicitly rather than hidden inside unknown.
Validated findings
src/specify_cli/cli/commands/next_cmd.py:22-34currently makes--agentmandatory even when--resultis omitted.src/specify_cli/next/runtime_bridge.py:556-624already has a dedicatedquery_current_state()path, but it returnssnapshot.issued_step_id or "unknown".src/specify_cli/next/decision.py:45-92has no field forpreview_step, andDecision.agentis currently always a required string.- Human-readable query output in
src/specify_cli/cli/commands/next_cmd.py:192-205prints[QUERY - no result provided, state not advanced]andMission: <type> @ <state>, so the current CLI can already branch onis_query.
Alternatives considered
- Keep
--agentrequired in query mode -> rejected because it adds meaningless ceremony. - Encode the preview inside a compound state string -> rejected because it hides lifecycle state inside a machine-unfriendly string.
- Return success with no preview when the mission has no issuable first step -> rejected because it masks a mission-definition error.
R6: Machine-Facing Compatibility And Docs
Decision
Treat the query JSON change as an intentional contract change, document it explicitly, and update active docs and command reference examples in the same mission.
Rationale
Compatibility support for query callers that still pass --agent is useful, but callers that parse mission_state: "unknown" for fresh runs need an explicit migration note.
Validated findings
docs/index.mdanddocs/reference/cli-commands.mdcurrently drift from the runtime behavior described in the validated issue report.docs/explanation/runtime-loop.mdanddocs/reference/agent-subcommands.mdalso referencespec-kitty next --agent <agent>as the canonical entrypoint.- The spec now makes
not_started+preview_stepthe canonical fresh-run query shape.
Alternatives considered
- Treat the change as purely internal -> rejected because machine consumers may parse current JSON.
- Keep
unknownforever as a compatibility alias -> rejected because it preserves the ambiguous state model.
R7: Agent Context Update Surface
Decision
Record agent-context mutation as skipped for this mission.
Rationale
The current CLI no longer exposes an update-context command, and this mission introduces no new toolchain or dependency that requires agent-context file mutation.
Validated findings
src/specify_cli/cli/commands/agent/context.py:117-118explicitly states that the update-context command was removed.spec-kitty agent context --helpcurrently exposes onlyresolve.
Alternatives considered
- Reintroduce an update-context command as part of this mission -> rejected because it is unrelated to the runtime/workflow contract being fixed.
- Mutate agent directories directly -> rejected by repo governance and
.gitignorepolicy.