@minonl/dia 0.0.15 → 0.0.17

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/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ var __esm = (fn, res, err) => () => {
11
11
  };
12
12
 
13
13
  // src/paths.ts
14
+ import { existsSync as existsSync3 } from "node:fs";
14
15
  import { homedir as homedir2 } from "node:os";
15
16
  import { dirname as dirname2, join as join3 } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
@@ -20,26 +21,31 @@ function diaHome() {
20
21
  }
21
22
  function installDir() {
22
23
  const env = process.env.DIA_INSTALL_DIR || process.env.DIA_HOME_INSTALL;
23
- if (env)
24
- return env;
25
- try {
26
- return dirname2(dirname2(fileURLToPath(import.meta.url)));
27
- } catch {
28
- return diaHome();
24
+ if (env) {
25
+ if (existsSync3(env))
26
+ return env;
27
+ process.stderr.write(`! warn: DIA_INSTALL_DIR=${env} 不存在,忽略该覆盖并自动探测安装目录
28
+ `);
29
29
  }
30
+ try {
31
+ const derived = dirname2(dirname2(fileURLToPath(import.meta.url)));
32
+ if (existsSync3(derived))
33
+ return derived;
34
+ } catch {}
35
+ return diaHome();
30
36
  }
31
37
  var personasDir = () => join3(diaHome(), "personas"), msgRoot = () => join3(diaHome(), "msg"), agentsRoot = () => join3(diaHome(), "agents"), stateDir = () => process.env.DIA_STATE_DIR || join3(diaHome(), "state"), cacheRoot = () => join3(diaHome(), "cache"), jobRoot = () => join3(diaHome(), "job"), ingestDir = () => join3(diaHome(), "ingest"), srcCacheDir = () => process.env.DIA_CACHE_DIR || join3(cacheRoot(), "src");
32
38
  var init_paths = () => {};
33
39
 
34
40
  // src/runtime.ts
35
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
41
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
36
42
  import { join as join5 } from "node:path";
37
43
  function callsFile() {
38
44
  return join5(DATA2(), "runtime", "calls.jsonl");
39
45
  }
40
46
  function allCalls() {
41
47
  const f = callsFile();
42
- if (!existsSync4(f))
48
+ if (!existsSync5(f))
43
49
  return [];
44
50
  const out = [];
45
51
  for (const line of readFileSync3(f, "utf8").split(`
@@ -69,10 +75,10 @@ var init_runtime = __esm(() => {
69
75
  });
70
76
 
71
77
  // src/freeze.ts
72
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, statSync as statSync3 } from "node:fs";
78
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, statSync as statSync3 } from "node:fs";
73
79
  import { join as join6 } from "node:path";
74
80
  function status() {
75
- if (!existsSync5(FILE))
81
+ if (!existsSync6(FILE))
76
82
  return { frozen: false };
77
83
  try {
78
84
  const s = JSON.parse(readFileSync4(FILE, "utf8"));
@@ -139,7 +145,7 @@ function thaw(opts) {
139
145
  }
140
146
  function snapshot() {
141
147
  const p = join6(STATE, "freeze-snapshot.json");
142
- if (!existsSync5(p))
148
+ if (!existsSync6(p))
143
149
  return null;
144
150
  return JSON.parse(readFileSync4(p, "utf8"));
145
151
  }
@@ -155,7 +161,7 @@ var init_freeze = __esm(() => {
155
161
  import {
156
162
  appendFileSync as appendFileSync2,
157
163
  chmodSync as chmodSync2,
158
- existsSync as existsSync6,
164
+ existsSync as existsSync7,
159
165
  mkdirSync as mkdirSync4,
160
166
  readFileSync as readFileSync5,
161
167
  realpathSync,
@@ -217,7 +223,7 @@ function checkpointTarget() {
217
223
  };
218
224
  }
219
225
  try {
220
- if (existsSync6(file) && statSync4(file).isDirectory()) {
226
+ if (existsSync7(file) && statSync4(file).isDirectory()) {
221
227
  return { ok: false, path: file, error: `${CHECKPOINT_ENV} points at a directory (${file}); expected a file path` };
222
228
  }
223
229
  } catch {}
@@ -229,7 +235,7 @@ function readCheckpoint(target = checkpointTarget()) {
229
235
  if (!target.ok)
230
236
  return { state: "misconfigured", path: target.path, error: target.error };
231
237
  const file = target.path;
232
- if (!existsSync6(file))
238
+ if (!existsSync7(file))
233
239
  return { state: "missing", path: file };
234
240
  let parsed;
235
241
  try {
@@ -401,7 +407,7 @@ function holderAlive(dir) {
401
407
  }
402
408
  function readHead() {
403
409
  const file = headFile();
404
- if (!existsSync6(file))
410
+ if (!existsSync7(file))
405
411
  return { state: "missing" };
406
412
  let parsed;
407
413
  try {
@@ -498,8 +504,8 @@ function append(ev) {
498
504
  full = { ...base, prev_hash };
499
505
  full.hash = hashEvent(full, prev_hash);
500
506
  const before = {
501
- size: existsSync6(file) ? statSync4(file).size : 0,
502
- head: existsSync6(headFile()) ? readFileSync5(headFile()) : null
507
+ size: existsSync7(file) ? statSync4(file).size : 0,
508
+ head: existsSync7(headFile()) ? readFileSync5(headFile()) : null
503
509
  };
504
510
  const anchored = { last_seq: last.seq ?? 0, last_hash: last.hash ?? GENESIS, row_count: last.count };
505
511
  const cpId = checkpoint_id || randomUUID2();
@@ -568,7 +574,7 @@ function append(ev) {
568
574
  }
569
575
  function rawLines() {
570
576
  const file = auditFile();
571
- if (!existsSync6(file))
577
+ if (!existsSync7(file))
572
578
  return [];
573
579
  return readFileSync5(file, "utf8").split(`
574
580
  `).filter((l) => l.trim());
@@ -1288,7 +1294,7 @@ function getDoc(root, rel, section) {
1288
1294
  // src/ingest.ts
1289
1295
  import { execFileSync } from "node:child_process";
1290
1296
  import { createHash as createHash2 } from "node:crypto";
1291
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "node:fs";
1297
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "node:fs";
1292
1298
  import { homedir as homedir3 } from "node:os";
1293
1299
  import { basename as basename2, dirname as dirname3, join as join4 } from "node:path";
1294
1300
  import { zstdDecompressSync } from "node:zlib";
@@ -1480,7 +1486,7 @@ async function readOpencode(dbPath, domain, project) {
1480
1486
  }
1481
1487
  function loadState(cacheDir) {
1482
1488
  const p = join4(cacheDir || CACHE, "state.json");
1483
- if (!existsSync3(p))
1489
+ if (!existsSync4(p))
1484
1490
  return {};
1485
1491
  try {
1486
1492
  return JSON.parse(readFileSync2(p, "utf8"));
@@ -1490,7 +1496,7 @@ function loadState(cacheDir) {
1490
1496
  }
1491
1497
  function seenSet(file) {
1492
1498
  const set = new Set;
1493
- if (!existsSync3(file))
1499
+ if (!existsSync4(file))
1494
1500
  return set;
1495
1501
  for (const line of readFileSync2(file, "utf8").split(`
1496
1502
  `)) {
@@ -1518,7 +1524,7 @@ async function ingest(opts = {}) {
1518
1524
  let episodes = [];
1519
1525
  if (sources.includes("dsh")) {
1520
1526
  const root = opts.sessionsRoot || join4(homedir3(), ".dsh", "sessions");
1521
- if (existsSync3(root)) {
1527
+ if (existsSync4(root)) {
1522
1528
  const cutoff = opts.full ? 0 : sinceMs ?? Number(state.dsh?.lastMtime || 0);
1523
1529
  for (const file of walkDsh(root)) {
1524
1530
  const mtime = Math.floor(statSync2(file).mtimeMs);
@@ -1536,7 +1542,7 @@ async function ingest(opts = {}) {
1536
1542
  }
1537
1543
  if (sources.includes("opencode")) {
1538
1544
  const dbPath = opts.opencodeDb || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
1539
- if (existsSync3(dbPath)) {
1545
+ if (existsSync4(dbPath)) {
1540
1546
  const cutoff = opts.full ? 0 : sinceMs ?? Number(state.opencode?.lastUpdated || 0);
1541
1547
  const all = await readOpencode(dbPath, domain, opts.project);
1542
1548
  result.scanned += all.length;
@@ -1699,7 +1705,7 @@ function resumeCard(session) {
1699
1705
 
1700
1706
  // src/persona.ts
1701
1707
  import { createHash as createHash7 } from "node:crypto";
1702
- import { closeSync, cpSync as cpSync2, existsSync as existsSync11, lstatSync as lstatSync2, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync10, readdirSync as readdirSync6, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync4, statSync as statSync8, symlinkSync as symlinkSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
1708
+ import { closeSync as closeSync2, cpSync as cpSync2, existsSync as existsSync12, lstatSync as lstatSync2, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync10, readdirSync as readdirSync6, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync8, symlinkSync as symlinkSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
1703
1709
  import { homedir as homedir8 } from "node:os";
1704
1710
  import { basename as basename6, dirname as dirname8, join as join13, relative as relative4, resolve as resolve7, sep as sep5 } from "node:path";
1705
1711
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -1707,14 +1713,17 @@ import { execFileSync as execFileSync3 } from "node:child_process";
1707
1713
  // src/kit.ts
1708
1714
  import { createHash as createHash6 } from "node:crypto";
1709
1715
  import {
1716
+ closeSync,
1710
1717
  cpSync,
1711
- existsSync as existsSync8,
1718
+ existsSync as existsSync9,
1712
1719
  lstatSync,
1713
1720
  mkdirSync as mkdirSync7,
1721
+ openSync,
1714
1722
  readFileSync as readFileSync7,
1715
1723
  readdirSync as readdirSync3,
1716
1724
  readlinkSync,
1717
1725
  realpathSync as realpathSync2,
1726
+ renameSync as renameSync3,
1718
1727
  rmSync as rmSync3,
1719
1728
  statSync as statSync5,
1720
1729
  symlinkSync,
@@ -1728,7 +1737,7 @@ import { execFileSync as execFileSync2 } from "node:child_process";
1728
1737
  // src/config.ts
1729
1738
  init_paths();
1730
1739
  import { createHash as createHash5 } from "node:crypto";
1731
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
1740
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
1732
1741
  import { homedir as homedir4 } from "node:os";
1733
1742
  import { dirname as dirname5, join as join9 } from "node:path";
1734
1743
  var home = () => process.env.DIA_HOME || homedir4();
@@ -1811,7 +1820,7 @@ function apply(entries, opts = {}) {
1811
1820
  const applied = [];
1812
1821
  for (const e of entries) {
1813
1822
  const path = expand(e.file);
1814
- const exists = existsSync7(path);
1823
+ const exists = existsSync8(path);
1815
1824
  const before = exists ? readFileSync6(path, "utf8") : "";
1816
1825
  let doc = {};
1817
1826
  if (exists && before.trim()) {
@@ -1846,7 +1855,7 @@ function apply(entries, opts = {}) {
1846
1855
  function revert(records) {
1847
1856
  const restored = [], skipped = [];
1848
1857
  for (const r of records) {
1849
- if (!existsSync7(r.path)) {
1858
+ if (!existsSync8(r.path)) {
1850
1859
  skipped.push(r.path);
1851
1860
  continue;
1852
1861
  }
@@ -1891,13 +1900,64 @@ function assertInsideHome(p) {
1891
1900
  function assertInside(base, p) {
1892
1901
  const abs = resolve5(base, p);
1893
1902
  const rootReal = realpathSync2(base);
1894
- if (!existsSync8(abs))
1903
+ if (!existsSync9(abs))
1895
1904
  throw new Error(`E_MANIFEST: source missing: ${p}`);
1896
1905
  const real = realpathSync2(abs);
1897
1906
  if (real !== rootReal && !real.startsWith(rootReal + sep4))
1898
1907
  throw new Error(`E_MANIFEST: source escapes kit: ${p}`);
1899
1908
  return abs;
1900
1909
  }
1910
+ var EMPTY_TREE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
1911
+ function cmpVersion(a, b) {
1912
+ const pa = a.split(/[.+-]/), pb = b.split(/[.+-]/);
1913
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
1914
+ const x = pa[i] ?? "0", y = pb[i] ?? "0";
1915
+ const nx = Number(x), ny = Number(y);
1916
+ const both = !Number.isNaN(nx) && !Number.isNaN(ny);
1917
+ if (both ? nx !== ny : x !== y)
1918
+ return both ? nx - ny : x < y ? -1 : 1;
1919
+ }
1920
+ return 0;
1921
+ }
1922
+ function versionDirs(dir) {
1923
+ return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort(cmpVersion);
1924
+ }
1925
+ function sleepSync2(ms) {
1926
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1927
+ }
1928
+ function withInstallLock(lockFile, fn) {
1929
+ const budget = Number(process.env.DIA_KIT_LOCK_MS || 5000);
1930
+ const start = Date.now();
1931
+ mkdirSync7(dirname6(lockFile), { recursive: true });
1932
+ for (;; ) {
1933
+ try {
1934
+ const fd = openSync(lockFile, "wx");
1935
+ writeFileSync5(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
1936
+ closeSync(fd);
1937
+ break;
1938
+ } catch (e) {
1939
+ if (e?.code !== "EEXIST")
1940
+ throw e;
1941
+ try {
1942
+ const st = statSync5(lockFile);
1943
+ if (Date.now() - st.mtimeMs > 120000) {
1944
+ rmSync3(lockFile, { force: true });
1945
+ continue;
1946
+ }
1947
+ } catch {
1948
+ continue;
1949
+ }
1950
+ if (Date.now() - start > budget)
1951
+ throw new Error(`E_KIT_BUSY: 另一个 dia 进程正在安装 kit(锁 ${lockFile};可用 DIA_KIT_LOCK_MS 调整等待)`);
1952
+ sleepSync2(50);
1953
+ }
1954
+ }
1955
+ try {
1956
+ return fn();
1957
+ } finally {
1958
+ rmSync3(lockFile, { force: true });
1959
+ }
1960
+ }
1901
1961
  var TREE_SKIP = new Set([".git", "node_modules", ".data"]);
1902
1962
  function copyFilter(base) {
1903
1963
  return (p) => !relative3(base, p).split(sep4).some((s) => TREE_SKIP.has(s));
@@ -1927,7 +1987,7 @@ function hashTree(root) {
1927
1987
  }
1928
1988
  function readKitManifest(dir) {
1929
1989
  const p = join10(dir, "dia.json");
1930
- if (!existsSync8(p))
1990
+ if (!existsSync9(p))
1931
1991
  throw new Error("E_MANIFEST: dia.json not found");
1932
1992
  let m;
1933
1993
  try {
@@ -1951,7 +2011,7 @@ function resolveSource(from, ref) {
1951
2011
  const key = createHash6("sha1").update(url).digest("hex").slice(0, 12);
1952
2012
  const dir = join10(cacheDir2(), key);
1953
2013
  const git = (args) => execFileSync2("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
1954
- if (!existsSync8(dir)) {
2014
+ if (!existsSync9(dir)) {
1955
2015
  mkdirSync7(cacheDir2(), { recursive: true });
1956
2016
  try {
1957
2017
  execFileSync2("git", ["clone", "--quiet", url, dir], { stdio: "pipe" });
@@ -1976,7 +2036,7 @@ function resolveSource(from, ref) {
1976
2036
  return { dir, commit: git(["rev-parse", "HEAD"]).trim() };
1977
2037
  }
1978
2038
  const dir = resolve5(from);
1979
- if (!existsSync8(dir))
2039
+ if (!existsSync9(dir))
1980
2040
  throw new Error("E_SOURCE: path not found");
1981
2041
  let commit = "local";
1982
2042
  try {
@@ -1985,7 +2045,7 @@ function resolveSource(from, ref) {
1985
2045
  return { dir, commit };
1986
2046
  }
1987
2047
  function checkTargetFree(path, kind, expected) {
1988
- if (!existsSync8(path) && !isSymlink(path))
2048
+ if (!existsSync9(path) && !isSymlink(path))
1989
2049
  return;
1990
2050
  if (kind === "file") {
1991
2051
  if (!isSymlink(path) && statSync5(path).isFile() && shaFile(path) === expected)
@@ -2051,7 +2111,7 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
2051
2111
  try {
2052
2112
  if (p.entry.type === "file" && p.content !== undefined) {
2053
2113
  checkTargetFree(p.dest, "file", sha256(Buffer.from(p.content, "base64")));
2054
- } else if (p.linkTarget && (existsSync8(p.dest) || isSymlink(p.dest))) {
2114
+ } else if (p.linkTarget && (existsSync9(p.dest) || isSymlink(p.dest))) {
2055
2115
  if (!linkManaged(p.dest, p.linkTarget))
2056
2116
  conflict = true;
2057
2117
  }
@@ -2065,7 +2125,7 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
2065
2125
  throw new Error(`E_CONFLICT: ${p.dest} exists and differs`);
2066
2126
  throw new Error(`E_CONFLICT: ${p.dest} exists and is not managed`);
2067
2127
  }
2068
- if (!isSymlink(p.dest) && existsSync8(p.dest)) {
2128
+ if (!isSymlink(p.dest) && existsSync9(p.dest)) {
2069
2129
  mkdirSync7(backupDir, { recursive: true });
2070
2130
  cpSync(p.dest, join10(backupDir, basename5(p.dest)));
2071
2131
  }
@@ -2079,7 +2139,7 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
2079
2139
  writeFileSync5(p.dest, Buffer.from(p.content, "base64"));
2080
2140
  p.entry.value = shaFile(p.dest);
2081
2141
  } else if (p.linkTarget) {
2082
- if (existsSync8(p.dest) || isSymlink(p.dest))
2142
+ if (existsSync9(p.dest) || isSymlink(p.dest))
2083
2143
  unlinkSync(p.dest);
2084
2144
  symlinkSync(resolve5(p.linkTarget), p.dest);
2085
2145
  p.entry.value = relative3(targetDir, resolve5(p.linkTarget));
@@ -2095,8 +2155,9 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
2095
2155
  function install(from, opts = {}) {
2096
2156
  const src = resolveSource(from, opts.ref);
2097
2157
  const m = readKitManifest(src.dir);
2098
- const target = resolve5(dataDir2(), "kits", m.id, m.version);
2099
- if (!target.startsWith(resolve5(dataDir2(), "kits") + sep4))
2158
+ const kitsRoot = resolve5(dataDir2(), "kits");
2159
+ const target = resolve5(kitsRoot, m.id, m.version);
2160
+ if (!target.startsWith(kitsRoot + sep4))
2100
2161
  throw new Error("E_MANIFEST: version escapes install dir");
2101
2162
  const kitRootRel = m.root ?? ".";
2102
2163
  const kitRoot = resolve5(target, kitRootRel);
@@ -2108,47 +2169,113 @@ function install(from, opts = {}) {
2108
2169
  const planned = renderContent(src.dir, target, m, { dryRun: true }) ?? [];
2109
2170
  if (opts.dryRun)
2110
2171
  return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered: planned, hash: null, changed: false };
2111
- const alreadyInstalled = existsSync8(join10(target, "kit.lock"));
2112
- if (!alreadyInstalled || hashTree(src.dir) !== hashTree(target)) {
2113
- rmSync3(target, { recursive: true, force: true });
2114
- mkdirSync7(target, { recursive: true });
2115
- cpSync(src.dir, target, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
2116
- }
2117
- const rendered = renderContent(src.dir, target, m) ?? planned;
2118
- const appliedConfig = apply(m.config ?? []);
2119
- const hash = hashTree(target);
2120
- if (hash === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") {
2121
- rmSync3(target, { recursive: true, force: true });
2122
- throw new Error(`E_KIT_EMPTY: kit ${m.id}@${m.version} 内容为空(检查 source 与清单路径)`);
2172
+ return withInstallLock(join10(kitsRoot, ".install.lock"), () => {
2173
+ const alreadyInstalled = existsSync9(join10(target, "kit.lock"));
2174
+ const changed = !alreadyInstalled || hashTree(src.dir) !== hashTree(target);
2175
+ if (changed) {
2176
+ const staging = join10(kitsRoot, ".staging", `${m.id}-${m.version}-${process.pid}-${Date.now()}`);
2177
+ mkdirSync7(staging, { recursive: true });
2178
+ try {
2179
+ cpSync(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
2180
+ const stagedHash = hashTree(staging);
2181
+ if (stagedHash === EMPTY_TREE_HASH)
2182
+ throw new Error(`E_KIT_EMPTY: kit ${m.id}@${m.version} 内容为空(检查 source 与清单路径)`);
2183
+ writeFileSync5(join10(staging, "kit.lock"), JSON.stringify({
2184
+ id: m.id,
2185
+ version: m.version,
2186
+ commit: src.commit,
2187
+ root: kitRootRel,
2188
+ hash: stagedHash,
2189
+ rendered: [],
2190
+ commands: m.commands ?? [],
2191
+ knowledge: m.knowledge ?? [],
2192
+ dataRoot: m.data_root ?? null,
2193
+ env: m.env ?? null,
2194
+ config: []
2195
+ }, null, 1));
2196
+ mkdirSync7(dirname6(target), { recursive: true });
2197
+ if (existsSync9(target)) {
2198
+ const bak = `${target}.old-${process.pid}-${Date.now()}`;
2199
+ renameSync3(target, bak);
2200
+ try {
2201
+ renameSync3(staging, target);
2202
+ rmSync3(bak, { recursive: true, force: true });
2203
+ } catch (e) {
2204
+ try {
2205
+ renameSync3(bak, target);
2206
+ } catch {}
2207
+ throw e;
2208
+ }
2209
+ } else {
2210
+ renameSync3(staging, target);
2211
+ }
2212
+ } finally {
2213
+ rmSync3(staging, { recursive: true, force: true });
2214
+ }
2215
+ }
2216
+ const rendered = renderContent(src.dir, target, m) ?? planned;
2217
+ const appliedConfig = apply(m.config ?? []);
2218
+ const hash = hashTree(target);
2219
+ writeFileSync5(join10(target, "kit.lock"), JSON.stringify({
2220
+ id: m.id,
2221
+ version: m.version,
2222
+ commit: src.commit,
2223
+ root: kitRootRel,
2224
+ hash,
2225
+ rendered,
2226
+ commands: m.commands ?? [],
2227
+ knowledge: m.knowledge ?? [],
2228
+ dataRoot: m.data_root ?? null,
2229
+ env: m.env ?? null,
2230
+ config: appliedConfig
2231
+ }, null, 1));
2232
+ return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered, hash, changed };
2233
+ });
2234
+ }
2235
+ function installed() {
2236
+ const root = join10(dataDir2(), "kits");
2237
+ if (!existsSync9(root))
2238
+ return [];
2239
+ const out = [];
2240
+ for (const e of readdirSync3(root, { withFileTypes: true })) {
2241
+ if (!e.isDirectory() || e.name.startsWith("."))
2242
+ continue;
2243
+ const versions = versionDirs(join10(root, e.name));
2244
+ const version = versions[versions.length - 1];
2245
+ if (!version)
2246
+ continue;
2247
+ const dir = join10(root, e.name, version);
2248
+ let tags = [];
2249
+ try {
2250
+ tags = readKitManifest(dir).tags ?? [];
2251
+ } catch {}
2252
+ out.push({ id: e.name, version, dir, tags });
2123
2253
  }
2124
- writeFileSync5(join10(target, "kit.lock"), JSON.stringify({
2125
- id: m.id,
2126
- version: m.version,
2127
- commit: src.commit,
2128
- root: kitRootRel,
2129
- hash,
2130
- rendered,
2131
- commands: m.commands ?? [],
2132
- knowledge: m.knowledge ?? [],
2133
- dataRoot: m.data_root ?? null,
2134
- env: m.env ?? null,
2135
- config: appliedConfig
2136
- }, null, 1));
2137
- return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered, hash, changed: true };
2254
+ return out;
2138
2255
  }
2139
2256
  function list() {
2140
2257
  const root = join10(dataDir2(), "kits");
2141
- if (!existsSync8(root))
2258
+ if (!existsSync9(root))
2142
2259
  return [];
2143
- return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ id: e.name, versions: readdirSync3(join10(root, e.name)).filter((v) => statSync5(join10(root, e.name, v)).isDirectory()) }));
2260
+ return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => {
2261
+ const versions = versionDirs(join10(root, e.name));
2262
+ const latest = versions[versions.length - 1];
2263
+ let tags = [];
2264
+ if (latest) {
2265
+ try {
2266
+ tags = readKitManifest(join10(root, e.name, latest)).tags ?? [];
2267
+ } catch {}
2268
+ }
2269
+ return { id: e.name, versions, tags };
2270
+ });
2144
2271
  }
2145
2272
  function readLock(id, version) {
2146
2273
  const idDir = join10(dataDir2(), "kits", id);
2147
- if (!existsSync8(idDir))
2274
+ if (!existsSync9(idDir))
2148
2275
  throw new Error("E_NOT_INSTALLED");
2149
- const v = version || readdirSync3(idDir).sort().pop();
2276
+ const v = version || versionDirs(idDir).pop();
2150
2277
  const lockFile = join10(idDir, v, "kit.lock");
2151
- if (!existsSync8(lockFile))
2278
+ if (!existsSync9(lockFile))
2152
2279
  throw new Error("E_NOT_INSTALLED");
2153
2280
  return { version: v, lock: JSON.parse(readFileSync7(lockFile, "utf8")) };
2154
2281
  }
@@ -2165,7 +2292,7 @@ function verify2(id, version) {
2165
2292
  for (const r of lock.rendered) {
2166
2293
  if (r.type === "dir")
2167
2294
  continue;
2168
- if (!existsSync8(r.path)) {
2295
+ if (!existsSync9(r.path)) {
2169
2296
  drift.push(`missing: ${r.path}`);
2170
2297
  continue;
2171
2298
  }
@@ -2189,19 +2316,19 @@ function verify2(id, version) {
2189
2316
  }
2190
2317
  function remove(id) {
2191
2318
  const idDir = join10(dataDir2(), "kits", id);
2192
- if (!existsSync8(idDir))
2319
+ if (!existsSync9(idDir))
2193
2320
  throw new Error("E_NOT_INSTALLED");
2194
2321
  const removed = [];
2195
2322
  const skipped = [];
2196
2323
  for (const v of readdirSync3(idDir)) {
2197
2324
  const dir = join10(idDir, v);
2198
2325
  const lockFile = join10(dir, "kit.lock");
2199
- if (existsSync8(lockFile)) {
2326
+ if (existsSync9(lockFile)) {
2200
2327
  const lock = JSON.parse(readFileSync7(lockFile, "utf8"));
2201
2328
  for (const r of lock.rendered) {
2202
2329
  if (r.type === "dir")
2203
2330
  continue;
2204
- if (!existsSync8(r.path) && !isSymlink(r.path))
2331
+ if (!existsSync9(r.path) && !isSymlink(r.path))
2205
2332
  continue;
2206
2333
  if (r.type === "file") {
2207
2334
  if (!isSymlink(r.path) && shaFile(r.path) === r.value) {
@@ -2218,7 +2345,7 @@ function remove(id) {
2218
2345
  continue;
2219
2346
  }
2220
2347
  const want = resolve5(dir, r.value);
2221
- if (existsSync8(want) && cur === realpathSync2(want)) {
2348
+ if (existsSync9(want) && cur === realpathSync2(want)) {
2222
2349
  unlinkSync(r.path);
2223
2350
  removed.push(r.path);
2224
2351
  } else
@@ -2233,7 +2360,7 @@ function remove(id) {
2233
2360
  }
2234
2361
 
2235
2362
  // src/default-msg.ts
2236
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
2363
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
2237
2364
  import { homedir as homedir6 } from "node:os";
2238
2365
  import { dirname as dirname7, join as join11, resolve as resolve6 } from "node:path";
2239
2366
  init_paths();
@@ -2259,7 +2386,7 @@ function writeConfig(c) {
2259
2386
  const tmp = `${file}.tmp.${process.pid}`;
2260
2387
  writeFileSync6(tmp, JSON.stringify(next, null, 1) + `
2261
2388
  `, { mode: 384 });
2262
- renameSync3(tmp, file);
2389
+ renameSync4(tmp, file);
2263
2390
  return next;
2264
2391
  }
2265
2392
  function setEnabled(enabled, personaId) {
@@ -2286,7 +2413,7 @@ function isEnabled(personaId) {
2286
2413
  }
2287
2414
  function installedVersion() {
2288
2415
  const dir = join11(diaHome(), "kits", "dia-msg");
2289
- if (!existsSync9(dir))
2416
+ if (!existsSync10(dir))
2290
2417
  return null;
2291
2418
  try {
2292
2419
  const rows = readdirSync4(dir).filter((v) => statSync6(join11(dir, v)).isDirectory()).sort();
@@ -2302,6 +2429,27 @@ function autoInstallAllowed() {
2302
2429
  return false;
2303
2430
  return resolve6(diaHome()) === resolve6(join11(homedir6(), ".dia"));
2304
2431
  }
2432
+ function pkgVersion() {
2433
+ try {
2434
+ return String(JSON.parse(readFileSync8(join11(installDir(), "package.json"), "utf8")).version || "unknown");
2435
+ } catch {
2436
+ return "unknown";
2437
+ }
2438
+ }
2439
+ var stampFile = () => join11(stateDir(), "bootstrap.json");
2440
+ function readStamp() {
2441
+ try {
2442
+ return JSON.parse(readFileSync8(stampFile(), "utf8"));
2443
+ } catch {
2444
+ return null;
2445
+ }
2446
+ }
2447
+ function writeStamp(kitId) {
2448
+ try {
2449
+ mkdirSync8(stateDir(), { recursive: true });
2450
+ writeFileSync6(stampFile(), JSON.stringify({ pkg: pkgVersion(), kit: kitId ?? null, at: new Date().toISOString() }, null, 1));
2451
+ } catch {}
2452
+ }
2305
2453
  function ensure(personaId, opts = {}) {
2306
2454
  const gate = isEnabled(personaId);
2307
2455
  if (!gate.enabled && !opts.force)
@@ -2309,10 +2457,11 @@ function ensure(personaId, opts = {}) {
2309
2457
  if (!opts.force && !autoInstallAllowed())
2310
2458
  return { ok: false, reason: "auto-install-skipped:non-default-root(dia collab enable 可强制安装)" };
2311
2459
  const source = builtinKitDir();
2312
- if (!existsSync9(join11(source, "dia.json")))
2460
+ if (!existsSync10(join11(source, "dia.json")))
2313
2461
  return { ok: false, reason: `missing-kit:${source}` };
2314
2462
  try {
2315
2463
  const r = install(source);
2464
+ writeStamp(r.id);
2316
2465
  return { ok: true, kit: `${r.id}@${r.version}` };
2317
2466
  } catch (e) {
2318
2467
  return { ok: false, reason: `kit-install:${e?.message || String(e)}` };
@@ -2341,7 +2490,7 @@ function ensureForPersona(id, capabilities) {
2341
2490
 
2342
2491
  // src/active.ts
2343
2492
  init_paths();
2344
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7, appendFileSync as appendFileSync4, writeFileSync as writeFileSync7 } from "node:fs";
2493
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7, appendFileSync as appendFileSync4, writeFileSync as writeFileSync7 } from "node:fs";
2345
2494
  import { userInfo } from "node:os";
2346
2495
  import { join as join12 } from "node:path";
2347
2496
  var dataDir3 = () => diaHome();
@@ -2357,10 +2506,10 @@ function readAll() {
2357
2506
  }
2358
2507
  function installedVersions(id) {
2359
2508
  const dir = join12(dataDir3(), "personas", id);
2360
- if (!existsSync10(dir))
2509
+ if (!existsSync11(dir))
2361
2510
  return [];
2362
2511
  const verRoot = join12(dir, "versions");
2363
- if (existsSync10(verRoot))
2512
+ if (existsSync11(verRoot))
2364
2513
  return readdirSync5(verRoot).filter((v) => statSync7(join12(verRoot, v)).isDirectory()).sort();
2365
2514
  const skip = new Set(["current", "latest", "data", "repo", "repo.git", "worktrees", "versions", ".lock"]);
2366
2515
  return readdirSync5(dir).filter((v) => !skip.has(v) && !v.startsWith(".staging-") && statSync7(join12(dir, v)).isDirectory()).sort();
@@ -2408,7 +2557,7 @@ function use(id, version, opts = {}) {
2408
2557
  return { previous, current: entry };
2409
2558
  }
2410
2559
  function history(id, limit = 20) {
2411
- if (!existsSync10(auditFile2()))
2560
+ if (!existsSync11(auditFile2()))
2412
2561
  return [];
2413
2562
  const rows = readFileSync9(auditFile2(), "utf8").split(`
2414
2563
  `).filter(Boolean).map((l) => {
@@ -2705,7 +2854,7 @@ var opencodeConfigDir = () => process.env.DIA_OPENCODE_CONFIG_DIR || join13(home
2705
2854
  var opencodeConfigCandidates = () => process.env.DIA_OPENCODE_CONFIG ? [process.env.DIA_OPENCODE_CONFIG] : OPENCODE_CONFIG_NAMES.map((n) => join13(opencodeConfigDir(), n));
2706
2855
  var opencodeConfig = () => {
2707
2856
  const candidates = opencodeConfigCandidates();
2708
- return candidates.find((p) => existsSync11(p)) ?? candidates[0];
2857
+ return candidates.find((p) => existsSync12(p)) ?? candidates[0];
2709
2858
  };
2710
2859
  function shaFile2(p) {
2711
2860
  return createHash7("sha256").update(readFileSync10(p)).digest("hex");
@@ -2727,7 +2876,7 @@ function walkFiles2(root) {
2727
2876
  }
2728
2877
  function readManifest(dir) {
2729
2878
  const p = join13(dir, "dia.json");
2730
- if (!existsSync11(p))
2879
+ if (!existsSync12(p))
2731
2880
  throw new Error("E_MANIFEST: dia.json not found");
2732
2881
  let m;
2733
2882
  try {
@@ -2751,7 +2900,7 @@ function resolveSource2(from, ref) {
2751
2900
  const key = createHash7("sha1").update(url).digest("hex").slice(0, 12);
2752
2901
  const dir = join13(cacheDir3(), key);
2753
2902
  const git = (args) => execFileSync3("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
2754
- if (!existsSync11(dir)) {
2903
+ if (!existsSync12(dir)) {
2755
2904
  mkdirSync10(cacheDir3(), { recursive: true });
2756
2905
  try {
2757
2906
  execFileSync3("git", ["clone", "--quiet", url, dir], { stdio: "pipe" });
@@ -2777,7 +2926,7 @@ function resolveSource2(from, ref) {
2777
2926
  return { dir, commit };
2778
2927
  }
2779
2928
  const dir = resolve7(from);
2780
- if (!existsSync11(dir))
2929
+ if (!existsSync12(dir))
2781
2930
  throw new Error("E_SOURCE: path not found");
2782
2931
  let commit = "local";
2783
2932
  try {
@@ -2816,7 +2965,7 @@ function install2(from, opts = {}) {
2816
2965
  if (!entry)
2817
2966
  throw new Error("E_MANIFEST: missing entry.opencode");
2818
2967
  const entryAbs = resolve7(src.dir, entry);
2819
- if (!existsSync11(entryAbs))
2968
+ if (!existsSync12(entryAbs))
2820
2969
  throw new Error("E_MANIFEST: entry.opencode file missing");
2821
2970
  if (lstatSync2(entryAbs).isSymbolicLink())
2822
2971
  throw new Error("E_MANIFEST: entry must not be a symlink");
@@ -2825,7 +2974,7 @@ function install2(from, opts = {}) {
2825
2974
  if (!entryReal.startsWith(srcReal + sep5))
2826
2975
  throw new Error("E_MANIFEST: entry escapes persona dir");
2827
2976
  const dest = join13(opencodeAgents(), `${m.id}.md`);
2828
- if (existsSync11(dest) && !readFileSync10(dest).equals(readFileSync10(entryAbs))) {
2977
+ if (existsSync12(dest) && !readFileSync10(dest).equals(readFileSync10(entryAbs))) {
2829
2978
  if (!opts.force)
2830
2979
  throw new Error("E_CONFLICT: agent file exists and differs; remove it first (或 --force 覆盖,覆盖前会备份)");
2831
2980
  overwrites.push(dest);
@@ -2836,7 +2985,7 @@ function install2(from, opts = {}) {
2836
2985
  if (!dshEntry)
2837
2986
  throw new Error("E_MANIFEST: missing entry.dsh");
2838
2987
  const dshSrc = resolve7(src.dir, dshEntry);
2839
- if (!existsSync11(dshSrc) || !statSync8(dshSrc).isDirectory())
2988
+ if (!existsSync12(dshSrc) || !statSync8(dshSrc).isDirectory())
2840
2989
  throw new Error("E_MANIFEST: entry.dsh dir missing");
2841
2990
  if (!realpathSync3(dshSrc).startsWith(realpathSync3(src.dir) + sep5) && realpathSync3(dshSrc) !== realpathSync3(src.dir))
2842
2991
  throw new Error("E_MANIFEST: entry escapes persona dir");
@@ -2847,12 +2996,12 @@ function install2(from, opts = {}) {
2847
2996
  if (!codexEntry)
2848
2997
  throw new Error("E_MANIFEST: missing entry.codex");
2849
2998
  const abs = resolve7(src.dir, codexEntry);
2850
- if (!existsSync11(abs) || !statSync8(abs).isFile())
2999
+ if (!existsSync12(abs) || !statSync8(abs).isFile())
2851
3000
  throw new Error("E_MANIFEST: entry.codex file missing");
2852
3001
  if (!realpathSync3(abs).startsWith(realpathSync3(src.dir) + sep5))
2853
3002
  throw new Error("E_MANIFEST: entry escapes persona dir");
2854
3003
  const dest = join13(codexHome, ".codex", "AGENTS.md");
2855
- if (existsSync11(dest) && !readFileSync10(dest).equals(readFileSync10(abs))) {
3004
+ if (existsSync12(dest) && !readFileSync10(dest).equals(readFileSync10(abs))) {
2856
3005
  if (!opts.force)
2857
3006
  throw new Error("E_CONFLICT: codex AGENTS.md exists and differs; use --force (备份后再覆盖)");
2858
3007
  overwrites.push(dest);
@@ -2871,12 +3020,12 @@ function install2(from, opts = {}) {
2871
3020
  let lockFd = null;
2872
3021
  try {
2873
3022
  try {
2874
- lockFd = openSync(lockPath, "wx");
3023
+ lockFd = openSync2(lockPath, "wx");
2875
3024
  } catch {
2876
3025
  const st = statSync8(lockPath);
2877
3026
  if (Date.now() - st.mtimeMs > 10 * 60000) {
2878
3027
  rmSync4(lockPath, { force: true });
2879
- lockFd = openSync(lockPath, "wx");
3028
+ lockFd = openSync2(lockPath, "wx");
2880
3029
  } else
2881
3030
  throw new Error("E_LOCKED: 另一个安装正在进行(10 分钟后视为陈旧)");
2882
3031
  }
@@ -2887,7 +3036,7 @@ function install2(from, opts = {}) {
2887
3036
  cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
2888
3037
  const stagingHash = hashTree2(staging);
2889
3038
  let reuseTarget = false;
2890
- if (existsSync11(target)) {
3039
+ if (existsSync12(target)) {
2891
3040
  const existing = (() => {
2892
3041
  try {
2893
3042
  return JSON.parse(readFileSync10(join13(target, "dia.lock"), "utf8"));
@@ -2906,11 +3055,11 @@ function install2(from, opts = {}) {
2906
3055
  }
2907
3056
  }
2908
3057
  if (!reuseTarget)
2909
- renameSync4(staging, target);
3058
+ renameSync5(staging, target);
2910
3059
  } finally {
2911
3060
  if (lockFd !== null) {
2912
3061
  try {
2913
- closeSync(lockFd);
3062
+ closeSync2(lockFd);
2914
3063
  } catch {}
2915
3064
  }
2916
3065
  try {
@@ -2936,7 +3085,7 @@ function install2(from, opts = {}) {
2936
3085
  const renderedDirs = [];
2937
3086
  if (hosts.includes("dsh")) {
2938
3087
  const dshDest = resolve7(dshProfiles(), m.id);
2939
- if (existsSync11(dshDest) && !existsSync11(join13(dshDest, ".dia-managed"))) {
3088
+ if (existsSync12(dshDest) && !existsSync12(join13(dshDest, ".dia-managed"))) {
2940
3089
  throw new Error("E_CONFLICT: dsh profile exists and is not managed by dia");
2941
3090
  }
2942
3091
  rmSync4(dshDest, { recursive: true, force: true });
@@ -2957,7 +3106,7 @@ function install2(from, opts = {}) {
2957
3106
  const srcSkill = resolve7(target, sk.from);
2958
3107
  if (!srcSkill.startsWith(target + sep5))
2959
3108
  throw new Error("E_MANIFEST: skill escapes persona dir: " + sk.from);
2960
- if (!existsSync11(srcSkill))
3109
+ if (!existsSync12(srcSkill))
2961
3110
  continue;
2962
3111
  const name = sk.name || basename6(srcSkill);
2963
3112
  mkdirSync10(skillRoot, { recursive: true });
@@ -2979,7 +3128,7 @@ function install2(from, opts = {}) {
2979
3128
  const kitDir = resolve7(target, ref);
2980
3129
  if (!kitDir.startsWith(target + sep5))
2981
3130
  throw new Error(`E_MANIFEST: kit escapes persona dir: ${ref}`);
2982
- if (!existsSync11(join13(kitDir, "dia.json")))
3131
+ if (!existsSync12(join13(kitDir, "dia.json")))
2983
3132
  throw new Error(`E_MANIFEST: kit manifest missing: ${ref}`);
2984
3133
  const r = install(kitDir);
2985
3134
  kitRefs.push({ id: r.id, version: r.version });
@@ -3007,13 +3156,13 @@ function install2(from, opts = {}) {
3007
3156
  }
3008
3157
  function list2() {
3009
3158
  const root = join13(dataDir4(), "personas");
3010
- if (!existsSync11(root))
3159
+ if (!existsSync12(root))
3011
3160
  return [];
3012
- return readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ id: e.name, versions: versionDirs(join13(root, e.name)) }));
3161
+ return readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ id: e.name, versions: versionDirs2(join13(root, e.name)) }));
3013
3162
  }
3014
- function versionDirs(idDir) {
3163
+ function versionDirs2(idDir) {
3015
3164
  const verRoot = join13(idDir, "versions");
3016
- const scan = (root, keep) => existsSync11(root) ? readdirSync6(root).filter((n) => {
3165
+ const scan = (root, keep) => existsSync12(root) ? readdirSync6(root).filter((n) => {
3017
3166
  if (!keep(n))
3018
3167
  return false;
3019
3168
  try {
@@ -3022,14 +3171,14 @@ function versionDirs(idDir) {
3022
3171
  return false;
3023
3172
  }
3024
3173
  }).sort() : [];
3025
- if (existsSync11(verRoot))
3174
+ if (existsSync12(verRoot))
3026
3175
  return scan(verRoot, () => true);
3027
3176
  const keep = (n) => !["current", "latest", "data", "repo", "repo.git", "worktrees", "versions"].includes(n) && !n.startsWith(".staging-") && n !== ".lock";
3028
3177
  return scan(idDir, keep);
3029
3178
  }
3030
3179
  function resolveVersion(id, sel) {
3031
3180
  const dir = join13(dataDir4(), "personas", id);
3032
- const all = versionDirs(dir);
3181
+ const all = versionDirs2(dir);
3033
3182
  if (all.includes(sel))
3034
3183
  return sel;
3035
3184
  const hits = all.filter((v) => v === sel || v.startsWith(sel + "+") || v.startsWith(sel + "-"));
@@ -3045,12 +3194,12 @@ function dirOf(id, version) {
3045
3194
  return null;
3046
3195
  const flat = join13(dataDir4(), "personas", id, v);
3047
3196
  const nested = join13(dataDir4(), "personas", id, "versions", v);
3048
- const dir = existsSync11(nested) ? nested : flat;
3049
- return existsSync11(dir) ? { dir, version: v } : null;
3197
+ const dir = existsSync12(nested) ? nested : flat;
3198
+ return existsSync12(dir) ? { dir, version: v } : null;
3050
3199
  }
3051
3200
  function readMarker(dir) {
3052
3201
  const p = join13(dir, ".dia-managed");
3053
- if (!existsSync11(p))
3202
+ if (!existsSync12(p))
3054
3203
  return null;
3055
3204
  try {
3056
3205
  return JSON.parse(readFileSync10(p, "utf8"));
@@ -3079,9 +3228,9 @@ function cmpSemver(a, b) {
3079
3228
  }
3080
3229
  function gc(id, opts = {}) {
3081
3230
  const idDir = join13(dataDir4(), "personas", id);
3082
- if (!existsSync11(idDir))
3231
+ if (!existsSync12(idDir))
3083
3232
  throw new Error("E_NOT_INSTALLED");
3084
- const all = versionDirs(idDir);
3233
+ const all = versionDirs2(idDir);
3085
3234
  const activeVer = effective(id)?.version;
3086
3235
  const keep = Math.max(1, opts.keep ?? 3);
3087
3236
  const sorted = [...all].sort((a, b) => cmpSemver(b, a));
@@ -3105,12 +3254,12 @@ function gc(id, opts = {}) {
3105
3254
  }
3106
3255
  function verify3(id, version) {
3107
3256
  const idDir = join13(dataDir4(), "personas", id);
3108
- if (!existsSync11(idDir))
3257
+ if (!existsSync12(idDir))
3109
3258
  throw new Error("E_NOT_INSTALLED");
3110
- const v = version || effective(id)?.version || versionDirs(idDir).sort((a, b) => cmpSemver(b, a))[0];
3259
+ const v = version || effective(id)?.version || versionDirs2(idDir).sort((a, b) => cmpSemver(b, a))[0];
3111
3260
  if (!v)
3112
3261
  throw new Error("E_NOT_INSTALLED");
3113
- const dir = existsSync11(join13(idDir, "versions", v)) ? join13(idDir, "versions", v) : join13(idDir, v);
3262
+ const dir = existsSync12(join13(idDir, "versions", v)) ? join13(idDir, "versions", v) : join13(idDir, v);
3114
3263
  const lock = JSON.parse(readFileSync10(join13(dir, "dia.lock"), "utf8"));
3115
3264
  const files = walkFiles2(dir).filter((f) => basename6(f) !== "dia.lock");
3116
3265
  const h = createHash7("sha256");
@@ -3122,13 +3271,13 @@ function verify3(id, version) {
3122
3271
  if (real !== lock.hash)
3123
3272
  issues.push(`内容哈希不一致(期望 ${lock.hash.slice(0, 8)} 实得 ${real.slice(0, 8)})`);
3124
3273
  for (const [f, expectedHash] of Object.entries(lock.renderedHashes || {})) {
3125
- if (!existsSync11(f))
3274
+ if (!existsSync12(f))
3126
3275
  issues.push(`产物缺失:${f}`);
3127
3276
  else if (shaFile2(f) !== expectedHash)
3128
3277
  issues.push(`产物被改动:${f}`);
3129
3278
  }
3130
3279
  for (const d of lock.renderedDirs || []) {
3131
- if (!existsSync11(d.path)) {
3280
+ if (!existsSync12(d.path)) {
3132
3281
  issues.push(`宿主 profile 缺失:${d.path}`);
3133
3282
  continue;
3134
3283
  }
@@ -3141,7 +3290,7 @@ function verify3(id, version) {
3141
3290
  for (const r of lock.content?.rendered ?? []) {
3142
3291
  if (r.type === "dir")
3143
3292
  continue;
3144
- if (!existsSync11(r.path)) {
3293
+ if (!existsSync12(r.path)) {
3145
3294
  issues.push(`内容产物缺失:${r.path}`);
3146
3295
  continue;
3147
3296
  }
@@ -3157,7 +3306,7 @@ function verify3(id, version) {
3157
3306
  continue;
3158
3307
  }
3159
3308
  const want = resolve7(dir, r.value);
3160
- if (!existsSync11(want) || cur !== realpathSync3(want))
3309
+ if (!existsSync12(want) || cur !== realpathSync3(want))
3161
3310
  issues.push(`内容产物指向不符:${r.path}`);
3162
3311
  }
3163
3312
  }
@@ -3274,7 +3423,7 @@ function create(id, opts = {}) {
3274
3423
  const hosts = opts.hosts?.length ? opts.hosts : ["opencode", "dsh"];
3275
3424
  for (const h of hosts)
3276
3425
  assertHost(h);
3277
- if (existsSync11(dir) && readdirSync6(dir).length)
3426
+ if (existsSync12(dir) && readdirSync6(dir).length)
3278
3427
  throw new Error(`E_CONFLICT: ${dir} 非空`);
3279
3428
  const files = skeleton(id, hosts, opts.msg !== false);
3280
3429
  if (!opts.dryRun) {
@@ -3302,7 +3451,7 @@ function compareDir(expected, actual) {
3302
3451
  if (HOST_OWNED_FILES.has(rel))
3303
3452
  continue;
3304
3453
  const dest = join13(actual, rel);
3305
- if (!existsSync11(dest))
3454
+ if (!existsSync12(dest))
3306
3455
  out.push({ path: dest, action: "missing" });
3307
3456
  else if (shaFile2(dest) !== shaFile2(f))
3308
3457
  out.push({ path: dest, action: "changed" });
@@ -3311,7 +3460,7 @@ function compareDir(expected, actual) {
3311
3460
  }
3312
3461
  function render(id, opts = {}) {
3313
3462
  const idDir = join13(dataDir4(), "personas", id);
3314
- if (!existsSync11(idDir))
3463
+ if (!existsSync12(idDir))
3315
3464
  throw new Error(`E_NOT_INSTALLED: persona ${id}`);
3316
3465
  const version = effective(id)?.version ?? readdirSync6(idDir).filter((v) => statSync8(join13(idDir, v)).isDirectory()).sort().pop();
3317
3466
  const dir = join13(idDir, version);
@@ -3330,10 +3479,10 @@ function render(id, opts = {}) {
3330
3479
  agentSrc = resolve7(dir, entry);
3331
3480
  if (!agentSrc.startsWith(dir + sep5))
3332
3481
  throw new Error("E_MANIFEST: entry escapes persona dir");
3333
- if (!existsSync11(agentSrc))
3482
+ if (!existsSync12(agentSrc))
3334
3483
  throw new Error("E_MANIFEST: entry.opencode file missing");
3335
3484
  agentDest = join13(opencodeAgents(), `${id}.md`);
3336
- if (!existsSync11(agentDest))
3485
+ if (!existsSync12(agentDest))
3337
3486
  drift.push({ path: agentDest, action: "missing" });
3338
3487
  else if (shaFile2(agentDest) !== shaFile2(agentSrc))
3339
3488
  drift.push({ path: agentDest, action: "changed" });
@@ -3345,10 +3494,10 @@ function render(id, opts = {}) {
3345
3494
  codexSrc = resolve7(dir, entry);
3346
3495
  if (!codexSrc.startsWith(dir + sep5))
3347
3496
  throw new Error("E_MANIFEST: entry escapes persona dir");
3348
- if (!existsSync11(codexSrc))
3497
+ if (!existsSync12(codexSrc))
3349
3498
  throw new Error("E_MANIFEST: entry.codex file missing");
3350
3499
  codexDest = join13(codexHomeFor(opts, m), ".codex", "AGENTS.md");
3351
- if (!existsSync11(codexDest))
3500
+ if (!existsSync12(codexDest))
3352
3501
  drift.push({ path: codexDest, action: "missing" });
3353
3502
  else if (shaFile2(codexDest) !== shaFile2(codexSrc))
3354
3503
  drift.push({ path: codexDest, action: "changed" });
@@ -3360,15 +3509,15 @@ function render(id, opts = {}) {
3360
3509
  dshSrc = resolve7(dir, entry);
3361
3510
  if (!dshSrc.startsWith(dir + sep5))
3362
3511
  throw new Error("E_MANIFEST: entry escapes persona dir");
3363
- if (!existsSync11(dshSrc))
3512
+ if (!existsSync12(dshSrc))
3364
3513
  throw new Error("E_MANIFEST: entry.dsh dir missing");
3365
3514
  dshDest = resolve7(dshProfiles(), id);
3366
- if (!existsSync11(dshDest))
3515
+ if (!existsSync12(dshDest))
3367
3516
  drift.push({ path: dshDest, action: "missing" });
3368
3517
  else {
3369
3518
  drift.push(...compareDir(dshSrc, dshDest));
3370
3519
  const marker = join13(dshDest, ".dia-managed");
3371
- if (!existsSync11(marker))
3520
+ if (!existsSync12(marker))
3372
3521
  drift.push({ path: marker, action: "missing" });
3373
3522
  else {
3374
3523
  try {
@@ -3388,7 +3537,7 @@ function render(id, opts = {}) {
3388
3537
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
3389
3538
  const bdir = join13(dataDir4(), "backups", `render-${stamp}`);
3390
3539
  for (const d of drift) {
3391
- if (!existsSync11(d.path))
3540
+ if (!existsSync12(d.path))
3392
3541
  continue;
3393
3542
  mkdirSync10(bdir, { recursive: true });
3394
3543
  const dest = join13(bdir, basename6(d.path));
@@ -3439,14 +3588,14 @@ function render(id, opts = {}) {
3439
3588
  }
3440
3589
  function renderDrift(id, opts = {}) {
3441
3590
  const idDir = join13(dataDir4(), "personas", id);
3442
- if (!existsSync11(idDir))
3591
+ if (!existsSync12(idDir))
3443
3592
  throw new Error(`E_NOT_INSTALLED: persona ${id}`);
3444
3593
  const version = effective(id)?.version ?? readdirSync6(idDir).filter((v) => statSync8(join13(idDir, v)).isDirectory()).sort().pop();
3445
3594
  if (!version)
3446
3595
  return { id, version: "", drift: [], skipped: `无已安装版本:${idDir}` };
3447
3596
  const dir = join13(idDir, version);
3448
3597
  const lockPath = join13(dir, "dia.lock");
3449
- if (!existsSync11(lockPath))
3598
+ if (!existsSync12(lockPath))
3450
3599
  return { id, version, drift: [], skipped: `无 dia.lock(安装未完成):${dir}` };
3451
3600
  let lock;
3452
3601
  try {
@@ -3464,18 +3613,18 @@ function renderDrift(id, opts = {}) {
3464
3613
  }
3465
3614
  };
3466
3615
  for (const [f, expected] of Object.entries(lock.renderedHashes ?? {})) {
3467
- if (!existsSync11(f))
3616
+ if (!existsSync12(f))
3468
3617
  push({ path: f, action: "missing" });
3469
3618
  else if (shaFile2(f) !== expected)
3470
3619
  push({ path: f, action: "changed" });
3471
3620
  }
3472
3621
  for (const d of lock.renderedDirs ?? []) {
3473
- if (!existsSync11(d.path)) {
3622
+ if (!existsSync12(d.path)) {
3474
3623
  push({ path: d.path, action: "missing" });
3475
3624
  continue;
3476
3625
  }
3477
3626
  const marker = join13(d.path, ".dia-managed");
3478
- if (!existsSync11(marker)) {
3627
+ if (!existsSync12(marker)) {
3479
3628
  push({ path: marker, action: "missing" });
3480
3629
  continue;
3481
3630
  }
@@ -3489,7 +3638,7 @@ function renderDrift(id, opts = {}) {
3489
3638
  for (const c of lock.content?.rendered ?? []) {
3490
3639
  if (c.type === "dir")
3491
3640
  continue;
3492
- if (!existsSync11(c.path)) {
3641
+ if (!existsSync12(c.path)) {
3493
3642
  push({ path: c.path, action: "missing" });
3494
3643
  continue;
3495
3644
  }
@@ -3506,22 +3655,22 @@ function renderDrift(id, opts = {}) {
3506
3655
  continue;
3507
3656
  }
3508
3657
  const want = resolve7(dir, c.value);
3509
- if (!existsSync11(want) || cur !== realpathSync3(want))
3658
+ if (!existsSync12(want) || cur !== realpathSync3(want))
3510
3659
  push({ path: c.path, action: "changed" });
3511
3660
  }
3512
3661
  return { id, version: r.version, drift };
3513
3662
  }
3514
3663
  function remove2(id) {
3515
3664
  const idDir = join13(dataDir4(), "personas", id);
3516
- if (!existsSync11(idDir))
3665
+ if (!existsSync12(idDir))
3517
3666
  throw new Error("E_NOT_INSTALLED");
3518
3667
  const removed = [];
3519
3668
  const skipped = [];
3520
3669
  const allowed = resolve7(opencodeAgents(), `${id}.md`);
3521
- for (const v of versionDirs(idDir)) {
3522
- const dir = existsSync11(join13(idDir, "versions", v)) ? join13(idDir, "versions", v) : join13(idDir, v);
3670
+ for (const v of versionDirs2(idDir)) {
3671
+ const dir = existsSync12(join13(idDir, "versions", v)) ? join13(idDir, "versions", v) : join13(idDir, v);
3523
3672
  const lockFile = join13(dir, "dia.lock");
3524
- if (existsSync11(lockFile)) {
3673
+ if (existsSync12(lockFile)) {
3525
3674
  const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
3526
3675
  for (const d of lock.renderedDirs || []) {
3527
3676
  const abs = resolve7(d.path);
@@ -3529,7 +3678,7 @@ function remove2(id) {
3529
3678
  skipped.push(abs);
3530
3679
  continue;
3531
3680
  }
3532
- if (!existsSync11(abs))
3681
+ if (!existsSync12(abs))
3533
3682
  continue;
3534
3683
  const m = readMarker(abs);
3535
3684
  if (!m || m.id !== id) {
@@ -3542,10 +3691,10 @@ function remove2(id) {
3542
3691
  for (const r of lock.content?.rendered ?? []) {
3543
3692
  if (r.type === "dir")
3544
3693
  continue;
3545
- if (!existsSync11(r.path))
3694
+ if (!existsSync12(r.path))
3546
3695
  continue;
3547
3696
  if (r.type === "file") {
3548
- if (!existsSync11(r.path) || shaFile2(r.path) !== r.value) {
3697
+ if (!existsSync12(r.path) || shaFile2(r.path) !== r.value) {
3549
3698
  skipped.push(r.path);
3550
3699
  continue;
3551
3700
  }
@@ -3560,7 +3709,7 @@ function remove2(id) {
3560
3709
  continue;
3561
3710
  }
3562
3711
  const want = resolve7(dir, r.value);
3563
- if (existsSync11(want) && cur === realpathSync3(want)) {
3712
+ if (existsSync12(want) && cur === realpathSync3(want)) {
3564
3713
  unlinkSync2(r.path);
3565
3714
  removed.push(r.path);
3566
3715
  } else
@@ -3575,7 +3724,7 @@ function remove2(id) {
3575
3724
  continue;
3576
3725
  }
3577
3726
  const expectedHash = lock.renderedHashes?.[f];
3578
- if (!expectedHash || !existsSync11(abs) || shaFile2(abs) !== expectedHash) {
3727
+ if (!expectedHash || !existsSync12(abs) || shaFile2(abs) !== expectedHash) {
3579
3728
  skipped.push(abs);
3580
3729
  continue;
3581
3730
  }
@@ -3606,7 +3755,7 @@ function showDefaultAgent(opts = {}) {
3606
3755
  if (host !== "opencode")
3607
3756
  throw new Error("E_HOST: only opencode has default_agent");
3608
3757
  const file = opencodeConfig();
3609
- if (!existsSync11(file))
3758
+ if (!existsSync12(file))
3610
3759
  return { file, current: null };
3611
3760
  const cfg = readOpencodeConfig(file);
3612
3761
  return { file, current: typeof cfg.default_agent === "string" ? cfg.default_agent : null };
@@ -3624,13 +3773,13 @@ function setDefaultAgent(id, opts = {}) {
3624
3773
  if (host !== "opencode")
3625
3774
  throw new Error("E_HOST: only opencode has default_agent");
3626
3775
  const file = opencodeConfig();
3627
- if (!existsSync11(file))
3776
+ if (!existsSync12(file))
3628
3777
  throw new Error(`E_CONFIG: ${file} 不存在(先装宿主,或设 DIA_OPENCODE_CONFIG)`);
3629
3778
  const raw = readFileSync10(file, "utf8");
3630
3779
  const cfg = readOpencodeConfig(file);
3631
3780
  const stateFile = join13(dataDir4(), "state", "default-agent.json");
3632
3781
  let state = { file, previous: null, set_at: null };
3633
- if (existsSync11(stateFile)) {
3782
+ if (existsSync12(stateFile)) {
3634
3783
  try {
3635
3784
  state = JSON.parse(readFileSync10(stateFile, "utf8"));
3636
3785
  } catch {}
@@ -3653,7 +3802,7 @@ function setDefaultAgent(id, opts = {}) {
3653
3802
  }
3654
3803
  function checkUpstream(id, version) {
3655
3804
  const idDir = join13(dataDir4(), "personas", id);
3656
- if (!existsSync11(idDir))
3805
+ if (!existsSync12(idDir))
3657
3806
  throw new Error("E_NOT_INSTALLED");
3658
3807
  const v = version || (effective(id)?.version ?? readdirSync6(idDir).sort().pop());
3659
3808
  const lock = JSON.parse(readFileSync10(join13(idDir, v, "dia.lock"), "utf8"));
@@ -3687,7 +3836,7 @@ function checkUpstream(id, version) {
3687
3836
  }
3688
3837
  function update(id, opts = {}) {
3689
3838
  const idDir = join13(dataDir4(), "personas", id);
3690
- if (!existsSync11(idDir))
3839
+ if (!existsSync12(idDir))
3691
3840
  throw new Error("E_NOT_INSTALLED");
3692
3841
  const cur = effective(id)?.version ?? readdirSync6(idDir).sort().pop();
3693
3842
  const lock = JSON.parse(readFileSync10(join13(idDir, cur, "dia.lock"), "utf8"));
@@ -3701,7 +3850,7 @@ function update(id, opts = {}) {
3701
3850
 
3702
3851
  // src/msg.ts
3703
3852
  init_paths();
3704
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, statSync as statSync9, writeFileSync as writeFileSync9 } from "node:fs";
3853
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync11, renameSync as renameSync6, statSync as statSync9, writeFileSync as writeFileSync9 } from "node:fs";
3705
3854
  import { join as join14 } from "node:path";
3706
3855
  import { randomBytes } from "node:crypto";
3707
3856
  var msgRoot2 = () => msgRoot();
@@ -3713,11 +3862,11 @@ function atomicWrite(path, data) {
3713
3862
  mkdirSync11(join14(path, ".."), { recursive: true });
3714
3863
  const tmp = `${path}.tmp.${process.pid}.${randomBytes(2).toString("hex")}`;
3715
3864
  writeFileSync9(tmp, data);
3716
- renameSync5(tmp, path);
3865
+ renameSync6(tmp, path);
3717
3866
  }
3718
3867
  function readAll2(agent) {
3719
3868
  const dir = dirOf2(agent);
3720
- if (!existsSync12(dir))
3869
+ if (!existsSync13(dir))
3721
3870
  return [];
3722
3871
  return readdirSync7(dir).filter((f) => f.endsWith(".json")).map((f) => {
3723
3872
  try {
@@ -3736,7 +3885,7 @@ function newId(prefix) {
3736
3885
  }
3737
3886
  function allInboxes() {
3738
3887
  const root = msgRoot2();
3739
- if (!existsSync12(root))
3888
+ if (!existsSync13(root))
3740
3889
  return [];
3741
3890
  return readdirSync7(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ agent: e.name, messages: readAll2(e.name) }));
3742
3891
  }
@@ -3749,7 +3898,7 @@ function send(opts) {
3749
3898
  throw new Error("E_USAGE: --subject 必填");
3750
3899
  if (opts.body && opts.bodyFile)
3751
3900
  throw new Error("E_USAGE: --body 与 --body-file 只能给一个");
3752
- if (opts.bodyFile && !existsSync12(opts.bodyFile))
3901
+ if (opts.bodyFile && !existsSync13(opts.bodyFile))
3753
3902
  throw new Error(`E_USAGE: body file 不存在: ${opts.bodyFile}`);
3754
3903
  const threadId = opts.threadId || newId("th");
3755
3904
  const existing = [...readAll2(opts.to), ...readAll2(opts.from)].filter((m) => m.thread_id === threadId);
@@ -3792,7 +3941,7 @@ function inbox(agent, opts = {}) {
3792
3941
  }
3793
3942
  function findInbox(agent, id) {
3794
3943
  const dir = dirOf2(agent);
3795
- if (!existsSync12(dir))
3944
+ if (!existsSync13(dir))
3796
3945
  return null;
3797
3946
  for (const f of readdirSync7(dir)) {
3798
3947
  if (!f.endsWith(".json"))
@@ -3901,7 +4050,7 @@ function stuck(minutes = 30, now = new Date) {
3901
4050
  }
3902
4051
  function agents() {
3903
4052
  const root = msgRoot2();
3904
- if (!existsSync12(root))
4053
+ if (!existsSync13(root))
3905
4054
  return [];
3906
4055
  return readdirSync7(root, { withFileTypes: true }).filter((e) => e.isDirectory() && statSync9(join14(root, e.name)).isDirectory()).map((e) => e.name);
3907
4056
  }
@@ -3909,13 +4058,13 @@ function agents() {
3909
4058
  // src/collab.ts
3910
4059
  init_paths();
3911
4060
  import {
3912
- closeSync as closeSync2,
3913
- existsSync as existsSync13,
4061
+ closeSync as closeSync3,
4062
+ existsSync as existsSync14,
3914
4063
  mkdirSync as mkdirSync12,
3915
- openSync as openSync2,
4064
+ openSync as openSync3,
3916
4065
  readdirSync as readdirSync8,
3917
4066
  readFileSync as readFileSync12,
3918
- renameSync as renameSync6,
4067
+ renameSync as renameSync7,
3919
4068
  rmSync as rmSync5,
3920
4069
  statSync as statSync10,
3921
4070
  writeFileSync as writeFileSync10
@@ -3954,7 +4103,7 @@ function writeAtomic(path, data) {
3954
4103
  ensureDir(dirname9(path));
3955
4104
  const tmp = `${path}.tmp.${process.pid}.${randomBytes2(2).toString("hex")}`;
3956
4105
  writeFileSync10(tmp, data, { mode: 384 });
3957
- renameSync6(tmp, path);
4106
+ renameSync7(tmp, path);
3958
4107
  }
3959
4108
  function sleep(ms) {
3960
4109
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -3967,7 +4116,7 @@ function withLock(name, fn) {
3967
4116
  let fd;
3968
4117
  for (let i = 0;i < 150; i++) {
3969
4118
  try {
3970
- fd = openSync2(lock, "wx", 384);
4119
+ fd = openSync3(lock, "wx", 384);
3971
4120
  break;
3972
4121
  } catch (e) {
3973
4122
  if (e?.code !== "EEXIST")
@@ -3985,7 +4134,7 @@ function withLock(name, fn) {
3985
4134
  return fn();
3986
4135
  } finally {
3987
4136
  try {
3988
- closeSync2(fd);
4137
+ closeSync3(fd);
3989
4138
  } catch {}
3990
4139
  try {
3991
4140
  rmSync5(lock, { force: true });
@@ -4053,7 +4202,7 @@ function messageFile(sessionId, id) {
4053
4202
  }
4054
4203
  function readAll3(sessionId) {
4055
4204
  const dir = join15(inboxRoot(), normalizeSessionId(sessionId));
4056
- if (!existsSync13(dir))
4205
+ if (!existsSync14(dir))
4057
4206
  return [];
4058
4207
  const out = [];
4059
4208
  for (const f of readdirSync8(dir)) {
@@ -4067,7 +4216,7 @@ function readAll3(sessionId) {
4067
4216
  return out.sort((a, b) => a.msg.created_at.localeCompare(b.msg.created_at) || a.msg.seq - b.msg.seq);
4068
4217
  }
4069
4218
  function allSessionsWithInbox() {
4070
- if (!existsSync13(inboxRoot()))
4219
+ if (!existsSync14(inboxRoot()))
4071
4220
  return [];
4072
4221
  return readdirSync8(inboxRoot(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4073
4222
  }
@@ -4090,7 +4239,7 @@ function legacySlug(agent) {
4090
4239
  return `${base}-${createHash8("sha1").update(agent).digest("hex").slice(0, 6)}`;
4091
4240
  }
4092
4241
  function legacyAll() {
4093
- if (!existsSync13(legacyRoot()))
4242
+ if (!existsSync14(legacyRoot()))
4094
4243
  return [];
4095
4244
  const out = [];
4096
4245
  for (const slug of readdirSync8(legacyRoot(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)) {
@@ -4132,7 +4281,7 @@ function readBody(opts) {
4132
4281
  throw new Error(`E_USAGE: body 超过 ${MAX_BODY} 字节,请拆分或引用文件`);
4133
4282
  return { body: opts.body, body_bytes: bytes };
4134
4283
  }
4135
- if (!existsSync13(opts.bodyFile))
4284
+ if (!existsSync14(opts.bodyFile))
4136
4285
  throw new Error(`E_USAGE: body file 不存在: ${opts.bodyFile}`);
4137
4286
  const buf = readFileSync12(opts.bodyFile);
4138
4287
  if (buf.byteLength > MAX_BODY)
@@ -4312,13 +4461,13 @@ function close(sessionId, id, reason) {
4312
4461
  }
4313
4462
  function migrateLegacy(opts = {}) {
4314
4463
  const oldRoot = join15(diaHome(), "msg");
4315
- if (!existsSync13(oldRoot))
4464
+ if (!existsSync14(oldRoot))
4316
4465
  return { total: 0, migrated: 0, skipped: 0, messages: [] };
4317
4466
  const rows = [];
4318
4467
  let migrated = 0, skipped = 0, total = 0;
4319
4468
  for (const agent of readdirSync8(oldRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)) {
4320
4469
  const dir = join15(oldRoot, agent, "inbox");
4321
- if (!existsSync13(dir))
4470
+ if (!existsSync14(dir))
4322
4471
  continue;
4323
4472
  const slug = legacySlug(agent);
4324
4473
  for (const f of readdirSync8(dir)) {
@@ -4336,7 +4485,7 @@ function migrateLegacy(opts = {}) {
4336
4485
  continue;
4337
4486
  }
4338
4487
  const target = join15(legacyRoot(), slug, `${legacyId}.json`);
4339
- if (existsSync13(target)) {
4488
+ if (existsSync14(target)) {
4340
4489
  skipped++;
4341
4490
  continue;
4342
4491
  }
@@ -4348,7 +4497,7 @@ function migrateLegacy(opts = {}) {
4348
4497
  const allowed = [resolve8(oldRoot), resolve8(tmpdir())].some((base) => abs === base || abs.startsWith(base + sep6));
4349
4498
  if (!allowed) {
4350
4499
  bodyMissing = true;
4351
- } else if (existsSync13(abs)) {
4500
+ } else if (existsSync14(abs)) {
4352
4501
  const buf = readFileSync12(abs);
4353
4502
  bodyBytes = buf.byteLength;
4354
4503
  if (bodyBytes > MAX_BODY) {
@@ -4396,7 +4545,7 @@ function migrateLegacy(opts = {}) {
4396
4545
 
4397
4546
  // src/claims.ts
4398
4547
  init_paths();
4399
- import { appendFileSync as appendFileSync5, existsSync as existsSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync7, writeFileSync as writeFileSync11 } from "node:fs";
4548
+ import { appendFileSync as appendFileSync5, existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync8, writeFileSync as writeFileSync11 } from "node:fs";
4400
4549
  import { execFileSync as execFileSync4 } from "node:child_process";
4401
4550
  import { randomBytes as randomBytes3 } from "node:crypto";
4402
4551
  import { basename as basename7, join as join16, relative as relative5, resolve as resolve9, sep as sep7 } from "node:path";
@@ -4408,7 +4557,7 @@ function append2(e) {
4408
4557
  `, { mode: 384 });
4409
4558
  }
4410
4559
  function fold() {
4411
- if (!existsSync14(file()))
4560
+ if (!existsSync15(file()))
4412
4561
  return [];
4413
4562
  const map = new Map;
4414
4563
  for (const line of readFileSync13(file(), "utf8").split(`
@@ -4520,7 +4669,7 @@ function compact(opts = {}) {
4520
4669
  const cutoff = now.getTime() - days * 86400000;
4521
4670
  return withLock("claims", () => {
4522
4671
  const f = file();
4523
- if (!existsSync14(f))
4672
+ if (!existsSync15(f))
4524
4673
  return { before: 0, after: 0, dropped: 0 };
4525
4674
  const raw = readFileSync13(f, "utf8");
4526
4675
  const lines = raw.split(`
@@ -4560,26 +4709,50 @@ function compact(opts = {}) {
4560
4709
  writeFileSync11(tmp, kept.join(`
4561
4710
  `) + (kept.length ? `
4562
4711
  ` : ""), { mode: 384 });
4563
- renameSync7(tmp, f);
4712
+ renameSync8(tmp, f);
4564
4713
  }
4565
- const after = existsSync14(f) ? readFileSync13(f, "utf8").length : 0;
4714
+ const after = existsSync15(f) ? readFileSync13(f, "utf8").length : 0;
4566
4715
  return { before: raw.length, after, dropped };
4567
4716
  });
4568
4717
  }
4569
4718
 
4570
4719
  // src/task.ts
4571
4720
  init_paths();
4572
- import { appendFileSync as appendFileSync6, existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync14 } from "node:fs";
4721
+ import { appendFileSync as appendFileSync6, existsSync as existsSync16, mkdirSync as mkdirSync14, readFileSync as readFileSync14 } from "node:fs";
4573
4722
  import { join as join17 } from "node:path";
4574
4723
  import { createHash as createHash9 } from "node:crypto";
4575
4724
  var DATA5 = () => diaHome();
4725
+ var PRIO_RE = /^P\d{1,2}$/;
4726
+ var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
4727
+ function normPrio(v) {
4728
+ const s = String(v).trim().toUpperCase();
4729
+ if (!PRIO_RE.test(s))
4730
+ throw new Error(`E_TASK: prio 必须是 P0–P99(收到 ${v})`);
4731
+ return s;
4732
+ }
4733
+ function normDay(v) {
4734
+ const s = String(v).trim();
4735
+ if (DAY_RE.test(s)) {
4736
+ const [y, mo, d] = s.split("-").map(Number);
4737
+ const dt = new Date(y, mo - 1, d);
4738
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d)
4739
+ return s;
4740
+ }
4741
+ throw new Error(`E_TASK: 日期必须是有效 YYYY-MM-DD(收到 ${v})`);
4742
+ }
4743
+ function normProgress(v) {
4744
+ const n = Number(v);
4745
+ if (!Number.isInteger(n) || n < 0 || n > 100)
4746
+ throw new Error(`E_TASK: progress 必须是 0–100 的整数(收到 ${v})`);
4747
+ return n;
4748
+ }
4576
4749
  function file2() {
4577
4750
  return join17(DATA5(), "tasks", "ledger.jsonl");
4578
4751
  }
4579
4752
  var uniq = (xs) => [...new Set(xs.filter(Boolean))];
4580
4753
  function fold2() {
4581
4754
  const map = new Map;
4582
- if (!existsSync15(file2()))
4755
+ if (!existsSync16(file2()))
4583
4756
  return map;
4584
4757
  for (const line of readFileSync14(file2(), "utf8").split(`
4585
4758
  `)) {
@@ -4603,10 +4776,39 @@ function fold2() {
4603
4776
  deps: uniq(e.deps || []),
4604
4777
  touches: uniq(e.touches || []),
4605
4778
  created_at: e.ts,
4606
- updated_at: e.ts
4779
+ updated_at: e.ts,
4780
+ ...readPlan(e)
4607
4781
  });
4608
4782
  continue;
4609
4783
  }
4784
+ if (e.event === "split" && e.parent && Array.isArray(e.children)) {
4785
+ const parent = map.get(e.parent);
4786
+ if (parent) {
4787
+ for (const c of e.children) {
4788
+ if (!c || !c.id || map.has(c.id))
4789
+ continue;
4790
+ map.set(c.id, {
4791
+ id: c.id,
4792
+ title: c.title || c.id,
4793
+ role: c.role || "builder",
4794
+ judge: c.judge || "",
4795
+ status: "todo",
4796
+ evidence: "",
4797
+ deps: [],
4798
+ touches: uniq(c.touches || []),
4799
+ created_at: e.ts,
4800
+ updated_at: e.ts,
4801
+ split_from: e.parent,
4802
+ ...c.judge ? { judge_inherited: true } : {},
4803
+ ...e.slice && e.slice === c.id ? { slice: true } : {}
4804
+ });
4805
+ }
4806
+ const ids = e.children.map((c) => c.id).filter((id) => map.has(id));
4807
+ parent.deps = uniq([...parent.deps, ...ids]);
4808
+ parent.split_children = uniq([...parent.split_children || [], ...ids]);
4809
+ }
4810
+ continue;
4811
+ }
4610
4812
  const t = map.get(e.id);
4611
4813
  if (!t)
4612
4814
  continue;
@@ -4614,9 +4816,16 @@ function fold2() {
4614
4816
  const remove = new Set([...e.remove_deps || [], ...e.remove_touches || []]);
4615
4817
  t.deps = uniq([...t.deps, ...e.deps || []]).filter((x) => !remove.has(x));
4616
4818
  t.touches = uniq([...t.touches, ...e.touches || []]).filter((x) => !remove.has(x));
4819
+ } else if (e.event === "plan") {
4820
+ Object.assign(t, readPlan(e));
4617
4821
  } else {
4618
- if (e.status)
4822
+ if (e.status) {
4619
4823
  t.status = e.status;
4824
+ if (e.status === "done" || e.status === "accepted")
4825
+ t.closed_at = t.closed_at || e.ts;
4826
+ else
4827
+ t.closed_at = undefined;
4828
+ }
4620
4829
  if (e.evidence !== undefined)
4621
4830
  t.evidence = e.evidence;
4622
4831
  if (e.closure_reason !== undefined)
@@ -4643,10 +4852,87 @@ function add(title, opts = {}) {
4643
4852
  judge: opts.judge || "",
4644
4853
  owner: opts.owner,
4645
4854
  deps: uniq(opts.deps || []),
4646
- touches: uniq(opts.touches || [])
4855
+ touches: uniq(opts.touches || []),
4856
+ ...planFields(opts)
4647
4857
  });
4648
4858
  return fold2().get(id);
4649
4859
  }
