@gaia-ai/core 0.10.0 → 0.11.1
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/dist/src/cli/load-gaia-config.d.ts +31 -0
- package/dist/src/cli/load-gaia-config.js +51 -7
- package/dist/src/cli/machine-context.d.ts +5 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/plugins/discover-addons.d.ts +1 -0
- package/dist/src/plugins/discover-addons.js +21 -1
- package/dist/src/workflow/step-contract.d.ts +119 -5
- package/dist/src/workflow/step-contract.js +254 -7
- package/package.json +4 -4
- package/dist/src/conductor-registry/index.d.ts +0 -23
- package/dist/src/conductor-registry/index.js +0 -59
|
@@ -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. */
|
|
@@ -27,6 +46,18 @@ export interface GaiaConnectionConfig {
|
|
|
27
46
|
*/
|
|
28
47
|
ui_entries: string[];
|
|
29
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;
|
|
60
|
+
}
|
|
30
61
|
/**
|
|
31
62
|
* Load the connection config for `gaia ui` / `gaia dropsh` / conductor-auth.
|
|
32
63
|
* Resolves the path (explicit → project walk-up → home → shipped fallback),
|
|
@@ -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
|
|
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,6 +154,7 @@ 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,
|
|
@@ -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;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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);
|
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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` (
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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` (
|
|
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[
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.11.1",
|
|
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.
|
|
31
|
-
"@dropsh/plugin-oauth2": "^0.
|
|
30
|
+
"@dropsh/plugin-markdown": "^0.6.1",
|
|
31
|
+
"@dropsh/plugin-oauth2": "^0.6.1",
|
|
32
32
|
"commander": "^12.1.0",
|
|
33
|
-
"dropsh": "^0.
|
|
33
|
+
"dropsh": "^0.6.1",
|
|
34
34
|
"pino": "^9.6.0",
|
|
35
35
|
"pino-pretty": "^13.0.0",
|
|
36
36
|
"semver": "^7.6.0",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One registered conductor in the local machine registry
|
|
3
|
-
* (`${GAIA_HOME ?? ~}/.gaia/conductors.json`). The registry is inherently
|
|
4
|
-
* machine + user scoped: it lists the conductors this user can drive locally.
|
|
5
|
-
*/
|
|
6
|
-
export interface ConductorRegistryEntry {
|
|
7
|
-
id: string;
|
|
8
|
-
path: string;
|
|
9
|
-
project: string;
|
|
10
|
-
label: string;
|
|
11
|
-
host: 'herdr' | 'process' | 'systemd';
|
|
12
|
-
handle: string;
|
|
13
|
-
}
|
|
14
|
-
/** `${GAIA_HOME ?? ~}/.gaia/conductors.json`. */
|
|
15
|
-
export declare function conductorRegistryPath(): string;
|
|
16
|
-
/** All registered conductors, in insertion order. */
|
|
17
|
-
export declare function listRegisteredConductors(): Promise<ConductorRegistryEntry[]>;
|
|
18
|
-
/** One registered conductor by id, or `null` when absent. */
|
|
19
|
-
export declare function getRegisteredConductor(id: string): Promise<ConductorRegistryEntry | null>;
|
|
20
|
-
/** Insert or overwrite a conductor by id (idempotent). */
|
|
21
|
-
export declare function registerConductor(entry: ConductorRegistryEntry): Promise<void>;
|
|
22
|
-
/** Remove a conductor by id; a no-op when it is not present. */
|
|
23
|
-
export declare function removeConductor(id: string): Promise<void>;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
/** `${GAIA_HOME ?? ~}/.gaia/conductors.json`. */
|
|
5
|
-
export function conductorRegistryPath() {
|
|
6
|
-
const home = process.env.GAIA_HOME ?? homedir();
|
|
7
|
-
return join(home, '.gaia', 'conductors.json');
|
|
8
|
-
}
|
|
9
|
-
async function read() {
|
|
10
|
-
let raw;
|
|
11
|
-
try {
|
|
12
|
-
raw = await readFile(conductorRegistryPath(), 'utf8');
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return {};
|
|
16
|
-
}
|
|
17
|
-
if (raw.trim() === '') {
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(raw);
|
|
22
|
-
if (typeof parsed === 'object' &&
|
|
23
|
-
parsed !== null &&
|
|
24
|
-
!Array.isArray(parsed)) {
|
|
25
|
-
return parsed;
|
|
26
|
-
}
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async function write(data) {
|
|
34
|
-
const path = conductorRegistryPath();
|
|
35
|
-
await mkdir(dirname(path), { recursive: true });
|
|
36
|
-
await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
37
|
-
}
|
|
38
|
-
/** All registered conductors, in insertion order. */
|
|
39
|
-
export async function listRegisteredConductors() {
|
|
40
|
-
return Object.values(await read());
|
|
41
|
-
}
|
|
42
|
-
/** One registered conductor by id, or `null` when absent. */
|
|
43
|
-
export async function getRegisteredConductor(id) {
|
|
44
|
-
return (await read())[id] ?? null;
|
|
45
|
-
}
|
|
46
|
-
/** Insert or overwrite a conductor by id (idempotent). */
|
|
47
|
-
export async function registerConductor(entry) {
|
|
48
|
-
const data = await read();
|
|
49
|
-
data[entry.id] = entry;
|
|
50
|
-
await write(data);
|
|
51
|
-
}
|
|
52
|
-
/** Remove a conductor by id; a no-op when it is not present. */
|
|
53
|
-
export async function removeConductor(id) {
|
|
54
|
-
const data = await read();
|
|
55
|
-
if (data[id]) {
|
|
56
|
-
delete data[id];
|
|
57
|
-
await write(data);
|
|
58
|
-
}
|
|
59
|
-
}
|