@brainervirus/workit-codex 7.0.0 → 7.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workit",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "Adaptive engineering workflow rails for Codex",
5
5
  "author": {
6
6
  "name": "BrainerVirus"
@@ -6625,7 +6625,7 @@ import { existsSync as existsSync9, realpathSync as realpathSync7, statSync as s
6625
6625
  import path21 from "node:path";
6626
6626
 
6627
6627
  // packages/workit-core/src/core.ts
6628
- import { spawnSync as spawnSync11 } from "node:child_process";
6628
+ import { spawnSync as spawnSync12 } from "node:child_process";
6629
6629
  import { existsSync as existsSync7, realpathSync as realpathSync5 } from "node:fs";
6630
6630
  import path19 from "node:path";
6631
6631
 
@@ -18884,6 +18884,11 @@ function worktreeSignal(cwd, options = {}) {
18884
18884
  }
18885
18885
  return `sig:${hash.digest("hex")}`;
18886
18886
  }
18887
+ function mergeBase(cwd, a, b) {
18888
+ if (!safeArg(a) || !safeArg(b))
18889
+ return null;
18890
+ return line(git2(cwd, ["merge-base", a, b]));
18891
+ }
18887
18892
  function patchId(cwd, base, head) {
18888
18893
  if (!safeArg(base) || !safeArg(head))
18889
18894
  return null;
@@ -18907,9 +18912,33 @@ function patchId(cwd, base, head) {
18907
18912
  const id = git2(cwd, ["patch-id", "--stable"], { input: diff.stdout });
18908
18913
  return line(id)?.split(/\s+/u)[0] ?? null;
18909
18914
  }
18915
+ function remoteNames(cwd) {
18916
+ return (git2(cwd, ["remote"]).stdout ?? "").split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
18917
+ }
18918
+ function resolveRef(cwd, ref) {
18919
+ if (!safeArg(ref))
18920
+ return null;
18921
+ return line(git2(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
18922
+ }
18923
+ var config2 = (cwd, key) => line(git2(cwd, ["config", "--get", key]));
18910
18924
  function currentBranch(cwd) {
18911
18925
  return line(git2(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
18912
18926
  }
18927
+ function pushRemoteName(cwd, branch) {
18928
+ const name = branch ?? currentBranch(cwd);
18929
+ const remotes = remoteNames(cwd);
18930
+ const candidates = [
18931
+ name ? config2(cwd, `branch.${name}.pushRemote`) : null,
18932
+ config2(cwd, "remote.pushDefault"),
18933
+ name ? config2(cwd, `branch.${name}.remote`) : null
18934
+ ];
18935
+ for (const candidate of candidates)
18936
+ if (candidate && candidate !== "." && remotes.includes(candidate))
18937
+ return candidate;
18938
+ if (remotes.includes("origin"))
18939
+ return "origin";
18940
+ return remotes.length === 1 ? remotes[0] : null;
18941
+ }
18913
18942
 
18914
18943
  // packages/workit-core/src/core/policy/derive.ts
18915
18944
  import { existsSync as existsSync3 } from "node:fs";
@@ -20523,6 +20552,8 @@ var readConfig = () => {
20523
20552
  throw new Error(result.error);
20524
20553
  return result.config ?? DEFAULTS;
20525
20554
  };
20555
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path12.join(configDir(), "vcs.json");
20556
+ var branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i");
20526
20557
  var resolveBranchPolicy = (config, workspace) => {
20527
20558
  const wp = workspace?.branchPolicy ?? {};
20528
20559
  const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
@@ -20530,7 +20561,7 @@ var resolveBranchPolicy = (config, workspace) => {
20530
20561
  allowed: wp.allowed,
20531
20562
  protectedNames: wp.protected
20532
20563
  }, config);
20533
- const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
20564
+ const allowed = merged.allowed.map(branchGlobPattern);
20534
20565
  return {
20535
20566
  preset,
20536
20567
  allowed,
@@ -20547,16 +20578,6 @@ var COMMIT_PRESETS = [
20547
20578
  "custom",
20548
20579
  "auto"
20549
20580
  ];
20550
- var resolveCommitPolicy = (config, workspace) => {
20551
- const wp = workspace?.commitPolicy ?? {};
20552
- if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
20553
- return {
20554
- preset: wp.preset,
20555
- ...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
20556
- };
20557
- }
20558
- return config.commitPolicy ?? { preset: "conventional" };
20559
- };
20560
20581
 
20561
20582
  // packages/workit-core/src/core/workspaces.ts
20562
20583
  import { createHash as createHash8 } from "node:crypto";
@@ -20595,26 +20616,31 @@ var versionSourceSchema = discriminatedUnion("kind", [
20595
20616
  object({ kind: literal("manual") }).strict()
20596
20617
  ]);
20597
20618
  var releaseTrackSchema = object({
20598
- strategy: branchPreset,
20619
+ strategy: branchPreset.optional(),
20599
20620
  productionBranch: nonBlank,
20600
20621
  integrationBranch: nonBlank,
20601
- naming: object({ feature: nonBlank, release: nonBlank, hotfix: nonBlank }).strict(),
20602
- baseBranch: nonBlank,
20603
- mergeBackBranches: array(nonBlank),
20604
- pullRequestTarget: nonBlank,
20605
- tagNamespace: string2(),
20606
- versionSource: versionSourceSchema,
20607
- requiredChecks: array(nonBlank)
20608
- }).strict().superRefine((track, context) => {
20622
+ naming: object({
20623
+ feature: nonBlank.optional(),
20624
+ release: nonBlank.optional(),
20625
+ hotfix: nonBlank.optional()
20626
+ }).passthrough().optional(),
20627
+ baseBranch: nonBlank.optional(),
20628
+ mergeBackBranches: array(nonBlank).optional(),
20629
+ pullRequestTarget: nonBlank.optional(),
20630
+ tagNamespace: string2().optional(),
20631
+ versionSource: versionSourceSchema.optional(),
20632
+ requiredChecks: array(nonBlank).optional(),
20633
+ critical: array(nonBlank).optional()
20634
+ }).passthrough().superRefine((track, context) => {
20609
20635
  for (const [field, values] of [
20610
- ["mergeBackBranches", track.mergeBackBranches],
20611
- ["requiredChecks", track.requiredChecks]
20636
+ ["mergeBackBranches", track.mergeBackBranches ?? []],
20637
+ ["requiredChecks", track.requiredChecks ?? []]
20612
20638
  ]) {
20613
20639
  if (new Set(values).size !== values.length) {
20614
20640
  context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
20615
20641
  }
20616
20642
  }
20617
- if (track.versionSource.kind === "package-json" && (path13.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
20643
+ if (track.versionSource?.kind === "package-json" && (path13.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
20618
20644
  context.addIssue({
20619
20645
  code: "custom",
20620
20646
  path: ["versionSource", "path"],
@@ -20882,7 +20908,7 @@ var resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd,
20882
20908
  var runtimeWorkspaceIndexSchema = object({
20883
20909
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
20884
20910
  }).passthrough();
20885
- var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
20911
+ var matchRuntimeWorkspace = (cwd) => {
20886
20912
  const file = path13.join(configDir(), "workspaces.json");
20887
20913
  let raw;
20888
20914
  try {
@@ -20917,8 +20943,17 @@ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
20917
20943
  });
20918
20944
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
20919
20945
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
20920
- if (!selected)
20946
+ return selected ? { file, selected } : null;
20947
+ };
20948
+ var resolveRuntimeReleaseTracks = (cwd) => {
20949
+ const matched = matchRuntimeWorkspace(cwd);
20950
+ return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
20951
+ };
20952
+ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
20953
+ const matched = matchRuntimeWorkspace(cwd);
20954
+ if (!matched)
20921
20955
  return null;
20956
+ const { file, selected } = matched;
20922
20957
  const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
20923
20958
  if (!policy.success) {
20924
20959
  const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
@@ -20944,15 +20979,410 @@ var resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
20944
20979
  source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
20945
20980
  };
20946
20981
  };
20982
+ function workspaceDefaultTarget(cwd, globalDefault) {
20983
+ const ws = resolveRuntimeWorkspaceVcs(cwd);
20984
+ const wp = ws?.branchPolicy ?? {};
20985
+ const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
20986
+ const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined);
20987
+ const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null).defaultTargetBranch;
20988
+ return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
20989
+ }
20947
20990
 
20948
20991
  // packages/workit-core/src/core/vcs-config.ts
20949
20992
  import fs12 from "node:fs";
20950
20993
  import path14 from "node:path";
20994
+ import { spawnSync as spawnSync8 } from "node:child_process";
20995
+
20996
+ // packages/workit-core/src/core/release-tracks.ts
20997
+ import { createHash as createHash9 } from "node:crypto";
20951
20998
  import { spawnSync as spawnSync7 } from "node:child_process";
20952
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path14.join(configDir(), "vcs.json");
20999
+ var KNOWN_FIELDS = new Set([
21000
+ "strategy",
21001
+ "productionBranch",
21002
+ "integrationBranch",
21003
+ "naming",
21004
+ "baseBranch",
21005
+ "mergeBackBranches",
21006
+ "pullRequestTarget",
21007
+ "tagNamespace",
21008
+ "versionSource",
21009
+ "requiredChecks",
21010
+ "critical"
21011
+ ]);
21012
+ var BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
21013
+ var PLACEHOLDER = /\{[^}]*\}/gu;
21014
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21015
+ var branchName = (value) => typeof value === "string" && value.trim() && !value.trim().startsWith("-") ? value.trim() : null;
21016
+ var namedBranches = (entry) => [
21017
+ ...BRANCH_FIELDS.map((field) => branchName(entry[field])),
21018
+ ...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
21019
+ ].filter((name) => name !== null);
21020
+ var literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim();
21021
+ function readReleaseTracks(raw) {
21022
+ const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
21023
+ if (raw === undefined || raw === null)
21024
+ return out;
21025
+ if (!isRecord3(raw)) {
21026
+ out.issues.push("releaseTracks is not an object; ignored");
21027
+ return out;
21028
+ }
21029
+ const fatal = [];
21030
+ const protectedNames = new Set;
21031
+ for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
21032
+ const label = `release track ${JSON.stringify(name)}`;
21033
+ if (!isRecord3(entry)) {
21034
+ out.issues.push(`${label} is not an object; ignored`);
21035
+ continue;
21036
+ }
21037
+ for (const branch of namedBranches(entry))
21038
+ protectedNames.add(branch);
21039
+ if (!name.trim()) {
21040
+ out.issues.push("a release track with a blank name was ignored");
21041
+ continue;
21042
+ }
21043
+ let critical = [];
21044
+ if (Array.isArray(entry.critical))
21045
+ critical = entry.critical.filter((item) => typeof item === "string");
21046
+ else if (entry.critical !== undefined)
21047
+ out.issues.push(`${label} critical: must be a list of field names; ignored`);
21048
+ const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
21049
+ const problem = (field, message) => {
21050
+ if (isCritical(field))
21051
+ fatal.push(`${label} ${field}: ${message}`);
21052
+ else
21053
+ out.issues.push(`${label} ${field}: ${message}`);
21054
+ };
21055
+ for (const key of Object.keys(entry))
21056
+ if (!KNOWN_FIELDS.has(key))
21057
+ problem(key, "unknown field, ignored");
21058
+ const productionBranch = branchName(entry.productionBranch);
21059
+ const integrationBranch = branchName(entry.integrationBranch);
21060
+ if (!productionBranch)
21061
+ problem("productionBranch", "missing or not a branch name");
21062
+ if (!integrationBranch)
21063
+ problem("integrationBranch", "missing or not a branch name");
21064
+ const optionalBranch = (field) => {
21065
+ if (entry[field] === undefined)
21066
+ return null;
21067
+ const value = branchName(entry[field]);
21068
+ if (!value)
21069
+ problem(field, "not a branch name; using the integration branch");
21070
+ return value;
21071
+ };
21072
+ const baseBranch = optionalBranch("baseBranch");
21073
+ const pullRequestTarget = optionalBranch("pullRequestTarget");
21074
+ let mergeBackBranches = [];
21075
+ if (entry.mergeBackBranches !== undefined) {
21076
+ const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
21077
+ if (list.some((item) => item === null))
21078
+ problem("mergeBackBranches", "must be a list of branch names; ignored");
21079
+ else
21080
+ mergeBackBranches = [...new Set(list)];
21081
+ }
21082
+ const naming = {};
21083
+ if (entry.naming !== undefined) {
21084
+ if (!isRecord3(entry.naming))
21085
+ problem("naming", "not an object; ignored");
21086
+ else
21087
+ for (const kind of ["feature", "release", "hotfix"]) {
21088
+ const template = entry.naming[kind];
21089
+ if (template === undefined)
21090
+ continue;
21091
+ if (typeof template !== "string" || !template.trim())
21092
+ problem(`naming.${kind}`, "not a template string; ignored");
21093
+ else if (!literalOf(template))
21094
+ problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
21095
+ else
21096
+ naming[kind] = template.trim();
21097
+ }
21098
+ }
21099
+ if (!productionBranch || !integrationBranch) {
21100
+ out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
21101
+ continue;
21102
+ }
21103
+ out.tracks.push({
21104
+ name,
21105
+ productionBranch,
21106
+ integrationBranch,
21107
+ baseBranch: baseBranch ?? integrationBranch,
21108
+ pullRequestTarget: pullRequestTarget ?? integrationBranch,
21109
+ mergeBackBranches,
21110
+ naming
21111
+ });
21112
+ }
21113
+ out.protectedBranches = [...protectedNames];
21114
+ if (fatal.length)
21115
+ out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
21116
+ return out;
21117
+ }
21118
+ var IMPLICIT = "";
21119
+ var ownedBy = (tracks, branch) => tracks.filter((track) => [track.integrationBranch, track.productionBranch, track.baseBranch].includes(branch));
21120
+ var shortRef = (ref, remotes) => {
21121
+ const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
21122
+ const remote = remotes.find((name) => bare.startsWith(`${name}/`));
21123
+ return remote ? bare.slice(remote.length + 1) : bare;
21124
+ };
21125
+ var MAX_RECORDED_HOPS = 10;
21126
+ function resolveReleaseTrack(input) {
21127
+ const tracks = [...input.tracks];
21128
+ const issues = [...input.issues ?? []];
21129
+ const requested = input.requested?.trim();
21130
+ if (tracks.length === 0) {
21131
+ if (requested)
21132
+ return {
21133
+ status: "invalid",
21134
+ error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
21135
+ issues
21136
+ };
21137
+ return { status: "none", issues };
21138
+ }
21139
+ const names = tracks.map((track) => track.name).join(", ");
21140
+ let envProblem = null;
21141
+ const done = (track, source, detail, warnings = [], ambiguous = null) => ({
21142
+ status: "resolved",
21143
+ track: track && track.name !== IMPLICIT ? track : null,
21144
+ source,
21145
+ detail,
21146
+ warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
21147
+ tracks,
21148
+ blocking: envProblem ?? ambiguous
21149
+ });
21150
+ if (requested) {
21151
+ const named = tracks.find((track) => track.name === requested);
21152
+ if (!named)
21153
+ return {
21154
+ status: "invalid",
21155
+ error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
21156
+ issues
21157
+ };
21158
+ return done(named, "flag", `track ${named.name} was requested explicitly`);
21159
+ }
21160
+ const owned = (branch) => branch !== null && tracks.some((track) => [
21161
+ track.integrationBranch,
21162
+ track.productionBranch,
21163
+ track.baseBranch,
21164
+ track.pullRequestTarget
21165
+ ].includes(branch));
21166
+ const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
21167
+ name: IMPLICIT,
21168
+ productionBranch: input.defaultBranch,
21169
+ integrationBranch: input.defaultBranch,
21170
+ baseBranch: input.defaultBranch,
21171
+ pullRequestTarget: input.defaultBranch,
21172
+ mergeBackBranches: [],
21173
+ naming: {}
21174
+ } : null;
21175
+ const candidates = implicit ? [...tracks, implicit] : tracks;
21176
+ const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
21177
+ const fallback = (why) => {
21178
+ const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
21179
+ const track = byDefault.length === 1 ? byDefault[0] : null;
21180
+ const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
21181
+ const ambiguous = candidates.length >= 2 ? `can't tell which release line ${input.branch ?? "HEAD"} is on (${why}); pass --track <name> (${names}) or --base <branch>` : null;
21182
+ return done(track, "default", `${why}; using the workspace ${target}`, [
21183
+ `release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
21184
+ ], ambiguous);
21185
+ };
21186
+ const branch = input.branch;
21187
+ if (branch) {
21188
+ const owners = ownedBy(candidates, branch);
21189
+ if (owners.length === 1)
21190
+ return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
21191
+ if (owners.length > 1)
21192
+ return fallback(`${branch} belongs to more than one track (${owners.map((track) => track.name).join(", ")})`);
21193
+ }
21194
+ const env = input.envRequested?.trim();
21195
+ if (env) {
21196
+ const named = tracks.find((track) => track.name === env);
21197
+ if (named)
21198
+ return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
21199
+ envProblem = `WORKFLOW_RELEASE_TRACK=${env} names no release track of this workspace (${names}); ignored`;
21200
+ }
21201
+ if (!branch)
21202
+ return fallback("HEAD is detached");
21203
+ if (tracks.length === 1 && !implicit)
21204
+ return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
21205
+ let remotes = null;
21206
+ const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
21207
+ const ownerOf = (ref) => {
21208
+ const owner = ownedBy(candidates, short(ref));
21209
+ return owner.length === 1 ? owner[0] : null;
21210
+ };
21211
+ const followRecorded = (start) => {
21212
+ let cursor = start;
21213
+ const seen = new Set([start]);
21214
+ for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
21215
+ const recorded = input.probe.recordedBase(cursor);
21216
+ if (!recorded)
21217
+ return null;
21218
+ const base = short(recorded);
21219
+ const owner = ownerOf(base);
21220
+ if (owner)
21221
+ return owner;
21222
+ if (ownedBy(candidates, base).length > 1 || seen.has(base))
21223
+ return null;
21224
+ seen.add(base);
21225
+ cursor = base;
21226
+ }
21227
+ return null;
21228
+ };
21229
+ const recordedOwner = followRecorded(branch);
21230
+ if (recordedOwner)
21231
+ return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
21232
+ for (const source of ["reflog", "checkout"]) {
21233
+ const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
21234
+ if (!ref)
21235
+ continue;
21236
+ const owner = ownerOf(ref) ?? followRecorded(short(ref));
21237
+ if (owner)
21238
+ return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
21239
+ }
21240
+ const forks = [];
21241
+ for (const track of candidates)
21242
+ for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
21243
+ const sha = input.probe.mergeBase(branch, ref);
21244
+ if (sha)
21245
+ forks.push({ track, ref, sha });
21246
+ }
21247
+ if (forks.length === 0)
21248
+ return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
21249
+ const ancestry = new Map;
21250
+ const descends = (older, newer) => {
21251
+ if (older === newer)
21252
+ return false;
21253
+ const key = `${older}>${newer}`;
21254
+ if (!ancestry.has(key))
21255
+ ancestry.set(key, input.probe.isAncestor(older, newer));
21256
+ return ancestry.get(key);
21257
+ };
21258
+ const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
21259
+ const winningTracks = [...new Set(winners.map((fork) => fork.track))];
21260
+ if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
21261
+ const win = winners[0];
21262
+ return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
21263
+ }
21264
+ if (winningTracks.length === 1)
21265
+ return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
21266
+ const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
21267
+ return fallback(`${branch} has the same or unrelated fork points with ${[...new Set(newest.map((fork) => `${fork.ref} (${fork.track.name === IMPLICIT ? "workspace default" : fork.track.name})`))].join(" and ")}`);
21268
+ }
21269
+ var git4 = (cwd, args) => {
21270
+ const run = spawnSync7("git", args, {
21271
+ cwd,
21272
+ encoding: "utf8",
21273
+ timeout: 20000,
21274
+ windowsHide: true,
21275
+ stdio: ["ignore", "pipe", "ignore"]
21276
+ });
21277
+ return run.status === 0 ? (run.stdout ?? "").trim() : null;
21278
+ };
21279
+ var recordedBaseKey = (branch) => `branch.${branch}.workitBase`;
21280
+ var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
21281
+ var reflog = (cwd, ref) => (git4(cwd, ["log", "-g", "--date=unix", "--format=%H%x09%gd%x09%gs", ref, "--"]) ?? "").split(`
21282
+ `).map((line) => {
21283
+ const [sha = "", selector = "", ...subject] = line.split("\t");
21284
+ return {
21285
+ sha,
21286
+ time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
21287
+ subject: subject.join("\t")
21288
+ };
21289
+ }).filter((entry) => entry.sha && Number.isFinite(entry.time));
21290
+ function gitTrackProbe(cwd) {
21291
+ let remote;
21292
+ let remotes;
21293
+ const tip = (branch) => {
21294
+ remote ??= pushRemoteName(cwd) ?? "origin";
21295
+ return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
21296
+ };
21297
+ return {
21298
+ remotes: () => remotes ??= remoteNames(cwd),
21299
+ recordedBase: (branch) => git4(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
21300
+ createdFrom: (branch) => {
21301
+ const log = git4(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
21302
+ const oldest = log?.split(`
21303
+ `).findLast(Boolean) ?? "";
21304
+ const match = /^branch: Created from (\S+)$/u.exec(oldest);
21305
+ return match && match[1] !== "HEAD" ? match[1] : null;
21306
+ },
21307
+ checkedOutFrom: (branch) => {
21308
+ const own = reflog(cwd, `refs/heads/${branch}`);
21309
+ const created = own.at(-1);
21310
+ if (!created || created.subject !== "branch: Created from HEAD")
21311
+ return null;
21312
+ const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
21313
+ const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
21314
+ return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
21315
+ },
21316
+ mergeBase: (branch, trackBranch) => {
21317
+ const base = tip(trackBranch);
21318
+ return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
21319
+ },
21320
+ isAncestor: (ancestor, descendant) => git4(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
21321
+ };
21322
+ }
21323
+ function workspaceReleaseTracks(cwd) {
21324
+ const raw = resolveRuntimeReleaseTracks(cwd);
21325
+ return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
21326
+ }
21327
+ var memo3 = new Map;
21328
+ var MEMO_LIMIT = 64;
21329
+ function resolveTrackFor(cwd, options) {
21330
+ const read = workspaceReleaseTracks(cwd);
21331
+ if (read.error)
21332
+ return { status: "invalid", error: read.error, issues: read.issues };
21333
+ if (read.tracks.length === 0 && !options.track?.trim())
21334
+ return { status: "none", issues: read.issues };
21335
+ const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
21336
+ const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
21337
+ const input = {
21338
+ tracks: read.tracks,
21339
+ requested: options.track ?? null,
21340
+ envRequested,
21341
+ branch,
21342
+ defaultBranch: options.defaultBranch,
21343
+ issues: read.issues
21344
+ };
21345
+ let key = null;
21346
+ if (!options.probe) {
21347
+ const refs = git4(cwd, [
21348
+ "for-each-ref",
21349
+ "--format=%(refname) %(objectname)",
21350
+ "refs/heads",
21351
+ "refs/remotes"
21352
+ ]) ?? "";
21353
+ const head = git4(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
21354
+ const own = branch ? [
21355
+ git4(cwd, [
21356
+ "log",
21357
+ "-g",
21358
+ "-n",
21359
+ "1",
21360
+ "--date=unix",
21361
+ "--format=%H %gd %gs",
21362
+ `refs/heads/${branch}`,
21363
+ "--"
21364
+ ]) ?? "",
21365
+ git4(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
21366
+ ] : [];
21367
+ key = createHash9("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
21368
+ const hit = memo3.get(key);
21369
+ if (hit)
21370
+ return hit;
21371
+ }
21372
+ const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
21373
+ if (key) {
21374
+ if (memo3.size >= MEMO_LIMIT)
21375
+ memo3.delete(memo3.keys().next().value);
21376
+ memo3.set(key, resolution);
21377
+ }
21378
+ return resolution;
21379
+ }
21380
+
21381
+ // packages/workit-core/src/core/vcs-config.ts
21382
+ var workspacesPath = () => path14.join(configDir(), "workspaces.json");
20953
21383
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
20954
21384
  var remoteProvider = (cwd) => {
20955
- const r = spawnSync7("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
21385
+ const r = spawnSync8("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20956
21386
  if (r.status !== 0)
20957
21387
  return null;
20958
21388
  const url = (r.stdout ?? "").trim();
@@ -20989,7 +21419,7 @@ var remoteEndpoint = (remote) => {
20989
21419
  var configuredSshEndpoint = (host) => {
20990
21420
  if (host.startsWith("-"))
20991
21421
  return null;
20992
- const resolved = spawnSync7("ssh", ["-G", host], {
21422
+ const resolved = spawnSync8("ssh", ["-G", host], {
20993
21423
  encoding: "utf8",
20994
21424
  timeout: 5000,
20995
21425
  maxBuffer: 1024 * 1024,
@@ -21054,7 +21484,16 @@ var readVcsConfig = () => {
21054
21484
  }
21055
21485
  return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
21056
21486
  };
21057
- function vcsConfig(mode, cwd) {
21487
+ var trackSummary = (resolution) => resolution.status === "resolved" ? {
21488
+ name: resolution.track?.name ?? null,
21489
+ source: resolution.source,
21490
+ detail: resolution.detail,
21491
+ warnings: resolution.warnings,
21492
+ track: resolution.track,
21493
+ tracks: resolution.tracks,
21494
+ blocking: resolution.blocking
21495
+ } : null;
21496
+ function vcsConfig(mode, cwd, options = {}) {
21058
21497
  const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
21059
21498
  const wsVcs = ws?.vcs ?? {};
21060
21499
  const wsYt = ws?.youtrack ?? {};
@@ -21064,10 +21503,7 @@ function vcsConfig(mode, cwd) {
21064
21503
  const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
21065
21504
  const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
21066
21505
  const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
21067
- const wp = ws?.branchPolicy ?? {};
21068
- const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
21069
- const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
21070
- const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
21506
+ const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
21071
21507
  const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
21072
21508
  const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
21073
21509
  let issuesProvider = null;
@@ -21079,11 +21515,24 @@ function vcsConfig(mode, cwd) {
21079
21515
  if (mode === "resolve") {
21080
21516
  if (cfgStatus === "malformed")
21081
21517
  return { ok: false, error: cfgError, configPath: cfgPath };
21518
+ const trackResolution = resolveTrackFor(root, {
21519
+ track: options.track,
21520
+ ...options.branch !== undefined ? { branch: options.branch } : {},
21521
+ defaultBranch: workspaceDefault
21522
+ });
21523
+ if (trackResolution.status === "invalid")
21524
+ return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
21525
+ const releaseTrack = trackSummary(trackResolution);
21526
+ const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
21527
+ const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
21082
21528
  return {
21083
21529
  ok: true,
21084
21530
  workspace_name: ws?.name ?? null,
21085
21531
  provider,
21086
21532
  defaultTargetBranch: defaultTarget,
21533
+ workspaceDefaultTargetBranch: workspaceDefault,
21534
+ baseBranch,
21535
+ releaseTrack,
21087
21536
  link_issues: linkIssues,
21088
21537
  youtrack_base_url: youtrackBaseUrl,
21089
21538
  issues_provider: issuesProvider,
@@ -21107,7 +21556,7 @@ function vcsConfig(mode, cwd) {
21107
21556
  ok: true,
21108
21557
  configPath: path14.resolve(cfgPath),
21109
21558
  provider,
21110
- defaultTargetBranch: defaultTarget,
21559
+ defaultTargetBranch: workspaceDefault,
21111
21560
  pr: cfg.pr ?? {},
21112
21561
  workspace_name: ws?.name ?? null,
21113
21562
  link_issues: linkIssues,
@@ -21143,7 +21592,7 @@ function mergedPrStyle(limit = 6, cwd) {
21143
21592
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
21144
21593
  descriptionPreview: desc.slice(0, 600)
21145
21594
  });
21146
- const remoteResult = spawnSync7("git", ["remote", "get-url", "--push", "--all", "origin"], {
21595
+ const remoteResult = spawnSync8("git", ["remote", "get-url", "--push", "--all", "origin"], {
21147
21596
  cwd,
21148
21597
  encoding: "utf8"
21149
21598
  });
@@ -21159,7 +21608,7 @@ function mergedPrStyle(limit = 6, cwd) {
21159
21608
  if (!hostingApiHostMatches(remote, host))
21160
21609
  return { ok: false, error: "PR style target does not match the provider host" };
21161
21610
  if (provider === "gitlab") {
21162
- const run = (args) => spawnSync7("glab", ["api", ...args], {
21611
+ const run = (args) => spawnSync8("glab", ["api", ...args], {
21163
21612
  cwd,
21164
21613
  encoding: "utf8",
21165
21614
  env
@@ -21179,7 +21628,7 @@ function mergedPrStyle(limit = 6, cwd) {
21179
21628
  });
21180
21629
  }
21181
21630
  } else if (provider === "github") {
21182
- const r = spawnSync7("gh", [
21631
+ const r = spawnSync8("gh", [
21183
21632
  "pr",
21184
21633
  "list",
21185
21634
  "--repo",
@@ -21210,20 +21659,6 @@ function mergedPrStyle(limit = 6, cwd) {
21210
21659
  examples
21211
21660
  };
21212
21661
  }
21213
-
21214
- // packages/workit-core/src/core/branch.ts
21215
- function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
21216
- const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
21217
- const config = readConfig();
21218
- return kind === "branch" ? {
21219
- branchPolicy: resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null),
21220
- provenance: { branchPolicy: selected.source }
21221
- } : {
21222
- commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
21223
- provenance: { commitPolicy: selected.source }
21224
- };
21225
- }
21226
- var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
21227
21662
  // packages/workit-core/src/core/task-engine.ts
21228
21663
  import { realpathSync as realpathSync4 } from "node:fs";
21229
21664
 
@@ -23077,7 +23512,7 @@ class WorkitCore {
23077
23512
  }
23078
23513
  }
23079
23514
  // packages/workit-core/src/core/repo-context.ts
23080
- import { spawnSync as spawnSync8 } from "node:child_process";
23515
+ import { spawnSync as spawnSync9 } from "node:child_process";
23081
23516
  import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "node:fs";
23082
23517
  import path16 from "node:path";
23083
23518
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -23106,7 +23541,7 @@ var invalidRange = (cwd) => ({
23106
23541
  cwd
23107
23542
  });
23108
23543
  var runGit = (cwd, args) => {
23109
- const result = spawnSync8("git", args, { cwd, encoding: "utf8" });
23544
+ const result = spawnSync9("git", args, { cwd, encoding: "utf8" });
23110
23545
  return {
23111
23546
  stdout: (result.stdout ?? "").trimEnd(),
23112
23547
  stderr: (result.stderr ?? "").trimEnd(),
@@ -23547,12 +23982,12 @@ function releaseNotesContext(root, rangeOrTag) {
23547
23982
  }
23548
23983
 
23549
23984
  // packages/workit-core/src/core/tracker-issues.ts
23550
- import { spawnSync as spawnSync10 } from "node:child_process";
23985
+ import { spawnSync as spawnSync11 } from "node:child_process";
23551
23986
 
23552
23987
  // packages/workit-core/src/core/pr-create.ts
23553
23988
  import fs13 from "node:fs";
23554
23989
  import path17 from "node:path";
23555
- import { spawnSync as spawnSync9 } from "node:child_process";
23990
+ import { spawnSync as spawnSync10 } from "node:child_process";
23556
23991
  // packages/workit-core/src/forge/pr-body.ts
23557
23992
  function parseGhIssue(value) {
23558
23993
  const m = /issues\/(\d+)/.exec(value);
@@ -23577,7 +24012,7 @@ function whichOnPath(tool) {
23577
24012
  }
23578
24013
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
23579
24014
  var pushRemote = (cwd) => {
23580
- const result = spawnSync9("git", ["remote", "get-url", "--push", "--all", "origin"], {
24015
+ const result = spawnSync10("git", ["remote", "get-url", "--push", "--all", "origin"], {
23581
24016
  cwd,
23582
24017
  encoding: "utf8"
23583
24018
  });
@@ -23608,7 +24043,7 @@ var defaultCli = (bin, available) => {
23608
24043
  if (!available)
23609
24044
  return null;
23610
24045
  return (args, root) => {
23611
- const out = spawnSync10(bin, args, {
24046
+ const out = spawnSync11(bin, args, {
23612
24047
  cwd: root,
23613
24048
  encoding: "utf8",
23614
24049
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -24126,7 +24561,7 @@ function resolveInside(root, candidate) {
24126
24561
  }
24127
24562
  function run2(root, executable, args, env = {}) {
24128
24563
  const cwd = realpathSync5(root);
24129
- const result = spawnSync11(executable, args, {
24564
+ const result = spawnSync12(executable, args, {
24130
24565
  cwd,
24131
24566
  encoding: "utf8",
24132
24567
  env: { ...process.env, ...env }
@@ -26572,8 +27007,8 @@ var SENSITIVE_WORDS = new Set([
26572
27007
  "query"
26573
27008
  ]);
26574
27009
  var STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
26575
- var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26576
- var homePattern = new RegExp(`${escapeRegExp(os4.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
27010
+ var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27011
+ var homePattern = new RegExp(`${escapeRegExp2(os4.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
26577
27012
  var applyPatterns = (value) => {
26578
27013
  let out = value;
26579
27014
  out = out.replace(SECRET_VALUE, REDACTED);
@@ -13872,6 +13872,7 @@ var readConfig = () => {
13872
13872
  throw new Error(result.error);
13873
13873
  return result.config ?? DEFAULTS;
13874
13874
  };
13875
+ var branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i");
13875
13876
  var resolveBranchPolicy = (config, workspace) => {
13876
13877
  const wp = workspace?.branchPolicy ?? {};
13877
13878
  const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
@@ -13879,7 +13880,7 @@ var resolveBranchPolicy = (config, workspace) => {
13879
13880
  allowed: wp.allowed,
13880
13881
  protectedNames: wp.protected
13881
13882
  }, config);
13882
- const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
13883
+ const allowed = merged.allowed.map(branchGlobPattern);
13883
13884
  return {
13884
13885
  preset,
13885
13886
  allowed,
@@ -13944,26 +13945,31 @@ var versionSourceSchema = discriminatedUnion("kind", [
13944
13945
  object({ kind: literal("manual") }).strict()
13945
13946
  ]);
13946
13947
  var releaseTrackSchema = object({
13947
- strategy: branchPreset,
13948
+ strategy: branchPreset.optional(),
13948
13949
  productionBranch: nonBlank,
13949
13950
  integrationBranch: nonBlank,
13950
- naming: object({ feature: nonBlank, release: nonBlank, hotfix: nonBlank }).strict(),
13951
- baseBranch: nonBlank,
13952
- mergeBackBranches: array(nonBlank),
13953
- pullRequestTarget: nonBlank,
13954
- tagNamespace: string2(),
13955
- versionSource: versionSourceSchema,
13956
- requiredChecks: array(nonBlank)
13957
- }).strict().superRefine((track, context) => {
13951
+ naming: object({
13952
+ feature: nonBlank.optional(),
13953
+ release: nonBlank.optional(),
13954
+ hotfix: nonBlank.optional()
13955
+ }).passthrough().optional(),
13956
+ baseBranch: nonBlank.optional(),
13957
+ mergeBackBranches: array(nonBlank).optional(),
13958
+ pullRequestTarget: nonBlank.optional(),
13959
+ tagNamespace: string2().optional(),
13960
+ versionSource: versionSourceSchema.optional(),
13961
+ requiredChecks: array(nonBlank).optional(),
13962
+ critical: array(nonBlank).optional()
13963
+ }).passthrough().superRefine((track, context) => {
13958
13964
  for (const [field, values] of [
13959
- ["mergeBackBranches", track.mergeBackBranches],
13960
- ["requiredChecks", track.requiredChecks]
13965
+ ["mergeBackBranches", track.mergeBackBranches ?? []],
13966
+ ["requiredChecks", track.requiredChecks ?? []]
13961
13967
  ]) {
13962
13968
  if (new Set(values).size !== values.length) {
13963
13969
  context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
13964
13970
  }
13965
13971
  }
13966
- if (track.versionSource.kind === "package-json" && (path13.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
13972
+ if (track.versionSource?.kind === "package-json" && (path13.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
13967
13973
  context.addIssue({
13968
13974
  code: "custom",
13969
13975
  path: ["versionSource", "path"],
@@ -14230,7 +14236,7 @@ var runtimeWorkspacePolicySchema = (kind) => {
14230
14236
  var runtimeWorkspaceIndexSchema = object({
14231
14237
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
14232
14238
  }).passthrough();
14233
- var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
14239
+ var matchRuntimeWorkspace = (cwd) => {
14234
14240
  const file = path13.join(configDir(), "workspaces.json");
14235
14241
  let raw;
14236
14242
  try {
@@ -14265,8 +14271,17 @@ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
14265
14271
  });
14266
14272
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
14267
14273
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
14268
- if (!selected)
14274
+ return selected ? { file, selected } : null;
14275
+ };
14276
+ var resolveRuntimeReleaseTracks = (cwd) => {
14277
+ const matched = matchRuntimeWorkspace(cwd);
14278
+ return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
14279
+ };
14280
+ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
14281
+ const matched = matchRuntimeWorkspace(cwd);
14282
+ if (!matched)
14269
14283
  return null;
14284
+ const { file, selected } = matched;
14270
14285
  const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
14271
14286
  if (!policy.success) {
14272
14287
  const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
@@ -16230,11 +16245,176 @@ var turnContextText = (input, descriptor) => {
16230
16245
  };
16231
16246
  // packages/workit-core/src/core/branch.ts
16232
16247
  import { createHash as createHash9 } from "node:crypto";
16248
+
16249
+ // packages/workit-core/src/core/release-tracks.ts
16250
+ class ReleaseTrackError extends Error {
16251
+ }
16252
+ var KNOWN_FIELDS = new Set([
16253
+ "strategy",
16254
+ "productionBranch",
16255
+ "integrationBranch",
16256
+ "naming",
16257
+ "baseBranch",
16258
+ "mergeBackBranches",
16259
+ "pullRequestTarget",
16260
+ "tagNamespace",
16261
+ "versionSource",
16262
+ "requiredChecks",
16263
+ "critical"
16264
+ ]);
16265
+ var BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
16266
+ var PLACEHOLDER = /\{[^}]*\}/gu;
16267
+ var HAS_PLACEHOLDER = /\{[^}]*\}/u;
16268
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16269
+ var branchName = (value) => typeof value === "string" && value.trim() && !value.trim().startsWith("-") ? value.trim() : null;
16270
+ var namedBranches = (entry) => [
16271
+ ...BRANCH_FIELDS.map((field) => branchName(entry[field])),
16272
+ ...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
16273
+ ].filter((name) => name !== null);
16274
+ var literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim();
16275
+ function readReleaseTracks(raw) {
16276
+ const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
16277
+ if (raw === undefined || raw === null)
16278
+ return out;
16279
+ if (!isRecord3(raw)) {
16280
+ out.issues.push("releaseTracks is not an object; ignored");
16281
+ return out;
16282
+ }
16283
+ const fatal = [];
16284
+ const protectedNames = new Set;
16285
+ for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
16286
+ const label = `release track ${JSON.stringify(name)}`;
16287
+ if (!isRecord3(entry)) {
16288
+ out.issues.push(`${label} is not an object; ignored`);
16289
+ continue;
16290
+ }
16291
+ for (const branch of namedBranches(entry))
16292
+ protectedNames.add(branch);
16293
+ if (!name.trim()) {
16294
+ out.issues.push("a release track with a blank name was ignored");
16295
+ continue;
16296
+ }
16297
+ let critical = [];
16298
+ if (Array.isArray(entry.critical))
16299
+ critical = entry.critical.filter((item) => typeof item === "string");
16300
+ else if (entry.critical !== undefined)
16301
+ out.issues.push(`${label} critical: must be a list of field names; ignored`);
16302
+ const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
16303
+ const problem = (field, message) => {
16304
+ if (isCritical(field))
16305
+ fatal.push(`${label} ${field}: ${message}`);
16306
+ else
16307
+ out.issues.push(`${label} ${field}: ${message}`);
16308
+ };
16309
+ for (const key of Object.keys(entry))
16310
+ if (!KNOWN_FIELDS.has(key))
16311
+ problem(key, "unknown field, ignored");
16312
+ const productionBranch = branchName(entry.productionBranch);
16313
+ const integrationBranch = branchName(entry.integrationBranch);
16314
+ if (!productionBranch)
16315
+ problem("productionBranch", "missing or not a branch name");
16316
+ if (!integrationBranch)
16317
+ problem("integrationBranch", "missing or not a branch name");
16318
+ const optionalBranch = (field) => {
16319
+ if (entry[field] === undefined)
16320
+ return null;
16321
+ const value = branchName(entry[field]);
16322
+ if (!value)
16323
+ problem(field, "not a branch name; using the integration branch");
16324
+ return value;
16325
+ };
16326
+ const baseBranch = optionalBranch("baseBranch");
16327
+ const pullRequestTarget = optionalBranch("pullRequestTarget");
16328
+ let mergeBackBranches = [];
16329
+ if (entry.mergeBackBranches !== undefined) {
16330
+ const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
16331
+ if (list.some((item) => item === null))
16332
+ problem("mergeBackBranches", "must be a list of branch names; ignored");
16333
+ else
16334
+ mergeBackBranches = [...new Set(list)];
16335
+ }
16336
+ const naming = {};
16337
+ if (entry.naming !== undefined) {
16338
+ if (!isRecord3(entry.naming))
16339
+ problem("naming", "not an object; ignored");
16340
+ else
16341
+ for (const kind of ["feature", "release", "hotfix"]) {
16342
+ const template = entry.naming[kind];
16343
+ if (template === undefined)
16344
+ continue;
16345
+ if (typeof template !== "string" || !template.trim())
16346
+ problem(`naming.${kind}`, "not a template string; ignored");
16347
+ else if (!literalOf(template))
16348
+ problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
16349
+ else
16350
+ naming[kind] = template.trim();
16351
+ }
16352
+ }
16353
+ if (!productionBranch || !integrationBranch) {
16354
+ out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
16355
+ continue;
16356
+ }
16357
+ out.tracks.push({
16358
+ name,
16359
+ productionBranch,
16360
+ integrationBranch,
16361
+ baseBranch: baseBranch ?? integrationBranch,
16362
+ pullRequestTarget: pullRequestTarget ?? integrationBranch,
16363
+ mergeBackBranches,
16364
+ naming
16365
+ });
16366
+ }
16367
+ out.protectedBranches = [...protectedNames];
16368
+ if (fatal.length)
16369
+ out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
16370
+ return out;
16371
+ }
16372
+ var trackBranches = (tracks) => [
16373
+ ...new Set(tracks.flatMap((track) => [
16374
+ track.productionBranch,
16375
+ track.integrationBranch,
16376
+ track.baseBranch,
16377
+ track.pullRequestTarget,
16378
+ ...track.mergeBackBranches
16379
+ ]))
16380
+ ];
16381
+ var namingGlob = (template) => HAS_PLACEHOLDER.test(template) ? template.replace(PLACEHOLDER, "*") : `${template.replace(/\/?\*?$/u, "")}/*`;
16382
+ function workspaceReleaseTracks(cwd) {
16383
+ const raw = resolveRuntimeReleaseTracks(cwd);
16384
+ return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
16385
+ }
16386
+ var memo3 = new Map;
16387
+
16388
+ // packages/workit-core/src/core/branch.ts
16389
+ var withReleaseTracks = (workspaceRoot, policy) => {
16390
+ const read = workspaceReleaseTracks(workspaceRoot);
16391
+ if (read.error)
16392
+ throw new ReleaseTrackError(read.error);
16393
+ if (read.tracks.length === 0 && read.protectedBranches.length === 0)
16394
+ return policy;
16395
+ const known = new Set(policy.allowed.map((pattern) => pattern.source));
16396
+ const allowed = [...policy.allowed];
16397
+ for (const glob of read.tracks.flatMap((track) => Object.values(track.naming).map(namingGlob))) {
16398
+ const pattern = branchGlobPattern(glob);
16399
+ if (!known.has(pattern.source)) {
16400
+ known.add(pattern.source);
16401
+ allowed.push(pattern);
16402
+ }
16403
+ }
16404
+ return {
16405
+ ...policy,
16406
+ allowed,
16407
+ protected: new Set([
16408
+ ...policy.protected,
16409
+ ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
16410
+ ])
16411
+ };
16412
+ };
16233
16413
  function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
16234
16414
  const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
16235
16415
  const config = readConfig();
16236
16416
  return kind === "branch" ? {
16237
- branchPolicy: resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null),
16417
+ branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null)),
16238
16418
  provenance: { branchPolicy: selected.source }
16239
16419
  } : {
16240
16420
  commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
@@ -16583,7 +16763,7 @@ function writeGate(cwd, paths) {
16583
16763
  // packages/workit-core/src/hooks/hosts/fields.ts
16584
16764
  import { existsSync as existsSync6, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
16585
16765
  import path17 from "node:path";
16586
- var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16766
+ var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16587
16767
  var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
16588
16768
  var optionalText = (value) => typeof value === "string" ? value : null;
16589
16769
  var existingDirectory = (value) => {
@@ -16639,7 +16819,7 @@ var WRITE_TOOLS = new Set([
16639
16819
  var isWriteTool = (name) => typeof name === "string" && WRITE_TOOLS.has(name.toLowerCase().replace(/[_-]/g, ""));
16640
16820
  var PATH_KEYS = ["file_path", "filePath", "notebook_path", "path", "target_file", "file"];
16641
16821
  var writePaths = (input) => {
16642
- if (!isRecord3(input))
16822
+ if (!isRecord4(input))
16643
16823
  return [];
16644
16824
  const paths = PATH_KEYS.map((key) => input[key]).filter(nonEmpty2);
16645
16825
  for (const value of Object.values(input))
@@ -16843,7 +17023,7 @@ var EVENTS = {
16843
17023
  };
16844
17024
  var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
16845
17025
  var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
16846
- var toolCommand = (value) => isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
17026
+ var toolCommand = (value) => isRecord4(value.tool_input) ? commandText(value.tool_input.command) : null;
16847
17027
  var eventOf = (name, value) => {
16848
17028
  const toolUseId = optionalText(value.tool_use_id);
16849
17029
  switch (name) {
@@ -16873,7 +17053,7 @@ var eventOf = (name, value) => {
16873
17053
  const command = toolCommand(value);
16874
17054
  if (!SHELL_TOOLS.has(String(value.tool_name)) || !command)
16875
17055
  return { ok: false, error: "only shell PostToolUse is mapped" };
16876
- const response = isRecord3(value.tool_response) ? value.tool_response : {};
17056
+ const response = isRecord4(value.tool_response) ? value.tool_response : {};
16877
17057
  return {
16878
17058
  ok: true,
16879
17059
  event: {
@@ -16949,14 +17129,14 @@ var render = (decision, native) => {
16949
17129
  var claudeCodeAdapter = {
16950
17130
  descriptor: CLAUDE_CODE_DESCRIPTOR,
16951
17131
  parse(raw) {
16952
- const native = isRecord3(raw) && Object.hasOwn(EVENTS, String(raw.hook_event_name)) ? raw.hook_event_name : null;
17132
+ const native = isRecord4(raw) && Object.hasOwn(EVENTS, String(raw.hook_event_name)) ? raw.hook_event_name : null;
16953
17133
  const fail = (error) => ({
16954
17134
  ok: false,
16955
17135
  error,
16956
17136
  native,
16957
17137
  event: native ? EVENTS[native] : null
16958
17138
  });
16959
- if (!isRecord3(raw) || !native)
17139
+ if (!isRecord4(raw) || !native)
16960
17140
  return fail("hook_event_name is required");
16961
17141
  if (!nonEmpty2(raw.session_id))
16962
17142
  return fail("session_id is required");
@@ -17099,7 +17279,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
17099
17279
  var isShellTool = (name) => SHELL_TOOLS2.has(String(name).toLowerCase());
17100
17280
  var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
17101
17281
  var parseCodexHookInput = (value) => {
17102
- if (!isRecord3(value) || !Object.hasOwn(EVENTS2, String(value.hook_event_name)))
17282
+ if (!isRecord4(value) || !Object.hasOwn(EVENTS2, String(value.hook_event_name)))
17103
17283
  return { ok: false, error: "hook_event_name is required" };
17104
17284
  const event = value.hook_event_name;
17105
17285
  const cwd = existingDirectory(value.cwd);
@@ -17107,7 +17287,7 @@ var parseCodexHookInput = (value) => {
17107
17287
  return { ok: false, error: "cwd must be an existing absolute directory" };
17108
17288
  if (event === "PreToolUse" && !nonEmpty2(value.tool_name))
17109
17289
  return { ok: false, error: "tool_name is required" };
17110
- const command = isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
17290
+ const command = isRecord4(value.tool_input) ? commandText(value.tool_input.command) : null;
17111
17291
  if (event === "PreToolUse" && isShellTool(value.tool_name) && !command)
17112
17292
  return { ok: false, error: "tool_input.command is required for shell tools" };
17113
17293
  if (event === "SubagentStart" && !nonEmpty2(value.agent_id))
@@ -17190,7 +17370,7 @@ var codexAdapter = {
17190
17370
  const host = detectCodexSurface(env);
17191
17371
  const parsed = parseCodexHookInput(raw);
17192
17372
  if (!parsed.ok) {
17193
- const native = isRecord3(raw) && Object.hasOwn(EVENTS2, String(raw.hook_event_name)) ? raw.hook_event_name : null;
17373
+ const native = isRecord4(raw) && Object.hasOwn(EVENTS2, String(raw.hook_event_name)) ? raw.hook_event_name : null;
17194
17374
  return { ok: false, error: parsed.error, native, event: native ? EVENTS2[native] : null };
17195
17375
  }
17196
17376
  const input = parsed.data;
@@ -17329,7 +17509,7 @@ var EVENTS3 = {
17329
17509
  preCompact: "compact.pre"
17330
17510
  };
17331
17511
  var parseCursorHookInput = (value) => {
17332
- if (!isRecord3(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
17512
+ if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
17333
17513
  return { ok: false, error: "hook_event_name is required" };
17334
17514
  const roots = value.workspace_roots;
17335
17515
  if (!Array.isArray(roots) || roots.length === 0 || !roots.every(nonEmpty2))
@@ -17435,7 +17615,7 @@ var cursorAdapter = {
17435
17615
  parse(raw) {
17436
17616
  const parsed = parseCursorHookInput(raw);
17437
17617
  if (!parsed.ok) {
17438
- const native = isRecord3(raw) && Object.hasOwn(EVENTS3, String(raw.hook_event_name)) ? raw.hook_event_name : null;
17618
+ const native = isRecord4(raw) && Object.hasOwn(EVENTS3, String(raw.hook_event_name)) ? raw.hook_event_name : null;
17439
17619
  return { ok: false, error: parsed.error, native, event: native ? EVENTS3[native] : null };
17440
17620
  }
17441
17621
  const input = parsed.data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-codex",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "private": false,
5
5
  "description": "Workit Codex CLI/desktop plugin — shared MCP transport, hooks, and sixteen method skills",
6
6
  "keywords": [
@@ -38,8 +38,8 @@
38
38
  "build": "bun scripts/build.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@brainervirus/workit-core": "^7.0.0",
42
- "@brainervirus/workit-mcp": "^7.0.0"
41
+ "@brainervirus/workit-core": "^7.1.0",
42
+ "@brainervirus/workit-mcp": "^7.1.0"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=24"