@brainervirus/workit-core 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/package.json +3 -7
  2. package/scripts/doctor-check.ts +20 -0
  3. package/scripts/install-cursor-plugin.sh +51 -28
  4. package/scripts/install-opencode-plugin.sh +19 -21
  5. package/scripts/rewrite-workspace-deps.ts +15 -9
  6. package/scripts/sync-runtime.sh +71 -19
  7. package/scripts/vendor-assets.ts +37 -0
  8. package/skills/wk-implement/SKILL.md +2 -2
  9. package/skills/wk-pr/SKILL.md +1 -1
  10. package/src/core/boundary.ts +27 -0
  11. package/src/core/branch-policy.ts +63 -0
  12. package/src/core/branch.ts +30 -16
  13. package/src/core/config.ts +193 -31
  14. package/src/core/docs-layout.ts +251 -0
  15. package/src/core/docs-migration.ts +639 -0
  16. package/src/core/docs-repo.ts +11 -9
  17. package/src/core/docs-validate.ts +18 -6
  18. package/src/core/doctor.ts +801 -0
  19. package/src/core/flow-state.ts +1579 -141
  20. package/src/core/git.ts +22 -5
  21. package/src/{tools/handoff.ts → core/handoff-tools.ts} +5 -57
  22. package/src/core/hygiene.ts +26 -12
  23. package/src/core/init.ts +43 -11
  24. package/src/core/logger.ts +321 -0
  25. package/src/core/package-root.ts +28 -0
  26. package/src/core/ports/init-toolkit-status.ts +1 -1
  27. package/src/core/ports/vcs-verify-token.ts +1 -1
  28. package/src/core/ports/youtrack-api.ts +1 -1
  29. package/src/core/ports/youtrack-verify-token.ts +1 -1
  30. package/src/core/pr-create.ts +116 -21
  31. package/src/core/registration.ts +215 -0
  32. package/src/core/repo-context.ts +447 -0
  33. package/src/core/repo-tools.ts +23 -0
  34. package/src/core/safe-write.ts +22 -0
  35. package/src/core/scripts.ts +3 -44
  36. package/src/core/sdd.ts +45 -28
  37. package/src/core/setup-state.ts +54 -0
  38. package/src/core/setup.ts +1216 -0
  39. package/src/core/skill-manifests.ts +95 -0
  40. package/src/core/support-matrix.ts +12 -0
  41. package/src/core/sync-runtime.ts +348 -0
  42. package/src/core/templates.ts +2 -2
  43. package/src/core/vcs-config.ts +107 -37
  44. package/src/core/verify-project.ts +181 -0
  45. package/src/core/workspaces.ts +136 -17
  46. package/src/core/youtrack-tools.ts +228 -0
  47. package/src/core/youtrack.ts +125 -67
  48. package/templates/execution-contract.md +9 -7
  49. package/templates/superpowers-doc-contract.md +4 -3
  50. package/scripts/_shared/common.sh +0 -173
  51. package/scripts/changelog-context.sh +0 -42
  52. package/scripts/docs-refresh-context.sh +0 -40
  53. package/scripts/init/apply.sh +0 -5
  54. package/scripts/init/status.sh +0 -5
  55. package/scripts/init/toolkit-status.sh +0 -5
  56. package/scripts/pr-create.sh +0 -5
  57. package/scripts/pr-ready-context.sh +0 -88
  58. package/scripts/present/ascii-wireframe.sh +0 -5
  59. package/scripts/present/flow-diagram.sh +0 -5
  60. package/scripts/release-notes-context.sh +0 -40
  61. package/scripts/vcs/config.sh +0 -5
  62. package/scripts/vcs/merged-style.sh +0 -5
  63. package/scripts/vcs/token-create-urls.sh +0 -5
  64. package/scripts/vcs/verify-token.sh +0 -5
  65. package/scripts/verify-project.sh +0 -140
  66. package/scripts/youtrack/api.sh +0 -5
  67. package/scripts/youtrack/config.sh +0 -5
  68. package/scripts/youtrack/greeting.sh +0 -5
  69. package/scripts/youtrack/parse-duration.sh +0 -5
  70. package/scripts/youtrack/token-create-url.sh +0 -5
  71. package/scripts/youtrack/verify-token.sh +0 -5
  72. package/scripts/youtrack/work-date-ms.sh +0 -5
  73. package/src/tools/docs-repo.ts +0 -51
  74. package/src/tools/flow.ts +0 -99
  75. package/src/tools/index.ts +0 -22
  76. package/src/tools/present.ts +0 -49
  77. package/src/tools/repo.ts +0 -490
  78. package/src/tools/rules.ts +0 -30
  79. package/src/tools/sdd.ts +0 -216
  80. package/src/tools/templates.ts +0 -27
  81. package/src/tools/youtrack.ts +0 -423
