@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.
Files changed (95) hide show
  1. package/package.json +10 -14
  2. package/scripts/doctor-check.ts +20 -0
  3. package/scripts/install-cursor-plugin.sh +51 -28
  4. package/scripts/install-opencode-plugin.sh +25 -22
  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 +166 -57
  13. package/src/core/changelog.ts +17 -14
  14. package/src/core/config-guard.ts +9 -2
  15. package/src/core/config.ts +227 -32
  16. package/src/core/detector.ts +22 -11
  17. package/src/core/docs-layout.ts +251 -0
  18. package/src/core/docs-migration.ts +639 -0
  19. package/src/core/docs-repo.ts +58 -21
  20. package/src/core/docs-validate.ts +181 -43
  21. package/src/core/doctor.ts +801 -0
  22. package/src/core/flow-state.ts +1579 -137
  23. package/src/core/git.ts +22 -5
  24. package/src/core/gitignore.ts +11 -2
  25. package/src/core/handoff-context.ts +18 -5
  26. package/src/{tools/handoff.ts → core/handoff-tools.ts} +16 -63
  27. package/src/core/hygiene.ts +53 -17
  28. package/src/core/init.ts +128 -31
  29. package/src/core/logger.ts +321 -0
  30. package/src/core/package-root.ts +28 -0
  31. package/src/core/parse-sections.ts +2 -2
  32. package/src/core/plan-tasks.ts +13 -3
  33. package/src/core/ports/init-toolkit-status.ts +1 -1
  34. package/src/core/ports/vcs-verify-token.ts +1 -1
  35. package/src/core/ports/youtrack-api.ts +4 -2
  36. package/src/core/ports/youtrack-config.ts +1 -3
  37. package/src/core/ports/youtrack-verify-token.ts +1 -1
  38. package/src/core/pr-create.ts +156 -29
  39. package/src/core/present.ts +11 -2
  40. package/src/core/registration.ts +215 -0
  41. package/src/core/reminder.ts +1 -2
  42. package/src/core/repo-context.ts +447 -0
  43. package/src/core/repo-tool.ts +4 -1
  44. package/src/core/repo-tools.ts +23 -0
  45. package/src/core/rules.ts +10 -7
  46. package/src/core/safe-write.ts +22 -0
  47. package/src/core/scripts.ts +3 -39
  48. package/src/core/sdd.ts +56 -31
  49. package/src/core/setup-state.ts +54 -0
  50. package/src/core/setup.ts +1216 -0
  51. package/src/core/skill-manifests.ts +95 -0
  52. package/src/core/support-matrix.ts +12 -0
  53. package/src/core/sync-runtime.ts +348 -0
  54. package/src/core/templates.ts +16 -6
  55. package/src/core/vcs-config.ts +191 -62
  56. package/src/core/verify-parse.ts +4 -2
  57. package/src/core/verify-project.ts +181 -0
  58. package/src/core/workspaces.ts +136 -17
  59. package/src/core/youtrack-tools.ts +228 -0
  60. package/src/core/youtrack.ts +320 -87
  61. package/src/core.ts +18 -3
  62. package/templates/execution-contract.md +9 -7
  63. package/templates/superpowers-doc-contract.md +5 -4
  64. package/scripts/_shared/common.sh +0 -158
  65. package/scripts/changelog-context.sh +0 -42
  66. package/scripts/docs-refresh-context.sh +0 -40
  67. package/scripts/init/apply.sh +0 -5
  68. package/scripts/init/status.sh +0 -5
  69. package/scripts/init/toolkit-status.sh +0 -5
  70. package/scripts/pr-create.sh +0 -5
  71. package/scripts/pr-ready-context.sh +0 -88
  72. package/scripts/present/ascii-wireframe.sh +0 -5
  73. package/scripts/present/flow-diagram.sh +0 -5
  74. package/scripts/release-notes-context.sh +0 -40
  75. package/scripts/vcs/config.sh +0 -5
  76. package/scripts/vcs/merged-style.sh +0 -5
  77. package/scripts/vcs/token-create-urls.sh +0 -5
  78. package/scripts/vcs/verify-token.sh +0 -5
  79. package/scripts/verify-project.sh +0 -140
  80. package/scripts/youtrack/api.sh +0 -5
  81. package/scripts/youtrack/config.sh +0 -5
  82. package/scripts/youtrack/greeting.sh +0 -5
  83. package/scripts/youtrack/parse-duration.sh +0 -5
  84. package/scripts/youtrack/token-create-url.sh +0 -5
  85. package/scripts/youtrack/verify-token.sh +0 -5
  86. package/scripts/youtrack/work-date-ms.sh +0 -5
  87. package/src/tools/docs-repo.ts +0 -42
  88. package/src/tools/flow.ts +0 -88
  89. package/src/tools/index.ts +0 -22
  90. package/src/tools/present.ts +0 -45
  91. package/src/tools/repo.ts +0 -357
  92. package/src/tools/rules.ts +0 -30
  93. package/src/tools/sdd.ts +0 -189
  94. package/src/tools/templates.ts +0 -27
  95. package/src/tools/youtrack.ts +0 -360
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,9 +57,12 @@ 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
- progress_lines = readFileSync(absProgress, "utf8").split("\n").map((ln) => ln.trim()).filter(Boolean);
62
+ progress_lines = readFileSync(absProgress, "utf8")
63
+ .split("\n")
64
+ .map((ln) => ln.trim())
65
+ .filter(Boolean);
61
66
  const pat = /^Task\s+(\d+):\s+complete\b/i;
