@pmelab/gtd 20.4.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": "20.4.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
@@ -56,7 +56,9 @@ enforces it at the OS level. See
56
56
 
57
57
  ## Quick start
58
58
 
59
- Pipe the output of `gtd install` into your coding agent and answer its
59
+ Run `gtd install` at your repository's root. It writes a `gtd.config.ts` that
60
+ runs gtd's shipped workflows — commit it; gtd refuses to run without one — and
61
+ prints a briefing. Pipe that briefing into your coding agent and answer its
60
62
  questions. It should build you a script that will autonomously run the process
61
63
  for you. Ask your agent if you have any questions.
62
64
 
@@ -92,9 +94,9 @@ Two commands make up the whole cycle. **`gtd next` says what to do now.
92
94
  that.
93
95
 
94
96
  The fastest way to understand it is to drive a few beats by hand. Any git
95
- repository with at least one commit will do — the first two beats need no setup
96
- at all, and `gtd init` seeds the one setting that matters later (your test
97
- command).
97
+ repository with at least one commit will do once `gtd install` has written its
98
+ `gtd.config.ts` and you have committed it — the first two beats need nothing
99
+ else. Later beats run your test command, set under `env:` in `.gtdrc`.
98
100
 
99
101
  ### Beat 1 — `gtd next` says whose turn it is
100
102
 
@@ -412,20 +414,22 @@ noted in steps 3 and 4 below.
412
414
  users only; model `haiku`, `--model <name>` overrides) and reuses its login.
413
415
  With no `--provider`, `gtd judge run` picks jev when `TYPESAFE_API_KEY` is
414
416
  set and non-empty and no `--model` is given, else llm — never falling back
415
- across them; a `.gtdrc` `judge: { provider, model }` key and the
416
- `GTD_JUDGE_PROVIDER` / `GTD_JUDGE_MODEL` environment variables choose the
417
- judge without editing the driver (flags win over both); `--model` with
418
- `--provider fixed`/`jev` is refused. jev and llm exit 1 with nothing on
419
- 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):
420
423
  - Every red round after the first: was the failure identical, new, or
421
424
  progress?
422
425
  - After you review: is each of your notes an edit, a question, a nit or
423
426
  praise?
424
427
 
425
- The reference driver answers these itself (`gtd judge run`, auto selection:
426
- jev when `TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model
427
- haiku, `--model <name>` overrides); if that fails it shows you the message
428
- 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
429
433
  measured probability, so a confidently wrong verdict can clear the 0.7 floor
430
434
  and skip a gate unattended.**
431
435
 
@@ -505,9 +509,10 @@ starts a pure review of everything from `<commitish>` to HEAD — straight to st
505
509
  4, no planning and no building. `gtd doors` lists every door; a `gtd.config.ts`
506
510
  can add its own. `gtd --workflow <name>` starts any workflow by name.
507
511
 
508
- The workflow itself is a plain async TypeScript function: a `gtd.config.ts` at
509
- the repository root replaces it, and the pieces the bundled one is built from
510
- are exported for yours to reuse. See
512
+ The workflow itself is a plain async TypeScript function, and the
513
+ `gtd.config.ts` at the repository root names it: the one `gtd install` writes
514
+ re-exports the shipped workflows from `@pmelab/gtd/workflows`, and the pieces
515
+ they are built from are exported for yours to reuse. See
511
516
  [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md) —
512
517
  and remember gtd evaluates that file on every command. A shared workflow package
513
518
  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 {