@stalfh233/omc-cli 0.6.12 → 0.6.14

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 (59) hide show
  1. package/coverage/m1-coverage-manifest-v1.json +2 -0
  2. package/dist/approval-flow.js +84 -3
  3. package/dist/batch-call.js +32 -2
  4. package/dist/bench/call-face-eval.js +2 -2
  5. package/dist/call.js +35 -2
  6. package/dist/capability-overview.js +21 -7
  7. package/dist/cli-contract.js +8 -2
  8. package/dist/cli.js +313 -72
  9. package/dist/describe-cache.js +8 -4
  10. package/dist/discovery.js +4 -0
  11. package/dist/environment-policy.js +20 -0
  12. package/dist/field-families.js +62 -4
  13. package/dist/form-support.js +293 -6
  14. package/dist/goal-routes.js +137 -22
  15. package/dist/identity.js +63 -11
  16. package/dist/lanes/app-contract.js +4 -4
  17. package/dist/lanes/app.js +20 -2
  18. package/dist/lanes/dictionary-contract.js +16 -5
  19. package/dist/lanes/dictionary-coverage.js +1 -0
  20. package/dist/lanes/dictionary.js +200 -8
  21. package/dist/lanes/environment-contract.js +18 -6
  22. package/dist/lanes/environment.js +129 -16
  23. package/dist/lanes/field-contract.js +3 -2
  24. package/dist/lanes/field.js +785 -63
  25. package/dist/lanes/form.js +52 -6
  26. package/dist/lanes/list-view-contract.js +3 -3
  27. package/dist/lanes/list-view.js +143 -12
  28. package/dist/lanes/meta.js +36 -10
  29. package/dist/lanes/model-contract.js +7 -6
  30. package/dist/lanes/model.js +154 -10
  31. package/dist/lanes/organization.js +67 -7
  32. package/dist/lanes/runtime-data-contract.js +4 -2
  33. package/dist/lanes/runtime-data.js +395 -41
  34. package/dist/lanes/workflow-contract.js +2 -2
  35. package/dist/lanes/workflow.js +381 -11
  36. package/dist/model-scaffold.js +11 -3
  37. package/dist/path-projection.js +181 -31
  38. package/dist/read-shapes.js +31 -11
  39. package/dist/recipes/b2-registration.js +1 -1
  40. package/dist/recipes/field.js +16 -3
  41. package/dist/recipes/form.js +4 -3
  42. package/dist/recipes/runtime-data.js +58 -8
  43. package/dist/recipes/workflow.js +86 -1
  44. package/dist/refusal-guidance.js +18 -6
  45. package/dist/rollback-spec.js +41 -2
  46. package/dist/rollback.js +49 -11
  47. package/dist/script.js +72 -12
  48. package/dist/skills.js +91 -3
  49. package/dist/tools.js +113 -1
  50. package/dist/wire.js +13 -1
  51. package/dist/write-lock.js +92 -4
  52. package/dist/write.js +129 -16
  53. package/package.json +3 -3
  54. package/skills/omc-business-configuration.md +1 -1
  55. package/skills/omc-capabilities.md +5 -4
  56. package/skills/omc-playbook.md +41 -0
  57. package/skills/omc-refusals.md +58 -6
  58. package/skills/omc-tool-selection.md +2 -0
  59. package/skills/omc-write-hazards.md +3 -2
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "
27
27
  import { artifactPathMissHint, nullProjectionHint, partialPathMissHint, projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
28
28
  import { basename, join, resolve } from "node:path";
29
29
  import { buildToolRegistry } from "./tools.js";
30
- import { acquireWriteLock } from "./write-lock.js";
30
+ import { acquireWriteLock, writeLockHeldFace } from "./write-lock.js";
31
31
  import { cmdCompute } from "./compute.js";
32
32
  import { cmdRuleWhen } from "./rule-when.js";
33
33
  import { cmdApprovalFlow } from "./approval-flow.js";
@@ -209,7 +209,9 @@ async function cmdDoctor(store, environment, serviceCode) {
209
209
  try {
210
210
  const resolved = store.resolveEnvironment(environment);
211
211
  steps.push(`配置 ok 环境=${resolved.name}(来源 ${resolved.source})`);
212
- steps.push(`写入 ok 写入=${environmentWritable(resolved.profile) ? "允许" : "只读"}${requiresProductionConfirmation(resolved.profile) ? ";生产环境:每笔写入需 confirmProduction" : ""}`);
212
+ steps.push(`远端写 ok 远端写=${environmentWritable(resolved.profile) ? "允许" : "只读"}${requiresProductionConfirmation(resolved.profile) ? ";生产环境:每笔写入需 confirmProduction" : ""}(平台侧变更)`);
213
+ // r29f F9:读面也会本地落盘(artifacts/usage/describe 缓存);与远端写分开说。
214
+ steps.push(`本地落盘 ok .omc/(artifacts·usage·describe 缓存;读面也会写)。重定位:OMC_CONFIG_HOME=<根>(其下 .omc/)或 OMC_ARTIFACTS_HOME=<产物目录>`);
213
215
  const removed = removedProfileKeys(resolved.profile);
214
216
  if (removed.length > 0)
215
217
  steps.push(`配置 ⚠ 含已移除键(请删除):${removed.join("、")}`);
@@ -367,8 +369,10 @@ function doctorAnchor() {
367
369
  const topics = listSkills().map((entry) => entry.name).join("、");
368
370
  return [
369
371
  "最小路径 omc next \"<目标>\"(目标→工具一步定位,不确定会明说)→ omc help omc-start-here → omc call discover_environment --env <env> --args '{}'(<env> 名以 doctor 的「环境=」或项目 .omc-env 为准,勿猜)→ omc capabilities / omc describe <tool>",
370
- `方法论 omc help <topic> --sections / --section "<标题>"(按节读,随包 skills/);topic:${topics}`,
372
+ `方法论 omc help <topic>(或 --topic <name>)--sections / --section "<标题>"(按节读,随包 skills/);topic:${topics}`,
371
373
  "大输出 超预算结果只回 artifactId/sha256/fetch;用 fetch 里的 omc artifact get <id>(可加 --jq <path>)取回,勿全量读入",
374
+ // r29f F9:只读面不碰远端,但仍会本地落盘——写清,避免「零写入」核对误判。
375
+ "落盘 远端写=环境 writable 决定的平台变更;本地落盘=.omc/(artifacts/usage/describe 缓存,读面也会写)。可用 OMC_CONFIG_HOME 重定位配置根(其下 .omc/),或 OMC_ARTIFACTS_HOME 单独重定位产物目录",
372
376
  ].join("\n");
373
377
  }
374
378
  function flag0(args, name) {
@@ -509,7 +513,20 @@ async function cmdCall(store, args) {
509
513
  const positional = args.filter((token, index) => !token.startsWith("--") && (index === 0 || !args[index - 1]?.startsWith("--env") && !args[index - 1]?.startsWith("--environment") && !args[index - 1]?.startsWith("--args")));
510
514
  const toolName = positional[0];
511
515
  if (!toolName || toolName === "help" || args.includes("--help")) {
516
+ // r29f F3:`omc call <tool> --help` 识别具名工具时转发 describe 摘要,
517
+ // 并写死输出预算默认值(别只回通用 call 选项)。
518
+ if (toolName && toolName !== "help") {
519
+ const registry = buildToolRegistry();
520
+ const described = describeTool(registry, toolName);
521
+ if (described) {
522
+ console.error(`工具:${toolName}`);
523
+ console.error(`输出预算:--max-output-bytes 默认 65536(只约束最终 stdout 信封;读面 diet ~2048 独立外置 detailArtifactId);artifact get 内联上限 8192`);
524
+ console.error(`投影:--jq/--extract 相对 result(不要写 result. 前缀;误写会自动剥并标 autoStripped);先投影再判预算`);
525
+ return emitTransportResult({ status: "ok", ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, { request, maxOutputBytes });
526
+ }
527
+ }
512
528
  console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
529
+ console.error("输出预算:--max-output-bytes 默认 65536(只约束最终 stdout 信封;读面 diet ~2048 独立外置 detailArtifactId);artifact get 内联上限 8192");
513
530
  console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
514
531
  return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
515
532
  }
@@ -617,26 +634,44 @@ async function cmdCall(store, args) {
617
634
  }
618
635
  const registry = buildToolRegistry();
619
636
  const tool = registry.find((definition) => definition.name === toolName);
620
- // 调用流畅性(2026-11-04,#18 的「查不到数据」同源):读面「宽松」不等于
621
- // 「无声」——`{"limt":5}` / `{"filterr":{…}}` 被 zod 剥掉后返回全量,调用方
622
- // 会读成「过滤没生效 / 数据就是这么多」。读面在**不阻断**的前提下把被忽略的
623
- // 键回显出来(写面仍是 fail-closed,见 parseToolInput)。
637
+ // r29f F7:读面未知参数默认硬拒(与写面 tool-input-unknown-keys 对齐);
638
+ // 显式 `lenientArgs:true` 才回退到旧的 ignoredArgs 回显。
624
639
  //
625
640
  // F6(r27):应用级键(appCode/applicationCode/application)静默回落会把写
626
641
  // 操作打到错误应用——这些键被忽略时直接拒绝,不进 ignoredArgs。
627
642
  const ignoredReadKeys = [];
628
643
  if (tool) {
629
644
  const schemaKeys = new Set(Object.keys(tool.inputSchema));
630
- const recognized = new Set(["environment", "capability", "requestId", "dryRun"]);
645
+ const recognized = new Set(["environment", "capability", "requestId", "dryRun", "lenientArgs"]);
646
+ const lenientArgs = parsedArgs.lenientArgs === true;
631
647
  const unknownKeys = Object.keys(parsedArgs).filter((key) => !schemaKeys.has(key) && !recognized.has(key));
632
648
  const appScopeUnknown = unknownKeys.filter((key) => APP_SCOPE_ARG_KEYS.has(key));
633
649
  if (appScopeUnknown.length > 0) {
634
- const canonical = schemaKeys.has("applicationCode")
635
- ? "applicationCode"
636
- : schemaKeys.has("appCode")
637
- ? "appCode"
638
- : "applicationCode(或 appCode,以 omc describe 为准)";
650
+ const hasApplicationCode = schemaKeys.has("applicationCode");
651
+ const hasAppCode = schemaKeys.has("appCode");
639
652
  const wrong = appScopeUnknown.join("、");
653
+ // r29a F7:schema 两个应用范围键都没有时,旧文案回退成
654
+ // 「请改用 applicationCode(或 appCode…)」——调用方传的正是它,形成死循环。
655
+ // 这种工具根本没有应用范围参数(如 rule.* 按 schemaCode 定位),走专用 blocker。
656
+ if (!hasApplicationCode && !hasAppCode) {
657
+ const locateBy = schemaKeys.has("schemaCode")
658
+ ? "schemaCode(模型 code)"
659
+ : schemaKeys.has("code")
660
+ ? "code"
661
+ : "本工具自己的定位键(见 omc describe)";
662
+ const result = {
663
+ status: "refused",
664
+ blocker: "tool-has-no-app-scope",
665
+ tool: toolName,
666
+ ignored: appScopeUnknown,
667
+ locateBy,
668
+ message: `本工具没有应用范围参数(不接受 applicationCode/appCode);你传的 ${wrong} 不会生效。` +
669
+ `它按 ${locateBy} 定位——应用边界请在 model.*/folder.*/app.* 面使用。` +
670
+ `(\`omc describe ${toolName}\` 可确认参数表)`,
671
+ };
672
+ return emit("refused", result, CLI_EXIT_CODES.policyRefused);
673
+ }
674
+ const canonical = hasApplicationCode ? "applicationCode" : "appCode";
640
675
  const result = {
641
676
  status: "refused",
642
677
  blocker: "unknown-app-scope-arg",
@@ -647,10 +682,29 @@ async function cmdCall(store, args) {
647
682
  };
648
683
  return emit("refused", result, CLI_EXIT_CODES.policyRefused);
649
684
  }
650
- if (tool.kind !== "write") {
651
- for (const key of unknownKeys) {
652
- // r25 真机:`limt:5` 被就近指到 `filter`(共享 "fi" 就够过阈值),照提示改
653
- // 会把「取 5 条」变成「按条件过滤」。短键走显式缩写表,语义才不跑偏。
685
+ const otherUnknown = unknownKeys.filter((key) => !APP_SCOPE_ARG_KEYS.has(key));
686
+ if (tool.kind !== "write" && otherUnknown.length > 0) {
687
+ if (!lenientArgs) {
688
+ const suggestions = otherUnknown.map((key) => ({ key, ...suggestArgumentKey(key, [...schemaKeys]) }));
689
+ const pairs = suggestions.map((entry) => {
690
+ if (entry.closest)
691
+ return `${entry.key}→${entry.closest}`;
692
+ if (entry.absentInTool)
693
+ return `${entry.key}(本工具没有 ${entry.absentInTool})`;
694
+ return entry.key;
695
+ });
696
+ const result = {
697
+ status: "error",
698
+ blocker: "tool-input-unknown-keys",
699
+ tool: toolName,
700
+ unknownKeys: otherUnknown,
701
+ suggestions,
702
+ message: `读面不接受未知键(与写面对齐):${pairs.join("、")}。` +
703
+ `改用合法键后重试;确需宽松忽略传 lenientArgs:true。参数表:omc describe ${toolName}`,
704
+ };
705
+ return emit("error", result, CLI_EXIT_CODES.invalidInput);
706
+ }
707
+ for (const key of otherUnknown) {
654
708
  ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys]) });
655
709
  }
656
710
  }
@@ -684,15 +738,16 @@ async function cmdCall(store, args) {
684
738
  if (lock.held)
685
739
  break;
686
740
  if (Date.now() >= deadline) {
687
- const conflict = {
688
- status: "refused",
689
- blocker: "write-lock-held",
741
+ // r31b F2: structured retryable/lockOwner/nextCommand so scripts do not
742
+ // invent a shell retry loop (help says use --wait; the refusal must too).
743
+ const conflict = writeLockHeldFace({
690
744
  environment: lockTarget,
691
745
  owner: lock.owner,
692
746
  recovery: lock.recovery,
693
- message: lock.recovery[0],
694
- };
695
- console.error(`write lock conflict: ${JSON.stringify(lock.owner)}`);
747
+ refusedTool: toolName,
748
+ waitMs: waitMs > 0 ? waitMs : 30_000,
749
+ });
750
+ console.error(`write lock conflict: ${JSON.stringify({ pid: lock.owner?.pid, tool: lock.owner?.tool ?? lock.owner?.command, startedAt: lock.owner?.startedAt })}`);
696
751
  writeEnvelope(envelope("refused", conflict, request, classifyResult(conflict).error));
697
752
  return CLI_EXIT_CODES.policyRefused;
698
753
  }
@@ -828,6 +883,18 @@ function projectArtifactPath(value, path) {
828
883
  ? { missed: true, ...(fanout.at !== undefined ? { at: fanout.at } : {}), ...(fanout.available !== undefined ? { available: fanout.available } : {}) }
829
884
  : { missed: false, content: fanout.values, mapped: fanout.values.length, ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries } : {}), nullEntries: fanout.nullEntries };
830
885
  }
886
+ /** Preserve source completeness/count facts on every artifact projection. */
887
+ function artifactProjectionMetadata(value) {
888
+ if (value === null || typeof value !== "object" || Array.isArray(value))
889
+ return {};
890
+ const record = value;
891
+ const metadata = {};
892
+ for (const key of ["truncated", "truncatedKeys", "itemsCount", "total"]) {
893
+ if (Object.prototype.hasOwnProperty.call(record, key))
894
+ metadata[key] = record[key];
895
+ }
896
+ return metadata;
897
+ }
831
898
  /**
832
899
  * T6(GitLab #17):`mapped`/`nullEntries` 的读法要跟数据一起走,否则 `mapped:5`
833
900
  * 会被读成"取到 5 个值"。文案的单一真相源在 path-projection(与 `--extract`
@@ -883,15 +950,17 @@ async function cmdArtifactGet(id, transport, options = {}) {
883
950
  // 一层 `projection:{path,content}`——`artifact get --jq` 与 `--summary --jq`
884
951
  // 因此同形(顶层 path/content),换读面不用重新探根键。
885
952
  return emitTransportResult(projected.missed
886
- ? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
953
+ ? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, ...artifactProjectionMetadata(content), keys, path: options.jq, content: null, projections: [{ path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }], pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
887
954
  : {
888
955
  status: "ok",
889
956
  artifactId: id,
890
957
  sha256: artifact.sha256,
891
958
  bytes: artifact.content.length,
959
+ ...artifactProjectionMetadata(content),
892
960
  keys,
893
961
  path: options.jq,
894
962
  content: projected.content ?? null,
963
+ projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }],
895
964
  ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}),
896
965
  ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}),
897
966
  ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}),
@@ -937,12 +1006,12 @@ async function cmdArtifactGet(id, transport, options = {}) {
937
1006
  ? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
938
1007
  : { path, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) };
939
1008
  });
940
- return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint: partialPathMissHint() } : {}) }, transport);
1009
+ return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint: partialPathMissHint() } : {}) }, transport);
941
1010
  }
942
1011
  const projected = projectArtifactPath(artifactContent, options.jq);
943
1012
  return emitTransportResult(projected.missed
944
- ? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
945
- : { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
1013
+ ? { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
1014
+ : { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: projected.content ?? null, projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }], ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
946
1015
  }
947
1016
  // r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
948
1017
  // large ones (up to 55 KB) pulled whole workflow templates just to read one
@@ -1053,6 +1122,76 @@ async function cmdBench(args) {
1053
1122
  const dims = report.dimensions;
1054
1123
  return failedSteps === 0 && dims.intent.passed === dims.intent.cases ? 0 : 1;
1055
1124
  }
1125
+ /** r29f F8:discovery 面复用同一投影层(相对本命令 result,不是 call 信封)。 */
1126
+ function emitProjectedResult(result, transport, jq) {
1127
+ const raw = Array.isArray(jq) ? jq : jq === undefined ? [] : [jq];
1128
+ const keysOnly = raw.some((entry) => entry === true) || (raw.length === 0 && jq === true);
1129
+ const paths = raw
1130
+ .filter((entry) => typeof entry === "string")
1131
+ .flatMap((path) => path.split(","))
1132
+ .map((path) => path.trim())
1133
+ .filter(Boolean);
1134
+ if (paths.length === 0 && !keysOnly)
1135
+ return emitTransportResult(result, transport);
1136
+ const projected = projectResultExtract(result, paths, keysOnly);
1137
+ return emitTransportResult(projected ?? result, transport);
1138
+ }
1139
+ function enrichDescribeParamMiss(registry, toolName, param, miss) {
1140
+ // r31b F1: a miss must teach the next move — closest name, or "that param
1141
+ // lives on another tool" — instead of dumping the whole available table.
1142
+ const available = Array.isArray(miss.available) ? miss.available.map((entry) => String(entry)) : [];
1143
+ const didYouMean = typeof miss.didYouMean === "string" ? miss.didYouMean : undefined;
1144
+ const family = toolName.includes(".") ? toolName.split(".")[0] : toolName;
1145
+ // Known cross-tool identity pairs first (新建 vs 更新), then same-family,
1146
+ // then any other tool that declares the param.
1147
+ const preferredPair = toolName === "data.save" && param === "bizObjectId"
1148
+ ? "data.update"
1149
+ : toolName === "data.update" && param === "formData"
1150
+ ? "data.save"
1151
+ : undefined;
1152
+ const otherTools = registry
1153
+ .filter((tool) => tool.name !== toolName && tool.inputSchema && Object.prototype.hasOwnProperty.call(tool.inputSchema, param))
1154
+ .map((tool) => tool.name)
1155
+ .sort((left, right) => {
1156
+ const rank = (name) => {
1157
+ if (preferredPair !== undefined && name === preferredPair)
1158
+ return 0;
1159
+ if (name.startsWith(`${family}.`))
1160
+ return 1;
1161
+ return 2;
1162
+ };
1163
+ return rank(left) - rank(right) || left.localeCompare(right);
1164
+ });
1165
+ const otherTool = preferredPair && otherTools.includes(preferredPair) ? preferredPair : otherTools[0];
1166
+ // Known cross-tool identity guidance for the data lane (新建 vs 更新).
1167
+ const dataIdentityHint = toolName === "data.save" && param === "bizObjectId"
1168
+ ? "新建用 data.save(schemaCode+formData);更新用 data.update + bizObjectId"
1169
+ : undefined;
1170
+ const cappedAvailable = available.slice(0, 8);
1171
+ const hintParts = [
1172
+ didYouMean !== undefined ? `本工具最接近的参数是 ${didYouMean}` : undefined,
1173
+ otherTool !== undefined ? `参数 "${param}" 属于 ${otherTool}${otherTools.length > 1 ? `(同名还在 ${otherTools.slice(1, 3).join("、")})` : ""}` : undefined,
1174
+ dataIdentityHint,
1175
+ ].filter((part) => typeof part === "string" && part.length > 0);
1176
+ return {
1177
+ ...miss,
1178
+ ...(didYouMean !== undefined ? { didYouMean, nextCommand: `omc describe ${toolName} --param ${didYouMean}` } : {}),
1179
+ ...(otherTool !== undefined
1180
+ ? {
1181
+ otherTool,
1182
+ otherTools: otherTools.slice(0, 5),
1183
+ nextCommand: `omc describe ${otherTool} --param ${param}`,
1184
+ }
1185
+ : {}),
1186
+ ...(hintParts.length > 0 ? { hint: hintParts.join(";") } : {}),
1187
+ ...(dataIdentityHint !== undefined ? { guidance: dataIdentityHint } : {}),
1188
+ available: cappedAvailable,
1189
+ ...(available.length > cappedAvailable.length ? { availableTruncated: available.length - cappedAvailable.length } : {}),
1190
+ message: hintParts.length > 0
1191
+ ? `本工具没有参数 "${param}"——${hintParts.join(";")}`
1192
+ : `本工具没有参数 "${param}"——available 是本工具的合法参数(已截断)`,
1193
+ };
1194
+ }
1056
1195
  function cmdDiscovery(kind, transport, value, options = {}) {
1057
1196
  const registry = buildToolRegistry();
1058
1197
  let result;
@@ -1079,7 +1218,7 @@ function cmdDiscovery(kind, transport, value, options = {}) {
1079
1218
  // agent paid 4×54 KB on the default face before any hint could land).
1080
1219
  if (options.json !== true && options.compact !== true && !scoped) {
1081
1220
  result = commandIndex(registry);
1082
- return emitTransportResult({ status: "ok", ...result }, transport);
1221
+ return emitProjectedResult({ status: "ok", ...result }, transport, options.jq);
1083
1222
  }
1084
1223
  const inventory = commandInventory(registry, { compact: options.compact === true });
1085
1224
  // r19 ticket 05: above this many hits a filter word is "broad" — the filtered
@@ -1170,16 +1309,28 @@ function cmdDiscovery(kind, transport, value, options = {}) {
1170
1309
  }
1171
1310
  else if (options.schemaOnly)
1172
1311
  result = describeSchemaOnly(registry, value ?? "");
1173
- else if (options.param)
1174
- result = describeParam(registry, value ?? "", options.param);
1312
+ else if (options.param) {
1313
+ const missOrHit = describeParam(registry, value ?? "", options.param);
1314
+ // r31b F1: enrich a param miss with otherTool / capped available / guidance.
1315
+ result =
1316
+ missOrHit && missOrHit.found === false
1317
+ ? enrichDescribeParamMiss(registry, value ?? "", options.param, missOrHit)
1318
+ : missOrHit;
1319
+ }
1175
1320
  else if (options.example !== undefined)
1176
1321
  result = describeExample(registry, value ?? "", typeof options.example === "string" ? options.example : undefined);
1177
1322
  else if (options.full) {
1178
1323
  const full = describeToolFull(registry, value ?? "");
1179
- // r9: the same `--full` contract was re-shipped 8 times (134 KB) while the
1180
- // caller only wanted to re-check a shape. Within a project session the
1181
- // second identical read returns an artifact reference instead of the body.
1182
- result = full ? (repeatFullDescribe(value ?? "", full) ?? full) : full;
1324
+ // r31b F3: `--full` means "give me the body". Short-circuit only when the
1325
+ // caller opts in with `--cache-ok` (legacy r9 repeat-read suppression).
1326
+ if (!full)
1327
+ result = full;
1328
+ else if (options.cacheOk)
1329
+ result = repeatFullDescribe(value ?? "", full) ?? full;
1330
+ else {
1331
+ repeatFullDescribe(value ?? "", full, { recordOnly: true });
1332
+ result = full;
1333
+ }
1183
1334
  }
1184
1335
  else {
1185
1336
  result = describeTool(registry, value ?? "");
@@ -1236,7 +1387,7 @@ function cmdDiscovery(kind, transport, value, options = {}) {
1236
1387
  const textNote = byText.length > 0 ? `;didYouMeanByText: ${byText.join("、")}` : "";
1237
1388
  return emitInvalid(transport, { message: `${kind} target not found: "${requested}"${shown.length > 0 ? `; did you mean: ${shown.join("、")}` : ""}${textNote}${absence}${approvalPointer}${intentPointer}${formPointer}${pointer}` });
1238
1389
  }
1239
- return emitTransportResult({ status: "ok", ...result }, transport);
1390
+ return emitProjectedResult({ status: "ok", ...result }, transport, options.jq);
1240
1391
  }
1241
1392
  function cmdUsage(transport, options = {}) {
1242
1393
  return emitTransportResult(usageReport(undefined, { records: options.records === true, top: options.top, resolveLikeWriter: true }), transport);
@@ -1254,11 +1405,11 @@ function cmdNext(goal, transport) {
1254
1405
  * capability overview. Read-only, zero environment contact; a projection of
1255
1406
  * the tool registry, the coverage manifest, and the intent capability registry.
1256
1407
  */
1257
- function cmdCapabilities(transport, compact = false) {
1408
+ function cmdCapabilities(transport, compact = false, jq) {
1258
1409
  const result = compactCapabilities(buildToolRegistry(), { compact });
1259
- return emitTransportResult({ status: "ok", ...result }, transport);
1410
+ return emitProjectedResult({ status: "ok", ...result }, transport, jq);
1260
1411
  }
1261
- function cmdCapability(id, transport, executable = false) {
1412
+ function cmdCapability(id, transport, executable = false, jq) {
1262
1413
  const registry = buildToolRegistry();
1263
1414
  const resolved = resolveCapabilityId(id);
1264
1415
  if (!resolved) {
@@ -1269,9 +1420,64 @@ function cmdCapability(id, transport, executable = false) {
1269
1420
  const bundle = executableCapabilityBundle(registry, resolved);
1270
1421
  if (!bundle)
1271
1422
  return emitInvalid(transport, { message: `capability "${id}" 没有可执行 bundle` });
1272
- return emitResult({ status: "ok", capability: bundle }, transport);
1273
- }
1274
- return emitResult({ status: "ok", capability: capabilityDetail(registry, resolved) }, transport);
1423
+ return emitProjectedResult({ status: "ok", capability: bundle }, transport, jq);
1424
+ }
1425
+ const detail = capabilityDetail(registry, resolved);
1426
+ if (!detail)
1427
+ return emitInvalid(transport, { message: `capability "${id}" 无详情` });
1428
+ // r29a F4:app/folder/model/rule 面附应用参数名矩阵(主/别名),一次看完。
1429
+ const appScopeMatrix = buildAppScopeMatrix(registry, resolved);
1430
+ return emitProjectedResult({
1431
+ status: "ok",
1432
+ capability: {
1433
+ ...detail,
1434
+ ...(appScopeMatrix !== undefined ? { appScopeArgs: appScopeMatrix } : {}),
1435
+ },
1436
+ }, transport, jq);
1437
+ }
1438
+ /**
1439
+ * r29a F4:跨 app/folder/model/rule 的「工具 → 应用参数名(主/别名)」小表。
1440
+ * 只在这四个能力面上挂,避免每条 capability 都膨胀。
1441
+ */
1442
+ function buildAppScopeMatrix(registry, capabilityId) {
1443
+ const faces = new Set(["app", "folder", "model", "businessrule", "rule"]);
1444
+ if (!faces.has(capabilityId))
1445
+ return undefined;
1446
+ const laneByCapability = {
1447
+ app: "app-management",
1448
+ folder: "app-tree-folders",
1449
+ model: "model-management",
1450
+ businessrule: "B1-businessrule",
1451
+ rule: "B1-businessrule",
1452
+ };
1453
+ const lane = laneByCapability[capabilityId];
1454
+ const rows = registry
1455
+ .filter((tool) => tool.lane === lane || ((capabilityId === "rule" || capabilityId === "businessrule") && tool.name.startsWith("rule.")))
1456
+ .map((tool) => {
1457
+ const keys = Object.keys(tool.inputSchema);
1458
+ const primary = keys.includes("applicationCode") ? "applicationCode" : keys.includes("appCode") ? "appCode" : null;
1459
+ const aliases = keys.includes("applicationCode") && keys.includes("appCode")
1460
+ ? ["applicationCode", "appCode"]
1461
+ : primary === "applicationCode"
1462
+ ? ["applicationCode"]
1463
+ : primary === "appCode"
1464
+ ? ["appCode"]
1465
+ : [];
1466
+ return {
1467
+ tool: tool.name,
1468
+ primary,
1469
+ aliases,
1470
+ note: primary === null ? "无应用范围参数(按 schemaCode/自身键定位)" : aliases.length > 1 ? "双别名;冲突报 app-code-conflict" : undefined,
1471
+ };
1472
+ });
1473
+ if (rows.length === 0)
1474
+ return undefined;
1475
+ return {
1476
+ face: capabilityId,
1477
+ rows,
1478
+ relatedTopics: ["app-scope"],
1479
+ hint: "完整矩阵也见 `omc help app-scope`",
1480
+ };
1275
1481
  }
1276
1482
  /**
1277
1483
  * Read + validate an intent file, or emit the shared invalid-input envelope.
@@ -1621,14 +1827,14 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
1621
1827
  : "")
1622
1828
  : "";
1623
1829
  if (topic === undefined) {
1624
- console.error("用法:omc help <topic> | omc help --search \"<词>\"(跨 topic 检索,命中节正文随结果返回)");
1830
+ console.error("用法:omc help <topic> | omc help --topic <name> | omc help --search \"<词>\"(跨 topic 检索,命中节正文随结果返回)");
1625
1831
  console.error("权威路线:omc help omc-start-here(随包 skills/,不是分发镜像)");
1626
1832
  console.error("环境就绪:omc call discover_environment --env <env> --args '{}'");
1627
1833
  console.error(`命令:${commands.join("、")}`);
1628
1834
  console.error(`方法论 topic:${topics.map((entry) => entry.name).join("、")}`);
1629
1835
  return emitTransportResult({
1630
1836
  status: "ok",
1631
- usage: "omc help <topic>",
1837
+ usage: "omc help <topic> | omc help --topic <name>",
1632
1838
  discovery: DISCOVERY_AUTHORITY,
1633
1839
  commands,
1634
1840
  topics,
@@ -1702,7 +1908,22 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
1702
1908
  }
1703
1909
  if (all) {
1704
1910
  console.error(`omc help ${document.name} --all(${Buffer.byteLength(document.content, "utf8")} B)`);
1705
- return emitTransportResult({ status: "ok", topic: document }, transport);
1911
+ // r32 F3(真机):三种模式三种形状——默认回 sections[]、--section 回顶层 content、
1912
+ // --all 却回 {topic:{name,description,content}}。按前两种写的解析(.content /
1913
+ // .result.topic 当字符串)在 --all 上全部取不到,而默认模式的 hint 恰恰把 --all
1914
+ // 指成"确需整篇"的下一步。
1915
+ // 现在 --all 与前两者同形:topic 是 topic 名(字符串),正文在顶层 content,
1916
+ // 并附 bytes/chars/complete。**不要再把 content 放进 topic**——那会让整篇在
1917
+ // 信封里出现两次,撑破 64KiB 预算后被外置成 artifact(r32 实测:help-write-hazards
1918
+ // 因此从"直接回正文"退化成"回一个每次不同的 artifactId",golden 永久漂移)。
1919
+ return emitTransportResult({
1920
+ status: "ok",
1921
+ topic: document.name,
1922
+ content: document.content,
1923
+ bytes: Buffer.byteLength(document.content, "utf8"),
1924
+ chars: document.content.length,
1925
+ complete: true,
1926
+ }, transport);
1706
1927
  }
1707
1928
  // Shape first. Round 10 paid 35,872 B per full read of one topic, seven times
1708
1929
  // in a single session, to use one or two headings. The index names every
@@ -1738,14 +1959,16 @@ function onboardingHelp() {
1738
1959
  return [
1739
1960
  "首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
1740
1961
  "中文可读输出:任何命令加 --utf8——它让人类可读诊断(stderr/散文)按 UTF-8 原文输出。stdout 的 JSON 信封在非 UTF-8 代码页下仍保持 \\uXXXX 转义(保证 JSON.parse/ConvertFrom-Json 一定可解析,这是有意为之)。要连信封也是中文明文,必须先让管道按 UTF-8 解码(chcp 65001)——只设 OMC_ASCII_STDOUT=0 关写出端转义,在 cp936 管道下会把信封解码坏。",
1741
- "方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。找具体事实(错误码/字段名/命令名)不要整篇 dump:omc help --search \"<词>\"(跨 topic,命中节正文随结果返回)。",
1962
+ "方法论按需读:omc help <topic>(或 omc help --topic <name>)默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。找具体事实(错误码/字段名/命令名)不要整篇 dump:omc help --search \"<词>\"(跨 topic,命中节正文随结果返回)。",
1742
1963
  "目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
1743
- "大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --jq <path>(可重复;`[]` 映射数组,如 items[].fields.planNo)或 --extract/--extract-keys,省掉再跑一次 omc artifact get;artifact get 正文超 8KB 默认也只回形状(--jq/--keys 定向取,--full 才整包)。批量形状预检用 --args-list … --preflight(零 wire、零写入,一次报全)。",
1964
+ // r30f F20:写清两道门——读面 diet(~2048) 与 stdout 预算(--max-output-bytes,默认 64KiB) 不是一回事。
1965
+ "输出预算:`--max-output-bytes` 默认 65536(64KiB;超则回 artifact 指针 + sha256),只约束最终 stdout 信封(投影/--jq 之后);`artifact get` 内联上限 8192。读面另有独立 diet 门(~2048)会先外置 detailArtifactId——抬高 --max-output-bytes 不会把已 diet 的大表重新内联。",
1966
+ "大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --jq <path>(可重复;`[]` 映射数组,如 items[].fields.planNo)或 --extract/--extract-keys,省掉再跑一次 omc artifact get。commands/capabilities/describe 也支持 --jq。批量形状预检用 --args-list … --preflight(零 wire、零写入,一次报全)。",
1744
1967
  // r25 真机(2026-11-04):探针连跑同一读面 2–3 次想验证「第二次是否 unchanged」,
1745
1968
  // 结果三次都是全量,还去 describe 找 dedupe 参数(多花 3 轮)。读面确实不做跨调用
1746
1969
  // 结果去重——**明说**比留白强,省掉一次探索。
1747
1970
  "读面不做跨调用结果去重:同一读面同参数连跑 N 次就是 N 次真实读(不会回 unchanged,也没有 dedupe 开关)。要省就 --jq 定向取值或把结果落到自己的变量里复用;`unchanged:true` 只出现在写面回读缓存那一条路径。",
1748
- "工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
1971
+ "工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。应用参数矩阵:omc help app-scope。",
1749
1972
  "单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
1750
1973
  "写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
1751
1974
  ].join("\n");
@@ -1819,6 +2042,22 @@ async function main() {
1819
2042
  process.stdout.write(`${ownPackageVersion()}\n`);
1820
2043
  return 0;
1821
2044
  }
2045
+ // r29f F3:commander 在 action 前就吞掉 `--help`。`omc call <tool> --help`
2046
+ // 必须在这里短路转发 describe,否则永远只见通用 call 选项。
2047
+ if (bare[0] === "call" && bare[1] && bare[1] !== "help" && !bare[1].startsWith("-") && (bare.includes("--help") || bare.includes("-h"))) {
2048
+ const toolName = bare[1];
2049
+ const registry = buildToolRegistry();
2050
+ const described = describeTool(registry, toolName);
2051
+ console.error(`工具:${toolName}`);
2052
+ console.error(`输出预算:--max-output-bytes 默认 2048(超则回 artifact 指针 + sha256);artifact get 内联上限 8192`);
2053
+ console.error(`投影:--jq/--extract 相对 result(不要写 result. 前缀;误写会自动剥并标 autoStripped)`);
2054
+ if (described) {
2055
+ writeEnvelope(envelope("ok", { ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, request));
2056
+ return CLI_EXIT_CODES.ok;
2057
+ }
2058
+ writeEnvelope(envelope("error", { tool: toolName }, request, { code: "invalid-input", message: `未知工具 ${toolName};用 omc commands --json --compact 查名单`, retryable: false }));
2059
+ return CLI_EXIT_CODES.invalidInput;
2060
+ }
1822
2061
  command.addHelpText("after", `\n${onboardingHelp()}\n`);
1823
2062
  // No command at all: guide the agent (help text + onboarding), exit 0. This
1824
2063
  // is a benign "show me the surface" invocation, not a failure.
@@ -1826,7 +2065,11 @@ async function main() {
1826
2065
  command.outputHelp();
1827
2066
  return CLI_EXIT_CODES.ok;
1828
2067
  }
1829
- command.command("help [topic]").option("--section [heading]", "按标题取该节内容;可重复或用逗号一次取多节:--section a --section b / --section a,b(不带值则列出该 topic 的节标题)").option("--sections").option("--all", "回整篇正文(默认只回节索引:标题 + 各节字节)").option("--search <query>", "跨 topic 全文检索(错误码/字段名/命令名);命中节正文随结果返回,不需要再取一次").option("--search-mode <mode>", "all(默认,同一节包含全部词)或 any(包含任一)").option("--limit <n>", "检索最多返回几节(默认 5,1–50)").option("--max-output-bytes <bytes>").action(async (topic, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdHelp(topic, command.commands.map((entry) => entry.name()), transport, options.section, options.sections === true, options.all === true, typeof options.search === "string" ? options.search : undefined, typeof options.searchMode === "string" ? options.searchMode : undefined, typeof options.limit === "string" ? options.limit : undefined)); });
2068
+ // r30f F15:topic 既是位置参数也接受 `--topic <name>` 别名(肌肉记忆对齐 capability/contract)。
2069
+ command.command("help [topic]").option("--topic <name>", "topic 别名(与位置参数等价:`omc help omc-refusals` = `omc help --topic omc-refusals`)").option("--section [heading]", "按标题取该节内容;可重复或用逗号一次取多节:--section a --section b / --section a,b(不带值则列出该 topic 的节标题)").option("--sections").option("--all", "回整篇正文(默认只回节索引:标题 + 各节字节)").option("--search <query>", "跨 topic 全文检索(错误码/字段名/命令名);命中节正文随结果返回,不需要再取一次").option("--search-mode <mode>", "all(默认,同一节包含全部词)或 any(包含任一)").option("--limit <n>", "检索最多返回几节(默认 5,1–50)").option("--max-output-bytes <bytes>").action(async (topic, options) => {
2070
+ const resolvedTopic = typeof options.topic === "string" && options.topic.trim() !== "" ? options.topic.trim() : topic;
2071
+ commandExitCode = await withTransport(options, request, (transport) => cmdHelp(resolvedTopic, command.commands.map((entry) => entry.name()), transport, options.section, options.sections === true, options.all === true, typeof options.search === "string" ? options.search : undefined, typeof options.searchMode === "string" ? options.searchMode : undefined, typeof options.limit === "string" ? options.limit : undefined));
2072
+ });
1830
2073
  command.command("setup").option("--list").option("--edit <environment>").option("--remove <environment>").action(async (options) => {
1831
2074
  // r25 真机(2026-11-04):setup 不在 ownsStdout 里,于是非 TTY 拒绝时主流程会补一个
1832
2075
  // `error.code=internal` 的信封——明明是可预期的用法错误(stdin 不是终端),却被包成
@@ -1886,7 +2129,7 @@ async function main() {
1886
2129
  commandExitCode = CLI_EXIT_CODES.ok;
1887
2130
  });
1888
2131
  command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
1889
- command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
2132
+ command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
1890
2133
  const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
1891
2134
  ...(options.keys ? ["--keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
1892
2135
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
@@ -2030,7 +2273,7 @@ async function main() {
2030
2273
  .option("--code <schemaCode>", "模型 code")
2031
2274
  .option("--schema <schemaCode>")
2032
2275
  .option("--name <name>", "模型名")
2033
- .option("--fields <json>", "JSON 数组,每条是一个 field.create 参数;子表加 childFields")
2276
+ .option("--fields <json>", "JSON 数组,每条是一个 field.create 参数;子表加 subFields")
2034
2277
  .option("--fields-json <json>")
2035
2278
  .option("--folder <folderId>", "建在哪个应用目录(省略=默认目录)")
2036
2279
  .option("--app <applicationCode>")
@@ -2049,7 +2292,7 @@ async function main() {
2049
2292
  ` 常见错写:DEPARTMENT_MULTI→DEPARTMENT_MULTI_SELECTOR、STAFF_SINGLE→STAFF_SELECTOR、DROP_DOWN_BOX→DROPDOWN_BOX\n\n` +
2050
2293
  `choices(RADIO/CHECKBOX/DROPDOWN 系列):**字符串数组**,如 [\"待处理\",\"已完成\"]——不是 [{key,value}]\n` +
2051
2294
  `关联字段(WORK_SHEET/MULT_WORK_SHEET):options:{\"schemaCode\":\"<目标模型>\"} + relativeCode:\"<目标模型>\"\n` +
2052
- `子表:childFields:[…](每条同字段形状)\n`)
2295
+ `子表:subFields:[…](每条同字段形状;childFields 仍作别名接受)\n`)
2053
2296
  .action(async (options) => {
2054
2297
  const rest = ["model-scaffold",
2055
2298
  ...(options.code ? ["--code", options.code] : []),
@@ -2107,23 +2350,20 @@ async function main() {
2107
2350
  ...(options.apply ? ["--apply"] : [])];
2108
2351
  commandExitCode = await cmdFieldAdd(rest);
2109
2352
  });
2110
- command.command("script [file]").option("--env <environment>", "行内无 env 时的默认环境").option("--continue", "某行失败后继续执行剩余行(默认停在首个失败)").description("NDJSON 多调脚本:每行 {\"tool\":\"<名称>\",\"args\":{...},\"env\":\"<环境>\",\"dryRun\":true}(# 注释/空行忽略;file 为 - 读 stdin);一个进程一次会话跑完全部,逐行输出 {index,tool,exitCode,result}").action(async (file, options) => {
2353
+ command.command("script [file]").option("--env <environment>", "行内无 env 时的默认环境").option("--continue", "某行失败后继续执行剩余行(默认首败即停)").description("NDJSON 多调脚本:每行 {\"tool\":\"<名称>\",\"args\":{...},\"env\":\"<环境>\",\"dryRun\":true}(# 与 // 注释/空行忽略;其它非法行跳过计入 summary.skipped;file 为 - 读 stdin)。一个进程一次会话串行跑完全部(共用登录,比逐条 omc call 快;测写锁竞争请用多个独立 omc call 进程)。逐行输出 {index,tool,exitCode,result},末行 {summary:{total,ok,failed,skipped,stoppedAt?}}。").action(async (file, options) => {
2111
2354
  const { runScript, parseScript, readScriptInput } = await import("./script.js");
2112
- let lines;
2113
- try {
2114
- lines = parseScript(readScriptInput(file ?? "-"));
2115
- }
2116
- catch (error) {
2117
- console.error(String(error instanceof Error ? error.message : error));
2355
+ const parsed = parseScript(readScriptInput(file ?? "-"));
2356
+ const lines = parsed.lines;
2357
+ if (lines.length === 0 && parsed.skipped.length === 0) {
2358
+ console.error("脚本为空:每行一个 {\"tool\":\"<名称>\",\"args\":{...}} 调用(# 与 // 开头为注释)");
2118
2359
  commandExitCode = CLI_EXIT_CODES.invalidInput;
2119
2360
  return;
2120
2361
  }
2121
- if (lines.length === 0) {
2122
- console.error("脚本为空:每行一个 {\"tool\":\"<名称>\",\"args\":{...}} 调用");
2123
- commandExitCode = CLI_EXIT_CODES.invalidInput;
2124
- return;
2125
- }
2126
- commandExitCode = await runScript(lines, { defaultEnv: options.env, continueOnError: options.continue === true });
2362
+ commandExitCode = await runScript(lines, {
2363
+ defaultEnv: options.env,
2364
+ continueOnError: options.continue === true,
2365
+ skipped: parsed.skipped,
2366
+ });
2127
2367
  // `script` emits NDJSON (one envelope per line), so the single-envelope
2128
2368
  // recorder never ran and usage.jsonl left every batch at its initial
2129
2369
  // "no-envelope" status — a successful 4-call batch looked like 4 failures
@@ -2132,7 +2372,7 @@ async function main() {
2132
2372
  recordUsageOutput({
2133
2373
  status: commandExitCode === 0 ? "ok" : "refused",
2134
2374
  ...(commandExitCode === 0 ? {} : { error: { code: "script-batch-failed" } }),
2135
- script: { lines: lines.length },
2375
+ script: { lines: lines.length, skipped: parsed.skipped.length },
2136
2376
  }, 0);
2137
2377
  });
2138
2378
  const rollbackCommand = command.command("rollback").description("配置回滚(git-revert 语义):list 列写时间线 / plan 预览恢复(diff+后续写覆盖警告)/ apply 执行恢复——恢复走完整写闸门并记 journal(回滚可再回滚)。接入全部配置写面(workflow/规则/数据规则/视图/表单草稿/在线JS/字典/字段/模型/应用/报表);publish 类生命周期推进不可逆(plan 会说明);journal 与 before 快照为本机边界(不跨机器)");
@@ -2187,14 +2427,15 @@ async function main() {
2187
2427
  pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
2188
2428
  });
2189
2429
  command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
2190
- command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}) })); });
2191
- command.command("describe <tool>").option("--full").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").action(async (tool, options) => {
2192
- commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("describe", transport, tool, { full: options.full === true, schemaOnly: options.schemaOnly === true, ...(options.param ? { param: options.param } : {}), ...(options.example ? { example: options.example } : {}) }));
2430
+ const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
2431
+ command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) })); });
2432
+ command.command("describe <tool>").option("--full").option("--cache-ok", "允许 --full 在同会话重复读时短路为 artifact 指针(默认每次回全文)").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (tool, options) => {
2433
+ commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("describe", transport, tool, { full: options.full === true, cacheOk: options.cacheOk === true, schemaOnly: options.schemaOnly === true, ...(options.param ? { param: options.param } : {}), ...(options.example ? { example: options.example } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) }));
2193
2434
  });
2194
- command.command("contract <lane>").option("--full").option("--tool <name>").option("--max-output-bytes <bytes>").action(async (lane, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("contract", transport, lane, { full: options.full === true, ...(options.tool ? { tool: options.tool } : {}) })); });
2435
+ command.command("contract <lane>").option("--full").option("--tool <name>").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (lane, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("contract", transport, lane, { full: options.full === true, ...(options.tool ? { tool: options.tool } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) })); });
2195
2436
  // 2026-11-05 用户实测:`commands --json` 存在,于是大家自然写 `capabilities --json`
2196
2437
  // ——旧实现报 `unknown option`(退出码 10)。输出本来就是 JSON 信封,接受它做兼容别名。
2197
- command.command("capabilities").option("--compact").option("--json", "兼容别名:输出本就是 JSON 信封(与 commands --json 对齐)").option("--max-output-bytes <bytes>").action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdCapabilities(transport, options.compact === true)); });
2438
+ command.command("capabilities").option("--compact").option("--json", "兼容别名:输出本就是 JSON 信封(与 commands --json 对齐)").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdCapabilities(transport, options.compact === true, options.jq)); });
2198
2439
  // `--env` is accepted but ignored: `next` is a zero-environment-contact route
2199
2440
  // solver, and a caller who reflexively passes `--env` should not be refused
2200
2441
  // (round 15 dogfood: `next "<goal>" --env local` → unknown option, one round).
@@ -2208,8 +2449,8 @@ async function main() {
2208
2449
  ...(Number.isFinite(parsedTop) && parsedTop > 0 ? { top: parsedTop } : {}),
2209
2450
  }));
2210
2451
  });
2211
- command.command("capability <id>").option("--executable").option("--max-output-bytes <bytes>").action(async (id, options) => {
2212
- commandExitCode = await withTransport(options, request, (transport) => cmdCapability(id, transport, options.executable === true));
2452
+ command.command("capability <id>").option("--executable").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (id, options) => {
2453
+ commandExitCode = await withTransport(options, request, (transport) => cmdCapability(id, transport, options.executable === true, options.jq));
2213
2454
  });
2214
2455
  const intentCommand = command.command("intent");
2215
2456
  intentCommand.command("schema").option("--example [id]").action((options) => { commandExitCode = cmdIntentSchema({ request, maxOutputBytes: CLI_DEFAULT_OUTPUT_BYTES }, typeof options.example === "string" ? options.example : undefined, options.example !== undefined); });