diff options
| author | soryu <soryu@soryu.co> | 2026-04-30 17:09:45 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-30 17:09:45 +0100 |
| commit | c03e9a323e266c6a9a7ccb17bbbb7841296bbd5c (patch) | |
| tree | 34c86fc502ea3232b4a50baf3accc9de38bf70c6 /makima/src/orchestration/directive.rs | |
| parent | fe6b78fa59657449be2e888402e3a0197b5c0621 (diff) | |
| download | soryu-c03e9a323e266c6a9a7ccb17bbbb7841296bbd5c.tar.gz soryu-c03e9a323e266c6a9a7ccb17bbbb7841296bbd5c.zip | |
feat(directives): amendment lifecycle — inactive status, new draft, before/after diff (#113)
Stage 4 of the doc-mode revamp. Closes the loop on living-spec contracts:
once a contract ships (PR raised) it becomes 'inactive', editing it kicks
off an amendment cycle, the planner sees the previously-merged content as
context, and "New draft" lets users abandon amendment and start the next
contract on a clean slate.
## inactive lifecycle
- New status `'inactive'`. Set automatically when `update_directive` detects
a `pr_url` transition None → Some, alongside the revision snapshot
(set_directive_inactive: idempotent, only flips active/idle/paused).
- `update_directive_goal` extends its CASE flip to include 'inactive', so
editing a shipped contract's goal reactivates it for the planner.
- Frontend: `DirectiveStatus` gains 'inactive'; STATUS_DOT and the legacy
STATUS_BADGEs (DirectiveDetail, DirectiveList) get color/label entries.
Sidebar sort puts inactive after draft / before archived.
## Amendment diff to the orchestrator
`build_planning_prompt` takes a new `previous_merged_revision` parameter.
When set, it prepends an "AMENDMENT TO A PREVIOUSLY-MERGED CONTRACT" header
that shows the merged content and the amended content explicitly, with
guidance to plan a delta rather than a from-scratch rebuild. Both the
planning and replanning phases call `get_latest_merged_revision` and pass
it through.
## "New draft" affordance
- New `repository::reset_directive_for_new_draft`: clears goal to '',
status → 'draft', detaches pr_url / pr_branch / orchestrator linkage.
Past revisions stay in directive_revisions as history.
- New `POST /api/v1/directives/{id}/new-draft` handler.
- DirectiveContextMenu surfaces "New draft" only when status === 'inactive',
via an optional onNewDraft callback (legacy tabular UI doesn't have to
wire it up). After reset, the page navigates to the contract so the user
starts typing the next iteration immediately.
## PR-state-aware updates
The user's spec — "open ⇒ update, merged ⇒ new PR, closed ⇒ new PR" — is
already implemented in `build_completion_prompt`'s `gh pr view` runtime
check, so no code change was needed here. The amendment cycle naturally
flows through it: inactive → goal save → status flips to active →
phase_replanning spawns a planner → completion task picks up the existing
pr_url, sees the GitHub state, and decides update vs new PR accordingly.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/src/orchestration/directive.rs')
| -rw-r--r-- | makima/src/orchestration/directive.rs | 52 |
1 files changed, 51 insertions, 1 deletions
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 7dbfe65..1e004bf 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -56,7 +56,21 @@ impl DirectiveOrchestrator { "Directive needs planning — spawning planning task" ); - let plan = build_planning_prompt(&directive, &[], 1, &[], None); + // If the contract has previously-merged revisions, this is an + // amendment — pass the latest merged revision so the planner can + // reason about the delta instead of replanning from scratch. + let prev_merged = repository::get_latest_merged_revision(&self.pool, directive.id) + .await + .unwrap_or(None); + + let plan = build_planning_prompt( + &directive, + &[], + 1, + &[], + None, + prev_merged.as_ref(), + ); if let Err(e) = self .spawn_orchestrator_task( @@ -484,12 +498,20 @@ impl DirectiveOrchestrator { let progress_summary = summarize_in_progress_steps(&existing_steps); + // If the contract has previously-merged revisions, this is an + // amendment — pass the latest merged revision so the planner + // sees the BEFORE→AFTER diff for the new PR. + let prev_merged = repository::get_latest_merged_revision(&self.pool, directive.id) + .await + .unwrap_or(None); + let plan = build_planning_prompt( &directive, &existing_steps, generation, &goal_history, progress_summary.as_deref(), + prev_merged.as_ref(), ); if let Err(e) = self @@ -1475,9 +1497,37 @@ fn build_planning_prompt( generation: i32, goal_history: &[crate::db::models::DirectiveGoalHistory], progress_summary: Option<&str>, + previous_merged_revision: Option<&crate::db::models::DirectiveRevision>, ) -> String { let mut prompt = String::new(); + // Amendments to a previously-shipped contract. When the user edits a + // contract whose prior revision was already merged, the planner needs to + // reason about the BEFORE→AFTER diff so the new PR reflects only the + // intended delta, not a from-scratch reinterpretation. + if let Some(prev) = previous_merged_revision { + prompt.push_str("── AMENDMENT TO A PREVIOUSLY-MERGED CONTRACT ──\n"); + prompt.push_str(&format!( + "This contract was previously shipped via PR {} (revision v{}, frozen {}). \ + The user has now edited the contract to amend or extend that work. \ + Plan the new PR as a DELTA on top of the merged prior PR, not a fresh build.\n\n", + prev.pr_url, + prev.version, + prev.frozen_at.format("%Y-%m-%d %H:%M:%S UTC"), + )); + prompt.push_str("PREVIOUSLY-MERGED CONTRACT (frozen content):\n"); + prompt.push_str(&prev.content); + prompt.push_str("\n\nAMENDED CONTRACT (what the user wants now):\n"); + prompt.push_str(&directive.goal); + prompt.push_str( + "\n\nIMPORTANT:\n\ + - Identify what CHANGED between the previously-merged contract and the amended one.\n\ + - Keep work that already shipped — only plan the delta.\n\ + - The amended PR should land on top of master containing JUST the additions/edits \ + implied by the diff, not a re-implementation of the original contract.\n\n", + ); + } + if let Some(progress) = progress_summary { let trimmed = progress.trim(); if !trimmed.is_empty() { |
