@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.
- package/README.md +63 -31
- package/dist/gtd.bundle.mjs +915 -845
- package/package.json +1 -1
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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/
|
|
388262
|
-
var
|
|
388263
|
-
var
|
|
388264
|
-
|
|
388265
|
-
|
|
388266
|
-
|
|
388267
|
-
|
|
388268
|
-
|
|
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
|
|
388355
|
-
|
|
388356
|
-
|
|
388357
|
-
|
|
388358
|
-
|
|
388359
|
-
|
|
388360
|
-
|
|
388361
|
-
|
|
388362
|
-
|
|
388363
|
-
|
|
388364
|
-
|
|
388365
|
-
|
|
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
|
|
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
|
|
388429
|
-
|
|
388430
|
-
|
|
388431
|
-
|
|
388432
|
-
|
|
388433
|
-
|
|
388434
|
-
|
|
388435
|
-
|
|
388436
|
-
|
|
388437
|
-
|
|
388438
|
-
|
|
388439
|
-
|
|
388440
|
-
|
|
388441
|
-
|
|
388442
|
-
|
|
388443
|
-
|
|
388444
|
-
|
|
388445
|
-
|
|
388446
|
-
|
|
388447
|
-
|
|
388448
|
-
|
|
388449
|
-
|
|
388450
|
-
|
|
388451
|
-
|
|
388452
|
-
|
|
388453
|
-
|
|
388454
|
-
|
|
388455
|
-
|
|
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
|
|
388458
|
-
|
|
388459
|
-
|
|
388460
|
-
|
|
388461
|
-
|
|
388462
|
-
|
|
388463
|
-
|
|
388464
|
-
|
|
388465
|
-
|
|
388466
|
-
|
|
388467
|
-
|
|
388468
|
-
|
|
388469
|
-
|
|
388470
|
-
|
|
388471
|
-
|
|
388472
|
-
|
|
388473
|
-
|
|
388474
|
-
|
|
388475
|
-
|
|
388476
|
-
|
|
388477
|
-
|
|
388478
|
-
|
|
388479
|
-
|
|
388480
|
-
|
|
388481
|
-
|
|
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
|
-
|
|
388486
|
-
|
|
388487
|
-
|
|
388488
|
-
|
|
388489
|
-
|
|
388490
|
-
|
|
388491
|
-
|
|
388492
|
-
|
|
388493
|
-
|
|
388494
|
-
|
|
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
|
-
|
|
388504
|
-
|
|
388505
|
-
|
|
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
|
-
return
|
|
388545
|
-
|
|
388546
|
-
|
|
388547
|
-
|
|
388548
|
-
|
|
388549
|
-
|
|
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
|
-
|
|
388565
|
-
|
|
388566
|
-
|
|
388567
|
-
|
|
388568
|
-
|
|
388569
|
-
|
|
388570
|
-
|
|
388571
|
-
|
|
388572
|
-
|
|
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 (
|
|
389171
|
-
|
|
389172
|
-
|
|
389173
|
-
|
|
389174
|
-
|
|
389175
|
-
|
|
389176
|
-
|
|
389177
|
-
|
|
389178
|
-
|
|
389179
|
-
|
|
389180
|
-
|
|
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
|
-
|
|
389189
|
-
|
|
389190
|
-
|
|
389191
|
-
|
|
389192
|
-
|
|
389193
|
-
|
|
389194
|
-
|
|
389195
|
-
|
|
389196
|
-
|
|
389197
|
-
|
|
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
|
-
|
|
389201
|
-
|
|
389202
|
-
|
|
389203
|
-
|
|
389204
|
-
|
|
389205
|
-
|
|
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
|
-
|
|
389209
|
-
|
|
389210
|
-
|
|
389211
|
-
|
|
389212
|
-
|
|
389213
|
-
|
|
389214
|
-
|
|
389215
|
-
|
|
389216
|
-
|
|
389217
|
-
|
|
389218
|
-
|
|
389219
|
-
|
|
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) {
|