@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.
- package/dist/packages/cli/src/commands/review.js +1 -0
- package/dist/runtime/src/review/runner.js +6 -1
- package/dist/runtime/src/surface/discovery.js +18 -7
- package/docs/en/spec/acceptance-and-evidence.md +8 -3
- package/docs/en/spec/review.md +3 -1
- package/docs/zh-TW/spec/acceptance-and-evidence.md +6 -4
- package/docs/zh-TW/spec/review.md +1 -1
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
118
|
-
|
|
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), `
|
|
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).
|
package/docs/en/spec/review.md
CHANGED
|
@@ -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
|
|
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
|
|
79
|
-
|
|
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 內的檔案)、`
|
|
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`
|
|
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