@pmelab/gtd 1.9.1 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -388261,978 +388261,1013 @@ var formatFile = (path15) => Effect_exports.gen(function* () {
388261
388261
  }
388262
388262
  }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))));
388263
388263
 
388264
- // src/Events.ts
388265
- var TODO_FILE = "TODO.md";
388266
- var GTD_DIR = ".gtd";
388267
- var REVIEW_FILE = "REVIEW.md";
388268
- var FEEDBACK_FILE = "FEEDBACK.md";
388269
- var ERRORS_FILE = "ERRORS.md";
388270
- var HEALTH_FILE = "HEALTH.md";
388271
- var EMPTY_FAILURE_SENTINEL = "Test command failed with no output (exit code non-zero).";
388272
- var UNANSWERED_MARKER = "<!-- user answers here -->";
388273
- var HEALTH_CHECK_SUBJECT = "gtd: health-check";
388274
- var HEALTH_FIX_SUBJECT = "gtd: health-fix";
388275
- var NEW_TASK_SUBJECT = "gtd: new task";
388276
- var GRILLING_SUBJECT = "gtd: grilling";
388277
- var PLANNING_SUBJECT = "gtd: planning";
388278
- var BUILDING_SUBJECT = "gtd: building";
388279
- var ERRORS_SUBJECT = "gtd: errors";
388280
- var FEEDBACK_SUBJECT = "gtd: feedback";
388281
- var PACKAGE_DONE_SUBJECT = "gtd: package done";
388282
- var AWAITING_REVIEW_SUBJECT = "gtd: awaiting review";
388283
- var DONE_SUBJECT = "gtd: done";
388284
- var REVIEW_FEEDBACK_SUBJECT = "gtd: review feedback";
388285
- var REVIEWING_SUBJECT = "gtd: reviewing";
388286
- var SQUASH_MSG_FILE = "SQUASH_MSG.md";
388287
- var STEERING_FILES = [
388288
- TODO_FILE,
388289
- REVIEW_FILE,
388290
- FEEDBACK_FILE,
388291
- ERRORS_FILE,
388292
- HEALTH_FILE,
388293
- SQUASH_MSG_FILE
388294
- ];
388295
- var WORKFLOW_FILE_EXCLUDES = [...STEERING_FILES, GTD_DIR];
388296
- var isGtdPath = (path15) => path15 === GTD_DIR || path15.startsWith(`${GTD_DIR}/`);
388297
- var isSteeringFile = (path15) => STEERING_FILES.includes(path15);
388298
- var isUncommittedStatus = (status2) => status2.includes("?") || status2.includes("A");
388299
- var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
388300
- var unquoteGitPath = (raw) => {
388301
- if (!raw.startsWith('"')) return raw;
388302
- const inner = raw.slice(1, raw.endsWith('"') ? raw.length - 1 : raw.length);
388303
- const bytes = [];
388304
- const chars = [];
388305
- const flushBytes = () => {
388306
- if (bytes.length === 0) return;
388307
- const buf = Buffer.from(bytes);
388308
- chars.push(buf.toString("utf8"));
388309
- bytes.length = 0;
388310
- };
388311
- let i = 0;
388312
- while (i < inner.length) {
388313
- if (inner[i] !== "\\") {
388314
- flushBytes();
388315
- chars.push(inner[i]);
388316
- i++;
388317
- continue;
388318
- }
388319
- const esc = inner[i + 1];
388320
- if (esc === void 0) {
388321
- flushBytes();
388322
- chars.push("\\");
388323
- i++;
388324
- continue;
388325
- }
388326
- if (esc >= "0" && esc <= "7") {
388327
- const oct = inner.slice(i + 1, i + 4);
388328
- bytes.push(parseInt(oct, 8));
388329
- i += 4;
388330
- continue;
388331
- }
388332
- flushBytes();
388333
- switch (esc) {
388334
- case "n":
388335
- chars.push("\n");
388336
- break;
388337
- case "t":
388338
- chars.push(" ");
388339
- break;
388340
- case "r":
388341
- chars.push("\r");
388342
- break;
388343
- case "\\":
388344
- chars.push("\\");
388345
- break;
388346
- case '"':
388347
- chars.push('"');
388348
- break;
388349
- default:
388350
- chars.push("\\", esc);
388351
- }
388352
- i += 2;
388264
+ // src/Machine.ts
388265
+ var HEALTH_SQUASH_SENTINEL = "<!-- gtd-health-squash-ready -->\n";
388266
+ var GtdStateError = class extends Error {
388267
+ kind;
388268
+ constructor(kind, message) {
388269
+ super(message);
388270
+ this.name = "GtdStateError";
388271
+ this.kind = kind;
388353
388272
  }
388354
- flushBytes();
388355
- return chars.join("");
388356
388273
  };
388357
- var parsePorcelainPaths = (porcelain) => porcelain.split("\n").map((line3) => line3.replace(/\r$/, "")).filter((line3) => line3.length > 0).map((line3) => ({ status: line3.slice(0, 2), path: unquoteGitPath(line3.slice(3)) }));
388358
- var isNumberedDir = (name) => /^\d+-/.test(name);
388359
- var isTaskFile = (name) => name.endsWith(".md");
388360
- var stripCode = (content) => content.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n\1[^\n]*|$(?![\s\S]))/gm, "").replace(/`[^`\n]+`/g, "");
388361
- var getPackages = (fs9, root2) => Effect_exports.gen(function* () {
388362
- const resolve5 = (p4) => join11(root2, p4);
388363
- const gtdExists = yield* fs9.exists(resolve5(GTD_DIR));
388364
- if (!gtdExists) return [];
388365
- const entries2 = yield* fs9.readDirectory(resolve5(GTD_DIR));
388366
- const packageDirs = entries2.filter(isNumberedDir).sort();
388367
- const packages = [];
388368
- for (const dir of packageDirs) {
388369
- const packagePath = `${GTD_DIR}/${dir}`;
388370
- const stat4 = yield* fs9.stat(resolve5(packagePath));
388371
- if (stat4.type !== "Directory") continue;
388372
- const files = yield* fs9.readDirectory(resolve5(packagePath));
388373
- const tasks = files.filter(isTaskFile).sort();
388374
- const taskContents = [];
388375
- for (const taskFile of tasks) {
388376
- const content = yield* fs9.readFileString(resolve5(`${packagePath}/${taskFile}`));
388377
- taskContents.push({ name: taskFile, content });
388378
- }
388379
- packages.push({ name: dir, tasks, taskContents });
388274
+ var foldCounters = (events) => {
388275
+ let testFixCount = 0;
388276
+ let reviewFixCount = 0;
388277
+ let healthFixCount = 0;
388278
+ for (const event of events) {
388279
+ if (event.type !== "COMMIT") continue;
388280
+ if (event.isPackageStart || event.isFeedback || event.removedErrors) testFixCount = 0;
388281
+ if (event.isErrors) testFixCount += 1;
388282
+ if (event.isPackageStart) reviewFixCount = 0;
388283
+ if (event.isFeedback) reviewFixCount += 1;
388284
+ if (event.isPackageStart || event.removedErrors) healthFixCount = 0;
388285
+ if (event.isHealthCheck) healthFixCount += 1;
388380
388286
  }
388381
- return packages;
388382
- }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))));
388383
- var CAPTURE_RULES = [
388384
- "Interpret the captured diff with these rules:",
388385
- "",
388386
- "- **Code changes** are suggestions, not finished work \u2014 plan to re-implement",
388387
- " them properly, including test coverage, rather than restoring them verbatim.",
388388
- "- **Code comments** are positional feedback about the code at that location.",
388389
- "- **TODO.md / REVIEW.md text changes** are global feedback on the plan or the",
388390
- " reviewed work as a whole.",
388391
- "- **Checkbox flips** in a captured REVIEW.md diff are approval noise \u2014 ignore",
388392
- " them."
388393
- ].join("\n");
388394
- var seedTodo = (capturedDiff) => {
388395
- const body = capturedDiff.replace(/\n+$/, "");
388396
- const fence = fenceFor(body);
388397
- return [
388398
- "# Plan",
388399
- "",
388400
- "## Captured input",
388401
- "",
388402
- "These changes were captured as the starting point for this feature. Develop",
388403
- "them into a concrete plan and surface any open questions for the user.",
388404
- "",
388405
- CAPTURE_RULES,
388406
- "",
388407
- `${fence}diff`,
388408
- body,
388409
- fence,
388410
- ""
388411
- ].join("\n");
388412
- };
388413
- var appendCapturedInput = (todo, capturedDiff) => {
388414
- const body = capturedDiff.replace(/\n+$/, "");
388415
- if (todo.includes(body)) return todo;
388416
- const fence = fenceFor(body);
388417
- const section = [
388418
- "## Captured input (grilling)",
388419
- "",
388420
- "These code changes were made during grilling and captured as suggestions;",
388421
- "gtd has reverted them from the working tree.",
388422
- "",
388423
- ...todo.includes(CAPTURE_RULES) ? [] : [CAPTURE_RULES, ""],
388424
- `${fence}diff`,
388425
- body,
388426
- fence,
388427
- ""
388428
- ].join("\n");
388429
- return todo.replace(/\n*$/, "\n\n") + section;
388287
+ return { testFixCount, reviewFixCount, healthFixCount };
388430
388288
  };
388431
- var isCheckboxOnlyDiff = (diff8) => {
388432
- if (diff8.trim() === "") return false;
388433
- const checkboxRe = /^(\s*- \[)([xX ])\](.*)$/;
388434
- const removedLines = [];
388435
- const addedLines = [];
388436
- for (const raw of diff8.split("\n")) {
388437
- const line3 = raw.replace(/\r$/, "");
388438
- if (line3.startsWith("---") || line3.startsWith("+++") || line3.startsWith("@@") || line3.startsWith("diff ") || line3.startsWith("index ") || line3.startsWith("new file") || line3.startsWith("deleted file") || line3.startsWith("similarity") || line3.startsWith("rename"))
388439
- continue;
388440
- if (line3.startsWith("-")) {
388441
- removedLines.push(line3.slice(1));
388442
- } else if (line3.startsWith("+")) {
388443
- addedLines.push(line3.slice(1));
388444
- }
388445
- }
388446
- if (removedLines.length !== addedLines.length) return false;
388447
- let flips = 0;
388448
- for (let i = 0; i < removedLines.length; i++) {
388449
- const rm4 = removedLines[i];
388450
- const add7 = addedLines[i];
388451
- if (rm4 === add7) continue;
388452
- if (!checkboxRe.test(rm4) || !checkboxRe.test(add7)) return false;
388453
- const rmNorm = rm4.replace(/\[[ xX]\]/, "[ ]");
388454
- const addNorm = add7.replace(/\[[ xX]\]/, "[ ]");
388455
- if (rmNorm !== addNorm) return false;
388456
- flips += 1;
388457
- }
388458
- return flips > 0;
388289
+ var isBoundary = (subject) => !subject.startsWith("gtd: ") || subject === "gtd: done";
388290
+ var DEFAULT_PAYLOAD = {
388291
+ todoExists: false,
388292
+ todoCommitted: false,
388293
+ gtdDirExists: false,
388294
+ reviewPresent: false,
388295
+ feedbackPresent: false,
388296
+ errorsPresent: false,
388297
+ gtdModified: false,
388298
+ codeDirty: false,
388299
+ todoMarkerPresent: false,
388300
+ feedbackCommitted: false,
388301
+ feedbackEmpty: false,
388302
+ feedbackContent: "",
388303
+ reviewCommitted: false,
388304
+ reviewDirty: false,
388305
+ reviewCheckboxOnly: false,
388306
+ pendingErrorsDeletion: false,
388307
+ lastCommitSubject: "",
388308
+ workingTreeClean: true,
388309
+ packages: [],
388310
+ diff: "",
388311
+ hasCommitsAfterLastDone: true,
388312
+ agenticReviewEnabled: true,
388313
+ fixAttemptCap: 3,
388314
+ reviewThreshold: 3,
388315
+ squashEnabled: false,
388316
+ squashMsgPresent: false,
388317
+ squashMsgContent: "",
388318
+ healthPresent: false,
388319
+ healthContent: "",
388320
+ healthCommitted: false
388459
388321
  };
388460
- var gatherEvents = () => (
388461
- // fallow-ignore-next-line complexity
388462
- Effect_exports.gen(function* () {
388463
- const git = yield* GitService;
388464
- const fs9 = yield* FileSystem_exports.FileSystem;
388465
- const config2 = yield* ConfigService;
388466
- const { root: root2 } = yield* Cwd;
388467
- const resolve5 = (p4) => join11(root2, p4);
388468
- const defaultBranch = yield* git.resolveDefaultBranch();
388469
- const headHash = yield* git.resolveRef("HEAD").pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388470
- const mergeBase = Option_exports.isSome(defaultBranch) ? yield* git.mergeBase(defaultBranch.value, "HEAD") : Option_exports.none();
388471
- const base = Option_exports.isSome(mergeBase) && mergeBase.value !== headHash ? mergeBase : Option_exports.none();
388472
- const history = yield* git.commitHistory(Option_exports.getOrUndefined(base));
388473
- const commitEvents = history.map(
388474
- ({ message, removedErrors }) => {
388475
- const subject = (message.split("\n")[0] ?? "").trim();
388476
- return {
388477
- type: "COMMIT",
388478
- isErrors: subject === ERRORS_SUBJECT,
388479
- isFeedback: subject === FEEDBACK_SUBJECT,
388480
- isPackageStart: subject === PLANNING_SUBJECT || subject === PACKAGE_DONE_SUBJECT,
388481
- isWorkflowCommit: subject.startsWith("gtd: "),
388482
- removedErrors,
388483
- isHealthCheck: subject === HEALTH_CHECK_SUBJECT,
388484
- isHealthFix: subject === HEALTH_FIX_SUBJECT
388485
- };
388322
+ var buildContext = (p4, counters, grillingCase) => ({
388323
+ testFixCount: counters.testFixCount,
388324
+ reviewFixCount: counters.reviewFixCount,
388325
+ packages: p4.packages,
388326
+ diff: p4.diff,
388327
+ ...p4.refDiff !== void 0 ? { refDiff: p4.refDiff } : {},
388328
+ ...p4.reviewBase !== void 0 ? { reviewBase: p4.reviewBase } : {},
388329
+ ...p4.squashBase !== void 0 ? { squashBase: p4.squashBase } : {},
388330
+ ...p4.squashDiff !== void 0 ? { squashDiff: p4.squashDiff } : {},
388331
+ lastCommitSubject: p4.lastCommitSubject,
388332
+ workingTreeClean: p4.workingTreeClean,
388333
+ feedbackContent: p4.feedbackContent !== "" ? p4.feedbackContent : p4.healthContent,
388334
+ ...grillingCase !== void 0 ? { grillingCase } : {}
388335
+ });
388336
+ var assertLegal = (p4) => {
388337
+ const fail20 = (msg) => {
388338
+ throw new GtdStateError("illegal-combination", msg);
388339
+ };
388340
+ if (p4.reviewPresent && p4.gtdDirExists) fail20("illegal combination: REVIEW.md + .gtd");
388341
+ if (p4.reviewPresent && p4.todoCommitted) fail20("illegal combination: REVIEW.md + committed TODO.md");
388342
+ if (p4.reviewPresent && !(p4.reviewCommitted || p4.reviewDirty) && p4.todoExists)
388343
+ fail20("illegal combination: uncommitted REVIEW.md + TODO.md");
388344
+ if (p4.feedbackPresent && p4.reviewPresent) fail20("illegal combination: FEEDBACK.md + REVIEW.md");
388345
+ if (p4.feedbackPresent && !p4.gtdDirExists) fail20("illegal combination: FEEDBACK.md without .gtd");
388346
+ if (p4.errorsPresent && p4.feedbackPresent) fail20("illegal combination: ERRORS.md + FEEDBACK.md");
388347
+ const isHealthCapEscalation = p4.lastCommitSubject === "gtd: health-check" || p4.lastCommitSubject === "gtd: health-fix";
388348
+ if (p4.errorsPresent && !p4.gtdDirExists && !isHealthCapEscalation)
388349
+ fail20("illegal combination: ERRORS.md without .gtd");
388350
+ if (p4.healthPresent && p4.gtdDirExists) fail20("illegal combination: HEALTH.md + .gtd");
388351
+ if (p4.healthPresent && p4.reviewPresent) fail20("illegal combination: HEALTH.md + REVIEW.md");
388352
+ if (p4.healthPresent && p4.feedbackPresent) fail20("illegal combination: HEALTH.md + FEEDBACK.md");
388353
+ if (p4.healthPresent && p4.errorsPresent) fail20("illegal combination: HEALTH.md + ERRORS.md");
388354
+ };
388355
+ var resolveHealth = (p4, counters) => {
388356
+ const hasPendingWork = !p4.healthCommitted || !p4.workingTreeClean;
388357
+ const prefix = p4.healthCommitted ? "gtd: health-fix" : "gtd: health-check";
388358
+ return {
388359
+ state: "health-fixing",
388360
+ autoAdvance: true,
388361
+ ...hasPendingWork ? {
388362
+ edgeAction: {
388363
+ kind: "commitPending",
388364
+ prefix,
388365
+ // Always remove HEALTH.md: on gtd: health-check this clears it so
388366
+ // the next run re-enters resolveCleanOrIdle; on gtd: health-fix it
388367
+ // clears the marker left by a prior health-check run.
388368
+ removeHealth: true
388486
388369
  }
388487
- );
388488
- const hasCommits = yield* git.hasCommits();
388489
- const porcelain = yield* git.statusPorcelain();
388490
- const entries2 = parsePorcelainPaths(porcelain);
388491
- const workingTreeClean = entries2.length === 0;
388492
- const lastCommitSubject = hasCommits ? yield* git.lastCommitSubject() : "";
388493
- const gtdModified = entries2.some((e7) => isGtdPath(e7.path));
388494
- const codeDirty = entries2.some((e7) => !isSteeringFile(e7.path) && !isGtdPath(e7.path));
388495
- const todoExists = yield* fs9.exists(resolve5(TODO_FILE));
388496
- const gtdDirExists = yield* fs9.exists(resolve5(GTD_DIR));
388497
- const reviewPresent = yield* fs9.exists(resolve5(REVIEW_FILE));
388498
- const feedbackPresent = yield* fs9.exists(resolve5(FEEDBACK_FILE));
388499
- const errorsPresent = yield* fs9.exists(resolve5(ERRORS_FILE));
388500
- const todoContent = todoExists ? yield* fs9.readFileString(resolve5(TODO_FILE)) : "";
388501
- const todoMarkerPresent = todoExists && stripCode(todoContent).includes(UNANSWERED_MARKER);
388502
- const isUncommitted = (path15) => {
388503
- const entry = entries2.find((e7) => e7.path === path15);
388504
- return entry !== void 0 && isUncommittedStatus(entry.status);
388370
+ } : {},
388371
+ context: buildContext(p4, counters)
388372
+ };
388373
+ };
388374
+ var resolveFeedback = (p4, counters) => {
388375
+ if (p4.feedbackEmpty) {
388376
+ return {
388377
+ state: "close-package",
388378
+ autoAdvance: true,
388379
+ edgeAction: { kind: "closePackage" },
388380
+ context: buildContext(p4, counters)
388505
388381
  };
388506
- const feedbackCommitted = feedbackPresent && !isUncommitted(FEEDBACK_FILE);
388507
- const feedbackContent = feedbackPresent ? yield* fs9.readFileString(resolve5(FEEDBACK_FILE)) : "";
388508
- const feedbackEmpty = feedbackPresent && !/\S/.test(feedbackContent);
388509
- const reviewTrackedAtHead = reviewPresent && !isUncommitted(REVIEW_FILE);
388510
- const reviewCommitted = reviewTrackedAtHead && workingTreeClean;
388511
- const reviewDirty = reviewTrackedAtHead && !workingTreeClean;
388512
- const todoCommitted = todoExists && !isUncommitted(TODO_FILE);
388513
- const pendingErrorsDeletion = entries2.some(
388514
- (e7) => e7.path === ERRORS_FILE && e7.status.includes("D")
388515
- );
388516
- const packages = yield* getPackages(fs9, root2);
388517
- const diff8 = hasCommits && entries2.length > 0 ? yield* git.diffHead() : "";
388518
- const onlyReviewDirty = entries2.length > 0 && entries2.every((e7) => e7.path === REVIEW_FILE);
388519
- const reviewCheckboxOnly = onlyReviewDirty && isCheckboxOnlyDiff(
388520
- yield* git.diffPath(REVIEW_FILE).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")))
388521
- );
388522
- let reviewBase;
388523
- let refDiff;
388524
- let hasCommitsAfterLastDone = true;
388525
- if (hasCommits) {
388526
- const allHistory = Option_exports.isNone(base) ? history : yield* git.commitHistory();
388527
- const lastDoneIdx = (() => {
388528
- let idx = -1;
388529
- for (let i = 0; i < allHistory.length; i++) {
388530
- const subject = (allHistory[i].message.split("\n")[0] ?? "").trim();
388531
- if (subject === DONE_SUBJECT) idx = i;
388532
- }
388533
- return idx;
388534
- })();
388535
- const currentCycle = lastDoneIdx === -1 ? allHistory : allHistory.slice(lastDoneIdx + 1);
388536
- hasCommitsAfterLastDone = lastDoneIdx === -1 || currentCycle.length > 0;
388537
- const firstGrilling = currentCycle.find(
388538
- (c7) => (c7.message.split("\n")[0] ?? "").trim() === GRILLING_SUBJECT
388539
- );
388540
- const lastAwaitingReview = (() => {
388541
- let found;
388542
- for (const c7 of currentCycle) {
388543
- if ((c7.message.split("\n")[0] ?? "").trim().startsWith(AWAITING_REVIEW_SUBJECT)) {
388544
- found = c7;
388545
- }
388546
- }
388547
- return found;
388548
- })();
388549
- const withinProcess = firstGrilling !== void 0;
388550
- let candidate;
388551
- if (withinProcess) {
388552
- if (lastAwaitingReview !== void 0) {
388553
- candidate = lastAwaitingReview.hash ?? EMPTY_TREE;
388554
- } else {
388555
- candidate = firstGrilling.hash ?? EMPTY_TREE;
388556
- }
388557
- } else {
388558
- }
388559
- if (candidate !== void 0 && hasCommitsAfterLastDone) {
388560
- const candidateDiff = yield* git.diffRef(candidate, WORKFLOW_FILE_EXCLUDES).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388561
- if (candidateDiff.trim().length > 0) {
388562
- reviewBase = candidate;
388563
- refDiff = candidateDiff;
388564
- }
388382
+ }
388383
+ return {
388384
+ state: "fixing",
388385
+ autoAdvance: true,
388386
+ edgeAction: {
388387
+ kind: "commitPending",
388388
+ prefix: p4.feedbackCommitted ? "gtd: fixing" : "gtd: feedback",
388389
+ // Delete FEEDBACK.md so its removal lands in this commit; without it the
388390
+ // next run re-detects FEEDBACK (precedence 2) and Fixing never returns to
388391
+ // Testing (STATES.md § Fixing: "FEEDBACK.md is removed either way").
388392
+ removeFeedback: true
388393
+ },
388394
+ context: buildContext(p4, counters)
388395
+ };
388396
+ };
388397
+ var resolveGtdLifecycle = (p4, counters, head7, corrupt) => {
388398
+ if (p4.gtdModified) {
388399
+ return {
388400
+ state: "planning",
388401
+ autoAdvance: true,
388402
+ edgeAction: { kind: "commitPending", prefix: "gtd: planning" },
388403
+ context: buildContext(p4, counters)
388404
+ };
388405
+ }
388406
+ const noOpFixer = p4.workingTreeClean && head7 === "gtd: fixing";
388407
+ if (p4.codeDirty || p4.pendingErrorsDeletion || noOpFixer) {
388408
+ const resume2 = p4.pendingErrorsDeletion;
388409
+ return {
388410
+ state: "testing",
388411
+ autoAdvance: true,
388412
+ edgeAction: {
388413
+ kind: "runTest",
388414
+ errorCount: resume2 ? 0 : counters.testFixCount,
388415
+ capReached: (resume2 ? 0 : counters.testFixCount) >= p4.fixAttemptCap
388416
+ },
388417
+ context: buildContext(p4, counters)
388418
+ };
388419
+ }
388420
+ if (p4.workingTreeClean) {
388421
+ if (head7 === "gtd: planning" || head7 === "gtd: package done") {
388422
+ if (head7 === "gtd: planning" && p4.todoExists) {
388423
+ return {
388424
+ state: "building",
388425
+ autoAdvance: true,
388426
+ edgeAction: { kind: "commitPending", prefix: "gtd: planning", removeTodo: true },
388427
+ context: buildContext(p4, counters)
388428
+ };
388565
388429
  }
388430
+ return { state: "building", autoAdvance: true, context: buildContext(p4, counters) };
388566
388431
  }
388567
- let squashBase;
388568
- let squashDiff;
388569
- if (hasCommits && lastCommitSubject === DONE_SUBJECT && config2.squash) {
388570
- const squashHistory = history;
388571
- const subjectOf = (c7) => (c7.message.split("\n")[0] ?? "").trim();
388572
- let lastDoneIdxForSquash = -1;
388573
- let prevDoneIdx = -1;
388574
- for (let i = 0; i < squashHistory.length; i++) {
388575
- if (subjectOf(squashHistory[i]) === DONE_SUBJECT) {
388576
- prevDoneIdx = lastDoneIdxForSquash;
388577
- lastDoneIdxForSquash = i;
388578
- }
388579
- }
388580
- if (lastDoneIdxForSquash !== -1) {
388581
- const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
388582
- let startIdx = -1;
388583
- for (let i = squashCycle.length - 1; i >= 0; i--) {
388584
- if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT || subjectOf(squashCycle[i]) === REVIEWING_SUBJECT) {
388585
- startIdx = i;
388586
- break;
388587
- }
388588
- }
388589
- if (startIdx === -1) {
388590
- for (let i = squashCycle.length - 1; i >= 0; i--) {
388591
- if (subjectOf(squashCycle[i]) === GRILLING_SUBJECT) {
388592
- startIdx = i;
388593
- while (startIdx > 0 && subjectOf(squashCycle[startIdx - 1]) === GRILLING_SUBJECT) {
388594
- startIdx--;
388595
- }
388596
- break;
388597
- }
388598
- }
388599
- }
388600
- const squashStart = startIdx === -1 ? void 0 : squashCycle[startIdx];
388601
- if (squashStart !== void 0) {
388602
- const squashStartParent = yield* git.resolveRef(`${squashStart.hash}~1`).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
388603
- const candidateDiff = yield* git.diffRef(squashStartParent).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388604
- if (candidateDiff.trim().length > 0) {
388605
- squashBase = squashStartParent;
388606
- squashDiff = candidateDiff;
388607
- }
388608
- }
388432
+ if (head7 === "gtd: building") {
388433
+ const forceApprove = !p4.agenticReviewEnabled || counters.reviewFixCount >= p4.reviewThreshold;
388434
+ if (forceApprove) {
388435
+ return {
388436
+ state: "close-package",
388437
+ autoAdvance: true,
388438
+ edgeAction: { kind: "closePackage" },
388439
+ context: buildContext(p4, counters)
388440
+ };
388609
388441
  }
388442
+ return { state: "agentic-review", autoAdvance: true, context: buildContext(p4, counters) };
388610
388443
  }
388611
- const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388612
- const squashMsgContent = squashMsgPresent ? yield* fs9.readFileString(resolve5(SQUASH_MSG_FILE)) : "";
388613
- const healthPresent = yield* fs9.exists(resolve5(HEALTH_FILE));
388614
- const healthContent = healthPresent ? yield* fs9.readFileString(resolve5(HEALTH_FILE)) : "";
388615
- const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
388616
- let healthFixBase;
388617
- if (config2.squash) {
388618
- const subjectOf = (c7) => (c7.message.split("\n")[0] ?? "").trim();
388619
- let firstHealthCheckHash;
388620
- let healthCheckCount = 0;
388621
- for (const commit3 of history) {
388622
- const s5 = subjectOf(commit3);
388623
- const isPackageStart = s5 === PLANNING_SUBJECT || s5 === PACKAGE_DONE_SUBJECT;
388624
- if (isPackageStart || commit3.removedErrors) {
388625
- firstHealthCheckHash = void 0;
388626
- healthCheckCount = 0;
388627
- }
388628
- if (s5 === HEALTH_CHECK_SUBJECT) {
388629
- healthCheckCount++;
388630
- if (healthCheckCount === 1) firstHealthCheckHash = commit3.hash;
388631
- }
388632
- }
388633
- if (healthCheckCount > 0 && firstHealthCheckHash !== void 0) {
388634
- const base2 = yield* git.resolveRef(`${firstHealthCheckHash}~1`).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
388635
- healthFixBase = base2;
388636
- }
388637
- }
388638
- const payload = {
388639
- todoExists,
388640
- todoCommitted,
388641
- gtdDirExists,
388642
- reviewPresent,
388643
- feedbackPresent,
388644
- errorsPresent,
388645
- gtdModified,
388646
- codeDirty,
388647
- todoMarkerPresent,
388648
- feedbackCommitted,
388649
- feedbackEmpty,
388650
- feedbackContent,
388651
- reviewCommitted,
388652
- reviewDirty,
388653
- reviewCheckboxOnly,
388654
- pendingErrorsDeletion,
388655
- lastCommitSubject,
388656
- workingTreeClean,
388657
- packages,
388658
- diff: diff8,
388659
- ...reviewBase !== void 0 ? { reviewBase } : {},
388660
- ...refDiff !== void 0 ? { refDiff } : {},
388661
- hasCommitsAfterLastDone,
388662
- agenticReviewEnabled: config2.agenticReview,
388663
- fixAttemptCap: config2.fixAttemptCap,
388664
- reviewThreshold: config2.reviewThreshold,
388665
- squashEnabled: config2.squash,
388666
- ...squashBase !== void 0 ? { squashBase } : {},
388667
- ...squashDiff !== void 0 ? { squashDiff } : {},
388668
- squashMsgPresent,
388669
- squashMsgContent,
388670
- healthPresent,
388671
- healthContent,
388672
- healthCommitted,
388673
- ...healthFixBase !== void 0 ? { healthFixBase } : {}
388444
+ }
388445
+ return corrupt();
388446
+ };
388447
+ var resolveReviewLifecycle = (p4, counters, head7) => {
388448
+ if (head7 === "gtd: review feedback") {
388449
+ return {
388450
+ state: "accept-review",
388451
+ autoAdvance: true,
388452
+ edgeAction: { kind: "seedAcceptReview" },
388453
+ context: buildContext(p4, counters)
388674
388454
  };
388675
- const resolveEvent = { type: "RESOLVE", payload };
388676
- return [...commitEvents, resolveEvent];
388677
- }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
388678
- );
388679
- var captureAndRevert = (git, subject, onRegen) => Effect_exports.gen(function* () {
388680
- const head7 = yield* git.lastCommitSubject().pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388681
- if (head7 !== subject) {
388682
- yield* git.commitAllWithPrefix(subject);
388683
- } else {
388684
- yield* onRegen;
388685
388455
  }
388686
- const base = yield* git.resolveRef("HEAD~1").pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
388687
- const captured = yield* git.diffRef(base).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388688
- yield* git.revertNoCommit("HEAD");
388689
- return captured;
388690
- });
388691
- var reviewAgainst = (target) => Effect_exports.gen(function* () {
388692
- const git = yield* GitService;
388693
- const targetHash = yield* git.resolveRef(target);
388694
- const mergeBase = yield* git.mergeBase(target, "HEAD");
388695
- const mergeBaseHash = Option_exports.isNone(mergeBase) || mergeBase.value === targetHash ? targetHash : mergeBase.value;
388696
- const refDiff = yield* git.diffRef(mergeBaseHash, WORKFLOW_FILE_EXCLUDES);
388697
- if (refDiff.trim().length === 0) return void 0;
388698
- return { reviewBase: mergeBaseHash, refDiff };
388699
- });
388700
- var perform = (action) => (
388701
- // fallow-ignore-next-line complexity
388702
- Effect_exports.gen(function* () {
388703
- const git = yield* GitService;
388704
- const fs9 = yield* FileSystem_exports.FileSystem;
388705
- const { root: root2 } = yield* Cwd;
388706
- const resolve5 = (p4) => join11(root2, p4);
388707
- switch (action.kind) {
388708
- // Transport: mixed-reset the hand-made `gtd: transport` HEAD, keeping the
388709
- // work in the tree, then re-derive. (No producer command — consume only.)
388710
- case "transportReset": {
388711
- yield* git.mixedResetHead();
388712
- return { stop: false };
388713
- }
388714
- // New Feature: capture the raw input as `gtd: new task` (unless HEAD is
388715
- // already there — the lost-seed regenerate case), revert it back to a
388716
- // clean baseline (inverse staged), and seed TODO.md from that diff.
388717
- case "seedNewFeature": {
388718
- const captured = yield* captureAndRevert(git, NEW_TASK_SUBJECT, Effect_exports.void);
388719
- yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388720
- return { stop: false };
388721
- }
388722
- // Accept Review: capture the human's pending changeset durably as
388723
- // `gtd: review feedback` (annotations, code edits, and new files alike),
388724
- // revert it back to the reviewed baseline, rm REVIEW.md (which is what
388725
- // stops Accept Review re-firing), and seed TODO.md from the captured
388726
- // diff. The regen case (the machine's rule-4 carve-out fired) discards
388727
- // any partial revert/seed state with a hard reset first.
388728
- case "seedAcceptReview": {
388729
- const captured = yield* captureAndRevert(git, REVIEW_FEEDBACK_SUBJECT, git.resetHard());
388730
- yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388731
- yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388732
- return { stop: false };
388733
- }
388734
- // Grilling capture: the plan is already committed and the user sketched
388735
- // code during the round. Fold the code diff (untracked included, steering
388736
- // files excluded) into TODO.md as a suggestion block, drop the code
388737
- // changes — reset tracked, delete untracked/added — and commit the lot as
388738
- // one `gtd: grilling`. TODO.md's own pending edits are preserved verbatim
388739
- // (snapshotted before the reset). Binary edits survive only as the diff's
388740
- // "Binary files differ" line — an accepted limitation.
388741
- case "captureGrillingEdits": {
388742
- const porcelain = yield* git.statusPorcelain();
388743
- const entries2 = parsePorcelainPaths(porcelain);
388744
- const pendingCodeFiles = entries2.filter(
388745
- (e7) => isUncommittedStatus(e7.status) && !isSteeringFile(e7.path) && !isGtdPath(e7.path)
388746
- );
388747
- const todoNow = yield* fs9.readFileString(resolve5(TODO_FILE));
388748
- const captured = yield* git.diffHead(WORKFLOW_FILE_EXCLUDES);
388749
- yield* git.resetHard();
388750
- for (const entry of pendingCodeFiles) {
388751
- yield* fs9.remove(resolve5(entry.path), { recursive: true }).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388752
- }
388753
- yield* fs9.writeFileString(resolve5(TODO_FILE), appendCapturedInput(todoNow, captured));
388754
- yield* git.commitAllWithPrefix(GRILLING_SUBJECT);
388755
- return { stop: false };
388756
- }
388757
- // Testing: commit the pending tree `gtd: building` (nothing pending in the
388758
- // no-op-fixer case), run tests; on red write FEEDBACK (below cap) or ERRORS
388759
- // (at cap) and commit `gtd: errors`; on green proceed.
388760
- case "runTest": {
388761
- const runner = yield* TestRunner;
388762
- const status2 = yield* git.statusPorcelain();
388763
- const committedBuilding = status2.trim().length > 0;
388764
- if (committedBuilding) {
388765
- yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
388766
- }
388767
- const result = yield* runner.run();
388768
- if (result.exitCode === 0) {
388769
- if (!committedBuilding) {
388770
- yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
388771
- }
388772
- return { stop: false };
388773
- }
388774
- const target = action.capReached ? ERRORS_FILE : FEEDBACK_FILE;
388775
- const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388776
- yield* fs9.writeFileString(resolve5(target), body);
388777
- yield* git.commitAllWithPrefix(ERRORS_SUBJECT);
388778
- return { stop: false };
388779
- }
388780
- // Health check: run tests on an idle/clean tree.
388781
- // Green, no prior fixes → stop immediately (no commit).
388782
- // Green, fixes exist → stop immediately (squash handled via healthFixBase
388783
- // when gatherEvents sets it; the machine resolves squashing state).
388784
- // Red below cap → write HEALTH.md (uncommitted); resolveHealth will
388785
- // commit it as `gtd: health-check` via `commitPending removeHealth`.
388786
- // Red at cap → write ERRORS.md and commit `gtd: health-check` immediately
388787
- // so the Escalate state fires on the next resolve.
388788
- case "runHealthCheck": {
388789
- if (action.commitErrorsReset === true) {
388790
- yield* git.commitAllWithPrefix(HEALTH_FIX_SUBJECT);
388791
- }
388792
- const runner = yield* TestRunner;
388793
- const result = yield* runner.run();
388794
- if (result.exitCode === 0) {
388795
- if (action.healthFixBase !== void 0 && action.commitErrorsReset !== true) {
388796
- yield* fs9.writeFileString(
388797
- resolve5(SQUASH_MSG_FILE),
388798
- "chore: health-check cycle squash\n"
388799
- );
388800
- return { stop: false };
388801
- }
388802
- return { stop: true };
388803
- }
388804
- const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388805
- if (action.capReached) {
388806
- yield* fs9.writeFileString(resolve5(ERRORS_FILE), body);
388807
- yield* git.commitAllWithPrefix(HEALTH_CHECK_SUBJECT);
388808
- } else {
388809
- yield* fs9.writeFileString(resolve5(HEALTH_FILE), body);
388810
- }
388811
- return { stop: false };
388812
- }
388813
- // Grilling / Grilled / Planning / Fixing: commit the pending tree under a
388814
- // fixed phase prefix. Fixing sets `removeFeedback` so FEEDBACK.md's removal
388815
- // lands in the `gtd: fixing` / `gtd: feedback` commit — otherwise the next
388816
- // run re-detects FEEDBACK (precedence 2) and Fixing re-fires forever instead
388817
- // of returning to Testing (STATES.md § Fixing).
388818
- case "commitPending": {
388819
- if (action.removeFeedback === true) {
388820
- yield* fs9.remove(resolve5(FEEDBACK_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388821
- }
388822
- if (action.removeTodo === true) {
388823
- yield* fs9.remove(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388824
- }
388825
- if (action.removeHealth === true) {
388826
- yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388827
- }
388828
- if (action.removeTodo !== true) {
388829
- yield* formatFile(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388830
- }
388831
- yield* git.commitAllWithPrefix(action.prefix);
388832
- return { stop: false };
388833
- }
388834
- // Close package: remove the (maybe-empty / maybe-absent) FEEDBACK.md, rm
388835
- // the first (finished) package dir (+ the now-empty `.gtd/`), commit
388836
- // `gtd: package done`. Tolerates an absent FEEDBACK.md (force-approve).
388837
- case "closePackage": {
388838
- yield* fs9.remove(resolve5(FEEDBACK_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388839
- const packages = yield* getPackages(fs9, root2);
388840
- const first2 = packages[0];
388841
- if (first2 !== void 0) {
388842
- yield* git.removePackageDir(`${GTD_DIR}/${first2.name}`);
388843
- }
388844
- yield* git.commitAllWithPrefix(PACKAGE_DONE_SUBJECT);
388845
- return { stop: false };
388846
- }
388847
- // Await Review: commit REVIEW.md as `gtd: awaiting review`.
388848
- case "commitReview": {
388849
- yield* formatFile(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388850
- yield* git.commitAllWithPrefix(AWAITING_REVIEW_SUBJECT);
388851
- return { stop: false };
388852
- }
388853
- // Done: rm REVIEW.md, commit `gtd: done`.
388854
- case "done": {
388855
- yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388856
- yield* git.commitAllWithPrefix(DONE_SUBJECT);
388857
- return { stop: false };
388858
- }
388859
- // Squash: rm SQUASH_MSG.md, soft-reset to squashBase, commit everything
388860
- // under the provided commit message.
388861
- case "squashCommit": {
388862
- yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388863
- yield* git.softResetTo(action.squashBase);
388864
- yield* git.commitAllWithPrefix(action.commitMessage);
388865
- return { stop: false };
388866
- }
388867
- }
388868
- }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
388869
- );
388870
-
388871
- // src/Machine.ts
388872
- var GtdStateError = class extends Error {
388873
- kind;
388874
- constructor(kind, message) {
388875
- super(message);
388876
- this.name = "GtdStateError";
388877
- this.kind = kind;
388456
+ if (p4.reviewCommitted || p4.reviewDirty && p4.reviewCheckboxOnly) {
388457
+ return {
388458
+ state: "done",
388459
+ autoAdvance: true,
388460
+ edgeAction: { kind: "done" },
388461
+ context: buildContext(p4, counters)
388462
+ };
388878
388463
  }
388879
- };
388880
- var foldCounters = (events) => {
388881
- let testFixCount = 0;
388882
- let reviewFixCount = 0;
388883
- let healthFixCount = 0;
388884
- for (const event of events) {
388885
- if (event.type !== "COMMIT") continue;
388886
- if (event.isPackageStart || event.isFeedback || event.removedErrors) testFixCount = 0;
388887
- if (event.isErrors) testFixCount += 1;
388888
- if (event.isPackageStart) reviewFixCount = 0;
388889
- if (event.isFeedback) reviewFixCount += 1;
388890
- if (event.isPackageStart || event.removedErrors) healthFixCount = 0;
388891
- if (event.isHealthCheck) healthFixCount += 1;
388464
+ if (p4.reviewDirty) {
388465
+ return {
388466
+ state: "accept-review",
388467
+ autoAdvance: true,
388468
+ edgeAction: { kind: "seedAcceptReview" },
388469
+ context: buildContext(p4, counters)
388470
+ };
388892
388471
  }
388893
- return { testFixCount, reviewFixCount, healthFixCount };
388894
- };
388895
- var isBoundary = (subject) => !subject.startsWith("gtd: ") || subject === "gtd: done";
388896
- var DEFAULT_PAYLOAD = {
388897
- todoExists: false,
388898
- todoCommitted: false,
388899
- gtdDirExists: false,
388900
- reviewPresent: false,
388901
- feedbackPresent: false,
388902
- errorsPresent: false,
388903
- gtdModified: false,
388904
- codeDirty: false,
388905
- todoMarkerPresent: false,
388906
- feedbackCommitted: false,
388907
- feedbackEmpty: false,
388908
- feedbackContent: "",
388909
- reviewCommitted: false,
388910
- reviewDirty: false,
388911
- reviewCheckboxOnly: false,
388912
- pendingErrorsDeletion: false,
388913
- lastCommitSubject: "",
388914
- workingTreeClean: true,
388915
- packages: [],
388916
- diff: "",
388917
- hasCommitsAfterLastDone: true,
388918
- agenticReviewEnabled: true,
388919
- fixAttemptCap: 3,
388920
- reviewThreshold: 3,
388921
- squashEnabled: false,
388922
- squashMsgPresent: false,
388923
- squashMsgContent: "",
388924
- healthPresent: false,
388925
- healthContent: "",
388926
- healthCommitted: false
388927
- };
388928
- var buildContext = (p4, counters, grillingCase) => ({
388929
- testFixCount: counters.testFixCount,
388930
- reviewFixCount: counters.reviewFixCount,
388931
- packages: p4.packages,
388932
- diff: p4.diff,
388933
- ...p4.refDiff !== void 0 ? { refDiff: p4.refDiff } : {},
388934
- ...p4.reviewBase !== void 0 ? { reviewBase: p4.reviewBase } : {},
388935
- ...p4.squashBase !== void 0 ? { squashBase: p4.squashBase } : {},
388936
- ...p4.squashDiff !== void 0 ? { squashDiff: p4.squashDiff } : {},
388937
- lastCommitSubject: p4.lastCommitSubject,
388938
- workingTreeClean: p4.workingTreeClean,
388939
- feedbackContent: p4.feedbackContent !== "" ? p4.feedbackContent : p4.healthContent,
388940
- ...grillingCase !== void 0 ? { grillingCase } : {}
388941
- });
388942
- var assertLegal = (p4) => {
388943
- const fail20 = (msg) => {
388944
- throw new GtdStateError("illegal-combination", msg);
388472
+ return {
388473
+ state: "await-review",
388474
+ autoAdvance: true,
388475
+ edgeAction: { kind: "commitReview" },
388476
+ context: buildContext(p4, counters)
388945
388477
  };
388946
- if (p4.reviewPresent && p4.gtdDirExists) fail20("illegal combination: REVIEW.md + .gtd");
388947
- if (p4.reviewPresent && p4.todoCommitted) fail20("illegal combination: REVIEW.md + committed TODO.md");
388948
- if (p4.reviewPresent && !(p4.reviewCommitted || p4.reviewDirty) && p4.todoExists)
388949
- fail20("illegal combination: uncommitted REVIEW.md + TODO.md");
388950
- if (p4.feedbackPresent && p4.reviewPresent) fail20("illegal combination: FEEDBACK.md + REVIEW.md");
388951
- if (p4.feedbackPresent && !p4.gtdDirExists) fail20("illegal combination: FEEDBACK.md without .gtd");
388952
- if (p4.errorsPresent && p4.feedbackPresent) fail20("illegal combination: ERRORS.md + FEEDBACK.md");
388953
- const isHealthCapEscalation = p4.lastCommitSubject === "gtd: health-check" || p4.lastCommitSubject === "gtd: health-fix";
388954
- if (p4.errorsPresent && !p4.gtdDirExists && !isHealthCapEscalation)
388955
- fail20("illegal combination: ERRORS.md without .gtd");
388956
- if (p4.healthPresent && p4.gtdDirExists) fail20("illegal combination: HEALTH.md + .gtd");
388957
- if (p4.healthPresent && p4.reviewPresent) fail20("illegal combination: HEALTH.md + REVIEW.md");
388958
- if (p4.healthPresent && p4.feedbackPresent) fail20("illegal combination: HEALTH.md + FEEDBACK.md");
388959
- if (p4.healthPresent && p4.errorsPresent) fail20("illegal combination: HEALTH.md + ERRORS.md");
388960
388478
  };
388961
- var resolveHealth = (p4, counters) => {
388962
- const hasPendingWork = !p4.healthCommitted || !p4.workingTreeClean;
388963
- const prefix = p4.healthCommitted ? "gtd: health-fix" : "gtd: health-check";
388479
+ var resolveGrilling = (p4, counters, head7) => {
388480
+ const grillCommit = p4.todoCommitted && p4.codeDirty ? { kind: "captureGrillingEdits" } : { kind: "commitPending", prefix: "gtd: grilling" };
388481
+ if (p4.todoMarkerPresent) {
388482
+ const alreadyAtGrillingStop = p4.workingTreeClean && head7 === "gtd: grilling";
388483
+ return {
388484
+ state: "grilling",
388485
+ autoAdvance: false,
388486
+ ...alreadyAtGrillingStop ? {} : { edgeAction: grillCommit },
388487
+ context: buildContext(p4, counters, "stop")
388488
+ };
388489
+ }
388490
+ if (!p4.workingTreeClean) {
388491
+ return {
388492
+ state: "grilling",
388493
+ autoAdvance: true,
388494
+ edgeAction: grillCommit,
388495
+ context: buildContext(p4, counters, "iterate")
388496
+ };
388497
+ }
388964
388498
  return {
388965
- state: "health-fixing",
388499
+ state: "grilled",
388966
388500
  autoAdvance: true,
388967
- ...hasPendingWork ? {
388968
- edgeAction: {
388969
- kind: "commitPending",
388970
- prefix,
388971
- // Always remove HEALTH.md: on gtd: health-check this clears it so
388972
- // the next run re-enters resolveCleanOrIdle; on gtd: health-fix it
388973
- // clears the marker left by a prior health-check run.
388974
- removeHealth: true
388975
- }
388976
- } : {},
388501
+ edgeAction: { kind: "commitPending", prefix: "gtd: grilled" },
388977
388502
  context: buildContext(p4, counters)
388978
388503
  };
388979
388504
  };
388980
- var resolveFeedback = (p4, counters) => {
388981
- if (p4.feedbackEmpty) {
388505
+ var resolveCleanOrIdle = (p4, counters, head7) => {
388506
+ const isHealthHead = head7 === "gtd: health-check" || head7 === "gtd: health-fix";
388507
+ if (!p4.workingTreeClean && !p4.pendingErrorsDeletion && isHealthHead) {
388982
388508
  return {
388983
- state: "close-package",
388509
+ state: "health-check",
388984
388510
  autoAdvance: true,
388985
- edgeAction: { kind: "closePackage" },
388511
+ edgeAction: { kind: "commitPending", prefix: "gtd: health-fix" },
388986
388512
  context: buildContext(p4, counters)
388987
388513
  };
388988
388514
  }
388515
+ if (!p4.workingTreeClean && !p4.pendingErrorsDeletion) return null;
388516
+ if (!p4.workingTreeClean && p4.pendingErrorsDeletion && p4.gtdDirExists) return null;
388517
+ if (!p4.workingTreeClean && p4.pendingErrorsDeletion && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
388518
+ return null;
388519
+ if (p4.workingTreeClean && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
388520
+ return null;
388521
+ if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0) {
388522
+ if (p4.squashMsgPresent) {
388523
+ return {
388524
+ state: "squashing",
388525
+ autoAdvance: false,
388526
+ edgeAction: {
388527
+ kind: "squashCommit",
388528
+ squashBase: p4.squashBase,
388529
+ commitMessage: p4.squashMsgContent
388530
+ },
388531
+ context: buildContext(p4, counters)
388532
+ };
388533
+ }
388534
+ return { state: "squashing", autoAdvance: true, context: buildContext(p4, counters) };
388535
+ }
388536
+ const reviewable = p4.hasCommitsAfterLastDone && p4.reviewBase !== void 0 && (p4.refDiff ?? "").trim().length > 0;
388537
+ if (reviewable) {
388538
+ return { state: "clean", autoAdvance: false, context: buildContext(p4, counters) };
388539
+ }
388540
+ const squashHealth = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388989
388541
  return {
388990
- state: "fixing",
388542
+ state: "health-check",
388991
388543
  autoAdvance: true,
388992
388544
  edgeAction: {
388993
- kind: "commitPending",
388994
- prefix: p4.feedbackCommitted ? "gtd: fixing" : "gtd: feedback",
388995
- // Delete FEEDBACK.md so its removal lands in this commit; without it the
388996
- // next run re-detects FEEDBACK (precedence 2) and Fixing never returns to
388997
- // Testing (STATES.md § Fixing: "FEEDBACK.md is removed either way").
388998
- removeFeedback: true
388545
+ kind: "runHealthCheck",
388546
+ errorCount: counters.healthFixCount,
388547
+ capReached: counters.healthFixCount >= p4.fixAttemptCap,
388548
+ ...squashHealth ? { healthFixBase: p4.healthFixBase } : {},
388549
+ ...p4.pendingErrorsDeletion ? { commitErrorsReset: true } : {}
388999
388550
  },
389000
388551
  context: buildContext(p4, counters)
389001
388552
  };
389002
388553
  };
389003
- var resolveGtdLifecycle = (p4, counters, head7, corrupt) => {
389004
- if (p4.gtdModified) {
388554
+ var resolve4 = (events) => {
388555
+ const counters = foldCounters(events);
388556
+ let payload = DEFAULT_PAYLOAD;
388557
+ for (const event of events) if (event.type === "RESOLVE") payload = event.payload;
388558
+ const p4 = payload;
388559
+ assertLegal(p4);
388560
+ const head7 = p4.lastCommitSubject;
388561
+ const corrupt = () => {
388562
+ throw new GtdStateError(
388563
+ "corruption",
388564
+ `no precedence rule matched (HEAD="${head7}", clean=${p4.workingTreeClean}); repo is in an unrecognized state \u2014 refusing to guess`
388565
+ );
388566
+ };
388567
+ if (head7 === "gtd: transport") {
389005
388568
  return {
389006
- state: "planning",
388569
+ state: "transport",
389007
388570
  autoAdvance: true,
389008
- edgeAction: { kind: "commitPending", prefix: "gtd: planning" },
388571
+ edgeAction: { kind: "transportReset" },
389009
388572
  context: buildContext(p4, counters)
389010
388573
  };
389011
388574
  }
389012
- const noOpFixer = p4.workingTreeClean && head7 === "gtd: fixing";
389013
- if (p4.codeDirty || p4.pendingErrorsDeletion || noOpFixer) {
389014
- const resume2 = p4.pendingErrorsDeletion;
388575
+ if (p4.errorsPresent)
388576
+ return { state: "escalate", autoAdvance: false, context: buildContext(p4, counters) };
388577
+ if (p4.healthPresent) return resolveHealth(p4, counters);
388578
+ if (p4.feedbackPresent) return resolveFeedback(p4, counters);
388579
+ if (p4.gtdDirExists) return resolveGtdLifecycle(p4, counters, head7, corrupt);
388580
+ if (p4.reviewPresent) return resolveReviewLifecycle(p4, counters, head7);
388581
+ if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0 && p4.squashMsgPresent && !p4.codeDirty) {
389015
388582
  return {
389016
- state: "testing",
389017
- autoAdvance: true,
388583
+ state: "squashing",
388584
+ autoAdvance: false,
389018
388585
  edgeAction: {
389019
- kind: "runTest",
389020
- errorCount: resume2 ? 0 : counters.testFixCount,
389021
- capReached: (resume2 ? 0 : counters.testFixCount) >= p4.fixAttemptCap
388586
+ kind: "squashCommit",
388587
+ squashBase: p4.squashBase,
388588
+ commitMessage: p4.squashMsgContent
389022
388589
  },
389023
388590
  context: buildContext(p4, counters)
389024
388591
  };
389025
388592
  }
389026
- if (p4.workingTreeClean) {
389027
- if (head7 === "gtd: planning" || head7 === "gtd: package done") {
389028
- if (head7 === "gtd: planning" && p4.todoExists) {
389029
- return {
389030
- state: "building",
389031
- autoAdvance: true,
389032
- edgeAction: { kind: "commitPending", prefix: "gtd: planning", removeTodo: true },
389033
- context: buildContext(p4, counters)
389034
- };
389035
- }
389036
- return { state: "building", autoAdvance: true, context: buildContext(p4, counters) };
389037
- }
389038
- if (head7 === "gtd: building") {
389039
- const forceApprove = !p4.agenticReviewEnabled || counters.reviewFixCount >= p4.reviewThreshold;
389040
- if (forceApprove) {
389041
- return {
389042
- state: "close-package",
389043
- autoAdvance: true,
389044
- edgeAction: { kind: "closePackage" },
389045
- context: buildContext(p4, counters)
389046
- };
389047
- }
389048
- return { state: "agentic-review", autoAdvance: true, context: buildContext(p4, counters) };
389049
- }
389050
- }
389051
- return corrupt();
389052
- };
389053
- var resolveReviewLifecycle = (p4, counters, head7) => {
389054
- if (head7 === "gtd: review feedback") {
388593
+ if (p4.squashEnabled && p4.healthFixBase !== void 0 && p4.squashMsgPresent && p4.squashMsgContent !== HEALTH_SQUASH_SENTINEL && !p4.codeDirty) {
389055
388594
  return {
389056
- state: "accept-review",
389057
- autoAdvance: true,
389058
- edgeAction: { kind: "seedAcceptReview" },
388595
+ state: "squashing",
388596
+ autoAdvance: false,
388597
+ edgeAction: {
388598
+ kind: "squashCommit",
388599
+ squashBase: p4.healthFixBase,
388600
+ commitMessage: p4.squashMsgContent
388601
+ },
389059
388602
  context: buildContext(p4, counters)
389060
388603
  };
389061
388604
  }
389062
- if (p4.reviewCommitted || p4.reviewDirty && p4.reviewCheckboxOnly) {
388605
+ if (p4.squashEnabled && p4.healthFixBase !== void 0 && p4.squashMsgPresent && p4.squashMsgContent === HEALTH_SQUASH_SENTINEL) {
389063
388606
  return {
389064
- state: "done",
388607
+ state: "squashing",
389065
388608
  autoAdvance: true,
389066
- edgeAction: { kind: "done" },
388609
+ edgeAction: { kind: "removeHealthSentinel" },
389067
388610
  context: buildContext(p4, counters)
389068
388611
  };
389069
388612
  }
389070
- if (p4.reviewDirty) {
388613
+ if (isBoundary(head7) && p4.squashMsgPresent && !p4.codeDirty) {
389071
388614
  return {
389072
- state: "accept-review",
388615
+ state: "squashing",
389073
388616
  autoAdvance: true,
389074
- edgeAction: { kind: "seedAcceptReview" },
388617
+ edgeAction: { kind: "removeStraySquashMsg" },
389075
388618
  context: buildContext(p4, counters)
389076
388619
  };
389077
388620
  }
389078
- return {
389079
- state: "await-review",
389080
- autoAdvance: true,
389081
- edgeAction: { kind: "commitReview" },
389082
- context: buildContext(p4, counters)
389083
- };
389084
- };
389085
- var resolveGrilling = (p4, counters, head7) => {
389086
- const grillCommit = p4.todoCommitted && p4.codeDirty ? { kind: "captureGrillingEdits" } : { kind: "commitPending", prefix: "gtd: grilling" };
389087
- if (p4.todoMarkerPresent) {
389088
- const alreadyAtGrillingStop = p4.workingTreeClean && head7 === "gtd: grilling";
389089
- return {
389090
- state: "grilling",
389091
- autoAdvance: false,
389092
- ...alreadyAtGrillingStop ? {} : { edgeAction: grillCommit },
389093
- context: buildContext(p4, counters, "stop")
389094
- };
389095
- }
389096
- if (!p4.workingTreeClean) {
388621
+ if (isBoundary(head7) && !p4.workingTreeClean && !p4.todoCommitted || head7 === "gtd: new task" && p4.workingTreeClean) {
389097
388622
  return {
389098
- state: "grilling",
388623
+ state: "new-feature",
389099
388624
  autoAdvance: true,
389100
- edgeAction: grillCommit,
389101
- context: buildContext(p4, counters, "iterate")
388625
+ edgeAction: { kind: "seedNewFeature" },
388626
+ context: buildContext(p4, counters)
389102
388627
  };
389103
388628
  }
389104
- return {
389105
- state: "grilled",
389106
- autoAdvance: true,
389107
- edgeAction: { kind: "commitPending", prefix: "gtd: grilled" },
389108
- context: buildContext(p4, counters)
388629
+ if (p4.todoExists) return resolveGrilling(p4, counters, head7);
388630
+ return resolveCleanOrIdle(p4, counters, head7) ?? corrupt();
388631
+ };
388632
+ var cleanResult = (args2) => ({
388633
+ state: "clean",
388634
+ autoAdvance: args2.autoAdvance,
388635
+ context: {
388636
+ ...buildContext(
388637
+ {
388638
+ ...DEFAULT_PAYLOAD,
388639
+ reviewBase: args2.reviewBase,
388640
+ refDiff: args2.refDiff
388641
+ },
388642
+ { testFixCount: 0, reviewFixCount: 0, healthFixCount: 0 }
388643
+ )
388644
+ }
388645
+ });
388646
+
388647
+ // src/Events.ts
388648
+ var TODO_FILE = "TODO.md";
388649
+ var GTD_DIR = ".gtd";
388650
+ var REVIEW_FILE = "REVIEW.md";
388651
+ var FEEDBACK_FILE = "FEEDBACK.md";
388652
+ var ERRORS_FILE = "ERRORS.md";
388653
+ var HEALTH_FILE = "HEALTH.md";
388654
+ var EMPTY_FAILURE_SENTINEL = "Test command failed with no output (exit code non-zero).";
388655
+ var UNANSWERED_MARKER = "<!-- user answers here -->";
388656
+ var HEALTH_CHECK_SUBJECT = "gtd: health-check";
388657
+ var HEALTH_FIX_SUBJECT = "gtd: health-fix";
388658
+ var NEW_TASK_SUBJECT = "gtd: new task";
388659
+ var GRILLING_SUBJECT = "gtd: grilling";
388660
+ var PLANNING_SUBJECT = "gtd: planning";
388661
+ var BUILDING_SUBJECT = "gtd: building";
388662
+ var ERRORS_SUBJECT = "gtd: errors";
388663
+ var FEEDBACK_SUBJECT = "gtd: feedback";
388664
+ var PACKAGE_DONE_SUBJECT = "gtd: package done";
388665
+ var AWAITING_REVIEW_SUBJECT = "gtd: awaiting review";
388666
+ var DONE_SUBJECT = "gtd: done";
388667
+ var REVIEW_FEEDBACK_SUBJECT = "gtd: review feedback";
388668
+ var REVIEWING_SUBJECT = "gtd: reviewing";
388669
+ var SQUASH_MSG_FILE = "SQUASH_MSG.md";
388670
+ var STEERING_FILES = [
388671
+ TODO_FILE,
388672
+ REVIEW_FILE,
388673
+ FEEDBACK_FILE,
388674
+ ERRORS_FILE,
388675
+ HEALTH_FILE,
388676
+ SQUASH_MSG_FILE
388677
+ ];
388678
+ var WORKFLOW_FILE_EXCLUDES = [...STEERING_FILES, GTD_DIR];
388679
+ var isGtdPath = (path15) => path15 === GTD_DIR || path15.startsWith(`${GTD_DIR}/`);
388680
+ var isSteeringFile = (path15) => STEERING_FILES.includes(path15);
388681
+ var isUncommittedStatus = (status2) => status2.includes("?") || status2.includes("A");
388682
+ var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
388683
+ var unquoteGitPath = (raw) => {
388684
+ if (!raw.startsWith('"')) return raw;
388685
+ const inner = raw.slice(1, raw.endsWith('"') ? raw.length - 1 : raw.length);
388686
+ const bytes = [];
388687
+ const chars = [];
388688
+ const flushBytes = () => {
388689
+ if (bytes.length === 0) return;
388690
+ const buf = Buffer.from(bytes);
388691
+ chars.push(buf.toString("utf8"));
388692
+ bytes.length = 0;
389109
388693
  };
388694
+ let i = 0;
388695
+ while (i < inner.length) {
388696
+ if (inner[i] !== "\\") {
388697
+ flushBytes();
388698
+ chars.push(inner[i]);
388699
+ i++;
388700
+ continue;
388701
+ }
388702
+ const esc = inner[i + 1];
388703
+ if (esc === void 0) {
388704
+ flushBytes();
388705
+ chars.push("\\");
388706
+ i++;
388707
+ continue;
388708
+ }
388709
+ if (esc >= "0" && esc <= "7") {
388710
+ const oct = inner.slice(i + 1, i + 4);
388711
+ bytes.push(parseInt(oct, 8));
388712
+ i += 4;
388713
+ continue;
388714
+ }
388715
+ flushBytes();
388716
+ switch (esc) {
388717
+ case "n":
388718
+ chars.push("\n");
388719
+ break;
388720
+ case "t":
388721
+ chars.push(" ");
388722
+ break;
388723
+ case "r":
388724
+ chars.push("\r");
388725
+ break;
388726
+ case "\\":
388727
+ chars.push("\\");
388728
+ break;
388729
+ case '"':
388730
+ chars.push('"');
388731
+ break;
388732
+ default:
388733
+ chars.push("\\", esc);
388734
+ }
388735
+ i += 2;
388736
+ }
388737
+ flushBytes();
388738
+ return chars.join("");
389110
388739
  };
389111
- var resolveCleanOrIdle = (p4, counters, head7) => {
389112
- const isHealthHead = head7 === "gtd: health-check" || head7 === "gtd: health-fix";
389113
- if (!p4.workingTreeClean && !p4.pendingErrorsDeletion && isHealthHead) {
389114
- return {
389115
- state: "health-check",
389116
- autoAdvance: true,
389117
- edgeAction: { kind: "commitPending", prefix: "gtd: health-fix" },
389118
- context: buildContext(p4, counters)
389119
- };
388740
+ var parsePorcelainPaths = (porcelain) => porcelain.split("\n").map((line3) => line3.replace(/\r$/, "")).filter((line3) => line3.length > 0).map((line3) => ({ status: line3.slice(0, 2), path: unquoteGitPath(line3.slice(3)) }));
388741
+ var isNumberedDir = (name) => /^\d+-/.test(name);
388742
+ var isTaskFile = (name) => name.endsWith(".md");
388743
+ var stripCode = (content) => content.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n\1[^\n]*|$(?![\s\S]))/gm, "").replace(/`[^`\n]+`/g, "");
388744
+ var getPackages = (fs9, root2) => Effect_exports.gen(function* () {
388745
+ const resolve5 = (p4) => join11(root2, p4);
388746
+ const gtdExists = yield* fs9.exists(resolve5(GTD_DIR));
388747
+ if (!gtdExists) return [];
388748
+ const entries2 = yield* fs9.readDirectory(resolve5(GTD_DIR));
388749
+ const packageDirs = entries2.filter(isNumberedDir).sort();
388750
+ const packages = [];
388751
+ for (const dir of packageDirs) {
388752
+ const packagePath = `${GTD_DIR}/${dir}`;
388753
+ const stat4 = yield* fs9.stat(resolve5(packagePath));
388754
+ if (stat4.type !== "Directory") continue;
388755
+ const files = yield* fs9.readDirectory(resolve5(packagePath));
388756
+ const tasks = files.filter(isTaskFile).sort();
388757
+ const taskContents = [];
388758
+ for (const taskFile of tasks) {
388759
+ const content = yield* fs9.readFileString(resolve5(`${packagePath}/${taskFile}`));
388760
+ taskContents.push({ name: taskFile, content });
388761
+ }
388762
+ packages.push({ name: dir, tasks, taskContents });
389120
388763
  }
389121
- if (!p4.workingTreeClean && !p4.pendingErrorsDeletion) return null;
389122
- if (!p4.workingTreeClean && p4.pendingErrorsDeletion && p4.gtdDirExists) return null;
389123
- if (!p4.workingTreeClean && p4.pendingErrorsDeletion && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
389124
- return null;
389125
- if (p4.workingTreeClean && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
389126
- return null;
389127
- if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0) {
389128
- if (p4.squashMsgPresent) {
389129
- return {
389130
- state: "squashing",
389131
- autoAdvance: false,
389132
- edgeAction: {
389133
- kind: "squashCommit",
389134
- squashBase: p4.squashBase,
389135
- commitMessage: p4.squashMsgContent
389136
- },
389137
- context: buildContext(p4, counters)
389138
- };
388764
+ return packages;
388765
+ }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))));
388766
+ var CAPTURE_RULES = [
388767
+ "Interpret the captured diff with these rules:",
388768
+ "",
388769
+ "- **Code changes** are suggestions, not finished work \u2014 plan to re-implement",
388770
+ " them properly, including test coverage, rather than restoring them verbatim.",
388771
+ "- **Code comments** are positional feedback about the code at that location.",
388772
+ "- **TODO.md / REVIEW.md text changes** are global feedback on the plan or the",
388773
+ " reviewed work as a whole.",
388774
+ "- **Checkbox flips** in a captured REVIEW.md diff are approval noise \u2014 ignore",
388775
+ " them."
388776
+ ].join("\n");
388777
+ var seedTodo = (capturedDiff) => {
388778
+ const body = capturedDiff.replace(/\n+$/, "");
388779
+ const fence = fenceFor(body);
388780
+ return [
388781
+ "# Plan",
388782
+ "",
388783
+ "## Captured input",
388784
+ "",
388785
+ "These changes were captured as the starting point for this feature. Develop",
388786
+ "them into a concrete plan and surface any open questions for the user.",
388787
+ "",
388788
+ CAPTURE_RULES,
388789
+ "",
388790
+ `${fence}diff`,
388791
+ body,
388792
+ fence,
388793
+ ""
388794
+ ].join("\n");
388795
+ };
388796
+ var appendCapturedInput = (todo, capturedDiff) => {
388797
+ const body = capturedDiff.replace(/\n+$/, "");
388798
+ if (todo.includes(body)) return todo;
388799
+ const fence = fenceFor(body);
388800
+ const section = [
388801
+ "## Captured input (grilling)",
388802
+ "",
388803
+ "These code changes were made during grilling and captured as suggestions;",
388804
+ "gtd has reverted them from the working tree.",
388805
+ "",
388806
+ ...todo.includes(CAPTURE_RULES) ? [] : [CAPTURE_RULES, ""],
388807
+ `${fence}diff`,
388808
+ body,
388809
+ fence,
388810
+ ""
388811
+ ].join("\n");
388812
+ return todo.replace(/\n*$/, "\n\n") + section;
388813
+ };
388814
+ var isCheckboxOnlyDiff = (diff8) => {
388815
+ if (diff8.trim() === "") return false;
388816
+ const checkboxRe = /^(\s*- \[)([xX ])\](.*)$/;
388817
+ const removedLines = [];
388818
+ const addedLines = [];
388819
+ for (const raw of diff8.split("\n")) {
388820
+ const line3 = raw.replace(/\r$/, "");
388821
+ if (line3.startsWith("---") || line3.startsWith("+++") || line3.startsWith("@@") || line3.startsWith("diff ") || line3.startsWith("index ") || line3.startsWith("new file") || line3.startsWith("deleted file") || line3.startsWith("similarity") || line3.startsWith("rename"))
388822
+ continue;
388823
+ if (line3.startsWith("-")) {
388824
+ removedLines.push(line3.slice(1));
388825
+ } else if (line3.startsWith("+")) {
388826
+ addedLines.push(line3.slice(1));
389139
388827
  }
389140
- return { state: "squashing", autoAdvance: true, context: buildContext(p4, counters) };
389141
388828
  }
389142
- const reviewable = p4.hasCommitsAfterLastDone && p4.reviewBase !== void 0 && (p4.refDiff ?? "").trim().length > 0;
389143
- if (reviewable) {
389144
- return { state: "clean", autoAdvance: false, context: buildContext(p4, counters) };
388829
+ if (removedLines.length !== addedLines.length) return false;
388830
+ let flips = 0;
388831
+ for (let i = 0; i < removedLines.length; i++) {
388832
+ const rm4 = removedLines[i];
388833
+ const add7 = addedLines[i];
388834
+ if (rm4 === add7) continue;
388835
+ if (!checkboxRe.test(rm4) || !checkboxRe.test(add7)) return false;
388836
+ const rmNorm = rm4.replace(/\[[ xX]\]/, "[ ]");
388837
+ const addNorm = add7.replace(/\[[ xX]\]/, "[ ]");
388838
+ if (rmNorm !== addNorm) return false;
388839
+ flips += 1;
389145
388840
  }
389146
- const squashHealth = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
389147
- return {
389148
- state: "health-check",
389149
- autoAdvance: true,
389150
- edgeAction: {
389151
- kind: "runHealthCheck",
389152
- errorCount: counters.healthFixCount,
389153
- capReached: counters.healthFixCount >= p4.fixAttemptCap,
389154
- ...squashHealth ? { healthFixBase: p4.healthFixBase } : {},
389155
- ...p4.pendingErrorsDeletion ? { commitErrorsReset: true } : {}
389156
- },
389157
- context: buildContext(p4, counters)
389158
- };
388841
+ return flips > 0;
389159
388842
  };
389160
- var resolve4 = (events) => {
389161
- const counters = foldCounters(events);
389162
- let payload = DEFAULT_PAYLOAD;
389163
- for (const event of events) if (event.type === "RESOLVE") payload = event.payload;
389164
- const p4 = payload;
389165
- assertLegal(p4);
389166
- const head7 = p4.lastCommitSubject;
389167
- const corrupt = () => {
389168
- throw new GtdStateError(
389169
- "corruption",
389170
- `no precedence rule matched (HEAD="${head7}", clean=${p4.workingTreeClean}); repo is in an unrecognized state \u2014 refusing to guess`
388843
+ var gatherEvents = () => (
388844
+ // fallow-ignore-next-line complexity
388845
+ Effect_exports.gen(function* () {
388846
+ const git = yield* GitService;
388847
+ const fs9 = yield* FileSystem_exports.FileSystem;
388848
+ const config2 = yield* ConfigService;
388849
+ const { root: root2 } = yield* Cwd;
388850
+ const resolve5 = (p4) => join11(root2, p4);
388851
+ const defaultBranch = yield* git.resolveDefaultBranch();
388852
+ const headHash = yield* git.resolveRef("HEAD").pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388853
+ const mergeBase = Option_exports.isSome(defaultBranch) ? yield* git.mergeBase(defaultBranch.value, "HEAD") : Option_exports.none();
388854
+ const base = Option_exports.isSome(mergeBase) && mergeBase.value !== headHash ? mergeBase : Option_exports.none();
388855
+ const history = yield* git.commitHistory(Option_exports.getOrUndefined(base));
388856
+ const commitEvents = history.map(
388857
+ ({ message, removedErrors }) => {
388858
+ const subject = (message.split("\n")[0] ?? "").trim();
388859
+ return {
388860
+ type: "COMMIT",
388861
+ isErrors: subject === ERRORS_SUBJECT,
388862
+ isFeedback: subject === FEEDBACK_SUBJECT,
388863
+ isPackageStart: subject === PLANNING_SUBJECT || subject === PACKAGE_DONE_SUBJECT,
388864
+ isWorkflowCommit: subject.startsWith("gtd: "),
388865
+ removedErrors,
388866
+ isHealthCheck: subject === HEALTH_CHECK_SUBJECT,
388867
+ isHealthFix: subject === HEALTH_FIX_SUBJECT
388868
+ };
388869
+ }
389171
388870
  );
389172
- };
389173
- if (head7 === "gtd: transport") {
389174
- return {
389175
- state: "transport",
389176
- autoAdvance: true,
389177
- edgeAction: { kind: "transportReset" },
389178
- context: buildContext(p4, counters)
389179
- };
389180
- }
389181
- if (p4.errorsPresent)
389182
- return { state: "escalate", autoAdvance: false, context: buildContext(p4, counters) };
389183
- if (p4.healthPresent) return resolveHealth(p4, counters);
389184
- if (p4.feedbackPresent) return resolveFeedback(p4, counters);
389185
- if (p4.gtdDirExists) return resolveGtdLifecycle(p4, counters, head7, corrupt);
389186
- if (p4.reviewPresent) return resolveReviewLifecycle(p4, counters, head7);
389187
- if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0 && p4.squashMsgPresent && !p4.codeDirty) {
389188
- return {
389189
- state: "squashing",
389190
- autoAdvance: false,
389191
- edgeAction: {
389192
- kind: "squashCommit",
389193
- squashBase: p4.squashBase,
389194
- commitMessage: p4.squashMsgContent
389195
- },
389196
- context: buildContext(p4, counters)
389197
- };
389198
- }
389199
- if (p4.squashEnabled && p4.healthFixBase !== void 0 && p4.squashMsgPresent && !p4.codeDirty) {
389200
- return {
389201
- state: "squashing",
389202
- autoAdvance: false,
389203
- edgeAction: {
389204
- kind: "squashCommit",
389205
- squashBase: p4.healthFixBase,
389206
- commitMessage: p4.squashMsgContent
389207
- },
389208
- context: buildContext(p4, counters)
388871
+ const hasCommits = yield* git.hasCommits();
388872
+ const porcelain = yield* git.statusPorcelain();
388873
+ const entries2 = parsePorcelainPaths(porcelain);
388874
+ const workingTreeClean = entries2.length === 0;
388875
+ const lastCommitSubject = hasCommits ? yield* git.lastCommitSubject() : "";
388876
+ const gtdModified = entries2.some((e7) => isGtdPath(e7.path));
388877
+ const codeDirty = entries2.some((e7) => !isSteeringFile(e7.path) && !isGtdPath(e7.path));
388878
+ const todoExists = yield* fs9.exists(resolve5(TODO_FILE));
388879
+ const gtdDirExists = yield* fs9.exists(resolve5(GTD_DIR));
388880
+ const reviewPresent = yield* fs9.exists(resolve5(REVIEW_FILE));
388881
+ const feedbackPresent = yield* fs9.exists(resolve5(FEEDBACK_FILE));
388882
+ const errorsPresent = yield* fs9.exists(resolve5(ERRORS_FILE));
388883
+ const todoContent = todoExists ? yield* fs9.readFileString(resolve5(TODO_FILE)) : "";
388884
+ const todoMarkerPresent = todoExists && stripCode(todoContent).includes(UNANSWERED_MARKER);
388885
+ const isUncommitted = (path15) => {
388886
+ const entry = entries2.find((e7) => e7.path === path15);
388887
+ return entry !== void 0 && isUncommittedStatus(entry.status);
389209
388888
  };
389210
- }
389211
- if (isBoundary(head7) && !p4.workingTreeClean && !p4.todoCommitted || head7 === "gtd: new task" && p4.workingTreeClean) {
389212
- return {
389213
- state: "new-feature",
389214
- autoAdvance: true,
389215
- edgeAction: { kind: "seedNewFeature" },
389216
- context: buildContext(p4, counters)
388889
+ const feedbackCommitted = feedbackPresent && !isUncommitted(FEEDBACK_FILE);
388890
+ const feedbackContent = feedbackPresent ? yield* fs9.readFileString(resolve5(FEEDBACK_FILE)) : "";
388891
+ const feedbackEmpty = feedbackPresent && !/\S/.test(feedbackContent);
388892
+ const reviewTrackedAtHead = reviewPresent && !isUncommitted(REVIEW_FILE);
388893
+ const reviewCommitted = reviewTrackedAtHead && workingTreeClean;
388894
+ const reviewDirty = reviewTrackedAtHead && !workingTreeClean;
388895
+ const todoCommitted = todoExists && !isUncommitted(TODO_FILE);
388896
+ const pendingErrorsDeletion = entries2.some(
388897
+ (e7) => e7.path === ERRORS_FILE && e7.status.includes("D")
388898
+ );
388899
+ const packages = yield* getPackages(fs9, root2);
388900
+ const diff8 = hasCommits && entries2.length > 0 ? yield* git.diffHead() : "";
388901
+ const onlyReviewDirty = entries2.length > 0 && entries2.every((e7) => e7.path === REVIEW_FILE);
388902
+ const reviewCheckboxOnly = onlyReviewDirty && isCheckboxOnlyDiff(
388903
+ yield* git.diffPath(REVIEW_FILE).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")))
388904
+ );
388905
+ let reviewBase;
388906
+ let refDiff;
388907
+ let hasCommitsAfterLastDone = true;
388908
+ if (hasCommits) {
388909
+ const allHistory = Option_exports.isNone(base) ? history : yield* git.commitHistory();
388910
+ const lastDoneIdx = (() => {
388911
+ let idx = -1;
388912
+ for (let i = 0; i < allHistory.length; i++) {
388913
+ const subject = (allHistory[i].message.split("\n")[0] ?? "").trim();
388914
+ if (subject === DONE_SUBJECT) idx = i;
388915
+ }
388916
+ return idx;
388917
+ })();
388918
+ const currentCycle = lastDoneIdx === -1 ? allHistory : allHistory.slice(lastDoneIdx + 1);
388919
+ hasCommitsAfterLastDone = lastDoneIdx === -1 || currentCycle.length > 0;
388920
+ const firstGrilling = currentCycle.find(
388921
+ (c7) => (c7.message.split("\n")[0] ?? "").trim() === GRILLING_SUBJECT
388922
+ );
388923
+ const lastAwaitingReview = (() => {
388924
+ let found;
388925
+ for (const c7 of currentCycle) {
388926
+ if ((c7.message.split("\n")[0] ?? "").trim().startsWith(AWAITING_REVIEW_SUBJECT)) {
388927
+ found = c7;
388928
+ }
388929
+ }
388930
+ return found;
388931
+ })();
388932
+ const withinProcess = firstGrilling !== void 0;
388933
+ let candidate;
388934
+ if (withinProcess) {
388935
+ if (lastAwaitingReview !== void 0) {
388936
+ candidate = lastAwaitingReview.hash ?? EMPTY_TREE;
388937
+ } else {
388938
+ candidate = firstGrilling.hash ?? EMPTY_TREE;
388939
+ }
388940
+ } else {
388941
+ }
388942
+ if (candidate !== void 0 && hasCommitsAfterLastDone) {
388943
+ const candidateDiff = yield* git.diffRef(candidate, WORKFLOW_FILE_EXCLUDES).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388944
+ if (candidateDiff.trim().length > 0) {
388945
+ reviewBase = candidate;
388946
+ refDiff = candidateDiff;
388947
+ }
388948
+ }
388949
+ }
388950
+ let squashBase;
388951
+ let squashDiff;
388952
+ if (hasCommits && lastCommitSubject === DONE_SUBJECT && config2.squash) {
388953
+ const squashHistory = history;
388954
+ const subjectOf = (c7) => (c7.message.split("\n")[0] ?? "").trim();
388955
+ let lastDoneIdxForSquash = -1;
388956
+ let prevDoneIdx = -1;
388957
+ for (let i = 0; i < squashHistory.length; i++) {
388958
+ if (subjectOf(squashHistory[i]) === DONE_SUBJECT) {
388959
+ prevDoneIdx = lastDoneIdxForSquash;
388960
+ lastDoneIdxForSquash = i;
388961
+ }
388962
+ }
388963
+ if (lastDoneIdxForSquash !== -1) {
388964
+ const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
388965
+ let startIdx = -1;
388966
+ for (let i = squashCycle.length - 1; i >= 0; i--) {
388967
+ if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT || subjectOf(squashCycle[i]) === REVIEWING_SUBJECT) {
388968
+ startIdx = i;
388969
+ break;
388970
+ }
388971
+ }
388972
+ if (startIdx === -1) {
388973
+ for (let i = squashCycle.length - 1; i >= 0; i--) {
388974
+ if (subjectOf(squashCycle[i]) === GRILLING_SUBJECT) {
388975
+ startIdx = i;
388976
+ while (startIdx > 0 && subjectOf(squashCycle[startIdx - 1]) === GRILLING_SUBJECT) {
388977
+ startIdx--;
388978
+ }
388979
+ break;
388980
+ }
388981
+ }
388982
+ }
388983
+ const squashStart = startIdx === -1 ? void 0 : squashCycle[startIdx];
388984
+ if (squashStart !== void 0) {
388985
+ const squashStartParent = yield* git.resolveRef(`${squashStart.hash}~1`).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
388986
+ const candidateDiff = yield* git.diffRef(squashStartParent).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
388987
+ if (candidateDiff.trim().length > 0) {
388988
+ squashBase = squashStartParent;
388989
+ squashDiff = candidateDiff;
388990
+ }
388991
+ }
388992
+ }
388993
+ }
388994
+ const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388995
+ const squashMsgContent = squashMsgPresent ? yield* fs9.readFileString(resolve5(SQUASH_MSG_FILE)) : "";
388996
+ const healthPresent = yield* fs9.exists(resolve5(HEALTH_FILE));
388997
+ const healthContent = healthPresent ? yield* fs9.readFileString(resolve5(HEALTH_FILE)) : "";
388998
+ const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
388999
+ let healthFixBase;
389000
+ if (config2.squash) {
389001
+ const subjectOf = (c7) => (c7.message.split("\n")[0] ?? "").trim();
389002
+ let firstHealthCheckHash;
389003
+ let healthCheckCount = 0;
389004
+ for (const commit3 of history) {
389005
+ const s5 = subjectOf(commit3);
389006
+ const isPackageStart = s5 === PLANNING_SUBJECT || s5 === PACKAGE_DONE_SUBJECT;
389007
+ if (isPackageStart || commit3.removedErrors) {
389008
+ firstHealthCheckHash = void 0;
389009
+ healthCheckCount = 0;
389010
+ }
389011
+ if (s5 === HEALTH_CHECK_SUBJECT) {
389012
+ healthCheckCount++;
389013
+ if (healthCheckCount === 1) firstHealthCheckHash = commit3.hash;
389014
+ }
389015
+ }
389016
+ if (healthCheckCount > 0 && firstHealthCheckHash !== void 0) {
389017
+ const base2 = yield* git.resolveRef(`${firstHealthCheckHash}~1`).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
389018
+ healthFixBase = base2;
389019
+ const healthCandidateDiff = yield* git.diffRef(base2).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
389020
+ if (healthCandidateDiff.trim().length > 0) {
389021
+ squashBase = base2;
389022
+ squashDiff = healthCandidateDiff;
389023
+ }
389024
+ }
389025
+ }
389026
+ const payload = {
389027
+ todoExists,
389028
+ todoCommitted,
389029
+ gtdDirExists,
389030
+ reviewPresent,
389031
+ feedbackPresent,
389032
+ errorsPresent,
389033
+ gtdModified,
389034
+ codeDirty,
389035
+ todoMarkerPresent,
389036
+ feedbackCommitted,
389037
+ feedbackEmpty,
389038
+ feedbackContent,
389039
+ reviewCommitted,
389040
+ reviewDirty,
389041
+ reviewCheckboxOnly,
389042
+ pendingErrorsDeletion,
389043
+ lastCommitSubject,
389044
+ workingTreeClean,
389045
+ packages,
389046
+ diff: diff8,
389047
+ ...reviewBase !== void 0 ? { reviewBase } : {},
389048
+ ...refDiff !== void 0 ? { refDiff } : {},
389049
+ hasCommitsAfterLastDone,
389050
+ agenticReviewEnabled: config2.agenticReview,
389051
+ fixAttemptCap: config2.fixAttemptCap,
389052
+ reviewThreshold: config2.reviewThreshold,
389053
+ squashEnabled: config2.squash,
389054
+ ...squashBase !== void 0 ? { squashBase } : {},
389055
+ ...squashDiff !== void 0 ? { squashDiff } : {},
389056
+ squashMsgPresent,
389057
+ squashMsgContent,
389058
+ healthPresent,
389059
+ healthContent,
389060
+ healthCommitted,
389061
+ ...healthFixBase !== void 0 ? { healthFixBase } : {}
389217
389062
  };
389063
+ const resolveEvent = { type: "RESOLVE", payload };
389064
+ return [...commitEvents, resolveEvent];
389065
+ }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
389066
+ );
389067
+ var captureAndRevert = (git, subject, onRegen) => Effect_exports.gen(function* () {
389068
+ const head7 = yield* git.lastCommitSubject().pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
389069
+ if (head7 !== subject) {
389070
+ yield* git.commitAllWithPrefix(subject);
389071
+ } else {
389072
+ yield* onRegen;
389218
389073
  }
389219
- if (p4.todoExists) return resolveGrilling(p4, counters, head7);
389220
- return resolveCleanOrIdle(p4, counters, head7) ?? corrupt();
389221
- };
389222
- var cleanResult = (args2) => ({
389223
- state: "clean",
389224
- autoAdvance: args2.autoAdvance,
389225
- context: {
389226
- ...buildContext(
389227
- {
389228
- ...DEFAULT_PAYLOAD,
389229
- reviewBase: args2.reviewBase,
389230
- refDiff: args2.refDiff
389231
- },
389232
- { testFixCount: 0, reviewFixCount: 0, healthFixCount: 0 }
389233
- )
389234
- }
389074
+ const base = yield* git.resolveRef("HEAD~1").pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
389075
+ const captured = yield* git.diffRef(base).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
389076
+ yield* git.revertNoCommit("HEAD");
389077
+ return captured;
389078
+ });
389079
+ var reviewAgainst = (target) => Effect_exports.gen(function* () {
389080
+ const git = yield* GitService;
389081
+ const targetHash = yield* git.resolveRef(target);
389082
+ const mergeBase = yield* git.mergeBase(target, "HEAD");
389083
+ const mergeBaseHash = Option_exports.isNone(mergeBase) || mergeBase.value === targetHash ? targetHash : mergeBase.value;
389084
+ const refDiff = yield* git.diffRef(mergeBaseHash, WORKFLOW_FILE_EXCLUDES);
389085
+ if (refDiff.trim().length === 0) return void 0;
389086
+ return { reviewBase: mergeBaseHash, refDiff };
389235
389087
  });
389088
+ var perform = (action) => (
389089
+ // fallow-ignore-next-line complexity
389090
+ Effect_exports.gen(function* () {
389091
+ const git = yield* GitService;
389092
+ const fs9 = yield* FileSystem_exports.FileSystem;
389093
+ const { root: root2 } = yield* Cwd;
389094
+ const resolve5 = (p4) => join11(root2, p4);
389095
+ switch (action.kind) {
389096
+ // Transport: mixed-reset the hand-made `gtd: transport` HEAD, keeping the
389097
+ // work in the tree, then re-derive. (No producer command — consume only.)
389098
+ case "transportReset": {
389099
+ yield* git.mixedResetHead();
389100
+ return { stop: false };
389101
+ }
389102
+ // New Feature: capture the raw input as `gtd: new task` (unless HEAD is
389103
+ // already there — the lost-seed regenerate case), revert it back to a
389104
+ // clean baseline (inverse staged), and seed TODO.md from that diff.
389105
+ case "seedNewFeature": {
389106
+ const captured = yield* captureAndRevert(git, NEW_TASK_SUBJECT, Effect_exports.void);
389107
+ yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
389108
+ return { stop: false };
389109
+ }
389110
+ // Accept Review: capture the human's pending changeset durably as
389111
+ // `gtd: review feedback` (annotations, code edits, and new files alike),
389112
+ // revert it back to the reviewed baseline, rm REVIEW.md (which is what
389113
+ // stops Accept Review re-firing), and seed TODO.md from the captured
389114
+ // diff. The regen case (the machine's rule-4 carve-out fired) discards
389115
+ // any partial revert/seed state with a hard reset first.
389116
+ case "seedAcceptReview": {
389117
+ const captured = yield* captureAndRevert(git, REVIEW_FEEDBACK_SUBJECT, git.resetHard());
389118
+ yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389119
+ yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
389120
+ return { stop: false };
389121
+ }
389122
+ // Grilling capture: the plan is already committed and the user sketched
389123
+ // code during the round. Fold the code diff (untracked included, steering
389124
+ // files excluded) into TODO.md as a suggestion block, drop the code
389125
+ // changes — reset tracked, delete untracked/added — and commit the lot as
389126
+ // one `gtd: grilling`. TODO.md's own pending edits are preserved verbatim
389127
+ // (snapshotted before the reset). Binary edits survive only as the diff's
389128
+ // "Binary files differ" line — an accepted limitation.
389129
+ case "captureGrillingEdits": {
389130
+ const porcelain = yield* git.statusPorcelain();
389131
+ const entries2 = parsePorcelainPaths(porcelain);
389132
+ const pendingCodeFiles = entries2.filter(
389133
+ (e7) => isUncommittedStatus(e7.status) && !isSteeringFile(e7.path) && !isGtdPath(e7.path)
389134
+ );
389135
+ const todoNow = yield* fs9.readFileString(resolve5(TODO_FILE));
389136
+ const captured = yield* git.diffHead(WORKFLOW_FILE_EXCLUDES);
389137
+ yield* git.resetHard();
389138
+ for (const entry of pendingCodeFiles) {
389139
+ yield* fs9.remove(resolve5(entry.path), { recursive: true }).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389140
+ }
389141
+ yield* fs9.writeFileString(resolve5(TODO_FILE), appendCapturedInput(todoNow, captured));
389142
+ yield* git.commitAllWithPrefix(GRILLING_SUBJECT);
389143
+ return { stop: false };
389144
+ }
389145
+ // Testing: commit the pending tree `gtd: building` (nothing pending in the
389146
+ // no-op-fixer case), run tests; on red write FEEDBACK (below cap) or ERRORS
389147
+ // (at cap) and commit `gtd: errors`; on green proceed.
389148
+ case "runTest": {
389149
+ const runner = yield* TestRunner;
389150
+ const status2 = yield* git.statusPorcelain();
389151
+ const committedBuilding = status2.trim().length > 0;
389152
+ if (committedBuilding) {
389153
+ yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
389154
+ }
389155
+ const result = yield* runner.run();
389156
+ if (result.exitCode === 0) {
389157
+ if (!committedBuilding) {
389158
+ yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
389159
+ }
389160
+ return { stop: false };
389161
+ }
389162
+ const target = action.capReached ? ERRORS_FILE : FEEDBACK_FILE;
389163
+ const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
389164
+ yield* fs9.writeFileString(resolve5(target), body);
389165
+ yield* git.commitAllWithPrefix(ERRORS_SUBJECT);
389166
+ return { stop: false };
389167
+ }
389168
+ // Health check: run tests on an idle/clean tree.
389169
+ // Green, no prior fixes → stop immediately (no commit).
389170
+ // Green, fixes exist → stop immediately (squash handled via healthFixBase
389171
+ // when gatherEvents sets it; the machine resolves squashing state).
389172
+ // Red below cap → write HEALTH.md (uncommitted); resolveHealth will
389173
+ // commit it as `gtd: health-check` via `commitPending removeHealth`.
389174
+ // Red at cap → write ERRORS.md and commit `gtd: health-check` immediately
389175
+ // so the Escalate state fires on the next resolve.
389176
+ case "runHealthCheck": {
389177
+ if (action.commitErrorsReset === true) {
389178
+ yield* git.commitAllWithPrefix(HEALTH_FIX_SUBJECT);
389179
+ }
389180
+ const runner = yield* TestRunner;
389181
+ const result = yield* runner.run();
389182
+ if (result.exitCode === 0) {
389183
+ if (action.healthFixBase !== void 0 && action.commitErrorsReset !== true) {
389184
+ yield* fs9.writeFileString(resolve5(SQUASH_MSG_FILE), HEALTH_SQUASH_SENTINEL);
389185
+ return { stop: false };
389186
+ }
389187
+ return { stop: true };
389188
+ }
389189
+ const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
389190
+ if (action.capReached) {
389191
+ yield* fs9.writeFileString(resolve5(ERRORS_FILE), body);
389192
+ yield* git.commitAllWithPrefix(HEALTH_CHECK_SUBJECT);
389193
+ } else {
389194
+ yield* fs9.writeFileString(resolve5(HEALTH_FILE), body);
389195
+ }
389196
+ return { stop: false };
389197
+ }
389198
+ // Grilling / Grilled / Planning / Fixing: commit the pending tree under a
389199
+ // fixed phase prefix. Fixing sets `removeFeedback` so FEEDBACK.md's removal
389200
+ // lands in the `gtd: fixing` / `gtd: feedback` commit — otherwise the next
389201
+ // run re-detects FEEDBACK (precedence 2) and Fixing re-fires forever instead
389202
+ // of returning to Testing (STATES.md § Fixing).
389203
+ case "commitPending": {
389204
+ if (action.removeFeedback === true) {
389205
+ yield* fs9.remove(resolve5(FEEDBACK_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389206
+ }
389207
+ if (action.removeTodo === true) {
389208
+ yield* fs9.remove(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389209
+ }
389210
+ if (action.removeHealth === true) {
389211
+ yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389212
+ }
389213
+ if (action.removeTodo !== true) {
389214
+ yield* formatFile(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389215
+ }
389216
+ yield* git.commitAllWithPrefix(action.prefix);
389217
+ return { stop: false };
389218
+ }
389219
+ // Close package: remove the (maybe-empty / maybe-absent) FEEDBACK.md, rm
389220
+ // the first (finished) package dir (+ the now-empty `.gtd/`), commit
389221
+ // `gtd: package done`. Tolerates an absent FEEDBACK.md (force-approve).
389222
+ case "closePackage": {
389223
+ yield* fs9.remove(resolve5(FEEDBACK_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389224
+ const packages = yield* getPackages(fs9, root2);
389225
+ const first2 = packages[0];
389226
+ if (first2 !== void 0) {
389227
+ yield* git.removePackageDir(`${GTD_DIR}/${first2.name}`);
389228
+ }
389229
+ yield* git.commitAllWithPrefix(PACKAGE_DONE_SUBJECT);
389230
+ return { stop: false };
389231
+ }
389232
+ // Await Review: commit REVIEW.md as `gtd: awaiting review`.
389233
+ case "commitReview": {
389234
+ yield* formatFile(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389235
+ yield* git.commitAllWithPrefix(AWAITING_REVIEW_SUBJECT);
389236
+ return { stop: false };
389237
+ }
389238
+ // Done: rm REVIEW.md, commit `gtd: done`.
389239
+ case "done": {
389240
+ yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389241
+ yield* git.commitAllWithPrefix(DONE_SUBJECT);
389242
+ return { stop: false };
389243
+ }
389244
+ // Squash: rm SQUASH_MSG.md, soft-reset to squashBase, commit everything
389245
+ // under the provided commit message.
389246
+ case "squashCommit": {
389247
+ yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389248
+ yield* git.softResetTo(action.squashBase);
389249
+ yield* git.commitAllWithPrefix(action.commitMessage);
389250
+ return { stop: false };
389251
+ }
389252
+ // Remove the HEALTH_SQUASH_SENTINEL file written by runHealthCheck on
389253
+ // green-with-fixes, so the squash-prompt state is presented clean. The
389254
+ // agent then writes the real SQUASH_MSG.md on its turn. This action runs
389255
+ // BEFORE the prompt is emitted (squashing is not edge-only, so the driver
389256
+ // emits the prompt after perform returns).
389257
+ case "removeHealthSentinel": {
389258
+ yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389259
+ return { stop: false };
389260
+ }
389261
+ // Stray SQUASH_MSG.md found under a boundary HEAD with no matching squash
389262
+ // cycle (squashBase absent / squashEnabled false). Remove it so the next
389263
+ // gather sees a clean tree and settles to health-check or idle.
389264
+ case "removeStraySquashMsg": {
389265
+ yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
389266
+ return { stop: false };
389267
+ }
389268
+ }
389269
+ }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
389270
+ );
389236
389271
 
389237
389272
  // src/State.ts
389238
389273
  var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([