ygl-trees 0.66.0 → 0.69.0

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.
@@ -17,9 +17,11 @@ class DatabaseService extends cordis_1.Service {
17
17
  // GPT-6.1 SOL fix: the DB lock was only applied in the profile-boot
18
18
  // fallback path, but the base bundle always instantiates this Service —
19
19
  // the primary path never locked. Lock here (the actual entry point for
20
- // every CLI invocation), preserving the YGL_TEST=1 bypass for parallel
21
- // test execution.
22
- this.db = new db_1.DB(dbFile, { lock: process.env.YGL_TEST !== "1" });
20
+ // every CLI invocation), preserving bypasses:
21
+ // YGL_TEST=1 — parallel test execution
22
+ // YGL_SERVE=1 — webui serve (long-lived; WAL permits concurrent access)
23
+ const lock = process.env.YGL_TEST !== "1" && process.env.YGL_SERVE !== "1";
24
+ this.db = new db_1.DB(dbFile, { lock });
23
25
  }
24
26
  }
25
27
  exports.DatabaseService = DatabaseService;
@@ -14,8 +14,14 @@ function parse(argv) {
14
14
  const flags = {};
15
15
  let rest = false;
16
16
  const takesValue = (name) => !BOOLEAN_FLAGS.has(name);
17
- /** `-1` looks like a flag but is a negative number — accept it as a value. */
18
- const isValue = (s) => !s.startsWith("-") || /^-\d+(\.\d+)?$/.test(s);
17
+ /** A token is a flag boundary only if this parser could itself consume it as
18
+ * a flag: `--`, a long `--name`, or a single-letter `-x`. Any other token
19
+ * starting with `-` (e.g. `- 双平台…`, `-1`, `-abc`) can never be parsed as
20
+ * a flag, so a value-taking flag safely claims it — `--quote "- text"`
21
+ * keeps its value instead of collapsing to `true` and leaking the text
22
+ * into positionals. */
23
+ const looksLikeFlag = (s) => s === "--" || s.startsWith("--") || /^-[A-Za-z]$/.test(s);
24
+ const isValue = (s) => !looksLikeFlag(s);
19
25
  for (let i = 0; i < argv.length; i++) {
20
26
  const a = argv[i];
21
27
  if (rest) {
@@ -137,9 +137,16 @@ function run(env, action, rest, args) {
137
137
  });
138
138
  }
139
139
  // ── 7. Stale lock check ──
140
+ // The CLI process running doctor holds its own .ygllock while checking —
141
+ // exclude our own live lock; warn only on foreign/stale holders.
140
142
  const dbPath = db.path;
141
- const lockPath = `${dbPath}.lock`;
142
- const hasLock = require("node:fs").existsSync(lockPath);
143
+ const lockPath = `${dbPath}.ygllock`;
144
+ let lockHolder = "";
145
+ try {
146
+ lockHolder = require("node:fs").readFileSync(`${lockPath}/pid`, "utf8").trim();
147
+ }
148
+ catch { /* absent/unreadable */ }
149
+ const hasLock = require("node:fs").existsSync(lockPath) && lockHolder !== String(process.pid);
143
150
  checks.push({
144
151
  name: "stale_lock",
145
152
  status: hasLock ? "warn" : "pass",
@@ -151,8 +158,8 @@ function run(env, action, rest, args) {
151
158
  // A malformed envelope silently degrades: evalChoice/evalScore fall back to
152
159
  // empty options/levels → the gate can never resolve (always pending).
153
160
  {
154
- const evalConds = db.all(`SELECT c.id, c.name, c.op, c.value, n.key AS node
155
- FROM node_conditions c JOIN nodes n ON n.id=c.node_id
161
+ const evalConds = db.all(`SELECT c.id, c.name, c.op, c.value, n.key AS node
162
+ FROM node_conditions c JOIN nodes n ON n.id=c.node_id
156
163
  WHERE c.op IN ('eval','eval-choice','eval-score')`);
157
164
  const bad = [];
158
165
  for (const c of evalConds) {
@@ -240,9 +247,14 @@ function run(env, action, rest, args) {
240
247
  db.run("UPDATE meta SET value='0' WHERE key='index_dirty'");
241
248
  fixes.push("index dirty flag cleared (run: ygl-trees node index)");
242
249
  }
243
- // stale lock → remove
244
- const lockDir = `${env.db.path}.lock`;
245
- if (fs.existsSync(lockDir)) {
250
+ // stale lock → remove (never our own live lock)
251
+ const lockDir = `${env.db.path}.ygllock`;
252
+ let holder = "";
253
+ try {
254
+ holder = fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
255
+ }
256
+ catch { /* */ }
257
+ if (fs.existsSync(lockDir) && holder !== String(process.pid)) {
246
258
  try {
247
259
  fs.rmSync(lockDir, { recursive: true, force: true });
248
260
  fixes.push("stale lock removed");
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.apply = exports.OpsService = exports.MIRROR_TABLES = exports.migrateToPg = exports.syncToPg = exports.configAll = exports.configReset = exports.configSet = exports.configGetBool = exports.configGetNumber = exports.configGet = exports.CONFIG_REGISTRY = void 0;
3
+ exports.buildTools = exports.serveMcp = exports.apply = exports.OpsService = exports.MIRROR_TABLES = exports.migrateToPg = exports.syncToPg = exports.configAll = exports.configReset = exports.configSet = exports.configGetBool = exports.configGetNumber = exports.configGet = exports.CONFIG_REGISTRY = void 0;
4
4
  /**
5
5
  * @module @ygl/ops-tools
6
6
  */
@@ -20,3 +20,6 @@ Object.defineProperty(exports, "MIRROR_TABLES", { enumerable: true, get: functio
20
20
  var plugin_1 = require("./plugin");
21
21
  Object.defineProperty(exports, "OpsService", { enumerable: true, get: function () { return plugin_1.OpsService; } });
22
22
  Object.defineProperty(exports, "apply", { enumerable: true, get: function () { return plugin_1.apply; } });
23
+ var mcp_1 = require("./mcp");
24
+ Object.defineProperty(exports, "serveMcp", { enumerable: true, get: function () { return mcp_1.serveMcp; } });
25
+ Object.defineProperty(exports, "buildTools", { enumerable: true, get: function () { return mcp_1.buildTools; } });
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildTools = buildTools;
37
+ exports.serveMcp = serveMcp;
38
+ /**
39
+ * MCP (Model Context Protocol) Server — stdio transport.
40
+ *
41
+ * Eight-tool surface designed by the unified three-pillar theory (R8 research):
42
+ * agent_brief · agent_match · kb_search · claim_evaluate
43
+ * tree_state · produce_report · doctor · task_advance
44
+ *
45
+ * Each tool routes to the CLI --json backend, inheriting its frozen contracts
46
+ * (idempotent writes, pure JSON errors, snake_case fields). Agents call tools
47
+ * directly — no command-line parsing layer.
48
+ *
49
+ * Protocol: JSON-RPC 2.0 over stdio (one message per line).
50
+ * → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
51
+ * ← {"jsonrpc":"2.0","id":1,"result":{...}}
52
+ *
53
+ * @module @ygl/ops-tools/mcp
54
+ */
55
+ const readline = __importStar(require("node:readline"));
56
+ /** Build a CLI invocation and capture JSON stdout.
57
+ * Uses process.argv[1] (the running script — bundle or source) to re-invoke
58
+ * the CLI as a child process. This works in both dev (dist/apps/...) and
59
+ * bundle (dist/bundle/bin.cjs) contexts. */
60
+ function cliJson(parts) {
61
+ const { spawn } = require("node:child_process");
62
+ const self = process.argv[1]; // the bin.cjs or bin.js that's running this MCP server
63
+ return new Promise((resolve, reject) => {
64
+ const child = spawn(process.execPath, [self, "--json", ...parts], {
65
+ env: { ...process.env, YGL_SERVE: "1", YGL_MCP_CHILD: "1" },
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ });
68
+ let out = "";
69
+ let err = "";
70
+ child.stdout.on("data", (d) => (out += d));
71
+ child.stderr.on("data", (d) => (err += d));
72
+ child.on("close", (code) => {
73
+ if (out.trim()) {
74
+ try {
75
+ resolve(JSON.parse(out));
76
+ return;
77
+ }
78
+ catch { /* fall through */ }
79
+ }
80
+ reject(new Error(`CLI rc=${code}: ${err.slice(0, 200) || out.slice(0, 200)}`));
81
+ });
82
+ child.on("error", (e) => reject(e));
83
+ });
84
+ }
85
+ function buildTools() {
86
+ return [
87
+ {
88
+ name: "agent_brief",
89
+ description: "Current workspace state: project, progress, counts, next actions, available skills. Always start here.",
90
+ inputSchema: { type: "object", properties: {} },
91
+ execute: () => cliJson(["agent", "brief"]),
92
+ },
93
+ {
94
+ name: "agent_match",
95
+ description: "Discover which skill handles a task. Returns matched skills with relevance scores.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: { query: { type: "string", description: "Task description to match" } },
99
+ required: ["query"],
100
+ },
101
+ execute: (a) => cliJson(["agent", "match", String(a.query)]),
102
+ },
103
+ {
104
+ name: "kb_search",
105
+ description: "Search knowledge notes (CJK full-text). Returns notes with title, body, tags, confidence.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ query: { type: "string", description: "Search query (Chinese supported)" },
110
+ eval: { type: "boolean", description: "Enable decision-model semantic re-ranking" },
111
+ },
112
+ required: ["query"],
113
+ },
114
+ execute: (a) => {
115
+ const parts = ["kb", "search", String(a.query)];
116
+ if (a.eval)
117
+ parts.push("--eval");
118
+ return cliJson(parts);
119
+ },
120
+ },
121
+ {
122
+ name: "claim_evaluate",
123
+ description: "Decision model evaluates a claim's confidence (score 0-5 → grade A/B/C/D).",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: { claim_id: { type: "string", description: "Claim ID to evaluate" } },
127
+ required: ["claim_id"],
128
+ },
129
+ execute: (a) => cliJson(["research", "claim", "evaluate", String(a.claim_id)]),
130
+ },
131
+ {
132
+ name: "tree_state",
133
+ description: "All trees with node states (locked/unlocked). Visualize the dependency graph.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: { tree: { type: "string", description: "Optional tree name (omit for all)" } },
137
+ },
138
+ execute: (a) => cliJson(["node", "list", ...(a.tree ? [String(a.tree)] : [])]),
139
+ },
140
+ {
141
+ name: "produce_report",
142
+ description: "Assemble claims + evidence into a markdown report. Returns report ID and content.",
143
+ inputSchema: { type: "object", properties: {} },
144
+ execute: () => cliJson(["produce", "report"]),
145
+ },
146
+ {
147
+ name: "doctor",
148
+ description: "9-item workspace health check. Pass fix=true to auto-fix safe issues.",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: { fix: { type: "boolean", description: "Auto-fix dirty index + stale locks" } },
152
+ },
153
+ execute: (a) => cliJson(["doctor", ...(a.fix ? ["--fix"] : [])]),
154
+ },
155
+ {
156
+ name: "task_advance",
157
+ description: "Advance a task through the state machine: todo → doing → done (or roll back).",
158
+ inputSchema: {
159
+ type: "object",
160
+ properties: {
161
+ task_id: { type: "string", description: "Task artifact ID" },
162
+ action: { type: "string", enum: ["doing", "done", "todo"], description: "Target status" },
163
+ },
164
+ required: ["task_id", "action"],
165
+ },
166
+ execute: (a) => cliJson(["task", String(a.action), String(a.task_id)]),
167
+ },
168
+ ];
169
+ }
170
+ /** Run the MCP server on stdio. */
171
+ function serveMcp() {
172
+ const tools = buildTools();
173
+ const rl = readline.createInterface({ input: process.stdin });
174
+ const respond = (id, result) => {
175
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
176
+ };
177
+ const respondErr = (id, code, message) => {
178
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
179
+ };
180
+ rl.on("line", (line) => {
181
+ if (!line.trim())
182
+ return;
183
+ let msg;
184
+ try {
185
+ msg = JSON.parse(line);
186
+ }
187
+ catch {
188
+ return;
189
+ }
190
+ const { id, method, params } = msg;
191
+ switch (method) {
192
+ case "initialize":
193
+ respond(id, {
194
+ protocolVersion: "2024-11-05",
195
+ capabilities: { tools: {} },
196
+ serverInfo: { name: "ygl-trees", version: "0.69.0" },
197
+ });
198
+ break;
199
+ case "tools/list":
200
+ respond(id, {
201
+ tools: tools.map((t) => ({
202
+ name: t.name,
203
+ description: t.description,
204
+ inputSchema: t.inputSchema,
205
+ })),
206
+ });
207
+ break;
208
+ case "tools/call": {
209
+ const name = params?.name;
210
+ const tool = tools.find((t) => t.name === name);
211
+ if (!tool) {
212
+ respondErr(id, -32602, `Unknown tool: ${name}`);
213
+ break;
214
+ }
215
+ tool
216
+ .execute(params?.arguments || {})
217
+ .then((data) => respond(id, { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }))
218
+ .catch((e) => respond(id, {
219
+ content: [{ type: "text", text: `Error: ${e?.message || e}` }],
220
+ isError: true,
221
+ }));
222
+ break;
223
+ }
224
+ case "ping":
225
+ respond(id, {});
226
+ break;
227
+ default:
228
+ if (id !== undefined)
229
+ respondErr(id, -32601, `Method not found: ${method}`);
230
+ }
231
+ });
232
+ process.stderr.write("[ygl-trees MCP] stdio server ready — 8 tools\n");
233
+ }
@@ -152,14 +152,22 @@ function pnpm(home, args) {
152
152
  // embedded/offline carriers without touching this module. PATH may carry
153
153
  // broken .bin shims (observed: a parent node_modules shim pointing at a
154
154
  // missing pnpm.cjs under vitest) — resolve by probing, not first-hit.
155
+ //
156
+ // --ignore-workspace: pnpm walks UP from --dir to the nearest
157
+ // pnpm-workspace.yaml. When the profile lives inside a workspace (the unit
158
+ // tests' tmp/ profiles — or any repo-embedded home), the profile registered
159
+ // as a workspace importer and leaked tmp/plugins-unit-* blocks into the
160
+ // repo lockfile (committed that way through 0.66.0). Standalone profiles
161
+ // (the real $YGL_HOME) are outside any workspace, so this flag is a no-op
162
+ // there. Requires pnpm >= 9.4.
155
163
  const pm = process.env.YGL_PACKAGE_MANAGER || "pnpm";
156
164
  if (process.platform === "win32") {
157
165
  const q = (s) => `"${String(s).replace(/["&|<>^]/g, "")}"`;
158
166
  const exe = resolvePm(pm);
159
- (0, node_child_process_1.execFileSync)([q(exe), ...args.map(q), "--dir", q(profileDir(home))].join(" "), { stdio: "pipe", shell: true });
167
+ (0, node_child_process_1.execFileSync)([q(exe), ...args.map(q), "--dir", q(profileDir(home)), "--ignore-workspace"].join(" "), { stdio: "pipe", shell: true });
160
168
  return;
161
169
  }
162
- (0, node_child_process_1.execFileSync)(pm, [...args, "--dir", profileDir(home)], { stdio: "pipe" });
170
+ (0, node_child_process_1.execFileSync)(pm, [...args, "--dir", profileDir(home), "--ignore-workspace"], { stdio: "pipe" });
163
171
  }
164
172
  let cachedPm;
165
173
  function resolvePm(pm) {
@@ -338,6 +338,11 @@ let Claim = class Claim {
338
338
  contentHash;
339
339
  created;
340
340
  updated;
341
+ // The @ManyToOne here was missing (likely lost in an early edit): Claim.project
342
+ // was an undecorated property, so Project.claims' inverseSide never resolved
343
+ // and every ORM find loading the claims relation threw
344
+ // "Cannot read properties of undefined (reading 'joinColumns')" —
345
+ // `project show` / demo have been broken on this since the TypeORM pilot.
341
346
  project;
342
347
  claimEvidence;
343
348
  };
@@ -375,6 +380,7 @@ __decorate([
375
380
  __metadata("design:type", String)
376
381
  ], Claim.prototype, "updated", void 0);
377
382
  __decorate([
383
+ (0, typeorm_1.ManyToOne)(() => Project, (p) => p.claims, { createForeignKeyConstraints: false }),
378
384
  (0, typeorm_1.JoinColumn)({ name: "project_id" }),
379
385
  __metadata("design:type", Project)
380
386
  ], Claim.prototype, "project", void 0);
@@ -344,7 +344,7 @@ async function taskSetStatus(ds, taskId, status) {
344
344
  }
345
345
  catch { /* malformed → reset */ }
346
346
  const from = meta.status || "todo";
347
- await mgr.query(`UPDATE artifacts SET meta=? WHERE id=?`, [JSON.stringify({ ...meta, status }), taskId]);
347
+ await mgr.query(`UPDATE artifacts SET meta=?, updated=? WHERE id=?`, [JSON.stringify({ ...meta, status }), (0, text_1.nowIso)(), taskId]);
348
348
  await logEvent(mgr, "task.status", "task", taskId, `${from} → ${status}`, status === "done" ? "info" : "info");
349
349
  return { id: taskId, title: rows[0].title, from, to: status };
350
350
  });
