@pmelab/gtd 27.6.0 → 27.7.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.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +25 -14
  3. package/claude/hooks/assist.ts +35 -2
  4. package/claude/hooks/delegate.ts +22 -0
  5. package/claude/hooks/handoff.ts +13 -4
  6. package/claude/hooks/jobs.ts +67 -0
  7. package/claude/hooks/paste.ts +17 -0
  8. package/claude/hooks/register.tsx +126 -65
  9. package/claude/hooks/ship.ts +7 -2
  10. package/claude/hooks/tools.ts +32 -0
  11. package/claude/hooks/wording.ts +11 -0
  12. package/dist/chunks/{reserved-Df8GeSHw.mjs → reserved-UdSuHDUJ.mjs} +2 -2
  13. package/dist/chunks/{tail-DmIfEAI4.mjs → tail-fj60rQ3y.mjs} +1 -1
  14. package/dist/gtd.bundle.mjs +3 -3
  15. package/dist/testkit.mjs +30 -18
  16. package/dist/workflows/cleanup.mjs +2 -2
  17. package/dist/workflows/feature.mjs +3 -3
  18. package/dist/workflows/fix.mjs +2 -2
  19. package/dist/workflows/review.mjs +2 -2
  20. package/dist/workflows/rollout.mjs +1 -1
  21. package/dist/workflows.mjs +3 -3
  22. package/package.json +1 -1
  23. package/src/cli/Cli.ts +2 -2
  24. package/src/testkit/EmittedScriptRecognizer.test.ts +41 -0
  25. package/src/testkit/EmittedScriptRecognizer.ts +16 -7
  26. package/src/testkit/TestRepo.test.ts +79 -1
  27. package/src/testkit/TestRepo.ts +17 -6
  28. package/src/testkit/steps.ts +44 -1
  29. package/src/testkit/testkit.feature +35 -0
  30. package/src/workflows/cleanup/cleanup-path.feature +94 -0
  31. package/src/workflows/cleanup/cleanup.feature +0 -29
  32. package/src/workflows/feature/fast-path.feature +114 -0
  33. package/src/workflows/feature/one-step-land.feature +108 -0
  34. package/src/workflows/feature/refactor-first.feature +137 -0
  35. package/src/workflows/feature/review.test.ts +9 -3
  36. package/src/workflows/feature/review.ts +3 -3
  37. package/src/workflows/feature/risk-tier.feature +0 -63
  38. package/src/workflows/feature/seam-rejection.feature +146 -0
  39. package/src/workflows/feature/seam.feature +0 -67
  40. package/src/workflows/shared/steps.ts +1 -2
  41. package/src/workflows/shared/tail.test.ts +18 -9
  42. package/src/workflows/shared/workflows.steps.ts +5 -4
@@ -2,6 +2,9 @@
2
2
 
3
3
  import type { Stop } from "../types"
4
4
  import type { Beat } from "./drive"
5
+ import type { Shipped } from "./ship"
6
+ import { JOB_NAME } from "./tools"
7
+ import type { JobName } from "./tools"
5
8
 
6
9
  const step = (s: { label?: string; state?: string }) => s.label ?? s.state ?? "the next step"
7
10
 
@@ -53,3 +56,11 @@ export function pushText(stop: Stop, repo: string, url?: string) {
53
56
  if (stop.kind !== "gate") return at
54
57
  return url ? `${at}. Open: ${url}` : at
55
58
  }
59
+
60
+ const capitalised = (s: string) => s[0]!.toUpperCase() + s.slice(1)
61
+
62
+ export const jobHeadline = (job: JobName, outcome: Shipped) =>
63
+ `${outcome.ok ? "✔" : "✘"} ${capitalised(JOB_NAME[job])} ${outcome.ok ? "finished" : "failed"}`
64
+
65
+ export const jobPushText = (job: JobName, outcome: Shipped, repo: string) =>
66
+ `${repo}: ${JOB_NAME[job]} ${outcome.ok ? "finished" : "failed"}. ${outcome.text}`.trim()
@@ -1234,13 +1234,13 @@ const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildRevi
1234
1234
  system: reviewerSystem(),
1235
1235
  allowEmpty: true
1236
1236
  });
1237
- const awaitReview = (base, message = buildReviewAwaitReviewMessage(base), acceptClean = false) => human("review.await-review", {
1237
+ const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
1238
1238
  message,
1239
1239
  label: "Awaiting your review",
1240
1240
  file: REVIEW,
1241
1241
  mode: "review",
1242
1242
  base,
1243
- ...acceptClean ? { acceptClean } : {}
1243
+ acceptClean: true
1244
1244
  });
