@brainervirus/workit-core 0.6.0 → 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 (95) hide show
  1. package/package.json +10 -14
  2. package/scripts/doctor-check.ts +20 -0
  3. package/scripts/install-cursor-plugin.sh +51 -28
  4. package/scripts/install-opencode-plugin.sh +25 -22
  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 +166 -57
  13. package/src/core/changelog.ts +17 -14
  14. package/src/core/config-guard.ts +9 -2
  15. package/src/core/config.ts +227 -32
  16. package/src/core/detector.ts +22 -11
  17. package/src/core/docs-layout.ts +251 -0
  18. package/src/core/docs-migration.ts +639 -0
  19. package/src/core/docs-repo.ts +58 -21
  20. package/src/core/docs-validate.ts +181 -43
  21. package/src/core/doctor.ts +801 -0
  22. package/src/core/flow-state.ts +1579 -137
  23. package/src/core/git.ts +22 -5
  24. package/src/core/gitignore.ts +11 -2
  25. package/src/core/handoff-context.ts +18 -5
  26. package/src/{tools/handoff.ts → core/handoff-tools.ts} +16 -63
  27. package/src/core/hygiene.ts +53 -17
  28. package/src/core/init.ts +128 -31
  29. package/src/core/logger.ts +321 -0
  30. package/src/core/package-root.ts +28 -0
  31. package/src/core/parse-sections.ts +2 -2
  32. package/src/core/plan-tasks.ts +13 -3
  33. package/src/core/ports/init-toolkit-status.ts +1 -1
  34. package/src/core/ports/vcs-verify-token.ts +1 -1
  35. package/src/core/ports/youtrack-api.ts +4 -2
  36. package/src/core/ports/youtrack-config.ts +1 -3
  37. package/src/core/ports/youtrack-verify-token.ts +1 -1
  38. package/src/core/pr-create.ts +156 -29
  39. package/src/core/present.ts +11 -2
  40. package/src/core/registration.ts +215 -0
  41. package/src/core/reminder.ts +1 -2
  42. package/src/core/repo-context.ts +447 -0
  43. package/src/core/repo-tool.ts +4 -1
  44. package/src/core/repo-tools.ts +23 -0
  45. package/src/core/rules.ts +10 -7
  46. package/src/core/safe-write.ts +22 -0
  47. package/src/core/scripts.ts +3 -39
  48. package/src/core/sdd.ts +56 -31
  49. package/src/core/setup-state.ts +54 -0
  50. package/src/core/setup.ts +1216 -0
  51. package/src/core/skill-manifests.ts +95 -0
  52. package/src/core/support-matrix.ts +12 -0
  53. package/src/core/sync-runtime.ts +348 -0
  54. package/src/core/templates.ts +16 -6
  55. package/src/core/vcs-config.ts +191 -62
  56. package/src/core/verify-parse.ts +4 -2
  57. package/src/core/verify-project.ts +181 -0
  58. package/src/core/workspaces.ts +136 -17
  59. package/src/core/youtrack-tools.ts +228 -0
  60. package/src/core/youtrack.ts +320 -87
  61. package/src/core.ts +18 -3
  62. package/templates/execution-contract.md +9 -7
  63. package/templates/superpowers-doc-contract.md +5 -4
  64. package/scripts/_shared/common.sh +0 -158
  65. package/scripts/changelog-context.sh +0 -42
  66. package/scripts/docs-refresh-context.sh +0 -40
  67. package/scripts/init/apply.sh +0 -5
  68. package/scripts/init/status.sh +0 -5
  69. package/scripts/init/toolkit-status.sh +0 -5
  70. package/scripts/pr-create.sh +0 -5
  71. package/scripts/pr-ready-context.sh +0 -88
  72. package/scripts/present/ascii-wireframe.sh +0 -5
  73. package/scripts/present/flow-diagram.sh +0 -5
  74. package/scripts/release-notes-context.sh +0 -40
  75. package/scripts/vcs/config.sh +0 -5
  76. package/scripts/vcs/merged-style.sh +0 -5
  77. package/scripts/vcs/token-create-urls.sh +0 -5
  78. package/scripts/vcs/verify-token.sh +0 -5
  79. package/scripts/verify-project.sh +0 -140
  80. package/scripts/youtrack/api.sh +0 -5
  81. package/scripts/youtrack/config.sh +0 -5
  82. package/scripts/youtrack/greeting.sh +0 -5
  83. package/scripts/youtrack/parse-duration.sh +0 -5
  84. package/scripts/youtrack/token-create-url.sh +0 -5
  85. package/scripts/youtrack/verify-token.sh +0 -5
  86. package/scripts/youtrack/work-date-ms.sh +0 -5
  87. package/src/tools/docs-repo.ts +0 -42
  88. package/src/tools/flow.ts +0 -88
  89. package/src/tools/index.ts +0 -22
  90. package/src/tools/present.ts +0 -45
  91. package/src/tools/repo.ts +0 -357
  92. package/src/tools/rules.ts +0 -30
  93. package/src/tools/sdd.ts +0 -189
  94. package/src/tools/templates.ts +0 -27
  95. package/src/tools/youtrack.ts +0 -360
