@pmelab/gtd 27.1.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.
Files changed (45) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +16 -9
  3. package/dist/chunks/registry-CP3YuV9j.mjs +101 -0
  4. package/dist/chunks/{reserved-BL9krPao.mjs → reserved-BPdVZkgv.mjs} +66 -73
  5. package/dist/chunks/tail-iLAMoigl.mjs +81 -0
  6. package/dist/gtd.bundle.mjs +5 -2
  7. package/dist/testkit.mjs +17 -2
  8. package/dist/workflows/cleanup.mjs +37 -0
  9. package/dist/workflows/feature.mjs +119 -5
  10. package/dist/workflows/fix.mjs +2 -1
  11. package/dist/workflows/review.mjs +2 -1
  12. package/dist/workflows/rollout.mjs +33 -0
  13. package/dist/workflows.mjs +362 -25
  14. package/package.json +11 -1
  15. package/src/flags/index.test.ts +28 -0
  16. package/src/flags/index.ts +9 -4
  17. package/src/testkit/EmittedScriptRecognizer.ts +21 -0
  18. package/src/testkit/EmittedScripts.test.ts +9 -0
  19. package/src/testkit/steps.ts +26 -0
  20. package/src/testkit/testkit.feature +8 -1
  21. package/src/workflows/cleanup/cleanup.feature +323 -0
  22. package/src/workflows/cleanup/cleanup.test.ts +272 -0
  23. package/src/workflows/cleanup/cleanup.ts +52 -0
  24. package/src/workflows/cleanup/index.ts +6 -0
  25. package/src/workflows/feature/index.ts +1 -0
  26. package/src/workflows/feature/refactor.test.ts +466 -0
  27. package/src/workflows/feature/refactor.ts +178 -0
  28. package/src/workflows/index.ts +4 -1
  29. package/src/workflows/rollout/index.ts +6 -0
  30. package/src/workflows/rollout/rollout.feature +214 -0
  31. package/src/workflows/rollout/rollout.test.ts +167 -0
  32. package/src/workflows/rollout/rollout.ts +44 -0
  33. package/src/workflows/shared/access.ts +2 -1
  34. package/src/workflows/shared/health.test.ts +103 -0
  35. package/src/workflows/shared/health.ts +32 -7
  36. package/src/workflows/shared/options.test.ts +30 -0
  37. package/src/workflows/shared/options.ts +20 -0
  38. package/src/workflows/shared/registry.test.ts +305 -0
  39. package/src/workflows/shared/registry.ts +158 -0
  40. package/src/workflows/shared/skills.test.ts +6 -0
  41. package/src/workflows/shared/skills.ts +5 -1
  42. package/src/workflows/shared/tail.test.ts +36 -2
  43. package/src/workflows/shared/tail.ts +30 -18
  44. package/src/workflows/shared/text.ts +35 -0
  45. package/src/workflows/shared/vars.ts +1 -0
@@ -1,6 +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 unwindFailedMessage, A as architectureDecomposePrompt, B as designSystem, C as fixRisks, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as packagesItemBuildingPrompt, I as buildReviewTriageMessage, J as reviewEditsCapture, K as packagesItemFixSuitePrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as startGateBlockedMessage, R as deferNotesScript, S as fixCheck, T as reviewMissing, V as designTriagePrompt, W as idleMessage, X as reviewNotesCapture, Z as reviewerSystem, _ as FEEDBACK, b as awaitReview, c as refusePendingHandOff, d as defaults, et as unwindFailure, f as envDefaults, g as ARCHITECTURE, h as healthy, i as skills, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as tail, m as gate, n as summary, o as closeReview, q as reviewEditNotesCapture, r as access, t as steering, u as shipped, v as REQUIREMENTS, w as planner, x as coder, y as REVIEW, z as designGateAnswerMessage } from "../chunks/reserved-BL9krPao.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";
4
5
  //#region src/workflows/feature/steps.ts
5
6
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
6
7
  const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
@@ -281,7 +282,7 @@ const triage$1 = (severities) => async (notes) => {
281
282
  return [n.id, severity.tier === "could" ? "defer" : "edit"];
282
283
  }));
283
284
  };
284
- const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
285
+ const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
285
286
  /**
286
287
  * A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
287
288
  * reviews and signs off or comments, and each note is judged: edits go to
@@ -294,7 +295,7 @@ const review = (base, escalations = { rounds: 0 }) => {
294
295
  return review$1(base, {
295
296
  file: REVIEW,
296
297
  feedback: REQUIREMENTS,
297
- isCode,
298
+ isCode: isCode$1,
298
299
  reviewing: (b, carry) => {
299
300
  deferred = [];
300
301
  return reviewing(b, carry);
@@ -404,7 +405,120 @@ const ordinaryStart = async () => {
404
405
  };
405
406
  const feature = shipped(ordinaryStart);
406
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
407
521
  //#region src/workflows/feature/index.ts
408
522
  var feature_default = feature;
409
523
  //#endregion
410
- 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,6 +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 { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, a as REVIEW_ORIG, d as defaults, f as envDefaults, i as skills, n as summary, p as baseline, r as access, s as handOffTail, t as steering, u as shipped, x as coder, y as REVIEW } from "../chunks/reserved-BL9krPao.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";
4
5
  //#region src/workflows/fix/fix.ts
5
6
  /**
6
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,5 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { Y as reviewGateBlockedMessage, c as refusePendingHandOff, d as defaults, f as envDefaults, i as skills, m as gate, n as summary, r as access, s as handOffTail, t as steering, u as shipped } from "../chunks/reserved-BL9krPao.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";
3
4
  //#region src/workflows/review/review.ts
4
5
  /**
5
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -0,0 +1,33 @@
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-BPdVZkgv.mjs";
3
+ import { i as requireFlag, n as flipProblems } from "../chunks/registry-CP3YuV9j.mjs";
4
+ //#region src/workflows/rollout/rollout.ts
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.`;
6
+ const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
7
+ const rolloutFlow = async () => {
8
+ const registry = vars.flagRegistry ?? "";
9
+ const name = vars.flag ?? "";
10
+ requireFlag("rollout", registry, name, false);
11
+ const base = head();
12
+ const before = read(registry) ?? "";
13
+ const guard = () => {
14
+ const strays = changesSince(base).paths.filter((path) => path !== registry && path !== ".gtd/FEEDBACK.md");
15
+ if (strays.length > 0) refuse(`gtd land: rollout-only: ${strays.join(", ")} changed — rollout may only set defaultOn: true on "${name}" in ${registry}`);
16
+ const problems = flipProblems(before, read(registry) ?? "", name);
17
+ if (problems.length > 0) refuse(`gtd land: rollout-only: other edits in ${registry}: ${problems.join("; ")}`);
18
+ };
19
+ await scope("rollout", async () => {
20
+ await agent("flipping", flippingPrompt(name, registry), {
21
+ label: `Rolling out ${name}`,
22
+ model: coder()
23
+ });
24
+ guard();
25
+ await gate("suite", suiteBlockedMessage(), guard);
26
+ });
27
+ };
28
+ const rollout = flagShipped(rolloutFlow);
29
+ //#endregion
30
+ //#region src/workflows/rollout/index.ts
31
+ var rollout_default = rollout;
32
+ //#endregion
33
+ export { access, rollout_default as default, defaults, envDefaults, rollout, rolloutFlow, skills, steering, summary };