@stalfh233/omc-cli 0.4.87 → 0.4.90

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 CHANGED
@@ -130,10 +130,11 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
130
130
  - **intent**:`partial` 表示该能力可声明式(`omc intent`)覆盖一部分——通常 create/update;
131
131
  `none` 表示只走 `omc call`。删除类、方法面、分类/连接池等仍走 `omc call` 逃生舱。
132
132
 
133
- ## 近期能力(0.4.74 → 0.4.87)
133
+ ## 近期能力(0.4.74 → 0.4.88)
134
134
 
135
135
  | 版本 | 新增 / 修复 |
136
136
  |---|---|
137
+ | 0.4.88 | **`omc model-purge`**:一条命令按依赖倒序清掉一个模型(流程实例 → model.delete 级联带走流程/视图/字段/表单/规则)。这是 r12–r14 三次「脏沙盒幽灵故障」的根治——顺手修了它的死锁(视图最后一条不能删 306103 + 列表绑定流程 303024,所以不删流程、让模型级联)。另:ladder 的 **recipe 命令提到 firstCommands**(r14 抱怨「ladder 没给可直接跑的命令」)、recipe 增加 `aliases` 意图词、`listview.configure` 的拒绝恢复补上 `targetCode` 按 bindAction 的语义 |
137
138
  | 0.4.87 | **r14 真机探针修复**:`workitem.list` 的 50000 **不再是无解题**——按 `participant` 走 JPA 查询端点(能查**别人**名下的待办、且不解析流程模板),全局搜索失败时回 `workitem-search-unavailable` 带成因与三条绕行;`workflow.delete` 的 402508 改 `workflow-has-instances` 并说明**不要改用 model.delete 绕过**;`model.delete` 在有未完结待办时回 `workflowWarning`(级联会留脏待办 → 全局待办搜索整页 NPE)。另修两个 composer 模板参数名写错(`form-default --value`→`--default`、`rule-when --rule`→`--on`),并加**模板 vs `--help` 自动校验测试** |
138
139
  | 0.4.86 | **r13 真机探针修复**:`omc next` 增加 **composer 路由**(含建模型+字段/审批流/计算/条件规则/表单默认值 → 直接给出对应一条命令;读面仍走 ladder,条件分支流程仍走 recipe)——r13 读了文档里那行 `model-scaffold` 却仍调 26 次 `field.create`,说明「写进文档」不等于「能被发现」。`data.submit` 审批提交**写前校验身份**:非本人在办的待办直接回 `submit-workitem-not-mine`(带待办持有者 + 三条出路),不再让平台回一个 `40005 权限不足`让 agent 猜 |
139
140
  | 0.4.84 | **回合数优化(从省字节转向省往返)**:新增 `omc model-scaffold`(一条命令建模型+N 字段+子表+发布+表单,先 dry-run 预检零写入)与 `omc call <tool> --args-list`(同工具 N 次、逐条执行、回汇总)。修三个真 bug:键名提示自相矛盾(`workflow.delete` 两边互指)、`--dry-run` 与真写对 NUMERICAL 判定不一致(两套真相)、`runToolCall` 的 dry-run 被判失败 |
@@ -236,6 +237,7 @@ omc intent readback intent.yaml --env <env> # 解码回 intent 形状做等价
236
237
  | `omc bench [--live-sample] [--env <env>]` | 工具面/wire 面的双面埋点跑分(离线 fake 或 live 采样) |
237
238
  | `omc model-scaffold --code <c> --name <n> --fields '<json>'` | 一条命令建模型:模型 → N 字段(含 `subFields` 子表)→ 发布字段 → 发布表单。**先对每个字段跑 dry-run 预检**(零写入),有错一次报全;执行期幂等(已存在记 skipped) |
238
239
  | `omc call <tool> --args-list '<json array>'` | 同一工具跑 N 次、一个进程:逐条执行、默认不中断,回 `succeeded`/`failed` 汇总;失败只列 `failures[]`(带 index),成功不回包。省掉手写 ndjson |
240
+ | `omc model-purge --code <c> [--purge-instances] --apply` | 一条命令按依赖倒序清掉模型:**流程实例 → model.delete 级联带走**流程模板/列表视图/字段/表单/规则。默认 dry-run;有实例不删时会 `blocked` 并说明为何不能改用 model.delete 绕过 |
239
241
  | `omc usage [--top <n>] [--records]` | 用量账本:默认只回汇总(调用数/字节/`redundancy` 重复浪费/`byTool` 排名/`estimatedTokens`≈字节/4);`--records` 才回原始记录。数据源 `.omc/usage.jsonl`(每条 `omc` 调用一行,`omc script` 的每行工具调用另记 `script-call`,带 `parentId`) |
