@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/tools/sdd.ts CHANGED
@@ -5,15 +5,12 @@ import { fail, ok, resolveGitRevision, resolveInside } from "../core";
5
5
  import { resolveBranch, docsBranch } from "../core/branch";
6
6
  import { docsValidate } from "../core/docs-validate";
7
7
  import { parsePlanTasks, resolveHandoffBranch } from "../core/plan-tasks";
8
- import {
9
- sddAppendProgress, sddContext, sddReviewPackage, sddTaskBrief,
10
- } from "../core/sdd";
8
+ import { sddAppendProgress, sddContext, sddReviewPackage, sddTaskBrief } from "../core/sdd";
11
9
  import { WorkflowStateStore } from "../state";
12
10
 
13
11
  const output = (value: unknown) => JSON.stringify(value, null, 2);
14
- const requireConfirmed = (confirmed: boolean) => confirmed === true
15
- ? null
16
- : output(fail("confirmed: true required"));
12
+ const requireConfirmed = (confirmed: boolean) =>
13
+ confirmed === true ? null : output(fail("confirmed: true required"));
17
14
 
18
15
  const relativePath = (root: string, candidate: string) => {
19
16
  if (path.isAbsolute(candidate)) throw new Error("path must be repository-relative");
@@ -49,89 +46,109 @@ const planPaths = (root: string, planPath: string, suppliedSpecPath?: string) =>
49
46
  };
50
47
 
