@agentxm/workspace-state 0.30.1 → 0.30.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.
@@ -22,6 +22,7 @@ export { SETTINGS_KNOWN_KEYS } from "./settings/schema.js";
22
22
  export { gitSourceLockFields } from "./lockfile/entry-fields.js";
23
23
  export { LOCK_ENTRY_SCHEMA_BY_TYPE } from "./lockfile/schema.js";
24
24
  export { ACQUIRED_EXTENSIONS_DIR, LOCK_FILENAME } from "./workspace/constants.js";
25
+ export { isInstallRootStagingName, observeInstallRoot, type InstallRootEntry, type InstallRootInventory, type InstalledPackageEntry, type ObserveInstallRootArgs, type UnrecognizedInstallRootEntry, } from "./workspace/install-root.js";
25
26
  export { acquiredExtensionDisplayPath, acquiredExtensionDisplayPathFromLockEntry, computeExtensionPathsForLayout, extensionPathSourceFromLockEntry, extensionContentFilename, extensionContentPath, type ExtensionPathLockEntry, type ExtensionPathSource, type ExtensionDirPaths, } from "./workspace/extension-paths.js";
26
27
  export { RenderedFilePathSchema, RenderedFilesMapSchema, computeSourceHash, type RenderedFilePath, type RenderedFilesMap, } from "./workspace/rendered-files.js";
27
28
  export { computePackageContentHash } from "./workspace/package-hash.js";
package/dist/src/index.js CHANGED
@@ -27,6 +27,7 @@ export { gitSourceLockFields } from "./lockfile/entry-fields.js";
27
27
  export { LOCK_ENTRY_SCHEMA_BY_TYPE } from "./lockfile/schema.js";
28
28
  // Extension path and identity vocabulary
29
29
  export { ACQUIRED_EXTENSIONS_DIR, LOCK_FILENAME } from "./workspace/constants.js";
30
+ export { isInstallRootStagingName, observeInstallRoot, } from "./workspace/install-root.js";
30
31
  export { acquiredExtensionDisplayPath, acquiredExtensionDisplayPathFromLockEntry, computeExtensionPathsForLayout, extensionPathSourceFromLockEntry, extensionContentFilename, extensionContentPath, } from "./workspace/extension-paths.js";
31
32
  export { RenderedFilePathSchema, RenderedFilesMapSchema, computeSourceHash, } from "./workspace/rendered-files.js";
32
33
  export { computePackageContentHash } from "./workspace/package-hash.js";
@@ -38,8 +38,8 @@ export type LockfileUpdate = (lockfile: Lockfile) => Lockfile;
38
38
  *
39
39
  * @experimental This API is unstable and may change without notice.
40
40
  */
