@brainervirus/workit-core 0.5.6 → 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.
Files changed (43) hide show
  1. package/README.md +4 -17
  2. package/package.json +9 -9
  3. package/scripts/_shared/common.sh +18 -3
  4. package/scripts/install-opencode-plugin.sh +14 -9
  5. package/scripts/lib/config-dir.sh +26 -0
  6. package/scripts/sync-runtime.sh +4 -1
  7. package/src/core/branch.ts +143 -48
  8. package/src/core/changelog.ts +17 -14
  9. package/src/core/config-guard.ts +9 -2
  10. package/src/core/config.ts +95 -15
  11. package/src/core/detector.ts +22 -11
  12. package/src/core/docs-repo.ts +50 -15
  13. package/src/core/docs-validate.ts +163 -37
  14. package/src/core/flow-state.ts +6 -2
  15. package/src/core/gitignore.ts +11 -2
  16. package/src/core/handoff-context.ts +18 -5
  17. package/src/core/hygiene.ts +27 -5
  18. package/src/core/init.ts +86 -21
  19. package/src/core/parse-sections.ts +2 -2
  20. package/src/core/plan-tasks.ts +13 -3
  21. package/src/core/ports/youtrack-api.ts +3 -1
  22. package/src/core/ports/youtrack-config.ts +1 -3
  23. package/src/core/pr-create.ts +47 -15
  24. package/src/core/present.ts +11 -2
  25. package/src/core/reminder.ts +1 -2
  26. package/src/core/repo-tool.ts +4 -1
  27. package/src/core/rules.ts +10 -7
  28. package/src/core/scripts.ts +7 -2
  29. package/src/core/sdd.ts +11 -3
  30. package/src/core/templates.ts +14 -4
  31. package/src/core/vcs-config.ts +94 -36
  32. package/src/core/verify-parse.ts +4 -2
  33. package/src/core/workspaces.ts +2 -2
  34. package/src/core/youtrack.ts +233 -58
  35. package/src/core.ts +18 -3
  36. package/src/tools/docs-repo.ts +12 -3
  37. package/src/tools/flow.ts +24 -13
  38. package/src/tools/handoff.ts +28 -23
  39. package/src/tools/present.ts +14 -10
  40. package/src/tools/repo.ts +220 -87
  41. package/src/tools/sdd.ts +93 -66
  42. package/src/tools/youtrack.ts +119 -52
  43. package/templates/superpowers-doc-contract.md +1 -1
package/src/core/init.ts CHANGED
@@ -9,7 +9,11 @@ import { youTrackTokenCreateUrl, youTrackVerifyToken } from "./youtrack";
9
9
  const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
10
10
 
11
11
  const readJson = (p: string): Record<string, any> | null => {
12
- try { return JSON.parse(fs.readFileSync(p, "utf8")) as Record<string, any>; } catch { return null; }
12
+ try {
13
+ return JSON.parse(fs.readFileSync(p, "utf8")) as Record<string, any>;
14
+ } catch {
15
+ return null;
16
+ }
13
17
  };
14
18
 
15
19
  const isPlaceholder = (text: string): boolean =>
@@ -19,7 +23,11 @@ const modeOk = (p: string): boolean =>
19
23
  process.platform === "win32" || (fs.statSync(p).mode & 0o777) === 0o600;
20
24
 
21
25
  const resolvePath = (p: string): string => {
22
- try { return fs.realpathSync(p); } catch { return path.resolve(p); }
26
+ try {
27
+ return fs.realpathSync(p);
28
+ } catch {
29
+ return path.resolve(p);
30
+ }
23
31
  };
24
32
 
25
33
  /** Port of scripts/init/status.sh — filesystem init state. */
@@ -50,11 +58,14 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
50
58
  }
51
59
  }
52
60
  const expanded = tokenFile ? path.resolve(tokenFile) : null;
