@kylecheng3146/agent-ops 0.1.4 → 0.1.6

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 (84) hide show
  1. package/README.md +164 -13
  2. package/dist/packages/cli/src/args.js +40 -7
  3. package/dist/packages/cli/src/bin.js +35 -11
  4. package/dist/packages/cli/src/cli.js +7 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +70 -0
  6. package/dist/packages/cli/src/commands/doctor.js +31 -9
  7. package/dist/packages/cli/src/commands/hook.js +33 -16
  8. package/dist/packages/cli/src/commands/init.js +9 -5
  9. package/dist/packages/cli/src/commands/review.js +5 -1
  10. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  11. package/dist/packages/cli/src/commands/update.js +16 -5
  12. package/dist/packages/cli/src/context.js +69 -3
  13. package/dist/packages/cli/src/hook-process.js +231 -16
  14. package/dist/packages/cli/src/loop-entry.js +8 -0
  15. package/dist/packages/cli/src/plan-output.js +9 -4
  16. package/dist/packages/cli/src/public-plan.js +62 -0
  17. package/dist/packages/cli/src/version.js +1 -1
  18. package/dist/packages/cli/src/wizard.js +36 -15
  19. package/dist/runtime/src/adapters/claude/config.js +57 -19
  20. package/dist/runtime/src/adapters/claude/events.js +33 -0
  21. package/dist/runtime/src/adapters/claude/output.js +8 -7
  22. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  23. package/dist/runtime/src/adapters/codex/config.js +66 -13
  24. package/dist/runtime/src/adapters/codex/events.js +33 -0
  25. package/dist/runtime/src/adapters/codex/output.js +10 -0
  26. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  27. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  28. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  29. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  30. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  31. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  32. package/dist/runtime/src/config/explain.js +7 -0
  33. package/dist/runtime/src/config/hash.js +24 -0
  34. package/dist/runtime/src/config/merge.js +6 -2
  35. package/dist/runtime/src/config/migrate.js +19 -4
  36. package/dist/runtime/src/contracts.js +10 -1
  37. package/dist/runtime/src/fs/managed-block.js +35 -18
  38. package/dist/runtime/src/fs/manifest.js +32 -1
  39. package/dist/runtime/src/fs/transaction.js +14 -2
  40. package/dist/runtime/src/hooks/advisory.js +16 -0
  41. package/dist/runtime/src/hooks/codex-loop.js +439 -0
  42. package/dist/runtime/src/hooks/stop-service.js +70 -0
  43. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  44. package/dist/runtime/src/install/codex-loop.js +139 -0
  45. package/dist/runtime/src/install/doctor.js +182 -14
  46. package/dist/runtime/src/install/harness.js +294 -35
  47. package/dist/runtime/src/install/hooks.js +22 -17
  48. package/dist/runtime/src/install/ownership.js +146 -35
  49. package/dist/runtime/src/install/plan.js +276 -31
  50. package/dist/runtime/src/install/probes.js +9 -43
  51. package/dist/runtime/src/install/profiles.js +12 -3
  52. package/dist/runtime/src/install/surface-inspection.js +296 -0
  53. package/dist/runtime/src/install/surfaces.js +11 -0
  54. package/dist/runtime/src/install/uninstall.js +11 -4
  55. package/dist/runtime/src/install/update.js +13 -3
  56. package/dist/runtime/src/logging/local-log.js +25 -0
  57. package/dist/runtime/src/schema/validate.js +45 -19
  58. package/dist/runtime/src/task/service.js +3 -3
  59. package/dist/runtime/src/task/store.js +12 -3
  60. package/dist/runtime/src/verify/command-executor.js +113 -0
  61. package/dist/runtime/src/verify/evidence.js +4 -24
  62. package/dist/runtime/src/verify/service.js +20 -92
  63. package/dist/runtime/src/verify/spawn.js +6 -1
  64. package/docs/en/guides/configuration.md +159 -0
  65. package/docs/en/guides/quickstart.md +9 -0
  66. package/docs/en/guides/security.md +5 -0
  67. package/docs/en/spec/README.md +9 -0
  68. package/docs/en/spec/harness-adapters.md +104 -2
  69. package/docs/en/spec/maintenance.md +11 -0
  70. package/docs/en/spec/review.md +11 -0
  71. package/docs/zh-TW/guides/configuration.md +145 -0
  72. package/docs/zh-TW/guides/quickstart.md +9 -0
  73. package/docs/zh-TW/guides/security.md +5 -0
  74. package/docs/zh-TW/spec/README.md +9 -0
  75. package/docs/zh-TW/spec/harness-adapters.md +89 -3
  76. package/docs/zh-TW/spec/maintenance.md +11 -1
  77. package/docs/zh-TW/spec/review.md +10 -0
  78. package/package.json +4 -2
  79. package/schemas/config.schema.json +56 -2
  80. package/schemas/manifest.schema.json +19 -3
  81. package/templates/common/AGENTS.block.md +2 -1
  82. package/templates/common/CLAUDE.block.md +2 -1
  83. package/dist/runtime/src/review/claude-runner.js +0 -4
  84. package/dist/runtime/src/review/codex-runner.js +0 -4