@@ -0,0 +1,228 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fail, ok, type Result } from "../core";
5
+ import { configDir, isConfigObject } from "./config";
6
+ import {
7
+ context as legacyContext,
8
+ logTime as legacyLogTime,
9
+ parseDuration as legacyParseDuration,
10
+ postUpdate as legacyPostUpdate,
11
+ verifyYouTrackToken,
12
+ } from "./youtrack";
13
+
14
+ export const ISSUE_RE = /^[A-Z]+-\d+$/;
15
+ export const message = (error: unknown) => (error instanceof Error ? error.message : String(error));
16
+
17
+ // Both override names point at the config dir itself, same precedence as
18
+ // src/core/config.ts and scripts/init/status.sh: WORKFLOW_TOOLKIT_CONFIG → WORKFLOW_TOOLKIT_CONFIG_DIR → XDG.
19
+ // Default env (no args) routes through configDir() so the legacy migration runs.
20
+ export const configPath = (env: NodeJS.ProcessEnv = process.env, home = os.homedir()) =>
21
+ path.join(
22
+ env === process.env
23
+ ? configDir()
24
+ : (env.WORKFLOW_TOOLKIT_CONFIG ??
25
+ env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
26
+ path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "workit")),
27
+ "youtrack.json",
28
+ );
29
+
30
+ export function readCredentials(env: NodeJS.ProcessEnv = process.env, home = os.homedir()) {
31
+ const resolvedConfig = configPath(env, home);
32
+ // AR-07/CA-37: a parseable non-object youtrack.json is malformed — exact-path
33
+ // error, never a raw TypeError or a silent tokenFile default.
34
+ let parsed: unknown;
35
+ try {
36
+ parsed = JSON.parse(readFileSync(resolvedConfig, "utf8"));
37
+ } catch {
38
+ throw new Error(`${resolvedConfig} is not valid JSON`);
39
+ }
40
+ if (!isConfigObject(parsed)) {
41
+ throw new Error(`${resolvedConfig} is not a JSON object`);
42
+ }
43
+ const config = parsed as { tokenFile?: string };
44
+ const tokenFile = config.tokenFile ?? "youtrack.token";
45
+ const tokenPath = path.resolve(path.dirname(resolvedConfig), tokenFile.replace(/^~(?=\/)/, home));
46
+ if (process.platform !== "win32" && (statSync(tokenPath).mode & 0o777) !== 0o600)
47
+ throw new Error("youtrack.token mode must be 0600");
48
+ const token = readFileSync(tokenPath, "utf8").trim();
49
+ if (!token) throw new Error("youtrack.token is empty");
50
+ return { configPath: resolvedConfig, token };
51
+ }
52
+
53
+ export const redact = (text: string, token: string) =>
54
+ token ? text.split(token).join("[REDACTED]") : text;
55
+
56
+ type MaybePromise<T> = T | Promise<T>;
57
+ export type LegacyValue = Record<string, unknown> | void;
58
+ export type NotApplied = { ok: false; error: string; outcome: "not_applied" };
59
+
60
+ export type YouTrackOperations = {
61
+ verifyToken(): MaybePromise<LegacyValue>;
62
+ context(input: Record<string, unknown>): MaybePromise<LegacyValue>;
63
+ parseDuration(text: string, workspaceRoot: string): MaybePromise<LegacyValue>;
64
+ postComment(issueId: string, markdown: string, workspaceRoot?: string): MaybePromise<LegacyValue>;
65
+ logTime(input: Record<string, unknown>): MaybePromise<LegacyValue>;
66
+ };
67
+
68
+ export const unwrap = (value: LegacyValue) => {
69
+ if (!value) return {};
70
+ if (value.error) throw new Error(String(value.error));
71
+ if (value.ok === false) throw new Error(String(value.error ?? "YouTrack operation failed"));
72
+ return (value.data as Record<string, unknown> | undefined) ?? value;
73
+ };
74
+
75
+ export const defaultOperations: YouTrackOperations = {
76
+ verifyToken: () => unwrap(verifyYouTrackToken()),
77
+ context: (input) => legacyContext(input as never),
78
+ parseDuration: (text, workspaceRoot) => legacyParseDuration(text, workspaceRoot),
79
+ postComment: async (issueId, markdown, workspaceRoot) =>
80
+ unwrap(
81
+ await legacyPostUpdate({
82
+ confirmed: true,
83
+ issueId,
84
+ markdown,
85
+ workspace_root: workspaceRoot,
86
+ } as never),
87
+ ),
88
+ logTime: async (input) => unwrap(await legacyLogTime(input as never)),
89
+ };
90
+
91
+ type PostInput = {
92
+ confirmed: boolean;
93
+ issueId: string;
94
+ markdown: string;
95
+ minutes?: number;
96
+ workspace_root?: string;
97
+ };
98
+
99
+ type PostData = {
100
+ issueId: string;
101
+ postedComment: boolean;
102
+ loggedMinutes: number;
103
+ outcome?: "unknown" | "not_applied";
104
+ instructions?: string;
105
+ retry?: "workflow_youtrack_post" | "workflow_youtrack_log_time";
106
+ };
107
+
108
+ const notApplied = (value: LegacyValue): value is NotApplied =>
109
+ value?.ok === false && value.outcome === "not_applied";
110
+
111
+ export async function postUpdate(
112
+ input: PostInput,
113
+ operations: Pick<YouTrackOperations, "postComment" | "logTime"> = defaultOperations,
114
+ ): Promise<Result<PostData>> {
115
+ if (input.confirmed !== true) return fail("confirmed: true required");
116
+ if (!ISSUE_RE.test(input.issueId)) return fail("invalid issueId");
117
+ if (!input.markdown?.trim()) return fail("markdown required");
118
+ if (input.minutes != null && input.minutes <= 0) return fail("minutes must be positive");
119
+
120
+ try {
121
+ const comment = await operations.postComment(
122
+ input.issueId,
123
+ input.markdown,
124
+ input.workspace_root,
125
+ );
126
+ if (notApplied(comment))
127
+ return fail(comment.error, {
128
+ issueId: input.issueId,
129
+ postedComment: false,
130
+ loggedMinutes: 0,
131
+ outcome: "not_applied",
132
+ retry: "workflow_youtrack_post",
133
+ });
134
+ unwrap(comment);
135
+ } catch (error) {
136
+ return fail(message(error), {
137
+ issueId: input.issueId,
138
+ postedComment: false,
139
+ loggedMinutes: 0,
140
+ outcome: "unknown",
141
+ instructions: "Check YouTrack comments manually; do not retry while the outcome is unknown.",
142
+ });
143
+ }
144
+
145
+ if (input.minutes != null) {
146
+ try {
147
+ const time = await operations.logTime({
148
+ issueId: input.issueId,
149
+ minutes: input.minutes,
150
+ text: "workit update",
151
+ workspace_root: input.workspace_root,
152
+ });
153
+ if (notApplied(time))
154
+ return fail(time.error, {
155
+ issueId: input.issueId,
156
+ postedComment: true,
157
+ loggedMinutes: 0,
158
+ outcome: "not_applied",
159
+ retry: "workflow_youtrack_log_time",
160
+ });
161
+ unwrap(time);
162
+ } catch (error) {
163
+ return fail(message(error), {
164
+ issueId: input.issueId,
165
+ postedComment: true,
166
+ loggedMinutes: 0,
167
+ outcome: "unknown",
168
+ instructions:
169
+ "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
170
+ });
171
+ }
172
+ }
173
+
174
+ return ok({
175
+ issueId: input.issueId,
176
+ postedComment: true,
177
+ loggedMinutes: input.minutes ?? 0,
178
+ });
179
+ }
180
+
181
+ export async function logTimeUpdate(
182
+ input: Record<string, unknown>,
183
+ operation: Pick<YouTrackOperations, "logTime"> = defaultOperations,
184
+ ): Promise<Result<Record<string, unknown>>> {
185
+ try {
186
+ const value = await operation.logTime(input);
187
+ if (notApplied(value))
188
+ return fail(value.error, {
189
+ issueId: input.issueId,
190
+ loggedMinutes: 0,
191
+ outcome: "not_applied",
192
+ retry: "workflow_youtrack_log_time",
193
+ });
194
+ return ok(unwrap(value));
195
+ } catch (error) {
196
+ return fail(message(error), {
197
+ issueId: input.issueId,
198
+ loggedMinutes: 0,
199
+ outcome: "unknown",
200
+ instructions:
201
+ "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
202
+ });
203
+ }
204
+ }
205
+
206
+ export function normalizeContext(value: LegacyValue, mode?: string): LegacyValue {
207
+ if (!value || mode !== "meetings") return value;
208
+ const config = value.config as Record<string, unknown> | undefined;
209
+ const issue = String(config?.meetingIssue || "IRPT-12");
210
+ const { meetingIssues: _meetingIssues, ...singleMeetingConfig } = config ?? {};
211
+ const options = Array.isArray(value.meetingOptions)
212
+ ? (value.meetingOptions as Array<Record<string, unknown>>)
213
+ : [];
214
+ const selected = options.find((option) => option.issue === issue) ?? {
215
+ key: "general",
216
+ issue,
217
+ label: issue,
218
+ workItemText: "Reuniones",
219
+ };
220
+ return {
221
+ ...value,
222
+ config: singleMeetingConfig,
223
+ meetingOptions: [selected],
224
+ requiresMeetingChoice: false,
225
+ issueId: issue,
226
+ workItemText: selected.workItemText ?? "Reuniones",
227
+ };
228
+ }
@@ -1,9 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
3
  import { readTemplate } from "./templates";