53
- const resolvedTokenFile = expanded && fs.existsSync(expanded)
54
- ? resolvePath(expanded)
55
- : expanded
56
- ? (path.isAbsolute(expanded) ? expanded : path.resolve(configDirPath, expanded))
57
- : null;
61
+ const resolvedTokenFile =
62
+ expanded && fs.existsSync(expanded)
63
+ ? resolvePath(expanded)
64
+ : expanded
65
+ ? path.isAbsolute(expanded)
66
+ ? expanded
67
+ : path.resolve(configDirPath, expanded)
68
+ : null;
58
69
  youtrackConfig = {
59
70
  config_edit_path: resolvePath(ytJson),
60
71
  baseUrl: base,
@@ -67,8 +78,18 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
67
78
  tokenFile: resolvedTokenFile,
68
79
  tokenDefaults: ytParsed.tokenDefaults,
69
80
  timeLogging: {
70
- meetings: { options: meetingIssues, skill: "/wk-meetings", logsTime: true, postsComment: false },
71
- taskWork: { issueSource: "active spec/plan **YouTrack:** field or --issue", skill: "/wk-issue-update", logsTime: true, postsComment: true },
81
+ meetings: {
82
+ options: meetingIssues,
83
+ skill: "/wk-meetings",
84
+ logsTime: true,
85
+ postsComment: false,
86
+ },
87
+ taskWork: {
88
+ issueSource: "active spec/plan **YouTrack:** field or --issue",
89
+ skill: "/wk-issue-update",
90
+ logsTime: true,
91
+ postsComment: true,
92
+ },
72
93
  },
73
94
  };
74
95
  youtrackTokenCreate = youTrackTokenCreateUrl().data;
@@ -91,20 +112,28 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
91
112
  const ytTokenPath = youtrackConfig?.tokenFile ?? path.join(configDirPath, "youtrack.token");
92
113
  const tokenText = fs.existsSync(ytTokenPath) ? fs.readFileSync(ytTokenPath, "utf8").trim() : "";
93
114
  const placeholder = fs.existsSync(ytTokenPath) && isPlaceholder(tokenText);
94
- const tokenOk = fs.existsSync(ytTokenPath) && modeOk(ytTokenPath) && Boolean(tokenText) && !isPlaceholder(tokenText);
115
+ const tokenOk =
116
+ fs.existsSync(ytTokenPath) &&
117
+ modeOk(ytTokenPath) &&
118
+ Boolean(tokenText) &&
119
+ !isPlaceholder(tokenText);
95
120
 
