ygl-trees 0.82.1 → 0.83.1

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.83.1(2026-10-17)job 运行史持久化热修
4
+ - **`jobFinish` 漏传 runId(UPDATE 静默 no-op)**:`UPDATE runs SET finished=?,rc=?,output=? WHERE id=?` 的参数数组只有 3 个值——缺失的占位符绑定为 NULL,WHERE id=NULL 匹配 0 行且不报错。后果:**runs 运行史从未落库**(rc/output/finished 恒空,job logs 看到的 rc 全部来自 reaper 兜底);jobs 侧的 last_run/status 更新正常,掩盖了缺陷。在验证 0.83 到期巡检任务的落库输出时捕获。demo §7 增回归断言(job run 后 runs 表 rc=0 可读回)→ 114
5
+
6
+ ## 0.83.0(2026-10-17)并发防护:指针钉住 + 半写拒绝 + 帮助面
7
+ - **写命令长旗标钉住**(用户上报①,最重:并发会话两次劫持共享 current-project 指针,资产碎片化跨 3 项目):`research *` / `produce *`(含 task)/ `project *` 全部接受 `--project <p>` 长旗标,`node *` 接受 `--tree <t>`(短旗标 `-p`/`-t` 不变;`node import --tree` 仍是目标树名,别名不遮蔽);source/evidence/claim/task 写回执带 `· project <名>` 回显落点——写错项目当场可见,不必事后对账
8
+ - **`source add --query` 半写防护**(上报①:attach 失败仍落源):引用不存在时**先拒绝后写入**(rc=1 点名、零落地),不再"源已落、链接失败";attach 落库后失败的路径改报已落 id + `research query attach` 重试指路
9
+ - **`--help` 任意深度**(上报②:`kb add --help` 不识别直接 rc=2):任意子命令带 `--help` 打印所在组的用法切片 rc=0(`--json` 返回 `{help}`),在 flag 校验与 db boot 之前短路;`webui serve --help` 等原有短路兼容
10
+ - **no-db 报错点名工作区根因**(上报③:`db init` 前报错不提示 YGL_HOME):无库时错误带 `workspace: <path>` 与来源(YGL_HOME 显式 / 默认 ~/.ygl-trees),冷启动错环境一眼可辨
11
+ - **`node can --eval` attachments 快照**(建议④:P 值过了看不出吃了哪些 rel 证据):输出回显本次判定实际吃到的 rel 挂接——`--json` 增 `eval_attachments[]`,人类视图 `attachments ×N:` 逐条(rel ← type:id 摘要),复现不必翻库;无挂接时字段省略(字节兼容)
12
+ - **job 子命令无锁启动**(建议⑤配套):`job run/tick` 的子进程带 `YGL_SERVE=1` 启动——此前 runner 自持应用级写锁,任何 `ygl-trees …` 型任务命令必撞自身死锁(rc=1 "locked");WAL 并发读 + SQLite 层写串行,与 webui serve 同推理。到期巡检范式:`job add "到期巡检" --every 1440 -- ygl-trees node cond --due --days 30`(真实工作区已挂,30 天窗口即刻命中 w1 三门槛)
13
+ - 测试:demo 5k 段 7 断言(--project 钉住 / 落点校验 / 半写拒绝+零落地 / --help rc=0 / attachments 快照)+ 5h `--tree` 别名断言 → 113;readme-verify +3(kb add --help、source add --project、node list --tree 点名)→ 46
14
+
3
15
  ## 0.82.1(2026-10-17)预测生命周期护栏:挂证据 ≠ 仲裁
4
16
  - **`claim link` 不再抢跑预测仲裁**(用户上报③:`--stance supports` 自动把 [预测·…] 断言置 supported,predict audit 判 supported||refuted → resolved——预测刚挂证据就退出跟踪表,实际无人仲裁):预测前缀断言挂证据时跳过自动晋升,状态保持 unverified 直到显式 `claim resolve` / 到期回测;证据链接与 stance 照常落库,回执明示 `auto_promote: false`(`--json` 返回 `{claim, evidence, stance, status, auto_promote}`);普通断言的自动推导不变(回归护栏)
5
17
  - **`claim unresolve <claimId>`**(建议②:误晋升后无归位命令):复位通道把断言重置回 unverified(撤销链接副作用 / 抢跑仲裁),幂等(已 unverified → `duplicate:true`),落 `claim.unresolve` 事件留痕——0.82.1 前被误晋升的预测可用它归队
package/README.md CHANGED
@@ -109,9 +109,12 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
109
109
 
110
110
  **跨树巡检 + 临期门槛(0.82)**:`node ready --all` 一次扫全部树(12 棵树 24 条命令 → 1 条);`node cond --due [--days N]` 列出问题文本带 `YYYY-MM-DD` 期限且 N 天内到期(含逾期)的 eval 条件——w1/d90 预测门槛不再依赖人记得看;`--eval` 逐条送判定模型(离线安全回落 pending),**判定只报告不仲裁**(通过 ≠ 自动解锁,resolve/unlock 保持人工,与 predict audit 同纪律)。
111
111
 
