@gaia-ai/conductor 0.6.0 → 0.6.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/config-schema.d.ts +42 -7
  3. package/dist/src/cli/config-schema.js +64 -9
  4. package/dist/src/cli/init.js +16 -10
  5. package/dist/src/cli/migrate-addon-names.d.ts +72 -0
  6. package/dist/src/cli/migrate-addon-names.js +318 -0
  7. package/dist/src/cli/upgrade.d.ts +15 -0
  8. package/dist/src/cli/upgrade.js +87 -8
  9. package/dist/src/commands/conductor.d.ts +27 -3
  10. package/dist/src/commands/conductor.js +39 -52
  11. package/dist/src/config.d.ts +15 -1
  12. package/dist/src/config.js +280 -3
  13. package/dist/src/contract.d.ts +8 -0
  14. package/dist/src/contract.js +16 -0
  15. package/dist/src/core/conductor.d.ts +16 -1
  16. package/dist/src/core/conductor.js +23 -1
  17. package/dist/src/index.d.ts +5 -4
  18. package/dist/src/index.js +17 -3
  19. package/dist/src/plugins/agent.d.ts +61 -0
  20. package/dist/src/plugins/agent.js +11 -0
  21. package/dist/src/plugins/executor.d.ts +104 -0
  22. package/dist/src/plugins/executor.js +1 -0
  23. package/dist/src/plugins/plugins.d.ts +60 -0
  24. package/dist/src/plugins/plugins.js +42 -0
  25. package/dist/src/plugins/preset.d.ts +48 -0
  26. package/dist/src/plugins/preset.js +23 -0
  27. package/dist/src/plugins/remote.d.ts +203 -0
  28. package/dist/src/plugins/remote.js +1 -0
  29. package/dist/src/plugins/workspace.d.ts +35 -0
  30. package/dist/src/plugins/workspace.js +1 -0
  31. package/dist/src/preset.d.ts +2 -2
  32. package/dist/src/types.d.ts +65 -0
  33. package/dist/src/types.js +1 -0
  34. package/package.json +4 -3
  35. package/dist/src/cli/conductor-registry.d.ts +0 -7
  36. package/dist/src/cli/conductor-registry.js +0 -6
  37. package/dist/src/cli/deployment.d.ts +0 -34
  38. package/dist/src/cli/deployment.js +0 -63
package/README.md CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.
4
4
 
5
- Part of the GAIA conductor. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all plugins. Source: https://git.key-tec.de/keytec/gaia (conductor/).
5
+ Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
@@ -18,12 +18,36 @@ export interface ConnectionConfigMigration {
18
18
  apply(currentText: string): string;
19
19
  }
20
20
  /**
21
- * The ordered, contiguous migration chain. Ships EMPTY at v1: nothing below v1
22
- * is a versioned config (v0 = seed / hand-authored / untouched). The FIRST real
23
- * shape change adds the first `{ from: 1, to: 2, … }` method here, which bumps
24
- * the derived CURRENT to 2 automatically. The extension point is real and
25
- * exercised by tests now, so the first shape change is a one-method addition,
26
- * never a re-plumb.
21
+ * GAIA-226: the package name whose `plugins[]` entry contributes the `md`
22
+ * renderer (plus the authoritative JSON:API schema) to a connection config. The
23
+ * AGGREGATOR is named, not `@dropsh/plugin-markdown` directly: a direct
24
+ * `@dropsh/*` descriptor is unresolvable from `loadGaiaConfig`'s bases
25
+ * (config dir / host module / cwd), whereas `@gaia-ai/addon-essentials` is a
26
+ * real dependency of both the host and the engine and therefore resolves in the
27
+ * monorepo AND from a global `@gaia-ai/gaia` install. No `export:` is needed —
28
+ * the package default-exports its factory.
29
+ */
30
+ export declare const RENDERER_PLUGIN_PACKAGE = "@gaia-ai/addon-essentials";
31
+ /** The exact `plugins[]` element text the v1→v2 migration and the scaffold emit. */
32
+ export declare const RENDERER_PLUGIN_ENTRY = "{ plugin: '@gaia-ai/addon-essentials' },";
33
+ /**
34
+ * GAIA-226 (v1 → v2): insert the renderer aggregator as the FIRST `plugins[]`
35
+ * element, so `gaia dropsh --format md` works instead of failing with
36
+ * `E_CONFIG: Unknown format 'md'. Available: json`.
37
+ *
38
+ * A pure TEXT transform — the upgrade runner must never execute a possibly
39
+ * side-effecting config module. Idempotent: text that already names the package
40
+ * in CODE is returned unchanged (whole-line `//` comments are stripped for that
41
+ * probe, so the template's own explanatory header does not read as a hit), and a
42
+ * config with no `plugins: [` array literal (an `addons: []`-shaped or
43
+ * hand-reshaped file) is left alone rather than guessed at.
44
+ */
45
+ export declare function addRendererPluginEntry(text: string): string;
46
+ /**
47
+ * The ordered, contiguous migration chain. v1 was the pre-GAIA-226 shape (auth
48
+ * plugins only); v2 adds the markdown renderer entry. Nothing below v1 is a
49
+ * versioned config (v0 = seed / hand-authored / untouched). `GAIA_CONFIG_SCHEMA_VERSION`
50
+ * derives from the chain's last `to` — never hardcode it.
27
51
  */