41
- export declare const writeLockfileAtPath: (lockfilePath: string, lockfile: Lockfile) => Effect.Effect<void, LockfileWriteError, FileSystem.FileSystem | Path.Path>;
42
- export declare const writeLockfile: (axmDir: string, lockfile: Lockfile) => Effect.Effect<void, LockfileWriteError, FileSystem.FileSystem | Path.Path>;
41
+ export declare const writeLockfileAtPath: (lockfilePath: string, lockfile: Lockfile) => Effect.Effect<void, import("@agentxm/workspace-transactions").WorkspaceSnapshotError | LockfileWriteError, FileSystem.FileSystem | Path.Path>;
42
+ export declare const writeLockfile: (axmDir: string, lockfile: Lockfile) => Effect.Effect<void, import("@agentxm/workspace-transactions").WorkspaceSnapshotError | LockfileWriteError, FileSystem.FileSystem | Path.Path>;
43
43
  /**
44
44
  * Applies lockfile updates in order without writing to disk.
45
45
  *
@@ -782,7 +782,7 @@ export declare const commitLockfileUpdates: (axmDir: string, lockfile: Lockfile,
782
782
  readonly treeIntegrity: string & import("effect/Brand").Brand<"TreeIntegrity">;
783
783
  };
784
784
  } | undefined;
785
- }, LockfileWriteError | LockfileValidationError, FileSystem.FileSystem | Path.Path>;
785
+ }, import("@agentxm/workspace-transactions").WorkspaceSnapshotError | LockfileWriteError | LockfileValidationError, FileSystem.FileSystem | Path.Path>;
786
786
  /**
787
787
  * Commits a caller-computed lockfile snapshot as an entry-level patch.
788
788
  *
@@ -1522,7 +1522,7 @@ export declare const commitLockfileSnapshotUpdateAtPath: (lockfilePath: string,
1522
1522
  readonly treeIntegrity: string & import("effect/Brand").Brand<"TreeIntegrity">;
1523
1523
  };
1524
1524
  } | undefined;
1525
- }, LockfileWriteError | LockfileValidationError, FileSystem.FileSystem | Path.Path>;
1525
+ }, import("@agentxm/workspace-transactions").WorkspaceSnapshotError | LockfileWriteError | LockfileValidationError, FileSystem.FileSystem | Path.Path>;
1526
1526
  export declare const commitLockfileSnapshotUpdate: (axmDir: string, base: Lockfile, next: Lockfile) => Effect.Effect<{
1527
1527
  readonly skills: {
1528
1528
  readonly [x: string]: {
@@ -2251,5 +2251,5 @@ export declare const commitLockfileSnapshotUpdate: (axmDir: string, base: Lockfi
2251
2251
  readonly treeIntegrity: string & import("effect/Brand").Brand<"TreeIntegrity">;
2252
2252
  };
2253
2253
  } | undefined;
2254
- }, LockfileWriteError | LockfileValidationError, FileSystem.FileSystem | Path.Path>;
2254
+ }, import("@agentxm/workspace-transactions").WorkspaceSnapshotError | LockfileWriteError | LockfileValidationError, FileSystem.FileSystem | Path.Path>;
2255
2255
  //# sourceMappingURL=lockfile.d.ts.map
@@ -14,7 +14,7 @@ import * as Effect from "effect/Effect";
14
14
  import * as Schema from "effect/Schema";
15
15
  import * as Semaphore from "effect/Semaphore";
16
16
  import YAML from "yaml";
17
- import { recordFootprint, sweepStaleAtomicWriteTemps, writeFileAtomic, } from "@agentxm/workspace-transactions";
17
+ import { protectWorkspacePath, recordFootprint, sweepStaleAtomicWriteTemps, writeFileAtomic, } from "@agentxm/workspace-transactions";
18
18
  import { LockfileValidationError, LockfileWriteError } from "./errors.js";
19
19
  import { LOCKFILE_VERSION, LockfileSchema } from "./schema.js";
20
20
  // -----------------------------------------------------------------------------
@@ -132,6 +132,7 @@ const withLockfileLock = (lockfilePath, effect) => Effect.gen(function* () {
132
132
  const writeLockfileUnlocked = (lockfilePath, lockfile) => Effect.gen(function* () {
133
133
  const fs = yield* FileSystem.FileSystem;
134
134
  const yamlContent = yield* encodeLockfileYaml(lockfilePath, lockfile);
135
+ yield* protectWorkspacePath(lockfilePath);
135
136
  const existed = yield* fs.exists(lockfilePath).pipe(Effect.orElseSucceed(() => true));
136
137
  yield* sweepStaleAtomicWriteTemps(fs, lockfilePath);
137
138
  const written = yield* writeFileAtomic(fs, {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Install-root inventory.
3
+ *
4
+ * Classifies every entry in a scope's install root (`agent_extensions`): an
5
+ * installed package whose path gives a well-formed extension identity or is
6
+ * proven by an accepted resolution, AXM's own interrupted staging, or an
7
+ * unrecognized entry. Each installed package records whether desired state
8
+ * still reaches it; one that nothing reaches is a leftover.
9
+ *
10
+ * Location inside the install root is AXM's install proof for canonical
11
+ * package directories only. It never proves ownership of agent projections.
12
+ *
13
+ * @experimental This API is unstable and may change without notice.
14
+ * @packageDocumentation
15
+ */
16
+ import * as Effect from "effect/Effect";
17
+ import * as FileSystem from "effect/FileSystem";
18
+ import * as Path from "effect/Path";
19
+ import { type ExtensionType } from "@agentxm/extension-model/unstable/extensions";
20
+ import type { DesiredStateGraph } from "./desired-state-graph.js";
21
+ import type { WorkspaceLayout } from "./layout.js";
22
+ import type { LockfileReaderService } from "./lockfile-reader.js";
23
+ /** An installed package directory in the install root. */
24
+ export interface InstalledPackageEntry {
25
+ readonly kind: "package";
26
+ readonly type: ExtensionType;
27
+ readonly name: string;
28
+ /** The owner handle the path names, when the path gives one. */
29
+ readonly owner: string | undefined;
30
+ /** The source directory the package sits under, when the path gives one. */
31
+ readonly sourceDirectory: string | undefined;
32
+ /** Absolute canonical package path. */
33
+ readonly path: string;
34
+ /** The lock key of the accepted resolution whose canonical path this is. */
35
+ readonly lockKey: string | undefined;
36
+ /** Whether any desired route reaches this canonical package. */
37
+ readonly reached: boolean;
38
+ }
39
+ /** An install-root entry that is neither an installed package nor AXM staging. */
40
+ export interface UnrecognizedInstallRootEntry {
41
+ readonly kind: "unrecognized";
42
+ readonly path: string;
43
+ readonly entryKind: "file" | "directory" | "symlink";
44
+ }
45
+ export type InstallRootEntry = InstalledPackageEntry | UnrecognizedInstallRootEntry;
46
+ export interface InstallRootInventory {
47
+ readonly root: string;
48
+ readonly packages: ReadonlyArray<InstalledPackageEntry>;
49
+ readonly leftovers: ReadonlyArray<InstalledPackageEntry>;
50
+ readonly unrecognized: ReadonlyArray<UnrecognizedInstallRootEntry>;
51
+ }
52
+ /** AXM's own interrupted replacement directories beside a canonical package. */
53
+ export declare const isInstallRootStagingName: (name: string) => boolean;
54
+ export interface ObserveInstallRootArgs {
55
+ readonly layout: WorkspaceLayout;
56
+ readonly graph: DesiredStateGraph;
57
+ readonly locks: LockfileReaderService;
58
+ }
59
+ /**
60
+ * Observe the install root. When desired state is incomplete, every installed
61
+ * package counts as reached: no leftover is claimed without full reachability.
62
+ */
63
+ export declare const observeInstallRoot: ({ layout, graph, locks }: ObserveInstallRootArgs) => Effect.Effect<{
64
+ root: string & import("effect/Brand").Brand<"AbsolutePath">;
65
+ packages: InstalledPackageEntry[];
66
+ leftovers: InstalledPackageEntry[];
67
+ unrecognized: UnrecognizedInstallRootEntry[];
68
+ }, import("./service-interface.ts").WorkspaceLockfileReadFailure, FileSystem.FileSystem | Path.Path>;
69
+ //# sourceMappingURL=install-root.d.ts.map
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Install-root inventory.
3
+ *
4
+ * Classifies every entry in a scope's install root (`agent_extensions`): an
5
+ * installed package whose path gives a well-formed extension identity or is
6
+ * proven by an accepted resolution, AXM's own interrupted staging, or an
7
+ * unrecognized entry. Each installed package records whether desired state
8
+ * still reaches it; one that nothing reaches is a leftover.
9
+ *
10
+ * Location inside the install root is AXM's install proof for canonical
11
+ * package directories only. It never proves ownership of agent projections.
12
+ *
13
+ * @experimental This API is unstable and may change without notice.
14
+ * @packageDocumentation
15
+ */
16
+ import * as Effect from "effect/Effect";
17
+ import * as FileSystem from "effect/FileSystem";
18
+ import * as Path from "effect/Path";
19
+ import { extensionTypes, isExtensionTypePlural, toExtensionType, toExtensionTypePlural, } from "@agentxm/extension-model/unstable/extensions";
20
+ import { sanitizeName } from "./extension-name.js";
21
+ import { computeExtensionPathsForLayout, extensionPathSourceFromLockEntry, } from "./extension-paths.js";
22
+ /** AXM's own interrupted replacement directories beside a canonical package. */
23
+ export const isInstallRootStagingName = (name) => name.endsWith(".axm-staging") || name.endsWith(".axm-backup");
24
+ const PLATFORM_METADATA_FILES = new Set([".DS_Store", "Thumbs.db"]);
25
+ const isCanonicalName = (name) => sanitizeName(name) === name;
26
+ const isOwnerSegment = (segment) => segment.startsWith("@") && segment.length > 1 && isCanonicalName(segment.slice(1));
27
+ /**
28
+ * `<source>/@<owner>/<plural>/<name>` for acquired packages, or
29
+ * `@<owner>/<plural>/<name>` for user-scope authored packages.
30
+ */
31
+ const identityOf = (segments) => {
32
+ const [first = "", second = "", third = "", fourth = ""] = segments;
33
+ const shape = segments.length === 4 && !first.startsWith("@")
34
+ ? { sourceDirectory: first, owner: second, plural: third, name: fourth }
35
+ : segments.length === 3
36
+ ? { sourceDirectory: undefined, owner: first, plural: second, name: third }
37
+ : undefined;
38
+ if (shape === undefined || !isOwnerSegment(shape.owner) || !isCanonicalName(shape.name))
39
+ return undefined;
40
+ if (!isExtensionTypePlural(shape.plural))
41
+ return undefined;
42
+ return {
43
+ type: toExtensionType(shape.plural),
44
+ name: shape.name,
45
+ owner: shape.owner,
46
+ sourceDirectory: shape.sourceDirectory,
47
+ };
48
+ };
49
+ /** Whether a directory can still lead to a well-formed identity path. */
50
+ const isIdentityPrefix = (segments) => {
51
+ const [first = "", second = "", third = ""] = segments;
52
+ switch (segments.length) {
53
+ case 1:
54
+ return true;
55
+ case 2:
56
+ return first.startsWith("@")
57
+ ? isOwnerSegment(first) && isExtensionTypePlural(second)
58
+ : isOwnerSegment(second);
59
+ case 3:
60
+ return !first.startsWith("@") && isExtensionTypePlural(third);
61
+ default:
62
+ return false;
63
+ }
64
+ };
65
+ /**
66
+ * Observe the install root. When desired state is incomplete, every installed
67
+ * package counts as reached: no leftover is claimed without full reachability.
68
+ */
69
+ export const observeInstallRoot = ({ layout, graph, locks }) => Effect.gen(function* () {
70
+ const fs = yield* FileSystem.FileSystem;
71
+ const path = yield* Path.Path;
72
+ const root = layout.acquiredRoot;
73
+ const accepted = (yield* Effect.forEach(extensionTypes, (type) => locks
74
+ .entries(type)
75
+ .pipe(Effect.map((entries) => Object.entries(entries).map(([key, entry]) => ({ type, key, entry })))))).flat();
76
+ const nodeReaches = (type, key) => graph.nodes.some((node) => node.type === type && (type === "mcp-server" ? node.identity === key : node.name === key));
77
+ const lockedPaths = new Map(accepted.map((row) => [
78
+ computeExtensionPathsForLayout(path.join, layout, extensionPathSourceFromLockEntry(row.entry), toExtensionTypePlural(row.type), row.entry.workspaceName).canonicalPath,
79
+ { ...row, reached: nodeReaches(row.type, row.key) },
80
+ ]));
81
+ // A desired extension whose resolution is not yet accepted may still be
82
+ // materialized into this path, so it reaches every same-named package.
83
+ const unlockedDesired = graph.nodes.filter((node) => !accepted.some((row) => row.type === node.type &&
84
+ (node.type === "mcp-server" ? row.key === node.identity : row.key === node.name)));
85
+ const packages = [];
86
+ const unrecognized = [];
87
+ const isLink = (target) => fs.readLink(target).pipe(Effect.as(true), Effect.orElseSucceed(() => false));
88
+ const leadsToLockedPath = (directory) => [...lockedPaths.keys()].some((locked) => {
89
+ const relative = path.relative(directory, locked);
90
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
91
+ });
92
+ const visit = (directory, segments) => Effect.gen(function* () {
93
+ const names = yield* fs.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
94
+ for (const name of [...names].sort()) {
95
+ const absolute = path.join(directory, name);
96
+ const childSegments = [...segments, name];
97
+ if (isInstallRootStagingName(name) || PLATFORM_METADATA_FILES.has(name))
98
+ continue;
99
+ if (yield* isLink(absolute)) {
100
+ unrecognized.push({ kind: "unrecognized", path: absolute, entryKind: "symlink" });
101
+ continue;
102
+ }
103
+ const info = yield* fs.stat(absolute).pipe(Effect.option);
104
+ if (info._tag === "None")
105
+ continue;
106
+ if (info.value.type !== "Directory") {
107
+ unrecognized.push({ kind: "unrecognized", path: absolute, entryKind: "file" });
108
+ continue;
109
+ }
110
+ const locked = lockedPaths.get(absolute);
111
+ const identity = identityOf(childSegments);
112
+ const type = locked?.type ?? identity?.type;
113
+ const packageName = locked?.entry.workspaceName ?? identity?.name;
114
+ if (type !== undefined && packageName !== undefined) {
115
+ // A path without a source directory is authored content, which is
116
+ // never inferred to be undesired.
117
+ const reached = !graph.complete ||
118
+ (locked === undefined && identity?.sourceDirectory === undefined) ||
119
+ (locked?.reached ?? false) ||
120
+ unlockedDesired.some((node) => node.type === type && node.name === packageName);
121
+ packages.push({
122
+ kind: "package",
123
+ type,
124
+ name: packageName,
125
+ owner: identity?.owner,
126
+ sourceDirectory: identity?.sourceDirectory,
127
+ path: absolute,
128
+ lockKey: locked?.key,
129
+ reached,
130
+ });
131
+ continue;
132
+ }
133
+ if (isIdentityPrefix(childSegments) || leadsToLockedPath(absolute)) {
134
+ yield* visit(absolute, childSegments);
135
+ continue;
136
+ }
137
+ unrecognized.push({ kind: "unrecognized", path: absolute, entryKind: "directory" });
138
+ }
139
+ });
140
+ if (yield* fs.exists(root).pipe(Effect.orElseSucceed(() => false)))
141
+ yield* visit(root, []);
142
+ return {
143
+ root,
144
+ packages,
145
+ leftovers: packages.filter((entry) => !entry.reached),
146
+ unrecognized,
147
+ };
148
+ });
149
+ //# sourceMappingURL=install-root.js.map
@@ -1,11 +1,22 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  import { type ConfiguredAgentOutcome } from "../../configured-agent-outcome.js";
3
3
  import type { ExtensionKey } from "../types.js";
