@stalfh233/omc-cli 0.4.99 → 0.4.100
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/dist/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +223 -9
- package/dist/goal-routes.js +127 -57
- package/dist/lanes/report.js +21 -5
- package/dist/refusal-guidance.js +207 -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/package.json +3 -3
- package/skills/omc-start-here.md +2 -1
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.100",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.100",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.100",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
package/skills/omc-start-here.md
CHANGED
|
@@ -26,7 +26,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
26
26
|
- `build`/`modify` → 按阶梯内联的**写工具 + 必填参数**执行。
|
|
27
27
|
- `scout`(能不能/怎么写/是否支持)→ 先探边界(`omc-capability-scouting`),别直接写。若 `next` 返回 **`boundary`**(`match.kind:"boundary"`,如 `ocr-e-signature`/`release-prod`/`inline-coding`),说明需求**不在覆盖内**(`write:false`):如实报告、用 `omc capabilities` 核对,**不要**用相近能力伪造实现。
|
|
28
28
|
阶梯步骤已内联工具必填参数(`route.ladder[].tools`/`readTools`)与命中 recipe 的 `describe <tool> --example <id>`;据此直接执行——**不要再逐能力 `commands --lane`**,也不要先扫 `commands --json`/全部 `contract`/全部 `describe --full`。
|
|
29
|
-
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB
|
|
29
|
+
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB,读一次就留在后续每一轮窗口)。**带着具体问题(错误码/跨能力语义/某个词)直接 `omc help --search "<词>"`**——跨全部 topic 全文检索,命中节的正文随结果内联返回,一次调用就是答案;**严禁自己写 `for t in …; do omc help $t --all | grep …` 循环**(r18 实测 12 调用/176KB,检索一次 8KB 搞定)。
|
|
30
30
|
2. 写前只需读 `omc-config-draft-review`(人审门)与 `omc-five-piece-flow`(发现→草稿→权限→执行→回读);`omc-business-configuration` 按命中的能力分节读(见 `next` 的 `readSections`),不要整篇预取。
|
|
31
31
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|
|
32
32
|
4. 只有工具不在 `next` 命中能力内、或要看 purpose/枚举时,才补一次 `commands --json --compact --lane <lane>`。非显然形状(规则图、流程模板、表单布局、RESTFUL)才单独 `describe --example`。
|
|
@@ -50,6 +50,7 @@ omc script - --env <env> <<< '{"tool":"model.list"}' # 多调链:NDJSON 每
|
|
|
50
50
|
omc capability <selected-id> # only the selected capability
|
|
51
51
|
omc describe <tool> --example <recipe-id> # recipe first; default describe otherwise
|
|
52
52
|
omc help <topic> --section "<heading>" # one methodology section, not all
|
|
53
|
+
omc help --search "40000" # full-text search across all topics (answer in one call)
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
Discovery precedes every write. 预算档位见 `omc-tool-selection` 的「Discovery 成本档位」;裸 `omc commands` 是有界索引,全量目录须显式 `commands --json`。
|