@brainervirus/workit-mcp 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 (2) hide show
  1. package/dist/index.js +849 -282
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6620,11 +6620,11 @@ var require_dist = __commonJS(function(exports, module) {
6620
6620
 
6621
6621
  // packages/workit-mcp/src/index.ts
6622
6622
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6623
- import path15 from "node:path";
6623
+ import path16 from "node:path";
6624
6624
 
6625
6625
  // packages/workit-mcp/src/server.ts
6626
- import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync7 } from "node:fs";
6627
- import path14 from "node:path";
6626
+ import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync8 } from "node:fs";
6627
+ import path15 from "node:path";
6628
6628
 
6629
6629
  // node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/core.js
6630
6630
  var NEVER = Object.freeze({
@@ -12477,8 +12477,8 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
12477
12477
 
12478
12478
  // packages/workit-core/src/core.ts
12479
12479
  import { spawnSync as spawnSync3 } from "node:child_process";
12480
- import { existsSync as existsSync5, realpathSync as realpathSync6 } from "node:fs";
12481
- import path10 from "node:path";
12480
+ import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
12481
+ import path11 from "node:path";
12482
12482
 
12483
12483
  // packages/workit-core/src/core/task-contract.ts
12484
12484
  import { createHash, randomUUID } from "node:crypto";
@@ -17785,6 +17785,17 @@ function _array2(Class, element, params) {
17785
17785
  ...normalizeParams2(params)
17786
17786
  });
17787
17787
  }
