@brainervirus/workit-core 0.6.1 → 0.7.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 (81) hide show
  1. package/package.json +3 -7
  2. package/scripts/doctor-check.ts +20 -0
  3. package/scripts/install-cursor-plugin.sh +51 -28
  4. package/scripts/install-opencode-plugin.sh +19 -21
  5. package/scripts/rewrite-workspace-deps.ts +15 -9
  6. package/scripts/sync-runtime.sh +71 -19
  7. package/scripts/vendor-assets.ts +37 -0
  8. package/skills/wk-implement/SKILL.md +2 -2
  9. package/skills/wk-pr/SKILL.md +1 -1
  10. package/src/core/boundary.ts +27 -0
  11. package/src/core/branch-policy.ts +63 -0
  12. package/src/core/branch.ts +30 -16
  13. package/src/core/config.ts +193 -31
  14. package/src/core/docs-layout.ts +251 -0
  15. package/src/core/docs-migration.ts +639 -0
  16. package/src/core/docs-repo.ts +11 -9
  17. package/src/core/docs-validate.ts +18 -6
  18. package/src/core/doctor.ts +801 -0
  19. package/src/core/flow-state.ts +1579 -141
  20. package/src/core/git.ts +22 -5
  21. package/src/{tools/handoff.ts → core/handoff-tools.ts} +5 -57
  22. package/src/core/hygiene.ts +26 -12
  23. package/src/core/init.ts +43 -11
  24. package/src/core/logger.ts +321 -0
  25. package/src/core/package-root.ts +28 -0
  26. package/src/core/ports/init-toolkit-status.ts +1 -1
  27. package/src/core/ports/vcs-verify-token.ts +1 -1
  28. package/src/core/ports/youtrack-api.ts +1 -1
  29. package/src/core/ports/youtrack-verify-token.ts +1 -1
  30. package/src/core/pr-create.ts +116 -21
  31. package/src/core/registration.ts +215 -0
  32. package/src/core/repo-context.ts +447 -0
  33. package/src/core/repo-tools.ts +23 -0
  34. package/src/core/safe-write.ts +22 -0
  35. package/src/core/scripts.ts +3 -44
  36. package/src/core/sdd.ts +45 -28
  37. package/src/core/setup-state.ts +54 -0
  38. package/src/core/setup.ts +1216 -0
  39. package/src/core/skill-manifests.ts +95 -0
  40. package/src/core/support-matrix.ts +12 -0
  41. package/src/core/sync-runtime.ts +348 -0
  42. package/src/core/templates.ts +2 -2
  43. package/src/core/vcs-config.ts +107 -37
  44. package/src/core/verify-project.ts +181 -0
  45. package/src/core/workspaces.ts +136 -17
  46. package/src/core/youtrack-tools.ts +228 -0
  47. package/src/core/youtrack.ts +125 -67
  48. package/templates/execution-contract.md +9 -7
  49. package/templates/superpowers-doc-contract.md +4 -3
  50. package/scripts/_shared/common.sh +0 -173
  51. package/scripts/changelog-context.sh +0 -42
  52. package/scripts/docs-refresh-context.sh +0 -40
  53. package/scripts/init/apply.sh +0 -5
  54. package/scripts/init/status.sh +0 -5
  55. package/scripts/init/toolkit-status.sh +0 -5
  56. package/scripts/pr-create.sh +0 -5
  57. package/scripts/pr-ready-context.sh +0 -88
  58. package/scripts/present/ascii-wireframe.sh +0 -5
  59. package/scripts/present/flow-diagram.sh +0 -5
  60. package/scripts/release-notes-context.sh +0 -40
  61. package/scripts/vcs/config.sh +0 -5
  62. package/scripts/vcs/merged-style.sh +0 -5
  63. package/scripts/vcs/token-create-urls.sh +0 -5
  64. package/scripts/vcs/verify-token.sh +0 -5
  65. package/scripts/verify-project.sh +0 -140
  66. package/scripts/youtrack/api.sh +0 -5
  67. package/scripts/youtrack/config.sh +0 -5
  68. package/scripts/youtrack/greeting.sh +0 -5
  69. package/scripts/youtrack/parse-duration.sh +0 -5
  70. package/scripts/youtrack/token-create-url.sh +0 -5
  71. package/scripts/youtrack/verify-token.sh +0 -5
  72. package/scripts/youtrack/work-date-ms.sh +0 -5
  73. package/src/tools/docs-repo.ts +0 -51
  74. package/src/tools/flow.ts +0 -99
  75. package/src/tools/index.ts +0 -22
  76. package/src/tools/present.ts +0 -49
  77. package/src/tools/repo.ts +0 -490
  78. package/src/tools/rules.ts +0 -30
  79. package/src/tools/sdd.ts +0 -216
  80. package/src/tools/templates.ts +0 -27
  81. package/src/tools/youtrack.ts +0 -423
