@pmelab/gtd 21.0.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32693,6 +32693,71 @@ const requireRevert = (edited, base) => {
32693
32693
  refuse(`gtd land: require-revert: ${residue.join(", ")} still differ from ${base}~1 — the revert did not take. Run \`git checkout ${base}~1 -- ${quoted}\`, then \`gtd land\` again.`);
32694
32694
  };
32695
32695
  //#endregion
32696
+ //#region src/flows/factory.ts
32697
+ const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory");
32698
+ /**
32699
+ * Define a workflow configured by options. `schema` is any Standard Schema
32700
+ * (https://standardschema.dev); it should reject unknown keys, so a removed or
32701
+ * misspelled option fails at load. Exported from `gtd.config.ts`, the factory
32702
+ * itself and a call's result are both workflows.
32703
+ */
32704
+ const defineWorkflow = (schema, build) => {
32705
+ const internals = {
32706
+ schema,
32707
+ build
32708
+ };
32709
+ const factory = (options) => ({
32710
+ [FACTORY]: {
32711
+ ...internals,
32712
+ options
32713
+ },
32714
+ then: () => {
32715
+ throw new Error("a configured workflow is not a flow — compose fixFlow/reviewFlow instead");
32716
+ }
32717
+ });
32718
+ return Object.assign(factory, { [FACTORY]: internals });
32719
+ };
32720
+ const internalsOf = (value) => (typeof value === "function" || typeof value === "object" && value !== null) && FACTORY in value ? value[FACTORY] : void 0;
32721
+ const isFactoryWorkflow = (value) => internalsOf(value) !== void 0;
32722
+ const segment = (key) => {
32723
+ const raw = typeof key === "object" ? key.key : key;
32724
+ return typeof raw === "number" ? raw : String(raw);
32725
+ };
32726
+ /**
32727
+ * Validate a factory workflow's options — those it was called with, `overrides`
32728
+ * on top — and build it. `undefined` for an export that is no factory workflow.
32729
+ */
32730
+ const configure$1 = (value, overrides = {}) => {
32731
+ const internals = internalsOf(value);
32732
+ if (internals === void 0) return void 0;
32733
+ const options = {
32734
+ ...internals.options,
32735
+ ...overrides
32736
+ };
32737
+ const result = internals.schema["~standard"].validate(options);
32738
+ if (result instanceof Promise) return {
32739
+ ok: false,
32740
+ issues: [{
32741
+ path: [],
32742
+ message: "the options schema validates asynchronously — gtd needs a synchronous one"
32743
+ }]
32744
+ };
32745
+ if (result.issues !== void 0) return {
32746
+ ok: false,
32747
+ issues: result.issues.length === 0 ? [{
32748
+ path: [],
32749
+ message: "the options schema rejected them without saying why"
32750
+ }] : result.issues.map((issue) => ({
32751
+ path: (issue.path ?? []).map(segment),
32752
+ message: issue.message
32753
+ }))
32754
+ };
32755
+ return {
32756
+ ok: true,
32757
+ build: internals.build(result.value)
32758
+ };
32759
+ };
32760
+ //#endregion
32696
32761
  //#region src/flows/index.ts