240
242
  | `npm run eval:discovery-cost --workspace omc-cli` | 三组离线 discovery 成本基线(调用次数、stdout 字节、拒绝/重试、CloudPivot 写入) |
241
243
 
@@ -75,8 +75,83 @@ export function serializeEnvelope(value) {
75
75
  export function requestId() {
76
76
  return randomUUID();
77
77
  }
78
+ /**
79
+ * Derive a runnable next command from a refusal payload. Tools that already
80
+ * carry a structured recovery (an explicit `nextCommand`, an artifact `fetch`
81
+ * hint, `recovery[].command`, command-shaped `checks`, or a prepared
82
+ * `nextSubmit`) do not need their prose parsed; everything else stays null
83
+ * rather than inventing a command that does not apply.
84
+ */
85
+ export function nextCommandFor(result) {
86
+ const commandFrom = (text) => {
87
+ if (typeof text !== "string")
88
+ return undefined;
89
+ const trimmed = text.trim();
90
+ if (/^omc\s/.test(trimmed))
91
+ return trimmed;
92
+ // Recovery entries are often "① 按流程收窄(最可靠):omc call …" — the
93
+ // command is there, just after a human marker.
94
+ const index = trimmed.indexOf("omc ");
95
+ return index > 0 ? trimmed.slice(index).trim() : undefined;
96
+ };
97
+ const explicit = result.nextCommand;
98
+ if (typeof explicit === "string" && explicit.trim().length > 0)
99
+ return explicit.trim();
100
+ const fromFetch = commandFrom(result.fetch);
101
+ if (fromFetch !== undefined)
102
+ return fromFetch;
103
+ const recovery = result.recovery;
104
+ if (Array.isArray(recovery)) {
105
+ for (const entry of recovery) {
106
+ if (entry === null || typeof entry !== "object")
107
+ continue;
108
+ const command = commandFrom(entry.command);
109
+ if (command !== undefined)
110
+ return command;
111
+ }
112
+ }
113
+ for (const list of [result.checks, result.workaround]) {
114
+ if (!Array.isArray(list))
115
+ continue;
116
+ for (const entry of list) {
117
+ const command = commandFrom(entry);
118
+ if (command !== undefined)
119
+ return command;
120
+ }
121
+ }
122
+ // A submit that advanced the flow already prepared the NEXT approval args —
123
+ // expose the command instead of making the caller reassemble JSON by hand.
124
+ const nextSubmit = result.nextSubmit;
125
+ if (nextSubmit !== null && typeof nextSubmit === "object" && !Array.isArray(nextSubmit)) {
126
+ const record = nextSubmit;
127
+ if (typeof record.schemaCode === "string" && typeof record.workItemId === "string") {
128
+ const args = {
129
+ schemaCode: record.schemaCode,
130
+ bizObjectId: record.bizObjectId ?? null,
131
+ workflowInstanceId: record.workflowInstanceId ?? null,
132
+ workItemId: record.workItemId,
133
+ agree: true,
134
+ };
135
+ return `omc call data.submit --args '${JSON.stringify(args)}'`;
136
+ }
137
+ }
138
+ const nextWorkItems = result.nextWorkItems;
139
+ if (Array.isArray(nextWorkItems) && nextWorkItems.length > 0 && typeof result.schemaCode === "string") {
140
+ const item = nextWorkItems[0];
141
+ if (item !== null && typeof item === "object") {
142
+ const workItemId = item.workItemId;
143
+ if (typeof workItemId === "string")
144
+ return `omc call data.submit --args '${JSON.stringify({ schemaCode: result.schemaCode, workItemId, agree: true })}'`;
145
+ }
146
+ }
147
+ return undefined;
148
+ }
78
149
  export function classifyResult(result) {
79
150
  const status = String(result.status ?? "ok");
151
+ const withNext = (error) => {
152
+ const nextCommand = nextCommandFor(result);
153
+ return nextCommand === undefined ? error : { ...error, nextCommand };
154
+ };
80
155
  if (status === "ok")
81
156
  return { status, error: null, exitCode: CLI_EXIT_CODES.ok };
82
157
  if (status === "dry-run")
@@ -88,30 +163,45 @@ export function classifyResult(result) {
88
163
  const succeeded = Number(result.succeeded ?? 0);
89
164
  const requested = Number(result.requested ?? 0);
90
165
  const failed = Number(result.failed ?? 0);
166
+ // A `--preflight` batch shape-checks entries instead of executing them; the
167
+ // message must not claim "N 成功/失败" for calls that never ran.
168
+ // A step-chain command (model-purge) also answers `partial`, and the batch
169
+ // wording read "6/7 成功,0 失败" — meaningless for steps (r15).
170
+ const isBatch = typeof result.tool === "string";
171
+ const message = result.mode === "preflight"
172
+ ? `${failed}/${requested} 条未过写前校验(preflight:零 wire、零写入);逐条原因见 failures[]`
173
+ : isBatch
174
+ ? `${succeeded}/${requested} 成功,${failed} 失败;逐条原因见 failures[] / failedSteps[](--args-list 的 index 指向第几条)`
175
+ : `部分步骤失败:逐条原因见 steps[] 的 status/detail(已在失败处停下;幂等,修好后重跑同一条命令)`;
91
176
  return {
92
177
  status,
93
- error: { code: "partial-batch", message: `${succeeded}/${requested} 成功,${failed} 失败;逐条原因见 failures[] / failedSteps[](--args-list 的 index 指向第几条)`, retryable: false },
178
+ error: withNext({ code: "partial-batch", message, retryable: false }),
94
179
  exitCode: 1,
95
180
  };
96
181
  }
97
182
  if (status === "unknown-tool") {
98
183
  const candidates = Array.isArray(result.candidates) ? result.candidates : [];
99
184
  const suffix = candidates.length > 0 ? `;did you mean: ${candidates.join("、")}` : ";用 `omc commands --json --compact --lane <lane>` 或 `omc next` 定位工具";
100
- return { status: "error", error: { code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }, exitCode: CLI_EXIT_CODES.invalidInput };
185
+ return { status: "error", error: withNext({ code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
101
186
  }
102
187
  if (result.blocker === "tool-input-invalid")
103
- return { status: "error", error: { code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }, exitCode: CLI_EXIT_CODES.invalidInput };
188
+ return { status: "error", error: withNext({ code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
104
189
  if (status === "uncertain")
105
- return { status, error: { code: "uncertain", message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }, exitCode: CLI_EXIT_CODES.uncertain };
190
+ return { status, error: withNext({ code: "uncertain", message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }), exitCode: CLI_EXIT_CODES.uncertain };
106
191
  if (status === "refused") {
107
192
  if (result.blocker === "write-lock-held")
108
- return { status, error: { code: "policy-refused", message: String(result.message ?? "write lock held"), retryable: true }, exitCode: CLI_EXIT_CODES.policyRefused };
193
+ return { status, error: withNext({ code: "policy-refused", message: String(result.message ?? "write lock held"), retryable: true }), exitCode: CLI_EXIT_CODES.policyRefused };
109
194
  const auth = String(result.blocker ?? "").startsWith("auth") || String(result.blocker ?? "").includes("credential");
110
- return { status, error: { code: auth ? "auth-config" : "policy-refused", message: String(result.message ?? result.blocker ?? "operation refused"), retryable: false }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
195
+ // Every refusal gets a next step: a specific command when the tool knows
196
+ // one, otherwise the refusal router (`omc-refusals` explains which gate
197
+ // produced it — the documented path in omc-start-here).
198
+ const error = { code: auth ? "auth-config" : "policy-refused", message: String(result.message ?? result.blocker ?? "operation refused"), retryable: false };
199
+ const nextCommand = nextCommandFor(result) ?? (auth ? undefined : "omc help omc-refusals");
200
+ return { status, error: nextCommand === undefined ? error : { ...error, nextCommand }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
111
201
  }
112
202
  const message = String(result.message ?? "internal error");
113
203
  const auth = /credential|configuration|environment|login|authentication|auth/i.test(message);
114
- return { status: "error", error: { code: auth ? "auth-config" : "internal", message, retryable: false }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.internal };
204
+ return { status: "error", error: withNext({ code: auth ? "auth-config" : "internal", message, retryable: false }), exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.internal };
115
205
  }
116
206
  export function errorEnvelope(error, code, exitCode, request) {
117
207
  const message = error instanceof Error ? error.message : String(error);
package/dist/cli.js CHANGED
@@ -13,13 +13,14 @@ import { redactText } from "./redact.js";
13
13
  import { readArgsFile, readStdinArgs, runToolCall, validateAgentPaths } from "./call.js";
14
14
  import { BATCH_MAX_ENTRIES, runBatchCall } from "./batch-call.js";
15
15
  import { cmdModelScaffold } from "./model-scaffold.js";
16
+ import { cmdModelPurge } from "./model-purge.js";
16
17
  import { environmentWritable, requiresProductionConfirmation } from "./environment-policy.js";
17
- import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, requestId } from "./cli-contract.js";
18
+ import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, refusalText, requestId } from "./cli-contract.js";
18
19
  import { configureOutput, humanSummary, stripOutputFlags, writeEnvelope } from "./cli-output.js";
19
20
  import { beginUsage, completeUsage, ensureProjectIgnore, recordUsageOutput, usageReport } from "./usage-log.js";
20
21
  import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
21
22
  import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
22
- import { projectJsonPath, projectResultExtract } from "./path-projection.js";
23
+ import { projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
23
24
  import { basename, join, resolve } from "node:path";
24
25
  import { buildToolRegistry } from "./tools.js";
25
26
  import { acquireWriteLock } from "./write-lock.js";
@@ -318,6 +319,52 @@ async function cmdBatchCall(store, toolName, rawList, options) {
318
319
  }));
319
320
  return CLI_EXIT_CODES.invalidInput;
320
321
  }
322
+ // Whole-batch shape precheck (issue 2026-10-25 §6): `--args-list --dry-run`
323
+ // runs every entry through the full tool (reads included) to report N
324
+ // previews. When the caller only wants "will this batch be accepted", the
325
+ // per-tool semantic gate is the thing that refuses mis-shapes, and it is
326
+ // pure — no wire, no writes. Run it for every entry and report ALL failures
327
+ // in one envelope instead of N round-trips.
328
+ if (options.preflight === true) {
329
+ const registry = buildToolRegistry();
330
+ const tool = registry.find((definition) => definition.name === toolName);
331
+ if (!tool) {
332
+ writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `unknown tool ${toolName};用 omc next 或 omc commands --json --compact --lane <lane> 定位`, retryable: false }));
333
+ return CLI_EXIT_CODES.invalidInput;
334
+ }
335
+ const failures = [];
336
+ entries.forEach((entry, index) => {
337
+ try {
338
+ const parsedEntry = parseToolInput(tool, {
339
+ ...entry,
340
+ ...(options.environment !== undefined && entry.environment === undefined ? { environment: options.environment } : {}),
341
+ });
342
+ tool.writePreflight?.(parsedEntry);
343
+ }
344
+ catch (error) {
345
+ // ToolRefusalError carries the real payload (blocker/citation/validation);
346
+ // rebuild it only when the failure is a plain Error.
347
+ const carried = error.payload;
348
+ const payload = error instanceof ToolInputError ? error.payload : carried ?? { status: "refused", blocker: "tool-input-invalid", message: error instanceof Error ? error.message : String(error) };
349
+ failures.push({ index, blocker: payload.blocker ?? null, message: refusalText(payload), ...(Array.isArray(payload.validation) ? { validation: payload.validation } : {}), ...(payload.citation !== undefined ? { citation: payload.citation } : {}) });
350
+ }
351
+ });
352
+ const result = {
353
+ status: failures.length > 0 ? "partial" : "ok",
354
+ mode: "preflight",
355
+ tool: toolName,
356
+ requested: entries.length,
357
+ shapeChecked: entries.length,
358
+ failed: failures.length,
359
+ writes: 0,
360
+ // On failure the caller has to change the JSON first, so there is no
361
+ // runnable command yet; on success, hand over the exact execution command.
362
+ ...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list ${JSON.stringify(JSON.stringify(entries))}` }),
363
+ };
364
+ const classified = classifyResult(result);
365
+ writeEnvelope(envelope(classified.status, result, request, classified.error));
366
+ return failures.length > 0 ? 1 : 0;
367
+ }
321
368
  const batched = await runBatchCall({
322
369
  tool: toolName,
323
370
  argsList: entries,
@@ -351,7 +398,7 @@ async function cmdCall(store, args) {
351
398
  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")));
352
399
  const toolName = positional[0];
353
400
  if (!toolName || toolName === "help" || args.includes("--help")) {
354
- console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--extract-keys]");
401
+ console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
355
402
  console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
356
403
  return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
357
404
  }
@@ -360,14 +407,19 @@ async function cmdCall(store, args) {
360
407
  // Batch mode: the SAME tool N times, one process (r12: 36 field.create calls).
361
408
  const argsList = flag("--args-list");
362
409
  if (argsList !== undefined)
363
- return cmdBatchCall(store, toolName, argsList, { environment, requestValue, args });
410
+ return cmdBatchCall(store, toolName, argsList, { environment, requestValue, args, preflight: args.includes("--preflight") });
364
411
  // 1.2 of issue 2026-10-23: read faces answer {detailArtifactId, sha256} and the
365
412
  // caller then needs a SECOND call to see one value (16 two-step chains in one
366
413
  // session; 15 more were pure --keys probes). `--extract <path>` (repeatable)
367
414
  // resolves the path inside the artifact of THIS result and returns only the
368
415
  // value; `--extract-keys` lists the artifact's top-level keys with sizes.
369
- const extractPaths = argvValues(args, "--extract");
370
- const extractKeys = args.includes("--extract-keys");
416
+ //
417
+ // issue 2026-10-25 §8: `artifact get` had `--jq` while `omc call` did not, so
418
+ // every list read that only wanted one column paid a second artifact round.
419
+ // `--jq` is the same path projection plus `[]` fan-out; a bare `--jq` is the
420
+ // key inventory (the same muscle memory as `artifact get <id> --jq`).
421
+ const extractPaths = [...argvValues(args, "--extract"), ...argvValues(args, "--jq")];
422
+ const extractKeys = args.includes("--extract-keys") || args.some((token, index) => token === "--jq" && (args[index + 1] === undefined || args[index + 1].startsWith("--")));
371
423
  const dryRun = args.includes("--dry-run");
372
424
  const waitValue = flag("--wait");
373
425
  let waitMs = 0;
@@ -546,6 +598,23 @@ function parseDurationMs(text) {
546
598
  * template re-read three times is 81 KB of the session.
547
599
  */
548
600
  const ARTIFACT_INLINE_CEILING_BYTES = 8192;
601
+ /**
602
+ * `artifact get --jq`: one path walk, with `[]` fan-out (same syntax as
603
+ * `omc call --jq`). Returns the envelope fields so both the `--summary` and the
604
+ * plain branch stay identical.
605
+ */
606
+ function projectArtifactPath(value, path) {
607
+ if (!path.includes("[]")) {
608
+ const projected = projectJsonPath(value, path);
609
+ return projected.missed
610
+ ? { missed: true, ...(projected.at !== undefined ? { at: projected.at } : {}), ...(projected.available !== undefined ? { available: projected.available } : {}) }
611
+ : { missed: false, content: projected.value ?? null };
612
+ }
613
+ const fanout = projectJsonPathFanout(value, path);
614
+ return fanout.missed
615
+ ? { missed: true, ...(fanout.at !== undefined ? { at: fanout.at } : {}), ...(fanout.available !== undefined ? { available: fanout.available } : {}) }
616
+ : { missed: false, content: fanout.values, mapped: fanout.values.length, ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries } : {}) };
617
+ }
549
618
  function formatBytes(bytes) {
550
619
  if (bytes < 1024)
551
620
  return `${bytes}B`;
@@ -578,10 +647,10 @@ async function cmdArtifactGet(id, transport, options = {}) {
578
647
  // this the caller had to spend one round on the shape and another on the
579
648
  // path (or pull the whole body); r5 hit that on 67 KB workflow templates.
580
649
  if (options.jq !== undefined) {
581
- const projected = projectJsonPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
650
+ const projected = projectArtifactPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
582
651
  return emitTransportResult(projected.missed
583
652
  ? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }, fetch: artifactFetchHint(id) }
584
- : { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: projected.value ?? null } }, transport);
653
+ : { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}) } }, transport);
585
654
  }
586
655
  return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, fetch: artifactFetchHint(id) }, transport);
587
656
  }
@@ -610,10 +679,10 @@ async function cmdArtifactGet(id, transport, options = {}) {
610
679
  return emitTransportResult(result, transport);
611
680
  }
612
681
  if (options.jq !== undefined) {
613
- const projected = projectJsonPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
682
+ const projected = projectArtifactPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
614
683
  return emitTransportResult(projected.missed
615
- ? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价` }
616
- : { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.value ?? null }, transport);
684
+ ? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价;[] 映射数组(items[].fields.x)` }
685
+ : { 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 } : {}) }, transport);
617
686
  }
618
687
  // r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
619
688
  // large ones (up to 55 KB) pulled whole workflow templates just to read one
@@ -1089,7 +1158,7 @@ function onboardingHelp() {
1089
1158
  "首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
1090
1159
  "方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。",
1091
1160
  "目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
1092
- "大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --extract <path>(可重复)或 --extract-keys,省掉再跑一次 omc artifact get;artifact get 正文超 8KB 默认也只回形状(--jq/--keys 定向取,--full 才整包)。",
1161
+ "大结果取值一步到位:读面回 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、零写入,一次报全)。",
1093
1162
  "工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
1094
1163
  "单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
1095
1164
  "写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
@@ -1213,8 +1282,8 @@ async function main() {
1213
1282
  commandExitCode = CLI_EXIT_CODES.ok;
1214
1283
  });
1215
1284
  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); });
1216
- 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 汇总。适合批量建字段等').addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").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) => {
1217
- 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] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(options.extractKeys ? ["--extract-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"] : [])];
1285
+ 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、零写入),一次报全所有形状问题").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) => {
1286
+ 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"] : []), ...(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"] : [])];
1218
1287
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
1219
1288
  });
1220
1289
  command.command("compute").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--target <propertyCode>").option("--property <propertyCode>").option("--formula <formula>").option("--where <condition>", "条件聚合:\"<子列code> = <值>\",与 --formula \"{<子表code>.<子列code>}\" 配合编译为 COUNTIF/SUMIF").option("--aggregate <count|sum>", "条件聚合用哪种:count(默认,→COUNTIF)/ sum(→SUMIF)").option("--allow-unfiltered", "显式接受「对整个子表列无条件聚合」;不给则拒绝(会把不符合条件的行也算进去)").option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n字段计算规则拼装器:公式 {字段} 引用 → datarule type2 完整形状(editorText/editorMark 自动生成)。默认 dry-run,--apply 写入。条件聚合用 --formula \"{子表.子列}\" --where \"子列 = 值\"。复杂场景(条件分支/跨模型)走 omc next 的 recipe 路径。\n").action(async (options) => {
@@ -1233,6 +1302,25 @@ async function main() {
1233
1302
  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"] : [])];
1234
1303
  commandExitCode = await cmdApprovalFlow(rest);
1235
1304
  });
1305
+ command
1306
+ .command("model-purge")
1307
+ .option("--code <schemaCode>", "模型 code")
1308
+ .option("--schema <schemaCode>")
1309
+ .option("--purge-instances", "连流程实例一起删(workflow.delete 遇到实例会 402508)")
1310
+ .option("--env <environment>")
1311
+ .option("--environment <environment>")
1312
+ .option("--apply")
1313
+ .addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 → 模型(字段/表单随模型级联)。默认 dry-run。\n")
1314
+ .action(async (options) => {
1315
+ const rest = ["model-purge",
1316
+ ...(options.code ? ["--code", options.code] : []),
1317
+ ...(options.schema ? ["--schema", options.schema] : []),
1318
+ ...(options.purgeInstances ? ["--purge-instances"] : []),
1319
+ ...(options.env ? ["--env", options.env] : []),
1320
+ ...(options.environment ? ["--environment", options.environment] : []),
1321
+ ...(options.apply ? ["--apply"] : [])];
1322
+ commandExitCode = await cmdModelPurge(rest);
1323
+ });
1236
1324
  command
1237
1325
  .command("model-scaffold")
1238
1326
  .option("--code <schemaCode>", "模型 code")
@@ -1404,7 +1492,7 @@ main().then((code) => {
1404
1492
  // envelope (or, for `env --json`, their own JSON) and were printing a second
1405
1493
  // empty envelope after it (live 2026-10-23: `omc rollback apply` answered two
1406
1494
  // lines, violating the one-envelope stdout contract).
1407
- const ownsStdout = ["call", "script", "artifact", "commands", "describe", "contract", "capabilities", "capability", "next", "usage", "intent", "help", "form-default", "compute", "rule-when", "approval-flow", "model-scaffold", "rollback", "env", "--version", "-V"];
1495
+ const ownsStdout = ["call", "script", "artifact", "commands", "describe", "contract", "capabilities", "capability", "next", "usage", "intent", "help", "form-default", "compute", "rule-when", "approval-flow", "model-scaffold", "model-purge", "rollback", "env", "--version", "-V"];
1408
1496
  if (ownsStdout.includes(argv[2] ?? "")) {
1409
1497
  completeUsage(code);
1410
1498
  finish(code);
package/dist/endpoints.js CHANGED
@@ -177,6 +177,16 @@ export const ENDPOINTS = {
177
177
  * so it can answer "what is pending for user X", not only "for me".
178
178
  */
179
179
  workflowListWorkItems: "/api/api/runtime/workflow/list_workitems",
180
+ /** Instances of a workflow (WorkflowInstanceController.list_instances). */
181
+ workflowListInstances: "/api/api/runtime/workflow/list_instances",
182
+ /**
183
+ * Delete one workflow instance (WorkflowInstanceController.delete_instance).
184
+ * Only workflowInstanceId is required — `userId` is taken from the login
185
+ * session, so an instance started by another user can be erased from a single
186
+ * admin session. Required before `workflow.delete`, which refuses (402508)
187
+ * while any instance exists.
188
+ */
189
+ workflowDeleteInstance: "/api/api/runtime/workflow/delete_instance",
180
190
  workflowGetBizObjectByWfInstanceId: "/api/api/runtime/workflow/getBizObjectByWfInstanceId",
181
191
  workflowListInstanceActivity: "/api/api/runtime/workflow/list_workflow_instance_activity",
182
192
  workflowListFinishedWorkItems: "/api/api/runtime/workflow/list_finished_workitems",
@@ -113,13 +113,36 @@ function capabilityScores(registry, goal) {
113
113
  }
114
114
  return scores;
115
115
  }
116
+ /**
117
+ * Default ladder examples (r15). A ladder step with no goal-matching recipe only
118
+ * got `omc commands --lane`, so "创建业务规则" handed the caller nothing runnable
119
+ * even though `rule.save` ships examples. These are the lane's canonical shapes,
120
+ * used ONLY when the goal does not match a specific recipe.
121
+ */
122
+ const DEFAULT_LADDER_RECIPES = {
123
+ businessrule: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
124
+ datarule: { tool: "datarule.create", recipe: "field-compute-rule" },
125
+ };
126
+ function defaultRecipeFor(registry, capabilityId, lane) {
127
+ const wanted = DEFAULT_LADDER_RECIPES[capabilityId];
128
+ if (!wanted)
129
+ return undefined;
130
+ for (const tool of registry) {
131
+ if (tool.name !== wanted.tool || tool.lane !== lane)
132
+ continue;
133
+ const recipe = (tool.executionMetadata?.recipes ?? []).find((entry) => entry.id === wanted.recipe);
134
+ if (recipe)
135
+ return { tool: tool.name, recipe: recipe.id, title: recipe.title };
136
+ }
137
+ return undefined;
138
+ }
116
139
  function bestRecipe(registry, capabilityId, lane, goal, minScore = 3) {
117
140
  let best;
118
141
  for (const tool of registry) {
119
142
  if (tool.lane !== lane)
120
143
  continue;
121
144
  for (const recipe of tool.executionMetadata?.recipes ?? []) {
122
- const score = overlap(goal, `${recipe.title} ${recipe.id}`);
145
+ const score = overlap(goal, `${recipe.title} ${recipe.id} ${(recipe.aliases ?? []).join(" ")}`);
123
146
  if (score >= minScore && (best === undefined || score > best.score))
124
147
  best = { tool: tool.name, recipe: recipe.id, title: recipe.title, score };
125
148
  }
@@ -238,17 +261,27 @@ function sectionPointersFor(ids) {
238
261
  return pointers;
239
262
  }
240
263
  /**
241
- * Ready-to-run methodology reads, one per topic, that fetch ALL of that topic's
242
- * relevant sections in a single call. Round 15: bench60-62 fetched each
243
- * `readSections` entry separately (~18 round-trips); `--section` is repeatable
244
- * and comma-separated, so grouping by topic collapses them to ~3 without adding
245
- * any bytes (the outputs land in context either way — the saving is turns).
264
+ * Ready-to-run methodology reads.
265
+ *
266
+ * Round 15 (bench60-62) collapsed ~18 per-section reads into one per topic to
267
+ * save turns. r10 F7 found the other edge: following that hint fetched
268
+ * EVERYTHING at once (~15 KB) when most tasks need only the first section.
269
+ * So the primary command per topic fetches the FIRST required section, and the
270
+ * remaining sections are offered as separate incremental commands (each keeps
271
+ * its byte count in `readSections`) — cheap by default, still one command away.
246
272
  */
247
- function batchReadCommands(pointers) {
273
+ function splitReadCommands(pointers) {
248
274
  const byTopic = new Map();
249
275
  for (const pointer of pointers)
250
276
  byTopic.set(pointer.topic, [...(byTopic.get(pointer.topic) ?? []), pointer.section]);
251
- return [...byTopic.entries()].map(([topic, sections]) => `omc help ${topic} ${sections.map((section) => `--section "${section}"`).join(" ")}`);
277
+ const readCommands = [];
278
+ const readMore = [];
279
+ for (const [topic, sections] of byTopic.entries()) {
280
+ readCommands.push(`omc help ${topic} --section "${sections[0]}"`);
281
+ for (const section of sections.slice(1))
282
+ readMore.push(`omc help ${topic} --section "${section}"`);
283
+ }
284
+ return { readCommands, readMore };
252
285
  }
253
286
  /**
254
287
  * The route's methodology-read surface: pointers (+ per-section bytes) +
@@ -279,14 +312,15 @@ function routeReads(readSections) {
279
312
  }
280
313
  }
281
314
  }
282
- return { readSections: budgeted, readCommands: batchReadCommands(readSections), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
315
+ const commands = splitReadCommands(readSections);
316
+ return { readSections: budgeted, readCommands: commands.readCommands, ...(commands.readMore.length > 0 ? { readMore: commands.readMore } : {}), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
283
317
  }
284
318
  /** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
285
319
  function sectionBytes(pointer) {
286
320
  const resolved = readSkillSection(pointer.topic, pointer.section);
287
321
  return resolved ? Buffer.byteLength(resolved.section, "utf8") : 0;
288
322
  }
289
- const READ_HINT = '按 readCommands 一次读完(每条命令用可重复的 `--section` 取该 topic 的所有相关节,等价于逐节 `omc help <topic> --section "<section>"` 但只花一次往返);单节字节数见 readSectionBudget,按预算自选。`omc help <topic>` 默认只回节索引(每节带字节数,约 1KB),不要再用 `--all` 读整篇(5–14KB,读一次就留在后续每一轮)。';
323
+ const READ_HINT = '先跑 readCommands(每个 topic 只取第一节,形状 `omc help <topic> --section "<section>"`,字节数见 readSections);不够再按需补 readMore,不要一次全读。`omc help <topic>` 默认只回节索引(每节带字节数,约 1KB),不要再用 `--all` 读整篇(5–14KB,读一次就留在后续每一轮)。';
290
324
  function orderedCapabilities(ids, index) {
291
325
  const selected = new Set(ids);
292
326
  const nodes = [...index.nodeById.values()];
@@ -322,6 +356,18 @@ function orderedCapabilities(ids, index) {
322
356
  */
323
357
  const WHOLE_PACKAGE_CAPABILITIES = ["model", "field", "form", "listview", "datarule", "businessrule", "workflow", "onlinejs"];
324
358
  const COMPOSER_ROUTES = Object.freeze([
359
+ {
360
+ id: "model-purge",
361
+ command: "omc model-purge --code <模型code> --purge-instances --apply",
362
+ usage: "omc model-purge --help",
363
+ summary: "清理一个模型:先删流程实例,再 model.delete 级联带走流程模板/视图/字段/表单/规则;默认 dry-run,不可删的停滞实例会跳过并在结尾列出",
364
+ // r15: "清理本轮创建的模型和流程实例" matched approval-flow (流程 + 创建)
365
+ // and would have CREATED a workflow instead of removing one. Cleanup intents
366
+ // are their own composer, and this one comes first.
367
+ groups: [["清理", "删除", "移除", "回收", "清除", "purge", "teardown", "clean", "remove"], ["模型", "实例", "数据", "model", "instance", "data"]],
368
+ avoid: ["字段", "角色", "部门", "视图", "按钮", "规则图"],
369
+ topics: ["omc-write-hazards"],
370
+ },
325
371
  {
326
372
  id: "model-scaffold",
327
373
  command: "omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON数组>' --apply",
@@ -339,7 +385,7 @@ const COMPOSER_ROUTES = Object.freeze([
339
385
  summary: "线性审批流一条命令:建流程 → 模板+字段只读权限 → 发布(多级用重复 --steps)",
340
386
  groups: [["审批", "流程"], ["建", "创建", "新建", "配置", "加", "搭"]],
341
387
  // Branching / countersign shapes are NOT this composer's business.
342
- avoid: ["条件分支", "分支", "会签", "并行", "或签", "网关", "子流程", "动态审批人"],
388
+ avoid: ["条件分支", "分支", "会签", "并行", "或签", "网关", "子流程", "动态审批人", "清理", "删除", "移除", "purge", "teardown"],
343
389
  topics: ["omc-five-piece-flow"],
344
390
  },
345
391
  {
@@ -531,7 +577,7 @@ export function goalRoute(registry, goal) {
531
577
  if (readish) {
532
578
  return { capability: id, lane: capability.lane, readTools: readToolsInLane(registry, capability.lane), firstCommand: `omc capability ${id}` };
533
579
  }
534
- const recipe = bestRecipe(registry, id, capability.lane, goal);
580
+ const recipe = bestRecipe(registry, id, capability.lane, goal) ?? defaultRecipeFor(registry, id, capability.lane);
535
581
  return {
536
582
  capability: id,
537
583
  lane: capability.lane,
@@ -553,7 +599,21 @@ export function goalRoute(registry, goal) {
553
599
  confident: true,
554
600
  mode: whole ? "whole-package" : mode,
555
601
  match: { kind: readish ? "read-ladder" : "ladder", ref: ordered.join("→"), capabilities: ordered },
556
- route: { ...(whole ? { primary: "intent", atomicSupplement } : { ladder }), ...routeReads(readSections), firstCommands: [DISCOVERY_AUTHORITY.readiness], write, ...(readish ? { readback: true } : {}) },
602
+ route: {
603
+ ...(whole ? { primary: "intent", atomicSupplement } : { ladder }),
604
+ ...routeReads(readSections),
605
+ // Each ladder step already carried a runnable recipe command, but it sat
606
+ // buried at `ladder[i].recipe.command` behind a readiness line — r14's
607
+ // probe reported "ladder 本身没给可直接跑的命令" and hand-built the rule
608
+ // graph instead. Surface them at the top level where `firstCommands` is
609
+ // the thing an agent actually reads.
610
+ firstCommands: [
611
+ DISCOVERY_AUTHORITY.readiness,
612
+ ...ladder.flatMap((step) => step.recipe?.command ? [step.recipe.command] : []),
613
+ ],
614
+ write,
615
+ ...(readish ? { readback: true } : {}),
616
+ },
557
617
  write,
558
618
  ...(whole ? { intent: intentGuidance() } : {}),
559
619
  };