@kontextmind/kxm 0.7.6 → 0.7.7

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.
@@ -0,0 +1,615 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
6
+ import { DatabaseSync } from "../sqlite.ts";
7
+ import {
8
+ DEFAULT_ROLES,
9
+ DEFAULT_ROLE_SEATS,
10
+ listRoles,
11
+ getRole,
12
+ addRole,
13
+ removeRole,
14
+ modifyRole,
15
+ loadRoleHostsConfig,
16
+ setRoleSeatHost,
17
+ resolveRoleSeat,
18
+ type KxmRoleDefinition,
19
+ } from "../role.ts";
20
+ import { discoverVnextProjectRoot } from "../vnext-config.ts";
21
+ import { ensureVnextSupervisor, vnextRuntimeRequest } from "../vnext-runtime-supervisor.ts";
22
+ import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
23
+ import { print, type CliIo, type Runtime } from "./types.ts";
24
+
25
+ export interface PickCandidate {
26
+ id: string;
27
+ label?: string | undefined;
28
+ description?: string | undefined;
29
+ payload?: any;
30
+ }
31
+
32
+ export async function resolvePickItem(
33
+ io: CliIo,
34
+ title: string,
35
+ items: PickCandidate[],
36
+ pickOption?: string | boolean | undefined,
37
+ env?: NodeJS.ProcessEnv | undefined,
38
+ ): Promise<PickCandidate | undefined> {
39
+ if (items.length === 0) return undefined;
40
+
41
+ if (typeof pickOption === "string" && pickOption.trim().length > 0) {
42
+ const trimmed = pickOption.trim();
43
+ const asNum = parseInt(trimmed, 10);
44
+ if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
45
+ return items[asNum - 1];
46
+ }
47
+ const matched = items.find((it) => it.id === trimmed || it.id.toLowerCase() === trimmed.toLowerCase());
48
+ if (matched) return matched;
49
+ }
50
+
51
+ const lines: string[] = [`${title}:`];
52
+ items.forEach((item, idx) => {
53
+ const desc = item.description ? ` - ${item.description}` : "";
54
+ const label = item.label ? ` [${item.label}]` : "";
55
+ lines.push(` ${(idx + 1).toString().padStart(2)}) ${item.id}${label}${desc}`);
56
+ });
57
+ io.stdout(`${lines.join("\n")}\n`);
58
+
59
+ const envSelect = env?.KXM_PICK_SELECT ?? process.env.KXM_PICK_SELECT;
60
+ if (envSelect) {
61
+ const asNum = parseInt(envSelect, 10);
62
+ if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
63
+ return items[asNum - 1];
64
+ }
65
+ const matched = items.find((it) => it.id === envSelect);
66
+ if (matched) return matched;
67
+ }
68
+
69
+ if (!process.stdin.isTTY) {
70
+ return undefined;
71
+ }
72
+
73
+ const rl = createInterface({
74
+ input: process.stdin,
75
+ output: process.stdout,
76
+ });
77
+
78
+ return new Promise((resolvePrompt) => {
79
+ rl.question(`Enter selection (1-${items.length}) or ID: `, (answer) => {
80
+ rl.close();
81
+ const ans = answer.trim();
82
+ const asNum = parseInt(ans, 10);
83
+ if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
84
+ resolvePrompt(items[asNum - 1]);
85
+ } else {
86
+ const matched = items.find((it) => it.id === ans);
87
+ resolvePrompt(matched ?? undefined);
88
+ }
89
+ });
90
+ });
91
+ }
92
+
93
+ export async function cmdRoleList(
94
+ runtime: Runtime,
95
+ options: { scope?: "all" | "global" | "local" | undefined },
96
+ ): Promise<number> {
97
+ const roles = listRoles({
98
+ scope: options.scope,
99
+ repoRoot: runtime.cwd,
100
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
101
+ });
102
+ if (runtime.json) {
103
+ print(runtime.io, true, { ok: true, command: "role list", roles }, "");
104
+ return 0;
105
+ }
106
+ if (roles.length === 0) {
107
+ runtime.io.stdout("No roles configured.\n");
108
+ return 0;
109
+ }
110
+ const lines: string[] = ["ROLES:"];
111
+ for (const r of roles) {
112
+ const scopeTag = r.scope === "overridden" ? "[local override]" : `[${r.scope}]`;
113
+ const modelTag = r.primaryModel ? `(${r.primaryHarness ?? "harness"}:${r.primaryModel})` : "";
114
+ lines.push(` ${r.id.padEnd(16)} ${scopeTag.padEnd(16)} ${modelTag.padEnd(28)} ${r.description}`);
115
+ }
116
+ print(runtime.io, false, {}, `${lines.join("\n")}\n`);
117
+ return 0;
118
+ }
119
+
120
+ export async function cmdRoleGet(
121
+ runtime: Runtime,
122
+ roleId: string,
123
+ options: { scope?: "all" | "global" | "local" | undefined },
124
+ ): Promise<number> {
125
+ const result = getRole(roleId, {
126
+ scope: options.scope,
127
+ repoRoot: runtime.cwd,
128
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
129
+ });
130
+ if (!result) {
131
+ runtime.io.stderr(`kxm: role '${roleId}' not found\n`);
132
+ return 1;
133
+ }
134
+ print(
135
+ runtime.io,
136
+ runtime.json,
137
+ { ok: true, command: "role get", roleId, scope: result.scope, filePath: result.filePath, role: result.role },
138
+ stringifyYaml(result.role),
139
+ );
140
+ return 0;
141
+ }
142
+
143
+ export async function cmdRoleAdd(
144
+ runtime: Runtime,
145
+ roleId: string | undefined,
146
+ options: {
147
+ file?: string | undefined;
148
+ description?: string | undefined;
149
+ skills?: string | undefined;
150
+ harness?: string | undefined;
151
+ model?: string | undefined;
152
+ scope?: "global" | "local" | undefined;
153
+ overwrite?: boolean | undefined;
154
+ pick?: string | boolean | undefined;
155
+ },
156
+ ): Promise<number> {
157
+ const scope = options.scope ?? "local";
158
+ if (!roleId || options.pick) {
159
+ const candidates: PickCandidate[] = Object.values(DEFAULT_ROLES).map((r) => ({
160
+ id: r.id,
161
+ description: r.description,
162
+ label: "template",
163
+ payload: r,
164
+ }));
165
+ if (scope === "local") {
166
+ const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
167
+ for (const gr of globalRoles) {
168
+ if (!candidates.some((c) => c.id === gr.id)) {
169
+ candidates.push({ id: gr.id, description: gr.description, label: "global", payload: gr });
170
+ }
171
+ }
172
+ }
173
+ const picked = await resolvePickItem(runtime.io, `Select a role template to add (${scope})`, candidates, options.pick, runtime.env);
174
+ if (!picked) {
175
+ if (!roleId) {
176
+ runtime.io.stderr("role add failed: missing roleId or pick selection\n");
177
+ return 1;
178
+ }
179
+ } else {
180
+ roleId = picked.id;
181
+ if (!options.file && picked.payload && DEFAULT_ROLES[picked.id]) {
182
+ const base = DEFAULT_ROLES[picked.id]!;
183
+ const roleDef: KxmRoleDefinition = {
184
+ ...base,
185
+ description: options.description || base.description,
186
+ skills: options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : base.skills,
187
+ roster: options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : base.roster,
188
+ };
189
+ try {
190
+ const res = addRole(roleDef, {
191
+ scope,
192
+ repoRoot: runtime.cwd,
193
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
194
+ overwrite: options.overwrite,
195
+ });
196
+ print(
197
+ runtime.io,
198
+ runtime.json,
199
+ { ok: true, command: "role add", roleId, ...res },
200
+ `Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
201
+ );
202
+ return 0;
203
+ } catch (err: unknown) {
204
+ runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
205
+ return 1;
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ let roleDef: KxmRoleDefinition;
212
+ if (options.file) {
213
+ const filePath = resolve(runtime.cwd, options.file);
214
+ const content = readFileSync(filePath, "utf8");
215
+ roleDef = parseYaml(content) as KxmRoleDefinition;
216
+ roleDef.id = roleId!;
217
+ } else {
218
+ const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : [];
219
+ const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : [];
220
+ roleDef = {
221
+ schema: "kxm.role.v1",
222
+ id: roleId!,
223
+ description: options.description || `Role ${roleId}`,
224
+ skills,
225
+ roster,
226
+ };
227
+ }
228
+
229
+ try {
230
+ const res = addRole(roleDef, {
231
+ scope,
232
+ repoRoot: runtime.cwd,
233
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
234
+ overwrite: options.overwrite,
235
+ });
236
+ print(
237
+ runtime.io,
238
+ runtime.json,
239
+ { ok: true, command: "role add", roleId, ...res },
240
+ `Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
241
+ );
242
+ return 0;
243
+ } catch (err: unknown) {
244
+ runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
245
+ return 1;
246
+ }
247
+ }
248
+
249
+ export async function cmdRoleRemove(
250
+ runtime: Runtime,
251
+ roleId: string | undefined,
252
+ options: { scope?: "global" | "local" | undefined; pick?: string | boolean | undefined },
253
+ ): Promise<number> {
254
+ const scope = options.scope ?? "local";
255
+ if (!roleId || options.pick) {
256
+ const roles = listRoles({
257
+ scope,
258
+ repoRoot: runtime.cwd,
259
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
260
+ });
261
+ if (roles.length === 0) {
262
+ runtime.io.stdout(`No roles configured in ${scope} scope to remove.\n`);
263
+ return 0;
264
+ }
265
+ const candidates: PickCandidate[] = roles.map((r) => ({
266
+ id: r.id,
267
+ description: r.description,
268
+ label: r.scope,
269
+ }));
270
+ const picked = await resolvePickItem(runtime.io, `Select a role to remove (${scope})`, candidates, options.pick, runtime.env);
271
+ if (!picked) {
272
+ if (!roleId) {
273
+ runtime.io.stderr("role remove failed: missing roleId or pick selection\n");
274
+ return 1;
275
+ }
276
+ } else {
277
+ roleId = picked.id;
278
+ }
279
+ }
280
+
281
+ try {
282
+ const res = removeRole(roleId!, {
283
+ scope,
284
+ repoRoot: runtime.cwd,
285
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
286
+ });
287
+ print(
288
+ runtime.io,
289
+ runtime.json,
290
+ { ok: true, command: "role remove", roleId, ...res },
291
+ `Removed role '${roleId}' from ${res.scope}\n`,
292
+ );
293
+ return 0;
294
+ } catch (err: unknown) {
295
+ runtime.io.stderr(`role remove failed: ${(err as Error).message}\n`);
296
+ return 1;
297
+ }
298
+ }
299
+
300
+ export async function cmdRoleModify(
301
+ runtime: Runtime,
302
+ roleId: string | undefined,
303
+ options: {
304
+ description?: string | undefined;
305
+ addSkill?: string | undefined;
306
+ removeSkill?: string | undefined;
307
+ addModel?: string | undefined;
308
+ removeModel?: string | undefined;
309
+ scope?: "global" | "local" | undefined;
310
+ pick?: string | boolean | undefined;
311
+ },
312
+ ): Promise<number> {
313
+ if (!roleId || options.pick) {
314
+ const roles = listRoles({
315
+ scope: options.scope,
316
+ repoRoot: runtime.cwd,
317
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
318
+ });
319
+ if (roles.length === 0) {
320
+ runtime.io.stdout("No roles configured to modify.\n");
321
+ return 0;
322
+ }
323
+ const candidates: PickCandidate[] = roles.map((r) => ({
324
+ id: r.id,
325
+ description: r.description,
326
+ label: r.scope,
327
+ }));
328
+ const picked = await resolvePickItem(runtime.io, "Select a role to modify", candidates, options.pick, runtime.env);
329
+ if (!picked) {
330
+ if (!roleId) {
331
+ runtime.io.stderr("role modify failed: missing roleId or pick selection\n");
332
+ return 1;
333
+ }
334
+ } else {
335
+ roleId = picked.id;
336
+ }
337
+ }
338
+
339
+ const existing = getRole(roleId!, {
340
+ scope: options.scope,
341
+ repoRoot: runtime.cwd,
342
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
343
+ });
344
+ if (!existing) {
345
+ runtime.io.stderr(`kxm: role '${roleId}' not found\n`);
346
+ return 1;
347
+ }
348
+
349
+ const role = existing.role;
350
+ let skills = [...(role.skills ?? [])];
351
+ if (options.addSkill && !skills.includes(options.addSkill)) {
352
+ skills.push(options.addSkill);
353
+ }
354
+ if (options.removeSkill) {
355
+ skills = skills.filter((s) => s !== options.removeSkill);
356
+ }
357
+
358
+ let roster = [...(role.roster ?? [])];
359
+ if (options.addModel) {
360
+ const [harnessOrModel, maybeModel] = options.addModel.split(":");
361
+ const harness = maybeModel ? harnessOrModel! : "pi";
362
+ const model = maybeModel || harnessOrModel!;
363
+ roster.push({ harness, model });
364
+ }
365
+ if (options.removeModel) {
366
+ roster = roster.filter((entry) => entry.model !== options.removeModel);
367
+ }
368
+
369
+ const updates: Partial<KxmRoleDefinition> = {
370
+ ...(options.description ? { description: options.description } : {}),
371
+ skills,
372
+ roster,
373
+ };
374
+
375
+ try {
376
+ const res = modifyRole(roleId!, updates, {
377
+ scope: options.scope,
378
+ repoRoot: runtime.cwd,
379
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
380
+ });
381
+ print(
382
+ runtime.io,
383
+ runtime.json,
384
+ { ok: true, command: "role modify", roleId, ...res },
385
+ `Modified role '${roleId}' in ${res.scope}\n`,
386
+ );
387
+ return 0;
388
+ } catch (err: unknown) {
389
+ runtime.io.stderr(`role modify failed: ${(err as Error).message}\n`);
390
+ return 1;
391
+ }
392
+ }
393
+
394
+ export async function cmdRoleHosts(
395
+ runtime: Runtime,
396
+ options: { scope?: "all" | "global" | "local" | undefined } = {},
397
+ ): Promise<number> {
398
+ const hostsConfig = loadRoleHostsConfig({
399
+ scope: options.scope,
400
+ repoRoot: runtime.cwd,
401
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
402
+ });
403
+
404
+ const seatIds = Array.from(
405
+ new Set([
406
+ ...Object.keys(DEFAULT_ROLE_SEATS),
407
+ ...Object.keys(hostsConfig.config.seats ?? {}),
408
+ ]),
409
+ ).sort();
410
+
411
+ const seats = seatIds.map((seatId) => {
412
+ const resolved = resolveRoleSeat(seatId, {
413
+ repoRoot: runtime.cwd,
414
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
415
+ });
416
+ return {
417
+ seatId,
418
+ host: resolved.host,
419
+ model: resolved.model,
420
+ provider: resolved.provider,
421
+ effort: resolved.effort,
422
+ source: resolved.source,
423
+ configuredHost: hostsConfig.config.seats?.[seatId]?.host,
424
+ configuredModel: hostsConfig.config.seats?.[seatId]?.model,
425
+ };
426
+ });
427
+
428
+ if (runtime.json) {
429
+ print(
430
+ runtime.io,
431
+ true,
432
+ {
433
+ ok: true,
434
+ command: "role hosts",
435
+ scope: hostsConfig.scope,
436
+ filePath: hostsConfig.filePath,
437
+ seats,
438
+ hostProviders: hostsConfig.config.hostProviders ?? {},
439
+ },
440
+ "",
441
+ );
442
+ return 0;
443
+ }
444
+
445
+ const lines: string[] = [
446
+ `ROLE SEATS (${hostsConfig.scope}${hostsConfig.filePath ? ` at ${hostsConfig.filePath}` : ""}):`,
447
+ ];
448
+ for (const s of seats) {
449
+ const modelStr = s.model ? ` [${s.model}]` : "";
450
+ const sourceTag = `(via ${s.source})`;
451
+ lines.push(` ${s.seatId.padEnd(16)} -> host: ${s.host.padEnd(12)} ${modelStr.padEnd(30)} ${sourceTag}`);
452
+ }
453
+ if (hostsConfig.config.hostProviders && Object.keys(hostsConfig.config.hostProviders).length > 0) {
454
+ lines.push("\nHOST PROVIDERS:");
455
+ for (const [h, p] of Object.entries(hostsConfig.config.hostProviders)) {
456
+ lines.push(` ${h.padEnd(16)} -> provider: ${p}`);
457
+ }
458
+ }
459
+ print(runtime.io, false, {}, `${lines.join("\n")}\n`);
460
+ return 0;
461
+ }
462
+
463
+ export async function cmdRoleSetHost(
464
+ runtime: Runtime,
465
+ seatId: string,
466
+ host: string,
467
+ options: {
468
+ model?: string | undefined;
469
+ effort?: "low" | "medium" | "high" | "xhigh" | undefined;
470
+ scope?: "global" | "local" | undefined;
471
+ } = {},
472
+ ): Promise<number> {
473
+ if (!seatId || !host) {
474
+ runtime.io.stderr("kxm role set-host requires <seatId> and <host>\n");
475
+ return 1;
476
+ }
477
+
478
+ try {
479
+ const result = setRoleSeatHost(seatId, host, {
480
+ model: options.model,
481
+ effort: options.effort,
482
+ scope: options.scope ?? "local",
483
+ repoRoot: runtime.cwd,
484
+ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
485
+ });
486
+
487
+ print(
488
+ runtime.io,
489
+ runtime.json,
490
+ {
491
+ ok: true,
492
+ command: "role set-host",
493
+ seatId,
494
+ host,
495
+ binding: result.binding,
496
+ filePath: result.filePath,
497
+ scope: result.scope,
498
+ },
499
+ `Bound seat '${seatId}' to host '${host}' in ${result.filePath}\n`,
500
+ );
501
+ return 0;
502
+ } catch (err: unknown) {
503
+ runtime.io.stderr(`role set-host failed: ${(err as Error).message}\n`);
504
+ return 1;
505
+ }
506
+ }
507
+
508
+ export async function cmdRoleResume(
509
+ runtime: Runtime,
510
+ runId: string,
511
+ ruling?: string | undefined,
512
+ ): Promise<number> {
513
+ if (!runId) {
514
+ runtime.io.stderr("kxm role resume requires <runId>\n");
515
+ return 1;
516
+ }
517
+
518
+ const effectiveRuling = ruling?.trim() || "operator_ruling: waived and resumed";
519
+
520
+ // Check if it's a vNext run
521
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd);
522
+ if (projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
523
+ if (runtime.dryRun) {
524
+ print(runtime.io, runtime.json, { ok: true, command: "role resume", runId, ruling: effectiveRuling }, `would resume vNext run ${runId}`);
525
+ return 0;
526
+ }
527
+ try {
528
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
529
+ const posted = await vnextRuntimeRequest(
530
+ supervisor,
531
+ "POST",
532
+ `/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
533
+ {
534
+ signalKey: "audit_escalation",
535
+ status: "passed",
536
+ summary: effectiveRuling,
537
+ action: "unblock",
538
+ },
539
+ );
540
+ print(
541
+ runtime.io,
542
+ runtime.json,
543
+ { ok: true, command: "role resume", runId, ruling: effectiveRuling, unblocked: posted.unblocked === true },
544
+ `Resumed vNext run ${runId} with ruling: ${effectiveRuling}\n`,
545
+ );
546
+ return 0;
547
+ } catch (error) {
548
+ const msg = error instanceof Error ? error.message : "resume_failed";
549
+ print(runtime.io, runtime.json, { ok: false, command: "role resume", error: "resume_failed", detail: msg }, `resume vNext run failed: ${msg}\n`);
550
+ return 1;
551
+ }
552
+ }
553
+
554
+ // Workflow run: check local state database (.kxm/state/kxm.db)
555
+ const dbPath = join(runtime.cwd, ".kxm", "state", "kxm.db");
556
+ if (existsSync(dbPath)) {
557
+ try {
558
+ const database = new DatabaseSync(dbPath);
559
+ try {
560
+ const row = database.prepare("SELECT record FROM workflow_runs WHERE id = ?").get(runId) as { record: string } | undefined;
561
+ if (!row) {
562
+ runtime.io.stderr(`kxm: workflow run '${runId}' not found in ${dbPath}\n`);
563
+ return 1;
564
+ }
565
+ const run = JSON.parse(row.record) as WorkflowRun;
566
+ const now = new Date().toISOString();
567
+ const resumeResult = resumeWorkflowFromRuling(run, effectiveRuling, now);
568
+
569
+ database.prepare("UPDATE workflow_runs SET record = ? WHERE id = ?").run(
570
+ JSON.stringify(resumeResult.run),
571
+ runId,
572
+ );
573
+
574
+ const journalId = randomUUID();
575
+ const journalPayload = {
576
+ id: journalId,
577
+ runId,
578
+ stageId: resumeResult.stageId,
579
+ category: "decision" as const,
580
+ area: "workflow" as const,
581
+ summary: `Role resume ruling: ${effectiveRuling}`,
582
+ details: { ruling: effectiveRuling },
583
+ evidence: {},
584
+ createdAt: now,
585
+ };
586
+
587
+ database.prepare(
588
+ "INSERT INTO workflow_journal (id, run_id, category, area, record) VALUES (?, ?, ?, ?, ?)",
589
+ ).run(
590
+ journalId,
591
+ runId,
592
+ "decision",
593
+ "workflow",
594
+ JSON.stringify(journalPayload),
595
+ );
596
+
597
+ print(
598
+ runtime.io,
599
+ runtime.json,
600
+ { ok: true, command: "role resume", runId, stageId: resumeResult.stageId, ruling: effectiveRuling, status: resumeResult.run.status },
601
+ `Resumed workflow run ${runId} (stage: ${resumeResult.stageId}) with ruling: ${effectiveRuling}\n`,
602
+ );
603
+ return 0;
604
+ } finally {
605
+ database.close();
606
+ }
607
+ } catch (err: unknown) {
608
+ runtime.io.stderr(`role resume failed: ${(err as Error).message}\n`);
609
+ return 1;
610
+ }
611
+ }
612
+
613
+ runtime.io.stderr(`kxm: no active run store or database found for run '${runId}'\n`);
614
+ return 1;
615
+ }