@gaia-ai/conductor 0.5.5 → 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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/config-schema.d.ts +89 -0
  3. package/dist/src/cli/config-schema.js +146 -0
  4. package/dist/src/cli/init.d.ts +31 -33
  5. package/dist/src/cli/init.js +106 -84
  6. package/dist/src/cli/migrate-addon-names.d.ts +72 -0
  7. package/dist/src/cli/migrate-addon-names.js +318 -0
  8. package/dist/src/cli/upgrade.d.ts +53 -0
  9. package/dist/src/cli/upgrade.js +222 -0
  10. package/dist/src/cli/version-check.js +5 -1
  11. package/dist/src/commands/conductor.d.ts +57 -0
  12. package/dist/src/{cli/gaia.js → commands/conductor.js} +107 -215
  13. package/dist/src/config.d.ts +65 -24
  14. package/dist/src/config.js +405 -154
  15. package/dist/src/contract.d.ts +8 -0
  16. package/dist/src/contract.js +16 -0
  17. package/dist/src/core/conductor.d.ts +16 -1
  18. package/dist/src/core/conductor.js +28 -9
  19. package/dist/src/index.d.ts +7 -5
  20. package/dist/src/index.js +26 -3
  21. package/dist/src/plugins/agent.d.ts +61 -0
  22. package/dist/src/plugins/agent.js +11 -0
  23. package/dist/src/plugins/executor.d.ts +104 -0
  24. package/dist/src/plugins/executor.js +1 -0
  25. package/dist/src/plugins/plugins.d.ts +60 -0
  26. package/dist/src/plugins/plugins.js +42 -0
  27. package/dist/src/plugins/preset.d.ts +48 -0
  28. package/dist/src/plugins/preset.js +23 -0
  29. package/dist/src/plugins/remote.d.ts +203 -0
  30. package/dist/src/plugins/remote.js +1 -0
  31. package/dist/src/plugins/workspace.d.ts +35 -0
  32. package/dist/src/plugins/workspace.js +1 -0
  33. package/dist/src/preset.d.ts +2 -0
  34. package/dist/src/preset.js +8 -0
  35. package/dist/src/types.d.ts +65 -0
  36. package/dist/src/types.js +1 -0
  37. package/package.json +8 -5
  38. package/dist/src/cli/gaia.d.ts +0 -23
  39. package/dist/src/cli/local-registry.d.ts +0 -14
  40. package/dist/src/cli/local-registry.js +0 -56
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/).
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A single, discrete, testable upgrade method `vN → vN+1`, registered in order —
3
+ * exactly like a Drupal `hook_update_N` chain. `gaia upgrade` resolves a config's
4
+ * current version and applies each method in sequence up to CURRENT (it does not
5
+ * blindly overwrite), so a method can PRESERVE operator-set fields across a bump.
6
+ */
7
+ export interface ConnectionConfigMigration {
8
+ /** The schema version this method upgrades FROM. */
9
+ from: number;
10
+ /** The schema version this method upgrades TO (=== from + 1). */
11
+ to: number;
12
+ /** What shape change this method performs (human-readable). */
13
+ description: string;
14
+ /**
15
+ * Upgrade the config text from `from` to `to`, returning the new file text
16
+ * WITHOUT the version header — the runner re-stamps `@gaia-schema-version to`.
17
+ */
18
+ apply(currentText: string): string;
19
+ }
20
+ /**
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.
51
+ */
52
+ export declare const CONNECTION_MIGRATIONS: ConnectionConfigMigration[];
53
+ /** The current connection-config schema version — single source of truth. */
54
+ export declare const GAIA_CONFIG_SCHEMA_VERSION: number;
55
+ /** Parse the `@gaia-schema-version` marker from config text; `0` when absent. */
56
+ export declare function schemaVersionOf(text: string): number;
57
+ /**
58
+ * Read the schema version from a connection config FILE by regex (sync, never
59
+ * executes the module). `0` when the file is absent, unreadable, or carries no
60
+ * marker (legacy / hand-authored).
61
+ */
62
+ export declare function readConfigSchemaVersion(path: string): number;
63
+ /** Strip a leading `// @gaia-schema-version <N>` header line, if present. */
64
+ export declare function stripVersionHeader(text: string): string;
65
+ /** Prepend (replacing any existing) a `// @gaia-schema-version <N>` header. */
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;
76
+ /**
77
+ * Validate a migration chain is contiguous: each `to === from + 1`, the first
78
+ * migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
79
+ * An empty chain is valid iff `current === 1`. Throws with an actionable message.
80
+ */
81
+ export declare function assertContiguous(chain: ConnectionConfigMigration[], current?: number): void;
82
+ /**
83
+ * Walk the migration chain from `from` up to `to`, applying each method's
84
+ * `apply` in sequence. Input/output text is WITHOUT the version header — the
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.
88
+ */
89
+ export declare function applyMigrations(text: string, from: number, to: number, chain?: ConnectionConfigMigration[]): string;
@@ -0,0 +1,146 @@
1
+ import { readFileSync } from 'node:fs';
2
+ // GAIA-216: schema versioning for the generated CONNECTION config
3
+ // (`gaia.config.js`, project + home). Decoupled from the npm package semver —
4
+ // the schema version bumps only when the config SHAPE changes, not every
5
+ // release. Every generated connection config carries a machine-readable header
6
+ // comment `// @gaia-schema-version <N>` (the source of truth for the upgrade
7
+ // decision, read by regex so `gaia upgrade` NEVER executes a possibly
8
+ // side-effecting config module) and a `schema_version: <N>` field for
9
+ // programmatic reads.
10
+ /** The header marker prepended to every generated connection config. */
11
+ const MARKER_RE = /@gaia-schema-version\s+(\d+)/;
12
+ const HEADER_LINE_RE = /^\/\/ @gaia-schema-version \d+\n/;
13
+ /**
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.
57
+ */
58
+ export const CONNECTION_MIGRATIONS = [
59
+ {
60
+ from: 1,
61
+ to: 2,
62
+ description: `add the ${RENDERER_PLUGIN_PACKAGE} renderer entry to plugins[]`,
63
+ apply: addRendererPluginEntry,
64
+ },
65
+ ];
66
+ /** The current connection-config schema version — single source of truth. */
67
+ export const GAIA_CONFIG_SCHEMA_VERSION = CONNECTION_MIGRATIONS.at(-1)?.to ?? 1;
68
+ /** Parse the `@gaia-schema-version` marker from config text; `0` when absent. */
69
+ export function schemaVersionOf(text) {
70
+ const m = MARKER_RE.exec(text);
71
+ return m ? Number.parseInt(m[1], 10) : 0;
72
+ }
73
+ /**
74
+ * Read the schema version from a connection config FILE by regex (sync, never
75
+ * executes the module). `0` when the file is absent, unreadable, or carries no
76
+ * marker (legacy / hand-authored).
77
+ */
78
+ export function readConfigSchemaVersion(path) {
79
+ try {
80
+ return schemaVersionOf(readFileSync(path, 'utf8'));
81
+ }
82
+ catch {
83
+ return 0;
84
+ }
85
+ }
86
+ /** Strip a leading `// @gaia-schema-version <N>` header line, if present. */
87
+ export function stripVersionHeader(text) {
88
+ return text.replace(HEADER_LINE_RE, '');
89
+ }
90
+ /** Prepend (replacing any existing) a `// @gaia-schema-version <N>` header. */
91
+ export function stampVersion(text, version) {
92
+ return `// @gaia-schema-version ${version}\n${stripVersionHeader(text)}`;
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
+ }
105
+ /**
106
+ * Validate a migration chain is contiguous: each `to === from + 1`, the first
107
+ * migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
108
+ * An empty chain is valid iff `current === 1`. Throws with an actionable message.
109
+ */
110
+ export function assertContiguous(chain, current = GAIA_CONFIG_SCHEMA_VERSION) {
111
+ let prev = 1;
112
+ chain.forEach((m, i) => {
113
+ if (m.to !== m.from + 1) {
114
+ throw new Error(`migration[${i}]: to (${m.to}) must equal from+1 (${m.from + 1})`);
115
+ }
116
+ const expectedFrom = i === 0 ? 1 : prev;
117
+ if (m.from !== expectedFrom) {
118
+ throw new Error(`migration[${i}]: from (${m.from}) must equal ${expectedFrom} (no gaps; chain starts at v1)`);
119
+ }
120
+ prev = m.to;
121
+ });
122
+ const end = chain.at(-1)?.to ?? 1;
123
+ if (end !== current) {
124
+ throw new Error(`migration chain ends at v${end}, expected CURRENT v${current}`);
125
+ }
126
+ }
127
+ /**
128
+ * Walk the migration chain from `from` up to `to`, applying each method's
129
+ * `apply` in sequence. Input/output text is WITHOUT the version header — the
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.
133
+ */
134
+ export function applyMigrations(text, from, to, chain = CONNECTION_MIGRATIONS) {
135
+ let cur = from;
136
+ let out = text;
137
+ while (cur < to) {
138
+ const step = chain.find((m) => m.from === cur);
139
+ if (step === undefined) {
140
+ throw new Error(`no migration from v${cur} (chain incomplete)`);
141
+ }
142
+ out = stampSchemaVersionField(step.apply(out), step.to);
143
+ cur = step.to;
144
+ }
145
+ return out;
146
+ }
@@ -1,3 +1,6 @@
1
+ import { type MachineContext, machineContextPath, readMachineContext } from '@gaia-ai/core';
2
+ export type { MachineContext };
3
+ export { machineContextPath, readMachineContext };
1
4
  export interface InitInputs {
2
5
  baseUrl: string;
3
6
  project: string;
@@ -21,19 +24,6 @@ export interface ScaffoldResult {
21
24
  wroteCommitted: boolean;
22
25
  machine: MachineContextResult;
23
26
  }
24
- /**
25
- * The user-global machine context: a plain importable module carrying the
26
- * developer's machine identity and connection (incl. the OAuth client secret).
27
- * Committed configs import it to compose machine_id and read
28
- * base_url / client_id / client_secret. Gitignored, user-only (chmod 0600).
29
- */
30
- export interface MachineContext {
31
- machine_id: string;
32
- user_id: string;
33
- base_url: string;
34
- client_id: string;
35
- client_secret: string;
36
- }
37
27
  export interface MachineContextOptions {
38
28
  path: string;
39
29
  userId: string;
@@ -48,35 +38,43 @@ export interface MachineContextResult {
48
38
  filledKeys: string[];
49
39
  }
50
40
  /**
51
- * The committed, structural conductor config. `project` is the only per-repo
52
- * value and is baked in here; connection + identity (incl. the secret) come from
53
- * the user-global machine context (~/.config/conductor/conductor.config.machine.js),
54
- * and machine_id is composed as `${user_id}-${machine_id}-${project}`. An
55
- * optional, gitignored conductor.config.local.js beside this file may override
56
- * any field — it is loaded if present but never created by `gaia conductor init`.
41
+ * The committed, structural ENGINE conductor config (GAIA-201). It carries the
42
+ * engine wiring only remote/executor/agent/workspace + `machine_id`
43
+ * composition and NO `site`/`plugins`, which now live in the sibling
44
+ * connection config `gaia.config.js`. Connection + identity (incl. the secret)
45
+ * come from the user-global machine context (`~/.gaia/machine.config.js`);
46
+ * `machine_id` is composed as `${user_id}-${machine_id}-${project}`.
57
47
  */
58
48
  export declare function renderCommittedConfig(inputs: Pick<InitInputs, 'project'>): string;
49
+ /**
50
+ * The committed CONNECTION config `./.gaia/gaia.config.js` (GAIA-201). A
51
+ * dropsh-shaped `{ site, plugins }` read by `gaia ui`, `gaia dropsh`, and the
52
+ * conductor's auth. Reads base_url + client credentials from the user-global
53
+ * machine context (`~/.gaia/machine.config.js`) and declares the session
54
+ * (default) + pm oauth2 profiles.
55
+ */
56
+ export declare function renderGaiaConfig(): string;
59
57
  /** The user-global machine context module: identity + connection (incl. secret). */
60
58
  export declare function renderMachineContext(ctx: MachineContext): string;
61
- /** The user-global machine context path: ~/.config/conductor/conductor.config.machine.js */
62
- export declare function machineContextPath(): string;
63
- /** Import an existing context module's default export, or {} if absent/broken. */
64
- export declare function readMachineContext(path: string): Promise<Partial<MachineContext>>;
65
59
  /**
66
60
  * Create-if-missing / fill-only-missing the user-global machine context.
67
61
  * Existing values always win; only absent/blank keys are filled. machine_id
68
- * defaults to hostname(). A no-op (no rewrite) when the file is already complete.
69
- * The file is written user-only (chmod 0600) since it holds the client secret.
62
+ * defaults to hostname(). Written user-only (chmod 0600) since it holds the
63
+ * client secret.
70
64
  */
71
65
  export declare function scaffoldMachineContext(opts: MachineContextOptions): Promise<MachineContextResult>;
66
+ /** Write the sibling connection config `gaia.config.js` next to the engine
67
+ * config. Created only if missing (unless `force`). Returns its path + whether
68
+ * it was written. */
69
+ export declare function scaffoldGaiaConfig(engineConfigPath: string, force: boolean): {
70
+ path: string;
71
+ wrote: boolean;
72
+ };
72
73
  /**
73
- * Scaffold the two conductor config files: the committed conductor.config.js
74
- * (created only if missing — never overwritten unless `force`) and the
75
- * user-global machine context (create-if-missing / fill-only-missing). The
76
- * optional per-project conductor.config.local.js is NOT generated.
77
- *
78
- * `skipMachine` writes the committed config only (project-only setup); its
79
- * mirror `skipCommitted` writes the machine context only (machine-only
80
- * onboarding, no repo). Setting both is a no-op.
74
+ * Scaffold the committed ENGINE conductor.config.js (created only if missing —
75
+ * never overwritten unless `force`) and the user-global machine context
76
+ * (create-if-missing / fill-only-missing). The connection config is scaffolded
77
+ * separately by `scaffoldGaiaConfig`. `skipMachine` writes the committed config
78
+ * only; `skipCommitted` writes the machine context only.
81
79
  */
82
80
  export declare function scaffold(inputs: InitInputs, opts: ScaffoldOptions): Promise<ScaffoldResult>;
@@ -1,50 +1,53 @@
1
1
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
4
+ import { machineContextPath, readMachineContext, } from '@gaia-ai/core';
5
+ import { GAIA_CONFIG_SCHEMA_VERSION, RENDERER_PLUGIN_ENTRY, } from './config-schema.js';
6
+ export { machineContextPath, readMachineContext };
5
7
  /** JS single-quoted string literal for a trusted, simple value. */
6
8
  function q(value) {
7
9
  return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
8
10
  }
9
11
  /**
10
- * The committed, structural conductor config. `project` is the only per-repo
11
- * value and is baked in here; connection + identity (incl. the secret) come from
12
- * the user-global machine context (~/.config/conductor/conductor.config.machine.js),
13
- * and machine_id is composed as `${user_id}-${machine_id}-${project}`. An
14
- * optional, gitignored conductor.config.local.js beside this file may override
15
- * any field — it is loaded if present but never created by `gaia conductor init`.
12
+ * The committed, structural ENGINE conductor config (GAIA-201). It carries the
13
+ * engine wiring only remote/executor/agent/workspace + `machine_id`
14
+ * composition and NO `site`/`plugins`, which now live in the sibling
15
+ * connection config `gaia.config.js`. Connection + identity (incl. the secret)
16
+ * come from the user-global machine context (`~/.gaia/machine.config.js`);
17
+ * `machine_id` is composed as `${user_id}-${machine_id}-${project}`.
16
18
  */
17
19
  export function renderCommittedConfig(inputs) {
18
- return `// Canonical GAIA conductor config — committed. Connection + identity come from
19
- // your user-global machine context (~/.config/conductor/conductor.config.machine.js:
20
+ return `// Canonical GAIA conductor ENGINE config — committed. Connection + identity
21
+ // come from your user-global machine context (~/.gaia/machine.config.js:
20
22
  // { machine_id, user_id, base_url, client_id, client_secret }); machine_id is
21
23
  // composed here as \`\${user_id}-\${machine_id}-\${project}\`. \`project\` is the only
22
- // per-repo value and is baked in below. The client_secret is read from the
23
- // machine context (gitignored, user-only)never committed here.
24
+ // per-repo value and is baked in below. This file is ENGINE-ONLY (GAIA-201):
25
+ // it has NO \`site\` and NO auth \`plugins\` those live in the sibling
26
+ // ./.gaia/gaia.config.js (the connection config that ui + dropsh + this
27
+ // conductor's auth all read).
24
28
  //
25
- // IMPORT-FREE (GAIA-78): the plugin slots + plugins[] are \`{ plugin, with }\`
26
- // descriptors naming the REAL published package (\`@gaia-ai/plugin-*\`,
27
- // \`@dropsh/plugin-*\`), not \`import\`ed constructors. loadConductorConfig
28
- // resolves each name ESLint-style (config dir cwd → conductor install), so
29
- // config load never depends on a \`node_modules/@gaia-ai\` symlink beside this
30
- // file. Each plugin package default-exports its factory, so the resolver's
31
- // auto-pick needs no \`export:\` hereEXCEPT the \`@gaia-ai/gaia/plugins\` host
32
- // barrel (many exports → \`export: 'drupalRemote'\`) and the merged herdr
33
- // workspace slot (\`@gaia-ai/plugin-herdr\` default-exports the executor, so the
34
- // workspace names \`export: 'herdrWorkspace'\` — GAIA-139).
29
+ // IMPORT-FREE (GAIA-78): the plugin slots are \`{ plugin, with }\` descriptors
30
+ // naming the REAL published package, not \`import\`ed constructors.
31
+ // loadConductorConfig resolves each name ESLint-style (config dir → cwd →
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\`.
35
38
 
36
39
  // The user-global machine context: identity + connection (incl. secret), shared
37
40
  // by every project on this machine. Never committed.
38
41
  async function loadMachine() {
39
42
  try {
40
- return (await import(\`\${process.env.HOME}/.config/conductor/conductor.config.machine.js\`)).default ?? {};
43
+ return (await import(\`\${process.env.HOME}/.gaia/machine.config.js\`)).default ?? {};
41
44
  } catch {}
42
45
  return {};
43
46
  }
44
47
 
45
48
  // OPTIONAL per-project override — create conductor.config.local.js beside this
46
- // file to override any field (machine_id, base_url, model, …). It is loaded only
47
- // if present and is NOT created by \`gaia conductor init\`.
49
+ // file to override any field (machine_id, model, …). Loaded only if present and
50
+ // NOT created by \`gaia conductor init\`.
48
51
  async function loadLocal() {
49
52
  try { return (await import('./conductor.config.local.js')).default ?? {}; } catch {}
50
53
  return {};
@@ -53,48 +56,79 @@ async function loadLocal() {
53
56
  const machine = await loadMachine();
54
57
  const local = await loadLocal();
55
58
  const project = local.project ?? ${q(inputs.project)};
56
- const baseUrl = local.base_url ?? machine.base_url;
57
- const clientId = local.oauth?.client_id ?? machine.client_id ?? 'gaia-agent';
58
- const clientSecret = local.oauth?.client_secret ?? machine.client_secret;
59
59
  const composedMachineId =
60
60
  machine.user_id && machine.machine_id
61
61
  ? \`\${machine.user_id}-\${machine.machine_id}-\${project}\`
62
62
  : undefined;
63
63
 
64
64
  export default {
65
- site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
66
65
  project,
67
66
  machine_id: local.machine_id ?? composedMachineId,
68
67
  states: ['spec', 'diagnose', 'coding', 'review'],
69
68
  max_parallel: 5,
70
- // Lifecycle hooks are executor-owned (GAIA-84): the executor invokes each
71
- // best-effort (logs loudly + continues, never aborts a run), so they live at
72
- // the config top level — NOT on a plugin descriptor's \`with.hooks\`.
69
+ // Lifecycle hooks are executor-owned (GAIA-84): they live at the config top
70
+ // level NOT on a plugin descriptor's \`with.hooks\`.
73
71
  hooks: { after_create: 'ddev init-worktree', after_done: 'ddev delete -Oy' },
74
- remote: { plugin: '@gaia-ai/gaia/plugins', export: 'drupalRemote' },
75
- // No hard-wired diff pane for review: the review diff surface is hunk
76
- // (GAIA-55) — agent-driven + opt-in in the human's interactive pane, not an
77
- // executor-forced git-diff pane. Clicking a changed file in that hunk pane
78
- // opens it editable in a spiceedit overlay (see conductor/README.md).
79
- executor: { plugin: '@gaia-ai/plugin-herdr' },
72
+ remote: { plugin: '@gaia-ai/addon-remote-drupal' },
73
+ executor: { plugin: '@gaia-ai/addon-herdr' },
80
74
  agent: {
81
- plugin: '@gaia-ai/plugin-claude',
75
+ plugin: '@gaia-ai/addon-claude',
82
76
  with: { model: local.model ?? 'claude-opus-4-8' },
83
77
  },
84
- // GAIA-139: the herdr workspace ships in @gaia-ai/plugin-herdr now (the
85
- // separate plugin-herdr-workspace was merged in). That package default-exports
86
- // the EXECUTOR, so the workspace slot must name export: 'herdrWorkspace'.
78
+ // GAIA-139: the herdr workspace ships in @gaia-ai/addon-herdr, which
79
+ // default-exports the EXECUTOR so the workspace slot names export: 'herdrWorkspace'.
87
80
  workspace: {
88
- plugin: '@gaia-ai/plugin-herdr',
81
+ plugin: '@gaia-ai/addon-herdr',
89
82
  export: 'herdrWorkspace',
90
83
  },
91
- // oauth2 is a real dep of the host (npm installs it alongside @gaia-ai/gaia).
92
- // NOTE: plugins[] is consumed by DROPSH, which reloads this config with its OWN
93
- // resolver (\`export ?? 'default'\`, no sole-function auto-pick) on every
94
- // \`gaia dropsh …\` command. @dropsh/plugin-oauth2 has no default export, so
95
- // these entries MUST name \`export: 'oauth2Plugin'\` unlike the four conductor
96
- // slots above, which the conductor resolves and auto-picks.
84
+ };
85
+ `;
86
+ }
87
+ /**
88
+ * The committed CONNECTION config `./.gaia/gaia.config.js` (GAIA-201). A
89
+ * dropsh-shaped `{ site, plugins }` read by `gaia ui`, `gaia dropsh`, and the
90
+ * conductor's auth. Reads base_url + client credentials from the user-global
91
+ * machine context (`~/.gaia/machine.config.js`) and declares the session
92
+ * (default) + pm oauth2 profiles.
93
+ */
94
+ export function renderGaiaConfig() {
95
+ return `// @gaia-schema-version ${GAIA_CONFIG_SCHEMA_VERSION}
96
+ // Canonical GAIA CONNECTION config — committed (GAIA-201). A dropsh-shaped
97
+ // { site, plugins } read by 'gaia ui', 'gaia dropsh', and this conductor's auth.
98
+ // Connection + credentials come from your user-global machine context
99
+ // (~/.gaia/machine.config.js). The engine wiring lives in the sibling
100
+ // conductor.config.js (engine-only).
101
+ //
102
+ // plugins[] is consumed by DROPSH, whose resolver has no sole-function
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.
107
+ async function loadMachine() {
108
+ try {
109
+ return (await import(\`\${process.env.HOME}/.gaia/machine.config.js\`)).default ?? {};
110
+ } catch {}
111
+ return {};
112
+ }
113
+
114
+ async function loadLocal() {
115
+ try { return (await import('./conductor.config.local.js')).default ?? {}; } catch {}
116
+ return {};
117
+ }
118
+
119
+ const machine = await loadMachine();
120
+ const local = await loadLocal();
121
+ const baseUrl = local.base_url ?? machine.base_url;
122
+ const clientId = local.oauth?.client_id ?? machine.client_id ?? 'gaia-agent';
123
+ const clientSecret = local.oauth?.client_secret ?? machine.client_secret;
124
+
125
+ export default {
126
+ // GAIA-216: the connection-config schema version — kept in sync with the
127
+ // header marker above so \`gaia upgrade\` can migrate a stale-shape config.
128
+ schema_version: ${GAIA_CONFIG_SCHEMA_VERSION},
129
+ site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
97
130
  plugins: [
131
+ ${RENDERER_PLUGIN_ENTRY}
98
132
  {
99
133
  plugin: '@dropsh/plugin-oauth2',
100
134
  export: 'oauth2Plugin',
@@ -126,12 +160,12 @@ export default {
126
160
  }
127
161
  /** The user-global machine context module: identity + connection (incl. secret). */
128
162
  export function renderMachineContext(ctx) {
129
- return `// User-global conductor context — gitignored, user-only (chmod 0600), never
163
+ return `// User-global GAIA machine context — gitignored, user-only (chmod 0600), never
130
164
  // committed. A plain importable module holding your machine identity +
131
- // connection, incl. the OAuth client secret. Committed conductor.config.js files
132
- // import this to compose machine_id (\`\${user_id}-\${machine_id}-\${project}\`) and
133
- // read base_url / client_id / client_secret. Created and gap-filled by
134
- // \`gaia conductor init\`; existing values are never overwritten.
165
+ // connection, incl. the OAuth client secret. The engine conductor.config.js
166
+ // composes machine_id (\`\${user_id}-\${machine_id}-\${project}\`) from it; the
167
+ // connection gaia.config.js reads base_url / client_id / client_secret. Created
168
+ // and gap-filled by \`gaia conductor init\`; existing values are never overwritten.
135
169
  export default {
136
170
  machine_id: ${q(ctx.machine_id)},
137
171
  user_id: ${q(ctx.user_id)},
@@ -141,31 +175,11 @@ export default {
141
175
  };
142
176
  `;
143
177
  }
144
- /** The user-global machine context path: ~/.config/conductor/conductor.config.machine.js */
145
- export function machineContextPath() {
146
- return join(process.env.HOME ?? '', '.config', 'conductor', 'conductor.config.machine.js');
147
- }
148
- /** Import an existing context module's default export, or {} if absent/broken. */
149
- export async function readMachineContext(path) {
150
- if (!existsSync(path))
151
- return {};
152
- try {
153
- // Cache-bust so a re-render within one process re-reads fresh.
154
- const mod = await import(`${pathToFileURL(path).href}?t=${Date.now()}`);
155
- const raw = mod.default;
156
- return raw && typeof raw === 'object'
157
- ? raw
158
- : {};
159
- }
160
- catch {
161
- return {};
162
- }
163
- }
164
178
  /**
165
179
  * Create-if-missing / fill-only-missing the user-global machine context.
166
180
  * Existing values always win; only absent/blank keys are filled. machine_id
167
- * defaults to hostname(). A no-op (no rewrite) when the file is already complete.
168
- * The file is written user-only (chmod 0600) since it holds the client secret.
181
+ * defaults to hostname(). Written user-only (chmod 0600) since it holds the
182
+ * client secret.
169
183
  */
170
184
  export async function scaffoldMachineContext(opts) {
171
185
  const existing = await readMachineContext(opts.path);
@@ -196,20 +210,28 @@ export async function scaffoldMachineContext(opts) {
196
210
  mkdirSync(dirname(opts.path), { recursive: true });
197
211
  writeFileSync(opts.path, renderMachineContext(merged), 'utf8');
198
212
  }
199
- // Always tighten perms — the file holds a secret.
200
213
  if (existsSync(opts.path))
201
214
  chmodSync(opts.path, 0o600);
202
215
  return { path: opts.path, created, filledKeys };
203
216
  }
217
+ /** Write the sibling connection config `gaia.config.js` next to the engine
218
+ * config. Created only if missing (unless `force`). Returns its path + whether
219
+ * it was written. */
220
+ export function scaffoldGaiaConfig(engineConfigPath, force) {
221
+ const path = join(dirname(engineConfigPath), 'gaia.config.js');
222
+ mkdirSync(dirname(path), { recursive: true });
223
+ if (!existsSync(path) || force) {
224
+ writeFileSync(path, renderGaiaConfig(), 'utf8');
225
+ return { path, wrote: true };
226
+ }
227
+ return { path, wrote: false };
228
+ }
204
229
  /**
205
- * Scaffold the two conductor config files: the committed conductor.config.js
206
- * (created only if missing — never overwritten unless `force`) and the
207
- * user-global machine context (create-if-missing / fill-only-missing). The
208
- * optional per-project conductor.config.local.js is NOT generated.
209
- *
210
- * `skipMachine` writes the committed config only (project-only setup); its
211
- * mirror `skipCommitted` writes the machine context only (machine-only
212
- * onboarding, no repo). Setting both is a no-op.
230
+ * Scaffold the committed ENGINE conductor.config.js (created only if missing —
231
+ * never overwritten unless `force`) and the user-global machine context
232
+ * (create-if-missing / fill-only-missing). The connection config is scaffolded
233
+ * separately by `scaffoldGaiaConfig`. `skipMachine` writes the committed config
234
+ * only; `skipCommitted` writes the machine context only.
213
235
  */
214
236
  export async function scaffold(inputs, opts) {
215
237
  const committedPath = opts.configPath;