@stalfh233/omc-cli 0.4.99 → 0.4.101
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 +13 -1
- package/coverage/m1-coverage-manifest-v1.json +9 -3
- package/dist/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli-contract.js +5 -1
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +230 -9
- package/dist/goal-routes.js +159 -57
- package/dist/governed-execution.js +1 -1
- package/dist/lanes/b2-registration.js +20 -3
- package/dist/lanes/businessrule-contract.js +4 -0
- package/dist/lanes/businessrule-coverage.js +1 -1
- package/dist/lanes/businessrule.js +93 -0
- package/dist/lanes/field.js +10 -1
- package/dist/lanes/form.js +2 -2
- package/dist/lanes/report.js +21 -5
- package/dist/lanes/workflow.js +38 -3
- package/dist/recipes/b2-registration.js +8 -7
- package/dist/recipes/businessrule.js +58 -1
- package/dist/refusal-guidance.js +207 -0
- package/dist/rule-support.js +96 -0
- package/dist/skills.js +89 -21
- package/dist/tools.js +35 -0
- package/dist/usage-log.js +21 -2
- package/dist/write.js +2 -2
- package/docs/images/omc-nixi/README.md +41 -0
- package/docs/omc-introduction.md +851 -0
- package/package.json +3 -3
- package/skills/omc-capabilities.md +3 -3
- package/skills/omc-refusals.md +1 -1
- package/skills/omc-start-here.md +2 -1
- package/skills/omc-tool-selection.md +1 -1
- package/skills/omc-write-hazards.md +5 -0
package/dist/skills.js
CHANGED
|
@@ -81,6 +81,22 @@ function scanHeadings(lines) {
|
|
|
81
81
|
}
|
|
82
82
|
return found;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Slice one heading's body using the documented rule: a section runs until the
|
|
86
|
+
* next heading of the same or a higher level. Shared by `--section` and search
|
|
87
|
+
* so a search hit can announce the exact bytes `--section` would return.
|
|
88
|
+
*/
|
|
89
|
+
function sliceSectionText(lines, start, level) {
|
|
90
|
+
let end = lines.length;
|
|
91
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
92
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
93
|
+
if (match && match[1].length <= level) {
|
|
94
|
+
end = index;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.slice(start, end).join("\n").trim() + "\n";
|
|
99
|
+
}
|
|
84
100
|
/** Return one Markdown heading and its nested content without loading the full topic into the envelope. */
|
|
85
101
|
export function readSkillSection(name, requested) {
|
|
86
102
|
const document = readSkill(name);
|
|
@@ -121,16 +137,7 @@ export function readSkillSection(name, requested) {
|
|
|
121
137
|
if (start < 0)
|
|
122
138
|
return { document, section: "", headings };
|
|
123
139
|
const startEntry = scanned.find((entry) => entry.index === start);
|
|
124
|
-
|
|
125
|
-
let end = lines.length;
|
|
126
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
127
|
-
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
128
|
-
if (match && match[1].length <= level) {
|
|
129
|
-
end = index;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return { document, section: lines.slice(start, end).join("\n").trim() + "\n", headings };
|
|
140
|
+
return { document, section: sliceSectionText(lines, startEntry.index, startEntry.level), headings };
|
|
134
141
|
}
|
|
135
142
|
/**
|
|
136
143
|
* The "shape" of a topic: every heading with the bytes it would cost to read.
|
|
@@ -147,17 +154,7 @@ export function readSkillOutline(name) {
|
|
|
147
154
|
const lines = document.content.split(/\r?\n/);
|
|
148
155
|
const headings = scanHeadings(lines);
|
|
149
156
|
const sections = headings.map((entry) => {
|
|
150
|
-
|
|
151
|
-
// Same slicing rule as readSkillSection: a section runs until the next
|
|
152
|
-
// heading of the same or a higher level, so reported bytes match the read.
|
|
153
|
-
for (let index = entry.index + 1; index < lines.length; index += 1) {
|
|
154
|
-
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
155
|
-
if (match && match[1].length <= entry.level) {
|
|
156
|
-
end = index;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const body = `${lines.slice(entry.index, end).join("\n").trim()}\n`;
|
|
157
|
+
const body = sliceSectionText(lines, entry.index, entry.level);
|
|
161
158
|
return { heading: entry.heading, level: entry.level, bytes: Buffer.byteLength(body, "utf8") };
|
|
162
159
|
});
|
|
163
160
|
const bytes = Buffer.byteLength(document.content, "utf8");
|
|
@@ -170,4 +167,75 @@ export function readSkillOutline(name) {
|
|
|
170
167
|
const visible = indexLevel === undefined ? sections : sections.filter((entry) => entry.level === indexLevel);
|
|
171
168
|
return { document, bytes, sections: visible.length > 0 ? visible : sections };
|
|
172
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Cross-topic full-text search (`omc help --search`).
|
|
172
|
+
*
|
|
173
|
+
* Why this exists: r18 spent 173.8 KB re-dumping five whole topics twice to grep
|
|
174
|
+
* for `40000`, because `omc help` is a bookshelf (one heading index per topic)
|
|
175
|
+
* and there was no way to ask a question. A search that returns only
|
|
176
|
+
* topic/heading would still cost a second call, so a hit carries its section
|
|
177
|
+
* body and the exact bytes `--section` would return.
|
|
178
|
+
*
|
|
179
|
+
* Matching is deliberately literal: case-insensitive substring, whitespace
|
|
180
|
+
* splits terms, `all` requires every term in the same section and `any`
|
|
181
|
+
* requires one. No stemming, no fuzzy matching — an agent searching an exact
|
|
182
|
+
* error code must never be steered to a similar-looking section.
|
|
183
|
+
*/
|
|
184
|
+
export function searchSkills(query, options) {
|
|
185
|
+
const terms = query.trim().toLowerCase().split(/\s+/).filter((term) => term.length > 0);
|
|
186
|
+
if (terms.length === 0)
|
|
187
|
+
return { matches: [], total: 0 };
|
|
188
|
+
const found = [];
|
|
189
|
+
for (const entry of listSkills()) {
|
|
190
|
+
const document = readSkill(entry.name);
|
|
191
|
+
if (!document)
|
|
192
|
+
continue;
|
|
193
|
+
const lines = document.content.split(/\r?\n/);
|
|
194
|
+
const scanned = scanHeadings(lines);
|
|
195
|
+
// Same visible levels as the index: a whole-document H1 must not swallow
|
|
196
|
+
// every H2 hit into one giant match.
|
|
197
|
+
const levels = [...new Set(scanned.map((item) => item.level))].sort((left, right) => left - right);
|
|
198
|
+
const singleRoot = levels.length > 1 && scanned.filter((item) => item.level === levels[0]).length === 1;
|
|
199
|
+
const indexLevel = singleRoot ? levels[1] : levels[0];
|
|
200
|
+
const visible = scanned.filter((heading) => heading.level === indexLevel);
|
|
201
|
+
for (const [ordinal, heading] of visible.entries()) {
|
|
202
|
+
if (indexLevel === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
const section = sliceSectionText(lines, heading.index, heading.level);
|
|
205
|
+
const lower = section.toLowerCase();
|
|
206
|
+
const hitTerms = terms.filter((term) => lower.includes(term));
|
|
207
|
+
if (hitTerms.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
if (options.mode === "all" && hitTerms.length < terms.length)
|
|
210
|
+
continue;
|
|
211
|
+
const matchLines = [];
|
|
212
|
+
for (let index = heading.index; index < lines.length && matchLines.length < 3; index += 1) {
|
|
213
|
+
const text = lines[index];
|
|
214
|
+
const lineTerms = hitTerms.filter((term) => text.toLowerCase().includes(term));
|
|
215
|
+
if (lineTerms.length === 0)
|
|
216
|
+
continue;
|
|
217
|
+
if (matchLines.length > 0 && /^(#{1,6})\s+/.test(text))
|
|
218
|
+
continue;
|
|
219
|
+
matchLines.push({ line: index + 1, text: text.trim(), terms: lineTerms });
|
|
220
|
+
}
|
|
221
|
+
const headingHit = hitTerms.some((term) => heading.heading.toLowerCase().includes(term));
|
|
222
|
+
const bytes = Buffer.byteLength(section, "utf8");
|
|
223
|
+
found.push({
|
|
224
|
+
topic: document.name,
|
|
225
|
+
heading: heading.heading,
|
|
226
|
+
sectionOrdinal: ordinal + 1,
|
|
227
|
+
matchLines,
|
|
228
|
+
sectionBytes: bytes,
|
|
229
|
+
section,
|
|
230
|
+
// Heading hits beat body hits; more distinct terms beat fewer; smaller
|
|
231
|
+
// sections beat larger ones (cheaper to carry, usually more specific).
|
|
232
|
+
score: (headingHit ? 1000 : 0) + hitTerms.length * 100 + Math.max(0, 100 - Math.round(bytes / 64)),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
found.sort((left, right) => right.score - left.score
|
|
237
|
+
|| left.topic.localeCompare(right.topic)
|
|
238
|
+
|| left.sectionOrdinal - right.sectionOrdinal);
|
|
239
|
+
return { matches: found.slice(0, Math.max(1, options.limit)), total: found.length };
|
|
240
|
+
}
|
|
173
241
|
//# sourceMappingURL=skills.js.map
|
package/dist/tools.js
CHANGED
|
@@ -21,6 +21,8 @@ import { registerListViewLane } from "./lanes/list-view.js";
|
|
|
21
21
|
import { registerReportLane } from "./lanes/report.js";
|
|
22
22
|
import { registerApplyChangesLane } from "./lanes/apply-changes.js";
|
|
23
23
|
import { executionMetadataFor } from "./execution-metadata.js";
|
|
24
|
+
import { attachRefusalRecovery } from "./refusal-guidance.js";
|
|
25
|
+
import { ToolRefusalError } from "./tool-types.js";
|
|
24
26
|
export { ToolRefusalError, ToolUncertainError } from "./tool-types.js";
|
|
25
27
|
export { makeToolContext };
|
|
26
28
|
/**
|
|
@@ -59,6 +61,39 @@ export function buildToolRegistry() {
|
|
|
59
61
|
if (metadata)
|
|
60
62
|
tool.executionMetadata = metadata;
|
|
61
63
|
}
|
|
64
|
+
// r19 ticket 06 — 拒绝即教学,收口在最深的一层:每个 handler 返回/抛出的
|
|
65
|
+
// refused 结果在这里统一附加 recovery(同源配方命令、正确形状、不要做什么)。
|
|
66
|
+
// 收口在 registry 而不是 CLI 出口,是因为 runToolCall、测试 harness、MCP
|
|
67
|
+
// client、组装器全部经过 handler——一处附加、处处生效。已自带 recovery 的
|
|
68
|
+
// 拒绝(write-lock-held)不会被覆盖(attachRefusalRecovery 幂等)。
|
|
69
|
+
for (const tool of tools) {
|
|
70
|
+
const inner = tool.handler;
|
|
71
|
+
const name = tool.name;
|
|
72
|
+
tool.handler = async (ctx, args, self) => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await inner(ctx, args, self);
|
|
75
|
+
if (result && typeof result === "object") {
|
|
76
|
+
const status = result.status;
|
|
77
|
+
// attachRefusalRecovery decides for itself: refused envelopes get
|
|
78
|
+
// guidance, uncertain ones only when the dashboard boundary is
|
|
79
|
+
// pinned in the frozen detail (r19 p14).
|
|
80
|
+
if (status === "refused" || status === "uncertain") {
|
|
81
|
+
return attachRefusalRecovery(result, name, tools);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const payload = error.payload;
|
|
88
|
+
if (payload && typeof payload === "object" && payload.status === "refused") {
|
|
89
|
+
const enriched = attachRefusalRecovery(payload, name, tools);
|
|
90
|
+
if (enriched !== payload)
|
|
91
|
+
throw new ToolRefusalError(enriched);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
62
97
|
return tools;
|
|
63
98
|
}
|
|
64
99
|
export { loadCoverageManifest };
|
package/dist/usage-log.js
CHANGED
|
@@ -34,6 +34,21 @@ const USAGE_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
34
34
|
* Memos still read `logPath()` only — a fallback log is append-only telemetry,
|
|
35
35
|
* never a source of "already did this" answers.
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* r19 ticket 02: explicit opt-out for the WRITE side of the ledger.
|
|
39
|
+
*
|
|
40
|
+
* A maintainer script or an investigation subprocess that runs read-only
|
|
41
|
+
* discovery faces inside a sandbox used to append to that round's
|
|
42
|
+
* `.omc/usage.jsonl` — the r18 probe was read as 469 calls where the agent
|
|
43
|
+
* made 184 (19 polling reads + 105 investigation reads). "What did this round
|
|
44
|
+
* cost" must mean "what did the AGENT spend": `OMC_NO_USAGE=1` mutes this
|
|
45
|
+
* invocation's records (project log, user fallback, explicit OMC_USAGE_LOG —
|
|
46
|
+
* the switch beats them all). Only the write entry points consult this guard;
|
|
47
|
+
* reading (`omc usage`, idempotency memos) is untouched.
|
|
48
|
+
*/
|
|
49
|
+
function recordingDisabled() {
|
|
50
|
+
return process.env.OMC_NO_USAGE === "1";
|
|
51
|
+
}
|
|
37
52
|
function recordLogPath() {
|
|
38
53
|
const explicit = process.env.OMC_USAGE_LOG;
|
|
39
54
|
if (explicit)
|
|
@@ -74,6 +89,8 @@ function appendRecord(path, record) {
|
|
|
74
89
|
}
|
|
75
90
|
/** Append a `script-call` record: one line of `omc script` = one inner tool call. */
|
|
76
91
|
export function recordScriptCall(input) {
|
|
92
|
+
if (recordingDisabled())
|
|
93
|
+
return;
|
|
77
94
|
const target = recordLogPath();
|
|
78
95
|
if (!target)
|
|
79
96
|
return;
|
|
@@ -209,7 +226,7 @@ function commandOf(argv) {
|
|
|
209
226
|
for (let index = 2; index < argv.length && values.length < 2; index += 1) {
|
|
210
227
|
const token = argv[index];
|
|
211
228
|
if (token.startsWith("--")) {
|
|
212
|
-
if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top"].includes(token))
|
|
229
|
+
if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top", "--search", "--search-mode", "--limit"].includes(token))
|
|
213
230
|
index += 1;
|
|
214
231
|
continue;
|
|
215
232
|
}
|
|
@@ -227,7 +244,7 @@ function facesOf(argv) {
|
|
|
227
244
|
// `omc artifact get <id> --jq <path>` showed up as a bare `artifact get` with
|
|
228
245
|
// empty argKeys, so the cost report could not tell a 200 B projection from a
|
|
229
246
|
// 44 KB pull — precisely the read path these flags exist to make cheap.
|
|
230
|
-
const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--out", "--quiet", "--jq", "--summary", "--keys", "--path-only", "--extract", "--extract-keys", "--records"];
|
|
247
|
+
const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--search", "--search-mode", "--out", "--quiet", "--jq", "--summary", "--keys", "--path-only", "--extract", "--extract-keys", "--records"];
|
|
231
248
|
return faces.filter((face) => argv.includes(face));
|
|
232
249
|
}
|
|
233
250
|
const safeArgumentKeys = new Set(["schemaCode", "propertyCode", "code", "workflowCode", "ruleCode", "sheetCode", "id", "dataRuleType", "propertyType", "published", "handler"]);
|
|
@@ -270,6 +287,8 @@ function summarizeArgs(argv) {
|
|
|
270
287
|
return summary;
|
|
271
288
|
}
|
|
272
289
|
export function beginUsage(argv) {
|
|
290
|
+
if (recordingDisabled())
|
|
291
|
+
return;
|
|
273
292
|
const target = recordLogPath();
|
|
274
293
|
if (!target)
|
|
275
294
|
return;
|
package/dist/write.js
CHANGED
|
@@ -124,7 +124,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
124
124
|
const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
|
|
125
125
|
return { status: executed.status, steps: executed.steps, failedStep: executed.failedStep, ...executed.envelope, ...(args.requestId ? { requestId: args.requestId } : {}), artifactFingerprint: artifactRef.detailSha256 };
|
|
126
126
|
}
|
|
127
|
-
const { performed, readback, steps } = executed;
|
|
127
|
+
const { performed, readback, steps, halfSuccess } = executed;
|
|
128
128
|
// Readback gate (live 2026-10-17): a tool that reports `readback.present:false`
|
|
129
129
|
// has NOT proven its own write — the platform accepted the request but the
|
|
130
130
|
// object is absent from the authoritative face. "Called without error" must
|
|
@@ -177,7 +177,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
177
177
|
});
|
|
178
178
|
rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
|
|
179
179
|
}
|
|
180
|
-
return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
|
|
180
|
+
return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(halfSuccess !== undefined ? { halfSuccess } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 截图占位目录(尼西实战案例)
|
|
2
|
+
|
|
3
|
+
`omc-introduction.md` 的「15. 实战一:尼西双表单与流程搭建」章节里引用了 9 张截图。
|
|
4
|
+
把截图按下面文件名放进本目录即可,文档中的占位说明可逐条删除。
|
|
5
|
+
|
|
6
|
+
## 客户源需求(3 张)
|
|
7
|
+
|
|
8
|
+
| 文件名 | 内容 |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `01-requirement-ft-sheet.png` | 客户 Excel 的 `FT` Sheet(字段清单 / 必填 / LotClass 显隐) |
|
|
11
|
+
| `02-requirement-fp-sheet.png` | 客户 Excel 的 `FP` Sheet |
|
|
12
|
+
| `03-requirement-flow.png` | 客户给的流程图(FT/FP,含 `SubConRootCause=1` 分支) |
|
|
13
|
+
|
|
14
|
+
## 对话截图(6 张)
|
|
15
|
+
|
|
16
|
+
| 文件名 | 内容 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `04-chat-app-create.png` | 确认新建应用 + `app.create ok` |
|
|
19
|
+
| `05-chat-intent-apply.png` | `intent apply` 返回 `DELIVERED` + `equivalent: true`(89 个动作) |
|
|
20
|
+
| `06-chat-workflow-role.png` | 流程节点与角色路由(PIC→蔡依涛 / MOS→赵益 / AOSIT / AOSENG) |
|
|
21
|
+
| `07-chat-permission-matrix.png` | 节点字段权限矩阵(本节点 + 之前所有节点可见) |
|
|
22
|
+
| `08-chat-biz-integration.png` | `service.register` 注册 productType 服务 + `method.test` 返回 `PRODUCT_TYPE=MOS` |
|
|
23
|
+
| `09-chat-onlinejs-publish.png` | `onlinejs.compose` 返回 `published: true` |
|
|
24
|
+
|
|
25
|
+
## 替换方式
|
|
26
|
+
|
|
27
|
+
文档里每处占位是:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
> **[截图 01|客户源需求:FT 表单规格]**
|
|
31
|
+
> 占位文件:`docs/images/omc-nixi/01-requirement-ft-sheet.png`
|
|
32
|
+
> 建议内容:...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
落地后改成标准图片引用即可:
|
|
36
|
+
|
|
37
|
+
```markdown
|
|
38
|
+

|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
建议截图宽度 ≥ 1200px,避免中文文字模糊。
|