62
67
  completed_task_ids = progress_lines
63
68
  .map((ln) => pat.exec(ln)?.[1])
@@ -66,9 +71,13 @@ export function sddContext({
66
71
  }
67
72
 
68
73
  let manifest: Record<string, unknown> = {};
69
- const absManifest = path.join(cwd, manifest_path);
74
+ const absManifest = path.join(resolved.layout.sdd, "manifest.json");
70
75
  if (existsSync(absManifest)) {
71
- try { manifest = JSON.parse(readFileSync(absManifest, "utf8")); } catch { manifest = {}; }
76
+ try {
77
+ manifest = JSON.parse(readFileSync(absManifest, "utf8"));
78
+ } catch {
79
+ manifest = {};
80
+ }
72
81
  }
73
82
 
74
83
  const legacy_path = path.join(cwd, ".superpowers/sdd");
@@ -77,13 +86,17 @@ export function sddContext({
77
86
  let todos: { id: string; content: string; status: string }[] = [];
78
87
  let task_count = 0;
79
88
  if (plan_path) {
80
- const absPlan = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
81
- const planText = readFileSync(absPlan, "utf8");
89
+ const planText = readFileSync(resolved.layout.plan, "utf8");
82
90
  const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
83
91
  const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
84
92
  if (spec_path) {
85
- const validated = docsValidate({ spec_path, plan_path, workspace_root: cwd });
86
- if (validated.ok === false) return { ok: false, errors: validated.errors, error: validated.error };
93
+ const validated = docsValidate({
94
+ spec_path,
95
+ plan_path: path.relative(cwd, resolved.layout.plan),
96
+ workspace_root: cwd,
97
+ });
98
+ if (validated.ok === false)
99
+ return { ok: false, errors: validated.errors, error: validated.error };
87
100
  }
88
101
  const tasks = parseTasksFromPlan(planText);
89
102
  if (tasks.length > 0) {
@@ -128,12 +141,13 @@ export function sddTaskBrief({
128
141
  section_text: string;
129
142
  workspace_root: string;
130
143
  }) {
131
- const cwd = path.resolve(workspace_root);
132
- 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;
133
147
  mkdirSync(dir, { recursive: true });
134
148
  const out = path.join(dir, `task-${task_id}-brief.md`);
135
149
  writeFileSync(out, `# Task ${task_id} brief\n\n${section_text}\n`, "utf8");
136
- const rel = posix(path.relative(cwd, out));
150
+ const rel = posix(path.relative(contained.base, out));
137
151
  return { brief_path: rel, task_id };
138
152
  }
139
153
 
@@ -148,16 +162,23 @@ export function sddReviewPackage({
148
162
  head_sha: string;
149
163
  workspace_root: string;
150
164
  }) {
151
- const cwd = path.resolve(workspace_root);
152
- 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;
153
168
  mkdirSync(dir, { recursive: true });
154
169
  const base7 = base_sha.slice(0, 7);
155
170
  const head7 = head_sha.slice(0, 7);
156
171
  const diffPath = path.join(dir, `review-${base7}..${head7}.diff`);
157
172
  try {
158
- 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
+ });
159
180
  writeFileSync(diffPath, diff, "utf8");
160
- const rel = posix(path.relative(cwd, diffPath));
181
+ const rel = posix(path.relative(contained.base, diffPath));
161
182
  return { diff_path: rel, base_sha, head_sha, base7, head7 };
162
183
  } catch (error) {
163
184
  return { error: error instanceof Error ? error.message : "git diff failed" };
@@ -175,14 +196,18 @@ export function sddAppendProgress({
175
196
  line: string;
176
197
  workspace_root: string;
177
198
  }) {
178
- const cwd = path.resolve(workspace_root);
179
- 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;
180
202
  const trimmed = line.trim();
181
203
  if (!PROGRESS_RE.test(trimmed)) {
182
204
  return { error: "invalid progress line format" };
183
205
  }
206
+ if (existsSync(path_) && statSync(path_).isDirectory()) {
207
+ return { error: `progress path is a directory: ${progress_path}` };
208
+ }
184
209
  mkdirSync(path.dirname(path_), { recursive: true });
185
210
  appendFileSync(path_, trimmed + "\n", "utf8");
186
- const rel = posix(path.relative(cwd, path_));
211
+ const rel = posix(path.relative(contained.base, path_));
187
212
  return { ok: true, line: trimmed, progress_path: rel };
188
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
+ });