32697
32762
  var flows_exports = /* @__PURE__ */ __exportAll({
32698
32763
  FLOWS_PROTOCOL: () => 1,
@@ -32706,12 +32771,15 @@ var flows_exports = /* @__PURE__ */ __exportAll({
32706
32771
  checkScript: () => checkScript,
32707
32772
  codeChanges: () => codeChanges,
32708
32773
  codeThreads: () => codeThreads,
32774
+ configure: () => configure$1,
32775
+ defineWorkflow: () => defineWorkflow,
32709
32776
  env: () => env,
32710
32777
  glob: () => glob,
32711
32778
  hasThreadFor: () => hasThreadFor,
32712
32779
  head: () => head,
32713
32780
  human: () => human,
32714
32781
  installContext: () => installContext,
32782
+ isFactoryWorkflow: () => isFactoryWorkflow,
32715
32783
  judge: () => judge,
32716
32784
  moveScript: () => moveScript,
32717
32785
  numeric: () => numeric,
@@ -49589,10 +49657,10 @@ const replay = async (input) => {
49589
49657
  };
49590
49658
  //#endregion
49591
49659
  //#region src/workflow/ConfigSchema.ts
49592
- /** The `vars:` shape: a flat name -> scalar map (`compileVarsMap` coerces every scalar to a string). */
49660
+ /** The `vars:` shape: a flat name -> scalar map (`compileVarsMap` coerces every scalar to a string). Its names are the workflow's business, never core's. */
49593
49661
  const varsJsonSchema = {
49594
49662
  type: "object",
49595
- description: "Flat name -> scalar map merged into the workflow's process settings (defaults), pinned for the whole process at its start. Scalars are coerced to strings.",
49663
+ description: "Flat name -> scalar map merged into the workflow's process settings (defaults), pinned for the whole process at its start. A workflow built by a factory receives these entries as options and validates them itself. Scalars are coerced to strings.",
49596
49664
  additionalProperties: { type: [
49597
49665
  "string",
49598
49666
  "number",
@@ -49602,7 +49670,7 @@ const varsJsonSchema = {
49602
49670
  /** The `env:` shape: environment settings, read live on every invocation and never pinned to a process. */
49603
49671
  const envJsonSchema = {
49604
49672
  type: "object",
49605
- description: "Flat name -> scalar map merged into the workflow's environment settings (envDefaults) — values that change how a step runs on this machine, like the test command or a model hint. Read fresh on every gtd call, never pinned to a process. GTD_<NAME> environment variables override these entries. Scalars are coerced to strings.",
49673
+ description: "Flat name -> scalar map merged into the workflow's environment settings (envDefaults) — values that change how a step runs on this machine. A workflow built by a factory receives these entries as options and validates them itself. Read fresh on every gtd call, never pinned to a process. GTD_<NAME> environment variables override these entries. Scalars are coerced to strings.",
49606
49674
  additionalProperties: { type: [
49607
49675
  "string",
49608
49676
  "number",
@@ -49676,9 +49744,9 @@ const UiSchema = Struct({
49676
49744
  format: optional$2(String$.annotations({ description: "Shell command run after every UI write, before it resolves ($GTD_FILE is the written file's absolute path). A non-zero exit or missing binary never reverts the write or refuses it — it's reported to the client as a notice naming the command and its exit code. Absent means no command runs at all." }))
49677
49745
  }).annotations({ description: "Settings for `gtd ui`: where it listens, and optional TLS." });
49678
49746
  const JudgeSchema = Struct({
49679
- provider: optional$2(Literal("fixed", "jev", "llm").annotations({ description: "Judge provider `gtd judge run` uses when no --provider flag and no GTD_JUDGE_PROVIDER is set. Absent means auto: jev when TYPESAFE_API_KEY is set, otherwise llm." })),
49680
- model: optional$2(String$.annotations({ description: "Model for provider llm, used when no --model flag and no GTD_JUDGE_MODEL is set. Pairing it with another provider is an error." }))
49681
- }).annotations({ description: "Which judge `gtd judge run` uses, when no flag or GTD_JUDGE_* variable says." });
49747
+ provider: optional$2(Literal("fixed", "jev", "llm").annotations({ description: "Judge provider `gtd judge run` uses when no --provider flag, no GTD_JUDGE_PROVIDER and no user-level judge.json says. Absent means auto: jev when TYPESAFE_API_KEY is set, otherwise llm." })),
49748
+ model: optional$2(String$.annotations({ description: "Model for provider llm, used when no --model flag, no GTD_JUDGE_MODEL and no user-level judge.json says. Pairing it with another provider is an error." }))
49749
+ }).annotations({ description: "Which judge `gtd judge run` uses, when no flag, GTD_JUDGE_* variable or user-level judge.json says." });
49682
49750
  const ConfigSchema = Struct({
49683
49751
  vars: optional$2(Unknown.annotations({ jsonSchema: varsJsonSchema })),
49684
49752
  env: optional$2(Unknown.annotations({ jsonSchema: envJsonSchema })),
@@ -57008,11 +57076,12 @@ const load = gen(function* () {
57008
57076
  catch: (e) => new GtdError(`gtd: ${e instanceof Error ? e.message : String(e)}`)
57009
57077
  });
57010
57078
  const loaded = yield* try_({
57011
- try: () => loadWorkflows(module, shipped),
57079
+ try: () => loadWorkflows(module, shipped, compiled),
57012
57080
  catch: (e) => new GtdError(`gtd config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
57013
57081
  });
57014
- const diagnostics = report(wrongKindDiagnostics(loaded.file, compiled), [...rcOrder, module.filepath]);
57082
+ const diagnostics = report([...loaded.diagnostics, ...wrongKindDiagnostics(loaded.settings, compiled)], [...rcOrder, module.filepath]);
57015
57083
  yield* failOnErrors(diagnostics);
57084
+ if (loaded.default === void 0) return yield* die("unreachable: a default fails to build only with an error reported");
57016
57085
  const initial = yield* firstStep(loaded.default, headTree(yield* Workspace));
57017
57086
  const steering = loaded.file.steering;
57018
57087
  const define = (entry) => ({
@@ -57034,8 +57103,8 @@ const load = gen(function* () {
57034
57103
  const resolve = (name, entry) => ({
57035
57104
  name,
57036
57105
  def: define(entry),
57037
- vars: { ...entry.file.defaults },
57038
- env: { ...entry.file.envDefaults }
57106
+ vars: { ...entry.defaults },
57107
+ env: { ...entry.envDefaults }
57039
57108
  });
57040
57109
  const defaultWorkflow = resolve(loaded.default.name, loaded.default);
57041
57110
  return {
@@ -57056,9 +57125,9 @@ const load = gen(function* () {
57056
57125
  };
57057
57126
  });
57058
57127
  /** A name under the wrong key (or under both) in a `.gtdrc` layer: each is an error at its own origin. */
57059
- const wrongKindDiagnostics = (file, compiled) => {
57060
- const isEnvOnly = (key) => Object.hasOwn(file.envDefaults, key);
57061
- const isVarOnly = (key) => Object.hasOwn(file.defaults, key);
57128
+ const wrongKindDiagnostics = (settings, compiled) => {
57129
+ const isEnvOnly = (key) => Object.hasOwn(settings.envDefaults, key);
57130
+ const isVarOnly = (key) => Object.hasOwn(settings.defaults, key);
57062
57131
  const error = (path, message, origin) => ({
57063
57132
  severity: "error",
57064
57133
  path,
@@ -57179,16 +57248,20 @@ const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
57179
57248
  "access",
57180
57249
  "doors"
57181
57250
  ]);
57251
+ /** The rules every declared setting meets, whether a file's export or a factory's build. */
57252
+ const checkSettings = (defaults, envDefaults, declares) => {
57253
+ for (const [kind, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) for (const [name, setting] of Object.entries(record)) {
57254
+ if (!isSettingName(name)) throw new Error(`${declares(kind)} ${JSON.stringify(name)}, not a valid setting name — ${SETTING_NAME_RULE}`);
57255
+ if (typeof setting !== "string") throw new Error(`${declares(kind)} ${JSON.stringify(name)}, whose value is not a string`);
57256
+ }
57257
+ const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
57258
+ if (both.length > 0) throw new Error(`${both.map((name) => `"${name}"`).join(", ")} declared in both "defaults" and "envDefaults" — a setting is a process setting or an environment setting, not both`);
57259
+ };
57182
57260
  /** A module's per-file exports: `defaults`, `envDefaults`, `steering`, `skills`, `access`, `summary`, `base`. */
57183
57261
  const fileFrom = (exports, origin, dependencyFiles) => {
57184
57262
  const defaults = stringRecord(exports, "defaults");
57185
57263
  const envDefaults = stringRecord(exports, "envDefaults");
57186
- for (const [exportName, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) {
57187
- const bad = Object.keys(record).find((name) => !isSettingName(name));
57188
- if (bad !== void 0) throw new Error(`the "${exportName}" export declares ${JSON.stringify(bad)}, not a valid setting name — ${SETTING_NAME_RULE}`);
57189
- }
57190
- const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
57191
- if (both.length > 0) throw new Error(`${both.map((name) => `"${name}"`).join(", ")} declared in both "defaults" and "envDefaults" — a setting is a process setting or an environment setting, not both`);
57264
+ checkSettings(defaults, envDefaults, (kind) => `the "${kind}" export declares`);
57192
57265
  return {
57193
57266
  defaults,
57194
57267
  envDefaults,
@@ -57243,43 +57316,172 @@ const checkDoors = (declared, catalogue) => {
57243
57316
  };
57244
57317
  const namespaceOf = (exported) => typeof exported === "object" && exported !== null ? exported : {};
57245
57318
  /**
57246
- * Evaluate `gtd.config.ts`. Every exported function but the reserved names is
57247
- * a workflow named by its export. `cache` is jiti's OWN per-call module cache
57248
- * (unrelated to the instance's `moduleCache: false`) — passing an empty object
57249
- * in has the side effect of collecting every real file jiti resolved
57250
- * evaluating this module, module AND every relative import it pulled in,
57251
- * transitively.
57319
+ * Evaluate `gtd.config.ts`. Every exported function or factory workflow but
57320
+ * the reserved names is a workflow named by its export. `cache` is jiti's OWN
57321
+ * per-call module cache (unrelated to the instance's `moduleCache: false`) —
57322
+ * passing an empty object in has the side effect of collecting every real file
57323
+ * jiti resolved evaluating this module, module AND every relative import it
57324
+ * pulled in, transitively.
57252
57325
  */
57253
- const loadWorkflows = (module, shipped) => {
57326
+ const loadWorkflows = (module, shipped, compiled) => {
57254
57327
  const cache = {};
57255
57328
  const exports = namespaceOf(jiti(shipped).evalModule(module.source, {
57256
57329
  filename: module.filepath,
57257
57330
  cache
57258
57331
  }));
57259
57332
  const file = fileFrom(exports, module.filepath, Object.keys(cache));
57260
- const catalogue = /* @__PURE__ */ new Map();
57261
- for (const [name, value] of Object.entries(exports)) {
57262
- if (RESERVED_EXPORTS.has(name) || typeof value !== "function") continue;
57263
- catalogue.set(name, {
57264
- flow: value,
57265
- file
57333
+ const exported = Object.entries(exports).filter(([name, value]) => !RESERVED_EXPORTS.has(name) && isWorkflow(value));
57334
+ const value = defaultExport(exports);
57335
+ const defaultName = exported.find(([, other]) => other === value)?.[0] ?? "default";
57336
+ const built = /* @__PURE__ */ new Map();
57337
+ const rejections = [];
57338
+ const declarers = declarersOf([...exported, [defaultName, value]], file, compiled);
57339
+ for (const [name, entry] of [...exported, [defaultName, value]]) if (!built.has(entry)) built.set(entry, buildWorkflow(name, entry, file, compiled, rejections, declarers));
57340
+ const catalogue = new Map(exported.flatMap(([name, entry]) => {
57341
+ const workflow = built.get(entry);
57342
+ return workflow === void 0 ? [] : [[name, workflow]];
57343
+ }));
57344
+ const defaultWorkflow = built.get(value);
57345
+ return {
57346
+ file,
57347
+ catalogue,
57348
+ doors: rejections.length > 0 ? /* @__PURE__ */ new Map() : checkDoors(doorsFrom(exports), catalogue),
57349
+ default: defaultWorkflow && {
57350
+ ...defaultWorkflow,
57351
+ name: defaultName
57352
+ },
57353
+ settings: mergeSettings([...built.values()].filter((w) => w !== void 0)),
57354
+ diagnostics: nameRejections(rejections)
57355
+ };
57356
+ };
57357
+ const isWorkflow = (value) => typeof value === "function" || isFactoryWorkflow(value);
57358
+ const defaultExport = (exports) => {
57359
+ const value = exports.default;
57360
+ if (value === void 0) throw new Error("no default export — export the workflow an ordinary start runs (`gtd install` scaffolds one)");
57361
+ if (!isWorkflow(value)) throw new Error("the default export is not a function");
57362
+ return value;
57363
+ };
57364
+ const mergeSettings = (all) => ({
57365
+ defaults: Object.assign({}, ...all.map((s) => s.defaults)),
57366
+ envDefaults: Object.assign({}, ...all.map((s) => s.envDefaults))
57367
+ });
57368
+ /** Each setting name, with who declares it: the file's exports, or a factory's own build. */
57369
+ const declarersOf = (exported, file, compiled) => {
57370
+ const declarers = /* @__PURE__ */ new Map();
57371
+ const declare = (key, who) => {
57372
+ declarers.set(key, (declarers.get(key) ?? /* @__PURE__ */ new Set()).add(who));
57373
+ };
57374
+ for (const key of [...Object.keys(file.defaults), ...Object.keys(file.envDefaults)]) declare(key, file);
57375
+ for (const [, value] of exported) {
57376
+ const own = ownBuild(value, {
57377
+ ...compiled.rcVars,
57378
+ ...compiled.rcEnv
57266
57379
  });
57380
+ if (own?.ok) for (const key of [...Object.keys(own.build.defaults ?? {}), ...Object.keys(own.build.envDefaults ?? {})]) declare(key, value);
57381
+ }
57382
+ return declarers;
57383
+ };
57384
+ /**
57385
+ * A factory's build as its call alone gives it — or, when the call fails, with
57386
+ * the `.gtdrc` values for just the options it flagged, since a name the factory
57387
+ * itself rejects is its own.
57388
+ */
57389
+ const ownBuild = (value, rc) => {
57390
+ const bare = configure$1(value);
57391
+ if (bare === void 0 || bare.ok) return bare;
57392
+ const flagged = new Set(bare.issues.map((issue) => issue.path[0]));
57393
+ return configure$1(value, Object.fromEntries(Object.entries(rc).filter(([key]) => flagged.has(key))));
57394
+ };
57395
+ /**
57396
+ * One exported workflow: a plain flow as-is, a factory validated and built.
57397
+ * `.gtdrc` `vars:`/`env:` values reach a factory as options, so the factory
57398
+ * judges them — except a name some other loaded workflow declares and this one
57399
+ * does not, which belongs to that workflow.
57400
+ */
57401
+ const buildWorkflow = (name, value, file, compiled, rejections, declarers) => {
57402
+ if (configure$1(value) === void 0) return {
57403
+ flow: value,
57404
+ file,
57405
+ defaults: file.defaults,
57406
+ envDefaults: file.envDefaults
57407
+ };
57408
+ const belongsElsewhere = (key) => {
57409
+ const who = declarers.get(key);
57410
+ return who !== void 0 && !who.has(value);
57411
+ };
57412
+ const overrides = Object.fromEntries(Object.entries({
57413
+ ...compiled.rcVars,
57414
+ ...compiled.rcEnv
57415
+ }).filter(([key]) => !belongsElsewhere(key)));
57416
+ const result = configure$1(value, overrides);
57417
+ if (!result.ok) {
57418
+ for (const issue of result.issues) rejections.push(optionDiagnostic(name, issue, overrides, file, compiled));
57419
+ return;
57267
57420
  }
57268
- const flow = exports.default;
57269
- if (flow === void 0) throw new Error("no default export — export the workflow an ordinary start runs (`gtd install` scaffolds one)");
57270
- if (typeof flow !== "function") throw new Error("the default export is not a function");
57271
- const name = [...catalogue].find(([, entry]) => entry.flow === flow)?.[0] ?? "default";
57272
57421
  return {
57422
+ flow: result.build.flow,
57273
57423
  file,
57274
- catalogue,
57275
- doors: checkDoors(doorsFrom(exports), catalogue),
57276
- default: {
57277
- flow,
57278
- file,
57279
- name
57280
- }
57424
+ ...builtSettings(name, file, result.build)
57425
+ };
57426
+ };
57427
+ /** A factory's settings over its file's, held to the rules a file's own exports are. */
57428
+ const builtSettings = (name, file, build) => {
57429
+ checkSettings(build.defaults ?? {}, build.envDefaults ?? {}, (kind) => `workflow "${name}" builds ${kind}`);
57430
+ const defaults = {
57431
+ ...file.defaults,
57432
+ ...build.defaults
57433
+ };
57434
+ const envDefaults = {
57435
+ ...file.envDefaults,
57436
+ ...build.envDefaults
57437
+ };
57438
+ checkSettings(defaults, envDefaults, (kind) => `workflow "${name}" builds ${kind}`);
57439
+ return {
57440
+ defaults,
57441
+ envDefaults
57281
57442
  };
57282
57443
  };
57444
+ /** A rejected option, reported where its value came from: the innermost `.gtdrc` that set it, else `gtd.config.ts`. */
57445
+ const optionDiagnostic = (workflow, issue, overrides, file, compiled) => {
57446
+ const [key] = issue.path;
57447
+ const message = `${issue.path.length > 0 ? `option "${issue.path.join(".")}"` : "options"}: ${issue.message}`;
57448
+ if (!(typeof key === "string" && Object.hasOwn(overrides, key))) return {
57449
+ workflow,
57450
+ severity: "error",
57451
+ path: issue.path,
57452
+ message,
57453
+ origin: file.origin
57454
+ };
57455
+ const kind = Object.hasOwn(compiled.rcEnv, key) ? "env" : "vars";
57456
+ const origin = [...kind === "env" ? compiled.envKeys : compiled.varsKeys].reverse().find((entry) => entry.key === key)?.origin;
57457
+ return {
57458
+ workflow,
57459
+ severity: "error",
57460
+ path: [kind, ...issue.path],
57461
+ message,
57462
+ origin: origin ?? file.origin
57463
+ };
57464
+ };
57465
+ const nameRejections = (rejections) => {
57466
+ const grouped = /* @__PURE__ */ new Map();
57467
+ for (const rejection of rejections) {
57468
+ const key = JSON.stringify([
57469
+ rejection.origin,
57470
+ rejection.path,
57471
+ rejection.message
57472
+ ]);
57473
+ const group = grouped.get(key);
57474
+ if (group === void 0) grouped.set(key, {
57475
+ first: rejection,
57476
+ workflows: [rejection.workflow]
57477
+ });
57478
+ else group.workflows.push(rejection.workflow);
57479
+ }
57480
+ return [...grouped.values()].map(({ first: { workflow: _, ...diagnostic }, workflows }) => ({
57481
+ ...diagnostic,
57482
+ message: `${workflows.length > 1 ? "workflows" : "workflow"} ${workflows.map((w) => `"${w}"`).join(", ")} ${diagnostic.message}`
57483
+ }));
57484
+ };
57283
57485
  const headTree = (workspace) => {
57284
57486
  let entries;
57285
57487
  const list = () => entries ??= workspace.treeSync("HEAD");
@@ -57748,23 +57950,73 @@ const envProvider = (env) => {
57748
57950
  return value;
57749
57951
  };
57750
57952
  const checkPairing = (resolved) => resolved.model !== void 0 && resolved.provider !== void 0 && resolved.provider !== "llm" ? fail(`a model only applies to provider llm, not ${resolved.provider}`) : resolved;
57953
+ const userConfigPath = (env) => {
57954
+ const xdg = env["XDG_CONFIG_HOME"];
57955
+ if (xdg) return `${xdg}/gtd/judge.json`;
57956
+ const home = env["HOME"];
57957
+ return home ? `${home}/.config/gtd/judge.json` : void 0;
57958
+ };
57959
+ const readText = async (path) => {
57960
+ try {
57961
+ return await readFile(path, "utf8");
57962
+ } catch (e) {
57963
+ if (e.code === "ENOENT") return void 0;
57964
+ return fail(`cannot read ${path}: ${e instanceof Error ? e.message : String(e)}`);
57965
+ }
57966
+ };
57967
+ const parseObject = (path, text) => {
57968
+ let json;
57969
+ try {
57970
+ json = JSON.parse(text);
57971
+ } catch {
57972
+ return fail(`${path} is not valid JSON`);
57973
+ }
57974
+ return typeof json === "object" && json !== null && !Array.isArray(json) ? json : fail(`${path} must be a JSON object`);
57975
+ };
57976
+ const decodeUserConfig = (path, text) => {
57977
+ const { provider, model, ...rest } = parseObject(path, text);
57978
+ const [extra] = Object.keys(rest);
57979
+ if (extra !== void 0) return fail(`${path}: unknown key "${extra}"`);
57980
+ if (provider !== void 0 && !PROVIDERS.includes(provider)) return fail(`${path}: "provider" must be one of ${PROVIDERS.join(", ")}`);
57981
+ if (model !== void 0 && typeof model !== "string") return fail(`${path}: "model" must be a string`);
57982
+ return {
57983
+ provider,
57984
+ model
57985
+ };
57986
+ };
57987
+ const readUserConfig = async (env) => {
57988
+ const path = userConfigPath(env);
57989
+ const text = path === void 0 ? void 0 : await readText(path);
57990
+ return path === void 0 || text === void 0 ? void 0 : decodeUserConfig(path, text);
57991
+ };
57751
57992
  /**
57752
57993
  * Per field, the first one set wins: flag, then `GTD_JUDGE_PROVIDER` /
57753
- * `GTD_JUDGE_MODEL` (empty counts as unset), then `.gtdrc` `judge:`. The
57754
- * provider/model pairing is checked on the resolved pair, whichever layer
57755
- * supplied each half.
57994
+ * `GTD_JUDGE_MODEL` (empty counts as unset), then the user file, then `.gtdrc`
57995
+ * `judge:`. The provider/model pairing is checked on the resolved pair,
57996
+ * whichever layer supplied each half.
57756
57997
  */
57757
- const selectJudge = ({ provider, model, configured, env }) => {
57758
- const resolved = {
57759
- provider: provider ?? envProvider(env) ?? configured?.provider,
57760
- model: model ?? (env["GTD_JUDGE_MODEL"] || void 0) ?? configured?.model
57761
- };
57762
- return checkPairing(resolved);
57998
+ const selectJudge = ({ provider, model, configured, env }, user) => {
57999
+ const layers = [
58000
+ {
58001
+ provider,
58002
+ model
58003
+ },
58004
+ {
58005
+ provider: envProvider(env),
58006
+ model: env["GTD_JUDGE_MODEL"] || void 0
58007
+ },
58008
+ user ?? {},
58009
+ configured ?? {}
58010
+ ];
58011
+ return checkPairing({
58012
+ provider: layers.find((l) => l.provider !== void 0)?.provider,
58013
+ model: layers.find((l) => l.model !== void 0)?.model
58014
+ });
57763
58015
  };
57764
58016
  /** The only place auto selection lives; it never falls back across providers. */
57765
- const answererFor = (opts) => {
58017
+ const answererFor = async (opts) => {
57766
58018
  const { answers, env, cwd } = opts;
57767
- const { provider, model } = selectJudge(opts);
58019
+ const { provider, model } = selectJudge(opts, await readUserConfig(env));
57768
58020
  if (provider === "fixed") return fixed({
57769
58021
  answersPath: answers,
57770
58022
  env
@@ -57785,7 +58037,7 @@ const runJudge = async (opts) => {
57785
58037
  }
57786
58038
  const { questions, state } = doc ?? {};
57787
58039
  if (!Array.isArray(questions)) return fail("stdin has no \"questions\" array");
57788
- const verdicts = await answererFor(opts)(questions, typeof state === "object" && state !== null ? state : {});
58040
+ const verdicts = await (await answererFor(opts))(questions, typeof state === "object" && state !== null ? state : {});
57789
58041
  return JSON.stringify(verdicts) + "\n";
57790
58042
  };
57791
58043
  //#endregion
@@ -75762,7 +76014,8 @@ const Beat = Struct({
75762
76014
  model: optional$2(String$),
75763
76015
  system: optional$2(String$),
75764
76016
  validate: optional$2(String$),
75765
- label: optional$2(String$)
76017
+ label: optional$2(String$),
76018
+ judge: optional$2(String$)
75766
76019
  });
75767
76020
  const Land = Struct({
75768
76021
  schema: Number$,
@@ -75958,10 +76211,11 @@ const gitRoot = gen(function* () {
75958
76211
  if (exit.code !== 0) return yield* fail$7(/* @__PURE__ */ new Error("not inside a git repository"));
75959
76212
  return exit.stdout.trim();
75960
76213
  });
75961
- const gtd = (ctx, args) => flatMap$4(ctx.host.spawn({
76214
+ const gtd = (ctx, args, stdin) => flatMap$4(ctx.host.spawn({
75962
76215
  command: ctx.gtd[0],
75963
76216
  args: [...ctx.gtd.slice(1), ...args],
75964
76217
  cwd: ctx.root,
76218
+ ...stdin === void 0 ? {} : { stdin },
75965
76219
  output: "capture"
75966
76220
  }), (exit) => exit.code === 0 ? succeed$3(exit.stdout) : fail$7(/* @__PURE__ */ new Error(`\`gtd ${args.join(" ")}\` failed (exit ${exit.code})\n${exit.stderr.trimEnd()}`)));
75967
76221
  const readBeat = (ctx) => flatMap$4(gtd(ctx, ["next", "--json"]), (stdout) => fromEither(decodeBeat(stdout)));
@@ -76044,29 +76298,41 @@ const runScript = (ctx, content) => as(ctx.host.spawn({
76044
76298
  output: { log: ctx.log }
76045
76299
  }), LAND);
76046
76300
  const stop = (ctx, sgr, headline, prose) => as(zipRight$1(ctx.host.appendLog(ctx.log, prose), rest(ctx, sgr, headline)), "stop");
76047
- const messageBeat = (ctx, beat, doc) => {
76048
- if (beat === 1) return succeed$3(LAND);
76301
+ const judgeGate = (ctx, doc) => gtd(ctx, ["judge", "run"], doc.judge).pipe(flatMap$4((verdict) => gtd(ctx, [
76302
+ "judge",
76303
+ "answer",
76304
+ "--json"
76305
+ ], verdict)), tap(() => ctx.host.say(` ${paint(ctx.host, "2", "⚖ judged")}\n`)), catchAll((error) => as(ctx.host.appendLog(ctx.log, `${error.message}\n`), void 0)));
76306
+ const messageBeat = (ctx, beat, doc, judged) => gen(function* () {
76307
+ const isJudge = doc.judge !== void 0 && doc.judge !== "";
76308
+ const landing = isJudge && judged !== doc.state ? yield* judgeGate(ctx, doc) : void 0;
76309
+ if (landing !== void 0) return {
76310
+ ...LAND,
76311
+ landing
76312
+ };
76313
+ if (beat === 1 && !isJudge) return LAND;
76049
76314
  const label = doc.label === void 0 ? "" : ` (${doc.label})`;
76050
- return stop(ctx, "33", `your turn — ${doc.state}${label}`, doc.content);
76051
- };
76052
- const kindBeat = (ctx, beat, doc) => {
76315
+ return yield* stop(ctx, "33", `your turn — ${doc.state}${label}`, doc.content);
76316
+ });
76317
+ const kindBeat = (ctx, beat, doc, judged) => {
76053
76318
  switch (doc.kind) {
76054
76319
  case "stalled": return zipRight$1(ctx.host.appendLog(ctx.log, doc.content), fail$7(new Error(doc.content.trimEnd())));
76055
- case "message": return messageBeat(ctx, beat, doc);
76320
+ case "message": return messageBeat(ctx, beat, doc, judged);
76056
76321
  case "capture": return succeed$3(LAND);
76057
76322
  case "script": return runScript(ctx, doc.content);
76058
76323
  case "prompt": return promptBeat(ctx, doc);
76059
76324
  default: return fail$7(/* @__PURE__ */ new Error(`unknown beat kind "${doc.kind}"`));
76060
76325
  }
76061
76326
  };
76062
- const runBeat = (ctx, beat, doc) => beat > 1 && doc.idle ? flatMap$4(gtd(ctx, ["next"]), (message) => stop(ctx, "32", `done — ${doc.state}`, message)) : kindBeat(ctx, beat, doc);
76327
+ const runBeat = (ctx, beat, doc, judged) => beat > 1 && doc.idle ? flatMap$4(gtd(ctx, ["next"]), (message) => stop(ctx, "32", `done — ${doc.state}`, message)) : kindBeat(ctx, beat, doc, judged);
76063
76328
  const costFlags = (outcome) => outcome.cost > 0 ? [`--cost=${outcome.cost.toFixed(6)}`, ...outcome.model === void 0 ? [] : [`--model=${outcome.model}`]] : [];
76064
76329
  const land = (ctx, outcome) => gen(function* () {
76065
- const doc = yield* fromEither(decodeLand(yield* gtd(ctx, [
76330
+ const landed = outcome.landing ?? (yield* gtd(ctx, [
76066
76331
  "land",
76067
76332
  "--json",
76068
76333
  ...costFlags(outcome)
76069
- ])));
76334
+ ]));
76335
+ const doc = yield* fromEither(decodeLand(landed));
76070
76336
  const exit = yield* ctx.host.spawn({
76071
76337
  command: "sh",
76072
76338
  args: [],
@@ -76106,11 +76372,13 @@ const runLoop = (options) => gen(function* () {
76106
76372
  log: resolve(root, doc.log)
76107
76373
  };
76108
76374
  yield* host.resetLog(ctx.log);
76375
+ let judged;
76109
76376
  for (let beat = 1;; beat++) {
76110
76377
  if (beat > 1) doc = yield* readBeat(ctx);
76111
76378
  yield* header(ctx, beat, doc);
76112
- const outcome = yield* runBeat(ctx, beat, doc);
76379
+ const outcome = yield* runBeat(ctx, beat, doc, judged);
76113
76380
  if (outcome === "stop" || (yield* land(ctx, outcome))) return;
76381
+ judged = outcome.landing === void 0 ? void 0 : doc.state;
76114
76382
  }
76115
76383
  });
76116
76384
  //#endregion
@@ -76897,8 +77165,8 @@ const FLAGS = [
76897
77165
  valueHint: "<name>",
76898
77166
  help: [
76899
77167
  "(gtd judge run only) the answerer: fixed | jev | llm;",
76900
- "omitted: GTD_JUDGE_PROVIDER, then .gtdrc judge:, then jev",
76901
- "when TYPESAFE_API_KEY is set, else llm"
77168
+ "omitted: GTD_JUDGE_PROVIDER, then judge.json, then .gtdrc",
77169
+ "judge:, then jev when TYPESAFE_API_KEY is set, else llm"
76902
77170
  ]
76903
77171
  },
76904
77172
  {
@@ -76936,7 +77204,8 @@ const FLAGS = [
76936
77204
  help: [
76937
77205
  "(gtd land, with --cost) tag that cost's model",
76938
77206
  "(gtd judge run, llm answerer) the claude model; omitted:",
76939
- "GTD_JUDGE_MODEL, then .gtdrc judge:, then haiku"
77207
+ "GTD_JUDGE_MODEL, then judge.json, then .gtdrc judge:, then",
77208
+ "haiku"
76940
77209
  ]
76941
77210
  },
76942
77211
  {
@@ -77336,11 +77605,11 @@ const COMMAND_ROWS = [
77336
77605
  "does not cover are left out. --provider jev asks TypeSafe",
77337
77606
  "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
77338
77607
  "on PATH (Claude Code only), model haiku unless --model.",
77339
- "No --provider: GTD_JUDGE_PROVIDER, then .gtdrc judge:",
77340
- "provider, else jev when TYPESAFE_API_KEY is set and no",
77341
- "model is given, else llm; --model likewise falls back to",
77342
- "GTD_JUDGE_MODEL, then judge: model. Needs no repository,",
77343
- "only .gtdrc files. jev and",
77608
+ "No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
77609
+ "judge.json, then .gtdrc judge: provider, else jev when",
77610
+ "TYPESAFE_API_KEY is set and no model is given, else llm;",
77611
+ "--model likewise falls back to GTD_JUDGE_MODEL, then the",
77612
+ "file's and judge:'s model. Needs no repository. jev and",
77344
77613
  "llm exit 1 when they cannot answer every question, with",
77345
77614
  "nothing on stdout"
77346
77615
  ]