@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/README.md
CHANGED
|
@@ -52,7 +52,8 @@ omc setup && omc doctor && omc commands --json
|
|
|
52
52
|
`route`、`readiness`、`distributedMirror`)。
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
omc capabilities # 能力
|
|
55
|
+
omc capabilities # 能力 id/lane/aliases/tools/依赖顺序 一张表
|
|
56
|
+
omc capabilities --compact # 只要能力索引(~2.5KB,工具名按需 omc capability <id>)
|
|
56
57
|
omc capability <id-or-alias> --executable # 选中能力的完整执行包(离线,不接触环境)
|
|
57
58
|
omc contract <lane-from-the-capability> # wire 契约 + 该能力路由
|
|
58
59
|
omc describe <tool-from-the-bundle> # schema + recipe + 能力路由
|
package/dist/approval-flow.js
CHANGED
|
@@ -88,8 +88,10 @@ export async function cmdApprovalFlow(args) {
|
|
|
88
88
|
catch (error) {
|
|
89
89
|
return invalid(error instanceof Error ? error.message : String(error));
|
|
90
90
|
}
|
|
91
|
+
// F1(r27):边界前置——成功回执就标出跨账号审批在单会话 CLI 内不可用。
|
|
92
|
+
const crossAccountApproval = "unsupported: single-session CLI";
|
|
91
93
|
if (!apply) {
|
|
92
|
-
writeEnvelope(envelope("ok", { schemaCode: schema, ...compiled, dryRun: true, note: "dry-run 仅编译预览;--apply 执行 workflow.create(无流程时)→update(模板+字段权限)→publish" }, request));
|
|
94
|
+
writeEnvelope(envelope("ok", { schemaCode: schema, ...compiled, dryRun: true, crossAccountApproval, note: "dry-run 仅编译预览;--apply 执行 workflow.create(无流程时)→update(模板+字段权限)→publish。边界:非发起人身份的审批无法在单会话 CLI 内推进(单 token);若要验跨账号审批,用第二账号另建环境,或把参与人设为 {Originator}" }, request));
|
|
93
95
|
return 0;
|
|
94
96
|
}
|
|
95
97
|
// 1) 确保流程存在(一个模型只能有一个:已存在则直接 update)
|
|
@@ -138,9 +140,9 @@ export async function cmdApprovalFlow(args) {
|
|
|
138
140
|
// 4) publish
|
|
139
141
|
const publishOutcome = await runToolCall("workflow.publish", { args: { schemaCode: schema, code: compiled.workflowCode, ...(environment ? { environment } : {}) }, requestId: request, environment, print: () => undefined });
|
|
140
142
|
const classified = classifyResult(publishOutcome.result);
|
|
141
|
-
writeEnvelope(envelope(classified.status, { schemaCode: schema, workflowCode: compiled.workflowCode, steps: steps.length, ...preexistingSection, publish: publishOutcome.result }, request, classified.error));
|
|
143
|
+
writeEnvelope(envelope(classified.status, { schemaCode: schema, workflowCode: compiled.workflowCode, steps: steps.length, crossAccountApproval, ...preexistingSection, publish: publishOutcome.result }, request, classified.error));
|
|
142
144
|
if (publishOutcome.exitCode === 0)
|
|
143
|
-
process.stderr.write("[omc] 前台生效验证清单:1) 造一条数据提交走流程;2) 每级审批人在门户有待办(角色用 FindUserByRole 需审批人挂在发起人同部门,否则 PENDING 不来);3) 审批节点字段只读(不可改单);4) 驳回退发起人核对;5)
|
|
145
|
+
process.stderr.write("[omc] 前台生效验证清单:1) 造一条数据提交走流程;2) 每级审批人在门户有待办(角色用 FindUserByRole 需审批人挂在发起人同部门,否则 PENDING 不来);3) 审批节点字段只读(不可改单);4) 驳回退发起人核对;5) 设计器打开流程确认节点链显示正常。边界:非发起人身份的审批无法在单会话 CLI 内推进(单 token);跨账号请另建环境或把参与人设为 {Originator}。\n");
|
|
144
146
|
return publishOutcome.exitCode;
|
|
145
147
|
}
|
|
146
148
|
//# sourceMappingURL=approval-flow.js.map
|
|
@@ -531,22 +531,38 @@ function resolveExternParameterType(parameter, label, index, collect) {
|
|
|
531
531
|
return hasChildren ? EXTERN_PARAMETER_NAMES.OBJECT : EXTERN_PARAMETER_NAMES.PROPERTY;
|
|
532
532
|
}
|
|
533
533
|
/**
|
|
534
|
-
* Read-side parameter-face normalization
|
|
535
|
-
*
|
|
536
|
-
* `
|
|
537
|
-
*
|
|
538
|
-
*
|
|
534
|
+
* Read-side parameter-face normalization.
|
|
535
|
+
*
|
|
536
|
+
* - `configJson.position` (RESTFUL only): wire INTEGER → canonical NAME
|
|
537
|
+
* (mirrors `normalizeMethodConfigReadback`). Non-RESTFUL faces leave position alone.
|
|
538
|
+
* - `bizPropertyType` (every adapter): wire INTEGER → canonical NAME, with the
|
|
539
|
+
* original index kept alongside as `bizPropertyTypeIndex` so callers can
|
|
540
|
+
* still match against platform int tables. A name already present passes
|
|
541
|
+
* through unchanged (idempotent).
|
|
542
|
+
*
|
|
543
|
+
* r28f F4: register readback used to echo bare wire ints while method.get
|
|
544
|
+
* decoded position-only — one decoder, one face.
|
|
539
545
|
*/
|
|
540
546
|
export function normalizeParameterFaceReadback(adapterCode, parameters) {
|
|
541
|
-
if (
|
|
547
|
+
if (!Array.isArray(parameters))
|
|
542
548
|
return parameters;
|
|
543
549
|
return parameters.map((parameter) => {
|
|
544
550
|
if (!isRecord(parameter))
|
|
545
551
|
return parameter;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
let next = parameter;
|
|
553
|
+
// bizPropertyType is adapter-agnostic (BizPropertyType.java).
|
|
554
|
+
const rawType = parameter.bizPropertyType;
|
|
555
|
+
if (typeof rawType === "number" && Number.isInteger(rawType) && rawType in BIZ_PROPERTY_TYPES) {
|
|
556
|
+
next = { ...next, bizPropertyType: BIZ_PROPERTY_TYPES[rawType], bizPropertyTypeIndex: rawType };
|
|
557
|
+
}
|
|
558
|
+
// position decode is RESTFUL-only (BizServiceLocationType).
|
|
559
|
+
if (adapterCode === "RESTFUL") {
|
|
560
|
+
const config = isRecord(next.configJson) ? next.configJson : typeof next.configJson === "string" ? safeParse(next.configJson) : undefined;
|
|
561
|
+
if (isRecord(config) && typeof config.position === "number" && Number.isInteger(config.position) && config.position in RESTFUL_PARAMETER_POSITIONS) {
|
|
562
|
+
next = { ...next, configJson: { ...config, position: RESTFUL_PARAMETER_POSITIONS[config.position] } };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return next;
|
|
550
566
|
});
|
|
551
567
|
}
|
|
552
568
|
/**
|
package/dist/call.js
CHANGED
|
@@ -55,6 +55,50 @@ export function readArgsFile(path, root = resolve(process.env.OMC_CONFIG_HOME ??
|
|
|
55
55
|
const fd = openSync(safePath, "r");
|
|
56
56
|
return parseJsonArgs(readBounded(fd, "--args-file"), "--args-file");
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* F7(r27):按工具 description/scope 做意图词检索。前缀/编辑距离在跨族
|
|
60
|
+
* 猜错时会空手(`app.tree` → 只给 `app.*`),而真实工具 `folder.tree` 的描述
|
|
61
|
+
* 里写着「应用树」。只补 1–2 个、且排在原候选之后,不改 didYouMean 类型。
|
|
62
|
+
*/
|
|
63
|
+
export function toolsByTextIntent(query, registry, limit = 2) {
|
|
64
|
+
const rawTokens = query
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.split(/[\s._\-/:]+/u)
|
|
67
|
+
.map((token) => token.trim())
|
|
68
|
+
.filter((token) => token.length >= 2);
|
|
69
|
+
if (rawTokens.length === 0)
|
|
70
|
+
return [];
|
|
71
|
+
const intentExpand = {
|
|
72
|
+
tree: ["tree", "树", "目录", "folder", "应用树"],
|
|
73
|
+
树: ["tree", "树", "目录", "folder", "应用树"],
|
|
74
|
+
目录: ["tree", "树", "目录", "folder", "应用树"],
|
|
75
|
+
folder: ["tree", "树", "目录", "folder", "应用树"],
|
|
76
|
+
};
|
|
77
|
+
const tokens = new Set();
|
|
78
|
+
for (const token of rawTokens) {
|
|
79
|
+
tokens.add(token);
|
|
80
|
+
for (const expanded of intentExpand[token] ?? [])
|
|
81
|
+
tokens.add(expanded);
|
|
82
|
+
}
|
|
83
|
+
const scored = [];
|
|
84
|
+
for (const tool of registry) {
|
|
85
|
+
const haystack = `${tool.name}\n${tool.description ?? ""}\n${tool.scope ?? ""}`.toLowerCase();
|
|
86
|
+
let score = 0;
|
|
87
|
+
for (const token of tokens)
|
|
88
|
+
if (haystack.includes(token))
|
|
89
|
+
score += token.length >= 3 ? 2 : 1;
|
|
90
|
+
// Require at least one query token (not only expanded synonyms) to avoid
|
|
91
|
+
// ranking every folder.* tool for an unrelated ask that happens to expand.
|
|
92
|
+
const hitRaw = rawTokens.some((token) => haystack.includes(token));
|
|
93
|
+
if (!hitRaw || score <= 0)
|
|
94
|
+
continue;
|
|
95
|
+
scored.push({ name: tool.name, score });
|
|
96
|
+
}
|
|
97
|
+
return scored
|
|
98
|
+
.sort((left, right) => right.score - left.score || left.name.localeCompare(right.name))
|
|
99
|
+
.slice(0, limit)
|
|
100
|
+
.map((entry) => entry.name);
|
|
101
|
+
}
|
|
58
102
|
export async function runToolCall(toolName, options = {}) {
|
|
59
103
|
const print = options.print ?? ((line) => console.log(line));
|
|
60
104
|
const context = options.context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
@@ -71,6 +115,8 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
71
115
|
// closestMatches 按名字相似度补 3 个跨族候选,并把下一条命令直接写出来。
|
|
72
116
|
const nearest = closestMatches(toolName, names, 3).map((entry) => entry.name).filter((name) => !candidates.includes(name));
|
|
73
117
|
const didYouMean = [...candidates.slice(0, 3), ...nearest].slice(0, 5);
|
|
118
|
+
// F7:description/scope 意图词补位(排在原候选之后,另字段不改 didYouMean 类型)。
|
|
119
|
+
const byText = toolsByTextIntent(toolName, registry, 2).filter((name) => !didYouMean.includes(name) && !candidates.includes(name));
|
|
74
120
|
// A requested operation the PLATFORM itself does not expose (e.g.
|
|
75
121
|
// dictionary.delete) must not read as an OMC gap: name the real recovery
|
|
76
122
|
// path and the documented absence instead of only the nearest tools.
|
|
@@ -80,6 +126,7 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
80
126
|
tool: toolName,
|
|
81
127
|
...(candidates.length > 0 ? { candidates } : {}),
|
|
82
128
|
...(didYouMean.length > 0 ? { didYouMean, nextCommand: `omc describe ${didYouMean[0]}` } : {}),
|
|
129
|
+
...(byText.length > 0 ? { didYouMeanByText: byText } : {}),
|
|
83
130
|
...(absence ? { platformAbsence: absence.replace(/^;/, "") } : {}),
|
|
84
131
|
// 跨族/表意不清时,「猜名字」本身就不该是路径——用目标路由一步到位。
|
|
85
132
|
goalCommand: `omc next "${toolName.replace(/[._]/g, " ")}"`,
|
|
@@ -28,23 +28,23 @@ const SURFACES = [
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
id: "app",
|
|
31
|
-
title: "
|
|
31
|
+
title: "应用包(app)",
|
|
32
32
|
lane: LANES.app,
|
|
33
|
-
description: "应用包(app
|
|
33
|
+
description: "应用包(app.*)生命周期:列出、创建、更新(分组移动)、admin 删除。不是应用内的目录节点——目录用 folder。",
|
|
34
34
|
operations: ["create", "update", "delete"],
|
|
35
35
|
requires: [],
|
|
36
36
|
intent: { level: "none", note: "未覆盖;用 omc call app.*。" },
|
|
37
|
-
notes: "环境已带默认 applicationCode
|
|
37
|
+
notes: "环境已带默认 applicationCode,建模不必先建应用。新建应用平台会自动塞一个默认分组(folder.isDefault);有模型/报表等非目录节点时 app.delete 拒 app-not-empty。",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
id: "folder",
|
|
41
|
-
title: "
|
|
41
|
+
title: "应用内目录节点(folder)",
|
|
42
42
|
lane: LANES.folder,
|
|
43
|
-
description: "
|
|
43
|
+
description: "应用内目录/分组节点(folder.*,不是应用包 app):读树(递归展平 path/depth)、建、改名/挂载、非递归删除。",
|
|
44
44
|
operations: ["tree", "create", "update", "delete"],
|
|
45
45
|
requires: ["app"],
|
|
46
46
|
intent: { level: "none", note: "未覆盖;用 omc call folder.*。移动模型用 model.move。" },
|
|
47
|
-
notes: "省略 parentId 建顶层目录(平台默认挂应用根);平台不校验重名(工具默认同父同名拒绝);删除非递归(有子节点回 DIRECTORY_NOT_EMPTY);应用树是嵌套 children,folder.tree
|
|
47
|
+
notes: "省略 parentId 建顶层目录(平台默认挂应用根);平台不校验重名(工具默认同父同名拒绝);删除非递归(有子节点回 DIRECTORY_NOT_EMPTY);应用树是嵌套 children,folder.tree 已展平。不存在的应用回 applicationExists:false(仍 ok+空树)。平台默认分组节点带 isDefault:true。",
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
id: "dictionary",
|
|
@@ -123,11 +123,11 @@ const SURFACES = [
|
|
|
123
123
|
id: "workflow",
|
|
124
124
|
title: "流程模板",
|
|
125
125
|
lane: LANES.workflow,
|
|
126
|
-
description: "流程模板生命周期:get_draft/get_published、create、update、publish、bindRule、delete、workitem.list
|
|
126
|
+
description: "流程模板生命周期:get_draft/get_published、create、update、publish、bindRule、delete、workitem.list。**含组织/部门/角色/权限范围(org.* 工具)**:部门树、角色组/角色、用户搜索、成员与管理范围(userScope)——流程参与人/可见性的候选来源。",
|
|
127
127
|
operations: ["create", "update", "publish", "bindRule", "delete"],
|
|
128
128
|
requires: ["model"],
|
|
129
129
|
intent: { level: "partial", note: "声明式支持 workflow.create/update/publish 与 bindRules(事件处理器规则绑定,merge/ensure-present);runtime workitem(提交/审批)仍走 escape hatch。" },
|
|
130
|
-
notes: "greenfield 首版;publish/bindRule 不得在 ValidateMessage[]
|
|
130
|
+
notes: "greenfield 首版;publish/bindRule 不得在 ValidateMessage[] 拒绝时报告成功。组织读写(org.*)挂在本能力下,别名 org/organization/organisation。",
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
id: "onlinejs",
|
|
@@ -190,7 +190,8 @@ const CAPABILITY_ALIASES = Object.freeze({
|
|
|
190
190
|
datarule: ["data-rules", "G1-datarule"],
|
|
191
191
|
businessrule: ["business-rules", "B1-businessrule"],
|
|
192
192
|
"rule-lifecycle": ["rule-lifecycles", "S25-rule-lifecycle"],
|
|
193
|
-
|
|
193
|
+
// r28h F1/F7: org.* lives under workflow; aliases let "我要管部门/组织" hit it.
|
|
194
|
+
workflow: ["workflows", "W7-S29-greenfield", "org", "organization", "organisation", "组织", "组织架构"],
|
|
194
195
|
onlinejs: ["online-js"],
|
|
195
196
|
programming: ["backend-online-coding", "programming-online-dev", "在线开发", "在线编码"],
|
|
196
197
|
bizservice: ["bizservices", "B2-registration"],
|
|
@@ -238,7 +239,13 @@ export const CAPABILITY_CUES = Object.freeze({
|
|
|
238
239
|
// rule is named by 业务规则/触发器/规则图, not by "automatic".
|
|
239
240
|
businessrule: ["业务规则", "触发器", "规则图", "校验", "事件", "按钮", "定时任务", "定时触发", "定时器", "每天", "每日", "周期执行", "调度", "SCHEDULER", "TIMING_TASK"],
|
|
240
241
|
"rule-lifecycle": ["启用规则", "停用规则", "删除规则", "规则启停", "触发规则", "执行规则", "跑规则", "按钮规则", "规则验收"],
|
|
241
|
-
|
|
242
|
+
// r28h F1/F7: narrow org cues so "管理组织:部门/角色/角色组/用户/权限范围"
|
|
243
|
+
// hits workflow (where org.* lives) instead of field→workflow. Keep them
|
|
244
|
+
// compound/longer than field's bare "部门"/"人员" so they outscore field.
|
|
245
|
+
// Also include bare "部门"/"组织" here so an org ask that only says 部门 still
|
|
246
|
+
// ties/beats field (alias "组织" already scores via CAPABILITY_ALIASES).
|
|
247
|
+
workflow: ["流程", "工作流", "审批", "节点", "待办", "流转", "参与人", "审批人", "角色", "部门负责人", "审批角色",
|
|
248
|
+
"组织架构", "组织管理", "管理组织", "角色组", "权限范围", "管理范围", "userScope", "部门树", "员工账号", "组织"],
|
|
242
249
|
onlinejs: ["在线js", "脚本", "联动", "onload", "onvalidate"],
|
|
243
250
|
programming: ["在线开发", "在线编码", "后端源码", "java源码", "编码模式", "handler", "programming", "源码编译"],
|
|
244
251
|
bizservice: ["集成", "接口", "服务", "回调", "对接", "外部系统", "datasql", "restful"],
|
|
@@ -522,33 +529,49 @@ export function executableCapabilityBundle(registry, id) {
|
|
|
522
529
|
* node keeps only `id`. The capability id/lane/tools/order/coverage truth is
|
|
523
530
|
* unchanged; only the repetition is gone.
|
|
524
531
|
*/
|
|
525
|
-
export function compactCapabilities(registry) {
|
|
532
|
+
export function compactCapabilities(registry, options = {}) {
|
|
526
533
|
const index = capabilityRouteIndex(registry);
|
|
534
|
+
// `--compact`(HANDOFF §三.3-7):默认面 ~8 KB,其中 `tools` 一项就占 ~3 KB。
|
|
535
|
+
// 冷启动只需要「有哪些能力、在哪个 lane、能不能声明式做」,工具名单用
|
|
536
|
+
// `omc capability <id>` 按需取。压缩档因此把 tools 换成 toolCount,并**明说**
|
|
537
|
+
// 省略了工具名,不让读者误以为该能力没有工具。
|
|
538
|
+
const compact = options.compact === true;
|
|
527
539
|
return {
|
|
528
540
|
status: "ok",
|
|
529
541
|
discovery: DISCOVERY_AUTHORITY,
|
|
530
542
|
// Size hint (dogfood R1): `capabilities` is ~6 KB, not a "safe small" face.
|
|
531
543
|
// Keep it a one-line pointer so it does not perturb the discovery-budget
|
|
532
544
|
// baseline more than a hair.
|
|
533
|
-
outputHint:
|
|
545
|
+
outputHint: compact
|
|
546
|
+
? "紧凑索引(无工具名单);工具名用 omc capability <id>,完整索引去掉 --compact"
|
|
547
|
+
: "~6KB 非安全小档;next 已给路线时可省,单能力用 omc capability <id>;只要能力索引可加 --compact",
|
|
534
548
|
commands: {
|
|
535
549
|
detail: "omc capability <id>",
|
|
536
550
|
executable: "omc capability <id> --executable",
|
|
537
551
|
contract: "omc contract <lane>",
|
|
538
552
|
readiness: DISCOVERY_AUTHORITY.readiness,
|
|
539
553
|
},
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
554
|
+
...(compact ? { toolsInlined: false, toolsHint: "每个能力的具体工具名见 omc capability <id>;完整索引用 omc capabilities(不带 --compact)" } : {}),
|
|
555
|
+
capabilities: index.routes.map((route) => compact
|
|
556
|
+
? {
|
|
557
|
+
id: route.id,
|
|
558
|
+
lane: route.lane,
|
|
559
|
+
intent: index.nodeById.get(route.id)?.intent.level ?? "none",
|
|
560
|
+
coverage: index.nodeById.get(route.id)?.coverage ?? "unregistered",
|
|
561
|
+
toolCount: route.tools.length,
|
|
562
|
+
}
|
|
563
|
+
: {
|
|
564
|
+
id: route.id,
|
|
565
|
+
title: route.title,
|
|
566
|
+
lane: route.lane,
|
|
567
|
+
aliases: route.aliases,
|
|
568
|
+
tools: route.tools,
|
|
569
|
+
relatedTopics: route.relatedTopics,
|
|
570
|
+
requires: index.nodeById.get(route.id)?.requires ?? [],
|
|
571
|
+
precedes: index.nodeById.get(route.id)?.precedes ?? [],
|
|
572
|
+
intent: index.nodeById.get(route.id)?.intent.level ?? "none",
|
|
573
|
+
coverage: index.nodeById.get(route.id)?.coverage ?? "unregistered",
|
|
574
|
+
}),
|
|
552
575
|
};
|
|
553
576
|
}
|
|
554
577
|
//# sourceMappingURL=capability-overview.js.map
|
package/dist/cli-contract.js
CHANGED
|
@@ -234,20 +234,43 @@ export function nextCommandFor(result) {
|
|
|
234
234
|
}
|
|
235
235
|
return undefined;
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Top-level envelope result statuses. Anything outside this set falls through
|
|
239
|
+
* to the unclassified-status / auth-config catch-all (exit 15/11). Historical
|
|
240
|
+
* accidents (0.6.6/0.6.7): `meta.get_status` → `"failed"`, `rollback apply` →
|
|
241
|
+
* `"stopped"` both used to land in `internal` before they were added here.
|
|
242
|
+
*/
|
|
243
|
+
export const RESULT_STATUSES = ["ok", "dry-run", "partial", "unknown-tool", "uncertain", "refused", "failed", "stopped", "blocked", "error"];
|
|
244
|
+
export const RESULT_STATUS_ROUTES = Object.freeze({
|
|
245
|
+
ok: { kind: "success", envelopeStatus: "ok", exitCode: CLI_EXIT_CODES.ok },
|
|
246
|
+
"dry-run": { kind: "success", envelopeStatus: "dry-run", exitCode: CLI_EXIT_CODES.ok },
|
|
247
|
+
partial: { kind: "partial", envelopeStatus: "partial", code: "partial-batch", exitCode: 1 },
|
|
248
|
+
"unknown-tool": { kind: "unknown-tool", envelopeStatus: "error", code: "invalid-input", exitCode: CLI_EXIT_CODES.invalidInput },
|
|
249
|
+
uncertain: { kind: "uncertain", envelopeStatus: "uncertain", code: "uncertain", exitCode: CLI_EXIT_CODES.uncertain },
|
|
250
|
+
refused: { kind: "refused", envelopeStatus: "refused" },
|
|
251
|
+
failed: { kind: "step-chain", envelopeStatus: "error", code: "step-chain-failed", exitCode: 1 },
|
|
252
|
+
stopped: { kind: "step-chain", envelopeStatus: "error", code: "step-chain-failed", exitCode: 1 },
|
|
253
|
+
blocked: { kind: "step-chain", envelopeStatus: "error", code: "step-chain-failed", exitCode: 1 },
|
|
254
|
+
// `error` keeps today's self-produced / blocker-bypass path: it is listed so
|
|
255
|
+
// the producer scan accepts it, but classifyResult does not give it a stable
|
|
256
|
+
// ordinary-path mapping of its own.
|
|
257
|
+
error: { kind: "passthrough" },
|
|
258
|
+
});
|
|
237
259
|
export function classifyResult(result) {
|
|
238
260
|
const status = String(result.status ?? "ok");
|
|
239
261
|
const withNext = (error) => {
|
|
240
262
|
const nextCommand = nextCommandFor(result);
|
|
241
263
|
return nextCommand === undefined ? error : { ...error, nextCommand };
|
|
242
264
|
};
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
const route = RESULT_STATUSES.includes(status)
|
|
266
|
+
? RESULT_STATUS_ROUTES[status]
|
|
267
|
+
: undefined;
|
|
268
|
+
if (route?.kind === "success")
|
|
269
|
+
return { status: route.envelopeStatus, error: null, exitCode: route.exitCode };
|
|
247
270
|
// `--args-list` partial success (some entries landed, some refused). Without
|
|
248
271
|
// this branch it fell through to "internal error", which reads as a CLI bug
|
|
249
272
|
// rather than "2 of 3 fields were created — retry index 2".
|
|
250
|
-
if (
|
|
273
|
+
if (route?.kind === "partial") {
|
|
251
274
|
const succeeded = Number(result.succeeded ?? 0);
|
|
252
275
|
const requested = Number(result.requested ?? 0);
|
|
253
276
|
const failed = Number(result.failed ?? 0);
|
|
@@ -262,15 +285,19 @@ export function classifyResult(result) {
|
|
|
262
285
|
? `${succeeded}/${requested} 成功,${failed} 失败;逐条原因见 failures[] / failedSteps[](--args-list 的 index 指向第几条)`
|
|
263
286
|
: `部分步骤失败:逐条原因见 steps[] 的 status/detail(已在失败处停下;幂等,修好后重跑同一条命令)`;
|
|
264
287
|
return {
|
|
265
|
-
status,
|
|
266
|
-
error: withNext({ code:
|
|
267
|
-
exitCode:
|
|
288
|
+
status: route.envelopeStatus,
|
|
289
|
+
error: withNext({ code: route.code, message, retryable: false }),
|
|
290
|
+
exitCode: route.exitCode,
|
|
268
291
|
};
|
|
269
292
|
}
|
|
270
|
-
if (
|
|
293
|
+
if (route?.kind === "unknown-tool") {
|
|
271
294
|
const candidates = Array.isArray(result.candidates) ? result.candidates : [];
|
|
272
295
|
const suffix = candidates.length > 0 ? `;did you mean: ${candidates.join("、")}` : ";用 `omc commands --json --compact --lane <lane>` 或 `omc next` 定位工具";
|
|
273
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
status: route.envelopeStatus,
|
|
298
|
+
error: withNext({ code: route.code, message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }),
|
|
299
|
+
exitCode: route.exitCode,
|
|
300
|
+
};
|
|
274
301
|
}
|
|
275
302
|
// r25 探针实证(2026-11-04):`blocker: "invalid-input"` 有 8 处产出点
|
|
276
303
|
// (workflow/runtime-data/compound-writes),但这里只认 `tool-input-*`——于是
|
|
@@ -278,16 +305,30 @@ export function classifyResult(result) {
|
|
|
278
305
|
// 读成"CLI 内部故障",而实际是**自己传错了参数**(探针原话:
|
|
279
306
|
// 「rejected ✓, but error.code=internal (should be invalid-input), and no nextCommand」)。
|
|
280
307
|
// 入参形状错误一律 exit 10,并给出下一步。
|
|
308
|
+
// Priority: must stay ahead of the remaining status routes (uncertain /
|
|
309
|
+
// refused / step-chain / unclassified) so `status:"refused"|"error"` with an
|
|
310
|
+
// input-shape blocker still exits 10. Do not move this above ok/dry-run/
|
|
311
|
+
// partial/unknown-tool — that would change today's semantics.
|
|
281
312
|
const inputShapeBlocker = result.blocker === "tool-input-invalid"
|
|
282
313
|
|| result.blocker === "tool-input-unknown-keys"
|
|
283
314
|
|| result.blocker === "invalid-input";
|
|
284
315
|
if (inputShapeBlocker) {
|
|
285
316
|
// `invalid-input` 也可能是 CLI 层参数校验(compound-writes 的 --schema 必填),
|
|
286
317
|
// 那时没有 tool 字段——别硬拼一条指向不存在的工具的 describe。
|
|
318
|
+
// 工具在时进一步收窄到**失败的那个参数**(`--param <顶层键>`):整份 describe
|
|
319
|
+
// 是 4–17 KB,而 issues[].path 已经把位置说清楚了(HANDOFF §三.1-1)。
|
|
287
320
|
const toolName = typeof result.tool === "string" && result.tool ? result.tool : undefined;
|
|
321
|
+
const failedParam = Array.isArray(result.issues)
|
|
322
|
+
? result.issues
|
|
323
|
+
.map((issue) => issue?.path)
|
|
324
|
+
.map((path) => (Array.isArray(path) ? path[0] : path))
|
|
325
|
+
.find((segment) => typeof segment === "string" && segment.length > 0)
|
|
326
|
+
: undefined;
|
|
288
327
|
const nextCommand = typeof result.nextCommand === "string"
|
|
289
328
|
? result.nextCommand
|
|
290
|
-
: toolName !== undefined
|
|
329
|
+
: toolName !== undefined
|
|
330
|
+
? `omc describe ${toolName}${failedParam !== undefined ? ` --param ${String(failedParam).split(".")[0]}` : ""}`
|
|
331
|
+
: undefined;
|
|
291
332
|
return {
|
|
292
333
|
status: "error",
|
|
293
334
|
error: {
|
|
@@ -297,22 +338,54 @@ export function classifyResult(result) {
|
|
|
297
338
|
exitCode: CLI_EXIT_CODES.invalidInput,
|
|
298
339
|
};
|
|
299
340
|
}
|
|
300
|
-
if (
|
|
301
|
-
return {
|
|
302
|
-
|
|
341
|
+
if (route?.kind === "uncertain") {
|
|
342
|
+
return {
|
|
343
|
+
status: route.envelopeStatus,
|
|
344
|
+
error: withNext({ code: route.code, message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }),
|
|
345
|
+
exitCode: route.exitCode,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (route?.kind === "refused") {
|
|
303
349
|
if (result.blocker === "write-lock-held")
|
|
304
|
-
return { status, error: withNext({ code: "policy-refused", message: String(result.message ?? "write lock held"), retryable: true }), exitCode: CLI_EXIT_CODES.policyRefused };
|
|
350
|
+
return { status: route.envelopeStatus, error: withNext({ code: "policy-refused", message: String(result.message ?? "write lock held"), retryable: true }), exitCode: CLI_EXIT_CODES.policyRefused };
|
|
305
351
|
const auth = String(result.blocker ?? "").startsWith("auth") || String(result.blocker ?? "").includes("credential");
|
|
306
352
|
// Every refusal gets a next step: a specific command when the tool knows
|
|
307
353
|
// one, otherwise the refusal router (`omc-refusals` explains which gate
|
|
308
354
|
// produced it — the documented path in omc-start-here).
|
|
309
355
|
const error = { code: auth ? "auth-config" : "policy-refused", message: String(result.message ?? result.blocker ?? "operation refused"), retryable: false };
|
|
310
356
|
const nextCommand = nextCommandFor(result) ?? (auth ? undefined : "omc help omc-refusals");
|
|
311
|
-
return { status, error: nextCommand === undefined ? error : { ...error, nextCommand }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
|
|
357
|
+
return { status: route.envelopeStatus, error: nextCommand === undefined ? error : { ...error, nextCommand }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
|
|
312
358
|
}
|
|
359
|
+
// r25 沙盒(2026-11-04,B1 的同类):`meta.get_status` 返回 `status:"failed"`、
|
|
360
|
+
// `rollback apply` 返回 `status:"stopped"`——两个都曾掉进 internal(exit 15):
|
|
361
|
+
// 调用方看到「CLI 内部故障」,其实是「配置/凭据读回失败」或「恢复链在 N 步停下」。
|
|
362
|
+
// 这类状态是**步骤链的中途状态**,语义就是 partial:先跑了一些、然后停在某处。
|
|
363
|
+
// 归到 exit 1 并保留 steps/结果原文。
|
|
364
|
+
if (route?.kind === "step-chain") {
|
|
365
|
+
return {
|
|
366
|
+
status: route.envelopeStatus,
|
|
367
|
+
error: withNext({
|
|
368
|
+
code: route.code,
|
|
369
|
+
message: String(result.message ?? `${status}:链在中途停止;逐条原因见 steps[] 的 status/error`),
|
|
370
|
+
retryable: false,
|
|
371
|
+
}),
|
|
372
|
+
exitCode: route.exitCode,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// `error` (passthrough) and any unknown status share the unclassified catch-all.
|
|
376
|
+
// Keep the actual `result.status` in the message (B10/B11): a brand-new value
|
|
377
|
+
// must never collapse to a bare "CLI 内部故障".
|
|
313
378
|
const message = String(result.message ?? "internal error");
|
|
314
379
|
const auth = /credential|configuration|environment|login|authentication|auth/i.test(message);
|
|
315
|
-
return {
|
|
380
|
+
return {
|
|
381
|
+
status: "error",
|
|
382
|
+
error: withNext({
|
|
383
|
+
code: auth ? "auth-config" : "unclassified-status",
|
|
384
|
+
message: `${message}(result.status=${status}${typeof result.blocker === "string" ? `, blocker=${result.blocker}` : ""}——此状态未在 envelope 分类表内,按失败处理并原样上报)`,
|
|
385
|
+
retryable: false,
|
|
386
|
+
}),
|
|
387
|
+
exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.internal,
|
|
388
|
+
};
|
|
316
389
|
}
|
|
317
390
|
export function errorEnvelope(error, code, exitCode, request) {
|
|
318
391
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/cli-output.js
CHANGED
|
@@ -2,6 +2,42 @@ import { writeFileSync } from "node:fs";
|
|
|
2
2
|
import { asciiStdout, serializeEnvelope } from "./cli-contract.js";
|
|
3
3
|
import { currentUsageCommand, priorRepeatedFailures, recordUsageOutput } from "./usage-log.js";
|
|
4
4
|
let options = { quiet: false, verbose: false };
|
|
5
|
+
/**
|
|
6
|
+
* F2'(r27):写面 + 非 TTY 时,stderr 人话末尾补一行「勿 2>&1 合流」提示。
|
|
7
|
+
* 读面保持安静;stdout 字节不变。状态由 beginWriteFace / noteHumanStderr /
|
|
8
|
+
* maybeWarnMergedStreams 协作,避免每个调用点手写条件。
|
|
9
|
+
*/
|
|
10
|
+
let writeFaceActive = false;
|
|
11
|
+
let humanStderrEmitted = false;
|
|
12
|
+
let pipeMergeHintEmitted = false;
|
|
13
|
+
/** Mark the current command as a write-face (call/script/composers that mutate). */
|
|
14
|
+
export function beginWriteFace() {
|
|
15
|
+
writeFaceActive = true;
|
|
16
|
+
humanStderrEmitted = false;
|
|
17
|
+
pipeMergeHintEmitted = false;
|
|
18
|
+
}
|
|
19
|
+
/** Clear the write-face marker (call after the command finishes). */
|
|
20
|
+
export function endWriteFace() {
|
|
21
|
+
writeFaceActive = false;
|
|
22
|
+
humanStderrEmitted = false;
|
|
23
|
+
}
|
|
24
|
+
const PIPE_MERGE_HINT = "提示:本行在 stderr;stdout 仍是**恰好一个** JSON 信封。脚本解析请勿 2>&1 合流(会得到两段文本)。";
|
|
25
|
+
/**
|
|
26
|
+
* Append the 2>&1 warning once, only when stdout is piped, a write face ran,
|
|
27
|
+
* and at least one human stderr line was already emitted.
|
|
28
|
+
*/
|
|
29
|
+
export function maybeWarnMergedStreams() {
|
|
30
|
+
if (pipeMergeHintEmitted || options.quiet)
|
|
31
|
+
return;
|
|
32
|
+
if (!writeFaceActive || !humanStderrEmitted)
|
|
33
|
+
return;
|
|
34
|
+
if (process.stdout.isTTY === true)
|
|
35
|
+
return;
|
|
36
|
+
pipeMergeHintEmitted = true;
|
|
37
|
+
process.stderr.write(`${PIPE_MERGE_HINT}\n`);
|
|
38
|
+
}
|
|
39
|
+
/** Test seam: the exact hint line (no trailing newline). */
|
|
40
|
+
export const PIPE_MERGE_HINT_FOR_TEST = PIPE_MERGE_HINT;
|
|
5
41
|
/** Parse `--quiet`, `--verbose` and `--out <file>` from the raw argv once, before dispatch. */
|
|
6
42
|
export function configureOutput(argv) {
|
|
7
43
|
const outIndex = argv.indexOf("--out");
|
|
@@ -177,7 +213,10 @@ export function writeEnvelope(value) {
|
|
|
177
213
|
}
|
|
178
214
|
/** Emit a human diagnostic on stderr, unless `--quiet` asked for none. */
|
|
179
215
|
export function humanSummary(line) {
|
|
180
|
-
if (
|
|
181
|
-
|
|
216
|
+
if (options.quiet)
|
|
217
|
+
return;
|
|
218
|
+
process.stderr.write(`${line}\n`);
|
|
219
|
+
humanStderrEmitted = true;
|
|
220
|
+
maybeWarnMergedStreams();
|
|
182
221
|
}
|
|
183
222
|
//# sourceMappingURL=cli-output.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which CLI roots write their own stdout envelope (HANDOFF §三.2-6).
|
|
3
|
+
*
|
|
4
|
+
* 这是**显式声明**,不是散落在 `main()` 里的字符串数组。历史上两次踩同一个坑:
|
|
5
|
+
* - r10:composers(`form-default` / `compute` / `rule-when` / `approval-flow`)
|
|
6
|
+
* 不在名单里 → 真信封后又跟一个 `{"ok":true,"result":{}}`,读成第二个答案。
|
|
7
|
+
* - 0.6.7:`setup` 非 TTY 报错不在名单里 → 补了一个谎报 `internal` 的信封(B9)。
|
|
8
|
+
* - 2026-11-05:`submit-verify` / `rule-save-enable` / `workflow-update-publish`
|
|
9
|
+
* 三个复合命令自己 `writeEnvelope`,却仍不在名单里 → 参数 JSON 写坏时
|
|
10
|
+
* stdout 出两行信封(实测 2 行),破坏「stdout 恰好一个信封」契约。
|
|
11
|
+
*
|
|
12
|
+
* 语义只有三档:
|
|
13
|
+
* - `envelope`:自己写**一个**结果信封(主流程不得再补)。
|
|
14
|
+
* - `raw`:自己写非信封输出(`env --json` 的 JSON、`--version` 的版本行),
|
|
15
|
+
* 同样不得再补。
|
|
16
|
+
* - `stderr-only`:只往 stderr 写人类诊断,成功/失败都不产生信封(`doctor`、
|
|
17
|
+
* `bench`)。主流程也不补,否则会给这类命令安一个假的 `ok`/`internal`。
|
|
18
|
+
*
|
|
19
|
+
* 新增一个根命令时必须在这里登记;`test/cli-stdout-contract.test.ts` 会遍历
|
|
20
|
+
* `cli.ts` 里注册的全部根命令,漏登记直接失败。
|
|
21
|
+
*/
|
|
22
|
+
export const CLI_STDOUT_OWNERS = {
|
|
23
|
+
// 工具执行与读面
|
|
24
|
+
call: "envelope",
|
|
25
|
+
script: "envelope",
|
|
26
|
+
artifact: "envelope",
|
|
27
|
+
commands: "envelope",
|
|
28
|
+
describe: "envelope",
|
|
29
|
+
contract: "envelope",
|
|
30
|
+
capabilities: "envelope",
|
|
31
|
+
capability: "envelope",
|
|
32
|
+
next: "envelope",
|
|
33
|
+
usage: "envelope",
|
|
34
|
+
intent: "envelope",
|
|
35
|
+
help: "envelope",
|
|
36
|
+
graph: "envelope",
|
|
37
|
+
programming: "envelope",
|
|
38
|
+
rollback: "envelope",
|
|
39
|
+
setup: "envelope",
|
|
40
|
+
// composers:自己合成结果信封
|
|
41
|
+
"form-default": "envelope",
|
|
42
|
+
compute: "envelope",
|
|
43
|
+
"rule-when": "envelope",
|
|
44
|
+
"approval-flow": "envelope",
|
|
45
|
+
"model-scaffold": "envelope",
|
|
46
|
+
"field-add": "envelope",
|
|
47
|
+
"model-purge": "envelope",
|
|
48
|
+
// 复合写命令(r19 tickets 11–13):两阶段回执是自己的信封
|
|
49
|
+
"submit-verify": "envelope",
|
|
50
|
+
"rule-save-enable": "envelope",
|
|
51
|
+
"workflow-update-publish": "envelope",
|
|
52
|
+
// 原始输出
|
|
53
|
+
env: "raw",
|
|
54
|
+
// MCP stdio 服务器:stdout 是 JSON-RPC 帧流,补一个信封会直接污染协议。
|
|
55
|
+
"mcp-graph": "raw",
|
|
56
|
+
"--version": "raw",
|
|
57
|
+
"-V": "raw",
|
|
58
|
+
// 人类体检 + 一个结构化回执(checks / unsupportedCapabilities 在 stdout)
|
|
59
|
+
doctor: "envelope",
|
|
60
|
+
// bench 只往 stderr 写报告,不产生信封
|
|
61
|
+
bench: "stderr-only",
|
|
62
|
+
};
|
|
63
|
+
/** 主流程绝不能在这些命令之后再补写一个信封。 */
|
|
64
|
+
export function ownsStdout(name) {
|
|
65
|
+
return Object.hasOwn(CLI_STDOUT_OWNERS, name);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 已登记的命令里,哪些**不**写信封——主流程连空的 `ok` 都不该补。
|
|
69
|
+
* (`ownsStdout` 已覆盖它们;这个集合只是为了把「为什么没有信封」写成代码。)
|
|
70
|
+
*/
|
|
71
|
+
export function isStderrOnlyCommand(name) {
|
|
72
|
+
return ownsStdout(name) && CLI_STDOUT_OWNERS[name] === "stderr-only";
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=cli-stdout.js.map
|