@gaia-ai/core 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/src/core/conductor-id.d.ts +1 -0
  4. package/dist/src/core/conductor-id.js +8 -0
  5. package/dist/src/core/exec.d.ts +41 -0
  6. package/dist/src/core/exec.js +75 -0
  7. package/dist/src/core/logger.d.ts +31 -0
  8. package/dist/src/core/logger.js +36 -0
  9. package/dist/src/core/shell.d.ts +21 -0
  10. package/dist/src/core/shell.js +46 -0
  11. package/dist/src/core/slug.d.ts +9 -0
  12. package/dist/src/core/slug.js +18 -0
  13. package/dist/src/index.d.ts +12 -0
  14. package/dist/src/index.js +6 -0
  15. package/dist/src/plugins/agent/agent.d.ts +20 -0
  16. package/dist/src/plugins/agent/agent.js +1 -0
  17. package/dist/src/plugins/auth/basic.d.ts +11 -0
  18. package/dist/src/plugins/auth/basic.js +35 -0
  19. package/dist/src/plugins/executor/executor.d.ts +92 -0
  20. package/dist/src/plugins/executor/executor.js +1 -0
  21. package/dist/src/plugins/plugins.d.ts +44 -0
  22. package/dist/src/plugins/plugins.js +16 -0
  23. package/dist/src/plugins/registry-exports.d.ts +6 -0
  24. package/dist/src/plugins/registry-exports.js +6 -0
  25. package/dist/src/plugins/remote/drupal.d.ts +48 -0
  26. package/dist/src/plugins/remote/drupal.js +363 -0
  27. package/dist/src/plugins/remote/fake.d.ts +87 -0
  28. package/dist/src/plugins/remote/fake.js +214 -0
  29. package/dist/src/plugins/remote/remote.d.ts +169 -0
  30. package/dist/src/plugins/remote/remote.js +1 -0
  31. package/dist/src/plugins/workspace/fake.d.ts +6 -0
  32. package/dist/src/plugins/workspace/fake.js +16 -0
  33. package/dist/src/plugins/workspace/git.d.ts +37 -0
  34. package/dist/src/plugins/workspace/git.js +89 -0
  35. package/dist/src/plugins/workspace/instructions.d.ts +6 -0
  36. package/dist/src/plugins/workspace/instructions.js +16 -0
  37. package/dist/src/plugins/workspace/workspace.d.ts +35 -0
  38. package/dist/src/plugins/workspace/workspace.js +1 -0
  39. package/dist/src/plugins-index.d.ts +1 -0
  40. package/dist/src/plugins-index.js +1 -0
  41. package/dist/src/types.d.ts +51 -0
  42. package/dist/src/types.js +1 -0
  43. package/package.json +33 -0
