@pmelab/gtd 29.2.0 → 29.2.1

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": "29.2.0",
3
+ "version": "29.2.1",
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"
@@ -83,7 +83,9 @@ export function judgePrompt(j: Judgment): string {
83
83
  "Answer every question below from the evidence alone. For each, give your answer and p, your confidence between 0 and 1 that the given answer is correct.",
84
84
  ]
85
85
  for (const [key, body] of Object.entries(j.state ?? {}))
86
- parts.push(`<evidence key="${key}">\n${body}\n</evidence>`)
86
+ parts.push(
87
+ `<evidence key="${key}">\n${body.replace(/<\/evidence/gi, "<\\/evidence")}\n</evidence>`,
88
+ )
87
89
  for (const q of j.questions) {
88
90
  const levels = levelsOf(q)
89
91
  if (typeof levels === "string") throw new Error(levels)
package/dist/flows.mjs CHANGED
@@ -370,7 +370,7 @@ const SHAPE_CRITERIA = [
370
370
  "land: every stray is incidental (a needed import, a test fixture, a renamed call site).",
371
371
  "refactor-first: the strays are a seam change the task needs; land them first as a behaviour-neutral refactor.",
372
372
  "reject: the naive pass solved the task the wrong way; discard it and re-implement, no human; strays a refactor could absorb never count as reject."
373
- ].join("\n");
373
+ ].join(" ");
374
374
  const readable = (c) => (c.status === "deleted" || c.after !== void 0) && (c.status === "added" || c.before !== void 0);
