@kylecheng3146/agent-ops 0.11.4 → 0.11.5

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.
@@ -317,6 +317,7 @@ const SURFACE_REASONS = {
317
317
  SURFACE_UNCLASSIFIED: "surface-unclassified",
318
318
  SURFACE_QUERY_TOO_BROAD: "surface-query-too-broad",
319
319
  SURFACE_EXCLUSION_TOO_BROAD: "surface-exclusion-too-broad",
320
+ SURFACE_INCLUSION_TOO_BROAD: "surface-inclusion-too-broad",
320
321
  SCENARIO_MISSING: "scenario-missing",
321
322
  SURFACE_BUDGET_EXCEEDED: "surface-budget-exceeded",
322
323
  SURFACE_UNASSIGNED: "surface-unassigned",
@@ -108,6 +108,8 @@ function taskDataBlock(invocation) {
108
108
  function surfaceBlock(invocation, round) {
109
109
  if (invocation.packet.surface === undefined)
110
110
  return [];
111
+ const discovery = invocation.packet.surface.discovery;
112
+ const skipped = round === "adversarial" ? discovery?.skipped ?? [] : [];
111
113
  return [
112
114
  "",
113
115
  "Surface coverage (narrow exception to \"Do not survey\"): task data carries the surface audit, its revision " +
@@ -121,7 +123,10 @@ function surfaceBlock(invocation, round) {
121
123
  "and indirect consumer. Do not run other searches beyond callers a specific question needs."
122
124
  : "Round 2: check the five fixed scenarios (initialState, redo, timeBoundary, identitySwitch, concurrency) against " +
123
125
  "the code, and hunt unlisted surfaces with at most 3 new `git grep -F` queries per axis (dependents, duplicates, " +
124
- "otherPaths, sharedState); cite each query you ran in evidence."
126
+ "otherPaths, sharedState); cite each query you ran in evidence.",
127
+ ...(skipped.length === 0 ? [] : ["Skipped derived queries (over 10 files outside the diff, so discovery never gated them; " +
128
+ `fewest first): ${skipped.map(row => `\`${row.query}\` (${row.files})`).join(", ")}. Beyond the per-axis budget, run at most 3 ` +
129
+ "extra `git grep -F` queries in total over these to find copied siblings the change missed; cite each in evidence."])
125
130
  ];
126
131
  }
127
132
  /** Escape Loop: both rounds adjudicate the earliest-gate label of each declared escape. */
@@ -11,8 +11,8 @@ import { globMatcher, isCovers, missingScenarios, parseSurface, SURFACE_AXES } f
11
11
  /** Cheapest first: a budget overrun stops before the long tail of diff tokens. */
12
12
  export const DISCOVERY_SOURCES = ["agent", "hunk-header", "template-fragment", "diff-token"];
13
13
  export const SURFACE_LIMITS = {
14
- // ponytail: derived queries over 10 files are skipped as generic; incidental ones under that are dismissed by query.
15
- agentQueryFiles: 200, derivedQueryFiles: 10, exclusionFiles: 10, dismissals: 128, dismissMinFiles: 4, budgetMs: 120_000, queryTimeoutMs: 30_000,
14
+ // ponytail: derived queries over 10 files outside the diff are skipped as generic; incidental ones under that are dismissed by query.
15
+ agentQueryFiles: 200, derivedQueryFiles: 10, exclusionFiles: 10, inclusionFiles: 10, dismissals: 128, dismissMinFiles: 4, budgetMs: 120_000, queryTimeoutMs: 30_000,
16
16
  tokenLength: 6, fragmentLength: 4, derivedQueries: 400
17
17
  };
18
18
  /** argv only: queries are task text and must never meet a shell or a regex engine. */
@@ -142,14 +142,23 @@ export async function runSurfaceGate(input) {
142
142
  const tracked = await git(input.root, ["ls-files", "-z"], Math.min(SURFACE_LIMITS.queryTimeoutMs, Math.max(1, remaining())));
143
143
  if (tracked.timedOut || remaining() <= 0)
144
144
  return notRun("SURFACE_BUDGET_EXCEEDED", "Discovery budget exhausted listing files.", []);
145
+ const trackedFiles = zList(tracked.stdout);
145
146
  const exclusions = surface.excluded.map(row => ({ ...row, match: globMatcher(row.glob) }));
146
147
  for (const row of exclusions) {
147
- const files = zList(tracked.stdout).filter(row.match);
148
+ const files = trackedFiles.filter(row.match);
148
149
  const diffFiles = [...changed].filter(row.match);
149
150
  if (files.length > SURFACE_LIMITS.exclusionFiles || diffFiles.length > 0)
150
151
  return notRun("SURFACE_EXCLUSION_TOO_BROAD", diffFiles.length > 0 ? "Files in the diff cannot be excluded."
151
152
  : `One exclusion may hide at most ${SURFACE_LIMITS.exclusionFiles} files.`, [{ glob: row.glob, files: files.length, diffFiles }]);
152
153
  }
154
+ // An included or UNKNOWN glob classifies every hit it matches, so it is capped like an exclusion.
155
+ for (const glob of [...surface.included.map(row => row.path), ...surface.unknown.flatMap(row => row.path === undefined ? [] : [row.path])]) {
156
+ const match = globMatcher(glob);
157
+ const files = trackedFiles.filter(path => match(path) && !changed.has(path)).length;
158
+ if (files > SURFACE_LIMITS.inclusionFiles)
159
+ return notRun("SURFACE_INCLUSION_TOO_BROAD", `One included or UNKNOWN glob may cover at most ${SURFACE_LIMITS.inclusionFiles} ` +
160
+ "files outside the diff; list narrower paths, each with its own behavior.", [{ glob, files }]);
161
+ }
153
162
  const head = (await git(input.root, ["rev-parse", "HEAD"], SURFACE_LIMITS.queryTimeoutMs)).stdout.toString("utf8").trim();
154
163
  const diff = (await git(input.root, ["diff", "-U0", "--no-color", "--no-ext-diff", input.base ?? "HEAD", "--"], SURFACE_LIMITS.queryTimeoutMs)).stdout.toString("utf8");
155
164
  const derived = extractDiffQueries(diff);
@@ -217,7 +226,7 @@ export async function runSurfaceGate(input) {
217
226
  return notRun("SURFACE_DISMISSAL_INVALID", "A recorded axis query cannot be dismissed; classify its hits.", [{ query }]);
218
227
  const hit = hits.find(row => row.query === query && row.source !== "agent");
219
228
  const outside = hit === undefined ? [] : hit.files.filter(path => !changed.has(path));
220
- if (hit !== undefined && outside.length < SURFACE_LIMITS.dismissMinFiles && hit.files.length <= SURFACE_LIMITS.derivedQueryFiles)
229
+ if (hit !== undefined && outside.length < SURFACE_LIMITS.dismissMinFiles)
221
230
  return notRun("SURFACE_DISMISSAL_INVALID", `A query reaching fewer than ${SURFACE_LIMITS.dismissMinFiles} files outside the diff ` +
222
231
  "is the likeliest sibling signal and cannot be dismissed; classify its files.", [{ query, files: outside }]);
223
232
  if (hit !== undefined)
@@ -227,8 +236,9 @@ export async function runSurfaceGate(input) {
227
236
  sources[hit.source].queries += 1;
228
237
  if (hit.source === "agent" && hit.files.length > SURFACE_LIMITS.agentQueryFiles)
229
238
  return notRun("SURFACE_QUERY_TOO_BROAD", `Query hits more than ${SURFACE_LIMITS.agentQueryFiles} files; record a more specific fixed string.`, [{ query: hit.query, files: hit.files.length }]);
230
- if (hit.source !== "agent" && hit.files.length > SURFACE_LIMITS.derivedQueryFiles) {
231
- skipped.push({ source: hit.source, query: hit.query, files: hit.files.length });
239
+ const outside = hit.files.filter(path => !changed.has(path)).length;
240
+ if (hit.source !== "agent" && outside > SURFACE_LIMITS.derivedQueryFiles) {
241
+ skipped.push({ source: hit.source, query: hit.query, files: outside });
232
242
  continue;
233
243
  }
234
244
  if (hit.source !== "agent" && dismissals.has(hit.query))
@@ -284,7 +294,8 @@ export async function runSurfaceGate(input) {
284
294
  return notRun("ESCAPE_RULE_NO_EXCLUDE", "A learned escape rule marks these places noExclude: include them or mark them UNKNOWN.", noExclude);
285
295
  for (const source of DISCOVERY_SOURCES)
286
296
  sources[source].files = metrics.get(source).hits.size;
287
- const discovery = { cached, sources, skipped: skipped.slice(0, 50), postBlockExclusions, dismissed };
297
+ const discovery = { cached, sources, skipped: skipped
298
+ .sort((a, b) => a.files - b.files || (a.query < b.query ? -1 : a.query > b.query ? 1 : 0)).slice(0, 50), postBlockExclusions, dismissed };
288
299
  if (input.metrics === true)
289
300
  await logMetrics(input.logHome ?? process.env.AGENT_OPS_HOME ?? homedir(), DISCOVERY_SOURCES.map(source => ({ source,
290
301
  hits: metrics.get(source).hits.size, included: metrics.get(source).included.size, excluded: metrics.get(source).excluded.size,
@@ -99,6 +99,9 @@ Every new task MUST record a surface audit of where else its change lands before
99
99
  `glob` and a reason saying why the surface is unaffected ("out of scope" is
100
100
  not a reason); `unknown` rows name a reader the repository cannot see.
101
101
  `contracts` declare `compatible` or `breaking`. A single surface is allowed.
102
+ An `included` or `unknown` path glob may cover at most 10 tracked files
103
+ outside the diff, like an exclusion; list narrower paths, each with its own
104
+ behavior.
102
105
  - `valueChanges` `[{producer, kind, change, consumers, indirectConsumers}]`
103
106
  list two hops whenever a value's semantics change (clear, redact, default,
104
107
  normalize, unit/format).
@@ -114,8 +117,9 @@ Every new task MUST record a surface audit of where else its change lands before
114
117
  template-literal fixed fragments (at least 4 characters, separator-bounded,
115
118
  such as `order:`, `user_`, `menu.`); compound identifiers (snake_case,
116
119
  camelCase or containing digits, so prose words are not queries) and string
117
- literals from added and removed lines (at least 6 characters; skipped as too
118
- generic above 10 file hits, changed files included). Every hit outside the diff and the surface lists stops review.
120
+ literals from added and removed lines (at least 6 characters). A derived
121
+ query is skipped as too generic above 10 hit files outside the diff; skipped
122
+ queries, fewest hits first, go to the second reviewer. Every hit outside the diff and the surface lists stops review.
119
123
  - A derived query (diff token, template fragment or hunk-header name) whose hits
120
124
  are incidental may be dismissed once in the surface's optional `dismissed`
121
125
  list (`{query, reason}`, reason at most 256 characters, "out of scope" is
@@ -132,7 +136,8 @@ Every new task MUST record a surface audit of where else its change lands before
132
136
  also refused at create), `SURFACE_UNCLASSIFIED`,
133
137
  `SURFACE_QUERY_TOO_BROAD` (one recorded query over 200 files),
134
138
  `SURFACE_EXCLUSION_TOO_BROAD` (one glob over 10 files, or any file in the
135
- diff), `SURFACE_DISMISSAL_TOO_BROAD` (over 128 dismissals),
139
+ diff), `SURFACE_INCLUSION_TOO_BROAD` (one included or UNKNOWN glob over 10
140
+ files outside the diff), `SURFACE_DISMISSAL_TOO_BROAD` (over 128 dismissals),
136
141
  `SURFACE_DISMISSAL_INVALID` (a recorded or rare query), `SCENARIO_MISSING`, `SURFACE_BUDGET_EXCEEDED` (120 s total, never
137
142
  silently skipped) or `SURFACE_UNASSIGNED` (children's covers differ from the
138
143
  parent's included list; checked by every review, including advance and batch).
@@ -108,7 +108,9 @@ successful refutation MUST make the run FAIL.
108
108
  session. A FAIL is already terminal and is never re-checked.
109
109
  - Note: For a surfaced task the second round also hunts unlisted surfaces and
110
110
  scenarios under `surface-coverage`: it checks the five fixed scenario
111
- dimensions and may run at most 3 new `git grep -F` queries per axis. Round 1
111
+ dimensions and may run at most 3 new `git grep -F` queries per axis, plus at
112
+ most 3 in total over the derived queries discovery skipped as too generic,
113
+ which the prompt names with their hit counts. Round 1
112
114
  replays the recorded queries and reads every listed `valueChanges` consumer;
113
115
  this is the only exception to "do not survey the repository".
114
116
 
@@ -62,7 +62,8 @@ English source version: 2026-10-20. Revalidate: when the English specification c
62
62
  - `included` 列含 `id`、`path` glob 與 `behavior`;`excluded` 列含 `glob` 與說明
63
63
  該 surface 為何不受影響的理由(「out of scope」不是理由);`unknown` 列記錄
64
64
  repository 看不到的讀取者。`contracts` 宣告 `compatible` 或 `breaking`。只有單一
65
- surface 也可以。
65
+ surface 也可以。`included` 或 `unknown` 的 path glob 與排除一樣,至多涵蓋 diff
66
+ 以外 10 個 tracked 檔案;請改列更窄的路徑,各自寫出 behavior。
66
67
  - 值的語意改變(clear、redact、default、normalize、unit/format)時,
67
68
  `valueChanges` `[{producer, kind, change, consumers, indirectConsumers}]` 列出兩層
68
69
  consumer。
@@ -75,8 +76,8 @@ English source version: 2026-10-20. Revalidate: when the English specification c
75
76
  不以 task 文字組 regex),由便宜的來源先跑:記錄的查詢;diff hunk header 的外層
76
77
  函式名;template literal 固定片段(至少 4 字元且以分隔符界定,例如 `order:`、
77
78
  `user_`、`menu.`);新增與刪除行的複合識別字(snake_case、camelCase 或含數字,
78
- 因此一般文字單字不會成為查詢)與字串常值(至少 6 字元;命中超過 10 個檔案〈含
79
- 變更檔案〉視為太通用而略過)。diff 與 surface 清單之外的任何命中都會擋下 review。
79
+ 因此一般文字單字不會成為查詢)與字串常值(至少 6 字元)。衍生查詢在 diff 以外命中
80
+ 超過 10 個檔案時視為太通用而略過;略過的查詢依命中數由少到多交給第二輪 reviewer。diff 與 surface 清單之外的任何命中都會擋下 review。
80
81
  - 衍生查詢(diff 字詞、template 片段或 hunk header 函式名)的命中若只是巧合,可在
81
82
  surface 選用的 `dismissed` 清單中以一筆 `{query, reason}` 排除(理由至多 256
82
83
  字元,「out of scope」不是理由),不必逐檔排除。記錄的軸查詢與 escape rule 命中
@@ -88,7 +89,8 @@ English source version: 2026-10-20. Revalidate: when the English specification c
88
89
  - Review 以 NOT_RUN 回報:`SURFACE_MISSING`(沒有或無法解析的 surface;
89
90
  task 建立時也會拒絕)、`SURFACE_UNCLASSIFIED`、`SURFACE_QUERY_TOO_BROAD`(單一
90
91
  記錄查詢超過 200 個檔案)、`SURFACE_EXCLUSION_TOO_BROAD`(單一 glob 超過 10 個
91
- 檔案,或包含 diff 內的檔案)、`SURFACE_DISMISSAL_TOO_BROAD`(超過 128 筆排除)、
92
+ 檔案,或包含 diff 內的檔案)、`SURFACE_INCLUSION_TOO_BROAD`(單一 included 或
93
+ UNKNOWN glob 涵蓋 diff 以外超過 10 個檔案)、`SURFACE_DISMISSAL_TOO_BROAD`(超過 128 筆排除)、
92
94
  `SURFACE_DISMISSAL_INVALID`(記錄的查詢或稀有查詢)、`SCENARIO_MISSING`、`SURFACE_BUDGET_EXCEEDED`
93
95
  (總計 120 秒,絕不默默略過)或 `SURFACE_UNASSIGNED`(子 task covers 與父 task
94
96
  included 不一致;每次 review 都檢查,包含 advance 與 batch)。
@@ -98,7 +98,7 @@ PASS MUST 交給第二個規劃的 target 嘗試反駁,且反駁成立時 MUST
98
98
  - Positive: `codex 判 PASS,claude 找到 blocking 缺陷,整體判定 FAIL。`
99
99
  - Negative: `為了讓複查看起來有效而編造反駁。`
100
100
  - Note: 只有一個設定 target 時,會在全新 session 再呼叫同一 target。FAIL 已是終局,不再複查。
101
- - Note: 有 surface 的 task,第二輪也會在 `surface-coverage` 下搜尋未列出的 surface 與情境:檢查五個固定情境維度,每個軸向最多可新增 3 個 `git grep -F` 查詢。第一輪重放記錄的查詢並閱讀每個列出的 `valueChanges` consumer;這是「不要普查 repository」唯一的例外。
101
+ - Note: 有 surface 的 task,第二輪也會在 `surface-coverage` 下搜尋未列出的 surface 與情境:檢查五個固定情境維度,每個軸向最多可新增 3 個 `git grep -F` 查詢,另外可針對 discovery 因太通用而略過的衍生查詢(prompt 會列出它們與命中數)總計最多再跑 3 個。第一輪重放記錄的查詢並閱讀每個列出的 `valueChanges` consumer;這是「不要普查 repository」唯一的例外。
102
102
 
103
103
  ## REVIEW-ORIGIN-001
104
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
4
4
  "description": "Evidence-driven development loops for agy, Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",