@brainervirus/workit-core 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/package.json +3 -7
  2. package/scripts/doctor-check.ts +20 -0
  3. package/scripts/install-cursor-plugin.sh +51 -28
  4. package/scripts/install-opencode-plugin.sh +19 -21
  5. package/scripts/rewrite-workspace-deps.ts +15 -9
  6. package/scripts/sync-runtime.sh +71 -19
  7. package/scripts/vendor-assets.ts +37 -0
  8. package/skills/wk-implement/SKILL.md +2 -2
  9. package/skills/wk-pr/SKILL.md +1 -1
  10. package/src/core/boundary.ts +27 -0
  11. package/src/core/branch-policy.ts +63 -0
  12. package/src/core/branch.ts +30 -16
  13. package/src/core/config.ts +193 -31
  14. package/src/core/docs-layout.ts +251 -0
  15. package/src/core/docs-migration.ts +639 -0
  16. package/src/core/docs-repo.ts +11 -9
  17. package/src/core/docs-validate.ts +18 -6
  18. package/src/core/doctor.ts +801 -0
  19. package/src/core/flow-state.ts +1579 -141
  20. package/src/core/git.ts +22 -5
  21. package/src/{tools/handoff.ts → core/handoff-tools.ts} +5 -57
  22. package/src/core/hygiene.ts +26 -12
  23. package/src/core/init.ts +43 -11
  24. package/src/core/logger.ts +321 -0
  25. package/src/core/package-root.ts +28 -0
  26. package/src/core/ports/init-toolkit-status.ts +1 -1
  27. package/src/core/ports/vcs-verify-token.ts +1 -1
  28. package/src/core/ports/youtrack-api.ts +1 -1
  29. package/src/core/ports/youtrack-verify-token.ts +1 -1
  30. package/src/core/pr-create.ts +116 -21
  31. package/src/core/registration.ts +215 -0
  32. package/src/core/repo-context.ts +447 -0
  33. package/src/core/repo-tools.ts +23 -0
  34. package/src/core/safe-write.ts +22 -0
  35. package/src/core/scripts.ts +3 -44
  36. package/src/core/sdd.ts +45 -28
  37. package/src/core/setup-state.ts +54 -0
  38. package/src/core/setup.ts +1216 -0
  39. package/src/core/skill-manifests.ts +95 -0
  40. package/src/core/support-matrix.ts +12 -0
  41. package/src/core/sync-runtime.ts +348 -0
  42. package/src/core/templates.ts +2 -2
  43. package/src/core/vcs-config.ts +107 -37
  44. package/src/core/verify-project.ts +181 -0
  45. package/src/core/workspaces.ts +136 -17
  46. package/src/core/youtrack-tools.ts +228 -0
  47. package/src/core/youtrack.ts +125 -67
  48. package/templates/execution-contract.md +9 -7
  49. package/templates/superpowers-doc-contract.md +4 -3
  50. package/scripts/_shared/common.sh +0 -173
  51. package/scripts/changelog-context.sh +0 -42
  52. package/scripts/docs-refresh-context.sh +0 -40
  53. package/scripts/init/apply.sh +0 -5
  54. package/scripts/init/status.sh +0 -5
  55. package/scripts/init/toolkit-status.sh +0 -5
  56. package/scripts/pr-create.sh +0 -5
  57. package/scripts/pr-ready-context.sh +0 -88
  58. package/scripts/present/ascii-wireframe.sh +0 -5
  59. package/scripts/present/flow-diagram.sh +0 -5
  60. package/scripts/release-notes-context.sh +0 -40
  61. package/scripts/vcs/config.sh +0 -5
  62. package/scripts/vcs/merged-style.sh +0 -5
  63. package/scripts/vcs/token-create-urls.sh +0 -5
  64. package/scripts/vcs/verify-token.sh +0 -5
  65. package/scripts/verify-project.sh +0 -140
  66. package/scripts/youtrack/api.sh +0 -5
  67. package/scripts/youtrack/config.sh +0 -5
  68. package/scripts/youtrack/greeting.sh +0 -5
  69. package/scripts/youtrack/parse-duration.sh +0 -5
  70. package/scripts/youtrack/token-create-url.sh +0 -5
  71. package/scripts/youtrack/verify-token.sh +0 -5
  72. package/scripts/youtrack/work-date-ms.sh +0 -5
  73. package/src/tools/docs-repo.ts +0 -51
  74. package/src/tools/flow.ts +0 -99
  75. package/src/tools/index.ts +0 -22
  76. package/src/tools/present.ts +0 -49
  77. package/src/tools/repo.ts +0 -490
  78. package/src/tools/rules.ts +0 -30
  79. package/src/tools/sdd.ts +0 -216
  80. package/src/tools/templates.ts +0 -27
  81. package/src/tools/youtrack.ts +0 -423
