@pmelab/gtd 21.0.0 → 21.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "21.0.0",
3
+ "version": "21.1.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -414,20 +414,22 @@ noted in steps 3 and 4 below.
414
414
  users only; model `haiku`, `--model <name>` overrides) and reuses its login.
415
415
  With no `--provider`, `gtd judge run` picks jev when `TYPESAFE_API_KEY` is
416
416
  set and non-empty and no `--model` is given, else llm — never falling back
417
- across them; a `.gtdrc` `judge: { provider, model }` key and the
418
- `GTD_JUDGE_PROVIDER` / `GTD_JUDGE_MODEL` environment variables choose the
419
- judge without editing the driver (flags win over both); `--model` with
420
- `--provider fixed`/`jev` is refused. jev and llm exit 1 with nothing on
421
- stdout when they cannot answer every question):
417
+ across them; the `GTD_JUDGE_PROVIDER` / `GTD_JUDGE_MODEL` environment
418
+ variables, a user-level `~/.config/gtd/judge.json` `{ provider, model }`, and
419
+ a `.gtdrc` `judge:` key choose the judge without editing the driver (flags
420
+ win over all three, in that order); `--model` with `--provider fixed`/`jev`
421
+ is refused. jev and llm exit 1 with nothing on stdout when they cannot answer
422
+ every question):
422
423
  - Every red round after the first: was the failure identical, new, or
423
424
  progress?
424
425
  - After you review: is each of your notes an edit, a question, a nit or
425
426
  praise?
426
427
 
427
- The reference driver answers these itself (`gtd judge run`, auto selection:
428
- jev when `TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model
429
- haiku, `--model <name>` overrides); if that fails it shows you the message
430
- and stops. **The `llm` provider's `p` is self-reported by the model, not a
428
+ gtd itself never calls a judge: `gtd loop`, the Claude Code mod and the
429
+ reference driver answer these through `gtd judge run` (auto selection: jev
430
+ when `TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model haiku,
431
+ `--model <name>` overrides); if that fails it shows you the message and
432
+ stops. **The `llm` provider's `p` is self-reported by the model, not a
431
433
  measured probability, so a confidently wrong verdict can clear the 0.7 floor
432
434
  and skip a gate unattended.**
433
435
 
