@kylecheng3146/agent-ops 0.0.1 → 0.1.2
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
CHANGED
|
@@ -6,17 +6,66 @@ verification, safe lifecycle hooks, and independent review into a repeatable
|
|
|
6
6
|
engineering workflow.
|
|
7
7
|
|
|
8
8
|
This repository is in its foundation stage. The CLI, hook runtime, normative
|
|
9
|
-
specification, and installation profiles are being developed
|
|
10
|
-
|
|
9
|
+
specification, and installation profiles are being developed as a reviewable
|
|
10
|
+
pre-1.0 interface.
|
|
11
11
|
|
|
12
|
-
The
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
The CLI is published as `@kylecheng3146/agent-ops` as a pre-1.0 interface;
|
|
13
|
+
command behavior may change before 1.0.
|
|
14
|
+
|
|
15
|
+
## Quick start from npm
|
|
16
|
+
|
|
17
|
+
Requires Node.js `>=22.14.0`. Install the published CLI globally:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --global @kylecheng3146/agent-ops@latest
|
|
21
|
+
agent-ops --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can run it without a global install with `npx`. When the command runs in
|
|
25
|
+
an interactive terminal without arguments, it opens the setup wizard
|
|
26
|
+
automatically:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx --yes @kylecheng3146/agent-ops@latest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use `--help` for the complete command reference or provide explicit options in
|
|
33
|
+
automation. The wizard never writes files until you review and confirm its
|
|
34
|
+
installation plan.
|
|
35
|
+
|
|
36
|
+
Preview a project installation before changing files:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
agent-ops init \
|
|
40
|
+
--dry-run --scope project --harness both --profile core --json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After reviewing the plan, apply it explicitly with `--yes`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
agent-ops init --scope project --harness both --profile core --yes
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The remaining day-to-day checks are:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
agent-ops trust status --json
|
|
53
|
+
agent-ops doctor --json
|
|
54
|
+
agent-ops config explain --json
|
|
55
|
+
agent-ops update --dry-run --json
|
|
56
|
+
agent-ops update --yes --json
|
|
57
|
+
agent-ops uninstall --dry-run --json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `--scope user` with user-home installations. Keep `--dry-run` for any
|
|
61
|
+
operation you want to inspect before applying; non-interactive automation should
|
|
62
|
+
pass `--yes` only after reviewing the plan. Add `--json` when another tool will
|
|
63
|
+
consume the result.
|
|
15
64
|
|
|
16
65
|
## Quick start from a source checkout
|
|
17
66
|
|
|
18
|
-
|
|
19
|
-
|
|
67
|
+
For development or to run the repository version directly, use a source
|
|
68
|
+
checkout:
|
|
20
69
|
|
|
21
70
|
```bash
|
|
22
71
|
git clone https://github.com/kylecheng3146/agent-ops.git
|
|
@@ -40,16 +89,17 @@ updates, and removal are separate commands:
|
|
|
40
89
|
node dist/packages/cli/src/bin.js init --scope project --harness both --profile core --yes
|
|
41
90
|
node dist/packages/cli/src/bin.js trust status --json
|
|
42
91
|
node dist/packages/cli/src/bin.js doctor --json
|
|
43
|
-
node dist/packages/cli/src/bin.js
|
|
92
|
+
node dist/packages/cli/src/bin.js config explain --json
|
|
93
|
+
node dist/packages/cli/src/bin.js update --dry-run --json
|
|
44
94
|
node dist/packages/cli/src/bin.js uninstall --dry-run --json
|
|
45
95
|
```
|
|
46
96
|
|
|
47
97
|
The commands after `init --yes` are post-apply operations; `doctor` may report
|
|
48
98
|
an unknown probe status until a repository-specific verification setup exists.
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
100
|
+
For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
|
|
101
|
+
`review` commands support acceptance tracking and independent verification when
|
|
102
|
+
the project configuration defines those workflows.
|
|
53
103
|
|
|
54
104
|
## Project principles
|
|
55
105
|
|
|
@@ -62,8 +112,9 @@ published package and provenance record.
|
|
|
62
112
|
|
|
63
113
|
## Project status
|
|
64
114
|
|
|
65
|
-
|
|
66
|
-
|
|
115
|
+
A pre-1.0 npm package is published. Use `@latest` for the current release, or
|
|
116
|
+
pin a specific version in automation when reproducibility matters; review
|
|
117
|
+
release notes before upgrading.
|
|
67
118
|
|
|
68
119
|
Documentation:
|
|
69
120
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { execFileSync } from "node:child_process";
|
|
6
7
|
import { commonHarnessAdapters } from "../../../runtime/src/install/harness.js";
|
|
8
|
+
import { hookRegistrationSatisfied, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
|
|
9
|
+
import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
|
|
7
10
|
import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
|
|
8
11
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
9
12
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
@@ -28,7 +31,7 @@ import { runTrustCommand } from "./commands/trust.js";
|
|
|
28
31
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
29
32
|
import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
30
33
|
import { errorEnvelope } from "./output.js";
|
|
31
|
-
const CLI_VERSION = "0.1.
|
|
34
|
+
const CLI_VERSION = "0.1.2";
|
|
32
35
|
const DEFAULT_CONFIG = {
|
|
33
36
|
schemaVersion: 1,
|
|
34
37
|
profiles: [],
|
|
@@ -95,6 +98,23 @@ async function loadEffectiveConfig(root, scope) {
|
|
|
95
98
|
}
|
|
96
99
|
return mergeConfigLayers(layers);
|
|
97
100
|
}
|
|
101
|
+
async function readOptionalJson(path) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function installedHarness(root) {
|
|
110
|
+
try {
|
|
111
|
+
return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8")).harness;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ponytail: no readable manifest means demand hooks for both harnesses.
|
|
115
|
+
return "both";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
98
118
|
async function repositoryTrust(root, config) {
|
|
99
119
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
100
120
|
const state = localStatePaths(home);
|
|
@@ -175,7 +195,20 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
|
|
175
195
|
});
|
|
176
196
|
}
|
|
177
197
|
if (args.command === "doctor") {
|
|
178
|
-
|
|
198
|
+
const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
|
|
199
|
+
return await runDoctorCommand({
|
|
200
|
+
root,
|
|
201
|
+
probes: {
|
|
202
|
+
hookRegistration: async () => hookRegistrationSatisfied({
|
|
203
|
+
harness: await installedHarness(root),
|
|
204
|
+
profiles: config.profiles,
|
|
205
|
+
claudeSettings: await readOptionalJson(join(root, ".claude", "settings.json")),
|
|
206
|
+
codexHooks: await readOptionalJson(join(root, ".codex", "hooks.json"))
|
|
207
|
+
}),
|
|
208
|
+
repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config)),
|
|
209
|
+
smokeAvailability: () => smokeAvailabilityStatus(config)
|
|
210
|
+
}
|
|
211
|
+
});
|
|
179
212
|
}
|
|
180
213
|
if (args.command === "uninstall") {
|
|
181
214
|
return await runUninstallCommand({
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { CliArgumentError, parseArgs } from "./args.js";
|
|
2
2
|
import { errorEnvelope, okEnvelope, writeEnvelope } from "./output.js";
|
|
3
3
|
import { completeInitChoices } from "./wizard.js";
|
|
4
|
+
const WELCOME_BANNER = [
|
|
5
|
+
"+--------------------------------------------------+",
|
|
6
|
+
"| LOOP ENGINEERING TOOLKIT |",
|
|
7
|
+
"| Safe setup for Codex + Claude Code |",
|
|
8
|
+
"+--------------------------------------------------+"
|
|
9
|
+
].join("\n");
|
|
10
|
+
export function renderWelcome(color) {
|
|
11
|
+
const cyan = color ? "\u001b[36m" : "";
|
|
12
|
+
const bold = color ? "\u001b[1m" : "";
|
|
13
|
+
const reset = color ? "\u001b[0m" : "";
|
|
14
|
+
return `${cyan}${bold}${WELCOME_BANNER}${reset}\n\n`;
|
|
15
|
+
}
|
|
4
16
|
export const HELP_TEXT = `Usage: agent-ops <command> [options]
|
|
5
17
|
|
|
18
|
+
Run \`agent-ops\` without arguments in a terminal to start the interactive setup wizard.
|
|
19
|
+
|
|
6
20
|
Commands:
|
|
7
21
|
init Plan or install agent-ops
|
|
8
22
|
config explain
|
|
@@ -41,10 +55,15 @@ function writeAndReturn(io, envelope, json, exitCode) {
|
|
|
41
55
|
return exitCode;
|
|
42
56
|
}
|
|
43
57
|
export async function runCli(argv, io, services) {
|
|
44
|
-
const
|
|
58
|
+
const launchesWizard = argv.length === 0 && io.isTTY;
|
|
59
|
+
const effectiveArgv = launchesWizard ? ["init"] : argv;
|
|
60
|
+
if (launchesWizard) {
|
|
61
|
+
io.writeStdout(renderWelcome(process.env.NO_COLOR === undefined && process.env.TERM !== "dumb"));
|
|
62
|
+
}
|
|
63
|
+
const json = wantsJson(effectiveArgv);
|
|
45
64
|
let args;
|
|
46
65
|
try {
|
|
47
|
-
args = parseArgs(
|
|
66
|
+
args = parseArgs(effectiveArgv);
|
|
48
67
|
}
|
|
49
68
|
catch (error) {
|
|
50
69
|
if (error instanceof CliArgumentError) {
|
|
@@ -6,6 +6,17 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { sha256 } from "./hash.js";
|
|
7
7
|
import { AgentOpsError, resolveContainedPath } from "./paths.js";
|
|
8
8
|
export { AgentOpsError } from "./paths.js";
|
|
9
|
+
/**
|
|
10
|
+
* Identity must come from bigint stats. Windows file indexes pack a sequence
|
|
11
|
+
* number above the 2^53 boundary, so a numeric `ino` silently rounds and an
|
|
12
|
+
* untouched file looks like it changed.
|
|
13
|
+
*/
|
|
14
|
+
export function fileIdentity(status) {
|
|
15
|
+
return {
|
|
16
|
+
device: status.dev.toString(),
|
|
17
|
+
inode: status.ino.toString()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
9
20
|
const MUTATION_WORKER_PATH = fileURLToPath(new URL("./mutation-worker.js", import.meta.url));
|
|
10
21
|
function isMissing(error) {
|
|
11
22
|
return (typeof error === "object" &&
|
|
@@ -25,10 +36,11 @@ async function captureParentGuard(parent) {
|
|
|
25
36
|
normalizedPath(canonicalPath) !== normalizedPath(parent)) {
|
|
26
37
|
throw new AgentOpsError("PRECONDITION_CHANGED", `Destination directory changed before mutation: ${parent}`);
|
|
27
38
|
}
|
|
39
|
+
const identity = fileIdentity(status);
|
|
28
40
|
return {
|
|
29
41
|
expectedParentPath: canonicalPath,
|
|
30
|
-
parentDevice:
|
|
31
|
-
parentInode:
|
|
42
|
+
parentDevice: identity.device,
|
|
43
|
+
parentInode: identity.inode
|
|
32
44
|
};
|
|
33
45
|
}
|
|
34
46
|
async function runAnchoredMutation(targetPath, action, expectedHash, content, mode) {
|
|
@@ -162,20 +174,21 @@ async function createBackup(snapshot, recoveryDirectory) {
|
|
|
162
174
|
async function snapshotOperation(root, operation) {
|
|
163
175
|
const targetPath = await resolveContainedPath(root, operation.path);
|
|
164
176
|
try {
|
|
165
|
-
const status = await lstat(targetPath);
|
|
177
|
+
const status = await lstat(targetPath, { bigint: true });
|
|
166
178
|
if (!status.isFile()) {
|
|
167
179
|
throw new AgentOpsError("UNSUPPORTED_FILE_TYPE", `Managed target must be a regular file: ${operation.path}`);
|
|
168
180
|
}
|
|
169
181
|
const content = await readFile(targetPath);
|
|
182
|
+
const identity = fileIdentity(status);
|
|
170
183
|
return {
|
|
171
184
|
operation,
|
|
172
185
|
targetPath,
|
|
173
186
|
existed: true,
|
|
174
187
|
content,
|
|
175
|
-
mode: status.mode & 0o777,
|
|
188
|
+
mode: Number(status.mode) & 0o777,
|
|
176
189
|
actualHash: sha256(content),
|
|
177
|
-
device:
|
|
178
|
-
inode:
|
|
190
|
+
device: identity.device,
|
|
191
|
+
inode: identity.inode,
|
|
179
192
|
backupPath: null,
|
|
180
193
|
createdDirectories: []
|
|
181
194
|
};
|
|
@@ -219,10 +232,7 @@ async function currentIdentity(path) {
|
|
|
219
232
|
if (!status.isFile() || status.isSymbolicLink()) {
|
|
220
233
|
throw new AgentOpsError("PRECONDITION_CHANGED", `A managed path is no longer a regular file: ${path}`);
|
|
221
234
|
}
|
|
222
|
-
return
|
|
223
|
-
device: status.dev.toString(),
|
|
224
|
-
inode: status.ino.toString()
|
|
225
|
-
};
|
|
235
|
+
return fileIdentity(status);
|
|
226
236
|
}
|
|
227
237
|
catch (error) {
|
|
228
238
|
if (isMissing(error)) {
|
|
@@ -165,9 +165,13 @@ async function checkProbe(id, probe) {
|
|
|
165
165
|
return check(id, "UNKNOWN", "No probe was provided.");
|
|
166
166
|
}
|
|
167
167
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const result = await probe();
|
|
169
|
+
const status = typeof result === "boolean" ? (result ? "PASS" : "FAIL") : result;
|
|
170
|
+
return check(id, status, status === "PASS"
|
|
171
|
+
? "Probe passed."
|
|
172
|
+
: status === "FAIL"
|
|
173
|
+
? "Probe failed."
|
|
174
|
+
: "Probe has nothing to verify yet.");
|
|
171
175
|
}
|
|
172
176
|
catch {
|
|
173
177
|
return check(id, "FAIL", "Probe failed.");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { buildClaudeHookSettings } from "../adapters/claude/config.js";
|
|
2
|
+
import { buildCodexHookConfig } from "../adapters/codex/config.js";
|
|
3
|
+
import { resolveProfiles } from "./profiles.js";
|
|
4
|
+
const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
|
|
5
|
+
const CODEX_COMMAND_PREFIX = "agent-ops hook codex ";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function hasManagedHandler(source, events, isManaged) {
|
|
10
|
+
if (!isRecord(source) || !isRecord(source.hooks)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const registered = source.hooks;
|
|
14
|
+
return events.every((event) => {
|
|
15
|
+
const groups = registered[event];
|
|
16
|
+
return (Array.isArray(groups) &&
|
|
17
|
+
groups.some((group) => isRecord(group) &&
|
|
18
|
+
Array.isArray(group.hooks) &&
|
|
19
|
+
group.hooks.some(isManaged)));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isManagedClaudeHandler(handler) {
|
|
23
|
+
return (isRecord(handler) &&
|
|
24
|
+
Array.isArray(handler.args) &&
|
|
25
|
+
handler.args.includes(CLAUDE_HOOK_MARKER));
|
|
26
|
+
}
|
|
27
|
+
function isManagedCodexHandler(handler) {
|
|
28
|
+
return (isRecord(handler) &&
|
|
29
|
+
typeof handler.command === "string" &&
|
|
30
|
+
handler.command.startsWith(CODEX_COMMAND_PREFIX));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Hook registration is satisfied when every hook event implied by the
|
|
34
|
+
* installed profiles carries an agent-ops owned handler for every installed
|
|
35
|
+
* harness. Installations without hook capabilities have nothing to register.
|
|
36
|
+
*/
|
|
37
|
+
export function hookRegistrationSatisfied(input) {
|
|
38
|
+
const { capabilities } = resolveProfiles(input.profiles);
|
|
39
|
+
const claudeEvents = Object.keys(buildClaudeHookSettings(capabilities, "probe").hooks);
|
|
40
|
+
const codexEvents = Object.keys(buildCodexHookConfig(capabilities).hooks);
|
|
41
|
+
if (claudeEvents.length === 0 && codexEvents.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (input.harness !== "codex" &&
|
|
45
|
+
!hasManagedHandler(input.claudeSettings, claudeEvents, isManagedClaudeHandler)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return (input.harness === "claude" ||
|
|
49
|
+
hasManagedHandler(input.codexHooks, codexEvents, isManagedCodexHandler));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Smoke availability stays UNKNOWN until the repository declares a
|
|
53
|
+
* verification command; the toolkit never invents one.
|
|
54
|
+
*/
|
|
55
|
+
export function smokeAvailabilityStatus(config) {
|
|
56
|
+
return config.verification.commands.length > 0 ? "PASS" : "UNKNOWN";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Installation approval never grants trust, so an ungranted repository is
|
|
60
|
+
* unconfigured rather than broken. A stale binding is a real failure.
|
|
61
|
+
*/
|
|
62
|
+
export function repositoryTrustStatus(trust) {
|
|
63
|
+
return trust === "TRUSTED"
|
|
64
|
+
? "PASS"
|
|
65
|
+
: trust === "STALE"
|
|
66
|
+
? "FAIL"
|
|
67
|
+
: "UNKNOWN";
|
|
68
|
+
}
|