4860
+ function planFields(o) {
4861
+ const out = {};
4862
+ if (o.prio !== undefined)
4863
+ out.prio = normPrio(o.prio);
4864
+ if (o.start !== undefined)
4865
+ out.start = normDay(o.start);
4866
+ if (o.due !== undefined)
4867
+ out.due = normDay(o.due);
4868
+ if (o.progress !== undefined)
4869
+ out.progress = normProgress(o.progress);
4870
+ return out;
4871
+ }
4872
+ function readPlan(e) {
4873
+ const out = {};
4874
+ try {
4875
+ if (e.prio !== undefined && PRIO_RE.test(String(e.prio).toUpperCase()))
4876
+ out.prio = String(e.prio).toUpperCase();
4877
+ } catch {}
4878
+ try {
4879
+ if (e.start !== undefined && DAY_RE.test(String(e.start)))
4880
+ out.start = String(e.start);
4881
+ } catch {}
4882
+ try {
4883
+ if (e.due !== undefined && DAY_RE.test(String(e.due)))
4884
+ out.due = String(e.due);
4885
+ } catch {}
4886
+ try {
4887
+ const n = Number(e.progress);
4888
+ if (e.progress !== undefined && Number.isInteger(n) && n >= 0 && n <= 100)
4889
+ out.progress = n;
4890
+ } catch {}
4891
+ return out;
4892
+ }
4893
+ function plan(id, patch) {
4894
+ const cur = get2(id);
4895
+ const fields = planFields(patch);
4896
+ if (!Object.keys(fields).length)
4897
+ throw new Error("E_TASK: plan 至少需要一个字段(--prio/--start/--due/--progress)");
4898
+ const start = fields.start ?? cur.start, due = fields.due ?? cur.due;
4899
+ if (start && due && start > due)
4900
+ throw new Error("E_TASK: start 不能晚于 due");
4901
+ append3({ event: "plan", id, ...fields });
4902
+ return get2(id);
4903
+ }
4904
+ var SATISFIED = ["done", "accepted", "dropped"];
4905
+ function isSatisfied(t) {
4906
+ return !!t && SATISFIED.includes(t.status);
4907
+ }
4908
+ function isTerminal2(t) {
4909
+ return !!t && ["done", "accepted", "dropped", "rejected"].includes(t.status);
4910
+ }
4911
+ function split(parentId, titles, opts = {}) {
4912
+ const parent = get2(parentId);
4913
+ if (isTerminal2(parent))
4914
+ throw new Error("E_TASK: 终态任务不能拆分");
4915
+ if (parent.status === "doing" && !opts.force)
4916
+ throw new Error("E_TASK: 进行中的任务拆分需 --force(拆分后会先等子任务)");
4917
+ const names = titles.map((x) => x.trim()).filter(Boolean);
4918
+ if (names.length < 2)
4919
+ throw new Error("E_TASK: 至少拆成 2 个切片");
4920
+ if (new Set(names).size !== names.length)
4921
+ throw new Error("E_TASK: 切片标题重复");
4922
+ if (opts.slice && !names.includes(opts.slice))
4923
+ throw new Error("E_TASK: --slice 必须是某个切片标题");
4924
+ const judge = opts.judge ?? parent.judge;
4925
+ const children = names.map((title) => ({
4926
+ id: "t-" + createHash9("sha1").update(title + Date.now() + Math.random()).digest("hex").slice(0, 8),
4927
+ title,
4928
+ role: opts.role || parent.role,
4929
+ judge,
4930
+ touches: uniq(opts.touches ?? parent.touches)
4931
+ }));
4932
+ const sliceId = opts.slice ? children[names.indexOf(opts.slice)].id : undefined;
4933
+ append3({ event: "split", parent: parentId, children, ...sliceId ? { slice: sliceId } : {} });
4934
+ return get2(parentId);
4935
+ }
4650
4936
  function list4() {
4651
4937
  return [...fold2().values()];
4652
4938
  }