@@ -119,7 +119,9 @@ async function act(
119
119
 
120
120
  async function message(io: Io, b: Beat, beat: number, judged: string | undefined): Promise<Acted> {
121
121
  const verdict = b.judge && judged !== b.state ? await io.judge() : undefined
122
- if (verdict || beat === 1) return { verdict }
122
+ // An unanswered judge gate never lands on its own, opening beat included:
123
+ // that would take the conservative default unseen.
124
+ if (verdict || (beat === 1 && !b.judge)) return { verdict }
123
125
  // Any later beat is a gate this run produced and the human has not read yet.
124
126
  const stop: Stop = { kind: "gate", text: b.content ?? "", ...where(b) }
125
127
  if (b.judge) stop.isJudge = true
@@ -1,7 +1,8 @@
1
- // gtd's `llm` judge provider, answered by a subagent instead of `claude -p`.
2
- // A port of src/judges/providers/llm.ts (which needs Node, which a mod has
3
- // not): claude/hooks/judge.test.ts pins the prompt to gtd's own, so the two
4
- // cannot drift apart unnoticed.
1
+ // A judge gate is answered by `gtd judge run`, except where its own
2
+ // configuration would pick the `llm` provider: that one is answered by a
3
+ // subagent instead of `claude -p`. A port of src/judges/providers/llm.ts
4
+ // (which needs Node, which a mod has not): claude/hooks/judge.test.ts pins the
5
+ // prompt to gtd's own, so the two cannot drift apart unnoticed.
5
6
 
6
7
  type Question = {
7
8
  id: string
@@ -14,6 +15,19 @@ export type Judgment = { state?: Record<string, string>; questions: Question[] }
14
15
 
15
16
  type Verdict = { id: string; answer: string | number; p: number }
16
17
 
18
+ // The env a `gtd judge run` spawn adds to the inherited one, or `undefined` to
19
+ // put the gate to the in-session subagent. A provider configured only in a
20
+ // file is invisible here, so no variable and no key means the llm, as `gtd
21
+ // judge run`'s own automatic pick does.
22
+ export function judgeRunEnv(env: {
23
+ provider?: string | undefined
24
+ key?: string | undefined
25
+ }): Record<string, string> | undefined {
26
+ const provider = env.provider || undefined
27
+ if (provider === "llm" || (provider === undefined && !env.key)) return undefined
28
+ return env.key ? { TYPESAFE_API_KEY: env.key } : {}
29
+ }
30
+
17
31
  export const JUDGE_SYSTEM =
18
32
  "You are a strict, impartial judge. Answer each question from the supplied evidence alone.\n" +
19
33
  "Reply only with the structured answer; never use tools or ask questions."
@@ -9,7 +9,7 @@ import type { Io, Turn, TurnEnd } from "./drive"
9
9
  import { doors, enter } from "./doors"
10
10
  import { subagentModel } from "./models"
11
11
  import { personaSpec, resumable } from "./persona"
12
- import { JUDGE_SYSTEM, judgePrompt, toVerdicts } from "./judge"
12
+ import { JUDGE_SYSTEM, judgePrompt, judgeRunEnv, toVerdicts } from "./judge"
13
13
  import type { Judgment } from "./judge"
14
14
  import { catchFrom, throwTo } from "./handoff"
15
15
  import { documents } from "./schema"
@@ -153,13 +153,16 @@ function io($: $): Io {
153
153
  },
154
154
  judge: () =>
155
155
  docs($).judge(async (doc, j) => {
156
- const key = (await $.env.get("TYPESAFE_API_KEY")) ?? (await $.env.get("TYPESAFE_AI_KEY"))
157
- if (!key) return judgeInSession($, j)
156
+ const env = judgeRunEnv({
157
+ provider: await $.env.get("GTD_JUDGE_PROVIDER"),
158
+ key: (await $.env.get("TYPESAFE_API_KEY")) || (await $.env.get("TYPESAFE_AI_KEY")),
159
+ })
160
+ if (env === undefined) return judgeInSession($, j)
158
161
  try {
159
- const r = await $.process.run(["gtd", "judge", "run", "--provider", "jev"], {
162
+ const r = await $.process.run(["gtd", "judge", "run"], {
160
163
  cwd: root,
161
164
  stdin: doc,
162
- env: { TYPESAFE_API_KEY: key },
165
+ env,
163
166
  timeoutMs: TEN_MINUTES,
164
167
  })
165
168
  return r.exitCode === 0 && r.stdout.trim() ? r.stdout : undefined
@@ -511,8 +514,8 @@ async function writer($: $, prompt: string) {
511
514
  return delegate($, SHIPPER, prompt, model, "Your final answer, exactly as you would print it.")
512
515
  }
513
516
 
514
- // Without a TypeSafe key a judge gate is put to a small model, as gtd's own
515
- // llm provider does, but as a subagent of this session. Any doubt leaves the
517
+ // Where `gtd judge run` would pick its llm, a judge gate is put to a small
518
+ // model as that provider does, but as a subagent of this session. Any doubt leaves the
516
519
  // gate to the person, which routes the workflow its cautious way.
517
520
  async function judgeInSession($: $, j: Judgment) {
518
521
  try {
@@ -49676,9 +49676,9 @@ const UiSchema = Struct({
49676
49676
  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
49677
  }).annotations({ description: "Settings for `gtd ui`: where it listens, and optional TLS." });
49678
49678
  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." });
49679
+ 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." })),
49680
+ 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." }))
49681
+ }).annotations({ description: "Which judge `gtd judge run` uses, when no flag, GTD_JUDGE_* variable or user-level judge.json says." });
49682
49682
  const ConfigSchema = Struct({
49683
49683
  vars: optional$2(Unknown.annotations({ jsonSchema: varsJsonSchema })),
49684
49684
  env: optional$2(Unknown.annotations({ jsonSchema: envJsonSchema })),
@@ -57748,23 +57748,73 @@ const envProvider = (env) => {
57748
57748
  return value;
57749
57749
  };
57750
57750
  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;
57751
+ const userConfigPath = (env) => {
57752
+ const xdg = env["XDG_CONFIG_HOME"];
57753
+ if (xdg) return `${xdg}/gtd/judge.json`;
57754
+ const home = env["HOME"];
57755
+ return home ? `${home}/.config/gtd/judge.json` : void 0;
57756
+ };
57757
+ const readText = async (path) => {
57758
+ try {
57759
+ return await readFile(path, "utf8");
57760
+ } catch (e) {
57761
+ if (e.code === "ENOENT") return void 0;
57762
+ return fail(`cannot read ${path}: ${e instanceof Error ? e.message : String(e)}`);
57763
+ }
57764
+ };
57765
+ const parseObject = (path, text) => {
57766
+ let json;
57767
+ try {
57768
+ json = JSON.parse(text);
57769
+ } catch {
57770
+ return fail(`${path} is not valid JSON`);
57771
+ }
57772
+ return typeof json === "object" && json !== null && !Array.isArray(json) ? json : fail(`${path} must be a JSON object`);
57773
+ };
57774
+ const decodeUserConfig = (path, text) => {
57775
+ const { provider, model, ...rest } = parseObject(path, text);
57776
+ const [extra] = Object.keys(rest);
57777
+ if (extra !== void 0) return fail(`${path}: unknown key "${extra}"`);
57778
+ if (provider !== void 0 && !PROVIDERS.includes(provider)) return fail(`${path}: "provider" must be one of ${PROVIDERS.join(", ")}`);
57779
+ if (model !== void 0 && typeof model !== "string") return fail(`${path}: "model" must be a string`);
57780
+ return {
57781
+ provider,
57782
+ model
57783
+ };
57784
+ };
57785
+ const readUserConfig = async (env) => {
57786
+ const path = userConfigPath(env);
57787
+ const text = path === void 0 ? void 0 : await readText(path);
57788
+ return path === void 0 || text === void 0 ? void 0 : decodeUserConfig(path, text);
57789
+ };
57751
57790
  /**
57752
57791
  * 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.
57792
+ * `GTD_JUDGE_MODEL` (empty counts as unset), then the user file, then `.gtdrc`
57793
+ * `judge:`. The provider/model pairing is checked on the resolved pair,
57794
+ * whichever layer supplied each half.
57756
57795
  */
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);
57796
+ const selectJudge = ({ provider, model, configured, env }, user) => {
57797
+ const layers = [
57798
+ {
57799
+ provider,
57800
+ model
57801
+ },
57802
+ {
57803
+ provider: envProvider(env),
57804
+ model: env["GTD_JUDGE_MODEL"] || void 0
57805
+ },
57806
+ user ?? {},
57807
+ configured ?? {}
57808
+ ];
57809
+ return checkPairing({
57810
+ provider: layers.find((l) => l.provider !== void 0)?.provider,
57811
+ model: layers.find((l) => l.model !== void 0)?.model
57812
+ });
57763
57813
  };
57764
57814
  /** The only place auto selection lives; it never falls back across providers. */
57765
- const answererFor = (opts) => {
57815
+ const answererFor = async (opts) => {
57766
57816
  const { answers, env, cwd } = opts;
57767
- const { provider, model } = selectJudge(opts);
57817
+ const { provider, model } = selectJudge(opts, await readUserConfig(env));
57768
57818
  if (provider === "fixed") return fixed({
57769
57819
  answersPath: answers,
57770
57820
  env
@@ -57785,7 +57835,7 @@ const runJudge = async (opts) => {
57785
57835
  }
57786
57836
  const { questions, state } = doc ?? {};
57787
57837
  if (!Array.isArray(questions)) return fail("stdin has no \"questions\" array");
57788
- const verdicts = await answererFor(opts)(questions, typeof state === "object" && state !== null ? state : {});
57838
+ const verdicts = await (await answererFor(opts))(questions, typeof state === "object" && state !== null ? state : {});
57789
57839
  return JSON.stringify(verdicts) + "\n";
57790
57840
  };
57791
57841
  //#endregion
@@ -75762,7 +75812,8 @@ const Beat = Struct({
75762
75812
  model: optional$2(String$),
75763
75813
  system: optional$2(String$),
75764
75814
  validate: optional$2(String$),
75765
- label: optional$2(String$)
75815
+ label: optional$2(String$),
75816
+ judge: optional$2(String$)
75766
75817
  });
75767
75818
  const Land = Struct({
75768
75819
  schema: Number$,
@@ -75958,10 +76009,11 @@ const gitRoot = gen(function* () {
75958
76009
  if (exit.code !== 0) return yield* fail$7(/* @__PURE__ */ new Error("not inside a git repository"));
75959
76010
  return exit.stdout.trim();
75960
76011
  });
75961
- const gtd = (ctx, args) => flatMap$4(ctx.host.spawn({
76012
+ const gtd = (ctx, args, stdin) => flatMap$4(ctx.host.spawn({
75962
76013
  command: ctx.gtd[0],
75963
76014
  args: [...ctx.gtd.slice(1), ...args],
75964
76015
  cwd: ctx.root,
76016
+ ...stdin === void 0 ? {} : { stdin },
75965
76017
  output: "capture"
75966
76018
  }), (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
76019
  const readBeat = (ctx) => flatMap$4(gtd(ctx, ["next", "--json"]), (stdout) => fromEither(decodeBeat(stdout)));
@@ -76044,29 +76096,41 @@ const runScript = (ctx, content) => as(ctx.host.spawn({
76044
76096
  output: { log: ctx.log }
76045
76097
  }), LAND);
76046
76098
  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);
76099
+ const judgeGate = (ctx, doc) => gtd(ctx, ["judge", "run"], doc.judge).pipe(flatMap$4((verdict) => gtd(ctx, [
76100
+ "judge",
76101
+ "answer",
76102
+ "--json"
76103
+ ], verdict)), tap(() => ctx.host.say(` ${paint(ctx.host, "2", "⚖ judged")}\n`)), catchAll((error) => as(ctx.host.appendLog(ctx.log, `${error.message}\n`), void 0)));
76104
+ const messageBeat = (ctx, beat, doc, judged) => gen(function* () {
76105
+ const isJudge = doc.judge !== void 0 && doc.judge !== "";
76106
+ const landing = isJudge && judged !== doc.state ? yield* judgeGate(ctx, doc) : void 0;
76107
+ if (landing !== void 0) return {
76108
+ ...LAND,
76109
+ landing
76110
+ };
76111
+ if (beat === 1 && !isJudge) return LAND;
76049
76112
  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) => {
76113
+ return yield* stop(ctx, "33", `your turn — ${doc.state}${label}`, doc.content);
76114
+ });
76115
+ const kindBeat = (ctx, beat, doc, judged) => {
76053
76116
  switch (doc.kind) {
76054
76117
  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);
76118
+ case "message": return messageBeat(ctx, beat, doc, judged);
76056
76119
  case "capture": return succeed$3(LAND);
76057
76120
  case "script": return runScript(ctx, doc.content);
76058
76121
  case "prompt": return promptBeat(ctx, doc);
76059
76122
  default: return fail$7(/* @__PURE__ */ new Error(`unknown beat kind "${doc.kind}"`));
76060
76123
  }
76061
76124
  };
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);
76125
+ 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
76126
  const costFlags = (outcome) => outcome.cost > 0 ? [`--cost=${outcome.cost.toFixed(6)}`, ...outcome.model === void 0 ? [] : [`--model=${outcome.model}`]] : [];
76064
76127
  const land = (ctx, outcome) => gen(function* () {
76065
- const doc = yield* fromEither(decodeLand(yield* gtd(ctx, [
76128
+ const landed = outcome.landing ?? (yield* gtd(ctx, [
76066
76129
  "land",
76067
76130
  "--json",
76068
76131
  ...costFlags(outcome)
76069
- ])));
76132
+ ]));
76133
+ const doc = yield* fromEither(decodeLand(landed));
76070
76134
  const exit = yield* ctx.host.spawn({
76071
76135
  command: "sh",
76072
76136
  args: [],
@@ -76106,11 +76170,13 @@ const runLoop = (options) => gen(function* () {
76106
76170
  log: resolve(root, doc.log)
76107
76171
  };
76108
76172
  yield* host.resetLog(ctx.log);
76173
+ let judged;
76109
76174
  for (let beat = 1;; beat++) {
76110
76175
  if (beat > 1) doc = yield* readBeat(ctx);
76111
76176
  yield* header(ctx, beat, doc);
76112
- const outcome = yield* runBeat(ctx, beat, doc);
76177
+ const outcome = yield* runBeat(ctx, beat, doc, judged);
76113
76178
  if (outcome === "stop" || (yield* land(ctx, outcome))) return;
76179
+ judged = outcome.landing === void 0 ? void 0 : doc.state;
76114
76180
  }
76115
76181
  });
76116
76182
  //#endregion
@@ -76897,8 +76963,8 @@ const FLAGS = [
76897
76963
  valueHint: "<name>",
76898
76964
  help: [
76899
76965
  "(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"
76966
+ "omitted: GTD_JUDGE_PROVIDER, then judge.json, then .gtdrc",
76967
+ "judge:, then jev when TYPESAFE_API_KEY is set, else llm"
76902
76968
  ]
76903
76969
  },
76904
76970
  {
@@ -76936,7 +77002,8 @@ const FLAGS = [
76936
77002
  help: [
76937
77003
  "(gtd land, with --cost) tag that cost's model",
76938
77004
  "(gtd judge run, llm answerer) the claude model; omitted:",
76939
- "GTD_JUDGE_MODEL, then .gtdrc judge:, then haiku"
77005
+ "GTD_JUDGE_MODEL, then judge.json, then .gtdrc judge:, then",
77006
+ "haiku"
76940
77007
  ]
76941
77008
  },
76942
77009
  {
@@ -77336,11 +77403,11 @@ const COMMAND_ROWS = [
77336
77403
  "does not cover are left out. --provider jev asks TypeSafe",
77337
77404
  "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
77338
77405
  "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",
77406
+ "No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
77407
+ "judge.json, then .gtdrc judge: provider, else jev when",
77408
+ "TYPESAFE_API_KEY is set and no model is given, else llm;",
77409
+ "--model likewise falls back to GTD_JUDGE_MODEL, then the",
77410
+ "file's and judge:'s model. Needs no repository. jev and",
77344
77411
  "llm exit 1 when they cannot answer every question, with",
77345
77412
  "nothing on stdout"
77346
77413
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "21.0.0",
3
+ "version": "21.1.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -36,15 +36,15 @@
36
36
  "jev",
37
37
  "llm"
38
38
  ],
39
- "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."
39
+ "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."
40
40
  },
41
41
  "model": {
42
42
  "type": "string",
43
- "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."
43
+ "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."
44
44
  }
45
45
  },
46
46
  "additionalProperties": false,
47
- "description": "Which judge `gtd judge run` uses, when no flag or GTD_JUDGE_* variable says."
47
+ "description": "Which judge `gtd judge run` uses, when no flag, GTD_JUDGE_* variable or user-level judge.json says."
48
48
  },
49
49
  "modes": {
50
50
  "type": "object",