96
121
  const youtrackTokenItem: Record<string, any> = {
97
122
  id: "youtrack_token",
98
123
  label: "YouTrack API token (mode 600, not placeholder)",
99
124
  ok: tokenOk,
100
125
  path: fs.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path.resolve(ytTokenPath),
101
- token_edit_path: fs.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path.resolve(ytTokenPath),
126
+ token_edit_path: fs.existsSync(ytTokenPath)
127
+ ? resolvePath(ytTokenPath)
128
+ : path.resolve(ytTokenPath),
102
129
  placeholder,
103
130
  fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER} with your permanent token, save, then /wk-status`,
104
131
  };
105
132
  if (youtrackTokenCreate) {
106
- if (youtrackTokenCreate.createUrl) youtrackTokenItem.token_create_url = youtrackTokenCreate.createUrl;
107
- if (youtrackTokenCreate.docsUrl) youtrackTokenItem.token_create_docs_url = youtrackTokenCreate.docsUrl;
133
+ if (youtrackTokenCreate.createUrl)
134
+ youtrackTokenItem.token_create_url = youtrackTokenCreate.createUrl;
135
+ if (youtrackTokenCreate.docsUrl)
136
+ youtrackTokenItem.token_create_docs_url = youtrackTokenCreate.docsUrl;
108
137
  if (youtrackTokenCreate.scopes) youtrackTokenItem.token_scopes = youtrackTokenCreate.scopes;
109
138
  if (youtrackTokenCreate.tokenName) youtrackTokenItem.token_name = youtrackTokenCreate.tokenName;
110
139
  if (youtrackTokenCreate.steps) youtrackTokenItem.token_create_steps = youtrackTokenCreate.steps;
@@ -181,8 +210,22 @@ export function initStatusData(configDirPath = configDir()): Record<string, any>
181
210
  for (const k of ["gitlab", "github"]) {
182
211
  vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path.join(configDirPath, `${k}.token`));
183
212
  }
184
- items.push(tokenItem("gitlab_token", "GitLab token (mode 600, not placeholder)", vcsTokenFiles.gitlab, "gitlab"));
185
- items.push(tokenItem("github_token", "GitHub token (mode 600, not placeholder)", vcsTokenFiles.github, "github"));
213
+ items.push(
214
+ tokenItem(
215
+ "gitlab_token",
216
+ "GitLab token (mode 600, not placeholder)",
217
+ vcsTokenFiles.gitlab,
218
+ "gitlab",
219
+ ),
220
+ );
221
+ items.push(
222
+ tokenItem(
223
+ "github_token",
224
+ "GitHub token (mode 600, not placeholder)",
225
+ vcsTokenFiles.github,
226
+ "github",
227
+ ),
228
+ );
186
229
 
187
230
  return {
188
231
  config_dir: configDirPath,
@@ -218,7 +261,8 @@ export function toolkitStatusData(configDirPath = configDir()): Record<string, a
218
261
  status.youtrack_verify = verify;
219
262
  status.youtrack_ok = placeholder ? false : Boolean((verify as Record<string, any>).ok);
220
263
  status.vcs_verify = vcsVerify;
221
- status.vcs_ok = vcsJsonOk && !vcsPlaceholder ? Boolean((vcsVerify as Record<string, any>).ok) : false;
264
+ status.vcs_ok =
265
+ vcsJsonOk && !vcsPlaceholder ? Boolean((vcsVerify as Record<string, any>).ok) : false;
222
266
 
223
267
  const fsReady = status.items.every((i: Record<string, any>) => i.required === false || i.ok);
224
268
  status.ready = fsReady && Boolean(status.youtrack_ok) && (!vcsJsonOk || Boolean(status.vcs_ok));
@@ -302,7 +346,10 @@ const vcsJsonContent = (dir: string): Record<string, any> => ({
302
346
  });
303
347
 
304
348
  /** Port of scripts/init/apply.sh — confirmed scaffold actions. */
305
- export function initApplyData(action: string, env: NodeJS.ProcessEnv = process.env): Record<string, any> {
349
+ export function initApplyData(
350
+ action: string,
351
+ env: NodeJS.ProcessEnv = process.env,
352
+ ): Record<string, any> {
306
353
  const dir = String(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
307
354
  fs.mkdirSync(dir, { recursive: true });
308
355
 
@@ -360,7 +407,12 @@ export function initApplyData(action: string, env: NodeJS.ProcessEnv = process.e
360
407
  locale: cfg.locale,
361
408
  tokenCreate,
362
409
  timeLogging: {
363
- meetings: { issue: meeting, skill: "/wk-meetings", logsTime: true, postsComment: false },
410
+ meetings: {
411
+ issue: meeting,
412
+ skill: "/wk-meetings",
413
+ logsTime: true,
414
+ postsComment: false,
415
+ },
364
416
  taskWork: {
365
417
  issueSource: "active spec/plan **YouTrack:** field or --issue",
366
418
  skill: "/wk-issue-update",
@@ -421,21 +473,34 @@ export function initApplyData(action: string, env: NodeJS.ProcessEnv = process.e
421
473
  }
422
474
  }
423
475
  default: {
424
- return { error: `unknown action ${action} (youtrack_scaffold|youtrack_json|youtrack_token_placeholder|vcs_scaffold)` };
476
+ return {
477
+ error: `unknown action ${action} (youtrack_scaffold|youtrack_json|youtrack_token_placeholder|vcs_scaffold)`,
478
+ };
425
479
  }
426
480
  }
427
481
  }
428
482
 
429
483
  export function initStatus(): Record<string, any> {
430
484
  const data = initStatusData();
431
- return { ...data, workspaces: { resolved: resolveWorkspace(process.cwd()), path: workspacesPath() } };
485
+ return {
486
+ ...data,
487
+ workspaces: { resolved: resolveWorkspace(process.cwd()), path: workspacesPath() },
488
+ };
432
489
  }
433
490
 
434
491
  export function toolkitStatus(): Record<string, any> {
435
492
  return toolkitStatusData();
436
493
  }
437
494
 
438
- export function initApply({ action, confirmed, env }: { action: string; confirmed: boolean; env?: Record<string, string> }): Record<string, any> {
495
+ export function initApply({
496
+ action,
497
+ confirmed,
498
+ env,
499
+ }: {
500
+ action: string;
501
+ confirmed: boolean;
502
+ env?: Record<string, string>;
503
+ }): Record<string, any> {
439
504
  if (!confirmed) return { error: "confirmed: true required" };
440
505
  return { data: initApplyData(action, env ? { ...process.env, ...env } : process.env) };
441
506
  }
@@ -3,7 +3,7 @@ export function parseSections(stdout: string): Record<string, string> {
3
3
  const sections: Record<string, string> = {};
4
4
  const parts = stdout.split(/\n## /);
5
5
  for (const part of parts.slice(1)) {
6
- const nl = part.indexOf('\n');
6
+ const nl = part.indexOf("\n");
7
7
  const title = part.slice(0, nl).trim();
8
8
  sections[title] = part.slice(nl + 1).trim();
9
9
  }
@@ -12,7 +12,7 @@ export function parseSections(stdout: string): Record<string, string> {
12
12
 
13
13
  export function parseKeyValueLines(text: string, keys: string[]): Record<string, string> {
14
14
  const out: Record<string, string> = {};
15
- for (const line of text.split('\n')) {
15
+ for (const line of text.split("\n")) {
16
16
  for (const key of keys) {
17
17
  const prefix = `${key}: `;
18
18
  if (line.startsWith(prefix)) {
@@ -4,13 +4,19 @@ import { parseTasksFromPlan } from "./docs-validate";
4
4
  import { resolveBranch } from "./branch";
5
5
 
6
6
  const readSafe = (p: string): string | null => {
7
- try { return readFileSync(p, "utf8"); } catch { return null; }
7
+ try {
8
+ return readFileSync(p, "utf8");
9
+ } catch {
10
+ return null;
11
+ }
8
12
  };
9
13
 
10
14
  export function parsePlanTasks(
11
15
  planPath: string,
12
16
  workspaceRoot: string,
13
- ): { task_count: number; tasks: { id: number; title: string; section_text: string }[] } | { error: string } {
17
+ ):
18
+ | { task_count: number; tasks: { id: number; title: string; section_text: string }[] }
19
+ | { error: string } {
14
20
  const cwd = path.resolve(workspaceRoot);
15
21
  const resolved = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
16
22
  const text = readSafe(resolved);
@@ -27,7 +33,11 @@ export function resolveHandoffBranch(
27
33
  planPath: string,
28
34
  workspaceRoot: string,
29
35
  ): { branch: string } | { error: string } {
30
- const resolved = resolveBranch({ spec_path: specPath, plan_path: planPath, workspace_root: workspaceRoot });
36
+ const resolved = resolveBranch({
37
+ spec_path: specPath,
38
+ plan_path: planPath,
39
+ workspace_root: workspaceRoot,
40
+ });
31
41
  if ("error" in resolved) return { error: resolved.error as string };
32
42
  return { branch: resolved.branch };
33
43
  }
@@ -5,7 +5,9 @@ const args = process.argv.slice(2);
5
5
  const cmd = args[0];
6
6
  if (cmd === "log-time" || cmd === "post-comment") {
7
7
  if ((process.env.WORKFLOW_YT_WRITE ?? "") !== "1") {
8
- console.error("ERROR: YouTrack write operations require WORKFLOW_YT_WRITE=1 (refusing to mutate production)");
8
+ console.error(
9
+ "ERROR: YouTrack write operations require WORKFLOW_YT_WRITE=1 (refusing to mutate production)",
10
+ );
9
11
  process.exit(1);
10
12
  }
11
13
  }
@@ -1,7 +1,5 @@
1
1
  // CLI port of scripts/youtrack/config.sh — JSON out, argv/env in.
2
- import {
3
- youTrackConfigLoad,
4
- } from "../youtrack";
2
+ import { youTrackConfigLoad } from "../youtrack";
5
3
 
6
4
  const args = process.argv.slice(2);
7
5
  const cmd = args[0] ?? "load";
@@ -61,7 +61,8 @@ function buildBody(
61
61
  return body ? `${body}\n\n${line}` : line;
62
62
  }
63
63
 
64
- const truthy = (v: string | undefined): boolean => ["1", "true", "yes"].includes(String(v ?? "").toLowerCase());
64
+ const truthy = (v: string | undefined): boolean =>
65
+ ["1", "true", "yes"].includes(String(v ?? "").toLowerCase());
65
66
 
66
67
  // Port of python's shutil.which — scan PATH in-process (no `which` binary needed).
67
68
  function whichOnPath(tool: string): string | null {
@@ -71,7 +72,9 @@ function whichOnPath(tool: string): string | null {
71
72
  try {
72
73
  fs.accessSync(candidate, fs.constants.X_OK);
73
74
  return candidate;
74
- } catch { /* keep scanning */ }
75
+ } catch {
76
+ /* keep scanning */
77
+ }
75
78
  }
76
79
  return null;
77
80
  }
@@ -107,12 +110,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
107
110
  const root = process.env.WORKFLOW_WORKSPACE_ROOT ?? repoRoot(cwd);
108
111
  const cfg = vcsConfig("load", root);
109
112
  if (!cfg.ok) return { error: cfg.error ?? "vcs config missing" };
110
- if (!cfg.tokenReady) return { error: "VCS token not ready — run /wk-init and edit token file locally" };
113
+ if (!cfg.tokenReady)
114
+ return { error: "VCS token not ready — run /wk-init and edit token file locally" };
111
115
 
112
116
  const provider = cfg.provider as string;
113
- if (provider !== "gitlab" && provider !== "github") return { error: `unsupported provider: ${provider}` };
117
+ if (provider !== "gitlab" && provider !== "github")
118
+ return { error: `unsupported provider: ${provider}` };
114
119
  const cli = provider === "gitlab" ? "glab" : "gh";
115
- const installUrl = provider === "gitlab" ? "https://gitlab.com/gitlab-org/cli" : "https://cli.github.com";
120
+ const installUrl =
121
+ provider === "gitlab" ? "https://gitlab.com/gitlab-org/cli" : "https://cli.github.com";
116
122
  if (whichOnPath(cli) === null) {
117
123
  return {
118
124
  ok: false,
@@ -129,16 +135,23 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
129
135
  const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
130
136
  const target = env.WF_PR_TARGET || String(cfg.defaultTargetBranch ?? "develop");
131
137
 
132
- const br = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root, encoding: "utf8" });
138
+ const br = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
139
+ cwd: root,
140
+ encoding: "utf8",
141
+ });
133
142
  const branch = br.status === 0 ? (br.stdout ?? "").trim() : "";
134
143
 
135
144
  let baseUrl = cfg.youtrack_base_url as string | undefined;
136
145
  if (!baseUrl) {
137
- const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path.join(path.dirname(String(cfg.configPath)), "youtrack.json");
146
+ const ytCfg =
147
+ process.env.WORKFLOW_YOUTRACK_CONFIG ??
148
+ path.join(path.dirname(String(cfg.configPath)), "youtrack.json");
138
149
  try {
139
150
  const yt = JSON.parse(fs.readFileSync(ytCfg, "utf8")) as Record<string, any>;
140
151
  if (yt && typeof yt === "object") baseUrl = yt.baseUrl;
141
- } catch { /* optional */ }
152
+ } catch {
153
+ /* optional */
154
+ }
142
155
  }
143
156
  const ghLinkOnPr = cfg.issues_provider === "github" && cfg.link_on_pr === true;
144
157
  let ghRepo: string | null = null;
@@ -147,8 +160,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
147
160
  if (rr.status === 0) ghRepo = parseGhRepo(rr.stdout ?? "");
148
161
  }
149
162
  const finalBody = buildBody(
150
- body, branch, cfg.link_issues === true, baseUrl ?? "", env.WORKFLOW_YT_ISSUE ?? "",
151
- ghLinkOnPr, env.WORKFLOW_GH_ISSUE ?? "", env.WORKFLOW_GH_ISSUE_RELATION ?? "closes", ghRepo,
163
+ body,
164
+ branch,
165
+ cfg.link_issues === true,
166
+ baseUrl ?? "",
167
+ env.WORKFLOW_YT_ISSUE ?? "",
168
+ ghLinkOnPr,
169
+ env.WORKFLOW_GH_ISSUE ?? "",
170
+ env.WORKFLOW_GH_ISSUE_RELATION ?? "closes",
171
+ ghRepo,
152
172
  );
153
173
 
154
174
  const squash = pr.squashOnMerge !== false;
@@ -178,9 +198,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
178
198
  if (result.status !== 0) {
179
199
  const err = (result.stderr ?? result.stdout ?? "").trim();
180
200
  let hint: Record<string, any> | null = null;
181
- if (provider === "gitlab" && (err.includes("409") || err.toLowerCase().includes("already exists"))) {
182
- const list = spawnSync("glab", ["mr", "list", `--source-branch=${branch}`, "--output=json"],
183
- { cwd: root, encoding: "utf8", env: cmdEnv });
201
+ if (
202
+ provider === "gitlab" &&
203
+ (err.includes("409") || err.toLowerCase().includes("already exists"))
204
+ ) {
205
+ const list = spawnSync("glab", ["mr", "list", `--source-branch=${branch}`, "--output=json"], {
206
+ cwd: root,
207
+ encoding: "utf8",
208
+ env: cmdEnv,
209
+ });
184
210
  if (list.status === 0 && (list.stdout ?? "").trim()) {
185
211
  try {
186
212
  const mrs = JSON.parse(list.stdout ?? "") as Array<Record<string, any>>;
@@ -191,10 +217,16 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
191
217
  next_step: "Use glab mr update or close the open MR before creating again",
192
218
  };
193
219
  }
194
- } catch { /* no hint */ }
220
+ } catch {
221
+ /* no hint */
222
+ }
195
223
  }
196
224
  }
197
- const payload: Record<string, any> = { error: "create failed", provider, stderr: err.slice(0, 800) };
225
+ const payload: Record<string, any> = {
226
+ error: "create failed",
227
+ provider,
228
+ stderr: err.slice(0, 800),
229
+ };
198
230
  if (hint) payload.hint = hint;
199
231
  return payload;
200
232
  }
@@ -30,7 +30,12 @@ export function renderAsciiWireframe(spec: unknown): string {
30
30
  }
31
31
  if (kind === "button") {
32
32
  const label = "[ " + String(row?.label ?? "Button") + " ]";
33
- lines.push(boxLine(label.padStart(Math.max(0, (width - 4 + label.length) / 2)).padEnd(width - 4), width));
33
+ lines.push(
34
+ boxLine(
35
+ label.padStart(Math.max(0, (width - 4 + label.length) / 2)).padEnd(width - 4),
36
+ width,
37
+ ),
38
+ );
34
39
  continue;
35
40
  }
36
41
  if (kind === "field") {
@@ -42,7 +47,11 @@ export function renderAsciiWireframe(spec: unknown): string {
42
47
  if (kind === "columns") {
43
48
  const cols = Array.isArray(row?.columns) ? row.columns : [];
44
49
  const colW = Math.floor((width - 4 - cols.length + 1) / Math.max(cols.length, 1));
45
- const parts = cols.map((c: any) => String(c?.label ?? "").slice(0, Math.max(0, colW - 1)).padEnd(Math.max(0, colW)));
50
+ const parts = cols.map((c: any) =>
51
+ String(c?.label ?? "")
52
+ .slice(0, Math.max(0, colW - 1))
53
+ .padEnd(Math.max(0, colW)),
54
+ );
46
55
  lines.push(boxLine(parts.join(" | ").trim(), width));
47
56
  continue;
48
57
  }
@@ -57,8 +57,7 @@ Skill: receiving-code-review. Verify before implementing — evaluate review fee
57
57
  export const shouldInjectVerification = (currentText: string): boolean =>
58
58
  !currentText.includes(VERIFICATION_TEXT);
59
59
 
60
- export const shouldInjectTdd = (currentText: string): boolean =>
61
- !currentText.includes(TDD_TEXT);
60
+ export const shouldInjectTdd = (currentText: string): boolean => !currentText.includes(TDD_TEXT);
62
61
 
63
62
  export const shouldInjectBrainstorm = (currentText: string): boolean =>
64
63
  !currentText.includes(BRAINSTORM_TEXT);
@@ -1,7 +1,10 @@
1
1
  import { resolveWorkspaceRoot } from "./scripts";
2
2
 
3
3
  /** Attach the resolved repository root to each repository tool response. */
4
- export function withWorkspace(workspaceRoot: string, data: Record<string, any> = {}): Record<string, any> {
4
+ export function withWorkspace(
5
+ workspaceRoot: string,
6
+ data: Record<string, any> = {},
7
+ ): Record<string, any> {
5
8
  return {
6
9
  workspace_root: resolveWorkspaceRoot(workspaceRoot),
7
10
  ...data,
package/src/core/rules.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { configDir } from "./config";
5
4
 
6
5
  export type RulePlatform = "cursor" | "opencode";
@@ -11,13 +10,12 @@ export type CanonicalRule = {
11
10
  body: string;
12
11
  };
13
12
 
14
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
15
-
16
13
  export const rulesDir = () => path.join(configDir(), "rules");
17
14
 
18
15
  export const parseRule = (markdown: string): CanonicalRule | { error: string } => {
19
16
  const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
20
- if (!fm) return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
17
+ if (!fm)
18
+ return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
21
19
  const meta: Record<string, string> = {};
22
20
  const unquote = (v: string) => v.replace(/^["']|["']$/g, "").trim();
23
21
  for (const line of fm[1].split("\n")) {
@@ -27,9 +25,13 @@ export const parseRule = (markdown: string): CanonicalRule | { error: string } =
27
25
  const name = meta.name ?? "";
28
26
  const description = meta.description ?? "";
29
27
  const rawPlatforms = (meta.platforms ?? "")
30
- .replace(/^\[|\]$/g, "").split(",").map((p) => p.trim().replace(/['"]/g, ""))
28
+ .replace(/^\[|\]$/g, "")
29
+ .split(",")
30
+ .map((p) => p.trim().replace(/['"]/g, ""))
31
31
  .filter(Boolean);
32
- const platforms = rawPlatforms.filter((p): p is RulePlatform => p === "cursor" || p === "opencode");
32
+ const platforms = rawPlatforms.filter(
33
+ (p): p is RulePlatform => p === "cursor" || p === "opencode",
34
+ );
33
35
  if (!name || !description || platforms.length === 0) {
34
36
  return { error: "rule frontmatter requires name, description, and platforms" };
35
37
  }
@@ -73,7 +75,8 @@ export const writeRule = (
73
75
  confirmed: boolean,
74
76
  ): { ok: true; path: string } | { ok: false; error: string } => {
75
77
  if (!confirmed) return { ok: false, error: "confirmed: true required" };
76
- if (!RULE_NAME_RE.test(rule.name)) return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
78
+ if (!RULE_NAME_RE.test(rule.name))
79
+ return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
77
80
  const dir = path.join(rulesDir(), rule.name);
78
81
  mkdirSync(dir, { recursive: true });
79
82
  const file = path.join(dir, "rule.md");
@@ -18,7 +18,7 @@ export function runScript(
18
18
  const result = spawnSync("bash", [scriptPath, ...args], {
19
19
  cwd,
20
20
  encoding: "utf8",
21
- env: { ...process.env, ...(extraEnv ?? {}) },
21
+ env: { ...process.env, ...extraEnv },
22
22
  });
23
23
  return {
24
24
  stdout: result.stdout ?? "",
@@ -29,7 +29,12 @@ export function runScript(
29
29
  };
30
30
  }
31
31
 
32
- export function runScriptJson(scriptName: string, args: string[], workspaceRoot: string, extraEnv?: Record<string, string>) {
32
+ export function runScriptJson(
33
+ scriptName: string,
34
+ args: string[],
35
+ workspaceRoot: string,
36
+ extraEnv?: Record<string, string>,
37
+ ) {
33
38
  const { stdout, stderr, exitCode } = runScript(scriptName, args, workspaceRoot, extraEnv);
34
39
  if (exitCode !== 0) {
35
40
  return { error: (stderr || stdout || "script failed").trim(), exitCode };
package/src/core/sdd.ts CHANGED
@@ -57,7 +57,10 @@ export function sddContext({
57
57
  let completed_task_ids: number[] = [];
58
58
  const absProgress = path.join(cwd, progress_path);
59
59
  if (existsSync(absProgress)) {
60
- progress_lines = readFileSync(absProgress, "utf8").split("\n").map((ln) => ln.trim()).filter(Boolean);
60
+ progress_lines = readFileSync(absProgress, "utf8")
61
+ .split("\n")
62
+ .map((ln) => ln.trim())
63
+ .filter(Boolean);
61
64
  const pat = /^Task\s+(\d+):\s+complete\b/i;
62
65
  completed_task_ids = progress_lines
63
66
  .map((ln) => pat.exec(ln)?.[1])
@@ -68,7 +71,11 @@ export function sddContext({
68
71
  let manifest: Record<string, unknown> = {};
69
72
  const absManifest = path.join(cwd, manifest_path);
70
73
  if (existsSync(absManifest)) {
71
- try { manifest = JSON.parse(readFileSync(absManifest, "utf8")); } catch { manifest = {}; }
74
+ try {
75
+ manifest = JSON.parse(readFileSync(absManifest, "utf8"));
76
+ } catch {
77
+ manifest = {};
78
+ }
72
79
  }
73
80
 
74
81
  const legacy_path = path.join(cwd, ".superpowers/sdd");
@@ -83,7 +90,8 @@ export function sddContext({
83
90
  const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
84
91
  if (spec_path) {
85
92
  const validated = docsValidate({ spec_path, plan_path, workspace_root: cwd });
86
- if (validated.ok === false) return { ok: false, errors: validated.errors, error: validated.error };
93
+ if (validated.ok === false)
94
+ return { ok: false, errors: validated.errors, error: validated.error };
87
95
  }
88
96
  const tasks = parseTasksFromPlan(planText);
89
97
  if (tasks.length > 0) {
@@ -7,12 +7,18 @@ export type TemplateName = "issue-update" | "greeting" | "headers";
7
7
 
8
8
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
9
 
10
- export const templatePath = (name: TemplateName): string => path.join(configDir(), "templates", `${name}.md`);
10
+ export const templatePath = (name: TemplateName): string =>
11
+ path.join(configDir(), "templates", `${name}.md`);
11
12
 
12
- export const readTemplate = (name: TemplateName): { source: "config" | "repo"; content: string } => {
13
+ export const readTemplate = (
14
+ name: TemplateName,
15
+ ): { source: "config" | "repo"; content: string } => {
13
16
  const cfg = templatePath(name);
14
17
  if (existsSync(cfg)) return { source: "config", content: readFileSync(cfg, "utf8") };
15
- return { source: "repo", content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8") };
18
+ return {
19
+ source: "repo",
20
+ content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8"),
21
+ };
16
22
  };
17
23
 
18
24
  export const writeTemplate = (
@@ -27,7 +33,11 @@ export const writeTemplate = (
27
33
  return { ok: true, path: file };
28
34
  };
29
35
 
30
- export const listTemplates = (): { name: TemplateName; source: "config" | "repo" | "missing"; path: string }[] =>
36
+ export const listTemplates = (): {
37
+ name: TemplateName;
38
+ source: "config" | "repo" | "missing";
39
+ path: string;
40
+ }[] =>
31
41
  (["issue-update", "greeting", "headers"] as TemplateName[]).map((name) => {
32
42
  const cfg = templatePath(name);
33
43
  const repoFile = path.join(repoRoot, "templates", `${name}.md`);