4
- export declare const ExtensionInventoryLifecycleSchema: Schema.Literals<readonly ["configured", "implicit", "unmanaged"]>;
4
+ /**
5
+ * How desired state explains one detected extension.
6
+ *
7
+ * - `configured` — declared directly in workspace settings.
8
+ * - `implicit` — reached indirectly, such as through a desired Pack.
9
+ * - `leftover` — an installed package in the install root that desired state
10
+ * no longer reaches.
11
+ * - `undeclared` — an authored package in its standard authoring folder with no
12
+ * workspace declaration.
13
+ * - `unmanaged` — native agent content AXM does not own.
14
+ */
15
+ export declare const ExtensionInventoryLifecycleSchema: Schema.Literals<readonly ["configured", "implicit", "leftover", "undeclared", "unmanaged"]>;
5
16
  export type ExtensionInventoryLifecycle = typeof ExtensionInventoryLifecycleSchema.Type;
6
17
  export declare const ExtensionInventoryClassificationSchema: Schema.Struct<{
7
18
  readonly kind: Schema.Literal<"lifecycle">;
8
- readonly lifecycle: Schema.Literals<readonly ["configured", "implicit", "unmanaged"]>;
19
+ readonly lifecycle: Schema.Literals<readonly ["configured", "implicit", "leftover", "undeclared", "unmanaged"]>;
9
20
  }>;