51
48
  export function createSddTools(state: WorkflowStateStore) {
52
- const record = (context: ToolContext, data: Record<string, unknown>) => state.set(context.sessionID, {
53
- spec: String(data.spec_path ?? ""),
54
- plan: String(data.plan_path ?? ""),
55
- sdd: String(data.sdd_dir ?? ""),
56
- });
49
+ const record = (context: ToolContext, data: Record<string, unknown>) =>
50
+ state.set(context.sessionID, {
51
+ spec: String(data.spec_path ?? ""),
52
+ plan: String(data.plan_path ?? ""),
53
+ sdd: String(data.sdd_dir ?? ""),
54
+ });
57
55
 
58
56
  return {
59
57
  workflow_docs_branch: tool({
60
- description: "Resolve branch for spec/plan authors: keep current feature|bugfix or create from develop",
58
+ description:
59
+ "Resolve branch for spec/plan authors: keep current feature|bugfix or create from the configured base",
61
60
  args: {
62
61
  plan_path: tool.schema.string().optional(),
63
62
  kind: tool.schema.enum(["feature", "bugfix"]).optional(),
64
63
  },
65
- execute: async ({ plan_path, kind }, context) => invoke(() => {
66
- if (plan_path) relativePath(context.directory, plan_path);
67
- return docsBranch({
68
- plan_path,
69
- kind,
70
- workspace_root: context.directory,
71
- }) as Record<string, unknown>;
72
- }),
64
+ execute: async ({ plan_path, kind }, context) =>
65
+ invoke(() => {
66
+ if (plan_path) relativePath(context.directory, plan_path);
67
+ return docsBranch({
68
+ plan_path,
69
+ kind,
70
+ workspace_root: context.directory,
71
+ }) as Record<string, unknown>;
72
+ }),
73
73
  }),
74
74
  workflow_docs_validate: tool({
75
- description: "Hard-fail validate spec/plan headers, link, branch, task order; returns quality findings (hard/warning)",
75
+ description:
76
+ "Hard-fail validate spec/plan headers, link, branch, task order; returns quality findings (hard/warning)",
76
77
  args: {
77
78
  spec_path: tool.schema.string(),
78
79
  plan_path: tool.schema.string(),
79
80
  },
80
- execute: async ({ spec_path, plan_path }, context) => invoke(() => {
81
- relativePath(context.directory, spec_path);
82
- relativePath(context.directory, plan_path);
83
- const result = docsValidate({
84
- spec_path,
85
- plan_path,
86
- workspace_root: context.directory,
87
- }) as Record<string, unknown>;
88
- if (result.error) return result;
89
- if (result.ok === false) return result;
90
- return result;
91
- }),
81
+ execute: async ({ spec_path, plan_path }, context) =>
82
+ invoke(() => {
83
+ relativePath(context.directory, spec_path);
84
+ relativePath(context.directory, plan_path);
85
+ const result = docsValidate({
86
+ spec_path,
87
+ plan_path,
88
+ workspace_root: context.directory,
89
+ }) as Record<string, unknown>;
90
+ if (result.error) return result;
91
+ if (result.ok === false) return result;
92
+ return result;
93
+ }),
92
94
  }),
93
95
  workflow_plan_tasks: tool({
94
96
  description: "Parse top-level tasks from a workflow plan",
95
97
  args: { plan_path: tool.schema.string(), spec_path: tool.schema.string().optional() },
96
- execute: async ({ plan_path, spec_path }, context) => invoke(() => {
97
- relativePath(context.directory, plan_path);
98
- const paths = planPaths(context.directory, plan_path, spec_path);
99
- const parsed = parsePlanTasks(plan_path, context.directory) as Record<string, unknown>;
100
- if (parsed.error) return parsed;
101
- const branch = paths.spec_path
102
- ? resolveHandoffBranch(paths.spec_path, plan_path, context.directory) as Record<string, unknown>
103
- : {};
104
- if (branch.error) return branch;
105
- const data = { ...parsed, ...paths, ...branch };
106
- record(context, data);
107
- return data;
108
- }),
98
+ execute: async ({ plan_path, spec_path }, context) =>
99
+ invoke(() => {
100
+ relativePath(context.directory, plan_path);
101
+ const paths = planPaths(context.directory, plan_path, spec_path);
102
+ const parsed = parsePlanTasks(plan_path, context.directory) as Record<string, unknown>;
103
+ if (parsed.error) return parsed;
104
+ const branch = paths.spec_path
105
+ ? (resolveHandoffBranch(paths.spec_path, plan_path, context.directory) as Record<
106
+ string,
107
+ unknown
108
+ >)
109
+ : {};
110
+ if (branch.error) return branch;
111
+ const data = { ...parsed, ...paths, ...branch };
112
+ record(context, data);
113
+ return data;
114
+ }),
109
115
  }),
110
116
  workflow_resolve_branch: tool({
111
117
  description: "Resolve a branch from repository spec and plan metadata",
112
118
  args: { spec_path: tool.schema.string(), plan_path: tool.schema.string() },
113
- execute: async ({ spec_path, plan_path }, context) => invoke(() => {
114
- relativePath(context.directory, spec_path);
115
- relativePath(context.directory, plan_path);
116
- return resolveBranch({ spec_path, plan_path, workspace_root: context.directory });
117
- }),
119
+ execute: async ({ spec_path, plan_path }, context) =>
120
+ invoke(() => {
121
+ relativePath(context.directory, spec_path);
122
+ relativePath(context.directory, plan_path);
123
+ return resolveBranch({ spec_path, plan_path, workspace_root: context.directory });
124
+ }),
118
125
  }),
119
126
  workflow_sdd_context: tool({
120
127
  description: "Resolve the SDD workspace and progress ledger",
121
128
  args: { plan_path: tool.schema.string() },
122
- execute: async ({ plan_path }, context) => invoke(() => {
123
- relativePath(context.directory, plan_path);
124
- const parsed = sddContext({ slug: undefined, plan_path, workspace_root: context.directory }) as Record<string, unknown>;
125
- if (parsed.error) return parsed;
126
- const todos = Array.isArray(parsed.todos)
127
- ? parsed.todos.map((todo: Record<string, unknown>) => todo.status === "in_progress"
128
- ? { ...todo, status: "pending" }
129
- : todo)
130
- : [];
131
- const data = { ...parsed, todos, ...planPaths(context.directory, plan_path), sdd_dir: parsed.sdd_dir };
132
- record(context, data);
133
- return data;
134
- }),
129
+ execute: async ({ plan_path }, context) =>
130
+ invoke(() => {
131
+ relativePath(context.directory, plan_path);
132
+ const parsed = sddContext({
133
+ slug: undefined,
134
+ plan_path,
135
+ workspace_root: context.directory,
136
+ }) as Record<string, unknown>;
137
+ if (parsed.error) return parsed;
138
+ const todos = Array.isArray(parsed.todos)
139
+ ? parsed.todos.map((todo: Record<string, unknown>) =>
140
+ todo.status === "in_progress" ? { ...todo, status: "pending" } : todo,
141
+ )
142
+ : [];
143
+ const data = {
144
+ ...parsed,
145
+ todos,
146
+ ...planPaths(context.directory, plan_path),
147
+ sdd_dir: parsed.sdd_dir,
148
+ };
149
+ record(context, data);
150
+ return data;
151
+ }),
135
152
  }),
136
153
  workflow_sdd_task_brief: tool({
137
154
  description: "Write a confirmed task brief",
@@ -146,7 +163,12 @@ export function createSddTools(state: WorkflowStateStore) {
146
163
  if (rejected) return rejected;
147
164
  return invoke(() => {
148
165
  relativePath(context.directory, sdd_dir);
149
- return sddTaskBrief({ sdd_dir, task_id, section_text, workspace_root: context.directory });
166
+ return sddTaskBrief({
167
+ sdd_dir,
168
+ task_id,
169
+ section_text,
170
+ workspace_root: context.directory,
171
+ });
150
172
  });
151
173
  },
152
174
  }),
@@ -165,7 +187,12 @@ export function createSddTools(state: WorkflowStateStore) {
165
187
  relativePath(context.directory, sdd_dir);
166
188
  resolveGitRevision(context.directory, base_sha);
167
189
  resolveGitRevision(context.directory, head_sha);
168
- return sddReviewPackage({ sdd_dir, base_sha, head_sha, workspace_root: context.directory });
190
+ return sddReviewPackage({
191
+ sdd_dir,
192
+ base_sha,
193
+ head_sha,
194
+ workspace_root: context.directory,
195
+ });
169
196
  });
170
197
  },
