@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.
- package/README.md +164 -13
- package/dist/packages/cli/src/args.js +40 -7
- package/dist/packages/cli/src/bin.js +35 -11
- package/dist/packages/cli/src/cli.js +7 -2
- package/dist/packages/cli/src/codex-loop-process.js +70 -0
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +33 -16
- package/dist/packages/cli/src/commands/init.js +9 -5
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +16 -5
- package/dist/packages/cli/src/context.js +69 -3
- package/dist/packages/cli/src/hook-process.js +231 -16
- package/dist/packages/cli/src/loop-entry.js +8 -0
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +36 -15
- package/dist/runtime/src/adapters/claude/config.js +57 -19
- package/dist/runtime/src/adapters/claude/events.js +33 -0
- package/dist/runtime/src/adapters/claude/output.js +8 -7
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +66 -13
- package/dist/runtime/src/adapters/codex/events.js +33 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/managed-block.js +35 -18
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/codex-loop.js +439 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/codex-loop.js +139 -0
- package/dist/runtime/src/install/doctor.js +182 -14
- package/dist/runtime/src/install/harness.js +294 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +146 -35
- package/dist/runtime/src/install/plan.js +276 -31
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +12 -3
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +11 -4
- package/dist/runtime/src/install/update.js +13 -3
- package/dist/runtime/src/logging/local-log.js +25 -0
- package/dist/runtime/src/schema/validate.js +45 -19
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +159 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +104 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +145 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +89 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +4 -2
- package/schemas/config.schema.json +56 -2
- package/schemas/manifest.schema.json +19 -3
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- package/dist/runtime/src/review/codex-runner.js +0 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { buildClaudeHookSettings, mergeClaudeSettings, stripClaudeManagedHooks } from "../adapters/claude/config.js";
|
|
2
|
-
import { buildCodexHookConfig, mergeCodexHookConfig, stripCodexManagedHooks } from "../adapters/codex/config.js";
|
|
3
1
|
import { AgentOpsError } from "../fs/paths.js";
|
|
2
|
+
import { harnessDescriptor, harnessHookPath } from "./harness.js";
|
|
4
3
|
export const CLAUDE_HOOK_PATH = ".claude/settings.json";
|
|
5
4
|
export const CODEX_HOOK_PATH = ".codex/hooks.json";
|
|
6
5
|
function format(value) {
|
|
@@ -17,31 +16,34 @@ function parseSettings(path, source) {
|
|
|
17
16
|
throw new AgentOpsError("HOOK_SETTINGS_INVALID_JSON", `Hook settings are not valid JSON: ${path}`, { cause: error });
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
export function hookRegistrationPath(harness, scope) {
|
|
19
|
+
export function hookRegistrationPath(harness, scope, root) {
|
|
21
20
|
// ponytail: user scope resolves against AGENT_OPS_HOME, so the same
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
return harness === "claude" ? CLAUDE_HOOK_PATH : CODEX_HOOK_PATH;
|
|
21
|
+
// root can resolve a scope-specific harness path.
|
|
22
|
+
return harnessHookPath(harness, scope, root);
|
|
25
23
|
}
|
|
26
24
|
/**
|
|
27
25
|
* Builds the merged settings file for one harness, preserving foreign hooks.
|
|
28
26
|
* Returns null when the selected capabilities register no events at all.
|
|
29
27
|
*/
|
|
30
28
|
export function planHookRegistration(options) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const descriptor = harnessDescriptor(options.harness);
|
|
30
|
+
if (descriptor.control.buildHooks === undefined ||
|
|
31
|
+
descriptor.control.mergeHooks === undefined) {
|
|
32
|
+
// File-backed adapters (currently opencode) track their plugin as a
|
|
33
|
+
// managed artifact rather than as a ManagedHookRecord.
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const path = options.path ?? hookRegistrationPath(options.harness, options.scope);
|
|
37
|
+
const managed = descriptor.control.buildHooks(options.capabilities, options.runtimePath);
|
|
35
38
|
const events = Object.keys(managed.hooks);
|
|
36
39
|
if (events.length === 0) {
|
|
37
40
|
return null;
|
|
38
41
|
}
|
|
39
42
|
const existing = parseSettings(path, options.currentSource);
|
|
40
|
-
const merged =
|
|
41
|
-
? mergeClaudeSettings(existing, managed)
|
|
42
|
-
: mergeCodexHookConfig(existing, managed);
|
|
43
|
+
const merged = descriptor.control.mergeHooks(existing, managed);
|
|
43
44
|
return {
|
|
44
45
|
content: format(merged),
|
|
46
|
+
disclosure: "opaque",
|
|
45
47
|
record: {
|
|
46
48
|
id: `${options.harness}-hooks`,
|
|
47
49
|
path,
|
|
@@ -52,7 +54,7 @@ export function planHookRegistration(options) {
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
function onlyManagedRemains(harness, value) {
|
|
55
|
-
const ownKeys = new Set(harness
|
|
57
|
+
const ownKeys = new Set(harnessDescriptor(harness).control.ownSettingsKeys ?? []);
|
|
56
58
|
const hooks = value.hooks;
|
|
57
59
|
return (Object.keys(value).every((key) => ownKeys.has(key)) &&
|
|
58
60
|
typeof hooks === "object" &&
|
|
@@ -64,12 +66,15 @@ function onlyManagedRemains(harness, value) {
|
|
|
64
66
|
* when nothing but agent-ops content is left, so foreign settings survive.
|
|
65
67
|
*/
|
|
66
68
|
export function planHookRemoval(record, currentSource) {
|
|
69
|
+
const descriptor = harnessDescriptor(record.harness);
|
|
70
|
+
if (descriptor.control.stripHooks === undefined) {
|
|
71
|
+
throw new AgentOpsError("HOOK_REMOVAL_UNSUPPORTED", `Harness hook records are not supported for ${record.harness}.`);
|
|
72
|
+
}
|
|
67
73
|
const existing = parseSettings(record.path, currentSource);
|
|
68
|
-
const stripped =
|
|
69
|
-
? stripClaudeManagedHooks(existing)
|
|
70
|
-
: stripCodexManagedHooks(existing);
|
|
74
|
+
const stripped = descriptor.control.stripHooks(existing);
|
|
71
75
|
return {
|
|
72
76
|
path: record.path,
|
|
77
|
+
disclosure: "opaque",
|
|
73
78
|
content: onlyManagedRemains(record.harness, stripped)
|
|
74
79
|
? null
|
|
75
80
|
: format(stripped)
|
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function expectedMarker(manifest, id) {
|
|
11
|
-
const isCodex = id === "codex";
|
|
12
|
-
const markerId = `${id}-routing`;
|
|
13
|
-
const markers = managedBlockMarkers(markerId, 1);
|
|
14
|
-
const instructionFile = isCodex ? "AGENTS.md" : "CLAUDE.md";
|
|
3
|
+
import { harnessDescriptor, harnessHookPath, routingBlockId, selectHarnessHookSurface, rulesArtifactId } from "./harness.js";
|
|
4
|
+
import { isOpencodePluginPath } from "../adapters/opencode/config.js";
|
|
5
|
+
import { LOOP_MARKER_ID, LOOP_MARKER_VERSION, loopIgnoreContent, loopLauncherArtifactId, loopLauncherPath, selectedLoopHarnesses } from "./codex-loop.js";
|
|
6
|
+
function expectedMarker(manifest, id, markerId) {
|
|
7
|
+
const descriptor = harnessDescriptor(id);
|
|
8
|
+
const markers = managedBlockMarkers(markerId, 1, "html");
|
|
15
9
|
return {
|
|
16
10
|
id: markerId,
|
|
17
11
|
path: manifest.scope === "project"
|
|
18
|
-
? instructionFile
|
|
19
|
-
: `.${id}/${instructionFile}`,
|
|
12
|
+
? descriptor.control.instructionFile
|
|
13
|
+
: `.${id}/${descriptor.control.instructionFile}`,
|
|
14
|
+
startMarker: markers.start,
|
|
15
|
+
endMarker: markers.end,
|
|
16
|
+
markerStyle: "html",
|
|
17
|
+
content: descriptor.control.routing.desired,
|
|
18
|
+
legacyContent: descriptor.control.routing.legacy
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function expectedLoopMarker(manifest) {
|
|
22
|
+
const markers = managedBlockMarkers(LOOP_MARKER_ID, LOOP_MARKER_VERSION, "hash");
|
|
23
|
+
return {
|
|
24
|
+
id: LOOP_MARKER_ID,
|
|
25
|
+
path: ".gitignore",
|
|
20
26
|
startMarker: markers.start,
|
|
21
27
|
endMarker: markers.end,
|
|
22
|
-
|
|
28
|
+
markerStyle: "hash",
|
|
29
|
+
content: loopIgnoreContent(manifest.harness),
|
|
30
|
+
legacyContent: []
|
|
23
31
|
};
|
|
24
32
|
}
|
|
33
|
+
function pathKey(path) {
|
|
34
|
+
return path.toLowerCase();
|
|
35
|
+
}
|
|
25
36
|
function manifestOwnershipError() {
|
|
26
37
|
return new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest does not match a supported managed installation shape.");
|
|
27
38
|
}
|
|
@@ -29,37 +40,129 @@ function manifestOwnershipError() {
|
|
|
29
40
|
* Hook records are optional: installations without hook capabilities, and
|
|
30
41
|
* manifests written before hook registration existed, carry none.
|
|
31
42
|
*/
|
|
32
|
-
function assertSupportedHookRecords(manifest, harnesses) {
|
|
43
|
+
function assertSupportedHookRecords(manifest, harnesses, root) {
|
|
33
44
|
const selected = new Set(harnesses);
|
|
34
45
|
const seen = new Set();
|
|
35
46
|
for (const hook of manifest.hooks ?? []) {
|
|
36
47
|
if (!selected.has(hook.harness) ||
|
|
37
48
|
seen.has(hook.harness) ||
|
|
49
|
+
harnessDescriptor(hook.harness).control.buildHooks === undefined ||
|
|
38
50
|
hook.id !== `${hook.harness}-hooks` ||
|
|
39
|
-
hook.path !== hookRegistrationPath(hook.harness, manifest.scope) ||
|
|
40
51
|
hook.events.length === 0) {
|
|
41
52
|
throw manifestOwnershipError();
|
|
42
53
|
}
|
|
54
|
+
try {
|
|
55
|
+
selectHarnessHookSurface({
|
|
56
|
+
harness: hook.harness,
|
|
57
|
+
scope: manifest.scope,
|
|
58
|
+
root,
|
|
59
|
+
persistedPath: hook.path
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw manifestOwnershipError();
|
|
64
|
+
}
|
|
43
65
|
seen.add(hook.harness);
|
|
44
66
|
}
|
|
45
67
|
}
|
|
46
|
-
export function assertSupportedManifestOwnership(manifest) {
|
|
47
|
-
const harnesses =
|
|
48
|
-
const
|
|
49
|
-
[
|
|
68
|
+
export function assertSupportedManifestOwnership(manifest, root) {
|
|
69
|
+
const harnesses = manifest.harness;
|
|
70
|
+
const expectedArtifactPaths = new Map([
|
|
71
|
+
[
|
|
72
|
+
pathKey(".agent-ops/config.json"),
|
|
73
|
+
{ path: ".agent-ops/config.json", ids: new Set(["config"]) }
|
|
74
|
+
]
|
|
75
|
+
]);
|
|
76
|
+
const requiredArtifactPaths = new Set([
|
|
77
|
+
pathKey(".agent-ops/config.json")
|
|
50
78
|
]);
|
|
51
79
|
const expectedMarkers = new Map();
|
|
80
|
+
const expectedMarkerPaths = new Set();
|
|
81
|
+
const loopHarnesses = selectedLoopHarnesses(harnesses);
|
|
82
|
+
const hasLoopArtifacts = manifest.artifacts.some(({ id }) => loopHarnesses.some((harness) => id === loopLauncherArtifactId(harness)));
|
|
83
|
+
const hasLoopMarker = manifest.markers.some(({ id }) => id === LOOP_MARKER_ID);
|
|
84
|
+
const hasLoop = hasLoopArtifacts || hasLoopMarker;
|
|
85
|
+
if (hasLoop &&
|
|
86
|
+
(manifest.scope !== "project" || loopHarnesses.length === 0)) {
|
|
87
|
+
throw manifestOwnershipError();
|
|
88
|
+
}
|
|
89
|
+
const recordedOpencodePluginPath = manifest.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
|
|
90
|
+
if (recordedOpencodePluginPath !== undefined &&
|
|
91
|
+
(!harnesses.includes("opencode") ||
|
|
92
|
+
!isOpencodePluginPath(manifest.scope, recordedOpencodePluginPath))) {
|
|
93
|
+
throw manifestOwnershipError();
|
|
94
|
+
}
|
|
52
95
|
for (const id of harnesses) {
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
96
|
+
const descriptor = harnessDescriptor(id);
|
|
97
|
+
const artifactPath = `.agent-ops/${descriptor.control.instructionFile}`;
|
|
98
|
+
const artifactKey = pathKey(artifactPath);
|
|
99
|
+
const artifactEntry = expectedArtifactPaths.get(artifactKey);
|
|
100
|
+
const artifactIds = artifactEntry?.ids ?? new Set();
|
|
101
|
+
artifactIds.add(rulesArtifactId(descriptor));
|
|
102
|
+
// Accept the PR 2 per-harness spelling while update rewrites it to the
|
|
103
|
+
// path-derived ID. This keeps already-installed manifests removable.
|
|
104
|
+
artifactIds.add(`${id}-rules`);
|
|
105
|
+
expectedArtifactPaths.set(artifactKey, {
|
|
106
|
+
path: artifactEntry?.path ?? artifactPath,
|
|
107
|
+
ids: artifactIds
|
|
108
|
+
});
|
|
109
|
+
requiredArtifactPaths.add(artifactKey);
|
|
110
|
+
const markerPath = manifest.scope === "project"
|
|
111
|
+
? descriptor.control.instructionFile
|
|
112
|
+
: `.${id}/${descriptor.control.instructionFile}`;
|
|
113
|
+
const markerKey = pathKey(markerPath);
|
|
114
|
+
expectedMarkerPaths.add(markerKey);
|
|
115
|
+
const currentId = routingBlockId(id, manifest.scope, descriptor);
|
|
116
|
+
const markerIds = new Set([currentId, `${id}-routing`]);
|
|
117
|
+
for (const markerId of markerIds) {
|
|
118
|
+
expectedMarkers.set(markerId, expectedMarker(manifest, id, markerId));
|
|
119
|
+
}
|
|
120
|
+
if (id === "opencode") {
|
|
121
|
+
const pluginPath = recordedOpencodePluginPath ?? harnessHookPath(id, manifest.scope, root);
|
|
122
|
+
const pluginKey = pathKey(pluginPath);
|
|
123
|
+
const pluginEntry = expectedArtifactPaths.get(pluginKey);
|
|
124
|
+
if (pluginEntry !== undefined && pluginEntry.path !== pluginPath) {
|
|
125
|
+
throw manifestOwnershipError();
|
|
126
|
+
}
|
|
127
|
+
const pluginIds = pluginEntry?.ids ?? new Set();
|
|
128
|
+
pluginIds.add("opencode-plugin");
|
|
129
|
+
expectedArtifactPaths.set(pluginKey, {
|
|
130
|
+
path: pluginPath,
|
|
131
|
+
ids: pluginIds
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (hasLoop) {
|
|
136
|
+
for (const harness of loopHarnesses) {
|
|
137
|
+
const path = loopLauncherPath(harness);
|
|
138
|
+
expectedArtifactPaths.set(pathKey(path), {
|
|
139
|
+
path,
|
|
140
|
+
ids: new Set([loopLauncherArtifactId(harness)])
|
|
141
|
+
});
|
|
142
|
+
requiredArtifactPaths.add(pathKey(path));
|
|
143
|
+
}
|
|
144
|
+
expectedMarkerPaths.add(pathKey(".gitignore"));
|
|
145
|
+
expectedMarkers.set(LOOP_MARKER_ID, expectedLoopMarker(manifest));
|
|
56
146
|
}
|
|
57
|
-
|
|
58
|
-
|
|
147
|
+
const opencodePluginPath = harnesses.includes("opencode")
|
|
148
|
+
? recordedOpencodePluginPath ??
|
|
149
|
+
harnessHookPath("opencode", manifest.scope, root)
|
|
150
|
+
: null;
|
|
151
|
+
const optionalArtifactCount = opencodePluginPath !== null &&
|
|
152
|
+
expectedArtifactPaths.has(pathKey(opencodePluginPath))
|
|
153
|
+
? 1
|
|
154
|
+
: 0;
|
|
155
|
+
if ((manifest.artifacts.length !== requiredArtifactPaths.size &&
|
|
156
|
+
manifest.artifacts.length !== requiredArtifactPaths.size +
|
|
157
|
+
optionalArtifactCount) ||
|
|
158
|
+
manifest.markers.length !== expectedMarkerPaths.size) {
|
|
59
159
|
throw manifestOwnershipError();
|
|
60
160
|
}
|
|
61
161
|
for (const artifact of manifest.artifacts) {
|
|
62
|
-
|
|
162
|
+
const expected = expectedArtifactPaths.get(pathKey(artifact.path));
|
|
163
|
+
if (expected === undefined ||
|
|
164
|
+
expected.path !== artifact.path ||
|
|
165
|
+
!expected.ids.has(artifact.id)) {
|
|
63
166
|
throw manifestOwnershipError();
|
|
64
167
|
}
|
|
65
168
|
}
|
|
@@ -72,7 +175,7 @@ export function assertSupportedManifestOwnership(manifest) {
|
|
|
72
175
|
throw manifestOwnershipError();
|
|
73
176
|
}
|
|
74
177
|
}
|
|
75
|
-
assertSupportedHookRecords(manifest, harnesses);
|
|
178
|
+
assertSupportedHookRecords(manifest, harnesses, root);
|
|
76
179
|
return expectedMarkers;
|
|
77
180
|
}
|
|
78
181
|
function exactMarkerCount(source, marker) {
|
|
@@ -92,13 +195,21 @@ export function assertExpectedManagedBlock(source, marker, expected) {
|
|
|
92
195
|
startIndex >= endIndex) {
|
|
93
196
|
throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block boundaries changed after installation: ${marker.path}`);
|
|
94
197
|
}
|
|
95
|
-
const expectedBlock = applyManagedBlock("", {
|
|
96
|
-
id: expected.id,
|
|
97
|
-
version: 1,
|
|
98
|
-
content: expected.content
|
|
99
|
-
}).replace(/\n$/u, "");
|
|
100
198
|
const currentBlock = source.slice(startIndex, endIndex + marker.endMarker.length);
|
|
101
|
-
|
|
102
|
-
|
|
199
|
+
const candidates = [
|
|
200
|
+
["desired", expected.content],
|
|
201
|
+
...expected.legacyContent.map((content) => ["legacy", content])
|
|
202
|
+
];
|
|
203
|
+
for (const [kind, content] of candidates) {
|
|
204
|
+
const expectedBlock = applyManagedBlock("", {
|
|
205
|
+
id: expected.id,
|
|
206
|
+
version: 1,
|
|
207
|
+
content,
|
|
208
|
+
markerStyle: expected.markerStyle
|
|
209
|
+
}).replace(/\n$/u, "");
|
|
210
|
+
if (currentBlock === expectedBlock) {
|
|
211
|
+
return kind;
|
|
212
|
+
}
|
|
103
213
|
}
|
|
214
|
+
throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block content changed after installation: ${marker.path}`);
|
|
104
215
|
}
|