@stalfh233/omc-cli 0.6.6 → 0.6.10
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.
- package/README.md +2 -1
- package/dist/approval-flow.js +5 -3
- package/dist/bizservice-config.js +26 -10
- package/dist/call.js +47 -0
- package/dist/capability-overview.js +47 -24
- package/dist/cli-contract.js +90 -17
- package/dist/cli-output.js +41 -2
- package/dist/cli-stdout.js +74 -0
- package/dist/cli.js +162 -54
- package/dist/command-transport.js +8 -1
- package/dist/config-store.js +17 -2
- package/dist/discovery.js +62 -3
- package/dist/field-families.js +35 -6
- package/dist/goal-routes.js +118 -11
- package/dist/human-summary.js +11 -0
- package/dist/lanes/app-contract.js +12 -5
- package/dist/lanes/app.js +221 -134
- package/dist/lanes/apply-changes.js +2 -1
- package/dist/lanes/b2-registration-contract.js +3 -3
- package/dist/lanes/b2-registration.js +890 -693
- package/dist/lanes/businessrule.js +75 -51
- package/dist/lanes/custom-page.js +166 -115
- package/dist/lanes/datarule-contract.js +2 -2
- package/dist/lanes/datarule.js +238 -154
- package/dist/lanes/dictionary.js +253 -110
- package/dist/lanes/environment.js +57 -9
- package/dist/lanes/field.js +160 -107
- package/dist/lanes/folder-contract.js +7 -3
- package/dist/lanes/folder.js +249 -118
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +188 -64
- package/dist/lanes/list-view.js +405 -369
- package/dist/lanes/meta.js +31 -6
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +610 -512
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +160 -146
- package/dist/lanes/organization.js +905 -640
- package/dist/lanes/programming.js +274 -136
- package/dist/lanes/report-contract.js +2 -2
- package/dist/lanes/report.js +354 -198
- package/dist/lanes/rule-lifecycle.js +294 -140
- package/dist/lanes/runtime-data-contract.js +4 -4
- package/dist/lanes/runtime-data.js +869 -745
- package/dist/lanes/workflow-contract.js +3 -3
- package/dist/lanes/workflow.js +732 -605
- package/dist/model-scaffold.js +26 -5
- package/dist/path-projection.js +196 -14
- package/dist/plan.js +25 -0
- package/dist/programming-workflow.js +55 -3
- package/dist/read-shapes.js +146 -0
- package/dist/read.js +40 -9
- package/dist/recipes/businessrule.js +74 -24
- package/dist/recipes/dictionary.js +7 -4
- package/dist/recipes/form.js +46 -0
- package/dist/recipes/runtime-data.js +3 -2
- package/dist/refusal-guidance.js +29 -5
- package/dist/rule-support.js +14 -1
- package/dist/rule-when.js +2 -2
- package/dist/tool-types.js +9 -3
- package/dist/version-family-gaps.js +101 -0
- package/dist/write.js +2 -1
- package/package.json +6 -4
- package/skills/omc-glossary.md +13 -0
- package/skills/omc-refusals.md +13 -5
- package/skills/omc-start-here.md +2 -2
- package/skills/omc-tool-selection.md +2 -1
- package/skills/omc-write-hazards.md +5 -2
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Command, CommanderError, Option } from "commander";
|
|
|
10
10
|
import { ConfigStore, defaultPaths, removedProfileKeys } from "./config-store.js";
|
|
11
11
|
import { loadCoverageManifest, versionFamilyNotice } from "./coverage.js";
|
|
12
12
|
import { redactText } from "./redact.js";
|
|
13
|
-
import { readArgsFile, readStdinArgs, runToolCall, validateAgentPaths } from "./call.js";
|
|
13
|
+
import { readArgsFile, readStdinArgs, runToolCall, toolsByTextIntent, validateAgentPaths } from "./call.js";
|
|
14
14
|
import { BATCH_MAX_ENTRIES, runBatchCall } from "./batch-call.js";
|
|
15
15
|
import { cmdModelScaffold } from "./model-scaffold.js";
|
|
16
16
|
import { cmdFieldAdd } from "./field-add.js";
|
|
@@ -18,11 +18,13 @@ import { cmdModelPurge } from "./model-purge.js";
|
|
|
18
18
|
import { cmdProgrammingAdopt, cmdProgrammingApply, cmdProgrammingPull, cmdProgrammingStatus, cmdProgrammingTest } from "./programming-workflow.js";
|
|
19
19
|
import { environmentWritable, requiresProductionConfirmation } from "./environment-policy.js";
|
|
20
20
|
import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, humanUtf8, installCodePageSafeStderr, refusalText, requestId } from "./cli-contract.js";
|
|
21
|
-
import {
|
|
21
|
+
import { ownsStdout } from "./cli-stdout.js";
|
|
22
|
+
import { formatDoctorHumanSummary, unsupportedCapabilities } from "./version-family-gaps.js";
|
|
23
|
+
import { beginWriteFace, configureOutput, endWriteFace, humanSummary, maybeWarnMergedStreams, stripOutputFlags, writeEnvelope } from "./cli-output.js";
|
|
22
24
|
import { beginUsage, completeUsage, currentUsageArgsDigest, ensureProjectIgnore, priorIdenticalSuccess, recordUsageOutput, usageReport } from "./usage-log.js";
|
|
23
25
|
import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitThrown, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
|
|
24
26
|
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
|
|
25
|
-
import { projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
|
|
27
|
+
import { artifactPathMissHint, nullProjectionHint, partialPathMissHint, projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
|
|
26
28
|
import { basename, join, resolve } from "node:path";
|
|
27
29
|
import { buildToolRegistry } from "./tools.js";
|
|
28
30
|
import { acquireWriteLock } from "./write-lock.js";
|
|
@@ -177,6 +179,9 @@ async function cmdDoctor(store, environment, serviceCode) {
|
|
|
177
179
|
const manifest = loadCoverageManifest();
|
|
178
180
|
const steps = [];
|
|
179
181
|
let ok = true;
|
|
182
|
+
// 版本族缺口(§三.3-9):没有版本证据时也要照实说「无证据 → 写 fail closed」,
|
|
183
|
+
// 所以用 undefined 而不是拼一个默认值。
|
|
184
|
+
let detectedVersion;
|
|
180
185
|
// r45 Windows 实测:PowerShell 管道按 GBK(936) 解码 CLI 的 UTF-8 输出 → 全屏乱码,
|
|
181
186
|
// 且用户常误判为「OMC 输出坏了」。doctor 是入口体检,顺手检测活动代码页。
|
|
182
187
|
if (process.platform === "win32") {
|
|
@@ -222,6 +227,7 @@ async function cmdDoctor(store, environment, serviceCode) {
|
|
|
222
227
|
for (const note of session.loginNotes())
|
|
223
228
|
steps.push(`OAuth 配置 WARN ${note}`);
|
|
224
229
|
const version = session.detectedVersion();
|
|
230
|
+
detectedVersion = version;
|
|
225
231
|
if (!version) {
|
|
226
232
|
steps.push(`版本 WARN 无版本证据;写入将保守拒绝(fail closed)`);
|
|
227
233
|
}
|
|
@@ -306,16 +312,56 @@ async function cmdDoctor(store, environment, serviceCode) {
|
|
|
306
312
|
? `环境 ${kind} 已配置环境:${configured || "(none)"};用 omc env list 查看,再以 --env <name> 指定或 omc setup 绑定;对象不存在(modelPresent:false)不是环境错误`
|
|
307
313
|
: `环境 ${kind} 环境已配置但平台不可达;先查网络/服务,不要改环境名试错(已配置:${configured || "(none)"})`);
|
|
308
314
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
// r26b 读回体积审计(2026-11-05):人类体检行 + 结构化信封曾把同一批 check
|
|
316
|
+
// **各写一份**(3073 B 里约 1450 B 是重复)。管道/agent 场景(非 TTY)只留一行
|
|
317
|
+
// 汇总 + 指针;真人在终端里看到的仍是完整逐项明细。
|
|
318
|
+
const interactive = process.stderr.isTTY === true;
|
|
319
|
+
if (interactive) {
|
|
320
|
+
for (const line of steps)
|
|
321
|
+
console.error(line);
|
|
322
|
+
console.error(doctorAnchor());
|
|
323
|
+
console.error(ok ? "doctor:通过" : "doctor:失败");
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const warns = steps.filter((line) => /(^|\s)(⚠|WARN|FAIL)(\s|$)/.test(line)).length;
|
|
327
|
+
// r28i F1: name version + unavailable capability ids in the human line
|
|
328
|
+
// (formatDoctorHumanSummary lives in version-family-gaps.ts).
|
|
329
|
+
const gaps = unsupportedCapabilities(manifest, detectedVersion);
|
|
330
|
+
console.error(formatDoctorHumanSummary({
|
|
331
|
+
ok,
|
|
332
|
+
warnCount: warns,
|
|
333
|
+
detectedVersion,
|
|
334
|
+
unsupported: gaps,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
// 结构化回执(HANDOFF §三.2-6 / §三.3-9):doctor 此前只写 stderr,主流程于是
|
|
338
|
+
// 补一个空 `ok` 信封(失败路径还补过谎报 `internal` 的)。现在它自己写**一个**
|
|
339
|
+
// 信封:体检行 + 冷启动锚点 + 已知能力缺口。冷启动锚点从 stderr 移到这里
|
|
340
|
+
// (r26b:stderr 给了 agent 也读不到结构化字段,而锚点是每会话都要用的指引)。
|
|
341
|
+
writeEnvelope(envelope(ok ? "ok" : "failed", {
|
|
342
|
+
checks: steps.map(parseDoctorStep),
|
|
343
|
+
anchor: doctorAnchor(),
|
|
344
|
+
unsupportedCapabilities: unsupportedCapabilities(manifest, detectedVersion),
|
|
345
|
+
}, requestId()));
|
|
317
346
|
return ok ? 0 : 1;
|
|
318
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* `配置 ok 环境=x` → `{area:"配置", level:"ok", message:"环境=x"}`。
|
|
350
|
+
* 解析失败的行(极少数自由文本)退化成 `{area:"doctor", level:"info"}`,
|
|
351
|
+
* 保证结构化回执不缺行——宁可字段糙一点,也不要静默丢一条体检结论。
|
|
352
|
+
*/
|
|
353
|
+
function parseDoctorStep(line) {
|
|
354
|
+
const match = /^(\S+)\s+(ok|⚠|WARN|FAIL|--)\s+(.*)$/.exec(line);
|
|
355
|
+
if (!match) {
|
|
356
|
+
// `环境 env-not-pinned …` 这类第二档:区域 + 机器码 + 说明。
|
|
357
|
+
const typed = /^(\S+)\s+([a-z][a-z0-9-]+)\s+(.*)$/.exec(line);
|
|
358
|
+
if (typed)
|
|
359
|
+
return { area: typed[1], level: "info", message: `${typed[2]}:${typed[3]}` };
|
|
360
|
+
return { area: "doctor", level: "info", message: line };
|
|
361
|
+
}
|
|
362
|
+
const level = match[2] === "⚠" ? "warn" : match[2] === "--" ? "skip" : match[2].toLowerCase();
|
|
363
|
+
return { area: match[1], level, message: match[3] };
|
|
364
|
+
}
|
|
319
365
|
/** The cold-start anchor `doctor` appends: minimal path, artifact convention, readiness. */
|
|
320
366
|
function doctorAnchor() {
|
|
321
367
|
const topics = listSkills().map((entry) => entry.name).join("、");
|
|
@@ -456,6 +502,7 @@ async function cmdCall(store, args) {
|
|
|
456
502
|
result = { ...result, nextAction: "read back the requestId before retrying; retry only after the outcome is established" };
|
|
457
503
|
const classified = classifyResult(result);
|
|
458
504
|
writeEnvelope(envelope(status, boundResult(result, maxOutputBytes), request, classified.error));
|
|
505
|
+
maybeWarnMergedStreams();
|
|
459
506
|
return exitCode;
|
|
460
507
|
};
|
|
461
508
|
const flag = (name) => argvValue(args, name);
|
|
@@ -574,16 +621,38 @@ async function cmdCall(store, args) {
|
|
|
574
621
|
// 「无声」——`{"limt":5}` / `{"filterr":{…}}` 被 zod 剥掉后返回全量,调用方
|
|
575
622
|
// 会读成「过滤没生效 / 数据就是这么多」。读面在**不阻断**的前提下把被忽略的
|
|
576
623
|
// 键回显出来(写面仍是 fail-closed,见 parseToolInput)。
|
|
624
|
+
//
|
|
625
|
+
// F6(r27):应用级键(appCode/applicationCode/application)静默回落会把写
|
|
626
|
+
// 操作打到错误应用——这些键被忽略时直接拒绝,不进 ignoredArgs。
|
|
577
627
|
const ignoredReadKeys = [];
|
|
578
|
-
if (tool
|
|
628
|
+
if (tool) {
|
|
579
629
|
const schemaKeys = new Set(Object.keys(tool.inputSchema));
|
|
580
630
|
const recognized = new Set(["environment", "capability", "requestId", "dryRun"]);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
631
|
+
const unknownKeys = Object.keys(parsedArgs).filter((key) => !schemaKeys.has(key) && !recognized.has(key));
|
|
632
|
+
const appScopeUnknown = unknownKeys.filter((key) => APP_SCOPE_ARG_KEYS.has(key));
|
|
633
|
+
if (appScopeUnknown.length > 0) {
|
|
634
|
+
const canonical = schemaKeys.has("applicationCode")
|
|
635
|
+
? "applicationCode"
|
|
636
|
+
: schemaKeys.has("appCode")
|
|
637
|
+
? "appCode"
|
|
638
|
+
: "applicationCode(或 appCode,以 omc describe 为准)";
|
|
639
|
+
const wrong = appScopeUnknown.join("、");
|
|
640
|
+
const result = {
|
|
641
|
+
status: "refused",
|
|
642
|
+
blocker: "unknown-app-scope-arg",
|
|
643
|
+
tool: toolName,
|
|
644
|
+
ignored: appScopeUnknown,
|
|
645
|
+
acceptedAppScopeArg: canonical,
|
|
646
|
+
message: `本工具的应用范围参数叫 ${canonical},你传的 ${wrong} 不会生效——为避免写到错误应用,本次调用已拒绝。请改用 ${canonical} 后重试(\`omc describe ${toolName}\` 可确认参数表)。`,
|
|
647
|
+
};
|
|
648
|
+
return emit("refused", result, CLI_EXIT_CODES.policyRefused);
|
|
649
|
+
}
|
|
650
|
+
if (tool.kind !== "write") {
|
|
651
|
+
for (const key of unknownKeys) {
|
|
652
|
+
// r25 真机:`limt:5` 被就近指到 `filter`(共享 "fi" 就够过阈值),照提示改
|
|
653
|
+
// 会把「取 5 条」变成「按条件过滤」。短键走显式缩写表,语义才不跑偏。
|
|
654
|
+
ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys]) });
|
|
655
|
+
}
|
|
587
656
|
}
|
|
588
657
|
}
|
|
589
658
|
if (tool) {
|
|
@@ -601,6 +670,9 @@ async function cmdCall(store, args) {
|
|
|
601
670
|
}
|
|
602
671
|
}
|
|
603
672
|
const toolKind = tool?.kind;
|
|
673
|
+
// F2':只对写面(含 dry-run 预览)标记;读面人话不追加 2>&1 提示。
|
|
674
|
+
if (toolKind === "write")
|
|
675
|
+
beginWriteFace();
|
|
604
676
|
let lock;
|
|
605
677
|
// A `--dry-run` never mutates the environment: the governed pipeline settles it
|
|
606
678
|
// before write admission, so it must not contend for the cross-process lock.
|
|
@@ -673,7 +745,13 @@ async function cmdCall(store, args) {
|
|
|
673
745
|
};
|
|
674
746
|
}
|
|
675
747
|
const classified = classifyResult(result);
|
|
676
|
-
|
|
748
|
+
try {
|
|
749
|
+
return emit(classified.status, result, classified.exitCode);
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
if (toolKind === "write")
|
|
753
|
+
endWriteFace();
|
|
754
|
+
}
|
|
677
755
|
}
|
|
678
756
|
/** Collect every value of a repeatable flag (`--extract a --extract b`). */
|
|
679
757
|
function argvValues(args, name) {
|
|
@@ -687,6 +765,8 @@ function argvValues(args, name) {
|
|
|
687
765
|
}
|
|
688
766
|
return values;
|
|
689
767
|
}
|
|
768
|
+
/** F6:应用级范围键。被工具 schema 忽略时必须拒绝,不能静默回落到默认应用。 */
|
|
769
|
+
const APP_SCOPE_ARG_KEYS = new Set(["appCode", "applicationCode", "application"]);
|
|
690
770
|
/**
|
|
691
771
|
* 读面「拼错键」建议(r25 真机实证,2026-11-04):`limt:5` 曾按编辑距离被指到
|
|
692
772
|
* `filter`——两者共享 "fi" 片段就过阈值,但语义完全不同(取 5 条 vs 按条件过滤)。
|
|
@@ -750,15 +830,12 @@ function projectArtifactPath(value, path) {
|
|
|
750
830
|
}
|
|
751
831
|
/**
|
|
752
832
|
* T6(GitLab #17):`mapped`/`nullEntries` 的读法要跟数据一起走,否则 `mapped:5`
|
|
753
|
-
* 会被读成"取到 5 个值"
|
|
833
|
+
* 会被读成"取到 5 个值"。文案的单一真相源在 path-projection(与 `--extract`
|
|
834
|
+
* 面共用),这里只负责从产物内容算 total。
|
|
754
835
|
*/
|
|
755
|
-
function
|
|
756
|
-
if (nullEntries === undefined || nullEntries === 0)
|
|
757
|
-
return undefined;
|
|
836
|
+
function artifactNullHint(content, nullEntries) {
|
|
758
837
|
const total = Array.isArray(content) ? content.length : 0;
|
|
759
|
-
|
|
760
|
-
return `其中 ${nullEntries} 个命中值为 null(不区分"字段为空"与"列表未投影")。`;
|
|
761
|
-
return `路径全部命中但值都是 null——mapped=${total} 只表示命中条目数。常见成因:列表视图未配置该列(data.list 只投影视图列)→ 用 data.load 读单条权威值,或先 listview.configure 配列。`;
|
|
838
|
+
return nullProjectionHint(nullEntries, total);
|
|
762
839
|
}
|
|
763
840
|
function formatBytes(bytes) {
|
|
764
841
|
if (bytes < 1024)
|
|
@@ -788,6 +865,15 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
788
865
|
if (options.summary) {
|
|
789
866
|
const record = content && typeof content === "object" && !Array.isArray(content) ? content : undefined;
|
|
790
867
|
const keys = record ? Object.entries(record).map(([key, value]) => ({ key, bytes: Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8"), kind: Array.isArray(value) ? `array[${value.length}]` : value === null ? "null" : typeof value })).sort((left, right) => right.bytes - left.bytes) : [];
|
|
868
|
+
// r25 真机(2026-11-04):`--summary` 与裸 `--keys` 曾输出逐字相同(各 878 B),
|
|
869
|
+
// 但 fetch 提示写「--summary(再 --keys/--jq)」暗示递进——探针白跑一次。
|
|
870
|
+
// 现在 `--summary` 多给一眼"要花多少/会不会被截断",`--keys` 保持纯 key 表。
|
|
871
|
+
const shapeFacts = {
|
|
872
|
+
bytes: artifact.content.length,
|
|
873
|
+
...(record ? { topLevelKeys: keys.length } : {}),
|
|
874
|
+
overInlineCeiling: artifact.content.length > ARTIFACT_INLINE_CEILING_BYTES,
|
|
875
|
+
...(record ? {} : { kind: Array.isArray(content) ? `array[${content.length}]` : typeof content }),
|
|
876
|
+
};
|
|
791
877
|
// `--summary --jq <path>`: shape AND the one value, in a single call. Without
|
|
792
878
|
// this the caller had to spend one round on the shape and another on the
|
|
793
879
|
// path (or pull the whole body); r5 hit that on 67 KB workflow templates.
|
|
@@ -797,7 +883,7 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
797
883
|
// 一层 `projection:{path,content}`——`artifact get --jq` 与 `--summary --jq`
|
|
798
884
|
// 因此同形(顶层 path/content),换读面不用重新探根键。
|
|
799
885
|
return emitTransportResult(projected.missed
|
|
800
|
-
? { 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:
|
|
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) }
|
|
801
887
|
: {
|
|
802
888
|
status: "ok",
|
|
803
889
|
artifactId: id,
|
|
@@ -809,10 +895,12 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
809
895
|
...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}),
|
|
810
896
|
...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}),
|
|
811
897
|
...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}),
|
|
812
|
-
...(
|
|
898
|
+
...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}),
|
|
813
899
|
}, transport);
|
|
814
900
|
}
|
|
815
|
-
|
|
901
|
+
// 裸 `--keys` / 裸 `--jq` 只要键表(`pureKeys`),不带 summary 的形状事实——
|
|
902
|
+
// 两者要有可见差异,否则 fetch 提示里的「--summary(再 --keys)」就是误导。
|
|
903
|
+
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, ...(options.pureKeys === true ? {} : { summary: shapeFacts }), keys, fetch: artifactFetchHint(id) }, transport);
|
|
816
904
|
}
|
|
817
905
|
if (options.keys !== undefined) {
|
|
818
906
|
// Round 15: tolerate a `result.`/`data.` prefix and report missing keys.
|
|
@@ -847,14 +935,14 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
847
935
|
const projected = projectArtifactPath(artifactContent, path);
|
|
848
936
|
return projected.missed
|
|
849
937
|
? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
|
|
850
|
-
: { 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 } : {}), ...(
|
|
938
|
+
: { 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) } : {}) };
|
|
851
939
|
});
|
|
852
|
-
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint:
|
|
940
|
+
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint: partialPathMissHint() } : {}) }, transport);
|
|
853
941
|
}
|
|
854
942
|
const projected = projectArtifactPath(artifactContent, options.jq);
|
|
855
943
|
return emitTransportResult(projected.missed
|
|
856
|
-
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint:
|
|
857
|
-
: { 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 } : {}), ...(
|
|
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);
|
|
858
946
|
}
|
|
859
947
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
860
948
|
// large ones (up to 55 KB) pulled whole workflow templates just to read one
|
|
@@ -1118,11 +1206,15 @@ function cmdDiscovery(kind, transport, value, options = {}) {
|
|
|
1118
1206
|
.map((tool) => tool.name)
|
|
1119
1207
|
.filter((name) => name === segment || name.startsWith(`${segment}.`) || name.includes(requested))
|
|
1120
1208
|
.slice(0, 8);
|
|
1121
|
-
|
|
1209
|
+
// F7:前缀候选之外,按 description/scope 意图词补 1–2 个(排在原候选之后)。
|
|
1210
|
+
const byText = toolsByTextIntent(requested, registry, 2).filter((name) => !candidates.includes(name));
|
|
1211
|
+
const shown = [...candidates, ...byText];
|
|
1212
|
+
const pointer = candidates.length === 0 && byText.length === 0 && (kind === "describe" || kind === "contract")
|
|
1122
1213
|
? `;describe 的目标是工具名(如 rule.save);图/模板/节点形状用 describe <tool> --full 或 contract <lane>,工具清单用 omc next "<目标>" / omc commands --json --compact`
|
|
1123
1214
|
: "";
|
|
1124
1215
|
const absence = absencePointer(requested);
|
|
1125
|
-
|
|
1216
|
+
const textNote = byText.length > 0 ? `;didYouMeanByText: ${byText.join("、")}` : "";
|
|
1217
|
+
return emitInvalid(transport, { message: `${kind} target not found: "${requested}"${shown.length > 0 ? `; did you mean: ${shown.join("、")}` : ""}${textNote}${absence}${approvalPointer}${intentPointer}${formPointer}${pointer}` });
|
|
1126
1218
|
}
|
|
1127
1219
|
return emitTransportResult({ status: "ok", ...result }, transport);
|
|
1128
1220
|
}
|
|
@@ -1142,8 +1234,8 @@ function cmdNext(goal, transport) {
|
|
|
1142
1234
|
* capability overview. Read-only, zero environment contact; a projection of
|
|
1143
1235
|
* the tool registry, the coverage manifest, and the intent capability registry.
|
|
1144
1236
|
*/
|
|
1145
|
-
function cmdCapabilities(transport) {
|
|
1146
|
-
const result = compactCapabilities(buildToolRegistry());
|
|
1237
|
+
function cmdCapabilities(transport, compact = false) {
|
|
1238
|
+
const result = compactCapabilities(buildToolRegistry(), { compact });
|
|
1147
1239
|
return emitTransportResult({ status: "ok", ...result }, transport);
|
|
1148
1240
|
}
|
|
1149
1241
|
function cmdCapability(id, transport, executable = false) {
|
|
@@ -1716,7 +1808,20 @@ async function main() {
|
|
|
1716
1808
|
}
|
|
1717
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)); });
|
|
1718
1810
|
command.command("setup").option("--list").option("--edit <environment>").option("--remove <environment>").action(async (options) => {
|
|
1719
|
-
|
|
1811
|
+
// r25 真机(2026-11-04):setup 不在 ownsStdout 里,于是非 TTY 拒绝时主流程会补一个
|
|
1812
|
+
// `error.code=internal` 的信封——明明是可预期的用法错误(stdin 不是终端),却被包成
|
|
1813
|
+
// 「CLI 内部故障」(探针原话:「差点以为是崩溃;看了 stderr 才确认是故意的策略拒绝」)。
|
|
1814
|
+
// 现由 setup 自己写正确档位的信封,并加入 ownsStdout 让主流程不再覆盖。
|
|
1815
|
+
const setupCode = await cmdSetup(new ConfigStore(defaultPaths()), [...(options.list ? ["--list"] : []), ...(options.edit ? ["--edit", options.edit] : []), ...(options.remove ? ["--remove", options.remove] : [])]);
|
|
1816
|
+
commandExitCode = setupCode;
|
|
1817
|
+
writeEnvelope(setupCode === 0
|
|
1818
|
+
? envelope("ok", {}, request, null)
|
|
1819
|
+
: envelope("error", { blocker: "setup-requires-tty" }, request, {
|
|
1820
|
+
code: "invalid-input",
|
|
1821
|
+
message: "setup 的交互式创建/编辑需要 TTY(stdin 不是终端)。改用 --list / --remove,或在真实终端里运行 --edit;脚本化注册直接改配置文件。",
|
|
1822
|
+
retryable: false,
|
|
1823
|
+
nextCommand: "omc setup --list",
|
|
1824
|
+
}));
|
|
1720
1825
|
});
|
|
1721
1826
|
command.command("env [action] [name]").option("--json").action(async (action, name, options) => {
|
|
1722
1827
|
// Read-only inventory of configured environment names. r18: the sandbox
|
|
@@ -1774,11 +1879,11 @@ async function main() {
|
|
|
1774
1879
|
// landed in the argParser slot and silently dropped every --steps value
|
|
1775
1880
|
// (live 2026-10-18: `--steps "审批@role:x"` answered "至少一个 --steps").
|
|
1776
1881
|
const collectValues = (value, previous = []) => [...previous, value];
|
|
1777
|
-
command.command("rule-when").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--on <trigger>").addOption(new Option("--when <condition>", "条件,可重复").argParser(collectValues).default([])).addOption(new Option("--then <action>", "动作,可重复").argParser(collectValues).default([])).option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n条件-动作规则拼装器:--when 条件(可多个 AND)→ --then set:字段=值/引用/公式。编译为设计器验证形状(预置触发+UPDATE filter/赋值)。默认 dry-run,--apply 保存并发布;前台生效按输出清单人工验证。\n").action(async (options) => {
|
|
1882
|
+
command.command("rule-when").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--on <trigger>", "触发时机:create | update(**没有 submit**——提交后执行请用 create 或 update,按平台触发面语义选)").addOption(new Option("--when <condition>", "条件,可重复").argParser(collectValues).default([])).addOption(new Option("--then <action>", "动作,可重复").argParser(collectValues).default([])).option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n条件-动作规则拼装器:--when 条件(可多个 AND)→ --then set:字段=值/引用/公式。编译为设计器验证形状(预置触发+UPDATE filter/赋值)。默认 dry-run,--apply 保存并发布;前台生效按输出清单人工验证。\n触发时机只有 create / update 两个(无 submit):\"提交后/审批前\"这类需求在平台侧没有独立触发点,用 create(新建即触发)或 update(更新即触发)表达;要按状态过滤,把条件写进 --when。\n").action(async (options) => {
|
|
1778
1883
|
const rest = ["rule-when", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(options.on ? ["--on", options.on] : []), ...(Array.isArray(options.when) ? options.when.flatMap((w) => ["--when", w]) : []), ...(Array.isArray(options.then) ? options.then.flatMap((a) => ["--then", a]) : []), ...(options.name ? ["--name", options.name] : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : [])];
|
|
1779
1884
|
commandExitCode = await cmdRuleWhen(rest);
|
|
1780
1885
|
});
|
|
1781
|
-
command.command("approval-flow").option("--schema <schemaCode>").option("--schema-code <schemaCode>").addOption(new Option("--steps <step>", "审批步骤,可重复").argParser(collectValues).default([])).option("--env <environment>").option("--environment <environment>").option("--apply").option("--no-originator-node").option("--no-reject-to-start").addHelpText("after", "\n审批流拼装器:--steps 名称@role:<角色code>(多级给多个)。编译为验证模板(START→填写→审批链→END,审批节点字段只读),--apply 建流+权限+发布。\n").action(async (options) => {
|
|
1886
|
+
command.command("approval-flow").option("--schema <schemaCode>").option("--schema-code <schemaCode>").addOption(new Option("--steps <step>", "审批步骤,可重复").argParser(collectValues).default([])).option("--env <environment>").option("--environment <environment>").option("--apply").option("--no-originator-node").option("--no-reject-to-start").addHelpText("after", "\n审批流拼装器:--steps 名称@role:<角色code>(多级给多个)。编译为验证模板(START→填写→审批链→END,审批节点字段只读),--apply 建流+权限+发布。\n边界:非发起人身份的审批无法在单会话 CLI 内推进(单 token);若要验跨账号审批,用第二账号另建环境,或把参与人设为 {Originator}。\n").action(async (options) => {
|
|
1782
1887
|
const rest = ["approval-flow", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(Array.isArray(options.steps) ? options.steps.flatMap((s) => ["--steps", s]) : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : []), ...(options.originatorNode === false ? ["--no-originator-node"] : []), ...(options.rejectToStart === false ? ["--no-reject-to-start"] : [])];
|
|
1783
1888
|
commandExitCode = await cmdApprovalFlow(rest);
|
|
1784
1889
|
});
|
|
@@ -2047,13 +2152,19 @@ async function main() {
|
|
|
2047
2152
|
console.error(`已清理 ${result.removed} 个读回产物,释放 ${formatBytes(result.freedBytes)};保留 ${result.kept} 个(${formatBytes(result.keptBytes)})`);
|
|
2048
2153
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2049
2154
|
});
|
|
2050
|
-
|
|
2155
|
+
// r28h F3(真机 2026-11-05):`omc call` 用 `--extract`、`artifact get` 用 `--jq`,
|
|
2156
|
+
// 语义完全相同。探针先试 `--extract` 拿到 `unknown option`(白花一次往返),
|
|
2157
|
+
// 才想起两者不同名。别名照收,help 里也标出来。
|
|
2158
|
+
artifactCommand.command("get <id>").option("--jq [path]").option("--extract <path>", "同 --jq(与 omc call 的 --extract 同名,省一次命名切换)").option("--path-only").option("--summary").option("--keys [a,b]").option("--full", "整包返回正文(默认:正文超过 8KB 时只回形状 outline + fetch,按需 --jq/--keys 定向取值)").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
2051
2159
|
// A bare `--jq`/`--keys` is a key-inventory request, not a missing argument:
|
|
2052
2160
|
// commander's "argument missing" (invalid-input + retry) is a dead end the
|
|
2053
2161
|
// caller cannot fix without already knowing the flag takes a value (r11
|
|
2054
2162
|
// burned six rounds on `artifact get <id> --jq` before trying `--summary`).
|
|
2163
|
+
// --extract 是 --jq 的同义别名(r28h F3)。
|
|
2164
|
+
const jqPath = typeof options.jq === "string" ? options.jq : typeof options.extract === "string" ? options.extract : undefined;
|
|
2055
2165
|
const bareJq = options.jq === true;
|
|
2056
|
-
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(
|
|
2166
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(jqPath !== undefined ? { jq: jqPath } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true || bareJq,
|
|
2167
|
+
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2057
2168
|
});
|
|
2058
2169
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
2059
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 } : {}) })); });
|
|
@@ -2061,7 +2172,7 @@ async function main() {
|
|
|
2061
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 } : {}) }));
|
|
2062
2173
|
});
|
|
2063
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 } : {}) })); });
|
|
2064
|
-
command.command("capabilities").option("--max-output-bytes <bytes>").action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdCapabilities(transport)); });
|
|
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)); });
|
|
2065
2176
|
// `--env` is accepted but ignored: `next` is a zero-environment-contact route
|
|
2066
2177
|
// solver, and a caller who reflexively passes `--env` should not be refused
|
|
2067
2178
|
// (round 15 dogfood: `next "<goal>" --env local` → unknown option, one round).
|
|
@@ -2165,17 +2276,14 @@ function finish(code) {
|
|
|
2165
2276
|
}
|
|
2166
2277
|
const request = requestId();
|
|
2167
2278
|
main().then((code) => {
|
|
2168
|
-
// Commands that own their stdout never grow a trailing envelope.
|
|
2169
|
-
//
|
|
2170
|
-
//
|
|
2171
|
-
//
|
|
2172
|
-
//
|
|
2173
|
-
//
|
|
2174
|
-
//
|
|
2175
|
-
|
|
2176
|
-
// lines, violating the one-envelope stdout contract).
|
|
2177
|
-
const ownsStdout = ["call", "script", "artifact", "commands", "describe", "contract", "capabilities", "capability", "next", "usage", "intent", "help", "form-default", "compute", "rule-when", "approval-flow", "model-scaffold", "field-add", "model-purge", "rollback", "env", "graph", "programming", "--version", "-V"];
|
|
2178
|
-
if (ownsStdout.includes(argv[2] ?? "")) {
|
|
2279
|
+
// Commands that own their stdout never grow a trailing envelope. The list is
|
|
2280
|
+
// an explicit declaration in `cli-stdout.ts` (HANDOFF §三.2-6): an inline array
|
|
2281
|
+
// was maintained by hand and twice lost a command — composers in r10 and
|
|
2282
|
+
// `setup` in 0.6.7 both answered two envelopes, and the compound writes
|
|
2283
|
+
// (`submit-verify` / `rule-save-enable` / `workflow-update-publish`) still did
|
|
2284
|
+
// on a bad JSON flag. `test/cli-stdout-contract.test.ts` walks every registered
|
|
2285
|
+
// root command so a new one cannot be forgotten.
|
|
2286
|
+
if (ownsStdout(argv[2] ?? "")) {
|
|
2179
2287
|
completeUsage(code);
|
|
2180
2288
|
finish(code);
|
|
2181
2289
|
return;
|
|
@@ -60,7 +60,14 @@ export function checkedOutputBudget(value) {
|
|
|
60
60
|
return value;
|
|
61
61
|
}
|
|
62
62
|
export function artifactRoot() {
|
|
63
|
-
|
|
63
|
+
// r28k F11(真机 2026-11-05):旧默认落在项目根 `artifacts/`,`describe --full`
|
|
64
|
+
// 等 transport spill 与「全程只读」探针预期的「零残留」冲突(账本 createdCodes
|
|
65
|
+
// 仍为空,但磁盘多了文件)。默认与工具产物目录对齐到 `.omc/artifacts`。
|
|
66
|
+
// OMC_ARTIFACTS_HOME 若设置,则它本身就是最终 artifact 根(历史契约,不再拼一层)。
|
|
67
|
+
const home = process.env.OMC_ARTIFACTS_HOME;
|
|
68
|
+
return home !== undefined && home.trim() !== ""
|
|
69
|
+
? resolve(home)
|
|
70
|
+
: resolve(joinConfigRoot(), ".omc", "artifacts");
|
|
64
71
|
}
|
|
65
72
|
export function joinConfigRoot() {
|
|
66
73
|
return resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
|
package/dist/config-store.js
CHANGED
|
@@ -277,9 +277,24 @@ export class ConfigStore {
|
|
|
277
277
|
console.error(`cross-env: 环境 "${name}" 不在本项目允许列表(${own.join("、") || "空"});已按 OMC_ALLOW_CROSS_ENV=1 放行本次访问。请确认这是用户批准的临时跨环境操作。`);
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
|
+
// r28j F8: cwd decides the project root. Agents often assemble JSON under
|
|
281
|
+
// /tmp then call --env local, and the refusal used to say only "允许:test"
|
|
282
|
+
// while `omc env` in another directory showed local — two sources, one
|
|
283
|
+
// unexplained. Spell both files + the resolved project root in the refusal.
|
|
284
|
+
const projectRoot = dirname(this.paths.projectEnvPath);
|
|
285
|
+
const bound = this.readProjectEnvBinding();
|
|
286
|
+
const allowlist = this.readProjectEnvsFile();
|
|
287
|
+
const boundText = bound === undefined
|
|
288
|
+
? `.omc-env=(无;未绑定)`
|
|
289
|
+
: `.omc-env=${bound}`;
|
|
290
|
+
const allowText = allowlist.length === 0
|
|
291
|
+
? `.omc-envs=(无或不存在;准入回退到 .omc-env ∪ 全局 default)`
|
|
292
|
+
: `.omc-envs=${allowlist.join(",")}`;
|
|
280
293
|
throw new ConfigurationError(`跨环境访问被拒绝:环境 "${name}" 不属于本项目(本项目允许:${own.join("、") || "(none)"})。` +
|
|
281
|
-
|
|
282
|
-
|
|
294
|
+
`解析到的项目根=${projectRoot};${boundText};${allowText}。` +
|
|
295
|
+
`准入由「当前工作目录」下的 .omc-envs(白名单)+ .omc-env(默认绑定)共同决定,不是由 --env 参数单独决定;` +
|
|
296
|
+
`在无声明的目录里 --env local 会落到机器全局 default(常见是 test)。` +
|
|
297
|
+
`请改到声明了该环境的项目根再跑,或先向用户报告并取得同意后执行 \`omc env allow ${name}\`(写入项目 .omc-envs),` +
|
|
283
298
|
`或临时设置 OMC_ALLOW_CROSS_ENV=1 单次放行。`);
|
|
284
299
|
}
|
|
285
300
|
bindProjectEnvironment(name) {
|
package/dist/discovery.js
CHANGED
|
@@ -7,6 +7,7 @@ import { LANE_CONTRACTS } from "./contract-resources.js";
|
|
|
7
7
|
import { intentCapabilityBlock } from "./intent/cli.js";
|
|
8
8
|
import { listSkills } from "./skills.js";
|
|
9
9
|
import { capabilityRouteForTool, capabilityRouteForLane, capabilityRouteIndex, DISCOVERY_AUTHORITY } from "./capability-overview.js";
|
|
10
|
+
import { readShapeFor } from "./read-shapes.js";
|
|
10
11
|
/**
|
|
11
12
|
* `commands --json` — the tool catalogue.
|
|
12
13
|
*
|
|
@@ -169,7 +170,11 @@ function jsonSchemaFor(shape) {
|
|
|
169
170
|
/** First sentence, whitespace-collapsed and bounded — enough to pick a tool. */
|
|
170
171
|
function summarizeDescription(description) {
|
|
171
172
|
const collapsed = description.replace(/\s+/g, " ").trim();
|
|
172
|
-
|
|
173
|
+
// r28k F8(真机 2026-11-05):此前把**任何**点号都当句末,于是
|
|
174
|
+
// `setup.commit` 的摘要被切在 "写 omc." ——恰好切掉了「写 omc.config.json 与
|
|
175
|
+
// 0600 凭据库」这个决定「要不要执行」的核心事实(探针必须再 --full 才拿到)。
|
|
176
|
+
// 点号只有当后面是空白或结尾时才是句末;`omc.config.json` / `v1.2.3` 里的不属于。
|
|
177
|
+
const firstStop = collapsed.search(/[。;;!!??]|\.(?=\s|$)/);
|
|
173
178
|
const sentence = firstStop >= 0 ? collapsed.slice(0, firstStop + 1) : collapsed;
|
|
174
179
|
return sentence.length > 200 ? `${sentence.slice(0, 197)}...` : sentence;
|
|
175
180
|
}
|
|
@@ -422,6 +427,23 @@ function firstCallFor(tool) {
|
|
|
422
427
|
};
|
|
423
428
|
return firstCall;
|
|
424
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Compact `returns` block for the default describe / --param faces.
|
|
432
|
+
* Only tools with an authored entry in read-shapes.ts emit this field —
|
|
433
|
+
* undeclared tools stay at zero extra bytes. Keep notes to at most one line.
|
|
434
|
+
*/
|
|
435
|
+
function returnsFace(toolName) {
|
|
436
|
+
const shape = readShapeFor(toolName);
|
|
437
|
+
if (!shape)
|
|
438
|
+
return undefined;
|
|
439
|
+
const notes = shape.notes?.slice(0, 1);
|
|
440
|
+
return {
|
|
441
|
+
returns: {
|
|
442
|
+
keys: shape.keys,
|
|
443
|
+
...(notes && notes.length > 0 ? { notes } : {}),
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
425
447
|
/** Default `omc describe`: the compact contract projection. */
|
|
426
448
|
export function describeTool(registry, name) {
|
|
427
449
|
const tool = registry.find((entry) => entry.name === name);
|
|
@@ -451,12 +473,18 @@ export function describeTool(registry, name) {
|
|
|
451
473
|
// reachable through --param/--full, i.e. discovering a param needed its
|
|
452
474
|
// name first. Name the documented params inline (cheap: names only) so one
|
|
453
475
|
// `describe` answers "does this tool take X, and what is X called".
|
|
476
|
+
// r26b 读回体积审计(2026-11-05):提示原写「单个参数注释,几百字节」,而
|
|
477
|
+
// `--param nodeGraph` 实测 **8773 B**(object 参数展开≈整份 --full)——照提示
|
|
478
|
+
// 做反而更贵。现在点名哪些参数是大对象,并改指 --example。
|
|
454
479
|
...(documentedParamNames(tool).length > 0
|
|
455
|
-
? { paramDocs: { available: documentedParamNames(tool), read:
|
|
480
|
+
? { paramDocs: { available: documentedParamNames(tool), read: largeParamDocsHint(tool) } }
|
|
456
481
|
: {}),
|
|
457
482
|
// T3(GitLab #17):封闭取值(schema 里没有 enum 的那些)在默认面内联一次,
|
|
458
483
|
// 省掉「写错 → 报错 → 再写」的循环;报错里的 accepted* 仍是权威兜底。
|
|
459
484
|
...(tool.paramEnums !== undefined ? { paramEnums: tool.paramEnums } : {}),
|
|
485
|
+
// High-frequency read faces: declare the RESULT keys so `--jq` does not
|
|
486
|
+
// have to learn them from a pathMiss. Zero bytes when undeclared.
|
|
487
|
+
...(returnsFace(tool.name) ?? {}),
|
|
460
488
|
// Keep the cost salient at the decision point: agents reached for --full even
|
|
461
489
|
// when --example/--param existed (sandbox22: 8 --full calls, one 19 KB).
|
|
462
490
|
full: `omc describe ${tool.name} --full(5–20KB;先试 --example/--param)`,
|
|
@@ -473,6 +501,24 @@ function documentedParamNames(tool) {
|
|
|
473
501
|
}
|
|
474
502
|
return names.sort();
|
|
475
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* `--param` 的价码提示(r26b 实测):标量参数 150–900 B,**object 参数 ≈ 整份
|
|
506
|
+
* `--full`**(`--param nodeGraph` = 8773 B,`--full` = 16950 B)。原来的提示只写
|
|
507
|
+
* 「几百字节」,照着做会付 10–50 倍。这里点名当前工具里哪些参数是大对象。
|
|
508
|
+
*/
|
|
509
|
+
function largeParamDocsHint(tool) {
|
|
510
|
+
const schema = jsonSchemaFor((tool.inputSchema ?? {}));
|
|
511
|
+
// 只算 **object**(record)参数:字符串数组(displayFields/columns)展开只要几十字节,
|
|
512
|
+
// 把它们也标成"大"会误导(r26b 实测 data.list --param displayFields 很小)。
|
|
513
|
+
const heavy = Object.entries(schema.properties ?? {})
|
|
514
|
+
.filter(([, node]) => node.type === "object")
|
|
515
|
+
.map(([key]) => key);
|
|
516
|
+
const base = `omc describe ${tool.name} --param <name>(标量 150–900B`;
|
|
517
|
+
if (heavy.length === 0)
|
|
518
|
+
return `${base})`;
|
|
519
|
+
const named = heavy.slice(0, 3).join("、") + (heavy.length > 3 ? " 等" : "");
|
|
520
|
+
return `${base};对象参数 ${named} ≈ --full,改用 --example)`;
|
|
521
|
+
}
|
|
476
522
|
/** `omc describe <tool> --example <id>`: one recipe in full, by id. */
|
|
477
523
|
export function describeExample(registry, name, exampleId) {
|
|
478
524
|
const tool = registry.find((entry) => entry.name === name);
|
|
@@ -535,7 +581,18 @@ export function describeParam(registry, name, param) {
|
|
|
535
581
|
const schema = jsonSchemaFor(tool.inputSchema);
|
|
536
582
|
const node = schema.properties?.[param];
|
|
537
583
|
if (!node) {
|
|
538
|
-
|
|
584
|
+
// r28e 真机(2026-11-05):调用方看到 `status:"ok"` + 一堆键,误以为"参数存在、
|
|
585
|
+
// 只是描述为空"。`found:false` 是机器信号,但人读时需要一句话点明——否则
|
|
586
|
+
// 会照着 `available` 之外的名字继续试。
|
|
587
|
+
const nearest = closestMatch(param, Object.keys(schema.properties ?? {}));
|
|
588
|
+
return {
|
|
589
|
+
name: tool.name,
|
|
590
|
+
param,
|
|
591
|
+
found: false,
|
|
592
|
+
message: `本工具没有参数 "${param}"${nearest !== undefined ? `;最接近的是 "${nearest}"` : ""}——available 是本工具的全部合法参数`,
|
|
593
|
+
...(nearest !== undefined ? { didYouMean: nearest, nextCommand: `omc describe ${tool.name} --param ${nearest}` } : {}),
|
|
594
|
+
available: Object.keys(schema.properties ?? {}),
|
|
595
|
+
};
|
|
539
596
|
}
|
|
540
597
|
return {
|
|
541
598
|
name: tool.name,
|
|
@@ -544,6 +601,8 @@ export function describeParam(registry, name, param) {
|
|
|
544
601
|
type: terseType(node),
|
|
545
602
|
...(node.description ? { description: node.description } : {}),
|
|
546
603
|
schema: node,
|
|
604
|
+
// Same authored return face as the default describe (only when declared).
|
|
605
|
+
...(returnsFace(tool.name) ?? {}),
|
|
547
606
|
};
|
|
548
607
|
}
|
|
549
608
|
/**
|