@gaia-ai/addon-herdr 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +8 -2
- package/dist/src/index.js +9 -1
- package/dist/src/workspace.d.ts +65 -4
- package/dist/src/workspace.js +94 -21
- package/package.json +4 -4
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecutorCapabilities, ExecutorPlugin, GaiaExecutor, HookContext, HookName, SpawnedSession, SpawnRunInput } from '@gaia-ai/conductor/contract';
|
|
1
|
+
import type { ExecutorCapabilities, ExecutorPlugin, GaiaExecutor, HookContext, HookName, HookResult, SpawnedSession, SpawnRunInput } from '@gaia-ai/conductor/contract';
|
|
2
2
|
import type { ConductorLogger } from '@gaia-ai/core';
|
|
3
3
|
import { type HerdrExecutorOptions } from './config.js';
|
|
4
4
|
import { type HerdrExec } from './pane-layout.js';
|
|
@@ -26,8 +26,14 @@ export declare class HerdrExecutor implements GaiaExecutor {
|
|
|
26
26
|
* silent no-op. A failing command → log loudly (hook + worktree + ticket +
|
|
27
27
|
* err) and return. NEVER throws, so a hook failure can't abort dispatch or
|
|
28
28
|
* wedge a run.
|
|
29
|
+
*
|
|
30
|
+
* GAIA-237: it also RETURNS what happened ({@link HookResult}). Not throwing
|
|
31
|
+
* kept a hook from wedging a run, but swallowing the outcome entirely left the
|
|
32
|
+
* caller unable to tell a provisioned worktree from a broken one — so an agent
|
|
33
|
+
* was dispatched into a worktree whose bootstrap had aborted, with nothing but
|
|
34
|
+
* a log line (which, on a TTY, went nowhere) to say so.
|
|
29
35
|
*/
|
|
30
|
-
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<
|
|
36
|
+
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<HookResult>;
|
|
31
37
|
/**
|
|
32
38
|
* List worktrees, ALWAYS anchored to the configured parent repo root
|
|
33
39
|
* (GAIA-211). `herdr worktree list` is machine-global, but run WITHOUT
|
package/dist/src/index.js
CHANGED
|
@@ -217,17 +217,25 @@ export class HerdrExecutor {
|
|
|
217
217
|
* silent no-op. A failing command → log loudly (hook + worktree + ticket +
|
|
218
218
|
* err) and return. NEVER throws, so a hook failure can't abort dispatch or
|
|
219
219
|
* wedge a run.
|
|
220
|
+
*
|
|
221
|
+
* GAIA-237: it also RETURNS what happened ({@link HookResult}). Not throwing
|
|
222
|
+
* kept a hook from wedging a run, but swallowing the outcome entirely left the
|
|
223
|
+
* caller unable to tell a provisioned worktree from a broken one — so an agent
|
|
224
|
+
* was dispatched into a worktree whose bootstrap had aborted, with nothing but
|
|
225
|
+
* a log line (which, on a TTY, went nowhere) to say so.
|
|
220
226
|
*/
|
|
221
227
|
async runHook(name, cwd, ctx, env) {
|
|
222
228
|
const command = this.options.hooks?.[name];
|
|
223
229
|
if (!command) {
|
|
224
|
-
return;
|
|
230
|
+
return { hook: name, ran: false, ok: true };
|
|
225
231
|
}
|
|
226
232
|
try {
|
|
227
233
|
await this.runShell(command, cwd, env);
|
|
234
|
+
return { hook: name, ran: true, ok: true };
|
|
228
235
|
}
|
|
229
236
|
catch (err) {
|
|
230
237
|
this.logger.error({ hook: name, worktree: cwd, ticket: ctx.ticket, err: String(err) }, 'lifecycle hook failed');
|
|
238
|
+
return { hook: name, ran: true, ok: false, error: String(err) };
|
|
231
239
|
}
|
|
232
240
|
}
|
|
233
241
|
/**
|
package/dist/src/workspace.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EnsuredWorkspace, GaiaWorkspace, WorkspacePlugin } from '@gaia-ai/conductor/contract';
|
|
2
2
|
import type { ConductorLogger } from '@gaia-ai/core';
|
|
3
3
|
import type { AgentLaunchHost } from './agent-host.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type PaneSpec } from './config.js';
|
|
5
5
|
import { type RootGitExec } from './root-anchor.js';
|
|
6
6
|
export type HerdrExec = (args: string[]) => Promise<string>;
|
|
7
7
|
export type GitExec = (args: string[]) => Promise<string>;
|
|
@@ -10,13 +10,35 @@ export type GitExec = (args: string[]) => Promise<string>;
|
|
|
10
10
|
* (GAIA-139): `command` launches a tool in the main pane (e.g. `spiceedit`) so
|
|
11
11
|
* the human lands in an editor instead of a bare shell; `panes[]` are optional
|
|
12
12
|
* splits off it. Same shape as the executor's per-state layout minus `tabLabel`
|
|
13
|
-
* — there is only ONE open layout, not a per-ticket-state map.
|
|
14
|
-
*
|
|
13
|
+
* — there is only ONE open layout, not a per-ticket-state map.
|
|
14
|
+
*
|
|
15
|
+
* GAIA-234: `{identifier}`, `{branch}` and `{worktreePath}` expand in `command`
|
|
16
|
+
* **and** in every `panes[].command`. The root command is rendered here, by
|
|
17
|
+
* `ensure`, before the finished layout reaches `applyPaneLayout` — that shared
|
|
18
|
+
* helper types its root command VERBATIM on purpose, because its other caller
|
|
19
|
+
* (the executor) pre-assembles `KEY='value' <agent>` env prefixes whose values
|
|
20
|
+
* may legitimately contain braces (spec D2).
|
|
21
|
+
*
|
|
22
|
+
* Unset → {@link DEFAULT_OPEN_LAYOUT}, the ticket deep link. `{}` → the bare
|
|
23
|
+
* empty tab (`applyPaneLayout` issues no calls for an empty layout).
|
|
15
24
|
*/
|
|
16
25
|
export interface OpenLayout {
|
|
17
26
|
command?: string;
|
|
18
27
|
panes?: PaneSpec[];
|
|
19
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The shipped default open layout (GAIA-234): a fresh worktree's initial pane
|
|
31
|
+
* renders the ticket it belongs to instead of an anonymous shell prompt.
|
|
32
|
+
*
|
|
33
|
+
* It lives in **code, not config**, because a default each repo has to opt into
|
|
34
|
+
* would leave the defect in place for every other consumer of this addon; the
|
|
35
|
+
* addon already ships opinionated defaults of this kind (`BUILTIN_DEFAULTS` in
|
|
36
|
+
* `./config.js`), and `gaia ui` is a command of the very host that ships it.
|
|
37
|
+
*
|
|
38
|
+
* `open: {}` is the documented opt-out back to the pre-GAIA-234 bare tab, so
|
|
39
|
+
* the default is not a one-way door.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_OPEN_LAYOUT: OpenLayout;
|
|
20
42
|
export interface HerdrWorkspaceOptions {
|
|
21
43
|
/** Main clone (a git repo): herdr `--cwd` target + worktree base. */
|
|
22
44
|
root: string;
|
|
@@ -30,7 +52,8 @@ export interface HerdrWorkspaceOptions {
|
|
|
30
52
|
baseBranch?: string;
|
|
31
53
|
/**
|
|
32
54
|
* Startup layout for the initial pane, applied on a FRESH worktree open only
|
|
33
|
-
* (GAIA-139). Unset →
|
|
55
|
+
* (GAIA-139). Unset → {@link DEFAULT_OPEN_LAYOUT} (`gaia ui --ticket <ID>`,
|
|
56
|
+
* GAIA-234); `{}` → the bare empty tab is left unchanged.
|
|
34
57
|
*/
|
|
35
58
|
open?: OpenLayout;
|
|
36
59
|
/** Overrideable herdr exec fn — defaults to the real `herdr` binary. */
|
|
@@ -48,6 +71,11 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
|
|
|
48
71
|
private readonly worktreeDir;
|
|
49
72
|
private readonly open;
|
|
50
73
|
private readonly logger;
|
|
74
|
+
/**
|
|
75
|
+
* Root pane ids captured by a FRESH `ensure` create, keyed by worktree path,
|
|
76
|
+
* awaiting {@link applyOpenLayout} (GAIA-237). One-shot: consumed on apply.
|
|
77
|
+
*/
|
|
78
|
+
private readonly pendingRootPanes;
|
|
51
79
|
/** Herdr-backed intent-level agent host for `gaia ui` (GAIA-190). */
|
|
52
80
|
readonly agentHost: AgentLaunchHost;
|
|
53
81
|
constructor(options: HerdrWorkspaceOptions);
|
|
@@ -68,7 +96,38 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
|
|
|
68
96
|
* Returns the ref when it exists, else undefined so the caller falls back.
|
|
69
97
|
*/
|
|
70
98
|
private verifyRemoteRef;
|
|
99
|
+
/**
|
|
100
|
+
* Ensure the branch's worktree + herdr workspace exist.
|
|
101
|
+
*
|
|
102
|
+
* `_identifier` is unused HERE: the worktree is located by branch, and since
|
|
103
|
+
* GAIA-237 the open layout — the one thing that consumed the identifier, to
|
|
104
|
+
* render `{identifier}` (GAIA-234) — is applied by {@link applyOpenLayout},
|
|
105
|
+
* which takes it on its own `ctx`. The parameter stays because it is the
|
|
106
|
+
* `GaiaWorkspace.ensure` contract position (the git workspace does use it).
|
|
107
|
+
* It is still taken as-passed rather than parsed back out of `branch`, which
|
|
108
|
+
* remains the reason the contract carries it at all.
|
|
109
|
+
*/
|
|
71
110
|
ensure(_identifier: string, branch?: string, baseRefOverride?: string): Promise<EnsuredWorkspace>;
|
|
111
|
+
/**
|
|
112
|
+
* Apply the open layout to the root pane captured by the most recent
|
|
113
|
+
* {@link ensure} create for `path` — by default (GAIA-234) the ticket's
|
|
114
|
+
* `gaia ui` deep link, so the tab shows the ticket instead of a bare shell.
|
|
115
|
+
* The pane's cwd is the worktree checkout, passed explicitly on the splits
|
|
116
|
+
* (the root pane's cwd is already the checkout).
|
|
117
|
+
*
|
|
118
|
+
* NO stored pane ⇒ silent no-op. That is the reused-worktree case: `ensure`
|
|
119
|
+
* took the reuse branch, there is no fresh root pane, and re-applying would
|
|
120
|
+
* start a duplicate editor process on reattach (GAIA-139). The capture is
|
|
121
|
+
* one-shot — consumed here — so a repeat call cannot double-apply and the map
|
|
122
|
+
* cannot grow without bound.
|
|
123
|
+
*
|
|
124
|
+
* Best-effort — mirrors the `fetchBaseRef` pattern: a broken layout is logged
|
|
125
|
+
* and swallowed so a startup-command failure can NEVER wedge dispatch.
|
|
126
|
+
*/
|
|
127
|
+
applyOpenLayout(path: string, ctx: {
|
|
128
|
+
identifier: string;
|
|
129
|
+
branch: string;
|
|
130
|
+
}): Promise<void>;
|
|
72
131
|
}
|
|
73
132
|
export declare function herdrWorkspace(opts?: {
|
|
74
133
|
/**
|
|
@@ -81,5 +140,7 @@ export declare function herdrWorkspace(opts?: {
|
|
|
81
140
|
open?: OpenLayout;
|
|
82
141
|
/** Overrideable git exec for root derivation (tests). */
|
|
83
142
|
execGit?: RootGitExec;
|
|
143
|
+
/** Overrideable herdr exec (tests) — sibling of `execGit`. */
|
|
144
|
+
execHerdr?: HerdrExec;
|
|
84
145
|
}): WorkspacePlugin;
|
|
85
146
|
export default herdrWorkspace;
|
package/dist/src/workspace.js
CHANGED
|
@@ -2,8 +2,24 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { loadInstructions } from '@gaia-ai/addon-workspace-git';
|
|
3
3
|
import { exec } from '@gaia-ai/core';
|
|
4
4
|
import { herdrAgentHost } from './agents.js';
|
|
5
|
+
import { renderTemplate } from './config.js';
|
|
5
6
|
import { applyPaneLayout, isRecord, parseJson } from './pane-layout.js';
|
|
6
7
|
import { deriveHerdrRoot } from './root-anchor.js';
|
|
8
|
+
/**
|
|
9
|
+
* The shipped default open layout (GAIA-234): a fresh worktree's initial pane
|
|
10
|
+
* renders the ticket it belongs to instead of an anonymous shell prompt.
|
|
11
|
+
*
|
|
12
|
+
* It lives in **code, not config**, because a default each repo has to opt into
|
|
13
|
+
* would leave the defect in place for every other consumer of this addon; the
|
|
14
|
+
* addon already ships opinionated defaults of this kind (`BUILTIN_DEFAULTS` in
|
|
15
|
+
* `./config.js`), and `gaia ui` is a command of the very host that ships it.
|
|
16
|
+
*
|
|
17
|
+
* `open: {}` is the documented opt-out back to the pre-GAIA-234 bare tab, so
|
|
18
|
+
* the default is not a one-way door.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_OPEN_LAYOUT = {
|
|
21
|
+
command: 'gaia ui --ticket {identifier}',
|
|
22
|
+
};
|
|
7
23
|
const defaultExec = (args) => exec('herdr', args);
|
|
8
24
|
const defaultGitExec = (args) => exec('git', args);
|
|
9
25
|
const noopLogger = {
|
|
@@ -74,6 +90,11 @@ export class HerdrWorkspace {
|
|
|
74
90
|
worktreeDir;
|
|
75
91
|
open;
|
|
76
92
|
logger;
|
|
93
|
+
/**
|
|
94
|
+
* Root pane ids captured by a FRESH `ensure` create, keyed by worktree path,
|
|
95
|
+
* awaiting {@link applyOpenLayout} (GAIA-237). One-shot: consumed on apply.
|
|
96
|
+
*/
|
|
97
|
+
pendingRootPanes = new Map();
|
|
77
98
|
/** Herdr-backed intent-level agent host for `gaia ui` (GAIA-190). */
|
|
78
99
|
agentHost;
|
|
79
100
|
constructor(options) {
|
|
@@ -82,7 +103,8 @@ export class HerdrWorkspace {
|
|
|
82
103
|
this.execGit = options.execGit ?? defaultGitExec;
|
|
83
104
|
this.root = resolve(options.root);
|
|
84
105
|
this.worktreeDir = options.worktreeDir ?? '.gaia-worktrees';
|
|
85
|
-
|
|
106
|
+
// `??`, not `||`: an explicit `open: {}` must survive as the opt-out.
|
|
107
|
+
this.open = options.open ?? DEFAULT_OPEN_LAYOUT;
|
|
86
108
|
this.logger = options.logger ?? noopLogger;
|
|
87
109
|
this.agentHost = herdrAgentHost(this.execHerdr);
|
|
88
110
|
}
|
|
@@ -150,6 +172,17 @@ export class HerdrWorkspace {
|
|
|
150
172
|
return undefined;
|
|
151
173
|
}
|
|
152
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Ensure the branch's worktree + herdr workspace exist.
|
|
177
|
+
*
|
|
178
|
+
* `_identifier` is unused HERE: the worktree is located by branch, and since
|
|
179
|
+
* GAIA-237 the open layout — the one thing that consumed the identifier, to
|
|
180
|
+
* render `{identifier}` (GAIA-234) — is applied by {@link applyOpenLayout},
|
|
181
|
+
* which takes it on its own `ctx`. The parameter stays because it is the
|
|
182
|
+
* `GaiaWorkspace.ensure` contract position (the git workspace does use it).
|
|
183
|
+
* It is still taken as-passed rather than parsed back out of `branch`, which
|
|
184
|
+
* remains the reason the contract carries it at all.
|
|
185
|
+
*/
|
|
153
186
|
async ensure(_identifier, branch, baseRefOverride) {
|
|
154
187
|
if (branch === undefined) {
|
|
155
188
|
throw new Error('herdr workspace requires a branch name');
|
|
@@ -214,37 +247,75 @@ export class HerdrWorkspace {
|
|
|
214
247
|
'--json',
|
|
215
248
|
]);
|
|
216
249
|
const path = parseWorktreePath(createOutput, 'worktree create');
|
|
217
|
-
// GAIA-
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
await applyPaneLayout(this.execHerdr, rootPaneId, this.open, path, {
|
|
228
|
-
branch,
|
|
229
|
-
worktreePath: path,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
catch (err) {
|
|
234
|
-
this.logger.warn({ branch, worktree: path, err: String(err) }, 'herdr open-layout failed (best-effort, ignored)');
|
|
235
|
-
}
|
|
250
|
+
// GAIA-237: CAPTURE the fresh root pane here, but do NOT lay it out yet.
|
|
251
|
+
// The layout's startup command names the gaia CLI, which the `after_create`
|
|
252
|
+
// hook builds — and that hook runs after `ensure()` returns. Applying the
|
|
253
|
+
// layout here fired the command before its binary existed, which is why the
|
|
254
|
+
// pane command needed a sentinel wait loop. The conductor now calls
|
|
255
|
+
// `applyOpenLayout` after the hook; a create with no root pane in its output
|
|
256
|
+
// stores nothing, so the layout is a no-op exactly as before.
|
|
257
|
+
const rootPaneId = parseWorktreeRootPane(createOutput);
|
|
258
|
+
if (rootPaneId) {
|
|
259
|
+
this.pendingRootPanes.set(path, rootPaneId);
|
|
236
260
|
}
|
|
237
261
|
// The `after_create` hook is run by the executor (GAIA-84), not here — the
|
|
238
262
|
// workspace only reports that it created a fresh worktree.
|
|
239
263
|
return { path, instructions: loadInstructions(path), created: true };
|
|
240
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Apply the open layout to the root pane captured by the most recent
|
|
267
|
+
* {@link ensure} create for `path` — by default (GAIA-234) the ticket's
|
|
268
|
+
* `gaia ui` deep link, so the tab shows the ticket instead of a bare shell.
|
|
269
|
+
* The pane's cwd is the worktree checkout, passed explicitly on the splits
|
|
270
|
+
* (the root pane's cwd is already the checkout).
|
|
271
|
+
*
|
|
272
|
+
* NO stored pane ⇒ silent no-op. That is the reused-worktree case: `ensure`
|
|
273
|
+
* took the reuse branch, there is no fresh root pane, and re-applying would
|
|
274
|
+
* start a duplicate editor process on reattach (GAIA-139). The capture is
|
|
275
|
+
* one-shot — consumed here — so a repeat call cannot double-apply and the map
|
|
276
|
+
* cannot grow without bound.
|
|
277
|
+
*
|
|
278
|
+
* Best-effort — mirrors the `fetchBaseRef` pattern: a broken layout is logged
|
|
279
|
+
* and swallowed so a startup-command failure can NEVER wedge dispatch.
|
|
280
|
+
*/
|
|
281
|
+
async applyOpenLayout(path, ctx) {
|
|
282
|
+
const rootPaneId = this.pendingRootPanes.get(path);
|
|
283
|
+
if (!rootPaneId) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.pendingRootPanes.delete(path);
|
|
287
|
+
try {
|
|
288
|
+
// Render the ROOT command here (spec D2). `applyPaneLayout` types its
|
|
289
|
+
// root command verbatim for the executor's brace-bearing env-prefixed
|
|
290
|
+
// agent command, so templating it there would corrupt that; the split
|
|
291
|
+
// commands it already renders from the same `vars`.
|
|
292
|
+
const vars = {
|
|
293
|
+
identifier: ctx.identifier,
|
|
294
|
+
branch: ctx.branch,
|
|
295
|
+
worktreePath: path,
|
|
296
|
+
};
|
|
297
|
+
const layout = {
|
|
298
|
+
...this.open,
|
|
299
|
+
...(this.open.command !== undefined
|
|
300
|
+
? { command: renderTemplate(this.open.command, vars) }
|
|
301
|
+
: {}),
|
|
302
|
+
};
|
|
303
|
+
await applyPaneLayout(this.execHerdr, rootPaneId, layout, path, vars);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
this.logger.warn({ branch: ctx.branch, worktree: path, err: String(err) }, 'herdr open-layout failed (best-effort, ignored)');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
241
309
|
}
|
|
242
310
|
export function herdrWorkspace(opts = {}) {
|
|
243
311
|
return {
|
|
244
312
|
kind: 'workspace',
|
|
245
313
|
id: 'herdr',
|
|
246
314
|
requiredModules: [],
|
|
247
|
-
|
|
315
|
+
// GAIA-234: `deps` carries the conductor logger, so the best-effort
|
|
316
|
+
// open-layout failure is actually logged instead of vanishing into the noop
|
|
317
|
+
// logger. Optional per the contract — absent, the noop default stays.
|
|
318
|
+
async createWorkspace(config, deps) {
|
|
248
319
|
const root = await deriveHerdrRoot(config.config_path, {
|
|
249
320
|
...(opts.root !== undefined ? { override: opts.root } : {}),
|
|
250
321
|
...(opts.execGit ? { execGit: opts.execGit } : {}),
|
|
@@ -254,6 +325,8 @@ export function herdrWorkspace(opts = {}) {
|
|
|
254
325
|
...(opts.worktreeDir ? { worktreeDir: opts.worktreeDir } : {}),
|
|
255
326
|
...(opts.baseBranch ? { baseBranch: opts.baseBranch } : {}),
|
|
256
327
|
...(opts.open ? { open: opts.open } : {}),
|
|
328
|
+
...(opts.execHerdr ? { execHerdr: opts.execHerdr } : {}),
|
|
329
|
+
...(deps?.logger ? { logger: deps.logger } : {}),
|
|
257
330
|
});
|
|
258
331
|
},
|
|
259
332
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/addon-herdr",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "GAIA herdr integration: spawn executor + per-ticket worktree workspace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"directory": "gaia-cli/addons/herdr"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@gaia-ai/addon-workspace-git": "^0.6.
|
|
23
|
+
"@gaia-ai/addon-workspace-git": "^0.6.4"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@gaia-ai/conductor": "^0.6.
|
|
27
|
-
"@gaia-ai/core": "^0.6.
|
|
26
|
+
"@gaia-ai/conductor": "^0.6.4",
|
|
27
|
+
"@gaia-ai/core": "^0.6.4"
|
|
28
28
|
}
|
|
29
29
|
}
|