171
198
  }),
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { tool, type ToolContext } from "@opencode-ai/plugin";
5
5
  import { fail, ok, resolveInside, type Result } from "../core";
6
6
  import { configGuardError, describeConfigGaps } from "../core/config-guard";
7
+ import { configDir } from "../core/config";
7
8
  import {
8
9
  buildDraft as legacyBuildDraft,
9
10
  context as legacyContext,
@@ -16,15 +17,18 @@ import {
16
17
 
17
18
  const ISSUE_RE = /^[A-Z]+-\d+$/;
18
19
  const output = (value: unknown) => JSON.stringify(value, null, 2);
19
- const message = (error: unknown) => error instanceof Error ? error.message : String(error);
20
+ const message = (error: unknown) => (error instanceof Error ? error.message : String(error));
20
21
 
21
22
  // Both override names point at the config dir itself, same precedence as
22
23
  // src/core/config.ts and scripts/init/status.sh: WORKFLOW_TOOLKIT_CONFIG → WORKFLOW_TOOLKIT_CONFIG_DIR → XDG.
24
+ // Default env (no args) routes through configDir() so the legacy migration runs.
23
25
  export const configPath = (env: NodeJS.ProcessEnv = process.env, home = os.homedir()) =>
24
26
  path.join(
25
- env.WORKFLOW_TOOLKIT_CONFIG
26
- ?? env.WORKFLOW_TOOLKIT_CONFIG_DIR
27
- ?? path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "workflow-toolkit"),
27
+ env === process.env
28
+ ? configDir()
29
+ : (env.WORKFLOW_TOOLKIT_CONFIG ??
30
+ env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
31
+ path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "workit")),
28
32
  "youtrack.json",
29
33
  );
30
34
 
