@brainervirus/workit-core 0.6.0 → 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 (40) hide show
  1. package/package.json +9 -9
  2. package/scripts/_shared/common.sh +18 -3
  3. package/scripts/install-opencode-plugin.sh +14 -9
  4. package/src/core/branch.ts +143 -48
  5. package/src/core/changelog.ts +17 -14
  6. package/src/core/config-guard.ts +9 -2
  7. package/src/core/config.ts +48 -15
  8. package/src/core/detector.ts +22 -11
  9. package/src/core/docs-repo.ts +49 -14
  10. package/src/core/docs-validate.ts +163 -37
  11. package/src/core/flow-state.ts +6 -2
  12. package/src/core/gitignore.ts +11 -2
  13. package/src/core/handoff-context.ts +18 -5
  14. package/src/core/hygiene.ts +27 -5
  15. package/src/core/init.ts +86 -21
  16. package/src/core/parse-sections.ts +2 -2
  17. package/src/core/plan-tasks.ts +13 -3
  18. package/src/core/ports/youtrack-api.ts +3 -1
  19. package/src/core/ports/youtrack-config.ts +1 -3
  20. package/src/core/pr-create.ts +47 -15
  21. package/src/core/present.ts +11 -2
  22. package/src/core/reminder.ts +1 -2
  23. package/src/core/repo-tool.ts +4 -1
  24. package/src/core/rules.ts +10 -7
  25. package/src/core/scripts.ts +7 -2
  26. package/src/core/sdd.ts +11 -3
  27. package/src/core/templates.ts +14 -4
  28. package/src/core/vcs-config.ts +93 -34
  29. package/src/core/verify-parse.ts +4 -2
  30. package/src/core/workspaces.ts +2 -2
  31. package/src/core/youtrack.ts +231 -56
  32. package/src/core.ts +18 -3
  33. package/src/tools/docs-repo.ts +12 -3
  34. package/src/tools/flow.ts +24 -13
  35. package/src/tools/handoff.ts +28 -23
  36. package/src/tools/present.ts +14 -10
  37. package/src/tools/repo.ts +220 -87
  38. package/src/tools/sdd.ts +93 -66
  39. package/src/tools/youtrack.ts +115 -52
  40. 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
  }),
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  const ISSUE_RE = /^[A-Z]+-\d+$/;
19
19
  const output = (value: unknown) => JSON.stringify(value, null, 2);
20
- const message = (error: unknown) => error instanceof Error ? error.message : String(error);
20
+ const message = (error: unknown) => (error instanceof Error ? error.message : String(error));
21
21
 
22
22
  // Both override names point at the config dir itself, same precedence as
23
23
  // src/core/config.ts and scripts/init/status.sh: WORKFLOW_TOOLKIT_CONFIG → WORKFLOW_TOOLKIT_CONFIG_DIR → XDG.
@@ -26,9 +26,9 @@ export const configPath = (env: NodeJS.ProcessEnv = process.env, home = os.homed
26
26
  path.join(
27
27
  env === process.env
28
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"),
29
+ : (env.WORKFLOW_TOOLKIT_CONFIG ??
30
+ env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
31
+ path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "workit")),
32
32
  "youtrack.json",
33
33
  );
34
34
 
@@ -37,7 +37,8 @@ export function readCredentials(env: NodeJS.ProcessEnv = process.env, home = os.
37
37
  const config = JSON.parse(readFileSync(resolvedConfig, "utf8")) as { tokenFile?: string };
38
38
  const tokenFile = config.tokenFile ?? "youtrack.token";
39
39
  const tokenPath = path.resolve(path.dirname(resolvedConfig), tokenFile.replace(/^~(?=\/)/, home));
40
- 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");
41
42
  const token = readFileSync(tokenPath, "utf8").trim();
42
43
  if (!token) throw new Error("youtrack.token is empty");
43
44
  return { configPath: resolvedConfig, token };
@@ -69,9 +70,13 @@ const defaultOperations: YouTrackOperations = {
69
70
  verifyToken: () => unwrap(verifyYouTrackToken()),
70
71
  context: (input) => legacyContext(input as never),
71
72
  parseDuration: (text, workspaceRoot) => legacyParseDuration(text, workspaceRoot),
72
- postComment: (issueId, markdown, workspaceRoot) => legacyPostUpdate({
73
- confirmed: true, issueId, markdown, workspace_root: workspaceRoot,
74
- } as never),
73
+ postComment: (issueId, markdown, workspaceRoot) =>
74
+ legacyPostUpdate({
75
+ confirmed: true,
76
+ issueId,
77
+ markdown,
78
+ workspace_root: workspaceRoot,
79
+ } as never),
75
80
  logTime: (input) => legacyLogTime(input as never),
76
81
  };
77
82
 
@@ -116,15 +121,26 @@ export async function postUpdate(
116
121
  if (input.minutes != null && input.minutes <= 0) return fail("minutes must be positive");
117
122
 
118
123
  try {
119
- const comment = await operations.postComment(input.issueId, input.markdown, input.workspace_root);
120
- if (notApplied(comment)) return fail(comment.error, {
121
- issueId: input.issueId, postedComment: false, loggedMinutes: 0,
122
- outcome: "not_applied", retry: "workflow_youtrack_post",
123
- });
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
+ });
124
137
  unwrap(comment);
125
138
  } catch (error) {
126
139
  return fail(message(error), {
127
- issueId: input.issueId, postedComment: false, loggedMinutes: 0, outcome: "unknown",
140
+ issueId: input.issueId,
141
+ postedComment: false,
142
+ loggedMinutes: 0,
143
+ outcome: "unknown",
128
144
  instructions: "Check YouTrack comments manually; do not retry while the outcome is unknown.",
129
145
  });
130
146
  }
