@brainervirus/workit-core 2.1.0 → 2.1.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 +2 -2
- package/package.json +2 -3
- package/scripts/sync-runtime.sh +9 -5
- package/src/core/init.ts +1 -256
- package/src/core/policy-resolver.ts +0 -45
- package/src/core/repo-context.ts +1 -1
- package/src/core/rules.ts +1 -61
- package/src/core/templates.ts +1 -26
- package/src/core/vcs-config.ts +0 -4
- package/src/core/workspaces.ts +1 -2
- package/src/core/config-guard.ts +0 -27
- package/src/core/doc-render.ts +0 -14
- package/src/core/docs-layout.ts +0 -279
- package/src/core/docs-migration.ts +0 -639
- package/src/core/docs-repo.ts +0 -233
- package/src/core/docs-validate.ts +0 -393
- package/src/core/parse-sections.ts +0 -24
- package/src/core/ports/init-apply.ts +0 -15
- package/src/core/ports/init-status.ts +0 -4
- package/src/core/ports/init-toolkit-status.ts +0 -4
- package/src/core/ports/pr-create.ts +0 -30
- package/src/core/ports/present-ascii.ts +0 -10
- package/src/core/ports/present-flow.ts +0 -10
- package/src/core/ports/vcs-config.ts +0 -14
- package/src/core/ports/vcs-merged-style.ts +0 -5
- package/src/core/ports/vcs-verify-token.ts +0 -4
- package/src/core/ports/youtrack-api.ts +0 -19
- package/src/core/ports/youtrack-config.ts +0 -21
- package/src/core/ports/youtrack-greeting.ts +0 -10
- package/src/core/ports/youtrack-parse-duration.ts +0 -14
- package/src/core/ports/youtrack-token-create-url.ts +0 -4
- package/src/core/ports/youtrack-verify-token.ts +0 -12
- package/src/core/ports/youtrack-work-date-ms.ts +0 -10
- package/src/core/present.ts +0 -109
- package/src/core/repo-tool.ts +0 -12
- package/src/core/repo-tools.ts +0 -22
- package/src/core/sync-runtime.ts +0 -375
- package/src/core/verify-parse.ts +0 -29
- package/src/core/verify-project.ts +0 -181
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ End users do **not** install this package directly — install `@brainervirus/wo
|
|
|
13
13
|
You consume `@brainervirus/workit-core` directly only when:
|
|
14
14
|
|
|
15
15
|
- Building a new host adapter (e.g. a future `codex/` adapter) that maps a host's native surfaces to the shared `src/core/` logic.
|
|
16
|
-
- Reading the canonical source of workflow behavior:
|
|
16
|
+
- Reading the canonical source of workflow behavior: task and policy engine, approval flow, commit/PR, changelog, YouTrack, doctor, and diagnostics.
|
|
17
17
|
|
|
18
18
|
## Layout and ownership
|
|
19
19
|
|
|
20
20
|
| Path | Contents |
|
|
21
21
|
| --- | --- |
|
|
22
|
-
| `src/core/` | Shared TS logic (setup, registration, doctor, logger, branch, PR, changelog, YouTrack,
|
|
22
|
+
| `src/core/` | Shared TS logic (setup, registration, doctor, logger, branch, PR, changelog, YouTrack, task/policy engine, support matrix). |
|
|
23
23
|
| `src/core.ts` | Package entry; adapters import `@brainervirus/workit-core/src/*`. |
|
|
24
24
|
| `skills/` | 14 `workit-*` method skills (single source; adapters ship byte-identical copies). |
|
|
25
25
|
| `scripts/` | Shared shell installers/launchers and the release-time workspace-dep rewrite. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
|
|
6
6
|
"keywords": [
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
"skills/",
|
|
32
32
|
"templates/",
|
|
33
33
|
"README.md",
|
|
34
|
-
"LICENSE"
|
|
35
|
-
"!scripts/verify-project.sh"
|
|
34
|
+
"LICENSE"
|
|
36
35
|
],
|
|
37
36
|
"type": "module",
|
|
38
37
|
"main": "./src/core.ts",
|
package/scripts/sync-runtime.sh
CHANGED
|
@@ -124,11 +124,15 @@ fi
|
|
|
124
124
|
# Canonical user rules -> Cursor .mdc (compiled by the shared core)
|
|
125
125
|
CONFIG_RULES_DIR="$(resolve_config_dir)/rules"
|
|
126
126
|
if [ -d "$CONFIG_RULES_DIR" ]; then
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
131
|
-
|
|
127
|
+
# rules.ts is loaded only here (scripts/check-reachability.ts allowlists it):
|
|
128
|
+
# a failure must surface, never leave the Cursor rules silently stale.
|
|
129
|
+
if ! "$BUN_BIN" -e "
|
|
130
|
+
const { writeCompiledCursorRules } = await import('${SHARE}/packages/workit-core/src/core/rules.ts');
|
|
131
|
+
writeCompiledCursorRules('${PLUGIN_DIR}/rules');
|
|
132
|
+
" >/dev/null; then
|
|
133
|
+
echo "FATAL: Cursor rule compilation failed: $CONFIG_RULES_DIR -> $PLUGIN_DIR/rules" >&2
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
132
136
|
fi
|
|
133
137
|
printf '%s\n' "$SHARE/packages/workit-core" >"$PLUGIN_DIR/.workit-root"
|
|
134
138
|
|
package/src/core/init.ts
CHANGED
|
@@ -2,12 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { configDir, isConfigObject } from "./config";
|
|
5
|
-
import { PLUGIN_ROOT } from "./scripts";
|
|
6
5
|
import { writeFileExclusive } from "./safe-write";
|
|
7
|
-
import { resolveWorkspace, workspacesPath } from "./workspaces";
|
|
8
6
|
import { applyWorkspaceBranchPolicy } from "./setup";
|
|
9
|
-
import {
|
|
10
|
-
import { youTrackTokenCreateUrl, youTrackVerifyToken } from "./youtrack";
|
|
7
|
+
import { youTrackTokenCreateUrl } from "./youtrack";
|
|
11
8
|
|
|
12
9
|
const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
13
10
|
|
|
@@ -22,12 +19,6 @@ const readJson = (p: string): Record<string, any> | null => {
|
|
|
22
19
|
}
|
|
23
20
|
};
|
|
24
21
|
|
|
25
|
-
const isPlaceholder = (text: string): boolean =>
|
|
26
|
-
text === TOKEN_PLACEHOLDER || text.startsWith(TOKEN_PLACEHOLDER);
|
|
27
|
-
|
|
28
|
-
const modeOk = (p: string): boolean =>
|
|
29
|
-
process.platform === "win32" || (fs.statSync(p).mode & 0o777) === 0o600;
|
|
30
|
-
|
|
31
22
|
/** Provider for a fresh vcs.json: explicit env, else origin remote, else null
|
|
32
23
|
* (no silent default — a tool for everyone must not assume a provider). */
|
|
33
24
|
export const resolveInitProvider = (cwd?: string): string | null => {
|
|
@@ -50,234 +41,6 @@ export const resolveInitProvider = (cwd?: string): string | null => {
|
|
|
50
41
|
return null;
|
|
51
42
|
};
|
|
52
43
|
|
|
53
|
-
const resolvePath = (p: string): string => {
|
|
54
|
-
try {
|
|
55
|
-
return fs.realpathSync(p);
|
|
56
|
-
} catch {
|
|
57
|
-
return path.resolve(p);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** Port of scripts/init/status.sh — filesystem init state. */
|
|
62
|
-
export function initStatusData(configDirPath = configDir()): Record<string, any> {
|
|
63
|
-
const ytJson = path.join(configDirPath, "youtrack.json");
|
|
64
|
-
const vcsJson = path.join(configDirPath, "vcs.json");
|
|
65
|
-
const items: Array<Record<string, any>> = [];
|
|
66
|
-
|
|
67
|
-
let youtrackConfig: Record<string, any> | null = null;
|
|
68
|
-
let youtrackTokenCreate: Record<string, any> | null = null;
|
|
69
|
-
const ytParsed = readJson(ytJson);
|
|
70
|
-
if (ytParsed) {
|
|
71
|
-
const base = String(ytParsed.baseUrl ?? "").replace(/\/+$/, "");
|
|
72
|
-
const meeting = String(ytParsed.meetingIssue ?? "");
|
|
73
|
-
const tokenFile = String(ytParsed.tokenFile ?? "");
|
|
74
|
-
const meetingIssues = [];
|
|
75
|
-
const rawMeetings = (ytParsed.meetingIssues ?? {}) as Record<string, any>;
|
|
76
|
-
if (rawMeetings && typeof rawMeetings === "object") {
|
|
77
|
-
for (const [key, item] of Object.entries(rawMeetings)) {
|
|
78
|
-
const iss = String(item?.issue ?? "");
|
|
79
|
-
meetingIssues.push({
|
|
80
|
-
key,
|
|
81
|
-
issue: iss,
|
|
82
|
-
label: item?.label ?? iss,
|
|
83
|
-
workItemText: item?.workItemText ?? "Reuniones",
|
|
84
|
-
url: item?.url ?? (base && iss ? `${base}/issue/${iss}` : null),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const expanded = tokenFile ? path.resolve(tokenFile) : null;
|
|
89
|
-
const resolvedTokenFile =
|
|
90
|
-
expanded && fs.existsSync(expanded)
|
|
91
|
-
? resolvePath(expanded)
|
|
92
|
-
: expanded
|
|
93
|
-
? path.isAbsolute(expanded)
|
|
94
|
-
? expanded
|
|
95
|
-
: path.resolve(configDirPath, expanded)
|
|
96
|
-
: null;
|
|
97
|
-
youtrackConfig = {
|
|
98
|
-
config_edit_path: resolvePath(ytJson),
|
|
99
|
-
baseUrl: base,
|
|
100
|
-
meetingIssue: meeting,
|
|
101
|
-
meetingIssues,
|
|
102
|
-
meetingIssueUrl: base && meeting ? `${base}/issue/${meeting}` : null,
|
|
103
|
-
defaultMention: ytParsed.defaultMention,
|
|
104
|
-
timezone: ytParsed.timezone,
|
|
105
|
-
locale: ytParsed.locale,
|
|
106
|
-
tokenFile: resolvedTokenFile,
|
|
107
|
-
tokenDefaults: ytParsed.tokenDefaults,
|
|
108
|
-
timeLogging: {
|
|
109
|
-
meetings: {
|
|
110
|
-
options: meetingIssues,
|
|
111
|
-
skill: "/wk-meetings",
|
|
112
|
-
logsTime: true,
|
|
113
|
-
postsComment: false,
|
|
114
|
-
},
|
|
115
|
-
taskWork: {
|
|
116
|
-
issueSource: "active spec/plan **YouTrack:** field or --issue",
|
|
117
|
-
skill: "/wk-issue-update",
|
|
118
|
-
logsTime: true,
|
|
119
|
-
postsComment: true,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
youtrackTokenCreate = youTrackTokenCreateUrl().data;
|
|
124
|
-
if (youtrackTokenCreate && !(youtrackConfig as Record<string, any>).error) {
|
|
125
|
-
youtrackConfig.tokenCreate = youtrackTokenCreate;
|
|
126
|
-
}
|
|
127
|
-
} else if (fs.existsSync(ytJson)) {
|
|
128
|
-
// Distinguish parse failure (legacy message, path in config_edit_path) from a
|
|
129
|
-
// parseable non-object, which gets the shared shape diagnostic with the path.
|
|
130
|
-
let parseFailed = false;
|
|
131
|
-
try {
|
|
132
|
-
JSON.parse(fs.readFileSync(ytJson, "utf8"));
|
|
133
|
-
} catch {
|
|
134
|
-
parseFailed = true;
|
|
135
|
-
}
|
|
136
|
-
youtrackConfig = {
|
|
137
|
-
config_edit_path: resolvePath(ytJson),
|
|
138
|
-
error: parseFailed ? "invalid youtrack.json" : `${resolvePath(ytJson)} is not a JSON object`,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
items.push({
|
|
143
|
-
id: "youtrack_json",
|
|
144
|
-
label: "YouTrack config",
|
|
145
|
-
ok: fs.existsSync(ytJson) && youtrackConfig !== null && !("error" in youtrackConfig),
|
|
146
|
-
path: fs.existsSync(ytJson) ? resolvePath(ytJson) : path.resolve(ytJson),
|
|
147
|
-
config_edit_path: resolvePath(ytJson),
|
|
148
|
-
fix: "workit_init_apply action=youtrack_scaffold",
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const ytTokenPath = youtrackConfig?.tokenFile ?? path.join(configDirPath, "youtrack.token");
|
|
152
|
-
const tokenText = fs.existsSync(ytTokenPath) ? fs.readFileSync(ytTokenPath, "utf8").trim() : "";
|
|
153
|
-
const placeholder = fs.existsSync(ytTokenPath) && isPlaceholder(tokenText);
|
|
154
|
-
const tokenOk =
|
|
155
|
-
fs.existsSync(ytTokenPath) &&
|
|
156
|
-
modeOk(ytTokenPath) &&
|
|
157
|
-
Boolean(tokenText) &&
|
|
158
|
-
!isPlaceholder(tokenText);
|
|
159
|
-
|
|
160
|
-
const youtrackTokenItem: Record<string, any> = {
|
|
161
|
-
id: "youtrack_token",
|
|
162
|
-
label: "YouTrack API token (mode 600, not placeholder)",
|
|
163
|
-
ok: tokenOk,
|
|
164
|
-
path: fs.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path.resolve(ytTokenPath),
|
|
165
|
-
token_edit_path: fs.existsSync(ytTokenPath)
|
|
166
|
-
? resolvePath(ytTokenPath)
|
|
167
|
-
: path.resolve(ytTokenPath),
|
|
168
|
-
placeholder,
|
|
169
|
-
fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER} with your permanent token, save, then /wk-status`,
|
|
170
|
-
};
|
|
171
|
-
if (youtrackTokenCreate) {
|
|
172
|
-
if (youtrackTokenCreate.createUrl)
|
|
173
|
-
youtrackTokenItem.token_create_url = youtrackTokenCreate.createUrl;
|
|
174
|
-
if (youtrackTokenCreate.docsUrl)
|
|
175
|
-
youtrackTokenItem.token_create_docs_url = youtrackTokenCreate.docsUrl;
|
|
176
|
-
if (youtrackTokenCreate.scopes) youtrackTokenItem.token_scopes = youtrackTokenCreate.scopes;
|
|
177
|
-
if (youtrackTokenCreate.tokenName) youtrackTokenItem.token_name = youtrackTokenCreate.tokenName;
|
|
178
|
-
if (youtrackTokenCreate.steps) youtrackTokenItem.token_create_steps = youtrackTokenCreate.steps;
|
|
179
|
-
youtrackTokenItem.token_prefill_supported = youtrackTokenCreate.prefillSupported ?? false;
|
|
180
|
-
}
|
|
181
|
-
items.push(youtrackTokenItem);
|
|
182
|
-
|
|
183
|
-
let vcsCfg: Record<string, any> | null = null;
|
|
184
|
-
const vcsParsed = readJson(vcsJson);
|
|
185
|
-
if (vcsParsed) {
|
|
186
|
-
const rawProvider = vcsParsed.provider;
|
|
187
|
-
const effective = vcsConfig("resolve", process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd());
|
|
188
|
-
const provider =
|
|
189
|
-
effective?.ok && typeof effective.provider === "string"
|
|
190
|
-
? effective.provider
|
|
191
|
-
: typeof rawProvider === "string" && rawProvider.trim()
|
|
192
|
-
? rawProvider.toLowerCase()
|
|
193
|
-
: null;
|
|
194
|
-
vcsCfg = {
|
|
195
|
-
config_edit_path: resolvePath(vcsJson),
|
|
196
|
-
provider,
|
|
197
|
-
defaultTargetBranch: effective.ok
|
|
198
|
-
? effective.defaultTargetBranch
|
|
199
|
-
: (vcsParsed.defaultTargetBranch ?? "develop"),
|
|
200
|
-
pr: vcsParsed.pr ?? {},
|
|
201
|
-
gitlab: vcsParsed.gitlab,
|
|
202
|
-
github: vcsParsed.github,
|
|
203
|
-
skill: "/wk-pr",
|
|
204
|
-
switchHint: 'Set "provider" to "gitlab" or "github" in vcs.json',
|
|
205
|
-
};
|
|
206
|
-
} else if (fs.existsSync(vcsJson)) {
|
|
207
|
-
vcsCfg = { config_edit_path: resolvePath(vcsJson), error: "invalid vcs.json" };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
items.push({
|
|
211
|
-
id: "vcs_json",
|
|
212
|
-
label: "VCS config (GitLab / GitHub)",
|
|
213
|
-
ok: fs.existsSync(vcsJson) && vcsCfg !== null && !("error" in vcsCfg),
|
|
214
|
-
path: fs.existsSync(vcsJson) ? resolvePath(vcsJson) : path.resolve(vcsJson),
|
|
215
|
-
config_edit_path: resolvePath(vcsJson),
|
|
216
|
-
fix: "workit_init_apply action=vcs_scaffold",
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
config_dir: configDirPath,
|
|
221
|
-
plugin_root: PLUGIN_ROOT,
|
|
222
|
-
token_placeholder: TOKEN_PLACEHOLDER,
|
|
223
|
-
youtrack_config: youtrackConfig,
|
|
224
|
-
vcs_config: vcsCfg,
|
|
225
|
-
items,
|
|
226
|
-
ready: false,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Port of scripts/init/toolkit-status.sh — filesystem + API health check. */
|
|
231
|
-
export async function toolkitStatusData(configDirPath = configDir()): Promise<Record<string, any>> {
|
|
232
|
-
const status = initStatusData(configDirPath);
|
|
233
|
-
const tokenItem = status.items.find((i: Record<string, any>) => i.id === "youtrack_token") ?? {};
|
|
234
|
-
const placeholder = Boolean(tokenItem.placeholder);
|
|
235
|
-
|
|
236
|
-
const vcsCfg = (status.vcs_config ?? {}) as Record<string, any>;
|
|
237
|
-
const provider =
|
|
238
|
-
typeof vcsCfg.provider === "string" && vcsCfg.provider.trim()
|
|
239
|
-
? vcsCfg.provider.toLowerCase()
|
|
240
|
-
: null;
|
|
241
|
-
const vcsJsonOk = Boolean(status.items.find((i: Record<string, any>) => i.id === "vcs_json")?.ok);
|
|
242
|
-
|
|
243
|
-
const verify = placeholder
|
|
244
|
-
? { ok: false, error: "token still placeholder YOUR_TOKEN_HERE" }
|
|
245
|
-
: await youTrackVerifyToken();
|
|
246
|
-
const vcsVerify =
|
|
247
|
-
provider === null
|
|
248
|
-
? {
|
|
249
|
-
ok: false,
|
|
250
|
-
error: "vcs provider unconfigured — set provider to gitlab or github in vcs.json",
|
|
251
|
-
}
|
|
252
|
-
: await vcsVerifyToken();
|
|
253
|
-
const youTrackHealth = ("data" in verify ? verify.data : verify) as Record<string, any>;
|
|
254
|
-
|
|
255
|
-
status.youtrack_verify = verify;
|
|
256
|
-
status.youtrack_ok = placeholder ? false : Boolean(youTrackHealth.ok);
|
|
257
|
-
status.vcs_verify = vcsVerify;
|
|
258
|
-
status.vcs_ok = vcsJsonOk && Boolean((vcsVerify as Record<string, any>).ok);
|
|
259
|
-
|
|
260
|
-
const fsReady = status.items.every((i: Record<string, any>) => i.required === false || i.ok);
|
|
261
|
-
status.ready = fsReady && Boolean(status.youtrack_ok) && (!vcsJsonOk || Boolean(status.vcs_ok));
|
|
262
|
-
|
|
263
|
-
if (placeholder) {
|
|
264
|
-
const tokenPath = tokenItem.token_edit_path ?? tokenItem.path ?? "";
|
|
265
|
-
const createUrl = tokenItem.token_create_url ?? status.youtrack_config?.tokenCreate?.createUrl;
|
|
266
|
-
status.token_edit_path = tokenPath;
|
|
267
|
-
if (createUrl) status.token_create_url = createUrl;
|
|
268
|
-
status.next_step = createUrl
|
|
269
|
-
? "Open the YouTrack create-token URL, New token (name workit, scope YouTrack), paste into the token file, save, then run /wk-status"
|
|
270
|
-
: "Open the YouTrack token file, replace YOUR_TOKEN_HERE, save, then run /wk-status";
|
|
271
|
-
} else if (!status.youtrack_ok) {
|
|
272
|
-
status.next_step = "Fix YouTrack token or re-run /wk-init";
|
|
273
|
-
} else if (vcsJsonOk && !status.vcs_ok) {
|
|
274
|
-
status.next_step = `Run ${provider === "gitlab" ? "glab" : "gh"} auth login/switch, then /wk-status`;
|
|
275
|
-
} else if (status.ready) {
|
|
276
|
-
status.next_step = "All checks passed";
|
|
277
|
-
}
|
|
278
|
-
return status;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
44
|
// Port of scripts/init/apply.sh write_youtrack_json — env overrides honored.
|
|
282
45
|
const youtrackJsonContent = (dir: string): Record<string, any> => ({
|
|
283
46
|
baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
|
|
@@ -467,24 +230,6 @@ export function initApplyData(
|
|
|
467
230
|
}
|
|
468
231
|
}
|
|
469
232
|
|
|
470
|
-
export function initStatus(): Record<string, any> {
|
|
471
|
-
const data = initStatusData();
|
|
472
|
-
const workspaces: Record<string, any> = { path: workspacesPath(), resolved: null };
|
|
473
|
-
try {
|
|
474
|
-
workspaces.resolved = resolveWorkspace(process.cwd());
|
|
475
|
-
} catch (error) {
|
|
476
|
-
workspaces.error = error instanceof Error ? error.message : String(error);
|
|
477
|
-
}
|
|
478
|
-
return {
|
|
479
|
-
...data,
|
|
480
|
-
workspaces,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
export async function toolkitStatus(): Promise<Record<string, any>> {
|
|
485
|
-
return toolkitStatusData();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
233
|
export function initApply({
|
|
489
234
|
action,
|
|
490
235
|
confirmed,
|
|
@@ -38,51 +38,6 @@ export type ResolverPreferences = {
|
|
|
38
38
|
thorough?: boolean;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
/** Size facts for deterministic spec triage (op1 with override). */
|
|
42
|
-
export type TriageFacts = {
|
|
43
|
-
/** New or changed observable behavior. */
|
|
44
|
-
newOrChangedBehavior: boolean;
|
|
45
|
-
/** Open ambiguity needing user resolution. */
|
|
46
|
-
ambiguityOpen: boolean;
|
|
47
|
-
/** Cross-package/host contract, auth/data/security surface, or irreversible migration. */
|
|
48
|
-
crossContract: boolean;
|
|
49
|
-
/** Subsystems touched. */
|
|
50
|
-
subsystems: number;
|
|
51
|
-
/** Packages touched (a 2-package change is Large even in one subsystem). */
|
|
52
|
-
packagesTouched: number;
|
|
53
|
-
/** Estimated implementation steps. */
|
|
54
|
-
steps: number;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type TriageTier = "spec-plan" | "plan-only" | "neither";
|
|
58
|
-
|
|
59
|
-
/** Large → spec + full plan; Medium → compact plan-only; Small → neither.
|
|
60
|
-
* Step count alone never escalates: a long but known, single-subsystem run
|
|
61
|
-
* stays plan-only. The lead may re-tier with a reason recorded in progress
|
|
62
|
-
* (override). */
|
|
63
|
-
export const triageTier = (facts: TriageFacts): TriageTier => {
|
|
64
|
-
if (
|
|
65
|
-
facts.newOrChangedBehavior ||
|
|
66
|
-
facts.ambiguityOpen ||
|
|
67
|
-
facts.crossContract ||
|
|
68
|
-
facts.subsystems >= 3 ||
|
|
69
|
-
facts.packagesTouched >= 2
|
|
70
|
-
)
|
|
71
|
-
return "spec-plan";
|
|
72
|
-
if (facts.steps >= 2) return "plan-only";
|
|
73
|
-
return "neither";
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** Tier → assessor signal values (durableAgreementNeeded, coordinationPlanNeeded). */
|
|
77
|
-
export const triageSignals = (
|
|
78
|
-
tier: TriageTier,
|
|
79
|
-
): { durableAgreementNeeded: boolean; coordinationPlanNeeded: boolean } =>
|
|
80
|
-
tier === "spec-plan"
|
|
81
|
-
? { durableAgreementNeeded: true, coordinationPlanNeeded: true }
|
|
82
|
-
: tier === "plan-only"
|
|
83
|
-
? { durableAgreementNeeded: false, coordinationPlanNeeded: true }
|
|
84
|
-
: { durableAgreementNeeded: false, coordinationPlanNeeded: false };
|
|
85
|
-
|
|
86
41
|
export type ResolverInput = {
|
|
87
42
|
intent: Intent;
|
|
88
43
|
assessment: Assessment;
|
package/src/core/repo-context.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { gitRevisionParts, resolveGitRevision } from "../core";
|
|
|
7
7
|
// Ports of scripts/_shared/common.sh + the four maintained context generators
|
|
8
8
|
// (pr-ready-context.sh, changelog-context.sh, docs-refresh-context.sh,
|
|
9
9
|
// release-notes-context.sh). Each generator returns the same stdout text shape
|
|
10
|
-
// the shell produced (## sections
|
|
10
|
+
// the shell produced (## sections).
|
|
11
11
|
|
|
12
12
|
export type ContextResult = { stdout: string; stderr: string; exitCode: number; cwd: string };
|
|
13
13
|
|
package/src/core/rules.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { configDir } from "./config";
|
|
4
4
|
|
|
@@ -41,71 +41,11 @@ export const parseRule = (markdown: string): CanonicalRule | { error: string } =
|
|
|
41
41
|
return { name, description, platforms, body: fm[2].trim() + "\n" };
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
export const listRules = (): { name: string; platforms: string[]; source: "config" | "repo" }[] => {
|
|
45
|
-
const result: { name: string; platforms: string[]; source: "config" | "repo" }[] = [];
|
|
46
|
-
const dir = rulesDir();
|
|
47
|
-
if (existsSync(dir)) {
|
|
48
|
-
for (const entry of readdirSync(dir)) {
|
|
49
|
-
const file = path.join(dir, entry, "rule.md");
|
|
50
|
-
if (!existsSync(file)) continue;
|
|
51
|
-
const parsed = parseRule(readFileSync(file, "utf8"));
|
|
52
|
-
if ("error" in parsed) continue;
|
|
53
|
-
result.push({ name: parsed.name, platforms: parsed.platforms, source: "config" });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const readRule = (
|
|
60
|
-
name: string,
|
|
61
|
-
): { source: "config" | "repo" | "missing"; rule: CanonicalRule } | { error: string } => {
|
|
62
|
-
const file = path.join(rulesDir(), name, "rule.md");
|
|
63
|
-
if (existsSync(file)) {
|
|
64
|
-
const parsed = parseRule(readFileSync(file, "utf8"));
|
|
65
|
-
if ("error" in parsed) return { error: parsed.error };
|
|
66
|
-
return { source: "config", rule: parsed };
|
|
67
|
-
}
|
|
68
|
-
return { source: "missing", rule: { name, description: "", platforms: [], body: "" } };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
44
|
const RULE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
72
45
|
|
|
73
|
-
export const writeRule = (
|
|
74
|
-
rule: CanonicalRule,
|
|
75
|
-
confirmed: boolean,
|
|
76
|
-
): { ok: true; path: string } | { ok: false; error: string } => {
|
|
77
|
-
if (!confirmed) return { ok: false, error: "confirmed: true required" };
|
|
78
|
-
if (!RULE_NAME_RE.test(rule.name))
|
|
79
|
-
return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
|
|
80
|
-
const dir = path.join(rulesDir(), rule.name);
|
|
81
|
-
mkdirSync(dir, { recursive: true });
|
|
82
|
-
const file = path.join(dir, "rule.md");
|
|
83
|
-
const md = `---\nname: ${rule.name}\ndescription: ${rule.description}\nplatforms: [${rule.platforms.join(", ")}]\n---\n${rule.body}`;
|
|
84
|
-
writeFileSync(file, md, "utf8");
|
|
85
|
-
return { ok: true, path: file };
|
|
86
|
-
};
|
|
87
|
-
|
|
88
46
|
export const compileRuleCursor = (rule: CanonicalRule): string =>
|
|
89
47
|
`---\ndescription: ${rule.description}\nalwaysApply: true\n---\n\n${rule.body}`;
|
|
90
48
|
|
|
91
|
-
export const compileRuleOpenCode = (rule: CanonicalRule): string =>
|
|
92
|
-
`## ${rule.name}\n\n${rule.body}`;
|
|
93
|
-
|
|
94
|
-
export const compiledOpenCodeSections = (): string => {
|
|
95
|
-
const sections: string[] = [];
|
|
96
|
-
const dir = rulesDir();
|
|
97
|
-
if (existsSync(dir)) {
|
|
98
|
-
for (const entry of readdirSync(dir)) {
|
|
99
|
-
const file = path.join(dir, entry, "rule.md");
|
|
100
|
-
if (!existsSync(file)) continue;
|
|
101
|
-
const parsed = parseRule(readFileSync(file, "utf8"));
|
|
102
|
-
if ("error" in parsed || !parsed.platforms.includes("opencode")) continue;
|
|
103
|
-
sections.push(compileRuleOpenCode(parsed));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return sections.join("\n\n");
|
|
107
|
-
};
|
|
108
|
-
|
|
109
49
|
export const writeCompiledCursorRules = (targetDir: string): string[] => {
|
|
110
50
|
const written: string[] = [];
|
|
111
51
|
const dir = rulesDir();
|
package/src/core/templates.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { configDir } from "./config";
|
|
4
4
|
import { assetRoot } from "./package-root";
|
|
@@ -20,28 +20,3 @@ export const readTemplate = (
|
|
|
20
20
|
content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8"),
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
export const writeTemplate = (
|
|
25
|
-
name: TemplateName,
|
|
26
|
-
content: string,
|
|
27
|
-
confirmed: boolean,
|
|
28
|
-
): { ok: true; path: string } | { ok: false; error: string } => {
|
|
29
|
-
if (!confirmed) return { ok: false, error: "confirmed: true required" };
|
|
30
|
-
const file = templatePath(name);
|
|
31
|
-
mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
-
writeFileSync(file, content, "utf8");
|
|
33
|
-
return { ok: true, path: file };
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const listTemplates = (): {
|
|
37
|
-
name: TemplateName;
|
|
38
|
-
source: "config" | "repo" | "missing";
|
|
39
|
-
path: string;
|
|
40
|
-
}[] =>
|
|
41
|
-
(["issue-update", "greeting", "headers"] as TemplateName[]).map((name) => {
|
|
42
|
-
const cfg = templatePath(name);
|
|
43
|
-
const repoFile = path.join(repoRoot, "templates", `${name}.md`);
|
|
44
|
-
if (existsSync(cfg)) return { name, source: "config", path: cfg };
|
|
45
|
-
if (existsSync(repoFile)) return { name, source: "repo", path: repoFile };
|
|
46
|
-
return { name, source: "missing", path: cfg };
|
|
47
|
-
});
|
package/src/core/vcs-config.ts
CHANGED
|
@@ -303,10 +303,6 @@ export function vcsCliIdentity(cwd?: string): Record<string, any> {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/** Legacy command name, now verifies native CLI auth rather than a separate token. */
|
|
306
|
-
export async function vcsVerifyToken(): Promise<Record<string, any>> {
|
|
307
|
-
return vcsCliIdentity();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
306
|
/** Port of scripts/vcs/merged-style.sh — recent merged MR/PR bodies for style reference. */
|
|
311
307
|
export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
|
|
312
308
|
const cfg = vcsConfig("load", cwd);
|
package/src/core/workspaces.ts
CHANGED
|
@@ -448,8 +448,7 @@ export const resolveWorkspaceFromEntries = <T extends { name: string; glob: stri
|
|
|
448
448
|
// macOS/Windows tmpdir symlinks (/var -> /private/var): git's
|
|
449
449
|
// --show-toplevel returns the realpath while config globs are usually
|
|
450
450
|
// written with the logical path, so a workspace would silently stop
|
|
451
|
-
// matching on macOS. Match both forms on each side
|
|
452
|
-
// docs-migration escape-guard realpath comparison; on Linux both forms
|
|
451
|
+
// matching on macOS. Match both forms on each side; on Linux both forms
|
|
453
452
|
// are identical so behavior is unchanged.
|
|
454
453
|
const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
|
|
455
454
|
const matches: T[] = [];
|
package/src/core/config-guard.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { initStatus } from "./init";
|
|
2
|
-
|
|
3
|
-
export const ALL_ITEM_IDS = ["youtrack_json", "youtrack_token", "vcs_json"];
|
|
4
|
-
export const CONFIG_GAP_MARKER = "workflow config missing";
|
|
5
|
-
|
|
6
|
-
export function describeConfigGaps(scope?: string[]): { missing: string[]; ok: boolean } {
|
|
7
|
-
const all = scope ?? ALL_ITEM_IDS;
|
|
8
|
-
try {
|
|
9
|
-
const status: unknown = initStatus();
|
|
10
|
-
if (!status || typeof status !== "object" || (status as Record<string, unknown>).error)
|
|
11
|
-
return { missing: all, ok: false };
|
|
12
|
-
const items = (status as Record<string, unknown>).items;
|
|
13
|
-
if (!Array.isArray(items) || items.length === 0) return { missing: all, ok: false };
|
|
14
|
-
const known = all;
|
|
15
|
-
const missing = items
|
|
16
|
-
.filter((item) => item && (item as Record<string, unknown>).ok === false)
|
|
17
|
-
.map((item) => String((item as Record<string, unknown>).id))
|
|
18
|
-
.filter((id) => known.includes(id));
|
|
19
|
-
return { missing, ok: missing.length === 0 };
|
|
20
|
-
} catch {
|
|
21
|
-
return { missing: all, ok: false };
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function configGuardError(missing: string[]): string {
|
|
26
|
-
return `${CONFIG_GAP_MARKER}: ${missing.join(", ")}. Run \`npx workit init\` or \`/wk-init\` to configure.`;
|
|
27
|
-
}
|
package/src/core/doc-render.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const MAX_LINES = 150;
|
|
2
|
-
export const MAX_BYTES = 8192;
|
|
3
|
-
export const MAX_MERMAID = 3;
|
|
4
|
-
|
|
5
|
-
const MERMAID_FENCE = /^```mermaid\s*$/gm;
|
|
6
|
-
|
|
7
|
-
export const shouldRenderDoc = (text: string): boolean => {
|
|
8
|
-
const lines = text.split(/\r?\n/);
|
|
9
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
10
|
-
if (lines.length > MAX_LINES) return false;
|
|
11
|
-
if (Buffer.byteLength(text, "utf8") > MAX_BYTES) return false;
|
|
12
|
-
const mermaidCount = text.match(MERMAID_FENCE)?.length ?? 0;
|
|
13
|
-
return mermaidCount <= MAX_MERMAID;
|
|
14
|
-
};
|