clouds-coder 2026.3.27__tar.gz → 2026.3.28__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -455,7 +455,7 @@ PLAN_MODE_USER_CHOICES = ("auto", "on", "off")
455
455
  TASK_PHASES = ("research", "design", "implement", "test", "review", "deploy")
456
456
  TASK_PHASE_ROUTING = {
457
457
  "research": "explorer",
458
- "design": "explorer",
458
+ "design": "developer",
459
459
  "implement": "developer",
460
460
  "test": "developer",
461
461
  "review": "reviewer",
@@ -480,6 +480,8 @@ EXPLORER_STALL_THRESHOLD = 3 # consecutive same-target delegations before force
480
480
  DEVELOPER_EDIT_STALL_THRESHOLD = 3 # consecutive edit_file failures on same file before forced strategy change
481
481
  PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 4096
482
482
  PLAN_MODE_MAX_OPTIONS = 3
483
+ PLAN_FILE_RELATIVE_PATH = ".clouds_coder/plan.md"
484
+ PLAN_BUBBLE_MAX_CHARS = 3800 # margin under ASSISTANT_MESSAGE_EVENT_MAX_CHARS (4000)
483
485
  PLAN_MODE_RESEARCH_TOOL_ALLOWLIST = {
484
486
  "bash", "read_file", "context_recall", "task_get", "task_list",
485
487
  "check_background", "read_from_blackboard", "write_to_blackboard",
@@ -4147,9 +4149,13 @@ class TodoManager:
4147
4149
  row["owner"] = owner
4148
4150
  if key:
4149
4151
  row["key"] = key
4152
+ # Preserve parent_step_id for subtask-to-plan-step linkage
4153
+ parent_step_id = trim(str(raw.get("parent_step_id", "") or ""), 20)
4154
+ if parent_step_id:
4155
+ row["parent_step_id"] = parent_step_id
4150
4156
  validated.append(row)
4151
- if len(validated) > 20:
4152
- raise ValueError("max 20 todos")
4157
+ if len(validated) > 40:
4158
+ raise ValueError("max 40 todos")
4153
4159
  if validated and not any(x["status"] == "in_progress" for x in validated):
4154
4160
  for row in validated:
4155
4161
  if row["status"] == "pending":
@@ -4792,6 +4798,7 @@ EMBEDDED_SKILLS_ARCHIVE_FILES = [
4792
4798
  "skills/generated/upload-office-parser/SKILL.md",
4793
4799
  "skills/generated/upload-parsers-capabilities.json",
4794
4800
  "skills/generated/upload-tabular-parser/SKILL.md",
4801
+ "skills/generated/upload-image-parser/SKILL.md",
4795
4802
  "skills/mcp-builder/SKILL.md",
4796
4803
  "skills/pdf/SKILL.md",
4797
4804
  "skills/skills_Gen/SKILL.md",
@@ -4950,6 +4957,36 @@ Use this skill when the user uploads Word/PowerPoint documents and needs content
4950
4957
  ## Notes
4951
4958
  - The backend automatically parses `.doc`, `.docx`, `.ppt`, `.pptx`.
4952
4959
  - If parser dependencies are unavailable, fallback extractor is used (may lose formatting).
4960
+ """
4961
+ image_skill = """---
4962
+ name: upload-image-parser
4963
+ description: Analyze uploaded image files (PNG/JPG/JPEG/WEBP/GIF/BMP) using model native vision capabilities as the primary method; no OCR tools needed for supported formats.
4964
+ ---
4965
+
4966
+ # Upload Image Parser
4967
+
4968
+ Use this skill when the user uploads image files and needs content description, analysis, extraction, or comparison.
4969
+
4970
+ ## Primary Approach: Model Vision (Multimodal)
4971
+ 1. Use `read_file` on the uploaded image path — the runtime automatically injects it as a native vision input to the model.
4972
+ 2. Analyze or describe the image directly with vision capabilities; no external tools required.
4973
+ 3. Uploaded image paths are under `files/uploaded/` in the session workspace. Check `Uploaded files context` in the system prompt for exact paths.
4974
+
4975
+ ## Format Notes
4976
+ - Native formats (sent directly, no conversion): `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`
4977
+ - Auto-converted formats (runtime handles via Pillow): `.bmp`, `.tiff`, `.tif`, `.heic`, `.heif`, `.avif`
4978
+ - If conversion fails, the runtime returns an error message; use bash fallback below.
4979
+ - `.svg` files: runtime returns the SVG markup as text — parse the XML/SVG source directly, do not treat as a raster image.
4980
+
4981
+ ## Fallback (only if runtime reports vision input unavailable)
4982
+ If the model cannot process the image natively (runtime message will say so):
4983
+ - OCR text extraction: `bash` → `tesseract <path> stdout`
4984
+ - Metadata / dimensions: `bash` → `identify <path>` (ImageMagick)
4985
+ - Pixel-level analysis: `bash` → `python3 -c "from PIL import Image; img=Image.open('<path>'); print(img.size, img.mode)"`
4986
+
4987
+ ## Notes
4988
+ - Never attempt text extraction (OCR) on images when vision input is available — use the model's native understanding instead.
4989
+ - For multi-image comparison tasks, load each image via `read_file` sequentially; the runtime accumulates them as pending media inputs for the next model call.
4953
4990
  """
4954
4991
  cap_json = json_dumps(
4955
4992
  {
@@ -4960,6 +4997,7 @@ Use this skill when the user uploads Word/PowerPoint documents and needs content
4960
4997
  )
4961
4998
  _write_text_if_changed(generated_root / "upload-tabular-parser" / "SKILL.md", tabular_skill)
4962
4999
  _write_text_if_changed(generated_root / "upload-office-parser" / "SKILL.md", office_skill)
5000
+ _write_text_if_changed(generated_root / "upload-image-parser" / "SKILL.md", image_skill)
4963
5001
  _write_text_if_changed(generated_root / "upload-parsers-capabilities.json", cap_json)
4964
5002
 
4965
5003
  def ensure_generated_image_coding_feedback_skill(skills_root: Path):
@@ -4982,12 +5020,12 @@ Use this skill when the task depends on image understanding in a coding workflow
4982
5020
  - Generated image(s): current output from app, script, or model generation pipeline.
4983
5021
  - Code scope: file paths, rendering command, and runtime constraints.
4984
5022
 
4985
- ## Capability Gate
4986
- 1. Check active model/image pipeline capability first.
4987
- 2. If image input is supported, use direct vision reasoning for detailed comparison.
4988
- 3. If image input is unavailable, use fallback checks:
5023
+ ## Image Analysis: Vision First
5024
+ 1. Load all reference and generated images via `read_file` — the runtime injects them as native vision inputs automatically.
5025
+ 2. Analyze images directly with model vision capabilities; do not use OCR or pixel heuristics when vision input is available.
5026
+ 3. Fallback (only if the runtime explicitly reports vision input unavailable):
4989
5027
  - deterministic metadata checks (size/aspect/background),
4990
- - text checks (OCR if available),
5028
+ - text checks via OCR tools (e.g., `tesseract`),
4991
5029
  - simple pixel-region checks from locally rendered output.
4992
5030
  4. Always report confidence level (`high|medium|low`) based on signal quality.
4993
5031
 
@@ -6385,6 +6423,7 @@ def ensure_generated_runtime_skills_manifest(skills_root: Path):
6385
6423
  "skills_Gen/knowledge_snapshot.json",
6386
6424
  "generated/upload-tabular-parser/SKILL.md",
6387
6425
  "generated/upload-office-parser/SKILL.md",
6426
+ "generated/upload-image-parser/SKILL.md",
6388
6427
  "generated/image-coding-feedback-loop/SKILL.md",
6389
6428
  "generated/execution-degradation-recovery/SKILL.md",
6390
6429
  "generated/deep-research-orchestrator/SKILL.md",
@@ -9860,6 +9899,8 @@ class SessionState:
9860
9899
  self.runtime_task_judgement = ""
9861
9900
  self.runtime_task_type = ""
9862
9901
  self.runtime_task_complexity = ""
9902
+ self.runtime_complexity_floor = ""
9903
+ self.runtime_task_level_floor = 0
9863
9904
  self.runtime_scale_preference = "balanced"
9864
9905
  self.runtime_direct_objective = ""
9865
9906
  self.runtime_reclassify_goal = ""
@@ -10940,6 +10981,12 @@ class SessionState:
10940
10981
  self.runtime_reclassify_required = bool(
10941
10982
  raw.get("runtime_reclassify_required", self.runtime_reclassify_required)
10942
10983
  )
10984
+ self.runtime_complexity_floor = str(
10985
+ raw.get("runtime_complexity_floor", self.runtime_complexity_floor) or ""
10986
+ )
10987
+ self.runtime_task_level_floor = int(
10988
+ raw.get("runtime_task_level_floor", self.runtime_task_level_floor) or 0
10989
+ )
10943
10990
  self.runtime_goal_reset_pending = bool(
10944
10991
  raw.get("runtime_goal_reset_pending", self.runtime_goal_reset_pending)
10945
10992
  )
@@ -11102,6 +11149,8 @@ class SessionState:
11102
11149
  "runtime_direct_objective": trim(str(self.runtime_direct_objective or ""), 800),
11103
11150
  "runtime_reclassify_goal": trim(str(self.runtime_reclassify_goal or ""), 4000),
11104
11151
  "runtime_reclassify_required": bool(self.runtime_reclassify_required),
11152
+ "runtime_complexity_floor": str(self.runtime_complexity_floor or ""),
11153
+ "runtime_task_level_floor": int(self.runtime_task_level_floor or 0),
11105
11154
  "runtime_goal_reset_pending": bool(self.runtime_goal_reset_pending),
11106
11155
  "runtime_plan_mode_needed": bool(self.runtime_plan_mode_needed),
11107
11156
  "runtime_plan_approved": bool(self.runtime_plan_approved),
@@ -11291,6 +11340,13 @@ class SessionState:
11291
11340
  self.todo.items = []
11292
11341
  except Exception:
11293
11342
  pass
11343
+ # Clean up stale plan.md file from previous run
11344
+ try:
11345
+ _pf = self._plan_file_path()
11346
+ if _pf.exists():
11347
+ _pf.unlink()
11348
+ except Exception:
11349
+ pass
11294
11350
 
11295
11351
  def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
11296
11352
  payload = dict(data or {})
@@ -12477,7 +12533,7 @@ class SessionState:
12477
12533
  o.pop("cons", None)
12478
12534
  o.pop("risk", None)
12479
12535
  elif tier >= 3:
12480
- # Minimal: only phase, chosen, steps
12536
+ # Minimal: only phase, chosen, steps — but preserve project_todos
12481
12537
  phase = plan.get("phase", "")
12482
12538
  chosen = plan.get("chosen", "")
12483
12539
  steps = plan.get("steps", [])
@@ -12493,7 +12549,10 @@ class SessionState:
12493
12549
  o for o in options if isinstance(o, dict) and o.get("id") == chosen_id
12494
12550
  ]
12495
12551
  if tier >= 3:
12496
- self.runtime_plan_proposal = {}
12552
+ # Only clear proposal if plan is fully done
12553
+ plan_phase = str(bb.get("plan", {}).get("phase", ""))
12554
+ if plan_phase not in ("executing", "awaiting_choice"):
12555
+ self.runtime_plan_proposal = {}
12497
12556
 
12498
12557
  def _apply_auto_compact_if_needed(self, reason: str = "auto") -> bool:
12499
12558
  metrics = self._context_budget_metrics()
@@ -12895,16 +12954,13 @@ class SessionState:
12895
12954
  meta = {"filtered": False, "reason": "", "original_chars": len(raw)}
12896
12955
  if not clean:
12897
12956
  return raw, meta
12898
- if (
12899
- not tool_calls
12900
- and len(clean) >= int(RAW_TOOLCALL_TEXT_FILTER_THRESHOLD)
12901
- and self._looks_like_raw_toolcall_blob(clean)
12902
- ):
12957
+ # Filter raw <tool_call> XML regardless of size — any size is invalid display content
12958
+ if not tool_calls and self._looks_like_raw_toolcall_blob(clean):
12903
12959
  note = (
12904
- "[toolcall payload omitted: detected oversized inline <toolcall> text, "
12905
- "likely truncated. Please regenerate a compact structured tool call.]"
12960
+ "[toolcall payload omitted: detected inline <toolcall> text. "
12961
+ "Please regenerate a compact structured tool call.]"
12906
12962
  )
12907
- meta.update({"filtered": True, "reason": "oversized_raw_toolcall"})
12963
+ meta.update({"filtered": True, "reason": "raw_toolcall"})
12908
12964
  return note, meta
12909
12965
  if len(raw) > int(ASSISTANT_TEXT_PERSIST_MAX_CHARS):
12910
12966
  clipped = trim(raw, int(ASSISTANT_TEXT_PERSIST_MAX_CHARS))
@@ -19070,7 +19126,7 @@ class SessionState:
19070
19126
  board["project_todos"] = []
19071
19127
  else:
19072
19128
  clean_todos = []
19073
- for pt in bb_src_todos[:20]:
19129
+ for pt in bb_src_todos[:40]:
19074
19130
  if not isinstance(pt, dict):
19075
19131
  continue
19076
19132
  clean_todos.append({
@@ -19139,6 +19195,15 @@ class SessionState:
19139
19195
  goal_preview = trim(str(src.get("loaded_skills_goal_preview", "") or ""), 240)
19140
19196
  if goal_preview:
19141
19197
  board["loaded_skills_goal_preview"] = goal_preview
19198
+ # Preserve step_files registry across normalization
19199
+ raw_step_files = src.get("step_files")
19200
+ if isinstance(raw_step_files, dict):
19201
+ clean_sf: dict[str, list] = {}
19202
+ for sf_key, sf_entries in raw_step_files.items():
19203
+ if isinstance(sf_entries, list):
19204
+ clean_sf[str(sf_key)] = sf_entries[-30:]
19205
+ if clean_sf:
19206
+ board["step_files"] = clean_sf
19142
19207
  return board
19143
19208
 
19144
19209
  def _normalize_failure_ledger(self, raw: dict) -> dict:
@@ -19566,9 +19631,17 @@ class SessionState:
19566
19631
  self.blackboard["loaded_skills"] = preserved_skills
19567
19632
  self.blackboard["loaded_skills_goal_sig"] = preserved_skills_sig
19568
19633
  self.blackboard["loaded_skills_goal_preview"] = trim(str(goal or ""), 240)
19569
- # Restore plan state if plan is in executing phase
19570
- if isinstance(preserved_plan, dict) and preserved_plan.get("phase") == "executing":
19571
- self.blackboard["plan"] = preserved_plan
19634
+ # Restore plan state if plan is active (any phase) or todos have pending work
19635
+ has_active_plan = (
19636
+ isinstance(preserved_plan, dict)
19637
+ and preserved_plan.get("phase") in ("executing", "research", "synthesis", "awaiting_choice")
19638
+ )
19639
+ has_active_todos = isinstance(preserved_todos, list) and any(
19640
+ t.get("status") != "completed" for t in preserved_todos if isinstance(t, dict)
19641
+ )
19642
+ if has_active_plan or has_active_todos:
19643
+ if isinstance(preserved_plan, dict):
19644
+ self.blackboard["plan"] = preserved_plan
19572
19645
  if isinstance(preserved_todos, list) and preserved_todos:
19573
19646
  self.blackboard["project_todos"] = preserved_todos
19574
19647
  if preserved_cursor is not None:
@@ -20131,14 +20204,19 @@ class SessionState:
20131
20204
  })
20132
20205
  self.blackboard = bb
20133
20206
  self._blackboard_touch()
20134
- # 步骤推进时清除 in_progress/pending worker 子任务,防止跨步骤堆积
20135
- # 保留 completed 的 worker 项,让 UI 保持已完成记录可见
20207
+ # Persist step status change to plan.md
20208
+ try:
20209
+ self._update_plan_file_step_status()
20210
+ except Exception:
20211
+ pass # Plan file update is best-effort
20212
+ # 步骤推进时按 parent_step_id 清除对应步骤的 worker 子任务
20213
+ # 保留 completed 的 worker 项和不属于当前步骤的 worker 项
20214
+ completed_step_id = str(current.get("id", "") or "")
20136
20215
  try:
20137
20216
  _snap = self.todo.snapshot()
20138
- _worker_owners = {"developer", "explorer", "reviewer"}
20139
20217
  _clean = [
20140
20218
  r for r in _snap
20141
- if str(r.get("owner", "") or "").lower() not in _worker_owners
20219
+ if str(r.get("parent_step_id", "") or "") != completed_step_id
20142
20220
  or str(r.get("status", "") or "").lower() == "completed"
20143
20221
  ]
20144
20222
  if len(_clean) < len(_snap):
@@ -20155,6 +20233,82 @@ class SessionState:
20155
20233
  pass
20156
20234
  return True
20157
20235
 
20236
+ def _post_execution_plan_step_check(self, route: dict, worker_step: dict):
20237
+ """After worker execution, check if current plan step should advance based on evidence."""
20238
+ bb = self._ensure_blackboard()
20239
+ current = next(
20240
+ (t for t in bb.get("project_todos", [])
20241
+ if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
20242
+ None,
20243
+ )
20244
+ if not current:
20245
+ return
20246
+ # 1. Manager explicitly requested advancement
20247
+ manager_requested = bool(route.get("advance_plan_step_requested", False))
20248
+ # 2. Worker produced concrete tool outputs
20249
+ worker_produced_output = self._worker_step_has_evidence(worker_step)
20250
+ # 3. All subtasks for this step are completed
20251
+ subtasks_all_done = self._step_subtasks_all_completed(current)
20252
+ # Advance only when evidence confirms step completion:
20253
+ # - Manager requested AND worker produced output, OR
20254
+ # - All subtasks completed AND worker produced output
20255
+ has_strong_evidence = worker_produced_output and (
20256
+ manager_requested or subtasks_all_done
20257
+ )
20258
+ if has_strong_evidence:
20259
+ evidence = self._collect_step_evidence(current, worker_step)
20260
+ self._advance_plan_step(
20261
+ evidence=evidence,
20262
+ actor=str(route.get("target", "developer") or "developer"),
20263
+ )
20264
+
20265
+ def _worker_step_has_evidence(self, step: dict) -> bool:
20266
+ """Check if worker step produced concrete tool outputs."""
20267
+ results = step.get("tool_results", []) or []
20268
+ return any(
20269
+ r.get("ok", False) and str(r.get("name", "")) in (
20270
+ "write_file", "edit_file", "bash", "read_file",
20271
+ "write_to_blackboard", "finish_current_task",
20272
+ )
20273
+ for r in results
20274
+ if isinstance(r, dict)
20275
+ )
20276
+
20277
+ def _step_subtasks_all_completed(self, plan_step: dict) -> bool:
20278
+ """Check if all worker subtasks linked to this plan step are completed."""
20279
+ step_id = str(plan_step.get("id", "") or "")
20280
+ if not step_id:
20281
+ return False
20282
+ snap = self.todo.snapshot()
20283
+ worker_owners = {"developer", "explorer", "reviewer"}
20284
+ worker_items = [
20285
+ r for r in snap
20286
+ if str(r.get("owner", "") or "").lower() in worker_owners
20287
+ and str(r.get("parent_step_id", "") or "") == step_id
20288
+ ]
20289
+ if not worker_items:
20290
+ return False
20291
+ return all(str(r.get("status", "")).lower() == "completed" for r in worker_items)
20292
+
20293
+ def _collect_step_evidence(self, plan_step: dict, worker_step: dict) -> str:
20294
+ """Collect evidence summary from worker step for plan step completion."""
20295
+ parts = []
20296
+ results = worker_step.get("tool_results", []) or []
20297
+ for r in results:
20298
+ if not isinstance(r, dict) or not r.get("ok", False):
20299
+ continue
20300
+ name = str(r.get("name", ""))
20301
+ if name in ("write_file", "edit_file"):
20302
+ path = str(r.get("args", {}).get("path", "") or "")
20303
+ parts.append(f"{name}: {path}")
20304
+ elif name == "bash":
20305
+ cmd = trim(str(r.get("args", {}).get("command", "") or ""), 80)
20306
+ parts.append(f"bash: {cmd}")
20307
+ elif name == "read_file":
20308
+ path = str(r.get("args", {}).get("path", "") or "")
20309
+ parts.append(f"read: {path}")
20310
+ return trim("; ".join(parts) or "post-execution evidence", 200)
20311
+
20158
20312
  def _single_agent_plan_step_check(self, tool_results: list[dict]):
20159
20313
  """In single-agent mode, check if current plan step should be advanced based on tool results."""
20160
20314
  bb = self._ensure_blackboard()
@@ -20228,6 +20382,7 @@ class SessionState:
20228
20382
  _step_label = f"Step {_step_idx}" + (f"/{_total}" if _total else "")
20229
20383
  _hint = (
20230
20384
  f"[plan-step-advance] Previous step completed. Now at {_step_label}: {_step_text}\n"
20385
+ f"Read updated plan: read_file {PLAN_FILE_RELATIVE_PATH}\n"
20231
20386
  "Call TodoWrite to set your task breakdown for this step "
20232
20387
  "(3-5 subtask items, one marked in_progress) before proceeding."
20233
20388
  )
@@ -20285,8 +20440,13 @@ class SessionState:
20285
20440
  if is_system_key or owner == "manager":
20286
20441
  continue
20287
20442
  non_system_rows.append(dict(row))
20288
- remaining_cap = max(0, 20 - len(system_rows) - len(worker_rows))
20289
- merged = list(system_rows) + worker_rows + non_system_rows[:remaining_cap]
20443
+ # Smart trim: keep all active (in_progress/pending) system rows,
20444
+ # but only recent 3 completed system rows to save capacity for worker subtasks
20445
+ active_system = [r for r in system_rows if r.get("status") != "completed"]
20446
+ completed_system = [r for r in system_rows if r.get("status") == "completed"]
20447
+ trimmed_system = active_system + completed_system[-3:]
20448
+ remaining_cap = max(0, 40 - len(trimmed_system) - len(worker_rows))
20449
+ merged = list(trimmed_system) + worker_rows + non_system_rows[:remaining_cap]
20290
20450
  try:
20291
20451
  todo_out = self.todo.update(merged)
20292
20452
  except Exception:
@@ -21441,7 +21601,8 @@ class SessionState:
21441
21601
  todos = bb.get("project_todos", [])
21442
21602
  if not todos or not any(t.get("category") == "plan_step" for t in todos):
21443
21603
  return ""
21444
- lines = ["APPROVED PLAN STEPS:"]
21604
+ lines = [f"PLAN FILE: {PLAN_FILE_RELATIVE_PATH} (read_file for full plan with live status)",
21605
+ "APPROVED PLAN STEPS:"]
21445
21606
  for t in todos:
21446
21607
  if t.get("category") != "plan_step":
21447
21608
  continue
@@ -21453,10 +21614,14 @@ class SessionState:
21453
21614
  lines.append(f" {mark} Step {idx}: {trim(str(t.get('content', '') or ''), 160)}{phase_tag}")
21454
21615
  lines.append("Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing. ")
21455
21616
  lines.append(
21456
- "STEP COMPLETION RULE: Set advance_plan_step=true when the worker has provided concrete evidence "
21457
- "that the current step is done e.g., research_notes populated for a read/analyze step, "
21458
- "code_artifacts created for an implement step, or all worker TodoWrite subtasks marked completed. "
21459
- "Do NOT keep re-delegating the same step once this evidence exists. "
21617
+ "STEP COMPLETION RULE: Set advance_plan_step=true ONLY when:\n"
21618
+ " 1. The worker has ALREADY executed tools and produced verifiable output "
21619
+ "(NOT when dispatching the work for the first time), AND\n"
21620
+ " 2. At least ONE of: research_notes populated, code_artifacts created, "
21621
+ "bash execution succeeded, or all worker TodoWrite subtasks (with parent_step_id) completed.\n"
21622
+ " NEVER set advance_plan_step=true in the SAME delegation that assigns the work. "
21623
+ "The step must have been executed FIRST.\n"
21624
+ "COMPLEXITY LOCK: Do NOT change complexity or task_level below the plan-approved levels. "
21460
21625
  )
21461
21626
  lines.append("MANDATORY: Your delegation instruction MUST reference the current plan step. "
21462
21627
  "Do NOT reinterpret or replace plan steps with your own objectives. ")
@@ -21485,12 +21650,15 @@ class SessionState:
21485
21650
  def _plan_step_phase_hint(self, step_content: str) -> str:
21486
21651
  """Infer the task phase from a plan step's content."""
21487
21652
  c = str(step_content or "").lower()
21653
+ # implement keywords take priority — if step produces output, it's implement not design/research
21654
+ if any(kw in c for kw in ("实现", "编写", "创建", "开发", "绘制", "生成", "写入",
21655
+ "implement", "write", "create", "build", "develop", "code",
21656
+ "generate", "draw", "scaffold", "mkdir", ".f90", ".py", ".cpp", ".md")):
21657
+ return "implement"
21488
21658
  if any(kw in c for kw in ("研究", "分析", "调研", "探索", "research", "analyze", "investigate", "explore", "inspect")):
21489
21659
  return "research"
21490
21660
  if any(kw in c for kw in ("设计", "架构", "规划", "design", "architect", "plan", "interface", "接口")):
21491
21661
  return "design"
21492
- if any(kw in c for kw in ("实现", "编写", "创建", "开发", "implement", "write", "create", "build", "develop", "code")):
21493
- return "implement"
21494
21662
  if any(kw in c for kw in ("测试", "验证", "检查", "test", "verify", "check", "validate", "compile")):
21495
21663
  return "test"
21496
21664
  if any(kw in c for kw in ("审查", "评审", "review", "audit", "inspect code")):
@@ -21553,11 +21721,24 @@ class SessionState:
21553
21721
  completed_w = [r for r in worker_todos if str(r.get("status", "")).lower() == "completed"]
21554
21722
  pending_w = [r for r in worker_todos if str(r.get("status", "")).lower() not in ("completed",)]
21555
21723
  if not pending_w and completed_w:
21556
- worker_hint = (
21557
- f"Worker subtasks: all {len(completed_w)} completed "
21558
- f"({', '.join(trim(str(r.get('content','') or ''), 40) for r in completed_w[:3])}). "
21559
- "→ Worker has finished. Set advance_plan_step=true NOW. "
21560
- )
21724
+ # Verify actual work evidence before suggesting advance
21725
+ bb_now = self._ensure_blackboard()
21726
+ has_artifacts = bool(bb_now.get("code_artifacts"))
21727
+ has_research = bool(bb_now.get("research_notes"))
21728
+ has_shell_output = bool(bb_now.get("execution_logs"))
21729
+ has_evidence = has_artifacts or has_research or has_shell_output
21730
+ if has_evidence:
21731
+ worker_hint = (
21732
+ f"Worker subtasks: all {len(completed_w)} completed "
21733
+ f"({', '.join(trim(str(r.get('content','') or ''), 40) for r in completed_w[:3])}). "
21734
+ "Blackboard has concrete outputs. \u2192 Set advance_plan_step=true NOW. "
21735
+ )
21736
+ else:
21737
+ worker_hint = (
21738
+ f"Worker subtasks: all {len(completed_w)} marked completed "
21739
+ "but blackboard has NO concrete outputs (no code_artifacts, research_notes, or execution_logs). "
21740
+ "\u2192 Do NOT advance. Re-delegate to verify actual work was done. "
21741
+ )
21561
21742
  elif completed_w or pending_w:
21562
21743
  worker_hint = (
21563
21744
  f"Worker subtasks: {len(completed_w)} done, {len(pending_w)} pending. "
@@ -22811,7 +22992,7 @@ class SessionState:
22811
22992
  prompt = (
22812
22993
  "Read the blackboard and delegate one next short timeslice. "
22813
22994
  "Return only one route_to_next_agent call.\n\n"
22814
- f"{self._blackboard_read_state_markdown(max_items=6)}"
22995
+ f"{self._blackboard_read_state_markdown(max_items=10)}"
22815
22996
  )
22816
22997
  self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
22817
22998
  self._microcompact_agent_messages(self.manager_context)
@@ -23010,21 +23191,12 @@ class SessionState:
23010
23191
  "round_budget": int(round_budget),
23011
23192
  "remaining_rounds": int(remaining_rounds),
23012
23193
  }
23013
- # advance_plan_step: only trust semantics from what the agent actually wrote,
23014
- # NOT from the manager's own route JSON the manager often sets this flag
23015
- # simultaneously with dispatching the work, advancing the step before it runs.
23016
- should_advance = self._instruction_implies_step_advance(
23017
- str(route.get("instruction", "") or ""),
23018
- str(route.get("reason", "") or ""),
23194
+ # advance_plan_step: REMOVED pre-execution advancement.
23195
+ # Step advancement now happens AFTER worker execution via _post_execution_plan_step_check.
23196
+ # Store the manager's advance request flag for post-execution validation.
23197
+ route_row["advance_plan_step_requested"] = _to_bool_like(
23198
+ route.get("advance_plan_step", False), default=False
23019
23199
  )
23020
- if should_advance:
23021
- self._advance_plan_step(
23022
- evidence=trim(str(route.get("instruction", "") or ""), 200),
23023
- actor=str(route.get("target", "developer") or "developer"),
23024
- )
23025
- # CRITICAL: re-anchor board to the updated blackboard so the
23026
- # self.blackboard = board at line ~22388 does NOT overwrite the advance.
23027
- board = self.blackboard
23028
23200
  self.manager_routes.append(route_row)
23029
23201
  self.manager_routes = self.manager_routes[-240:]
23030
23202
  # Failure ledger: persist route and record delegation
@@ -23041,7 +23213,11 @@ class SessionState:
23041
23213
  self.blackboard = board
23042
23214
  self._ledger_record_delegation(target, instruction)
23043
23215
  profile = self._ensure_blackboard_task_profile(board)
23044
- profile["task_level"] = int(task_level)
23216
+ # Complexity / task_level floor protection: prevent manager downgrade during plan execution
23217
+ effective_level = int(task_level)
23218
+ if int(self.runtime_task_level_floor or 0) > 0:
23219
+ effective_level = max(effective_level, int(self.runtime_task_level_floor))
23220
+ profile["task_level"] = effective_level
23045
23221
  profile["execution_mode"] = execution_mode
23046
23222
  profile["participants"] = list(participants)
23047
23223
  profile["assigned_expert"] = assigned_expert
@@ -23051,6 +23227,9 @@ class SessionState:
23051
23227
  if task_type in TASK_PROFILE_TYPES:
23052
23228
  profile["task_type"] = task_type
23053
23229
  if complexity in TASK_COMPLEXITY_LEVELS:
23230
+ # Floor protection: if plan mode set a floor, do not allow downgrade
23231
+ if self.runtime_complexity_floor == "complex" and complexity == "simple":
23232
+ complexity = "complex"
23054
23233
  profile["complexity"] = complexity
23055
23234
  profile["scale_preference"] = scale_preference if scale_preference in TASK_SCALE_PREFERENCES else "balanced"
23056
23235
  if objective:
@@ -23258,7 +23437,7 @@ class SessionState:
23258
23437
  "Do NOT repeat previous failed fixes.\n"
23259
23438
  "</compile-error-context>\n"
23260
23439
  )
23261
- board_md = self._blackboard_read_state_markdown(max_items=5)
23440
+ board_md = self._blackboard_read_state_markdown(max_items=10)
23262
23441
  # Include loaded skills hint in delegation
23263
23442
  loaded_skills_note = self._loaded_skills_prompt_hint(for_role=role_key)
23264
23443
  # Include current plan step in delegation so agents stay on track
@@ -23278,11 +23457,46 @@ class SessionState:
23278
23457
  # developer 被调用时有活跃 plan step,要求在开始前调用 TodoWrite 刷新子任务
23279
23458
  todo_update_note = ""
23280
23459
  if role_key == "developer" and current_plan_step_note:
23460
+ # Extract step_id from the matched plan step for parent_step_id linkage
23461
+ _active_step_id = ""
23462
+ if isinstance(plan_todos, list):
23463
+ for _pt in plan_todos:
23464
+ if isinstance(_pt, dict) and _pt.get("category") == "plan_step" and _pt.get("status") == "in_progress":
23465
+ _active_step_id = str(_pt.get("id", "") or "")
23466
+ break
23281
23467
  todo_update_note = (
23282
- "TODO UPDATE: Call TodoWrite at the start of your work to set your task breakdown "
23283
- "for this step (3-5 subtask items, one marked in_progress). "
23284
- "Mark each subtask completed as you finish it.\n"
23285
- )
23468
+ f"TODO UPDATE: At the START of your work, call TodoWrite to set subtasks for this step.\n"
23469
+ f"Each subtask MUST include parent_step_id='{_active_step_id}' to link it to this plan step.\n"
23470
+ f"Format: 3-5 items, one marked in_progress, others pending.\n"
23471
+ f"Mark each subtask completed as you finish it. When ALL subtasks are done, the step auto-advances.\n"
23472
+ )
23473
+ # Build step_files context note for cross-agent file visibility
23474
+ step_files_note = ""
23475
+ if current_plan_step_note:
23476
+ try:
23477
+ _sf_bb = self._ensure_blackboard()
23478
+ _sf_data = _sf_bb.get("step_files", {})
23479
+ _sf_step_id = ""
23480
+ if isinstance(plan_todos, list):
23481
+ for _sfpt in plan_todos:
23482
+ if isinstance(_sfpt, dict) and _sfpt.get("category") == "plan_step" and _sfpt.get("status") == "in_progress":
23483
+ _sf_step_id = str(_sfpt.get("id", "") or "")
23484
+ break
23485
+ if _sf_step_id and isinstance(_sf_data, dict):
23486
+ _sf_entries = _sf_data.get(_sf_step_id, [])
23487
+ if _sf_entries:
23488
+ _sf_paths = list(dict.fromkeys(
23489
+ str(e.get("path", "") or "")
23490
+ for e in _sf_entries[-20:]
23491
+ if isinstance(e, dict) and e.get("path")
23492
+ ))
23493
+ if _sf_paths:
23494
+ step_files_note = (
23495
+ "FILES ACCESSED IN THIS STEP:\n"
23496
+ + "\n".join(f"- {p}" for p in _sf_paths[:15]) + "\n"
23497
+ )
23498
+ except Exception:
23499
+ pass
23286
23500
  payload = (
23287
23501
  "<manager-delegate>\n"
23288
23502
  f"target={role_key}\n"
@@ -23297,6 +23511,7 @@ class SessionState:
23297
23511
  f"{loaded_skills_note}"
23298
23512
  f"{current_plan_step_note}"
23299
23513
  f"{todo_update_note}"
23514
+ f"{step_files_note}"
23300
23515
  f"{error_section}"
23301
23516
  "</manager-delegate>\n"
23302
23517
  "<blackboard-state>\n"
@@ -23404,6 +23619,32 @@ class SessionState:
23404
23619
  role_key,
23405
23620
  f"tool_error {name}: {output}",
23406
23621
  )
23622
+ # Track file operations in step_files registry for cross-agent context
23623
+ if name in ("read_file", "write_file", "edit_file"):
23624
+ file_path = trim(str(args.get("path", "") or "").strip(), 240)
23625
+ if file_path:
23626
+ try:
23627
+ bb = self._ensure_blackboard()
23628
+ current_step = next(
23629
+ (t for t in bb.get("project_todos", [])
23630
+ if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
23631
+ None,
23632
+ )
23633
+ if current_step:
23634
+ step_id = str(current_step.get("id", "") or "")
23635
+ if step_id:
23636
+ sf = bb.setdefault("step_files", {})
23637
+ entries = sf.setdefault(step_id, [])
23638
+ entries.append({
23639
+ "path": file_path,
23640
+ "role": role_key,
23641
+ "op": name,
23642
+ "ts": float(now_ts()),
23643
+ })
23644
+ if len(entries) > 30:
23645
+ sf[step_id] = entries[-30:]
23646
+ except Exception:
23647
+ pass
23407
23648
 
23408
23649
  def _blackboard_update_from_worker_step(self, role: str, step: dict):
23409
23650
  role_key = self._sanitize_agent_role(role)
@@ -25722,8 +25963,13 @@ class SessionState:
25722
25963
  self.runtime_plan_mode_needed = True
25723
25964
  # Reset completed plan/todo/skills blackboard state so the manager
25724
25965
  # does not see status=COMPLETED on the very first round and immediately finish.
25966
+ # But preserve plan state if user is continuing an existing task.
25725
25967
  if not _awaiting_plan_choice:
25726
- self._reset_blackboard_plan_state_locked()
25968
+ clean_goal_pre = trim(str(content or "").strip(), 4000)
25969
+ if self._is_continuation_input(clean_goal_pre):
25970
+ pass # Preserve plan state for continuation
25971
+ else:
25972
+ self._reset_blackboard_plan_state_locked()
25727
25973
  self.run_generation = int(self.run_generation) + 1
25728
25974
  clean_goal = trim(str(content or "").strip(), 4000)
25729
25975
  self._refresh_runtime_code_reference(clean_goal or content)
@@ -26330,6 +26576,8 @@ class SessionState:
26330
26576
  media_inputs_round=role_media_inputs,
26331
26577
  )
26332
26578
  self._blackboard_update_from_worker_step(role, step)
26579
+ # Post-execution plan step advancement (replaces pre-execution advancement)
26580
+ self._post_execution_plan_step_check(route, step if isinstance(step, dict) else {})
26333
26581
  # ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
26334
26582
  agent_text = self._latest_agent_assistant_text(role)
26335
26583
  if (
@@ -26626,6 +26874,15 @@ class SessionState:
26626
26874
  self._plan_mode_update_findings(step)
26627
26875
 
26628
26876
  # Phase 2: Manager 综合分析
26877
+ # Inject pending user inputs before synthesis
26878
+ self._inject_pending_user_inputs()
26879
+ # Check if user sent a substantive goal change during research
26880
+ if self._has_pending_goal_change():
26881
+ self._emit("status", {"summary": "plan-mode: user changed task during research, restarting"})
26882
+ self.runtime_plan_mode_needed = False
26883
+ self.runtime_plan_approved = False
26884
+ return
26885
+
26629
26886
  self._emit("status", {"summary": "plan-mode: synthesizing proposals"})
26630
26887
  bb = self._ensure_blackboard()
26631
26888
  if not isinstance(bb.get("plan"), dict):
@@ -26633,7 +26890,17 @@ class SessionState:
26633
26890
  bb["plan"]["phase"] = "synthesis"
26634
26891
  self.blackboard = bb
26635
26892
 
26636
- proposal = self._plan_mode_synthesize_proposal(pinned_selection)
26893
+ # Synthesis with retry (up to 2 attempts) + minimal fallback
26894
+ proposal = None
26895
+ for _synth_attempt in range(2):
26896
+ proposal = self._plan_mode_synthesize_proposal(pinned_selection)
26897
+ if proposal and proposal.get("options"):
26898
+ break
26899
+ if _synth_attempt == 0:
26900
+ self._emit("status", {"summary": "plan-mode: synthesis retry"})
26901
+ if not proposal or not proposal.get("options"):
26902
+ # Last resort: minimal fallback with simpler prompt and higher token budget
26903
+ proposal = self._synthesis_minimal_fallback(pinned_selection)
26637
26904
  if not proposal or not proposal.get("options"):
26638
26905
  self._emit("status", {"summary": "plan-mode: synthesis failed, falling back to direct execution"})
26639
26906
  self.runtime_plan_mode_needed = False
@@ -26649,16 +26916,23 @@ class SessionState:
26649
26916
  bb["plan"]["proposal"] = proposal
26650
26917
  self.blackboard = bb
26651
26918
 
26652
- plan_text = self._format_plan_proposal_markdown(proposal)
26919
+ # Write full plan to file for model consumption
26920
+ try:
26921
+ self._write_plan_file(self._format_plan_file_preselection(proposal))
26922
+ except Exception:
26923
+ pass
26924
+
26925
+ # Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS)
26926
+ bubble_text = self._format_plan_bubble_preselection(proposal)
26653
26927
  self.messages.append({
26654
26928
  "role": "assistant",
26655
- "content": plan_text,
26929
+ "content": bubble_text,
26656
26930
  "ts": now_ts(),
26657
26931
  "agent_role": "planner",
26658
26932
  })
26659
26933
  self._emit("message", {
26660
26934
  "role": "assistant",
26661
- "text": trim(plan_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
26935
+ "text": trim(bubble_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
26662
26936
  "summary": "plan-mode proposal",
26663
26937
  "agent_role": "planner",
26664
26938
  })
@@ -26706,7 +26980,12 @@ class SessionState:
26706
26980
  f" - Relevant skills found (names, what they do, how to invoke them)\n"
26707
26981
  f" - File inventory (uploaded files, their types, sizes, key content)\n"
26708
26982
  f" - Skill workflow breakdown (concrete tools, scripts, paths for each relevant skill)\n"
26709
- f" - Content analysis (key themes, structure, data points extracted from inputs)\n\n"
26983
+ f" - Content analysis (key themes, structure, data points extracted from inputs)\n"
26984
+ f"9. For coding tasks, identify the test strategy:\n"
26985
+ f" - What build/compilation commands are available? (Makefile, npm, cargo, cmake, etc.)\n"
26986
+ f" - What test frameworks/suites exist? (pytest, jest, go test, etc.)\n"
26987
+ f" - What are the critical paths that must be tested?\n"
26988
+ f" - Record these in plan_findings under 'test_strategy'.\n\n"
26710
26989
  f"Workspace: \"{self.files_root}\" ($SESSION_ROOT)\n"
26711
26990
  f"{os_note}\n"
26712
26991
  f"{lang_note}"
@@ -27168,8 +27447,17 @@ class SessionState:
27168
27447
  f"- Each step should be completable in 1-3 tool calls\n"
27169
27448
  f"- Group related substeps under numbered headings (e.g., '2.1 Read report 1', '2.2 Read report 2')\n"
27170
27449
  f"Make options meaningfully different (e.g. different approaches, scope levels, or trade-offs).\n"
27171
- f"{model_language_instruction(self.ui_language)}"
27172
- )
27450
+ "\nVERIFICATION & TESTING:\n"
27451
+ "Judge from the task content and research findings whether the task involves writing, "
27452
+ "modifying, or generating code/scripts/configurations. If it does:\n"
27453
+ "- Include compile/build/lint verification steps after implementation steps.\n"
27454
+ "- Include a dedicated testing step with specific commands before final review.\n"
27455
+ "- For large plans (10+ steps), insert intermediate test checkpoints.\n"
27456
+ "- If the task modifies existing code, include a regression test step.\n"
27457
+ "If the task is pure research, analysis, or document generation with no executable code, "
27458
+ "skip compile/test steps — use your judgement.\n"
27459
+ )
27460
+ synthesis_prompt += f"{model_language_instruction(self.ui_language)}"
27173
27461
  synthesis_ctx = [
27174
27462
  {"role": "system", "content": (
27175
27463
  "You are a technical architect synthesizing research into actionable plans. "
@@ -27198,6 +27486,63 @@ class SessionState:
27198
27486
  return dict(args)
27199
27487
  return {}
27200
27488
 
27489
+ def _synthesis_minimal_fallback(self, pinned_selection: str) -> dict:
27490
+ """Last-resort: ask model for a single simple plan with higher max_tokens."""
27491
+ goal = trim(str(self.runtime_reclassify_goal or self._latest_user_goal_text() or ""), 2000)
27492
+ bb = self._ensure_blackboard()
27493
+ findings = bb.get("plan", {}).get("findings", []) if isinstance(bb.get("plan"), dict) else []
27494
+ findings_text = "\n".join(
27495
+ trim(str(f.get("content", "") if isinstance(f, dict) else ""), 600)
27496
+ for f in (findings[:5] if isinstance(findings, list) else [])
27497
+ )
27498
+ prompt = (
27499
+ f"Generate ONE simple plan for this task. Call submit_plan_proposal with exactly 1 option.\n\n"
27500
+ f"Task: {goal}\n\nFindings: {trim(findings_text, 3000)}\n\n"
27501
+ f"Return a single option with id='A', title, summary, and 5-10 concrete steps.\n"
27502
+ f"{model_language_instruction(self.ui_language)}"
27503
+ )
27504
+ ctx = [
27505
+ {"role": "system", "content": "You must call submit_plan_proposal tool.", "ts": now_ts()},
27506
+ {"role": "user", "content": prompt, "ts": now_ts()},
27507
+ ]
27508
+ try:
27509
+ response = self._chat_with_same_model_retry(
27510
+ ctx,
27511
+ tools=self._plan_mode_synthesis_tools(),
27512
+ system="Call submit_plan_proposal now.",
27513
+ max_tokens=6000,
27514
+ think=False,
27515
+ stream_thinking=False,
27516
+ on_thinking_chunk=self._append_live_thinking,
27517
+ pinned_selection=pinned_selection,
27518
+ context_label="plan-mode minimal fallback",
27519
+ retries=2,
27520
+ )
27521
+ for tc in response.get("tool_calls", []):
27522
+ if tc.get("function", {}).get("name") == "submit_plan_proposal":
27523
+ args = tc["function"].get("arguments", {})
27524
+ if isinstance(args, dict) and args.get("options"):
27525
+ return dict(args)
27526
+ except Exception:
27527
+ pass
27528
+ return {}
27529
+
27530
+ def _has_pending_goal_change(self) -> bool:
27531
+ """Check if user sent a substantive goal change (not just plan choice or continuation)."""
27532
+ with self.lock:
27533
+ for row in self.pending_user_inputs:
27534
+ content = str(row.get("content", "") or "").strip().lower()
27535
+ if not content:
27536
+ continue
27537
+ if content in {
27538
+ "继续", "continue", "go on", "接着", "a", "b", "c",
27539
+ "方案a", "方案b", "方案c", "keep going", "proceed",
27540
+ }:
27541
+ continue
27542
+ if len(content) > 10:
27543
+ return True
27544
+ return False
27545
+
27201
27546
  def _plan_mode_synthesis_tools(self) -> list:
27202
27547
  return [tool_def(
27203
27548
  "submit_plan_proposal",
@@ -27225,6 +27570,185 @@ class SessionState:
27225
27570
  ["context", "options", "recommended"],
27226
27571
  )]
27227
27572
 
27573
+ # ── Plan MD File helpers ──────────────────────────────────────────
27574
+
27575
+ def _plan_file_path(self) -> Path:
27576
+ """Plan MD file path (inside files_root sandbox, accessible via read_file)."""
27577
+ return self.files_root / ".clouds_coder" / "plan.md"
27578
+
27579
+ def _write_plan_file(self, content: str) -> bool:
27580
+ """Atomically write plan.md: write tmp → os.replace."""
27581
+ import uuid as _uuid_mod
27582
+ target = self._plan_file_path()
27583
+ target.parent.mkdir(parents=True, exist_ok=True)
27584
+ tmp = target.with_suffix(f".{_uuid_mod.uuid4().hex[:8]}.tmp")
27585
+ try:
27586
+ tmp.write_text(content, encoding="utf-8")
27587
+ os.replace(str(tmp), str(target))
27588
+ return True
27589
+ except Exception:
27590
+ try:
27591
+ tmp.unlink(missing_ok=True)
27592
+ except Exception:
27593
+ pass
27594
+ return False
27595
+
27596
+ def _read_plan_file(self) -> str:
27597
+ """Read plan.md; returns empty string if file does not exist."""
27598
+ try:
27599
+ p = self._plan_file_path()
27600
+ return p.read_text(encoding="utf-8") if p.exists() else ""
27601
+ except Exception:
27602
+ return ""
27603
+
27604
+ def _format_plan_file_preselection(self, proposal: dict) -> str:
27605
+ """Full MD content with ALL options for model review (no char limit)."""
27606
+ lines = ["# Execution Plan Proposals\n"]
27607
+ context = str(proposal.get("context", "") or "").strip()
27608
+ if context:
27609
+ lines.append(f"## Background\n{context}\n")
27610
+ recommended = str(proposal.get("recommended", "") or "").strip()
27611
+ options = proposal.get("options", [])
27612
+ if not isinstance(options, list):
27613
+ options = []
27614
+ for opt in options[:PLAN_MODE_MAX_OPTIONS]:
27615
+ if not isinstance(opt, dict):
27616
+ continue
27617
+ opt_id = str(opt.get("id", "") or "").strip()
27618
+ title = str(opt.get("title", "") or "").strip()
27619
+ header = f"## Option {opt_id}: {title}"
27620
+ if opt_id == recommended:
27621
+ header += " [RECOMMENDED]"
27622
+ lines.append("---\n")
27623
+ lines.append(header)
27624
+ summary = str(opt.get("summary", "") or "").strip()
27625
+ if summary:
27626
+ lines.append(summary)
27627
+ steps = opt.get("steps", [])
27628
+ if isinstance(steps, list) and steps:
27629
+ lines.append("\n### Steps")
27630
+ for i, s in enumerate(steps):
27631
+ lines.append(f"{i + 1}. {s}")
27632
+ pros = str(opt.get("pros", "") or "").strip()
27633
+ if pros:
27634
+ lines.append(f"\n**Pros:** {pros}")
27635
+ cons = str(opt.get("cons", "") or "").strip()
27636
+ if cons:
27637
+ lines.append(f"**Cons:** {cons}")
27638
+ risk = str(opt.get("risk", "") or "").strip()
27639
+ if risk:
27640
+ lines.append(f"**Risk:** {risk}")
27641
+ lines.append("")
27642
+ lines.append("---")
27643
+ lines.append("> Awaiting user choice.")
27644
+ return "\n".join(lines)
27645
+
27646
+ def _format_plan_file_execution(self, choice_id: str) -> str:
27647
+ """Render execution-phase plan.md with live step statuses from blackboard."""
27648
+ bb = self._ensure_blackboard()
27649
+ proposal = self.runtime_plan_proposal or {}
27650
+ chosen = next(
27651
+ (o for o in proposal.get("options", [])
27652
+ if isinstance(o, dict) and o.get("id") == choice_id),
27653
+ None,
27654
+ )
27655
+ title = str((chosen or {}).get("title", "") or choice_id).strip()
27656
+ summary = str((chosen or {}).get("summary", "") or "").strip()
27657
+ todos = bb.get("project_todos", [])
27658
+ plan_todos = [t for t in todos if t.get("category") == "plan_step"]
27659
+ total = len(plan_todos)
27660
+ completed = sum(1 for t in plan_todos if t.get("status") == "completed")
27661
+ current_idx = completed + 1
27662
+
27663
+ lines = [f"# Active Plan: {title}\n"]
27664
+ lines.append(f"> Status: EXECUTING | Step {current_idx}/{total}")
27665
+ lines.append(f"> Chosen: Option {choice_id}")
27666
+ from datetime import datetime as _dt_cls
27667
+ lines.append(f"> Updated: {_dt_cls.now().isoformat(timespec='seconds')}\n")
27668
+ if summary:
27669
+ lines.append(f"## Summary\n{summary}\n")
27670
+ lines.append("## Steps\n")
27671
+ for t in plan_todos:
27672
+ idx = int(t.get("plan_step_index", 0) or 0) + 1
27673
+ text = str(t.get("content", "") or "").strip()
27674
+ status = str(t.get("status", "pending") or "pending")
27675
+ if status == "completed":
27676
+ actor = str(t.get("completed_by", "") or "")
27677
+ evidence = str(t.get("evidence", "") or "")
27678
+ lines.append(f"- [x] Step {idx}: {text}")
27679
+ meta_parts = []
27680
+ if actor:
27681
+ meta_parts.append(f"Completed by: {actor}")
27682
+ if evidence:
27683
+ meta_parts.append(f"Evidence: {evidence}")
27684
+ if meta_parts:
27685
+ lines.append(f" > {' | '.join(meta_parts)}")
27686
+ elif status == "in_progress":
27687
+ lines.append(f"- [>] Step {idx}: {text} <-- CURRENT")
27688
+ else:
27689
+ lines.append(f"- [ ] Step {idx}: {text}")
27690
+ return "\n".join(lines) + "\n"
27691
+
27692
+ def _update_plan_file_step_status(self) -> bool:
27693
+ """Re-render execution-phase plan.md from current blackboard state and write atomically."""
27694
+ choice_id = str(self.runtime_plan_choice or "")
27695
+ if not choice_id:
27696
+ bb = self._ensure_blackboard()
27697
+ plan_data = bb.get("plan", {})
27698
+ choice_id = str(plan_data.get("chosen", "") if isinstance(plan_data, dict) else "")
27699
+ if not choice_id:
27700
+ return False
27701
+ content = self._format_plan_file_execution(choice_id)
27702
+ return self._write_plan_file(content)
27703
+
27704
+ def _format_plan_bubble_preselection(self, proposal: dict) -> str:
27705
+ """Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS). No full step listing."""
27706
+ lines = ["## 📋 执行方案\n"]
27707
+ context = str(proposal.get("context", "") or "").strip()
27708
+ if context:
27709
+ lines.append(f"**背景:** {trim(context, 300)}\n")
27710
+ recommended = str(proposal.get("recommended", "") or "").strip()
27711
+ options = proposal.get("options", [])
27712
+ if not isinstance(options, list):
27713
+ options = []
27714
+ for opt in options[:PLAN_MODE_MAX_OPTIONS]:
27715
+ if not isinstance(opt, dict):
27716
+ continue
27717
+ opt_id = str(opt.get("id", "") or "").strip()
27718
+ title = str(opt.get("title", "") or "").strip()
27719
+ is_rec = opt_id == recommended
27720
+ header = f"### 方案 {opt_id}: {title}"
27721
+ if is_rec:
27722
+ header += " ⭐推荐"
27723
+ lines.append(header)
27724
+ summary = str(opt.get("summary", "") or "").strip()
27725
+ if summary:
27726
+ lines.append(trim(summary, 200))
27727
+ steps = opt.get("steps", [])
27728
+ step_count = len(steps) if isinstance(steps, list) else 0
27729
+ risk = str(opt.get("risk", "") or "").strip()
27730
+ meta = f"步骤数: {step_count}"
27731
+ if risk:
27732
+ meta += f" | 风险: {risk}"
27733
+ lines.append(meta)
27734
+ lines.append("")
27735
+ lines.append("---")
27736
+ lines.append(f"完整方案详见: `{PLAN_FILE_RELATIVE_PATH}`")
27737
+ lines.append('请回复选择(如"方案A"、"A"、"选1"),或输入修改意见。')
27738
+ return trim("\n".join(lines), PLAN_BUBBLE_MAX_CHARS)
27739
+
27740
+ def _plan_file_read_instruction(self) -> str:
27741
+ """Short instruction for models: read the plan file instead of embedding full plan text."""
27742
+ return (
27743
+ f"[plan-file] The approved execution plan is at `{PLAN_FILE_RELATIVE_PATH}`.\n"
27744
+ f"Use: read_file {PLAN_FILE_RELATIVE_PATH} to review full steps and live status.\n"
27745
+ "The plan file is the authoritative source for step ordering and completion status.\n"
27746
+ "Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing.\n"
27747
+ "If a step references a skill or workflow, call load_skill to load it before proceeding."
27748
+ )
27749
+
27750
+ # ── (legacy) _format_plan_proposal_markdown ──────────────────────
27751
+
27228
27752
  def _format_plan_proposal_markdown(self, proposal: dict) -> str:
27229
27753
  lines = ["## 📋 执行方案\n"]
27230
27754
  context = str(proposal.get("context", "") or "").strip()
@@ -27350,16 +27874,9 @@ class SessionState:
27350
27874
  )
27351
27875
  if not chosen:
27352
27876
  return
27353
- steps_text = "\n".join(f"{i+1}. {s}" for i, s in enumerate(chosen.get("steps", [])))
27354
- plan_msg = (
27355
- f"[approved-plan] Execute the following plan:\n"
27356
- f"## {chosen.get('title', '')}\n"
27357
- f"{chosen.get('summary', '')}\n\n"
27358
- f"### Steps:\n{steps_text}\n\n"
27359
- f"Follow these steps. Use tools to implement each step concretely.\n"
27360
- f"If a step references a skill or workflow you don't fully understand, "
27361
- f"call load_skill to load it and read its instructions before proceeding."
27362
- )
27877
+ # Write execution-phase plan file (detailed, for model read_file)
27878
+ # Must happen BEFORE blackboard todos are created so the file reflects initial state
27879
+ plan_msg = self._plan_file_read_instruction()
27363
27880
  self.messages.append({
27364
27881
  "role": "system",
27365
27882
  "content": plan_msg,
@@ -27377,6 +27894,9 @@ class SessionState:
27377
27894
  bb["plan"] = {"phase": "executing", "chosen": choice_id, "steps": chosen.get("steps", [])}
27378
27895
  self.blackboard = bb
27379
27896
  self._blackboard_history("manager", f"plan approved: option {choice_id} — {chosen.get('title', '')}")
27897
+ # Lock complexity/level floor to prevent manager downgrade during plan execution
27898
+ self.runtime_complexity_floor = str(self.runtime_task_complexity or "complex")
27899
+ self.runtime_task_level_floor = int(self.runtime_task_level or 4)
27380
27900
  # Auto-create todos from plan steps → write into bb["project_todos"]
27381
27901
  steps = chosen.get("steps", [])
27382
27902
  if steps and isinstance(steps, list):
@@ -27397,7 +27917,7 @@ class SessionState:
27397
27917
  "evidence": "",
27398
27918
  })
27399
27919
  if plan_todos:
27400
- bb["project_todos"] = plan_todos[:20]
27920
+ bb["project_todos"] = plan_todos[:40]
27401
27921
  bb["plan_step_cursor"] = 0
27402
27922
  bb["plan_step_total"] = len(plan_todos)
27403
27923
  self.blackboard = bb
@@ -27412,12 +27932,13 @@ class SessionState:
27412
27932
  "status": t["status"],
27413
27933
  "activeForm": f"Working on: {t['content']}" if t["status"] == "in_progress" else f"Pending: {t['content']}",
27414
27934
  }
27415
- for t in plan_todos[:20]
27935
+ for t in plan_todos[:40]
27416
27936
  ])
27417
27937
  except Exception:
27418
27938
  pass
27939
+ # Write execution-phase plan file (now with todos populated)
27419
27940
  try:
27420
- pass # Skills are loaded on-demand by the model via load_skill
27941
+ self._write_plan_file(self._format_plan_file_execution(choice_id))
27421
27942
  except Exception:
27422
27943
  pass
27423
27944
  # Pre-load skills explicitly mentioned in plan steps
@@ -30507,6 +31028,9 @@ h3{font-size:.96rem;margin:10px 0 6px}
30507
31028
  .todo-item,.task-item{border:1px solid #e4ebf4;border-left-width:4px;border-radius:10px;padding:8px 10px;background:#fcfdff}
30508
31029
  .todo-item.st-pending,.task-item.st-pending{border-left-color:#7b8798}
30509
31030
  .todo-item.st-in_progress,.task-item.st-in_progress{border-left-color:#1f6feb;background:#eef5ff}
31031
+ .todo-group-label{font-size:.72rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:6px 0 2px 2px}
31032
+ .todo-subtask{margin-left:16px;border-left-width:3px;border-radius:8px;padding:6px 10px;font-size:.9em}
31033
+ .todo-subtask::before{content:"↳ ";color:var(--muted);font-size:.85em}
30510
31034
  .todo-item.st-completed,.task-item.st-completed{border-left-color:#13b8a6;background:#edfcf7}
30511
31035
  .todo-item.st-blocked,.task-item.st-blocked{border-left-color:#b96b00;background:#fff6ea}
30512
31036
  .todo-item.st-deleted,.task-item.st-deleted{border-left-color:#a0a6b0;background:#f7f8fa}
@@ -30665,7 +31189,8 @@ const I18N={
30665
31189
  llm_fill_config:'Fill LLM Config',llm_provider:'Provider',llm_confirm:'Confirm',llm_import_config:'Import config',
30666
31190
  llm_thinking_stream:'Thinking Stream',llm_enabled:'Enabled',llm_disabled:'Disabled',
30667
31191
  llm_model:'Model',llm_scan:'Scan',llm_scan_hint:'Click Scan to detect models from Ollama',llm_scan_first:'Scan models first',
30668
- llm_scanning:'Scanning...',llm_scan_found:'Found {n} model(s)',llm_scan_empty:'No models found',llm_scan_error:'Scan failed'
31192
+ llm_scanning:'Scanning...',llm_scan_found:'Found {n} model(s)',llm_scan_empty:'No models found',llm_scan_error:'Scan failed',
31193
+ todo_plan_steps:'Plan Steps',todo_subtasks:'Subtasks'
30669
31194
  },
30670
31195
  'zh-CN':{
30671
31196
  app_title:'Clouds Coder',app_subtitle:'WebUI 驱动的会话式集成编程 Agent 平台',powered_by:'Powered By Fona',
@@ -30700,7 +31225,8 @@ const I18N={
30700
31225
  llm_fill_config:'填写 LLM 配置',llm_provider:'供应商',llm_confirm:'确认',llm_import_config:'导入配置',
30701
31226
  llm_thinking_stream:'思维流',llm_enabled:'启用',llm_disabled:'禁用',
30702
31227
  llm_model:'模型',llm_scan:'扫描',llm_scan_hint:'点击扫描检测 Ollama 可用模型',llm_scan_first:'请先扫描模型',
30703
- llm_scanning:'扫描中...',llm_scan_found:'发现 {n} 个模型',llm_scan_empty:'未发现模型',llm_scan_error:'扫描失败'
31228
+ llm_scanning:'扫描中...',llm_scan_found:'发现 {n} 个模型',llm_scan_empty:'未发现模型',llm_scan_error:'扫描失败',
31229
+ todo_plan_steps:'计划步骤',todo_subtasks:'子任务'
30704
31230
  },
30705
31231
  'zh-TW':{
30706
31232
  app_title:'Clouds Coder',app_subtitle:'WebUI 驅動的會話式整合程式 Agent 平台',powered_by:'Powered By Fona',
@@ -30735,7 +31261,8 @@ const I18N={
30735
31261
  llm_fill_config:'填寫 LLM 設定',llm_provider:'供應商',llm_confirm:'確認',llm_import_config:'匯入設定',
30736
31262
  llm_thinking_stream:'思維流',llm_enabled:'啟用',llm_disabled:'停用',
30737
31263
  llm_model:'模型',llm_scan:'掃描',llm_scan_hint:'點擊掃描偵測 Ollama 可用模型',llm_scan_first:'請先掃描模型',
30738
- llm_scanning:'掃描中...',llm_scan_found:'發現 {n} 個模型',llm_scan_empty:'未發現模型',llm_scan_error:'掃描失敗'
31264
+ llm_scanning:'掃描中...',llm_scan_found:'發現 {n} 個模型',llm_scan_empty:'未發現模型',llm_scan_error:'掃描失敗',
31265
+ todo_plan_steps:'計劃步驟',todo_subtasks:'子任務'
30739
31266
  },
30740
31267
  'ja':{
30741
31268
  app_title:'Clouds Coder',app_subtitle:'WebUI 駆動の対話型コーディング Agent プラットフォーム',powered_by:'Powered By Fona',
@@ -30770,7 +31297,8 @@ const I18N={
30770
31297
  llm_fill_config:'LLM設定入力',llm_provider:'プロバイダー',llm_confirm:'確認',llm_import_config:'設定をインポート',
30771
31298
  llm_thinking_stream:'シンキングストリーム',llm_enabled:'有効',llm_disabled:'無効',
30772
31299
  llm_model:'モデル',llm_scan:'スキャン',llm_scan_hint:'スキャンをクリックしてOllamaモデルを検出',llm_scan_first:'先にモデルをスキャン',
30773
- llm_scanning:'スキャン中...',llm_scan_found:'{n}個のモデルを検出',llm_scan_empty:'モデルが見つかりません',llm_scan_error:'スキャン失敗'
31300
+ llm_scanning:'スキャン中...',llm_scan_found:'{n}個のモデルを検出',llm_scan_empty:'モデルが見つかりません',llm_scan_error:'スキャン失敗',
31301
+ todo_plan_steps:'計画ステップ',todo_subtasks:'サブタスク'
30774
31302
  }
30775
31303
  };
30776
31304
  function currentLang(){const fromSnap=String(S.snap?.ui_language||'').trim();if(fromSnap&&I18N[fromSnap])return fromSnap;const fromCfg=String(S.config?.language||'').trim();if(fromCfg&&I18N[fromCfg])return fromCfg;return 'zh-CN'}
@@ -33081,7 +33609,54 @@ function statusClass(status){return `st-${normalizeStatus(status)}`}
33081
33609
  function statusLabel(status){const s=normalizeStatus(status);if(s==='in_progress')return t('status_in_progress');if(s==='completed')return t('status_completed');if(s==='blocked')return t('status_blocked');if(s==='deleted')return t('status_deleted');return t('status_pending')}
33082
33610
  function cleanWorkText(text,status=''){let s=String(text??'').replace(/\\s+/g,' ').trim();if(!s)return '';s=s.replace(/^\\[[ x>\\-]\\]\\s*/i,'');s=s.replace(/^(pending|in[_\\-\\s]?progress|completed|done|blocked)\\s*[·:\\-\\]]\\s*/i,'');if(status){const st=String(status).replace('_','[_\\\\-\\\\s]?');s=s.replace(new RegExp(`\\\\s*[—-]\\\\s*${st}\\\\s*$`,'i'),'')}s=s.replace(/\\s*[—-]\\s*(pending|in[_\\-\\s]?progress|completed|done|blocked)\\s*$/i,'');return s.trim()||String(text??'').trim()}
33083
33611
  function formatTs(ts){const v=Number(ts||0);if(!v)return '';try{return new Date(v*1000).toLocaleString()}catch(_){return ''}}
33084
- function renderTodoBoard(items){const todos=Array.isArray(items)?items:[];if(!todos.length)return `<div class=\"mono\">${esc(t('no_todos'))}</div>`;const done=todos.filter(t=>normalizeStatus(t?.status)==='completed').length;const open=todos.length-done;const cards=todos.map((t,idx)=>{const status=normalizeStatus(t?.status);const content=cleanWorkText(t?.content,status)||'(empty todo)';const active=String(t?.activeForm||'').trim();const meta=status==='in_progress'&&active?`<div class=\"todo-meta\">${esc(cleanWorkText(active,status))}</div>`:'';return `<div class=\"todo-item ${statusClass(status)}\"><div class=\"todo-head\"><span class=\"status-badge ${statusClass(status)}\">${esc(statusLabel(status))}</span><span class=\"mono todo-index\">#${idx+1}</span></div><div class=\"todo-content\">${esc(content)}</div>${meta}</div>`}).join('');return `<div class=\"board-summary\"><span>${esc(open)} ${esc(t('open'))}</span><span>${esc(done)}/${esc(todos.length)} ${esc(t('completed'))}</span></div><div class=\"todo-list\">${cards}</div>`}
33612
+ function renderTodoBoard(items){
33613
+ const todos=Array.isArray(items)?items:[];
33614
+ if(!todos.length)return `<div class="mono">${esc(t('no_todos'))}</div>`;
33615
+ const done=todos.filter(x=>normalizeStatus(x?.status)==='completed').length;
33616
+ const open=todos.length-done;
33617
+ function todoCard(item,idx,extraClass=''){
33618
+ const status=normalizeStatus(item?.status);
33619
+ const content=cleanWorkText(item?.content,status)||'(empty todo)';
33620
+ const active=String(item?.activeForm||'').trim();
33621
+ const meta=status==='in_progress'&&active?`<div class="todo-meta">${esc(cleanWorkText(active,status))}</div>`:'';
33622
+ return `<div class="todo-item ${statusClass(status)}${extraClass?(' '+extraClass):''}"><div class="todo-head"><span class="status-badge ${statusClass(status)}">${esc(statusLabel(status))}</span><span class="mono todo-index">#${idx+1}</span></div><div class="todo-content">${esc(content)}</div>${meta}</div>`;
33623
+ }
33624
+ // Split into plan steps (bb:proj:) and worker subtasks
33625
+ const planSteps=todos.filter(x=>String(x?.key||'').startsWith('bb:proj:'));
33626
+ const workerTodos=todos.filter(x=>!String(x?.key||'').startsWith('bb:proj:'));
33627
+ let html='';
33628
+ if(planSteps.length&&workerTodos.length){
33629
+ // Build parent_step_id index: map step key suffix to its subtasks
33630
+ const stepIdFromKey=(key)=>{const k=String(key||'');return k.startsWith('bb:proj:')?k.slice(8):''};
33631
+ const subtasksByStep={};
33632
+ const unlinked=[];
33633
+ workerTodos.forEach(sub=>{
33634
+ const pid=String(sub?.parent_step_id||'').trim();
33635
+ if(pid){(subtasksByStep[pid]=subtasksByStep[pid]||[]).push(sub)}
33636
+ else{unlinked.push(sub)}
33637
+ });
33638
+ // Find active plan step for unlinked subtasks fallback
33639
+ const activeStepIdx=planSteps.findIndex(x=>normalizeStatus(x?.status)==='in_progress');
33640
+ html+=`<div class="todo-group-label">${esc(t('todo_plan_steps'))}</div><div class="todo-list">`;
33641
+ planSteps.forEach((step,i)=>{
33642
+ html+=todoCard(step,i);
33643
+ const sid=stepIdFromKey(step?.key);
33644
+ // Show subtasks linked to this step via parent_step_id
33645
+ const linked=sid?subtasksByStep[sid]||[]:[];
33646
+ // Also attach unlinked subtasks under the active plan step (backward compat)
33647
+ const subs=i===activeStepIdx?linked.concat(unlinked):linked;
33648
+ if(subs.length){
33649
+ html+=`<div class="todo-group-label" style="margin-left:16px">${esc(t('todo_subtasks'))}</div>`;
33650
+ subs.forEach((sub,j)=>{html+=todoCard(sub,j,'todo-subtask')});
33651
+ }
33652
+ });
33653
+ html+=`</div>`;
33654
+ } else {
33655
+ // No grouping needed — flat list
33656
+ html+=`<div class="todo-list">${todos.map((x,i)=>todoCard(x,i)).join('')}</div>`;
33657
+ }
33658
+ return `<div class="board-summary"><span>${esc(open)} ${esc(t('open'))}</span><span>${esc(done)}/${esc(todos.length)} ${esc(t('completed'))}</span></div>${html}`;
33659
+ }
33085
33660
  function renderTaskBoard(items){const tasks=Array.isArray(items)?items:[];if(!tasks.length)return `<div class=\"mono\">${esc(t('no_tasks'))}</div>`;const completed=tasks.filter(row=>normalizeStatus(row?.status,'pending')==='completed').length;const blocked=tasks.filter(row=>normalizeStatus(row?.status,'pending')==='blocked').length;const cards=tasks.map(row=>{const status=normalizeStatus(row?.status,'pending');const id=Number(row?.id||0)||'-';const subject=cleanWorkText(row?.subject,status)||'(empty task)';const owner=String(row?.owner||'').trim();const blockedBy=Array.isArray(row?.blockedBy)&&row.blockedBy.length?`blocked_by=${row.blockedBy.map(x=>`#${x}`).join(', ')}`:'';const blocks=Array.isArray(row?.blocks)&&row.blocks.length?`blocks=${row.blocks.map(x=>`#${x}`).join(', ')}`:'';const timeTxt=formatTs(row?.updated_at||row?.created_at);const meta=[owner?`owner=@${owner}`:t('owner_unassigned'),blockedBy,blocks,timeTxt].filter(Boolean).join(' · ');return `<div class=\"task-item ${statusClass(status)}\"><div class=\"task-head\"><span class=\"mono task-id\">#${esc(id)}</span><span class=\"status-badge ${statusClass(status)}\">${esc(statusLabel(status))}</span></div><div class=\"task-subject\">${esc(subject)}</div><div class=\"task-meta\">${esc(meta)}</div></div>`}).join('');return `<div class=\"board-summary\"><span>${esc(tasks.length-completed)} ${esc(t('open'))}</span><span>${esc(completed)} ${esc(t('completed'))} · ${esc(blocked)} ${esc(t('blocked'))}</span></div><div class=\"task-list\">${cards}</div>`}
33086
33661
  function ensureFileExplorerState(sessionId){const sid=String(sessionId||S.activeId||'').trim();if(!sid)return null;if(!S.fileExplorerBySession)S.fileExplorerBySession={};if(!S.fileExplorerBySession[sid]||typeof S.fileExplorerBySession[sid]!=='object'){S.fileExplorerBySession[sid]={tree:null,root:'',nodeCount:0,truncated:false,maxNodes:0,fetchedAt:0,inflight:false,selected:'',expanded:{'':true}}}const st=S.fileExplorerBySession[sid];if(!st.expanded||typeof st.expanded!=='object')st.expanded={'':true};st.expanded['']=true;return st}
33087
33662
  function _fePath(sessionId){const sid=encodeURIComponent(String(sessionId||'').trim());return `/api/sessions/${sid}/files-tree`}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clouds-coder
3
- Version: 2026.3.27
3
+ Version: 2026.3.28
4
4
  Summary: Clouds Coder: local-first Cloud CLI coder runtime with Web UI and Skills Studio.
5
5
  Author: Clouds Coder Contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clouds-coder
3
- Version: 2026.3.27
3
+ Version: 2026.3.28
4
4
  Summary: Clouds Coder: local-first Cloud CLI coder runtime with Web UI and Skills Studio.
5
5
  Author: Clouds Coder Contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clouds-coder"
7
- version = "2026.3.27"
7
+ version = "2026.3.28"
8
8
  description = "Clouds Coder: local-first Cloud CLI coder runtime with Web UI and Skills Studio."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"