28
52
  export declare const CONNECTION_MIGRATIONS: ConnectionConfigMigration[];
29
53
  /** The current connection-config schema version — single source of truth. */
@@ -40,6 +64,15 @@ export declare function readConfigSchemaVersion(path: string): number;
40
64
  export declare function stripVersionHeader(text: string): string;
41
65
  /** Prepend (replacing any existing) a `// @gaia-schema-version <N>` header. */
42
66
  export declare function stampVersion(text: string, version: number): string;
67
+ /**
68
+ * Re-stamp the in-body `schema_version: <N>` field, which the generated config
69
+ * carries alongside the header marker for programmatic reads. The header is the
70
+ * upgrade decision's source of truth and is stamped by the runner; this keeps the
71
+ * FIELD from going stale when a migration bumps the version (GAIA-226 — the first
72
+ * migration is where the two could first disagree). No-op when the field is
73
+ * absent (a hand-authored config need not carry it).
74
+ */
75
+ export declare function stampSchemaVersionField(text: string, version: number): string;
43
76
  /**
44
77
  * Validate a migration chain is contiguous: each `to === from + 1`, the first
45
78
  * migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
@@ -49,6 +82,8 @@ export declare function assertContiguous(chain: ConnectionConfigMigration[], cur
49
82
  /**
50
83
  * Walk the migration chain from `from` up to `to`, applying each method's
51
84
  * `apply` in sequence. Input/output text is WITHOUT the version header — the
52
- * caller stamps CURRENT after. `chain` is injectable for tests.
85
+ * caller stamps CURRENT after. After each step the in-body `schema_version:`
86
+ * field is re-stamped to that step's `to`, so no individual method has to know
87
+ * (or hardcode) its own number twice. `chain` is injectable for tests.
53
88
  */
54
89
  export declare function applyMigrations(text: string, from: number, to: number, chain?: ConnectionConfigMigration[]): string;
@@ -11,15 +11,57 @@ import { readFileSync } from 'node:fs';
11
11
  const MARKER_RE = /@gaia-schema-version\s+(\d+)/;
12
12
  const HEADER_LINE_RE = /^\/\/ @gaia-schema-version \d+\n/;
