@kylecheng3146/agent-ops 0.5.3 → 0.6.0
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/README.md +48 -0
- package/dist/packages/cli/src/args.js +165 -2
- package/dist/packages/cli/src/bin.js +64 -17
- package/dist/packages/cli/src/cli.js +33 -6
- package/dist/packages/cli/src/commands/advance.js +23 -3
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +100 -9
- package/dist/packages/cli/src/commands/run.js +111 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +63 -1
- package/dist/packages/cli/src/hook-process.js +31 -1
- package/dist/packages/cli/src/owned-run-step.js +65 -0
- package/dist/packages/cli/src/parallel-deps.js +18 -5
- package/dist/packages/cli/src/run-deps.js +314 -0
- package/dist/packages/cli/src/run-entry.js +943 -0
- package/dist/packages/cli/src/run-setup-entry.js +32 -0
- package/dist/packages/cli/src/run-step-entry.js +58 -0
- package/dist/runtime/src/config/explain.js +6 -0
- package/dist/runtime/src/config/hash.js +1 -1
- package/dist/runtime/src/config/merge.js +9 -1
- package/dist/runtime/src/contracts.js +2 -2
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +5 -1
- package/dist/runtime/src/parallel/integrate.js +288 -10
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +71 -5
- package/dist/runtime/src/parallel/service.js +41 -1
- package/dist/runtime/src/review/attestation.js +36 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- package/dist/runtime/src/review/runner.js +12 -1
- package/dist/runtime/src/review/scope.js +18 -1
- package/dist/runtime/src/review/slots.js +35 -10
- package/dist/runtime/src/run/authorization.js +60 -0
- package/dist/runtime/src/run/budget.js +30 -0
- package/dist/runtime/src/run/controls.js +139 -0
- package/dist/runtime/src/run/convergence.js +177 -0
- package/dist/runtime/src/run/hosts/claude.js +180 -0
- package/dist/runtime/src/run/hosts/codex.js +279 -0
- package/dist/runtime/src/run/hosts/index.js +4 -0
- package/dist/runtime/src/run/hosts/protocol.js +254 -0
- package/dist/runtime/src/run/hosts/types.js +1 -0
- package/dist/runtime/src/run/hosts/util.js +136 -0
- package/dist/runtime/src/run/integration.js +204 -0
- package/dist/runtime/src/run/macOS.js +189 -0
- package/dist/runtime/src/run/ownership.js +22 -0
- package/dist/runtime/src/run/planning.js +25 -0
- package/dist/runtime/src/run/policy-transition.js +1076 -0
- package/dist/runtime/src/run/policy.js +176 -0
- package/dist/runtime/src/run/ratchet.js +71 -0
- package/dist/runtime/src/run/scheduler.js +260 -0
- package/dist/runtime/src/run/service.js +425 -0
- package/dist/runtime/src/run/supervisor.js +635 -0
- package/dist/runtime/src/run/transport.js +159 -0
- package/dist/runtime/src/run/types.js +1 -0
- package/dist/runtime/src/run/usage.js +81 -0
- package/dist/runtime/src/run/verification.js +64 -0
- package/dist/runtime/src/run/worker-gate.js +48 -0
- package/dist/runtime/src/schema/acceptance.js +91 -0
- package/dist/runtime/src/schema/validate.js +67 -7
- package/dist/runtime/src/task/completion.js +14 -2
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/service.js +316 -13
- package/dist/runtime/src/task/store.js +57 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
- package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
- package/dist/runtime/src/verify/acceptance-replay.js +239 -0
- package/dist/runtime/src/verify/adapters/index.js +5 -0
- package/dist/runtime/src/verify/adapters/jest.js +142 -0
- package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
- package/dist/runtime/src/verify/adapters/node.js +254 -0
- package/dist/runtime/src/verify/adapters/pytest.js +188 -0
- package/dist/runtime/src/verify/adapters/rust.js +84 -0
- package/dist/runtime/src/verify/adapters/shared.js +82 -0
- package/dist/runtime/src/verify/adapters/vitest.js +218 -0
- package/dist/runtime/src/verify/evidence.js +5 -2
- package/dist/runtime/src/verify/service.js +24 -8
- package/dist/runtime/src/verify/source-fingerprint.js +1 -1
- package/dist/runtime/src/verify/spawn.js +13 -2
- package/docs/en/spec/acceptance-and-evidence.md +48 -0
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +221 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +265 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- package/templates/acceptance/agent_ops_pytest.py +46 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
7
|
+
import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
|
|
8
|
+
const execFile = promisify(execFileCallback);
|
|
9
|
+
function xml(value) {
|
|
10
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">")
|
|
11
|
+
.replace(/"/gu, """).replace(/'/gu, "'");
|
|
12
|
+
}
|
|
13
|
+
function shellSafe(value) {
|
|
14
|
+
return value.replace(/[^A-Za-z0-9._-]/gu, "-").slice(0, 60) || "worker";
|
|
15
|
+
}
|
|
16
|
+
function assertPathInside(root, candidate) {
|
|
17
|
+
const canonicalRoot = resolve(root);
|
|
18
|
+
const canonicalCandidate = resolve(candidate);
|
|
19
|
+
const fromRoot = relative(canonicalRoot, canonicalCandidate);
|
|
20
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
|
|
21
|
+
throw new AgentOpsError("LAUNCHD_PATH_INVALID", "launchd transient state escaped its private directory.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function defaultUid() {
|
|
25
|
+
if (typeof process.getuid !== "function") {
|
|
26
|
+
throw new AgentOpsError("LAUNCHD_UID_UNAVAILABLE", "launchd requires a macOS user id.");
|
|
27
|
+
}
|
|
28
|
+
return process.getuid();
|
|
29
|
+
}
|
|
30
|
+
function defaultExecFile(file, args, options) {
|
|
31
|
+
return execFile(file, [...args], options).then((result) => ({
|
|
32
|
+
stdout: result.stdout,
|
|
33
|
+
stderr: result.stderr
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
export function createLaunchdDescriptor(input) {
|
|
37
|
+
if (input.runId.length === 0 || input.workerId.length === 0 || input.command.length === 0 || input.cwd.length === 0) {
|
|
38
|
+
throw new AgentOpsError("LAUNCHD_DESCRIPTOR_INVALID", "launchd descriptor fields cannot be empty.");
|
|
39
|
+
}
|
|
40
|
+
const privateDirectory = resolve(input.privateDirectory);
|
|
41
|
+
const label = `com.agent-ops.run.${shellSafe(input.runId)}.${shellSafe(input.workerId)}.${createHash("sha256").update(input.runId).digest("hex").slice(0, 8)}`;
|
|
42
|
+
const path = join(privateDirectory, `${label}.plist`);
|
|
43
|
+
const disabledPath = join(privateDirectory, `${label}.disabled.json`);
|
|
44
|
+
assertPathInside(privateDirectory, path);
|
|
45
|
+
assertPathInside(privateDirectory, disabledPath);
|
|
46
|
+
const uid = input.uid ?? (process.platform === "darwin" ? defaultUid() : 0);
|
|
47
|
+
if (!Number.isSafeInteger(uid) || uid < 0) {
|
|
48
|
+
throw new AgentOpsError("LAUNCHD_UID_INVALID", "launchd user id is invalid.");
|
|
49
|
+
}
|
|
50
|
+
const stdoutPath = input.stdoutPath ?? join(privateDirectory, `${label}.stdout.log`);
|
|
51
|
+
const stderrPath = input.stderrPath ?? join(privateDirectory, `${label}.stderr.log`);
|
|
52
|
+
assertPathInside(privateDirectory, stdoutPath);
|
|
53
|
+
assertPathInside(privateDirectory, stderrPath);
|
|
54
|
+
const argumentsXml = [input.command, ...input.args]
|
|
55
|
+
.map((arg) => ` <string>${xml(arg)}</string>`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
const xmlDocument = `<?xml version="1.0" encoding="UTF-8"?>
|
|
58
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
59
|
+
<plist version="1.0">
|
|
60
|
+
<dict>
|
|
61
|
+
<key>Label</key><string>${xml(label)}</string>
|
|
62
|
+
<key>ProgramArguments</key>
|
|
63
|
+
<array>
|
|
64
|
+
${argumentsXml}
|
|
65
|
+
</array>
|
|
66
|
+
<key>WorkingDirectory</key><string>${xml(input.cwd)}</string>
|
|
67
|
+
${input.pathEnvironment === undefined ? "" : " <key>EnvironmentVariables</key><dict><key>PATH</key><string>" + xml(input.pathEnvironment) + "</string></dict>"}
|
|
68
|
+
<key>RunAtLoad</key><true/>
|
|
69
|
+
<key>KeepAlive</key>
|
|
70
|
+
<dict><key>SuccessfulExit</key><false/></dict>
|
|
71
|
+
<key>StandardOutPath</key><string>${xml(stdoutPath)}</string>
|
|
72
|
+
<key>StandardErrorPath</key><string>${xml(stderrPath)}</string>
|
|
73
|
+
</dict>
|
|
74
|
+
</plist>
|
|
75
|
+
`;
|
|
76
|
+
return {
|
|
77
|
+
label,
|
|
78
|
+
domain: `gui/${uid}`,
|
|
79
|
+
path,
|
|
80
|
+
privateDirectory,
|
|
81
|
+
disabledPath,
|
|
82
|
+
xml: xmlDocument
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function compareBootLogin(saved, current) {
|
|
86
|
+
if (saved.boot !== current.boot) {
|
|
87
|
+
return { resume: false, reason: "boot-change" };
|
|
88
|
+
}
|
|
89
|
+
if (saved.login !== current.login) {
|
|
90
|
+
return { resume: false, reason: "login-change" };
|
|
91
|
+
}
|
|
92
|
+
return { resume: true, reason: "same-login" };
|
|
93
|
+
}
|
|
94
|
+
export async function readBootIdentity(platform = process.platform, run = defaultExecFile) {
|
|
95
|
+
if (platform !== "darwin") {
|
|
96
|
+
return `platform:${platform}`;
|
|
97
|
+
}
|
|
98
|
+
const result = await run("sysctl", ["-n", "kern.boottime"], {});
|
|
99
|
+
const value = result.stdout?.trim();
|
|
100
|
+
if (value === undefined || value.length === 0) {
|
|
101
|
+
throw new AgentOpsError("LAUNCHD_BOOT_IDENTITY_UNAVAILABLE", "macOS boot identity was empty.");
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
export function readLoginIdentity(uid = defaultUid()) {
|
|
106
|
+
if (!Number.isSafeInteger(uid) || uid < 0) {
|
|
107
|
+
throw new AgentOpsError("LAUNCHD_UID_INVALID", "macOS login user id is invalid.");
|
|
108
|
+
}
|
|
109
|
+
return `uid:${uid}:${process.env.LOGNAME ?? process.env.USER ?? "unknown"}`;
|
|
110
|
+
}
|
|
111
|
+
/** A uid alone survives logout/login; the GUI audit session identifies this login. */
|
|
112
|
+
export async function readGuiLoginIdentity(uid = defaultUid(), run = defaultExecFile) {
|
|
113
|
+
if (!Number.isSafeInteger(uid) || uid < 0)
|
|
114
|
+
throw new AgentOpsError("LAUNCHD_UID_INVALID", "Invalid macOS login user id.");
|
|
115
|
+
const result = await run("launchctl", ["print", `gui/${uid}`], {});
|
|
116
|
+
const security = result.stdout?.match(/^\tsecurity context = \{[\s\S]*?^\t\}/mu)?.[0];
|
|
117
|
+
const auditSession = security?.match(/^\s*asid = ([0-9]+)$/mu)?.[1];
|
|
118
|
+
if (auditSession === undefined)
|
|
119
|
+
throw new AgentOpsError("LAUNCHD_LOGIN_IDENTITY_UNAVAILABLE", "Cannot identify the current GUI audit session.");
|
|
120
|
+
return `gui:${uid}:asid:${auditSession}`;
|
|
121
|
+
}
|
|
122
|
+
export class LaunchdController {
|
|
123
|
+
platform;
|
|
124
|
+
uid;
|
|
125
|
+
run;
|
|
126
|
+
constructor(options = {}) {
|
|
127
|
+
this.platform = options.platform ?? process.platform;
|
|
128
|
+
this.uid = options.uid ?? (this.platform === "darwin" ? defaultUid() : 0);
|
|
129
|
+
this.run = options.execFile ?? defaultExecFile;
|
|
130
|
+
}
|
|
131
|
+
get supported() {
|
|
132
|
+
return this.platform === "darwin";
|
|
133
|
+
}
|
|
134
|
+
async writeDescriptor(descriptor) {
|
|
135
|
+
this.assertSupported();
|
|
136
|
+
await writePrivateFile(descriptor.path, descriptor.xml, descriptor.privateDirectory);
|
|
137
|
+
}
|
|
138
|
+
async bootstrap(descriptor) {
|
|
139
|
+
this.assertSupported();
|
|
140
|
+
await this.run("launchctl", ["bootstrap", descriptor.domain, descriptor.path], {
|
|
141
|
+
cwd: descriptor.privateDirectory
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async wake(descriptor) {
|
|
145
|
+
this.assertSupported();
|
|
146
|
+
try {
|
|
147
|
+
await this.bootstrap(descriptor);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
await this.run("launchctl", ["kickstart", `${descriptor.domain}/${descriptor.label}`], { cwd: descriptor.privateDirectory });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async bootout(descriptor) {
|
|
154
|
+
this.assertSupported();
|
|
155
|
+
try {
|
|
156
|
+
await this.run("launchctl", ["bootout", `${descriptor.domain}/${descriptor.label}`], { cwd: descriptor.privateDirectory });
|
|
157
|
+
}
|
|
158
|
+
catch (cause) {
|
|
159
|
+
const failure = cause;
|
|
160
|
+
if (failure.code !== 3 || typeof failure.stderr !== "string" || !failure.stderr.includes("No such process"))
|
|
161
|
+
throw cause;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async disableRestart(descriptor, reason) {
|
|
165
|
+
this.assertSupported();
|
|
166
|
+
if (reason.length === 0 || reason.length > 512 || reason.includes("\0")) {
|
|
167
|
+
throw new AgentOpsError("LAUNCHD_DISABLE_REASON_INVALID", "launchd disable reason is invalid.");
|
|
168
|
+
}
|
|
169
|
+
await writePrivateFile(descriptor.disabledPath, `${JSON.stringify({ disabled: true, reason, at: new Date().toISOString() })}\n`, descriptor.privateDirectory);
|
|
170
|
+
await this.run("launchctl", ["disable", `${descriptor.domain}/${descriptor.label}`], {
|
|
171
|
+
cwd: descriptor.privateDirectory
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async isRestartDisabled(descriptor) {
|
|
175
|
+
return (await readPrivateFile(descriptor.disabledPath, descriptor.privateDirectory)) !== null;
|
|
176
|
+
}
|
|
177
|
+
async enableRestart(descriptor) {
|
|
178
|
+
this.assertSupported();
|
|
179
|
+
await this.run("launchctl", ["enable", `${descriptor.domain}/${descriptor.label}`], {
|
|
180
|
+
cwd: descriptor.privateDirectory
|
|
181
|
+
});
|
|
182
|
+
await rm(descriptor.disabledPath, { force: true });
|
|
183
|
+
}
|
|
184
|
+
assertSupported() {
|
|
185
|
+
if (!this.supported) {
|
|
186
|
+
throw new AgentOpsError("LAUNCHD_UNSUPPORTED", "Transient launchd primitives require macOS.");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { FileRunRepository } from "./service.js";
|
|
3
|
+
/** The common run ledger, rather than worker-local metadata, grants writer ownership. */
|
|
4
|
+
export async function registeredRunWriter(record, sessionId, agentId) {
|
|
5
|
+
if (record.runId === undefined || record.workerId === undefined || record.workerGeneration === undefined)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
const common = join(record.mainRoot, ".git");
|
|
9
|
+
const state = await new FileRunRepository(join(common, "agent-ops", "runs"), common).read(record.runId);
|
|
10
|
+
const worker = state?.workers.find(w => w.workerId === record.workerId);
|
|
11
|
+
return state !== null && state !== undefined && state.status === "active" && !state.disableRestart &&
|
|
12
|
+
worker !== undefined && worker.worktree === record.path && worker.generation === record.workerGeneration &&
|
|
13
|
+
worker.ownerSessionId === record.ownerSessionId && state.coordinatorId === record.coordinatorId &&
|
|
14
|
+
["running", "idle"].includes(worker.status) && worker.stopIntent === null &&
|
|
15
|
+
(worker.nativeSessionId === sessionId || worker.ownerSessionId === sessionId) &&
|
|
16
|
+
worker.leaseExpiresAt !== null && Date.parse(worker.leaseExpiresAt) > Date.now() &&
|
|
17
|
+
(agentId === undefined || agentId === record.agentId);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
2
|
+
import { validateCriterion } from "../schema/validate.js";
|
|
3
|
+
const plain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
|
+
const text = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 4096 && !v.includes("\0");
|
|
5
|
+
/** Validate a deferred worker plan without freezing criteria before dependencies are delivered. */
|
|
6
|
+
export function workerPlan(value, taskId, state) {
|
|
7
|
+
if (!plain(value) || Object.keys(value).some(k => !["title", "intent", "criteria", "dependencies"].includes(k)) ||
|
|
8
|
+
!text(value.title) || !text(value.intent) || !Array.isArray(value.criteria) || value.criteria.length < 2 || value.criteria.length > 5 ||
|
|
9
|
+
!Array.isArray(value.dependencies) || value.dependencies.length > 512 || new Set(value.dependencies).size !== value.dependencies.length ||
|
|
10
|
+
value.dependencies.some(id => typeof id !== "string" || id === state.rootTaskId || !state.tasks.some(task => task.taskId === id)))
|
|
11
|
+
throw new AgentOpsError("RUN_WORKER_PLAN_INVALID", "Worker plans require 2–5 criteria and known non-coordinator dependencies.");
|
|
12
|
+
const ids = new Set();
|
|
13
|
+
const criteria = value.criteria.map((criterion, index) => {
|
|
14
|
+
if (!plain(criterion) || criterion.finding !== undefined || (plain(criterion.acceptance) && criterion.acceptance.baselineCommit !== undefined))
|
|
15
|
+
throw new AgentOpsError("RUN_WORKER_BASELINE_DEFERRED", "New worker baselines are captured only after dependencies enter its checkout; finding pins belong to an existing task.");
|
|
16
|
+
const normalized = { ...criterion, ...(plain(criterion.acceptance) ? { acceptance: { ...criterion.acceptance, baselineCommit: "0".repeat(40) } } : {}) };
|
|
17
|
+
const checked = validateCriterion(normalized, `$.criteria[${index}]`);
|
|
18
|
+
if (!checked.ok || ids.has(checked.value.id))
|
|
19
|
+
throw new AgentOpsError("RUN_WORKER_PLAN_INVALID", "Worker criteria must be unique valid contracts.");
|
|
20
|
+
ids.add(checked.value.id);
|
|
21
|
+
// TaskService.create resolves the real checkout SHA when this worker starts.
|
|
22
|
+
return structuredClone(criterion);
|
|
23
|
+
});
|
|
24
|
+
return { taskId, title: value.title, intent: value.intent, criteria, dependencies: [...value.dependencies] };
|
|
25
|
+
}
|