@@ -137,15 +153,23 @@ export async function postUpdate(
137
153
  text: "workit update",
138
154
  workspace_root: input.workspace_root,
139
155
  });
140
- if (notApplied(time)) return fail(time.error, {
141
- issueId: input.issueId, postedComment: true, loggedMinutes: 0,
142
- outcome: "not_applied", retry: "workflow_youtrack_log_time",
143
- });
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
+ });
144
164
  unwrap(time);
145
165
  } catch (error) {
146
166
  return fail(message(error), {
147
- issueId: input.issueId, postedComment: true, loggedMinutes: 0, outcome: "unknown",
148
- 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.",
149
173
  });
150
174
  }
151
175
  }
@@ -163,15 +187,21 @@ export async function logTimeUpdate(
163
187
  ): Promise<Result<Record<string, unknown>>> {
164
188
  try {
165
189
  const value = await operation.logTime(input);
166
- if (notApplied(value)) return fail(value.error, {
167
- issueId: input.issueId, loggedMinutes: 0, outcome: "not_applied",
168
- retry: "workflow_youtrack_log_time",
169
- });
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
+ });
170
197
  return ok(unwrap(value));
171
198
  } catch (error) {
172
199
  return fail(message(error), {
173
- issueId: input.issueId, loggedMinutes: 0, outcome: "unknown",
174
- 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.",
175
205
  });
176
206
  }
177
207
  }
@@ -182,10 +212,13 @@ export function normalizeContext(value: LegacyValue, mode?: string): LegacyValue
182
212
  const issue = String(config?.meetingIssue || "IRPT-12");
183
213
  const { meetingIssues: _meetingIssues, ...singleMeetingConfig } = config ?? {};
184
214
  const options = Array.isArray(value.meetingOptions)
185
- ? value.meetingOptions as Array<Record<string, unknown>>
215
+ ? (value.meetingOptions as Array<Record<string, unknown>>)
186
216
  : [];
187
217
  const selected = options.find((option) => option.issue === issue) ?? {
188
- key: "general", issue, label: issue, workItemText: "Reuniones",
218
+ key: "general",
219
+ issue,
220
+ label: issue,
221
+ workItemText: "Reuniones",
189
222
  };