375
375
  const strayContent = (strays) => strays.map((c) => [
376
376
  `=== ${c.path} (${c.status})`,
@@ -22014,7 +22014,7 @@ const SHAPE_CRITERIA = [
22014
22014
  "land: every stray is incidental (a needed import, a test fixture, a renamed call site).",
22015
22015
  "refactor-first: the strays are a seam change the task needs; land them first as a behaviour-neutral refactor.",
22016
22016
  "reject: the naive pass solved the task the wrong way; discard it and re-implement, no human; strays a refactor could absorb never count as reject."
22017
- ].join("\n");
22017
+ ].join(" ");
22018
22018
  const readable = (c) => (c.status === "deleted" || c.after !== void 0) && (c.status === "added" || c.before !== void 0);
22019
22019
  const strayContent = (strays) => strays.map((c) => [
22020
22020
  `=== ${c.path} (${c.status})`,
@@ -41238,7 +41238,9 @@ const planLanding = (opts = {}, current) => gen(function* () {
41238
41238
  * closes or sits inside its own closing message. Writes nothing — no git, no
41239
41239
  * state transition, no file. Refuses (throws, mapped to the runtime-error
41240
41240
  * exit code) when the workflow declares no `summary` prompt, or the resolved
41241
- * run has no commits to name. The edge never narrates: the CLI prints the
41241
+ * run has no commits to name. `--slice` also refuses an unknown slice name
41242
+ * (listing the valid slices), a config edited since the process closed, and a
41243
+ * replay that diverged. The edge never narrates: the CLI prints the
41242
41244
  * version warning, before the unknown-slice refusal; a replay refusal carries
41243
41245
  * it in its message.
41244
41246
  */
package/dist/testkit.mjs CHANGED
@@ -10341,7 +10341,9 @@ const planLanding = (opts = {}, current) => gen(function* () {
10341
10341
  * closes or sits inside its own closing message. Writes nothing — no git, no
10342
10342
  * state transition, no file. Refuses (throws, mapped to the runtime-error
10343
10343
  * exit code) when the workflow declares no `summary` prompt, or the resolved
10344
- * run has no commits to name. The edge never narrates: the CLI prints the
10344
+ * run has no commits to name. `--slice` also refuses an unknown slice name
10345
+ * (listing the valid slices), a config edited since the process closed, and a
10346
+ * replay that diverged. The edge never narrates: the CLI prints the
10345
10347
  * version warning, before the unknown-slice refusal; a replay refusal carries
10346
10348
  * it in its message.
10347
10349
  */
@@ -349,7 +349,7 @@ const SHAPE_CRITERIA = [
349
349
  "land: every stray is incidental (a needed import, a test fixture, a renamed call site).",
350
350
  "refactor-first: the strays are a seam change the task needs; land them first as a behaviour-neutral refactor.",
351
351
  "reject: the naive pass solved the task the wrong way; discard it and re-implement, no human; strays a refactor could absorb never count as reject."
352
- ].join("\n");
352
+ ].join(" ");
353
353
  const readable = (c) => (c.status === "deleted" || c.after !== void 0) && (c.status === "added" || c.before !== void 0);
354
354
  const strayContent = (strays) => strays.map((c) => [
355
355
  `=== ${c.path} (${c.status})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "29.2.0",
3
+ "version": "29.2.1",
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": {
@@ -326,7 +326,9 @@ const planLanding = (
326
326
  * closes or sits inside its own closing message. Writes nothing — no git, no
327
327
  * state transition, no file. Refuses (throws, mapped to the runtime-error
328
328
  * exit code) when the workflow declares no `summary` prompt, or the resolved
329
- * run has no commits to name. The edge never narrates: the CLI prints the
329
+ * run has no commits to name. `--slice` also refuses an unknown slice name
330
+ * (listing the valid slices), a config edited since the process closed, and a
331
+ * replay that diverged. The edge never narrates: the CLI prints the
330
332
  * version warning, before the unknown-slice refusal; a replay refusal carries
331
333
  * it in its message.
332
334
  */
@@ -174,7 +174,7 @@ const SHAPE_CRITERIA = [
174
174
  "land: every stray is incidental (a needed import, a test fixture, a renamed call site).",
175
175
  "refactor-first: the strays are a seam change the task needs; land them first as a behaviour-neutral refactor.",
176
176
  "reject: the naive pass solved the task the wrong way; discard it and re-implement, no human; strays a refactor could absorb never count as reject.",
177
- ].join("\n")
177
+ ].join(" ")
178
178
 
179
179
  const readable = (c: Change): boolean =>
180
180
  (c.status === "deleted" || c.after !== undefined) &&
@@ -7,6 +7,7 @@ import {
7
7
  type JudgeAnswer,
8
8
  type StepRequest,
9
9
  } from "../flows/index.js"
10
+ import { toRequest } from "../judges/index.js"
10
11
  import { globMatches } from "../wire/index.js"
11
12
 
12
13
  // Lives in replay, which wires `matches: globMatches` (flows tests may not import wire).
@@ -123,6 +124,19 @@ describe("judgeDiffShape", () => {
123
124
  expect(q?.criteria).toContain("strays a refactor could absorb never count as reject")
124
125
  })
125
126
 
127
+ it("is sendable to jev with exactly its three labels", async () => {
128
+ const requests = install({ shape: { answer: "land", p: 0.9 } })
129
+ await judgeDiffShape("shape", { changes: naive, allow: ["src/**"] })
130
+ const request = requests[0]
131
+ if (request?.kind !== "judge") throw new Error("expected a judge step")
132
+ const { questions } = toRequest(request.questions, {})
133
+ expect(Object.keys((questions["shape"] as { criteria: object }).criteria)).toEqual([
134
+ "land",
135
+ "refactor-first",
136
+ "reject",
137
+ ])
138
+ })
139
+
126
140
  it("carries exactly four evidence keys, with stray content before/after and in-allowlist paths only", async () => {
127
141
  const requests = install({ shape: { answer: "land", p: 0.9 } })
128
142
  await judgeDiffShape("shape", { changes: naive, allow: ["src/**"] })
@@ -3,6 +3,7 @@ import {
3
3
  installContext,
4
4
  type Change,
5
5
  type JudgeAnswer,
6
+ type JudgeQuestion,
6
7
  type ScopeOptions,
7
8
  type StepRequest,
8
9
  } from "../../flows/index.js"
@@ -18,6 +19,7 @@ import type { ReviewNote } from "../../steering/index.js"
18
19
  import * as bundled from "../index.js"
19
20
  import type { BuildTailOptions as PublicBuildTailOptions } from "./index.js"
20
21
  import { fixtureContext } from "../shared/text.fixture.js"
22
+ import { toRequest } from "../../judges/index.js"
21
23
 
22
24
  afterEach(() => installContext(undefined))
23
25
 
@@ -65,6 +67,7 @@ interface Run {
65
67
  readonly prompts: Map<string, string>
66
68
  readonly result: ReviewOutcome | "stopped"
67
69
  readonly judged: readonly string[]
70
+ readonly asked: readonly JudgeQuestion[]
68
71
  /** `acceptClean` of each `review.await-review` gate, in order. */
69
72
  readonly gates: readonly boolean[]
70
73
  readonly gateMessages: readonly string[]
@@ -78,7 +81,7 @@ interface Run {
78
81
  const drive = async (d: Drive): Promise<Run> => {
79
82
  const log: string[] = []
80
83
  const prompts = new Map<string, string>()
81
- const judged: string[] = []
84
+ const asked: JudgeQuestion[] = []
82
85
  const gates: boolean[] = []
83
86
  const gateMessages: string[] = []
84
87
  const since: string[] = []
@@ -113,7 +116,7 @@ const drive = async (d: Drive): Promise<Run> => {
113
116
  gateMessages.push(request.options.message ?? "")
114
117
  }
115
118
  if (request.kind === "agent") prompts.set(request.name, request.prompt)
116
- if (request.kind === "judge") judged.push(...request.questions.map((q) => q.id))
119
+ if (request.kind === "judge") asked.push(...request.questions)
117
120
  }
118
121
  const context = fixtureContext(
119
122
  { vars: d.vars ?? {} },
@@ -159,7 +162,17 @@ const drive = async (d: Drive): Promise<Run> => {
159
162
  if (!(error instanceof Stop)) throw error
160
163
  result = "stopped"
161
164
  }
162
- return { log, prompts, result, judged, gates, gateMessages, since, scopes }
165
+ return {
166
+ log,
167
+ prompts,
168
+ result,
169
+ judged: asked.map((q) => q.id),
170
+ asked,
171
+ gates,
172
+ gateMessages,
173
+ since,
174
+ scopes,
175
+ }
163
176
  }
164
177
 
165
178
  const verdict = (answer: string, p = 0.9): JudgeAnswer => ({ answer, p })
@@ -418,6 +431,15 @@ describe("review verdict routing", () => {
418
431
  expect(run.log.filter((n) => n === "review.triage")).toHaveLength(1)
419
432
  })
420
433
 
434
+ it("asks only questions sendable to jev, each choice with its labels", async () => {
435
+ const run = await drive({ notes: ["add — a"] })
436
+ const { questions } = toRequest(run.asked, {})
437
+ const labels = (id: string) => Object.keys((questions[id] as { criteria: object }).criteria)
438
+ expect(labels("note-1")).toEqual(["edit", "question", "nit", "praise"])
439
+ expect(labels("note-1.reversibility")).toEqual(["one-way", "two-way"])
440
+ expect(labels("note-1.blastRadius")).toEqual(["local", "module", "contract"])
441
+ })
442
+
421
443
  it("an unanswered note counts as edit", async () => {
422
444
  const run = await drive({ notes: ["add — typo", "sub", "mul"], answers: {} })
423
445
  expect(run.log).toEqual([