@pmelab/gtd 27.2.0 → 27.3.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": "27.2.0",
3
+ "version": "27.3.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"
@@ -591,6 +591,26 @@ ${fixFeedbackPrompt}
591
591
  then legitimately report itself already satisfied later
592
592
  - Leave everything uncommitted and finish your turn — do not commit
593
593
  `;
594
+ const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
595
+ - Change only these paths, nothing else — a test file only when it is
596
+ one of them:
597
+ ${strays.map((path) => ` - \`${path}\``).join("\n")}
598
+ - Introduce no new flag read and add no second read of an existing one:
599
+ every match of \`${pattern}\` may move, never increase in number`;
600
+ const refactorBuildingPrompt = (pkg, strays, pattern, reference) => `${stateFileRules}
601
+ - The package to implement is \`${pkg}\`; never delete it
602
+ ${refactorRules(strays, pattern)}
603
+ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
604
+ earlier attempt at this change. Read it for ideas; never apply,
605
+ cherry-pick or check it out — write a fresh refactor
606
+ `}- Leave everything uncommitted and finish your turn — do not commit
607
+ `;
608
+ const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
609
+ ${fixFeedbackPrompt}
610
+ ${refactorRules(strays, pattern)}
611
+ - Never do another package's work, even when it would green the suite
612
+ - Leave everything uncommitted and finish your turn — do not commit
613
+ `;
594
614
  const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
