@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.
- package/README.md +5 -2
- package/dist/gtd.bundle.mjs +932 -897
- package/package.json +1 -1
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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/
|
|
388265
|
-
var
|
|
388266
|
-
var
|
|
388267
|
-
|
|
388268
|
-
|
|
388269
|
-
|
|
388270
|
-
|
|
388271
|
-
|
|
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
|
|
388358
|
-
|
|
388359
|
-
|
|
388360
|
-
|
|
388361
|
-
|
|
388362
|
-
|
|
388363
|
-
|
|
388364
|
-
|
|
388365
|
-
|
|
388366
|
-
|
|
388367
|
-
|
|
388368
|
-
|
|
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
|
|
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
|
|
388432
|
-
|
|
388433
|
-
|
|
388434
|
-
|
|
388435
|
-
|
|
388436
|
-
|
|
388437
|
-
|
|
388438
|
-
|
|
388439
|
-
|
|
388440
|
-
|
|
388441
|
-
|
|
388442
|
-
|
|
388443
|
-
|
|
388444
|
-
|
|
388445
|
-
|
|
388446
|
-
|
|
388447
|
-
|
|
388448
|
-
|
|
388449
|
-
|
|
388450
|
-
|
|
388451
|
-
|
|
388452
|
-
|
|
388453
|
-
|
|
388454
|
-
|
|
388455
|
-
|
|
388456
|
-
|
|
388457
|
-
|
|
388458
|
-
|
|
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
|
|
388461
|
-
|
|
388462
|
-
|
|
388463
|
-
|
|
388464
|
-
|
|
388465
|
-
|
|
388466
|
-
|
|
388467
|
-
|
|
388468
|
-
|
|
388469
|
-
|
|
388470
|
-
|
|
388471
|
-
|
|
388472
|
-
|
|
388473
|
-
|
|
388474
|
-
|
|
388475
|
-
|
|
388476
|
-
|
|
388477
|
-
|
|
388478
|
-
|
|
388479
|
-
|
|
388480
|
-
|
|
388481
|
-
|
|
388482
|
-
|
|
388483
|
-
|
|
388484
|
-
|
|
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
|
-
|
|
388489
|
-
|
|
388490
|
-
|
|
388491
|
-
|
|
388492
|
-
|
|
388493
|
-
|
|
388494
|
-
|
|
388495
|
-
|
|
388496
|
-
|
|
388497
|
-
|
|
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
|
-
|
|
388507
|
-
|
|
388508
|
-
|
|
388509
|
-
|
|
388510
|
-
|
|
388511
|
-
|
|
388512
|
-
|
|
388513
|
-
|
|
388514
|
-
(
|
|
388515
|
-
|
|
388516
|
-
|
|
388517
|
-
|
|
388518
|
-
|
|
388519
|
-
|
|
388520
|
-
|
|
388521
|
-
|
|
388522
|
-
|
|
388523
|
-
|
|
388524
|
-
|
|
388525
|
-
|
|
388526
|
-
|
|
388527
|
-
|
|
388528
|
-
|
|
388529
|
-
|
|
388530
|
-
|
|
388531
|
-
|
|
388532
|
-
|
|
388533
|
-
|
|
388534
|
-
|
|
388535
|
-
|
|
388536
|
-
|
|
388537
|
-
|
|
388538
|
-
|
|
388539
|
-
|
|
388540
|
-
|
|
388541
|
-
|
|
388542
|
-
|
|
388543
|
-
|
|
388544
|
-
|
|
388545
|
-
|
|
388546
|
-
|
|
388547
|
-
return
|
|
388548
|
-
|
|
388549
|
-
|
|
388550
|
-
|
|
388551
|
-
|
|
388552
|
-
|
|
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
|
-
|
|
388568
|
-
|
|
388569
|
-
|
|
388570
|
-
|
|
388571
|
-
|
|
388572
|
-
|
|
388573
|
-
|
|
388574
|
-
|
|
388575
|
-
|
|
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
|
-
|
|
388612
|
-
|
|
388613
|
-
|
|
388614
|
-
|
|
388615
|
-
|
|
388616
|
-
|
|
388617
|
-
|
|
388618
|
-
|
|
388619
|
-
|
|
388620
|
-
|
|
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
|
-
|
|
388687
|
-
|
|
388688
|
-
|
|
388689
|
-
|
|
388690
|
-
}
|
|
388691
|
-
|
|
388692
|
-
|
|
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
|
-
|
|
388881
|
-
|
|
388882
|
-
|
|
388883
|
-
|
|
388884
|
-
|
|
388885
|
-
|
|
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 {
|
|
388894
|
-
|
|
388895
|
-
|
|
388896
|
-
|
|
388897
|
-
|
|
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
|
|
388962
|
-
const
|
|
388963
|
-
|
|
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: "
|
|
388499
|
+
state: "grilled",
|
|
388966
388500
|
autoAdvance: true,
|
|
388967
|
-
|
|
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
|
|
388981
|
-
|
|
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: "
|
|
388509
|
+
state: "health-check",
|
|
388984
388510
|
autoAdvance: true,
|
|
388985
|
-
edgeAction: { kind: "
|
|
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: "
|
|
388542
|
+
state: "health-check",
|
|
388991
388543
|
autoAdvance: true,
|
|
388992
388544
|
edgeAction: {
|
|
388993
|
-
kind: "
|
|
388994
|
-
|
|
388995
|
-
|
|
388996
|
-
|
|
388997
|
-
|
|
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
|
|
389004
|
-
|
|
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: "
|
|
388569
|
+
state: "transport",
|
|
389007
388570
|
autoAdvance: true,
|
|
389008
|
-
edgeAction: { kind: "
|
|
388571
|
+
edgeAction: { kind: "transportReset" },
|
|
389009
388572
|
context: buildContext(p4, counters)
|
|
389010
388573
|
};
|
|
389011
388574
|
}
|
|
389012
|
-
|
|
389013
|
-
|
|
389014
|
-
|
|
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: "
|
|
389017
|
-
autoAdvance:
|
|
388583
|
+
state: "squashing",
|
|
388584
|
+
autoAdvance: false,
|
|
389018
388585
|
edgeAction: {
|
|
389019
|
-
kind: "
|
|
389020
|
-
|
|
389021
|
-
|
|
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.
|
|
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: "
|
|
389057
|
-
autoAdvance:
|
|
389058
|
-
edgeAction: {
|
|
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.
|
|
388605
|
+
if (p4.squashEnabled && p4.healthFixBase !== void 0 && p4.squashMsgPresent && p4.squashMsgContent === HEALTH_SQUASH_SENTINEL) {
|
|
389063
388606
|
return {
|
|
389064
|
-
state: "
|
|
388607
|
+
state: "squashing",
|
|
389065
388608
|
autoAdvance: true,
|
|
389066
|
-
edgeAction: { kind: "
|
|
388609
|
+
edgeAction: { kind: "removeHealthSentinel" },
|
|
389067
388610
|
context: buildContext(p4, counters)
|
|
389068
388611
|
};
|
|
389069
388612
|
}
|
|
389070
|
-
if (p4.
|
|
388613
|
+
if (isBoundary(head7) && p4.squashMsgPresent && !p4.codeDirty) {
|
|
389071
388614
|
return {
|
|
389072
|
-
state: "
|
|
388615
|
+
state: "squashing",
|
|
389073
388616
|
autoAdvance: true,
|
|
389074
|
-
edgeAction: { kind: "
|
|
388617
|
+
edgeAction: { kind: "removeStraySquashMsg" },
|
|
389075
388618
|
context: buildContext(p4, counters)
|
|
389076
388619
|
};
|
|
389077
388620
|
}
|
|
389078
|
-
|
|
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: "
|
|
388623
|
+
state: "new-feature",
|
|
389099
388624
|
autoAdvance: true,
|
|
389100
|
-
edgeAction:
|
|
389101
|
-
context: buildContext(p4, counters
|
|
388625
|
+
edgeAction: { kind: "seedNewFeature" },
|
|
388626
|
+
context: buildContext(p4, counters)
|
|
389102
388627
|
};
|
|
389103
388628
|
}
|
|
389104
|
-
return
|
|
389105
|
-
|
|
389106
|
-
|
|
389107
|
-
|
|
389108
|
-
|
|
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
|
|
389112
|
-
|
|
389113
|
-
|
|
389114
|
-
|
|
389115
|
-
|
|
389116
|
-
|
|
389117
|
-
|
|
389118
|
-
|
|
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
|
-
|
|
389122
|
-
|
|
389123
|
-
|
|
389124
|
-
|
|
389125
|
-
|
|
389126
|
-
|
|
389127
|
-
|
|
389128
|
-
|
|
389129
|
-
|
|
389130
|
-
|
|
389131
|
-
|
|
389132
|
-
|
|
389133
|
-
|
|
389134
|
-
|
|
389135
|
-
|
|
389136
|
-
|
|
389137
|
-
|
|
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
|
-
|
|
389143
|
-
|
|
389144
|
-
|
|
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
|
-
|
|
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
|
|
389161
|
-
|
|
389162
|
-
|
|
389163
|
-
|
|
389164
|
-
|
|
389165
|
-
|
|
389166
|
-
|
|
389167
|
-
|
|
389168
|
-
|
|
389169
|
-
|
|
389170
|
-
|
|
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
|
-
|
|
389174
|
-
|
|
389175
|
-
|
|
389176
|
-
|
|
389177
|
-
|
|
389178
|
-
|
|
389179
|
-
|
|
389180
|
-
|
|
389181
|
-
|
|
389182
|
-
|
|
389183
|
-
|
|
389184
|
-
|
|
389185
|
-
|
|
389186
|
-
|
|
389187
|
-
|
|
389188
|
-
|
|
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
|
-
|
|
389212
|
-
|
|
389213
|
-
|
|
389214
|
-
|
|
389215
|
-
|
|
389216
|
-
|
|
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
|
-
|
|
389220
|
-
|
|
389221
|
-
|
|
389222
|
-
|
|
389223
|
-
|
|
389224
|
-
|
|
389225
|
-
|
|
389226
|
-
|
|
389227
|
-
|
|
389228
|
-
|
|
389229
|
-
|
|
389230
|
-
|
|
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([
|