@pmelab/gtd 64.1.0 → 64.1.2

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.
package/dist/wire.mjs CHANGED
@@ -1,2 +1 @@
1
- import { A as UNATTRIBUTED_MODEL, C as EXIT_RUNTIME_ERROR, D as EXIT_USAGE_ERROR, E as EXIT_STRAYS_FOUND, M as renderFormat, O as ACCESS_REFUSAL, S as EXIT_OK, T as EXIT_SIGTERM, _ as BEAT_KINDS, a as renderScriptJson, b as stallDiagnosis, c as landProseText, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, j as WIRE_SCHEMA, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, w as EXIT_SIGINT, x as EXIT_CODES, y as demandOf } from "./chunks/wire-CPNSm_9N.mjs";
2
- export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, PIN_PATH, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderScriptJson, resolveSession, scriptFields, selectPath, stallDiagnosis, statusOf };
1
+ export { O as ACCESS_REFUSAL, _ as BEAT_KINDS, f as DEMAND_BRIEFING, x as EXIT_CODES, S as EXIT_OK, C as EXIT_RUNTIME_ERROR, w as EXIT_SIGINT, T as EXIT_SIGTERM, E as EXIT_STRAYS_FOUND, D as EXIT_USAGE_ERROR, k as PIN_PATH, A as UNATTRIBUTED_MODEL, j as WIRE_SCHEMA, m as beatDocument, v as beatKindOf, y as demandOf, t as globMatches, i as judgeJson, s as landFields, c as landProseText, l as noopText, h as renderBeatJson, p as renderBeatPlain, M as renderFormat, u as renderLandJson, d as renderLandPlain, a as renderScriptJson, n as resolveSession, o as scriptFields, r as selectPath, b as stallDiagnosis, g as statusOf } from "./chunks/wire-CaPJ9Tme.mjs";
@@ -1,6 +1,6 @@
1
- import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-B7ZRCe-B.mjs";
2
- import { C as reviewRisks, N as parseThreads, S as reviewNotes, T as unansweredNotes, n as stripCodeThreads, x as reviewDecisions } from "./chunks/steering-Cem5i6Xl.mjs";
3
- //#region src/workflow-kit/health.ts
1
+ import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-dnIE_qbe.mjs";
2
+ import { C as reviewRisks, N as parseThreads, S as reviewNotes, T as unansweredNotes, n as stripCodeThreads, x as reviewDecisions } from "./chunks/steering-CQzL-lX1.mjs";
3
+ // src/workflow-kit/health.ts
4
4
  /** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
5
5
  const escalation = async (count, steps) => {
6
6
  if (count.rounds >= steps.limit) return steps.exhausted();
@@ -74,8 +74,7 @@ const healthy = async (fix, health) => {
74
74
  const gate = (name, passes, blocked) => scope(name, async () => {
75
75
  while (!await passes()) await blocked();
76
76
  });
77
- //#endregion
78
- //#region src/workflow-kit/quality.ts
77
+ // src/workflow-kit/quality.ts
79
78
  const lensOf = (entry) => {
80
79
  const at = entry.indexOf("=");
81
80
  return at < 0 ? {
@@ -106,8 +105,7 @@ const fixFindings = async (f) => {
106
105
  await f.escalate();
107
106
  return false;
108
107
  };
109
- //#endregion
110
- //#region src/workflow-kit/scenarios.ts
108
+ // src/workflow-kit/scenarios.ts
111
109
  const DOC_STRING = /^("""|```)/;
112
110
  const THEN = /^Then\s/;
113
111
  const CONTINUATION = /^(And|But|\*)\s/;
@@ -161,8 +159,7 @@ const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".f
161
159
  lines
162
160
  };
163
161
  }).filter((r) => r.lines.length > 0);
164
- //#endregion
165
- //#region src/workflow-kit/review.ts
162
+ // src/workflow-kit/review.ts
166
163
  const collect = async (p, capture, round, requested) => {
167
164
  await p.collecting(capture, round);
168
165
  if (read(p.feedback) !== void 0) return "feedback";
@@ -426,5 +423,4 @@ const reviewToHandOff = async (base, p) => {
426
423
  return;
427
424
  }
428
425
  };
429
- //#endregion
430
426
  export { assertionLines, escalation, fixFindings, gate, healthy, lensEntriesOf, lensOf, lensScopes, lensesOf, qualityLap, removedAssertions, review, reviewToHandOff, untilSettled };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "64.1.0",
3
+ "version": "64.1.2",
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": {
@@ -83,33 +83,17 @@
83
83
  "scripts": {
84
84
  "prepare": "husky",
85
85
  "dev": "node dev/run.mjs",
86
- "build": "tsdown --filter web && npx @tailwindcss/cli -i src/web/styles.css -o dist/web/main.css --minify && node scripts/inline-web-client.mjs && tsdown --filter gtd --filter core && tsdown --filter entrypoints",
87
- "postbuild": "node scripts/assert-no-test-doubles.mjs",
88
- "watch": "tsdown --watch",
86
+ "watch": "vp pack --watch",
89
87
  "bench:replay": "node scripts/bench-replay.mjs",
90
88
  "test": "jiti scripts/test-states.main.ts",
91
- "test:unit": "vitest run --project unit",
92
- "test:workflows": "vitest run --project workflows",
93
89
  "test:watch": "vitest",
94
90
  "test:changed": "vitest run --project unit --project e2e-inmem --project workflows --changed HEAD",
95
- "test:e2e": "npm run test:e2e:inmem && npm run test:e2e:live",
96
- "typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.json",
97
- "lint": "oxlint .",
98
- "lint:sh": "jiti scripts/generate-shell-corpus.ts --check && shellcheck -s sh tests/shell/corpus/*.sh",
99
- "lint:boundaries": "depcruise src tests claude",
100
- "format": "oxfmt --write .",
101
- "format:check": "oxfmt --check .",
91
+ "test:e2e": "vp run test:e2e:inmem && vp run test:e2e:live",
92
+ "format": "vp fmt",
102
93
  "test:mutation": "stryker run",
103
- "test:e2e:inmem": "vitest run --project e2e-inmem",
104
- "test:e2e:live": "vitest run --project e2e-live --no-file-parallelism",
105
- "analyze": "fallow --summary --quiet",
106
- "test:web": "vitest run --project storybook",
107
94
  "storybook": "storybook dev --host 0.0.0.0",
108
95
  "coverage": "vitest run --project unit --coverage"
109
96
  },
110
- "lint-staged": {
111
- "*": "oxfmt --no-error-on-unmatched-pattern --write"
112
- },
113
97
  "devDependencies": {
114
98
  "@commitlint/cli": "^21.2.3",
115
99
  "@commitlint/config-conventional": "^21.2.3",
@@ -119,8 +103,8 @@
119
103
  "@semantic-release/git": "^10.0.1",
120
104
  "@semantic-release/github": "^12.0.8",
121
105
  "@semantic-release/npm": "^12.0.0",
122
- "@storybook/addon-vitest": "^10.6.0",
123
- "@storybook/react-vite": "^10.6.0",
106
+ "@storybook/addon-vitest": "^10.6.5",
107
+ "@storybook/react-vite": "^10.6.5",
124
108
  "@stryker-mutator/core": "^9.6.1",
125
109
  "@stryker-mutator/typescript-checker": "^9.6.1",
126
110
  "@stryker-mutator/vitest-runner": "^9.6.1",
@@ -134,29 +118,27 @@
134
118
  "@types/react": "^19.2.7",
135
119
  "@types/react-dom": "^19.2.3",
136
120
  "@vitejs/plugin-react": "^5.2.0",
137
- "@vitest/browser": "^4.1.12",
138
- "@vitest/browser-playwright": "^4.1.12",
139
- "@vitest/coverage-v8": "^4.1.12",
121
+ "@vitest/browser": "5.0.5",
122
+ "@vitest/browser-playwright": "5.0.5",
123
+ "@vitest/coverage-v8": "5.0.5",
140
124
  "dependency-cruiser": "^18.3.1",
141
125
  "fallow": "3.25.0",
142
126
  "fast-check": "^4.8.0",
143
127
  "husky": "^9.1.7",
144
128
  "jiti": "^2.7.0",
145
- "lint-staged": "^17.0.8",
146
- "oxfmt": "^0.59.0",
147
- "oxlint": "^1.74.0",
129
+ "oxfmt": "0.59.0",
130
+ "oxlint": "1.74.0",
148
131
  "playwright": "^1.62.1",
149
132
  "prettier": "^3.8.1",
150
133
  "quickpickle": "1.11.2",
151
134
  "react": "^19.2.0",
152
135
  "react-dom": "^19.2.0",
153
136
  "semantic-release": "^25.0.5",
154
- "storybook": "^10.6.0",
137
+ "storybook": "^10.6.5",
155
138
  "tailwind-merge": "^3.7.0",
156
139
  "tailwindcss": "^4.3.3",
157
- "tsdown": "^0.22.8",
158
- "turbo": "^2.10.9",
159
- "vitest": "^4.1.12",
140
+ "vite-plus": "1.4.0",
141
+ "vitest": "5.0.5",
160
142
  "yaml": "^2.8.2",
161
143
  "zustand": "^5.0.15"
162
144
  },
@@ -190,5 +172,10 @@
190
172
  "rolldown": "^1.1.5",
191
173
  "vscode-languageserver": "^10.1.0",
192
174
  "vscode-languageserver-textdocument": "^1.0.13"
175
+ },
176
+ "overrides": {
177
+ "storybook": {
178
+ "vite-plus": "$vite-plus"
179
+ }
193
180
  }
194
181
  }
@@ -757,7 +757,7 @@ describe("restAt", () => {
757
757
  `--model=${model}`,
758
758
  )
759
759
  expect(exitCode).toBe(0)
760
- await applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
760
+ applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
761
761
  await takePinBeat(repo, "add")
762
762
  }
763
763
  const { cost } = await provide(currentRest, repo)
package/src/edge/Edge.ts CHANGED
@@ -238,7 +238,7 @@ const historyFormatFault = (history: History, location: EpisodeLocation): string
238
238
  const runOf = (
239
239
  history: History,
240
240
  location: EpisodeLocation,
241
- closingHash: string | undefined = undefined,
241
+ closingHash?: string,
242
242
  ): Effect.Effect<ProcessRun, Error> => {
243
243
  const fault = historyFormatFault(history, location)
244
244
  return fault === undefined
@@ -234,7 +234,7 @@ describe("shellQuote", () => {
234
234
  })
235
235
 
236
236
  // 500 samples through 500 bash spawns starved the 30s test timeout whenever
237
- // the suite ran under turbo's parallel task load. The property is unchanged;
237
+ // the suite ran under `npm test`'s parallel task load. The property is unchanged;
238
238
  // all samples now round-trip through a SINGLE bash, NUL-delimited on the way
239
239
  // back out (NUL is filtered from the inputs, so it can't collide).
240
240
  it("round-trips arbitrary strings — including newlines and non-ASCII — through a real bash printf", () => {
@@ -146,7 +146,7 @@ describe("kept()", () => {
146
146
  })
147
147
 
148
148
  /** A context that records every step request and resolves it with `reply`. */