5
4
  import { resolveWorkspaceRoot } from "./scripts";
6
- import { configDir } from "./config";
5
+ import { configDir, isConfigObject } from "./config";
7
6
 
8
7
  const ISSUE_RE = /^[A-Z]+-\d+$/;
9
8
  const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
@@ -19,25 +18,45 @@ const youTrackTokenModeOk = (p: string): boolean => {
19
18
  return mode === 0o600;
20
19
  };
21
20
 
21
+ // AR-07/CA-37: the shared shape rule applies here too — a parseable non-object
22
+ // youtrack.json (null, scalar, array) is malformed with the exact path, never
23
+ // missing/unconfigured, never silently defaulted.
22
24
  function readYouTrackConfig(
23
25
  required: boolean,
24
- ): { config: Record<string, any>; path: string } | { error: string } {
26
+ ): { config: Record<string, any>; path: string } | { error: string; path: string } {
25
27
  const cfgPath = youTrackConfigPath();
26
28
  if (!fs.existsSync(cfgPath)) {
27
- return required ? { error: "ERROR: missing youtrack.json" } : { config: {}, path: cfgPath };
29
+ return required
30
+ ? { error: "ERROR: missing youtrack.json", path: cfgPath }
31
+ : { config: {}, path: cfgPath };
28
32
  }
33
+ let parsed: unknown;
29
34
  try {
30
- const config = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record<string, any>;
31
- return { config, path: cfgPath };
35
+ parsed = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
32
36
  } catch {
33
- return { error: "invalid youtrack.json" };
37
+ return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
34
38
  }
39
+ if (!isConfigObject(parsed)) {
40
+ return { error: `${cfgPath} is not a JSON object`, path: cfgPath };
41
+ }
42
+ return { config: parsed as Record<string, any>, path: cfgPath };
35
43
  }
