Skip to content

Commit d2f8df7

Browse files
committed
Fix single active plan reuse flow
Release-Sync: yes Release-Version: 2026-03-21.224637 Release-Date: 2026-03-21
1 parent fbc27ec commit d2f8df7

12 files changed

Lines changed: 470 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@ This changelog is maintained manually (not auto-generated).
66

77
## [Unreleased]
88

9+
## [2026-03-21.224637] - 2026-03-21
10+
11+
### Changed
12+
13+
- Updated release-relevant files:
14+
- `runtime/engine.py`
15+
- `runtime/models.py`
16+
- `runtime/plan_scaffold.py`
17+
- `runtime/router.py`
18+
19+
### Tests
20+
21+
- Updated automated coverage:
22+
- `tests/test_runtime.py`
23+
924
## [2026-03-21.212430] - 2026-03-21
1025

1126
### Changed

Claude/Skills/CN/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- bootstrap: lang=zh-CN; encoding=UTF-8 -->
2-
<!-- SOPIFY_VERSION: 2026-03-21.212430 -->
2+
<!-- SOPIFY_VERSION: 2026-03-21.224637 -->
33
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->
44

55
# Sopify (Sop AI) Skills - 自适应 AI 编程助手

Claude/Skills/EN/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- bootstrap: lang=en-US; encoding=UTF-8 -->
2-
<!-- SOPIFY_VERSION: 2026-03-21.212430 -->
2+
<!-- SOPIFY_VERSION: 2026-03-21.224637 -->
33
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->
44

55
# Sopify (Sop AI) Skills - Adaptive AI Programming Assistant

Codex/Skills/CN/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- bootstrap: lang=zh-CN; encoding=UTF-8 -->
2-
<!-- SOPIFY_VERSION: 2026-03-21.212430 -->
2+
<!-- SOPIFY_VERSION: 2026-03-21.224637 -->
33
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->
44

55
# Sopify (Sop AI) Skills - 自适应 AI 编程助手

Codex/Skills/EN/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- bootstrap: lang=en-US; encoding=UTF-8 -->
2-
<!-- SOPIFY_VERSION: 2026-03-21.212430 -->
2+
<!-- SOPIFY_VERSION: 2026-03-21.224637 -->
33
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->
44

