@kylecheng3146/agent-ops 0.0.1

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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/SECURITY.md +22 -0
  4. package/dist/packages/cli/src/args.js +322 -0
  5. package/dist/packages/cli/src/bin.js +290 -0
  6. package/dist/packages/cli/src/cli.js +80 -0
  7. package/dist/packages/cli/src/commands/config.js +8 -0
  8. package/dist/packages/cli/src/commands/doctor.js +32 -0
  9. package/dist/packages/cli/src/commands/index.js +16 -0
  10. package/dist/packages/cli/src/commands/init.js +65 -0
  11. package/dist/packages/cli/src/commands/review.js +71 -0
  12. package/dist/packages/cli/src/commands/task.js +141 -0
  13. package/dist/packages/cli/src/commands/trust.js +48 -0
  14. package/dist/packages/cli/src/commands/uninstall.js +58 -0
  15. package/dist/packages/cli/src/commands/update.js +58 -0
  16. package/dist/packages/cli/src/commands/verify.js +108 -0
  17. package/dist/packages/cli/src/output.js +53 -0
  18. package/dist/packages/cli/src/plan-output.js +28 -0
  19. package/dist/packages/cli/src/wizard.js +75 -0
  20. package/dist/runtime/src/adapters/claude/config.js +109 -0
  21. package/dist/runtime/src/adapters/claude/events.js +8 -0
  22. package/dist/runtime/src/adapters/claude/input.js +38 -0
  23. package/dist/runtime/src/adapters/claude/output.js +30 -0
  24. package/dist/runtime/src/adapters/codex/config.js +93 -0
  25. package/dist/runtime/src/adapters/codex/events.js +8 -0
  26. package/dist/runtime/src/adapters/codex/input.js +41 -0
  27. package/dist/runtime/src/adapters/codex/output.js +24 -0
  28. package/dist/runtime/src/config/explain.js +34 -0
  29. package/dist/runtime/src/config/load.js +25 -0
  30. package/dist/runtime/src/config/merge.js +170 -0
  31. package/dist/runtime/src/config/migrate.js +61 -0
  32. package/dist/runtime/src/contracts.js +1 -0
  33. package/dist/runtime/src/discovery/go.js +145 -0
  34. package/dist/runtime/src/discovery/index.js +40 -0
  35. package/dist/runtime/src/discovery/make.js +162 -0
  36. package/dist/runtime/src/discovery/node.js +175 -0
  37. package/dist/runtime/src/discovery/python.js +163 -0
  38. package/dist/runtime/src/discovery/rust.js +159 -0
  39. package/dist/runtime/src/discovery/types.js +1 -0
  40. package/dist/runtime/src/fs/hash.js +19 -0
  41. package/dist/runtime/src/fs/managed-block.js +90 -0
  42. package/dist/runtime/src/fs/manifest.js +24 -0
  43. package/dist/runtime/src/fs/mutation-worker.js +185 -0
  44. package/dist/runtime/src/fs/paths.js +96 -0
  45. package/dist/runtime/src/fs/transaction.js +498 -0
  46. package/dist/runtime/src/guardrails/destructive.js +207 -0
  47. package/dist/runtime/src/guardrails/evaluate.js +9 -0
  48. package/dist/runtime/src/guardrails/exceptions.js +49 -0
  49. package/dist/runtime/src/guardrails/secrets.js +97 -0
  50. package/dist/runtime/src/guardrails/types.js +9 -0
  51. package/dist/runtime/src/hooks/dispatch.js +78 -0
  52. package/dist/runtime/src/hooks/events.js +1 -0
  53. package/dist/runtime/src/hooks/hook-entry.js +19 -0
  54. package/dist/runtime/src/hooks/normalize.js +59 -0
  55. package/dist/runtime/src/hooks/output.js +12 -0
  56. package/dist/runtime/src/hooks/shell.js +138 -0
  57. package/dist/runtime/src/hooks/stop-verify.js +70 -0
  58. package/dist/runtime/src/install/apply.js +70 -0
  59. package/dist/runtime/src/install/doctor.js +196 -0
  60. package/dist/runtime/src/install/harness.js +87 -0
  61. package/dist/runtime/src/install/ownership.js +84 -0
  62. package/dist/runtime/src/install/plan.js +257 -0
  63. package/dist/runtime/src/install/profiles.js +28 -0
  64. package/dist/runtime/src/install/types.js +1 -0
  65. package/dist/runtime/src/install/uninstall.js +206 -0
  66. package/dist/runtime/src/install/update.js +123 -0
  67. package/dist/runtime/src/logging/local-log.js +158 -0
  68. package/dist/runtime/src/registry/npm.js +141 -0
  69. package/dist/runtime/src/review/claude-runner.js +4 -0
  70. package/dist/runtime/src/review/codex-runner.js +4 -0
  71. package/dist/runtime/src/review/packet.js +10 -0
  72. package/dist/runtime/src/review/result.js +24 -0
  73. package/dist/runtime/src/review/roles.js +3 -0
  74. package/dist/runtime/src/review/runner.js +45 -0
  75. package/dist/runtime/src/schema/validate.js +584 -0
  76. package/dist/runtime/src/security/permissions.js +654 -0
  77. package/dist/runtime/src/security/redact.js +41 -0
  78. package/dist/runtime/src/security/trust.js +209 -0
  79. package/dist/runtime/src/task/render.js +43 -0
  80. package/dist/runtime/src/task/service.js +235 -0
  81. package/dist/runtime/src/task/store.js +265 -0
  82. package/dist/runtime/src/verify/change-surface.js +86 -0
  83. package/dist/runtime/src/verify/evidence.js +89 -0
  84. package/dist/runtime/src/verify/fingerprint.js +67 -0
  85. package/dist/runtime/src/verify/scope.js +69 -0
  86. package/dist/runtime/src/verify/service.js +217 -0
  87. package/dist/runtime/src/verify/spawn.js +326 -0
  88. package/dist/runtime/src/verify/test-count.js +148 -0
  89. package/docs/en/spec/README.md +13 -0
  90. package/docs/en/spec/acceptance-and-evidence.md +21 -0
  91. package/docs/en/spec/delegation.md +21 -0
  92. package/docs/en/spec/guardrails.md +21 -0
  93. package/docs/en/spec/harness-adapters.md +21 -0
  94. package/docs/en/spec/judgment.md +21 -0
  95. package/docs/en/spec/loop-engineering.md +23 -0
  96. package/docs/en/spec/maintenance.md +21 -0
  97. package/docs/en/spec/review.md +21 -0
  98. package/docs/en/spec/troubleshooting.md +21 -0
  99. package/docs/zh-TW/spec/README.md +13 -0
  100. package/docs/zh-TW/spec/acceptance-and-evidence.md +23 -0
  101. package/docs/zh-TW/spec/delegation.md +23 -0
  102. package/docs/zh-TW/spec/guardrails.md +23 -0
  103. package/docs/zh-TW/spec/harness-adapters.md +23 -0
  104. package/docs/zh-TW/spec/judgment.md +23 -0
  105. package/docs/zh-TW/spec/loop-engineering.md +23 -0
  106. package/docs/zh-TW/spec/maintenance.md +23 -0
  107. package/docs/zh-TW/spec/review.md +23 -0
  108. package/docs/zh-TW/spec/troubleshooting.md +23 -0
  109. package/package.json +41 -0
  110. package/schemas/config.schema.json +231 -0
  111. package/schemas/evidence.schema.json +115 -0
  112. package/schemas/manifest.schema.json +116 -0
  113. package/schemas/task.schema.json +59 -0
  114. package/templates/common/AGENTS.block.md +3 -0
  115. package/templates/common/CLAUDE.block.md +3 -0
