@stalfh233/omc-cli 0.6.11 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) 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 +34 -4
  8. package/dist/cli-output.js +6 -2
  9. package/dist/cli.js +303 -69
  10. package/dist/describe-cache.js +8 -4
  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 +19 -1
  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 +2 -1
  24. package/dist/lanes/field.js +707 -59
  25. package/dist/lanes/form.js +52 -6
  26. package/dist/lanes/list-view-contract.js +2 -2
  27. package/dist/lanes/list-view.js +98 -2
  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 +192 -32
  38. package/dist/read-shapes.js +31 -11
  39. package/dist/recipes/field.js +16 -3
  40. package/dist/recipes/form.js +2 -1
  41. package/dist/recipes/runtime-data.js +58 -8
  42. package/dist/recipes/workflow.js +86 -1
  43. package/dist/refusal-guidance.js +18 -6
  44. package/dist/rollback-spec.js +41 -2
  45. package/dist/rollback.js +49 -11
  46. package/dist/script.js +72 -12
  47. package/dist/skills.js +91 -3
  48. package/dist/tools.js +113 -1
  49. package/dist/wire.js +13 -1
  50. package/dist/write-lock.js +92 -4
  51. package/dist/write.js +129 -16
  52. package/package.json +5 -3
  53. package/skills/omc-business-configuration.md +1 -1
  54. package/skills/omc-capabilities.md +5 -4
  55. package/skills/omc-playbook.md +41 -0
  56. package/skills/omc-refusals.md +58 -6
  57. package/skills/omc-tool-selection.md +2 -0
  58. 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
  }