36
44
 
45
+ // RL-01: typed load result — malformed carries {ok:false, error, configPath}
46
+ // mirroring vcsConfig, so risky consumers stop on the exact path.
47
+ export type YouTrackConfigResult =
48
+ | { data: Record<string, any> }
49
+ | { error: string }
50
+ | { ok: false; error: string; configPath: string };
51
+
37
52
  /** Load + redact youtrack.json; validates the token file like youtrack/config.sh load. */
38
- export function youTrackConfigLoad(): { data: Record<string, any> } | { error: string } {
53
+ export function youTrackConfigLoad(): YouTrackConfigResult {
39
54
  const loaded = readYouTrackConfig(true);
40
- if ("error" in loaded) return loaded;
55
+ if ("error" in loaded) {
56
+ // RL-01: malformed (parse failure or non-object) fails closed with the exact
57
+ // path, mirroring vcsConfig's {ok:false,error,configPath} shape.
58
+ return { ok: false, error: loaded.error, configPath: loaded.path };
59
+ }
41
60
  const cfgPath = loaded.path;
42
61
  const tokenFile = String(loaded.config.tokenFile ?? "");
43
62
  const tokenPath = tokenFile
@@ -87,8 +106,23 @@ export function youTrackGreeting(configOverride?: string): {
87
106
  stderr: string;
88
107
  } {
89
108
  const cfgPath = configOverride ?? youTrackConfigPath();
109
+ let parsed: unknown;
110
+ try {
111
+ parsed = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
112
+ } catch {
113
+ return {
114
+ stdout: "",
115
+ exitCode: 1,
116
+ stderr: fs.existsSync(cfgPath)
117
+ ? `${cfgPath} is not valid JSON`
118
+ : `missing youtrack.json: ${cfgPath}`,
119
+ };
120
+ }
121
+ if (!isConfigObject(parsed)) {
122
+ return { stdout: "", exitCode: 1, stderr: `${cfgPath} is not a JSON object` };
123
+ }
124
+ const config = parsed as Record<string, any>;
90
125
  try {
91
- const config = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record<string, any>;
92
126
  const tz = String(config.timezone ?? "America/Santiago");
93
127
  const now = new Date();
94
128
  const { y, m, d, hour, minute } = tzParts(now, tz);
@@ -134,11 +168,20 @@ export function youTrackWorkDateMs(
134
168
  ): { data: { dateMs: number; timezone: string; localDate: string } } | { error: string } {
135
169
  const cfgPath = youTrackConfigPath();
136
170
  let tz = "America/Santiago";
137
- try {
138
- const config = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record<string, any>;
139
- tz = String(config.timezone ?? "America/Santiago");
140
- } catch {
141
- /* defaults */
171
+ // Missing file is a legitimate unconfigured state (reader: "missing" keeps
172
+ // defaults); a parseable non-object is malformed and must propagate the
173
+ // exact-path error instead of silently defaulting the timezone.
174
+ if (fs.existsSync(cfgPath)) {
175
+ let parsed: unknown;
176
+ try {
177
+ parsed = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
178
+ } catch {
179
+ return { error: `${cfgPath} is not valid JSON` };
180
+ }
181
+ if (!isConfigObject(parsed)) {
182
+ return { error: `${cfgPath} is not a JSON object` };
183
+ }
184
+ tz = String((parsed as Record<string, any>).timezone ?? "America/Santiago");
142
185
  }
143
186
  const raw = dateRaw || "auto";
144
187
  try {
@@ -192,16 +235,38 @@ const youTrackToken = (): { token: string; base: string } | { error: string } =>
192
235
  return { token, base };
193
236
  };
194
237
 
195
- function youTrackCurl(args: string[]): { status: number; stdout: string; stderr: string } {
196
- const result = spawnSync("curl", ["-fsS", ...args], { encoding: "utf8" });
197
- return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
238
+ // fetch replaces the previous curl -fsS request helper: check res.ok,
239
+ // surface HTTP errors without a token-bearing body, parse JSON on success.
240
+ async function youTrackRequest(
241
+ url: string,
242
+ init: { method: string; token: string; body?: unknown },
243
+ ): Promise<{ status: number; stdout: string; stderr: string }> {
244
+ const headers: Record<string, string> = {
245
+ Authorization: `Bearer ${init.token}`,
246
+ Accept: "application/json",
247
+ };
248
+ let body: string | undefined;
249
+ if (init.body !== undefined) {
250
+ headers["Content-Type"] = "application/json";
251
+ body = JSON.stringify(init.body);
252
+ }
253
+ try {
254
+ const res = await fetch(url, { method: init.method, headers, body });
255
+ const text = await res.text();
256
+ if (!res.ok) {
257
+ return { status: res.status, stdout: "", stderr: text.slice(0, 200) };
258
+ }
259
+ return { status: 0, stdout: text, stderr: "" };
260
+ } catch (err) {
261
+ return { status: 1, stdout: "", stderr: err instanceof Error ? err.message : "network error" };
262
+ }
198
263
  }
199
264
 
200
265
  /** Port of scripts/youtrack/api.sh — log-time / post-comment with the WORKFLOW_YT_WRITE guard. */
201
- export function youTrackApi(
266
+ export async function youTrackApi(
202
267
  args: string[],
203
268
  writeFlag = process.env.WORKFLOW_YT_WRITE ?? "",
204
- ): { data: Record<string, any> } | { error: string } {
269
+ ): Promise<{ data: Record<string, any> } | { error: string }> {
205
270
  const cmd = args[0];
206
271
  if (cmd === "log-time" || cmd === "post-comment") {
207
272
  if (writeFlag !== "1") {
@@ -214,22 +279,20 @@ export function youTrackApi(
214
279
  const creds = youTrackToken();
215
280
  if ("error" in creds) return creds;
216
281
  const { token, base } = creds;
217
- const auth = ["-H", `Authorization: Bearer ${token}`, "-H", "Accept: application/json"];
218
282
 
219
283
  if (cmd === "log-time") {
220
284
  const [issue, minutesRaw, text, dateArg] = args.slice(1);
221
285
  const minutes = Number(minutesRaw);
222
286
  const dateMs = youTrackWorkDateMs(dateArg ?? "auto");
223
287
  if ("error" in dateMs) return dateMs;
224
- const body = JSON.stringify({ duration: { minutes }, text, date: dateMs.data.dateMs });
225
- const out = youTrackCurl([
226
- ...auth,
227
- "-H",
228
- "Content-Type: application/json",
229
- "-d",
230
- body,
288
+ const out = await youTrackRequest(
231
289
  `${base}/api/issues/${issue}/timeTracking/workItems?fields=id,idReadable`,
232
- ]);
290
+ {
291
+ method: "POST",
292
+ token,
293
+ body: { duration: { minutes }, text, date: dateMs.data.dateMs },
294
+ },
295
+ );
233
296
  if (out.status !== 0) return { error: "YouTrack HTTP request failed" };
234
297
  try {
235
298
  const created = JSON.parse(out.stdout) as Record<string, any>;
@@ -248,15 +311,11 @@ export function youTrackApi(
248
311
  }
249
312
  if (cmd === "post-comment") {
250
313
  const [issue, text] = args.slice(1);
251
- const body = JSON.stringify({ text });
252
- const out = youTrackCurl([
253
- ...auth,
254
- "-H",
255
- "Content-Type: application/json",
256
- "-d",
257
- body,
258
- `${base}/api/issues/${issue}/comments`,
259
- ]);
314
+ const out = await youTrackRequest(`${base}/api/issues/${issue}/comments`, {
315
+ method: "POST",
316
+ token,
317
+ body: { text },
318
+ });
260
319
  if (out.status !== 0) return { error: "YouTrack HTTP request failed" };
261
320
  return { data: { ok: true, issueId: issue } };
262
321
  }
@@ -264,26 +323,24 @@ export function youTrackApi(
264
323
  }
265
324
 
266
325
  /** Port of scripts/youtrack/verify-token.sh — read-only GET /api/users/me. */
267
- export function youTrackVerifyToken():
268
- | { data: Record<string, any> }
269
- | { error: string; http_status?: number; path?: string } {
326
+ export async function youTrackVerifyToken(): Promise<
327
+ { data: Record<string, any> } | { error: string; http_status?: number; path?: string }
328
+ > {
270
329
  const cfgPath = youTrackConfigPath();
271
330
  if (!fs.existsSync(cfgPath)) return { error: "missing youtrack.json" };
272
331
  const creds = youTrackToken();
273
332
  if ("error" in creds) return { error: creds.error };
274
333
  const { token, base } = creds;
275
334
 
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
- ]);
335
+ const me = await youTrackRequest(`${base}/api/users/me?fields=id,login,name,email`, {
336
+ method: "GET",
337
+ token,
338
+ });
283
339
  if (me.status !== 0) {
284
- const body = me.stderr.trim() || me.stdout.trim();
285
340
  const err =
286
- me.status === 22 ? "authentication failed (401/403)" : `HTTP error: ${body.slice(0, 200)}`;
341
+ me.status === 401 || me.status === 403
342
+ ? "authentication failed (401/403)"
343
+ : `HTTP error: ${me.stderr.slice(0, 200)}`;
287
344
  return { error: err, http_status: me.status };
288
345
  }
289
346
  let user: Record<string, any>;
@@ -304,13 +361,10 @@ export function youTrackVerifyToken():
304
361
  const meeting = readYouTrackConfig(false);
305
362
  const meetingIssue = "config" in meeting ? meeting.config.meetingIssue : undefined;
306
363
  if (meetingIssue) {
307
- const issue = youTrackCurl([
308
- "-H",
309
- `Authorization: Bearer ${token}`,
310
- "-H",
311
- "Accept: application/json",
364
+ const issue = await youTrackRequest(
312
365
  `${base}/api/issues/${meetingIssue}?fields=id,idReadable,summary`,
313
- ]);
366
+ { method: "GET", token },
367
+ );
314
368
  if (issue.status === 0) {
315
369
  try {
316
370
  const parsed = JSON.parse(issue.stdout) as Record<string, any>;
@@ -333,8 +387,10 @@ export function youTrackVerifyToken():
333
387
  export function youTrackTokenCreateUrl(): { data: Record<string, any> } {
334
388
  const tokenName = process.env.WORKFLOW_YT_TOKEN_NAME ?? "workit";
335
389
  const loaded = readYouTrackConfig(false);
336
- const config = loaded && "config" in loaded ? loaded.config : {};
337
- const cfgPath = loaded && "config" in loaded ? loaded.path : youTrackConfigPath();
390
+ if ("error" in loaded) {
391
+ return { data: { error: loaded.error } };
392
+ }
393
+ const config = loaded.config;
338
394
  const defaults = (config.tokenDefaults ?? {}) as Record<string, any>;
339
395
  const name = String(defaults.name ?? tokenName);
340
396
  const desc = String(
@@ -342,7 +398,9 @@ export function youTrackTokenCreateUrl(): { data: Record<string, any> } {
342
398
  );
343
399
  const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
344
400
  const base = String(config.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
345
- const tokenFile = String(config.tokenFile ?? path.join(path.dirname(cfgPath), "youtrack.token"));
401
+ const tokenFile = String(
402
+ config.tokenFile ?? path.join(path.dirname(loaded.path), "youtrack.token"),
403
+ );
346
404
  const tab = String(defaults.profileTab ?? "account-security");
347
405
  const createUrl = `${base}/users/me?${new URLSearchParams({ tab })}`;
348
406
  const docsUrl = "https://www.jetbrains.com/help/youtrack/cloud/manage-permanent-token.html";
@@ -394,7 +452,7 @@ export type YouTrackScripts = {
394
452
  config(): Record<string, any>;
395
453
  greeting(): { stdout: string; exitCode: number; stderr: string };
396
454
  parseDuration(text: string): Record<string, any>;
397
- api(args: string[]): Record<string, any>;
455
+ api(args: string[]): Record<string, any> | Promise<Record<string, any>>;
398
456
  };
399
457
 
400
458
  const defaultScripts: YouTrackScripts = {
@@ -532,7 +590,7 @@ export function parseDuration(
532
590
  return out.data;
533
591
  }
534
592
 
535
- export function logTime(
593
+ export async function logTime(
536
594
  {
537
595
  issueId,
538
596
  minutes,
@@ -549,13 +607,13 @@ export function logTime(
549
607
  workspace_root: string;
550
608
  },
551
609
  scripts: YouTrackScripts = defaultScripts,
552
- ): Record<string, any> {
610
+ ): Promise<Record<string, any>> {
553
611
  if (!issueId || !ISSUE_RE.test(issueId)) return { error: "invalid issueId" };
554
612
  if (!minutes || minutes <= 0) return { error: "minutes must be positive" };
555
613
  const workText = text ?? "workit";
556
614
  const dateArg =
557
615
  dateMs != null ? String(dateMs) : date && /^\d+$/.test(String(date)) ? String(date) : "auto";
558
- const out = scripts.api(["log-time", issueId, String(minutes), workText, dateArg]);
616
+ const out = await scripts.api(["log-time", issueId, String(minutes), workText, dateArg]);
559
617
  if (out.error) return { error: out.error };
560
618
  return { issueId, minutes, text: workText, ...out.data, ok: true };
561
619
  }
@@ -609,7 +667,7 @@ export function buildDraft({
609
667
  return { issueId, markdown };
610
668
  }
611
669
 
612
- export function postUpdate(
670
+ export async function postUpdate(
613
671
  {
614
672
  confirmed,
615
673
  issueId,
@@ -624,7 +682,7 @@ export function postUpdate(
624
682
  workspace_root?: string;
625
683
  },
626
684
  operations?: Record<string, any>,
627
- ): Record<string, any> {
685
+ ): Promise<Record<string, any>> {
628
686
  operations ??= {};
629
687
  if (!confirmed) return { error: "confirmed: true required" };
630
688
  if (!issueId || !ISSUE_RE.test(issueId)) return { error: "invalid issueId" };
@@ -635,11 +693,11 @@ export function postUpdate(
635
693
  ((id: string, text: string, _root: string) =>
636
694
  youTrackApi(["post-comment", id, text], process.env.WORKFLOW_YT_WRITE ?? ""));
637
695
  const logTimeOperation = operations.logTime ?? logTime;
638
- const comment = postComment(issueId, markdown, workspace_root);
696
+ const comment = await postComment(issueId, markdown, workspace_root);
639
697
  if (comment.error) return { error: comment.error };
640
698
 
641
699
  if (minutes && minutes > 0) {
642
- const time = logTimeOperation({
700
+ const time = await logTimeOperation({
643
701
  issueId,
644
702
  minutes,
645
703
  text: "workit update",
@@ -9,16 +9,18 @@ Load `using-superpowers`, `subagent-driven-development`, `test-driven-developmen
9
9
 
10
10
  - The parent is coordinator-only: it does not edit product code or perform delegated exploration.
11
11
  - Never use a worktree. Branch changes are in-place through `workflow_branch_setup` on `feature/*` or `bugfix/*`; never commit on protected branches.
12
- - Tracked state, briefs, ledgers, and review diffs live only under `<SDD_DIR>` in `docs/<slug>/sdd/` and use `workflow_sdd_*` tools.
13
- - Use native `todowrite` for visible task state as well as the tracked ledger.
14
- - Use native `question` for branch/stash choices and guarded external mutations; call mutation tools only after approval with `confirmed: true`.
12
+ - Working state, briefs, ledgers, and review diffs live only under gitignored `<SDD_DIR>` in `docs/<slug>/sdd/` and use `workflow_sdd_*` tools.
13
+ - Use native `todowrite` for visible task state as well as the gitignored ledger.
14
+ - Use native `question` for branch/stash choices and guarded external mutations; call mutation tools only after approval with `confirmed: true` (grounded in the recorded NativeChoiceEvidence).
15
+ - Flow-tool confirmations are never agent-typed booleans and never caller-supplied evidence objects: on OpenCode the plugin records the user's native-`question` answer as a host-observed one-use receipt (`attested: true`, `callID`, `selectedLabel`, `recordedAt`) consumed by `workflow_spec_approve` / `workflow_plan_approve` / `workflow_plan_menu` — no evidence argument exists, and delegated worker status comes from host session parentage (`parentID`), never a caller `role` field. On Cursor, confirmations are policy-only (`attested: false`) and subagent-driven execution is rejected as unsupported.
16
+ - On Cursor, for every repository-scoped `workflow_*` call, pass the active Cursor workspace as `workspace_root`; never rely on the MCP process default.
15
17
  - Use native `task` with only the built-in `explore` and `general` agents.
16
18
 
17
19
  ## Flow gates (HARD)
18
20
 
19
21
  - `wk-implement` refuses to run unless the plan is `approved` (flow.json) and the post-plan menu was presented.
20
22
  - `wk-handoff` refuses to run unless both spec and plan are `approved`.
21
- - Sequence is enforced by tools: `workflow_spec_approve` (×2), `workflow_plan_approve` (×2), `workflow_plan_menu` — never skip a step.
23
+ - Sequence is enforced by tools: `workflow_spec_approve`, `workflow_plan_approve`, `workflow_plan_menu` — never skip a step (the spec/plan self-review runs automatically inside the transition; only the final approval asks for your confirmation).
22
24
 
23
25
  ## Setup
24
26
 
@@ -35,16 +37,16 @@ Load `using-superpowers`, `subagent-driven-development`, `test-driven-developmen
35
37
  For each top-level task absent from `completed_task_ids`:
36
38
 
37
39
  1. Mark it `in_progress` with `todowrite`.
38
- 2. Create a tracked brief with `workflow_sdd_task_brief` and `confirmed: true`.
40
+ 2. Create a working-state brief with `workflow_sdd_task_brief` and `confirmed: true`.
39
41
  3. Delegate read-only discovery, when needed, to an `explore` agent. Delegate implementation to a fresh `general` agent. Product changes follow TDD.
40
- 4. Create a tracked diff with `workflow_sdd_review_package` and `confirmed: true`.
42
+ 4. Create a working-state diff with `workflow_sdd_review_package` and `confirmed: true`.
41
43
  5. Delegate spec-compliance review and code-quality review to separate `general` agents.
42
44
  6. **Blocking** findings (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them to `<SDD_DIR>/advisories.md`.
43
45
  7. Append the validated ledger entry with `workflow_sdd_append_progress` and `confirmed: true`; mark the todo completed.
44
46
 
45
47
  ## Final gate
46
48
 
47
- Run a separate full-branch code review, then `workflow_verify`. Present the full `<SDD_DIR>/advisories.md` roll-up once, then use native `question` so the user can choose which advisory items to fix, discuss, or discard. Report exact check results and never infer success. Use `workflow_git_context` for a commit preview and load `wk-commit` through `skill` for an approved commit. If tracked state contains a stash reference, preview reapplication through `question`, then call `workflow_branch_setup` with `confirmed: true` after approval.
49
+ Run a separate full-branch code review, then `workflow_verify`. Present the full `<SDD_DIR>/advisories.md` roll-up once, then use native `question` so the user can choose which advisory items to fix, discuss, or discard. Report exact check results and never infer success. Use `workflow_git_context` for a commit preview and load `wk-commit` through `skill` for an approved commit. If working state contains a stash reference, preview reapplication through `question`, then call `workflow_branch_setup` with `confirmed: true` after approval.
48
50
 
49
51
  ## Task order
50
52
 
@@ -34,8 +34,9 @@ Before writing **Branch:** into a new spec or plan, call `workflow_docs_branch`
34
34
  - Implementation uses `wk-implement` and subagent-driven development, with native `todowrite` and `task`.
35
35
  - Commits use `wk-commit` after its native `question` confirmation.
36
36
  - Continuation uses `wk-handoff`, whose `workflow_handoff_session` creates and seeds the OpenCode session automatically.
37
- - Never use worktrees. Resolve the declared branch with `workflow_resolve_branch`, preview dirty-tree stash choices with `question`, and apply an approved in-place checkout through `workflow_branch_setup` with `confirmed: true`.
38
- - Keep all SDD state tracked under `docs/<slug>/sdd/`; use `workflow_sdd_context` and the registered `workflow_sdd_*` tools.
37
+ - Never use worktrees. Resolve the declared branch with `workflow_resolve_branch`, preview dirty-tree stash choices with `question`, and apply an approved in-place checkout through `workflow_branch_setup` with `confirmed: true` (grounded in the recorded NativeChoiceEvidence).
38
+ - Flow-tool confirmations are never agent-typed booleans and never caller-supplied evidence objects: on OpenCode the plugin records the user's native-`question` answer as a host-observed one-use receipt (`attested: true`, `callID`, `selectedLabel`, `recordedAt`) consumed by `workflow_spec_approve` / `workflow_plan_approve` / `workflow_plan_menu` — no evidence argument exists, and delegated worker status comes from host session parentage (`parentID`), never a caller `role` field. On Cursor, confirmations are policy-only (`attested: false`) and subagent-driven execution is rejected as unsupported.
39
+ - Keep all SDD state under the gitignored `docs/<slug>/sdd/`; use `workflow_sdd_context` and the registered `workflow_sdd_*` tools.
39
40
  - After implementation, use `question` before an approved stash reapply through `workflow_branch_setup` with `confirmed: true`.
40
41
 
41
42
  ## YouTrack content
@@ -46,7 +47,7 @@ Chat follows the user's language. YouTrack task comments are Spanish (`es-CL`) a
46
47
 
47
48
  Before handoff, call `workflow_docs_validate` on the linked spec/plan pair. Hard-fail on any error; never offer execution when validation fails.
48
49
 
49
- Before handoff, verify the saved spec path, plan path, declared branch, top-level task numbering, and tracked SDD directory through the registered read-only workflow tools. Report structured failures; never infer success.
50
+ Before handoff, verify the saved spec path, plan path, declared branch, top-level task numbering, and workflow-managed SDD directory through the registered read-only workflow tools. Report structured failures; never infer success.
50
51
 
51
52
  ## Post-plan execution choice
52
53