@pmelab/gtd 60.1.0 → 62.0.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/README.md +16 -0
- package/dist/chunks/{testkit-BhTey1Do.mjs → testkit-XQeYCNMn.mjs} +1670 -1470
- package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
- package/dist/gtd.bundle.mjs +991 -627
- package/dist/gtd.core.mjs +117 -117
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.ts +6 -1
- package/src/cli/History.test.ts +10 -1
- package/src/cli/Launch.test.ts +52 -0
- package/src/cli/Launch.ts +143 -0
- package/src/cli/Metrics.test.ts +10 -1
- package/src/cli/Program.test.ts +30 -20
- package/src/cli/Program.ts +4 -2
- package/src/cli/Strays.ts +3 -2
- package/src/cli/index.ts +1 -0
- package/src/edge/Edge.test.ts +79 -26
- package/src/edge/Edge.ts +178 -99
- package/src/emit/GitScript.ts +21 -3
- package/src/emit/index.ts +3 -0
- package/src/gherkin/messages.test.ts +1 -0
- package/src/gherkin/module-config.feature +1 -0
- package/src/gherkin/more-steps.feature +1 -0
- package/src/gherkin/testkit.feature +2 -1
- package/src/install/Install.ts +9 -2
- package/src/platform/PinFile.ts +61 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Format.test.ts +35 -8
- package/src/replay/Format.ts +7 -4
- package/src/replay/Replay.test.ts +54 -0
- package/src/replay/Replay.ts +61 -17
- package/src/replay/Trailers.test.ts +11 -48
- package/src/replay/Trailers.ts +36 -42
- package/src/replay/Tree.ts +11 -0
- package/src/replay/index.ts +3 -0
- package/src/step/LandStep.ts +1 -0
- package/src/step/RepoSnapshot.ts +2 -0
- package/src/step/planStart.test.ts +2 -3
- package/src/step/planStart.ts +1 -5
- package/src/step/planStep.test.ts +4 -4
- package/src/step/planStep.ts +10 -8
- package/src/testkit/TestRepo.test.ts +4 -1
- package/src/testkit/TestRepo.ts +3 -1
- package/src/twins/EmittedScriptRecognizer.ts +32 -1
- package/src/twins/GitTiers.ts +23 -0
- package/src/twins/InMemRepo.ts +14 -0
- package/src/twins/PinBeats.ts +33 -0
- package/src/twins/PinFile.test.ts +107 -0
- package/src/twins/PinFile.ts +12 -0
- package/src/twins/index.ts +2 -0
- package/src/wire/BeatDocument.ts +26 -5
- package/src/wire/Demand.ts +16 -6
- package/src/wire/constants.ts +3 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/fingerprint.ts +2 -2
- package/src/workflow/index.ts +2 -2
- package/src/workflow/load.ts +14 -1
- package/src/workflow/pin.ts +15 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as moveScript, D as revertScript, E as restoreScript, H as installContext, K as markedOptions, O as showScript, S as checkScript, T as removeScript, Y as protocolMismatch, i as configure, k as writeScript$1, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, x as applyScript } from "./flows-DENt0uWV.mjs";
|
|
2
|
-
import { O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson,
|
|
2
|
+
import { M as renderFormat, O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, y as demandOf } from "./wire-CPNSm_9N.mjs";
|
|
3
3
|
import { M as headingSectionBodies, N as formatFinding, a as sections, c as openThreads, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-DF_BMadH.mjs";
|
|
4
4
|
import { createRequire, isBuiltin } from "node:module";
|
|
5
5
|
import * as Crypto from "node:crypto";
|
|
@@ -22274,7 +22274,13 @@ The \`kind\` field (\`--json=kind\`) selects what to do:
|
|
|
22274
22274
|
Land it immediately, no display needed.
|
|
22275
22275
|
- \`script\` — run \`--json=content\` with \`sh -c\`, appending its output to
|
|
22276
22276
|
\`--json=log\`, and ignore its exit code — the outcome lives in the tree,
|
|
22277
|
-
not in the exit code.
|
|
22277
|
+
not in the exit code. Two script beats carry a \`pin\` mark: the **pin
|
|
22278
|
+
beat** right after a process starts (\`pin: "add"\`, its script writes
|
|
22279
|
+
\`gtd bundle\`'s output to \`.gtd/process.gtdpin\`) and the **unpin beat**
|
|
22280
|
+
once the flow has ended (\`pin: "remove"\`, its script deletes that file).
|
|
22281
|
+
Both run unattended, like any script beat. A driver may write or delete the
|
|
22282
|
+
file its own way instead of running the script; either way it then lands,
|
|
22283
|
+
and the unpin's landing is the commit that closes the process.
|
|
22278
22284
|
- \`prompt\` — send plain \`gtd next\`'s own output to your agent CLI over
|
|
22279
22285
|
stdin (it's the bare content at this kind, so no separate \`--json=content\`
|
|
22280
22286
|
read is needed), using the accompanying \`session.id\`/\`session.resume\`/
|
|
@@ -22317,7 +22323,8 @@ skills, \`skills\` (an array of the skill names, already trimmed and split —
|
|
|
22317
22323
|
\`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
|
|
22318
22324
|
document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
|
|
22319
22325
|
\`cost\`, \`costByModel\`. Always \`approvals\` (an array of
|
|
22320
|
-
\`{step, name, email, commit}\`, \`[]\` when none).
|
|
22326
|
+
\`{step, name, email, commit}\`, \`[]\` when none). Only at a pin or unpin
|
|
22327
|
+
beat: \`pin\` (\`"add"\` or \`"remove"\`).
|
|
22321
22328
|
|
|
22322
22329
|
\`gtd land\` fields, in fixed order: \`schema\`, \`script\` (the POSIX sh to run), \`settled\`
|
|
22323
22330
|
(true for a no-op at a \`script\` rest — stop immediately, nothing more to
|
|
@@ -22792,11 +22799,20 @@ const diffTrees$1 = (before, after) => {
|
|
|
22792
22799
|
};
|
|
22793
22800
|
};
|
|
22794
22801
|
const isEmptyDiff = (diff) => diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0;
|
|
22802
|
+
const withoutPath = (tree, hidden) => {
|
|
22803
|
+
let paths;
|
|
22804
|
+
return {
|
|
22805
|
+
paths: () => paths ??= tree.paths().filter((path) => path !== hidden),
|
|
22806
|
+
read: (path) => path === hidden ? void 0 : tree.read(path),
|
|
22807
|
+
...tree.id !== void 0 ? { id: (path) => path === hidden ? void 0 : tree.id(path) } : {}
|
|
22808
|
+
};
|
|
22809
|
+
};
|
|
22810
|
+
const OLDEST_READABLE_FORMAT = 1;
|
|
22795
22811
|
const short$1 = (hash) => hash.slice(0, 7);
|
|
22796
22812
|
const formatFault = (commits) => {
|
|
22797
|
-
const foreign = commits.find((c) => c.format
|
|
22813
|
+
const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 2);
|
|
22798
22814
|
if (foreign === void 0) return void 0;
|
|
22799
|
-
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads
|
|
22815
|
+
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads formats ${OLDEST_READABLE_FORMAT} to 2 — run a gtd release that reads format ${foreign.format} to continue this process`;
|
|
22800
22816
|
};
|
|
22801
22817
|
//#endregion
|
|
22802
22818
|
//#region src/replay/Trailers.ts
|
|
@@ -22833,6 +22849,11 @@ const parseStepId = (raw) => {
|
|
|
22833
22849
|
occurrence: Number(match[2])
|
|
22834
22850
|
};
|
|
22835
22851
|
};
|
|
22852
|
+
/** What a pin and an unpin beat rest at, and their commits name — neither is a step of the workflow. */
|
|
22853
|
+
const PIN_STATES = {
|
|
22854
|
+
add: "pin",
|
|
22855
|
+
remove: "unpin"
|
|
22856
|
+
};
|
|
22836
22857
|
const isVerdict$1 = (value) => {
|
|
22837
22858
|
if (typeof value !== "object" || value === null) return false;
|
|
22838
22859
|
const v = value;
|
|
@@ -22852,13 +22873,7 @@ const parseJson = (raw) => {
|
|
|
22852
22873
|
const TRAILER_RE = /^(Gtd-[A-Za-z-]+):[ \t]*(.*?)[ \t]*$/gm;
|
|
22853
22874
|
const COST_RE = /^([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+))?$/;
|
|
22854
22875
|
const VAR_RE = /^([^=\s]+)=(.*)$/;
|
|
22855
|
-
const CONFIG_RE = /^([0-9a-f]{16}) (.+)$/;
|
|
22856
22876
|
const CONTROL_CHARACTER$1 = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
|
|
22857
|
-
const encodePath = (path) => path.trim() !== path || path.startsWith("\"") || CONTROL_CHARACTER$1.test(path) ? JSON.stringify(path) : path;
|
|
22858
|
-
const decodePath = (raw) => {
|
|
22859
|
-
const path = raw.startsWith("\"") ? parseJson(raw) : raw;
|
|
22860
|
-
return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
|
|
22861
|
-
};
|
|
22862
22877
|
const decodeApproval = (raw) => {
|
|
22863
22878
|
const parsed = parseJson(raw);
|
|
22864
22879
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
@@ -22910,11 +22925,6 @@ const TRAILER_READERS = {
|
|
|
22910
22925
|
const kv = VAR_RE.exec(value);
|
|
22911
22926
|
if (kv !== null) into.vars[kv[1]] = kv[2];
|
|
22912
22927
|
},
|
|
22913
|
-
"Gtd-Config": (value, into) => {
|
|
22914
|
-
const entry = CONFIG_RE.exec(value);
|
|
22915
|
-
const path = entry === null ? void 0 : decodePath(entry[2]);
|
|
22916
|
-
if (path !== void 0) into.config[path] = entry[1];
|
|
22917
|
-
},
|
|
22918
22928
|
"Gtd-Review-Base": (value, into) => {
|
|
22919
22929
|
into.reviewBase ??= value.split(/\s/)[0];
|
|
22920
22930
|
},
|
|
@@ -22930,6 +22940,9 @@ const TRAILER_READERS = {
|
|
|
22930
22940
|
"Gtd-Approved": (value, into) => {
|
|
22931
22941
|
into.approved ??= decodeApproval(value);
|
|
22932
22942
|
},
|
|
22943
|
+
"Gtd-Pin": (value, into) => {
|
|
22944
|
+
if (value === "add" || value === "remove") into.pin ??= value;
|
|
22945
|
+
},
|
|
22933
22946
|
"Gtd-Format": (value, into) => {
|
|
22934
22947
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
22935
22948
|
},
|
|
@@ -22952,10 +22965,10 @@ const parseCommitMessage = (message) => {
|
|
|
22952
22965
|
version: void 0,
|
|
22953
22966
|
kept: void 0,
|
|
22954
22967
|
approved: void 0,
|
|
22968
|
+
pin: void 0,
|
|
22955
22969
|
judge: [],
|
|
22956
22970
|
marks: [],
|
|
22957
22971
|
vars: {},
|
|
22958
|
-
config: {},
|
|
22959
22972
|
cost: [],
|
|
22960
22973
|
format: void 0
|
|
22961
22974
|
};
|
|
@@ -22969,24 +22982,28 @@ const parseCommitMessage = (message) => {
|
|
|
22969
22982
|
};
|
|
22970
22983
|
const sortedEntries = (record = {}) => Object.entries(record).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
22971
22984
|
/** Trailer order is fixed so the same spec always yields the same bytes. */
|
|
22985
|
+
const trailer = (name, value) => value === void 0 ? [] : [`Gtd-${name}: ${value}`];
|
|
22986
|
+
const costValue = (cost) => cost === void 0 ? void 0 : `${cost.cost}${cost.model !== void 0 ? ` ${cost.model}` : ""}`;
|
|
22972
22987
|
const formatCommitMessage = (spec) => {
|
|
22973
|
-
const lines = [
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22988
|
+
const lines = [
|
|
22989
|
+
...trailer("Step", spec.step && formatStepId(spec.step)),
|
|
22990
|
+
...trailer("Workflow", spec.workflow),
|
|
22991
|
+
...trailer("Review-Base", spec.reviewBase),
|
|
22992
|
+
...sortedEntries(spec.vars).map(([name, value]) => `Gtd-Var: ${name}=${value}`),
|
|
22993
|
+
...trailer("Cost", costValue(spec.cost)),
|
|
22994
|
+
...(spec.judge ?? []).map((verdict) => `Gtd-Judge: ${JSON.stringify(verdict)}`),
|
|
22995
|
+
...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
|
|
22996
|
+
...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
|
|
22997
|
+
...trailer("Pin", spec.pin),
|
|
22998
|
+
...trailer("Branch", spec.branch),
|
|
22999
|
+
...trailer("Version", spec.version),
|
|
23000
|
+
`Gtd-Format: 2`
|
|
23001
|
+
];
|
|
22986
23002
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
22987
23003
|
};
|
|
22988
23004
|
//#endregion
|
|
22989
23005
|
//#region src/replay/Replay.ts
|
|
23006
|
+
const UNPIN_STATE = PIN_STATES.remove;
|
|
22990
23007
|
const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
22991
23008
|
const memoryScopeOf = (name) => {
|
|
22992
23009
|
const dot = name.lastIndexOf(".");
|
|
@@ -23120,7 +23137,33 @@ const modeWithoutFile = (step) => {
|
|
|
23120
23137
|
if (options.mode === void 0 || options.file !== void 0) return void 0;
|
|
23121
23138
|
return `gtd: step "${step.name}": mode requires file in ${step.kind}() options — name the steering file the mode checks`;
|
|
23122
23139
|
};
|
|
23123
|
-
const
|
|
23140
|
+
const withoutPin = (input) => {
|
|
23141
|
+
const hide = (tree) => withoutPath(tree, PIN_PATH);
|
|
23142
|
+
return {
|
|
23143
|
+
...input,
|
|
23144
|
+
episode: {
|
|
23145
|
+
base: {
|
|
23146
|
+
...input.episode.base,
|
|
23147
|
+
tree: hide(input.episode.base.tree)
|
|
23148
|
+
},
|
|
23149
|
+
commits: input.episode.commits.map((c) => ({
|
|
23150
|
+
...c,
|
|
23151
|
+
tree: hide(c.tree),
|
|
23152
|
+
...c.kept !== void 0 ? { kept: {
|
|
23153
|
+
...c.kept,
|
|
23154
|
+
tree: hide(c.kept.tree)
|
|
23155
|
+
} } : {}
|
|
23156
|
+
}))
|
|
23157
|
+
},
|
|
23158
|
+
...input.startTree !== void 0 ? { startTree: hide(input.startTree) } : {},
|
|
23159
|
+
...input.pending !== void 0 ? { pending: {
|
|
23160
|
+
...input.pending,
|
|
23161
|
+
tree: hide(input.pending.tree)
|
|
23162
|
+
} } : {}
|
|
23163
|
+
};
|
|
23164
|
+
};
|
|
23165
|
+
const replay = async (raw) => {
|
|
23166
|
+
const input = withoutPin(raw);
|
|
23124
23167
|
const commits = input.episode.commits.map((c) => ({
|
|
23125
23168
|
...c,
|
|
23126
23169
|
parsed: parseCommitMessage(c.message)
|
|
@@ -23129,6 +23172,9 @@ const replay = async (input) => {
|
|
|
23129
23172
|
let cursor = 0;
|
|
23130
23173
|
let pendingUsed = false;
|
|
23131
23174
|
let position = input.episode.base;
|
|
23175
|
+
const skipPins = () => {
|
|
23176
|
+
while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) cursor++;
|
|
23177
|
+
};
|
|
23132
23178
|
let previousPosition = input.episode.base;
|
|
23133
23179
|
let expectedNext;
|
|
23134
23180
|
const occurrences = /* @__PURE__ */ new Map();
|
|
@@ -23310,19 +23356,24 @@ const replay = async (input) => {
|
|
|
23310
23356
|
if (unapproved !== void 0) return finish(unapproved);
|
|
23311
23357
|
cursor++;
|
|
23312
23358
|
advance(commit);
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
hash: commit.kept.hash,
|
|
23316
|
-
before: commit.tree,
|
|
23317
|
-
after: commit.kept.tree
|
|
23318
|
-
});
|
|
23359
|
+
skipPins();
|
|
23360
|
+
noteKept(step, commit);
|
|
23319
23361
|
const to = commit.parsed.parsed?.to;
|
|
23320
|
-
if (to !== void 0) expectedNext = {
|
|
23362
|
+
if (to !== void 0 && to !== UNPIN_STATE) expectedNext = {
|
|
23321
23363
|
name: to,
|
|
23322
23364
|
hash: commit.hash
|
|
23323
23365
|
};
|
|
23324
23366
|
return replyFor(step.request, commit.parsed.judge);
|
|
23325
23367
|
};
|
|
23368
|
+
const noteKept = (step, commit) => {
|
|
23369
|
+
if (commit.parsed.kept === void 0) return;
|
|
23370
|
+
if (commit.kept === void 0) keptTips.delete(step.name);
|
|
23371
|
+
else keptTips.set(step.name, {
|
|
23372
|
+
hash: commit.kept.hash,
|
|
23373
|
+
before: commit.tree,
|
|
23374
|
+
after: commit.kept.tree
|
|
23375
|
+
});
|
|
23376
|
+
};
|
|
23326
23377
|
const landPending = (step, pending) => {
|
|
23327
23378
|
pendingUsed = true;
|
|
23328
23379
|
landed = step;
|
|
@@ -23339,7 +23390,10 @@ const replay = async (input) => {
|
|
|
23339
23390
|
return replyFor(step.request, pending.verdicts ?? []);
|
|
23340
23391
|
};
|
|
23341
23392
|
const answer = (step) => {
|
|
23342
|
-
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name))
|
|
23393
|
+
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) {
|
|
23394
|
+
cursor++;
|
|
23395
|
+
skipPins();
|
|
23396
|
+
}
|
|
23343
23397
|
const commit = commits[cursor];
|
|
23344
23398
|
if (commit !== void 0) return consumeCommit(step, commit);
|
|
23345
23399
|
if (input.pending === void 0 || pendingUsed) return finish({
|
|
@@ -23352,6 +23406,7 @@ const replay = async (input) => {
|
|
|
23352
23406
|
return landPending(step, input.pending);
|
|
23353
23407
|
};
|
|
23354
23408
|
const requestFault = (request) => {
|
|
23409
|
+
if (Object.values(PIN_STATES).includes(scoped(request.name))) return `gtd: step "${scoped(request.name)}": the name is reserved for the pin and unpin beats — rename the step`;
|
|
23355
23410
|
if (request.kind === "run" && typeof request.body !== "string") return `gtd: step "${scoped(request.name)}": a run() body is a shell script string — decide in flow code, then render the script (see checkScript and friends)`;
|
|
23356
23411
|
if (request.kind !== "agent") return void 0;
|
|
23357
23412
|
const name = scoped(request.name);
|
|
@@ -23378,15 +23433,18 @@ const replay = async (input) => {
|
|
|
23378
23433
|
const blocker = blockerAt(step);
|
|
23379
23434
|
return blocker === void 0 ? answer(step) : finish(blocker);
|
|
23380
23435
|
};
|
|
23381
|
-
const endOfFlow = (via) =>
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23436
|
+
const endOfFlow = (via) => {
|
|
23437
|
+
skipPins();
|
|
23438
|
+
return cursor < commits.length ? {
|
|
23439
|
+
kind: "divergence",
|
|
23440
|
+
message: `gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode — ${DIVERGED}`
|
|
23441
|
+
} : {
|
|
23442
|
+
kind: "ended",
|
|
23443
|
+
via,
|
|
23444
|
+
trace,
|
|
23445
|
+
landed,
|
|
23446
|
+
marks
|
|
23447
|
+
};
|
|
23390
23448
|
};
|
|
23391
23449
|
const changesSince = (hash) => {
|
|
23392
23450
|
const tree = treeAt(hash);
|
|
@@ -23449,10 +23507,11 @@ const replay = async (input) => {
|
|
|
23449
23507
|
skillsFor: (localName) => resolveSkills(memoryScopeOf(scoped(localName))) ?? [],
|
|
23450
23508
|
accessFor: (localName, file) => foldedAccessFor(resolveAccess(memoryScopeOf(scoped(localName))), file),
|
|
23451
23509
|
head: () => {
|
|
23452
|
-
const rest = commits.slice(cursor);
|
|
23510
|
+
const rest = commits.slice(cursor).filter((c) => c.parsed.pin === void 0);
|
|
23453
23511
|
return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
|
|
23454
23512
|
}
|
|
23455
23513
|
};
|
|
23514
|
+
skipPins();
|
|
23456
23515
|
installContext(context);
|
|
23457
23516
|
try {
|
|
23458
23517
|
const flowDone = input.flow({}).then(() => {
|
|
@@ -23840,7 +23899,8 @@ const load = gen(function* () {
|
|
|
23840
23899
|
},
|
|
23841
23900
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
23842
23901
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
23843
|
-
fingerprint: pinned?.fingerprint ?? configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk)
|
|
23902
|
+
fingerprint: pinned?.fingerprint ?? configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
|
|
23903
|
+
drift: pinned === void 0 ? [] : changedConfigFiles(pinned.fingerprint, configFingerprint(Object.keys(pinned.fingerprint).map((file) => resolve(host.root, file)), host.root, readFromDisk))
|
|
23844
23904
|
};
|
|
23845
23905
|
});
|
|
23846
23906
|
const readFromDisk = (path) => {
|
|
@@ -24490,10 +24550,6 @@ const resolveScopeAccess = (def, skills, vars) => gen(function* () {
|
|
|
24490
24550
|
//#endregion
|
|
24491
24551
|
//#region src/workflow/Workflow.ts
|
|
24492
24552
|
const knownModes = (def) => Object.keys(def.modes);
|
|
24493
|
-
//#endregion
|
|
24494
|
-
//#region src/workflow/pin.ts
|
|
24495
|
-
/** Bumped when a launcher could no longer run an older pin the same way. */
|
|
24496
|
-
const PIN_FORMAT = 1;
|
|
24497
24553
|
/**
|
|
24498
24554
|
* The engine a pin carries, as ES module source. Live, the `gtd.core.mjs` the
|
|
24499
24555
|
* running gtd ships beside its own bundle.
|
|
@@ -24519,1549 +24575,1650 @@ const pin = (module, root, version) => gen(function* () {
|
|
|
24519
24575
|
try: () => pinnedWorkflowSource(module, root, version),
|
|
24520
24576
|
catch: (e) => new GtdError(`gtd bundle: ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
24521
24577
|
})) + core.replace(/^#!.*\n/, "");
|
|
24522
|
-
return Buffer.concat([Buffer.from(`gtdpin
|
|
24578
|
+
return Buffer.concat([Buffer.from(`gtdpin 1 gtd ${version}\n`), gzipSync(source, { level: 9 })]);
|
|
24523
24579
|
});
|
|
24524
24580
|
//#endregion
|
|
24525
|
-
//#region src/
|
|
24581
|
+
//#region src/emit/GitScript.ts
|
|
24582
|
+
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
24526
24583
|
/**
|
|
24527
|
-
*
|
|
24528
|
-
*
|
|
24529
|
-
*
|
|
24584
|
+
* `--allow-empty` because gtd's workflow commits may be empty on purpose.
|
|
24585
|
+
* Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
|
|
24586
|
+
* "empty git commit" rejection message, discriminated from the captured
|
|
24587
|
+
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
24588
|
+
* error, a commit-msg hook) still fails.
|
|
24530
24589
|
*/
|
|
24531
|
-
const
|
|
24532
|
-
|
|
24533
|
-
|
|
24534
|
-
|
|
24535
|
-
|
|
24536
|
-
|
|
24537
|
-
|
|
24538
|
-
|
|
24539
|
-
|
|
24540
|
-
|
|
24541
|
-
|
|
24542
|
-
kind: "noop",
|
|
24543
|
-
state: snapshot.state,
|
|
24544
|
-
settled: landing.settled
|
|
24545
|
-
};
|
|
24546
|
-
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
24547
|
-
cost: opts.cost,
|
|
24548
|
-
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
24549
|
-
} };
|
|
24550
|
-
if (landing.kind === "attempt") {
|
|
24551
|
-
const message = formatCommitMessage({
|
|
24552
|
-
actor: snapshot.actor,
|
|
24553
|
-
to: snapshot.state,
|
|
24554
|
-
...cost
|
|
24555
|
-
});
|
|
24556
|
-
return {
|
|
24557
|
-
kind: "commit",
|
|
24558
|
-
state: snapshot.state,
|
|
24559
|
-
to: snapshot.state,
|
|
24560
|
-
subject: landing.subject,
|
|
24561
|
-
steps: [{
|
|
24562
|
-
kind: "gitWrite",
|
|
24563
|
-
write: {
|
|
24564
|
-
kind: "commitAll",
|
|
24565
|
-
message
|
|
24566
|
-
}
|
|
24567
|
-
}, {
|
|
24568
|
-
kind: "outcome",
|
|
24569
|
-
outcome: {
|
|
24570
|
-
kind: "commit",
|
|
24571
|
-
subject: landing.subject
|
|
24572
|
-
}
|
|
24573
|
-
}]
|
|
24574
|
-
};
|
|
24575
|
-
}
|
|
24576
|
-
const message = formatCommitMessage({
|
|
24577
|
-
...landing.spec,
|
|
24578
|
-
...cost,
|
|
24579
|
-
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
24580
|
-
});
|
|
24581
|
-
const subject = message.split("\n")[0];
|
|
24582
|
-
const outcome = landing.to === snapshot.state ? {
|
|
24583
|
-
kind: "outcome",
|
|
24584
|
-
outcome: {
|
|
24585
|
-
kind: "commit",
|
|
24586
|
-
subject
|
|
24587
|
-
}
|
|
24588
|
-
} : {
|
|
24589
|
-
kind: "outcome",
|
|
24590
|
-
outcome: {
|
|
24591
|
-
kind: "transition",
|
|
24592
|
-
from: snapshot.state,
|
|
24593
|
-
to: landing.to
|
|
24594
|
-
}
|
|
24595
|
-
};
|
|
24596
|
-
return {
|
|
24597
|
-
kind: "commit",
|
|
24598
|
-
state: snapshot.state,
|
|
24599
|
-
to: landing.to,
|
|
24600
|
-
subject,
|
|
24601
|
-
steps: [
|
|
24602
|
-
...uncheckStep(snapshot),
|
|
24603
|
-
{
|
|
24604
|
-
kind: "gitWrite",
|
|
24605
|
-
write: landing.keepTurn !== void 0 ? {
|
|
24606
|
-
kind: "keepTurn",
|
|
24607
|
-
ref: landing.keepTurn,
|
|
24608
|
-
message
|
|
24609
|
-
} : landing.spec.approved !== void 0 ? {
|
|
24610
|
-
kind: "commitClean",
|
|
24611
|
-
message
|
|
24612
|
-
} : {
|
|
24613
|
-
kind: "commitAll",
|
|
24614
|
-
message
|
|
24615
|
-
}
|
|
24616
|
-
},
|
|
24617
|
-
...landing.keepHistory !== void 0 ? [{
|
|
24618
|
-
kind: "gitWrite",
|
|
24619
|
-
write: {
|
|
24620
|
-
kind: "updateRef",
|
|
24621
|
-
ref: landing.keepHistory,
|
|
24622
|
-
target: "HEAD"
|
|
24623
|
-
}
|
|
24624
|
-
}] : [],
|
|
24625
|
-
outcome
|
|
24626
|
-
]
|
|
24627
|
-
};
|
|
24590
|
+
const commitAllowEmpty = (message, only) => {
|
|
24591
|
+
const m = shellQuote(message);
|
|
24592
|
+
const paths = only === void 0 ? "" : ` -- ${shellQuote(only)}`;
|
|
24593
|
+
return [
|
|
24594
|
+
`if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
|
|
24595
|
+
` case "$out" in`,
|
|
24596
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m}${paths} || exit 1 ;;`,
|
|
24597
|
+
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
24598
|
+
` esac`,
|
|
24599
|
+
`fi`
|
|
24600
|
+
].join("\n");
|
|
24628
24601
|
};
|
|
24629
|
-
|
|
24630
|
-
|
|
24602
|
+
const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
|
|
24603
|
+
/** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
|
|
24604
|
+
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
24605
|
+
/** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
|
|
24606
|
+
const commitPath = (path, message) => `git add -A -- ${shellQuote(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
|
|
24607
|
+
/** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
|
|
24608
|
+
const pinScript = (path) => {
|
|
24609
|
+
const p = shellQuote(path);
|
|
24610
|
+
const slash = path.lastIndexOf("/");
|
|
24611
|
+
return `${slash === -1 ? "" : `mkdir -p ${shellQuote(path.slice(0, slash))}\n`}gtd bundle >${p} || { rm -f -- ${p}; exit 1; }`;
|
|
24612
|
+
};
|
|
24613
|
+
/** An unpin beat's script, and `gtd abandon`'s removal of the pin. */
|
|
24614
|
+
const unpinScript = (path) => `rm -f -- ${shellQuote(path)}`;
|
|
24631
24615
|
/**
|
|
24632
|
-
*
|
|
24633
|
-
*
|
|
24634
|
-
* runs
|
|
24616
|
+
* An approval records exactly the judged tree: HEAD is pinned before the
|
|
24617
|
+
* clean check and the commit is built over its tree with `commit-tree`, so no
|
|
24618
|
+
* hook runs and none can add to it. A HEAD that moved fails the guarded
|
|
24619
|
+
* `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
|
|
24635
24620
|
*/
|
|
24636
|
-
const
|
|
24637
|
-
|
|
24638
|
-
|
|
24639
|
-
|
|
24621
|
+
const commitClean = (message) => [
|
|
24622
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
24623
|
+
`if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
|
|
24624
|
+
` printf '%s\\n' ${shellQuote(CHANGED_SINCE_LAND)} >&2`,
|
|
24625
|
+
` exit 1`,
|
|
24626
|
+
`fi`,
|
|
24627
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
24628
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
24629
|
+
`gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote(message)}) || exit 1`,
|
|
24630
|
+
`git update-ref -m ${shellQuote(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
|
|
24631
|
+
].join("\n");
|
|
24632
|
+
/** No current caller — see `Git.ts`'s `GitWriterOperations.commitAsIs` for why it's kept anyway. */
|
|
24633
|
+
const commitAsIs = (message) => commitAllowEmpty(message);
|
|
24634
|
+
/** No current caller — see `Git.ts`'s `GitWriterOperations.softResetTo` for why it's kept anyway. */
|
|
24635
|
+
const softResetTo = (ref) => `git reset --soft ${shellQuote(ref)}`;
|
|
24636
|
+
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote(ref)}`;
|
|
24637
|
+
const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
|
|
24638
|
+
/**
|
|
24639
|
+
* `git add -A` then `git reset --hard HEAD` — discards every pending change,
|
|
24640
|
+
* tracked or untracked. Joined with `&&`: a failed stage must not reach the
|
|
24641
|
+
* hard reset, or untracked survivors remain — the exact outcome this builder
|
|
24642
|
+
* exists to avoid. No current caller — see `Git.ts`'s
|
|
24643
|
+
* `GitWriterOperations.discardPending` for why it's kept anyway.
|
|
24644
|
+
*/
|
|
24645
|
+
const discardPending = () => `git add -A && git reset --hard HEAD`;
|
|
24646
|
+
/**
|
|
24647
|
+
* POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
|
|
24648
|
+
* tree, so no hook runs on it and its tree is the pre-turn tree by
|
|
24649
|
+
* construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
|
|
24650
|
+
* `-S`, read (and failing closed) before any object or ref is written; the
|
|
24651
|
+
* index is already staged by then, so an early exit leaves the turn staged.
|
|
24652
|
+
* The kept ref is deleted only after `reset -q` succeeds: it is then the
|
|
24653
|
+
* turn's only handle.
|
|
24654
|
+
* The branch moves before the final hard reset, so if that reset fails the
|
|
24655
|
+
* marker has landed and the turn is still in the working tree (kept ref
|
|
24656
|
+
* present); every other failure leaves the branch untouched.
|
|
24657
|
+
*/
|
|
24658
|
+
const keepTurn = (refPrefix, message) => {
|
|
24659
|
+
const keptRef = `${shellQuote(`${refPrefix}/`)}"$gtd_kept"`;
|
|
24640
24660
|
return [
|
|
24641
|
-
|
|
24642
|
-
|
|
24643
|
-
`
|
|
24661
|
+
`git add -A || exit 1`,
|
|
24662
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
24663
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
24664
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
24665
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
24666
|
+
`gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote(message)}) || exit 1`,
|
|
24667
|
+
`git update-ref ${keptRef} "$gtd_kept" || exit 1`,
|
|
24668
|
+
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote(message)} "$gtd_kept")`,
|
|
24669
|
+
`if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
|
|
24670
|
+
` git reset -q && git update-ref -d ${keptRef}`,
|
|
24671
|
+
` exit 1`,
|
|
24672
|
+
`fi`,
|
|
24673
|
+
`git reset --hard HEAD`
|
|
24644
24674
|
].join("\n");
|
|
24645
24675
|
};
|
|
24646
|
-
|
|
24647
|
-
|
|
24648
|
-
const
|
|
24649
|
-
kind: "refusal",
|
|
24650
|
-
message
|
|
24651
|
-
});
|
|
24652
|
-
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
24653
|
-
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
24654
|
-
if (environmentOnly.length > 0) return yield* fail$4(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it as a workflow option in gtd.config.ts or with a GTD_<NAME> environment variable`));
|
|
24655
|
-
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
24656
|
-
if (undeclared.length > 0) return yield* fail$4(new GtdUsageError(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`));
|
|
24657
|
-
});
|
|
24676
|
+
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
24677
|
+
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
24678
|
+
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
24658
24679
|
/**
|
|
24659
|
-
*
|
|
24660
|
-
*
|
|
24661
|
-
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
24662
|
-
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
|
|
24663
|
-
* and a `Gtd-Config:` trailer per config file.
|
|
24664
|
-
* All validation is a REFUSAL, not an Effect failure.
|
|
24680
|
+
* No `+` on the refspec: a local ref the remote's tip does not descend from
|
|
24681
|
+
* fails the fetch instead of being overwritten (a fast-forward still applies).
|
|
24665
24682
|
*/
|
|
24666
|
-
const
|
|
24667
|
-
|
|
24668
|
-
|
|
24669
|
-
|
|
24670
|
-
|
|
24671
|
-
|
|
24672
|
-
|
|
24673
|
-
|
|
24674
|
-
|
|
24675
|
-
|
|
24676
|
-
|
|
24677
|
-
|
|
24678
|
-
|
|
24679
|
-
|
|
24680
|
-
|
|
24681
|
-
|
|
24682
|
-
|
|
24683
|
-
|
|
24684
|
-
|
|
24685
|
-
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
24686
|
-
if (typeof base === "object") return base;
|
|
24687
|
-
const subject = formatSubject(actor, firstStep);
|
|
24688
|
-
return {
|
|
24689
|
-
kind: "start",
|
|
24690
|
-
workflow: name,
|
|
24691
|
-
subject,
|
|
24692
|
-
steps: [{
|
|
24693
|
-
kind: "gitWrite",
|
|
24694
|
-
write: {
|
|
24695
|
-
kind: "commitAll",
|
|
24696
|
-
message: formatCommitMessage({
|
|
24697
|
-
actor,
|
|
24698
|
-
to: firstStep,
|
|
24699
|
-
workflow: name,
|
|
24700
|
-
...base !== void 0 ? { reviewBase: base } : {},
|
|
24701
|
-
vars,
|
|
24702
|
-
config: start.config
|
|
24703
|
-
})
|
|
24704
|
-
}
|
|
24705
|
-
}, {
|
|
24706
|
-
kind: "outcome",
|
|
24707
|
-
outcome: {
|
|
24708
|
-
kind: "commit",
|
|
24709
|
-
subject
|
|
24710
|
-
}
|
|
24711
|
-
}]
|
|
24712
|
-
};
|
|
24713
|
-
});
|
|
24714
|
-
/**
|
|
24715
|
-
* The workflow's diff base: `git merge-base <base> HEAD`, where a blank base
|
|
24716
|
-
* means the default branch. The merge-base is what gets pinned, so a base on
|
|
24717
|
-
* another branch reviews exactly this branch's own commits.
|
|
24718
|
-
*/
|
|
24719
|
-
const resolveBase = (commandLabel, value) => gen(function* () {
|
|
24720
|
-
const git = yield* GitService;
|
|
24721
|
-
const rendered = value.trim();
|
|
24722
|
-
const target = rendered === "" ? yield* git.defaultBranch() : rendered;
|
|
24723
|
-
const resolvedBase = yield* result(git.resolveRef(target));
|
|
24724
|
-
if (resolvedBase._tag === "Failure") return refusal(`${commandLabel}: "${target}" does not resolve to a commit`);
|
|
24725
|
-
const mergeBase = yield* git.mergeBase(resolvedBase.success, "HEAD");
|
|
24726
|
-
if (isNone(mergeBase)) return refusal(`${commandLabel}: "${target}" shares no common ancestor with HEAD`);
|
|
24727
|
-
if (mergeBase.value === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: nothing to review: HEAD has no commits beyond ${target}`);
|
|
24728
|
-
return mergeBase.value;
|
|
24729
|
-
});
|
|
24730
|
-
//#endregion
|
|
24731
|
-
//#region src/step/strayPaths.ts
|
|
24732
|
-
/** Globs use the `access.write` dialect (`globMatches`). */
|
|
24733
|
-
const strayPaths = (touched, allow) => touched.filter((path) => !allow.some((glob) => globMatches(path, glob))).sort();
|
|
24734
|
-
//#endregion
|
|
24735
|
-
//#region src/edge/Edge.ts
|
|
24736
|
-
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
24737
|
-
const ACTORS = /* @__PURE__ */ new Set([
|
|
24738
|
-
"agent",
|
|
24739
|
-
"human",
|
|
24740
|
-
"check",
|
|
24741
|
-
"judge"
|
|
24742
|
-
]);
|
|
24743
|
-
/**
|
|
24744
|
-
* Where the episode HEAD belongs to begins, read off subjects alone. Walking
|
|
24745
|
-
* back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
|
|
24746
|
-
* `gtd start` writes — no landing ever produces one — and opens an
|
|
24747
|
-
* episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
|
|
24748
|
-
* a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
|
|
24749
|
-
* entering the flow's first step (a finished episode), bounds the episode
|
|
24750
|
-
* from below.
|
|
24751
|
-
*/
|
|
24752
|
-
const locateEpisode = (def, history) => {
|
|
24753
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
24754
|
-
const message = parseCommitMessage(history[i].message);
|
|
24755
|
-
const subject = message.parsed;
|
|
24756
|
-
if (subject !== void 0 && message.step === void 0 && subject.from === void 0 && subject.actor === "human") return {
|
|
24757
|
-
workflow: message.workflow,
|
|
24758
|
-
legacyOpening: message.workflow === void 0,
|
|
24759
|
-
baseIndex: i,
|
|
24760
|
-
processStart: i
|
|
24761
|
-
};
|
|
24762
|
-
if (subject === void 0 || !ACTORS.has(subject.actor) || subject.to === def.initial) return {
|
|
24763
|
-
workflow: void 0,
|
|
24764
|
-
legacyOpening: false,
|
|
24765
|
-
baseIndex: i,
|
|
24766
|
-
processStart: i + 1
|
|
24767
|
-
};
|
|
24683
|
+
const fetchHistoryRefs = (remote) => [
|
|
24684
|
+
`set -eu`,
|
|
24685
|
+
`before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
24686
|
+
`git fetch ${shellQuote(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
|
|
24687
|
+
`after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
24688
|
+
`printf '%s history refs arrived\\n' "$((after - before))"`
|
|
24689
|
+
].join("\n");
|
|
24690
|
+
/** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
|
|
24691
|
+
const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
|
|
24692
|
+
const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
24693
|
+
const renderOutcome = (outcome) => {
|
|
24694
|
+
switch (outcome.kind) {
|
|
24695
|
+
case "transition": return printfLine$1("%s %s → %s\n", [
|
|
24696
|
+
"'->'",
|
|
24697
|
+
shellQuote(outcome.from),
|
|
24698
|
+
shellQuote(outcome.to)
|
|
24699
|
+
]);
|
|
24700
|
+
case "commit": return printfLine$1("%s %s\n", ["'[commit]'", shellQuote(outcome.subject)]);
|
|
24701
|
+
case "note": return printfLine$1("%s\n", [shellQuote(outcome.text)]);
|
|
24768
24702
|
}
|
|
24769
|
-
return {
|
|
24770
|
-
workflow: void 0,
|
|
24771
|
-
legacyOpening: false,
|
|
24772
|
-
baseIndex: -1,
|
|
24773
|
-
processStart: 0
|
|
24774
|
-
};
|
|
24775
|
-
};
|
|
24776
|
-
const headTurnOf = (head) => {
|
|
24777
|
-
if (head === void 0) return void 0;
|
|
24778
|
-
const message = parseCommitMessage(head.message);
|
|
24779
|
-
const subject = message.parsed;
|
|
24780
|
-
if (subject === void 0 || !ACTORS.has(subject.actor)) return void 0;
|
|
24781
|
-
return {
|
|
24782
|
-
state: subject.to,
|
|
24783
|
-
actor: subject.actor,
|
|
24784
|
-
empty: head.touched.length === 0,
|
|
24785
|
-
step: message.step !== void 0
|
|
24786
|
-
};
|
|
24787
24703
|
};
|
|
24788
|
-
const
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
model: c.model ?? "unspecified",
|
|
24796
|
-
hash
|
|
24797
|
-
}));
|
|
24798
|
-
const hashAt = (history, index) => history[index]?.hash;
|
|
24799
|
-
const historyFormatFault = (history, location) => formatFault(history.slice(location.baseIndex + 1).concat(history[location.baseIndex]?.message.startsWith("gtd") ? [history[location.baseIndex]] : []).map((c) => ({
|
|
24800
|
-
hash: c.hash,
|
|
24801
|
-
format: parseCommitMessage(c.message).format
|
|
24802
|
-
})));
|
|
24803
|
-
/** Fails on a history in a format this gtd does not read, before anything acts on it. */
|
|
24804
|
-
const runOf = (history, location, closingHash = void 0) => {
|
|
24805
|
-
const fault = historyFormatFault(history, location);
|
|
24806
|
-
return fault === void 0 ? succeed$1(buildRun(history, location, closingHash)) : fail$4(new Error(fault));
|
|
24807
|
-
};
|
|
24808
|
-
const pinnedConfigOf = (history) => {
|
|
24809
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
24810
|
-
const files = parseCommitMessage(history[i].message).config;
|
|
24811
|
-
if (Object.keys(files).length > 0) return {
|
|
24812
|
-
commit: history[i].hash,
|
|
24813
|
-
files
|
|
24814
|
-
};
|
|
24704
|
+
const renderGitWrite = (write) => {
|
|
24705
|
+
switch (write.kind) {
|
|
24706
|
+
case "commitAll": return commitAll(write.message);
|
|
24707
|
+
case "commitPath": return commitPath(write.path, write.message);
|
|
24708
|
+
case "commitClean": return commitClean(write.message);
|
|
24709
|
+
case "updateRef": return updateRef(write.ref, write.target);
|
|
24710
|
+
case "keepTurn": return keepTurn(write.ref, write.message);
|
|
24815
24711
|
}
|
|
24816
24712
|
};
|
|
24817
|
-
const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
|
|
24818
|
-
step: formatStepId(message.approved.step),
|
|
24819
|
-
name: message.approved.name,
|
|
24820
|
-
email: message.approved.email,
|
|
24821
|
-
commit: hash
|
|
24822
|
-
}];
|
|
24823
|
-
const buildRun = (history, location, closingHash) => {
|
|
24824
|
-
const processCommits = history.slice(location.processStart);
|
|
24825
|
-
const parsed = processCommits.map((c) => parseCommitMessage(c.message));
|
|
24826
|
-
const first = parsed[0];
|
|
24827
|
-
const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
|
|
24828
|
-
const head = history[history.length - 1];
|
|
24829
|
-
return {
|
|
24830
|
-
workflow: location.workflow,
|
|
24831
|
-
legacyOpening: location.legacyOpening,
|
|
24832
|
-
startHash: hashAt(history, location.processStart) ?? head?.hash ?? EMPTY_TREE$1,
|
|
24833
|
-
startParentHash,
|
|
24834
|
-
diffBase: first?.reviewBase ?? startParentHash,
|
|
24835
|
-
trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
|
|
24836
|
-
costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
|
|
24837
|
-
judgeVerdicts: parsed.flatMap((m) => m.judge),
|
|
24838
|
-
marks: parsed.flatMap((m) => m.marks),
|
|
24839
|
-
approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
|
|
24840
|
-
pinnedVars: { ...first?.vars },
|
|
24841
|
-
pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
|
|
24842
|
-
headTurn: headTurnOf(head),
|
|
24843
|
-
closingHash,
|
|
24844
|
-
episode: {
|
|
24845
|
-
base: hashAt(history, location.baseIndex),
|
|
24846
|
-
commits: history.slice(location.baseIndex + 1)
|
|
24847
|
-
}
|
|
24848
|
-
};
|
|
24849
|
-
};
|
|
24850
|
-
const PAGE_TIERS = [
|
|
24851
|
-
32,
|
|
24852
|
-
128,
|
|
24853
|
-
512,
|
|
24854
|
-
Infinity
|
|
24855
|
-
];
|
|
24856
24713
|
/**
|
|
24857
|
-
* `
|
|
24858
|
-
*
|
|
24859
|
-
*
|
|
24860
|
-
*
|
|
24861
|
-
* `locateEpisode`'s own `opening` branch (a started episode's first commit),
|
|
24862
|
-
* which takes priority there over the initial-state check no matter what the
|
|
24863
|
-
* workflow's first step is named — including one named after `def.initial`
|
|
24864
|
-
* itself, where the two branches would otherwise disagree. A bare human commit can
|
|
24865
|
-
* also be a self-loop step landing rather than a real opening (this
|
|
24866
|
-
* subject-only pass can't read the `Gtd-Step` trailer that would tell them
|
|
24867
|
-
* apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
|
|
24868
|
-
* commits further back than strictly needed — never stop short of the
|
|
24869
|
-
* boundary `locateEpisode` would find once it has full commit bodies in
|
|
24870
|
-
* hand. That asymmetry is the only direction this approximation may err in;
|
|
24871
|
-
* it must never call a real boundary "not yet found".
|
|
24714
|
+
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
24715
|
+
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
24716
|
+
* `failurePromptWrapper` (same reverse-import concern as `renderOutcome`
|
|
24717
|
+
* above: `Emit.ts` imports `shellQuote` from here already).
|
|
24872
24718
|
*/
|
|
24873
|
-
const
|
|
24874
|
-
|
|
24875
|
-
|
|
24876
|
-
|
|
24877
|
-
|
|
24719
|
+
const failurePromptWrapper$1 = (command, prompt) => {
|
|
24720
|
+
return [
|
|
24721
|
+
`gtd_validate_status=0`,
|
|
24722
|
+
`gtd_validate_out="$( {`,
|
|
24723
|
+
command,
|
|
24724
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
24725
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
24726
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote(prompt)} "$gtd_validate_out"`,
|
|
24727
|
+
` exit "$gtd_validate_status"`,
|
|
24728
|
+
`fi`
|
|
24729
|
+
].join("\n");
|
|
24878
24730
|
};
|
|
24879
|
-
|
|
24880
|
-
|
|
24881
|
-
|
|
24731
|
+
const renderLandStep = (step) => {
|
|
24732
|
+
if (step.kind === "gitWrite") return renderGitWrite(step.write);
|
|
24733
|
+
if (step.kind === "outcome") return renderOutcome(step.outcome);
|
|
24734
|
+
if (step.kind === "uncheck") return `gtd uncheck ${shellQuote(step.file)}`;
|
|
24735
|
+
return step.onFailure !== void 0 ? failurePromptWrapper$1(step.command, step.onFailure) : step.command;
|
|
24882
24736
|
};
|
|
24737
|
+
const ScriptSurface = { render: (steps) => {
|
|
24738
|
+
const rendered = steps.map(renderLandStep);
|
|
24739
|
+
return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
|
|
24740
|
+
} };
|
|
24741
|
+
//#endregion
|
|
24742
|
+
//#region src/emit/Emit.ts
|
|
24743
|
+
/** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
|
|
24744
|
+
const fileExistsGuard = (file) => `[ -f ${shellQuote(file)} ] || exit 0`;
|
|
24745
|
+
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
24746
|
+
const withFileVar = (command, file) => `export GTD_FILE=${shellQuote(file)}\n${command}`;
|
|
24747
|
+
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
|
|
24748
|
+
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
|
|
24749
|
+
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
|
|
24750
|
+
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
24751
|
+
const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote(file));
|
|
24883
24752
|
/**
|
|
24884
|
-
*
|
|
24885
|
-
*
|
|
24886
|
-
*
|
|
24887
|
-
*
|
|
24888
|
-
* the boundary commit itself: `commitHistory`'s range excludes `base`, and
|
|
24889
|
-
* `runOf` needs the boundary commit present in the read history
|
|
24890
|
-
* (`episode.base`, `startParentHash`). `base: undefined` means "read from the
|
|
24891
|
-
* repository root" — an empty repository, an unresolvable `head`, or an
|
|
24892
|
-
* episode that reaches the root commit, none of which cost anything extra
|
|
24893
|
-
* since that IS the whole history in those cases.
|
|
24894
|
-
*
|
|
24895
|
-
* `head` is the CONCRETE hash the literal `head` argument resolved to on this
|
|
24896
|
-
* call's first read — `undefined` only when it never resolved (an empty
|
|
24897
|
-
* repository, or an unresolvable ref). The caller must hand this hash, not
|
|
24898
|
-
* the original `head` argument, to its own `commitHistory` call: resolving a
|
|
24899
|
-
* symbolic ref (literal `HEAD`, most callers' default) a second time risks
|
|
24900
|
-
* reading a DIFFERENT commit than this function paged through, if something
|
|
24901
|
-
* moves HEAD in between — pinning both reads to one resolved hash is what
|
|
24902
|
-
* keeps the two-read split from disagreeing with itself.
|
|
24753
|
+
* The leading word of a rendered `format:`/`validate:` command, when the
|
|
24754
|
+
* command has exactly one unambiguous binary to probe — `undefined` for
|
|
24755
|
+
* anything a shell metacharacter could hide a second command behind. A missing
|
|
24756
|
+
* guard degrades to a raw exit; a wrong guard would refuse a working command.
|
|
24903
24757
|
*/
|
|
24904
|
-
const
|
|
24905
|
-
|
|
24906
|
-
|
|
24907
|
-
|
|
24908
|
-
|
|
24909
|
-
const wholeHistory = !Number.isFinite(pageSize) || page.length < pageSize;
|
|
24910
|
-
const boundaryIdx = boundaryIndexIn(def, page);
|
|
24911
|
-
if (boundaryIdx === void 0 || boundaryIdx === 0) {
|
|
24912
|
-
if (wholeHistory) return {
|
|
24913
|
-
base: void 0,
|
|
24914
|
-
head: pinnedHead
|
|
24915
|
-
};
|
|
24916
|
-
continue;
|
|
24917
|
-
}
|
|
24918
|
-
return {
|
|
24919
|
-
base: page[boundaryIdx - 1].hash,
|
|
24920
|
-
head: pinnedHead
|
|
24921
|
-
};
|
|
24922
|
-
}
|
|
24923
|
-
return {
|
|
24924
|
-
base: void 0,
|
|
24925
|
-
head: pinnedHead
|
|
24926
|
-
};
|
|
24927
|
-
});
|
|
24928
|
-
const historyUpTo = (git, def, head) => flatMap(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
24929
|
-
const computeProcessRun = (git, def, head) => flatMap(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
24930
|
-
const changedSinceStart = (pinned, current, workspace) => {
|
|
24931
|
-
if (pinned === void 0) return [];
|
|
24932
|
-
return changedConfigFiles(pinned.files, current).filter((path) => {
|
|
24933
|
-
const committed = workspace.readCommittedSync(path, pinned.commit);
|
|
24934
|
-
return current[path] === void 0 || committed === void 0 || contentHash(committed) !== current[path];
|
|
24935
|
-
});
|
|
24936
|
-
};
|
|
24937
|
-
const warnConfigChanges = (run, current, workspace) => gen(function* () {
|
|
24938
|
-
const changed = changedSinceStart(run.pinnedConfig, current, workspace);
|
|
24939
|
-
if (changed.length === 0) return;
|
|
24940
|
-
yield* (yield* Narrator).warn(`gtd: warning: the config changed since this process started (${changed.join(", ")}) — the rest of the process replays under the changed config`);
|
|
24941
|
-
});
|
|
24942
|
-
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
|
|
24943
|
-
const currentRun = gen(function* () {
|
|
24944
|
-
const git = yield* GitService;
|
|
24945
|
-
const config = yield* (yield* ConfigService).load;
|
|
24946
|
-
return yield* computeProcessRun(git, config.workflow);
|
|
24947
|
-
});
|
|
24948
|
-
const runAtTip = (git, def, tip) => gen(function* () {
|
|
24949
|
-
if (tip === void 0) {
|
|
24950
|
-
const history = yield* historyUpTo(git, def, void 0);
|
|
24951
|
-
return yield* runOf(history, locateEpisode(def, history));
|
|
24952
|
-
}
|
|
24953
|
-
if (!(parseCommitMessage(tip.subject).parsed?.to === def.initial)) {
|
|
24954
|
-
const history = yield* historyUpTo(git, def, tip.hash);
|
|
24955
|
-
return yield* runOf(history, locateEpisode(def, history));
|
|
24956
|
-
}
|
|
24957
|
-
const parentRef = `${tip.hash}~1`;
|
|
24958
|
-
const before = yield* historyUpTo(git, def, parentRef);
|
|
24959
|
-
const location = locateEpisode(def, before);
|
|
24960
|
-
let closing = yield* git.commitHistory(parentRef, tip.hash);
|
|
24961
|
-
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
24962
|
-
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
24963
|
-
});
|
|
24964
|
-
const runAtHead = (git, def) => gen(function* () {
|
|
24965
|
-
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
|
|
24966
|
-
});
|
|
24967
|
-
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
24968
|
-
const summaryRun = gen(function* () {
|
|
24969
|
-
const git = yield* GitService;
|
|
24970
|
-
const def = (yield* (yield* ConfigService).load).workflow;
|
|
24971
|
-
return yield* runAtHead(git, def);
|
|
24972
|
-
});
|
|
24973
|
-
const commitTree = (workspace, hash) => {
|
|
24974
|
-
let entries;
|
|
24975
|
-
const list = () => entries ??= workspace.treeSync(hash);
|
|
24976
|
-
let sortedPaths;
|
|
24977
|
-
const contents = /* @__PURE__ */ new Map();
|
|
24978
|
-
return {
|
|
24979
|
-
paths: () => sortedPaths ??= [...list().keys()].sort(),
|
|
24980
|
-
read: (path) => {
|
|
24981
|
-
if (!list().has(path)) return void 0;
|
|
24982
|
-
if (!contents.has(path)) contents.set(path, workspace.readCommittedSync(path, hash));
|
|
24983
|
-
return contents.get(path);
|
|
24984
|
-
},
|
|
24985
|
-
id: (path) => list().get(path)
|
|
24986
|
-
};
|
|
24758
|
+
const extractLeadingBinary = (command) => {
|
|
24759
|
+
if (command.includes("\n")) return void 0;
|
|
24760
|
+
if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return void 0;
|
|
24761
|
+
const match = SIMPLE_LEADING_WORD_RE.exec(command);
|
|
24762
|
+
return match === null ? void 0 : match[0];
|
|
24987
24763
|
};
|
|
24988
24764
|
/**
|
|
24989
|
-
*
|
|
24990
|
-
*
|
|
24991
|
-
*
|
|
24992
|
-
* field's own comment for why the memo lives there and not on `Workspace`.
|
|
24765
|
+
* Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
|
|
24766
|
+
* `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
|
|
24767
|
+
* resolves it in-process. Exits 127, matching bash's "command not found".
|
|
24993
24768
|
*/
|
|
24994
|
-
const
|
|
24995
|
-
const
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
25001
|
-
|
|
25002
|
-
};
|
|
25003
|
-
return {
|
|
25004
|
-
paths: () => paths,
|
|
25005
|
-
read: (path) => {
|
|
25006
|
-
if (!entries.has(path)) return void 0;
|
|
25007
|
-
const content = readWorktree(path);
|
|
25008
|
-
return content === void 0 || rewrite?.path !== path ? content : rewrite.apply(content);
|
|
25009
|
-
},
|
|
25010
|
-
id: (path) => rewrite?.path === path ? void 0 : entries.get(path)
|
|
25011
|
-
};
|
|
24769
|
+
const binaryGuard = (binary, mode, key) => {
|
|
24770
|
+
const messageQ = shellQuote(`gtd: mode "${mode}": "${key}" command not found: ${binary}`);
|
|
24771
|
+
return [
|
|
24772
|
+
`command -v ${shellQuote(binary)} >/dev/null 2>&1 || {`,
|
|
24773
|
+
` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
|
|
24774
|
+
` exit 127`,
|
|
24775
|
+
`}`
|
|
24776
|
+
].join("\n");
|
|
25012
24777
|
};
|
|
25013
24778
|
/**
|
|
25014
|
-
*
|
|
25015
|
-
*
|
|
25016
|
-
*
|
|
25017
|
-
* are always live. `startOverrides` are `--var` values, passed only while a
|
|
25018
|
-
* process is being started.
|
|
24779
|
+
* Prints `prompt` plus the captured output on a non-zero exit, then propagates
|
|
24780
|
+
* it. The `{ … }` group inlines a multi-line `command` verbatim; the
|
|
24781
|
+
* assignment sits left of `||` so `set -e` never trips on the failure itself.
|
|
25019
24782
|
*/
|
|
25020
|
-
const
|
|
25021
|
-
|
|
25022
|
-
|
|
25023
|
-
|
|
25024
|
-
|
|
25025
|
-
|
|
24783
|
+
const failurePromptWrapper = (command, prompt) => {
|
|
24784
|
+
return [
|
|
24785
|
+
`gtd_validate_status=0`,
|
|
24786
|
+
`gtd_validate_out="$( {`,
|
|
24787
|
+
command,
|
|
24788
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
24789
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
24790
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote(prompt)} "$gtd_validate_out"`,
|
|
24791
|
+
` exit "$gtd_validate_status"`,
|
|
24792
|
+
`fi`
|
|
24793
|
+
].join("\n");
|
|
24794
|
+
};
|
|
24795
|
+
const renderStep = (step) => {
|
|
24796
|
+
if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
|
|
24797
|
+
return step.command;
|
|
24798
|
+
};
|
|
24799
|
+
const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
|
|
24800
|
+
/** Build both halves from already-rendered command strings; an empty (or omitted) array produces the empty-string result, so a driver checking `if [ -n "$required" ]` never has to special-case a bare preamble. */
|
|
24801
|
+
const emitScripts = (required = [], optional = []) => ({
|
|
24802
|
+
required: assembleScript(required),
|
|
24803
|
+
optional: assembleScript(optional)
|
|
25026
24804
|
});
|
|
25027
|
-
/**
|
|
25028
|
-
const
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
|
|
25033
|
-
|
|
24805
|
+
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
24806
|
+
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
|
|
24807
|
+
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
24808
|
+
const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
|
|
24809
|
+
/**
|
|
24810
|
+
* A write command's single pasteable script (`gtd abandon | sh`,
|
|
24811
|
+
* `gtd land --json=script`'s own field). `optional`, when non-empty, is
|
|
24812
|
+
* wrapped in a subshell whose failure is swallowed — presentation-only, so it
|
|
24813
|
+
* must never turn a landed turn into a non-zero exit.
|
|
24814
|
+
*/
|
|
24815
|
+
const combinedScript = (required, optional) => {
|
|
24816
|
+
if (required.length === 0) return "";
|
|
24817
|
+
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
24818
|
+
return [
|
|
24819
|
+
DID_NOT_RUN_COMMENT,
|
|
24820
|
+
"",
|
|
24821
|
+
required,
|
|
24822
|
+
"",
|
|
24823
|
+
PRESENTATION_ONLY_COMMENT,
|
|
24824
|
+
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
24825
|
+
].join("\n");
|
|
25034
24826
|
};
|
|
24827
|
+
//#endregion
|
|
24828
|
+
//#region src/emit/OutcomeScript.ts
|
|
24829
|
+
/**
|
|
24830
|
+
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
24831
|
+
* tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
|
|
24832
|
+
* re-quoted here: a value reaches the script as a printf ARGUMENT, never
|
|
24833
|
+
* interpolated into the format itself, so a subject or state name containing
|
|
24834
|
+
* `%` can never be read as a conversion spec. Markers like `->` are arguments
|
|
24835
|
+
* too, keeping every format string free of a leading `-` that a shell's
|
|
24836
|
+
* `printf` could mistake for an option. A real newline in `fmt` is emitted as
|
|
24837
|
+
* the two-character `\n` escape `printf` itself expands, keeping every emitted
|
|
24838
|
+
* statement on one line.
|
|
24839
|
+
*/
|
|
24840
|
+
const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
24841
|
+
/**
|
|
24842
|
+
* Marks an emitted block as print-only. Load-bearing beyond documentation:
|
|
24843
|
+
* `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
|
|
24844
|
+
* this line, and a bare `printf` would be ambiguous with a workflow
|
|
24845
|
+
* `script:` command that happens to start the same way.
|
|
24846
|
+
*/
|
|
24847
|
+
const OUTCOME_MARKER = "# gtd: outcome (print-only)";
|
|
24848
|
+
const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
|
|
24849
|
+
const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\nEverything the process produced is kept as uncommitted changes (`git status`); discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.\n";
|
|
24850
|
+
const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
|
|
24851
|
+
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
24852
|
+
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
24853
|
+
/** One already-rendered plain line. */
|
|
24854
|
+
const noteOutcome = (text) => printfLine("%s\n", [shellQuote(text)]);
|
|
24855
|
+
/**
|
|
24856
|
+
* Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
|
|
24857
|
+
* — this line runs AFTER the reset that made `head` the new HEAD, so neither
|
|
24858
|
+
* value is knowable when gtd generates it.
|
|
24859
|
+
*/
|
|
24860
|
+
const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
|
|
24861
|
+
shellQuote(from),
|
|
24862
|
+
`"$(git rev-parse --short ${shellQuote(head)})"`,
|
|
24863
|
+
`"$(git log -1 --format=%s ${shellQuote(head)})"`,
|
|
24864
|
+
shellQuote(state)
|
|
24865
|
+
]);
|
|
24866
|
+
/** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
|
|
24867
|
+
const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
|
|
24868
|
+
/** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
|
|
24869
|
+
const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
|
|
24870
|
+
`"$(git rev-parse --short ${shellQuote(to)})"`,
|
|
24871
|
+
`"$(git log -1 --format=%s ${shellQuote(to)})"`,
|
|
24872
|
+
shellQuote(state)
|
|
24873
|
+
]);
|
|
24874
|
+
//#endregion
|
|
24875
|
+
//#region src/emit/ModeContradiction.ts
|
|
25035
24876
|
/**
|
|
25036
|
-
*
|
|
25037
|
-
*
|
|
25038
|
-
*
|
|
24877
|
+
* Written for two readers at once, since gtd validate's exit code carries no
|
|
24878
|
+
* second meaning to distinguish them: the agent (this is a CONFIGURATION BUG,
|
|
24879
|
+
* not the steering file — stop, don't edit it, or a fix-retry loop will
|
|
24880
|
+
* thrash on the document) and the human (which mode, and the exact rendered
|
|
24881
|
+
* `format:` command, to fix `gtd.config.ts` without reproducing the failure).
|
|
25039
24882
|
*/
|
|
25040
|
-
const
|
|
25041
|
-
const judgeBudgetBytes = (vars) => {
|
|
25042
|
-
const raw = vars.judgeBudgetBytes;
|
|
25043
|
-
if (raw === void 0) return DEFAULT_JUDGE_BUDGET_BYTES;
|
|
25044
|
-
const n = Number(raw);
|
|
25045
|
-
if (raw.trim() === "" || !Number.isInteger(n) || n <= 0) throw new Error(`"judgeBudgetBytes" must be a positive integer — got ${JSON.stringify(raw)} (blanking this var disables the payload bound rather than the mechanism it guards, so it is refused rather than defaulted)`);
|
|
25046
|
-
return n;
|
|
25047
|
-
};
|
|
25048
|
-
const replayFor = (setup, pending) => replay({
|
|
25049
|
-
flow: setup.def.flow,
|
|
25050
|
-
episode: {
|
|
25051
|
-
base: setup.base,
|
|
25052
|
-
commits: setup.episode
|
|
25053
|
-
},
|
|
25054
|
-
vars: setup.vars,
|
|
25055
|
-
env: setup.env,
|
|
25056
|
-
start: setup.run.diffBase,
|
|
25057
|
-
startTree: setup.run.diffBase === EMPTY_TREE$1 ? treeFromRecord({}) : commitTree(setup.workspace, setup.run.diffBase),
|
|
25058
|
-
budgetBytes: setup.budget,
|
|
25059
|
-
skills: setup.skills,
|
|
25060
|
-
configuredSkills: setup.def.configuredSkills,
|
|
25061
|
-
access: setup.access,
|
|
25062
|
-
configuredAccess: setup.def.configuredAccess,
|
|
25063
|
-
...pending !== void 0 ? { pending } : {}
|
|
25064
|
-
});
|
|
25065
|
-
const replayError = (outcome) => {
|
|
25066
|
-
if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
|
|
25067
|
-
if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
|
|
25068
|
-
if (outcome.kind === "ended") return /* @__PURE__ */ new Error("gtd: history ends the episode, but HEAD does not enter the default workflow's first step — the workflow changed under this process; run `gtd abandon` to start over");
|
|
25069
|
-
};
|
|
25070
|
-
const judgeDocument = (step) => step.request.kind === "judge" ? judgeJson(step.request.evidence, step.request.questions) : void 0;
|
|
24883
|
+
const contradictionMessage = (mode, formatCommand) => `gtd: mode "${mode}"'s format: command breaks its own validator — this is a CONFIGURATION BUG, not a problem with the steering file. Do NOT edit the steering file — stop and end your turn now.\nThe rendered format: command that ran:\n${formatCommand}\nWhat it turned mode "${mode}"'s own canonical sample into:`;
|
|
25071
24884
|
/**
|
|
25072
|
-
*
|
|
25073
|
-
*
|
|
25074
|
-
*
|
|
24885
|
+
* Formats a copy of the mode's own canonical sample and re-validates it with
|
|
24886
|
+
* `gtd check <mode>`, failing loudly when the round-trip breaks it — detects a
|
|
24887
|
+
* mode whose `format:` contradicts its own `validate:` without ever touching
|
|
24888
|
+
* the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
|
|
24889
|
+
* heredoc, since no delimiter chosen ahead of time can collide with the
|
|
24890
|
+
* sample's own bytes. One bash fragment with no blank lines (so
|
|
24891
|
+
* `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
|
|
24892
|
+
* to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
|
|
24893
|
+
* still fires at a first-write beat where the real steering file doesn't
|
|
24894
|
+
* exist yet.
|
|
25075
24895
|
*/
|
|
25076
|
-
const
|
|
25077
|
-
const
|
|
25078
|
-
const
|
|
25079
|
-
const
|
|
25080
|
-
|
|
25081
|
-
|
|
25082
|
-
|
|
25083
|
-
|
|
25084
|
-
|
|
25085
|
-
|
|
25086
|
-
|
|
25087
|
-
|
|
25088
|
-
|
|
25089
|
-
|
|
25090
|
-
|
|
25091
|
-
content: request.prompt,
|
|
25092
|
-
...optional("model", request.options.model),
|
|
25093
|
-
...optional("system", request.options.system),
|
|
25094
|
-
...optional("allowEmpty", request.options.allowEmpty),
|
|
25095
|
-
...optional("keep", request.options.keep),
|
|
25096
|
-
...optional("skills", skills),
|
|
25097
|
-
...optional("access", access)
|
|
25098
|
-
});
|
|
25099
|
-
const scriptDef = (common, request) => ({
|
|
25100
|
-
...common,
|
|
25101
|
-
kind: "script",
|
|
25102
|
-
content: request.body
|
|
25103
|
-
});
|
|
25104
|
-
const judgeDef = (common, request, step) => {
|
|
25105
|
-
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
25106
|
-
return {
|
|
25107
|
-
...common,
|
|
25108
|
-
kind: "message",
|
|
25109
|
-
content: step.truncated ? `${message}\n\n${TRUNCATION_NOTICE}` : message,
|
|
25110
|
-
...optional("judge", judgeDocument(step))
|
|
25111
|
-
};
|
|
24896
|
+
const buildModeContradictionCheck = (inputs) => {
|
|
24897
|
+
const { mode, samplePath, sample, formatCommand } = inputs;
|
|
24898
|
+
const pathQ = shellQuote(samplePath);
|
|
24899
|
+
const messageQ = shellQuote(contradictionMessage(mode, formatCommand));
|
|
24900
|
+
return [
|
|
24901
|
+
`printf '%s' ${shellQuote(sample)} > ${pathQ}`,
|
|
24902
|
+
formatCommand,
|
|
24903
|
+
`gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
|
|
24904
|
+
` printf '%s\\n' ${messageQ} >&2`,
|
|
24905
|
+
` cat ${pathQ} >&2`,
|
|
24906
|
+
` rm -f ${pathQ}`,
|
|
24907
|
+
` exit 1`,
|
|
24908
|
+
`}`,
|
|
24909
|
+
`rm -f ${pathQ}`
|
|
24910
|
+
].join("\n");
|
|
25112
24911
|
};
|
|
25113
|
-
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
const request = step.request;
|
|
25122
|
-
const common = commonOf(step);
|
|
25123
|
-
switch (request.kind) {
|
|
25124
|
-
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
25125
|
-
case "run": return scriptDef(common, request);
|
|
25126
|
-
case "judge": return judgeDef(common, request, step);
|
|
25127
|
-
case "human": return messageDef(common, request, step);
|
|
25128
|
-
}
|
|
24912
|
+
/**
|
|
24913
|
+
* Skip notice for a mode whose validator is EXTERNAL (a user `validate:`
|
|
24914
|
+
* command, not one of the two built-in modes) — there's no in-process parser
|
|
24915
|
+
* to round-trip a sample through. Printed to stderr since silence would read
|
|
24916
|
+
* as a clean bill of health.
|
|
24917
|
+
*/
|
|
24918
|
+
const modeContradictionSkipNotice = (mode) => {
|
|
24919
|
+
return `printf '%s\\n' ${shellQuote(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
|
|
25129
24920
|
};
|
|
25130
|
-
|
|
24921
|
+
//#endregion
|
|
24922
|
+
//#region src/emit/SteeringMode.ts
|
|
25131
24923
|
/**
|
|
25132
|
-
*
|
|
25133
|
-
*
|
|
25134
|
-
*
|
|
25135
|
-
*
|
|
24924
|
+
* `format` alone (no live/external verdict) when there's no validator at
|
|
24925
|
+
* all; a live in-process validator for a `"builtin"` validate OR a command
|
|
24926
|
+
* that is exactly the format's own seeded string; `externalValidate` for any
|
|
24927
|
+
* other command. Each arm returns directly — a flat sequence, not nested
|
|
24928
|
+
* ternaries — so the four outcomes stay each their own case to read, not a
|
|
24929
|
+
* boolean expression to re-derive.
|
|
25136
24930
|
*/
|
|
25137
|
-
const
|
|
25138
|
-
|
|
25139
|
-
if (
|
|
25140
|
-
|
|
25141
|
-
|
|
24931
|
+
const capabilitiesFor = (mode, format, validate) => {
|
|
24932
|
+
if (format === void 0) return validate?.kind === "command" ? { externalValidate: true } : {};
|
|
24933
|
+
if (validate?.kind === "builtin") return {
|
|
24934
|
+
format,
|
|
24935
|
+
liveValidate: format.validate
|
|
24936
|
+
};
|
|
24937
|
+
if (validate === void 0) return { format };
|
|
24938
|
+
if (isSeededValidateCommand(mode, validate.command)) return {
|
|
24939
|
+
format,
|
|
24940
|
+
liveValidate: format.validate
|
|
25142
24941
|
};
|
|
25143
|
-
const start = scopeRunStart(trace, rest.memoryScope);
|
|
25144
|
-
const token = start <= 0 ? run.startParentHash : trace[start - 1].enteredAt;
|
|
25145
|
-
const resumed = trace.slice(Math.max(start, 0), trace.length - 1).some((s) => s.kind === "agent" && s.memoryScope === rest.memoryScope);
|
|
25146
24942
|
return {
|
|
25147
|
-
|
|
25148
|
-
|
|
24943
|
+
format,
|
|
24944
|
+
externalValidate: true
|
|
25149
24945
|
};
|
|
25150
24946
|
};
|
|
25151
|
-
|
|
25152
|
-
|
|
25153
|
-
|
|
25154
|
-
|
|
25155
|
-
|
|
25156
|
-
|
|
25157
|
-
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
25161
|
-
|
|
25162
|
-
|
|
25163
|
-
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
25169
|
-
|
|
25170
|
-
|
|
25171
|
-
|
|
25172
|
-
|
|
25173
|
-
byModel: costByModel(entries)
|
|
25174
|
-
});
|
|
25175
|
-
const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(function* () {
|
|
25176
|
-
const resolved = yield* workflowOf(config, run);
|
|
25177
|
-
const def = resolved.def;
|
|
25178
|
-
const startFlags = run.trace.length === 0 ? yield* StartFlags : {};
|
|
25179
|
-
const { vars, env } = settingsFor(resolved, run.pinnedVars, host.env, startOverrides, startFlags);
|
|
25180
|
-
const skills = yield* resolveScopeSkills(def, vars);
|
|
25181
|
-
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
25182
|
-
const budget = yield* try_({
|
|
25183
|
-
try: () => judgeBudgetBytes(vars),
|
|
25184
|
-
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
25185
|
-
});
|
|
25186
|
-
yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
|
|
25187
|
-
const base = run.episode.base === void 0 ? {
|
|
25188
|
-
hash: EMPTY_TREE$1,
|
|
25189
|
-
tree: treeFromRecord({})
|
|
25190
|
-
} : {
|
|
25191
|
-
hash: run.episode.base,
|
|
25192
|
-
tree: commitTree(workspace, run.episode.base)
|
|
25193
|
-
};
|
|
25194
|
-
const git = yield* GitService;
|
|
25195
|
-
const episode = [];
|
|
25196
|
-
for (const c of run.episode.commits) {
|
|
25197
|
-
const tip = parseCommitMessage(c.message).kept;
|
|
25198
|
-
const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
|
|
25199
|
-
episode.push({
|
|
25200
|
-
hash: c.hash,
|
|
25201
|
-
message: c.message,
|
|
25202
|
-
tree: commitTree(workspace, c.hash),
|
|
25203
|
-
...exists ? { kept: {
|
|
25204
|
-
hash: tip,
|
|
25205
|
-
tree: commitTree(workspace, tip)
|
|
25206
|
-
} } : {}
|
|
25207
|
-
});
|
|
25208
|
-
}
|
|
25209
|
-
let pendingMemo;
|
|
25210
|
-
return {
|
|
25211
|
-
resolved,
|
|
25212
|
-
def,
|
|
25213
|
-
vars,
|
|
25214
|
-
env,
|
|
25215
|
-
setup: {
|
|
25216
|
-
def,
|
|
25217
|
-
run,
|
|
25218
|
-
vars,
|
|
25219
|
-
env,
|
|
25220
|
-
budget,
|
|
25221
|
-
skills,
|
|
25222
|
-
access,
|
|
25223
|
-
workspace,
|
|
25224
|
-
base,
|
|
25225
|
-
episode,
|
|
25226
|
-
pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
|
|
25227
|
-
}
|
|
24947
|
+
/**
|
|
24948
|
+
* Resolve a `mode:` name against `def.modes` plus the built-in registry
|
|
24949
|
+
* (`src/workflow/SteeringFormats.ts`) — half by half: a declared `format:`/`validate:`
|
|
24950
|
+
* wins, and an undeclared `validate:` falls back to the built-in format's own
|
|
24951
|
+
* parser when the name is registered. `format` is set from the registry
|
|
24952
|
+
* ALONE, independent of which half of `validate` wins — a declared validator
|
|
24953
|
+
* overrides validation, not the format identity.
|
|
24954
|
+
*
|
|
24955
|
+
* `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
|
|
24956
|
+
* against the registry alone, with no workflow definition in hand (e.g. the
|
|
24957
|
+
* basename dispatch for a `REVIEW.md` no config maps).
|
|
24958
|
+
*
|
|
24959
|
+
* The `"unknown"` arm carries the whole refusal message, naming what IS known
|
|
24960
|
+
* when `def` is given — this is the one place that message is built, so it
|
|
24961
|
+
* can never drift from what actually resolved.
|
|
24962
|
+
*/
|
|
24963
|
+
const resolveMode = (def, state, mode) => {
|
|
24964
|
+
const declared = def?.modes?.[mode];
|
|
24965
|
+
const format = steeringFormatFor(mode);
|
|
24966
|
+
if (declared === void 0 && format === void 0) return {
|
|
24967
|
+
kind: "unknown",
|
|
24968
|
+
message: def !== void 0 ? `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})` : `mode "${mode}" is not a built-in steering format`
|
|
25228
24969
|
};
|
|
25229
|
-
|
|
25230
|
-
|
|
25231
|
-
|
|
25232
|
-
|
|
25233
|
-
|
|
25234
|
-
|
|
25235
|
-
}
|
|
25236
|
-
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
25237
|
-
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
25238
|
-
const git = yield* GitService;
|
|
25239
|
-
const config = yield* (yield* ConfigService).load;
|
|
25240
|
-
const workspace = yield* Workspace;
|
|
25241
|
-
const host = yield* Host;
|
|
25242
|
-
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
25243
|
-
const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
|
|
25244
|
-
const outcome = yield* promise(() => replayFor(setup));
|
|
25245
|
-
const error = replayError(outcome);
|
|
25246
|
-
if (error !== void 0 || outcome.kind !== "rest") return yield* fail$4(error ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
|
|
25247
|
-
const step = outcome.rest;
|
|
25248
|
-
yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
|
|
25249
|
-
const stepDef = stepDefOf(step);
|
|
25250
|
-
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$4(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
25251
|
-
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
25252
|
-
status: normalizeStatus(e.status),
|
|
25253
|
-
path: e.path
|
|
25254
|
-
})) : [];
|
|
25255
|
-
const memory = memoryOf(outcome.trace, run);
|
|
25256
|
-
const configNow = config.fingerprint;
|
|
25257
|
-
if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
|
|
24970
|
+
const validate = declared?.validate !== void 0 ? {
|
|
24971
|
+
kind: "command",
|
|
24972
|
+
command: declared.validate
|
|
24973
|
+
} : format !== void 0 ? {
|
|
24974
|
+
kind: "builtin",
|
|
24975
|
+
format
|
|
24976
|
+
} : void 0;
|
|
25258
24977
|
return {
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
trace: outcome.trace,
|
|
25266
|
-
run,
|
|
25267
|
-
vars,
|
|
25268
|
-
env,
|
|
25269
|
-
config: configNow,
|
|
25270
|
-
changes,
|
|
25271
|
-
memory: memory.key,
|
|
25272
|
-
memoryResumed: memory.resumed,
|
|
25273
|
-
hints: hintsOf(stepDef),
|
|
25274
|
-
cost: costTotals(run.costEntries),
|
|
25275
|
-
setup
|
|
24978
|
+
kind: "resolved",
|
|
24979
|
+
mode,
|
|
24980
|
+
...format !== void 0 ? { format } : {},
|
|
24981
|
+
...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
|
|
24982
|
+
...validate !== void 0 ? { validate } : {},
|
|
24983
|
+
capabilities: capabilitiesFor(mode, format, validate)
|
|
25276
24984
|
};
|
|
24985
|
+
};
|
|
24986
|
+
/** `gtd next`'s fix-retry instruction, printed ahead of the failing command's captured output. */
|
|
24987
|
+
const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
|
|
24988
|
+
/**
|
|
24989
|
+
* Pushes a `binaryGuard` step ahead of `command` when its own leading word
|
|
24990
|
+
* is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
|
|
24991
|
+
* the script on its own `exit 127`, without touching `failurePromptWrapper`'s
|
|
24992
|
+
* captured-output path. Each command guards on its OWN leading word — a
|
|
24993
|
+
* guard naming the wrong binary is worse than none.
|
|
24994
|
+
*/
|
|
24995
|
+
const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
|
|
24996
|
+
const binary = extractLeadingBinary(command);
|
|
24997
|
+
if (binary !== void 0) steps.push({
|
|
24998
|
+
kind: "command",
|
|
24999
|
+
command: binaryGuard(binary, mode, key)
|
|
25000
|
+
});
|
|
25001
|
+
steps.push({
|
|
25002
|
+
kind: "command",
|
|
25003
|
+
command: withFileVar(command, file),
|
|
25004
|
+
...onFailure !== void 0 ? { onFailure } : {}
|
|
25005
|
+
});
|
|
25006
|
+
};
|
|
25007
|
+
/**
|
|
25008
|
+
* `resolved`'s `format:`/`validate:` as `EmitStep[]` — format first,
|
|
25009
|
+
* validate last, the last one wrapped with `onFailure` when the validator is
|
|
25010
|
+
* a command (a built-in's own in-process parser has no shell step to wrap:
|
|
25011
|
+
* `gtd check`'s seeded command carries that instead). Each command gets its
|
|
25012
|
+
* own `binaryGuard` immediately ahead of it.
|
|
25013
|
+
*/
|
|
25014
|
+
const modeCommandSteps = (resolved, file) => {
|
|
25015
|
+
const steps = [];
|
|
25016
|
+
if (resolved.formatCommand !== void 0) pushGuardedCommand(steps, resolved.mode, "format", resolved.formatCommand, file, void 0);
|
|
25017
|
+
if (resolved.validate?.kind === "command") pushGuardedCommand(steps, resolved.mode, "validate", resolved.validate.command, file, fixPromptInstruction(file));
|
|
25018
|
+
return steps;
|
|
25019
|
+
};
|
|
25020
|
+
/** `<Host.scratchDir>/gtd-mode-sample-<mode>-<pid>.md` — an absolute literal baked in at emit time, never a shell variable. `<pid>` avoids collisions between concurrent `gtd` processes. Never a `/tmp` literal or `mktemp` (`tests/tooling/no-tmp-assumption.test.ts` scans for both). */
|
|
25021
|
+
const scratchSamplePath = (scratchDir, mode) => join(scratchDir, `gtd-mode-sample-${mode}-${process.pid}.md`);
|
|
25022
|
+
/**
|
|
25023
|
+
* The contradiction round-trip/skip-notice steps for `resolved`'s `mode:`.
|
|
25024
|
+
* No `formatCommand` means nothing to round-trip: empty. Otherwise: a live
|
|
25025
|
+
* built-in validator runs the round-trip against that format's own canonical
|
|
25026
|
+
* sample; an external validator prints a one-line skip notice instead, since
|
|
25027
|
+
* silence there would read as a clean bill of health; a format-only mode with
|
|
25028
|
+
* neither has nothing to round-trip either.
|
|
25029
|
+
*/
|
|
25030
|
+
const modeContradictionSteps = (resolved) => gen(function* () {
|
|
25031
|
+
if (resolved.formatCommand === void 0) return [];
|
|
25032
|
+
const { capabilities } = resolved;
|
|
25033
|
+
if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
|
|
25034
|
+
const { scratchDir } = yield* Host;
|
|
25035
|
+
const samplePath = scratchSamplePath(scratchDir, resolved.mode);
|
|
25036
|
+
const formatCommand = withFileVar(resolved.formatCommand, samplePath);
|
|
25037
|
+
return [{
|
|
25038
|
+
kind: "command",
|
|
25039
|
+
command: buildModeContradictionCheck({
|
|
25040
|
+
mode: resolved.mode,
|
|
25041
|
+
samplePath,
|
|
25042
|
+
sample: capabilities.format.sample,
|
|
25043
|
+
formatCommand
|
|
25044
|
+
})
|
|
25045
|
+
}];
|
|
25046
|
+
}
|
|
25047
|
+
if (capabilities.externalValidate === true) return [{
|
|
25048
|
+
kind: "command",
|
|
25049
|
+
command: modeContradictionSkipNotice(resolved.mode)
|
|
25050
|
+
}];
|
|
25051
|
+
return [];
|
|
25277
25052
|
});
|
|
25278
|
-
const currentRest = restAt(void 0);
|
|
25279
25053
|
/**
|
|
25280
|
-
*
|
|
25281
|
-
*
|
|
25282
|
-
*
|
|
25054
|
+
* Renders `resolved`'s complete validate script for `file`: the mode's
|
|
25055
|
+
* format/validate contradiction check, a guard for a not-yet-written file,
|
|
25056
|
+
* then the format/validate commands themselves (format first). gtd never
|
|
25057
|
+
* runs this script itself — `resolveMode`+`validateScriptFor` only render
|
|
25058
|
+
* text for a driver to execute (`gtd renders; the driver executes`).
|
|
25283
25059
|
*/
|
|
25284
|
-
const
|
|
25285
|
-
|
|
25286
|
-
|
|
25287
|
-
|
|
25288
|
-
|
|
25289
|
-
|
|
25290
|
-
const outcome = yield* promise(() => replay({
|
|
25291
|
-
flow: resolved.def.flow,
|
|
25292
|
-
episode: {
|
|
25293
|
-
base: {
|
|
25294
|
-
hash: "",
|
|
25295
|
-
tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
|
|
25296
|
-
},
|
|
25297
|
-
commits: []
|
|
25060
|
+
const validateScriptFor = (resolved, file) => gen(function* () {
|
|
25061
|
+
return { script: emitScripts([
|
|
25062
|
+
...yield* modeContradictionSteps(resolved),
|
|
25063
|
+
{
|
|
25064
|
+
kind: "command",
|
|
25065
|
+
command: fileExistsGuard(file)
|
|
25298
25066
|
},
|
|
25299
|
-
|
|
25300
|
-
|
|
25301
|
-
start: "",
|
|
25302
|
-
budgetBytes: rest.setup.budget,
|
|
25303
|
-
skills,
|
|
25304
|
-
configuredSkills: resolved.def.configuredSkills,
|
|
25305
|
-
access,
|
|
25306
|
-
configuredAccess: resolved.def.configuredAccess
|
|
25307
|
-
}));
|
|
25308
|
-
if (outcome.kind === "rest") return { firstStep: outcome.rest.name };
|
|
25309
|
-
if (outcome.kind === "refused") return { refusal: outcome.message };
|
|
25310
|
-
const why = outcome.kind === "ended" ? "the flow reaches no step" : outcome.message;
|
|
25311
|
-
return { refusal: `workflow "${resolved.name}" cannot start — ${why}` };
|
|
25312
|
-
});
|
|
25313
|
-
/** The review window's diff base at the rest. */
|
|
25314
|
-
const reviewBaseFor = (rest) => rest.step.request.options.base ?? rest.run.diffBase;
|
|
25315
|
-
const renderRest = (rest) => succeed$1({
|
|
25316
|
-
state: rest.state,
|
|
25317
|
-
actor: rest.actor,
|
|
25318
|
-
kind: rest.stepDef.kind,
|
|
25319
|
-
content: rest.stepDef.content,
|
|
25320
|
-
...rest.hints,
|
|
25321
|
-
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
25322
|
-
memoryResumed: rest.memoryResumed
|
|
25067
|
+
...modeCommandSteps(resolved, file)
|
|
25068
|
+
]).required };
|
|
25323
25069
|
});
|
|
25070
|
+
//#endregion
|
|
25071
|
+
//#region src/step/planStep.ts
|
|
25324
25072
|
/**
|
|
25325
|
-
*
|
|
25326
|
-
*
|
|
25073
|
+
* At the human review gate an unconditional `gtd uncheck '<file>'` runs
|
|
25074
|
+
* ahead of the commit, resetting every checkbox before `git add -A` picks it
|
|
25075
|
+
* up — a tick is read-progress, never sign-off.
|
|
25327
25076
|
*/
|
|
25328
|
-
const
|
|
25329
|
-
|
|
25330
|
-
|
|
25331
|
-
|
|
25332
|
-
const
|
|
25333
|
-
|
|
25334
|
-
|
|
25335
|
-
|
|
25336
|
-
|
|
25337
|
-
if (rest.changes.length > 0) return {
|
|
25338
|
-
kind: "refusal",
|
|
25339
|
-
message: `gtd land: approval-gate: ${formatStepId(id)} approves exactly the judged diff — revert your edits and run \`gtd land\` on a clean tree to approve, or run \`gtd abandon\``
|
|
25077
|
+
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
25078
|
+
kind: "uncheck",
|
|
25079
|
+
file: snapshot.file
|
|
25080
|
+
}] : [];
|
|
25081
|
+
const gitWriteOf = (landing, message) => {
|
|
25082
|
+
if (landing.only !== void 0) return {
|
|
25083
|
+
kind: "commitPath",
|
|
25084
|
+
path: landing.only,
|
|
25085
|
+
message
|
|
25340
25086
|
};
|
|
25341
|
-
if (
|
|
25342
|
-
kind: "
|
|
25343
|
-
|
|
25087
|
+
if (landing.keepTurn !== void 0) return {
|
|
25088
|
+
kind: "keepTurn",
|
|
25089
|
+
ref: landing.keepTurn,
|
|
25090
|
+
message
|
|
25344
25091
|
};
|
|
25345
|
-
if (
|
|
25346
|
-
|
|
25347
|
-
|
|
25348
|
-
}
|
|
25349
|
-
|
|
25350
|
-
|
|
25092
|
+
if (landing.spec.approved !== void 0) return {
|
|
25093
|
+
kind: "commitClean",
|
|
25094
|
+
message
|
|
25095
|
+
};
|
|
25096
|
+
return {
|
|
25097
|
+
kind: "commitAll",
|
|
25098
|
+
message
|
|
25351
25099
|
};
|
|
25352
25100
|
};
|
|
25353
|
-
|
|
25354
|
-
const
|
|
25355
|
-
if (
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
kind: "attempt",
|
|
25359
|
-
subject: formatSubject(rest.actor, rest.state)
|
|
25101
|
+
const planStep = (snapshot, opts = {}) => {
|
|
25102
|
+
const landing = snapshot.landing;
|
|
25103
|
+
if (landing.kind === "refusal") return {
|
|
25104
|
+
kind: "refusal",
|
|
25105
|
+
message: landing.message
|
|
25360
25106
|
};
|
|
25361
|
-
if (
|
|
25107
|
+
if (landing.kind === "noop") return {
|
|
25362
25108
|
kind: "noop",
|
|
25363
|
-
|
|
25109
|
+
state: snapshot.state,
|
|
25110
|
+
settled: landing.settled
|
|
25111
|
+
};
|
|
25112
|
+
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
25113
|
+
cost: opts.cost,
|
|
25114
|
+
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
25115
|
+
} };
|
|
25116
|
+
if (landing.kind === "attempt") {
|
|
25117
|
+
const message = formatCommitMessage({
|
|
25118
|
+
actor: snapshot.actor,
|
|
25119
|
+
to: snapshot.state,
|
|
25120
|
+
...cost
|
|
25121
|
+
});
|
|
25122
|
+
return {
|
|
25123
|
+
kind: "commit",
|
|
25124
|
+
state: snapshot.state,
|
|
25125
|
+
to: snapshot.state,
|
|
25126
|
+
subject: landing.subject,
|
|
25127
|
+
steps: [{
|
|
25128
|
+
kind: "gitWrite",
|
|
25129
|
+
write: {
|
|
25130
|
+
kind: "commitAll",
|
|
25131
|
+
message
|
|
25132
|
+
}
|
|
25133
|
+
}, {
|
|
25134
|
+
kind: "outcome",
|
|
25135
|
+
outcome: {
|
|
25136
|
+
kind: "commit",
|
|
25137
|
+
subject: landing.subject
|
|
25138
|
+
}
|
|
25139
|
+
}]
|
|
25140
|
+
};
|
|
25141
|
+
}
|
|
25142
|
+
const message = formatCommitMessage({
|
|
25143
|
+
...landing.spec,
|
|
25144
|
+
...cost,
|
|
25145
|
+
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
25146
|
+
});
|
|
25147
|
+
const subject = message.split("\n")[0];
|
|
25148
|
+
const outcome = landing.to === snapshot.state ? {
|
|
25149
|
+
kind: "outcome",
|
|
25150
|
+
outcome: {
|
|
25151
|
+
kind: "commit",
|
|
25152
|
+
subject
|
|
25153
|
+
}
|
|
25154
|
+
} : {
|
|
25155
|
+
kind: "outcome",
|
|
25156
|
+
outcome: {
|
|
25157
|
+
kind: "transition",
|
|
25158
|
+
from: snapshot.state,
|
|
25159
|
+
to: landing.to
|
|
25160
|
+
}
|
|
25364
25161
|
};
|
|
25365
|
-
};
|
|
25366
|
-
/** At the human review gate the pending tree lands with every tick cleared, as the landing script will. */
|
|
25367
|
-
const reviewGateRewrite = (def) => {
|
|
25368
|
-
const reviewFormat = steeringFormatFor("review");
|
|
25369
|
-
if (!isHumanReviewGate(def) || def.file === void 0 || reviewFormat === void 0) return;
|
|
25370
25162
|
return {
|
|
25371
|
-
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25163
|
+
kind: "commit",
|
|
25164
|
+
state: snapshot.state,
|
|
25165
|
+
to: landing.to,
|
|
25166
|
+
subject,
|
|
25167
|
+
steps: [
|
|
25168
|
+
...uncheckStep(snapshot),
|
|
25169
|
+
{
|
|
25170
|
+
kind: "gitWrite",
|
|
25171
|
+
write: gitWriteOf(landing, message)
|
|
25172
|
+
},
|
|
25173
|
+
...landing.keepHistory !== void 0 ? [{
|
|
25174
|
+
kind: "gitWrite",
|
|
25175
|
+
write: {
|
|
25176
|
+
kind: "updateRef",
|
|
25177
|
+
ref: landing.keepHistory,
|
|
25178
|
+
target: "HEAD"
|
|
25179
|
+
}
|
|
25180
|
+
}] : [],
|
|
25181
|
+
outcome
|
|
25182
|
+
]
|
|
25380
25183
|
};
|
|
25381
25184
|
};
|
|
25382
|
-
|
|
25383
|
-
|
|
25384
|
-
kind: "refusal",
|
|
25385
|
-
message: outcome.message
|
|
25386
|
-
});
|
|
25387
|
-
if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$4(new Error(outcome.message));
|
|
25388
|
-
if (outcome.kind !== "rest") return succeed$1(rest.def.initial);
|
|
25389
|
-
const reserved = initialStepRefusal(rest.run.workflow, outcome.rest.name, rest.def.initial);
|
|
25390
|
-
return succeed$1(reserved === void 0 ? outcome.rest.name : reserved);
|
|
25391
|
-
};
|
|
25185
|
+
//#endregion
|
|
25186
|
+
//#region src/step/accessRefusal.ts
|
|
25392
25187
|
/**
|
|
25393
|
-
*
|
|
25394
|
-
*
|
|
25395
|
-
*
|
|
25188
|
+
* The landing refusal for changes outside `write`, or `undefined` when every
|
|
25189
|
+
* path is allowed. `null` is unrestricted. The message is the fix prompt: gtd
|
|
25190
|
+
* runs no fix turn of its own.
|
|
25396
25191
|
*/
|
|
25397
|
-
const
|
|
25192
|
+
const accessRefusal = (changes, write) => {
|
|
25193
|
+
if (write === null) return void 0;
|
|
25194
|
+
const offending = [...new Set(changes.filter((c) => !write.some((glob) => globMatches(c.path, glob))).map((c) => c.path))];
|
|
25195
|
+
if (offending.length === 0) return void 0;
|
|
25196
|
+
return [
|
|
25197
|
+
ACCESS_REFUSAL,
|
|
25198
|
+
...offending.map((path) => ` - ${path}`),
|
|
25199
|
+
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
25200
|
+
].join("\n");
|
|
25201
|
+
};
|
|
25202
|
+
//#endregion
|
|
25203
|
+
//#region src/step/planStart.ts
|
|
25204
|
+
const refusal = (message) => ({
|
|
25398
25205
|
kind: "refusal",
|
|
25399
|
-
message
|
|
25400
|
-
} : void 0;
|
|
25401
|
-
const closingTrailers = (branch) => ({
|
|
25402
|
-
version: GTD_VERSION,
|
|
25403
|
-
...isSome(branch) ? { branch: branch.value } : {}
|
|
25206
|
+
message
|
|
25404
25207
|
});
|
|
25405
|
-
const
|
|
25406
|
-
const
|
|
25407
|
-
|
|
25408
|
-
|
|
25409
|
-
|
|
25410
|
-
}
|
|
25411
|
-
|
|
25412
|
-
|
|
25413
|
-
|
|
25414
|
-
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
const
|
|
25208
|
+
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
25209
|
+
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
25210
|
+
if (environmentOnly.length > 0) return yield* fail$4(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it as a workflow option in gtd.config.ts or with a GTD_<NAME> environment variable`));
|
|
25211
|
+
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
25212
|
+
if (undeclared.length > 0) return yield* fail$4(new GtdUsageError(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`));
|
|
25213
|
+
});
|
|
25214
|
+
/**
|
|
25215
|
+
* `gtd start <name>`: start a brand NEW process on a workflow, writing an
|
|
25216
|
+
* opening commit that names the workflow's first step and carries a
|
|
25217
|
+
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
25218
|
+
* and a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base.
|
|
25219
|
+
* All validation is a REFUSAL, not an Effect failure.
|
|
25220
|
+
*/
|
|
25221
|
+
const planStart = (current, actor, start) => gen(function* () {
|
|
25222
|
+
const { workflow, first, commandLabel, vars: varOverrides } = start;
|
|
25223
|
+
const name = workflow.name;
|
|
25224
|
+
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before starting another`);
|
|
25225
|
+
if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`);
|
|
25226
|
+
const { firstStep } = first;
|
|
25227
|
+
yield* checkOverrides(commandLabel, varOverrides, Object.keys(workflow.vars), Object.keys(workflow.env));
|
|
25228
|
+
const baseOf = workflow.def.base;
|
|
25229
|
+
const flags = yield* StartFlags;
|
|
25230
|
+
const hostEnv = (yield* Host).env;
|
|
25231
|
+
const badFlag = ownFlagRefusal(workflow.vars, flags, hostEnv);
|
|
25232
|
+
if (badFlag !== void 0) return refusal(`${commandLabel}: ${badFlag}`);
|
|
25233
|
+
const vars = resolveStartVars(workflow.vars, flags, varOverrides, hostEnv);
|
|
25234
|
+
const multiline = multilineSetting(vars);
|
|
25235
|
+
if (multiline !== void 0) return refusal(`${commandLabel}: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`);
|
|
25236
|
+
const template = yield* try_({
|
|
25237
|
+
try: () => baseOf?.(name, vars),
|
|
25238
|
+
catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
|
|
25239
|
+
});
|
|
25240
|
+
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
25241
|
+
if (typeof base === "object") return base;
|
|
25242
|
+
const subject = formatSubject(actor, firstStep);
|
|
25421
25243
|
return {
|
|
25422
|
-
kind: "
|
|
25423
|
-
|
|
25424
|
-
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
|
|
25435
|
-
|
|
25244
|
+
kind: "start",
|
|
25245
|
+
workflow: name,
|
|
25246
|
+
subject,
|
|
25247
|
+
steps: [{
|
|
25248
|
+
kind: "gitWrite",
|
|
25249
|
+
write: {
|
|
25250
|
+
kind: "commitAll",
|
|
25251
|
+
message: formatCommitMessage({
|
|
25252
|
+
actor,
|
|
25253
|
+
to: firstStep,
|
|
25254
|
+
workflow: name,
|
|
25255
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
25256
|
+
vars
|
|
25257
|
+
})
|
|
25258
|
+
}
|
|
25259
|
+
}, {
|
|
25260
|
+
kind: "outcome",
|
|
25261
|
+
outcome: {
|
|
25262
|
+
kind: "commit",
|
|
25263
|
+
subject
|
|
25264
|
+
}
|
|
25265
|
+
}]
|
|
25436
25266
|
};
|
|
25437
|
-
};
|
|
25438
|
-
const startTrailers = (rest, starts) => starts ? {
|
|
25439
|
-
vars: rest.vars,
|
|
25440
|
-
config: rest.config
|
|
25441
|
-
} : {};
|
|
25442
|
-
const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "ended" ? outcome.marks : [];
|
|
25267
|
+
});
|
|
25443
25268
|
/**
|
|
25444
|
-
*
|
|
25445
|
-
*
|
|
25446
|
-
*
|
|
25269
|
+
* The workflow's diff base: `git merge-base <base> HEAD`, where a blank base
|
|
25270
|
+
* means the default branch. The merge-base is what gets pinned, so a base on
|
|
25271
|
+
* another branch reviews exactly this branch's own commits.
|
|
25447
25272
|
*/
|
|
25448
|
-
const
|
|
25449
|
-
const unapproved = approvalRefusal(rest, approver);
|
|
25450
|
-
if (unapproved !== void 0) return unapproved;
|
|
25451
|
-
const early = cleanLanding(rest);
|
|
25452
|
-
if (early !== void 0) return early;
|
|
25453
|
-
if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
|
|
25454
|
-
const message = accessRefusal(rest.changes, rest.stepDef.access.write);
|
|
25455
|
-
if (message !== void 0) return {
|
|
25456
|
-
kind: "refusal",
|
|
25457
|
-
message
|
|
25458
|
-
};
|
|
25459
|
-
}
|
|
25460
|
-
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
25461
|
-
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
25462
|
-
...verdicts !== void 0 ? { verdicts } : {}
|
|
25463
|
-
}));
|
|
25464
|
-
const to = yield* landingTarget(outcome, rest);
|
|
25465
|
-
if (typeof to !== "string") return to;
|
|
25466
|
-
return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
25467
|
-
});
|
|
25468
|
-
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
25469
|
-
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$1(void 0)));
|
|
25470
|
-
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
25471
|
-
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
25273
|
+
const resolveBase = (commandLabel, value) => gen(function* () {
|
|
25472
25274
|
const git = yield* GitService;
|
|
25473
|
-
const
|
|
25474
|
-
const
|
|
25475
|
-
const
|
|
25275
|
+
const rendered = value.trim();
|
|
25276
|
+
const target = rendered === "" ? yield* git.defaultBranch() : rendered;
|
|
25277
|
+
const resolvedBase = yield* result(git.resolveRef(target));
|
|
25278
|
+
if (resolvedBase._tag === "Failure") return refusal(`${commandLabel}: "${target}" does not resolve to a commit`);
|
|
25279
|
+
const mergeBase = yield* git.mergeBase(resolvedBase.success, "HEAD");
|
|
25280
|
+
if (isNone(mergeBase)) return refusal(`${commandLabel}: "${target}" shares no common ancestor with HEAD`);
|
|
25281
|
+
if (mergeBase.value === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: nothing to review: HEAD has no commits beyond ${target}`);
|
|
25282
|
+
return mergeBase.value;
|
|
25283
|
+
});
|
|
25284
|
+
//#endregion
|
|
25285
|
+
//#region src/step/strayPaths.ts
|
|
25286
|
+
/** Globs use the `access.write` dialect (`globMatches`). */
|
|
25287
|
+
const strayPaths = (touched, allow) => touched.filter((path) => !allow.some((glob) => globMatches(path, glob))).sort();
|
|
25288
|
+
//#endregion
|
|
25289
|
+
//#region src/edge/Edge.ts
|
|
25290
|
+
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
25291
|
+
const ACTORS = /* @__PURE__ */ new Set([
|
|
25292
|
+
"agent",
|
|
25293
|
+
"human",
|
|
25294
|
+
"check",
|
|
25295
|
+
"judge"
|
|
25296
|
+
]);
|
|
25297
|
+
const isOpening = ({ parsed, step }) => parsed !== void 0 && step === void 0 && parsed.from === void 0 && parsed.actor === "human";
|
|
25298
|
+
const boundsEpisode = (def, { parsed, pin }) => pin !== "add" && (parsed === void 0 || !ACTORS.has(parsed.actor) || parsed.to === def.initial);
|
|
25299
|
+
/**
|
|
25300
|
+
* Where the episode HEAD belongs to begins, read off subjects alone. Walking
|
|
25301
|
+
* back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
|
|
25302
|
+
* `gtd start` writes — no landing ever produces one — and opens an
|
|
25303
|
+
* episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
|
|
25304
|
+
* a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
|
|
25305
|
+
* entering the flow's first step (a finished episode), bounds the episode
|
|
25306
|
+
* from below.
|
|
25307
|
+
*/
|
|
25308
|
+
const locateEpisode = (def, history) => {
|
|
25309
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
25310
|
+
const message = parseCommitMessage(history[i].message);
|
|
25311
|
+
if (isOpening(message)) return {
|
|
25312
|
+
workflow: message.workflow,
|
|
25313
|
+
legacyOpening: message.workflow === void 0,
|
|
25314
|
+
baseIndex: i,
|
|
25315
|
+
processStart: i
|
|
25316
|
+
};
|
|
25317
|
+
if (boundsEpisode(def, message)) return {
|
|
25318
|
+
workflow: void 0,
|
|
25319
|
+
legacyOpening: false,
|
|
25320
|
+
baseIndex: i,
|
|
25321
|
+
processStart: i + 1
|
|
25322
|
+
};
|
|
25323
|
+
}
|
|
25476
25324
|
return {
|
|
25477
|
-
|
|
25478
|
-
|
|
25479
|
-
|
|
25480
|
-
|
|
25481
|
-
file: rest.hints.file,
|
|
25482
|
-
landing
|
|
25325
|
+
workflow: void 0,
|
|
25326
|
+
legacyOpening: false,
|
|
25327
|
+
baseIndex: -1,
|
|
25328
|
+
processStart: 0
|
|
25483
25329
|
};
|
|
25484
|
-
}
|
|
25485
|
-
const
|
|
25486
|
-
|
|
25487
|
-
|
|
25488
|
-
|
|
25489
|
-
|
|
25490
|
-
const entryCommit = run.trace[0].hash;
|
|
25491
|
-
const { total, byModel } = costTotals(run.costEntries);
|
|
25330
|
+
};
|
|
25331
|
+
const headTurnOf = (head) => {
|
|
25332
|
+
if (head === void 0) return void 0;
|
|
25333
|
+
const message = parseCommitMessage(head.message);
|
|
25334
|
+
const subject = message.parsed;
|
|
25335
|
+
if (subject === void 0 || !ACTORS.has(subject.actor) || message.pin !== void 0) return void 0;
|
|
25492
25336
|
return {
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25497
|
-
processCost: total,
|
|
25498
|
-
processCostByModel: byModel,
|
|
25499
|
-
marks: run.marks,
|
|
25500
|
-
...settings
|
|
25337
|
+
state: subject.to,
|
|
25338
|
+
actor: subject.actor,
|
|
25339
|
+
empty: head.touched.length === 0,
|
|
25340
|
+
step: message.step !== void 0
|
|
25501
25341
|
};
|
|
25502
25342
|
};
|
|
25503
|
-
const
|
|
25504
|
-
|
|
25505
|
-
|
|
25506
|
-
|
|
25507
|
-
const summary = resolved.def.summary;
|
|
25508
|
-
if (summary === void 0 || run.trace.length === 0) return { prompt: void 0 };
|
|
25509
|
-
return { prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))) };
|
|
25510
|
-
});
|
|
25511
|
-
/** Repository-relative paths of the config files today's config and the process's own record name; one outside the repository cannot be read at a ref, so it is left out. */
|
|
25512
|
-
const inRepoConfigPaths = (root, files, recorded) => {
|
|
25513
|
-
const paths = new Set(Object.keys(recorded));
|
|
25514
|
-
for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
|
|
25515
|
-
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
|
|
25516
|
-
};
|
|
25517
|
-
const configRefusal = (command, tip, paths, reason = "A history ref replays under today's config, so it is unreadable after the config changes") => /* @__PURE__ */ new Error(`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`);
|
|
25518
|
-
const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
|
|
25519
|
-
const differing = [];
|
|
25520
|
-
for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint), run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
|
|
25521
|
-
return differing;
|
|
25343
|
+
const traceEntryOf = (hash, message) => ({
|
|
25344
|
+
state: message.parsed?.to ?? "",
|
|
25345
|
+
hash,
|
|
25346
|
+
actor: message.parsed?.actor ?? ""
|
|
25522
25347
|
});
|
|
25523
|
-
const
|
|
25524
|
-
|
|
25525
|
-
|
|
25526
|
-
|
|
25527
|
-
|
|
25528
|
-
|
|
25529
|
-
|
|
25530
|
-
|
|
25531
|
-
|
|
25532
|
-
|
|
25533
|
-
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
});
|
|
25348
|
+
const costEntriesOf = (hash, message) => message.cost.map((c) => ({
|
|
25349
|
+
cost: c.cost,
|
|
25350
|
+
model: c.model ?? "unspecified",
|
|
25351
|
+
hash
|
|
25352
|
+
}));
|
|
25353
|
+
const hashAt = (history, index) => history[index]?.hash;
|
|
25354
|
+
const historyFormatFault = (history, location) => formatFault(history.slice(location.baseIndex + 1).concat(history[location.baseIndex]?.message.startsWith("gtd") ? [history[location.baseIndex]] : []).map((c) => ({
|
|
25355
|
+
hash: c.hash,
|
|
25356
|
+
format: parseCommitMessage(c.message).format
|
|
25357
|
+
})));
|
|
25358
|
+
/** Fails on a history in a format this gtd does not read, before anything acts on it. */
|
|
25359
|
+
const runOf = (history, location, closingHash = void 0) => {
|
|
25360
|
+
const fault = historyFormatFault(history, location);
|
|
25361
|
+
return fault === void 0 ? succeed$1(buildRun(history, location, closingHash)) : fail$4(new Error(fault));
|
|
25538
25362
|
};
|
|
25539
|
-
const
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
const
|
|
25546
|
-
const
|
|
25547
|
-
const
|
|
25548
|
-
const
|
|
25549
|
-
const
|
|
25550
|
-
const
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
});
|
|
25554
|
-
const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
|
|
25555
|
-
command: "gtd history",
|
|
25556
|
-
notEnded: (o) => notClosedError(hash, o)
|
|
25557
|
-
});
|
|
25558
|
-
return {
|
|
25559
|
-
tip: hash,
|
|
25560
|
-
steps: historySteps(run),
|
|
25561
|
-
ended: outcome.via,
|
|
25562
|
-
version: {
|
|
25563
|
-
recorded,
|
|
25564
|
-
running: GTD_VERSION
|
|
25565
|
-
},
|
|
25566
|
-
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
25567
|
-
};
|
|
25568
|
-
});
|
|
25569
|
-
/** Refuses on config drift before replaying, so a stale config never runs the flow. */
|
|
25570
|
-
const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
25571
|
-
const git = yield* GitService;
|
|
25572
|
-
const workspace = yield* Workspace;
|
|
25573
|
-
const host = yield* Host;
|
|
25574
|
-
const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
|
|
25575
|
-
const versionWarning = versionWarningFor(recorded);
|
|
25576
|
-
const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
|
|
25577
|
-
const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
|
|
25578
|
-
if (differing.length > 0) return yield* fail$4(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
|
|
25579
|
-
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
25580
|
-
const outcome = yield* promise(() => replayFor(setup));
|
|
25581
|
-
if (outcome.kind !== "ended") return yield* fail$4(withWarning(how.notEnded(outcome)));
|
|
25363
|
+
const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
|
|
25364
|
+
step: formatStepId(message.approved.step),
|
|
25365
|
+
name: message.approved.name,
|
|
25366
|
+
email: message.approved.email,
|
|
25367
|
+
commit: hash
|
|
25368
|
+
}];
|
|
25369
|
+
const buildRun = (history, location, closingHash) => {
|
|
25370
|
+
const allProcessCommits = history.slice(location.processStart);
|
|
25371
|
+
const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
|
|
25372
|
+
const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
|
|
25373
|
+
const parsed = allParsed.filter((m) => m.pin !== "add");
|
|
25374
|
+
const first = parsed[0];
|
|
25375
|
+
const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
|
|
25376
|
+
const head = history[history.length - 1];
|
|
25582
25377
|
return {
|
|
25583
|
-
|
|
25584
|
-
|
|
25585
|
-
|
|
25378
|
+
workflow: location.workflow,
|
|
25379
|
+
legacyOpening: location.legacyOpening,
|
|
25380
|
+
startHash: hashAt(history, location.processStart) ?? head?.hash ?? EMPTY_TREE$1,
|
|
25381
|
+
startParentHash,
|
|
25382
|
+
diffBase: first?.reviewBase ?? startParentHash,
|
|
25383
|
+
trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
|
|
25384
|
+
costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
|
|
25385
|
+
judgeVerdicts: parsed.flatMap((m) => m.judge),
|
|
25386
|
+
pinCommitted: allParsed.some((m) => m.pin === "add"),
|
|
25387
|
+
marks: parsed.flatMap((m) => m.marks),
|
|
25388
|
+
approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
|
|
25389
|
+
pinnedVars: { ...first?.vars },
|
|
25390
|
+
headTurn: headTurnOf(head),
|
|
25391
|
+
closingHash,
|
|
25392
|
+
episode: {
|
|
25393
|
+
base: hashAt(history, location.baseIndex),
|
|
25394
|
+
commits: history.slice(location.baseIndex + 1)
|
|
25395
|
+
}
|
|
25586
25396
|
};
|
|
25587
|
-
});
|
|
25588
|
-
//#endregion
|
|
25589
|
-
//#region src/emit/GitScript.ts
|
|
25590
|
-
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
25591
|
-
/**
|
|
25592
|
-
* `--allow-empty` because gtd's workflow commits may be empty on purpose.
|
|
25593
|
-
* Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
|
|
25594
|
-
* "empty git commit" rejection message, discriminated from the captured
|
|
25595
|
-
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
25596
|
-
* error, a commit-msg hook) still fails.
|
|
25597
|
-
*/
|
|
25598
|
-
const commitAllowEmpty = (message) => {
|
|
25599
|
-
const m = shellQuote(message);
|
|
25600
|
-
return [
|
|
25601
|
-
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
25602
|
-
` case "$out" in`,
|
|
25603
|
-
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
|
|
25604
|
-
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
25605
|
-
` esac`,
|
|
25606
|
-
`fi`
|
|
25607
|
-
].join("\n");
|
|
25608
25397
|
};
|
|
25609
|
-
const
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
* hook runs and none can add to it. A HEAD that moved fails the guarded
|
|
25616
|
-
* `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
|
|
25617
|
-
*/
|
|
25618
|
-
const commitClean = (message) => [
|
|
25619
|
-
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
25620
|
-
`if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
|
|
25621
|
-
` printf '%s\\n' ${shellQuote(CHANGED_SINCE_LAND)} >&2`,
|
|
25622
|
-
` exit 1`,
|
|
25623
|
-
`fi`,
|
|
25624
|
-
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
25625
|
-
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
25626
|
-
`gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote(message)}) || exit 1`,
|
|
25627
|
-
`git update-ref -m ${shellQuote(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
|
|
25628
|
-
].join("\n");
|
|
25629
|
-
/** No current caller — see `Git.ts`'s `GitWriterOperations.commitAsIs` for why it's kept anyway. */
|
|
25630
|
-
const commitAsIs = (message) => commitAllowEmpty(message);
|
|
25631
|
-
/** No current caller — see `Git.ts`'s `GitWriterOperations.softResetTo` for why it's kept anyway. */
|
|
25632
|
-
const softResetTo = (ref) => `git reset --soft ${shellQuote(ref)}`;
|
|
25633
|
-
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote(ref)}`;
|
|
25634
|
-
const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
|
|
25635
|
-
/**
|
|
25636
|
-
* `git add -A` then `git reset --hard HEAD` — discards every pending change,
|
|
25637
|
-
* tracked or untracked. Joined with `&&`: a failed stage must not reach the
|
|
25638
|
-
* hard reset, or untracked survivors remain — the exact outcome this builder
|
|
25639
|
-
* exists to avoid. No current caller — see `Git.ts`'s
|
|
25640
|
-
* `GitWriterOperations.discardPending` for why it's kept anyway.
|
|
25641
|
-
*/
|
|
25642
|
-
const discardPending = () => `git add -A && git reset --hard HEAD`;
|
|
25398
|
+
const PAGE_TIERS = [
|
|
25399
|
+
32,
|
|
25400
|
+
128,
|
|
25401
|
+
512,
|
|
25402
|
+
Infinity
|
|
25403
|
+
];
|
|
25643
25404
|
/**
|
|
25644
|
-
*
|
|
25645
|
-
*
|
|
25646
|
-
*
|
|
25647
|
-
*
|
|
25648
|
-
*
|
|
25649
|
-
*
|
|
25650
|
-
*
|
|
25651
|
-
*
|
|
25652
|
-
*
|
|
25653
|
-
*
|
|
25654
|
-
|
|
25655
|
-
|
|
25656
|
-
|
|
25657
|
-
|
|
25658
|
-
|
|
25659
|
-
|
|
25660
|
-
|
|
25661
|
-
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
|
|
25667
|
-
|
|
25668
|
-
|
|
25669
|
-
|
|
25670
|
-
`git reset --hard HEAD`
|
|
25671
|
-
].join("\n");
|
|
25405
|
+
* `locateEpisode`'s own boundary test, subject-only: a commit that isn't a
|
|
25406
|
+
* gtd step commit, or one landing the flow's initial state, bounds an
|
|
25407
|
+
* episode from below — EXCEPT a bare `gtd(human): <x>` subject (no `→`),
|
|
25408
|
+
* which is NEVER a boundary here, whatever `<x>` is. That bare shape is
|
|
25409
|
+
* `locateEpisode`'s own `opening` branch (a started episode's first commit),
|
|
25410
|
+
* which takes priority there over the initial-state check no matter what the
|
|
25411
|
+
* workflow's first step is named — including one named after `def.initial`
|
|
25412
|
+
* itself, where the two branches would otherwise disagree. A bare human commit can
|
|
25413
|
+
* also be a self-loop step landing rather than a real opening (this
|
|
25414
|
+
* subject-only pass can't read the `Gtd-Step` trailer that would tell them
|
|
25415
|
+
* apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
|
|
25416
|
+
* commits further back than strictly needed — never stop short of the
|
|
25417
|
+
* boundary `locateEpisode` would find once it has full commit bodies in
|
|
25418
|
+
* hand. That asymmetry is the only direction this approximation may err in;
|
|
25419
|
+
* it must never call a real boundary "not yet found".
|
|
25420
|
+
*/
|
|
25421
|
+
const subjectIsBoundary = (def, subject) => {
|
|
25422
|
+
const parsed = parseCommitMessage(subject).parsed;
|
|
25423
|
+
if (parsed === void 0 || !ACTORS.has(parsed.actor)) return true;
|
|
25424
|
+
if (parsed.actor === "human" && parsed.from === void 0) return false;
|
|
25425
|
+
if (parsed.actor === "check" && parsed.from === PIN_STATES.add) return false;
|
|
25426
|
+
return parsed.to === def.initial;
|
|
25427
|
+
};
|
|
25428
|
+
/** The last page index (walking newest→oldest, same direction as `locateEpisode`) whose subject is a boundary, or `undefined` if none in `page`. */
|
|
25429
|
+
const boundaryIndexIn = (def, page) => {
|
|
25430
|
+
for (let i = page.length - 1; i >= 0; i--) if (subjectIsBoundary(def, page[i].subject)) return i;
|
|
25672
25431
|
};
|
|
25673
|
-
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
25674
|
-
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
25675
|
-
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
25676
25432
|
/**
|
|
25677
|
-
*
|
|
25678
|
-
*
|
|
25433
|
+
* `findBoundaryBase`'s result: `base` is the hash to pass `commitHistory` so
|
|
25434
|
+
* a `git.commitHistory(base, head)` call reads exactly the episode ending at
|
|
25435
|
+
* `head`, plus whatever a subject-only read couldn't rule out — never the
|
|
25436
|
+
* whole repository. `base` is deliberately the boundary commit's PARENT, not
|
|
25437
|
+
* the boundary commit itself: `commitHistory`'s range excludes `base`, and
|
|
25438
|
+
* `runOf` needs the boundary commit present in the read history
|
|
25439
|
+
* (`episode.base`, `startParentHash`). `base: undefined` means "read from the
|
|
25440
|
+
* repository root" — an empty repository, an unresolvable `head`, or an
|
|
25441
|
+
* episode that reaches the root commit, none of which cost anything extra
|
|
25442
|
+
* since that IS the whole history in those cases.
|
|
25443
|
+
*
|
|
25444
|
+
* `head` is the CONCRETE hash the literal `head` argument resolved to on this
|
|
25445
|
+
* call's first read — `undefined` only when it never resolved (an empty
|
|
25446
|
+
* repository, or an unresolvable ref). The caller must hand this hash, not
|
|
25447
|
+
* the original `head` argument, to its own `commitHistory` call: resolving a
|
|
25448
|
+
* symbolic ref (literal `HEAD`, most callers' default) a second time risks
|
|
25449
|
+
* reading a DIFFERENT commit than this function paged through, if something
|
|
25450
|
+
* moves HEAD in between — pinning both reads to one resolved hash is what
|
|
25451
|
+
* keeps the two-read split from disagreeing with itself.
|
|
25679
25452
|
*/
|
|
25680
|
-
const
|
|
25681
|
-
|
|
25682
|
-
|
|
25683
|
-
|
|
25684
|
-
|
|
25685
|
-
|
|
25686
|
-
|
|
25687
|
-
|
|
25688
|
-
|
|
25689
|
-
|
|
25690
|
-
|
|
25691
|
-
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
case "note": return printfLine$1("%s\n", [shellQuote(outcome.text)]);
|
|
25453
|
+
const findBoundaryBase = (git, def, head) => gen(function* () {
|
|
25454
|
+
let pinnedHead = head;
|
|
25455
|
+
for (const pageSize of PAGE_TIERS) {
|
|
25456
|
+
const page = yield* git.subjectHistory(pageSize, 0, pinnedHead);
|
|
25457
|
+
if (pinnedHead === void 0) pinnedHead = page[page.length - 1]?.hash;
|
|
25458
|
+
const wholeHistory = !Number.isFinite(pageSize) || page.length < pageSize;
|
|
25459
|
+
const boundaryIdx = boundaryIndexIn(def, page);
|
|
25460
|
+
if (boundaryIdx === void 0 || boundaryIdx === 0) {
|
|
25461
|
+
if (wholeHistory) return {
|
|
25462
|
+
base: void 0,
|
|
25463
|
+
head: pinnedHead
|
|
25464
|
+
};
|
|
25465
|
+
continue;
|
|
25466
|
+
}
|
|
25467
|
+
return {
|
|
25468
|
+
base: page[boundaryIdx - 1].hash,
|
|
25469
|
+
head: pinnedHead
|
|
25470
|
+
};
|
|
25699
25471
|
}
|
|
25700
|
-
|
|
25701
|
-
|
|
25702
|
-
|
|
25703
|
-
|
|
25704
|
-
|
|
25705
|
-
|
|
25706
|
-
|
|
25472
|
+
return {
|
|
25473
|
+
base: void 0,
|
|
25474
|
+
head: pinnedHead
|
|
25475
|
+
};
|
|
25476
|
+
});
|
|
25477
|
+
const historyUpTo = (git, def, head) => flatMap(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
25478
|
+
const computeProcessRun = (git, def, head) => flatMap(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
25479
|
+
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
|
|
25480
|
+
const currentRun = gen(function* () {
|
|
25481
|
+
const git = yield* GitService;
|
|
25482
|
+
const config = yield* (yield* ConfigService).load;
|
|
25483
|
+
return yield* computeProcessRun(git, config.workflow);
|
|
25484
|
+
});
|
|
25485
|
+
const runAtTip = (git, def, tip) => gen(function* () {
|
|
25486
|
+
if (tip === void 0) {
|
|
25487
|
+
const history = yield* historyUpTo(git, def, void 0);
|
|
25488
|
+
return yield* runOf(history, locateEpisode(def, history));
|
|
25489
|
+
}
|
|
25490
|
+
if (!(parseCommitMessage(tip.subject).parsed?.to === def.initial)) {
|
|
25491
|
+
const history = yield* historyUpTo(git, def, tip.hash);
|
|
25492
|
+
return yield* runOf(history, locateEpisode(def, history));
|
|
25707
25493
|
}
|
|
25494
|
+
const parentRef = `${tip.hash}~1`;
|
|
25495
|
+
const before = yield* historyUpTo(git, def, parentRef);
|
|
25496
|
+
const location = locateEpisode(def, before);
|
|
25497
|
+
let closing = yield* git.commitHistory(parentRef, tip.hash);
|
|
25498
|
+
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
25499
|
+
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
25500
|
+
});
|
|
25501
|
+
const runAtHead = (git, def) => gen(function* () {
|
|
25502
|
+
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
|
|
25503
|
+
});
|
|
25504
|
+
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
25505
|
+
const summaryRun = gen(function* () {
|
|
25506
|
+
const git = yield* GitService;
|
|
25507
|
+
const def = (yield* (yield* ConfigService).load).workflow;
|
|
25508
|
+
return yield* runAtHead(git, def);
|
|
25509
|
+
});
|
|
25510
|
+
const commitTree = (workspace, hash) => {
|
|
25511
|
+
let entries;
|
|
25512
|
+
const list = () => entries ??= workspace.treeSync(hash);
|
|
25513
|
+
let sortedPaths;
|
|
25514
|
+
const contents = /* @__PURE__ */ new Map();
|
|
25515
|
+
return {
|
|
25516
|
+
paths: () => sortedPaths ??= [...list().keys()].sort(),
|
|
25517
|
+
read: (path) => {
|
|
25518
|
+
if (!list().has(path)) return void 0;
|
|
25519
|
+
if (!contents.has(path)) contents.set(path, workspace.readCommittedSync(path, hash));
|
|
25520
|
+
return contents.get(path);
|
|
25521
|
+
},
|
|
25522
|
+
id: (path) => list().get(path)
|
|
25523
|
+
};
|
|
25708
25524
|
};
|
|
25709
25525
|
/**
|
|
25710
|
-
*
|
|
25711
|
-
*
|
|
25712
|
-
*
|
|
25713
|
-
*
|
|
25526
|
+
* The tree a landing would commit: the working tree, with any rewrite the
|
|
25527
|
+
* landing script applies first. `entries` is `setup.pendingWorktree()` —
|
|
25528
|
+
* already memoized — never a fresh `workspace.worktreeSync()` call; see that
|
|
25529
|
+
* field's own comment for why the memo lives there and not on `Workspace`.
|
|
25714
25530
|
*/
|
|
25715
|
-
const
|
|
25716
|
-
|
|
25717
|
-
|
|
25718
|
-
|
|
25719
|
-
|
|
25720
|
-
|
|
25721
|
-
|
|
25722
|
-
|
|
25723
|
-
|
|
25724
|
-
|
|
25725
|
-
|
|
25726
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25531
|
+
const pendingTree = (workspace, entries, rewrite) => {
|
|
25532
|
+
const paths = [...entries.keys()];
|
|
25533
|
+
const readWorktree = (path) => {
|
|
25534
|
+
try {
|
|
25535
|
+
return workspace.readSync(path);
|
|
25536
|
+
} catch {
|
|
25537
|
+
return;
|
|
25538
|
+
}
|
|
25539
|
+
};
|
|
25540
|
+
return {
|
|
25541
|
+
paths: () => paths,
|
|
25542
|
+
read: (path) => {
|
|
25543
|
+
if (!entries.has(path)) return void 0;
|
|
25544
|
+
const content = readWorktree(path);
|
|
25545
|
+
return content === void 0 || rewrite?.path !== path ? content : rewrite.apply(content);
|
|
25546
|
+
},
|
|
25547
|
+
id: (path) => rewrite?.path === path ? void 0 : entries.get(path)
|
|
25548
|
+
};
|
|
25732
25549
|
};
|
|
25733
|
-
const ScriptSurface = { render: (steps) => {
|
|
25734
|
-
const rendered = steps.map(renderLandStep);
|
|
25735
|
-
return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
|
|
25736
|
-
} };
|
|
25737
|
-
//#endregion
|
|
25738
|
-
//#region src/emit/Emit.ts
|
|
25739
|
-
/** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
|
|
25740
|
-
const fileExistsGuard = (file) => `[ -f ${shellQuote(file)} ] || exit 0`;
|
|
25741
|
-
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
25742
|
-
const withFileVar = (command, file) => `export GTD_FILE=${shellQuote(file)}\n${command}`;
|
|
25743
|
-
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
|
|
25744
|
-
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
|
|
25745
|
-
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
|
|
25746
|
-
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
25747
|
-
const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote(file));
|
|
25748
25550
|
/**
|
|
25749
|
-
* The
|
|
25750
|
-
*
|
|
25751
|
-
*
|
|
25752
|
-
*
|
|
25551
|
+
* The one resolver of both setting kinds. Process settings: the recorded
|
|
25552
|
+
* (pinned) value wins per name, anything unrecorded resolves live — the
|
|
25553
|
+
* fallback for a process started before pinning existed. Environment settings
|
|
25554
|
+
* are always live. `startOverrides` are `--var` values, passed only while a
|
|
25555
|
+
* process is being started.
|
|
25753
25556
|
*/
|
|
25754
|
-
const
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25557
|
+
const settingsFor = (workflow, pinnedVars, hostEnv, startOverrides = {}, startFlags) => ({
|
|
25558
|
+
vars: {
|
|
25559
|
+
...resolveStartVars(workflow.vars, startFlags, startOverrides, hostEnv),
|
|
25560
|
+
...pinnedVars
|
|
25561
|
+
},
|
|
25562
|
+
env: resolveVars(workflow.env, {}, hostEnv)
|
|
25563
|
+
});
|
|
25564
|
+
/** The workflow a run replays on: its pinned one, else the default. */
|
|
25565
|
+
const workflowOf = (config, run) => {
|
|
25566
|
+
if (run.legacyOpening) return fail$4(/* @__PURE__ */ new Error("gtd: this process was opened by `gtd --entry`, which no longer exists — run `gtd abandon` to start over"));
|
|
25567
|
+
if (run.workflow === void 0) return succeed$1(config.defaultWorkflow);
|
|
25568
|
+
const named = config.workflowNamed(run.workflow);
|
|
25569
|
+
if (named !== void 0) return succeed$1(named);
|
|
25570
|
+
return fail$4(/* @__PURE__ */ new Error(`gtd: this process runs workflow "${run.workflow}", which gtd.config.ts no longer defines — run \`gtd abandon\` to start over`));
|
|
25759
25571
|
};
|
|
25760
25572
|
/**
|
|
25761
|
-
*
|
|
25762
|
-
*
|
|
25763
|
-
*
|
|
25573
|
+
* `judgeBudgetBytes` is the one var that refuses rather than disabling its
|
|
25574
|
+
* mechanism when blanked: without a bound the judge payload is rejected
|
|
25575
|
+
* anyway. Absent falls back to a conservative default.
|
|
25764
25576
|
*/
|
|
25765
|
-
const
|
|
25766
|
-
|
|
25767
|
-
|
|
25768
|
-
|
|
25769
|
-
|
|
25770
|
-
|
|
25771
|
-
|
|
25772
|
-
|
|
25577
|
+
const DEFAULT_JUDGE_BUDGET_BYTES = 32768;
|
|
25578
|
+
const judgeBudgetBytes = (vars) => {
|
|
25579
|
+
const raw = vars.judgeBudgetBytes;
|
|
25580
|
+
if (raw === void 0) return DEFAULT_JUDGE_BUDGET_BYTES;
|
|
25581
|
+
const n = Number(raw);
|
|
25582
|
+
if (raw.trim() === "" || !Number.isInteger(n) || n <= 0) throw new Error(`"judgeBudgetBytes" must be a positive integer — got ${JSON.stringify(raw)} (blanking this var disables the payload bound rather than the mechanism it guards, so it is refused rather than defaulted)`);
|
|
25583
|
+
return n;
|
|
25584
|
+
};
|
|
25585
|
+
const replayFor = (setup, pending) => replay({
|
|
25586
|
+
flow: setup.def.flow,
|
|
25587
|
+
episode: {
|
|
25588
|
+
base: setup.base,
|
|
25589
|
+
commits: setup.episode
|
|
25590
|
+
},
|
|
25591
|
+
vars: setup.vars,
|
|
25592
|
+
env: setup.env,
|
|
25593
|
+
start: setup.run.diffBase,
|
|
25594
|
+
startTree: setup.run.diffBase === EMPTY_TREE$1 ? treeFromRecord({}) : commitTree(setup.workspace, setup.run.diffBase),
|
|
25595
|
+
budgetBytes: setup.budget,
|
|
25596
|
+
skills: setup.skills,
|
|
25597
|
+
configuredSkills: setup.def.configuredSkills,
|
|
25598
|
+
access: setup.access,
|
|
25599
|
+
configuredAccess: setup.def.configuredAccess,
|
|
25600
|
+
...pending !== void 0 ? { pending } : {}
|
|
25601
|
+
});
|
|
25602
|
+
const replayError = (outcome) => {
|
|
25603
|
+
if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
|
|
25604
|
+
if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
|
|
25605
|
+
if (outcome.kind === "ended") return /* @__PURE__ */ new Error("gtd: history ends the episode, but HEAD does not enter the default workflow's first step — the workflow changed under this process; run `gtd abandon` to start over");
|
|
25773
25606
|
};
|
|
25607
|
+
const judgeDocument = (step) => step.request.kind === "judge" ? judgeJson(step.request.evidence, step.request.questions) : void 0;
|
|
25774
25608
|
/**
|
|
25775
|
-
*
|
|
25776
|
-
*
|
|
25777
|
-
*
|
|
25609
|
+
* The fixed sentence appended to a judge gate's message when a bounded read
|
|
25610
|
+
* dropped bytes to fit `judgeBudgetBytes` — a constant, so no workflow can
|
|
25611
|
+
* reword or drop it.
|
|
25778
25612
|
*/
|
|
25779
|
-
const
|
|
25780
|
-
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25613
|
+
const TRUNCATION_NOTICE = "Note: some evidence above was truncated to fit the judge's payload budget.";
|
|
25614
|
+
const DEFAULT_JUDGE_MESSAGE = "A judgment is pending. Run `gtd judge answer` and pipe a verdict, or land to take the conservative default with no verdict recorded.";
|
|
25615
|
+
const optional = (key, value) => value === void 0 ? {} : { [key]: value };
|
|
25616
|
+
const commonOf = (step) => {
|
|
25617
|
+
const options = step.request.options;
|
|
25618
|
+
return {
|
|
25619
|
+
actor: step.actor,
|
|
25620
|
+
...optional("label", options.label),
|
|
25621
|
+
...optional("file", options.file),
|
|
25622
|
+
...optional("mode", options.mode)
|
|
25623
|
+
};
|
|
25790
25624
|
};
|
|
25791
|
-
const
|
|
25792
|
-
|
|
25793
|
-
|
|
25625
|
+
const promptDef = (common, request, skills, access) => ({
|
|
25626
|
+
...common,
|
|
25627
|
+
kind: "prompt",
|
|
25628
|
+
content: request.prompt,
|
|
25629
|
+
...optional("model", request.options.model),
|
|
25630
|
+
...optional("system", request.options.system),
|
|
25631
|
+
...optional("allowEmpty", request.options.allowEmpty),
|
|
25632
|
+
...optional("keep", request.options.keep),
|
|
25633
|
+
...optional("skills", skills),
|
|
25634
|
+
...optional("access", access)
|
|
25635
|
+
});
|
|
25636
|
+
const scriptDef = (common, request) => ({
|
|
25637
|
+
...common,
|
|
25638
|
+
kind: "script",
|
|
25639
|
+
content: request.body
|
|
25640
|
+
});
|
|
25641
|
+
const judgeDef = (common, request, step) => {
|
|
25642
|
+
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
25643
|
+
return {
|
|
25644
|
+
...common,
|
|
25645
|
+
kind: "message",
|
|
25646
|
+
content: step.truncated ? `${message}\n\n${TRUNCATION_NOTICE}` : message,
|
|
25647
|
+
...optional("judge", judgeDocument(step))
|
|
25648
|
+
};
|
|
25794
25649
|
};
|
|
25795
|
-
const
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
optional
|
|
25650
|
+
const messageDef = (common, request, step) => ({
|
|
25651
|
+
...common,
|
|
25652
|
+
kind: "message",
|
|
25653
|
+
content: request.options.message ?? request.options.label ?? step.name,
|
|
25654
|
+
...optional("acceptClean", request.options.approve === true || request.options.acceptClean),
|
|
25655
|
+
...optional("approve", request.options.approve)
|
|
25800
25656
|
});
|
|
25801
|
-
|
|
25802
|
-
const
|
|
25803
|
-
const
|
|
25804
|
-
|
|
25805
|
-
|
|
25806
|
-
|
|
25807
|
-
|
|
25808
|
-
|
|
25809
|
-
|
|
25810
|
-
*/
|
|
25811
|
-
const combinedScript = (required, optional) => {
|
|
25812
|
-
if (required.length === 0) return "";
|
|
25813
|
-
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
25814
|
-
return [
|
|
25815
|
-
DID_NOT_RUN_COMMENT,
|
|
25816
|
-
"",
|
|
25817
|
-
required,
|
|
25818
|
-
"",
|
|
25819
|
-
PRESENTATION_ONLY_COMMENT,
|
|
25820
|
-
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
25821
|
-
].join("\n");
|
|
25657
|
+
const stepDefOf = (step) => {
|
|
25658
|
+
const request = step.request;
|
|
25659
|
+
const common = commonOf(step);
|
|
25660
|
+
switch (request.kind) {
|
|
25661
|
+
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
25662
|
+
case "run": return scriptDef(common, request);
|
|
25663
|
+
case "judge": return judgeDef(common, request, step);
|
|
25664
|
+
case "human": return messageDef(common, request, step);
|
|
25665
|
+
}
|
|
25822
25666
|
};
|
|
25823
|
-
|
|
25824
|
-
//#region src/emit/OutcomeScript.ts
|
|
25825
|
-
/**
|
|
25826
|
-
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
25827
|
-
* tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
|
|
25828
|
-
* re-quoted here: a value reaches the script as a printf ARGUMENT, never
|
|
25829
|
-
* interpolated into the format itself, so a subject or state name containing
|
|
25830
|
-
* `%` can never be read as a conversion spec. Markers like `->` are arguments
|
|
25831
|
-
* too, keeping every format string free of a leading `-` that a shell's
|
|
25832
|
-
* `printf` could mistake for an option. A real newline in `fmt` is emitted as
|
|
25833
|
-
* the two-character `\n` escape `printf` itself expands, keeping every emitted
|
|
25834
|
-
* statement on one line.
|
|
25835
|
-
*/
|
|
25836
|
-
const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
25667
|
+
const ROOT_MEMORY_SCOPE_NAME = "root";
|
|
25837
25668
|
/**
|
|
25838
|
-
*
|
|
25839
|
-
*
|
|
25840
|
-
*
|
|
25841
|
-
*
|
|
25669
|
+
* An agent rest's memory key, `<scope>#<hash7>`: the hash is the commit the
|
|
25670
|
+
* current unbroken run into the scope started FROM, so the same run always
|
|
25671
|
+
* re-derives the same key. `resumed` is whether an agent turn in that run
|
|
25672
|
+
* already landed.
|
|
25842
25673
|
*/
|
|
25843
|
-
const
|
|
25844
|
-
const
|
|
25845
|
-
|
|
25846
|
-
|
|
25847
|
-
|
|
25848
|
-
|
|
25849
|
-
|
|
25850
|
-
const
|
|
25674
|
+
const memoryOf = (trace, run) => {
|
|
25675
|
+
const rest = trace[trace.length - 1];
|
|
25676
|
+
if (rest === void 0 || rest.kind !== "agent") return {
|
|
25677
|
+
key: void 0,
|
|
25678
|
+
resumed: false
|
|
25679
|
+
};
|
|
25680
|
+
const start = scopeRunStart(trace, rest.memoryScope);
|
|
25681
|
+
const token = start <= 0 ? run.startParentHash : trace[start - 1].enteredAt;
|
|
25682
|
+
const resumed = trace.slice(Math.max(start, 0), trace.length - 1).some((s) => s.kind === "agent" && s.memoryScope === rest.memoryScope);
|
|
25683
|
+
return {
|
|
25684
|
+
key: `${rest.memoryScope || ROOT_MEMORY_SCOPE_NAME}#${token.slice(0, 7)}`,
|
|
25685
|
+
resumed
|
|
25686
|
+
};
|
|
25687
|
+
};
|
|
25688
|
+
const hintsOf = (def) => ({
|
|
25689
|
+
...optional("model", def.model),
|
|
25690
|
+
...optional("label", def.label),
|
|
25691
|
+
...optional("file", def.file),
|
|
25692
|
+
...optional("judge", def.judge),
|
|
25693
|
+
...optional("system", def.system),
|
|
25694
|
+
...optional("mode", def.mode),
|
|
25695
|
+
...optional("skills", def.skills),
|
|
25696
|
+
...optional("access", def.access)
|
|
25697
|
+
});
|
|
25698
|
+
const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
|
|
25699
|
+
/** Per-model token totals, highest-cost first (ties broken by model name). */
|
|
25700
|
+
const costByModel = (entries) => {
|
|
25701
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
25702
|
+
for (const entry of entries) byModel.set(entry.model, (byModel.get(entry.model) ?? 0) + entry.cost);
|
|
25703
|
+
return [...byModel.entries()].map(([model, cost]) => ({
|
|
25704
|
+
model,
|
|
25705
|
+
cost
|
|
25706
|
+
})).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
25707
|
+
};
|
|
25708
|
+
const costTotals = (entries) => ({
|
|
25709
|
+
total: entries.reduce((sum, entry) => sum + entry.cost, 0),
|
|
25710
|
+
byModel: costByModel(entries)
|
|
25711
|
+
});
|
|
25712
|
+
const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(function* () {
|
|
25713
|
+
const resolved = yield* workflowOf(config, run);
|
|
25714
|
+
const def = resolved.def;
|
|
25715
|
+
const startFlags = run.trace.length === 0 ? yield* StartFlags : {};
|
|
25716
|
+
const { vars, env } = settingsFor(resolved, run.pinnedVars, host.env, startOverrides, startFlags);
|
|
25717
|
+
const skills = yield* resolveScopeSkills(def, vars);
|
|
25718
|
+
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
25719
|
+
const budget = yield* try_({
|
|
25720
|
+
try: () => judgeBudgetBytes(vars),
|
|
25721
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
25722
|
+
});
|
|
25723
|
+
yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
|
|
25724
|
+
const base = run.episode.base === void 0 ? {
|
|
25725
|
+
hash: EMPTY_TREE$1,
|
|
25726
|
+
tree: treeFromRecord({})
|
|
25727
|
+
} : {
|
|
25728
|
+
hash: run.episode.base,
|
|
25729
|
+
tree: commitTree(workspace, run.episode.base)
|
|
25730
|
+
};
|
|
25731
|
+
const git = yield* GitService;
|
|
25732
|
+
const episode = [];
|
|
25733
|
+
for (const c of run.episode.commits) {
|
|
25734
|
+
const tip = parseCommitMessage(c.message).kept;
|
|
25735
|
+
const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
|
|
25736
|
+
episode.push({
|
|
25737
|
+
hash: c.hash,
|
|
25738
|
+
message: c.message,
|
|
25739
|
+
tree: commitTree(workspace, c.hash),
|
|
25740
|
+
...exists ? { kept: {
|
|
25741
|
+
hash: tip,
|
|
25742
|
+
tree: commitTree(workspace, tip)
|
|
25743
|
+
} } : {}
|
|
25744
|
+
});
|
|
25745
|
+
}
|
|
25746
|
+
let pendingMemo;
|
|
25747
|
+
return {
|
|
25748
|
+
resolved,
|
|
25749
|
+
def,
|
|
25750
|
+
vars,
|
|
25751
|
+
env,
|
|
25752
|
+
setup: {
|
|
25753
|
+
def,
|
|
25754
|
+
run,
|
|
25755
|
+
vars,
|
|
25756
|
+
env,
|
|
25757
|
+
budget,
|
|
25758
|
+
skills,
|
|
25759
|
+
access,
|
|
25760
|
+
workspace,
|
|
25761
|
+
base,
|
|
25762
|
+
episode,
|
|
25763
|
+
pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
|
|
25764
|
+
}
|
|
25765
|
+
};
|
|
25766
|
+
});
|
|
25767
|
+
const pendingChanges = (git, withIndex) => gen(function* () {
|
|
25768
|
+
const worktree = yield* git.changedPaths();
|
|
25769
|
+
if (!withIndex) return worktree;
|
|
25770
|
+
const seen = new Set(worktree.map((e) => e.path));
|
|
25771
|
+
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
|
|
25772
|
+
});
|
|
25773
|
+
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
25774
|
+
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
25775
|
+
const git = yield* GitService;
|
|
25776
|
+
const config = yield* (yield* ConfigService).load;
|
|
25777
|
+
const workspace = yield* Workspace;
|
|
25778
|
+
const host = yield* Host;
|
|
25779
|
+
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
25780
|
+
const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
|
|
25781
|
+
const outcome = yield* promise(() => replayFor(setup));
|
|
25782
|
+
const pinned = headHasPin(workspace, run);
|
|
25783
|
+
const pin = pinBeatAt(outcome, run, def, pinned);
|
|
25784
|
+
const resting = restingOn(outcome, pin);
|
|
25785
|
+
if (resting === void 0) return yield* fail$4(replayError(outcome) ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
|
|
25786
|
+
const { reached, step } = resting;
|
|
25787
|
+
const { stepDef, state, memory } = restFacts(pin, step, reached.trace, run);
|
|
25788
|
+
yield* (yield* Narrator).narrate(`rest resolved: ${state} (awaits ${stepDef.actor})`);
|
|
25789
|
+
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$4(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
25790
|
+
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
25791
|
+
status: normalizeStatus(e.status),
|
|
25792
|
+
path: e.path
|
|
25793
|
+
})) : [];
|
|
25794
|
+
if (ref === void 0 && config.drift.length > 0) yield* (yield* Narrator).warn(`gtd: notice: the live config differs from the pinned one (${config.drift.join(", ")}); \`gtd repin\` to switch`);
|
|
25795
|
+
return {
|
|
25796
|
+
def,
|
|
25797
|
+
workflowName: resolved.name,
|
|
25798
|
+
state,
|
|
25799
|
+
stepDef,
|
|
25800
|
+
actor: stepDef.actor,
|
|
25801
|
+
pin,
|
|
25802
|
+
pinned,
|
|
25803
|
+
step,
|
|
25804
|
+
trace: reached.trace,
|
|
25805
|
+
run,
|
|
25806
|
+
vars,
|
|
25807
|
+
env,
|
|
25808
|
+
changes,
|
|
25809
|
+
memory: memory.key,
|
|
25810
|
+
memoryResumed: memory.resumed,
|
|
25811
|
+
hints: hintsOf(stepDef),
|
|
25812
|
+
cost: costTotals(run.costEntries),
|
|
25813
|
+
setup
|
|
25814
|
+
};
|
|
25815
|
+
});
|
|
25816
|
+
const currentRest = restAt(void 0);
|
|
25817
|
+
/** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
|
|
25818
|
+
const restingOn = (outcome, pin) => {
|
|
25819
|
+
if (outcome.kind === "rest") return {
|
|
25820
|
+
reached: outcome,
|
|
25821
|
+
step: outcome.rest
|
|
25822
|
+
};
|
|
25823
|
+
if (outcome.kind !== "ended" || pin !== "remove") return void 0;
|
|
25824
|
+
const step = outcome.trace.at(-1);
|
|
25825
|
+
return step === void 0 ? void 0 : {
|
|
25826
|
+
reached: outcome,
|
|
25827
|
+
step
|
|
25828
|
+
};
|
|
25829
|
+
};
|
|
25830
|
+
/** A pin or unpin beat has its own script, no step's, and no agent memory. */
|
|
25831
|
+
const restFacts = (pin, step, trace, run) => pin === void 0 ? {
|
|
25832
|
+
stepDef: stepDefOf(step),
|
|
25833
|
+
state: step.name,
|
|
25834
|
+
memory: memoryOf(trace, run)
|
|
25835
|
+
} : {
|
|
25836
|
+
stepDef: pinStepDef(pin),
|
|
25837
|
+
state: PIN_STATES[pin],
|
|
25838
|
+
memory: {
|
|
25839
|
+
key: void 0,
|
|
25840
|
+
resumed: false
|
|
25841
|
+
}
|
|
25842
|
+
};
|
|
25843
|
+
const headHasPin = (workspace, run) => {
|
|
25844
|
+
const head = run.episode.commits.at(-1)?.hash ?? run.episode.base;
|
|
25845
|
+
return head !== void 0 && workspace.treeSync(head).has(".gtd/process.gtdpin");
|
|
25846
|
+
};
|
|
25847
|
+
const underwayAt = (run, step, def) => run.workflow !== void 0 || step.name !== def.initial || step.id.occurrence !== 1;
|
|
25851
25848
|
/**
|
|
25852
|
-
*
|
|
25853
|
-
*
|
|
25854
|
-
* value is knowable when gtd generates it.
|
|
25849
|
+
* A started process pins first — again, should a turn have deleted the pin;
|
|
25850
|
+
* a pinned flow that ended unpins before the closing commit.
|
|
25855
25851
|
*/
|
|
25856
|
-
const
|
|
25857
|
-
|
|
25858
|
-
|
|
25859
|
-
|
|
25860
|
-
|
|
25861
|
-
|
|
25862
|
-
|
|
25863
|
-
|
|
25864
|
-
|
|
25865
|
-
|
|
25866
|
-
|
|
25867
|
-
`"$(git log -1 --format=%s ${shellQuote(to)})"`,
|
|
25868
|
-
shellQuote(state)
|
|
25869
|
-
]);
|
|
25870
|
-
//#endregion
|
|
25871
|
-
//#region src/emit/ModeContradiction.ts
|
|
25852
|
+
const pinBeatAt = (outcome, run, def, pinned) => {
|
|
25853
|
+
if (flowEnded(outcome, run, def)) return pinned || run.pinCommitted ? "remove" : void 0;
|
|
25854
|
+
if (outcome.kind !== "rest" || pinned) return void 0;
|
|
25855
|
+
return underwayAt(run, outcome.rest, def) ? "add" : void 0;
|
|
25856
|
+
};
|
|
25857
|
+
const flowEnded = (outcome, run, def) => outcome.kind === "ended" || outcome.kind === "rest" && run.workflow === void 0 && outcome.rest.name === def.initial && underwayAt(run, outcome.rest, def);
|
|
25858
|
+
const pinStepDef = (pin) => ({
|
|
25859
|
+
actor: "check",
|
|
25860
|
+
kind: "script",
|
|
25861
|
+
content: pin === "add" ? pinScript(PIN_PATH) : unpinScript(PIN_PATH)
|
|
25862
|
+
});
|
|
25872
25863
|
/**
|
|
25873
|
-
*
|
|
25874
|
-
*
|
|
25875
|
-
*
|
|
25876
|
-
* thrash on the document) and the human (which mode, and the exact rendered
|
|
25877
|
-
* `format:` command, to fix `gtd.config.ts` without reproducing the failure).
|
|
25864
|
+
* Whether `gtd start <name>` can start a process here: the workflow must
|
|
25865
|
+
* reach a step from the tree the opening commit would capture. That step is
|
|
25866
|
+
* what the opening commit's subject names.
|
|
25878
25867
|
*/
|
|
25879
|
-
const
|
|
25868
|
+
const startRefusal = (rest, resolved, startVars) => gen(function* () {
|
|
25869
|
+
const host = yield* Host;
|
|
25870
|
+
const { vars, env } = settingsFor(resolved, {}, host.env, startVars, yield* StartFlags);
|
|
25871
|
+
const skills = yield* resolveScopeSkills(resolved.def, vars);
|
|
25872
|
+
const access = yield* resolveScopeAccess(resolved.def, skills, vars);
|
|
25873
|
+
const workspace = rest.setup.workspace;
|
|
25874
|
+
const outcome = yield* promise(() => replay({
|
|
25875
|
+
flow: resolved.def.flow,
|
|
25876
|
+
episode: {
|
|
25877
|
+
base: {
|
|
25878
|
+
hash: "",
|
|
25879
|
+
tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
|
|
25880
|
+
},
|
|
25881
|
+
commits: []
|
|
25882
|
+
},
|
|
25883
|
+
vars,
|
|
25884
|
+
env,
|
|
25885
|
+
start: "",
|
|
25886
|
+
budgetBytes: rest.setup.budget,
|
|
25887
|
+
skills,
|
|
25888
|
+
configuredSkills: resolved.def.configuredSkills,
|
|
25889
|
+
access,
|
|
25890
|
+
configuredAccess: resolved.def.configuredAccess
|
|
25891
|
+
}));
|
|
25892
|
+
if (outcome.kind === "rest") return { firstStep: outcome.rest.name };
|
|
25893
|
+
if (outcome.kind === "refused") return { refusal: outcome.message };
|
|
25894
|
+
const why = outcome.kind === "ended" ? "the flow reaches no step" : outcome.message;
|
|
25895
|
+
return { refusal: `workflow "${resolved.name}" cannot start — ${why}` };
|
|
25896
|
+
});
|
|
25897
|
+
/** The review window's diff base at the rest. */
|
|
25898
|
+
const reviewBaseFor = (rest) => rest.step.request.options.base ?? rest.run.diffBase;
|
|
25899
|
+
const renderRest = (rest) => succeed$1({
|
|
25900
|
+
state: rest.state,
|
|
25901
|
+
actor: rest.actor,
|
|
25902
|
+
kind: rest.stepDef.kind,
|
|
25903
|
+
content: rest.stepDef.content,
|
|
25904
|
+
...rest.hints,
|
|
25905
|
+
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
25906
|
+
memoryResumed: rest.memoryResumed,
|
|
25907
|
+
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
25908
|
+
});
|
|
25880
25909
|
/**
|
|
25881
|
-
*
|
|
25882
|
-
*
|
|
25883
|
-
* mode whose `format:` contradicts its own `validate:` without ever touching
|
|
25884
|
-
* the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
|
|
25885
|
-
* heredoc, since no delimiter chosen ahead of time can collide with the
|
|
25886
|
-
* sample's own bytes. One bash fragment with no blank lines (so
|
|
25887
|
-
* `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
|
|
25888
|
-
* to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
|
|
25889
|
-
* still fires at a first-write beat where the real steering file doesn't
|
|
25890
|
-
* exist yet.
|
|
25910
|
+
* Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
|
|
25911
|
+
* attempt at this very agent rest, and another dispatch would repeat it.
|
|
25891
25912
|
*/
|
|
25892
|
-
const
|
|
25893
|
-
|
|
25894
|
-
|
|
25895
|
-
|
|
25896
|
-
|
|
25897
|
-
|
|
25898
|
-
|
|
25899
|
-
|
|
25900
|
-
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
`
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25913
|
+
const stalledAt = (rest) => rest.changes.length === 0 && rest.stepDef.kind === "prompt" && rest.stepDef.allowEmpty !== true && rest.run.headTurn?.state === rest.state && rest.run.headTurn.actor === rest.actor && !rest.run.headTurn.step && rest.run.headTurn.empty;
|
|
25914
|
+
/** No process is underway: the default workflow's first step, first visit — a dirty tree there is a turn not yet landed, not a process. */
|
|
25915
|
+
const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
|
|
25916
|
+
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
25917
|
+
const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
|
|
25918
|
+
const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
|
|
25919
|
+
const approvalRefusal = (rest, approver) => {
|
|
25920
|
+
if (rest.stepDef.approve !== true) return void 0;
|
|
25921
|
+
const id = rest.step.id;
|
|
25922
|
+
if (rest.changes.length > 0) return {
|
|
25923
|
+
kind: "refusal",
|
|
25924
|
+
message: `gtd land: approval-gate: ${formatStepId(id)} approves exactly the judged diff — revert your edits and run \`gtd land\` on a clean tree to approve, or run \`gtd abandon\``
|
|
25925
|
+
};
|
|
25926
|
+
if (isNone(approver)) return {
|
|
25927
|
+
kind: "refusal",
|
|
25928
|
+
message: `gtd land: approval-gate: ${formatStepId(id)} records who approved it, but git has no author identity — set user.name and user.email`
|
|
25929
|
+
};
|
|
25930
|
+
if (!isRecordableApproval({
|
|
25931
|
+
step: id,
|
|
25932
|
+
...approver.value
|
|
25933
|
+
})) return {
|
|
25934
|
+
kind: "refusal",
|
|
25935
|
+
message: `gtd land: approval-gate: ${formatStepId(id)} cannot record ${approver.value.name} <${approver.value.email}> as approver — it would not read back unchanged from its Gtd-Approved trailer (a control character, for example)`
|
|
25936
|
+
};
|
|
25907
25937
|
};
|
|
25908
|
-
/**
|
|
25909
|
-
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
25913
|
-
|
|
25914
|
-
|
|
25915
|
-
|
|
25938
|
+
/** The landing a clean tree decides before any replay, if it decides one. */
|
|
25939
|
+
const cleanLanding = (rest) => {
|
|
25940
|
+
if (rest.changes.length > 0) return void 0;
|
|
25941
|
+
const def = rest.stepDef;
|
|
25942
|
+
if (def.kind === "prompt" && def.allowEmpty !== true) return {
|
|
25943
|
+
kind: "attempt",
|
|
25944
|
+
subject: formatSubject(rest.actor, rest.state)
|
|
25945
|
+
};
|
|
25946
|
+
if (rest.actor === "human" && def.acceptClean !== true) return {
|
|
25947
|
+
kind: "noop",
|
|
25948
|
+
settled: false
|
|
25949
|
+
};
|
|
25950
|
+
};
|
|
25951
|
+
/** At the human review gate the pending tree lands with every tick cleared, as the landing script will. */
|
|
25952
|
+
const reviewGateRewrite = (def) => {
|
|
25953
|
+
const reviewFormat = steeringFormatFor("review");
|
|
25954
|
+
if (!isHumanReviewGate(def) || def.file === void 0 || reviewFormat === void 0) return;
|
|
25955
|
+
return {
|
|
25956
|
+
path: def.file,
|
|
25957
|
+
apply: (content) => reviewFormat.clearTicks(content)
|
|
25958
|
+
};
|
|
25959
|
+
};
|
|
25960
|
+
const multilineRefusal = (vars) => {
|
|
25961
|
+
const multiline = multilineSetting(vars);
|
|
25962
|
+
return multiline === void 0 ? void 0 : {
|
|
25963
|
+
kind: "refusal",
|
|
25964
|
+
message: `gtd: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`
|
|
25965
|
+
};
|
|
25966
|
+
};
|
|
25967
|
+
const landingTarget = (outcome, rest) => {
|
|
25968
|
+
if (outcome.kind === "refused") return succeed$1({
|
|
25969
|
+
kind: "refusal",
|
|
25970
|
+
message: outcome.message
|
|
25971
|
+
});
|
|
25972
|
+
if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$4(new Error(outcome.message));
|
|
25973
|
+
if (outcome.kind !== "rest") return succeed$1(rest.def.initial);
|
|
25974
|
+
const reserved = initialStepRefusal(rest.run.workflow, outcome.rest.name, rest.def.initial);
|
|
25975
|
+
return succeed$1(reserved === void 0 ? outcome.rest.name : reserved);
|
|
25916
25976
|
};
|
|
25917
|
-
//#endregion
|
|
25918
|
-
//#region src/emit/SteeringMode.ts
|
|
25919
25977
|
/**
|
|
25920
|
-
*
|
|
25921
|
-
*
|
|
25922
|
-
* that
|
|
25923
|
-
* other command. Each arm returns directly — a flat sequence, not nested
|
|
25924
|
-
* ternaries — so the four outcomes stay each their own case to read, not a
|
|
25925
|
-
* boolean expression to re-derive.
|
|
25978
|
+
* A step commit entering the default's first step is what ends an episode, so
|
|
25979
|
+
* a named workflow landing there would read back as finished. Its opening
|
|
25980
|
+
* commit may name that step: a bare opening is never read as a boundary.
|
|
25926
25981
|
*/
|
|
25927
|
-
const
|
|
25928
|
-
|
|
25929
|
-
|
|
25930
|
-
|
|
25931
|
-
|
|
25982
|
+
const initialStepRefusal = (workflow, step, initial) => workflow !== void 0 && step === initial ? {
|
|
25983
|
+
kind: "refusal",
|
|
25984
|
+
message: `gtd: workflow "${workflow}" reaches step "${step}", the default workflow's first step — that is where a finished process waits, so a started process must never land there; rename the step`
|
|
25985
|
+
} : void 0;
|
|
25986
|
+
const closingTrailers = (branch) => ({
|
|
25987
|
+
version: GTD_VERSION,
|
|
25988
|
+
...isSome(branch) ? { branch: branch.value } : {}
|
|
25989
|
+
});
|
|
25990
|
+
const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
|
|
25991
|
+
const keptTurn = (rest) => rest.stepDef.keep === true && rest.changes.length > 0 ? { keepTurn: keptRef(rest.run.startHash, rest.state) } : {};
|
|
25992
|
+
const approvedTrailer = (rest, approver) => isSome(approver) ? { approved: {
|
|
25993
|
+
step: rest.step.id,
|
|
25994
|
+
...approver.value
|
|
25995
|
+
} } : {};
|
|
25996
|
+
const landingTo = (rest, to, branch, approver, marks) => {
|
|
25997
|
+
if (isSettledNoop(rest, to)) return {
|
|
25998
|
+
kind: "noop",
|
|
25999
|
+
settled: true
|
|
25932
26000
|
};
|
|
25933
|
-
|
|
25934
|
-
|
|
25935
|
-
|
|
25936
|
-
|
|
26001
|
+
const underway = !noProcessUnderway(rest);
|
|
26002
|
+
const starts = !underway && to !== rest.def.initial;
|
|
26003
|
+
const refused = starts ? multilineRefusal(rest.vars) : void 0;
|
|
26004
|
+
if (refused !== void 0) return refused;
|
|
26005
|
+
const { finishes, landsAt } = endingOf(rest, to, underway);
|
|
26006
|
+
return {
|
|
26007
|
+
kind: "commit",
|
|
26008
|
+
to: landsAt,
|
|
26009
|
+
spec: {
|
|
26010
|
+
actor: rest.actor,
|
|
26011
|
+
from: rest.state,
|
|
26012
|
+
to: landsAt,
|
|
26013
|
+
step: rest.step.id,
|
|
26014
|
+
...marks.length > 0 && { marks },
|
|
26015
|
+
...approvedTrailer(rest, approver),
|
|
26016
|
+
...startTrailers(rest, starts),
|
|
26017
|
+
...finishes ? closingTrailers(branch) : {}
|
|
26018
|
+
},
|
|
26019
|
+
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {},
|
|
26020
|
+
...keptTurn(rest)
|
|
25937
26021
|
};
|
|
26022
|
+
};
|
|
26023
|
+
const endingOf = (rest, to, underway) => {
|
|
26024
|
+
const ends = underway && to === rest.def.initial;
|
|
25938
26025
|
return {
|
|
25939
|
-
|
|
25940
|
-
|
|
26026
|
+
finishes: ends && !rest.pinned,
|
|
26027
|
+
landsAt: ends && rest.pinned ? PIN_STATES.remove : to
|
|
26028
|
+
};
|
|
26029
|
+
};
|
|
26030
|
+
/** A pin or unpin commit carries the pin alone; the unpin commit closes the process. */
|
|
26031
|
+
const pinLanding = (rest, pin, branch) => {
|
|
26032
|
+
const change = rest.changes.find((c) => c.path === PIN_PATH)?.status;
|
|
26033
|
+
return pin === "add" ? pinCommit(rest, change) : unpinCommit(rest, change, branch);
|
|
26034
|
+
};
|
|
26035
|
+
const pinCommit = (rest, change) => {
|
|
26036
|
+
if (change === void 0 || change === "D") return pinRefusal("add");
|
|
26037
|
+
const to = rest.step.name;
|
|
26038
|
+
return {
|
|
26039
|
+
kind: "commit",
|
|
26040
|
+
to,
|
|
26041
|
+
spec: {
|
|
26042
|
+
actor: rest.actor,
|
|
26043
|
+
from: rest.state,
|
|
26044
|
+
to,
|
|
26045
|
+
pin: "add"
|
|
26046
|
+
},
|
|
26047
|
+
only: PIN_PATH
|
|
26048
|
+
};
|
|
26049
|
+
};
|
|
26050
|
+
const unpinCommit = (rest, change, branch) => {
|
|
26051
|
+
if (change !== "D" && rest.pinned) return pinRefusal("remove");
|
|
26052
|
+
const to = rest.def.initial;
|
|
26053
|
+
return {
|
|
26054
|
+
kind: "commit",
|
|
26055
|
+
to,
|
|
26056
|
+
spec: {
|
|
26057
|
+
actor: rest.actor,
|
|
26058
|
+
from: rest.state,
|
|
26059
|
+
to,
|
|
26060
|
+
pin: "remove",
|
|
26061
|
+
...closingTrailers(branch)
|
|
26062
|
+
},
|
|
26063
|
+
keepHistory: processHistoryRef(rest.run.startHash),
|
|
26064
|
+
...rest.pinned ? { only: PIN_PATH } : {}
|
|
25941
26065
|
};
|
|
25942
26066
|
};
|
|
26067
|
+
const pinRefusal = (pin) => ({
|
|
26068
|
+
kind: "refusal",
|
|
26069
|
+
message: pin === "add" ? `gtd land: ${PIN_PATH} is missing — run the pin beat's script (\`gtd next --json=content\`); if its \`gtd bundle\` fails, fix what it reports, or run \`gtd abandon\`` : `gtd land: ${PIN_PATH} is still there — run the unpin beat's script (\`gtd next --json=content\`), then land`
|
|
26070
|
+
});
|
|
26071
|
+
const startTrailers = (rest, starts) => starts ? { vars: rest.vars } : {};
|
|
26072
|
+
const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "ended" ? outcome.marks : [];
|
|
25943
26073
|
/**
|
|
25944
|
-
*
|
|
25945
|
-
*
|
|
25946
|
-
*
|
|
25947
|
-
* parser when the name is registered. `format` is set from the registry
|
|
25948
|
-
* ALONE, independent of which half of `validate` wins — a declared validator
|
|
25949
|
-
* overrides validation, not the format identity.
|
|
25950
|
-
*
|
|
25951
|
-
* `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
|
|
25952
|
-
* against the registry alone, with no workflow definition in hand (e.g. the
|
|
25953
|
-
* basename dispatch for a `REVIEW.md` no config maps).
|
|
25954
|
-
*
|
|
25955
|
-
* The `"unknown"` arm carries the whole refusal message, naming what IS known
|
|
25956
|
-
* when `def` is given — this is the one place that message is built, so it
|
|
25957
|
-
* can never drift from what actually resolved.
|
|
26074
|
+
* What landing the pending turn at `rest` does — a pure decision from a
|
|
26075
|
+
* replay with the working tree as the pending turn. The target step is
|
|
26076
|
+
* computed by replaying, never matched.
|
|
25958
26077
|
*/
|
|
25959
|
-
const
|
|
25960
|
-
|
|
25961
|
-
const
|
|
25962
|
-
if (
|
|
25963
|
-
|
|
25964
|
-
|
|
26078
|
+
const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
26079
|
+
if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
|
|
26080
|
+
const unapproved = approvalRefusal(rest, approver);
|
|
26081
|
+
if (unapproved !== void 0) return unapproved;
|
|
26082
|
+
const early = cleanLanding(rest);
|
|
26083
|
+
if (early !== void 0) return early;
|
|
26084
|
+
if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
|
|
26085
|
+
const message = accessRefusal(rest.changes, rest.stepDef.access.write);
|
|
26086
|
+
if (message !== void 0) return {
|
|
26087
|
+
kind: "refusal",
|
|
26088
|
+
message
|
|
26089
|
+
};
|
|
26090
|
+
}
|
|
26091
|
+
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
26092
|
+
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
26093
|
+
...verdicts !== void 0 ? { verdicts } : {}
|
|
26094
|
+
}));
|
|
26095
|
+
const to = yield* landingTarget(outcome, rest);
|
|
26096
|
+
if (typeof to !== "string") return to;
|
|
26097
|
+
return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
26098
|
+
});
|
|
26099
|
+
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
26100
|
+
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$1(void 0)));
|
|
26101
|
+
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
26102
|
+
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
26103
|
+
const git = yield* GitService;
|
|
26104
|
+
const branch = yield* git.currentBranch();
|
|
26105
|
+
const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none();
|
|
26106
|
+
const landing = yield* decideLanding(rest, verdicts, branch, approver);
|
|
26107
|
+
return {
|
|
26108
|
+
stepDef: rest.stepDef,
|
|
26109
|
+
state: rest.state,
|
|
26110
|
+
actor: rest.actor,
|
|
26111
|
+
changes: rest.changes,
|
|
26112
|
+
file: rest.hints.file,
|
|
26113
|
+
landing
|
|
25965
26114
|
};
|
|
25966
|
-
|
|
25967
|
-
|
|
25968
|
-
|
|
25969
|
-
|
|
25970
|
-
|
|
25971
|
-
|
|
25972
|
-
|
|
26115
|
+
});
|
|
26116
|
+
const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
|
|
26117
|
+
hash: entry.hash,
|
|
26118
|
+
state: entry.state
|
|
26119
|
+
}));
|
|
26120
|
+
const summaryInput = (run, settings) => {
|
|
26121
|
+
const entryCommit = run.trace[0].hash;
|
|
26122
|
+
const { total, byModel } = costTotals(run.costEntries);
|
|
25973
26123
|
return {
|
|
25974
|
-
|
|
25975
|
-
|
|
25976
|
-
|
|
25977
|
-
|
|
25978
|
-
|
|
25979
|
-
|
|
26124
|
+
entryCommit,
|
|
26125
|
+
processBase: run.startParentHash,
|
|
26126
|
+
processTip: run.trace[run.trace.length - 1].hash,
|
|
26127
|
+
humanCommits: humanCommitsOf(run.trace, entryCommit),
|
|
26128
|
+
processCost: total,
|
|
26129
|
+
processCostByModel: byModel,
|
|
26130
|
+
marks: run.marks,
|
|
26131
|
+
...settings
|
|
25980
26132
|
};
|
|
25981
26133
|
};
|
|
25982
|
-
|
|
25983
|
-
const
|
|
25984
|
-
|
|
25985
|
-
|
|
25986
|
-
|
|
25987
|
-
|
|
25988
|
-
|
|
25989
|
-
|
|
25990
|
-
*/
|
|
25991
|
-
const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
|
|
25992
|
-
const binary = extractLeadingBinary(command);
|
|
25993
|
-
if (binary !== void 0) steps.push({
|
|
25994
|
-
kind: "command",
|
|
25995
|
-
command: binaryGuard(binary, mode, key)
|
|
25996
|
-
});
|
|
25997
|
-
steps.push({
|
|
25998
|
-
kind: "command",
|
|
25999
|
-
command: withFileVar(command, file),
|
|
26000
|
-
...onFailure !== void 0 ? { onFailure } : {}
|
|
26001
|
-
});
|
|
26002
|
-
};
|
|
26134
|
+
const summaryFor = (run) => gen(function* () {
|
|
26135
|
+
const config = yield* (yield* ConfigService).load;
|
|
26136
|
+
const host = yield* Host;
|
|
26137
|
+
const resolved = yield* workflowOf(config, run);
|
|
26138
|
+
const summary = resolved.def.summary;
|
|
26139
|
+
if (summary === void 0 || run.trace.length === 0) return { prompt: void 0 };
|
|
26140
|
+
return { prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))) };
|
|
26141
|
+
});
|
|
26003
26142
|
/**
|
|
26004
|
-
*
|
|
26005
|
-
*
|
|
26006
|
-
*
|
|
26007
|
-
* `gtd check`'s seeded command carries that instead). Each command gets its
|
|
26008
|
-
* own `binaryGuard` immediately ahead of it.
|
|
26143
|
+
* Repository-relative paths of today's config files; one outside the
|
|
26144
|
+
* repository cannot be read at a ref, so it is left out. A file only the old
|
|
26145
|
+
* config read needs no check: whichever file imported it differs too.
|
|
26009
26146
|
*/
|
|
26010
|
-
const
|
|
26011
|
-
const
|
|
26012
|
-
|
|
26013
|
-
|
|
26014
|
-
return steps;
|
|
26147
|
+
const inRepoConfigPaths = (root, files) => {
|
|
26148
|
+
const paths = /* @__PURE__ */ new Set();
|
|
26149
|
+
for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
|
|
26150
|
+
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
|
|
26015
26151
|
};
|
|
26016
|
-
|
|
26017
|
-
const
|
|
26018
|
-
|
|
26019
|
-
*
|
|
26020
|
-
|
|
26021
|
-
* built-in validator runs the round-trip against that format's own canonical
|
|
26022
|
-
* sample; an external validator prints a one-line skip notice instead, since
|
|
26023
|
-
* silence there would read as a clean bill of health; a format-only mode with
|
|
26024
|
-
* neither has nothing to round-trip either.
|
|
26025
|
-
*/
|
|
26026
|
-
const modeContradictionSteps = (resolved) => gen(function* () {
|
|
26027
|
-
if (resolved.formatCommand === void 0) return [];
|
|
26028
|
-
const { capabilities } = resolved;
|
|
26029
|
-
if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
|
|
26030
|
-
const { scratchDir } = yield* Host;
|
|
26031
|
-
const samplePath = scratchSamplePath(scratchDir, resolved.mode);
|
|
26032
|
-
const formatCommand = withFileVar(resolved.formatCommand, samplePath);
|
|
26033
|
-
return [{
|
|
26034
|
-
kind: "command",
|
|
26035
|
-
command: buildModeContradictionCheck({
|
|
26036
|
-
mode: resolved.mode,
|
|
26037
|
-
samplePath,
|
|
26038
|
-
sample: capabilities.format.sample,
|
|
26039
|
-
formatCommand
|
|
26040
|
-
})
|
|
26041
|
-
}];
|
|
26042
|
-
}
|
|
26043
|
-
if (capabilities.externalValidate === true) return [{
|
|
26044
|
-
kind: "command",
|
|
26045
|
-
command: modeContradictionSkipNotice(resolved.mode)
|
|
26046
|
-
}];
|
|
26047
|
-
return [];
|
|
26152
|
+
const configRefusal = (command, tip, paths, reason = "A history ref replays under today's config, so it is unreadable after the config changes") => /* @__PURE__ */ new Error(`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`);
|
|
26153
|
+
const differingConfigPaths = (git, workspace, root, config, hash) => gen(function* () {
|
|
26154
|
+
const differing = [];
|
|
26155
|
+
for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint))) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
|
|
26156
|
+
return differing;
|
|
26048
26157
|
});
|
|
26158
|
+
const notClosedError = (hash, outcome) => outcome.kind === "rest" ? /* @__PURE__ */ new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`) : replayError(outcome) ?? /* @__PURE__ */ new Error("gtd history: replay failed");
|
|
26159
|
+
const historySteps = (run) => {
|
|
26160
|
+
const messages = new Map(run.episode.commits.map((c) => [c.hash, c.message]));
|
|
26161
|
+
return run.trace.map((entry) => {
|
|
26162
|
+
const message = parseCommitMessage(messages.get(entry.hash) ?? "");
|
|
26163
|
+
return {
|
|
26164
|
+
hash: entry.hash,
|
|
26165
|
+
subject: (messages.get(entry.hash) ?? "").split("\n")[0] ?? "",
|
|
26166
|
+
actor: entry.actor,
|
|
26167
|
+
from: message.parsed?.from,
|
|
26168
|
+
to: entry.state,
|
|
26169
|
+
verdicts: message.judge,
|
|
26170
|
+
...optional("approval", approvalEntryOf(entry.hash, message)[0])
|
|
26171
|
+
};
|
|
26172
|
+
});
|
|
26173
|
+
};
|
|
26174
|
+
const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd: warning: this history was recorded by ${recorded === void 0 ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} — the engine the workflow runs on may have changed since`;
|
|
26049
26175
|
/**
|
|
26050
|
-
*
|
|
26051
|
-
*
|
|
26052
|
-
*
|
|
26053
|
-
* runs this script itself — `resolveMode`+`validateScriptFor` only render
|
|
26054
|
-
* text for a driver to execute (`gtd renders; the driver executes`).
|
|
26176
|
+
* Replays the closed run at `tip` under today's config. The workflow is not
|
|
26177
|
+
* fingerprinted for a version change: the engine under it moves with every
|
|
26178
|
+
* upgrade, and the version warning is the only signal.
|
|
26055
26179
|
*/
|
|
26056
|
-
const
|
|
26057
|
-
|
|
26058
|
-
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26180
|
+
const historyTrail = (tip) => gen(function* () {
|
|
26181
|
+
const git = yield* GitService;
|
|
26182
|
+
const config = yield* (yield* ConfigService).load;
|
|
26183
|
+
const hash = yield* git.resolveRef(tip);
|
|
26184
|
+
const subject = yield* git.lastCommitSubject(hash);
|
|
26185
|
+
const run = yield* runAtTip(git, config.workflow, {
|
|
26186
|
+
hash,
|
|
26187
|
+
subject
|
|
26188
|
+
});
|
|
26189
|
+
const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
|
|
26190
|
+
command: "gtd history",
|
|
26191
|
+
notEnded: (o) => notClosedError(hash, o)
|
|
26192
|
+
});
|
|
26193
|
+
return {
|
|
26194
|
+
tip: hash,
|
|
26195
|
+
steps: historySteps(run),
|
|
26196
|
+
ended: outcome.via,
|
|
26197
|
+
version: {
|
|
26198
|
+
recorded,
|
|
26199
|
+
running: GTD_VERSION
|
|
26062
26200
|
},
|
|
26063
|
-
...
|
|
26064
|
-
|
|
26201
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
26202
|
+
};
|
|
26203
|
+
});
|
|
26204
|
+
/** Refuses on config drift before replaying, so a stale config never runs the flow. */
|
|
26205
|
+
const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
26206
|
+
const git = yield* GitService;
|
|
26207
|
+
const workspace = yield* Workspace;
|
|
26208
|
+
const host = yield* Host;
|
|
26209
|
+
const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
|
|
26210
|
+
const versionWarning = versionWarningFor(recorded);
|
|
26211
|
+
const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
|
|
26212
|
+
const differing = yield* differingConfigPaths(git, workspace, host.root, config, hash);
|
|
26213
|
+
if (differing.length > 0) return yield* fail$4(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
|
|
26214
|
+
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
26215
|
+
const outcome = yield* promise(() => replayFor(setup));
|
|
26216
|
+
if (outcome.kind !== "ended") return yield* fail$4(withWarning(how.notEnded(outcome)));
|
|
26217
|
+
return {
|
|
26218
|
+
outcome,
|
|
26219
|
+
recorded,
|
|
26220
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
26221
|
+
};
|
|
26065
26222
|
});
|
|
26066
26223
|
//#endregion
|
|
26067
26224
|
//#region src/cli/ToolCommands.ts
|
|
@@ -26118,7 +26275,7 @@ const runStraysCommand = (command, json, out) => gen(function* () {
|
|
|
26118
26275
|
const narrator = yield* Narrator;
|
|
26119
26276
|
const base = yield* resolveSide(git, command.base);
|
|
26120
26277
|
const tip = yield* resolveSide(git, command.tip);
|
|
26121
|
-
const touched = yield* git.diffPaths(base, tip);
|
|
26278
|
+
const touched = (yield* git.diffPaths(base, tip)).filter((path) => path !== PIN_PATH);
|
|
26122
26279
|
const strays = strayPaths(touched, command.globs);
|
|
26123
26280
|
const total = touched.length;
|
|
26124
26281
|
if (json.kind === "select") yield* writeSelection(out, strays, json.path).pipe(catchIf((error) => error instanceof SelectorUsageError && /^\d+$/.test(json.path), () => void_));
|
|
@@ -27014,8 +27171,7 @@ const startWorkflow = (workflow, varOverrides, json, out, commandLabel) => gen(f
|
|
|
27014
27171
|
workflow: resolved,
|
|
27015
27172
|
first: started,
|
|
27016
27173
|
commandLabel,
|
|
27017
|
-
vars: varOverrides
|
|
27018
|
-
config: rest.config
|
|
27174
|
+
vars: varOverrides
|
|
27019
27175
|
});
|
|
27020
27176
|
if (plan.kind === "refusal") return yield* fail$4(new Error(plan.message));
|
|
27021
27177
|
yield* writeScript(json, out, listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
|
|
@@ -27127,6 +27283,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
27127
27283
|
kind: "gitWrite",
|
|
27128
27284
|
command: mixedResetTo(run.startParentHash)
|
|
27129
27285
|
},
|
|
27286
|
+
{
|
|
27287
|
+
kind: "gitWrite",
|
|
27288
|
+
command: unpinScript(PIN_PATH)
|
|
27289
|
+
},
|
|
27130
27290
|
{
|
|
27131
27291
|
kind: "outcome",
|
|
27132
27292
|
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
@@ -27284,7 +27444,7 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
|
|
|
27284
27444
|
if (rest.run.trace.length === 0) return;
|
|
27285
27445
|
const git = yield* GitService;
|
|
27286
27446
|
const workspace = yield* Workspace;
|
|
27287
|
-
const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D").flatMap(({ path }) => {
|
|
27447
|
+
const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D" && c.path !== ".gtd/process.gtdpin").flatMap(({ path }) => {
|
|
27288
27448
|
const { threads, findings } = parseCodeThreads(path, workspace.atPath(path) ?? "");
|
|
27289
27449
|
return [...threads.filter((t) => t.waitingOn === "human").map((t) => `${path}:${t.line + 1}: ${t.entries[0].text}`), ...findings.map((f) => `${path}:${(f.line ?? 0) + 1}: ${f.message}`)];
|
|
27290
27450
|
});
|
|
@@ -28013,6 +28173,7 @@ const flagHeader = (row) => {
|
|
|
28013
28173
|
};
|
|
28014
28174
|
const GLOBAL_OPTIONS = [
|
|
28015
28175
|
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
28176
|
+
["--live", "Run this gtd, not the process's pin; it may misread the process"],
|
|
28016
28177
|
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
28017
28178
|
["-V, --version", "Print the version"],
|
|
28018
28179
|
["--", "End the options: every word after it is an argument"]
|
|
@@ -28024,7 +28185,7 @@ const EXTRA_TOPICS = [[
|
|
|
28024
28185
|
], [
|
|
28025
28186
|
"version",
|
|
28026
28187
|
"Print the version",
|
|
28027
|
-
"Print gtd's version."
|
|
28188
|
+
"Print gtd's version, and the version of a pinned process's gtd."
|
|
28028
28189
|
]];
|
|
28029
28190
|
const EXAMPLES = ` $ gtd start list what can start
|
|
28030
28191
|
$ gtd start <name> start a process
|
|
@@ -28865,6 +29026,17 @@ var InMemRepo = class InMemRepo {
|
|
|
28865
29026
|
this.index = new Map(this.worktree);
|
|
28866
29027
|
this.resetHard();
|
|
28867
29028
|
}
|
|
29029
|
+
/** Twin of `GitScript.commitPath`: commits `path` as the worktree has it, every other staged change left staged. */
|
|
29030
|
+
commitPath(path, message) {
|
|
29031
|
+
if (!this.worktree.has(path) && !this.headTree().has(path) && !this.index.has(path)) return `error: pathspec '${path}' did not match any file(s) known to git`;
|
|
29032
|
+
const staged = new Map(this.index);
|
|
29033
|
+
this.index = new Map(this.headTree());
|
|
29034
|
+
this.stage(path);
|
|
29035
|
+
const failure = this.commitAsIs(message);
|
|
29036
|
+
this.index = staged;
|
|
29037
|
+
this.stage(path);
|
|
29038
|
+
return failure;
|
|
29039
|
+
}
|
|
28868
29040
|
commitAllWithPrefix(prefix) {
|
|
28869
29041
|
this.index = new Map(this.worktree);
|
|
28870
29042
|
return this.commitAsIs(prefix);
|
|
@@ -29631,6 +29803,16 @@ const recognizeGitBuilders = (repo, block) => {
|
|
|
29631
29803
|
}
|
|
29632
29804
|
const conditionLine = conditionStatement(block);
|
|
29633
29805
|
if (block.startsWith("git add -A || exit 1\ngtd_sign_cfg=")) return recognizeKeepTurn(repo, block);
|
|
29806
|
+
if (block.startsWith("git add -A -- ")) {
|
|
29807
|
+
const [path, message] = extractQuotedTokens(block);
|
|
29808
|
+
if (path === void 0 || message === void 0 || commitPath(path, message) !== block) return;
|
|
29809
|
+
const failure = repo.commitPath(path, message);
|
|
29810
|
+
return failure === void 0 ? { kind: "applied" } : {
|
|
29811
|
+
kind: "failed",
|
|
29812
|
+
error: failure,
|
|
29813
|
+
status: 1
|
|
29814
|
+
};
|
|
29815
|
+
}
|
|
29634
29816
|
if (block.startsWith("git add -A || exit 1\n")) {
|
|
29635
29817
|
const [message] = extractQuotedTokens(conditionLine);
|
|
29636
29818
|
if (message !== void 0 && commitAll(message) === block) {
|
|
@@ -29708,6 +29890,19 @@ const recognizeGitBuilders = (repo, block) => {
|
|
|
29708
29890
|
return;
|
|
29709
29891
|
}
|
|
29710
29892
|
};
|
|
29893
|
+
/** What the in-memory `gtd bundle` prints: a pin header, and no engine behind it. */
|
|
29894
|
+
const IN_MEMORY_PIN = "gtdpin 1 gtd in-memory\n";
|
|
29895
|
+
/** A pin or unpin beat's script. */
|
|
29896
|
+
const recognizePinScripts = (repo, block) => {
|
|
29897
|
+
if (block === pinScript(".gtd/process.gtdpin")) {
|
|
29898
|
+
repo.writeFile(PIN_PATH, IN_MEMORY_PIN);
|
|
29899
|
+
return { kind: "applied" };
|
|
29900
|
+
}
|
|
29901
|
+
if (block === unpinScript(".gtd/process.gtdpin")) {
|
|
29902
|
+
if (repo.readFile(".gtd/process.gtdpin") !== void 0) repo.deleteFile(PIN_PATH);
|
|
29903
|
+
return { kind: "applied" };
|
|
29904
|
+
}
|
|
29905
|
+
};
|
|
29711
29906
|
/** The REAL `fileExistsGuard` block, re-derived from the extracted path and string-compared. The one recognizer that can report `{ kind: "stopped" }`: a real `exit 0` there ends the script successfully. */
|
|
29712
29907
|
const recognizeFileExistsGuard = (repo, block) => {
|
|
29713
29908
|
const [file] = extractQuotedTokens(block);
|
|
@@ -30073,6 +30268,7 @@ const recognizersFor = (repo, commands, git) => [
|
|
|
30073
30268
|
(block) => recognizeBinaryGuard(commands, block),
|
|
30074
30269
|
(block) => git ? recognizeGitBuilders(repo, block) : void 0,
|
|
30075
30270
|
(block) => recognizeWriteLines(repo, block),
|
|
30271
|
+
(block) => recognizePinScripts(repo, block),
|
|
30076
30272
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
30077
30273
|
(block) => recognizePresentationSubshell(repo, commands, block, git),
|
|
30078
30274
|
(block) => recognizeModeContradictionSkipNotice(block),
|
|
@@ -30310,12 +30506,16 @@ const expectedVerdict = (doc) => {
|
|
|
30310
30506
|
return ["expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:", `[${questions.map(({ id, primitive }) => `{ "id": ${JSON.stringify(id)}, "answer": ${ANSWER[primitive] ?? "\"<answer>\""}, "p": 0.9 }`).join(", ")}]`].join("\n");
|
|
30311
30507
|
};
|
|
30312
30508
|
const quoted = (names) => names.map((name) => `"${name}"`).join(", ");
|
|
30313
|
-
/** Who acts at `doc`: its scripted actor, or
|
|
30509
|
+
/** Who acts at `doc`: its scripted actor, or its own script at a pin beat or a check under `checks: "run"`. A stall has none. */
|
|
30314
30510
|
const actorAt = (doc, actors, checks) => {
|
|
30315
30511
|
if (doc.kind === "stalled") return {
|
|
30316
30512
|
scripted: false,
|
|
30317
30513
|
actor: void 0
|
|
30318
30514
|
};
|
|
30515
|
+
if (doc.pin !== void 0) return {
|
|
30516
|
+
scripted: false,
|
|
30517
|
+
actor: runsItsScript
|
|
30518
|
+
};
|
|
30319
30519
|
if (Object.hasOwn(actors, doc.state)) return {
|
|
30320
30520
|
scripted: true,
|
|
30321
30521
|
actor: actors[doc.state]
|