@nail00749/agent-gvozd 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -987,13 +987,12 @@ ${b}
987
987
  var i = `${styleText("gray", S_BAR)} `;
988
988
 
989
989
  // src/cli.ts
990
- import { realpathSync as realpathSync3 } from "node:fs";
990
+ import { realpathSync as realpathSync5 } from "node:fs";
991
991
  import { fileURLToPath as fileURLToPath2 } from "node:url";
992
992
 
993
993
  // src/config.ts
994
- import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
995
- import { homedir } from "node:os";
996
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
994
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
995
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
997
996
  import { fileURLToPath } from "node:url";
998
997
 
999
998
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -2463,6 +2462,14 @@ function cleanRegex(source) {
2463
2462
  const end = source.endsWith("$") ? source.length - 1 : source.length;
2464
2463
  return source.slice(start, end);
2465
2464
  }
2465
+ function floatSafeRemainder(val, step) {
2466
+ const ratio = val / step;
2467
+ const roundedRatio = Math.round(ratio);
2468
+ const tolerance = Number.EPSILON * Math.max(Math.abs(ratio), 1);
2469
+ if (Math.abs(ratio - roundedRatio) < tolerance)
2470
+ return 0;
2471
+ return ratio - roundedRatio;
2472
+ }
2466
2473
  var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
2467
2474
  function defineLazy(object, key, getter) {
2468
2475
  let value = undefined;
@@ -3021,6 +3028,7 @@ var string = (params) => {
3021
3028
  const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
3022
3029
  return new RegExp(`^${regex}$`);
3023
3030
  };
3031
+ var integer = /^-?\d+$/;
3024
3032
  var number = /^-?\d+(?:\.\d+)?$/;
3025
3033
  var boolean = /^(?:true|false)$/i;
3026
3034
  var lowercase = /^[^A-Z]*$/;
@@ -3033,6 +3041,168 @@ var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => {
3033
3041
  inst._zod.def = def;
3034
3042
  (_a = inst._zod).onattach ?? (_a.onattach = []);
3035
3043
  });
3044
+ var numericOriginMap = {
3045
+ number: "number",
3046
+ bigint: "bigint",
3047
+ object: "date"
3048
+ };
3049
+ var $ZodCheckLessThan = /* @__PURE__ */ $constructor("$ZodCheckLessThan", (inst, def) => {
3050
+ $ZodCheck.init(inst, def);
3051
+ const origin = numericOriginMap[typeof def.value];
3052
+ inst._zod.onattach.push((inst) => {
3053
+ const bag = inst._zod.bag;
3054
+ const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY;
3055
+ if (def.value < curr) {
3056
+ if (def.inclusive)
3057
+ bag.maximum = def.value;
3058
+ else
3059
+ bag.exclusiveMaximum = def.value;
3060
+ }
3061
+ });
3062
+ inst._zod.check = (payload) => {
3063
+ if (def.inclusive ? payload.value <= def.value : payload.value < def.value) {
3064
+ return;
3065
+ }
3066
+ payload.issues.push({
3067
+ origin,
3068
+ code: "too_big",
3069
+ maximum: typeof def.value === "object" ? def.value.getTime() : def.value,
3070
+ input: payload.value,
3071
+ inclusive: def.inclusive,
3072
+ inst,
3073
+ continue: !def.abort
3074
+ });
3075
+ };
3076
+ });
3077
+ var $ZodCheckGreaterThan = /* @__PURE__ */ $constructor("$ZodCheckGreaterThan", (inst, def) => {
3078
+ $ZodCheck.init(inst, def);
3079
+ const origin = numericOriginMap[typeof def.value];
3080
+ inst._zod.onattach.push((inst) => {
3081
+ const bag = inst._zod.bag;
3082
+ const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY;
3083
+ if (def.value > curr) {
3084
+ if (def.inclusive)
3085
+ bag.minimum = def.value;
3086
+ else
3087
+ bag.exclusiveMinimum = def.value;
3088
+ }
3089
+ });
3090
+ inst._zod.check = (payload) => {
3091
+ if (def.inclusive ? payload.value >= def.value : payload.value > def.value) {
3092
+ return;
3093
+ }
3094
+ payload.issues.push({
3095
+ origin,
3096
+ code: "too_small",
3097
+ minimum: typeof def.value === "object" ? def.value.getTime() : def.value,
3098
+ input: payload.value,
3099
+ inclusive: def.inclusive,
3100
+ inst,
3101
+ continue: !def.abort
3102
+ });
3103
+ };
3104
+ });
3105
+ var $ZodCheckMultipleOf = /* @__PURE__ */ $constructor("$ZodCheckMultipleOf", (inst, def) => {
3106
+ $ZodCheck.init(inst, def);
3107
+ inst._zod.onattach.push((inst) => {
3108
+ var _a;
3109
+ (_a = inst._zod.bag).multipleOf ?? (_a.multipleOf = def.value);
3110
+ });
3111
+ inst._zod.check = (payload) => {
3112
+ if (typeof payload.value !== typeof def.value)
3113
+ throw new Error("Cannot mix number and bigint in multiple_of check.");
3114
+ const isMultiple = typeof payload.value === "bigint" ? payload.value % def.value === BigInt(0) : floatSafeRemainder(payload.value, def.value) === 0;
3115
+ if (isMultiple)
3116
+ return;
3117
+ payload.issues.push({
3118
+ origin: typeof payload.value,
3119
+ code: "not_multiple_of",
3120
+ divisor: def.value,
3121
+ input: payload.value,
3122
+ inst,
3123
+ continue: !def.abort
3124
+ });
3125
+ };
3126
+ });
3127
+ var $ZodCheckNumberFormat = /* @__PURE__ */ $constructor("$ZodCheckNumberFormat", (inst, def) => {
3128
+ $ZodCheck.init(inst, def);
3129
+ def.format = def.format || "float64";
3130
+ const isInt = def.format?.includes("int");
3131
+ const origin = isInt ? "int" : "number";
3132
+ const [minimum, maximum] = NUMBER_FORMAT_RANGES[def.format];
3133
+ inst._zod.onattach.push((inst) => {
3134
+ const bag = inst._zod.bag;
3135
+ bag.format = def.format;
3136
+ bag.minimum = minimum;
3137
+ bag.maximum = maximum;
3138
+ if (isInt)
3139
+ bag.pattern = integer;
3140
+ });
3141
+ inst._zod.check = (payload) => {
3142
+ const input = payload.value;
3143
+ if (isInt) {
3144
+ if (!Number.isInteger(input)) {
3145
+ payload.issues.push({
3146
+ expected: origin,
3147
+ format: def.format,
3148
+ code: "invalid_type",
3149
+ continue: false,
3150
+ input,
3151
+ inst
3152
+ });
3153
+ return;
3154
+ }
3155
+ if (!Number.isSafeInteger(input)) {
3156
+ if (input > 0) {
3157
+ payload.issues.push({
3158
+ input,
3159
+ code: "too_big",
3160
+ maximum: Number.MAX_SAFE_INTEGER,
3161
+ note: "Integers must be within the safe integer range.",
3162
+ inst,
3163
+ origin,
3164
+ inclusive: true,
3165
+ continue: !def.abort
3166
+ });
3167
+ } else {
3168
+ payload.issues.push({
3169
+ input,
3170
+ code: "too_small",
3171
+ minimum: Number.MIN_SAFE_INTEGER,
3172
+ note: "Integers must be within the safe integer range.",
3173
+ inst,
3174
+ origin,
3175
+ inclusive: true,
3176
+ continue: !def.abort
3177
+ });
3178
+ }
3179
+ return;
3180
+ }
3181
+ }
3182
+ if (input < minimum) {
3183
+ payload.issues.push({
3184
+ origin: "number",
3185
+ input,
3186
+ code: "too_small",
3187
+ minimum,
3188
+ inclusive: true,
3189
+ inst,
3190
+ continue: !def.abort
3191
+ });
3192
+ }
3193
+ if (input > maximum) {
3194
+ payload.issues.push({
3195
+ origin: "number",
3196
+ input,
3197
+ code: "too_big",
3198
+ maximum,
3199
+ inclusive: true,
3200
+ inst,
3201
+ continue: !def.abort
3202
+ });
3203
+ }
3204
+ };
3205
+ });
3036
3206
  var $ZodCheckMaxLength = /* @__PURE__ */ $constructor("$ZodCheckMaxLength", (inst, def) => {
3037
3207
  var _a;
3038
3208
  $ZodCheck.init(inst, def);
@@ -3715,6 +3885,33 @@ var $ZodJWT = /* @__PURE__ */ $constructor("$ZodJWT", (inst, def) => {
3715
3885
  });
3716
3886
  };
3717
3887
  });
3888
+ var $ZodNumber = /* @__PURE__ */ $constructor("$ZodNumber", (inst, def) => {
3889
+ $ZodType.init(inst, def);
3890
+ inst._zod.pattern = inst._zod.bag.pattern ?? number;
3891
+ inst._zod.parse = (payload, _ctx) => {
3892
+ if (def.coerce)
3893
+ try {
3894
+ payload.value = Number(payload.value);
3895
+ } catch (_) {}
3896
+ const input = payload.value;
3897
+ if (typeof input === "number" && !Number.isNaN(input) && Number.isFinite(input)) {
3898
+ return payload;
3899
+ }
3900
+ const received = typeof input === "number" ? Number.isNaN(input) ? "NaN" : !Number.isFinite(input) ? "Infinity" : undefined : undefined;
3901
+ payload.issues.push({
3902
+ expected: "number",
3903
+ code: "invalid_type",
3904
+ input,
3905
+ inst,
3906
+ ...received ? { received } : {}
3907
+ });
3908
+ return payload;
3909
+ };
3910
+ });
3911
+ var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, def) => {
3912
+ $ZodCheckNumberFormat.init(inst, def);
3913
+ $ZodNumber.init(inst, def);
3914
+ });
3718
3915
  var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
3719
3916
  $ZodType.init(inst, def);
3720
3917
  inst._zod.pattern = boolean;
@@ -4929,6 +5126,22 @@ function _isoDuration(Class, params) {
4929
5126
  ...normalizeParams(params)
4930
5127
  });
4931
5128
  }
5129
+ function _number(Class, params) {
5130
+ return new Class({
5131
+ type: "number",
5132
+ checks: [],
5133
+ ...normalizeParams(params)
5134
+ });
5135
+ }
5136
+ function _int(Class, params) {
5137
+ return new Class({
5138
+ type: "number",
5139
+ check: "number_format",
5140
+ abort: false,
5141
+ format: "safeint",
5142
+ ...normalizeParams(params)
5143
+ });
5144
+ }
4932
5145
  function _boolean(Class, params) {
4933
5146
  return new Class({
4934
5147
  type: "boolean",
@@ -4946,6 +5159,45 @@ function _never(Class, params) {
4946
5159
  ...normalizeParams(params)
4947
5160
  });
4948
5161
  }
5162
+ function _lt(value, params) {
5163
+ return new $ZodCheckLessThan({
5164
+ check: "less_than",
5165
+ ...normalizeParams(params),
5166
+ value,
5167
+ inclusive: false
5168
+ });
5169
+ }
5170
+ function _lte(value, params) {
5171
+ return new $ZodCheckLessThan({
5172
+ check: "less_than",
5173
+ ...normalizeParams(params),
5174
+ value,
5175
+ inclusive: true
5176
+ });
5177
+ }
5178
+ function _gt(value, params) {
5179
+ return new $ZodCheckGreaterThan({
5180
+ check: "greater_than",
5181
+ ...normalizeParams(params),
5182
+ value,
5183
+ inclusive: false
5184
+ });
5185
+ }
5186
+ function _gte(value, params) {
5187
+ return new $ZodCheckGreaterThan({
5188
+ check: "greater_than",
5189
+ ...normalizeParams(params),
5190
+ value,
5191
+ inclusive: true
5192
+ });
5193
+ }
5194
+ function _multipleOf(value, params) {
5195
+ return new $ZodCheckMultipleOf({
5196
+ check: "multiple_of",
5197
+ ...normalizeParams(params),
5198
+ value
5199
+ });
5200
+ }
4949
5201
  function _maxLength(maximum, params) {
4950
5202
  const ch = new $ZodCheckMaxLength({
4951
5203
  check: "max_length",
@@ -5471,6 +5723,39 @@ var stringProcessor = (schema, ctx, _json, _params) => {
5471
5723
  }
5472
5724
  }
5473
5725
  };
5726
+ var numberProcessor = (schema, ctx, _json, _params) => {
5727
+ const json = _json;
5728
+ const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
5729
+ if (typeof format === "string" && format.includes("int"))
5730
+ json.type = "integer";
5731
+ else
5732
+ json.type = "number";
5733
+ const exMin = typeof exclusiveMinimum === "number" && exclusiveMinimum >= (minimum ?? Number.NEGATIVE_INFINITY);
5734
+ const exMax = typeof exclusiveMaximum === "number" && exclusiveMaximum <= (maximum ?? Number.POSITIVE_INFINITY);
5735
+ const legacy = ctx.target === "draft-04" || ctx.target === "openapi-3.0";
5736
+ if (exMin) {
5737
+ if (legacy) {
5738
+ json.minimum = exclusiveMinimum;
5739
+ json.exclusiveMinimum = true;
5740
+ } else {
5741
+ json.exclusiveMinimum = exclusiveMinimum;
5742
+ }
5743
+ } else if (typeof minimum === "number") {
5744
+ json.minimum = minimum;
5745
+ }
5746
+ if (exMax) {
5747
+ if (legacy) {
5748
+ json.maximum = exclusiveMaximum;
5749
+ json.exclusiveMaximum = true;
5750
+ } else {
5751
+ json.exclusiveMaximum = exclusiveMaximum;
5752
+ }
5753
+ } else if (typeof maximum === "number") {
5754
+ json.maximum = maximum;
5755
+ }
5756
+ if (typeof multipleOf === "number")
5757
+ json.multipleOf = multipleOf;
5758
+ };
5474
5759
  var booleanProcessor = (_schema, _ctx, json, _params) => {
5475
5760
  json.type = "boolean";
5476
5761
  };
@@ -6094,6 +6379,74 @@ var ZodJWT = /* @__PURE__ */ $constructor("ZodJWT", (inst, def) => {
6094
6379
  $ZodJWT.init(inst, def);
6095
6380
  ZodStringFormat.init(inst, def);
6096
6381
  });
6382
+ var ZodNumber = /* @__PURE__ */ $constructor("ZodNumber", (inst, def) => {
6383
+ $ZodNumber.init(inst, def);
6384
+ ZodType.init(inst, def);
6385
+ inst._zod.processJSONSchema = (ctx, json, params) => numberProcessor(inst, ctx, json, params);
6386
+ _installLazyMethods(inst, "ZodNumber", {
6387
+ gt(value, params) {
6388
+ return this.check(_gt(value, params));
6389
+ },
6390
+ gte(value, params) {
6391
+ return this.check(_gte(value, params));
6392
+ },
6393
+ min(value, params) {
6394
+ return this.check(_gte(value, params));
6395
+ },
6396
+ lt(value, params) {
6397
+ return this.check(_lt(value, params));
6398
+ },
6399
+ lte(value, params) {
6400
+ return this.check(_lte(value, params));
6401
+ },
6402
+ max(value, params) {
6403
+ return this.check(_lte(value, params));
6404
+ },
6405
+ int(params) {
6406
+ return this.check(int(params));
6407
+ },
6408
+ safe(params) {
6409
+ return this.check(int(params));
6410
+ },
6411
+ positive(params) {
6412
+ return this.check(_gt(0, params));
6413
+ },
6414
+ nonnegative(params) {
6415
+ return this.check(_gte(0, params));
6416
+ },
6417
+ negative(params) {
6418
+ return this.check(_lt(0, params));
6419
+ },
6420
+ nonpositive(params) {
6421
+ return this.check(_lte(0, params));
6422
+ },
6423
+ multipleOf(value, params) {
6424
+ return this.check(_multipleOf(value, params));
6425
+ },
6426
+ step(value, params) {
6427
+ return this.check(_multipleOf(value, params));
6428
+ },
6429
+ finite() {
6430
+ return this;
6431
+ }
6432
+ });
6433
+ const bag = inst._zod.bag;
6434
+ inst.minValue = Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null;
6435
+ inst.maxValue = Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null;
6436
+ inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5);
6437
+ inst.isFinite = true;
6438
+ inst.format = bag.format ?? null;
6439
+ });
6440
+ function number2(params) {
6441
+ return _number(ZodNumber, params);
6442
+ }
6443
+ var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def) => {
6444
+ $ZodNumberFormat.init(inst, def);
6445
+ ZodNumber.init(inst, def);
6446
+ });
6447
+ function int(params) {
6448
+ return _int(ZodNumberFormat, params);
6449
+ }
6097
6450
  var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
