@stalfh233/omc-cli 0.4.67 → 0.4.68

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.
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { asciiStdout, serializeEnvelope } from "./cli-contract.js";
3
- import { recordUsageOutput } from "./usage-log.js";
3
+ import { currentUsageCommand, priorRepeatedFailures, recordUsageOutput } from "./usage-log.js";
4
4
  let options = { quiet: false, verbose: false };
5
5
  /** Parse `--quiet`, `--verbose` and `--out <file>` from the raw argv once, before dispatch. */
6
6
  export function configureOutput(argv) {
@@ -85,8 +85,38 @@ function emitAsciiBannerOnce() {
85
85
  // 横幅走 stderr(不进管道、不破坏单信封契约),在痛点时刻给出两个解法。
86
86
  process.stderr.write("[omc] 当前控制台代码页非 UTF-8,中文已 ASCII 转义防乱码。要原样中文:本次调用加 --utf8,或 chcp 65001 后重跑,或设 OMC_ASCII_STDOUT=0。\n");
87
87
  }
88
+ /**
89
+ * Attach a repeat warning to a refusal that repeats the previous one. Refusal
90
+ * envelopes already say what is wrong; a third identical failure means the
91
+ * caller is tuning arguments inside a dead end (r6-onboarding 2026-10-21: 16
92
+ * repeat runs, 27 avoidable calls), so say so where it will be read.
93
+ */
94
+ function withRepeatWarning(value) {
95
+ if (!value || typeof value !== "object")
96
+ return value;
97
+ const envelope = value;
98
+ const status = String(envelope.status ?? "");
99
+ if (status !== "refused" && status !== "error" && status !== "uncertain")
100
+ return value;
101
+ const result = envelope.result && typeof envelope.result === "object" ? envelope.result : undefined;
102
+ const blocker = typeof result?.blocker === "string" ? result.blocker : typeof envelope.blocker === "string" ? envelope.blocker : undefined;
103
+ const prior = priorRepeatedFailures(currentUsageCommand(), blocker);
104
+ if (prior.count < 2)
105
+ return value;
106
+ const repeatWarning = {
107
+ sameFailureCount: prior.count + 1,
108
+ blocker: blocker ?? null,
109
+ ...(prior.requestIds.length > 0 ? { previousRequestIds: prior.requestIds } : {}),
110
+ hint: status === "uncertain"
111
+ ? `同一调用已连续返回 uncertain ${prior.count + 1} 次。uncertain 表示结果未确定:先按 requestId 回读真实状态再决定,不要盲目重发写请求。`
112
+ : `同一调用已连续失败 ${prior.count + 1} 次(blocker ${blocker ?? "unknown"})。不要再微调参数重试:换形状或换工具——先 describe/配方确认契约,或按 blocker 指向的替代路径处理。`,
113
+ };
114
+ if (result)
115
+ return { ...envelope, result: { ...result, repeatWarning } };
116
+ return { ...envelope, repeatWarning };
117
+ }
88
118
  export function writeEnvelope(value) {
89
- const outputValue = compactForOutput(value);
119
+ const outputValue = compactForOutput(withRepeatWarning(value));
90
120
  const text = serializeEnvelope(outputValue);
91
121
  if (asciiStdout())
92
122
  emitAsciiBannerOnce();
@@ -2226,10 +2226,18 @@ export function registerWorkflowLane(tools) {
2226
2226
  schemaCode: linked?.schemaCode ?? wireString(item.schemaCode),
2227
2227
  };
2228
2228
  });