595
615
  \`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
596
616
  is in history. Run \`gtd judge answer\` and pipe a
@@ -1161,9 +1181,9 @@ const guarded = (turn, guard) => async (...a) => {
1161
1181
  await turn(...a);
1162
1182
  guard?.();
1163
1183
  };
1164
- const guardedSteps = (guard) => ({
1184
+ const guardedSteps = (guard, describeGuard) => ({
1165
1185
  limit: escalationSteps.limit,
1166
- describe: guarded(escalationSteps.describe, guard),
1186
+ describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
1167
1187
  stop: guarded(escalationSteps.stop, guard),
1168
1188
  exhausted: guarded(escalationSteps.exhausted, guard)
1169
1189
  });
@@ -1215,7 +1235,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
1215
1235
  fixesSoFar: options.fixesSoFar ?? 0,
1216
1236
  sameFailure,
1217
1237
  escalations: options.escalations ?? { rounds: 0 },
1218
- escalation: guardedSteps(options.guard)
1238
+ escalation: guardedSteps(options.guard, options.describeGuard)
1219
1239
  });
1220
1240
  /**
1221
1241
  * Hold the process at `<name>.blocked` until the suite is green. `guard` runs
@@ -1236,6 +1256,7 @@ const defaults = {
1236
1256
  judgeIdenticalMinP: "0.7",
1237
1257
  reviewNoteActionable: "0.7",
1238
1258
  reviewSeverityMinP: "0.7",
1259
+ refactorFlagPattern: "\\bGTD_FLAG_\\w+",
1239
1260
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
1240
1261
  };
1241
1262
  /** Environment settings. */
@@ -1253,6 +1274,7 @@ const ShippedOptions = Struct({
1253
1274
  judgeIdenticalMinP: setting,
1254
1275
  reviewNoteActionable: setting,
1255
1276
  reviewSeverityMinP: setting,
1277
+ refactorFlagPattern: setting,
1256
1278
  qualityReviews: setting,
1257
1279
  testCommand: setting,
1258
1280
  plannerModel: setting,
@@ -1314,6 +1336,9 @@ const skills = (vars) => ({
1314
1336
  "build.review.fix.nits": ["incremental-implementation", "code-simplification"],
1315
1337
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
1316
1338
  notes: ["incremental-implementation", "code-simplification"],
1339
+ refactor: ["ponytail"],
1340
+ "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
1341
+ "refactor.health": ["ponytail", "debugging-and-error-recovery"],
1317
1342
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
1318
1343
  });
1319
1344
  //#endregion
@@ -1337,4 +1362,4 @@ const steering = {
1337
1362
  [REVIEW]: "review"
1338
1363
  };
1339
1364
  //#endregion
1340
- export { reviewerSystem as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixRisks as C, reviewing as D, reviewQuality as E, buildReviewCollectingPrompt as F, handOffAwaitReviewMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, packagesItemFixSuitePrompt as J, idleMessage as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewNotesCapture as Q, builderSystem as R, fixQuality as S, reviewMissing as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, reviewEditsCapture as X, reviewEditNotesCapture as Y, reviewGateBlockedMessage as Z, REQUIREMENTS as _, flagShipped as a, coder as b, envDefaults as c, gate as d, startGateBlockedMessage as et, guarded as f, QUALITY as g, FEEDBACK as h, skills as i, architectureDecomposePrompt as j, architectSystem as k, baseline as l, ARCHITECTURE as m, summary as n, unwindFailure as nt, shipped as o, healthy as p, packagesItemBuildingPrompt as q, access as r, defaults as s, steering as t, unwindFailedMessage as tt, escalation as u, REVIEW as v, planner as w, fixCheck as x, awaitReview as y, deferNotesScript as z };
1365
+ export { reviewGateBlockedMessage as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixRisks as C, reviewing as D, reviewQuality as E, buildReviewCollectingPrompt as F, handOffAwaitReviewMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, packagesItemFixSuitePrompt as J, idleMessage as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewEditsCapture as Q, builderSystem as R, fixQuality as S, reviewMissing as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, refactorFixPrompt as X, refactorBuildingPrompt as Y, reviewEditNotesCapture as Z, REQUIREMENTS as _, flagShipped as a, coder as b, envDefaults as c, gate as d, reviewNotesCapture as et, guarded as f, QUALITY as g, FEEDBACK as h, skills as i, unwindFailure as it, architectureDecomposePrompt as j, architectSystem as k, baseline as l, ARCHITECTURE as m, summary as n, startGateBlockedMessage as nt, shipped as o, healthy as p, packagesItemBuildingPrompt as q, access as r, unwindFailedMessage as rt, defaults as s, steering as t, reviewerSystem as tt, escalation as u, REVIEW as v, planner as w, fixCheck as x, awaitReview as y, deferNotesScript as z };
@@ -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-B2QaImnN.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-BPdVZkgv.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";
package/dist/testkit.mjs CHANGED
@@ -12961,6 +12961,17 @@ const recognizeDeferNotes = (repo, block) => {
12961
12961
  repo.writeFile(".gtd/TODO.md", `${old}${sep}${lines.join("\n")}\n`);
12962
12962
  return { kind: "applied" };
12963
12963
  };
12964
+ /** `mkdir -p <dir>` then `printf '%s\n' <quoted lines> > <path>`: a flow's one-file write. Re-rendered from what was extracted, so any other shape stays unrecognized. */
12965
+ const recognizeWriteLines = (repo, block) => {
12966
+ const [mkdir, printf, ...rest] = block.split("\n");
12967
+ if (rest.length > 0 || !mkdir?.startsWith("mkdir -p ") || !printf?.startsWith("printf '%s\\n' ")) return;
12968
+ const tokens = extractQuotedTokens(printf.slice(14));
12969
+ const path = tokens.pop();
12970
+ if (path === void 0 || !path.includes("/")) return void 0;
12971
+ if (`mkdir -p ${path.slice(0, path.lastIndexOf("/"))}\nprintf '%s\\n' ${tokens.map(shellQuote).join(" ")} > ${shellQuote(path)}` !== block) return void 0;
12972
+ repo.writeFile(path, `${tokens.join("\n")}\n`);
12973
+ return { kind: "applied" };
12974
+ };
12964
12975
  /** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
12965
12976
  const recognizersFor = (repo, commands) => [
12966
12977
  (block) => recognizeSetFlags(block),
@@ -12970,6 +12981,7 @@ const recognizersFor = (repo, commands) => [
12970
12981
  (block) => recognizeBinaryGuard(commands, block),
12971
12982
  (block) => recognizeGitBuilders(repo, block),
12972
12983
  (block) => recognizeDeferNotes(repo, block),
12984
+ (block) => recognizeWriteLines(repo, block),
12973
12985
  (block) => recognizeFailurePromptWrapper(repo, commands, block),
12974
12986
  (block) => recognizePresentationSubshell(repo, commands, block),
12975
12987
  (block) => recognizeModeContradictionSkipNotice(block),
@@ -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-B2QaImnN.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BnhIxCYv.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-BPdVZkgv.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-iLAMoigl.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
- import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
1
+ import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
2
2
  import { review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as reviewerSystem, A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as packagesItemFixSuitePrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as reviewNotesCapture, R as builderSystem, T as reviewMissing, V as designSystem, X as reviewEditsCapture, Y as reviewEditNotesCapture, _ as REQUIREMENTS, b as coder, c as envDefaults, d as gate, et as startGateBlockedMessage, h as FEEDBACK, i as skills, j as architectureDecomposePrompt, k as architectSystem, m as ARCHITECTURE, n as summary, nt as unwindFailure, o as shipped, p as healthy, q as packagesItemBuildingPrompt, r as access, s as defaults, t as steering, tt as unwindFailedMessage, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-B2QaImnN.mjs";
4
- import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-BnhIxCYv.mjs";
3
+ import { A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as packagesItemFixSuitePrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as reviewEditsCapture, R as builderSystem, T as reviewMissing, V as designSystem, X as refactorFixPrompt, Y as refactorBuildingPrompt, Z as reviewEditNotesCapture, _ as REQUIREMENTS, b as coder, c as envDefaults, d as gate, et as reviewNotesCapture, h as FEEDBACK, i as skills, it as unwindFailure, j as architectureDecomposePrompt, k as architectSystem, m as ARCHITECTURE, n as summary, nt as startGateBlockedMessage, o as shipped, p as healthy, q as packagesItemBuildingPrompt, r as access, rt as unwindFailedMessage, s as defaults, t as steering, tt as reviewerSystem, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-BPdVZkgv.mjs";
4
+ import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-iLAMoigl.mjs";
5
5
  //#region src/workflows/feature/steps.ts
6
6
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
7
7
  const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
@@ -282,7 +282,7 @@ const triage$1 = (severities) => async (notes) => {
282
282
  return [n.id, severity.tier === "could" ? "defer" : "edit"];
283
283
  }));
284
284
  };
285
- const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
285
+ const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
286
286
  /**
287
287
  * A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
288
288
  * reviews and signs off or comments, and each note is judged: edits go to
@@ -295,7 +295,7 @@ const review = (base, escalations = { rounds: 0 }) => {
295
295
  return review$1(base, {
296
296
  file: REVIEW,
297
297
  feedback: REQUIREMENTS,
298
- isCode,
298
+ isCode: isCode$1,
299
299
  reviewing: (b, carry) => {
300
300
  deferred = [];
301
301
  return reviewing(b, carry);
@@ -405,7 +405,120 @@ const ordinaryStart = async () => {
405
405
  };
406
406
  const feature = shipped(ordinaryStart);
407
407
  //#endregion
408
+ //#region src/workflows/feature/refactor.ts
409
+ const REFACTOR_PACKAGE = ".gtd/REFACTOR.md";
410
+ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
411
+ const requireWithin = (allowed) => {
412
+ const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
413
+ if (outside.length === 0) return;
414
+ refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
415
+ };
416
+ const flagExpression = (pattern) => {
417
+ if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
418
+ let expression;
419
+ try {
420
+ expression = new RegExp(pattern, "g");
421
+ } catch (error) {
422
+ throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`);
423
+ }
424
+ if (expression.test("")) throw new Error("gtd: refactorFlagPattern must not match the empty string");
425
+ return expression;
426
+ };
427
+ const countFlags = (text, expression, into) => {
428
+ let zeroLength = false;
429
+ for (const [name] of text.matchAll(expression)) if (name === "") zeroLength = true;
430
+ else into.set(name, (into.get(name) ?? 0) + 1);
431
+ return zeroLength;
432
+ };
433
+ const listPaths = (paths) => paths.map((path) => ` - ${path}`).join("\n");
434
+ const requireNoNewFlags = (pattern, since) => {
435
+ const expression = flagExpression(pattern);
436
+ const before = /* @__PURE__ */ new Map();
437
+ const after = /* @__PURE__ */ new Map();
438
+ const unreadable = [];
439
+ const zeroLength = [];
440
+ for (const { path, status, before: was, after: now } of changesSince(since)) {
441
+ if (!isCode(path)) continue;
442
+ const oldSide = status === "added" ? "" : was;
443
+ const newSide = status === "deleted" ? "" : now;
444
+ if (oldSide === void 0 || newSide === void 0) {
445
+ unreadable.push(path);
446
+ continue;
447
+ }
448
+ countFlags(oldSide, expression, before);
449
+ if (countFlags(newSide, expression, after)) zeroLength.push(path);
450
+ }
451
+ if (unreadable.length > 0) refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`);
452
+ if (zeroLength.length > 0) refuse(`gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`);
453
+ const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0));
454
+ if (raised.length === 0) return;
455
+ refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
456
+ };
457
+ const packageLines = (strays, pattern, reference) => [
458
+ "# Behaviour-neutral refactor",
459
+ "",
460
+ "## Scope (read-only)",
461
+ "",
462
+ ...strays.map((path) => `- \`${path}\``),
463
+ "",
464
+ "This list is fixed when the package is derived: editing it here widens or narrows nothing.",
465
+ "",
466
+ "## Rules",
467
+ "",
468
+ "- The suite's outcome must not change",
469
+ "- Change only the paths above; a test file only when it is one of them",
470
+ `- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
471
+ "",
472
+ "## Reference",
473
+ "",
474
+ reference === void 0 ? "None." : `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
475
+ ""
476
+ ];
477
+ const deriveScript = (lines) => `mkdir -p .gtd
478
+ printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
479
+ `;
480
+ const refactorFirst = async (shape, options = {}) => {
481
+ if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return false;
482
+ const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
483
+ const expression = flagExpression(pattern);
484
+ const { strays } = shape;
485
+ const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
486
+ if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
487
+ const reference = options.reference?.trim() ? options.reference : void 0;
488
+ return scope({
489
+ name: "refactor",
490
+ access: { write: [...strays, ".gtd/**"] }
491
+ }, async () => {
492
+ await run("deriving", deriveScript(packageLines(strays, pattern, reference)), {
493
+ label: "Deriving the refactor package",
494
+ file: REFACTOR_PACKAGE
495
+ });
496
+ const from = head();
497
+ const guard = () => {
498
+ requireWithin(strays);
499
+ requireNoNewFlags(pattern, from);
500
+ };
501
+ await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference), {
502
+ label: "Refactoring",
503
+ model: coder(),
504
+ system: builderSystem()
505
+ });
506
+ guard();
507
+ await healthy(async () => {
508
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
509
+ label: "Fixing the check",
510
+ file: FEEDBACK,
511
+ model: coder(),
512
+ system: builderSystem()
513
+ });
514
+ guard();
515
+ }, { describeGuard: guard });
516
+ await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
517
+ return true;
518
+ });
519
+ };
520
+ //#endregion
408
521
  //#region src/workflows/feature/index.ts