112
+ **并发钉住 + 半写防护(0.83,真实用户报告)**:并发会话共享 `current_project` 指针互踩导致资产碎片化——研究/产出/树三域写命令全部接受长旗标钉住:`research * --project <p>`、`produce/task * --project <p>`、`node * --tree <t>`(短旗标 `-p`/`-t` 不变);写回执带 `· project <名>` 回显落点,写错项目当场可见。`source add --query` 引用不存在时**先拒绝后写入**(rc=1、零落地),不再出现"源已落、链接失败"的半写;`job run/tick` 的子命令以无锁环境启动(WAL 并发读),`ygl-trees …` 型任务命令不再与运行器自身死锁。`--help` 在任意深度(如 `kb add --help`)打组帮助 rc=0;`db init` 前的报错提示 `YGL_HOME` 工作区根因;到期巡检可直接挂 job(`job add "到期巡检" --every 1440 -- ygl-trees node cond --due --days 30`)。
113
+
112
114
  **决策模型评估门(spec R8;0.14 布尔 → 0.26 三题型 → 0.27 混批 → 0.80 织物并入)**:语义型必检条件用 eval 题型声明,`node can <key> --eval` 会把**节点元数据 + 直接父(关联)+ 祖先链(闭包索引)+ 条件组 + 已解锁集 + --state 袋 + rel 挂接摘要**组装为上下文包,交给评估模型(TypeSafe jev,经 Vercel AI Gateway)判定:
113
115
 
114
116
  - **rel 织物并入(0.80)**:节点挂接的 evidence(quote 摘要)/ claim(text+status+confidence)/ kb/source(标题)等 rel 关系(双向,≤12 条,文本截 120 字)自动进上下文包的 `attachments`——「证据挂链」纪律从此对语义门槛可见,不必再 `--state k=v` 手动携带裁决事实(实测 P 0.30 → 0.92 的差距由挂链补平)
117
+ - **attachments 快照(0.83)**:`node can <key> --eval` 的输出(`--json` 的 `eval_attachments[]` + 人类视图 `attachments ×N:`)回显本次判定实际吃到的 rel 挂接——P 值过了也能一眼看出依据是哪几条 evidence/claim,复现不必翻库
115
118
 
116
119
  - **三题型**:`--op eval`(布尔 P(true))/ `--op eval-choice`(多选一,判 **P(期望答案)**,`--options "k=desc,..."` + `--answer k`)/ `--op eval-score`(等级评分 `--levels "级0,..."` 2-10 级 + `--min-score n`,判概率加权均值 ±1 灰带)
117
120
  - P≥0.7 通过 · P≤0.4 未通过 · **灰带 pending**(过程态,禁作终态,单列不阻塞也不放行);score 的灰带是 [min−1, min)
@@ -131,7 +134,8 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
131
134
  ```
132
135
  query add 登记检索式
133
136
  query attach 检索式 ↔ 来源关联(query list 显示召回数)
134
- source add 收来源,按 (project, url_hash) 去重;--query 顺带关联
137
+ source add 收来源,按 (project, url_hash) 去重;--query 顺带关联(引用不存在 → 整体拒绝 rc=1,零落地)
138
+ 全组接受 --project <p> 长旗标钉住目标项目(并发会话不依赖共享 current 指针;回执带 · project 回显)
135
139
  --adjudication "<裁决摘要>":会话内裁决原生载体(url 自动 session://adjudication/<时刻>,摘要兼作标题);--url 收任意 <scheme>://(https/file/session/a2a/… 跨 Agent 引用免落盘)
136
140
  source fetch <url> 直接抓取网页入库:title/meta description/去标签正文;15s 超时、非 html 拒收、失败零副作用,正文过薄自动打 needs_review
137
141
  evidence add 从来源摘原文片段,带 locator(页码/章节);长摘录用 --file <path> 免转义
@@ -296,7 +300,7 @@ ygl-trees agent ask "<question>" natural language interface
296
300
  ## 五b、开发与运维工具
297
301
 
298
302
  ```bash
299
- pnpm test # 四段制回归:unit (vitest 177 例) + build (tsc + tsdown) + demo(106 条断言)+ contract:audit(86 项)
303
+ pnpm test # 四段制回归:unit (vitest 177 例) + build (tsc + tsdown) + demo(114 条断言)+ contract:audit(86 项)
300
304
  pnpm bench # 性能基准七席(startup/写路径/检索/索引),每席含 RSS 内存端点 → tmp/bench-results.json
301
305
  node benchmarks/bench.js --quick # 缩小规模快速跑
302
306
  pnpm pack:check # 发布就绪门禁:打包 → 隔离安装 → 驱动发布产物跑命令面 + --json 纯净性
@@ -42,6 +42,7 @@ exports.loadBundle = loadBundle;
42
42
  exports.repoRoot = repoRoot;
43
43
  exports.appVersion = appVersion;
44
44
  exports.usage = usage;
45
+ exports.groupHelp = groupHelp;
45
46
  exports.main = main;
46
47
  exports.generateLlmsManifest = generateLlmsManifest;
47
48
  /**
@@ -251,6 +252,25 @@ Global: --json --no-color --db <file> --home <dir>
251
252
  function usage() {
252
253
  return USAGE;
253
254
  }
255
+ /** 0.83 (user report ②): `--help` at any depth prints the relevant usage
256
+ * slice — `kb add --help` used to fall into flag validation rc=2. Renders the
257
+ * group's own block (found by its bold header line); unknown groups get the
258
+ * full usage. */
259
+ function groupHelp(group) {
260
+ const pfx = (0, fmt_1.paint)(fmt_1.C.bold, group, true);
261
+ const lines = USAGE.split("\n");
262
+ const start = lines.findIndex((l) => l.startsWith(pfx));
263
+ if (start < 0)
264
+ return USAGE;
265
+ let end = lines.length;
266
+ for (let i = start + 1; i < lines.length; i++) {
267
+ if (lines[i].startsWith("\u001b[1m")) {
268
+ end = i;
269
+ break;
270
+ }
271
+ }
272
+ return lines.slice(0, 4).join("\n") + "\n\n" + lines.slice(start, end).join("\n").trimEnd();
273
+ }
254
274
  // ── Main ──