13
13
  /**
14
- * The ordered, contiguous migration chain. Ships EMPTY at v1: nothing below v1
15
- * is a versioned config (v0 = seed / hand-authored / untouched). The FIRST real
16
- * shape change adds the first `{ from: 1, to: 2, … }` method here, which bumps
17
- * the derived CURRENT to 2 automatically. The extension point is real and
18
- * exercised by tests now, so the first shape change is a one-method addition,
19
- * never a re-plumb.
14
+ * GAIA-226: the package name whose `plugins[]` entry contributes the `md`
15
+ * renderer (plus the authoritative JSON:API schema) to a connection config. The
16
+ * AGGREGATOR is named, not `@dropsh/plugin-markdown` directly: a direct
17
+ * `@dropsh/*` descriptor is unresolvable from `loadGaiaConfig`'s bases
18
+ * (config dir / host module / cwd), whereas `@gaia-ai/addon-essentials` is a
19
+ * real dependency of both the host and the engine and therefore resolves in the
20
+ * monorepo AND from a global `@gaia-ai/gaia` install. No `export:` is needed —
21
+ * the package default-exports its factory.
22
+ */
23
+ export const RENDERER_PLUGIN_PACKAGE = '@gaia-ai/addon-essentials';
24
+ /** The exact `plugins[]` element text the v1→v2 migration and the scaffold emit. */
25
+ export const RENDERER_PLUGIN_ENTRY = `{ plugin: '${RENDERER_PLUGIN_PACKAGE}' },`;
26
+ /** `plugins: [` — captures its own indentation so the inserted element aligns. */
27
+ const PLUGINS_ARRAY_OPEN_RE = /^([ \t]*)plugins:\s*\[[ \t]*$/m;
28
+ /** Whole-line `//` comments — stripped before the presence probe (see below). */
29
+ const LINE_COMMENT_RE = /^[ \t]*\/\/.*$/gm;
30
+ /**
31
+ * GAIA-226 (v1 → v2): insert the renderer aggregator as the FIRST `plugins[]`
32
+ * element, so `gaia dropsh --format md` works instead of failing with
33
+ * `E_CONFIG: Unknown format 'md'. Available: json`.
34
+ *
35
+ * A pure TEXT transform — the upgrade runner must never execute a possibly
36
+ * side-effecting config module. Idempotent: text that already names the package
37
+ * in CODE is returned unchanged (whole-line `//` comments are stripped for that
38
+ * probe, so the template's own explanatory header does not read as a hit), and a
39
+ * config with no `plugins: [` array literal (an `addons: []`-shaped or
40
+ * hand-reshaped file) is left alone rather than guessed at.
41
+ */
42
+ export function addRendererPluginEntry(text) {
43
+ if (text.replace(LINE_COMMENT_RE, '').includes(RENDERER_PLUGIN_PACKAGE)) {
44
+ return text;
45
+ }
46
+ const m = PLUGINS_ARRAY_OPEN_RE.exec(text);
47
+ if (m === null)
48
+ return text;
49
+ const indent = `${m[1]} `;
50
+ return text.replace(PLUGINS_ARRAY_OPEN_RE, (line) => `${line}\n${indent}${RENDERER_PLUGIN_ENTRY}`);
51
+ }
52
+ /**
53
+ * The ordered, contiguous migration chain. v1 was the pre-GAIA-226 shape (auth
54
+ * plugins only); v2 adds the markdown renderer entry. Nothing below v1 is a
55
+ * versioned config (v0 = seed / hand-authored / untouched). `GAIA_CONFIG_SCHEMA_VERSION`
56
+ * derives from the chain's last `to` — never hardcode it.
20
57
  */
21
58
  export const CONNECTION_MIGRATIONS = [
22
- // { from: 1, to: 2, description: 'add site.jsonapi_prefix default', apply: (t) => … },
59
+ {
60
+ from: 1,
61
+ to: 2,
62
+ description: `add the ${RENDERER_PLUGIN_PACKAGE} renderer entry to plugins[]`,
63
+ apply: addRendererPluginEntry,
64
+ },
23
65
  ];
24
66
  /** The current connection-config schema version — single source of truth. */
25
67
  export const GAIA_CONFIG_SCHEMA_VERSION = CONNECTION_MIGRATIONS.at(-1)?.to ?? 1;
@@ -49,6 +91,17 @@ export function stripVersionHeader(text) {
49
91
  export function stampVersion(text, version) {
50
92
  return `// @gaia-schema-version ${version}\n${stripVersionHeader(text)}`;
51
93
  }
94
+ /**
95
+ * Re-stamp the in-body `schema_version: <N>` field, which the generated config
96
+ * carries alongside the header marker for programmatic reads. The header is the
97
+ * upgrade decision's source of truth and is stamped by the runner; this keeps the
98
+ * FIELD from going stale when a migration bumps the version (GAIA-226 — the first
99
+ * migration is where the two could first disagree). No-op when the field is
100
+ * absent (a hand-authored config need not carry it).
101
+ */
102
+ export function stampSchemaVersionField(text, version) {
103
+ return text.replace(/^([ \t]*)schema_version:\s*\d+\s*,/m, `$1schema_version: ${version},`);
104
+ }
52
105
  /**
53
106
  * Validate a migration chain is contiguous: each `to === from + 1`, the first
54
107
  * migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
@@ -74,7 +127,9 @@ export function assertContiguous(chain, current = GAIA_CONFIG_SCHEMA_VERSION) {
74
127
  /**
75
128
  * Walk the migration chain from `from` up to `to`, applying each method's
76
129
  * `apply` in sequence. Input/output text is WITHOUT the version header — the
77
- * caller stamps CURRENT after. `chain` is injectable for tests.
130
+ * caller stamps CURRENT after. After each step the in-body `schema_version:`
131
+ * field is re-stamped to that step's `to`, so no individual method has to know
132
+ * (or hardcode) its own number twice. `chain` is injectable for tests.
78
133
  */
79
134
  export function applyMigrations(text, from, to, chain = CONNECTION_MIGRATIONS) {
80
135
  let cur = from;
@@ -84,7 +139,7 @@ export function applyMigrations(text, from, to, chain = CONNECTION_MIGRATIONS) {
84
139
  if (step === undefined) {
85
140
  throw new Error(`no migration from v${cur} (chain incomplete)`);
86
141
  }
87
- out = step.apply(out);
142
+ out = stampSchemaVersionField(step.apply(out), step.to);
88
143
  cur = step.to;
89
144
  }
90
145
  return out;
@@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { machineContextPath, readMachineContext, } from '@gaia-ai/core';
5
- import { GAIA_CONFIG_SCHEMA_VERSION } from './config-schema.js';
5
+ import { GAIA_CONFIG_SCHEMA_VERSION, RENDERER_PLUGIN_ENTRY, } from './config-schema.js';
6
6
  export { machineContextPath, readMachineContext };
7
7
  /** JS single-quoted string literal for a trusted, simple value. */
8
8
  function q(value) {
@@ -29,10 +29,12 @@ export function renderCommittedConfig(inputs) {
29
29
  // IMPORT-FREE (GAIA-78): the plugin slots are \`{ plugin, with }\` descriptors
30
30
  // naming the REAL published package, not \`import\`ed constructors.
31
31
  // loadConductorConfig resolves each name ESLint-style (config dir → cwd →
32
- // conductor install). Each plugin package default-exports its factory (no
33
- // \`export:\`) EXCEPT the multi-export host barrel and the merged herdr workspace
34
- // (\`@gaia-ai/plugin-herdr\` default-exports the executor, so the workspace names
35
- // \`export: 'herdrWorkspace'\` — GAIA-139).
32
+ // conductor install). Every addon package default-exports its single factory, so
33
+ // no slot needs an \`export:\` EXCEPT the merged herdr workspace
34
+ // (\`@gaia-ai/addon-herdr\` default-exports the executor, so the workspace names
35
+ // \`export: 'herdrWorkspace'\` — GAIA-139). GAIA-224 replaced the old
36
+ // \`{ plugin: '@gaia-ai/gaia/plugins', export: 'drupalRemote' }\` host-barrel
37
+ // descriptor with the real addon package \`@gaia-ai/addon-remote-drupal\`.
36
38
 
37
39
  // The user-global machine context: identity + connection (incl. secret), shared
38
40
  // by every project on this machine. Never committed.
@@ -67,16 +69,16 @@ export default {
67
69
  // Lifecycle hooks are executor-owned (GAIA-84): they live at the config top
68
70
  // level — NOT on a plugin descriptor's \`with.hooks\`.
69
71
  hooks: { after_create: 'ddev init-worktree', after_done: 'ddev delete -Oy' },
70
- remote: { plugin: '@gaia-ai/gaia/plugins', export: 'drupalRemote' },
71
- executor: { plugin: '@gaia-ai/plugin-herdr' },
72
+ remote: { plugin: '@gaia-ai/addon-remote-drupal' },
73
+ executor: { plugin: '@gaia-ai/addon-herdr' },
72
74
  agent: {
73
- plugin: '@gaia-ai/plugin-claude',
75
+ plugin: '@gaia-ai/addon-claude',
74
76
  with: { model: local.model ?? 'claude-opus-4-8' },
75
77
  },
76
- // GAIA-139: the herdr workspace ships in @gaia-ai/plugin-herdr, which
78
+ // GAIA-139: the herdr workspace ships in @gaia-ai/addon-herdr, which
77
79
  // default-exports the EXECUTOR — so the workspace slot names export: 'herdrWorkspace'.
78
80
  workspace: {
79
- plugin: '@gaia-ai/plugin-herdr',
81
+ plugin: '@gaia-ai/addon-herdr',
80
82
  export: 'herdrWorkspace',
81
83
  },
82
84
  };
@@ -99,6 +101,9 @@ export function renderGaiaConfig() {
99
101
  //
100
102
  // plugins[] is consumed by DROPSH, whose resolver has no sole-function
101
103
  // auto-pick, so @dropsh/plugin-oauth2 entries MUST name export: 'oauth2Plugin'.
104
+ // The '@gaia-ai/addon-essentials' entry (GAIA-226) needs NO export: — that
105
+ // package default-exports its factory. It contributes the markdown renderer
106
+ // ('gaia dropsh --format md') plus the authoritative JSON:API schema.
102
107
  async function loadMachine() {
103
108
  try {
104
109
  return (await import(\`\${process.env.HOME}/.gaia/machine.config.js\`)).default ?? {};
@@ -123,6 +128,7 @@ export default {
123
128
  schema_version: ${GAIA_CONFIG_SCHEMA_VERSION},
124
129
  site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
125
130
  plugins: [
131
+ ${RENDERER_PLUGIN_ENTRY}
126
132
  {
127
133
  plugin: '@dropsh/plugin-oauth2',
128
134
  export: 'oauth2Plugin',
@@ -0,0 +1,72 @@
1
+ /** Suffix of the pre-rewrite backup a real (non-dry) rewrite leaves behind. */
2
+ export declare const ADDON_RENAME_BAK_SUFFIX = ".pre-addon-rename.bak";
3
+ /**
4
+ * The 14 addon packages renamed by GAIA-224 (spec v3 decision 14), including
5
+ * the GAIA-220 `pi` agent addon merged in from develop. A CLOSED
6
+ * set — the pass rewrites only these exact specifiers, so an unknown
7
+ * `@gaia-ai/plugin-something` is left alone rather than silently retargeted at a
8
+ * package that does not exist.
9
+ */
10
+ export declare const RENAMED_ADDONS: readonly ["auth-basic", "claude", "codex", "deployment", "dropsh", "essentials", "fake", "gaia-ui", "herdr", "kimi", "opencode", "pi", "remote-drupal", "workspace-git"];
11
+ /**
12
+ * Where each export of the DELETED `@gaia-ai/core/plugins` host barrel lives now.
13
+ *
14
+ * The key set is the barrel's own export list — `core/src/plugins/registry-exports.ts`
15
+ * as of the commit that deleted it — re-pointed at the addon that owns each
16
+ * factory today. `defaultExport` records whether the target package
17
+ * DEFAULT-exports exactly that factory; when it does, the descriptor's `export:`
18
+ * becomes redundant and is dropped.
19
+ *
20
+ * Deliberately absent, so they are left untouched:
21
+ * - `selectRemote`/`selectWorkspace`/`selectExecutor`/`selectAgent(s)` — moved
22
+ * to `@gaia-ai/conductor/contract`, but they are config-shape SELECTORS, not
23
+ * plugin factories, so no descriptor can legitimately name them;
24
+ * - any other/unknown export name — a config naming one is reported by the
25
+ * loader rather than silently retargeted at a package that may not have it.
26
+ */
27
+ export declare const CORE_PLUGINS_BARREL_EXPORTS: Readonly<Record<string, {
28
+ target: string;
29
+ defaultExport: boolean;
30
+ }>>;
31
+ /** Which surface a config file speaks — decides the ambiguous builtins mapping. */
32
+ export type ConfigSurface = 'connection' | 'engine';
33
+ /**
34
+ * Rewrite the legacy specifiers in one config's SOURCE TEXT — all three: the
35
+ * renamed `@gaia-ai/plugin-*` packages, the deleted `@gaia-ai/core/builtins`
36
+ * preset, and the deleted `@gaia-ai/core/plugins` barrel descriptors. Pure; line
37
+ * count is preserved (every replacement is line-local), which is what lets the
38
+ * reported diff be a plain index-aligned line diff.
39
+ */
40
+ export declare function migrateAddonNames(text: string, surface: ConfigSurface): {
41
+ text: string;
42
+ count: number;
43
+ };
44
+ /** The outcome of the rename pass over a single config file. */
45
+ export interface AddonRenameResult {
46
+ /** Report line, in the shape of the other upgrade steps. */
47
+ action: string;
48
+ /** `- old` / `+ new` lines for the rewritten specifiers. */
49
+ diff: string[];
50
+ /** Number of rewritten specifiers. */
51
+ count: number;
52
+ }
53
+ /**
54
+ * Rename pass over ONE config file. Returns `undefined` when the file is absent
55
+ * or already on the new names (idempotency: no write, no backup, no report
56
+ * line — the file stays byte-identical).
57
+ */
58
+ export declare function migrateAddonNamesInFile(path: string, surface: ConfigSurface, opts?: {
59
+ dryRun?: boolean;
60
+ }): AddonRenameResult | undefined;
61
+ /**
62
+ * Rename pass over every recognised config in the given `.gaia/` dirs (project
63
+ * + home). Emits report lines ONLY for files it actually rewrites, so an install
64
+ * already on `addon-*` adds no noise to the upgrade report.
65
+ */
66
+ export declare function runAddonRenameMigration(opts?: {
67
+ gaiaDirs: string[];
68
+ dryRun?: boolean;
69
+ }): {
70
+ actions: string[];
71
+ changed: boolean;
72
+ };
@@ -0,0 +1,318 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { listConductorConfigFiles } from '../config.js';
4
+ // GAIA-224 (Finding 8b, spec v3 decision 17): the `gaia upgrade` RENAME pass.
5
+ //
6
+ // The addon packages were renamed `@gaia-ai/plugin-<name>` → `@gaia-ai/addon-<name>`
7
+ // (decision 14) and the core barrels `@gaia-ai/core/{builtins,plugins}` were
8
+ // deleted (Finding 6). Both are npm-breaking for an existing install, whose
9
+ // `.gaia/` configs still name the old packages — so `gaia upgrade` rewrites them
10
+ // in the user's PROJECT `./.gaia/` and HOME `~/.gaia/` configs.
11
+ //
12
+ // All THREE deleted/renamed specifiers are covered. The `@gaia-ai/gaia/plugins`
13
+ // barrel is deliberately NOT covered: it still exists as a back-compat
14
+ // aggregator. `@gaia-ai/core/plugins` has no such reprieve — it is gone, and the
15
+ // pre-GAIA-224 project docs explicitly told configs to name `@gaia-ai/core/plugins`
16
+ // and NOT `@gaia-ai/gaia/plugins` (core resolved from both the dev monorepo and a
17
+ // global install; the meta package did not), so real installs DO name the deleted
18
+ // one. See `CORE_PLUGINS_BARREL_EXPORTS`.
19
+ //
20
+ // Covered per config dir:
21
+ // - the connection config `gaia.config.js` (its `plugins[]` / `addons[]`);
22
+ // - EVERY engine config the loader itself recognises — the default
23
+ // `conductor.config.js` AND each `<variant>.conductor.config.js`
24
+ // (GAIA-137 naming convention, enumerated via `listConductorConfigFiles` so
25
+ // the pass and the loader can never disagree). A legacy `conductor.config.js`
26
+ // still carrying `site`/`plugins` (the back-compat connection source) is
27
+ // therefore covered too, and its `plugins[]` entries are mapped on the
28
+ // CONNECTION surface — see `builtinsTargetFor`.
29
+ //
30
+ // Safety (decision 18 + the v3 risk register):
31
+ // - Replacement is anchored on a CLOSED set of 14 known package names inside
32
+ // a matched quote pair — never a broad `@gaia-ai/plugin-.*` sweep. A
33
+ // hand-written config's comments, formatting and unrelated code survive.
34
+ // The `@gaia-ai/core/plugins` rewrite is likewise closed-set: it is driven by
35
+ // the barrel's OWN export list, and an export name outside that list is left
36
+ // untouched rather than retargeted at a guess.
37
+ // - `*Plugin` interface identifiers (`RemotePlugin`, `herdrExecutorPlugin`, …),
38
+ // the `plugins:`/`addons:` config KEYS and the `@gaia-ai/gaia/plugins`
39
+ // subpath barrel are excluded BY CONSTRUCTION: they carry neither the
40
+ // `@gaia-ai/plugin-` scope prefix nor the `@gaia-ai/core/` path.
41
+ // - Idempotent: a config already on `addon-*` produces zero matches and is
42
+ // left BYTE-unchanged (no rewrite, no backup, no report line).
43
+ // - `--dry-run` prints the line diff and writes nothing.
44
+ // - A real rewrite backs the original up next to the file, like the
45
+ // schema-migration path does.
46
+ /** Suffix of the pre-rewrite backup a real (non-dry) rewrite leaves behind. */
47
+ export const ADDON_RENAME_BAK_SUFFIX = '.pre-addon-rename.bak';
48
+ const AUTH_BASIC = '@gaia-ai/addon-auth-basic';
49
+ const REMOTE_DRUPAL = '@gaia-ai/addon-remote-drupal';
50
+ const WORKSPACE_GIT = '@gaia-ai/addon-workspace-git';
51
+ const FAKE = '@gaia-ai/addon-fake';
52
+ /**
53
+ * The 14 addon packages renamed by GAIA-224 (spec v3 decision 14), including
54
+ * the GAIA-220 `pi` agent addon merged in from develop. A CLOSED
55
+ * set — the pass rewrites only these exact specifiers, so an unknown
56
+ * `@gaia-ai/plugin-something` is left alone rather than silently retargeted at a
57
+ * package that does not exist.
58
+ */
59
+ export const RENAMED_ADDONS = [
60
+ 'auth-basic',
61
+ 'claude',
62
+ 'codex',
63
+ 'deployment',
64
+ 'dropsh',
65
+ 'essentials',
66
+ 'fake',
67
+ 'gaia-ui',
68
+ 'herdr',
69
+ 'kimi',
70
+ 'opencode',
71
+ 'pi',
72
+ 'remote-drupal',
73
+ 'workspace-git',
74
+ ];
75
+ /**
76
+ * `'@gaia-ai/plugin-<known>'` (or `"…"` / `` `…` ``), with an optional subpath
77
+ * (`/preset`), inside a MATCHED quote pair. The quote pair + the closing anchor
78
+ * are what make this an exact-specifier match: `@gaia-ai/plugin-herdr-x` cannot
79
+ * match `herdr`, and a bare `RemotePlugin` identifier cannot match at all.
80
+ * Longest name first so a longer name always wins the alternation.
81
+ */
82
+ const QUOTED_LEGACY_ADDON = new RegExp(`(['"\`])@gaia-ai/plugin-(${[...RENAMED_ADDONS]
83
+ .sort((a, b) => b.length - a.length)
84
+ .join('|')})((?:/[^'"\`]*)?)\\1`, 'g');
85
+ /** `'@gaia-ai/core/builtins'` inside a matched quote pair (the deleted preset). */
86
+ const QUOTED_LEGACY_BUILTINS = /(['"`])@gaia-ai\/core\/builtins\1/g;
87
+ /**
88
+ * Where each export of the DELETED `@gaia-ai/core/plugins` host barrel lives now.
89
+ *
90
+ * The key set is the barrel's own export list — `core/src/plugins/registry-exports.ts`
91
+ * as of the commit that deleted it — re-pointed at the addon that owns each
92
+ * factory today. `defaultExport` records whether the target package
93
+ * DEFAULT-exports exactly that factory; when it does, the descriptor's `export:`
94
+ * becomes redundant and is dropped.
95
+ *
96
+ * Deliberately absent, so they are left untouched:
97
+ * - `selectRemote`/`selectWorkspace`/`selectExecutor`/`selectAgent(s)` — moved
98
+ * to `@gaia-ai/conductor/contract`, but they are config-shape SELECTORS, not
99
+ * plugin factories, so no descriptor can legitimately name them;
100
+ * - any other/unknown export name — a config naming one is reported by the
101
+ * loader rather than silently retargeted at a package that may not have it.
102
+ */
103
+ export const CORE_PLUGINS_BARREL_EXPORTS = {
104
+ DrupalGaiaRemote: { target: REMOTE_DRUPAL, defaultExport: false },
105
+ FakeGaiaRemote: { target: FAKE, defaultExport: false },
106
+ FakeWorkspace: { target: FAKE, defaultExport: false },
107
+ GitWorkspace: { target: WORKSPACE_GIT, defaultExport: false },
108
+ basicAuthProvider: { target: AUTH_BASIC, defaultExport: true },
109
+ drupalRemote: { target: REMOTE_DRUPAL, defaultExport: true },
110
+ // @gaia-ai/addon-fake ships the whole deterministic triple as NAMED exports
111
+ // (no default), so these keep their `export:`.
112
+ fakeRemote: { target: FAKE, defaultExport: false },
113
+ fakeWorkspace: { target: FAKE, defaultExport: false },
114
+ gitWorkspace: { target: WORKSPACE_GIT, defaultExport: true },
115
+ };
116
+ const BARREL_PATH = '@gaia-ai/core/plugins';
117
+ /** `plugin:` / `use:` — the two descriptor keys that name a package. */
118
+ const PKG_KEY = '(?:plugin|use)';
119
+ /** A JS identifier, i.e. an `export:` value the barrel could have carried. */
120
+ const IDENT = String.raw `[A-Za-z_$][\w$]*`;
121
+ /**
122
+ * `plugin: '@gaia-ai/core/plugins', export: 'drupalRemote'` — the barrel
123
+ * descriptor in its written order, single/double/back quoted, one line or many
124
+ * (`\s` spans newlines). Requiring the `export:` to be ADJACENT is what keeps
125
+ * this from ever pairing a barrel mention with an unrelated slot's `export:`.
126
+ */
127
+ const BARREL_THEN_EXPORT = new RegExp(`(${PKG_KEY}\\s*:\\s*)(['"\`])${BARREL_PATH}\\2(\\s*,\\s*export\\s*:\\s*)(['"\`])(${IDENT})\\4`, 'g');
128
+ /** The same descriptor with the keys written the other way round. */
129
+ const EXPORT_THEN_BARREL = new RegExp(`export\\s*:\\s*(['"\`])(${IDENT})\\1(\\s*,\\s*)(${PKG_KEY}\\s*:\\s*)(['"\`])${BARREL_PATH}\\5`, 'g');
130
+ /**
131
+ * Rewrite the `@gaia-ai/core/plugins` descriptors in one config's source text.
132
+ *
133
+ * A mapped descriptor becomes `<key>: '<addon>'` — with the `export:` DROPPED
134
+ * when the addon default-exports that factory *and* the whole descriptor sits on
135
+ * one line, so the rewrite stays line-local (the index-aligned report diff and
136
+ * `migrateAddonNames`' line-count invariant depend on that). A multi-line
137
+ * descriptor keeps its `export:`: every mapped name is also a NAMED export of its
138
+ * target addon, so the result resolves either way — only the redundancy survives.
139
+ *
140
+ * Left byte-untouched: an unmapped/unknown `export:` name, and a barrel
141
+ * occurrence with no adjacent `export:` at all (a bare `addons: ['…/plugins']` or
142
+ * a prose mention) — neither carries enough information to pick a target.
143
+ */
144
+ function migrateBarrelDescriptors(text) {
145
+ let count = 0;
146
+ const rewrite = (whole, name, keyPrefix, quote, keepExport) => {
147
+ const hit = CORE_PLUGINS_BARREL_EXPORTS[name];
148
+ if (hit === undefined)
149
+ return whole;
150
+ count++;
151
+ const spec = `${keyPrefix}${quote}${hit.target}${quote}`;
152
+ if (hit.defaultExport && !whole.includes('\n'))
153
+ return spec;
154
+ return `${spec}${keepExport()}`;
155
+ };
156
+ const out = text
157
+ .replace(BARREL_THEN_EXPORT, (whole, keyPrefix, pkgQuote, exportSep, nameQuote, name) => rewrite(whole, name, keyPrefix, pkgQuote, () => `${exportSep}${nameQuote}${name}${nameQuote}`))
158
+ .replace(EXPORT_THEN_BARREL, (whole, nameQuote, name, sep, keyPrefix, pkgQuote) => rewrite(whole, name, keyPrefix, pkgQuote, () => `${sep}export: ${nameQuote}${name}${nameQuote}`));
159
+ return { text: out, count };
160
+ }
161
+ /**
162
+ * The object-literal entry that encloses `at`, e.g.
163
+ * `{ use: '@gaia-ai/core/builtins', with: { basic: token } }`. Empty string when
164
+ * the occurrence is a BARE string entry (`addons: ['@gaia-ai/core/builtins']`) —
165
+ * the backward scan then hits the array/argument boundary first.
166
+ */
167
+ function enclosingEntry(text, at) {
168
+ let depth = 0;
169
+ let start = -1;
170
+ for (let i = at; i >= 0; i--) {
171
+ const c = text[i];
172
+ if (c === '}')
173
+ depth++;
174
+ else if (c === '{') {
175
+ if (depth === 0) {
176
+ start = i;
177
+ break;
178
+ }
179
+ depth--;
180
+ }
181
+ else if (depth === 0 && (c === '[' || c === '(' || c === ';')) {
182
+ break;
183
+ }
184
+ }
185
+ if (start < 0)
186
+ return '';
187
+ let d = 0;
188
+ for (let i = start; i < text.length; i++) {
189
+ const c = text[i];
190
+ if (c === '{')
191
+ d++;
192
+ else if (c === '}') {
193
+ d--;
194
+ if (d === 0)
195
+ return text.slice(start, i + 1);
196
+ }
197
+ }
198
+ return text.slice(start);
199
+ }
200
+ /** The nearest `plugins:` / `addons:` array key BEFORE `at`, if any. */
201
+ function nearestArrayKey(text, at) {
202
+ const re = /\b(plugins|addons)\s*:/g;
203
+ let last;
204
+ for (let m = re.exec(text); m !== null && m.index < at; m = re.exec(text)) {
205
+ last = m[1];
206
+ }
207
+ return last;
208
+ }
209
+ /**
210
+ * `@gaia-ai/core/builtins` was ONE preset contributing to BOTH surfaces, so its
211
+ * replacement is per-occurrence (spec v3 decision 17, the Finding 6/8 interplay):
212
+ *
213
+ * | signal (first that applies) | target |
214
+ * | the entry carries a `basic` token | `@gaia-ai/addon-auth-basic` |
215
+ * | the nearest array key is `plugins:` | `@gaia-ai/addon-auth-basic` |
216
+ * | the file is a connection config | `@gaia-ai/addon-auth-basic` |
217
+ * | otherwise (engine `addons:` / a slot) | `@gaia-ai/addon-remote-drupal` |
218
+ *
219
+ * The `plugins:` rule is what makes a LEGACY `conductor.config.js` (engine file
220
+ * name, but still carrying a connection `site`/`plugins`) come out right.
221
+ */
222
+ function builtinsTargetFor(text, at, surface) {
223
+ if (/basic/i.test(enclosingEntry(text, at)))
224
+ return AUTH_BASIC;
225
+ if (nearestArrayKey(text, at) === 'plugins')
226
+ return AUTH_BASIC;
227
+ return surface === 'connection' ? AUTH_BASIC : REMOTE_DRUPAL;
228
+ }
229
+ /**
230
+ * Rewrite the legacy specifiers in one config's SOURCE TEXT — all three: the
231
+ * renamed `@gaia-ai/plugin-*` packages, the deleted `@gaia-ai/core/builtins`
232
+ * preset, and the deleted `@gaia-ai/core/plugins` barrel descriptors. Pure; line
233
+ * count is preserved (every replacement is line-local), which is what lets the
234
+ * reported diff be a plain index-aligned line diff.
235
+ */
236
+ export function migrateAddonNames(text, surface) {
237
+ let count = 0;
238
+ let out = text.replace(QUOTED_LEGACY_ADDON, (_m, quote, name, subpath) => {
239
+ count++;
240
+ return `${quote}@gaia-ai/addon-${name}${subpath}${quote}`;
241
+ });
242
+ out = out.replace(QUOTED_LEGACY_BUILTINS, (_m, quote, offset, whole) => {
243
+ count++;
244
+ return `${quote}${builtinsTargetFor(whole, offset, surface)}${quote}`;
245
+ });
246
+ const barrel = migrateBarrelDescriptors(out);
247
+ return { text: barrel.text, count: count + barrel.count };
248
+ }
249
+ /** An index-aligned `- old` / `+ new` line diff (line count is preserved). */
250
+ function lineDiff(before, after) {
251
+ const a = before.split('\n');
252
+ const b = after.split('\n');
253
+ const out = [];
254
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
255
+ if (a[i] === b[i])
256
+ continue;
257
+ if (a[i] !== undefined)
258
+ out.push(` - ${a[i]}`);
259
+ if (b[i] !== undefined)
260
+ out.push(` + ${b[i]}`);
261
+ }
262
+ return out;
263
+ }
264
+ /**
265
+ * Rename pass over ONE config file. Returns `undefined` when the file is absent
266
+ * or already on the new names (idempotency: no write, no backup, no report
267
+ * line — the file stays byte-identical).
268
+ */
269
+ export function migrateAddonNamesInFile(path, surface, opts = {}) {
270
+ if (!existsSync(path))
271
+ return undefined;
272
+ const before = readFileSync(path, 'utf8');
273
+ const { text: after, count } = migrateAddonNames(before, surface);
274
+ if (count === 0 || after === before)
275
+ return undefined;
276
+ const diff = lineDiff(before, after);
277
+ const what = `${count} legacy addon reference(s) → @gaia-ai/addon-*`;
278
+ if (opts.dryRun ?? false) {
279
+ return { action: `would rewrite ${path} (${what})`, diff, count };
280
+ }
281
+ const bak = `${path}${ADDON_RENAME_BAK_SUFFIX}`;
282
+ writeFileSync(bak, before, 'utf8');
283
+ writeFileSync(path, after, 'utf8');
284
+ return {
285
+ action: `rewrote ${path} (${what}; backup ${bak})`,
286
+ diff,
287
+ count,
288
+ };
289
+ }
290
+ /**
291
+ * Rename pass over every recognised config in the given `.gaia/` dirs (project
292
+ * + home). Emits report lines ONLY for files it actually rewrites, so an install
293
+ * already on `addon-*` adds no noise to the upgrade report.
294
+ */
295
+ export function runAddonRenameMigration(opts = { gaiaDirs: [] }) {
296
+ const actions = [];
297
+ let changed = false;
298
+ const seen = new Set();
299
+ for (const dir of opts.gaiaDirs) {
300
+ if (seen.has(dir) || !existsSync(dir))
301
+ continue;
302
+ seen.add(dir);
303
+ const files = [
304
+ [join(dir, 'gaia.config.js'), 'connection'],
305
+ ...listConductorConfigFiles(dir).map((f) => [join(dir, f), 'engine']),
306
+ ];
307
+ for (const [path, surface] of files) {
308
+ const res = migrateAddonNamesInFile(path, surface, {
309
+ dryRun: opts.dryRun ?? false,
310
+ });
311
+ if (res === undefined)
312
+ continue;
313
+ actions.push(res.action, ...res.diff);
314
+ changed = true;
315
+ }
316
+ }
317
+ return { actions, changed };
318
+ }