@@ -33,7 +37,8 @@ export function readCredentials(env: NodeJS.ProcessEnv = process.env, home = os.
33
37
  const config = JSON.parse(readFileSync(resolvedConfig, "utf8")) as { tokenFile?: string };
34
38
  const tokenFile = config.tokenFile ?? "youtrack.token";
35
39
  const tokenPath = path.resolve(path.dirname(resolvedConfig), tokenFile.replace(/^~(?=\/)/, home));
36
- if (process.platform !== "win32" && (statSync(tokenPath).mode & 0o777) !== 0o600) throw new Error("youtrack.token mode must be 0600");
40
+ if (process.platform !== "win32" && (statSync(tokenPath).mode & 0o777) !== 0o600)
41
+ throw new Error("youtrack.token mode must be 0600");
37
42
  const token = readFileSync(tokenPath, "utf8").trim();
38
43
  if (!token) throw new Error("youtrack.token is empty");
39
44
  return { configPath: resolvedConfig, token };
@@ -65,9 +70,13 @@ const defaultOperations: YouTrackOperations = {
65
70
  verifyToken: () => unwrap(verifyYouTrackToken()),
66
71
  context: (input) => legacyContext(input as never),
67
72
  parseDuration: (text, workspaceRoot) => legacyParseDuration(text, workspaceRoot),
68
- postComment: (issueId, markdown, workspaceRoot) => legacyPostUpdate({
69
- confirmed: true, issueId, markdown, workspace_root: workspaceRoot,
70
- } as never),
73
+ postComment: (issueId, markdown, workspaceRoot) =>
74
+ legacyPostUpdate({
75
+ confirmed: true,
76
+ issueId,
77
+ markdown,
78
+ workspace_root: workspaceRoot,
79
+ } as never),
71
80
  logTime: (input) => legacyLogTime(input as never),
72
81
  };
73
82
 
@@ -112,15 +121,26 @@ export async function postUpdate(
112
121
  if (input.minutes != null && input.minutes <= 0) return fail("minutes must be positive");
113
122
 
114
123
  try {
115
- const comment = await operations.postComment(input.issueId, input.markdown, input.workspace_root);
116
- if (notApplied(comment)) return fail(comment.error, {
117
- issueId: input.issueId, postedComment: false, loggedMinutes: 0,
118
- outcome: "not_applied", retry: "workflow_youtrack_post",
119
- });
124
+ const comment = await operations.postComment(
125
+ input.issueId,
126
+ input.markdown,
127
+ input.workspace_root,
128
+ );
129
+ if (notApplied(comment))
130
+ return fail(comment.error, {
131
+ issueId: input.issueId,
132
+ postedComment: false,
133
+ loggedMinutes: 0,
134
+ outcome: "not_applied",
135
+ retry: "workflow_youtrack_post",
136
+ });
120
137
  unwrap(comment);
121
138
  } catch (error) {
122
139
  return fail(message(error), {
123
- issueId: input.issueId, postedComment: false, loggedMinutes: 0, outcome: "unknown",
140
+ issueId: input.issueId,
141
+ postedComment: false,
142
+ loggedMinutes: 0,
143
+ outcome: "unknown",
124
144
  instructions: "Check YouTrack comments manually; do not retry while the outcome is unknown.",
125
145
  });
126
146
  }
@@ -133,15 +153,23 @@ export async function postUpdate(
133
153
  text: "workit update",
134
154
  workspace_root: input.workspace_root,
135
155
  });
136
- if (notApplied(time)) return fail(time.error, {
137
- issueId: input.issueId, postedComment: true, loggedMinutes: 0,
138
- outcome: "not_applied", retry: "workflow_youtrack_log_time",
139
- });
156
+ if (notApplied(time))
157
+ return fail(time.error, {
158
+ issueId: input.issueId,
159
+ postedComment: true,
160
+ loggedMinutes: 0,
161
+ outcome: "not_applied",
162
+ retry: "workflow_youtrack_log_time",
163
+ });
140
164
  unwrap(time);
141
165
  } catch (error) {
142
166
  return fail(message(error), {
143
- issueId: input.issueId, postedComment: true, loggedMinutes: 0, outcome: "unknown",
144
- instructions: "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
167
+ issueId: input.issueId,
168
+ postedComment: true,
169
+ loggedMinutes: 0,
170
+ outcome: "unknown",
171
+ instructions:
172
+ "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
145
173
  });
146
174
  }
147
175
  }
@@ -159,15 +187,21 @@ export async function logTimeUpdate(
159
187
  ): Promise<Result<Record<string, unknown>>> {
160
188
  try {
161
189
  const value = await operation.logTime(input);
162
- if (notApplied(value)) return fail(value.error, {
163
- issueId: input.issueId, loggedMinutes: 0, outcome: "not_applied",
164
- retry: "workflow_youtrack_log_time",
165
- });
190
+ if (notApplied(value))
191
+ return fail(value.error, {
192
+ issueId: input.issueId,
193
+ loggedMinutes: 0,
194
+ outcome: "not_applied",
195
+ retry: "workflow_youtrack_log_time",
196
+ });
166
197
  return ok(unwrap(value));
167
198
  } catch (error) {
168
199
  return fail(message(error), {
169
- issueId: input.issueId, loggedMinutes: 0, outcome: "unknown",
170
- instructions: "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
200
+ issueId: input.issueId,
201
+ loggedMinutes: 0,
202
+ outcome: "unknown",
203
+ instructions:
204
+ "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
171
205
  });
172
206
  }
173
207
  }
