oasis_test 0.1.11 → 0.1.13

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 +1169 -553
  2. package/package.json +10 -4
package/dist/index.js CHANGED
@@ -73,6 +73,8 @@ function fold(model, op) {
73
73
  ...p2.description !== void 0 ? { description: p2.description } : {},
74
74
  ...p2.docType !== void 0 ? { docType: p2.docType } : {},
75
75
  ...p2.parts !== void 0 ? { parts: p2.parts } : {},
76
+ // 浅拷贝 fields:edit 会 {...a.fields,...} 重建,但 spawn 这份不能别名日志 payload
77
+ ...p2.fields !== void 0 ? { fields: { ...p2.fields } } : {},
76
78
  // 深拷贝:投影状态绝不别名日志数据(边后续会被 link/bump 改写,日志必须不可变)
77
79
  inputs: (p2.inputs ?? []).map((e) => ({ ...e }))
78
80
  });
@@ -90,6 +92,7 @@ function fold(model, op) {
90
92
  if (p2.title !== void 0) a.title = p2.title;
91
93
  if (p2.description !== void 0) a.description = p2.description;
92
94
  if (p2.docType !== void 0) a.docType = p2.docType;
95
+ if (p2.fields !== void 0) a.fields = { ...a.fields, ...p2.fields };
93
96
  break;
94
97
  }
95
98
  case "propose_revision": {
@@ -601,6 +604,40 @@ var init_read_model = __esm({
601
604
  }
602
605
  });
603
606
 
607
+ // ../core/src/node-identity.ts
608
+ function slugify(s2, max) {
609
+ return s2.trim().toLowerCase().replace(/^wo-/, "").replace(/[^a-z0-9一-龥]+/g, "-").replace(/^-|-$/g, "").slice(0, max);
610
+ }
611
+ function deriveArtifactId(type, workspace, title, usedIds) {
612
+ const wsSlug = slugify(workspace.split(":").pop() ?? workspace, 24) || "ws";
613
+ const titleSlug = slugify(title, 30) || "untitled";
614
+ const base = `artifact:${type}:${wsSlug}-${titleSlug}`;
615
+ let id = base;
616
+ let n = 2;
617
+ while (usedIds.has(id)) id = `${base}-${n++}`;
618
+ usedIds.add(id);
619
+ return id;
620
+ }
621
+ function resolveNodeRef(ref, candidates) {
622
+ const r = ref.trim();
623
+ if (r.startsWith("artifact:")) {
624
+ const hit = candidates.find((n) => n.id === r);
625
+ return hit ? { ok: true, id: hit.id } : { ok: false, reason: "not_found" };
626
+ }
627
+ const sep = r.indexOf(":");
628
+ const type = (sep >= 0 ? r.slice(0, sep) : r).trim().toLowerCase();
629
+ const title = sep >= 0 ? r.slice(sep + 1).trim().toLowerCase() : null;
630
+ const matches = candidates.filter((n) => n.type.toLowerCase() === type && (title === null || (n.title ?? "").toLowerCase() === title));
631
+ if (matches.length === 0) return { ok: false, reason: "not_found" };
632
+ if (matches.length === 1) return { ok: true, id: matches[0].id };
633
+ return { ok: false, reason: "ambiguous", candidates: matches };
634
+ }
635
+ var init_node_identity = __esm({
636
+ "../core/src/node-identity.ts"() {
637
+ "use strict";
638
+ }
639
+ });
640
+
604
641
  // ../core/src/content-handler.ts
