oasis_test 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/dist/index.js +460 -151
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -955,13 +955,6 @@ var init_chat = __esm({
955
955
  }
956
956
  });
957
957
 
958
- // ../contract/src/chat-session.ts
959
- var init_chat_session = __esm({
960
- "../contract/src/chat-session.ts"() {
961
- "use strict";
962
- }
963
- });
964
-
965
958
  // ../contract/src/http-api.ts
966
959
  var init_http_api = __esm({
967
960
  "../contract/src/http-api.ts"() {
@@ -995,7 +988,6 @@ var init_src = __esm({
995
988
  init_telemetry();
996
989
  init_trace();
997
990
  init_chat();
998
- init_chat_session();
999
991
  init_http_api();
1000
992
  }
1001
993
  });
@@ -1785,6 +1777,11 @@ var init_kernel = __esm({
1785
1777
  async seal(args) {
1786
1778
  this.ensureActive(args.artifactId);
1787
1779
  this.artifactOrThrow(args.artifactId);
1780
+ if (args.reason === "cancelled") {
1781
+ for (const edge of this.edgesTouching(args.artifactId)) {
1782
+ await this.unlinkInput({ artifactId: edge.artifactId, to: edge.to, actor: args.actor });
1783
+ }
1784
+ }
1788
1785
  const payload = {
1789
1786
  reason: args.reason,
1790
1787
  ...args.note !== void 0 ? { note: args.note } : {}
@@ -1878,6 +1875,14 @@ var init_kernel = __esm({
1878
1875
  for (const a of fork.artifacts.values()) {
1879
1876
  if (lifecycleOf(fork, a.id) === "active" && !a.owner) violations.push(`active \u65E0 owner: ${a.id}`);
1880
1877
  }
1878
+ for (const a of fork.artifacts.values()) {
1879
+ if (lifecycleOf(fork, a.id) !== "active") continue;
1880
+ for (const e of a.inputs) {
1881
+ if (lifecycleOf(fork, e.to) === "sealed:cancelled") {
1882
+ violations.push(`\u4F9D\u8D56\u5E9F\u5F03\u8282\u70B9: ${a.id} \u2192 ${e.to}\uFF08cancelled \u4E0D\u53EF\u88AB\u4F9D\u8D56\uFF1B\u6539\u6295\u6216\u64A4\u8FB9\uFF09`);
1883
+ }
1884
+ }
1885
+ }
1881
1886
  for (const id of touched) {
1882
1887
  if (fork.artifacts.get(id)?.createdVia === "prereq" && this.prereqChainDepth(fork, id) > MAX_PREREQ_DEPTH) {
1883
1888
  violations.push(`\u524D\u7F6E\u94FE\u8D85\u4E0A\u9650 ${MAX_PREREQ_DEPTH}\uFF1A${id}\uFF08\u7591\u4F3C\u5931\u63A7\u9012\u5F52\uFF0C\u505C\u5728\u6B64\u3001\u5347\u4EBA\uFF09`);
@@ -1900,10 +1905,14 @@ var init_kernel = __esm({
1900
1905
  if (edge?.required) advisory.push(`\u65AD\u5F00 required \u8FB9 ${op.artifactId} \u2192 ${op.to}\uFF08\u627F\u91CD\u53EF\u80FD\uFF1A\u6D88\u8D39\u65B9\u518D\u6D3E\u751F\u5C06\u5931\u53BB\u6B64\u8F93\u5165\uFF09`);
1901
1906
  }
1902
1907
  if (op.action === "seal" && op.reason === "cancelled") {
1903
- const n = consumersOf(this.model, op.artifactId).length;
1904
- if (n > 0) advisory.push(`seal(cancelled) ${op.artifactId}\uFF1A${n} \u4E2A\u6D88\u8D39\u8005\u5C06\u5E26"\u5E9F\u5F03\u5730\u57FA"\u6807\u8BB0\uFF08\u5B50\u6811\u6E05\u7406\u662F\u4EBA\u7684\u5224\u65AD\uFF0C\xA711.6\uFF09`);
1908
+ const edges = this.edgesTouching(op.artifactId).length;
1909
+ const consumers = consumersOf(this.model, op.artifactId).length;
1910
+ if (edges > 0)
1911
+ advisory.push(
1912
+ `cancel ${op.artifactId}\uFF1A\u81EA\u52A8\u65AD\u5F00 ${edges} \u6761\u76F8\u5173\u8FB9\uFF08\u53CC\u5411\uFF09` + (consumers > 0 ? `\uFF1B${consumers} \u4E2A\u6D88\u8D39\u8005\u5C06\u88AB\u91CD\u5524\u53BB\u6389\u5BF9\u5B83\u7684\u9002\u914D\u2014\u2014\u6309\u9700\u6539\u6295(link\u2192\u66FF\u4EE3)/\u7EA7\u8054 cancel` : "")
1913
+ );
1905
1914
  }
1906
- if (op.action === "seal" && !isConcluded(this.model, op.artifactId)) {
1915
+ if (op.action === "seal" && op.reason !== "cancelled" && !isConcluded(this.model, op.artifactId)) {
1907
1916
  const strandedConsumers = consumersOf(this.model, op.artifactId).filter(
1908
1917
  (c) => c.inputs.some((e) => e.to === op.artifactId && e.required)
1909
1918
  );
@@ -1930,37 +1939,6 @@ var init_kernel = __esm({
1930
1939
  advisory.push(`\u8282\u70B9\u4FE1\u606F\u4E3A\u7A7A\uFF1A${id} \u6CA1\u6709 title/description\u2014\u2014\u5EFA\u4E86\u4E2A\u6CA1\u8BF4\u660E\u7684\u8282\u70B9\uFF1F\uFF08\u4ECD\u53EF\u63D0\u4EA4\uFF0C\u8BF7\u786E\u8BA4\uFF09`);
1931
1940
  }
1932
1941
  }
1933
- const touchedWorkspaces = new Set(
1934
- touched.map((id) => fork.artifacts.get(id)?.workspace).filter((w) => !!w)
1935
- );
1936
- for (const ws of touchedWorkspaces) {
1937
- const members = [...fork.artifacts.values()].filter((a) => a.workspace === ws);
1938
- if (members.length < 2) continue;
1939
- const root = members.find((a) => a.type === "brief") ?? members.find((a) => a.inputs.length === 0);
1940
- if (!root) continue;
1941
- const seen = /* @__PURE__ */ new Set([root.id]);
1942
- const stack = [root.id];
1943
- while (stack.length > 0) {
1944
- const cur = stack.pop();
1945
- const neighbors = [
1946
- ...fork.artifacts.get(cur)?.inputs.map((e) => e.to) ?? [],
1947
- // 上游
1948
- ...consumersOf(fork, cur).map((c) => c.id)
1949
- // 下游
1950
- ];
1951
- for (const n of neighbors) {
1952
- if (fork.artifacts.has(n) && !seen.has(n)) {
1953
- seen.add(n);
1954
- stack.push(n);
1955
- }
1956
- }
1957
- }
1958
- for (const a of members) {
1959
- if (lifecycleOf(fork, a.id) === "active" && !seen.has(a.id)) {
1960
- advisory.push(`\u5B64\u5C9B\u8282\u70B9\uFF1A${a.id} \u4E0E\u6839 brief \u4E0D\u8FDE\u901A\u2014\u2014\u6F0F\u8FDE\u4F9D\u8D56\u8FB9\uFF1F\uFF08\u4ECD\u53EF\u63D0\u4EA4\uFF0C\u8BF7\u786E\u8BA4\u4E0D\u662F\u8BEF\u5EFA\uFF09`);
1961
- }
1962
- }
1963
- }
1964
1942
  const closure = /* @__PURE__ */ new Set();
1965
1943
  const expand = (id) => {
1966
1944
  for (const c of consumersOf(this.model, id)) {
@@ -2032,8 +2010,42 @@ var init_kernel = __esm({
2032
2010
  }
2033
2011
  return { interventionId, applied: constituents.length };
2034
2012
  }
2035
- /** InterventionOp 词汇编译成按依赖序排列的 oplog 构件(spawn 先、seal 最后) */
2013
+ /** D2:cancel 一个节点要断的所有相关边(双向)——消费者→它(C.inputs∋id)、它→上游(id.inputs∋U)。 */
2014
+ edgesTouching(id) {
2015
+ const out = [];
2016
+ for (const c of consumersOf(this.model, id)) out.push({ artifactId: c.id, to: id });
2017
+ for (const e of this.model.artifacts.get(id)?.inputs ?? []) out.push({ artifactId: id, to: e.to });
2018
+ return out;
2019
+ }
2020
+ /** D2:cancel(id) 会级联断掉的相关边(双向)——供 stage 回显/preview 列明"断了哪些边"。 */
2021
+ cancelCascadeEdges(id) {
2022
+ return this.edgesTouching(id);
2023
+ }
2024
+ /**
2025
+ * 编译 InterventionOp → oplog 构件。D2(决策 0017):seal(cancelled) 级联展开——在该 seal **之前**
2026
+ * 插入"断掉与本节点相关的所有边(双向)"的 unlink(趁本节点还 active:X→U 的 unlink 需 X active),再 seal;
2027
+ * 去重 plan 里已显式 unlink 的边。accepted/frozen 不级联(输出保留、消费者继续依赖)。
2028
+ */
2036
2029
  buildConstituents(plan) {
2030
+ const already = new Set(
2031
+ plan.ops.filter((o) => o.action === "unlink").map((o) => `${o.artifactId} ${o.to}`)
2032
+ );
2033
+ const out = [];
2034
+ for (const c of this.buildConstituentsRaw(plan)) {
2035
+ if (c.kind === "seal" && c.payload.reason === "cancelled") {
2036
+ for (const edge of this.edgesTouching(c.artifactId)) {
2037
+ const key = `${edge.artifactId} ${edge.to}`;
2038
+ if (already.has(key)) continue;
2039
+ already.add(key);
2040
+ out.push({ artifactId: edge.artifactId, kind: "unlink_input", target: edge.artifactId, payload: { to: edge.to } });
2041
+ }
2042
+ }
2043
+ out.push(c);
2044
+ }
2045
+ return out;
2046
+ }
2047
+ /** 把 InterventionOp 词汇编译成按依赖序排列的 oplog 构件(spawn 先、seal 最后) */
2048
+ buildConstituentsRaw(plan) {
2037
2049
  const order = { spawn: 0, edit: 1, reopen: 2, assign: 3, link: 4, pin: 5, unlink: 6, cancelPart: 7, seal: 8, annotate: 9 };
2038
2050
  const sorted = [...plan.ops].sort((a, b) => order[a.action] - order[b.action]);
2039
2051
  const spawnedHere = new Set(plan.ops.flatMap((op) => op.action === "spawn" ? [op.id] : []));
@@ -2373,6 +2385,18 @@ async function assembleContext(args) {
2373
2385
  const headRev = head !== null ? model.revisions.get(head) : null;
2374
2386
  const headSpread = headRev !== null ? await spreadRevisionContent(blobs, headRev, files, "deliverable") : null;
2375
2387
  if (headSpread?.inlineText != null) files["deliverable/CONTENT.md"] = headSpread.inlineText;
2388
+ let orphanBriefNote = null;
2389
+ if (artifact.inputs.length === 0 && artifact.type !== "brief") {
2390
+ const brief = [...model.artifacts.values()].find(
2391
+ (a) => a.workspace === artifact.workspace && a.type === "brief" && a.id !== artifactId
2392
+ );
2393
+ const briefHead = brief?.currentRev ? model.revisions.get(brief.currentRev) ?? null : null;
2394
+ if (brief && briefHead) {
2395
+ const spread = await spreadRevisionContent(blobs, briefHead, files, "inputs/work-order");
2396
+ if (spread.inlineText != null) files["inputs/work-order/CONTENT.md"] = spread.inlineText;
2397
+ orphanBriefNote = `\u4F60\u6CA1\u6709\u4E0A\u6E38\u4F9D\u8D56\u2014\u2014\u4EE5\u5DE5\u5355\u6839 brief \`${brief.id}\` \u4E3A\u51C6\uFF08\u5DF2\u653E\u8FDB \`inputs/work-order/\`\uFF09\u3002`;
2398
+ }
2399
+ }
2376
2400
  const integrating = args.action === "integrate";
2377
2401
  const queuedParts = [];
2378
2402
  if (integrating) {
@@ -2410,11 +2434,16 @@ async function assembleContext(args) {
2410
2434
  whatLines.push(`\u628A\u4F60\u6D3E\u51FA\u53BB\u7684\u5404\u90E8\u4EF6\u6536\u53E3\u6210**\u4E00\u7248\u5B8C\u6574\u3001\u81EA\u6D3D\u7684 R\\***\u2014\u2014\u5404\u90E8\u4EF6\u5728\u54EA\u3001\u4EE5\u4EC0\u4E48\u4E3A\u5E95\u600E\u4E48\u5408\uFF0C\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF08\u968F\u5185\u5BB9\u7C7B\u578B\u4E0D\u540C\uFF09\u3002`);
2411
2435
  } else {
2412
2436
  if (unfoundedHere.length > 0) {
2413
- whyLines.push(`**\u8FD4\u5DE5\uFF1A\u4E0A\u6E38\u66F4\u65B0\u4E86**\u2014\u2014\u4F60\u5F53\u524D head \u57FA\u4E8E\u7684\u662F\u65E7\u7248\uFF0C\u8FD9\u4E9B\u4E0A\u6E38\u53D8\u4E86\uFF08diff \u5982\u4E0B\uFF09\uFF1A`);
2437
+ whyLines.push(`**\u8FD4\u5DE5\uFF1A\u4F60\u4F9D\u8D56\u7684\u4E0A\u6E38\u53D8\u4E86**\u2014\u2014\u4F60\u5F53\u524D head \u57FA\u4E8E\u65E7\u60C5\u51B5\uFF1B\u4E0B\u5217\u4E0A\u6E38\u6709\u53D8\uFF08\u66F4\u65B0\uFF0F\u88AB\u65AD\u6216\u5E9F\u5F03\uFF09\uFF0C\u636E\u6B64\u4FEE\u8BA2\u4F60\u7684\u4EA7\u7269\uFF1A`);
2414
2438
  for (const u of unfoundedHere) {
2415
2439
  const oldC = await contentMapOf(u.basedOn ? model.revisions.get(u.basedOn) ?? null : null);
2416
2440
  const newC = u.pinned ? await contentMapOf(model.revisions.get(u.pinned) ?? null) : /* @__PURE__ */ new Map();
2417
- whyLines.push(`- \u4E0A\u6E38 ${u.upstream}\uFF08${(u.basedOn ?? "\u2205").slice(9, 17)} \u2192 ${(u.pinned ?? "\u2205").slice(9, 17)}\uFF09\uFF1A`);
2441
+ if (u.pinned === null) {
2442
+ const status = lifecycleOf(model, u.upstream) === "sealed:cancelled" ? "\u5DF2\u5E9F\u5F03(cancelled)" : "\u5DF2\u65AD\u5F00(unlink)";
2443
+ whyLines.push(`- \u4E0A\u6E38 ${u.upstream} **${status}**\uFF08\u4F60\u66FE\u57FA\u4E8E\u5B83\u7684 ${(u.basedOn ?? "\u2205").slice(9, 17)} \u7248\uFF09\uFF1A**\u53BB\u6389\u4F60\u4EA7\u7269\u91CC\u4F9D\u8D56\u5B83\u7684\u90E8\u5206**\uFF08\u5B83\u7684\u65E7\u5185\u5BB9\u89C1\u4E0B\uFF09\uFF1A`);
2444
+ } else {
2445
+ whyLines.push(`- \u4E0A\u6E38 ${u.upstream}\uFF08${(u.basedOn ?? "\u2205").slice(9, 17)} \u2192 ${(u.pinned ?? "\u2205").slice(9, 17)}\uFF09\uFF1A`);
2446
+ }
2418
2447
  whyLines.push(oldC !== null && newC !== null ? manifestDiff(oldC, newC) : `\uFF08\u4EE3\u7801-git\uFF1A\u7528 \`git log -p\` \u81EA\u67E5\uFF1B\u65B0\u7248\u4E5F\u5728 inputs/ \u91CC\uFF09`);
2419
2448
  }
2420
2449
  whatLines.push(`\u636E\u4E0A\u6E38\u65B0\u7248**\u66F4\u65B0\u53D7\u5F71\u54CD\u7684\u90E8\u5206**\u3001\u91CD\u65B0\u63D0\u4EA4\uFF1B\u4E0D\u53D7\u5F71\u54CD\u7684\u522B\u52A8\uFF08\u600E\u4E48\u843D\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002`);
@@ -2500,6 +2529,7 @@ async function assembleContext(args) {
2500
2529
  `- type: ${artifact.type}${typeDef ? `\uFF08contentType=${typeDef.contentType}\uFF09` : ""}`,
2501
2530
  `- \u4F60\u662F **${typeDef?.ownerRole ?? "owner"}**\uFF08\u4F60\u7684\u4E13\u957F/\u98CE\u683C\u89C1 \`identity/\`\uFF09`,
2502
2531
  `- \u5F53\u524D head: ${head ?? "\u2205\uFF08\u80DA\u80CE\u671F\u2014\u2014\u8BF7\u4EA7\u51FA\u9996\u4E2A revision\uFF09"}`,
2532
+ ...orphanBriefNote ? [`- \u2693 ${orphanBriefNote}`] : [],
2503
2533
  ...whyLines.length > 0 ? [``, `## \u672C\u6B21\u4E3A\u4EC0\u4E48\u5524\u8D77\u4F60`, ...whyLines] : [],
2504
2534
  ...whatLines.length > 0 ? [``, `## \u9700\u8981\u4F60\u505A\u4EC0\u4E48`, ...whatLines] : [],
2505
2535
  ``,
@@ -2784,6 +2814,7 @@ var init_dispatcher = __esm({
2784
2814
  async tick() {
2785
2815
  const { kernel } = this.opts;
2786
2816
  let started = 0;
2817
+ await this.autoConcludeReadyMergedArtifacts();
2787
2818
  for (const job of computeProduceJobs(kernel.model)) {
2788
2819
  if (kernel.isFrozen(job.artifactId)) continue;
2789
2820
  const artifact = kernel.model.artifacts.get(job.artifactId);
@@ -2923,74 +2954,134 @@ var init_dispatcher = __esm({
2923
2954
  wallClockMs: job.limits.wallClockMs
2924
2955
  });
2925
2956
  session.onExit((info) => {
2926
- this.inFlight.delete(jobKey);
2927
- const seqAtExit = kernel.model.lastSeq.get(spec.artifactId) ?? 0;
2928
- if (sink) {
2929
- void (async () => {
2930
- const ops = await this.opts.oplog.read(spec.artifactId);
2931
- const opRefs = ops.filter((o) => o.seq > seqAtDispatch && o.seq <= seqAtExit).map((o) => ({ seq: o.seq, kind: o.kind, id: o.id, target: o.target }));
2932
- sink.end(session.id, {
2933
- exit: {
2934
- code: info.code,
2935
- at: (/* @__PURE__ */ new Date()).toISOString(),
2936
- durationMs: Date.now() - dispatchedAtMs,
2937
- wallclockKilled: info.code === null,
2938
- // 适配器从 stream-json 抽出的 token/成本(含 cache);不带则缺省。
2939
- ...info.usage ? { usage: info.usage } : {}
2940
- },
2941
- opRefs
2942
- });
2943
- })().catch(() => {
2957
+ void this.handleSessionExit({
2958
+ jobKey,
2959
+ spec,
2960
+ session,
2961
+ info,
2962
+ sink,
2963
+ seqAtDispatch,
2964
+ dispatchedAtMs
2965
+ });
2966
+ });
2967
+ return 1;
2968
+ }
2969
+ async handleSessionExit(args) {
2970
+ const { kernel } = this.opts;
2971
+ const { jobKey, spec, session, info, sink, seqAtDispatch, dispatchedAtMs } = args;
2972
+ const seqBeforeAutoConclude = kernel.model.lastSeq.get(spec.artifactId) ?? 0;
2973
+ if (info.code === 0 && seqBeforeAutoConclude !== seqAtDispatch) {
2974
+ await this.maybeAutoConclude(spec);
2975
+ }
2976
+ this.inFlight.delete(jobKey);
2977
+ const seqAtExit = kernel.model.lastSeq.get(spec.artifactId) ?? 0;
2978
+ if (sink) {
2979
+ void (async () => {
2980
+ const ops = await this.opts.oplog.read(spec.artifactId);
2981
+ const opRefs = ops.filter((o) => o.seq > seqAtDispatch && o.seq <= seqAtExit).map((o) => ({ seq: o.seq, kind: o.kind, id: o.id, target: o.target }));
2982
+ sink.end(session.id, {
2983
+ exit: {
2984
+ code: info.code,
2985
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2986
+ durationMs: Date.now() - dispatchedAtMs,
2987
+ wallclockKilled: info.code === null,
2988
+ // 适配器从 stream-json 抽出的 token/成本(含 cache);不带则缺省。
2989
+ ...info.usage ? { usage: info.usage } : {}
2990
+ },
2991
+ opRefs
2944
2992
  });
2945
- }
2946
- const progressed = seqAtExit !== seqAtDispatch;
2947
- let consecutive = 0;
2948
- if (progressed) {
2949
- this.strikes.delete(jobKey);
2950
- } else {
2951
- consecutive = (this.strikes.get(jobKey)?.count ?? 0) + 1;
2952
- const max = this.opts.backoff?.maxConsecutiveFailures ?? 3;
2953
- const base = this.opts.backoff?.baseMs ?? 3e4;
2954
- const fused = consecutive >= max;
2955
- this.strikes.set(jobKey, {
2993
+ })().catch(() => {
2994
+ });
2995
+ }
2996
+ const progressed = seqAtExit !== seqAtDispatch;
2997
+ let consecutive = 0;
2998
+ if (progressed) {
2999
+ this.strikes.delete(jobKey);
3000
+ } else {
3001
+ consecutive = (this.strikes.get(jobKey)?.count ?? 0) + 1;
3002
+ const max = this.opts.backoff?.maxConsecutiveFailures ?? 3;
3003
+ const base = this.opts.backoff?.baseMs ?? 3e4;
3004
+ const fused = consecutive >= max;
3005
+ this.strikes.set(jobKey, {
3006
+ artifactId: spec.artifactId,
3007
+ actor: spec.actor,
3008
+ action: spec.action,
3009
+ count: consecutive,
3010
+ nextEligibleAt: Date.now() + base * 2 ** (consecutive - 1),
3011
+ fused,
3012
+ ...fused ? { fusedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
3013
+ seqAtVerdict: seqAtExit,
3014
+ ...info.reason ? { lastReason: info.reason } : {}
3015
+ });
3016
+ if (fused) {
3017
+ this.emitJournal({
3018
+ kind: "fused",
3019
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3020
+ jobKey,
2956
3021
  artifactId: spec.artifactId,
2957
3022
  actor: spec.actor,
2958
3023
  action: spec.action,
2959
- count: consecutive,
2960
- nextEligibleAt: Date.now() + base * 2 ** (consecutive - 1),
2961
- fused,
2962
- ...fused ? { fusedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
2963
- seqAtVerdict: seqAtExit,
2964
- ...info.reason ? { lastReason: info.reason } : {}
3024
+ consecutiveFailures: consecutive
2965
3025
  });
2966
- if (fused) {
2967
- this.emitJournal({
2968
- kind: "fused",
2969
- at: (/* @__PURE__ */ new Date()).toISOString(),
2970
- jobKey,
2971
- artifactId: spec.artifactId,
2972
- actor: spec.actor,
2973
- action: spec.action,
2974
- consecutiveFailures: consecutive
2975
- });
2976
- }
2977
3026
  }
2978
- this.emitJournal({
2979
- kind: "exited",
2980
- at: (/* @__PURE__ */ new Date()).toISOString(),
2981
- jobKey,
3027
+ }
3028
+ this.emitJournal({
3029
+ kind: "exited",
3030
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3031
+ jobKey,
3032
+ artifactId: spec.artifactId,
3033
+ actor: spec.actor,
3034
+ action: spec.action,
3035
+ code: info.code,
3036
+ ...info.reason ? { reason: info.reason } : {},
3037
+ // BUG-5:结构化退出归因
3038
+ durationMs: Date.now() - dispatchedAtMs,
3039
+ progressed,
3040
+ consecutiveFailures: consecutive
3041
+ });
3042
+ }
3043
+ async maybeAutoConclude(spec) {
3044
+ if (!this.shouldAutoConclude(spec)) return;
3045
+ try {
3046
+ await this.opts.kernel.conclude({
2982
3047
  artifactId: spec.artifactId,
2983
3048
  actor: spec.actor,
2984
- action: spec.action,
2985
- code: info.code,
2986
- ...info.reason ? { reason: info.reason } : {},
2987
- // BUG-5:结构化退出归因
2988
- durationMs: Date.now() - dispatchedAtMs,
2989
- progressed,
2990
- consecutiveFailures: consecutive
3049
+ note: "auto-conclude after successful dispatch"
2991
3050
  });
2992
- });
2993
- return 1;
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ async autoConcludeReadyMergedArtifacts() {
3055
+ for (const artifact of this.opts.kernel.model.artifacts.values()) {
3056
+ if (!artifact.owner.startsWith("actor:agent:")) continue;
3057
+ await this.maybeAutoConclude({
3058
+ artifactId: artifact.id,
3059
+ actor: artifact.owner,
3060
+ action: (artifact.parts ?? []).length > 0 ? "integrate" : "produce"
3061
+ });
3062
+ }
3063
+ }
3064
+ shouldAutoConclude(spec) {
3065
+ if (spec.action !== "produce" && spec.action !== "integrate") return false;
3066
+ if (spec.part !== void 0) return false;
3067
+ const model = this.opts.kernel.model;
3068
+ const artifact = model.artifacts.get(spec.artifactId);
3069
+ if (!artifact) return false;
3070
+ if (!artifact.owner.startsWith("actor:agent:")) return false;
3071
+ if (lifecycleOf(model, spec.artifactId) !== "active") return false;
3072
+ if (isConcluded(model, spec.artifactId)) return false;
3073
+ if (pendingConcludeAttempt(model, spec.artifactId)) return false;
3074
+ if (!artifact.currentRev) return false;
3075
+ if (queueOf(model, spec.artifactId).length > 0) return false;
3076
+ if (unresolvedGapsOf(model, spec.artifactId).length > 0) return false;
3077
+ if (annotationsOf(model, spec.artifactId).some((a) => a.state === "open" && a.raisedFrom === spec.artifactId)) return false;
3078
+ if (hasOpenHumanChangeRequest(model, spec.artifactId)) return false;
3079
+ const parts = artifact.parts ?? [];
3080
+ if (parts.length === 0) return true;
3081
+ if (spec.action !== "integrate") return false;
3082
+ return parts.every(
3083
+ (p) => revisionsOf(model, spec.artifactId).some((r) => r.part === p.name && r.state !== "rejected")
3084
+ );
2994
3085
  }
2995
3086
  /** per-type 预算优先(schema.budget),其次 Dispatcher 配置,最后 10min 兜底 */
2996
3087
  wallClockFor(artifactId) {
@@ -16475,6 +16566,29 @@ async function closeHumanChangeRequestForRevision(kernel, oplog, artifactId, rev
16475
16566
  await kernel.resolveAnnotation({ annotationId, actor, resolution, note });
16476
16567
  }
16477
16568
  }
16569
+ async function mergeApprovedRevision(kernel, args) {
16570
+ if (args.revisionId === void 0) {
16571
+ const artifact2 = await kernel.merge({ artifactId: args.artifactId, actor: args.actor });
16572
+ return { artifact: artifact2, mergedRevisionId: artifact2.currentRev, approvedRevisionId: artifact2.currentRev };
16573
+ }
16574
+ const queue = queueOf(kernel.model, args.artifactId);
16575
+ const target = kernel.model.revisions.get(args.revisionId);
16576
+ if (queue.length === 1 && queue[0]?.id === args.revisionId && target?.state === "proposed" && isStale(kernel.model, target)) {
16577
+ const successor = await kernel.proposeRevision({
16578
+ artifactId: args.artifactId,
16579
+ actor: args.actor,
16580
+ contentRef: target.contentRef,
16581
+ contentKind: target.contentKind,
16582
+ reason: `\u6279\u51C6\u8FC7\u671F\u4FEE\u8BA2\u540E\u91CD\u653E\uFF1A${target.reason}`,
16583
+ ...target.part !== void 0 ? { part: target.part } : {},
16584
+ rebasedFrom: target.id
16585
+ });
16586
+ const artifact2 = await kernel.merge({ artifactId: args.artifactId, actor: args.actor, revisionId: successor.id });
16587
+ return { artifact: artifact2, mergedRevisionId: successor.id, approvedRevisionId: target.id, rebasedFrom: target.id };
16588
+ }
16589
+ const artifact = await kernel.merge({ artifactId: args.artifactId, actor: args.actor, revisionId: args.revisionId });
16590
+ return { artifact, mergedRevisionId: args.revisionId, approvedRevisionId: args.revisionId };
16591
+ }
16478
16592
  function splitIdentityFiles2(prompt) {
16479
16593
  const SEP = /<!-- FILE: (.+?) -->\n?/g;
16480
16594
  const parts = prompt.split(SEP);
@@ -16690,22 +16804,24 @@ async function runCommand(kernel, blobs, oplog, actor, command, args, ctx = {})
16690
16804
  case "merge": {
16691
16805
  const revisionId = optStr(args, "revisionId");
16692
16806
  const artifactId = str(args, "artifactId");
16693
- const artifact = await kernel.merge({
16807
+ const merged = await mergeApprovedRevision(kernel, {
16694
16808
  artifactId,
16695
16809
  actor,
16696
16810
  ...revisionId !== void 0 ? { revisionId } : {}
16697
16811
  });
16698
- const mergedRevisionId = revisionId ?? artifact.currentRev;
16699
16812
  await closeHumanChangeRequestForRevision(
16700
16813
  kernel,
16701
16814
  oplog,
16702
16815
  artifactId,
16703
- mergedRevisionId,
16816
+ merged.approvedRevisionId,
16704
16817
  actor,
16705
16818
  "resolved",
16706
- `\u5BA1\u6838\u901A\u8FC7${revisionId ? `\uFF1A${revisionId}` : ""}\uFF0C\u53D8\u66F4\u8BF7\u6C42\u5DF2\u89E3\u51B3\u3002`
16819
+ merged.rebasedFrom ? `\u5BA1\u6838\u901A\u8FC7\uFF1A${merged.rebasedFrom}\uFF08\u5DF2\u91CD\u653E\u4E3A ${merged.mergedRevisionId}\uFF09\uFF0C\u53D8\u66F4\u8BF7\u6C42\u5DF2\u89E3\u51B3\u3002` : `\u5BA1\u6838\u901A\u8FC7${revisionId ? `\uFF1A${revisionId}` : ""}\uFF0C\u53D8\u66F4\u8BF7\u6C42\u5DF2\u89E3\u51B3\u3002`
16707
16820
  );
16708
- return { message: `merged\uFF1Ahead \u2192 ${artifact.currentRev}` };
16821
+ return {
16822
+ message: merged.rebasedFrom ? `merged(rebase)\uFF1Ahead \u2192 ${merged.artifact.currentRev}\uFF1Bsuperseded ${merged.rebasedFrom}` : `merged\uFF1Ahead \u2192 ${merged.artifact.currentRev}`,
16823
+ data: merged.rebasedFrom ? { rebasedFrom: merged.rebasedFrom, revisionId: merged.mergedRevisionId } : void 0
16824
+ };
16709
16825
  }
16710
16826
  case "advance": {
16711
16827
  const result = await kernel.advanceQueue(str(args, "artifactId"), actor);
@@ -17271,7 +17387,10 @@ async function startOasisServer(opts) {
17271
17387
  const buf = stagingBuffers.get(sessionKey) ?? [];
17272
17388
  buf.push(op);
17273
17389
  stagingBuffers.set(sessionKey, buf);
17274
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ count: buf.length, staged: op }));
17390
+ const cascade = op.action === "seal" && op.reason === "cancelled" ? engine.kernel.cancelCascadeEdges(op.artifactId) : void 0;
17391
+ res.writeHead(200, { "content-type": "application/json" }).end(
17392
+ JSON.stringify({ count: buf.length, staged: op, ...cascade && cascade.length > 0 ? { cascade } : {} })
17393
+ );
17275
17394
  } catch (err) {
17276
17395
  res.writeHead(400, { "content-type": "application/json" }).end(
17277
17396
  JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
@@ -17497,13 +17616,13 @@ async function startOasisServer(opts) {
17497
17616
  const actorCtx = await opts.resolveActorContext(body.actorId).catch(() => null);
17498
17617
  if (actorCtx) {
17499
17618
  const { mkdtempSync: mkdtempSync6, mkdirSync: mkdirSync13, writeFileSync: writeFileSync13 } = await import("node:fs");
17500
- const { join: join16, dirname: dirname12 } = await import("node:path");
17619
+ const { join: join16, dirname: dirname13 } = await import("node:path");
17501
17620
  const { tmpdir: tmpdir8 } = await import("node:os");
17502
17621
  const dir = mkdtempSync6(join16(tmpdir8(), "oasis-chat-"));
17503
17622
  if (actorCtx.config?.prompt) {
17504
17623
  for (const [rel, content] of Object.entries(splitIdentityFiles2(actorCtx.config.prompt))) {
17505
17624
  const file = join16(dir, rel);
17506
- mkdirSync13(dirname12(file), { recursive: true });
17625
+ mkdirSync13(dirname13(file), { recursive: true });
17507
17626
  writeFileSync13(file, content);
17508
17627
  }
17509
17628
  }
@@ -18595,11 +18714,11 @@ function deriveRevisionStatus(revision, currentRev) {
18595
18714
  return { status: "editing" };
18596
18715
  }
18597
18716
  }
18598
- function deriveArtifactStatus(currentRev, revisions) {
18599
- if (currentRev) return "current";
18717
+ function deriveArtifactStatus(model, artifactId, currentRev, revisions) {
18718
+ if (isConcluded(model, artifactId)) return "current";
18600
18719
  if (revisions.some((r) => r.state === "proposed")) return "pending_review";
18601
18720
  if (revisions.length > 0 && revisions.every((r) => r.state === "rejected")) return "rejected";
18602
- if (revisions.length > 0) return "editing";
18721
+ if (currentRev || revisions.length > 0) return "editing";
18603
18722
  return "pending";
18604
18723
  }
18605
18724
  function deriveActorKind(actor) {
@@ -18640,7 +18759,7 @@ function buildArtifactProjection(input) {
18640
18759
  projectId,
18641
18760
  type: artifact.docType ?? descriptor.docType ?? artifact.type,
18642
18761
  title: artifact.title ?? descriptor.title ?? artifactDerivation?.title ?? artifactId,
18643
- status: deriveArtifactStatus(currentRev, ordered),
18762
+ status: deriveArtifactStatus(model, artifactId, currentRev, ordered),
18644
18763
  ...currentRev ? { currentRevisionId: currentRev } : {},
18645
18764
  ...latest ? { latestRevisionId: latest.id } : {},
18646
18765
  ...artifactDerivation ? { createdFromWorkOrderId: artifactDerivation.workOrderId } : {},
@@ -18854,6 +18973,12 @@ var init_projection = __esm({
18854
18973
  });
18855
18974
 
18856
18975
  // ../server/src/domains/collab/node-state.ts
18976
+ function nodeLabel(a) {
18977
+ if (a.title) return a.title;
18978
+ if (a.description) return a.description;
18979
+ const tail = a.id.split(":").slice(2).join(":");
18980
+ return tail ? `\u672A\u547D\u540D ${a.type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${a.type}`;
18981
+ }
18857
18982
  function nodeInfo(model, a) {
18858
18983
  const lifecycle = lifecycleOf(model, a.id);
18859
18984
  const sealed = lifecycle !== "active";
@@ -18993,10 +19118,10 @@ function renderIndexYaml(index) {
18993
19118
  }
18994
19119
  function summarizeArtifacts(artifacts) {
18995
19120
  const total = artifacts.length;
18996
- const current = artifacts.filter((artifact) => artifact.status === "current").length;
18997
- const editing = artifacts.filter((artifact) => artifact.status === "editing").length;
18998
- const pendingReview = artifacts.filter((artifact) => artifact.status === "pending_review").length;
18999
- const rejected = artifacts.filter((artifact) => artifact.status === "rejected").length;
19121
+ const current = artifacts.filter((artifact) => (artifact.businessStatus ?? businessStatusFromLegacyStatus(artifact.status)) === "completed").length;
19122
+ const editing = artifacts.filter((artifact) => ["producing", "produced", "blocked"].includes(artifact.businessStatus ?? businessStatusFromLegacyStatus(artifact.status))).length;
19123
+ const pendingReview = artifacts.filter((artifact) => (artifact.businessStatus ?? businessStatusFromLegacyStatus(artifact.status)) === "review").length;
19124
+ const rejected = artifacts.filter((artifact) => (artifact.businessStatus ?? businessStatusFromLegacyStatus(artifact.status)) === "rejected").length;
19000
19125
  return {
19001
19126
  total,
19002
19127
  current,
@@ -19006,14 +19131,80 @@ function summarizeArtifacts(artifacts) {
19006
19131
  progressPercent: total === 0 ? 0 : Math.round(current / total * 100)
19007
19132
  };
19008
19133
  }
19009
- function artifactStatusFromNode(info) {
19010
- if (info.concluded) return "current";
19134
+ function businessStatusFromLegacyStatus(status) {
19135
+ switch (status) {
19136
+ case "current":
19137
+ case "history":
19138
+ return "completed";
19139
+ case "pending_review":
19140
+ return "review";
19141
+ case "rejected":
19142
+ return "rejected";
19143
+ case "archived":
19144
+ return "archived";
19145
+ case "producing":
19146
+ return "producing";
19147
+ case "editing":
19148
+ return "produced";
19149
+ case "pending":
19150
+ default:
19151
+ return "not_started";
19152
+ }
19153
+ }
19154
+ function artifactStatusFromNode(info, revisions) {
19011
19155
  const stage = deriveNodeStage(info);
19156
+ if (stage === "concluded") return "current";
19012
19157
  if (stage === "review" || stage === "gate") return "pending_review";
19013
19158
  if (stage === "sealed") return "archived";
19159
+ if (hasProposedRevision(revisions) && !info.concluded) return "pending_review";
19160
+ if (hasOnlyRejectedRevisions(revisions) && !info.concluded) return "rejected";
19014
19161
  if (stage === "editing" || stage === "merged" || stage === "blocked") return "editing";
19015
19162
  return "producing";
19016
19163
  }
19164
+ function artifactOutputStatusFromNode(info, revisions) {
19165
+ const stage = deriveNodeStage(info);
19166
+ if (hasProposedRevision(revisions) || hasOnlyRejectedRevisions(revisions)) return "produced";
19167
+ if (stage === "editing") return "producing";
19168
+ if (stage === "merged" || stage === "review" || stage === "gate" || stage === "concluded") return "produced";
19169
+ if (stage === "blocked" && info.hasWorking) return "producing";
19170
+ if (stage === "blocked" && info.queueLen > 0) return "produced";
19171
+ if (stage === "blocked" && info.concluded) return "produced";
19172
+ if (stage === "sealed" && info.concluded) return "produced";
19173
+ return "not_started";
19174
+ }
19175
+ function artifactAcceptanceStatusFromNode(info, revisions) {
19176
+ const stage = deriveNodeStage(info);
19177
+ if (stage === "concluded" || stage === "sealed" && info.concluded) return "accepted";
19178
+ if (stage === "review" || stage === "gate") return "review_pending";
19179
+ if (hasProposedRevision(revisions) && !info.concluded) return "review_pending";
19180
+ if (hasOnlyRejectedRevisions(revisions) && !info.concluded) return "rejected";
19181
+ if (stage === "blocked") return "blocked";
19182
+ return "not_ready";
19183
+ }
19184
+ function hasProposedRevision(revisions) {
19185
+ return revisions.some((revision) => revision.state === "proposed");
19186
+ }
19187
+ function hasOnlyRejectedRevisions(revisions) {
19188
+ return revisions.length > 0 && revisions.every((revision) => revision.state === "rejected");
19189
+ }
19190
+ function artifactBusinessStatusFromLayers(outputStatus, acceptanceStatus, info) {
19191
+ if (info.sealed) return "archived";
19192
+ switch (acceptanceStatus) {
19193
+ case "accepted":
19194
+ return "completed";
19195
+ case "review_pending":
19196
+ return "review";
19197
+ case "blocked":
19198
+ return "blocked";
19199
+ case "rejected":
19200
+ return "rejected";
19201
+ case "not_ready":
19202
+ default:
19203
+ if (outputStatus === "produced" || outputStatus === "stale") return "produced";
19204
+ if (outputStatus === "producing") return "producing";
19205
+ return "not_started";
19206
+ }
19207
+ }
19017
19208
  function actorToProjectMember(projectId, actor, addedAt) {
19018
19209
  return {
19019
19210
  projectId,
@@ -19438,11 +19629,14 @@ var init_service = __esm({
19438
19629
  async getProjectContents(projectId) {
19439
19630
  const project = await this.getProject(projectId);
19440
19631
  if (!project) return null;
19441
- const artifacts = (await this.artifacts.listArtifacts({ projectId })).map((a) => ({
19632
+ const artifacts = (await this.listAllProjectArtifacts(projectId)).map((a) => ({
19442
19633
  artifactId: a.artifactId,
19443
19634
  type: a.type,
19444
19635
  title: a.title,
19445
19636
  status: a.status,
19637
+ ...a.outputStatus !== void 0 ? { outputStatus: a.outputStatus } : {},
19638
+ ...a.acceptanceStatus !== void 0 ? { acceptanceStatus: a.acceptanceStatus } : {},
19639
+ ...a.businessStatus !== void 0 ? { businessStatus: a.businessStatus } : {},
19446
19640
  ...a.currentRevisionId !== void 0 ? { currentRevisionId: a.currentRevisionId } : {},
19447
19641
  updatedAt: a.updatedAt
19448
19642
  }));
@@ -19713,6 +19907,10 @@ var init_service = __esm({
19713
19907
  } else if (kernelRev?.state !== "merged") {
19714
19908
  throw new Error(`revision not approvable (state=${kernelRev?.state ?? "missing"}): ${input.revisionId}`);
19715
19909
  }
19910
+ const artifactAfterMerge = this.kernel.model.artifacts.get(input.artifactId);
19911
+ if (artifactAfterMerge?.currentRev === input.revisionId && concludedHead(this.kernel.model, input.artifactId) !== input.revisionId) {
19912
+ await this.kernel.conclude({ artifactId: input.artifactId, actor, note: "conclude after artifact revision approval" });
19913
+ }
19716
19914
  await this.syncProjection();
19717
19915
  const updatedArtifact = await this.artifacts.getArtifact(input.artifactId);
19718
19916
  const approved = await this.artifacts.getRevision(input.revisionId);
@@ -19948,18 +20146,29 @@ var init_service = __esm({
19948
20146
  const projectByWorkorder = new Map(bindings.map((binding) => [binding.workOrderId, binding.projectId]));
19949
20147
  for (const artifact of this.kernel.model.artifacts.values()) {
19950
20148
  if (projectByWorkorder.get(artifact.workspace) !== projectId) continue;
19951
- if (byArtifact.has(artifact.id)) continue;
19952
20149
  const descriptor = decodeDocumentDescription(artifact.description);
19953
20150
  const info = nodeInfo(this.kernel.model, artifact);
20151
+ const revisions = revisionsOf(this.kernel.model, artifact.id);
19954
20152
  const updatedAt = this.kernel.model.lastOpAt.get(artifact.id) ?? (/* @__PURE__ */ new Date(0)).toISOString();
20153
+ const existing = byArtifact.get(artifact.id);
20154
+ const status = artifactStatusFromNode(info, revisions);
20155
+ const outputStatus = artifactOutputStatusFromNode(info, revisions);
20156
+ const acceptanceStatus = artifactAcceptanceStatusFromNode(info, revisions);
20157
+ const businessStatus = artifactBusinessStatusFromLayers(outputStatus, acceptanceStatus, info);
19955
20158
  byArtifact.set(artifact.id, {
20159
+ ...existing ?? {},
19956
20160
  artifactId: artifact.id,
19957
20161
  projectId,
19958
20162
  type: artifact.docType ?? descriptor.docType ?? artifact.type,
19959
20163
  title: artifact.title ?? descriptor.title ?? artifact.id,
19960
- status: artifactStatusFromNode(info),
20164
+ status,
20165
+ outputStatus,
20166
+ acceptanceStatus,
20167
+ businessStatus,
20168
+ ...artifact.currentRev ? { currentRevisionId: artifact.currentRev } : {},
20169
+ ...existing?.latestRevisionId !== void 0 ? { latestRevisionId: existing.latestRevisionId } : {},
19961
20170
  createdFromWorkOrderId: artifact.workspace,
19962
- createdAt: updatedAt,
20171
+ createdAt: existing?.createdAt ?? updatedAt,
19963
20172
  updatedAt
19964
20173
  });
19965
20174
  }
@@ -26049,6 +26258,9 @@ function toApiArtifact(artifact) {
26049
26258
  type: artifact.type,
26050
26259
  title: artifact.title,
26051
26260
  status: artifact.status,
26261
+ output_status: artifact.outputStatus,
26262
+ acceptance_status: artifact.acceptanceStatus,
26263
+ business_status: artifact.businessStatus,
26052
26264
  current_revision_id: artifact.currentRevisionId,
26053
26265
  latest_revision_id: artifact.latestRevisionId,
26054
26266
  source_work_order_id: artifact.createdFromWorkOrderId,
@@ -27155,6 +27367,10 @@ Write-Host " Stop : Stop-ScheduledTask -TaskName 'OasisNode-${p.nodeId}'"
27155
27367
  Write-Host " Remove : Unregister-ScheduledTask -TaskName 'OasisNode-${p.nodeId}' -Confirm:$false"
27156
27368
  `;
27157
27369
  }
27370
+ function buildNpxCmd(p) {
27371
+ const namePart = p.nodeName ? ` --name ${p.nodeName}` : "";
27372
+ return `npx -y oasis_test@latest start --server-ws ${p.gatewayUrl} --id ${p.nodeId}${namePart} --token ${p.token}`;
27373
+ }
27158
27374
  var init_connect_script = __esm({
27159
27375
  "../server/src/domains/nodes/connect-script.ts"() {
27160
27376
  "use strict";
@@ -27190,8 +27406,20 @@ function nodesDomain(deps) {
27190
27406
  }));
27191
27407
  return { status: 200, body: { items: [...online, ...offline] } };
27192
27408
  });
27409
+ const suppressedRuntimes = /* @__PURE__ */ new Set();
27410
+ let hubListenerAdded = false;
27193
27411
  router.get("/api/runtimes", async () => {
27194
27412
  const hub = deps.getHub();
27413
+ if (hub && !hubListenerAdded) {
27414
+ hubListenerAdded = true;
27415
+ hub.addMessageListener((daemonId, msg) => {
27416
+ if (msg.type === "hello") {
27417
+ for (const id of suppressedRuntimes) {
27418
+ if (id.startsWith(`runtime:${daemonId}:`)) suppressedRuntimes.delete(id);
27419
+ }
27420
+ }
27421
+ });
27422
+ }
27195
27423
  const online = (hub?.connectedDaemons() ?? []).flatMap((d) => {
27196
27424
  if (d.meta.nodeName) nodeNames.set(d.daemonId, d.meta.nodeName);
27197
27425
  return runtimeInfos(d.daemonId, d.meta, "online");
@@ -27200,7 +27428,13 @@ function nodesDomain(deps) {
27200
27428
  const offline = (hub?.recentlyDisconnected() ?? []).flatMap(
27201
27429
  (g) => runtimeInfos(g.daemonId, g.meta, "offline").filter((r) => !onlineIds.has(r.id))
27202
27430
  );
27203
- return { status: 200, body: { items: [...online, ...offline] } };
27431
+ return { status: 200, body: { items: [...online, ...offline].filter((r) => !suppressedRuntimes.has(r.id)) } };
27432
+ });
27433
+ router.delete("/api/runtimes/:runtimeId", async (req) => {
27434
+ const runtimeId = req.params["runtimeId"];
27435
+ if (!runtimeId) return { status: 400, body: { error: "missing runtimeId" } };
27436
+ suppressedRuntimes.add(runtimeId);
27437
+ return { status: 200, body: { ok: true } };
27204
27438
  });
27205
27439
  router.post("/api/nodes/enroll", async (req) => {
27206
27440
  const body = req.body ?? {};
@@ -27214,7 +27448,8 @@ function nodesDomain(deps) {
27214
27448
  nodeNames.set(nodeId, nodeName);
27215
27449
  const params = { gatewayUrl: deps.gatewayUrl, nodeId, token, repoRemote: deps.repoRemote };
27216
27450
  const script = body.platform === "windows" ? buildWindowsConnectScript(params) : buildConnectScript(params);
27217
- return { status: 200, body: { nodeId, nodeName, token, script, gatewayUrl: deps.gatewayUrl } };
27451
+ const npxCmd = buildNpxCmd({ gatewayUrl: deps.gatewayUrl, nodeId, token, nodeName });
27452
+ return { status: 200, body: { nodeId, nodeName, token, script, gatewayUrl: deps.gatewayUrl, npxCmd } };
27218
27453
  });
27219
27454
  router.delete("/api/nodes/:nodeId", async (req) => {
27220
27455
  const nodeId = req.params["nodeId"];
@@ -27276,7 +27511,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
27276
27511
  return {
27277
27512
  id: a.id,
27278
27513
  type: a.type,
27279
- label: a.title ?? a.description ?? a.id,
27514
+ label: nodeLabel(a),
27280
27515
  stage: deriveNodeStage(info),
27281
27516
  currentRev: a.currentRev,
27282
27517
  queue: info.queueLen,
@@ -27402,7 +27637,7 @@ function buildActivity(model, arts, ref) {
27402
27637
  function buildAcceptance(model, arts) {
27403
27638
  return arts.map((a) => ({
27404
27639
  artifactId: a.id,
27405
- label: a.description ?? a.id,
27640
+ label: nodeLabel(a),
27406
27641
  state: isConcluded(model, a.id) ? "done" : a.currentRev ? "active" : "pending"
27407
27642
  }));
27408
27643
  }
@@ -27420,7 +27655,7 @@ function buildInbox(model, me) {
27420
27655
  const out = [];
27421
27656
  for (const a of model.artifacts.values()) {
27422
27657
  const ws = a.workspace;
27423
- const label = a.title ?? a.description ?? a.id;
27658
+ const label = nodeLabel(a);
27424
27659
  const queue = queueOf(model, a.id);
27425
27660
  const myOpenChangeRequest = queue.length > 0 ? annotationsOf(model, a.id).find((an) => an.state === "open" && an.author === me && an.author.startsWith("actor:human:")) : void 0;
27426
27661
  if (myOpenChangeRequest) {
@@ -27475,6 +27710,7 @@ var init_inbox = __esm({
27475
27710
  "../server/src/domains/collab/inbox.ts"() {
27476
27711
  "use strict";
27477
27712
  init_src2();
27713
+ init_node_state();
27478
27714
  }
27479
27715
  });
27480
27716
 
@@ -27547,7 +27783,7 @@ async function buildDashboard(model, oplog, me, deps) {
27547
27783
  inFlight.push({
27548
27784
  artifactId: a.id,
27549
27785
  workspace: a.workspace,
27550
- label: a.title ?? a.description ?? a.id,
27786
+ label: nodeLabel(a),
27551
27787
  stage: deriveNodeStage(nodeInfo(model, a)),
27552
27788
  heldMs,
27553
27789
  onSlaClock,
@@ -29023,7 +29259,8 @@ var init_worker = __esm({
29023
29259
  - \`oasis spawn <id> --type <t> --title "<\u77ED\u540D>" --brief "<\u89C4\u683C>" [--input <up>]\` \u2014\u2014 \u65B0\u5EFA\u8282\u70B9
29024
29260
  - \`oasis edit <id> [--title "\u2026"] [--brief "\u2026"]\` \u2014\u2014 \u6539\u8282\u70B9\u6807\u9898/brief
29025
29261
  - \`oasis unlink <consumer> --to <up>\` \u2014\u2014 \u65AD\u4F9D\u8D56\u8FB9
29026
- - \`oasis seal <id> --as cancelled|frozen [--note "\u2026"]\` \u2014\u2014 \u5C01\u8282\u70B9
29262
+ - \`oasis cancel <id> [--note "\u2026"]\` \u2014\u2014 \u5E9F\u5F03\u8282\u70B9\uFF08\u81EA\u52A8\u7EA7\u8054\u65AD\u5B83\u7684\u6240\u6709\u76F8\u5173\u8FB9\u3001\u91CD\u5524\u4E0B\u6E38\u53BB\u6389\u5BF9\u5B83\u7684\u9002\u914D\uFF09
29263
+ - \`oasis freeze <id> [--note "\u2026"]\` \u2014\u2014 \u51BB\u7ED3\u8282\u70B9\uFF08\u4FDD\u7559\u8F93\u51FA\u3001\u505C\u6B62\u6539\u52A8\uFF1B\u8FB9\u4E0D\u52A8\uFF09
29027
29264
  - \`oasis cancel-part <id> --part <\u540D>\` \u2014\u2014 \u64A4\u90E8\u4EF6
29028
29265
  - \`oasis staged\` \u2014\u2014 \u770B\u5F53\u524D\u7F13\u51B2\u5168\u90E8\u5F85\u63D0\u4EA4\u9879\uFF08+ \u63D0\u4EA4\u540E\u9884\u89C8\uFF09
29029
29266
  - \`oasis apply --reason "\u4E3A\u4EC0\u4E48\u8FD9\u4E48\u6539"\` \u2014\u2014 \u4E00\u6B21\u63D0\u4EA4\u7F13\u51B2
@@ -29184,7 +29421,7 @@ var init_worker = __esm({
29184
29421
  `## \u4F60\u7684\u52A8\u4F5C\uFF08\u6682\u5B58 \u2192 apply\uFF09`,
29185
29422
  `- \u67D0\u65E2\u6709\u6EE1\u8DB3\u8FD9\u9700\u6C42 \u2192 \`oasis link ${gap.artifactId} --to <\u65E2\u6709id>\`\uFF0C\`oasis apply --reason "\u2026"\`\uFF08apply \u540E\u81EA\u52A8\u89E3\u8FD9\u6761 gap\uFF09\u3002`,
29186
29423
  `- \u786E\u65E0\u5339\u914D\u3001\u65B0\u9700\u6C42 \u2192 \`oasis spawn <\u65B0id> --type <\u7C7B\u578B> --title "<\u77ED\u540D>" --brief "${gap.description}"\` + \`oasis link ${gap.artifactId} --to <\u65B0id>\` \u2192 apply\u3002`,
29187
- `- \u987A\u624B\u53D1\u73B0\u4E24\u4E2A\u4EA7\u7269\u91CD\u590D \u2192 **\u5148\u628A\u4E0A\u9762\u89E3 gap \u90A3\u7B14\u5355\u72EC apply \u6389**\uFF0C\u518D\u53E6\u8D77\u4E00\u7B14\uFF1A\`oasis edit\` \u6269\u4FDD\u7559\u4EF6 brief + \`oasis link\` \u6539\u8FDE + \`oasis seal\` \u5E9F\u5F03\u4EF6\uFF0C\u4E00\u8D77\u6682\u5B58\u518D apply\u3002`,
29424
+ `- \u987A\u624B\u53D1\u73B0\u4E24\u4E2A\u4EA7\u7269\u91CD\u590D \u2192 **\u5148\u628A\u4E0A\u9762\u89E3 gap \u90A3\u7B14\u5355\u72EC apply \u6389**\uFF0C\u518D\u53E6\u8D77\u4E00\u7B14\uFF1A\`oasis edit\` \u6269\u4FDD\u7559\u4EF6 brief + \`oasis link\` \u628A\u6D88\u8D39\u8005\u6539\u8FDE\u5230\u4FDD\u7559\u4EF6 + \`oasis cancel\` \u5E9F\u5F03\u4EF6\uFF08\u81EA\u52A8\u65AD\u5B83\u7684\u8FB9\u3001\u91CD\u5524\u88AB\u6539\u8FDE\u7684\u6D88\u8D39\u8005\uFF09\uFF0C\u4E00\u8D77\u6682\u5B58\u518D apply\u3002`,
29188
29425
  `- \u62FF\u4E0D\u51C6 \u2192 \u522B\u6682\u5B58\uFF0C\u4E00\u53E5\u8BDD\u8BF4\u660E\u7591\u8651\u6536\u5DE5\u3002`
29189
29426
  ].join("\n");
29190
29427
  await this.runCoordinator(gap.artifactId, task, `reconcile ${gap.artifactId}: ${gap.description.slice(0, 50)}`);
@@ -38495,13 +38732,12 @@ async function startServe(opts) {
38495
38732
  const blobs = new DirBlobStore(path11.join(opts.dir, "blobs"));
38496
38733
  const assets = new DirBlobStore(path11.join(opts.dir, "assets"));
38497
38734
  const defaultRootSchema = [
38498
- { name: "brief", description: "\u5DE5\u5355\u603B\u7EB2\uFF1A\u53D1\u8D77\u65B9\u5BF9\u9F50\u7684\u603B\u76EE\u6807\u4E0E\u8303\u56F4\uFF0C\u6574\u5F20\u534F\u4F5C\u56FE\u7684\u6839\u3002", contentType: "text", ownerRole: "founder", triggersOnMerge: [{ action: "spawn", type: "prd" }] },
38735
+ { name: "brief", description: "\u5DE5\u5355\u603B\u7EB2\uFF1A\u53D1\u8D77\u65B9\u5BF9\u9F50\u7684\u603B\u76EE\u6807\u4E0E\u8303\u56F4\uFF0C\u6574\u5F20\u534F\u4F5C\u56FE\u7684\u6839\u3002", contentType: "text", ownerRole: "founder" },
38499
38736
  {
38500
38737
  name: "prd",
38501
38738
  description: "\u4EA7\u54C1\u9700\u6C42\u6587\u6863\uFF1A\u8981\u505A\u4EC0\u4E48\u3001\u7ED9\u8C01\u3001\u6838\u5FC3\u573A\u666F\u4E0E\u9A8C\u6536\u6807\u51C6\u3002",
38502
38739
  contentType: "text",
38503
38740
  ownerRole: "pm",
38504
- triggersOnMerge: [{ action: "spawn", type: "adr" }],
38505
38741
  reviews: { gates: [{ on: "conclude", reviewers: ["architect", "designer"], quorum: 2 }] }
38506
38742
  },
38507
38743
  { name: "adr", description: "\u67B6\u6784\u51B3\u7B56\u8BB0\u5F55\uFF1A\u6280\u672F\u65B9\u6848\u9009\u578B\u4E0E\u53D6\u820D\u7406\u7531\u3002", contentType: "text", ownerRole: "architect" },
@@ -39482,7 +39718,7 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
39482
39718
  token \u89E3\u6790\u987A\u5E8F: --token > $OASIS_TOKEN > <dir>/operator-token
39483
39719
 
39484
39720
  \u7248\u672C\u673A\uFF08\xA75\uFF09
39485
- spawn <artifactId> --type <t> [--input <\u4E0A\u6E38id>[,<id>\u2026]] [--owner <actorId>] [--workspace ws] [--persistence temp|persistent] [--parts-file <json>]
39721
+ spawn <artifactId> --type <t> [--title <\u77ED\u540D>] [--brief <\u89C4\u683C>] [--input <\u4E0A\u6E38id>[,<id>\u2026]] [--owner <actorId>] [--workspace ws] [--persistence temp|persistent] [--parts-file <json>]
39486
39722
  # --parts-file\uFF1Aconvergent \u5E76\u884C\u5206\u89E3\u7684\u90E8\u4EF6\u6E05\u5355 JSON [{name, spec?, deps?}]\uFF08Stage 3\uFF0C\u7CFB\u7EDF\u6309\u90E8\u4EF6\u6D3E N \u8DEF\uFF09
39487
39723
  propose <artifactId> (--file <path> | --from-dir <path> | --git-commit <sha>) --reason <text> [--base <revisionId|none>] [--part <name>] [--rebase-of <revisionId>]
39488
39724
  # --file \u5355\u6587\u4EF6\uFF1B--from-dir \u9012\u5F52\u6574\u76EE\u5F55\u4E3A\u591A\u6587\u4EF6 revision\uFF08\u88C5\u7BB1\u5355\uFF0C\u8DE8\u7248\u672C\u53BB\u91CD\uFF0CF1\uFF09\u3002
@@ -39505,7 +39741,9 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
39505
39741
  pin <artifactId> --to <\u4E0A\u6E38> --rev <revisionId>
39506
39742
  stale <artifactId>
39507
39743
  blocked [artifactId]
39508
- seal <artifactId> --as accepted|cancelled|frozen
39744
+ cancel <artifactId> [--note "\u2026"] # \u5E9F\u5F03\u8282\u70B9\uFF1A\u7EA7\u8054\u65AD\u76F8\u5173\u8FB9\u3001\u91CD\u5524\u4E0B\u6E38
39745
+ freeze <artifactId> [--note "\u2026"] # \u51BB\u7ED3\u8282\u70B9\uFF1A\u4FDD\u7559\u8F93\u51FA\u3001\u505C\u6B62\u6539\u52A8
39746
+ accept <artifactId> [--note "\u2026"] # \u5B9A\u7A3F\uFF08\u5148 conclude \u518D accept\uFF09
39509
39747
  assign <artifactId> --owner <actorId>
39510
39748
  gc # \u5B64\u513F blob \u56DE\u6536\uFF08mark-and-sweep\uFF1B\u64CD\u4F5C\u8005\u547D\u4EE4\uFF0CF1\uFF09
39511
39749
  \u8BFB\u4EA7\u7269: status <id>\uFF08\u72B6\u6001\u7D22\u5F15\uFF0C\u4E0D\u542B\u6B63\u6587\uFF09/ content <id> [path]\uFF08\u5B9E\u9645\u5185\u5BB9\uFF0C\u591A\u6587\u4EF6\u7ED9 path \u53D6\u5355\u4E2A\uFF09/ project <id>\uFF08\u9879\u76EE\u76EE\u5F55\uFF09 # \u7ED9 agent\uFF0C\u6E10\u8FDB\u62AB\u9732
@@ -39620,10 +39858,73 @@ async function runCli(argv, println = console.log) {
39620
39858
  const dir = flags.get("dir") ?? process.env["OASIS_DIR"] ?? path12.resolve(".oasis");
39621
39859
  if (command === "node" && positional[0] !== "output") {
39622
39860
  const serverUrl = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"] ?? "ws://127.0.0.1:7320/node-gateway";
39623
- const nodeId = flags.get("id") ?? process.env["OASIS_NODE_ID"] ?? `node-${(0, import_node_crypto20.randomUUID)()}`;
39861
+ const nodeName = flags.get("name") ?? process.env["OASIS_NODE_NAME"];
39862
+ const nodeIdFile = path12.join(dir, "node-id");
39863
+ const storedNodeId = fs13.existsSync(nodeIdFile) ? fs13.readFileSync(nodeIdFile, "utf8").trim() : null;
39864
+ const nodeId = storedNodeId ?? flags.get("id") ?? process.env["OASIS_NODE_ID"] ?? `node-${(0, import_node_crypto20.randomUUID)()}`;
39865
+ if (!storedNodeId) {
39866
+ fs13.mkdirSync(dir, { recursive: true });
39867
+ fs13.writeFileSync(nodeIdFile, nodeId);
39868
+ }
39624
39869
  const nodeTokenFile = path12.join(dir, "node-token");
39625
39870
  const token2 = flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? (fs13.existsSync(nodeTokenFile) ? fs13.readFileSync(nodeTokenFile, "utf8").trim() : void 0);
39626
- await startNode({ serverUrl, nodeId, ...token2 ? { token: token2 } : {} });
39871
+ if (flags.has("service")) {
39872
+ const { spawnSync, execSync: execSync2 } = await import("node:child_process");
39873
+ if (token2) {
39874
+ fs13.mkdirSync(dir, { recursive: true });
39875
+ fs13.writeFileSync(nodeTokenFile, token2, { mode: 384 });
39876
+ }
39877
+ println("\u2192 Installing oasis_test globally...");
39878
+ spawnSync("npm", ["install", "-g", "oasis_test@latest"], { stdio: "inherit" });
39879
+ const prefix = execSync2("npm prefix -g").toString().trim();
39880
+ const binPath = path12.join(prefix, "bin", "oasis_test");
39881
+ const svcArgs = `node --server-ws ${serverUrl} --id ${nodeId} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
39882
+ if (process.platform === "linux") {
39883
+ const svcFile = path12.join(os7.homedir(), ".config/systemd/user/oasis-node.service");
39884
+ fs13.mkdirSync(path12.dirname(svcFile), { recursive: true });
39885
+ if (spawnSync("systemctl", ["--user", "is-active", "--quiet", "oasis-node"], {}).status === 0)
39886
+ spawnSync("systemctl", ["--user", "stop", "oasis-node"], { stdio: "inherit" });
39887
+ fs13.writeFileSync(svcFile, [
39888
+ "[Unit]",
39889
+ "Description=Oasis Node Daemon",
39890
+ "After=network.target",
39891
+ "",
39892
+ "[Service]",
39893
+ `ExecStart=${binPath} ${svcArgs}`,
39894
+ "Restart=on-failure",
39895
+ "RestartSec=5",
39896
+ "StandardOutput=journal",
39897
+ "StandardError=journal",
39898
+ "",
39899
+ "[Install]",
39900
+ "WantedBy=default.target",
39901
+ ""
39902
+ ].join("\n"));
39903
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
39904
+ spawnSync("systemctl", ["--user", "enable", "--now", "oasis-node"], { stdio: "inherit" });
39905
+ spawnSync("loginctl", ["enable-linger", os7.userInfo().username], { stdio: "ignore" });
39906
+ println("\u2713 Oasis node service started (systemd --user oasis-node)");
39907
+ println(" Status : systemctl --user status oasis-node");
39908
+ println(" Logs : journalctl --user -u oasis-node -f");
39909
+ println(" Stop : systemctl --user stop oasis-node");
39910
+ } else if (process.platform === "darwin") {
39911
+ const plist = path12.join(os7.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
39912
+ fs13.mkdirSync(path12.dirname(plist), { recursive: true });
39913
+ spawnSync("launchctl", ["unload", plist], { stdio: "ignore" });
39914
+ const argXml = [binPath, "node", "--server-ws", serverUrl, "--id", nodeId, "--dir", dir, ...nodeName ? ["--name", nodeName] : []].map((a) => `<string>${a}</string>`).join("");
39915
+ fs13.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.oasis.node</string><key>ProgramArguments</key><array>${argXml}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${os7.homedir()}/.oasis-node.log</string><key>StandardErrorPath</key><string>${os7.homedir()}/.oasis-node.err</string></dict></plist>`);
39916
+ spawnSync("launchctl", ["load", plist], { stdio: "inherit" });
39917
+ println("\u2713 Oasis node service started (macOS LaunchAgent com.oasis.node)");
39918
+ println(` Logs : tail -f ${os7.homedir()}/.oasis-node.log`);
39919
+ println(` Stop : launchctl unload ${plist}`);
39920
+ } else {
39921
+ spawnSync("sh", ["-c", `nohup ${binPath} ${svcArgs} > ${path12.join(os7.homedir(), ".oasis-node.log")} 2>&1 &`], { stdio: "inherit" });
39922
+ println(`\u2713 Oasis node started in background (nohup). Logs: ~/.oasis-node.log`);
39923
+ println(" Note: no automatic restart on reboot in this environment.");
39924
+ }
39925
+ return;
39926
+ }
39927
+ await startNode({ serverUrl, nodeId, ...token2 ? { token: token2 } : {}, ...nodeName ? { nodeName } : {} });
39627
39928
  return;
39628
39929
  }
39629
39930
  if (command === "node-token") {
@@ -39762,7 +40063,7 @@ async function runCli(argv, println = console.log) {
39762
40063
  if (out.length === 0) throw new Error(`\u76EE\u5F55\u4E3A\u7A7A\uFF1A${root}`);
39763
40064
  return out;
39764
40065
  };
39765
- const STAGE_COMMANDS = /* @__PURE__ */ new Set(["link", "spawn", "unlink", "seal", "cancel-part", "edit"]);
40066
+ const STAGE_COMMANDS = /* @__PURE__ */ new Set(["link", "spawn", "unlink", "cancel", "freeze", "cancel-part", "edit"]);
39766
40067
  const buildStageOp = (cmd) => {
39767
40068
  switch (cmd) {
39768
40069
  case "link":
@@ -39783,11 +40084,12 @@ async function runCli(argv, println = console.log) {
39783
40084
  };
39784
40085
  case "unlink":
39785
40086
  return { action: "unlink", artifactId: needPos(positional, 0, "oasis unlink <consumer> --to <upstream>"), to: need(flags, "to") };
39786
- case "seal":
40087
+ case "cancel":
40088
+ case "freeze":
39787
40089
  return {
39788
40090
  action: "seal",
39789
- artifactId: needPos(positional, 0, "oasis seal <id> --as cancelled|frozen"),
39790
- reason: need(flags, "as"),
40091
+ artifactId: needPos(positional, 0, `oasis ${cmd} <id> [--note \u2026]`),
40092
+ reason: cmd === "cancel" ? "cancelled" : "frozen",
39791
40093
  ...flags.get("note") !== void 0 ? { note: flags.get("note") } : {}
39792
40094
  };
39793
40095
  case "cancel-part":
@@ -39812,7 +40114,7 @@ async function runCli(argv, println = console.log) {
39812
40114
  case "unlink":
39813
40115
  return `unlink ${op["artifactId"]} \u2715 ${op["to"]}`;
39814
40116
  case "seal":
39815
- return `seal ${op["artifactId"]}\uFF08${op["reason"]}\uFF09`;
40117
+ return `${op["reason"] === "cancelled" ? "cancel" : op["reason"] === "frozen" ? "freeze" : "accept"} ${op["artifactId"]}`;
39816
40118
  case "cancelPart":
39817
40119
  return `cancel-part ${op["artifactId"]} part=${op["part"]}`;
39818
40120
  case "edit":
@@ -39824,7 +40126,11 @@ async function runCli(argv, println = console.log) {
39824
40126
  if (process.env["OASIS_STAGE"] === "1" && STAGE_COMMANDS.has(command)) {
39825
40127
  const op = buildStageOp(command);
39826
40128
  const r = await api.post("/api/stage", { op });
39827
- println(`\u5DF2\u6682\u5B58\uFF1A${describeStageOp(op)}\uFF08\u7F13\u51B2 ${r.count} \u9879\uFF1B\u6539\u5B8C \`oasis apply --reason "\u2026"\` \u63D0\u4EA4\u3001\`oasis staged\` \u770B\u5168\u90E8\uFF09`);
40129
+ let msg = `\u5DF2\u6682\u5B58\uFF1A${describeStageOp(op)}\uFF08\u7F13\u51B2 ${r.count} \u9879\uFF1B\u6539\u5B8C \`oasis apply --reason "\u2026"\` \u63D0\u4EA4\u3001\`oasis staged\` \u770B\u5168\u90E8\uFF09`;
40130
+ if (r.cascade && r.cascade.length > 0)
40131
+ msg += `
40132
+ apply \u65F6\u7EA7\u8054\u65AD\u8FB9\uFF1A${r.cascade.map((e) => `${e.artifactId}\u2192${e.to}`).join("\u3001")}\u2014\u2014\u6D88\u8D39\u8005\u4F1A\u88AB\u91CD\u5524\uFF1B\u6309\u9700\u6539\u6295(link\u2192\u66FF\u4EE3)/\u7EA7\u8054 cancel`;
40133
+ println(msg);
39828
40134
  return;
39829
40135
  }
39830
40136
  switch (command) {
@@ -40135,10 +40441,12 @@ async function runCli(argv, println = console.log) {
40135
40441
  println(message);
40136
40442
  break;
40137
40443
  }
40138
- case "seal": {
40444
+ case "cancel":
40445
+ case "freeze":
40446
+ case "accept": {
40139
40447
  const { message } = await api.cmd("seal", {
40140
- artifactId: needPos(positional, 0, "oasis seal <artifactId> --as reason"),
40141
- reason: need(flags, "as"),
40448
+ artifactId: needPos(positional, 0, `oasis ${command} <artifactId> [--note \u2026]`),
40449
+ reason: command === "cancel" ? "cancelled" : command === "freeze" ? "frozen" : "accepted",
40142
40450
  note: flags.get("note")
40143
40451
  });
40144
40452
  println(message);
@@ -40566,7 +40874,7 @@ void (async () => {
40566
40874
  await startNode({
40567
40875
  serverUrl: cfg.serverUrl,
40568
40876
  token: cfg.token,
40569
- nodeId: `node-${(0, import_node_crypto21.randomUUID)()}`,
40877
+ nodeId: cfg.nodeId ?? `node-${(0, import_node_crypto21.randomUUID)()}`,
40570
40878
  ...cfg.name ? { nodeName: cfg.name } : {}
40571
40879
  });
40572
40880
  return;
@@ -40578,7 +40886,8 @@ void (async () => {
40578
40886
  console.error("Usage: oasis start --server-ws <wss://...> --token <token> [--name <name>]");
40579
40887
  process.exit(1);
40580
40888
  }
40581
- saveConfig({ serverUrl, token, name: flags.get("name") ?? readConfig()?.name });
40889
+ const nodeId = flags.get("id") ?? readConfig()?.nodeId ?? `node-${(0, import_node_crypto21.randomUUID)()}`;
40890
+ saveConfig({ serverUrl, token, name: flags.get("name") ?? readConfig()?.name, nodeId });
40582
40891
  installBinary();
40583
40892
  stopDaemon();
40584
40893
  setupAutostart();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": { "oasis": "./dist/index.js" },
6
6
  "files": ["dist"],