oasis_test 0.1.11 → 0.1.12

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 +1077 -549
  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,26 @@ 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
+ var init_node_identity = __esm({
622
+ "../core/src/node-identity.ts"() {
623
+ "use strict";
624
+ }
625
+ });
626
+
604
627
  // ../core/src/content-handler.ts
605
628
  function defaultHandlersByContentType() {
606
629
  return /* @__PURE__ */ new Map([
@@ -678,7 +701,7 @@ var init_content_type_handler = __esm({
678
701
  const parts = ctx.queuedPartLabels.map((p2) => `\`${p2}\``).join("\u3001") || "\uFF08\u961F\u5217\u4E3A\u7A7A\uFF09";
679
702
  return [
680
703
  `\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`,
704
+ `\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
705
  `\`oasis integrate ${ctx.artifactId} --from-dir deliverable/ --reason "<\u96C6\u6210\u8BF4\u660E>"\` \u2192 \`oasis conclude ${ctx.artifactId}\`\u3002`,
683
706
  ``,
684
707
  `**\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 +752,7 @@ var init_content_type_handler = __esm({
729
752
  `\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
753
  `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
754
  `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`,
755
+ `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
756
  ``,
734
757
  `**\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
758
  ];
@@ -1248,6 +1271,48 @@ var init_grounding = __esm({
1248
1271
  });
1249
1272
 
1250
1273
  // ../core/src/kernel.ts
1274
+ function fieldValueMatches(val, type) {
1275
+ switch (type) {
1276
+ case "string":
1277
+ return typeof val === "string";
1278
+ case "number":
1279
+ return typeof val === "number";
1280
+ case "boolean":
1281
+ return typeof val === "boolean";
1282
+ case "string[]":
1283
+ return Array.isArray(val) && val.every((x2) => typeof x2 === "string");
1284
+ }
1285
+ }
1286
+ function coerceFieldValue(key, val, type) {
1287
+ if (fieldValueMatches(val, type)) return val;
1288
+ if (typeof val === "string") {
1289
+ if (type === "number" && val.trim() !== "" && !Number.isNaN(Number(val))) return Number(val);
1290
+ if (type === "boolean" && (val === "true" || val === "false")) return val === "true";
1291
+ }
1292
+ throw new KernelError(`\u5B57\u6BB5 '${key}' \u7C7B\u578B\u5E94\u4E3A ${type}${type === "string[]" ? "\uFF08\u6570\u7EC4\u503C\u8BF7\u7528 --set-json\uFF09" : ""}`);
1293
+ }
1294
+ function validateArtifactFields(fields, specs, mode) {
1295
+ const declared = new Map((specs ?? []).map((s2) => [s2.name, s2]));
1296
+ let out;
1297
+ if (fields !== void 0) {
1298
+ out = {};
1299
+ for (const [key, val] of Object.entries(fields)) {
1300
+ const spec = declared.get(key);
1301
+ if (!spec) {
1302
+ 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`);
1303
+ }
1304
+ out[key] = val === null ? null : coerceFieldValue(key, val, spec.type);
1305
+ }
1306
+ }
1307
+ if (mode === "spawn") {
1308
+ for (const spec of specs ?? []) {
1309
+ if (spec.required && (fields?.[spec.name] === void 0 || fields[spec.name] === null)) {
1310
+ throw new KernelError(`\u7F3A\u5FC5\u586B\u5B57\u6BB5 '${spec.name}'\uFF08${spec.type}\uFF09`);
1311
+ }
1312
+ }
1313
+ }
1314
+ return out;
1315
+ }
1251
1316
  var import_node_crypto, SYSTEM_ACTOR, MAX_PREREQ_DEPTH, KernelError, FrozenError, Kernel;
1252
1317
  var init_kernel = __esm({
1253
1318
  "../core/src/kernel.ts"() {
@@ -1350,6 +1415,7 @@ var init_kernel = __esm({
1350
1415
  );
1351
1416
  }
1352
1417
  this.enforceAcl("spawn", typeDef, args.actor);
1418
+ const fields = this.schema.size > 0 ? validateArtifactFields(args.fields, typeDef?.fieldDefs, "spawn") : args.fields;
1353
1419
  const inputs = [];
1354
1420
  for (const edge of args.inputs ?? []) {
1355
1421
  if (edge.to === args.id) throw new KernelError("\u4E0D\u80FD\u4F9D\u8D56\u81EA\u5DF1");
@@ -1378,7 +1444,8 @@ var init_kernel = __esm({
1378
1444
  ...args.title !== void 0 ? { title: args.title } : {},
1379
1445
  ...args.description !== void 0 ? { description: args.description } : {},
1380
1446
  ...args.docType !== void 0 ? { docType: args.docType } : {},
1381
- ...args.parts !== void 0 ? { parts: args.parts } : {}
1447
+ ...args.parts !== void 0 ? { parts: args.parts } : {},
1448
+ ...fields !== void 0 ? { fields } : {}
1382
1449
  };
1383
1450
  await this.commit(args.id, args.actor, "spawn_artifact", args.id, payload);
1384
1451
  return this.model.artifacts.get(args.id);
@@ -2206,6 +2273,7 @@ var init_kernel = __esm({
2206
2273
  case "spawn": {
2207
2274
  const typeDef = this.schema.get(op.type);
2208
2275
  if (this.schema.size > 0 && !typeDef) throw new KernelError(`unknown ArtifactType: ${op.type}`);
2276
+ const spawnFields = this.schema.size > 0 ? validateArtifactFields(op.fields, typeDef?.fieldDefs, "spawn") : op.fields;
2209
2277
  const owner = op.owner ?? this.resolveOwner(op.type, typeDef);
2210
2278
  return {
2211
2279
  artifactId: op.id,
@@ -2220,6 +2288,7 @@ var init_kernel = __esm({
2220
2288
  ...op.title !== void 0 ? { title: op.title } : {},
2221
2289
  ...op.description !== void 0 ? { description: op.description } : {},
2222
2290
  ...op.docType !== void 0 ? { docType: op.docType } : {},
2291
+ ...spawnFields !== void 0 ? { fields: spawnFields } : {},
2223
2292
  inputs: (op.inputs ?? []).map((e) => ({
2224
2293
  to: e.to,
2225
2294
  pinned: concludedHead(this.model, e.to),
@@ -2254,7 +2323,12 @@ var init_kernel = __esm({
2254
2323
  ...op.blocks !== void 0 ? { blocks: op.blocks } : {}
2255
2324
  }
2256
2325
  };
2257
- case "edit":
2326
+ case "edit": {
2327
+ let editFields = op.fields;
2328
+ if (this.schema.size > 0 && op.fields !== void 0) {
2329
+ const a = this.model.artifacts.get(op.artifactId);
2330
+ editFields = validateArtifactFields(op.fields, a ? this.schema.get(a.type)?.fieldDefs : void 0, "edit");
2331
+ }
2258
2332
  return {
2259
2333
  artifactId: op.artifactId,
2260
2334
  kind: "edit_artifact",
@@ -2262,9 +2336,11 @@ var init_kernel = __esm({
2262
2336
  payload: {
2263
2337
  ...op.title !== void 0 ? { title: op.title } : {},
2264
2338
  ...op.description !== void 0 ? { description: op.description } : {},
2265
- ...op.docType !== void 0 ? { docType: op.docType } : {}
2339
+ ...op.docType !== void 0 ? { docType: op.docType } : {},
2340
+ ...editFields !== void 0 ? { fields: editFields } : {}
2266
2341
  }
2267
2342
  };
2343
+ }
2268
2344
  }
2269
2345
  });
2270
2346
  }
@@ -2500,7 +2576,7 @@ function commandReference(opts) {
2500
2576
  const write = [`**\u5199**`];
2501
2577
  if (opts.action === "integrate") {
2502
2578
  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`
2579
+ 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
2580
  );
2505
2581
  } else {
2506
2582
  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 +2586,15 @@ function commandReference(opts) {
2510
2586
  }
2511
2587
  write.push(
2512
2588
  `- \`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`,
2589
+ `- \`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
2590
  `- \`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
2591
  `- \`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
2592
  );
2517
2593
  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`);
2594
+ 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
2595
  }
2520
2596
  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`);
2597
+ 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
2598
  }
2523
2599
  return [`## \u4F60\u80FD\u7528\u7684 oasis \u547D\u4EE4\uFF08\u672C\u6B21\uFF09`, ...write, ``, ...readFull];
2524
2600
  }
@@ -2672,6 +2748,10 @@ async function assembleContext(args) {
2672
2748
  }
2673
2749
  }
2674
2750
  }
2751
+ if (args.materializeSkills) {
2752
+ const skillFiles = await args.materializeSkills().catch(() => ({}));
2753
+ for (const [p2, c] of Object.entries(skillFiles)) files[p2] = c;
2754
+ }
2675
2755
  const wsCodeRepo = args.codeRepo ?? null;
2676
2756
  const isCodeArtifact = typeDef?.contentType === "code" && !!wsCodeRepo?.repos?.some((r) => r.remote);
2677
2757
  files["TASK.md"] = [
@@ -2935,6 +3015,8 @@ var init_dispatcher = __esm({
2935
3015
  inFlight = /* @__PURE__ */ new Map();
2936
3016
  /** v1 调度准入:在途会话的 actor(jobKey → actorId),用于每 agent 并发计数。与 inFlight 同生同灭。 */
2937
3017
  inFlightActor = /* @__PURE__ */ new Map();
3018
+ /** 在途会话的派发时刻(jobKey → ms),算"已跑多久"用(协调者病历的会话情况)。与 inFlight 同生同灭。 */
3019
+ inFlightStartedAt = /* @__PURE__ */ new Map();
2938
3020
  strikes = /* @__PURE__ */ new Map();
2939
3021
  /** v1 调度准入:上一轮 tick 中"够格但被并发上限挡下"的 produce(隐式队列;下个 tick 重评)。
2940
3022
  * jobKey → 撞的是哪道闸(global 全局总量 / per-actor owner 自己到上限)+ 该 actor——
@@ -2979,8 +3061,9 @@ var init_dispatcher = __esm({
2979
3061
  artifactId: this.artifactIdFromJobKey(jobKey),
2980
3062
  sessionId: handle.id,
2981
3063
  actor: this.inFlightActor.get(jobKey) ?? "",
2982
- action: jobKey.slice(0, jobKey.indexOf("::"))
3064
+ action: jobKey.slice(0, jobKey.indexOf("::")),
2983
3065
  // produce / review / integrate
3066
+ startedAt: new Date(this.inFlightStartedAt.get(jobKey) ?? Date.now()).toISOString()
2984
3067
  });
2985
3068
  }
2986
3069
  return out;
@@ -3129,6 +3212,8 @@ var init_dispatcher = __esm({
3129
3212
  const lastFailure = retry && retry.count > 0 && retry.lastReason ? { reason: retry.lastReason } : void 0;
3130
3213
  const ws = kernel.model.artifacts.get(spec.artifactId)?.workspace;
3131
3214
  const codeRepo = ws ? await this.opts.resolveCodeRepo?.(ws) ?? null : null;
3215
+ const binding = await this.opts.resolveBinding?.(spec.actor) ?? void 0;
3216
+ const materializeSkills = this.opts.materializeSkills ? () => this.opts.materializeSkills(spec.actor, binding?.runtimeKind ?? "claude") : void 0;
3132
3217
  const bundle = await assembleContext({
3133
3218
  model: kernel.model,
3134
3219
  blobs: this.opts.blobs,
@@ -3140,9 +3225,9 @@ var init_dispatcher = __esm({
3140
3225
  ...lastFailure ? { lastFailure } : {},
3141
3226
  ...this.opts.schema !== void 0 ? { schema: this.opts.schema } : {},
3142
3227
  ...codeRepo ? { codeRepo } : {},
3143
- ...this.opts.resolveActorContext !== void 0 ? { resolveActorContext: this.opts.resolveActorContext } : {}
3228
+ ...this.opts.resolveActorContext !== void 0 ? { resolveActorContext: this.opts.resolveActorContext } : {},
3229
+ ...materializeSkills ? { materializeSkills } : {}
3144
3230
  });
3145
- const binding = await this.opts.resolveBinding?.(spec.actor) ?? void 0;
3146
3231
  const provisioned = this.opts.provision ? await this.opts.provision(spec.actor) : void 0;
3147
3232
  const job = {
3148
3233
  actor: spec.actor,
@@ -3159,6 +3244,7 @@ var init_dispatcher = __esm({
3159
3244
  this.inFlight.set(jobKey, session);
3160
3245
  this.inFlightActor.set(jobKey, spec.actor);
3161
3246
  const dispatchedAtMs = Date.now();
3247
+ this.inFlightStartedAt.set(jobKey, dispatchedAtMs);
3162
3248
  const seqAtDispatch = seqNow;
3163
3249
  const sink = this.opts.trajectory;
3164
3250
  if (sink) {
@@ -3224,6 +3310,7 @@ var init_dispatcher = __esm({
3224
3310
  }
3225
3311
  this.inFlight.delete(jobKey);
3226
3312
  this.inFlightActor.delete(jobKey);
3313
+ this.inFlightStartedAt.delete(jobKey);
3227
3314
  const seqAtExit = kernel.model.lastSeq.get(spec.artifactId) ?? 0;
3228
3315
  if (sink) {
3229
3316
  void (async () => {
@@ -3363,6 +3450,7 @@ var init_src2 = __esm({
3363
3450
  "../core/src/index.ts"() {
3364
3451
  "use strict";
3365
3452
  init_read_model();
3453
+ init_node_identity();
3366
3454
  init_content_handler();
3367
3455
  init_content_type_handler();
3368
3456
  init_diff();
@@ -20363,6 +20451,36 @@ var init_drafts = __esm({
20363
20451
  }
20364
20452
  });
20365
20453
 
20454
+ // ../server/src/governance/workorder-drafts.ts
20455
+ var WorkorderDraftStore;
20456
+ var init_workorder_drafts = __esm({
20457
+ "../server/src/governance/workorder-drafts.ts"() {
20458
+ "use strict";
20459
+ WorkorderDraftStore = class {
20460
+ drafts = /* @__PURE__ */ new Map();
20461
+ seq = 0;
20462
+ submit(d) {
20463
+ const draft = { ...d, id: `wodraft:${++this.seq}`, at: (/* @__PURE__ */ new Date()).toISOString() };
20464
+ this.drafts.set(draft.id, draft);
20465
+ return draft;
20466
+ }
20467
+ list() {
20468
+ return [...this.drafts.values()];
20469
+ }
20470
+ /** 按 workspace 取最新一份:friday 用它发起建单时生成的 workspace id 取回草案,无需解析 chat 文本。 */
20471
+ forWorkspace(workspace) {
20472
+ return [...this.drafts.values()].reverse().find((d) => d.workspace === workspace);
20473
+ }
20474
+ get(id) {
20475
+ return this.drafts.get(id);
20476
+ }
20477
+ remove(id) {
20478
+ return this.drafts.delete(id);
20479
+ }
20480
+ };
20481
+ }
20482
+ });
20483
+
20366
20484
  // ../server/src/domains/collab/planner.ts
20367
20485
  function ensureDefaultWorkorderTypes(schema) {
20368
20486
  const out = [...schema];
@@ -20371,18 +20489,17 @@ function ensureDefaultWorkorderTypes(schema) {
20371
20489
  }
20372
20490
  return out;
20373
20491
  }
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 [];
20492
+ function upgradeDefaultFieldDefs(schema) {
20493
+ const changed = [];
20494
+ for (const def of DEFAULT_TYPES) {
20495
+ if (!def.fieldDefs) continue;
20496
+ const cur = schema.find((item) => item.name === def.name);
20497
+ if (cur && !cur.fieldDefs) {
20498
+ cur.fieldDefs = def.fieldDefs;
20499
+ changed.push(def.name);
20500
+ }
20382
20501
  }
20383
- }
20384
- function workorderSpecBody(spec) {
20385
- return WORKORDER_SPEC_PREFIX + JSON.stringify(spec);
20502
+ return changed;
20386
20503
  }
20387
20504
  function parseWorkorderSpec(body) {
20388
20505
  const first = body.split("\n", 1)[0] ?? "";
@@ -20420,6 +20537,12 @@ async function buildDynamicWorkorderPlan(input) {
20420
20537
  return match;
20421
20538
  };
20422
20539
  const briefOwner = input.actor;
20540
+ const specFields = {};
20541
+ if (input.goal?.trim()) specFields["goal"] = input.goal.trim();
20542
+ if (input.acceptanceCriteria?.length) {
20543
+ const items = input.acceptanceCriteria.map((x2) => x2.trim()).filter(Boolean);
20544
+ if (items.length > 0) specFields["acceptanceCriteria"] = items;
20545
+ }
20423
20546
  const ops = [
20424
20547
  {
20425
20548
  action: "spawn",
@@ -20429,23 +20552,12 @@ async function buildDynamicWorkorderPlan(input) {
20429
20552
  workspace: input.workspace,
20430
20553
  // 工单名 → brief.title;工单目标/brief → brief.description(别再把 name 塞进 description、丢掉 goal)
20431
20554
  ...input.title?.trim() ? { title: input.title.trim() } : {},
20432
- description: input.description
20555
+ description: input.description,
20556
+ ...Object.keys(specFields).length > 0 ? { fields: specFields } : {}
20433
20557
  }
20434
20558
  ];
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
20559
  const spawned = [{ id: briefId, type: "brief", role: "pm", owner: briefOwner }];
20445
20560
  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
20561
  if (input.agentPlan && !agentGraph) {
20450
20562
  issues.push({
20451
20563
  code: "planner_invalid",
@@ -20453,51 +20565,63 @@ async function buildDynamicWorkorderPlan(input) {
20453
20565
  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
20566
  });
20455
20567
  }
20456
- const desired = agentGraph?.types ?? desiredArtifacts(primaryType, input.description);
20568
+ for (const dup of agentGraph?.dupTitles ?? []) {
20569
+ 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` });
20570
+ }
20571
+ const usedIds = /* @__PURE__ */ new Set([briefId]);
20457
20572
  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`
20573
+ if (agentGraph) {
20574
+ for (const node of agentGraph.nodes) {
20575
+ const def = schemaByName.get(node.type);
20576
+ if (!def) {
20577
+ 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` });
20578
+ continue;
20579
+ }
20580
+ const owner = await resolveRole(def.ownerRole, node.type);
20581
+ const ownerActor = owner.actor ?? `pending:role:${def.ownerRole}`;
20582
+ availableNodes.push({
20583
+ type: node.type,
20584
+ title: node.title,
20585
+ id: deriveArtifactId(node.type, input.workspace, node.title, usedIds),
20586
+ role: def.ownerRole,
20587
+ owner: ownerActor,
20588
+ dependsOn: node.dependsOn,
20589
+ ...node.brief ? { brief: node.brief } : {}
20466
20590
  });
20467
- continue;
20468
20591
  }
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 });
20592
+ } else {
20593
+ const desired = desiredArtifacts(primaryType, input.description);
20594
+ for (const type of desired) {
20595
+ const def = schemaByName.get(type);
20596
+ if (!def) {
20597
+ 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` });
20598
+ continue;
20599
+ }
20600
+ const owner = await resolveRole(def.ownerRole, type);
20601
+ const ownerActor = owner.actor ?? (type === primaryType ? input.actor : `pending:role:${def.ownerRole}`);
20602
+ const id = type === primaryType ? primaryId : `artifact:${type}:${slug4}`;
20603
+ availableNodes.push({ type, id, role: def.ownerRole, owner: ownerActor });
20604
+ }
20474
20605
  }
20475
20606
  for (const node of availableNodes) {
20476
- const inputs = inputEdgesFor(node.type, availableNodes, briefId, agentGraph?.dependsOn);
20607
+ const inputs = agentGraph ? agentEdgesFor(node.dependsOn ?? [], availableNodes, briefId, issues) : ruleEdgesFor(node.type, availableNodes, briefId);
20477
20608
  ops.push({
20478
20609
  action: "spawn",
20479
20610
  id: node.id,
20480
20611
  type: node.type,
20481
20612
  owner: node.owner,
20482
20613
  workspace: input.workspace,
20614
+ ...node.title ? { title: node.title } : {},
20483
20615
  inputs,
20484
- description: node.type === "prd" && issues.length > 0 ? `\u964D\u7EA7\u65B9\u6848\u6587\u6863\uFF1A${displayTitle}` : `${labelForType(node.type)}\uFF1A${displayTitle}`
20616
+ description: !agentGraph && node.type === "prd" && issues.length > 0 ? `\u964D\u7EA7\u65B9\u6848\u6587\u6863\uFF1A${displayTitle}` : `${labelForType(node.type)}\uFF1A${node.title ?? displayTitle}`
20485
20617
  });
20486
20618
  ops.push({
20487
20619
  action: "annotate",
20488
20620
  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),
20621
+ body: node.brief ?? taskBookFor(node.type, input.description, node.role, issues, agentGraph?.notes),
20499
20622
  blocks: false
20500
20623
  });
20624
+ spawned.push({ id: node.id, type: node.type, role: node.role, owner: node.owner });
20501
20625
  }
20502
20626
  return {
20503
20627
  plan: {
@@ -20543,18 +20667,21 @@ function buildPmAgentPlannerPrompt(input) {
20543
20667
  "",
20544
20668
  JSON.stringify({
20545
20669
  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" }
20670
+ { 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" },
20671
+ { type: "research", title: "\u7ADE\u54C1\u8C03\u7814", dependsOn: ["brief"], brief: "\u2026" },
20672
+ { type: "dev", title: "\u540E\u7AEF\u5B9E\u73B0", dependsOn: ["prd:\u9700\u6C42\u4E0E\u8303\u56F4\u8BF4\u660E"], brief: "\u2026" }
20548
20673
  ],
20549
20674
  notes: ["\u53EF\u9009\uFF1A\u89C4\u5212\u7406\u7531\u6216\u98CE\u9669\u63D0\u793A"]
20550
20675
  }, null, 2),
20551
20676
  "",
20552
20677
  "\u89C4\u5219\uFF1A",
20553
20678
  "- 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"
20679
+ "- 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",
20680
+ "- \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",
20681
+ '- \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',
20682
+ "- \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",
20683
+ '- 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',
20684
+ "- \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
20685
  ].join("\n");
20559
20686
  }
20560
20687
  function parseAgentWorkorderPlan(text) {
@@ -20652,16 +20779,28 @@ function desiredArtifacts(primaryType, description) {
20652
20779
  }
20653
20780
  function normalizeAgentPlan(plan, validTypes) {
20654
20781
  const allowed = validTypes.map((t) => t.trim().toLowerCase()).filter((t) => t !== "brief");
20655
- const types2 = [];
20656
- const dependsOn = /* @__PURE__ */ new Map();
20782
+ const nodes = [];
20783
+ const seen = /* @__PURE__ */ new Set();
20784
+ const dupTitles = [];
20657
20785
  for (const node of plan.nodes) {
20658
20786
  const type = node.type.trim().toLowerCase();
20659
20787
  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))]);
20788
+ if (!allowed.includes(type)) continue;
20789
+ const title = (node.title?.trim() || labelForType(type)).slice(0, 40);
20790
+ const key = `${type}:${title.toLowerCase()}`;
20791
+ if (seen.has(key)) {
20792
+ dupTitles.push(`${type}:${title}`);
20793
+ continue;
20794
+ }
20795
+ seen.add(key);
20796
+ nodes.push({
20797
+ type,
20798
+ title,
20799
+ dependsOn: node.dependsOn?.map((d) => d.trim()).filter(Boolean) ?? [],
20800
+ ...node.brief ? { brief: node.brief } : {}
20801
+ });
20662
20802
  }
20663
- const filtered = allowed.filter((type) => types2.includes(type));
20664
- return filtered.length > 0 ? { types: filtered, dependsOn, ...plan.notes?.length ? { notes: plan.notes } : {} } : null;
20803
+ return nodes.length > 0 ? { nodes, dupTitles, ...plan.notes?.length ? { notes: plan.notes } : {} } : null;
20665
20804
  }
20666
20805
  function extractJson(text) {
20667
20806
  const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
@@ -20674,23 +20813,30 @@ function extractJson(text) {
20674
20813
  if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);
20675
20814
  return null;
20676
20815
  }
20677
- function inputEdgesFor(type, nodes, briefId, plannedDependsOn) {
20678
- const idOf = (t) => nodes.find((node) => node.type === t)?.id;
20816
+ function agentEdgesFor(dependsOn, nodes, briefId, issues) {
20679
20817
  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
- }
20818
+ for (const dep of dependsOn) {
20819
+ const d = dep.trim();
20820
+ if (!d) continue;
20821
+ if (d.toLowerCase() === "brief") {
20822
+ edges.add(briefId);
20823
+ continue;
20688
20824
  }
20825
+ const sep = d.indexOf(":");
20826
+ const depType = (sep >= 0 ? d.slice(0, sep) : d).trim().toLowerCase();
20827
+ const depTitle = sep >= 0 ? d.slice(sep + 1).trim().toLowerCase() : null;
20828
+ const match = nodes.find((n) => n.type === depType && (depTitle === null || (n.title ?? "").toLowerCase() === depTitle));
20829
+ if (match) edges.add(match.id);
20830
+ 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
20831
  }
20690
- if (edges.size === 0) {
20691
- if (type !== "prd") edges.add(idOf("prd") ?? briefId);
20692
- else edges.add(briefId);
20693
- }
20832
+ if (edges.size === 0) edges.add(briefId);
20833
+ return [...edges].map((to) => ({ to, required: true }));
20834
+ }
20835
+ function ruleEdgesFor(type, nodes, briefId) {
20836
+ const idOf = (t) => nodes.find((node) => node.type === t)?.id;
20837
+ const edges = /* @__PURE__ */ new Set();
20838
+ if (type !== "prd") edges.add(idOf("prd") ?? briefId);
20839
+ else edges.add(briefId);
20694
20840
  if (type === "adr") {
20695
20841
  const research = idOf("research");
20696
20842
  if (research) edges.add(research);
@@ -20727,17 +20873,6 @@ ${plannerNotes.map((note) => `- ${note}`).join("\n")}` : "";
20727
20873
  notes
20728
20874
  ].join("\n");
20729
20875
  }
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
20876
  function dedupeIssues(issues) {
20742
20877
  const seen = /* @__PURE__ */ new Set();
20743
20878
  const out = [];
@@ -20763,16 +20898,19 @@ function labelForType(type) {
20763
20898
  }
20764
20899
  function artifactSlug(idOrWorkspace, description) {
20765
20900
  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()}`;
20901
+ return slugify(source, 40) || `wo-${Date.now()}`;
20768
20902
  }
20769
- var PLANNING_ISSUES_PREFIX, DEFAULT_TYPES, WORKORDER_SPEC_PREFIX;
20903
+ var DEFAULT_TYPES, WORKORDER_SPEC_PREFIX;
20770
20904
  var init_planner = __esm({
20771
20905
  "../server/src/domains/collab/planner.ts"() {
20772
20906
  "use strict";
20773
- PLANNING_ISSUES_PREFIX = "OASIS_PLANNING_ISSUES ";
20907
+ init_src2();
20774
20908
  DEFAULT_TYPES = [
20775
- { name: "brief", contentType: "text", ownerRole: "pm" },
20909
+ // brief goal/acceptanceCriteria 现在是【自定义字段】(Phase 3,替代旧 OASIS_WORKORDER_SPEC 批注,铲 #1 死锁根)
20910
+ { name: "brief", contentType: "text", ownerRole: "pm", fieldDefs: [
20911
+ { name: "goal", type: "string", description: "\u4E00\u53E5\u8BDD\u76EE\u6807\uFF1A\u8FD9\u4E2A\u5DE5\u5355\u8981\u8FBE\u6210\u4EC0\u4E48" },
20912
+ { name: "acceptanceCriteria", type: "string[]", description: "\u9A8C\u6536\u6761\u76EE\uFF08\u6EE1\u8DB3\u5373\u7B97\u5B8C\u6210\uFF09" }
20913
+ ] },
20776
20914
  { name: "prd", contentType: "text", ownerRole: "pm" },
20777
20915
  { name: "research", contentType: "text", ownerRole: "research" },
20778
20916
  { name: "design", contentType: "text", ownerRole: "designer" },
@@ -20784,145 +20922,6 @@ var init_planner = __esm({
20784
20922
  }
20785
20923
  });
20786
20924
 
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
20925
  // ../server/src/server.ts
20927
20926
  function wantsChatParts(req) {
20928
20927
  const accept = req.headers.accept;
@@ -21183,6 +21182,10 @@ async function runCommand(kernel, blobs, oplog, actor, command, args, ctx = {})
21183
21182
  ...p2.spec !== void 0 ? { spec: String(p2.spec) } : {},
21184
21183
  ...Array.isArray(p2.deps) ? { deps: p2.deps.map(String) } : {}
21185
21184
  }));
21185
+ const rawFields = args["fields"];
21186
+ if (rawFields !== void 0 && (typeof rawFields !== "object" || rawFields === null || Array.isArray(rawFields))) {
21187
+ throw new Error("\u53C2\u6570 fields \u5FC5\u987B\u662F\u5BF9\u8C61 {key: value}");
21188
+ }
21186
21189
  const createdViaArg = optStr(args, "createdVia");
21187
21190
  if (createdViaArg !== void 0 && createdViaArg !== "seed" && createdViaArg !== "manual-spawn") {
21188
21191
  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 +21277,33 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
21274
21277
  if (!preview2.endState.ok) {
21275
21278
  throw new Error(`\u52A8\u6001\u89C4\u5212\u7EC8\u6001\u6821\u9A8C\u4E0D\u8FC7\uFF1A${preview2.endState.violations.join("\uFF1B")}`);
21276
21279
  }
21280
+ if (args["stage"] === true && ctx.workorderDrafts) {
21281
+ const draft = ctx.workorderDrafts.submit({
21282
+ workspace,
21283
+ plan: planned.plan,
21284
+ rootArtifactId: planned.rootArtifactId,
21285
+ seed: {
21286
+ brief,
21287
+ ...goal !== void 0 ? { goal } : {},
21288
+ ...acceptanceCriteria && acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {},
21289
+ ...args["dispatch"] === false ? { dispatch: false } : {}
21290
+ },
21291
+ draftedBy: actor
21292
+ });
21293
+ return {
21294
+ 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`,
21295
+ data: {
21296
+ draftId: draft.id,
21297
+ workspace,
21298
+ rootArtifactId: planned.rootArtifactId,
21299
+ spawned: planned.spawned,
21300
+ planning: {
21301
+ status: planned.issues.length > 0 ? "needs_staffing" : "ready",
21302
+ issues: planned.issues
21303
+ }
21304
+ }
21305
+ };
21306
+ }
21277
21307
  const applied = await kernel.applyIntervention(planned.plan, preview2.baseSeqs, actor);
21278
21308
  const briefArtifactId = planned.rootArtifactId;
21279
21309
  if (brief && brief.trim()) {
@@ -21283,7 +21313,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
21283
21313
  await kernel.conclude({ artifactId: briefArtifactId, actor, note: "\u5EFA\u5355\u65F6\u7531\u53D1\u8D77\u4EBA\u62CD\u677F\u5B9A\u7A3F" });
21284
21314
  }
21285
21315
  if (args["dispatch"] === false) {
21286
- await kernel.annotate({ artifactId: briefArtifactId, actor, body: `${DISPATCH_HOLD_PREFIX} \u5F85\u53D1\u8D77\u4EBA\u6D3E\u5355` });
21316
+ await ctx.artifactState?.setWorkspaceDispatchHold(workspace, true);
21287
21317
  }
21288
21318
  return {
21289
21319
  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 +21342,8 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
21312
21342
  ...optStr(args, "description") !== void 0 ? { description: optStr(args, "description") } : {},
21313
21343
  ...optStr(args, "docType") !== void 0 ? { docType: optStr(args, "docType") } : {},
21314
21344
  ...inputs !== void 0 ? { inputs } : {},
21315
- ...parts !== void 0 ? { parts } : {}
21345
+ ...parts !== void 0 ? { parts } : {},
21346
+ ...rawFields !== void 0 ? { fields: rawFields } : {}
21316
21347
  });
21317
21348
  return { message: `spawned ${artifact.id}\uFF08owner=${artifact.owner}${artifact.parts?.length ? `\uFF0Cparts=${artifact.parts.map((p2) => p2.name).join("/")}` : ""}\uFF09`, data: artifact };
21318
21349
  }
@@ -21836,17 +21867,9 @@ async function startOasisServer(opts) {
21836
21867
  ...opts.planWorkorderWithAgent ? { planWorkorderWithAgent: opts.planWorkorderWithAgent } : {},
21837
21868
  ...opts.resolveCodeRepo ? { resolveCodeRepo: opts.resolveCodeRepo } : {},
21838
21869
  ...opts.dispatchProduce ? { dispatchProduce: opts.dispatchProduce } : {},
21870
+ ...opts.workorderDrafts ? { workorderDrafts: opts.workorderDrafts } : {},
21839
21871
  ...opts.gitEnv ? { gitEnv: opts.gitEnv } : {}
21840
21872
  });
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
21873
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
21851
21874
  } catch (err) {
21852
21875
  const status = err instanceof FrozenError ? 409 : 400;
@@ -21883,6 +21906,40 @@ async function startOasisServer(opts) {
21883
21906
  }
21884
21907
  return;
21885
21908
  }
21909
+ if (url.pathname === "/api/workorder/draft" && req.method === "GET" && opts.workorderDrafts) {
21910
+ const workspace = url.searchParams.get("workspace");
21911
+ const draft = workspace ? opts.workorderDrafts.forWorkspace(workspace) : void 0;
21912
+ if (!draft) {
21913
+ res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "no staged workorder draft" }));
21914
+ return;
21915
+ }
21916
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ data: draft }));
21917
+ return;
21918
+ }
21919
+ if (url.pathname === "/api/workorder/apply" && req.method === "POST") {
21920
+ try {
21921
+ const body = JSON.parse(rawBody);
21922
+ const result = await engine.kernel.applyIntervention(body.plan, body.baseSeqs, actor);
21923
+ const brief = engine.kernel.model.artifacts.get(body.briefArtifactId);
21924
+ if (brief && brief.description && brief.description.trim()) {
21925
+ const briefRef = await engine.blobs.put(new TextEncoder().encode(brief.description));
21926
+ 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" });
21927
+ await engine.kernel.advanceQueue(body.briefArtifactId, actor);
21928
+ await engine.kernel.conclude({ artifactId: body.briefArtifactId, actor, note: "\u5EFA\u5355\u65F6\u7531\u53D1\u8D77\u4EBA\u62CD\u677F\u5B9A\u7A3F" });
21929
+ }
21930
+ if (body.dispatch === false && brief?.workspace) {
21931
+ await opts.artifactState?.setWorkspaceDispatchHold(brief.workspace, true);
21932
+ }
21933
+ res.writeHead(200, { "content-type": "application/json" }).end(
21934
+ JSON.stringify({ ...result, briefArtifactId: body.briefArtifactId, workspace: brief?.workspace ?? null })
21935
+ );
21936
+ } catch (err) {
21937
+ res.writeHead(409, { "content-type": "application/json" }).end(
21938
+ JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
21939
+ );
21940
+ }
21941
+ return;
21942
+ }
21886
21943
  if (url.pathname === "/api/intervention/draft" && req.method === "POST" && opts.drafts) {
21887
21944
  try {
21888
21945
  const plan = JSON.parse(rawBody);
@@ -22129,8 +22186,7 @@ async function startOasisServer(opts) {
22129
22186
  const session = await opts.dispatchChat({
22130
22187
  actorId: body.actorId,
22131
22188
  message: body.message,
22132
- ...body.sessionId ? { sessionId: body.sessionId } : {},
22133
- ...body.chatSessionId ? { chatSessionId: body.chatSessionId } : {}
22189
+ ...body.sessionId ? { sessionId: body.sessionId } : {}
22134
22190
  });
22135
22191
  let finished = false;
22136
22192
  res.on("close", () => {
@@ -22216,6 +22272,14 @@ async function startOasisServer(opts) {
22216
22272
  });
22217
22273
  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
22274
  }
22275
+ if (opts.materializeSkills) {
22276
+ const skillFiles = await opts.materializeSkills(body.actorId, "claude").catch(() => ({}));
22277
+ for (const [rel, content] of Object.entries(skillFiles)) {
22278
+ const file = join16(dir, rel);
22279
+ mkdirSync13(dirname13(file), { recursive: true });
22280
+ writeFileSync14(file, content);
22281
+ }
22282
+ }
22219
22283
  const activeConnectors = actorCtx.connectors.filter(
22220
22284
  (c) => c.status === "connected" || c.status === "verifying"
22221
22285
  );
@@ -22231,8 +22295,8 @@ async function startOasisServer(opts) {
22231
22295
  }
22232
22296
  }
22233
22297
  const { spawn: spawn6 } = await import("node:child_process");
22234
- const { randomUUID: randomUUID15 } = await import("node:crypto");
22235
- const sessionId = body.sessionId ?? randomUUID15();
22298
+ const { randomUUID: randomUUID14 } = await import("node:crypto");
22299
+ const sessionId = body.sessionId ?? randomUUID14();
22236
22300
  const args = [
22237
22301
  "-p",
22238
22302
  body.message,
@@ -22243,7 +22307,7 @@ async function startOasisServer(opts) {
22243
22307
  ];
22244
22308
  const child = spawn6(opts.claudeBin ?? "claude", args, {
22245
22309
  ...spawnCwd ? { cwd: spawnCwd } : {},
22246
- env: { ...process.env, ...env, ...body.chatSessionId ? { OASIS_CHAT_SESSION_ID: body.chatSessionId } : {} },
22310
+ env: { ...process.env, ...env },
22247
22311
  stdio: ["ignore", "pipe", "pipe"]
22248
22312
  });
22249
22313
  if (typedParts) {
@@ -22535,8 +22599,8 @@ var init_server3 = __esm({
22535
22599
  init_mcp2();
22536
22600
  init_router();
22537
22601
  init_drafts();
22602
+ init_workorder_drafts();
22538
22603
  init_planner();
22539
- init_workorders();
22540
22604
  defaultResolveActor = (token) => token.startsWith("token:") ? token.slice("token:".length) : null;
22541
22605
  enc = (s2) => new TextEncoder().encode(s2);
22542
22606
  CHAT_OUTPUT_LIMIT = 16e3;
@@ -22614,11 +22678,11 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
22614
22678
  return m2;
22615
22679
  };
22616
22680
  return {
22617
- issue(nodeId, ttlMs = defaultTtlMs) {
22681
+ issue(name, ttlMs = defaultTtlMs) {
22618
22682
  const m2 = prune();
22619
22683
  const token = `ent_${(0, import_node_crypto3.randomBytes)(24).toString("base64url")}`;
22620
22684
  const expiresAt = Date.now() + ttlMs;
22621
- m2.set(token, { nodeId, expiresAt });
22685
+ m2.set(token, { name, expiresAt });
22622
22686
  write(m2);
22623
22687
  return { token, expiresAt: new Date(expiresAt).toISOString() };
22624
22688
  },
@@ -22626,7 +22690,7 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
22626
22690
  const m2 = prune();
22627
22691
  const entry = m2.get(token);
22628
22692
  if (!entry) return null;
22629
- return { nodeId: entry.nodeId };
22693
+ return { name: entry.name, ...entry.issuedNodeId ? { issuedNodeId: entry.issuedNodeId } : {} };
22630
22694
  },
22631
22695
  consume(token) {
22632
22696
  const m2 = prune();
@@ -22634,7 +22698,15 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
22634
22698
  if (!entry) return null;
22635
22699
  m2.delete(token);
22636
22700
  write(m2);
22637
- return { nodeId: entry.nodeId };
22701
+ return { name: entry.name, ...entry.issuedNodeId ? { issuedNodeId: entry.issuedNodeId } : {} };
22702
+ },
22703
+ setIssuedNodeId(token, nodeId) {
22704
+ const m2 = read();
22705
+ const entry = m2.get(token);
22706
+ if (entry) {
22707
+ entry.issuedNodeId = nodeId;
22708
+ write(m2);
22709
+ }
22638
22710
  }
22639
22711
  };
22640
22712
  }
@@ -23746,6 +23818,147 @@ var init_projection = __esm({
23746
23818
  }
23747
23819
  });
23748
23820
 
23821
+ // ../server/src/domains/collab/node-state.ts
23822
+ function nodeLabel(a) {
23823
+ if (a.title) return a.title;
23824
+ if (a.description) return a.description;
23825
+ const tail = a.id.split(":").slice(2).join(":");
23826
+ return tail ? `\u672A\u547D\u540D ${a.type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${a.type}`;
23827
+ }
23828
+ function nodeInfo(model, a) {
23829
+ const lifecycle = lifecycleOf(model, a.id);
23830
+ const sealed = lifecycle !== "active";
23831
+ const concluded = isConcluded(model, a.id);
23832
+ const mark = sealed ? lifecycle : pendingConcludeAttempt(model, a.id) ? "gate-pending" : blockedOf(model, a.id).blocked ? "blocked" : concluded ? "concluded" : a.currentRev ? "active" : "embryo";
23833
+ const hasWorking = revisionsOf(model, a.id).some((r) => r.state === "working");
23834
+ return { mark, queueLen: queueOf(model, a.id).length, concluded, sealed, hasWorking };
23835
+ }
23836
+ function deriveStage(members) {
23837
+ if (members.length > 0 && members.every((m2) => m2.sealed)) return "sealed";
23838
+ const live = members.filter((m2) => !m2.sealed);
23839
+ if (live.length > 0 && live.every((m2) => m2.concluded)) return "concluded";
23840
+ const reviewing = live.some((m2) => m2.mark === "gate-pending" || m2.mark === "active" && m2.queueLen > 0);
23841
+ if (reviewing) return "review";
23842
+ const progressing = live.some((m2) => m2.mark === "active" || m2.mark === "embryo");
23843
+ const blocked = live.some((m2) => m2.mark === "blocked");
23844
+ if (blocked && !progressing) return "blocked";
23845
+ return "executing";
23846
+ }
23847
+ function deriveNodeStage(info) {
23848
+ if (info.sealed) return "sealed";
23849
+ switch (info.mark) {
23850
+ case "concluded":
23851
+ return "concluded";
23852
+ case "gate-pending":
23853
+ return "gate";
23854
+ case "blocked":
23855
+ return "blocked";
23856
+ case "embryo":
23857
+ return "embryo";
23858
+ default:
23859
+ if (info.hasWorking) return "editing";
23860
+ if (info.queueLen > 0) return "review";
23861
+ return "merged";
23862
+ }
23863
+ }
23864
+ var init_node_state = __esm({
23865
+ "../server/src/domains/collab/node-state.ts"() {
23866
+ "use strict";
23867
+ init_src2();
23868
+ }
23869
+ });
23870
+
23871
+ // ../server/src/domains/collab/workorders.ts
23872
+ function buildWorkorderSummaries(model, resolveActor, resolveProject, dispatchPausedOf) {
23873
+ const byWorkspace = /* @__PURE__ */ new Map();
23874
+ for (const a of model.artifacts.values()) {
23875
+ const arr = byWorkspace.get(a.workspace) ?? [];
23876
+ arr.push(a);
23877
+ byWorkspace.set(a.workspace, arr);
23878
+ }
23879
+ const ref = (id) => {
23880
+ const extra = resolveActor?.(id);
23881
+ return {
23882
+ id,
23883
+ ...extra?.name ? { name: extra.name } : {},
23884
+ ...extra?.role ? { role: extra.role } : {},
23885
+ ...extra?.avatar ? { avatar: extra.avatar } : {}
23886
+ };
23887
+ };
23888
+ const out = [];
23889
+ for (const [workspace, arts] of byWorkspace) {
23890
+ const members = arts.map((a) => nodeInfo(model, a));
23891
+ const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
23892
+ const spec = workorderSpecOf(model, arts);
23893
+ const concluded = members.filter((m2) => m2.concluded).length;
23894
+ const lastOps = arts.map((a) => model.lastOpAt.get(a.id)).filter((t) => Boolean(t)).sort();
23895
+ const participantIds = [...new Set(arts.map((a) => a.owner))];
23896
+ const woTitle = seed.title ?? seed.description ?? workspace;
23897
+ out.push({
23898
+ id: workspace,
23899
+ title: woTitle,
23900
+ // 工单完整内容 = 根产物 description;仅当与标题不同(根产物有独立 title)时给出,避免与 title 重复
23901
+ ...seed.description && seed.description !== woTitle ? { description: seed.description } : {},
23902
+ // 一句话目标:优先取结构化 spec.goal;缺省时前端回退 description 首段(投影不替前端做回退,只给真值)
23903
+ ...spec?.goal ? { goal: spec.goal } : {},
23904
+ stage: deriveStage(members),
23905
+ owner: ref(seed.owner),
23906
+ participants: participantIds.map(ref),
23907
+ artifactCount: arts.length,
23908
+ progress: { concluded, total: arts.length },
23909
+ updatedAt: lastOps[lastOps.length - 1] ?? "",
23910
+ projectId: resolveProject?.(workspace) ?? null,
23911
+ planning: planningStatusOf(model, arts),
23912
+ ...dispatchPausedOf?.(workspace) ? { dispatchPaused: true } : {}
23913
+ });
23914
+ }
23915
+ return out.sort((a, b2) => a.updatedAt < b2.updatedAt ? 1 : a.updatedAt > b2.updatedAt ? -1 : 0);
23916
+ }
23917
+ function planningStatusOf(_model, arts) {
23918
+ const issues = [];
23919
+ const seenRoles = /* @__PURE__ */ new Set();
23920
+ for (const a of arts) {
23921
+ const m2 = /^pending:role:(.+)$/.exec(a.owner);
23922
+ if (!m2) continue;
23923
+ const role = m2[1];
23924
+ if (seenRoles.has(role)) continue;
23925
+ seenRoles.add(role);
23926
+ 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` });
23927
+ }
23928
+ return issues.length > 0 ? { status: "needs_staffing", issues } : { status: "ready", issues: [] };
23929
+ }
23930
+ function specFromFields(fields) {
23931
+ if (!fields) return null;
23932
+ const out = {};
23933
+ const goal = fields["goal"];
23934
+ if (typeof goal === "string" && goal.trim()) out.goal = goal.trim();
23935
+ const ac = fields["acceptanceCriteria"];
23936
+ if (Array.isArray(ac)) {
23937
+ const items = ac.filter((x2) => typeof x2 === "string" && x2.trim().length > 0).map((x2) => x2.trim());
23938
+ if (items.length > 0) out.acceptanceCriteria = items;
23939
+ }
23940
+ return out.goal !== void 0 || out.acceptanceCriteria !== void 0 ? out : null;
23941
+ }
23942
+ function workorderSpecOf(model, arts) {
23943
+ const seed = arts.find((a) => a.type === "brief") ?? arts.find((a) => a.inputs.length === 0);
23944
+ if (!seed) return null;
23945
+ const fromFields = specFromFields(seed.fields);
23946
+ if (fromFields) return fromFields;
23947
+ for (const annotation of model.annotations.values()) {
23948
+ if (annotation.anchor.target !== seed.id) continue;
23949
+ const spec = parseWorkorderSpec(annotation.body);
23950
+ if (spec) return spec;
23951
+ }
23952
+ return null;
23953
+ }
23954
+ var init_workorders = __esm({
23955
+ "../server/src/domains/collab/workorders.ts"() {
23956
+ "use strict";
23957
+ init_node_state();
23958
+ init_planner();
23959
+ }
23960
+ });
23961
+
23749
23962
  // ../server/src/domains/projects/service.ts
23750
23963
  function namedError(name, message) {
23751
23964
  const err = new Error(message);
@@ -24003,6 +24216,8 @@ var init_service = __esm({
24003
24216
  nodeOutputs = /* @__PURE__ */ new Map();
24004
24217
  // B5:工单→项目绑定(key=workOrderId)。与 node_outputs 同属合法薄旁存——reset 不清、重放时种回。
24005
24218
  workspaceBindings = /* @__PURE__ */ new Map();
24219
+ // #2 派单门:处于"待派发"的工单 id(薄旁存,同 binding——dispatcher/工单视图读它,不进 oplog)。
24220
+ dispatchHeld = /* @__PURE__ */ new Set();
24006
24221
  /**
24007
24222
  * B2 清空式重建用:清掉 oplog 派生的事实投影(artifact/revision/event/annotation),
24008
24223
  * 保留 node_output(合法薄投影,投影正依赖它补 source*;由 registerNodeOutput 独立维护)。
@@ -24070,6 +24285,13 @@ var init_service = __esm({
24070
24285
  async listWorkspaceBindings() {
24071
24286
  return [...this.workspaceBindings.values()].map((binding) => ({ ...binding }));
24072
24287
  }
24288
+ async setWorkspaceDispatchHold(workOrderId, held) {
24289
+ if (held) this.dispatchHeld.add(workOrderId);
24290
+ else this.dispatchHeld.delete(workOrderId);
24291
+ }
24292
+ async listWorkspaceDispatchHeld() {
24293
+ return [...this.dispatchHeld];
24294
+ }
24073
24295
  async listAll() {
24074
24296
  return {
24075
24297
  artifacts: [...this.artifacts.values()].map(cloneArtifact),
@@ -24077,7 +24299,8 @@ var init_service = __esm({
24077
24299
  events: [...this.events.values()].map(cloneEvent),
24078
24300
  annotations: [...this.annotations.values()].map(cloneAnnotation),
24079
24301
  nodeOutputs: [...this.nodeOutputs.values()].map(cloneNodeOutput),
24080
- workspaceBindings: [...this.workspaceBindings.values()].map((binding) => ({ ...binding }))
24302
+ workspaceBindings: [...this.workspaceBindings.values()].map((binding) => ({ ...binding })),
24303
+ workspaceDispatchHeld: [...this.dispatchHeld]
24081
24304
  };
24082
24305
  }
24083
24306
  };
@@ -25205,9 +25428,10 @@ var init_dev_store = __esm({
25205
25428
  for (const annotation of snap.annotations ?? []) await MemoryArtifactStateStore.prototype.upsertAnnotation.call(store, annotation);
25206
25429
  for (const output of snap.nodeOutputs ?? []) await MemoryArtifactStateStore.prototype.upsertNodeOutput.call(store, output);
25207
25430
  for (const binding of snap.workspaceBindings ?? []) await MemoryArtifactStateStore.prototype.upsertWorkspaceBinding.call(store, binding);
25431
+ for (const ws of snap.workspaceDispatchHeld ?? []) await MemoryArtifactStateStore.prototype.setWorkspaceDispatchHold.call(store, ws, true);
25208
25432
  } else {
25209
25433
  fs2.mkdirSync(path.dirname(file), { recursive: true });
25210
- fs2.writeFileSync(file, JSON.stringify({ artifacts: [], revisions: [], events: [], annotations: [], nodeOutputs: [], workspaceBindings: [] }, null, 2));
25434
+ fs2.writeFileSync(file, JSON.stringify({ artifacts: [], revisions: [], events: [], annotations: [], nodeOutputs: [], workspaceBindings: [], workspaceDispatchHeld: [] }, null, 2));
25211
25435
  }
25212
25436
  return store;
25213
25437
  }
@@ -25235,6 +25459,10 @@ var init_dev_store = __esm({
25235
25459
  await super.upsertWorkspaceBinding(binding);
25236
25460
  await this.save();
25237
25461
  }
25462
+ async setWorkspaceDispatchHold(workOrderId, held) {
25463
+ await super.setWorkspaceDispatchHold(workOrderId, held);
25464
+ await this.save();
25465
+ }
25238
25466
  async save() {
25239
25467
  fs2.writeFileSync(this.file, JSON.stringify(await this.listAll(), null, 2));
25240
25468
  }
@@ -25321,7 +25549,6 @@ var init_dev_store = __esm({
25321
25549
  MemoryChatSessionStore = class {
25322
25550
  sessions = /* @__PURE__ */ new Map();
25323
25551
  messages = /* @__PURE__ */ new Map();
25324
- sessionWorkOrders = /* @__PURE__ */ new Map();
25325
25552
  async createSession(s2) {
25326
25553
  this.sessions.set(s2.id, s2);
25327
25554
  }
@@ -25338,7 +25565,6 @@ var init_dev_store = __esm({
25338
25565
  async deleteSession(id) {
25339
25566
  this.sessions.delete(id);
25340
25567
  this.messages.delete(id);
25341
- this.sessionWorkOrders.delete(id);
25342
25568
  }
25343
25569
  async appendMessage(m2) {
25344
25570
  const list = this.messages.get(m2.sessionId) ?? [];
@@ -25349,14 +25575,6 @@ var init_dev_store = __esm({
25349
25575
  const list = (this.messages.get(sessionId) ?? []).sort((a, b2) => a.seq - b2.seq);
25350
25576
  return limit ? list.slice(-limit) : list;
25351
25577
  }
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
25578
  };
25361
25579
  }
25362
25580
  });
@@ -25474,12 +25692,36 @@ function typeDefError(def, mode) {
25474
25692
  }
25475
25693
  return null;
25476
25694
  }
25477
- var TYPE_DEF_SCHEMA, TypesAdminService;
25695
+ var RESERVED_FIELD_NAMES, TYPE_DEF_SCHEMA, TypesAdminService;
25478
25696
  var init_types_admin = __esm({
25479
25697
  "../server/src/types-admin.ts"() {
25480
25698
  "use strict";
25481
25699
  init_zod();
25482
25700
  init_src2();
25701
+ RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
25702
+ // Artifact 内置字段
25703
+ "id",
25704
+ "type",
25705
+ "title",
25706
+ "description",
25707
+ "docType",
25708
+ "currentRev",
25709
+ "owner",
25710
+ "workspace",
25711
+ "persistence",
25712
+ "createdVia",
25713
+ "inputs",
25714
+ "parts",
25715
+ "fields",
25716
+ // edit/spawn 自己的 flag + 全局 infra flag(--<字段> 转发要避开)
25717
+ "brief",
25718
+ "input",
25719
+ "set-json",
25720
+ "clear",
25721
+ "server",
25722
+ "token",
25723
+ "dir"
25724
+ ]);
25483
25725
  TYPE_DEF_SCHEMA = external_exports.object({
25484
25726
  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
25727
  description: external_exports.string(),
@@ -25505,8 +25747,16 @@ var init_types_admin = __esm({
25505
25747
  budget: external_exports.object({ wallClockMinutes: external_exports.number().positive().optional() }).strict(),
25506
25748
  cascade: external_exports.enum(["auto", "manual"]),
25507
25749
  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())
25750
+ mirror: external_exports.record(external_exports.string(), external_exports.unknown()),
25509
25751
  // MirrorConfig 是连接器侧复杂类型——只要求是对象、内部松校验
25752
+ fieldDefs: external_exports.array(
25753
+ external_exports.object({
25754
+ 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"),
25755
+ type: external_exports.enum(["string", "string[]", "number", "boolean"]),
25756
+ required: external_exports.boolean().optional(),
25757
+ description: external_exports.string().optional()
25758
+ }).strict()
25759
+ )
25510
25760
  }).partial().strict();
25511
25761
  TypesAdminService = class {
25512
25762
  constructor(store, kernels) {
@@ -25576,6 +25826,36 @@ var init_types_admin = __esm({
25576
25826
  for (const k2 of this.kernels()) k2.removeType(name);
25577
25827
  return { ok: true };
25578
25828
  }
25829
+ // —— B 平面:自定义字段声明(fieldDefs)的增/改/删。都落到 edit(复用 zod 校验 + 传播 + usage 影响半径)。
25830
+ // 改名禁、删=deprecate(存量值留、新写入拒)都由"只有这三个动词、且 name 钉死"在结构上保证。
25831
+ /** 加一条字段声明。name 已声明 / 撞保留集(edit 的 zod 把住)则拒。 */
25832
+ async addField(typeName, spec) {
25833
+ const cur = (await this.store.list()).find((t) => t.name === typeName);
25834
+ if (!cur) return { ok: false, error: `\u7C7B\u578B\u4E0D\u5B58\u5728: ${typeName}` };
25835
+ if ((cur.fieldDefs ?? []).some((f2) => f2.name === spec.name)) {
25836
+ return { ok: false, error: `\u5B57\u6BB5\u5DF2\u58F0\u660E: ${spec.name}\uFF08\u6539\u5C5E\u6027\u7528 edit-field\u3001\u5220\u7528 drop-field\uFF09` };
25837
+ }
25838
+ return this.edit(typeName, { fieldDefs: [...cur.fieldDefs ?? [], spec] });
25839
+ }
25840
+ /** 改一条字段声明的属性(type/required/description);**name 钉死、不可改名**。 */
25841
+ async editField(typeName, fieldName, attrs) {
25842
+ const cur = (await this.store.list()).find((t) => t.name === typeName);
25843
+ if (!cur) return { ok: false, error: `\u7C7B\u578B\u4E0D\u5B58\u5728: ${typeName}` };
25844
+ const idx = (cur.fieldDefs ?? []).findIndex((f2) => f2.name === fieldName);
25845
+ 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` };
25846
+ const nextDefs = [...cur.fieldDefs];
25847
+ nextDefs[idx] = { ...nextDefs[idx], ...attrs, name: fieldName };
25848
+ return this.edit(typeName, { fieldDefs: nextDefs });
25849
+ }
25850
+ /** 删一条字段声明(deprecate 语义:存量值留、新写入拒)。返回 usage 供影响半径预览。 */
25851
+ async dropField(typeName, fieldName) {
25852
+ const cur = (await this.store.list()).find((t) => t.name === typeName);
25853
+ if (!cur) return { ok: false, error: `\u7C7B\u578B\u4E0D\u5B58\u5728: ${typeName}` };
25854
+ if (!(cur.fieldDefs ?? []).some((f2) => f2.name === fieldName)) {
25855
+ return { ok: false, error: `\u5B57\u6BB5\u672A\u58F0\u660E: ${fieldName}` };
25856
+ }
25857
+ return this.edit(typeName, { fieldDefs: (cur.fieldDefs ?? []).filter((f2) => f2.name !== fieldName) });
25858
+ }
25579
25859
  };
25580
25860
  }
25581
25861
  });
@@ -30047,6 +30327,10 @@ ${input.description}
30047
30327
  if (enabledKeys.size === 0) return [];
30048
30328
  return all.filter((s2) => enabledKeys.has(s2.id) || enabledKeys.has(s2.name));
30049
30329
  }
30330
+ /** 全局已装技能平铺列表(「默认挂」materialize 用;installedSkillsWithAssignments 带分配关系太重)。 */
30331
+ async listInstalledSkills() {
30332
+ return this.opts.store.listInstalledSkills();
30333
+ }
30050
30334
  /* ---------- 变量(2.8):AES-256-GCM,读侧只见掩码 ---------- */
30051
30335
  /**
30052
30336
  * 写入变量。
@@ -30565,6 +30849,61 @@ var init_routes = __esm({
30565
30849
  }
30566
30850
  });
30567
30851
 
30852
+ // ../server/src/domains/actors/skill-materializer.ts
30853
+ function skillsDirForRuntime(runtimeKind) {
30854
+ switch (runtimeKind) {
30855
+ case "claude":
30856
+ case "claude-code":
30857
+ return ".claude/skills";
30858
+ default:
30859
+ return ".agent_context/skills";
30860
+ }
30861
+ }
30862
+ function sanitizeSkillDir(name) {
30863
+ const s2 = name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
30864
+ return s2 || "skill";
30865
+ }
30866
+ async function materializeSkillFiles(args) {
30867
+ const own = await args.service.listInstalledSkillsForActor(args.actorId);
30868
+ let extras = [];
30869
+ if (args.extraSkillIds && args.extraSkillIds.length > 0) {
30870
+ const ownIds = new Set(own.map((s2) => s2.id));
30871
+ const wanted = new Set(args.extraSkillIds);
30872
+ extras = (await args.service.listInstalledSkills()).filter((s2) => wanted.has(s2.id) && !ownIds.has(s2.id));
30873
+ }
30874
+ const skills = [...own, ...extras];
30875
+ if (skills.length === 0) return {};
30876
+ const prefix = skillsDirForRuntime(args.runtimeKind);
30877
+ const out = {};
30878
+ const dec2 = new TextDecoder();
30879
+ const usedDirs = /* @__PURE__ */ new Set();
30880
+ for (const skill of skills) {
30881
+ const files = await args.service.listSkillFiles(skill.id);
30882
+ if (files.length === 0) continue;
30883
+ let dir = sanitizeSkillDir(skill.name);
30884
+ if (usedDirs.has(dir)) {
30885
+ let n = 2;
30886
+ while (usedDirs.has(`${dir}-${n}`)) n++;
30887
+ dir = `${dir}-${n}`;
30888
+ }
30889
+ usedDirs.add(dir);
30890
+ for (const f2 of files) {
30891
+ try {
30892
+ const bytes = await args.blobs.get(f2.blobHash);
30893
+ out[`${prefix}/${dir}/${f2.path}`] = dec2.decode(bytes);
30894
+ } catch (err) {
30895
+ console.warn(`[skill-materializer] skip ${skill.id}/${f2.path} (blob ${f2.blobHash.slice(0, 12)}\u2026): ${String(err)}`);
30896
+ }
30897
+ }
30898
+ }
30899
+ return out;
30900
+ }
30901
+ var init_skill_materializer = __esm({
30902
+ "../server/src/domains/actors/skill-materializer.ts"() {
30903
+ "use strict";
30904
+ }
30905
+ });
30906
+
30568
30907
  // ../server/src/domains/actors/index.ts
30569
30908
  async function syncRegistryRolesToKernel(store, kernel) {
30570
30909
  for (const actor of await store.listActors()) {
@@ -30622,6 +30961,7 @@ var init_actors = __esm({
30622
30961
  init_service2();
30623
30962
  init_routes();
30624
30963
  init_stats();
30964
+ init_skill_materializer();
30625
30965
  }
30626
30966
  });
30627
30967
 
@@ -32111,14 +32451,20 @@ OASIS_DIR="$(pwd)"
32111
32451
  NODE_DIR="$OASIS_DIR/${dir}"
32112
32452
  NODE_CMD="$OASIS_DIR/node_modules/.bin/tsx $OASIS_DIR/packages/cli/src/main.ts"
32113
32453
 
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
32454
+ # \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
32455
+ # CLI \u7684 oasis node \u9996\u6B21\u542F\u52A8\u4F1A\u7528\u5B83\u5411\u670D\u52A1\u7AEF exchange \u53D6\u56DE nodeId + \u957F\u671F node-token\uFF0C
32456
+ # \u4E4B\u540E\u843D\u76D8\u5230 <dir>/node-id \u4E0E <dir>/node-token\uFF0C\u91CD\u542F\u4E0D\u518D exchange\u3002
32116
32457
  mkdir -p "$NODE_DIR"
32117
- ( umask 077; cat > "$NODE_DIR/node-token" <<'OASIS_NODE_TOKEN_EOF'
32458
+ ( umask 077; cat > "$NODE_DIR/enroll-token" <<'OASIS_ENROLL_TOKEN_EOF'
32118
32459
  ${p2.token}
32119
- OASIS_NODE_TOKEN_EOF
32460
+ OASIS_ENROLL_TOKEN_EOF
32120
32461
  )
32121
32462
 
32463
+ # "Script was run" marker: tells the daemon to (re)report its available runtimes once on
32464
+ # next start. Adapters are picked up only on an explicit script run \u2014 never on plain
32465
+ # reconnects/restarts \u2014 so admin-side runtime deletions stay deleted until you re-run this.
32466
+ touch "$NODE_DIR/report-runtimes"
32467
+
32122
32468
  # \u7528 systemd \u7528\u6237\u670D\u52A1\uFF08Linux\uFF09\u4FDD\u6301\u540E\u53F0\u5E38\u9A7B + \u5F00\u673A\u81EA\u542F
32123
32469
  if command -v systemctl &>/dev/null && systemctl --user daemon-reload &>/dev/null 2>&1; then
32124
32470
  SERVICE_FILE=~/.config/systemd/user/oasis-node.service
@@ -32133,11 +32479,11 @@ if command -v systemctl &>/dev/null && systemctl --user daemon-reload &>/dev/nul
32133
32479
  echo '\u2192 \u5199\u5165 systemd \u7528\u6237\u670D\u52A1\u914D\u7F6E...'
32134
32480
  cat > "$SERVICE_FILE" << EOF
32135
32481
  [Unit]
32136
- Description=Oasis Node Daemon (${p2.nodeId})
32482
+ Description=Oasis Node Daemon
32137
32483
  After=network.target
32138
32484
 
32139
32485
  [Service]
32140
- ExecStart=$NODE_CMD node --server-ws ${p2.gatewayUrl} --id ${p2.nodeId} --dir $NODE_DIR
32486
+ ExecStart=$NODE_CMD node --server-ws ${p2.gatewayUrl} --dir $NODE_DIR
32141
32487
  WorkingDirectory=$OASIS_DIR
32142
32488
  Restart=on-failure
32143
32489
  RestartSec=5
@@ -32176,7 +32522,6 @@ elif [[ "$(uname)" == "Darwin" ]]; then
32176
32522
  <string>$MAIN</string>
32177
32523
  <string>node</string>
32178
32524
  <string>--server-ws</string><string>${p2.gatewayUrl}</string>
32179
- <string>--id</string><string>${p2.nodeId}</string>
32180
32525
  <string>--dir</string><string>$NODE_DIR</string>
32181
32526
  </array>
32182
32527
  <key>WorkingDirectory</key><string>$OASIS_DIR</string>
@@ -32204,7 +32549,7 @@ else
32204
32549
  sleep 1
32205
32550
  fi
32206
32551
  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 &
32552
+ nohup $NODE_CMD node --server-ws ${p2.gatewayUrl} --dir $NODE_DIR > ~/.oasis-node.log 2>&1 &
32208
32553
  echo "\u2713 \u8282\u70B9\u5DF2\u5728\u540E\u53F0\u542F\u52A8\uFF08PID $!\uFF09"
32209
32554
  echo ' \u65E5\u5FD7\uFF1Atail -f ~/.oasis-node.log'
32210
32555
  echo ' \u505C\u6B62\uFF1Apkill -f "oasis node"'
@@ -32239,9 +32584,10 @@ if (-not (Test-Path "node_modules")) {
32239
32584
  Write-Host "\u2192 Dependencies already present, skipping."
32240
32585
  }
32241
32586
 
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
32587
+ # \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
32588
+ # \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
32589
  New-Item -ItemType Directory -Force -Path $NODE_DIR | Out-Null
32244
- $tokenPath = "$NODE_DIR\\node-token"
32590
+ $tokenPath = "$NODE_DIR\\enroll-token"
32245
32591
  Set-Content -Path $tokenPath -Value "${p2.token}" -NoNewline -Encoding utf8
32246
32592
  $acl = Get-Acl $tokenPath
32247
32593
  $acl.SetAccessRuleProtection($true, $false)
@@ -32250,10 +32596,15 @@ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
32250
32596
  $acl.AddAccessRule($rule)
32251
32597
  Set-Acl $tokenPath $acl
32252
32598
 
32599
+ # "Script was run" marker: tells the daemon to (re)report its available runtimes once on
32600
+ # next start. Adapters are picked up only on an explicit script run \u2014 never on plain
32601
+ # reconnects/restarts \u2014 so admin-side runtime deletions stay deleted until you re-run this.
32602
+ Set-Content -Path "$NODE_DIR\\report-runtimes" -Value "" -NoNewline -Encoding utf8
32603
+
32253
32604
  # \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
32605
  $tsx = "$INSTALL_DIR\\node_modules\\.bin\\tsx.cmd"
32255
32606
  $main = "$INSTALL_DIR\\packages\\cli\\src\\main.ts"
32256
- $args = "\`"$main\`" node --server-ws ${p2.gatewayUrl} --id ${p2.nodeId} --dir \`"$NODE_DIR\`""
32607
+ $args = "\`"$main\`" node --server-ws ${p2.gatewayUrl} --dir \`"$NODE_DIR\`""
32257
32608
 
32258
32609
  $action = New-ScheduledTaskAction -Execute $tsx -Argument $args -WorkingDirectory $INSTALL_DIR
32259
32610
  $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
@@ -32261,23 +32612,23 @@ $settings = New-ScheduledTaskSettingsSet -RestartCount 5 \`
32261
32612
  -RestartInterval (New-TimeSpan -Minutes 1) \`
32262
32613
  -ExecutionTimeLimit ([TimeSpan]::Zero) \`
32263
32614
  -StartWhenAvailable $true
32264
- Register-ScheduledTask -TaskName "OasisNode-${p2.nodeId}" \`
32615
+ Register-ScheduledTask -TaskName "OasisNode" \`
32265
32616
  -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null
32266
32617
 
32267
32618
  # \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}"
32619
+ Start-ScheduledTask -TaskName "OasisNode"
32269
32620
  Pop-Location
32270
32621
 
32271
32622
  Write-Host ""
32272
32623
  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"
32624
+ Write-Host " Status : Get-ScheduledTask -TaskName 'OasisNode'"
32625
+ Write-Host " Stop : Stop-ScheduledTask -TaskName 'OasisNode'"
32626
+ Write-Host " Remove : Unregister-ScheduledTask -TaskName 'OasisNode' -Confirm:$false"
32276
32627
  `;
32277
32628
  }
32278
32629
  function buildNpxCmd(p2) {
32279
32630
  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}`;
32631
+ return `npx -y oasis_test@latest start --server-ws ${p2.gatewayUrl}${namePart} --enroll-token ${p2.token}`;
32281
32632
  }
32282
32633
  var init_connect_script = __esm({
32283
32634
  "../server/src/domains/nodes/connect-script.ts"() {
@@ -32287,7 +32638,6 @@ var init_connect_script = __esm({
32287
32638
 
32288
32639
  // ../server/src/domains/nodes/routes.ts
32289
32640
  function nodesDomain(deps) {
32290
- const nodeNames = /* @__PURE__ */ new Map();
32291
32641
  let hubWired = false;
32292
32642
  function wireHub(hub) {
32293
32643
  if (hubWired) return;
@@ -32298,19 +32648,20 @@ function nodesDomain(deps) {
32298
32648
  if (msg.type !== "hello") return;
32299
32649
  const d = hub.connectedDaemons().find((c) => c.daemonId === daemonId);
32300
32650
  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({
32651
+ if (msg.meta.runtimes) {
32652
+ const existing = await nodeStore.getNode(daemonId);
32653
+ await nodeStore.upsertNode({
32654
+ id: daemonId,
32655
+ hostname: d.meta.hostname,
32656
+ nodeVersion: d.meta.nodeVersion ?? "unknown",
32657
+ status: "online",
32658
+ // revives a previously soft-deleted node
32659
+ lastSeenAt: now(),
32660
+ enrolledAt: existing?.enrolledAt ?? now(),
32661
+ ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
32662
+ });
32663
+ for (const cap of msg.meta.runtimes) {
32664
+ await nodeStore.addRuntimeIfAbsent({
32314
32665
  id: `runtime:${daemonId}:${cap.kind}`,
32315
32666
  nodeId: daemonId,
32316
32667
  kind: cap.kind,
@@ -32321,7 +32672,9 @@ function nodesDomain(deps) {
32321
32672
  lastSeenAt: now()
32322
32673
  });
32323
32674
  }
32675
+ await nodeStore.setNodeRuntimesOnline(daemonId, true);
32324
32676
  } else {
32677
+ await nodeStore.setNodeOnline(daemonId, true);
32325
32678
  await nodeStore.setNodeRuntimesOnline(daemonId, true);
32326
32679
  }
32327
32680
  });
@@ -32331,35 +32684,24 @@ function nodesDomain(deps) {
32331
32684
  };
32332
32685
  }
32333
32686
  return (router) => {
32687
+ let wirePoll = null;
32688
+ const tryWire = () => {
32689
+ const hub = deps.getHub();
32690
+ if (hub) {
32691
+ wireHub(hub);
32692
+ if (wirePoll) {
32693
+ clearInterval(wirePoll);
32694
+ wirePoll = null;
32695
+ }
32696
+ }
32697
+ };
32698
+ tryWire();
32699
+ if (!hubWired) wirePoll = setInterval(tryWire, 100);
32334
32700
  async function syncConnectedDaemon(daemonId, hub) {
32335
32701
  const d = hub.connectedDaemons().find((c) => c.daemonId === daemonId);
32336
32702
  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
- }
32703
+ await deps.nodeStore.setNodeOnline(daemonId, true);
32704
+ await deps.nodeStore.setNodeRuntimesOnline(daemonId, true);
32363
32705
  }
32364
32706
  router.get("/api/nodes", async () => {
32365
32707
  const hub = deps.getHub();
@@ -32414,16 +32756,11 @@ function nodesDomain(deps) {
32414
32756
  router.post("/api/nodes/enroll", async (req) => {
32415
32757
  const body = req.body ?? {};
32416
32758
  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 };
32759
+ const enroll = deps.enrollTokens.issue(nodeName ?? "");
32760
+ const params = { gatewayUrl: deps.gatewayUrl, token: enroll.token, repoRemote: deps.repoRemote };
32424
32761
  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 } };
32762
+ const npxCmd = buildNpxCmd({ gatewayUrl: deps.gatewayUrl, token: enroll.token, nodeName });
32763
+ return { status: 200, body: { nodeName, token: enroll.token, script, gatewayUrl: deps.gatewayUrl, npxCmd } };
32427
32764
  });
32428
32765
  router.post("/api/nodes/exchange", async (req) => {
32429
32766
  const body = req.body ?? {};
@@ -32431,15 +32768,22 @@ function nodesDomain(deps) {
32431
32768
  if (!enrollToken) return { status: 400, body: { error: "missing enrollToken" } };
32432
32769
  const entry = deps.enrollTokens.peek(enrollToken);
32433
32770
  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);
32771
+ const incomingNodeId = body.nodeId?.trim();
32772
+ const ownsIncoming = !!incomingNodeId && (deps.nodeTokens.list().some((e) => e.nodeId === incomingNodeId) || await deps.nodeStore.getNode(incomingNodeId) !== null);
32773
+ let nodeId;
32774
+ if (entry.issuedNodeId) {
32775
+ if (incomingNodeId && incomingNodeId !== entry.issuedNodeId && !ownsIncoming)
32776
+ return { status: 409, body: { error: "nodeId mismatch" } };
32777
+ nodeId = entry.issuedNodeId;
32778
+ } else if (ownsIncoming) {
32779
+ nodeId = incomingNodeId;
32780
+ deps.enrollTokens.setIssuedNodeId(enrollToken, nodeId);
32781
+ } else {
32782
+ nodeId = `node-${(0, import_node_crypto9.randomUUID)().slice(0, 8)}`;
32783
+ deps.enrollTokens.setIssuedNodeId(enrollToken, nodeId);
32784
+ }
32785
+ deps.enrollTokens.consume(enrollToken);
32786
+ if (entry.name) await deps.nodeStore.updateNodeName(nodeId, entry.name);
32443
32787
  const existing = deps.nodeTokens.list().find((e) => e.nodeId === nodeId);
32444
32788
  const token = existing?.token ?? deps.nodeTokens.issue(nodeId);
32445
32789
  return { status: 200, body: { nodeId, token } };
@@ -32505,6 +32849,24 @@ function typesDomain(deps) {
32505
32849
  const r = await deps.admin.delete(name);
32506
32850
  return r.ok ? { status: 200, body: { ok: true } } : { status: 400, body: { error: r.error, ...r.usage ? { usage: r.usage } : {} } };
32507
32851
  });
32852
+ router.post("/api/artifact-types/:name/fields", async (req) => {
32853
+ const name = req.params["name"];
32854
+ if (!name) return { status: 400, body: { error: "missing name" } };
32855
+ const r = await deps.admin.addField(name, req.body ?? {});
32856
+ return r.ok ? { status: 200, body: { ok: true, ...r.usage ? { usage: r.usage } : {} } } : { status: 400, body: { error: r.error } };
32857
+ });
32858
+ router.patch("/api/artifact-types/:name/fields/:field", async (req) => {
32859
+ const { name, field } = req.params;
32860
+ if (!name || !field) return { status: 400, body: { error: "missing name/field" } };
32861
+ const r = await deps.admin.editField(name, field, req.body ?? {});
32862
+ return r.ok ? { status: 200, body: { ok: true, ...r.usage ? { usage: r.usage } : {} } } : { status: 400, body: { error: r.error } };
32863
+ });
32864
+ router.delete("/api/artifact-types/:name/fields/:field", async (req) => {
32865
+ const { name, field } = req.params;
32866
+ if (!name || !field) return { status: 400, body: { error: "missing name/field" } };
32867
+ const r = await deps.admin.dropField(name, field);
32868
+ return r.ok ? { status: 200, body: { ok: true, ...r.usage ? { usage: r.usage } : {} } } : { status: 400, body: { error: r.error } };
32869
+ });
32508
32870
  };
32509
32871
  }
32510
32872
  var init_types3 = __esm({
@@ -32522,17 +32884,20 @@ var init_node_store = __esm({
32522
32884
  MemoryNodeStore = class {
32523
32885
  nodes = /* @__PURE__ */ new Map();
32524
32886
  runtimes = /* @__PURE__ */ new Map();
32525
- deletedRuntimeIds = /* @__PURE__ */ new Set();
32526
32887
  async upsertNode(node) {
32527
32888
  this.nodes.set(node.id, { ...this.nodes.get(node.id), ...node });
32528
32889
  }
32529
32890
  async upsertRuntime(rt) {
32530
- if (this.deletedRuntimeIds.has(rt.id)) return;
32531
32891
  this.runtimes.set(rt.id, { ...this.runtimes.get(rt.id), ...rt });
32532
32892
  }
32893
+ async addRuntimeIfAbsent(rt) {
32894
+ if (this.runtimes.has(rt.id)) return false;
32895
+ this.runtimes.set(rt.id, rt);
32896
+ return true;
32897
+ }
32533
32898
  async setNodeOnline(id, online) {
32534
32899
  const n = this.nodes.get(id);
32535
- if (n) {
32900
+ if (n && n.status !== "deleted") {
32536
32901
  n.status = online ? "online" : "offline";
32537
32902
  n.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
32538
32903
  }
@@ -32554,7 +32919,7 @@ var init_node_store = __esm({
32554
32919
  return this.nodes.get(id) ?? null;
32555
32920
  }
32556
32921
  async listNodes() {
32557
- return [...this.nodes.values()];
32922
+ return [...this.nodes.values()].filter((n) => n.status !== "deleted");
32558
32923
  }
32559
32924
  async listRuntimes(nodeId) {
32560
32925
  const all = [...this.runtimes.values()];
@@ -32565,23 +32930,24 @@ var init_node_store = __esm({
32565
32930
  for (const [k2, rt] of this.runtimes) {
32566
32931
  if (rt.nodeId === id) this.runtimes.delete(k2);
32567
32932
  }
32568
- for (const k2 of [...this.deletedRuntimeIds]) {
32569
- if (k2.startsWith(`runtime:${id}:`)) this.deletedRuntimeIds.delete(k2);
32570
- }
32571
32933
  }
32572
- async clearDeletedRuntimesForNode(nodeId) {
32573
- for (const id of [...this.deletedRuntimeIds]) {
32574
- if (id.startsWith(`runtime:${nodeId}:`)) this.deletedRuntimeIds.delete(id);
32934
+ async softDeleteNode(id) {
32935
+ const n = this.nodes.get(id);
32936
+ if (n) {
32937
+ n.status = "deleted";
32938
+ n.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
32939
+ }
32940
+ for (const [k2, rt] of this.runtimes) {
32941
+ if (rt.nodeId === id) this.runtimes.delete(k2);
32575
32942
  }
32576
32943
  }
32577
32944
  async deleteRuntime(id) {
32578
- this.deletedRuntimeIds.add(id);
32579
32945
  const rt = this.runtimes.get(id);
32580
32946
  this.runtimes.delete(id);
32581
32947
  if (!rt) return { nodeDeleted: false };
32582
32948
  const remaining = [...this.runtimes.values()].filter((r) => r.nodeId === rt.nodeId);
32583
32949
  if (remaining.length === 0) {
32584
- this.nodes.delete(rt.nodeId);
32950
+ await this.softDeleteNode(rt.nodeId);
32585
32951
  return { nodeDeleted: true };
32586
32952
  }
32587
32953
  return { nodeDeleted: false };
@@ -32598,7 +32964,6 @@ var init_node_store = __esm({
32598
32964
  const snap = JSON.parse(fs5.readFileSync(file, "utf8"));
32599
32965
  for (const n of snap.nodes ?? []) s2.nodes.set(n.id, n);
32600
32966
  for (const r of snap.runtimes ?? []) s2.runtimes.set(r.id, r);
32601
- for (const id of snap.deletedRuntimeIds ?? []) s2.deletedRuntimeIds.add(id);
32602
32967
  } catch {
32603
32968
  fs5.writeFileSync(file, JSON.stringify({ nodes: [], runtimes: [] }, null, 2));
32604
32969
  }
@@ -32607,8 +32972,7 @@ var init_node_store = __esm({
32607
32972
  flush() {
32608
32973
  fs5.writeFileSync(this.file, JSON.stringify({
32609
32974
  nodes: [...this.nodes.values()],
32610
- runtimes: [...this.runtimes.values()],
32611
- deletedRuntimeIds: [...this.deletedRuntimeIds]
32975
+ runtimes: [...this.runtimes.values()]
32612
32976
  }, null, 2));
32613
32977
  }
32614
32978
  async upsertNode(node) {
@@ -32619,6 +32983,11 @@ var init_node_store = __esm({
32619
32983
  await super.upsertRuntime(rt);
32620
32984
  this.flush();
32621
32985
  }
32986
+ async addRuntimeIfAbsent(rt) {
32987
+ const r = await super.addRuntimeIfAbsent(rt);
32988
+ if (r) this.flush();
32989
+ return r;
32990
+ }
32622
32991
  async setNodeOnline(id, online) {
32623
32992
  await super.setNodeOnline(id, online);
32624
32993
  this.flush();
@@ -32635,8 +33004,8 @@ var init_node_store = __esm({
32635
33004
  await super.deleteNode(id);
32636
33005
  this.flush();
32637
33006
  }
32638
- async clearDeletedRuntimesForNode(nodeId) {
32639
- await super.clearDeletedRuntimesForNode(nodeId);
33007
+ async softDeleteNode(id) {
33008
+ await super.softDeleteNode(id);
32640
33009
  this.flush();
32641
33010
  }
32642
33011
  async deleteRuntime(id) {
@@ -32649,10 +33018,10 @@ var init_node_store = __esm({
32649
33018
  });
32650
33019
 
32651
33020
  // ../server/src/domains/collab/workorder-detail.ts
32652
- function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject, dispatchJournal = []) {
33021
+ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject, dispatchJournal = [], dispatchPausedOf) {
32653
33022
  const arts = [...model.artifacts.values()].filter((a) => a.workspace === workspaceId);
32654
33023
  if (arts.length === 0) return null;
32655
- const summary = buildWorkorderSummaries(model, resolveActor, resolveProject).find((w2) => w2.id === workspaceId);
33024
+ const summary = buildWorkorderSummaries(model, resolveActor, resolveProject, dispatchPausedOf).find((w2) => w2.id === workspaceId);
32656
33025
  if (!summary) return null;
32657
33026
  const ref = (id) => {
32658
33027
  const extra = resolveActor?.(id);
@@ -32823,6 +33192,26 @@ var init_workorder_detail = __esm({
32823
33192
  // ../server/src/domains/collab/inbox.ts
32824
33193
  function buildInbox(model, me) {
32825
33194
  const out = [];
33195
+ const creatorOf = /* @__PURE__ */ new Map();
33196
+ for (const a of model.artifacts.values()) {
33197
+ if (a.type === "brief") creatorOf.set(a.workspace, a.owner);
33198
+ }
33199
+ const staffingSeen = /* @__PURE__ */ new Set();
33200
+ for (const a of model.artifacts.values()) {
33201
+ const pendingRole = /^pending:role:(.+)$/.exec(a.owner);
33202
+ if (!pendingRole || creatorOf.get(a.workspace) !== me) continue;
33203
+ const key = `${a.workspace}:${pendingRole[1]}`;
33204
+ if (staffingSeen.has(key)) continue;
33205
+ staffingSeen.add(key);
33206
+ out.push({
33207
+ kind: "needs-staffing",
33208
+ artifactId: a.id,
33209
+ workspace: a.workspace,
33210
+ since: model.lastOpAt.get(a.id) ?? "",
33211
+ actionRequired: true,
33212
+ 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`
33213
+ });
33214
+ }
32826
33215
  for (const a of model.artifacts.values()) {
32827
33216
  const ws = a.workspace;
32828
33217
  const label = nodeLabel(a);
@@ -33018,6 +33407,11 @@ async function buildProjectResolver(artifacts) {
33018
33407
  const byWorkOrder = new Map(bindings.map((binding) => [binding.workOrderId, binding.projectId]));
33019
33408
  return (workOrderId) => byWorkOrder.get(workOrderId) ?? null;
33020
33409
  }
33410
+ async function buildDispatchPausedResolver(artifacts) {
33411
+ if (!artifacts) return void 0;
33412
+ const held = new Set(await artifacts.listWorkspaceDispatchHeld());
33413
+ return (workspace) => held.has(workspace);
33414
+ }
33021
33415
  function collabDomain(opts) {
33022
33416
  const defaultCtx = {
33023
33417
  kernel: opts.kernel,
@@ -33044,28 +33438,30 @@ function collabDomain(opts) {
33044
33438
  const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
33045
33439
  const resolve3 = await buildResolver(registry2);
33046
33440
  const resolveProject = await buildProjectResolver(artifacts);
33047
- return { status: 200, body: { items: buildWorkorderSummaries(kernel.model, resolve3, resolveProject) } };
33441
+ const dispatchPausedOf = await buildDispatchPausedResolver(artifacts);
33442
+ return { status: 200, body: { items: buildWorkorderSummaries(kernel.model, resolve3, resolveProject, dispatchPausedOf) } };
33048
33443
  });
33049
33444
  router.get("/api/workorders/:id", async (req) => {
33050
33445
  const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
33051
33446
  const resolve3 = await buildResolver(registry2);
33052
33447
  const resolveProject = await buildProjectResolver(artifacts);
33053
- const detail = buildWorkorderDetail(kernel.model, req.params.id, resolve3, resolveProject, opts.dispatchJournal?.() ?? []);
33448
+ const dispatchPausedOf = await buildDispatchPausedResolver(artifacts);
33449
+ const detail = buildWorkorderDetail(kernel.model, req.params.id, resolve3, resolveProject, opts.dispatchJournal?.() ?? [], dispatchPausedOf);
33054
33450
  if (!detail) {
33055
33451
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${req.params.id}` } } };
33056
33452
  }
33057
33453
  return { status: 200, body: detail };
33058
33454
  });
33059
33455
  router.post("/api/workorders/:id/dispatch", async (req) => {
33060
- const { kernel } = await resolveCtx(req.auth.companyId);
33456
+ const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
33061
33457
  const ws = req.params.id;
33062
33458
  const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws);
33063
33459
  if (arts.length === 0) {
33064
33460
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
33065
33461
  }
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" });
33462
+ const held = artifacts ? (await artifacts.listWorkspaceDispatchHeld()).includes(ws) : false;
33463
+ if (!held) return { status: 200, body: { dispatched: false } };
33464
+ await artifacts.setWorkspaceDispatchHold(ws, false);
33069
33465
  return { status: 200, body: { dispatched: true } };
33070
33466
  });
33071
33467
  router.get("/api/inbox", async (req) => {
@@ -34016,23 +34412,6 @@ function createChatSessionsDomain(opts) {
34016
34412
  await store.updateSession(req.params.id, { touchedAt: (/* @__PURE__ */ new Date()).toISOString() });
34017
34413
  return { status: 201, body: msg };
34018
34414
  });
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
34415
  };
34037
34416
  }
34038
34417
  var import_node_crypto10;
@@ -34041,7 +34420,6 @@ var init_chat_sessions = __esm({
34041
34420
  "use strict";
34042
34421
  import_node_crypto10 = require("node:crypto");
34043
34422
  init_router();
34044
- init_workorders();
34045
34423
  }
34046
34424
  });
34047
34425
 
@@ -35582,16 +35960,16 @@ var init_worker = __esm({
35582
35960
  \u2461 **\u786E\u5C5E\u56FE\u75C5\u624D\u4E0B\u5200**\uFF08link / spawn / cancel-part / \u6539\u8FDE\uFF09\uFF1B
35583
35961
  \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
35962
 
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
35963
+ \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
35964
+ - \`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
35965
+ - \`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
35966
+ - \`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
35967
+ - \`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
35968
+ - \`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
35969
+ - \`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
35970
+ - \`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
35971
+ - \`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
35972
+ - \`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
35973
  \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
35974
  - \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
35975
  - \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 +35977,10 @@ var init_worker = __esm({
35599
35977
  - \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
35978
 
35601
35979
  \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`;
35980
+ - \`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
35981
+ - \`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
35982
+ - \`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
35983
+ - \`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
35984
  CoordinatorWorker = class {
35607
35985
  constructor(deps) {
35608
35986
  this.deps = deps;
@@ -35751,15 +36129,6 @@ var init_worker = __esm({
35751
36129
  gaps.length > 0 ? `\u672A\u89E3\u6790gap\xD7${gaps.length}` : null
35752
36130
  ].filter(Boolean).join("\u3001");
35753
36131
  }
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
36132
  /** 结构摘要(reconcile 去重用):id / type / 是否已交付 / title+brief / 依赖——不含进度态(去重只看语义)。 */
35764
36133
  structDigest(exclude) {
35765
36134
  const m2 = this.deps.kernel.model;
@@ -35804,22 +36173,51 @@ var init_worker = __esm({
35804
36173
  * 真死锁且某 part 卡住整条集成 → 起草 cancelPart(C 类,人确认);能解就解;拿不准 / 非图病 → 别动、交人。
35805
36174
  */
35806
36175
  async reconcileDeadlock(p2) {
35807
- const node = this.deps.kernel.model.artifacts.get(p2.id);
36176
+ const m2 = this.deps.kernel.model;
36177
+ const node = m2.artifacts.get(p2.id);
35808
36178
  const mins = Math.round(p2.staleMs / 6e4);
35809
- const parts = node?.parts?.map((x2) => x2.name).join("\u3001") || "\uFF08\u65E0\u5206\u89E3\uFF09";
35810
36179
  const brief = node?.title ?? node?.description ?? "";
36180
+ const now = Date.now();
35811
36181
  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";
36182
+ const sessionsLine = (id) => {
36183
+ const ss = this.deps.sessionsOf?.(id) ?? [];
36184
+ if (ss.length === 0) return "\u65E0\u5728\u8DD1\u4F1A\u8BDD";
36185
+ 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")}`;
36186
+ };
36187
+ const nodeBlock = (id) => {
36188
+ const a = m2.artifacts.get(id);
36189
+ if (!a) return `- **${id}**\uFF08\u4E0D\u5B58\u5728 / \u5DF2\u88AB\u6E05\u7406\uFF09`;
36190
+ const head = a.currentRev ? `head@${String(a.currentRev).slice(9, 17)}` : "\u65E0head";
36191
+ const blk = blockedOf(m2, id);
36192
+ const status = [lifecycleOf(m2, id), head, blk.blocked ? `\u963B\u585E[${blk.reasons.join("\uFF1B")}]` : null].filter(Boolean).join("\uFF5C");
36193
+ const label = a.title ?? a.description ?? "\uFF08\u65E0 brief\uFF09";
36194
+ return [
36195
+ `- **${id}**\uFF08type=${a.type}\uFF1A${label}\uFF09`,
36196
+ ` - \u72B6\u6001\uFF1A${status}${a.parts?.length ? `\uFF5C\u90E8\u4EF6 [${a.parts.map((x2) => x2.name).join("\u3001")}]` : ""}`,
36197
+ ` - \u5728\u8DD1\u4F1A\u8BDD\uFF1A${sessionsLine(id)}`
36198
+ ].join("\n");
36199
+ };
36200
+ const upstreams = (node?.inputs ?? []).map((e) => e.to);
36201
+ const downstreams = consumersOf(m2, p2.id).map((a) => a.id);
35812
36202
  const task = [
35813
36203
  `## \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`,
36204
+ `\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
36205
  ``,
35816
36206
  `## \u75C5\u5386\uFF08\u673A\u68B0\u8BCA\u65AD\uFF1B\u5148\u8BFB\u8FD9\u4E2A\uFF09`,
35817
36207
  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`,
36208
+ `\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`,
36209
+ ``,
36210
+ `## \u672C\u8282\u70B9 + \u90BB\u57DF`,
36211
+ `> \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`,
36212
+ ``,
36213
+ `### \u672C\u8282\u70B9`,
36214
+ nodeBlock(p2.id),
36215
+ ``,
36216
+ `### \u4E0A\u6E38\uFF08\u672C\u8282\u70B9\u4F9D\u8D56\u8C01\u2014\u2014\u5B83\u4EEC\u6CA1\u5C31\u7EEA\uFF0C\u672C\u8282\u70B9\u52A8\u4E0D\u4E86\uFF09`,
36217
+ upstreams.length ? upstreams.map(nodeBlock).join("\n") : "- \uFF08\u65E0\u4E0A\u6E38\u4F9D\u8D56\uFF09",
35819
36218
  ``,
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),
36219
+ `### \u4E0B\u6E38\uFF08\u8C01\u4F9D\u8D56\u672C\u8282\u70B9\u2014\u2014\u672C\u8282\u70B9\u5361\uFF0C\u5B83\u4EEC\u8DDF\u7740\u5361\uFF09`,
36220
+ downstreams.length ? downstreams.map(nodeBlock).join("\n") : "- \uFF08\u65E0\u4E0B\u6E38\u6D88\u8D39\u8005\uFF09",
35823
36221
  ``,
35824
36222
  `## \u4F60\u7684\u52A8\u4F5C\uFF08\u6682\u5B58 \u2192 apply\uFF09`,
35825
36223
  `- \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 +36305,7 @@ var init_src4 = __esm({
35907
36305
  init_resolve();
35908
36306
  init_worker();
35909
36307
  init_drafts();
36308
+ init_workorder_drafts();
35910
36309
  }
35911
36310
  });
35912
36311
 
@@ -37478,11 +37877,11 @@ var require_pg_connection_string = __commonJS({
37478
37877
  config2.client_encoding = result.searchParams.get("encoding");
37479
37878
  return config2;
37480
37879
  }
37481
- const hostname3 = dummyHost ? "" : result.hostname;
37880
+ const hostname5 = dummyHost ? "" : result.hostname;
37482
37881
  if (!config2.host) {
37483
- config2.host = decodeURIComponent(hostname3);
37484
- } else if (hostname3 && /^%2f/i.test(hostname3)) {
37485
- result.pathname = hostname3 + result.pathname;
37882
+ config2.host = decodeURIComponent(hostname5);
37883
+ } else if (hostname5 && /^%2f/i.test(hostname5)) {
37884
+ result.pathname = hostname5 + result.pathname;
37486
37885
  }
37487
37886
  if (!config2.port) {
37488
37887
  config2.port = result.port;
@@ -41092,7 +41491,6 @@ var init_trajectory = __esm({
41092
41491
  });
41093
41492
 
41094
41493
  // src/index.ts
41095
- var import_node_crypto22 = require("node:crypto");
41096
41494
  var fs15 = __toESM(require("node:fs"));
41097
41495
  var os8 = __toESM(require("node:os"));
41098
41496
  var path13 = __toESM(require("node:path"));
@@ -41874,6 +42272,9 @@ var CodexAdapter = class {
41874
42272
  "-C",
41875
42273
  dir,
41876
42274
  ...this.opts.model ? ["--model", this.opts.model] : [],
42275
+ // 打开 reasoning 摘要通道:默认 auto,让 codex 产出 reasoning item(→ thought → 前端思考块)。
42276
+ ...this.opts.reasoningSummary !== "none" ? ["-c", `model_reasoning_summary=${this.opts.reasoningSummary ?? "auto"}`] : [],
42277
+ ...this.opts.reasoningEffort ? ["-c", `model_reasoning_effort=${this.opts.reasoningEffort}`] : [],
41877
42278
  ...this.opts.extraArgs ?? [],
41878
42279
  prompt
41879
42280
  ];
@@ -43907,6 +44308,10 @@ var PostgresArtifactStateStore = class _PostgresArtifactStateStore {
43907
44308
  work_order_id text PRIMARY KEY,
43908
44309
  project_id text NOT NULL
43909
44310
  )`);
44311
+ await pool.query(`
44312
+ CREATE TABLE IF NOT EXISTS "${s2}".workspace_dispatch_hold (
44313
+ work_order_id text PRIMARY KEY
44314
+ )`);
43910
44315
  return new _PostgresArtifactStateStore(pool, schema);
43911
44316
  }
43912
44317
  /* ---------- artifacts ---------- */
@@ -44180,14 +44585,29 @@ var PostgresArtifactStateStore = class _PostgresArtifactStateStore {
44180
44585
  const r = await this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`);
44181
44586
  return r.rows.map(rowToWorkspaceBinding);
44182
44587
  }
44588
+ async setWorkspaceDispatchHold(workOrderId, held) {
44589
+ if (held) {
44590
+ await this.pool.query(
44591
+ `INSERT INTO ${this.s}.workspace_dispatch_hold (work_order_id) VALUES ($1) ON CONFLICT DO NOTHING`,
44592
+ [workOrderId]
44593
+ );
44594
+ } else {
44595
+ await this.pool.query(`DELETE FROM ${this.s}.workspace_dispatch_hold WHERE work_order_id = $1`, [workOrderId]);
44596
+ }
44597
+ }
44598
+ async listWorkspaceDispatchHeld() {
44599
+ const r = await this.pool.query(`SELECT work_order_id FROM ${this.s}.workspace_dispatch_hold`);
44600
+ return r.rows.map((row) => String(row.work_order_id));
44601
+ }
44183
44602
  async listAll() {
44184
- const [artifacts, revisions, events, annotations, outputs, bindings] = await Promise.all([
44603
+ const [artifacts, revisions, events, annotations, outputs, bindings, held] = await Promise.all([
44185
44604
  this.pool.query(`SELECT * FROM ${this.s}.artifacts`),
44186
44605
  this.pool.query(`SELECT * FROM ${this.s}.artifact_revisions`),
44187
44606
  this.pool.query(`SELECT * FROM ${this.s}.artifact_events ORDER BY seq`),
44188
44607
  this.pool.query(`SELECT * FROM ${this.s}.artifact_annotations`),
44189
44608
  this.pool.query(`SELECT * FROM ${this.s}.node_outputs`),
44190
- this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`)
44609
+ this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`),
44610
+ this.pool.query(`SELECT work_order_id FROM ${this.s}.workspace_dispatch_hold`)
44191
44611
  ]);
44192
44612
  const nodeOutputs = [];
44193
44613
  for (const row of outputs.rows) {
@@ -44203,7 +44623,8 @@ var PostgresArtifactStateStore = class _PostgresArtifactStateStore {
44203
44623
  events: events.rows.map(rowToEvent),
44204
44624
  annotations: annotations.rows.map(rowToAnnotation),
44205
44625
  nodeOutputs,
44206
- workspaceBindings: bindings.rows.map(rowToWorkspaceBinding)
44626
+ workspaceBindings: bindings.rows.map(rowToWorkspaceBinding),
44627
+ workspaceDispatchHeld: held.rows.map((row) => String(row.work_order_id))
44207
44628
  };
44208
44629
  }
44209
44630
  /**
@@ -45249,13 +45670,6 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
45249
45670
  created_at timestamptz NOT NULL
45250
45671
  )`);
45251
45672
  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
45673
  return new _PostgresChatSessionStore(pool, schema);
45260
45674
  }
45261
45675
  async createSession(s2) {
@@ -45319,19 +45733,6 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
45319
45733
  const rows = r.rows.map(rowToMessage);
45320
45734
  return limit ? rows.reverse() : rows;
45321
45735
  }
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
45736
  };
45336
45737
  var rowToSession = (row) => ({
45337
45738
  id: row.id,
@@ -45389,10 +45790,6 @@ var PostgresNodeStore = class _PostgresNodeStore {
45389
45790
  last_seen_at timestamptz NOT NULL
45390
45791
  )`);
45391
45792
  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
45793
  return new _PostgresNodeStore(pool, schema);
45397
45794
  }
45398
45795
  async upsertNode(n) {
@@ -45409,8 +45806,6 @@ var PostgresNodeStore = class _PostgresNodeStore {
45409
45806
  );
45410
45807
  }
45411
45808
  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
45809
  await this.pool.query(
45415
45810
  `INSERT INTO ${this.s}.node_runtimes (id,node_id,kind,hostname,runtime_binary,version,status,last_seen_at)
45416
45811
  VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
@@ -45423,8 +45818,18 @@ var PostgresNodeStore = class _PostgresNodeStore {
45423
45818
  [r.id, r.nodeId, r.kind, r.hostname, r.binary ?? null, r.version ?? null, r.status, r.lastSeenAt]
45424
45819
  );
45425
45820
  }
45821
+ /** Insert only if absent (ON CONFLICT DO NOTHING). Returns true if a row was inserted. */
45822
+ async addRuntimeIfAbsent(r) {
45823
+ const res = await this.pool.query(
45824
+ `INSERT INTO ${this.s}.node_runtimes (id,node_id,kind,hostname,runtime_binary,version,status,last_seen_at)
45825
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
45826
+ ON CONFLICT (id) DO NOTHING`,
45827
+ [r.id, r.nodeId, r.kind, r.hostname, r.binary ?? null, r.version ?? null, r.status, r.lastSeenAt]
45828
+ );
45829
+ return (res.rowCount ?? 0) > 0;
45830
+ }
45426
45831
  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()]);
45832
+ 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
45833
  }
45429
45834
  async setNodeRuntimesOnline(nodeId, online) {
45430
45835
  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 +45842,7 @@ var PostgresNodeStore = class _PostgresNodeStore {
45437
45842
  return r.rows[0] ? rowToNode(r.rows[0]) : null;
45438
45843
  }
45439
45844
  async listNodes() {
45440
- const r = await this.pool.query(`SELECT * FROM ${this.s}.nodes ORDER BY enrolled_at ASC`);
45845
+ const r = await this.pool.query(`SELECT * FROM ${this.s}.nodes WHERE status <> 'deleted' ORDER BY enrolled_at ASC`);
45441
45846
  return r.rows.map(rowToNode);
45442
45847
  }
45443
45848
  async listRuntimes(nodeId) {
@@ -45446,24 +45851,24 @@ var PostgresNodeStore = class _PostgresNodeStore {
45446
45851
  }
45447
45852
  async deleteNode(id) {
45448
45853
  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}:%`]);
45854
+ }
45855
+ /** Soft-delete: mark node as deleted (excluded from normal API, kept for audit) and drop its runtime rows. */
45856
+ async softDeleteNode(id) {
45857
+ await this.pool.query(`UPDATE ${this.s}.nodes SET status='deleted',last_seen_at=$2 WHERE id=$1`, [id, (/* @__PURE__ */ new Date()).toISOString()]);
45858
+ await this.pool.query(`DELETE FROM ${this.s}.node_runtimes WHERE node_id=$1`, [id]);
45450
45859
  }
45451
45860
  async deleteRuntime(id) {
45452
- await this.pool.query(`INSERT INTO ${this.s}.deleted_runtime_ids (id) VALUES ($1) ON CONFLICT DO NOTHING`, [id]);
45453
45861
  const existing = await this.pool.query(`SELECT node_id FROM ${this.s}.node_runtimes WHERE id=$1`, [id]);
45454
45862
  await this.pool.query(`DELETE FROM ${this.s}.node_runtimes WHERE id=$1`, [id]);
45455
45863
  if (!existing.rows[0]) return { nodeDeleted: false };
45456
45864
  const nodeId = existing.rows[0].node_id;
45457
45865
  const remaining = await this.pool.query(`SELECT 1 FROM ${this.s}.node_runtimes WHERE node_id=$1 LIMIT 1`, [nodeId]);
45458
45866
  if (!remaining.rowCount) {
45459
- await this.deleteNode(nodeId);
45867
+ await this.softDeleteNode(nodeId);
45460
45868
  return { nodeDeleted: true };
45461
45869
  }
45462
45870
  return { nodeDeleted: false };
45463
45871
  }
45464
- async clearDeletedRuntimesForNode(nodeId) {
45465
- await this.pool.query(`DELETE FROM ${this.s}.deleted_runtime_ids WHERE id LIKE $1`, [`runtime:${nodeId}:%`]);
45466
- }
45467
45872
  };
45468
45873
  var PostgresNodeTokenStore = class _PostgresNodeTokenStore {
45469
45874
  constructor(pool, schema) {
@@ -45521,6 +45926,7 @@ var rowToRuntime = (row) => ({
45521
45926
  // ../cli/src/serve.ts
45522
45927
  init_src3();
45523
45928
  init_trajectory();
45929
+ var DEFAULT_CHAT_SKILLS = ["build-workorder"];
45524
45930
  var oasisWrapperCache;
45525
45931
  function oasisWrapperScript() {
45526
45932
  if (oasisWrapperCache && fs13.existsSync(oasisWrapperCache)) return oasisWrapperCache;
@@ -45634,6 +46040,7 @@ async function startServe(opts) {
45634
46040
  if ((await typeStore.list()).length === 0) for (const d of schema) await typeStore.put(d);
45635
46041
  {
45636
46042
  const live = await typeStore.list();
46043
+ upgradeDefaultFieldDefs(live);
45637
46044
  schema.length = 0;
45638
46045
  schema.push(...live);
45639
46046
  }
@@ -45800,6 +46207,7 @@ async function startServe(opts) {
45800
46207
  return auth.sessionAccountId(headers);
45801
46208
  };
45802
46209
  const interventionDrafts = new InterventionDraftStore();
46210
+ const workorderDrafts = new WorkorderDraftStore();
45803
46211
  const journalFile = path11.join(opts.dir, "dispatch-journal.ndjson");
45804
46212
  const journalRing = [];
45805
46213
  if (fs13.existsSync(journalFile)) {
@@ -45880,6 +46288,15 @@ async function startServe(opts) {
45880
46288
  if (actorCtx?.config?.prompt) {
45881
46289
  Object.assign(files, splitIdentityFiles3(actorCtx.config.prompt));
45882
46290
  }
46291
+ Object.assign(files, await materializeSkillFiles({
46292
+ service: actors.service,
46293
+ blobs: assets,
46294
+ actorId: planner.id,
46295
+ runtimeKind: binding.runtimeKind
46296
+ }).catch((err) => {
46297
+ console.warn(`[planner] skill materialize failed for ${planner.id}: ${String(err)}`);
46298
+ return {};
46299
+ }));
45883
46300
  const handle = await chatRemoteAdapter.spawn({
45884
46301
  actor: planner.id,
45885
46302
  actorToken: issuer.issue(planner.id),
@@ -45927,10 +46344,10 @@ async function startServe(opts) {
45927
46344
  }),
45928
46345
  trace.register,
45929
46346
  automations.register,
45930
- ...chatSessionStore ? [createChatSessionsDomain({ store: chatSessionStore, registry: registryStore, kernel, artifactState: artifactStateStore })] : []
46347
+ ...chatSessionStore ? [createChatSessionsDomain({ store: chatSessionStore, registry: registryStore })] : []
45931
46348
  ],
45932
46349
  drafts: interventionDrafts,
45933
- ...chatSessionStore ? { chatSession: chatSessionStore } : {},
46350
+ workorderDrafts,
45934
46351
  // 暴露注册的 artifact 类型,给前端「发起工单」动态拉取根产物可选类型——schema.json 增改即自动扩展,
45935
46352
  // 前端不再写死枚举。走通用 /api/view(generic data),非域路由契约,不进 gen:api。
45936
46353
  views: {
@@ -45949,6 +46366,7 @@ async function startServe(opts) {
45949
46366
  },
45950
46367
  resolveActorEnv: async (actorId) => (await buildActorProvision(actors.service, actorId)).env,
45951
46368
  resolveActorContext: (actorId) => buildActorContext(actors.service, actorId),
46369
+ materializeSkills: (actorId, runtimeKind) => materializeSkillFiles({ service: actors.service, blobs: assets, actorId, runtimeKind }),
45952
46370
  dispatchProduce: async ({ artifactId, actorId, part }) => {
45953
46371
  if (!dispatcher) {
45954
46372
  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 +46377,7 @@ async function startServe(opts) {
45959
46377
  ...part !== void 0 ? { part } : {}
45960
46378
  });
45961
46379
  },
45962
- dispatchChat: async ({ actorId, message, sessionId, chatSessionId }) => {
46380
+ dispatchChat: async ({ actorId, message, sessionId }) => {
45963
46381
  const binding = await actors.service.resolveBinding()(actorId);
45964
46382
  if (!binding) {
45965
46383
  throw new ApiError(409, "ACTOR_BINDING_MISSING", `\u6570\u5B57\u5458\u5DE5 ${actorId} \u6CA1\u6709 active runtime \u7ED1\u5B9A`);
@@ -46049,6 +46467,16 @@ async function startServe(opts) {
46049
46467
  if (actorCtx?.config?.prompt) {
46050
46468
  Object.assign(files, splitIdentityFiles3(actorCtx.config.prompt));
46051
46469
  }
46470
+ Object.assign(files, await materializeSkillFiles({
46471
+ service: actors.service,
46472
+ blobs: assets,
46473
+ actorId,
46474
+ runtimeKind: binding.runtimeKind,
46475
+ extraSkillIds: DEFAULT_CHAT_SKILLS
46476
+ }).catch((err) => {
46477
+ console.warn(`[chat] skill materialize failed for ${actorId}: ${String(err)}`);
46478
+ return {};
46479
+ }));
46052
46480
  const handle = await chatRemoteAdapter.spawn({
46053
46481
  actor: actorId,
46054
46482
  actorToken: issuer.issue(actorId),
@@ -46060,7 +46488,7 @@ async function startServe(opts) {
46060
46488
  server: { url: localUrl },
46061
46489
  binding,
46062
46490
  limits: { wallClockMs: 10 * 6e4 },
46063
- env: { ...provision.env, ...chatSessionId ? { OASIS_CHAT_SESSION_ID: chatSessionId } : {} },
46491
+ env: provision.env,
46064
46492
  wrapperPaths: provision.wrapperPaths
46065
46493
  });
46066
46494
  const buffered = [];
@@ -46183,11 +46611,12 @@ async function startServe(opts) {
46183
46611
  resolveBinding: actors.service.resolveBinding(),
46184
46612
  provision: (actorId) => buildActorProvision(actors.service, actorId),
46185
46613
  resolveActorContext: (actorId) => buildActorContext(actors.service, actorId),
46614
+ materializeSkills: (actorId, runtimeKind) => materializeSkillFiles({ service: actors.service, blobs: assets, actorId, runtimeKind }),
46186
46615
  schema: new Map(schema.map((d2) => [d2.name, d2])),
46187
46616
  resolveCodeRepo,
46188
46617
  // §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)),
46618
+ // #2 派单开关:工单"待派发"(控制面旁存标 held)时不派其活。held 集通常空、表极小,每次查可接受。
46619
+ resolveDispatchHold: async (ws) => (await artifactStateStore.listWorkspaceDispatchHeld()).includes(ws),
46191
46620
  tokenFor: (actor) => issuer.issue(actor),
46192
46621
  journal,
46193
46622
  // TrajectoryEvent 是唯一事件模型;trace 旁账与 M7 蒸馏库并行消费同一流(§16.D 收口)。
@@ -46343,6 +46772,8 @@ async function startServe(opts) {
46343
46772
  // 阻塞诊断(proposal D1/D3):派发器运行时信号(queued/fused/backoff/hasInFlight)注入诊断,
46344
46773
  // 让协调者把"资源 / 运行时"病因和"图病"分开——资源排队的节点不会被误唤去砍流程。
46345
46774
  runtimeSignals: (id) => dispatcher?.runtimeSignalsFor(id) ?? { hasInFlight: false },
46775
+ // 病历里展示节点的"在跑会话情况"(runId + 跑了多久):从派发器在途会话按 artifactId 过滤。
46776
+ sessionsOf: (id) => (dispatcher?.inFlightSessions() ?? []).filter((s2) => s2.artifactId === id).map((s2) => ({ runId: s2.sessionId, action: s2.action, startedAt: s2.startedAt })),
46346
46777
  log: (m2) => console.log(m2)
46347
46778
  });
46348
46779
  coordTimer = setInterval(() => {
@@ -46473,6 +46904,7 @@ var DaemonWsClient = class {
46473
46904
  ws.on("open", () => {
46474
46905
  log2("[node-cli]", `connected id=${this.daemonId}`);
46475
46906
  this.send({ type: "hello", daemonId: this.daemonId, meta: this.buildMeta() });
46907
+ this.reportRuntimes = false;
46476
46908
  this.pingTimer = setInterval(() => this.send({ type: "pong" }), PING_INTERVAL_MS);
46477
46909
  });
46478
46910
  ws.on("message", (raw) => {
@@ -46636,7 +47068,7 @@ async function startNode(opts) {
46636
47068
  claude
46637
47069
  )
46638
47070
  );
46639
- const client = new DaemonWsClient(opts.serverUrl, opts.nodeId, sessions, adapters, opts.token, runtimes, opts.nodeName, opts.reportRuntimes ?? true);
47071
+ const client = new DaemonWsClient(opts.serverUrl, opts.nodeId, sessions, adapters, opts.token, runtimes, opts.nodeName, opts.reportRuntimes ?? false);
46640
47072
  console.log(`[oasis node] ${opts.nodeId} \u2192 ${opts.serverUrl}`);
46641
47073
  client.start();
46642
47074
  await new Promise((resolve3) => {
@@ -46672,8 +47104,9 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
46672
47104
  token \u89E3\u6790\u987A\u5E8F: --token > $OASIS_TOKEN > <dir>/operator-token
46673
47105
 
46674
47106
  \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>]
47107
+ 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
47108
  # --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
47109
+ # --<\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
47110
  propose <artifactId> (--file <path> | --from-dir <path> | --git-commit <sha>) --reason <text> [--base <revisionId|none>] [--part <name>] [--rebase-of <revisionId>]
46678
47111
  # --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
47112
  # --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 +47138,12 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
46705
47138
  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
47139
  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
47140
  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
47141
+ 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
47142
  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
47143
+ 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
47144
+ 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
47145
+ 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
47146
+ 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
47147
  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
47148
 
46712
47149
  \u6CBB\u7406\uFF08\xA711\uFF09
@@ -46767,10 +47204,7 @@ function projectSlug(value) {
46767
47204
  return value.trim().toLowerCase().replace(/[^\w\u4e00-\u9fa5\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 32);
46768
47205
  }
46769
47206
  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;
47207
+ const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
46774
47208
  const fail = async (res) => {
46775
47209
  const body = await res.json().catch(() => ({}));
46776
47210
  const err = body.error;
@@ -46823,25 +47257,50 @@ async function runCli(argv, println = console.log) {
46823
47257
  const serverUrl = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"] ?? "ws://127.0.0.1:7320/node-gateway";
46824
47258
  const nodeName = flags.get("name") ?? process.env["OASIS_NODE_NAME"];
46825
47259
  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) {
47260
+ const nodeTokenFile = path12.join(dir, "node-token");
47261
+ const enrollTokenFile = path12.join(dir, "enroll-token");
47262
+ const reportMarkerFile = path12.join(dir, "report-runtimes");
47263
+ let nodeId = fs14.existsSync(nodeIdFile) ? fs14.readFileSync(nodeIdFile, "utf8").trim() : null;
47264
+ let token2 = flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? (fs14.existsSync(nodeTokenFile) ? fs14.readFileSync(nodeTokenFile, "utf8").trim() : void 0);
47265
+ const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"] ?? (fs14.existsSync(enrollTokenFile) ? fs14.readFileSync(enrollTokenFile, "utf8").trim() : void 0);
47266
+ let freshEnrollment = false;
47267
+ if ((!nodeId || !token2) && enrollToken) {
47268
+ const httpBase2 = serverUrl.replace(/^ws(s?):\/\//, "http$1://").replace(/\/node-gateway.*$/, "");
47269
+ const res = await fetch(`${httpBase2}/api/nodes/exchange`, {
47270
+ method: "POST",
47271
+ headers: { "content-type": "application/json" },
47272
+ body: JSON.stringify({ enrollToken, ...nodeId ? { nodeId } : {}, hostname: os7.hostname() })
47273
+ });
47274
+ if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
47275
+ const body = await res.json();
47276
+ nodeId = body.nodeId;
47277
+ token2 = body.token;
46829
47278
  fs14.mkdirSync(dir, { recursive: true });
46830
47279
  fs14.writeFileSync(nodeIdFile, nodeId);
47280
+ fs14.writeFileSync(nodeTokenFile, token2, { mode: 384 });
47281
+ try {
47282
+ fs14.unlinkSync(enrollTokenFile);
47283
+ } catch {
47284
+ }
47285
+ freshEnrollment = true;
46831
47286
  }
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);
47287
+ if (!nodeId) throw new Error("no nodeId \u2014 provide --enroll-token <ent_...> on first run");
47288
+ const scriptRun = freshEnrollment || fs14.existsSync(reportMarkerFile);
46834
47289
  if (flags.has("service")) {
46835
47290
  const { spawnSync, execSync: execSync2 } = await import("node:child_process");
46836
47291
  if (token2) {
46837
47292
  fs14.mkdirSync(dir, { recursive: true });
46838
47293
  fs14.writeFileSync(nodeTokenFile, token2, { mode: 384 });
46839
47294
  }
47295
+ if (scriptRun) {
47296
+ fs14.mkdirSync(dir, { recursive: true });
47297
+ fs14.writeFileSync(reportMarkerFile, "");
47298
+ }
46840
47299
  println("\u2192 Installing oasis_test globally...");
46841
47300
  spawnSync("npm", ["install", "-g", "oasis_test@latest"], { stdio: "inherit" });
46842
47301
  const prefix = execSync2("npm prefix -g").toString().trim();
46843
47302
  const binPath = path12.join(prefix, "bin", "oasis_test");
46844
- const svcArgs = `node --server-ws ${serverUrl} --id ${nodeId} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
47303
+ const svcArgs = `node --server-ws ${serverUrl} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
46845
47304
  if (process.platform === "linux") {
46846
47305
  const svcFile = path12.join(os7.homedir(), ".config/systemd/user/oasis-node.service");
46847
47306
  fs14.mkdirSync(path12.dirname(svcFile), { recursive: true });
@@ -46874,7 +47333,7 @@ async function runCli(argv, println = console.log) {
46874
47333
  const plist = path12.join(os7.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
46875
47334
  fs14.mkdirSync(path12.dirname(plist), { recursive: true });
46876
47335
  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("");
47336
+ const argXml = [binPath, "node", "--server-ws", serverUrl, "--dir", dir, ...nodeName ? ["--name", nodeName] : []].map((a) => `<string>${a}</string>`).join("");
46878
47337
  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
47338
  spawnSync("launchctl", ["load", plist], { stdio: "inherit" });
46880
47339
  println("\u2713 Oasis node service started (macOS LaunchAgent com.oasis.node)");
@@ -46887,7 +47346,13 @@ async function runCli(argv, println = console.log) {
46887
47346
  }
46888
47347
  return;
46889
47348
  }
46890
- await startNode({ serverUrl, nodeId, ...token2 ? { token: token2 } : {}, ...nodeName ? { nodeName } : {} });
47349
+ if (fs14.existsSync(reportMarkerFile)) {
47350
+ try {
47351
+ fs14.unlinkSync(reportMarkerFile);
47352
+ } catch {
47353
+ }
47354
+ }
47355
+ await startNode({ serverUrl, nodeId, ...token2 ? { token: token2 } : {}, ...nodeName ? { nodeName } : {}, reportRuntimes: scriptRun });
46891
47356
  return;
46892
47357
  }
46893
47358
  if (command === "node-token") {
@@ -47029,6 +47494,30 @@ async function runCli(argv, println = console.log) {
47029
47494
  return out;
47030
47495
  };
47031
47496
  const STAGE_COMMANDS = /* @__PURE__ */ new Set(["link", "spawn", "unlink", "cancel", "freeze", "cancel-part", "edit"]);
47497
+ const GLOBAL_NON_FIELD_FLAGS = /* @__PURE__ */ new Set(["server", "token", "dir", "set-json", "clear"]);
47498
+ const SPAWN_OWN_FLAGS = /* @__PURE__ */ new Set(["type", "owner", "workspace", "persistence", "title", "brief", "description", "input", "parts-file"]);
47499
+ const EDIT_OWN_FLAGS = /* @__PURE__ */ new Set(["title", "brief"]);
47500
+ const fieldsFromFlags = (ownFlags) => {
47501
+ const fields = {};
47502
+ for (const [k2, v2] of flags) {
47503
+ if (ownFlags.has(k2) || GLOBAL_NON_FIELD_FLAGS.has(k2)) continue;
47504
+ fields[k2] = v2;
47505
+ }
47506
+ const sj = flags.get("set-json");
47507
+ if (sj !== void 0) {
47508
+ const parsed = JSON.parse(sj);
47509
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
47510
+ throw new Error(`--set-json \u9700\u8981\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u5982 '{"goal":"..."}'`);
47511
+ }
47512
+ Object.assign(fields, parsed);
47513
+ }
47514
+ const clear = flags.get("clear");
47515
+ if (clear !== void 0) for (const k2 of clear.split(",")) {
47516
+ const t = k2.trim();
47517
+ if (t) fields[t] = null;
47518
+ }
47519
+ return Object.keys(fields).length > 0 ? fields : void 0;
47520
+ };
47032
47521
  const buildStageOp = (cmd) => {
47033
47522
  switch (cmd) {
47034
47523
  case "link":
@@ -47038,15 +47527,18 @@ async function runCli(argv, println = console.log) {
47038
47527
  to: need(flags, "to"),
47039
47528
  ...flags.get("required") !== void 0 ? { required: flags.get("required") === "true" } : {}
47040
47529
  };
47041
- case "spawn":
47530
+ case "spawn": {
47531
+ const fields = fieldsFromFlags(SPAWN_OWN_FLAGS);
47042
47532
  return {
47043
47533
  action: "spawn",
47044
47534
  id: needPos(positional, 0, "oasis spawn <id> --type <t>"),
47045
47535
  type: need(flags, "type"),
47046
47536
  ...flags.get("title") !== void 0 ? { title: flags.get("title") } : {},
47047
47537
  ...(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() })) } : {}
47538
+ ...flags.get("input") !== void 0 ? { inputs: flags.get("input").split(",").map((to) => ({ to: to.trim() })) } : {},
47539
+ ...fields !== void 0 ? { fields } : {}
47049
47540
  };
47541
+ }
47050
47542
  case "unlink":
47051
47543
  return { action: "unlink", artifactId: needPos(positional, 0, "oasis unlink <consumer> --to <upstream>"), to: need(flags, "to") };
47052
47544
  case "cancel":
@@ -47059,13 +47551,16 @@ async function runCli(argv, println = console.log) {
47059
47551
  };
47060
47552
  case "cancel-part":
47061
47553
  return { action: "cancelPart", artifactId: needPos(positional, 0, "oasis cancel-part <id> --part <name>"), part: need(flags, "part") };
47062
- case "edit":
47554
+ case "edit": {
47555
+ const fields = fieldsFromFlags(EDIT_OWN_FLAGS);
47063
47556
  return {
47064
47557
  action: "edit",
47065
- artifactId: needPos(positional, 0, "oasis edit <id> [--title \u2026] [--brief \u2026]"),
47558
+ 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
47559
  ...flags.get("title") !== void 0 ? { title: flags.get("title") } : {},
47067
- ...flags.get("brief") !== void 0 ? { description: flags.get("brief") } : {}
47560
+ ...flags.get("brief") !== void 0 ? { description: flags.get("brief") } : {},
47561
+ ...fields !== void 0 ? { fields } : {}
47068
47562
  };
47563
+ }
47069
47564
  default:
47070
47565
  throw new Error(`\u672A\u77E5\u6682\u5B58\u547D\u4EE4: ${cmd}`);
47071
47566
  }
@@ -47099,43 +47594,28 @@ async function runCli(argv, println = console.log) {
47099
47594
  return;
47100
47595
  }
47101
47596
  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;
47597
+ case "create-workorder": {
47598
+ const planRaw = flags.get("plan-file") !== void 0 ? fs14.readFileSync(flags.get("plan-file"), "utf8") : flags.get("plan");
47599
+ const acceptanceFlag = flags.get("acceptance");
47600
+ const args = {
47601
+ createdVia: "seed",
47602
+ stage: true,
47603
+ type: need(flags, "type"),
47604
+ workspace: need(flags, "workspace"),
47605
+ title: need(flags, "title"),
47606
+ ...flags.get("brief") !== void 0 ? { brief: flags.get("brief") } : {},
47607
+ ...flags.get("goal") !== void 0 ? { goal: flags.get("goal") } : {},
47608
+ ...acceptanceFlag !== void 0 ? { acceptance_criteria: parseStringArrayFlag(acceptanceFlag, "acceptance") } : {},
47609
+ ...planRaw !== void 0 ? { plan: parseJsonFlag(planRaw, "plan") } : {},
47610
+ ...flags.get("project") !== void 0 ? { project_id: flags.get("project") } : {},
47611
+ ...flags.get("dispatch") === "false" ? { dispatch: false } : {}
47612
+ };
47613
+ const res = await api.cmd("spawn", args);
47614
+ println(res.message);
47615
+ const data = res.data;
47616
+ if (data?.draftId) {
47617
+ println(`draftId=${data.draftId} workspace=${data.workspace} \u8282\u70B9=${data.spawned?.length ?? 0} \u89C4\u5212=${data.planning?.status ?? "?"}`);
47132
47618
  }
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
47619
  break;
47140
47620
  }
47141
47621
  case "projects": {
@@ -47326,6 +47806,7 @@ async function runCli(argv, println = console.log) {
47326
47806
  }
47327
47807
  case "spawn": {
47328
47808
  const partsFile = flags.get("parts-file");
47809
+ const spawnFields = fieldsFromFlags(SPAWN_OWN_FLAGS);
47329
47810
  const { message } = await api.cmd("spawn", {
47330
47811
  id: positional[0],
47331
47812
  type: need(flags, "type"),
@@ -47335,7 +47816,8 @@ async function runCli(argv, println = console.log) {
47335
47816
  title: flags.get("title"),
47336
47817
  description: flags.get("brief") ?? flags.get("description"),
47337
47818
  inputs: flags.get("input")?.split(",").map((to) => ({ to: to.trim() })),
47338
- ...partsFile !== void 0 ? { parts: JSON.parse(readFileArg(partsFile)) } : {}
47819
+ ...partsFile !== void 0 ? { parts: JSON.parse(readFileArg(partsFile)) } : {},
47820
+ ...spawnFields !== void 0 ? { fields: spawnFields } : {}
47339
47821
  });
47340
47822
  println(message);
47341
47823
  break;
@@ -47787,6 +48269,52 @@ async function runCli(argv, println = console.log) {
47787
48269
  println(`\u5DF2\u5220\u9664 ${name}`);
47788
48270
  break;
47789
48271
  }
48272
+ if (sub === "add-field") {
48273
+ const name = needPos(positional, 1, "oasis artifact-types add-field <type> <\u5B57\u6BB5> --type <string|string[]|number|boolean> [--required true] [--description \u2026]");
48274
+ const field = needPos(positional, 2, "oasis artifact-types add-field <type> <\u5B57\u6BB5> --type <t> [--required true] [--description \u2026]");
48275
+ const spec = {
48276
+ name: field,
48277
+ type: need(flags, "type"),
48278
+ ...flags.get("required") === "true" ? { required: true } : {},
48279
+ ...flags.get("description") !== void 0 ? { description: flags.get("description") } : {}
48280
+ };
48281
+ const r = await api.request("POST", `/api/artifact-types/${encodeURIComponent(name)}/fields`, spec);
48282
+ 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` : ""}`);
48283
+ break;
48284
+ }
48285
+ if (sub === "edit-field") {
48286
+ 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");
48287
+ const field = needPos(positional, 2, "oasis artifact-types edit-field <type> <\u5B57\u6BB5> [--type \u2026] [--required \u2026] [--description \u2026]");
48288
+ const attrs = {
48289
+ ...flags.get("type") !== void 0 ? { type: flags.get("type") } : {},
48290
+ ...flags.get("required") !== void 0 ? { required: flags.get("required") === "true" } : {},
48291
+ ...flags.get("description") !== void 0 ? { description: flags.get("description") } : {}
48292
+ };
48293
+ const r = await api.request("PATCH", `/api/artifact-types/${encodeURIComponent(name)}/fields/${encodeURIComponent(field)}`, attrs);
48294
+ 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` : ""}`);
48295
+ break;
48296
+ }
48297
+ if (sub === "drop-field") {
48298
+ const name = needPos(positional, 1, "oasis artifact-types drop-field <type> <\u5B57\u6BB5>");
48299
+ const field = needPos(positional, 2, "oasis artifact-types drop-field <type> <\u5B57\u6BB5>");
48300
+ const r = await api.request("DELETE", `/api/artifact-types/${encodeURIComponent(name)}/fields/${encodeURIComponent(field)}`, void 0);
48301
+ 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` : ""}`);
48302
+ break;
48303
+ }
48304
+ if (sub === "show") {
48305
+ const name = needPos(positional, 1, "oasis artifact-types show <type>");
48306
+ const { items } = await api.request("GET", "/api/artifact-types");
48307
+ const t = items.find((x2) => x2.name === name);
48308
+ if (!t) {
48309
+ println(`\uFF08\u65E0\u6B64\u7C7B\u578B: ${name}\uFF09`);
48310
+ break;
48311
+ }
48312
+ println(`${t.name}${t.status === "deprecated" ? " [\u5DF2\u5F03\u7528]" : ""} (${t.contentType}, owner=${t.ownerRole})${t.description ? ` \u2014\u2014 ${t.description}` : ""}`);
48313
+ const fds = t.fieldDefs ?? [];
48314
+ println(fds.length === 0 ? " \u81EA\u5B9A\u4E49\u5B57\u6BB5\uFF1A\u65E0" : " \u81EA\u5B9A\u4E49\u5B57\u6BB5\uFF1A");
48315
+ for (const f2 of fds) println(` ${f2.name} ${f2.type} ${f2.required ? "\u5FC5\u586B" : "\u53EF\u9009"}${f2.description ? ` ${f2.description}` : ""}`);
48316
+ break;
48317
+ }
47790
48318
  const all = await api.view(
47791
48319
  "artifact-types"
47792
48320
  );
@@ -47878,7 +48406,7 @@ var isRunning = (pid) => {
47878
48406
  return false;
47879
48407
  }
47880
48408
  };
47881
- var httpBase = (wsUrl) => wsUrl.replace(/^wss?:\/\//, "http://").replace(/\/node-gateway.*$/, "");
48409
+ var httpBase = (wsUrl) => wsUrl.replace(/^ws(s?):\/\//, "http$1://").replace(/\/node-gateway.*$/, "");
47882
48410
  function installBinary() {
47883
48411
  fs15.mkdirSync(path13.dirname(BIN_FILE), { recursive: true });
47884
48412
  fs15.copyFileSync(process.argv[1], BIN_FILE);
@@ -48001,8 +48529,11 @@ void (async () => {
48001
48529
  await startNode({
48002
48530
  serverUrl: cfg.serverUrl,
48003
48531
  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)()}`,
48532
+ nodeId: cfg.nodeId ?? (() => {
48533
+ console.error("[oasis] no nodeId in config \u2014 run `oasis start` first");
48534
+ process.exit(1);
48535
+ })(),
48536
+ nodeName: cfg.name ?? cfg.nodeId,
48006
48537
  reportRuntimes
48007
48538
  });
48008
48539
  return;
@@ -48011,32 +48542,29 @@ void (async () => {
48011
48542
  const prev = readConfig();
48012
48543
  const serverUrl = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"] ?? prev?.serverUrl;
48013
48544
  if (!serverUrl) {
48014
- console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--id <nodeId>]");
48545
+ console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>]");
48015
48546
  process.exit(1);
48016
48547
  }
48017
- const commandNodeId = flags.get("id");
48018
48548
  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)()}`;
48549
+ let nodeId = serverChanged ? void 0 : prev?.nodeId;
48020
48550
  let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
48021
48551
  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`, {
48552
+ if (prev?.nodeId && prev?.token && !serverChanged) {
48553
+ nodeId = prev.nodeId;
48554
+ token = prev.token;
48555
+ } else if (enrollToken) {
48556
+ const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
48024
48557
  method: "POST",
48025
48558
  headers: { "content-type": "application/json" },
48026
- body: JSON.stringify({ enrollToken, nodeId: candidateNodeId })
48559
+ body: JSON.stringify({ enrollToken, hostname: os8.hostname() })
48027
48560
  });
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
48561
  if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
48034
48562
  const body = await res.json();
48035
48563
  token = body.token;
48036
- nodeId = body.nodeId || nodeId;
48564
+ nodeId = body.nodeId;
48037
48565
  }
48038
- if (!token) {
48039
- console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--id <nodeId>]");
48566
+ if (!token || !nodeId) {
48567
+ console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>]");
48040
48568
  process.exit(1);
48041
48569
  }
48042
48570
  const name = flags.get("name") ?? prev?.name;