@agentxm/workspace-state 0.29.4 → 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";
@@ -67,16 +68,18 @@ export { WorkspaceLocation, type WorkspaceLocationService } from "./workspace/lo
67
68
  export { SettingsReader, type SettingsReaderService } from "./workspace/settings-reader.js";
68
69
  export { LockfileReader, type LockfileReaderService } from "./workspace/lockfile-reader.js";
69
70
  export { readOtherScopeState, type OtherScopeState } from "./workspace/other-scope-reader.js";
70
- export { DesiredStateReader, type DesiredStateReaderService, } from "./workspace/desired-state-reader.js";
71
+ export { DesiredStateReader, type DesiredStateReaderService, type DesiredStateGraphInputs, } from "./workspace/desired-state-reader.js";
71
72
  export { WorkspaceRecords, type WorkspaceRecordsService } from "./workspace/workspace-records.js";
72
73
  export { ExtensionPaths, type ExtensionPathsService } from "./workspace/extension-paths-service.js";
73
74
  export { SettingsWriter, type SettingsWriterService } from "./workspace/settings-writer.js";
74
75
  export { AcceptedResolutionWriter, type AcceptedResolutionWriterService, } from "./workspace/accepted-resolution-writer.js";
75
76
  export { DesiredStateWriter, type DeclareArgsByType, type DesiredStateWriterService, } from "./workspace/desired-state-writer.js";
77
+ export { settingsEntries, lockEntries } from "./workspace/entry-accessors.js";
76
78
  export type { LockEntriesOf, LockEntryByType, SettingsEntriesOf, SettingsEntryByType, } from "./workspace/entry-accessors.js";
77
79
  export { WorkspaceMutations, type WorkspaceMutationsService, type WorkspaceMutationsError, type WorkspaceMutationsOptions, type WorkspaceSettingsReadFailure, type WorkspaceLockfileReadFailure, type WorkspaceStateReadFailure, type WorkspaceSettingsMutationFailure, type WorkspaceLockfileMutationFailure, type WorkspaceStateMutationFailure, type SetSkillArgs, type SetPackArgs, type SetMcpServerArgs, type SetSubagentArgs, type SetRuleArgs, type SetHookArgs, type SetKnowledgeArgs, type PackDirPath, type ExtensionTarget, type ExtensionTargetFor, type LockfileState, type SkillExtensionTarget, type PackExtensionTarget, type McpServerExtensionTarget, type SubagentExtensionTarget, type RuleExtensionTarget, type HookExtensionTarget, type KnowledgeExtensionTarget, } from "./workspace/service-interface.js";
78
80
  export { LockfileDecodeError, LockfileIoError, LockfileParseError, LockfileVersionUnsupported, SettingsDecodeError, SettingsIoError, SettingsParseError, SkillDiscoveryRootInvalid, SubagentScanFailed, WorkspaceRootEscape, type LockfileReadError, type SettingsReadError, } from "./workspace/read-model/errors.js";
79
81
  export { AcceptedResolutionMissing, CanonicalPathRemovalError, DesiredPackGraphIncomplete, InlineExtensionSourceMissing, InvalidAgentId, LockedSkillMissing, LockEntryEndpointConflict, LockEntryNameInvalid, LockEntrySourceMissing, LockEntrySourceTypeConflict, LockEntryUrlInvalid, PackageContentHashFailed, SettingsEntryMissing, SupersededCanonicalRemovalFailed, SymlinkCreationError, WorkspaceLayoutError, WorkspaceNotInitialized, WorkspaceSourceInvalid, } from "./workspace/errors.js";
80
82
  export { setupScopeSupport, setupScopeSupportOutcomes, type SetupScopeSupportCategory, type SetupScopeSupportOutcome, type SetupScopeSupportReasonCode, type SetupScopeSupportStatus, } from "./workspace/setup-scope-support.js";
