@brainervirus/workit-core 1.2.1 → 1.3.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 (44) hide show
  1. package/package.json +1 -1
  2. package/skills/workit-babysit/SKILL.md +20 -26
  3. package/skills/workit-behavioral-tdd/SKILL.md +0 -5
  4. package/skills/workit-blast-radius/SKILL.md +0 -5
  5. package/skills/workit-challenge/SKILL.md +30 -47
  6. package/skills/workit-debug/SKILL.md +5 -27
  7. package/skills/workit-deslop/SKILL.md +0 -5
  8. package/skills/workit-diagram/SKILL.md +0 -5
  9. package/skills/workit-green-run/SKILL.md +0 -5
  10. package/skills/workit-handoff/SKILL.md +2 -8
  11. package/skills/workit-implement/SKILL.md +14 -20
  12. package/skills/workit-mockup/SKILL.md +0 -5
  13. package/skills/workit-plan/SKILL.md +30 -63
  14. package/skills/workit-review/SKILL.md +0 -5
  15. package/skills/workit-steer/SKILL.md +27 -27
  16. package/src/core/authority.ts +6 -1
  17. package/src/core/auto-approval.ts +34 -14
  18. package/src/core/branch.ts +293 -42
  19. package/src/core/config-conversion.ts +4 -4
  20. package/src/core/config-guard.ts +1 -7
  21. package/src/core/config.ts +1 -1
  22. package/src/core/cutover.ts +1207 -140
  23. package/src/core/doctor.ts +155 -82
  24. package/src/core/external-action-effects.ts +886 -172
  25. package/src/core/external-action.ts +214 -63
  26. package/src/core/init.ts +25 -115
  27. package/src/core/legacy-ownership.ts +47 -0
  28. package/src/core/methods.ts +56 -52
  29. package/src/core/ports/pr-create.ts +23 -15
  30. package/src/core/pr-create.ts +374 -84
  31. package/src/core/registration.ts +0 -15
  32. package/src/core/repo-tools.ts +0 -1
  33. package/src/core/route-intent.ts +34 -82
  34. package/src/core/safe-write.ts +66 -1
  35. package/src/core/setup-state.ts +7 -0
  36. package/src/core/setup.ts +175 -82
  37. package/src/core/task-contract.ts +7 -1
  38. package/src/core/task-engine.ts +47 -2
  39. package/src/core/task-store.ts +159 -1
  40. package/src/core/tracker-issues.ts +55 -118
  41. package/src/core/vcs-config.ts +178 -187
  42. package/src/core/workspaces.ts +548 -46
  43. package/src/core.ts +1 -2
  44. package/src/core/ports/vcs-token-create-urls.ts +0 -4
@@ -2,12 +2,9 @@
2
2
  // installed Workit surfaces — pins, versions, assets, launchers, runtimes,
3
3
  // utilities, registrations, config, workspace match, credential metadata, and
4
4
  // log writability — with no network access except the optional registry probe
5
- // behind the stale-install comparison (CA-04) and the fail-open identity
6
- // probes behind github_identity, which fail open. Credential values are never
7
- // reported: only existence, mode, and a placeholder flag are evaluated, and
8
- // the identity check reads a token file solely to build an Authorization
9
- // header for a login lookup — token bytes never enter the report, any fix
10
- // text, or any log event.
5
+ // behind the stale-install comparison (CA-04) and fail-open provider CLI
6
+ // identity probes. Credentials are handled by gh/glab; token values never enter
7
+ // the report, any fix text, or any log event.
11
8
  import { spawnSync } from "node:child_process";
