@brainervirus/workit-core 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "private": false,
5
5
  "description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
6
6
  "keywords": [
@@ -1271,9 +1271,20 @@ export class HostReceiptStore {
1271
1271
  }
1272
1272
  }
1273
1273
 
1274
- /** Menu labels compare case-insensitively: the host presents "Inline", the
1275
- * enum stores "inline" (FINDING 3). */
1276
- const sameChoiceLabel = (a: string, b: string): boolean => a.toLowerCase() === b.toLowerCase();
1274
+ /** Menu labels compare semantically: hosts decorate choices with
1275
+ * parenthesized qualifiers ("Handoff (new session only)") that the enum does
1276
+ * not carry, so we strip them, trim, collapse whitespace, and lowercase both
1277
+ * sides before comparing. Only the comparison normalizes — the stored label
1278
+ * and evidence bytes are preserved verbatim. */
1279
+ const sameChoiceLabel = (a: string, b: string): boolean => normalizeLabel(a) === normalizeLabel(b);
1280
+
1281
+ const normalizeLabel = (s: string): string =>
1282
+ s
1283
+ .replace(/\s*\([^)]*\)/g, " ")
1284
+ .replace(/\s*\bfirst\b\s*$/i, " ")
1285
+ .replace(/[^a-z0-9]+/gi, " ")
1286
+ .trim()
1287
+ .toLowerCase();
1277
1288
 
1278
1289
  /** Derive the evidence record from a consumed host receipt (AR-12). */
1279
1290
  export const createOpenCodeEvidence = (receipt: HostReceipt): OpenCodeChoiceEvidence => ({
@@ -147,9 +147,15 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
147
147
  // target is validated against the resolved branch policy so a PR can never
148
148
  // be aimed at a protected or disallowed branch.
149
149
  const targetOverride = env.WF_PR_TARGET;
150
- const target =
151
- targetOverride || String(cfg.defaultTargetBranch ?? policy.defaultTargetBranch ?? "develop");
152
- if (targetOverride) {
150
+ const resolvedDefault = String(
151
+ cfg.defaultTargetBranch ?? policy.defaultTargetBranch ?? "develop",
152
+ );
153
+ const target = targetOverride || resolvedDefault;
154
+ // CA-06: an explicit override equal to the resolved default (e.g. WF_PR_TARGET
155
+ // "main" under github-flow) is authoritative — the same value flows
156
+ // unvalidated from config, so it must not be rejected as a protected
157
+ // override. Genuine differing overrides keep the strict validation.
158
+ if (targetOverride && targetOverride !== resolvedDefault) {
153
159
  const { allowed, protected: protectedTargets } = policy;
154
160
  if (protectedTargets.has(targetOverride.toLowerCase()))
155
161
  return {
@@ -283,6 +289,31 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
283
289
  // uv_spawn fails with ENOENT even though the CLI is on PATH.
284
290
  cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GITLAB_TOKEN: token };
285
291
  } else {
292
+ // T2: GitHub has no --push flag — push the branch first so `gh pr create`
293
+ // never runs against an unpushed branch when pushBranch is enabled.
294
+ if (push) {
295
+ if (!branch) {
296
+ return {
297
+ error: "push failed",
298
+ provider,
299
+ mode: "push",
300
+ targetBranch: target,
301
+ stderr: "empty current branch (detached HEAD or unborn HEAD)",
302
+ };
303
+ }
304
+ const pushRes = spawnSync("git", ["push", "-u", "origin", branch], {
305
+ cwd: root,
306
+ encoding: "utf8",
307
+ });
308
+ if (pushRes.status !== 0) {
309
+ return {
310
+ error: "push failed",
311
+ provider,
312
+ targetBranch: target,
313
+ stderr: (pushRes.stderr ?? "").slice(0, 800),
314
+ };
315
+ }
316
+ }
286
317
  cmd = ["gh", "pr", "create", "--title", title, "--base", target];
287
318
  if (finalBody) cmd.push("--body", finalBody);
288
319
  if (draft) cmd.push("--draft");
@@ -4,8 +4,10 @@ import { DESTINATION_MENU_LABELS, HANDOFF_DESTINATION_MARKER, SOURCE_MENU_LABELS
4
4
  // the reminder PROSE like the other source surfaces (bootstrap.ts, session-start,
5
5
  // superpowers-doc-contract.md, ask-question-only.mdc), never in the machine
6
6
  // label tuple `SOURCE_MENU_LABELS` — the receipt label must stay exactly
7
- // `Handoff` for the native-question match (AR-12).
8
- const SOURCE_MENU_LABELS_DISPLAY = SOURCE_MENU_LABELS.map((label) =>
7
+ // `Handoff` for the native-question match (AR-12). Exported so contract tests
8
+ // assert the rendered reminder against this single source of truth instead of
9
+ // re-deriving the mapping.
10
+ export const SOURCE_MENU_LABELS_DISPLAY = SOURCE_MENU_LABELS.map((label) =>
9
11
  label === "Handoff" ? "Handoff (new session only)" : label,
10
12
  );
11
13
 
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
- import { configDir } from "./config";
4
+ import { configDir, PRESETS } from "./config";
5
5
  import { resolveWorkspace } from "./workspaces";
6
6
  import { resolveBranchPolicyFor } from "./branch";
7
7
  // Ports of scripts/vcs/config.sh + verify-token.sh + token-create-urls.sh + merged-style.sh.
@@ -90,10 +90,17 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
90
90
  // and resolve so both surfaces stay consistent.
91
91
  // CA-09: the one resolver wrapper — same policy resolution every consumer
92
92
  // uses, so the tightened gate in branch-policy-resolver.test.ts stays green.
93
+ // CA-02: a matched workspace's OWN branchPolicy default beats any global
94
+ // vcs.json default (PR #43: global develop shadowed the personal github-flow
95
+ // main). Explicit workspace vcs.defaultTargetBranch stays authoritative; a
96
+ // workspace without a branchPolicy still falls back to the global vcs.json
97
+ // default, and unmatched repos keep it too.
98
+ const wp = (ws?.branchPolicy ?? {}) as Record<string, any>;
99
+ const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
100
+ const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
93
101
  const defaultTarget = String(
94
102
  wsVcs.defaultTargetBranch ??
95
- cfg.defaultTargetBranch ??
96
- resolveBranchPolicyFor(root).defaultTargetBranch ??
103
+ (hasWorkspacePolicy ? policyDefault : (cfg.defaultTargetBranch ?? policyDefault)) ??
97
104
  "develop",
98
105
  );
99
106
  const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;