255
275
  async function main(argv) {
256
276
  const args = (0, types_1.parse)(argv);
@@ -291,6 +311,17 @@ async function main(argv) {
291
311
  (0, fmt_1.out)(ctx, appVersion());
292
312
  return 0;
293
313
  }
314
+ // 0.83 (user report ②): `--help` at ANY depth prints the group's usage
315
+ // slice rc=0 — `kb add --help` used to fall into flag validation rc=2.
316
+ // Fires before dispatch and before db boot: help never touches the db.
317
+ if (args.flags.help === true && group) {
318
+ const text = groupHelp(group);
319
+ if (ctx.json)
320
+ console.log(JSON.stringify({ help: text.replace(/\u001b\[[0-9;]*m/g, "") }, null, 2));
321
+ else
322
+ (0, fmt_1.out)(ctx, text);
323
+ return 0;
324
+ }
294
325
  // llms: structured capability manifest for LLM consumption (Ant Design
295
326
  // "for-agents" pattern — one output gives any Agent the full command surface).
296
327
  // Onboarding: detect first run (no DB) and guide to db init
@@ -364,6 +395,10 @@ async function main(argv) {
364
395
  if (needInit && !require("node:fs").existsSync(file)) {
365
396
  (0, fmt_1.bad)(ctx, `no database at ${file}`);
366
397
  (0, fmt_1.out)(ctx, ` run: ygl-trees db init`);
398
+ // 0.83 (user report ③): name the workspace root cause — a foreign
399
+ // YGL_HOME (or its absence) is invisible otherwise, and the user chases
400
+ // the db instead of the environment.
401
+ (0, fmt_1.out)(ctx, ` workspace: ${home}${process.env.YGL_HOME ? " (from YGL_HOME)" : " (default ~/.ygl-trees — set YGL_HOME to switch)"}`);
367
402
  return 2;
368
403
  }
369
404
  // ── Cordis composition via bootApp (DSH profile-boot exemplar) ──
@@ -3092,7 +3092,8 @@ var Store = class {
3092
3092
  this.db.run("UPDATE runs SET finished=?, rc=?, output=? WHERE id=?", [
3093
3093
  require_text.nowIso(),
3094
3094
  rc,
3095
- output.slice(0, 8e3)
3095
+ output.slice(0, 8e3),
3096
+ runId
3096
3097
  ]);
3097
3098
  this.db.run("UPDATE jobs SET runs=runs+1, last_run=?, status=? WHERE id=(SELECT job_id FROM runs WHERE id=?)", [
3098
3099
  require_text.nowIso(),
@@ -5788,7 +5789,11 @@ function execute(env, job) {
5788
5789
  "pipe",
5789
5790
  "pipe"
5790
5791
  ],
5791
- timeout: 6e5
5792
+ timeout: 6e5,
5793
+ env: {
5794
+ ...process.env,
5795
+ YGL_SERVE: "1"
5796
+ }
5792
5797
  });
5793
5798
  rc = 0;
5794
5799
  } catch (e) {
@@ -6631,7 +6636,7 @@ function run$8(env, action, rest, args) {
6631
6636
  const subAction = rest[0];
6632
6637
  let project;
6633
6638
  try {
6634
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
6639
+ project = store.projectResolve(args.flags.p ?? args.flags.project ? String(args.flags.p ?? args.flags.project) : void 0);
6635
6640
  } catch (e) {
6636
6641
  bad(ctx, e.message);
6637
6642
  return 1;
@@ -6770,12 +6775,25 @@ function run$8(env, action, rest, args) {
6770
6775
  tags: String(args.flags.tags || "")
6771
6776
  };
6772
6777
  const qRef = String(args.flags.query || "");
6778
+ if (qRef) {
6779
+ if (!store.db.get("SELECT id FROM queries WHERE (id=? OR text=?) AND project_id=?", [
6780
+ qRef,
6781
+ qRef,
6782
+ project.id
6783
+ ])) {
6784
+ bad(ctx, `query not found: ${require_text.clip(qRef, 40)} — refusing to write (source add would land without its query link; fix the ref or drop --query)`);
6785
+ return 1;
6786
+ }
6787
+ }
6773
6788
  return getOrm(env.db.path).then(async (ds) => {
6774
6789
  const r = await sourceAdd(ds, project.id, spec);
6775
- writeResult(ctx, r, (row) => `source added: ${row.id} ${require_text.clip(row.title, 60)}`);
6776
- if (qRef) {
6790
+ writeResult(ctx, r, (row) => `source added: ${row.id} ${require_text.clip(row.title, 60)} · project ${project.name}`);
6791
+ if (qRef) try {
6777
6792
  const at = await queryAttach(ds, project.id, qRef, r.row.id);
6778
6793
  if (!ctx.json && !at.dup) out(ctx, paint(C.dim, ` attached to query: "${require_text.clip(at.queryText, 40)}"`, ctx.color));
6794
+ } catch (e) {
6795
+ bad(ctx, `source landed as ${r.row.id} but attach failed: ${e?.message} — research query attach ${require_text.clip(qRef, 20)} ${r.row.id} to retry`);
6796
+ return 1;
6779
6797
  }
6780
6798
  return 0;
6781
6799
  }).catch((e) => {
@@ -6948,7 +6966,7 @@ function run$8(env, action, rest, args) {
6948
6966
  }
6949
6967
  const note = String(args.flags.note || "");
6950
6968
  const locator = String(args.flags.locator || "");
6951
- return runWrite(env, (ds) => evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${require_text.clip(quote, 50)}`);
6969
+ return runWrite(env, (ds) => evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${require_text.clip(quote, 50)} · project ${project.name}`);
6952
6970
  }
6953
6971
  if (action === "evidence" && subAction === "assess") {
6954
6972
  const id = rest[1];
@@ -7044,7 +7062,7 @@ function run$8(env, action, rest, args) {
7044
7062
  return 2;
7045
7063
  }
7046
7064
  const confidence = String(args.flags.confidence || configGet(env.db, "default_confidence"));
7047
- return runWrite(env, (ds) => claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${require_text.clip(text, 60)}`);
7065
+ return runWrite(env, (ds) => claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${require_text.clip(text, 60)} · project ${project.name}`);
7048
7066
  }
7049
7067
  if (subAction === "link") {
7050
7068
  if (!rest[1] || !rest[2]) {
@@ -7425,7 +7443,7 @@ function run$7(env, action, rest, args) {
7425
7443
  ack(ctx, { project: name }, `now using: ${name}`);
7426
7444
  return 0;
7427
7445
  case "show": {
7428
- const resolved = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7446
+ const resolved = store.projectResolve(args.flags.p ?? args.flags.project ? String(args.flags.p ?? args.flags.project) : void 0);
7429
7447
  return getOrm(env.db.path).then(async (ds) => {
7430
7448
  const proj = await ds.getRepository(Project).findOne({
7431
7449
  where: { id: resolved.id },
@@ -7846,7 +7864,7 @@ function run$5(env, action, rest, args) {
7846
7864
  }
7847
7865
  let project;
7848
7866
  try {
7849
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7867
+ project = store.projectResolve(args.flags.p ?? args.flags.project ? String(args.flags.p ?? args.flags.project) : void 0);
7850
7868
  } catch (e) {
7851
7869
  bad(ctx, e.message);
7852
7870
  return 1;
@@ -7958,7 +7976,7 @@ function run$5(env, action, rest, args) {
7958
7976
  return getOrm(env.db.path).then((ds) => taskAdd(ds, pid, title, note)).then((row) => {
7959
7977
  if (!nodeRef) {
7960
7978
  if (ctx.json) console.log(JSON.stringify(row, null, 2));
7961
- else ok(ctx, `task added: ${row.id} ${title}`);
7979
+ else ok(ctx, `task added: ${row.id} ${title} · project ${project.name}`);
7962
7980
  return 0;
7963
7981
  }
7964
7982
  return relAdd(env, `node:${nodeRef}`, `task:${row.id}`, "tracked-by").then((r) => {
@@ -8782,7 +8800,7 @@ require_text.init_text();
8782
8800
  function run$4(env, action, rest, args) {
8783
8801
  const { ctx, db } = env;
8784
8802
  const trees = new TreeStore(db);
8785
- const sel = args.flags.t ? String(args.flags.t) : void 0;
8803
+ const sel = !(action === "import") ? args.flags.t ?? args.flags.tree ? String(args.flags.t ?? args.flags.tree) : void 0 : args.flags.t ? String(args.flags.t) : void 0;
8786
8804
  const flagNum = (k, d) => {
8787
8805
  const v = Number(args.flags[k]);
8788
8806
  return Number.isFinite(v) ? v : d;
@@ -9343,6 +9361,8 @@ function run$4(env, action, rest, args) {
9343
9361
  }
9344
9362
  r.pending = pendingList;
9345
9363
  r.evaluated = evaluated;
9364
+ const atts = contextPkg.attachments;
9365
+ if (Array.isArray(atts) && atts.length) r.eval_attachments = atts;
9346
9366
  r.ok = r.missing_required.length === 0 && r.blocked_by.length === 0 && pendingList.length === 0;
9347
9367
  try {
9348
9368
  const run = process.env.YGL_RUN_ID || "";
@@ -9426,6 +9446,13 @@ function run$4(env, action, rest, args) {
9426
9446
  for (const m of r.missing_optional) out(ctx, ` - ${m}`);
9427
9447
  }
9428
9448
  for (const e of r.evaluated) out(ctx, paint(C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
9449
+ if (Array.isArray(r.eval_attachments) && r.eval_attachments.length) {
9450
+ out(ctx, paint(C.dim, ` attachments ×${r.eval_attachments.length}:`, ctx.color));
9451
+ for (const a of r.eval_attachments) {
9452
+ const label = a.type === "claim" ? `${a.id}(${a.status})` : a.type === "evidence" ? `${a.id} "${require_text.clip(a.quote, 36)}"` : a.type === "kb" || a.type === "source" ? `${a.id} ${require_text.clip(a.title, 30)}` : a.type === "node" ? `${a.key ?? a.id}` : String(a.id);
9453
+ out(ctx, paint(C.dim, ` - ${a.rel} ← ${a.type}:${label}`, ctx.color));
9454
+ }
9455
+ }
9429
9456
  if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
9430
9457
  return 0;
9431
9458
  }
@@ -9717,10 +9744,11 @@ function run$4(env, action, rest, args) {
9717
9744
  if (sub === "list" || sub === void 0) {
9718
9745
  for (const f of Object.keys(args.flags || {})) if (![
9719
9746
  "t",
9747
+ "tree",
9720
9748
  "json",
9721
9749
  "no-color"
9722
9750
  ].includes(f)) {
9723
- bad(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
9751
+ bad(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t|--tree <tree>' (positional <tree> also works)`);
9724
9752
  return 2;
9725
9753
  }
9726
9754
  table(ctx, trees.nodeList(tid), [
@@ -12136,6 +12164,22 @@ Global: --json --no-color --db <file> --home <dir>
12136
12164
  function usage() {
12137
12165
  return USAGE;
12138
12166
  }
12167
+ /** 0.83 (user report ②): `--help` at any depth prints the relevant usage
12168
+ * slice — `kb add --help` used to fall into flag validation rc=2. Renders the
12169
+ * group's own block (found by its bold header line); unknown groups get the
12170
+ * full usage. */
12171
+ function groupHelp(group) {
12172
+ const pfx = paint(C.bold, group, true);
12173
+ const lines = USAGE.split("\n");
12174
+ const start = lines.findIndex((l) => l.startsWith(pfx));
12175
+ if (start < 0) return USAGE;
12176
+ let end = lines.length;
12177
+ for (let i = start + 1; i < lines.length; i++) if (lines[i].startsWith("\x1B[1m")) {
12178
+ end = i;
12179
+ break;
12180
+ }
12181
+ return lines.slice(0, 4).join("\n") + "\n\n" + lines.slice(start, end).join("\n").trimEnd();
12182
+ }
12139
12183
  async function main(argv) {
12140
12184
  const args = parse(argv);
12141
12185
  const ctx = {
@@ -12151,6 +12195,12 @@ async function main(argv) {
12151
12195
  out(ctx, appVersion());
12152
12196
  return 0;
12153
12197
  }
12198
+ if (args.flags.help === true && group) {
12199
+ const text = groupHelp(group);
12200
+ if (ctx.json) console.log(JSON.stringify({ help: text.replace(/\u001b\[[0-9;]*m/g, "") }, null, 2));
12201
+ else out(ctx, text);
12202
+ return 0;
12203
+ }
12154
12204
  if (!group || group === "help") {
12155
12205
  const dbFile = require("node:path").join(process.env.YGL_HOME || require("node:path").join(require("node:os").homedir(), ".ygl-trees"), "ygl-trees.db");
12156
12206
  if (!require("node:fs").existsSync(dbFile)) {
@@ -12211,6 +12261,7 @@ async function main(argv) {
12211
12261
  if (!(group === "db" && action === "init") && group !== "plugin" && !require("node:fs").existsSync(file)) {
12212
12262
  bad(ctx, `no database at ${file}`);
12213
12263
  out(ctx, ` run: ygl-trees db init`);
12264
+ out(ctx, ` workspace: ${home}${process.env.YGL_HOME ? " (from YGL_HOME)" : " (default ~/.ygl-trees — set YGL_HOME to switch)"}`);
12214
12265
  return 2;
12215
12266
  }
12216
12267
  const VERSION = appVersion();
@@ -12664,6 +12715,7 @@ if (require.main === module) main(process.argv.slice(2)).then((rc) => {
12664
12715
  exports.HOME = HOME;
12665
12716
  exports.appVersion = appVersion;
12666
12717
  exports.generateLlmsManifest = generateLlmsManifest;
12718
+ exports.groupHelp = groupHelp;
12667
12719
  exports.loadBundle = loadBundle;
12668
12720
  exports.main = main;
12669
12721
  exports.repoRoot = repoRoot;
@@ -162,7 +162,17 @@ function execute(env, job) {
162
162
  let output = "";
163
163
  const started = Date.now();
164
164
  try {
165
- output = (0, node_child_process_1.execSync)(job.command, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 600_000 });
165
+ // 0.83: the runner process itself holds the app-level write lock; a job
166
+ // command that invokes ygl-trees again would collide with its own parent
167
+ // (self-deadlock — every `ygl-trees …` job failed rc=1 "locked"). Children
168
+ // boot lock-free like webui serve: WAL already permits concurrent readers
169
+ // and serializes writers at the SQLite layer.
170
+ output = (0, node_child_process_1.execSync)(job.command, {
171
+ encoding: "utf8",
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ timeout: 600_000,
174
+ env: { ...process.env, YGL_SERVE: "1" },
175
+ });
166
176
  rc = 0;
167
177
  }
168
178
  catch (e) {
@@ -72,7 +72,7 @@ function run(env, action, rest, args) {
72
72
  // -- project resolution (allow --p override) -----------------------------
73
73
  let project;
74
74
  try {
75
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : undefined);
75
+ project = store.projectResolve((args.flags.p ?? args.flags.project) ? String(args.flags.p ?? args.flags.project) : undefined);
76
76
  }
77
77
  catch (e) {
78
78
  (0, fmt_1.bad)(ctx, e.message);
@@ -222,14 +222,32 @@ function run(env, action, rest, args) {
222
222
  tags: String(args.flags.tags || ""),
223
223
  };
224
224
  const qRef = String(args.flags.query || "");
225
+ // 0.83 (user report ①): half-write guard — `source add --query X` used
226
+ // to commit the source FIRST and attach separately; a bad query ref
227
+ // left the source landed with a pure error receipt (assets fragment
228
+ // silently). Pre-validate the query ref before ANY write; if the
229
+ // attach still fails post-write, name the landed source + the retry.
230
+ if (qRef) {
231
+ const q = store.db.get("SELECT id FROM queries WHERE (id=? OR text=?) AND project_id=?", [qRef, qRef, project.id]);
232
+ if (!q) {
233
+ (0, fmt_1.bad)(ctx, `query not found: ${(0, text_1.clip)(qRef, 40)} — refusing to write (source add would land without its query link; fix the ref or drop --query)`);
234
+ return 1;
235
+ }
236
+ }
225
237
  return (0, orm_1.getOrm)(env.db.path)
226
238
  .then(async (ds) => {
227
239
  const r = await writes.sourceAdd(ds, project.id, spec);
228
- (0, fmt_1.writeResult)(ctx, r, (row) => `source added: ${row.id} ${(0, text_1.clip)(row.title, 60)}`);
240
+ (0, fmt_1.writeResult)(ctx, r, (row) => `source added: ${row.id} ${(0, text_1.clip)(row.title, 60)} · project ${project.name}`);
229
241
  if (qRef) {
230
- const at = await writes.queryAttach(ds, project.id, qRef, r.row.id);
231
- if (!ctx.json && !at.dup)
232
- (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` attached to query: "${(0, text_1.clip)(at.queryText, 40)}"`, ctx.color));
242
+ try {
243
+ const at = await writes.queryAttach(ds, project.id, qRef, r.row.id);
244
+ if (!ctx.json && !at.dup)
245
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` attached to query: "${(0, text_1.clip)(at.queryText, 40)}"`, ctx.color));
246
+ }
247
+ catch (e) {
248
+ (0, fmt_1.bad)(ctx, `source landed as ${r.row.id} but attach failed: ${e?.message} — research query attach ${(0, text_1.clip)(qRef, 20)} ${r.row.id} to retry`);
249
+ return 1;
250
+ }
233
251
  }
234
252
  return 0;
235
253
  })
@@ -411,7 +429,7 @@ function run(env, action, rest, args) {
411
429
  }
412
430
  const note = String(args.flags.note || "");
413
431
  const locator = String(args.flags.locator || "");
414
- return runWrite(env, (ds) => writes.evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${(0, text_1.clip)(quote, 50)}`);
432
+ return runWrite(env, (ds) => writes.evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${(0, text_1.clip)(quote, 50)} · project ${project.name}`);
415
433
  }
416
434
  // R15.3: evidence assess — decision model evaluates evidence quality
417
435
  if (action === "evidence" && subAction === "assess") {
@@ -504,7 +522,7 @@ function run(env, action, rest, args) {
504
522
  return 2;
505
523
  }
506
524
  const confidence = String(args.flags.confidence || (0, config_1.configGet)(env.db, "default_confidence"));
507
- return runWrite(env, (ds) => writes.claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${(0, text_1.clip)(text, 60)}`);
525
+ return runWrite(env, (ds) => writes.claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${(0, text_1.clip)(text, 60)} · project ${project.name}`);
508
526
  }
509
527
  if (subAction === "link") {
510
528
  if (!rest[1] || !rest[2]) {
@@ -297,7 +297,11 @@ class Store {
297
297
  return id;
298
298
  }
299
299
  jobFinish(runId, rc, output) {
300
- this.db.run("UPDATE runs SET finished=?, rc=?, output=? WHERE id=?", [(0, text_1.nowIso)(), rc, output.slice(0, 8000)]);
300
+ // 0.83.1: the UPDATE params were missing runId (3 values for 4
301
+ // placeholders → WHERE id=NULL → silent no-op). Run history (rc/output/
302
+ // finished) never persisted — only the jobs-row side updated. Caught
303
+ // while verifying the 0.83 patrol job's captured output.
304
+ this.db.run("UPDATE runs SET finished=?, rc=?, output=? WHERE id=?", [(0, text_1.nowIso)(), rc, output.slice(0, 8000), runId]);
301
305
  this.db.run("UPDATE jobs SET runs=runs+1, last_run=?, status=? WHERE id=(SELECT job_id FROM runs WHERE id=?)", [
302
306
  (0, text_1.nowIso)(),
303
307
  rc === 0 ? "ok" : "failed",
@@ -60,7 +60,7 @@ function run(env, action, rest, args) {
60
60
  }
61
61
  let project;
62
62
  try {
63
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : undefined);
63
+ project = store.projectResolve((args.flags.p ?? args.flags.project) ? String(args.flags.p ?? args.flags.project) : undefined);
64
64
  }
65
65
  catch (e) {
66
66
  (0, fmt_1.bad)(ctx, e.message);
@@ -204,7 +204,7 @@ function run(env, action, rest, args) {
204
204
  if (ctx.json)
205
205
  console.log(JSON.stringify(row, null, 2));
206
206
  else
207
- (0, fmt_1.ok)(ctx, `task added: ${row.id} ${title}`);
207
+ (0, fmt_1.ok)(ctx, `task added: ${row.id} ${title} · project ${project.name}`);
208
208
  return 0;
209
209
  }
210
210
  // 0.81 ⑦: tasks join the relation fabric — `task add --node` links
@@ -115,7 +115,7 @@ function run(env, action, rest, args) {
115
115
  return 0;
116
116
  }
117
117
  case "show": {
118
- const resolved = store.projectResolve(args.flags.p ? String(args.flags.p) : undefined);
118
+ const resolved = store.projectResolve((args.flags.p ?? args.flags.project) ? String(args.flags.p ?? args.flags.project) : undefined);
119
119
  return (0, orm_1.getOrm)(env.db.path)
120
120
  .then(async (ds) => {
121
121
  const proj = await ds.getRepository(entities_1.Project).findOne({
@@ -61,7 +61,14 @@ const text_1 = require("../../../core/text/src/text");
61
61
  function run(env, action, rest, args) {
62
62
  const { ctx, db } = env;
63
63
  const trees = new reads_1.TreeStore(db);
64
- const sel = args.flags.t ? String(args.flags.t) : undefined;
64
+ // 0.83 (user report ①): --tree long alias alongside -t — pointer-hijack
65
+ // pinning needs an ergonomic, self-documenting form. EXCEPTION: `node
66
+ // import --tree <name>` is that command's own target-tree flag (create or
67
+ // merge destination), never a selector — alias must not shadow it.
68
+ const treeFlagAliasOk = !(action === "import");
69
+ const sel = treeFlagAliasOk
70
+ ? ((args.flags.t ?? args.flags.tree) ? String(args.flags.t ?? args.flags.tree) : undefined)
71
+ : (args.flags.t ? String(args.flags.t) : undefined);
65
72
  const flagNum = (k, d) => {
66
73
  const v = Number(args.flags[k]);
67
74
  return Number.isFinite(v) ? v : d;
@@ -548,6 +555,12 @@ function run(env, action, rest, args) {
548
555
  }
549
556
  r.pending = pendingList;
550
557
  r.evaluated = evaluated;
558
+ // 0.83 (user report ④): snapshot the rel attachments the eval actually
559
+ // consumed — otherwise a passing P value can't be reproduced without
560
+ // digging the db. Byte-compat: field only when attachments exist.
561
+ const atts = contextPkg.attachments;
562
+ if (Array.isArray(atts) && atts.length)
563
+ r.eval_attachments = atts;
551
564
  r.ok = r.missing_required.length === 0 && r.blocked_by.length === 0 && pendingList.length === 0;
552
565
  try {
553
566
  const run = process.env.YGL_RUN_ID || "";
@@ -623,6 +636,18 @@ function run(env, action, rest, args) {
623
636
  }
624
637
  for (const e of r.evaluated)
625
638
  (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
639
+ // 0.83 ④: what the model actually ate this run (rel fabric snapshot)
640
+ if (Array.isArray(r.eval_attachments) && r.eval_attachments.length) {
641
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` attachments ×${r.eval_attachments.length}:`, ctx.color));
642
+ for (const a of r.eval_attachments) {
643
+ const label = a.type === "claim" ? `${a.id}(${a.status})`
644
+ : a.type === "evidence" ? `${a.id} "${(0, text_1.clip)(a.quote, 36)}"`
645
+ : a.type === "kb" || a.type === "source" ? `${a.id} ${(0, text_1.clip)(a.title, 30)}`
646
+ : a.type === "node" ? `${a.key ?? a.id}`
647
+ : String(a.id);
648
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` - ${a.rel} ← ${a.type}:${label}`, ctx.color));
649
+ }
650
+ }
626
651
  if (r.cost_multiplier !== 1)
627
652
  (0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
628
653
  return 0;
@@ -819,11 +844,11 @@ function run(env, action, rest, args) {
819
844
  if (sub === "list" || sub === undefined) {
820
845
  // Contract ⑤ extension (user report: `node list --tree X` silently
821
846
  // swallowed the unknown flag and listed the CURRENT tree). node list
822
- // takes exactly -t <tree> / --json — any other flag is a typo worth
823
- // an explicit rc=2 instead of silent misdirection.
847
+ // takes exactly -t/--tree <tree> / --json — any other flag is a typo
848
+ // worth an explicit rc=2 instead of silent misdirection.
824
849
  for (const f of Object.keys(args.flags || {})) {
825
- if (!["t", "json", "no-color"].includes(f)) {
826
- (0, fmt_1.bad)(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
850
+ if (!["t", "tree", "json", "no-color"].includes(f)) {
851
+ (0, fmt_1.bad)(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t|--tree <tree>' (positional <tree> also works)`);
827
852
  return 2;
828
853
  }
829
854
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.82.1",
3
+ "version": "0.83.1",
4
4
  "description": "猿光 · 科技树 — database-centric CLI for research → production → knowledge reuse, built for AI agents.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/apps/cli/index.js",
@@ -25,10 +25,10 @@
25
25
  "node-sqlite3-wasm": "^0.8.61",
26
26
  "reflect-metadata": "^0.2.2",
27
27
  "typeorm": "^1.1.2",
28
- "@deepseek-ai/cordis": "~4.0.5-alpha.2",
29
28
  "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
30
- "@deepseek-ai/schemastery": "~3.18.5-alpha.2",
31
- "@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
29
+ "@deepseek-ai/cordis": "~4.0.5-alpha.2",
30
+ "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
31
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",
@@ -54,7 +54,8 @@
54
54
  ],
55
55
  "env": {
56
56
  "YGL_TEST": "1"
57
- }
57
+ },
58
+ "hookTimeout": 30000
58
59
  },
59
60
  "scripts": {
60
61
  "build": "tsc -p tsconfig.json && pnpm build:bundle:only && pnpm --filter @ygl/web build",
@@ -63,7 +64,7 @@
63
64
  "start": "node dist/bundle/bin.cjs",
64
65
  "demo": "bash examples/demo.sh",
65
66
  "test": "pnpm test:unit && pnpm build && bash examples/demo.sh && bash scripts/contract-audit.sh",
66
- "test:unit": "vitest run",
67
+ "test:unit": "vitest run --hook-timeout=30000",
67
68
  "smoke:bun": "bash scripts/bun-smoke.sh",
68
69
  "bench": "pnpm build && node benchmarks/bench.js",
69
70
  "pack:check": "bash scripts/pack-check.sh",
@@ -66,6 +66,8 @@ ygl-trees node can gate-node --eval --json # evaluated[] 带 type 字段 + p
66
66
  claim(text+status+confidence)、evidence(quote 截 120 字)、kb/source(标题)、node(key+name)、
67
67
  其余类型 id-only。裁决链 `decided-by` 挂好后 eval 模型直接可见,不必 `--state k=v` 手动携带事实
68
68
  (用户实测:不挂链 P=0.30,挂链后 0.92)。
69
+ **快照回显(0.83)**:`node can <key> --eval` 的输出把本次吃到的 attachments 打回来——`--json` 的
70
+ `eval_attachments[]` + 人类视图 `attachments ×N:` 逐条;P 值依据可复现,不必翻库。
69
71
 
70
72
  ## 5. 凭证与端点(优先级:config > 环境变量/文件 > 默认网关)
71
73
 
@@ -67,12 +67,16 @@ ygl-trees webui serve --help # 只打印用法不启服务(0.79.1 短路)
67
67
 
68
68
  PG 迁移预检:`db migrate pg` 前自动逐表扫描 INTEGER 列类型——非整数值会**点名 table.column rowid=N** 而非迁移时才爆。
69
69
 
70
- ## 2b2. 多会话安全(0.79)
70
+ ## 2b2. 多会话安全(0.79 → 0.83)
71
71
 
72
+ - **0.83 并发钉住(最重要)**:并发会话共享 current 指针互踩——写命令直接钉住目标:`research * --project <p>`、`produce/task * --project <p>`、`node * --tree <t>`(`node import --tree` 是目标树名,非选择器);写回执带 `· project <名>` 回显落点
72
73
  - `tree use` / `project use` 落 `tree.use` / `project.use` 事件(`YGL_SESSION` 会话标识)
73
74
  - `agent brief` 的 `selection_changed`:最近 10 分钟选择变更;跨会话变更置顶提示带 `-t`/`-p`
74
75
  - `kb add` 回执自检(`verified:true`)+ 工作区回显(防写错 home 的"丢笔记"事故)
75
76
  - `research claim link <cid> <sid> --by-source`:来源下全部证据一次挂齐;误传来源 id 会列出其证据 id
77
+ - `research source add --query` 引用不存在 → 整体拒绝 rc=1 零落地(0.83 半写防护)
78
+ - `--help` 任意深度打组帮助 rc=0(如 `kb add --help`);job 子命令无锁启动(`ygl-trees …` 型任务命令不再与 runner 死锁)
79
+ - 到期巡检范式:`job add "到期巡检" --every 1440 -- ygl-trees node cond --due --days 30`
76
80
 
77
81
  ## 2c. DB 锁行为(0.68 → 0.79.1)
78
82
 
@@ -34,6 +34,7 @@ ygl-trees research source add --adjudication "会话内裁决:三条件已逐
34
34
 
35
35
  来源按 `(project, url_hash)` 去重;`--query` 顺带建立召回关联(query list 显示召回数)。
36
36
  `--url` 收任意 `<scheme>://`(https/file/session/a2a/…,0.81)——跨 Agent 引用免落盘。
37
+ **并发钉住(0.83)**:全组接受 `--project <p>` 长旗标(与 `-p` 等价)——并发会话共享 current 指针互踩时直接钉住目标项目,回执带 `· project <名>` 回显落点;`--query` 引用不存在 → 整体拒绝 rc=1 零落地(半写防护)。
37
38
 
38
39
  ## 2. 摘录与断言
39
40
 
@@ -54,6 +54,8 @@ ygl-trees node unlock mining && ygl-trees node lock mining
54
54
  - `--state-file` 两种形状:扁平 `{"y_imported":40}` 全节点共享;按节点 `{"a":{"y_imported":40}}` 各取各袋;多键 `--json` 返回数组(0.79.1)
55
55
  - `ready --all` / `cond --due` 前置于树解析:无当前树也能跑,跨树巡检一条命令(0.82)
56
56
  - `cond --due` 提取 eval 问题文本中的 `YYYY-MM-DD`;默认 7 天窗口(含逾期),`--eval` 判定后仍需人工 resolve/unlock——判定 ≠ 仲裁(0.82)
57
+ - **`--tree <t>` 长旗标(0.83)**:node 组树选择别名(与 `-t` 等价)——并发会话互踩 current_tree 时钉住目标;例外:`node import --tree <name>` 是导入目标树名,不是选择器
58
+ - **`can --eval` attachments 快照(0.83)**:`--json` 增 `eval_attachments[]`、人类视图 `attachments ×N:`——本次判定吃了哪些 rel 证据一目了然,P 值复现不必翻库
57
59
 
58
60
  ## 3. 迁移与保真
59
61