@brainervirus/workit-core 0.6.0 → 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 +10 -14
- package/scripts/doctor-check.ts +20 -0
- package/scripts/install-cursor-plugin.sh +51 -28
- package/scripts/install-opencode-plugin.sh +25 -22
- 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 +166 -57
- package/src/core/changelog.ts +17 -14
- package/src/core/config-guard.ts +9 -2
- package/src/core/config.ts +227 -32
- package/src/core/detector.ts +22 -11
- package/src/core/docs-layout.ts +251 -0
- package/src/core/docs-migration.ts +639 -0
- package/src/core/docs-repo.ts +58 -21
- package/src/core/docs-validate.ts +181 -43
- package/src/core/doctor.ts +801 -0
- package/src/core/flow-state.ts +1579 -137
- package/src/core/git.ts +22 -5
- package/src/core/gitignore.ts +11 -2
- package/src/core/handoff-context.ts +18 -5
- package/src/{tools/handoff.ts → core/handoff-tools.ts} +16 -63
- package/src/core/hygiene.ts +53 -17
- package/src/core/init.ts +128 -31
- package/src/core/logger.ts +321 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/parse-sections.ts +2 -2
- package/src/core/plan-tasks.ts +13 -3
- 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 +4 -2
- package/src/core/ports/youtrack-config.ts +1 -3
- package/src/core/ports/youtrack-verify-token.ts +1 -1
- package/src/core/pr-create.ts +156 -29
- package/src/core/present.ts +11 -2
- package/src/core/registration.ts +215 -0
- package/src/core/reminder.ts +1 -2
- package/src/core/repo-context.ts +447 -0
- package/src/core/repo-tool.ts +4 -1
- package/src/core/repo-tools.ts +23 -0
- package/src/core/rules.ts +10 -7
- package/src/core/safe-write.ts +22 -0
- package/src/core/scripts.ts +3 -39
- package/src/core/sdd.ts +56 -31
- 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 +16 -6
- package/src/core/vcs-config.ts +191 -62
- package/src/core/verify-parse.ts +4 -2
- 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 +320 -87
- package/src/core.ts +18 -3
- package/templates/execution-contract.md +9 -7
- package/templates/superpowers-doc-contract.md +5 -4
- package/scripts/_shared/common.sh +0 -158
- 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 -42
- package/src/tools/flow.ts +0 -88
- package/src/tools/index.ts +0 -22
- package/src/tools/present.ts +0 -45
- package/src/tools/repo.ts +0 -357
- package/src/tools/rules.ts +0 -30
- package/src/tools/sdd.ts +0 -189
- package/src/tools/templates.ts +0 -27
- package/src/tools/youtrack.ts +0 -360
package/src/core/plan-tasks.ts
CHANGED
|
@@ -4,13 +4,19 @@ import { parseTasksFromPlan } from "./docs-validate";
|
|
|
4
4
|
import { resolveBranch } from "./branch";
|
|
5
5
|
|
|
6
6
|
const readSafe = (p: string): string | null => {
|
|
7
|
-
try {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(p, "utf8");
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
8
12
|
};
|
|
9
13
|
|
|
10
14
|
export function parsePlanTasks(
|
|
11
15
|
planPath: string,
|
|
12
16
|
workspaceRoot: string,
|
|
13
|
-
):
|
|
17
|
+
):
|
|
18
|
+
| { task_count: number; tasks: { id: number; title: string; section_text: string }[] }
|
|
19
|
+
| { error: string } {
|
|
14
20
|
const cwd = path.resolve(workspaceRoot);
|
|
15
21
|
const resolved = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
|
|
16
22
|
const text = readSafe(resolved);
|
|
@@ -27,7 +33,11 @@ export function resolveHandoffBranch(
|
|
|
27
33
|
planPath: string,
|
|
28
34
|
workspaceRoot: string,
|
|
29
35
|
): { branch: string } | { error: string } {
|
|
30
|
-
const resolved = resolveBranch({
|
|
36
|
+
const resolved = resolveBranch({
|
|
37
|
+
spec_path: specPath,
|
|
38
|
+
plan_path: planPath,
|
|
39
|
+
workspace_root: workspaceRoot,
|
|
40
|
+
});
|
|
31
41
|
if ("error" in resolved) return { error: resolved.error as string };
|
|
32
42
|
return { branch: resolved.branch };
|
|
33
43
|
}
|
|
@@ -5,11 +5,13 @@ const args = process.argv.slice(2);
|
|
|
5
5
|
const cmd = args[0];
|
|
6
6
|
if (cmd === "log-time" || cmd === "post-comment") {
|
|
7
7
|
if ((process.env.WORKFLOW_YT_WRITE ?? "") !== "1") {
|
|
8
|
-
console.error(
|
|
8
|
+
console.error(
|
|
9
|
+
"ERROR: YouTrack write operations require WORKFLOW_YT_WRITE=1 (refusing to mutate production)",
|
|
10
|
+
);
|
|
9
11
|
process.exit(1);
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
|
-
const out = youTrackApi(args, process.env.WORKFLOW_YT_WRITE ?? "");
|
|
14
|
+
const out = await youTrackApi(args, process.env.WORKFLOW_YT_WRITE ?? "");
|
|
13
15
|
if ("error" in out) {
|
|
14
16
|
console.log(JSON.stringify({ ok: false, error: out.error }));
|
|
15
17
|
process.exit(1);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// CLI port of scripts/youtrack/verify-token.sh.
|
|
2
2
|
import { youTrackVerifyToken } from "../youtrack";
|
|
3
3
|
|
|
4
|
-
const out = youTrackVerifyToken();
|
|
4
|
+
const out = await youTrackVerifyToken();
|
|
5
5
|
if ("error" in out) {
|
|
6
6
|
const payload: Record<string, any> = { ok: false, error: out.error };
|
|
7
7
|
if (out.http_status !== undefined) payload.http_status = out.http_status;
|
package/src/core/pr-create.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { vcsConfig } from "./vcs-config";
|
|
5
|
+
import { resolveBranchPolicyFor } from "./branch";
|
|
5
6
|
|
|
6
7
|
// Port of scripts/pr-create.sh — build MR/PR body issue linking + create via glab/gh.
|
|
7
8
|
|
|
@@ -19,6 +20,23 @@ function parseGhIssue(value: string): string {
|
|
|
19
20
|
return m ? m[1] : String(value).trim().replace(/^#/, "");
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// RL-03/CA-25/AR-08: a branch-derived numeric issue id must be a bare number at
|
|
24
|
+
// a segment or dash boundary — and never part of a date segment. Year-first
|
|
25
|
+
// (feature/2024-01-15/x) and day-first (feature/15-01-2024/x) dates are both
|
|
26
|
+
// skipped — a complete date anywhere in a segment (release-2024-01-15,
|
|
27
|
+
// v2-2024-01-15-fix) — so no date digit ever closes an issue. Deliberate
|
|
28
|
+
// numeric issue branches (feature/42-title, feature/2024-fix) keep linking.
|
|
29
|
+
function deriveGhIssueFromBranch(branch: string): string {
|
|
30
|
+
for (const segment of branch.split("/")) {
|
|
31
|
+
if (/^\d{4}-\d/.test(segment)) continue; // year-first date-like segment (incl. year-month)
|
|
32
|
+
if (/\d{4}-\d{1,2}-\d{1,2}/.test(segment)) continue; // complete year-first date anywhere
|
|
33
|
+
if (/\d{1,2}-\d{1,2}-\d{4}/.test(segment)) continue; // complete day-first date anywhere
|
|
34
|
+
const m = /(?:^|-)(\d+)(?:-|$)/.exec(segment);
|
|
35
|
+
if (m) return m[1];
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
function buildBody(
|
|
23
41
|
body: string,
|
|
24
42
|
branch: string,
|
|
@@ -44,9 +62,7 @@ function buildBody(
|
|
|
44
62
|
if (!issue && branch) {
|
|
45
63
|
// pure-number issue id (feature/42-title -> 42); digits must be followed by a dash or end-of-string
|
|
46
64
|
// so version tokens (release/1.2.3, backport/8.0.1, lodash-4.17.21, 2024.1) never link
|
|
47
|
-
|
|
48
|
-
const m = /(?:^|\/|-)(\d+)(?:-|$)/.exec(branch);
|
|
49
|
-
if (m) issue = m[1];
|
|
65
|
+
issue = deriveGhIssueFromBranch(branch);
|
|
50
66
|
}
|
|
51
67
|
if (issue) {
|
|
52
68
|
if (ghRelation === "related") {
|
|
@@ -61,17 +77,25 @@ function buildBody(
|
|
|
61
77
|
return body ? `${body}\n\n${line}` : line;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
const truthy = (v: string | undefined): boolean =>
|
|
80
|
+
const truthy = (v: string | undefined): boolean =>
|
|
81
|
+
["1", "true", "yes"].includes(String(v ?? "").toLowerCase());
|
|
65
82
|
|
|
66
83
|
// Port of python's shutil.which — scan PATH in-process (no `which` binary needed).
|
|
67
84
|
function whichOnPath(tool: string): string | null {
|
|
68
|
-
|
|
85
|
+
// win32 CLIs/stubs carry .exe/.cmd suffixes (gh.exe, gh.cmd), so probe them
|
|
86
|
+
// too — accessSync with the bare name would never find them.
|
|
87
|
+
const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
|
|
88
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
69
89
|
if (!dir) continue;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
const candidate = path.join(dir, name);
|
|
92
|
+
try {
|
|
93
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
94
|
+
return candidate;
|
|
95
|
+
} catch {
|
|
96
|
+
/* keep scanning */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
75
99
|
}
|
|
76
100
|
return null;
|
|
77
101
|
}
|
|
@@ -105,14 +129,96 @@ export function prBuildBody(env: NodeJS.ProcessEnv, cwd?: string): string {
|
|
|
105
129
|
/** Port of scripts/pr-create.sh create mode — glab/gh MR/PR creation. */
|
|
106
130
|
export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, any> {
|
|
107
131
|
const root = process.env.WORKFLOW_WORKSPACE_ROOT ?? repoRoot(cwd);
|
|
132
|
+
const policy = resolveBranchPolicyFor(root);
|
|
108
133
|
const cfg = vcsConfig("load", root);
|
|
109
134
|
if (!cfg.ok) return { error: cfg.error ?? "vcs config missing" };
|
|
110
|
-
|
|
135
|
+
// Merge mode finishes the feature locally (git merge + push) — no glab/gh,
|
|
136
|
+
// no API token, so token readiness is only required for the PR path.
|
|
137
|
+
if (policy.integration !== "merge" && !cfg.tokenReady)
|
|
138
|
+
return { error: "VCS token not ready — run /wk-init and edit token file locally" };
|
|
111
139
|
|
|
112
140
|
const provider = cfg.provider as string;
|
|
113
|
-
if (provider !== "gitlab" && provider !== "github")
|
|
141
|
+
if (provider !== "gitlab" && provider !== "github")
|
|
142
|
+
return { error: `unsupported provider: ${provider}` };
|
|
143
|
+
|
|
144
|
+
// B1/RL-03: WF_PR_TARGET is the one deliberate override knob on the create
|
|
145
|
+
// surface (resolvePrBranchContext/docsBranch have none). Unlike the config
|
|
146
|
+
// default — which is authoritative by construction — a caller-supplied
|
|
147
|
+
// target is validated against the resolved branch policy so a PR can never
|
|
148
|
+
// be aimed at a protected or disallowed branch.
|
|
149
|
+
const targetOverride = env.WF_PR_TARGET;
|
|
150
|
+
const target =
|
|
151
|
+
targetOverride || String(cfg.defaultTargetBranch ?? policy.defaultTargetBranch ?? "develop");
|
|
152
|
+
if (targetOverride) {
|
|
153
|
+
const { allowed, protected: protectedTargets } = policy;
|
|
154
|
+
if (protectedTargets.has(targetOverride.toLowerCase()))
|
|
155
|
+
return {
|
|
156
|
+
error: `PR target ${JSON.stringify(targetOverride)} is a protected branch — override must be an allowed non-protected target`,
|
|
157
|
+
};
|
|
158
|
+
if (!allowed.some((r) => r.test(targetOverride)))
|
|
159
|
+
return {
|
|
160
|
+
error: `PR target ${JSON.stringify(targetOverride)} is not allowed by the branch policy`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const title = String(env.WF_PR_TITLE ?? "");
|
|
165
|
+
const br = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
166
|
+
cwd: root,
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
});
|
|
169
|
+
const branch = br.status === 0 ? (br.stdout ?? "").trim() : "";
|
|
170
|
+
|
|
171
|
+
if (policy.integration === "merge") {
|
|
172
|
+
const finish = (): Record<string, any> => {
|
|
173
|
+
const merge = spawnSync("git", ["merge", "--no-ff", branch, "-m", title], {
|
|
174
|
+
cwd: root,
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
});
|
|
177
|
+
if (merge.status !== 0)
|
|
178
|
+
return {
|
|
179
|
+
error: "merge failed",
|
|
180
|
+
mode: "merge",
|
|
181
|
+
targetBranch: target,
|
|
182
|
+
stderr: (merge.stderr ?? "").slice(0, 800),
|
|
183
|
+
};
|
|
184
|
+
const push = spawnSync("git", ["push", "origin", target], { cwd: root, encoding: "utf8" });
|
|
185
|
+
if (push.status !== 0)
|
|
186
|
+
return {
|
|
187
|
+
error: "push failed",
|
|
188
|
+
mode: "merge",
|
|
189
|
+
targetBranch: target,
|
|
190
|
+
stderr: (push.stderr ?? "").slice(0, 800),
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
mode: "merge",
|
|
195
|
+
targetBranch: target,
|
|
196
|
+
merged: true,
|
|
197
|
+
pushed: true,
|
|
198
|
+
output: (push.stdout ?? "").trim(),
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
const co = spawnSync("git", ["checkout", target], { cwd: root, encoding: "utf8" });
|
|
202
|
+
if (co.status !== 0)
|
|
203
|
+
return {
|
|
204
|
+
error: `cannot checkout target ${target}`,
|
|
205
|
+
mode: "merge",
|
|
206
|
+
targetBranch: target,
|
|
207
|
+
stderr: (co.stderr ?? "").slice(0, 800),
|
|
208
|
+
};
|
|
209
|
+
// finish() returns error objects, it never throws, so the best-effort
|
|
210
|
+
// `git checkout branch` restore below runs on ALL outcomes (success,
|
|
211
|
+
// merge failure, and push failure) — the tree always returns to the
|
|
212
|
+
// feature branch. Deliberately not a try/finally: the restore is
|
|
213
|
+
// best-effort and its own failure is not actionable on this path.
|
|
214
|
+
const result = finish();
|
|
215
|
+
spawnSync("git", ["checkout", branch], { cwd: root, encoding: "utf8" }); // best-effort return
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
114
219
|
const cli = provider === "gitlab" ? "glab" : "gh";
|
|
115
|
-
const installUrl =
|
|
220
|
+
const installUrl =
|
|
221
|
+
provider === "gitlab" ? "https://gitlab.com/gitlab-org/cli" : "https://cli.github.com";
|
|
116
222
|
if (whichOnPath(cli) === null) {
|
|
117
223
|
return {
|
|
118
224
|
ok: false,
|
|
@@ -124,21 +230,20 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
124
230
|
|
|
125
231
|
const pr = (cfg.pr ?? {}) as Record<string, any>;
|
|
126
232
|
const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
|
|
127
|
-
const title = String(env.WF_PR_TITLE ?? "");
|
|
128
233
|
const body = env.WF_PR_BODY ?? "";
|
|
129
234
|
const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
|
|
130
|
-
const target = env.WF_PR_TARGET || String(cfg.defaultTargetBranch ?? "develop");
|
|
131
|
-
|
|
132
|
-
const br = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root, encoding: "utf8" });
|
|
133
|
-
const branch = br.status === 0 ? (br.stdout ?? "").trim() : "";
|
|
134
235
|
|
|
135
236
|
let baseUrl = cfg.youtrack_base_url as string | undefined;
|
|
136
237
|
if (!baseUrl) {
|
|
137
|
-
const ytCfg =
|
|
238
|
+
const ytCfg =
|
|
239
|
+
process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
240
|
+
path.join(path.dirname(String(cfg.configPath)), "youtrack.json");
|
|
138
241
|
try {
|
|
139
242
|
const yt = JSON.parse(fs.readFileSync(ytCfg, "utf8")) as Record<string, any>;
|
|
140
243
|
if (yt && typeof yt === "object") baseUrl = yt.baseUrl;
|
|
141
|
-
} catch {
|
|
244
|
+
} catch {
|
|
245
|
+
/* optional */
|
|
246
|
+
}
|
|
142
247
|
}
|
|
143
248
|
const ghLinkOnPr = cfg.issues_provider === "github" && cfg.link_on_pr === true;
|
|
144
249
|
let ghRepo: string | null = null;
|
|
@@ -147,8 +252,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
147
252
|
if (rr.status === 0) ghRepo = parseGhRepo(rr.stdout ?? "");
|
|
148
253
|
}
|
|
149
254
|
const finalBody = buildBody(
|
|
150
|
-
body,
|
|
151
|
-
|
|
255
|
+
body,
|
|
256
|
+
branch,
|
|
257
|
+
cfg.link_issues === true,
|
|
258
|
+
baseUrl ?? "",
|
|
259
|
+
env.WORKFLOW_YT_ISSUE ?? "",
|
|
260
|
+
ghLinkOnPr,
|
|
261
|
+
env.WORKFLOW_GH_ISSUE ?? "",
|
|
262
|
+
env.WORKFLOW_GH_ISSUE_RELATION ?? "closes",
|
|
263
|
+
ghRepo,
|
|
152
264
|
);
|
|
153
265
|
|
|
154
266
|
const squash = pr.squashOnMerge !== false;
|
|
@@ -166,21 +278,30 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
166
278
|
if (draft) cmd.push("--draft");
|
|
167
279
|
if (push) cmd.push("--push");
|
|
168
280
|
if (skipConfirm) cmd.push("--yes");
|
|
169
|
-
|
|
281
|
+
// bun's Windows spawn only consults PATH when the env object carries an
|
|
282
|
+
// explicit PATH key — a spread-only PATH is invisible to the lookup and
|
|
283
|
+
// uv_spawn fails with ENOENT even though the CLI is on PATH.
|
|
284
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GITLAB_TOKEN: token };
|
|
170
285
|
} else {
|
|
171
286
|
cmd = ["gh", "pr", "create", "--title", title, "--base", target];
|
|
172
287
|
if (finalBody) cmd.push("--body", finalBody);
|
|
173
288
|
if (draft) cmd.push("--draft");
|
|
174
|
-
cmdEnv = { ...process.env, GH_TOKEN: token };
|
|
289
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GH_TOKEN: token };
|
|
175
290
|
}
|
|
176
291
|
|
|
177
292
|
const result = spawnSync(cmd[0], cmd.slice(1), { cwd: root, encoding: "utf8", env: cmdEnv });
|
|
178
293
|
if (result.status !== 0) {
|
|
179
294
|
const err = (result.stderr ?? result.stdout ?? "").trim();
|
|
180
295
|
let hint: Record<string, any> | null = null;
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
296
|
+
if (
|
|
297
|
+
provider === "gitlab" &&
|
|
298
|
+
(err.includes("409") || err.toLowerCase().includes("already exists"))
|
|
299
|
+
) {
|
|
300
|
+
const list = spawnSync("glab", ["mr", "list", `--source-branch=${branch}`, "--output=json"], {
|
|
301
|
+
cwd: root,
|
|
302
|
+
encoding: "utf8",
|
|
303
|
+
env: cmdEnv,
|
|
304
|
+
});
|
|
184
305
|
if (list.status === 0 && (list.stdout ?? "").trim()) {
|
|
185
306
|
try {
|
|
186
307
|
const mrs = JSON.parse(list.stdout ?? "") as Array<Record<string, any>>;
|
|
@@ -191,10 +312,16 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
191
312
|
next_step: "Use glab mr update or close the open MR before creating again",
|
|
192
313
|
};
|
|
193
314
|
}
|
|
194
|
-
} catch {
|
|
315
|
+
} catch {
|
|
316
|
+
/* no hint */
|
|
317
|
+
}
|
|
195
318
|
}
|
|
196
319
|
}
|
|
197
|
-
const payload: Record<string, any> = {
|
|
320
|
+
const payload: Record<string, any> = {
|
|
321
|
+
error: "create failed",
|
|
322
|
+
provider,
|
|
323
|
+
stderr: err.slice(0, 800),
|
|
324
|
+
};
|
|
198
325
|
if (hint) payload.hint = hint;
|
|
199
326
|
return payload;
|
|
200
327
|
}
|
package/src/core/present.ts
CHANGED
|
@@ -30,7 +30,12 @@ export function renderAsciiWireframe(spec: unknown): string {
|
|
|
30
30
|
}
|
|
31
31
|
if (kind === "button") {
|
|
32
32
|
const label = "[ " + String(row?.label ?? "Button") + " ]";
|
|
33
|
-
lines.push(
|
|
33
|
+
lines.push(
|
|
34
|
+
boxLine(
|
|
35
|
+
label.padStart(Math.max(0, (width - 4 + label.length) / 2)).padEnd(width - 4),
|
|
36
|
+
width,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
34
39
|
continue;
|
|
35
40
|
}
|
|
36
41
|
if (kind === "field") {
|
|
@@ -42,7 +47,11 @@ export function renderAsciiWireframe(spec: unknown): string {
|
|
|
42
47
|
if (kind === "columns") {
|
|
43
48
|
const cols = Array.isArray(row?.columns) ? row.columns : [];
|
|
44
49
|
const colW = Math.floor((width - 4 - cols.length + 1) / Math.max(cols.length, 1));
|
|
45
|
-
const parts = cols.map((c: any) =>
|
|
50
|
+
const parts = cols.map((c: any) =>
|
|
51
|
+
String(c?.label ?? "")
|
|
52
|
+
.slice(0, Math.max(0, colW - 1))
|
|
53
|
+
.padEnd(Math.max(0, colW)),
|
|
54
|
+
);
|
|
46
55
|
lines.push(boxLine(parts.join(" | ").trim(), width));
|
|
47
56
|
continue;
|
|
48
57
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Registration merge helpers for the OpenCode/Cursor installers (RR-06).
|
|
2
|
+
// Pure functions: accept the existing user config, return the deduplicated
|
|
3
|
+
// config PLUS the explicit list of keys changed. Unrelated user settings are
|
|
4
|
+
// never rewritten — their values round-trip JSON-identical. The install
|
|
5
|
+
// scripts (`packages/workit-core/scripts/install-*-plugin.sh`) import these so
|
|
6
|
+
// there is exactly one source of truth for registration merging.
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface MergeResult<T> {
|
|
11
|
+
config: T;
|
|
12
|
+
changed: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
16
|
+
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
17
|
+
|
|
18
|
+
/** Exact identity match — `name` or `name@version` — never a substring (D3). */
|
|
19
|
+
const named = (s: string, name: string) => s === name || s.startsWith(`${name}@`);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when a plugin identity is a current or legacy Workit entry. Matched by
|
|
23
|
+
* exact identity, never by substring: an unrelated plugin whose id merely
|
|
24
|
+
* contains "workflow-toolkit" is preserved (D3). Path-style identities are
|
|
25
|
+
* compared with normalized separators so a Windows file:// pin (backslashes)
|
|
26
|
+
* still matches the packages/workit-* path checks.
|
|
27
|
+
*/
|
|
28
|
+
export function isWorkitPlugin(value: unknown): boolean {
|
|
29
|
+
const s = String(value).replaceAll("\\", "/");
|
|
30
|
+
const url = s.startsWith("file://") || s.startsWith("git+file://");
|
|
31
|
+
const pkgPath =
|
|
32
|
+
s.includes("/packages/workit-opencode/") ||
|
|
33
|
+
s.includes("/packages/workit-cursor/") ||
|
|
34
|
+
s.includes("/node_modules/@brainervirus/workit-opencode/") ||
|
|
35
|
+
s.includes("/node_modules/@brainervirus/workit-cursor/");
|
|
36
|
+
return (
|
|
37
|
+
named(s, "workflow-toolkit") ||
|
|
38
|
+
named(s, "workflow-toolkit-opencode") ||
|
|
39
|
+
named(s, "local/workflow-toolkit") ||
|
|
40
|
+
named(s, "@brainervirus/workit-opencode") ||
|
|
41
|
+
named(s, "@brainervirus/workit-cursor") ||
|
|
42
|
+
(url && pkgPath) ||
|
|
43
|
+
(s.startsWith("git+file://") && s.includes("workflow-toolkit"))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Deduplicate every legacy/current Workit plugin identity to one dev pin. */
|
|
48
|
+
export function mergeOpenCodePlugins(plugin: unknown, pin: string): MergeResult<unknown[]> {
|
|
49
|
+
const existing = Array.isArray(plugin) ? plugin : typeof plugin === "string" ? [plugin] : [];
|
|
50
|
+
const config = [pin, ...existing.filter((p) => !isWorkitPlugin(p))];
|
|
51
|
+
const changed = JSON.stringify(config) !== JSON.stringify(existing) ? ["plugin"] : [];
|
|
52
|
+
return { config, changed };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Merge an existing OpenCode config with a dev pin; preserve unrelated keys. */
|
|
56
|
+
export function mergeOpenCodeConfig(
|
|
57
|
+
config: unknown,
|
|
58
|
+
pin: string,
|
|
59
|
+
): MergeResult<Record<string, unknown>> {
|
|
60
|
+
const base = isRecord(config) ? { ...config } : {};
|
|
61
|
+
const changed: string[] = [];
|
|
62
|
+
|
|
63
|
+
const plugins = mergeOpenCodePlugins(base.plugin, pin);
|
|
64
|
+
if (plugins.changed.length > 0) {
|
|
65
|
+
base.plugin = plugins.config;
|
|
66
|
+
changed.push("plugin");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Drop share skills.paths — native ~/.config/opencode/skills links avoid
|
|
70
|
+
// triple-load duplicates. Matched by exact path segment, never substring: an
|
|
71
|
+
// unrelated dir like `~/projects/my-workflow-toolkit-skills` is preserved (D3).
|
|
72
|
+
const skills = base.skills;
|
|
73
|
+
if (isRecord(skills) && Array.isArray(skills.paths)) {
|
|
74
|
+
const next = skills.paths.filter((p) => {
|
|
75
|
+
const segment = String(p).split(/[\\/]/);
|
|
76
|
+
return !segment.some((seg) => named(seg, "workflow-toolkit"));
|
|
77
|
+
});
|
|
78
|
+
if (next.length !== skills.paths.length) {
|
|
79
|
+
base.skills = { ...skills, paths: next };
|
|
80
|
+
changed.push("skills.paths");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { config: base, changed };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Collapse current + legacy Cursor plugin identities to a single one. */
|
|
88
|
+
export function mergeCursorEnabledPlugins(enabled: unknown): MergeResult<Record<string, boolean>> {
|
|
89
|
+
const prev = isRecord(enabled) ? { ...(enabled as Record<string, boolean>) } : {};
|
|
90
|
+
const next: Record<string, boolean> = { ...prev, "workflow-toolkit": true };
|
|
91
|
+
delete next["local/workflow-toolkit"]; // legacy duplicate identity
|
|
92
|
+
const changed = JSON.stringify(next) !== JSON.stringify(prev) ? ["enabled_plugins"] : [];
|
|
93
|
+
return { config: next, changed };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Append the plugin dir once, preserving any existing plugin directories. */
|
|
97
|
+
export function mergeCursorPluginDirs(
|
|
98
|
+
pluginDirs: unknown,
|
|
99
|
+
pluginDir: string,
|
|
100
|
+
): MergeResult<string[]> {
|
|
101
|
+
// path.join does not strip a single trailing separator; do it explicitly for
|
|
102
|
+
// the dedup comparison (guarding the filesystem-root case).
|
|
103
|
+
const strip = (p: string) => {
|
|
104
|
+
const j = path.join(p);
|
|
105
|
+
return path.dirname(j) === j ? j : j.replace(/[\\/]+$/, "");
|
|
106
|
+
};
|
|
107
|
+
const normalized = strip(pluginDir);
|
|
108
|
+
const prev = Array.isArray(pluginDirs) ? pluginDirs.map(String) : [];
|
|
109
|
+
// Normalize both sides for comparison so a trailing-slash variant of an
|
|
110
|
+
// existing entry is not appended as a duplicate; existing entries are kept
|
|
111
|
+
// verbatim.
|
|
112
|
+
const exists = prev.some((d) => strip(d) === normalized);
|
|
113
|
+
const next = exists ? prev : [...prev, normalized];
|
|
114
|
+
const changed = next.length !== prev.length ? ["plugin_dirs"] : [];
|
|
115
|
+
return { config: next, changed };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Merge Cursor settings: one plugin identity, dirs appended, unrelated keys kept. */
|
|
119
|
+
export function mergeCursorSettings(
|
|
120
|
+
settings: unknown,
|
|
121
|
+
pluginDir: string,
|
|
122
|
+
): MergeResult<Record<string, unknown>> {
|
|
123
|
+
const base = isRecord(settings) ? { ...settings } : {};
|
|
124
|
+
const changed: string[] = [];
|
|
125
|
+
|
|
126
|
+
const enabled = mergeCursorEnabledPlugins(base.enabled_plugins);
|
|
127
|
+
if (enabled.changed.length > 0) {
|
|
128
|
+
base.enabled_plugins = enabled.config;
|
|
129
|
+
changed.push("enabled_plugins");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const dirs = mergeCursorPluginDirs(base.plugin_dirs, pluginDir);
|
|
133
|
+
if (dirs.changed.length > 0) {
|
|
134
|
+
base.plugin_dirs = dirs.config;
|
|
135
|
+
changed.push("plugin_dirs");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { config: base, changed };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Set one portable workit MCP server, dropping the legacy server name. */
|
|
142
|
+
export function mergeCursorMcp(
|
|
143
|
+
mcp: unknown,
|
|
144
|
+
serverName: string,
|
|
145
|
+
server: Record<string, unknown>,
|
|
146
|
+
): MergeResult<Record<string, unknown>> {
|
|
147
|
+
const base = isRecord(mcp) ? { ...mcp } : {};
|
|
148
|
+
const servers = isRecord(base.mcpServers)
|
|
149
|
+
? { ...(base.mcpServers as Record<string, unknown>) }
|
|
150
|
+
: {};
|
|
151
|
+
delete servers["workflow-toolkit"]; // legacy duplicate registration
|
|
152
|
+
servers[serverName] = server;
|
|
153
|
+
const changed = JSON.stringify(servers) !== JSON.stringify(base.mcpServers) ? ["mcpServers"] : [];
|
|
154
|
+
base.mcpServers = servers;
|
|
155
|
+
return { config: base, changed };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Swap the sessionStart hook command, preserving other hooks and fields. */
|
|
159
|
+
export function mergeCursorHooks(
|
|
160
|
+
hooks: unknown,
|
|
161
|
+
sessionStartEntry: Record<string, unknown>,
|
|
162
|
+
): MergeResult<Record<string, unknown>> {
|
|
163
|
+
const base: Record<string, unknown> = isRecord(hooks) ? { ...hooks } : { version: 1 };
|
|
164
|
+
const hooksMap = isRecord(base.hooks) ? { ...(base.hooks as Record<string, unknown>) } : {};
|
|
165
|
+
const list = Array.isArray(hooksMap.sessionStart) ? hooksMap.sessionStart : [];
|
|
166
|
+
const same =
|
|
167
|
+
list.length === 1 &&
|
|
168
|
+
isRecord(list[0]) &&
|
|
169
|
+
JSON.stringify(list[0]) === JSON.stringify(sessionStartEntry);
|
|
170
|
+
if (same) return { config: base, changed: [] };
|
|
171
|
+
hooksMap.sessionStart = [sessionStartEntry];
|
|
172
|
+
base.hooks = hooksMap;
|
|
173
|
+
return { config: base, changed: ["hooks.sessionStart"] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Portable Cursor MCP server entry for an installed plugin dir: prefer the
|
|
178
|
+
* self-contained node dist bundle (PT-10); fall back to the bash shim for a
|
|
179
|
+
* dist-less dev checkout.
|
|
180
|
+
*/
|
|
181
|
+
export function cursorMcpServerEntry(packageDir: string): {
|
|
182
|
+
command: string;
|
|
183
|
+
args: string[];
|
|
184
|
+
} {
|
|
185
|
+
if (existsSync(path.join(packageDir, "dist", "mcp-server.js"))) {
|
|
186
|
+
return {
|
|
187
|
+
command: "node",
|
|
188
|
+
args: [path.join(packageDir, "dist", "mcp-server.js"), "${workspaceFolder}"],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
command: "bash",
|
|
193
|
+
args: [path.join(packageDir, "mcp", "run-server.sh"), "${workspaceFolder}"],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Portable Cursor sessionStart hook entry: prefer the self-contained node dist
|
|
199
|
+
* bundle; fall back to the bash shim for a dist-less dev checkout (AR-06).
|
|
200
|
+
*/
|
|
201
|
+
export function cursorHooksEntry(packageDir: string): {
|
|
202
|
+
command: string;
|
|
203
|
+
args: string[];
|
|
204
|
+
} {
|
|
205
|
+
if (existsSync(path.join(packageDir, "dist", "cursor-session-start.js"))) {
|
|
206
|
+
return {
|
|
207
|
+
command: "node",
|
|
208
|
+
args: [path.join(packageDir, "dist", "cursor-session-start.js")],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
command: "bash",
|
|
213
|
+
args: [path.join(packageDir, "hooks", "session-start")],
|
|
214
|
+
};
|
|
215
|
+
}
|
package/src/core/reminder.ts
CHANGED
|
@@ -57,8 +57,7 @@ Skill: receiving-code-review. Verify before implementing — evaluate review fee
|
|
|
57
57
|
export const shouldInjectVerification = (currentText: string): boolean =>
|
|
58
58
|
!currentText.includes(VERIFICATION_TEXT);
|
|
59
59
|
|
|
60
|
-
export const shouldInjectTdd = (currentText: string): boolean =>
|
|
61
|
-
!currentText.includes(TDD_TEXT);
|
|
60
|
+
export const shouldInjectTdd = (currentText: string): boolean => !currentText.includes(TDD_TEXT);
|
|
62
61
|
|
|
63
62
|
export const shouldInjectBrainstorm = (currentText: string): boolean =>
|
|
64
63
|
!currentText.includes(BRAINSTORM_TEXT);
|