@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
@@ -1,18 +1,17 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { spawnSync } from "node:child_process";
5
4
  import { readTemplate } from "./templates";
6
5
  import { resolveWorkspaceRoot } from "./scripts";
6
+ import { configDir } from "./config";
7
7
 
8
8
  const ISSUE_RE = /^[A-Z]+-\d+$/;
9
9
  const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
10
10
 
11
11
  // Port of scripts/youtrack/config.sh chain: WORKFLOW_YOUTRACK_CONFIG ->
12
- // XDG_CONFIG_HOME / HOME .config + workflow-toolkit/youtrack.json.
12
+ // configDir() (XDG_CONFIG_HOME / HOME .config + workit).
13
13
  export const youTrackConfigPath = (): string =>
14
- process.env.WORKFLOW_YOUTRACK_CONFIG ??
15
- path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workflow-toolkit", "youtrack.json");
14
+ process.env.WORKFLOW_YOUTRACK_CONFIG ?? path.join(configDir(), "youtrack.json");
16
15
 
17
16
  const youTrackTokenModeOk = (p: string): boolean => {
18
17
  if (process.platform === "win32") return true;
@@ -20,7 +19,9 @@ const youTrackTokenModeOk = (p: string): boolean => {
20
19
  return mode === 0o600;
21
20
  };
22
21
 
23
- function readYouTrackConfig(required: boolean): { config: Record<string, any>; path: string } | { error: string } {
22
+ function readYouTrackConfig(
23
+ required: boolean,
24
+ ): { config: Record<string, any>; path: string } | { error: string } {
24
25
  const cfgPath = youTrackConfigPath();
25
26
  if (!fs.existsSync(cfgPath)) {
26
27
  return required ? { error: "ERROR: missing youtrack.json" } : { config: {}, path: cfgPath };
@@ -40,7 +41,9 @@ export function youTrackConfigLoad(): { data: Record<string, any> } | { error: s
40
41
  const cfgPath = loaded.path;
41
42
  const tokenFile = String(loaded.config.tokenFile ?? "");
42
43
  const tokenPath = tokenFile
43
- ? (path.isAbsolute(tokenFile) ? path.resolve(tokenFile) : path.resolve(process.cwd(), tokenFile))
44
+ ? path.isAbsolute(tokenFile)
45
+ ? path.resolve(tokenFile)
46
+ : path.resolve(process.cwd(), tokenFile)
44
47
  : "";
45
48
  if (!tokenPath || !fs.existsSync(tokenPath)) {
46
49
  return { error: "missing youtrack.token" };
@@ -60,17 +63,29 @@ export function youTrackConfigLoad(): { data: Record<string, any> } | { error: s
60
63
  return { data: redacted };
61
64
  }
62
65
 
63
- function tzParts(date: Date, tz: string): { y: string; m: string; d: string; hour: string; minute: string } {
66
+ function tzParts(
67
+ date: Date,
68
+ tz: string,
69
+ ): { y: string; m: string; d: string; hour: string; minute: string } {
64
70
  const parts = new Intl.DateTimeFormat("en-GB", {
65
- timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
66
- hour: "2-digit", minute: "2-digit", hourCycle: "h23",
71
+ timeZone: tz,
72
+ year: "numeric",
73
+ month: "2-digit",
74
+ day: "2-digit",
75
+ hour: "2-digit",
76
+ minute: "2-digit",
77
+ hourCycle: "h23",
67
78
  }).formatToParts(date);
68
79
  const map = Object.fromEntries(parts.map((p) => [p.type, p.value]));
69
80
  return { y: map.year, m: map.month, d: map.day, hour: map.hour, minute: map.minute };
70
81
  }
71
82
 
72
83
  /** Port of scripts/youtrack/greeting.sh. */
73
- export function youTrackGreeting(configOverride?: string): { stdout: string; exitCode: number; stderr: string } {
84
+ export function youTrackGreeting(configOverride?: string): {
85
+ stdout: string;
86
+ exitCode: number;
87
+ stderr: string;
88
+ } {
74
89
  const cfgPath = configOverride ?? youTrackConfigPath();
75
90
  try {
76
91
  const config = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record<string, any>;
@@ -81,20 +96,29 @@ export function youTrackGreeting(configOverride?: string): { stdout: string; exi
81
96
  const cutoffHour = Number(cutoff[0]);
82
97
  const cutoffMinute = Number(cutoff[1] ?? 0);
83
98
  const greetings = (config.greetings ?? {}) as Record<string, string>;
84
- const isMorning = Number(hour) < cutoffHour || (Number(hour) === cutoffHour && Number(minute) < cutoffMinute);
99
+ const isMorning =
100
+ Number(hour) < cutoffHour || (Number(hour) === cutoffHour && Number(minute) < cutoffMinute);
85
101
  const greeting = isMorning
86
- ? greetings.morning ?? "buenos días"
87
- : greetings.afternoon ?? "buenas tardes";
102
+ ? (greetings.morning ?? "buenos días")
103
+ : (greetings.afternoon ?? "buenas tardes");
88
104
  const mention = String(config.defaultMention ?? "Alejandra.Flores");
89
- void y; void m; void d;
105
+ void y;
106
+ void m;
107
+ void d;
90
108
  return { stdout: `@${mention} Hola, ${greeting}.\n`, exitCode: 0, stderr: "" };
91
109
  } catch (err) {
92
- return { stdout: "", exitCode: 1, stderr: err instanceof Error ? err.message : "greeting failed" };
110
+ return {
111
+ stdout: "",
112
+ exitCode: 1,
113
+ stderr: err instanceof Error ? err.message : "greeting failed",
114
+ };
93
115
  }
94
116
  }
95
117
 
96
118
  /** Port of scripts/youtrack/parse-duration.sh. */
97
- export function youTrackParseDuration(text: string): { data: { minutes: number; text: string } } | { error: string } {
119
+ export function youTrackParseDuration(
120
+ text: string,
121
+ ): { data: { minutes: number; text: string } } | { error: string } {
98
122
  const lower = String(text).toLowerCase().trim();
99
123
  let total = 0;
100
124
  for (const match of lower.matchAll(/(\d+)\s*h/g)) total += Number(match[1]) * 60;
@@ -105,13 +129,17 @@ export function youTrackParseDuration(text: string): { data: { minutes: number;
105
129
  }
106
130
 
107
131
  /** Port of scripts/youtrack/work-date-ms.sh — resolve work-item date as epoch ms. */
108
- export function youTrackWorkDateMs(dateRaw: string): { data: { dateMs: number; timezone: string; localDate: string } } | { error: string } {
132
+ export function youTrackWorkDateMs(
133
+ dateRaw: string,
134
+ ): { data: { dateMs: number; timezone: string; localDate: string } } | { error: string } {
109
135
  const cfgPath = youTrackConfigPath();
110
136
  let tz = "America/Santiago";
111
137
  try {
112
138
  const config = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record<string, any>;
113
139
  tz = String(config.timezone ?? "America/Santiago");
114
- } catch { /* defaults */ }
140
+ } catch {
141
+ /* defaults */
142
+ }
115
143
  const raw = dateRaw || "auto";
116
144
  try {
117
145
  if (raw === "auto" || !raw) {
@@ -128,7 +156,13 @@ export function youTrackWorkDateMs(dateRaw: string): { data: { dateMs: number; t
128
156
  const [y, m, d] = raw.split("-").map(Number);
129
157
  const iso = `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}T00:00:00`;
130
158
  const dateMs = Math.floor(Date.parse(iso) / 86400000) * 86400000;
131
- return { data: { dateMs, timezone: tz, localDate: `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}` } };
159
+ return {
160
+ data: {
161
+ dateMs,
162
+ timezone: tz,
163
+ localDate: `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`,
164
+ },
165
+ };
132
166
  } catch (err) {
133
167
  return { error: err instanceof Error ? err.message : "could not resolve date" };
134
168
  }
@@ -139,14 +173,19 @@ const youTrackToken = (): { token: string; base: string } | { error: string } =>
139
173
  if ("error" in loaded) return loaded;
140
174
  const tokenFile = String(loaded.config.tokenFile ?? "");
141
175
  const tokenPath = tokenFile
142
- ? (path.isAbsolute(tokenFile) ? path.resolve(tokenFile) : path.resolve(process.cwd(), tokenFile))
176
+ ? path.isAbsolute(tokenFile)
177
+ ? path.resolve(tokenFile)
178
+ : path.resolve(process.cwd(), tokenFile)
143
179
  : "";
144
180
  if (!tokenPath || !fs.existsSync(tokenPath)) return { error: "missing youtrack.token" };
145
181
  if (!youTrackTokenModeOk(tokenPath)) return { error: "youtrack.token mode must be 0600" };
146
182
  const token = fs.readFileSync(tokenPath, "utf8").trim();
147
183
  if (!token) return { error: "empty token file" };
148
184
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
149
- return { error: "token file still has placeholder YOUR_TOKEN_HERE — edit the file locally, then run /wk-status" };
185
+ return {
186
+ error:
187
+ "token file still has placeholder YOUR_TOKEN_HERE — edit the file locally, then run /wk-status",
188
+ };
150
189
  }
151
190
  const base = String(loaded.config.baseUrl ?? "").replace(/\/+$/, "");
152
191
  if (!base) return { error: "baseUrl missing in config" };
@@ -159,11 +198,17 @@ function youTrackCurl(args: string[]): { status: number; stdout: string; stderr:
159
198
  }
160
199
 
161
200
  /** Port of scripts/youtrack/api.sh — log-time / post-comment with the WORKFLOW_YT_WRITE guard. */
162
- export function youTrackApi(args: string[], writeFlag = process.env.WORKFLOW_YT_WRITE ?? ""): { data: Record<string, any> } | { error: string } {
201
+ export function youTrackApi(
202
+ args: string[],
203
+ writeFlag = process.env.WORKFLOW_YT_WRITE ?? "",
204
+ ): { data: Record<string, any> } | { error: string } {
163
205
  const cmd = args[0];
164
206
  if (cmd === "log-time" || cmd === "post-comment") {
165
207
  if (writeFlag !== "1") {
166
- return { error: "YouTrack write operations require WORKFLOW_YT_WRITE=1 (refusing to mutate production)" };
208
+ return {
209
+ error:
210
+ "YouTrack write operations require WORKFLOW_YT_WRITE=1 (refusing to mutate production)",
211
+ };
167
212
  }
168
213
  }
169
214
  const creds = youTrackToken();
@@ -178,13 +223,25 @@ export function youTrackApi(args: string[], writeFlag = process.env.WORKFLOW_YT_
178
223
  if ("error" in dateMs) return dateMs;
179
224
  const body = JSON.stringify({ duration: { minutes }, text, date: dateMs.data.dateMs });
180
225
  const out = youTrackCurl([
181
- ...auth, "-H", "Content-Type: application/json", "-d", body,
226
+ ...auth,
227
+ "-H",
228
+ "Content-Type: application/json",
229
+ "-d",
230
+ body,
182
231
  `${base}/api/issues/${issue}/timeTracking/workItems?fields=id,idReadable`,
183
232
  ]);
184
233
  if (out.status !== 0) return { error: "YouTrack HTTP request failed" };
185
234
  try {
186
235
  const created = JSON.parse(out.stdout) as Record<string, any>;
187
- return { data: { ok: true, issueId: issue, workItemId: created.id, dateMs: dateMs.data.dateMs, minutes } };
236
+ return {
237
+ data: {
238
+ ok: true,
239
+ issueId: issue,
240
+ workItemId: created.id,
241
+ dateMs: dateMs.data.dateMs,
242
+ minutes,
243
+ },
244
+ };
188
245
  } catch {
189
246
  return { error: "invalid JSON from YouTrack API" };
190
247
  }
@@ -193,7 +250,11 @@ export function youTrackApi(args: string[], writeFlag = process.env.WORKFLOW_YT_
193
250
  const [issue, text] = args.slice(1);
194
251
  const body = JSON.stringify({ text });
195
252
  const out = youTrackCurl([
196
- ...auth, "-H", "Content-Type: application/json", "-d", body,
253
+ ...auth,
254
+ "-H",
255
+ "Content-Type: application/json",
256
+ "-d",
257
+ body,
197
258
  `${base}/api/issues/${issue}/comments`,
198
259
  ]);
199
260
  if (out.status !== 0) return { error: "YouTrack HTTP request failed" };
@@ -203,18 +264,26 @@ export function youTrackApi(args: string[], writeFlag = process.env.WORKFLOW_YT_
203
264
  }
204
265
 
205
266
  /** Port of scripts/youtrack/verify-token.sh — read-only GET /api/users/me. */
206
- export function youTrackVerifyToken(): { data: Record<string, any> } | { error: string; http_status?: number; path?: string } {
267
+ export function youTrackVerifyToken():
268
+ | { data: Record<string, any> }
269
+ | { error: string; http_status?: number; path?: string } {
207
270
  const cfgPath = youTrackConfigPath();
208
271
  if (!fs.existsSync(cfgPath)) return { error: "missing youtrack.json" };
209
272
  const creds = youTrackToken();
210
273
  if ("error" in creds) return { error: creds.error };
211
274
  const { token, base } = creds;
212
275
 
213
- const me = youTrackCurl(["-H", `Authorization: Bearer ${token}`, "-H", "Accept: application/json",
214
- `${base}/api/users/me?fields=id,login,name,email`]);
276
+ const me = youTrackCurl([
277
+ "-H",
278
+ `Authorization: Bearer ${token}`,
279
+ "-H",
280
+ "Accept: application/json",
281
+ `${base}/api/users/me?fields=id,login,name,email`,
282
+ ]);
215
283
  if (me.status !== 0) {
216
284
  const body = me.stderr.trim() || me.stdout.trim();
217
- const err = me.status === 22 ? "authentication failed (401/403)" : `HTTP error: ${body.slice(0, 200)}`;
285
+ const err =
286
+ me.status === 22 ? "authentication failed (401/403)" : `HTTP error: ${body.slice(0, 200)}`;
218
287
  return { error: err, http_status: me.status };
219
288
  }
220
289
  let user: Record<string, any>;
@@ -224,21 +293,33 @@ export function youTrackVerifyToken(): { data: Record<string, any> } | { error:
224
293
  return { error: "invalid JSON from YouTrack /api/users/me" };
225
294
  }
226
295
  const result: Record<string, any> = {
227
- ok: true, method: "GET /api/users/me", baseUrl: base,
228
- login: user.login, name: user.name, email: user.email, id: user.id,
296
+ ok: true,
297
+ method: "GET /api/users/me",
298
+ baseUrl: base,
299
+ login: user.login,
300
+ name: user.name,
301
+ email: user.email,
302
+ id: user.id,
229
303
  };
230
304
  const meeting = readYouTrackConfig(false);
231
305
  const meetingIssue = "config" in meeting ? meeting.config.meetingIssue : undefined;
232
306
  if (meetingIssue) {
233
- const issue = youTrackCurl(["-H", `Authorization: Bearer ${token}`, "-H", "Accept: application/json",
234
- `${base}/api/issues/${meetingIssue}?fields=id,idReadable,summary`]);
307
+ const issue = youTrackCurl([
308
+ "-H",
309
+ `Authorization: Bearer ${token}`,
310
+ "-H",
311
+ "Accept: application/json",
312
+ `${base}/api/issues/${meetingIssue}?fields=id,idReadable,summary`,
313
+ ]);
235
314
  if (issue.status === 0) {
236
315
  try {
237
316
  const parsed = JSON.parse(issue.stdout) as Record<string, any>;
238
317
  result.meetingIssue = meetingIssue;
239
318
  result.meetingIssueReadable = true;
240
319
  result.meetingIssueSummary = parsed.summary;
241
- } catch { /* unreadable */ }
320
+ } catch {
321
+ /* unreadable */
322
+ }
242
323
  } else {
243
324
  result.meetingIssue = meetingIssue;
244
325
  result.meetingIssueReadable = false;
@@ -256,7 +337,9 @@ export function youTrackTokenCreateUrl(): { data: Record<string, any> } {
256
337
  const cfgPath = loaded && "config" in loaded ? loaded.path : youTrackConfigPath();
257
338
  const defaults = (config.tokenDefaults ?? {}) as Record<string, any>;
258
339
  const name = String(defaults.name ?? tokenName);
259
- const desc = String(defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings");
340
+ const desc = String(
341
+ defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings",
342
+ );
260
343
  const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
261
344
  const base = String(config.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
262
345
  const tokenFile = String(config.tokenFile ?? path.join(path.dirname(cfgPath), "youtrack.token"));
@@ -284,7 +367,9 @@ export function youTrackTokenCreateUrl(): { data: Record<string, any> } {
284
367
  }
285
368
 
286
369
  /** Parse bare id (NSR-40) or YouTrack URL into issue id. */
287
- export function parseIssueRef(input: unknown): { issueId: string; source: string } | { error: string } {
370
+ export function parseIssueRef(
371
+ input: unknown,
372
+ ): { issueId: string; source: string } | { error: string } {
288
373
  const trimmed = String(input ?? "").trim();
289
374
  if (!trimmed) return { error: "empty issue reference" };
290
375
 
@@ -319,11 +404,17 @@ const defaultScripts: YouTrackScripts = {
319
404
  api: (args) => youTrackApi(args, process.env.WORKFLOW_YT_WRITE ?? ""),
320
405
  };
321
406
 
322
- export function verifyYouTrackToken(scripts: YouTrackScripts = defaultScripts): Record<string, any> {
407
+ export function verifyYouTrackToken(
408
+ scripts: YouTrackScripts = defaultScripts,
409
+ ): Record<string, any> {
323
410
  return scripts.config();
324
411
  }
325
412
 
326
- function resolveYouTrackFromPaths(spec_path: string | undefined, plan_path: string | undefined, workspace_root: string): string | null {
413
+ function resolveYouTrackFromPaths(
414
+ spec_path: string | undefined,
415
+ plan_path: string | undefined,
416
+ workspace_root: string,
417
+ ): string | null {
327
418
  const root = resolveWorkspaceRoot(workspace_root);
328
419
  for (const rel of [spec_path, plan_path].filter(Boolean) as string[]) {
329
420
  const full = path.isAbsolute(rel) ? rel : path.join(root, rel);
@@ -358,7 +449,26 @@ function meetingOptionsFromConfig(cfg: any): Record<string, any>[] {
358
449
  ];
359
450
  }
360
451
 
361
- export function context({ spec_path, plan_path, issue_id, issue_url, issue_ref, mode, workspace_root }: { spec_path?: string; plan_path?: string; issue_id?: string; issue_url?: string; issue_ref?: string; mode?: string; workspace_root: string }, scripts: YouTrackScripts = defaultScripts): Record<string, any> {
452
+ export function context(
453
+ {
454
+ spec_path,
455
+ plan_path,
456
+ issue_id,
457
+ issue_url,
458
+ issue_ref,
459
+ mode,
460
+ workspace_root,
461
+ }: {
462
+ spec_path?: string;
463
+ plan_path?: string;
464
+ issue_id?: string;
465
+ issue_url?: string;
466
+ issue_ref?: string;
467
+ mode?: string;
468
+ workspace_root: string;
469
+ },
470
+ scripts: YouTrackScripts = defaultScripts,
471
+ ): Record<string, any> {
362
472
  const cfg = scripts.config();
363
473
  if (cfg.error) return { error: cfg.error };
364
474
 
@@ -390,7 +500,8 @@ export function context({ spec_path, plan_path, issue_id, issue_url, issue_ref,
390
500
  if (!issue) issue = resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) ?? undefined;
391
501
  if (!issue || !ISSUE_RE.test(issue)) {
392
502
  return {
393
- error: "invalid or missing issue id — pass issue_url, issue_id, or spec/plan with **YouTrack:**",
503
+ error:
504
+ "invalid or missing issue id — pass issue_url, issue_id, or spec/plan with **YouTrack:**",
394
505
  requiresIssueInput: true,
395
506
  };
396
507
  }
@@ -411,39 +522,86 @@ export function context({ spec_path, plan_path, issue_id, issue_url, issue_ref,
411
522
  };
412
523
  }
413
524
 
414
- export function parseDuration(text: string, _workspace_root: string, scripts: YouTrackScripts = defaultScripts): Record<string, any> {
525
+ export function parseDuration(
526
+ text: string,
527
+ _workspace_root: string,
528
+ scripts: YouTrackScripts = defaultScripts,
529
+ ): Record<string, any> {
415
530
  const out = scripts.parseDuration(text);
416
531
  if (out.error) return { error: out.error };
417
532
  return out.data;
418
533
  }
419
534
 
420
- export function logTime({ issueId, minutes, text, date, dateMs, workspace_root }: { issueId: string; minutes: number; text?: string; date?: string; dateMs?: number; workspace_root: string }, scripts: YouTrackScripts = defaultScripts): Record<string, any> {
535
+ export function logTime(
536
+ {
537
+ issueId,
538
+ minutes,
539
+ text,
540
+ date,
541
+ dateMs,
542
+ workspace_root: _workspace_root,
543
+ }: {
544
+ issueId: string;
545
+ minutes: number;
546
+ text?: string;
547
+ date?: string;
548
+ dateMs?: number;
549
+ workspace_root: string;
550
+ },
551
+ scripts: YouTrackScripts = defaultScripts,
552
+ ): Record<string, any> {
421
553
  if (!issueId || !ISSUE_RE.test(issueId)) return { error: "invalid issueId" };
422
554
  if (!minutes || minutes <= 0) return { error: "minutes must be positive" };
423
555
  const workText = text ?? "workit";
424
556
  const dateArg =
425
- dateMs != null
426
- ? String(dateMs)
427
- : date && /^\d+$/.test(String(date))
428
- ? String(date)
429
- : "auto";
557
+ dateMs != null ? String(dateMs) : date && /^\d+$/.test(String(date)) ? String(date) : "auto";
430
558
  const out = scripts.api(["log-time", issueId, String(minutes), workText, dateArg]);
431
559
  if (out.error) return { error: out.error };
432
560
  return { issueId, minutes, text: workText, ...out.data, ok: true };
433
561
  }
434
562
 
435
-
436
- export function buildDraft({ issueId, projectName, userNotes, greeting, facts, includeProjectOpener, includeFacts }: { issueId: string; projectName?: string; userNotes?: string; greeting?: string; facts?: any; includeProjectOpener?: boolean; includeFacts?: boolean }): Record<string, any> {
563
+ export function buildDraft({
564
+ issueId,
565
+ projectName,
566
+ userNotes,
567
+ greeting,
568
+ facts,
569
+ includeProjectOpener,
570
+ includeFacts,
571
+ }: {
572
+ issueId: string;
573
+ projectName?: string;
574
+ userNotes?: string;
575
+ greeting?: string;
576
+ facts?: any;
577
+ includeProjectOpener?: boolean;
578
+ includeFacts?: boolean;
579
+ }): Record<string, any> {
437
580
  const tpl = readTemplate("issue-update").content;
438
581
  const para = (value: string): string => (value ? `\n\n${value}` : "");
439
582
  const filled = tpl
440
583
  .replaceAll("{{greetingSection}}", para(greeting ? `${greeting}` : ""))
441
- .replaceAll("{{projectSection}}", para(includeProjectOpener && projectName ? `Hoy estuve full con ${projectName}.` : ""))
584
+ .replaceAll(
585
+ "{{projectSection}}",
586
+ para(includeProjectOpener && projectName ? `Hoy estuve full con ${projectName}.` : ""),
587
+ )
442
588
  .replaceAll("{{userNotesSection}}", para((userNotes ?? "").trim()))
443
- .replaceAll("{{progressSection}}", para(includeFacts && facts?.progress_excerpt?.length
444
- ? facts.progress_excerpt.map((l: string) => `- ${l}`).join("\n") : ""))
445
- .replaceAll("{{gitCommitsSection}}", para(includeFacts && facts?.git_commits?.length
446
- ? facts.git_commits.map((c: string) => `- ${c}`).join("\n") : ""));
589
+ .replaceAll(
590
+ "{{progressSection}}",
591
+ para(
592
+ includeFacts && facts?.progress_excerpt?.length
593
+ ? facts.progress_excerpt.map((l: string) => `- ${l}`).join("\n")
594
+ : "",
595
+ ),
596
+ )
597
+ .replaceAll(
598
+ "{{gitCommitsSection}}",
599
+ para(
600
+ includeFacts && facts?.git_commits?.length
601
+ ? facts.git_commits.map((c: string) => `- ${c}`).join("\n")
602
+ : "",
603
+ ),
604
+ );
447
605
  const collapsed = filled.replace(/\n{3,}/g, "\n\n").trimEnd();
448
606
  // Bare draft keeps the header's trailing blank line (matches legacy output);
449
607
  // drafts with sections end right after the last one.
@@ -451,14 +609,31 @@ export function buildDraft({ issueId, projectName, userNotes, greeting, facts, i
451
609
  return { issueId, markdown };
452
610
  }
453
611
 
454
- export function postUpdate({ confirmed, issueId, markdown, minutes, workspace_root }: { confirmed: boolean; issueId: string; markdown: string; minutes?: number; workspace_root?: string }, operations?: Record<string, any>): Record<string, any> {
612
+ export function postUpdate(
613
+ {
614
+ confirmed,
615
+ issueId,
616
+ markdown,
617
+ minutes,
618
+ workspace_root,
619
+ }: {
620
+ confirmed: boolean;
621
+ issueId: string;
622
+ markdown: string;
623
+ minutes?: number;
624
+ workspace_root?: string;
625
+ },
626
+ operations?: Record<string, any>,
627
+ ): Record<string, any> {
455
628
  operations ??= {};
456
629
  if (!confirmed) return { error: "confirmed: true required" };
457
630
  if (!issueId || !ISSUE_RE.test(issueId)) return { error: "invalid issueId" };
458
631
  if (!markdown?.trim()) return { error: "markdown required" };
459
632
 
460
- const postComment = operations.postComment ?? ((id: string, text: string, root: string) =>
461
- youTrackApi(["post-comment", id, text], process.env.WORKFLOW_YT_WRITE ?? ""));
633
+ const postComment =
634
+ operations.postComment ??
635
+ ((id: string, text: string, _root: string) =>
636
+ youTrackApi(["post-comment", id, text], process.env.WORKFLOW_YT_WRITE ?? ""));
462
637
  const logTimeOperation = operations.logTime ?? logTime;
463
638
  const comment = postComment(issueId, markdown, workspace_root);
464
639
  if (comment.error) return { error: comment.error };
package/src/core.ts CHANGED
@@ -7,7 +7,11 @@ export type Result<T> =
7
7
  | { ok: false; data: T | null; error: string };
8
8
 
9
9
  export const ok = <T>(data: T): Result<T> => ({ ok: true, data, error: null });
10
- export const fail = <T = never>(error: string, data: T | null = null): Result<T> => ({ ok: false, data, error });
10
+ export const fail = <T = never>(error: string, data: T | null = null): Result<T> => ({
11
+ ok: false,
12
+ data,
13
+ error,
14
+ });
11
15
 
12
16
  const revision = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
13
17
 
@@ -25,7 +29,13 @@ export function gitRevisionParts(value: string): string[] {
25
29
 
26
30
  export function resolveGitRevision(root: string, value: string): void {
27
31
  for (const part of gitRevisionParts(value)) {
28
- const result = run(root, "git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${part}^{commit}`]);
32
+ const result = run(root, "git", [
33
+ "rev-parse",
34
+ "--verify",
35
+ "--quiet",
36
+ "--end-of-options",
37
+ `${part}^{commit}`,
38
+ ]);
29
39
  if (result.exitCode !== 0) throw new Error(`invalid Git revision or range: ${value}`);
30
40
  }
31
41
  }
@@ -46,7 +56,12 @@ export function resolveInside(root: string, candidate: string): string {
46
56
  return target;
47
57
  }
48
58
 
49
- export function run(root: string, executable: string, args: string[], env: Record<string, string> = {}) {
59
+ export function run(
60
+ root: string,
61
+ executable: string,
62
+ args: string[],
63
+ env: Record<string, string> = {},
64
+ ) {
50
65
  const cwd = realpathSync(root);
51
66
  const result = spawnSync(executable, args, {
52
67
  cwd,
@@ -7,7 +7,8 @@ const output = (value: unknown) => JSON.stringify(value, null, 2);
7
7
  export function createDocsRepoTools() {
8
8
  return {
9
9
  workflow_docs_repo_link: tool({
10
- description: "Link the component docs repo in the toolkit config (validates git repo + features/)",
10
+ description:
11
+ "Link the component docs repo in the toolkit config (validates git repo + features/)",
11
12
  args: {
12
13
  path: tool.schema.string(),
13
14
  confirmed: tool.schema.boolean(),
@@ -26,7 +27,8 @@ export function createDocsRepoTools() {
26
27
  },
27
28
  }),
28
29
  workflow_docs_promote: tool({
29
- description: "Promote a spec (+plan) to the linked docs repo features/YYYY-MM-<slug>/ with quality gate",
30
+ description:
31
+ "Promote a spec (+plan) to the linked docs repo features/YYYY-MM-<slug>/ with quality gate",
30
32
  args: {
31
33
  slug: tool.schema.string(),
32
34
  confirmed: tool.schema.boolean(),
@@ -34,7 +36,14 @@ export function createDocsRepoTools() {
34
36
  },
35
37
  execute: async ({ slug, confirmed, force }, context) => {
36
38
  const result = promoteSpec(context.directory, slug, { confirmed, force });
37
- if (result.ok) return output(ok({ target_dir: result.target_dir, files: result.files, index_updated: result.index_updated }));
39
+ if (result.ok)
40
+ return output(
41
+ ok({
42
+ target_dir: result.target_dir,
43
+ files: result.files,
44
+ index_updated: result.index_updated,
45
+ }),
46
+ );
38
47
  return output(fail(result.error, { findings: result.findings ?? [] } as never));
39
48
  },
40
49
  }),
package/src/tools/flow.ts CHANGED
@@ -11,8 +11,7 @@ import path from "node:path";
11
11
 
12
12
  const output = (value: unknown) => JSON.stringify(value, null, 2);
13
13
 
14
- const flowPathFor = (slug: string) =>
15
- path.posix.join("docs", slug, "sdd", "flow.json");
14
+ const flowPathFor = (slug: string) => path.posix.join("docs", slug, "sdd", "flow.json");
16
15
 
17
16
  export function createFlowTools() {
18
17
  return {
@@ -27,20 +26,23 @@ export function createFlowTools() {
27
26
  const slug = slugFromPath(plan_path ?? spec_path ?? "");
28
27
  if (!slug) return output(fail("plan_path or spec_path required"));
29
28
  const state = readFlowState(context.directory, slug);
30
- return output(ok({
31
- slug,
32
- spec: state.spec,
33
- plan: state.plan,
34
- menu: state.menu,
35
- flow_path: flowPathFor(slug),
36
- }));
29
+ return output(
30
+ ok({
31
+ slug,
32
+ spec: state.spec,
33
+ plan: state.plan,
34
+ menu: state.menu,
35
+ flow_path: flowPathFor(slug),
36
+ }),
37
+ );
37
38
  } catch (error) {
38
39
  return output(fail(error instanceof Error ? error.message : "flow status failed"));
39
40
  }
40
41
  },
41
42
  }),
42
43
  workflow_spec_approve: tool({
43
- description: "Advance spec status: first call self_reviewed, second call approved (after user approval)",
44
+ description:
45
+ "Advance spec status: first call self_reviewed, second call approved (after user approval)",
44
46
  args: {
45
47
  confirmed: tool.schema.boolean(),
46
48
  spec_path: tool.schema.string(),
@@ -56,7 +58,8 @@ export function createFlowTools() {
56
58
  },
57
59
  }),
58
60
  workflow_plan_approve: tool({
59
- description: "Advance plan status: first call self_reviewed, second call approved. Requires approved spec.",
61
+ description:
62
+ "Advance plan status: first call self_reviewed, second call approved. Requires approved spec.",
60
63
  args: {
61
64
  confirmed: tool.schema.boolean(),
62
65
  plan_path: tool.schema.string(),
@@ -76,12 +79,20 @@ export function createFlowTools() {
76
79
  args: {
77
80
  confirmed: tool.schema.boolean(),
78
81
  plan_path: tool.schema.string(),
79
- choice: tool.schema.enum(["subagent-driven", "inline", "handoff", "review-spec", "review-plan"]),
82
+ choice: tool.schema.enum([
83
+ "subagent-driven",
84
+ "inline",
85
+ "handoff",
86
+ "review-spec",
87
+ "review-plan",
88
+ ]),
80
89
  },
81
90
  execute: async ({ confirmed, plan_path, choice }, context) => {
82
91
  const slug = slugFromPath(plan_path);
83
92
  const result = recordMenuChoice(context.directory, slug, plan_path, choice, confirmed);
84
- return output(result.ok ? ok({ menu: { presented: true, chosen: choice } }) : fail(result.error));
93
+ return output(
94
+ result.ok ? ok({ menu: { presented: true, chosen: choice } }) : fail(result.error),
95
+ );
85
96
  },
86
97
  }),
87
98
  };