@gaia-ai/core 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.
- package/README.md +2 -2
- package/dist/src/cli/commands.d.ts +27 -0
- package/dist/src/cli/gaia-dir.d.ts +51 -0
- package/dist/src/cli/gaia-dir.js +152 -0
- package/dist/src/cli/load-gaia-config.d.ts +25 -0
- package/dist/src/cli/load-gaia-config.js +117 -0
- package/dist/src/cli/machine-context.d.ts +24 -0
- package/dist/src/cli/machine-context.js +45 -0
- package/dist/src/cli/paths.d.ts +11 -0
- package/dist/src/cli/paths.js +31 -0
- package/dist/src/cli/resolve-module.d.ts +6 -0
- package/dist/src/cli/resolve-module.js +24 -0
- package/dist/src/conductor-registry/index.d.ts +23 -0
- package/dist/src/conductor-registry/index.js +59 -0
- package/dist/src/core/exec.d.ts +1 -1
- package/dist/src/core/exec.js +1 -1
- package/dist/src/index.d.ts +11 -8
- package/dist/src/index.js +23 -3
- package/dist/src/plugins/discover-addons.d.ts +33 -0
- package/dist/src/plugins/discover-addons.js +238 -0
- package/dist/src/plugins/preset.d.ts +94 -0
- package/dist/src/plugins/preset.js +52 -0
- package/dist/src/workflow/step-contract.d.ts +119 -0
- package/dist/src/workflow/step-contract.js +430 -0
- package/package.json +11 -6
- package/dist/src/plugins/agent/agent.d.ts +0 -61
- package/dist/src/plugins/agent/agent.js +0 -11
- package/dist/src/plugins/auth/basic.d.ts +0 -11
- package/dist/src/plugins/auth/basic.js +0 -35
- package/dist/src/plugins/executor/executor.d.ts +0 -104
- package/dist/src/plugins/plugins.d.ts +0 -60
- package/dist/src/plugins/plugins.js +0 -42
- package/dist/src/plugins/registry-exports.d.ts +0 -6
- package/dist/src/plugins/registry-exports.js +0 -6
- package/dist/src/plugins/remote/drupal.d.ts +0 -35
- package/dist/src/plugins/remote/drupal.js +0 -369
- package/dist/src/plugins/remote/fake.d.ts +0 -109
- package/dist/src/plugins/remote/fake.js +0 -243
- package/dist/src/plugins/remote/remote.d.ts +0 -193
- package/dist/src/plugins/remote/remote.js +0 -1
- package/dist/src/plugins/workspace/fake.d.ts +0 -6
- package/dist/src/plugins/workspace/fake.js +0 -16
- package/dist/src/plugins/workspace/git.d.ts +0 -37
- package/dist/src/plugins/workspace/git.js +0 -89
- package/dist/src/plugins/workspace/instructions.d.ts +0 -6
- package/dist/src/plugins/workspace/instructions.js +0 -16
- package/dist/src/plugins/workspace/workspace.d.ts +0 -35
- package/dist/src/plugins/workspace/workspace.js +0 -1
- package/dist/src/plugins-index.d.ts +0 -1
- package/dist/src/plugins-index.js +0 -1
- package/dist/src/types.d.ts +0 -52
- package/dist/src/types.js +0 -1
- /package/dist/src/{plugins/executor/executor.js → cli/commands.js} +0 -0
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
export interface SpawnedSession {
|
|
2
|
-
sessionRef: string;
|
|
3
|
-
}
|
|
4
|
-
export interface ExecutorCapabilities {
|
|
5
|
-
persistent: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface SpawnRunInput {
|
|
8
|
-
ticket: {
|
|
9
|
-
uuid: string;
|
|
10
|
-
identifier: string;
|
|
11
|
-
title: string;
|
|
12
|
-
branchName: string;
|
|
13
|
-
state: string;
|
|
14
|
-
url?: string;
|
|
15
|
-
};
|
|
16
|
-
run: {
|
|
17
|
-
uuid: string;
|
|
18
|
-
id: number;
|
|
19
|
-
handler: string;
|
|
20
|
-
};
|
|
21
|
-
workspacePath: string;
|
|
22
|
-
instructions: {
|
|
23
|
-
path: string;
|
|
24
|
-
sha256: string;
|
|
25
|
-
text: string;
|
|
26
|
-
} | null;
|
|
27
|
-
command?: string;
|
|
28
|
-
env?: Record<string, string>;
|
|
29
|
-
}
|
|
30
|
-
/** The four lifecycle-hook slots, keyed exactly like `config.hooks`. */
|
|
31
|
-
export type HookName = 'after_create' | 'before_run' | 'after_run' | 'after_done';
|
|
32
|
-
/** Context for a hook invocation — carries the ticket for observable logging. */
|
|
33
|
-
export interface HookContext {
|
|
34
|
-
/** The ticket identifier/uuid the hook is running for (log context). */
|
|
35
|
-
ticket: string;
|
|
36
|
-
}
|
|
37
|
-
export interface GaiaExecutor {
|
|
38
|
-
id: string;
|
|
39
|
-
capabilities(): ExecutorCapabilities;
|
|
40
|
-
/**
|
|
41
|
-
* Run the lifecycle hook `name` (command from `config.hooks[name]`) in `cwd`,
|
|
42
|
-
* best-effort. This is the SINGLE catch point for all lifecycle hooks:
|
|
43
|
-
*
|
|
44
|
-
* - MUST NEVER throw. No configured command → silent no-op. A failing command
|
|
45
|
-
* → `logger.error({hook,worktree,ticket,err}, 'lifecycle hook failed')` then
|
|
46
|
-
* return. So a hook failure never aborts dispatch or wedges a run in
|
|
47
|
-
* `claimed`.
|
|
48
|
-
* - The underlying shell command still fails honestly (a non-zero exit
|
|
49
|
-
* rejects); only the executor catches + logs + continues.
|
|
50
|
-
*
|
|
51
|
-
* `env`, when given, is the run's resolved environment (per-ticket env_vars
|
|
52
|
-
* merged with the core GAIA_* vars, GAIA-99), merged over the hook process's
|
|
53
|
-
* inherited env. Values may be sensitive — implementations must never log
|
|
54
|
-
* them (log key names only).
|
|
55
|
-
*/
|
|
56
|
-
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<void>;
|
|
57
|
-
startRun(input: SpawnRunInput): Promise<SpawnedSession>;
|
|
58
|
-
/**
|
|
59
|
-
* Signal the run's agent to stop. Called by the conductor's run-finalise pass
|
|
60
|
-
* BEFORE it captures the agent transcript (GAIA-132), so a stop must not
|
|
61
|
-
* destroy the log. For herdr this is a NO-OP: at finalise time the agent is
|
|
62
|
-
* idle and its jsonl transcript is already fully written to disk, and the
|
|
63
|
-
* subsequent {@link cleanupRun} tab-close kills the PTY (the agent dies with
|
|
64
|
-
* it). The seam exists for executors whose agent outlives its UI surface.
|
|
65
|
-
* Best-effort; gated by the conductor on `capabilities().persistent`.
|
|
66
|
-
*/
|
|
67
|
-
stopRun(branch: string): Promise<void>;
|
|
68
|
-
/**
|
|
69
|
-
* Tear down ONE run's hosted UI surface — for herdr, close only the tab whose
|
|
70
|
-
* label carries the `#<runId>` token (`startRun` labels every tab
|
|
71
|
-
* `<identifier> · <state> #<run.id>`), via `herdr tab close <tab_id>` (killing
|
|
72
|
-
* that PTY). Run-scoped by design (GAIA-183): a sibling run's tab in the same
|
|
73
|
-
* branch workspace — a still-open prior-state run or a concurrent re-claim —
|
|
74
|
-
* and any non-run tab are left untouched. Called as the LAST finalisation step,
|
|
75
|
-
* strictly after the transcript is captured, and again at dispatch to clear a
|
|
76
|
-
* reused workspace's leftover tab for the run being (re-)started. No matching
|
|
77
|
-
* tab → a quiet no-op. Does NOT touch the branch worktree (that is the
|
|
78
|
-
* reap/{@link removeWorktree} lifecycle). Best-effort: a failing tab-close is
|
|
79
|
-
* swallowed and never aborts finalisation or dispatch. A no-op for
|
|
80
|
-
* non-persistent executors.
|
|
81
|
-
*/
|
|
82
|
-
cleanupRun(branch: string, runId: number): Promise<void>;
|
|
83
|
-
/**
|
|
84
|
-
* Tear down the branch's entire worktree (git worktree + hosted workspace),
|
|
85
|
-
* reclaiming its disk. Called by the conductor's ticket-cleanup pass once a
|
|
86
|
-
* ticket is done. A no-op for non-persistent executors (no hosted workspace);
|
|
87
|
-
* the conductor gates the call on `capabilities().persistent`.
|
|
88
|
-
*
|
|
89
|
-
* `worktreePath` is the stable, identifier-derived worktree path (from the
|
|
90
|
-
* ticket's latest run). Prefer it over `branch` to resolve the worktree: the
|
|
91
|
-
* checked-out branch is mutable (the coding agent may rename/switch it), the
|
|
92
|
-
* path is not.
|
|
93
|
-
*
|
|
94
|
-
* Returns whether the worktree was actually present on THIS host and torn
|
|
95
|
-
* down (or reclaimed on disk) — i.e. whether the teardown happened locally.
|
|
96
|
-
* `false` means nothing matched here: the worktree is either already gone or
|
|
97
|
-
* lives on another conductor's host. The reaper uses this to avoid marking a
|
|
98
|
-
* ticket `cleaned_up` for a worktree it did not actually tear down (a
|
|
99
|
-
* cross-host false-teardown), leaving it for the host that physically holds
|
|
100
|
-
* it. A genuine failure still throws (surfaced as a teardown miss); a `false`
|
|
101
|
-
* return is a clean "not here", not an error.
|
|
102
|
-
*/
|
|
103
|
-
removeWorktree(branch: string, worktreePath?: string): Promise<boolean>;
|
|
104
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { DropSHPlugin } from 'dropsh/plugin';
|
|
2
|
-
import type { ConductorLogger } from '../core/logger.js';
|
|
3
|
-
import type { ConductorFileConfig } from '../types.js';
|
|
4
|
-
import type { GaiaAgent } from './agent/agent.js';
|
|
5
|
-
import type { GaiaExecutor } from './executor/executor.js';
|
|
6
|
-
import type { GaiaRemote, Ticket } from './remote/remote.js';
|
|
7
|
-
import type { GaiaWorkspace } from './workspace/workspace.js';
|
|
8
|
-
/** Runtime dependencies handed to a plugin factory at selection time. */
|
|
9
|
-
export interface ExecutorDeps {
|
|
10
|
-
/** The in-scope conductor logger — the executor logs hook failures through it. */
|
|
11
|
-
logger: ConductorLogger;
|
|
12
|
-
}
|
|
13
|
-
export interface RemotePlugin extends DropSHPlugin {
|
|
14
|
-
readonly kind: 'remote';
|
|
15
|
-
readonly id: string;
|
|
16
|
-
readonly requiredModules: string[];
|
|
17
|
-
createRemote(config: ConductorFileConfig): Promise<GaiaRemote>;
|
|
18
|
-
}
|
|
19
|
-
export interface ExecutorPlugin extends DropSHPlugin {
|
|
20
|
-
readonly kind: 'executor';
|
|
21
|
-
readonly id: string;
|
|
22
|
-
readonly requiredModules: string[];
|
|
23
|
-
createExecutor(config: ConductorFileConfig, deps: ExecutorDeps): Promise<GaiaExecutor>;
|
|
24
|
-
}
|
|
25
|
-
export interface WorkspacePlugin extends DropSHPlugin {
|
|
26
|
-
readonly kind: 'workspace';
|
|
27
|
-
readonly id: string;
|
|
28
|
-
readonly requiredModules: string[];
|
|
29
|
-
createWorkspace(config: ConductorFileConfig): Promise<GaiaWorkspace>;
|
|
30
|
-
}
|
|
31
|
-
export interface AgentPlugin extends DropSHPlugin {
|
|
32
|
-
readonly kind: 'agent';
|
|
33
|
-
readonly id: string;
|
|
34
|
-
readonly requiredModules: string[];
|
|
35
|
-
createAgent(config: ConductorFileConfig): Promise<GaiaAgent>;
|
|
36
|
-
}
|
|
37
|
-
/** Resolves the remote from its named config slot. */
|
|
38
|
-
export declare function selectRemote(config: ConductorFileConfig): Promise<GaiaRemote>;
|
|
39
|
-
/** Resolves the executor from its named config slot, injecting the logger. */
|
|
40
|
-
export declare function selectExecutor(config: ConductorFileConfig, logger: ConductorLogger): Promise<GaiaExecutor>;
|
|
41
|
-
/** Resolves the workspace from its named config slot. */
|
|
42
|
-
export declare function selectWorkspace(config: ConductorFileConfig): Promise<GaiaWorkspace>;
|
|
43
|
-
/** A config-side agent choice: an agent plugin + an optional static priority over the ticket. */
|
|
44
|
-
export interface AgentCandidate {
|
|
45
|
-
agent: AgentPlugin;
|
|
46
|
-
priority?: (ticket: Ticket) => number;
|
|
47
|
-
}
|
|
48
|
-
/** A constructed candidate: the agent id, its GaiaAgent, and the config-side priority. */
|
|
49
|
-
export interface ResolvedAgent {
|
|
50
|
-
id: string;
|
|
51
|
-
agent: GaiaAgent;
|
|
52
|
-
priority?: (ticket: Ticket) => number;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Pick the agent for a ticket: highest `priority(ticket)` wins; a candidate with
|
|
56
|
-
* no `priority` scores lowest; ties resolve by config array order (stable sort).
|
|
57
|
-
*/
|
|
58
|
-
export declare function selectAgent(candidates: ResolvedAgent[], ticket: Ticket): ResolvedAgent;
|
|
59
|
-
/** Constructs every agent candidate from the resolved config slot (single or array). */
|
|
60
|
-
export declare function selectAgents(config: ConductorFileConfig): Promise<ResolvedAgent[]>;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/** Resolves the remote from its named config slot. */
|
|
2
|
-
export async function selectRemote(config) {
|
|
3
|
-
return config.remote.createRemote(config);
|
|
4
|
-
}
|
|
5
|
-
/** Resolves the executor from its named config slot, injecting the logger. */
|
|
6
|
-
export async function selectExecutor(config, logger) {
|
|
7
|
-
return config.executor.createExecutor(config, { logger });
|
|
8
|
-
}
|
|
9
|
-
/** Resolves the workspace from its named config slot. */
|
|
10
|
-
export async function selectWorkspace(config) {
|
|
11
|
-
return config.workspace.createWorkspace(config);
|
|
12
|
-
}
|
|
13
|
-
/** A missing `priority` scores strictly below any real number (AC-4). */
|
|
14
|
-
const DEFAULT_PRIORITY = Number.NEGATIVE_INFINITY;
|
|
15
|
-
/**
|
|
16
|
-
* Pick the agent for a ticket: highest `priority(ticket)` wins; a candidate with
|
|
17
|
-
* no `priority` scores lowest; ties resolve by config array order (stable sort).
|
|
18
|
-
*/
|
|
19
|
-
export function selectAgent(candidates, ticket) {
|
|
20
|
-
const best = candidates
|
|
21
|
-
.map((c, index) => ({
|
|
22
|
-
c,
|
|
23
|
-
index,
|
|
24
|
-
score: c.priority ? c.priority(ticket) : DEFAULT_PRIORITY,
|
|
25
|
-
}))
|
|
26
|
-
.sort((a, b) => b.score - a.score || a.index - b.index)[0];
|
|
27
|
-
if (!best) {
|
|
28
|
-
throw new Error('selectAgent: no agent candidates configured');
|
|
29
|
-
}
|
|
30
|
-
return best.c;
|
|
31
|
-
}
|
|
32
|
-
/** Constructs every agent candidate from the resolved config slot (single or array). */
|
|
33
|
-
export async function selectAgents(config) {
|
|
34
|
-
const candidates = Array.isArray(config.agent)
|
|
35
|
-
? config.agent
|
|
36
|
-
: [config.agent];
|
|
37
|
-
return Promise.all(candidates.map(async (c) => ({
|
|
38
|
-
id: c.agent.id,
|
|
39
|
-
agent: await c.agent.createAgent(config),
|
|
40
|
-
...(c.priority ? { priority: c.priority } : {}),
|
|
41
|
-
})));
|
|
42
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { basicAuthProvider } from './auth/basic.js';
|
|
2
|
-
export { type AgentCandidate, type ResolvedAgent, selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins.js';
|
|
3
|
-
export { DrupalGaiaRemote, drupalRemote } from './remote/drupal.js';
|
|
4
|
-
export { FakeGaiaRemote, type FakeRemoteSeed, fakeRemote, } from './remote/fake.js';
|
|
5
|
-
export { FakeWorkspace, fakeWorkspace } from './workspace/fake.js';
|
|
6
|
-
export { GitWorkspace, gitWorkspace } from './workspace/git.js';
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { basicAuthProvider } from './auth/basic.js';
|
|
2
|
-
export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins.js';
|
|
3
|
-
export { DrupalGaiaRemote, drupalRemote } from './remote/drupal.js';
|
|
4
|
-
export { FakeGaiaRemote, fakeRemote, } from './remote/fake.js';
|
|
5
|
-
export { FakeWorkspace, fakeWorkspace } from './workspace/fake.js';
|
|
6
|
-
export { GitWorkspace, gitWorkspace } from './workspace/git.js';
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { JsonApiClient } from 'dropsh/plugin';
|
|
2
|
-
import type { RemotePlugin } from '../plugins.js';
|
|
3
|
-
import type { ActiveRun, ClaimedRun, ClaimOptions, ConductorRegistration, ConductorStatus, FinalizableRun, GaiaRemote, RunMetrics, RunWriteAttributes, Ticket, UncleanTicket } from './remote.js';
|
|
4
|
-
export declare class DrupalGaiaRemote implements GaiaRemote {
|
|
5
|
-
private readonly api;
|
|
6
|
-
constructor(api: JsonApiClient);
|
|
7
|
-
fetchActiveRuns(id: string): Promise<ActiveRun[]>;
|
|
8
|
-
activeRunCount(id: string): Promise<number>;
|
|
9
|
-
claimNext(c: ClaimOptions): Promise<ClaimedRun | null>;
|
|
10
|
-
getTicket(uuid: string): Promise<Ticket>;
|
|
11
|
-
getRunWorktree(uuid: string): Promise<string>;
|
|
12
|
-
getRunTicketIdentifier(uuid: string): Promise<string>;
|
|
13
|
-
getRunTicketBranchName(uuid: string): Promise<string>;
|
|
14
|
-
registerConductor(reg: ConductorRegistration): Promise<string>;
|
|
15
|
-
heartbeat(reg: ConductorRegistration, load: number, lease?: number): Promise<string>;
|
|
16
|
-
getConductorStatus(id: string): Promise<string | null>;
|
|
17
|
-
setConductorStatus(id: string, status: string): Promise<void>;
|
|
18
|
-
listConductors(owner?: 'me'): Promise<ConductorStatus[]>;
|
|
19
|
-
markRunning(uuid: string, attrs?: RunWriteAttributes): Promise<void>;
|
|
20
|
-
markFailed(uuid: string, errorLog: string): Promise<void>;
|
|
21
|
-
fetchFinalizableRuns(id: string): Promise<FinalizableRun[]>;
|
|
22
|
-
finalizeRun(uuid: string, log: string, metrics?: RunMetrics): Promise<void>;
|
|
23
|
-
fetchUncleanedTickets(id: string): Promise<UncleanTicket[]>;
|
|
24
|
-
closeTicket(uuid: string): Promise<void>;
|
|
25
|
-
markTicketCleanedUp(uuid: string): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* Absolute worktree path of the ticket's latest run (highest run id with a
|
|
28
|
-
* non-empty worktree_path), or '' when none — the cwd the cleanup command
|
|
29
|
-
* runs in. The conductor persists worktree_path on markRunning, so a done
|
|
30
|
-
* ticket's run carries the path even after the run closed.
|
|
31
|
-
*/
|
|
32
|
-
private latestRunWorktree;
|
|
33
|
-
private projectUuid;
|
|
34
|
-
}
|
|
35
|
-
export declare function drupalRemote(): RemotePlugin;
|
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { resolveAuth } from 'dropsh';
|
|
2
|
-
import { createHttpClient, createJsonApiClient } from 'dropsh/plugin';
|
|
3
|
-
const ACTIVE = ['claimed', 'running'];
|
|
4
|
-
export class DrupalGaiaRemote {
|
|
5
|
-
api;
|
|
6
|
-
constructor(api) {
|
|
7
|
-
this.api = api;
|
|
8
|
-
}
|
|
9
|
-
async fetchActiveRuns(id) {
|
|
10
|
-
// The conductor identity `id` is the machine_id (hash of hostname+path).
|
|
11
|
-
// gaia_run links to its owning conductor via the `conductor_id` entity-ref;
|
|
12
|
-
// filter on the related conductor's `machine_id` (nested relationship filter).
|
|
13
|
-
const rows = await this.api
|
|
14
|
-
.collection('gaia_run')
|
|
15
|
-
.where('conductor_id.machine_id', '=', id)
|
|
16
|
-
.whereIn('state', ACTIVE)
|
|
17
|
-
.fields(['state', 'state_at_start', 'worktree_path'])
|
|
18
|
-
.page(100)
|
|
19
|
-
.list();
|
|
20
|
-
return Promise.all(rows.map(async (r) => ({
|
|
21
|
-
runUuid: r.id,
|
|
22
|
-
ticketUuid: r.rel('ticket_id') ?? '',
|
|
23
|
-
ticketIdentifier: await this.getRunTicketIdentifier(r.id),
|
|
24
|
-
branchName: await this.getRunTicketBranchName(r.id),
|
|
25
|
-
state: r.attr('state') ?? '',
|
|
26
|
-
stateAtStart: r.attr('state_at_start') ?? '',
|
|
27
|
-
worktreePath: r.attr('worktree_path') ?? '',
|
|
28
|
-
})));
|
|
29
|
-
}
|
|
30
|
-
async activeRunCount(id) {
|
|
31
|
-
// Filter on the related conductor's `machine_id` (nested relationship filter).
|
|
32
|
-
return this.api
|
|
33
|
-
.collection('gaia_run')
|
|
34
|
-
.where('conductor_id.machine_id', '=', id)
|
|
35
|
-
.whereIn('state', ACTIVE)
|
|
36
|
-
.fields(['drupal_internal__id'])
|
|
37
|
-
.page(100)
|
|
38
|
-
.count();
|
|
39
|
-
}
|
|
40
|
-
async claimNext(c) {
|
|
41
|
-
const res = (await this.api.post('gaia/claim-next', {
|
|
42
|
-
data: {
|
|
43
|
-
attributes: {
|
|
44
|
-
lease_seconds: c.leaseSeconds,
|
|
45
|
-
// Server (`ConductorResolverTrait`) narrows to the caller's conductor
|
|
46
|
-
// by the `machine_id` attribute; `c.conductorId` IS the machine_id.
|
|
47
|
-
machine_id: c.conductorId,
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
}));
|
|
51
|
-
if (!res?.data) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const d = res.data;
|
|
55
|
-
const rawId = d.attributes?.drupal_internal__id;
|
|
56
|
-
if (rawId === undefined || rawId === null) {
|
|
57
|
-
throw new Error(`claimNext: missing drupal_internal__id on run ${d.id}`);
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
runUuid: d.id,
|
|
61
|
-
runId: Number(rawId),
|
|
62
|
-
ticketUuid: d.relationships?.ticket_id?.data?.id ?? '',
|
|
63
|
-
stateAtStart: String(d.attributes?.state_at_start ?? ''),
|
|
64
|
-
// handler_id was dropped; the handler is the work the run does, i.e. the
|
|
65
|
-
// ticket state the run started in.
|
|
66
|
-
handler: String(d.attributes?.state_at_start ?? ''),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
async getTicket(uuid) {
|
|
70
|
-
// The agent self-reads the ticket + its comments at run start (GAIA-112),
|
|
71
|
-
// so getTicket needs no ?include=comments — but it DOES sideload labels +
|
|
72
|
-
// environments (GAIA-144) so a config-side priority(ticket) can read them
|
|
73
|
-
// without fetching (AC-5).
|
|
74
|
-
const doc = (await this.api.get(`gaia_ticket/gaia_ticket/${uuid}?include=labels,environments`));
|
|
75
|
-
const attrs = doc.data?.attributes ?? {};
|
|
76
|
-
const issueUrl = typeof attrs.origin === 'string' ? attrs.origin : undefined;
|
|
77
|
-
const included = Array.isArray(doc.included) ? doc.included : [];
|
|
78
|
-
const byId = new Map(included.map((r) => [`${r.type}:${r.id}`, r]));
|
|
79
|
-
const refs = (name) => {
|
|
80
|
-
const data = doc.data?.relationships?.[name]?.data;
|
|
81
|
-
return Array.isArray(data) ? data : [];
|
|
82
|
-
};
|
|
83
|
-
const labels = refs('labels')
|
|
84
|
-
.map((ref) => byId.get(`${ref.type}:${ref.id}`)?.attributes?.name)
|
|
85
|
-
.filter((n) => typeof n === 'string');
|
|
86
|
-
const environments = refs('environments')
|
|
87
|
-
.map((ref) => {
|
|
88
|
-
const a = byId.get(`${ref.type}:${ref.id}`)?.attributes ?? {};
|
|
89
|
-
return {
|
|
90
|
-
name: typeof a.name === 'string' ? a.name : '',
|
|
91
|
-
tier: typeof a.tier === 'string' ? a.tier : '',
|
|
92
|
-
};
|
|
93
|
-
})
|
|
94
|
-
.filter((e) => e.name !== '');
|
|
95
|
-
return {
|
|
96
|
-
uuid: doc.data?.id ?? uuid,
|
|
97
|
-
identifier: typeof attrs.identifier === 'string' ? attrs.identifier : uuid,
|
|
98
|
-
title: typeof attrs.title === 'string' ? attrs.title : '',
|
|
99
|
-
state: typeof attrs.state === 'string' ? attrs.state : '',
|
|
100
|
-
branchName: typeof attrs.branch_name === 'string' ? attrs.branch_name : '',
|
|
101
|
-
...(typeof attrs.base_branch === 'string' && attrs.base_branch !== ''
|
|
102
|
-
? { baseBranch: attrs.base_branch }
|
|
103
|
-
: {}),
|
|
104
|
-
...(typeof attrs.effective_env_vars === 'string' &&
|
|
105
|
-
attrs.effective_env_vars !== ''
|
|
106
|
-
? { effectiveEnvVars: attrs.effective_env_vars }
|
|
107
|
-
: {}),
|
|
108
|
-
...(issueUrl ? { issueUrl } : {}),
|
|
109
|
-
labels,
|
|
110
|
-
environments,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
async getRunWorktree(uuid) {
|
|
114
|
-
const r = await this.api.resource('gaia_run', uuid);
|
|
115
|
-
return r.attr('worktree_path') ?? '';
|
|
116
|
-
}
|
|
117
|
-
async getRunTicketIdentifier(uuid) {
|
|
118
|
-
const run = await this.api.resource('gaia_run', uuid);
|
|
119
|
-
const ticketUuid = run.rel('ticket_id');
|
|
120
|
-
if (!ticketUuid) {
|
|
121
|
-
return '';
|
|
122
|
-
}
|
|
123
|
-
const ticket = await this.api.resource('gaia_ticket', ticketUuid);
|
|
124
|
-
return ticket.attr('identifier') ?? '';
|
|
125
|
-
}
|
|
126
|
-
async getRunTicketBranchName(uuid) {
|
|
127
|
-
const run = await this.api.resource('gaia_run', uuid);
|
|
128
|
-
const ticketUuid = run.rel('ticket_id');
|
|
129
|
-
if (!ticketUuid) {
|
|
130
|
-
return '';
|
|
131
|
-
}
|
|
132
|
-
const ticket = await this.api.resource('gaia_ticket', ticketUuid);
|
|
133
|
-
return ticket.attr('branch_name') ?? '';
|
|
134
|
-
}
|
|
135
|
-
async registerConductor(reg) {
|
|
136
|
-
const r = await this.api.upsert('gaia_conductor', { path: 'machine_id', value: reg.id }, {
|
|
137
|
-
attributes: {
|
|
138
|
-
machine_id: reg.id,
|
|
139
|
-
status: 'online',
|
|
140
|
-
workspace_root: reg.workspace,
|
|
141
|
-
label: reg.label,
|
|
142
|
-
states: reg.states,
|
|
143
|
-
max_parallel: reg.max_parallel,
|
|
144
|
-
},
|
|
145
|
-
relationships: {
|
|
146
|
-
owner_user_id: {
|
|
147
|
-
data: { type: 'user--user', id: await this.api.me() },
|
|
148
|
-
},
|
|
149
|
-
project_id: {
|
|
150
|
-
data: {
|
|
151
|
-
type: 'gaia_project--gaia_project',
|
|
152
|
-
id: await this.projectUuid(reg.project),
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
return r.id; // drupal uuid
|
|
158
|
-
}
|
|
159
|
-
async heartbeat(reg, load, lease = 300) {
|
|
160
|
-
// Server-side upsert by machine_id (see HeartbeatResource): never 404s,
|
|
161
|
-
// heals a vanished registration, and returns the resulting status. Sending
|
|
162
|
-
// the full registration lets the server recreate the entity when missing.
|
|
163
|
-
const res = (await this.api.post('gaia/heartbeat', {
|
|
164
|
-
data: {
|
|
165
|
-
attributes: {
|
|
166
|
-
machine_id: reg.id,
|
|
167
|
-
project: reg.project,
|
|
168
|
-
states: reg.states,
|
|
169
|
-
workspace_root: reg.workspace,
|
|
170
|
-
label: reg.label,
|
|
171
|
-
max_parallel: reg.max_parallel,
|
|
172
|
-
current_load: load,
|
|
173
|
-
lease_seconds: lease,
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
}));
|
|
177
|
-
const status = res?.data?.attributes?.status;
|
|
178
|
-
return typeof status === 'string' ? status : 'online';
|
|
179
|
-
}
|
|
180
|
-
async getConductorStatus(id) {
|
|
181
|
-
const c = await this.api
|
|
182
|
-
.collection('gaia_conductor')
|
|
183
|
-
.where('machine_id', '=', id)
|
|
184
|
-
.fields(['status'])
|
|
185
|
-
.first();
|
|
186
|
-
return c ? (c.attr('status') ?? null) : null;
|
|
187
|
-
}
|
|
188
|
-
async setConductorStatus(id, status) {
|
|
189
|
-
const c = await this.api
|
|
190
|
-
.collection('gaia_conductor')
|
|
191
|
-
.where('machine_id', '=', id)
|
|
192
|
-
.first();
|
|
193
|
-
if (c) {
|
|
194
|
-
await this.api.update('gaia_conductor', c.id, { attributes: { status } });
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async listConductors(owner) {
|
|
198
|
-
let col = this.api.collection('gaia_conductor');
|
|
199
|
-
if (owner === 'me') {
|
|
200
|
-
col = col.where('owner_user_id.id', '=', await this.api.me());
|
|
201
|
-
}
|
|
202
|
-
const rows = await col.page(200).list();
|
|
203
|
-
return rows.map((r) => ({
|
|
204
|
-
id: r.attr('machine_id') ?? r.id,
|
|
205
|
-
project: '',
|
|
206
|
-
label: r.attr('label') ?? '',
|
|
207
|
-
status: r.attr('status') ?? '',
|
|
208
|
-
lastSeen: r.attr('last_seen') ?? 0,
|
|
209
|
-
load: r.attr('current_load') ?? 0,
|
|
210
|
-
}));
|
|
211
|
-
}
|
|
212
|
-
async markRunning(uuid, attrs) {
|
|
213
|
-
const t = Math.floor(Date.now() / 1000);
|
|
214
|
-
await this.api.update('gaia_run', uuid, {
|
|
215
|
-
attributes: {
|
|
216
|
-
state: 'running',
|
|
217
|
-
heartbeat: t,
|
|
218
|
-
...(attrs?.worktree_path ? { worktree_path: attrs.worktree_path } : {}),
|
|
219
|
-
...(attrs?.agent ? { agent: attrs.agent } : {}),
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
async markFailed(uuid, errorLog) {
|
|
224
|
-
const t = Math.floor(Date.now() / 1000);
|
|
225
|
-
await this.api.update('gaia_run', uuid, {
|
|
226
|
-
attributes: {
|
|
227
|
-
state: 'failed',
|
|
228
|
-
error_log: errorLog,
|
|
229
|
-
closed: true,
|
|
230
|
-
closed_date: t,
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
async fetchFinalizableRuns(id) {
|
|
235
|
-
const rows = await this.api
|
|
236
|
-
.collection('gaia_run')
|
|
237
|
-
.where('conductor_id.machine_id', '=', id)
|
|
238
|
-
.where('state', '=', 'done')
|
|
239
|
-
.where('closed', '=', '0')
|
|
240
|
-
.fields(['worktree_path', 'agent', 'drupal_internal__id'])
|
|
241
|
-
.page(100)
|
|
242
|
-
.list();
|
|
243
|
-
// No `started_at` read: the run's duration_s is derived from the agent
|
|
244
|
-
// transcript at finalize (GAIA-151), so the conductor no longer round-trips
|
|
245
|
-
// the timestamp back through JSON:API.
|
|
246
|
-
return rows.map((r) => ({
|
|
247
|
-
runUuid: r.id,
|
|
248
|
-
// The `#<runId>` tab-label token: scopes the run's tab-close (GAIA-183).
|
|
249
|
-
runId: Number(r.attr('drupal_internal__id')),
|
|
250
|
-
worktreePath: r.attr('worktree_path') ?? '',
|
|
251
|
-
agent: r.attr('agent') ?? '',
|
|
252
|
-
}));
|
|
253
|
-
}
|
|
254
|
-
async finalizeRun(uuid, log, metrics) {
|
|
255
|
-
const t = Math.floor(Date.now() / 1000);
|
|
256
|
-
await this.api.update('gaia_run', uuid, {
|
|
257
|
-
attributes: {
|
|
258
|
-
closed: true,
|
|
259
|
-
closed_date: t,
|
|
260
|
-
...(log ? { log } : {}),
|
|
261
|
-
...(metrics ?? {}),
|
|
262
|
-
},
|
|
263
|
-
}); // NO state — the run is already done.
|
|
264
|
-
}
|
|
265
|
-
async fetchUncleanedTickets(id) {
|
|
266
|
-
// Finished tickets whose worktree is not yet torn down (cleaned_up=0),
|
|
267
|
-
// driven by the durable `cleaned_up` flag (GAIA-89) AND scoped to this
|
|
268
|
-
// conductor (GAIA-121): only tickets assigned to `id` are loaded, so a
|
|
269
|
-
// ticket owned by another conductor never reaches the reaper loop. "Finished"
|
|
270
|
-
// = reached `done` OR already `closed`; the collection builder has no OR, so
|
|
271
|
-
// this is two AND-queries merged + de-duped by uuid. The `conductor_id`
|
|
272
|
-
// relationship carries the assigned conductor's `machine_id` — the same
|
|
273
|
-
// nested filter the sibling reaper queries (`fetchFinalizableRuns` etc.) use.
|
|
274
|
-
const fields = ['branch_name', 'state', 'closed'];
|
|
275
|
-
const done = await this.api
|
|
276
|
-
.collection('gaia_ticket')
|
|
277
|
-
.where('conductor_id.machine_id', '=', id)
|
|
278
|
-
.where('state', '=', 'done')
|
|
279
|
-
.where('cleaned_up', '=', '0')
|
|
280
|
-
.fields(fields)
|
|
281
|
-
.page(100)
|
|
282
|
-
.list();
|
|
283
|
-
const closed = await this.api
|
|
284
|
-
.collection('gaia_ticket')
|
|
285
|
-
.where('conductor_id.machine_id', '=', id)
|
|
286
|
-
.where('closed', '=', '1')
|
|
287
|
-
.where('cleaned_up', '=', '0')
|
|
288
|
-
.fields(fields)
|
|
289
|
-
.page(100)
|
|
290
|
-
.list();
|
|
291
|
-
const byUuid = new Map();
|
|
292
|
-
for (const r of [...done, ...closed]) {
|
|
293
|
-
byUuid.set(r.id, r);
|
|
294
|
-
}
|
|
295
|
-
return Promise.all([...byUuid.values()].map(async (r) => ({
|
|
296
|
-
ticketUuid: r.id,
|
|
297
|
-
branchName: r.attr('branch_name') ?? '',
|
|
298
|
-
state: r.attr('state') ?? '',
|
|
299
|
-
closed: r.attr('closed') ?? false,
|
|
300
|
-
worktreePath: await this.latestRunWorktree(r.id),
|
|
301
|
-
})));
|
|
302
|
-
}
|
|
303
|
-
async closeTicket(uuid) {
|
|
304
|
-
const t = Math.floor(Date.now() / 1000);
|
|
305
|
-
await this.api.update('gaia_ticket', uuid, {
|
|
306
|
-
attributes: { closed: true, closed_date: t },
|
|
307
|
-
}); // NO state — the ticket is already done.
|
|
308
|
-
}
|
|
309
|
-
async markTicketCleanedUp(uuid) {
|
|
310
|
-
await this.api.update('gaia_ticket', uuid, {
|
|
311
|
-
attributes: { cleaned_up: true },
|
|
312
|
-
}); // NO state/closed — teardown flag only, decoupled from the lifecycle.
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Absolute worktree path of the ticket's latest run (highest run id with a
|
|
316
|
-
* non-empty worktree_path), or '' when none — the cwd the cleanup command
|
|
317
|
-
* runs in. The conductor persists worktree_path on markRunning, so a done
|
|
318
|
-
* ticket's run carries the path even after the run closed.
|
|
319
|
-
*/
|
|
320
|
-
async latestRunWorktree(ticketUuid) {
|
|
321
|
-
const rows = await this.api
|
|
322
|
-
.collection('gaia_run')
|
|
323
|
-
.where('ticket_id.id', '=', ticketUuid)
|
|
324
|
-
.fields(['worktree_path', 'drupal_internal__id'])
|
|
325
|
-
.sort('-drupal_internal__id')
|
|
326
|
-
.page(100)
|
|
327
|
-
.list();
|
|
328
|
-
for (const r of rows) {
|
|
329
|
-
const path = r.attr('worktree_path');
|
|
330
|
-
if (path) {
|
|
331
|
-
return path;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return '';
|
|
335
|
-
}
|
|
336
|
-
async projectUuid(name) {
|
|
337
|
-
const p = await this.api
|
|
338
|
-
.collection('gaia_project')
|
|
339
|
-
.where('name', '=', name)
|
|
340
|
-
.first();
|
|
341
|
-
if (!p) {
|
|
342
|
-
throw new Error(`gaia project "${name}" not found`);
|
|
343
|
-
}
|
|
344
|
-
return p.id;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
export function drupalRemote() {
|
|
348
|
-
return {
|
|
349
|
-
kind: 'remote',
|
|
350
|
-
id: 'drupal',
|
|
351
|
-
requiredModules: [],
|
|
352
|
-
async createRemote(config) {
|
|
353
|
-
const http = createHttpClient();
|
|
354
|
-
const auth = await resolveAuth({
|
|
355
|
-
baseUrl: config.site.base_url,
|
|
356
|
-
plugins: config.plugins ?? [],
|
|
357
|
-
http,
|
|
358
|
-
now: Date.now,
|
|
359
|
-
// stateDir omitted → dropsh defaultStateDir() (~/.config/dropsh)
|
|
360
|
-
});
|
|
361
|
-
return new DrupalGaiaRemote(createJsonApiClient({
|
|
362
|
-
baseUrl: config.site.base_url,
|
|
363
|
-
prefix: config.site.jsonapi_prefix,
|
|
364
|
-
http,
|
|
365
|
-
auth: auth,
|
|
366
|
-
}));
|
|
367
|
-
},
|
|
368
|
-
};
|
|
369
|
-
}
|