codebee 0.1.63 → 0.1.64

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.
@@ -1541,13 +1541,27 @@ def _run_code(run, task, agents, ev, stats, mode):
1541
1541
  _write_project_memory(task, workdir, _mem_lines)
1542
1542
  except Exception:
1543
1543
  pass
1544
+ # Stop gate(planning-with-files):收工时活计划若有未勾选项,摘要与 verdict
1545
+ # 如实点名——「计划没走完就报完成」属于静默降级(赛马/异常路径不回写勾选时
1546
+ # 此前无任何可见信号)。只保可见不改判:勾选是文件事实,质量判据仍是评审。
1547
+ try:
1548
+ gate_total, gate_left = plan_stopgate(workdir)
1549
+ except Exception:
1550
+ gate_total, gate_left = 0, []
1551
+ gate_note = ""
1552
+ if gate_left:
1553
+ verdict["plan_gate"] = "活计划 %d 项中 %d 项未勾选完成:%s" % (
1554
+ gate_total, len(gate_left),
1555
+ "、".join("#%d[%s]%s" % (n, mk, t) for n, mk, t in gate_left[:3]))
1556
+ gate_note = ";⚠ 活计划 %d/%d 项完成" % (gate_total - len(gate_left), gate_total)
1544
1557
  store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
1545
- summary="代码任务%s(验证%s / 评审%s%s)" % (
1558
+ summary="代码任务%s(验证%s / 评审%s%s%s)" % (
1546
1559
  "通过" if overall_pass else "未通过",
1547
1560
  "通过" if verify_pass else "未通过",
1548
1561
  (("通过" if review_json.get("pass") else "未通过")
1549
1562
  if workflow["review_required"] else "按策略省略"),
1550
- ",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else ""),
1563
+ ",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else "",
1564
+ gate_note),
1551
1565
  ended_at=_now())
1552
1566
 
1553
1567
 
@@ -4145,6 +4159,31 @@ def _write_task_plan(task, workdir, plan):
4145
4159
  _PLAN_MARK = {"done": "x", "fail": "!", "run": ">"}
4146
4160
 
4147
4161
 
4162
+ def plan_stopgate(workdir):
4163
+ """Stop gate(planning-with-files 第四件):收工前校验活计划勾选完整性。
4164
+ 返回 (总项数, [(序号, 标记, 标题)]);计划文件缺失/异常 → (0, [])(无计划不拦)。
4165
+ 未完成 = 勾选框不是 [x](含 [ ] 未跑、[!] 失败、[>] 中断在跑)。"""
4166
+ try:
4167
+ from pathlib import Path as _P
4168
+ import re as _re
4169
+ root = _P(workdir).resolve()
4170
+ target = (root / ".codebee" / "task_plan.md").resolve()
4171
+ if root not in target.parents or not target.is_file():
4172
+ return 0, []
4173
+ pat = _re.compile(r"^(\d+)\. \[([ x!>])\] ")
4174
+ total, unfinished = 0, []
4175
+ for ln in target.read_text("utf-8").splitlines():
4176
+ m = pat.match(ln)
4177
+ if not m:
4178
+ continue
4179
+ total += 1
4180
+ if m.group(2) != "x":
4181
+ unfinished.append((int(m.group(1)), m.group(2), ln[m.end():][:60]))
4182
+ return total, unfinished
4183
+ except Exception:
4184
+ return 0, []
4185
+
4186
+
4148
4187
  def mark_task_plan(workdir, index, status):