17788
+ function _custom2(Class, fn, _params) {
17789
+ const norm = normalizeParams2(_params);
17790
+ norm.abort ?? (norm.abort = true);
17791
+ const schema = new Class({
17792
+ type: "custom",
17793
+ check: "custom",
17794
+ fn,
17795
+ ...norm
17796
+ });
17797
+ return schema;
17798
+ }
17788
17799
  function _refine2(Class, fn, _params) {
17789
17800
  const schema = new Class({
17790
17801
  type: "custom",
@@ -19938,6 +19949,9 @@ var ZodCustom2 = /* @__PURE__ */ $constructor2("ZodCustom", (inst, def) => {
19938
19949
  ZodType2.init(inst, def);
19939
19950
  inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);
19940
19951
  });
19952
+ function custom2(fn, _params) {
19953
+ return _custom2(ZodCustom2, fn ?? (() => true), _params);
19954
+ }
19941
19955
  function refine2(fn, _params = {}) {
19942
19956
  return _refine2(ZodCustom2, fn, _params);
19943
19957
  }
@@ -20371,7 +20385,8 @@ var decisionSchema = object4({
20371
20385
  consumption: object4({
20372
20386
  state: _enum2(["reserved", "consumed", "uncertain"]),
20373
20387
  at: utc,
20374
- actionRef: refSchema
20388
+ actionRef: refSchema,
20389
+ historicalActionId: text.optional()
20375
20390
  }).strict().nullable()
20376
20391
  }).strict();
20377
20392
  var findingSchema = object4({
@@ -20514,7 +20529,7 @@ var taskSummarySchema = object4({
20514
20529
  requirements: array3(requirementEvaluationSchema),
20515
20530
  writer: workspaceRecordSchema.shape.writer
20516
20531
  }).strict();
20517
- var taskListItemSchema = taskSummarySchema.omit({ policy: true, requirements: true });
20532
+ var taskListItemSchema = taskSummarySchema.omit({ policy: true, requirements: true }).extend({ source: provenanceSchema.pick({ host: true, kind: true }) }).strict();
20518
20533
  var exportBundleSchema = object4({
20519
20534
  schemaVersion: literal3(1),
20520
20535
  exportedAt: utc,
@@ -20539,6 +20554,7 @@ var taskOperations = {
20539
20554
  list: operation({
20540
20555
  action: literal3("list"),
20541
20556
  status: _enum2(["open", "closed", "all"]).optional(),
20557
+ query: string5().trim().min(1).max(200).optional(),
20542
20558
  limit: number5().int().min(1).max(50).optional()
20543
20559
  }),
20544
20560
  inspect: operation({
@@ -20886,6 +20902,7 @@ function requirementId(input) {
20886
20902
  }
20887
20903
  // packages/workit-core/src/core/task-store.ts
20888
20904
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
20905
+ import { AsyncLocalStorage } from "node:async_hooks";
20889
20906
  import * as fs4 from "node:fs";
20890
20907
  import { hostname as hostname2 } from "node:os";
20891
20908
  import path4 from "node:path";
@@ -21521,7 +21538,8 @@ var metadataLockSchema = object4({
21521
21538
  pid: number5().int().nonnegative().safe(),
21522
21539
  processStart: string5().nullable(),
21523
21540
  host: string5().min(1),
21524
- nonce: string5().min(1)
21541
+ nonce: string5().min(1),
21542
+ externalAction: literal3(true).optional()
21525
21543
  }).strict();
21526
21544
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
21527
21545
  var cachedRuntimeVersion = null;
@@ -21573,6 +21591,28 @@ var sameMetadataLock = (left, right) => {
21573
21591
  const parsed = metadataLockSchema.safeParse(left);
21574
21592
  return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
21575
21593
  };
21594
+ var sameDirectoryIdentity = (left, right) => {
21595
+ if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
21596
+ return false;
21597
+ const normalizedLeft = path4.resolve(left);
21598
+ const normalizedRight = path4.resolve(right);
21599
+ try {
21600
+ const a = fs4.statSync(normalizedLeft, { bigint: true });
21601
+ const b = fs4.statSync(normalizedRight, { bigint: true });
21602
+ if (!a.isDirectory() || !b.isDirectory())
21603
+ return false;
21604
+ if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
21605
+ return true;
21606
+ const canonicalLeft = fs4.realpathSync(normalizedLeft);
21607
+ const canonicalRight = fs4.realpathSync(normalizedRight);
21608
+ if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
21609
+ return true;
21610
+ return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
21611
+ } catch {
21612
+ return false;
21613
+ }
21614
+ };
21615
+ var externalActionLockRoots = new AsyncLocalStorage;
21576
21616
 
21577
21617
  class TaskStore {
21578
21618
  root;
@@ -21633,7 +21673,7 @@ class TaskStore {
21633
21673
  const item = this.readRecord(this.workspacePath, workspaceRecordSchema);
21634
21674
  if (!item.exists)
21635
21675
  return success(null, null, null);
21636
- if (item.result.ok && item.result.data.root !== this.root)
21676
+ if (item.result.ok && item.result.data.root !== this.root && !sameDirectoryIdentity(item.result.data.root, this.root))
21637
21677
  return failure("recovery_required", "workspace root binding is invalid", {
21638
21678
  path: this.workspacePath
21639
21679
  });
@@ -21724,6 +21764,29 @@ class TaskStore {
21724
21764
  const current = this.readWorkspace();
21725
21765
  if (!current.ok)
21726
21766
  return current;
21767
+ const origin = input.task.origin;
21768
+ if (current.data && origin) {
21769
+ if (origin.workspaceId === current.data.id) {
21770
+ const source = this.readTask(origin.taskId);
21771
+ if (source.ok)
21772
+ return success(source.data.revision, current.data.revision, source.data);
21773
+ if (source.code !== "not_found")
21774
+ return source;
21775
+ return failure("not_found", "source task is not present in this shared workspace");
21776
+ }
21777
+ const tasks = this.listTasks();
21778
+ if (!tasks.ok)
21779
+ return tasks;
21780
+ const prior = tasks.data.filter((task) => task.origin?.workspaceId === origin.workspaceId && task.origin?.taskId === origin.taskId);
21781
+ if (prior.length > 1)
21782
+ return failure("recovery_required", `source task has multiple imported mappings: ${prior.map((task) => task.id).join(", ")}`);
21783
+ if (prior.length === 1) {
21784
+ const existing = prior[0];
21785
+ if (existing.origin.exportDigest !== origin.exportDigest)
21786
+ return failure("revision_conflict", "source task export changed; reconcile the mapped destination task before importing", { taskId: existing.id });
21787
+ return success(existing.revision, current.data.revision, existing);
21788
+ }
21789
+ }
21727
21790
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
21728
21791
  return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
21729
21792
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
@@ -22112,6 +22175,73 @@ class TaskStore {
22112
22175
  })
22113
22176
  };
22114
22177
  }
22178
+ externalActionLockOptions() {
22179
+ const options = this.metadataLockOptions();
22180
+ return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
22181
+ }
22182
+ async withExternalActionLock(operation, reentrant = false) {
22183
+ const activeRoots = externalActionLockRoots.getStore();
22184
+ if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
22185
+ return operation();
22186
+ try {
22187
+ this.initializeMutationStorage();
22188
+ } catch (error) {
22189
+ return failure("storage_error", `unable to initialize store: ${String(error)}`, {
22190
+ path: this.workitDir
22191
+ });
22192
+ }
22193
+ let handle;
22194
+ try {
22195
+ handle = acquireFileLockSync(this.workspacePath, this.externalActionLockOptions());
22196
+ } catch (error) {
22197
+ return this.lockFailure(error);
22198
+ }
22199
+ let result;
22200
+ try {
22201
+ if (!handle.verifyStillHeld())
22202
+ result = failure("recovery_required", "metadata lock was compromised", {
22203
+ path: this.lockPath
22204
+ });
22205
+ else {
22206
+ try {
22207
+ const run = () => operation();
22208
+ result = await (reentrant ? externalActionLockRoots.run(new Set([...activeRoots ?? [], this.root]), run) : run());
22209
+ } catch {
22210
+ result = failure("external_outcome_unknown", "external action outcome is unknown", {
22211
+ outcome: "unknown"
22212
+ });
22213
+ }
22214
+ try {
22215
+ if (!handle.verifyStillHeld())
22216
+ result = failure("external_outcome_unknown", "metadata lock was compromised", {
22217
+ outcome: "unknown",
22218
+ path: this.lockPath
22219
+ });
22220
+ } catch {
22221
+ result = failure("external_outcome_unknown", "metadata lock could not be verified", {
22222
+ outcome: "unknown",
22223
+ path: this.lockPath
22224
+ });
22225
+ }
22226
+ }
22227
+ } catch {
22228
+ result = failure("external_outcome_unknown", "external action lock operation failed", {
22229
+ outcome: "unknown",
22230
+ path: this.lockPath
22231
+ });
22232
+ }
22233
+ try {
22234
+ handle.release();
22235
+ } catch (error) {
22236
+ const released = this.lockFailure(error);
22237
+ const releaseError = released.ok ? "metadata lock release failed" : released.error;
22238
+ return result.ok ? failure("external_outcome_unknown", "external action metadata lock release is uncertain", {
22239
+ outcome: "unknown",
22240
+ path: this.lockPath
22241
+ }) : failure("recovery_required", `${result.error}; ${releaseError}`, result.details);
22242
+ }
22243
+ return result;
22244
+ }
22115
22245
  recoveryLockOptions(observed, evidence, gate) {
22116
22246
  const options = this.metadataLockOptions();
22117
22247
  options.staleRecovery = "remove-if-unchanged";
@@ -22181,6 +22311,14 @@ class TaskStore {
22181
22311
  lockFailure(error) {
22182
22312
  const value = error;
22183
22313
  const code = typeof value?.code === "string" ? value.code : "";
22314
+ if (code === "EEXIST" || code === "file_lock_timeout") {
22315
+ const lock = this.readLockSnapshot();
22316
+ if (lock.ok && lock.data?.data.externalAction)
22317
+ return failure("writer_conflict", "workspace is reserved by a managed external action", {
22318
+ outcome: "not_started",
22319
+ path: this.lockPath
22320
+ });
22321
+ }
22184
22322
  const recovery = code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
22185
22323
  return failure(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
22186
22324
  }
@@ -22448,21 +22586,23 @@ function assertProductWriteAllowed(input) {
22448
22586
  }
22449
22587
 
22450
22588
  // packages/workit-core/src/core/task-evaluation.ts
22451
- import { createHash as createHash5 } from "node:crypto";
22589
+ import { createHash as createHash6 } from "node:crypto";
22452
22590
  import * as fs7 from "node:fs";
22453
22591
  import { spawnSync as spawnSync2 } from "node:child_process";
22454
- import path9 from "node:path";
22592
+ import path10 from "node:path";
22455
22593
 
22456
22594
  // packages/workit-core/src/core/authority.ts
22457
- import { createHash as createHash4 } from "node:crypto";
22595
+ import { createHash as createHash5 } from "node:crypto";
22458
22596
  import * as fs6 from "node:fs";
22459
- import path8 from "node:path";
22597
+ import path9 from "node:path";
22460
22598
 
22461
22599
  // packages/workit-core/src/core/auto-approval.ts
22462
- import { createHash as createHash3 } from "node:crypto";
22463
- import { readFileSync as readFileSync4 } from "node:fs";
22600
+ import { createHash as createHash4 } from "node:crypto";
22601
+ import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
22602
+ import path8 from "node:path";
22464
22603
 
22465
22604
  // packages/workit-core/src/core/workspaces.ts
22605
+ import { createHash as createHash3 } from "node:crypto";
22466
22606
  import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
22467
22607
  import path6 from "node:path";
22468
22608
 
@@ -22480,7 +22620,7 @@ import os from "node:os";
22480
22620
  import path5 from "node:path";
22481
22621
 
22482
22622
  // packages/workit-core/src/core/boundary.ts
22483
- import { statSync } from "node:fs";
22623
+ import { statSync as statSync2 } from "node:fs";
22484
22624
  var EVENT = {
22485
22625
  initialization: "initialization",
22486
22626
  provenance: "provenance",
@@ -22508,7 +22648,7 @@ var changedSourcesSinceLoad = (marker) => {
22508
22648
  const changed = [];
22509
22649
  for (const file of marker.files) {
22510
22650
  try {
22511
- if (statSync(file).mtimeMs > marker.loadedAt)
22651
+ if (statSync2(file).mtimeMs > marker.loadedAt)
22512
22652
  changed.push(file);
22513
22653
  } catch {}
22514
22654
  }
@@ -22667,33 +22807,197 @@ var COMMIT_PRESETS = [
22667
22807
  "custom",
22668
22808
  "auto"
22669
22809
  ];
22810
+ var resolveCommitPolicy = (config, workspace) => {
22811
+ const wp = workspace?.commitPolicy ?? {};
22812
+ if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
22813
+ return {
22814
+ preset: wp.preset,
22815
+ ...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
22816
+ };
22817
+ }
22818
+ return config.commitPolicy ?? { preset: "conventional" };
22819
+ };
22670
22820
 
22671
22821
  // packages/workit-core/src/core/workspaces.ts
22822
+ var nonBlank = string5().refine((value) => value.trim().length > 0, "must not be blank");
22823
+ var branchPreset = custom2((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
22824
+ var commitPreset = custom2((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
22825
+ var workspaceBranchPolicySchema = object4({
22826
+ preset: branchPreset,
22827
+ developBranch: nonBlank.optional(),
22828
+ prefixes: object4({
22829
+ feature: nonBlank,
22830
+ bugfix: nonBlank,
22831
+ release: nonBlank,
22832
+ hotfix: nonBlank
22833
+ }).passthrough().optional(),
22834
+ allowed: array3(nonBlank).optional(),
22835
+ protected: array3(nonBlank).optional(),
22836
+ integration: _enum2(["pr", "merge"]).optional()
22837
+ }).strict();
22838
+ var workspaceCommitPolicySchema = object4({ preset: commitPreset, pattern: string5().optional() }).strict();
22839
+ var workspaceProfileSchema = object4({
22840
+ branchPolicy: workspaceBranchPolicySchema.optional(),
22841
+ commitPolicy: workspaceCommitPolicySchema.optional()
22842
+ }).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
22843
+ message: "a profile must define branchPolicy or commitPolicy"
22844
+ });
22845
+ var versionSourceSchema = discriminatedUnion2("kind", [
22846
+ object4({
22847
+ kind: literal3("package-json"),
22848
+ path: nonBlank,
22849
+ field: nonBlank
22850
+ }).strict(),
22851
+ object4({ kind: literal3("git-tag") }).strict(),
22852
+ object4({ kind: literal3("manual") }).strict()
22853
+ ]);
22854
+ var releaseTrackSchema = object4({
22855
+ strategy: branchPreset,
22856
+ productionBranch: nonBlank,
22857
+ integrationBranch: nonBlank,
22858
+ naming: object4({ feature: nonBlank, release: nonBlank, hotfix: nonBlank }).strict(),
22859
+ baseBranch: nonBlank,
22860
+ mergeBackBranches: array3(nonBlank),
22861
+ pullRequestTarget: nonBlank,
22862
+ tagNamespace: string5(),
22863
+ versionSource: versionSourceSchema,
22864
+ requiredChecks: array3(nonBlank)
22865
+ }).strict().superRefine((track, context) => {
22866
+ for (const [field, values] of [
22867
+ ["mergeBackBranches", track.mergeBackBranches],
22868
+ ["requiredChecks", track.requiredChecks]
22869
+ ]) {
22870
+ if (new Set(values).size !== values.length) {
22871
+ context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
22872
+ }
22873
+ }
22874
+ if (track.versionSource.kind === "package-json" && (path6.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
22875
+ context.addIssue({
22876
+ code: "custom",
22877
+ path: ["versionSource", "path"],
22878
+ message: "must stay inside the repository"
22879
+ });
22880
+ }
22881
+ });
22882
+ var workspaceConfigSchema = object4({
22883
+ name: nonBlank,
22884
+ glob: nonBlank,
22885
+ vcs: object4({
22886
+ provider: _enum2(["gitlab", "github"]),
22887
+ defaultTargetBranch: nonBlank.optional(),
22888
+ tokenFile: nonBlank.optional(),
22889
+ account: nonBlank.optional()
22890
+ }).passthrough().optional(),
22891
+ autoApprove: union3([boolean5(), array3(nonBlank)]).optional(),
22892
+ youtrack: object4({ baseUrl: nonBlank.optional(), link_issues: boolean5().optional() }).passthrough().optional(),
22893
+ issues: object4({ provider: literal3("github").optional(), link_on_pr: boolean5().optional() }).passthrough().optional(),
22894
+ branchPolicy: workspaceBranchPolicySchema.optional(),
22895
+ commitPolicy: workspaceCommitPolicySchema.optional(),
22896
+ defaultProfile: nonBlank.optional(),
22897
+ profiles: record3(nonBlank, workspaceProfileSchema).optional(),
22898
+ releaseTracks: record3(nonBlank, releaseTrackSchema).optional()
22899
+ }).passthrough().superRefine((workspace, context) => {
22900
+ if (workspace.youtrack && workspace.vcs?.provider !== "gitlab") {
22901
+ context.addIssue({
22902
+ code: "custom",
22903
+ path: ["youtrack"],
22904
+ message: `YouTrack issue linking requires the gitlab provider, got ${workspace.vcs?.provider ?? "unset"}`
22905
+ });
22906
+ }
22907
+ if (workspace.issues && workspace.vcs?.provider !== "github") {
22908
+ context.addIssue({
22909
+ code: "custom",
22910
+ path: ["issues"],
22911
+ message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
22912
+ });
22913
+ }
22914
+ if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
22915
+ context.addIssue({
22916
+ code: "custom",
22917
+ path: ["defaultProfile"],
22918
+ message: `does not name a configured profile`
22919
+ });
22920
+ }
22921
+ });
22922
+ var workspacesFileSchema = object4({ workspaces: array3(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
22923
+ const names = new Set;
22924
+ for (const [index, workspace] of (file.workspaces ?? []).entries()) {
22925
+ if (names.has(workspace.name)) {
22926
+ context.addIssue({
22927
+ code: "custom",
22928
+ path: ["workspaces", index, "name"],
22929
+ message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
22930
+ });
22931
+ }
22932
+ names.add(workspace.name);
22933
+ }
22934
+ });
22935
+ var workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex");
22936
+ var validateWorkspacesDocument = (parsed, file) => {
22937
+ if (!isConfigObject(parsed)) {
22938
+ return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
22939
+ }
22940
+ const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
22941
+ if (!validated.success) {
22942
+ const detail = validated.error.issues.map((issue) => {
22943
+ const field = String(issue.path[issue.path.length - 1] ?? "");
22944
+ const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
22945
+ return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
22946
+ }).join("; ");
22947
+ return {
22948
+ status: "invalid",
22949
+ path: file,
22950
+ entries: [],
22951
+ error: `${file} has invalid workspace configuration: ${detail}`
22952
+ };
22953
+ }
22954
+ for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
22955
+ const glob = validateWorkspaceGlob(workspace.glob);
22956
+ if (!glob.ok) {
22957
+ return {
22958
+ status: "invalid",
22959
+ path: file,
22960
+ entries: [],
22961
+ error: `${file} workspaces.${index}.glob: ${glob.error}`
22962
+ };
22963
+ }
22964
+ }
22965
+ return {
22966
+ status: "valid",
22967
+ path: file,
22968
+ entries: validated.data.workspaces ?? [],
22969
+ document: parsed
22970
+ };
22971
+ };
22672
22972
  var readWorkspacesResult = (dir = configDir()) => {
22673
22973
  const file = path6.join(dir, "workspaces.json");
22674
22974
  let raw;
22675
22975
  try {
22676
22976
  raw = readFileSync3(file, "utf8");
22677
- } catch {
22678
- return { status: "missing", path: file, entries: [] };
22977
+ } catch (error) {
22978
+ if (error.code === "ENOENT")
22979
+ return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
22980
+ return {
22981
+ status: "malformed",
22982
+ path: file,
22983
+ entries: [],
22984
+ error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
22985
+ };
22679
22986
  }
22680
22987
  let parsed;
22681
22988
  try {
22682
22989
  parsed = JSON.parse(raw);
22683
22990
  } catch {
22684
- return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
22685
- }
22686
- if (!isConfigObject(parsed)) {
22687
- return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
22991
+ return {
22992
+ status: "malformed",
22993
+ path: file,
22994
+ entries: [],
22995
+ revision: workspacesRevision(raw),
22996
+ error: `${file} is not valid JSON`
22997
+ };
22688
22998
  }
22689
- const list = parsed.workspaces;
22690
- return {
22691
- status: "valid",
22692
- path: file,
22693
- entries: Array.isArray(list) ? list : []
22694
- };
22999
+ return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
22695
23000
  };
22696
- var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
22697
23001
  var globToRegExp = (glob) => {
22698
23002
  let out = "";
22699
23003
  for (let i = 0;i < glob.length; i++) {
@@ -22728,6 +23032,30 @@ var globToRegExp = (glob) => {
22728
23032
  return new RegExp(`^${out}$`);
22729
23033
  };
22730
23034
  var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
23035
+ var UNSUPPORTED_GLOB = /[?[\]{}]/;
23036
+ var UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
23037
+ var validateWorkspaceGlob = (glob) => {
23038
+ const trimmed = glob.trim();
23039
+ if (!trimmed)
23040
+ return { ok: false, error: "workspace pattern is required" };
23041
+ const m = UNSUPPORTED_GLOB.exec(trimmed);
23042
+ if (m) {
23043
+ return {
23044
+ ok: false,
23045
+ error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
23046
+ };
23047
+ }
23048
+ const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
23049
+ if (ext) {
23050
+ const token = ext[0];
23051
+ const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
23052
+ return {
23053
+ ok: false,
23054
+ error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
23055
+ };
23056
+ }
23057
+ return { ok: true };
23058
+ };
22731
23059
  var realpathOf = (p) => {
22732
23060
  const real = realpathSync2.native ?? realpathSync2;
22733
23061
  try {
@@ -22745,29 +23073,137 @@ var canonicalGlob = (glob) => {
22745
23073
  const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
22746
23074
  return real + (prefix.endsWith("/") ? "/" : "") + rest;
22747
23075
  };
22748
- var resolveWorkspaceFrom = (cwd, dir) => {
23076
+ var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
23077
+ const result = readWorkspacesResult(dir);
23078
+ if (result.status === "malformed" || result.status === "invalid")
23079
+ throw new Error(result.error);
22749
23080
  const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
22750
- for (const entry of loadWorkspacesFrom(dir)) {
22751
- if (!entry || typeof entry !== "object")
22752
- continue;
22753
- const ws = entry;
22754
- if (typeof ws.glob !== "string" || !ws.glob)
22755
- continue;
23081
+ const matches = [];
23082
+ for (const ws of result.entries) {
22756
23083
  const glob = ws.glob.replaceAll("\\", "/");
22757
23084
  const canonical = canonicalGlob(glob);
23085
+ let matched = false;
22758
23086
  for (const target of targets) {
22759
- if (matchWorkspace(glob, target))
22760
- return ws;
22761
- if (canonical !== glob && matchWorkspace(canonical, target))
22762
- return ws;
23087
+ if (matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target)) {
23088
+ matched = true;
23089
+ break;
23090
+ }
22763
23091
  }
23092
+ if (matched)
23093
+ matches.push(ws);
22764
23094
  }
22765
- return null;
23095
+ if (workspaceName !== undefined) {
23096
+ const selected = matches.find((ws) => ws.name === workspaceName);
23097
+ if (!selected && matches.length > 0) {
23098
+ const matchingNames = matches.map((ws) => ws.name);
23099
+ throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matchingNames.join(", ")}`);
23100
+ }
23101
+ if (selected)
23102
+ return selected;
23103
+ throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
23104
+ }
23105
+ if (matches.length > 1) {
23106
+ throw new Error(`ambiguous workspace for ${cwd}; choose one of: ${matches.map((ws) => ws.name).join(", ")}`);
23107
+ }
23108
+ return matches[0] ?? null;
23109
+ };
23110
+ var runtimeWorkspacePolicySchema = (kind) => {
23111
+ const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
23112
+ const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object4({
23113
+ provider: _enum2(["gitlab", "github"]),
23114
+ defaultTargetBranch: nonBlank.optional(),
23115
+ account: nonBlank.optional()
23116
+ }).passthrough();
23117
+ const profileSchema = object4({ [key]: policySchema.optional() }).passthrough();
23118
+ const entrySchema = object4({
23119
+ name: nonBlank,
23120
+ glob: nonBlank,
23121
+ [key]: policySchema.optional(),
23122
+ defaultProfile: nonBlank.optional(),
23123
+ profiles: record3(nonBlank, profileSchema).optional()
23124
+ }).passthrough();
23125
+ return entrySchema;
23126
+ };
23127
+ var resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs");
23128
+ var runtimeWorkspaceIndexSchema = object4({
23129
+ workspaces: array3(object4({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
23130
+ }).passthrough();
23131
+ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
23132
+ const file = path6.join(configDir(), "workspaces.json");
23133
+ let raw;
23134
+ try {
23135
+ raw = readFileSync3(file, "utf8");
23136
+ } catch (error) {
23137
+ if (error.code === "ENOENT")
23138
+ return null;
23139
+ throw new Error(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`);
23140
+ }
23141
+ let parsed;
23142
+ try {
23143
+ parsed = JSON.parse(raw);
23144
+ } catch {
23145
+ throw new Error(`${file} is not valid JSON`);
23146
+ }
23147
+ const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
23148
+ if (!validated.success) {
23149
+ const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
23150
+ throw new Error(`${file} has invalid workspace matching data: ${detail}`);
23151
+ }
23152
+ const entries = validated.data.workspaces;
23153
+ for (const entry of entries ?? []) {
23154
+ const glob = validateWorkspaceGlob(entry.glob);
23155
+ if (!glob.ok)
23156
+ throw new Error(`${file} workspace ${entry.name}: ${glob.error}`);
23157
+ }
23158
+ const targets = [cwd, realpathOf(cwd)].map((value) => value.replaceAll("\\", "/"));
23159
+ const matches = (entries ?? []).filter((entry) => {
23160
+ const glob = entry.glob.replaceAll("\\", "/");
23161
+ const canonical = canonicalGlob(glob);
23162
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
23163
+ });
23164
+ const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
23165
+ let selected;
23166
+ if (workspaceName) {
23167
+ const named = matches.filter((entry) => entry.name === workspaceName);
23168
+ if (named.length === 1)
23169
+ selected = named[0];
23170
+ else if (named.length > 1)
23171
+ throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
23172
+ else if (matches.length)
23173
+ throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
23174
+ else
23175
+ throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
23176
+ } else if (matches.length > 1)
23177
+ throw new Error(`ambiguous workspace for ${cwd}; choose one of: ${matches.map((entry) => entry.name).join(", ")}`);
23178
+ else
23179
+ selected = matches[0];
23180
+ if (!selected)
23181
+ return null;
23182
+ const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
23183
+ if (!policy.success) {
23184
+ const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
23185
+ throw new Error(`${file} has invalid ${kind} policy configuration: ${detail}`);
23186
+ }
23187
+ return policy.data;
23188
+ };
23189
+ var resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
23190
+ const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
23191
+ if (!candidate)
23192
+ return { policy: undefined, source: "user-default" };
23193
+ const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
23194
+ const profileName = requestedProfile?.trim() || candidate.defaultProfile;
23195
+ if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
23196
+ throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
23197
+ const profile = profileName ? candidate.profiles?.[profileName] : undefined;
23198
+ if (profileName && !profile)
23199
+ throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
23200
+ const profilePolicy = profile?.[key];
23201
+ const workspacePolicy = candidate[key];
23202
+ return {
23203
+ policy: profilePolicy ?? workspacePolicy,
23204
+ source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
23205
+ };
22766
23206
  };
22767
- var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
22768
-
22769
- // packages/workit-core/src/core/branch.ts
22770
- import { execFileSync as execFileSync2 } from "node:child_process";
22771
23207
 
22772
23208
  // packages/workit-core/src/core/git.ts
22773
23209
  import { execFileSync } from "node:child_process";
@@ -22841,7 +23277,6 @@ var gitContext = (workspaceRoot, paths = []) => {
22841
23277
  import fs5 from "node:fs";
22842
23278
  import path7 from "node:path";
22843
23279
  import { spawnSync } from "node:child_process";
22844
- var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
22845
23280
  var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
22846
23281
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
22847
23282
  var remoteProvider = (cwd) => {
@@ -22855,6 +23290,79 @@ var remoteProvider = (cwd) => {
22855
23290
  return "gitlab";
22856
23291
  return null;
22857
23292
  };
23293
+ var remoteEndpoint = (remote) => {
23294
+ const raw = remote.trim();
23295
+ if (!raw)
23296
+ return null;
23297
+ const scp = /^(?:[^@\s]+@)?([^:]+):(.+)$/u.exec(raw);
23298
+ if (scp && !raw.includes("://"))
23299
+ return {
23300
+ host: scp[1].toLowerCase(),
23301
+ path: scp[2].replace(/^\//u, "").replace(/\.git$/u, ""),
23302
+ protocol: "ssh:",
23303
+ port: ""
23304
+ };
23305
+ try {
23306
+ const url = new URL(raw);
23307
+ return {
23308
+ host: url.hostname.toLowerCase(),
23309
+ path: url.pathname.replace(/^\//u, "").replace(/\.git$/u, "").replace(/\/$/u, ""),
23310
+ protocol: url.protocol,
23311
+ port: url.port
23312
+ };
23313
+ } catch {
23314
+ return null;
23315
+ }
23316
+ };
23317
+ var configuredSshEndpoint = (host) => {
23318
+ if (host.startsWith("-"))
23319
+ return null;
23320
+ const resolved = spawnSync("ssh", ["-G", host], {
23321
+ encoding: "utf8",
23322
+ timeout: 5000,
23323
+ maxBuffer: 1024 * 1024,
23324
+ env: { ...process.env, PATH: process.env.PATH ?? "" }
23325
+ });
23326
+ if (resolved.status !== 0)
23327
+ return null;
23328
+ const output = resolved.stdout ?? "";
23329
+ const hostname = /^hostname\s+(\S+)/imu.exec(output)?.[1]?.toLowerCase();
23330
+ if (!hostname)
23331
+ return null;
23332
+ return { host: hostname, port: /^port\s+(\d+)/imu.exec(output)?.[1] ?? "22" };
23333
+ };
23334
+ var sshApiHostAliases = {
23335
+ "github.com": [{ host: "ssh.github.com", port: "443" }],
23336
+ "gitlab.com": [{ host: "altssh.gitlab.com", port: "443" }]
23337
+ };
23338
+ var hostingApiHostMatches = (remote, apiHost, resolveSshEndpoint = configuredSshEndpoint) => {
23339
+ const endpoint = remoteEndpoint(remote);
23340
+ if (!endpoint)
23341
+ return false;
23342
+ let expected;
23343
+ try {
23344
+ const url = new URL(`https://${apiHost}`);
23345
+ if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
23346
+ return false;
23347
+ expected = {
23348
+ host: url.hostname.toLowerCase().replace(/\.$/u, ""),
23349
+ port: url.port
23350
+ };
23351
+ } catch {
23352
+ return false;
23353
+ }
23354
+ if (endpoint.protocol !== "ssh:") {
23355
+ return endpoint.host.replace(/\.$/u, "") === expected.host && endpoint.port === expected.port;
23356
+ }
23357
+ const resolved = resolveSshEndpoint(endpoint.host);
23358
+ if (!resolved)
23359
+ return false;
23360
+ const hostname = resolved.host.toLowerCase().replace(/\.$/u, "");
23361
+ const port = endpoint.port || resolved.port;
23362
+ if (hostname === expected.host)
23363
+ return port === (expected.port || "22");
23364
+ return Boolean(sshApiHostAliases[expected.host]?.some((alias) => alias.host === hostname && alias.port === port && (!expected.port || expected.port === port)));
23365
+ };
22858
23366
  var readVcsConfig = () => {
22859
23367
  const file = vcsConfigPath();
22860
23368
  let raw;
@@ -22875,7 +23383,7 @@ var readVcsConfig = () => {
22875
23383
  return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
22876
23384
  };
22877
23385
  function vcsConfig(mode, cwd) {
22878
- const ws = resolveWorkspace(vcsCwd(cwd));
23386
+ const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
22879
23387
  const wsVcs = ws?.vcs ?? {};
22880
23388
  const wsYt = ws?.youtrack ?? {};
22881
23389
  const wsIssues = ws?.issues ?? {};
@@ -22888,8 +23396,8 @@ function vcsConfig(mode, cwd) {
22888
23396
  const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
22889
23397
  const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
22890
23398
  const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
22891
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
22892
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
23399
+ const linkIssues = provider === "gitlab" && typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
23400
+ const youtrackBaseUrl = provider === "gitlab" && typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
22893
23401
  let issuesProvider = null;
22894
23402
  let linkOnPr = null;
22895
23403
  if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
@@ -22913,9 +23421,8 @@ function vcsConfig(mode, cwd) {
22913
23421
  if (cfgStatus === "malformed") {
22914
23422
  return { ok: false, error: cfgError, configPath: cfgPath };
22915
23423
  }
22916
- if (cfgStatus === "missing") {
23424
+ if (cfgStatus === "missing" && provider === null)
22917
23425
  return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
22918
- }
22919
23426
  if (provider === null) {
22920
23427
  return {
22921
23428
  ok: false,
@@ -22924,24 +23431,12 @@ function vcsConfig(mode, cwd) {
22924
23431
  };
22925
23432
  }
22926
23433
  const prov = cfg[provider] ?? {};
22927
- const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
22928
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path7.join(configDir(), `${provider}.token`));
22929
- const tokenPath = path7.resolve(tokenFile);
22930
- let tokenOk = false;
22931
- if (fs5.existsSync(tokenPath)) {
22932
- const token = fs5.readFileSync(tokenPath, "utf8").trim();
22933
- const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
22934
- tokenOk = !placeholder;
22935
- }
22936
23434
  const out = {
22937
23435
  ok: true,
22938
23436
  configPath: path7.resolve(cfgPath),
22939
23437
  provider,
22940
23438
  defaultTargetBranch: defaultTarget,
22941
23439
  pr: cfg.pr ?? {},
22942
- tokenPath,
22943
- tokenPresent: fs5.existsSync(tokenPath),
22944
- tokenReady: tokenOk,
22945
23440
  workspace_name: ws?.name ?? null,
22946
23441
  link_issues: linkIssues,
22947
23442
  youtrack_base_url: youtrackBaseUrl,
@@ -22956,16 +23451,58 @@ function vcsConfig(mode, cwd) {
22956
23451
  } else if (provider === "github") {
22957
23452
  out.github = { host: prov.host ?? "github.com" };
22958
23453
  }
22959
- if (mode === "summary")
22960
- delete out.tokenPath;
22961
23454
  return out;
22962
23455
  }
23456
+ function vcsCliIdentity(cwd) {
23457
+ const root = vcsCwd(cwd);
23458
+ const cfg = vcsConfig("load", root);
23459
+ if (!cfg.ok)
23460
+ return { ok: false, error: cfg.error ?? "vcs config not ready" };
23461
+ const provider = cfg.provider;
23462
+ if (provider !== "gitlab" && provider !== "github")
23463
+ return { ok: false, error: `unsupported provider: ${provider}` };
23464
+ const bin = provider === "gitlab" ? "glab" : "gh";
23465
+ const host = String(provider === "github" ? cfg.github?.host ?? "github.com" : cfg.gitlab?.host ?? "gitlab.com").toLowerCase();
23466
+ const env = {
23467
+ ...process.env,
23468
+ PATH: process.env.PATH ?? "",
23469
+ ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
23470
+ };
23471
+ const result = spawnSync(bin, ["api", "user"], {
23472
+ cwd: root,
23473
+ encoding: "utf8",
23474
+ env
23475
+ });
23476
+ if (result.status !== 0)
23477
+ return { ok: false, provider, error: `${bin} is missing or not authenticated` };
23478
+ try {
23479
+ const user = JSON.parse(result.stdout ?? "");
23480
+ const username = provider === "gitlab" ? user.username : user.login;
23481
+ if (typeof username !== "string" || !username)
23482
+ return { ok: false, provider, error: `invalid ${bin} identity response` };
23483
+ const expected = resolveRuntimeWorkspaceVcs(root)?.vcs?.account;
23484
+ if (expected && username.toLowerCase() !== expected.toLowerCase())
23485
+ return {
23486
+ ok: false,
23487
+ provider,
23488
+ error: `${bin} account ${username} does not match ${expected}`
23489
+ };
23490
+ return { ok: true, provider, host, username, name: user.name };
23491
+ } catch {
23492
+ return { ok: false, provider, error: `invalid JSON from ${bin} api user` };
23493
+ }
23494
+ }
22963
23495
  function mergedPrStyle(limit = 6, cwd) {
22964
23496
  const cfg = vcsConfig("load", cwd);
22965
- if (!cfg.ok || !cfg.tokenReady)
23497
+ if (!cfg.ok)
22966
23498
  return { ok: false, error: "vcs not configured" };
22967
23499
  const provider = cfg.provider;
22968
- const token = fs5.readFileSync(cfg.tokenPath, "utf8").trim();
23500
+ const host = String(provider === "github" ? cfg.github?.host ?? "github.com" : cfg.gitlab?.host ?? "gitlab.com").toLowerCase();
23501
+ const env = {
23502
+ ...process.env,
23503
+ PATH: process.env.PATH ?? "",
23504
+ ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
23505
+ };
22969
23506
  const examples = [];
22970
23507
  const descInfo = (desc, caseInsensitiveNotes = false) => ({
22971
23508
  hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
@@ -22973,23 +23510,30 @@ function mergedPrStyle(limit = 6, cwd) {
22973
23510
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
22974
23511
  descriptionPreview: desc.slice(0, 600)
22975
23512
  });
23513
+ const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
23514
+ cwd,
23515
+ encoding: "utf8"
23516
+ });
23517
+ const remotes = (remoteResult.stdout ?? "").split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
23518
+ if (remotes.length === 0)
23519
+ return { ok: false, error: "no origin remote" };
23520
+ if (remotes.length !== 1)
23521
+ return { ok: false, error: "origin has ambiguous push destinations" };
23522
+ const remote = remotes[0];
23523
+ const endpoint = remoteEndpoint(remote);
23524
+ if (!endpoint?.path)
23525
+ return { ok: false, error: "hosting repository could not be resolved" };
23526
+ if (!hostingApiHostMatches(remote, host))
23527
+ return { ok: false, error: "PR style target does not match the provider host" };
22976
23528
  if (provider === "gitlab") {
22977
- const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
22978
- if (remote.status !== 0)
22979
- return { ok: false, error: "no origin remote" };
22980
- const url = (remote.stdout ?? "").trim();
22981
- const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
22982
- if (!m)
22983
- return { ok: false, error: "not a gitlab.com origin" };
22984
- const project = m[1];
22985
- const env = { ...process.env, GITLAB_TOKEN: token };
22986
- const run = (args) => spawnSync("glab", ["api", ...args], { cwd, encoding: "utf8", env });
23529
+ const run = (args) => spawnSync("glab", ["api", ...args], {
23530
+ cwd,
23531
+ encoding: "utf8",
23532
+ env
23533
+ });
22987
23534
  let r = run([
22988
- `projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
23535
+ `projects/${endpoint.path.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
22989
23536
  ]);
22990
- if (r.status !== 0) {
22991
- r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
22992
- }
22993
23537
  if (r.status !== 0)
22994
23538
  return { ok: false, error: "could not list merge requests" };
22995
23539
  for (const mr of JSON.parse(r.stdout ?? "[]")) {
@@ -23002,7 +23546,18 @@ function mergedPrStyle(limit = 6, cwd) {
23002
23546
  });
23003
23547
  }
23004
23548
  } else if (provider === "github") {
23005
- const r = spawnSync("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
23549
+ const r = spawnSync("gh", [
23550
+ "pr",
23551
+ "list",
23552
+ "--repo",
23553
+ `${host}/${endpoint.path}`,
23554
+ "--state",
23555
+ "merged",
23556
+ "--limit",
23557
+ String(limit),
23558
+ "--json",
23559
+ "title,url,body"
23560
+ ], { cwd, encoding: "utf8", env });
23006
23561
  if (r.status !== 0)
23007
23562
  return { ok: false, error: "could not list pull requests" };
23008
23563
  for (const pr of JSON.parse(r.stdout ?? "[]")) {
@@ -23024,26 +23579,28 @@ function mergedPrStyle(limit = 6, cwd) {
23024
23579
  }
23025
23580
 
23026
23581
  // packages/workit-core/src/core/branch.ts
23027
- var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
23582
+ function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
23583
+ const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
23584
+ const config = readConfig();
23585
+ return kind === "branch" ? {
23586
+ branchPolicy: resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null),
23587
+ provenance: { branchPolicy: selected.source }
23588
+ } : {
23589
+ commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
23590
+ provenance: { commitPolicy: selected.source }
23591
+ };
23592
+ }
23593
+ var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
23028
23594
  var verifyPushIdentity = (root, provider, account) => {
23029
23595
  if (!account || typeof account !== "string")
23030
23596
  return { ok: false, error: "push identity requires a configured area account" };
23031
- const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
23032
- const bin = provider === "gitlab" ? "glab" : "gh";
23033
- let login;
23034
- try {
23035
- login = execFileSync2(bin, [...probe], {
23036
- cwd: root,
23037
- encoding: "utf8",
23038
- stdio: ["pipe", "pipe", "pipe"]
23039
- }).trim();
23040
- } catch {
23041
- return { ok: false, error: `${bin} identity could not be resolved` };
23042
- }
23043
- if (!login || login.toLowerCase() !== account.toLowerCase())
23597
+ const identity = vcsCliIdentity(root);
23598
+ if (!identity.ok || identity.provider !== provider)
23599
+ return { ok: false, error: identity.error ?? "hosting CLI identity could not be resolved" };
23600
+ if (identity.username?.toLowerCase() !== account.toLowerCase())
23044
23601
  return {
23045
23602
  ok: false,
23046
- error: `push identity ${login || "(unknown)"} does not match area account ${account}`
23603
+ error: `push identity ${identity.username || "(unknown)"} does not match area account ${account}`
23047
23604
  };
23048
23605
  return { ok: true };
23049
23606
  };
@@ -23061,7 +23618,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
23061
23618
  var digestOf = (configDirPath) => {
23062
23619
  try {
23063
23620
  const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
23064
- return createHash3("sha256").update(raw).digest("hex");
23621
+ return createHash4("sha256").update(raw).digest("hex");
23065
23622
  } catch {
23066
23623
  return null;
23067
23624
  }
@@ -23099,14 +23656,30 @@ var resolveAutoApproval = (root, dir = configDir()) => {
23099
23656
  };
23100
23657
  };
23101
23658
  var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
23102
- var standingApprovalLive = (root, receipt, cls, dir) => {
23659
+ var targetOf = (root, descriptor) => {
23660
+ try {
23661
+ const parsed = typeof descriptor === "string" ? JSON.parse(descriptor) : descriptor;
23662
+ const cwd = parsed?.payload?.cwd;
23663
+ if (cwd === undefined)
23664
+ return root;
23665
+ if (typeof cwd !== "string" || !path8.isAbsolute(cwd))
23666
+ return null;
23667
+ return realpathSync3(cwd);
23668
+ } catch {
23669
+ return null;
23670
+ }
23671
+ };
23672
+ var standingApprovalLive = (root, receipt, cls, dir, descriptor) => {
23103
23673
  if (!isStandingReceipt(receipt))
23104
23674
  return false;
23105
23675
  if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
23106
23676
  return false;
23107
23677
  if (receipt.class !== cls)
23108
23678
  return false;
23109
- const resolved = resolveAutoApproval(root, dir);
23679
+ const target = targetOf(root, descriptor);
23680
+ if (!target)
23681
+ return false;
23682
+ const resolved = resolveAutoApproval(target, dir);
23110
23683
  if (resolved.status !== "on")
23111
23684
  return false;
23112
23685
  if (resolved.workspace !== receipt.workspace)
@@ -23142,16 +23715,19 @@ var verifyStandingApproval = (root, task, caller, binding) => {
23142
23715
  const cls = operationAutoClass(operation);
23143
23716
  if (!cls || cls !== standing.class)
23144
23717
  return failure("permission_denied", "standing approval does not cover this operation");
23145
- const resolved = resolveAutoApproval(root);
23718
+ const target = targetOf(root, binding.approvedContent);
23719
+ if (!target)
23720
+ return failure("permission_denied", "standing action target is invalid");
23721
+ const resolved = resolveAutoApproval(target);
23146
23722
  if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
23147
23723
  return failure("permission_denied", "standing auto-approval is not live for this operation");
23148
23724
  if (cls === "push") {
23149
- const workspace = resolveWorkspaceFrom(root, configDir());
23725
+ const workspace = resolveWorkspaceFrom(target, configDir());
23150
23726
  const provider = workspace?.vcs?.provider;
23151
23727
  const account = resolved.account;
23152
23728
  if (!provider || !account)
23153
23729
  return failure("permission_denied", "auto-push requires a workspace provider and area account");
23154
- const identity = verifyPushIdentity(root, provider, account);
23730
+ const identity = verifyPushIdentity(target, provider, account);
23155
23731
  if (!identity.ok)
23156
23732
  return failure("permission_denied", identity.error);
23157
23733
  }
@@ -23160,7 +23736,7 @@ var verifyStandingApproval = (root, task, caller, binding) => {
23160
23736
  host: caller.host,
23161
23737
  session: { kind: "host", host: caller.host, handle: caller.actor },
23162
23738
  workerId: null,
23163
- receipts: [standingReceiptFor(root, standing.workspace, cls)]
23739
+ receipts: [standingReceiptFor(target, standing.workspace, cls)]
23164
23740
  });
23165
23741
  };
23166
23742
 
@@ -23311,7 +23887,7 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
23311
23887
  return [];
23312
23888
  return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
23313
23889
  }
23314
- var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
23890
+ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
23315
23891
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23316
23892
  let root;
23317
23893
  try {
@@ -23324,23 +23900,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23324
23900
  continue;
23325
23901
  if (!reference.digest)
23326
23902
  return failure("invalid_input", "document references require a byte digest");
23327
- const target = path8.resolve(root, reference.path);
23328
- if (target !== root && !target.startsWith(`${root}${path8.sep}`))
23903
+ const target = path9.resolve(root, reference.path);
23904
+ if (target !== root && !target.startsWith(`${root}${path9.sep}`))
23329
23905
  return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23330
23906
  fields: [
23331
23907
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
23332
23908
  ]
23333
23909
  });
23334
23910
  try {
23335
- const relative = path8.relative(root, target);
23911
+ const relative = path9.relative(root, target);
23336
23912
  let current = root;
23337
- for (const segment of relative.split(path8.sep)) {
23913
+ for (const segment of relative.split(path9.sep)) {
23338
23914
  if (!segment)
23339
23915
  continue;
23340
- current = path8.join(current, segment);
23916
+ current = path9.join(current, segment);
23341
23917
  const stat = fs6.lstatSync(current);
23342
23918
  const real = fs6.realpathSync(current);
23343
- if (real !== root && !real.startsWith(`${root}${path8.sep}`))
23919
+ if (real !== root && !real.startsWith(`${root}${path9.sep}`))
23344
23920
  return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23345
23921
  fields: [
23346
23922
  {
@@ -23418,7 +23994,7 @@ var validateAction = (store, task, workspaceId, input, authority) => {
23418
23994
  operation = null;
23419
23995
  }
23420
23996
  const cls = operationAutoClass(operation);
23421
- if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
23997
+ if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls, undefined, decision.binding.approvedContent)))
23422
23998
  return failure("permission_denied", "standing auto-approval is not live for this operation");
23423
23999
  }
23424
24000
  if (decision.revoked)
@@ -23720,15 +24296,15 @@ var bindingCovers = scopeCovers;
23720
24296
  var verifyDecisionContent = verifyContentRefs;
23721
24297
 
23722
24298
  // packages/workit-core/src/core/task-evaluation.ts
23723
- var digestBytes3 = (value) => createHash5("sha256").update(value).digest("hex");
23724
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path9.sep}`);
23725
- var relative = (root, candidate) => path9.relative(root, candidate).split(path9.sep).join("/") || ".";
24299
+ var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
24300
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
24301
+ var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
23726
24302
  var canonicalPath = (value) => {
23727
24303
  if (typeof value !== "string" || !value)
23728
24304
  return null;
23729
24305
  if (value.includes("\\") || value.split("/").includes(".."))
23730
24306
  return null;
23731
- const normalized = path9.posix.normalize(value);
24307
+ const normalized = path10.posix.normalize(value);
23732
24308
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
23733
24309
  return null;
23734
24310
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -23776,14 +24352,14 @@ var gitPaths = (root) => {
23776
24352
  git: /not a git repository/i.test(stderr) ? false : true
23777
24353
  };
23778
24354
  }
23779
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")) : [];
24355
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
23780
24356
  const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
23781
24357
  if (stagedDeleted.status !== 0) {
23782
24358
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
23783
24359
  if (!/not a git repository/i.test(stderr))
23784
24360
  return { paths, uncertain: true, git: true };
23785
24361
  } else if (stagedDeleted.stdout) {
23786
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")));
24362
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")));
23787
24363
  }
23788
24364
  return { paths: [...new Set(paths)], uncertain: false, git: true };
23789
24365
  };
@@ -23798,7 +24374,7 @@ var walk = (root, directory, output) => {
23798
24374
  for (const name of names) {
23799
24375
  if (name === ".git" || name === ".workit")
23800
24376
  continue;
23801
- const target = path9.join(directory, name);
24377
+ const target = path10.join(directory, name);
23802
24378
  let stat;
23803
24379
  try {
23804
24380
  stat = fs7.lstatSync(target);
@@ -23816,12 +24392,12 @@ var walk = (root, directory, output) => {
23816
24392
  var scopeRoots = (root, scope) => {
23817
24393
  const roots = [];
23818
24394
  for (const item of scope.paths.length ? scope.paths : ["."]) {
23819
- const target = path9.resolve(root, item);
24395
+ const target = path10.resolve(root, item);
23820
24396
  if (!inside(root, target))
23821
24397
  return failure("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
23822
24398
  let ancestor = target;
23823
24399
  while (!fs7.existsSync(ancestor) && ancestor !== root)
23824
- ancestor = path9.dirname(ancestor);
24400
+ ancestor = path10.dirname(ancestor);
23825
24401
  try {
23826
24402
  if (!inside(root, fs7.realpathSync(ancestor)))
23827
24403
  return failure("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
@@ -23873,7 +24449,7 @@ function captureCandidate(root, scope, environment = []) {
23873
24449
  for (const item of [...names].sort()) {
23874
24450
  if (!scopeMatches(item, normalizedScope))
23875
24451
  continue;
23876
- const target = path9.join(checkout, item);
24452
+ const target = path10.join(checkout, item);
23877
24453
  try {
23878
24454
  const stat = fs7.lstatSync(target);
23879
24455
  if (stat.isSymbolicLink()) {
@@ -24255,7 +24831,8 @@ var externalActionSchema = discriminatedUnion2("operation", [
24255
24831
  action: _enum2(["setup", "reapply_stash"]).optional(),
24256
24832
  sdd_dir: string5().optional(),
24257
24833
  target_branch: string5().optional(),
24258
- stash: _enum2(["yes", "no"]).optional()
24834
+ stash: _enum2(["yes", "no"]).optional(),
24835
+ cwd: string5().min(1).optional()
24259
24836
  }).strict()
24260
24837
  }).strict(),
24261
24838
  object4({
@@ -24263,12 +24840,13 @@ var externalActionSchema = discriminatedUnion2("operation", [
24263
24840
  payload: object4({
24264
24841
  message: string5().min(1).optional(),
24265
24842
  plan_steps: array3(chainStepSchema).min(1).max(32).optional(),
24266
- plan_branch: string5().min(1).optional()
24843
+ plan_branch: string5().min(1).optional(),
24844
+ cwd: string5().min(1).optional()
24267
24845
  }).strict()
24268
24846
  }).strict(),
24269
24847
  object4({
24270
24848
  operation: literal3("git.push"),
24271
- payload: object4({ branch: string5().min(1).optional() }).strict()
24849
+ payload: object4({ branch: string5().min(1).optional(), cwd: string5().min(1).optional() }).strict()
24272
24850
  }).strict(),
24273
24851
  object4({
24274
24852
  operation: literal3("hosting.pull_request"),
@@ -24277,16 +24855,22 @@ var externalActionSchema = discriminatedUnion2("operation", [
24277
24855
  body: string5().optional(),
24278
24856
  draft: boolean5().optional(),
24279
24857
  target_branch: string5().optional(),
24280
- babysit: boolean5().optional()
24858
+ babysit: boolean5().optional(),
24859
+ cwd: string5().min(1).optional()
24281
24860
  }).strict()
24282
24861
  }).strict(),
24283
24862
  object4({
24284
24863
  operation: literal3("hosting.merge"),
24285
24864
  payload: object4({
24286
24865
  target_branch: string5().min(1).optional(),
24287
- source_branch: string5().min(1).optional()
24866
+ source_branch: string5().min(1).optional(),
24867
+ cwd: string5().min(1).optional()
24288
24868
  }).strict()
24289
24869
  }).strict(),
24870
+ object4({
24871
+ operation: literal3("hosting.delete_branch"),
24872
+ payload: object4({ branch: string5().min(1), cwd: string5().min(1).optional() }).strict()
24873
+ }).strict(),
24290
24874
  object4({
24291
24875
  operation: literal3("youtrack.update"),
24292
24876
  payload: object4({
@@ -24342,12 +24926,13 @@ var externalActionSchema = discriminatedUnion2("operation", [
24342
24926
  issueRef: string5().optional(),
24343
24927
  mode: string5().optional(),
24344
24928
  specPath: string5().optional(),
24345
- planPath: string5().optional()
24929
+ planPath: string5().optional(),
24930
+ cwd: string5().min(1).optional()
24346
24931
  }).strict()
24347
24932
  }).strict()
24348
24933
  ]);
24349
24934
  // packages/workit-core/src/core/task-engine.ts
24350
- import { realpathSync as realpathSync5 } from "node:fs";
24935
+ import { realpathSync as realpathSync6 } from "node:fs";
24351
24936
 
24352
24937
  // packages/workit-core/src/core/policy-resolver.ts
24353
24938
  var resolverInputSchema = object4({
@@ -24892,6 +25477,18 @@ var portableTask = (task) => {
24892
25477
  };
24893
25478
  for (const entry of clone.assessments)
24894
25479
  demote(entry.data.facts, entry.data.signals, entry.data.consequences);
25480
+ const sourceDecisions = new Map(task.decisions.map((entry) => [entry.id, entry]));
25481
+ for (const entry of clone.decisions) {
25482
+ const consumption = sourceDecisions.get(entry.id)?.data.consumption;
25483
+ if (!consumption)
25484
+ continue;
25485
+ entry.data.consumption = {
25486
+ ...consumption,
25487
+ state: consumption.state === "reserved" ? "uncertain" : consumption.state,
25488
+ actionRef: { kind: "record", collection: "decisions", id: entry.id },
25489
+ ...consumption.historicalActionId ? { historicalActionId: consumption.historicalActionId } : consumption.actionRef.kind === "host" ? { historicalActionId: consumption.actionRef.handle } : {}
25490
+ };
25491
+ }
24895
25492
  clone.candidates = [];
24896
25493
  return clone;
24897
25494
  };
@@ -25017,7 +25614,11 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
25017
25614
  taskId: "",
25018
25615
  workspaceId: destinationWorkspaceId
25019
25616
  },
25020
- consumption: null
25617
+ consumption: data.consumption ? {
25618
+ ...data.consumption,
25619
+ state: data.consumption.state === "reserved" ? "uncertain" : data.consumption.state,
25620
+ actionRef: { kind: "record", collection: "decisions", id: entry.id }
25621
+ } : null
25021
25622
  }
25022
25623
  };
25023
25624
  }),
@@ -25058,7 +25659,7 @@ class WorkitCore {
25058
25659
  }
25059
25660
  contextRootError() {
25060
25661
  try {
25061
- if (realpathSync5(this.context.root) !== this.store.root)
25662
+ if (realpathSync6(this.context.root) !== this.store.root)
25062
25663
  return failure("invalid_input", "operation context root does not match the task store root");
25063
25664
  } catch {
25064
25665
  return failure("invalid_input", "operation context root cannot be resolved");
@@ -25217,7 +25818,7 @@ class WorkitCore {
25217
25818
  return parsed;
25218
25819
  const input = parsed.data;
25219
25820
  if ((this.context.workerId ?? null) !== null) {
25220
- if (input.action !== "inspect")
25821
+ if (input.action !== "inspect" && input.action !== "list")
25221
25822
  return failure("permission_denied", "helpers cannot control task lifecycle or scope");
25222
25823
  }
25223
25824
  switch (input.action) {
@@ -25244,7 +25845,18 @@ class WorkitCore {
25244
25845
  return listed;
25245
25846
  const status = input.status ?? "open";
25246
25847
  const limit = input.limit ?? 20;
25247
- const selected = listed.data.filter((task) => status === "all" ? true : status === "closed" ? task.status === "closed" : task.status !== "closed").sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, limit);
25848
+ const query = input.query?.toLowerCase().split(/\s+/);
25849
+ const selected = listed.data.filter((task) => status === "all" ? true : status === "closed" ? task.status === "closed" : task.status !== "closed").filter((task) => !query || query.every((term) => JSON.stringify({
25850
+ id: task.id,
25851
+ workspaceId: task.workspaceId,
25852
+ origin: task.origin,
25853
+ createdAt: task.createdAt,
25854
+ updatedAt: task.updatedAt,
25855
+ source: task.intent.provenance,
25856
+ objective: task.intent.data.objective,
25857
+ progress: task.progress,
25858
+ decisions: task.decisions.map(({ data }) => data)
25859
+ }).toLowerCase().includes(term))).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, limit);
25248
25860
  if (selected.length === 0)
25249
25861
  return success(null, null, []);
25250
25862
  const workspace = this.store.readWorkspace();
@@ -25263,7 +25875,11 @@ class WorkitCore {
25263
25875
  status: task.status,
25264
25876
  closure: task.closure,
25265
25877
  progress: task.progress,
25266
- writer: task.status === "closed" ? null : workspace.data.writer
25878
+ writer: task.status === "closed" ? null : workspace.data.writer,
25879
+ source: {
25880
+ host: task.intent.provenance.host,
25881
+ kind: task.intent.provenance.kind
25882
+ }
25267
25883
  }));
25268
25884
  return success(null, null, tasks);
25269
25885
  }
@@ -26359,7 +26975,7 @@ class WorkitCore {
26359
26975
  if (view.task.workspaceId !== view.workspace.id)
26360
26976
  return failure("invalid_input", "task and workspace bindings are invalid");
26361
26977
  try {
26362
- if (realpathSync5(view.workspace.root) !== this.store.root)
26978
+ if (realpathSync6(view.workspace.root) !== this.store.root)
26363
26979
  return failure("invalid_input", "task view root does not match the task store");
26364
26980
  } catch {
26365
26981
  return failure("invalid_input", "task view root cannot be resolved");
@@ -26636,22 +27252,22 @@ function resolveGitRevision(root, value) {
26636
27252
  }
26637
27253
  }
26638
27254
  function resolveInside(root, candidate) {
26639
- const base = realpathSync6(root);
26640
- const target = path10.resolve(base, candidate);
26641
- if (target !== base && !target.startsWith(base + path10.sep)) {
27255
+ const base = realpathSync7(root);
27256
+ const target = path11.resolve(base, candidate);
27257
+ if (target !== base && !target.startsWith(base + path11.sep)) {
26642
27258
  throw new Error("path must stay inside repository root");
26643
27259
  }
26644
27260
  let ancestor = target;
26645
27261
  while (!existsSync5(ancestor))
26646
- ancestor = path10.dirname(ancestor);
26647
- const canonicalAncestor = realpathSync6(ancestor);
26648
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path10.sep)) {
27262
+ ancestor = path11.dirname(ancestor);
27263
+ const canonicalAncestor = realpathSync7(ancestor);
27264
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path11.sep)) {
26649
27265
  throw new Error("path must stay inside repository root");
26650
27266
  }
26651
27267
  return target;
26652
27268
  }
26653
27269
  function run2(root, executable, args, env = {}) {
26654
- const cwd = realpathSync6(root);
27270
+ const cwd = realpathSync7(root);
26655
27271
  const result = spawnSync3(executable, args, {
26656
27272
  cwd,
26657
27273
  encoding: "utf8",
@@ -26724,18 +27340,19 @@ var redactSecrets = (value) => applyPatterns(value);
26724
27340
 
26725
27341
  // packages/workit-core/src/core/pr-create.ts
26726
27342
  import fs8 from "node:fs";
26727
- import path11 from "node:path";
27343
+ import path12 from "node:path";
27344
+ import { spawnSync as spawnSync4 } from "node:child_process";
26728
27345
  function parseGhIssue(value) {
26729
27346
  const m = /issues\/(\d+)/.exec(value);
26730
27347
  return m ? m[1] : String(value).trim().replace(/^#/, "");
26731
27348
  }
26732
27349
  function whichOnPath(tool) {
26733
27350
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
26734
- for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
27351
+ for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
26735
27352
  if (!dir)
26736
27353
  continue;
26737
27354
  for (const name of names) {
26738
- const candidate = path11.join(dir, name);
27355
+ const candidate = path12.join(dir, name);
26739
27356
  try {
26740
27357
  fs8.accessSync(candidate, fs8.constants.X_OK);
26741
27358
  return candidate;
@@ -26745,11 +27362,21 @@ function whichOnPath(tool) {
26745
27362
  return null;
26746
27363
  }
26747
27364
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
27365
+ var pushRemote = (cwd) => {
27366
+ const result = spawnSync4("git", ["remote", "get-url", "--push", "--all", "origin"], {
27367
+ cwd,
27368
+ encoding: "utf8"
27369
+ });
27370
+ if (result.status !== 0)
27371
+ return null;
27372
+ const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
27373
+ return urls.length === 1 ? urls[0] : null;
27374
+ };
26748
27375
 
26749
27376
  // packages/workit-core/src/core/repo-context.ts
26750
- import { spawnSync as spawnSync4 } from "node:child_process";
27377
+ import { spawnSync as spawnSync5 } from "node:child_process";
26751
27378
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
26752
- import path12 from "node:path";
27379
+ import path13 from "node:path";
26753
27380
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
26754
27381
  try {
26755
27382
  gitRevisionParts(value);
@@ -26776,7 +27403,7 @@ var invalidRange = (cwd) => ({
26776
27403
  cwd
26777
27404
  });
26778
27405
  var runGit = (cwd, args) => {
26779
- const result = spawnSync4("git", args, { cwd, encoding: "utf8" });
27406
+ const result = spawnSync5("git", args, { cwd, encoding: "utf8" });
26780
27407
  return {
26781
27408
  stdout: (result.stdout ?? "").trimEnd(),
26782
27409
  stderr: (result.stderr ?? "").trimEnd(),
@@ -26883,7 +27510,7 @@ var findPrTemplate = (cwd) => {
26883
27510
  ]) {
26884
27511
  const parts = rel.split("/");
26885
27512
  const base = parts[parts.length - 1];
26886
- const dir = parts.length > 1 ? path12.join(cwd, ...parts.slice(0, -1)) : cwd;
27513
+ const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
26887
27514
  const actual = probeCaseInsensitive(dir, base);
26888
27515
  if (actual)
26889
27516
  return [...parts.slice(0, -1), actual].join("/");
@@ -26914,7 +27541,7 @@ var diffStatForRange = (cwd, range) => {
26914
27541
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
26915
27542
  };
26916
27543
  var packageScripts = (cwd, keys) => {
26917
- const pkgPath = path12.join(cwd, "package.json");
27544
+ const pkgPath = path13.join(cwd, "package.json");
26918
27545
  if (!existsSync6(pkgPath))
26919
27546
  return [];
26920
27547
  try {
@@ -26940,9 +27567,9 @@ var documentationFiles = (cwd) => {
26940
27567
  if (entry.isDirectory()) {
26941
27568
  if (excluded.has(entry.name))
26942
27569
  continue;
26943
- walk(path12.join(dir, entry.name), depth + 1);
27570
+ walk(path13.join(dir, entry.name), depth + 1);
26944
27571
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
26945
- matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
27572
+ matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
26946
27573
  }
26947
27574
  }
26948
27575
  };
@@ -26958,7 +27585,7 @@ var readTrimmed = (file, maxLines) => {
26958
27585
  `);
26959
27586
  };
26960
27587
  function prReadyContext(root, range) {
26961
- const cwd = path12.resolve(repoRoot(root));
27588
+ const cwd = path13.resolve(repoRoot(root));
26962
27589
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26963
27590
  return invalidRange(cwd);
26964
27591
  let stdout = "";
@@ -27016,7 +27643,7 @@ ${status.stderr}` : "");
27016
27643
  stdout += `template_path: ${template}
27017
27644
 
27018
27645
  `;
27019
- stdout += readTrimmed(path12.join(cwd, template), 220);
27646
+ stdout += readTrimmed(path13.join(cwd, template), 220);
27020
27647
  } else {
27021
27648
  stdout += `template_path: none
27022
27649
 
@@ -27026,7 +27653,7 @@ ${status.stderr}` : "");
27026
27653
  stdout += `
27027
27654
  `;
27028
27655
  stdout += printSection("Recent Validation Signals");
27029
- if (existsSync6(path12.join(cwd, "package.json"))) {
27656
+ if (existsSync6(path13.join(cwd, "package.json"))) {
27030
27657
  stdout += `package.json detected. Common scripts:
27031
27658
  `;
27032
27659
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -27035,7 +27662,7 @@ ${status.stderr}` : "");
27035
27662
  `) + `
27036
27663
  `;
27037
27664
  }
27038
- if (existsSync6(path12.join(cwd, "Cargo.toml")) || existsSync6(path12.join(cwd, "src-tauri/Cargo.toml"))) {
27665
+ if (existsSync6(path13.join(cwd, "Cargo.toml")) || existsSync6(path13.join(cwd, "src-tauri/Cargo.toml"))) {
27039
27666
  stdout += `Rust project detected.
27040
27667
  `;
27041
27668
  }
@@ -27068,7 +27695,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
27068
27695
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
27069
27696
  - If Unreleased already has duplicate category headings, normalize_only first.`;
27070
27697
  function changelogContext(root, range) {
27071
- const cwd = path12.resolve(repoRoot(root));
27698
+ const cwd = path13.resolve(repoRoot(root));
27072
27699
  if (range !== undefined && !isResolvableContextRange(cwd, range))
27073
27700
  return invalidRange(cwd);
27074
27701
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -27086,7 +27713,7 @@ function changelogContext(root, range) {
27086
27713
  stdout += CHANGELOG_RULES + `
27087
27714
  `;
27088
27715
  stdout += printSection("Existing CHANGELOG.md");
27089
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
27716
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
27090
27717
  if (existsSync6(changelogPath)) {
27091
27718
  stdout += readTrimmed(changelogPath, 260) + `
27092
27719
  `;
@@ -27106,7 +27733,7 @@ function changelogContext(root, range) {
27106
27733
  return { stdout, stderr: "", exitCode: 0, cwd };
27107
27734
  }
27108
27735
  function docsRefreshContext(root, range) {
27109
- const cwd = path12.resolve(repoRoot(root));
27736
+ const cwd = path13.resolve(repoRoot(root));
27110
27737
  if (range !== undefined && !isResolvableContextRange(cwd, range))
27111
27738
  return invalidRange(cwd);
27112
27739
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -27128,7 +27755,7 @@ function docsRefreshContext(root, range) {
27128
27755
  `) + `
27129
27756
  `;
27130
27757
  stdout += printSection("README Preview");
27131
- const readme = path12.join(cwd, "README.md");
27758
+ const readme = path13.join(cwd, "README.md");
27132
27759
  if (existsSync6(readme)) {
27133
27760
  stdout += readTrimmed(readme, 220) + `
27134
27761
  `;
@@ -27137,7 +27764,7 @@ function docsRefreshContext(root, range) {
27137
27764
  `;
27138
27765
  }
27139
27766
  stdout += printSection("Package Scripts");
27140
- const pkgPath = path12.join(cwd, "package.json");
27767
+ const pkgPath = path13.join(cwd, "package.json");
27141
27768
  if (existsSync6(pkgPath)) {
27142
27769
  try {
27143
27770
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
@@ -27151,7 +27778,7 @@ function docsRefreshContext(root, range) {
27151
27778
  return { stdout, stderr: "", exitCode: 0, cwd };
27152
27779
  }
27153
27780
  function releaseNotesContext(root, rangeOrTag) {
27154
- const cwd = path12.resolve(repoRoot(root));
27781
+ const cwd = path13.resolve(repoRoot(root));
27155
27782
  if (!rangeOrTag) {
27156
27783
  return { stdout: "", stderr: `ERROR: release tag or range required
27157
27784
  `, exitCode: 1, cwd };
@@ -27189,7 +27816,7 @@ function releaseNotesContext(root, rangeOrTag) {
27189
27816
  `;
27190
27817
  stdout += printSection("Existing Release Files");
27191
27818
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
27192
- if (existsSync6(path12.join(cwd, rel)))
27819
+ if (existsSync6(path13.join(cwd, rel)))
27193
27820
  stdout += rel + `
27194
27821
  `;
27195
27822
  }
@@ -27217,19 +27844,7 @@ function releaseNotesContext(root, rangeOrTag) {
27217
27844
  }
27218
27845
 
27219
27846
  // packages/workit-core/src/core/tracker-issues.ts
27220
- import fs9 from "node:fs";
27221
- import { spawnSync as spawnSync5 } from "node:child_process";
27222
- var trackerRequest = async (url, init) => {
27223
- try {
27224
- const res = await fetch(url, { method: init.method, headers: init.headers });
27225
- const text = await res.text();
27226
- if (!res.ok)
27227
- return { status: res.status, stdout: "", stderr: text.slice(0, 200) };
27228
- return { status: 0, stdout: text, stderr: "" };
27229
- } catch (err) {
27230
- return { status: 1, stdout: "", stderr: err instanceof Error ? err.message : "network error" };
27231
- }
27232
- };
27847
+ import { spawnSync as spawnSync6 } from "node:child_process";
27233
27848
  var repoPathFromRemote = (remote) => {
27234
27849
  let rest = (remote || "").trim().replace(/\/+$/, "");
27235
27850
  if (!rest)
@@ -27250,7 +27865,11 @@ var defaultCli = (bin, available) => {
27250
27865
  if (!available)
27251
27866
  return null;
27252
27867
  return (args, root) => {
27253
- const out = spawnSync5(bin, args, { cwd: root, encoding: "utf8" });
27868
+ const out = spawnSync6(bin, args, {
27869
+ cwd: root,
27870
+ encoding: "utf8",
27871
+ env: { ...process.env, PATH: process.env.PATH ?? "" }
27872
+ });
27254
27873
  return Promise.resolve({
27255
27874
  status: out.status ?? 1,
27256
27875
  stdout: out.stdout ?? "",
@@ -27258,49 +27877,22 @@ var defaultCli = (bin, available) => {
27258
27877
  });
27259
27878
  };
27260
27879
  };
27261
- var tryCliTriple = async (cli, args, root, id, idKeys, bodyKeys) => {
27880
+ var tryCliTriple = async (cli, name, args, root, id, idKeys, bodyKeys) => {
27262
27881
  if (!cli)
27263
- return null;
27882
+ return { error: `${name} CLI is unavailable`, kind: "creds" };
27264
27883
  const run = cli(args, root);
27265
27884
  if (!run)
27266
- return null;
27885
+ return { error: `${name} CLI is unavailable`, kind: "creds" };
27267
27886
  const out = await run;
27268
27887
  if (out.status !== 0)
27269
- return null;
27888
+ return { error: `${name} CLI is not authenticated or the issue is unavailable`, kind: "creds" };
27270
27889
  try {
27271
- return parseTriple(JSON.parse(out.stdout), id, idKeys, bodyKeys);
27890
+ const triple = parseTriple(JSON.parse(out.stdout), id, idKeys, bodyKeys);
27891
+ return triple ? { data: triple } : { error: `unexpected ${name} issue response`, kind: "request" };
27272
27892
  } catch {
27273
- return null;
27893
+ return { error: `invalid ${name} issue response`, kind: "request" };
27274
27894
  }
27275
27895
  };
27276
- var originRemote = (root) => {
27277
- const out = spawnSync5("git", ["remote", "get-url", "origin"], { cwd: root, encoding: "utf8" });
27278
- return out.status === 0 ? (out.stdout ?? "").trim() || null : null;
27279
- };
27280
- var trackerCreds = (root, provider) => {
27281
- const cfg = vcsConfig("load", root);
27282
- if (!cfg || cfg.provider !== provider || typeof cfg.tokenPath !== "string" || !cfg.tokenPath)
27283
- return { error: `unconfigured ${provider} provider` };
27284
- let token = "";
27285
- try {
27286
- token = fs9.readFileSync(cfg.tokenPath, "utf8").trim();
27287
- } catch {
27288
- return { error: `missing ${provider} token` };
27289
- }
27290
- if (!token)
27291
- return { error: `empty ${provider} token file` };
27292
- if (provider === "gitlab") {
27293
- const gitlab = cfg.gitlab;
27294
- const apiUrl = String(gitlab?.apiUrl ?? "https://gitlab.com/api/v4").replace(/\/+$/, "");
27295
- return { token, api: apiUrl };
27296
- }
27297
- const github = cfg.github;
27298
- const host = String(github?.host ?? "github.com").toLowerCase();
27299
- return {
27300
- token,
27301
- api: host === "github.com" ? "https://api.github.com" : `https://${host}/api/v3`
27302
- };
27303
- };
27304
27896
  var parseTriple = (parsed, id, idKeys, bodyKeys) => {
27305
27897
  const title = parsed.title;
27306
27898
  if (typeof title !== "string")
@@ -27323,62 +27915,36 @@ var fetchGitHubIssueBody = async (ref, root, deps = {}) => {
27323
27915
  const id = parseGhIssue(ref);
27324
27916
  if (!/^\d+$/.test(id))
27325
27917
  return { error: `invalid GitHub issue ref "${ref}"`, kind: "input" };
27326
- const repo = repoPathFromRemote((deps.remote ?? originRemote)(root) ?? "");
27918
+ const remote = (deps.remote ?? pushRemote)(root);
27919
+ const repo = repoPathFromRemote(remote ?? "");
27327
27920
  if (!repo)
27328
27921
  return { error: "no origin remote to resolve the repository", kind: "input" };
27329
- const viaCli = await tryCliTriple(deps.cli ?? defaultCli("gh", hostingCliAvailable("github")), ["issue", "view", id, "--repo", repo, "--json", "number,title,body,state"], root, id, ["number"], ["body"]);
27330
- if (viaCli)
27331
- return { data: viaCli };
27332
- const credentials = (deps.creds ?? ((r) => trackerCreds(r, "github")))(root);
27333
- if ("error" in credentials)
27334
- return { ...credentials, kind: "creds" };
27335
- const out = await (deps.request ?? trackerRequest)(`${credentials.api}/repos/${repo}/issues/${id}`, {
27336
- method: "GET",
27337
- headers: {
27338
- Authorization: `Bearer ${credentials.token}`,
27339
- Accept: "application/vnd.github+json"
27340
- }
27341
- });
27342
- if (out.status !== 0)
27343
- return { error: out.stderr || "GitHub issue fetch failed", kind: "request" };
27344
- try {
27345
- const triple = parseTriple(JSON.parse(out.stdout), id, ["number"], ["body"]);
27346
- if (!triple)
27347
- return { error: "unexpected GitHub issue shape", kind: "request" };
27348
- return { data: triple };
27349
- } catch {
27350
- return { error: "invalid GitHub issue response", kind: "request" };
27351
- }
27922
+ const cfg = vcsConfig("load", root);
27923
+ const host = (deps.host ?? (cfg.ok && cfg.provider === "github" ? cfg.github?.host : null) ?? "github.com").toLowerCase();
27924
+ if (!remote || !hostingApiHostMatches(remote, host))
27925
+ return { error: "hosting API host does not match the push destination", kind: "input" };
27926
+ const repoArg = host === "github.com" ? repo : `${host}/${repo}`;
27927
+ return tryCliTriple(deps.cli ?? defaultCli("gh", hostingCliAvailable("github")), "gh", ["issue", "view", id, "--repo", repoArg, "--json", "number,title,body,state"], root, id, ["number"], ["body"]);
27352
27928
  };
27353
27929
  var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
27354
27930
  const id = parseGhIssue(ref);
27355
27931
  if (!/^\d+$/.test(id))
27356
27932
  return { error: `invalid GitLab issue ref "${ref}"`, kind: "input" };
27357
- const repo = repoPathFromRemote((deps.remote ?? originRemote)(root) ?? "");
27933
+ const remote = (deps.remote ?? pushRemote)(root);
27934
+ const repo = repoPathFromRemote(remote ?? "");
27358
27935
  if (!repo)
27359
27936
  return { error: "no origin remote to resolve the project", kind: "input" };
27360
- const viaCli = await tryCliTriple(deps.cli ?? defaultCli("glab", hostingCliAvailable("gitlab")), ["issue", "view", id, "-R", repo, "-F", "json"], root, id, ["iid"], ["description"]);
27361
- if (viaCli)
27362
- return { data: viaCli };
27363
- const credentials = (deps.creds ?? ((r) => trackerCreds(r, "gitlab")))(root);
27364
- if ("error" in credentials)
27365
- return { ...credentials, kind: "creds" };
27366
- const out = await (deps.request ?? trackerRequest)(`${credentials.api}/projects/${encodeURIComponent(repo)}/issues/${id}`, { method: "GET", headers: { "PRIVATE-TOKEN": credentials.token } });
27367
- if (out.status !== 0)
27368
- return { error: out.stderr || "GitLab issue fetch failed", kind: "request" };
27369
- try {
27370
- const triple = parseTriple(JSON.parse(out.stdout), id, ["iid"], ["description"]);
27371
- if (!triple)
27372
- return { error: "unexpected GitLab issue shape", kind: "request" };
27373
- return { data: triple };
27374
- } catch {
27375
- return { error: "invalid GitLab issue response", kind: "request" };
27376
- }
27937
+ const cfg = vcsConfig("load", root);
27938
+ const host = (deps.host ?? (cfg.ok && cfg.provider === "gitlab" ? cfg.gitlab?.host : null) ?? "gitlab.com").toLowerCase();
27939
+ if (!remote || !hostingApiHostMatches(remote, host))
27940
+ return { error: "hosting API host does not match the push destination", kind: "input" };
27941
+ const repoArg = host === "gitlab.com" ? repo : `https://${host}/${repo}`;
27942
+ return tryCliTriple(deps.cli ?? defaultCli("glab", hostingCliAvailable("gitlab")), "glab", ["issue", "view", id, "-R", repoArg, "-F", "json"], root, id, ["iid"], ["description"]);
27377
27943
  };
27378
27944
 
27379
27945
  // packages/workit-core/src/core/youtrack.ts
27380
- import fs10 from "node:fs";
27381
- import path13 from "node:path";
27946
+ import fs9 from "node:fs";
27947
+ import path14 from "node:path";
27382
27948
 
27383
27949
  // packages/workit-core/src/core/templates.ts
27384
27950
  var repoRoot2 = assetRoot();
@@ -27390,23 +27956,23 @@ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
27390
27956
 
27391
27957
  // packages/workit-core/src/core/youtrack.ts
27392
27958
  var ISSUE_RE = /^[A-Z]+-\d+$/;
27393
- var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE";
27394
- var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path13.join(configDir(), "youtrack.json");
27395
- var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path13.join(resolveConfigDir(), "youtrack.json");
27959
+ var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
27960
+ var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path14.join(configDir(), "youtrack.json");
27961
+ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path14.join(resolveConfigDir(), "youtrack.json");
27396
27962
  var youTrackTokenModeOk = (p) => {
27397
27963
  if (process.platform === "win32")
27398
27964
  return true;
27399
- const mode = fs10.statSync(p).mode & 511;
27965
+ const mode = fs9.statSync(p).mode & 511;
27400
27966
  return mode === 384;
27401
27967
  };
27402
27968
  function readYouTrackConfig(required) {
27403
27969
  const cfgPath = youTrackConfigPath();
27404
- if (!fs10.existsSync(cfgPath)) {
27970
+ if (!fs9.existsSync(cfgPath)) {
27405
27971
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
27406
27972
  }
27407
27973
  let parsed;
27408
27974
  try {
27409
- parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
27975
+ parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
27410
27976
  } catch {
27411
27977
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
27412
27978
  }
@@ -27417,11 +27983,11 @@ function readYouTrackConfig(required) {
27417
27983
  }
27418
27984
  var readYouTrackContextConfig = () => {
27419
27985
  const cfgPath = youTrackReadOnlyConfigPath();
27420
- if (!fs10.existsSync(cfgPath))
27986
+ if (!fs9.existsSync(cfgPath))
27421
27987
  return { data: {} };
27422
27988
  let parsed;
27423
27989
  try {
27424
- parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
27990
+ parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
27425
27991
  } catch {
27426
27992
  return { error: "YouTrack configuration is unavailable" };
27427
27993
  }
@@ -27446,12 +28012,12 @@ function youTrackGreeting(configOverride) {
27446
28012
  const cfgPath = configOverride ?? youTrackConfigPath();
27447
28013
  let parsed;
27448
28014
  try {
27449
- parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
28015
+ parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
27450
28016
  } catch {
27451
28017
  return {
27452
28018
  stdout: "",
27453
28019
  exitCode: 1,
27454
- stderr: fs10.existsSync(cfgPath) ? `${cfgPath} is not valid JSON` : `missing youtrack.json: ${cfgPath}`
28020
+ stderr: fs9.existsSync(cfgPath) ? `${cfgPath} is not valid JSON` : `missing youtrack.json: ${cfgPath}`
27455
28021
  };
27456
28022
  }
27457
28023
  if (!isConfigObject(parsed)) {
@@ -27497,15 +28063,15 @@ var youTrackToken = () => {
27497
28063
  if ("error" in loaded)
27498
28064
  return loaded;
27499
28065
  const tokenFile = String(loaded.config.tokenFile ?? "");
27500
- const tokenPath = tokenFile ? path13.isAbsolute(tokenFile) ? path13.resolve(tokenFile) : path13.resolve(process.cwd(), tokenFile) : "";
27501
- if (!tokenPath || !fs10.existsSync(tokenPath))
28066
+ const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
28067
+ if (!tokenPath || !fs9.existsSync(tokenPath))
27502
28068
  return { error: "missing youtrack.token" };
27503
28069
  if (!youTrackTokenModeOk(tokenPath))
27504
28070
  return { error: "youtrack.token mode must be 0600" };
27505
- const token = fs10.readFileSync(tokenPath, "utf8").trim();
28071
+ const token = fs9.readFileSync(tokenPath, "utf8").trim();
27506
28072
  if (!token)
27507
28073
  return { error: "empty token file" };
27508
- if (token === TOKEN_PLACEHOLDER2 || token.startsWith(TOKEN_PLACEHOLDER2)) {
28074
+ if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
27509
28075
  return {
27510
28076
  error: "token file still has placeholder YOUR_TOKEN_HERE — edit the file locally, then run /wk-status"
27511
28077
  };
@@ -27568,9 +28134,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
27568
28134
  } catch {
27569
28135
  continue;
27570
28136
  }
27571
- if (!fs10.existsSync(full))
28137
+ if (!fs9.existsSync(full))
27572
28138
  continue;
27573
- const text = fs10.readFileSync(full, "utf8");
28139
+ const text = fs9.readFileSync(full, "utf8");
27574
28140
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
27575
28141
  if (m)
27576
28142
  return m[1];
@@ -27804,6 +28370,7 @@ var readExternalContext = async (root, payload) => {
27804
28370
  }
27805
28371
  }
27806
28372
  };
28373
+ var targetWriterActionLease = Symbol("target-writer-action-lease");
27807
28374
 
27808
28375
  // packages/workit-mcp/src/server.ts
27809
28376
  class McpCapabilityUnavailableError extends Error {
@@ -27848,13 +28415,13 @@ var rootVariants = (workspaceRoot) => {
27848
28415
  if (!workspaceRoot)
27849
28416
  return [];
27850
28417
  const roots = new Set;
27851
- for (const candidate of [workspaceRoot, path14.resolve(workspaceRoot)]) {
27852
- if (path14.isAbsolute(candidate) && candidate !== path14.parse(candidate).root)
28418
+ for (const candidate of [workspaceRoot, path15.resolve(workspaceRoot)]) {
28419
+ if (path15.isAbsolute(candidate) && candidate !== path15.parse(candidate).root)
27853
28420
  roots.add(candidate);
27854
28421
  }
27855
28422
  try {
27856
- const canonical = realpathSync7(workspaceRoot);
27857
- if (canonical !== path14.parse(canonical).root)
28423
+ const canonical = realpathSync8(workspaceRoot);
28424
+ if (canonical !== path15.parse(canonical).root)
27858
28425
  roots.add(canonical);
27859
28426
  } catch {}
27860
28427
  return [...roots].sort((left, right) => right.length - left.length);
@@ -27955,11 +28522,11 @@ var thrownResult = (tool, error, workspaceRoot) => {
27955
28522
  };
27956
28523
  function createMcpServer(host, contextProvider) {
27957
28524
  assertMcpHost(host);
27958
- const here = path14.dirname(fileURLToPath2(import.meta.url));
28525
+ const here = path15.dirname(fileURLToPath2(import.meta.url));
27959
28526
  const sourceMarker = markSourcesLoaded([
27960
- path14.join(here, "server.ts"),
27961
- path14.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
27962
- path14.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
28527
+ path15.join(here, "server.ts"),
28528
+ path15.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
28529
+ path15.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
27963
28530
  ].filter((file) => existsSync7(file)));
27964
28531
  let staleWarned = false;
27965
28532
  const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
@@ -28139,8 +28706,8 @@ var main = async () => {
28139
28706
  process.exitCode = 2;
28140
28707
  }
28141
28708
  };
28142
- var entryPath = process.argv[1] ? path15.resolve(process.argv[1]) : "";
28143
- if (entryPath === path15.resolve(fileURLToPath3(import.meta.url)))
28709
+ var entryPath = process.argv[1] ? path16.resolve(process.argv[1]) : "";
28710
+ if (entryPath === path16.resolve(fileURLToPath3(import.meta.url)))
28144
28711
  await main();
28145
28712
  export {
28146
28713
  McpCapabilityUnavailableError,