149
- const recording = (reply: unknown = undefined) => {
149
+ const recording = (reply?: unknown) => {
150
150
  const requests: StepRequest[] = []
151
151
  installContext({
152
152
  step: async (request: StepRequest) => {
@@ -370,7 +370,9 @@ describe("replay", () => {
370
370
  })
371
371
 
372
372
  it("is reported when history continues past the end of the flow", async () => {
373
- const short = async () => void (await human("only"))
373
+ const short = async () => {
374
+ await human("only")
375
+ }
374
376
  const h = new History().land("human", "only", 1, "only").land("human", "only", 2, "only")
375
377
  expect((await replayOf(short, h)).kind).toBe("divergence")
376
378
  })
@@ -606,7 +608,9 @@ describe("replay", () => {
606
608
  })
607
609
 
608
610
  it("ends the episode on return and on restart", async () => {
609
- const returning = async () => void (await human("only"))
611
+ const returning = async () => {
612
+ await human("only")
613
+ }
610
614
  const h = new History().land("human", "only", 1, "only")
611
615
  expect(await replayOf(returning, h)).toMatchObject({ kind: "ended", via: "return" })
612
616
 
@@ -1337,7 +1341,9 @@ describe("replay: a repin's anchor", () => {
1337
1341
  })
1338
1342
 
1339
1343
  it("diverges when the flow ends where it names a step", async () => {
1340
- const outcome = await replayOf(async () => void (await human("idle")), anchoredHistory())
1344
+ const outcome = await replayOf(async () => {
1345
+ await human("idle")
1346
+ }, anchoredHistory())
1341
1347
  expect(outcome).toMatchObject({ kind: "divergence", at: hashOf(3), reached: undefined })
1342
1348
  })
1343
1349
 
@@ -1346,7 +1352,9 @@ describe("replay: a repin's anchor", () => {
1346
1352
  .land("human", "idle", 1, "build", { "a.txt": "a" })
1347
1353
  .land("agent", "build", 1, "old")
1348
1354
  .raw(anchorTo("gtd:unpin", 1))
1349
- const outcome = await replayOf(async () => void (await human("idle")), h)
1355
+ const outcome = await replayOf(async () => {
1356
+ await human("idle")
1357
+ }, h)
1350
1358
  expect(outcome.kind).toBe("ended")
1351
1359
  })
1352
1360
 
@@ -1355,13 +1363,17 @@ describe("replay: a repin's anchor", () => {
1355
1363
  .land("human", "idle", 1, "build", { "a.txt": "a" })
1356
1364
  .land("agent", "build", 1, "old")
1357
1365
  .raw(`gtd(check): pin → unpin\n\nGtd-Pin: add\nGtd-Anchor: ${hashOf(1)}\nGtd-Format: 3`)
1358
- const outcome = await replayOf(async () => void (await human("idle")), h)
1366
+ const outcome = await replayOf(async () => {
1367
+ await human("idle")
1368
+ }, h)
1359
1369
  expect(outcome.kind).toBe("ended")
1360
1370
  })
1361
1371
 
1362
1372
  it("reads a format 4 anchor naming unpin as a step", async () => {
1363
1373
  const outcome = await replayOf(
1364
- async () => void (await human("idle")),
1374
+ async () => {
1375
+ await human("idle")
1376
+ },
1365
1377
  new History()
1366
1378
  .land("human", "idle", 1, "build", { "a.txt": "a" })
1367
1379
  .land("agent", "build", 1, "old")
@@ -1372,7 +1384,9 @@ describe("replay: a repin's anchor", () => {
1372
1384
 
1373
1385
  it("fails when it names no commit replay reads", async () => {
1374
1386
  const h = anchoredHistory().raw(anchorTo("build", 2))
1375
- const outcome = await replayOf(async () => void (await human("idle")), h)
1387
+ const outcome = await replayOf(async () => {
1388
+ await human("idle")
1389
+ }, h)
1376
1390
  expect(outcome.kind === "failed" && outcome.message).toContain(
1377
1391
  "which is no commit replay reads",
1378
1392
  )
@@ -35,7 +35,9 @@ describe("blockRunInline — spec-feedback regression: a nested list item's own
35
35
  const nodes = blockNodesOf(content, tree)
36
36
  const topItem = nodes[0]?.block?.items?.[0]
37
37
  expect(topItem?.inline).toEqual([{ kind: "text", value: "outer" }])
38
- expect(topItem?.items).toEqual([{ text: "inner", inline: [{ kind: "text", value: "inner" }] }])
38
+ expect(topItem?.items).toEqual([
39
+ { text: "inner", inline: [{ kind: "text", value: "inner" }], line: 1 },
40
+ ])
39
41
  })
40
42
  })
41
43
 
@@ -128,21 +128,28 @@ const nodeInline = (
128
128
  return title.length > 0 ? [{ kind: "text", value: title }] : []
129
129
  }
130
130
 
131
+ type NoteAt = ReturnType<typeof noteLookup>
132
+
131
133
  /** One list item as a `BlockListItem`, recursing into a nested `list` child (there is at most one, CommonMark's own shape) as its own `items`. */
132
- const blockListItemOf = (content: string, item: ListItem): BlockListItem => {
134
+ const blockListItemOf = (content: string, item: ListItem, noteAt: NoteAt): BlockListItem => {
133
135
  const nested = item.children.filter((c): c is List => c.type === "list")
134
- const items = nested.flatMap((list) => blockListItemsOf(content, list.children))
136
+ const items = nested.flatMap((list) => blockListItemsOf(content, list.children, noteAt))
137
+ const line = item.position === undefined ? undefined : toLspPosition(item.position.start).line
135
138
  return {
136
139
  text: listItemText(content, item),
137
140
  inline: listItemInline(content, item),
138
141
  ...(item.checked === true || item.checked === false ? { checked: item.checked } : {}),
139
142
  ...(items.length > 0 ? { items } : {}),
143
+ ...(line === undefined ? {} : { line, ...noteAt(line) }),
140
144
  }
141
145
  }
142
146
 
143
147
  /** Every item of a `list` node's own `children`, as `BlockListItem`s, in document order. */
144
- const blockListItemsOf = (content: string, items: readonly ListItem[]): readonly BlockListItem[] =>
145
- items.map((item) => blockListItemOf(content, item))
148
+ const blockListItemsOf = (
149
+ content: string,
150
+ items: readonly ListItem[],
151
+ noteAt: NoteAt,
152
+ ): readonly BlockListItem[] => items.map((item) => blockListItemOf(content, item, noteAt))
146
153
 
147
154
  /** The `title` an empty fenced code block (a `` ``` ``/`` ``` `` pair with nothing between them) falls back to — its real body is `""`, and every node still carries a non-empty title. */
148
155
  const EMPTY_CODE_BLOCK_TITLE = "(empty code block)"
@@ -184,6 +191,7 @@ const blockTitle = (content: string, node: RootContent, options?: BlockWalkOptio
184
191
  const blockOf = (
185
192
  content: string,
186
193
  node: RootContent,
194
+ noteAt: NoteAt,
187
195
  options?: BlockWalkOptions,
188
196
  ): SteeringViewNode["block"] | undefined => {
189
197
  switch (node.type) {
@@ -197,7 +205,7 @@ const blockOf = (
197
205
  return {
198
206
  kind: "list",
199
207
  ordered: node.ordered === true,
200
- items: blockListItemsOf(content, visibleListItems(node, options)),
208
+ items: blockListItemsOf(content, visibleListItems(node, options), noteAt),
201
209
  }
202
210
  case "code":
203
211
  return {
@@ -285,7 +293,7 @@ export const blockNodesOfRun = (
285
293
  const noteAt = noteLookup(content, options?.footnotes)
286
294
  return visibleNodes(content, nodes, options).map((node) => {
287
295
  const startLine = toLspPosition(node.position!.start).line
288
- const block = blockOf(content, node, options)
296
+ const block = blockOf(content, node, noteAt, options)
289
297
  return {
290
298
  title: blockTitle(content, node, options),
291
299
  anchor: { kind: "paragraph" as const, line: startLine },
@@ -89,6 +89,11 @@ export interface BlockListItem {
89
89
  readonly items?: readonly BlockListItem[]
90
90
  /** `text`'s own inline structure — the shared renderer walks this instead of `text` when present. Populated at every nesting depth. */
91
91
  readonly inline?: readonly InlineNode[]
92
+ /** The item's own 0-based source line: a `paragraph` anchor at it notes this item alone. */
93
+ readonly line?: number
94
+ /** The note or thread whose marker sits on `line`, as on a view node. */
95
+ readonly note?: string
96
+ readonly thread?: SteeringViewThread
92
97
  }
93
98
 
94
99
  export interface SteeringViewThread {
@@ -1172,7 +1172,7 @@ describe("qa.view — block nodes (package 02, T1/T2)", () => {
1172
1172
  expect(topItem?.text).not.toContain("Nested")
1173
1173
  expect(topItem?.text).not.toContain("-")
1174
1174
  expect(topItem?.items).toEqual([
1175
- { text: "Nested", inline: [{ kind: "text", value: "Nested" }] },
1175
+ { text: "Nested", inline: [{ kind: "text", value: "Nested" }], line: 2 },
1176
1176
  ])
1177
1177
  })
1178
1178
 
@@ -1180,8 +1180,8 @@ describe("qa.view — block nodes (package 02, T1/T2)", () => {
1180
1180
  const content = ["- [ ] Not done", "- [x] Done", ""].join("\n")
1181
1181
  const view = qa.view(content)
1182
1182
  expect(view.nodes[0]?.block?.items).toEqual([
1183
- { text: "Not done", checked: false, inline: [{ kind: "text", value: "Not done" }] },
1184
- { text: "Done", checked: true, inline: [{ kind: "text", value: "Done" }] },
1183
+ { text: "Not done", checked: false, inline: [{ kind: "text", value: "Not done" }], line: 0 },
1184
+ { text: "Done", checked: true, inline: [{ kind: "text", value: "Done" }], line: 1 },
1185
1185
  ])
1186
1186
  })
1187
1187
 
@@ -822,7 +822,7 @@ const replaceOptionTextEdit = (
822
822
  * Collapses a typed answer to the single-line shape a list-item label must
823
823
  * be. A raw textarea value's interior newlines would break `.gtd/`'s oxfmt
824
824
  * fixed point the moment they are spliced onto a `- [x] ` line, and every
825
- * steering file — server-written ones included — is covered by `format:check`.
825
+ * steering file — server-written ones included — is covered by the `check` task.
826
826
  */
827
827
  /**
828
828
  * Empty normalizes to `FREE_TEXT_PLACEHOLDER`, never `""`: an empty label
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { FOOTNOTE_ACTION_TITLE, steeringFormatFor, viewOf } from "./internal.js"
2
+ import { applySteeringEdits, FOOTNOTE_ACTION_TITLE, steeringFormatFor, viewOf } from "./internal.js"
3
3
  import { parseReviewDoc, unansweredNotes } from "./review.js"
4
4
  import { getParseCount } from "./internal.js"
5
5
 
@@ -1370,6 +1370,31 @@ describe("review.annotate", () => {
1370
1370
  expect(result.ok).toBe(true)
1371
1371
  })
1372
1372
 
1373
+ it("a paragraph anchor on one item of a chunk's list notes that item alone", () => {
1374
+ const summary = [
1375
+ "# Review: abc1234",
1376
+ "<!-- base: abc1234def5678901234567890123456789abcd -->",
1377
+ "",
1378
+ "## Quality lap",
1379
+ "",
1380
+ "Fixed automatically:",
1381
+ "",
1382
+ "- Rounds wrong (lens correctness)",
1383
+ "- Missing test (lens tests)",
1384
+ "",
1385
+ ].join("\n")
1386
+ const before = review.view(summary).nodes[0]!.body![1]!.block!.items!
1387
+ expect(before.map((item) => item.line)).toEqual([7, 8])
1388
+ const result = review.annotate(summary, { kind: "paragraph", line: 8 }, "reopen it")
1389
+ if (!result.ok) throw new Error(result.reason)
1390
+ const after = applySteeringEdits(summary, result.edits)
1391
+ expect(review.validate(after)).toEqual([])
1392
+ const items = review.view(after).nodes[0]!.body![1]!.block!.items!
1393
+ expect(items[0]).not.toHaveProperty("thread")
1394
+ expect(items[0]).not.toHaveProperty("note")
1395
+ expect(items[1]!.thread?.entries).toEqual([{ author: "me", text: "reopen it" }])
1396
+ })
1397
+
1373
1398
  it("accepts a paragraph anchor in a prose-only document", () => {
1374
1399
  const result = review.annotate(
1375
1400
  "Just some prose.\n",
@@ -16,7 +16,7 @@ export type GitWrite =
16
16
  /** A landed step's trailing report, rendered by `GitScript.ts` into a `printf` line. */
17
17
  export type Outcome =
18
18
  | { readonly kind: "transition"; readonly from: StateName; readonly to: StateName }
19
- | { readonly kind: "commit"; readonly subject: StateName | string }
19
+ | { readonly kind: "commit"; readonly subject: string }
20
20
  | { readonly kind: "note"; readonly text: string }
21
21
 
22
22
  /**
@@ -14,7 +14,7 @@ import { tmpdir } from "node:os"
14
14
  import { execFileSync, execSync, spawnSync } from "node:child_process"
15
15
  // Imported directly (not just from *.test.ts) — a leak of this module into
16
16
  // the shipped CLI bundle would try to inline `vitest` and fail the build
17
- // loudly (`deps.alwaysBundle` in `tsdown.config.ts`), a guard alongside the
17
+ // loudly (`deps.alwaysBundle` in `vite.config.ts`), a guard alongside the
18
18
  // `TEST_DOUBLE_SENTINEL` check.
19
19
  import { afterEach, beforeEach, describe, expect, it } from "vitest"
20
20
  import { Effect, Exit, Option } from "effect"
@@ -32,9 +32,9 @@ const nextStatusLine = (next: BeatDocument["next"]): string =>
32
32
 
33
33
  /** Builds `{[key]: value}` for each entry whose value isn't `undefined` — the shared "omit absent optional fields" shape the header uses. */
34
34
  const definedFields = (
35
- entries: readonly (readonly [string, unknown])[],
36
- ): Record<string, unknown> => {
37
- const result: Record<string, unknown> = {}
35
+ entries: readonly (readonly [string, string | undefined])[],
36
+ ): Record<string, string> => {
37
+ const result: Record<string, string> = {}
38
38
  for (const [key, value] of entries) if (value !== undefined) result[key] = value
39
39
  return result
40
40
  }
@@ -18,7 +18,7 @@ const toSelection = (value: unknown): Selection => {
18
18
  if (typeof value === "object") {
19
19
  return { kind: "value", text: JSON.stringify(value) }
20
20
  }
21
- return { kind: "value", text: String(value) }
21
+ return { kind: "value", text: String(value as string | number | boolean | bigint | undefined) }
22
22
  }
23
23
 
24
24
  /** A segment that resolves to no key at all (missing from an object, out-of-range or non-numeric against an array) — distinct from a present key holding `undefined`. */