4149
4188
  """活计划回写:把 .codebee/task_plan.md 第 index 项(1 基)的勾选框
4150
4189
  翻成 [x](完成)/[!](失败)/[>](进行中)。planning-with-files 的断点
@@ -4211,7 +4250,7 @@ def execute_run(run_id):
4211
4250
  if initial_status not in ("queued", "running"):
4212
4251
  return
4213
4252
  if store.update_run(run_id, expected_status=initial_status, status="running",
4214
- started_at=_now()) is None:
4253
+ started_at=_now(), resume_enqueue_at="", warnings=[]) is None:
4215
4254
  return
4216
4255
  if task is None:
4217
4256
  store.update_run(run_id, expected_status="running", status="failed",
@@ -981,7 +981,7 @@ def _model_flag(kind, model):
981
981
  return ["--model", model] # claude / opencode / aider
982
982
 
983
983
 
984
- _TRANSIENT = ("503", "502", "529", "429", "no available channel", "temporarily",
984
+ _TRANSIENT_DEFAULT = ("503", "502", "529", "429", "no available channel", "temporarily",
985
985
  "unavailable", "overloaded", "rate limit", "timeout", "timed out",
986
986
  "输出停滞",
987
987
  # 2026-09-15 连载验收实测:网关故障形态远不止 HTTP 5xx——
@@ -1007,10 +1007,15 @@ def _model_flag(kind, model):
1007
1007
  "enotfound", "getaddrinfo", "name or service not known",
1008
1008
  "name resolution")
1009
1009
 
1010
+ # 保留兼容别名:旧 worker 热重载时可能短暂丢失模块级变量,判断函数不能因此
1011
+ # 把一次普通上游故障升级成 NameError。
1012
+ _TRANSIENT = _TRANSIENT_DEFAULT
1010
1013
 
1011
- def _transient_error(err):
1014
+
1015
+ def _transient_error(err, _default_markers=_TRANSIENT_DEFAULT):
1012
1016
  err = (err or "").lower()
1013
- return any(k in err for k in _TRANSIENT)
1017
+ markers = globals().get("_TRANSIENT") or _default_markers
1018
+ return any(k in err for k in markers)
1014
1019
 
1015
1020
 
1016
1021
  # 欠费/配额/限流耗尽:换 KEY 与换厂商都该继续(同厂商另一账号往往还能用)。
package/app/core/store.py CHANGED
@@ -18,7 +18,7 @@ import threading
18
18
  import time
19
19
  from pathlib import Path
20
20
 
21
- from . import paths, runner, volumes
21
+ from . import artifacts, paths, runner, volumes
22
22
 
23
23
  LOCK = threading.RLock()
24
24
  _TASKS = {}
@@ -746,7 +746,7 @@ def task_side(task_id):
746
746
  total_steps = sum(len(r.get("steps") or []) for r in runs)
747
747
  # 成品口径闸:任务一步都没跑出来过(如历次都在检出前失败)→ 工作目录里的
748
748
  # 文件变动全是并行活动的噪音,不算这个任务的成品
749
- wd, arts = run_artifacts(latest["id"], limit=50) if (latest and total_steps > 0) else ("", [])
749
+ wd, arts = run_artifacts(latest["id"], limit=50, cache=True) if (latest and total_steps > 0) else ("", [])
750
750
  return {
751
751
  "task": {k: task.get(k) for k in ("id", "title", "status", "workdir", "git_state",
752
752
  "git_rev")},
@@ -966,41 +966,6 @@ def run_workdir(run_id):
966
966
  return str(task.get("workdir") or "") if task else ""
967
967
 
968
968
 
969
- _SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", ".idea", ".vscode"}
970
-
971
- # 构建产物/依赖缓存目录:里面的文件是工具链再生成的,不是任务成品
972
- _BUILD_DIRS = {
973
- "target", "build", "dist", "out", "bin", "obj", # 通用构建输出
974
- "surefire", "failsafe-reports", "test-output", "reports", # 测试/报告输出
975
- ".next", ".nuxt", ".output", ".gradle", ".gradle-home", # 前端/Gradle
976
- "__MACOSX",
977
- }
978
-
979
- # CLI 自身的历史/评审中间件不属于用户成果。它们通常落在工作目录根部,
980
- # 只按隐藏目录过滤会漏掉 Aider 的 .aider.* 与流程生成的 *_review.json。
981
- _PROCESS_ARTIFACT_NAMES = {
982
- ".aider.chat.history.md", ".aider.input.history", ".aider.input.history.md",
983
- ".aider.tags.cache.v4", ".aider.tags.cache.v3", ".aider.conf.yml",
984
- }
985
- _PROCESS_ARTIFACT_SUFFIXES = ("_review.json", ".review.json")
986
- _PROCESS_ARTIFACT_PREFIXES = ("tutti_prompt_", "_tutti_prompt_")
987
-
988
-
989
- def _is_process_artifact(rel_name):
990
- """判断工作目录文件是否为 CLI/评审过程产物,而非可交付成果。"""
991
- name = str(rel_name or "").replace("\\", "/")
992
- base = name.rsplit("/", 1)[-1].lower()
993
- if base in _PROCESS_ARTIFACT_NAMES:
994
- return True
995
- if base.startswith(".") and base.startswith(".aider"):
996
- return True
997
- if base.endswith(_PROCESS_ARTIFACT_SUFFIXES):
998
- return True
999
- if base.startswith(_PROCESS_ARTIFACT_PREFIXES):
1000
- return True
1001
- return False
1002
-
1003
-
1004
969
  def task_first_start(task_id, fallback=""):
1005
970
  """该任务最早一次运行的开始时间(含回退:任务创建时间 → 指定回退值)。"""
1006
971
  stamps = []
@@ -1019,13 +984,15 @@ def task_first_start(task_id, fallback=""):
1019
984
  return best
1020
985
 
1021
986
 
1022
- def run_artifacts(run_id, limit=200):
987
+ def run_artifacts(run_id, limit=200, cache=False):
1023
988
  """列一次运行的「成品文件」:工作目录里自该任务首次运行以来新产生/修改的文件。
