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