@astrale-os/cli 1.0.0-beta.30 → 1.0.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,7 +105,11 @@ astrale update
105
105
  from `astrale-os/cli` is installed and healthy, and follows the beta channel by default. It reports
106
106
  whether the skill pass was unchanged, installed, updated, or repaired. Use `--check`,
107
107
  `--channel <channel>`, or `--version <version>` to control the CLI release target; skills always
108
- track the repository's current `main` source. `--no-skills` is the explicit opt-out.
108
+ resolve the repository's current `main` once, verify the installed trees against that commit, and
109
+ stamp the exact revision/tree binding into the Astrale-owned receipt. A current
110
+ `astrale update --check --json` result includes the resolved source revision plus every shipped
111
+ skill's Git tree and entrypoint, so runners can retain the actual loadout rather than only
112
+ `status: current`. `--no-skills` is the explicit opt-out.
109
113
 
110
114
  ## Commands
111
115
 
package/dist/astrale.js CHANGED
@@ -2578,7 +2578,7 @@ var package_default;
2578
2578
  var init_package = __esm(() => {
2579
2579
  package_default = {
2580
2580
  name: "@astrale-os/cli",
2581
- version: "1.0.0-beta.30",
2581
+ version: "1.0.0-beta.31",
2582
2582
  description: "Astrale CLI — connect to existing Astrale kernels",
2583
2583
  keywords: [
2584
2584
  "astrale",
@@ -61700,7 +61700,7 @@ async function inspectAstraleSkills(snapshot2, home, lockPath) {
61700
61700
  }
61701
61701
  }));
61702
61702
  if (managed.length === 0 && expectedPresence.every((present2) => !present2)) {
61703
- return { state: "absent", managedNames: [], managedFolders: [] };
61703
+ return { state: "absent", sourceCurrent: false, managedNames: [], managedFolders: [] };
61704
61704
  }
61705
61705
  const folders = managed.flatMap(([name, entry]) => {
61706
61706
  const folder = folderName(entry);
@@ -61742,12 +61742,17 @@ async function inspectAstraleSkills(snapshot2, home, lockPath) {
61742
61742
  }
61743
61743
  }
61744
61744
  }
61745
- const exactCurrent = coherent && managed.length === snapshot2.skills.length && folders.every(({ key, folder, entry }) => {
61745
+ const sourceCurrent = coherent && managed.length === snapshot2.skills.length && folders.every(({ key, folder, entry }) => {
61746
61746
  const skill = expected.get(folder);
61747
61747
  return key === folder && skill !== undefined && entry.ref === snapshot2.ref && actualHashes.get(folder) === skill.tree;
61748
61748
  });
61749
+ const exactCurrent = sourceCurrent && folders.every(({ folder, entry }) => {
61750
+ const skill = expected.get(folder);
61751
+ return skill !== undefined && entry.astraleSourceRevision === snapshot2.revision && entry.astraleSourceTree === skill.tree;
61752
+ });
61749
61753
  return {
61750
61754
  state: exactCurrent ? "current" : coherent ? "outdated" : "unhealthy",
61755
+ sourceCurrent,
61751
61756
  managedNames: managed.map(([name]) => name),
61752
61757
  managedFolders: [...uniqueFolders]
61753
61758
  };
@@ -61768,6 +61773,20 @@ function installerArgs(...args) {
61768
61773
  async function installSnapshot(snapshot2, execute, selectedAgents) {
61769
61774
  return await execute("npx", installerArgs("add", `${ASTRALE_CLI_SKILL_SOURCE}#${snapshot2.ref}`, "-g", "-y", "--skill", ...snapshot2.skills.map((skill) => skill.name), ...selectedAgents.length > 0 ? ["--agent", ...selectedAgents] : []));
61770
61775
  }
61776
+ async function stampAstraleSource(snapshot2, lockPath) {
61777
+ const { lock } = await readSkillLock(lockPath);
61778
+ if (!lock)
61779
+ throw new Error("skill installer receipt is unavailable after installation");
61780
+ for (const skill of snapshot2.skills) {
61781
+ const entry = lock.skills[skill.name];
61782
+ if (!entry || !sourceOwned(entry) || entry.ref !== snapshot2.ref || entry.skillPath !== skill.path) {
61783
+ throw new Error(`skill installer receipt is incomplete for ${skill.name}`);
61784
+ }
61785
+ entry.astraleSourceRevision = snapshot2.revision;
61786
+ entry.astraleSourceTree = skill.tree;
61787
+ }
61788
+ await writeSkillLock(lockPath, lock);
61789
+ }
61771
61790
  async function selectedAgents(home, lockPath) {
61772
61791
  const { lock } = await readSkillLock(lockPath);
61773
61792
  const agents = new Set((lock?.lastSelectedAgents ?? []).filter((agent) => typeof agent === "string" && SAFE_NAME.test(agent)));
@@ -61989,8 +62008,20 @@ async function checkAstraleSkills(overrides = {}) {
61989
62008
  try {
61990
62009
  const snapshot2 = await sourceSnapshot(dependencies);
61991
62010
  const inspection = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
62011
+ const status = inspection.state === "current" ? "current" : inspection.state === "unhealthy" ? "repair-needed" : "update-available";
61992
62012
  return {
61993
- status: inspection.state === "current" ? "current" : inspection.state === "unhealthy" ? "repair-needed" : "update-available"
62013
+ status,
62014
+ ...status === "current" ? {
62015
+ source: {
62016
+ repository: ASTRALE_CLI_SKILL_SOURCE,
62017
+ revision: snapshot2.revision,
62018
+ skills: snapshot2.skills.map(({ name, tree, path: path4 }) => ({
62019
+ name,
62020
+ tree,
62021
+ entrypoint: path4
62022
+ }))
62023
+ }
62024
+ } : {}
61994
62025
  };
61995
62026
  } catch (error52) {
61996
62027
  return { status: "unavailable", error: error52 instanceof Error ? error52.message : String(error52) };
@@ -62040,6 +62071,11 @@ async function syncAstraleSkills(overrides = {}) {
62040
62071
  throw new Error(installed.stderr || installed.stdout || "skill installer failed");
62041
62072
  }
62042
62073
  await pruneObsoleteEntries(snapshot2, dependencies.home, dependencies.lockPath, dependencies.run, knownRetired);
62074
+ const installedInspection = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
62075
+ if (!installedInspection.sourceCurrent) {
62076
+ throw new Error("installed Astrale skills do not match the resolved source");
62077
+ }
62078
+ await stampAstraleSource(snapshot2, dependencies.lockPath);
62043
62079
  const verified = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
62044
62080
  if (verified.state !== "current") {
62045
62081
  throw new Error("installed Astrale skills did not pass verification");
@@ -82689,7 +82725,7 @@ Behavior:
82689
82725
  script installs only — if Astrale was installed by another package manager this
82690
82726
  command refuses so that manager stays in charge; downloads are checksum-verified
82691
82727
  before the binary is replaced. (2) The Astrale agent skills: installs every
82692
- top-level skill published from astrale-os/cli main, updates healthy older
82728
+ top-level skill published from one resolved astrale-os/cli main commit, updates healthy older
82693
82729
  installs, repairs inconsistent installs, and verifies the result before
82694
82730
  reporting success. (3) SDK deps: inside a pnpm domain
82695
82731
  project, proposes any @astrale-os/* dependency with a newer release and, on
@@ -83288,20 +83324,20 @@ var init_query5 = __esm(() => {
83288
83324
  function prepareQuery(input) {
83289
83325
  const limit = positiveInteger2(input.limit, "--limit", DEFAULT_LIMIT);
83290
83326
  if (input.ast !== undefined) {
83291
- if (input.sources.length > 0 || input.definition !== undefined || input.edge !== undefined || input.direction !== undefined) {
83292
- throw new TypeError("--ast/--file cannot be combined with sources, --definition, --edge, or --direction");
83327
+ if (input.sources.length > 0 || input.class !== undefined || input.edge !== undefined || input.direction !== undefined) {
83328
+ throw new TypeError("--ast/--file cannot be combined with sources, --class, --edge, or --direction");
83293
83329
  }
83294
83330
  return withPage(QueryAST.decode(input.ast), limit, input.cursor);
83295
83331
  }
83296
- if (input.sources.length === 0 && input.definition === undefined) {
83297
- throw new TypeError("query requires a Path source, --definition, or a canonical Query V6 document");
83332
+ if (input.sources.length === 0 && input.class === undefined) {
83333
+ throw new TypeError("query requires a Path source, --class, or a canonical Query V6 document");
83298
83334
  }
83299
83335
  if (input.direction !== undefined && input.edge === undefined) {
83300
83336
  throw new TypeError("--direction requires --edge");
83301
83337
  }
83302
83338
  const paths3 = input.sources.map((source2) => Path.parse(source2));
83303
- const definition2 = input.definition === undefined ? undefined : classReference(input.definition, "--definition");
83304
- const nodes = definition2 === undefined ? paths3 : [...paths3, definition2];
83339
+ const selectedClass = input.class === undefined ? undefined : classReference(input.class, "--class");
83340
+ const nodes = selectedClass === undefined ? paths3 : [...paths3, selectedClass];
83305
83341
  const query4 = Query.from({
83306
83342
  nodes
83307
83343
  });
@@ -83477,7 +83513,7 @@ async function queryCommand(sources2, opts) {
83477
83513
  input = {
83478
83514
  sources: sources2,
83479
83515
  ...ast === undefined ? {} : { ast },
83480
- definition: opts.definition,
83516
+ class: opts.class,
83481
83517
  edge: opts.edge,
83482
83518
  direction: opts.direction,
83483
83519
  limit: opts.limit,
@@ -83547,7 +83583,7 @@ var init_query7 = __esm(() => {
83547
83583
  description: "Run one canonical Query V6 graph read",
83548
83584
  afterHelpText: `
83549
83585
  Behavior:
83550
- Positional Paths and --definition author a finite Query V6 read. --edge adds
83586
+ Positional Paths and --class author a finite Query V6 read. --edge adds
83551
83587
  one exact Edge-Class expansion; --direction defaults to outgoing. --ast and
83552
83588
  --file accept a complete canonical astrale.graph.query/v6 document, including
83553
83589
  Property ordering and Node or Edge reference/value projections. --cursor resumes
@@ -83559,7 +83595,7 @@ Behavior:
83559
83595
 
83560
83596
  Examples:
83561
83597
  $ astrale query /:notes.example.dev:class.Note --limit 50
83562
- $ astrale query --definition /:notes.example.dev:class.Note --limit 50
83598
+ $ astrale query --class /:notes.example.dev:class.Note --limit 50
83563
83599
  $ astrale query @note --edge /:notes.example.dev:class.references --direction outgoing --limit 25
83564
83600
  $ astrale query --file query.v6.json --cursor "$CURSOR"
83565
83601
  `,
@@ -83568,7 +83604,7 @@ Examples:
83568
83604
  { flags: "--ast <json>", description: "Canonical Query V6 JSON document" },
83569
83605
  { flags: "-f, --file <path>", description: "Read a canonical Query V6 document from a file" },
83570
83606
  {
83571
- flags: "--definition <path>",
83607
+ flags: "--class <path>",
83572
83608
  description: "Select Nodes satisfying one exact Class"
83573
83609
  },
83574
83610
  { flags: "--edge <class>", description: "Expand one exact Edge Class" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.30",
3
+ "version": "1.0.0-beta.31",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -195,7 +195,7 @@ describe('query command', () => {
195
195
 
196
196
  await queryCommand([], {
197
197
  json: true,
198
- definition: '/:issues.astrale.ai:class.Issue',
198
+ class: '/:issues.astrale.ai:class.Issue',
199
199
  limit: '20',
200
200
  })
201
201
 
@@ -239,7 +239,7 @@ describe('query command', () => {
239
239
 
240
240
  await queryCommand([], {
241
241
  json: true,
242
- definition: '/:issues.astrale.ai:class.Issue',
242
+ class: '/:issues.astrale.ai:class.Issue',
243
243
  limit: '1',
244
244
  })
245
245
 
@@ -14,7 +14,7 @@ import { isMachine, output } from '../lib/output'
14
14
  type QueryOpts = KernelCommandOpts & {
15
15
  ast?: string
16
16
  file?: string
17
- definition?: string
17
+ class?: string
18
18
  edge?: string
19
19
  direction?: QueryDirection
20
20
  limit?: string
@@ -28,7 +28,7 @@ export async function queryCommand(sources: string[], opts: QueryOpts): Promise<
28
28
  input = {
29
29
  sources,
30
30
  ...(ast === undefined ? {} : { ast }),
31
- definition: opts.definition,
31
+ class: opts.class,
32
32
  edge: opts.edge,
33
33
  direction: opts.direction,
34
34
  limit: opts.limit,
@@ -103,7 +103,7 @@ export default {
103
103
  description: 'Run one canonical Query V6 graph read',
104
104
  afterHelpText: `
105
105
  Behavior:
106
- Positional Paths and --definition author a finite Query V6 read. --edge adds
106
+ Positional Paths and --class author a finite Query V6 read. --edge adds
107
107
  one exact Edge-Class expansion; --direction defaults to outgoing. --ast and
108
108
  --file accept a complete canonical astrale.graph.query/v6 document, including
109
109
  Property ordering and Node or Edge reference/value projections. --cursor resumes
@@ -115,7 +115,7 @@ Behavior:
115
115
 
116
116
  Examples:
117
117
  $ astrale query /:notes.example.dev:class.Note --limit 50
118
- $ astrale query --definition /:notes.example.dev:class.Note --limit 50
118
+ $ astrale query --class /:notes.example.dev:class.Note --limit 50
119
119
  $ astrale query @note --edge /:notes.example.dev:class.references --direction outgoing --limit 25
120
120
  $ astrale query --file query.v6.json --cursor "$CURSOR"
121
121
  `,
@@ -124,7 +124,7 @@ Examples:
124
124
  { flags: '--ast <json>', description: 'Canonical Query V6 JSON document' },
125
125
  { flags: '-f, --file <path>', description: 'Read a canonical Query V6 document from a file' },
126
126
  {
127
- flags: '--definition <path>',
127
+ flags: '--class <path>',
128
128
  description: 'Select Nodes satisfying one exact Class',
129
129
  },
130
130
  { flags: '--edge <class>', description: 'Expand one exact Edge Class' },
@@ -112,7 +112,8 @@ async function refreshSdkDeps(check: boolean, assumeYes = false): Promise<boolea
112
112
  * unified and NON-THROWING: an explicit package-managed result uses npm release
113
113
  * identity, while script-install failures remain script failures in `error`
114
114
  * instead of being recategorized. The SDK axis is already best-effort. Skills
115
- * report meaningful health/freshness states without exposing installer metadata.
115
+ * report meaningful health/freshness states. A current cohort also exposes its exact
116
+ * source revision, skill trees, and entrypoints without leaking installer-local paths.
116
117
  */
117
118
  export type StaleReport = {
118
119
  stale: boolean
@@ -237,7 +238,7 @@ Behavior:
237
238
  script installs only — if Astrale was installed by another package manager this
238
239
  command refuses so that manager stays in charge; downloads are checksum-verified
239
240
  before the binary is replaced. (2) The Astrale agent skills: installs every
240
- top-level skill published from astrale-os/cli main, updates healthy older
241
+ top-level skill published from one resolved astrale-os/cli main commit, updates healthy older
241
242
  installs, repairs inconsistent installs, and verifies the result before
242
243
  reporting success. (3) SDK deps: inside a pnpm domain
243
244
  project, proposes any @astrale-os/* dependency with a newer release and, on
@@ -11,7 +11,7 @@ export function classKey(input: string, label: string): ClassKey
11
11
  /** Untrusted CLI fields used to author or admit one exact Query V6 request. */
12
12
  export interface QueryCommandInput {
13
13
  readonly sources: readonly string[]
14
- readonly definition?: string
14
+ readonly class?: string
15
15
  readonly ast?: unknown
16
16
  readonly edge?: string
17
17
  readonly direction?: QueryDirection
@@ -1,14 +1,14 @@
1
1
  import { defineLaw } from '@astrale-os/spec/authoring'
2
2
 
3
- export const CLI_GRAPH_QUERY_V5 = defineLaw({
4
- id: 'CLI-GRAPH-QUERY-V5',
3
+ export const CLI_GRAPH_QUERY_V6 = defineLaw({
4
+ id: 'CLI-GRAPH-QUERY-V6',
5
5
  statement:
6
- 'CLI query input becomes exactly one canonical Query V5 document with an explicit finite limit; exact Property ordering and Node or Edge projection profiles are admitted through the canonical AST surface, while legacy versions and unsupported selector combinations fail before a Graph call.',
6
+ 'CLI query input becomes exactly one canonical Query V6 document with an explicit finite limit; --class authors one exact Class source, exact Property ordering and Node or Edge projection profiles are admitted through the canonical AST surface, and legacy versions or unsupported selector combinations fail before a Graph call.',
7
7
  tests: [
8
- { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-QUERY-V5' },
9
- { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-DEFINITION-QUERY' },
10
- { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-V5-ORDERING' },
11
- { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-V5-PROJECTIONS' },
8
+ { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-QUERY-V6' },
9
+ { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-CLASS-QUERY' },
10
+ { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-V6-ORDERING' },
11
+ { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-V6-PROJECTIONS' },
12
12
  { file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-REJECTS-LEGACY-QUERY' },
13
13
  ],
14
14
  })
@@ -37,11 +37,11 @@ describe('prepareQuery', () => {
37
37
  })
38
38
  })
39
39
 
40
- /** @evidence TEST-CLI-GRAPH-AUTHORS-DEFINITION-QUERY */
40
+ /** @evidence TEST-CLI-GRAPH-AUTHORS-CLASS-QUERY */
41
41
  test('authors an exact Class source without backend query text', () => {
42
42
  const prepared = prepareQuery({
43
43
  sources: [],
44
- definition: '/:issues.astrale.ai:class.Issue',
44
+ class: '/:issues.astrale.ai:class.Issue',
45
45
  limit: '201',
46
46
  })
47
47
 
@@ -67,7 +67,7 @@ describe('prepareQuery', () => {
67
67
  test('unions positional Paths and one Class source in authored order', () => {
68
68
  const prepared = prepareQuery({
69
69
  sources: ['@note'],
70
- definition: '/:issues.astrale.ai:class.Issue',
70
+ class: '/:issues.astrale.ai:class.Issue',
71
71
  })
72
72
 
73
73
  expect(JSON.parse(JSON.stringify(prepared.ast.source))).toEqual({
@@ -167,7 +167,14 @@ describe('prepareQuery', () => {
167
167
  prepareQuery({ sources: ['/:notes.example.dev:class.Note'], limit: 'all' }),
168
168
  ).toThrow('--limit must be a positive integer')
169
169
  expect(() =>
170
- prepareQuery({ sources: [], definition: '/:notes.example.dev:view.note' }),
171
- ).toThrow('--definition must be one canonical Class Path')
170
+ prepareQuery({
171
+ sources: [],
172
+ ast: {},
173
+ class: '/:notes.example.dev:class.Note',
174
+ }),
175
+ ).toThrow('--ast/--file cannot be combined with sources, --class, --edge, or --direction')
176
+ expect(() => prepareQuery({ sources: [], class: '/:notes.example.dev:view.note' })).toThrow(
177
+ '--class must be one canonical Class Path',
178
+ )
172
179
  })
173
180
  })
@@ -12,7 +12,7 @@ import { classReference } from './class'
12
12
 
13
13
  export interface QueryCommandInput {
14
14
  readonly sources: readonly string[]
15
- readonly definition?: string
15
+ readonly class?: string
16
16
  readonly ast?: unknown
17
17
  readonly edge?: string
18
18
  readonly direction?: QueryDirection
@@ -27,36 +27,34 @@ export interface PreparedQuery {
27
27
 
28
28
  const DEFAULT_LIMIT = 100
29
29
 
30
- /** Admit canonical Query V6 or author the intentionally small Path/Definition/one-edge subset. */
30
+ /** Admit canonical Query V6 or author the intentionally small Path/Class/one-edge subset. */
31
31
  export function prepareQuery(input: QueryCommandInput): PreparedQuery {
32
32
  const limit = positiveInteger(input.limit, '--limit', DEFAULT_LIMIT)
33
33
  if (input.ast !== undefined) {
34
34
  if (
35
35
  input.sources.length > 0 ||
36
- input.definition !== undefined ||
36
+ input.class !== undefined ||
37
37
  input.edge !== undefined ||
38
38
  input.direction !== undefined
39
39
  ) {
40
40
  throw new TypeError(
41
- '--ast/--file cannot be combined with sources, --definition, --edge, or --direction',
41
+ '--ast/--file cannot be combined with sources, --class, --edge, or --direction',
42
42
  )
43
43
  }
44
44
  return withPage(QueryAST.decode(input.ast), limit, input.cursor)
45
45
  }
46
46
 
47
- if (input.sources.length === 0 && input.definition === undefined) {
48
- throw new TypeError(
49
- 'query requires a Path source, --definition, or a canonical Query V6 document',
50
- )
47
+ if (input.sources.length === 0 && input.class === undefined) {
48
+ throw new TypeError('query requires a Path source, --class, or a canonical Query V6 document')
51
49
  }
52
50
  if (input.direction !== undefined && input.edge === undefined) {
53
51
  throw new TypeError('--direction requires --edge')
54
52
  }
55
53
 
56
54
  const paths = input.sources.map((source) => Path.parse(source))
57
- const definition =
58
- input.definition === undefined ? undefined : classReference(input.definition, '--definition')
59
- const nodes = definition === undefined ? paths : [...paths, definition]
55
+ const selectedClass =
56
+ input.class === undefined ? undefined : classReference(input.class, '--class')
57
+ const nodes = selectedClass === undefined ? paths : [...paths, selectedClass]
60
58
  const query: NodeQueryBuilder<unknown> = Query.from({
61
59
  nodes: nodes as [QueryNodeInput, ...QueryNodeInput[]],
62
60
  })
@@ -145,6 +145,12 @@ async function installFixture(
145
145
  ) {
146
146
  const calls: string[][] = []
147
147
  await installer(sourceRoot, snapshot, home, lockPath, calls)('npx', [])
148
+ const lock = await readLock(lockPath)
149
+ for (const skill of snapshot.skills) {
150
+ lock.skills[skill.name].astraleSourceRevision = snapshot.revision
151
+ lock.skills[skill.name].astraleSourceTree = skill.tree
152
+ }
153
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
148
154
  }
149
155
 
150
156
  describe('Astrale skill reconciliation', () => {
@@ -164,9 +170,17 @@ describe('Astrale skill reconciliation', () => {
164
170
  expect(calls).toHaveLength(1)
165
171
  expect(calls[0]).toContain(`astrale-os/cli#${source.snapshot.ref}`)
166
172
  expect(requestedSkills(calls[0])).toEqual(source.snapshot.skills.map((skill) => skill.name))
167
- expect(Object.keys((await readLock(target.lockPath)).skills).sort()).toEqual(
173
+ const installedLock = await readLock(target.lockPath)
174
+ expect(Object.keys(installedLock.skills).sort()).toEqual(
168
175
  source.snapshot.skills.map((skill) => skill.name).sort(),
169
176
  )
177
+ for (const skill of source.snapshot.skills) {
178
+ expect(installedLock.skills[skill.name]).toMatchObject({
179
+ ref: 'main',
180
+ astraleSourceRevision: source.snapshot.revision,
181
+ astraleSourceTree: skill.tree,
182
+ })
183
+ }
170
184
  })
171
185
 
172
186
  test('a coherent older cohort updates every current source skill', async () => {
@@ -398,7 +412,18 @@ describe('Astrale skill reconciliation', () => {
398
412
  lockPath: target.lockPath,
399
413
  resolveSource: async () => source.snapshot,
400
414
  }),
401
- ).toEqual({ status: 'current' })
415
+ ).toEqual({
416
+ status: 'current',
417
+ source: {
418
+ repository: 'astrale-os/cli',
419
+ revision: source.snapshot.revision,
420
+ skills: source.snapshot.skills.map(({ name, tree, path }) => ({
421
+ name,
422
+ tree,
423
+ entrypoint: path,
424
+ })),
425
+ },
426
+ })
402
427
  expect(await filesystemSnapshot(target.root)).toBe(beforeCurrentCheck)
403
428
 
404
429
  await rm(join(target.home, '.agents/skills/astrale-cli'), { recursive: true, force: true })
@@ -52,6 +52,11 @@ export type SkillApplyStatus =
52
52
  export type SkillCheckResult = {
53
53
  status: SkillCheckStatus
54
54
  error?: string
55
+ source?: {
56
+ repository: typeof ASTRALE_CLI_SKILL_SOURCE
57
+ revision: string
58
+ skills: Array<{ name: string; tree: string; entrypoint: string }>
59
+ }
55
60
  }
56
61
 
57
62
  export type SkillApplyResult = {
@@ -70,6 +75,8 @@ type SkillLockEntry = {
70
75
  skillFolderHash?: string
71
76
  installedAt?: string
72
77
  updatedAt?: string
78
+ astraleSourceRevision?: string
79
+ astraleSourceTree?: string
73
80
  }
74
81
 
75
82
  type SkillLock = {
@@ -83,6 +90,7 @@ type SkillState = 'absent' | 'current' | 'outdated' | 'unhealthy'
83
90
 
84
91
  type SkillInspection = {
85
92
  state: SkillState
93
+ sourceCurrent: boolean
86
94
  managedNames: string[]
87
95
  managedFolders: string[]
88
96
  }
@@ -297,7 +305,7 @@ async function inspectAstraleSkills(
297
305
  }),
298
306
  )
299
307
  if (managed.length === 0 && expectedPresence.every((present) => !present)) {
300
- return { state: 'absent', managedNames: [], managedFolders: [] }
308
+ return { state: 'absent', sourceCurrent: false, managedNames: [], managedFolders: [] }
301
309
  }
302
310
 
303
311
  const folders = managed.flatMap(([name, entry]) => {
@@ -348,7 +356,7 @@ async function inspectAstraleSkills(
348
356
  }
349
357
  }
350
358
 
351
- const exactCurrent =
359
+ const sourceCurrent =
352
360
  coherent &&
353
361
  managed.length === snapshot.skills.length &&
354
362
  folders.every(({ key, folder, entry }) => {
@@ -360,9 +368,20 @@ async function inspectAstraleSkills(
360
368
  actualHashes.get(folder) === skill.tree
361
369
  )
362
370
  })
371
+ const exactCurrent =
372
+ sourceCurrent &&
373
+ folders.every(({ folder, entry }) => {
374
+ const skill = expected.get(folder)
375
+ return (
376
+ skill !== undefined &&
377
+ entry.astraleSourceRevision === snapshot.revision &&
378
+ entry.astraleSourceTree === skill.tree
379
+ )
380
+ })
363
381
 
364
382
  return {
365
383
  state: exactCurrent ? 'current' : coherent ? 'outdated' : 'unhealthy',
384
+ sourceCurrent,
366
385
  managedNames: managed.map(([name]) => name),
367
386
  managedFolders: [...uniqueFolders],
368
387
  }
@@ -405,6 +424,28 @@ async function installSnapshot(
405
424
  )
406
425
  }
407
426
 
427
+ async function stampAstraleSource(
428
+ snapshot: AstraleSkillSourceSnapshot,
429
+ lockPath: string,
430
+ ): Promise<void> {
431
+ const { lock } = await readSkillLock(lockPath)
432
+ if (!lock) throw new Error('skill installer receipt is unavailable after installation')
433
+ for (const skill of snapshot.skills) {
434
+ const entry = lock.skills[skill.name]
435
+ if (
436
+ !entry ||
437
+ !sourceOwned(entry) ||
438
+ entry.ref !== snapshot.ref ||
439
+ entry.skillPath !== skill.path
440
+ ) {
441
+ throw new Error(`skill installer receipt is incomplete for ${skill.name}`)
442
+ }
443
+ entry.astraleSourceRevision = snapshot.revision
444
+ entry.astraleSourceTree = skill.tree
445
+ }
446
+ await writeSkillLock(lockPath, lock)
447
+ }
448
+
408
449
  async function selectedAgents(home: string, lockPath: string): Promise<string[]> {
409
450
  const { lock } = await readSkillLock(lockPath)
410
451
  const agents = new Set(
@@ -693,13 +734,27 @@ export async function checkAstraleSkills(
693
734
  dependencies.home,
694
735
  dependencies.lockPath,
695
736
  )
737
+ const status =
738
+ inspection.state === 'current'
739
+ ? 'current'
740
+ : inspection.state === 'unhealthy'
741
+ ? 'repair-needed'
742
+ : 'update-available'
696
743
  return {
697
- status:
698
- inspection.state === 'current'
699
- ? 'current'
700
- : inspection.state === 'unhealthy'
701
- ? 'repair-needed'
702
- : 'update-available',
744
+ status,
745
+ ...(status === 'current'
746
+ ? {
747
+ source: {
748
+ repository: ASTRALE_CLI_SKILL_SOURCE,
749
+ revision: snapshot.revision,
750
+ skills: snapshot.skills.map(({ name, tree, path }) => ({
751
+ name,
752
+ tree,
753
+ entrypoint: path,
754
+ })),
755
+ },
756
+ }
757
+ : {}),
703
758
  }
704
759
  } catch (error) {
705
760
  return { status: 'unavailable', error: error instanceof Error ? error.message : String(error) }
@@ -768,6 +823,15 @@ export async function syncAstraleSkills(
768
823
  dependencies.run,
769
824
  knownRetired,
770
825
  )
826
+ const installedInspection = await inspectAstraleSkills(
827
+ snapshot,
828
+ dependencies.home,
829
+ dependencies.lockPath,
830
+ )
831
+ if (!installedInspection.sourceCurrent) {
832
+ throw new Error('installed Astrale skills do not match the resolved source')
833
+ }
834
+ await stampAstraleSource(snapshot, dependencies.lockPath)
771
835
  const verified = await inspectAstraleSkills(
772
836
  snapshot,
773
837
  dependencies.home,
@@ -195,7 +195,7 @@ describe('program composition', () => {
195
195
  'whoami',
196
196
  ])
197
197
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
198
- '092b8f4f737740105c8baed9d01a81604711b5b2719dfcb834eeeac414732c61',
198
+ '91c35b18985ad8606b5f2429fd8860b020a93edccb30dfbaa74d501929a8513b',
199
199
  )
200
200
  })
201
201
 
@@ -464,13 +464,50 @@ describe('help contract — read command split', () => {
464
464
  expect(queryHelp).not.toContain('--children <json>')
465
465
  expect(queryHelp).not.toContain('--edges <json>')
466
466
  expect(queryHelp).toContain('--ast <json>')
467
- expect(queryHelp).toContain('--definition <path>')
467
+ expect(queryHelp).toContain('--class <path>')
468
+ expect(queryHelp).not.toContain('--definition')
468
469
  expect(queryHelp).toContain('--edge <class>')
469
470
  expect(queryHelp).toContain('--direction <direction>')
470
471
  expect(queryHelp).toContain('--limit <n>')
471
472
  expect(queryHelp).toContain('--cursor <token>')
472
473
  expect(queryHelp).not.toContain('--cypher <query>')
473
474
  })
475
+
476
+ test('keeps the removed query --definition flag outside the parsed command surface', async () => {
477
+ const program = await buildProgram()
478
+ const query = allCommands(program).find((command) => command.name() === 'query')
479
+ const parsed = query?.parseOptions(['--definition', '/:notes.example.dev:class.Note'])
480
+
481
+ expect(parsed?.unknown).toContain('--definition')
482
+ expect(query?.options.some((option) => option.attributeName() === 'definition')).toBe(false)
483
+ })
484
+
485
+ test('routes query --class through Commander as the exact Class option', async () => {
486
+ const program = await buildProgram()
487
+ const query = allCommands(program).find((command) => command.name() === 'query')
488
+ let observed: unknown
489
+ query?.action((sources, options) => {
490
+ observed = { sources, class: options.class, limit: options.limit, json: options.json }
491
+ })
492
+
493
+ await program.parseAsync([
494
+ 'node',
495
+ 'astrale',
496
+ 'query',
497
+ '--class',
498
+ '/:notes.example.dev:class.Note',
499
+ '--limit',
500
+ '1',
501
+ '--json',
502
+ ])
503
+
504
+ expect(observed).toEqual({
505
+ sources: [],
506
+ class: '/:notes.example.dev:class.Note',
507
+ limit: '1',
508
+ json: true,
509
+ })
510
+ })
474
511
  })
475
512
 
476
513
  describe('help contract — explicit anonymous authentication', () => {
@@ -341,7 +341,7 @@ const bundle = {
341
341
 
342
342
  const queryArgs = [
343
343
  'query',
344
- '--definition',
344
+ '--class',
345
345
  '/:issues.example.dev:class.Issue',
346
346
  '--limit',
347
347
  '201',
@@ -45,7 +45,7 @@ test('machine runner uses the exact descriptor, appends json mode, and versions
45
45
  })
46
46
 
47
47
  const result = await runStudioCliJson(
48
- ['query', '--definition', '/:example.dev:class.Issue'],
48
+ ['query', '--class', '/:example.dev:class.Issue'],
49
49
  decodeJsonObject,
50
50
  )
51
51
 
@@ -55,7 +55,7 @@ test('machine runner uses the exact descriptor, appends json mode, and versions
55
55
  exitCode: 0,
56
56
  timedOut: false,
57
57
  data: {
58
- command: ['query', '--definition', '/:example.dev:class.Issue', '--json'],
58
+ command: ['query', '--class', '/:example.dev:class.Issue', '--json'],
59
59
  },
60
60
  })
61
61
  })
@@ -65,7 +65,7 @@ export async function listViewTargets(
65
65
  const result = await runAstraleJson<RawQueryResult>(
66
66
  [
67
67
  'query',
68
- '--definition',
68
+ '--class',
69
69
  definition,
70
70
  '--limit',
71
71
  String(TARGET_LIMIT + 1),