@@ -3,22 +3,46 @@ import { execFileSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { gitContext } from "./git";
5
5
  import { readConfig, resolveBranchPolicy } from "./config";
6
+ import { resolveWorkspace } from "./workspaces";
7
+ import { vcsConfig } from "./vcs-config";
6
8
 
7
- const policy = () => resolveBranchPolicy(readConfig());
8
- const allowedBranch = (name: string) => policy().allowed.some((r) => r.test(name));
9
- const isProtected = (name: string) => policy().protected.has(name.toLowerCase());
9
+ /** CA-09: the one policy resolver every consumer calls. */
10
+ export const resolveBranchPolicyFor = (workspaceRoot: string) =>
11
+ resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
12
+
13
+ const policy = (root: string) => resolveBranchPolicyFor(root);
14
+ const allowedBranch = (root: string, name: string) =>
15
+ policy(root).allowed.some((r) => r.test(name));
16
+ const isProtected = (root: string, name: string) => policy(root).protected.has(name.toLowerCase());
17
+ // RL-01: malformed vcs.json blocks branch resolution with an exact-path error.
18
+ const baseBranch = (cwd: string): { base: string } | { error: string } => {
19
+ const resolved = vcsConfig("resolve", cwd);
20
+ if (resolved.ok === false) return { error: String(resolved.error) };
21
+ return { base: String(resolved.defaultTargetBranch ?? "develop") };
22
+ };
10
23
  const DECLARE_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/gim;
11
24
  const USE_CURRENT_RE = /^\s*\*+Branch:\*+\s*use-current\s*$/im;
12
25
  const readSafe = (p: string): string | null => {
13
- try { return readFileSync(p, "utf8"); } catch { return null; }
26
+ try {
27
+ return readFileSync(p, "utf8");
28
+ } catch {
29
+ return null;
30
+ }
14
31
  };
15
32
 
16
- const normalizeBranch = (name: string): string | null => {
33
+ const normalizeBranch = (root: string, name: string): string | null => {
17
34
  const n = name.trim().replace(/`/g, "").replace(/\.+$/, "");
18
- if (isProtected(n)) return null;
19
- if (!allowedBranch(n)) return null;
20
- const parts = n.toLowerCase().split("/").map((p) =>
21
- p.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-"));
35
+ if (isProtected(root, n)) return null;
36
+ if (!allowedBranch(root, n)) return null;
37
+ const parts = n
38
+ .toLowerCase()
39
+ .split("/")
40
+ .map((p) =>
41
+ p
42
+ .replace(/[^\w.-]+/g, "-")
43
+ .replace(/^-+|-+$/g, "")
44
+ .replace(/-{2,}/g, "-"),
45
+ );
22
46
  if (parts.some((p) => !p)) return null;
23
47
  return parts.join("/");
24
48
  };
@@ -29,14 +53,21 @@ const deriveSlug = (planPath: string): string => {
29
53
  return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
30
54
  };
31
55
 
32
- const deriveKind = (planPath: string, fallback: "feature" | "bugfix" = "feature"): "feature" | "bugfix" => {
56
+ const deriveKind = (
57
+ planPath: string,
58
+ fallback: "feature" | "bugfix" = "feature",
59
+ ): "feature" | "bugfix" => {
33
60
  const slug = deriveSlug(planPath);
34
61
  const text = readSafe(planPath) ?? "";
35
62
  let kind = fallback;
36
63
  if (/\bbugfix\b/i.test(slug) || /^fix-/i.test(slug)) {
37
64
  kind = "bugfix";
38
65
  } else {
39
- const goal = text.split("\n").find((line) => line.startsWith("**Goal:**"))?.toLowerCase() ?? "";
66
+ const goal =
67
+ text
68
+ .split("\n")
69
+ .find((line) => line.startsWith("**Goal:**"))
70
+ ?.toLowerCase() ?? "";
40
71
  if (/\b(bugfix|bug fix)\b/.test(goal) && !/\b(feat|feature|upgrade|add)\b/.test(goal)) {
41
72
  kind = "bugfix";
42
73
  }
@@ -49,7 +80,11 @@ export const resolveBranch = ({
49
80
  spec_path,
50
81
  plan_path,
51
82
  workspace_root,
52
- }: { spec_path: string; plan_path: string; workspace_root: string }) => {
83
+ }: {
84
+ spec_path: string;
85
+ plan_path: string;
86
+ workspace_root: string;
87
+ }) => {
53
88
  const cwd = path.resolve(workspace_root);
54
89
  const abs = (p: string) => (path.isAbsolute(p) ? p : path.join(cwd, p));
55
90
  const spec = abs(spec_path);
@@ -69,27 +104,30 @@ export const resolveBranch = ({
69
104
  const text = readSafe(file);
70
105
  if (!text) continue;
71
106
  if (USE_CURRENT_RE.test(text)) {
72
- if (!current || !allowedBranch(current) || isProtected(current)) {
107
+ if (!current || !allowedBranch(cwd, current) || isProtected(cwd, current)) {
73
108
  return { error: `use-current but HEAD ${current} is not an allowed branch` };
74
109
  }
75
110
  return finish(current, "use-current");
76
111
  }
77
112
  }
78
113
 
79
- if (current && allowedBranch(current) && !isProtected(current)) return finish(current, "keep-current");
114
+ if (current && allowedBranch(cwd, current) && !isProtected(cwd, current))
115
+ return finish(current, "keep-current");
80
116
 
81
117
  let declaredButInvalid: string | null = null;
82
118
  for (const file of [spec, plan]) {
83
119
  const text = readSafe(file);
84
120
  if (!text) continue;
85
121
  for (const match of text.matchAll(DECLARE_RE)) {
86
- const normalized = normalizeBranch(match[1]);
122
+ const normalized = normalizeBranch(cwd, match[1]);
87
123
  if (normalized) return finish(normalized, file === spec ? "spec" : "plan");
88
124
  declaredButInvalid ??= match[1];
89
125
  }
90
126
  }
91
127
  if (declaredButInvalid) {
92
- return { error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy` };
128
+ return {
129
+ error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy`,
130
+ };
93
131
  }
94
132
 
95
133
  const slug = deriveSlug(plan);
@@ -103,55 +141,103 @@ export const docsBranch = ({
103
141
  plan_path,
104
142
  kind,
105
143
  workspace_root,
106
- }: { plan_path?: string; kind?: string; workspace_root: string }) => {
144
+ }: {
145
+ plan_path?: string;
146
+ kind?: string;
147
+ workspace_root: string;
148
+ }) => {
107
149
  const cwd = path.resolve(workspace_root);
108
150
  const git = gitContext(cwd);
109
151
  const current = git.branch;
110
152
  const kindArg = (kind ?? "feature").toLowerCase();
153
+ const baseResolved = baseBranch(cwd);
154
+ if ("error" in baseResolved) return { error: baseResolved.error };
155
+ const base = baseResolved.base;
111
156
 
112
- if (current && allowedBranch(current) && !isProtected(current)) {
113
- return { branch: current, action: "keep", current_branch: current, base: "develop", dirty: Boolean(git.status_short.trim()) };
114
- }
115
- if (current === "main" || current === "master" || current === "develop") {
157
+ if (current === base || current === "main" || current === "master" || current === "develop") {
116
158
  let slug = "";
117
159
  if (plan_path) {
118
160
  const plan = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
119
161
  slug = deriveSlug(plan);
120
162
  }
121
163
  if (!slug) {
122
- return { error: "plan_path required to derive branch slug when not on feature/* or bugfix/*" };
164
+ return {
165
+ error: "plan_path required to derive branch slug when not on feature/* or bugfix/*",
166
+ };
123
167
  }
124
168
  const branchKind = kindArg === "bugfix" ? "bugfix" : "feature";
125
- return { branch: `${branchKind}/${slug}`, action: "create_from_develop", current_branch: current, base: "develop", dirty: Boolean(git.status_short.trim()) };
169
+ return {
170
+ branch: `${branchKind}/${slug}`,
171
+ action: base === "develop" ? "create_from_develop" : "create_from_base",
172
+ current_branch: current,
173
+ base,
174
+ dirty: Boolean(git.status_short.trim()),
175
+ };
176
+ }
177
+ if (current && allowedBranch(cwd, current) && !isProtected(cwd, current)) {
178
+ return {
179
+ branch: current,
180
+ action: "keep",
181
+ current_branch: current,
182
+ base,
183
+ dirty: Boolean(git.status_short.trim()),
184
+ };
126
185
  }
127
186
  return { error: `cannot resolve docs branch from HEAD ${JSON.stringify(current)}` };
128
187
  };
129
188
 
130
189
  // Port of scripts/lib/ensure-develop-base.sh
131
- export const ensureDevelopBase = (cwd: string): { ok: boolean; error?: string } => {
190
+ export const ensureBaseBranch = (cwd: string, base: string): { ok: boolean; error?: string } => {
132
191
  const git = gitContext(cwd);
133
- if (!git.branch || git.branch === "unknown") return { ok: false, error: "not in a git repository" };
134
- const run = (args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
192
+ if (!git.branch || git.branch === "unknown")
193
+ return { ok: false, error: "not in a git repository" };
194
+ const run = (args: string[]) =>
195
+ execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
135
196
  try {
136
197
  try {
137
- run(["fetch", "origin", "develop", "--prune"]);
198
+ run(["fetch", "origin", base, "--prune"]);
138
199
  } catch {
139
200
  run(["fetch", "origin", "--prune"]);
140
201
  }
141
- let hasOriginDevelop = true;
142
- try { execFileSync("git", ["show-ref", "--verify", "--quiet", "refs/remotes/origin/develop"], { cwd, stdio: "pipe" }); } catch { hasOriginDevelop = false; }
143
- if (!hasOriginDevelop) return { ok: false, error: "origin/develop missing — push develop before creating feature/* or bugfix/* branches" };
144
- let hasLocalDevelop = true;
145
- try { execFileSync("git", ["show-ref", "--verify", "--quiet", "refs/heads/develop"], { cwd, stdio: "pipe" }); } catch { hasLocalDevelop = false; }
146
- if (hasLocalDevelop) {
147
- run(["checkout", "develop"]);
148
- try { run(["merge", "--ff-only", "origin/develop"]); } catch { /* non-fast-forward: keep local */ }
202
+ let hasOriginBase = true;
203
+ try {
204
+ execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
205
+ cwd,
206
+ stdio: "pipe",
207
+ });
208
+ } catch {
209
+ hasOriginBase = false;
210
+ }
211
+ if (!hasOriginBase)
212
+ return {
213
+ ok: false,
214
+ error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`,
215
+ };
216
+ let hasLocalBase = true;
217
+ try {
218
+ execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
219
+ cwd,
220
+ stdio: "pipe",
221
+ });
222
+ } catch {
223
+ hasLocalBase = false;
224
+ }
225
+ if (hasLocalBase) {
226
+ run(["checkout", base]);
227
+ try {
228
+ run(["merge", "--ff-only", `origin/${base}`]);
229
+ } catch {
230
+ /* non-fast-forward: keep local */
231
+ }
149
232
  } else {
150
- run(["checkout", "-b", "develop", "--track", "origin/develop"]);
233
+ run(["checkout", "-b", base, "--track", `origin/${base}`]);
151
234
  }
152
235
  return { ok: true };
153
236
  } catch (error) {
154
- return { ok: false, error: error instanceof Error ? error.message : "ensure-develop-base failed" };
237
+ return {
238
+ ok: false,
239
+ error: error instanceof Error ? error.message : "ensure-base-branch failed",
240
+ };
155
241
  }