package/src/core/git.ts CHANGED
@@ -2,7 +2,13 @@ import { execFileSync } from "node:child_process";
2
2
 
3
3
  const run = (cwd: string, args: string[]): { stdout: string; stderr: string; exitCode: number } => {
4
4
  try {
5
- const stdout = execFileSync("git", args, { cwd, encoding: "utf8" });
5
+ const stdout = execFileSync("git", args, {
6
+ cwd,
7
+ encoding: "utf8",
8
+ // AR-14: expected negative fixtures (non-repo dirs, unborn HEADs, bogus
9
+ // shas) must not inherit raw git usage/fatal stderr into the suite output.
10
+ stdio: ["pipe", "pipe", "pipe"],
11
+ });
6
12
  return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
7
13
  } catch (error) {
8
14
  const e = error as { stdout?: string; stderr?: string; status?: number };
@@ -16,8 +22,18 @@ const run = (cwd: string, args: string[]): { stdout: string; stderr: string; exi
16
22
 
17
23
  export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
18
24
  const cwd = workspaceRoot;
19
- const branch = run(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]).stdout || "unknown";
20
- const status_short = run(cwd, ["status", "--porcelain"]).stdout;
25
+ let failure: { stderr: string; exitCode: number } | undefined;
26
+
27
+ const runHere = (args: string[]) => {
28
+ const result = run(cwd, args);
29
+ if (!failure && result.exitCode !== 0) {
30
+ failure = { stderr: result.stderr, exitCode: result.exitCode };
31
+ }
32
+ return result.stdout;
33
+ };
34
+
35
+ const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
36
+ const status_short = runHere(["status", "--porcelain"]);
21
37
 
22
38
  const staged: string[] = [];
23
39
  const unstaged: string[] = [];
@@ -35,8 +51,8 @@ export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
35
51
  }
36
52
 
37
53
  const pathArgs = paths.length ? ["--", ...paths] : [];
38
- const diff_stat = run(cwd, ["diff", "--stat", ...pathArgs]).stdout;
39
- const cached_stat = run(cwd, ["diff", "--cached", "--stat", ...pathArgs]).stdout;
54
+ const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
55
+ const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
40
56
 
41
57
  const stagedSet = new Set(staged);
42
58
  const unstagedSet = new Set(unstaged);
@@ -52,5 +68,6 @@ export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
52
68
  diff_stat: [diff_stat, cached_stat].filter(Boolean).join("\n"),
53
69
  partial_staged: partial_staged.length > 0,
54
70
  partial_staged_files: partial_staged,
71
+ ...(failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}),
55
72
  };
56
73
  };
@@ -1,10 +1,7 @@
1
1
  import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { tool, type PluginInput } from "@opencode-ai/plugin";
4
2
  import { fail, ok, type Result } from "../core";
5
- import { assertFlowGates } from "../core/flow-state";
6
- import { resolveWorkflowPaths, buildHandoffContract } from "../core/handoff-context";
7
- import { WorkflowStateStore } from "../state";
3
+ import { resolveWorkflowPaths, buildHandoffContract } from "./handoff-context";
4
+ import { assetRoot } from "./package-root";
8
5
 
