WP Prompt & Ops Debrief — Model / Schema Proposal
First-cut concrete schema suggestion, following the grounded idea note
(wp-op-schema-model.md) and the ticket map
(wp-op-schema-related-tickets.md). Pydantic v2,
model-first (registers into scripts/generate_schemas.py like the 10 doctrine
schemas).
Decisions locked (operator, 2026-07-16)
| Decision | Choice | Consequence |
|---|---|---|
| WP prompt authority | YAML-authoritative, markdown derived | wps.yaml (enriched) is the source of truth; tasks/WP##.md is rendered one-way, "do not edit". |
| Ops debrief obligation | Required on every Op close | A structured debrief is a mandatory field on the completion event, not opt-in. |
Both are the maximal choices. This proposal designs to them and builds in
the mitigations for the two risks that stance carries (§Risks):
body-authoring collision, wps.yaml adoption (stalled at 5/278), and trivial-op
friction.
Status: pressure-tested → REWORK. A 3-lens adversarial squad (Part 4) found the intent of both choices sound but the mechanism/sequencing not buildable as written (4 BLOCKERs). Both choices are preserved; Part 4 carries the corrected, buildable path. Read Parts 1–3 as the design under review and Part 4 as the current direction.
Part 1 — WP Prompt model
1.1 Design principles (from grounding)
- One canonical model, not a fourth. Today three field lists describe a WP
(
WorkPackageEntry,WPMetadata,frontmatter.WP_FIELD_ORDER). This proposal elects and enrichesWorkPackageEntry→WorkPackageSpecas the single authoring model;WPMetadata/WP_FIELD_ORDERbecome derived read-projections of it, not parallel authorities. - Static design-intent only in the spec; dynamic runtime state stays in the
event log. This is #2093/#2400's ruling, kept intact — the model carries
id/title/deps/owned_files/acceptance/…but nevershell_pid,history,lane,review_*. Those churn and are event-log-owned. - Three content classes, three homes. Every line in today's
WP##.mdis one of: authored semantic (→ the spec), boilerplate (→ rendered from the doctrine step template), or mutable runtime (→ rendered from the event log at display time). Only the first is authored/hashed.
Body-section census (real WPs) mapped to class:
Today's ## section |
Freq | Class | Home |
|---|---|---|---|
| Objective / Objectives & Success Criteria | 1727 | authored | prompt.objective |
| Context & Constraints | 1557 | authored | prompt.context |
| Definition of Done / Acceptance | 1273 | authored | prompt.acceptance[] |
| Subtasks & Detailed Guidance | 1267 | authored | prompt.steps[] |
| Risks & Mitigations | 1294 | authored | prompt.risks[] |
| Test Strategy | 470 | authored | prompt.test_strategy |
| Review/Reviewer Guidance | 1396 | authored | prompt.review_guidance |
| Non-goals | (sample) | authored | prompt.non_goals[] |
| References / Spec anchors | 56+ | authored | prompt.references[] |
| Load Agent Profile | 1024 | boilerplate | doctrine step template |
| Branch Strategy | 1184 | metadata | WorkPackageSpec (static) |
| Activity Log | 1670 | mutable | event log → derived md |
| Review Feedback / Status | 316 | mutable | event log → derived md |
1.2 Model sketch
# specify_cli/core/wp_spec.py (enriches core/wps_manifest.py:WorkPackageEntry)
class AcceptanceCriterion(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str # "AC-01" (pattern ^AC-\d{2}$)
statement: str = Field(min_length=1)
verify: str | None = None # test id / command / "manual: …"
class WPStep(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str # "T001" (pattern ^T\d{3}$)
description: str = Field(min_length=1)
guidance: str | None = None # multi-line prose allowed
class WPPromptBody(BaseModel):
"""Authored semantic content — the prose an agent needs to do the work."""
model_config = ConfigDict(extra="forbid")
objective: str = Field(min_length=1) # the one required narrative
context: str | None = None
scope_note: str | None = None # prose; owned_files is structured, below
steps: list[WPStep] = Field(default_factory=list)
acceptance: list[AcceptanceCriterion] = Field(default_factory=list)
non_goals: list[str] = Field(default_factory=list)
risks: list[str] = Field(default_factory=list)
test_strategy: str | None = None
review_guidance: str | None = None
references: list[str] = Field(default_factory=list) # FR-/NFR-/C- anchors, URLs, tracker refs
class WorkPackageSpec(BaseModel):
"""THE canonical WP authoring model. Static design-intent only."""
model_config = ConfigDict(extra="forbid")
# identity + dependency graph (from WorkPackageEntry, kept)
id: str # ^WP\d{2}$
title: str = Field(min_length=1)
dependencies: list[str] = Field(default_factory=list) # ^WP\d{2}$
# static design-intent (from WPMetadata static subset)
requirement_refs: list[str] = Field(default_factory=list)
plan_concern_refs: list[str] = Field(default_factory=list) # ^IC-\d{2}$
tracker_refs: list[str] = Field(default_factory=list)
owned_files: list[str] = Field(default_factory=list)
create_intent: list[str] = Field(default_factory=list)
authoritative_surface: str | None = None
scope: Literal["codebase-wide"] | None = None
task_type: str | None = None
cross_cutting: bool = False
agent_profile: str | None = None # authored assignment; resolved binding → event log
# authored body
prompt: WPPromptBody
# DELIBERATELY ABSENT (event-log owned, never authored/hashed here):
# shell_pid, shell_pid_created_at, history, lane, review_status,
# reviewed_by, review_feedback, base_branch, base_commit, created_at
wps.yaml becomes work_packages: list[WorkPackageSpec] (the existing
WpsManifest container, enriched).
1.3 The flow: author → validate → render → hash
wps.yaml (WorkPackageSpec[], SOURCE OF TRUTH — authored by human OR agent)
│ load + Pydantic validate (extra="forbid", pattern checks)
▼
WorkPackageSpec ──► render_wp_markdown(spec, step_template, event_log)
│ │
│ ├─ authored sections ← spec.prompt.*
│ ├─ boilerplate ← doctrine step template
│ └─ Activity Log / Review ← event log (display-time)
│ ▼
│ tasks/WP03.md (DERIVED, "_Generated — do not edit directly._")
▼
content_hash = sha256(canonical_yaml(spec)) # ← hashes the SPEC, not the md
This kills the hash-churn the whole arc started from: the dossier/sync hash
(dossier/hasher.py, sync/body_upload.py) moves off the whole rendered file onto
the canonical WorkPackageSpec projection. An Activity-Log append or a review-cycle
write lands only in the derived markdown / event log — it never touches the hashed
spec. Extends the proven generate_tasks_md_from_manifest() renderer
(core/wps_manifest.py:170) from the index to the per-WP file.
1.4 On-disk example
wps.yaml (source) → tasks/WP03.md (derived)
# wps.yaml (authored)
work_packages:
- id: WP03
title: Semantic-only WP content hash
dependencies: [WP01]
requirement_refs: [FR-004, NFR-002]
owned_files: [src/specify_cli/dossier/hasher.py, src/specify_cli/dossier/indexer.py]
agent_profile: python-pedro
prompt:
objective: Hash the WorkPackageSpec projection, not the rendered markdown bytes.
context: |
Today dossier/hasher.py streams whole-file bytes; any inert edit trips parity drift.
steps:
- id: T001
description: Route indexer through WorkPackageSpec canonical projection.
acceptance:
- id: AC-01
statement: An Activity-Log append does not change the WP content hash.
verify: tests/dossier/test_semantic_hash.py::test_activity_log_inert
non_goals: [Changing the parity-hash algorithm itself]
risks: [Legacy WPs without wps.yaml must fall back to whole-file hash]
test_strategy: Red test first — append to activity log, assert hash stable.
review_guidance: Confirm no raw-byte hashing path remains for wps.yaml missions.
references: [FR-004, NFR-002, "docs/.../wp-op-schema-model.md"]
<!-- tasks/WP03.md — GENERATED, do not edit directly -->
# WP03 — Semantic-only WP content hash
_Generated from wps.yaml. Do not edit directly._
**Dependencies**: WP01 · **Requirement Refs**: FR-004, NFR-002
## ⚡ Do This First: Load Agent Profile <!-- boilerplate from step template -->
## Objective
Hash the WorkPackageSpec projection, not the rendered markdown bytes.
## Definition of Done
- [ ] AC-01 — An Activity-Log append does not change the WP content hash. _(verify: tests/…)_
## Activity Log <!-- rendered from event log, display-only -->
- 2026-07-16 claimed by python-pedro
Part 2 — Ops Debrief model
2.1 Design principles
- Field-extension, not a new primitive. The debrief attaches to the existing
OpCompletedEvent(invocation/record.py:65) — honouring ADR 2026-06-11-1's C-005 no-parallel-primitive. No "small-WP-for-an-Op" artifact. - Required presence, graduated depth. The operator chose required on close.
To keep that from taxing trivial lookups, presence is always mandatory but
required depth scales with
mode_of_work(carried from the started event). This is the mitigation for the friction the maximal choice accepts. - Elevates Tier-1. Today
MinimalViableTrailPolicytier-1 records facts. This makes a structured why/what part of the mandatory tier-1 record — the exact "Ops persist intent" change the idea wanted.
2.2 Model sketch
class OpDebrief(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
intent: str = Field(min_length=1) # WHY — restated purpose (≥ request_text)
change_surface: list[str] = Field(default_factory=list) # WHAT — files/areas touched/intended
actions: list[str] = Field(default_factory=list) # what was done (bullets)
decisions: list[str] = Field(default_factory=list) # key choices / tradeoffs
follow_ups: list[str] = Field(default_factory=list) # residual work / tickets
class OpCompletedEvent(BaseModel): # EXTENDED
... # existing: outcome, closed_by, completed_at, evidence_ref
debrief: OpDebrief # NOW REQUIRED
2.3 Graduated-depth rule (the friction mitigation)
Validated against the started event's mode_of_work:
mode_of_work |
Required in debrief | Auto-fillable |
|---|---|---|
query |
intent only |
intent ← request_text; rest empty ✔ |
advisory |
intent |
intent ← request_text; actions optional |
task_execution |
intent + change_surface (≥1) + actions (≥1) |
none — agent must author |
mission_step |
intent + change_surface + actions |
none |
So a trivial query Op still closes in one line (auto-derived), while a
change-making Op must state what surface it touched and what it did — restoring the
ad-hoc↔op distinction exactly where work happened. closed_by="doctor_sweep"
(orphan auto-close) fills a sentinel intent="unrecorded — swept", never fabricates.
2.4 Debrief closes the loop with the WP
OpDebrief.change_surface is the Op-scale analogue of WorkPackageSpec.owned_files;
intent/actions/decisions mirror prompt.objective/steps/review rationale.
Same vocabulary at both grains — a mission WP and an ad-hoc Op now both answer
"what surface, to what end, with what result."
Part 3 — Schema generation & registration
Model-first, reusing scripts/generate_schemas.py (the machinery behind the 10
doctrine schemas + the --check drift gate).
3.1 One structural change: output-dir override
Today register() writes every schema under a single module constant
SCHEMA_DIR = src/doctrine/schemas/ (generate_schemas.py:34). WP/Op models live
outside the doctrine tree, so the only pipeline change needed is a per-entry
output directory. Minimal, backward-compatible extension:
# generate_schemas.py — add an optional schema_dir to the registry tuple
def register(stem, module, cls, title, description, extra=None, *,
by_alias=False, schema_dir: Path = SCHEMA_DIR) -> None:
REGISTRY[stem] = (module, cls, title, description, extra, by_alias, schema_dir)
# _emit() writes to that entry's schema_dir instead of the global SCHEMA_DIR.
New sink: SPECIFY_SCHEMA_DIR = src/specify_cli/schemas/. No doctrine schema moves;
existing entries default to SCHEMA_DIR unchanged.
3.2 Registration calls
SPECIFY_SCHEMA_DIR = ROOT / "src" / "specify_cli" / "schemas"
register(
"work-package-spec",
"specify_cli.core.wp_spec", "WorkPackageSpec",
"Work Package Spec",
"Canonical YAML-authoritative work-package record; tasks/WP##.md is derived.",
extra=lambda s: _add_item_patterns(s, {
"dependencies": r"^WP\d{2}$",
"plan_concern_refs": r"^IC-\d{2}$",
}),
schema_dir=SPECIFY_SCHEMA_DIR,
)
register(
"op-completed-event",
"specify_cli.invocation.record", "OpCompletedEvent",
"Op Completed Event",
"Op completion event carrying the mandatory structured debrief (why/what).",
schema_dir=SPECIFY_SCHEMA_DIR,
)
AcceptanceCriterion/WPStep/WPPromptBody/OpDebrief need no separate entry —
they emit as definitions/* inside their parent (same as tactic_step).
3.3 Example generated output — work-package-spec.schema.yaml
generated schema (abridged; mirrors the toolguide format)
$schema: https://json-schema.org/draft/2020-12/schema
$id: https://spec-kitty.dev/schemas/doctrine/work-package-spec.schema.yaml
title: Work Package Spec
description: Canonical YAML-authoritative work-package record; tasks/WP##.md is derived.
type: object
additionalProperties: false # ← from model_config extra="forbid"
required:
- id
- title
- prompt
properties:
id:
type: string
pattern: ^WP\d{2}$
title:
type: string
minLength: 1 # ← required string, auto-added
dependencies:
type: array
items: {type: string, pattern: ^WP\d{2}$}
requirement_refs:
type: array
items: {type: string}
plan_concern_refs:
type: array
items: {type: string, pattern: ^IC-\d{2}$}
owned_files:
type: array
items: {type: string}
create_intent:
type: array
items: {type: string}
authoritative_surface: {type: string}
scope: {const: codebase-wide} # single-value Literal emits const, not enum (see Part 4 F-S1)
task_type: {type: string}
cross_cutting: {type: boolean}
agent_profile: {type: string}
tracker_refs:
type: array
items: {type: string}
prompt:
$ref: '#/definitions/wp_prompt_body'
definitions:
acceptance_criterion:
type: object
additionalProperties: false
required: [id, statement]
properties:
id: {type: string, pattern: ^AC-\d{2}$}
statement: {type: string, minLength: 1}
verify: {type: string}
wp_step:
type: object
additionalProperties: false
required: [id, description]
properties:
id: {type: string, pattern: ^T\d{3}$}
description: {type: string, minLength: 1}
guidance: {type: string}
wp_prompt_body:
type: object
additionalProperties: false
required: [objective]
properties:
objective: {type: string, minLength: 1}
context: {type: string}
scope_note: {type: string}
steps:
type: array
items: {$ref: '#/definitions/wp_step'}
acceptance:
type: array
items: {$ref: '#/definitions/acceptance_criterion'}
non_goals: {type: array, items: {type: string}}
risks: {type: array, items: {type: string}}
test_strategy: {type: string}
review_guidance: {type: string}
references: {type: array, items: {type: string}}
3.4 Example generated output — op-completed-event.schema.yaml
generated schema (abridged)
$schema: https://json-schema.org/draft/2020-12/schema
$id: https://spec-kitty.dev/schemas/doctrine/op-completed-event.schema.yaml
title: Op Completed Event
description: Op completion event carrying the mandatory structured debrief (why/what).
type: object
additionalProperties: false
required: # NB: 'event' has a default → NOT in required (Part 4 F-S1)
- invocation_id
- completed_at
- outcome
- closed_by
properties:
event: {const: completed}
invocation_id: {type: string, pattern: ^[0-9A-HJKMNP-TV-Z]{26}$}
completed_at: {type: string, minLength: 1}
outcome: {type: string, enum: [done, failed, abandoned]}
closed_by: {type: string, enum: [agent, doctor_sweep]}
evidence_ref: {type: string}
debrief:
$ref: '#/definitions/op_debrief'
definitions:
op_debrief:
type: object
additionalProperties: false
required: [intent] # graduated depth enforced in-model, not schema
properties:
intent: {type: string, minLength: 1}
change_surface: {type: array, items: {type: string}}
actions: {type: array, items: {type: string}}
decisions: {type: array, items: {type: string}}
follow_ups: {type: array, items: {type: string}}
Note — where graduated depth lives. JSON-Schema
requiredis static, so the schema fixes only the floor (intent). The mode-conditional depth (§2.3 —task_executionalso needschange_surface+actions) is a Pydanticmodel_validatoronOpCompletedEventthat reads the started event'smode_of_work. Keep it in the model, not the JSON-Schema, so the drift gate and the runtime enforce the same floor and the model adds the conditional ceiling.
3.5 Drift gate
python scripts/generate_schemas.py --check gains both stems; the
clean-install-verification / schema-freshness tests fail if
wp_spec.py / record.py drift from the emitted YAML — identical guarantee to the
doctrine schemas today.
Risks & reconciliation
| Risk (from grounding) | Mitigation in this design |
|---|---|
| #2093 decided frontmatter stays authority | This proposal supersedes that persistence ruling (YAML-authoritative per operator choice) but preserves its substance — static-intent-only, dynamic→event-log. ⚠ This flip must be ratified with #2400's owner before implementation — it is the one place we override a decided ticket, not just consume it. |
wps.yaml stalled at 5/278 missions |
Adoption, not the model, was the blocker. Mitigation: (a) a spec-kitty migrate wp-spec backfill that parses today's markdown bodies → WorkPackageSpec; (b) runtime reads spec-first, whole-file-hash fallback for un-migrated missions (no flag day); (c) fix #2642 (runtime still requires tasks.md) as a hard dependency. |
| Body no longer hand-editable | The authoring surface moves to wps.yaml (agents author structured fields directly; humans edit YAML). The rendered WP##.md carries a "do not edit" stamp + a pre-commit guard that rejects edits to derived files, pointing back to the spec. |
| Required debrief taxes trivial ops | Graduated depth (§2.3): query/advisory auto-fill from request_text; only change-making ops must author. |
Smallest shippable path (unchanged from grounding, now concrete)
⚠ Superseded by Part 4. The adversarial squad found steps 3–4 below blocked by runtime writers to
WP##.md(B3) and the debrief mechanism unbuildable as sketched (B1/B2). Follow Part 4's revised path.
- Tidy-first — extract
WPPromptBody/WorkPackageSpec, register ingenerate_schemas.py; makeWPMetadataa read-projection. No behaviour change. - Semantic-only hash — repoint
dossier/synchashing at the spec projection (kills churn; the unticketed high-signal slice). - Render per-WP md from spec — extend
generate_tasks_md_from_manifest; add the do-not-edit guard; ship themigrate wp-specbackfill. - Op debrief — extend
OpCompletedEvent; graduated-depth validator; updateprofile-invocation completeto collect/auto-fill it. Independent of 1–3.
Open questions for the next iteration
- Reconciliation gate: do we take the #2093 authority-flip to #2400's owner as an ADR amendment, or fold this under #1676 (which already wants model-first authoring)?
- Boilerplate rendering: does the per-WP
WP##.mdrender pull boilerplate from the resolved doctrine step template live, or snapshot it at finalize time? - Debrief authoring UX: does
profile-invocation completeprompt for the debrief fields interactively, accept--debrief-file, or inferchange_surfacefrom the Op's git diff? - Should
agent_profile(authored) vs resolved-binding live as two names, to keep #2093's authored-intent/resolved-binding split crisp inside the spec?
Part 4 — Squad pressure-test (2026-07-16)
A three-lens adversarial squad (architect-alphonso · reviewer-renata ·
paula-patterns), read-only against this proposal + the real code.
Verdict: 3 × REWORK, unanimous. The design is directionally right but not
buildable as written — four BLOCKERs, all verified against file:line, plus a
cluster of MAJORs. The two locked operator choices survive in intent; only their
mechanism and sequencing must change.
Consolidated findings
| ID | Sev | Finding | Evidence |
|---|---|---|---|
| B1 | BLOCKER | Graduated-depth validator can't see mode_of_work. It lives on OpStartedEvent, not OpCompletedEvent; a model_validator on the completed model literally cannot read it, and would fire on every historical read. |
record.py:48 vs :65; parse_op_event :100. Fix: enforce at executor.complete_invocation, which already reads it via _read_started_mode() (executor.py:381). |
| B2 | BLOCKER | Required debrief breaks read-back of every existing Op. parse_op_event validates historical kitty-ops/*.jsonl completed lines; a new required field reclassifies all of them LegacyRecordError. No Op-record migration was specified. |
record.py:100,106. Fix: keep debrief optional on the model (read path); enforce presence + depth at the emission seam only. |
| B3 | BLOCKER | WP##.md is written at runtime by live phase-2 code — declaring it read-only-derived clobbers claim-liveness and done-inference. implement.py:1730 writes shell_pid on every implement; subtask checkboxes are flipped and read back as implementation evidence. |
implement.py:1730, stale_detection.py:221, subtask_rows.py:181, tasks_mark_status.py:446. Fix: evicting every runtime writer off WP##.md into the event log is a prerequisite mission, not a step in this one. |
| B4 | BLOCKER-for-"no-behaviour-change" | The demotion is on the wrong axis → 4 authorities, not 1. WPMetadata is a superset (static ⋈ runtime), so it cannot become a read-projection of a static-only WorkPackageSpec. Step-1 "make WPMetadata a projection, no behaviour change" is a no-op-alias (4 live lists) or requires the B3 migration first. |
wp_metadata.py:183,204-272, frontmatter.py:49, wps_manifest.py:16. Grounding's own Slice-0 was the reverse: make WP_FIELD_ORDER+WorkPackageEntry derive from WPMetadata. |
| M1 | MAJOR | Silent static-field loss. Census sends Branch Strategy (1184×) to the spec, but the model omits branch_strategy/merge_target_branch/planning_base_branch (77+ consumer sites) and priority/phase — static planning intent, not runtime. |
wp_metadata.py planning block; worktree.py:229. |
| M2 | MAJOR | Hash-on-spec breaks body_upload. It re-hashes raw bytes as a TOCTOU guard and uploads the rendered md; a spec hash → guaranteed content_hash_mismatch, or re-plumbing what the dashboard displays. Mixed spec/raw parity pool also breaks drift across 273 un-migrated missions. |
sync/body_upload.py:116,207; dossier/hasher.py:170. |
| M3 | MAJOR | The 5 existing wps.yaml become invalid. extra="forbid" + required prompt rejects today's subtasks:[…]/prompt_file: shape. The "enriches, no flag day" framing is contradicted — the only adopters stop loading. |
real kitty-specs/083-*/wps.yaml; wps_manifest.py:15. |
| M4 | MAJOR | Fallback = permanent split-brain. Spec-first + whole-file-hash fallback keeps two authoring models + two hash algorithms + a no-op-on-legacy renderer alive forever — more split surface than today. | mission_finalize.py:1703 (renderer wired at planning only). |
| M5 | MAJOR | register() override is 4 edits, not 1, and the $id stays in the /doctrine/ namespace. generate_schema unpacks a fixed 6-tuple (:888); write_schema/check_schema/main hardcode SCHEMA_DIR (:954,974,1013); _schema_id hardcodes the doctrine host (:52). |
generate_schemas.py:52,888,947-975. |
| M6 | MAJOR | Invented IDs unsupported by corpus. AC-\d{2} appears in 3/1793 WPs; the backfill must synthesize acceptance IDs from freeform DoD checkboxes for 99.8% of missions — not "reliable." |
body census. |
| M7 | MAJOR | Event log can't reconstruct Activity-Log narrative or review-feedback text — it carries transition fields + a review_ref pointer, not prose. "Rendered from event log" is silent data loss unless new event payloads are added. |
wp_metadata.py:271-272; status event shape. |
| M8 | MAJOR | #1619 dropped from Risks though the grounding made "settle the mid-revision WP/Mission aggregate first" a hard precondition. Electing a WP authority model now front-runs it. | grounding §Sequencing; tickets.md #1666. |
| F-S1 | MINOR | Example-schema divergences (now corrected in Part 3): single-value Literal→const not enum; event has a default so is not required; $id namespace mismatch. definitions/* naming was faithful. |
generate_schemas.py:522,570. |
| F-S2 | MINOR | Dual cross-cutting encoding — scope: Literal["codebase-wide"] and cross_cutting: bool both model "exempt from ownership checks" in the model meant to be canonical. Pick one. |
§1.2. |
What survives, and the corrected direction
The two operator choices are honoured — mechanism/sequencing corrected:
Op debrief (required on close) — LANDABLE on its own, after a small relocation. Keep
debriefoptional onOpCompletedEvent(fixes B2's read-back break); enforce required-presence + graduated-depth atexecutor.complete_invocation, which already reads the startedmode_of_work(fixes B1). "Required on close" is preserved — presence is still mandatory, just enforced at the write seam, not in a model that also parses history. This is a clean, independent slice (all three reviewers agree) and should be carved out and shipped first.WP YAML-authority (markdown derived) — NOT landable as sequenced; it has a prerequisite mission. The derived-md flip is blocked behind B3:
shell_pid(claim-liveness), subtask-checkbox state (done-inference), Activity-Log narrative, and review-feedback prose are all runtime-mutable state living insideWP##.md. The real enabling mission is "evict every runtime writer offWP##.mdinto the event log" (generalising the shippedlaneretirement — this is exactly #2093/#2400's charter). Only once the file holds nothing runtime can it become derived, the content-hash move (M2) co-designed withbody_upload, and the authority question re-opened — and there, the grounding's original axis (enrichWPMetadata, not elect the 9-fieldWorkPackageEntry) is the sounder election (B4).
Revised smallest path (supersedes §"Smallest shippable path"):
- Op debrief slice — optional field + executor-seam enforcement + graduated depth. Independent, low-risk, ships now. (Fixes B1, B2.)
- Runtime-writer eviction from
WP##.md— the real prerequisite mission (shell_pid, checkbox state, activity-log, review-feedback → event log). Reconcile with #2093/#2400. (Unblocks B3, B4, M7.) - Semantic-only content-hash — co-designed with
body_upload/parity, atomic across all missions (no mixed pool). (Fixes M2, M4.) - Then — WP model formalization (enrich
WPMetadata; full field reconciliation M1/M3/M6; register with the 4-editgenerate_schemas.pychange M5), and only last the YAML-authoritative/derived-markdown flip, gated on #1619 (M8).
Net: the arc's original grounded disposition holds — the Op field and the semantic hash are the near-term wins; the full YAML-authoritative flip is real but sits behind a runtime-eviction mission and the #1619 aggregate. The maximal WP choice isn't wrong, it's out of order: the enabling migration is the mission, the schema is the finish.