@brainervirus/workit-core 0.4.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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -0
  3. package/commands/wk-changelog.md +2 -0
  4. package/commands/wk-commit.md +2 -0
  5. package/commands/wk-docs-refresh.md +2 -0
  6. package/commands/wk-handoff.md +2 -0
  7. package/commands/wk-implement.md +2 -0
  8. package/commands/wk-init.md +2 -0
  9. package/commands/wk-issue-update.md +2 -0
  10. package/commands/wk-meetings.md +2 -0
  11. package/commands/wk-pr.md +2 -0
  12. package/commands/wk-release-notes.md +2 -0
  13. package/commands/wk-status.md +2 -0
  14. package/commands/wk-verify.md +2 -0
  15. package/package.json +43 -0
  16. package/scripts/_shared/common.sh +158 -0
  17. package/scripts/changelog-context.sh +42 -0
  18. package/scripts/docs-refresh-context.sh +40 -0
  19. package/scripts/fixtures/sample-plan.md +15 -0
  20. package/scripts/fixtures/sample-sdd/progress.md +2 -0
  21. package/scripts/init/apply.sh +5 -0
  22. package/scripts/init/status.sh +5 -0
  23. package/scripts/init/toolkit-status.sh +5 -0
  24. package/scripts/install-cursor-plugin.sh +83 -0
  25. package/scripts/install-opencode-plugin.sh +76 -0
  26. package/scripts/pr-create.sh +5 -0
  27. package/scripts/pr-ready-context.sh +88 -0
  28. package/scripts/present/ascii-wireframe.sh +5 -0
  29. package/scripts/present/flow-diagram.sh +5 -0
  30. package/scripts/release-notes-context.sh +40 -0
  31. package/scripts/rewrite-workspace-deps.ts +17 -0
  32. package/scripts/run-cursor-mcp.sh +10 -0
  33. package/scripts/sync-runtime.sh +96 -0
  34. package/scripts/update-superpowers.sh +56 -0
  35. package/scripts/vcs/config.sh +5 -0
  36. package/scripts/vcs/merged-style.sh +5 -0
  37. package/scripts/vcs/token-create-urls.sh +5 -0
  38. package/scripts/vcs/verify-token.sh +5 -0
  39. package/scripts/verify-project.sh +140 -0
  40. package/scripts/youtrack/api.sh +5 -0
  41. package/scripts/youtrack/config.sh +5 -0
  42. package/scripts/youtrack/greeting.sh +5 -0
  43. package/scripts/youtrack/parse-duration.sh +5 -0
  44. package/scripts/youtrack/token-create-url.sh +5 -0
  45. package/scripts/youtrack/verify-token.sh +5 -0
  46. package/scripts/youtrack/work-date-ms.sh +5 -0
  47. package/skills/wk-changelog/SKILL.md +15 -0
  48. package/skills/wk-commit/SKILL.md +16 -0
  49. package/skills/wk-docs-refresh/SKILL.md +15 -0
  50. package/skills/wk-handoff/SKILL.md +17 -0
  51. package/skills/wk-implement/SKILL.md +41 -0
  52. package/skills/wk-init/SKILL.md +31 -0
  53. package/skills/wk-issue-update/SKILL.md +27 -0
  54. package/skills/wk-issue-update/references/youtrack-update-style.md +81 -0
  55. package/skills/wk-meetings/SKILL.md +17 -0
  56. package/skills/wk-pr/SKILL.md +27 -0
  57. package/skills/wk-release-notes/SKILL.md +15 -0
  58. package/skills/wk-status/SKILL.md +16 -0
  59. package/skills/wk-verify/SKILL.md +16 -0
  60. package/src/core/branch.ts +246 -0
  61. package/src/core/changelog.ts +312 -0
  62. package/src/core/config-guard.ts +26 -0
  63. package/src/core/config.ts +73 -0
  64. package/src/core/detector.ts +207 -0
  65. package/src/core/doc-render.ts +14 -0
  66. package/src/core/docs-repo.ts +196 -0
  67. package/src/core/docs-validate.ts +255 -0
  68. package/src/core/flow-state.ts +225 -0
  69. package/src/core/git.ts +56 -0
  70. package/src/core/gitignore.ts +43 -0
  71. package/src/core/handoff-context.ts +115 -0
  72. package/src/core/hygiene.ts +77 -0
  73. package/src/core/init.ts +443 -0
  74. package/src/core/parse-sections.ts +24 -0
  75. package/src/core/plan-tasks.ts +33 -0
  76. package/src/core/ports/init-apply.ts +15 -0
  77. package/src/core/ports/init-status.ts +4 -0
  78. package/src/core/ports/init-toolkit-status.ts +4 -0
  79. package/src/core/ports/pr-create.ts +22 -0
  80. package/src/core/ports/present-ascii.ts +10 -0
  81. package/src/core/ports/present-flow.ts +10 -0
  82. package/src/core/ports/vcs-config.ts +14 -0
  83. package/src/core/ports/vcs-merged-style.ts +5 -0
  84. package/src/core/ports/vcs-token-create-urls.ts +4 -0
  85. package/src/core/ports/vcs-verify-token.ts +4 -0
  86. package/src/core/ports/youtrack-api.ts +17 -0
  87. package/src/core/ports/youtrack-config.ts +23 -0
  88. package/src/core/ports/youtrack-greeting.ts +10 -0
  89. package/src/core/ports/youtrack-parse-duration.ts +14 -0
  90. package/src/core/ports/youtrack-token-create-url.ts +4 -0
  91. package/src/core/ports/youtrack-verify-token.ts +12 -0
  92. package/src/core/ports/youtrack-work-date-ms.ts +10 -0
  93. package/src/core/pr-create.ts +212 -0
  94. package/src/core/present.ts +100 -0
  95. package/src/core/reminder.ts +82 -0
  96. package/src/core/repo-tool.ts +9 -0
  97. package/src/core/rules.ts +122 -0
  98. package/src/core/scripts.ts +42 -0
  99. package/src/core/sdd.ts +188 -0
  100. package/src/core/templates.ts +37 -0
  101. package/src/core/vcs-config.ts +252 -0
  102. package/src/core/verify-parse.ts +27 -0
  103. package/src/core/workspaces.ts +81 -0
  104. package/src/core/youtrack.ts +488 -0
  105. package/src/core.ts +62 -0
  106. package/src/state.ts +22 -0
  107. package/src/tools/docs-repo.ts +42 -0
  108. package/src/tools/flow.ts +88 -0
  109. package/src/tools/handoff.ts +160 -0
  110. package/src/tools/index.ts +22 -0
  111. package/src/tools/present.ts +45 -0
  112. package/src/tools/repo.ts +357 -0
  113. package/src/tools/rules.ts +30 -0
  114. package/src/tools/sdd.ts +189 -0
  115. package/src/tools/templates.ts +27 -0
  116. package/src/tools/youtrack.ts +356 -0
  117. package/templates/execution-contract.md +56 -0
  118. package/templates/greeting.md +1 -0
  119. package/templates/headers.md +3 -0
  120. package/templates/hygiene/.editorconfig +8 -0
  121. package/templates/hygiene/.gitattributes +3 -0
  122. package/templates/hygiene/CHANGELOG.md +14 -0
  123. package/templates/hygiene/CONTRIBUTING.md +3 -0
  124. package/templates/hygiene/LICENSE +21 -0
  125. package/templates/hygiene/README.md +3 -0
  126. package/templates/issue-update.md +6 -0
  127. package/templates/plan-template.md +25 -0
  128. package/templates/spec-template.md +51 -0
  129. package/templates/superpowers-doc-contract.md +69 -0
  130. package/vendor/superpowers/skills/brainstorming/SKILL.md +159 -0
  131. package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
  132. package/vendor/superpowers/skills/brainstorming/scripts/helper.js +167 -0
  133. package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
  134. package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
  135. package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
  136. package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  137. package/vendor/superpowers/skills/brainstorming/visual-companion.md +291 -0
  138. package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +185 -0
  139. package/vendor/superpowers/skills/executing-plans/SKILL.md +70 -0
  140. package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +241 -0
  141. package/vendor/superpowers/skills/receiving-code-review/SKILL.md +213 -0
  142. package/vendor/superpowers/skills/requesting-code-review/SKILL.md +103 -0
  143. package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +172 -0
  144. package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +418 -0
  145. package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +139 -0
  146. package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +44 -0
  147. package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +22 -0
  148. package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +40 -0
  149. package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +188 -0
  150. package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
  151. package/vendor/superpowers/skills/systematic-debugging/SKILL.md +296 -0
  152. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  153. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
  154. package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
  155. package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +63 -0
  156. package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
  157. package/vendor/superpowers/skills/systematic-debugging/test-academic.md +14 -0
  158. package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
  159. package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
  160. package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
  161. package/vendor/superpowers/skills/test-driven-development/SKILL.md +371 -0
  162. package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +299 -0
  163. package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +202 -0
  164. package/vendor/superpowers/skills/using-superpowers/SKILL.md +62 -0
  165. package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
  166. package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +39 -0
  167. package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
  168. package/vendor/superpowers/skills/verification-before-completion/SKILL.md +139 -0
  169. package/vendor/superpowers/skills/writing-plans/SKILL.md +174 -0
  170. package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
  171. package/vendor/superpowers/skills/writing-skills/SKILL.md +689 -0
  172. package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
  173. package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  174. package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
  175. package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
  176. package/vendor/superpowers/skills/writing-skills/render-graphs.js +168 -0
  177. package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