409
522
  var feature_default = feature;
410
523
  //#endregion
411
- export { access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, requireDeclaredTests, skills, steering, summary, triage, unwind };
524
+ export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, refactorFirst, requireDeclaredTests, requireNoNewFlags, requireWithin, skills, steering, summary, triage, unwind };
@@ -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-B2QaImnN.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-BnhIxCYv.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-BPdVZkgv.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-iLAMoigl.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 { Z as reviewGateBlockedMessage, c as envDefaults, d as gate, i as skills, n as summary, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-B2QaImnN.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BnhIxCYv.mjs";
2
+ import { $ as reviewGateBlockedMessage, c as envDefaults, d as gate, i as skills, n as summary, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-BPdVZkgv.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-iLAMoigl.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-B2QaImnN.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-BPdVZkgv.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.`;
@@ -250,9 +250,9 @@ const check = async (name, command, options) => {
250
250
  }), { label });
251
251
  return !wrote(report);
252
252
  };
253
- const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
253
+ const isCode$2 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
254
254
  /** The last step's changes outside `.gtd/`. */
255
- const codeChanges = () => changes().filter((c) => isCode$1(c.path));
255
+ const codeChanges = () => changes().filter((c) => isCode$2(c.path));
256
256
  /** Refuse a turn whose only change deletes `file` — unless it says it found nothing to do. */
257
257
  const requireProgress = (file) => {
258
258
  const change = changes().get(file);
@@ -17235,6 +17235,26 @@ ${fixFeedbackPrompt}
17235
17235
  then legitimately report itself already satisfied later
17236
17236
  - Leave everything uncommitted and finish your turn — do not commit
17237
17237
  `;
17238
+ const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
17239
+ - Change only these paths, nothing else — a test file only when it is
17240
+ one of them:
17241
+ ${strays.map((path) => ` - \`${path}\``).join("\n")}
17242
+ - Introduce no new flag read and add no second read of an existing one:
17243
+ every match of \`${pattern}\` may move, never increase in number`;
17244
+ const refactorBuildingPrompt = (pkg, strays, pattern, reference) => `${stateFileRules}
17245
+ - The package to implement is \`${pkg}\`; never delete it
17246
+ ${refactorRules(strays, pattern)}
17247
+ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
17248
+ earlier attempt at this change. Read it for ideas; never apply,
17249
+ cherry-pick or check it out — write a fresh refactor
17250
+ `}- Leave everything uncommitted and finish your turn — do not commit
17251
+ `;
17252
+ const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
17253
+ ${fixFeedbackPrompt}
17254
+ ${refactorRules(strays, pattern)}
17255
+ - Never do another package's work, even when it would green the suite
17256
+ - Leave everything uncommitted and finish your turn — do not commit
17257
+ `;
17238
17258
  const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
17239
17259
  \`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
17240
17260
  is in history. Run \`gtd judge answer\` and pipe a
@@ -17809,9 +17829,9 @@ const guarded = (turn, guard) => async (...a) => {
17809
17829
  await turn(...a);
17810
17830
  guard?.();
17811
17831
  };
17812
- const guardedSteps = (guard) => ({
17832
+ const guardedSteps = (guard, describeGuard) => ({
17813
17833
  limit: escalationSteps.limit,
17814
- describe: guarded(escalationSteps.describe, guard),
17834
+ describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
17815
17835
  stop: guarded(escalationSteps.stop, guard),
17816
17836
  exhausted: guarded(escalationSteps.exhausted, guard)
17817
17837
  });
@@ -17863,7 +17883,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
17863
17883
  fixesSoFar: options.fixesSoFar ?? 0,
17864
17884
  sameFailure,
17865
17885
  escalations: options.escalations ?? { rounds: 0 },
17866
- escalation: guardedSteps(options.guard)
17886
+ escalation: guardedSteps(options.guard, options.describeGuard)
17867
17887
  });
17868
17888
  /**
17869
17889
  * Hold the process at `<name>.blocked` until the suite is green. `guard` runs
@@ -37198,6 +37218,7 @@ const defaults = {
37198
37218
  judgeIdenticalMinP: "0.7",
37199
37219
  reviewNoteActionable: "0.7",
37200
37220
  reviewSeverityMinP: "0.7",
37221
+ refactorFlagPattern: "\\bGTD_FLAG_\\w+",
37201
37222
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
37202
37223
  };
37203
37224
  /** Environment settings. */
@@ -37215,6 +37236,7 @@ const ShippedOptions = Struct({
37215
37236
  judgeIdenticalMinP: setting,
37216
37237
  reviewNoteActionable: setting,
37217
37238
  reviewSeverityMinP: setting,
37239
+ refactorFlagPattern: setting,
37218
37240
  qualityReviews: setting,
37219
37241
  testCommand: setting,
37220
37242
  plannerModel: setting,
@@ -37614,7 +37636,7 @@ const triage$1 = (severities) => async (notes) => {
37614
37636
  return [n.id, severity.tier === "could" ? "defer" : "edit"];
37615
37637
  }));
37616
37638
  };
37617
- const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
37639
+ const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
37618
37640
  /**
37619
37641
  * A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
37620
37642
  * reviews and signs off or comments, and each note is judged: edits go to
@@ -37627,7 +37649,7 @@ const review$1 = (base, escalations = { rounds: 0 }) => {
37627
37649
  return review$2(base, {
37628
37650
  file: REVIEW,
37629
37651
  feedback: REQUIREMENTS,
37630
- isCode,
37652
+ isCode: isCode$1,
37631
37653
  reviewing: (b, carry) => {
37632
37654
  deferred = [];
37633
37655
  return reviewing(b, carry);
@@ -37756,6 +37778,9 @@ const skills = (vars) => ({
37756
37778
  "build.review.fix.nits": ["incremental-implementation", "code-simplification"],
37757
37779
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
37758
37780
  notes: ["incremental-implementation", "code-simplification"],
37781
+ refactor: ["ponytail"],
37782
+ "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
37783
+ "refactor.health": ["ponytail", "debugging-and-error-recovery"],
37759
37784
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
37760
37785
  });
37761
37786
  //#endregion
@@ -37779,6 +37804,119 @@ const steering = {
37779
37804
  [REVIEW]: "review"
37780
37805
  };
37781
37806
  //#endregion
37807
+ //#region src/workflows/feature/refactor.ts
37808
+ const REFACTOR_PACKAGE = ".gtd/REFACTOR.md";
37809
+ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
37810
+ const requireWithin = (allowed) => {
37811
+ const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
37812
+ if (outside.length === 0) return;
37813
+ refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
37814
+ };
37815
+ const flagExpression = (pattern) => {
37816
+ if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
37817
+ let expression;
37818
+ try {
37819
+ expression = new RegExp(pattern, "g");
37820
+ } catch (error) {
37821
+ throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`);
37822
+ }
37823
+ if (expression.test("")) throw new Error("gtd: refactorFlagPattern must not match the empty string");
37824
+ return expression;
37825
+ };
37826
+ const countFlags = (text, expression, into) => {
37827
+ let zeroLength = false;
37828
+ for (const [name] of text.matchAll(expression)) if (name === "") zeroLength = true;
37829
+ else into.set(name, (into.get(name) ?? 0) + 1);
37830
+ return zeroLength;
37831
+ };
37832
+ const listPaths = (paths) => paths.map((path) => ` - ${path}`).join("\n");
37833
+ const requireNoNewFlags = (pattern, since) => {
37834
+ const expression = flagExpression(pattern);
37835
+ const before = /* @__PURE__ */ new Map();
37836
+ const after = /* @__PURE__ */ new Map();
37837
+ const unreadable = [];
37838
+ const zeroLength = [];
37839
+ for (const { path, status, before: was, after: now } of changesSince(since)) {
37840
+ if (!isCode(path)) continue;
37841
+ const oldSide = status === "added" ? "" : was;
37842
+ const newSide = status === "deleted" ? "" : now;
37843
+ if (oldSide === void 0 || newSide === void 0) {
37844
+ unreadable.push(path);
37845
+ continue;
37846
+ }
37847
+ countFlags(oldSide, expression, before);
37848
+ if (countFlags(newSide, expression, after)) zeroLength.push(path);
37849
+ }
37850
+ if (unreadable.length > 0) refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`);
37851
+ if (zeroLength.length > 0) refuse(`gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`);
37852
+ const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0));
37853
+ if (raised.length === 0) return;
37854
+ refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
37855
+ };
37856
+ const packageLines = (strays, pattern, reference) => [
37857
+ "# Behaviour-neutral refactor",
37858
+ "",
37859
+ "## Scope (read-only)",
37860
+ "",
37861
+ ...strays.map((path) => `- \`${path}\``),
37862
+ "",
37863
+ "This list is fixed when the package is derived: editing it here widens or narrows nothing.",
37864
+ "",
37865
+ "## Rules",
37866
+ "",
37867
+ "- The suite's outcome must not change",
37868
+ "- Change only the paths above; a test file only when it is one of them",
37869
+ `- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
37870
+ "",
37871
+ "## Reference",
37872
+ "",
37873
+ reference === void 0 ? "None." : `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
37874
+ ""
37875
+ ];
37876
+ const deriveScript = (lines) => `mkdir -p .gtd
37877
+ printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
37878
+ `;
37879
+ const refactorFirst = async (shape, options = {}) => {
37880
+ if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return false;
37881
+ const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
37882
+ const expression = flagExpression(pattern);
37883
+ const { strays } = shape;
37884
+ const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
37885
+ if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
37886
+ const reference = options.reference?.trim() ? options.reference : void 0;
37887
+ return scope({
37888
+ name: "refactor",
37889
+ access: { write: [...strays, ".gtd/**"] }
37890
+ }, async () => {
37891
+ await run("deriving", deriveScript(packageLines(strays, pattern, reference)), {
37892
+ label: "Deriving the refactor package",
37893
+ file: REFACTOR_PACKAGE
37894
+ });
37895
+ const from = head$1();
37896
+ const guard = () => {
37897
+ requireWithin(strays);
37898
+ requireNoNewFlags(pattern, from);
37899
+ };
37900
+ await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference), {
37901
+ label: "Refactoring",
37902
+ model: coder(),
37903
+ system: builderSystem()
37904
+ });
37905
+ guard();
37906
+ await healthy(async () => {
37907
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
37908
+ label: "Fixing the check",
37909
+ file: FEEDBACK,
37910
+ model: coder(),
37911
+ system: builderSystem()
37912
+ });
37913
+ guard();
37914
+ }, { describeGuard: guard });
37915
+ await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
37916
+ return true;
37917
+ });
37918
+ };
37919
+ //#endregion
37782
37920
  //#region src/workflows/review/review.ts
