@kylecheng3146/agent-ops 0.1.17 → 0.1.19
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/runtime/src/fs/managed-block.js +23 -10
- package/dist/runtime/src/install/ownership.js +4 -4
- package/dist/runtime/src/review/execute.js +13 -5
- package/dist/runtime/src/review/render.js +4 -1
- package/docs/en/guides/configuration.md +5 -2
- package/docs/zh-TW/guides/configuration.md +4 -2
- package/package.json +1 -1
|
@@ -2,6 +2,13 @@ import { AgentOpsError } from "./paths.js";
|
|
|
2
2
|
function escapeRegExp(value) {
|
|
3
3
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4
4
|
}
|
|
5
|
+
export function normalizeToLF(source) {
|
|
6
|
+
return source.replace(/\r\n/g, "\n");
|
|
7
|
+
}
|
|
8
|
+
/** Newline sequence to use for newly rendered content, inferred from the file. */
|
|
9
|
+
function detectNewline(source) {
|
|
10
|
+
return source.includes("\r\n") ? "\r\n" : "\n";
|
|
11
|
+
}
|
|
5
12
|
function assertBlockId(id) {
|
|
6
13
|
if (!/^[a-z][a-z0-9-]{0,127}$/.test(id)) {
|
|
7
14
|
throw new AgentOpsError("INVALID_BLOCK_ID", `Invalid block ID: ${id}`);
|
|
@@ -70,25 +77,30 @@ function locateMarkers(source, id, version, markerStyle = "html") {
|
|
|
70
77
|
}
|
|
71
78
|
return { start, end, startIndex, endIndex };
|
|
72
79
|
}
|
|
73
|
-
function renderBlock(options) {
|
|
80
|
+
function renderBlock(options, nl) {
|
|
74
81
|
if (/(?:<!--|#)\s*agent-ops:/.test(options.content)) {
|
|
75
82
|
throw new AgentOpsError("AMBIGUOUS_MANAGED_CONTENT", "Managed content must not contain agent-ops marker boundaries.");
|
|
76
83
|
}
|
|
77
84
|
const { start, end } = managedBlockMarkers(options.id, options.version, options.markerStyle);
|
|
78
|
-
const content = options.content
|
|
79
|
-
|
|
85
|
+
const content = normalizeToLF(options.content)
|
|
86
|
+
.replace(/\n+$/g, "")
|
|
87
|
+
.split("\n")
|
|
88
|
+
.join(nl);
|
|
89
|
+
return `${start}${nl}${content}${nl}${end}`;
|
|
80
90
|
}
|
|
81
91
|
export function applyManagedBlock(source, options) {
|
|
82
|
-
const
|
|
92
|
+
const nl = detectNewline(source);
|
|
93
|
+
const block = renderBlock(options, nl);
|
|
83
94
|
const located = locateMarkers(source, options.id, options.version, options.markerStyle);
|
|
84
95
|
if (located === null) {
|
|
85
96
|
if (source.length === 0) {
|
|
86
|
-
return `${block}
|
|
97
|
+
return `${block}${nl}`;
|
|
87
98
|
}
|
|
88
|
-
return `${source.replace(
|
|
99
|
+
return `${source.replace(/(?:\r\n|\n)*$/, `${nl}${nl}`)}${block}${nl}`;
|
|
89
100
|
}
|
|
90
101
|
return `${source.slice(0, located.startIndex)}${block}${source.slice(located.endIndex + located.end.length)}`;
|
|
91
102
|
}
|
|
103
|
+
const NEWLINE_STYLES = ["\r\n", "\n"];
|
|
92
104
|
export function removeManagedBlock(source, id, markerStyle = "html") {
|
|
93
105
|
const located = locateMarkers(source, id, undefined, markerStyle);
|
|
94
106
|
if (located === null) {
|
|
@@ -96,12 +108,13 @@ export function removeManagedBlock(source, id, markerStyle = "html") {
|
|
|
96
108
|
}
|
|
97
109
|
let before = source.slice(0, located.startIndex);
|
|
98
110
|
let after = source.slice(located.endIndex + located.end.length);
|
|
99
|
-
if (before.length === 0 && after ===
|
|
111
|
+
if (before.length === 0 && NEWLINE_STYLES.some((nl) => after === nl)) {
|
|
100
112
|
return "";
|
|
101
113
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
const collapseNl = NEWLINE_STYLES.find((nl) => before.endsWith(`${nl}${nl}`) && after.startsWith(nl));
|
|
115
|
+
if (collapseNl !== undefined) {
|
|
116
|
+
before = before.slice(0, -collapseNl.length);
|
|
117
|
+
after = after.slice(collapseNl.length);
|
|
105
118
|
}
|
|
106
119
|
return `${before}${after}`;
|
|
107
120
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
|
|
1
|
+
import { applyManagedBlock, managedBlockMarkers, normalizeToLF } from "../fs/managed-block.js";
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
3
|
import { harnessDescriptor, harnessHookPath, routingBlockId, selectHarnessHookSurface, rulesArtifactId } from "./harness.js";
|
|
4
4
|
import { isOpencodePluginPath } from "../adapters/opencode/config.js";
|
|
@@ -211,18 +211,18 @@ export function assertExpectedManagedBlock(source, marker, expected) {
|
|
|
211
211
|
startIndex >= endIndex) {
|
|
212
212
|
throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block boundaries changed after installation: ${marker.path}`);
|
|
213
213
|
}
|
|
214
|
-
const currentBlock = source.slice(startIndex, endIndex + marker.endMarker.length);
|
|
214
|
+
const currentBlock = normalizeToLF(source.slice(startIndex, endIndex + marker.endMarker.length));
|
|
215
215
|
const candidates = [
|
|
216
216
|
["desired", expected.content],
|
|
217
217
|
...expected.legacyContent.map((content) => ["legacy", content])
|
|
218
218
|
];
|
|
219
219
|
for (const [kind, content] of candidates) {
|
|
220
|
-
const expectedBlock = applyManagedBlock("", {
|
|
220
|
+
const expectedBlock = normalizeToLF(applyManagedBlock("", {
|
|
221
221
|
id: expected.id,
|
|
222
222
|
version: 1,
|
|
223
223
|
content,
|
|
224
224
|
markerStyle: expected.markerStyle
|
|
225
|
-
}).replace(/\n$/u, "");
|
|
225
|
+
}).replace(/\n$/u, ""));
|
|
226
226
|
if (currentBlock === expectedBlock) {
|
|
227
227
|
return kind;
|
|
228
228
|
}
|
|
@@ -103,6 +103,17 @@ function firstComplaint(...streams) {
|
|
|
103
103
|
}
|
|
104
104
|
return undefined;
|
|
105
105
|
}
|
|
106
|
+
function rejectedCallReason(output) {
|
|
107
|
+
if (/\b(?:quota|rate limit|usage limit|too many requests)\b/iu.test(output)) {
|
|
108
|
+
return "quota-exhausted";
|
|
109
|
+
}
|
|
110
|
+
if (/\b(?:not logged in|login required|log in to|authentication required|unauthenticated|unauthorized)\b/iu.test(output) ||
|
|
111
|
+
/\b(?:invalid|expired)\s+(?:api key|token|credential)/iu.test(output) ||
|
|
112
|
+
/\b401\b/u.test(output)) {
|
|
113
|
+
return "login-required";
|
|
114
|
+
}
|
|
115
|
+
return "capability-unavailable";
|
|
116
|
+
}
|
|
106
117
|
async function snapshotRepository(request, destination, options) {
|
|
107
118
|
const cloned = await runVerificationCommand({
|
|
108
119
|
id: `review-snapshot-${request.label}`,
|
|
@@ -256,11 +267,8 @@ async function attemptTarget(request, options) {
|
|
|
256
267
|
return skip("output-too-large", `${spawned.stdoutTruncated ? "stdout" : "stderr"} exceeded the capture limit`);
|
|
257
268
|
}
|
|
258
269
|
if (spawned.failureClass === "nonzero-exit") {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// the reader to `doctor --check-auth` for a problem it cannot see. The
|
|
262
|
-
// target's own first line of complaint distinguishes the two.
|
|
263
|
-
return skip("login-required", firstComplaint(spawned.stderr, spawned.stdout) ??
|
|
270
|
+
const output = `${spawned.stderr}\n${spawned.stdout}`;
|
|
271
|
+
return skip(rejectedCallReason(output), firstComplaint(spawned.stderr, spawned.stdout) ??
|
|
264
272
|
`the call was rejected with exit ${spawned.exitCode ?? "unknown"} and no output`);
|
|
265
273
|
}
|
|
266
274
|
const payload = extractReviewObject(target, spawned.stdout);
|
|
@@ -40,7 +40,10 @@ export function renderReviewResult(result) {
|
|
|
40
40
|
for (const error of result.validationErrors ?? []) {
|
|
41
41
|
lines.push(`- ${safe(error.path)}: ${safe(error.code)} — ${safe(error.message)}`);
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
if (result.reason === "login-required" ||
|
|
44
|
+
result.attempts?.some((attempt) => attempt.reason === "login-required")) {
|
|
45
|
+
lines.push("Run: agent-ops doctor --check-auth to verify target authentication.");
|
|
46
|
+
}
|
|
44
47
|
return `${lines.join("\n")}\n`;
|
|
45
48
|
}
|
|
46
49
|
const report = result.report;
|
|
@@ -94,8 +94,11 @@ human-readable report is transient. Completed tasks are never rewritten.
|
|
|
94
94
|
`--yes` is still required for every review run: init selection decides which
|
|
95
95
|
targets are permitted, `--yes` decides whether to spend money now.
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
Reviewer failures are classified conservatively from their output: recognizable
|
|
98
|
+
authentication messages become `login-required`, quota messages become
|
|
99
|
+
`quota-exhausted`, and other non-zero exits become `capability-unavailable`.
|
|
100
|
+
The review suggests `doctor --check-auth` only when an authentication failure
|
|
101
|
+
was detected. Confirm target authentication with:
|
|
99
102
|
|
|
100
103
|
```bash
|
|
101
104
|
agent-ops doctor # presence only: no tokens, no network
|
|
@@ -82,8 +82,10 @@ policy 設定仍相同的 task;`--criterion` 用來篩選 id。請先執行
|
|
|
82
82
|
每次執行 review 仍需 `--yes`:init 的勾選決定「允許哪些目標」,
|
|
83
83
|
`--yes` 決定「現在是否要花錢」。
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
Reviewer 失敗會依輸出採保守分類:可辨識的認證訊息為 `login-required`,
|
|
86
|
+
quota 訊息為 `quota-exhausted`,其他非零退出則為 `capability-unavailable`。
|
|
87
|
+
只有偵測到認證失敗時,review 才會建議執行 `doctor --check-auth`。
|
|
88
|
+
可用以下指令確認目標的認證狀態:
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
91
|
agent-ops doctor # 只驗執行檔存在:零 token、零網路
|