12
9
  import {
13
10
  existsSync,
@@ -34,7 +31,7 @@ import {
34
31
  cursorMcpServerEntry,
35
32
  isWorkitPlugin,
36
33
  } from "./registration";
37
- import { resolveWorkspaceFrom } from "./workspaces";
34
+ import { readWorkspacesResult, resolveWorkspaceFrom } from "./workspaces";
38
35
  import { validateCursorSkills, WORKIT_METHOD_SKILLS } from "./skill-manifests";
39
36
  import {
40
37
  classifyHostGeneration,
@@ -66,6 +63,7 @@ export type DoctorCheckId =
66
63
  | "workspace_mismatch"
67
64
  | "credential_metadata"
68
65
  | "github_identity"
66
+ | "gitlab_identity"
69
67
  | "log_writable"
70
68
  | "legacy_component"
71
69
  | "mixed_generation"
@@ -1166,22 +1164,36 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
1166
1164
  };
1167
1165
 
1168
1166
  const checkWorkspaceMismatch = (res: Resolved): DoctorCheck => {
1169
- const file = path.join(res.configDir, "workspaces.json");
1170
- if (!existsSync(file))
1167
+ const result = readWorkspacesResult(res.configDir);
1168
+ const file = result.path;
1169
+ if (result.status === "missing" || (result.status === "valid" && result.entries.length === 0))
1171
1170
  return {
1172
1171
  id: "workspace_mismatch",
1173
1172
  status: "pass",
1174
1173
  detail: "no workspaces configured",
1175
1174
  };
1176
- const ws = readJson(file);
1177
- if (!Array.isArray(ws?.workspaces)) {
1175
+ if (result.status === "malformed" || result.status === "invalid")
1178
1176
  return {
1179
1177
  id: "workspace_mismatch",
1180
- status: "pass",
1181
- detail: "no workspaces configured",
1178
+ status: "fail",
1179
+ detail: result.error ?? `${file} has invalid workspace configuration`,
1180
+ fix: `Repair workspace configuration in ${file}`,
1181
+ };
1182
+ let match;
1183
+ try {
1184
+ match = resolveWorkspaceFrom(
1185
+ res.cwd,
1186
+ res.configDir,
1187
+ res.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined,
1188
+ );
1189
+ } catch (error) {
1190
+ return {
1191
+ id: "workspace_mismatch",
1192
+ status: "fail",
1193
+ detail: error instanceof Error ? error.message : String(error),
1194
+ fix: `Choose a matching workspace with WORKFLOW_WORKSPACE_NAME or repair ${file}`,
1182
1195
  };
1183
1196
  }
1184
- const match = resolveWorkspaceFrom(res.cwd, res.configDir);
1185
1197
  if (match)
1186
1198
  return {
1187
1199
  id: "workspace_mismatch",
@@ -1218,26 +1230,6 @@ const checkCredentialMetadata = (res: Resolved): DoctorCheck => {
1218
1230
  tokenPaths.push(path.join(res.configDir, "youtrack.token"));
1219
1231
  }
1220
1232
 
1221
- const vcsJson = readJson(path.join(res.configDir, "vcs.json"));
1222
- // Only the active VCS provider's token is required. vcs.json scaffolds both
1223
- // github and gitlab blocks for switching later — inactive tokenFile paths
1224
- // must not fail the doctor when provider is set to the other host.
1225
- const active =
1226
- typeof vcsJson?.provider === "string" ? vcsJson.provider.trim().toLowerCase() : null;
1227
- const vcsKeys =
1228
- active === "gitlab" || active === "github"
1229
- ? ([active] as const)
1230
- : (["gitlab", "github"] as const);
1231
- for (const key of vcsKeys) {
1232
- const provider = vcsJson?.[key];
1233
- if (provider && typeof provider === "object") {
1234
- const tf = provider.tokenFile;
1235
- tokenPaths.push(typeof tf === "string" ? tf : path.join(res.configDir, `${key}.token`));
1236
- } else if (!vcsJson && existsSync(path.join(res.configDir, `${key}.token`))) {
1237
- tokenPaths.push(path.join(res.configDir, `${key}.token`));
1238
- }
1239
- }
1240
-
1241
1233
  if (tokenPaths.length === 0) {
1242
1234
  return {
1243
1235
  id: "credential_metadata",
@@ -1273,15 +1265,13 @@ const checkCredentialMetadata = (res: Resolved): DoctorCheck => {
1273
1265
  };
1274
1266
  };
1275
1267
 
1276
- // Identity surfaces for the github_identity check. Logins are public
1277
- // usernames (safe to report); the token itself only ever travels as an
1278
- // Authorization header value and never enters details, fixes, or logs.
1268
+ // Provider CLI and SSH identities are public usernames; Workit never reads a VCS token file.
1279
1269
  export type IdentitySurface = { surface: string; login: string | null };
1280
1270
 
1281
1271
  export type IdentityProbes = {
1282
1272
  origin: (cwd: string) => string | null;
1283
1273
  ghLogin: (env: NodeJS.ProcessEnv) => string | null;
1284
- tokenLogin: (tokenFile: string, host: string) => string | null;
1274
+ glabLogin?: (env: NodeJS.ProcessEnv) => string | null;
1285
1275
  sshLogin: (host: string) => string | null;
1286
1276
  };
1287
1277
 
@@ -1298,31 +1288,82 @@ export const githubHostFromRemote = (remote: string): string | null => {
1298
1288
  }
1299
1289
  };
1300
1290
 
1301
- export const githubIdentityFinding = (surfaces: IdentitySurface[]): DoctorCheck => {
1291
+ const identityProvider = (
1292
+ host: string,
1293
+ config: Record<string, any> | null,
1294
+ workspaceProvider?: unknown,
1295
+ ): "github" | "gitlab" | null =>
1296
+ workspaceProvider === "github" || workspaceProvider === "gitlab"
1297
+ ? workspaceProvider
1298
+ : host === "github.com"
1299
+ ? "github"
1300
+ : host === "gitlab.com"
1301
+ ? "gitlab"
1302
+ : config?.provider === "github" || config?.provider === "gitlab"
1303
+ ? config.provider
1304
+ : config?.github
1305
+ ? "github"
1306
+ : config?.gitlab
1307
+ ? "gitlab"
1308
+ : null;
1309
+
1310
+ const providerIdentityFinding = (
1311
+ provider: "github" | "gitlab",
1312
+ surfaces: IdentitySurface[],
1313
+ ): DoctorCheck => {
1314
+ const id = `${provider}_identity` as "github_identity" | "gitlab_identity";
1315
+ const label = provider === "github" ? "GitHub" : "GitLab";
1302
1316
  const resolved = surfaces.filter(
1303
1317
  (entry): entry is { surface: string; login: string } => typeof entry.login === "string",
1304
1318
  );
1305
1319
  const distinct = [...new Set(resolved.map((entry) => entry.login.toLowerCase()))];
1306
1320
  if (distinct.length < 2) {
1307
1321
  return {
1308
- id: "github_identity",
1322
+ id,
1309
1323
  status: "pass",
1310
1324
  detail:
1311
1325
  resolved.length === 0
1312
- ? "no GitHub identities configured"
1313
- : "single GitHub identity across configured surfaces",
1326
+ ? `no ${label} identities configured`
1327
+ : `single ${label} identity across configured surfaces`,
1314
1328
  };
1315
1329
  }
1316
1330
  return {
1317
- id: "github_identity",
1331
+ id,
1318
1332
  status: "warn",
1319
- detail: `GitHub identities disagree across configured surfaces: ${resolved
1333
+ detail: `${label} identities disagree across configured surfaces: ${resolved
1320
1334
  .map((entry) => `${entry.surface} reports "${entry.login}"`)
1321
1335
  .join("; ")}. Operations may land under the wrong account.`,
1322
- fix: "Align every surface on the intended account via your own git/gh configuration (gh auth login/switch plus the vcs token file at mode 0600) — workit never hardcodes accounts",
1336
+ fix:
1337
+ provider === "github"
1338
+ ? "Align the effective gh account and SSH identity via gh auth switch/login and your Git configuration"
1339
+ : "Align the effective glab account and SSH identity via glab auth login and your Git configuration",
1323
1340
  };
1324
1341
  };
1325
1342
 
1343
+ export const githubIdentityFinding = (surfaces: IdentitySurface[]): DoctorCheck =>
1344
+ providerIdentityFinding("github", surfaces);
1345
+
1346
+ export const gitlabIdentityFinding = (surfaces: IdentitySurface[]): DoctorCheck =>
1347
+ providerIdentityFinding("gitlab", surfaces);
1348
+
1349
+ const workspaceProviderForIdentity = (
1350
+ cwd: string,
1351
+ configDir: string,
1352
+ env: NodeJS.ProcessEnv,
1353
+ ): { provider?: unknown; error?: string } => {
1354
+ try {
1355
+ return {
1356
+ provider: resolveWorkspaceFrom(
1357
+ cwd,
1358
+ configDir,
1359
+ env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined,
1360
+ )?.vcs?.provider,
1361
+ };
1362
+ } catch (error) {
1363
+ return { error: error instanceof Error ? error.message : String(error) };
1364
+ }
1365
+ };
1366
+
1326
1367
  const defaultIdentityProbes = (env: NodeJS.ProcessEnv): IdentityProbes => ({
1327
1368
  origin: (cwd) => {
1328
1369
  try {
@@ -1345,18 +1386,11 @@ const defaultIdentityProbes = (env: NodeJS.ProcessEnv): IdentityProbes => ({
1345
1386
  return null;
1346
1387
  }
1347
1388
  },
1348
- tokenLogin: (tokenFile, host) => {
1349
- let token = "";
1350
- try {
1351
- token = readFileSync(tokenFile, "utf8").trim();
1352
- } catch {
1353
- return null;
1354
- }
1355
- if (!token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) return null;
1389
+ glabLogin: (e) => {
1356
1390
  try {
1357
- const out = spawnSync("gh", ["api", "user", "--jq", ".login"], {
1391
+ const out = spawnSync("glab", ["api", "user", "--jq", ".username"], {
1358
1392
  encoding: "utf8",
1359
- env: { ...env, GH_TOKEN: token, ...(host === "github.com" ? {} : { GH_HOST: host }) },
1393
+ env: e,
1360
1394
  });
1361
1395
  const login = (out.stdout ?? "").trim();
1362
1396
  return out.status === 0 && login ? login : null;
@@ -1375,7 +1409,7 @@ const defaultIdentityProbes = (env: NodeJS.ProcessEnv): IdentityProbes => ({
1375
1409
  },
1376
1410
  );
1377
1411
  const text = `${out.stdout ?? ""}${out.stderr ?? ""}`;
1378
- const match = /^Hi ([^!]+)!/.exec(text);
1412
+ const match = /^(?:Hi |Welcome to GitLab, @)([^!]+)!/m.exec(text);
1379
1413
  return match ? match[1].trim() || null : null;
1380
1414
  } catch {
1381
1415
  return null;
@@ -1383,40 +1417,73 @@ const defaultIdentityProbes = (env: NodeJS.ProcessEnv): IdentityProbes => ({
1383
1417
  },
1384
1418
  });
1385
1419
 
1386
- const githubTokenFile = (res: Pick<Resolved, "configDir">): string | null => {
1387
- const vcsJson = readJson(path.join(res.configDir, "vcs.json"));
1388
- const provider = vcsJson?.github;
1389
- if (provider && typeof provider === "object") {
1390
- const tokenFile = (provider as Record<string, unknown>).tokenFile;
1391
- const raw =
1392
- typeof tokenFile === "string" ? tokenFile : path.join(res.configDir, "github.token");
1393
- return path.isAbsolute(raw) ? raw : path.resolve(res.configDir, raw);
1394
- }
1395
- if (!vcsJson) {
1396
- const fallback = path.join(res.configDir, "github.token");
1397
- return existsSync(fallback) ? fallback : null;
1398
- }
1399
- return null;
1400
- };
1401
-
1402
1420
  export const checkGithubIdentity = (
1403
1421
  res: Pick<Resolved, "cwd" | "configDir" | "env">,
1404
1422
  probes: IdentityProbes = defaultIdentityProbes(res.env),
1405
1423
  ): DoctorCheck => {
1406
- const host = githubHostFromRemote(probes.origin(res.cwd) ?? "");
1407
- if (!host)
1424
+ const remoteHost = githubHostFromRemote(probes.origin(res.cwd) ?? "");
1425
+ if (!remoteHost)
1408
1426
  return { id: "github_identity", status: "pass", detail: "no GitHub remote configured" };
1409
- const surfaces: IdentitySurface[] = [{ surface: "gh CLI", login: probes.ghLogin(res.env) }];
1410
- const tokenFile = githubTokenFile(res);
1411
- if (tokenFile)
1412
- surfaces.push({
1413
- surface: `token file ${tokenFile}`,
1414
- login: probes.tokenLogin(tokenFile, host),
1415
- });
1416
- surfaces.push({ surface: "SSH", login: probes.sshLogin(host) });
1427
+ const vcsJson = readJson(res.env.WORKFLOW_VCS_CONFIG ?? path.join(res.configDir, "vcs.json"));
1428
+ const workspace = workspaceProviderForIdentity(res.cwd, res.configDir, res.env);
1429
+ if (workspace.error)
1430
+ return {
1431
+ id: "github_identity",
1432
+ status: "fail",
1433
+ detail: workspace.error,
1434
+ fix: "Repair workspace configuration or choose a matching workspace",
1435
+ };
1436
+ const provider = identityProvider(remoteHost, vcsJson, workspace.provider);
1437
+ if (provider !== "github")
1438
+ return { id: "github_identity", status: "pass", detail: "no GitHub remote configured" };
1439
+ const cliHost = String(vcsJson?.github?.host ?? "github.com").toLowerCase();
1440
+ const cliLogin = probes.ghLogin({ ...res.env, GH_HOST: cliHost });
1441
+ if (!cliLogin)
1442
+ return {
1443
+ id: "github_identity",
1444
+ status: "fail",
1445
+ detail: "gh CLI is not authenticated",
1446
+ fix: "Run gh auth login",
1447
+ };
1448
+ const surfaces: IdentitySurface[] = [{ surface: "gh CLI", login: cliLogin }];
1449
+ surfaces.push({ surface: "SSH", login: probes.sshLogin(remoteHost) });
1417
1450
  return githubIdentityFinding(surfaces);
1418
1451
  };
1419
1452
 
1453
+ export const checkGitLabIdentity = (
1454
+ res: Pick<Resolved, "cwd" | "configDir" | "env">,
1455
+ probes: IdentityProbes = defaultIdentityProbes(res.env),
1456
+ ): DoctorCheck => {
1457
+ const remoteHost = githubHostFromRemote(probes.origin(res.cwd) ?? "");
1458
+ if (!remoteHost)
1459
+ return { id: "gitlab_identity", status: "pass", detail: "no GitLab remote configured" };
1460
+ const vcsJson = readJson(res.env.WORKFLOW_VCS_CONFIG ?? path.join(res.configDir, "vcs.json"));
1461
+ const workspace = workspaceProviderForIdentity(res.cwd, res.configDir, res.env);
1462
+ if (workspace.error)
1463
+ return {
1464
+ id: "gitlab_identity",
1465
+ status: "fail",
1466
+ detail: workspace.error,
1467
+ fix: "Repair workspace configuration or choose a matching workspace",
1468
+ };
1469
+ const provider = identityProvider(remoteHost, vcsJson, workspace.provider);
1470
+ if (provider !== "gitlab")
1471
+ return { id: "gitlab_identity", status: "pass", detail: "no GitLab remote configured" };
1472
+ const cliHost = String(vcsJson?.gitlab?.host ?? "gitlab.com").toLowerCase();
1473
+ const cliLogin = probes.glabLogin?.({ ...res.env, GITLAB_HOST: cliHost }) ?? null;
1474
+ if (!cliLogin)
1475
+ return {
1476
+ id: "gitlab_identity",
1477
+ status: "fail",
1478
+ detail: "glab CLI is not authenticated",
1479
+ fix: "Run glab auth login",
1480
+ };
1481
+ return gitlabIdentityFinding([
1482
+ { surface: "glab CLI", login: cliLogin },
1483
+ { surface: "SSH", login: probes.sshLogin(remoteHost) },
1484
+ ]);
1485
+ };
1486
+
1420
1487
  const checkLogWritable = (res: Resolved): DoctorCheck => {
1421
1488
  const logsDir = path.join(res.stateDir, "logs");
1422
1489
  // Fixed-name probe: even a killed process leaves at most one bounded file that
@@ -1458,6 +1525,11 @@ const generationPaths = (res: Resolved) => ({
1458
1525
  cursorSettings: res.cursorSettings,
1459
1526
  cursorMcp: res.cursorMcp,
1460
1527
  cursorPluginDir: res.cursorPluginDir,
1528
+ piConfig: path.join(res.home, ".pi", "config.json"),
1529
+ piSettings: path.join(
1530
+ res.env.PI_CODING_AGENT_DIR ?? path.join(res.home, ".pi", "agent"),
1531
+ "settings.json",
1532
+ ),
1461
1533
  sessions: res.sessions,
1462
1534
  });
1463
1535
 
@@ -1627,6 +1699,7 @@ const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
1627
1699
  checkWorkspaceMismatch,
1628
1700
  checkCredentialMetadata,
1629
1701
  checkGithubIdentity,
1702
+ checkGitLabIdentity,
1630
1703
  checkLogWritable,
1631
1704
  checkMixedGeneration,
1632
1705
  checkLegacyComponent,