@@ -0,0 +1,22 @@
1
+ import { writeFileSync } from "node:fs";
2
+
3
+ // Shared credential-safe write (Task 14 Step 7 / CA-13): `wx` exclusive create
4
+ // closes the TOCTOU window between an existence check and the write, and EEXIST
5
+ // means the other writer won — their bytes are preserved instead of clobbered.
6
+ // Every token write in the wizard (ensureToken), the apply path (create-file
7
+ // mutations) and initApplyData routes through this one primitive.
8
+ export type ExclusiveWriteResult = "created" | "preserved";
9
+
10
+ export function writeFileExclusive(
11
+ file: string,
12
+ content: string,
13
+ mode?: number,
14
+ ): ExclusiveWriteResult {
15
+ try {
16
+ writeFileSync(file, content, { encoding: "utf8", flag: "wx", mode });
17
+ return "created";
18
+ } catch (err) {
19
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") return "preserved";
20
+ throw err;
21
+ }
22
+ }
@@ -1,47 +1,6 @@
1
- import { spawnSync } from "node:child_process";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
1
+ import { assetRoot, packageRoot } from "./package-root";
4
2
 
5
- const dirname = path.dirname(fileURLToPath(import.meta.url));
6
- export const PLUGIN_ROOT = path.resolve(dirname, "../..");
3
+ export const PLUGIN_ROOT = packageRoot();
4
+ export const ASSET_ROOT = assetRoot();
7
5
 
8
6
  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(
33
- scriptName: string,
34
- args: string[],
35
- workspaceRoot: string,
36
- extraEnv?: Record<string, string>,
37
- ) {
38
- const { stdout, stderr, exitCode } = runScript(scriptName, args, workspaceRoot, extraEnv);
39
- if (exitCode !== 0) {
40
- return { error: (stderr || stdout || "script failed").trim(), exitCode };
41
- }
42
- try {
43
- return { data: JSON.parse(stdout.trim()) };
44
- } catch {
45
- return { error: "invalid JSON from script", raw: stdout.trim() };
46
- }
47
- }
package/src/core/sdd.ts CHANGED
@@ -1,17 +1,20 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ statSync,
7
+ writeFileSync,
8
+ } from "node:fs";
2
9
  import { execFileSync } from "node:child_process";
3
10
  import path from "node:path";
4
11
  import { parseTasksFromPlan } from "./docs-validate";
5
12
  import { docsValidate } from "./docs-validate";
6
13
  import { readFlowState } from "./flow-state";
14
+ import { resolveCanonicalLayout, resolveDocsPath } from "./docs-layout";
7
15
 
8
16
  const posix = (p: string) => p.split(path.sep).join("/");
9
17
 
10
- export const slugFromPlan = (planPath: string) => {
11
- const dirName = path.basename(path.dirname(planPath));
12
- return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
13
- };
14
-
15
18
  /** OpenCode todowrite payload. SDD ledger is persistence, not a UI substitute. */