37783
37921
  /**
37784
37922
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -37996,4 +38134,4 @@ var workflows_default = feature;
37996
38134
  */
37997
38135
  const doors = {};
37998
38136
  //#endregion
37999
- export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, requireDeclaredTests, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, rollout, rolloutFlow, skills, steering, summary, tail, threads, triage, unwind };
38137
+ export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, refactorFirst, requireDeclaredTests, requireNoNewFlags, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, rollout, rolloutFlow, skills, steering, summary, tail, threads, triage, unwind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "27.2.0",
3
+ "version": "27.3.0",
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": {
@@ -667,6 +667,26 @@ const recognizeDeferNotes = (repo: InMemRepo, block: string): BlockOutcome | und
667
667
  return { kind: "applied" }
668
668
  }
669
669
 
670
+ /** `mkdir -p <dir>` then `printf '%s\n' <quoted lines> > <path>`: a flow's one-file write. Re-rendered from what was extracted, so any other shape stays unrecognized. */
671
+ const recognizeWriteLines = (repo: InMemRepo, block: string): BlockOutcome | undefined => {
672
+ const [mkdir, printf, ...rest] = block.split("\n")
673
+ if (
674
+ rest.length > 0 ||
675
+ !mkdir?.startsWith("mkdir -p ") ||
676
+ !printf?.startsWith("printf '%s\\n' ")
677
+ ) {
678
+ return undefined
679
+ }
680
+ const tokens = extractQuotedTokens(printf.slice("printf '%s\\n' ".length))
681
+ const path = tokens.pop()
682
+ if (path === undefined || !path.includes("/")) return undefined
683
+ const dir = path.slice(0, path.lastIndexOf("/"))
684
+ const rendered = `mkdir -p ${dir}\nprintf '%s\\n' ${tokens.map(shellQuote).join(" ")} > ${shellQuote(path)}`
685
+ if (rendered !== block) return undefined
686
+ repo.writeFile(path, `${tokens.join("\n")}\n`)
687
+ return { kind: "applied" }
688
+ }
689
+
670
690
  /** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
671
691
  const recognizersFor = (
672
692
  repo: InMemRepo,
@@ -679,6 +699,7 @@ const recognizersFor = (
679
699
  (block) => recognizeBinaryGuard(commands, block),
680
700
  (block) => recognizeGitBuilders(repo, block),
681
701
  (block) => recognizeDeferNotes(repo, block),
702
+ (block) => recognizeWriteLines(repo, block),
682
703
  (block) => recognizeFailurePromptWrapper(repo, commands, block),
683
704
  (block) => recognizePresentationSubshell(repo, commands, block),
684
705
  (block) => recognizeModeContradictionSkipNotice(block),
@@ -275,6 +275,15 @@ for (const makeTier of gitTiers) {
275
275
  })
276
276
  })
277
277
 
278
+ it("a mkdir + printf write lands one line per token, whatever the token holds", () => {
279
+ const lines = String.raw`mkdir -p .gtd/packages
280
+ printf '%s\n' 'a' '' 'it'\''s' '100%' '-x' 'a\b' '.gtd/packages/00-x.md' > '.gtd/packages/00-x.md'`
281
+ expect(run(lines).ok).toBe(true)
282
+ expect(read(".gtd/packages/00-x.md")).toBe(
283
+ "a\n\nit's\n100%\n-x\na\\b\n.gtd/packages/00-x.md\n",
284
+ )
285
+ })
286
+
278
287
  describe("commitAll", () => {
279
288
  it("a staging hook's edit lands in the commit", () => {
280
289
  t.seed.stagingHook("readme.txt", "hooked")
@@ -3,6 +3,7 @@ import { feature } from "./feature.js"
3
3
  export * from "./steps.js"
4
4
  export * from "./planning.js"
5
5
  export * from "./packages.js"
6
+ export * from "./refactor.js"
6
7
  export { buildTail, type ReviewOutcome } from "./review.js"
7
8
  export * from "./feature.js"
8
9
  export * from "../shared/reserved.js"