@gaia-ai/core 0.9.2 → 0.11.0

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.
@@ -1,6 +1,20 @@
1
1
  import type { DropSHPlugin } from 'dropsh/plugin';
2
2
  import type { GaiaCommandHost } from './commands.js';
3
3
  import { type GaiaConfigSource } from './gaia-dir.js';
4
+ /**
5
+ * A connection config that was RESOLVED but is not usable — a missing
6
+ * `site.base_url`, a missing `workspace_id`.
7
+ *
8
+ * It exists so a caller can tell "there is no config" from "the config is right
9
+ * there and says something wrong". `gaia dropsh` otherwise reports every load
10
+ * failure as `no connection config could be loaded`, which sends the reader
11
+ * hunting for a file that was found and read. Matched by `name` rather than
12
+ * `instanceof`, so a duplicated `@gaia-ai/core` in a pnpm tree cannot break the
13
+ * check.
14
+ */
15
+ export declare class GaiaConnectionConfigError extends Error {
16
+ readonly name = "GaiaConnectionConfigError";
17
+ }
4
18
  /** The resolved control-plane connection: site + constructed auth plugins. */
5
19
  export interface GaiaConnectionConfig {
6
20
  site: {
@@ -8,6 +22,11 @@ export interface GaiaConnectionConfig {
8
22
  jsonapi_prefix: string;
9
23
  };
10
24
  plugins: DropSHPlugin[];
25
+ /**
26
+ * GAIA-351: the uuid of the `gaia_workspace` this connection creates into.
27
+ * REQUIRED at the config's top level — see `loadGaiaConfig`.
28
+ */
29
+ workspace_id: string;
11
30
  /** Absolute path of the loaded connection config. */
12
31
  config_path: string;
13
32
  /** true when this is the shipped machine-context fallback. */
@@ -16,6 +35,28 @@ export interface GaiaConnectionConfig {
16
35
  source: GaiaConfigSource;
17
36
  /** true when the loaded file is a legacy `conductor.config.js` connection. */
18
37
  legacy: boolean;
38
+ /**
39
+ * GAIA-373: UI extension entry modules discovered from `addons[]`, as
40
+ * RESOLVED `file://` URLs (see `resolveUiEntries` for why resolution happens
41
+ * here rather than at the import).
42
+ *
43
+ * Nothing is imported — `gaia dropsh` loads this same connection and never
44
+ * renders a TUI, so the cost of a UI contribution must not be paid until
45
+ * `gaia ui`'s kernel actually boots (spec F2/D3).
46
+ */
47
+ ui_entries: string[];
48
+ }
49
+ /**
50
+ * The connection config's top-level values, handed to every contribution as its
51
+ * base options (GAIA-351). An addon entry's own `with`/`options` is merged over
52
+ * them, so an explicit per-entry value always wins.
53
+ *
54
+ * This exists because the ambient workspace is NOT configured per addon: the
55
+ * filler is a child of `@gaia-ai/addon-essentials`, which no config passes
56
+ * options to, so the uuid has to travel as a global rather than as a `with`.
57
+ */
58
+ export interface GaiaConnectionGlobals {
59
+ workspace_id: string;
19
60
  }
20
61
  /**
21
62
  * Load the connection config for `gaia ui` / `gaia dropsh` / conductor-auth.
@@ -4,19 +4,44 @@ import { discoverAddons } from '../plugins/discover-addons.js';
4
4
  import { resolveGaiaConfigPath } from './gaia-dir.js';
5
5
  import { resolveMachineContextPath } from './machine-context.js';
6
6
  import { resolveModuleEslintStyle } from './resolve-module.js';
7
+ // GAIA-201: the CONNECTION loader. It reads ONLY `{ site, plugins }` from a
8
+ // `gaia.config.js` (or a legacy `conductor.config.js` — back-compat), and never
9
+ // constructs the engine slots (remote/executor/agent/workspace). This is the
10
+ // deepest AC-2 fix: `gaia dropsh …` used to call `loadConductorConfig`, which
11
+ // eagerly built the whole engine plugin farm just to read the auth `plugins`.
12
+ // dropsh + ui now use this lenient loader instead, so a JSON:API read (or a
13
+ // home-rooted connection with no engine wiring at all) loads cleanly.
14
+ /**
15
+ * A connection config that was RESOLVED but is not usable — a missing
16
+ * `site.base_url`, a missing `workspace_id`.
17
+ *
18
+ * It exists so a caller can tell "there is no config" from "the config is right
19
+ * there and says something wrong". `gaia dropsh` otherwise reports every load
20
+ * failure as `no connection config could be loaded`, which sends the reader
21
+ * hunting for a file that was found and read. Matched by `name` rather than
22
+ * `instanceof`, so a duplicated `@gaia-ai/core` in a pnpm tree cannot break the
23
+ * check.
24
+ */
25
+ export class GaiaConnectionConfigError extends Error {
26
+ name = 'GaiaConnectionConfigError';
27
+ }
7
28
  function isRecord(value) {
8
29
  return typeof value === 'object' && value !== null && !Array.isArray(value);
9
30
  }
10
31
  function isPluginDescriptor(entry) {
11
32
  return isRecord(entry) && typeof entry.plugin === 'string';
12
33
  }
34
+ /** Merge the config globals under an entry's own options; the entry wins. */
35
+ function withGlobals(globals, own) {
36
+ return isRecord(own) ? { ...globals, ...own } : { ...globals };
37
+ }
13
38
  /**
14
39
  * Construct a `{ plugin, export?, with?/options? }` descriptor. Resolution base
15
40
  * order: the config's own dir first (so a project config can name a locally
16
41
  * installed plugin), then the host's `resolveBases` (host install → cwd) for
17
42
  * pnpm-safe resolution of the shipped fallback's `@dropsh/plugin-*`.
18
43
  */
19
- async function loadDescriptor(entry, bases) {
44
+ async function loadDescriptor(entry, bases, globals) {
20
45
  const resolved = resolveModuleEslintStyle(entry.plugin, bases);
21
46
  if (resolved === undefined) {
22
47
  throw new Error(`gaia.config.js cannot resolve plugin '${entry.plugin}'`);
@@ -41,15 +66,15 @@ async function loadDescriptor(entry, bases) {
41
66
  throw new Error(`plugin '${entry.plugin}' has no default export and ${fns.length} function exports (${fns.join(', ')}); specify "export"`);
42
67
  }
43
68
  }
44
- return factory(entry.with ?? entry.options);
69
+ return factory(withGlobals(globals, entry.with ?? entry.options));
45
70
  }
46
71
  /** Resolve the `plugins[]` array: descriptors constructed, constructed entries
47
72
  * passed through; a factory returning an array is flattened one level (dropsh
48
73
  * `composePlugins` semantics), mirroring the engine loader. */
49
- async function resolvePlugins(raw, bases) {
74
+ async function resolvePlugins(raw, bases, globals) {
50
75
  if (!Array.isArray(raw))
51
76
  return [];
52
- const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadDescriptor(entry, bases) : entry));
77
+ const resolved = await Promise.all(raw.map((entry) => isPluginDescriptor(entry) ? loadDescriptor(entry, bases, globals) : entry));
53
78
  return resolved.flat();
54
79
  }
55
80
  /**
@@ -87,11 +112,27 @@ export async function loadGaiaConfig(host, opts = {}) {
87
112
  const expected = resolution.fallback
88
113
  ? 'the shipped machine-context fallback resolved no base_url — run `gaia conductor init` or create ~/.gaia/gaia.config.js'
89
114
  : 'expected a gaia.config.js (project ./.gaia/gaia.config.js or home ~/.gaia/gaia.config.js) declaring site.base_url';
90
- throw new Error(`gaia connection config requires site.base_url — resolved ${configPath}; ${expected}`);
115
+ throw new GaiaConnectionConfigError(`gaia connection config requires site.base_url — resolved ${configPath}; ${expected}`);
91
116
  }
92
117
  const jsonapiPrefix = typeof site.jsonapi_prefix === 'string' && site.jsonapi_prefix.trim() !== ''
93
118
  ? site.jsonapi_prefix
94
119
  : '/jsonapi';
120
+ // GAIA-351: `workspace_id` is REQUIRED, at the config's TOP LEVEL — a sibling
121
+ // of `site`, not a member of it and not an addon's `with`. The ambient-
122
+ // workspace filler ships inside `@gaia-ai/addon-essentials`, which every
123
+ // connection loads and which no config passes options to, so the uuid has
124
+ // nowhere else to come from. Required rather than optional because there is no
125
+ // longer a "leave the addon out" form: the filler is always present, and a
126
+ // connection that cannot say which workspace it creates into would fail later,
127
+ // on the wire, as an opaque 422 from the server's `GaiaWorkspaceScope`.
128
+ const workspaceId = typeof raw.workspace_id === 'string' ? raw.workspace_id.trim() : '';
129
+ if (workspaceId === '') {
130
+ throw new GaiaConnectionConfigError(`gaia connection config requires a top-level workspace_id — resolved ${configPath}; ` +
131
+ "add `workspace_id: '<gaia_workspace uuid>'` beside `site` (it is the workspace every create is bound to). " +
132
+ 'Find the uuid on the control plane at /admin/gaia/workspace — not with `gaia dropsh`, ' +
133
+ 'which reads this same config and would fail the same way.');
134
+ }
135
+ const globals = { workspace_id: workspaceId };
95
136
  // Config dir first, then the host bases (pnpm-safe for the shipped fallback).
96
137
  const bases = [
97
138
  pathToFileURL(`${dirname(configPath)}/`).href,
@@ -101,9 +142,11 @@ export async function loadGaiaConfig(host, opts = {}) {
101
142
  // the legacy `plugins:[]` descriptor array AND the Storybook-style `addons:[]`
102
143
  // discovered via the shared engine (connection surface = the auth/renderer
103
144
  // plugins). Concatenate legacy first, then discovered.
104
- const legacy = await resolvePlugins(raw.plugins, bases);
145
+ const legacy = await resolvePlugins(raw.plugins, bases, globals);
105
146
  const discovered = Array.isArray(raw.addons)
106
- ? await discoverAddons(raw.addons, 'connection', bases)
147
+ ? await discoverAddons(raw.addons, 'connection', bases, {
148
+ globals,
149
+ })
107
150
  : undefined;
108
151
  const plugins = discovered
109
152
  ? [...legacy, ...discovered.connectionPlugins]
@@ -111,9 +154,40 @@ export async function loadGaiaConfig(host, opts = {}) {
111
154
  return {
112
155
  site: { base_url: baseUrl, jsonapi_prefix: jsonapiPrefix },
113
156
  plugins,
157
+ workspace_id: workspaceId,
114
158
  config_path: configPath,
115
159
  fallback: resolution.fallback,
116
160
  source: resolution.source,
117
161
  legacy: resolution.legacy,
162
+ ui_entries: discovered ? resolveUiEntries(discovered.uiEntries, bases) : [],
118
163
  };
119
164
  }
165
+ /**
166
+ * Resolve each UI entry specifier to an importable file URL — GAIA-373.
167
+ *
168
+ * Resolution belongs HERE, against the very bases that found the addon's
169
+ * `./preset`, and not in the renderer that eventually imports them. A preset
170
+ * names a package specifier (`@gaia-ai/addon-x/ui`) because that is what its
171
+ * author can honestly write; but the module that imports it is the TUI kernel,
172
+ * inside `@gaia-ai/addon-gaia-ui`, and a bare specifier there resolves against
173
+ * THAT package's dependencies. A contribution addon is its SIBLING, never its
174
+ * dependency — the layered DAG forbids the backwards edge — so the bare import
175
+ * fails, and it fails at cockpit boot rather than at config load.
176
+ *
177
+ * Measured, not reasoned: `gaia ui GAIA-373` with the addon listed reported
178
+ * `Cannot find module '@gaia-ai/addon-gaia-ui-artifacts/ui' from
179
+ * .../addons/gaia-ui/dist/src/Extension/load.js`.
180
+ *
181
+ * Still NOTHING IS IMPORTED here (F2): a resolve is a path lookup, and the
182
+ * megabytes behind the specifier stay unloaded until a TUI actually boots.
183
+ */
184
+ function resolveUiEntries(entries, bases) {
185
+ return entries.map((entry) => {
186
+ const resolved = resolveModuleEslintStyle(entry, bases);
187
+ if (resolved === undefined) {
188
+ throw new Error(`gaia.config.js cannot resolve UI entry '${entry}' — the addon that ` +
189
+ 'contributes it must be installed where the config can see it');
190
+ }
191
+ return pathToFileURL(resolved).href;
192
+ });
193
+ }
@@ -9,6 +9,11 @@ export interface MachineContext {
9
9
  base_url: string;
10
10
  client_id: string;
11
11
  client_secret: string;
12
+ /**
13
+ * Host concurrent-run budget (GAIA-353). Optional; consumers default to 1.
14
+ * Source of truth for capacity — not the engine conductor.config.js.
15
+ */
16
+ max_parallel?: number;
12
17
  }
13
18
  /** Canonical machine-context path: `~/.gaia/machine.config.js` (GAIA-201). */
14
19
  export declare function machineContextPath(): string;
@@ -2,7 +2,7 @@ export type { GaiaCommandHost, GaiaCommandPlugin } from './cli/commands.js';
2
2
  export { declaresTopLevelKey, findDefaultExportObject, matchDelimiter, type PropEntry, readKey, scanTopLevelProperties, scanToTopLevelComma, skipBlockComment, skipLineComment, skipString, skipTrivia, } from './cli/config-source.js';
3
3
  export type { GaiaConfigResolution, GaiaConfigSource, } from './cli/gaia-dir.js';
4
4
  export { findGaiaConfig, findGaiaDir, findProjectGaiaDir, homeGaiaDir, resolveConfigPath, resolveGaiaConfigPath, stemForConfigFile, } from './cli/gaia-dir.js';
5
- export { type GaiaConnectionConfig, loadGaiaConfig, } from './cli/load-gaia-config.js';
5
+ export { type GaiaConnectionConfig, GaiaConnectionConfigError, type GaiaConnectionGlobals, loadGaiaConfig, } from './cli/load-gaia-config.js';
6
6
  export { legacyMachineContextPath, type MachineContext, machineContextPath, readMachineContext, resolveMachineContextPath, } from './cli/machine-context.js';
7
7
  export { corePackageRoot, homeFallbackGaiaConfigPath, } from './cli/paths.js';
8
8
  export { resolveModuleEslintStyle } from './cli/resolve-module.js';
@@ -16,4 +16,4 @@ export { shellQuote } from './core/shell.js';
16
16
  export { DEFAULT_SLUG_MAX_LENGTH, slugify } from './core/slug.js';
17
17
  export { discoverAddons, type ResolvedConductorSlots, resolveConductorSlots, } from './plugins/discover-addons.js';
18
18
  export { type AddonEntry, type CommandDescriptor, type DiscoveredContributions, emptyContributions, type GaiaPreset, type GaiaSurface, type OpaqueAccumulator, PRESET_FUNCTION_KEYS, SURFACE_KEYS, } from './plugins/preset.js';
19
- export { type ExpandedLoad, expandLoad, type InputDecl, parseStepValues, readSkillWhen, type SkillContract, type SkillWhen, StepContractError, type StepValues, type Triple, validateLoad, type WhenValue, WORKFLOW_STEPS, type WorkflowStep, } from './workflow/step-contract.js';
19
+ export { deriveProjectTriples, type EmptyLoaderShape, type ExpandedLoad, expandLoad, type IndentedLoadSite, type InputDecl, measureEmptyLoader, parseStepValues, readSkillWhen, type SkillContract, type SkillWhen, StepContractError, type StepContractErrorCode, type StepValues, type Triple, validateLoad, type WhenValue, WORKFLOW_CLAIMABLE_STATES, WORKFLOW_STEPS, type WorkflowStep, } from './workflow/step-contract.js';
package/dist/src/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // config source declare?" — core is layer 0 and cannot import the conductor's copy.
5
5
  export { declaresTopLevelKey, findDefaultExportObject, matchDelimiter, readKey, scanTopLevelProperties, scanToTopLevelComma, skipBlockComment, skipLineComment, skipString, skipTrivia, } from './cli/config-source.js';
6
6
  export { findGaiaConfig, findGaiaDir, findProjectGaiaDir, homeGaiaDir, resolveConfigPath, resolveGaiaConfigPath, stemForConfigFile, } from './cli/gaia-dir.js';
7
- export { loadGaiaConfig, } from './cli/load-gaia-config.js';
7
+ export { GaiaConnectionConfigError, loadGaiaConfig, } from './cli/load-gaia-config.js';
8
8
  export { legacyMachineContextPath, machineContextPath, readMachineContext, resolveMachineContextPath, } from './cli/machine-context.js';
9
9
  export { corePackageRoot, homeFallbackGaiaConfigPath, } from './cli/paths.js';
10
10
  export { resolveModuleEslintStyle } from './cli/resolve-module.js';
@@ -32,4 +32,4 @@ export { emptyContributions, PRESET_FUNCTION_KEYS, SURFACE_KEYS, } from './plugi
32
32
  // GAIA-194 AC-2: the agent-host contract (`AgentLaunchHost` / `HostedAgent` /
33
33
  // `supportsAgentHost`) is likewise NOT here — it is the TUI renderer's own plugin
34
34
  // seam (`@gaia-ai/addon-gaia-ui`), and herdr types its impl locally.
35
- export { expandLoad, parseStepValues, readSkillWhen, StepContractError, validateLoad, WORKFLOW_STEPS, } from './workflow/step-contract.js';
35
+ export { deriveProjectTriples, expandLoad, measureEmptyLoader, parseStepValues, readSkillWhen, StepContractError, validateLoad, WORKFLOW_CLAIMABLE_STATES, WORKFLOW_STEPS, } from './workflow/step-contract.js';
@@ -9,6 +9,7 @@ import { type AddonEntry, type DiscoveredContributions, type GaiaSurface } from
9
9
  */
10
10
  export declare function discoverAddons(entries: AddonEntry[] | undefined, surface: GaiaSurface, bases: string[], opts?: {
11
11
  logger?: ConductorLogger;
12
+ globals?: unknown;
12
13
  }): Promise<DiscoveredContributions>;
13
14
  /**
14
15
  * The resolved conductor slots (last-wins singletons + agent candidate list).
@@ -135,6 +135,26 @@ async function loadPreset(spec, bases, warn) {
135
135
  return { preset: mod, key: mainPath };
136
136
  return { preset: adaptDefaultExport(mod, spec, warn), key: mainPath };
137
137
  }
138
+ /**
139
+ * Merge the caller's global options under an entry's own `with` (GAIA-351).
140
+ *
141
+ * A preset normally reads only what its own config entry passed it. That breaks
142
+ * down for a value carried by a meta-addon's CHILD: a child is listed as a bare
143
+ * package name, so it has no `with` at all, and the config has no place to write
144
+ * one. `globals` is the config-level fallback — every preset sees it, an entry's
145
+ * own `with` overrides it key by key, and an entry that ignores the extra keys
146
+ * (all of them but one, today) is unaffected.
147
+ */
148
+ function mergeGlobals(globals, own) {
149
+ if (globals === undefined)
150
+ return own;
151
+ const g = globals;
152
+ if (own === undefined)
153
+ return { ...g };
154
+ if (typeof own !== 'object' || own === null || Array.isArray(own))
155
+ return own;
156
+ return { ...g, ...own };
157
+ }
138
158
  /** Run one preset's surface accumulators into `acc`, attaching `priority` to any
139
159
  * newly appended agent candidates. */
140
160
  async function applyPreset(preset, opts, priority, surface, acc) {
@@ -186,7 +206,7 @@ export async function discoverAddons(entries, surface, bases, opts = {}) {
186
206
  if (Array.isArray(preset.addons) && preset.addons.length > 0) {
187
207
  await walk(preset.addons);
188
208
  }
189
- await applyPreset(preset, withOpts, priority, surface, acc);
209
+ await applyPreset(preset, mergeGlobals(opts.globals, withOpts), priority, surface, acc);
190
210
  }
191
211
  };
192
212
  await walk(entries);
@@ -60,6 +60,18 @@ export interface DiscoveredContributions {
60
60
  /** Opaque: `AgentCandidate[]` on the conductor surface. */
61
61
  agents: unknown[];
62
62
  connectionPlugins: DropSHPlugin[];
63
+ /**
64
+ * GAIA-373: UI extension entry MODULES — specifiers, never constructed
65
+ * contributions.
66
+ *
67
+ * Storybook's `managerEntries` model, and load-bearing for a measured reason:
68
+ * `discoverAddons` runs on every connection load (`gaia dropsh` included), and
69
+ * `@opentui/core` is megabytes of native code that a preset returning objects
70
+ * would pull onto that path — the very import GAIA-326 had to make dynamic.
71
+ * Concretely typed rather than opaque: a specifier is a string, so the kernel
72
+ * needs no foreign type for it and stays surface-agnostic all the same.
73
+ */
74
+ uiEntries: string[];
63
75
  }
64
76
  /**
65
77
  * An OPAQUE per-surface accumulator. The kernel threads it without knowing its
@@ -87,6 +99,7 @@ export interface GaiaPreset<O = unknown> {
87
99
  workspaces?: OpaqueAccumulator<O>;
88
100
  agents?: OpaqueAccumulator<O>;
89
101
  connectionPlugins?: (acc: DropSHPlugin[], opts: O) => DropSHPlugin[] | Promise<DropSHPlugin[]>;
102
+ uiEntries?: (acc: string[], opts: O) => string[] | Promise<string[]>;
90
103
  }
91
104
  /** The preset function keys, for shape-detection during discovery. */
92
105
  export declare const PRESET_FUNCTION_KEYS: Array<keyof DiscoveredContributions>;
@@ -28,7 +28,7 @@
28
28
  export const SURFACE_KEYS = {
29
29
  command: ['commands'],
30
30
  conductor: ['remotes', 'executors', 'workspaces', 'agents'],
31
- connection: ['connectionPlugins'],
31
+ connection: ['connectionPlugins', 'uiEntries'],
32
32
  };
33
33
  /** The preset function keys, for shape-detection during discovery. */
34
34
  export const PRESET_FUNCTION_KEYS = [
@@ -38,6 +38,7 @@ export const PRESET_FUNCTION_KEYS = [
38
38
  'workspaces',
39
39
  'agents',
40
40
  'connectionPlugins',
41
+ 'uiEntries',
41
42
  ];
42
43
  /** An empty contributions accumulator. */
43
44
  export function emptyContributions() {
@@ -48,5 +49,6 @@ export function emptyContributions() {
48
49
  workspaces: [],
49
50
  agents: [],
50
51
  connectionPlugins: [],
52
+ uiEntries: [],
51
53
  };
52
54
  }
@@ -1,9 +1,42 @@
1
1
  /** The workflow states an agent works — the legal `step` values. */
2
2
  export declare const WORKFLOW_STEPS: readonly ["qualification", "spec", "diagnose", "coding", "review", "pre_deployment", "post_deployment", "verifying", "summary"];
3
3
  export type WorkflowStep = (typeof WORKFLOW_STEPS)[number];
4
- /** Typed defect raised on the first contract violation. */
4
+ /**
5
+ * The machine-readable classification of a contract defect, for consumers that
6
+ * must branch on *which* defect it is rather than on what it says.
7
+ *
8
+ * GAIA-436: `gaia validate` used to recover this from a regex over the message
9
+ * text (`/^loaded skill \`([^\`]+)\` not found\.$/`). That leaked twice — the
10
+ * sibling defect raised by the same `walk()` (`bundle \`x\` loads unknown skill
11
+ * \`y\`.`) is the same class of problem and matched nothing, so it got no remedy
12
+ * printed; and rewording any message would have broken the consumer across the
13
+ * package boundary without a single failing test to say so.
14
+ *
15
+ * Deliberately narrow: exactly the two codes a caller branches on **today**. Most
16
+ * throw sites in this module carry no code, and that is correct — a code is added
17
+ * when a consumer needs to distinguish that defect, never speculatively, because
18
+ * an unused code is a contract nobody validates.
19
+ */
20
+ export type StepContractErrorCode = 'unresolved_skill' | 'empty_loader';
21
+ /**
22
+ * Typed defect raised on the first contract violation.
23
+ *
24
+ * The single-argument form `new StepContractError(message)` is the norm and stays
25
+ * the norm; `details` is opt-in for the few defects a consumer classifies.
26
+ */
5
27
  export declare class StepContractError extends Error {
6
- constructor(message: string);
28
+ /** The defect's classification, when it is one a consumer branches on. */
29
+ readonly code?: StepContractErrorCode;
30
+ /**
31
+ * For `unresolved_skill`: the skill name that could not be resolved — the root
32
+ * bullet's, or the bundle member's. Named `skill` rather than `name` because
33
+ * `Error.name` is already taken (and set to `StepContractError` below).
34
+ */
35
+ readonly skill?: string;
36
+ constructor(message: string, details?: {
37
+ code: StepContractErrorCode;
38
+ skill?: string;
39
+ });
7
40
  }
8
41
  /**
9
42
  * A normalized `when` clause value: a concrete list of allowed values, the
@@ -60,14 +93,50 @@ export interface Triple {
60
93
  workflow: string;
61
94
  step: WorkflowStep;
62
95
  }
63
- /** Per-skill values, keyed by skill `name` (the `@gaia/` prefix stripped). */
96
+ /** Per-skill values, keyed by skill `name` (the namespace prefix stripped). */
64
97
  export type StepValues = Record<string, Record<string, string>>;
98
+ /** One `- @…` line the indent-0 anchor read as value rather than as an address. */
99
+ export interface IndentedLoadSite {
100
+ /** 1-based line number, so a report can point the author straight at it. */
101
+ line: number;
102
+ /** Width of the whitespace before the marker — a TAB counts as one. */
103
+ indent: number;
104
+ /** The line without its indent: the address the author meant to write. */
105
+ text: string;
106
+ }
107
+ /** What a `## Loaded skills` section that produced no load site actually holds. */
108
+ export interface EmptyLoaderShape {
109
+ /** The indented address-shaped lines, in document order. */
110
+ indented: IndentedLoadSite[];
111
+ /** Non-blank lines in the section body; `0` means the section is empty. */
112
+ bodyLines: number;
113
+ }
114
+ /**
115
+ * Measure a `## Loaded skills` section that yielded no load site, so a consumer
116
+ * can say WHICH of the shapes behind `empty_loader` it is looking at.
117
+ *
118
+ * GAIA-436: that code covers three documents — a list that is wholly indented, a
119
+ * section whose lines are not address-shaped at all (a `*` marker, a `-@` with no
120
+ * space), and an empty section. Only the first is the "your list is indented" the
121
+ * single message can describe; the other two get a remedy pointing at whitespace
122
+ * that is already fine, which is the same misdirection the anchoring fixed.
123
+ *
124
+ * It lives HERE, not in the CLI that renders it: where the section starts and ends
125
+ * and what counts as an address are rules of this module, and a second copy of
126
+ * them in the host is exactly the drift this ticket is about. The host measures
127
+ * (in `runValidate`) and renders (in `formatReport`); the rule stays in core.
128
+ *
129
+ * Answers for any document — a section it cannot find simply holds nothing — so a
130
+ * caller need not pre-check. It classifies, never throws: the defect is already
131
+ * raised by `expandLoad` before this is reached.
132
+ */
133
+ export declare function measureEmptyLoader(workflowMd: string): EmptyLoaderShape;
65
134
  /**
66
135
  * Read the per-skill value **overrides** from a `WORKFLOW.md`'s `## Loaded skills`
67
136
  * section. A project overrides a skill default **inline under that skill's bullet**
68
137
  * (indented `key: value` YAML, block scalars allowed), not in one global block.
69
- * Returns a map keyed by skill `name` (the `@gaia/` prefix stripped); a skill with no
70
- * overrides maps to `{}` (it runs on its declared defaults).
138
+ * Returns a map keyed by skill `name` (any `@<namespace>/` prefix stripped); a skill
139
+ * with no overrides maps to `{}` (it runs on its declared defaults).
71
140
  */
72
141
  export declare function parseStepValues(workflowMd: string): StepValues;
73
142
  /**
@@ -75,6 +144,47 @@ export declare function parseStepValues(workflowMd: string): StepValues;
75
144
  * present) and the declared `inputs`. Malformed frontmatter raises `StepContractError`.
76
145
  */
77
146
  export declare function readSkillWhen(skillMd: string): SkillContract;
147
+ /**
148
+ * The **claimable states** of each GAIA workflow — the only `step` values a ticket
149
+ * in that workflow can ever present to a step owner.
150
+ *
151
+ * Source of truth: `web/modules/custom/gaia_core/gaia_core.workflows.yml`. It is
152
+ * transcribed here rather than read, because this module is a pure library with no
153
+ * filesystem and no Drupal access. A workflow added there must be added here too.
154
+ */
155
+ export declare const WORKFLOW_CLAIMABLE_STATES: Record<string, readonly WorkflowStep[]>;
156
+ /**
157
+ * Derive the `(work_type, workflow, step)` set a project can produce, from the
158
+ * skills it loads plus the fixed workflow/claimable-state table above — the input
159
+ * `validateLoad` needs, which until GAIA-436 only the contract test could build
160
+ * (from a hardcoded list) and therefore no shipped command could.
161
+ *
162
+ * A **work type** exists for the project because some loaded **owner** declares it;
163
+ * its participating steps are the union of `when.step` across every owner declaring
164
+ * it. Those steps are then crossed against **all five** workflows — not merely the
165
+ * ones the declaring skill happens to list — because that is what makes a coverage
166
+ * gap real rather than tautological: a project that can route `work:code` at
167
+ * `coding` must be able to route it at `coding` in every workflow that claims there.
168
+ *
169
+ * A step no work type participates in carries a `null` work type (qualification,
170
+ * the deployment and verification steps). An owner whose `when.work_type` is `*` or
171
+ * omitted is a non-work-typed owner: it contributes to the `null` side, never to a
172
+ * named work type. Helpers (no `when`) contribute nothing — they own no step.
173
+ *
174
+ * **Known blind spot: the work-type universe is derived from the load, not from the
175
+ * project.** Because a work type exists here only if some loaded owner declares it,
176
+ * deleting the last owner of a work type does not open a coverage gap — it shrinks
177
+ * the triple set, and the validation stays green. Removing `- @gaia/docs-authoring`
178
+ * from this repository's own `WORKFLOW.md` yields `9 → 8` owners and `24 → 16`
179
+ * triples, all covered, although every `work:docs` ticket would then reach `coding`
180
+ * with no owner at all. The set of work types a project's tickets can actually
181
+ * carry lives in the ticket store's `work:*` vocabulary, which this function cannot
182
+ * see; closing the gap means feeding that vocabulary in, which is its own change.
183
+ * What is validated here is therefore: *given what the project loads, is the load
184
+ * internally complete and unambiguous* — not *does the project load everything its
185
+ * tickets need*.
186
+ */
187
+ export declare function deriveProjectTriples(skills: SkillContract[]): Triple[];
78
188
  /** What a load expands to: the flat leaf skills plus the values that reached them. */
79
189
  export interface ExpandedLoad {
80
190
  /** The leaf skills (step-owners + helpers), deduplicated, in first-seen order. */
@@ -114,6 +224,10 @@ export declare function expandLoad(workflowMd: string, byName: Map<string, Skill
114
224
  * resolve to a value: the `values` map (as resolved by `expandLoad`) or the input's
115
225
  * own declared `default`.
116
226
  *
227
+ * Finally (GAIA-436) every override the load resolved must reach a declared input:
228
+ * an override key no loaded skill declares is a defect, because it resolves to
229
+ * nothing at all and the skill silently runs on its default instead.
230
+ *
117
231
  * Reports the first defect as a typed `StepContractError`. Never dispatches or routes.
118
232
  */
119
233
  export declare function validateLoad(skills: SkillContract[], values: StepValues, projectTriples: Triple[]): void;
@@ -30,15 +30,62 @@ export const WORKFLOW_STEPS = [
30
30
  'verifying',
31
31
  'summary',
32
32
  ];
33
- /** Typed defect raised on the first contract violation. */
33
+ /**
34
+ * Typed defect raised on the first contract violation.
35
+ *
36
+ * The single-argument form `new StepContractError(message)` is the norm and stays
37
+ * the norm; `details` is opt-in for the few defects a consumer classifies.
38
+ */
34
39
  export class StepContractError extends Error {
35
- constructor(message) {
40
+ /** The defect's classification, when it is one a consumer branches on. */
41
+ code;
42
+ /**
43
+ * For `unresolved_skill`: the skill name that could not be resolved — the root
44
+ * bullet's, or the bundle member's. Named `skill` rather than `name` because
45
+ * `Error.name` is already taken (and set to `StepContractError` below).
46
+ */
47
+ skill;
48
+ constructor(message, details) {
36
49
  super(message);
37
50
  this.name = 'StepContractError';
51
+ // `exactOptionalPropertyTypes` is on: assign an optional property only when
52
+ // the value is actually defined, never `undefined`.
53
+ if (details !== undefined)
54
+ this.code = details.code;
55
+ if (details?.skill !== undefined)
56
+ this.skill = details.skill;
38
57
  }
39
58
  }
40
59
  const LOADED_SKILLS_HEADING = /^##\s+Loaded skills\s*$/;
41
- const BULLET = /^\s*-\s+@gaia\/([a-z0-9-]+)\s*$/;
60
+ /**
61
+ * One loaded-skill bullet: `- @<namespace>/<skill-name>`, **at indent 0**.
62
+ *
63
+ * GAIA-436: the namespace is captured only to be discarded. A skill is addressed
64
+ * by NAME — the prefix says which plugin ships it, and a project may load a step
65
+ * owner from any plugin (`@designbook-gaia/debo-config-sync`), not just `@gaia/`.
66
+ * Hardcoding `@gaia/` here made every foreign bullet unmatched, and an unmatched
67
+ * bullet at indent 0 fell through to the "dedented prose" branch below — dropping
68
+ * its own overrides *and* flushing away the preceding bullet's, in silence.
69
+ *
70
+ * The indent-0 anchor is the second half of the same defect. A load site is a
71
+ * top-level list item — in `WORKFLOW.md` and in a bundle body alike — while an
72
+ * indented `- @…` line is *value*: `.prompt` block scalars are written as lists
73
+ * of required skills. Matching at any indent read such a line as an address,
74
+ * which truncated the block scalar mid-value and invented a phantom skill from
75
+ * the line, and `validateLoad` then reported that corrupted load as clean.
76
+ */
77
+ const BULLET = /^-\s+@([a-z0-9-]+)\/([a-z0-9-]+)\s*$/;
78
+ /**
79
+ * A line merely *shaped* like a bullet — a top-level list item whose content
80
+ * starts `@`. Inside `## Loaded skills` such a line is an address, never prose,
81
+ * so one that `BULLET` cannot parse is REPORTED (GAIA-436) instead of taking the
82
+ * prose branch. That report is what stops a malformed address from damaging its
83
+ * neighbour. It carries `BULLET`'s indent-0 anchor for the same reason: without
84
+ * it, a near-miss inside a block scalar — `- @gaia/read-ticket before anything
85
+ * else.` — failed a document that was correct, and the remedy the message
86
+ * prescribes steered the author onto the exact form, which is the silent case.
87
+ */
88
+ const BULLET_SHAPED = /^-\s+@/;
42
89
  /**
43
90
  * Locate the `## Loaded skills` section in a markdown document: the `[start, end)`
44
91
  * line range of its body, or `null` when the document carries no such section.
@@ -60,12 +107,54 @@ function loadedSkillsRange(lines) {
60
107
  }
61
108
  return [start, end];
62
109
  }
110
+ /**
111
+ * Measure a `## Loaded skills` section that yielded no load site, so a consumer
112
+ * can say WHICH of the shapes behind `empty_loader` it is looking at.
113
+ *
114
+ * GAIA-436: that code covers three documents — a list that is wholly indented, a
115
+ * section whose lines are not address-shaped at all (a `*` marker, a `-@` with no
116
+ * space), and an empty section. Only the first is the "your list is indented" the
117
+ * single message can describe; the other two get a remedy pointing at whitespace
118
+ * that is already fine, which is the same misdirection the anchoring fixed.
119
+ *
120
+ * It lives HERE, not in the CLI that renders it: where the section starts and ends
121
+ * and what counts as an address are rules of this module, and a second copy of
122
+ * them in the host is exactly the drift this ticket is about. The host measures
123
+ * (in `runValidate`) and renders (in `formatReport`); the rule stays in core.
124
+ *
125
+ * Answers for any document — a section it cannot find simply holds nothing — so a
126
+ * caller need not pre-check. It classifies, never throws: the defect is already
127
+ * raised by `expandLoad` before this is reached.
128
+ */
129
+ export function measureEmptyLoader(workflowMd) {
130
+ const lines = workflowMd.split('\n');
131
+ const range = loadedSkillsRange(lines);
132
+ if (range === null)
133
+ return { indented: [], bodyLines: 0 };
134
+ const [start, end] = range;
135
+ const indented = [];
136
+ let bodyLines = 0;
137
+ for (let i = start + 1; i < end; i++) {
138
+ const raw = lines[i] ?? '';
139
+ if (raw.trim() === '')
140
+ continue;
141
+ bodyLines++;
142
+ const indent = raw.length - raw.trimStart().length;
143
+ // Asks `BULLET_SHAPED` about the DE-indented line rather than restating its
144
+ // body with a leading `\s+`: "address-shaped" keeps exactly one definition,
145
+ // so this measurement cannot drift from the anchor it is explaining.
146
+ if (indent > 0 && BULLET_SHAPED.test(raw.slice(indent))) {
147
+ indented.push({ line: i + 1, indent, text: raw.trim() });
148
+ }
149
+ }
150
+ return { indented, bodyLines };
151
+ }
63
152
  /**
64
153
  * Read the per-skill value **overrides** from a `WORKFLOW.md`'s `## Loaded skills`
65
154
  * section. A project overrides a skill default **inline under that skill's bullet**
66
155
  * (indented `key: value` YAML, block scalars allowed), not in one global block.
67
- * Returns a map keyed by skill `name` (the `@gaia/` prefix stripped); a skill with no
68
- * overrides maps to `{}` (it runs on its declared defaults).
156
+ * Returns a map keyed by skill `name` (any `@<namespace>/` prefix stripped); a skill
157
+ * with no overrides maps to `{}` (it runs on its declared defaults).
69
158
  */
70
159
  export function parseStepValues(workflowMd) {
71
160
  return parseLoadedSection(workflowMd).values;
@@ -126,11 +215,16 @@ function parseLoadedSection(workflowMd) {
126
215
  const bullet = BULLET.exec(raw);
127
216
  if (bullet) {
128
217
  flush();
129
- current = bullet[1] ?? null;
218
+ current = bullet[2] ?? null; // group 2 = the name; group 1 = the namespace
130
219
  if (current !== null)
131
220
  names.push(current);
132
221
  continue;
133
222
  }
223
+ if (BULLET_SHAPED.test(raw)) {
224
+ // GAIA-436: a bullet the loader cannot read is a defect in the loader's
225
+ // input, not prose. Reporting it here is what keeps the neighbour intact.
226
+ throw new StepContractError(`\`## Loaded skills\` carries a bullet this loader cannot read: \`${raw.trim()}\` — write \`- @<namespace>/<skill-name>\`, lowercase with digits and \`-\` only.`);
227
+ }
134
228
  if (current === null)
135
229
  continue; // prose before the first bullet
136
230
  if (raw.trim() === '') {
@@ -320,6 +414,108 @@ function matchesTriple(when, triple) {
320
414
  function tripleLabel(triple) {
321
415
  return `(${triple.work_type ?? '—'}, ${triple.workflow}, ${triple.step})`;
322
416
  }
417
+ /**
418
+ * The **claimable states** of each GAIA workflow — the only `step` values a ticket
419
+ * in that workflow can ever present to a step owner.
420
+ *
421
+ * Source of truth: `web/modules/custom/gaia_core/gaia_core.workflows.yml`. It is
422
+ * transcribed here rather than read, because this module is a pure library with no
423
+ * filesystem and no Drupal access. A workflow added there must be added here too.
424
+ */
425
+ export const WORKFLOW_CLAIMABLE_STATES = {
426
+ gaia_feature: ['qualification', 'spec', 'coding', 'review'],
427
+ gaia_bug: ['qualification', 'diagnose', 'coding', 'review'],
428
+ gaia_chore: ['qualification', 'spec', 'coding', 'review'],
429
+ gaia_deployment: ['pre_deployment', 'post_deployment'],
430
+ gaia_verification: ['verifying', 'summary'],
431
+ };
432
+ /**
433
+ * Derive the `(work_type, workflow, step)` set a project can produce, from the
434
+ * skills it loads plus the fixed workflow/claimable-state table above — the input
435
+ * `validateLoad` needs, which until GAIA-436 only the contract test could build
436
+ * (from a hardcoded list) and therefore no shipped command could.
437
+ *
438
+ * A **work type** exists for the project because some loaded **owner** declares it;
439
+ * its participating steps are the union of `when.step` across every owner declaring
440
+ * it. Those steps are then crossed against **all five** workflows — not merely the
441
+ * ones the declaring skill happens to list — because that is what makes a coverage
442
+ * gap real rather than tautological: a project that can route `work:code` at
443
+ * `coding` must be able to route it at `coding` in every workflow that claims there.
444
+ *
445
+ * A step no work type participates in carries a `null` work type (qualification,
446
+ * the deployment and verification steps). An owner whose `when.work_type` is `*` or
447
+ * omitted is a non-work-typed owner: it contributes to the `null` side, never to a
448
+ * named work type. Helpers (no `when`) contribute nothing — they own no step.
449
+ *
450
+ * **Known blind spot: the work-type universe is derived from the load, not from the
451
+ * project.** Because a work type exists here only if some loaded owner declares it,
452
+ * deleting the last owner of a work type does not open a coverage gap — it shrinks
453
+ * the triple set, and the validation stays green. Removing `- @gaia/docs-authoring`
454
+ * from this repository's own `WORKFLOW.md` yields `9 → 8` owners and `24 → 16`
455
+ * triples, all covered, although every `work:docs` ticket would then reach `coding`
456
+ * with no owner at all. The set of work types a project's tickets can actually
457
+ * carry lives in the ticket store's `work:*` vocabulary, which this function cannot
458
+ * see; closing the gap means feeding that vocabulary in, which is its own change.
459
+ * What is validated here is therefore: *given what the project loads, is the load
460
+ * internally complete and unambiguous* — not *does the project load everything its
461
+ * tickets need*.
462
+ */
463
+ export function deriveProjectTriples(skills) {
464
+ const stepsByWorkType = new Map();
465
+ for (const skill of skills) {
466
+ const when = skill.when;
467
+ if (when === undefined)
468
+ continue; // helper: owns no step
469
+ const workTypes = when.work_type;
470
+ if (workTypes === undefined || workTypes === '*')
471
+ continue; // non-work-typed
472
+ const steps = when.step === undefined || when.step === '*'
473
+ ? WORKFLOW_STEPS
474
+ : when.step;
475
+ for (const workType of workTypes) {
476
+ let set = stepsByWorkType.get(workType);
477
+ if (set === undefined) {
478
+ set = new Set();
479
+ stepsByWorkType.set(workType, set);
480
+ }
481
+ for (const step of steps)
482
+ set.add(step);
483
+ }
484
+ }
485
+ const workTypedSteps = new Set();
486
+ for (const set of stepsByWorkType.values())
487
+ for (const step of set)
488
+ workTypedSteps.add(step);
489
+ const triples = [];
490
+ const seen = new Set();
491
+ const push = (triple) => {
492
+ // JSON rather than a joined string: it keeps a `null` work type distinct from
493
+ // any string one without inventing a sentinel character that a work type could
494
+ // itself contain.
495
+ const key = JSON.stringify([
496
+ triple.work_type,
497
+ triple.workflow,
498
+ triple.step,
499
+ ]);
500
+ if (seen.has(key))
501
+ return;
502
+ seen.add(key);
503
+ triples.push(triple);
504
+ };
505
+ for (const [workflow, states] of Object.entries(WORKFLOW_CLAIMABLE_STATES)) {
506
+ for (const step of states) {
507
+ if (!workTypedSteps.has(step)) {
508
+ push({ work_type: null, workflow, step });
509
+ continue;
510
+ }
511
+ for (const [workType, steps] of stepsByWorkType) {
512
+ if (steps.has(step))
513
+ push({ work_type: workType, workflow, step });
514
+ }
515
+ }
516
+ }
517
+ return triples;
518
+ }
323
519
  /**
324
520
  * Read a `WORKFLOW.md` and expand what it loads into the flat, deduplicated list of
325
521
  * **leaf** skills (step-owners + helpers) the project effectively runs, together with
@@ -344,6 +540,19 @@ function tripleLabel(triple) {
344
540
  */
345
541
  export function expandLoad(workflowMd, byName) {
346
542
  const { names: rootNames, values: workflowValues } = parseLoadedSection(workflowMd);
543
+ // GAIA-436: a `## Loaded skills` section that yields no load site at all is its
544
+ // own defect and needs its own message. Expanding it into an empty skill set
545
+ // let the failure surface much later as `no skill matches triple (—,
546
+ // gaia_feature, qualification) — coverage gap`, which sends the author looking
547
+ // for a missing work-type skill when the real cause is almost always that their
548
+ // list is indented — the one shape the indent-0 anchor turns into nothing.
549
+ //
550
+ // Here rather than in `parseLoadedSection`, because a BUNDLE that lists nothing
551
+ // is reported by `readSkillWhen` with the bundle's own name in the message; a
552
+ // check in the parser would displace that more specific report.
553
+ if (rootNames.length === 0) {
554
+ throw new StepContractError('the `## Loaded skills` section carries no load site: write each skill as `- @<namespace>/<skill-name>` at indent 0. An indented `- @…` line is read as value, never as an address.', { code: 'empty_loader' });
555
+ }
347
556
  const skills = [];
348
557
  const emitted = new Set();
349
558
  const values = {};
@@ -362,7 +571,15 @@ export function expandLoad(workflowMd, byName) {
362
571
  const via = stack.length > 0
363
572
  ? `bundle \`${stack[stack.length - 1]}\` loads unknown skill \`${name}\``
364
573
  : `loaded skill \`${name}\` not found`;
365
- throw new StepContractError(`${via}.`);
574
+ // GAIA-436: both halves are the same class of defect — a name that resolves
575
+ // to nothing — and a consumer's remedy for them is identical ("that skill is
576
+ // not vendored where the scan looked: pass `--skills`"). They are tagged
577
+ // alike, with the unresolved name as data, so the host stops recovering it
578
+ // from the message text and stops missing the bundle-member half entirely.
579
+ throw new StepContractError(`${via}.`, {
580
+ code: 'unresolved_skill',
581
+ skill: name,
582
+ });
366
583
  }
367
584
  if (skill.loads !== undefined) {
368
585
  if (stack.includes(name)) {
@@ -401,6 +618,10 @@ export function expandLoad(workflowMd, byName) {
401
618
  * resolve to a value: the `values` map (as resolved by `expandLoad`) or the input's
402
619
  * own declared `default`.
403
620
  *
621
+ * Finally (GAIA-436) every override the load resolved must reach a declared input:
622
+ * an override key no loaded skill declares is a defect, because it resolves to
623
+ * nothing at all and the skill silently runs on its default instead.
624
+ *
404
625
  * Reports the first defect as a typed `StepContractError`. Never dispatches or routes.
405
626
  */
406
627
  export function validateLoad(skills, values, projectTriples) {
@@ -427,4 +648,30 @@ export function validateLoad(skills, values, projectTriples) {
427
648
  }
428
649
  }
429
650
  }
651
+ // GAIA-436: the loop above reads overrides only through the DECLARED inputs of a
652
+ // matched owner (`overrides?.[input.key]`), so it never looks at an override key
653
+ // the skill does not declare. Such a key resolves to nothing — the skill runs on
654
+ // its default and the project's stated intent evaporates without a word. That is
655
+ // how a key rename silently degraded eleven overrides on a consumer project for
656
+ // six weeks. Sweep the loaded skills for it, and name both skill and key.
657
+ //
658
+ // Deliberately AFTER the triple loop, so a coverage gap or a collision is still
659
+ // reported first with its own message; and over the `skills` LIST rather than the
660
+ // `values` map, because `values` may legitimately carry entries for skills this
661
+ // load does not contain. Owners and helpers alike: a helper is loaded precisely
662
+ // for its inputs, so a dead override key on one is the same defect.
663
+ for (const skill of skills) {
664
+ const overrides = values[skill.name];
665
+ if (overrides === undefined)
666
+ continue;
667
+ const declared = new Set(skill.inputs.map((i) => i.key));
668
+ for (const key of Object.keys(overrides)) {
669
+ if (declared.has(key))
670
+ continue;
671
+ const known = declared.size === 0
672
+ ? 'the skill declares no inputs at all'
673
+ : `the skill declares \`${[...declared].join('`, `')}\``;
674
+ throw new StepContractError(`skill \`${skill.name}\` has no input \`${key}\`: the override resolves to nothing and the skill silently runs on its default — ${known}. Rename the override to a declared key rather than deleting it.`);
675
+ }
676
+ }
430
677
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/core",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "description": "GAIA surface-agnostic kernel: host contract, split-config helpers, addon preset/discovery machinery, shared primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,10 +27,10 @@
27
27
  "directory": "gaia-cli/core"
28
28
  },
29
29
  "dependencies": {
30
- "@dropsh/plugin-markdown": "^0.5.8",
31
- "@dropsh/plugin-oauth2": "^0.5.7",
30
+ "@dropsh/plugin-markdown": "^0.6.1",
31
+ "@dropsh/plugin-oauth2": "^0.6.1",
32
32
  "commander": "^12.1.0",
33
- "dropsh": "^0.5.8",
33
+ "dropsh": "^0.6.1",
34
34
  "pino": "^9.6.0",
35
35
  "pino-pretty": "^13.0.0",
36
36
  "semver": "^7.6.0",