9
6
  type ApiResponse<T> = { data?: T; error?: unknown };
10
7
  type ApiResult<T> = Promise<ApiResponse<T>>;
@@ -31,20 +28,6 @@ export type HandoffClient = {
31
28
  };
32
29
  };
33
30
 
34
- export const adaptPluginHandoffClient = (client: PluginInput["client"]): HandoffClient => ({
35
- session: {
36
- create: (input) => client.session.create(input),
37
- promptAsync: (input) => client.session.promptAsync(input),
38
- },
39
- tui: {
40
- selectSession: ({ body, query }) =>
41
- client.tui.publish({
42
- body: { type: "tui.session.select", properties: { sessionID: body.sessionID } } as never,
43
- query,
44
- }),
45
- },
46
- });
47
-
48
31
  export type HandoffRequest = {
49
32
  directory: string;
50
33
  title: string;
@@ -59,13 +42,13 @@ type HandoffData = {
59
42
  stage?: "create" | "seed" | "select";
60
43
  };
61
44
 
62
- const message = (error: unknown) =>
45
+ export const message = (error: unknown) =>
63
46
  error instanceof Error
64
47
  ? error.message
65
48
  : typeof error === "object" && error !== null && "message" in error
66
49
  ? String(error.message)
67
50
  : String(error);
68
- const apiError = (response: ApiResponse<unknown> | void) => response?.error;
51
+ export const apiError = (response: ApiResponse<unknown> | void) => response?.error;
69
52
 
70
53
  export async function handoffSession(
71
54
  client: HandoffClient,
@@ -110,8 +93,6 @@ export async function handoffSession(
110
93
  }
111
94
  }
112
95
 
113
- const output = (value: unknown) => JSON.stringify(value, null, 2);
114
-
115
96
  export type HandoffContextResult =
116
97
  | { prompt: string; spec: string; plan: string; sdd: string }
117
98
  | { error: string };
@@ -119,10 +100,7 @@ export type HandoffContextResult =
119
100
  export const buildHandoffPrompt = (root: string, message: string): HandoffContextResult => {
120
101
  const resolved = resolveWorkflowPaths(root, message);
121
102
  if ("error" in resolved) return { error: resolved.error };
122
- const templatePath = path.resolve(
123
- path.dirname(fileURLToPath(import.meta.url)),
124
- "../../templates/execution-contract.md",
125
- );
103
+ const templatePath = path.join(assetRoot(), "templates", "execution-contract.md");
126
104
  const contract = buildHandoffContract({
127
105
  root,
128
106
  spec: resolved.spec,
@@ -133,33 +111,3 @@ export const buildHandoffPrompt = (root: string, message: string): HandoffContex
133
111
  const sdd = `docs/${path.basename(path.dirname(resolved.plan))}/sdd`;
134
112
  return { prompt: contract.prompt, spec: resolved.spec, plan: resolved.plan, sdd };
135
113
  };
136
-
137
- export function createHandoffTools(client: HandoffClient, state: WorkflowStateStore) {
138
- return {
139
- workflow_handoff_session: tool({
140
- description:
141
- "Create, seed, and select a continuation session; --stay in the message skips selection",
142
- args: { message: tool.schema.string() },
143
- execute: async ({ message: userMessage }, context) => {
144
- const built = buildHandoffPrompt(context.directory, userMessage);
145
- if ("error" in built) return output(fail(built.error));
146
- const active = built;
147
- try {
148
- const gate = assertFlowGates(context.directory, active.plan);
149
- if (!gate.ok) return output(fail(gate.error));
150
- state.set(context.sessionID, { spec: active.spec, plan: active.plan, sdd: active.sdd });
151
- return output(
152
- await handoffSession(client, {
153
- directory: context.directory,
154
- title: `Continue ${path.basename(path.dirname(active.plan))}`,
155
- prompt: active.prompt,
156
- stay: /(?:^|\s)--stay(?:\s|$)/.test(userMessage),
157
- }),
158
- );
159
- } catch (error) {
160
- return output(fail(message(error)));
161
- }
162
- },
163
- }),
164
- };
165
- }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { changelogUnreleasedStats } from "./changelog";
4
+ import { assetRoot } from "./package-root";
5
5
 
6
6
  export type HygieneFile =
7
7
  | "CHANGELOG.md"
@@ -12,7 +12,7 @@ export type HygieneFile =
12
12
  | "CONTRIBUTING.md";
13
13
  type State = "missing" | "invalid" | "ok" | "skip";
14
14
 
15
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
15
+ const repoRoot = assetRoot();
16
16
  const templatesDir = () => path.join(repoRoot, "templates", "hygiene");
17
17
 
18
18
  const packageJson = (root: string): Record<string, unknown> | null => {
@@ -75,25 +75,39 @@ export const hygieneFiles = (
75
75
  return { state, openSource };
76
76
  };
77
77
 
78
- export const ensureHygieneFiles = (
78
+ // Pure planner: which hygiene files would be created and with what content,
79
+ // without touching the filesystem. Drives the wizard preview (WZ-08) and the
80
+ // apply path; ensureHygieneFiles is just "plan then write".
81
+ export const planHygieneFiles = (
79
82
  root: string,
80
- opts: { confirmed: boolean; includeOpenSource?: boolean },
81
- ): { ok: true; created: string[] } | { ok: false; error: string } => {
82
- if (!opts.confirmed) return { ok: false, error: "confirmed: true required" };
83
+ opts: { includeOpenSource?: boolean } = {},
84
+ ): Array<{ path: string; content: string }> => {
85
+ const openSource = opts.includeOpenSource ?? isOpenSource(root);
83
86
  const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"] as HygieneFile[];
84
- if (opts.includeOpenSource) files.push("LICENSE", "CONTRIBUTING.md");
85
- const created: string[] = [];
86
- const tplDir = templatesDir();
87
+ if (openSource) files.push("LICENSE", "CONTRIBUTING.md");
88
+ const planned: Array<{ path: string; content: string }> = [];
87
89
  for (const file of files) {
88
90
  if (existsSync(path.join(root, file))) continue;
89
- const tpl = path.join(tplDir, file);
91
+ const tpl = path.join(templatesDir(), file);
90
92
  if (!existsSync(tpl)) continue; // skip missing template, never fail
91
93
  const content = readFileSync(tpl, "utf8")
92
94
  .replace(/<PROJECT>/g, path.basename(root))
93
95
  .replace(/<YEAR>/g, String(new Date().getFullYear()))
94
96
  .replace(/<HOLDER>\s*/g, licenseHolder(root));
95
- writeFileSync(path.join(root, file), content, "utf8");
96
- created.push(file);
97
+ planned.push({ path: path.join(root, file), content });
98
+ }
99
+ return planned;
100
+ };
101
+
102
+ export const ensureHygieneFiles = (
103
+ root: string,
104
+ opts: { confirmed: boolean; includeOpenSource?: boolean },
105
+ ): { ok: true; created: string[] } | { ok: false; error: string } => {
106
+ if (!opts.confirmed) return { ok: false, error: "confirmed: true required" };
107
+ const created: string[] = [];
108
+ for (const planned of planHygieneFiles(root, opts)) {
109
+ writeFileSync(planned.path, planned.content, "utf8");
110
+ created.push(path.basename(planned.path));
97
111
  }
98
112
  return { ok: true, created };
99
113
  };
package/src/core/init.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { configDir } from "./config";
3
+ import { configDir, isConfigObject } from "./config";
4
4
  import { PLUGIN_ROOT } from "./scripts";
5
+ import { writeFileExclusive } from "./safe-write";
5
6
  import { resolveWorkspace, workspacesPath } from "./workspaces";
7
+ import { applyWorkspaceBranchPolicy } from "./setup";
6
8
  import { vcsTokenCreateUrls, vcsVerifyToken } from "./vcs-config";
7
9
  import { youTrackTokenCreateUrl, youTrackVerifyToken } from "./youtrack";
8
10
 
9
11
  const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
10
12
 
13
+ // AR-07/CA-37: a parseable non-object (null, scalar, array) is not a config
14
+ // file — never display it as configured (fail-open) nor as unconfigured.
11
15
  const readJson = (p: string): Record<string, any> | null => {
12
16
  try {
13
- return JSON.parse(fs.readFileSync(p, "utf8")) as Record<string, any>;
17
+ const parsed: unknown = JSON.parse(fs.readFileSync(p, "utf8"));
18
+ return isConfigObject(parsed) ? (parsed as Record<string, any>) : null;
14
19
  } catch {
15
20
  return null;
16
21
  }
@@ -97,7 +102,18 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
97
102
  youtrackConfig.tokenCreate = youtrackTokenCreate;
98
103
  }
99
104
  } else if (fs.existsSync(ytJson)) {
100
- youtrackConfig = { config_edit_path: resolvePath(ytJson), error: "invalid youtrack.json" };
105
+ // Distinguish parse failure (legacy message, path in config_edit_path) from a
106
+ // parseable non-object, which gets the shared shape diagnostic with the path.
107
+ let parseFailed = false;
108
+ try {
109
+ JSON.parse(fs.readFileSync(ytJson, "utf8"));
110
+ } catch {
111
+ parseFailed = true;
112
+ }
113
+ youtrackConfig = {
114
+ config_edit_path: resolvePath(ytJson),
115
+ error: parseFailed ? "invalid youtrack.json" : `${resolvePath(ytJson)} is not a JSON object`,
116
+ };
101
117
  }
102
118
 
103
119
  items.push({
@@ -239,7 +255,7 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
239
255
  }
240
256
 
241
257
  /** Port of scripts/init/toolkit-status.sh — filesystem + API health check. */
242
- export function toolkitStatusData(configDirPath = configDir()): Record<string, any> {
258
+ export async function toolkitStatusData(configDirPath = configDir()): Promise<Record<string, any>> {
243
259
  const status = initStatusData(configDirPath);
244
260
  const tokenItem = status.items.find((i: Record<string, any>) => i.id === "youtrack_token") ?? {};
245
261
  const placeholder = Boolean(tokenItem.placeholder);
@@ -253,13 +269,14 @@ export function toolkitStatusData(configDirPath = configDir()): Record<string, a
253
269
 
254
270
  const verify = placeholder
255
271
  ? { ok: false, error: "token still placeholder YOUR_TOKEN_HERE" }
256
- : youTrackVerifyToken();
272
+ : await youTrackVerifyToken();
257
273
  const vcsVerify = vcsPlaceholder
258
274
  ? { ok: false, error: "vcs token still placeholder YOUR_TOKEN_HERE" }
259
- : vcsVerifyToken();
275
+ : await vcsVerifyToken();
276
+ const youTrackHealth = ("data" in verify ? verify.data : verify) as Record<string, any>;
260
277
 
261
278
  status.youtrack_verify = verify;
262
- status.youtrack_ok = placeholder ? false : Boolean((verify as Record<string, any>).ok);
279
+ status.youtrack_ok = placeholder ? false : Boolean(youTrackHealth.ok);
263
280
  status.vcs_verify = vcsVerify;
264
281
  status.vcs_ok =
265
282
  vcsJsonOk && !vcsPlaceholder ? Boolean((vcsVerify as Record<string, any>).ok) : false;
@@ -361,7 +378,9 @@ export function initApplyData(
361
378
  }
362
379
  case "youtrack_token_placeholder": {
363
380
  const p = path.join(dir, "youtrack.token");
364
- fs.writeFileSync(p, TOKEN_PLACEHOLDER + "\n", { encoding: "utf8", mode: 0o600 });
381
+ // wx + EEXIST-as-preserved (CA-13): an existing real token is never
382
+ // clobbered — shared with the CLI wizard's ensureToken via safe-write.
383
+ const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER + "\n", 0o600) === "preserved";
365
384
  const abs = path.resolve(p);
366
385
  return {
367
386
  action,
@@ -369,6 +388,7 @@ export function initApplyData(
369
388
  path: abs,
370
389
  token_edit_path: abs,
371
390
  placeholder: TOKEN_PLACEHOLDER,
391
+ preserved,
372
392
  instruction: `Open ${abs} in your editor, replace YOUR_TOKEN_HERE with your YouTrack permanent token, save, then run /wk-status`,
373
393
  };
374
394
  }
@@ -376,7 +396,8 @@ export function initApplyData(
376
396
  const jsonOut = path.join(dir, "youtrack.json");
377
397
  const tokenOut = path.join(dir, "youtrack.token");
378
398
  fs.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + "\n", "utf8");
379
- fs.writeFileSync(tokenOut, TOKEN_PLACEHOLDER + "\n", { encoding: "utf8", mode: 0o600 });
399
+ const preserved =
400
+ writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER + "\n", 0o600) === "preserved";
380
401
  const configPath = path.resolve(jsonOut);
381
402
  const tokenPath = path.resolve(tokenOut);
382
403
  const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
@@ -397,6 +418,7 @@ export function initApplyData(
397
418
  token_create: tokenCreate,
398
419
  config_edit_path: configPath,
399
420
  placeholder: TOKEN_PLACEHOLDER,
421
+ preserved,
400
422
  youtrack_config: {
401
423
  config_edit_path: configPath,
402
424
  baseUrl: base,
@@ -433,8 +455,11 @@ export function initApplyData(
433
455
  fs.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(dir), null, 2) + "\n", "utf8");
434
456
  const glPath = path.join(dir, "gitlab.token");
435
457
  const ghPath = path.join(dir, "github.token");
458
+ const preservedTokens: string[] = [];
436
459
  for (const p of [glPath, ghPath]) {
437
- fs.writeFileSync(p, TOKEN_PLACEHOLDER + "\n", { encoding: "utf8", mode: 0o600 });
460
+ if (writeFileExclusive(p, TOKEN_PLACEHOLDER + "\n", 0o600) === "preserved") {
461
+ preservedTokens.push(path.resolve(p));
462
+ }
438
463
  }
439
464
  const configPath = path.resolve(jsonOut);
440
465
  const prev = process.env.WORKFLOW_VCS_CONFIG;
@@ -455,6 +480,7 @@ export function initApplyData(
455
480
  token_edit_path: activePath,
456
481
  token_create_url: active.createUrl,
457
482
  token_create_urls: tokenUrls,
483
+ preserved_tokens: preservedTokens,
458
484
  vcs_config: {
459
485
  config_edit_path: configPath,
460
486
  provider,
@@ -472,6 +498,12 @@ export function initApplyData(
472
498
  else process.env.WORKFLOW_VCS_CONFIG = prev;
473
499
  }
474
500
  }
501
+ case "branch_policy": {
502
+ const root = env.WORKFLOW_WORKSPACE_ROOT?.trim()
503
+ ? env.WORKFLOW_WORKSPACE_ROOT
504
+ : process.cwd();
505
+ return applyWorkspaceBranchPolicy({ workspace_root: root, env });
506
+ }
475
507
  default: {
476
508
  return {
477
509
  error: `unknown action ${action} (youtrack_scaffold|youtrack_json|youtrack_token_placeholder|vcs_scaffold)`,
@@ -488,7 +520,7 @@ export function initStatus(): Record<string, any> {
488
520
  };
489
521
  }
490
522
 
491
- export function toolkitStatus(): Record<string, any> {
523
+ export async function toolkitStatus(): Promise<Record<string, any>> {
492
524
  return toolkitStatusData();
493
525
  }
494
526