2229
+ // r6-onboarding 2026-10-21: 89 of 117 calls used this tool with only a
2230
+ // schemaCode — mostly to ask "where did the flow go", which the todo query
2231
+ // cannot answer (it lists pending items only). Say what this call is for
2232
+ // at the moment it is used, and name the two substitutes.
2233
+ const purposeHint = args.workflowInstanceId === undefined && args.workflowCode === undefined
2234
+ ? "本调用用于发现待办并取 workItemId 做审批;只想看流程走到哪,用 workflow.instanceActivity;提交/审批后优先复用 data.submit 的 readback.nextSubmit,不要重复查询。"
2235
+ : undefined;
2229
2236
  return {
2230
2237
  total: page.total,
2231
2238
  totalPages: page.totalPages,
2232
2239
  count: items.length,
2240
+ ...(purposeHint ? { purposeHint } : {}),
2233
2241
  ...(page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
2234
2242
  items,
2235
2243
  };
package/dist/usage-log.js CHANGED
@@ -1,4 +1,4 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, statSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { dirname, resolve } from "node:path";
4
4
  let pending;
@@ -9,6 +9,47 @@ function logPath() {
9
9
  const projectEnv = resolve(process.cwd(), ".omc-env");
10
10
  return existsSync(projectEnv) ? resolve(process.cwd(), ".omc", "usage.jsonl") : undefined;
11
11
  }
12
+ /** The command string of the in-flight invocation (for diagnostics). */
13
+ export function currentUsageCommand() {
14
+ return pending?.command;
15
+ }
16
+ /**
17
+ * How many consecutive PRIOR invocations failed the same way. A refusal tells
18
+ * the caller what is wrong; three identical refusals in a row mean the caller is
19
+ * tuning arguments inside a dead end instead of changing approach (r6-onboarding
20
+ * 2026-10-21: 16 such runs, 43 calls, 27 of them avoidable).
21
+ */
22
+ export function priorRepeatedFailures(command, blocker, limit = 40) {
23
+ if (!command)
24
+ return { count: 0, requestIds: [] };
25
+ const path = logPath();
26
+ if (!path)
27
+ return { count: 0, requestIds: [] };
28
+ // Read only the tail: this runs on every refusal, and the log grows with the
29
+ // session (r6: 1528 records). Parsing the last ~64 KB is enough for `limit`.
30
+ const tail = readUsageTail(path, 65536).slice(-limit);
31
+ const matches = (record) => {
32
+ if (String(record.command ?? "") !== command)
33
+ return false;
34
+ const status = String(record.status ?? "");
35
+ if (status === "ok" || status === "dry-run" || status === "no-envelope")
36
+ return false;
37
+ if (blocker === undefined)
38
+ return true;
39
+ return String(record.blocker ?? "") === blocker;
40
+ };
41
+ let count = 0;
42
+ const requestIds = [];
43
+ for (let index = tail.length - 1; index >= 0; index -= 1) {
44
+ if (!matches(tail[index]))
45
+ break;
46
+ count += 1;
47
+ const requestId = tail[index].requestId;
48
+ if (typeof requestId === "string" && requestIds.length < 3)
49
+ requestIds.push(requestId);
50
+ }
51
+ return { count, requestIds };
52
+ }
12
53
  function commandOf(argv) {
13
54
  const values = [];
14
55
  for (let index = 2; index < argv.length && values.length < 2; index += 1) {
@@ -91,6 +132,12 @@ export function recordUsageOutput(value, stdoutBytes, fileBytes = 0) {
91
132
  pending.status = String(envelope.status ?? "unknown");
92
133
  if (typeof error?.code === "string")
93
134
  pending.errorCode = error.code;
135
+ // The blocker is the actionable half of a refusal (policy-refused alone says
136
+ // nothing); record it so repeat detection and accounting can group by cause.
137
+ const resultEnvelope = envelope.result && typeof envelope.result === "object" ? envelope.result : undefined;
138
+ const blocker = typeof resultEnvelope?.blocker === "string" ? resultEnvelope.blocker : typeof envelope.blocker === "string" ? envelope.blocker : undefined;
139
+ if (blocker)
140
+ pending.blocker = blocker;
94
141
  // 0.4.16:requestId 落盘(信封瘦身把它从 stdout 移除后,排障关联的唯一落点)
95
142
  if (typeof envelope.requestId === "string")
96
143
  pending.requestId = envelope.requestId;
@@ -118,10 +165,37 @@ export function completeUsage(exitCode) {
118
165
  status: current.status,
119
166
  exitCode,
120
167
  ...(current.errorCode ? { errorCode: current.errorCode } : {}),
168
+ ...(current.blocker ? { blocker: current.blocker } : {}),
121
169
  ...(current.requestId ? { requestId: current.requestId } : {}),
122
170
  };
123
171
  appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
124
172
  }
173
+ function readUsageTail(path, maxBytes) {
174
+ if (!existsSync(path))
175
+ return [];
176
+ const size = statSync(path).size;
177
+ const start = Math.max(0, size - maxBytes);
178
+ const fd = openSync(path, "r");
179
+ try {
180
+ const buffer = Buffer.alloc(size - start);
181
+ readSync(fd, buffer, 0, buffer.length, start);
182
+ const text = buffer.toString("utf8");
183
+ // Drop a leading partial line when the tail started mid-record.
184
+ const body = start > 0 ? text.slice(text.indexOf("\n") + 1) : text;
185
+ return body.split(/\r?\n/).filter(Boolean).flatMap((line) => {
186
+ try {
187
+ const value = JSON.parse(line);
188
+ return value && typeof value === "object" ? [value] : [];
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ });
194
+ }
195
+ finally {
196
+ closeSync(fd);
197
+ }
198
+ }
125
199
  function readUsage(path) {
126
200
  if (!existsSync(path))
127
201
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.67",
3
+ "version": "0.4.68",
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.67",
53
+ "@stalfh233/omc-fake-platform": "0.4.68",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.67",
55
+ "omc-core": "0.4.68",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",