@@ -134,8 +134,8 @@ async function buildIndex(mgr, treeId) {
134
134
  inDegree[n.key] = (requiredParents.get(n.id) || []).length;
135
135
  for (const n of nodes)
136
136
  await mgr.query(`UPDATE nodes SET tier=? WHERE id=? AND tier<>?`, [tierOf.get(n.id) ?? 0, n.id, tierOf.get(n.id) ?? 0]);
137
- await mgr.query(`INSERT INTO node_index(tree_id,topo_order,tiers,in_degree,built_at,dirty) VALUES(?,?,?,?,?,0)
138
- ON CONFLICT(tree_id) DO UPDATE SET topo_order=excluded.topo_order, tiers=excluded.tiers,
137
+ await mgr.query(`INSERT INTO node_index(tree_id,topo_order,tiers,in_degree,built_at,dirty) VALUES(?,?,?,?,?,0)
138
+ ON CONFLICT(tree_id) DO UPDATE SET topo_order=excluded.topo_order, tiers=excluded.tiers,
139
139
  in_degree=excluded.in_degree, built_at=excluded.built_at, dirty=0`, [treeId, JSON.stringify(topo), JSON.stringify(tiers), JSON.stringify(inDegree), (0, text_1.nowIso)()]);
140
140
  return { topo, tiers, inDegree };
141
141
  }
@@ -222,10 +222,10 @@ async function link(ds, treeId, parentKey, childKey, o = {}) {
222
222
  const before = (await mgr.query(`SELECT id FROM node_edges WHERE parent_id=? AND child_id=? AND rel=?`, [p[0].id, c[0].id, rel]));
223
223
  const duplicate = before.length > 0;
224
224
  const id = duplicate ? before[0].id : (0, text_1.shortId)(["edge", p[0].id, c[0].id, rel], 12);
225
- await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
226
- VALUES(?,?,?,?,?,?,?,?,?,?,?)
227
- ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
228
- mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
225
+ await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
226
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)
227
+ ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
228
+ mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
229
229
  weight=excluded.weight, note=excluded.note`, [id, treeId, p[0].id, c[0].id, rel, mode, o.required === false ? 0 : 1, o.min_level ?? 1, o.weight ?? 1, o.note || "", (0, text_1.nowIso)()]);
230
230
  await rebuildClosure(mgr, treeId);
231
231
  await logEvent(mgr, "node.link", id, { tree: treeId, parent: parentKey, child: childKey, rel, mode, required: o.required !== false, duplicate });
@@ -256,7 +256,7 @@ async function condAdd(ds, treeId, keyOrId, o) {
256
256
  if (!db_1.COND_KINDS.includes(kind))
257
257
  throw new Error(`kind must be one of ${db_1.COND_KINDS.join("|")}`);
258
258
  const id = (0, text_1.shortId)(["cond", n[0].id, o.name], 12);
259
- await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
259
+ await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
260
260
  VALUES(?,?,?,?,?,?,?,?,?,?,?)`, [id, n[0].id, o.name, kind, o.expr || o.name, o.op || ">=", o.value || "", o.required === false ? 0 : 1, o.effect || "gate", o.weight ?? 1, o.note || ""]);
261
261
  await markDirty(mgr, treeId);
262
262
  await logEvent(mgr, "node.cond", id, { tree: treeId, node: keyOrId, name: o.name, kind, op: o.op || ">=", required: o.required !== false });
@@ -269,7 +269,7 @@ async function stateSet(ds, treeId, keyOrId, status, context = "default", level
269
269
  const n = (await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [treeId, keyOrId, keyOrId]));
270
270
  if (!n.length)
271
271
  throw new Error(`node not found: ${keyOrId}`);
272
- await mgr.query(`INSERT INTO node_state(tree_id,context,node_id,status,level,progress,updated) VALUES(?,?,?,?,?,?,?)
272
+ await mgr.query(`INSERT INTO node_state(tree_id,context,node_id,status,level,progress,updated) VALUES(?,?,?,?,?,?,?)
273
273
  ON CONFLICT(tree_id,context,node_id) DO UPDATE SET status=excluded.status, level=excluded.level, updated=excluded.updated`, [treeId, context, n[0].id, status, level, status === "unlocked" ? 1 : 0, (0, text_1.nowIso)()]);
274
274
  // R10.1: unlock/lock are the timeline anchors for R11's time axis
275
275
  await logEvent(mgr, status === "unlocked" ? "node.unlock" : "node.lock", n[0].id, {
@@ -414,20 +414,20 @@ async function importTree(ds, spec, opts = {}) {
414
414
  for (const e of edges) {
415
415
  const pid = nodeId.get(String(e.parent));
416
416
  const cid = nodeId.get(String(e.child));
417
- await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
418
- VALUES(?,?,?,?,?,?,?,?,?,?)
419
- ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
420
- mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
417
+ await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
418
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)
419
+ ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
420
+ mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
421
421
  weight=excluded.weight, note=excluded.note`, [
422
422
  (0, text_1.shortId)(["edge", tid, pid, cid, String(e.rel)], 12), tid, pid, cid,
423
423
  String(e.rel), String(e.mode || "AND"), e.required === false ? 0 : 1,
424
- Number(e.min_level ?? 1), Number(e.weight ?? 1), String(e.note || ""),
424
+ Number(e.min_level ?? 1), Number(e.weight ?? 1), String(e.note || ""), (0, text_1.nowIso)(),
425
425
  ]);
426
426
  }
427
427
  for (const c of conds) {
428
428
  const nid = nodeId.get(String(c.node));
429
429
  await mgr.query(`DELETE FROM node_conditions WHERE node_id=? AND name=?`, [nid, String(c.name)]);
430
- await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
430
+ await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
431
431
  VALUES(?,?,?,?,?,?,?,?,?,?,?)`, [
432
432
  (0, text_1.shortId)(["cond", tid, nid, String(c.name)], 10), nid, String(c.name),
433
433
  String(c.kind || "flag"), String(c.expr || ""), String(c.op || ">="), String(c.value ?? ""),
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # ygl-trees v0.65.0 — AI Agent Research Workbench
1
+ # ygl-trees v0.69.0 — AI Agent Research Workbench
2
2
 
3
3
  > Single SQLite source of truth. Research → Production → Knowledge Reuse + Tech Tree.
4
4
  > CLI for AI agents: every command supports --json (machine-readable, including errors).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.66.0",
3
+ "version": "0.69.0",
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",
@@ -26,8 +26,8 @@
26
26
  "typeorm": "^1.1.2",
27
27
  "@deepseek-ai/cordis": "~4.0.5-alpha.2",
28
28
  "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
29
- "@deepseek-ai/schemastery": "~3.18.5-alpha.2",
30
- "@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
29
+ "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
30
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.0.0",