@@ -4688,11 +4974,258 @@ function status3(id, status, evidence, opts = {}) {
4688
4974
  return get2(id);
4689
4975
  }
4690
4976
 
4977
+ // src/plan.ts
4978
+ var NO_PRIO = "P99";
4979
+ var TERMINAL3 = new Set(["done", "accepted", "dropped", "rejected"]);
4980
+ var SATISFIED2 = new Set(["done", "accepted", "dropped"]);
4981
+ var PROPAGATE = new Set(["P0", "P1"]);
4982
+ var isTerminal3 = (t) => TERMINAL3.has(t.status);
4983
+ var isSatisfied2 = (t) => !!t && SATISFIED2.has(t.status);
4984
+ function prioIndex(p) {
4985
+ const m = /^P(\d{1,2})$/.exec(String(p ?? "").toUpperCase());
4986
+ return m ? Number(m[1]) : 99;
4987
+ }
4988
+ function prioName(idx) {
4989
+ return idx >= 99 ? NO_PRIO : `P${idx}`;
4990
+ }
4991
+ function graphOf(tasks) {
4992
+ const byId = new Map(tasks.map((t) => [t.id, t]));
4993
+ const dependents = new Map;
4994
+ for (const t of tasks)
4995
+ for (const d of t.deps)
4996
+ if (byId.has(d))
4997
+ (dependents.get(d) ?? dependents.set(d, []).get(d)).push(t.id);
4998
+ return { byId, dependents };
4999
+ }
5000
+ function findCycles(tasks) {
5001
+ const byId = new Map(tasks.map((t) => [t.id, t]));
5002
+ const state = new Map;
5003
+ const stack = [];
5004
+ const cycles = [];
5005
+ const seen = new Set;
5006
+ const visit = (id) => {
5007
+ const st = state.get(id) || 0;
5008
+ if (st === 1) {
5009
+ const i = stack.indexOf(id);
5010
+ const cyc = i >= 0 ? stack.slice(i) : [id];
5011
+ const key = [...cyc].sort().join(",");
5012
+ if (!seen.has(key)) {
5013
+ seen.add(key);
5014
+ cycles.push(cyc);
5015
+ }
5016
+ return;
5017
+ }
5018
+ if (st === 2)
5019
+ return;
5020
+ state.set(id, 1);
5021
+ stack.push(id);
5022
+ for (const dep of byId.get(id)?.deps || [])
5023
+ if (byId.has(dep))
5024
+ visit(dep);
5025
+ stack.pop();
5026
+ state.set(id, 2);
5027
+ };
5028
+ for (const t of tasks)
5029
+ visit(t.id);
5030
+ return cycles.sort((a, b) => a.join(",").localeCompare(b.join(",")));
5031
+ }
5032
+ function computeEff(tasks, dependents) {
5033
+ const byId = new Map(tasks.map((t) => [t.id, t]));
5034
+ const eff = new Map;
5035
+ for (const t of tasks)
5036
+ eff.set(t.id, { idx: prioIndex(t.prio) });
5037
+ for (const src of tasks) {
5038
+ const idx = prioIndex(src.prio);
5039
+ if (isTerminal3(src) || !PROPAGATE.has(prioName(idx)))
5040
+ continue;
5041
+ const seen = new Map([[src.id, 0]]);
5042
+ const queue = [src.id];
5043
+ while (queue.length) {
5044
+ const cur = queue.shift();
5045
+ const dist = seen.get(cur);
5046
+ for (const up of byId.get(cur)?.deps || []) {
5047
+ if (!byId.has(up) || seen.has(up))
5048
+ continue;
5049
+ const upTask = byId.get(up);
5050
+ if (isTerminal3(upTask))
5051
+ continue;
5052
+ seen.set(up, dist + 1);
5053
+ queue.push(up);
5054
+ }
5055
+ }
5056
+ for (const [id, distance] of seen) {
5057
+ if (id === src.id)
5058
+ continue;
5059
+ const cur = eff.get(id);
5060
+ const cand = { id: src.id, declared: prioName(idx), distance };
5061
+ if (idx < cur.idx)
5062
+ eff.set(id, { idx, src: cand });
5063
+ else if (idx === cur.idx && cur.src) {
5064
+ const better = cand.distance < cur.src.distance || cand.distance === cur.src.distance && cand.id < cur.src.id;
5065
+ if (better)
5066
+ eff.set(id, { idx, src: cand });
5067
+ }
5068
+ }
5069
+ }
5070
+ return eff;
5071
+ }
5072
+ function longestDepth(tasks, byId, cyclic) {
5073
+ const memo = new Map;
5074
+ const visit = (id, stack) => {
5075
+ if (memo.has(id))
5076
+ return memo.get(id);
5077
+ if (stack.has(id))
5078
+ return 0;
5079
+ const t = byId.get(id);
5080
+ if (!t)
5081
+ return 0;
5082
+ stack.add(id);
5083
+ let d = 0;
5084
+ for (const dep of t.deps)
5085
+ if (byId.has(dep) && !cyclic.has(dep))
5086
+ d = Math.max(d, visit(dep, stack) + 1);
5087
+ stack.delete(id);
5088
+ memo.set(id, d);
5089
+ return d;
5090
+ };
5091
+ for (const t of tasks)
5092
+ visit(t.id, new Set);
5093
+ return memo;
5094
+ }
5095
+ function transitiveDependents(tasks, dependents) {
5096
+ const out = new Map;
5097
+ for (const t of tasks) {
5098
+ const seen = new Set;
5099
+ const queue = [...dependents.get(t.id) || []];
5100
+ while (queue.length) {
5101
+ const cur = queue.shift();
5102
+ if (seen.has(cur))
5103
+ continue;
5104
+ seen.add(cur);
5105
+ queue.push(...dependents.get(cur) || []);
5106
+ }
5107
+ out.set(t.id, seen);
5108
+ }
5109
+ return out;
5110
+ }
5111
+ function derive(tasks) {
5112
+ const { byId, dependents } = graphOf(tasks);
5113
+ const cycles = findCycles(tasks);
5114
+ const cyclic = new Set(cycles.flat());
5115
+ const eff = computeEff(tasks, dependents);
5116
+ const depth = longestDepth(tasks, byId, cyclic);
5117
+ const down = transitiveDependents(tasks, dependents);
5118
+ const critical = new Set;
5119
+ for (const t of tasks) {
5120
+ const idx = prioIndex(t.prio);
5121
+ if (isTerminal3(t) || !PROPAGATE.has(prioName(idx)))
5122
+ continue;
5123
+ const seen = new Set;
5124
+ const queue = [t.id];
5125
+ while (queue.length) {
5126
+ const cur = queue.shift();
5127
+ for (const dep of byId.get(cur)?.deps || []) {
5128
+ if (seen.has(dep) || !byId.has(dep))
5129
+ continue;
5130
+ seen.add(dep);
5131
+ critical.add(dep);
5132
+ queue.push(dep);
5133
+ }
5134
+ }
5135
+ }
5136
+ const byIdOut = new Map;
5137
+ for (const t of tasks) {
5138
+ const deps = t.deps;
5139
+ const dangling = deps.filter((d) => !byId.has(d));
5140
+ const unsatisfied = deps.filter((d) => byId.has(d) ? !isSatisfied2(byId.get(d)) : true);
5141
+ const released = deps.filter((d) => byId.get(d)?.status === "dropped");
5142
+ const hasCycle = cyclic.has(t.id);
5143
+ const ready = !isTerminal3(t) && !hasCycle && unsatisfied.length === 0;
5144
+ const e = eff.get(t.id);
5145
+ byIdOut.set(t.id, {
5146
+ id: t.id,
5147
+ eff_prio: prioName(e.idx),
5148
+ prio_inherited: e.idx < prioIndex(t.prio),
5149
+ ...e.src && e.idx < prioIndex(t.prio) ? { prio_source: e.src } : {},
5150
+ critical: critical.has(t.id),
5151
+ ready,
5152
+ startable: ready && t.status === "todo",
5153
+ blocked_by: unsatisfied,
5154
+ released_by: released,
5155
+ dangling_deps: dangling,
5156
+ depth: depth.get(t.id) ?? 0,
5157
+ unblocks: down.get(t.id)?.size ?? 0,
5158
+ rank: null,
5159
+ ...t.split_from ? { split_from: t.split_from } : {},
5160
+ ...t.split_children?.length ? { split_children: t.split_children } : {},
5161
+ ...t.slice ? { slice: true } : {}
5162
+ });
5163
+ }
5164
+ const nodes = tasks.filter((t) => !isTerminal3(t) && !cyclic.has(t.id));
5165
+ const indeg = new Map;
5166
+ for (const t of nodes)
5167
+ indeg.set(t.id, t.deps.filter((d) => nodes.some((n) => n.id === d)).length);
5168
+ const readyQueue = nodes.filter((t) => (indeg.get(t.id) ?? 0) === 0);
5169
+ const key = (t) => {
5170
+ const d = byIdOut.get(t.id);
5171
+ return [
5172
+ prioIndex(d.eff_prio),
5173
+ t.due ?? "9999-12-31",
5174
+ d.critical ? 0 : 1,
5175
+ d.slice ? 0 : 1,
5176
+ -d.unblocks,
5177
+ d.depth,
5178
+ t.created_at,
5179
+ t.id
5180
+ ];
5181
+ };
5182
+ const cmp = (a, b) => {
5183
+ const ka = key(a), kb = key(b);
5184
+ for (let i = 0;i < ka.length; i++) {
5185
+ if (ka[i] < kb[i])
5186
+ return -1;
5187
+ if (ka[i] > kb[i])
5188
+ return 1;
5189
+ }
5190
+ return 0;
5191
+ };
5192
+ let rank = 0;
5193
+ const pending = [...readyQueue].sort(cmp);
5194
+ while (pending.length) {
5195
+ const t = pending.shift();
5196
+ byIdOut.get(t.id).rank = ++rank;
5197
+ for (const d of dependents.get(t.id) || []) {
5198
+ const n = indeg.get(d);
5199
+ if (n === undefined)
5200
+ continue;
5201
+ indeg.set(d, n - 1);
5202
+ if (n - 1 === 0)
5203
+ pending.push(byId.get(d));
5204
+ }
5205
+ pending.sort(cmp);
5206
+ }
5207
+ const lint = [];
5208
+ for (const t of tasks) {
5209
+ const d = byIdOut.get(t.id);
5210
+ for (const x of d.dangling_deps)
5211
+ lint.push({ kind: "dangling_dep", id: t.id, detail: `依赖不存在:${x}` });
5212
+ for (const x of d.released_by)
5213
+ lint.push({ kind: "released_by_drop", id: t.id, detail: `依赖已 dropped:${x}(前提失效,确认是否还需要做)` });
5214
+ if (PROPAGATE.has(t.prio ?? "") && !isTerminal3(t) && d.depth >= 3) {
5215
+ lint.push({ kind: "long_chain", id: t.id, detail: `依赖链 ${d.depth} 跳,考虑拆分` });
5216
+ }
5217
+ }
5218
+ for (const c of cycles)
5219
+ lint.push({ kind: "cycle", id: c[0], detail: `环:${c.join(" → ")}` });
5220
+ return { byId: byIdOut, cycles, lint, ranking: { policy: "kahn-v1" } };
5221
+ }
5222
+
4691
5223
  // src/graph.ts