1024
989
 
1025
990
  断点续跑会拆成多条 run,只按本 run 过滤会漏掉早期章节;这里以「任务首跑」
1026
991
  为起点。返回 (workdir, files);files 按 mtime 新→旧,name 为工作目录内
1027
- 相对路径,已跳过 .git / 隐藏目录 / node_modules / target 等构建产物目录,
1028
- 最多 limit 个。
992
+ 相对路径,已跳过 .git / 隐藏目录 / node_modules / target 等构建产物目录。
993
+ cache=True 供 UI 轮询热路径(详情/侧栏/状态快照)复用短 TTL 缓存与在飞
994
+ 合并;发布/预览/知识库等正确性敏感方保持默认,永远现扫。
995
+ 剪枝遍历与过滤实现在 artifacts.py(几万文件目录从秒级降到百毫秒级)。
1029
996
  """
1030
997
  wd = run_workdir(run_id)
1031
998
  if not wd:
@@ -1040,36 +1007,12 @@ def run_artifacts(run_id, limit=200):
1040
1007
  root = Path(wd)
1041
1008
  if not root.is_dir():
1042
1009
  return wd, []
1043
- files = []
1044
- try:
1045
- for p in root.rglob("*"):
1046
- if not p.is_file():
1047
- continue
1048
- if any(part in _SKIP_DIRS for part in p.parts):
1049
- continue
1050
- if any(part in _BUILD_DIRS for part in p.parts[:-1]):
1051
- continue
1052
- # 隐藏目录一律是工具过程文件(.mimocode/.zcode/.claude/.codex…),
1053
- # 不是成品;按前缀通排,免得每来一个新 agent CLI 就补一次白名单
1054
- if any(part.startswith(".") for part in p.parts[:-1]):
1055
- continue
1056
- try:
1057
- st = p.stat()
1058
- except OSError:
1059
- continue
1060
- if st.st_mtime < t0:
1061
- continue
1062
- rel_name = str(p.relative_to(root)).replace("\\", "/")
1063
- if _is_process_artifact(rel_name):
1064
- continue
1065
- files.append({"name": rel_name,
1066
- "size": st.st_size, "mtime": int(st.st_mtime)})
1067
- if len(files) >= 800: # 防超大目录拖垮接口;截断后再排序取最新
1068
- break
1069
- except OSError:
1070
- pass
1071
- files.sort(key=lambda f: -f["mtime"])
1072
- return wd, files[:limit]
1010
+ running = False
1011
+ if cache:
1012
+ task = get_task(run.get("task_id") or "") if run.get("task_id") else None
1013
+ running = bool(task) and task.get("status") in ("queued", "running")
1014
+ return wd, artifacts.scan(str(root), t0, limit=limit,
1015
+ running=running, cache=cache)
1073
1016
 
1074
1017
 
1075
1018
  def task_step_count(task_id):