@@ -1027,11 +1082,101 @@ async function cmdBench(args) {
1027
1082
  const liveSample = args.includes("--live-sample");
1028
1083
  const envIndex = args.indexOf("--env");
1029
1084
  const liveEnvironment = envIndex >= 0 ? args[envIndex + 1] : undefined;
1030
- const { runBench, formatBenchReport } = await import("./bench/run.js");
1085
+ // 2026-11-05(安装事故):`@stalfh233/omc-fake-platform` 是**评测基座**,只有
1086
+ // `omc bench` 用得到;它此前是硬依赖,导致「这一个包在某人缓存里还没同步」
1087
+ // 就把整次 `npm i -g omc-cli` 卡死(ETARGET)。现在它降为 optionalDependencies
1088
+ // (装不上不阻塞安装),bench 在缺失时给可执行的补救指引而不是抛堆栈。
1089
+ let runBenchImport;
1090
+ try {
1091
+ const mod = await import("./bench/run.js");
1092
+ runBenchImport = async () => mod;
1093
+ }
1094
+ catch (error) {
1095
+ const code = error.code;
1096
+ if (code === "ERR_MODULE_NOT_FOUND" && String(error.message).includes("omc-fake-platform")) {
1097
+ console.error("omc bench 需要评测基座 @stalfh233/omc-fake-platform(**可选依赖**,不装 CLI 也能正常用)。\n" +
1098
+ "补齐:npm i -D @stalfh233/omc-fake-platform@<与 omc-cli 同版本>\n" +
1099
+ "(它只是本地假平台/假 HTTP 服务器,仅 bench 与测试使用,配置 CloudPivot 完全不需要。)");
1100
+ return 10;
1101
+ }
1102
+ throw error;
1103
+ }
1104
+ const { runBench, formatBenchReport } = await runBenchImport();
1031
1105
  const report = await runBench({ mode: liveSample ? "live-sample" : "fake", ...(liveEnvironment ? { liveEnvironment } : {}) });
1032
1106
  console.error(formatBenchReport(report));
1033
1107
  const failedSteps = report.steps.filter((step) => !step.ok).length;
1034
- return failedSteps === 0 && report.dimensions.intent.passed === report.dimensions.intent.cases ? 0 : 1;
1108
+ const dims = report.dimensions;
1109
+ return failedSteps === 0 && dims.intent.passed === dims.intent.cases ? 0 : 1;
1110
+ }
1111
+ /** r29f F8:discovery 面复用同一投影层(相对本命令 result,不是 call 信封)。 */
1112
+ function emitProjectedResult(result, transport, jq) {
1113
+ const raw = Array.isArray(jq) ? jq : jq === undefined ? [] : [jq];
1114
+ const keysOnly = raw.some((entry) => entry === true) || (raw.length === 0 && jq === true);
1115
+ const paths = raw
1116
+ .filter((entry) => typeof entry === "string")
1117
+ .flatMap((path) => path.split(","))
1118
+ .map((path) => path.trim())
1119
+ .filter(Boolean);
1120
+ if (paths.length === 0 && !keysOnly)
1121
+ return emitTransportResult(result, transport);
1122
+ const projected = projectResultExtract(result, paths, keysOnly);
1123
+ return emitTransportResult(projected ?? result, transport);
1124
+ }
1125
+ function enrichDescribeParamMiss(registry, toolName, param, miss) {
1126
+ // r31b F1: a miss must teach the next move — closest name, or "that param
1127
+ // lives on another tool" — instead of dumping the whole available table.
1128
+ const available = Array.isArray(miss.available) ? miss.available.map((entry) => String(entry)) : [];
1129
+ const didYouMean = typeof miss.didYouMean === "string" ? miss.didYouMean : undefined;
1130
+ const family = toolName.includes(".") ? toolName.split(".")[0] : toolName;
1131
+ // Known cross-tool identity pairs first (新建 vs 更新), then same-family,
1132
+ // then any other tool that declares the param.
1133
+ const preferredPair = toolName === "data.save" && param === "bizObjectId"
1134
+ ? "data.update"
1135
+ : toolName === "data.update" && param === "formData"
1136
+ ? "data.save"
1137
+ : undefined;
1138
+ const otherTools = registry
1139
+ .filter((tool) => tool.name !== toolName && tool.inputSchema && Object.prototype.hasOwnProperty.call(tool.inputSchema, param))
1140
+ .map((tool) => tool.name)
1141
+ .sort((left, right) => {
1142
+ const rank = (name) => {
1143
+ if (preferredPair !== undefined && name === preferredPair)
1144
+ return 0;
1145
+ if (name.startsWith(`${family}.`))
1146
+ return 1;
1147
+ return 2;
1148
+ };
1149
+ return rank(left) - rank(right) || left.localeCompare(right);
1150
+ });
1151
+ const otherTool = preferredPair && otherTools.includes(preferredPair) ? preferredPair : otherTools[0];
1152
+ // Known cross-tool identity guidance for the data lane (新建 vs 更新).
1153
+ const dataIdentityHint = toolName === "data.save" && param === "bizObjectId"
1154
+ ? "新建用 data.save(schemaCode+formData);更新用 data.update + bizObjectId"
1155
+ : undefined;
1156
+ const cappedAvailable = available.slice(0, 8);
1157
+ const hintParts = [
1158
+ didYouMean !== undefined ? `本工具最接近的参数是 ${didYouMean}` : undefined,
1159
+ otherTool !== undefined ? `参数 "${param}" 属于 ${otherTool}${otherTools.length > 1 ? `(同名还在 ${otherTools.slice(1, 3).join("、")})` : ""}` : undefined,
1160
+ dataIdentityHint,
1161
+ ].filter((part) => typeof part === "string" && part.length > 0);
1162
+ return {
1163
+ ...miss,
1164
+ ...(didYouMean !== undefined ? { didYouMean, nextCommand: `omc describe ${toolName} --param ${didYouMean}` } : {}),
1165
+ ...(otherTool !== undefined
1166
+ ? {
1167
+ otherTool,
1168
+ otherTools: otherTools.slice(0, 5),
1169
+ nextCommand: `omc describe ${otherTool} --param ${param}`,
1170
+ }
1171
+ : {}),
1172
+ ...(hintParts.length > 0 ? { hint: hintParts.join(";") } : {}),
1173
+ ...(dataIdentityHint !== undefined ? { guidance: dataIdentityHint } : {}),
1174
+ available: cappedAvailable,
1175
+ ...(available.length > cappedAvailable.length ? { availableTruncated: available.length - cappedAvailable.length } : {}),
1176
+ message: hintParts.length > 0
1177
+ ? `本工具没有参数 "${param}"——${hintParts.join(";")}`
1178
+ : `本工具没有参数 "${param}"——available 是本工具的合法参数(已截断)`,
1179
+ };
1035
1180
  }