@@ -178,10 +212,13 @@ export function normalizeContext(value: LegacyValue, mode?: string): LegacyValue
178
212
  const issue = String(config?.meetingIssue || "IRPT-12");
179
213
  const { meetingIssues: _meetingIssues, ...singleMeetingConfig } = config ?? {};
180
214
  const options = Array.isArray(value.meetingOptions)
181
- ? value.meetingOptions as Array<Record<string, unknown>>
215
+ ? (value.meetingOptions as Array<Record<string, unknown>>)
182
216
  : [];
183
217
  const selected = options.find((option) => option.issue === issue) ?? {
184
- key: "general", issue, label: issue, workItemText: "Reuniones",
218
+ key: "general",
219
+ issue,
220
+ label: issue,
221
+ workItemText: "Reuniones",
185
222
  };
186
223
  return {
187
224
  ...value,
@@ -216,9 +253,8 @@ const configGap = () => {
216
253
  const { missing } = describeConfigGaps(["youtrack_json", "youtrack_token"]);
217
254
  return missing.length > 0 ? output(fail(configGuardError(missing))) : null;
218
255
  };
219
- const requireConfirmed = (confirmed: boolean) => confirmed === true
220
- ? null
221
- : output(fail("confirmed: true required"));
256
+ const requireConfirmed = (confirmed: boolean) =>
257
+ confirmed === true ? null : output(fail("confirmed: true required"));
222
258
 
223
259
  const rejectedTimeInput = (issueId: string, minutes: number) => {
224
260
  const error = !ISSUE_RE.test(issueId)
@@ -226,11 +262,17 @@ const rejectedTimeInput = (issueId: string, minutes: number) => {
226
262
  : !Number.isFinite(minutes) || minutes <= 0
227
263
  ? "minutes must be positive"
228
264
  : null;
229
- return error ? output(fail(error, {
230
- issueId, loggedMinutes: 0, outcome: "not_applied",
231
- retry: "workflow_youtrack_log_time",
232
- instructions: "Correct the invalid input, then retry workflow_youtrack_log_time once.",
233
- })) : null;
265
+ return error
266
+ ? output(
267
+ fail(error, {
268
+ issueId,
269
+ loggedMinutes: 0,
270
+ outcome: "not_applied",
271
+ retry: "workflow_youtrack_log_time",
272
+ instructions: "Correct the invalid input, then retry workflow_youtrack_log_time once.",
273
+ }),
274
+ )
275
+ : null;
234
276
  };
235
277
 
236
278
  export function createYouTrackTools(operations: YouTrackOperations = defaultOperations) {
@@ -240,7 +282,9 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
240
282
  args: {},
241
283
  execute: async () => {
242
284
  let token = "";
243
- try { token = credentials().token; } catch (error) {
285
+ try {
286
+ token = credentials().token;
287
+ } catch (error) {
244
288
  const gap = configGap();
245
289
  if (gap) return gap;
246
290
  return output(fail(message(error)));
@@ -254,7 +298,8 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
254
298
  execute: async ({ issue_ref }) => invoke(() => parseIssueRef(issue_ref)),
255
299
  }),
256
300
  workflow_youtrack_context: tool({
257
- description: "Load YouTrack context for the configured meeting issue or an existing task issue",
301
+ description:
302
+ "Load YouTrack context for the configured meeting issue or an existing task issue",
258
303
  args: {
259
304
  mode: tool.schema.enum(["meetings", "task"]).optional(),
260
305
  issue_id: tool.schema.string().optional(),
@@ -271,23 +316,37 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
271
316
  }
272
317
  } catch (error) {
273
318
  const detail = message(error);
274
- return output(fail(detail.includes("repository-relative") ? detail : `path must be repository-relative: ${detail}`));
319
+ return output(
320
+ fail(
321
+ detail.includes("repository-relative")
322
+ ? detail
323
+ : `path must be repository-relative: ${detail}`,
324
+ ),
325
+ );
275
326
  }
276
327
  let token = "";
277
- try { token = credentials().token; } catch (error) {
328
+ try {
329
+ token = credentials().token;
330
+ } catch (error) {
278
331
  const gap = configGap();
279
332
  if (gap) return gap;
280
333
  return output(fail(message(error)));
281
334
  }
282
- return invoke(async () => normalizeContext(
283
- await operations.context({ ...input, workspace_root: context.directory }), input.mode,
284
- ), token);
335
+ return invoke(
336
+ async () =>
337
+ normalizeContext(
338
+ await operations.context({ ...input, workspace_root: context.directory }),
339
+ input.mode,
340
+ ),
341
+ token,
342
+ );
285
343
  },
286
344
  }),
287
345
  workflow_youtrack_parse_duration: tool({
288
346
  description: "Parse duration text into integer minutes",
289
347
  args: { text: tool.schema.string() },
290
- execute: async ({ text }, context) => invoke(() => operations.parseDuration(text, context.directory)),
348
+ execute: async ({ text }, context) =>
349
+ invoke(() => operations.parseDuration(text, context.directory)),
291
350
  }),
292
351
  workflow_youtrack_draft: tool({
293
352
  description: "Build an es-CL update comment without posting it",
@@ -298,10 +357,12 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
298
357
  projectName: tool.schema.string().optional(),
299
358
  includeProjectOpener: tool.schema.boolean().optional(),
300
359
  includeFacts: tool.schema.boolean().optional(),
301
- facts: tool.schema.object({
302
- progress_excerpt: tool.schema.array(tool.schema.string()).optional(),
303
- git_commits: tool.schema.array(tool.schema.string()).optional(),
304
- }).optional(),
360
+ facts: tool.schema
361
+ .object({
362
+ progress_excerpt: tool.schema.array(tool.schema.string()).optional(),
363
+ git_commits: tool.schema.array(tool.schema.string()).optional(),
364
+ })
365
+ .optional(),
305
366
  },
306
367
  execute: async (input) => invoke(() => legacyBuildDraft(input as never)),
307
368
  }),
@@ -320,13 +381,16 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
320
381
  const invalid = rejectedTimeInput(input.issueId, input.minutes);
321
382
  if (invalid) return invalid;
322
383
  let token = "";
323
- try { token = credentials().token; } catch (error) {
384
+ try {
385
+ token = credentials().token;
386
+ } catch (error) {
324
387
  const gap = configGap();
325
388
  if (gap) return gap;
326
389
  return output(fail(message(error)));
327
390
  }
328
391
  const result = await withWriteFlag(() =>
329
- logTimeUpdate({ ...input, workspace_root: context.directory }, operations));
392
+ logTimeUpdate({ ...input, workspace_root: context.directory }, operations),
393
+ );
330
394
  return output(result.ok ? result : { ...result, error: redact(result.error, token) });
331
395
  },
332
396
  }),
