@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "21.0.0",
3
+ "version": "22.0.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
 
@@ -510,7 +512,10 @@ can add its own. `gtd --workflow <name>` starts any workflow by name.
510
512
  The workflow itself is a plain async TypeScript function, and the
511
513
  `gtd.config.ts` at the repository root names it: the one `gtd install` writes
512
514
  re-exports the shipped workflows from `@pmelab/gtd/workflows`, and the pieces
513
- they are built from are exported for yours to reuse. See
515
+ they are built from are exported for yours to reuse. Each shipped workflow is a
516
+ factory — `feature({ testCommand: "npm run ci" })` — whose options gtd validates
517
+ when it loads the config: a misspelled or mistyped one fails every command,
518
+ naming the workflow, the key and what was expected. See
514
519
  [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md) —
515
520
  and remember gtd evaluates that file on every command. A shared workflow package
516
521
  bundling its own `@pmelab/gtd` is checked against the running gtd and refused,
@@ -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 {