1036
1181
  function cmdDiscovery(kind, transport, value, options = {}) {
1037
1182
  const registry = buildToolRegistry();
@@ -1059,7 +1204,7 @@ function cmdDiscovery(kind, transport, value, options = {}) {
1059
1204
  // agent paid 4×54 KB on the default face before any hint could land).
1060
1205
  if (options.json !== true && options.compact !== true && !scoped) {
1061
1206
  result = commandIndex(registry);
1062
- return emitTransportResult({ status: "ok", ...result }, transport);
1207
+ return emitProjectedResult({ status: "ok", ...result }, transport, options.jq);
1063
1208
  }
1064
1209
  const inventory = commandInventory(registry, { compact: options.compact === true });
1065
1210
  // r19 ticket 05: above this many hits a filter word is "broad" — the filtered
@@ -1150,16 +1295,28 @@ function cmdDiscovery(kind, transport, value, options = {}) {
1150
1295
  }
1151
1296
  else if (options.schemaOnly)
1152
1297
  result = describeSchemaOnly(registry, value ?? "");
1153
- else if (options.param)
1154
- result = describeParam(registry, value ?? "", options.param);
1298
+ else if (options.param) {
1299
+ const missOrHit = describeParam(registry, value ?? "", options.param);
1300
+ // r31b F1: enrich a param miss with otherTool / capped available / guidance.
1301
+ result =
1302
+ missOrHit && missOrHit.found === false
1303
+ ? enrichDescribeParamMiss(registry, value ?? "", options.param, missOrHit)
1304
+ : missOrHit;
1305
+ }
1155
1306
  else if (options.example !== undefined)
1156
1307
  result = describeExample(registry, value ?? "", typeof options.example === "string" ? options.example : undefined);
1157
1308
  else if (options.full) {
1158
1309
  const full = describeToolFull(registry, value ?? "");
1159
- // r9: the same `--full` contract was re-shipped 8 times (134 KB) while the
1160
- // caller only wanted to re-check a shape. Within a project session the
1161
- // second identical read returns an artifact reference instead of the body.
1162
- result = full ? (repeatFullDescribe(value ?? "", full) ?? full) : full;
1310
+ // r31b F3: `--full` means "give me the body". Short-circuit only when the
1311
+ // caller opts in with `--cache-ok` (legacy r9 repeat-read suppression).
1312
+ if (!full)
1313
+ result = full;
1314
+ else if (options.cacheOk)
1315
+ result = repeatFullDescribe(value ?? "", full) ?? full;
1316
+ else {
1317
+ repeatFullDescribe(value ?? "", full, { recordOnly: true });
1318
+ result = full;
1319
+ }
1163
1320
  }
1164
1321
  else {
1165
1322
  result = describeTool(registry, value ?? "");
@@ -1216,7 +1373,7 @@ function cmdDiscovery(kind, transport, value, options = {}) {
1216
1373
  const textNote = byText.length > 0 ? `;didYouMeanByText: ${byText.join("、")}` : "";
1217
1374
  return emitInvalid(transport, { message: `${kind} target not found: "${requested}"${shown.length > 0 ? `; did you mean: ${shown.join("、")}` : ""}${textNote}${absence}${approvalPointer}${intentPointer}${formPointer}${pointer}` });
1218
1375
  }
1219
- return emitTransportResult({ status: "ok", ...result }, transport);
1376
+ return emitProjectedResult({ status: "ok", ...result }, transport, options.jq);
1220
1377
  }
1221
1378
  function cmdUsage(transport, options = {}) {
1222
1379
  return emitTransportResult(usageReport(undefined, { records: options.records === true, top: options.top, resolveLikeWriter: true }), transport);
@@ -1234,11 +1391,11 @@ function cmdNext(goal, transport) {
1234
1391
  * capability overview. Read-only, zero environment contact; a projection of
1235
1392
  * the tool registry, the coverage manifest, and the intent capability registry.
1236
1393
  */
1237
- function cmdCapabilities(transport, compact = false) {
1394
+ function cmdCapabilities(transport, compact = false, jq) {
1238
1395
  const result = compactCapabilities(buildToolRegistry(), { compact });
1239
- return emitTransportResult({ status: "ok", ...result }, transport);
1396
+ return emitProjectedResult({ status: "ok", ...result }, transport, jq);
1240
1397
  }
1241
- function cmdCapability(id, transport, executable = false) {
1398
+ function cmdCapability(id, transport, executable = false, jq) {
1242
1399
  const registry = buildToolRegistry();
1243
1400
  const resolved = resolveCapabilityId(id);
1244
1401
  if (!resolved) {
@@ -1249,9 +1406,64 @@ function cmdCapability(id, transport, executable = false) {
1249
1406
  const bundle = executableCapabilityBundle(registry, resolved);
1250
1407
  if (!bundle)
1251
1408
  return emitInvalid(transport, { message: `capability "${id}" 没有可执行 bundle` });
1252
- return emitResult({ status: "ok", capability: bundle }, transport);
1253
- }
1254
- return emitResult({ status: "ok", capability: capabilityDetail(registry, resolved) }, transport);
1409
+ return emitProjectedResult({ status: "ok", capability: bundle }, transport, jq);
1410
+ }
1411
+ const detail = capabilityDetail(registry, resolved);
1412
+ if (!detail)
1413
+ return emitInvalid(transport, { message: `capability "${id}" 无详情` });
1414
+ // r29a F4:app/folder/model/rule 面附应用参数名矩阵(主/别名),一次看完。
1415
+ const appScopeMatrix = buildAppScopeMatrix(registry, resolved);
1416
+ return emitProjectedResult({
1417
+ status: "ok",
1418
+ capability: {
1419
+ ...detail,
1420
+ ...(appScopeMatrix !== undefined ? { appScopeArgs: appScopeMatrix } : {}),
1421
+ },
1422
+ }, transport, jq);
1423
+ }
1424
+ /**
1425
+ * r29a F4:跨 app/folder/model/rule 的「工具 → 应用参数名(主/别名)」小表。
1426
+ * 只在这四个能力面上挂,避免每条 capability 都膨胀。
1427
+ */
1428
+ function buildAppScopeMatrix(registry, capabilityId) {
1429
+ const faces = new Set(["app", "folder", "model", "businessrule", "rule"]);
1430
+ if (!faces.has(capabilityId))
1431
+ return undefined;
1432
+ const laneByCapability = {
1433
+ app: "app-management",
1434
+ folder: "app-tree-folders",
1435
+ model: "model-management",
1436
+ businessrule: "B1-businessrule",
1437
+ rule: "B1-businessrule",
1438
+ };
1439
+ const lane = laneByCapability[capabilityId];
1440
+ const rows = registry
1441
+ .filter((tool) => tool.lane === lane || ((capabilityId === "rule" || capabilityId === "businessrule") && tool.name.startsWith("rule.")))
1442
+ .map((tool) => {
1443
+ const keys = Object.keys(tool.inputSchema);
1444
+ const primary = keys.includes("applicationCode") ? "applicationCode" : keys.includes("appCode") ? "appCode" : null;
1445
+ const aliases = keys.includes("applicationCode") && keys.includes("appCode")
1446
+ ? ["applicationCode", "appCode"]
1447
+ : primary === "applicationCode"
1448
+ ? ["applicationCode"]
1449
+ : primary === "appCode"
1450
+ ? ["appCode"]
1451
+ : [];
1452
+ return {
1453
+ tool: tool.name,
1454
+ primary,
1455
+ aliases,
1456
+ note: primary === null ? "无应用范围参数(按 schemaCode/自身键定位)" : aliases.length > 1 ? "双别名;冲突报 app-code-conflict" : undefined,
1457
+ };
1458
+ });
1459
+ if (rows.length === 0)
1460
+ return undefined;
1461
+ return {
1462
+ face: capabilityId,
1463
+ rows,
1464
+ relatedTopics: ["app-scope"],
1465
+ hint: "完整矩阵也见 `omc help app-scope`",
1466
+ };
1255
1467
  }
1256
1468
  /**
1257
1469
  * Read + validate an intent file, or emit the shared invalid-input envelope.
@@ -1601,14 +1813,14 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
1601
1813
  : "")
1602
1814
  : "";
1603
1815
  if (topic === undefined) {
1604
- console.error("用法:omc help <topic> | omc help --search \"<词>\"(跨 topic 检索,命中节正文随结果返回)");
1816
+ console.error("用法:omc help <topic> | omc help --topic <name> | omc help --search \"<词>\"(跨 topic 检索,命中节正文随结果返回)");
1605
1817
  console.error("权威路线:omc help omc-start-here(随包 skills/,不是分发镜像)");
1606
1818
  console.error("环境就绪:omc call discover_environment --env <env> --args '{}'");
1607
1819
  console.error(`命令:${commands.join("、")}`);
1608
1820
  console.error(`方法论 topic:${topics.map((entry) => entry.name).join("、")}`);
1609
1821
  return emitTransportResult({
1610
1822
  status: "ok",
1611
- usage: "omc help <topic>",
1823
+ usage: "omc help <topic> | omc help --topic <name>",
1612
1824
  discovery: DISCOVERY_AUTHORITY,
1613
1825
  commands,
1614
1826
  topics,
@@ -1718,14 +1930,16 @@ function onboardingHelp() {
1718
1930
  return [
1719
1931
  "首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
1720
1932
  "中文可读输出:任何命令加 --utf8——它让人类可读诊断(stderr/散文)按 UTF-8 原文输出。stdout 的 JSON 信封在非 UTF-8 代码页下仍保持 \\uXXXX 转义(保证 JSON.parse/ConvertFrom-Json 一定可解析,这是有意为之)。要连信封也是中文明文,必须先让管道按 UTF-8 解码(chcp 65001)——只设 OMC_ASCII_STDOUT=0 关写出端转义,在 cp936 管道下会把信封解码坏。",
1721
- "方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。找具体事实(错误码/字段名/命令名)不要整篇 dump:omc help --search \"<词>\"(跨 topic,命中节正文随结果返回)。",
1933
+ "方法论按需读:omc help <topic>(或 omc help --topic <name>)默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。找具体事实(错误码/字段名/命令名)不要整篇 dump:omc help --search \"<词>\"(跨 topic,命中节正文随结果返回)。",
1722
1934
  "目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
1723
- "大结果取值一步到位:读面回 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、零写入,一次报全)。",
1935
+ // r30f F20:写清两道门——读面 diet(~2048) 与 stdout 预算(--max-output-bytes,默认 64KiB) 不是一回事。
1936
+ "输出预算:`--max-output-bytes` 默认 65536(64KiB;超则回 artifact 指针 + sha256),只约束最终 stdout 信封(投影/--jq 之后);`artifact get` 内联上限 8192。读面另有独立 diet 门(~2048)会先外置 detailArtifactId——抬高 --max-output-bytes 不会把已 diet 的大表重新内联。",
1937
+ "大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --jq <path>(可重复;`[]` 映射数组,如 items[].fields.planNo)或 --extract/--extract-keys,省掉再跑一次 omc artifact get。commands/capabilities/describe 也支持 --jq。批量形状预检用 --args-list … --preflight(零 wire、零写入,一次报全)。",
1724
1938
  // r25 真机(2026-11-04):探针连跑同一读面 2–3 次想验证「第二次是否 unchanged」,
1725
1939
  // 结果三次都是全量,还去 describe 找 dedupe 参数(多花 3 轮)。读面确实不做跨调用
1726
1940
  // 结果去重——**明说**比留白强,省掉一次探索。
1727
1941
  "读面不做跨调用结果去重:同一读面同参数连跑 N 次就是 N 次真实读(不会回 unchanged,也没有 dedupe 开关)。要省就 --jq 定向取值或把结果落到自己的变量里复用;`unchanged:true` 只出现在写面回读缓存那一条路径。",
1728
- "工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
1942
+ "工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。应用参数矩阵:omc help app-scope。",
1729
1943
  "单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
1730
1944
  "写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
1731
1945
  ].join("\n");
@@ -1799,6 +2013,22 @@ async function main() {
1799
2013
  process.stdout.write(`${ownPackageVersion()}\n`);
1800
2014
  return 0;
1801
2015
  }
2016
+ // r29f F3:commander 在 action 前就吞掉 `--help`。`omc call <tool> --help`
2017
+ // 必须在这里短路转发 describe,否则永远只见通用 call 选项。
2018
+ if (bare[0] === "call" && bare[1] && bare[1] !== "help" && !bare[1].startsWith("-") && (bare.includes("--help") || bare.includes("-h"))) {
2019
+ const toolName = bare[1];
2020
+ const registry = buildToolRegistry();
2021
+ const described = describeTool(registry, toolName);
2022
+ console.error(`工具:${toolName}`);
2023
+ console.error(`输出预算:--max-output-bytes 默认 2048(超则回 artifact 指针 + sha256);artifact get 内联上限 8192`);
2024
+ console.error(`投影:--jq/--extract 相对 result(不要写 result. 前缀;误写会自动剥并标 autoStripped)`);
2025
+ if (described) {
2026
+ writeEnvelope(envelope("ok", { ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, request));
2027
+ return CLI_EXIT_CODES.ok;
2028
+ }
2029
+ writeEnvelope(envelope("error", { tool: toolName }, request, { code: "invalid-input", message: `未知工具 ${toolName};用 omc commands --json --compact 查名单`, retryable: false }));
2030
+ return CLI_EXIT_CODES.invalidInput;
2031
+ }
1802
2032
  command.addHelpText("after", `\n${onboardingHelp()}\n`);
1803
2033
  // No command at all: guide the agent (help text + onboarding), exit 0. This
1804
2034
  // is a benign "show me the surface" invocation, not a failure.
@@ -1806,7 +2036,11 @@ async function main() {
1806
2036
  command.outputHelp();
1807
2037
  return CLI_EXIT_CODES.ok;
1808
2038
  }
1809
- 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)); });
2039
+ // r30f F15:topic 既是位置参数也接受 `--topic <name>` 别名(肌肉记忆对齐 capability/contract)。
2040
+ 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) => {
2041
+ const resolvedTopic = typeof options.topic === "string" && options.topic.trim() !== "" ? options.topic.trim() : topic;
2042
+ 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));
2043
+ });
1810
2044
  command.command("setup").option("--list").option("--edit <environment>").option("--remove <environment>").action(async (options) => {
1811
2045
  // r25 真机(2026-11-04):setup 不在 ownsStdout 里,于是非 TTY 拒绝时主流程会补一个
1812
2046
  // `error.code=internal` 的信封——明明是可预期的用法错误(stdin 不是终端),却被包成
@@ -1866,7 +2100,7 @@ async function main() {
1866
2100
  commandExitCode = CLI_EXIT_CODES.ok;
1867
2101
  });
1868
2102
  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); });
1869
- 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) => {
2103
+ 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) => {
1870
2104
  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"] : []),
1871
2105
  ...(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"] : [])];
1872
2106
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
@@ -2010,7 +2244,7 @@ async function main() {
2010
2244
  .option("--code <schemaCode>", "模型 code")
2011
2245
  .option("--schema <schemaCode>")
2012
2246
  .option("--name <name>", "模型名")
2013
- .option("--fields <json>", "JSON 数组,每条是一个 field.create 参数;子表加 childFields")
2247
+ .option("--fields <json>", "JSON 数组,每条是一个 field.create 参数;子表加 subFields")
2014
2248
  .option("--fields-json <json>")
2015
2249
  .option("--folder <folderId>", "建在哪个应用目录(省略=默认目录)")
2016
2250
  .option("--app <applicationCode>")
@@ -2029,7 +2263,7 @@ async function main() {
2029
2263
  ` 常见错写:DEPARTMENT_MULTI→DEPARTMENT_MULTI_SELECTOR、STAFF_SINGLE→STAFF_SELECTOR、DROP_DOWN_BOX→DROPDOWN_BOX\n\n` +
2030
2264
  `choices(RADIO/CHECKBOX/DROPDOWN 系列):**字符串数组**,如 [\"待处理\",\"已完成\"]——不是 [{key,value}]\n` +
2031
2265
  `关联字段(WORK_SHEET/MULT_WORK_SHEET):options:{\"schemaCode\":\"<目标模型>\"} + relativeCode:\"<目标模型>\"\n` +
2032
- `子表:childFields:[…](每条同字段形状)\n`)
2266
+ `子表:subFields:[…](每条同字段形状;childFields 仍作别名接受)\n`)
2033
2267
  .action(async (options) => {
2034
2268
  const rest = ["model-scaffold",
2035
2269
  ...(options.code ? ["--code", options.code] : []),
@@ -2087,23 +2321,20 @@ async function main() {
2087
2321
  ...(options.apply ? ["--apply"] : [])];
2088
2322
  commandExitCode = await cmdFieldAdd(rest);
2089
2323
  });
2090
- 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) => {
2324
+ 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) => {
2091
2325
  const { runScript, parseScript, readScriptInput } = await import("./script.js");
2092
- let lines;
2093
- try {
2094
- lines = parseScript(readScriptInput(file ?? "-"));
2095
- }
2096
- catch (error) {
2097
- console.error(String(error instanceof Error ? error.message : error));
2326
+ const parsed = parseScript(readScriptInput(file ?? "-"));
2327
+ const lines = parsed.lines;
2328
+ if (lines.length === 0 && parsed.skipped.length === 0) {
2329
+ console.error("脚本为空:每行一个 {\"tool\":\"<名称>\",\"args\":{...}} 调用(# 与 // 开头为注释)");
2098
2330
  commandExitCode = CLI_EXIT_CODES.invalidInput;
2099
2331
  return;
2100
2332
  }
2101
- if (lines.length === 0) {
2102
- console.error("脚本为空:每行一个 {\"tool\":\"<名称>\",\"args\":{...}} 调用");
2103
- commandExitCode = CLI_EXIT_CODES.invalidInput;
2104
- return;
2105
- }
2106
- commandExitCode = await runScript(lines, { defaultEnv: options.env, continueOnError: options.continue === true });
2333
+ commandExitCode = await runScript(lines, {
2334
+ defaultEnv: options.env,
2335
+ continueOnError: options.continue === true,
2336
+ skipped: parsed.skipped,
2337
+ });
2107
2338
  // `script` emits NDJSON (one envelope per line), so the single-envelope
2108
2339
  // recorder never ran and usage.jsonl left every batch at its initial
2109
2340
  // "no-envelope" status — a successful 4-call batch looked like 4 failures
@@ -2112,7 +2343,7 @@ async function main() {
2112
2343
  recordUsageOutput({
2113
2344
  status: commandExitCode === 0 ? "ok" : "refused",
2114
2345
  ...(commandExitCode === 0 ? {} : { error: { code: "script-batch-failed" } }),
2115
- script: { lines: lines.length },
2346
+ script: { lines: lines.length, skipped: parsed.skipped.length },
2116
2347
  }, 0);
2117
2348
  });
2118
2349
  const rollbackCommand = command.command("rollback").description("配置回滚(git-revert 语义):list 列写时间线 / plan 预览恢复(diff+后续写覆盖警告)/ apply 执行恢复——恢复走完整写闸门并记 journal(回滚可再回滚)。接入全部配置写面(workflow/规则/数据规则/视图/表单草稿/在线JS/字典/字段/模型/应用/报表);publish 类生命周期推进不可逆(plan 会说明);journal 与 before 快照为本机边界(不跨机器)");
@@ -2167,12 +2398,15 @@ async function main() {
2167
2398
  pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
2168
2399
  });
2169
2400
  command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
2170
- 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 } : {}) })); });
2171
- command.command("describe <tool>").option("--full").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").action(async (tool, options) => {
2172
- 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 } : {}) }));
2401
+ const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
2402
+ 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 } : {}) })); });
2403
+ 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) => {
2404
+ 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 } : {}) }));
2173
2405
  });
2174
- 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 } : {}) })); });
2175
- command.command("capabilities").option("--compact").option("--max-output-bytes <bytes>").action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdCapabilities(transport, options.compact === true)); });
2406
+ 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 } : {}) })); });
2407
+ // 2026-11-05 用户实测:`commands --json` 存在,于是大家自然写 `capabilities --json`
2408
+ // ——旧实现报 `unknown option`(退出码 10)。输出本来就是 JSON 信封,接受它做兼容别名。
2409
+ 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)); });
2176
2410
  // `--env` is accepted but ignored: `next` is a zero-environment-contact route
2177
2411
  // solver, and a caller who reflexively passes `--env` should not be refused
2178
2412
  // (round 15 dogfood: `next "<goal>" --env local` → unknown option, one round).
@@ -2186,8 +2420,8 @@ async function main() {
2186
2420
  ...(Number.isFinite(parsedTop) && parsedTop > 0 ? { top: parsedTop } : {}),
2187
2421
  }));
2188
2422
  });
2189
- command.command("capability <id>").option("--executable").option("--max-output-bytes <bytes>").action(async (id, options) => {
2190
- commandExitCode = await withTransport(options, request, (transport) => cmdCapability(id, transport, options.executable === true));
2423
+ command.command("capability <id>").option("--executable").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (id, options) => {
2424
+ commandExitCode = await withTransport(options, request, (transport) => cmdCapability(id, transport, options.executable === true, options.jq));
2191
2425
  });
2192
2426
  const intentCommand = command.command("intent");
2193
2427
  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); });