codebee 0.1.3 → 0.1.5
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.
- package/CHANGELOG.md +36 -0
- package/README.md +9 -0
- package/app/core/attachments.py +40 -0
- package/app/core/bookmeta.py +157 -52
- package/app/core/bookmeta_catalog.py +67 -90
- package/app/core/builtin_agent.py +564 -0
- package/app/core/flows.py +328 -328
- package/app/core/gitmod.py +82 -8
- package/app/core/history.py +8 -2
- package/app/core/jobs.py +448 -424
- package/app/core/manager.py +151 -1
- package/app/core/modelhub.py +86 -4
- package/app/core/pipeline.py +2326 -2164
- package/app/core/router.py +8 -3
- package/app/core/runner.py +185 -21
- package/app/core/selfupdate.py +54 -11
- package/app/core/store.py +4 -2
- package/app/main.py +117 -12
- package/app/ui/app.js +279 -47
- package/app/ui/i18n.js +21 -5
- package/app/ui/index.html +6 -2
- package/app/ui/style.css +3379 -2764
- package/package.json +2 -1
package/app/core/gitmod.py
CHANGED
|
@@ -66,6 +66,11 @@ def repo_info(workdir):
|
|
|
66
66
|
if h.strip():
|
|
67
67
|
recent.append({"hash": h.strip(), "subject": subj.strip()[:120]})
|
|
68
68
|
br = (branch["stdout"] or "").strip() if branch["ok"] else ""
|
|
69
|
+
if br == "HEAD":
|
|
70
|
+
# 游离 HEAD 时 abbrev-ref 返回字面量 "HEAD"——统一成哨兵值。
|
|
71
|
+
# 否则「切回 from_branch」会把 "HEAD" 当分支名原样 checkout,
|
|
72
|
+
# 而 git checkout HEAD 是原地空转(退出码 0 但不换位),分支永远切不走。
|
|
73
|
+
br = "(游离 HEAD)"
|
|
69
74
|
return {
|
|
70
75
|
"repo": True,
|
|
71
76
|
"branch": br or "(游离 HEAD)",
|
|
@@ -408,7 +413,7 @@ def finalize_run(workdir, gitinfo, message):
|
|
|
408
413
|
# 切回原分支;原为游离 HEAD 时回到基线提交(同样游离)
|
|
409
414
|
back = (gitinfo or {}).get("from_branch") or ""
|
|
410
415
|
base = (gitinfo or {}).get("base_commit") or ""
|
|
411
|
-
target = back if (back and back
|
|
416
|
+
target = back if (back and back not in ("(游离 HEAD)", "HEAD") and back != branch) else base
|
|
412
417
|
if target:
|
|
413
418
|
co = _git(workdir, "checkout", "--quiet", target, timeout=60)
|
|
414
419
|
if not co["ok"]:
|
|
@@ -472,6 +477,12 @@ def merge_task_branch(workdir, task):
|
|
|
472
477
|
out = {"restore_error": ""}
|
|
473
478
|
_restore_stash(wd, stash_sha, out)
|
|
474
479
|
return refuse("无法确定任务分支的基线分支(找不到带检出信息的运行记录),请手工合并")
|
|
480
|
+
if origin in ("HEAD", "(游离 HEAD)"):
|
|
481
|
+
# 老数据里游离基线存的是字面量 "HEAD":合并没有分支可落,显式拒绝
|
|
482
|
+
if stash_sha:
|
|
483
|
+
out = {"restore_error": ""}
|
|
484
|
+
_restore_stash(wd, stash_sha, out)
|
|
485
|
+
return refuse("任务分支从游离 HEAD 检出,没有可合并的基线分支;请手工合并")
|
|
475
486
|
if cur != origin:
|
|
476
487
|
if stash_sha:
|
|
477
488
|
out = {"restore_error": ""}
|
|
@@ -516,11 +527,56 @@ def merge_task_branch(workdir, task):
|
|
|
516
527
|
return True, "", info_out
|
|
517
528
|
|
|
518
529
|
|
|
530
|
+
def _worktrees_on_branch(workdir, br):
|
|
531
|
+
"""git worktree list --porcelain → 检出 br 的工作树路径列表。
|
|
532
|
+
|
|
533
|
+
porcelain 自 git 2.7 起可用;解析失败返回 None(调用方显式拒绝,
|
|
534
|
+
绝不带着「分支可能仍被占用」硬删)。
|
|
535
|
+
"""
|
|
536
|
+
w = _git(workdir, "worktree", "list", "--porcelain")
|
|
537
|
+
if not w["ok"]:
|
|
538
|
+
return None
|
|
539
|
+
entries, cur = [], None
|
|
540
|
+
for ln in (w["stdout"] or "").splitlines():
|
|
541
|
+
if ln.startswith("worktree "):
|
|
542
|
+
cur = {"path": ln[len("worktree "):].strip(), "branch": ""}
|
|
543
|
+
entries.append(cur)
|
|
544
|
+
elif cur is not None and ln.startswith("branch refs/heads/"):
|
|
545
|
+
cur["branch"] = ln[len("branch refs/heads/"):].strip()
|
|
546
|
+
return [x for x in entries if x["branch"] == br]
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _switch_off_branch(workdir, br, target):
|
|
550
|
+
"""把工作树从 br 上切走,返回错误信息(成功为空串)。
|
|
551
|
+
|
|
552
|
+
先尝试切到 target;两种失败形态都兜底成游离:
|
|
553
|
+
- checkout 直接失败(如目标分支被别的工作树占用)→ --detach target;
|
|
554
|
+
- checkout「成功」却没换位(target 是 HEAD 这类原地引用,实测
|
|
555
|
+
git checkout HEAD 退出码 0 且停在原分支)→ 复查后强制游离。
|
|
556
|
+
最终以 rev-parse 复核 br 真的腾空为准,不信 checkout 的退出码。
|
|
557
|
+
"""
|
|
558
|
+
co = _git(workdir, "checkout", "--quiet", target, timeout=60) if target \
|
|
559
|
+
else _git(workdir, "checkout", "--quiet", "--detach", timeout=60)
|
|
560
|
+
cur = _git(workdir, "rev-parse", "--abbrev-ref", "HEAD")
|
|
561
|
+
if (cur["stdout"] or "").strip() == br:
|
|
562
|
+
co = _git(workdir, "checkout", "--quiet", "--detach", target, timeout=60) \
|
|
563
|
+
if target else _git(workdir, "checkout", "--quiet", "--detach", timeout=60)
|
|
564
|
+
if not co["ok"]:
|
|
565
|
+
return "切离任务分支失败:%s" % (co["stderr"] or "")[-120:]
|
|
566
|
+
elif not co["ok"]:
|
|
567
|
+
return "切回 %s 失败:%s" % (target, (co["stderr"] or "")[-120:])
|
|
568
|
+
return ""
|
|
569
|
+
|
|
570
|
+
|
|
519
571
|
def discard_task_branch(workdir, task):
|
|
520
572
|
"""丢弃任务分支(人审后的「否决」出口,不可恢复)。
|
|
521
573
|
|
|
522
574
|
守卫同 merge(活跃任务/仓库缺失/分支不存在拒绝);若工作区恰好停在
|
|
523
575
|
任务分支上且干净(收尾出错的遗留现场),先切回基线再删分支。
|
|
576
|
+
分支也可能被**其它** worktree 检出(run 收尾被中断、任务分支滞留在
|
|
577
|
+
另一个工作树里)——git 拒绝删除被检出的分支,所以删前逐个清理:
|
|
578
|
+
干净的工作树切回基线(目标分支被占用就游离在那),有未提交改动的
|
|
579
|
+
不动、显式拒绝;目录已消失的残留登记由 worktree prune 清掉。
|
|
524
580
|
返回 (ok, 错误信息)。
|
|
525
581
|
"""
|
|
526
582
|
def refuse(msg):
|
|
@@ -536,19 +592,37 @@ def discard_task_branch(workdir, task):
|
|
|
536
592
|
br = branch_name(tid)
|
|
537
593
|
if not _git(wd, "rev-parse", "--verify", "--quiet", "refs/heads/" + br)["ok"]:
|
|
538
594
|
return refuse("任务分支 %s 不存在(可能已被合并或丢弃)" % br)
|
|
595
|
+
latest = _latest_run_git(tid) or {}
|
|
596
|
+
back = latest.get("from_branch") or ""
|
|
597
|
+
base = latest.get("base_commit") or ""
|
|
598
|
+
target = back if (back and back not in ("(游离 HEAD)", "HEAD") and back != br) else base
|
|
539
599
|
cur = info.get("branch") or ""
|
|
540
600
|
if cur == br:
|
|
541
601
|
if info.get("dirty"):
|
|
542
602
|
return refuse("工作区停在任务分支上且有未提交改动,请先处理再丢弃")
|
|
543
|
-
latest = _latest_run_git(tid) or {}
|
|
544
|
-
back = latest.get("from_branch") or ""
|
|
545
|
-
base = latest.get("base_commit") or ""
|
|
546
|
-
target = back if (back and back != "(游离 HEAD)" and back != br) else base
|
|
547
603
|
if not target:
|
|
548
604
|
return refuse("工作区停在任务分支上且找不到可切回的基线,请手工切走后再丢弃")
|
|
549
|
-
|
|
550
|
-
if
|
|
551
|
-
return refuse(
|
|
605
|
+
err = _switch_off_branch(wd, br, target)
|
|
606
|
+
if err:
|
|
607
|
+
return refuse(err)
|
|
608
|
+
wts = _worktrees_on_branch(wd, br)
|
|
609
|
+
if wts is None:
|
|
610
|
+
return refuse("无法枚举 worktree,请手工处理占用任务分支的工作树后再丢弃")
|
|
611
|
+
for wt in wts:
|
|
612
|
+
p = wt["path"]
|
|
613
|
+
if not os.path.isdir(p):
|
|
614
|
+
continue
|
|
615
|
+
st = _git(p, "status", "--porcelain")
|
|
616
|
+
dirty = [ln for ln in (st["stdout"] or "").splitlines()
|
|
617
|
+
if ln.strip() and not _attach_entry(ln.strip())] if st["ok"] else []
|
|
618
|
+
if dirty:
|
|
619
|
+
return refuse("任务分支仍被工作树 %s 占用且其中有未提交改动,"
|
|
620
|
+
"请先处理后再丢弃" % p)
|
|
621
|
+
err = _switch_off_branch(p, br, target)
|
|
622
|
+
if err:
|
|
623
|
+
return refuse("工作树 %s %s" % (p, err))
|
|
624
|
+
if wts:
|
|
625
|
+
_git(wd, "worktree", "prune")
|
|
552
626
|
d = _git(wd, "branch", "-D", br)
|
|
553
627
|
if not d["ok"]:
|
|
554
628
|
return refuse("删除任务分支失败:%s" % (d["stderr"] or "")[-160:])
|
package/app/core/history.py
CHANGED
|
@@ -18,14 +18,20 @@ def agent_stats(limit=200):
|
|
|
18
18
|
s["wins"] += 1
|
|
19
19
|
|
|
20
20
|
for run in store.list_runs(limit):
|
|
21
|
-
if run.get("kind") != "orchestration"
|
|
21
|
+
if run.get("kind") != "orchestration":
|
|
22
|
+
continue
|
|
23
|
+
# 失败运行也要进历史(记为全负):以前只数 done——常挂的 CLI 永远
|
|
24
|
+
# 「无历史记录」满血参选,2026-09-17 opencode 当日秒挂 3 次仍被换将
|
|
25
|
+
# 选中。cancelled 不算(用户取消非智能体之过)。
|
|
26
|
+
if run.get("status") not in ("done", "failed"):
|
|
22
27
|
continue
|
|
23
28
|
verdict = run.get("verdict") or {}
|
|
24
29
|
task = store.get_task(run.get("task_id")) if run.get("task_id") else None
|
|
25
30
|
ttype = (task or {}).get("type") or verdict.get("type")
|
|
26
31
|
if not ttype:
|
|
27
32
|
continue
|
|
28
|
-
win =
|
|
33
|
+
win = run.get("status") == "done" and bool(
|
|
34
|
+
verdict.get("pass") or verdict.get("publishable"))
|
|
29
35
|
for s in run.get("steps") or []:
|
|
30
36
|
if s.get("role") in ("implement", "draft"):
|
|
31
37
|
bump(s.get("agent"), ttype, win)
|