156
242
  };
157
243
 
@@ -162,17 +248,29 @@ export const branchSetup = ({
162
248
  target_branch,
163
249
  stash,
164
250
  workspace_root,
165
- }: { action?: string; sdd_dir?: string; target_branch?: string; stash?: string; workspace_root: string }) => {
251
+ }: {
252
+ action?: string;
253
+ sdd_dir?: string;
254
+ target_branch?: string;
255
+ stash?: string;
256
+ workspace_root: string;
257
+ }) => {
166
258
  const cwd = path.resolve(workspace_root);
167
259
  const exec = (args: string[]): string =>
168
260
  execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
169
261
  const current = gitContext(cwd).branch;
170
262
  if (!current || current === "unknown") return { error: "not in a git repository" };
171
263
  const sdd = sdd_dir ?? "docs";
172
- const manifestPath = path.isAbsolute(sdd) ? path.join(sdd, "manifest.json") : path.join(cwd, sdd, "manifest.json");
264
+ const manifestPath = path.isAbsolute(sdd)
265
+ ? path.join(sdd, "manifest.json")
266
+ : path.join(cwd, sdd, "manifest.json");
173
267
  mkdirSync(path.dirname(manifestPath), { recursive: true, mode: 0o755 });
174
268
  const readManifest = (): Record<string, unknown> => {
175
- try { return JSON.parse(readFileSync(manifestPath, "utf8")); } catch { return {}; }
269
+ try {
270
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
271
+ } catch {
272
+ return {};
273
+ }
176
274
  };
177
275
  const writeManifest = (data: Record<string, unknown>) =>
178
276
  writeFileSync(manifestPath, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -181,7 +279,9 @@ export const branchSetup = ({
181
279
  const manifest = readManifest();
182
280
  const ref = manifest.stash_ref;
183
281
  if (!ref) return { error: "no stash_ref in manifest" };
184
- try { exec(["stash", "pop", String(ref)]); } catch (error) {
282
+ try {
283
+ exec(["stash", "pop", String(ref)]);
284
+ } catch (error) {
185
285
  return { error: error instanceof Error ? error.message : "stash pop failed" };
186
286
  }
187
287
  delete manifest.stash_ref;
@@ -192,15 +292,19 @@ export const branchSetup = ({
192
292
 
193
293
  const target = target_branch ?? "";
194
294
  if (!target) return { error: "target branch required" };
195
- if (isProtected(target)) return { error: `protected branch ${target}` };
196
- if (!allowedBranch(target)) return { error: `target branch ${target} is not allowed by the branch policy` };
295
+ if (isProtected(cwd, target)) return { error: `protected branch ${target}` };
296
+ if (!allowedBranch(cwd, target))
297
+ return { error: `target branch ${target} is not allowed by the branch policy` };
197
298
 
198
299
  let stash_ref: string | undefined;
199
300
  if (current !== target) {
200
301
  const dirty = Boolean(gitContext(cwd).status_short.trim());
201
302
  if (dirty) {
202
303
  if (stash !== "yes") {
203
- return { error: "dirty working tree — ask with native question, then call workflow_branch_setup with stash=yes" };
304
+ return {
305
+ error:
306
+ "dirty working tree — ask with native question, then call workflow_branch_setup with stash=yes",
307
+ };
204
308
  }
205
309
  try {
206
310
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`, "--", ":!docs/*/sdd"]);
@@ -214,22 +318,20 @@ export const branchSetup = ({
214
318
  } catch (error) {
215
319
  const message = error instanceof Error ? error.message : "checkout failed";
216
320
  if (/worktree/i.test(message)) {
217
- return { error: `branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)` };
321
+ return {
322
+ error: `branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`,
323
+ };
218
324
  }
219
325
  try {
220
- // Branch does not exist yet: base it on develop (never main/master).
221
- const current = gitContext(cwd).branch;
222
- if (current === "main" || current === "master") {
223
- const baseResult = ensureDevelopBase(cwd);
224
- if (!baseResult.ok) return { error: baseResult.error };
225
- } else {
226
- // Already on develop or another feature branch: still require origin/develop to exist.
227
- const baseResult = ensureDevelopBase(cwd);
228
- if (!baseResult.ok) return { error: baseResult.error };
229
- }
326
+ const baseResolved = baseBranch(cwd);
327
+ if ("error" in baseResolved) return { error: baseResolved.error };
328
+ const baseResult = ensureBaseBranch(cwd, baseResolved.base);
329
+ if (!baseResult.ok) return { error: baseResult.error };
230
330
  exec(["checkout", "-b", target]);
231
331
  } catch (createError) {
232
- return { error: createError instanceof Error ? createError.message : "branch create failed" };
332
+ return {
333
+ error: createError instanceof Error ? createError.message : "branch create failed",
334
+ };
233
335
  }
234
336
  }
235
337
  }
@@ -242,5 +344,12 @@ export const branchSetup = ({
242
344
  manifest.stash_created_at = new Date().toISOString();
243
345
  }
244
346
  writeManifest(manifest);
245
- return { action: "setup", ok: true, branch: target, previous_branch: current, stash_ref: stash_ref ?? null, manifest: manifestPath };
347
+ return {
348
+ action: "setup",
349
+ ok: true,
350
+ branch: target,
351
+ previous_branch: current,
352
+ stash_ref: stash_ref ?? null,
353
+ manifest: manifestPath,
354
+ };
246
355
  };
@@ -2,14 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveWorkspaceRoot } from "./scripts";
4
4
 
5
- const CATEGORIES = [
6
- "Added",
7
- "Changed",
8
- "Deprecated",
9
- "Removed",
10
- "Fixed",
11
- "Security",
12
- ];
5
+ const CATEGORIES = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
13
6
 
14
7
  // Port of scripts/changelog/apply-unreleased.py — merge Keep a Changelog
15
8
  // entries into ## [Unreleased] without duplicating ### headings.
@@ -73,10 +66,21 @@ function splitUnreleased(text: string): [string, string, string] {
73
66
  function canonicalCategory(heading: string): string | null {
74
67
  const match = CAT_RE.exec(heading.replace(/\r?\n$/, ""));
75
68
  if (!match) return null;
76
- return CATEGORIES.find((c) => c.toLowerCase() === match[0].replace(/^###\s+/, "").trim().toLowerCase()) ?? null;
69
+ return (
70
+ CATEGORIES.find(
71
+ (c) =>
72
+ c.toLowerCase() ===
73
+ match[0]
74
+ .replace(/^###\s+/, "")
75
+ .trim()
76
+ .toLowerCase(),
77
+ ) ?? null
78
+ );
77
79
  }
78
80
 
79
- function splitSections(body: string): [string[], Array<{ heading: string; category: string | null; body: string[] }>] {
81
+ function splitSections(
82
+ body: string,
83
+ ): [string[], Array<{ heading: string; category: string | null; body: string[] }>] {
80
84
  const preamble: string[] = [];
81
85
  const sections: Array<{ heading: string; category: string | null; body: string[] }> = [];
82
86
  let current: { heading: string; category: string | null; body: string[] } | null = null;
@@ -207,7 +211,8 @@ function applyChangelog(
207
211
  break;
208
212
  }
209
213
  }
210
- text = lines.slice(0, insertAt).join("") + "## [Unreleased]\n\n" + lines.slice(insertAt).join("");
214
+ text =
215
+ lines.slice(0, insertAt).join("") + "## [Unreleased]\n\n" + lines.slice(insertAt).join("");
211
216
  }
212
217
 
213
218
  const [before, body, after] = splitUnreleased(text);
@@ -290,9 +295,7 @@ export function changelogApply({
290
295
 
291
296
  export function changelogUnreleasedStats(workspace_root: string, changelogPath = "CHANGELOG.md") {
292
297
  const cwd = resolveWorkspaceRoot(workspace_root);
293
- const abs = path.isAbsolute(changelogPath)
294
- ? changelogPath
295
- : path.join(cwd, changelogPath);
298
+ const abs = path.isAbsolute(changelogPath) ? changelogPath : path.join(cwd, changelogPath);
296
299
  if (!fs.existsSync(abs)) return { exists: false };
297
300
  const text = fs.readFileSync(abs, "utf8");
298
301
  const m = text.match(/##\s+\[Unreleased\]([\s\S]*?)(?=\n##\s+\[|$)/i);
@@ -1,13 +1,20 @@
1
1
  import { initStatus } from "./init";
2
2
 
3
- export const ALL_ITEM_IDS = ["youtrack_json", "youtrack_token", "vcs_json", "gitlab_token", "github_token"];
3
+ export const ALL_ITEM_IDS = [
4
+ "youtrack_json",
5
+ "youtrack_token",
6
+ "vcs_json",
7
+ "gitlab_token",
8
+ "github_token",
9
+ ];
4
10
  export const CONFIG_GAP_MARKER = "workflow config missing";
5
11
 
6
12
  export function describeConfigGaps(scope?: string[]): { missing: string[]; ok: boolean } {
7
13
  const all = scope ?? ALL_ITEM_IDS;
8
14
  try {
9
15
  const status: unknown = initStatus();
10
- if (!status || typeof status !== "object" || (status as Record<string, unknown>).error) return { missing: all, ok: false };
16
+ if (!status || typeof status !== "object" || (status as Record<string, unknown>).error)
17
+ return { missing: all, ok: false };
11
18
  const items = (status as Record<string, unknown>).items;
12
19
  if (!Array.isArray(items) || items.length === 0) return { missing: all, ok: false };
13
20
  const known = all;