@@ -0,0 +1,265 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ import { validateTask } from "../schema/validate.js";
4
+ import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../security/permissions.js";
5
+ const DEFAULT_MAX_BYTES = 1024 * 1024;
6
+ const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
7
+ function isMissing(error) {
8
+ return (typeof error === "object" &&
9
+ error !== null &&
10
+ "code" in error &&
11
+ error.code === "ENOENT");
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function hasExactKeys(value, expected) {
17
+ const actual = Object.keys(value).sort();
18
+ const sortedExpected = [...expected].sort();
19
+ return (actual.length === sortedExpected.length &&
20
+ sortedExpected.every((key, index) => actual[index] === key));
21
+ }
22
+ function isTimestamp(value) {
23
+ return (typeof value === "string" &&
24
+ value.length > 0 &&
25
+ value.length <= 64 &&
26
+ Number.isFinite(Date.parse(value)));
27
+ }
28
+ function invalidState(message) {
29
+ throw new AgentOpsError("TASK_STATE_INVALID", message);
30
+ }
31
+ function parseEvidence(value, task, requireComplete) {
32
+ if (!isRecord(value)) {
33
+ return invalidState("Task evidence must be an object.");
34
+ }
35
+ const criterionIds = new Set(task.criteria.map((criterion) => criterion.id));
36
+ const evidence = {};
37
+ for (const [criterionId, references] of Object.entries(value)) {
38
+ if (!criterionIds.has(criterionId) ||
39
+ !Array.isArray(references) ||
40
+ references.length === 0 ||
41
+ references.some((reference) => typeof reference !== "string" ||
42
+ reference.length === 0 ||
43
+ reference.length > 4096 ||
44
+ reference.includes("\0")) ||
45
+ new Set(references).size !== references.length) {
46
+ return invalidState("Task evidence references are invalid.");
47
+ }
48
+ evidence[criterionId] = [...references];
49
+ }
50
+ if (requireComplete &&
51
+ task.criteria.some((criterion) => evidence[criterion.id] === undefined)) {
52
+ return invalidState("Completed task state requires evidence for every criterion.");
53
+ }
54
+ return evidence;
55
+ }
56
+ function parseTaskRecord(value) {
57
+ const baseKeys = [
58
+ "archivedAt",
59
+ "completedAt",
60
+ "createdAt",
61
+ "evidence",
62
+ "status",
63
+ "task",
64
+ "updatedAt"
65
+ ];
66
+ if (!isRecord(value) ||
67
+ (!hasExactKeys(value, baseKeys) &&
68
+ !hasExactKeys(value, [...baseKeys, "failureFingerprint"]))) {
69
+ return invalidState("Task state contains an invalid task record.");
70
+ }
71
+ const task = validateTask(value.task);
72
+ if (!task.ok) {
73
+ return invalidState("Task state contains an invalid task.");
74
+ }
75
+ if (!["active", "archived", "complete"].includes(String(value.status)) ||
76
+ !isTimestamp(value.createdAt) ||
77
+ !isTimestamp(value.updatedAt) ||
78
+ (value.completedAt !== null && !isTimestamp(value.completedAt)) ||
79
+ (value.archivedAt !== null && !isTimestamp(value.archivedAt))) {
80
+ return invalidState("Task state contains invalid lifecycle metadata.");
81
+ }
82
+ const status = value.status;
83
+ if ((status === "active" &&
84
+ (value.completedAt !== null || value.archivedAt !== null)) ||
85
+ (status === "complete" &&
86
+ (value.completedAt === null || value.archivedAt !== null)) ||
87
+ (status === "archived" && value.archivedAt === null)) {
88
+ return invalidState("Task lifecycle timestamps do not match its status.");
89
+ }
90
+ const evidence = parseEvidence(value.evidence, task.value, status === "complete" ||
91
+ (status === "archived" && value.completedAt !== null));
92
+ let failureFingerprint = null;
93
+ if (value.failureFingerprint !== undefined &&
94
+ value.failureFingerprint !== null) {
95
+ const fingerprint = value.failureFingerprint;
96
+ if (!isRecord(fingerprint) ||
97
+ !hasExactKeys(fingerprint, [
98
+ "commandId",
99
+ "consecutive",
100
+ "diagnostics",
101
+ "exitCategory",
102
+ "failureClass",
103
+ "recordedAt",
104
+ "value"
105
+ ]) ||
106
+ typeof fingerprint.value !== "string" ||
107
+ !/^[a-f0-9]{64}$/u.test(fingerprint.value) ||
108
+ typeof fingerprint.commandId !== "string" ||
109
+ typeof fingerprint.failureClass !== "string" ||
110
+ typeof fingerprint.exitCategory !== "string" ||
111
+ typeof fingerprint.diagnostics !== "string" ||
112
+ fingerprint.diagnostics.includes("\0") ||
113
+ Buffer.byteLength(fingerprint.diagnostics, "utf8") > 512 ||
114
+ !Number.isSafeInteger(fingerprint.consecutive) ||
115
+ fingerprint.consecutive <= 0 ||
116
+ !isTimestamp(fingerprint.recordedAt)) {
117
+ return invalidState("Task state contains an invalid failure fingerprint.");
118
+ }
119
+ failureFingerprint = {
120
+ value: fingerprint.value,
121
+ commandId: fingerprint.commandId,
122
+ failureClass: fingerprint.failureClass,
123
+ exitCategory: fingerprint.exitCategory,
124
+ diagnostics: fingerprint.diagnostics,
125
+ consecutive: fingerprint.consecutive,
126
+ recordedAt: fingerprint.recordedAt
127
+ };
128
+ }
129
+ return {
130
+ task: task.value,
131
+ status,
132
+ evidence,
133
+ createdAt: value.createdAt,
134
+ updatedAt: value.updatedAt,
135
+ completedAt: value.completedAt,
136
+ archivedAt: value.archivedAt,
137
+ failureFingerprint
138
+ };
139
+ }
140
+ function parseSession(value) {
141
+ if (!isRecord(value) ||
142
+ !hasExactKeys(value, ["attachedAt", "sessionId", "taskId"]) ||
143
+ typeof value.sessionId !== "string" ||
144
+ !SESSION_ID_PATTERN.test(value.sessionId) ||
145
+ typeof value.taskId !== "string" ||
146
+ !isTimestamp(value.attachedAt)) {
147
+ return invalidState("Task state contains an invalid session attachment.");
148
+ }
149
+ return {
150
+ sessionId: value.sessionId,
151
+ taskId: value.taskId,
152
+ attachedAt: value.attachedAt
153
+ };
154
+ }
155
+ function parseState(source) {
156
+ if (source === null) {
157
+ return { schemaVersion: 1, tasks: [], sessions: [] };
158
+ }
159
+ let value;
160
+ try {
161
+ value = JSON.parse(source);
162
+ }
163
+ catch {
164
+ return invalidState("Task state is not valid JSON.");
165
+ }
166
+ if (!isRecord(value) ||
167
+ !hasExactKeys(value, ["schemaVersion", "sessions", "tasks"]) ||
168
+ value.schemaVersion !== 1 ||
169
+ !Array.isArray(value.tasks) ||
170
+ !Array.isArray(value.sessions)) {
171
+ return invalidState("Task state has an unsupported structure.");
172
+ }
173
+ const tasks = value.tasks.map(parseTaskRecord);
174
+ const sessions = value.sessions.map(parseSession);
175
+ if (new Set(tasks.map(({ task }) => task.id)).size !== tasks.length ||
176
+ new Set(sessions.map(({ sessionId }) => sessionId)).size !==
177
+ sessions.length) {
178
+ return invalidState("Task and session identifiers must be unique.");
179
+ }
180
+ const tasksById = new Map(tasks.map((record) => [record.task.id, record]));
181
+ if (sessions.some(({ taskId }) => {
182
+ const record = tasksById.get(taskId);
183
+ return record === undefined || record.status === "archived";
184
+ })) {
185
+ return invalidState("Session attachments must reference known tasks.");
186
+ }
187
+ return { schemaVersion: 1, tasks, sessions };
188
+ }
189
+ function positiveMaxBytes(value) {
190
+ if (value === undefined) {
191
+ return DEFAULT_MAX_BYTES;
192
+ }
193
+ if (!Number.isSafeInteger(value) || value <= 0) {
194
+ throw new AgentOpsError("TASK_STATE_LIMIT_INVALID", "Task state byte limit must be a positive integer.");
195
+ }
196
+ return value;
197
+ }
198
+ export class FileTaskStore {
199
+ #path;
200
+ #anchorDirectory;
201
+ #maxBytes;
202
+ constructor(path, anchorDirectory, options = {}) {
203
+ this.#path = path;
204
+ this.#anchorDirectory = anchorDirectory;
205
+ this.#maxBytes = positiveMaxBytes(options.maxBytes);
206
+ }
207
+ async #readState() {
208
+ let before = null;
209
+ try {
210
+ const status = await lstat(this.#path, { bigint: true });
211
+ if (!status.isFile() ||
212
+ status.isSymbolicLink() ||
213
+ status.size > BigInt(this.#maxBytes)) {
214
+ if (status.size > BigInt(this.#maxBytes)) {
215
+ throw new AgentOpsError("TASK_STATE_TOO_LARGE", "Task state exceeds its configured byte limit.");
216
+ }
217
+ throw new AgentOpsError("TASK_STATE_INVALID", "Task state must be a regular private file.");
218
+ }
219
+ before = { device: status.dev, inode: status.ino };
220
+ }
221
+ catch (error) {
222
+ if (!isMissing(error)) {
223
+ throw error;
224
+ }
225
+ }
226
+ const source = await readPrivateFile(this.#path, this.#anchorDirectory);
227
+ if (before === null && source !== null) {
228
+ throw new AgentOpsError("TASK_STATE_INVALID", "Task state appeared during inspection.");
229
+ }
230
+ if (before !== null) {
231
+ if (source === null) {
232
+ throw new AgentOpsError("TASK_STATE_INVALID", "Task state disappeared during inspection.");
233
+ }
234
+ const after = await lstat(this.#path, { bigint: true });
235
+ if (!after.isFile() ||
236
+ after.isSymbolicLink() ||
237
+ after.dev !== before.device ||
238
+ after.ino !== before.inode ||
239
+ after.size > BigInt(this.#maxBytes)) {
240
+ throw new AgentOpsError("TASK_STATE_INVALID", "Task state changed during inspection.");
241
+ }
242
+ }
243
+ if (source !== null &&
244
+ Buffer.byteLength(source, "utf8") > this.#maxBytes) {
245
+ throw new AgentOpsError("TASK_STATE_TOO_LARGE", "Task state exceeds its configured byte limit.");
246
+ }
247
+ return parseState(source);
248
+ }
249
+ async read() {
250
+ return await withPrivateFileLock(this.#path, this.#anchorDirectory, async () => structuredClone(await this.#readState()));
251
+ }
252
+ async mutate(action) {
253
+ return await withPrivateFileLock(this.#path, this.#anchorDirectory, async () => {
254
+ const state = await this.#readState();
255
+ const result = await action(state);
256
+ const validated = parseState(JSON.stringify(state));
257
+ const content = `${JSON.stringify(validated, null, 2)}\n`;
258
+ if (Buffer.byteLength(content, "utf8") > this.#maxBytes) {
259
+ throw new AgentOpsError("TASK_STATE_TOO_LARGE", "Task state exceeds its configured byte limit.");
260
+ }
261
+ await writePrivateFile(this.#path, content, this.#anchorDirectory);
262
+ return result;
263
+ });
264
+ }
265
+ }
@@ -0,0 +1,86 @@
1
+ import { TextDecoder } from "node:util";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ const WINDOWS_RESERVED_SEGMENT = /^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)(?:\..*)?$/i;
4
+ function sortedUnique(values) {
5
+ return [...new Set(values)].sort();
6
+ }
7
+ function normalizePortablePath(path) {
8
+ if (path.length === 0 ||
9
+ path.includes("\\") ||
10
+ path.startsWith("/") ||
11
+ /^[A-Za-z]:/.test(path)) {
12
+ throw new AgentOpsError("CHANGE_SURFACE_UNSAFE_PATH", `Git reported an unsafe path: ${path}`);
13
+ }
14
+ const normalizedSegments = [];
15
+ for (const segment of path.split("/")) {
16
+ if (segment.length === 0 || segment === ".") {
17
+ continue;
18
+ }
19
+ if (segment === ".." ||
20
+ segment.endsWith(".") ||
21
+ segment.endsWith(" ") ||
22
+ WINDOWS_RESERVED_SEGMENT.test(segment) ||
23
+ /[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/u
24
+ .test(segment)) {
25
+ throw new AgentOpsError("CHANGE_SURFACE_UNSAFE_PATH", `Git reported a path with an unsafe segment: ${path}`);
26
+ }
27
+ normalizedSegments.push(segment);
28
+ }
29
+ if (normalizedSegments.length === 0) {
30
+ throw new AgentOpsError("CHANGE_SURFACE_UNSAFE_PATH", `Git reported an empty normalized path: ${path}`);
31
+ }
32
+ return normalizedSegments.join("/");
33
+ }
34
+ function parseNulPaths(stdout) {
35
+ if (stdout.byteLength === 0) {
36
+ return [];
37
+ }
38
+ if (stdout[stdout.byteLength - 1] !== 0) {
39
+ throw new AgentOpsError("CHANGE_SURFACE_INVALID_OUTPUT", "Git path output must end with a NUL delimiter.");
40
+ }
41
+ let decoded;
42
+ try {
43
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(stdout);
44
+ }
45
+ catch (error) {
46
+ throw new AgentOpsError("CHANGE_SURFACE_INVALID_OUTPUT", "Git path output is not valid UTF-8.", { cause: error });
47
+ }
48
+ const entries = decoded.split("\0");
49
+ entries.pop();
50
+ if (entries.some((entry) => entry.length === 0)) {
51
+ throw new AgentOpsError("CHANGE_SURFACE_INVALID_OUTPUT", "Git path output contains an empty NUL-delimited entry.");
52
+ }
53
+ return sortedUnique(entries.map(normalizePortablePath));
54
+ }
55
+ async function collectPaths(runner, args) {
56
+ const result = await runner.run(args);
57
+ if (result.exitCode !== 0) {
58
+ throw new AgentOpsError("CHANGE_SURFACE_GIT_FAILED", `Git command failed with exit code ${result.exitCode}: ${args.join(" ")}`);
59
+ }
60
+ return parseNulPaths(result.stdout);
61
+ }
62
+ export async function collectChangeSurface(runner) {
63
+ const staged = await collectPaths(runner, [
64
+ "diff",
65
+ "--cached",
66
+ "--name-only",
67
+ "-z"
68
+ ]);
69
+ const unstaged = await collectPaths(runner, [
70
+ "diff",
71
+ "--name-only",
72
+ "-z"
73
+ ]);
74
+ const untracked = await collectPaths(runner, [
75
+ "ls-files",
76
+ "--others",
77
+ "--exclude-standard",
78
+ "-z"
79
+ ]);
80
+ return {
81
+ staged,
82
+ unstaged,
83
+ untracked,
84
+ paths: sortedUnique([...staged, ...unstaged, ...untracked])
85
+ };
86
+ }
@@ -0,0 +1,89 @@
1
+ import { join } from "node:path";
2
+ import { SCHEMA_VERSION } from "../contracts.js";
3
+ import { sha256 } from "../fs/hash.js";
4
+ import { AgentOpsError } from "../fs/paths.js";
5
+ import { validateEvidence } from "../schema/validate.js";
6
+ import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
7
+ import { redactSecrets } from "../security/redact.js";
8
+ function canonicalJson(value) {
9
+ if (value === null ||
10
+ typeof value === "boolean" ||
11
+ typeof value === "number" ||
12
+ typeof value === "string") {
13
+ return JSON.stringify(value);
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return `[${value.map(canonicalJson).join(",")}]`;
17
+ }
18
+ if (typeof value === "object") {
19
+ const record = value;
20
+ return `{${Object.keys(record)
21
+ .sort()
22
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
23
+ .join(",")}}`;
24
+ }
25
+ throw new AgentOpsError("CONFIG_HASH_INVALID", "Configuration contains an unsupported value.");
26
+ }
27
+ export function calculateConfigHash(config) {
28
+ return sha256(canonicalJson(config));
29
+ }
30
+ function redactRecord(record) {
31
+ return Object.fromEntries(Object.entries(record).map(([key, value]) => [
32
+ redactSecrets(key),
33
+ redactSecrets(value)
34
+ ]));
35
+ }
36
+ function validateBuiltEvidence(value) {
37
+ const validation = validateEvidence(value);
38
+ if (!validation.ok) {
39
+ throw new AgentOpsError("EVIDENCE_INVALID", validation.errors[0]?.message ??
40
+ "Verification evidence is invalid.");
41
+ }
42
+ return validation.value;
43
+ }
44
+ export function buildVerificationEvidence(input) {
45
+ return validateBuiltEvidence({
46
+ schemaVersion: SCHEMA_VERSION,
47
+ taskId: input.taskId,
48
+ criterionId: input.criterionId,
49
+ commandId: input.command.id,
50
+ argv: [input.command.command, ...input.command.args].map(redactSecrets),
51
+ cwd: input.command.cwd,
52
+ scope: input.scope,
53
+ startedAt: input.startedAt,
54
+ finishedAt: input.finishedAt,
55
+ exitCode: input.exitCode,
56
+ testCount: input.testCount,
57
+ toolVersions: redactRecord(input.toolVersions),
58
+ configHash: calculateConfigHash(input.config)
59
+ });
60
+ }
61
+ export class FileEvidenceStore {
62
+ #root;
63
+ #anchorDirectory;
64
+ constructor(root, anchorDirectory) {
65
+ this.#root = root;
66
+ this.#anchorDirectory = anchorDirectory;
67
+ }
68
+ async save(value) {
69
+ const evidence = validateBuiltEvidence(value);
70
+ const content = `${JSON.stringify(evidence, null, 2)}\n`;
71
+ const contentHash = sha256(content);
72
+ const relativePath = [
73
+ ".agent-ops",
74
+ "tasks",
75
+ "evidence",
76
+ evidence.taskId,
77
+ `${evidence.commandId}-${contentHash.slice(0, 16)}.json`
78
+ ].join("/");
79
+ const absolutePath = join(this.#root, ...relativePath.split("/"));
80
+ const existing = await readPrivateFile(absolutePath, this.#anchorDirectory);
81
+ if (existing !== null && existing !== content) {
82
+ throw new AgentOpsError("EVIDENCE_CONFLICT", "Evidence path already contains different content.");
83
+ }
84
+ if (existing === null) {
85
+ await writePrivateFile(absolutePath, content, this.#anchorDirectory);
86
+ }
87
+ return relativePath;
88
+ }
89
+ }
@@ -0,0 +1,67 @@
1
+ import { sha256 } from "../fs/hash.js";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ import { redactSecrets } from "../security/redact.js";
4
+ const COMPONENT_PATTERN = /^[a-z][a-z0-9-]{0,127}$/u;
5
+ const MAX_DIAGNOSTIC_BYTES = 512;
6
+ function boundedUtf8(value, maxBytes) {
7
+ let output = "";
8
+ let bytes = 0;
9
+ for (const character of value) {
10
+ const characterBytes = Buffer.byteLength(character, "utf8");
11
+ if (bytes + characterBytes > maxBytes) {
12
+ break;
13
+ }
14
+ output += character;
15
+ bytes += characterBytes;
16
+ }
17
+ return output;
18
+ }
19
+ function normalizedDiagnostic(value) {
20
+ const redacted = redactSecrets(value)
21
+ .replace(/\r\n?/gu, "\n")
22
+ .split("\n")
23
+ .map((line) => line.trim().replace(/[ \t]+/gu, " "))
24
+ .filter((line) => line.length > 0)
25
+ .join("\n");
26
+ return boundedUtf8(redacted, MAX_DIAGNOSTIC_BYTES);
27
+ }
28
+ function assertComponent(name, value) {
29
+ if (!COMPONENT_PATTERN.test(value)) {
30
+ throw new AgentOpsError("FINGERPRINT_INVALID", `Failure fingerprint ${name} is invalid.`);
31
+ }
32
+ }
33
+ export function createFailureFingerprint(input) {
34
+ assertComponent("commandId", input.commandId);
35
+ assertComponent("failureClass", input.failureClass);
36
+ assertComponent("exitCategory", input.exitCategory);
37
+ const diagnostics = normalizedDiagnostic(input.diagnostics);
38
+ const components = {
39
+ commandId: input.commandId,
40
+ failureClass: input.failureClass,
41
+ exitCategory: input.exitCategory,
42
+ diagnostics
43
+ };
44
+ return {
45
+ value: sha256(JSON.stringify(components)),
46
+ ...components
47
+ };
48
+ }
49
+ export function advanceFailureFingerprint(previous, current, recordedAt) {
50
+ if (!Number.isFinite(Date.parse(recordedAt))) {
51
+ throw new AgentOpsError("FINGERPRINT_TIMESTAMP_INVALID", "Failure fingerprint timestamp must be ISO-compatible.");
52
+ }
53
+ const consecutive = previous?.value === current.value
54
+ ? previous.consecutive + 1
55
+ : 1;
56
+ if (!Number.isSafeInteger(consecutive) || consecutive <= 0) {
57
+ throw new AgentOpsError("FINGERPRINT_COUNT_INVALID", "Failure fingerprint repetition count is invalid.");
58
+ }
59
+ return {
60
+ state: {
61
+ ...current,
62
+ consecutive,
63
+ recordedAt
64
+ },
65
+ signal: consecutive >= 2 ? "CHANGE_APPROACH_REQUIRED" : null
66
+ };
67
+ }
@@ -0,0 +1,69 @@
1
+ import { AgentOpsError } from "../fs/paths.js";
2
+ function sortedUnique(values) {
3
+ return [...new Set(values)].sort();
4
+ }
5
+ function mappingMatches(path, mapping) {
6
+ return path === mapping.path || path.startsWith(`${mapping.path}/`);
7
+ }
8
+ function mappingEvidence(path, mappings) {
9
+ return {
10
+ changedPath: path,
11
+ mappingPaths: sortedUnique(mappings.map((mapping) => mapping.path)),
12
+ verifierIds: sortedUnique(mappings.flatMap((mapping) => mapping.verifierIds))
13
+ };
14
+ }
15
+ function sameVerifierIds(left, right) {
16
+ return (left.length === right.length &&
17
+ left.every((verifierId, index) => verifierId === right[index]));
18
+ }
19
+ function fallbackSelection(reason, evidence) {
20
+ if (evidence.requiredVerifierIds.length === 0) {
21
+ throw new AgentOpsError("VERIFICATION_SCOPE_EMPTY", `Verification scope fallback (${reason}) has no required commands.`);
22
+ }
23
+ return {
24
+ verifierIds: evidence.requiredVerifierIds,
25
+ fallback: true,
26
+ reason,
27
+ evidence
28
+ };
29
+ }
30
+ export function selectVerificationScope(paths, config) {
31
+ const changedPaths = sortedUnique(paths);
32
+ const requiredVerifierIds = sortedUnique(config.verification.commands
33
+ .filter((command) => command.required)
34
+ .map((command) => command.id));
35
+ const mappings = changedPaths.map((changedPath) => {
36
+ const matches = config.pathMappings.filter((mapping) => mappingMatches(changedPath, mapping));
37
+ return mappingEvidence(changedPath, matches);
38
+ });
39
+ const evidence = {
40
+ changedPaths,
41
+ mappings,
42
+ requiredVerifierIds
43
+ };
44
+ if (changedPaths.length === 0) {
45
+ return fallbackSelection("no-changes", evidence);
46
+ }
47
+ if (mappings.some((mapping) => mapping.mappingPaths.length === 0)) {
48
+ return fallbackSelection("unknown-path", evidence);
49
+ }
50
+ if (mappings.some((mapping) => mapping.mappingPaths.length > 1)) {
51
+ return fallbackSelection("ambiguous-path", evidence);
52
+ }
53
+ if (mappings.some((mapping) => mapping.verifierIds.length === 0)) {
54
+ return fallbackSelection("empty-mapping", evidence);
55
+ }
56
+ const selectedVerifierIds = mappings[0]?.verifierIds ?? [];
57
+ if (mappings.some((mapping) => !sameVerifierIds(mapping.verifierIds, selectedVerifierIds))) {
58
+ return fallbackSelection("conflicting-scope", evidence);
59
+ }
60
+ if (selectedVerifierIds.length === 0) {
61
+ return fallbackSelection("empty-mapping", evidence);
62
+ }
63
+ return {
64
+ verifierIds: selectedVerifierIds,
65
+ fallback: false,
66
+ reason: "mapped",
67
+ evidence
68
+ };
69
+ }