5224
+ var DERIVED_PROGRESS = { todo: 0, doing: 50, review: 80, done: 100, accepted: 100, blocked: 0, dropped: 0, rejected: 0, escalated: 50, awaiting_acceptance: 90 };
4692
5225
  var taskNode = (id) => `task:${id}`;
4693
5226
  var sessionNode = (id) => `session:${id}`;
4694
5227
  var pathNode = (p) => `path:${p}`;
4695
- function findCycles(tasks) {
5228
+ function findCycles2(tasks) {
4696
5229
  const byId = new Map(tasks.map((t) => [t.id, t]));
4697
5230
  const state = new Map;
4698
5231
  const stack = [];
@@ -4726,13 +5259,46 @@ function findCycles(tasks) {
4726
5259
  }
4727
5260
  function build(opts = {}) {
4728
5261
  const tasks = list4();
5262
+ const derived = derive(tasks);
4729
5263
  const active = list3();
4730
5264
  const nodes = new Map;
4731
5265
  const edges = [];
4732
5266
  const byId = new Map(tasks.map((t) => [t.id, t]));
4733
5267
  for (const t of tasks) {
4734
- const blocked = t.deps.filter((d) => byId.get(d)?.status !== "done");
4735
- nodes.set(taskNode(t.id), { id: taskNode(t.id), kind: "task", label: t.title, status: t.status, owner: t.owner, ...blocked.length ? { blocked_by: blocked } : {} });
5268
+ const blocked = t.deps.filter((d) => !isSatisfied(byId.get(d)));
5269
+ const d = derived.byId.get(t.id);
5270
+ nodes.set(taskNode(t.id), {
5271
+ id: taskNode(t.id),
5272
+ kind: "task",
5273
+ label: t.title,
5274
+ status: t.status,
5275
+ owner: t.owner,
5276
+ ...blocked.length ? { blocked_by: blocked } : {},
5277
+ ...t.prio ? { prio: t.prio } : {},
5278
+ ...t.start ? { start: t.start } : {},
5279
+ ...t.due ? { due: t.due } : {},
5280
+ progress: t.progress ?? (DERIVED_PROGRESS[t.status] ?? 0),
5281
+ progress_derived: t.progress === undefined,
5282
+ created_at: t.created_at,
5283
+ updated_at: t.updated_at,
5284
+ ...t.closed_at ? { closed_at: t.closed_at } : {},
5285
+ deps: t.deps,
5286
+ touches: t.touches,
5287
+ eff_prio: d.eff_prio,
5288
+ prio_inherited: d.prio_inherited,
5289
+ ...d.prio_source ? { prio_source: d.prio_source } : {},
5290
+ critical: d.critical,
5291
+ ready: d.ready,
5292
+ startable: d.startable,
5293
+ released_by: d.released_by,
5294
+ dangling_deps: d.dangling_deps,
5295
+ depth: d.depth,
5296
+ unblocks: d.unblocks,
5297
+ rank: d.rank,
5298
+ ...d.split_from ? { split_from: d.split_from } : {},
5299
+ ...d.split_children ? { split_children: d.split_children } : {},
5300
+ ...d.slice ? { slice: true } : {}
5301
+ });
4736
5302
  if (t.owner)
4737
5303
  nodes.set(sessionNode(t.owner), { id: sessionNode(t.owner), kind: "session", label: t.owner });
4738
5304
  for (const d of t.deps) {
@@ -4773,7 +5339,7 @@ function build(opts = {}) {
4773
5339
  }
4774
5340
  filteredNodes.sort((a, b) => a.id.localeCompare(b.id));
4775
5341
  filteredEdges.sort((a, b) => (a.from + "\x00" + a.to + "\x00" + a.kind).localeCompare(b.from + "\x00" + b.to + "\x00" + b.kind));
4776
- return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles(tasks) };
5342
+ return { schema: 1, capabilities: ["prio", "timeline", "progress", "eff-prio", "ready", "rank", "split"], ranking: derived.ranking, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles2(tasks) };
4777
5343
  }
4778
5344
 
4779
5345
  // src/graph-tui.ts
@@ -4861,12 +5427,848 @@ function watch(build, opts = {}) {
4861
5427
  return new Promise(() => {});
4862
5428
  }
4863
5429
 
5430
+ // src/graph-html/template.html
5431
+ var template_default = `<!doctype html>
5432
+ <html lang="zh">
5433
+ <head>
5434
+ <meta charset="utf-8">
5435
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5436
+ <title>dia graph · 快照</title>
5437
+ <style>
5438
+ :root{
5439
+ --bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
5440
+ --blue:#2563eb; --violet:#7c3aed; --cyan:#0891b2; --green:#16a34a; --red:#dc2626; --amber:#d97706;
5441
+ --shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
5442
+ }
5443
+ @media (prefers-color-scheme: dark){
5444
+ :root{ --bg:#0f1115; --panel:#171a21; --ink:#e5e7eb; --muted:#9ca3af; --line:#2a2f3a;
5445
+ --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.25); }
5446
+ }
5447
+ *{box-sizing:border-box}
5448
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
5449
+ header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap}
5450
+ header h1{font-size:17px;margin:0;letter-spacing:.2px}
5451
+ header .sub{color:var(--muted);font-size:12px}
5452
+ header .spacer{flex:1}
5453
+ .tabs{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
5454
+ .tabs button{border:0;background:transparent;color:var(--muted);padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
5455
+ .tabs button.on{background:var(--blue);color:#fff}
5456
+ .pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
5457
+ main{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
5458
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
5459
+ .toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
5460
+ .seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
5461
+ .seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
5462
+ .seg button.on{background:var(--blue);color:#fff}
5463
+ .seg button:disabled{opacity:.35;cursor:not-allowed}
5464
+ .win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
5465
+ .search{position:relative;display:flex;align-items:center;gap:4px;flex-wrap:wrap;border:1px solid var(--line);background:var(--bg);border-radius:8px;padding:4px 8px;min-width:240px;max-width:420px;cursor:text}
5466
+ .search input{border:0;background:transparent;color:var(--ink);padding:2px 0;font:inherit;font-size:12px;flex:1;min-width:80px;outline:none}
5467
+ .token{display:inline-flex;align-items:center;gap:4px;background:color-mix(in srgb,var(--blue) 22%,transparent);color:var(--blue);border-radius:999px;padding:2px 4px 2px 8px;font-size:11px;white-space:nowrap}
5468
+ .token button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:0 2px}
5469
+ .search .q-list{position:absolute;left:0;top:calc(100% + 4px);z-index:20;width:300px;max-height:250px;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);display:none}
5470
+ .search .q-list.open{display:block}
5471
+ .q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
5472
+ .q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
5473
+ .q-item .k{color:var(--muted)}
5474
+ .q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
5475
+ .q-item.sel{color:var(--blue)}
5476
+ .q-item.sel .k{color:var(--blue)}
5477
+ .q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
5478
+ .q-empty{padding:10px;font-size:12px;color:var(--muted)}
5479
+ .node.hit{outline:2px solid var(--blue);outline-offset:1px}
5480
+ .node.dim{opacity:.22}
5481
+ .focus-chip{display:none;align-items:center;gap:4px;border:1px solid var(--blue);color:var(--blue);border-radius:999px;padding:3px 4px 3px 10px;font-size:11px}
5482
+ .focus-chip.on{display:inline-flex}
5483
+ .focus-chip button{border:0;background:transparent;color:var(--blue);cursor:pointer;font:inherit;font-size:11px;padding:0 6px}
5484
+ .focus-chip .fd{border:0;background:transparent;color:var(--blue);font:inherit;font-size:11px}
5485
+ .toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
5486
+ .canvas{position:relative;height:620px;overflow:hidden}
5487
+ .canvas-scroll{position:absolute;inset:0;overflow:auto;padding:10px}
5488
+ .lane-rail{position:absolute;left:0;top:0;bottom:0;width:110px;z-index:6;pointer-events:none;background:linear-gradient(90deg,var(--panel) 86%,transparent)}
5489
+ svg.edges{position:absolute;inset:0;pointer-events:none;overflow:visible}
5490
+ .node{position:absolute;width:190px;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:10px;background:var(--panel);padding:8px 10px;box-shadow:var(--shadow);cursor:pointer}
5491
+ .node.path{border-left-color:var(--violet)}
5492
+ .node.session{border-left-color:var(--cyan)}
5493
+ .node b{display:block;font-size:13px;line-height:1.3;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5494
+ .node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5495
+ .progress{height:4px;border-radius:99px;background:var(--line);margin-top:6px;overflow:hidden}
5496
+ .progress i{display:block;height:100%;background:var(--blue);background-image:linear-gradient(45deg,rgba(255,255,255,.28) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.28) 50%,rgba(255,255,255,.28) 75%,transparent 75%);background-size:12px 12px;animation:flow 1s linear infinite}
5497
+ .progress i.blocked{background-color:var(--red);animation-play-state:paused;opacity:.65}
5498
+ .progress i.review{background-color:var(--amber)}
5499
+ .progress i.done,.progress i.accepted{background-color:var(--muted);background-image:none;animation:none}
5500
+ .progress i.todo{background-color:var(--muted);background-image:none;animation:none}
5501
+ @keyframes flow{to{background-position:12px 0}}
5502
+ .st{font-weight:500}
5503
+ .st.doing{color:var(--ink)} .st.blocked{color:var(--red)} .st.review{color:var(--amber)}
5504
+ .st.done,.st.accepted{color:var(--muted)} .st.todo{color:var(--muted)}
5505
+ .prio{float:right;font-size:10px;font-weight:600;border-radius:5px;padding:0 6px;color:#fff;margin-left:4px}
5506
+ .prio{background:transparent;border:1px solid var(--line)}
5507
+ .prio.p0{background:var(--red);border-color:var(--red);color:#fff}
5508
+ .prio.p1{border-color:var(--amber);color:var(--amber)}
5509
+ .prio.p2{border-color:var(--line);color:var(--muted)}
5510
+ .pgroup{position:absolute;left:0;pointer-events:none;border-bottom:1px dashed var(--line)}
5511
+ .plane-label{position:absolute;left:20px;width:78px;z-index:6;pointer-events:none;padding:2px 6px 2px 0;background:linear-gradient(90deg,var(--panel) 72%,transparent);transform:translateY(-50%);font-size:13px;font-weight:700;letter-spacing:.1em}
5512
+ .plane-label small{display:block;font-size:9px;font-weight:400;letter-spacing:0;opacity:.7;margin-top:2px}
5513
+ .legend-overlay{position:absolute;right:12px;bottom:12px;z-index:5;background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);border-radius:10px;padding:8px 10px;box-shadow:var(--shadow);font-size:11px;color:var(--muted);display:grid;gap:5px;min-width:190px}
5514
+ .legend-overlay .row{display:flex;align-items:center;gap:8px;white-space:nowrap}
5515
+ .legend-overlay .sample{width:38px;height:10px;flex:0 0 38px}
5516
+ .legend-overlay b{color:var(--ink);font-weight:600}
5517
+ .canvas.simple .legend-overlay .detail-row{display:none}
5518
+ aside{display:flex;flex-direction:column;gap:14px}
5519
+ .side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
5520
+ .side-title h2{font-size:13px;margin:0}
5521
+ .side-title span{font-size:11px;color:var(--muted)}
5522
+ .list{padding:6px 14px 12px}
5523
+ .item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
5524
+ .item:last-child{border-bottom:0}
5525
+ .item .k{color:var(--muted)}
5526
+ .attention{padding:8px 14px 14px}
5527
+ .attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
5528
+ .attention .row:last-child{border-bottom:0}
5529
+ .tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
5530
+ .tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
5531
+ .attention .row{cursor:pointer}
5532
+ .quick{display:flex;gap:6px;margin-top:6px}
5533
+ .quick button,.btn{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:7px;padding:3px 8px;font:inherit;font-size:11px;cursor:pointer}
5534
+ .quick button.primary,.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}
5535
+ .backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .18s;z-index:40}
5536
+ .backdrop.show{opacity:1;pointer-events:auto}
5537
+ .drawer{position:fixed;top:0;right:0;bottom:0;width:440px;max-width:92vw;background:var(--panel);border-left:1px solid var(--line);box-shadow:-12px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .2s ease;z-index:50;display:flex;flex-direction:column}
5538
+ .drawer.show{transform:none}
5539
+ .drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
5540
+ .drawer-head h3{font-size:15px;margin:0 0 3px}
5541
+ .drawer-head .meta{color:var(--muted);font-size:11px}
5542
+ .drawer-head .close{margin-left:auto;border:0;background:transparent;color:var(--muted);font-size:18px;cursor:pointer}
5543
+ .drawer-tabs{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--line)}
5544
+ .drawer-tabs button{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px;cursor:pointer}
5545
+ .drawer-tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff}
5546
+ .drawer-body{padding:14px 16px 24px;overflow:auto}
5547
+ .drawer-body h4{font-size:12px;margin:14px 0 6px;color:var(--muted);letter-spacing:.05em;text-transform:uppercase}
5548
+ .kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
5549
+ .kv .k{color:var(--muted)}
5550
+ .iv-seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:12px}
5551
+ .iv-seg button{flex:1;border:0;background:transparent;color:var(--muted);padding:7px 8px;font:inherit;font-size:12px;cursor:pointer}
5552
+ .iv-seg button.on{background:var(--blue);color:#fff}
5553
+ .iv-row{display:grid;gap:6px;margin-bottom:10px}
5554
+ .iv-row label{font-size:11px;color:var(--muted)}
5555
+ .iv-row select,.iv-row textarea{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);padding:8px;font:inherit;font-size:12px}
5556
+ .iv-row textarea{min-height:96px;resize:vertical}
5557
+ .iv-status{color:var(--green);font-size:11px;margin-left:8px}
5558
+ .rel-list .item{display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line);font-size:12px}
5559
+ .rel-list .item:last-child{border-bottom:0}
5560
+ .rel-list .k{color:var(--muted)}
5561
+ .kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
5562
+ .kv .k{color:var(--muted)}
5563
+ /* timeline */
5564
+ .timeline{padding:14px 16px 18px;overflow:hidden;position:relative}
5565
+ .tl-grid{display:grid;grid-template-columns:190px minmax(0,1fr);gap:8px;align-items:start}
5566
+ .tl-labels{overflow:hidden;position:relative}
5567
+ .tl-label-head{height:34px;display:flex;align-items:flex-end;font-size:11px;color:var(--muted);padding-bottom:6px}
5568
+ .tl-labels-body{will-change:transform}
5569
+ .tl-label-row{height:38px;box-sizing:border-box;padding:6px 8px 4px 0;border-top:1px dashed var(--line);font-size:12px;overflow:hidden}
5570
+ .tl-label-row b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5571
+ .tl-label-row span{color:var(--muted);font-size:10px}
5572
+ .tl-scroll{overflow:auto;max-height:620px;scrollbar-width:thin}
5573
+ .scroll-hint{position:absolute;right:18px;top:10px;z-index:6;background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 9px;font-size:10px;color:var(--muted);box-shadow:var(--shadow)}
5574
+ .tl-head{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:end;color:var(--muted);font-size:11px;margin-bottom:8px;position:sticky;top:0;z-index:4;background:var(--panel)}
5575
+ .tl-axis{position:relative;height:34px;width:var(--tlw,900px)}
5576
+ .tl-axis span{position:absolute;transform:translateX(-50%);white-space:nowrap;line-height:1.2}
5577
+ .tl-axis span small{display:block;font-size:9px;opacity:.55}
5578
+ .tl-body{position:relative}
5579
+ .tl-row{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:start;min-height:38px;border-top:1px dashed var(--line)}
5580
+ .tl-track{position:relative;height:28px;overflow:hidden;width:var(--tlw,900px);margin-top:4px}
5581
+ .tl-bar{position:absolute;top:7px;height:14px;border-radius:5px;background:var(--blue);opacity:.9;cursor:pointer}
5582
+ .tl-bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:rgba(255,255,255,.45)}
5583
+ .tl-bar.blocked{background:var(--red)} .tl-bar.done,.tl-bar.accepted,.tl-bar.todo{background:var(--muted)}
5584
+ .tl-bar.review{background:var(--amber)}
5585
+ .tl-bar.critical{outline:2px solid var(--amber);outline-offset:2px}
5586
+ .tl-bar.open{border-right:2px dashed var(--muted);opacity:.8}
5587
+ .tl-today{position:absolute;top:0;bottom:0;pointer-events:none;z-index:2;border-left:1px dashed var(--red);color:var(--red);font-size:10px;padding-left:4px}
5588
+ .tl-caption{color:var(--muted);font-size:12px;padding:10px 0 2px}
5589
+ footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
5590
+ @media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
5591
+ </style>
5592
+ </head>
5593
+ <body>
5594
+ <header>
5595
+ <h1>dia graph</h1>
5596
+ <span class="sub">只读依赖图 + 时间线 · 静态快照</span>
5597
+ <div class="tabs" id="tabs">
5598
+ <button class="on" data-v="graph">关系图</button>
5599
+ <button data-v="time">时间线</button>
5600
+ </div>
5601
+ <span class="spacer"></span>
5602
+ <span class="pill" id="trunc-pill" style="display:none"></span>
5603
+ <span class="pill">静态快照 · 无网络</span>
5604
+ <span class="pill" id="route">#/graph</span>
5605
+ <span class="pill" id="updated">-</span>
5606
+ </header>
5607
+
5608
+ <main>
5609
+ <section class="panel">
5610
+ <div class="toolbar">
5611
+ <div id="graph-tools" style="display:flex;gap:8px;align-items:center">
5612
+ <div class="seg" id="layout">
5613
+ <button class="on" data-l="priority">优先级视图</button>
5614
+ <button data-l="dep">依赖视图</button>
5615
+ </div>
5616
+ <div class="seg" id="mode">
5617
+ <button class="on" data-m="simple">简约</button>
5618
+ <button data-m="detail">详细</button>
5619
+ </div>
5620
+ <div class="search" id="search-box">
5621
+ <span id="tokens" style="display:contents"></span>
5622
+ <input id="q" type="search" placeholder="搜索任务,选中后变成 tag" autocomplete="off">
5623
+ <div class="q-list" id="q-list"></div>
5624
+ </div>
5625
+ <span class="focus-chip" id="focus-chip"><select id="focus-fd" class="fd"><option value="1">1 层</option><option value="2" selected>2 层</option><option value="all">全链路</option></select><button id="focus-exit">清空</button></span>
5626
+ </div>
5627
+ <div id="time-tools" style="display:none;gap:8px;align-items:center">
5628
+ <div class="seg" id="zoom">
5629
+ <button data-z="-">-</button>
5630
+ <button data-z="reset">100%</button>
5631
+ <button data-z="+">+</button>
5632
+ </div>
5633
+ </div>
5634
+ <select class="win-select" id="win" title="按任务开始时间过滤,结束时间不限(两个视图共用)">
5635
+ <option value="7">近 7 天</option>
5636
+ <option value="14">近 14 天</option>
5637
+ <option value="30">近 30 天</option>
5638
+ <option value="0">全部时间</option>
5639
+ </select>
5640
+ <span class="hint" id="hint">按优先级分行(P0 最高);行内从左到右 = 依赖顺序。简约只看任务和依赖,详细展开具体关联。</span>
5641
+ </div>
5642
+ <div id="view-graph">
5643
+ <div class="canvas" id="canvas">
5644
+ <div class="canvas-scroll" id="canvas-scroll">
5645
+ <svg class="edges" id="edges"></svg>
5646
+ </div>
5647
+ <div class="lane-rail" id="lane-rail"></div>
5648
+ <div class="legend-overlay" id="legend">
5649
+ <div class="row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-dep" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#2563eb"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#2563eb" stroke-width="1.6" marker-end="url(#lg-dep)"/></svg><span><b>→ depends_on</b> 单向</span></div>
5650
+ </div>
5651
+ </div>
5652
+ </div>
5653
+ <div id="view-time" style="display:none">
5654
+ <div class="timeline">
5655
+ <div class="scroll-hint">← 左右滚动 →</div>
5656
+ <div class="tl-grid">
5657
+ <div class="tl-labels">
5658
+ <div class="tl-label-head">任务</div>
5659
+ <div class="tl-labels-body" id="tl-labels-body"></div>
5660
+ </div>
5661
+ <div class="tl-scroll" id="tl-scroll">
5662
+ <div class="tl-head"><div class="tl-axis" id="tl-axis"></div></div>
5663
+ <div class="tl-body" id="tl-body"></div>
5664
+ </div>
5665
+ </div>
5666
+ <div class="tl-caption">红色虚线 = 今天;虚线右端 = 未录入 due 的开放任务;斜纹 = 进度按状态估算。</div>
5667
+ </div>
5668
+ </div>
5669
+ </section>
5670
+
5671
+ <aside>
5672
+ <div class="panel">
5673
+ <div class="side-title"><h2>概览</h2><span>snapshot</span></div>
5674
+ <div class="list">
5675
+ <div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>任务</span><b id="stat-tasks">0</b></div>
5676
+ <div class="item"><span class="k"><i class="dot" style="background:var(--red)"></i>阻塞</span><b id="stat-blocked">0</b></div>
5677
+ <div class="item"><span class="k"><i class="dot" style="background:var(--amber)"></i>待评审</span><b id="stat-review">0</b></div>
5678
+ <div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>完成</span><b id="stat-done">0</b></div>
5679
+ <div class="item"><span class="k">claims</span><b id="stat-claims">0</b></div>
5680
+ </div>
5681
+ </div>
5682
+ <div class="panel">
5683
+ <div class="side-title"><h2>需要人</h2><span>attention</span></div>
5684
+ <div class="attention" id="attention-list"></div>
5685
+ </div>
5686
+ </aside>
5687
+ </main>
5688
+ <div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
5689
+ <aside class="drawer" id="drawer" aria-hidden="true">
5690
+ <div class="drawer-head">
5691
+ <div><h3 id="drawer-title">任务详情</h3><div class="meta" id="drawer-sub"></div></div>
5692
+ <button class="close" onclick="closeDrawer()">×</button>
5693
+ </div>
5694
+ <div class="drawer-tabs">
5695
+ <button data-dtab="task" onclick="setDrawerTab('task')">任务</button>
5696
+ <button data-dtab="intervene" onclick="setDrawerTab('intervene')">命令</button>
5697
+ </div>
5698
+ <div class="drawer-body" id="drawer-body"></div>
5699
+ </aside>
5700
+ <footer>颜色只表达警告/异常:红=阻塞/P0,橙=待评审/P1;其余状态用中性灰 + 单一蓝色进度。关系图按依赖深度分层,时间线独立展示执行情况。</footer>
5701
+
5702
+ <script type="application/json" id="dia-graph">__DIA_PAYLOAD__</script>
5703
+ <script>
5704
+ const payload=(()=>{ try{ const el=document.getElementById('dia-graph'); return JSON.parse(el?el.textContent:'{}')||{}; }catch{ return {}; } })();
5705
+ const TODAY=payload.today||'1970-01-01';
5706
+ const STATUSES=new Set(['todo','doing','review','blocked','done','accepted','dropped','awaiting_acceptance','rejected','escalated']);
5707
+ const tasks=(payload.tasks||[]).map(t=>{
5708
+ const status=STATUSES.has(t.status)?t.status:'todo';
5709
+ const progress=Number.isFinite(t.progress)?Math.max(0,Math.min(100,Math.round(t.progress))):0;
5710
+ return {
5711
+ id:String(t.id), label:String(t.label||t.id), status,
5712
+ prio:String(t.prio||'P?'), progress, derived:!!t.derived,
5713
+ start:Number.isFinite(t.start)?t.start:null,
5714
+ end:Number.isFinite(t.end)?t.end:null,
5715
+ owner:t.owner?String(t.owner):null,
5716
+ deps:Array.isArray(t.deps)?t.deps.map(String):[],
5717
+ touches:Array.isArray(t.touches)?t.touches.map(String):[],
5718
+ };
5719
+ });
5720
+ const taskEdges=(payload.edges||[]).filter(e=>e&&e.kind==='depends_on').map(e=>({from:String(e.from),to:String(e.to),kind:'depends_on'}));
5721
+ const truncated=payload.truncated||null;
5722
+ const state={view:'graph', layout:'priority', mode:'simple', zoom:28, window:7, q:'', sel:[], fd:2, from:null, to:null};
5723
+ const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
5724
+ const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
5725
+ const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○',dropped:'⨯',rejected:'✗',escalated:'▲',awaiting_acceptance:'…'};
5726
+ const stIconOf=s=>stIcon[s]||'·';
5727
+
5728
+ function focusSet(id,depth){
5729
+ const by=new Map(tasks.map(t=>[t.id,t]));
5730
+ const max=depth==='all'?Infinity:Math.max(1,Number(depth)||2);
5731
+ const out=new Set([id]); let frontier=[id];
5732
+ for(let step=0; step<max && frontier.length; step++){
5733
+ const next=[];
5734
+ frontier.forEach(cur=>{
5735
+ const t=by.get(cur); if(!t) return;
5736
+ (t.deps||[]).forEach(x=>{ if(by.has(x)&&!out.has(x)){ out.add(x); next.push(x); } });
5737
+ tasks.forEach(o=>{ if((o.deps||[]).includes(cur)&&!out.has(o.id)){ out.add(o.id); next.push(o.id); } });
5738
+ });
5739
+ frontier=next;
5740
+ }
5741
+ return out;
5742
+ }
5743
+ function visibleTasks(){
5744
+ const base=tasks.filter(t=>state.window<=0 || t.start===null || t.start>=-state.window);
5745
+ if(!state.sel.length) return base;
5746
+ const keep=new Set();
5747
+ state.sel.forEach(id=>focusSet(id,state.fd).forEach(x=>keep.add(x)));
5748
+ return base.filter(t=>keep.has(t.id));
5749
+ }
5750
+ function searchMatches(){
5751
+ const q=(state.q||'').trim().toLowerCase();
5752
+ if(!q) return null;
5753
+ return tasks.filter(t=>t.id.toLowerCase().includes(q)||t.label.toLowerCase().includes(q)||String(t.owner||'').toLowerCase().includes(q));
5754
+ }
5755
+ function syncStats(){
5756
+ const list=visibleTasks(), st=id=>document.getElementById(id);
5757
+ if(!st('stat-tasks')) return;
5758
+ st('stat-tasks').textContent = list.length===tasks.length ? String(tasks.length) : list.length+' / '+tasks.length;
5759
+ st('stat-blocked').textContent=String(list.filter(t=>t.status==='blocked').length);
5760
+ st('stat-review').textContent=String(list.filter(t=>t.status==='review').length);
5761
+ st('stat-done').textContent=String(list.filter(t=>t.status==='done'||t.status==='accepted').length);
5762
+ if(st('stat-claims')) st('stat-claims').textContent=String((payload.claims||[]).length);
5763
+ renderAttention(list);
5764
+ const tp=document.getElementById('trunc-pill');
5765
+ if(tp){ if(truncated&&truncated.shown<truncated.total){ tp.style.display=''; tp.textContent='已截断 '+truncated.shown+' / '+truncated.total+'(--all 全量)'; } else tp.style.display='none'; }
5766
+ }
5767
+ function renderAttention(list){
5768
+ const box=document.getElementById('attention-list'); if(!box) return;
5769
+ const rows=[];
5770
+ list.filter(t=>t.status==='blocked').slice(0,3).forEach(t=>rows.push({tag:'阻塞',cls:'',t,note:'blocked'}));
5771
+ list.filter(t=>t.status==='review').slice(0,3).forEach(t=>rows.push({tag:'评审',cls:' amber',t,note:'等待评审'}));
5772
+ box.innerHTML=rows.length?rows.map(r=>'<div class="row" data-task="'+esc(r.t.id)+'"><span class="tag'+r.cls+'">'+r.tag+'</span><div><b>'+esc(r.t.label)+'</b><br><span class="k">'+esc(r.t.id)+' · '+esc(r.note)+'</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer(\\''+esc(r.t.id)+'\\',\\'intervene\\')">命令</button><button onclick="event.stopPropagation();openDrawer(\\''+esc(r.t.id)+'\\',\\'task\\')">查看</button></div></div></div>').join('')
5773
+ :'<p class="k" style="padding:8px 0">当前没有阻塞或待评审任务。</p>';
5774
+ box.querySelectorAll('.row[data-task]').forEach(el=>el.onclick=()=>openDrawer(el.dataset.task,'task'));
5775
+ }
5776
+ function renderEmpty(){
5777
+ if(tasks.length) return;
5778
+ const el=document.getElementById('canvas-scroll');
5779
+ if(el) el.innerHTML='<div style="padding:60px 20px;text-align:center;color:var(--muted)">账本里还没有任务。<br>先跑 <code>dia task add &lt;title&gt; --prio P1</code>,再重新导出。</div>';
5780
+ }
5781
+ function updateHint(){
5782
+ const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
5783
+ const n=visibleTasks().length;
5784
+ if(state.sel.length){
5785
+ h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
5786
+ return;
5787
+ }
5788
+ h.textContent='显示 '+n+' / '+tasks.length+' 个任务 · '+(state.layout==='dep'?'从左到右 = 依赖深度':'按优先级分行(P0 最高)');
5789
+ }
5790
+ function depths(){
5791
+ const list=visibleTasks();
5792
+ const by=new Map(list.map(t=>[t.id,t])), memo=new Map();
5793
+ const depth=t=>{
5794
+ if(!t) return 0;
5795
+ if(memo.has(t.id)) return memo.get(t.id);
5796
+ memo.set(t.id,0);
5797
+ const d=t.deps.length?Math.max(...t.deps.map(x=>{ const dt=by.get(x); return dt?depth(dt):-1; }))+1:0;
5798
+ memo.set(t.id,d); return d;
5799
+ };
5800
+ list.forEach(depth); return memo;
5801
+ }
5802
+ function levels(){
5803
+ return [...new Set(visibleTasks().map(t=>t.prio))].sort((a,b)=>{
5804
+ const na=parseInt(String(a).replace(/^P/i,''),10), nb=parseInt(String(b).replace(/^P/i,''),10);
5805
+ return (Number.isFinite(na)?na:99)-(Number.isFinite(nb)?nb:99)||String(a).localeCompare(String(b));
5806
+ });
5807
+ }
5808
+ function rowsFor(){
5809
+ const list=visibleTasks(), d=depths();
5810
+ const gap=state.mode==='detail'?230:145;
5811
+ const h=state.mode==='detail'?205:110;
5812
+ return levels().map((prio,i)=>{
5813
+ const rowTasks=list.filter(t=>t.prio===prio).sort((a,b)=>(d.get(a.id)-d.get(b.id))||a.label.localeCompare(b.label));
5814
+ return {prio,y:36+i*gap,h,tasks:rowTasks};
5815
+ }).filter(r=>r.tasks.length);
5816
+ }
5817
+ let cardMetrics={mid:52,h:58}, clickTimer=null;
5818
+ function measureCard(){
5819
+ const probe=card(tasks[0],-9999,-9999);
5820
+ probe.style.visibility='hidden';
5821
+ layer.appendChild(probe);
5822
+ const prog=probe.querySelector('.progress');
5823
+ const mid=prog?prog.offsetTop+prog.offsetHeight/2+probe.clientTop:44;
5824
+ const h=probe.offsetHeight||58;
5825
+ probe.remove();
5826
+ return {mid,h};
5827
+ }
5828
+ function bandCenter(r, bandH){ return r.y-12+bandH/2; }
5829
+ function cardTop(r){
5830
+ const bandH=state.mode==='detail'?230:145;
5831
+ const center=bandCenter(r,bandH);
5832
+ // 详细:进度条中心线对齐泳道中心;简约:卡片盒整体垂直居中
5833
+ return state.mode==='detail'?center-cardMetrics.mid:center-cardMetrics.h/2;
5834
+ }
5835
+ function edgeOffset(){ return state.mode==='detail'?cardMetrics.mid:cardMetrics.h/2; }
5836
+ function pos(){
5837
+ const rows=rowsFor(), p=new Map();
5838
+ rows.forEach(r=>r.tasks.forEach((t,i)=>p.set(t.id,{x:170+i*230,y:cardTop(r)})));
5839
+ return {p,rows};
5840
+ }
5841
+ function card(t,x,y,showPrio=false){
5842
+ const el=document.createElement('div'); el.className='node task '+t.status; el.style.left=x+'px'; el.style.top=y+'px';
5843
+ const rel=taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
5844
+ const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
5845
+ const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
5846
+ return '<div class="meta" style="margin-top:2px">↳ '+esc(kind)+': '+esc(other)+'</div>';
5847
+ }).slice(0,4).join('');
5848
+ const badge=showPrio?'<span class="prio '+esc(t.prio.toLowerCase())+'">'+esc(t.prio)+'</span>':'';
5849
+ el.innerHTML='<b>'+esc(t.label)+badge+'</b>'+
5850
+ '<div class="meta">'+esc(t.id)+' · <span class="st '+esc(t.status)+'">'+stIconOf(t.status)+' '+esc(t.status)+'</span> · '+t.progress+'%</div>'+
5851
+ '<div class="progress"><i class="'+esc(t.status)+'" style="width:'+t.progress+'%"></i></div>'+
5852
+ (state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
5853
+ el.dataset.id=t.id;
5854
+ el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
5855
+ el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); toggleSel(t.id); };
5856
+ return el;
5857
+ }
5858
+ function edgePath(a,b){
5859
+ const o=edgeOffset(), x1=a.x+190,y1=a.y+o,x2=b.x,y2=b.y+o;
5860
+ if(x2>=x1) return \`M\${x1} \${y1} H\${x1+Math.max(24,(x2-x1)/2)} V\${y2} H\${x2}\`;
5861
+ return \`M\${x1} \${y1} H\${x1-30} V\${y2} H\${x2}\`;
5862
+ }
5863
+ function drawGraph(){
5864
+ canvas.classList.toggle('simple', state.mode==='simple');
5865
+ if(state.layout==='dep'){ drawDepGraph(); return; }
5866
+ cardMetrics=measureCard();
5867
+ const {p,rows}=pos();
5868
+ [...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
5869
+ rail.style.display=''; rail.querySelectorAll('.plane-label').forEach(e=>e.remove());
5870
+ const maxX=Math.max(600, ...rows.map(r=>170+(r.tasks.length-1)*230+190+10));
5871
+ const maxY=rows.length?Math.max(...rows.map(r=>r.y+r.h))+40:600;
5872
+ const bandH=state.mode==='detail'?230:145;
5873
+ rows.forEach((r,i)=>{
5874
+ const g=document.createElement('div'); g.className='pgroup '+r.prio.toLowerCase();
5875
+ g.style.top=(r.y-12)+'px'; g.style.width=maxX+'px'; g.style.height=bandH+'px';
5876
+ const alpha=Math.max(0.02, 0.075-i*0.012);
5877
+ g.style.background='color-mix(in srgb, var(--blue) '+Math.round(alpha*100)+'%, transparent)';
5878
+ layer.appendChild(g);
5879
+ const label=document.createElement('div'); label.className='plane-label '+r.prio.toLowerCase();
5880
+ label.dataset.y=String(r.y-12+bandH/2); label.style.top=(r.y-12+bandH/2-layer.scrollTop)+'px';
5881
+ const meta=r.prio==='P0'?['var(--red)','紧急']:r.prio==='P1'?['var(--amber)','高']:r.prio==='P?'?['var(--muted)','未排期']:['var(--muted)','常规'];
5882
+ label.style.color=meta[0];
5883
+ label.innerHTML=esc(r.prio)+' · '+r.tasks.length+'<small>'+meta[1]+'</small>';
5884
+ rail.appendChild(label);
5885
+ r.tasks.forEach((t,j)=>layer.appendChild(card(t,170+j*230,cardTop(r))));
5886
+ });
5887
+ svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
5888
+ const defs=\`<defs>
5889
+ <marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
5890
+ <marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
5891
+ </defs>\`;
5892
+ const lines=taskEdges.map(e=>{
5893
+ const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
5894
+ return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
5895
+ }).join('');
5896
+ svg.innerHTML=defs+lines;
5897
+ document.getElementById('updated').textContent='快照 '+(payload.generated_at||'-');
5898
+ syncStats(); updateHint(); applySearch();
5899
+ }
5900
+ let drawerTask=null, drawerTab='task';
5901
+ function relations(t){
5902
+ return taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
5903
+ const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
5904
+ const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
5905
+ return '<div class="item"><span class="k">'+esc(kind)+'</span><span>'+esc(other)+'</span></div>';
5906
+ }).join('') || '<p class="k">无边</p>';
5907
+ }
5908
+ function taskHtml(t){
5909
+ return '<div class="kv"><div class="k">status</div><div>'+esc(t.status)+'</div>'+
5910
+ '<div class="k">priority</div><div>'+esc(t.prio)+'</div>'+
5911
+ '<div class="k">owner</div><div>'+esc(t.owner||'-')+'</div>'+
5912
+ '<div class="k">start</div><div>'+(t.start===null?'-(按创建时间)':esc(isoDate(t.start)))+'</div>'+
5913
+ '<div class="k">due</div><div>'+(Number.isFinite(t.end)?esc(isoDate(t.end)):'开放')+'</div>'+
5914
+ '<div class="k">progress</div><div>'+t.progress+'%'+(t.derived?'(估算)':'')+'</div>'+
5915
+ '<div class="k">deps</div><div>'+esc(t.deps.join(', ')||'-')+'</div>'+
5916
+ '<div class="k">touches</div><div>'+esc(t.touches.join(', ')||'-')+'</div></div>'+
5917
+ '<h4>关联</h4><div class="rel-list">'+relations(t)+'</div>';
5918
+ }
5919
+ function commandHtml(t){
5920
+ const cmds=[
5921
+ ['dia task plan '+t.id+' --prio P1', '设置优先级'],
5922
+ ['dia task plan '+t.id+' --start '+isoDate(0)+' --due '+isoDate(7), '设置计划起止'],
5923
+ ['dia task plan '+t.id+' --progress 50', '更新进度'],
5924
+ ['dia msg send <session> "关于 '+t.id+': "', '给负责人发消息'],
5925
+ ['dia claim '+t.id, '声明正在处理'],
5926
+ ];
5927
+ return '<p class="k">本页是静态快照:命令只供复制,不会发送。</p>'+
5928
+ cmds.map(([c,l])=>'<div class="cmd"><code>'+esc(c)+'</code><button onclick="copyCmd(this)" data-cmd="'+esc(c)+'">复制</button><span class="k">'+esc(l)+'</span></div>').join('');
5929
+ }
5930
+ function copyCmd(btn){
5931
+ const text=btn.dataset.cmd||'';
5932
+ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(()=>{ btn.textContent='已复制'; },()=>{ btn.textContent='复制失败'; }); }
5933
+ else { btn.textContent='请手动复制'; }
5934
+ }
5935
+ function openDrawer(id, tab){
5936
+ drawerTask=tasks.find(t=>t.id===id); if(!drawerTask) return;
5937
+ drawerTab=tab||'task';
5938
+ document.getElementById('drawer-title').textContent='任务详情';
5939
+ document.getElementById('drawer-sub').textContent=drawerTask.label+' · '+drawerTask.id+' · '+drawerTask.prio+' · '+drawerTask.status;
5940
+ document.getElementById('drawer').classList.add('show');
5941
+ document.getElementById('drawer').setAttribute('aria-hidden','false');
5942
+ document.getElementById('backdrop').classList.add('show');
5943
+ setDrawerTab(drawerTab);
5944
+ }
5945
+ function setDrawerTab(tab){
5946
+ drawerTab=tab;
5947
+ document.querySelectorAll('[data-dtab]').forEach(b=>b.classList.toggle('on',b.dataset.dtab===tab));
5948
+ const body=document.getElementById('drawer-body');
5949
+ body.innerHTML=tab==='intervene'?commandHtml(drawerTask):taskHtml(drawerTask);
5950
+ }
5951
+ function closeDrawer(){
5952
+ document.getElementById('drawer').classList.remove('show');
5953
+ document.getElementById('drawer').setAttribute('aria-hidden','true');
5954
+ document.getElementById('backdrop').classList.remove('show');
5955
+ }
5956
+ function todayUTC(){ const [y,m,d]=TODAY.split('-').map(Number); return Date.UTC(y,(m||1)-1,d||1); }
5957
+ function dayDate(d){
5958
+ const dt=new Date(todayUTC()+d*86400000);
5959
+ return ('0'+(dt.getUTCMonth()+1)).slice(-2)+'-'+('0'+dt.getUTCDate()).slice(-2);
5960
+ }
5961
+ function isoDate(d){ return new Date(todayUTC()+d*86400000).toISOString().slice(0,10); }
5962
+ function timelineWindow(){
5963
+ if(state.from!==null&&state.to!==null) return {from:state.from,to:state.to};
5964
+ const list=visibleTasks();
5965
+ const w=state.window>0?state.window:14;
5966
+ const starts=list.map(t=>t.start).filter(Number.isFinite);
5967
+ const ends=list.map(t=>t.end).filter(Number.isFinite);
5968
+ const from=Math.min(-w, starts.length?Math.min(...starts):-w);
5969
+ const to=Math.max(0, ends.length?Math.max(...ends):0)+1;
5970
+ return {from,to};
5971
+ }
5972
+ function drawTimeline(){
5973
+ const win=timelineWindow(), span=Math.max(1,win.to-win.from);
5974
+ const x=v=>((v-win.from)/span)*100;
5975
+ const chartW=Math.max(640, span*state.zoom);
5976
+ document.getElementById('view-time').style.setProperty('--tlw', chartW+'px');
5977
+ const axis=document.getElementById('tl-axis'); axis.innerHTML='';
5978
+ const step=span<=10?1:span<=18?2:3;
5979
+ for(let d=win.from;d<=win.to;d+=step){
5980
+ const el=document.createElement('span'); el.style.left=x(d)+'%';
5981
+ el.innerHTML=dayDate(d)+'<small>'+(d===0?'今天':(d>0?'+'+d+'d':d+'d'))+'</small>';
5982
+ if(d===win.from) el.style.transform='translateX(0)'; else if(d+step>win.to) el.style.transform='translateX(-100%)';
5983
+ if(d===0)el.style.color='var(--red)'; axis.appendChild(el);
5984
+ }
5985
+ const body=document.getElementById('tl-body'); body.innerHTML=''; const labelsBody=document.getElementById('tl-labels-body'); labelsBody.innerHTML='';
5986
+ visibleTasks().slice().sort((a,b)=>a.start-b.start).forEach(t=>{
5987
+ const row=document.createElement('div'); row.className='tl-row';
5988
+ row.innerHTML='<div class="tl-track"></div>';
5989
+ const label=document.createElement('div'); label.className='tl-label-row'; label.innerHTML='<b>'+esc(t.label)+'</b><span>'+esc(t.id)+' · '+esc(t.prio)+' · '+esc(t.status)+'</span>'; labelsBody.appendChild(label);
5990
+ const track=row.querySelector('.tl-track');
5991
+ const startOff=t.start===null?win.from:t.start;
5992
+ const endOff=Number.isFinite(t.end)?t.end:win.to;
5993
+ const bar=document.createElement('div'); bar.className='tl-bar '+t.status+(Number.isFinite(t.end)?'':' open');
5994
+ bar.style.left=x(startOff)+'%'; bar.style.width=Math.max(1.5,x(endOff)-x(startOff))+'%';
5995
+ bar.title=dayDate(startOff)+' → '+(Number.isFinite(t.end)?dayDate(t.end):'开放')+' ('+t.progress+'%'+(t.derived?' 估算':'')+')';
5996
+ bar.innerHTML='<div class="fill" style="width:'+t.progress+'%"></div>'; bar.onclick=()=>openDrawer(t.id,'task'); track.appendChild(bar);
5997
+ if(win.from<=0&&win.to>=0){ const line=document.createElement('div'); line.className='tl-today'; line.style.left=x(0)+'%'; line.style.top='0'; line.style.bottom='0'; line.textContent=''; track.appendChild(line); }
5998
+ body.appendChild(row);
5999
+ });
6000
+ syncStats();
6001
+ }
6002
+ function drawDepGraph(){
6003
+ cardMetrics=measureCard();
6004
+ const list=visibleTasks(), d=depths(), groups=new Map();
6005
+ list.forEach(t=>{ const k=d.get(t.id)||0; groups.set(k,[...(groups.get(k)||[]),t]); });
6006
+ const p=new Map(); const rows=[];
6007
+ const gap=14;
6008
+ [...groups.keys()].sort((a,b)=>a-b).forEach((depth,ci)=>{
6009
+ const all=groups.get(depth).sort((a,b)=>a.prio.localeCompare(b.prio)||a.label.localeCompare(b.label));
6010
+ rows.push({depth,all,x:30+ci*230});
6011
+ });
6012
+ [...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
6013
+ rail.innerHTML=''; rail.style.display='none';
6014
+ let maxY=0;
6015
+ // 先渲染卡片,再按实测高度逐列堆叠,避免详细模式卡片相互重叠
6016
+ rows.forEach(r=>{
6017
+ let y=60;
6018
+ r.all.forEach(t=>{
6019
+ const el=card(t,r.x,y,true);
6020
+ el.style.top=y+'px';
6021
+ layer.appendChild(el);
6022
+ p.set(t.id,{x:r.x,y});
6023
+ y+=el.offsetHeight+gap;
6024
+ });
6025
+ maxY=Math.max(maxY,y);
6026
+ });
6027
+ maxY=Math.max(600,maxY);
6028
+ const maxX=Math.max(600,...rows.map(r=>r.x+200+10));
6029
+ svg.setAttribute('width',maxX); svg.setAttribute('height',maxY+40);
6030
+ const defs=\`<defs>
6031
+ <marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
6032
+ <marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
6033
+ </defs>\`;
6034
+ const lines=taskEdges.map(e=>{
6035
+ const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
6036
+ return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
6037
+ }).join('');
6038
+ svg.innerHTML=defs+lines;
6039
+ document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
6040
+ syncStats(); updateHint(); applySearch();
6041
+ }
6042
+ function syncLaneLabels(){
6043
+ rail.querySelectorAll('.plane-label').forEach(el=>{
6044
+ el.style.top=(Number(el.dataset.y||0)-layer.scrollTop)+'px';
6045
+ });
6046
+ }
6047
+ function applySearch(){
6048
+ const ms=searchMatches();
6049
+ layer.querySelectorAll('.node.task').forEach(el=>{
6050
+ el.classList.remove('hit','dim');
6051
+ if(!ms) return;
6052
+ if(ms.some(t=>t.id===el.dataset.id)) el.classList.add('hit'); else el.classList.add('dim');
6053
+ });
6054
+ const box=document.getElementById('q-list');
6055
+ if(box && box.classList.contains('open')) renderSearchList(ms);
6056
+ }
6057
+ function renderSearchList(ms){
6058
+ const box=document.getElementById('q-list'); if(!box) return;
6059
+ const q=(state.q||'').trim();
6060
+ const browse=!q;
6061
+ const pool=browse?tasks.filter(t=>state.window<=0||t.start>=-state.window).slice().sort((a,b)=>a.prio.localeCompare(b.prio)||a.start-b.start):ms;
6062
+ if(!pool||!pool.length){
6063
+ box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
6064
+ box.classList.add('open'); return;
6065
+ }
6066
+ const head=browse?('<div class="q-head">'+pool.length+' 个任务 · ↑↓ 选择,Enter 加入</div>')
6067
+ :('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 加入</div>');
6068
+ box.innerHTML=head+pool.slice(0,20).map(t=>{
6069
+ const on=state.sel.includes(t.id);
6070
+ return '<div class="q-item'+(on?' sel':'')+'" data-id="'+esc(t.id)+'"><span>'+esc(t.label)+'<i class="q-prio">'+esc(t.prio)+'</i></span><span class="k">'+(on?'✓ ':'')+esc(t.id)+'</span></div>';
6071
+ }).join('');
6072
+ box.classList.add('open');
6073
+ box.querySelectorAll('.q-item').forEach(el=>el.onclick=e=>{ e.stopPropagation(); toggleSel(el.dataset.id); });
6074
+ }
6075
+ function searchListItems(){
6076
+ const box=document.getElementById('q-list');
6077
+ return box?[...box.querySelectorAll('.q-item')]:[];
6078
+ }
6079
+ function moveSearchCursor(delta){
6080
+ let items=searchListItems();
6081
+ if(!items.length){ renderSearchList(searchMatches()); items=searchListItems(); if(!items.length) return; }
6082
+ let idx=items.findIndex(el=>el.classList.contains('active'));
6083
+ idx=idx<0?0:(idx+delta+items.length)%items.length;
6084
+ items.forEach((el,i)=>el.classList.toggle('active',i===idx));
6085
+ items[idx].scrollIntoView({block:'nearest'});
6086
+ }
6087
+ function toggleSel(id){
6088
+ const i=state.sel.indexOf(id);
6089
+ if(i>=0) state.sel.splice(i,1); else state.sel.push(id);
6090
+ state.fd=state.fd||2; state.q='';
6091
+ const qi=document.getElementById('q'); if(qi) qi.value='';
6092
+ go(state.view,state.mode,state.range,state.zoom);
6093
+ }
6094
+ function renderTokens(){
6095
+ const box=document.getElementById('tokens'); if(!box) return;
6096
+ box.innerHTML=state.sel.map(id=>'<span class="token">'+esc(id)+'<button data-id="'+esc(id)+'" title="移除">×</button></span>').join('');
6097
+ box.querySelectorAll('button').forEach(b=>b.onclick=e=>{ e.stopPropagation(); toggleSel(b.dataset.id); });
6098
+ }
6099
+ function readRoute(){
6100
+ const raw=location.hash.replace(/^#\\/?/,''); const [path,qs]=raw.split('?');
6101
+ const params=new URLSearchParams(qs||location.search);
6102
+ const range=(params.get('range')||'30d');
6103
+ const zoom=Math.min(84,Math.max(14,parseInt(params.get('zoom')||'28',10)||28));
6104
+ const sx=Number(params.get('sx')||0);
6105
+ const fromRaw=params.get('from'), toRaw=params.get('to');
6106
+ const toOff=v=>v?Math.round((new Date(v+'T00:00:00').getTime()-new Date('2026-11-06T00:00:00').getTime())/86400000):null;
6107
+ const winRaw=params.get('w');
6108
+ const rangeRaw=params.get('range');
6109
+ const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
6110
+ const fdRaw=params.get('fd');
6111
+ const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
6112
+ return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'', sel:(params.get('sel')||params.get('focus')||'').split(',').map(x=>x.trim()).filter(x=>x&&tasks.some(t=>t.id===x)), fd, from:toOff(fromRaw), to:toOff(toRaw), task:params.get('task'), tab:params.get('tab')==='intervene'?'intervene':'task'};
6113
+ }
6114
+ function routeString(view,mode,range,zoom,from,to){
6115
+ const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
6116
+ if(view==='graph'&&mode==='detail') qs.set('mode','detail');
6117
+ if(view==='graph'&&state.layout==='dep') qs.set('layout','dep');
6118
+ if(view==='graph'){
6119
+ if(state.window!==7) qs.set('w',String(state.window));
6120
+ if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
6121
+ if(state.sel.length){ qs.set('sel',state.sel.join(',')); if(state.fd!==2) qs.set('fd',String(state.fd)); }
6122
+ }
6123
+ if(view==='time'){
6124
+ if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
6125
+ if(state.window!==7) qs.set('w',String(state.window));
6126
+ if(zoom&&zoom!==28) qs.set('zoom',String(zoom));
6127
+ }
6128
+ return '#/'+p+(qs.toString()?'?'+qs.toString():'');
6129
+ }
6130
+ function applyRoute(){
6131
+ const stampEl=document.getElementById('updated');
6132
+ if(stampEl) stampEl.textContent='快照 '+(payload.generated_at||'-');
6133
+ const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.sel=r.sel; state.fd=r.fd; state.from=r.from; state.to=r.to;
6134
+ document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
6135
+ document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
6136
+ document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
6137
+ document.getElementById('view-graph').style.display=r.view==='graph'?'':'none';
6138
+ document.getElementById('view-time').style.display=r.view==='time'?'':'none';
6139
+ document.getElementById('graph-tools').style.display=r.view==='graph'?'flex':'none';
6140
+ document.getElementById('time-tools').style.display=r.view==='time'?'flex':'none';
6141
+ const zbtn=document.querySelector('#zoom button[data-z="reset"]'); if(zbtn) zbtn.textContent=Math.round(r.zoom/28*100)+'%';
6142
+ const dec=document.querySelector('#zoom button[data-z="-"]'), inc=document.querySelector('#zoom button[data-z="+"]'); if(dec) dec.disabled=r.zoom<=14; if(inc) inc.disabled=r.zoom>=84;
6143
+ if(r.view!=='graph') document.getElementById('hint').textContent='时间线:左右滚动查看;-/+缩放;红色虚线 = 今天';
6144
+ const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
6145
+ const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
6146
+ const chip=document.getElementById('focus-chip');
6147
+ if(chip) chip.classList.toggle('on',r.sel.length>0);
6148
+ if(chip) chip.title='按依赖邻域聚焦;清空即回全量';
6149
+ const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
6150
+ renderTokens();
6151
+ const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
6152
+ document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
6153
+ if(r.view==='time'){ drawTimeline(); const sc=document.getElementById('tl-scroll'), lb=document.getElementById('tl-labels-body'); sc.scrollTop=0; lb.style.transform='translateY(0px)'; if(state.sx) sc.scrollLeft=state.sx; } else drawGraph();
6154
+ if(r.task) openDrawer(r.task,r.tab);
6155
+ syncLaneLabels();
6156
+ if(r.view==='graph') renderEmpty();
6157
+ }
6158
+ function go(view,mode,range,zoom){ const h=routeString(view,mode,range||state.range,zoom||state.zoom,state.from,state.to); if(location.hash===h) applyRoute(); else location.hash=h; }
6159
+ document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>go(b.dataset.v==='time'?'time':'graph',state.mode,state.range));
6160
+ document.querySelectorAll('#layout button').forEach(b=>b.onclick=()=>{state.layout=b.dataset.l;go(state.view,state.mode,state.range,state.zoom);});
6161
+ const winSelEl=document.getElementById('win');
6162
+ if(winSelEl) winSelEl.onchange=()=>{ state.window=Number(winSelEl.value); go(state.view,state.mode,state.range,state.zoom); };
6163
+ const qInput=document.getElementById('q');
6164
+ if(qInput){
6165
+ qInput.oninput=()=>{ state.q=qInput.value; applySearch(); renderSearchList(searchMatches()); };
6166
+ qInput.onkeydown=e=>{
6167
+ if(e.key==='ArrowDown'){ e.preventDefault(); moveSearchCursor(1); }
6168
+ else if(e.key==='ArrowUp'){ e.preventDefault(); moveSearchCursor(-1); }
6169
+ else if(e.key==='Enter'){
6170
+ const items=searchListItems();
6171
+ const active=items.find(el=>el.classList.contains('active'))||items[0];
6172
+ if(active) toggleSel(active.dataset.id);
6173
+ } else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
6174
+ else if(e.key==='Backspace' && !qInput.value && state.sel.length){ e.preventDefault(); toggleSel(state.sel[state.sel.length-1]); }
6175
+ };
6176
+ qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
6177
+ qInput.onclick=()=>{ renderSearchList(searchMatches()); };
6178
+ const sbox=document.getElementById('search-box');
6179
+ if(sbox) sbox.onclick=e=>{ if(e.target.closest('.q-item')||e.target.closest('.token')) return; qInput.focus(); };
6180
+ qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box && !document.getElementById('search-box').contains(document.activeElement)) box.classList.remove('open'); },160); };
6181
+ }
6182
+ const fdSel=document.getElementById('focus-fd');
6183
+ if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
6184
+ const fxBtn=document.getElementById('focus-exit');
6185
+ if(fxBtn) fxBtn.onclick=()=>{ state.sel=[]; go(state.view,state.mode,state.range,state.zoom); };
6186
+ document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
6187
+ document.querySelectorAll('#zoom button').forEach(b=>b.onclick=()=>{ const z=b.dataset.z==='+'?Math.min(84,state.zoom+8):b.dataset.z==='-'?Math.max(14,state.zoom-8):28; go('time',state.mode,state.range,z); });
6188
+ layer.addEventListener('scroll',syncLaneLabels,{passive:true});
6189
+ const tlScroller=document.getElementById('tl-scroll');
6190
+ tlScroller.addEventListener('scroll',()=>{ const lb=document.getElementById('tl-labels-body'); if(lb) lb.style.transform='translateY(-'+tlScroller.scrollTop+'px)'; },{passive:true});
6191
+ document.querySelectorAll('.attention .row[data-task]').forEach(r=>r.onclick=()=>openDrawer(r.dataset.task,'intervene'));
6192
+ window.addEventListener('hashchange',applyRoute);
6193
+ applyRoute();
6194
+ if(document.fonts&&document.fonts.ready){ document.fonts.ready.then(()=>{ if(state.view==='graph'){ if(state.layout==='dep') drawDepGraph(); else drawGraph(); } }); }
6195
+ </script>
6196
+ </body>
6197
+ </html>
6198
+ `;
6199
+
6200
+ // src/graph-html/render.ts
6201
+ var pad = (n) => String(n).padStart(2, "0");
6202
+ function localDay(d) {
6203
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
6204
+ }
6205
+ function dayDiff(from, to) {
6206
+ const a = from.split("-").map(Number), b = to.split("-").map(Number);
6207
+ return Math.round((Date.UTC(b[0], b[1] - 1, b[2]) - Date.UTC(a[0], a[1] - 1, a[2])) / 86400000);
6208
+ }
6209
+ function shortHash(s) {
6210
+ let h = 2166136261;
6211
+ for (let i = 0;i < s.length; i++) {
6212
+ h ^= s.charCodeAt(i);
6213
+ h = Math.imul(h, 16777619);
6214
+ }
6215
+ return (h >>> 0).toString(16).padStart(8, "0").slice(0, 8);
6216
+ }
6217
+ function safeJson(v) {
6218
+ return JSON.stringify(v).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6219
+ }
6220
+ var bare = (id) => id.replace(/^task:/, "");
6221
+ function buildPayload(graph, opts = {}) {
6222
+ const now = opts.now ?? new Date;
6223
+ const today = localDay(now);
6224
+ const limit = opts.limit === undefined ? 500 : opts.limit;
6225
+ const redact = !!opts.redact;
6226
+ const taskNodes = graph.nodes.filter((n) => n.kind === "task");
6227
+ const known = new Set(taskNodes.map((n) => n.id));
6228
+ const ordered = [...taskNodes].sort((a, b) => String(a.prio ?? "P9").localeCompare(String(b.prio ?? "P9")) || String(a.start ?? a.created_at ?? "").localeCompare(String(b.start ?? b.created_at ?? "")) || a.id.localeCompare(b.id));
6229
+ const shown = limit > 0 ? ordered.slice(0, limit) : ordered;
6230
+ const tasks = shown.map((n) => {
6231
+ const startDay = n.start ?? (n.created_at ? localDay(new Date(n.created_at)) : null);
6232
+ const endDay = n.due ?? (n.closed_at ? localDay(new Date(n.closed_at)) : null);
6233
+ return {
6234
+ id: bare(n.id),
6235
+ label: n.label,
6236
+ status: n.status ?? "todo",
6237
+ prio: n.prio ?? null,
6238
+ start: startDay ? dayDiff(today, startDay) : null,
6239
+ end: endDay && startDay ? dayDiff(today, endDay) : null,
6240
+ progress: typeof n.progress === "number" ? n.progress : 0,
6241
+ derived: n.progress_derived !== false,
6242
+ owner: n.owner ? redact ? `owner-${shortHash(n.owner)}` : n.owner : null,
6243
+ deps: (n.deps ?? []).filter((d) => known.has(`task:${d}`)),
6244
+ touches: (n.touches ?? []).map((p) => redact ? p.split("/").pop() || p : p)
6245
+ };
6246
+ });
6247
+ const shownIds = new Set(tasks.map((t) => t.id));
6248
+ const edges = graph.edges.filter((e) => e.kind === "depends_on").map((e) => ({ from: bare(e.from), to: bare(e.to), kind: "depends_on" })).filter((e) => shownIds.has(e.from) && shownIds.has(e.to));
6249
+ return {
6250
+ schema: 1,
6251
+ today,
6252
+ generated_at: `${today} ${pad(now.getHours())}:${pad(now.getMinutes())}`,
6253
+ tasks,
6254
+ edges,
6255
+ cycles: graph.cycles.map((c) => c.map(bare)),
6256
+ claims: opts.claims ?? graph.edges.filter((e) => e.kind === "claims").length,
6257
+ truncated: limit > 0 && ordered.length > limit ? { shown: limit, total: ordered.length } : null,
6258
+ capabilities: graph.capabilities ?? []
6259
+ };
6260
+ }
6261
+ function renderHtml(graph, opts = {}) {
6262
+ const payload = buildPayload(graph, opts);
6263
+ return template_default.replace("__DIA_PAYLOAD__", safeJson(payload));
6264
+ }
6265
+
4864
6266
  // packages/cli/src/index.ts
4865
6267
  init_paths();
4866
6268
 
4867
6269
  // src/agents.ts
4868
6270
  init_paths();
4869
- import { existsSync as existsSync16, mkdirSync as mkdirSync15, readdirSync as readdirSync9, readFileSync as readFileSync15, renameSync as renameSync8, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
6271
+ import { existsSync as existsSync17, mkdirSync as mkdirSync15, readdirSync as readdirSync9, readFileSync as readFileSync15, renameSync as renameSync9, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
4870
6272
  import { basename as basename8, join as join18, resolve as resolve10 } from "node:path";
4871
6273
  import { execFileSync as execFileSync5 } from "node:child_process";
4872
6274
  var agentsRoot2 = () => agentsRoot();
@@ -4875,7 +6277,7 @@ function atomicWrite2(path, data) {
4875
6277
  mkdirSync15(join18(path, ".."), { recursive: true });
4876
6278
  const tmp = `${path}.tmp.${process.pid}`;
4877
6279
  writeFileSync12(tmp, data);
4878
- renameSync8(tmp, path);
6280
+ renameSync9(tmp, path);
4879
6281
  }
4880
6282
  function sh(cmd, args) {
4881
6283
  try {
@@ -4896,14 +6298,14 @@ function register(rec) {
4896
6298
  }
4897
6299
  function unregister(agent) {
4898
6300
  const p = fileOf(agent);
4899
- if (!existsSync16(p))
6301
+ if (!existsSync17(p))
4900
6302
  return false;
4901
6303
  rmSync6(p, { force: true });
4902
6304
  return true;
4903
6305
  }
4904
6306
  function read2(agent) {
4905
6307
  const p = fileOf(agent);
4906
- if (!existsSync16(p))
6308
+ if (!existsSync17(p))
4907
6309
  return null;
4908
6310
  try {
4909
6311
  return JSON.parse(readFileSync15(p, "utf8"));
@@ -4913,7 +6315,7 @@ function read2(agent) {
4913
6315
  }
4914
6316
  function all() {
4915
6317
  const root = agentsRoot2();
4916
- if (!existsSync16(root))
6318
+ if (!existsSync17(root))
4917
6319
  return [];
4918
6320
  return readdirSync9(root).filter((f) => f.endsWith(".json")).map((f) => {
4919
6321
  try {
@@ -5179,7 +6581,7 @@ async function deliverAcp(agent, id, opts = {}) {
5179
6581
  }
5180
6582
 
5181
6583
  // src/scope.ts
5182
- import { existsSync as existsSync17, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "node:fs";
6584
+ import { existsSync as existsSync18, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "node:fs";
5183
6585
  init_paths();
5184
6586
  import { homedir as homedir9 } from "node:os";
5185
6587
  import { join as join20, resolve as resolve11 } from "node:path";
@@ -5219,7 +6621,7 @@ function loadKit(id, version) {
5219
6621
  }
5220
6622
  function installedPersonas() {
5221
6623
  const p = join20(dataDir5(), "personas");
5222
- if (!existsSync17(p))
6624
+ if (!existsSync18(p))
5223
6625
  return [];
5224
6626
  return readdirSync10(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5225
6627
  }
@@ -5235,7 +6637,7 @@ function current(explicit) {
5235
6637
  if (!id)
5236
6638
  return null;
5237
6639
  const idDir = join20(dataDir5(), "personas", id);
5238
- if (!existsSync17(idDir))
6640
+ if (!existsSync18(idDir))
5239
6641
  throw new Error(`E_SCOPE: persona not installed: ${id}`);
5240
6642
  const version = effective(id)?.version ?? latest(idDir);
5241
6643
  const dir = join20(idDir, version);
@@ -5285,13 +6687,13 @@ function scopeEnv(s, extra = {}) {
5285
6687
  }
5286
6688
 
5287
6689
  // src/doctor.ts
5288
- import { existsSync as existsSync22, readFileSync as readFileSync19 } from "node:fs";
6690
+ import { closeSync as closeSync4, existsSync as existsSync23, openSync as openSync4, readFileSync as readFileSync19, readSync, realpathSync as realpathSync5, statSync as statSync12 } from "node:fs";
5289
6691
  import { homedir as homedir12 } from "node:os";
5290
- import { join as join25, resolve as resolve13 } from "node:path";
6692
+ import { delimiter, dirname as dirname13, join as join25, resolve as resolve13 } from "node:path";
5291
6693
 
5292
6694
  // src/dsh.ts
5293
6695
  import { execFileSync as execFileSync7 } from "node:child_process";
5294
- import { existsSync as existsSync18, realpathSync as realpathSync4 } from "node:fs";
6696
+ import { existsSync as existsSync19, realpathSync as realpathSync4 } from "node:fs";
5295
6697
  import { dirname as dirname10, join as join21 } from "node:path";
5296
6698
  var dshBin = () => process.env.DIA_DSH_BIN || "dsh";
5297
6699
  function which(cmd) {
@@ -5299,7 +6701,7 @@ function which(cmd) {
5299
6701
  if (!dir)
5300
6702
  continue;
5301
6703
  const p = join21(dir, cmd);
5302
- if (existsSync18(p))
6704
+ if (existsSync19(p))
5303
6705
  return p;
5304
6706
  }
5305
6707
  return null;
@@ -5322,7 +6724,7 @@ var packageFor = (id) => `@deepseek-ai/dsh-${id}`;
5322
6724
  function requireConfig(root, id) {
5323
6725
  const pkg = packageFor(id);
5324
6726
  const dir = join21(root, "node_modules", ...pkg.split("/"), "lib", "index.js");
5325
- if (!existsSync18(dir))
6727
+ if (!existsSync19(dir))
5326
6728
  return { required: false };
5327
6729
  try {
5328
6730
  const out = execFileSync7(process.execPath, [
@@ -5367,7 +6769,7 @@ function pluginsWithoutConfig(dump, personaId) {
5367
6769
  return missing;
5368
6770
  }
5369
6771
  function checkPreset(personaId, profileDir) {
5370
- if (!existsSync18(profileDir))
6772
+ if (!existsSync19(profileDir))
5371
6773
  return { ok: true, skipped: "未渲染 dsh profile", issues: [] };
5372
6774
  let dump;
5373
6775
  try {
@@ -5395,7 +6797,7 @@ function checkPreset(personaId, profileDir) {
5395
6797
  }
5396
6798
 
5397
6799
  // src/platforms.ts
5398
- import { existsSync as existsSync19, readFileSync as readFileSync17 } from "node:fs";
6800
+ import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
5399
6801
  init_paths();
5400
6802
  import { join as join22 } from "node:path";
5401
6803
  import { execFileSync as execFileSync8 } from "node:child_process";
@@ -5410,7 +6812,7 @@ function registryCandidates() {
5410
6812
  return out;
5411
6813
  }
5412
6814
  function registryPath() {
5413
- return registryCandidates().find((p) => existsSync19(p)) ?? null;
6815
+ return registryCandidates().find((p) => existsSync20(p)) ?? null;
5414
6816
  }
5415
6817
  function load() {
5416
6818
  const p = registryPath();
@@ -5484,7 +6886,7 @@ function checks(reg) {
5484
6886
 
5485
6887
  // src/deps.ts
5486
6888
  init_paths();
5487
- import { existsSync as existsSync20, readFileSync as readFileSync18, mkdirSync as mkdirSync16, writeFileSync as writeFileSync14, chmodSync as chmodSync3 } from "node:fs";
6889
+ import { existsSync as existsSync21, readFileSync as readFileSync18, mkdirSync as mkdirSync16, writeFileSync as writeFileSync14, chmodSync as chmodSync3 } from "node:fs";
5488
6890
  import { homedir as homedir10 } from "node:os";
5489
6891
  import { dirname as dirname11, join as join23 } from "node:path";
5490
6892
  import { execFileSync as execFileSync9 } from "node:child_process";
@@ -5500,7 +6902,7 @@ function candidates() {
5500
6902
  return out;
5501
6903
  }
5502
6904
  function file3() {
5503
- return candidates().find((p) => existsSync20(p)) ?? null;
6905
+ return candidates().find((p) => existsSync21(p)) ?? null;
5504
6906
  }
5505
6907
  function load2(p) {
5506
6908
  const path = p ?? file3();
@@ -5562,7 +6964,7 @@ function check(e) {
5562
6964
  return { id: e.id, status: "ok", detail: `${e.id}:无固定路径(跳过探测)` };
5563
6965
  }
5564
6966
  const path = expand3(r.replace(/^link:/, ""));
5565
- return existsSync20(path) ? { id: e.id, status: "ok", detail: `${e.id}:${path}` } : missing(`${path} 不存在`);
6967
+ return existsSync21(path) ? { id: e.id, status: "ok", detail: `${e.id}:${path}` } : missing(`${path} 不存在`);
5566
6968
  }
5567
6969
  return { id: e.id, status: "ok", detail: `${e.id}:kind=${e.kind} 无探测规则` };
5568
6970
  }
@@ -5590,7 +6992,7 @@ function append4(entry, p) {
5590
6992
  mkdirSync16(dirname11(path), { recursive: true });
5591
6993
  const line = JSON.stringify(entry) + `
5592
6994
  `;
5593
- const prev = existsSync20(path) ? readFileSync18(path, "utf8") : "";
6995
+ const prev = existsSync21(path) ? readFileSync18(path, "utf8") : "";
5594
6996
  writeFileSync14(path, prev.endsWith(`
5595
6997
  `) || !prev ? prev + line : prev + `
5596
6998
  ` + line);
@@ -5606,7 +7008,7 @@ init_paths();
5606
7008
  // src/job.ts
5607
7009
  init_paths();
5608
7010
  import { spawnSync as spawnSync2 } from "node:child_process";
5609
- import { existsSync as existsSync21 } from "node:fs";
7011
+ import { existsSync as existsSync22 } from "node:fs";
5610
7012
  import { homedir as homedir11 } from "node:os";
5611
7013
  import { dirname as dirname12, join as join24, resolve as resolve12 } from "node:path";
5612
7014
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -5614,7 +7016,7 @@ function jobBin() {
5614
7016
  const env = process.env.DIA_JOB_BIN;
5615
7017
  if (env) {
5616
7018
  const abs = resolve12(env);
5617
- return existsSync21(abs) ? { bin: abs } : { bin: null, error: `DIA_JOB_BIN 不存在:${abs}` };
7019
+ return existsSync22(abs) ? { bin: abs } : { bin: null, error: `DIA_JOB_BIN 不存在:${abs}` };
5618
7020
  }
5619
7021
  const here = dirname12(fileURLToPath2(import.meta.url));
5620
7022
  const candidates = [
@@ -5622,7 +7024,7 @@ function jobBin() {
5622
7024
  join24(here, "..", "..", "packages", "job", "bin", "dia-job"),
5623
7025
  join24(homedir11(), ".local", "bin", "dia-job")
5624
7026
  ];
5625
- const found = candidates.find((p) => existsSync21(p));
7027
+ const found = candidates.find((p) => existsSync22(p));
5626
7028
  return found ? { bin: found } : { bin: null };
5627
7029
  }
5628
7030
  function jobRootResolved() {
@@ -5630,7 +7032,7 @@ function jobRootResolved() {
5630
7032
  return process.env.DIA_JOB_ROOT;
5631
7033
  const next = jobRoot();
5632
7034
  const legacy = join24(homedir11(), ".local", "share", "dia-job");
5633
- return !existsSync21(next) && existsSync21(legacy) ? legacy : next;
7035
+ return !existsSync22(next) && existsSync22(legacy) ? legacy : next;
5634
7036
  }
5635
7037
  function runJobCapture(argv) {
5636
7038
  const { bin, error } = jobBin();
@@ -5684,6 +7086,102 @@ function platformChecks() {
5684
7086
  function quoteArg(v) {
5685
7087
  return /^[\w.@%+=:,\/-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
5686
7088
  }
7089
+ function npmOwnedPath(p) {
7090
+ return /(^|[\\/])node_modules[\\/]@minonl[\\/]dia([\\/]|$)/.test(p);
7091
+ }
7092
+ function pkgShimPath(p) {
7093
+ try {
7094
+ const st = statSync12(p);
7095
+ if (!st.isFile() || st.size > 1024 * 1024)
7096
+ return false;
7097
+ const fd = openSync4(p, "r");
7098
+ try {
7099
+ const buf = Buffer.alloc(8192);
7100
+ const n = readSync(fd, buf, 0, buf.length, 0);
7101
+ return /@minonl\/dia|node_modules\/(@minonl\/)?dia\//.test(buf.subarray(0, n).toString("utf8"));
7102
+ } finally {
7103
+ closeSync4(fd);
7104
+ }
7105
+ } catch {
7106
+ return false;
7107
+ }
7108
+ }
7109
+ function pkgBinDirOwned(p) {
7110
+ const d = realOrSelf(dirname13(p));
7111
+ const home = homedir12();
7112
+ const known = [
7113
+ join25(home, ".volta/bin"),
7114
+ join25(home, ".bun/bin"),
7115
+ join25(home, ".yarn/bin"),
7116
+ join25(home, ".pnpm-global/bin"),
7117
+ join25(home, ".local/share/pnpm"),
7118
+ join25(home, ".npm-global/bin"),
7119
+ process.env.PNPM_HOME,
7120
+ process.env.YARN_GLOBAL_FOLDER,
7121
+ process.env.VOLTA_HOME ? join25(process.env.VOLTA_HOME, "bin") : "",
7122
+ process.env.BUN_INSTALL ? join25(process.env.BUN_INSTALL, "bin") : ""
7123
+ ].filter(Boolean);
7124
+ return known.some((k) => realOrSelf(k) === d);
7125
+ }
7126
+ function pkgOwnedPath(p) {
7127
+ return npmOwnedPath(p) || pkgShimPath(p) || pkgBinDirOwned(p);
7128
+ }
7129
+ function realOrSelf(p) {
7130
+ try {
7131
+ return realpathSync5(p);
7132
+ } catch {
7133
+ return resolve13(p);
7134
+ }
7135
+ }
7136
+ function whichDia(pathEnv = process.env.PATH || "") {
7137
+ for (const dir of pathEnv.split(delimiter)) {
7138
+ if (!dir)
7139
+ continue;
7140
+ const cand = join25(dir, "dia");
7141
+ try {
7142
+ const st = statSync12(cand);
7143
+ if (!st.isFile())
7144
+ continue;
7145
+ if (process.platform !== "win32" && (st.mode & 73) === 0)
7146
+ continue;
7147
+ return cand;
7148
+ } catch {}
7149
+ }
7150
+ return null;
7151
+ }
7152
+ function installShadowCheck() {
7153
+ const name = "install_shadow";
7154
+ const install = installDir();
7155
+ const installNpm = npmOwnedPath(realOrSelf(install));
7156
+ if (!installNpm && !existsSync23(join25(install, "install.json"))) {
7157
+ return { name, status: "ok", detail: `源码运行(install=${install} 无 install.json,跳过一致性判定)` };
7158
+ }
7159
+ const bin = whichDia();
7160
+ if (!bin)
7161
+ return { name, status: "ok", detail: `PATH 中没有 dia(跳过一致性判定;install=${install})` };
7162
+ const binReal = realOrSelf(bin);
7163
+ const binNpm = pkgOwnedPath(binReal);
7164
+ const kind = (npm) => npm ? "包管理器全局安装" : "脚本安装";
7165
+ if (installNpm === binNpm) {
7166
+ return { name, status: "ok", detail: `一致(${kind(installNpm)}):install=${install};PATH dia=${binReal}` };
7167
+ }
7168
+ const fix = installNpm ? `ln -sfn "$(npm prefix -g)/bin/dia" ${quoteArg(bin)}` : `ln -sfn ${quoteArg(join25(install, "bin/dia.js"))} ${quoteArg(bin)}`;
7169
+ return {
7170
+ name,
7171
+ status: "warn",
7172
+ detail: `安装类型不一致:install=${install}(${kind(installNpm)})但 PATH dia=${binReal}(${kind(binNpm)});修复:${fix}`
7173
+ };
7174
+ }
7175
+ function bootstrapCheck() {
7176
+ const stamp = readStamp();
7177
+ const pkg = pkgVersion();
7178
+ if (!stamp)
7179
+ return { name: "bootstrap", status: "warn", detail: "内置能力尚未装配(首次 persona add/render 或 dia collab enable 会自动装)" };
7180
+ if (stamp.pkg !== pkg) {
7181
+ return { name: "bootstrap", status: "warn", detail: `内置能力可能过期:自举戳 pkg=${stamp.pkg},当前 ${pkg};修复:dia collab enable` };
7182
+ }
7183
+ return { name: "bootstrap", status: "ok", detail: `内置能力已装配(pkg=${pkg},kit=${stamp.kit ?? "-"})` };
7184
+ }
5687
7185
  function describeStale(st) {
5688
7186
  const diff = st.added + st.removed + st.changed;
5689
7187
  const parts = [st.added ? `新增 ${st.added}` : "", st.removed ? `删除 ${st.removed}` : "", st.changed ? `修改 ${st.changed}` : ""].filter(Boolean);
@@ -5692,7 +7190,7 @@ function describeStale(st) {
5692
7190
  }
5693
7191
  async function knowledgeCheck(id, prefix, root) {
5694
7192
  const name = `${prefix}knowledge`;
5695
- if (!existsSync22(root))
7193
+ if (!existsSync23(root))
5696
7194
  return { name, status: "fail", detail: root };
5697
7195
  const rebuild = `dia kb index --persona ${quoteArg(id)}`;
5698
7196
  let st;
@@ -5829,20 +7327,22 @@ async function run(opts = {}) {
5829
7327
  status: install === diaHome() ? "warn" : "ok",
5830
7328
  detail: `install=${install} data=${diaHome()}${install === diaHome() ? "(安装物与数据同根)" : ""}`
5831
7329
  });
7330
+ checks.push(installShadowCheck());
7331
+ checks.push(bootstrapCheck());
5832
7332
  const wt = join25(install, "packages/wt/bin/dia-wt");
5833
- checks.push({ name: "wt", status: existsSync22(wt) ? "ok" : "warn", detail: existsSync22(wt) ? wt : "dia wt 未安装(dia install.sh)" });
7333
+ checks.push({ name: "wt", status: existsSync23(wt) ? "ok" : "warn", detail: existsSync23(wt) ? wt : "dia wt 未安装(dia install.sh)" });
5834
7334
  const jobRoot = jobRootResolved();
5835
7335
  checks.push({
5836
7336
  name: "job",
5837
- status: existsSync22(jobRoot) ? "ok" : "warn",
5838
- detail: `${jobRoot}${existsSync22(jobRoot) ? "" : "(未创建;dia job install 首次运行会建)"}`
7337
+ status: existsSync23(jobRoot) ? "ok" : "warn",
7338
+ detail: `${jobRoot}${existsSync23(jobRoot) ? "" : "(未创建;dia job install 首次运行会建)"}`
5839
7339
  });
5840
7340
  const verdict = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
5841
7341
  return { persona: ids.length === 1 ? ids[0] : null, personas: ids, checks, verdict };
5842
7342
  }
5843
7343
 
5844
7344
  // src/env.ts
5845
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "node:fs";
7345
+ import { existsSync as existsSync24, readFileSync as readFileSync20 } from "node:fs";
5846
7346
  import { homedir as homedir13 } from "node:os";
5847
7347
  import { join as join26 } from "node:path";
5848
7348
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -5886,7 +7386,7 @@ function loadValues(cfg) {
5886
7386
  return all;
5887
7387
  }
5888
7388
  function list6(cfg) {
5889
- const files = cfg.files.map((p) => ({ path: p, exists: existsSync23(p), keys: existsSync23(p) ? Object.keys(parseFile(p)).length : 0 }));
7389
+ const files = cfg.files.map((p) => ({ path: p, exists: existsSync24(p), keys: existsSync24(p) ? Object.keys(parseFile(p)).length : 0 }));
5890
7390
  const values = loadValues(cfg);
5891
7391
  return { files, keys: cfg.keys.map((k) => ({ key: k, found: !!values[k] })) };
5892
7392
  }
@@ -5927,7 +7427,7 @@ function run2(cfg, cmd, opts = {}) {
5927
7427
  }
5928
7428
 
5929
7429
  // src/bw.ts
5930
- import { chmodSync as chmodSync4, existsSync as existsSync24, mkdirSync as mkdirSync17, readFileSync as readFileSync21, rmSync as rmSync7, writeFileSync as writeFileSync15 } from "node:fs";
7430
+ import { chmodSync as chmodSync4, existsSync as existsSync25, mkdirSync as mkdirSync17, readFileSync as readFileSync21, rmSync as rmSync7, writeFileSync as writeFileSync15 } from "node:fs";
5931
7431
  init_paths();
5932
7432
  import { join as join27 } from "node:path";
5933
7433
  import { execFileSync as execFileSync10 } from "node:child_process";
@@ -6142,7 +7642,7 @@ function serve(opts = {}) {
6142
7642
  }
6143
7643
  function askBroker(req, timeoutMs = 5000) {
6144
7644
  const sock = socketFile();
6145
- if (!existsSync24(sock))
7645
+ if (!existsSync25(sock))
6146
7646
  return Promise.resolve(null);
6147
7647
  return new Promise((resolve, reject) => {
6148
7648
  const conn = connect(sock);
@@ -6181,19 +7681,19 @@ function askBroker(req, timeoutMs = 5000) {
6181
7681
 
6182
7682
  // packages/cli/src/index.ts
6183
7683
  import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
6184
- import { mkdirSync as mkdirSync20, openSync as openSync3 } from "node:fs";
7684
+ import { mkdirSync as mkdirSync20, openSync as openSync5 } from "node:fs";
6185
7685
  import { basename as basename10, join as join32 } from "node:path";
6186
- import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
7686
+ import { existsSync as existsSync30, readFileSync as readFileSync26, writeFileSync as writeFileSync18 } from "node:fs";
6187
7687
  import { fileURLToPath as fileURLToPath4 } from "node:url";
6188
7688
  import { resolve as resolvePath, sep as pathSep } from "node:path";
6189
7689
 
6190
7690
  // src/team.ts
6191
- import { appendFileSync as appendFileSync8, cpSync as cpSync3, existsSync as existsSync26, mkdirSync as mkdirSync18, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync8, writeFileSync as writeFileSync16 } from "node:fs";
7691
+ import { appendFileSync as appendFileSync8, cpSync as cpSync3, existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync8, writeFileSync as writeFileSync16 } from "node:fs";
6192
7692
  import { homedir as homedir15, tmpdir as tmpdir3 } from "node:os";
6193
7693
  import { join as join29, resolve as resolve14 } from "node:path";
6194
7694
 
6195
7695
  // src/pricing.ts
6196
- import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
7696
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
6197
7697
  import { homedir as homedir14 } from "node:os";
6198
7698
  import { join as join28 } from "node:path";
6199
7699
  var DEFAULT = {
@@ -6205,7 +7705,7 @@ function pricesFile() {
6205
7705
  return process.env.DIA_PRICES_FILE || join28(homedir14(), ".config", "dia", "prices.json");
6206
7706
  }
6207
7707
  function loadPrices() {
6208
- if (!existsSync25(pricesFile()))
7708
+ if (!existsSync26(pricesFile()))
6209
7709
  return { ...DEFAULT };
6210
7710
  try {
6211
7711
  const user = JSON.parse(readFileSync22(pricesFile(), "utf8"));
@@ -6291,7 +7791,7 @@ function judgeWorkspace(dir, fallback) {
6291
7791
  function gate(id) {
6292
7792
  const dir = runDir(id);
6293
7793
  const need = ["plan.md", "diff.patch", "test.log", "verify.json", "redteam.json", "packet.md", "run.json"];
6294
- const missing = need.filter((f) => !existsSync26(join29(dir, f)));
7794
+ const missing = need.filter((f) => !existsSync27(join29(dir, f)));
6295
7795
  if (missing.length)
6296
7796
  return { ok: false, reason: "missing: " + missing.join(",") };
6297
7797
  let verify, redteam, run;
@@ -6337,7 +7837,7 @@ function gate(id) {
6337
7837
  return { ok: false, reason: "mock run 不能作为验收证据(请用 --acp/--dsh 真跑判据)" };
6338
7838
  }
6339
7839
  const workspace = String(run.workspace || "");
6340
- if (!workspace || !existsSync26(workspace))
7840
+ if (!workspace || !existsSync27(workspace))
6341
7841
  return { ok: false, reason: "run.json workspace 不存在" };
6342
7842
  const started = Date.now();
6343
7843
  let exit = 1, out = "";
@@ -6745,11 +8245,11 @@ Return only the JSON object.`);
6745
8245
  }
6746
8246
  function dshSessionFile(sessionId) {
6747
8247
  const root = join29(homedir15(), ".dsh", "sessions");
6748
- if (!existsSync26(root))
8248
+ if (!existsSync27(root))
6749
8249
  return null;
6750
8250
  for (const d of readdirSync11(root)) {
6751
8251
  const f = join29(root, d, sessionId, "session.v4.jsonl.zstd");
6752
- if (existsSync26(f))
8252
+ if (existsSync27(f))
6753
8253
  return f;
6754
8254
  }
6755
8255
  return null;
@@ -7039,7 +8539,7 @@ function reject(id, reason) {
7039
8539
  var DEFAULT_ACP_TIMEOUT = 600000;
7040
8540
 
7041
8541
  // src/proposal.ts
7042
- import { existsSync as existsSync27, readFileSync as readFileSync24, readdirSync as readdirSync12 } from "node:fs";
8542
+ import { existsSync as existsSync28, readFileSync as readFileSync24, readdirSync as readdirSync12 } from "node:fs";
7043
8543
  import { join as join30 } from "node:path";
7044
8544
  import { createHash as createHash10 } from "node:crypto";
7045
8545
  init_paths();
@@ -7062,7 +8562,7 @@ function readJson3(path) {
7062
8562
  }
7063
8563
  function collectRuns(out) {
7064
8564
  const root = join30(DATA7(), "runs");
7065
- if (!existsSync27(root))
8565
+ if (!existsSync28(root))
7066
8566
  return;
7067
8567
  for (const id of readdirSync12(root)) {
7068
8568
  const dir = join30(root, id);
@@ -7106,7 +8606,7 @@ function collectTasks(out) {
7106
8606
  }
7107
8607
  }
7108
8608
  function collectDeps(out, path) {
7109
- if (!existsSync27(path))
8609
+ if (!existsSync28(path))
7110
8610
  return;
7111
8611
  for (const e of load2(path)) {
7112
8612
  if (e.result === "unknown" || e.support === "unknown") {
@@ -7139,7 +8639,7 @@ function expandIds(cell) {
7139
8639
  return out;
7140
8640
  }
7141
8641
  function collectHypotheses(out, path) {
7142
- if (!existsSync27(path))
8642
+ if (!existsSync28(path))
7143
8643
  return;
7144
8644
  const lines = readFileSync24(path, "utf8").split(`
7145
8645
  `);
@@ -7181,20 +8681,20 @@ import { createHash as createHash11, randomBytes as randomBytes4 } from "node:cr
7181
8681
  import {
7182
8682
  appendFileSync as appendFileSync9,
7183
8683
  cpSync as cpSync4,
7184
- existsSync as existsSync28,
8684
+ existsSync as existsSync29,
7185
8685
  lstatSync as lstatSync3,
7186
8686
  mkdirSync as mkdirSync19,
7187
8687
  readFileSync as readFileSync25,
7188
8688
  readdirSync as readdirSync13,
7189
8689
  readlinkSync as readlinkSync2,
7190
8690
  rmSync as rmSync9,
7191
- statSync as statSync12,
8691
+ statSync as statSync13,
7192
8692
  symlinkSync as symlinkSync3,
7193
8693
  unlinkSync as unlinkSync3,
7194
8694
  writeFileSync as writeFileSync17
7195
8695
  } from "node:fs";
7196
8696
  import { hostname as osHostname } from "node:os";
7197
- import { basename as basename9, dirname as dirname13, isAbsolute as isAbsolute4, join as join31, relative as relative6, resolve as resolve15, sep as sep8 } from "node:path";
8697
+ import { basename as basename9, dirname as dirname14, isAbsolute as isAbsolute4, join as join31, relative as relative6, resolve as resolve15, sep as sep8 } from "node:path";
7198
8698
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7199
8699
  init_paths();
7200
8700
  var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
@@ -7375,7 +8875,7 @@ function validateServiceManifest(m) {
7375
8875
  }
7376
8876
  function readServiceManifest(dir) {
7377
8877
  const p = join31(dir, "dia.json");
7378
- if (!existsSync28(p))
8878
+ if (!existsSync29(p))
7379
8879
  throw new Error(`E_NO_MANIFEST: dia.json not found in ${dir}(adopted 服务需先补 dia.json)`);
7380
8880
  let raw;
7381
8881
  try {
@@ -7405,7 +8905,7 @@ function resolveEntry(cmd, baseDir) {
7405
8905
  }
7406
8906
  function isExecutableFile(p) {
7407
8907
  try {
7408
- const st = statSync12(p);
8908
+ const st = statSync13(p);
7409
8909
  if (!st.isFile())
7410
8910
  return false;
7411
8911
  return process.platform === "win32" ? true : (st.mode & 73) !== 0;
@@ -7642,11 +9142,11 @@ function diaCmd() {
7642
9142
  if (onPath)
7643
9143
  return "dia";
7644
9144
  const entries = [
7645
- resolve15(dirname13(fileURLToPath3(import.meta.url)), "..", "packages", "cli", "src", "index.ts"),
7646
- resolve15(dirname13(process.argv[1] ?? ""), "..", "src", "cli.ts"),
7647
- resolve15(dirname13(process.argv[1] ?? ""), "..", "dist", "cli.js")
9145
+ resolve15(dirname14(fileURLToPath3(import.meta.url)), "..", "packages", "cli", "src", "index.ts"),
9146
+ resolve15(dirname14(process.argv[1] ?? ""), "..", "src", "cli.ts"),
9147
+ resolve15(dirname14(process.argv[1] ?? ""), "..", "dist", "cli.js")
7648
9148
  ];
7649
- const entry = entries.find((p) => existsSync28(p));
9149
+ const entry = entries.find((p) => existsSync29(p));
7650
9150
  return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
7651
9151
  }
7652
9152
  function decorateStatus(text, view) {
@@ -7674,10 +9174,10 @@ var serviceDir = (id) => join31(servicesRoot(), id);
7674
9174
  var serviceLockPath = (id) => join31(serviceDir(id), "service.lock.json");
7675
9175
  var healthStatePath = (id) => process.env.DIA_SERVICE_HEALTH_FILE || join31(jobRoot2(), "tasks", `${healthJobName(id)}.state.jsonl`);
7676
9176
  function readLockFile(id) {
7677
- if (!existsSync28(serviceDir(id)))
9177
+ if (!existsSync29(serviceDir(id)))
7678
9178
  throw new Error(`E_NOT_INSTALLED: service ${id}`);
7679
9179
  const p = serviceLockPath(id);
7680
- if (!existsSync28(p))
9180
+ if (!existsSync29(p))
7681
9181
  throw new Error(`E_NOT_INSTALLED: service ${id}(缺 lock)`);
7682
9182
  try {
7683
9183
  return JSON.parse(readFileSync25(p, "utf8"));
@@ -7698,7 +9198,7 @@ function generationOf(id) {
7698
9198
  return `g-sess${(++ephemeralGen).toString(16).padStart(4, "0")}`;
7699
9199
  }
7700
9200
  function readHealthLines(file) {
7701
- if (!existsSync28(file))
9201
+ if (!existsSync29(file))
7702
9202
  return [];
7703
9203
  const rows = [];
7704
9204
  const lines = readFileSync25(file, "utf8").split(`
@@ -7733,7 +9233,7 @@ function readHealthLines(file) {
7733
9233
  return rows;
7734
9234
  }
7735
9235
  function appendHealthRecord(file, rec) {
7736
- mkdirSync19(dirname13(file), { recursive: true });
9236
+ mkdirSync19(dirname14(file), { recursive: true });
7737
9237
  appendFileSync9(file, JSON.stringify(rec) + `
7738
9238
  `);
7739
9239
  }
@@ -7780,7 +9280,7 @@ function listServices(opts = {}) {
7780
9280
  const reg = opts.registry === null ? null : readRegistryTasks(opts.registry ?? registryPath2());
7781
9281
  const rows = [];
7782
9282
  const seen = new Set;
7783
- if (existsSync28(root)) {
9283
+ if (existsSync29(root)) {
7784
9284
  for (const e of readdirSync13(root, { withFileTypes: true })) {
7785
9285
  if (!e.isDirectory() || e.name.startsWith("."))
7786
9286
  continue;
@@ -7789,7 +9289,7 @@ function listServices(opts = {}) {
7789
9289
  }
7790
9290
  }
7791
9291
  const adoptRoot = join31(root, "adopted");
7792
- if (existsSync28(adoptRoot)) {
9292
+ if (existsSync29(adoptRoot)) {
7793
9293
  for (const e of readdirSync13(adoptRoot, { withFileTypes: true })) {
7794
9294
  if (!e.isDirectory() || seen.has(e.name))
7795
9295
  continue;
@@ -8077,7 +9577,7 @@ function whichTimeout1() {
8077
9577
  var serviceJobName = (id) => `svc-${id}`;
8078
9578
  var healthJobName = (id) => `svc-${id}-health`;
8079
9579
  function readRegistryTasks(file = registryPath2()) {
8080
- if (!existsSync28(file))
9580
+ if (!existsSync29(file))
8081
9581
  return null;
8082
9582
  try {
8083
9583
  const d = JSON.parse(readFileSync25(file, "utf8"));
@@ -8099,7 +9599,7 @@ function collectManagedFiles(base, hc) {
8099
9599
  const out = {};
8100
9600
  const rel = "dia.json";
8101
9601
  const p = join31(base, rel);
8102
- if (existsSync28(p))
9602
+ if (existsSync29(p))
8103
9603
  out[rel] = shaFile3(p);
8104
9604
  if (!hc)
8105
9605
  return out;
@@ -8114,7 +9614,7 @@ function collectManagedFiles(base, hc) {
8114
9614
  continue;
8115
9615
  let st;
8116
9616
  try {
8117
- st = statSync12(abs);
9617
+ st = statSync13(abs);
8118
9618
  } catch {
8119
9619
  continue;
8120
9620
  }
@@ -8128,11 +9628,11 @@ function verify4(dir, opts = {}) {
8128
9628
  const svc = readServiceManifest(dir);
8129
9629
  const entry = resolveEntry(entryCmdOf(svc), dir);
8130
9630
  const drift = [];
8131
- if (!existsSync28(entry.path))
9631
+ if (!existsSync29(entry.path))
8132
9632
  drift.push({ code: "entry_missing", detail: `entry 不存在:${entry.path}` });
8133
9633
  else if (!isExecutableFile(entry.path))
8134
9634
  drift.push({ code: "entry_not_executable", detail: `entry 不可执行:${entry.path}` });
8135
- const lock = opts.lock === undefined ? existsSync28(serviceDir(svc.id)) ? tryLock(svc.id) : null : opts.lock;
9635
+ const lock = opts.lock === undefined ? existsSync29(serviceDir(svc.id)) ? tryLock(svc.id) : null : opts.lock;
8136
9636
  if (lock) {
8137
9637
  if (entry.kind === "ref") {
8138
9638
  const want = lock.entry.link ? resolve15(lock.entry.link) : null;
@@ -8169,7 +9669,7 @@ function tryLock(id) {
8169
9669
  return null;
8170
9670
  }
8171
9671
  }
8172
- var looksLikeRepo = (from) => existsSync28(join31(from, "dia.json")) || existsSync28(join31(from, ".git")) || existsSync28(join31(from, "package.json"));
9672
+ var looksLikeRepo = (from) => existsSync29(join31(from, "dia.json")) || existsSync29(join31(from, ".git")) || existsSync29(join31(from, "package.json"));
8173
9673
  function linkEntry(path) {
8174
9674
  const dir = entryDir();
8175
9675
  mkdirSync19(dir, { recursive: true });
@@ -8177,7 +9677,7 @@ function linkEntry(path) {
8177
9677
  const dest = join31(dir, name);
8178
9678
  if (isSymlink2(dest))
8179
9679
  unlinkSync3(dest);
8180
- else if (existsSync28(dest))
9680
+ else if (existsSync29(dest))
8181
9681
  throw new Error(`E_CONFLICT: 稳定入口 ${dest} 已存在且不是软链`);
8182
9682
  symlinkSync3(resolve15(path), dest);
8183
9683
  return dest;
@@ -8202,7 +9702,7 @@ function add2(opts) {
8202
9702
  let copied = false;
8203
9703
  if (opts.from !== undefined) {
8204
9704
  const src = resolve15(opts.from);
8205
- if (!existsSync28(src))
9705
+ if (!existsSync29(src))
8206
9706
  throw new Error(`E_SOURCE: path not found: ${src}`);
8207
9707
  if (looksLikeRepo(src))
8208
9708
  cloneInto(src, rawDir);
@@ -8230,7 +9730,7 @@ function add2(opts) {
8230
9730
  if (svc.healthcheck)
8231
9731
  healthJobInterval(svc);
8232
9732
  const entry = resolveEntry(entryCmdOf(svc), dir);
8233
- if (!existsSync28(entry.path))
9733
+ if (!existsSync29(entry.path))
8234
9734
  throw new Error(`E_UNSTABLE_ENTRY: entry 不存在:${entry.path}`);
8235
9735
  if (!isExecutableFile(entry.path))
8236
9736
  throw new Error(`E_UNSTABLE_ENTRY: entry 不可执行:${entry.path}`);
@@ -8262,7 +9762,7 @@ function add2(opts) {
8262
9762
  err.code = code;
8263
9763
  throw err;
8264
9764
  };
8265
- if (existsSync28(serviceLockPath(svc.id)))
9765
+ if (existsSync29(serviceLockPath(svc.id)))
8266
9766
  remove3({ id: svc.id, runner });
8267
9767
  let entryPath = entry.path;
8268
9768
  if (entry.kind === "repo") {
@@ -8333,10 +9833,10 @@ function copyTree(from, to) {
8333
9833
  cpSync4(from, to, { recursive: true, dereference: true, filter: (p) => !isRuntimeNoise(relative6(from, p)) });
8334
9834
  }
8335
9835
  function copyAdopted(from, to) {
8336
- if (existsSync28(to))
9836
+ if (existsSync29(to))
8337
9837
  throw new Error(`E_CONFLICT: ${to} 已存在`);
8338
- mkdirSync19(dirname13(to), { recursive: true });
8339
- if (statSync12(from).isDirectory())
9838
+ mkdirSync19(dirname14(to), { recursive: true });
9839
+ if (statSync13(from).isDirectory())
8340
9840
  copyTree(from, to);
8341
9841
  else {
8342
9842
  mkdirSync19(to, { recursive: true });
@@ -8344,7 +9844,7 @@ function copyAdopted(from, to) {
8344
9844
  }
8345
9845
  }
8346
9846
  function isGitRepo(p) {
8347
- return existsSync28(join31(p, ".git"));
9847
+ return existsSync29(join31(p, ".git"));
8348
9848
  }
8349
9849
  function remove3(opts) {
8350
9850
  const lock = readLockFile(opts.id);
@@ -8358,7 +9858,7 @@ function remove3(opts) {
8358
9858
  }
8359
9859
  if (lock.entry.kind === "repo" && lock.entry.link && isSymlink2(lock.entry.link)) {
8360
9860
  const rel = readlinkSync2(lock.entry.link);
8361
- if (rel.startsWith(lock.dir) || resolve15(dirname13(lock.entry.link), rel).startsWith(lock.dir)) {
9861
+ if (rel.startsWith(lock.dir) || resolve15(dirname14(lock.entry.link), rel).startsWith(lock.dir)) {
8362
9862
  unlinkSync3(lock.entry.link);
8363
9863
  removed.push(lock.entry.link);
8364
9864
  } else
@@ -8371,7 +9871,7 @@ function remove3(opts) {
8371
9871
  function runServiceHealth(id, opts = {}) {
8372
9872
  const root = opts.servicesRoot ?? servicesRoot();
8373
9873
  const dir = join31(root, id);
8374
- if (!existsSync28(dir))
9874
+ if (!existsSync29(dir))
8375
9875
  throw usageError(`E_NOT_INSTALLED: service ${id}`);
8376
9876
  const svc = readServiceManifest(dir);
8377
9877
  if (!svc.healthcheck)
@@ -8591,8 +10091,11 @@ usage:
8591
10091
  dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
8592
10092
  dia agents list|register|unregister|discover
8593
10093
  dia msg deliver <agent> <id> [--transport tmux|acp]
8594
- dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
10094
+ dia task add|list|link|plan|split|status|done [--owner S] [--depends a,b] [--touches p1,p2] [--prio P1] [--start D] [--due D] [--progress 0-100]
10095
+ dia task split <id> --into "切片A,切片B" [--slice 切片A] [--role R] [--judge J] [--touches p] [--force]
8595
10096
  dia graph [--json] [--task t-...] [--tui [--watch] [--status S] [--owner S] [--width N] [--no-color]]
10097
+ dia graph --export-html [path] [--out path] [--now ISO] [--redact] [--all] [--limit N] [--force]
10098
+ dia graph --next [n] | --explain <id> | --lint # 只读编排建议(Kahn-v1)
8596
10099
  dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] [--gc [--days 7]] | dia unclaim <path|id> [--force]
8597
10100
  dia team run <id> --mock | --acp|--dsh --workspace DIR
8598
10101
  dia team gate|accept|reject|next
@@ -8636,15 +10139,49 @@ async function kb(argv) {
8636
10139
  throw e;
8637
10140
  }
8638
10141
  }
10142
+ const tagFilter = flag(args, "--tag");
10143
+ const rootsByPath = new Map;
10144
+ let kitHits = 0;
10145
+ for (const k of installed()) {
10146
+ if (tagFilter && !k.tags.includes(tagFilter))
10147
+ continue;
10148
+ let kr = [];
10149
+ try {
10150
+ if ((await indexStatus(k.dir)).state !== "ok")
10151
+ await buildIndex(k.dir);
10152
+ kr = await search(k.dir, q, undefined, limit);
10153
+ } catch {
10154
+ try {
10155
+ await buildIndex(k.dir);
10156
+ kr = await search(k.dir, q, undefined, limit);
10157
+ } catch {
10158
+ kr = [];
10159
+ }
10160
+ }
10161
+ for (const h of kr) {
10162
+ rootsByPath.set(`${k.id}:${h.path}`, k.dir);
10163
+ rows.push({ ...h, path: `${k.id}:${h.path}`, source: `kit:${k.id}@${k.version}` });
10164
+ kitHits++;
10165
+ }
10166
+ }
10167
+ if (kitHits) {
10168
+ const kitRows = rows.filter((r) => r.source);
10169
+ const personaRows = rows.filter((r) => !r.source);
10170
+ const kitQuota = Math.min(kitRows.length, Math.max(1, Math.floor(limit / 2)));
10171
+ const picked = [...personaRows.slice(0, limit - kitQuota), ...kitRows.slice(0, kitQuota)];
10172
+ rows = picked.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, limit);
10173
+ }
8639
10174
  const full = sub === "query" && has(args, "--full");
8640
10175
  const bytes = sub === "query" && has(args, "--bytes");
8641
10176
  const enrich = rows.map((r) => {
8642
10177
  const item = { ...r };
10178
+ const rowRoot = rootsByPath.get(r.path) ?? root;
10179
+ const rowPath = r.source ? r.path.split(":").slice(1).join(":") : r.path;
8643
10180
  try {
8644
10181
  if (bytes)
8645
- item.bytes = Buffer.byteLength(getDoc(root, r.path), "utf8");
10182
+ item.bytes = Buffer.byteLength(getDoc(rowRoot, rowPath), "utf8");
8646
10183
  if (full)
8647
- item.text = getDoc(root, r.path);
10184
+ item.text = getDoc(rowRoot, rowPath);
8648
10185
  } catch {
8649
10186
  item.stale_index = true;
8650
10187
  item.text = null;
@@ -8656,7 +10193,7 @@ async function kb(argv) {
8656
10193
  out(JSON.stringify(enrich));
8657
10194
  else
8658
10195
  for (const r of enrich) {
8659
- out(`${r.path} ${r.title}
10196
+ out(`${r.source ? `[${r.source}] ` : ""}${r.path} ${r.title}
8660
10197
  ${r.snippet}`);
8661
10198
  if (r.bytes !== undefined)
8662
10199
  out(` bytes ${r.bytes}`);
@@ -8935,7 +10472,18 @@ async function taskCli(argv) {
8935
10472
  err("missing title");
8936
10473
  return 2;
8937
10474
  }
8938
- const t = add(title, { role: flag(args, "--role"), judge: flag(args, "--judge"), owner: flag(args, "--owner"), deps: csv("--depends"), touches: csv("--touches") });
10475
+ const prog = flag(args, "--progress");
10476
+ const t = add(title, {
10477
+ role: flag(args, "--role"),
10478
+ judge: flag(args, "--judge"),
10479
+ owner: flag(args, "--owner"),
10480
+ deps: csv("--depends"),
10481
+ touches: csv("--touches"),
10482
+ prio: flag(args, "--prio"),
10483
+ start: flag(args, "--start"),
10484
+ due: flag(args, "--due"),
10485
+ progress: prog === undefined ? undefined : normProgress(prog)
10486
+ });
8939
10487
  out(asJson ? JSON.stringify(t) : `${t.id} ${t.title}`);
8940
10488
  return 0;
8941
10489
  }
@@ -8955,6 +10503,45 @@ async function taskCli(argv) {
8955
10503
  out(asJson ? JSON.stringify(t) : `${t.id} deps=${t.deps.join(",") || "-"} touches=${t.touches.join(",") || "-"}`);
8956
10504
  return 0;
8957
10505
  }
10506
+ if (sub === "plan") {
10507
+ const id = positional(args)[0];
10508
+ if (!id) {
10509
+ err("usage: dia task plan <id> [--prio P1] [--start YYYY-MM-DD] [--due YYYY-MM-DD] [--progress 0-100]");
10510
+ return 2;
10511
+ }
10512
+ const prog = flag(args, "--progress");
10513
+ const t = plan(id, {
10514
+ prio: flag(args, "--prio"),
10515
+ start: flag(args, "--start"),
10516
+ due: flag(args, "--due"),
10517
+ progress: prog === undefined ? undefined : normProgress(prog)
10518
+ });
10519
+ out(asJson ? JSON.stringify(t) : `${t.id} prio=${t.prio || "-"} start=${t.start || "-"} due=${t.due || "-"} progress=${t.progress ?? "-"}`);
10520
+ return 0;
10521
+ }
10522
+ if (sub === "split") {
10523
+ const id = positional(args)[0];
10524
+ const into = flag(args, "--into");
10525
+ if (!id || !into) {
10526
+ err('usage: dia task split <id> --into "切片A,切片B" [--slice 切片A] [--force]');
10527
+ return 2;
10528
+ }
10529
+ const parent = split(id, into.split(","), {
10530
+ slice: flag(args, "--slice"),
10531
+ role: flag(args, "--role"),
10532
+ judge: flag(args, "--judge"),
10533
+ touches: csv("--touches"),
10534
+ force: has(args, "--force")
10535
+ });
10536
+ const kids = (parent.split_children || []).map((cid) => {
10537
+ const c = get2(cid);
10538
+ return `${c.id} ${c.title}${c.slice ? "\t[slice]" : ""}`;
10539
+ });
10540
+ out(asJson ? JSON.stringify(parent) : `${parent.id} children=${(parent.split_children || []).length}
10541
+ ${kids.join(`
10542
+ `)}`);
10543
+ return 0;
10544
+ }
8958
10545
  if (sub === "status") {
8959
10546
  const id = positional(args)[0];
8960
10547
  const st = positional(args)[1];
@@ -8980,11 +10567,118 @@ async function taskCli(argv) {
8980
10567
  err(e?.message || String(e));
8981
10568
  return 30;
8982
10569
  }
8983
- err("usage: dia task add|list|link|status|done");
10570
+ err("usage: dia task add|list|link|plan|split|status|done");
8984
10571
  return 2;
8985
10572
  }
8986
10573
  async function graphCli(argv) {
8987
10574
  const asJson = has(argv, "--json");
10575
+ if (has(argv, "--next") || has(argv, "--explain") || has(argv, "--lint")) {
10576
+ const tasks = list4();
10577
+ const dr = derive(tasks);
10578
+ if (has(argv, "--explain")) {
10579
+ const id = flag(argv, "--explain");
10580
+ if (!id || id.startsWith("--")) {
10581
+ err("usage: dia graph --explain <id>");
10582
+ return 2;
10583
+ }
10584
+ const t = tasks.find((x) => x.id === id);
10585
+ const d = dr.byId.get(id);
10586
+ if (!t || !d) {
10587
+ err(`E_GRAPH: 任务不存在 ${id}`);
10588
+ return 2;
10589
+ }
10590
+ const info = {
10591
+ id,
10592
+ title: t.title,
10593
+ status: t.status,
10594
+ declared: t.prio ?? null,
10595
+ eff: d.eff_prio,
10596
+ inherited: d.prio_inherited,
10597
+ source: d.prio_source ?? null,
10598
+ ready: d.ready,
10599
+ startable: d.startable,
10600
+ rank: d.rank,
10601
+ blocked_by: d.blocked_by,
10602
+ released_by: d.released_by,
10603
+ dangling: d.dangling_deps,
10604
+ unblocks: d.unblocks,
10605
+ critical: d.critical,
10606
+ slice: !!t.slice
10607
+ };
10608
+ out(asJson ? JSON.stringify(info) : [
10609
+ `${id} ${t.title}`,
10610
+ `status=${t.status} declared=${info.declared ?? "-"} eff=${info.eff}${d.prio_inherited ? ` (继承自 ${d.prio_source?.id} ${d.prio_source?.declared}, ${d.prio_source?.distance} 跳)` : ""}`,
10611
+ `ready=${d.ready} startable=${d.startable} rank=${d.rank ?? "-"} unblocks=${d.unblocks} critical=${d.critical}`,
10612
+ `blocked_by=${d.blocked_by.join(",") || "-"} released_by=${d.released_by.join(",") || "-"} dangling=${d.dangling_deps.join(",") || "-"}`
10613
+ ].join(`
10614
+ `));
10615
+ return 0;
10616
+ }
10617
+ if (has(argv, "--lint")) {
10618
+ const findings = dr.lint;
10619
+ out(asJson ? JSON.stringify({ findings, cycles: dr.cycles }) : findings.length ? findings.map((f) => `${f.kind} ${f.id} ${f.detail}`).join(`
10620
+ `) : "(clean)");
10621
+ return dr.cycles.length ? 3 : 0;
10622
+ }
10623
+ const raw = flag(argv, "--next");
10624
+ const n = raw && /^\d+$/.test(raw) ? Math.max(1, Number(raw)) : 5;
10625
+ const doing = tasks.filter((t) => t.status === "doing" && dr.byId.get(t.id).rank !== null).sort((a, b) => dr.byId.get(a.id).rank - dr.byId.get(b.id).rank);
10626
+ const startable = tasks.filter((t) => dr.byId.get(t.id).startable).sort((a, b) => dr.byId.get(a.id).rank - dr.byId.get(b.id).rank);
10627
+ const picked = [...doing, ...startable].slice(0, n);
10628
+ const items = picked.map((t) => {
10629
+ const d = dr.byId.get(t.id);
10630
+ return {
10631
+ id: t.id,
10632
+ title: t.title,
10633
+ status: t.status,
10634
+ rank: d.rank,
10635
+ declared: t.prio ?? null,
10636
+ eff: d.eff_prio,
10637
+ inherited: d.prio_inherited,
10638
+ ready: d.ready,
10639
+ critical: d.critical,
10640
+ unblocks: d.unblocks,
10641
+ blocked_by: d.blocked_by,
10642
+ why: `${t.status === "doing" ? "进行中" : "可开工"} · eff=${d.eff_prio}${d.prio_inherited ? `(←${d.prio_source?.id})` : ""} · 解锁${d.unblocks}${d.critical ? " · 关键路径" : ""}`
10643
+ };
10644
+ });
10645
+ out(asJson ? JSON.stringify({ policy: dr.ranking.policy, items }) : items.length ? items.map((x, i) => `#${i + 1} ${x.id} ${x.eff}${x.inherited ? "(继承)" : ""} ${x.title} ${x.why}`).join(`
10646
+ `) : "(没有可开工任务)");
10647
+ return dr.cycles.length ? 3 : 0;
10648
+ }
10649
+ if (has(argv, "--export-html") || has(argv, "--out")) {
10650
+ const nowRaw = flag(argv, "--now");
10651
+ const now = nowRaw ? new Date(nowRaw) : new Date;
10652
+ if (Number.isNaN(now.getTime())) {
10653
+ err(`E_GRAPH: --now 不是合法时间(收到 ${nowRaw})`);
10654
+ return 2;
10655
+ }
10656
+ const limitRaw = flag(argv, "--limit");
10657
+ const limit = has(argv, "--all") ? 0 : limitRaw === undefined ? 500 : Number(limitRaw);
10658
+ if (!Number.isInteger(limit) || limit < 0) {
10659
+ err("E_GRAPH: --limit 必须是 >=0 的整数");
10660
+ return 2;
10661
+ }
10662
+ const stamp = `${localDay(now).replace(/-/g, "")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
10663
+ const argPath = flag(argv, "--export-html");
10664
+ const positionalOut = argPath && !argPath.startsWith("-") ? argPath : "";
10665
+ const outPath = flag(argv, "--out") || positionalOut || join32(diaHome(), "export", `graph-${stamp}.html`);
10666
+ if (existsSync30(outPath) && !has(argv, "--force")) {
10667
+ err(`E_GRAPH: 已存在 ${outPath}(用 --force 覆盖)`);
10668
+ return 2;
10669
+ }
10670
+ try {
10671
+ const g = build({ task: flag(argv, "--task") });
10672
+ const html = renderHtml(g, { now, redact: has(argv, "--redact"), limit, claims: list3().length });
10673
+ mkdirSync20(join32(outPath, ".."), { recursive: true });
10674
+ writeFileSync18(outPath, html, { mode: 384 });
10675
+ out(outPath);
10676
+ return 0;
10677
+ } catch (e) {
10678
+ err(e?.message || String(e));
10679
+ return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
10680
+ }
10681
+ }
8988
10682
  if (has(argv, "--tui") || has(argv, "--watch")) {
8989
10683
  const width = Number(flag(argv, "--width") || 0);
8990
10684
  const tuiOpts = {
@@ -9089,7 +10783,7 @@ function spawnDetachedDelivery(args) {
9089
10783
  const dir = join32(root, agent);
9090
10784
  mkdirSync20(dir, { recursive: true });
9091
10785
  const log = join32(dir, `deliver-${id}.log`);
9092
- const fd = openSync3(log, "a");
10786
+ const fd = openSync5(log, "a");
9093
10787
  const bin = process.argv[1];
9094
10788
  const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
9095
10789
  detached: true,
@@ -9454,6 +11148,8 @@ async function collabCli(argv) {
9454
11148
  }
9455
11149
  const kitInfo = sub === "enable" ? ensure(personaId, { force: true }) : null;
9456
11150
  out(asJson ? JSON.stringify({ ...c, removed, kit: kitInfo }, null, 1) : `${sub}d: enabled=${c.enabled}${c.disabled_personas.length ? ` disabled_personas=${c.disabled_personas.join(",")}` : ""}` + (removed.length ? ` removed=${removed.length}` : "") + (kitInfo ? kitInfo.ok ? ` kit=${kitInfo.kit}` : ` ⚠ ${kitInfo.reason}` : ""));
11151
+ if (kitInfo && !kitInfo.ok)
11152
+ return 32;
9457
11153
  return 0;
9458
11154
  }
9459
11155
  if (sub === "whoami") {
@@ -9935,7 +11631,7 @@ async function kitCli(argv) {
9935
11631
  }
9936
11632
  if (sub === "list") {
9937
11633
  const rows = list();
9938
- out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}`).join(`
11634
+ out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}${r.tags?.length ? ` [${r.tags.join(", ")}]` : ""}`).join(`
9939
11635
  `) || "(none)");
9940
11636
  return 0;
9941
11637
  }
@@ -9980,7 +11676,7 @@ function wtCli(argv) {
9980
11676
  home,
9981
11677
  resolvePath(fileURLToPath4(new URL("../", import.meta.url)), "packages/wt/bin/dia-wt")
9982
11678
  ].filter(Boolean);
9983
- const bin = candidates.find((c) => existsSync29(c));
11679
+ const bin = candidates.find((c) => existsSync30(c));
9984
11680
  if (!bin) {
9985
11681
  err("E_WT: dia-wt 未安装(先跑 dia install.sh,或设 DIA_WT_BIN)");
9986
11682
  return 33;