@brainervirus/workit-core 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -7
- package/scripts/doctor-check.ts +20 -0
- package/scripts/install-cursor-plugin.sh +51 -28
- package/scripts/install-opencode-plugin.sh +19 -21
- package/scripts/rewrite-workspace-deps.ts +15 -9
- package/scripts/sync-runtime.sh +71 -19
- package/scripts/vendor-assets.ts +37 -0
- package/skills/wk-implement/SKILL.md +2 -2
- package/skills/wk-pr/SKILL.md +1 -1
- package/src/core/boundary.ts +27 -0
- package/src/core/branch-policy.ts +63 -0
- package/src/core/branch.ts +30 -16
- package/src/core/config.ts +193 -31
- package/src/core/docs-layout.ts +251 -0
- package/src/core/docs-migration.ts +639 -0
- package/src/core/docs-repo.ts +11 -9
- package/src/core/docs-validate.ts +18 -6
- package/src/core/doctor.ts +801 -0
- package/src/core/flow-state.ts +1579 -141
- package/src/core/git.ts +22 -5
- package/src/{tools/handoff.ts → core/handoff-tools.ts} +5 -57
- package/src/core/hygiene.ts +26 -12
- package/src/core/init.ts +43 -11
- package/src/core/logger.ts +321 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/ports/init-toolkit-status.ts +1 -1
- package/src/core/ports/vcs-verify-token.ts +1 -1
- package/src/core/ports/youtrack-api.ts +1 -1
- package/src/core/ports/youtrack-verify-token.ts +1 -1
- package/src/core/pr-create.ts +116 -21
- package/src/core/registration.ts +215 -0
- package/src/core/repo-context.ts +447 -0
- package/src/core/repo-tools.ts +23 -0
- package/src/core/safe-write.ts +22 -0
- package/src/core/scripts.ts +3 -44
- package/src/core/sdd.ts +45 -28
- package/src/core/setup-state.ts +54 -0
- package/src/core/setup.ts +1216 -0
- package/src/core/skill-manifests.ts +95 -0
- package/src/core/support-matrix.ts +12 -0
- package/src/core/sync-runtime.ts +348 -0
- package/src/core/templates.ts +2 -2
- package/src/core/vcs-config.ts +107 -37
- package/src/core/verify-project.ts +181 -0
- package/src/core/workspaces.ts +136 -17
- package/src/core/youtrack-tools.ts +228 -0
- package/src/core/youtrack.ts +125 -67
- package/templates/execution-contract.md +9 -7
- package/templates/superpowers-doc-contract.md +4 -3
- package/scripts/_shared/common.sh +0 -173
- package/scripts/changelog-context.sh +0 -42
- package/scripts/docs-refresh-context.sh +0 -40
- package/scripts/init/apply.sh +0 -5
- package/scripts/init/status.sh +0 -5
- package/scripts/init/toolkit-status.sh +0 -5
- package/scripts/pr-create.sh +0 -5
- package/scripts/pr-ready-context.sh +0 -88
- package/scripts/present/ascii-wireframe.sh +0 -5
- package/scripts/present/flow-diagram.sh +0 -5
- package/scripts/release-notes-context.sh +0 -40
- package/scripts/vcs/config.sh +0 -5
- package/scripts/vcs/merged-style.sh +0 -5
- package/scripts/vcs/token-create-urls.sh +0 -5
- package/scripts/vcs/verify-token.sh +0 -5
- package/scripts/verify-project.sh +0 -140
- package/scripts/youtrack/api.sh +0 -5
- package/scripts/youtrack/config.sh +0 -5
- package/scripts/youtrack/greeting.sh +0 -5
- package/scripts/youtrack/parse-duration.sh +0 -5
- package/scripts/youtrack/token-create-url.sh +0 -5
- package/scripts/youtrack/verify-token.sh +0 -5
- package/scripts/youtrack/work-date-ms.sh +0 -5
- package/src/tools/docs-repo.ts +0 -51
- package/src/tools/flow.ts +0 -99
- package/src/tools/index.ts +0 -22
- package/src/tools/present.ts +0 -49
- package/src/tools/repo.ts +0 -490
- package/src/tools/rules.ts +0 -30
- package/src/tools/sdd.ts +0 -216
- package/src/tools/templates.ts +0 -27
- package/src/tools/youtrack.ts +0 -423
|
@@ -0,0 +1,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
|
+
}
|
package/src/core/scripts.ts
CHANGED
|
@@ -1,47 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { assetRoot, packageRoot } from "./package-root";
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
export const
|
|
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 {
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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({
|
|
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
|
|
140
|
-
|
|
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(
|
|
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
|
|
160
|
-
|
|
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], {
|
|
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(
|
|
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
|
|
187
|
-
|
|
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(
|
|
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
|
+
});
|