@@ -0,0 +1,169 @@
1
+ export interface ConductorRegistration {
2
+ id: string;
3
+ project: string;
4
+ states: string[];
5
+ workspace: string;
6
+ label: string;
7
+ max_parallel: number;
8
+ }
9
+ export interface ClaimOptions {
10
+ leaseSeconds: number;
11
+ conductorId: string;
12
+ }
13
+ export interface ClaimedRun {
14
+ runUuid: string;
15
+ runId: number;
16
+ ticketUuid: string;
17
+ stateAtStart: string;
18
+ handler: string;
19
+ }
20
+ /** A ticket comment, as the conductor injects it into the dispatch prompt. */
21
+ export interface TicketComment {
22
+ /** comment | spec | plan | debug_diagnose | summary | acceptance | test */
23
+ type: string;
24
+ /** Raw body (gaia_rich HTML). */
25
+ body: string;
26
+ /** ISO 8601 creation timestamp. */
27
+ created: string;
28
+ }
29
+ export interface Ticket {
30
+ uuid: string;
31
+ identifier: string;
32
+ title: string;
33
+ state: string;
34
+ branchName: string;
35
+ /** Branch the ticket's work is based on (computed server-side): parent branch, project default, or 'main'. */
36
+ baseBranch?: string;
37
+ /**
38
+ * Effective environment variables (GAIA-99), resolved server-side along the
39
+ * parent_id chain (child wins) as canonical `.env`-style `KEY=value` lines.
40
+ * The conductor parses this, strips reserved keys, overlays the core GAIA_*
41
+ * vars, and injects the result into the agent run env AND the workspace hooks.
42
+ * Omitted when the ticket (and its ancestors) set none. Values may be secret.
43
+ */
44
+ effectiveEnvVars?: string;
45
+ issueUrl?: string;
46
+ /**
47
+ * Comments on the ticket, oldest first. The conductor renders these into the
48
+ * dispatch prompt so a review→coding bounce carries the review summary to the
49
+ * coder. Empty when the ticket has none — or when the session role lacks
50
+ * `view gaia_comment` (the include degrades to an empty set, never an error).
51
+ */
52
+ comments: TicketComment[];
53
+ }
54
+ export interface ActiveRun {
55
+ runUuid: string;
56
+ ticketUuid: string;
57
+ ticketIdentifier: string;
58
+ branchName: string;
59
+ state: string;
60
+ stateAtStart: string;
61
+ /** Absolute path of the per-run git worktree, or '' when unset. */
62
+ worktreePath: string;
63
+ }
64
+ export interface ConductorStatus {
65
+ id: string;
66
+ project: string;
67
+ label: string;
68
+ status: string;
69
+ lastSeen: number;
70
+ load: number;
71
+ }
72
+ export interface FinalizableRun {
73
+ runUuid: string;
74
+ /** Absolute path of the per-run git worktree, or '' when unset. */
75
+ worktreePath: string;
76
+ }
77
+ /**
78
+ * A finished ticket whose worktree has not yet been torn down — the
79
+ * conductor's teardown work list, driven by the durable `cleaned_up` flag
80
+ * (GAIA-89). "Finished" = reached `done` OR already `closed`; `cleaned_up=0`
81
+ * means the herdr worktree still needs reclaiming. The reconciliation is NOT
82
+ * scoped to a conductor's machine_id, so it also surfaces tickets whose
83
+ * conductor was down at `done` or whose `conductor_id` was reassigned.
84
+ */
85
+ export interface UncleanTicket {
86
+ ticketUuid: string;
87
+ /** The ticket's git branch — the key herdr resolves the worktree by. */
88
+ branchName: string;
89
+ /**
90
+ * Absolute path of the ticket's worktree (from its latest run's
91
+ * `worktree_path`), or '' when unresolved. The cwd the teardown runs in.
92
+ */
93
+ worktreePath: string;
94
+ /** Workflow state (e.g. `coding`, `done`). */
95
+ state: string;
96
+ /** Whether the ticket's lifecycle has already been closed out. */
97
+ closed: boolean;
98
+ }
99
+ /** Extra conductor-writable gaia_run attributes (besides state/lease). */
100
+ export interface RunWriteAttributes {
101
+ /** Absolute path of the per-run git worktree (set by the conductor). */
102
+ worktree_path?: string;
103
+ }
104
+ export interface GaiaRemote {
105
+ /** Upserts by machine_id; sets status=online. Returns the Drupal entity uuid. */
106
+ registerConductor(reg: ConductorRegistration): Promise<string>;
107
+ /**
108
+ * Records a heartbeat server-side: upserts by machine_id (recreating a
109
+ * vanished registration), refreshes last_seen/lease/load, and brings the
110
+ * conductor online. Keyed on machine_id, so it never 404s — the self-heal
111
+ * lives on the server, not the client. Returns the resulting status
112
+ * (`online` | `offline`).
113
+ */
114
+ heartbeat(reg: ConductorRegistration, currentLoad: number, leaseSeconds?: number): Promise<string>;
115
+ getConductorStatus(conductorId: string): Promise<string | null>;
116
+ setConductorStatus(conductorId: string, status: 'offline' | 'online'): Promise<void>;
117
+ listConductors(owner?: 'me'): Promise<ConductorStatus[]>;
118
+ activeRunCount(conductorId: string): Promise<number>;
119
+ fetchActiveRuns(conductorId: string): Promise<ActiveRun[]>;
120
+ claimNext(claim: ClaimOptions): Promise<ClaimedRun | null>;
121
+ getTicket(ticketUuid: string): Promise<Ticket>;
122
+ /** Returns the run's worktree_path, or '' when unset. */
123
+ getRunWorktree(runUuid: string): Promise<string>;
124
+ /**
125
+ * Returns the identifier of the run's ticket, or '' when unresolved. Lets the
126
+ * release path derive the tab matching ref (the ticket identifier) from a run
127
+ * uuid alone.
128
+ */
129
+ getRunTicketIdentifier(runUuid: string): Promise<string>;
130
+ /**
131
+ * Returns the branch_name of the run's ticket, or '' when unresolved. Used
132
+ * by dispatch for the per-(branch,state) herdr tab model.
133
+ */
134
+ getRunTicketBranchName(runUuid: string): Promise<string>;
135
+ markRunning(runUuid: string, attrs?: RunWriteAttributes): Promise<void>;
136
+ /**
137
+ * Runs this conductor owns that are state=done but not yet closed — the
138
+ * conductor's one-shot finalisation work list.
139
+ */
140
+ fetchFinalizableRuns(conductorId: string): Promise<FinalizableRun[]>;
141
+ /**
142
+ * Finalise a run: set closed + closed_date and (when non-empty) log. No state
143
+ * change — the run is already done.
144
+ */
145
+ finalizeRun(uuid: string, log: string): Promise<void>;
146
+ /**
147
+ * Finished tickets (state=done OR closed) whose worktree is not yet torn
148
+ * down (cleaned_up=0) — the conductor's teardown work list (GAIA-89). Driven
149
+ * by the durable `cleaned_up` flag, NOT machine-scoped: the same query backs
150
+ * both the live tick and the standalone `gaia conductor reap`, so it catches
151
+ * orphans a conductor missed while down / after a restart / after a
152
+ * `conductor_id` reassignment. Empty once every finished ticket is cleaned.
153
+ */
154
+ fetchUncleanedTickets(): Promise<UncleanTicket[]>;
155
+ /**
156
+ * Close a ticket: set closed=true + closed_date. Ticket-lifecycle only, and
157
+ * decoupled from teardown — a done ticket is closed even if its worktree
158
+ * teardown later fails. No state change (the ticket is already done).
159
+ */
160
+ closeTicket(uuid: string): Promise<void>;
161
+ /**
162
+ * Mark a ticket's worktree torn down: set cleaned_up=true so it drops off the
163
+ * {@link fetchUncleanedTickets} work list. Written ONLY after a verified
164
+ * teardown, so a teardown miss leaves cleaned_up=0 and the next reconciliation
165
+ * retries — one miss never orphans the workspace, and a re-run on an
166
+ * already-cleaned ticket is a no-op (it is no longer on the list).
167
+ */
168
+ markTicketCleanedUp(uuid: string): Promise<void>;
169
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { WorkspacePlugin } from '../plugins.js';
2
+ import type { EnsuredWorkspace, GaiaWorkspace } from './workspace.js';
3
+ export declare class FakeWorkspace implements GaiaWorkspace {
4
+ ensure(identifier: string, _title?: string): Promise<EnsuredWorkspace>;
5
+ }
6
+ export declare function fakeWorkspace(): WorkspacePlugin;
@@ -0,0 +1,16 @@
1
+ export class FakeWorkspace {
2
+ async ensure(identifier, _title) {
3
+ return { path: `/fake/${identifier}`, instructions: null, created: true };
4
+ }
5
+ }
6
+ export function fakeWorkspace() {
7
+ const workspace = new FakeWorkspace();
8
+ return {
9
+ kind: 'workspace',
10
+ id: 'fake',
11
+ requiredModules: [],
12
+ async createWorkspace(_config) {
13
+ return workspace;
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,37 @@
1
+ import type { WorkspacePlugin } from '../plugins.js';
2
+ import type { EnsuredWorkspace, GaiaWorkspace } from './workspace.js';
3
+ /** Default branch template: `<prefix><key>-<title-slug>` (readable). */
4
+ export declare const DEFAULT_BRANCH_TEMPLATE = "{branchPrefix}{key}-{titleSlug}";
5
+ export type GitRunner = (args: string[], cwd: string) => Promise<void>;
6
+ export interface GitWorkspaceOptions {
7
+ /** Main clone (a git repo): config source + worktree base. */
8
+ root: string;
9
+ /** Where per-ticket worktrees live. Default: `${root}-worktrees`. */
10
+ worktreesRoot?: string;
11
+ /** Branch name prefix per ticket. Default: 'gaia/'. */
12
+ branchPrefix?: string;
13
+ /**
14
+ * Branch name template. Vars: {branchPrefix}, {key} (sanitized identifier),
15
+ * {identifier}, {titleSlug}. Default: `{branchPrefix}{key}-{titleSlug}`.
16
+ */
17
+ branchTemplate?: string;
18
+ runGit?: GitRunner;
19
+ }
20
+ export declare class GitWorkspace implements GaiaWorkspace {
21
+ private readonly runGit;
22
+ private readonly root;
23
+ private readonly worktreesRoot;
24
+ private readonly branchPrefix;
25
+ private readonly branchTemplate;
26
+ constructor(options: GitWorkspaceOptions);
27
+ /** Render the branch name for a ticket from the configured template. */
28
+ branchFor(identifier: string, title?: string): string;
29
+ keyFor(identifier: string): string;
30
+ private pathFor;
31
+ ensure(identifier: string, title?: string, _baseRef?: string): Promise<EnsuredWorkspace>;
32
+ remove(identifier: string): Promise<void>;
33
+ }
34
+ export declare function gitWorkspace(opts?: {
35
+ branchPrefix?: string;
36
+ branchTemplate?: string;
37
+ }): WorkspacePlugin;
@@ -0,0 +1,89 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { exec } from '../../core/exec.js';
4
+ import { slugify } from '../../core/slug.js';
5
+ import { loadInstructions } from './instructions.js';
6
+ /** Default branch template: `<prefix><key>-<title-slug>` (readable). */
7
+ export const DEFAULT_BRANCH_TEMPLATE = '{branchPrefix}{key}-{titleSlug}';
8
+ function renderBranchTemplate(template, vars) {
9
+ const rendered = template.replace(/\{(\w+)\}/g, (whole, key) => {
10
+ const value = vars[key];
11
+ return value === undefined ? whole : value;
12
+ });
13
+ // An empty title slug leaves a dangling separator (e.g. `gaia/GL-9-`) —
14
+ // trim trailing branch separators so the branch stays a valid, tidy ref.
15
+ return rendered.replace(/[-_./]+$/g, '');
16
+ }
17
+ const defaultGitRunner = async (args, cwd) => {
18
+ await exec('git', args, { cwd });
19
+ };
20
+ export class GitWorkspace {
21
+ runGit;
22
+ root;
23
+ worktreesRoot;
24
+ branchPrefix;
25
+ branchTemplate;
26
+ constructor(options) {
27
+ this.runGit = options.runGit ?? defaultGitRunner;
28
+ this.root = resolve(options.root);
29
+ this.worktreesRoot = options.worktreesRoot
30
+ ? resolve(options.worktreesRoot)
31
+ : `${this.root}-worktrees`;
32
+ this.branchPrefix = options.branchPrefix ?? 'gaia/';
33
+ this.branchTemplate = options.branchTemplate ?? DEFAULT_BRANCH_TEMPLATE;
34
+ }
35
+ /** Render the branch name for a ticket from the configured template. */
36
+ branchFor(identifier, title) {
37
+ return renderBranchTemplate(this.branchTemplate, {
38
+ branchPrefix: this.branchPrefix,
39
+ key: this.keyFor(identifier),
40
+ identifier,
41
+ titleSlug: slugify(title ?? ''),
42
+ });
43
+ }
44
+ keyFor(identifier) {
45
+ return identifier.replace(/[^A-Za-z0-9._-]/g, '_');
46
+ }
47
+ pathFor(key) {
48
+ return resolve(join(this.worktreesRoot, key));
49
+ }
50
+ async ensure(identifier, title, _baseRef) {
51
+ const key = this.keyFor(identifier);
52
+ // Reject empty and dot-leading keys: a dot-leading key would produce an
53
+ // invalid git ref (e.g. `gaia/.hidden`) and covers '.'/'..' too.
54
+ if (key === '' || key.startsWith('.')) {
55
+ throw new Error(`invalid workspace identifier: ${identifier}`);
56
+ }
57
+ const path = this.pathFor(key);
58
+ if (existsSync(path)) {
59
+ return { path, instructions: loadInstructions(path), created: false };
60
+ }
61
+ const branch = this.branchFor(identifier, title);
62
+ await this.runGit(['-C', this.root, 'worktree', 'add', '--force', '-B', branch, path], this.root);
63
+ // The `after_create` hook is run by the executor (GAIA-84), not here — the
64
+ // workspace only reports that it created a fresh worktree.
65
+ return { path, instructions: loadInstructions(path), created: true };
66
+ }
67
+ async remove(identifier) {
68
+ const key = this.keyFor(identifier);
69
+ const path = this.pathFor(key);
70
+ await this.runGit(['-C', this.root, 'worktree', 'remove', '--force', path], this.root);
71
+ }
72
+ }
73
+ export function gitWorkspace(opts = {}) {
74
+ return {
75
+ kind: 'workspace',
76
+ id: 'git',
77
+ requiredModules: [],
78
+ async createWorkspace(config) {
79
+ const root = config.config_path
80
+ ? dirname(config.config_path)
81
+ : process.cwd();
82
+ return new GitWorkspace({
83
+ root,
84
+ ...(opts.branchPrefix ? { branchPrefix: opts.branchPrefix } : {}),
85
+ ...(opts.branchTemplate ? { branchTemplate: opts.branchTemplate } : {}),
86
+ });
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,6 @@
1
+ /** Loads ./WORKFLOW.md from the workspace, hashing its content. */
2
+ export declare function loadInstructions(workspacePath: string): {
3
+ path: string;
4
+ sha256: string;
5
+ text: string;
6
+ } | null;
@@ -0,0 +1,16 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ /** Loads ./WORKFLOW.md from the workspace, hashing its content. */
5
+ export function loadInstructions(workspacePath) {
6
+ const path = resolve(workspacePath, 'WORKFLOW.md');
7
+ if (!existsSync(path)) {
8
+ return null;
9
+ }
10
+ const text = readFileSync(path, 'utf8');
11
+ return {
12
+ path,
13
+ sha256: createHash('sha256').update(text).digest('hex'),
14
+ text,
15
+ };
16
+ }
@@ -0,0 +1,35 @@
1
+ export interface EnsuredWorkspace {
2
+ path: string;
3
+ instructions: {
4
+ path: string;
5
+ sha256: string;
6
+ text: string;
7
+ } | null;
8
+ /**
9
+ * Whether this call CREATED the worktree (true) or reused an existing one
10
+ * (false). The core runs the `after_create` lifecycle hook only on a fresh
11
+ * worktree, so it needs to know which happened. Lifecycle hooks themselves
12
+ * are no longer a workspace concern (GAIA-84): the executor owns and runs
13
+ * them best-effort.
14
+ */
15
+ created: boolean;
16
+ }
17
+ export interface GaiaWorkspace {
18
+ /**
19
+ * Ensure a per-ticket worktree exists. `branch`, when given, is the branch
20
+ * name the dispatcher computed for the ticket. The git plugin treats it as a
21
+ * readable title to slug into its branch template; the herdr plugin uses it
22
+ * verbatim as the worktree branch. The worktree directory key/path stays
23
+ * identifier- or branch-derived so reuse is stable across runs.
24
+ *
25
+ * `baseRef`, when given, overrides the base the new branch is created from
26
+ * (e.g. `origin/<base_branch>` so a sub-ticket stacks on its parent); if it
27
+ * does not resolve on the remote the implementation falls back to its default
28
+ * base — never a hard error.
29
+ *
30
+ * Reports `created` so the caller can run the `after_create` hook only on a
31
+ * fresh worktree. Lifecycle-hook invocation is NOT a workspace responsibility
32
+ * anymore — the executor owns all hooks (GAIA-84).
33
+ */
34
+ ensure(identifier: string, branch?: string, baseRef?: string): Promise<EnsuredWorkspace>;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from './plugins/registry-exports.js';
@@ -0,0 +1 @@
1
+ export * from './plugins/registry-exports.js';
@@ -0,0 +1,51 @@
1
+ import type { DropSHPlugin } from 'dropsh/plugin';
2
+ import type { AgentPlugin, ExecutorPlugin, RemotePlugin, WorkspacePlugin } from './plugins/plugins.js';
3
+ /** Normalized single conductor. */
4
+ export interface ConductorSettings {
5
+ /** Default: `${project} @ ${checkoutRoot}`. */
6
+ label: string;
7
+ /**
8
+ * Stable node identity (gaia_conductor.machine_id) this process registers as.
9
+ * Defaults to a hash of hostname + checkout path. Set it to pin a conductor to
10
+ * a known identity — e.g. so a ticket can be pre-assigned to it (conductor
11
+ * assignment is the run-start trigger), which the e2e fixtures rely on.
12
+ */
13
+ machine_id?: string;
14
+ /** Project name (gaia_project.name); resolved at registration. */
15
+ project: string;
16
+ /** Workflow states this conductor serves. */
17
+ states: string[];
18
+ /**
19
+ * Agent prompt template - the GAIA run contract, NOT project workflow. Bounds
20
+ * the agent to exactly one state and tells it to release + stop, keeping run
21
+ * mechanics out of the repo's WORKFLOW.md. Placeholders: `{identifier}`,
22
+ * `{state}`, `{runUuid}`. Defaults to `DEFAULT_AGENT_PROMPT`.
23
+ */
24
+ prompt: string;
25
+ /** Default: 1. */
26
+ max_parallel: number;
27
+ poll_interval_ms: number;
28
+ lease_seconds: number;
29
+ hooks?: {
30
+ after_create?: string;
31
+ before_run?: string;
32
+ after_run?: string;
33
+ after_done?: string;
34
+ };
35
+ /** Control-plane site settings used for dropsh and agent env. */
36
+ site: {
37
+ base_url: string;
38
+ jsonapi_prefix: string;
39
+ };
40
+ /** Absolute path of the loaded config. */
41
+ config_path: string;
42
+ }
43
+ /** Whole conductor.config.js (dropsh-superset): site + named plugin slots. */
44
+ export interface ConductorFileConfig extends ConductorSettings {
45
+ remote: RemotePlugin;
46
+ executor: ExecutorPlugin;
47
+ agent: AgentPlugin;
48
+ workspace: WorkspacePlugin;
49
+ /** dropsh-layer plugins (auth etc.); separate from the GAIA slots. */
50
+ plugins?: DropSHPlugin[];
51
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@gaia-ai/core",
3
+ "version": "0.4.0",
4
+ "description": "GAIA global contract: plugin API, built-in remotes/workspaces/auth, shared primitives.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20.19"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/src/index.js",
12
+ "./plugins": "./dist/src/plugins-index.js",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist/src",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://git.key-tec.de/keytec/gaia.git",
26
+ "directory": "conductor/packages/core"
27
+ },
28
+ "dependencies": {
29
+ "dropsh": "^0.5.1",
30
+ "pino": "^9.6.0",
31
+ "pino-pretty": "^13.0.0"
32
+ }
33
+ }