10
21
  export type ExtensionInventoryClassification = typeof ExtensionInventoryClassificationSchema.Type;
11
22
  export declare const ExtensionInventoryRowSchema: Schema.Struct<{
@@ -14,7 +25,7 @@ export declare const ExtensionInventoryRowSchema: Schema.Struct<{
14
25
  readonly name: Schema.String;
15
26
  readonly classification: Schema.Struct<{
16
27
  readonly kind: Schema.Literal<"lifecycle">;
17
- readonly lifecycle: Schema.Literals<readonly ["configured", "implicit", "unmanaged"]>;
28
+ readonly lifecycle: Schema.Literals<readonly ["configured", "implicit", "leftover", "undeclared", "unmanaged"]>;
18
29
  }>;
19
30
  readonly enabled: Schema.NullOr<Schema.Boolean>;
20
31
  readonly installed: Schema.Boolean;
@@ -47,7 +58,7 @@ export declare const ExtensionInventorySchema: Schema.Struct<{
47
58
  readonly name: Schema.String;
48
59
  readonly classification: Schema.Struct<{
49
60
  readonly kind: Schema.Literal<"lifecycle">;
50
- readonly lifecycle: Schema.Literals<readonly ["configured", "implicit", "unmanaged"]>;
61
+ readonly lifecycle: Schema.Literals<readonly ["configured", "implicit", "leftover", "undeclared", "unmanaged"]>;
51
62
  }>;
52
63
  readonly enabled: Schema.NullOr<Schema.Boolean>;
53
64
  readonly installed: Schema.Boolean;
@@ -76,6 +87,8 @@ export declare const ExtensionInventorySchema: Schema.Struct<{
76
87
  readonly configuredCount: Schema.Number;
77
88
  readonly implicitCount: Schema.Number;
78
89
  readonly installedCount: Schema.Number;
90
+ readonly leftoverCount: Schema.Number;
91
+ readonly undeclaredCount: Schema.Number;
79
92
  readonly unmanagedCount: Schema.Number;
80
93
  }>;
81
94
  export type ExtensionInventory = typeof ExtensionInventorySchema.Type;
@@ -95,5 +108,9 @@ export interface ProjectExtensionInventoryInput {
95
108
  readonly lifecycle: ReadonlyArray<LifecycleInventoryCandidate>;
96
109
  readonly agents?: ReadonlyArray<string>;
97
110
  }
111
+ /** Whether desired state reaches rows with this lifecycle. */
112
+ export declare const isDesiredInventoryLifecycle: (lifecycle: ExtensionInventoryLifecycle) => boolean;
113
+ /** Aggregate counts for an inventory's already-filtered items. */
114
+ export declare const countExtensionInventory: (items: ReadonlyArray<ExtensionInventoryRow>) => ExtensionInventory;
98
115
  export declare const projectExtensionInventory: (input: ProjectExtensionInventoryInput) => ExtensionInventory;
99
116
  //# sourceMappingURL=inventory.d.ts.map
@@ -1,9 +1,22 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  import { ExtensionTypeSchema } from "@agentxm/extension-model/unstable/extensions/common";
3
3
  import { ConfiguredAgentOutcomeSchema, } from "../../configured-agent-outcome.js";
4
+ /**
5
+ * How desired state explains one detected extension.
6
+ *
7
+ * - `configured` — declared directly in workspace settings.
8
+ * - `implicit` — reached indirectly, such as through a desired Pack.
9
+ * - `leftover` — an installed package in the install root that desired state
10
+ * no longer reaches.
11
+ * - `undeclared` — an authored package in its standard authoring folder with no
12
+ * workspace declaration.
13
+ * - `unmanaged` — native agent content AXM does not own.
14
+ */
4
15
  export const ExtensionInventoryLifecycleSchema = Schema.Literals([
5
16
  "configured",
6
17
  "implicit",
18
+ "leftover",
19
+ "undeclared",
7
20
  "unmanaged",
8
21
  ]);
9
22
  export const ExtensionInventoryClassificationSchema = Schema.Struct({
@@ -35,12 +48,32 @@ export const ExtensionInventorySchema = Schema.Struct({
35
48
  configuredCount: Schema.Number,
36
49
  implicitCount: Schema.Number,
37
50
  installedCount: Schema.Number,
51
+ leftoverCount: Schema.Number,
52
+ undeclaredCount: Schema.Number,
38
53
  unmanagedCount: Schema.Number,
39
54
  });
40
55
  const lifecyclePriority = {
41
56
  configured: 0,
42
57
  implicit: 1,
43
- unmanaged: 2,
58
+ leftover: 2,
59
+ undeclared: 3,
60
+ unmanaged: 4,
61
+ };
62
+ /** Whether desired state reaches rows with this lifecycle. */
63
+ export const isDesiredInventoryLifecycle = (lifecycle) => lifecycle === "configured" || lifecycle === "implicit";
64
+ /** Aggregate counts for an inventory's already-filtered items. */
65
+ export const countExtensionInventory = (items) => {
66
+ const withLifecycle = (lifecycle) => items.filter((item) => item.classification.lifecycle === lifecycle).length;
67
+ return {
68
+ items,
69
+ count: items.length,
70
+ configuredCount: withLifecycle("configured"),
71
+ implicitCount: withLifecycle("implicit"),
72
+ installedCount: items.filter((item) => item.installed).length,
73
+ leftoverCount: withLifecycle("leftover"),
74
+ undeclaredCount: withLifecycle("undeclared"),
75
+ unmanagedCount: withLifecycle("unmanaged"),
76
+ };
44
77
  };
45
78
  const keyString = (key) => `${key.scope}:${key.type}:${key.name}`;
46
79
  const addAll = (target, values) => {
@@ -95,17 +128,6 @@ export const projectExtensionInventory = (input) => {
95
128
  origins: sorted(aggregate.origins),
96
129
  paths: sorted(aggregate.paths),
97
130
  }));
98
- const items = lifecycleRows.sort((left, right) => left.name.localeCompare(right.name));
99
- const configuredCount = items.filter((item) => item.classification.lifecycle === "configured").length;
100
- const implicitCount = items.filter((item) => item.classification.lifecycle === "implicit").length;
101
- const unmanagedCount = items.filter((item) => item.classification.lifecycle === "unmanaged").length;
102
- return {
103
- items,
104
- count: items.length,
105
- configuredCount,
106
- implicitCount,
107
- installedCount: items.filter((item) => item.installed).length,
108
- unmanagedCount,
109
- };
131
+ return countExtensionInventory(lifecycleRows.sort((left, right) => left.name.localeCompare(right.name)));
110
132
  };
111
133
  //# sourceMappingURL=inventory.js.map
@@ -37,6 +37,7 @@ import { MANIFEST_FILENAME_BY_TYPE } from "@agentxm/extension-content";
37
37
  import { parseSkillMd } from "@agentxm/extension-content";
38
38
  import { DISCOVERY_SKIPPED_DIRECTORIES } from "@agentxm/extension-model/unstable/discovery-walk";
39
39
  import { makeAbsolutePath } from "@agentxm/extension-model/unstable/path-types";
40
+ import { isInstallRootStagingName } from "../../install-root.js";
40
41
  import { childEntries, fileExists, filterDirectories, splitAbsolutePathSegments, } from "./fs-helpers.js";
41
42
  const SCANNER_NAME = "canonical-extensions";
42
43
  /**
@@ -187,7 +188,7 @@ const scanAcquiredDirectory = (deps, dir) => Effect.gen(function* () {
187
188
  }
188
189
  }
189
190
  const childCandidates = entries
190
- .filter((entry) => !DISCOVERY_SKIPPED_DIRECTORIES.has(entry))
191
+ .filter((entry) => !DISCOVERY_SKIPPED_DIRECTORIES.has(entry) && !isInstallRootStagingName(entry))
191
192
  .map((entry) => deps.path.join(dir, entry));
192
193
  const childDirs = yield* filterDirectories(deps.fs, childCandidates);
193
194
  const nested = yield* Effect.forEach(childDirs, (childDir) => scanAcquiredDirectory(deps, childDir), { concurrency: "unbounded" });
@@ -8,6 +8,7 @@ import * as Path from "effect/Path";
8
8
  import { type Handle } from "@agentxm/extension-model/unstable/extensions/handle";
9
9
  import type { SourceHostConfig } from "../../settings/schema.js";
10
10
  import { type AbsolutePath } from "@agentxm/extension-model/unstable/path-types";
11
+ import { type WorkspaceLayout } from "../layout.js";
11
12
  import { AgentRootResolver } from "./agent-root-resolver.js";
12
13
  import { type ScopedAgentsApi } from "./agents/index.js";
13
14
  import { type Warning } from "./diagnostics.js";
@@ -55,6 +56,8 @@ export interface WorkspaceReadModel {
55
56
  readonly owner: ScopedOwnerApi;
56
57
  readonly diagnostics: Effect.Effect<ReadonlyArray<Warning>>;
57
58
  readonly canonicalExtensions: Effect.Effect<ReadonlyArray<CanonicalExtensionOccurrence>>;
59
+ /** The resolved storage layout, or none when the layout could not be resolved. */
60
+ readonly layout: Option.Option<WorkspaceLayout>;
58
61
  }
59
62
  /**
60
63
  * The read-model family that carries each catalog extension type, or `null`
@@ -343,6 +343,7 @@ const buildScope = Effect.fn("workspace.read-model.build-scope")(function* (deps
343
343
  owner,
344
344
  diagnostics: diagnostics.snapshot,
345
345
  canonicalExtensions: canonicalScanner,
346
+ layout: Result.isFailure(layoutResult) ? Option.none() : Option.some(layoutResult.success),
346
347
  };
347
348
  });
348
349
  // ---------------------------------------------------------------------------
@@ -10,7 +10,7 @@ import { installableExtensionTypes, } from "@agentxm/extension-model/unstable/ex
10
10
  import { isAxmManagedMcpEntry } from "@agentxm/agent-integration";
11
11
  import { createDefaultSettings } from "../settings/index.js";
12
12
  import { configuredAgentLifecycleOutcomes } from "./configured-agent-outcomes.js";
13
- import { projectExtensionInventory, } from "./read-model/extensions/inventory.js";
13
+ import { countExtensionInventory, isDesiredInventoryLifecycle, projectExtensionInventory, } from "./read-model/extensions/inventory.js";
14
14
  import { deriveSourceMetaFromLockType } from "./source-metadata.js";
15
15
  export const makeReadModelRecordReaders = (args) => {
16
16
  const packagingKindForSource = (type, source) => {
@@ -88,9 +88,38 @@ export const makeReadModelRecordReaders = (args) => {
88
88
  paths: observations.paths,
89
89
  };
90
90
  };
91
- const lifecycleCandidateFromUnmanaged = (row) => ({
91
+ const isWithin = (root, candidate) => {
92
+ const relative = args.path.relative(root, candidate);
93
+ return !relative.startsWith("..") && !args.path.isAbsolute(relative);
94
+ };
95
+ /**
96
+ * Classify one occurrence desired state does not explain: an AXM package
97
+ * under the install root is `leftover`, an AXM package under its type's
98
+ * standard authoring folder is `undeclared`, and anything else — native
99
+ * agent content — is `unmanaged`.
100
+ */
101
+ const unexplainedLifecycle = (layout, key, actual) => {
102
+ if (Option.isNone(layout) || typeof actual !== "object" || actual === null)
103
+ return "unmanaged";
104
+ const originTag = stringProperty("origin" in actual ? actual.origin : undefined, "_tag");
105
+ const packageLocation = stringProperty(actual, "packageRoot") ?? stringProperty(actual, "contentRoot");
106
+ if (originTag === null || packageLocation === null)
107
+ return "unmanaged";
108
+ const canonical = originTag.startsWith("canonical-axm-");
109
+ if (!canonical && !originTag.startsWith("external-axm-"))
110
+ return "unmanaged";
111
+ if (isWithin(layout.value.acquiredRoot, packageLocation))
112
+ return "leftover";
113
+ if (canonical &&
114
+ layout.value.scope === "project" &&
115
+ isWithin(layout.value.authoredRoot(key.type), packageLocation)) {
116
+ return "undeclared";
117
+ }
118
+ return "unmanaged";
119
+ };
120
+ const lifecycleCandidateFromUnexplained = (layout, row) => ({
92
121
  key: row.key,
93
- lifecycle: "unmanaged",
122
+ lifecycle: unexplainedLifecycle(layout, row.key, row.actual),
94
123
  enabled: null,
95
124
  installed: true,
96
125
  ...observationFromActual(row.actual),
@@ -273,7 +302,7 @@ export const makeReadModelRecordReaders = (args) => {
273
302
  paths: [],
274
303
  }));
275
304
  const implicitObserved = desiredUnmanaged.map((row) => ({
276
- ...lifecycleCandidateFromUnmanaged(row),
305
+ ...lifecycleCandidateFromUnexplained(input.layout, row),
277
306
  lifecycle: "implicit",
278
307
  enabled: true,
279
308
  }));
@@ -287,8 +316,8 @@ export const makeReadModelRecordReaders = (args) => {
287
316
  ...implicitMissing,
288
317
  ...input.unmanaged
289
318
  .filter((row) => !desiredPackMembers.has(row.key.name))
290
- .map(lifecycleCandidateFromUnmanaged),
291
- ...stalePackActuals.map(lifecycleCandidateFromUnmanaged),
319
+ .map((row) => lifecycleCandidateFromUnexplained(input.layout, row)),
320
+ ...stalePackActuals.map((row) => lifecycleCandidateFromUnexplained(input.layout, row)),
292
321
  ],
293
322
  agents: input.agents,
294
323
  });