6098
6451
  $ZodBoolean.init(inst, def);
6099
6452
  ZodType.init(inst, def);
@@ -6466,12 +6819,184 @@ function refine(fn, _params = {}) {
6466
6819
  function superRefine(fn, params) {
6467
6820
  return _superRefine(fn, params);
6468
6821
  }
6822
+ // src/config-root.ts
6823
+ import { homedir } from "node:os";
6824
+ import { isAbsolute, join, resolve } from "node:path";
6825
+ var GVOZD_CONFIG_ROOT_ENV = "GVOZD_OPENCODE_CONFIG_ROOT";
6826
+ function absolute(path, label) {
6827
+ if (!isAbsolute(path))
6828
+ throw new Error(`${label} must be an absolute path`);
6829
+ return resolve(path);
6830
+ }
6831
+ function resolveOpenCodeConfigRootContract(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
6832
+ if (explicitRoot)
6833
+ return { path: absolute(explicitRoot, "OpenCode config root"), source: "explicit" };
6834
+ const override = env[GVOZD_CONFIG_ROOT_ENV];
6835
+ if (override) {
6836
+ return { path: absolute(override, GVOZD_CONFIG_ROOT_ENV), source: GVOZD_CONFIG_ROOT_ENV };
6837
+ }
6838
+ if (env.XDG_CONFIG_HOME) {
6839
+ return { path: join(absolute(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME"), "opencode"), source: "XDG_CONFIG_HOME" };
6840
+ }
6841
+ if (platform === "win32" && env.APPDATA) {
6842
+ return { path: join(env.APPDATA, "opencode"), source: "APPDATA" };
6843
+ }
6844
+ return { path: join(home, ".config", "opencode"), source: "platform-default" };
6845
+ }
6846
+ function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
6847
+ return resolveOpenCodeConfigRootContract(env, platform, home, explicitRoot).path;
6848
+ }
6849
+
6850
+ // src/project-trust.ts
6851
+ import { createHash } from "node:crypto";
6852
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
6853
+ import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "node:path";
6854
+ var PROJECT_TRUST_ENV = "GVOZD_TRUST_PROJECT_CONFIG";
6855
+ function findRoot(start) {
6856
+ let current = resolve2(start);
6857
+ while (true) {
6858
+ if (existsSync(join2(current, ".git")))
6859
+ return realpathSync(current);
6860
+ const parent = dirname(current);
6861
+ if (parent === current)
6862
+ return realpathSync(resolve2(start));
6863
+ current = parent;
6864
+ }
6865
+ }
6866
+ function within(base, target, label) {
6867
+ const child = relative(base, target);
6868
+ if (child === "" || !child.startsWith("..") && !isAbsolute2(child))
6869
+ return;
6870
+ throw new Error(`${label} must stay inside ${base}: ${target}`);
6871
+ }
6872
+ function rejectSymlinkComponents(base, target, label) {
6873
+ within(base, target, label);
6874
+ const segments = relative(base, target).split(/[\\/]/).filter(Boolean);
6875
+ let current = base;
6876
+ for (const segment of segments) {
6877
+ current = join2(current, segment);
6878
+ if (!existsSync(current))
6879
+ continue;
6880
+ if (lstatSync(current).isSymbolicLink())
6881
+ throw new Error(`${label} must not contain symlink components: ${current}`);
6882
+ }
6883
+ }
6884
+ function readJsonc(path) {
6885
+ const bytes = readFileSync(path);
6886
+ const errors = [];
6887
+ const value = parse2(bytes.toString("utf8"), errors, { allowTrailingComma: true, disallowComments: false });
6888
+ if (errors.length > 0 || !value || typeof value !== "object" || Array.isArray(value)) {
6889
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
6890
+ throw new Error(`Invalid JSONC in ${path}${details ? `: ${details}` : ""}`);
6891
+ }
6892
+ return { value, bytes };
6893
+ }
6894
+ function promptFromPatch(patch) {
6895
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
6896
+ return;
6897
+ const prompt = patch.prompt;
6898
+ return typeof prompt === "string" && prompt.length > 0 ? prompt : undefined;
6899
+ }
6900
+ function collectProjectTrustInputs(projectDirectory) {
6901
+ const canonicalRoot = findRoot(projectDirectory);
6902
+ const layer = join2(canonicalRoot, "docs", ".gvozd");
6903
+ const rootPath = join2(layer, "config.jsonc");
6904
+ if (!existsSync(rootPath))
6905
+ return {
6906
+ canonicalRoot,
6907
+ inputs: [{ identity: "docs/.gvozd/config.jsonc:<missing>", bytes: Buffer.alloc(0) }]
6908
+ };
6909
+ rejectSymlinkComponents(canonicalRoot, rootPath, "Project config path");
6910
+ const rootStat = lstatSync(rootPath);
6911
+ if (!rootStat.isFile() || rootStat.isSymbolicLink())
6912
+ throw new Error(`Project config must be a regular non-symlink file: ${rootPath}`);
6913
+ const root = readJsonc(rootPath);
6914
+ const files = new Map([[relative(canonicalRoot, rootPath).replaceAll("\\", "/"), root.bytes]]);
6915
+ const prompts = [];
6916
+ const inlineAgents = root.value.agents;
6917
+ if (inlineAgents && typeof inlineAgents === "object" && !Array.isArray(inlineAgents)) {
6918
+ for (const patch of Object.values(inlineAgents)) {
6919
+ const prompt = promptFromPatch(patch);
6920
+ if (prompt)
6921
+ prompts.push({ source: rootPath, value: prompt });
6922
+ }
6923
+ }
6924
+ const configuredDirectory = root.value.agentsDirectory;
6925
+ if (configuredDirectory !== undefined && (typeof configuredDirectory !== "string" || configuredDirectory.length === 0)) {
6926
+ throw new Error(`Invalid agentsDirectory in ${rootPath}`);
6927
+ }
6928
+ const agentsDirectory = resolve2(layer, configuredDirectory ?? "agents");
6929
+ within(layer, agentsDirectory, "agentsDirectory");
6930
+ if (existsSync(agentsDirectory)) {
6931
+ rejectSymlinkComponents(canonicalRoot, agentsDirectory, "agentsDirectory");
6932
+ const stat = lstatSync(agentsDirectory);
6933
+ if (!stat.isDirectory() || stat.isSymbolicLink())
6934
+ throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
6935
+ within(realpathSync(layer), realpathSync(agentsDirectory), "agentsDirectory");
6936
+ for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
6937
+ if (!entry.name.endsWith(".jsonc"))
6938
+ continue;
6939
+ const path = join2(agentsDirectory, entry.name);
6940
+ if (!entry.isFile())
6941
+ throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
6942
+ rejectSymlinkComponents(canonicalRoot, path, "Agent fragment path");
6943
+ const stat = lstatSync(path);
6944
+ if (!stat.isFile() || stat.isSymbolicLink())
6945
+ throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
6946
+ const fragment = readJsonc(path);
6947
+ files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), fragment.bytes);
6948
+ const prompt = promptFromPatch(fragment.value);
6949
+ if (prompt)
6950
+ prompts.push({ source: path, value: prompt });
6951
+ }
6952
+ }
6953
+ for (const prompt of prompts) {
6954
+ const path = resolve2(dirname(prompt.source), prompt.value);
6955
+ within(layer, path, "Agent prompt");
6956
+ rejectSymlinkComponents(canonicalRoot, path, "Agent prompt");
6957
+ if (!existsSync(path))
6958
+ throw new Error(`Agent prompt is missing: ${path}`);
6959
+ const stat = lstatSync(path);
6960
+ if (!stat.isFile() || stat.isSymbolicLink())
6961
+ throw new Error(`Agent prompt must be a regular non-symlink file: ${path}`);
6962
+ const canonical = realpathSync(path);
6963
+ within(realpathSync(layer), canonical, "Agent prompt");
6964
+ files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), readFileSync(path));
6965
+ }
6966
+ return {
6967
+ canonicalRoot,
6968
+ inputs: [...files].map(([identity, bytes]) => ({ identity, bytes })).sort((left, right) => left.identity.localeCompare(right.identity))
6969
+ };
6970
+ }
6971
+ function computeProjectTrustToken(projectDirectory) {
6972
+ const collected = collectProjectTrustInputs(projectDirectory);
6973
+ const hash = createHash("sha256");
6974
+ hash.update("agent-gvozd-project-trust-v1\x00");
6975
+ hash.update(collected.canonicalRoot);
6976
+ hash.update("\x00");
6977
+ for (const input of collected.inputs) {
6978
+ hash.update(String(Buffer.byteLength(input.identity)));
6979
+ hash.update(":");
6980
+ hash.update(input.identity);
6981
+ hash.update(":");
6982
+ hash.update(String(input.bytes.length));
6983
+ hash.update(":");
6984
+ hash.update(input.bytes);
6985
+ hash.update("\x00");
6986
+ }
6987
+ return `sha256:${hash.digest("hex")}`;
6988
+ }
6989
+
6990
+ // src/file-leases.ts
6991
+ var DEFAULT_RESERVATION_TTL_MS = 5 * 60 * 1000;
6992
+ var DEFAULT_ACTIVE_TTL_MS = 30 * 60 * 1000;
6993
+
6469
6994
  // src/config.ts
6470
6995
  var permissionSchema = object({
6471
6996
  action: string2().min(1),
6472
6997
  resource: string2().min(1),
6473
6998
  effect: _enum(["allow", "ask", "deny"])
6474
- });
6999
+ }).strict();
6475
7000
  var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
6476
7001
  var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
6477
7002
  var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
