@minonl/dia 0.0.8 → 0.0.10
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/README.md +12 -0
- package/dist/cli.js +425 -22
- package/docs/index.md +1 -1
- package/docs/msg-collab-m1.md +29 -0
- package/docs/overview.md +6 -0
- package/docs/service-noun.md +113 -3
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +237 -94
- package/packages/wt/tests/adopt.test.sh +80 -154
package/README.md
CHANGED
|
@@ -90,6 +90,18 @@ bash tests/smoke.sh # 用 Node 跑一遍构建产物
|
|
|
90
90
|
bun src/cli.ts version # 直接跑源码
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### CI 矩阵(`.github/workflows/test.yml`)
|
|
94
|
+
|
|
95
|
+
| job | 触发 | 是否 required | 说明 |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `test (ubuntu-latest)` | PR | ✅ | 全量:单测 / kb / job / build / install / smoke / e2e |
|
|
98
|
+
| `test (macos-latest)` | PR | ✅ | 同上(**macOS 特有差异只有这里能抓到**,例如没有 `timeout(1)`) |
|
|
99
|
+
| `windows-smoke` | PR(代码改动) | ❌ 非阻塞 | 装依赖 + `dia --version` + 纯逻辑测试;`dia job` 的 Windows 后端尚未实现(`install` 返回 rc=3),落地后再升 required |
|
|
100
|
+
| `changes` | PR | ❌ | 判定改动是否只碰 `docs/**` / `llms*.txt`:是则跳过测试矩阵(docs-only 快通道) |
|
|
101
|
+
|
|
102
|
+
> 只在 `pull_request` 上跑(不再对特性分支的 push 重复跑)。
|
|
103
|
+
> 改这里时**保持 `test (…)` 的 job 名不变**——分支保护按名字要求这两个 check。
|
|
104
|
+
|
|
93
105
|
## 命令
|
|
94
106
|
|
|
95
107
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -4720,6 +4720,67 @@ function build(opts = {}) {
|
|
|
4720
4720
|
return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles(tasks) };
|
|
4721
4721
|
}
|
|
4722
4722
|
|
|
4723
|
+
// src/graph-tui.ts
|
|
4724
|
+
var ORDER = ["doing", "blocked", "review", "todo", "awaiting_acceptance", "accepted", "done", "dropped", "rejected", "escalated"];
|
|
4725
|
+
var color = (s, code, on) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
4726
|
+
function render2(g, opts = {}) {
|
|
4727
|
+
const on = opts.color ?? false;
|
|
4728
|
+
const tasks = g.nodes.filter((n) => n.kind === "task");
|
|
4729
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
4730
|
+
const edgesFrom = (id, kind) => g.edges.filter((e) => e.from === id && e.kind === kind);
|
|
4731
|
+
const lines = [];
|
|
4732
|
+
const groups = new Map;
|
|
4733
|
+
for (const t of tasks) {
|
|
4734
|
+
const st = t.status || "todo";
|
|
4735
|
+
groups.set(st, [...groups.get(st) || [], t]);
|
|
4736
|
+
}
|
|
4737
|
+
for (const st of [...ORDER.filter((s) => groups.has(s)), ...[...groups.keys()].filter((s) => !ORDER.includes(s))]) {
|
|
4738
|
+
lines.push(color(`[${st}]`, "1;36", on));
|
|
4739
|
+
for (const t of groups.get(st).sort((a, b) => a.label.localeCompare(b.label))) {
|
|
4740
|
+
lines.push(` ${color(t.label, "1", on)} ${color(t.id.replace(/^task:/, ""), "2", on)}${t.owner ? ` owner=${t.owner}` : ""}`);
|
|
4741
|
+
for (const e of edgesFrom(t.id, "depends_on")) {
|
|
4742
|
+
const dep = taskById.get(e.to);
|
|
4743
|
+
lines.push(` ${color("↳ depends", "34", on)} ${dep?.label || e.to}${dep ? ` [${dep.status}]` : ""}`);
|
|
4744
|
+
}
|
|
4745
|
+
for (const e of edgesFrom(t.id, "touches"))
|
|
4746
|
+
lines.push(` ${color("↳ touches", "35", on)} ${e.to.replace(/^path:/, "")}`);
|
|
4747
|
+
if (t.blocked_by?.length)
|
|
4748
|
+
lines.push(` ${color(`⚠ blocked_by ${t.blocked_by.join(", ")}`, "31", on)}`);
|
|
4749
|
+
}
|
|
4750
|
+
lines.push("");
|
|
4751
|
+
}
|
|
4752
|
+
if (!tasks.length)
|
|
4753
|
+
lines.push("(no tasks)");
|
|
4754
|
+
const claimEdges = g.edges.filter((e) => e.kind === "claims");
|
|
4755
|
+
if (claimEdges.length) {
|
|
4756
|
+
lines.push(color("claims", "1;36", on));
|
|
4757
|
+
for (const e of claimEdges)
|
|
4758
|
+
lines.push(` ${e.from.replace(/^session:/, "")} → ${e.to.replace(/^path:/, "")}`);
|
|
4759
|
+
lines.push("");
|
|
4760
|
+
}
|
|
4761
|
+
for (const c of g.cycles)
|
|
4762
|
+
lines.push(color(`⚠ cycle: ${c.join(" -> ")}`, "31", on));
|
|
4763
|
+
return lines.join(`
|
|
4764
|
+
`);
|
|
4765
|
+
}
|
|
4766
|
+
function watch(build, opts = {}) {
|
|
4767
|
+
const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), { color: opts.color ?? process.stdout.isTTY }) + `
|
|
4768
|
+
`);
|
|
4769
|
+
paint();
|
|
4770
|
+
const timer = setInterval(() => {
|
|
4771
|
+
try {
|
|
4772
|
+
paint();
|
|
4773
|
+
} catch {}
|
|
4774
|
+
}, opts.intervalMs ?? 2000);
|
|
4775
|
+
const stop = () => {
|
|
4776
|
+
clearInterval(timer);
|
|
4777
|
+
process.exit(0);
|
|
4778
|
+
};
|
|
4779
|
+
process.on("SIGINT", stop);
|
|
4780
|
+
process.on("SIGTERM", stop);
|
|
4781
|
+
return new Promise(() => {});
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4723
4784
|
// packages/cli/src/index.ts
|
|
4724
4785
|
init_paths();
|
|
4725
4786
|
|
|
@@ -5491,6 +5552,25 @@ function jobRootResolved() {
|
|
|
5491
5552
|
const legacy = join24(homedir11(), ".local", "share", "dia-job");
|
|
5492
5553
|
return !existsSync21(next) && existsSync21(legacy) ? legacy : next;
|
|
5493
5554
|
}
|
|
5555
|
+
function runJobCapture(argv) {
|
|
5556
|
+
const { bin, error } = jobBin();
|
|
5557
|
+
if (!bin) {
|
|
5558
|
+
process.stderr.write(`dia job: ${error || "未找到 dia-job;先跑 packages/job/install.sh 或设置 DIA_JOB_BIN"}
|
|
5559
|
+
`);
|
|
5560
|
+
return { status: 2, stdout: "", stderr: "" };
|
|
5561
|
+
}
|
|
5562
|
+
const r = spawnSync2(bin, argv, {
|
|
5563
|
+
encoding: "utf8",
|
|
5564
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
5565
|
+
env: { ...process.env, DIA_JOB_ROOT: jobRootResolved() }
|
|
5566
|
+
});
|
|
5567
|
+
if (r.error) {
|
|
5568
|
+
process.stderr.write(`dia job: ${r.error.message}
|
|
5569
|
+
`);
|
|
5570
|
+
return { status: 1, stdout: "", stderr: "" };
|
|
5571
|
+
}
|
|
5572
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
5573
|
+
}
|
|
5494
5574
|
function runJob(argv) {
|
|
5495
5575
|
const { bin, error } = jobBin();
|
|
5496
5576
|
if (!bin) {
|
|
@@ -6020,7 +6100,7 @@ function askBroker(req, timeoutMs = 5000) {
|
|
|
6020
6100
|
}
|
|
6021
6101
|
|
|
6022
6102
|
// packages/cli/src/index.ts
|
|
6023
|
-
import { execFileSync as execFileSync12, spawn as
|
|
6103
|
+
import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
6024
6104
|
import { mkdirSync as mkdirSync20, openSync as openSync3 } from "node:fs";
|
|
6025
6105
|
import { basename as basename10, join as join32 } from "node:path";
|
|
6026
6106
|
import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
|
|
@@ -7016,7 +7096,7 @@ function propose(opts = {}) {
|
|
|
7016
7096
|
}
|
|
7017
7097
|
|
|
7018
7098
|
// src/service.ts
|
|
7019
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
7099
|
+
import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
|
|
7020
7100
|
import { createHash as createHash11, randomBytes as randomBytes4 } from "node:crypto";
|
|
7021
7101
|
import {
|
|
7022
7102
|
appendFileSync as appendFileSync9,
|
|
@@ -7041,6 +7121,52 @@ var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
|
7041
7121
|
var VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
7042
7122
|
var LIFECYCLE_TYPES = new Set(["periodic", "bounded", "service"]);
|
|
7043
7123
|
var STDIN_TYPES = new Set(["json", "text", "none"]);
|
|
7124
|
+
var ACP_HOSTS = ["dsh", "opencode"];
|
|
7125
|
+
var HEALTHCHECK_KINDS = new Set(["script", "acp"]);
|
|
7126
|
+
var isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
7127
|
+
var isNonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
|
|
7128
|
+
function acpAgentCommand(agent) {
|
|
7129
|
+
if (!ACP_HOSTS.includes(agent?.host))
|
|
7130
|
+
throw new Error(`E_MANIFEST: entry.agent.host must be ${ACP_HOSTS.join("|")}`);
|
|
7131
|
+
const profile = agent.profile?.trim();
|
|
7132
|
+
if (agent.host === "dsh")
|
|
7133
|
+
return ["dsh", ...profile ? ["--profile", profile] : [], "acp"];
|
|
7134
|
+
return ["opencode", "acp"];
|
|
7135
|
+
}
|
|
7136
|
+
var isAgentEntry = (e) => !!e && e.agent !== undefined && e.agent !== null;
|
|
7137
|
+
var healthcheckKind = (hc) => hc?.kind ?? "script";
|
|
7138
|
+
var entryCmdOf = (svc) => svc.entry.cmd ?? "";
|
|
7139
|
+
function checkAgentEntry(a) {
|
|
7140
|
+
if (!isPlainObject(a))
|
|
7141
|
+
throw new Error("E_MANIFEST: entry.agent must be an object");
|
|
7142
|
+
if (!ACP_HOSTS.includes(a.host))
|
|
7143
|
+
throw new Error(`E_MANIFEST: entry.agent.host must be ${ACP_HOSTS.join("|")}`);
|
|
7144
|
+
for (const k of ["profile", "persona"]) {
|
|
7145
|
+
if (a[k] !== undefined && !isNonEmptyString(a[k]))
|
|
7146
|
+
throw new Error(`E_MANIFEST: entry.agent.${k} must be a non-empty string`);
|
|
7147
|
+
}
|
|
7148
|
+
if (a.host !== "dsh" && a.profile !== undefined) {
|
|
7149
|
+
throw new Error(`E_MANIFEST: entry.agent.profile 只适用于 host=dsh(${a.host} 没有 profile)`);
|
|
7150
|
+
}
|
|
7151
|
+
return a;
|
|
7152
|
+
}
|
|
7153
|
+
function checkEntry(entry) {
|
|
7154
|
+
if (!isPlainObject(entry))
|
|
7155
|
+
throw new Error("E_MANIFEST: entry required");
|
|
7156
|
+
const hasCmd = entry.cmd !== undefined && entry.cmd !== null;
|
|
7157
|
+
const hasAgent = entry.agent !== undefined && entry.agent !== null;
|
|
7158
|
+
if (hasCmd && hasAgent)
|
|
7159
|
+
throw new Error("E_MANIFEST: entry.cmd 与 entry.agent 互斥,只能声明其一");
|
|
7160
|
+
if (!hasCmd && !hasAgent)
|
|
7161
|
+
throw new Error("E_MANIFEST: entry.cmd required(脚本型)或 entry.agent(agent 型)");
|
|
7162
|
+
if (hasCmd) {
|
|
7163
|
+
if (typeof entry.cmd !== "string" || !entry.cmd.trim())
|
|
7164
|
+
throw new Error("E_MANIFEST: entry.cmd required");
|
|
7165
|
+
} else {
|
|
7166
|
+
checkAgentEntry(entry.agent);
|
|
7167
|
+
}
|
|
7168
|
+
return entry;
|
|
7169
|
+
}
|
|
7044
7170
|
function entryDir() {
|
|
7045
7171
|
return process.env.DIA_JOB_ENTRY_DIR || join31(jobRoot2(), "bin");
|
|
7046
7172
|
}
|
|
@@ -7083,8 +7209,14 @@ var LEVELS = ["ok", "degraded", "fail"];
|
|
|
7083
7209
|
function checkHealthcheck(hc) {
|
|
7084
7210
|
if (hc === null || typeof hc !== "object" || Array.isArray(hc))
|
|
7085
7211
|
throw new Error("E_MANIFEST: healthcheck must be an object");
|
|
7086
|
-
if (
|
|
7087
|
-
throw new Error("E_MANIFEST: healthcheck.
|
|
7212
|
+
if (hc.kind !== undefined && !HEALTHCHECK_KINDS.has(hc.kind))
|
|
7213
|
+
throw new Error("E_MANIFEST: healthcheck.kind must be script|acp");
|
|
7214
|
+
if (healthcheckKind(hc) === "script") {
|
|
7215
|
+
if (typeof hc.cmd !== "string" || !hc.cmd.trim())
|
|
7216
|
+
throw new Error("E_MANIFEST: healthcheck.cmd required");
|
|
7217
|
+
} else if (hc.cmd !== undefined) {
|
|
7218
|
+
throw new Error("E_MANIFEST: healthcheck.kind=acp 不得再声明 cmd(判据是 initialize + session/new,不是外部脚本)");
|
|
7219
|
+
}
|
|
7088
7220
|
if (hc.interval !== undefined) {
|
|
7089
7221
|
const why = checkCronInterval(hc.interval);
|
|
7090
7222
|
if (why)
|
|
@@ -7119,10 +7251,14 @@ function validateServiceManifest(m) {
|
|
|
7119
7251
|
throw new Error("E_MANIFEST: bad id");
|
|
7120
7252
|
if (!m.version || !VERSION_RE.test(m.version))
|
|
7121
7253
|
throw new Error("E_MANIFEST: bad version");
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
if (
|
|
7125
|
-
throw new Error(
|
|
7254
|
+
checkEntry(m.entry);
|
|
7255
|
+
const agentEntry = isAgentEntry(m.entry);
|
|
7256
|
+
if (agentEntry && healthcheckKind(m.healthcheck) !== "acp") {
|
|
7257
|
+
throw new Error('E_MANIFEST: entry.agent 必须配 healthcheck.kind="acp"(脚本型 healthcheck 无 cmd 可执行)');
|
|
7258
|
+
}
|
|
7259
|
+
if (!agentEntry && m.healthcheck && healthcheckKind(m.healthcheck) === "acp") {
|
|
7260
|
+
throw new Error('E_MANIFEST: healthcheck.kind="acp" 只适用于 entry.agent 型服务');
|
|
7261
|
+
}
|
|
7126
7262
|
if (m.lifecycle === null || typeof m.lifecycle !== "object" || Array.isArray(m.lifecycle))
|
|
7127
7263
|
throw new Error("E_MANIFEST: lifecycle required");
|
|
7128
7264
|
if (!LIFECYCLE_TYPES.has(m.lifecycle.type))
|
|
@@ -7237,7 +7373,18 @@ var STALE_FACTOR = 1.5;
|
|
|
7237
7373
|
var MIN_HEALTH_INTERVAL = 60;
|
|
7238
7374
|
var healthInterval = (hc) => hc?.interval ?? DEFAULT_HEALTH_INTERVAL;
|
|
7239
7375
|
var staleAfter = (hc) => Math.floor(healthInterval(hc) * STALE_FACTOR);
|
|
7240
|
-
var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" &&
|
|
7376
|
+
var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" && (v.state === "ok" || v.state === "degraded" || v.state === "fail");
|
|
7377
|
+
var TS_RE = /"ts"\s*:\s*(-?\d+)/;
|
|
7378
|
+
var KIND_RE = /"kind"\s*:\s*"([A-Za-z0-9_-]+)"/;
|
|
7379
|
+
function parseStateLine(line) {
|
|
7380
|
+
const head = line.slice(0, 256), tail = line.slice(-256);
|
|
7381
|
+
const ts = TS_RE.exec(head)?.[1] ?? TS_RE.exec(tail)?.[1];
|
|
7382
|
+
const kind = KIND_RE.exec(head)?.[1] ?? KIND_RE.exec(tail)?.[1];
|
|
7383
|
+
if (!ts || !kind)
|
|
7384
|
+
return null;
|
|
7385
|
+
const n = Number(ts);
|
|
7386
|
+
return Number.isFinite(n) ? { ts: n, kind } : null;
|
|
7387
|
+
}
|
|
7241
7388
|
function findLastHealth(lines, service, gen) {
|
|
7242
7389
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
7243
7390
|
const r = lines[i];
|
|
@@ -7422,6 +7569,24 @@ function diaCmd() {
|
|
|
7422
7569
|
const entry = entries.find((p) => existsSync28(p));
|
|
7423
7570
|
return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
|
|
7424
7571
|
}
|
|
7572
|
+
function decorateStatus(text, view) {
|
|
7573
|
+
const lines = String(text ?? "").split(`
|
|
7574
|
+
`);
|
|
7575
|
+
const outLines = [];
|
|
7576
|
+
for (const line of lines) {
|
|
7577
|
+
outLines.push(line);
|
|
7578
|
+
const m = line.match(/local\.dia-job-svc-([A-Za-z0-9._-]+)/) || line.match(/(?:^|\s)svc-([A-Za-z0-9._-]+)\s/);
|
|
7579
|
+
if (!m)
|
|
7580
|
+
continue;
|
|
7581
|
+
try {
|
|
7582
|
+
const v = view(m[1]);
|
|
7583
|
+
if (v?.label)
|
|
7584
|
+
outLines.push(` health=${v.label}`);
|
|
7585
|
+
} catch {}
|
|
7586
|
+
}
|
|
7587
|
+
return outLines.join(`
|
|
7588
|
+
`);
|
|
7589
|
+
}
|
|
7425
7590
|
function servicesRoot() {
|
|
7426
7591
|
return process.env.DIA_SERVICE_HOME || join31(diaHome(), "services");
|
|
7427
7592
|
}
|
|
@@ -7456,15 +7621,34 @@ function readHealthLines(file) {
|
|
|
7456
7621
|
if (!existsSync28(file))
|
|
7457
7622
|
return [];
|
|
7458
7623
|
const rows = [];
|
|
7459
|
-
|
|
7460
|
-
`)
|
|
7624
|
+
const lines = readFileSync25(file, "utf8").split(`
|
|
7625
|
+
`);
|
|
7626
|
+
for (const line of lines) {
|
|
7461
7627
|
if (!line.trim())
|
|
7462
7628
|
continue;
|
|
7463
7629
|
try {
|
|
7464
7630
|
const d = JSON.parse(line);
|
|
7465
7631
|
if (isRecord(d))
|
|
7466
7632
|
rows.push(d);
|
|
7633
|
+
continue;
|
|
7467
7634
|
} catch {}
|
|
7635
|
+
const head = parseStateLine(line);
|
|
7636
|
+
const state = head ? /"state"\s*:\s*"(ok|degraded|fail)"/.exec(line)?.[1] : undefined;
|
|
7637
|
+
if (head && state) {
|
|
7638
|
+
rows.push({
|
|
7639
|
+
...head,
|
|
7640
|
+
state,
|
|
7641
|
+
service: "",
|
|
7642
|
+
gen: "",
|
|
7643
|
+
source: "schedule",
|
|
7644
|
+
candidate: state,
|
|
7645
|
+
streak: 0,
|
|
7646
|
+
raw: state,
|
|
7647
|
+
exit: null,
|
|
7648
|
+
timeout: false,
|
|
7649
|
+
detail: ""
|
|
7650
|
+
});
|
|
7651
|
+
}
|
|
7468
7652
|
}
|
|
7469
7653
|
return rows;
|
|
7470
7654
|
}
|
|
@@ -7473,6 +7657,41 @@ function appendHealthRecord(file, rec) {
|
|
|
7473
7657
|
appendFileSync9(file, JSON.stringify(rec) + `
|
|
7474
7658
|
`);
|
|
7475
7659
|
}
|
|
7660
|
+
function readServiceHealth(id, opts = {}) {
|
|
7661
|
+
const { healthFile } = opts;
|
|
7662
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
7663
|
+
const jr = opts.jobRoot ?? jobRoot2();
|
|
7664
|
+
const dir = opts.servicesRoot ?? (process.env.DIA_SERVICE_HOME || join31(jr, "services"));
|
|
7665
|
+
const installDir = join31(dir, id);
|
|
7666
|
+
let lock = null;
|
|
7667
|
+
try {
|
|
7668
|
+
lock = readLockFile(id);
|
|
7669
|
+
} catch {}
|
|
7670
|
+
let svc = null;
|
|
7671
|
+
try {
|
|
7672
|
+
svc = readServiceManifest(lock?.dir ?? installDir);
|
|
7673
|
+
} catch {}
|
|
7674
|
+
if (!svc)
|
|
7675
|
+
return healthView({ adopted: true, hasHealthcheck: false, now });
|
|
7676
|
+
const hc = svc?.healthcheck;
|
|
7677
|
+
if (!hc)
|
|
7678
|
+
return healthView({ hasHealthcheck: false, now });
|
|
7679
|
+
const ledger = healthFile ?? healthStatePath(id);
|
|
7680
|
+
const recs = readHealthLines(ledger).filter(isRecord);
|
|
7681
|
+
let rec = lock ? findLastHealth(recs, id, lock.gen) : null;
|
|
7682
|
+
if (!rec) {
|
|
7683
|
+
for (let i = recs.length - 1;i >= 0; i--) {
|
|
7684
|
+
const r = recs[i];
|
|
7685
|
+
if (r.state !== "ok" && r.state !== "degraded" && r.state !== "fail")
|
|
7686
|
+
continue;
|
|
7687
|
+
if (r.service && r.service !== id)
|
|
7688
|
+
continue;
|
|
7689
|
+
if (!rec || r.ts > rec.ts)
|
|
7690
|
+
rec = r;
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
return healthView({ record: rec, hasHealthcheck: true, hc, now });
|
|
7694
|
+
}
|
|
7476
7695
|
function listServices(opts = {}) {
|
|
7477
7696
|
const root = opts.servicesRoot ?? servicesRoot();
|
|
7478
7697
|
const jr = opts.jobRoot ?? jobRoot2();
|
|
@@ -7641,6 +7860,134 @@ function runHealth(svc, opts) {
|
|
|
7641
7860
|
}
|
|
7642
7861
|
return { judgement, report, stdout, stderr, ms };
|
|
7643
7862
|
}
|
|
7863
|
+
async function runAcpHealth(svc, opts) {
|
|
7864
|
+
const hc = svc.healthcheck;
|
|
7865
|
+
if (!hc || healthcheckKind(hc) !== "acp")
|
|
7866
|
+
throw new Error("E_MANIFEST: runAcpHealth 只处理 healthcheck.kind=acp");
|
|
7867
|
+
const agent = svc.entry.agent;
|
|
7868
|
+
if (!agent)
|
|
7869
|
+
throw new Error("E_MANIFEST: healthcheck.kind=acp 需要 entry.agent");
|
|
7870
|
+
const timeoutMs = (hc.timeout ?? DEFAULT_HEALTH_TIMEOUT) * 1000;
|
|
7871
|
+
const now = opts.now ?? Date.now;
|
|
7872
|
+
const t0 = now();
|
|
7873
|
+
const argv = acpAgentCommand(agent);
|
|
7874
|
+
const env = { ...process.env, ...opts.env ?? {} };
|
|
7875
|
+
const steps = [];
|
|
7876
|
+
const finish = (status, exit) => {
|
|
7877
|
+
const judgement = {
|
|
7878
|
+
...judgeHealth(exit, {}),
|
|
7879
|
+
state: status,
|
|
7880
|
+
exit,
|
|
7881
|
+
timeout: false,
|
|
7882
|
+
detail: steps.filter((x) => x.state !== "ok").map((x) => `${x.step}: ${x.detail}`).join("; ") || `initialize + session/new ok(${agent.host}${agent.profile ? " --profile " + agent.profile : ""})`
|
|
7883
|
+
};
|
|
7884
|
+
return { judgement, report: { status, checks: steps.map((x) => ({ name: x.step, status: x.state, detail: x.detail })) }, ms: now() - t0, steps };
|
|
7885
|
+
};
|
|
7886
|
+
const child = spawn2(argv[0], argv.slice(1), { cwd: opts.dir, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
7887
|
+
const pending = new Map;
|
|
7888
|
+
let buf = "";
|
|
7889
|
+
child.stdout.setEncoding("utf8");
|
|
7890
|
+
child.stdout.on("data", (chunk) => {
|
|
7891
|
+
buf += chunk;
|
|
7892
|
+
for (let i = buf.indexOf(`
|
|
7893
|
+
`);i >= 0; i = buf.indexOf(`
|
|
7894
|
+
`)) {
|
|
7895
|
+
const line = buf.slice(0, i).trim();
|
|
7896
|
+
buf = buf.slice(i + 1);
|
|
7897
|
+
if (!line)
|
|
7898
|
+
continue;
|
|
7899
|
+
let msg;
|
|
7900
|
+
try {
|
|
7901
|
+
msg = JSON.parse(line);
|
|
7902
|
+
} catch {
|
|
7903
|
+
continue;
|
|
7904
|
+
}
|
|
7905
|
+
const waiter = msg && msg.id !== undefined ? pending.get(msg.id) : undefined;
|
|
7906
|
+
if (waiter) {
|
|
7907
|
+
pending.delete(msg.id);
|
|
7908
|
+
waiter(msg);
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
});
|
|
7912
|
+
const spawnError = await new Promise((resolve) => {
|
|
7913
|
+
let settled = false;
|
|
7914
|
+
const done = (v) => {
|
|
7915
|
+
if (!settled) {
|
|
7916
|
+
settled = true;
|
|
7917
|
+
resolve(v);
|
|
7918
|
+
}
|
|
7919
|
+
};
|
|
7920
|
+
child.once("error", (e) => done(String(e?.message || e)));
|
|
7921
|
+
child.once("spawn", () => done(null));
|
|
7922
|
+
setTimeout(() => done(null), 50);
|
|
7923
|
+
});
|
|
7924
|
+
if (spawnError)
|
|
7925
|
+
return steps.push({ step: "initialize", state: "degraded", detail: `端点起不来:${spawnError}` }), finish("degraded", 20);
|
|
7926
|
+
const call = (id, method, params) => new Promise((resolve) => {
|
|
7927
|
+
const timer = setTimeout(() => {
|
|
7928
|
+
pending.delete(id);
|
|
7929
|
+
resolve({ timeout: true });
|
|
7930
|
+
}, timeoutMs);
|
|
7931
|
+
pending.set(id, (m) => {
|
|
7932
|
+
clearTimeout(timer);
|
|
7933
|
+
resolve(m);
|
|
7934
|
+
});
|
|
7935
|
+
try {
|
|
7936
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + `
|
|
7937
|
+
`);
|
|
7938
|
+
} catch (e) {
|
|
7939
|
+
clearTimeout(timer);
|
|
7940
|
+
pending.delete(id);
|
|
7941
|
+
resolve({ died: String(e?.message || e) });
|
|
7942
|
+
}
|
|
7943
|
+
});
|
|
7944
|
+
try {
|
|
7945
|
+
const init = await call(1, "initialize", { protocolVersion: 1, clientCapabilities: {} });
|
|
7946
|
+
if (init.timeout)
|
|
7947
|
+
steps.push({ step: "initialize", state: "degraded", detail: "initialize 超时" });
|
|
7948
|
+
else if (init.died)
|
|
7949
|
+
steps.push({ step: "initialize", state: "degraded", detail: `端点已退出:${init.died}` });
|
|
7950
|
+
else if (init.error)
|
|
7951
|
+
steps.push({ step: "initialize", state: "degraded", detail: `initialize 报错:${init.error?.message ?? JSON.stringify(init.error)}` });
|
|
7952
|
+
else if (!init.result)
|
|
7953
|
+
steps.push({ step: "initialize", state: "degraded", detail: "initialize 无 result" });
|
|
7954
|
+
else
|
|
7955
|
+
steps.push({ step: "initialize", state: "ok", detail: "initialize ok" });
|
|
7956
|
+
if (steps[0].state !== "ok")
|
|
7957
|
+
return finish("degraded", 20);
|
|
7958
|
+
const ns = await call(2, "session/new", { cwd: opts.dir, mcpServers: [] });
|
|
7959
|
+
if (ns.timeout)
|
|
7960
|
+
steps.push({ step: "session/new", state: "fail", detail: "session/new 超时" });
|
|
7961
|
+
else if (ns.error)
|
|
7962
|
+
steps.push({ step: "session/new", state: "fail", detail: `session/new 报错:${ns.error?.message ?? JSON.stringify(ns.error)}` });
|
|
7963
|
+
else if (!ns.result?.sessionId)
|
|
7964
|
+
steps.push({ step: "session/new", state: "fail", detail: "session/new 未返回 sessionId" });
|
|
7965
|
+
else
|
|
7966
|
+
steps.push({ step: "session/new", state: "ok", detail: `session ${ns.result.sessionId}` });
|
|
7967
|
+
return steps[1].state === "ok" ? finish("ok", 0) : finish("fail", 21);
|
|
7968
|
+
} finally {
|
|
7969
|
+
try {
|
|
7970
|
+
child.kill("SIGKILL");
|
|
7971
|
+
} catch {}
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
function recordHealth(svc, judgement, opts = {}) {
|
|
7975
|
+
const id = svc.id;
|
|
7976
|
+
const gen = opts.gen || generationOf(id);
|
|
7977
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
7978
|
+
const file = opts.stateFile || process.env.DIA_SERVICE_HEALTH_FILE || healthStatePath(id);
|
|
7979
|
+
const record = transition(findLastHealth(readHealthLines(file), id, gen), {
|
|
7980
|
+
service: id,
|
|
7981
|
+
gen,
|
|
7982
|
+
source: opts.source ?? "manual",
|
|
7983
|
+
ts: now,
|
|
7984
|
+
judgement,
|
|
7985
|
+
debounce: svc.healthcheck?.debounce,
|
|
7986
|
+
checks: opts.checks
|
|
7987
|
+
});
|
|
7988
|
+
appendHealthRecord(file, record);
|
|
7989
|
+
return { record, statePath: file };
|
|
7990
|
+
}
|
|
7644
7991
|
function whichTimeout1() {
|
|
7645
7992
|
const probe = spawnSync4("bash", ["-c", "command -v timeout || command -v gtimeout"], { encoding: "utf8" });
|
|
7646
7993
|
const p = String(probe.stdout ?? "").trim().split(`
|
|
@@ -7699,7 +8046,7 @@ function collectManagedFiles(base, hc) {
|
|
|
7699
8046
|
}
|
|
7700
8047
|
function verify4(dir, opts = {}) {
|
|
7701
8048
|
const svc = readServiceManifest(dir);
|
|
7702
|
-
const entry = resolveEntry(svc
|
|
8049
|
+
const entry = resolveEntry(entryCmdOf(svc), dir);
|
|
7703
8050
|
const drift = [];
|
|
7704
8051
|
if (!existsSync28(entry.path))
|
|
7705
8052
|
drift.push({ code: "entry_missing", detail: `entry 不存在:${entry.path}` });
|
|
@@ -7802,7 +8149,7 @@ function add2(opts) {
|
|
|
7802
8149
|
}
|
|
7803
8150
|
if (svc.healthcheck)
|
|
7804
8151
|
healthJobInterval(svc);
|
|
7805
|
-
const entry = resolveEntry(svc
|
|
8152
|
+
const entry = resolveEntry(entryCmdOf(svc), dir);
|
|
7806
8153
|
if (!existsSync28(entry.path))
|
|
7807
8154
|
throw new Error(`E_UNSTABLE_ENTRY: entry 不存在:${entry.path}`);
|
|
7808
8155
|
if (!isExecutableFile(entry.path))
|
|
@@ -7816,7 +8163,7 @@ function add2(opts) {
|
|
|
7816
8163
|
gen,
|
|
7817
8164
|
dryRun: !!opts.dryRun,
|
|
7818
8165
|
backend,
|
|
7819
|
-
entry: { declared: svc
|
|
8166
|
+
entry: { declared: entryCmdOf(svc), kind: entry.kind, path: entry.path, link: null },
|
|
7820
8167
|
jobs: { service: serviceJobName(svc.id), health: svc.healthcheck ? healthJobName(svc.id) : null },
|
|
7821
8168
|
target_mismatch: mismatch
|
|
7822
8169
|
};
|
|
@@ -7825,7 +8172,7 @@ function add2(opts) {
|
|
|
7825
8172
|
rmSync9(dir, { recursive: true, force: true });
|
|
7826
8173
|
return result;
|
|
7827
8174
|
}
|
|
7828
|
-
const backfill = ["--allow-unstable", "--desc", `dia service ${svc.id}: healthcheck 只读判定,稳定入口 ${entryPathOf(svc
|
|
8175
|
+
const backfill = ["--allow-unstable", "--desc", `dia service ${svc.id}: healthcheck 只读判定,稳定入口 ${entryPathOf(entryCmdOf(svc), dir)}`];
|
|
7829
8176
|
const runner = opts.runner ?? defaultRunner({ backfill });
|
|
7830
8177
|
const installed = [];
|
|
7831
8178
|
const fail = (code, msg, rc) => {
|
|
@@ -7842,7 +8189,7 @@ function add2(opts) {
|
|
|
7842
8189
|
entryPath = linkEntry(entry.path);
|
|
7843
8190
|
result.entry.link = entryPath;
|
|
7844
8191
|
result.entry.path = entryPath;
|
|
7845
|
-
} else if (svc.
|
|
8192
|
+
} else if (entryCmdOf(svc).startsWith(entryDir())) {
|
|
7846
8193
|
result.entry.link = entry.path;
|
|
7847
8194
|
}
|
|
7848
8195
|
const jobArgs = (name, cmd) => {
|
|
@@ -8159,13 +8506,13 @@ usage:
|
|
|
8159
8506
|
dia collab status|enable|disable [--persona ID] # dia-msg 默认能力开关(默认开)
|
|
8160
8507
|
dia whoami [--json] [--name NAME] # 当前会话身份(session id)
|
|
8161
8508
|
dia in [--unread] [--all] [--legacy] [--json] # 会话收件箱(正文内联)
|
|
8162
|
-
dia out --to <session_id> --subject S (--body B|--body-file F) [--kind K] [--reply-to ID]
|
|
8509
|
+
dia out --to <session_id> --subject S (--body B|--body-file F) [--kind K] [--touch p1,p2] [--reply-to ID]
|
|
8163
8510
|
dia ack <id> | dia reply <id> --body B | dia close <id> --reason R | dia show <id>
|
|
8164
8511
|
dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
|
|
8165
8512
|
dia agents list|register|unregister|discover
|
|
8166
8513
|
dia msg deliver <agent> <id> [--transport tmux|acp]
|
|
8167
8514
|
dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
|
|
8168
|
-
dia graph [--json] [--task t-...]
|
|
8515
|
+
dia graph [--json] [--task t-...] [--tui [--watch] [--no-color]]
|
|
8169
8516
|
dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] | dia unclaim <path|id> [--force]
|
|
8170
8517
|
dia team run <id> --mock | --acp|--dsh --workspace DIR
|
|
8171
8518
|
dia team gate|accept|reject|next
|
|
@@ -8558,6 +8905,19 @@ async function taskCli(argv) {
|
|
|
8558
8905
|
}
|
|
8559
8906
|
async function graphCli(argv) {
|
|
8560
8907
|
const asJson = has(argv, "--json");
|
|
8908
|
+
if (has(argv, "--tui") || has(argv, "--watch")) {
|
|
8909
|
+
const paint = (g) => out(render2(g, { color: !!process.stdout.isTTY && !has(argv, "--no-color") }));
|
|
8910
|
+
try {
|
|
8911
|
+
if (has(argv, "--watch"))
|
|
8912
|
+
return await watch(() => build({ task: flag(argv, "--task") }), { color: !!process.stdout.isTTY && !has(argv, "--no-color") });
|
|
8913
|
+
const g = build({ task: flag(argv, "--task") });
|
|
8914
|
+
paint(g);
|
|
8915
|
+
return g.cycles.length ? 3 : 0;
|
|
8916
|
+
} catch (e) {
|
|
8917
|
+
err(e?.message || String(e));
|
|
8918
|
+
return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8561
8921
|
try {
|
|
8562
8922
|
const g = build({ task: flag(argv, "--task") });
|
|
8563
8923
|
if (asJson)
|
|
@@ -8644,7 +9004,7 @@ function spawnDetachedDelivery(args) {
|
|
|
8644
9004
|
const log = join32(dir, `deliver-${id}.log`);
|
|
8645
9005
|
const fd = openSync3(log, "a");
|
|
8646
9006
|
const bin = process.argv[1];
|
|
8647
|
-
const child =
|
|
9007
|
+
const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
|
|
8648
9008
|
detached: true,
|
|
8649
9009
|
stdio: ["ignore", fd, fd],
|
|
8650
9010
|
env: process.env
|
|
@@ -8934,6 +9294,13 @@ async function collabCli(argv) {
|
|
|
8934
9294
|
i++;
|
|
8935
9295
|
}
|
|
8936
9296
|
}
|
|
9297
|
+
const touchPaths = [...flags(args, "--touch"), ...(flag(args, "--touches") || "").split(",")].map((x) => x.trim()).filter(Boolean);
|
|
9298
|
+
if (touchPaths.length)
|
|
9299
|
+
refs.touches = touchPaths.join(",");
|
|
9300
|
+
const refPaths = (m) => [
|
|
9301
|
+
...(m.refs?.touches || "").split(","),
|
|
9302
|
+
...m.refs?.path ? [m.refs.path] : []
|
|
9303
|
+
].map((x) => x.trim()).filter(Boolean);
|
|
8937
9304
|
const view = (m) => ({ ...m, body: bodyOf(m) });
|
|
8938
9305
|
const print = (m) => {
|
|
8939
9306
|
out(`${m.id} [${m.state}] ${m.subject}`);
|
|
@@ -8942,6 +9309,12 @@ async function collabCli(argv) {
|
|
|
8942
9309
|
out(` kind: ${m.kind}${m.requires_reply ? " 需回复" : ""}${m.deadline ? ` deadline=${m.deadline}` : ""} thread=${m.thread_id}#${m.seq}`);
|
|
8943
9310
|
if (m.refs && Object.keys(m.refs).length)
|
|
8944
9311
|
out(` refs: ${Object.entries(m.refs).map(([k, v]) => `${k}=${v}`).join(" ")}`);
|
|
9312
|
+
const touches = refPaths(m);
|
|
9313
|
+
if (touches.length) {
|
|
9314
|
+
const held = list3({}).filter((c) => touches.includes(c.path) && c.holder !== m.to.session_id);
|
|
9315
|
+
for (const c of held)
|
|
9316
|
+
out(` ⚠ claim: ${c.path} 由 ${c.holder} 持有(${c.reason || "no reason"})`);
|
|
9317
|
+
}
|
|
8945
9318
|
const body = bodyOf(m);
|
|
8946
9319
|
if (body) {
|
|
8947
9320
|
const max = 4000;
|
|
@@ -9044,7 +9417,14 @@ async function collabCli(argv) {
|
|
|
9044
9417
|
idempotencyKey: flag(args, "--idempotency-key"),
|
|
9045
9418
|
hops: flag(args, "--hops") ? Number(flag(args, "--hops")) : undefined
|
|
9046
9419
|
});
|
|
9047
|
-
|
|
9420
|
+
const overlaps = list3({}).filter((c) => touchPaths.includes(c.path) && c.holder !== m.from.session_id);
|
|
9421
|
+
if (asJson)
|
|
9422
|
+
out(JSON.stringify(overlaps.length ? { ...m, claim_overlaps: overlaps } : m, null, 1));
|
|
9423
|
+
else {
|
|
9424
|
+
out(`${m.id} ${m.thread_id}#${m.seq} ${m.from.session_id} → ${m.to.session_id} ${m.kind} ${m.subject}`);
|
|
9425
|
+
for (const c of overlaps)
|
|
9426
|
+
out(` ⚠ claim overlap: ${c.path} 由 ${c.holder} 持有(${c.reason || "no reason"})`);
|
|
9427
|
+
}
|
|
9048
9428
|
return 0;
|
|
9049
9429
|
}
|
|
9050
9430
|
if (sub === "ack") {
|
|
@@ -9638,6 +10018,18 @@ async function serviceCli(argv) {
|
|
|
9638
10018
|
}
|
|
9639
10019
|
const fromWrapper = has(args, "--from-wrapper");
|
|
9640
10020
|
const ex = flag(args, "--exit");
|
|
10021
|
+
const dir = serviceDir(id);
|
|
10022
|
+
const svc = readServiceManifest(dir);
|
|
10023
|
+
if (healthcheckKind(svc.healthcheck) === "acp" && !fromWrapper) {
|
|
10024
|
+
const run = await runAcpHealth(svc, { dir });
|
|
10025
|
+
const rec = recordHealth(svc, run.judgement, { gen: flag(args, "--gen"), stateFile: flag(args, "--state-file") });
|
|
10026
|
+
const code = run.judgement.state === "ok" ? 0 : run.judgement.state === "degraded" ? 30 : 1;
|
|
10027
|
+
if (asJson)
|
|
10028
|
+
out(JSON.stringify({ id, verdict: run.judgement.state, shown: rec.record.state, exit: run.judgement.exit, detail: run.judgement.detail, steps: run.steps }, null, 1));
|
|
10029
|
+
else
|
|
10030
|
+
out(`${id}: ${run.judgement.state}(shown=${rec.record.state}${run.judgement.detail ? ", " + run.judgement.detail : ""})`);
|
|
10031
|
+
return code;
|
|
10032
|
+
}
|
|
9641
10033
|
const r = runServiceHealth(id, {
|
|
9642
10034
|
wrapperExit: fromWrapper ? Number(ex ?? "1") : undefined,
|
|
9643
10035
|
gen: flag(args, "--gen"),
|
|
@@ -10086,8 +10478,19 @@ async function main(rawArgv) {
|
|
|
10086
10478
|
return agentsCli(argv.slice(1));
|
|
10087
10479
|
if (cmd === "team")
|
|
10088
10480
|
return teamCli(argv.slice(1));
|
|
10089
|
-
if (cmd === "job")
|
|
10090
|
-
|
|
10481
|
+
if (cmd === "job") {
|
|
10482
|
+
const rest = argv.slice(1);
|
|
10483
|
+
if (rest[0] === "status" || rest[0] === "list") {
|
|
10484
|
+
const r = runJobCapture(rest);
|
|
10485
|
+
if (r.stderr)
|
|
10486
|
+
process.stderr.write(r.stderr);
|
|
10487
|
+
const text = decorateStatus(r.stdout, (id) => readServiceHealth(id));
|
|
10488
|
+
if (text)
|
|
10489
|
+
out(text.replace(/\n$/, ""));
|
|
10490
|
+
return r.status;
|
|
10491
|
+
}
|
|
10492
|
+
return runJob(rest);
|
|
10493
|
+
}
|
|
10091
10494
|
if (cmd === "price") {
|
|
10092
10495
|
const table = loadPrices();
|
|
10093
10496
|
if (has(argv.slice(1), "--json"))
|
package/docs/index.md
CHANGED
|
@@ -31,7 +31,7 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
|
|
|
31
31
|
| [价目表(tokens → 金额)](pricing.md) | 原则:没有价格就不编造;unpriced 明确记录,预算按 tokens 兜底。 |
|
|
32
32
|
| [目标提案(team next)](proposals.md) | 定位:团队可以提候选,不能定目标。 |
|
|
33
33
|
| [Sandbox 细化设计(dia)](security-sandbox.md) | 依据:dsh 本机源码实测、Hermes/Pi 取证、[/tmp/dia-sbx 实验]。 |
|
|
34
|
-
| [服务(service):可托管的执行体](service-noun.md) |
|
|
34
|
+
| [服务(service):可托管的执行体](service-noun.md) | 状态:已实现(v0.0.10,脚本型 + agent 型)。脚本型走 entry.cmd,agent 型走 entry.agent + healthcheck.kind="acp"; |
|
|
35
35
|
| [dia 状态页(单一事实源)](status.md) | 任何"现在做到哪了"的问题,以本页为准;其他文档是细节。 |
|
|
36
36
|
| [全自动团队:人只在验收介入](team-automation.md) | 目标:计划→实现→验证→对抗→修复全自动循环;人只在验收阶段拍板。 |
|
|
37
37
|
| [团队章程:谁来做 dia(开发团队)](team.md) | 2026-10-28 升级:开发团队执行模式改为全自动循环(计划→实现→验证→对抗→修复),人只在验收阶段介入; |
|