@@ -0,0 +1,14 @@
1
+ // CLI port of scripts/youtrack/parse-duration.sh.
2
+ import { youTrackParseDuration } from "../youtrack";
3
+
4
+ const text = process.argv.slice(2).join(" ");
5
+ if (!text) {
6
+ console.error("ERROR: duration text required");
7
+ process.exit(1);
8
+ }
9
+ const out = youTrackParseDuration(text);
10
+ if ("error" in out) {
11
+ console.error("ERROR: " + out.error);
12
+ process.exit(1);
13
+ }
14
+ console.log(JSON.stringify(out.data));
@@ -0,0 +1,4 @@
1
+ // CLI port of scripts/youtrack/token-create-url.sh.
2
+ import { youTrackTokenCreateUrl } from "../youtrack";
3
+
4
+ console.log(JSON.stringify(youTrackTokenCreateUrl().data, null, 2));
@@ -0,0 +1,12 @@
1
+ // CLI port of scripts/youtrack/verify-token.sh.
2
+ import { youTrackVerifyToken } from "../youtrack";
3
+
4
+ const out = youTrackVerifyToken();
5
+ if ("error" in out) {
6
+ const payload: Record<string, any> = { ok: false, error: out.error };
7
+ if (out.http_status !== undefined) payload.http_status = out.http_status;
8
+ if (out.path !== undefined) payload.path = out.path;
9
+ console.log(JSON.stringify(payload, null, 2));
10
+ process.exit(1);
11
+ }
12
+ console.log(JSON.stringify(out.data, null, 2));
@@ -0,0 +1,10 @@
1
+ // CLI port of scripts/youtrack/work-date-ms.sh.
2
+ import { youTrackWorkDateMs } from "../youtrack";
3
+
4
+ const raw = process.argv[2] ?? "auto";
5
+ const out = youTrackWorkDateMs(raw);
6
+ if ("error" in out) {
7
+ console.error("ERROR: " + out.error);
8
+ process.exit(1);
9
+ }
10
+ console.log(JSON.stringify(out.data));
@@ -0,0 +1,212 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { vcsConfig } from "./vcs-config";
5
+
6
+ // Port of scripts/pr-create.sh — build MR/PR body issue linking + create via glab/gh.
7
+
8
+ function parseGhRepo(remote: string): string | null {
9
+ remote = (remote || "").trim().replace(/\/+$/, "");
10
+ if (!remote) return null;
11
+ if (remote.endsWith(".git")) remote = remote.slice(0, -4);
12
+ if (remote.includes(":")) remote = remote.split(":").pop() ?? ""; // drop git@host part (scp-style URL)
13
+ const parts = remote.split("/").filter(Boolean);
14
+ return parts.length >= 2 ? parts.slice(-2).join("/") : null;
15
+ }
16
+
17
+ function parseGhIssue(value: string): string {
18
+ const m = /issues\/(\d+)/.exec(value);
19
+ return m ? m[1] : String(value).trim().replace(/^#/, "");
20
+ }
21
+
22
+ function buildBody(
23
+ body: string,
24
+ branch: string,
25
+ linkIssues: boolean,
26
+ baseUrl: string,
27
+ ytIssue: string,
28
+ ghLinkOnPr: boolean,
29
+ ghIssue: string,
30
+ ghRelation: string,
31
+ ghRepo: string | null,
32
+ ): string {
33
+ let line: string | null = null;
34
+ if (linkIssues) {
35
+ let issue = ytIssue;
36
+ if (!issue && branch) {
37
+ // anchored prefix + \b boundary, 3+ digits so version-like tokens (POSTGRES-16, HTTP-3) never link
38
+ const m = /(?:^|\/|-)([A-Z]{2,}-\d{3,})\b/.exec(branch);
39
+ if (m) issue = m[1];
40
+ }
41
+ if (issue && baseUrl) line = `Related to: ${baseUrl.replace(/\/+$/, "")}/issue/${issue}`;
42
+ } else if (ghLinkOnPr) {
43
+ let issue = parseGhIssue(ghIssue);
44
+ if (!issue && branch) {
45
+ // pure-number issue id (feature/42-title -> 42); digits must be followed by a dash or end-of-string
46
+ // so version tokens (release/1.2.3, backport/8.0.1, lodash-4.17.21, 2024.1) never link
47
+ // ponytail: known date-style false positive (feature/2024-01-fix -> Closes #2024); accepted — bare 42-title support is deliberate
48
+ const m = /(?:^|\/|-)(\d+)(?:-|$)/.exec(branch);
49
+ if (m) issue = m[1];
50
+ }
51
+ if (issue) {
52
+ if (ghRelation === "related") {
53
+ line = `Related to #${issue}`;
54
+ if (ghRepo) line += ` — https://github.com/${ghRepo}/issues/${issue}`;
55
+ } else {
56
+ line = `Closes #${issue}`;
57
+ }
58
+ }
59
+ }
60
+ if (line === null) return body;
61
+ return body ? `${body}\n\n${line}` : line;
62
+ }
63
+
64
+ const truthy = (v: string | undefined): boolean => ["1", "true", "yes"].includes(String(v ?? "").toLowerCase());
65
+
66
+ // Port of python's shutil.which — scan PATH in-process (no `which` binary needed).
67
+ function whichOnPath(tool: string): string | null {
68
+ for (const dir of (process.env.PATH ?? "").split(":")) {
69
+ if (!dir) continue;
70
+ const candidate = path.join(dir, tool);
71
+ try {
72
+ fs.accessSync(candidate, fs.constants.X_OK);
73
+ return candidate;
74
+ } catch { /* keep scanning */ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function repoRoot(cwd: string): string {
80
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
81
+ return result.status === 0 ? (result.stdout ?? "").trim() : cwd;
82
+ }
83
+
84
+ /** Port of pr-create.sh --build-body — pure body builder, no network. */
85
+ export function prBuildBody(env: NodeJS.ProcessEnv, cwd?: string): string {
86
+ const ghLinkOnPr = truthy(env.GH_LINK_ON_PR);
87
+ let ghRepo = env.GH_REPO || null;
88
+ if (!ghRepo && ghLinkOnPr) {
89
+ const result = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
90
+ if (result.status === 0) ghRepo = parseGhRepo(result.stdout ?? "");
91
+ }
92
+ return buildBody(
93
+ env.BODY ?? "",
94
+ env.BRANCH ?? "",
95
+ truthy(env.LINK_ISSUES),
96
+ env.YT_BASE_URL ?? "",
97
+ env.WORKFLOW_YT_ISSUE ?? "",
98
+ ghLinkOnPr,
99
+ env.WORKFLOW_GH_ISSUE ?? "",
100
+ env.WORKFLOW_GH_ISSUE_RELATION ?? "closes",
101
+ ghRepo,
102
+ );
103
+ }
104
+
105
+ /** Port of scripts/pr-create.sh create mode — glab/gh MR/PR creation. */
106
+ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, any> {
107
+ const root = process.env.WORKFLOW_WORKSPACE_ROOT ?? repoRoot(cwd);
108
+ const cfg = vcsConfig("load", root);
109
+ if (!cfg.ok) return { error: cfg.error ?? "vcs config missing" };
110
+ if (!cfg.tokenReady) return { error: "VCS token not ready — run /wk-init and edit token file locally" };
111
+
112
+ const provider = cfg.provider as string;
113
+ if (provider !== "gitlab" && provider !== "github") return { error: `unsupported provider: ${provider}` };
114
+ const cli = provider === "gitlab" ? "glab" : "gh";
115
+ const installUrl = provider === "gitlab" ? "https://gitlab.com/gitlab-org/cli" : "https://cli.github.com";
116
+ if (whichOnPath(cli) === null) {
117
+ return {
118
+ ok: false,
119
+ cli_missing: true,
120
+ error: `workflow CLI missing: ${cli} (required for ${provider}). Install: ${installUrl}`,
121
+ install_url: installUrl,
122
+ };
123
+ }
124
+
125
+ const pr = (cfg.pr ?? {}) as Record<string, any>;
126
+ const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
127
+ const title = String(env.WF_PR_TITLE ?? "");
128
+ const body = env.WF_PR_BODY ?? "";
129
+ 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
+
135
+ let baseUrl = cfg.youtrack_base_url as string | undefined;
136
+ if (!baseUrl) {
137
+ const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path.join(path.dirname(String(cfg.configPath)), "youtrack.json");
138
+ try {
139
+ const yt = JSON.parse(fs.readFileSync(ytCfg, "utf8")) as Record<string, any>;
140
+ if (yt && typeof yt === "object") baseUrl = yt.baseUrl;
141
+ } catch { /* optional */ }
142
+ }
143
+ const ghLinkOnPr = cfg.issues_provider === "github" && cfg.link_on_pr === true;
144
+ let ghRepo: string | null = null;
145
+ if (ghLinkOnPr) {
146
+ const rr = spawnSync("git", ["remote", "get-url", "origin"], { cwd: root, encoding: "utf8" });
147
+ if (rr.status === 0) ghRepo = parseGhRepo(rr.stdout ?? "");
148
+ }
149
+ const finalBody = buildBody(
150
+ body, branch, cfg.link_issues === true, baseUrl ?? "", env.WORKFLOW_YT_ISSUE ?? "",
151
+ ghLinkOnPr, env.WORKFLOW_GH_ISSUE ?? "", env.WORKFLOW_GH_ISSUE_RELATION ?? "closes", ghRepo,
152
+ );
153
+
154
+ const squash = pr.squashOnMerge !== false;
155
+ const removeBranch = pr.removeSourceBranch !== false;
156
+ const push = pr.pushBranch !== false;
157
+ const skipConfirm = pr.confirmSkip !== false;
158
+
159
+ let cmd: string[];
160
+ let cmdEnv: NodeJS.ProcessEnv;
161
+ if (provider === "gitlab") {
162
+ // glab non-interactive mode requires BOTH title and description flags (issue #652).
163
+ cmd = ["glab", "mr", "create", "-t", title, "-d", finalBody || "", "-b", target];
164
+ cmd.push(squash ? "--squash-before-merge" : "--squash-before-merge=false");
165
+ cmd.push(removeBranch ? "--remove-source-branch" : "--remove-source-branch=false");
166
+ if (draft) cmd.push("--draft");
167
+ if (push) cmd.push("--push");
168
+ if (skipConfirm) cmd.push("--yes");
169
+ cmdEnv = { ...process.env, GITLAB_TOKEN: token };
170
+ } else {
171
+ cmd = ["gh", "pr", "create", "--title", title, "--base", target];
172
+ if (finalBody) cmd.push("--body", finalBody);
173
+ if (draft) cmd.push("--draft");
174
+ cmdEnv = { ...process.env, GH_TOKEN: token };
175
+ }
176
+
177
+ const result = spawnSync(cmd[0], cmd.slice(1), { cwd: root, encoding: "utf8", env: cmdEnv });
178
+ if (result.status !== 0) {
179
+ const err = (result.stderr ?? result.stdout ?? "").trim();
180
+ let hint: Record<string, any> | null = null;
181
+ if (provider === "gitlab" && (err.includes("409") || err.toLowerCase().includes("already exists"))) {
182
+ const list = spawnSync("glab", ["mr", "list", `--source-branch=${branch}`, "--output=json"],
183
+ { cwd: root, encoding: "utf8", env: cmdEnv });
184
+ if (list.status === 0 && (list.stdout ?? "").trim()) {
185
+ try {
186
+ const mrs = JSON.parse(list.stdout ?? "") as Array<Record<string, any>>;
187
+ if (mrs.length) {
188
+ hint = {
189
+ reason: "merge_request_already_exists",
190
+ existing: mrs[0],
191
+ next_step: "Use glab mr update or close the open MR before creating again",
192
+ };
193
+ }
194
+ } catch { /* no hint */ }
195
+ }
196
+ }
197
+ const payload: Record<string, any> = { error: "create failed", provider, stderr: err.slice(0, 800) };
198
+ if (hint) payload.hint = hint;
199
+ return payload;
200
+ }
201
+
202
+ return {
203
+ ok: true,
204
+ provider,
205
+ targetBranch: target,
206
+ squashOnMerge: squash,
207
+ removeSourceBranch: removeBranch,
208
+ output: (result.stdout ?? "").trim(),
209
+ };
210
+ }
211
+
212
+ export { buildBody, parseGhRepo, parseGhIssue };
@@ -0,0 +1,100 @@
1
+ // Ports of scripts/present/ascii-wireframe.sh + flow-diagram.sh — pure TS renderers.
2
+
3
+ function boxLine(text: string, width: number): string {
4
+ const inner = width - 4;
5
+ let t = text;
6
+ if (t.length > inner) t = t.slice(0, inner - 1) + "…";
7
+ return "│ " + t.padEnd(inner) + " │";
8
+ }
9
+
10
+ export function renderAsciiWireframe(spec: unknown): string {
11
+ const parsed = typeof spec === "string" ? JSON.parse(spec) : spec;
12
+ const title = String((parsed as any).title ?? "UI");
13
+ const width = Number((parsed as any).width ?? 72);
14
+ const rows = Array.isArray((parsed as any).rows) ? (parsed as any).rows : [];
15
+
16
+ const top = "┌" + "─".repeat(Math.max(0, width - 2)) + "┐";
17
+ const bot = "└" + "─".repeat(Math.max(0, width - 2)) + "┘";
18
+ const lines = [top, boxLine(title, width)];
19
+ lines.push("├" + "─".repeat(Math.max(0, width - 2)) + "┤");
20
+
21
+ for (const row of rows) {
22
+ const kind = row?.type ?? "text";
23
+ if (kind === "separator") {
24
+ lines.push("├" + "─".repeat(Math.max(0, width - 2)) + "┤");
25
+ continue;
26
+ }
27
+ if (kind === "header") {
28
+ lines.push(boxLine(String(row?.label ?? ""), width));
29
+ continue;
30
+ }
31
+ if (kind === "button") {
32
+ const label = "[ " + String(row?.label ?? "Button") + " ]";
33
+ lines.push(boxLine(label.padStart(Math.max(0, (width - 4 + label.length) / 2)).padEnd(width - 4), width));
34
+ continue;
35
+ }
36
+ if (kind === "field") {
37
+ const label = String(row?.label ?? "Field");
38
+ const value = String(row?.value ?? "_______________");
39
+ lines.push(boxLine(`${label}: ${value}`, width));
40
+ continue;
41
+ }
42
+ if (kind === "columns") {
43
+ const cols = Array.isArray(row?.columns) ? row.columns : [];
44
+ const colW = Math.floor((width - 4 - cols.length + 1) / Math.max(cols.length, 1));
45
+ const parts = cols.map((c: any) => String(c?.label ?? "").slice(0, Math.max(0, colW - 1)).padEnd(Math.max(0, colW)));
46
+ lines.push(boxLine(parts.join(" | ").trim(), width));
47
+ continue;
48
+ }
49
+ lines.push(boxLine(String(row?.label ?? String(row)), width));
50
+ }
51
+
52
+ lines.push(bot);
53
+ return lines.join("\n");
54
+ }
55
+
56
+ export function renderFlowDiagram(spec: unknown): string {
57
+ const parsed = typeof spec === "string" ? JSON.parse(spec) : spec;
58
+ const direction = String((parsed as any).direction ?? "TD");
59
+ const nodes = Array.isArray((parsed as any).nodes) ? (parsed as any).nodes : [];
60
+ const edges = Array.isArray((parsed as any).edges) ? (parsed as any).edges : [];
61
+ const title = (parsed as any).title;
62
+
63
+ const lines = ["flowchart " + direction];
64
+ if (title) lines.push(" %% " + String(title));
65
+
66
+ for (const n of nodes) {
67
+ const nid = String(n?.id ?? "");
68
+ const shape = String(n?.shape ?? "box");
69
+ const label = String(n?.label ?? nid).replaceAll('"', "'");
70
+ if (shape === "diamond") lines.push(` ${nid}{"${label}"}`);
71
+ else if (shape === "start") lines.push(` ${nid}(["${label}"])`);
72
+ else lines.push(` ${nid}["${label}"]`);
73
+ }
74
+
75
+ for (const e of edges) {
76
+ const src = String(e?.from ?? "");
77
+ const dst = String(e?.to ?? "");
78
+ const lbl = e?.label;
79
+ if (lbl) lines.push(` ${src} -->|${lbl}| ${dst}`);
80
+ else lines.push(` ${src} --> ${dst}`);
81
+ }
82
+
83
+ return lines.join("\n");
84
+ }
85
+
86
+ export function asciiWireframe(spec: unknown): Record<string, any> {
87
+ try {
88
+ return { data: { ascii: renderAsciiWireframe(spec), format: "ascii-wireframe" } };
89
+ } catch (err) {
90
+ return { error: err instanceof Error ? err.message : "ascii-wireframe render failed" };
91
+ }
92
+ }
93
+
94
+ export function flowDiagram(spec: unknown): Record<string, any> {
95
+ try {
96
+ return { data: { mermaid: renderFlowDiagram(spec), format: "mermaid" } };
97
+ } catch (err) {
98
+ return { error: err instanceof Error ? err.message : "flow-diagram render failed" };
99
+ }
100
+ }
@@ -0,0 +1,82 @@
1
+ export const REMINDER_TEXT = `<workflow-contract-reminder>
2
+ - Bounded user choices → call the native \`question\` tool (never A/B/C or 1/2/3 lists in prose).
3
+ - After a plan is approved → native \`question\` menu with exactly: Subagent-driven, Inline, Handoff (new session only), Review spec first, Review plan first.
4
+ - Tools with \`confirmed\` → call them; never fabricate their result.
5
+ - Before the first \`workflow_spec_approve\`/\`workflow_plan_approve\` (self-review) run the superpowers writing-plans Self-Review checklist: spec coverage (every spec requirement maps to a task), placeholder scan, type consistency; fix findings inline.
6
+ - Delivering docs → clickable markdown link \`[spec.md](docs/<slug>/spec.md)\` + 3-5 bullet summary.
7
+ </workflow-contract-reminder>`;
8
+
9
+ export const DETECTION_TEXT = `<workflow-detection>
10
+ Your previous message presented choices as a numbered/bulleted list in prose.
11
+ That is a bounded user choice — use the native \`question\` tool instead (re-ask with \`question\` if still relevant).
12
+ </workflow-detection>`;
13
+
14
+ export const DOC_DELIVERY_TEXT = `<workflow-doc-delivery>
15
+ You referenced a doc with a backtick-only path. Deliver docs with a clickable markdown link \`[spec.md](docs/<slug>/spec.md)\` and a 3-5 bullet summary of the content.
16
+ </workflow-doc-delivery>`;
17
+
18
+ export const SDD_REMINDER_TEXT = `<workflow-sdd-reminder>
19
+ An approved plan is subagent-driven — execute it via \`wk-implement\` / \`task\` delegation. Never implement the approved plan inline in the main session.
20
+ </workflow-sdd-reminder>`;
21
+
22
+ export const DOC_RENDER_TEXT = `<workflow-doc-render>
23
+ When delivering a spec or plan, by default render the full markdown content of the doc in chat (headings, tables, mermaid fences preserved) — NOT a backtick-wrapped raw block.
24
+ If the doc exceeds the render threshold (more than 150 lines, over 8KB, or more than 3 mermaid diagrams), deliver only the clickable link \`[spec.md](docs/<slug>/spec.md)\` + a 3-5 bullet summary.
25
+ On an explicit raw request ("raw", "para copiar", "sin render"), show the full fenced block instead.
26
+ Platform note: always use the standard \`\`\`mermaid fence — Cursor CLI renders it as an ASCII diagram, editors/GitHub render it natively; OpenCode TUI shows it as code text (renderer limitation, not a defect). Inline markdown links are not clickable in the OpenCode TUI.
27
+ </workflow-doc-render>`;
28
+
29
+ export const ISSUE_RAIL_TEXT = `<workflow-issue-rail>
30
+ A clickable \`question\` option whose label is an instruction (e.g. "Type the issue URL/ID") returns the label literal when clicked, not free text — ask for free text in plain prose with the custom answer field enabled instead.
31
+ </workflow-issue-rail>`;
32
+
33
+ export const CONFIG_GUARD_TEXT = `<workflow-config-guard>
34
+ A tool failed with a config-gap error (\`workflow config missing\`). Never configure without asking — ask with the native \`question\` tool, exactly three options: (1) configure only what's missing (guided, via the /wk-init skill flow for those actions), (2) run the full wizard (\`npx workit init\`), (3) skip — report the final error naming the missing items and how to configure them.
35
+ </workflow-config-guard>`;
36
+
37
+ export const VERIFICATION_TEXT = `<workflow-verification-rail>
38
+ Skill: verification-before-completion. NO completion claims without fresh verification evidence — run the check command (e.g. \`bun run check\` / \`workflow_verify\`) and show its output before claiming done/fixed/passing. If you haven't run the verification command in this message, you cannot claim it passes.
39
+ </workflow-verification-rail>`;
40
+
41
+ export const TDD_TEXT = `<workflow-tdd-rail>
42
+ Skill: test-driven-development. NO production code without a failing test first — write the test, watch it fail, then write the minimal code to pass.
43
+ </workflow-tdd-rail>`;
44
+
45
+ export const BRAINSTORM_TEXT = `<workflow-brainstorm-rail>
46
+ Skill: brainstorming. NO implementation until a design is presented and approved — explore intent, ask clarifying questions one at a time, present the design, and get user approval before writing any code.
47
+ </workflow-brainstorm-rail>`;
48
+
49
+ export const DEBUG_TEXT = `<workflow-debug-rail>
50
+ Skill: systematic-debugging. NO fixes without root cause investigation first — read the error, reproduce consistently, find the root cause, then fix. Symptom fixes are failure.
51
+ </workflow-debug-rail>`;
52
+
53
+ export const REVIEW_RECEPTION_TEXT = `<workflow-review-reception-rail>
54
+ Skill: receiving-code-review. Verify before implementing — evaluate review feedback against codebase reality (read, restate, verify) before accepting or acting on it.
55
+ </workflow-review-reception-rail>`;
56
+
57
+ export const shouldInjectVerification = (currentText: string): boolean =>
58
+ !currentText.includes(VERIFICATION_TEXT);
59
+
60
+ export const shouldInjectTdd = (currentText: string): boolean =>
61
+ !currentText.includes(TDD_TEXT);
62
+
63
+ export const shouldInjectBrainstorm = (currentText: string): boolean =>
64
+ !currentText.includes(BRAINSTORM_TEXT);
65
+
66
+ export const shouldInjectDebug = (currentText: string): boolean =>
67
+ !currentText.includes(DEBUG_TEXT);
68
+
69
+ export const shouldInjectReviewReception = (currentText: string): boolean =>
70
+ !currentText.includes(REVIEW_RECEPTION_TEXT);
71
+
72
+ export const shouldInjectDocRender = (currentText: string): boolean =>
73
+ !currentText.includes(DOC_RENDER_TEXT);
74
+
75
+ export const shouldInjectSddReminder = (currentText: string): boolean =>
76
+ !currentText.includes(SDD_REMINDER_TEXT);
77
+
78
+ export const shouldInjectConfigGuard = (currentText: string): boolean =>
79
+ !currentText.includes(CONFIG_GUARD_TEXT);
80
+
81
+ export const shouldInjectIssueRail = (currentText: string): boolean =>
82
+ !currentText.includes(ISSUE_RAIL_TEXT);
@@ -0,0 +1,9 @@
1
+ import { resolveWorkspaceRoot } from "./scripts";
2
+
3
+ /** Attach the resolved repository root to each repository tool response. */
4
+ export function withWorkspace(workspaceRoot: string, data: Record<string, any> = {}): Record<string, any> {
5
+ return {
6
+ workspace_root: resolveWorkspaceRoot(workspaceRoot),
7
+ ...data,
8
+ };
9
+ }
@@ -0,0 +1,122 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { configDir } from "./config";
5
+
6
+ export type RulePlatform = "cursor" | "opencode";
7
+ export type CanonicalRule = {
8
+ name: string;
9
+ description: string;
10
+ platforms: RulePlatform[];
11
+ body: string;
12
+ };
13
+
14
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
15
+
16
+ export const rulesDir = () => path.join(configDir(), "rules");
17
+
18
+ export const parseRule = (markdown: string): CanonicalRule | { error: string } => {
19
+ const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
20
+ if (!fm) return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
21
+ const meta: Record<string, string> = {};
22
+ const unquote = (v: string) => v.replace(/^["']|["']$/g, "").trim();
23
+ for (const line of fm[1].split("\n")) {
24
+ const idx = line.indexOf(":");
25
+ if (idx > 0) meta[line.slice(0, idx).trim()] = unquote(line.slice(idx + 1).trim());
26
+ }
27
+ const name = meta.name ?? "";
28
+ const description = meta.description ?? "";
29
+ const rawPlatforms = (meta.platforms ?? "")
30
+ .replace(/^\[|\]$/g, "").split(",").map((p) => p.trim().replace(/['"]/g, ""))
31
+ .filter(Boolean);
32
+ const platforms = rawPlatforms.filter((p): p is RulePlatform => p === "cursor" || p === "opencode");
33
+ if (!name || !description || platforms.length === 0) {
34
+ return { error: "rule frontmatter requires name, description, and platforms" };
35
+ }
36
+ if (platforms.length !== rawPlatforms.length) {
37
+ return { error: `invalid platform in frontmatter: ${rawPlatforms.join(", ")}` };
38
+ }
39
+ return { name, description, platforms, body: fm[2].trim() + "\n" };
40
+ };
41
+
42
+ export const listRules = (): { name: string; platforms: string[]; source: "config" | "repo" }[] => {
43
+ const result: { name: string; platforms: string[]; source: "config" | "repo" }[] = [];
44
+ const dir = rulesDir();
45
+ if (existsSync(dir)) {
46
+ for (const entry of readdirSync(dir)) {
47
+ const file = path.join(dir, entry, "rule.md");
48
+ if (!existsSync(file)) continue;
49
+ const parsed = parseRule(readFileSync(file, "utf8"));
50
+ if ("error" in parsed) continue;
51
+ result.push({ name: parsed.name, platforms: parsed.platforms, source: "config" });
52
+ }
53
+ }
54
+ return result;
55
+ };
56
+
57
+ export const readRule = (
58
+ name: string,
59
+ ): { source: "config" | "repo" | "missing"; rule: CanonicalRule } | { error: string } => {
60
+ const file = path.join(rulesDir(), name, "rule.md");
61
+ if (existsSync(file)) {
62
+ const parsed = parseRule(readFileSync(file, "utf8"));
63
+ if ("error" in parsed) return { error: parsed.error };
64
+ return { source: "config", rule: parsed };
65
+ }
66
+ return { source: "missing", rule: { name, description: "", platforms: [], body: "" } };
67
+ };
68
+
69
+ const RULE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
70
+
71
+ export const writeRule = (
72
+ rule: CanonicalRule,
73
+ confirmed: boolean,
74
+ ): { ok: true; path: string } | { ok: false; error: string } => {
75
+ if (!confirmed) return { ok: false, error: "confirmed: true required" };
76
+ if (!RULE_NAME_RE.test(rule.name)) return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
77
+ const dir = path.join(rulesDir(), rule.name);
78
+ mkdirSync(dir, { recursive: true });
79
+ const file = path.join(dir, "rule.md");
80
+ const md = `---\nname: ${rule.name}\ndescription: ${rule.description}\nplatforms: [${rule.platforms.join(", ")}]\n---\n${rule.body}`;
81
+ writeFileSync(file, md, "utf8");
82
+ return { ok: true, path: file };
83
+ };
84
+
85
+ export const compileRuleCursor = (rule: CanonicalRule): string =>
86
+ `---\ndescription: ${rule.description}\nalwaysApply: true\n---\n\n${rule.body}`;
87
+
88
+ export const compileRuleOpenCode = (rule: CanonicalRule): string =>
89
+ `## ${rule.name}\n\n${rule.body}`;
90
+
91
+ export const compiledOpenCodeSections = (): string => {
92
+ const sections: string[] = [];
93
+ const dir = rulesDir();
94
+ if (existsSync(dir)) {
95
+ for (const entry of readdirSync(dir)) {
96
+ const file = path.join(dir, entry, "rule.md");
97
+ if (!existsSync(file)) continue;
98
+ const parsed = parseRule(readFileSync(file, "utf8"));
99
+ if ("error" in parsed || !parsed.platforms.includes("opencode")) continue;
100
+ sections.push(compileRuleOpenCode(parsed));
101
+ }
102
+ }
103
+ return sections.join("\n\n");
104
+ };
105
+
106
+ export const writeCompiledCursorRules = (targetDir: string): string[] => {
107
+ const written: string[] = [];
108
+ const dir = rulesDir();
109
+ if (existsSync(dir)) {
110
+ for (const entry of readdirSync(dir)) {
111
+ const file = path.join(dir, entry, "rule.md");
112
+ if (!existsSync(file)) continue;
113
+ const parsed = parseRule(readFileSync(file, "utf8"));
114
+ if ("error" in parsed || !parsed.platforms.includes("cursor")) continue;
115
+ if (!RULE_NAME_RE.test(parsed.name)) continue;
116
+ const out = path.join(targetDir, `${parsed.name}.mdc`);
117
+ writeFileSync(out, compileRuleCursor(parsed), "utf8");
118
+ written.push(out);
119
+ }
120
+ }
121
+ return written;
122
+ };
@@ -0,0 +1,42 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ export const PLUGIN_ROOT = path.resolve(dirname, "../..");
7
+
8
+ export const resolveWorkspaceRoot = (explicit?: string) => explicit || process.cwd();
9
+
10
+ export function runScript(
11
+ scriptName: string,
12
+ args: string[],
13
+ workspaceRoot: string,
14
+ extraEnv?: Record<string, string>,
15
+ ) {
16
+ const cwd = resolveWorkspaceRoot(workspaceRoot);
17
+ const scriptPath = path.join(PLUGIN_ROOT, "scripts", scriptName);
18
+ const result = spawnSync("bash", [scriptPath, ...args], {
19
+ cwd,
20
+ encoding: "utf8",
21
+ env: { ...process.env, ...(extraEnv ?? {}) },
22
+ });
23
+ return {
24
+ stdout: result.stdout ?? "",
25
+ stderr: result.stderr ?? "",
26
+ exitCode: result.status ?? 1,
27
+ scriptPath,
28
+ cwd,
29
+ };
30
+ }
31
+
32
+ export function runScriptJson(scriptName: string, args: string[], workspaceRoot: string, extraEnv?: Record<string, string>) {
33
+ const { stdout, stderr, exitCode } = runScript(scriptName, args, workspaceRoot, extraEnv);
34
+ if (exitCode !== 0) {
35
+ return { error: (stderr || stdout || "script failed").trim(), exitCode };
36
+ }
37
+ try {
38
+ return { data: JSON.parse(stdout.trim()) };
39
+ } catch {
40
+ return { error: "invalid JSON from script", raw: stdout.trim() };
41
+ }
42
+ }