@brainervirus/workit-core 0.5.6 → 0.6.1
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/README.md +4 -17
- package/package.json +9 -9
- package/scripts/_shared/common.sh +18 -3
- package/scripts/install-opencode-plugin.sh +14 -9
- package/scripts/lib/config-dir.sh +26 -0
- package/scripts/sync-runtime.sh +4 -1
- package/src/core/branch.ts +143 -48
- package/src/core/changelog.ts +17 -14
- package/src/core/config-guard.ts +9 -2
- package/src/core/config.ts +95 -15
- package/src/core/detector.ts +22 -11
- package/src/core/docs-repo.ts +50 -15
- package/src/core/docs-validate.ts +163 -37
- package/src/core/flow-state.ts +6 -2
- package/src/core/gitignore.ts +11 -2
- package/src/core/handoff-context.ts +18 -5
- package/src/core/hygiene.ts +27 -5
- package/src/core/init.ts +86 -21
- package/src/core/parse-sections.ts +2 -2
- package/src/core/plan-tasks.ts +13 -3
- package/src/core/ports/youtrack-api.ts +3 -1
- package/src/core/ports/youtrack-config.ts +1 -3
- package/src/core/pr-create.ts +47 -15
- package/src/core/present.ts +11 -2
- package/src/core/reminder.ts +1 -2
- package/src/core/repo-tool.ts +4 -1
- package/src/core/rules.ts +10 -7
- package/src/core/scripts.ts +7 -2
- package/src/core/sdd.ts +11 -3
- package/src/core/templates.ts +14 -4
- package/src/core/vcs-config.ts +94 -36
- package/src/core/verify-parse.ts +4 -2
- package/src/core/workspaces.ts +2 -2
- package/src/core/youtrack.ts +233 -58
- package/src/core.ts +18 -3
- package/src/tools/docs-repo.ts +12 -3
- package/src/tools/flow.ts +24 -13
- package/src/tools/handoff.ts +28 -23
- package/src/tools/present.ts +14 -10
- package/src/tools/repo.ts +220 -87
- package/src/tools/sdd.ts +93 -66
- package/src/tools/youtrack.ts +119 -52
- package/templates/superpowers-doc-contract.md +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Multi-platform Superpowers workflow plugin for **Cursor** and **OpenCode**: veri
|
|
|
9
9
|
| **Shared core** | `packages/workit-core/` (src, skills, commands, scripts, templates) |  |
|
|
10
10
|
| **CLI** | `packages/workit-cli/` (Ink wizard, bin `workit`) |  |
|
|
11
11
|
|
|
12
|
-
Config directory (both platforms): `~/.config/workflow-toolkit/`
|
|
12
|
+
Config directory (both platforms): `~/.config/workit/` — legacy `~/.config/workflow-toolkit/` is auto-migrated on first run and kept as a fallback.
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/@brainervirus/workit-opencode)
|
|
15
15
|
[](https://github.com/BrainerVirus/workit/actions)
|
|
@@ -119,7 +119,6 @@ GitHub Actions:
|
|
|
119
119
|
|
|
120
120
|
- **CI** — on push/PR to `main`: matrix of 3 OS (ubuntu, macos, windows) running `actions/checkout@v7` + `oven-sh/setup-bun@v2`, then `bun install --frozen-lockfile` + `bun run check`
|
|
121
121
|
- **Release** — on push to `main`: [semantic-release](https://github.com/semantic-release/semantic-release) computes the next version from Conventional Commits, publishes the four workspaces to npm in dependency order — `workit-core`, `workit-opencode`, `workit-cursor`, `workit-cli` (with provenance), and creates the git tag + GitHub Release
|
|
122
|
-
- **Code review** — on every PR: the [OpenCode GitHub integration](https://opencode.ai/docs/github/) reviews the diff (see [Code review](#code-review))
|
|
123
122
|
|
|
124
123
|
No manual tags — semantic-release owns the version/tag flow:
|
|
125
124
|
|
|
@@ -127,22 +126,10 @@ No manual tags — semantic-release owns the version/tag flow:
|
|
|
127
126
|
git push origin main
|
|
128
127
|
```
|
|
129
128
|
|
|
130
|
-
##
|
|
129
|
+
## Quality gates
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Checks fall into two categories:
|
|
135
|
-
|
|
136
|
-
- **BLOCKING — the `pull_request` review** (`review` job): runs on every PR (opened/synchronized/reopened/ready_for_review) and is registered as a required check in branch protection — the PR cannot merge while it's red.
|
|
137
|
-
- **ADVISORY — on-demand and triage flows** (never block merge): mention `/oc` or `/opencode` in a comment — issue, PR thread, or a specific line in the Files tab — to have OpenCode fix, explain, or update the PR (`oc` job); new issues are triaged with guidance and doc links (`triage` job).
|
|
138
|
-
|
|
139
|
-
To activate the review on a fork or fresh clone:
|
|
140
|
-
|
|
141
|
-
1. Install the [OpenCode GitHub app](https://github.com/apps/opencode-agent) on the repository.
|
|
142
|
-
2. Add the `OPENCODE_API_KEY` secret under Settings → Secrets and variables → Actions.
|
|
143
|
-
3. PRs trigger the review on opened/synchronized/reopened events automatically.
|
|
144
|
-
|
|
145
|
-
[Cursor Bugbot](https://cursor.com/dashboard/bugbot) is an optional alternative to the OpenCode review.
|
|
131
|
+
- **CI checks** — the per-package check jobs (workit-core/opencode/cursor/cli/shared) are required in branch protection; the PR cannot merge while any is red.
|
|
132
|
+
- **Subagent review** — `wk-implement` runs a two-stage review (spec compliance + code quality) per task with fresh `general` agents.
|
|
146
133
|
|
|
147
134
|
## Architecture
|
|
148
135
|
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
|
|
6
|
-
"license": "MIT",
|
|
7
6
|
"keywords": [
|
|
8
|
-
"
|
|
7
|
+
"agent",
|
|
9
8
|
"cursor",
|
|
10
|
-
"workflow",
|
|
11
9
|
"mcp",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
10
|
+
"opencode",
|
|
11
|
+
"workflow",
|
|
12
|
+
"youTrack"
|
|
14
13
|
],
|
|
15
14
|
"bugs": {
|
|
16
15
|
"url": "https://github.com/BrainerVirus/workit/issues"
|
|
17
16
|
},
|
|
17
|
+
"license": "MIT",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
20
|
"url": "https://github.com/BrainerVirus/workit.git"
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"./src/*": "./src/*.ts",
|
|
38
38
|
"./package.json": "./package.json"
|
|
39
39
|
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"typecheck": "tsc --noEmit"
|
|
42
|
-
},
|
|
43
40
|
"publishConfig": {
|
|
44
41
|
"access": "public"
|
|
45
42
|
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
48
48
|
"zod": "^3.24.0"
|
|
@@ -53,7 +53,6 @@ is_pr_branch() {
|
|
|
53
53
|
esac
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
# ponytail: develop-only base — main is release-only in this workflow; never compare PRs to main
|
|
57
56
|
resolve_pr_branch_context() {
|
|
58
57
|
branch=$(current_branch)
|
|
59
58
|
|
|
@@ -74,7 +73,23 @@ resolve_pr_branch_context() {
|
|
|
74
73
|
best_ref=""
|
|
75
74
|
best_mb=""
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
resolved=$(bash "$SCRIPT_DIR/vcs/config.sh" resolve 2>/dev/null) || {
|
|
77
|
+
printf 'ERROR: cannot resolve the configured PR target branch\n' >&2
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
base=$(printf '%s\n' "$resolved" | bun -e '
|
|
81
|
+
const value = JSON.parse(await Bun.stdin.text()).defaultTargetBranch;
|
|
82
|
+
if (typeof value === "string" && value) process.stdout.write(value);
|
|
83
|
+
') || {
|
|
84
|
+
printf 'ERROR: invalid VCS target-branch configuration\n' >&2
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
if [ "$base" = "" ] || ! git check-ref-format --branch "$base" >/dev/null 2>&1; then
|
|
88
|
+
printf 'ERROR: invalid configured PR target branch %s\n' "$base" >&2
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
for ref in "origin/$base" "$base"; do
|
|
78
93
|
git rev-parse --verify "$ref" >/dev/null 2>&1 || continue
|
|
79
94
|
mb=$(git merge-base "$ref" HEAD 2>/dev/null) || continue
|
|
80
95
|
best_ref=$ref
|
|
@@ -83,7 +98,7 @@ resolve_pr_branch_context() {
|
|
|
83
98
|
done
|
|
84
99
|
|
|
85
100
|
if [ "$best_ref" = "" ] || [ "$best_mb" = "" ]; then
|
|
86
|
-
printf 'ERROR:
|
|
101
|
+
printf 'ERROR: configured PR target branch %s not found — fetch/checkout it or pass an explicit git range\n' "$base" >&2
|
|
87
102
|
return 1
|
|
88
103
|
fi
|
|
89
104
|
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
6
|
-
ROOT="$(CDPATH= cd -- "$SCRIPT_DIR
|
|
6
|
+
ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)"
|
|
7
|
+
CORE_SCRIPTS="$ROOT/packages/workit-core/scripts"
|
|
7
8
|
SHARE="${HOME}/.local/share/workflow-toolkit"
|
|
8
|
-
|
|
9
|
-
DEV="${WORKFLOW_TOOLKIT_DEV:-$DEV_DEFAULT}"
|
|
9
|
+
DEV="${WORKFLOW_TOOLKIT_DEV:-$ROOT}"
|
|
10
10
|
CONFIG="${HOME}/.config/opencode/opencode.json"
|
|
11
11
|
|
|
12
|
-
chmod +x "$
|
|
13
|
-
WORKFLOW_TOOLKIT_DEV="$ROOT" "$
|
|
12
|
+
chmod +x "$CORE_SCRIPTS/sync-runtime.sh"
|
|
13
|
+
WORKFLOW_TOOLKIT_DEV="$ROOT" "$CORE_SCRIPTS/sync-runtime.sh"
|
|
14
14
|
|
|
15
15
|
# Prefer monorepo with .git. file:// pins skip opencode's bundled npm installer —
|
|
16
16
|
# git+file:// installs an EMPTY cache dir and fails silently, so the plugin never
|
|
@@ -40,9 +40,14 @@ const pin = process.env.PIN_PATH!;
|
|
|
40
40
|
const data = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
41
41
|
let plugins = data.plugin || [];
|
|
42
42
|
if (typeof plugins === "string") plugins = [plugins];
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// Load the dev pin first and remove every stale/current Workit identity.
|
|
44
|
+
const isWorkit = (p) => {
|
|
45
|
+
const value = String(p);
|
|
46
|
+
return value.includes("workflow-toolkit") ||
|
|
47
|
+
value.includes("@brainervirus/workit-opencode") ||
|
|
48
|
+
value.includes("/packages/workit-opencode/");
|
|
49
|
+
};
|
|
50
|
+
data.plugin = [pin, ...plugins.filter((p) => !isWorkit(p))];
|
|
46
51
|
// Drop share skills.paths — native ~/.config/opencode/skills links avoid triple-load dups
|
|
47
52
|
const skills = data.skills;
|
|
48
53
|
if (skills && typeof skills === "object") {
|
|
@@ -73,4 +78,4 @@ if [ ! -s "$PLUGIN_ENTRY" ]; then
|
|
|
73
78
|
exit 1
|
|
74
79
|
fi
|
|
75
80
|
|
|
76
|
-
echo "OpenCode install done. Fully quit all opencode processes, then restart."
|
|
81
|
+
echo "OpenCode install done. Fully quit all opencode processes, then restart."
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Resolve the config dir (workit default) with one-time legacy migration.
|
|
3
|
+
# Mirrors src/core/config.ts resolveConfigDir + ensureConfigDir.
|
|
4
|
+
# Env overrides bypass migration (explicit user intent wins); migration is
|
|
5
|
+
# idempotent: only runs when workit/ is absent; never overwrites, never
|
|
6
|
+
# deletes the legacy workflow-toolkit/ dir.
|
|
7
|
+
resolve_config_dir() {
|
|
8
|
+
if [ -n "${WORKFLOW_TOOLKIT_CONFIG:-}" ]; then
|
|
9
|
+
printf '%s\n' "$WORKFLOW_TOOLKIT_CONFIG"
|
|
10
|
+
return
|
|
11
|
+
fi
|
|
12
|
+
if [ -n "${WORKFLOW_TOOLKIT_CONFIG_DIR:-}" ]; then
|
|
13
|
+
printf '%s\n' "$WORKFLOW_TOOLKIT_CONFIG_DIR"
|
|
14
|
+
return
|
|
15
|
+
fi
|
|
16
|
+
local base="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
17
|
+
local NEW="$base/workit"
|
|
18
|
+
local LEGACY="$base/workflow-toolkit"
|
|
19
|
+
if [ ! -d "$NEW" ] && [ -d "$LEGACY" ]; then
|
|
20
|
+
mkdir -p "$NEW"
|
|
21
|
+
if ! cp -r "$LEGACY/." "$NEW/"; then
|
|
22
|
+
printf 'workit: warning: partial config migration from %s (kept in place; retry on next run)\n' "$LEGACY" >&2
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
printf '%s\n' "$NEW"
|
|
26
|
+
}
|
package/scripts/sync-runtime.sh
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
# Prefer local monorepo (dev) when present; otherwise git-pull the share clone.
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
7
|
+
. "$SCRIPT_DIR/lib/config-dir.sh"
|
|
8
|
+
|
|
6
9
|
SHARE="${HOME}/.local/share/workflow-toolkit"
|
|
7
10
|
PLUGIN_DIR="${HOME}/.cursor/plugins/local/workflow-toolkit"
|
|
8
11
|
OPENCODE_PLUGINS="${HOME}/.config/opencode/plugins"
|
|
@@ -54,7 +57,7 @@ if [ -d "$SHARE/packages/workit-core/vendor/superpowers/skills" ]; then
|
|
|
54
57
|
rsync -a --delete "$SHARE/packages/workit-core/vendor/superpowers/skills" "$PLUGIN_DIR/vendor/superpowers/"
|
|
55
58
|
fi
|
|
56
59
|
# Canonical user rules -> Cursor .mdc (compiled by the shared core)
|
|
57
|
-
CONFIG_RULES_DIR="$
|
|
60
|
+
CONFIG_RULES_DIR="$(resolve_config_dir)/rules"
|
|
58
61
|
if [ -d "$CONFIG_RULES_DIR" ]; then
|
|
59
62
|
"$HOME/.bun/bin/bun" -e "
|
|
60
63
|
import('${SHARE}/packages/workit-core/src/core/rules.ts').then(async ({ writeCompiledCursorRules }) => {
|
package/src/core/branch.ts
CHANGED
|
@@ -3,22 +3,36 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { gitContext } from "./git";
|
|
5
5
|
import { readConfig, resolveBranchPolicy } from "./config";
|
|
6
|
+
import { vcsConfig } from "./vcs-config";
|
|
6
7
|
|
|
7
8
|
const policy = () => resolveBranchPolicy(readConfig());
|
|
8
9
|
const allowedBranch = (name: string) => policy().allowed.some((r) => r.test(name));
|
|
9
10
|
const isProtected = (name: string) => policy().protected.has(name.toLowerCase());
|
|
11
|
+
const baseBranch = (cwd: string) =>
|
|
12
|
+
String(vcsConfig("resolve", cwd).defaultTargetBranch ?? "develop");
|
|
10
13
|
const DECLARE_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/gim;
|
|
11
14
|
const USE_CURRENT_RE = /^\s*\*+Branch:\*+\s*use-current\s*$/im;
|
|
12
15
|
const readSafe = (p: string): string | null => {
|
|
13
|
-
try {
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(p, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
const normalizeBranch = (name: string): string | null => {
|
|
17
24
|
const n = name.trim().replace(/`/g, "").replace(/\.+$/, "");
|
|
18
25
|
if (isProtected(n)) return null;
|
|
19
26
|
if (!allowedBranch(n)) return null;
|
|
20
|
-
const parts = n
|
|
21
|
-
|
|
27
|
+
const parts = n
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.split("/")
|
|
30
|
+
.map((p) =>
|
|
31
|
+
p
|
|
32
|
+
.replace(/[^\w.-]+/g, "-")
|
|
33
|
+
.replace(/^-+|-+$/g, "")
|
|
34
|
+
.replace(/-{2,}/g, "-"),
|
|
35
|
+
);
|
|
22
36
|
if (parts.some((p) => !p)) return null;
|
|
23
37
|
return parts.join("/");
|
|
24
38
|
};
|
|
@@ -29,14 +43,21 @@ const deriveSlug = (planPath: string): string => {
|
|
|
29
43
|
return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
|
|
30
44
|
};
|
|
31
45
|
|
|
32
|
-
const deriveKind = (
|
|
46
|
+
const deriveKind = (
|
|
47
|
+
planPath: string,
|
|
48
|
+
fallback: "feature" | "bugfix" = "feature",
|
|
49
|
+
): "feature" | "bugfix" => {
|
|
33
50
|
const slug = deriveSlug(planPath);
|
|
34
51
|
const text = readSafe(planPath) ?? "";
|
|
35
52
|
let kind = fallback;
|
|
36
53
|
if (/\bbugfix\b/i.test(slug) || /^fix-/i.test(slug)) {
|
|
37
54
|
kind = "bugfix";
|
|
38
55
|
} else {
|
|
39
|
-
const goal =
|
|
56
|
+
const goal =
|
|
57
|
+
text
|
|
58
|
+
.split("\n")
|
|
59
|
+
.find((line) => line.startsWith("**Goal:**"))
|
|
60
|
+
?.toLowerCase() ?? "";
|
|
40
61
|
if (/\b(bugfix|bug fix)\b/.test(goal) && !/\b(feat|feature|upgrade|add)\b/.test(goal)) {
|
|
41
62
|
kind = "bugfix";
|
|
42
63
|
}
|
|
@@ -49,7 +70,11 @@ export const resolveBranch = ({
|
|
|
49
70
|
spec_path,
|
|
50
71
|
plan_path,
|
|
51
72
|
workspace_root,
|
|
52
|
-
}: {
|
|
73
|
+
}: {
|
|
74
|
+
spec_path: string;
|
|
75
|
+
plan_path: string;
|
|
76
|
+
workspace_root: string;
|
|
77
|
+
}) => {
|
|
53
78
|
const cwd = path.resolve(workspace_root);
|
|
54
79
|
const abs = (p: string) => (path.isAbsolute(p) ? p : path.join(cwd, p));
|
|
55
80
|
const spec = abs(spec_path);
|
|
@@ -76,7 +101,8 @@ export const resolveBranch = ({
|
|
|
76
101
|
}
|
|
77
102
|
}
|
|
78
103
|
|
|
79
|
-
if (current && allowedBranch(current) && !isProtected(current))
|
|
104
|
+
if (current && allowedBranch(current) && !isProtected(current))
|
|
105
|
+
return finish(current, "keep-current");
|
|
80
106
|
|
|
81
107
|
let declaredButInvalid: string | null = null;
|
|
82
108
|
for (const file of [spec, plan]) {
|
|
@@ -89,7 +115,9 @@ export const resolveBranch = ({
|
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
117
|
if (declaredButInvalid) {
|
|
92
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy`,
|
|
120
|
+
};
|
|
93
121
|
}
|
|
94
122
|
|
|
95
123
|
const slug = deriveSlug(plan);
|
|
@@ -103,55 +131,101 @@ export const docsBranch = ({
|
|
|
103
131
|
plan_path,
|
|
104
132
|
kind,
|
|
105
133
|
workspace_root,
|
|
106
|
-
}: {
|
|
134
|
+
}: {
|
|
135
|
+
plan_path?: string;
|
|
136
|
+
kind?: string;
|
|
137
|
+
workspace_root: string;
|
|
138
|
+
}) => {
|
|
107
139
|
const cwd = path.resolve(workspace_root);
|
|
108
140
|
const git = gitContext(cwd);
|
|
109
141
|
const current = git.branch;
|
|
110
142
|
const kindArg = (kind ?? "feature").toLowerCase();
|
|
143
|
+
const base = baseBranch(cwd);
|
|
111
144
|
|
|
112
|
-
if (current
|
|
113
|
-
return { branch: current, action: "keep", current_branch: current, base: "develop", dirty: Boolean(git.status_short.trim()) };
|
|
114
|
-
}
|
|
115
|
-
if (current === "main" || current === "master" || current === "develop") {
|
|
145
|
+
if (current === base || current === "main" || current === "master" || current === "develop") {
|
|
116
146
|
let slug = "";
|
|
117
147
|
if (plan_path) {
|
|
118
148
|
const plan = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
|
|
119
149
|
slug = deriveSlug(plan);
|
|
120
150
|
}
|
|
121
151
|
if (!slug) {
|
|
122
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
error: "plan_path required to derive branch slug when not on feature/* or bugfix/*",
|
|
154
|
+
};
|
|
123
155
|
}
|
|
124
156
|
const branchKind = kindArg === "bugfix" ? "bugfix" : "feature";
|
|
125
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
branch: `${branchKind}/${slug}`,
|
|
159
|
+
action: base === "develop" ? "create_from_develop" : "create_from_base",
|
|
160
|
+
current_branch: current,
|
|
161
|
+
base,
|
|
162
|
+
dirty: Boolean(git.status_short.trim()),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (current && allowedBranch(current) && !isProtected(current)) {
|
|
166
|
+
return {
|
|
167
|
+
branch: current,
|
|
168
|
+
action: "keep",
|
|
169
|
+
current_branch: current,
|
|
170
|
+
base,
|
|
171
|
+
dirty: Boolean(git.status_short.trim()),
|
|
172
|
+
};
|
|
126
173
|
}
|
|
127
174
|
return { error: `cannot resolve docs branch from HEAD ${JSON.stringify(current)}` };
|
|
128
175
|
};
|
|
129
176
|
|
|
130
177
|
// Port of scripts/lib/ensure-develop-base.sh
|
|
131
|
-
export const
|
|
178
|
+
export const ensureBaseBranch = (cwd: string, base: string): { ok: boolean; error?: string } => {
|
|
132
179
|
const git = gitContext(cwd);
|
|
133
|
-
if (!git.branch || git.branch === "unknown")
|
|
134
|
-
|
|
180
|
+
if (!git.branch || git.branch === "unknown")
|
|
181
|
+
return { ok: false, error: "not in a git repository" };
|
|
182
|
+
const run = (args: string[]) =>
|
|
183
|
+
execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
135
184
|
try {
|
|
136
185
|
try {
|
|
137
|
-
run(["fetch", "origin",
|
|
186
|
+
run(["fetch", "origin", base, "--prune"]);
|
|
138
187
|
} catch {
|
|
139
188
|
run(["fetch", "origin", "--prune"]);
|
|
140
189
|
}
|
|
141
|
-
let
|
|
142
|
-
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
let hasOriginBase = true;
|
|
191
|
+
try {
|
|
192
|
+
execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
193
|
+
cwd,
|
|
194
|
+
stdio: "pipe",
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
hasOriginBase = false;
|
|
198
|
+
}
|
|
199
|
+
if (!hasOriginBase)
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`,
|
|
203
|
+
};
|
|
204
|
+
let hasLocalBase = true;
|
|
205
|
+
try {
|
|
206
|
+
execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
207
|
+
cwd,
|
|
208
|
+
stdio: "pipe",
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
hasLocalBase = false;
|
|
212
|
+
}
|
|
213
|
+
if (hasLocalBase) {
|
|
214
|
+
run(["checkout", base]);
|
|
215
|
+
try {
|
|
216
|
+
run(["merge", "--ff-only", `origin/${base}`]);
|
|
217
|
+
} catch {
|
|
218
|
+
/* non-fast-forward: keep local */
|
|
219
|
+
}
|
|
149
220
|
} else {
|
|
150
|
-
run(["checkout", "-b",
|
|
221
|
+
run(["checkout", "-b", base, "--track", `origin/${base}`]);
|
|
151
222
|
}
|
|
152
223
|
return { ok: true };
|
|
153
224
|
} catch (error) {
|
|
154
|
-
return {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: error instanceof Error ? error.message : "ensure-base-branch failed",
|
|
228
|
+
};
|
|
155
229
|
}
|
|
156
230
|
};
|
|
157
231
|
|
|
@@ -162,17 +236,29 @@ export const branchSetup = ({
|
|
|
162
236
|
target_branch,
|
|
163
237
|
stash,
|
|
164
238
|
workspace_root,
|
|
165
|
-
}: {
|
|
239
|
+
}: {
|
|
240
|
+
action?: string;
|
|
241
|
+
sdd_dir?: string;
|
|
242
|
+
target_branch?: string;
|
|
243
|
+
stash?: string;
|
|
244
|
+
workspace_root: string;
|
|
245
|
+
}) => {
|
|
166
246
|
const cwd = path.resolve(workspace_root);
|
|
167
247
|
const exec = (args: string[]): string =>
|
|
168
248
|
execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
169
249
|
const current = gitContext(cwd).branch;
|
|
170
250
|
if (!current || current === "unknown") return { error: "not in a git repository" };
|
|
171
251
|
const sdd = sdd_dir ?? "docs";
|
|
172
|
-
const manifestPath = path.isAbsolute(sdd)
|
|
252
|
+
const manifestPath = path.isAbsolute(sdd)
|
|
253
|
+
? path.join(sdd, "manifest.json")
|
|
254
|
+
: path.join(cwd, sdd, "manifest.json");
|
|
173
255
|
mkdirSync(path.dirname(manifestPath), { recursive: true, mode: 0o755 });
|
|
174
256
|
const readManifest = (): Record<string, unknown> => {
|
|
175
|
-
try {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
259
|
+
} catch {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
176
262
|
};
|
|
177
263
|
const writeManifest = (data: Record<string, unknown>) =>
|
|
178
264
|
writeFileSync(manifestPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
@@ -181,7 +267,9 @@ export const branchSetup = ({
|
|
|
181
267
|
const manifest = readManifest();
|
|
182
268
|
const ref = manifest.stash_ref;
|
|
183
269
|
if (!ref) return { error: "no stash_ref in manifest" };
|
|
184
|
-
try {
|
|
270
|
+
try {
|
|
271
|
+
exec(["stash", "pop", String(ref)]);
|
|
272
|
+
} catch (error) {
|
|
185
273
|
return { error: error instanceof Error ? error.message : "stash pop failed" };
|
|
186
274
|
}
|
|
187
275
|
delete manifest.stash_ref;
|
|
@@ -193,14 +281,18 @@ export const branchSetup = ({
|
|
|
193
281
|
const target = target_branch ?? "";
|
|
194
282
|
if (!target) return { error: "target branch required" };
|
|
195
283
|
if (isProtected(target)) return { error: `protected branch ${target}` };
|
|
196
|
-
if (!allowedBranch(target))
|
|
284
|
+
if (!allowedBranch(target))
|
|
285
|
+
return { error: `target branch ${target} is not allowed by the branch policy` };
|
|
197
286
|
|
|
198
287
|
let stash_ref: string | undefined;
|
|
199
288
|
if (current !== target) {
|
|
200
289
|
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
201
290
|
if (dirty) {
|
|
202
291
|
if (stash !== "yes") {
|
|
203
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
error:
|
|
294
|
+
"dirty working tree — ask with native question, then call workflow_branch_setup with stash=yes",
|
|
295
|
+
};
|
|
204
296
|
}
|
|
205
297
|
try {
|
|
206
298
|
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`, "--", ":!docs/*/sdd"]);
|
|
@@ -214,22 +306,18 @@ export const branchSetup = ({
|
|
|
214
306
|
} catch (error) {
|
|
215
307
|
const message = error instanceof Error ? error.message : "checkout failed";
|
|
216
308
|
if (/worktree/i.test(message)) {
|
|
217
|
-
return {
|
|
309
|
+
return {
|
|
310
|
+
error: `branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`,
|
|
311
|
+
};
|
|
218
312
|
}
|
|
219
313
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (current === "main" || current === "master") {
|
|
223
|
-
const baseResult = ensureDevelopBase(cwd);
|
|
224
|
-
if (!baseResult.ok) return { error: baseResult.error };
|
|
225
|
-
} else {
|
|
226
|
-
// Already on develop or another feature branch: still require origin/develop to exist.
|
|
227
|
-
const baseResult = ensureDevelopBase(cwd);
|
|
228
|
-
if (!baseResult.ok) return { error: baseResult.error };
|
|
229
|
-
}
|
|
314
|
+
const baseResult = ensureBaseBranch(cwd, baseBranch(cwd));
|
|
315
|
+
if (!baseResult.ok) return { error: baseResult.error };
|
|
230
316
|
exec(["checkout", "-b", target]);
|
|
231
317
|
} catch (createError) {
|
|
232
|
-
return {
|
|
318
|
+
return {
|
|
319
|
+
error: createError instanceof Error ? createError.message : "branch create failed",
|
|
320
|
+
};
|
|
233
321
|
}
|
|
234
322
|
}
|
|
235
323
|
}
|
|
@@ -242,5 +330,12 @@ export const branchSetup = ({
|
|
|
242
330
|
manifest.stash_created_at = new Date().toISOString();
|
|
243
331
|
}
|
|
244
332
|
writeManifest(manifest);
|
|
245
|
-
return {
|
|
333
|
+
return {
|
|
334
|
+
action: "setup",
|
|
335
|
+
ok: true,
|
|
336
|
+
branch: target,
|
|
337
|
+
previous_branch: current,
|
|
338
|
+
stash_ref: stash_ref ?? null,
|
|
339
|
+
manifest: manifestPath,
|
|
340
|
+
};
|
|
246
341
|
};
|
package/src/core/changelog.ts
CHANGED
|
@@ -2,14 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveWorkspaceRoot } from "./scripts";
|
|
4
4
|
|
|
5
|
-
const CATEGORIES = [
|
|
6
|
-
"Added",
|
|
7
|
-
"Changed",
|
|
8
|
-
"Deprecated",
|
|
9
|
-
"Removed",
|
|
10
|
-
"Fixed",
|
|
11
|
-
"Security",
|
|
12
|
-
];
|
|
5
|
+
const CATEGORIES = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
|
|
13
6
|
|
|
14
7
|
// Port of scripts/changelog/apply-unreleased.py — merge Keep a Changelog
|
|
15
8
|
// entries into ## [Unreleased] without duplicating ### headings.
|
|
@@ -73,10 +66,21 @@ function splitUnreleased(text: string): [string, string, string] {
|
|
|
73
66
|
function canonicalCategory(heading: string): string | null {
|
|
74
67
|
const match = CAT_RE.exec(heading.replace(/\r?\n$/, ""));
|
|
75
68
|
if (!match) return null;
|
|
76
|
-
return
|
|
69
|
+
return (
|
|
70
|
+
CATEGORIES.find(
|
|
71
|
+
(c) =>
|
|
72
|
+
c.toLowerCase() ===
|
|
73
|
+
match[0]
|
|
74
|
+
.replace(/^###\s+/, "")
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase(),
|
|
77
|
+
) ?? null
|
|
78
|
+
);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function splitSections(
|
|
81
|
+
function splitSections(
|
|
82
|
+
body: string,
|
|
83
|
+
): [string[], Array<{ heading: string; category: string | null; body: string[] }>] {
|
|
80
84
|
const preamble: string[] = [];
|
|
81
85
|
const sections: Array<{ heading: string; category: string | null; body: string[] }> = [];
|
|
82
86
|
let current: { heading: string; category: string | null; body: string[] } | null = null;
|
|
@@ -207,7 +211,8 @@ function applyChangelog(
|
|
|
207
211
|
break;
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
|
-
text =
|
|
214
|
+
text =
|
|
215
|
+
lines.slice(0, insertAt).join("") + "## [Unreleased]\n\n" + lines.slice(insertAt).join("");
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
const [before, body, after] = splitUnreleased(text);
|
|
@@ -290,9 +295,7 @@ export function changelogApply({
|
|
|
290
295
|
|
|
291
296
|
export function changelogUnreleasedStats(workspace_root: string, changelogPath = "CHANGELOG.md") {
|
|
292
297
|
const cwd = resolveWorkspaceRoot(workspace_root);
|
|
293
|
-
const abs = path.isAbsolute(changelogPath)
|
|
294
|
-
? changelogPath
|
|
295
|
-
: path.join(cwd, changelogPath);
|
|
298
|
+
const abs = path.isAbsolute(changelogPath) ? changelogPath : path.join(cwd, changelogPath);
|
|
296
299
|
if (!fs.existsSync(abs)) return { exists: false };
|
|
297
300
|
const text = fs.readFileSync(abs, "utf8");
|
|
298
301
|
const m = text.match(/##\s+\[Unreleased\]([\s\S]*?)(?=\n##\s+\[|$)/i);
|
package/src/core/config-guard.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { initStatus } from "./init";
|
|
2
2
|
|
|
3
|
-
export const ALL_ITEM_IDS = [
|
|
3
|
+
export const ALL_ITEM_IDS = [
|
|
4
|
+
"youtrack_json",
|
|
5
|
+
"youtrack_token",
|
|
6
|
+
"vcs_json",
|
|
7
|
+
"gitlab_token",
|
|
8
|
+
"github_token",
|
|
9
|
+
];
|
|
4
10
|
export const CONFIG_GAP_MARKER = "workflow config missing";
|
|
5
11
|
|
|
6
12
|
export function describeConfigGaps(scope?: string[]): { missing: string[]; ok: boolean } {
|
|
7
13
|
const all = scope ?? ALL_ITEM_IDS;
|
|
8
14
|
try {
|
|
9
15
|
const status: unknown = initStatus();
|
|
10
|
-
if (!status || typeof status !== "object" || (status as Record<string, unknown>).error)
|
|
16
|
+
if (!status || typeof status !== "object" || (status as Record<string, unknown>).error)
|
|
17
|
+
return { missing: all, ok: false };
|
|
11
18
|
const items = (status as Record<string, unknown>).items;
|
|
12
19
|
if (!Array.isArray(items) || items.length === 0) return { missing: all, ok: false };
|
|
13
20
|
const known = all;
|