@pmelab/gtd 27.2.0 → 27.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/chunks/{reserved-B2QaImnN.mjs → reserved-BPdVZkgv.mjs} +29 -4
- package/dist/chunks/{tail-BnhIxCYv.mjs → tail-iLAMoigl.mjs} +1 -1
- package/dist/testkit.mjs +12 -0
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +119 -6
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +146 -8
- package/package.json +1 -1
- package/src/testkit/EmittedScriptRecognizer.ts +21 -0
- package/src/testkit/EmittedScripts.test.ts +9 -0
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.test.ts +466 -0
- package/src/workflows/feature/refactor.ts +178 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/access.ts +2 -1
- package/src/workflows/shared/health.test.ts +67 -3
- package/src/workflows/shared/health.ts +5 -3
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/skills.test.ts +6 -0
- package/src/workflows/shared/skills.ts +5 -1
- package/src/workflows/shared/text.ts +35 -0
- package/src/workflows/shared/vars.ts +1 -0
|
@@ -591,6 +591,26 @@ ${fixFeedbackPrompt}
|
|
|
591
591
|
then legitimately report itself already satisfied later
|
|
592
592
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
593
593
|
`;
|
|
594
|
+
const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
|
|
595
|
+
- Change only these paths, nothing else — a test file only when it is
|
|
596
|
+
one of them:
|
|
597
|
+
${strays.map((path) => ` - \`${path}\``).join("\n")}
|
|
598
|
+
- Introduce no new flag read and add no second read of an existing one:
|
|
599
|
+
every match of \`${pattern}\` may move, never increase in number`;
|
|
600
|
+
const refactorBuildingPrompt = (pkg, strays, pattern, reference) => `${stateFileRules}
|
|
601
|
+
- The package to implement is \`${pkg}\`; never delete it
|
|
602
|
+
${refactorRules(strays, pattern)}
|
|
603
|
+
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
604
|
+
earlier attempt at this change. Read it for ideas; never apply,
|
|
605
|
+
cherry-pick or check it out — write a fresh refactor
|
|
606
|
+
`}- Leave everything uncommitted and finish your turn — do not commit
|
|
607
|
+
`;
|
|
608
|
+
const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
|
|
609
|
+
${fixFeedbackPrompt}
|
|
610
|
+
${refactorRules(strays, pattern)}
|
|
611
|
+
- Never do another package's work, even when it would green the suite
|
|
612
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
613
|
+
`;
|
|
594
614
|
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
595
615
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
596
616
|
is in history. Run \`gtd judge answer\` and pipe a
|
|
@@ -1161,9 +1181,9 @@ const guarded = (turn, guard) => async (...a) => {
|
|
|
1161
1181
|
await turn(...a);
|
|
1162
1182
|
guard?.();
|
|
1163
1183
|
};
|
|
1164
|
-
const guardedSteps = (guard) => ({
|
|
1184
|
+
const guardedSteps = (guard, describeGuard) => ({
|
|
1165
1185
|
limit: escalationSteps.limit,
|
|
1166
|
-
describe: guarded(escalationSteps.describe, guard),
|
|
1186
|
+
describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
|
|
1167
1187
|
stop: guarded(escalationSteps.stop, guard),
|
|
1168
1188
|
exhausted: guarded(escalationSteps.exhausted, guard)
|
|
1169
1189
|
});
|
|
@@ -1215,7 +1235,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
1215
1235
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
1216
1236
|
sameFailure,
|
|
1217
1237
|
escalations: options.escalations ?? { rounds: 0 },
|
|
1218
|
-
escalation: guardedSteps(options.guard)
|
|
1238
|
+
escalation: guardedSteps(options.guard, options.describeGuard)
|
|
1219
1239
|
});
|
|
1220
1240
|
/**
|
|
1221
1241
|
* Hold the process at `<name>.blocked` until the suite is green. `guard` runs
|
|
@@ -1236,6 +1256,7 @@ const defaults = {
|
|
|
1236
1256
|
judgeIdenticalMinP: "0.7",
|
|
1237
1257
|
reviewNoteActionable: "0.7",
|
|
1238
1258
|
reviewSeverityMinP: "0.7",
|
|
1259
|
+
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
1239
1260
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
|
|
1240
1261
|
};
|
|
1241
1262
|
/** Environment settings. */
|
|
@@ -1253,6 +1274,7 @@ const ShippedOptions = Struct({
|
|
|
1253
1274
|
judgeIdenticalMinP: setting,
|
|
1254
1275
|
reviewNoteActionable: setting,
|
|
1255
1276
|
reviewSeverityMinP: setting,
|
|
1277
|
+
refactorFlagPattern: setting,
|
|
1256
1278
|
qualityReviews: setting,
|
|
1257
1279
|
testCommand: setting,
|
|
1258
1280
|
plannerModel: setting,
|
|
@@ -1314,6 +1336,9 @@ const skills = (vars) => ({
|
|
|
1314
1336
|
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
1315
1337
|
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
1316
1338
|
notes: ["incremental-implementation", "code-simplification"],
|
|
1339
|
+
refactor: ["ponytail"],
|
|
1340
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
1341
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
1317
1342
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
|
|
1318
1343
|
});
|
|
1319
1344
|
//#endregion
|
|
@@ -1337,4 +1362,4 @@ const steering = {
|
|
|
1337
1362
|
[REVIEW]: "review"
|
|
1338
1363
|
};
|
|
1339
1364
|
//#endregion
|
|
1340
|
-
export {
|
|
1365
|
+
export { reviewGateBlockedMessage as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixRisks as C, reviewing as D, reviewQuality as E, buildReviewCollectingPrompt as F, handOffAwaitReviewMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, packagesItemFixSuitePrompt as J, idleMessage as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewEditsCapture as Q, builderSystem as R, fixQuality as S, reviewMissing as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, refactorFixPrompt as X, refactorBuildingPrompt as Y, reviewEditNotesCapture as Z, REQUIREMENTS as _, flagShipped as a, coder as b, envDefaults as c, gate as d, reviewNotesCapture as et, guarded as f, QUALITY as g, FEEDBACK as h, skills as i, unwindFailure as it, architectureDecomposePrompt as j, architectSystem as k, baseline as l, ARCHITECTURE as m, summary as n, startGateBlockedMessage as nt, shipped as o, healthy as p, packagesItemBuildingPrompt as q, access as r, unwindFailedMessage as rt, defaults as s, steering as t, reviewerSystem as tt, escalation as u, REVIEW as v, planner as w, fixCheck as x, awaitReview as y, deferNotesScript as z };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-
|
|
3
|
+
import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-BPdVZkgv.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
package/dist/testkit.mjs
CHANGED
|
@@ -12961,6 +12961,17 @@ const recognizeDeferNotes = (repo, block) => {
|
|
|
12961
12961
|
repo.writeFile(".gtd/TODO.md", `${old}${sep}${lines.join("\n")}\n`);
|
|
12962
12962
|
return { kind: "applied" };
|
|
12963
12963
|
};
|
|
12964
|
+
/** `mkdir -p <dir>` then `printf '%s\n' <quoted lines> > <path>`: a flow's one-file write. Re-rendered from what was extracted, so any other shape stays unrecognized. */
|
|
12965
|
+
const recognizeWriteLines = (repo, block) => {
|
|
12966
|
+
const [mkdir, printf, ...rest] = block.split("\n");
|
|
12967
|
+
if (rest.length > 0 || !mkdir?.startsWith("mkdir -p ") || !printf?.startsWith("printf '%s\\n' ")) return;
|
|
12968
|
+
const tokens = extractQuotedTokens(printf.slice(14));
|
|
12969
|
+
const path = tokens.pop();
|
|
12970
|
+
if (path === void 0 || !path.includes("/")) return void 0;
|
|
12971
|
+
if (`mkdir -p ${path.slice(0, path.lastIndexOf("/"))}\nprintf '%s\\n' ${tokens.map(shellQuote).join(" ")} > ${shellQuote(path)}` !== block) return void 0;
|
|
12972
|
+
repo.writeFile(path, `${tokens.join("\n")}\n`);
|
|
12973
|
+
return { kind: "applied" };
|
|
12974
|
+
};
|
|
12964
12975
|
/** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
|
|
12965
12976
|
const recognizersFor = (repo, commands) => [
|
|
12966
12977
|
(block) => recognizeSetFlags(block),
|
|
@@ -12970,6 +12981,7 @@ const recognizersFor = (repo, commands) => [
|
|
|
12970
12981
|
(block) => recognizeBinaryGuard(commands, block),
|
|
12971
12982
|
(block) => recognizeGitBuilders(repo, block),
|
|
12972
12983
|
(block) => recognizeDeferNotes(repo, block),
|
|
12984
|
+
(block) => recognizeWriteLines(repo, block),
|
|
12973
12985
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
12974
12986
|
(block) => recognizePresentationSubshell(repo, commands, block),
|
|
12975
12987
|
(block) => recognizeModeContradictionSkipNotice(block),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-BPdVZkgv.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-iLAMoigl.mjs";
|
|
4
4
|
import { i as requireFlag, r as registryEntry, t as flagReferences } from "../chunks/registry-CP3YuV9j.mjs";
|
|
5
5
|
//#region src/workflows/cleanup/cleanup.ts
|
|
6
6
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
|
|
1
|
+
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
|
|
2
2
|
import { review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-
|
|
3
|
+
import { A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as packagesItemFixSuitePrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as reviewEditsCapture, R as builderSystem, T as reviewMissing, V as designSystem, X as refactorFixPrompt, Y as refactorBuildingPrompt, Z as reviewEditNotesCapture, _ as REQUIREMENTS, b as coder, c as envDefaults, d as gate, et as reviewNotesCapture, h as FEEDBACK, i as skills, it as unwindFailure, j as architectureDecomposePrompt, k as architectSystem, m as ARCHITECTURE, n as summary, nt as startGateBlockedMessage, o as shipped, p as healthy, q as packagesItemBuildingPrompt, r as access, rt as unwindFailedMessage, s as defaults, t as steering, tt as reviewerSystem, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-BPdVZkgv.mjs";
|
|
4
|
+
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-iLAMoigl.mjs";
|
|
5
5
|
//#region src/workflows/feature/steps.ts
|
|
6
6
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
7
7
|
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
|
@@ -282,7 +282,7 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
282
282
|
return [n.id, severity.tier === "could" ? "defer" : "edit"];
|
|
283
283
|
}));
|
|
284
284
|
};
|
|
285
|
-
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
285
|
+
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
286
286
|
/**
|
|
287
287
|
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
288
288
|
* reviews and signs off or comments, and each note is judged: edits go to
|
|
@@ -295,7 +295,7 @@ const review = (base, escalations = { rounds: 0 }) => {
|
|
|
295
295
|
return review$1(base, {
|
|
296
296
|
file: REVIEW,
|
|
297
297
|
feedback: REQUIREMENTS,
|
|
298
|
-
isCode,
|
|
298
|
+
isCode: isCode$1,
|
|
299
299
|
reviewing: (b, carry) => {
|
|
300
300
|
deferred = [];
|
|
301
301
|
return reviewing(b, carry);
|
|
@@ -405,7 +405,120 @@ const ordinaryStart = async () => {
|
|
|
405
405
|
};
|
|
406
406
|
const feature = shipped(ordinaryStart);
|
|
407
407
|
//#endregion
|
|
408
|
+
//#region src/workflows/feature/refactor.ts
|
|
409
|
+
const REFACTOR_PACKAGE = ".gtd/REFACTOR.md";
|
|
410
|
+
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
411
|
+
const requireWithin = (allowed) => {
|
|
412
|
+
const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
|
|
413
|
+
if (outside.length === 0) return;
|
|
414
|
+
refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
|
|
415
|
+
};
|
|
416
|
+
const flagExpression = (pattern) => {
|
|
417
|
+
if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
|
|
418
|
+
let expression;
|
|
419
|
+
try {
|
|
420
|
+
expression = new RegExp(pattern, "g");
|
|
421
|
+
} catch (error) {
|
|
422
|
+
throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`);
|
|
423
|
+
}
|
|
424
|
+
if (expression.test("")) throw new Error("gtd: refactorFlagPattern must not match the empty string");
|
|
425
|
+
return expression;
|
|
426
|
+
};
|
|
427
|
+
const countFlags = (text, expression, into) => {
|
|
428
|
+
let zeroLength = false;
|
|
429
|
+
for (const [name] of text.matchAll(expression)) if (name === "") zeroLength = true;
|
|
430
|
+
else into.set(name, (into.get(name) ?? 0) + 1);
|
|
431
|
+
return zeroLength;
|
|
432
|
+
};
|
|
433
|
+
const listPaths = (paths) => paths.map((path) => ` - ${path}`).join("\n");
|
|
434
|
+
const requireNoNewFlags = (pattern, since) => {
|
|
435
|
+
const expression = flagExpression(pattern);
|
|
436
|
+
const before = /* @__PURE__ */ new Map();
|
|
437
|
+
const after = /* @__PURE__ */ new Map();
|
|
438
|
+
const unreadable = [];
|
|
439
|
+
const zeroLength = [];
|
|
440
|
+
for (const { path, status, before: was, after: now } of changesSince(since)) {
|
|
441
|
+
if (!isCode(path)) continue;
|
|
442
|
+
const oldSide = status === "added" ? "" : was;
|
|
443
|
+
const newSide = status === "deleted" ? "" : now;
|
|
444
|
+
if (oldSide === void 0 || newSide === void 0) {
|
|
445
|
+
unreadable.push(path);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
countFlags(oldSide, expression, before);
|
|
449
|
+
if (countFlags(newSide, expression, after)) zeroLength.push(path);
|
|
450
|
+
}
|
|
451
|
+
if (unreadable.length > 0) refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`);
|
|
452
|
+
if (zeroLength.length > 0) refuse(`gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
453
|
+
const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0));
|
|
454
|
+
if (raised.length === 0) return;
|
|
455
|
+
refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
|
|
456
|
+
};
|
|
457
|
+
const packageLines = (strays, pattern, reference) => [
|
|
458
|
+
"# Behaviour-neutral refactor",
|
|
459
|
+
"",
|
|
460
|
+
"## Scope (read-only)",
|
|
461
|
+
"",
|
|
462
|
+
...strays.map((path) => `- \`${path}\``),
|
|
463
|
+
"",
|
|
464
|
+
"This list is fixed when the package is derived: editing it here widens or narrows nothing.",
|
|
465
|
+
"",
|
|
466
|
+
"## Rules",
|
|
467
|
+
"",
|
|
468
|
+
"- The suite's outcome must not change",
|
|
469
|
+
"- Change only the paths above; a test file only when it is one of them",
|
|
470
|
+
`- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
|
|
471
|
+
"",
|
|
472
|
+
"## Reference",
|
|
473
|
+
"",
|
|
474
|
+
reference === void 0 ? "None." : `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
|
|
475
|
+
""
|
|
476
|
+
];
|
|
477
|
+
const deriveScript = (lines) => `mkdir -p .gtd
|
|
478
|
+
printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
|
|
479
|
+
`;
|
|
480
|
+
const refactorFirst = async (shape, options = {}) => {
|
|
481
|
+
if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return false;
|
|
482
|
+
const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
|
|
483
|
+
const expression = flagExpression(pattern);
|
|
484
|
+
const { strays } = shape;
|
|
485
|
+
const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
|
|
486
|
+
if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
487
|
+
const reference = options.reference?.trim() ? options.reference : void 0;
|
|
488
|
+
return scope({
|
|
489
|
+
name: "refactor",
|
|
490
|
+
access: { write: [...strays, ".gtd/**"] }
|
|
491
|
+
}, async () => {
|
|
492
|
+
await run("deriving", deriveScript(packageLines(strays, pattern, reference)), {
|
|
493
|
+
label: "Deriving the refactor package",
|
|
494
|
+
file: REFACTOR_PACKAGE
|
|
495
|
+
});
|
|
496
|
+
const from = head();
|
|
497
|
+
const guard = () => {
|
|
498
|
+
requireWithin(strays);
|
|
499
|
+
requireNoNewFlags(pattern, from);
|
|
500
|
+
};
|
|
501
|
+
await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference), {
|
|
502
|
+
label: "Refactoring",
|
|
503
|
+
model: coder(),
|
|
504
|
+
system: builderSystem()
|
|
505
|
+
});
|
|
506
|
+
guard();
|
|
507
|
+
await healthy(async () => {
|
|
508
|
+
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
509
|
+
label: "Fixing the check",
|
|
510
|
+
file: FEEDBACK,
|
|
511
|
+
model: coder(),
|
|
512
|
+
system: builderSystem()
|
|
513
|
+
});
|
|
514
|
+
guard();
|
|
515
|
+
}, { describeGuard: guard });
|
|
516
|
+
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
517
|
+
return true;
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
//#endregion
|
|
408
521
|
//#region src/workflows/feature/index.ts
|
|
409
522
|
var feature_default = feature;
|
|
410
523
|
//#endregion
|
|
411
|
-
export { access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, requireDeclaredTests, skills, steering, summary, triage, unwind };
|
|
524
|
+
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, refactorFirst, requireDeclaredTests, requireNoNewFlags, requireWithin, skills, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-BPdVZkgv.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-iLAMoigl.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { $ as reviewGateBlockedMessage, c as envDefaults, d as gate, i as skills, n as summary, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-BPdVZkgv.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-iLAMoigl.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, head, read, refuse, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-
|
|
2
|
+
import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-BPdVZkgv.mjs";
|
|
3
3
|
import { i as requireFlag, n as flipProblems } from "../chunks/registry-CP3YuV9j.mjs";
|
|
4
4
|
//#region src/workflows/rollout/rollout.ts
|
|
5
5
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
package/dist/workflows.mjs
CHANGED
|
@@ -250,9 +250,9 @@ const check = async (name, command, options) => {
|
|
|
250
250
|
}), { label });
|
|
251
251
|
return !wrote(report);
|
|
252
252
|
};
|
|
253
|
-
const isCode$
|
|
253
|
+
const isCode$2 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
254
254
|
/** The last step's changes outside `.gtd/`. */
|
|
255
|
-
const codeChanges = () => changes().filter((c) => isCode$
|
|
255
|
+
const codeChanges = () => changes().filter((c) => isCode$2(c.path));
|
|
256
256
|
/** Refuse a turn whose only change deletes `file` — unless it says it found nothing to do. */
|
|
257
257
|
const requireProgress = (file) => {
|
|
258
258
|
const change = changes().get(file);
|
|
@@ -17235,6 +17235,26 @@ ${fixFeedbackPrompt}
|
|
|
17235
17235
|
then legitimately report itself already satisfied later
|
|
17236
17236
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
17237
17237
|
`;
|
|
17238
|
+
const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
|
|
17239
|
+
- Change only these paths, nothing else — a test file only when it is
|
|
17240
|
+
one of them:
|
|
17241
|
+
${strays.map((path) => ` - \`${path}\``).join("\n")}
|
|
17242
|
+
- Introduce no new flag read and add no second read of an existing one:
|
|
17243
|
+
every match of \`${pattern}\` may move, never increase in number`;
|
|
17244
|
+
const refactorBuildingPrompt = (pkg, strays, pattern, reference) => `${stateFileRules}
|
|
17245
|
+
- The package to implement is \`${pkg}\`; never delete it
|
|
17246
|
+
${refactorRules(strays, pattern)}
|
|
17247
|
+
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
17248
|
+
earlier attempt at this change. Read it for ideas; never apply,
|
|
17249
|
+
cherry-pick or check it out — write a fresh refactor
|
|
17250
|
+
`}- Leave everything uncommitted and finish your turn — do not commit
|
|
17251
|
+
`;
|
|
17252
|
+
const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
|
|
17253
|
+
${fixFeedbackPrompt}
|
|
17254
|
+
${refactorRules(strays, pattern)}
|
|
17255
|
+
- Never do another package's work, even when it would green the suite
|
|
17256
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
17257
|
+
`;
|
|
17238
17258
|
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
17239
17259
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
17240
17260
|
is in history. Run \`gtd judge answer\` and pipe a
|
|
@@ -17809,9 +17829,9 @@ const guarded = (turn, guard) => async (...a) => {
|
|
|
17809
17829
|
await turn(...a);
|
|
17810
17830
|
guard?.();
|
|
17811
17831
|
};
|
|
17812
|
-
const guardedSteps = (guard) => ({
|
|
17832
|
+
const guardedSteps = (guard, describeGuard) => ({
|
|
17813
17833
|
limit: escalationSteps.limit,
|
|
17814
|
-
describe: guarded(escalationSteps.describe, guard),
|
|
17834
|
+
describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
|
|
17815
17835
|
stop: guarded(escalationSteps.stop, guard),
|
|
17816
17836
|
exhausted: guarded(escalationSteps.exhausted, guard)
|
|
17817
17837
|
});
|
|
@@ -17863,7 +17883,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
17863
17883
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
17864
17884
|
sameFailure,
|
|
17865
17885
|
escalations: options.escalations ?? { rounds: 0 },
|
|
17866
|
-
escalation: guardedSteps(options.guard)
|
|
17886
|
+
escalation: guardedSteps(options.guard, options.describeGuard)
|
|
17867
17887
|
});
|
|
17868
17888
|
/**
|
|
17869
17889
|
* Hold the process at `<name>.blocked` until the suite is green. `guard` runs
|
|
@@ -37198,6 +37218,7 @@ const defaults = {
|
|
|
37198
37218
|
judgeIdenticalMinP: "0.7",
|
|
37199
37219
|
reviewNoteActionable: "0.7",
|
|
37200
37220
|
reviewSeverityMinP: "0.7",
|
|
37221
|
+
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
37201
37222
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
|
|
37202
37223
|
};
|
|
37203
37224
|
/** Environment settings. */
|
|
@@ -37215,6 +37236,7 @@ const ShippedOptions = Struct({
|
|
|
37215
37236
|
judgeIdenticalMinP: setting,
|
|
37216
37237
|
reviewNoteActionable: setting,
|
|
37217
37238
|
reviewSeverityMinP: setting,
|
|
37239
|
+
refactorFlagPattern: setting,
|
|
37218
37240
|
qualityReviews: setting,
|
|
37219
37241
|
testCommand: setting,
|
|
37220
37242
|
plannerModel: setting,
|
|
@@ -37614,7 +37636,7 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
37614
37636
|
return [n.id, severity.tier === "could" ? "defer" : "edit"];
|
|
37615
37637
|
}));
|
|
37616
37638
|
};
|
|
37617
|
-
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
37639
|
+
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
37618
37640
|
/**
|
|
37619
37641
|
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
37620
37642
|
* reviews and signs off or comments, and each note is judged: edits go to
|
|
@@ -37627,7 +37649,7 @@ const review$1 = (base, escalations = { rounds: 0 }) => {
|
|
|
37627
37649
|
return review$2(base, {
|
|
37628
37650
|
file: REVIEW,
|
|
37629
37651
|
feedback: REQUIREMENTS,
|
|
37630
|
-
isCode,
|
|
37652
|
+
isCode: isCode$1,
|
|
37631
37653
|
reviewing: (b, carry) => {
|
|
37632
37654
|
deferred = [];
|
|
37633
37655
|
return reviewing(b, carry);
|
|
@@ -37756,6 +37778,9 @@ const skills = (vars) => ({
|
|
|
37756
37778
|
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
37757
37779
|
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
37758
37780
|
notes: ["incremental-implementation", "code-simplification"],
|
|
37781
|
+
refactor: ["ponytail"],
|
|
37782
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
37783
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
37759
37784
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
|
|
37760
37785
|
});
|
|
37761
37786
|
//#endregion
|
|
@@ -37779,6 +37804,119 @@ const steering = {
|
|
|
37779
37804
|
[REVIEW]: "review"
|
|
37780
37805
|
};
|
|
37781
37806
|
//#endregion
|
|
37807
|
+
//#region src/workflows/feature/refactor.ts
|
|
37808
|
+
const REFACTOR_PACKAGE = ".gtd/REFACTOR.md";
|
|
37809
|
+
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
37810
|
+
const requireWithin = (allowed) => {
|
|
37811
|
+
const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
|
|
37812
|
+
if (outside.length === 0) return;
|
|
37813
|
+
refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
|
|
37814
|
+
};
|
|
37815
|
+
const flagExpression = (pattern) => {
|
|
37816
|
+
if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
|
|
37817
|
+
let expression;
|
|
37818
|
+
try {
|
|
37819
|
+
expression = new RegExp(pattern, "g");
|
|
37820
|
+
} catch (error) {
|
|
37821
|
+
throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`);
|
|
37822
|
+
}
|
|
37823
|
+
if (expression.test("")) throw new Error("gtd: refactorFlagPattern must not match the empty string");
|
|
37824
|
+
return expression;
|
|
37825
|
+
};
|
|
37826
|
+
const countFlags = (text, expression, into) => {
|
|
37827
|
+
let zeroLength = false;
|
|
37828
|
+
for (const [name] of text.matchAll(expression)) if (name === "") zeroLength = true;
|
|
37829
|
+
else into.set(name, (into.get(name) ?? 0) + 1);
|
|
37830
|
+
return zeroLength;
|
|
37831
|
+
};
|
|
37832
|
+
const listPaths = (paths) => paths.map((path) => ` - ${path}`).join("\n");
|
|
37833
|
+
const requireNoNewFlags = (pattern, since) => {
|
|
37834
|
+
const expression = flagExpression(pattern);
|
|
37835
|
+
const before = /* @__PURE__ */ new Map();
|
|
37836
|
+
const after = /* @__PURE__ */ new Map();
|
|
37837
|
+
const unreadable = [];
|
|
37838
|
+
const zeroLength = [];
|
|
37839
|
+
for (const { path, status, before: was, after: now } of changesSince(since)) {
|
|
37840
|
+
if (!isCode(path)) continue;
|
|
37841
|
+
const oldSide = status === "added" ? "" : was;
|
|
37842
|
+
const newSide = status === "deleted" ? "" : now;
|
|
37843
|
+
if (oldSide === void 0 || newSide === void 0) {
|
|
37844
|
+
unreadable.push(path);
|
|
37845
|
+
continue;
|
|
37846
|
+
}
|
|
37847
|
+
countFlags(oldSide, expression, before);
|
|
37848
|
+
if (countFlags(newSide, expression, after)) zeroLength.push(path);
|
|
37849
|
+
}
|
|
37850
|
+
if (unreadable.length > 0) refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`);
|
|
37851
|
+
if (zeroLength.length > 0) refuse(`gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
37852
|
+
const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0));
|
|
37853
|
+
if (raised.length === 0) return;
|
|
37854
|
+
refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
|
|
37855
|
+
};
|
|
37856
|
+
const packageLines = (strays, pattern, reference) => [
|
|
37857
|
+
"# Behaviour-neutral refactor",
|
|
37858
|
+
"",
|
|
37859
|
+
"## Scope (read-only)",
|
|
37860
|
+
"",
|
|
37861
|
+
...strays.map((path) => `- \`${path}\``),
|
|
37862
|
+
"",
|
|
37863
|
+
"This list is fixed when the package is derived: editing it here widens or narrows nothing.",
|
|
37864
|
+
"",
|
|
37865
|
+
"## Rules",
|
|
37866
|
+
"",
|
|
37867
|
+
"- The suite's outcome must not change",
|
|
37868
|
+
"- Change only the paths above; a test file only when it is one of them",
|
|
37869
|
+
`- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
|
|
37870
|
+
"",
|
|
37871
|
+
"## Reference",
|
|
37872
|
+
"",
|
|
37873
|
+
reference === void 0 ? "None." : `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
|
|
37874
|
+
""
|
|
37875
|
+
];
|
|
37876
|
+
const deriveScript = (lines) => `mkdir -p .gtd
|
|
37877
|
+
printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
|
|
37878
|
+
`;
|
|
37879
|
+
const refactorFirst = async (shape, options = {}) => {
|
|
37880
|
+
if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return false;
|
|
37881
|
+
const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
|
|
37882
|
+
const expression = flagExpression(pattern);
|
|
37883
|
+
const { strays } = shape;
|
|
37884
|
+
const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
|
|
37885
|
+
if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
37886
|
+
const reference = options.reference?.trim() ? options.reference : void 0;
|
|
37887
|
+
return scope({
|
|
37888
|
+
name: "refactor",
|
|
37889
|
+
access: { write: [...strays, ".gtd/**"] }
|
|
37890
|
+
}, async () => {
|
|
37891
|
+
await run("deriving", deriveScript(packageLines(strays, pattern, reference)), {
|
|
37892
|
+
label: "Deriving the refactor package",
|
|
37893
|
+
file: REFACTOR_PACKAGE
|
|
37894
|
+
});
|
|
37895
|
+
const from = head$1();
|
|
37896
|
+
const guard = () => {
|
|
37897
|
+
requireWithin(strays);
|
|
37898
|
+
requireNoNewFlags(pattern, from);
|
|
37899
|
+
};
|
|
37900
|
+
await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference), {
|
|
37901
|
+
label: "Refactoring",
|
|
37902
|
+
model: coder(),
|
|
37903
|
+
system: builderSystem()
|
|
37904
|
+
});
|
|
37905
|
+
guard();
|
|
37906
|
+
await healthy(async () => {
|
|
37907
|
+
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
37908
|
+
label: "Fixing the check",
|
|
37909
|
+
file: FEEDBACK,
|
|
37910
|
+
model: coder(),
|
|
37911
|
+
system: builderSystem()
|
|
37912
|
+
});
|
|
37913
|
+
guard();
|
|
37914
|
+
}, { describeGuard: guard });
|
|
37915
|
+
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
37916
|
+
return true;
|
|
37917
|
+
});
|
|
37918
|
+
};
|
|
37919
|
+
//#endregion
|
|
37782
37920
|
//#region src/workflows/review/review.ts
|
|
37783
37921
|
/**
|
|
37784
37922
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -37996,4 +38134,4 @@ var workflows_default = feature;
|
|
|
37996
38134
|
*/
|
|
37997
38135
|
const doors = {};
|
|
37998
38136
|
//#endregion
|
|
37999
|
-
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, requireDeclaredTests, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, rollout, rolloutFlow, skills, steering, summary, tail, threads, triage, unwind };
|
|
38137
|
+
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, refactorFirst, requireDeclaredTests, requireNoNewFlags, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, rollout, rolloutFlow, skills, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
|
@@ -667,6 +667,26 @@ const recognizeDeferNotes = (repo: InMemRepo, block: string): BlockOutcome | und
|
|
|
667
667
|
return { kind: "applied" }
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
/** `mkdir -p <dir>` then `printf '%s\n' <quoted lines> > <path>`: a flow's one-file write. Re-rendered from what was extracted, so any other shape stays unrecognized. */
|
|
671
|
+
const recognizeWriteLines = (repo: InMemRepo, block: string): BlockOutcome | undefined => {
|
|
672
|
+
const [mkdir, printf, ...rest] = block.split("\n")
|
|
673
|
+
if (
|
|
674
|
+
rest.length > 0 ||
|
|
675
|
+
!mkdir?.startsWith("mkdir -p ") ||
|
|
676
|
+
!printf?.startsWith("printf '%s\\n' ")
|
|
677
|
+
) {
|
|
678
|
+
return undefined
|
|
679
|
+
}
|
|
680
|
+
const tokens = extractQuotedTokens(printf.slice("printf '%s\\n' ".length))
|
|
681
|
+
const path = tokens.pop()
|
|
682
|
+
if (path === undefined || !path.includes("/")) return undefined
|
|
683
|
+
const dir = path.slice(0, path.lastIndexOf("/"))
|
|
684
|
+
const rendered = `mkdir -p ${dir}\nprintf '%s\\n' ${tokens.map(shellQuote).join(" ")} > ${shellQuote(path)}`
|
|
685
|
+
if (rendered !== block) return undefined
|
|
686
|
+
repo.writeFile(path, `${tokens.join("\n")}\n`)
|
|
687
|
+
return { kind: "applied" }
|
|
688
|
+
}
|
|
689
|
+
|
|
670
690
|
/** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
|
|
671
691
|
const recognizersFor = (
|
|
672
692
|
repo: InMemRepo,
|
|
@@ -679,6 +699,7 @@ const recognizersFor = (
|
|
|
679
699
|
(block) => recognizeBinaryGuard(commands, block),
|
|
680
700
|
(block) => recognizeGitBuilders(repo, block),
|
|
681
701
|
(block) => recognizeDeferNotes(repo, block),
|
|
702
|
+
(block) => recognizeWriteLines(repo, block),
|
|
682
703
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
683
704
|
(block) => recognizePresentationSubshell(repo, commands, block),
|
|
684
705
|
(block) => recognizeModeContradictionSkipNotice(block),
|
|
@@ -275,6 +275,15 @@ for (const makeTier of gitTiers) {
|
|
|
275
275
|
})
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
+
it("a mkdir + printf write lands one line per token, whatever the token holds", () => {
|
|
279
|
+
const lines = String.raw`mkdir -p .gtd/packages
|
|
280
|
+
printf '%s\n' 'a' '' 'it'\''s' '100%' '-x' 'a\b' '.gtd/packages/00-x.md' > '.gtd/packages/00-x.md'`
|
|
281
|
+
expect(run(lines).ok).toBe(true)
|
|
282
|
+
expect(read(".gtd/packages/00-x.md")).toBe(
|
|
283
|
+
"a\n\nit's\n100%\n-x\na\\b\n.gtd/packages/00-x.md\n",
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
|
|
278
287
|
describe("commitAll", () => {
|
|
279
288
|
it("a staging hook's edit lands in the commit", () => {
|
|
280
289
|
t.seed.stagingHook("readme.txt", "hooked")
|
|
@@ -3,6 +3,7 @@ import { feature } from "./feature.js"
|
|
|
3
3
|
export * from "./steps.js"
|
|
4
4
|
export * from "./planning.js"
|
|
5
5
|
export * from "./packages.js"
|
|
6
|
+
export * from "./refactor.js"
|
|
6
7
|
export { buildTail, type ReviewOutcome } from "./review.js"
|
|
7
8
|
export * from "./feature.js"
|
|
8
9
|
export * from "../shared/reserved.js"
|