190
223
  return {
191
224
  ...value,
@@ -220,9 +253,8 @@ const configGap = () => {
220
253
  const { missing } = describeConfigGaps(["youtrack_json", "youtrack_token"]);
221
254
  return missing.length > 0 ? output(fail(configGuardError(missing))) : null;
222
255
  };
223
- const requireConfirmed = (confirmed: boolean) => confirmed === true
224
- ? null
225
- : output(fail("confirmed: true required"));
256
+ const requireConfirmed = (confirmed: boolean) =>
257
+ confirmed === true ? null : output(fail("confirmed: true required"));
226
258
 
227
259
  const rejectedTimeInput = (issueId: string, minutes: number) => {
228
260
  const error = !ISSUE_RE.test(issueId)
@@ -230,11 +262,17 @@ const rejectedTimeInput = (issueId: string, minutes: number) => {
230
262
  : !Number.isFinite(minutes) || minutes <= 0
231
263
  ? "minutes must be positive"
232
264
  : null;
233
- return error ? output(fail(error, {
234
- issueId, loggedMinutes: 0, outcome: "not_applied",
235
- retry: "workflow_youtrack_log_time",
236
- instructions: "Correct the invalid input, then retry workflow_youtrack_log_time once.",
237
- })) : 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;
238
276
  };
239
277
 
240
278
  export function createYouTrackTools(operations: YouTrackOperations = defaultOperations) {
@@ -244,7 +282,9 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
244
282
  args: {},
245
283
  execute: async () => {
246
284
  let token = "";
247
- try { token = credentials().token; } catch (error) {
285
+ try {
286
+ token = credentials().token;
287
+ } catch (error) {
248
288
  const gap = configGap();
249
289
  if (gap) return gap;
250
290
  return output(fail(message(error)));
@@ -258,7 +298,8 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
258
298
  execute: async ({ issue_ref }) => invoke(() => parseIssueRef(issue_ref)),
259
299
  }),
260
300
  workflow_youtrack_context: tool({
261
- 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",
262
303
  args: {
263
304
  mode: tool.schema.enum(["meetings", "task"]).optional(),
264
305
  issue_id: tool.schema.string().optional(),
@@ -275,23 +316,37 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
275
316
  }
276
317
  } catch (error) {
277
318
  const detail = message(error);
278
- 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
+ );
279
326
  }
280
327
  let token = "";
281
- try { token = credentials().token; } catch (error) {
328
+ try {
329
+ token = credentials().token;
330
+ } catch (error) {
282
331
  const gap = configGap();
283
332
  if (gap) return gap;
284
333
  return output(fail(message(error)));
285
334
  }
286
- return invoke(async () => normalizeContext(
287
- await operations.context({ ...input, workspace_root: context.directory }), input.mode,
288
- ), token);
335
+ return invoke(
336
+ async () =>
337
+ normalizeContext(
338
+ await operations.context({ ...input, workspace_root: context.directory }),
339
+ input.mode,
340
+ ),
341
+ token,
342
+ );
289
343
  },
290
344
  }),
291
345
  workflow_youtrack_parse_duration: tool({
292
346
  description: "Parse duration text into integer minutes",
293
347
  args: { text: tool.schema.string() },
294
- execute: async ({ text }, context) => invoke(() => operations.parseDuration(text, context.directory)),
348
+ execute: async ({ text }, context) =>
349
+ invoke(() => operations.parseDuration(text, context.directory)),
295
350
  }),
296
351
  workflow_youtrack_draft: tool({
297
352
  description: "Build an es-CL update comment without posting it",
@@ -302,10 +357,12 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
302
357
  projectName: tool.schema.string().optional(),
303
358
  includeProjectOpener: tool.schema.boolean().optional(),
304
359
  includeFacts: tool.schema.boolean().optional(),
305
- facts: tool.schema.object({
306
- progress_excerpt: tool.schema.array(tool.schema.string()).optional(),
307
- git_commits: tool.schema.array(tool.schema.string()).optional(),
308
- }).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(),
309
366
  },
310
367
  execute: async (input) => invoke(() => legacyBuildDraft(input as never)),
311
368
  }),
@@ -324,13 +381,16 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
324
381
  const invalid = rejectedTimeInput(input.issueId, input.minutes);
325
382
  if (invalid) return invalid;
326
383
  let token = "";
327
- try { token = credentials().token; } catch (error) {
384
+ try {
385
+ token = credentials().token;
386
+ } catch (error) {
328
387
  const gap = configGap();
329
388
  if (gap) return gap;
330
389
  return output(fail(message(error)));
331
390
  }
332
391
  const result = await withWriteFlag(() =>
333
- logTimeUpdate({ ...input, workspace_root: context.directory }, operations));
392
+ logTimeUpdate({ ...input, workspace_root: context.directory }, operations),
393
+ );
334
394
  return output(result.ok ? result : { ...result, error: redact(result.error, token) });
335
395
  },
336
396
  }),
@@ -346,13 +406,16 @@ export function createYouTrackTools(operations: YouTrackOperations = defaultOper
346
406
  const rejected = requireConfirmed(input.confirmed);
347
407
  if (rejected) return rejected;
348
408
  let token = "";
349
- try { token = credentials().token; } catch (error) {
409
+ try {
410
+ token = credentials().token;
411
+ } catch (error) {
350
412
  const gap = configGap();
351
413
  if (gap) return gap;
352
414
  return output(fail(message(error)));
353
415
  }
354
416
  const result = await withWriteFlag(() =>
355
- postUpdate({ ...input, workspace_root: context.directory }, operations));
417
+ postUpdate({ ...input, workspace_root: context.directory }, operations),
418
+ );
356
419
  return output(result.ok ? result : { ...result, error: redact(result.error, token) });
357
420
  },
358
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