@@ -0,0 +1,170 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { AgentOpsError } from "../../fs/paths.js";
4
+ export const OPENCODE_MANAGED_MARKER = "--managed-by=agent-ops";
5
+ export const OPENCODE_PLUGIN_PATH = ".opencode/plugins/agent-ops.js";
6
+ export const OPENCODE_USER_PLUGIN_PATH = ".config/opencode/plugins/agent-ops.js";
7
+ function userPluginPath(root, configuredHome = process.env.XDG_CONFIG_HOME, configuredDirectory = process.env.OPENCODE_CONFIG_DIR) {
8
+ const installationRoot = resolve(root ?? process.env.AGENT_OPS_HOME ?? homedir());
9
+ const customConfigDirectory = configuredDirectory?.trim();
10
+ const configDirectory = customConfigDirectory !== undefined && customConfigDirectory.length > 0
11
+ ? customConfigDirectory
12
+ : configuredHome === undefined || configuredHome.trim().length === 0
13
+ ? join(installationRoot, ".config", "opencode")
14
+ : join(configuredHome, "opencode");
15
+ if (!isAbsolute(configDirectory)) {
16
+ throw new AgentOpsError("OPENCODE_CONFIG_PATH_INVALID", "OpenCode's config directory must be an absolute path for user-scope installation.");
17
+ }
18
+ const target = resolve(configDirectory, "plugins", "agent-ops.js");
19
+ const relativeTarget = relative(installationRoot, target);
20
+ if (relativeTarget.length === 0 ||
21
+ relativeTarget === ".." ||
22
+ relativeTarget.startsWith(`..${sep}`) ||
23
+ isAbsolute(relativeTarget)) {
24
+ throw new AgentOpsError("OPENCODE_CONFIG_PATH_INVALID", "OpenCode's config directory must stay inside the managed user-scope root.");
25
+ }
26
+ return relativeTarget.split(sep).join("/");
27
+ }
28
+ export function opencodePluginTarget(scope, root, configuredHome, configuredDirectory) {
29
+ return {
30
+ path: scope === "project"
31
+ ? OPENCODE_PLUGIN_PATH
32
+ : root === undefined && configuredHome === undefined &&
33
+ configuredDirectory === undefined &&
34
+ process.env.XDG_CONFIG_HOME === undefined &&
35
+ process.env.OPENCODE_CONFIG_DIR === undefined
36
+ ? OPENCODE_USER_PLUGIN_PATH
37
+ : userPluginPath(root, configuredHome, configuredDirectory),
38
+ representation: "javascript",
39
+ requiresProjectTrust: scope === "project"
40
+ };
41
+ }
42
+ /**
43
+ * Validates the persisted plugin shape without consulting the current
44
+ * environment. User-scope installations may use a custom config directory,
45
+ * so their manifest path is allowed to have any safe prefix but must still
46
+ * name OpenCode's plugin entrypoint.
47
+ */
48
+ export function isOpencodePluginPath(scope, path) {
49
+ if (scope === "project") {
50
+ return path === OPENCODE_PLUGIN_PATH;
51
+ }
52
+ return /^(?:[A-Za-z0-9._-]+\/)*plugins\/agent-ops\.js$/u.test(path);
53
+ }
54
+ function isAbsoluteRuntimePath(value) {
55
+ return (value.startsWith("/") ||
56
+ /^[A-Za-z]:[\\/]/u.test(value) ||
57
+ value.startsWith("\\\\"));
58
+ }
59
+ function assertRuntimePath(runtimePath) {
60
+ if (!isAbsoluteRuntimePath(runtimePath) ||
61
+ runtimePath.length > 4096 ||
62
+ runtimePath.includes("\0") ||
63
+ runtimePath.includes("\n") ||
64
+ runtimePath.includes("\r")) {
65
+ throw new AgentOpsError("OPENCODE_PLUGIN_PATH_INVALID", "opencode plugin runtime path must be absolute and valid.");
66
+ }
67
+ }
68
+ export function opencodePluginEvents(capabilities) {
69
+ const events = [];
70
+ if (capabilities.includes("lifecycle-summary")) {
71
+ events.push("SessionStart");
72
+ }
73
+ if (capabilities.includes("command-policy")) {
74
+ events.push("PreToolUse");
75
+ }
76
+ if (capabilities.includes("optional-stop-verify")) {
77
+ events.push("Stop");
78
+ }
79
+ return events;
80
+ }
81
+ /**
82
+ * Generates the dependency-free plugin installed under the selected local
83
+ * or user plugin directory.
84
+ * The only host dependency is opencode's Bun `$` shell tag, supplied by the
85
+ * plugin context. Keeping the shell function in the context makes the source
86
+ * importable under Node tests with a small fake.
87
+ */
88
+ export function buildOpencodePlugin(capabilities, runtimePath) {
89
+ const events = opencodePluginEvents(capabilities);
90
+ if (events.length === 0) {
91
+ return null;
92
+ }
93
+ assertRuntimePath(runtimePath);
94
+ const lines = [
95
+ "// Managed by agent-ops. Do not edit: `agent-ops update` rewrites this file.",
96
+ `const RUNTIME_PATH = ${JSON.stringify(runtimePath)};`,
97
+ `const MARKER = ${JSON.stringify(OPENCODE_MANAGED_MARKER)};`,
98
+ "",
99
+ "function outputText(result) {",
100
+ " if (typeof result?.stdout === \"string\") return result.stdout;",
101
+ " return new TextDecoder().decode(result?.stdout ?? new Uint8Array());",
102
+ "}",
103
+ "",
104
+ "async function invokeRuntime($, event, payload, projectRoot) {",
105
+ " try {",
106
+ " const result = await $`node ${RUNTIME_PATH} opencode ${event} ${MARKER} < ${new Response(JSON.stringify(payload))}`.cwd(projectRoot).quiet();",
107
+ " const decision = JSON.parse(outputText(result).trim());",
108
+ " if (decision?.decision !== \"allow\" && decision?.decision !== \"deny\") return null;",
109
+ " return decision;",
110
+ " } catch {",
111
+ " return null;",
112
+ " }",
113
+ "}",
114
+ "",
115
+ "async function runManagedHook($, event, payload) {",
116
+ " const decision = await invokeRuntime($, event, payload, payload.projectRoot);",
117
+ " if (decision === null) {",
118
+ " if (event === \"PreToolUse\") throw new Error(\"agent-ops: command policy is unavailable\");",
119
+ " return;",
120
+ " }",
121
+ " if (decision.decision === \"deny\") {",
122
+ " throw new Error(`agent-ops: ${decision.reason ?? \"denied\"}`);",
123
+ " }",
124
+ "}",
125
+ "",
126
+ "export const AgentOps = async ({ $, directory, worktree } = {}) => {",
127
+ " const projectRoot = directory ?? worktree ?? process.cwd();",
128
+ " const hooks = {};"
129
+ ];
130
+ if (events.includes("SessionStart")) {
131
+ lines.push(" // opencode initializes plugins once per app, not once per session.", " await runManagedHook($, \"SessionStart\", {", " event: \"SessionStart\",", " projectRoot", " });");
132
+ }
133
+ if (events.includes("PreToolUse")) {
134
+ lines.push(' hooks["tool.execute.before"] = async (input, output) => {', ' if (input?.tool !== "bash") return;', " await runManagedHook($, \"PreToolUse\", {", " event: \"PreToolUse\",", " projectRoot,", " input,", " output", " });", " };");
135
+ }
136
+ if (events.includes("Stop")) {
137
+ lines.push(" hooks.event = async ({ event }) => {", ' if (event?.type !== "session.idle") return;', " await runManagedHook($, \"Stop\", { event: \"Stop\", projectRoot });", " };");
138
+ }
139
+ lines.push(" return hooks;", "};", "");
140
+ return lines.join("\n");
141
+ }
142
+ export function isOpencodeManagedPlugin(source) {
143
+ return (source !== null &&
144
+ source.includes("Managed by agent-ops") &&
145
+ source.includes(OPENCODE_MANAGED_MARKER));
146
+ }
147
+ /**
148
+ * Registration probing checks the generated source's managed marker and the
149
+ * capability-implied hook registrations. The artifact hash check remains the
150
+ * exact-content check; this probe answers whether the relevant hooks exist.
151
+ */
152
+ export function isOpencodePluginRegistered(source, capabilities) {
153
+ if (typeof source !== "string") {
154
+ return opencodePluginEvents(capabilities).length === 0;
155
+ }
156
+ if (!isOpencodeManagedPlugin(source)) {
157
+ return false;
158
+ }
159
+ return opencodePluginEvents(capabilities).every((event) => {
160
+ if (event === "SessionStart") {
161
+ return source.includes('runManagedHook($, "SessionStart"');
162
+ }
163
+ if (event === "PreToolUse") {
164
+ return source.includes('"tool.execute.before"') &&
165
+ source.includes('runManagedHook($, "PreToolUse"');
166
+ }
167
+ return source.includes('event?.type !== "session.idle"') &&
168
+ source.includes('runManagedHook($, "Stop"');
169
+ });
170
+ }
@@ -0,0 +1,49 @@
1
+ export const OPENCODE_SUPPORTED_EVENTS = [
2
+ "SessionStart",
3
+ "PreToolUse",
4
+ "Stop"
5
+ ];
6
+ /**
7
+ * opencode exposes plugin hooks rather than named lifecycle events. Only
8
+ * `tool.execute.before` is documented as able to stop an action, so blocking
9
+ * outcomes for the other two stay UNKNOWN the way the Codex adapter records
10
+ * unconfirmed native behavior.
11
+ */
12
+ export const OPENCODE_PLUGIN_HOOKS = {
13
+ SessionStart: "plugin-init",
14
+ PreToolUse: "tool.execute.before",
15
+ Stop: "event:session.idle"
16
+ };
17
+ export const OPENCODE_PRE_TOOL_BLOCKING = "throws";
18
+ export const OPENCODE_STOP_BLOCKING = "UNKNOWN";
19
+ /**
20
+ * A plugin's factory runs once when opencode starts, not once per session, so
21
+ * SessionStart is a degraded approximation rather than an equivalent.
22
+ */
23
+ export const OPENCODE_SESSION_START_FIDELITY = "app-init";
24
+ export const OPENCODE_CAPABILITY_REGISTRATIONS = [
25
+ {
26
+ capability: "lifecycle-summary",
27
+ normalizedEvent: "session-start",
28
+ nativeEvent: "SessionStart",
29
+ surfaceId: "opencode-plugin",
30
+ support: "degraded",
31
+ runtimeFailure: "fail-open"
32
+ },
33
+ {
34
+ capability: "command-policy",
35
+ normalizedEvent: "command",
36
+ nativeEvent: "PreToolUse",
37
+ surfaceId: "opencode-plugin",
38
+ support: "supported",
39
+ runtimeFailure: "fail-closed"
40
+ },
41
+ {
42
+ capability: "optional-stop-verify",
43
+ normalizedEvent: "stop",
44
+ nativeEvent: "Stop",
45
+ surfaceId: "opencode-plugin",
46
+ support: "degraded",
47
+ runtimeFailure: "fail-open"
48
+ }
49
+ ];
@@ -0,0 +1,32 @@
1
+ import { normalizeHookEvent } from "../../hooks/normalize.js";
2
+ import { normalizeShellHookEvent } from "../../hooks/shell.js";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ /**
7
+ * The managed plugin forwards opencode's own hook payload untouched under
8
+ * `input`, so every field this adapter reads is documented plugin input rather
9
+ * than a shape the plugin invented.
10
+ */
11
+ export function normalizeOpencodeHookInput(input) {
12
+ if (!isRecord(input)) {
13
+ return normalizeHookEvent(input);
14
+ }
15
+ const projectRoot = input.projectRoot;
16
+ if (input.event === "SessionStart") {
17
+ return normalizeHookEvent({ event: "session-start", projectRoot });
18
+ }
19
+ if (input.event === "Stop") {
20
+ return normalizeHookEvent({ event: "stop", projectRoot });
21
+ }
22
+ if (input.event === "PreToolUse" &&
23
+ isRecord(input.input) &&
24
+ input.input.tool === "bash" &&
25
+ isRecord(input.output) &&
26
+ isRecord(input.output.args) &&
27
+ typeof input.output.args.command === "string" &&
28
+ typeof projectRoot === "string") {
29
+ return normalizeShellHookEvent(input.output.args.command, projectRoot);
30
+ }
31
+ return normalizeHookEvent({ event: "unsupported", projectRoot });
32
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The consumer is the managed plugin, so the wire format is ours to define: one
3
+ * JSON decision on stdout. Only `tool.execute.before` can act on a denial, so
4
+ * the other events always report `allow` and carry the code for logging.
5
+ */
6
+ export function opencodeHookOutput(event, result) {
7
+ const decision = event === "PreToolUse" && result.action === "block"
8
+ ? { decision: "deny", reason: result.code }
9
+ : {
10
+ decision: "allow",
11
+ ...(result.status === "PASS" && event !== "Stop"
12
+ ? {}
13
+ : { reason: result.code }),
14
+ ...(event === "Stop" && result.evidence !== undefined
15
+ ? { evidence: result.evidence }
16
+ : {})
17
+ };
18
+ return {
19
+ exitCode: 0,
20
+ stdout: JSON.stringify(decision),
21
+ stderr: ""
22
+ };
23
+ }
@@ -0,0 +1,23 @@
1
+ import { opencodePluginTarget } from "./config.js";
2
+ export const OPENCODE_SURFACE_IDS = {
3
+ plugin: "opencode-plugin",
4
+ config: "opencode-config"
5
+ };
6
+ export function opencodeSurfaces(scope, root) {
7
+ return [
8
+ {
9
+ id: OPENCODE_SURFACE_IDS.plugin,
10
+ path: opencodePluginTarget(scope, root).path,
11
+ scope,
12
+ access: "managed-default",
13
+ representation: "javascript"
14
+ },
15
+ {
16
+ id: OPENCODE_SURFACE_IDS.config,
17
+ path: "opencode.json",
18
+ scope,
19
+ access: "inspect-only",
20
+ representation: "json"
21
+ }
22
+ ];
23
+ }
@@ -1,6 +1,13 @@
1
1
  export function explainConfig(merged) {
2
2
  return {
3
3
  schemaVersion: merged.config.schemaVersion,
4
+ features: {
5
+ stopVerification: {
6
+ enabled: merged.config.features.stopVerification.enabled,
7
+ source: merged.provenance.features.source,
8
+ sourcePath: merged.provenance.features.sourcePath
9
+ }
10
+ },
4
11
  profiles: merged.provenance.profiles.map((entry) => ({
5
12
  id: entry.value,
6
13
  source: entry.source,
@@ -0,0 +1,24 @@
1
+ import { sha256 } from "../fs/hash.js";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ function canonicalJson(value) {
4
+ if (value === null ||
5
+ typeof value === "boolean" ||
6
+ typeof value === "number" ||
7
+ typeof value === "string") {
8
+ return JSON.stringify(value);
9
+ }
10
+ if (Array.isArray(value)) {
11
+ return `[${value.map(canonicalJson).join(",")}]`;
12
+ }
13
+ if (typeof value === "object") {
14
+ const record = value;
15
+ return `{${Object.keys(record)
16
+ .sort()
17
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
18
+ .join(",")}}`;
19
+ }
20
+ throw new AgentOpsError("CONFIG_HASH_INVALID", "Configuration contains an unsupported value.");
21
+ }
22
+ export function calculateConfigHash(config) {
23
+ return sha256(canonicalJson(config));
24
+ }
@@ -108,8 +108,10 @@ export function mergeConfigLayers(inputLayers) {
108
108
  const mappings = new Map();
109
109
  const exceptions = new Map();
110
110
  let schemaVersion;
111
+ let features;
111
112
  for (const layer of layers) {
112
113
  schemaVersion = effective(layer.config.schemaVersion, layer);
114
+ features = effective(layer.config.features, layer);
113
115
  for (const profile of layer.config.profiles) {
114
116
  profiles.set(profile, effective(profile, layer));
115
117
  }
@@ -139,22 +141,24 @@ export function mergeConfigLayers(inputLayers) {
139
141
  exceptions.set(key, effective(securityException, layer));
140
142
  }
141
143
  }
142
- if (schemaVersion === undefined) {
144
+ if (schemaVersion === undefined || features === undefined) {
143
145
  throw new AgentOpsError("CONFIG_LAYER_REQUIRED", "At least one configuration layer is required.");
144
146
  }
145
147
  const provenance = {
146
148
  schemaVersion,
149
+ features,
147
150
  profiles: [...profiles.values()],
148
151
  verificationCommands: [...commands.values()],
149
152
  pathMappings: [...mappings.values()],
150
153
  securityExceptions: [...exceptions.values()]
151
154
  };
152
155
  const config = {
153
- schemaVersion: 1,
156
+ schemaVersion: schemaVersion.value,
154
157
  profiles: provenance.profiles.map(({ value }) => value),
155
158
  verification: {
156
159
  commands: provenance.verificationCommands.map(({ value }) => value)
157
160
  },
161
+ features: provenance.features.value,
158
162
  pathMappings: provenance.pathMappings.map(({ value }) => value),
159
163
  securityExceptions: provenance.securityExceptions.map(({ value }) => value)
160
164
  };
@@ -1,4 +1,4 @@
1
- import { SCHEMA_VERSION } from "../contracts.js";
1
+ import { CONFIG_SCHEMA_VERSION } from "../contracts.js";
2
2
  import { AgentOpsError } from "../fs/paths.js";
3
3
  import { validateConfig } from "../schema/validate.js";
4
4
  function isRecord(value) {
@@ -18,6 +18,21 @@ const MIGRATIONS = new Map([
18
18
  verification: { commands: clone(commands) }
19
19
  };
20
20
  }
21
+ ],
22
+ [
23
+ 1,
24
+ (input) => {
25
+ const { schemaVersion: _schemaVersion, ...rest } = input;
26
+ return {
27
+ ...rest,
28
+ schemaVersion: CONFIG_SCHEMA_VERSION,
29
+ features: {
30
+ stopVerification: {
31
+ enabled: false
32
+ }
33
+ }
34
+ };
35
+ }
21
36
  ]
22
37
  ]);
23
38
  function schemaVersionOf(value) {
@@ -30,13 +45,13 @@ function schemaVersionOf(value) {
30
45
  }
31
46
  export function previewConfigMigration(input) {
32
47
  const sourceVersion = schemaVersionOf(input);
33
- if (sourceVersion > SCHEMA_VERSION) {
34
- throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${SCHEMA_VERSION}.`);
48
+ if (sourceVersion > CONFIG_SCHEMA_VERSION) {
49
+ throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${CONFIG_SCHEMA_VERSION}.`);
35
50
  }
36
51
  let migrated = clone(input);
37
52
  let version = sourceVersion;
38
53
  const steps = [];
39
- while (version < SCHEMA_VERSION) {
54
+ while (version < CONFIG_SCHEMA_VERSION) {
40
55
  const migration = MIGRATIONS.get(version);
41
56
  if (migration === undefined || !isRecord(migrated)) {
42
57
  throw new AgentOpsError("CONFIG_MIGRATION_MISSING", `No migration is registered from config schemaVersion ${version}.`);
@@ -1 +1,10 @@
1
- export const SCHEMA_VERSION = 1;
1
+ export const CONFIG_SCHEMA_VERSION = 2;
2
+ export const TASK_SCHEMA_VERSION = 1;
3
+ export const EVIDENCE_SCHEMA_VERSION = 1;
4
+ /** @deprecated Use the document-specific schema version constants. */
5
+ export const SCHEMA_VERSION = CONFIG_SCHEMA_VERSION;
6
+ /**
7
+ * The manifest versions independently of config, tasks, and evidence: only its
8
+ * own shape changed when the harness selection became a list.
9
+ */
10
+ export const MANIFEST_SCHEMA_VERSION = 2;
@@ -7,37 +7,54 @@ function assertBlockId(id) {
7
7
  throw new AgentOpsError("INVALID_BLOCK_ID", `Invalid block ID: ${id}`);
8
8
  }
9
9
  }
10
- function assertExactMarkerSyntax(source) {
11
- const validMarker = /^<!-- agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127}) -->$/;
10
+ function assertExactMarkerSyntax(source, markerStyle) {
11
+ const validMarker = markerStyle === "html"
12
+ ? /^<!-- agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127}) -->$/
13
+ : /^# agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127})$/;
14
+ const markerPrefix = markerStyle === "html"
15
+ ? /<!--\s*agent-ops:/
16
+ : /#\s*agent-ops:/;
12
17
  for (const line of source.split(/\r?\n/)) {
13
- if (/<!--\s*agent-ops:/.test(line) && !validMarker.test(line)) {
18
+ if (markerPrefix.test(line) && !validMarker.test(line)) {
14
19
  throw new AgentOpsError("MALFORMED_MANAGED_BLOCK", "Managed block markers must use the exact agent-ops marker syntax.");
15
20
  }
16
21
  }
17
22
  }
18
- export function managedBlockMarkers(id, version) {
23
+ export function managedBlockMarkers(id, version, markerStyle = "html") {
19
24
  assertBlockId(id);
20
25
  if (!Number.isSafeInteger(version) || version < 1) {
21
26
  throw new AgentOpsError("INVALID_BLOCK_VERSION", `Invalid block version: ${version}`);
22
27
  }
23
- return {
24
- start: `<!-- agent-ops:start ${id} v${version} -->`,
25
- end: `<!-- agent-ops:end ${id} -->`
26
- };
28
+ return markerStyle === "html"
29
+ ? {
30
+ start: `<!-- agent-ops:start ${id} v${version} -->`,
31
+ end: `<!-- agent-ops:end ${id} -->`
32
+ }
33
+ : {
34
+ start: `# agent-ops:start ${id} v${version}`,
35
+ end: `# agent-ops:end ${id}`
36
+ };
27
37
  }
28
- function locateMarkers(source, id, version) {
38
+ function locateMarkers(source, id, version, markerStyle = "html") {
29
39
  assertBlockId(id);
30
- assertExactMarkerSyntax(source);
40
+ assertExactMarkerSyntax(source, markerStyle);
31
41
  const escapedId = escapeRegExp(id);
42
+ const startPattern = markerStyle === "html"
43
+ ? `<!-- agent-ops:start ${escapedId} v[0-9]+ -->`
44
+ : `# agent-ops:start ${escapedId} v[0-9]+`;
45
+ const end = markerStyle === "html"
46
+ ? `<!-- agent-ops:end ${id} -->`
47
+ : `# agent-ops:end ${id}`;
32
48
  const startMatches = [
33
- ...source.matchAll(new RegExp(`<!-- agent-ops:start ${escapedId} v[0-9]+ -->`, "g"))
49
+ ...source.matchAll(new RegExp(startPattern, "g"))
34
50
  ];
35
- const end = `<!-- agent-ops:end ${id} -->`;
36
51
  const endMatches = [...source.matchAll(new RegExp(escapeRegExp(end), "g"))];
37
52
  if (startMatches.length === 0 && endMatches.length === 0) {
38
53
  return null;
39
54
  }
40
- const expectedStart = version === undefined ? startMatches[0]?.[0] : managedBlockMarkers(id, version).start;
55
+ const expectedStart = version === undefined
56
+ ? startMatches[0]?.[0]
57
+ : managedBlockMarkers(id, version, markerStyle).start;
41
58
  const start = startMatches[0]?.[0];
42
59
  const startIndex = startMatches[0]?.index;
43
60
  const endIndex = endMatches[0]?.index;
@@ -54,16 +71,16 @@ function locateMarkers(source, id, version) {
54
71
  return { start, end, startIndex, endIndex };
55
72
  }
56
73
  function renderBlock(options) {
57
- if (/<!--\s*agent-ops:/.test(options.content)) {
74
+ if (/(?:<!--|#)\s*agent-ops:/.test(options.content)) {
58
75
  throw new AgentOpsError("AMBIGUOUS_MANAGED_CONTENT", "Managed content must not contain agent-ops marker boundaries.");
59
76
  }
60
- const { start, end } = managedBlockMarkers(options.id, options.version);
77
+ const { start, end } = managedBlockMarkers(options.id, options.version, options.markerStyle);
61
78
  const content = options.content.replace(/\r\n/g, "\n").replace(/\n+$/g, "");
62
79
  return `${start}\n${content}\n${end}`;
63
80
  }
64
81
  export function applyManagedBlock(source, options) {
65
82
  const block = renderBlock(options);
66
- const located = locateMarkers(source, options.id, options.version);
83
+ const located = locateMarkers(source, options.id, options.version, options.markerStyle);
67
84
  if (located === null) {
68
85
  if (source.length === 0) {
69
86
  return `${block}\n`;
@@ -72,8 +89,8 @@ export function applyManagedBlock(source, options) {
72
89
  }
73
90
  return `${source.slice(0, located.startIndex)}${block}${source.slice(located.endIndex + located.end.length)}`;
74
91
  }
75
- export function removeManagedBlock(source, id) {
76
- const located = locateMarkers(source, id);
92
+ export function removeManagedBlock(source, id, markerStyle = "html") {
93
+ const located = locateMarkers(source, id, undefined, markerStyle);
77
94
  if (located === null) {
78
95
  return source;
79
96
  }
@@ -1,6 +1,37 @@
1
+ import { MANIFEST_SCHEMA_VERSION } from "../contracts.js";
1
2
  import { validateManifest } from "../schema/validate.js";
2
3
  import { AgentOpsError } from "./paths.js";
3
4
  export const PROJECT_MANIFEST_PATH = ".agent-ops/manifest.json";
5
+ const LEGACY_HARNESS_SELECTIONS = {
6
+ both: ["codex", "claude"],
7
+ claude: ["claude"],
8
+ codex: ["codex"]
9
+ };
10
+ /**
11
+ * Manifests written up to 0.1.4 stored a single harness string, with `"both"`
12
+ * standing in for two harnesses. Reading one upgrades it in memory; the file is
13
+ * rewritten the next time `update` runs.
14
+ */
15
+ function migrateLegacyManifest(value) {
16
+ if (typeof value !== "object" ||
17
+ value === null ||
18
+ Array.isArray(value)) {
19
+ return value;
20
+ }
21
+ const record = value;
22
+ if (record.schemaVersion !== 1 || typeof record.harness !== "string") {
23
+ return value;
24
+ }
25
+ const harness = LEGACY_HARNESS_SELECTIONS[record.harness];
26
+ if (harness === undefined) {
27
+ return value;
28
+ }
29
+ return {
30
+ ...record,
31
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
32
+ harness
33
+ };
34
+ }
4
35
  export function parseInstallManifest(source) {
5
36
  let value;
6
37
  try {
@@ -9,7 +40,7 @@ export function parseInstallManifest(source) {
9
40
  catch (error) {
10
41
  throw new AgentOpsError("MANIFEST_INVALID_JSON", "The installation manifest is not valid JSON.", { cause: error });
11
42
  }
12
- const result = validateManifest(value);
43
+ const result = validateManifest(migrateLegacyManifest(value));
13
44
  if (!result.ok) {
14
45
  throw new AgentOpsError("MANIFEST_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid installation manifest."}`);
15
46
  }
@@ -147,13 +147,25 @@ function assertTransactionPlan(plan) {
147
147
  }
148
148
  if (operation.kind === "write") {
149
149
  if (typeof operation.content !== "string" ||
150
- !hasOnlyKeys(operation, ["kind", "path", "content", "expectedHash"])) {
150
+ (operation.disclosure !== undefined &&
151
+ operation.disclosure !== "full" &&
152
+ operation.disclosure !== "opaque") ||
153
+ !hasOnlyKeys(operation, [
154
+ "kind",
155
+ "path",
156
+ "content",
157
+ "expectedHash",
158
+ "disclosure"
159
+ ])) {
151
160
  throw new AgentOpsError("INVALID_TRANSACTION_PLAN", `Invalid write operation at index ${index}.`);
152
161
  }
153
162
  continue;
154
163
  }
155
164
  if (operation.kind !== "remove" ||
156
- !hasOnlyKeys(operation, ["kind", "path", "expectedHash"])) {
165
+ (operation.disclosure !== undefined &&
166
+ operation.disclosure !== "full" &&
167
+ operation.disclosure !== "opaque") ||
168
+ !hasOnlyKeys(operation, ["kind", "path", "expectedHash", "disclosure"])) {
157
169
  throw new AgentOpsError("INVALID_TRANSACTION_PLAN", `Invalid remove operation at index ${index}.`);
158
170
  }
159
171
  }
@@ -0,0 +1,16 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { appendLocalLog } from "../logging/local-log.js";
4
+ import { localStatePaths } from "../security/permissions.js";
5
+ /**
6
+ * Records the bounded lifecycle summary signal locally. It deliberately does
7
+ * not include project paths, prompts, commands, or verification evidence.
8
+ */
9
+ export async function runLifecycleAdvisory(_event, options = {}) {
10
+ const home = options.homeDirectory ?? process.env.AGENT_OPS_HOME ?? homedir();
11
+ const state = localStatePaths(home);
12
+ await appendLocalLog(join(state.logDirectory, "events.ndjson"), { type: "diagnostic", code: "LIFECYCLE_SUMMARY" }, {
13
+ anchorDirectory: state.anchorDirectory,
14
+ ...(options.now === undefined ? {} : { now: options.now })
15
+ });
16
+ }