16
19
  export function todosFromTasks(
17
20
  tasks: { id: number | string; title: string }[],
@@ -40,13 +43,12 @@ export function sddContext({
40
43
  plan_path?: string;
41
44
  workspace_root: string;
42
45
  }) {
43
- const cwd = path.resolve(workspace_root);
44
- let resolvedSlug = slug;
45
- let resolvedPlanPath = plan_path;
46
- if (!resolvedSlug && plan_path) {
47
- resolvedPlanPath = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
48
- resolvedSlug = slugFromPlan(resolvedPlanPath);
49
- }
46
+ // One shared contained path contract (DC-01, DC-02): slug and/or plan_path
47
+ // resolve to a canonical layout or the call fails before anything is read.
48
+ const resolved = resolveCanonicalLayout({ workspace_root, slug, plan_path });
49
+ if (!resolved.ok) return { error: resolved.error };
50
+ const cwd = resolved.layout.workspace;
51
+ const resolvedSlug = resolved.layout.slug;
50
52
  if (!resolvedSlug) return { error: "slug or plan_path required" };
51
53
 
52
54
  const sdd_dir = path.posix.join("docs", resolvedSlug, "sdd");
@@ -55,7 +57,7 @@ export function sddContext({
55
57
 
56
58
  let progress_lines: string[] = [];
57
59
  let completed_task_ids: number[] = [];
58
- const absProgress = path.join(cwd, progress_path);
60
+ const absProgress = path.join(resolved.layout.sdd, "progress.md");
59
61
  if (existsSync(absProgress)) {
60
62
  progress_lines = readFileSync(absProgress, "utf8")
61
63
  .split("\n")
@@ -69,7 +71,7 @@ export function sddContext({
69
71
  }
70
72
 
71
73
  let manifest: Record<string, unknown> = {};
72
- const absManifest = path.join(cwd, manifest_path);
74
+ const absManifest = path.join(resolved.layout.sdd, "manifest.json");
73
75
  if (existsSync(absManifest)) {
74
76
  try {
75
77
  manifest = JSON.parse(readFileSync(absManifest, "utf8"));
@@ -84,12 +86,15 @@ export function sddContext({
84
86
  let todos: { id: string; content: string; status: string }[] = [];
85
87
  let task_count = 0;
86
88
  if (plan_path) {
87
- const absPlan = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
88
- const planText = readFileSync(absPlan, "utf8");
89
+ const planText = readFileSync(resolved.layout.plan, "utf8");
89
90
  const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
90
91
  const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
91
92
  if (spec_path) {
92
- const validated = docsValidate({ spec_path, plan_path, workspace_root: cwd });
93
+ const validated = docsValidate({
94
+ spec_path,
95
+ plan_path: path.relative(cwd, resolved.layout.plan),
96
+ workspace_root: cwd,
97
+ });
93
98
  if (validated.ok === false)
94
99
  return { ok: false, errors: validated.errors, error: validated.error };
95
100
  }
@@ -136,12 +141,13 @@ export function sddTaskBrief({
136
141
  section_text: string;
137
142
  workspace_root: string;
138
143
  }) {
139
- const cwd = path.resolve(workspace_root);
140
- const dir = path.isAbsolute(sdd_dir) ? sdd_dir : path.join(cwd, sdd_dir);
144
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
145
+ if (!contained.ok) return { error: contained.error };
146
+ const dir = contained.path;
141
147
  mkdirSync(dir, { recursive: true });
142
148
  const out = path.join(dir, `task-${task_id}-brief.md`);
143
149
  writeFileSync(out, `# Task ${task_id} brief\n\n${section_text}\n`, "utf8");
144
- const rel = posix(path.relative(cwd, out));
150
+ const rel = posix(path.relative(contained.base, out));
145
151
  return { brief_path: rel, task_id };
146
152
  }
147
153
 
@@ -156,16 +162,23 @@ export function sddReviewPackage({
156
162
  head_sha: string;
157
163
  workspace_root: string;
158
164
  }) {
159
- const cwd = path.resolve(workspace_root);
160
- const dir = path.isAbsolute(sdd_dir) ? sdd_dir : path.join(cwd, sdd_dir);
165
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
166
+ if (!contained.ok) return { error: contained.error };
167
+ const dir = contained.path;
161
168
  mkdirSync(dir, { recursive: true });
162
169
  const base7 = base_sha.slice(0, 7);
163
170
  const head7 = head_sha.slice(0, 7);
164
171
  const diffPath = path.join(dir, `review-${base7}..${head7}.diff`);
165
172
  try {
166
- const diff = execFileSync("git", ["diff", base_sha, head_sha], { cwd, encoding: "utf8" });
173
+ const diff = execFileSync("git", ["diff", base_sha, head_sha], {
174
+ cwd: contained.base,
175
+ encoding: "utf8",
176
+ // AR-14: a failed diff (missing shas, non-repo fixture) must stay in the
177
+ // captured error, never in the suite output.
178
+ stdio: ["pipe", "pipe", "pipe"],
179
+ });
167
180
  writeFileSync(diffPath, diff, "utf8");
168
- const rel = posix(path.relative(cwd, diffPath));
181
+ const rel = posix(path.relative(contained.base, diffPath));
169
182
  return { diff_path: rel, base_sha, head_sha, base7, head7 };
170
183
  } catch (error) {
171
184
  return { error: error instanceof Error ? error.message : "git diff failed" };
@@ -183,14 +196,18 @@ export function sddAppendProgress({
183
196
  line: string;
184
197
  workspace_root: string;
185
198
  }) {
186
- const cwd = path.resolve(workspace_root);
187
- const path_ = path.isAbsolute(progress_path) ? progress_path : path.join(cwd, progress_path);
199
+ const contained = resolveDocsPath({ workspace_root, path: progress_path });
200
+ if (!contained.ok) return { error: contained.error };
201
+ const path_ = contained.path;
188
202
  const trimmed = line.trim();
189
203
  if (!PROGRESS_RE.test(trimmed)) {
190
204
  return { error: "invalid progress line format" };
191
205
  }
206
+ if (existsSync(path_) && statSync(path_).isDirectory()) {
207
+ return { error: `progress path is a directory: ${progress_path}` };
208
+ }
192
209
  mkdirSync(path.dirname(path_), { recursive: true });
193
210
  appendFileSync(path_, trimmed + "\n", "utf8");
194
- const rel = posix(path.relative(cwd, path_));
211
+ const rel = posix(path.relative(contained.base, path_));
195
212
  return { ok: true, line: trimmed, progress_path: rel };
196
213
  }
@@ -0,0 +1,54 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isConfigObject, resolveConfigDir } from "./config";
4
+
5
+ // WZ-06: typed setup-state reader distinguishing missing from malformed
6
+ // configuration. Purely read-only — nothing here ever writes; Apply decision
7
+ // (Task 13/14) reads this state and refuses to proceed on malformed files.
8
+
9
+ export type FileState = {
10
+ file: string;
11
+ status: "missing" | "valid" | "malformed";
12
+ error?: string;
13
+ };
14
+
15
+ export type SetupState = {
16
+ configDir: string;
17
+ config: FileState;
18
+ youtrack: FileState;
19
+ vcs: FileState;
20
+ workspaces: FileState;
21
+ };
22
+
23
+ export const classifySetupFile = (dir: string, name: string): FileState => {
24
+ const file = path.join(dir, name);
25
+ let raw: string;
26
+ try {
27
+ raw = readFileSync(file, "utf8");
28
+ } catch {
29
+ // An existing-but-unreadable file (EACCES) is NOT "missing": mutations must
30
+ // never be generated against it. Report it as malformed so risky consumers
31
+ // (wizard/installer/doctor) block on the exact path.
32
+ if (existsSync(file)) {
33
+ return { file, status: "malformed", error: `${file} is not readable` };
34
+ }
35
+ return { file, status: "missing" };
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(raw);
39
+ if (!isConfigObject(parsed)) {
40
+ return { file, status: "malformed", error: `${file} is not a JSON object` };
41
+ }
42
+ } catch {
43
+ return { file, status: "malformed", error: `${file} is not valid JSON` };
44
+ }
45
+ return { file, status: "valid" };
46
+ };
47
+
48
+ export const readSetupState = (dir: string = resolveConfigDir()): SetupState => ({
49
+ configDir: dir,
50
+ config: classifySetupFile(dir, "config.json"),
51
+ youtrack: classifySetupFile(dir, "youtrack.json"),
52
+ vcs: classifySetupFile(dir, "vcs.json"),
53
+ workspaces: classifySetupFile(dir, "workspaces.json"),
54
+ });