@brainervirus/workit-mcp 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +893 -289
  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({
@@ -20739,7 +20755,47 @@ function operationJsonSchema(family) {
20739
20755
  return toJSONSchema(operationSchemas[family], { target: "draft-2020-12" });
20740
20756
  }
20741
20757
  var OPERATION_SCHEMA_DEPTH = 1;
20742
- var canonicalFieldsDescription = (fields) => `Canonical object fields: ${fields.join(", ")}. Workit validates the complete nested value.`;
20758
+ var schemaShapeDescription = (value, depth = 0) => {
20759
+ if (!value || typeof value !== "object")
20760
+ return "value";
20761
+ const schema = value;
20762
+ if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
20763
+ const options = Array.isArray(schema.oneOf) ? schema.oneOf : schema.anyOf;
20764
+ return `(${options.map((option) => schemaShapeDescription(option, depth)).join("|")})`;
20765
+ }
20766
+ if (schema.const !== undefined)
20767
+ return `=${JSON.stringify(schema.const)}`;
20768
+ if (Array.isArray(schema.enum))
20769
+ return `[${schema.enum.map(String).join("|")}]`;
20770
+ const type = Array.isArray(schema.type) ? schema.type.join("/") : schema.type;
20771
+ if (type === "array")
20772
+ return `arr<${schemaShapeDescription(schema.items, depth)}>`;
20773
+ if (type === "object" && schema.properties && typeof schema.properties === "object") {
20774
+ if (depth >= 2)
20775
+ return "obj";
20776
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
20777
+ const properties = schema.properties;
20778
+ const fields = Object.entries(properties).map(([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child, depth + 1)}`);
20779
+ return `obj{${fields.join(",")}}`;
20780
+ }
20781
+ if (typeof type !== "string")
20782
+ return "?";
20783
+ return type.split("/").map((part) => ({ string: "str", boolean: "bool", number: "num", null: "null" })[part] ?? part).join("|");
20784
+ };
20785
+ var canonicalFieldsDescription = (value) => {
20786
+ if (Array.isArray(value))
20787
+ return `Canonical object fields: ${value.join(", ")}. Workit validates the complete nested value.`;
20788
+ const isZodType = (schema) => ("_zod" in schema) && typeof schema._zod === "object";
20789
+ const schema = isZodType(value) ? toJSONSchema(value, { target: "draft-2020-12" }) : value;
20790
+ if (!schema.properties || typeof schema.properties !== "object") {
20791
+ const variants = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : null;
20792
+ return variants ? `Canonical shapes: ${variants.map((variant) => schemaShapeDescription(variant)).join(" | ")}. Full nested value is validated.` : "Canonical nested value; Workit validates the complete value.";
20793
+ }
20794
+ const properties = schema.properties;
20795
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
20796
+ const fields = Object.entries(properties).map(([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child)}`);
20797
+ return `Fields (! required): ${fields.join(", ")}. Full nested value is validated.`;
20798
+ };
20743
20799
  function boundedOperationJsonSchema(family, maxDepth = OPERATION_SCHEMA_DEPTH, stringifiedObjects = false) {
20744
20800
  const tolerate = (projected) => {
20745
20801
  if (!stringifiedObjects)
@@ -20763,9 +20819,10 @@ function boundedOperationJsonSchema(family, maxDepth = OPERATION_SCHEMA_DEPTH, s
20763
20819
  return node;
20764
20820
  const record = node;
20765
20821
  if (record.properties && typeof record.properties === "object") {
20766
- const fields = Object.keys(record.properties);
20822
+ const properties = record.properties;
20823
+ const fields = Object.keys(properties);
20767
20824
  if (currentDepth >= maxDepth || fields.length === 0) {
20768
- const description = stringifiedObjects ? `${canonicalFieldsDescription(fields)} A JSON-encoded string is also accepted.` : canonicalFieldsDescription(fields);
20825
+ const description = stringifiedObjects ? `${canonicalFieldsDescription(record)} A JSON-encoded string is also accepted.` : canonicalFieldsDescription(record);
20769
20826
  return tolerate({ type: "object", description });
20770
20827
  }
20771
20828
  return tolerate({
@@ -20886,6 +20943,7 @@ function requirementId(input) {
20886
20943
  }
20887
20944
  // packages/workit-core/src/core/task-store.ts
20888
20945
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
20946
+ import { AsyncLocalStorage } from "node:async_hooks";
20889
20947
  import * as fs4 from "node:fs";
20890
20948
  import { hostname as hostname2 } from "node:os";
20891
20949
  import path4 from "node:path";
@@ -21521,7 +21579,8 @@ var metadataLockSchema = object4({
21521
21579
  pid: number5().int().nonnegative().safe(),
21522
21580
  processStart: string5().nullable(),
21523
21581
  host: string5().min(1),
21524
- nonce: string5().min(1)
21582
+ nonce: string5().min(1),
21583
+ externalAction: literal3(true).optional()
21525
21584
  }).strict();
21526
21585
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
21527
21586
  var cachedRuntimeVersion = null;
@@ -21573,6 +21632,28 @@ var sameMetadataLock = (left, right) => {
21573
21632
  const parsed = metadataLockSchema.safeParse(left);
21574
21633
  return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
21575
21634
  };
21635
+ var sameDirectoryIdentity = (left, right) => {
21636
+ if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
21637
+ return false;
21638
+ const normalizedLeft = path4.resolve(left);
21639
+ const normalizedRight = path4.resolve(right);
21640
+ try {
21641
+ const a = fs4.statSync(normalizedLeft, { bigint: true });
21642
+ const b = fs4.statSync(normalizedRight, { bigint: true });
21643
+ if (!a.isDirectory() || !b.isDirectory())
21644
+ return false;
21645
+ if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
21646
+ return true;
21647
+ const canonicalLeft = fs4.realpathSync(normalizedLeft);
21648
+ const canonicalRight = fs4.realpathSync(normalizedRight);
21649
+ if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
21650
+ return true;
21651
+ return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
21652
+ } catch {
21653
+ return false;
21654
+ }
21655
+ };
21656
+ var externalActionLockRoots = new AsyncLocalStorage;
21576
21657
 
21577
21658
  class TaskStore {
21578
21659
  root;
@@ -21633,7 +21714,7 @@ class TaskStore {
21633
21714
  const item = this.readRecord(this.workspacePath, workspaceRecordSchema);
21634
21715
  if (!item.exists)
21635
21716
  return success(null, null, null);
21636
- if (item.result.ok && item.result.data.root !== this.root)
21717
+ if (item.result.ok && item.result.data.root !== this.root && !sameDirectoryIdentity(item.result.data.root, this.root))
21637
21718
  return failure("recovery_required", "workspace root binding is invalid", {
21638
21719
  path: this.workspacePath
21639
21720
  });
@@ -21724,6 +21805,29 @@ class TaskStore {
21724
21805
  const current = this.readWorkspace();
21725
21806
  if (!current.ok)
21726
21807
  return current;
21808
+ const origin = input.task.origin;
21809
+ if (current.data && origin) {
21810
+ if (origin.workspaceId === current.data.id) {
21811
+ const source = this.readTask(origin.taskId);
21812
+ if (source.ok)
21813
+ return success(source.data.revision, current.data.revision, source.data);
21814
+ if (source.code !== "not_found")
21815
+ return source;
21816
+ return failure("not_found", "source task is not present in this shared workspace");
21817
+ }
21818
+ const tasks = this.listTasks();
21819
+ if (!tasks.ok)
21820
+ return tasks;
21821
+ const prior = tasks.data.filter((task) => task.origin?.workspaceId === origin.workspaceId && task.origin?.taskId === origin.taskId);
21822
+ if (prior.length > 1)
21823
+ return failure("recovery_required", `source task has multiple imported mappings: ${prior.map((task) => task.id).join(", ")}`);
21824
+ if (prior.length === 1) {
21825
+ const existing = prior[0];
21826
+ if (existing.origin.exportDigest !== origin.exportDigest)
21827
+ return failure("revision_conflict", "source task export changed; reconcile the mapped destination task before importing", { taskId: existing.id });
21828
+ return success(existing.revision, current.data.revision, existing);
21829
+ }
21830
+ }
21727
21831
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
21728
21832
  return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
21729
21833
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
@@ -22112,6 +22216,73 @@ class TaskStore {
22112
22216
  })
22113
22217
  };
22114
22218
  }
22219
+ externalActionLockOptions() {
22220
+ const options = this.metadataLockOptions();
22221
+ return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
22222
+ }
22223
+ async withExternalActionLock(operation, reentrant = false) {
22224
+ const activeRoots = externalActionLockRoots.getStore();
22225
+ if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
22226
+ return operation();
22227
+ try {
22228
+ this.initializeMutationStorage();
22229
+ } catch (error) {
22230
+ return failure("storage_error", `unable to initialize store: ${String(error)}`, {
22231
+ path: this.workitDir
22232
+ });
22233
+ }
22234
+ let handle;
22235
+ try {
22236
+ handle = acquireFileLockSync(this.workspacePath, this.externalActionLockOptions());
22237
+ } catch (error) {
22238
+ return this.lockFailure(error);
22239
+ }
22240
+ let result;
22241
+ try {
22242
+ if (!handle.verifyStillHeld())
22243
+ result = failure("recovery_required", "metadata lock was compromised", {
22244
+ path: this.lockPath
22245
+ });
22246
+ else {
22247
+ try {
22248
+ const run = () => operation();
22249
+ result = await (reentrant ? externalActionLockRoots.run(new Set([...activeRoots ?? [], this.root]), run) : run());
22250
+ } catch {
22251
+ result = failure("external_outcome_unknown", "external action outcome is unknown", {
22252
+ outcome: "unknown"
22253
+ });
22254
+ }
22255
+ try {
22256
+ if (!handle.verifyStillHeld())
22257
+ result = failure("external_outcome_unknown", "metadata lock was compromised", {
22258
+ outcome: "unknown",
22259
+ path: this.lockPath
22260
+ });
22261
+ } catch {
22262
+ result = failure("external_outcome_unknown", "metadata lock could not be verified", {
22263
+ outcome: "unknown",
22264
+ path: this.lockPath
22265
+ });
22266
+ }
22267
+ }
22268
+ } catch {
22269
+ result = failure("external_outcome_unknown", "external action lock operation failed", {
22270
+ outcome: "unknown",
22271
+ path: this.lockPath
22272
+ });
22273
+ }
22274
+ try {
22275
+ handle.release();
22276
+ } catch (error) {
22277
+ const released = this.lockFailure(error);
22278
+ const releaseError = released.ok ? "metadata lock release failed" : released.error;
22279
+ return result.ok ? failure("external_outcome_unknown", "external action metadata lock release is uncertain", {
22280
+ outcome: "unknown",
22281
+ path: this.lockPath
22282
+ }) : failure("recovery_required", `${result.error}; ${releaseError}`, result.details);
22283
+ }
22284
+ return result;
22285
+ }
22115
22286
  recoveryLockOptions(observed, evidence, gate) {
22116
22287
  const options = this.metadataLockOptions();
22117
22288
  options.staleRecovery = "remove-if-unchanged";
@@ -22181,6 +22352,14 @@ class TaskStore {
22181
22352
  lockFailure(error) {
22182
22353
  const value = error;
22183
22354
  const code = typeof value?.code === "string" ? value.code : "";
22355
+ if (code === "EEXIST" || code === "file_lock_timeout") {
22356
+ const lock = this.readLockSnapshot();
22357
+ if (lock.ok && lock.data?.data.externalAction)
22358
+ return failure("writer_conflict", "workspace is reserved by a managed external action", {
22359
+ outcome: "not_started",
22360
+ path: this.lockPath
22361
+ });
22362
+ }
22184
22363
  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
22364
  return failure(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
22186
22365
  }
@@ -22448,21 +22627,23 @@ function assertProductWriteAllowed(input) {
22448
22627
  }
22449
22628
 
22450
22629
  // packages/workit-core/src/core/task-evaluation.ts
22451
- import { createHash as createHash5 } from "node:crypto";
22630
+ import { createHash as createHash6 } from "node:crypto";
22452
22631
  import * as fs7 from "node:fs";
22453
22632
  import { spawnSync as spawnSync2 } from "node:child_process";
22454
- import path9 from "node:path";
22633
+ import path10 from "node:path";
22455
22634
 
22456
22635
  // packages/workit-core/src/core/authority.ts
22457
- import { createHash as createHash4 } from "node:crypto";
22636
+ import { createHash as createHash5 } from "node:crypto";
22458
22637
  import * as fs6 from "node:fs";
22459
- import path8 from "node:path";
22638
+ import path9 from "node:path";
22460
22639
 
22461
22640
  // packages/workit-core/src/core/auto-approval.ts
22462
- import { createHash as createHash3 } from "node:crypto";
22463
- import { readFileSync as readFileSync4 } from "node:fs";
22641
+ import { createHash as createHash4 } from "node:crypto";
22642
+ import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
22643
+ import path8 from "node:path";
22464
22644
 
22465
22645
  // packages/workit-core/src/core/workspaces.ts
22646
+ import { createHash as createHash3 } from "node:crypto";
22466
22647
  import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
22467
22648
  import path6 from "node:path";
22468
22649
 
@@ -22480,7 +22661,7 @@ import os from "node:os";
22480
22661
  import path5 from "node:path";
22481
22662
 
22482
22663
  // packages/workit-core/src/core/boundary.ts
22483
- import { statSync } from "node:fs";
22664
+ import { statSync as statSync2 } from "node:fs";
22484
22665
  var EVENT = {
22485
22666
  initialization: "initialization",
22486
22667
  provenance: "provenance",
@@ -22508,7 +22689,7 @@ var changedSourcesSinceLoad = (marker) => {
22508
22689
  const changed = [];
22509
22690
  for (const file of marker.files) {
22510
22691
  try {
22511
- if (statSync(file).mtimeMs > marker.loadedAt)
22692
+ if (statSync2(file).mtimeMs > marker.loadedAt)
22512
22693
  changed.push(file);
22513
22694
  } catch {}
22514
22695
  }
@@ -22667,33 +22848,197 @@ var COMMIT_PRESETS = [
22667
22848
  "custom",
22668
22849
  "auto"
22669
22850
  ];
22851
+ var resolveCommitPolicy = (config, workspace) => {
22852
+ const wp = workspace?.commitPolicy ?? {};
22853
+ if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
22854
+ return {
22855
+ preset: wp.preset,
22856
+ ...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
22857
+ };
22858
+ }
22859
+ return config.commitPolicy ?? { preset: "conventional" };
22860
+ };
22670
22861
 
22671
22862
  // packages/workit-core/src/core/workspaces.ts
22863
+ var nonBlank = string5().refine((value) => value.trim().length > 0, "must not be blank");
22864
+ var branchPreset = custom2((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
22865
+ var commitPreset = custom2((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
22866
+ var workspaceBranchPolicySchema = object4({
22867
+ preset: branchPreset,
22868
+ developBranch: nonBlank.optional(),
22869
+ prefixes: object4({
22870
+ feature: nonBlank,
22871
+ bugfix: nonBlank,
22872
+ release: nonBlank,
22873
+ hotfix: nonBlank
22874
+ }).passthrough().optional(),
22875
+ allowed: array3(nonBlank).optional(),
22876
+ protected: array3(nonBlank).optional(),
22877
+ integration: _enum2(["pr", "merge"]).optional()
22878
+ }).strict();
22879
+ var workspaceCommitPolicySchema = object4({ preset: commitPreset, pattern: string5().optional() }).strict();
22880
+ var workspaceProfileSchema = object4({
22881
+ branchPolicy: workspaceBranchPolicySchema.optional(),
22882
+ commitPolicy: workspaceCommitPolicySchema.optional()
22883
+ }).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
22884
+ message: "a profile must define branchPolicy or commitPolicy"
22885
+ });
22886
+ var versionSourceSchema = discriminatedUnion2("kind", [
22887
+ object4({
22888
+ kind: literal3("package-json"),
22889
+ path: nonBlank,
22890
+ field: nonBlank
22891
+ }).strict(),
22892
+ object4({ kind: literal3("git-tag") }).strict(),
22893
+ object4({ kind: literal3("manual") }).strict()
22894
+ ]);
22895
+ var releaseTrackSchema = object4({
22896
+ strategy: branchPreset,
22897
+ productionBranch: nonBlank,
22898
+ integrationBranch: nonBlank,
22899
+ naming: object4({ feature: nonBlank, release: nonBlank, hotfix: nonBlank }).strict(),
22900
+ baseBranch: nonBlank,
22901
+ mergeBackBranches: array3(nonBlank),
22902
+ pullRequestTarget: nonBlank,
22903
+ tagNamespace: string5(),
22904
+ versionSource: versionSourceSchema,
22905
+ requiredChecks: array3(nonBlank)
22906
+ }).strict().superRefine((track, context) => {
22907
+ for (const [field, values] of [
22908
+ ["mergeBackBranches", track.mergeBackBranches],
22909
+ ["requiredChecks", track.requiredChecks]
22910
+ ]) {
22911
+ if (new Set(values).size !== values.length) {
22912
+ context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
22913
+ }
22914
+ }
22915
+ 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(".."))) {
22916
+ context.addIssue({
22917
+ code: "custom",
22918
+ path: ["versionSource", "path"],
22919
+ message: "must stay inside the repository"
22920
+ });
22921
+ }
22922
+ });
22923
+ var workspaceConfigSchema = object4({
22924
+ name: nonBlank,
22925
+ glob: nonBlank,
22926
+ vcs: object4({
22927
+ provider: _enum2(["gitlab", "github"]),
22928
+ defaultTargetBranch: nonBlank.optional(),
22929
+ tokenFile: nonBlank.optional(),
22930
+ account: nonBlank.optional()
22931
+ }).passthrough().optional(),
22932
+ autoApprove: union3([boolean5(), array3(nonBlank)]).optional(),
22933
+ youtrack: object4({ baseUrl: nonBlank.optional(), link_issues: boolean5().optional() }).passthrough().optional(),
22934
+ issues: object4({ provider: literal3("github").optional(), link_on_pr: boolean5().optional() }).passthrough().optional(),
22935
+ branchPolicy: workspaceBranchPolicySchema.optional(),
22936
+ commitPolicy: workspaceCommitPolicySchema.optional(),
22937
+ defaultProfile: nonBlank.optional(),
22938
+ profiles: record3(nonBlank, workspaceProfileSchema).optional(),
22939
+ releaseTracks: record3(nonBlank, releaseTrackSchema).optional()
22940
+ }).passthrough().superRefine((workspace, context) => {
22941
+ if (workspace.youtrack && workspace.vcs?.provider !== "gitlab") {
22942
+ context.addIssue({
22943
+ code: "custom",
22944
+ path: ["youtrack"],
22945
+ message: `YouTrack issue linking requires the gitlab provider, got ${workspace.vcs?.provider ?? "unset"}`
22946
+ });
22947
+ }
22948
+ if (workspace.issues && workspace.vcs?.provider !== "github") {
22949
+ context.addIssue({
22950
+ code: "custom",
22951
+ path: ["issues"],
22952
+ message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
22953
+ });
22954
+ }
22955
+ if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
22956
+ context.addIssue({
22957
+ code: "custom",
22958
+ path: ["defaultProfile"],
22959
+ message: `does not name a configured profile`
22960
+ });
22961
+ }
22962
+ });
22963
+ var workspacesFileSchema = object4({ workspaces: array3(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
22964
+ const names = new Set;
22965
+ for (const [index, workspace] of (file.workspaces ?? []).entries()) {
22966
+ if (names.has(workspace.name)) {
22967
+ context.addIssue({
22968
+ code: "custom",
22969
+ path: ["workspaces", index, "name"],
22970
+ message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
22971
+ });
22972
+ }
22973
+ names.add(workspace.name);
22974
+ }
22975
+ });
22976
+ var workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex");
22977
+ var validateWorkspacesDocument = (parsed, file) => {
22978
+ if (!isConfigObject(parsed)) {
22979
+ return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
22980
+ }
22981
+ const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
22982
+ if (!validated.success) {
22983
+ const detail = validated.error.issues.map((issue) => {
22984
+ const field = String(issue.path[issue.path.length - 1] ?? "");
22985
+ const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
22986
+ return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
22987
+ }).join("; ");
22988
+ return {
22989
+ status: "invalid",
22990
+ path: file,
22991
+ entries: [],
22992
+ error: `${file} has invalid workspace configuration: ${detail}`
22993
+ };
22994
+ }
22995
+ for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
22996
+ const glob = validateWorkspaceGlob(workspace.glob);
22997
+ if (!glob.ok) {
22998
+ return {
22999
+ status: "invalid",
23000
+ path: file,
23001
+ entries: [],
23002
+ error: `${file} workspaces.${index}.glob: ${glob.error}`
23003
+ };
23004
+ }
23005
+ }
23006
+ return {
23007
+ status: "valid",
23008
+ path: file,
23009
+ entries: validated.data.workspaces ?? [],
23010
+ document: parsed
23011
+ };
23012
+ };
22672
23013
  var readWorkspacesResult = (dir = configDir()) => {
22673
23014
  const file = path6.join(dir, "workspaces.json");
22674
23015
  let raw;
22675
23016
  try {
22676
23017
  raw = readFileSync3(file, "utf8");
22677
- } catch {
22678
- return { status: "missing", path: file, entries: [] };
23018
+ } catch (error) {
23019
+ if (error.code === "ENOENT")
23020
+ return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
23021
+ return {
23022
+ status: "malformed",
23023
+ path: file,
23024
+ entries: [],
23025
+ error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
23026
+ };
22679
23027
  }
22680
23028
  let parsed;
22681
23029
  try {
22682
23030
  parsed = JSON.parse(raw);
22683
23031
  } 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` };
23032
+ return {
23033
+ status: "malformed",
23034
+ path: file,
23035
+ entries: [],
23036
+ revision: workspacesRevision(raw),
23037
+ error: `${file} is not valid JSON`
23038
+ };
22688
23039
  }
22689
- const list = parsed.workspaces;
22690
- return {
22691
- status: "valid",
22692
- path: file,
22693
- entries: Array.isArray(list) ? list : []
22694
- };
23040
+ return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
22695
23041
  };
22696
- var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
22697
23042
  var globToRegExp = (glob) => {
22698
23043
  let out = "";
22699
23044
  for (let i = 0;i < glob.length; i++) {
@@ -22728,6 +23073,30 @@ var globToRegExp = (glob) => {
22728
23073
  return new RegExp(`^${out}$`);
22729
23074
  };
22730
23075
  var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
23076
+ var UNSUPPORTED_GLOB = /[?[\]{}]/;
23077
+ var UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
23078
+ var validateWorkspaceGlob = (glob) => {
23079
+ const trimmed = glob.trim();
23080
+ if (!trimmed)
23081
+ return { ok: false, error: "workspace pattern is required" };
23082
+ const m = UNSUPPORTED_GLOB.exec(trimmed);
23083
+ if (m) {
23084
+ return {
23085
+ ok: false,
23086
+ error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
23087
+ };
23088
+ }
23089
+ const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
23090
+ if (ext) {
23091
+ const token = ext[0];
23092
+ const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
23093
+ return {
23094
+ ok: false,
23095
+ error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
23096
+ };
23097
+ }
23098
+ return { ok: true };
23099
+ };
22731
23100
  var realpathOf = (p) => {
22732
23101
  const real = realpathSync2.native ?? realpathSync2;
22733
23102
  try {
@@ -22745,29 +23114,132 @@ var canonicalGlob = (glob) => {
22745
23114
  const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
22746
23115
  return real + (prefix.endsWith("/") ? "/" : "") + rest;
22747
23116
  };
22748
- var resolveWorkspaceFrom = (cwd, dir) => {
23117
+ var globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length;
23118
+ var selectWorkspaceMatch = (matches, cwd, workspaceName) => {
23119
+ if (workspaceName !== undefined) {
23120
+ const named = matches.filter((entry) => entry.name === workspaceName);
23121
+ if (named.length === 1)
23122
+ return named[0];
23123
+ if (named.length > 1)
23124
+ throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
23125
+ if (matches.length > 0) {
23126
+ throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
23127
+ }
23128
+ throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
23129
+ }
23130
+ if (matches.length < 2)
23131
+ return matches[0];
23132
+ const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
23133
+ const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
23134
+ if (mostSpecific.length > 1) {
23135
+ throw new Error(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
23136
+ }
23137
+ return mostSpecific[0];
23138
+ };
23139
+ var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
23140
+ const result = readWorkspacesResult(dir);
23141
+ if (result.status === "malformed" || result.status === "invalid")
23142
+ throw new Error(result.error);
22749
23143
  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;
23144
+ const matches = [];
23145
+ for (const ws of result.entries) {
22756
23146
  const glob = ws.glob.replaceAll("\\", "/");
22757
23147
  const canonical = canonicalGlob(glob);
23148
+ let matched = false;
22758
23149
  for (const target of targets) {
22759
- if (matchWorkspace(glob, target))
22760
- return ws;
22761
- if (canonical !== glob && matchWorkspace(canonical, target))
22762
- return ws;
23150
+ if (matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target)) {
23151
+ matched = true;
23152
+ break;
23153
+ }
22763
23154
  }
23155
+ if (matched)
23156
+ matches.push(ws);
22764
23157
  }
22765
- return null;
23158
+ return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
23159
+ };
23160
+ var runtimeWorkspacePolicySchema = (kind) => {
23161
+ const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
23162
+ const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object4({
23163
+ provider: _enum2(["gitlab", "github"]),
23164
+ defaultTargetBranch: nonBlank.optional(),
23165
+ account: nonBlank.optional()
23166
+ }).passthrough();
23167
+ const profileSchema = object4({ [key]: policySchema.optional() }).passthrough();
23168
+ const entrySchema = object4({
23169
+ name: nonBlank,
23170
+ glob: nonBlank,
23171
+ [key]: policySchema.optional(),
23172
+ defaultProfile: nonBlank.optional(),
23173
+ profiles: record3(nonBlank, profileSchema).optional()
23174
+ }).passthrough();
23175
+ return entrySchema;
23176
+ };
23177
+ var resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs");
23178
+ var runtimeWorkspaceIndexSchema = object4({
23179
+ workspaces: array3(object4({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
23180
+ }).passthrough();
23181
+ var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
23182
+ const file = path6.join(configDir(), "workspaces.json");
23183
+ let raw;
23184
+ try {
23185
+ raw = readFileSync3(file, "utf8");
23186
+ } catch (error) {
23187
+ if (error.code === "ENOENT")
23188
+ return null;
23189
+ throw new Error(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`);
23190
+ }
23191
+ let parsed;
23192
+ try {
23193
+ parsed = JSON.parse(raw);
23194
+ } catch {
23195
+ throw new Error(`${file} is not valid JSON`);
23196
+ }
23197
+ const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
23198
+ if (!validated.success) {
23199
+ const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
23200
+ throw new Error(`${file} has invalid workspace matching data: ${detail}`);
23201
+ }
23202
+ const entries = validated.data.workspaces;
23203
+ for (const entry of entries ?? []) {
23204
+ const glob = validateWorkspaceGlob(entry.glob);
23205
+ if (!glob.ok)
23206
+ throw new Error(`${file} workspace ${entry.name}: ${glob.error}`);
23207
+ }
23208
+ const targets = [cwd, realpathOf(cwd)].map((value) => value.replaceAll("\\", "/"));
23209
+ const matches = (entries ?? []).filter((entry) => {
23210
+ const glob = entry.glob.replaceAll("\\", "/");
23211
+ const canonical = canonicalGlob(glob);
23212
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
23213
+ });
23214
+ const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
23215
+ const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
23216
+ if (!selected)
23217
+ return null;
23218
+ const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
23219
+ if (!policy.success) {
23220
+ const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
23221
+ throw new Error(`${file} has invalid ${kind} policy configuration: ${detail}`);
23222
+ }
23223
+ return policy.data;
23224
+ };
23225
+ var resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
23226
+ const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
23227
+ if (!candidate)
23228
+ return { policy: undefined, source: "user-default" };
23229
+ const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
23230
+ const profileName = requestedProfile?.trim() || candidate.defaultProfile;
23231
+ if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
23232
+ throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
23233
+ const profile = profileName ? candidate.profiles?.[profileName] : undefined;
23234
+ if (profileName && !profile)
23235
+ throw new Error(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
23236
+ const profilePolicy = profile?.[key];
23237
+ const workspacePolicy = candidate[key];
23238
+ return {
23239
+ policy: profilePolicy ?? workspacePolicy,
23240
+ source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
23241
+ };
22766
23242
  };
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
23243
 
22772
23244
  // packages/workit-core/src/core/git.ts
22773
23245
  import { execFileSync } from "node:child_process";
@@ -22841,7 +23313,6 @@ var gitContext = (workspaceRoot, paths = []) => {
22841
23313
  import fs5 from "node:fs";
22842
23314
  import path7 from "node:path";
22843
23315
  import { spawnSync } from "node:child_process";
22844
- var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
22845
23316
  var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
22846
23317
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
22847
23318
  var remoteProvider = (cwd) => {
@@ -22855,6 +23326,79 @@ var remoteProvider = (cwd) => {
22855
23326
  return "gitlab";
22856
23327
  return null;
22857
23328
  };
23329
+ var remoteEndpoint = (remote) => {
23330
+ const raw = remote.trim();
23331
+ if (!raw)
23332
+ return null;
23333
+ const scp = /^(?:[^@\s]+@)?([^:]+):(.+)$/u.exec(raw);
23334
+ if (scp && !raw.includes("://"))
23335
+ return {
23336
+ host: scp[1].toLowerCase(),
23337
+ path: scp[2].replace(/^\//u, "").replace(/\.git$/u, ""),
23338
+ protocol: "ssh:",
23339
+ port: ""
23340
+ };
23341
+ try {
23342
+ const url = new URL(raw);
23343
+ return {
23344
+ host: url.hostname.toLowerCase(),
23345
+ path: url.pathname.replace(/^\//u, "").replace(/\.git$/u, "").replace(/\/$/u, ""),
23346
+ protocol: url.protocol,
23347
+ port: url.port
23348
+ };
23349
+ } catch {
23350
+ return null;
23351
+ }
23352
+ };
23353
+ var configuredSshEndpoint = (host) => {
23354
+ if (host.startsWith("-"))
23355
+ return null;
23356
+ const resolved = spawnSync("ssh", ["-G", host], {
23357
+ encoding: "utf8",
23358
+ timeout: 5000,
23359
+ maxBuffer: 1024 * 1024,
23360
+ env: { ...process.env, PATH: process.env.PATH ?? "" }
23361
+ });
23362
+ if (resolved.status !== 0)
23363
+ return null;
23364
+ const output = resolved.stdout ?? "";
23365
+ const hostname = /^hostname\s+(\S+)/imu.exec(output)?.[1]?.toLowerCase();
23366
+ if (!hostname)
23367
+ return null;
23368
+ return { host: hostname, port: /^port\s+(\d+)/imu.exec(output)?.[1] ?? "22" };
23369
+ };
23370
+ var sshApiHostAliases = {
23371
+ "github.com": [{ host: "ssh.github.com", port: "443" }],
23372
+ "gitlab.com": [{ host: "altssh.gitlab.com", port: "443" }]
23373
+ };
23374
+ var hostingApiHostMatches = (remote, apiHost, resolveSshEndpoint = configuredSshEndpoint) => {
23375
+ const endpoint = remoteEndpoint(remote);
23376
+ if (!endpoint)
23377
+ return false;
23378
+ let expected;
23379
+ try {
23380
+ const url = new URL(`https://${apiHost}`);
23381
+ if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
23382
+ return false;
23383
+ expected = {
23384
+ host: url.hostname.toLowerCase().replace(/\.$/u, ""),
23385
+ port: url.port
23386
+ };
23387
+ } catch {
23388
+ return false;
23389
+ }
23390
+ if (endpoint.protocol !== "ssh:") {
23391
+ return endpoint.host.replace(/\.$/u, "") === expected.host && endpoint.port === expected.port;
23392
+ }
23393
+ const resolved = resolveSshEndpoint(endpoint.host);
23394
+ if (!resolved)
23395
+ return false;
23396
+ const hostname = resolved.host.toLowerCase().replace(/\.$/u, "");
23397
+ const port = endpoint.port || resolved.port;
23398
+ if (hostname === expected.host)
23399
+ return port === (expected.port || "22");
23400
+ return Boolean(sshApiHostAliases[expected.host]?.some((alias) => alias.host === hostname && alias.port === port && (!expected.port || expected.port === port)));
23401
+ };
22858
23402
  var readVcsConfig = () => {
22859
23403
  const file = vcsConfigPath();
22860
23404
  let raw;
@@ -22875,7 +23419,7 @@ var readVcsConfig = () => {
22875
23419
  return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
22876
23420
  };
22877
23421
  function vcsConfig(mode, cwd) {
22878
- const ws = resolveWorkspace(vcsCwd(cwd));
23422
+ const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
22879
23423
  const wsVcs = ws?.vcs ?? {};
22880
23424
  const wsYt = ws?.youtrack ?? {};
22881
23425
  const wsIssues = ws?.issues ?? {};
@@ -22888,8 +23432,8 @@ function vcsConfig(mode, cwd) {
22888
23432
  const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
22889
23433
  const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
22890
23434
  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;
23435
+ const linkIssues = provider === "gitlab" && typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
23436
+ const youtrackBaseUrl = provider === "gitlab" && typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
22893
23437
  let issuesProvider = null;
22894
23438
  let linkOnPr = null;
22895
23439
  if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
@@ -22913,9 +23457,8 @@ function vcsConfig(mode, cwd) {
22913
23457
  if (cfgStatus === "malformed") {
22914
23458
  return { ok: false, error: cfgError, configPath: cfgPath };
22915
23459
  }
22916
- if (cfgStatus === "missing") {
23460
+ if (cfgStatus === "missing" && provider === null)
22917
23461
  return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
22918
- }
22919
23462
  if (provider === null) {
22920
23463
  return {
22921
23464
  ok: false,
@@ -22924,24 +23467,12 @@ function vcsConfig(mode, cwd) {
22924
23467
  };
22925
23468
  }
22926
23469
  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
23470
  const out = {
22937
23471
  ok: true,
22938
23472
  configPath: path7.resolve(cfgPath),
22939
23473
  provider,
22940
23474
  defaultTargetBranch: defaultTarget,
22941
23475
  pr: cfg.pr ?? {},
22942
- tokenPath,
22943
- tokenPresent: fs5.existsSync(tokenPath),
22944
- tokenReady: tokenOk,
22945
23476
  workspace_name: ws?.name ?? null,
22946
23477
  link_issues: linkIssues,
22947
23478
  youtrack_base_url: youtrackBaseUrl,
@@ -22956,16 +23487,58 @@ function vcsConfig(mode, cwd) {
22956
23487
  } else if (provider === "github") {
22957
23488
  out.github = { host: prov.host ?? "github.com" };
22958
23489
  }
22959
- if (mode === "summary")
22960
- delete out.tokenPath;
22961
23490
  return out;
22962
23491
  }
23492
+ function vcsCliIdentity(cwd) {
23493
+ const root = vcsCwd(cwd);
23494
+ const cfg = vcsConfig("load", root);
23495
+ if (!cfg.ok)
23496
+ return { ok: false, error: cfg.error ?? "vcs config not ready" };
23497
+ const provider = cfg.provider;
23498
+ if (provider !== "gitlab" && provider !== "github")
23499
+ return { ok: false, error: `unsupported provider: ${provider}` };
23500
+ const bin = provider === "gitlab" ? "glab" : "gh";
23501
+ const host = String(provider === "github" ? cfg.github?.host ?? "github.com" : cfg.gitlab?.host ?? "gitlab.com").toLowerCase();
23502
+ const env = {
23503
+ ...process.env,
23504
+ PATH: process.env.PATH ?? "",
23505
+ ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
23506
+ };
23507
+ const result = spawnSync(bin, ["api", "user"], {
23508
+ cwd: root,
23509
+ encoding: "utf8",
23510
+ env
23511
+ });
23512
+ if (result.status !== 0)
23513
+ return { ok: false, provider, error: `${bin} is missing or not authenticated` };
23514
+ try {
23515
+ const user = JSON.parse(result.stdout ?? "");
23516
+ const username = provider === "gitlab" ? user.username : user.login;
23517
+ if (typeof username !== "string" || !username)
23518
+ return { ok: false, provider, error: `invalid ${bin} identity response` };
23519
+ const expected = resolveRuntimeWorkspaceVcs(root)?.vcs?.account;
23520
+ if (expected && username.toLowerCase() !== expected.toLowerCase())
23521
+ return {
23522
+ ok: false,
23523
+ provider,
23524
+ error: `${bin} account ${username} does not match ${expected}`
23525
+ };
23526
+ return { ok: true, provider, host, username, name: user.name };
23527
+ } catch {
23528
+ return { ok: false, provider, error: `invalid JSON from ${bin} api user` };
23529
+ }
23530
+ }
22963
23531
  function mergedPrStyle(limit = 6, cwd) {
22964
23532
  const cfg = vcsConfig("load", cwd);
22965
- if (!cfg.ok || !cfg.tokenReady)
23533
+ if (!cfg.ok)
22966
23534
  return { ok: false, error: "vcs not configured" };
22967
23535
  const provider = cfg.provider;
22968
- const token = fs5.readFileSync(cfg.tokenPath, "utf8").trim();
23536
+ const host = String(provider === "github" ? cfg.github?.host ?? "github.com" : cfg.gitlab?.host ?? "gitlab.com").toLowerCase();
23537
+ const env = {
23538
+ ...process.env,
23539
+ PATH: process.env.PATH ?? "",
23540
+ ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
23541
+ };
22969
23542
  const examples = [];
22970
23543
  const descInfo = (desc, caseInsensitiveNotes = false) => ({
22971
23544
  hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
@@ -22973,23 +23546,30 @@ function mergedPrStyle(limit = 6, cwd) {
22973
23546
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
22974
23547
  descriptionPreview: desc.slice(0, 600)
22975
23548
  });
23549
+ const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
23550
+ cwd,
23551
+ encoding: "utf8"
23552
+ });
23553
+ const remotes = (remoteResult.stdout ?? "").split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
23554
+ if (remotes.length === 0)
23555
+ return { ok: false, error: "no origin remote" };
23556
+ if (remotes.length !== 1)
23557
+ return { ok: false, error: "origin has ambiguous push destinations" };
23558
+ const remote = remotes[0];
23559
+ const endpoint = remoteEndpoint(remote);
23560
+ if (!endpoint?.path)
23561
+ return { ok: false, error: "hosting repository could not be resolved" };
23562
+ if (!hostingApiHostMatches(remote, host))
23563
+ return { ok: false, error: "PR style target does not match the provider host" };
22976
23564
  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 });
23565
+ const run = (args) => spawnSync("glab", ["api", ...args], {
23566
+ cwd,
23567
+ encoding: "utf8",
23568
+ env
23569
+ });
22987
23570
  let r = run([
22988
- `projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
23571
+ `projects/${endpoint.path.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
22989
23572
  ]);
22990
- if (r.status !== 0) {
22991
- r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
22992
- }
22993
23573
  if (r.status !== 0)
22994
23574
  return { ok: false, error: "could not list merge requests" };
22995
23575
  for (const mr of JSON.parse(r.stdout ?? "[]")) {
@@ -23002,7 +23582,18 @@ function mergedPrStyle(limit = 6, cwd) {
23002
23582
  });
23003
23583
  }
23004
23584
  } 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 } });
23585
+ const r = spawnSync("gh", [
23586
+ "pr",
23587
+ "list",
23588
+ "--repo",
23589
+ `${host}/${endpoint.path}`,
23590
+ "--state",
23591
+ "merged",
23592
+ "--limit",
23593
+ String(limit),
23594
+ "--json",
23595
+ "title,url,body"
23596
+ ], { cwd, encoding: "utf8", env });
23006
23597
  if (r.status !== 0)
23007
23598
  return { ok: false, error: "could not list pull requests" };
23008
23599
  for (const pr of JSON.parse(r.stdout ?? "[]")) {
@@ -23024,26 +23615,28 @@ function mergedPrStyle(limit = 6, cwd) {
23024
23615
  }
23025
23616
 
23026
23617
  // packages/workit-core/src/core/branch.ts
23027
- var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
23618
+ function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
23619
+ const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
23620
+ const config = readConfig();
23621
+ return kind === "branch" ? {
23622
+ branchPolicy: resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null),
23623
+ provenance: { branchPolicy: selected.source }
23624
+ } : {
23625
+ commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
23626
+ provenance: { commitPolicy: selected.source }
23627
+ };
23628
+ }
23629
+ var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
23028
23630
  var verifyPushIdentity = (root, provider, account) => {
23029
23631
  if (!account || typeof account !== "string")
23030
23632
  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())
23633
+ const identity = vcsCliIdentity(root);
23634
+ if (!identity.ok || identity.provider !== provider)
23635
+ return { ok: false, error: identity.error ?? "hosting CLI identity could not be resolved" };
23636
+ if (identity.username?.toLowerCase() !== account.toLowerCase())
23044
23637
  return {
23045
23638
  ok: false,
23046
- error: `push identity ${login || "(unknown)"} does not match area account ${account}`
23639
+ error: `push identity ${identity.username || "(unknown)"} does not match area account ${account}`
23047
23640
  };
23048
23641
  return { ok: true };
23049
23642
  };
@@ -23061,7 +23654,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
23061
23654
  var digestOf = (configDirPath) => {
23062
23655
  try {
23063
23656
  const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
23064
- return createHash3("sha256").update(raw).digest("hex");
23657
+ return createHash4("sha256").update(raw).digest("hex");
23065
23658
  } catch {
23066
23659
  return null;
23067
23660
  }
@@ -23099,14 +23692,30 @@ var resolveAutoApproval = (root, dir = configDir()) => {
23099
23692
  };
23100
23693
  };
23101
23694
  var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
23102
- var standingApprovalLive = (root, receipt, cls, dir) => {
23695
+ var targetOf = (root, descriptor) => {
23696
+ try {
23697
+ const parsed = typeof descriptor === "string" ? JSON.parse(descriptor) : descriptor;
23698
+ const cwd = parsed?.payload?.cwd;
23699
+ if (cwd === undefined)
23700
+ return root;
23701
+ if (typeof cwd !== "string" || !path8.isAbsolute(cwd))
23702
+ return null;
23703
+ return realpathSync3(cwd);
23704
+ } catch {
23705
+ return null;
23706
+ }
23707
+ };
23708
+ var standingApprovalLive = (root, receipt, cls, dir, descriptor) => {
23103
23709
  if (!isStandingReceipt(receipt))
23104
23710
  return false;
23105
23711
  if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
23106
23712
  return false;
23107
23713
  if (receipt.class !== cls)
23108
23714
  return false;
23109
- const resolved = resolveAutoApproval(root, dir);
23715
+ const target = targetOf(root, descriptor);
23716
+ if (!target)
23717
+ return false;
23718
+ const resolved = resolveAutoApproval(target, dir);
23110
23719
  if (resolved.status !== "on")
23111
23720
  return false;
23112
23721
  if (resolved.workspace !== receipt.workspace)
@@ -23142,16 +23751,19 @@ var verifyStandingApproval = (root, task, caller, binding) => {
23142
23751
  const cls = operationAutoClass(operation);
23143
23752
  if (!cls || cls !== standing.class)
23144
23753
  return failure("permission_denied", "standing approval does not cover this operation");
23145
- const resolved = resolveAutoApproval(root);
23754
+ const target = targetOf(root, binding.approvedContent);
23755
+ if (!target)
23756
+ return failure("permission_denied", "standing action target is invalid");
23757
+ const resolved = resolveAutoApproval(target);
23146
23758
  if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
23147
23759
  return failure("permission_denied", "standing auto-approval is not live for this operation");
23148
23760
  if (cls === "push") {
23149
- const workspace = resolveWorkspaceFrom(root, configDir());
23761
+ const workspace = resolveWorkspaceFrom(target, configDir());
23150
23762
  const provider = workspace?.vcs?.provider;
23151
23763
  const account = resolved.account;
23152
23764
  if (!provider || !account)
23153
23765
  return failure("permission_denied", "auto-push requires a workspace provider and area account");
23154
- const identity = verifyPushIdentity(root, provider, account);
23766
+ const identity = verifyPushIdentity(target, provider, account);
23155
23767
  if (!identity.ok)
23156
23768
  return failure("permission_denied", identity.error);
23157
23769
  }
@@ -23160,7 +23772,7 @@ var verifyStandingApproval = (root, task, caller, binding) => {
23160
23772
  host: caller.host,
23161
23773
  session: { kind: "host", host: caller.host, handle: caller.actor },
23162
23774
  workerId: null,
23163
- receipts: [standingReceiptFor(root, standing.workspace, cls)]
23775
+ receipts: [standingReceiptFor(target, standing.workspace, cls)]
23164
23776
  });
23165
23777
  };
23166
23778
 
@@ -23311,7 +23923,7 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
23311
23923
  return [];
23312
23924
  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
23925
  }
23314
- var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
23926
+ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
23315
23927
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23316
23928
  let root;
23317
23929
  try {
@@ -23324,23 +23936,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23324
23936
  continue;
23325
23937
  if (!reference.digest)
23326
23938
  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}`))
23939
+ const target = path9.resolve(root, reference.path);
23940
+ if (target !== root && !target.startsWith(`${root}${path9.sep}`))
23329
23941
  return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23330
23942
  fields: [
23331
23943
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
23332
23944
  ]
23333
23945
  });
23334
23946
  try {
23335
- const relative = path8.relative(root, target);
23947
+ const relative = path9.relative(root, target);
23336
23948
  let current = root;
23337
- for (const segment of relative.split(path8.sep)) {
23949
+ for (const segment of relative.split(path9.sep)) {
23338
23950
  if (!segment)
23339
23951
  continue;
23340
- current = path8.join(current, segment);
23952
+ current = path9.join(current, segment);
23341
23953
  const stat = fs6.lstatSync(current);
23342
23954
  const real = fs6.realpathSync(current);
23343
- if (real !== root && !real.startsWith(`${root}${path8.sep}`))
23955
+ if (real !== root && !real.startsWith(`${root}${path9.sep}`))
23344
23956
  return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23345
23957
  fields: [
23346
23958
  {
@@ -23418,7 +24030,7 @@ var validateAction = (store, task, workspaceId, input, authority) => {
23418
24030
  operation = null;
23419
24031
  }
23420
24032
  const cls = operationAutoClass(operation);
23421
- if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
24033
+ if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls, undefined, decision.binding.approvedContent)))
23422
24034
  return failure("permission_denied", "standing auto-approval is not live for this operation");
23423
24035
  }
23424
24036
  if (decision.revoked)
@@ -23720,15 +24332,15 @@ var bindingCovers = scopeCovers;
23720
24332
  var verifyDecisionContent = verifyContentRefs;
23721
24333
 
23722
24334
  // 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("/") || ".";
24335
+ var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
24336
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
24337
+ var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
23726
24338
  var canonicalPath = (value) => {
23727
24339
  if (typeof value !== "string" || !value)
23728
24340
  return null;
23729
24341
  if (value.includes("\\") || value.split("/").includes(".."))
23730
24342
  return null;
23731
- const normalized = path9.posix.normalize(value);
24343
+ const normalized = path10.posix.normalize(value);
23732
24344
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
23733
24345
  return null;
23734
24346
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -23776,14 +24388,14 @@ var gitPaths = (root) => {
23776
24388
  git: /not a git repository/i.test(stderr) ? false : true
23777
24389
  };
23778
24390
  }
23779
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")) : [];
24391
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
23780
24392
  const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
23781
24393
  if (stagedDeleted.status !== 0) {
23782
24394
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
23783
24395
  if (!/not a git repository/i.test(stderr))
23784
24396
  return { paths, uncertain: true, git: true };
23785
24397
  } else if (stagedDeleted.stdout) {
23786
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")));
24398
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")));
23787
24399
  }
23788
24400
  return { paths: [...new Set(paths)], uncertain: false, git: true };
23789
24401
  };
@@ -23798,7 +24410,7 @@ var walk = (root, directory, output) => {
23798
24410
  for (const name of names) {
23799
24411
  if (name === ".git" || name === ".workit")
23800
24412
  continue;
23801
- const target = path9.join(directory, name);
24413
+ const target = path10.join(directory, name);
23802
24414
  let stat;
23803
24415
  try {
23804
24416
  stat = fs7.lstatSync(target);
@@ -23816,12 +24428,12 @@ var walk = (root, directory, output) => {
23816
24428
  var scopeRoots = (root, scope) => {
23817
24429
  const roots = [];
23818
24430
  for (const item of scope.paths.length ? scope.paths : ["."]) {
23819
- const target = path9.resolve(root, item);
24431
+ const target = path10.resolve(root, item);
23820
24432
  if (!inside(root, target))
23821
24433
  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
24434
  let ancestor = target;
23823
24435
  while (!fs7.existsSync(ancestor) && ancestor !== root)
23824
- ancestor = path9.dirname(ancestor);
24436
+ ancestor = path10.dirname(ancestor);
23825
24437
  try {
23826
24438
  if (!inside(root, fs7.realpathSync(ancestor)))
23827
24439
  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 +24485,7 @@ function captureCandidate(root, scope, environment = []) {
23873
24485
  for (const item of [...names].sort()) {
23874
24486
  if (!scopeMatches(item, normalizedScope))
23875
24487
  continue;
23876
- const target = path9.join(checkout, item);
24488
+ const target = path10.join(checkout, item);
23877
24489
  try {
23878
24490
  const stat = fs7.lstatSync(target);
23879
24491
  if (stat.isSymbolicLink()) {
@@ -24255,7 +24867,8 @@ var externalActionSchema = discriminatedUnion2("operation", [
24255
24867
  action: _enum2(["setup", "reapply_stash"]).optional(),
24256
24868
  sdd_dir: string5().optional(),
24257
24869
  target_branch: string5().optional(),
24258
- stash: _enum2(["yes", "no"]).optional()
24870
+ stash: _enum2(["yes", "no"]).optional(),
24871
+ cwd: string5().min(1).optional()
24259
24872
  }).strict()
24260
24873
  }).strict(),
24261
24874
  object4({
@@ -24263,12 +24876,13 @@ var externalActionSchema = discriminatedUnion2("operation", [
24263
24876
  payload: object4({
24264
24877
  message: string5().min(1).optional(),
24265
24878
  plan_steps: array3(chainStepSchema).min(1).max(32).optional(),
24266
- plan_branch: string5().min(1).optional()
24879
+ plan_branch: string5().min(1).optional(),
24880
+ cwd: string5().min(1).optional()
24267
24881
  }).strict()
24268
24882
  }).strict(),
24269
24883
  object4({
24270
24884
  operation: literal3("git.push"),
24271
- payload: object4({ branch: string5().min(1).optional() }).strict()
24885
+ payload: object4({ branch: string5().min(1).optional(), cwd: string5().min(1).optional() }).strict()
24272
24886
  }).strict(),
24273
24887
  object4({
24274
24888
  operation: literal3("hosting.pull_request"),
@@ -24277,16 +24891,22 @@ var externalActionSchema = discriminatedUnion2("operation", [
24277
24891
  body: string5().optional(),
24278
24892
  draft: boolean5().optional(),
24279
24893
  target_branch: string5().optional(),
24280
- babysit: boolean5().optional()
24894
+ babysit: boolean5().optional(),
24895
+ cwd: string5().min(1).optional()
24281
24896
  }).strict()
24282
24897
  }).strict(),
24283
24898
  object4({
24284
24899
  operation: literal3("hosting.merge"),
24285
24900
  payload: object4({
24286
24901
  target_branch: string5().min(1).optional(),
24287
- source_branch: string5().min(1).optional()
24902
+ source_branch: string5().min(1).optional(),
24903
+ cwd: string5().min(1).optional()
24288
24904
  }).strict()
24289
24905
  }).strict(),
24906
+ object4({
24907
+ operation: literal3("hosting.delete_branch"),
24908
+ payload: object4({ branch: string5().min(1), cwd: string5().min(1).optional() }).strict()
24909
+ }).strict(),
24290
24910
  object4({
24291
24911
  operation: literal3("youtrack.update"),
24292
24912
  payload: object4({
@@ -24342,12 +24962,13 @@ var externalActionSchema = discriminatedUnion2("operation", [
24342
24962
  issueRef: string5().optional(),
24343
24963
  mode: string5().optional(),
24344
24964
  specPath: string5().optional(),
24345
- planPath: string5().optional()
24965
+ planPath: string5().optional(),
24966
+ cwd: string5().min(1).optional()
24346
24967
  }).strict()
24347
24968
  }).strict()
24348
24969
  ]);
24349
24970
  // packages/workit-core/src/core/task-engine.ts
24350
- import { realpathSync as realpathSync5 } from "node:fs";
24971
+ import { realpathSync as realpathSync6 } from "node:fs";
24351
24972
 
24352
24973
  // packages/workit-core/src/core/policy-resolver.ts
24353
24974
  var resolverInputSchema = object4({
@@ -24520,6 +25141,7 @@ function resolveRequirements(input) {
24520
25141
  dependentAction: null,
24521
25142
  acceptanceAllowed: false
24522
25143
  }));
25144
+ const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
24523
25145
  if (signals.behaviorChange.value === true)
24524
25146
  requirements.push(requirement({
24525
25147
  ruleId: "behavioral-verification",
@@ -24531,14 +25153,14 @@ function resolveRequirements(input) {
24531
25153
  dependentAction: null,
24532
25154
  acceptanceAllowed: false
24533
25155
  }), requirement({
24534
- ruleId: "fresh-context-review",
25156
+ ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
24535
25157
  dimension: "review",
24536
25158
  scope,
24537
- reason: "A non-mechanical behavior change requires fresh-context review.",
24538
- satisfaction: "A separate review context examines the current candidate and records its result.",
25159
+ reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
25160
+ satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
24539
25161
  before: "close",
24540
25162
  dependentAction: null,
24541
- acceptanceAllowed: false
25163
+ acceptanceAllowed: !independentBehaviorReview
24542
25164
  }));
24543
25165
  else if (signals.mechanicalLowRisk.value === true)
24544
25166
  requirements.push(requirement({
@@ -24892,6 +25514,18 @@ var portableTask = (task) => {
24892
25514
  };
24893
25515
  for (const entry of clone.assessments)
24894
25516
  demote(entry.data.facts, entry.data.signals, entry.data.consequences);
25517
+ const sourceDecisions = new Map(task.decisions.map((entry) => [entry.id, entry]));
25518
+ for (const entry of clone.decisions) {
25519
+ const consumption = sourceDecisions.get(entry.id)?.data.consumption;
25520
+ if (!consumption)
25521
+ continue;
25522
+ entry.data.consumption = {
25523
+ ...consumption,
25524
+ state: consumption.state === "reserved" ? "uncertain" : consumption.state,
25525
+ actionRef: { kind: "record", collection: "decisions", id: entry.id },
25526
+ ...consumption.historicalActionId ? { historicalActionId: consumption.historicalActionId } : consumption.actionRef.kind === "host" ? { historicalActionId: consumption.actionRef.handle } : {}
25527
+ };
25528
+ }
24895
25529
  clone.candidates = [];
24896
25530
  return clone;
24897
25531
  };
@@ -25017,7 +25651,11 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
25017
25651
  taskId: "",
25018
25652
  workspaceId: destinationWorkspaceId
25019
25653
  },
25020
- consumption: null
25654
+ consumption: data.consumption ? {
25655
+ ...data.consumption,
25656
+ state: data.consumption.state === "reserved" ? "uncertain" : data.consumption.state,
25657
+ actionRef: { kind: "record", collection: "decisions", id: entry.id }
25658
+ } : null
25021
25659
  }
25022
25660
  };
25023
25661
  }),
@@ -25058,7 +25696,7 @@ class WorkitCore {
25058
25696
  }
25059
25697
  contextRootError() {
25060
25698
  try {
25061
- if (realpathSync5(this.context.root) !== this.store.root)
25699
+ if (realpathSync6(this.context.root) !== this.store.root)
25062
25700
  return failure("invalid_input", "operation context root does not match the task store root");
25063
25701
  } catch {
25064
25702
  return failure("invalid_input", "operation context root cannot be resolved");
@@ -25217,7 +25855,7 @@ class WorkitCore {
25217
25855
  return parsed;
25218
25856
  const input = parsed.data;
25219
25857
  if ((this.context.workerId ?? null) !== null) {
25220
- if (input.action !== "inspect")
25858
+ if (input.action !== "inspect" && input.action !== "list")
25221
25859
  return failure("permission_denied", "helpers cannot control task lifecycle or scope");
25222
25860
  }
25223
25861
  switch (input.action) {
@@ -25244,7 +25882,18 @@ class WorkitCore {
25244
25882
  return listed;
25245
25883
  const status = input.status ?? "open";
25246
25884
  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);
25885
+ const query = input.query?.toLowerCase().split(/\s+/);
25886
+ 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({
25887
+ id: task.id,
25888
+ workspaceId: task.workspaceId,
25889
+ origin: task.origin,
25890
+ createdAt: task.createdAt,
25891
+ updatedAt: task.updatedAt,
25892
+ source: task.intent.provenance,
25893
+ objective: task.intent.data.objective,
25894
+ progress: task.progress,
25895
+ decisions: task.decisions.map(({ data }) => data)
25896
+ }).toLowerCase().includes(term))).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, limit);
25248
25897
  if (selected.length === 0)
25249
25898
  return success(null, null, []);
25250
25899
  const workspace = this.store.readWorkspace();
@@ -25263,7 +25912,11 @@ class WorkitCore {
25263
25912
  status: task.status,
25264
25913
  closure: task.closure,
25265
25914
  progress: task.progress,
25266
- writer: task.status === "closed" ? null : workspace.data.writer
25915
+ writer: task.status === "closed" ? null : workspace.data.writer,
25916
+ source: {
25917
+ host: task.intent.provenance.host,
25918
+ kind: task.intent.provenance.kind
25919
+ }
25267
25920
  }));
25268
25921
  return success(null, null, tasks);
25269
25922
  }
@@ -26359,7 +27012,7 @@ class WorkitCore {
26359
27012
  if (view.task.workspaceId !== view.workspace.id)
26360
27013
  return failure("invalid_input", "task and workspace bindings are invalid");
26361
27014
  try {
26362
- if (realpathSync5(view.workspace.root) !== this.store.root)
27015
+ if (realpathSync6(view.workspace.root) !== this.store.root)
26363
27016
  return failure("invalid_input", "task view root does not match the task store");
26364
27017
  } catch {
26365
27018
  return failure("invalid_input", "task view root cannot be resolved");
@@ -26636,22 +27289,22 @@ function resolveGitRevision(root, value) {
26636
27289
  }
26637
27290
  }
26638
27291
  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)) {
27292
+ const base = realpathSync7(root);
27293
+ const target = path11.resolve(base, candidate);
27294
+ if (target !== base && !target.startsWith(base + path11.sep)) {
26642
27295
  throw new Error("path must stay inside repository root");
26643
27296
  }
26644
27297
  let ancestor = target;
26645
27298
  while (!existsSync5(ancestor))
26646
- ancestor = path10.dirname(ancestor);
26647
- const canonicalAncestor = realpathSync6(ancestor);
26648
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path10.sep)) {
27299
+ ancestor = path11.dirname(ancestor);
27300
+ const canonicalAncestor = realpathSync7(ancestor);
27301
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path11.sep)) {
26649
27302
  throw new Error("path must stay inside repository root");
26650
27303
  }
26651
27304
  return target;
26652
27305
  }
26653
27306
  function run2(root, executable, args, env = {}) {
26654
- const cwd = realpathSync6(root);
27307
+ const cwd = realpathSync7(root);
26655
27308
  const result = spawnSync3(executable, args, {
26656
27309
  cwd,
26657
27310
  encoding: "utf8",
@@ -26724,18 +27377,19 @@ var redactSecrets = (value) => applyPatterns(value);
26724
27377
 
26725
27378
  // packages/workit-core/src/core/pr-create.ts
26726
27379
  import fs8 from "node:fs";
26727
- import path11 from "node:path";
27380
+ import path12 from "node:path";
27381
+ import { spawnSync as spawnSync4 } from "node:child_process";
26728
27382
  function parseGhIssue(value) {
26729
27383
  const m = /issues\/(\d+)/.exec(value);
26730
27384
  return m ? m[1] : String(value).trim().replace(/^#/, "");
26731
27385
  }
26732
27386
  function whichOnPath(tool) {
26733
27387
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
26734
- for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
27388
+ for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
26735
27389
  if (!dir)
26736
27390
  continue;
26737
27391
  for (const name of names) {
26738
- const candidate = path11.join(dir, name);
27392
+ const candidate = path12.join(dir, name);
26739
27393
  try {
26740
27394
  fs8.accessSync(candidate, fs8.constants.X_OK);
26741
27395
  return candidate;
@@ -26745,11 +27399,21 @@ function whichOnPath(tool) {
26745
27399
  return null;
26746
27400
  }
26747
27401
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
27402
+ var pushRemote = (cwd) => {
27403
+ const result = spawnSync4("git", ["remote", "get-url", "--push", "--all", "origin"], {
27404
+ cwd,
27405
+ encoding: "utf8"
27406
+ });
27407
+ if (result.status !== 0)
27408
+ return null;
27409
+ const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
27410
+ return urls.length === 1 ? urls[0] : null;
27411
+ };
26748
27412
 
26749
27413
  // packages/workit-core/src/core/repo-context.ts
26750
- import { spawnSync as spawnSync4 } from "node:child_process";
27414
+ import { spawnSync as spawnSync5 } from "node:child_process";
26751
27415
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
26752
- import path12 from "node:path";
27416
+ import path13 from "node:path";
26753
27417
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
26754
27418
  try {
26755
27419
  gitRevisionParts(value);
@@ -26776,7 +27440,7 @@ var invalidRange = (cwd) => ({
26776
27440
  cwd
26777
27441
  });
26778
27442
  var runGit = (cwd, args) => {
26779
- const result = spawnSync4("git", args, { cwd, encoding: "utf8" });
27443
+ const result = spawnSync5("git", args, { cwd, encoding: "utf8" });
26780
27444
  return {
26781
27445
  stdout: (result.stdout ?? "").trimEnd(),
26782
27446
  stderr: (result.stderr ?? "").trimEnd(),
@@ -26883,7 +27547,7 @@ var findPrTemplate = (cwd) => {
26883
27547
  ]) {
26884
27548
  const parts = rel.split("/");
26885
27549
  const base = parts[parts.length - 1];
26886
- const dir = parts.length > 1 ? path12.join(cwd, ...parts.slice(0, -1)) : cwd;
27550
+ const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
26887
27551
  const actual = probeCaseInsensitive(dir, base);
26888
27552
  if (actual)
26889
27553
  return [...parts.slice(0, -1), actual].join("/");
@@ -26914,7 +27578,7 @@ var diffStatForRange = (cwd, range) => {
26914
27578
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
26915
27579
  };
26916
27580
  var packageScripts = (cwd, keys) => {
26917
- const pkgPath = path12.join(cwd, "package.json");
27581
+ const pkgPath = path13.join(cwd, "package.json");
26918
27582
  if (!existsSync6(pkgPath))
26919
27583
  return [];
26920
27584
  try {
@@ -26940,9 +27604,9 @@ var documentationFiles = (cwd) => {
26940
27604
  if (entry.isDirectory()) {
26941
27605
  if (excluded.has(entry.name))
26942
27606
  continue;
26943
- walk(path12.join(dir, entry.name), depth + 1);
27607
+ walk(path13.join(dir, entry.name), depth + 1);
26944
27608
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
26945
- matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
27609
+ matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
26946
27610
  }
26947
27611
  }
26948
27612
  };
@@ -26958,7 +27622,7 @@ var readTrimmed = (file, maxLines) => {
26958
27622
  `);
26959
27623
  };
26960
27624
  function prReadyContext(root, range) {
26961
- const cwd = path12.resolve(repoRoot(root));
27625
+ const cwd = path13.resolve(repoRoot(root));
26962
27626
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26963
27627
  return invalidRange(cwd);
26964
27628
  let stdout = "";
@@ -27016,7 +27680,7 @@ ${status.stderr}` : "");
27016
27680
  stdout += `template_path: ${template}
27017
27681
 
27018
27682
  `;
27019
- stdout += readTrimmed(path12.join(cwd, template), 220);
27683
+ stdout += readTrimmed(path13.join(cwd, template), 220);
27020
27684
  } else {
27021
27685
  stdout += `template_path: none
27022
27686
 
@@ -27026,7 +27690,7 @@ ${status.stderr}` : "");
27026
27690
  stdout += `
27027
27691
  `;
27028
27692
  stdout += printSection("Recent Validation Signals");
27029
- if (existsSync6(path12.join(cwd, "package.json"))) {
27693
+ if (existsSync6(path13.join(cwd, "package.json"))) {
27030
27694
  stdout += `package.json detected. Common scripts:
27031
27695
  `;
27032
27696
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -27035,7 +27699,7 @@ ${status.stderr}` : "");
27035
27699
  `) + `
27036
27700
  `;
27037
27701
  }
27038
- if (existsSync6(path12.join(cwd, "Cargo.toml")) || existsSync6(path12.join(cwd, "src-tauri/Cargo.toml"))) {
27702
+ if (existsSync6(path13.join(cwd, "Cargo.toml")) || existsSync6(path13.join(cwd, "src-tauri/Cargo.toml"))) {
27039
27703
  stdout += `Rust project detected.
27040
27704
  `;
27041
27705
  }
@@ -27068,7 +27732,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
27068
27732
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
27069
27733
  - If Unreleased already has duplicate category headings, normalize_only first.`;
27070
27734
  function changelogContext(root, range) {
27071
- const cwd = path12.resolve(repoRoot(root));
27735
+ const cwd = path13.resolve(repoRoot(root));
27072
27736
  if (range !== undefined && !isResolvableContextRange(cwd, range))
27073
27737
  return invalidRange(cwd);
27074
27738
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -27086,7 +27750,7 @@ function changelogContext(root, range) {
27086
27750
  stdout += CHANGELOG_RULES + `
27087
27751
  `;
27088
27752
  stdout += printSection("Existing CHANGELOG.md");
27089
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
27753
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
27090
27754
  if (existsSync6(changelogPath)) {
27091
27755
  stdout += readTrimmed(changelogPath, 260) + `
27092
27756
  `;
@@ -27106,7 +27770,7 @@ function changelogContext(root, range) {
27106
27770
  return { stdout, stderr: "", exitCode: 0, cwd };
27107
27771
  }
27108
27772
  function docsRefreshContext(root, range) {
27109
- const cwd = path12.resolve(repoRoot(root));
27773
+ const cwd = path13.resolve(repoRoot(root));
27110
27774
  if (range !== undefined && !isResolvableContextRange(cwd, range))
27111
27775
  return invalidRange(cwd);
27112
27776
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -27128,7 +27792,7 @@ function docsRefreshContext(root, range) {
27128
27792
  `) + `
27129
27793
  `;
27130
27794
  stdout += printSection("README Preview");
27131
- const readme = path12.join(cwd, "README.md");
27795
+ const readme = path13.join(cwd, "README.md");
27132
27796
  if (existsSync6(readme)) {
27133
27797
  stdout += readTrimmed(readme, 220) + `
27134
27798
  `;
@@ -27137,7 +27801,7 @@ function docsRefreshContext(root, range) {
27137
27801
  `;
27138
27802
  }
27139
27803
  stdout += printSection("Package Scripts");
27140
- const pkgPath = path12.join(cwd, "package.json");
27804
+ const pkgPath = path13.join(cwd, "package.json");
27141
27805
  if (existsSync6(pkgPath)) {
27142
27806
  try {
27143
27807
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
@@ -27151,7 +27815,7 @@ function docsRefreshContext(root, range) {
27151
27815
  return { stdout, stderr: "", exitCode: 0, cwd };
27152
27816
  }
27153
27817
  function releaseNotesContext(root, rangeOrTag) {
27154
- const cwd = path12.resolve(repoRoot(root));
27818
+ const cwd = path13.resolve(repoRoot(root));
27155
27819
  if (!rangeOrTag) {
27156
27820
  return { stdout: "", stderr: `ERROR: release tag or range required
27157
27821
  `, exitCode: 1, cwd };
@@ -27189,7 +27853,7 @@ function releaseNotesContext(root, rangeOrTag) {
27189
27853
  `;
27190
27854
  stdout += printSection("Existing Release Files");
27191
27855
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
27192
- if (existsSync6(path12.join(cwd, rel)))
27856
+ if (existsSync6(path13.join(cwd, rel)))
27193
27857
  stdout += rel + `
27194
27858
  `;
27195
27859
  }
@@ -27217,19 +27881,7 @@ function releaseNotesContext(root, rangeOrTag) {
27217
27881
  }
27218
27882
 
27219
27883
  // 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
- };
27884
+ import { spawnSync as spawnSync6 } from "node:child_process";
27233
27885
  var repoPathFromRemote = (remote) => {
27234
27886
  let rest = (remote || "").trim().replace(/\/+$/, "");
27235
27887
  if (!rest)
@@ -27250,7 +27902,11 @@ var defaultCli = (bin, available) => {
27250
27902
  if (!available)
27251
27903
  return null;
27252
27904
  return (args, root) => {
27253
- const out = spawnSync5(bin, args, { cwd: root, encoding: "utf8" });
27905
+ const out = spawnSync6(bin, args, {
27906
+ cwd: root,
27907
+ encoding: "utf8",
27908
+ env: { ...process.env, PATH: process.env.PATH ?? "" }
27909
+ });
27254
27910
  return Promise.resolve({
27255
27911
  status: out.status ?? 1,
27256
27912
  stdout: out.stdout ?? "",
@@ -27258,49 +27914,22 @@ var defaultCli = (bin, available) => {
27258
27914
  });
27259
27915
  };
27260
27916
  };
27261
- var tryCliTriple = async (cli, args, root, id, idKeys, bodyKeys) => {
27917
+ var tryCliTriple = async (cli, name, args, root, id, idKeys, bodyKeys) => {
27262
27918
  if (!cli)
27263
- return null;
27919
+ return { error: `${name} CLI is unavailable`, kind: "creds" };
27264
27920
  const run = cli(args, root);
27265
27921
  if (!run)
27266
- return null;
27922
+ return { error: `${name} CLI is unavailable`, kind: "creds" };
27267
27923
  const out = await run;
27268
27924
  if (out.status !== 0)
27269
- return null;
27925
+ return { error: `${name} CLI is not authenticated or the issue is unavailable`, kind: "creds" };
27270
27926
  try {
27271
- return parseTriple(JSON.parse(out.stdout), id, idKeys, bodyKeys);
27927
+ const triple = parseTriple(JSON.parse(out.stdout), id, idKeys, bodyKeys);
27928
+ return triple ? { data: triple } : { error: `unexpected ${name} issue response`, kind: "request" };
27272
27929
  } catch {
27273
- return null;
27930
+ return { error: `invalid ${name} issue response`, kind: "request" };
27274
27931
  }
27275
27932
  };
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
27933
  var parseTriple = (parsed, id, idKeys, bodyKeys) => {
27305
27934
  const title = parsed.title;
27306
27935
  if (typeof title !== "string")
@@ -27323,62 +27952,36 @@ var fetchGitHubIssueBody = async (ref, root, deps = {}) => {
27323
27952
  const id = parseGhIssue(ref);
27324
27953
  if (!/^\d+$/.test(id))
27325
27954
  return { error: `invalid GitHub issue ref "${ref}"`, kind: "input" };
27326
- const repo = repoPathFromRemote((deps.remote ?? originRemote)(root) ?? "");
27955
+ const remote = (deps.remote ?? pushRemote)(root);
27956
+ const repo = repoPathFromRemote(remote ?? "");
27327
27957
  if (!repo)
27328
27958
  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
- }
27959
+ const cfg = vcsConfig("load", root);
27960
+ const host = (deps.host ?? (cfg.ok && cfg.provider === "github" ? cfg.github?.host : null) ?? "github.com").toLowerCase();
27961
+ if (!remote || !hostingApiHostMatches(remote, host))
27962
+ return { error: "hosting API host does not match the push destination", kind: "input" };
27963
+ const repoArg = host === "github.com" ? repo : `${host}/${repo}`;
27964
+ return tryCliTriple(deps.cli ?? defaultCli("gh", hostingCliAvailable("github")), "gh", ["issue", "view", id, "--repo", repoArg, "--json", "number,title,body,state"], root, id, ["number"], ["body"]);
27352
27965
  };
27353
27966
  var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
27354
27967
  const id = parseGhIssue(ref);
27355
27968
  if (!/^\d+$/.test(id))
27356
27969
  return { error: `invalid GitLab issue ref "${ref}"`, kind: "input" };
27357
- const repo = repoPathFromRemote((deps.remote ?? originRemote)(root) ?? "");
27970
+ const remote = (deps.remote ?? pushRemote)(root);
27971
+ const repo = repoPathFromRemote(remote ?? "");
27358
27972
  if (!repo)
27359
27973
  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
- }
27974
+ const cfg = vcsConfig("load", root);
27975
+ const host = (deps.host ?? (cfg.ok && cfg.provider === "gitlab" ? cfg.gitlab?.host : null) ?? "gitlab.com").toLowerCase();
27976
+ if (!remote || !hostingApiHostMatches(remote, host))
27977
+ return { error: "hosting API host does not match the push destination", kind: "input" };
27978
+ const repoArg = host === "gitlab.com" ? repo : `https://${host}/${repo}`;
27979
+ return tryCliTriple(deps.cli ?? defaultCli("glab", hostingCliAvailable("gitlab")), "glab", ["issue", "view", id, "-R", repoArg, "-F", "json"], root, id, ["iid"], ["description"]);
27377
27980
  };
27378
27981
 
27379
27982
  // packages/workit-core/src/core/youtrack.ts
27380
- import fs10 from "node:fs";
27381
- import path13 from "node:path";
27983
+ import fs9 from "node:fs";
27984
+ import path14 from "node:path";
27382
27985
 
27383
27986
  // packages/workit-core/src/core/templates.ts
27384
27987
  var repoRoot2 = assetRoot();
@@ -27390,23 +27993,23 @@ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
27390
27993
 
27391
27994
  // packages/workit-core/src/core/youtrack.ts
27392
27995
  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");
27996
+ var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
27997
+ var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path14.join(configDir(), "youtrack.json");
27998
+ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path14.join(resolveConfigDir(), "youtrack.json");
27396
27999
  var youTrackTokenModeOk = (p) => {
27397
28000
  if (process.platform === "win32")
27398
28001
  return true;
27399
- const mode = fs10.statSync(p).mode & 511;
28002
+ const mode = fs9.statSync(p).mode & 511;
27400
28003
  return mode === 384;
27401
28004
  };
27402
28005
  function readYouTrackConfig(required) {
27403
28006
  const cfgPath = youTrackConfigPath();
27404
- if (!fs10.existsSync(cfgPath)) {
28007
+ if (!fs9.existsSync(cfgPath)) {
27405
28008
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
27406
28009
  }
27407
28010
  let parsed;
27408
28011
  try {
27409
- parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
28012
+ parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
27410
28013
  } catch {
27411
28014
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
27412
28015
  }
@@ -27417,11 +28020,11 @@ function readYouTrackConfig(required) {
27417
28020
  }
27418
28021
  var readYouTrackContextConfig = () => {
27419
28022
  const cfgPath = youTrackReadOnlyConfigPath();
27420
- if (!fs10.existsSync(cfgPath))
28023
+ if (!fs9.existsSync(cfgPath))
27421
28024
  return { data: {} };
27422
28025
  let parsed;
27423
28026
  try {
27424
- parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
28027
+ parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
27425
28028
  } catch {
27426
28029
  return { error: "YouTrack configuration is unavailable" };
27427
28030
  }
@@ -27446,12 +28049,12 @@ function youTrackGreeting(configOverride) {
27446
28049
  const cfgPath = configOverride ?? youTrackConfigPath();
27447
28050
  let parsed;
27448
28051
  try {
27449
- parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
28052
+ parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
27450
28053
  } catch {
27451
28054
  return {
27452
28055
  stdout: "",
27453
28056
  exitCode: 1,
27454
- stderr: fs10.existsSync(cfgPath) ? `${cfgPath} is not valid JSON` : `missing youtrack.json: ${cfgPath}`
28057
+ stderr: fs9.existsSync(cfgPath) ? `${cfgPath} is not valid JSON` : `missing youtrack.json: ${cfgPath}`
27455
28058
  };
27456
28059
  }
27457
28060
  if (!isConfigObject(parsed)) {
@@ -27497,15 +28100,15 @@ var youTrackToken = () => {
27497
28100
  if ("error" in loaded)
27498
28101
  return loaded;
27499
28102
  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))
28103
+ const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
28104
+ if (!tokenPath || !fs9.existsSync(tokenPath))
27502
28105
  return { error: "missing youtrack.token" };
27503
28106
  if (!youTrackTokenModeOk(tokenPath))
27504
28107
  return { error: "youtrack.token mode must be 0600" };
27505
- const token = fs10.readFileSync(tokenPath, "utf8").trim();
28108
+ const token = fs9.readFileSync(tokenPath, "utf8").trim();
27506
28109
  if (!token)
27507
28110
  return { error: "empty token file" };
27508
- if (token === TOKEN_PLACEHOLDER2 || token.startsWith(TOKEN_PLACEHOLDER2)) {
28111
+ if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
27509
28112
  return {
27510
28113
  error: "token file still has placeholder YOUR_TOKEN_HERE — edit the file locally, then run /wk-status"
27511
28114
  };
@@ -27568,9 +28171,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
27568
28171
  } catch {
27569
28172
  continue;
27570
28173
  }
27571
- if (!fs10.existsSync(full))
28174
+ if (!fs9.existsSync(full))
27572
28175
  continue;
27573
- const text = fs10.readFileSync(full, "utf8");
28176
+ const text = fs9.readFileSync(full, "utf8");
27574
28177
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
27575
28178
  if (m)
27576
28179
  return m[1];
@@ -27804,6 +28407,7 @@ var readExternalContext = async (root, payload) => {
27804
28407
  }
27805
28408
  }
27806
28409
  };
28410
+ var targetWriterActionLease = Symbol("target-writer-action-lease");
27807
28411
 
27808
28412
  // packages/workit-mcp/src/server.ts
27809
28413
  class McpCapabilityUnavailableError extends Error {
@@ -27848,13 +28452,13 @@ var rootVariants = (workspaceRoot) => {
27848
28452
  if (!workspaceRoot)
27849
28453
  return [];
27850
28454
  const roots = new Set;
27851
- for (const candidate of [workspaceRoot, path14.resolve(workspaceRoot)]) {
27852
- if (path14.isAbsolute(candidate) && candidate !== path14.parse(candidate).root)
28455
+ for (const candidate of [workspaceRoot, path15.resolve(workspaceRoot)]) {
28456
+ if (path15.isAbsolute(candidate) && candidate !== path15.parse(candidate).root)
27853
28457
  roots.add(candidate);
27854
28458
  }
27855
28459
  try {
27856
- const canonical = realpathSync7(workspaceRoot);
27857
- if (canonical !== path14.parse(canonical).root)
28460
+ const canonical = realpathSync8(workspaceRoot);
28461
+ if (canonical !== path15.parse(canonical).root)
27858
28462
  roots.add(canonical);
27859
28463
  } catch {}
27860
28464
  return [...roots].sort((left, right) => right.length - left.length);
@@ -27955,11 +28559,11 @@ var thrownResult = (tool, error, workspaceRoot) => {
27955
28559
  };
27956
28560
  function createMcpServer(host, contextProvider) {
27957
28561
  assertMcpHost(host);
27958
- const here = path14.dirname(fileURLToPath2(import.meta.url));
28562
+ const here = path15.dirname(fileURLToPath2(import.meta.url));
27959
28563
  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")
28564
+ path15.join(here, "server.ts"),
28565
+ path15.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
28566
+ path15.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
27963
28567
  ].filter((file) => existsSync7(file)));
27964
28568
  let staleWarned = false;
27965
28569
  const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
@@ -28139,8 +28743,8 @@ var main = async () => {
28139
28743
  process.exitCode = 2;
28140
28744
  }
28141
28745
  };
28142
- var entryPath = process.argv[1] ? path15.resolve(process.argv[1]) : "";
28143
- if (entryPath === path15.resolve(fileURLToPath3(import.meta.url)))
28746
+ var entryPath = process.argv[1] ? path16.resolve(process.argv[1]) : "";
28747
+ if (entryPath === path16.resolve(fileURLToPath3(import.meta.url)))
28144
28748
  await main();
28145
28749
  export {
28146
28750
  McpCapabilityUnavailableError,