605
642
  function defaultHandlersByContentType() {
606
643
  return /* @__PURE__ */ new Map([
@@ -678,7 +715,7 @@ var init_content_type_handler = __esm({
678
715
  const parts = ctx.queuedPartLabels.map((p2) => `\`${p2}\``).join("\u3001") || "\uFF08\u961F\u5217\u4E3A\u7A7A\uFF09";
679
716
  return [
680
717
  `\u5404\u90E8\u4EF6\u4ECE**\u5171\u540C\u7956\u5148 H** \u5206\u5934\u4EA7\u51FA\u3001\u5404\u6539\u5404\u5757\uFF1A\u6BCF\u6761\u5185\u5BB9\u5728 \`parts/<\u540D>/CONTENT.md\`\uFF08\u6216\u5176\u5C55\u5F00\u6587\u4EF6\uFF09\u91CC\uFF08${parts}\uFF09\uFF0CH \u7684\u5185\u5BB9\u5728 \`deliverable/\`\uFF08\u4ECE\u96F6\u5206\u89E3\u5219\u4E3A\u7A7A\uFF09\u3002`,
681
- `\u628A\u5B83\u4EEC\u5408\u6210 R\\* \u7684\u5B8C\u6574\u5185\u5BB9**\u5C31\u5730\u5199\u8FDB \`deliverable/\`**\u2014\u2014**\u4EE5 H \u4E3A\u5E95**\u3001\u628A\u5404\u90E8\u4EF6\u6539\u52A8**\u5168\u5E76\u8FDB\u6765**\uFF08\u5355\u6587\u4EF6 \`deliverable/CONTENT.md\`\uFF0C\u591A\u6587\u4EF6\u94FA\u6210\u591A\u4E2A\u6587\u4EF6\uFF1B\u5927\u6587\u4EF6\u589E\u91CF\u5199\uFF1AWrite \u9AA8\u67B6 \u2192 Edit \u8FFD\u52A0\uFF09\uFF0C\u7136\u540E\uFF1A`,
718
+ `\u628A\u5B83\u4EEC\u5408\u6210\u4E00\u4E2A\u5B8C\u6574\u7684\u6574\u5408\u7248**\u5C31\u5730\u5199\u8FDB \`deliverable/\`**\u2014\u2014**\u4EE5 H \u4E3A\u5E95**\u3001\u628A\u5404\u90E8\u4EF6\u6539\u52A8**\u5168\u5E76\u8FDB\u6765**\uFF08\u5355\u6587\u4EF6 \`deliverable/CONTENT.md\`\uFF0C\u591A\u6587\u4EF6\u94FA\u6210\u591A\u4E2A\u6587\u4EF6\uFF1B\u5927\u6587\u4EF6\u589E\u91CF\u5199\uFF1AWrite \u9AA8\u67B6 \u2192 Edit \u8FFD\u52A0\uFF09\uFF0C\u7136\u540E\uFF1A`,
682
719
  `\`oasis integrate ${ctx.artifactId} --from-dir deliverable/ --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2192 \`oasis conclude ${ctx.artifactId}\`\u3002`,
683
720
  ``,
684
721
  `**\u5FE0\u5B9E\u5408\u5E76\u3001\u522B\u4E22\u4EFB\u4F55\u90E8\u4EF6**\uFF1B\u63A5\u53E3/\u547D\u540D\u5BF9\u4E0D\u9F50\u5C31\u6309\u4E0A\u6E38\u89C4\u683C\u6821\u6B63\uFF0C\u8BA9 N \u4E2A\u90E8\u4EF6\u62FC\u6210\u4E00\u4E2A\u81EA\u6D3D\u6574\u4F53\u3002`
@@ -729,7 +766,7 @@ var init_content_type_handler = __esm({
729
766
  `\u8FD9\u662F\u4EE3\u7801\u2014\u2014\u5404\u90E8\u4EF6**\u5DF2\u81EA\u884C\u5E76\u5165\u5DE5\u5355\u5206\u652F \`${feat}\`**\uFF08\u6BCF\u4E2A part \u5E72\u5B8C\u81EA\u5DF1 merge \u8FDB\u6765\u3001\u89E3\u4E86\u5404\u81EA\u7684\u51B2\u7A81\uFF09\u3002\u4F60 owner **\u4E0D\u7528\u518D\u9010\u6761\u5408\u5E76**\uFF0C\u53EA\u9A8C\u6574\u4F53 + \u6536\u53E3\uFF1A`,
730
767
  `1. \`git clone ${r0?.remote ?? "<remote>"} repos/${r0?.id ?? "<id>"}\` \u2192 \`git checkout ${feat}\`\uFF0C**\u9A8C\u6574\u4F53\u80FD\u6784\u5EFA/\u80FD\u8DD1**\uFF08\u8FD9\u624D\u662F\u4F60 owner \u7684\u5224\u65AD\u6D3B\uFF09\u3002`,
731
768
  `2. \u82E5\u6709\u6B8B\u7559\u95EE\u9898\uFF08\u63A5\u53E3\u6CA1\u5BF9\u9F50\u3001\u6784\u5EFA\u5931\u8D25\u3001\u90E8\u4EF6\u8854\u63A5\u5904\u8981\u6536\u5C3E\uFF09\u2192 \u5C31\u5730\u4FEE\u3001\`git commit\`\u3001\`git push origin ${feat}\`\u3002`,
732
- `3. \`oasis integrate ${artifactId} --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2192 \`oasis conclude ${artifactId}\`\uFF08\u7CFB\u7EDF\u4ECE \`${feat}\` \u6536\u96C6 R\\*\uFF1Bconclude \u540E\u9010\u4ED3\u540C\u6B65\u8FDB develop\uFF09\u3002`,
769
+ `3. \`oasis integrate ${artifactId} --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2192 \`oasis conclude ${artifactId}\`\uFF08\u7CFB\u7EDF\u4ECE \`${feat}\` \u6536\u96C6\u6574\u5408\u7248\uFF1Bconclude \u540E\u9010\u4ED3\u540C\u6B65\u8FDB develop\uFF09\u3002`,
733
770
  ``,
734
771
  `**\u4F60\u8FD9\u7EA7\u662F\u628A\u5173\u6574\u4F53\u81EA\u6D3D\u3001\u4E0D\u662F\u91CD\u65B0\u5408\u5E76**\u2014\u2014\u5408\u5E76\u5DF2\u7ECF\u5728\u5404\u90E8\u4EF6\u6536\u5C3E\u65F6\u5404\u81EA\u505A\u5B8C\u4E86\u3002`
735
772
  ];
@@ -1248,6 +1285,48 @@ var init_grounding = __esm({
1248
1285
  });
1249
1286
 
1250
1287
  // ../core/src/kernel.ts
1288
+ function fieldValueMatches(val, type) {
1289
+ switch (type) {
1290
+ case "string":
1291
+ return typeof val === "string";
1292
+ case "number":
1293
+ return typeof val === "number";
1294
+ case "boolean":
1295
+ return typeof val === "boolean";
1296
+ case "string[]":
1297
+ return Array.isArray(val) && val.every((x2) => typeof x2 === "string");
1298
+ }
1299
+ }
1300
+ function coerceFieldValue(key, val, type) {
1301
+ if (fieldValueMatches(val, type)) return val;
1302
+ if (typeof val === "string") {
1303
+ if (type === "number" && val.trim() !== "" && !Number.isNaN(Number(val))) return Number(val);
1304
+ if (type === "boolean" && (val === "true" || val === "false")) return val === "true";
1305
+ }
1306
+ throw new KernelError(`\u5B57\u6BB5 '${key}' \u7C7B\u578B\u5E94\u4E3A ${type}${type === "string[]" ? "\uFF08\u6570\u7EC4\u503C\u8BF7\u7528 --set-json\uFF09" : ""}`);
1307
+ }
1308
+ function validateArtifactFields(fields, specs, mode) {
1309
+ const declared = new Map((specs ?? []).map((s2) => [s2.name, s2]));
1310
+ let out;
1311
+ if (fields !== void 0) {
1312
+ out = {};
1313
+ for (const [key, val] of Object.entries(fields)) {
1314
+ const spec = declared.get(key);
1315
+ if (!spec) {
1316
+ throw new KernelError(`\u672A\u58F0\u660E\u5B57\u6BB5 '${key}'\uFF08${declared.size > 0 ? `\u8BE5\u7C7B\u578B\u58F0\u660E\uFF1A${[...declared.keys()].join(", ")}` : "\u8BE5\u7C7B\u578B\u672A\u58F0\u660E\u4EFB\u4F55\u81EA\u5B9A\u4E49\u5B57\u6BB5"}\uFF09`);
1317
+ }
1318
+ out[key] = val === null ? null : coerceFieldValue(key, val, spec.type);
1319
+ }
1320
+ }
1321
+ if (mode === "spawn") {
1322
+ for (const spec of specs ?? []) {
1323
+ if (spec.required && (fields?.[spec.name] === void 0 || fields[spec.name] === null)) {
1324
+ throw new KernelError(`\u7F3A\u5FC5\u586B\u5B57\u6BB5 '${spec.name}'\uFF08${spec.type}\uFF09`);
1325
+ }
1326
+ }
1327
+ }
1328
+ return out;
1329
+ }
1251
1330
  var import_node_crypto, SYSTEM_ACTOR, MAX_PREREQ_DEPTH, KernelError, FrozenError, Kernel;
1252
1331
  var init_kernel = __esm({
1253
1332
  "../core/src/kernel.ts"() {
@@ -1256,6 +1335,7 @@ var init_kernel = __esm({
1256
1335
  init_src();
1257
1336
  init_read_model();
1258
1337
  init_content_handler();
1338
+ init_node_identity();
1259
1339
  init_governance();
1260
1340
  init_manifest_store();
1261
1341
  init_grounding();
@@ -1350,6 +1430,7 @@ var init_kernel = __esm({
1350
1430
  );
1351
1431
  }
1352
1432
  this.enforceAcl("spawn", typeDef, args.actor);
1433
+ const fields = this.schema.size > 0 ? validateArtifactFields(args.fields, typeDef?.fieldDefs, "spawn") : args.fields;
1353
1434
  const inputs = [];
1354
1435
  for (const edge of args.inputs ?? []) {
1355
1436
  if (edge.to === args.id) throw new KernelError("\u4E0D\u80FD\u4F9D\u8D56\u81EA\u5DF1");
@@ -1378,7 +1459,8 @@ var init_kernel = __esm({
1378
1459
  ...args.title !== void 0 ? { title: args.title } : {},
1379
1460
  ...args.description !== void 0 ? { description: args.description } : {},
1380
1461
  ...args.docType !== void 0 ? { docType: args.docType } : {},
1381
- ...args.parts !== void 0 ? { parts: args.parts } : {}
1462
+ ...args.parts !== void 0 ? { parts: args.parts } : {},
1463
+ ...fields !== void 0 ? { fields } : {}
1382
1464
  };
1383
1465
  await this.commit(args.id, args.actor, "spawn_artifact", args.id, payload);
1384
1466
  return this.model.artifacts.get(args.id);
@@ -1993,7 +2075,64 @@ var init_kernel = __esm({
1993
2075
  * 预检(②终态校验 + ③advisory + ④爆炸半径)。preview 白送:
1994
2076
  * structuredClone 读模型、灌假设 op、看终态——derived-not-stored 的红利,无须仿真引擎。
1995
2077
  */
2078
+ /**
2079
+ * 二期 title-identity:把 plan 里的【节点引用】(`type` | `type:title` | `id`)解析成最终 id。
2080
+ * - spawn 省略 id → 据 (type,title) 派生(usedIds 预种工单现有 id + 本批显式 id,不和存量撞)。
2081
+ * - 引用现有/本批节点 → 在 plan.workspace 范围内 {现有 ∪ 本批 spawn} 匹配;撞多个 throw 带候选、无则 throw。
2082
+ * - 批内**先派生所有 spawn 的 id,再解引用**(link 才引得到本批新建的)。
2083
+ * - workspace 缺省 = 只走 id(向后兼容 passthrough)。**幂等**:解析一份已解析的 plan = 原样。
2084
+ * 落 oplog 的是解析后的 id(重放确定)。
2085
+ */
2086
+ resolveInterventionRefs(plan) {
2087
+ const ws = plan.workspace;
2088
+ const existing = ws ? [...this.model.artifacts.values()].filter((a) => a.workspace === ws).map((a) => ({ id: a.id, type: a.type, ...a.title ? { title: a.title } : {} })) : [];
2089
+ const usedIds = new Set(existing.map((n) => n.id));
2090
+ for (const op of plan.ops) if (op.action === "spawn" && op.id) usedIds.add(op.id);
2091
+ const batch = [];
2092
+ const spawnIds = plan.ops.map((op) => {
2093
+ if (op.action !== "spawn") return void 0;
2094
+ if (op.id) {
2095
+ batch.push({ id: op.id, type: op.type, ...op.title ? { title: op.title } : {} });
2096
+ return op.id;
2097
+ }
2098
+ if (!ws) throw new KernelError("spawn \u7701\u7565 id \u9700\u8981 InterventionPlan.workspace + title \u624D\u80FD\u6D3E\u751F");
2099
+ const title = op.title?.trim();
2100
+ if (!title) throw new KernelError("spawn \u7701\u7565 id \u65F6 title \u5FC5\u586B\uFF08\u636E (type,title) \u6D3E\u751F id\uFF09");
2101
+ if ([...existing, ...batch].some((n) => n.type === op.type && (n.title ?? "").toLowerCase() === title.toLowerCase()))
2102
+ throw new KernelError(`\u8282\u70B9 title \u649E\u540D\uFF1A${op.type}\u300C${title}\u300D\u5728\u672C\u5DE5\u5355\u5DF2\u5B58\u5728,\u8BF7\u6362\u4E2A\u533A\u5206\u7684\u540D\u5B57`);
2103
+ const id = deriveArtifactId(op.type, ws, title, usedIds);
2104
+ batch.push({ id, type: op.type, title });
2105
+ return id;
2106
+ });
2107
+ const candidates = [...existing, ...batch];
2108
+ const ref = (r, what) => {
2109
+ if (r === void 0) return void 0;
2110
+ if (!ws) return r;
2111
+ const res = resolveNodeRef(r, candidates);
2112
+ if (res.ok) return res.id;
2113
+ if (res.reason === "ambiguous")
2114
+ throw new KernelError(`${what}\u300C${r}\u300D\u5339\u914D\u5230\u591A\u4E2A\u8282\u70B9,\u8BF7\u6307\u660E\u54EA\u4E2A:
2115
+ ${res.candidates.map((c) => ` - ${c.type}:${c.title ?? "(\u65E0title)"} \u2192 ${c.id}`).join("\n")}`);
2116
+ throw new KernelError(`${what}\u300C${r}\u300D\u627E\u4E0D\u5230\u5BF9\u5E94\u8282\u70B9\uFF08\u53EF\u7528 type / type:title / id\uFF09`);
2117
+ };
2118
+ const ops = plan.ops.map((op, i) => {
2119
+ switch (op.action) {
2120
+ case "spawn":
2121
+ return { ...op, id: spawnIds[i], ...op.workspace === void 0 && ws ? { workspace: ws } : {}, ...op.inputs ? { inputs: op.inputs.map((e) => ({ to: ref(e.to, "spawn \u4F9D\u8D56"), ...e.required !== void 0 ? { required: e.required } : {} })) } : {} };
2122
+ case "link":
2123
+ case "unlink":
2124
+ case "pin":
2125
+ return { ...op, artifactId: ref(op.artifactId, op.action), to: ref(op.to, `${op.action} \u4E0A\u6E38`) };
2126
+ case "annotate":
2127
+ return { ...op, artifactId: ref(op.artifactId, "annotate"), ...op.raisedFrom ? { raisedFrom: ref(op.raisedFrom, "annotate raisedFrom") } : {} };
2128
+ default:
2129
+ return { ...op, artifactId: ref(op.artifactId, op.action) };
2130
+ }
2131
+ });
2132
+ return { ...plan, ops };
2133
+ }
1996
2134
  async previewIntervention(plan) {
2135
+ plan = this.resolveInterventionRefs(plan);
1997
2136
  const constituents = this.buildConstituents(plan);
1998
2137
  const touched = [...new Set(constituents.map((c) => c.artifactId))];
1999
2138
  const baseSeqs = Object.fromEntries(touched.map((id) => [id, this.model.lastSeq.get(id) ?? 0]));
@@ -2122,6 +2261,7 @@ var init_kernel = __esm({
2122
2261
  * v1 注:inverse 批次未物化(oplog 信息足以推导),可逆性=再发一个 intervention。
2123
2262
  */
2124
2263
  async applyIntervention(plan, baseSeqs, actor) {
2264
+ plan = this.resolveInterventionRefs(plan);
2125
2265
  const { changeClass, nonMonotonicOps } = classifyIntervention(plan);
2126
2266
  if (changeClass === "C" && !isHumanActor(actor)) {
2127
2267
  throw new KernelError(
@@ -2199,27 +2339,31 @@ var init_kernel = __esm({
2199
2339
  buildConstituentsRaw(plan) {
2200
2340
  const order = { spawn: 0, edit: 1, reopen: 2, assign: 3, link: 4, pin: 5, unlink: 6, cancelPart: 7, seal: 8, annotate: 9 };
2201
2341
  const sorted = [...plan.ops].sort((a, b2) => order[a.action] - order[b2.action]);
2202
- const spawnedHere = new Set(plan.ops.flatMap((op) => op.action === "spawn" ? [op.id] : []));
2342
+ const spawnedHere = new Set(plan.ops.flatMap((op) => op.action === "spawn" && op.id ? [op.id] : []));
2203
2343
  const isPrereqFill = (id) => plan.ops.some((op) => op.action === "link" && op.to === id && !spawnedHere.has(op.artifactId));
2204
2344
  return sorted.map((op) => {
2205
2345
  switch (op.action) {
2206
2346
  case "spawn": {
2347
+ const spawnId = op.id;
2348
+ if (!spawnId) throw new KernelError("internal: spawn id \u672A\u89E3\u6790\uFF08\u5E94\u5148\u8FC7 resolveInterventionRefs\uFF09");
2207
2349
  const typeDef = this.schema.get(op.type);
2208
2350
  if (this.schema.size > 0 && !typeDef) throw new KernelError(`unknown ArtifactType: ${op.type}`);
2351
+ const spawnFields = this.schema.size > 0 ? validateArtifactFields(op.fields, typeDef?.fieldDefs, "spawn") : op.fields;
2209
2352
  const owner = op.owner ?? this.resolveOwner(op.type, typeDef);
2210
2353
  return {
2211
- artifactId: op.id,
2354
+ artifactId: spawnId,
2212
2355
  kind: "spawn_artifact",
2213
- target: op.id,
2356
+ target: spawnId,
2214
2357
  payload: {
2215
2358
  type: op.type,
2216
2359
  owner,
2217
2360
  workspace: op.workspace ?? "ws:default",
2218
2361
  persistence: "persistent",
2219
- createdVia: isPrereqFill(op.id) ? "prereq" : "manual-spawn",
2362
+ createdVia: isPrereqFill(spawnId) ? "prereq" : "manual-spawn",
2220
2363
  ...op.title !== void 0 ? { title: op.title } : {},
2221
2364
  ...op.description !== void 0 ? { description: op.description } : {},
2222
2365
  ...op.docType !== void 0 ? { docType: op.docType } : {},
2366
+ ...spawnFields !== void 0 ? { fields: spawnFields } : {},
2223
2367
  inputs: (op.inputs ?? []).map((e) => ({
2224
2368
  to: e.to,
2225
2369
  pinned: concludedHead(this.model, e.to),
@@ -2254,7 +2398,12 @@ var init_kernel = __esm({
2254
2398
  ...op.blocks !== void 0 ? { blocks: op.blocks } : {}
2255
2399
  }
2256
2400
  };
2257
- case "edit":
2401
+ case "edit": {
2402
+ let editFields = op.fields;
2403
+ if (this.schema.size > 0 && op.fields !== void 0) {
2404
+ const a = this.model.artifacts.get(op.artifactId);
2405
+ editFields = validateArtifactFields(op.fields, a ? this.schema.get(a.type)?.fieldDefs : void 0, "edit");
2406
+ }
2258
2407
  return {
2259
2408
  artifactId: op.artifactId,
2260
2409
  kind: "edit_artifact",
@@ -2262,9 +2411,11 @@ var init_kernel = __esm({
2262
2411
  payload: {
2263
2412
  ...op.title !== void 0 ? { title: op.title } : {},
2264
2413
  ...op.description !== void 0 ? { description: op.description } : {},
2265
- ...op.docType !== void 0 ? { docType: op.docType } : {}
2414
+ ...op.docType !== void 0 ? { docType: op.docType } : {},
2415
+ ...editFields !== void 0 ? { fields: editFields } : {}
2266
2416
  }
2267
2417
  };
2418
+ }
2268
2419
  }
2269
2420
  });
2270
2421
  }
@@ -2500,7 +2651,7 @@ function commandReference(opts) {
2500
2651
  const write = [`**\u5199**`];
2501
2652
  if (opts.action === "integrate") {
2502
2653
  write.push(
2503
- opts.isCode ? `- \`oasis integrate ${id} --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2014\u2014 **\u6536\u53E3\u4E13\u7528**\uFF1A\u628A\u5DE5\u5355\u5206\u652F\u7684\u6574\u5408\u7248\u843D\u4E3A R\\*\uFF08\u7CFB\u7EDF\u4ECE git \u6536\u96C6\uFF1B\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002**\u7EDD\u4E0D\u8981\u7528 propose/advance \u4EA4\u96C6\u6210**\u3002` : `- \`oasis integrate ${id} --from-dir deliverable/ --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2014\u2014 **\u6536\u53E3\u4E13\u7528**\uFF1A\u628A\u4F60\u5728 deliverable/ \u5408\u6210\u7684 R\\* \u843D\u4E3A\u65B0 head\uFF08\u5185\u90E8\u81EA\u52A8 FF \u961F\u9996 + \u628A\u5176\u4F59\u7ADE\u4E89\u7248\u6298\u8FDB\u6765\uFF09\u3002**\u7EDD\u4E0D\u8981\u7528 propose/advance \u4EA4\u96C6\u6210**\uFF08\u4F1A\u628A\u96C6\u6210\u5F53\u6210\u53C8\u4E00\u4E2A\u7ADE\u4E89\u7248\u3001\u843D\u9519\u7248\u672C\uFF09\u3002`
2654
+ opts.isCode ? `- \`oasis integrate ${id} --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2014\u2014 **\u6536\u53E3\u4E13\u7528**\uFF1A\u628A\u5DE5\u5355\u5206\u652F\u7684\u6574\u5408\u7248\u843D\u4E3A R\\*\uFF08\u7CFB\u7EDF\u4ECE git \u6536\u96C6\uFF1B\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002**\u7EDD\u4E0D\u8981\u7528 propose/advance \u4EA4\u96C6\u6210**\u3002` : `- \`oasis integrate ${id} --from-dir deliverable/ --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2014\u2014 **\u6536\u53E3\u4E13\u7528**\uFF1A\u628A\u4F60\u5728 deliverable/ \u5408\u6210\u7684\u6574\u5408\u7248\u843D\u4E3A\u65B0 head\uFF08\u7CFB\u7EDF\u5185\u90E8\u4F1A\u628A\u961F\u5217\u91CC\u5176\u4F59\u5F85\u843D\u7248\u672C\u4E00\u5E76\u5E76\u8FDB\u6765\uFF09\u3002**\u7EDD\u4E0D\u8981\u7528 propose / advance \u4EA4\u96C6\u6210**\uFF08\u90A3\u4F1A\u628A\u96C6\u6210\u5F53\u6210\u53C8\u4E00\u4E2A\u5E76\u5217\u7248\u672C\u3001\u843D\u9519\u7248\u672C\uFF09\u3002`
2504
2655
  );
2505
2656
  } else {
2506
2657
  const reviewSuffix = opts.hasOpenAnnotations ? "\u5F53\u524D\u6709\u672A\u51B3\u4EBA\u5DE5/\u6279\u6CE8\u8981\u6C42\uFF0C\u63D0\u4EA4\u540E\u4F1A\u8FDB\u5165\u4EBA\u5DE5\u5BA1\u6838/\u961F\u5217\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u843D head\uFF1B\u4E0D\u8981\u628A\u4EBA\u5DE5\u53D8\u66F4\u8BF7\u6C42\u81EA\u884C\u6807\u4E3A resolved\uFF0C\u7B49\u4EBA\u5BA1\u6838\u901A\u8FC7\u6216\u9000\u56DE\u3002" : "\u4F60\u662F\u5355 producer\u3001\u961F\u5217\u7A7A\u65F6\u5B83**\u81EA\u52A8\u843D head**\uFF0C\u4E0D\u7528 advance\u3002";
@@ -2510,15 +2661,15 @@ function commandReference(opts) {
2510
2661
  }
2511
2662
  write.push(
2512
2663
  `- \`oasis conclude ${id}\` \u2014\u2014 \u5BA3\u5E03\u4EA7\u7269**\u5B8C\u6574\u3001\u53EF\u88AB\u4E0B\u6E38\u6D88\u8D39**\uFF0C\u76D6\u4E00\u4E2A\u91CC\u7A0B\u7891\u3002\u524D\u63D0\uFF1A\u961F\u5217\u5DF2\u6E05\u7A7A\u3001\u4E14\u65E0\u672A\u89E3\u6790 gap / \u672A\u7B54\u590D\u56DE\u538B\uFF08\u5426\u5219\u88AB\u62D2\uFF09\u3002\u76D6\u5B8C\u4E0B\u6E38\u6536\u5230\u901A\u77E5\u3002`,
2513
- `- \`oasis add-revision ${id} --part <\u90E8\u4EF6\u540D> --role <\u89D2\u8272> --desc "<\u8FD9\u90E8\u4EF6\u505A\u4EC0\u4E48>"\` \u2014\u2014 \u628A\u672C\u4EA7\u7269\u62C6\u6210\u4E00\u4E2A**\u53EF\u5E76\u884C\u4EA7\u51FA\u7684\u5B50\u90E8\u4EF6**\u6D3E\u51FA\u53BB\u3002\`--role\` \u7F3A\u7701=\u4F60\u81EA\u5DF1\uFF1B\u5404\u90E8\u4EF6\u4EA7\u5B8C\u7531\u4F60 \`oasis integrate\` \u6536\u53E3\u3002`,
2664
+ `- \`oasis add-revision ${id} --desc "<\u8FD9\u90E8\u4EF6\u505A\u4EC0\u4E48>" [--name <\u90E8\u4EF6\u540D>] [--role <\u89D2\u8272>]\` \u2014\u2014 \u628A\u672C\u4EA7\u7269\u62C6\u51FA\u4E00\u4E2A**\u53EF\u5E76\u884C\u4EA7\u51FA\u7684\u5B50\u90E8\u4EF6**\u6D3E\u51FA\u53BB\u3002\`--desc\`\uFF08**\u5FC5\u586B**\uFF09= \u8FD9\u4E2A\u5B50\u90E8\u4EF6\u8981\u5B8C\u6210\u4EC0\u4E48\uFF1B\`--name\` = \u7ED9\u5B83\u8D77\u7684\u90E8\u4EF6\u540D\uFF08\u53EF\u9009\uFF0C\u7F3A\u7701\u81EA\u52A8\u751F\u6210\uFF09\uFF1B\`--role\` = \u8C01\u6765\u505A\uFF08\u53EF\u9009\uFF0C\u7F3A\u7701=\u4F60\u81EA\u5DF1\uFF09\u3002\u5404\u90E8\u4EF6\u4EA7\u5B8C\u7531\u4F60 \`oasis integrate\` \u6536\u53E3\u3002`,
2514
2665
  `- \`oasis gap ${id} --desc "<\u7F3A\u4EC0\u4E48>"\` \u2014\u2014 \u7F3A\u4E00\u4E2A\u8FD8\u4E0D\u5B58\u5728\u7684\u524D\u7F6E\u3001\u505A\u4E0D\u4E0B\u53BB\u65F6\u7528\u3002\`--desc\` \u63CF\u8FF0\u7F3A\u4EC0\u4E48\uFF08\u534F\u8C03\u8005\u636E\u5B83\u5224\u8FDE\u65E2\u6709/\u65B0\u5EFA\uFF09\u3002\u62A5\u5B8C**\u6536\u5DE5**\uFF1A\u8FD9\u6761\u4F1A\u6682\u505C\u3001\u524D\u7F6E\u5C31\u7EEA\u540E\u91CD\u65B0\u5524\u8D77\u4F60\uFF1B\u671F\u95F4\u522B propose\u3002`,
2515
2666
  `- \`oasis annotate <\u4E0A\u6E38id> --from ${id} --body "<\u7591\u95EE>"\` \u2014\u2014 \u5BF9\u67D0\u4E0A\u6E38\u63D0\u7591\u95EE/\u5F02\u8BAE\uFF08\u56DE\u538B\uFF09\u3002\`<\u4E0A\u6E38id>\`=\u88AB\u95EE\u7684\u4E0A\u6E38\uFF1B\`--from\`=\u672C\u4EA7\u7269\uFF08\u7CFB\u7EDF\u636E\u6B64\u5728\u4E0A\u6E38\u7B54\u590D\u540E\u91CD\u6D3E\u4F60\uFF09\uFF1B\`--body\`=\u5177\u4F53\u95EE\u9898\u3002\u63D0\u5B8C**\u6536\u5DE5**\u7B49\u7B54\u590D\uFF0C\u522B propose\u3002`
2516
2667
  );
2517
2668
  if (opts.hasOpenAnnotations) {
2518
- write.push(`- \`oasis resolve <annId> --resolution resolved|wontfix|acknowledged [--note "..."]\` \u2014\u2014 \u7B54\u590D/\u4E86\u7ED3**\u6302\u5728\u4F60\u4EA7\u7269\u4E0A\u7684** annotation\uFF08\u542B\u4E0B\u6E38\u56DE\u538B\uFF09\u3002`);
2669
+ write.push(`- \`oasis resolve <annId> --as resolved|wontfix|acknowledged [--note "<\u8BF4\u660E>"]\` \u2014\u2014 \u7B54\u590D / \u4E86\u7ED3**\u6302\u5728\u4F60\u4EA7\u7269\u4E0A\u7684** annotation\uFF08\u542B\u4E0B\u6E38\u56DE\u538B\uFF09\u3002\`<annId>\`\uFF08\u4F4D\u7F6E\u53C2\u6570\uFF09= \u8981\u4E86\u7ED3\u7684\u6279\u6CE8 id\uFF1B\`--as\` = \u7ED3\u8BBA\uFF08resolved=\u5DF2\u89E3\u51B3 / wontfix=\u4E0D\u6539 / acknowledged=\u5DF2\u77E5\u6089\uFF09\uFF1B\`--note\` = \u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09\u3002`);
2519
2670
  }
2520
2671
  if (opts.action === "integrate") {
2521
- write.push(`- \`oasis reject <revId> --reason "<\u4E3A\u4EC0\u4E48>"\` \u2014\u2014 \u5F03\u7528\u961F\u5217\u91CC\u67D0\u4E2A\u574F\u7684\u90E8\u4EF6-revision\uFF08\u4E0D\u5E76\u8FDB\u96C6\u6210\uFF09\u3002`);
2672
+ write.push(`- \`oasis reject <artifactId> <revId> --reason "<\u4E3A\u4EC0\u4E48>"\` \u2014\u2014 \u5F03\u7528\u961F\u5217\u91CC\u67D0\u4E2A\u574F\u7684\u90E8\u4EF6-revision\uFF08\u4E0D\u5E76\u8FDB\u96C6\u6210\uFF09\u3002\`<artifactId>\`\uFF08**\u7B2C\u4E00\u4E2A**\u4F4D\u7F6E\u53C2\u6570\uFF09= \u4EA7\u7269 id\uFF1B\`<revId>\`\uFF08\u7B2C\u4E8C\u4E2A\uFF09= \u8981\u5F03\u7528\u7684 revision id\uFF1B\`--reason\` = \u4E3A\u4EC0\u4E48\u5F03\u3002`);
2522
2673
  }
2523
2674
  return [`## \u4F60\u80FD\u7528\u7684 oasis \u547D\u4EE4\uFF08\u672C\u6B21\uFF09`, ...write, ``, ...readFull];
2524
2675
  }
@@ -2672,6 +2823,10 @@ async function assembleContext(args) {
2672
2823
  }
2673
2824
  }
2674
2825
  }
2826
+ if (args.materializeSkills) {
2827
+ const skillFiles = await args.materializeSkills().catch(() => ({}));
2828
+ for (const [p2, c] of Object.entries(skillFiles)) files[p2] = c;
2829
+ }
2675
2830
  const wsCodeRepo = args.codeRepo ?? null;
2676
2831
  const isCodeArtifact = typeDef?.contentType === "code" && !!wsCodeRepo?.repos?.some((r) => r.remote);
2677
2832
  files["TASK.md"] = [
@@ -2935,6 +3090,8 @@ var init_dispatcher = __esm({
2935
3090
  inFlight = /* @__PURE__ */ new Map();
2936
3091
  /** v1 调度准入:在途会话的 actor(jobKey → actorId),用于每 agent 并发计数。与 inFlight 同生同灭。 */
2937
3092
  inFlightActor = /* @__PURE__ */ new Map();
3093
+ /** 在途会话的派发时刻(jobKey → ms),算"已跑多久"用(协调者病历的会话情况)。与 inFlight 同生同灭。 */
3094
+ inFlightStartedAt = /* @__PURE__ */ new Map();
2938
3095
  strikes = /* @__PURE__ */ new Map();
2939
3096
  /** v1 调度准入:上一轮 tick 中"够格但被并发上限挡下"的 produce(隐式队列;下个 tick 重评)。
2940
3097
  * jobKey → 撞的是哪道闸(global 全局总量 / per-actor owner 自己到上限)+ 该 actor——
@@ -2979,8 +3136,9 @@ var init_dispatcher = __esm({
2979
3136
  artifactId: this.artifactIdFromJobKey(jobKey),
2980
3137
  sessionId: handle.id,
2981
3138
  actor: this.inFlightActor.get(jobKey) ?? "",
2982
- action: jobKey.slice(0, jobKey.indexOf("::"))
3139
+ action: jobKey.slice(0, jobKey.indexOf("::")),
2983
3140
  // produce / review / integrate
3141
+ startedAt: new Date(this.inFlightStartedAt.get(jobKey) ?? Date.now()).toISOString()
2984
3142
  });
2985
3143
  }
2986
3144
  return out;
@@ -3129,6 +3287,8 @@ var init_dispatcher = __esm({
3129
3287
  const lastFailure = retry && retry.count > 0 && retry.lastReason ? { reason: retry.lastReason } : void 0;
3130
3288
  const ws = kernel.model.artifacts.get(spec.artifactId)?.workspace;
3131
3289
  const codeRepo = ws ? await this.opts.resolveCodeRepo?.(ws) ?? null : null;
3290
+ const binding = await this.opts.resolveBinding?.(spec.actor) ?? void 0;
3291
+ const materializeSkills = this.opts.materializeSkills ? () => this.opts.materializeSkills(spec.actor, binding?.runtimeKind ?? "claude") : void 0;
3132
3292
  const bundle = await assembleContext({
3133
3293
  model: kernel.model,
3134
3294
  blobs: this.opts.blobs,
@@ -3140,9 +3300,9 @@ var init_dispatcher = __esm({
3140
3300
  ...lastFailure ? { lastFailure } : {},
3141
3301
  ...this.opts.schema !== void 0 ? { schema: this.opts.schema } : {},
3142
3302
  ...codeRepo ? { codeRepo } : {},
3143
- ...this.opts.resolveActorContext !== void 0 ? { resolveActorContext: this.opts.resolveActorContext } : {}
3303
+ ...this.opts.resolveActorContext !== void 0 ? { resolveActorContext: this.opts.resolveActorContext } : {},
3304
+ ...materializeSkills ? { materializeSkills } : {}
3144
3305
  });
3145
- const binding = await this.opts.resolveBinding?.(spec.actor) ?? void 0;
3146
3306
  const provisioned = this.opts.provision ? await this.opts.provision(spec.actor) : void 0;
3147
3307
  const job = {
3148
3308
  actor: spec.actor,
@@ -3159,6 +3319,7 @@ var init_dispatcher = __esm({
3159
3319
  this.inFlight.set(jobKey, session);
3160
3320
  this.inFlightActor.set(jobKey, spec.actor);
3161
3321
  const dispatchedAtMs = Date.now();
3322
+ this.inFlightStartedAt.set(jobKey, dispatchedAtMs);
3162
3323
  const seqAtDispatch = seqNow;
3163
3324
  const sink = this.opts.trajectory;
3164
3325
  if (sink) {
@@ -3224,6 +3385,7 @@ var init_dispatcher = __esm({
3224
3385
  }
3225
3386
  this.inFlight.delete(jobKey);
3226
3387
  this.inFlightActor.delete(jobKey);
3388
+ this.inFlightStartedAt.delete(jobKey);
3227
3389
  const seqAtExit = kernel.model.lastSeq.get(spec.artifactId) ?? 0;
3228
3390
  if (sink) {
3229
3391
  void (async () => {
@@ -3363,6 +3525,7 @@ var init_src2 = __esm({
3363
3525
  "../core/src/index.ts"() {
3364
3526
  "use strict";
3365
3527
  init_read_model();
3528
+ init_node_identity();
3366
3529
  init_content_handler();
3367
3530
  init_content_type_handler();
3368
3531
  init_diff();
@@ -20363,6 +20526,36 @@ var init_drafts = __esm({
20363
20526
  }
20364
20527
  });
20365
20528
 
20529
+ // ../server/src/governance/workorder-drafts.ts
20530
+ var WorkorderDraftStore;
20531
+ var init_workorder_drafts = __esm({
20532
+ "../server/src/governance/workorder-drafts.ts"() {
20533
+ "use strict";
20534
+ WorkorderDraftStore = class {
20535
+ drafts = /* @__PURE__ */ new Map();
20536
+ seq = 0;
20537
+ submit(d) {
20538
+ const draft = { ...d, id: `wodraft:${++this.seq}`, at: (/* @__PURE__ */ new Date()).toISOString() };
20539
+ this.drafts.set(draft.id, draft);
20540
+ return draft;
20541
+ }
20542
+ list() {
20543
+ return [...this.drafts.values()];
20544
+ }
20545
+ /** 按 workspace 取最新一份:friday 用它发起建单时生成的 workspace id 取回草案,无需解析 chat 文本。 */
20546
+ forWorkspace(workspace) {
20547
+ return [...this.drafts.values()].reverse().find((d) => d.workspace === workspace);
20548
+ }
20549
+ get(id) {
20550
+ return this.drafts.get(id);
20551
+ }
20552
+ remove(id) {
20553
+ return this.drafts.delete(id);
20554
+ }
20555
+ };
20556
+ }
20557
+ });
20558
+
20366
20559
  // ../server/src/domains/collab/planner.ts
20367
20560
  function ensureDefaultWorkorderTypes(schema) {
20368
20561
  const out = [...schema];
@@ -20371,18 +20564,17 @@ function ensureDefaultWorkorderTypes(schema) {
20371
20564
  }
20372
20565
  return out;
20373
20566
  }
20374
- function parsePlanningIssues(body) {
20375
- const first = body.split("\n", 1)[0] ?? "";
20376
- if (!first.startsWith(PLANNING_ISSUES_PREFIX)) return [];
20377
- try {
20378
- const parsed = JSON.parse(first.slice(PLANNING_ISSUES_PREFIX.length));
20379
- return Array.isArray(parsed) ? parsed : [];
20380
- } catch {
20381
- return [];
20567
+ function upgradeDefaultFieldDefs(schema) {
20568
+ const changed = [];
20569
+ for (const def of DEFAULT_TYPES) {
20570
+ if (!def.fieldDefs) continue;
20571
+ const cur = schema.find((item) => item.name === def.name);
20572
+ if (cur && !cur.fieldDefs) {
20573
+ cur.fieldDefs = def.fieldDefs;
20574
+ changed.push(def.name);
20575
+ }
20382
20576
  }
20383
- }
20384
- function workorderSpecBody(spec) {
20385
- return WORKORDER_SPEC_PREFIX + JSON.stringify(spec);
20577
+ return changed;
20386
20578
  }
20387
20579
  function parseWorkorderSpec(body) {
20388
20580
  const first = body.split("\n", 1)[0] ?? "";
@@ -20420,6 +20612,12 @@ async function buildDynamicWorkorderPlan(input) {
20420
20612
  return match;
20421
20613
  };
20422
20614
  const briefOwner = input.actor;
20615
+ const specFields = {};
20616
+ if (input.goal?.trim()) specFields["goal"] = input.goal.trim();
20617
+ if (input.acceptanceCriteria?.length) {
20618
+ const items = input.acceptanceCriteria.map((x2) => x2.trim()).filter(Boolean);
20619
+ if (items.length > 0) specFields["acceptanceCriteria"] = items;
20620
+ }
20423
20621
  const ops = [
20424
20622
  {
20425
20623
  action: "spawn",
@@ -20429,23 +20627,12 @@ async function buildDynamicWorkorderPlan(input) {
20429
20627
  workspace: input.workspace,
20430
20628
  // 工单名 → brief.title;工单目标/brief → brief.description(别再把 name 塞进 description、丢掉 goal)
20431
20629
  ...input.title?.trim() ? { title: input.title.trim() } : {},
20432
- description: input.description
20630
+ description: input.description,
20631
+ ...Object.keys(specFields).length > 0 ? { fields: specFields } : {}
20433
20632
  }
20434
20633
  ];
20435
- const spec = {};
20436
- if (input.goal?.trim()) spec.goal = input.goal.trim();
20437
- if (input.acceptanceCriteria?.length) {
20438
- const items = input.acceptanceCriteria.map((x2) => x2.trim()).filter(Boolean);
20439
- if (items.length > 0) spec.acceptanceCriteria = items;
20440
- }
20441
- if (spec.goal !== void 0 || spec.acceptanceCriteria !== void 0) {
20442
- ops.push({ action: "annotate", artifactId: briefId, body: workorderSpecBody(spec), blocks: false });
20443
- }
20444
20634
  const spawned = [{ id: briefId, type: "brief", role: "pm", owner: briefOwner }];
20445
20635
  const agentGraph = input.agentPlan ? normalizeAgentPlan(input.agentPlan, schema.map((d) => d.name)) : null;
20446
- const briefByType = new Map(
20447
- (input.agentPlan?.nodes ?? []).filter((n) => typeof n.brief === "string" && n.brief.trim().length > 0).map((n) => [n.type.trim().toLowerCase(), n.brief.trim()])
20448
- );
20449
20636
  if (input.agentPlan && !agentGraph) {
20450
20637
  issues.push({
20451
20638
  code: "planner_invalid",
@@ -20453,51 +20640,63 @@ async function buildDynamicWorkorderPlan(input) {
20453
20640
  message: "pm_agent \u7684\u89C4\u5212\u7ED3\u679C\u4E3A\u7A7A\u6216\u683C\u5F0F\u65E0\u6548\uFF0C\u5DF2\u56DE\u9000\u5230\u89C4\u5219 planner\u3002"
20454
20641
  });
20455
20642
  }
20456
- const desired = agentGraph?.types ?? desiredArtifacts(primaryType, input.description);
20643
+ for (const dup of agentGraph?.dupTitles ?? []) {
20644
+ issues.push({ code: "title_collision", severity: "warning", message: `\u8282\u70B9 title \u649E\u540D\u300C${dup}\u300D\u2014\u2014\u540C\u4E00\u5DE5\u5355\u540C\u7C7B\u578B title \u987B\u552F\u4E00\uFF0C\u5DF2\u5FFD\u7565\u91CD\u590D\u9879\uFF1B\u8BF7\u6539\u4E2A\u533A\u5206\u7684\u540D\u5B57\u3002` });
20645
+ }
20646
+ const usedIds = /* @__PURE__ */ new Set([briefId]);
20457
20647
  const availableNodes = [];
20458
- for (const type of desired) {
20459
- const def = schemaByName.get(type);
20460
- if (!def) {
20461
- issues.push({
20462
- code: "missing_artifact_type",
20463
- severity: "warning",
20464
- artifactType: type,
20465
- message: `\u7F3A\u5C11 ArtifactType ${type}\uFF0C\u672C\u6B21\u4E0D\u4F1A\u521B\u5EFA\u8BE5\u8282\u70B9\u3002`
20648
+ if (agentGraph) {
20649
+ for (const node of agentGraph.nodes) {
20650
+ const def = schemaByName.get(node.type);
20651
+ if (!def) {
20652
+ issues.push({ code: "missing_artifact_type", severity: "warning", artifactType: node.type, message: `\u7F3A\u5C11 ArtifactType ${node.type}\uFF0C\u672C\u6B21\u4E0D\u4F1A\u521B\u5EFA\u8BE5\u8282\u70B9\u3002` });
20653
+ continue;
20654
+ }
20655
+ const owner = await resolveRole(def.ownerRole, node.type);
20656
+ const ownerActor = owner.actor ?? `pending:role:${def.ownerRole}`;
20657
+ availableNodes.push({
20658
+ type: node.type,
20659
+ title: node.title,
20660
+ id: deriveArtifactId(node.type, input.workspace, node.title, usedIds),
20661
+ role: def.ownerRole,
20662
+ owner: ownerActor,
20663
+ dependsOn: node.dependsOn,
20664
+ ...node.brief ? { brief: node.brief } : {}
20466
20665
  });
20467
- continue;
20468
20666
  }
20469
- const owner = await resolveRole(def.ownerRole, type);
20470
- const ownerActor = owner.actor ?? (type === primaryType ? input.actor : void 0);
20471
- if (!ownerActor) continue;
20472
- const id = type === primaryType ? primaryId : `artifact:${type}:${slug4}`;
20473
- availableNodes.push({ type, id, role: def.ownerRole, owner: ownerActor });
20667
+ } else {
20668
+ const desired = desiredArtifacts(primaryType, input.description);
20669
+ for (const type of desired) {
20670
+ const def = schemaByName.get(type);
20671
+ if (!def) {
20672
+ issues.push({ code: "missing_artifact_type", severity: "warning", artifactType: type, message: `\u7F3A\u5C11 ArtifactType ${type}\uFF0C\u672C\u6B21\u4E0D\u4F1A\u521B\u5EFA\u8BE5\u8282\u70B9\u3002` });
20673
+ continue;
20674
+ }
20675
+ const owner = await resolveRole(def.ownerRole, type);
20676
+ const ownerActor = owner.actor ?? (type === primaryType ? input.actor : `pending:role:${def.ownerRole}`);
20677
+ const id = type === primaryType ? primaryId : `artifact:${type}:${slug4}`;
20678
+ availableNodes.push({ type, id, role: def.ownerRole, owner: ownerActor });
20679
+ }
20474
20680
  }
20475
20681
  for (const node of availableNodes) {
20476
- const inputs = inputEdgesFor(node.type, availableNodes, briefId, agentGraph?.dependsOn);
20682
+ const inputs = agentGraph ? agentEdgesFor(node.dependsOn ?? [], availableNodes, briefId, issues) : ruleEdgesFor(node.type, availableNodes, briefId);
20477
20683
  ops.push({
20478
20684
  action: "spawn",
20479
20685
  id: node.id,
20480
20686
  type: node.type,
20481
20687
  owner: node.owner,
20482
20688
  workspace: input.workspace,
20689
+ ...node.title ? { title: node.title } : {},
20483
20690
  inputs,
20484
- description: node.type === "prd" && issues.length > 0 ? `\u964D\u7EA7\u65B9\u6848\u6587\u6863\uFF1A${displayTitle}` : `${labelForType(node.type)}\uFF1A${displayTitle}`
20691
+ description: !agentGraph && node.type === "prd" && issues.length > 0 ? `\u964D\u7EA7\u65B9\u6848\u6587\u6863\uFF1A${displayTitle}` : `${labelForType(node.type)}\uFF1A${node.title ?? displayTitle}`
20485
20692
  });
20486
20693
  ops.push({
20487
20694
  action: "annotate",
20488
20695
  artifactId: node.id,
20489
- body: briefByType.get(node.type) ?? taskBookFor(node.type, input.description, node.role, issues, agentGraph?.notes),
20490
- blocks: false
20491
- });
20492
- spawned.push(node);
20493
- }
20494
- if (issues.length > 0) {
20495
- ops.push({
20496
- action: "annotate",
20497
- artifactId: briefId,
20498
- body: planningIssueBody(issues),
20696
+ body: node.brief ?? taskBookFor(node.type, input.description, node.role, issues, agentGraph?.notes),
20499
20697
  blocks: false
20500
20698
  });
20699
+ spawned.push({ id: node.id, type: node.type, role: node.role, owner: node.owner });
20501
20700
  }
20502
20701
  return {
20503
20702
  plan: {
@@ -20543,18 +20742,21 @@ function buildPmAgentPlannerPrompt(input) {
20543
20742
  "",
20544
20743
  JSON.stringify({
20545
20744
  nodes: [
20546
- { type: "prd", dependsOn: ["brief"], title: "\u9700\u6C42\u4E0E\u8303\u56F4\u8BF4\u660E", brief: "\u8FD9\u4E2A\u8282\u70B9\u5177\u4F53\u8981\u505A\u4EC0\u4E48\u7684\u89C4\u683C\uFF1A\u4EA7\u51FA\u4EC0\u4E48\u3001\u8981\u70B9\u3001\u7EA6\u675F" },
20547
- { type: "dev", dependsOn: ["prd"], title: "\u5B9E\u73B0", brief: "\u2026" }
20745
+ { type: "prd", title: "\u9700\u6C42\u4E0E\u8303\u56F4\u8BF4\u660E", dependsOn: ["brief"], brief: "\u8FD9\u4E2A\u8282\u70B9\u5177\u4F53\u8981\u505A\u4EC0\u4E48\u7684\u89C4\u683C\uFF1A\u4EA7\u51FA\u4EC0\u4E48\u3001\u8981\u70B9\u3001\u7EA6\u675F" },
20746
+ { type: "research", title: "\u7ADE\u54C1\u8C03\u7814", dependsOn: ["brief"], brief: "\u2026" },
20747
+ { type: "dev", title: "\u540E\u7AEF\u5B9E\u73B0", dependsOn: ["prd:\u9700\u6C42\u4E0E\u8303\u56F4\u8BF4\u660E"], brief: "\u2026" }
20548
20748
  ],
20549
20749
  notes: ["\u53EF\u9009\uFF1A\u89C4\u5212\u7406\u7531\u6216\u98CE\u9669\u63D0\u793A"]
20550
20750
  }, null, 2),
20551
20751
  "",
20552
20752
  "\u89C4\u5219\uFF1A",
20553
20753
  "- brief \u6839\u8282\u70B9\u7531\u7CFB\u7EDF\u56FA\u5B9A\u521B\u5EFA\uFF0C\u4F60\u4E0D\u8981\u628A brief \u5199\u8FDB nodes\u3002",
20554
- "- type \u53EA\u80FD\u4ECE\u4E0A\u9762\u3010\u53EF\u7528 ArtifactType\u3011\u76EE\u5F55\u91CC\u9009\uFF08\u4EE5\u6CE8\u5165\u76EE\u5F55\u4E3A\u51C6\uFF0C\u4E0D\u9650\u4E8E\u67D0\u51E0\u4E2A\u56FA\u5B9A\u7C7B\u578B\uFF09\u3002",
20555
- "- \u6BCF\u4E2A\u8282\u70B9\u5199 brief\uFF1A\u8FD9\u4E2A\u8282\u70B9\u5177\u4F53\u8981\u505A\u4EC0\u4E48\u7684\u89C4\u683C\uFF08\u4EA7\u51FA\u4EC0\u4E48\u3001\u8981\u70B9\u3001\u7EA6\u675F\uFF09\uFF0C\u522B\u5957\u6A21\u677F\u3001\u522B\u53EA\u5199\u7C7B\u578B\u540D\u3002",
20556
- "- \u7B80\u5355\u9700\u6C42\u53EF\u4EE5\u53EA\u8981 prd\uFF1B\u6709\u8C03\u7814/\u8BBE\u8BA1/\u5F00\u53D1/\u6D4B\u8BD5/\u67B6\u6784\u9700\u8981\u65F6\u518D\u52A0\u5BF9\u5E94\u7C7B\u578B\u7684\u8282\u70B9\u3002",
20557
- "- dependsOn \u4F7F\u7528 type \u540D\uFF0C\u5141\u8BB8\u5F15\u7528 brief\u3002"
20754
+ "- type \u53EA\u80FD\u4ECE\u4E0A\u9762\u3010\u53EF\u7528 ArtifactType\u3011\u76EE\u5F55\u91CC\u9009\uFF08\u4EE5\u6CE8\u5165\u76EE\u5F55\u4E3A\u51C6\uFF09\u3002",
20755
+ "- \u6BCF\u4E2A\u8282\u70B9\u5FC5\u7ED9 title\uFF08\u226420\u5B57\u3001\u7528\u6765\u533A\u5206\u8282\u70B9\uFF09+ brief\uFF08\u8FD9\u4E2A\u8282\u70B9\u8981\u4EA7\u51FA\u4EC0\u4E48\u7684\u89C4\u683C\uFF0C\u522B\u5957\u6A21\u677F\u3001\u522B\u53EA\u5199\u7C7B\u578B\u540D\uFF09\u3002",
20756
+ '- \u3010\u53EF\u6709\u591A\u4E2A\u540C type \u8282\u70B9\u3011\uFF0C\u7528\u4E0D\u540C title \u533A\u5206\uFF08\u5982\u4E24\u4E2A prd\uFF1A"\u524D\u7AEF\u9700\u6C42"\u3001"\u540E\u7AEF\u9700\u6C42"\uFF1B\u591A\u4EFD\u8C03\u7814\u62A5\u544A\u5404\u53D6\u5176\u540D\uFF09\u3002',
20757
+ "- \u540C\u4E00\u5DE5\u5355\u5185\u3001\u540C type \u4E0B title \u5FC5\u987B\u552F\u4E00\uFF1Btitle \u4E0D\u542B\u534A\u89D2\u5192\u53F7 `:`\uFF08\u4E2D\u6587\u5168\u89D2 `\uFF1A` \u53EF\u4EE5\uFF09\u3002",
20758
+ '- dependsOn \u5F15\u7528\u4E0A\u6E38\u8282\u70B9\u7684 "type:title"\uFF08\u5982 "prd:\u9700\u6C42\u4E0E\u8303\u56F4\u8BF4\u660E"\uFF09\uFF0C\u6216\u7528 "brief" \u5F15\u7528\u5DE5\u5355\u6839\uFF1B\u65E0\u4E0A\u6E38\u7ED9 []\u3002',
20759
+ "- \u7B80\u5355\u9700\u6C42\u53EF\u4EE5\u53EA\u8981 prd\uFF1B\u6709\u8C03\u7814/\u8BBE\u8BA1/\u5F00\u53D1/\u6D4B\u8BD5/\u67B6\u6784\u9700\u8981\u65F6\u518D\u52A0\u5BF9\u5E94\u8282\u70B9\u3002"
20558
20760
  ].join("\n");
20559
20761
  }
20560
20762
  function parseAgentWorkorderPlan(text) {
@@ -20652,16 +20854,28 @@ function desiredArtifacts(primaryType, description) {
20652
20854
  }
20653
20855
  function normalizeAgentPlan(plan, validTypes) {
20654
20856
  const allowed = validTypes.map((t) => t.trim().toLowerCase()).filter((t) => t !== "brief");
20655
- const types2 = [];
20656
- const dependsOn = /* @__PURE__ */ new Map();
20857
+ const nodes = [];
20858
+ const seen = /* @__PURE__ */ new Set();
20859
+ const dupTitles = [];
20657
20860
  for (const node of plan.nodes) {
20658
20861
  const type = node.type.trim().toLowerCase();
20659
20862
  if (type === "brief") continue;
20660
- if (!types2.includes(type)) types2.push(type);
20661
- if (node.dependsOn?.length) dependsOn.set(type, [...new Set(node.dependsOn.map((dep) => dep.trim().toLowerCase()).filter(Boolean))]);
20863
+ if (!allowed.includes(type)) continue;
20864
+ const title = (node.title?.trim() || labelForType(type)).slice(0, 40);
20865
+ const key = `${type}:${title.toLowerCase()}`;
20866
+ if (seen.has(key)) {
20867
+ dupTitles.push(`${type}:${title}`);
20868
+ continue;
20869
+ }
20870
+ seen.add(key);
20871
+ nodes.push({
20872
+ type,
20873
+ title,
20874
+ dependsOn: node.dependsOn?.map((d) => d.trim()).filter(Boolean) ?? [],
20875
+ ...node.brief ? { brief: node.brief } : {}
20876
+ });
20662
20877
  }
20663
- const filtered = allowed.filter((type) => types2.includes(type));
20664
- return filtered.length > 0 ? { types: filtered, dependsOn, ...plan.notes?.length ? { notes: plan.notes } : {} } : null;
20878
+ return nodes.length > 0 ? { nodes, dupTitles, ...plan.notes?.length ? { notes: plan.notes } : {} } : null;
20665
20879
  }
20666
20880
  function extractJson(text) {
20667
20881
  const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
@@ -20674,23 +20888,30 @@ function extractJson(text) {
20674
20888
  if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);
20675
20889
  return null;
20676
20890
  }
20677
- function inputEdgesFor(type, nodes, briefId, plannedDependsOn) {
20678
- const idOf = (t) => nodes.find((node) => node.type === t)?.id;
20891
+ function agentEdgesFor(dependsOn, nodes, briefId, issues) {
20679
20892
  const edges = /* @__PURE__ */ new Set();
20680
- const planned = plannedDependsOn?.get(type);
20681
- if (planned) {
20682
- for (const dep of planned) {
20683
- if (dep === "brief") edges.add(briefId);
20684
- else {
20685
- const id = idOf(dep);
20686
- if (id) edges.add(id);
20687
- }
20893
+ for (const dep of dependsOn) {
20894
+ const d = dep.trim();
20895
+ if (!d) continue;
20896
+ if (d.toLowerCase() === "brief") {
20897
+ edges.add(briefId);
20898
+ continue;
20688
20899
  }
20900
+ const sep = d.indexOf(":");
20901
+ const depType = (sep >= 0 ? d.slice(0, sep) : d).trim().toLowerCase();
20902
+ const depTitle = sep >= 0 ? d.slice(sep + 1).trim().toLowerCase() : null;
20903
+ const match = nodes.find((n) => n.type === depType && (depTitle === null || (n.title ?? "").toLowerCase() === depTitle));
20904
+ if (match) edges.add(match.id);
20905
+ else issues.push({ code: "planner_invalid", severity: "warning", message: `\u8282\u70B9\u4F9D\u8D56\u300C${dep}\u300D\u627E\u4E0D\u5230\u5BF9\u5E94\u4E0A\u6E38\uFF08\u6309 type:title \u5339\u914D\uFF09\uFF0C\u5DF2\u5FFD\u7565\u8FD9\u6761\u8FB9\u3002` });
20689
20906
  }
20690
- if (edges.size === 0) {
20691
- if (type !== "prd") edges.add(idOf("prd") ?? briefId);
20692
- else edges.add(briefId);
20693
- }
20907
+ if (edges.size === 0) edges.add(briefId);
20908
+ return [...edges].map((to) => ({ to, required: true }));
20909
+ }
20910
+ function ruleEdgesFor(type, nodes, briefId) {
20911
+ const idOf = (t) => nodes.find((node) => node.type === t)?.id;
20912
+ const edges = /* @__PURE__ */ new Set();
20913
+ if (type !== "prd") edges.add(idOf("prd") ?? briefId);
20914
+ else edges.add(briefId);
20694
20915
  if (type === "adr") {
20695
20916
  const research = idOf("research");
20696
20917
  if (research) edges.add(research);
@@ -20727,17 +20948,6 @@ ${plannerNotes.map((note) => `- ${note}`).join("\n")}` : "";
20727
20948
  notes
20728
20949
  ].join("\n");
20729
20950
  }
20730
- function planningIssueBody(issues) {
20731
- return [
20732
- `${PLANNING_ISSUES_PREFIX}${JSON.stringify(dedupeIssues(issues))}`,
20733
- "",
20734
- "# \u5DE5\u5355\u89C4\u5212\u63D0\u793A",
20735
- "",
20736
- ...dedupeIssues(issues).map((issue2) => `- ${issue2.message}`),
20737
- "",
20738
- "\u7CFB\u7EDF\u5DF2\u5148\u521B\u5EFA\u53EF\u63A5\u5355\u8282\u70B9\uFF1B\u7F3A\u5931\u80FD\u529B\u6682\u65F6\u964D\u7EA7\u4E3A PM \u65B9\u6848\u6587\u6863\uFF0C\u8865\u9F50\u5458\u5DE5\u6216\u8FD0\u884C\u65F6\u7ED1\u5B9A\u540E\u53EF\u518D\u6269\u56FE\u3002"
20739
- ].join("\n");
20740
- }
20741
20951
  function dedupeIssues(issues) {
20742
20952
  const seen = /* @__PURE__ */ new Set();
20743
20953
  const out = [];
@@ -20763,16 +20973,19 @@ function labelForType(type) {
20763
20973
  }
20764
20974
  function artifactSlug(idOrWorkspace, description) {
20765
20975
  const source = idOrWorkspace.split(":").pop() ?? description;
20766
- const slug4 = source.trim().toLowerCase().replace(/^wo-/, "").replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
20767
- return slug4 || `wo-${Date.now()}`;
20976
+ return slugify(source, 40) || `wo-${Date.now()}`;
20768
20977
  }
20769
- var PLANNING_ISSUES_PREFIX, DEFAULT_TYPES, WORKORDER_SPEC_PREFIX;
20978
+ var DEFAULT_TYPES, WORKORDER_SPEC_PREFIX;
20770
20979
  var init_planner = __esm({
20771
20980
  "../server/src/domains/collab/planner.ts"() {
20772
20981
  "use strict";
20773
- PLANNING_ISSUES_PREFIX = "OASIS_PLANNING_ISSUES ";
20982
+ init_src2();
20774
20983
  DEFAULT_TYPES = [
20775
- { name: "brief", contentType: "text", ownerRole: "pm" },
20984
+ // brief goal/acceptanceCriteria 现在是【自定义字段】(Phase 3,替代旧 OASIS_WORKORDER_SPEC 批注,铲 #1 死锁根)
20985
+ { name: "brief", contentType: "text", ownerRole: "pm", fieldDefs: [
20986
+ { name: "goal", type: "string", description: "\u4E00\u53E5\u8BDD\u76EE\u6807\uFF1A\u8FD9\u4E2A\u5DE5\u5355\u8981\u8FBE\u6210\u4EC0\u4E48" },
20987
+ { name: "acceptanceCriteria", type: "string[]", description: "\u9A8C\u6536\u6761\u76EE\uFF08\u6EE1\u8DB3\u5373\u7B97\u5B8C\u6210\uFF09" }
20988
+ ] },
20776
20989
  { name: "prd", contentType: "text", ownerRole: "pm" },
20777
20990
  { name: "research", contentType: "text", ownerRole: "research" },
20778
20991
  { name: "design", contentType: "text", ownerRole: "designer" },
@@ -20784,145 +20997,6 @@ var init_planner = __esm({
20784
20997
  }
20785
20998
  });
20786
20999
 
20787
- // ../server/src/domains/collab/node-state.ts
20788
- function nodeLabel(a) {
20789
- if (a.title) return a.title;
20790
- if (a.description) return a.description;
20791
- const tail = a.id.split(":").slice(2).join(":");
20792
- return tail ? `\u672A\u547D\u540D ${a.type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${a.type}`;
20793
- }
20794
- function nodeInfo(model, a) {
20795
- const lifecycle = lifecycleOf(model, a.id);
20796
- const sealed = lifecycle !== "active";
20797
- const concluded = isConcluded(model, a.id);
20798
- const mark = sealed ? lifecycle : pendingConcludeAttempt(model, a.id) ? "gate-pending" : blockedOf(model, a.id).blocked ? "blocked" : concluded ? "concluded" : a.currentRev ? "active" : "embryo";
20799
- const hasWorking = revisionsOf(model, a.id).some((r) => r.state === "working");
20800
- return { mark, queueLen: queueOf(model, a.id).length, concluded, sealed, hasWorking };
20801
- }
20802
- function deriveStage(members) {
20803
- if (members.length > 0 && members.every((m2) => m2.sealed)) return "sealed";
20804
- const live = members.filter((m2) => !m2.sealed);
20805
- if (live.length > 0 && live.every((m2) => m2.concluded)) return "concluded";
20806
- const reviewing = live.some((m2) => m2.mark === "gate-pending" || m2.mark === "active" && m2.queueLen > 0);
20807
- if (reviewing) return "review";
20808
- const progressing = live.some((m2) => m2.mark === "active" || m2.mark === "embryo");
20809
- const blocked = live.some((m2) => m2.mark === "blocked");
20810
- if (blocked && !progressing) return "blocked";
20811
- return "executing";
20812
- }
20813
- function deriveNodeStage(info) {
20814
- if (info.sealed) return "sealed";
20815
- switch (info.mark) {
20816
- case "concluded":
20817
- return "concluded";
20818
- case "gate-pending":
20819
- return "gate";
20820
- case "blocked":
20821
- return "blocked";
20822
- case "embryo":
20823
- return "embryo";
20824
- default:
20825
- if (info.hasWorking) return "editing";
20826
- if (info.queueLen > 0) return "review";
20827
- return "merged";
20828
- }
20829
- }
20830
- var init_node_state = __esm({
20831
- "../server/src/domains/collab/node-state.ts"() {
20832
- "use strict";
20833
- init_src2();
20834
- }
20835
- });
20836
-
20837
- // ../server/src/domains/collab/workorders.ts
20838
- function buildWorkorderSummaries(model, resolveActor, resolveProject) {
20839
- const byWorkspace = /* @__PURE__ */ new Map();
20840
- for (const a of model.artifacts.values()) {
20841
- const arr = byWorkspace.get(a.workspace) ?? [];
20842
- arr.push(a);
20843
- byWorkspace.set(a.workspace, arr);
20844
- }
20845
- const ref = (id) => {
20846
- const extra = resolveActor?.(id);
20847
- return {
20848
- id,
20849
- ...extra?.name ? { name: extra.name } : {},
20850
- ...extra?.role ? { role: extra.role } : {},
20851
- ...extra?.avatar ? { avatar: extra.avatar } : {}
20852
- };
20853
- };
20854
- const out = [];
20855
- for (const [workspace, arts] of byWorkspace) {
20856
- const members = arts.map((a) => nodeInfo(model, a));
20857
- const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
20858
- const spec = workorderSpecOf(model, arts);
20859
- const concluded = members.filter((m2) => m2.concluded).length;
20860
- const lastOps = arts.map((a) => model.lastOpAt.get(a.id)).filter((t) => Boolean(t)).sort();
20861
- const participantIds = [...new Set(arts.map((a) => a.owner))];
20862
- const woTitle = seed.title ?? seed.description ?? workspace;
20863
- out.push({
20864
- id: workspace,
20865
- title: woTitle,
20866
- // 工单完整内容 = 根产物 description;仅当与标题不同(根产物有独立 title)时给出,避免与 title 重复
20867
- ...seed.description && seed.description !== woTitle ? { description: seed.description } : {},
20868
- // 一句话目标:优先取结构化 spec.goal;缺省时前端回退 description 首段(投影不替前端做回退,只给真值)
20869
- ...spec?.goal ? { goal: spec.goal } : {},
20870
- stage: deriveStage(members),
20871
- owner: ref(seed.owner),
20872
- participants: participantIds.map(ref),
20873
- artifactCount: arts.length,
20874
- progress: { concluded, total: arts.length },
20875
- updatedAt: lastOps[lastOps.length - 1] ?? "",
20876
- projectId: resolveProject?.(workspace) ?? null,
20877
- planning: planningStatusOf(model, arts),
20878
- ...workspaceDispatchPaused(model, arts) ? { dispatchPaused: true } : {}
20879
- });
20880
- }
20881
- return out.sort((a, b2) => a.updatedAt < b2.updatedAt ? 1 : a.updatedAt > b2.updatedAt ? -1 : 0);
20882
- }
20883
- function planningStatusOf(model, arts) {
20884
- const ids = new Set(arts.map((a) => a.id));
20885
- const issues = [...model.annotations.values()].filter((annotation) => ids.has(annotation.anchor.target)).flatMap((annotation) => parsePlanningIssues(annotation.body));
20886
- return issues.length > 0 ? { status: "needs_staffing", issues } : { status: "ready", issues: [] };
20887
- }
20888
- function workorderSpecOf(model, arts) {
20889
- const seed = arts.find((a) => a.type === "brief") ?? arts.find((a) => a.inputs.length === 0);
20890
- if (!seed) return null;
20891
- for (const annotation of model.annotations.values()) {
20892
- if (annotation.anchor.target !== seed.id) continue;
20893
- const spec = parseWorkorderSpec(annotation.body);
20894
- if (spec) return spec;
20895
- }
20896
- return null;
20897
- }
20898
- function workspaceDispatchPaused(model, arts) {
20899
- const seed = arts.find((a) => a.type === "brief") ?? arts.find((a) => a.inputs.length === 0);
20900
- if (!seed) return false;
20901
- for (const annotation of model.annotations.values()) {
20902
- if (annotation.anchor.target !== seed.id) continue;
20903
- if (annotation.state === "open" && annotation.body.startsWith(DISPATCH_HOLD_PREFIX)) return true;
20904
- }
20905
- return false;
20906
- }
20907
- function openDispatchHoldOf(model, arts) {
20908
- const seed = arts.find((a) => a.type === "brief") ?? arts.find((a) => a.inputs.length === 0);
20909
- if (!seed) return null;
20910
- for (const annotation of model.annotations.values()) {
20911
- if (annotation.anchor.target !== seed.id) continue;
20912
- if (annotation.state === "open" && annotation.body.startsWith(DISPATCH_HOLD_PREFIX)) return annotation.id;
20913
- }
20914
- return null;
20915
- }
20916
- var DISPATCH_HOLD_PREFIX;
20917
- var init_workorders = __esm({
20918
- "../server/src/domains/collab/workorders.ts"() {
20919
- "use strict";
20920
- init_node_state();
20921
- init_planner();
20922
- DISPATCH_HOLD_PREFIX = "OASIS_DISPATCH_HOLD";
20923
- }
20924
- });
20925
-
20926
21000
  // ../server/src/server.ts
20927
21001
  function wantsChatParts(req) {
20928
21002
  const accept = req.headers.accept;
@@ -21183,6 +21257,10 @@ async function runCommand(kernel, blobs, oplog, actor, command, args, ctx = {})
21183
21257
  ...p2.spec !== void 0 ? { spec: String(p2.spec) } : {},
21184
21258
  ...Array.isArray(p2.deps) ? { deps: p2.deps.map(String) } : {}
21185
21259
  }));
21260
+ const rawFields = args["fields"];
21261
+ if (rawFields !== void 0 && (typeof rawFields !== "object" || rawFields === null || Array.isArray(rawFields))) {
21262
+ throw new Error("\u53C2\u6570 fields \u5FC5\u987B\u662F\u5BF9\u8C61 {key: value}");
21263
+ }
21186
21264
  const createdViaArg = optStr(args, "createdVia");
21187
21265
  if (createdViaArg !== void 0 && createdViaArg !== "seed" && createdViaArg !== "manual-spawn") {
21188
21266
  throw new Error(`createdVia \u4EC5\u63A5\u53D7 seed\uFF08\u53D1\u8D77\u5DE5\u5355\uFF09\u6216 manual-spawn\uFF1Bauto-trigger/prereq \u7531\u5185\u6838\u5185\u90E8\u8BBE\u7F6E`);
@@ -21274,6 +21352,33 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
21274
21352
  if (!preview2.endState.ok) {
21275
21353
  throw new Error(`\u52A8\u6001\u89C4\u5212\u7EC8\u6001\u6821\u9A8C\u4E0D\u8FC7\uFF1A${preview2.endState.violations.join("\uFF1B")}`);
21276
21354
  }
21355
+ if (args["stage"] === true && ctx.workorderDrafts) {
21356
+ const draft = ctx.workorderDrafts.submit({
21357
+ workspace,
21358
+ plan: planned.plan,
21359
+ rootArtifactId: planned.rootArtifactId,
21360
+ seed: {
21361
+ brief,
21362
+ ...goal !== void 0 ? { goal } : {},
21363
+ ...acceptanceCriteria && acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {},
21364
+ ...args["dispatch"] === false ? { dispatch: false } : {}
21365
+ },
21366
+ draftedBy: actor
21367
+ });
21368
+ return {
21369
+ message: planned.issues.length > 0 ? `staged workorder draft ${draft.id}\uFF1A${planned.spawned.length} \u4E2A\u8282\u70B9\u5F85\u4EBA\u786E\u8BA4\uFF0C${planned.issues.length} \u4E2A staffing \u63D0\u793A` : `staged workorder draft ${draft.id}\uFF1A${planned.spawned.length} \u4E2A\u8282\u70B9\u5F85\u4EBA\u786E\u8BA4`,
21370
+ data: {
21371
+ draftId: draft.id,
21372
+ workspace,
21373
+ rootArtifactId: planned.rootArtifactId,
21374
+ spawned: planned.spawned,
21375
+ planning: {
21376
+ status: planned.issues.length > 0 ? "needs_staffing" : "ready",
21377
+ issues: planned.issues
21378
+ }
21379
+ }
21380
+ };
21381
+ }
21277
21382
  const applied = await kernel.applyIntervention(planned.plan, preview2.baseSeqs, actor);
21278
21383
  const briefArtifactId = planned.rootArtifactId;
21279
21384
  if (brief && brief.trim()) {
@@ -21283,7 +21388,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
21283
21388
  await kernel.conclude({ artifactId: briefArtifactId, actor, note: "\u5EFA\u5355\u65F6\u7531\u53D1\u8D77\u4EBA\u62CD\u677F\u5B9A\u7A3F" });
21284
21389
  }
21285
21390
  if (args["dispatch"] === false) {
21286
- await kernel.annotate({ artifactId: briefArtifactId, actor, body: `${DISPATCH_HOLD_PREFIX} \u5F85\u53D1\u8D77\u4EBA\u6D3E\u5355` });
21391
+ await ctx.artifactState?.setWorkspaceDispatchHold(workspace, true);
21287
21392
  }
21288
21393
  return {
21289
21394
  message: planned.issues.length > 0 ? `planned ${workspace}: created ${planned.spawned.length} nodes\uFF0C${planned.issues.length} \u4E2A staffing \u63D0\u793A\u5DF2\u5199\u5165\u5DE5\u5355` : `planned ${workspace}: created ${planned.spawned.length} nodes`,
@@ -21312,7 +21417,8 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
21312
21417
  ...optStr(args, "description") !== void 0 ? { description: optStr(args, "description") } : {},
21313
21418
  ...optStr(args, "docType") !== void 0 ? { docType: optStr(args, "docType") } : {},
21314
21419
  ...inputs !== void 0 ? { inputs } : {},
21315
- ...parts !== void 0 ? { parts } : {}
21420
+ ...parts !== void 0 ? { parts } : {},
21421
+ ...rawFields !== void 0 ? { fields: rawFields } : {}
21316
21422
  });
21317
21423
  return { message: `spawned ${artifact.id}\uFF08owner=${artifact.owner}${artifact.parts?.length ? `\uFF0Cparts=${artifact.parts.map((p2) => p2.name).join("/")}` : ""}\uFF09`, data: artifact };
21318
21424
  }
@@ -21836,17 +21942,9 @@ async function startOasisServer(opts) {
21836
21942
  ...opts.planWorkorderWithAgent ? { planWorkorderWithAgent: opts.planWorkorderWithAgent } : {},
21837
21943
  ...opts.resolveCodeRepo ? { resolveCodeRepo: opts.resolveCodeRepo } : {},
21838
21944
  ...opts.dispatchProduce ? { dispatchProduce: opts.dispatchProduce } : {},
21945
+ ...opts.workorderDrafts ? { workorderDrafts: opts.workorderDrafts } : {},
21839
21946
  ...opts.gitEnv ? { gitEnv: opts.gitEnv } : {}
21840
21947
  });
21841
- if (body.command === "spawn" && opts.chatSession) {
21842
- const workspace = body.args?.["workspace"];
21843
- if (workspace && workspace !== "ws:default") {
21844
- const chatSid = req.headers["x-chat-session-id"];
21845
- if (typeof chatSid === "string") {
21846
- await opts.chatSession.linkWorkOrder(chatSid, workspace).catch(() => void 0);
21847
- }
21848
- }
21849
- }
21850
21948
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
21851
21949
  } catch (err) {
21852
21950
  const status = err instanceof FrozenError ? 409 : 400;
@@ -21883,6 +21981,40 @@ async function startOasisServer(opts) {
21883
21981
  }
21884
21982
  return;
21885
21983
  }
21984
+ if (url.pathname === "/api/workorder/draft" && req.method === "GET" && opts.workorderDrafts) {
21985
+ const workspace = url.searchParams.get("workspace");
21986
+ const draft = workspace ? opts.workorderDrafts.forWorkspace(workspace) : void 0;
21987
+ if (!draft) {
21988
+ res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "no staged workorder draft" }));
21989
+ return;
21990
+ }
21991
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ data: draft }));
21992
+ return;
21993
+ }
21994
+ if (url.pathname === "/api/workorder/apply" && req.method === "POST") {
21995
+ try {
21996
+ const body = JSON.parse(rawBody);
21997
+ const result = await engine.kernel.applyIntervention(body.plan, body.baseSeqs, actor);
21998
+ const brief = engine.kernel.model.artifacts.get(body.briefArtifactId);
21999
+ if (brief && brief.description && brief.description.trim()) {
22000
+ const briefRef = await engine.blobs.put(new TextEncoder().encode(brief.description));
22001
+ await engine.kernel.proposeRevision({ artifactId: body.briefArtifactId, actor, contentRef: briefRef, reason: "\u5EFA\u5355\uFF1A\u843D\u5B9A\u53D1\u8D77\u4EBA\u6572\u5B9A\u7684 brief \u9996\u7248" });
22002
+ await engine.kernel.advanceQueue(body.briefArtifactId, actor);
22003
+ await engine.kernel.conclude({ artifactId: body.briefArtifactId, actor, note: "\u5EFA\u5355\u65F6\u7531\u53D1\u8D77\u4EBA\u62CD\u677F\u5B9A\u7A3F" });
22004
+ }
22005
+ if (body.dispatch === false && brief?.workspace) {
22006
+ await opts.artifactState?.setWorkspaceDispatchHold(brief.workspace, true);
22007
+ }
22008
+ res.writeHead(200, { "content-type": "application/json" }).end(
22009
+ JSON.stringify({ ...result, briefArtifactId: body.briefArtifactId, workspace: brief?.workspace ?? null })
22010
+ );
22011
+ } catch (err) {
22012
+ res.writeHead(409, { "content-type": "application/json" }).end(
22013
+ JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
22014
+ );
22015
+ }
22016
+ return;
22017
+ }
21886
22018
  if (url.pathname === "/api/intervention/draft" && req.method === "POST" && opts.drafts) {
21887
22019
  try {
21888
22020
  const plan = JSON.parse(rawBody);
@@ -22129,8 +22261,7 @@ async function startOasisServer(opts) {
22129
22261
  const session = await opts.dispatchChat({
22130
22262
  actorId: body.actorId,
22131
22263
  message: body.message,
22132
- ...body.sessionId ? { sessionId: body.sessionId } : {},
22133
- ...body.chatSessionId ? { chatSessionId: body.chatSessionId } : {}
22264
+ ...body.sessionId ? { sessionId: body.sessionId } : {}
22134
22265
  });
22135
22266
  let finished = false;
22136
22267
  res.on("close", () => {
@@ -22216,6 +22347,14 @@ async function startOasisServer(opts) {
22216
22347
  });
22217
22348
  writeFileSync14(join16(dir, "SKILLS.md"), ["# \u53EF\u7528\u6280\u80FD", "", "\u4EE5\u4E0B\u6280\u80FD\u5DF2\u4E3A\u4F60\u542F\u7528\uFF0C\u53EF\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u76F4\u63A5\u4F7F\u7528\uFF1A", "", ...lines].join("\n"));
22218
22349
  }
22350
+ if (opts.materializeSkills) {
22351
+ const skillFiles = await opts.materializeSkills(body.actorId, "claude").catch(() => ({}));
22352
+ for (const [rel, content] of Object.entries(skillFiles)) {
22353
+ const file = join16(dir, rel);
22354
+ mkdirSync13(dirname13(file), { recursive: true });
22355
+ writeFileSync14(file, content);
22356
+ }
22357
+ }
22219
22358
  const activeConnectors = actorCtx.connectors.filter(
22220
22359
  (c) => c.status === "connected" || c.status === "verifying"
22221
22360
  );
@@ -22231,8 +22370,8 @@ async function startOasisServer(opts) {
22231
22370
  }
22232
22371
  }
22233
22372
  const { spawn: spawn6 } = await import("node:child_process");
22234
- const { randomUUID: randomUUID15 } = await import("node:crypto");
22235
- const sessionId = body.sessionId ?? randomUUID15();
22373
+ const { randomUUID: randomUUID14 } = await import("node:crypto");
22374
+ const sessionId = body.sessionId ?? randomUUID14();
22236
22375
  const args = [
22237
22376
  "-p",
22238
22377
  body.message,
@@ -22243,7 +22382,7 @@ async function startOasisServer(opts) {
22243
22382
  ];
22244
22383
  const child = spawn6(opts.claudeBin ?? "claude", args, {
22245
22384
  ...spawnCwd ? { cwd: spawnCwd } : {},
22246
- env: { ...process.env, ...env, ...body.chatSessionId ? { OASIS_CHAT_SESSION_ID: body.chatSessionId } : {} },
22385
+ env: { ...process.env, ...env },
22247
22386
  stdio: ["ignore", "pipe", "pipe"]
22248
22387
  });
22249
22388
  if (typedParts) {
@@ -22535,8 +22674,8 @@ var init_server3 = __esm({
22535
22674
  init_mcp2();
22536
22675
  init_router();
22537
22676
  init_drafts();
22677
+ init_workorder_drafts();
22538
22678
  init_planner();
22539
- init_workorders();
22540
22679
  defaultResolveActor = (token) => token.startsWith("token:") ? token.slice("token:".length) : null;
22541
22680
  enc = (s2) => new TextEncoder().encode(s2);
22542
22681
  CHAT_OUTPUT_LIMIT = 16e3;
@@ -22614,11 +22753,11 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
22614
22753
  return m2;
22615
22754
  };
22616
22755
  return {
22617
- issue(nodeId, ttlMs = defaultTtlMs) {
22756
+ issue(name, ttlMs = defaultTtlMs) {
22618
22757
  const m2 = prune();
22619
22758
  const token = `ent_${(0, import_node_crypto3.randomBytes)(24).toString("base64url")}`;
22620
22759
  const expiresAt = Date.now() + ttlMs;
22621
- m2.set(token, { nodeId, expiresAt });
22760
+ m2.set(token, { name, expiresAt });
22622
22761
  write(m2);
22623
22762
  return { token, expiresAt: new Date(expiresAt).toISOString() };
22624
22763
  },
@@ -22626,7 +22765,7 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
22626
22765
  const m2 = prune();
22627
22766
  const entry = m2.get(token);
22628
22767
  if (!entry) return null;
22629
- return { nodeId: entry.nodeId };
22768
+ return { name: entry.name, ...entry.issuedNodeId ? { issuedNodeId: entry.issuedNodeId } : {} };
22630
22769
  },
22631
22770
  consume(token) {
22632
22771
  const m2 = prune();
@@ -22634,7 +22773,15 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
22634
22773
  if (!entry) return null;
22635
22774
  m2.delete(token);
22636
22775
  write(m2);
22637
- return { nodeId: entry.nodeId };
22776
+ return { name: entry.name, ...entry.issuedNodeId ? { issuedNodeId: entry.issuedNodeId } : {} };
22777
+ },
22778
+ setIssuedNodeId(token, nodeId) {
22779
+ const m2 = read();
22780
+ const entry = m2.get(token);
22781
+ if (entry) {
22782
+ entry.issuedNodeId = nodeId;
22783
+ write(m2);
22784
+ }
22638
22785
  }
22639
22786
  };
22640
22787
  }
@@ -23746,6 +23893,147 @@ var init_projection = __esm({
23746
23893
  }
23747
23894
  });
23748
23895
 
23896
+ // ../server/src/domains/collab/node-state.ts
23897
+ function nodeLabel(a) {
23898
+ if (a.title) return a.title;
23899
+ if (a.description) return a.description;
23900
+ const tail = a.id.split(":").slice(2).join(":");
23901
+ return tail ? `\u672A\u547D\u540D ${a.type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${a.type}`;
23902
+ }
23903
+ function nodeInfo(model, a) {
23904
+ const lifecycle = lifecycleOf(model, a.id);
23905
+ const sealed = lifecycle !== "active";
23906
+ const concluded = isConcluded(model, a.id);
23907
+ const mark = sealed ? lifecycle : pendingConcludeAttempt(model, a.id) ? "gate-pending" : blockedOf(model, a.id).blocked ? "blocked" : concluded ? "concluded" : a.currentRev ? "active" : "embryo";
23908
+ const hasWorking = revisionsOf(model, a.id).some((r) => r.state === "working");
23909
+ return { mark, queueLen: queueOf(model, a.id).length, concluded, sealed, hasWorking };
23910
+ }
23911
+ function deriveStage(members) {
23912
+ if (members.length > 0 && members.every((m2) => m2.sealed)) return "sealed";
23913
+ const live = members.filter((m2) => !m2.sealed);
23914
+ if (live.length > 0 && live.every((m2) => m2.concluded)) return "concluded";
23915
+ const reviewing = live.some((m2) => m2.mark === "gate-pending" || m2.mark === "active" && m2.queueLen > 0);
23916
+ if (reviewing) return "review";
23917
+ const progressing = live.some((m2) => m2.mark === "active" || m2.mark === "embryo");
23918
+ const blocked = live.some((m2) => m2.mark === "blocked");
23919
+ if (blocked && !progressing) return "blocked";
23920
+ return "executing";
23921
+ }
23922
+ function deriveNodeStage(info) {
23923
+ if (info.sealed) return "sealed";
23924
+ switch (info.mark) {
23925
+ case "concluded":
23926
+ return "concluded";
23927
+ case "gate-pending":
23928
+ return "gate";
23929
+ case "blocked":
23930
+ return "blocked";
23931
+ case "embryo":
23932
+ return "embryo";
23933
+ default:
23934
+ if (info.hasWorking) return "editing";
23935
+ if (info.queueLen > 0) return "review";
23936
+ return "merged";
23937
+ }
23938
+ }
23939
+ var init_node_state = __esm({
23940
+ "../server/src/domains/collab/node-state.ts"() {
23941
+ "use strict";
23942
+ init_src2();
23943
+ }
23944
+ });
23945
+
23946
+ // ../server/src/domains/collab/workorders.ts
23947
+ function buildWorkorderSummaries(model, resolveActor, resolveProject, dispatchPausedOf) {
23948
+ const byWorkspace = /* @__PURE__ */ new Map();
23949
+ for (const a of model.artifacts.values()) {
23950
+ const arr = byWorkspace.get(a.workspace) ?? [];
23951
+ arr.push(a);
23952
+ byWorkspace.set(a.workspace, arr);
23953
+ }
23954
+ const ref = (id) => {
23955
+ const extra = resolveActor?.(id);
23956
+ return {
23957
+ id,
23958
+ ...extra?.name ? { name: extra.name } : {},
23959
+ ...extra?.role ? { role: extra.role } : {},
23960
+ ...extra?.avatar ? { avatar: extra.avatar } : {}
23961
+ };
23962
+ };
23963
+ const out = [];
23964
+ for (const [workspace, arts] of byWorkspace) {
23965
+ const members = arts.map((a) => nodeInfo(model, a));
23966
+ const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
23967
+ const spec = workorderSpecOf(model, arts);
23968
+ const concluded = members.filter((m2) => m2.concluded).length;
23969
+ const lastOps = arts.map((a) => model.lastOpAt.get(a.id)).filter((t) => Boolean(t)).sort();
23970
+ const participantIds = [...new Set(arts.map((a) => a.owner))];
23971
+ const woTitle = seed.title ?? seed.description ?? workspace;
23972
+ out.push({
23973
+ id: workspace,
23974
+ title: woTitle,
23975
+ // 工单完整内容 = 根产物 description;仅当与标题不同(根产物有独立 title)时给出,避免与 title 重复
23976
+ ...seed.description && seed.description !== woTitle ? { description: seed.description } : {},
23977
+ // 一句话目标:优先取结构化 spec.goal;缺省时前端回退 description 首段(投影不替前端做回退,只给真值)
23978
+ ...spec?.goal ? { goal: spec.goal } : {},
23979
+ stage: deriveStage(members),
23980
+ owner: ref(seed.owner),
23981
+ participants: participantIds.map(ref),
23982
+ artifactCount: arts.length,
23983
+ progress: { concluded, total: arts.length },
23984
+ updatedAt: lastOps[lastOps.length - 1] ?? "",
23985
+ projectId: resolveProject?.(workspace) ?? null,
23986
+ planning: planningStatusOf(model, arts),
23987
+ ...dispatchPausedOf?.(workspace) ? { dispatchPaused: true } : {}
23988
+ });
23989
+ }
23990
+ return out.sort((a, b2) => a.updatedAt < b2.updatedAt ? 1 : a.updatedAt > b2.updatedAt ? -1 : 0);
23991
+ }
23992
+ function planningStatusOf(_model, arts) {
23993
+ const issues = [];
23994
+ const seenRoles = /* @__PURE__ */ new Set();
23995
+ for (const a of arts) {
23996
+ const m2 = /^pending:role:(.+)$/.exec(a.owner);
23997
+ if (!m2) continue;
23998
+ const role = m2[1];
23999
+ if (seenRoles.has(role)) continue;
24000
+ seenRoles.add(role);
24001
+ issues.push({ code: "missing_role", severity: "warning", role, message: `${role} \u89D2\u8272\u65E0 active \u5458\u5DE5\uFF0C\u76F8\u5173\u8282\u70B9\u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216 assign\u3002` });
24002
+ }
24003
+ return issues.length > 0 ? { status: "needs_staffing", issues } : { status: "ready", issues: [] };
24004
+ }
24005
+ function specFromFields(fields) {
24006
+ if (!fields) return null;
24007
+ const out = {};
24008
+ const goal = fields["goal"];
24009
+ if (typeof goal === "string" && goal.trim()) out.goal = goal.trim();
24010
+ const ac = fields["acceptanceCriteria"];
24011
+ if (Array.isArray(ac)) {
24012
+ const items = ac.filter((x2) => typeof x2 === "string" && x2.trim().length > 0).map((x2) => x2.trim());
24013
+ if (items.length > 0) out.acceptanceCriteria = items;
24014
+ }
24015
+ return out.goal !== void 0 || out.acceptanceCriteria !== void 0 ? out : null;
24016
+ }
24017
+ function workorderSpecOf(model, arts) {
24018
+ const seed = arts.find((a) => a.type === "brief") ?? arts.find((a) => a.inputs.length === 0);
24019
+ if (!seed) return null;
24020
+ const fromFields = specFromFields(seed.fields);
24021
+ if (fromFields) return fromFields;
24022
+ for (const annotation of model.annotations.values()) {
24023
+ if (annotation.anchor.target !== seed.id) continue;
24024
+ const spec = parseWorkorderSpec(annotation.body);
24025
+ if (spec) return spec;
24026
+ }
24027
+ return null;
24028
+ }
24029
+ var init_workorders = __esm({
24030
+ "../server/src/domains/collab/workorders.ts"() {
24031
+ "use strict";
24032
+ init_node_state();
24033
+ init_planner();
24034
+ }
24035
+ });
24036
+
23749
24037
  // ../server/src/domains/projects/service.ts
23750
24038
  function namedError(name, message) {
23751
24039
  const err = new Error(message);
@@ -24003,6 +24291,8 @@ var init_service = __esm({
24003
24291
  nodeOutputs = /* @__PURE__ */ new Map();
24004
24292
  // B5:工单→项目绑定(key=workOrderId)。与 node_outputs 同属合法薄旁存——reset 不清、重放时种回。
24005
24293
  workspaceBindings = /* @__PURE__ */ new Map();
24294
+ // #2 派单门:处于"待派发"的工单 id(薄旁存,同 binding——dispatcher/工单视图读它,不进 oplog)。
24295
+ dispatchHeld = /* @__PURE__ */ new Set();
24006
24296
  /**
24007
24297
  * B2 清空式重建用:清掉 oplog 派生的事实投影(artifact/revision/event/annotation),
24008
24298
  * 保留 node_output(合法薄投影,投影正依赖它补 source*;由 registerNodeOutput 独立维护)。
@@ -24070,6 +24360,13 @@ var init_service = __esm({
24070
24360
  async listWorkspaceBindings() {
24071
24361
  return [...this.workspaceBindings.values()].map((binding) => ({ ...binding }));
24072
24362
  }
24363
+ async setWorkspaceDispatchHold(workOrderId, held) {
24364
+ if (held) this.dispatchHeld.add(workOrderId);
24365
+ else this.dispatchHeld.delete(workOrderId);
24366
+ }
24367
+ async listWorkspaceDispatchHeld() {
24368
+ return [...this.dispatchHeld];
24369
+ }
24073
24370
  async listAll() {
24074
24371
  return {
24075
24372
  artifacts: [...this.artifacts.values()].map(cloneArtifact),
@@ -24077,7 +24374,8 @@ var init_service = __esm({
24077
24374
  events: [...this.events.values()].map(cloneEvent),
24078
24375
  annotations: [...this.annotations.values()].map(cloneAnnotation),
24079
24376
  nodeOutputs: [...this.nodeOutputs.values()].map(cloneNodeOutput),
24080
- workspaceBindings: [...this.workspaceBindings.values()].map((binding) => ({ ...binding }))
24377
+ workspaceBindings: [...this.workspaceBindings.values()].map((binding) => ({ ...binding })),
24378
+ workspaceDispatchHeld: [...this.dispatchHeld]
24081
24379
  };
24082
24380
  }
24083
24381
  };
@@ -25205,9 +25503,10 @@ var init_dev_store = __esm({
25205
25503
  for (const annotation of snap.annotations ?? []) await MemoryArtifactStateStore.prototype.upsertAnnotation.call(store, annotation);
25206
25504
  for (const output of snap.nodeOutputs ?? []) await MemoryArtifactStateStore.prototype.upsertNodeOutput.call(store, output);
25207
25505
  for (const binding of snap.workspaceBindings ?? []) await MemoryArtifactStateStore.prototype.upsertWorkspaceBinding.call(store, binding);
25506
+ for (const ws of snap.workspaceDispatchHeld ?? []) await MemoryArtifactStateStore.prototype.setWorkspaceDispatchHold.call(store, ws, true);
25208
25507
  } else {
25209
25508
  fs2.mkdirSync(path.dirname(file), { recursive: true });
25210
- fs2.writeFileSync(file, JSON.stringify({ artifacts: [], revisions: [], events: [], annotations: [], nodeOutputs: [], workspaceBindings: [] }, null, 2));
25509
+ fs2.writeFileSync(file, JSON.stringify({ artifacts: [], revisions: [], events: [], annotations: [], nodeOutputs: [], workspaceBindings: [], workspaceDispatchHeld: [] }, null, 2));
25211
25510
  }
25212
25511
  return store;
25213
25512
  }
@@ -25235,6 +25534,10 @@ var init_dev_store = __esm({
25235
25534
  await super.upsertWorkspaceBinding(binding);
25236
25535
  await this.save();
25237
25536
  }
25537
+ async setWorkspaceDispatchHold(workOrderId, held) {
25538
+ await super.setWorkspaceDispatchHold(workOrderId, held);
25539
+ await this.save();
25540
+ }
25238
25541
  async save() {
25239
25542
  fs2.writeFileSync(this.file, JSON.stringify(await this.listAll(), null, 2));
25240
25543
  }
@@ -25321,7 +25624,6 @@ var init_dev_store = __esm({
25321
25624
  MemoryChatSessionStore = class {
25322
25625
  sessions = /* @__PURE__ */ new Map();
25323
25626
  messages = /* @__PURE__ */ new Map();
25324
- sessionWorkOrders = /* @__PURE__ */ new Map();
25325
25627
  async createSession(s2) {
25326
25628
  this.sessions.set(s2.id, s2);
25327
25629
  }
@@ -25338,7 +25640,6 @@ var init_dev_store = __esm({
25338
25640
  async deleteSession(id) {
25339
25641
  this.sessions.delete(id);
25340
25642
  this.messages.delete(id);
25341
- this.sessionWorkOrders.delete(id);
25342
25643
  }
25343
25644
  async appendMessage(m2) {
25344
25645
  const list = this.messages.get(m2.sessionId) ?? [];
@@ -25349,14 +25650,6 @@ var init_dev_store = __esm({
25349
25650
  const list = (this.messages.get(sessionId) ?? []).sort((a, b2) => a.seq - b2.seq);
25350
25651
  return limit ? list.slice(-limit) : list;
25351
25652
  }
25352
- async linkWorkOrder(sessionId, workOrderId) {
25353
- const set = this.sessionWorkOrders.get(sessionId) ?? /* @__PURE__ */ new Set();
25354
- set.add(workOrderId);
25355
- this.sessionWorkOrders.set(sessionId, set);
25356
- }
25357
- async listWorkOrdersForSession(sessionId) {
25358
- return [...this.sessionWorkOrders.get(sessionId) ?? []];
25359
- }
25360
25653
  };
25361
25654
  }
25362
25655
  });
@@ -25474,12 +25767,36 @@ function typeDefError(def, mode) {
25474
25767
  }
25475
25768
  return null;
25476
25769
  }
25477
- var TYPE_DEF_SCHEMA, TypesAdminService;
25770
+ var RESERVED_FIELD_NAMES, TYPE_DEF_SCHEMA, TypesAdminService;
25478
25771
  var init_types_admin = __esm({
25479
25772
  "../server/src/types-admin.ts"() {
25480
25773
  "use strict";
25481
25774
  init_zod();
25482
25775
  init_src2();
25776
+ RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
25777
+ // Artifact 内置字段
25778
+ "id",
25779
+ "type",
25780
+ "title",
25781
+ "description",
25782
+ "docType",
25783
+ "currentRev",
25784
+ "owner",
25785
+ "workspace",
25786
+ "persistence",
25787
+ "createdVia",
25788
+ "inputs",
25789
+ "parts",
25790
+ "fields",
25791
+ // edit/spawn 自己的 flag + 全局 infra flag(--<字段> 转发要避开)
25792
+ "brief",
25793
+ "input",
25794
+ "set-json",
25795
+ "clear",
25796
+ "server",
25797
+ "token",
25798
+ "dir"
25799
+ ]);
25483
25800
  TYPE_DEF_SCHEMA = external_exports.object({
25484
25801
  name: external_exports.string().regex(/^[a-z][a-z0-9-]*$/, "name \u987B\u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\u3001\u9650\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/\u8FDE\u5B57\u7B26"),
25485
25802
  description: external_exports.string(),
@@ -25505,8 +25822,16 @@ var init_types_admin = __esm({
25505
25822
  budget: external_exports.object({ wallClockMinutes: external_exports.number().positive().optional() }).strict(),
25506
25823
  cascade: external_exports.enum(["auto", "manual"]),
25507
25824
  acl: external_exports.object({ propose: external_exports.array(external_exports.string()).optional(), annotate: external_exports.array(external_exports.string()).optional(), spawn: external_exports.array(external_exports.string()).optional() }).strict(),
25508
- mirror: external_exports.record(external_exports.string(), external_exports.unknown())
25825
+ mirror: external_exports.record(external_exports.string(), external_exports.unknown()),
25509
25826
  // MirrorConfig 是连接器侧复杂类型——只要求是对象、内部松校验
25827
+ fieldDefs: external_exports.array(
25828
+ external_exports.object({
25829
+ name: external_exports.string().regex(/^[a-z][a-zA-Z0-9_]*$/, "\u5B57\u6BB5 name \u987B\u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\u3001\u9650\u5B57\u6BCD/\u6570\u5B57/\u4E0B\u5212\u7EBF").refine((n) => !RESERVED_FIELD_NAMES.has(n), "\u5B57\u6BB5 name \u4E0D\u5F97\u649E Artifact \u5185\u7F6E\u5B57\u6BB5\u6216\u6838\u5FC3\u547D\u4EE4 flag\uFF08\u649E\u4E86\u6539\u7528 --set-json\uFF09"),
25830
+ type: external_exports.enum(["string", "string[]", "number", "boolean"]),
25831
+ required: external_exports.boolean().optional(),
25832
+ description: external_exports.string().optional()
25833
+ }).strict()
25834
+ )
25510
25835
  }).partial().strict();
25511
25836
  TypesAdminService = class {
25512
25837
  constructor(store, kernels) {
@@ -25576,6 +25901,36 @@ var init_types_admin = __esm({
25576
25901
  for (const k2 of this.kernels()) k2.removeType(name);
25577
25902
  return { ok: true };
25578
25903
  }
25904
+ // —— B 平面:自定义字段声明(fieldDefs)的增/改/删。都落到 edit(复用 zod 校验 + 传播 + usage 影响半径)。
25905
+ // 改名禁、删=deprecate(存量值留、新写入拒)都由"只有这三个动词、且 name 钉死"在结构上保证。
25906
+ /** 加一条字段声明。name 已声明 / 撞保留集(edit 的 zod 把住)则拒。 */
25907
+ async addField(typeName, spec) {
25908
+ const cur = (await this.store.list()).find((t) => t.name === typeName);
25909
+ if (!cur) return { ok: false, error: `\u7C7B\u578B\u4E0D\u5B58\u5728: ${typeName}` };
25910
+ if ((cur.fieldDefs ?? []).some((f2) => f2.name === spec.name)) {
25911
+ return { ok: false, error: `\u5B57\u6BB5\u5DF2\u58F0\u660E: ${spec.name}\uFF08\u6539\u5C5E\u6027\u7528 edit-field\u3001\u5220\u7528 drop-field\uFF09` };
25912
+ }
25913
+ return this.edit(typeName, { fieldDefs: [...cur.fieldDefs ?? [], spec] });
25914
+ }
25915
+ /** 改一条字段声明的属性(type/required/description);**name 钉死、不可改名**。 */
25916
+ async editField(typeName, fieldName, attrs) {
25917
+ const cur = (await this.store.list()).find((t) => t.name === typeName);
25918
+ if (!cur) return { ok: false, error: `\u7C7B\u578B\u4E0D\u5B58\u5728: ${typeName}` };
25919
+ const idx = (cur.fieldDefs ?? []).findIndex((f2) => f2.name === fieldName);
25920
+ if (idx < 0) return { ok: false, error: `\u5B57\u6BB5\u672A\u58F0\u660E: ${fieldName}\uFF08${typeName} \u58F0\u660E\uFF1A${(cur.fieldDefs ?? []).map((f2) => f2.name).join(", ") || "\u65E0"}\uFF09` };
25921
+ const nextDefs = [...cur.fieldDefs];
25922
+ nextDefs[idx] = { ...nextDefs[idx], ...attrs, name: fieldName };
25923
+ return this.edit(typeName, { fieldDefs: nextDefs });
25924
+ }
25925
+ /** 删一条字段声明(deprecate 语义:存量值留、新写入拒)。返回 usage 供影响半径预览。 */
25926
+ async dropField(typeName, fieldName) {
25927
+ const cur = (await this.store.list()).find((t) => t.name === typeName);
25928
+ if (!cur) return { ok: false, error: `\u7C7B\u578B\u4E0D\u5B58\u5728: ${typeName}` };
25929
+ if (!(cur.fieldDefs ?? []).some((f2) => f2.name === fieldName)) {
25930
+ return { ok: false, error: `\u5B57\u6BB5\u672A\u58F0\u660E: ${fieldName}` };
25931
+ }
25932
+ return this.edit(typeName, { fieldDefs: (cur.fieldDefs ?? []).filter((f2) => f2.name !== fieldName) });
25933
+ }
25579
25934
  };
25580
25935
  }
25581
25936
  });
@@ -30047,6 +30402,10 @@ ${input.description}
30047
30402
  if (enabledKeys.size === 0) return [];
30048
30403
  return all.filter((s2) => enabledKeys.has(s2.id) || enabledKeys.has(s2.name));
30049
30404
  }
30405
+ /** 全局已装技能平铺列表(「默认挂」materialize 用;installedSkillsWithAssignments 带分配关系太重)。 */
30406
+ async listInstalledSkills() {
30407
+ return this.opts.store.listInstalledSkills();
30408
+ }
30050
30409
  /* ---------- 变量(2.8):AES-256-GCM,读侧只见掩码 ---------- */
30051
30410
  /**
30052
30411
  * 写入变量。
@@ -30565,6 +30924,61 @@ var init_routes = __esm({
30565
30924
  }
30566
30925
  });
30567
30926
 
30927
+ // ../server/src/domains/actors/skill-materializer.ts
30928
+ function skillsDirForRuntime(runtimeKind) {
30929
+ switch (runtimeKind) {
30930
+ case "claude":
30931
+ case "claude-code":
30932
+ return ".claude/skills";
30933
+ default:
30934
+ return ".agent_context/skills";
30935
+ }
30936
+ }
30937
+ function sanitizeSkillDir(name) {
30938
+ const s2 = name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
30939
+ return s2 || "skill";
30940
+ }
30941
+ async function materializeSkillFiles(args) {
30942
+ const own = await args.service.listInstalledSkillsForActor(args.actorId);
30943
+ let extras = [];
30944
+ if (args.extraSkillIds && args.extraSkillIds.length > 0) {
30945
+ const ownIds = new Set(own.map((s2) => s2.id));
30946
+ const wanted = new Set(args.extraSkillIds);
30947
+ extras = (await args.service.listInstalledSkills()).filter((s2) => wanted.has(s2.id) && !ownIds.has(s2.id));
30948
+ }
30949
+ const skills = [...own, ...extras];
30950
+ if (skills.length === 0) return {};
30951
+ const prefix = skillsDirForRuntime(args.runtimeKind);
30952
+ const out = {};
30953
+ const dec2 = new TextDecoder();
30954
+ const usedDirs = /* @__PURE__ */ new Set();
30955
+ for (const skill of skills) {
30956
+ const files = await args.service.listSkillFiles(skill.id);
30957
+ if (files.length === 0) continue;
30958
+ let dir = sanitizeSkillDir(skill.name);
30959
+ if (usedDirs.has(dir)) {
30960
+ let n = 2;
30961
+ while (usedDirs.has(`${dir}-${n}`)) n++;
30962
+ dir = `${dir}-${n}`;
30963
+ }
30964
+ usedDirs.add(dir);
30965
+ for (const f2 of files) {
30966
+ try {
30967
+ const bytes = await args.blobs.get(f2.blobHash);
30968
+ out[`${prefix}/${dir}/${f2.path}`] = dec2.decode(bytes);
30969
+ } catch (err) {
30970
+ console.warn(`[skill-materializer] skip ${skill.id}/${f2.path} (blob ${f2.blobHash.slice(0, 12)}\u2026): ${String(err)}`);
30971
+ }
30972
+ }
30973
+ }
30974
+ return out;
30975
+ }
30976
+ var init_skill_materializer = __esm({
30977
+ "../server/src/domains/actors/skill-materializer.ts"() {
30978
+ "use strict";
30979
+ }
30980
+ });
30981
+
30568
30982
  // ../server/src/domains/actors/index.ts
30569
30983
  async function syncRegistryRolesToKernel(store, kernel) {
30570
30984
  for (const actor of await store.listActors()) {
@@ -30622,6 +31036,7 @@ var init_actors = __esm({
30622
31036
  init_service2();
30623
31037
  init_routes();
30624
31038
  init_stats();
31039
+ init_skill_materializer();
30625
31040
  }
30626
31041
  });
30627
31042
 
@@ -32111,14 +32526,20 @@ OASIS_DIR="$(pwd)"
32111
32526
  NODE_DIR="$OASIS_DIR/${dir}"
32112
32527
  NODE_CMD="$OASIS_DIR/node_modules/.bin/tsx $OASIS_DIR/packages/cli/src/main.ts"
32113
32528
 
32114
- # \u5199\u5165\u8282\u70B9\u51ED\u8BC1\uFF1A600 \u6743\u9650\u3001\u72EC\u7ACB\u6587\u4EF6\uFF0C\u4E0D\u653E\u8FDB\u547D\u4EE4\u884C/ExecStart\uFF08\u9632\u6CC4\u6F0F\u5230 ps/journald\uFF09\u3002
32115
- # CLI \u7684 oasis node \u4F1A\u4ECE <dir>/node-token \u81EA\u52A8\u8BFB\u53D6\u3002
32529
+ # \u5199\u5165\u4E00\u6B21\u6027\u63A5\u5165\u4EE4\u724C\uFF1A600 \u6743\u9650\u3001\u72EC\u7ACB\u6587\u4EF6\uFF0C\u4E0D\u653E\u8FDB\u547D\u4EE4\u884C/ExecStart\uFF08\u9632\u6CC4\u6F0F\u5230 ps/journald\uFF09\u3002
32530
+ # CLI \u7684 oasis node \u9996\u6B21\u542F\u52A8\u4F1A\u7528\u5B83\u5411\u670D\u52A1\u7AEF exchange \u53D6\u56DE nodeId + \u957F\u671F node-token\uFF0C
32531
+ # \u4E4B\u540E\u843D\u76D8\u5230 <dir>/node-id \u4E0E <dir>/node-token\uFF0C\u91CD\u542F\u4E0D\u518D exchange\u3002
32116
32532
  mkdir -p "$NODE_DIR"
32117
- ( umask 077; cat > "$NODE_DIR/node-token" <<'OASIS_NODE_TOKEN_EOF'
32533
+ ( umask 077; cat > "$NODE_DIR/enroll-token" <<'OASIS_ENROLL_TOKEN_EOF'
32118
32534
  ${p2.token}
32119
- OASIS_NODE_TOKEN_EOF
32535
+ OASIS_ENROLL_TOKEN_EOF
32120
32536
  )
32121
32537
 
32538
+ # "Script was run" marker: tells the daemon to (re)report its available runtimes once on
32539
+ # next start. Adapters are picked up only on an explicit script run \u2014 never on plain
32540
+ # reconnects/restarts \u2014 so admin-side runtime deletions stay deleted until you re-run this.
32541
+ touch "$NODE_DIR/report-runtimes"
32542
+
32122
32543
  # \u7528 systemd \u7528\u6237\u670D\u52A1\uFF08Linux\uFF09\u4FDD\u6301\u540E\u53F0\u5E38\u9A7B + \u5F00\u673A\u81EA\u542F
32123
32544
  if command -v systemctl &>/dev/null && systemctl --user daemon-reload &>/dev/null 2>&1; then
32124
32545
  SERVICE_FILE=~/.config/systemd/user/oasis-node.service
@@ -32133,11 +32554,12 @@ if command -v systemctl &>/dev/null && systemctl --user daemon-reload &>/dev/nul
32133
32554
  echo '\u2192 \u5199\u5165 systemd \u7528\u6237\u670D\u52A1\u914D\u7F6E...'
32134
32555
  cat > "$SERVICE_FILE" << EOF
32135
32556
  [Unit]
32136
- Description=Oasis Node Daemon (${p2.nodeId})
32557
+ Description=Oasis Node Daemon
32137
32558
  After=network.target
32138
32559
 
32139
32560
  [Service]
32140
- ExecStart=$NODE_CMD node --server-ws ${p2.gatewayUrl} --id ${p2.nodeId} --dir $NODE_DIR
32561
+ ExecStart=$NODE_CMD node --server-ws ${p2.gatewayUrl} --dir $NODE_DIR
32562
+ Environment=PATH=$PATH:$HOME/.npm-global/bin:$HOME/.local/bin:/usr/local/bin
32141
32563
  WorkingDirectory=$OASIS_DIR
32142
32564
  Restart=on-failure
32143
32565
  RestartSec=5
@@ -32176,7 +32598,6 @@ elif [[ "$(uname)" == "Darwin" ]]; then
32176
32598
  <string>$MAIN</string>
32177
32599
  <string>node</string>
32178
32600
  <string>--server-ws</string><string>${p2.gatewayUrl}</string>
32179
- <string>--id</string><string>${p2.nodeId}</string>
32180
32601
  <string>--dir</string><string>$NODE_DIR</string>
32181
32602
  </array>
32182
32603
  <key>WorkingDirectory</key><string>$OASIS_DIR</string>
@@ -32204,7 +32625,7 @@ else
32204
32625
  sleep 1
32205
32626
  fi
32206
32627
  echo '\u2192 \u542F\u52A8\u8282\u70B9\uFF08nohup \u540E\u53F0\uFF09...'
32207
- nohup $NODE_CMD node --server-ws ${p2.gatewayUrl} --id ${p2.nodeId} --dir $NODE_DIR > ~/.oasis-node.log 2>&1 &
32628
+ nohup $NODE_CMD node --server-ws ${p2.gatewayUrl} --dir $NODE_DIR > ~/.oasis-node.log 2>&1 &
32208
32629
  echo "\u2713 \u8282\u70B9\u5DF2\u5728\u540E\u53F0\u542F\u52A8\uFF08PID $!\uFF09"
32209
32630
  echo ' \u65E5\u5FD7\uFF1Atail -f ~/.oasis-node.log'
32210
32631
  echo ' \u505C\u6B62\uFF1Apkill -f "oasis node"'
@@ -32239,9 +32660,10 @@ if (-not (Test-Path "node_modules")) {
32239
32660
  Write-Host "\u2192 Dependencies already present, skipping."
32240
32661
  }
32241
32662
 
32242
- # \u2500\u2500 write token (ACL-restricted) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32663
+ # \u2500\u2500 write enroll token (ACL-restricted) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32664
+ # \u4E00\u6B21\u6027\u4EE4\u724C\uFF1Boasis node \u9996\u6B21\u542F\u52A8\u7528\u5B83 exchange \u53D6\u56DE nodeId + \u957F\u671F node-token\uFF0C\u4E4B\u540E\u843D\u76D8\u590D\u7528\u3002
32243
32665
  New-Item -ItemType Directory -Force -Path $NODE_DIR | Out-Null
32244
- $tokenPath = "$NODE_DIR\\node-token"
32666
+ $tokenPath = "$NODE_DIR\\enroll-token"
32245
32667
  Set-Content -Path $tokenPath -Value "${p2.token}" -NoNewline -Encoding utf8
32246
32668
  $acl = Get-Acl $tokenPath
32247
32669
  $acl.SetAccessRuleProtection($true, $false)
@@ -32250,10 +32672,15 @@ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
32250
32672
  $acl.AddAccessRule($rule)
32251
32673
  Set-Acl $tokenPath $acl
32252
32674
 
32675
+ # "Script was run" marker: tells the daemon to (re)report its available runtimes once on
32676
+ # next start. Adapters are picked up only on an explicit script run \u2014 never on plain
32677
+ # reconnects/restarts \u2014 so admin-side runtime deletions stay deleted until you re-run this.
32678
+ Set-Content -Path "$NODE_DIR\\report-runtimes" -Value "" -NoNewline -Encoding utf8
32679
+
32253
32680
  # \u2500\u2500 register Task Scheduler (run at logon, restart on failure) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32254
32681
  $tsx = "$INSTALL_DIR\\node_modules\\.bin\\tsx.cmd"
32255
32682
  $main = "$INSTALL_DIR\\packages\\cli\\src\\main.ts"
32256
- $args = "\`"$main\`" node --server-ws ${p2.gatewayUrl} --id ${p2.nodeId} --dir \`"$NODE_DIR\`""
32683
+ $args = "\`"$main\`" node --server-ws ${p2.gatewayUrl} --dir \`"$NODE_DIR\`""
32257
32684
 
32258
32685
  $action = New-ScheduledTaskAction -Execute $tsx -Argument $args -WorkingDirectory $INSTALL_DIR
32259
32686
  $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
@@ -32261,23 +32688,23 @@ $settings = New-ScheduledTaskSettingsSet -RestartCount 5 \`
32261
32688
  -RestartInterval (New-TimeSpan -Minutes 1) \`
32262
32689
  -ExecutionTimeLimit ([TimeSpan]::Zero) \`
32263
32690
  -StartWhenAvailable $true
32264
- Register-ScheduledTask -TaskName "OasisNode-${p2.nodeId}" \`
32691
+ Register-ScheduledTask -TaskName "OasisNode" \`
32265
32692
  -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null
32266
32693
 
32267
32694
  # \u2500\u2500 start now \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32268
- Start-ScheduledTask -TaskName "OasisNode-${p2.nodeId}"
32695
+ Start-ScheduledTask -TaskName "OasisNode"
32269
32696
  Pop-Location
32270
32697
 
32271
32698
  Write-Host ""
32272
32699
  Write-Host "\u2713 Node started (Task Scheduler, runs at every logon)"
32273
- Write-Host " Status : Get-ScheduledTask -TaskName 'OasisNode-${p2.nodeId}'"
32274
- Write-Host " Stop : Stop-ScheduledTask -TaskName 'OasisNode-${p2.nodeId}'"
32275
- Write-Host " Remove : Unregister-ScheduledTask -TaskName 'OasisNode-${p2.nodeId}' -Confirm:$false"
32700
+ Write-Host " Status : Get-ScheduledTask -TaskName 'OasisNode'"
32701
+ Write-Host " Stop : Stop-ScheduledTask -TaskName 'OasisNode'"
32702
+ Write-Host " Remove : Unregister-ScheduledTask -TaskName 'OasisNode' -Confirm:$false"
32276
32703
  `;
32277
32704
  }
32278
32705
  function buildNpxCmd(p2) {
32279
32706
  const namePart = p2.nodeName ? ` --name ${p2.nodeName}` : "";
32280
- return `npx -y oasis_test@latest start --server-ws ${p2.gatewayUrl} --id ${p2.nodeId}${namePart} --enroll-token ${p2.token}`;
32707
+ return `npx -y oasis_test@latest start --server-ws ${p2.gatewayUrl}${namePart} --enroll-token ${p2.token}`;
32281
32708
  }
32282
32709
  var init_connect_script = __esm({
32283
32710
  "../server/src/domains/nodes/connect-script.ts"() {
@@ -32287,7 +32714,6 @@ var init_connect_script = __esm({
32287
32714
 
32288
32715
  // ../server/src/domains/nodes/routes.ts
32289
32716
  function nodesDomain(deps) {
32290
- const nodeNames = /* @__PURE__ */ new Map();
32291
32717
  let hubWired = false;
32292
32718
  function wireHub(hub) {
32293
32719
  if (hubWired) return;
@@ -32298,19 +32724,20 @@ function nodesDomain(deps) {
32298
32724
  if (msg.type !== "hello") return;
32299
32725
  const d = hub.connectedDaemons().find((c) => c.daemonId === daemonId);
32300
32726
  if (!d) return;
32301
- const existing = await nodeStore.getNode(daemonId);
32302
- await nodeStore.upsertNode({
32303
- id: daemonId,
32304
- hostname: d.meta.hostname,
32305
- nodeVersion: d.meta.nodeVersion ?? "unknown",
32306
- status: "online",
32307
- lastSeenAt: now(),
32308
- enrolledAt: existing?.enrolledAt ?? now(),
32309
- ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
32310
- });
32311
- if (d.meta.runtimes) {
32312
- for (const cap of d.meta.runtimes) {
32313
- await nodeStore.upsertRuntime({
32727
+ if (msg.meta.runtimes) {
32728
+ const existing = await nodeStore.getNode(daemonId);
32729
+ await nodeStore.upsertNode({
32730
+ id: daemonId,
32731
+ hostname: d.meta.hostname,
32732
+ nodeVersion: d.meta.nodeVersion ?? "unknown",
32733
+ status: "online",
32734
+ // revives a previously soft-deleted node
32735
+ lastSeenAt: now(),
32736
+ enrolledAt: existing?.enrolledAt ?? now(),
32737
+ ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
32738
+ });
32739
+ for (const cap of msg.meta.runtimes) {
32740
+ await nodeStore.addRuntimeIfAbsent({
32314
32741
  id: `runtime:${daemonId}:${cap.kind}`,
32315
32742
  nodeId: daemonId,
32316
32743
  kind: cap.kind,
@@ -32321,7 +32748,9 @@ function nodesDomain(deps) {
32321
32748
  lastSeenAt: now()
32322
32749
  });
32323
32750
  }
32751
+ await nodeStore.setNodeRuntimesOnline(daemonId, true);
32324
32752
  } else {
32753
+ await nodeStore.setNodeOnline(daemonId, true);
32325
32754
  await nodeStore.setNodeRuntimesOnline(daemonId, true);
32326
32755
  }
32327
32756
  });
@@ -32331,35 +32760,24 @@ function nodesDomain(deps) {
32331
32760
  };
32332
32761
  }
32333
32762
  return (router) => {
32763
+ let wirePoll = null;
32764
+ const tryWire = () => {
32765
+ const hub = deps.getHub();
32766
+ if (hub) {
32767
+ wireHub(hub);
32768
+ if (wirePoll) {
32769
+ clearInterval(wirePoll);
32770
+ wirePoll = null;
32771
+ }
32772
+ }
32773
+ };
32774
+ tryWire();
32775
+ if (!hubWired) wirePoll = setInterval(tryWire, 100);
32334
32776
  async function syncConnectedDaemon(daemonId, hub) {
32335
32777
  const d = hub.connectedDaemons().find((c) => c.daemonId === daemonId);
32336
32778
  if (!d) return;
32337
- const existing = await deps.nodeStore.getNode(daemonId);
32338
- await deps.nodeStore.upsertNode({
32339
- id: daemonId,
32340
- hostname: d.meta.hostname,
32341
- nodeVersion: d.meta.nodeVersion ?? "unknown",
32342
- status: "online",
32343
- lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
32344
- enrolledAt: existing?.enrolledAt ?? (/* @__PURE__ */ new Date()).toISOString(),
32345
- ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
32346
- });
32347
- if (d.meta.runtimes) {
32348
- for (const cap of d.meta.runtimes) {
32349
- await deps.nodeStore.upsertRuntime({
32350
- id: `runtime:${daemonId}:${cap.kind}`,
32351
- nodeId: daemonId,
32352
- kind: cap.kind,
32353
- hostname: d.meta.hostname,
32354
- ...cap.binary ? { binary: cap.binary } : {},
32355
- ...cap.version ? { version: cap.version } : {},
32356
- status: "online",
32357
- lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
32358
- });
32359
- }
32360
- } else {
32361
- await deps.nodeStore.setNodeRuntimesOnline(daemonId, true);
32362
- }
32779
+ await deps.nodeStore.setNodeOnline(daemonId, true);
32780
+ await deps.nodeStore.setNodeRuntimesOnline(daemonId, true);
32363
32781
  }
32364
32782
  router.get("/api/nodes", async () => {
32365
32783
  const hub = deps.getHub();
@@ -32414,16 +32832,11 @@ function nodesDomain(deps) {
32414
32832
  router.post("/api/nodes/enroll", async (req) => {
32415
32833
  const body = req.body ?? {};
32416
32834
  const nodeName = body.nodeName?.trim() || void 0;
32417
- const requestedId = body.nodeId?.trim();
32418
- const existing = requestedId ? await deps.nodeStore.getNode(requestedId) : null;
32419
- const nodeId = existing?.id ?? requestedId ?? `node-${(0, import_node_crypto9.randomUUID)().slice(0, 8)}`;
32420
- const enroll = deps.enrollTokens.issue(nodeId);
32421
- if (nodeName) nodeNames.set(nodeId, nodeName);
32422
- if (existing && nodeName) await deps.nodeStore.updateNodeName(nodeId, nodeName);
32423
- const params = { gatewayUrl: deps.gatewayUrl, nodeId, token: enroll.token, repoRemote: deps.repoRemote };
32835
+ const enroll = deps.enrollTokens.issue(nodeName ?? "");
32836
+ const params = { gatewayUrl: deps.gatewayUrl, token: enroll.token, repoRemote: deps.repoRemote };
32424
32837
  const script = body.platform === "windows" ? buildWindowsConnectScript(params) : buildConnectScript(params);
32425
- const npxCmd = buildNpxCmd({ gatewayUrl: deps.gatewayUrl, nodeId, token: enroll.token, nodeName });
32426
- return { status: 200, body: { nodeId, nodeName, token: enroll.token, script, gatewayUrl: deps.gatewayUrl, npxCmd } };
32838
+ const npxCmd = buildNpxCmd({ gatewayUrl: deps.gatewayUrl, token: enroll.token, nodeName });
32839
+ return { status: 200, body: { nodeName, token: enroll.token, script, gatewayUrl: deps.gatewayUrl, npxCmd } };
32427
32840
  });
32428
32841
  router.post("/api/nodes/exchange", async (req) => {
32429
32842
  const body = req.body ?? {};
@@ -32431,15 +32844,22 @@ function nodesDomain(deps) {
32431
32844
  if (!enrollToken) return { status: 400, body: { error: "missing enrollToken" } };
32432
32845
  const entry = deps.enrollTokens.peek(enrollToken);
32433
32846
  if (!entry) return { status: 401, body: { error: "invalid or expired enroll token" } };
32434
- const requestedNodeId = body.nodeId?.trim() || entry.nodeId;
32435
- const requestedExists = await deps.nodeStore.getNode(requestedNodeId);
32436
- if (requestedNodeId !== entry.nodeId && !requestedExists) {
32437
- return { status: 409, body: { error: "requested node does not exist" } };
32438
- }
32439
- const consumed = deps.enrollTokens.consume(enrollToken);
32440
- if (!consumed) return { status: 401, body: { error: "invalid or expired enroll token" } };
32441
- const nodeId = requestedNodeId;
32442
- await deps.nodeStore.clearDeletedRuntimesForNode(nodeId);
32847
+ const incomingNodeId = body.nodeId?.trim();
32848
+ const ownsIncoming = !!incomingNodeId && (deps.nodeTokens.list().some((e) => e.nodeId === incomingNodeId) || await deps.nodeStore.getNode(incomingNodeId) !== null);
32849
+ let nodeId;
32850
+ if (entry.issuedNodeId) {
32851
+ if (incomingNodeId && incomingNodeId !== entry.issuedNodeId && !ownsIncoming)
32852
+ return { status: 409, body: { error: "nodeId mismatch" } };
32853
+ nodeId = entry.issuedNodeId;
32854
+ } else if (ownsIncoming) {
32855
+ nodeId = incomingNodeId;
32856
+ deps.enrollTokens.setIssuedNodeId(enrollToken, nodeId);
32857
+ } else {
32858
+ nodeId = `node-${(0, import_node_crypto9.randomUUID)().slice(0, 8)}`;
32859
+ deps.enrollTokens.setIssuedNodeId(enrollToken, nodeId);
32860
+ }
32861
+ deps.enrollTokens.consume(enrollToken);
32862
+ if (entry.name) await deps.nodeStore.updateNodeName(nodeId, entry.name);
32443
32863
  const existing = deps.nodeTokens.list().find((e) => e.nodeId === nodeId);
32444
32864
  const token = existing?.token ?? deps.nodeTokens.issue(nodeId);
32445
32865
  return { status: 200, body: { nodeId, token } };
@@ -32505,6 +32925,24 @@ function typesDomain(deps) {
32505
32925
  const r = await deps.admin.delete(name);
32506
32926
  return r.ok ? { status: 200, body: { ok: true } } : { status: 400, body: { error: r.error, ...r.usage ? { usage: r.usage } : {} } };
32507
32927
  });
32928
+ router.post("/api/artifact-types/:name/fields", async (req) => {
32929
+ const name = req.params["name"];
32930
+ if (!name) return { status: 400, body: { error: "missing name" } };
32931
+ const r = await deps.admin.addField(name, req.body ?? {});
32932
+ return r.ok ? { status: 200, body: { ok: true, ...r.usage ? { usage: r.usage } : {} } } : { status: 400, body: { error: r.error } };
32933
+ });
32934
+ router.patch("/api/artifact-types/:name/fields/:field", async (req) => {
32935
+ const { name, field } = req.params;
32936
+ if (!name || !field) return { status: 400, body: { error: "missing name/field" } };
32937
+ const r = await deps.admin.editField(name, field, req.body ?? {});
32938
+ return r.ok ? { status: 200, body: { ok: true, ...r.usage ? { usage: r.usage } : {} } } : { status: 400, body: { error: r.error } };
32939
+ });
32940
+ router.delete("/api/artifact-types/:name/fields/:field", async (req) => {
32941
+ const { name, field } = req.params;
32942
+ if (!name || !field) return { status: 400, body: { error: "missing name/field" } };
32943
+ const r = await deps.admin.dropField(name, field);
32944
+ return r.ok ? { status: 200, body: { ok: true, ...r.usage ? { usage: r.usage } : {} } } : { status: 400, body: { error: r.error } };
32945
+ });
32508
32946
  };
32509
32947
  }
32510
32948
  var init_types3 = __esm({
@@ -32522,17 +32960,20 @@ var init_node_store = __esm({
32522
32960
  MemoryNodeStore = class {
32523
32961
  nodes = /* @__PURE__ */ new Map();
32524
32962
  runtimes = /* @__PURE__ */ new Map();
32525
- deletedRuntimeIds = /* @__PURE__ */ new Set();
32526
32963
  async upsertNode(node) {
32527
32964
  this.nodes.set(node.id, { ...this.nodes.get(node.id), ...node });
32528
32965
  }
32529
32966
  async upsertRuntime(rt) {
32530
- if (this.deletedRuntimeIds.has(rt.id)) return;
32531
32967
  this.runtimes.set(rt.id, { ...this.runtimes.get(rt.id), ...rt });
32532
32968
  }
32969
+ async addRuntimeIfAbsent(rt) {
32970
+ if (this.runtimes.has(rt.id)) return false;
32971
+ this.runtimes.set(rt.id, rt);
32972
+ return true;
32973
+ }
32533
32974
  async setNodeOnline(id, online) {
32534
32975
  const n = this.nodes.get(id);
32535
- if (n) {
32976
+ if (n && n.status !== "deleted") {
32536
32977
  n.status = online ? "online" : "offline";
32537
32978
  n.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
32538
32979
  }
@@ -32554,7 +32995,7 @@ var init_node_store = __esm({
32554
32995
  return this.nodes.get(id) ?? null;
32555
32996
  }
32556
32997
  async listNodes() {
32557
- return [...this.nodes.values()];
32998
+ return [...this.nodes.values()].filter((n) => n.status !== "deleted");
32558
32999
  }
32559
33000
  async listRuntimes(nodeId) {
32560
33001
  const all = [...this.runtimes.values()];
@@ -32565,23 +33006,24 @@ var init_node_store = __esm({
32565
33006
  for (const [k2, rt] of this.runtimes) {
32566
33007
  if (rt.nodeId === id) this.runtimes.delete(k2);
32567
33008
  }
32568
- for (const k2 of [...this.deletedRuntimeIds]) {
32569
- if (k2.startsWith(`runtime:${id}:`)) this.deletedRuntimeIds.delete(k2);
32570
- }
32571
33009
  }
32572
- async clearDeletedRuntimesForNode(nodeId) {
32573
- for (const id of [...this.deletedRuntimeIds]) {
32574
- if (id.startsWith(`runtime:${nodeId}:`)) this.deletedRuntimeIds.delete(id);
33010
+ async softDeleteNode(id) {
33011
+ const n = this.nodes.get(id);
33012
+ if (n) {
33013
+ n.status = "deleted";
33014
+ n.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
33015
+ }
33016
+ for (const [k2, rt] of this.runtimes) {
33017
+ if (rt.nodeId === id) this.runtimes.delete(k2);
32575
33018
  }
32576
33019
  }
32577
33020
  async deleteRuntime(id) {
32578
- this.deletedRuntimeIds.add(id);
32579
33021
  const rt = this.runtimes.get(id);
32580
33022
  this.runtimes.delete(id);
32581
33023
  if (!rt) return { nodeDeleted: false };
32582
33024
  const remaining = [...this.runtimes.values()].filter((r) => r.nodeId === rt.nodeId);
32583
33025
  if (remaining.length === 0) {
32584
- this.nodes.delete(rt.nodeId);
33026
+ await this.softDeleteNode(rt.nodeId);
32585
33027
  return { nodeDeleted: true };
32586
33028
  }
32587
33029
  return { nodeDeleted: false };
@@ -32598,7 +33040,6 @@ var init_node_store = __esm({
32598
33040
  const snap = JSON.parse(fs5.readFileSync(file, "utf8"));
32599
33041
  for (const n of snap.nodes ?? []) s2.nodes.set(n.id, n);
32600
33042
  for (const r of snap.runtimes ?? []) s2.runtimes.set(r.id, r);
32601
- for (const id of snap.deletedRuntimeIds ?? []) s2.deletedRuntimeIds.add(id);
32602
33043
  } catch {
32603
33044
  fs5.writeFileSync(file, JSON.stringify({ nodes: [], runtimes: [] }, null, 2));
32604
33045
  }
@@ -32607,8 +33048,7 @@ var init_node_store = __esm({
32607
33048
  flush() {
32608
33049
  fs5.writeFileSync(this.file, JSON.stringify({
32609
33050
  nodes: [...this.nodes.values()],
32610
- runtimes: [...this.runtimes.values()],
32611
- deletedRuntimeIds: [...this.deletedRuntimeIds]
33051
+ runtimes: [...this.runtimes.values()]
32612
33052
  }, null, 2));
32613
33053
  }
32614
33054
  async upsertNode(node) {
@@ -32619,6 +33059,11 @@ var init_node_store = __esm({
32619
33059
  await super.upsertRuntime(rt);
32620
33060
  this.flush();
32621
33061
  }
33062
+ async addRuntimeIfAbsent(rt) {
33063
+ const r = await super.addRuntimeIfAbsent(rt);
33064
+ if (r) this.flush();
33065
+ return r;
33066
+ }
32622
33067
  async setNodeOnline(id, online) {
32623
33068
  await super.setNodeOnline(id, online);
32624
33069
  this.flush();
@@ -32635,8 +33080,8 @@ var init_node_store = __esm({
32635
33080
  await super.deleteNode(id);
32636
33081
  this.flush();
32637
33082
  }
32638
- async clearDeletedRuntimesForNode(nodeId) {
32639
- await super.clearDeletedRuntimesForNode(nodeId);
33083
+ async softDeleteNode(id) {
33084
+ await super.softDeleteNode(id);
32640
33085
  this.flush();
32641
33086
  }
32642
33087
  async deleteRuntime(id) {
@@ -32649,10 +33094,10 @@ var init_node_store = __esm({
32649
33094
  });
32650
33095
 
32651
33096
  // ../server/src/domains/collab/workorder-detail.ts
32652
- function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject, dispatchJournal = []) {
33097
+ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject, dispatchJournal = [], dispatchPausedOf) {
32653
33098
  const arts = [...model.artifacts.values()].filter((a) => a.workspace === workspaceId);
32654
33099
  if (arts.length === 0) return null;
32655
- const summary = buildWorkorderSummaries(model, resolveActor, resolveProject).find((w2) => w2.id === workspaceId);
33100
+ const summary = buildWorkorderSummaries(model, resolveActor, resolveProject, dispatchPausedOf).find((w2) => w2.id === workspaceId);
32656
33101
  if (!summary) return null;
32657
33102
  const ref = (id) => {
32658
33103
  const extra = resolveActor?.(id);
@@ -32823,6 +33268,26 @@ var init_workorder_detail = __esm({
32823
33268
  // ../server/src/domains/collab/inbox.ts
32824
33269
  function buildInbox(model, me) {
32825
33270
  const out = [];
33271
+ const creatorOf = /* @__PURE__ */ new Map();
33272
+ for (const a of model.artifacts.values()) {
33273
+ if (a.type === "brief") creatorOf.set(a.workspace, a.owner);
33274
+ }
33275
+ const staffingSeen = /* @__PURE__ */ new Set();
33276
+ for (const a of model.artifacts.values()) {
33277
+ const pendingRole = /^pending:role:(.+)$/.exec(a.owner);
33278
+ if (!pendingRole || creatorOf.get(a.workspace) !== me) continue;
33279
+ const key = `${a.workspace}:${pendingRole[1]}`;
33280
+ if (staffingSeen.has(key)) continue;
33281
+ staffingSeen.add(key);
33282
+ out.push({
33283
+ kind: "needs-staffing",
33284
+ artifactId: a.id,
33285
+ workspace: a.workspace,
33286
+ since: model.lastOpAt.get(a.id) ?? "",
33287
+ actionRequired: true,
33288
+ summary: `\u5DE5\u5355\u7F3A\u4EBA\u624B\uFF1A${pendingRole[1]} \u89D2\u8272\u65E0\u5458\u5DE5\uFF0C\u300C${nodeLabel(a)}\u300D\u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216 assign`
33289
+ });
33290
+ }
32826
33291
  for (const a of model.artifacts.values()) {
32827
33292
  const ws = a.workspace;
32828
33293
  const label = nodeLabel(a);
@@ -33018,6 +33483,11 @@ async function buildProjectResolver(artifacts) {
33018
33483
  const byWorkOrder = new Map(bindings.map((binding) => [binding.workOrderId, binding.projectId]));
33019
33484
  return (workOrderId) => byWorkOrder.get(workOrderId) ?? null;
33020
33485
  }
33486
+ async function buildDispatchPausedResolver(artifacts) {
33487
+ if (!artifacts) return void 0;
33488
+ const held = new Set(await artifacts.listWorkspaceDispatchHeld());
33489
+ return (workspace) => held.has(workspace);
33490
+ }
33021
33491
  function collabDomain(opts) {
33022
33492
  const defaultCtx = {
33023
33493
  kernel: opts.kernel,
@@ -33044,28 +33514,30 @@ function collabDomain(opts) {
33044
33514
  const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
33045
33515
  const resolve3 = await buildResolver(registry2);
33046
33516
  const resolveProject = await buildProjectResolver(artifacts);
33047
- return { status: 200, body: { items: buildWorkorderSummaries(kernel.model, resolve3, resolveProject) } };
33517
+ const dispatchPausedOf = await buildDispatchPausedResolver(artifacts);
33518
+ return { status: 200, body: { items: buildWorkorderSummaries(kernel.model, resolve3, resolveProject, dispatchPausedOf) } };
33048
33519
  });
33049
33520
  router.get("/api/workorders/:id", async (req) => {
33050
33521
  const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
33051
33522
  const resolve3 = await buildResolver(registry2);
33052
33523
  const resolveProject = await buildProjectResolver(artifacts);
33053
- const detail = buildWorkorderDetail(kernel.model, req.params.id, resolve3, resolveProject, opts.dispatchJournal?.() ?? []);
33524
+ const dispatchPausedOf = await buildDispatchPausedResolver(artifacts);
33525
+ const detail = buildWorkorderDetail(kernel.model, req.params.id, resolve3, resolveProject, opts.dispatchJournal?.() ?? [], dispatchPausedOf);
33054
33526
  if (!detail) {
33055
33527
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${req.params.id}` } } };
33056
33528
  }
33057
33529
  return { status: 200, body: detail };
33058
33530
  });
33059
33531
  router.post("/api/workorders/:id/dispatch", async (req) => {
33060
- const { kernel } = await resolveCtx(req.auth.companyId);
33532
+ const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
33061
33533
  const ws = req.params.id;
33062
33534
  const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws);
33063
33535
  if (arts.length === 0) {
33064
33536
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
33065
33537
  }
33066
- const holdId = openDispatchHoldOf(kernel.model, arts);
33067
- if (!holdId) return { status: 200, body: { dispatched: false } };
33068
- await kernel.resolveAnnotation({ annotationId: holdId, actor: req.auth.actor, resolution: "resolved", note: "\u4EBA\u5DE5\u6D3E\u5355" });
33538
+ const held = artifacts ? (await artifacts.listWorkspaceDispatchHeld()).includes(ws) : false;
33539
+ if (!held) return { status: 200, body: { dispatched: false } };
33540
+ await artifacts.setWorkspaceDispatchHold(ws, false);
33069
33541
  return { status: 200, body: { dispatched: true } };
33070
33542
  });
33071
33543
  router.get("/api/inbox", async (req) => {
@@ -34016,23 +34488,6 @@ function createChatSessionsDomain(opts) {
34016
34488
  await store.updateSession(req.params.id, { touchedAt: (/* @__PURE__ */ new Date()).toISOString() });
34017
34489
  return { status: 201, body: msg };
34018
34490
  });
34019
- router.get("/api/chat-sessions/:id/work-orders", async (req) => {
34020
- const session = await store.getSession(req.params.id);
34021
- if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
34022
- const workOrderIds = await store.listWorkOrdersForSession(req.params.id);
34023
- if (!opts.kernel || workOrderIds.length === 0) return { status: 200, body: { items: [] } };
34024
- const bindings = opts.artifactState ? await opts.artifactState.listWorkspaceBindings() : [];
34025
- const byWo = new Map(bindings.map((b2) => [b2.workOrderId, b2.projectId]));
34026
- const actors = opts.kernel ? await registry2.listActors().catch(() => []) : [];
34027
- const byId = new Map(actors.map((a) => [a.id, a]));
34028
- const resolver = (id) => {
34029
- const a = byId.get(id);
34030
- return a ? { name: a.name } : void 0;
34031
- };
34032
- const allSummaries = buildWorkorderSummaries(opts.kernel.model, resolver, (wo) => byWo.get(wo) ?? null);
34033
- const items = allSummaries.filter((s2) => workOrderIds.includes(s2.id));
34034
- return { status: 200, body: { items } };
34035
- });
34036
34491
  };
34037
34492
  }
34038
34493
  var import_node_crypto10;
@@ -34041,7 +34496,6 @@ var init_chat_sessions = __esm({
34041
34496
  "use strict";
34042
34497
  import_node_crypto10 = require("node:crypto");
34043
34498
  init_router();
34044
- init_workorders();
34045
34499
  }
34046
34500
  });
34047
34501
 
@@ -35582,16 +36036,16 @@ var init_worker = __esm({
35582
36036
  \u2461 **\u786E\u5C5E\u56FE\u75C5\u624D\u4E0B\u5200**\uFF08link / spawn / cancel-part / \u6539\u8FDE\uFF09\uFF1B
35583
36037
  \u2462 **\u62FF\u4E0D\u51C6\u3001\u6216\u6839\u56E0\u4E0D\u5728\u56FE\u4E0A**\uFF08\u50CF\u8FD0\u884C\u65F6\u5361\u6B7B\u3001\u5185\u5BB9\u5199\u4E0D\u51FA\u3001\u9700\u6C42\u672C\u8EAB\u77DB\u76FE\uFF09\u2192 **\u522B\u52A8\u56FE**\uFF0C\u4E00\u53E5\u8BDD\u8BF4\u6E05\u4F60\u5224\u65AD\u7684\u6839\u56E0 + \u5EFA\u8BAE\u8C01\u5904\u7F6E\uFF0C\u6536\u5DE5\u3002**\u8BEF\u5220\u4E00\u6761\u672C\u8BE5\u7B49\u7684\u6D41\u7A0B\uFF0C\u6BD4\u665A\u6551\u4E00\u4F1A\u513F\u66F4\u7CDF\u3002**
35584
36038
 
35585
- \u3010\u6539\u56FE\uFF1A\u6682\u5B58 \u2192 \u63D0\u4EA4\u3011\u4F60\u80FD\u7528\u7684\u6539\u56FE\u547D\u4EE4\uFF08\u90FD\u5148\u8FDB**\u7F16\u8F91\u7F13\u51B2**\u3001\u4E0D\u7ACB\u5373\u6539\u771F\u56FE\uFF09\uFF1A
35586
- - \`oasis link <consumer> --to <up> [--required]\` \u2014\u2014 \u52A0\u4F9D\u8D56\u8FB9
35587
- - \`oasis spawn <id> --type <t> --title "<\u77ED\u540D>" --brief "<\u89C4\u683C>" [--input <up>]\` \u2014\u2014 \u65B0\u5EFA\u8282\u70B9
35588
- - \`oasis edit <id> [--title "\u2026"] [--brief "\u2026"]\` \u2014\u2014 \u6539\u8282\u70B9\u6807\u9898/brief
35589
- - \`oasis unlink <consumer> --to <up>\` \u2014\u2014 \u65AD\u4F9D\u8D56\u8FB9
35590
- - \`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
35591
- - \`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
35592
- - \`oasis cancel-part <id> --part <\u540D>\` \u2014\u2014 \u64A4\u90E8\u4EF6
35593
- - \`oasis staged\` \u2014\u2014 \u770B\u5F53\u524D\u7F13\u51B2\u5168\u90E8\u5F85\u63D0\u4EA4\u9879\uFF08+ \u63D0\u4EA4\u540E\u9884\u89C8\uFF09
35594
- - \`oasis apply --reason "\u4E3A\u4EC0\u4E48\u8FD9\u4E48\u6539"\` \u2014\u2014 \u4E00\u6B21\u63D0\u4EA4\u7F13\u51B2
36039
+ \u3010\u6539\u56FE\uFF1A\u6682\u5B58 \u2192 \u63D0\u4EA4\u3011\u4F60\u80FD\u7528\u7684\u6539\u56FE\u547D\u4EE4\uFF08**\u90FD\u5148\u8FDB\u7F16\u8F91\u7F13\u51B2\u3001\u4E0D\u7ACB\u5373\u6539\u771F\u56FE**\uFF1B\u6539\u5B8C\u7528 \`oasis apply\` \u4E00\u6B21\u6027\u63D0\u4EA4\uFF09\uFF1A
36040
+ - \`oasis link <consumer> --to <up> [--required true]\` \u2014\u2014 \u7ED9 consumer \u52A0\u4E00\u6761\u6307\u5411\u4E0A\u6E38 up \u7684\u4F9D\u8D56\u8FB9\u3002\`<consumer>\`\uFF08\u4F4D\u7F6E\u53C2\u6570\uFF09= \u8981\u52A0\u4F9D\u8D56\u7684\u4E0B\u6E38\u8282\u70B9 id\uFF1B\`--to <up>\` = \u88AB\u4F9D\u8D56\u7684\u4E0A\u6E38 id\uFF1B\`--required true\` = \u8FD9\u6761\u662F**\u963B\u585E\u6027**\u4F9D\u8D56\uFF08up \u6CA1 conclude\uFF0Cconsumer \u5C31\u52A8\u4E0D\u4E86\uFF09\uFF0C**\u7701\u7565\u5219\u662F\u8F6F\u4F9D\u8D56**\uFF08\u53EA\u8BB0\u5173\u7CFB\u3001\u4E0D\u963B\u585E\uFF09\u3002\u53BB\u91CD\u65F6\u7528\u5B83\u628A"\u8BE5\u8FDE\u65E2\u6709"\u7684\u8FB9\u8865\u4E0A\u3002
36041
+ - \`oasis spawn <id> --type <t> [--title "<\u77ED\u540D>"] [--brief "<\u89C4\u683C>"] [--input <up>[,<up2>\u2026]]\` \u2014\u2014 \u65B0\u5EFA\u4E00\u4E2A\u8282\u70B9\u3002\`<id>\`\uFF08\u4F4D\u7F6E\u53C2\u6570\uFF09= \u65B0\u8282\u70B9 id\uFF08\u5F62\u5982 \`artifact:<type>:<\u540D>\`\uFF09\uFF1B\`--type <t>\` = \u7C7B\u578B\u540D\uFF08**\u5FC5\u586B**\uFF0C\u5148 \`oasis artifact-types\` \u770B\u53EF\u9009\u7C7B\u578B\u4E0E\u7528\u9014\uFF09\uFF1B\`--title\` = \u77ED\u540D\uFF1B\`--brief\` = \u7ED9\u5B83\u7684\u4EA7\u51FA\u89C4\u683C\uFF08\u5199\u6E05\u8981\u505A\u6210\u4EC0\u4E48\uFF0C\u4F1A\u6210\u4E3A\u8BE5\u8282\u70B9 owner \u7684\u4EFB\u52A1\u4E66\uFF09\uFF1B\`--input\` = \u5B83\u4F9D\u8D56\u54EA\u4E9B\u4E0A\u6E38\uFF08**\u9017\u53F7\u5206\u9694**\u591A\u4E2A\uFF09\u3002**\u786E\u65E0\u65E2\u6709\u53EF\u8FDE\u3001\u662F\u771F\u65B0\u9700\u6C42\u624D\u7528**\u3002
36042
+ - \`oasis edit <id> [--title "<\u65B0\u77ED\u540D>"] [--brief "<\u65B0\u89C4\u683C>"]\` \u2014\u2014 \u6539\u4E00\u4E2A**\u5DF2\u5B58\u5728**\u8282\u70B9\u7684\u6807\u9898 / brief\uFF08**\u53EA\u6539\u8FD9\u4E24\u6837\u5143\u6570\u636E\uFF0C\u7EDD\u4E0D\u78B0\u6B63\u6587**\uFF09\u3002\`<id>\` = \u88AB\u6539\u8282\u70B9\uFF1B\`--title\` / \`--brief\` \u7ED9\u54EA\u4E2A\u4F20\u54EA\u4E2A\u3002\u5408\u5E76\u53BB\u91CD\u65F6\u7528\u5B83\u628A\u4FDD\u7559\u4EF6\u7684 brief \u6269\u5199\u3002
36043
+ - \`oasis unlink <consumer> --to <up>\` \u2014\u2014 \u5220\u6389 consumer \u6307\u5411 up \u7684\u90A3\u6761\u4F9D\u8D56\u8FB9\u3002\`<consumer>\` = \u4E0B\u6E38\uFF1B\`--to <up>\` = \u4E0D\u518D\u4F9D\u8D56\u7684\u4E0A\u6E38\u3002\u6539\u8FDE\uFF08\u65AD\u9519\u8FB9 / \u6539\u8FDE\u5230\u6B63\u786E\u4E0A\u6E38\uFF09\u65F6\u7528\u3002
36044
+ - \`oasis cancel <id> [--note "<\u539F\u56E0>"]\` \u2014\u2014 **\u5F7B\u5E95\u5E9F\u5F03**\u4E00\u4E2A\u8282\u70B9\uFF1A\u5B83\u4E0D\u518D\u7B97\u6570\uFF0C\u7CFB\u7EDF**\u81EA\u52A8\u7EA7\u8054**\u65AD\u6389\u5B83\u6240\u6709\u4F9D\u8D56\u8FB9\u3001\u5E76\u91CD\u5524\u4E0B\u6E38\u8BA9\u5B83\u4EEC\u53BB\u6389\u5BF9\u5B83\u7684\u9002\u914D\u3002\`<id>\`\uFF08\u4F4D\u7F6E\u53C2\u6570\uFF09= \u88AB\u5E9F\u8282\u70B9\uFF1B\`--note\` = \u4E3A\u4EC0\u4E48\u5E9F\uFF08\u7559\u75D5\uFF09\u3002\u5EFA\u5728\u5DF2\u5E9F\u5730\u57FA\u4E0A\u3001\u6216\u8FD9\u8282\u70B9\u6839\u672C\u4E0D\u8BE5\u5B58\u5728\u65F6\u7528\u3002\u5E9F\u5F03\u540E\u8BE5\u8282\u70B9\u5728 \`oasis status\` \u91CC\u663E\u793A lifecycle=sealed:cancelled\u3002**\u9500\u6BC1\u6027**\u2014\u2014apply \u65F6\u6253\u5305\u6210\u63D0\u6848\u4EA4\u4EBA\u786E\u8BA4\u3002
36045
+ - \`oasis freeze <id> [--note "<\u539F\u56E0>"]\` \u2014\u2014 **\u51BB\u7ED3**\u4E00\u4E2A\u8282\u70B9\uFF1A\u4FDD\u7559\u5B83\u73B0\u6709\u4EA7\u51FA\u3001\u505C\u6B62\u518D\u6539\u52A8\uFF0C\u4F46**\u4F9D\u8D56\u8FB9\u90FD\u4FDD\u7559\u4E0D\u52A8**\uFF08\u4E0E cancel \u7684\u5173\u952E\u533A\u522B\u2014\u2014cancel \u662F\u65AD\u8FB9\u5E9F\u5F03\uFF0Cfreeze \u53EA\u662F\u6309\u4E0B\u6682\u505C\uFF09\u3002\`<id>\`\uFF08\u4F4D\u7F6E\u53C2\u6570\uFF09= \u88AB\u51BB\u8282\u70B9\uFF1B\`--note\` = \u4E3A\u4EC0\u4E48\u51BB\uFF08\u7559\u75D5\uFF09\u3002\u67D0\u8282\u70B9\u6682\u4E0D\u63A8\u8FDB\u3001\u4F46\u6210\u679C\u8981\u7559\u7740\u65F6\u7528\u3002\u51BB\u7ED3\u540E\u8BE5\u8282\u70B9\u5728 \`oasis status\` \u91CC\u663E\u793A lifecycle=sealed:frozen\u3002
36046
+ - \`oasis cancel-part <id> --part <\u90E8\u4EF6\u540D>\` \u2014\u2014 \u4ECE\u4E00\u4E2A\u5206\u89E3\u4E86\u90E8\u4EF6\u7684\u8282\u70B9\u4E0A**\u64A4\u6389\u4E00\u4E2A\u90E8\u4EF6**\u3002\`<id>\` = \u8282\u70B9\uFF1B\`--part\` = \u8981\u64A4\u7684\u90E8\u4EF6\u540D\uFF08**\u5FC5\u586B**\uFF09\u3002\u67D0 part \u6C38\u4E45\u4EA4\u4ED8\u4E0D\u4E86\u3001\u5361\u6B7B\u6574\u6761\u96C6\u6210\u65F6\u64A4\u6389\u5B83\u6551\u5176\u4F59\u3002**\u9500\u6BC1\u6027**\u2014\u2014\u8F6C\u4EBA\u5BA1\u3002
36047
+ - \`oasis staged\` \u2014\u2014 \u770B\u5F53\u524D\u7F16\u8F91\u7F13\u51B2\u91CC**\u5168\u90E8\u5F85\u63D0\u4EA4\u9879** + \u63D0\u4EA4\u540E\u5404\u53D7\u5F71\u54CD\u8282\u70B9\u4F1A\u53D8\u6210\u4EC0\u4E48\u6837\uFF08apply \u524D\u81EA\u68C0\uFF1B\u65E0\u53C2\u6570\uFF09\u3002
36048
+ - \`oasis apply --reason "<\u4E3A\u4EC0\u4E48\u8FD9\u4E48\u6539>"\` \u2014\u2014 **\u4E00\u6B21\u6027\u63D0\u4EA4**\u6574\u4E2A\u7F13\u51B2\u3002\`--reason\`\uFF08**\u5FC5\u586B**\uFF09\u5199\u6E05\u8FD9\u6279\u6539\u52A8\u7684\u610F\u56FE\u3002
35595
36049
  \u6BCF\u4E2A\u6539\u56FE\u547D\u4EE4\u56DE\u663E"\u5DF2\u6682\u5B58\u4EC0\u4E48 + \u53D7\u5F71\u54CD\u8282\u70B9\u6682\u5B58\u540E\u7684\u6837\u5B50"\u3002\`apply\` \u65F6\u7CFB\u7EDF\u770B\u4F60\u8FD9\u6279 diff \u81EA\u52A8\u51B3\u5B9A\uFF1A\u80FD\u76F4\u63A5\u843D\u7684\u76F4\u63A5\u843D\uFF0C\u6D89\u53CA\u9500\u6BC1/\u6539\u6D3E/\u89E3\u5C01\u7684\u6253\u5305\u6210\u4E00\u4EFD\u63D0\u6848\u4EA4\u4EBA\u786E\u8BA4\u2014\u2014**\u4F60\u4E0D\u5FC5\u81EA\u5DF1\u5206\u7C7B\u3001\u4E0D\u5199 JSON**\u3002\u6388\u6743\u5355\u4F4D\u662F **diff \u4E0D\u662F\u80FD\u529B**\u3002
35596
36050
  - \u6539\u5B8C**\u4E00\u5B9A apply**\uFF08\u5FD8\u4E86\u4E5F\u515C\u5E95\uFF1A\u6536\u5DE5\u65F6\u7F13\u51B2\u975E\u7A7A\uFF0C\u7CFB\u7EDF\u6309\u4E0A\u9762\u89C4\u5219\u66FF\u4F60\u63D0\u4EA4\uFF09\u3002
35597
36051
  - \u4E00\u4EFD\u63D0\u6848\uFF1D**\u4E00\u4E2A\u8FDE\u8D2F\u4E8B\u52A1**\uFF1A\u5408\u5E76\u91CD\u590D\u5C31\u628A"\`edit\` \u6269\u4FDD\u7559\u4EF6 brief + \u6539\u8FDE + \`seal\` \u5E9F\u5F03\u4EF6"\u4E00\u8D77\u6682\u5B58\u518D apply\u3002
@@ -35599,10 +36053,10 @@ var init_worker = __esm({
35599
36053
  - \u63D0\u4EA4\u540E**\u5C31\u6536\u5DE5**\uFF1B\u8F6C\u4EBA\u5BA1\u5C31\u5230\u6B64\u4E3A\u6B62\u2014\u2014\u4EBA\u786E\u8BA4\u540E\u56FE\u4F1A\u53D8\uFF0C\u9700\u8981\u65F6\u7CFB\u7EDF\u6309**\u65B0\u72B6\u6001**\u91CD\u65B0\u5524\u8D77\u4F60\uFF08\u4E0D\u5FC5\u7B49\u3001\u4E0D\u5FC5\u8BB0\u540E\u7EED\u3001\u4ECE\u56FE\u91CD\u5224\uFF09\u3002**\u8282\u70B9\u4E0A\u6709"\u88AB\u9A73\u56DE"\u7559\u75D5\u5C31\u522B\u91CD\u590D\u63D0\u3002**
35600
36054
 
35601
36055
  \u3010\u8BFB\u56FE\u3011\uFF08\u90FD\u8BFB**\u771F\u56FE**\uFF0C\u4E0E\u522B\u7684 agent \u540C\u8BED\u4E49\uFF0C\u4E0D\u53D7\u4F60\u6682\u5B58\u5F71\u54CD\uFF09
35602
- - \`oasis graph\` / \`oasis ls\` \u2014\u2014 \u5168\u56FE\u4F9D\u8D56\u8FB9 / \u5168\u4EA7\u7269\u5217\u8868\u3002
35603
- - \`oasis status <id>\` \u2014\u2014 \u5355\u4EA7\u7269\u72B6\u6001\uFF08lifecycle/head/\u4F9D\u8D56/\u963B\u585E/\u672A\u51B3\u6279\u6CE8\uFF09\u3002
35604
- - \`oasis content <id> [path]\` \u2014\u2014 \u5355\u4EA7\u7269\u6B63\u6587\uFF08\u5224\u65AD\u4E24\u4E2A\u662F\u5426\u8BED\u4E49\u91CD\u590D\u65F6\u7528\uFF09\u3002
35605
- - \`oasis session-log <id|runId> [--n N]\` \u2014\u2014 \u770B\u67D0\u8282\u70B9\u5728\u8DD1\u7684 session \u6700\u65B0\u8F93\u51FA + "\u8DDD\u4E0A\u6B21\u4E8B\u4EF6 X \u5206\u949F"\uFF0C\u5224\u5B83\u662F**\u5728\u60F3**\u8FD8\u662F**\u5361\u6B7B**\uFF08\u6B8B\u5DEE\u4E8C\u6B21\u5F52\u56E0\u3001\u6216\u6000\u7591\u8FD0\u884C\u65F6\u5361\u6B7B\u65F6\u7528\uFF1A>10min\u2248\u5361\uFF0C1\u20132min \u518D\u7B49\uFF09\u3002`;
36056
+ - \`oasis graph\` \u2014\u2014 \u5168\u56FE\u4F9D\u8D56\u8FB9\uFF08\u8C01\u8FDE\u8C01\uFF09\uFF1B\`oasis ls\` \u2014\u2014 \u5168\u4EA7\u7269\u5217\u8868\uFF08id / type / owner / head / \u961F\u5217 / \u72B6\u6001\u6807\uFF09\u3002\u5148\u7528\u5B83\u4EEC\u5EFA\u7ACB\u5168\u5C40\u56FE\u666F\u3002
36057
+ - \`oasis status <id>\` \u2014\u2014 \u5355\u4EA7\u7269\u7684\u72B6\u6001\u7D22\u5F15\uFF1Alifecycle\u3001\u5F53\u524D head\u3001\u4F9D\u8D56\u3001\u662F\u5426\u963B\u585E\u3001\u672A\u51B3\u6279\u6CE8\uFF08**\u4E0D\u542B\u6B63\u6587**\uFF0C\u5148\u770B\u8FD9\u4E2A\uFF09\u3002\`<id>\` = \u8981\u770B\u7684\u4EA7\u7269\u3002
36058
+ - \`oasis content <id> [path]\` \u2014\u2014 \u53D6\u5355\u4EA7\u7269**\u5B9E\u9645\u6B63\u6587**\uFF08\u5224\u65AD\u4E24\u8282\u70B9\u662F\u5426\u8BED\u4E49\u91CD\u590D\u3001\u8981\u4E0D\u8981\u5408\u5E76\u65F6\u8BFB\uFF09\u3002\`<id>\` = \u4EA7\u7269\uFF1B\u591A\u6587\u4EF6\u4EA7\u7269**\u7701\u7565 \`path\`** \u5148\u770B\u6587\u4EF6\u6E05\u5355\u3001\u518D\u7ED9 \`path\` \u53D6\u5355\u4E2A\u6587\u4EF6\u3002
36059
+ - \`oasis session-log <id|runId> [--n N]\` \u2014\u2014 \u770B\u67D0\u8282\u70B9 / \u67D0\u4F1A\u8BDD\u7684**\u8FD0\u884C\u65F6\u8F93\u51FA**\uFF08agent \u6B64\u523B\u5728\u8BF4 / \u5E72\u4EC0\u4E48\uFF0C\u975E\u5185\u6838\u4E8B\u5B9E\uFF09+ "\u8DDD\u4E0A\u6B21\u4E8B\u4EF6 X \u5206\u949F"\uFF0C\u5224\u5B83\u5728**\u60F3**\u8FD8\u662F**\u5361\u6B7B**\uFF08>10min\u2248\u5361\uFF09\u3002\u4F20 \`<artifactId>\` \u4F1A\u5217\u51FA\u8BE5\u8282\u70B9\u7684**\u5168\u90E8\u4F1A\u8BDD**\u3001\u518D\u5C55\u5F00\u6700\u8FD1\u5728\u8DD1\u7684\u90A3\u4E2A\uFF1B\u4F20\u5177\u4F53 \`<runId>\` \u76F4\u63A5\u770B\u90A3\u4E00\u4E2A\uFF1B\`--n N\` = \u770B\u6700\u65B0 N \u6761\u4E8B\u4EF6\uFF08\u9ED8\u8BA4 20\uFF09\u3002\u6B8B\u5DEE\u4E8C\u6B21\u5F52\u56E0\u3001\u6216\u60F3\u786E\u8BA4\u4E0A\u6E38\u662F\u5426\u771F\u5728\u88AB\u5E72\u65F6\u7528\u3002`;
35606
36060
  CoordinatorWorker = class {
35607
36061
  constructor(deps) {
35608
36062
  this.deps = deps;
@@ -35751,15 +36205,6 @@ var init_worker = __esm({
35751
36205
  gaps.length > 0 ? `\u672A\u89E3\u6790gap\xD7${gaps.length}` : null
35752
36206
  ].filter(Boolean).join("\u3001");
35753
36207
  }
35754
- graphDigest(exclude) {
35755
- const lines = [];
35756
- for (const a of this.deps.kernel.model.artifacts.values()) {
35757
- if (a.id === exclude) continue;
35758
- const deps = a.inputs.length > 0 ? `\uFF1B\u4F9D\u8D56 [${a.inputs.map((e) => e.to).join(", ")}]` : "";
35759
- lines.push(`- ${a.id}\uFF08type=${a.type}\uFF5C${this.nodeState(a.id)}\uFF09${a.description ? `\uFF1A${a.description}` : ""}${deps}`);
35760
- }
35761
- return lines.length > 0 ? lines.join("\n") : "\uFF08\u56FE\u91CC\u6682\u65E0\u5176\u5B83\u4EA7\u7269\uFF09";
35762
- }
35763
36208
  /** 结构摘要(reconcile 去重用):id / type / 是否已交付 / title+brief / 依赖——不含进度态(去重只看语义)。 */
35764
36209
  structDigest(exclude) {
35765
36210
  const m2 = this.deps.kernel.model;
@@ -35804,22 +36249,51 @@ var init_worker = __esm({
35804
36249
  * 真死锁且某 part 卡住整条集成 → 起草 cancelPart(C 类,人确认);能解就解;拿不准 / 非图病 → 别动、交人。
35805
36250
  */
35806
36251
  async reconcileDeadlock(p2) {
35807
- const node = this.deps.kernel.model.artifacts.get(p2.id);
36252
+ const m2 = this.deps.kernel.model;
36253
+ const node = m2.artifacts.get(p2.id);
35808
36254
  const mins = Math.round(p2.staleMs / 6e4);
35809
- const parts = node?.parts?.map((x2) => x2.name).join("\u3001") || "\uFF08\u65E0\u5206\u89E3\uFF09";
35810
36255
  const brief = node?.title ?? node?.description ?? "";
36256
+ const now = Date.now();
35811
36257
  const caseFile = p2.diag.causes.length ? p2.diag.causes.map((c) => `> - [${c.category}\uFF5C\u6765\u6E90 ${c.source}\uFF5C\u5EFA\u8BAE\u5904\u7F6E ${c.handler}] ${c.detail}`).join("\n") : "> \uFF08\u673A\u68B0\u7A77\u4E3E\u65E0\u660E\u786E\u75C5\u56E0\u2014\u2014\u6B8B\u5DEE\uFF0C\u9700\u4F60\u4E8C\u6B21\u5F52\u56E0\uFF09";
36258
+ const sessionsLine = (id) => {
36259
+ const ss = this.deps.sessionsOf?.(id) ?? [];
36260
+ if (ss.length === 0) return "\u65E0\u5728\u8DD1\u4F1A\u8BDD";
36261
+ return `${ss.length} \u4E2A \u2014\u2014 ${ss.map((s2) => `${s2.action} run ${s2.runId}\uFF08\u5DF2\u8DD1 ${Math.round((now - Date.parse(s2.startedAt)) / 6e4)} \u5206\u949F\uFF09`).join("\u3001")}`;
36262
+ };
36263
+ const nodeBlock = (id) => {
36264
+ const a = m2.artifacts.get(id);
36265
+ if (!a) return `- **${id}**\uFF08\u4E0D\u5B58\u5728 / \u5DF2\u88AB\u6E05\u7406\uFF09`;
36266
+ const head = a.currentRev ? `head@${String(a.currentRev).slice(9, 17)}` : "\u65E0head";
36267
+ const blk = blockedOf(m2, id);
36268
+ const status = [lifecycleOf(m2, id), head, blk.blocked ? `\u963B\u585E[${blk.reasons.join("\uFF1B")}]` : null].filter(Boolean).join("\uFF5C");
36269
+ const label = a.title ?? a.description ?? "\uFF08\u65E0 brief\uFF09";
36270
+ return [
36271
+ `- **${id}**\uFF08type=${a.type}\uFF1A${label}\uFF09`,
36272
+ ` - \u72B6\u6001\uFF1A${status}${a.parts?.length ? `\uFF5C\u90E8\u4EF6 [${a.parts.map((x2) => x2.name).join("\u3001")}]` : ""}`,
36273
+ ` - \u5728\u8DD1\u4F1A\u8BDD\uFF1A${sessionsLine(id)}`
36274
+ ].join("\n");
36275
+ };
36276
+ const upstreams = (node?.inputs ?? []).map((e) => e.to);
36277
+ const downstreams = consumersOf(m2, p2.id).map((a) => a.id);
35812
36278
  const task = [
35813
36279
  `## \u672C\u6B21\u4E3A\u4EC0\u4E48\u5524\u8D77\u4F60`,
35814
- `\u8282\u70B9 **${p2.id}**${brief ? `\uFF08${brief}\uFF09` : ""} \u5DF2\u963B\u585E\u7EA6 **${mins} \u5206\u949F**\u6CA1\u8FDB\u5C55\u3002\u7CFB\u7EDF\u5DF2\u673A\u68B0\u8BCA\u65AD\u3001\u786E\u8BA4**\u8FD9\u662F\u56FE\u75C5\u6216\u673A\u68B0\u5B9A\u4F4D\u4E0D\u5230\u7684\u6B8B\u5DEE**\uFF08\u8D44\u6E90 / \u4EBA / \u8FD0\u884C\u65F6\u7C7B\u75C5\u56E0\u5DF2\u53E6\u884C\u8DEF\u7531\uFF0C\u4E0D\u52B3\u4F60\uFF09\u3002`,
36280
+ `\u8282\u70B9 **${p2.id}**${brief ? `\uFF08${brief}\uFF09` : ""} \u5DF2\u963B\u585E\u7EA6 **${mins} \u5206\u949F**\u6CA1\u8FDB\u5C55\u3002\u7CFB\u7EDF\u5DF2\u673A\u68B0\u8BCA\u65AD\u3001\u786E\u8BA4**\u8FD9\u662F\u56FE\u75C5\u6216\u673A\u68B0\u5B9A\u4F4D\u4E0D\u5230\u7684\u6B8B\u5DEE**\uFF08\u8D44\u6E90 / \u4EBA / \u8FD0\u884C\u65F6\u7C7B\u75C5\u56E0\u5DF2\u6392\u9664\uFF09\u3002`,
35815
36281
  ``,
35816
36282
  `## \u75C5\u5386\uFF08\u673A\u68B0\u8BCA\u65AD\uFF1B\u5148\u8BFB\u8FD9\u4E2A\uFF09`,
35817
36283
  caseFile,
35818
- `\u5148\u8FA8\u300C\u771F\u6B7B\u9501 / \u8BE5\u6539\u56FE\u300D\u8FD8\u662F\u300C\u53EA\u662F\u5728\u7B49\u6B63\u5728\u4EA7\u7684\u4E0A\u6E38\u300D\u2014\u2014**\u5BBD\u677E\u544A\u8B66\uFF0C\u522B\u8BEF\u6740**\u3002`,
36284
+ `\u5148\u8FA8\u662F**\u771F\u6B7B\u9501 / \u8BE5\u6539\u56FE**\uFF0C\u8FD8\u662F**\u53EA\u662F\u5728\u7B49\u4E00\u4E2A\u5065\u5EB7\u5728\u4EA7\u7684\u4E0A\u6E38**\u2014\u2014**\u786E\u662F\u771F\u6B7B\u9501\u5C31\u679C\u65AD\u6539\u56FE\uFF08\u8FD9\u6B63\u662F\u8981\u4F60\u505A\u7684\uFF09\uFF1B\u53EA\u6709\u5065\u5EB7\u7B49\u5F85\u624D\u522B\u52A8**\u3002`,
36285
+ ``,
36286
+ `## \u672C\u8282\u70B9 + \u90BB\u57DF`,
36287
+ `> \u5B57\u6BB5\u8BFB\u6CD5\uFF1Alifecycle\uFF08active=\u8FDB\u884C\u4E2D / sealed:frozen=\u51BB\u7ED3\u3001\u4EA7\u4E0D\u51FA\u65B0\u91CC\u7A0B\u7891 / sealed:cancelled=\u5DF2\u5E9F\u5F03 / sealed:accepted=\u5DF2\u5B9A\u7A3F\u7EC8\u6001\uFF09\uFF1Bhead=\u5F53\u524D\u5B9A\u7A3F\u7248\u672C\uFF08\u65E0head=\u8FD8\u6CA1\u4EA7\u51FA\u8FC7\u4EFB\u4F55\u91CC\u7A0B\u7891\uFF09\uFF1B\u963B\u585E[\u2026]=\u5361\u5728\u4EC0\u4E48\u4E0A\uFF1B\u5728\u8DD1\u4F1A\u8BDD=\u6B64\u523B\u6709\u51E0\u4E2A agent \u4F1A\u8BDD\u5728\u5E72\u5B83\u3001\u5404\u8DD1\u591A\u4E45\u3002`,
36288
+ ``,
36289
+ `### \u672C\u8282\u70B9`,
36290
+ nodeBlock(p2.id),
35819
36291
  ``,
35820
- `## \u672C\u8282\u70B9 + \u90BB\u57DF\u72B6\u6001\uFF08\u636E\u6B64\u5224\u4F1A\u81EA\u5DF1\u597D vs \u771F\u5361\u6B7B\uFF09`,
35821
- `- **${p2.id}**\uFF08\u672C\u8282\u70B9\uFF0C\u90E8\u4EF6\uFF1A${parts}\uFF09\uFF1A${this.nodeState(p2.id)}`,
35822
- this.graphDigest(p2.id),
36292
+ `### \u4E0A\u6E38\uFF08\u672C\u8282\u70B9\u4F9D\u8D56\u8C01\u2014\u2014\u5B83\u4EEC\u6CA1\u5C31\u7EEA\uFF0C\u672C\u8282\u70B9\u52A8\u4E0D\u4E86\uFF09`,
36293
+ upstreams.length ? upstreams.map(nodeBlock).join("\n") : "- \uFF08\u65E0\u4E0A\u6E38\u4F9D\u8D56\uFF09",
36294
+ ``,
36295
+ `### \u4E0B\u6E38\uFF08\u8C01\u4F9D\u8D56\u672C\u8282\u70B9\u2014\u2014\u672C\u8282\u70B9\u5361\uFF0C\u5B83\u4EEC\u8DDF\u7740\u5361\uFF09`,
36296
+ downstreams.length ? downstreams.map(nodeBlock).join("\n") : "- \uFF08\u65E0\u4E0B\u6E38\u6D88\u8D39\u8005\uFF09",
35823
36297
  ``,
35824
36298
  `## \u4F60\u7684\u52A8\u4F5C\uFF08\u6682\u5B58 \u2192 apply\uFF09`,
35825
36299
  `- \u5728**\u7B49\u4E00\u4E2A active\u3001\u6709 head/\u5728\u4EA7\u7684\u4E0A\u6E38** \u2192 **\u4E0D\u662F\u6B7B\u9501\uFF0C\u522B\u52A8**\uFF0C\u6536\u5DE5\uFF08\u53EF \`oasis status <\u4E0A\u6E38>\` \u786E\u8BA4\uFF09\u3002`,
@@ -35907,6 +36381,7 @@ var init_src4 = __esm({
35907
36381
  init_resolve();
35908
36382
  init_worker();
35909
36383
  init_drafts();
36384
+ init_workorder_drafts();
35910
36385
  }
35911
36386
  });
35912
36387
 
@@ -37478,11 +37953,11 @@ var require_pg_connection_string = __commonJS({
37478
37953
  config2.client_encoding = result.searchParams.get("encoding");
37479
37954
  return config2;
37480
37955
  }
37481
- const hostname3 = dummyHost ? "" : result.hostname;
37956
+ const hostname5 = dummyHost ? "" : result.hostname;
37482
37957
  if (!config2.host) {
37483
- config2.host = decodeURIComponent(hostname3);
37484
- } else if (hostname3 && /^%2f/i.test(hostname3)) {
37485
- result.pathname = hostname3 + result.pathname;
37958
+ config2.host = decodeURIComponent(hostname5);
37959
+ } else if (hostname5 && /^%2f/i.test(hostname5)) {
37960
+ result.pathname = hostname5 + result.pathname;
37486
37961
  }
37487
37962
  if (!config2.port) {
37488
37963
  config2.port = result.port;
@@ -41092,7 +41567,6 @@ var init_trajectory = __esm({
41092
41567
  });
41093
41568
 
41094
41569
  // src/index.ts
41095
- var import_node_crypto22 = require("node:crypto");
41096
41570
  var fs15 = __toESM(require("node:fs"));
41097
41571
  var os8 = __toESM(require("node:os"));
41098
41572
  var path13 = __toESM(require("node:path"));
@@ -41874,6 +42348,9 @@ var CodexAdapter = class {
41874
42348
  "-C",
41875
42349
  dir,
41876
42350
  ...this.opts.model ? ["--model", this.opts.model] : [],
42351
+ // 打开 reasoning 摘要通道:默认 auto,让 codex 产出 reasoning item(→ thought → 前端思考块)。
42352
+ ...this.opts.reasoningSummary !== "none" ? ["-c", `model_reasoning_summary=${this.opts.reasoningSummary ?? "auto"}`] : [],
42353
+ ...this.opts.reasoningEffort ? ["-c", `model_reasoning_effort=${this.opts.reasoningEffort}`] : [],
41877
42354
  ...this.opts.extraArgs ?? [],
41878
42355
  prompt
41879
42356
  ];
@@ -43907,6 +44384,10 @@ var PostgresArtifactStateStore = class _PostgresArtifactStateStore {
43907
44384
  work_order_id text PRIMARY KEY,
43908
44385
  project_id text NOT NULL
43909
44386
  )`);
44387
+ await pool.query(`
44388
+ CREATE TABLE IF NOT EXISTS "${s2}".workspace_dispatch_hold (
44389
+ work_order_id text PRIMARY KEY
44390
+ )`);
43910
44391
  return new _PostgresArtifactStateStore(pool, schema);
43911
44392
  }
43912
44393
  /* ---------- artifacts ---------- */
@@ -44180,14 +44661,29 @@ var PostgresArtifactStateStore = class _PostgresArtifactStateStore {
44180
44661
  const r = await this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`);
44181
44662
  return r.rows.map(rowToWorkspaceBinding);
44182
44663
  }
44664
+ async setWorkspaceDispatchHold(workOrderId, held) {
44665
+ if (held) {
44666
+ await this.pool.query(
44667
+ `INSERT INTO ${this.s}.workspace_dispatch_hold (work_order_id) VALUES ($1) ON CONFLICT DO NOTHING`,
44668
+ [workOrderId]
44669
+ );
44670
+ } else {
44671
+ await this.pool.query(`DELETE FROM ${this.s}.workspace_dispatch_hold WHERE work_order_id = $1`, [workOrderId]);
44672
+ }
44673
+ }
44674
+ async listWorkspaceDispatchHeld() {
44675
+ const r = await this.pool.query(`SELECT work_order_id FROM ${this.s}.workspace_dispatch_hold`);
44676
+ return r.rows.map((row) => String(row.work_order_id));
44677
+ }
44183
44678
  async listAll() {
44184
- const [artifacts, revisions, events, annotations, outputs, bindings] = await Promise.all([
44679
+ const [artifacts, revisions, events, annotations, outputs, bindings, held] = await Promise.all([
44185
44680
  this.pool.query(`SELECT * FROM ${this.s}.artifacts`),
44186
44681
  this.pool.query(`SELECT * FROM ${this.s}.artifact_revisions`),
44187
44682
  this.pool.query(`SELECT * FROM ${this.s}.artifact_events ORDER BY seq`),
44188
44683
  this.pool.query(`SELECT * FROM ${this.s}.artifact_annotations`),
44189
44684
  this.pool.query(`SELECT * FROM ${this.s}.node_outputs`),
44190
- this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`)
44685
+ this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`),
44686
+ this.pool.query(`SELECT work_order_id FROM ${this.s}.workspace_dispatch_hold`)
44191
44687
  ]);
44192
44688
  const nodeOutputs = [];
44193
44689
  for (const row of outputs.rows) {
@@ -44203,7 +44699,8 @@ var PostgresArtifactStateStore = class _PostgresArtifactStateStore {
44203
44699
  events: events.rows.map(rowToEvent),
44204
44700
  annotations: annotations.rows.map(rowToAnnotation),
44205
44701
  nodeOutputs,
44206
- workspaceBindings: bindings.rows.map(rowToWorkspaceBinding)
44702
+ workspaceBindings: bindings.rows.map(rowToWorkspaceBinding),
44703
+ workspaceDispatchHeld: held.rows.map((row) => String(row.work_order_id))
44207
44704
  };
44208
44705
  }
44209
44706
  /**
@@ -45249,13 +45746,6 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
45249
45746
  created_at timestamptz NOT NULL
45250
45747
  )`);
45251
45748
  await pool.query(`CREATE INDEX IF NOT EXISTS chat_messages_session_idx ON "${s2}".chat_messages (session_id, seq)`);
45252
- await pool.query(`
45253
- CREATE TABLE IF NOT EXISTS "${s2}".chat_session_work_orders (
45254
- session_id text NOT NULL,
45255
- work_order_id text NOT NULL,
45256
- linked_at timestamptz NOT NULL DEFAULT now(),
45257
- PRIMARY KEY (session_id, work_order_id)
45258
- )`);
45259
45749
  return new _PostgresChatSessionStore(pool, schema);
45260
45750
  }
45261
45751
  async createSession(s2) {
@@ -45319,19 +45809,6 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
45319
45809
  const rows = r.rows.map(rowToMessage);
45320
45810
  return limit ? rows.reverse() : rows;
45321
45811
  }
45322
- async linkWorkOrder(sessionId, workOrderId) {
45323
- await this.pool.query(
45324
- `INSERT INTO ${this.s}.chat_session_work_orders (session_id, work_order_id) VALUES ($1,$2) ON CONFLICT DO NOTHING`,
45325
- [sessionId, workOrderId]
45326
- );
45327
- }
45328
- async listWorkOrdersForSession(sessionId) {
45329
- const r = await this.pool.query(
45330
- `SELECT work_order_id FROM ${this.s}.chat_session_work_orders WHERE session_id = $1 ORDER BY linked_at`,
45331
- [sessionId]
45332
- );
45333
- return r.rows.map((row) => row.work_order_id);
45334
- }
45335
45812
  };
45336
45813
  var rowToSession = (row) => ({
45337
45814
  id: row.id,
@@ -45389,10 +45866,6 @@ var PostgresNodeStore = class _PostgresNodeStore {
45389
45866
  last_seen_at timestamptz NOT NULL
45390
45867
  )`);
45391
45868
  await pool.query(`CREATE INDEX IF NOT EXISTS node_runtimes_node_idx ON "${s2}".node_runtimes (node_id)`);
45392
- await pool.query(`
45393
- CREATE TABLE IF NOT EXISTS "${s2}".deleted_runtime_ids (
45394
- id text PRIMARY KEY
45395
- )`);
45396
45869
  return new _PostgresNodeStore(pool, schema);
45397
45870
  }
45398
45871
  async upsertNode(n) {
@@ -45409,8 +45882,6 @@ var PostgresNodeStore = class _PostgresNodeStore {
45409
45882
  );
45410
45883
  }
45411
45884
  async upsertRuntime(r) {
45412
- const deleted = await this.pool.query(`SELECT 1 FROM ${this.s}.deleted_runtime_ids WHERE id=$1`, [r.id]);
45413
- if (deleted.rowCount) return;
45414
45885
  await this.pool.query(
45415
45886
  `INSERT INTO ${this.s}.node_runtimes (id,node_id,kind,hostname,runtime_binary,version,status,last_seen_at)
45416
45887
  VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
@@ -45423,8 +45894,18 @@ var PostgresNodeStore = class _PostgresNodeStore {
45423
45894
  [r.id, r.nodeId, r.kind, r.hostname, r.binary ?? null, r.version ?? null, r.status, r.lastSeenAt]
45424
45895
  );
45425
45896
  }
45897
+ /** Insert only if absent (ON CONFLICT DO NOTHING). Returns true if a row was inserted. */
45898
+ async addRuntimeIfAbsent(r) {
45899
+ const res = await this.pool.query(
45900
+ `INSERT INTO ${this.s}.node_runtimes (id,node_id,kind,hostname,runtime_binary,version,status,last_seen_at)
45901
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
45902
+ ON CONFLICT (id) DO NOTHING`,
45903
+ [r.id, r.nodeId, r.kind, r.hostname, r.binary ?? null, r.version ?? null, r.status, r.lastSeenAt]
45904
+ );
45905
+ return (res.rowCount ?? 0) > 0;
45906
+ }
45426
45907
  async setNodeOnline(id, online) {
45427
- await this.pool.query(`UPDATE ${this.s}.nodes SET status=$2,last_seen_at=$3 WHERE id=$1`, [id, online ? "online" : "offline", (/* @__PURE__ */ new Date()).toISOString()]);
45908
+ await this.pool.query(`UPDATE ${this.s}.nodes SET status=$2,last_seen_at=$3 WHERE id=$1 AND status <> 'deleted'`, [id, online ? "online" : "offline", (/* @__PURE__ */ new Date()).toISOString()]);
45428
45909
  }
45429
45910
  async setNodeRuntimesOnline(nodeId, online) {
45430
45911
  await this.pool.query(`UPDATE ${this.s}.node_runtimes SET status=$2,last_seen_at=$3 WHERE node_id=$1`, [nodeId, online ? "online" : "offline", (/* @__PURE__ */ new Date()).toISOString()]);
@@ -45437,7 +45918,7 @@ var PostgresNodeStore = class _PostgresNodeStore {
45437
45918
  return r.rows[0] ? rowToNode(r.rows[0]) : null;
45438
45919
  }
45439
45920
  async listNodes() {
45440
- const r = await this.pool.query(`SELECT * FROM ${this.s}.nodes ORDER BY enrolled_at ASC`);
45921
+ const r = await this.pool.query(`SELECT * FROM ${this.s}.nodes WHERE status <> 'deleted' ORDER BY enrolled_at ASC`);
45441
45922
  return r.rows.map(rowToNode);
45442
45923
  }
45443
45924
  async listRuntimes(nodeId) {
@@ -45446,24 +45927,24 @@ var PostgresNodeStore = class _PostgresNodeStore {
45446
45927
  }
45447
45928
  async deleteNode(id) {
45448
45929
  await this.pool.query(`DELETE FROM ${this.s}.nodes WHERE id=$1`, [id]);
45449
- await this.pool.query(`DELETE FROM ${this.s}.deleted_runtime_ids WHERE id LIKE $1`, [`runtime:${id}:%`]);
45930
+ }
45931
+ /** Soft-delete: mark node as deleted (excluded from normal API, kept for audit) and drop its runtime rows. */
45932
+ async softDeleteNode(id) {
45933
+ await this.pool.query(`UPDATE ${this.s}.nodes SET status='deleted',last_seen_at=$2 WHERE id=$1`, [id, (/* @__PURE__ */ new Date()).toISOString()]);
45934
+ await this.pool.query(`DELETE FROM ${this.s}.node_runtimes WHERE node_id=$1`, [id]);
45450
45935
  }
45451
45936
  async deleteRuntime(id) {
45452
- await this.pool.query(`INSERT INTO ${this.s}.deleted_runtime_ids (id) VALUES ($1) ON CONFLICT DO NOTHING`, [id]);
45453
45937
  const existing = await this.pool.query(`SELECT node_id FROM ${this.s}.node_runtimes WHERE id=$1`, [id]);
45454
45938
  await this.pool.query(`DELETE FROM ${this.s}.node_runtimes WHERE id=$1`, [id]);
45455
45939
  if (!existing.rows[0]) return { nodeDeleted: false };
45456
45940
  const nodeId = existing.rows[0].node_id;
45457
45941
  const remaining = await this.pool.query(`SELECT 1 FROM ${this.s}.node_runtimes WHERE node_id=$1 LIMIT 1`, [nodeId]);
45458
45942
  if (!remaining.rowCount) {
45459
- await this.deleteNode(nodeId);
45943
+ await this.softDeleteNode(nodeId);
45460
45944
  return { nodeDeleted: true };
45461
45945
  }
45462
45946
  return { nodeDeleted: false };
45463
45947
  }
45464
- async clearDeletedRuntimesForNode(nodeId) {
45465
- await this.pool.query(`DELETE FROM ${this.s}.deleted_runtime_ids WHERE id LIKE $1`, [`runtime:${nodeId}:%`]);
45466
- }
45467
45948
  };
45468
45949
  var PostgresNodeTokenStore = class _PostgresNodeTokenStore {
45469
45950
  constructor(pool, schema) {
@@ -45521,6 +46002,7 @@ var rowToRuntime = (row) => ({
45521
46002
  // ../cli/src/serve.ts
45522
46003
  init_src3();
45523
46004
  init_trajectory();
46005
+ var DEFAULT_CHAT_SKILLS = ["build-workorder"];
45524
46006
  var oasisWrapperCache;
45525
46007
  function oasisWrapperScript() {
45526
46008
  if (oasisWrapperCache && fs13.existsSync(oasisWrapperCache)) return oasisWrapperCache;
@@ -45634,6 +46116,7 @@ async function startServe(opts) {
45634
46116
  if ((await typeStore.list()).length === 0) for (const d of schema) await typeStore.put(d);
45635
46117
  {
45636
46118
  const live = await typeStore.list();
46119
+ upgradeDefaultFieldDefs(live);
45637
46120
  schema.length = 0;
45638
46121
  schema.push(...live);
45639
46122
  }
@@ -45800,6 +46283,7 @@ async function startServe(opts) {
45800
46283
  return auth.sessionAccountId(headers);
45801
46284
  };
45802
46285
  const interventionDrafts = new InterventionDraftStore();
46286
+ const workorderDrafts = new WorkorderDraftStore();
45803
46287
  const journalFile = path11.join(opts.dir, "dispatch-journal.ndjson");
45804
46288
  const journalRing = [];
45805
46289
  if (fs13.existsSync(journalFile)) {
@@ -45880,6 +46364,15 @@ async function startServe(opts) {
45880
46364
  if (actorCtx?.config?.prompt) {
45881
46365
  Object.assign(files, splitIdentityFiles3(actorCtx.config.prompt));
45882
46366
  }
46367
+ Object.assign(files, await materializeSkillFiles({
46368
+ service: actors.service,
46369
+ blobs: assets,
46370
+ actorId: planner.id,
46371
+ runtimeKind: binding.runtimeKind
46372
+ }).catch((err) => {
46373
+ console.warn(`[planner] skill materialize failed for ${planner.id}: ${String(err)}`);
46374
+ return {};
46375
+ }));
45883
46376
  const handle = await chatRemoteAdapter.spawn({
45884
46377
  actor: planner.id,
45885
46378
  actorToken: issuer.issue(planner.id),
@@ -45927,10 +46420,10 @@ async function startServe(opts) {
45927
46420
  }),
45928
46421
  trace.register,
45929
46422
  automations.register,
45930
- ...chatSessionStore ? [createChatSessionsDomain({ store: chatSessionStore, registry: registryStore, kernel, artifactState: artifactStateStore })] : []
46423
+ ...chatSessionStore ? [createChatSessionsDomain({ store: chatSessionStore, registry: registryStore })] : []
45931
46424
  ],
45932
46425
  drafts: interventionDrafts,
45933
- ...chatSessionStore ? { chatSession: chatSessionStore } : {},
46426
+ workorderDrafts,
45934
46427
  // 暴露注册的 artifact 类型,给前端「发起工单」动态拉取根产物可选类型——schema.json 增改即自动扩展,
45935
46428
  // 前端不再写死枚举。走通用 /api/view(generic data),非域路由契约,不进 gen:api。
45936
46429
  views: {
@@ -45949,6 +46442,7 @@ async function startServe(opts) {
45949
46442
  },
45950
46443
  resolveActorEnv: async (actorId) => (await buildActorProvision(actors.service, actorId)).env,
45951
46444
  resolveActorContext: (actorId) => buildActorContext(actors.service, actorId),
46445
+ materializeSkills: (actorId, runtimeKind) => materializeSkillFiles({ service: actors.service, blobs: assets, actorId, runtimeKind }),
45952
46446
  dispatchProduce: async ({ artifactId, actorId, part }) => {
45953
46447
  if (!dispatcher) {
45954
46448
  throw new ApiError(503, "DISPATCH_NOT_ENABLED", "\u670D\u52A1\u7AEF\u672A\u542F\u7528 --dispatch true\uFF0C\u4E0D\u80FD\u62C9\u8D77\u771F\u5B9E\u667A\u80FD\u4F53\u4FEE\u6539");
@@ -45959,7 +46453,7 @@ async function startServe(opts) {
45959
46453
  ...part !== void 0 ? { part } : {}
45960
46454
  });
45961
46455
  },
45962
- dispatchChat: async ({ actorId, message, sessionId, chatSessionId }) => {
46456
+ dispatchChat: async ({ actorId, message, sessionId }) => {
45963
46457
  const binding = await actors.service.resolveBinding()(actorId);
45964
46458
  if (!binding) {
45965
46459
  throw new ApiError(409, "ACTOR_BINDING_MISSING", `\u6570\u5B57\u5458\u5DE5 ${actorId} \u6CA1\u6709 active runtime \u7ED1\u5B9A`);
@@ -46049,6 +46543,16 @@ async function startServe(opts) {
46049
46543
  if (actorCtx?.config?.prompt) {
46050
46544
  Object.assign(files, splitIdentityFiles3(actorCtx.config.prompt));
46051
46545
  }
46546
+ Object.assign(files, await materializeSkillFiles({
46547
+ service: actors.service,
46548
+ blobs: assets,
46549
+ actorId,
46550
+ runtimeKind: binding.runtimeKind,
46551
+ extraSkillIds: DEFAULT_CHAT_SKILLS
46552
+ }).catch((err) => {
46553
+ console.warn(`[chat] skill materialize failed for ${actorId}: ${String(err)}`);
46554
+ return {};
46555
+ }));
46052
46556
  const handle = await chatRemoteAdapter.spawn({
46053
46557
  actor: actorId,
46054
46558
  actorToken: issuer.issue(actorId),
@@ -46060,7 +46564,7 @@ async function startServe(opts) {
46060
46564
  server: { url: localUrl },
46061
46565
  binding,
46062
46566
  limits: { wallClockMs: 10 * 6e4 },
46063
- env: { ...provision.env, ...chatSessionId ? { OASIS_CHAT_SESSION_ID: chatSessionId } : {} },
46567
+ env: provision.env,
46064
46568
  wrapperPaths: provision.wrapperPaths
46065
46569
  });
46066
46570
  const buffered = [];
@@ -46183,11 +46687,12 @@ async function startServe(opts) {
46183
46687
  resolveBinding: actors.service.resolveBinding(),
46184
46688
  provision: (actorId) => buildActorProvision(actors.service, actorId),
46185
46689
  resolveActorContext: (actorId) => buildActorContext(actors.service, actorId),
46690
+ materializeSkills: (actorId, runtimeKind) => materializeSkillFiles({ service: actors.service, blobs: assets, actorId, runtimeKind }),
46186
46691
  schema: new Map(schema.map((d2) => [d2.name, d2])),
46187
46692
  resolveCodeRepo,
46188
46693
  // §4/C:异步从权威源(store)解析;dispatcher 装配前 await
46189
- // #2 派单开关:工单"待派发"(brief 挂未 resolve 的 OASIS_DISPATCH_HOLD 批注)时不派其活。
46190
- resolveDispatchHold: (ws) => workspaceDispatchPaused(kernel.model, [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws)),
46694
+ // #2 派单开关:工单"待派发"(控制面旁存标 held)时不派其活。held 集通常空、表极小,每次查可接受。
46695
+ resolveDispatchHold: async (ws) => (await artifactStateStore.listWorkspaceDispatchHeld()).includes(ws),
46191
46696
  tokenFor: (actor) => issuer.issue(actor),
46192
46697
  journal,
46193
46698
  // TrajectoryEvent 是唯一事件模型;trace 旁账与 M7 蒸馏库并行消费同一流(§16.D 收口)。
@@ -46343,6 +46848,8 @@ async function startServe(opts) {
46343
46848
  // 阻塞诊断(proposal D1/D3):派发器运行时信号(queued/fused/backoff/hasInFlight)注入诊断,
46344
46849
  // 让协调者把"资源 / 运行时"病因和"图病"分开——资源排队的节点不会被误唤去砍流程。
46345
46850
  runtimeSignals: (id) => dispatcher?.runtimeSignalsFor(id) ?? { hasInFlight: false },
46851
+ // 病历里展示节点的"在跑会话情况"(runId + 跑了多久):从派发器在途会话按 artifactId 过滤。
46852
+ sessionsOf: (id) => (dispatcher?.inFlightSessions() ?? []).filter((s2) => s2.artifactId === id).map((s2) => ({ runId: s2.sessionId, action: s2.action, startedAt: s2.startedAt })),
46346
46853
  log: (m2) => console.log(m2)
46347
46854
  });
46348
46855
  coordTimer = setInterval(() => {
@@ -46473,6 +46980,7 @@ var DaemonWsClient = class {
46473
46980
  ws.on("open", () => {
46474
46981
  log2("[node-cli]", `connected id=${this.daemonId}`);
46475
46982
  this.send({ type: "hello", daemonId: this.daemonId, meta: this.buildMeta() });
46983
+ this.reportRuntimes = false;
46476
46984
  this.pingTimer = setInterval(() => this.send({ type: "pong" }), PING_INTERVAL_MS);
46477
46985
  });
46478
46986
  ws.on("message", (raw) => {
@@ -46636,7 +47144,7 @@ async function startNode(opts) {
46636
47144
  claude
46637
47145
  )
46638
47146
  );
46639
- const client = new DaemonWsClient(opts.serverUrl, opts.nodeId, sessions, adapters, opts.token, runtimes, opts.nodeName, opts.reportRuntimes ?? true);
47147
+ const client = new DaemonWsClient(opts.serverUrl, opts.nodeId, sessions, adapters, opts.token, runtimes, opts.nodeName, opts.reportRuntimes ?? false);
46640
47148
  console.log(`[oasis node] ${opts.nodeId} \u2192 ${opts.serverUrl}`);
46641
47149
  client.start();
46642
47150
  await new Promise((resolve3) => {
@@ -46672,8 +47180,9 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
46672
47180
  token \u89E3\u6790\u987A\u5E8F: --token > $OASIS_TOKEN > <dir>/operator-token
46673
47181
 
46674
47182
  \u7248\u672C\u673A\uFF08\xA75\uFF09
46675
- 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>]
47183
+ 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>] [--<\u5B57\u6BB5> <\u503C>] [--set-json <json>]
46676
47184
  # --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
47185
+ # --<\u5B57\u6BB5> <\u503C>\uFF1A\u8BE5\u7C7B\u578B\u3010\u52A8\u6001\u58F0\u660E\u3011\u7684\u81EA\u5B9A\u4E49\u5B57\u6BB5\uFF08\u4E0D\u662F\u56FA\u5B9A\u6E05\u5355\uFF09\u2014\u2014\u7528 "oasis artifact-types show <type>" \u770B\u53EF\u586B\u5B57\u6BB5\uFF1Bstring[]/\u6279\u91CF\u7528 --set-json\uFF1B\u7F3A\u5FC5\u586B\u4F1A\u62A5\u9519\u5217\u51FA
46677
47186
  propose <artifactId> (--file <path> | --from-dir <path> | --git-commit <sha>) --reason <text> [--base <revisionId|none>] [--part <name>] [--rebase-of <revisionId>]
46678
47187
  # --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
46679
47188
  # --git-commit\uFF1AModel Y \u4EE3\u7801-git project\u2014\u2014\u5185\u5BB9\u6D3B\u5728 git\uFF0C\u53EA external-pin \u90A3\u4E2A commit sha\uFF08\u5148 push \u4F60\u7684 feat \u5206\u652F\uFF09\u3002
@@ -46705,8 +47214,12 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
46705
47214
  log <artifactId> # \u8BE5\u4EA7\u7269\u7684**\u64CD\u4F5C\u65E5\u5FD7**\uFF1A\u5DF2\u53D1\u751F\u7684\u5185\u6838\u4E8B\u5B9E\uFF08spawn/propose/merge/conclude\u2026\uFF09\uFF0C\u4ECE oplog \u8BFB
46706
47215
  session-log <runId|artifactId> [--n 20] # \u67D0 agent **\u4F1A\u8BDD\u7684\u8FD0\u884C\u65F6\u8F93\u51FA**\uFF08\u65C1\u8D26\uFF0C\u975E\u5185\u6838\u4E8B\u5B9E\uFF09\uFF1A\u6700\u65B0 N \u6761\u4E8B\u4EF6 + "\u8DDD\u4E0A\u6B21\u4E8B\u4EF6 X \u5206\u949F"\uFF08>10min\u2248\u5361\uFF09+ \u5728\u98DE tool_use\u2014\u2014\u5224"\u60F3 vs \u5361"\u3002\u6CE8\u610F\u2260 log\uFF1Alog \u770B\u4EA7\u7269\u7ECF\u5386\u4E86\u4EC0\u4E48\uFF0Csession-log \u770B agent \u6B64\u523B\u5728\u8BF4/\u5E72\u4EC0\u4E48
46707
47216
  artifact-types [list | search <q>] # \u770B/\u68C0\u7D22\u5DF2\u6CE8\u518C\u7C7B\u578B\uFF08\u7528\u9014/owner/\u72B6\u6001\uFF09\u2014\u2014\u7ED9\u4EBA\u548C agent \u9009 type
47217
+ artifact-types show <type> # \u770B\u5355\u7C7B\u578B\u8BE6\u60C5\uFF1A\u542B\u3010\u81EA\u5B9A\u4E49\u5B57\u6BB5\u8868\u3011(\u540D/\u7C7B\u578B/\u5FC5\u586B/\u8BF4\u660E)\u2014\u2014spawn/edit \u524D\u67E5\u80FD\u586B\u5565
46708
47218
  artifact-types add <name> (--content-type <t> --owner-role <r> | --file <json>) [--description \u2026] # \u65B0\u589E\uFF08--file \u6536\u5B8C\u6574 ArtifactTypeDef\uFF1Atriggers/reviews/grounding/budget/acl/mirror\u2026\uFF09
46709
- artifact-types edit <name> [--file <json>] [--description \u2026] [--owner-role \u2026] [--content-type \u2026] # \u6539\u5B57\u6BB5\uFF08name \u4E0D\u53EF\u6539\uFF1BcontentType \u6709\u5B58\u91CF\u5373\u62E6\uFF1B--file \u6536\u5B8C\u6574 patch\uFF09
47219
+ artifact-types edit <name> [--file <json>] [--description \u2026] [--owner-role \u2026] [--content-type \u2026] # \u6539\u7C7B\u578B\u914D\u7F6E\uFF08name \u4E0D\u53EF\u6539\uFF1BcontentType \u6709\u5B58\u91CF\u5373\u62E6\uFF1B--file \u6536\u5B8C\u6574 patch\u3002\u6539\u5B57\u6BB5\u7528 *-field\uFF09
47220
+ artifact-types add-field <type> <\u5B57\u6BB5> --type <string|string[]|number|boolean> [--required true] [--description \u2026] # \u7ED9\u7C7B\u578B\u3010\u58F0\u660E\u3011\u4E00\u4E2A\u81EA\u5B9A\u4E49\u5B57\u6BB5\uFF08\u6539 schema\u3001\u5F71\u54CD\u8BE5 type \u6240\u6709\u8282\u70B9\u7684\u672A\u6765\u5199\u5165\uFF09
47221
+ artifact-types edit-field <type> <\u5B57\u6BB5> [--type \u2026] [--required true|false] [--description \u2026] # \u6539\u5B57\u6BB5\u58F0\u660E\u7684\u5C5E\u6027\uFF08\u4E0D\u53EF\u6539\u540D\uFF1B\u524D\u5411\u751F\u6548 + \u5F71\u54CD\u534A\u5F84\uFF09
47222
+ artifact-types drop-field <type> <\u5B57\u6BB5> # \u5220\u5B57\u6BB5\u58F0\u660E\uFF08deprecate\uFF1A\u5B58\u91CF\u503C\u7559\u3001\u65B0\u5199\u5165\u62D2\uFF1B\u5E26\u5F71\u54CD\u534A\u5F84\uFF09
46710
47223
  artifact-types deprecate <name> | delete <name> # \u5F03\u7528\uFF08\u7981\u65B0\u5EFA\u3001\u5B58\u91CF\u4E0D\u52A8\uFF09/ \u786C\u5220\uFF08\u4EC5\u96F6\u5F15\u7528\uFF0C\u5426\u5219\u5F15\u5BFC deprecate\uFF09
46711
47224
 
46712
47225
  \u6CBB\u7406\uFF08\xA711\uFF09
@@ -46767,10 +47280,7 @@ function projectSlug(value) {
46767
47280
  return value.trim().toLowerCase().replace(/[^\w\u4e00-\u9fa5\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 32);
46768
47281
  }
46769
47282
  function makeClient(base, token) {
46770
- const baseHeaders = { authorization: `Bearer ${token}`, "content-type": "application/json" };
46771
- const chatSessionId = process.env["OASIS_CHAT_SESSION_ID"];
46772
- if (chatSessionId) baseHeaders["x-chat-session-id"] = chatSessionId;
46773
- const headers = baseHeaders;
47283
+ const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
46774
47284
  const fail = async (res) => {
46775
47285
  const body = await res.json().catch(() => ({}));
46776
47286
  const err = body.error;
@@ -46823,25 +47333,50 @@ async function runCli(argv, println = console.log) {
46823
47333
  const serverUrl = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"] ?? "ws://127.0.0.1:7320/node-gateway";
46824
47334
  const nodeName = flags.get("name") ?? process.env["OASIS_NODE_NAME"];
46825
47335
  const nodeIdFile = path12.join(dir, "node-id");
46826
- const storedNodeId = fs14.existsSync(nodeIdFile) ? fs14.readFileSync(nodeIdFile, "utf8").trim() : null;
46827
- const nodeId = storedNodeId ?? flags.get("id") ?? process.env["OASIS_NODE_ID"] ?? `node-${(0, import_node_crypto21.randomUUID)()}`;
46828
- if (!storedNodeId) {
47336
+ const nodeTokenFile = path12.join(dir, "node-token");
47337
+ const enrollTokenFile = path12.join(dir, "enroll-token");
47338
+ const reportMarkerFile = path12.join(dir, "report-runtimes");
47339
+ let nodeId = fs14.existsSync(nodeIdFile) ? fs14.readFileSync(nodeIdFile, "utf8").trim() : null;
47340
+ let token2 = flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? (fs14.existsSync(nodeTokenFile) ? fs14.readFileSync(nodeTokenFile, "utf8").trim() : void 0);
47341
+ const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"] ?? (fs14.existsSync(enrollTokenFile) ? fs14.readFileSync(enrollTokenFile, "utf8").trim() : void 0);
47342
+ let freshEnrollment = false;
47343
+ if ((!nodeId || !token2) && enrollToken) {
47344
+ const httpBase2 = serverUrl.replace(/^ws(s?):\/\//, "http$1://").replace(/\/node-gateway.*$/, "");
47345
+ const res = await fetch(`${httpBase2}/api/nodes/exchange`, {
47346
+ method: "POST",
47347
+ headers: { "content-type": "application/json" },
47348
+ body: JSON.stringify({ enrollToken, ...nodeId ? { nodeId } : {}, hostname: os7.hostname() })
47349
+ });
47350
+ if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
47351
+ const body = await res.json();
47352
+ nodeId = body.nodeId;
47353
+ token2 = body.token;
46829
47354
  fs14.mkdirSync(dir, { recursive: true });
46830
47355
  fs14.writeFileSync(nodeIdFile, nodeId);
47356
+ fs14.writeFileSync(nodeTokenFile, token2, { mode: 384 });
47357
+ try {
47358
+ fs14.unlinkSync(enrollTokenFile);
47359
+ } catch {
47360
+ }
47361
+ freshEnrollment = true;
46831
47362
  }
46832
- const nodeTokenFile = path12.join(dir, "node-token");
46833
- const token2 = flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? (fs14.existsSync(nodeTokenFile) ? fs14.readFileSync(nodeTokenFile, "utf8").trim() : void 0);
47363
+ if (!nodeId) throw new Error("no nodeId \u2014 provide --enroll-token <ent_...> on first run");
47364
+ const scriptRun = freshEnrollment || fs14.existsSync(reportMarkerFile);
46834
47365
  if (flags.has("service")) {
46835
47366
  const { spawnSync, execSync: execSync2 } = await import("node:child_process");
46836
47367
  if (token2) {
46837
47368
  fs14.mkdirSync(dir, { recursive: true });
46838
47369
  fs14.writeFileSync(nodeTokenFile, token2, { mode: 384 });
46839
47370
  }
47371
+ if (scriptRun) {
47372
+ fs14.mkdirSync(dir, { recursive: true });
47373
+ fs14.writeFileSync(reportMarkerFile, "");
47374
+ }
46840
47375
  println("\u2192 Installing oasis_test globally...");
46841
47376
  spawnSync("npm", ["install", "-g", "oasis_test@latest"], { stdio: "inherit" });
46842
47377
  const prefix = execSync2("npm prefix -g").toString().trim();
46843
47378
  const binPath = path12.join(prefix, "bin", "oasis_test");
46844
- const svcArgs = `node --server-ws ${serverUrl} --id ${nodeId} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
47379
+ const svcArgs = `node --server-ws ${serverUrl} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
46845
47380
  if (process.platform === "linux") {
46846
47381
  const svcFile = path12.join(os7.homedir(), ".config/systemd/user/oasis-node.service");
46847
47382
  fs14.mkdirSync(path12.dirname(svcFile), { recursive: true });
@@ -46854,6 +47389,9 @@ async function runCli(argv, println = console.log) {
46854
47389
  "",
46855
47390
  "[Service]",
46856
47391
  `ExecStart=${binPath} ${svcArgs}`,
47392
+ // Carry the install-time PATH so the detached daemon detects CLIs in user dirs
47393
+ // (~/.npm-global/bin, ~/.local/bin) — systemd --user otherwise gives a minimal PATH.
47394
+ `Environment=PATH=${process.env["PATH"] ?? ""}:${path12.join(os7.homedir(), ".npm-global/bin")}:${path12.join(os7.homedir(), ".local/bin")}:/usr/local/bin`,
46857
47395
  "Restart=on-failure",
46858
47396
  "RestartSec=5",
46859
47397
  "StandardOutput=journal",
@@ -46874,7 +47412,7 @@ async function runCli(argv, println = console.log) {
46874
47412
  const plist = path12.join(os7.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
46875
47413
  fs14.mkdirSync(path12.dirname(plist), { recursive: true });
46876
47414
  spawnSync("launchctl", ["unload", plist], { stdio: "ignore" });
46877
- const argXml = [binPath, "node", "--server-ws", serverUrl, "--id", nodeId, "--dir", dir, ...nodeName ? ["--name", nodeName] : []].map((a) => `<string>${a}</string>`).join("");
47415
+ const argXml = [binPath, "node", "--server-ws", serverUrl, "--dir", dir, ...nodeName ? ["--name", nodeName] : []].map((a) => `<string>${a}</string>`).join("");
46878
47416
  fs14.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>`);
46879
47417
  spawnSync("launchctl", ["load", plist], { stdio: "inherit" });
46880
47418
  println("\u2713 Oasis node service started (macOS LaunchAgent com.oasis.node)");
@@ -46887,7 +47425,13 @@ async function runCli(argv, println = console.log) {
46887
47425
  }
46888
47426
  return;
46889
47427
  }
46890
- await startNode({ serverUrl, nodeId, ...token2 ? { token: token2 } : {}, ...nodeName ? { nodeName } : {} });
47428
+ if (fs14.existsSync(reportMarkerFile)) {
47429
+ try {
47430
+ fs14.unlinkSync(reportMarkerFile);
47431
+ } catch {
47432
+ }
47433
+ }
47434
+ await startNode({ serverUrl, nodeId, ...token2 ? { token: token2 } : {}, ...nodeName ? { nodeName } : {}, reportRuntimes: scriptRun });
46891
47435
  return;
46892
47436
  }
46893
47437
  if (command === "node-token") {
@@ -47029,6 +47573,30 @@ async function runCli(argv, println = console.log) {
47029
47573
  return out;
47030
47574
  };
47031
47575
  const STAGE_COMMANDS = /* @__PURE__ */ new Set(["link", "spawn", "unlink", "cancel", "freeze", "cancel-part", "edit"]);
47576
+ const GLOBAL_NON_FIELD_FLAGS = /* @__PURE__ */ new Set(["server", "token", "dir", "set-json", "clear"]);
47577
+ const SPAWN_OWN_FLAGS = /* @__PURE__ */ new Set(["type", "owner", "workspace", "persistence", "title", "brief", "description", "input", "parts-file"]);
47578
+ const EDIT_OWN_FLAGS = /* @__PURE__ */ new Set(["title", "brief"]);
47579
+ const fieldsFromFlags = (ownFlags) => {
47580
+ const fields = {};
47581
+ for (const [k2, v2] of flags) {
47582
+ if (ownFlags.has(k2) || GLOBAL_NON_FIELD_FLAGS.has(k2)) continue;
47583
+ fields[k2] = v2;
47584
+ }
47585
+ const sj = flags.get("set-json");
47586
+ if (sj !== void 0) {
47587
+ const parsed = JSON.parse(sj);
47588
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
47589
+ throw new Error(`--set-json \u9700\u8981\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u5982 '{"goal":"..."}'`);
47590
+ }
47591
+ Object.assign(fields, parsed);
47592
+ }
47593
+ const clear = flags.get("clear");
47594
+ if (clear !== void 0) for (const k2 of clear.split(",")) {
47595
+ const t = k2.trim();
47596
+ if (t) fields[t] = null;
47597
+ }
47598
+ return Object.keys(fields).length > 0 ? fields : void 0;
47599
+ };
47032
47600
  const buildStageOp = (cmd) => {
47033
47601
  switch (cmd) {
47034
47602
  case "link":
@@ -47038,15 +47606,18 @@ async function runCli(argv, println = console.log) {
47038
47606
  to: need(flags, "to"),
47039
47607
  ...flags.get("required") !== void 0 ? { required: flags.get("required") === "true" } : {}
47040
47608
  };
47041
- case "spawn":
47609
+ case "spawn": {
47610
+ const fields = fieldsFromFlags(SPAWN_OWN_FLAGS);
47042
47611
  return {
47043
47612
  action: "spawn",
47044
47613
  id: needPos(positional, 0, "oasis spawn <id> --type <t>"),
47045
47614
  type: need(flags, "type"),
47046
47615
  ...flags.get("title") !== void 0 ? { title: flags.get("title") } : {},
47047
47616
  ...(flags.get("brief") ?? flags.get("description")) !== void 0 ? { description: flags.get("brief") ?? flags.get("description") } : {},
47048
- ...flags.get("input") !== void 0 ? { inputs: flags.get("input").split(",").map((to) => ({ to: to.trim() })) } : {}
47617
+ ...flags.get("input") !== void 0 ? { inputs: flags.get("input").split(",").map((to) => ({ to: to.trim() })) } : {},
47618
+ ...fields !== void 0 ? { fields } : {}
47049
47619
  };
47620
+ }
47050
47621
  case "unlink":
47051
47622
  return { action: "unlink", artifactId: needPos(positional, 0, "oasis unlink <consumer> --to <upstream>"), to: need(flags, "to") };
47052
47623
  case "cancel":
@@ -47059,13 +47630,16 @@ async function runCli(argv, println = console.log) {
47059
47630
  };
47060
47631
  case "cancel-part":
47061
47632
  return { action: "cancelPart", artifactId: needPos(positional, 0, "oasis cancel-part <id> --part <name>"), part: need(flags, "part") };
47062
- case "edit":
47633
+ case "edit": {
47634
+ const fields = fieldsFromFlags(EDIT_OWN_FLAGS);
47063
47635
  return {
47064
47636
  action: "edit",
47065
- artifactId: needPos(positional, 0, "oasis edit <id> [--title \u2026] [--brief \u2026]"),
47637
+ artifactId: needPos(positional, 0, "oasis edit <id> [--title \u2026] [--brief \u2026] [--<\u5B57\u6BB5> <\u503C>] [--set-json <json>] [--clear <\u5B57\u6BB5,\u2026>]\uFF08--<\u5B57\u6BB5> \u662F\u8BE5\u7C7B\u578B\u52A8\u6001\u58F0\u660E\u7684\uFF0C\u7528 oasis show <id> / artifact-types show <type> \u770B\u53EF\u586B\u5B57\u6BB5\uFF09"),
47066
47638
  ...flags.get("title") !== void 0 ? { title: flags.get("title") } : {},
47067
- ...flags.get("brief") !== void 0 ? { description: flags.get("brief") } : {}
47639
+ ...flags.get("brief") !== void 0 ? { description: flags.get("brief") } : {},
47640
+ ...fields !== void 0 ? { fields } : {}
47068
47641
  };
47642
+ }
47069
47643
  default:
47070
47644
  throw new Error(`\u672A\u77E5\u6682\u5B58\u547D\u4EE4: ${cmd}`);
47071
47645
  }
@@ -47099,43 +47673,28 @@ async function runCli(argv, println = console.log) {
47099
47673
  return;
47100
47674
  }
47101
47675
  switch (command) {
47102
- case "workorders": {
47103
- const sessionId = flags.get("session");
47104
- if (sessionId) {
47105
- const res = await api.request(
47106
- "GET",
47107
- `/api/chat-sessions/${encodeURIComponent(sessionId)}/work-orders`
47108
- );
47109
- println(`\u4F1A\u8BDD ${sessionId} \u7684\u5DE5\u5355\uFF08${res.items.length}\uFF09\uFF1A`);
47110
- for (const w2 of res.items) {
47111
- const p2 = w2.progress ? ` [${w2.progress.concluded}/${w2.progress.total}]` : "";
47112
- println(` ${w2.id} ${w2.title} [${w2.stage}]${p2}`);
47113
- }
47114
- } else {
47115
- const res = await api.request(
47116
- "GET",
47117
- "/api/workorders"
47118
- );
47119
- println(`\u5DE5\u5355\uFF08${res.items.length}\uFF09\uFF1A`);
47120
- for (const w2 of res.items) {
47121
- const p2 = w2.progress ? ` [${w2.progress.concluded}/${w2.progress.total}]` : "";
47122
- println(` ${w2.id} ${w2.title} [${w2.stage}]${p2}`);
47123
- }
47124
- }
47125
- break;
47126
- }
47127
- case "artifacts": {
47128
- const workOrderId = positional[0];
47129
- if (!workOrderId) {
47130
- println("\u7528\u6CD5: oasis artifacts <workOrderId>");
47131
- break;
47676
+ case "create-workorder": {
47677
+ const planRaw = flags.get("plan-file") !== void 0 ? fs14.readFileSync(flags.get("plan-file"), "utf8") : flags.get("plan");
47678
+ const acceptanceFlag = flags.get("acceptance");
47679
+ const args = {
47680
+ createdVia: "seed",
47681
+ stage: true,
47682
+ type: need(flags, "type"),
47683
+ workspace: need(flags, "workspace"),
47684
+ title: need(flags, "title"),
47685
+ ...flags.get("brief") !== void 0 ? { brief: flags.get("brief") } : {},
47686
+ ...flags.get("goal") !== void 0 ? { goal: flags.get("goal") } : {},
47687
+ ...acceptanceFlag !== void 0 ? { acceptance_criteria: parseStringArrayFlag(acceptanceFlag, "acceptance") } : {},
47688
+ ...planRaw !== void 0 ? { plan: parseJsonFlag(planRaw, "plan") } : {},
47689
+ ...flags.get("project") !== void 0 ? { project_id: flags.get("project") } : {},
47690
+ ...flags.get("dispatch") === "false" ? { dispatch: false } : {}
47691
+ };
47692
+ const res = await api.cmd("spawn", args);
47693
+ println(res.message);
47694
+ const data = res.data;
47695
+ if (data?.draftId) {
47696
+ println(`draftId=${data.draftId} workspace=${data.workspace} \u8282\u70B9=${data.spawned?.length ?? 0} \u89C4\u5212=${data.planning?.status ?? "?"}`);
47132
47697
  }
47133
- const res = await api.request(
47134
- "GET",
47135
- `/api/workorders/${encodeURIComponent(workOrderId)}`
47136
- );
47137
- println(`\u5DE5\u5355 ${res.summary.title} \u7684\u4EA7\u7269\uFF08${res.graph.nodes.length}\uFF09\uFF1A`);
47138
- for (const n of res.graph.nodes) println(` ${n.id} [${n.type}] ${n.label} stage=${n.stage}`);
47139
47698
  break;
47140
47699
  }
47141
47700
  case "projects": {
@@ -47326,6 +47885,7 @@ async function runCli(argv, println = console.log) {
47326
47885
  }
47327
47886
  case "spawn": {
47328
47887
  const partsFile = flags.get("parts-file");
47888
+ const spawnFields = fieldsFromFlags(SPAWN_OWN_FLAGS);
47329
47889
  const { message } = await api.cmd("spawn", {
47330
47890
  id: positional[0],
47331
47891
  type: need(flags, "type"),
@@ -47335,7 +47895,8 @@ async function runCli(argv, println = console.log) {
47335
47895
  title: flags.get("title"),
47336
47896
  description: flags.get("brief") ?? flags.get("description"),
47337
47897
  inputs: flags.get("input")?.split(",").map((to) => ({ to: to.trim() })),
47338
- ...partsFile !== void 0 ? { parts: JSON.parse(readFileArg(partsFile)) } : {}
47898
+ ...partsFile !== void 0 ? { parts: JSON.parse(readFileArg(partsFile)) } : {},
47899
+ ...spawnFields !== void 0 ? { fields: spawnFields } : {}
47339
47900
  });
47340
47901
  println(message);
47341
47902
  break;
@@ -47787,6 +48348,52 @@ async function runCli(argv, println = console.log) {
47787
48348
  println(`\u5DF2\u5220\u9664 ${name}`);
47788
48349
  break;
47789
48350
  }
48351
+ if (sub === "add-field") {
48352
+ const name = needPos(positional, 1, "oasis artifact-types add-field <type> <\u5B57\u6BB5> --type <string|string[]|number|boolean> [--required true] [--description \u2026]");
48353
+ const field = needPos(positional, 2, "oasis artifact-types add-field <type> <\u5B57\u6BB5> --type <t> [--required true] [--description \u2026]");
48354
+ const spec = {
48355
+ name: field,
48356
+ type: need(flags, "type"),
48357
+ ...flags.get("required") === "true" ? { required: true } : {},
48358
+ ...flags.get("description") !== void 0 ? { description: flags.get("description") } : {}
48359
+ };
48360
+ const r = await api.request("POST", `/api/artifact-types/${encodeURIComponent(name)}/fields`, spec);
48361
+ println(`\u5DF2\u7ED9 ${name} \u52A0\u5B57\u6BB5 ${field}\uFF08${spec.type}${spec["required"] ? "\uFF0C\u5FC5\u586B" : ""}\uFF09${r.usage && r.usage.total > 0 ? `\u2014\u2014${r.usage.total} \u4E2A\u5B58\u91CF\u8282\u70B9\u6682\u65E0\u6B64\u5B57\u6BB5\uFF08\u524D\u5411\uFF1A\u65B0\u5199\u5165\u53EF\u586B\uFF09` : ""}`);
48362
+ break;
48363
+ }
48364
+ if (sub === "edit-field") {
48365
+ const name = needPos(positional, 1, "oasis artifact-types edit-field <type> <\u5B57\u6BB5> [--type \u2026] [--required true|false] [--description \u2026]\uFF08\u4E0D\u53EF\u6539\u540D\uFF09");
48366
+ const field = needPos(positional, 2, "oasis artifact-types edit-field <type> <\u5B57\u6BB5> [--type \u2026] [--required \u2026] [--description \u2026]");
48367
+ const attrs = {
48368
+ ...flags.get("type") !== void 0 ? { type: flags.get("type") } : {},
48369
+ ...flags.get("required") !== void 0 ? { required: flags.get("required") === "true" } : {},
48370
+ ...flags.get("description") !== void 0 ? { description: flags.get("description") } : {}
48371
+ };
48372
+ const r = await api.request("PATCH", `/api/artifact-types/${encodeURIComponent(name)}/fields/${encodeURIComponent(field)}`, attrs);
48373
+ println(`\u5DF2\u6539 ${name}.${field}${r.usage && r.usage.total > 0 ? `\uFF08\u524D\u5411\u751F\u6548\uFF0C\u5F71\u54CD ${r.usage.total} \u4E2A\u5B58\u91CF\u8282\u70B9\uFF09` : ""}`);
48374
+ break;
48375
+ }
48376
+ if (sub === "drop-field") {
48377
+ const name = needPos(positional, 1, "oasis artifact-types drop-field <type> <\u5B57\u6BB5>");
48378
+ const field = needPos(positional, 2, "oasis artifact-types drop-field <type> <\u5B57\u6BB5>");
48379
+ const r = await api.request("DELETE", `/api/artifact-types/${encodeURIComponent(name)}/fields/${encodeURIComponent(field)}`, void 0);
48380
+ println(`\u5DF2\u5220\u5B57\u6BB5\u58F0\u660E ${name}.${field}${r.usage && r.usage.total > 0 ? `\u2014\u2014${r.usage.total} \u4E2A\u5B58\u91CF\u8282\u70B9\u4ECD\u4FDD\u7559\u8BE5\u5B57\u6BB5\u7684\u503C\uFF08\u6570\u636E\u672A\u52A8\uFF1B\u65B0\u5199\u5165\u5C06\u62D2\uFF09` : ""}`);
48381
+ break;
48382
+ }
48383
+ if (sub === "show") {
48384
+ const name = needPos(positional, 1, "oasis artifact-types show <type>");
48385
+ const { items } = await api.request("GET", "/api/artifact-types");
48386
+ const t = items.find((x2) => x2.name === name);
48387
+ if (!t) {
48388
+ println(`\uFF08\u65E0\u6B64\u7C7B\u578B: ${name}\uFF09`);
48389
+ break;
48390
+ }
48391
+ println(`${t.name}${t.status === "deprecated" ? " [\u5DF2\u5F03\u7528]" : ""} (${t.contentType}, owner=${t.ownerRole})${t.description ? ` \u2014\u2014 ${t.description}` : ""}`);
48392
+ const fds = t.fieldDefs ?? [];
48393
+ println(fds.length === 0 ? " \u81EA\u5B9A\u4E49\u5B57\u6BB5\uFF1A\u65E0" : " \u81EA\u5B9A\u4E49\u5B57\u6BB5\uFF1A");
48394
+ for (const f2 of fds) println(` ${f2.name} ${f2.type} ${f2.required ? "\u5FC5\u586B" : "\u53EF\u9009"}${f2.description ? ` ${f2.description}` : ""}`);
48395
+ break;
48396
+ }
47790
48397
  const all = await api.view(
47791
48398
  "artifact-types"
47792
48399
  );
@@ -47878,7 +48485,7 @@ var isRunning = (pid) => {
47878
48485
  return false;
47879
48486
  }
47880
48487
  };
47881
- var httpBase = (wsUrl) => wsUrl.replace(/^wss?:\/\//, "http://").replace(/\/node-gateway.*$/, "");
48488
+ var httpBase = (wsUrl) => wsUrl.replace(/^ws(s?):\/\//, "http$1://").replace(/\/node-gateway.*$/, "");
47882
48489
  function installBinary() {
47883
48490
  fs15.mkdirSync(path13.dirname(BIN_FILE), { recursive: true });
47884
48491
  fs15.copyFileSync(process.argv[1], BIN_FILE);
@@ -47889,6 +48496,13 @@ exec node "${BIN_FILE}" "$@"
47889
48496
  }
47890
48497
  function setupAutostart() {
47891
48498
  const cmd = `${process.execPath} "${BIN_FILE}" --_daemon`;
48499
+ const home = os8.homedir();
48500
+ const extraBins = [
48501
+ path13.join(home, ".npm-global", "bin"),
48502
+ path13.join(home, ".local", "bin"),
48503
+ "/usr/local/bin"
48504
+ ];
48505
+ const daemonPath = [process.env["PATH"] ?? "", ...extraBins].filter(Boolean).join(":");
47892
48506
  if (process.platform === "linux") {
47893
48507
  const unitDir = path13.join(os8.homedir(), ".config", "systemd", "user");
47894
48508
  fs15.mkdirSync(unitDir, { recursive: true });
@@ -47899,6 +48513,7 @@ function setupAutostart() {
47899
48513
  "",
47900
48514
  "[Service]",
47901
48515
  `ExecStart=${cmd}`,
48516
+ `Environment=PATH=${daemonPath}`,
47902
48517
  `StandardOutput=append:${LOG_FILE}`,
47903
48518
  `StandardError=append:${LOG_FILE}`,
47904
48519
  "Restart=on-failure",
@@ -47920,6 +48535,7 @@ function setupAutostart() {
47920
48535
  <plist version="1.0"><dict>
47921
48536
  <key>Label</key><string>com.oasis.node</string>
47922
48537
  <key>ProgramArguments</key><array><string>/bin/sh</string><string>-c</string><string>${cmd}</string></array>
48538
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${daemonPath}</string></dict>
47923
48539
  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
47924
48540
  <key>StandardOutPath</key><string>${LOG_FILE}</string>
47925
48541
  <key>StandardErrorPath</key><string>${LOG_FILE}</string>
@@ -48001,8 +48617,11 @@ void (async () => {
48001
48617
  await startNode({
48002
48618
  serverUrl: cfg.serverUrl,
48003
48619
  token: cfg.token,
48004
- nodeId: cfg.nodeId ?? `node-${(0, import_node_crypto22.randomUUID)()}`,
48005
- nodeName: cfg.name ?? cfg.nodeId ?? `node-${(0, import_node_crypto22.randomUUID)()}`,
48620
+ nodeId: cfg.nodeId ?? (() => {
48621
+ console.error("[oasis] no nodeId in config \u2014 run `oasis start` first");
48622
+ process.exit(1);
48623
+ })(),
48624
+ nodeName: cfg.name ?? cfg.nodeId,
48006
48625
  reportRuntimes
48007
48626
  });
48008
48627
  return;
@@ -48011,32 +48630,29 @@ void (async () => {
48011
48630
  const prev = readConfig();
48012
48631
  const serverUrl = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"] ?? prev?.serverUrl;
48013
48632
  if (!serverUrl) {
48014
- console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--id <nodeId>]");
48633
+ console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>]");
48015
48634
  process.exit(1);
48016
48635
  }
48017
- const commandNodeId = flags.get("id");
48018
48636
  const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
48019
- let nodeId = serverChanged ? commandNodeId ?? `node-${(0, import_node_crypto22.randomUUID)()}` : prev?.nodeId ?? commandNodeId ?? `node-${(0, import_node_crypto22.randomUUID)()}`;
48637
+ let nodeId = serverChanged ? void 0 : prev?.nodeId;
48020
48638
  let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
48021
48639
  const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"];
48022
- if (enrollToken) {
48023
- const exchange = async (candidateNodeId) => fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
48640
+ if (prev?.nodeId && prev?.token && !serverChanged) {
48641
+ nodeId = prev.nodeId;
48642
+ token = prev.token;
48643
+ } else if (enrollToken) {
48644
+ const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
48024
48645
  method: "POST",
48025
48646
  headers: { "content-type": "application/json" },
48026
- body: JSON.stringify({ enrollToken, nodeId: candidateNodeId })
48647
+ body: JSON.stringify({ enrollToken, hostname: os8.hostname() })
48027
48648
  });
48028
- let res = await exchange(nodeId);
48029
- if (!res.ok && commandNodeId && commandNodeId !== nodeId) {
48030
- res = await exchange(commandNodeId);
48031
- if (res.ok) nodeId = commandNodeId;
48032
- }
48033
48649
  if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
48034
48650
  const body = await res.json();
48035
48651
  token = body.token;
48036
- nodeId = body.nodeId || nodeId;
48652
+ nodeId = body.nodeId;
48037
48653
  }
48038
- if (!token) {
48039
- console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--id <nodeId>]");
48654
+ if (!token || !nodeId) {
48655
+ console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>]");
48040
48656
  process.exit(1);
48041
48657
  }
48042
48658
  const name = flags.get("name") ?? prev?.name;