@@ -6485,12 +7010,18 @@ var agentPatchSchema = object({
6485
7010
  permissions: array(permissionSchema).optional(),
6486
7011
  fileLease: fileLeaseRoleSchema.optional(),
6487
7012
  disabled: boolean2().optional()
6488
- });
7013
+ }).strict();
7014
+ var leaseSchema = object({
7015
+ reservationTtlMinutes: number2().int().positive().max(24 * 60).optional(),
7016
+ activeTtlMinutes: number2().int().positive().max(24 * 60).optional()
7017
+ }).strict();
6489
7018
  var rootPatchSchema = object({
7019
+ $schema: string2().min(1).optional(),
6490
7020
  defaultAgent: agentIdSchema.optional(),
6491
7021
  agentsDirectory: string2().min(1).optional(),
6492
- agents: record(agentIdSchema, agentPatchSchema).optional()
6493
- });
7022
+ agents: record(agentIdSchema, agentPatchSchema).optional(),
7023
+ lease: leaseSchema.optional()
7024
+ }).strict();
6494
7025
  var resolvedAgentSchema = agentPatchSchema.extend({
6495
7026
  description: string2().min(1),
6496
7027
  mode: _enum(["primary", "subagent", "all"]),
@@ -6502,9 +7033,18 @@ var resolvedAgentSchema = agentPatchSchema.extend({
6502
7033
  fileLease: fileLeaseRoleSchema,
6503
7034
  disabled: boolean2()
6504
7035
  });
6505
- function readJsonc(path) {
7036
+ var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
7037
+ function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
7038
+ if (trusted)
7039
+ return;
7040
+ const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
7041
+ if (fields.length === 0 && knownAgents.has(id))
7042
+ return;
7043
+ throw new Error(`Untrusted project config ${sourcePath} cannot override ${fields.join(", ") || `unknown agent ${id}`}. ` + `Set ${PROJECT_TRUST_ENV} to the exact token returned by computeProjectTrustToken() after reviewing these files.`);
7044
+ }
7045
+ function readJsonc2(path) {
6506
7046
  const errors = [];
6507
- const value = parse2(readFileSync(path, "utf8"), errors, {
7047
+ const value = parse2(readFileSync2(path, "utf8"), errors, {
6508
7048
  allowTrailingComma: true,
6509
7049
  disallowComments: false
6510
7050
  });
@@ -6515,51 +7055,69 @@ function readJsonc(path) {
6515
7055
  return value;
6516
7056
  }
6517
7057
  function assertWithin(base, target, label) {
6518
- const child = relative(base, target);
6519
- if (child === "" || !child.startsWith("..") && !isAbsolute(child))
7058
+ const child = relative2(base, target);
7059
+ if (child === "" || !child.startsWith("..") && !isAbsolute3(child))
6520
7060
  return;
6521
7061
  throw new Error(`${label} must stay inside ${base}: ${target}`);
6522
7062
  }
6523
7063
  function resolvePrompt(patch, sourcePath, layerDirectory) {
6524
7064
  if (!patch.prompt)
6525
7065
  return patch;
6526
- const prompt = resolve(dirname(sourcePath), patch.prompt);
7066
+ const prompt = resolve3(dirname2(sourcePath), patch.prompt);
6527
7067
  assertWithin(layerDirectory, prompt, "Agent prompt");
6528
- if (!existsSync(prompt))
7068
+ if (!existsSync2(prompt))
6529
7069
  throw new Error(`Agent prompt is missing: ${prompt}`);
6530
- const canonical = realpathSync(prompt);
6531
- assertWithin(realpathSync(layerDirectory), canonical, "Agent prompt");
6532
- return { ...patch, prompt: canonical };
6533
- }
6534
- function loadLayer(directory, rootFileName, required) {
6535
- const rootPath = join(directory, rootFileName);
6536
- if (!existsSync(rootPath)) {
7070
+ const promptStat = lstatSync2(prompt);
7071
+ if (promptStat.isSymbolicLink() || !promptStat.isFile())
7072
+ throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
7073
+ const canonical = realpathSync2(prompt);
7074
+ assertWithin(realpathSync2(layerDirectory), canonical, "Agent prompt");
7075
+ const promptContent = readFileSync2(canonical, "utf8");
7076
+ return { ...patch, prompt: canonical, promptContent };
7077
+ }
7078
+ function loadLayer(directory, rootFileName, required, projectPolicy) {
7079
+ const rootPath = join3(directory, rootFileName);
7080
+ if (!existsSync2(rootPath)) {
6537
7081
  if (required)
6538
7082
  throw new Error(`Required config is missing: ${rootPath}`);
6539
7083
  return { agents: {}, sources: [] };
6540
7084
  }
6541
- const root = rootPatchSchema.parse(readJsonc(rootPath));
7085
+ const root = rootPatchSchema.parse(readJsonc2(rootPath));
7086
+ if (projectPolicy && !projectPolicy.trusted) {
7087
+ const restricted = ["defaultAgent", "agentsDirectory", "lease"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
7088
+ if (restricted.length > 0)
7089
+ throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
7090
+ }
6542
7091
  const agents = {};
6543
7092
  for (const [id, patch] of Object.entries(root.agents ?? {})) {
7093
+ if (projectPolicy)
7094
+ assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
6544
7095
  agents[id] = resolvePrompt(patch, rootPath, directory);
6545
7096
  }
6546
- const agentsDirectory = resolve(directory, root.agentsDirectory ?? "agents");
7097
+ const agentsDirectory = resolve3(directory, root.agentsDirectory ?? "agents");
6547
7098
  assertWithin(directory, agentsDirectory, "agentsDirectory");
6548
- if (existsSync(agentsDirectory)) {
6549
- assertWithin(realpathSync(directory), realpathSync(agentsDirectory), "agentsDirectory");
6550
- for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
7099
+ if (existsSync2(agentsDirectory)) {
7100
+ const directoryStat = lstatSync2(agentsDirectory);
7101
+ if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
7102
+ throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
7103
+ }
7104
+ assertWithin(realpathSync2(directory), realpathSync2(agentsDirectory), "agentsDirectory");
7105
+ for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
6551
7106
  if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
6552
7107
  continue;
6553
7108
  const id = entry.name.slice(0, -".jsonc".length);
6554
7109
  agentIdSchema.parse(id);
6555
- const agentPath = join(agentsDirectory, entry.name);
6556
- const patch = agentPatchSchema.parse(readJsonc(agentPath));
7110
+ const agentPath = join3(agentsDirectory, entry.name);
7111
+ const patch = agentPatchSchema.parse(readJsonc2(agentPath));
7112
+ if (projectPolicy)
7113
+ assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
6557
7114
  agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
6558
7115
  }
6559
7116
  }
6560
7117
  return {
6561
7118
  defaultAgent: root.defaultAgent,
6562
7119
  agents,
7120
+ lease: root.lease,
6563
7121
  sources: [rootPath]
6564
7122
  };
6565
7123
  }
@@ -6567,22 +7125,24 @@ function mergeAgent(base, override) {
6567
7125
  return { ...base ?? {}, ...override };
6568
7126
  }
6569
7127
  function resolveAgentConfig(patch) {
6570
- const parsed = agentPatchSchema.parse(patch);
6571
- return resolvedAgentSchema.parse({
7128
+ const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
7129
+ const { promptContent, ...agentPatch } = parsed;
7130
+ const resolved = resolvedAgentSchema.parse({
6572
7131
  skills: [],
6573
7132
  mcp: [],
6574
7133
  permissions: [],
6575
7134
  fileLease: "readonly",
6576
7135
  disabled: false,
6577
- ...parsed
7136
+ ...agentPatch
6578
7137
  });
7138
+ return promptContent === undefined ? resolved : { ...resolved, promptContent };
6579
7139
  }
6580
7140
  function findPackageRoot() {
6581
- let current = dirname(fileURLToPath(import.meta.url));
7141
+ let current = dirname2(fileURLToPath(import.meta.url));
6582
7142
  while (true) {
6583
- if (existsSync(join(current, "defaults", "default.jsonc")))
7143
+ if (existsSync2(join3(current, "defaults", "default.jsonc")))
6584
7144
  return current;
6585
- const parent = dirname(current);
7145
+ const parent = dirname2(current);
6586
7146
  if (parent === current)
6587
7147
  break;
6588
7148
  current = parent;
@@ -6590,34 +7150,49 @@ function findPackageRoot() {
6590
7150
  throw new Error("Unable to locate agent-gvozd package defaults");
6591
7151
  }
6592
7152
  function findProjectRoot(start) {
6593
- let current = resolve(start);
7153
+ let current = resolve3(start);
6594
7154
  while (true) {
6595
- const git = join(current, ".git");
6596
- if (existsSync(git))
7155
+ const git = join3(current, ".git");
7156
+ if (existsSync2(git))
6597
7157
  return current;
6598
- const parent = dirname(current);
7158
+ const parent = dirname2(current);
6599
7159
  if (parent === current)
6600
- return resolve(start);
7160
+ return resolve3(start);
6601
7161
  current = parent;
6602
7162
  }
6603
7163
  }
6604
- function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir()) {
6605
- if (env.XDG_CONFIG_HOME)
6606
- return join(env.XDG_CONFIG_HOME, "opencode");
6607
- if (platform === "win32" && env.APPDATA)
6608
- return join(env.APPDATA, "opencode");
6609
- return join(home, ".config", "opencode");
6610
- }
6611
7164
  function loadConfig(projectDirectory, options = {}) {
6612
7165
  const projectRoot = findProjectRoot(projectDirectory);
6613
7166
  const packageRoot = findPackageRoot();
6614
- const projectConfigDirectory = join(projectRoot, "docs", ".gvozd");
6615
- const globalConfigDirectory = join(options.configRoot ?? resolveOpenCodeConfigRoot(options.env, options.platform, options.home), "gvozd");
6616
- const layers = [
6617
- loadLayer(join(packageRoot, "defaults"), "default.jsonc", true),
6618
- loadLayer(globalConfigDirectory, "config.jsonc", false),
6619
- ...options.includeProject === false ? [] : [loadLayer(projectConfigDirectory, "config.jsonc", false)]
7167
+ const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
7168
+ const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
7169
+ const baseLayers = [
7170
+ loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
7171
+ loadLayer(globalConfigDirectory, "config.jsonc", false)
6620
7172
  ];
7173
+ const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
7174
+ const includeProject = options.includeProject !== false;
7175
+ const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
7176
+ const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
7177
+ const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
7178
+ if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
7179
+ throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
7180
+ }
7181
+ const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
7182
+ const lease = {
7183
+ reservationTtlMs: DEFAULT_RESERVATION_TTL_MS,
7184
+ activeTtlMs: DEFAULT_ACTIVE_TTL_MS
7185
+ };
7186
+ for (const layer of layers) {
7187
+ if (!layer.lease)
7188
+ continue;
7189
+ if (layer.lease.reservationTtlMinutes !== undefined) {
7190
+ lease.reservationTtlMs = layer.lease.reservationTtlMinutes * 60000;
7191
+ }
7192
+ if (layer.lease.activeTtlMinutes !== undefined) {
7193
+ lease.activeTtlMs = layer.lease.activeTtlMinutes * 60000;
7194
+ }
7195
+ }
6621
7196
  let defaultAgent;
6622
7197
  const agents = {};
6623
7198
  for (const layer of layers) {
@@ -6630,7 +7205,12 @@ function loadConfig(projectDirectory, options = {}) {
6630
7205
  throw new Error("defaultAgent is not configured");
6631
7206
  const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
6632
7207
  id,
6633
- resolveAgentConfig(patch)
7208
+ (() => {
7209
+ const agent = resolveAgentConfig(patch);
7210
+ if (agent.promptContent === undefined)
7211
+ throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
7212
+ return agent;
7213
+ })()
6634
7214
  ]));
6635
7215
  const defaultConfig = resolvedAgents[defaultAgent];
6636
7216
  if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
@@ -6639,6 +7219,7 @@ function loadConfig(projectDirectory, options = {}) {
6639
7219
  return {
6640
7220
  defaultAgent,
6641
7221
  agents: resolvedAgents,
7222
+ lease,
6642
7223
  packageRoot,
6643
7224
  projectRoot,
6644
7225
  projectConfigDirectory,
@@ -6648,15 +7229,157 @@ function loadConfig(projectDirectory, options = {}) {
6648
7229
  }
6649
7230
 
6650
7231
  // src/cli/doctor.ts
6651
- import { lstatSync, readFileSync as readFileSync3 } from "node:fs";
6652
- import { join as join2, resolve as resolve2 } from "node:path";
7232
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
7233
+ import { join as join4, resolve as resolve4 } from "node:path";
6653
7234
 
6654
- // src/agent-generation.ts
6655
- import { readFileSync as readFileSync2 } from "node:fs";
7235
+ // src/tool-permissions.ts
7236
+ function family(command, ...variants) {
7237
+ return [command, ...variants].map((entry) => ({
7238
+ exact: entry,
7239
+ wildcard: `${entry} *`
7240
+ }));
7241
+ }
7242
+ var INSPECTION_COMMANDS = [
7243
+ ...family("pwd", "true", "test"),
7244
+ ...family("cat", "head", "tail", "wc", "sort", "uniq"),
7245
+ ...family("grep", "rg", "find", "diff", "cmp"),
7246
+ ...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
7247
+ ...family("shasum", "sha256sum", "md5sum"),
7248
+ ...family("uname", "whoami", "hostname", "date", "printenv"),
7249
+ ...family("which", "command -v"),
7250
+ ...family("mktemp"),
7251
+ ...family("tr", "cut", "paste", "column"),
7252
+ ...family("node --version", "python3 --version", "python --version", "deno --version")
7253
+ ];
7254
+ var TOOLCHAIN_COMMANDS = [
7255
+ ...family("bun test", "bun run test", "bun --version"),
7256
+ ...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
7257
+ ...family("tsc --noEmit", "npx tsc --noEmit"),
7258
+ ...family("eslint", "biome check", "prettier --check"),
7259
+ ...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
7260
+ ...family("pnpm test", "pnpm run test", "pnpm run build"),
7261
+ ...family("yarn test", "yarn build"),
7262
+ ...family("vitest run", "jest", "playwright test"),
7263
+ ...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
7264
+ ...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
7265
+ ...family("pytest", "python3 -m pytest", "python -m pytest"),
7266
+ ...family("ruff check", "mypy", "pyright"),
7267
+ ...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
7268
+ ...family("make test", "make check", "make build", "make --version"),
7269
+ ...family("just --list")
7270
+ ];
7271
+ var GIT_READONLY_COMMANDS = [
7272
+ ...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
7273
+ ...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
7274
+ ...family("git log", "git show", "git reflog"),
7275
+ ...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file", "git symbolic-ref"),
7276
+ ...family("git ls-files", "git ls-remote", "git grep"),
7277
+ ...family("git branch", "git remote", "git stash list", "git tag", "git describe"),
7278
+ ...family("git config --get", "git config --get-regexp"),
7279
+ ...family("git -C")
7280
+ ];
7281
+ var GIT_MUTATING_COMMANDS = [
7282
+ ...family("git add", "git rm --cached"),
7283
+ ...family("git commit", "git merge --ff-only", "git merge --no-ff"),
7284
+ ...family("git push", "git fetch", "git pull --ff-only"),
7285
+ ...family("git tag -a", "git tag -v", "git tag --list"),
7286
+ ...family("git stash", "git cherry-pick", "git revert"),
7287
+ ...family("git switch", "git checkout -b", "git worktree list", "git worktree add")
7288
+ ];
7289
+ var GIT_ENV_PREFIXES = ["GIT_OPTIONAL_LOCKS=0"];
7290
+ function withEnvPrefixes(rule, prefixes = GIT_ENV_PREFIXES) {
7291
+ return prefixes.map((prefix) => ({
7292
+ action: rule.action,
7293
+ resource: `${prefix} ${rule.resource}`,
7294
+ effect: rule.effect
7295
+ }));
7296
+ }
7297
+
7298
+ // src/agent-permissions.ts
7299
+ function normalizeMcpName(name) {
7300
+ return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
7301
+ }
7302
+ function buildAgentPermissions(agent, mcpServers) {
7303
+ const result = [
7304
+ ...agent.permissions,
7305
+ { action: "skill", resource: "*", effect: "deny" },
7306
+ ...agent.skills.map((skill) => ({ action: "skill", resource: skill, effect: "allow" }))
7307
+ ];
7308
+ for (const server of mcpServers) {
7309
+ const prefix = `${normalizeMcpName(server)}_`;
7310
+ result.push({
7311
+ action: `${prefix}*`,
7312
+ resource: "*",
7313
+ effect: agent.mcp.includes(server) ? "allow" : "deny"
7314
+ });
7315
+ result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
7316
+ }
7317
+ for (const rule of agent.permissions) {
7318
+ if (rule.action !== "shell")
7319
+ continue;
7320
+ if (!/(^|\s|["'])git(?:$|\s)/.test(rule.resource))
7321
+ continue;
7322
+ if (rule.resource.startsWith("GIT_"))
7323
+ continue;
7324
+ result.push(...withEnvPrefixes(rule));
7325
+ }
7326
+ return result;
7327
+ }
7328
+
7329
+ // src/release-metadata.ts
7330
+ var PACKAGE_NAME = "@nail00749/agent-gvozd";
7331
+ var PACKAGE_VERSION = "0.1.3";
7332
+ var PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
7333
+ var SUPPORTED_OPENCODE_VERSION = "0.0.0-beta-19425";
7334
+ var CONFIG_SCHEMA_VERSION = 2;
6656
7335
 
6657
7336
  // src/constants.ts
6658
7337
  var GENERATED_MARKER = "# Generated by agent-gvozd sync. Do not edit this file directly.";
6659
7338
  var GENERATED_PLUGIN_MARKER = "// Generated by agent-gvozd sync. Do not edit this file directly.";
7339
+ function hasGeneratedAgentMarker(content) {
7340
+ return content.startsWith(`---
7341
+ ${GENERATED_MARKER}
7342
+ `);
7343
+ }
7344
+ function hasGeneratedPluginMarker(content) {
7345
+ return content.startsWith(`${GENERATED_PLUGIN_MARKER}
7346
+ `);
7347
+ }
7348
+ function hasGeneratedSchemaMarker(content) {
7349
+ const errors = [];
7350
+ const value = parse2(content, errors, { allowTrailingComma: true, disallowComments: false });
7351
+ if (errors.length > 0 || value === null || typeof value !== "object" || Array.isArray(value)) {
7352
+ return false;
7353
+ }
7354
+ if (value.$comment !== GENERATED_PLUGIN_MARKER)
7355
+ return false;
7356
+ const version = value["x-agent-gvozd-schema-version"];
7357
+ return typeof version === "number" && Number.isSafeInteger(version) && version >= 1 && version <= CONFIG_SCHEMA_VERSION;
7358
+ }
7359
+ function stable(value) {
7360
+ if (Array.isArray(value))
7361
+ return value.map(stable);
7362
+ if (!value || typeof value !== "object")
7363
+ return value;
7364
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stable(entry)]));
7365
+ }
7366
+ function isEquivalentLegacySchema(content, generated) {
7367
+ try {
7368
+ const previous = JSON.parse(content);
7369
+ const expected = JSON.parse(generated);
7370
+ if (!previous || Array.isArray(previous) || previous.$comment !== undefined)
7371
+ return false;
7372
+ if (previous["x-agent-gvozd-schema-version"] !== undefined && previous["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION)
7373
+ return false;
7374
+ delete previous.$comment;
7375
+ delete previous["x-agent-gvozd-schema-version"];
7376
+ delete expected.$comment;
7377
+ delete expected["x-agent-gvozd-schema-version"];
7378
+ return JSON.stringify(stable(previous)) === JSON.stringify(stable(expected));
7379
+ } catch {
7380
+ return false;
7381
+ }
7382
+ }
6660
7383
 
6661
7384
  // src/agent-generation.ts
6662
7385
  function yamlString(value) {
@@ -6675,12 +7398,11 @@ function renderPermissions(rules) {
6675
7398
  ];
6676
7399
  }
6677
7400
  function renderAgent(agent) {
6678
- const prompt = readFileSync2(agent.prompt, "utf8").trim();
6679
- const permissions = [
6680
- ...agent.permissions,
6681
- { action: "skill", resource: "*", effect: "deny" },
6682
- ...agent.skills.map((skill) => ({ action: "skill", resource: skill, effect: "allow" }))
6683
- ];
7401
+ if (agent.promptContent === undefined) {
7402
+ throw new Error(`Agent is missing its immutable prompt snapshot (${agent.prompt})`);
7403
+ }
7404
+ const prompt = agent.promptContent.trim();
7405
+ const permissions = buildAgentPermissions(agent, []);
6684
7406
  return [
6685
7407
  "---",
6686
7408
  GENERATED_MARKER,
@@ -6695,9 +7417,15 @@ function renderAgent(agent) {
6695
7417
  `);
6696
7418
  }
6697
7419
 
7420
+ // src/runtime-events.ts
7421
+ function redactDiagnostic(error) {
7422
+ const message = error instanceof Error ? error.message : String(error);
7423
+ return message.replace(/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi, "[redacted private key]").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]*@/gi, "$1[redacted]@").replace(/([?&](?:access_?token|auth(?:orization)?|api_?key|cookie|credential|password|secret|token)=)[^&#\s]*/gi, "$1[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b([A-Za-z0-9_]*(?:token|password|authorization|api_?key|secret|credential|cookie)[A-Za-z0-9_]*)\s*[:=]\s*["']?[^\s,;}"']+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 300);
7424
+ }
7425
+
6698
7426
  // src/cli/opencode.ts
6699
7427
  import { spawn } from "node:child_process";
6700
- import { isAbsolute as isAbsolute2 } from "node:path";
7428
+ import { isAbsolute as isAbsolute4 } from "node:path";
6701
7429
  var MAX_OUTPUT_BYTES = 64 * 1024;
6702
7430
  var DEFAULT_TIMEOUT_MS = 15000;
6703
7431
  function appendBounded(current, chunk) {
@@ -6709,43 +7437,96 @@ function appendBounded(current, chunk) {
6709
7437
  var defaultProcessRunner = {
6710
7438
  run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS) {
6711
7439
  return new Promise((resolve, reject) => {
7440
+ const grouped = process.platform !== "win32";
6712
7441
  const child = spawn(executable, [...args], {
7442
+ detached: grouped,
6713
7443
  shell: false,
6714
7444
  stdio: ["ignore", "pipe", "pipe"]
6715
7445
  });
6716
7446
  let stdout = "";
6717
7447
  let stderr = "";
7448
+ let settled = false;
6718
7449
  let timedOut = false;
6719
7450
  let forceKill;
7451
+ let hardDeadline;
7452
+ const terminate = (signal) => {
7453
+ try {
7454
+ if (grouped && child.pid)
7455
+ process.kill(-child.pid, signal);
7456
+ else
7457
+ child.kill(signal);
7458
+ } catch {
7459
+ try {
7460
+ child.kill(signal);
7461
+ } catch {}
7462
+ }
7463
+ };
7464
+ const onStdout = (chunk) => {
7465
+ stdout = appendBounded(stdout, chunk);
7466
+ };
7467
+ const onStderr = (chunk) => {
7468
+ stderr = appendBounded(stderr, chunk);
7469
+ };
7470
+ const timeoutError = () => {
7471
+ const error = new Error(`OpenCode command timed out after ${timeoutMs}ms`);
7472
+ error.code = "ETIMEDOUT";
7473
+ return error;
7474
+ };
7475
+ const clearTimers = () => {
7476
+ clearTimeout(timer);
7477
+ if (forceKill)
7478
+ clearTimeout(forceKill);
7479
+ if (hardDeadline)
7480
+ clearTimeout(hardDeadline);
7481
+ };
7482
+ const settleTimeout = (destroy) => {
7483
+ if (settled)
7484
+ return;
7485
+ settled = true;
7486
+ clearTimers();
7487
+ if (destroy) {
7488
+ child.stdout.off("data", onStdout);
7489
+ child.stderr.off("data", onStderr);
7490
+ child.stdout.destroy();
7491
+ child.stderr.destroy();
7492
+ }
7493
+ reject(timeoutError());
7494
+ };
6720
7495
  const timer = setTimeout(() => {
7496
+ if (settled)
7497
+ return;
6721
7498
  timedOut = true;
6722
- child.kill("SIGTERM");
6723
- forceKill = setTimeout(() => child.kill("SIGKILL"), 500);
7499
+ terminate("SIGTERM");
7500
+ forceKill = setTimeout(() => terminate("SIGKILL"), 500);
6724
7501
  forceKill.unref();
7502
+ hardDeadline = setTimeout(() => settleTimeout(true), 1750);
7503
+ hardDeadline.unref();
6725
7504
  }, timeoutMs);
6726
7505
  timer.unref();
6727
- child.stdout.on("data", (chunk) => {
6728
- stdout = appendBounded(stdout, chunk);
6729
- });
6730
- child.stderr.on("data", (chunk) => {
6731
- stderr = appendBounded(stderr, chunk);
6732
- });
7506
+ child.stdout.on("data", onStdout);
7507
+ child.stderr.on("data", onStderr);
6733
7508
  child.once("error", (error) => {
6734
- clearTimeout(timer);
6735
- if (forceKill)
6736
- clearTimeout(forceKill);
7509
+ clearTimers();
7510
+ if (settled)
7511
+ return;
7512
+ settled = true;
7513
+ if (timedOut) {
7514
+ child.stdout.off("data", onStdout);
7515
+ child.stderr.off("data", onStderr);
7516
+ child.stdout.destroy();
7517
+ child.stderr.destroy();
7518
+ }
6737
7519
  reject(error);
6738
7520
  });
6739
7521
  child.once("close", (code) => {
6740
- clearTimeout(timer);
6741
- if (forceKill)
6742
- clearTimeout(forceKill);
7522
+ clearTimers();
7523
+ if (settled)
7524
+ return;
6743
7525
  if (timedOut) {
6744
- const error = new Error(`OpenCode command timed out after ${timeoutMs}ms`);
6745
- error.code = "ETIMEDOUT";
6746
- reject(error);
7526
+ settleTimeout(false);
6747
7527
  return;
6748
7528
  }
7529
+ settled = true;
6749
7530
  resolve({ code: code ?? 1, stdout, stderr });
6750
7531
  });
6751
7532
  });
@@ -6757,8 +7538,8 @@ function bounded(value) {
6757
7538
  async function checked(runner, executable, args, timeoutMs) {
6758
7539
  const result = await runner.run(executable, args, timeoutMs);
6759
7540
  if (result.code !== 0) {
6760
- const detail = bounded(result.stderr).trim();
6761
- throw new Error(`${executable} ${args.join(" ")} exited ${result.code}${detail ? `: ${detail}` : ""}`);
7541
+ const detail = redactDiagnostic(bounded(result.stderr).trim());
7542
+ throw new Error(`OpenCode command exited ${result.code}${detail ? `: ${detail}` : ""}`);
6762
7543
  }
6763
7544
  return bounded(result.stdout);
6764
7545
  }
@@ -6770,7 +7551,7 @@ function parseDebugPaths(output) {
6770
7551
  continue;
6771
7552
  paths[match[1]] = match[2];
6772
7553
  }
6773
- if (!paths.config || !isAbsolute2(paths.config)) {
7554
+ if (!paths.config || !isAbsolute4(paths.config)) {
6774
7555
  throw new Error("OpenCode did not report an absolute config path");
6775
7556
  }
6776
7557
  return paths;
@@ -6875,10 +7656,6 @@ function manualProfile(provider, fast, deep, catalog) {
6875
7656
 
6876
7657
  // src/cli/doctor.ts
6877
7658
  var SETUP_COMMAND = "gvozd setup";
6878
- function redact(value) {
6879
- const message = value instanceof Error ? value.message : String(value);
6880
- return message.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b([A-Za-z0-9_]*(?:token|password|authorization|api_?key|secret|credential|cookie)[A-Za-z0-9_]*)\s*[:=]\s*["']?[^\s,;}"']+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 300);
6881
- }
6882
7659
  function aggregate(checks) {
6883
7660
  if (checks.some((check) => check.status === "fail"))
6884
7661
  return "fail";
@@ -6888,7 +7665,7 @@ function aggregate(checks) {
6888
7665
  }
6889
7666
  function safeFile(path) {
6890
7667
  try {
6891
- const stat = lstatSync(path);
7668
+ const stat = lstatSync3(path);
6892
7669
  return stat.isFile() && !stat.isSymbolicLink();
6893
7670
  } catch {
6894
7671
  return false;
@@ -6900,8 +7677,10 @@ function escapeRegExp(value) {
6900
7677
  function hasAgentIdentifier(output, id) {
6901
7678
  return new RegExp(`(?:^|[^A-Za-z0-9_-])${escapeRegExp(id)}(?=$|[^A-Za-z0-9_-])`, "m").test(output);
6902
7679
  }
6903
- function hasPluginIdentifier(output, name) {
6904
- return new RegExp(`(?:^|[\\s"'|│])${escapeRegExp(name)}(?:@[^\\s"'|│,}\\]]+)?(?=$|[\\s"'|│,}\\]])`, "m").test(output);
7680
+ function hasInstalledPluginVersion(output, name, version) {
7681
+ const boundary = `[\\s"'|│,}\\]]`;
7682
+ const pattern = `(?:^|${boundary})${escapeRegExp(name)}(?:@|\\s+)v?${escapeRegExp(version)}(?=$|${boundary})`;
7683
+ return new RegExp(pattern, "m").test(output);
6905
7684
  }
6906
7685
  function checkGlobalFiles(config, configRoot) {
6907
7686
  const missing = [];
@@ -6909,27 +7688,43 @@ function checkGlobalFiles(config, configRoot) {
6909
7688
  for (const [id, agent] of Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right))) {
6910
7689
  if (agent.disabled)
6911
7690
  continue;
6912
- const path = join2(configRoot, "agents", `${id}.md`);
7691
+ const path = join4(configRoot, "agents", `${id}.md`);
6913
7692
  if (!safeFile(path)) {
6914
7693
  missing.push(id);
6915
7694
  continue;
6916
7695
  }
6917
7696
  const content = readFileSync3(path, "utf8");
6918
- if (!content.includes(GENERATED_MARKER) || content !== renderAgent(agent))
7697
+ if (!hasGeneratedAgentMarker(content) || content !== renderAgent(agent))
6919
7698
  stale.push(id);
6920
7699
  }
6921
- if (missing.length + stale.length === 0) {
6922
- return { id: "global-agents", status: "pass", summary: `${Object.keys(config.agents).length} managed global agents are current` };
7700
+ const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
7701
+ const orphans = [];
7702
+ const directory = join4(configRoot, "agents");
7703
+ if (existsSync3(directory) && lstatSync3(directory).isDirectory() && !lstatSync3(directory).isSymbolicLink()) {
7704
+ for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
7705
+ if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
7706
+ continue;
7707
+ const content = readFileSync3(join4(directory, entry.name), "utf8");
7708
+ if (hasGeneratedAgentMarker(content))
7709
+ orphans.push(entry.name.slice(0, -3));
7710
+ }
7711
+ }
7712
+ if (missing.length + stale.length + orphans.length === 0) {
7713
+ return { id: "global-agents", status: "pass", summary: `${enabled.size} managed global agents are current` };
6923
7714
  }
6924
- const details = [missing.length ? `missing: ${missing.join(", ")}` : "", stale.length ? `unmanaged or stale: ${stale.join(", ")}` : ""].filter(Boolean).join("; ");
7715
+ const details = [
7716
+ missing.length ? `missing: ${missing.join(", ")}` : "",
7717
+ stale.length ? `unmanaged or stale: ${stale.join(", ")}` : "",
7718
+ orphans.length ? `orphan managed agents: ${orphans.join(", ")}` : ""
7719
+ ].filter(Boolean).join("; ");
6925
7720
  return { id: "global-agents", status: "fail", summary: details, remediation: SETUP_COMMAND };
6926
7721
  }
6927
7722
  function checkLegacy(config) {
6928
7723
  const duplicates = [];
6929
- const plugin = join2(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
7724
+ const plugin = join4(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
6930
7725
  if (safeFile(plugin))
6931
7726
  duplicates.push("local plugin");
6932
- const agents = Object.keys(config.agents).filter((id) => safeFile(join2(config.projectRoot, ".opencode", "agents", `${id}.md`)));
7727
+ const agents = Object.keys(config.agents).filter((id) => safeFile(join4(config.projectRoot, ".opencode", "agents", `${id}.md`)));
6933
7728
  if (agents.length > 0)
6934
7729
  duplicates.push(`${agents.length} local agents`);
6935
7730
  if (duplicates.length === 0)
@@ -6942,58 +7737,59 @@ function checkLegacy(config) {
6942
7737
  };
6943
7738
  }
6944
7739
  async function runDoctor(input) {
6945
- const packageName = input.packageName ?? "@nail00749/agent-gvozd";
6946
- const packageVersion = input.packageVersion ?? "0.1.0";
6947
- const supportedVersion = input.supportedOpenCodeVersion ?? "0.0.0-beta-19425";
7740
+ const packageName = input.packageName ?? PACKAGE_NAME;
7741
+ const packageVersion = input.packageVersion ?? PACKAGE_VERSION;
7742
+ const supportedVersion = input.supportedOpenCodeVersion ?? SUPPORTED_OPENCODE_VERSION;
6948
7743
  const checks = [];
6949
7744
  try {
6950
7745
  const version = await input.client.version();
6951
- checks.push(parseOpenCodeVersion(version) === supportedVersion ? { id: "opencode-version", status: "pass", summary: `OpenCode ${supportedVersion} is available` } : { id: "opencode-version", status: "fail", summary: `unsupported OpenCode version: ${redact(version)}`, remediation: `Install OpenCode ${supportedVersion}` });
7746
+ checks.push(parseOpenCodeVersion(version) === supportedVersion ? { id: "opencode-version", status: "pass", summary: `OpenCode ${supportedVersion} is available` } : { id: "opencode-version", status: "fail", summary: `unsupported OpenCode version: ${redactDiagnostic(version)}`, remediation: `Install OpenCode ${supportedVersion}` });
6952
7747
  } catch (error) {
6953
- checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${redact(error)}`, remediation: `Install OpenCode ${supportedVersion}` });
7748
+ checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${redactDiagnostic(error)}`, remediation: `Install OpenCode ${supportedVersion}` });
6954
7749
  }
6955
7750
  try {
6956
7751
  await input.client.serviceStatus();
6957
7752
  checks.push({ id: "service", status: "pass", summary: "OpenCode service is reachable" });
6958
7753
  } catch (error) {
6959
- checks.push({ id: "service", status: "fail", summary: `OpenCode service is unavailable: ${redact(error)}`, remediation: `${input.client.executable} service restart` });
7754
+ checks.push({ id: "service", status: "fail", summary: `OpenCode service is unavailable: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} service restart` });
6960
7755
  }
6961
7756
  try {
6962
7757
  const output = await input.client.pluginList();
6963
- checks.push(hasPluginIdentifier(output, packageName) ? { id: "plugin", status: "pass", summary: `${packageName} is registered` } : { id: "plugin", status: "fail", summary: `${packageName} is not registered`, remediation: SETUP_COMMAND });
7758
+ checks.push(hasInstalledPluginVersion(output, packageName, packageVersion) ? { id: "plugin", status: "pass", summary: `${packageName} ${packageVersion} is registered` } : { id: "plugin", status: "fail", summary: `${packageName} ${packageVersion} is not registered`, remediation: SETUP_COMMAND });
6964
7759
  } catch (error) {
6965
- checks.push({ id: "plugin", status: "fail", summary: `plugin list failed: ${redact(error)}`, remediation: SETUP_COMMAND });
7760
+ checks.push({ id: "plugin", status: "fail", summary: `plugin list failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
6966
7761
  }
6967
7762
  try {
6968
- const output = await input.client.pluginCheck(`${packageName}@^${packageVersion}`);
6969
- const unhealthy = /\b(error|failed|incompatible)\b/i.test(output);
6970
- checks.push(unhealthy ? { id: "plugin-check", status: "fail", summary: "OpenCode reports an unhealthy Gvozd plugin", remediation: SETUP_COMMAND } : { id: "plugin-check", status: "pass", summary: "Gvozd plugin check passed" });
7763
+ await input.client.pluginCheck(PACKAGE_SPEC);
7764
+ checks.push({ id: "plugin-check", status: "pass", summary: "Gvozd plugin check passed" });
6971
7765
  } catch (error) {
6972
- checks.push({ id: "plugin-check", status: "fail", summary: `plugin check failed: ${redact(error)}`, remediation: SETUP_COMMAND });
7766
+ checks.push({ id: "plugin-check", status: "fail", summary: `plugin check failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
6973
7767
  }
6974
7768
  try {
6975
7769
  const paths = await input.client.debugPaths();
6976
- checks.push(resolve2(paths.config) === resolve2(input.configRoot) ? { id: "config-root", status: "pass", summary: "runtime and CLI config roots match" } : { id: "config-root", status: "fail", summary: "runtime and CLI config roots differ", remediation: `${input.client.executable} debug paths` });
7770
+ const reported = resolve4(paths.config);
7771
+ const expected = resolve4(input.runtimeConfigRoot ?? input.configRoot);
7772
+ checks.push(reported === resolve4(input.configRoot) && reported === expected ? { id: "config-root", status: "pass", summary: "runtime and CLI config roots match" } : { id: "config-root", status: "fail", summary: "OpenCode debug path and independently resolved runtime config root differ", remediation: `${input.client.executable} debug paths; set GVOZD_OPENCODE_CONFIG_ROOT to the reported config path before starting OpenCode` });
6977
7773
  } catch (error) {
6978
- checks.push({ id: "config-root", status: "fail", summary: `config path check failed: ${redact(error)}` });
7774
+ checks.push({ id: "config-root", status: "fail", summary: `config path check failed: ${redactDiagnostic(error)}` });
6979
7775
  }
6980
7776
  let config;
6981
7777
  let globalConfig;
6982
- const configPath = join2(input.configRoot, "gvozd", "config.jsonc");
6983
- const schemaPath = join2(input.configRoot, "gvozd", "schema.json");
7778
+ const configPath = join4(input.configRoot, "gvozd", "config.jsonc");
7779
+ const schemaPath = join4(input.configRoot, "gvozd", "schema.json");
6984
7780
  try {
6985
7781
  if (!safeFile(configPath) || !safeFile(schemaPath))
6986
7782
  throw new Error("global config.jsonc or schema.json is missing");
6987
7783
  const schemaErrors = [];
6988
7784
  const schema = parse2(readFileSync3(schemaPath, "utf8"), schemaErrors);
6989
- if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !== 1 || !readFileSync3(schemaPath, "utf8").includes(GENERATED_PLUGIN_MARKER)) {
7785
+ if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION || schema?.$comment !== GENERATED_PLUGIN_MARKER) {
6990
7786
  throw new Error("global schema is invalid, incompatible, or unmanaged");
6991
7787
  }
6992
7788
  globalConfig = loadConfig(input.cwd, { configRoot: input.configRoot, includeProject: false });
6993
7789
  config = loadConfig(input.cwd, { configRoot: input.configRoot });
6994
7790
  checks.push({ id: "config", status: "pass", summary: "global Gvozd config and schema are valid" });
6995
7791
  } catch (error) {
6996
- checks.push({ id: "config", status: "fail", summary: `global config check failed: ${redact(error)}`, remediation: SETUP_COMMAND });
7792
+ checks.push({ id: "config", status: "fail", summary: `global config check failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
6997
7793
  }
6998
7794
  let catalog = parseModels([]);
6999
7795
  try {
@@ -7003,12 +7799,14 @@ async function runDoctor(input) {
7003
7799
  if (!config) {
7004
7800
  checks.push({ id: "models", status: "fail", summary: "configured models cannot be validated without a valid config", remediation: SETUP_COMMAND });
7005
7801
  } else {
7006
- const configured = new Set(Object.values(config.agents).flatMap((agent) => agent.models));
7007
- const missing = [...configured].filter((model) => !catalog.models.includes(model)).sort();
7008
- checks.push(missing.length === 0 ? { id: "models", status: "pass", summary: `${catalog.models.length} available models cover the Gvozd profile` } : { id: "models", status: "fail", summary: `configured models are unavailable: ${missing.join(", ")}`, remediation: "gvozd config" });
7802
+ const available = new Set(catalog.models);
7803
+ const enabled = Object.entries(config.agents).filter(([, agent]) => !agent.disabled);
7804
+ const unavailableAgents = enabled.filter(([, agent]) => !agent.models.some((model) => available.has(model))).map(([id]) => id);
7805
+ const missingFallbacks = [...new Set(enabled.flatMap(([, agent]) => agent.models.filter((model) => !available.has(model))))].sort();
7806
+ checks.push(unavailableAgents.length > 0 ? { id: "models", status: "fail", summary: `no configured model is available for: ${unavailableAgents.join(", ")}`, remediation: "gvozd config" } : missingFallbacks.length > 0 ? { id: "models", status: "warn", summary: `primary coverage is available; unavailable fallback models: ${missingFallbacks.join(", ")}`, remediation: "gvozd config" } : { id: "models", status: "pass", summary: `${catalog.models.length} available models cover the Gvozd profile` });
7009
7807
  }
7010
7808
  } catch (error) {
7011
- checks.push({ id: "models", status: "fail", summary: `model catalog check failed: ${redact(error)}`, remediation: `${input.client.executable} auth` });
7809
+ checks.push({ id: "models", status: "fail", summary: `model catalog check failed: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} auth` });
7012
7810
  }
7013
7811
  if (globalConfig)
7014
7812
  checks.push(checkGlobalFiles(globalConfig, input.configRoot));
@@ -7019,10 +7817,10 @@ async function runDoctor(input) {
7019
7817
  const missing = Object.entries(config?.agents ?? {}).filter(([, agent]) => !agent.disabled).map(([id]) => id).filter((id) => !hasAgentIdentifier(output, id));
7020
7818
  checks.push(missing.length === 0 && config ? { id: "runtime-agents", status: "pass", summary: "all enabled Gvozd agents are visible to OpenCode" } : { id: "runtime-agents", status: "fail", summary: `runtime agents are missing: ${missing.join(", ") || "config unavailable"}`, remediation: `${input.client.executable} service restart` });
7021
7819
  } catch (error) {
7022
- checks.push({ id: "runtime-agents", status: "fail", summary: `runtime agent check failed: ${redact(error)}`, remediation: `${input.client.executable} service restart` });
7820
+ checks.push({ id: "runtime-agents", status: "fail", summary: `runtime agent check failed: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} service restart` });
7023
7821
  }
7024
7822
  checks.push(config ? checkLegacy(config) : { id: "legacy-local", status: "warn", summary: "legacy duplicates could not be checked" });
7025
- return { schemaVersion: 1, status: aggregate(checks), checks };
7823
+ return { schemaVersion: CONFIG_SCHEMA_VERSION, status: aggregate(checks), checks };
7026
7824
  }
7027
7825
  function doctorExitCode(report) {
7028
7826
  return report.status === "fail" ? 1 : 0;
@@ -7044,20 +7842,84 @@ function doctorOperationalFailure(error) {
7044
7842
  const checks = [{
7045
7843
  id: "opencode-discovery",
7046
7844
  status: "fail",
7047
- summary: `OpenCode discovery failed: ${redact(error)}`,
7048
- remediation: "Install OpenCode 0.0.0-beta-19425 and run gvozd doctor again"
7845
+ summary: `OpenCode discovery failed: ${redactDiagnostic(error)}`,
7846
+ remediation: `Install OpenCode ${SUPPORTED_OPENCODE_VERSION} and run gvozd doctor again`
7049
7847
  }];
7050
- return { schemaVersion: 1, status: "fail", checks };
7848
+ return { schemaVersion: CONFIG_SCHEMA_VERSION, status: "fail", checks };
7051
7849
  }
7052
7850
 
7053
7851
  // src/cli/setup.ts
7054
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
7055
- import { dirname as dirname3, join as join5 } from "node:path";
7852
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
7853
+ import { dirname as dirname6, join as join8 } from "node:path";
7056
7854
 
7057
7855
  // src/cli/config-store.ts
7058
- import { closeSync, existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync, openSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "node:fs";
7856
+ import { accessSync, closeSync, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "node:fs";
7059
7857
  import { randomUUID } from "node:crypto";
7060
- import { dirname as dirname2, join as join3 } from "node:path";
7858
+ import { dirname as dirname3, join as join6 } from "node:path";
7859
+
7860
+ // src/secure-path.ts
7861
+ import { lstatSync as lstatSync4, realpathSync as realpathSync3 } from "node:fs";
7862
+ import { isAbsolute as isAbsolute5, join as join5, parse as parse6, relative as relative3, resolve as resolve5, sep } from "node:path";
7863
+ function secureCanonicalPath(target, label = "Path") {
7864
+ if (!isAbsolute5(target))
7865
+ throw new Error(`${label} must be absolute: ${target}`);
7866
+ const lexical = resolve5(target);
7867
+ const root = parse6(lexical).root;
7868
+ const components = relative3(root, lexical).split(sep).filter(Boolean);
7869
+ let current = root;
7870
+ let nearestExisting = root;
7871
+ const missingSuffix = [];
7872
+ let missing = false;
7873
+ const rootStat = lstatSync4(root);
7874
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
7875
+ throw new Error(`${label} has an unsafe filesystem root: ${root}`);
7876
+ const existingDirectories = [{ path: root, stat: rootStat }];
7877
+ for (const component of components) {
7878
+ current = join5(current, component);
7879
+ if (missing) {
7880
+ missingSuffix.push(component);
7881
+ continue;
7882
+ }
7883
+ let stat;
7884
+ try {
7885
+ stat = lstatSync4(current);
7886
+ } catch (error) {
7887
+ if (error.code !== "ENOENT")
7888
+ throw error;
7889
+ missing = true;
7890
+ missingSuffix.push(component);
7891
+ continue;
7892
+ }
7893
+ if (stat.isSymbolicLink())
7894
+ throw new Error(`${label} contains a symbolic-link component: ${current}`);
7895
+ if (current !== lexical && !stat.isDirectory()) {
7896
+ throw new Error(`${label} contains a non-directory ancestor: ${current}`);
7897
+ }
7898
+ if (stat.isDirectory())
7899
+ existingDirectories.push({ path: current, stat });
7900
+ nearestExisting = current;
7901
+ }
7902
+ if (process.platform !== "win32") {
7903
+ const uid = process.getuid?.();
7904
+ for (let index = 0;index < existingDirectories.length; index++) {
7905
+ const entry = existingDirectories[index];
7906
+ if ((entry.stat.mode & 18) === 0)
7907
+ continue;
7908
+ const sticky = (entry.stat.mode & 512) !== 0;
7909
+ const hasPrivateBoundary = sticky && uid !== undefined && existingDirectories.slice(index + 1).some((candidate) => candidate.stat.uid === uid && (candidate.stat.mode & 18) === 0);
7910
+ if (!hasPrivateBoundary) {
7911
+ throw new Error(`${label} contains a group/world-writable directory without an existing private boundary: ${entry.path}`);
7912
+ }
7913
+ }
7914
+ }
7915
+ const canonicalAncestor = realpathSync3(nearestExisting);
7916
+ if (canonicalAncestor !== nearestExisting) {
7917
+ throw new Error(`${label} contains a non-canonical or changed ancestor: ${nearestExisting}`);
7918
+ }
7919
+ return resolve5(canonicalAncestor, ...missingSuffix);
7920
+ }
7921
+
7922
+ // src/cli/config-store.ts
7061
7923
  var FAST_AGENT_IDS = ["back-fast", "front-fast", "review-fast", "researcher", "git", "docs", "verifier"];
7062
7924
  var DEEP_AGENT_IDS = ["master", "planner", "back-deep", "front-deep", "review-deep", "debugger", "security", "devops"];
7063
7925
  var ALL_AGENT_IDS = [...DEEP_AGENT_IDS, ...FAST_AGENT_IDS, "explorer"];
@@ -7084,14 +7946,30 @@ function applyModelProfile(source, profile) {
7084
7946
  updated = setJsonc(updated, ["agents", "explorer", "models"], profile.agentOverrides.explorer ?? profile.fast);
7085
7947
  return updated;
7086
7948
  }
7087
- function atomicWrite(path, content) {
7088
- mkdirSync(dirname2(path), { recursive: true, mode: 448 });
7089
- const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
7949
+ function matchesSnapshot(path, expected) {
7950
+ if (!expected.exists)
7951
+ return !existsSync4(path);
7952
+ if (!existsSync4(path))
7953
+ return false;
7954
+ const stat = lstatSync5(path);
7955
+ return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino && readFileSync4(path, "utf8") === expected.bytes;
7956
+ }
7957
+ function atomicWrite(path, content, expected) {
7958
+ const canonicalPath = secureCanonicalPath(path, "Managed global config path");
7959
+ if (canonicalPath !== expected.path)
7960
+ throw new Error(`Global config path changed after preflight: ${path}`);
7961
+ mkdirSync(dirname3(canonicalPath), { recursive: true, mode: 448 });
7962
+ if (secureCanonicalPath(canonicalPath, "Managed global config path") !== canonicalPath)
7963
+ throw new Error(`Global config path changed during write: ${path}`);
7964
+ assertWriteable(dirname3(canonicalPath), "Managed global config directory");
7965
+ const temporary = `${canonicalPath}.tmp-${process.pid}-${randomUUID()}`;
7090
7966
  const descriptor = openSync(temporary, "wx", 384);
7091
7967
  try {
7092
7968
  writeFileSync(descriptor, content);
7093
7969
  closeSync(descriptor);
7094
- renameSync(temporary, path);
7970
+ if (!matchesSnapshot(canonicalPath, expected))
7971
+ throw new Error(`Refusing to replace concurrently changed file: ${canonicalPath}`);
7972
+ renameSync(temporary, canonicalPath);
7095
7973
  } catch (error) {
7096
7974
  try {
7097
7975
  closeSync(descriptor);
@@ -7103,51 +7981,107 @@ function atomicWrite(path, content) {
7103
7981
  }
7104
7982
  }
7105
7983
  function isRegularFile(path) {
7106
- const stat = lstatSync2(path);
7984
+ const stat = lstatSync5(path);
7107
7985
  return stat.isFile() && !stat.isSymbolicLink();
7108
7986
  }
7109
- function preflightGlobalConfig(configRoot) {
7110
- const rootStat = existsSync3(configRoot) ? lstatSync2(configRoot) : undefined;
7987
+ function assertWriteable(path, label) {
7988
+ let candidate = path;
7989
+ while (!existsSync4(candidate)) {
7990
+ const parent = dirname3(candidate);
7991
+ if (parent === candidate)
7992
+ break;
7993
+ candidate = parent;
7994
+ }
7995
+ try {
7996
+ const stat = lstatSync5(candidate);
7997
+ if (stat.isSymbolicLink() || !stat.isDirectory() && candidate !== path)
7998
+ throw new Error("unsafe parent");
7999
+ const uid = process.getuid?.();
8000
+ if (uid !== undefined && (stat.uid !== uid || (stat.mode & 18) !== 0))
8001
+ throw new Error("unsafe ownership or mode");
8002
+ accessSync(candidate, fsConstants.W_OK | (stat.isDirectory() ? fsConstants.X_OK : 0));
8003
+ } catch {
8004
+ throw new Error(`${label} is not writeable: ${path}`);
8005
+ }
8006
+ }
8007
+ function legacySchemaMatches(source, generated) {
8008
+ try {
8009
+ const previous = JSON.parse(source);
8010
+ const previousVersion = previous["x-agent-gvozd-schema-version"];
8011
+ if (previous.$comment !== undefined || previousVersion !== undefined && previousVersion !== CONFIG_SCHEMA_VERSION) {
8012
+ return false;
8013
+ }
8014
+ if (previous.$id !== "https://example.invalid/agent-gvozd.schema.json")
8015
+ return false;
8016
+ return generated ? isEquivalentLegacySchema(source, generated) : true;
8017
+ } catch {
8018
+ return false;
8019
+ }
8020
+ }
8021
+ function snapshot(path) {
8022
+ if (!existsSync4(path))
8023
+ return Object.freeze({ path, exists: false });
8024
+ const stat = lstatSync5(path);
8025
+ if (!stat.isFile() || stat.isSymbolicLink())
8026
+ throw new Error(`Managed global config snapshot target is unsafe: ${path}`);
8027
+ return Object.freeze({ path, exists: true, bytes: readFileSync4(path, "utf8"), dev: stat.dev, ino: stat.ino });
8028
+ }
8029
+ function preflightGlobalConfig(configRoot, schemaSource) {
8030
+ const canonicalRoot = secureCanonicalPath(configRoot, "OpenCode config root");
8031
+ const rootStat = existsSync4(canonicalRoot) ? lstatSync5(canonicalRoot) : undefined;
7111
8032
  if (rootStat && (rootStat.isSymbolicLink() || !rootStat.isDirectory())) {
7112
- throw new Error(`OpenCode config root is not a safe directory: ${configRoot}`);
8033
+ throw new Error(`OpenCode config root is not a safe directory: ${canonicalRoot}`);
7113
8034
  }
7114
- const directory = join3(configRoot, "gvozd");
7115
- const directoryStat = existsSync3(directory) ? lstatSync2(directory) : undefined;
8035
+ assertWriteable(canonicalRoot, "OpenCode config root");
8036
+ const directory = secureCanonicalPath(join6(canonicalRoot, "gvozd"), "Global Gvozd directory");
8037
+ const directoryStat = existsSync4(directory) ? lstatSync5(directory) : undefined;
7116
8038
  if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
7117
8039
  throw new Error(`Global Gvozd path is not a safe directory: ${directory}`);
7118
8040
  }
7119
- const configPath = join3(directory, "config.jsonc");
7120
- const schemaPath = join3(directory, "schema.json");
7121
- if (existsSync3(configPath)) {
8041
+ assertWriteable(directory, "Global Gvozd directory");
8042
+ const configPath = join6(directory, "config.jsonc");
8043
+ const schemaPath = join6(directory, "schema.json");
8044
+ if (existsSync4(configPath)) {
7122
8045
  if (!isRegularFile(configPath))
7123
8046
  throw new Error(`Global Gvozd config is not a safe file: ${configPath}`);
7124
8047
  assertValidJsonc(readFileSync4(configPath, "utf8"), configPath);
8048
+ assertWriteable(configPath, "Global Gvozd config");
7125
8049
  }
7126
- if (existsSync3(schemaPath) && (!isRegularFile(schemaPath) || !readFileSync4(schemaPath, "utf8").includes(GENERATED_PLUGIN_MARKER))) {
7127
- throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
8050
+ if (existsSync4(schemaPath)) {
8051
+ if (!isRegularFile(schemaPath))
8052
+ throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
8053
+ const schema = readFileSync4(schemaPath, "utf8");
8054
+ if (!hasGeneratedSchemaMarker(schema) && !legacySchemaMatches(schema, schemaSource)) {
8055
+ throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
8056
+ }
8057
+ assertWriteable(schemaPath, "Global Gvozd schema");
7128
8058
  }
8059
+ return Object.freeze({ configRoot: canonicalRoot, config: snapshot(configPath), schema: snapshot(schemaPath) });
7129
8060
  }
7130
8061
  function writeGlobalConfig(input) {
7131
- preflightGlobalConfig(input.configRoot);
7132
- const directory = join3(input.configRoot, "gvozd");
7133
- const configPath = join3(directory, "config.jsonc");
7134
- const schemaPath = join3(directory, "schema.json");
7135
- const base = existsSync3(configPath) ? readFileSync4(configPath, "utf8") : `{
8062
+ const state = input.snapshot ?? preflightGlobalConfig(input.configRoot, input.schemaSource);
8063
+ const canonicalRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
8064
+ if (state.configRoot !== canonicalRoot)
8065
+ throw new Error("Global config snapshot belongs to another config root");
8066
+ const directory = join6(state.configRoot, "gvozd");
8067
+ const configPath = join6(directory, "config.jsonc");
8068
+ const schemaPath = join6(directory, "schema.json");
8069
+ const base = state.config.exists ? state.config.bytes : `{
7136
8070
  "$schema": "./schema.json",
7137
8071
  "agents": {}
7138
8072
  }
7139
8073
  `;
7140
- if (!input.schemaSource.includes(GENERATED_PLUGIN_MARKER)) {
8074
+ if (!hasGeneratedSchemaMarker(input.schemaSource)) {
7141
8075
  throw new Error("Package schema is missing the Gvozd ownership marker");
7142
8076
  }
7143
8077
  const config = applyModelProfile(base, input.profile);
7144
8078
  assertValidJsonc(input.schemaSource, "package Gvozd schema");
7145
8079
  atomicWrite(schemaPath, input.schemaSource.endsWith(`
7146
8080
  `) ? input.schemaSource : `${input.schemaSource}
7147
- `);
8081
+ `, state.schema);
7148
8082
  atomicWrite(configPath, config.endsWith(`
7149
8083
  `) ? config : `${config}
7150
- `);
8084
+ `, state.config);
7151
8085
  return { configPath, schemaPath, config };
7152
8086
  }
7153
8087
 
@@ -7220,20 +8154,43 @@ function profileFromAgents(agents, catalog) {
7220
8154
  }
7221
8155
 
7222
8156
  // src/cli/global-sync.ts
7223
- import { closeSync as closeSync2, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
8157
+ import { accessSync as accessSync2, closeSync as closeSync2, constants as fsConstants2, existsSync as existsSync5, lstatSync as lstatSync6, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync5, readdirSync as readdirSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
7224
8158
  import { randomUUID as randomUUID2 } from "node:crypto";
7225
- import { join as join4 } from "node:path";
8159
+ import { dirname as dirname4, join as join7 } from "node:path";
7226
8160
  function stat(path) {
7227
8161
  try {
7228
- return lstatSync3(path);
8162
+ return lstatSync6(path);
7229
8163
  } catch (error) {
7230
8164
  if (error.code === "ENOENT")
7231
8165
  return;
7232
8166
  throw error;
7233
8167
  }
7234
8168
  }
8169
+ function assertWriteable2(path, label) {
8170
+ let candidate = path;
8171
+ while (!existsSync5(candidate)) {
8172
+ const parent = dirname4(candidate);
8173
+ if (parent === candidate)
8174
+ break;
8175
+ candidate = parent;
8176
+ }
8177
+ try {
8178
+ const current = lstatSync6(candidate);
8179
+ if (current.isSymbolicLink() || !current.isDirectory() && candidate !== path)
8180
+ throw new Error("unsafe parent");
8181
+ const uid = process.getuid?.();
8182
+ if (uid !== undefined && (current.uid !== uid || (current.mode & 18) !== 0))
8183
+ throw new Error("unsafe ownership or mode");
8184
+ accessSync2(candidate, fsConstants2.W_OK | (current.isDirectory() ? fsConstants2.X_OK : 0));
8185
+ } catch {
8186
+ throw new Error(`${label} is not writeable: ${path}`);
8187
+ }
8188
+ }
7235
8189
  function createFile(path, content) {
7236
- const descriptor = openSync2(path, "wx", 384);
8190
+ const canonicalPath = secureCanonicalPath(path, "Managed global agent path");
8191
+ if (canonicalPath !== path)
8192
+ throw new Error(`Managed global agent path changed: ${path}`);
8193
+ const descriptor = openSync2(canonicalPath, "wx", 384);
7237
8194
  try {
7238
8195
  writeFileSync2(descriptor, content);
7239
8196
  } finally {
@@ -7241,6 +8198,8 @@ function createFile(path, content) {
7241
8198
  }
7242
8199
  }
7243
8200
  function replaceFile(path, content) {
8201
+ if (secureCanonicalPath(path, "Managed global agent path") !== path)
8202
+ throw new Error(`Managed global agent path changed: ${path}`);
7244
8203
  const temporary = `${path}.tmp-${process.pid}-${randomUUID2()}`;
7245
8204
  createFile(temporary, content);
7246
8205
  try {
@@ -7253,21 +8212,41 @@ function replaceFile(path, content) {
7253
8212
  }
7254
8213
  }
7255
8214
  function writeManagedAgents(input) {
7256
- const agentsDirectory = join4(input.configRoot, "agents");
7257
- const result = { created: [], updated: [], unchanged: [], conflicts: [] };
8215
+ const configRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
8216
+ const agentsDirectory = secureCanonicalPath(join7(configRoot, "agents"), "OpenCode agents path");
8217
+ const result = { created: [], updated: [], unchanged: [], conflicts: [], removed: [] };
7258
8218
  const writes = [];
7259
- const rootStat = stat(input.configRoot);
8219
+ const removals = new Map;
8220
+ const enabled = new Set(Object.entries(input.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
8221
+ const rootStat = stat(configRoot);
7260
8222
  if (rootStat && (rootStat.isSymbolicLink() || !rootStat.isDirectory())) {
7261
- throw new Error(`OpenCode config root is not a safe directory: ${input.configRoot}`);
8223
+ throw new Error(`OpenCode config root is not a safe directory: ${configRoot}`);
7262
8224
  }
8225
+ assertWriteable2(configRoot, "OpenCode config root");
7263
8226
  const directoryStat = stat(agentsDirectory);
7264
8227
  if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
7265
8228
  throw new Error(`OpenCode agents path is not a safe directory: ${agentsDirectory}`);
7266
8229
  }
8230
+ assertWriteable2(agentsDirectory, "OpenCode agents directory");
8231
+ if (directoryStat) {
8232
+ for (const entry of readdirSync4(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
8233
+ if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
8234
+ continue;
8235
+ const path = join7(agentsDirectory, entry.name);
8236
+ const currentStat = stat(path);
8237
+ if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile())
8238
+ continue;
8239
+ if (hasGeneratedAgentMarker(readFileSync5(path, "utf8"))) {
8240
+ assertWriteable2(path, "Managed global agent");
8241
+ result.removed.push(path);
8242
+ removals.set(path, readFileSync5(path, "utf8"));
8243
+ }
8244
+ }
8245
+ }
7267
8246
  for (const [id, agent] of Object.entries(input.agents).sort(([left], [right]) => left.localeCompare(right))) {
7268
8247
  if (agent.disabled)
7269
8248
  continue;
7270
- const path = join4(agentsDirectory, `${id}.md`);
8249
+ const path = join7(agentsDirectory, `${id}.md`);
7271
8250
  const content = renderAgent(agent);
7272
8251
  const currentStat = stat(path);
7273
8252
  if (!currentStat) {
@@ -7280,45 +8259,134 @@ function writeManagedAgents(input) {
7280
8259
  continue;
7281
8260
  }
7282
8261
  const current = readFileSync5(path, "utf8");
7283
- if (!current.includes(GENERATED_MARKER)) {
8262
+ if (!hasGeneratedAgentMarker(current)) {
7284
8263
  result.conflicts.push(path);
7285
8264
  continue;
7286
8265
  }
8266
+ assertWriteable2(path, "Managed global agent");
7287
8267
  if (current === content) {
7288
8268
  result.unchanged.push(path);
7289
8269
  continue;
7290
8270
  }
7291
8271
  result.updated.push(path);
7292
- writes.push({ path, content, replace: true });
8272
+ writes.push({ path, content, replace: true, previous: current });
7293
8273
  }
7294
8274
  if (result.conflicts.length > 0) {
7295
8275
  throw new Error(`Refusing to overwrite unmanaged global agents: ${result.conflicts.join(", ")}`);
7296
8276
  }
7297
8277
  if (input.check)
7298
8278
  return result;
7299
- mkdirSync2(input.configRoot, { recursive: true, mode: 448 });
8279
+ if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
8280
+ throw new Error("OpenCode config root changed before write");
8281
+ mkdirSync2(configRoot, { recursive: true, mode: 448 });
8282
+ if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
8283
+ throw new Error("OpenCode config root changed during creation");
8284
+ assertWriteable2(configRoot, "OpenCode config root");
8285
+ if (secureCanonicalPath(agentsDirectory, "OpenCode agents path") !== agentsDirectory)
8286
+ throw new Error("OpenCode agents path changed before creation");
7300
8287
  mkdirSync2(agentsDirectory, { recursive: true, mode: 448 });
8288
+ if (secureCanonicalPath(agentsDirectory, "OpenCode agents path") !== agentsDirectory)
8289
+ throw new Error("OpenCode agents path changed before write");
8290
+ assertWriteable2(configRoot, "OpenCode config root");
8291
+ assertWriteable2(agentsDirectory, "OpenCode agents directory");
8292
+ for (const path of result.removed) {
8293
+ if (secureCanonicalPath(path, "Managed global agent path") !== path)
8294
+ throw new Error(`Managed global agent path changed: ${path}`);
8295
+ const currentStat = stat(path);
8296
+ if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile()) {
8297
+ throw new Error(`Refusing to remove a changed or unmanaged global agent: ${path}`);
8298
+ }
8299
+ const current = readFileSync5(path, "utf8");
8300
+ if (!hasGeneratedAgentMarker(current) || current !== removals.get(path)) {
8301
+ throw new Error(`Refusing to remove a concurrently changed global agent: ${path}`);
8302
+ }
8303
+ unlinkSync2(path);
8304
+ }
7301
8305
  for (const write of writes) {
7302
- if (write.replace)
8306
+ if (write.replace) {
8307
+ const currentStat = stat(write.path);
8308
+ const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ? readFileSync5(write.path, "utf8") : undefined;
8309
+ if (!current || !hasGeneratedAgentMarker(current) || current !== write.previous) {
8310
+ throw new Error(`Refusing to replace a concurrently changed global agent: ${write.path}`);
8311
+ }
7303
8312
  replaceFile(write.path, write.content);
7304
- else
8313
+ } else
7305
8314
  createFile(write.path, write.content);
7306
8315
  }
7307
8316
  return result;
7308
8317
  }
7309
8318
 
8319
+ // src/file-lock.ts
8320
+ import { randomUUID as randomUUID3 } from "node:crypto";
8321
+ import { accessSync as accessSync3, closeSync as closeSync3, constants, existsSync as existsSync6, fstatSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
8322
+ import { dirname as dirname5 } from "node:path";
8323
+ function assertSecure(path) {
8324
+ const stat = lstatSync7(path);
8325
+ if (!stat.isDirectory() || stat.isSymbolicLink())
8326
+ throw new Error(`Lock directory is unsafe: ${path}`);
8327
+ const uid = process.getuid?.();
8328
+ if (uid !== undefined && (stat.uid !== uid || (stat.mode & 18) !== 0)) {
8329
+ throw new Error(`Lock directory must be owner-controlled and not group/world writable: ${path}`);
8330
+ }
8331
+ accessSync3(path, constants.W_OK | constants.X_OK);
8332
+ }
8333
+ function acquire(path, operation) {
8334
+ const canonicalPath = secureCanonicalPath(path, "Lock path");
8335
+ const directory = dirname5(canonicalPath);
8336
+ mkdirSync3(directory, { recursive: true, mode: 448 });
8337
+ if (secureCanonicalPath(canonicalPath, "Lock path") !== canonicalPath)
8338
+ throw new Error(`Lock path changed during creation: ${path}`);
8339
+ assertSecure(directory);
8340
+ const nonce = `${process.pid}:${randomUUID3()}
8341
+ `;
8342
+ let descriptor;
8343
+ try {
8344
+ descriptor = openSync3(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
8345
+ } catch (error) {
8346
+ if (error.code === "EEXIST") {
8347
+ throw new Error(`Another Gvozd ${operation} is active or left a lock at ${canonicalPath}; inspect it manually and do not delete it while work may be running`);
8348
+ }
8349
+ throw error;
8350
+ }
8351
+ const created = fstatSync(descriptor);
8352
+ writeFileSync3(descriptor, nonce);
8353
+ return { path: canonicalPath, descriptor, nonce, dev: created.dev, ino: created.ino };
8354
+ }
8355
+ function release(lock) {
8356
+ closeSync3(lock.descriptor);
8357
+ if (existsSync6(lock.path)) {
8358
+ const current = lstatSync7(lock.path);
8359
+ if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino && readFileSync6(lock.path, "utf8") === lock.nonce)
8360
+ unlinkSync3(lock.path);
8361
+ }
8362
+ }
8363
+ async function withExclusiveFileLock(path, callback) {
8364
+ const lock = acquire(path, "setup");
8365
+ try {
8366
+ return await callback();
8367
+ } finally {
8368
+ release(lock);
8369
+ }
8370
+ }
8371
+ function withExclusiveFileLockSync(path, callback, operation = "operation") {
8372
+ const lock = acquire(path, operation);
8373
+ try {
8374
+ return callback();
8375
+ } finally {
8376
+ release(lock);
8377
+ }
8378
+ }
8379
+
7310
8380
  // src/cli/setup.ts
7311
- var PACKAGE_SPEC = "@nail00749/agent-gvozd@^0.1.0";
7312
- var SUPPORTED_VERSION = "0.0.0-beta-19425";
7313
8381
  function assertVersion(version) {
7314
- if (parseOpenCodeVersion(version) !== SUPPORTED_VERSION) {
7315
- throw new Error(`Unsupported OpenCode version. Gvozd 0.1.0 requires ${SUPPORTED_VERSION}.`);
8382
+ if (parseOpenCodeVersion(version) !== SUPPORTED_OPENCODE_VERSION) {
8383
+ throw new Error(`Unsupported OpenCode version. Gvozd ${PACKAGE_VERSION} requires ${SUPPORTED_OPENCODE_VERSION}.`);
7316
8384
  }
7317
8385
  }
7318
8386
  async function selectProfile(input, client, configRoot) {
7319
8387
  const catalog = parseModels(await client.models());
7320
8388
  const config = loadConfig(input.cwd, { configRoot, includeProject: false });
7321
- const hasGlobalConfig = existsSync4(join5(configRoot, "gvozd", "config.jsonc"));
8389
+ const hasGlobalConfig = existsSync7(join8(configRoot, "gvozd", "config.jsonc"));
7322
8390
  return chooseModelProfile({
7323
8391
  catalog,
7324
8392
  ui: input.ui,
@@ -7335,54 +8403,100 @@ async function confirm2(input, message) {
7335
8403
  const answer = await input.ui.confirm({ message, initialValue: true });
7336
8404
  return typeof answer === "symbol" ? false : answer;
7337
8405
  }
8406
+ function sameSnapshot(left, right) {
8407
+ return left.configRoot === right.configRoot && left.config.exists === right.config.exists && left.config.dev === right.config.dev && left.config.ino === right.config.ino && left.config.bytes === right.config.bytes && left.schema.exists === right.schema.exists && left.schema.dev === right.schema.dev && left.schema.ino === right.schema.ino && left.schema.bytes === right.schema.bytes;
8408
+ }
7338
8409
  async function runSetup(input) {
7339
8410
  const client = await (input.findClient ?? (() => findOpenCode()))();
7340
8411
  const paths = await client.debugPaths();
7341
- const configRoot = paths.config;
7342
- if (!configRoot)
8412
+ if (!paths.config)
7343
8413
  throw new Error("OpenCode did not report its config path");
7344
- assertVersion(await client.version());
8414
+ const configRoot = secureCanonicalPath(paths.config, "OpenCode config root");
7345
8415
  preflightGlobalConfig(configRoot);
8416
+ const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
8417
+ assertVersion(await client.version());
7346
8418
  const before = loadConfig(input.cwd, { configRoot, includeProject: false });
7347
- writeManagedAgents({ configRoot, agents: before.agents, check: true });
8419
+ const schemaSource = join8(dirname6(before.sources[0]), "schema.json");
8420
+ const packagedSchema = readFileSync7(schemaSource, "utf8");
8421
+ const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
8422
+ const preview = writeManagedAgents({ configRoot, agents: before.agents, check: true });
7348
8423
  const profile = await selectProfile(input, client, configRoot);
7349
8424
  if (!profile)
7350
8425
  return { status: "cancelled" };
7351
- input.output?.(`Register ${PACKAGE_SPEC}
7352
- Write ${join5(configRoot, "gvozd", "config.jsonc")}
7353
- Write managed agents in ${join5(configRoot, "agents")}`);
8426
+ input.output?.([
8427
+ `Register ${PACKAGE_SPEC}`,
8428
+ `Write ${join8(configRoot, "gvozd", "config.jsonc")}`,
8429
+ `Write managed agents in ${join8(configRoot, "agents")}`,
8430
+ ...preview.removed.length > 0 ? [`Remove ${preview.removed.length} stale or disabled managed agent(s)`] : []
8431
+ ].join(`
8432
+ `));
7354
8433
  if (!await confirm2(input, "Run setup?"))
7355
8434
  return { status: "cancelled" };
7356
- await client.pluginAdd(PACKAGE_SPEC);
7357
- try {
7358
- const schemaSource = join5(dirname3(before.sources[0]), "schema.json");
7359
- writeGlobalConfig({ configRoot, profile, schemaSource: readFileSync6(schemaSource, "utf8") });
7360
- const configured = loadConfig(input.cwd, { configRoot, includeProject: false });
7361
- writeManagedAgents({ configRoot, agents: configured.agents });
7362
- await client.serviceRestart();
7363
- const report = await runDoctor({ client, configRoot, cwd: input.cwd });
7364
- return { status: "complete", report };
7365
- } catch (error) {
7366
- throw new Error(`Gvozd plugin was registered, but setup is incomplete: ${error instanceof Error ? error.message : String(error)}. Rerun: gvozd setup`);
7367
- }
8435
+ return withExclusiveFileLock(join8(configRoot, "gvozd", "setup.lock"), async () => {
8436
+ if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
8437
+ throw new Error("OpenCode config root changed while setup awaited the lock");
8438
+ const lockedSchema = readFileSync7(schemaSource, "utf8");
8439
+ const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
8440
+ if (!sameSnapshot(previewSnapshot, snapshot))
8441
+ throw new Error("Global Gvozd configuration changed while setup awaited confirmation; review and rerun setup");
8442
+ const lockedBefore = loadConfig(input.cwd, { configRoot, includeProject: false });
8443
+ writeManagedAgents({ configRoot, agents: lockedBefore.agents, check: true });
8444
+ const lockedProfile = input.yes ? await selectProfile(input, client, configRoot) : profile;
8445
+ if (!lockedProfile)
8446
+ throw new Error("Model profile changed while setup awaited the global lock; rerun setup");
8447
+ if (!sameSnapshot(snapshot, preflightGlobalConfig(configRoot, lockedSchema))) {
8448
+ throw new Error("Global Gvozd configuration changed during locked setup revalidation; review and rerun setup");
8449
+ }
8450
+ writeManagedAgents({ configRoot, agents: lockedBefore.agents, check: true });
8451
+ await client.pluginAdd(PACKAGE_SPEC);
8452
+ try {
8453
+ writeGlobalConfig({ configRoot, profile: lockedProfile, schemaSource: lockedSchema, snapshot });
8454
+ const configured = loadConfig(input.cwd, { configRoot, includeProject: false });
8455
+ writeManagedAgents({ configRoot, agents: configured.agents });
8456
+ await client.serviceRestart();
8457
+ const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
8458
+ return { status: "complete", report };
8459
+ } catch (error) {
8460
+ const detail = redactDiagnostic(error);
8461
+ throw new Error(`${PACKAGE_NAME} remains registered, but setup is incomplete: ${detail}. ` + `Managed files under ${configRoot} may be partially updated; no automatic rollback was attempted because concurrent or user changes cannot be distinguished safely. ` + "Fix the reported cause, then rerun: gvozd setup");
8462
+ }
8463
+ });
7368
8464
  }
7369
8465
  async function runConfigure(input) {
7370
8466
  const client = await (input.findClient ?? (() => findOpenCode()))();
7371
8467
  const paths = await client.debugPaths();
7372
- const configRoot = paths.config;
7373
- if (!configRoot)
8468
+ if (!paths.config)
7374
8469
  throw new Error("OpenCode did not report its config path");
7375
- assertVersion(await client.version());
8470
+ const configRoot = secureCanonicalPath(paths.config, "OpenCode config root");
7376
8471
  preflightGlobalConfig(configRoot);
8472
+ const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
8473
+ assertVersion(await client.version());
8474
+ const config = loadConfig(input.cwd, { configRoot, includeProject: false });
8475
+ const schemaSource = join8(dirname6(config.sources[0]), "schema.json");
8476
+ const packagedSchema = readFileSync7(schemaSource, "utf8");
8477
+ const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
7377
8478
  const profile = await selectProfile(input, client, configRoot);
7378
8479
  if (!profile || !await confirm2(input, "Apply model configuration?"))
7379
8480
  return { status: "cancelled" };
7380
- const config = loadConfig(input.cwd, { configRoot, includeProject: false });
7381
- const schemaSource = join5(dirname3(config.sources[0]), "schema.json");
7382
- writeGlobalConfig({ configRoot, profile, schemaSource: readFileSync6(schemaSource, "utf8") });
7383
- await client.serviceRestart();
7384
- const report = await runDoctor({ client, configRoot, cwd: input.cwd });
7385
- return { status: "complete", report };
8481
+ return withExclusiveFileLock(join8(configRoot, "gvozd", "setup.lock"), async () => {
8482
+ if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
8483
+ throw new Error("OpenCode config root changed while configuration awaited the lock");
8484
+ const lockedSchema = readFileSync7(schemaSource, "utf8");
8485
+ const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
8486
+ if (!sameSnapshot(previewSnapshot, snapshot))
8487
+ throw new Error("Global Gvozd configuration changed while configuration awaited confirmation; review and rerun");
8488
+ loadConfig(input.cwd, { configRoot, includeProject: false });
8489
+ const lockedProfile = input.yes ? await selectProfile(input, client, configRoot) : profile;
8490
+ if (!lockedProfile)
8491
+ throw new Error("Model profile changed while configuration awaited the global lock; rerun configuration");
8492
+ if (!sameSnapshot(snapshot, preflightGlobalConfig(configRoot, lockedSchema))) {
8493
+ throw new Error("Global Gvozd configuration changed during locked configuration revalidation; review and rerun");
8494
+ }
8495
+ writeGlobalConfig({ configRoot, profile: lockedProfile, schemaSource: lockedSchema, snapshot });
8496
+ await client.serviceRestart();
8497
+ const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
8498
+ return { status: "complete", report };
8499
+ });
7386
8500
  }
7387
8501
  function setupExitCode(result) {
7388
8502
  if (result.status === "cancelled")
@@ -7391,13 +8505,13 @@ function setupExitCode(result) {
7391
8505
  }
7392
8506
 
7393
8507
  // src/sync.ts
7394
- import { closeSync as closeSync3, lstatSync as lstatSync4, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
7395
- import { randomUUID as randomUUID3 } from "node:crypto";
7396
- import { basename, dirname as dirname4, join as join6, relative as relative2 } from "node:path";
8508
+ import { closeSync as closeSync4, lstatSync as lstatSync8, mkdirSync as mkdirSync4, openSync as openSync4, readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync as realpathSync4, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
8509
+ import { randomUUID as randomUUID4 } from "node:crypto";
8510
+ import { basename, dirname as dirname7, join as join9, relative as relative4 } from "node:path";
7397
8511
  function renderPluginEntrypoint(config, destination) {
7398
8512
  let moduleSpecifier = "agent-gvozd/server";
7399
- if (realpathSync2(config.packageRoot) === realpathSync2(config.projectRoot)) {
7400
- moduleSpecifier = relative2(destination, join6(config.packageRoot, "src", "index")).replaceAll("\\", "/");
8513
+ if (realpathSync4(config.packageRoot) === realpathSync4(config.projectRoot)) {
8514
+ moduleSpecifier = relative4(destination, join9(config.packageRoot, "src", "index")).replaceAll("\\", "/");
7401
8515
  if (!moduleSpecifier.startsWith("."))
7402
8516
  moduleSpecifier = `./${moduleSpecifier}`;
7403
8517
  }
@@ -7432,7 +8546,7 @@ function renderDiff(path, before, after) {
7432
8546
  }
7433
8547
  function stat2(path) {
7434
8548
  try {
7435
- return lstatSync4(path);
8549
+ return lstatSync8(path);
7436
8550
  } catch (error) {
7437
8551
  if (error.code === "ENOENT")
7438
8552
  return;
@@ -7440,9 +8554,9 @@ function stat2(path) {
7440
8554
  }
7441
8555
  }
7442
8556
  function safeDirectory(root, segments, create) {
7443
- let current = realpathSync2(root);
8557
+ let current = realpathSync4(root);
7444
8558
  for (const segment of segments) {
7445
- current = join6(current, segment);
8559
+ current = join9(current, segment);
7446
8560
  const currentStat = stat2(current);
7447
8561
  if (currentStat) {
7448
8562
  if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
@@ -7451,7 +8565,7 @@ function safeDirectory(root, segments, create) {
7451
8565
  continue;
7452
8566
  }
7453
8567
  if (create)
7454
- mkdirSync3(current);
8568
+ mkdirSync4(current);
7455
8569
  }
7456
8570
  return current;
7457
8571
  }
@@ -7464,145 +8578,191 @@ function assertRegularFile(path) {
7464
8578
  return true;
7465
8579
  }
7466
8580
  function createFile2(path, content) {
7467
- const descriptor = openSync3(path, "wx", 384);
8581
+ const descriptor = openSync4(path, "wx", 384);
7468
8582
  try {
7469
- writeFileSync3(descriptor, content);
8583
+ writeFileSync4(descriptor, content);
7470
8584
  } finally {
7471
- closeSync3(descriptor);
8585
+ closeSync4(descriptor);
7472
8586
  }
7473
8587
  }
7474
8588
  function replaceFile2(path, content) {
7475
- const temporary = `${path}.tmp-${process.pid}-${randomUUID3()}`;
8589
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID4()}`;
7476
8590
  createFile2(temporary, content);
7477
8591
  try {
7478
8592
  renameSync3(temporary, path);
7479
8593
  } catch (error) {
7480
8594
  try {
7481
- unlinkSync3(temporary);
8595
+ unlinkSync4(temporary);
7482
8596
  } catch {}
7483
8597
  throw error;
7484
8598
  }
7485
8599
  }
7486
- function ensureProjectTemplate(config, check) {
8600
+ function planProjectTemplate(config, result, check) {
7487
8601
  const directory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], !check);
7488
- const path = join6(directory, "config.jsonc");
7489
- if (check)
7490
- return;
7491
- if (!assertRegularFile(path)) {
7492
- createFile2(path, [
7493
- "{",
7494
- ' "$schema": "./schema.json",',
7495
- " // Project overrides are merged after built-in and global configuration.",
7496
- ' "agents": {}',
7497
- "}",
7498
- ""
7499
- ].join(`
7500
- `));
8602
+ const writes = [];
8603
+ const configPath = join9(directory, "config.jsonc");
8604
+ if (!assertRegularFile(configPath)) {
8605
+ result.created.push(configPath);
8606
+ writes.push({
8607
+ target: configPath,
8608
+ replace: false,
8609
+ content: [
8610
+ "{",
8611
+ ' "$schema": "./schema.json",',
8612
+ " // Project overrides are merged after built-in and global configuration.",
8613
+ ' "agents": {}',
8614
+ "}",
8615
+ ""
8616
+ ].join(`
8617
+ `)
8618
+ });
7501
8619
  }
7502
- const schemaSource = join6(dirname4(config.sources[0]), "schema.json");
7503
- const schemaTarget = join6(directory, "schema.json");
7504
- const schema = readFileSync7(schemaSource, "utf8");
7505
- if (assertRegularFile(schemaTarget))
7506
- replaceFile2(schemaTarget, schema);
7507
- else
7508
- createFile2(schemaTarget, schema);
8620
+ const schemaSource = join9(dirname7(config.sources[0]), "schema.json");
8621
+ const schemaTarget = join9(directory, "schema.json");
8622
+ const schema = readFileSync8(schemaSource, "utf8");
8623
+ if (!assertRegularFile(schemaTarget)) {
8624
+ result.created.push(schemaTarget);
8625
+ writes.push({ target: schemaTarget, content: schema, replace: false });
8626
+ } else {
8627
+ const current = readFileSync8(schemaTarget, "utf8");
8628
+ if (current !== schema) {
8629
+ if (!hasGeneratedSchemaMarker(current) && !isEquivalentLegacySchema(current, schema)) {
8630
+ throw new Error(`Refusing to overwrite an unmanaged project schema: ${schemaTarget}`);
8631
+ }
8632
+ result.updated.push(schemaTarget);
8633
+ writes.push({ target: schemaTarget, content: schema, replace: true, previous: current });
8634
+ }
8635
+ }
8636
+ return writes;
7509
8637
  }
7510
- function syncAgents(config, options = {}) {
8638
+ function syncAgentsUnlocked(config, options) {
7511
8639
  const check = options.check ?? false;
7512
8640
  const destination = safeDirectory(config.projectRoot, [".opencode", "agents"], false);
7513
8641
  const pluginDestination = safeDirectory(config.projectRoot, [".opencode", "plugins", "agent-gvozd"], false);
7514
8642
  const result = { created: [], updated: [], removed: [], unchanged: [] };
7515
8643
  const writes = [];
7516
8644
  let pluginWrite;
8645
+ const removals = new Map;
8646
+ const templateWrites = planProjectTemplate(config, result, check);
7517
8647
  const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
7518
8648
  if (stat2(destination)) {
7519
- for (const entry of readdirSync2(destination, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
8649
+ for (const entry of readdirSync5(destination, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
7520
8650
  if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
7521
8651
  continue;
7522
- const target = join6(destination, entry.name);
7523
- const current = readFileSync7(target, "utf8");
7524
- if (!current.includes(GENERATED_MARKER))
8652
+ const target = join9(destination, entry.name);
8653
+ const current = readFileSync8(target, "utf8");
8654
+ if (!hasGeneratedAgentMarker(current))
7525
8655
  continue;
7526
8656
  result.removed.push(target);
8657
+ removals.set(target, current);
7527
8658
  options.onDiff?.(renderDiff(target, current, ""));
7528
8659
  }
7529
8660
  }
7530
8661
  for (const [id, agent] of Object.entries(config.agents)) {
7531
8662
  if (agent.disabled)
7532
8663
  continue;
7533
- const target = join6(destination, `${id}.md`);
8664
+ const target = join9(destination, `${id}.md`);
7534
8665
  const content = renderAgent(agent);
7535
8666
  if (!assertRegularFile(target)) {
7536
8667
  result.created.push(target);
7537
8668
  writes.push({ target, content, replace: false });
7538
8669
  continue;
7539
8670
  }
7540
- const current = readFileSync7(target, "utf8");
8671
+ const current = readFileSync8(target, "utf8");
7541
8672
  if (current === content) {
7542
8673
  result.unchanged.push(target);
7543
8674
  continue;
7544
8675
  }
7545
- if (!current.includes(GENERATED_MARKER)) {
8676
+ if (!hasGeneratedAgentMarker(current)) {
7546
8677
  throw new Error(`Refusing to overwrite a non-generated agent file: ${target}`);
7547
8678
  }
7548
8679
  result.updated.push(target);
7549
8680
  options.onDiff?.(renderDiff(target, current, content));
7550
- writes.push({ target, content, replace: true });
8681
+ writes.push({ target, content, replace: true, previous: current });
7551
8682
  }
7552
- const pluginTarget = join6(pluginDestination, "index.ts");
8683
+ const pluginTarget = join9(pluginDestination, "index.ts");
7553
8684
  const pluginContent = renderPluginEntrypoint(config, pluginDestination);
7554
8685
  if (!assertRegularFile(pluginTarget)) {
7555
8686
  result.created.push(pluginTarget);
7556
8687
  pluginWrite = { target: pluginTarget, content: pluginContent, replace: false };
7557
8688
  } else {
7558
- const current = readFileSync7(pluginTarget, "utf8");
8689
+ const current = readFileSync8(pluginTarget, "utf8");
7559
8690
  if (current === pluginContent) {
7560
8691
  result.unchanged.push(pluginTarget);
7561
8692
  } else {
7562
- if (!current.includes(GENERATED_PLUGIN_MARKER)) {
8693
+ if (!hasGeneratedPluginMarker(current)) {
7563
8694
  throw new Error(`Refusing to overwrite a non-generated plugin entrypoint: ${pluginTarget}`);
7564
8695
  }
7565
8696
  result.updated.push(pluginTarget);
7566
8697
  options.onDiff?.(renderDiff(pluginTarget, current, pluginContent));
7567
- pluginWrite = { target: pluginTarget, content: pluginContent, replace: true };
8698
+ pluginWrite = { target: pluginTarget, content: pluginContent, replace: true, previous: current };
7568
8699
  }
7569
8700
  }
7570
8701
  if (!check) {
7571
8702
  const writableDestination = safeDirectory(config.projectRoot, [".opencode", "agents"], true);
7572
8703
  const writablePluginDestination = safeDirectory(config.projectRoot, [".opencode", "plugins", "agent-gvozd"], true);
7573
8704
  for (const target of result.removed) {
7574
- if (!assertRegularFile(target) || !readFileSync7(target, "utf8").includes(GENERATED_MARKER)) {
8705
+ if (!assertRegularFile(target)) {
7575
8706
  throw new Error(`Refusing to remove a changed or unsafe agent file: ${target}`);
7576
8707
  }
7577
- unlinkSync3(target);
8708
+ const current = readFileSync8(target, "utf8");
8709
+ if (!hasGeneratedAgentMarker(current) || current !== removals.get(target)) {
8710
+ throw new Error(`Refusing to remove a concurrently changed agent file: ${target}`);
8711
+ }
8712
+ unlinkSync4(target);
7578
8713
  }
7579
8714
  for (const write of writes) {
7580
8715
  if (!write.replace) {
7581
- createFile2(join6(writableDestination, basename(write.target)), write.content);
8716
+ createFile2(join9(writableDestination, basename(write.target)), write.content);
7582
8717
  continue;
7583
8718
  }
7584
- if (!assertRegularFile(write.target) || !readFileSync7(write.target, "utf8").includes(GENERATED_MARKER)) {
8719
+ if (!assertRegularFile(write.target)) {
7585
8720
  throw new Error(`Refusing to replace a changed or unsafe agent file: ${write.target}`);
7586
8721
  }
8722
+ const current = readFileSync8(write.target, "utf8");
8723
+ if (!hasGeneratedAgentMarker(current) || current !== write.previous) {
8724
+ throw new Error(`Refusing to replace a concurrently changed agent file: ${write.target}`);
8725
+ }
7587
8726
  replaceFile2(write.target, write.content);
7588
8727
  }
7589
8728
  if (pluginWrite) {
7590
- const target = join6(writablePluginDestination, basename(pluginWrite.target));
8729
+ const target = join9(writablePluginDestination, basename(pluginWrite.target));
7591
8730
  if (!pluginWrite.replace) {
7592
8731
  createFile2(target, pluginWrite.content);
7593
8732
  } else {
7594
- if (!assertRegularFile(target) || !readFileSync7(target, "utf8").includes(GENERATED_PLUGIN_MARKER)) {
8733
+ if (!assertRegularFile(target)) {
7595
8734
  throw new Error(`Refusing to replace a changed or unsafe plugin entrypoint: ${target}`);
7596
8735
  }
8736
+ const current = readFileSync8(target, "utf8");
8737
+ if (!hasGeneratedPluginMarker(current) || current !== pluginWrite.previous) {
8738
+ throw new Error(`Refusing to replace a concurrently changed plugin entrypoint: ${target}`);
8739
+ }
7597
8740
  replaceFile2(target, pluginWrite.content);
7598
8741
  }
7599
8742
  }
8743
+ const templateDirectory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], true);
8744
+ for (const write of templateWrites) {
8745
+ const target = join9(templateDirectory, basename(write.target));
8746
+ if (!write.replace)
8747
+ createFile2(target, write.content);
8748
+ else {
8749
+ if (!assertRegularFile(target) || readFileSync8(target, "utf8") !== write.previous) {
8750
+ throw new Error(`Refusing to replace a concurrently changed project schema: ${target}`);
8751
+ }
8752
+ replaceFile2(target, write.content);
8753
+ }
8754
+ }
7600
8755
  }
7601
- ensureProjectTemplate(config, check);
7602
8756
  if (!check)
7603
8757
  safeDirectory(config.projectRoot, ["docs", ".gvozd", "tasks"], true);
7604
8758
  return result;
7605
8759
  }
8760
+ function syncAgents(config, options = {}) {
8761
+ if (options.check)
8762
+ return syncAgentsUnlocked(config, options);
8763
+ const root = realpathSync4(config.projectRoot);
8764
+ return withExclusiveFileLockSync(join9(root, ".agent-gvozd-sync.lock"), () => syncAgentsUnlocked(config, options), "sync");
8765
+ }
7606
8766
  function formatSyncResult(result, check) {
7607
8767
  const lines = [check ? "agent-gvozd sync check" : "agent-gvozd sync complete"];
7608
8768
  for (const [label, paths] of [
@@ -7631,8 +8791,23 @@ var promptUI = {
7631
8791
  intro,
7632
8792
  outro
7633
8793
  };
8794
+ var HELP = [
8795
+ "Usage: gvozd <setup|config|doctor|sync|trust-project> [options]",
8796
+ "",
8797
+ "Commands:",
8798
+ " setup [--yes] Install or upgrade the global agent team",
8799
+ " config [--yes] Configure model preferences",
8800
+ " doctor [--json] Diagnose the global installation",
8801
+ " sync [--check] Maintain the legacy project-local installation",
8802
+ " trust-project [directory] Print the current project trust token",
8803
+ "",
8804
+ "Options:",
8805
+ " --help Show this help",
8806
+ " --version Show the Gvozd version"
8807
+ ].join(`
8808
+ `);
7634
8809
  function usage(io) {
7635
- io.stderr("Usage: gvozd <setup|config|doctor|sync> [options]");
8810
+ io.stderr(HELP);
7636
8811
  return 2;
7637
8812
  }
7638
8813
  function parseFlags(args, allowed) {
@@ -7644,6 +8819,14 @@ function parseFlags(args, allowed) {
7644
8819
  async function runCli(args, io = defaultIO, commands = { setup: runSetup, configure: runConfigure }) {
7645
8820
  const [command, ...rest] = args;
7646
8821
  try {
8822
+ if ((command === "--help" || command === "help") && rest.length === 0) {
8823
+ io.stdout(HELP);
8824
+ return 0;
8825
+ }
8826
+ if (command === "--version" && rest.length === 0) {
8827
+ io.stdout(PACKAGE_VERSION);
8828
+ return 0;
8829
+ }
7647
8830
  if (command === "setup" || command === "config") {
7648
8831
  const parsed = parseFlags(rest, ["--yes"]);
7649
8832
  if (!parsed || parsed.positional.length > 0)
@@ -7672,7 +8855,12 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
7672
8855
  try {
7673
8856
  const client = await (commands.findClient ?? findOpenCode)();
7674
8857
  const paths = await client.debugPaths();
7675
- report = await runDoctor({ client, configRoot: paths.config, cwd: io.cwd() });
8858
+ report = await runDoctor({
8859
+ client,
8860
+ configRoot: paths.config,
8861
+ runtimeConfigRoot: resolveOpenCodeConfigRoot(),
8862
+ cwd: io.cwd()
8863
+ });
7676
8864
  } catch (error) {
7677
8865
  report = doctorOperationalFailure(error);
7678
8866
  }
@@ -7690,14 +8878,21 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
7690
8878
  io.stdout(formatSyncResult(result, check));
7691
8879
  return check && result.created.length + result.updated.length + result.removed.length > 0 ? 1 : 0;
7692
8880
  }
8881
+ if (command === "trust-project") {
8882
+ const parsed = parseFlags(rest, []);
8883
+ if (!parsed || parsed.positional.length > 1)
8884
+ return usage(io);
8885
+ io.stdout(computeProjectTrustToken(parsed.positional[0] ?? io.cwd()));
8886
+ return 0;
8887
+ }
7693
8888
  return usage(io);
7694
8889
  } catch (error) {
7695
- io.stderr(error instanceof Error ? error.message : String(error));
8890
+ io.stderr(redactDiagnostic(error));
7696
8891
  return 1;
7697
8892
  }
7698
8893
  }
7699
8894
  var invokedPath = process.argv[1];
7700
- if (invokedPath && realpathSync3(invokedPath) === realpathSync3(fileURLToPath2(import.meta.url))) {
8895
+ if (invokedPath && realpathSync5(invokedPath) === realpathSync5(fileURLToPath2(import.meta.url))) {
7701
8896
  process.exitCode = await runCli(process.argv.slice(2));
7702
8897
  }
7703
8898
  export {