@@ -304,9 +333,8 @@ export const makeReadModelRecordReaders = (args) => {
304
333
  const withOutcomes = inventory.items.map((row) => {
305
334
  return {
306
335
  ...row,
307
- agentOutcomes: row.classification.lifecycle === "unmanaged"
308
- ? []
309
- : configuredAgentLifecycleOutcomes({
336
+ agentOutcomes: isDesiredInventoryLifecycle(row.classification.lifecycle)
337
+ ? configuredAgentLifecycleOutcomes({
310
338
  type: row.type,
311
339
  name: row.name,
312
340
  agentIds: configuredAgents,
@@ -315,22 +343,14 @@ export const makeReadModelRecordReaders = (args) => {
315
343
  targetState: row.enabled === false ? "disabled" : "enabled",
316
344
  installed: row.installed,
317
345
  observedAgentIds: row.agents,
318
- }),
346
+ })
347
+ : [],
319
348
  };
320
349
  });
321
350
  const items = withOutcomes.filter((row) => agents.length === 0 ||
322
351
  agents.some((agentId) => row.agents.includes(agentId) ||
323
352
  row.agentOutcomes.some((outcome) => outcome.agentId === agentId)));
324
- return {
325
- items,
326
- count: items.length,
327
- configuredCount: items.filter((item) => item.classification.lifecycle === "configured").length,
328
- implicitCount: items.filter((item) => item.classification.lifecycle === "implicit")
329
- .length,
330
- installedCount: items.filter((item) => item.installed).length,
331
- unmanagedCount: items.filter((item) => item.classification.lifecycle === "unmanaged")
332
- .length,
333
- };
353
+ return countExtensionInventory(items);
334
354
  };
335
355
  switch (type) {
336
356
  case "skill": {
@@ -339,6 +359,7 @@ export const makeReadModelRecordReaders = (args) => {
339
359
  const unmanaged = yield* scoped.skills.unmanaged;
340
360
  return finalizeInventory(projectStandardInventory({
341
361
  scope: scoped.scope,
362
+ layout: scoped.layout,
342
363
  type,
343
364
  installed,
344
365
  resolved: Option.getOrElse(resolved, () => []),
@@ -354,6 +375,7 @@ export const makeReadModelRecordReaders = (args) => {
354
375
  const unmanaged = yield* scoped.mcpServers.unmanaged;
355
376
  return finalizeInventory(projectStandardInventory({
356
377
  scope: scoped.scope,
378
+ layout: scoped.layout,
357
379
  type,
358
380
  installed,
359
381
  resolved: Option.getOrElse(resolved, () => []),
@@ -369,6 +391,7 @@ export const makeReadModelRecordReaders = (args) => {
369
391
  const unmanaged = yield* scoped.subagents.unmanaged;
370
392
  return finalizeInventory(projectStandardInventory({
371
393
  scope: scoped.scope,
394
+ layout: scoped.layout,
372
395
  type,
373
396
  installed,
374
397
  resolved: Option.getOrElse(resolved, () => []),
@@ -384,6 +407,7 @@ export const makeReadModelRecordReaders = (args) => {
384
407
  const unmanaged = yield* scoped.packs.unmanaged;
385
408
  return finalizeInventory(projectStandardInventory({
386
409
  scope: scoped.scope,
410
+ layout: scoped.layout,
387
411
  type,
388
412
  installed,
389
413
  resolved: Option.getOrElse(resolved, () => []),
@@ -399,6 +423,7 @@ export const makeReadModelRecordReaders = (args) => {
399
423
  const unmanaged = yield* scoped.rules.unmanaged;
400
424
  return finalizeInventory(projectStandardInventory({
401
425
  scope: scoped.scope,
426
+ layout: scoped.layout,
402
427
  type,
403
428
  installed,
404
429
  resolved: Option.getOrElse(resolved, () => []),
@@ -414,6 +439,7 @@ export const makeReadModelRecordReaders = (args) => {
414
439
  const unmanaged = yield* scoped.hooks.unmanaged;
415
440
  return finalizeInventory(projectStandardInventory({
416
441
  scope: scoped.scope,
442
+ layout: scoped.layout,
417
443
  type,
418
444
  installed,
419
445
  resolved: Option.getOrElse(resolved, () => []),
@@ -429,6 +455,7 @@ export const makeReadModelRecordReaders = (args) => {
429
455
  const unmanaged = yield* scoped.knowledge.unmanaged;
430
456
  return finalizeInventory(projectStandardInventory({
431
457
  scope: scoped.scope,
458
+ layout: scoped.layout,
432
459
  type,
433
460
  installed,
434
461
  resolved: Option.getOrElse(resolved, () => []),
@@ -457,6 +484,8 @@ export const makeReadModelRecordReaders = (args) => {
457
484
  configuredCount: inventories.reduce((total, inventory) => total + inventory.configuredCount, 0),
458
485
  implicitCount: inventories.reduce((total, inventory) => total + inventory.implicitCount, 0),
459
486
  installedCount: inventories.reduce((total, inventory) => total + inventory.installedCount, 0),
487
+ leftoverCount: inventories.reduce((total, inventory) => total + inventory.leftoverCount, 0),
488
+ undeclaredCount: inventories.reduce((total, inventory) => total + inventory.undeclaredCount, 0),
460
489
  unmanagedCount: inventories.reduce((total, inventory) => total + inventory.unmanagedCount, 0),
461
490
  };
462
491
  });
@@ -106,7 +106,7 @@ export type WorkspaceStateReadFailure = SettingsReadError | LockfileReadError |
106
106
  */
107
107
  export type WorkspaceSettingsMutationFailure = WorkspaceSettingsReadFailure | SettingsWriteError | WorkspaceSnapshotError;
108
108
  /** Lockfile read-modify-write through the snapshot-commit path. */
109
- export type WorkspaceLockfileMutationFailure = WorkspaceLockfileReadFailure | LockfileValidationError | LockfileWriteError;
109
+ export type WorkspaceLockfileMutationFailure = WorkspaceLockfileReadFailure | LockfileValidationError | LockfileWriteError | WorkspaceSnapshotError;
110
110
  /** Coupled settings-and-lockfile mutation. */
111
111
  export type WorkspaceStateMutationFailure = WorkspaceSettingsMutationFailure | WorkspaceLockfileMutationFailure;
112
112
  export interface WorkspaceReadModelRecords {
@@ -61,6 +61,8 @@ export declare const readModelRecordStubs: {
61
61
  configuredCount: number;
62
62
  implicitCount: number;
63
63
  installedCount: number;
64
+ leftoverCount: number;
65
+ undeclaredCount: number;
64
66
  unmanagedCount: number;
65
67
  }, never, never>;
66
68
  readonly getExtensionInventory: () => Effect.Effect<ExtensionInventory, WorkspaceStateReadFailure>;
@@ -23,6 +23,8 @@ const emptyInventory = () => Effect.succeed({
23
23
  configuredCount: 0,
24
24
  implicitCount: 0,
25
25
  installedCount: 0,
26
+ leftoverCount: 0,
27
+ undeclaredCount: 0,
26
28
  unmanagedCount: 0,
27
29
  });
28
30
  const fs = (() => {
@@ -94,6 +96,8 @@ export const readModelRecordStubs = {
94
96
  configuredCount: 0,
95
97
  implicitCount: 0,
96
98
  installedCount: 0,
99
+ leftoverCount: 0,
100
+ undeclaredCount: 0,
97
101
  unmanagedCount: 0,
98
102
  }),
99
103
  getExtensionInventory: emptyInventory,
package/package.json CHANGED
@@ -4,11 +4,11 @@
4
4
  "url": "https://github.com/agentxm/axm/issues"
5
5
  },
6
6
  "dependencies": {
7
- "@agentxm/agent-integration": "^0.30.1",
8
- "@agentxm/extension-content": "^0.30.1",
9
- "@agentxm/extension-model": "^0.30.1",
10
- "@agentxm/registry-protocol": "^0.30.1",
11
- "@agentxm/workspace-transactions": "^0.30.1",
7
+ "@agentxm/agent-integration": "^0.30.2",
8
+ "@agentxm/extension-content": "^0.30.2",
9
+ "@agentxm/extension-model": "^0.30.2",
10
+ "@agentxm/registry-protocol": "^0.30.2",
11
+ "@agentxm/workspace-transactions": "^0.30.2",
12
12
  "effect": "4.0.0-rc.115",
13
13
  "jsonc-parser": "^3.3.1",
14
14
  "semver": "^7.8.5",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "description": "AXM workspace state kernel: settings, lockfile, read model, and desired-state vocabulary for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
19
19
  "devDependencies": {
20
- "@agentxm/specification-metadata": "^0.30.1",
20
+ "@agentxm/specification-metadata": "^0.30.2",
21
21
  "@effect/platform-node": "4.0.0-rc.115",
22
22
  "@effect/vitest": "4.0.0-rc.115",
23
23
  "@fast-check/vitest": "^0.5.0",
@@ -63,5 +63,5 @@
63
63
  },
64
64
  "sideEffects": false,
65
65
  "type": "module",
66
- "version": "0.30.1"
66
+ "version": "0.30.2"
67
67
  }