@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.
- package/package.json +3 -7
- package/scripts/doctor-check.ts +20 -0
- package/scripts/install-cursor-plugin.sh +51 -28
- package/scripts/install-opencode-plugin.sh +19 -21
- package/scripts/rewrite-workspace-deps.ts +15 -9
- package/scripts/sync-runtime.sh +71 -19
- package/scripts/vendor-assets.ts +37 -0
- package/skills/wk-implement/SKILL.md +2 -2
- package/skills/wk-pr/SKILL.md +1 -1
- package/src/core/boundary.ts +27 -0
- package/src/core/branch-policy.ts +63 -0
- package/src/core/branch.ts +30 -16
- package/src/core/config.ts +193 -31
- package/src/core/docs-layout.ts +251 -0
- package/src/core/docs-migration.ts +639 -0
- package/src/core/docs-repo.ts +11 -9
- package/src/core/docs-validate.ts +18 -6
- package/src/core/doctor.ts +801 -0
- package/src/core/flow-state.ts +1579 -141
- package/src/core/git.ts +22 -5
- package/src/{tools/handoff.ts → core/handoff-tools.ts} +5 -57
- package/src/core/hygiene.ts +26 -12
- package/src/core/init.ts +43 -11
- package/src/core/logger.ts +321 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/ports/init-toolkit-status.ts +1 -1
- package/src/core/ports/vcs-verify-token.ts +1 -1
- package/src/core/ports/youtrack-api.ts +1 -1
- package/src/core/ports/youtrack-verify-token.ts +1 -1
- package/src/core/pr-create.ts +116 -21
- package/src/core/registration.ts +215 -0
- package/src/core/repo-context.ts +447 -0
- package/src/core/repo-tools.ts +23 -0
- package/src/core/safe-write.ts +22 -0
- package/src/core/scripts.ts +3 -44
- package/src/core/sdd.ts +45 -28
- package/src/core/setup-state.ts +54 -0
- package/src/core/setup.ts +1216 -0
- package/src/core/skill-manifests.ts +95 -0
- package/src/core/support-matrix.ts +12 -0
- package/src/core/sync-runtime.ts +348 -0
- package/src/core/templates.ts +2 -2
- package/src/core/vcs-config.ts +107 -37
- package/src/core/verify-project.ts +181 -0
- package/src/core/workspaces.ts +136 -17
- package/src/core/youtrack-tools.ts +228 -0
- package/src/core/youtrack.ts +125 -67
- package/templates/execution-contract.md +9 -7
- package/templates/superpowers-doc-contract.md +4 -3
- package/scripts/_shared/common.sh +0 -173
- package/scripts/changelog-context.sh +0 -42
- package/scripts/docs-refresh-context.sh +0 -40
- package/scripts/init/apply.sh +0 -5
- package/scripts/init/status.sh +0 -5
- package/scripts/init/toolkit-status.sh +0 -5
- package/scripts/pr-create.sh +0 -5
- package/scripts/pr-ready-context.sh +0 -88
- package/scripts/present/ascii-wireframe.sh +0 -5
- package/scripts/present/flow-diagram.sh +0 -5
- package/scripts/release-notes-context.sh +0 -40
- package/scripts/vcs/config.sh +0 -5
- package/scripts/vcs/merged-style.sh +0 -5
- package/scripts/vcs/token-create-urls.sh +0 -5
- package/scripts/vcs/verify-token.sh +0 -5
- package/scripts/verify-project.sh +0 -140
- package/scripts/youtrack/api.sh +0 -5
- package/scripts/youtrack/config.sh +0 -5
- package/scripts/youtrack/greeting.sh +0 -5
- package/scripts/youtrack/parse-duration.sh +0 -5
- package/scripts/youtrack/token-create-url.sh +0 -5
- package/scripts/youtrack/verify-token.sh +0 -5
- package/scripts/youtrack/work-date-ms.sh +0 -5
- package/src/tools/docs-repo.ts +0 -51
- package/src/tools/flow.ts +0 -99
- package/src/tools/index.ts +0 -22
- package/src/tools/present.ts +0 -49
- package/src/tools/repo.ts +0 -490
- package/src/tools/rules.ts +0 -30
- package/src/tools/sdd.ts +0 -216
- package/src/tools/templates.ts +0 -27
- 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
|
+
}
|
package/src/core/youtrack.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
31
|
-
return { config, path: cfgPath };
|
|
35
|
+
parsed = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
32
36
|
} catch {
|
|
33
|
-
return { error:
|
|
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():
|
|
53
|
+
export function youTrackConfigLoad(): YouTrackConfigResult {
|
|
39
54
|
const loaded = readYouTrackConfig(true);
|
|
40
|
-
if ("error" in 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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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 =
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
|
|
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 ===
|
|
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 =
|
|
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
|
-
|
|
337
|
-
|
|
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(
|
|
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
|
-
-
|
|
13
|
-
- Use native `todowrite` for visible task state as well as the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|