1245
1245
  /** The round at commit `round` left no `.gtd/REVIEW.md`. */
1246
1246
  const reviewMissing = (round) => human("review.review-missing", {
@@ -1,6 +1,6 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
2
2
  import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-Df8GeSHw.mjs";
3
+ import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-UdSuHDUJ.mjs";
4
4
  //#region src/workflows/shared/tail.ts
5
5
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
6
6
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -41695,7 +41695,7 @@ const FLAGS = [
41695
41695
  help: [
41696
41696
  "(gtd land, with --cost) tag that cost's model",
41697
41697
  "(gtd judge run, llm answerer) the claude model; omitted:",
41698
- "GTD_JUDGE_MODEL, then judge.json, then haiku"
41698
+ "GTD_JUDGE_MODEL, then judge.json, then sonnet"
41699
41699
  ]
41700
41700
  },
41701
41701
  {
@@ -42088,7 +42088,7 @@ const COMMAND_ROWS = [
42088
42088
  "--answers <path> or GTD_JUDGE_ANSWERS; questions the file",
42089
42089
  "does not cover are left out. --provider jev asks TypeSafe",
42090
42090
  "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
42091
- "on PATH (Claude Code only), model haiku unless --model.",
42091
+ "on PATH (Claude Code only), model sonnet unless --model.",
42092
42092
  "No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
42093
42093
  "judge.json, else jev when TYPESAFE_API_KEY is set and no",
42094
42094
  "model is given, else llm; --model likewise falls back to",
@@ -58805,7 +58805,7 @@ var DEFAULT_MODEL, TIMEOUT_MS, SYSTEM_PROMPT, fail$1, unreadable, levelsOf, opti
58805
58805
  var init_llm = __esmMin((() => {
58806
58806
  init_criteria();
58807
58807
  init_io();
58808
- DEFAULT_MODEL = "haiku";
58808
+ DEFAULT_MODEL = "sonnet";
58809
58809
  TIMEOUT_MS = 18e4;
58810
58810
  SYSTEM_PROMPT = "You are a strict, impartial judge. Answer each question from the supplied evidence alone.\nReply only with the structured answer; never use tools or ask questions.";
58811
58811
  fail$1 = (message) => {
package/dist/testkit.mjs CHANGED
@@ -10825,7 +10825,7 @@ const FLAGS = [
10825
10825
  help: [
10826
10826
  "(gtd land, with --cost) tag that cost's model",
10827
10827
  "(gtd judge run, llm answerer) the claude model; omitted:",
10828
- "GTD_JUDGE_MODEL, then judge.json, then haiku"
10828
+ "GTD_JUDGE_MODEL, then judge.json, then sonnet"
10829
10829
  ]
10830
10830
  },
10831
10831
  {
@@ -11218,7 +11218,7 @@ const COMMAND_ROWS = [
11218
11218
  "--answers <path> or GTD_JUDGE_ANSWERS; questions the file",
11219
11219
  "does not cover are left out. --provider jev asks TypeSafe",
11220
11220
  "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
11221
- "on PATH (Claude Code only), model haiku unless --model.",
11221
+ "on PATH (Claude Code only), model sonnet unless --model.",
11222
11222
  "No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
11223
11223
  "judge.json, else jev when TYPESAFE_API_KEY is set and no",
11224
11224
  "model is given, else llm; --model likewise falls back to",
@@ -13076,18 +13076,22 @@ const recognizeGtdUncheck = (repo, block) => {
13076
13076
  * definition) keeps the `@inmem` tier green.
13077
13077
  */
13078
13078
  const recognizeDidNotRunComment = (block) => block === "# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn" ? { kind: "noop" } : void 0;
13079
- /** `combinedScript`'s optional half. ALWAYS reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`, where the subshell's exit status is swallowed and must never fail the outer script. */
13079
+ /** `combinedScript`'s optional half. Reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`. Exception: under `git: false` any unrecognized inner block fails the outer script (git or not), so the in-memory twin knowingly diverges from `sh` rather than run git unseen. */
13080
13080
  const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0].replace(/\\n$/, "")}\n`;
13081
13081
  const PRESENTATION_SUBSHELL_PREFIX = `${PRESENTATION_ONLY_COMMENT}\n(\n`;
13082
13082
  const PRESENTATION_SUBSHELL_SUFFIX = `\n) || ${PRESENTATION_FAILURE_WARNING}`;
13083
- const recognizePresentationSubshell = (repo, commands, block) => {
13083
+ const recognizePresentationSubshell = (repo, commands, block, git) => {
13084
13084
  if (!block.startsWith(PRESENTATION_SUBSHELL_PREFIX) || !block.endsWith(PRESENTATION_SUBSHELL_SUFFIX)) return;
13085
13085
  const inner = block.slice(PRESENTATION_SUBSHELL_PREFIX.length, block.length - PRESENTATION_SUBSHELL_SUFFIX.length);
13086
- const result = applyEmittedScript(repo, commands, inner);
13086
+ const result = applyEmittedScript(repo, commands, inner, { git });
13087
13087
  if (result.ok) return {
13088
13088
  kind: "noop",
13089
13089
  output: result.output
13090
13090
  };
13091
+ if (!git && result.error?.startsWith(UNRECOGNIZED_PREFIX)) return {
13092
+ kind: "failed",
13093
+ error: `${result.output}${result.error}`
13094
+ };
13091
13095
  return {
13092
13096
  kind: "noop",
13093
13097
  output: `${result.output}${PRESENTATION_FAILURE_TEXT}`
@@ -13194,17 +13198,17 @@ const recognizeWriteLines = (repo, block) => {
13194
13198
  return { kind: "applied" };
13195
13199
  };
13196
13200
  /** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
13197
- const recognizersFor = (repo, commands) => [
13201
+ const recognizersFor = (repo, commands, git) => [
13198
13202
  (block) => recognizeSetFlags(block),
13199
13203
  (block) => recognizeDidNotRunComment(block),
13200
13204
  (block) => recognizePrecondition(repo, block),
13201
13205
  (block) => recognizeFileExistsGuard(repo, block),
13202
13206
  (block) => recognizeBinaryGuard(commands, block),
13203
- (block) => recognizeGitBuilders(repo, block),
13207
+ (block) => git ? recognizeGitBuilders(repo, block) : void 0,
13204
13208
  (block) => recognizeDeferNotes(repo, block),
13205
13209
  (block) => recognizeWriteLines(repo, block),
13206
13210
  (block) => recognizeFailurePromptWrapper(repo, commands, block),
13207
- (block) => recognizePresentationSubshell(repo, commands, block),
13211
+ (block) => recognizePresentationSubshell(repo, commands, block, git),
13208
13212
  (block) => recognizeModeContradictionSkipNotice(block),
13209
13213
  (block) => recognizeModeContradictionCheck(repo, commands, block),
13210
13214
  (block) => recognizeGtdCheck(repo, block),
@@ -13227,7 +13231,7 @@ const recognizeBlock = (recognizers, block) => {
13227
13231
  * non-zero scripted-command exit — exactly like `set -euo pipefail` stops a
13228
13232
  * real script. No block after the failing one is applied.
13229
13233
  */
13230
- const applyEmittedScript = (repo, commands, script) => {
13234
+ const applyEmittedScript = (repo, commands, script, { git = true } = {}) => {
13231
13235
  const flow = applyFlowScript(repo, commands, script);
13232
13236
  if (flow !== void 0) return flow.ok ? {
13233
13237
  ok: true,
@@ -13239,10 +13243,10 @@ const applyEmittedScript = (repo, commands, script) => {
13239
13243
  output: flow.output,
13240
13244
  error: flow.error ?? ""
13241
13245
  };
13242
- return script.startsWith("#!") ? unrecognized(script) : applyBlocks(repo, commands, script);
13246
+ return script.startsWith("#!") ? unrecognized(script) : applyBlocks(repo, commands, script, git);
13243
13247
  };
13244
- const applyBlocks = (repo, commands, script) => {
13245
- const recognizers = recognizersFor(repo, commands);
13248
+ const applyBlocks = (repo, commands, script, git) => {
13249
+ const recognizers = recognizersFor(repo, commands, git);
13246
13250
  let output = "";
13247
13251
  for (const block of splitBlocks(script)) {
13248
13252
  const outcome = recognizeBlock(recognizers, block);
@@ -13266,11 +13270,12 @@ const failedAt = (outcome, output) => {
13266
13270
  error: outcome.error
13267
13271
  };
13268
13272
  };
13273
+ const UNRECOGNIZED_PREFIX = "unrecognized script block: ";
13269
13274
  const unrecognized = (block, output = "") => ({
13270
13275
  ok: false,
13271
13276
  status: 1,
13272
13277
  output,
13273
- error: `unrecognized script block: ${block.split("\n")[0]}`
13278
+ error: `${UNRECOGNIZED_PREFIX}${block.split("\n")[0]}`
13274
13279
  });
13275
13280
  //#endregion
13276
13281
  //#region src/testkit/TestRepo.ts
@@ -13322,10 +13327,17 @@ const LINES = [
13322
13327
  repo.writeFile(dest, content);
13323
13328
  }]
13324
13329
  ];
13325
- const runLine = (repo, line) => {
13326
- const known = LINES.find(([pattern]) => pattern.test(line));
13327
- if (known === void 0) throw new Error(`testkit: cannot run \`${line}\` in memory — give the step an actor instead`);
13328
- known[1](repo, words(line));
13330
+ const knownLine = (line) => LINES.find(([pattern]) => pattern.test(line));
13331
+ const cannotRun = (error) => /* @__PURE__ */ new Error(`testkit: cannot run script in memory — give the step an actor instead\n${error}`);
13332
+ const runLines = (repo, script) => {
13333
+ const lines = script.split("\n").map((line) => line.trim());
13334
+ const known = lines.map(knownLine);
13335
+ if (known.every(Boolean)) {
13336
+ known.forEach((k, i) => k[1](repo, words(lines[i])));
13337
+ return;
13338
+ }
13339
+ const applied = applyEmittedScript(repo, /* @__PURE__ */ new Map(), script, { git: false });
13340
+ if (!applied.ok) throw cannotRun(applied.error ?? "");
13329
13341
  };
13330
13342
  const failed = (what, result) => /* @__PURE__ */ new Error(`${what} exited ${result.exitCode}\n${result.stderr}${result.stdout}`);
13331
13343
  const testRepo = (options) => {
@@ -13407,7 +13419,7 @@ const testRepo = (options) => {
13407
13419
  show: (ref, path) => repo.fileAtRef(ref, path) ?? void 0,
13408
13420
  runScript: (script) => {
13409
13421
  const flow = applyFlowScript(repo, /* @__PURE__ */ new Map(), script);
13410
- if (flow === void 0) for (const line of script.split("\n")) runLine(repo, line.trim());
13422
+ if (flow === void 0) runLines(repo, script);
13411
13423
  else if (!flow.ok) throw new Error(`testkit: ${flow.error ?? ""}`);
13412
13424
  },
13413
13425
  gtd,
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-Df8GeSHw.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DmIfEAI4.mjs";
2
+ import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-fj60rQ3y.mjs";
4
4
  import { i as requireFlag, r as registryEntry, t as flagReferences } from "../chunks/registry-CP3YuV9j.mjs";
5
5
  //#region src/workflows/cleanup/cleanup.ts
6
6
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
@@ -1,7 +1,7 @@
1
1
  import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, seamGate, start, vars } from "../flows.mjs";
2
2
  import { review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as refactorFixPrompt, A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as implementFixPrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as refactorBuildingPrompt, R as builderSystem, T as reviewMissing, V as designSystem, X as packagesItemBuildingPrompt, Y as naivePassPrompt, Z as packagesItemFixSuitePrompt, _ as REQUIREMENTS, at as seamByHandMessage, b as coder, c as envDefaults, ct as unwindFailedMessage, d as gate, et as reviewEditNotesCapture, h as FEEDBACK, i as skills, it as reviewerSystem, j as architectureDecomposePrompt, k as architectSystem, lt as unwindFailure, m as ARCHITECTURE, n as summary, o as shipped, ot as seamSteerMessage, p as healthy, q as implementBuildingPrompt, r as access, rt as reviewNotesCapture, s as defaults, st as startGateBlockedMessage, t as steering, tt as reviewEditsCapture, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-Df8GeSHw.mjs";
4
- import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-DmIfEAI4.mjs";
3
+ import { $ as refactorFixPrompt, A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as implementFixPrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as refactorBuildingPrompt, R as builderSystem, T as reviewMissing, V as designSystem, X as packagesItemBuildingPrompt, Y as naivePassPrompt, Z as packagesItemFixSuitePrompt, _ as REQUIREMENTS, at as seamByHandMessage, b as coder, c as envDefaults, ct as unwindFailedMessage, d as gate, et as reviewEditNotesCapture, h as FEEDBACK, i as skills, it as reviewerSystem, j as architectureDecomposePrompt, k as architectSystem, lt as unwindFailure, m as ARCHITECTURE, n as summary, o as shipped, ot as seamSteerMessage, p as healthy, q as implementBuildingPrompt, r as access, rt as reviewNotesCapture, s as defaults, st as startGateBlockedMessage, t as steering, tt as reviewEditsCapture, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-UdSuHDUJ.mjs";
4
+ import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-fj60rQ3y.mjs";
5
5
  //#region src/workflows/feature/tier.ts
6
6
  /** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
7
7
  const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
@@ -355,7 +355,7 @@ const review = (base, escalations = { rounds: 0 }) => {
355
355
  awaitReview: (b) => {
356
356
  const notes = deferred;
357
357
  deferred = [];
358
- return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes), true) : awaitReview(b);
358
+ return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
359
359
  },
360
360
  reviewMissing,
361
361
  collecting,
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-Df8GeSHw.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DmIfEAI4.mjs";
3
+ import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-UdSuHDUJ.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-fj60rQ3y.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { c as envDefaults, d as gate, i as skills, n as summary, nt as reviewGateBlockedMessage, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-Df8GeSHw.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DmIfEAI4.mjs";
2
+ import { c as envDefaults, d as gate, i as skills, n as summary, nt as reviewGateBlockedMessage, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-fj60rQ3y.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, head, read, refuse, scope, vars } from "../flows.mjs";
2
- import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-Df8GeSHw.mjs";
2
+ import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
3
3
  import { i as requireFlag, n as flipProblems } from "../chunks/registry-CP3YuV9j.mjs";
4
4
  //#region src/workflows/rollout/rollout.ts
5
5
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
@@ -17995,13 +17995,13 @@ const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildRevi
17995
17995
  system: reviewerSystem(),
17996
17996
  allowEmpty: true
17997
17997
  });
17998
- const awaitReview = (base, message = buildReviewAwaitReviewMessage(base), acceptClean = false) => human("review.await-review", {
17998
+ const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
17999
17999
  message,
18000
18000
  label: "Awaiting your review",
18001
18001
  file: REVIEW,
18002
18002
  mode: "review",
18003
18003
  base,
18004
- ...acceptClean ? { acceptClean } : {}
18004
+ acceptClean: true
18005
18005
  });
18006
18006
  /** The round at commit `round` left no `.gtd/REVIEW.md`. */
18007
18007
  const reviewMissing = (round) => human("review.review-missing", {
@@ -37911,7 +37911,7 @@ const review$1 = (base, escalations = { rounds: 0 }) => {
37911
37911
  awaitReview: (b) => {
37912
37912
  const notes = deferred;
37913
37913
  deferred = [];
37914
- return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes), true) : awaitReview(b);
37914
+ return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
37915
37915
  },
37916
37916
  reviewMissing,
37917
37917
  collecting,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "27.6.0",
3
+ "version": "27.7.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": {
package/src/cli/Cli.ts CHANGED
@@ -241,7 +241,7 @@ const FLAGS: readonly FlagRow[] = [
241
241
  help: [
242
242
  "(gtd land, with --cost) tag that cost's model",
243
243
  "(gtd judge run, llm answerer) the claude model; omitted:",
244
- "GTD_JUDGE_MODEL, then judge.json, then haiku",
244
+ "GTD_JUDGE_MODEL, then judge.json, then sonnet",
245
245
  ],
246
246
  },
247
247
  {
@@ -673,7 +673,7 @@ const COMMAND_ROWS: readonly CommandRow[] = [
673
673
  "--answers <path> or GTD_JUDGE_ANSWERS; questions the file",
674
674
  "does not cover are left out. --provider jev asks TypeSafe",
675
675
  "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
676
- "on PATH (Claude Code only), model haiku unless --model.",
676
+ "on PATH (Claude Code only), model sonnet unless --model.",
677
677
  "No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
678
678
  "judge.json, else jev when TYPESAFE_API_KEY is set and no",
679
679
  "model is given, else llm; --model likewise falls back to",
@@ -630,6 +630,18 @@ describe("applyEmittedScript — Emit.ts's combinedScript (the whole stdout of a
630
630
  expect(repo.lastCommitMessage()).toBe("gtd(human): idle")
631
631
  })
632
632
 
633
+ it("fails an unrecognized optional block under git: false", () => {
634
+ const repo = new InMemRepo()
635
+ const result = applyEmittedScript(
636
+ repo,
637
+ NO_COMMANDS,
638
+ combinedScript("set -eu", "totally-unrecognized-command"),
639
+ { git: false },
640
+ )
641
+ expect(result.ok).toBe(false)
642
+ expect(result.error).toContain("totally-unrecognized-command")
643
+ })
644
+
633
645
  it("still fails loudly on an unrecognized block outside the presentation-only subshell", () => {
634
646
  const repo = new InMemRepo()
635
647
  const script = `${combinedScript("totally-unrecognized-required-command", "")}`
@@ -691,3 +703,32 @@ describe("applyEmittedScript — commitClean", () => {
691
703
  expect(snapshot(repo)).toEqual(before)
692
704
  })
693
705
  })
706
+
707
+ describe("applyEmittedScript — git: false reaches through the presentation subshell", () => {
708
+ it("refuses a git builder wrapped in combinedScript and changes nothing", () => {
709
+ const repo = new InMemRepo()
710
+ repo.writeFile("a.txt", "1")
711
+ const before = snapshot(repo)
712
+ const result = applyEmittedScript(
713
+ repo,
714
+ NO_COMMANDS,
715
+ combinedScript("set -eu", commitAll("chore: sneaky")),
716
+ { git: false },
717
+ )
718
+ expect(result.ok).toBe(false)
719
+ expect(result.error).toMatch(/unrecognized script block/)
720
+ expect(snapshot(repo)).toEqual(before)
721
+ })
722
+
723
+ it("still applies a non-git write-lines optional half", () => {
724
+ const repo = new InMemRepo()
725
+ const result = applyEmittedScript(
726
+ repo,
727
+ NO_COMMANDS,
728
+ combinedScript("set -eu", "mkdir -p d\nprintf '%s\\n' 'x' > 'd/a.txt'"),
729
+ { git: false },
730
+ )
731
+ expect(result).toMatchObject({ ok: true })
732
+ expect(repo.readFile("d/a.txt")).toBe("x\n")
733
+ })
734
+ })
@@ -563,7 +563,7 @@ const recognizeGtdUncheck = (repo: InMemRepo, block: string): BlockOutcome | und
563
563
  const recognizeDidNotRunComment = (block: string): BlockOutcome | undefined =>
564
564
  block === DID_NOT_RUN_COMMENT ? { kind: "noop" } : undefined
565
565
 
566
- /** `combinedScript`'s optional half. ALWAYS reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`, where the subshell's exit status is swallowed and must never fail the outer script. */
566
+ /** `combinedScript`'s optional half. Reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`. Exception: under `git: false` any unrecognized inner block fails the outer script (git or not), so the in-memory twin knowingly diverges from `sh` rather than run git unseen. */
567
567
  const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0]!.replace(/\\n$/, "")}\n`
568
568
  const PRESENTATION_SUBSHELL_PREFIX = `${PRESENTATION_ONLY_COMMENT}\n(\n`
569
569
  const PRESENTATION_SUBSHELL_SUFFIX = `\n) || ${PRESENTATION_FAILURE_WARNING}`
@@ -572,6 +572,7 @@ const recognizePresentationSubshell = (
572
572
  repo: InMemRepo,
573
573
  commands: ReadonlyMap<string, ScriptedCommand>,
574
574
  block: string,
575
+ git: boolean,
575
576
  ): BlockOutcome | undefined => {
576
577
  if (
577
578
  !block.startsWith(PRESENTATION_SUBSHELL_PREFIX) ||
@@ -583,8 +584,11 @@ const recognizePresentationSubshell = (
583
584
  PRESENTATION_SUBSHELL_PREFIX.length,
584
585
  block.length - PRESENTATION_SUBSHELL_SUFFIX.length,
585
586
  )
586
- const result = applyEmittedScript(repo, commands, inner)
587
+ const result = applyEmittedScript(repo, commands, inner, { git })
587
588
  if (result.ok) return { kind: "noop", output: result.output }
589
+ if (!git && result.error?.startsWith(UNRECOGNIZED_PREFIX)) {
590
+ return { kind: "failed", error: `${result.output}${result.error}` }
591
+ }
588
592
  return { kind: "noop", output: `${result.output}${PRESENTATION_FAILURE_TEXT}` }
589
593
  }
590
594
 
@@ -703,17 +707,18 @@ const recognizeWriteLines = (repo: InMemRepo, block: string): BlockOutcome | und
703
707
  const recognizersFor = (
704
708
  repo: InMemRepo,
705
709
  commands: ReadonlyMap<string, ScriptedCommand>,
710
+ git: boolean,
706
711
  ): ReadonlyArray<(block: string) => BlockOutcome | undefined> => [
707
712
  (block) => recognizeSetFlags(block),
708
713
  (block) => recognizeDidNotRunComment(block),
709
714
  (block) => recognizePrecondition(repo, block),
710
715
  (block) => recognizeFileExistsGuard(repo, block),
711
716
  (block) => recognizeBinaryGuard(commands, block),
712
- (block) => recognizeGitBuilders(repo, block),
717
+ (block) => (git ? recognizeGitBuilders(repo, block) : undefined),
713
718
  (block) => recognizeDeferNotes(repo, block),
714
719
  (block) => recognizeWriteLines(repo, block),
715
720
  (block) => recognizeFailurePromptWrapper(repo, commands, block),
716
- (block) => recognizePresentationSubshell(repo, commands, block),
721
+ (block) => recognizePresentationSubshell(repo, commands, block, git),
717
722
  (block) => recognizeModeContradictionSkipNotice(block),
718
723
  (block) => recognizeModeContradictionCheck(repo, commands, block),
719
724
  (block) => recognizeGtdCheck(repo, block),
@@ -746,6 +751,7 @@ export const applyEmittedScript = (
746
751
  repo: InMemRepo,
747
752
  commands: ReadonlyMap<string, ScriptedCommand>,
748
753
  script: string,
754
+ { git = true }: { readonly git?: boolean } = {},
749
755
  ): AppliedScriptResult => {
750
756
  const flow = applyFlowScript(repo, commands, script)
751
757
  if (flow !== undefined) {
@@ -753,15 +759,16 @@ export const applyEmittedScript = (
753
759
  ? { ok: true, status: 0, output: flow.output }
754
760
  : { ok: false, status: 1, output: flow.output, error: flow.error ?? "" }
755
761
  }
756
- return script.startsWith("#!") ? unrecognized(script) : applyBlocks(repo, commands, script)
762
+ return script.startsWith("#!") ? unrecognized(script) : applyBlocks(repo, commands, script, git)
757
763
  }
758
764
 
759
765
  const applyBlocks = (
760
766
  repo: InMemRepo,
761
767
  commands: ReadonlyMap<string, ScriptedCommand>,
762
768
  script: string,
769
+ git: boolean,
763
770
  ): AppliedScriptResult => {
764
- const recognizers = recognizersFor(repo, commands)
771
+ const recognizers = recognizersFor(repo, commands, git)
765
772
  let output = ""
766
773
  for (const block of splitBlocks(script)) {
767
774
  const outcome = recognizeBlock(recognizers, block)
@@ -786,9 +793,11 @@ const failedAt = (
786
793
  }
787
794
  }
788
795
 
796
+ const UNRECOGNIZED_PREFIX = "unrecognized script block: "
797
+
789
798
  const unrecognized = (block: string, output = ""): AppliedScriptResult => ({
790
799
  ok: false,
791
800
  status: 1,
792
801
  output,
793
- error: `unrecognized script block: ${block.split("\n")[0]}`,
802
+ error: `${UNRECOGNIZED_PREFIX}${block.split("\n")[0]}`,
794
803
  })
@@ -1,4 +1,17 @@
1
1
  import { describe, expect, it } from "vitest"
2
+ import {
3
+ commitAll,
4
+ combinedScript,
5
+ commitAsIs,
6
+ commitClean,
7
+ deleteRef,
8
+ discardPending,
9
+ hardResetTo,
10
+ keepTurn,
11
+ mixedResetTo,
12
+ softResetTo,
13
+ updateRef,
14
+ } from "../emit/index.js"
2
15
  import { agent, human, moveScript, read, removeScript, run } from "../flows/index.js"
3
16
  import { runsItsScript, testRepo } from "./TestRepo.js"
4
17
 
@@ -93,7 +106,72 @@ describe("testRepo", () => {
93
106
 
94
107
  it("refuses to fake a script it cannot run", () => {
95
108
  const repo = testRepo({ workflow: { default: tiny } })
96
- expect(() => repo.runScript("npm test\n")).toThrow(/cannot run `npm test` in memory/)
109
+ expect(() => repo.runScript("npm test\n")).toThrow(/cannot run script in memory[\s\S]*npm test/)
110
+ })
111
+
112
+ it("carries the recognizer's error when a git-free fallback fails", () => {
113
+ const repo = testRepo({ workflow: { default: tiny } })
114
+ expect(() => repo.runScript("printf 'x' > 'y'\n")).toThrow(
115
+ "testkit: cannot run script in memory — give the step an actor instead\nunrecognized script block: printf 'x' > 'y'",
116
+ )
117
+ })
118
+
119
+ it("names the block that failed, not an earlier line that ran", () => {
120
+ const repo = testRepo({ workflow: { default: tiny } })
121
+ let message = ""
122
+ try {
123
+ repo.runScript("mkdir -p d\nprintf '%s\\n' 'x' > 'd/a.txt'\n\nnpm test\n")
124
+ } catch (e) {
125
+ message = (e as Error).message
126
+ }
127
+ expect(message).toContain("unrecognized script block: npm test")
128
+ expect(message).not.toContain("printf")
129
+ })
130
+
131
+ it.each([
132
+ ["commitAll", commitAll("chore: sneaky")],
133
+ ["commitClean", commitClean("chore: sneaky")],
134
+ ["commitAsIs", commitAsIs("chore: sneaky")],
135
+ ["keepTurn", keepTurn("refs/x", "chore: sneaky")],
136
+ ["discardPending", discardPending()],
137
+ ["softResetTo", softResetTo("HEAD~1")],
138
+ ["mixedResetTo", mixedResetTo("HEAD~1")],
139
+ ["hardResetTo", hardResetTo("HEAD~1")],
140
+ ["updateRef", updateRef("refs/x", "HEAD")],
141
+ ["deleteRef", deleteRef("refs/x")],
142
+ ])("refuses a %s script instead of mutating git in memory", (_, script) => {
143
+ const repo = testRepo({ workflow: { default: tiny } })
144
+ repo.write("a.txt", "dirty\n")
145
+ const before = repo.subjects()
146
+ expect(() => repo.runScript(script)).toThrow(/unrecognized script block/)
147
+ expect(repo.subjects()).toEqual(before)
148
+ })
149
+
150
+ it.each([
151
+ ["commitAll", commitAll("chore: sneaky")],
152
+ ["commitClean", commitClean("chore: sneaky")],
153
+ ["commitAsIs", commitAsIs("chore: sneaky")],
154
+ ["keepTurn", keepTurn("refs/x", "chore: sneaky")],
155
+ ["discardPending", discardPending()],
156
+ ["softResetTo", softResetTo("HEAD~1")],
157
+ ["mixedResetTo", mixedResetTo("HEAD~1")],
158
+ ["hardResetTo", hardResetTo("HEAD~1")],
159
+ ["updateRef", updateRef("refs/x", "HEAD")],
160
+ ["deleteRef", deleteRef("refs/x")],
161
+ ])("refuses a wrapped %s script instead of mutating git in memory", (_, script) => {
162
+ const repo = testRepo({ workflow: { default: tiny } })
163
+ repo.write("a.txt", "dirty\n")
164
+ const before = repo.subjects()
165
+ expect(() => repo.runScript(combinedScript("set -eu", script))).toThrow(
166
+ /cannot run script in memory[\s\S]*unrecognized script block/,
167
+ )
168
+ expect(repo.subjects()).toEqual(before)
169
+ })
170
+
171
+ it("applies a write block `LINES` lacks through the git-free recognizers", () => {
172
+ const repo = testRepo({ workflow: { default: tiny } })
173
+ repo.runScript("mkdir -p d\nprintf '%s\\n' 'echo $(git rev-parse HEAD)' > 'd/a.txt'\n")
174
+ expect(repo.read("d/a.txt")).toBe("echo $(git rev-parse HEAD)\n")
97
175
  })
98
176
 
99
177
  it("reads a file as committed", () => {
@@ -150,12 +150,23 @@ const LINES: ReadonlyArray<readonly [RegExp, (repo: InMemRepo, words: string[])
150
150
  ],
151
151
  ]
152
152
 
153
- const runLine = (repo: InMemRepo, line: string): void => {
154
- const known = LINES.find(([pattern]) => pattern.test(line))
155
- if (known === undefined) {
156
- throw new Error(`testkit: cannot run \`${line}\` in memory — give the step an actor instead`)
153
+ const knownLine = (line: string) => LINES.find(([pattern]) => pattern.test(line))
154
+
155
+ const cannotRun = (error: string): Error =>
156
+ new Error(`testkit: cannot run script in memory — give the step an actor instead\n${error}`)
157
+
158
+ // A script with a line `LINES` lacks goes to the contract-tested recognizers
159
+ // whole, minus the git builders: they would apply a commit/reset/update-ref in
160
+ // memory, so any git-mutating block is refused whatever its spelling.
161
+ const runLines = (repo: InMemRepo, script: string): void => {
162
+ const lines = script.split("\n").map((line) => line.trim())
163
+ const known = lines.map(knownLine)
164
+ if (known.every(Boolean)) {
165
+ known.forEach((k, i) => k![1](repo, words(lines[i]!)))
166
+ return
157
167
  }
158
- known[1](repo, words(line))
168
+ const applied = applyEmittedScript(repo, new Map(), script, { git: false })
169
+ if (!applied.ok) throw cannotRun(applied.error ?? "")
159
170
  }
160
171
 
161
172
  const failed = (what: string, result: CapturedCliResult): Error =>
@@ -251,7 +262,7 @@ export const testRepo = (options: TestRepoOptions): TestRepo => {
251
262
  show: (ref, path) => repo.fileAtRef(ref, path) ?? undefined,
252
263
  runScript: (script) => {
253
264
  const flow = applyFlowScript(repo, new Map(), script)
254
- if (flow === undefined) for (const line of script.split("\n")) runLine(repo, line.trim())
265
+ if (flow === undefined) runLines(repo, script)
255
266
  else if (!flow.ok) throw new Error(`testkit: ${flow.error ?? ""}`)
256
267
  },
257
268
  gtd,