@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 in a reviewable
10
- feature branch before the first pre-1.0 release.
9
+ specification, and installation profiles are being developed as a reviewable
10
+ pre-1.0 interface.
11
11
 
12
- The current CLI is an unreleased development interface. Its packed artifact is
13
- checked by `npm run package:check`; command behavior may change before the first
14
- tagged pre-1.0 release.
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
- Version 0.1.0 is prepared for release but has not yet been published to npm.
19
- Until the protected release workflow completes, use a source checkout:
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 update --target-version 0.1.0 --dry-run --json
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
- Use `--scope user` with user-home installations. Do not install from npm until
51
- the `v0.1.0` tag has been published; the release workflow is the source of the
52
- published package and provenance record.
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
- No npm package has been published yet. Do not depend on the current repository
66
- as a stable interface until a tagged release is available.
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.0";
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
- return await runDoctorCommand({ root });
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 json = wantsJson(argv);
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(argv);
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: status.dev.toString(),
31
- parentInode: status.ino.toString()
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: status.dev.toString(),
178
- inode: status.ino.toString(),
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
- return (await probe())
169
- ? check(id, "PASS", "Probe passed.")
170
- : check(id, "FAIL", "Probe failed.");
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.0.1",
3
+ "version": "0.1.2",
4
4
  "description": "Evidence-driven development loops for Codex and Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",