@@ -342,13 +406,16 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
342
406
  const rejected = requireConfirmed(input.confirmed);
343
407
  if (rejected) return rejected;
344
408
  let token = "";
345
- try { token = credentials().token; } catch (error) {
409
+ try {
410
+ token = credentials().token;
411
+ } catch (error) {
346
412
  const gap = configGap();
347
413
  if (gap) return gap;
348
414
  return output(fail(message(error)));
349
415
  }
350
416
  const result = await withWriteFlag(() =>
351
- postUpdate({ ...input, workspace_root: context.directory }, operations));
417
+ postUpdate({ ...input, workspace_root: context.directory }, operations),
418
+ );
352
419
  return output(result.ok ? result : { ...result, error: redact(result.error, token) });
353
420
  },
354
421
  }),
@@ -27,7 +27,7 @@ Plans require:
27
27
 
28
28
  `bugfix/<slug>` is also valid. Never use `main`, `develop`, `master`, or `prod`. Use plain backtick paths. Top-level headings are exactly `### Task N: Title`; steps use `- [ ] **Step N:** ...`; task headings never appear inside fences.
29
29
 
30
- Before writing **Branch:** into a new spec or plan, call `workflow_docs_branch` and write the returned `branch` verbatim. When `action` is `keep`, use the current feature/bugfix branch. When `action` is `create_from_develop`, create the branch only through `workflow_branch_setup` (never branch from `main`/`master`).
30
+ Before writing **Branch:** into a new spec or plan, call `workflow_docs_branch` and write the returned `branch` verbatim. When `action` is `keep`, use the current feature/bugfix branch. When `action` is `create_from_develop` or `create_from_base`, create the branch only through `workflow_branch_setup`; it uses the configured workspace/global target branch.
31
31
 
32
32
  ## Execution and handoff
33
33