@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/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
- const level = startEntry.level;
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
- let end = lines.length;
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.99",
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.99",
53
+ "@stalfh233/omc-fake-platform": "0.4.100",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.99",
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",
@@ -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`。