summaryrefslogtreecommitdiff
path: root/makima/src/db/repository.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-30 17:09:45 +0100
committerGitHub <noreply@github.com>2026-04-30 17:09:45 +0100
commitc03e9a323e266c6a9a7ccb17bbbb7841296bbd5c (patch)
tree34c86fc502ea3232b4a50baf3accc9de38bf70c6 /makima/src/db/repository.rs
parentfe6b78fa59657449be2e888402e3a0197b5c0621 (diff)
downloadsoryu-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/db/repository.rs')
-rw-r--r--makima/src/db/repository.rs74
1 files changed, 68 insertions, 6 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 1021c35..27bd47e 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -5625,12 +5625,15 @@ pub async fn check_directive_idle(
}
/// Update a directive's goal and bump goal_updated_at.
-/// Reactivates draft/idle/paused directives and clears any stale orchestrator
-/// task so that planning/replanning triggers on the next reconciler tick.
+/// Reactivates draft/idle/paused/inactive directives and clears any stale
+/// orchestrator task so that planning/replanning triggers on the next
+/// reconciler tick.
///
-/// `draft` is included in the flip set because the document-mode UI treats
-/// the first goal save as the implicit "start" — without this, a brand-new
-/// directive's goal save would persist but never spawn a planner.
+/// `draft` flips because the document-mode UI treats the first goal save as
+/// the implicit "start". `inactive` flips because editing a contract whose
+/// last revision was already shipped is the way the user kicks off an
+/// amendment — the planner picks it up via phase_planning/replanning and
+/// uses get_latest_merged_revision to learn the BEFORE→AFTER diff.
pub async fn update_directive_goal(
pool: &PgPool,
owner_id: Uuid,
@@ -5642,7 +5645,10 @@ pub async fn update_directive_goal(
UPDATE directives
SET goal = $3,
goal_updated_at = NOW(),
- status = CASE WHEN status IN ('draft', 'idle', 'paused') THEN 'active' ELSE status END,
+ status = CASE
+ WHEN status IN ('draft', 'idle', 'paused', 'inactive') THEN 'active'
+ ELSE status
+ END,
orchestrator_task_id = NULL,
updated_at = NOW(),
version = version + 1
@@ -5657,6 +5663,62 @@ pub async fn update_directive_goal(
.await
}
+/// Mark a directive 'inactive'. Used at the moment a PR is raised — at that
+/// point the contract's current iteration is "shipped" and editing the goal
+/// (Stage 4) starts an amendment cycle. Idempotent: no-op if status is
+/// already inactive or already past it (e.g. archived).
+pub async fn set_directive_inactive(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"
+ UPDATE directives
+ SET status = 'inactive',
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1
+ AND status IN ('active', 'idle', 'paused')
+ "#,
+ )
+ .bind(directive_id)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Reset a directive for a "new draft" cycle: clear the goal back to empty,
+/// flip status to 'draft', and detach the current pr_url / pr_branch /
+/// orchestrator linkage so the next goal save starts fresh. Prior revisions
+/// remain in `directive_revisions` as the historical record. Used by the
+/// sidebar's "New draft" right-click on inactive contracts.
+pub async fn reset_directive_for_new_draft(
+ pool: &PgPool,
+ owner_id: Uuid,
+ directive_id: Uuid,
+) -> Result<Option<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET goal = '',
+ goal_updated_at = NOW(),
+ status = 'draft',
+ pr_url = NULL,
+ pr_branch = NULL,
+ orchestrator_task_id = NULL,
+ completion_task_id = NULL,
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+}
+
/// Update a directive's goal WITHOUT clearing the orchestrator task id.
///
/// This is the path used by the goal-edit interrupt cycle: when a small goal