55
# Sopify (Sop AI) Skills - Adaptive AI Programming Assistant

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
[![许可证](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
88
[![文档](https://img.shields.io/badge/docs-CC%20BY%204.0-green.svg)](./LICENSE-docs)
9-
[![版本](https://img.shields.io/badge/version-2026--03--21.212430-orange.svg)](#版本历史)
9+
[![版本](https://img.shields.io/badge/version-2026--03--21.224637-orange.svg)](#版本历史)
1010
[![欢迎PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)
1111

1212
[English](./README_EN.md) · [简体中文](./README.md) · [快速开始](#快速开始) · [配置说明](#配置说明)

README_EN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
88
[![Docs](https://img.shields.io/badge/docs-CC%20BY%204.0-green.svg)](./LICENSE-docs)
9-
[![Version](https://img.shields.io/badge/version-2026--03--21.212430-orange.svg)](#version-history)
9+
[![Version](https://img.shields.io/badge/version-2026--03--21.224637-orange.svg)](#version-history)
1010
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)
1111

1212
[English](./README_EN.md) · [简体中文](./README.md) · [Quick Start](#quick-start) · [Configuration](#configuration)

runtime/engine.py

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
from .handoff import build_runtime_handoff
3131
from .kb import bootstrap_kb, ensure_blueprint_index, ensure_blueprint_scaffold
3232
from .models import ClarificationState, DecisionState, ExecutionGate, KbArtifact, PlanArtifact, ReplayEvent, RouteDecision, RunState, RuntimeHandoff, RuntimeResult, SkillActivation, SkillMeta
33-
from .plan_scaffold import create_plan_scaffold
33+
from .plan_scaffold import (
34+
create_plan_scaffold,
35+
find_plan_by_request_reference,
36+
request_explicitly_wants_new_plan,
37+
)
3438
from .replay import ReplayWriter, build_compare_replay_event, build_decision_replay_event
3539
from .router import Router
3640
from .skill_registry import SkillRegistry
@@ -1197,7 +1201,11 @@ def _advance_planning_route(
11971201
pending_decision = _build_route_native_decision_state(decision, config=config)
11981202
if pending_decision is not None:
11991203
state_store.set_current_decision(pending_decision)
1200-
state_store.clear_current_plan()
1204+
_preserve_or_clear_current_plan_for_pending_decision(
1205+
decision,
1206+
state_store=state_store,
1207+
config=config,
1208+
)
12011209
decision_gate = evaluate_execution_gate(
12021210
decision=decision,
12031211
plan_artifact=None,
@@ -1216,20 +1224,21 @@ def _advance_planning_route(
12161224
return (
12171225
_decision_pending_route(decision, reason="Detected an explicit design split that requires confirmation"),
12181226
None,
1219-
notes,
1220-
kb_artifact,
1221-
)
1227+
notes,
1228+
kb_artifact,
1229+
)
12221230

12231231
level = decision.plan_level or _default_plan_level(decision)
1224-
plan_artifact = create_plan_scaffold(
1225-
decision.request_text,
1232+
plan_artifact, plan_notes = _select_plan_for_request(
1233+
decision,
1234+
state_store=state_store,
12261235
config=config,
12271236
level=level,
1228-
decision_state=confirmed_decision,
1237+
confirmed_decision=confirmed_decision,
12291238
)
12301239
state_store.set_current_plan(plan_artifact)
12311240
kb_artifact = _merge_kb_artifacts(kb_artifact, ensure_blueprint_index(config), config=config)
1232-
notes.append(f"Plan scaffold created at {plan_artifact.path}")
1241+
notes.extend(plan_notes)
12331242

12341243
routed_decision, plan_artifact, gate_notes = _apply_execution_gate_to_plan(
12351244
decision,
@@ -1242,6 +1251,73 @@ def _advance_planning_route(
12421251
return (routed_decision, plan_artifact, notes, kb_artifact)
12431252

12441253

1254+
def _select_plan_for_request(
1255+
decision: RouteDecision,
1256+
*,
1257+
state_store: StateStore,
1258+
config: RuntimeConfig,
1259+
level: str,
1260+
confirmed_decision: DecisionState | None,
1261+
) -> tuple[PlanArtifact, list[str]]:
1262+
if confirmed_decision is not None:
1263+
created = create_plan_scaffold(
1264+
decision.request_text,
1265+
config=config,
1266+
level=level,
1267+
decision_state=confirmed_decision,
1268+
)
1269+
return created, [f"Plan scaffold created at {created.path}"]
1270+
1271+
current_plan = state_store.get_current_plan()
1272+
explicit_new_plan = request_explicitly_wants_new_plan(decision.request_text)
1273+
explicit_plan = find_plan_by_request_reference(decision.request_text, config=config)
1274+
1275+
if explicit_new_plan:
1276+
created = create_plan_scaffold(
1277+
decision.request_text,
1278+
config=config,
1279+
level=level,
1280+
decision_state=None,
1281+
)
1282+
return created, [f"Plan scaffold created at {created.path} (explicit new-plan request)"]
1283+
1284+
if explicit_plan is not None:
1285+
if current_plan is not None and explicit_plan.plan_id == current_plan.plan_id:
1286+
return current_plan, [f"Reused active plan {current_plan.path} (explicit self-reference)"]
1287+
return explicit_plan, [f"Rebound planning context to existing plan {explicit_plan.path} (explicit plan reference)"]
1288+
1289+
if current_plan is not None:
1290+
return current_plan, [f"Reused active plan {current_plan.path} under strict single-active-plan policy"]
1291+
1292+
created = create_plan_scaffold(
1293+
decision.request_text,
1294+
config=config,
1295+
level=level,
1296+
decision_state=None,
1297+
)
1298+
return created, [f"Plan scaffold created at {created.path}"]
1299+
1300+
1301+
def _preserve_or_clear_current_plan_for_pending_decision(
1302+
decision: RouteDecision,
1303+
*,
1304+
state_store: StateStore,
1305+
config: RuntimeConfig,
1306+
) -> None:
1307+
current_plan = state_store.get_current_plan()
1308+
if current_plan is None:
1309+
return
1310+
1311+
if request_explicitly_wants_new_plan(decision.request_text):
1312+
state_store.clear_current_plan()
1313+
return
1314+
1315+
explicit_plan = find_plan_by_request_reference(decision.request_text, config=config)
1316+
if explicit_plan is not None and explicit_plan.plan_id != current_plan.plan_id:
1317+
state_store.set_current_plan(explicit_plan)
1318+
return
1319+
1320+
12451321
def _apply_execution_gate_to_plan(
12461322
decision: RouteDecision,
12471323
*,

runtime/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ class PlanArtifact:
811811
path: str
812812
files: tuple[str, ...]
813813
created_at: str
814+
topic_key: str = ""
814815

815816
def to_dict(self) -> dict[str, Any]:
816817
return {
@@ -821,6 +822,7 @@ def to_dict(self) -> dict[str, Any]:
821822
"path": self.path,
822823
"files": list(self.files),
823824
"created_at": self.created_at,
825+
"topic_key": self.topic_key,
824826
}
825827

826828
@classmethod
@@ -833,6 +835,7 @@ def from_dict(cls, data: Mapping[str, Any]) -> "PlanArtifact":
833835
path=str(data.get("path") or ""),
834836
files=tuple(data.get("files") or ()),
835837
created_at=str(data.get("created_at") or ""),
838+
topic_key=str(data.get("topic_key") or data.get("feature_key") or ""),
836839
)
837840

838841

0 commit comments

Comments
 (0)