81
83
  export { configuredAgentLifecycleOutcomes, EXTENSION_CONFIGURED_AGENT_POLICY, type ConfiguredAgentLifecycleState, } from "./workspace/configured-agent-outcomes.js";
84
+ export { lockEntrySemanticallyEqual } from "./workspace/accepted-resolution-writer.js";
82
85
  //# sourceMappingURL=index.d.ts.map
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";
@@ -85,6 +86,7 @@ export { ExtensionPaths } from "./workspace/extension-paths-service.js";
85
86
  export { SettingsWriter } from "./workspace/settings-writer.js";
86
87
  export { AcceptedResolutionWriter, } from "./workspace/accepted-resolution-writer.js";
87
88
  export { DesiredStateWriter, } from "./workspace/desired-state-writer.js";
89
+ export { settingsEntries, lockEntries } from "./workspace/entry-accessors.js";
88
90
  // TRANSITIONAL workspace mutation facade. No CLI handler composes it any
89
91
  // more; the domain packages under packages/core still do, and it is removed
90
92
  // when the last of them reads and writes through the narrow services above.
@@ -95,4 +97,5 @@ export { LockfileDecodeError, LockfileIoError, LockfileParseError, LockfileVersi
95
97
  export { AcceptedResolutionMissing, CanonicalPathRemovalError, DesiredPackGraphIncomplete, InlineExtensionSourceMissing, InvalidAgentId, LockedSkillMissing, LockEntryEndpointConflict, LockEntryNameInvalid, LockEntrySourceMissing, LockEntrySourceTypeConflict, LockEntryUrlInvalid, PackageContentHashFailed, SettingsEntryMissing, SupersededCanonicalRemovalFailed, SymlinkCreationError, WorkspaceLayoutError, WorkspaceNotInitialized, WorkspaceSourceInvalid, } from "./workspace/errors.js";
96
98
  export { setupScopeSupport, setupScopeSupportOutcomes, } from "./workspace/setup-scope-support.js";
97
99
  export { configuredAgentLifecycleOutcomes, EXTENSION_CONFIGURED_AGENT_POLICY, } from "./workspace/configured-agent-outcomes.js";
100
+ export { lockEntrySemanticallyEqual } from "./workspace/accepted-resolution-writer.js";
98
101
  //# sourceMappingURL=index.js.map
@@ -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, {
@@ -14,6 +14,8 @@ interface AcceptedCanonicalRefArgs {
14
14
  readonly workspace: WorkspaceMutationsService;
15
15
  readonly type: DesiredExtensionNode["type"];
16
16
  readonly name: string;
17
+ /** Proposed authority when preparing a transition before publishing intent. */
18
+ readonly desired?: DesiredExtensionNode;
17
19
  }
18
20
  /** Every failure the accepted-canonical reconstruction functions can produce. */
19
21
  export type AcceptedCanonicalRefError = WorkspaceStateReadFailure | LockEntryToRefError | AcceptedResolutionMissing | InlineExtensionSourceMissing | WorkspaceSourceInvalid | PathTraversalDetected | PackageContentHashFailed;
@@ -50,7 +52,7 @@ export declare const acceptedLockedResolutionRef: (args: AcceptedCanonicalRefArg
50
52
  * even when its canonical materialization is absent or divergent.
51
53
  */
52
54
  export declare const acceptedResolutionRef: (args: AcceptedCanonicalRefArgs) => Effect.Effect<Option.Option<ExtensionRef>, AcceptedCanonicalRefError, FileSystem.FileSystem | Path.Path>;
53
- export declare const acceptedCanonicalObservation: ({ workspace, type, name, }: AcceptedCanonicalRefArgs) => Effect.Effect<Option.Option<AcceptedCanonicalObservation>, AcceptedCanonicalRefError, FileSystem.FileSystem | Path.Path>;
55
+ export declare const acceptedCanonicalObservation: ({ workspace, type, name, desired: proposed, }: AcceptedCanonicalRefArgs) => Effect.Effect<Option.Option<AcceptedCanonicalObservation>, AcceptedCanonicalRefError, FileSystem.FileSystem | Path.Path>;
54
56
  export declare const usableAcceptedCanonicalObservation: (args: AcceptedCanonicalRefArgs) => Effect.Effect<Option.Option<UsableAcceptedCanonicalObservation>, AcceptedCanonicalRefError, FileSystem.FileSystem | Path.Path>;
55
57
  export declare const usableAcceptedCanonical: (args: AcceptedCanonicalRefArgs) => Effect.Effect<Option.Option<UsableAcceptedCanonical>, AcceptedCanonicalRefError, FileSystem.FileSystem | Path.Path>;
56
58
  export declare const usableAcceptedCanonicalRef: (args: AcceptedCanonicalRefArgs) => Effect.Effect<Option.Option<ExtensionRef>, AcceptedCanonicalRefError, FileSystem.FileSystem | Path.Path>;
@@ -188,16 +188,16 @@ const refForDesired = (workspace, desired) => Effect.gen(function* () {
188
188
  * even when its canonical materialization is absent or divergent.
189
189
  */
190
190
  export const acceptedResolutionRef = (args) => Effect.gen(function* () {
191
- const graph = yield* args.workspace.getDesiredStateGraph();
192
- const desired = graph.nodes.find((node) => node.type === args.type && node.name === args.name);
191
+ const desired = args.desired ??
192
+ (yield* args.workspace.getDesiredStateGraph()).nodes.find((node) => node.type === args.type && node.name === args.name);
193
193
  if (desired === undefined || desired.identity.startsWith("workspace:")) {
194
194
  return Option.none();
195
195
  }
196
196
  return yield* acceptedLockedResolutionRef(args);
197
197
  });
198
- export const acceptedCanonicalObservation = ({ workspace, type, name, }) => Effect.gen(function* () {
199
- const graph = yield* workspace.getDesiredStateGraph();
200
- const desired = graph.nodes.find((node) => node.type === type && node.name === name);
198
+ export const acceptedCanonicalObservation = ({ workspace, type, name, desired: proposed, }) => Effect.gen(function* () {
199
+ const desired = proposed ??
200
+ (yield* workspace.getDesiredStateGraph()).nodes.find((node) => node.type === type && node.name === name);
201
201
  if (desired === undefined)
202
202
  return Option.none();
203
203
  const accepted = yield* getAcceptedResolution(workspace, type, name);
@@ -223,7 +223,10 @@ export const usableAcceptedCanonical = (args) => Effect.gen(function* () {
223
223
  if (Option.isNone(canonical))
224
224
  return Option.none();
225
225
  const ref = yield* refForDesired(args.workspace, canonical.value.desired);
226
- return Option.some({ ...canonical.value, ref });
226
+ // A usable local acquisition is the accepted copy, even if the original
227
+ // directory has changed or disappeared. Keep sourcePath as its identity.
228
+ const retainedRef = ref.refType === "local" ? { ...ref, location: canonical.value.observation.path } : ref;
229
+ return Option.some({ ...canonical.value, ref: retainedRef });
227
230
  });
228
231
  export const usableAcceptedCanonicalRef = (args) => usableAcceptedCanonical(args).pipe(Effect.map(Option.map((canonical) => canonical.ref)));
229
232
  //# sourceMappingURL=accepted-canonical-ref.js.map
@@ -37,8 +37,6 @@ export const stableCompare = (left, right) => JSON.stringify(normalizeForStableC
37
37
  export const lockEntrySemanticallyEqual = (current, next) => current !== undefined && stableCompare(current, next);
38
38
  /** Keep the recorded object when the replacement carries the same facts. */
39
39
  export const preserveAcceptedResolutionOnNoop = (current, next) => (lockEntrySemanticallyEqual(current, next) && current !== undefined ? current : next);
40
- /** Types whose semantically unchanged resolution skips the commit entirely. */
41
- const skipsUnchangedCommit = (type) => type === "skill" || type === "subagent";
42
40
  export const makeAcceptedResolutionWriter = (location, mutex) => {
43
41
  const current = readLockfileCell(location, location.runtimeDir);
44
42
  const commit = (base, next) => commitLockfileSnapshotUpdateAtPath(location.lockPath, base, next);
@@ -48,7 +46,7 @@ export const makeAcceptedResolutionWriter = (location, mutex) => {
48
46
  const lockfile = yield* current;
49
47
  const accessor = lockEntries[type];
50
48
  const previous = accessor.entries(lockfile)[key];
51
- if (skipsUnchangedCommit(type) && lockEntrySemanticallyEqual(previous, entry))
49
+ if (lockEntrySemanticallyEqual(previous, entry))
52
50
  return;
53
51
  yield* commit(lockfile, accessor.set(lockfile, key, preserveAcceptedResolutionOnNoop(previous, entry)));
54
52
  })).pipe(Effect.withSpan("AcceptedResolutionWriter.setAccepted")),
@@ -2,17 +2,18 @@ import * as Effect from "effect/Effect";
2
2
  import * as FileSystem from "effect/FileSystem";
3
3
  import * as Path from "effect/Path";
4
4
  import type { Lockfile } from "../lockfile/schema.js";
5
- import { type DesiredStateGraph } from "./desired-state-graph.js";
5
+ import { type DesiredStateGraph, type ProspectivePackRef } from "./desired-state-graph.js";
6
6
  import type { WorkspaceLayout } from "./layout.js";
7
7
  interface ValidateDesiredPackLockArgs {
8
8
  readonly graph: DesiredStateGraph;
9
9
  readonly lockfile: Lockfile;
10
10
  readonly layout: WorkspaceLayout;
11
+ readonly prospectivePacks?: ReadonlyArray<ProspectivePackRef>;
11
12
  }
12
13
  /**
13
14
  * Authorize enabled external Pack manifests against their accepted lock row.
14
15
  * Workspace-authored Pack manifests are desired authority and need no lock row.
15
16
  */
16
- export declare const validateDesiredPackLock: ({ graph, lockfile, layout, }: ValidateDesiredPackLockArgs) => Effect.Effect<DesiredStateGraph, never, FileSystem.FileSystem | Path.Path>;
17
+ export declare const validateDesiredPackLock: ({ graph, lockfile, layout, prospectivePacks, }: ValidateDesiredPackLockArgs) => Effect.Effect<DesiredStateGraph, never, FileSystem.FileSystem | Path.Path>;
17
18
  export {};
18
19
  //# sourceMappingURL=desired-pack-lock.d.ts.map
@@ -52,7 +52,7 @@ const decodeManifest = Schema.decodeUnknownSync(PackManifestSchema, {
52
52
  * Authorize enabled external Pack manifests against their accepted lock row.
53
53
  * Workspace-authored Pack manifests are desired authority and need no lock row.
54
54
  */
55
- export const validateDesiredPackLock = ({ graph, lockfile, layout, }) => Effect.gen(function* () {
55
+ export const validateDesiredPackLock = ({ graph, lockfile, layout, prospectivePacks = [], }) => Effect.gen(function* () {
56
56
  const fs = yield* FileSystem.FileSystem;
57
57
  const path = yield* Path.Path;
58
58
  const problems = [];
@@ -82,24 +82,25 @@ export const validateDesiredPackLock = ({ graph, lockfile, layout, }) => Effect.
82
82
  continue;
83
83
  }
84
84
  const manifestPath = path.join(computePackPathsForLayout(path.join, layout, entry.sourceName, identity.owner, identity.name).canonicalPath, PACK_MANIFEST_FILENAME);
85
- const readResult = yield* Effect.result(fs.readFileString(manifestPath));
86
- if (Result.isFailure(readResult)) {
87
- problems.push({
88
- type: "pack-manifest-content-mismatch",
89
- pack: node.identity,
90
- path: manifestPath,
91
- status: "missing",
92
- acceptedVersion: entry.resolvedVersion,
93
- acceptedContentIdentity: entry.manifestContentIdentity,
94
- });
95
- invalidPacks.add(normalizedPackIdentity(node.identity));
96
- continue;
97
- }
98
- const decoded = Result.try({
99
- try: () => decodeManifest(JSON.parse(readResult.success)),
100
- catch: () => undefined,
101
- });
102
- const observedManifest = Result.isSuccess(decoded) && decoded.success !== undefined ? decoded.success : undefined;
85
+ const prospective = prospectivePacks.find((ref) => ref.owner === identity.owner && ref.pack.name === identity.name);
86
+ const observedManifest = prospective === undefined
87
+ ? yield* Effect.gen(function* () {
88
+ const readResult = yield* Effect.result(fs.readFileString(manifestPath));
89
+ if (Result.isFailure(readResult))
90
+ return undefined;
91
+ const decoded = Result.try({
92
+ try: () => decodeManifest(JSON.parse(readResult.success)),
93
+ catch: () => undefined,
94
+ });
95
+ return Result.isSuccess(decoded) ? decoded.success : undefined;
96
+ })
97
+ : {
98
+ owner: prospective.owner,
99
+ type: "pack",
100
+ name: prospective.pack.name,
101
+ version: prospective.version,
102
+ dependencies: prospective.pack.dependencies,
103
+ };
103
104
  const observedContentIdentity = observedManifest === undefined
104
105
  ? undefined
105
106
  : computePackManifestContentIdentity(observedManifest);
@@ -109,7 +110,7 @@ export const validateDesiredPackLock = ({ graph, lockfile, layout, }) => Effect.
109
110
  type: "pack-manifest-content-mismatch",
110
111
  pack: node.identity,
111
112
  path: manifestPath,
112
- status: "changed",
113
+ status: observedManifest === undefined ? "missing" : "changed",
113
114
  acceptedVersion: entry.resolvedVersion,
114
115
  acceptedContentIdentity: entry.manifestContentIdentity,
115
116
  ...(observedManifest === undefined || observedContentIdentity === undefined
@@ -242,6 +242,8 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
242
242
  });
243
243
  const workspacePack = isWorkspaceSourceLocator(entry.source);
244
244
  const configuredRegistrySource = registryLocator(entry.source)?.sourceName ?? "agentxm";
245
+ if (entry.enabled === false)
246
+ continue;
245
247
  const manifestPath = path.join(layout === undefined
246
248
  ? path.join(baseDir, workspacePack
247
249
  ? configuredAuthoredDirectory(settings, "pack")
@@ -308,7 +310,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
308
310
  identity: dependencyIdentity,
309
311
  authority: "sourced",
310
312
  source: `${fqn}@${constraint}`,
311
- enabled: entry.enabled !== false,
313
+ enabled: true,
312
314
  constraint,
313
315
  origin: {
314
316
  type: "pack",
@@ -316,7 +318,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
316
318
  manifestPath: path.relative(baseDir, manifestPath),
317
319
  source: fqn,
318
320
  constraint,
319
- enabled: entry.enabled !== false,
321
+ enabled: true,
320
322
  },
321
323
  });
322
324
  }
@@ -11,15 +11,21 @@ import type * as FileSystem from "effect/FileSystem";
11
11
  import * as Layer from "effect/Layer";
12
12
  import type * as Path from "effect/Path";
13
13
  import { type DesiredStateGraph, type ProspectivePackRef } from "./desired-state-graph.js";
14
+ import type { Settings } from "../settings/index.js";
15
+ import type { Lockfile } from "../lockfile/index.js";
14
16
  import { DesiredPackGraphIncomplete } from "./errors.js";
15
17
  import { WorkspaceLocation, type WorkspaceLocationService } from "./location.js";
16
18
  import type { ExtensionTarget, WorkspaceStateReadFailure } from "./service-interface.js";
17
19
  import { SettingsReader, type SettingsReaderService } from "./settings-reader.js";
20
+ /** Authoritative inputs for evaluating a candidate without publishing it. */
21
+ export interface DesiredStateGraphInputs {
22
+ readonly settings?: Settings;
23
+ readonly acceptedResolutions?: Lockfile;
24
+ readonly prospectivePacks?: ReadonlyArray<ProspectivePackRef>;
25
+ }
18
26
  export interface DesiredStateReaderService {
19
27
  /** Build desired extension state from settings and installed or prospective Pack manifests. */
20
- readonly graph: (options?: {
21
- readonly prospectivePacks?: ReadonlyArray<ProspectivePackRef>;
22
- }) => Effect.Effect<DesiredStateGraph, WorkspaceStateReadFailure, FileSystem.FileSystem | Path.Path>;
28
+ readonly graph: (options?: DesiredStateGraphInputs) => Effect.Effect<DesiredStateGraph, WorkspaceStateReadFailure, FileSystem.FileSystem | Path.Path>;
23
29
  /** Whether an installed Pack's dependency maps reference the target. */
24
30
  readonly isRequiredByInstalledPack: (target: ExtensionTarget) => Effect.Effect<boolean, WorkspaceStateReadFailure | DesiredPackGraphIncomplete, FileSystem.FileSystem | Path.Path>;
25
31
  }
@@ -19,7 +19,7 @@ export class DesiredStateReader extends ServiceMap.Service()("@agentxm/workspace
19
19
  }
20
20
  export const makeDesiredStateReader = (location, settings) => {
21
21
  const graph = (options) => Effect.gen(function* () {
22
- const current = yield* settings.settings;
22
+ const current = options?.settings ?? (yield* settings.settings);
23
23
  const configuredSources = yield* settings.configuredSources;
24
24
  const layout = yield* Ref.get(location.layout);
25
25
  const registryAuthorities = Object.fromEntries(configuredSources.flatMap((source) => source.type === "registry" ? [[source.name, source.location]] : []));
@@ -32,8 +32,15 @@ export const makeDesiredStateReader = (location, settings) => {
32
32
  ? {}
33
33
  : { prospectivePacks: options.prospectivePacks }),
34
34
  });
35
- const lockfile = yield* readLockfileCell(location, location.runtimeDir);
36
- return yield* validateDesiredPackLock({ graph: built, lockfile, layout });
35
+ const lockfile = options?.acceptedResolutions ?? (yield* readLockfileCell(location, location.runtimeDir));
36
+ return yield* validateDesiredPackLock({
37
+ graph: built,
38
+ lockfile,
39
+ layout,
40
+ ...(options?.prospectivePacks === undefined
41
+ ? {}
42
+ : { prospectivePacks: options.prospectivePacks }),
43
+ });
37
44
  }).pipe(Effect.withSpan("DesiredStateReader.graph"));
38
45
  return {
39
46
  graph,
@@ -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
  });
@@ -28,7 +28,8 @@ import type { ReadModelRecordRow } from "./read-model-record-types.js";
28
28
  import type { WorkspaceScope } from "@agentxm/extension-model/unstable/workspace-scope";
29
29
  import type { ExtensionInventory } from "./read-model/extensions/inventory.js";
30
30
  import type { ResolvedKnowledgeDiscoveryConfig } from "../knowledge/discovery-config.js";
31
- import type { DesiredStateGraph, ProspectivePackRef } from "./desired-state-graph.js";
31
+ import type { DesiredStateGraphInputs } from "./desired-state-reader.js";
32
+ import type { DesiredStateGraph } from "./desired-state-graph.js";
32
33
  import type { AbsolutePath } from "@agentxm/extension-model/unstable/path-types";
33
34
  import type { WorkspaceLayout } from "./layout.js";
34
35
  import type { ExtensionPathSource } from "./extension-paths.js";
@@ -105,7 +106,7 @@ export type WorkspaceStateReadFailure = SettingsReadError | LockfileReadError |
105
106
  */
106
107
  export type WorkspaceSettingsMutationFailure = WorkspaceSettingsReadFailure | SettingsWriteError | WorkspaceSnapshotError;
107
108
  /** Lockfile read-modify-write through the snapshot-commit path. */
108
- export type WorkspaceLockfileMutationFailure = WorkspaceLockfileReadFailure | LockfileValidationError | LockfileWriteError;
109
+ export type WorkspaceLockfileMutationFailure = WorkspaceLockfileReadFailure | LockfileValidationError | LockfileWriteError | WorkspaceSnapshotError;
109
110
  /** Coupled settings-and-lockfile mutation. */
110
111
  export type WorkspaceStateMutationFailure = WorkspaceSettingsMutationFailure | WorkspaceLockfileMutationFailure;
111
112
  export interface WorkspaceReadModelRecords {
@@ -211,9 +212,7 @@ export interface WorkspaceMutationsService {
211
212
  /** Probe lockfile state for policy decisions: ok | missing | invalid. */
212
213
  readonly getLockfileState: () => Effect.Effect<LockfileState, LockfileValidationError | WorkspaceRootEscape>;
213
214
  /** Build desired extension state from settings and installed or prospective Pack manifests. */
214
- readonly getDesiredStateGraph: (options?: {
215
- readonly prospectivePacks?: ReadonlyArray<ProspectivePackRef>;
216
- }) => Effect.Effect<DesiredStateGraph, WorkspaceStateReadFailure>;
215
+ readonly getDesiredStateGraph: (options?: DesiredStateGraphInputs) => Effect.Effect<DesiredStateGraph, WorkspaceStateReadFailure>;
217
216
  /** Merged sources from project, user-scope, and built-in defaults. Cached per workspace lifetime. */
218
217
  readonly getConfiguredSources: () => Effect.Effect<ReadonlyArray<SourceHostConfig>, WorkspaceSettingsReadFailure>;
219
218
  /** Lookup a source by name from the merged sources list. */
@@ -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,20 +4,20 @@
4
4
  "url": "https://github.com/agentxm/axm/issues"
5
5
  },
6
6
  "dependencies": {
7
- "@agentxm/agent-integration": "^0.29.4",
8
- "@agentxm/extension-content": "^0.29.4",
9
- "@agentxm/extension-model": "^0.29.4",
10
- "@agentxm/registry-protocol": "^0.29.4",
11
- "@agentxm/workspace-transactions": "^0.29.4",
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",
15
- "smol-toml": "1.6.1",
15
+ "smol-toml": "^1.7.1",
16
16
  "yaml": "^2.9.0"
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.29.4",
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",
@@ -31,19 +31,16 @@
31
31
  },
32
32
  "exports": {
33
33
  ".": {
34
- "axm-source": "./src/index.ts",
35
- "default": "./dist/src/index.js",
36
- "types": "./dist/src/index.d.ts"
34
+ "types": "./dist/src/index.d.ts",
35
+ "default": "./dist/src/index.js"
37
36
  },
38
37
  "./live": {
39
- "axm-source": "./src/live.ts",
40
- "default": "./dist/src/live.js",
41
- "types": "./dist/src/live.d.ts"
38
+ "types": "./dist/src/live.d.ts",
39
+ "default": "./dist/src/live.js"
42
40
  },
43
41
  "./testing": {
44
- "axm-source": "./src/testing.ts",
45
- "default": "./dist/src/testing.js",
46
- "types": "./dist/src/testing.d.ts"
42
+ "types": "./dist/src/testing.d.ts",
43
+ "default": "./dist/src/testing.js"
47
44
  }
48
45
  },
49
46
  "files": [
@@ -66,5 +63,5 @@
66
63
  },
67
64
  "sideEffects": false,
68
65
  "type": "module",
69
- "version": "0.29.4"
66
+ "version": "0.30.2"
70
67
  }