@pmelab/gtd 60.1.0 → 61.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 +7 -0
- package/dist/chunks/{testkit-BhTey1Do.mjs → testkit-C9sq0x07.mjs} +1695 -1454
- package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
- package/dist/gtd.bundle.mjs +1666 -1449
- package/dist/gtd.core.mjs +86 -86
- 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/History.test.ts +10 -1
- package/src/cli/Metrics.test.ts +10 -1
- package/src/cli/Program.test.ts +30 -19
- package/src/cli/Program.ts +4 -1
- package/src/cli/Strays.ts +3 -2
- package/src/edge/Edge.test.ts +78 -25
- package/src/edge/Edge.ts +161 -28
- 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/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 +12 -12
- package/src/replay/Trailers.ts +35 -20
- 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 -2
- 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/index.ts +1 -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/dist/gtd.bundle.mjs
CHANGED
|
@@ -12326,7 +12326,13 @@ The \`kind\` field (\`--json=kind\`) selects what to do:
|
|
|
12326
12326
|
Land it immediately, no display needed.
|
|
12327
12327
|
- \`script\` — run \`--json=content\` with \`sh -c\`, appending its output to
|
|
12328
12328
|
\`--json=log\`, and ignore its exit code — the outcome lives in the tree,
|
|
12329
|
-
not in the exit code.
|
|
12329
|
+
not in the exit code. Two script beats carry a \`pin\` mark: the **pin
|
|
12330
|
+
beat** right after a process starts (\`pin: "add"\`, its script writes
|
|
12331
|
+
\`gtd bundle\`'s output to \`.gtd/process.gtdpin\`) and the **unpin beat**
|
|
12332
|
+
once the flow has ended (\`pin: "remove"\`, its script deletes that file).
|
|
12333
|
+
Both run unattended, like any script beat. A driver may write or delete the
|
|
12334
|
+
file its own way instead of running the script; either way it then lands,
|
|
12335
|
+
and the unpin's landing is the commit that closes the process.
|
|
12330
12336
|
- \`prompt\` — send plain \`gtd next\`'s own output to your agent CLI over
|
|
12331
12337
|
stdin (it's the bare content at this kind, so no separate \`--json=content\`
|
|
12332
12338
|
read is needed), using the accompanying \`session.id\`/\`session.resume\`/
|
|
@@ -12369,7 +12375,8 @@ skills, \`skills\` (an array of the skill names, already trimmed and split —
|
|
|
12369
12375
|
\`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
|
|
12370
12376
|
document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
|
|
12371
12377
|
\`cost\`, \`costByModel\`. Always \`approvals\` (an array of
|
|
12372
|
-
\`{step, name, email, commit}\`, \`[]\` when none).
|
|
12378
|
+
\`{step, name, email, commit}\`, \`[]\` when none). Only at a pin or unpin
|
|
12379
|
+
beat: \`pin\` (\`"add"\` or \`"remove"\`).
|
|
12373
12380
|
|
|
12374
12381
|
\`gtd land\` fields, in fixed order: \`schema\`, \`script\` (the POSIX sh to run), \`settled\`
|
|
12375
12382
|
(true for a no-op at a \`script\` rest — stop immediately, nothing more to
|
|
@@ -28942,6 +28949,8 @@ const renderFormat = (fmt, ...args) => {
|
|
|
28942
28949
|
};
|
|
28943
28950
|
/** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
|
|
28944
28951
|
const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
|
|
28952
|
+
/** Where a process keeps its pin: the engine and workflow it runs on, as `gtd bundle` prints them. */
|
|
28953
|
+
const PIN_PATH = ".gtd/process.gtdpin";
|
|
28945
28954
|
//#endregion
|
|
28946
28955
|
//#region src/wire/Demand.ts
|
|
28947
28956
|
/**
|
|
@@ -28982,14 +28991,20 @@ const demandOf = (input) => {
|
|
|
28982
28991
|
read: null,
|
|
28983
28992
|
write: null
|
|
28984
28993
|
};
|
|
28985
|
-
|
|
28994
|
+
if (kind === "prompt") return {
|
|
28986
28995
|
kind,
|
|
28987
28996
|
content,
|
|
28988
28997
|
session,
|
|
28989
28998
|
validate,
|
|
28990
28999
|
skills,
|
|
28991
29000
|
access
|
|
28992
|
-
}
|
|
29001
|
+
};
|
|
29002
|
+
if (kind === "script" && rendered.pin !== void 0) return {
|
|
29003
|
+
kind,
|
|
29004
|
+
content,
|
|
29005
|
+
pin: rendered.pin
|
|
29006
|
+
};
|
|
29007
|
+
return {
|
|
28993
29008
|
kind,
|
|
28994
29009
|
content
|
|
28995
29010
|
};
|
|
@@ -29042,33 +29057,39 @@ const assertNeverDemand = (demand) => {
|
|
|
29042
29057
|
* falling through to "no session."
|
|
29043
29058
|
*/
|
|
29044
29059
|
const dispatchFieldsOf = (demand) => {
|
|
29060
|
+
const none = {
|
|
29061
|
+
session: void 0,
|
|
29062
|
+
validate: void 0,
|
|
29063
|
+
skills: void 0,
|
|
29064
|
+
access: void 0,
|
|
29065
|
+
pin: void 0
|
|
29066
|
+
};
|
|
29045
29067
|
switch (demand.kind) {
|
|
29046
29068
|
case "prompt": return {
|
|
29069
|
+
...none,
|
|
29047
29070
|
session: demand.session,
|
|
29048
29071
|
validate: demand.validate,
|
|
29049
29072
|
skills: demand.skills,
|
|
29050
29073
|
access: demand.access
|
|
29051
29074
|
};
|
|
29075
|
+
case "script": return {
|
|
29076
|
+
...none,
|
|
29077
|
+
pin: demand.pin
|
|
29078
|
+
};
|
|
29052
29079
|
case "capture":
|
|
29053
29080
|
case "message":
|
|
29054
|
-
case "
|
|
29055
|
-
case "stalled": return {
|
|
29056
|
-
session: void 0,
|
|
29057
|
-
validate: void 0,
|
|
29058
|
-
skills: void 0,
|
|
29059
|
-
access: void 0
|
|
29060
|
-
};
|
|
29081
|
+
case "stalled": return none;
|
|
29061
29082
|
default: return assertNeverDemand(demand);
|
|
29062
29083
|
}
|
|
29063
29084
|
};
|
|
29064
29085
|
/**
|
|
29065
|
-
* Flatten a `Demand` plus its `BeatStatus` into the single
|
|
29086
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 26-key document
|
|
29066
29087
|
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
29067
29088
|
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
29068
29089
|
* (`cost <= 0`).
|
|
29069
29090
|
*/
|
|
29070
29091
|
const beatDocument = (demand, status) => {
|
|
29071
|
-
const { session, validate, skills, access } = dispatchFieldsOf(demand);
|
|
29092
|
+
const { session, validate, skills, access, pin } = dispatchFieldsOf(demand);
|
|
29072
29093
|
const hasCost = status.cost > 0;
|
|
29073
29094
|
return {
|
|
29074
29095
|
schema: 1,
|
|
@@ -29080,6 +29101,7 @@ const beatDocument = (demand, status) => {
|
|
|
29080
29101
|
model: status.model,
|
|
29081
29102
|
system: status.system,
|
|
29082
29103
|
validate,
|
|
29104
|
+
pin,
|
|
29083
29105
|
log: status.log,
|
|
29084
29106
|
state: status.state,
|
|
29085
29107
|
actor: status.actor,
|
|
@@ -29417,11 +29439,20 @@ const diffTrees = (before, after) => {
|
|
|
29417
29439
|
};
|
|
29418
29440
|
};
|
|
29419
29441
|
const isEmptyDiff = (diff) => diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0;
|
|
29442
|
+
const withoutPath = (tree, hidden) => {
|
|
29443
|
+
let paths;
|
|
29444
|
+
return {
|
|
29445
|
+
paths: () => paths ??= tree.paths().filter((path) => path !== hidden),
|
|
29446
|
+
read: (path) => path === hidden ? void 0 : tree.read(path),
|
|
29447
|
+
...tree.id !== void 0 ? { id: (path) => path === hidden ? void 0 : tree.id(path) } : {}
|
|
29448
|
+
};
|
|
29449
|
+
};
|
|
29450
|
+
const OLDEST_READABLE_FORMAT = 1;
|
|
29420
29451
|
const short$1 = (hash) => hash.slice(0, 7);
|
|
29421
29452
|
const formatFault = (commits) => {
|
|
29422
|
-
const foreign = commits.find((c) => c.format
|
|
29453
|
+
const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 2);
|
|
29423
29454
|
if (foreign === void 0) return void 0;
|
|
29424
|
-
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads
|
|
29455
|
+
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`;
|
|
29425
29456
|
};
|
|
29426
29457
|
//#endregion
|
|
29427
29458
|
//#region src/replay/Trailers.ts
|
|
@@ -29458,6 +29489,11 @@ const parseStepId = (raw) => {
|
|
|
29458
29489
|
occurrence: Number(match[2])
|
|
29459
29490
|
};
|
|
29460
29491
|
};
|
|
29492
|
+
/** What a pin and an unpin beat rest at, and their commits name — neither is a step of the workflow. */
|
|
29493
|
+
const PIN_STATES = {
|
|
29494
|
+
add: "pin",
|
|
29495
|
+
remove: "unpin"
|
|
29496
|
+
};
|
|
29461
29497
|
const isVerdict = (value) => {
|
|
29462
29498
|
if (typeof value !== "object" || value === null) return false;
|
|
29463
29499
|
const v = value;
|
|
@@ -29555,6 +29591,9 @@ const TRAILER_READERS = {
|
|
|
29555
29591
|
"Gtd-Approved": (value, into) => {
|
|
29556
29592
|
into.approved ??= decodeApproval(value);
|
|
29557
29593
|
},
|
|
29594
|
+
"Gtd-Pin": (value, into) => {
|
|
29595
|
+
if (value === "add" || value === "remove") into.pin ??= value;
|
|
29596
|
+
},
|
|
29558
29597
|
"Gtd-Format": (value, into) => {
|
|
29559
29598
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
29560
29599
|
},
|
|
@@ -29577,6 +29616,7 @@ const parseCommitMessage = (message) => {
|
|
|
29577
29616
|
version: void 0,
|
|
29578
29617
|
kept: void 0,
|
|
29579
29618
|
approved: void 0,
|
|
29619
|
+
pin: void 0,
|
|
29580
29620
|
judge: [],
|
|
29581
29621
|
marks: [],
|
|
29582
29622
|
vars: {},
|
|
@@ -29594,24 +29634,29 @@ const parseCommitMessage = (message) => {
|
|
|
29594
29634
|
};
|
|
29595
29635
|
const sortedEntries = (record = {}) => Object.entries(record).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
29596
29636
|
/** Trailer order is fixed so the same spec always yields the same bytes. */
|
|
29637
|
+
const trailer = (name, value) => value === void 0 ? [] : [`Gtd-${name}: ${value}`];
|
|
29638
|
+
const costValue = (cost) => cost === void 0 ? void 0 : `${cost.cost}${cost.model !== void 0 ? ` ${cost.model}` : ""}`;
|
|
29597
29639
|
const formatCommitMessage = (spec) => {
|
|
29598
|
-
const lines = [
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
|
|
29608
|
-
|
|
29609
|
-
|
|
29610
|
-
|
|
29640
|
+
const lines = [
|
|
29641
|
+
...trailer("Step", spec.step && formatStepId(spec.step)),
|
|
29642
|
+
...trailer("Workflow", spec.workflow),
|
|
29643
|
+
...trailer("Review-Base", spec.reviewBase),
|
|
29644
|
+
...sortedEntries(spec.vars).map(([name, value]) => `Gtd-Var: ${name}=${value}`),
|
|
29645
|
+
...sortedEntries(spec.config).map(([path, hash]) => `Gtd-Config: ${hash} ${encodePath(path)}`),
|
|
29646
|
+
...trailer("Cost", costValue(spec.cost)),
|
|
29647
|
+
...(spec.judge ?? []).map((verdict) => `Gtd-Judge: ${JSON.stringify(verdict)}`),
|
|
29648
|
+
...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
|
|
29649
|
+
...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
|
|
29650
|
+
...trailer("Pin", spec.pin),
|
|
29651
|
+
...trailer("Branch", spec.branch),
|
|
29652
|
+
...trailer("Version", spec.version),
|
|
29653
|
+
`Gtd-Format: 2`
|
|
29654
|
+
];
|
|
29611
29655
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
29612
29656
|
};
|
|
29613
29657
|
//#endregion
|
|
29614
29658
|
//#region src/replay/Replay.ts
|
|
29659
|
+
const UNPIN_STATE = PIN_STATES.remove;
|
|
29615
29660
|
const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
29616
29661
|
const memoryScopeOf = (name) => {
|
|
29617
29662
|
const dot = name.lastIndexOf(".");
|
|
@@ -29745,7 +29790,33 @@ const modeWithoutFile = (step) => {
|
|
|
29745
29790
|
if (options.mode === void 0 || options.file !== void 0) return void 0;
|
|
29746
29791
|
return `gtd: step "${step.name}": mode requires file in ${step.kind}() options — name the steering file the mode checks`;
|
|
29747
29792
|
};
|
|
29748
|
-
const
|
|
29793
|
+
const withoutPin = (input) => {
|
|
29794
|
+
const hide = (tree) => withoutPath(tree, PIN_PATH);
|
|
29795
|
+
return {
|
|
29796
|
+
...input,
|
|
29797
|
+
episode: {
|
|
29798
|
+
base: {
|
|
29799
|
+
...input.episode.base,
|
|
29800
|
+
tree: hide(input.episode.base.tree)
|
|
29801
|
+
},
|
|
29802
|
+
commits: input.episode.commits.map((c) => ({
|
|
29803
|
+
...c,
|
|
29804
|
+
tree: hide(c.tree),
|
|
29805
|
+
...c.kept !== void 0 ? { kept: {
|
|
29806
|
+
...c.kept,
|
|
29807
|
+
tree: hide(c.kept.tree)
|
|
29808
|
+
} } : {}
|
|
29809
|
+
}))
|
|
29810
|
+
},
|
|
29811
|
+
...input.startTree !== void 0 ? { startTree: hide(input.startTree) } : {},
|
|
29812
|
+
...input.pending !== void 0 ? { pending: {
|
|
29813
|
+
...input.pending,
|
|
29814
|
+
tree: hide(input.pending.tree)
|
|
29815
|
+
} } : {}
|
|
29816
|
+
};
|
|
29817
|
+
};
|
|
29818
|
+
const replay = async (raw) => {
|
|
29819
|
+
const input = withoutPin(raw);
|
|
29749
29820
|
const commits = input.episode.commits.map((c) => ({
|
|
29750
29821
|
...c,
|
|
29751
29822
|
parsed: parseCommitMessage(c.message)
|
|
@@ -29754,6 +29825,9 @@ const replay = async (input) => {
|
|
|
29754
29825
|
let cursor = 0;
|
|
29755
29826
|
let pendingUsed = false;
|
|
29756
29827
|
let position = input.episode.base;
|
|
29828
|
+
const skipPins = () => {
|
|
29829
|
+
while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) cursor++;
|
|
29830
|
+
};
|
|
29757
29831
|
let previousPosition = input.episode.base;
|
|
29758
29832
|
let expectedNext;
|
|
29759
29833
|
const occurrences = /* @__PURE__ */ new Map();
|
|
@@ -29935,19 +30009,24 @@ const replay = async (input) => {
|
|
|
29935
30009
|
if (unapproved !== void 0) return finish(unapproved);
|
|
29936
30010
|
cursor++;
|
|
29937
30011
|
advance(commit);
|
|
29938
|
-
|
|
29939
|
-
|
|
29940
|
-
hash: commit.kept.hash,
|
|
29941
|
-
before: commit.tree,
|
|
29942
|
-
after: commit.kept.tree
|
|
29943
|
-
});
|
|
30012
|
+
skipPins();
|
|
30013
|
+
noteKept(step, commit);
|
|
29944
30014
|
const to = commit.parsed.parsed?.to;
|
|
29945
|
-
if (to !== void 0) expectedNext = {
|
|
30015
|
+
if (to !== void 0 && to !== UNPIN_STATE) expectedNext = {
|
|
29946
30016
|
name: to,
|
|
29947
30017
|
hash: commit.hash
|
|
29948
30018
|
};
|
|
29949
30019
|
return replyFor(step.request, commit.parsed.judge);
|
|
29950
30020
|
};
|
|
30021
|
+
const noteKept = (step, commit) => {
|
|
30022
|
+
if (commit.parsed.kept === void 0) return;
|
|
30023
|
+
if (commit.kept === void 0) keptTips.delete(step.name);
|
|
30024
|
+
else keptTips.set(step.name, {
|
|
30025
|
+
hash: commit.kept.hash,
|
|
30026
|
+
before: commit.tree,
|
|
30027
|
+
after: commit.kept.tree
|
|
30028
|
+
});
|
|
30029
|
+
};
|
|
29951
30030
|
const landPending = (step, pending) => {
|
|
29952
30031
|
pendingUsed = true;
|
|
29953
30032
|
landed = step;
|
|
@@ -29964,7 +30043,10 @@ const replay = async (input) => {
|
|
|
29964
30043
|
return replyFor(step.request, pending.verdicts ?? []);
|
|
29965
30044
|
};
|
|
29966
30045
|
const answer = (step) => {
|
|
29967
|
-
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name))
|
|
30046
|
+
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) {
|
|
30047
|
+
cursor++;
|
|
30048
|
+
skipPins();
|
|
30049
|
+
}
|
|
29968
30050
|
const commit = commits[cursor];
|
|
29969
30051
|
if (commit !== void 0) return consumeCommit(step, commit);
|
|
29970
30052
|
if (input.pending === void 0 || pendingUsed) return finish({
|
|
@@ -29977,6 +30059,7 @@ const replay = async (input) => {
|
|
|
29977
30059
|
return landPending(step, input.pending);
|
|
29978
30060
|
};
|
|
29979
30061
|
const requestFault = (request) => {
|
|
30062
|
+
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`;
|
|
29980
30063
|
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)`;
|
|
29981
30064
|
if (request.kind !== "agent") return void 0;
|
|
29982
30065
|
const name = scoped(request.name);
|
|
@@ -30003,15 +30086,18 @@ const replay = async (input) => {
|
|
|
30003
30086
|
const blocker = blockerAt(step);
|
|
30004
30087
|
return blocker === void 0 ? answer(step) : finish(blocker);
|
|
30005
30088
|
};
|
|
30006
|
-
const endOfFlow = (via) =>
|
|
30007
|
-
|
|
30008
|
-
|
|
30009
|
-
|
|
30010
|
-
|
|
30011
|
-
|
|
30012
|
-
|
|
30013
|
-
|
|
30014
|
-
|
|
30089
|
+
const endOfFlow = (via) => {
|
|
30090
|
+
skipPins();
|
|
30091
|
+
return cursor < commits.length ? {
|
|
30092
|
+
kind: "divergence",
|
|
30093
|
+
message: `gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode — ${DIVERGED}`
|
|
30094
|
+
} : {
|
|
30095
|
+
kind: "ended",
|
|
30096
|
+
via,
|
|
30097
|
+
trace,
|
|
30098
|
+
landed,
|
|
30099
|
+
marks
|
|
30100
|
+
};
|
|
30015
30101
|
};
|
|
30016
30102
|
const changesSince = (hash) => {
|
|
30017
30103
|
const tree = treeAt(hash);
|
|
@@ -30074,10 +30160,11 @@ const replay = async (input) => {
|
|
|
30074
30160
|
skillsFor: (localName) => resolveSkills(memoryScopeOf(scoped(localName))) ?? [],
|
|
30075
30161
|
accessFor: (localName, file) => foldedAccessFor(resolveAccess(memoryScopeOf(scoped(localName))), file),
|
|
30076
30162
|
head: () => {
|
|
30077
|
-
const rest = commits.slice(cursor);
|
|
30163
|
+
const rest = commits.slice(cursor).filter((c) => c.parsed.pin === void 0);
|
|
30078
30164
|
return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
|
|
30079
30165
|
}
|
|
30080
30166
|
};
|
|
30167
|
+
skipPins();
|
|
30081
30168
|
installContext(context);
|
|
30082
30169
|
try {
|
|
30083
30170
|
const flowDone = input.flow({}).then(() => {
|
|
@@ -31161,1539 +31248,1665 @@ const pin = (module, root, version) => gen(function* () {
|
|
|
31161
31248
|
return Buffer.concat([Buffer.from(`gtdpin ${PIN_FORMAT} gtd ${version}\n`), gzipSync(source, { level: 9 })]);
|
|
31162
31249
|
});
|
|
31163
31250
|
//#endregion
|
|
31164
|
-
//#region src/
|
|
31251
|
+
//#region src/emit/GitScript.ts
|
|
31252
|
+
const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
31165
31253
|
/**
|
|
31166
|
-
*
|
|
31167
|
-
*
|
|
31168
|
-
*
|
|
31254
|
+
* `--allow-empty` because gtd's workflow commits may be empty on purpose.
|
|
31255
|
+
* Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
|
|
31256
|
+
* "empty git commit" rejection message, discriminated from the captured
|
|
31257
|
+
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
31258
|
+
* error, a commit-msg hook) still fails.
|
|
31169
31259
|
*/
|
|
31170
|
-
const
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31178
|
-
|
|
31179
|
-
|
|
31180
|
-
|
|
31181
|
-
kind: "noop",
|
|
31182
|
-
state: snapshot.state,
|
|
31183
|
-
settled: landing.settled
|
|
31184
|
-
};
|
|
31185
|
-
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
31186
|
-
cost: opts.cost,
|
|
31187
|
-
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
31188
|
-
} };
|
|
31189
|
-
if (landing.kind === "attempt") {
|
|
31190
|
-
const message = formatCommitMessage({
|
|
31191
|
-
actor: snapshot.actor,
|
|
31192
|
-
to: snapshot.state,
|
|
31193
|
-
...cost
|
|
31194
|
-
});
|
|
31195
|
-
return {
|
|
31196
|
-
kind: "commit",
|
|
31197
|
-
state: snapshot.state,
|
|
31198
|
-
to: snapshot.state,
|
|
31199
|
-
subject: landing.subject,
|
|
31200
|
-
steps: [{
|
|
31201
|
-
kind: "gitWrite",
|
|
31202
|
-
write: {
|
|
31203
|
-
kind: "commitAll",
|
|
31204
|
-
message
|
|
31205
|
-
}
|
|
31206
|
-
}, {
|
|
31207
|
-
kind: "outcome",
|
|
31208
|
-
outcome: {
|
|
31209
|
-
kind: "commit",
|
|
31210
|
-
subject: landing.subject
|
|
31211
|
-
}
|
|
31212
|
-
}]
|
|
31213
|
-
};
|
|
31214
|
-
}
|
|
31215
|
-
const message = formatCommitMessage({
|
|
31216
|
-
...landing.spec,
|
|
31217
|
-
...cost,
|
|
31218
|
-
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
31219
|
-
});
|
|
31220
|
-
const subject = message.split("\n")[0];
|
|
31221
|
-
const outcome = landing.to === snapshot.state ? {
|
|
31222
|
-
kind: "outcome",
|
|
31223
|
-
outcome: {
|
|
31224
|
-
kind: "commit",
|
|
31225
|
-
subject
|
|
31226
|
-
}
|
|
31227
|
-
} : {
|
|
31228
|
-
kind: "outcome",
|
|
31229
|
-
outcome: {
|
|
31230
|
-
kind: "transition",
|
|
31231
|
-
from: snapshot.state,
|
|
31232
|
-
to: landing.to
|
|
31233
|
-
}
|
|
31234
|
-
};
|
|
31235
|
-
return {
|
|
31236
|
-
kind: "commit",
|
|
31237
|
-
state: snapshot.state,
|
|
31238
|
-
to: landing.to,
|
|
31239
|
-
subject,
|
|
31240
|
-
steps: [
|
|
31241
|
-
...uncheckStep(snapshot),
|
|
31242
|
-
{
|
|
31243
|
-
kind: "gitWrite",
|
|
31244
|
-
write: landing.keepTurn !== void 0 ? {
|
|
31245
|
-
kind: "keepTurn",
|
|
31246
|
-
ref: landing.keepTurn,
|
|
31247
|
-
message
|
|
31248
|
-
} : landing.spec.approved !== void 0 ? {
|
|
31249
|
-
kind: "commitClean",
|
|
31250
|
-
message
|
|
31251
|
-
} : {
|
|
31252
|
-
kind: "commitAll",
|
|
31253
|
-
message
|
|
31254
|
-
}
|
|
31255
|
-
},
|
|
31256
|
-
...landing.keepHistory !== void 0 ? [{
|
|
31257
|
-
kind: "gitWrite",
|
|
31258
|
-
write: {
|
|
31259
|
-
kind: "updateRef",
|
|
31260
|
-
ref: landing.keepHistory,
|
|
31261
|
-
target: "HEAD"
|
|
31262
|
-
}
|
|
31263
|
-
}] : [],
|
|
31264
|
-
outcome
|
|
31265
|
-
]
|
|
31266
|
-
};
|
|
31260
|
+
const commitAllowEmpty = (message, only) => {
|
|
31261
|
+
const m = shellQuote$1(message);
|
|
31262
|
+
const paths = only === void 0 ? "" : ` -- ${shellQuote$1(only)}`;
|
|
31263
|
+
return [
|
|
31264
|
+
`if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
|
|
31265
|
+
` case "$out" in`,
|
|
31266
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m}${paths} || exit 1 ;;`,
|
|
31267
|
+
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
31268
|
+
` esac`,
|
|
31269
|
+
`fi`
|
|
31270
|
+
].join("\n");
|
|
31267
31271
|
};
|
|
31268
|
-
|
|
31269
|
-
|
|
31272
|
+
const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
|
|
31273
|
+
/** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
|
|
31274
|
+
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
31275
|
+
/** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
|
|
31276
|
+
const commitPath = (path, message) => `git add -A -- ${shellQuote$1(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
|
|
31277
|
+
/** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
|
|
31278
|
+
const pinScript = (path) => {
|
|
31279
|
+
const p = shellQuote$1(path);
|
|
31280
|
+
const slash = path.lastIndexOf("/");
|
|
31281
|
+
return `${slash === -1 ? "" : `mkdir -p ${shellQuote$1(path.slice(0, slash))}\n`}gtd bundle >${p} || { rm -f -- ${p}; exit 1; }`;
|
|
31282
|
+
};
|
|
31283
|
+
/** An unpin beat's script, and `gtd abandon`'s removal of the pin. */
|
|
31284
|
+
const unpinScript = (path) => `rm -f -- ${shellQuote$1(path)}`;
|
|
31270
31285
|
/**
|
|
31271
|
-
*
|
|
31272
|
-
*
|
|
31273
|
-
* runs
|
|
31286
|
+
* An approval records exactly the judged tree: HEAD is pinned before the
|
|
31287
|
+
* clean check and the commit is built over its tree with `commit-tree`, so no
|
|
31288
|
+
* hook runs and none can add to it. A HEAD that moved fails the guarded
|
|
31289
|
+
* `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
|
|
31274
31290
|
*/
|
|
31275
|
-
const
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
31291
|
+
const commitClean = (message) => [
|
|
31292
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
31293
|
+
`if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
|
|
31294
|
+
` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
|
|
31295
|
+
` exit 1`,
|
|
31296
|
+
`fi`,
|
|
31297
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
31298
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
31299
|
+
`gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
31300
|
+
`git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
|
|
31301
|
+
].join("\n");
|
|
31302
|
+
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
|
|
31303
|
+
const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
|
|
31304
|
+
/**
|
|
31305
|
+
* POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
|
|
31306
|
+
* tree, so no hook runs on it and its tree is the pre-turn tree by
|
|
31307
|
+
* construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
|
|
31308
|
+
* `-S`, read (and failing closed) before any object or ref is written; the
|
|
31309
|
+
* index is already staged by then, so an early exit leaves the turn staged.
|
|
31310
|
+
* The kept ref is deleted only after `reset -q` succeeds: it is then the
|
|
31311
|
+
* turn's only handle.
|
|
31312
|
+
* The branch moves before the final hard reset, so if that reset fails the
|
|
31313
|
+
* marker has landed and the turn is still in the working tree (kept ref
|
|
31314
|
+
* present); every other failure leaves the branch untouched.
|
|
31315
|
+
*/
|
|
31316
|
+
const keepTurn = (refPrefix, message) => {
|
|
31317
|
+
const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
|
|
31279
31318
|
return [
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
`
|
|
31319
|
+
`git add -A || exit 1`,
|
|
31320
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
31321
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
31322
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
31323
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
31324
|
+
`gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
31325
|
+
`git update-ref ${keptRef} "$gtd_kept" || exit 1`,
|
|
31326
|
+
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
|
|
31327
|
+
`if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
|
|
31328
|
+
` git reset -q && git update-ref -d ${keptRef}`,
|
|
31329
|
+
` exit 1`,
|
|
31330
|
+
`fi`,
|
|
31331
|
+
`git reset --hard HEAD`
|
|
31283
31332
|
].join("\n");
|
|
31284
31333
|
};
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
const refusal = (message) => ({
|
|
31289
|
-
kind: "refusal",
|
|
31290
|
-
message
|
|
31291
|
-
});
|
|
31292
|
-
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
31293
|
-
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
31294
|
-
if (environmentOnly.length > 0) return yield* fail$8(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`));
|
|
31295
|
-
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
31296
|
-
if (undeclared.length > 0) return yield* fail$8(new GtdUsageError(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`));
|
|
31297
|
-
});
|
|
31334
|
+
const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
|
|
31335
|
+
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
31336
|
+
const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
|
|
31298
31337
|
/**
|
|
31299
|
-
*
|
|
31300
|
-
*
|
|
31301
|
-
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
31302
|
-
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
|
|
31303
|
-
* and a `Gtd-Config:` trailer per config file.
|
|
31304
|
-
* All validation is a REFUSAL, not an Effect failure.
|
|
31338
|
+
* No `+` on the refspec: a local ref the remote's tip does not descend from
|
|
31339
|
+
* fails the fetch instead of being overwritten (a fast-forward still applies).
|
|
31305
31340
|
*/
|
|
31306
|
-
const
|
|
31307
|
-
|
|
31308
|
-
|
|
31309
|
-
|
|
31310
|
-
|
|
31311
|
-
|
|
31312
|
-
|
|
31313
|
-
|
|
31314
|
-
|
|
31315
|
-
|
|
31316
|
-
|
|
31317
|
-
|
|
31318
|
-
|
|
31319
|
-
|
|
31320
|
-
|
|
31321
|
-
|
|
31322
|
-
|
|
31323
|
-
|
|
31324
|
-
|
|
31325
|
-
|
|
31326
|
-
|
|
31327
|
-
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
|
|
31334
|
-
|
|
31335
|
-
|
|
31336
|
-
message: formatCommitMessage({
|
|
31337
|
-
actor,
|
|
31338
|
-
to: firstStep,
|
|
31339
|
-
workflow: name,
|
|
31340
|
-
...base !== void 0 ? { reviewBase: base } : {},
|
|
31341
|
-
vars,
|
|
31342
|
-
config: start.config
|
|
31343
|
-
})
|
|
31344
|
-
}
|
|
31345
|
-
}, {
|
|
31346
|
-
kind: "outcome",
|
|
31347
|
-
outcome: {
|
|
31348
|
-
kind: "commit",
|
|
31349
|
-
subject
|
|
31350
|
-
}
|
|
31351
|
-
}]
|
|
31352
|
-
};
|
|
31353
|
-
});
|
|
31341
|
+
const fetchHistoryRefs = (remote) => [
|
|
31342
|
+
`set -eu`,
|
|
31343
|
+
`before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
31344
|
+
`git fetch ${shellQuote$1(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
|
|
31345
|
+
`after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
31346
|
+
`printf '%s history refs arrived\\n' "$((after - before))"`
|
|
31347
|
+
].join("\n");
|
|
31348
|
+
/** 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. */
|
|
31349
|
+
const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
|
|
31350
|
+
const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
31351
|
+
const renderOutcome = (outcome) => {
|
|
31352
|
+
switch (outcome.kind) {
|
|
31353
|
+
case "transition": return printfLine$1("%s %s → %s\n", [
|
|
31354
|
+
"'->'",
|
|
31355
|
+
shellQuote$1(outcome.from),
|
|
31356
|
+
shellQuote$1(outcome.to)
|
|
31357
|
+
]);
|
|
31358
|
+
case "commit": return printfLine$1("%s %s\n", ["'[commit]'", shellQuote$1(outcome.subject)]);
|
|
31359
|
+
case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
|
|
31360
|
+
}
|
|
31361
|
+
};
|
|
31362
|
+
const renderGitWrite = (write) => {
|
|
31363
|
+
switch (write.kind) {
|
|
31364
|
+
case "commitAll": return commitAll(write.message);
|
|
31365
|
+
case "commitPath": return commitPath(write.path, write.message);
|
|
31366
|
+
case "commitClean": return commitClean(write.message);
|
|
31367
|
+
case "updateRef": return updateRef(write.ref, write.target);
|
|
31368
|
+
case "keepTurn": return keepTurn(write.ref, write.message);
|
|
31369
|
+
}
|
|
31370
|
+
};
|
|
31354
31371
|
/**
|
|
31355
|
-
*
|
|
31356
|
-
*
|
|
31357
|
-
*
|
|
31372
|
+
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
31373
|
+
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
31374
|
+
* `failurePromptWrapper` (same reverse-import concern as `renderOutcome`
|
|
31375
|
+
* above: `Emit.ts` imports `shellQuote` from here already).
|
|
31358
31376
|
*/
|
|
31359
|
-
const
|
|
31360
|
-
|
|
31361
|
-
|
|
31362
|
-
|
|
31363
|
-
|
|
31364
|
-
|
|
31365
|
-
|
|
31366
|
-
|
|
31367
|
-
|
|
31368
|
-
|
|
31369
|
-
|
|
31370
|
-
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
31377
|
+
const failurePromptWrapper$1 = (command, prompt) => {
|
|
31378
|
+
return [
|
|
31379
|
+
`gtd_validate_status=0`,
|
|
31380
|
+
`gtd_validate_out="$( {`,
|
|
31381
|
+
command,
|
|
31382
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
31383
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
31384
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
|
|
31385
|
+
` exit "$gtd_validate_status"`,
|
|
31386
|
+
`fi`
|
|
31387
|
+
].join("\n");
|
|
31388
|
+
};
|
|
31389
|
+
const renderLandStep = (step) => {
|
|
31390
|
+
if (step.kind === "gitWrite") return renderGitWrite(step.write);
|
|
31391
|
+
if (step.kind === "outcome") return renderOutcome(step.outcome);
|
|
31392
|
+
if (step.kind === "uncheck") return `gtd uncheck ${shellQuote$1(step.file)}`;
|
|
31393
|
+
return step.onFailure !== void 0 ? failurePromptWrapper$1(step.command, step.onFailure) : step.command;
|
|
31394
|
+
};
|
|
31395
|
+
const ScriptSurface = { render: (steps) => {
|
|
31396
|
+
const rendered = steps.map(renderLandStep);
|
|
31397
|
+
return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
|
|
31398
|
+
} };
|
|
31374
31399
|
//#endregion
|
|
31375
|
-
//#region src/
|
|
31376
|
-
|
|
31377
|
-
|
|
31378
|
-
|
|
31379
|
-
const
|
|
31380
|
-
|
|
31381
|
-
|
|
31382
|
-
|
|
31383
|
-
|
|
31384
|
-
|
|
31400
|
+
//#region src/emit/Emit.ts
|
|
31401
|
+
/** 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`. */
|
|
31402
|
+
const fileExistsGuard = (file) => `[ -f ${shellQuote$1(file)} ] || exit 0`;
|
|
31403
|
+
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
31404
|
+
const withFileVar$1 = (command, file) => `export GTD_FILE=${shellQuote$1(file)}\n${command}`;
|
|
31405
|
+
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
|
|
31406
|
+
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
|
|
31407
|
+
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
|
|
31408
|
+
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
31409
|
+
const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote$1(file));
|
|
31385
31410
|
/**
|
|
31386
|
-
*
|
|
31387
|
-
*
|
|
31388
|
-
*
|
|
31389
|
-
*
|
|
31390
|
-
* a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
|
|
31391
|
-
* entering the flow's first step (a finished episode), bounds the episode
|
|
31392
|
-
* from below.
|
|
31411
|
+
* The leading word of a rendered `format:`/`validate:` command, when the
|
|
31412
|
+
* command has exactly one unambiguous binary to probe — `undefined` for
|
|
31413
|
+
* anything a shell metacharacter could hide a second command behind. A missing
|
|
31414
|
+
* guard degrades to a raw exit; a wrong guard would refuse a working command.
|
|
31393
31415
|
*/
|
|
31394
|
-
const
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
|
|
31399
|
-
workflow: message.workflow,
|
|
31400
|
-
legacyOpening: message.workflow === void 0,
|
|
31401
|
-
baseIndex: i,
|
|
31402
|
-
processStart: i
|
|
31403
|
-
};
|
|
31404
|
-
if (subject === void 0 || !ACTORS.has(subject.actor) || subject.to === def.initial) return {
|
|
31405
|
-
workflow: void 0,
|
|
31406
|
-
legacyOpening: false,
|
|
31407
|
-
baseIndex: i,
|
|
31408
|
-
processStart: i + 1
|
|
31409
|
-
};
|
|
31410
|
-
}
|
|
31411
|
-
return {
|
|
31412
|
-
workflow: void 0,
|
|
31413
|
-
legacyOpening: false,
|
|
31414
|
-
baseIndex: -1,
|
|
31415
|
-
processStart: 0
|
|
31416
|
-
};
|
|
31417
|
-
};
|
|
31418
|
-
const headTurnOf = (head) => {
|
|
31419
|
-
if (head === void 0) return void 0;
|
|
31420
|
-
const message = parseCommitMessage(head.message);
|
|
31421
|
-
const subject = message.parsed;
|
|
31422
|
-
if (subject === void 0 || !ACTORS.has(subject.actor)) return void 0;
|
|
31423
|
-
return {
|
|
31424
|
-
state: subject.to,
|
|
31425
|
-
actor: subject.actor,
|
|
31426
|
-
empty: head.touched.length === 0,
|
|
31427
|
-
step: message.step !== void 0
|
|
31428
|
-
};
|
|
31429
|
-
};
|
|
31430
|
-
const traceEntryOf = (hash, message) => ({
|
|
31431
|
-
state: message.parsed?.to ?? "",
|
|
31432
|
-
hash,
|
|
31433
|
-
actor: message.parsed?.actor ?? ""
|
|
31434
|
-
});
|
|
31435
|
-
const costEntriesOf = (hash, message) => message.cost.map((c) => ({
|
|
31436
|
-
cost: c.cost,
|
|
31437
|
-
model: c.model ?? "unspecified",
|
|
31438
|
-
hash
|
|
31439
|
-
}));
|
|
31440
|
-
const hashAt = (history, index) => history[index]?.hash;
|
|
31441
|
-
const historyFormatFault = (history, location) => formatFault(history.slice(location.baseIndex + 1).concat(history[location.baseIndex]?.message.startsWith("gtd") ? [history[location.baseIndex]] : []).map((c) => ({
|
|
31442
|
-
hash: c.hash,
|
|
31443
|
-
format: parseCommitMessage(c.message).format
|
|
31444
|
-
})));
|
|
31445
|
-
/** Fails on a history in a format this gtd does not read, before anything acts on it. */
|
|
31446
|
-
const runOf = (history, location, closingHash = void 0) => {
|
|
31447
|
-
const fault = historyFormatFault(history, location);
|
|
31448
|
-
return fault === void 0 ? succeed$1(buildRun(history, location, closingHash)) : fail$8(new Error(fault));
|
|
31449
|
-
};
|
|
31450
|
-
const pinnedConfigOf = (history) => {
|
|
31451
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
31452
|
-
const files = parseCommitMessage(history[i].message).config;
|
|
31453
|
-
if (Object.keys(files).length > 0) return {
|
|
31454
|
-
commit: history[i].hash,
|
|
31455
|
-
files
|
|
31456
|
-
};
|
|
31457
|
-
}
|
|
31416
|
+
const extractLeadingBinary = (command) => {
|
|
31417
|
+
if (command.includes("\n")) return void 0;
|
|
31418
|
+
if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return void 0;
|
|
31419
|
+
const match = SIMPLE_LEADING_WORD_RE.exec(command);
|
|
31420
|
+
return match === null ? void 0 : match[0];
|
|
31458
31421
|
};
|
|
31459
|
-
|
|
31460
|
-
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
31464
|
-
|
|
31465
|
-
const
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
workflow: location.workflow,
|
|
31473
|
-
legacyOpening: location.legacyOpening,
|
|
31474
|
-
startHash: hashAt(history, location.processStart) ?? head?.hash ?? EMPTY_TREE$1,
|
|
31475
|
-
startParentHash,
|
|
31476
|
-
diffBase: first?.reviewBase ?? startParentHash,
|
|
31477
|
-
trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
|
|
31478
|
-
costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
|
|
31479
|
-
judgeVerdicts: parsed.flatMap((m) => m.judge),
|
|
31480
|
-
marks: parsed.flatMap((m) => m.marks),
|
|
31481
|
-
approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
|
|
31482
|
-
pinnedVars: { ...first?.vars },
|
|
31483
|
-
pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
|
|
31484
|
-
headTurn: headTurnOf(head),
|
|
31485
|
-
closingHash,
|
|
31486
|
-
episode: {
|
|
31487
|
-
base: hashAt(history, location.baseIndex),
|
|
31488
|
-
commits: history.slice(location.baseIndex + 1)
|
|
31489
|
-
}
|
|
31490
|
-
};
|
|
31422
|
+
/**
|
|
31423
|
+
* Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
|
|
31424
|
+
* `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
|
|
31425
|
+
* resolves it in-process. Exits 127, matching bash's "command not found".
|
|
31426
|
+
*/
|
|
31427
|
+
const binaryGuard = (binary, mode, key) => {
|
|
31428
|
+
const messageQ = shellQuote$1(`gtd: mode "${mode}": "${key}" command not found: ${binary}`);
|
|
31429
|
+
return [
|
|
31430
|
+
`command -v ${shellQuote$1(binary)} >/dev/null 2>&1 || {`,
|
|
31431
|
+
` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
|
|
31432
|
+
` exit 127`,
|
|
31433
|
+
`}`
|
|
31434
|
+
].join("\n");
|
|
31491
31435
|
};
|
|
31492
|
-
const PAGE_TIERS = [
|
|
31493
|
-
32,
|
|
31494
|
-
128,
|
|
31495
|
-
512,
|
|
31496
|
-
Infinity
|
|
31497
|
-
];
|
|
31498
31436
|
/**
|
|
31499
|
-
* `
|
|
31500
|
-
*
|
|
31501
|
-
*
|
|
31502
|
-
* which is NEVER a boundary here, whatever `<x>` is. That bare shape is
|
|
31503
|
-
* `locateEpisode`'s own `opening` branch (a started episode's first commit),
|
|
31504
|
-
* which takes priority there over the initial-state check no matter what the
|
|
31505
|
-
* workflow's first step is named — including one named after `def.initial`
|
|
31506
|
-
* itself, where the two branches would otherwise disagree. A bare human commit can
|
|
31507
|
-
* also be a self-loop step landing rather than a real opening (this
|
|
31508
|
-
* subject-only pass can't read the `Gtd-Step` trailer that would tell them
|
|
31509
|
-
* apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
|
|
31510
|
-
* commits further back than strictly needed — never stop short of the
|
|
31511
|
-
* boundary `locateEpisode` would find once it has full commit bodies in
|
|
31512
|
-
* hand. That asymmetry is the only direction this approximation may err in;
|
|
31513
|
-
* it must never call a real boundary "not yet found".
|
|
31437
|
+
* Prints `prompt` plus the captured output on a non-zero exit, then propagates
|
|
31438
|
+
* it. The `{ … }` group inlines a multi-line `command` verbatim; the
|
|
31439
|
+
* assignment sits left of `||` so `set -e` never trips on the failure itself.
|
|
31514
31440
|
*/
|
|
31515
|
-
const
|
|
31516
|
-
|
|
31517
|
-
|
|
31518
|
-
|
|
31519
|
-
|
|
31441
|
+
const failurePromptWrapper = (command, prompt) => {
|
|
31442
|
+
return [
|
|
31443
|
+
`gtd_validate_status=0`,
|
|
31444
|
+
`gtd_validate_out="$( {`,
|
|
31445
|
+
command,
|
|
31446
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
31447
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
31448
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
|
|
31449
|
+
` exit "$gtd_validate_status"`,
|
|
31450
|
+
`fi`
|
|
31451
|
+
].join("\n");
|
|
31520
31452
|
};
|
|
31521
|
-
|
|
31522
|
-
|
|
31523
|
-
|
|
31453
|
+
const renderStep = (step) => {
|
|
31454
|
+
if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
|
|
31455
|
+
return step.command;
|
|
31524
31456
|
};
|
|
31457
|
+
const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
|
|
31458
|
+
/** 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. */
|
|
31459
|
+
const emitScripts = (required = [], optional = []) => ({
|
|
31460
|
+
required: assembleScript(required),
|
|
31461
|
+
optional: assembleScript(optional)
|
|
31462
|
+
});
|
|
31463
|
+
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
31464
|
+
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
|
|
31465
|
+
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
31466
|
+
const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
|
|
31525
31467
|
/**
|
|
31526
|
-
*
|
|
31527
|
-
*
|
|
31528
|
-
*
|
|
31529
|
-
*
|
|
31530
|
-
* the boundary commit itself: `commitHistory`'s range excludes `base`, and
|
|
31531
|
-
* `runOf` needs the boundary commit present in the read history
|
|
31532
|
-
* (`episode.base`, `startParentHash`). `base: undefined` means "read from the
|
|
31533
|
-
* repository root" — an empty repository, an unresolvable `head`, or an
|
|
31534
|
-
* episode that reaches the root commit, none of which cost anything extra
|
|
31535
|
-
* since that IS the whole history in those cases.
|
|
31536
|
-
*
|
|
31537
|
-
* `head` is the CONCRETE hash the literal `head` argument resolved to on this
|
|
31538
|
-
* call's first read — `undefined` only when it never resolved (an empty
|
|
31539
|
-
* repository, or an unresolvable ref). The caller must hand this hash, not
|
|
31540
|
-
* the original `head` argument, to its own `commitHistory` call: resolving a
|
|
31541
|
-
* symbolic ref (literal `HEAD`, most callers' default) a second time risks
|
|
31542
|
-
* reading a DIFFERENT commit than this function paged through, if something
|
|
31543
|
-
* moves HEAD in between — pinning both reads to one resolved hash is what
|
|
31544
|
-
* keeps the two-read split from disagreeing with itself.
|
|
31468
|
+
* A write command's single pasteable script (`gtd abandon | sh`,
|
|
31469
|
+
* `gtd land --json=script`'s own field). `optional`, when non-empty, is
|
|
31470
|
+
* wrapped in a subshell whose failure is swallowed — presentation-only, so it
|
|
31471
|
+
* must never turn a landed turn into a non-zero exit.
|
|
31545
31472
|
*/
|
|
31546
|
-
const
|
|
31547
|
-
|
|
31548
|
-
|
|
31549
|
-
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
31553
|
-
|
|
31554
|
-
|
|
31555
|
-
|
|
31556
|
-
|
|
31557
|
-
};
|
|
31558
|
-
continue;
|
|
31559
|
-
}
|
|
31560
|
-
return {
|
|
31561
|
-
base: page[boundaryIdx - 1].hash,
|
|
31562
|
-
head: pinnedHead
|
|
31563
|
-
};
|
|
31564
|
-
}
|
|
31565
|
-
return {
|
|
31566
|
-
base: void 0,
|
|
31567
|
-
head: pinnedHead
|
|
31568
|
-
};
|
|
31569
|
-
});
|
|
31570
|
-
const historyUpTo = (git, def, head) => flatMap(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
31571
|
-
const computeProcessRun = (git, def, head) => flatMap(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
31572
|
-
const changedSinceStart = (pinned, current, workspace) => {
|
|
31573
|
-
if (pinned === void 0) return [];
|
|
31574
|
-
return changedConfigFiles(pinned.files, current).filter((path) => {
|
|
31575
|
-
const committed = workspace.readCommittedSync(path, pinned.commit);
|
|
31576
|
-
return current[path] === void 0 || committed === void 0 || contentHash(committed) !== current[path];
|
|
31577
|
-
});
|
|
31578
|
-
};
|
|
31579
|
-
const warnConfigChanges = (run, current, workspace) => gen(function* () {
|
|
31580
|
-
const changed = changedSinceStart(run.pinnedConfig, current, workspace);
|
|
31581
|
-
if (changed.length === 0) return;
|
|
31582
|
-
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`);
|
|
31583
|
-
});
|
|
31584
|
-
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
|
|
31585
|
-
const currentRun = gen(function* () {
|
|
31586
|
-
const git = yield* GitService;
|
|
31587
|
-
const config = yield* (yield* ConfigService).load;
|
|
31588
|
-
return yield* computeProcessRun(git, config.workflow);
|
|
31589
|
-
});
|
|
31590
|
-
const runAtTip = (git, def, tip) => gen(function* () {
|
|
31591
|
-
if (tip === void 0) {
|
|
31592
|
-
const history = yield* historyUpTo(git, def, void 0);
|
|
31593
|
-
return yield* runOf(history, locateEpisode(def, history));
|
|
31594
|
-
}
|
|
31595
|
-
if (!(parseCommitMessage(tip.subject).parsed?.to === def.initial)) {
|
|
31596
|
-
const history = yield* historyUpTo(git, def, tip.hash);
|
|
31597
|
-
return yield* runOf(history, locateEpisode(def, history));
|
|
31598
|
-
}
|
|
31599
|
-
const parentRef = `${tip.hash}~1`;
|
|
31600
|
-
const before = yield* historyUpTo(git, def, parentRef);
|
|
31601
|
-
const location = locateEpisode(def, before);
|
|
31602
|
-
let closing = yield* git.commitHistory(parentRef, tip.hash);
|
|
31603
|
-
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
31604
|
-
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
31605
|
-
});
|
|
31606
|
-
const runAtHead = (git, def) => gen(function* () {
|
|
31607
|
-
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
|
|
31608
|
-
});
|
|
31609
|
-
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
31610
|
-
const summaryRun = gen(function* () {
|
|
31611
|
-
const git = yield* GitService;
|
|
31612
|
-
const def = (yield* (yield* ConfigService).load).workflow;
|
|
31613
|
-
return yield* runAtHead(git, def);
|
|
31614
|
-
});
|
|
31615
|
-
const commitTree = (workspace, hash) => {
|
|
31616
|
-
let entries;
|
|
31617
|
-
const list = () => entries ??= workspace.treeSync(hash);
|
|
31618
|
-
let sortedPaths;
|
|
31619
|
-
const contents = /* @__PURE__ */ new Map();
|
|
31620
|
-
return {
|
|
31621
|
-
paths: () => sortedPaths ??= [...list().keys()].sort(),
|
|
31622
|
-
read: (path) => {
|
|
31623
|
-
if (!list().has(path)) return void 0;
|
|
31624
|
-
if (!contents.has(path)) contents.set(path, workspace.readCommittedSync(path, hash));
|
|
31625
|
-
return contents.get(path);
|
|
31626
|
-
},
|
|
31627
|
-
id: (path) => list().get(path)
|
|
31628
|
-
};
|
|
31473
|
+
const combinedScript = (required, optional) => {
|
|
31474
|
+
if (required.length === 0) return "";
|
|
31475
|
+
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
31476
|
+
return [
|
|
31477
|
+
DID_NOT_RUN_COMMENT,
|
|
31478
|
+
"",
|
|
31479
|
+
required,
|
|
31480
|
+
"",
|
|
31481
|
+
PRESENTATION_ONLY_COMMENT,
|
|
31482
|
+
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
31483
|
+
].join("\n");
|
|
31629
31484
|
};
|
|
31485
|
+
//#endregion
|
|
31486
|
+
//#region src/emit/OutcomeScript.ts
|
|
31630
31487
|
/**
|
|
31631
|
-
*
|
|
31632
|
-
*
|
|
31633
|
-
*
|
|
31634
|
-
*
|
|
31488
|
+
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
31489
|
+
* tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
|
|
31490
|
+
* re-quoted here: a value reaches the script as a printf ARGUMENT, never
|
|
31491
|
+
* interpolated into the format itself, so a subject or state name containing
|
|
31492
|
+
* `%` can never be read as a conversion spec. Markers like `->` are arguments
|
|
31493
|
+
* too, keeping every format string free of a leading `-` that a shell's
|
|
31494
|
+
* `printf` could mistake for an option. A real newline in `fmt` is emitted as
|
|
31495
|
+
* the two-character `\n` escape `printf` itself expands, keeping every emitted
|
|
31496
|
+
* statement on one line.
|
|
31635
31497
|
*/
|
|
31636
|
-
const
|
|
31637
|
-
const paths = [...entries.keys()];
|
|
31638
|
-
const readWorktree = (path) => {
|
|
31639
|
-
try {
|
|
31640
|
-
return workspace.readSync(path);
|
|
31641
|
-
} catch {
|
|
31642
|
-
return;
|
|
31643
|
-
}
|
|
31644
|
-
};
|
|
31645
|
-
return {
|
|
31646
|
-
paths: () => paths,
|
|
31647
|
-
read: (path) => {
|
|
31648
|
-
if (!entries.has(path)) return void 0;
|
|
31649
|
-
const content = readWorktree(path);
|
|
31650
|
-
return content === void 0 || rewrite?.path !== path ? content : rewrite.apply(content);
|
|
31651
|
-
},
|
|
31652
|
-
id: (path) => rewrite?.path === path ? void 0 : entries.get(path)
|
|
31653
|
-
};
|
|
31654
|
-
};
|
|
31498
|
+
const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
31655
31499
|
/**
|
|
31656
|
-
*
|
|
31657
|
-
*
|
|
31658
|
-
*
|
|
31659
|
-
*
|
|
31660
|
-
* process is being started.
|
|
31500
|
+
* Marks an emitted block as print-only. Load-bearing beyond documentation:
|
|
31501
|
+
* `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
|
|
31502
|
+
* this line, and a bare `printf` would be ambiguous with a workflow
|
|
31503
|
+
* `script:` command that happens to start the same way.
|
|
31661
31504
|
*/
|
|
31662
|
-
const
|
|
31663
|
-
|
|
31664
|
-
|
|
31665
|
-
|
|
31666
|
-
|
|
31667
|
-
|
|
31668
|
-
|
|
31669
|
-
|
|
31670
|
-
|
|
31671
|
-
|
|
31672
|
-
|
|
31673
|
-
|
|
31674
|
-
|
|
31675
|
-
|
|
31505
|
+
const OUTCOME_MARKER = "# gtd: outcome (print-only)";
|
|
31506
|
+
const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
|
|
31507
|
+
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";
|
|
31508
|
+
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";
|
|
31509
|
+
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
31510
|
+
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
31511
|
+
/** One already-rendered plain line. */
|
|
31512
|
+
const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
|
|
31513
|
+
/**
|
|
31514
|
+
* Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
|
|
31515
|
+
* — this line runs AFTER the reset that made `head` the new HEAD, so neither
|
|
31516
|
+
* value is knowable when gtd generates it.
|
|
31517
|
+
*/
|
|
31518
|
+
const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
|
|
31519
|
+
shellQuote$1(from),
|
|
31520
|
+
`"$(git rev-parse --short ${shellQuote$1(head)})"`,
|
|
31521
|
+
`"$(git log -1 --format=%s ${shellQuote$1(head)})"`,
|
|
31522
|
+
shellQuote$1(state)
|
|
31523
|
+
]);
|
|
31524
|
+
/** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
|
|
31525
|
+
const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
|
|
31526
|
+
/** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
|
|
31527
|
+
const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
|
|
31528
|
+
`"$(git rev-parse --short ${shellQuote$1(to)})"`,
|
|
31529
|
+
`"$(git log -1 --format=%s ${shellQuote$1(to)})"`,
|
|
31530
|
+
shellQuote$1(state)
|
|
31531
|
+
]);
|
|
31532
|
+
//#endregion
|
|
31533
|
+
//#region src/emit/ModeContradiction.ts
|
|
31534
|
+
/**
|
|
31535
|
+
* Written for two readers at once, since gtd validate's exit code carries no
|
|
31536
|
+
* second meaning to distinguish them: the agent (this is a CONFIGURATION BUG,
|
|
31537
|
+
* not the steering file — stop, don't edit it, or a fix-retry loop will
|
|
31538
|
+
* thrash on the document) and the human (which mode, and the exact rendered
|
|
31539
|
+
* `format:` command, to fix `gtd.config.ts` without reproducing the failure).
|
|
31540
|
+
*/
|
|
31541
|
+
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:`;
|
|
31542
|
+
/**
|
|
31543
|
+
* Formats a copy of the mode's own canonical sample and re-validates it with
|
|
31544
|
+
* `gtd check <mode>`, failing loudly when the round-trip breaks it — detects a
|
|
31545
|
+
* mode whose `format:` contradicts its own `validate:` without ever touching
|
|
31546
|
+
* the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
|
|
31547
|
+
* heredoc, since no delimiter chosen ahead of time can collide with the
|
|
31548
|
+
* sample's own bytes. One bash fragment with no blank lines (so
|
|
31549
|
+
* `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
|
|
31550
|
+
* to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
|
|
31551
|
+
* still fires at a first-write beat where the real steering file doesn't
|
|
31552
|
+
* exist yet.
|
|
31553
|
+
*/
|
|
31554
|
+
const buildModeContradictionCheck = (inputs) => {
|
|
31555
|
+
const { mode, samplePath, sample, formatCommand } = inputs;
|
|
31556
|
+
const pathQ = shellQuote$1(samplePath);
|
|
31557
|
+
const messageQ = shellQuote$1(contradictionMessage(mode, formatCommand));
|
|
31558
|
+
return [
|
|
31559
|
+
`printf '%s' ${shellQuote$1(sample)} > ${pathQ}`,
|
|
31560
|
+
formatCommand,
|
|
31561
|
+
`gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
|
|
31562
|
+
` printf '%s\\n' ${messageQ} >&2`,
|
|
31563
|
+
` cat ${pathQ} >&2`,
|
|
31564
|
+
` rm -f ${pathQ}`,
|
|
31565
|
+
` exit 1`,
|
|
31566
|
+
`}`,
|
|
31567
|
+
`rm -f ${pathQ}`
|
|
31568
|
+
].join("\n");
|
|
31676
31569
|
};
|
|
31677
31570
|
/**
|
|
31678
|
-
*
|
|
31679
|
-
*
|
|
31680
|
-
*
|
|
31571
|
+
* Skip notice for a mode whose validator is EXTERNAL (a user `validate:`
|
|
31572
|
+
* command, not one of the two built-in modes) — there's no in-process parser
|
|
31573
|
+
* to round-trip a sample through. Printed to stderr since silence would read
|
|
31574
|
+
* as a clean bill of health.
|
|
31681
31575
|
*/
|
|
31682
|
-
const
|
|
31683
|
-
|
|
31684
|
-
const raw = vars.judgeBudgetBytes;
|
|
31685
|
-
if (raw === void 0) return DEFAULT_JUDGE_BUDGET_BYTES;
|
|
31686
|
-
const n = Number(raw);
|
|
31687
|
-
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)`);
|
|
31688
|
-
return n;
|
|
31689
|
-
};
|
|
31690
|
-
const replayFor = (setup, pending) => replay({
|
|
31691
|
-
flow: setup.def.flow,
|
|
31692
|
-
episode: {
|
|
31693
|
-
base: setup.base,
|
|
31694
|
-
commits: setup.episode
|
|
31695
|
-
},
|
|
31696
|
-
vars: setup.vars,
|
|
31697
|
-
env: setup.env,
|
|
31698
|
-
start: setup.run.diffBase,
|
|
31699
|
-
startTree: setup.run.diffBase === EMPTY_TREE$1 ? treeFromRecord({}) : commitTree(setup.workspace, setup.run.diffBase),
|
|
31700
|
-
budgetBytes: setup.budget,
|
|
31701
|
-
skills: setup.skills,
|
|
31702
|
-
configuredSkills: setup.def.configuredSkills,
|
|
31703
|
-
access: setup.access,
|
|
31704
|
-
configuredAccess: setup.def.configuredAccess,
|
|
31705
|
-
...pending !== void 0 ? { pending } : {}
|
|
31706
|
-
});
|
|
31707
|
-
const replayError = (outcome) => {
|
|
31708
|
-
if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
|
|
31709
|
-
if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
|
|
31710
|
-
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");
|
|
31576
|
+
const modeContradictionSkipNotice = (mode) => {
|
|
31577
|
+
return `printf '%s\\n' ${shellQuote$1(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
|
|
31711
31578
|
};
|
|
31712
|
-
|
|
31579
|
+
//#endregion
|
|
31580
|
+
//#region src/emit/SteeringMode.ts
|
|
31581
|
+
init_dist$1();
|
|
31582
|
+
init_steering();
|
|
31713
31583
|
/**
|
|
31714
|
-
*
|
|
31715
|
-
*
|
|
31716
|
-
*
|
|
31584
|
+
* `format` alone (no live/external verdict) when there's no validator at
|
|
31585
|
+
* all; a live in-process validator for a `"builtin"` validate OR a command
|
|
31586
|
+
* that is exactly the format's own seeded string; `externalValidate` for any
|
|
31587
|
+
* other command. Each arm returns directly — a flat sequence, not nested
|
|
31588
|
+
* ternaries — so the four outcomes stay each their own case to read, not a
|
|
31589
|
+
* boolean expression to re-derive.
|
|
31717
31590
|
*/
|
|
31718
|
-
const
|
|
31719
|
-
|
|
31720
|
-
|
|
31721
|
-
|
|
31722
|
-
|
|
31723
|
-
return {
|
|
31724
|
-
actor: step.actor,
|
|
31725
|
-
...optional("label", options.label),
|
|
31726
|
-
...optional("file", options.file),
|
|
31727
|
-
...optional("mode", options.mode)
|
|
31728
|
-
};
|
|
31729
|
-
};
|
|
31730
|
-
const promptDef = (common, request, skills, access) => ({
|
|
31731
|
-
...common,
|
|
31732
|
-
kind: "prompt",
|
|
31733
|
-
content: request.prompt,
|
|
31734
|
-
...optional("model", request.options.model),
|
|
31735
|
-
...optional("system", request.options.system),
|
|
31736
|
-
...optional("allowEmpty", request.options.allowEmpty),
|
|
31737
|
-
...optional("keep", request.options.keep),
|
|
31738
|
-
...optional("skills", skills),
|
|
31739
|
-
...optional("access", access)
|
|
31740
|
-
});
|
|
31741
|
-
const scriptDef = (common, request) => ({
|
|
31742
|
-
...common,
|
|
31743
|
-
kind: "script",
|
|
31744
|
-
content: request.body
|
|
31745
|
-
});
|
|
31746
|
-
const judgeDef = (common, request, step) => {
|
|
31747
|
-
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
31748
|
-
return {
|
|
31749
|
-
...common,
|
|
31750
|
-
kind: "message",
|
|
31751
|
-
content: step.truncated ? `${message}\n\n${TRUNCATION_NOTICE}` : message,
|
|
31752
|
-
...optional("judge", judgeDocument(step))
|
|
31591
|
+
const capabilitiesFor = (mode, format, validate) => {
|
|
31592
|
+
if (format === void 0) return validate?.kind === "command" ? { externalValidate: true } : {};
|
|
31593
|
+
if (validate?.kind === "builtin") return {
|
|
31594
|
+
format,
|
|
31595
|
+
liveValidate: format.validate
|
|
31753
31596
|
};
|
|
31754
|
-
};
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
content: request.options.message ?? request.options.label ?? step.name,
|
|
31759
|
-
...optional("acceptClean", request.options.approve === true || request.options.acceptClean),
|
|
31760
|
-
...optional("approve", request.options.approve)
|
|
31761
|
-
});
|
|
31762
|
-
const stepDefOf = (step) => {
|
|
31763
|
-
const request = step.request;
|
|
31764
|
-
const common = commonOf(step);
|
|
31765
|
-
switch (request.kind) {
|
|
31766
|
-
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
31767
|
-
case "run": return scriptDef(common, request);
|
|
31768
|
-
case "judge": return judgeDef(common, request, step);
|
|
31769
|
-
case "human": return messageDef(common, request, step);
|
|
31770
|
-
}
|
|
31771
|
-
};
|
|
31772
|
-
const ROOT_MEMORY_SCOPE_NAME = "root";
|
|
31773
|
-
/**
|
|
31774
|
-
* An agent rest's memory key, `<scope>#<hash7>`: the hash is the commit the
|
|
31775
|
-
* current unbroken run into the scope started FROM, so the same run always
|
|
31776
|
-
* re-derives the same key. `resumed` is whether an agent turn in that run
|
|
31777
|
-
* already landed.
|
|
31778
|
-
*/
|
|
31779
|
-
const memoryOf = (trace, run) => {
|
|
31780
|
-
const rest = trace[trace.length - 1];
|
|
31781
|
-
if (rest === void 0 || rest.kind !== "agent") return {
|
|
31782
|
-
key: void 0,
|
|
31783
|
-
resumed: false
|
|
31597
|
+
if (validate === void 0) return { format };
|
|
31598
|
+
if (isSeededValidateCommand(mode, validate.command)) return {
|
|
31599
|
+
format,
|
|
31600
|
+
liveValidate: format.validate
|
|
31784
31601
|
};
|
|
31785
|
-
const start = scopeRunStart(trace, rest.memoryScope);
|
|
31786
|
-
const token = start <= 0 ? run.startParentHash : trace[start - 1].enteredAt;
|
|
31787
|
-
const resumed = trace.slice(Math.max(start, 0), trace.length - 1).some((s) => s.kind === "agent" && s.memoryScope === rest.memoryScope);
|
|
31788
31602
|
return {
|
|
31789
|
-
|
|
31790
|
-
|
|
31603
|
+
format,
|
|
31604
|
+
externalValidate: true
|
|
31791
31605
|
};
|
|
31792
31606
|
};
|
|
31793
|
-
|
|
31794
|
-
|
|
31795
|
-
|
|
31796
|
-
|
|
31797
|
-
|
|
31798
|
-
|
|
31799
|
-
|
|
31800
|
-
|
|
31801
|
-
|
|
31802
|
-
|
|
31803
|
-
|
|
31804
|
-
|
|
31805
|
-
|
|
31806
|
-
|
|
31807
|
-
|
|
31808
|
-
|
|
31809
|
-
|
|
31810
|
-
|
|
31811
|
-
|
|
31812
|
-
|
|
31813
|
-
|
|
31814
|
-
|
|
31815
|
-
|
|
31816
|
-
|
|
31817
|
-
|
|
31818
|
-
|
|
31819
|
-
|
|
31820
|
-
|
|
31821
|
-
|
|
31822
|
-
|
|
31823
|
-
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
31824
|
-
const budget = yield* try_({
|
|
31825
|
-
try: () => judgeBudgetBytes(vars),
|
|
31826
|
-
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
31827
|
-
});
|
|
31828
|
-
yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
|
|
31829
|
-
const base = run.episode.base === void 0 ? {
|
|
31830
|
-
hash: EMPTY_TREE$1,
|
|
31831
|
-
tree: treeFromRecord({})
|
|
31832
|
-
} : {
|
|
31833
|
-
hash: run.episode.base,
|
|
31834
|
-
tree: commitTree(workspace, run.episode.base)
|
|
31835
|
-
};
|
|
31836
|
-
const git = yield* GitService;
|
|
31837
|
-
const episode = [];
|
|
31838
|
-
for (const c of run.episode.commits) {
|
|
31839
|
-
const tip = parseCommitMessage(c.message).kept;
|
|
31840
|
-
const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
|
|
31841
|
-
episode.push({
|
|
31842
|
-
hash: c.hash,
|
|
31843
|
-
message: c.message,
|
|
31844
|
-
tree: commitTree(workspace, c.hash),
|
|
31845
|
-
...exists ? { kept: {
|
|
31846
|
-
hash: tip,
|
|
31847
|
-
tree: commitTree(workspace, tip)
|
|
31848
|
-
} } : {}
|
|
31849
|
-
});
|
|
31850
|
-
}
|
|
31851
|
-
let pendingMemo;
|
|
31852
|
-
return {
|
|
31853
|
-
resolved,
|
|
31854
|
-
def,
|
|
31855
|
-
vars,
|
|
31856
|
-
env,
|
|
31857
|
-
setup: {
|
|
31858
|
-
def,
|
|
31859
|
-
run,
|
|
31860
|
-
vars,
|
|
31861
|
-
env,
|
|
31862
|
-
budget,
|
|
31863
|
-
skills,
|
|
31864
|
-
access,
|
|
31865
|
-
workspace,
|
|
31866
|
-
base,
|
|
31867
|
-
episode,
|
|
31868
|
-
pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
|
|
31869
|
-
}
|
|
31870
|
-
};
|
|
31871
|
-
});
|
|
31872
|
-
const pendingChanges = (git, withIndex) => gen(function* () {
|
|
31873
|
-
const worktree = yield* git.changedPaths();
|
|
31874
|
-
if (!withIndex) return worktree;
|
|
31875
|
-
const seen = new Set(worktree.map((e) => e.path));
|
|
31876
|
-
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
|
|
31877
|
-
});
|
|
31878
|
-
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
31879
|
-
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
31880
|
-
const git = yield* GitService;
|
|
31881
|
-
const config = yield* (yield* ConfigService).load;
|
|
31882
|
-
const workspace = yield* Workspace;
|
|
31883
|
-
const host = yield* Host;
|
|
31884
|
-
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
31885
|
-
const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
|
|
31886
|
-
const outcome = yield* promise(() => replayFor(setup));
|
|
31887
|
-
const error = replayError(outcome);
|
|
31888
|
-
if (error !== void 0 || outcome.kind !== "rest") return yield* fail$8(error ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
|
|
31889
|
-
const step = outcome.rest;
|
|
31890
|
-
yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
|
|
31891
|
-
const stepDef = stepDefOf(step);
|
|
31892
|
-
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$8(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
31893
|
-
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
31894
|
-
status: normalizeStatus(e.status),
|
|
31895
|
-
path: e.path
|
|
31896
|
-
})) : [];
|
|
31897
|
-
const memory = memoryOf(outcome.trace, run);
|
|
31898
|
-
const configNow = config.fingerprint;
|
|
31899
|
-
if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
|
|
31607
|
+
/**
|
|
31608
|
+
* Resolve a `mode:` name against `def.modes` plus the built-in registry
|
|
31609
|
+
* (`src/workflow/SteeringFormats.ts`) — half by half: a declared `format:`/`validate:`
|
|
31610
|
+
* wins, and an undeclared `validate:` falls back to the built-in format's own
|
|
31611
|
+
* parser when the name is registered. `format` is set from the registry
|
|
31612
|
+
* ALONE, independent of which half of `validate` wins — a declared validator
|
|
31613
|
+
* overrides validation, not the format identity.
|
|
31614
|
+
*
|
|
31615
|
+
* `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
|
|
31616
|
+
* against the registry alone, with no workflow definition in hand (e.g. the
|
|
31617
|
+
* basename dispatch for a `REVIEW.md` no config maps).
|
|
31618
|
+
*
|
|
31619
|
+
* The `"unknown"` arm carries the whole refusal message, naming what IS known
|
|
31620
|
+
* when `def` is given — this is the one place that message is built, so it
|
|
31621
|
+
* can never drift from what actually resolved.
|
|
31622
|
+
*/
|
|
31623
|
+
const resolveMode = (def, state, mode) => {
|
|
31624
|
+
const declared = def?.modes?.[mode];
|
|
31625
|
+
const format = steeringFormatFor(mode);
|
|
31626
|
+
if (declared === void 0 && format === void 0) return {
|
|
31627
|
+
kind: "unknown",
|
|
31628
|
+
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`
|
|
31629
|
+
};
|
|
31630
|
+
const validate = declared?.validate !== void 0 ? {
|
|
31631
|
+
kind: "command",
|
|
31632
|
+
command: declared.validate
|
|
31633
|
+
} : format !== void 0 ? {
|
|
31634
|
+
kind: "builtin",
|
|
31635
|
+
format
|
|
31636
|
+
} : void 0;
|
|
31900
31637
|
return {
|
|
31901
|
-
|
|
31902
|
-
|
|
31903
|
-
|
|
31904
|
-
|
|
31905
|
-
|
|
31906
|
-
|
|
31907
|
-
trace: outcome.trace,
|
|
31908
|
-
run,
|
|
31909
|
-
vars,
|
|
31910
|
-
env,
|
|
31911
|
-
config: configNow,
|
|
31912
|
-
changes,
|
|
31913
|
-
memory: memory.key,
|
|
31914
|
-
memoryResumed: memory.resumed,
|
|
31915
|
-
hints: hintsOf(stepDef),
|
|
31916
|
-
cost: costTotals(run.costEntries),
|
|
31917
|
-
setup
|
|
31638
|
+
kind: "resolved",
|
|
31639
|
+
mode,
|
|
31640
|
+
...format !== void 0 ? { format } : {},
|
|
31641
|
+
...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
|
|
31642
|
+
...validate !== void 0 ? { validate } : {},
|
|
31643
|
+
capabilities: capabilitiesFor(mode, format, validate)
|
|
31918
31644
|
};
|
|
31645
|
+
};
|
|
31646
|
+
/** `gtd next`'s fix-retry instruction, printed ahead of the failing command's captured output. */
|
|
31647
|
+
const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
|
|
31648
|
+
/**
|
|
31649
|
+
* Pushes a `binaryGuard` step ahead of `command` when its own leading word
|
|
31650
|
+
* is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
|
|
31651
|
+
* the script on its own `exit 127`, without touching `failurePromptWrapper`'s
|
|
31652
|
+
* captured-output path. Each command guards on its OWN leading word — a
|
|
31653
|
+
* guard naming the wrong binary is worse than none.
|
|
31654
|
+
*/
|
|
31655
|
+
const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
|
|
31656
|
+
const binary = extractLeadingBinary(command);
|
|
31657
|
+
if (binary !== void 0) steps.push({
|
|
31658
|
+
kind: "command",
|
|
31659
|
+
command: binaryGuard(binary, mode, key)
|
|
31660
|
+
});
|
|
31661
|
+
steps.push({
|
|
31662
|
+
kind: "command",
|
|
31663
|
+
command: withFileVar$1(command, file),
|
|
31664
|
+
...onFailure !== void 0 ? { onFailure } : {}
|
|
31665
|
+
});
|
|
31666
|
+
};
|
|
31667
|
+
/**
|
|
31668
|
+
* `resolved`'s `format:`/`validate:` as `EmitStep[]` — format first,
|
|
31669
|
+
* validate last, the last one wrapped with `onFailure` when the validator is
|
|
31670
|
+
* a command (a built-in's own in-process parser has no shell step to wrap:
|
|
31671
|
+
* `gtd check`'s seeded command carries that instead). Each command gets its
|
|
31672
|
+
* own `binaryGuard` immediately ahead of it.
|
|
31673
|
+
*/
|
|
31674
|
+
const modeCommandSteps = (resolved, file) => {
|
|
31675
|
+
const steps = [];
|
|
31676
|
+
if (resolved.formatCommand !== void 0) pushGuardedCommand(steps, resolved.mode, "format", resolved.formatCommand, file, void 0);
|
|
31677
|
+
if (resolved.validate?.kind === "command") pushGuardedCommand(steps, resolved.mode, "validate", resolved.validate.command, file, fixPromptInstruction(file));
|
|
31678
|
+
return steps;
|
|
31679
|
+
};
|
|
31680
|
+
/** `<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). */
|
|
31681
|
+
const scratchSamplePath = (scratchDir, mode) => join(scratchDir, `gtd-mode-sample-${mode}-${process.pid}.md`);
|
|
31682
|
+
/**
|
|
31683
|
+
* The contradiction round-trip/skip-notice steps for `resolved`'s `mode:`.
|
|
31684
|
+
* No `formatCommand` means nothing to round-trip: empty. Otherwise: a live
|
|
31685
|
+
* built-in validator runs the round-trip against that format's own canonical
|
|
31686
|
+
* sample; an external validator prints a one-line skip notice instead, since
|
|
31687
|
+
* silence there would read as a clean bill of health; a format-only mode with
|
|
31688
|
+
* neither has nothing to round-trip either.
|
|
31689
|
+
*/
|
|
31690
|
+
const modeContradictionSteps = (resolved) => gen(function* () {
|
|
31691
|
+
if (resolved.formatCommand === void 0) return [];
|
|
31692
|
+
const { capabilities } = resolved;
|
|
31693
|
+
if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
|
|
31694
|
+
const { scratchDir } = yield* Host;
|
|
31695
|
+
const samplePath = scratchSamplePath(scratchDir, resolved.mode);
|
|
31696
|
+
const formatCommand = withFileVar$1(resolved.formatCommand, samplePath);
|
|
31697
|
+
return [{
|
|
31698
|
+
kind: "command",
|
|
31699
|
+
command: buildModeContradictionCheck({
|
|
31700
|
+
mode: resolved.mode,
|
|
31701
|
+
samplePath,
|
|
31702
|
+
sample: capabilities.format.sample,
|
|
31703
|
+
formatCommand
|
|
31704
|
+
})
|
|
31705
|
+
}];
|
|
31706
|
+
}
|
|
31707
|
+
if (capabilities.externalValidate === true) return [{
|
|
31708
|
+
kind: "command",
|
|
31709
|
+
command: modeContradictionSkipNotice(resolved.mode)
|
|
31710
|
+
}];
|
|
31711
|
+
return [];
|
|
31919
31712
|
});
|
|
31920
|
-
const currentRest = restAt(void 0);
|
|
31921
31713
|
/**
|
|
31922
|
-
*
|
|
31923
|
-
*
|
|
31924
|
-
*
|
|
31714
|
+
* Renders `resolved`'s complete validate script for `file`: the mode's
|
|
31715
|
+
* format/validate contradiction check, a guard for a not-yet-written file,
|
|
31716
|
+
* then the format/validate commands themselves (format first). gtd never
|
|
31717
|
+
* runs this script itself — `resolveMode`+`validateScriptFor` only render
|
|
31718
|
+
* text for a driver to execute (`gtd renders; the driver executes`).
|
|
31925
31719
|
*/
|
|
31926
|
-
const
|
|
31927
|
-
|
|
31928
|
-
|
|
31929
|
-
|
|
31930
|
-
|
|
31931
|
-
|
|
31932
|
-
const outcome = yield* promise(() => replay({
|
|
31933
|
-
flow: resolved.def.flow,
|
|
31934
|
-
episode: {
|
|
31935
|
-
base: {
|
|
31936
|
-
hash: "",
|
|
31937
|
-
tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
|
|
31938
|
-
},
|
|
31939
|
-
commits: []
|
|
31720
|
+
const validateScriptFor = (resolved, file) => gen(function* () {
|
|
31721
|
+
return { script: emitScripts([
|
|
31722
|
+
...yield* modeContradictionSteps(resolved),
|
|
31723
|
+
{
|
|
31724
|
+
kind: "command",
|
|
31725
|
+
command: fileExistsGuard(file)
|
|
31940
31726
|
},
|
|
31941
|
-
|
|
31942
|
-
|
|
31943
|
-
start: "",
|
|
31944
|
-
budgetBytes: rest.setup.budget,
|
|
31945
|
-
skills,
|
|
31946
|
-
configuredSkills: resolved.def.configuredSkills,
|
|
31947
|
-
access,
|
|
31948
|
-
configuredAccess: resolved.def.configuredAccess
|
|
31949
|
-
}));
|
|
31950
|
-
if (outcome.kind === "rest") return { firstStep: outcome.rest.name };
|
|
31951
|
-
if (outcome.kind === "refused") return { refusal: outcome.message };
|
|
31952
|
-
const why = outcome.kind === "ended" ? "the flow reaches no step" : outcome.message;
|
|
31953
|
-
return { refusal: `workflow "${resolved.name}" cannot start — ${why}` };
|
|
31954
|
-
});
|
|
31955
|
-
/** The review window's diff base at the rest. */
|
|
31956
|
-
const reviewBaseFor = (rest) => rest.step.request.options.base ?? rest.run.diffBase;
|
|
31957
|
-
const renderRest = (rest) => succeed$1({
|
|
31958
|
-
state: rest.state,
|
|
31959
|
-
actor: rest.actor,
|
|
31960
|
-
kind: rest.stepDef.kind,
|
|
31961
|
-
content: rest.stepDef.content,
|
|
31962
|
-
...rest.hints,
|
|
31963
|
-
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
31964
|
-
memoryResumed: rest.memoryResumed
|
|
31727
|
+
...modeCommandSteps(resolved, file)
|
|
31728
|
+
]).required };
|
|
31965
31729
|
});
|
|
31730
|
+
//#endregion
|
|
31731
|
+
//#region src/step/planStep.ts
|
|
31966
31732
|
/**
|
|
31967
|
-
*
|
|
31968
|
-
*
|
|
31733
|
+
* At the human review gate an unconditional `gtd uncheck '<file>'` runs
|
|
31734
|
+
* ahead of the commit, resetting every checkbox before `git add -A` picks it
|
|
31735
|
+
* up — a tick is read-progress, never sign-off.
|
|
31969
31736
|
*/
|
|
31970
|
-
const
|
|
31971
|
-
|
|
31972
|
-
|
|
31973
|
-
|
|
31974
|
-
const
|
|
31975
|
-
|
|
31976
|
-
|
|
31977
|
-
|
|
31978
|
-
|
|
31979
|
-
if (rest.changes.length > 0) return {
|
|
31980
|
-
kind: "refusal",
|
|
31981
|
-
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\``
|
|
31737
|
+
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
31738
|
+
kind: "uncheck",
|
|
31739
|
+
file: snapshot.file
|
|
31740
|
+
}] : [];
|
|
31741
|
+
const gitWriteOf = (landing, message) => {
|
|
31742
|
+
if (landing.only !== void 0) return {
|
|
31743
|
+
kind: "commitPath",
|
|
31744
|
+
path: landing.only,
|
|
31745
|
+
message
|
|
31982
31746
|
};
|
|
31983
|
-
if (
|
|
31984
|
-
kind: "
|
|
31985
|
-
|
|
31747
|
+
if (landing.keepTurn !== void 0) return {
|
|
31748
|
+
kind: "keepTurn",
|
|
31749
|
+
ref: landing.keepTurn,
|
|
31750
|
+
message
|
|
31986
31751
|
};
|
|
31987
|
-
if (
|
|
31988
|
-
|
|
31989
|
-
|
|
31990
|
-
}
|
|
31991
|
-
|
|
31992
|
-
|
|
31752
|
+
if (landing.spec.approved !== void 0) return {
|
|
31753
|
+
kind: "commitClean",
|
|
31754
|
+
message
|
|
31755
|
+
};
|
|
31756
|
+
return {
|
|
31757
|
+
kind: "commitAll",
|
|
31758
|
+
message
|
|
31993
31759
|
};
|
|
31994
31760
|
};
|
|
31995
|
-
|
|
31996
|
-
const
|
|
31997
|
-
if (
|
|
31998
|
-
|
|
31999
|
-
|
|
32000
|
-
kind: "attempt",
|
|
32001
|
-
subject: formatSubject(rest.actor, rest.state)
|
|
31761
|
+
const planStep = (snapshot, opts = {}) => {
|
|
31762
|
+
const landing = snapshot.landing;
|
|
31763
|
+
if (landing.kind === "refusal") return {
|
|
31764
|
+
kind: "refusal",
|
|
31765
|
+
message: landing.message
|
|
32002
31766
|
};
|
|
32003
|
-
if (
|
|
31767
|
+
if (landing.kind === "noop") return {
|
|
32004
31768
|
kind: "noop",
|
|
32005
|
-
|
|
31769
|
+
state: snapshot.state,
|
|
31770
|
+
settled: landing.settled
|
|
31771
|
+
};
|
|
31772
|
+
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
31773
|
+
cost: opts.cost,
|
|
31774
|
+
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
31775
|
+
} };
|
|
31776
|
+
if (landing.kind === "attempt") {
|
|
31777
|
+
const message = formatCommitMessage({
|
|
31778
|
+
actor: snapshot.actor,
|
|
31779
|
+
to: snapshot.state,
|
|
31780
|
+
...cost
|
|
31781
|
+
});
|
|
31782
|
+
return {
|
|
31783
|
+
kind: "commit",
|
|
31784
|
+
state: snapshot.state,
|
|
31785
|
+
to: snapshot.state,
|
|
31786
|
+
subject: landing.subject,
|
|
31787
|
+
steps: [{
|
|
31788
|
+
kind: "gitWrite",
|
|
31789
|
+
write: {
|
|
31790
|
+
kind: "commitAll",
|
|
31791
|
+
message
|
|
31792
|
+
}
|
|
31793
|
+
}, {
|
|
31794
|
+
kind: "outcome",
|
|
31795
|
+
outcome: {
|
|
31796
|
+
kind: "commit",
|
|
31797
|
+
subject: landing.subject
|
|
31798
|
+
}
|
|
31799
|
+
}]
|
|
31800
|
+
};
|
|
31801
|
+
}
|
|
31802
|
+
const message = formatCommitMessage({
|
|
31803
|
+
...landing.spec,
|
|
31804
|
+
...cost,
|
|
31805
|
+
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
31806
|
+
});
|
|
31807
|
+
const subject = message.split("\n")[0];
|
|
31808
|
+
const outcome = landing.to === snapshot.state ? {
|
|
31809
|
+
kind: "outcome",
|
|
31810
|
+
outcome: {
|
|
31811
|
+
kind: "commit",
|
|
31812
|
+
subject
|
|
31813
|
+
}
|
|
31814
|
+
} : {
|
|
31815
|
+
kind: "outcome",
|
|
31816
|
+
outcome: {
|
|
31817
|
+
kind: "transition",
|
|
31818
|
+
from: snapshot.state,
|
|
31819
|
+
to: landing.to
|
|
31820
|
+
}
|
|
32006
31821
|
};
|
|
32007
|
-
};
|
|
32008
|
-
/** At the human review gate the pending tree lands with every tick cleared, as the landing script will. */
|
|
32009
|
-
const reviewGateRewrite = (def) => {
|
|
32010
|
-
const reviewFormat = steeringFormatFor("review");
|
|
32011
|
-
if (!isHumanReviewGate(def) || def.file === void 0 || reviewFormat === void 0) return;
|
|
32012
31822
|
return {
|
|
32013
|
-
|
|
32014
|
-
|
|
32015
|
-
|
|
32016
|
-
|
|
32017
|
-
|
|
32018
|
-
|
|
32019
|
-
|
|
32020
|
-
|
|
32021
|
-
|
|
31823
|
+
kind: "commit",
|
|
31824
|
+
state: snapshot.state,
|
|
31825
|
+
to: landing.to,
|
|
31826
|
+
subject,
|
|
31827
|
+
steps: [
|
|
31828
|
+
...uncheckStep(snapshot),
|
|
31829
|
+
{
|
|
31830
|
+
kind: "gitWrite",
|
|
31831
|
+
write: gitWriteOf(landing, message)
|
|
31832
|
+
},
|
|
31833
|
+
...landing.keepHistory !== void 0 ? [{
|
|
31834
|
+
kind: "gitWrite",
|
|
31835
|
+
write: {
|
|
31836
|
+
kind: "updateRef",
|
|
31837
|
+
ref: landing.keepHistory,
|
|
31838
|
+
target: "HEAD"
|
|
31839
|
+
}
|
|
31840
|
+
}] : [],
|
|
31841
|
+
outcome
|
|
31842
|
+
]
|
|
32022
31843
|
};
|
|
32023
31844
|
};
|
|
32024
|
-
|
|
32025
|
-
|
|
32026
|
-
kind: "refusal",
|
|
32027
|
-
message: outcome.message
|
|
32028
|
-
});
|
|
32029
|
-
if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$8(new Error(outcome.message));
|
|
32030
|
-
if (outcome.kind !== "rest") return succeed$1(rest.def.initial);
|
|
32031
|
-
const reserved = initialStepRefusal(rest.run.workflow, outcome.rest.name, rest.def.initial);
|
|
32032
|
-
return succeed$1(reserved === void 0 ? outcome.rest.name : reserved);
|
|
32033
|
-
};
|
|
31845
|
+
//#endregion
|
|
31846
|
+
//#region src/step/accessRefusal.ts
|
|
32034
31847
|
/**
|
|
32035
|
-
*
|
|
32036
|
-
*
|
|
32037
|
-
*
|
|
31848
|
+
* The landing refusal for changes outside `write`, or `undefined` when every
|
|
31849
|
+
* path is allowed. `null` is unrestricted. The message is the fix prompt: gtd
|
|
31850
|
+
* runs no fix turn of its own.
|
|
32038
31851
|
*/
|
|
32039
|
-
const
|
|
31852
|
+
const accessRefusal = (changes, write) => {
|
|
31853
|
+
if (write === null) return void 0;
|
|
31854
|
+
const offending = [...new Set(changes.filter((c) => !write.some((glob) => globMatches(c.path, glob))).map((c) => c.path))];
|
|
31855
|
+
if (offending.length === 0) return void 0;
|
|
31856
|
+
return [
|
|
31857
|
+
ACCESS_REFUSAL,
|
|
31858
|
+
...offending.map((path) => ` - ${path}`),
|
|
31859
|
+
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
31860
|
+
].join("\n");
|
|
31861
|
+
};
|
|
31862
|
+
//#endregion
|
|
31863
|
+
//#region src/step/planStart.ts
|
|
31864
|
+
init_dist$1();
|
|
31865
|
+
const refusal = (message) => ({
|
|
32040
31866
|
kind: "refusal",
|
|
32041
|
-
message
|
|
32042
|
-
} : void 0;
|
|
32043
|
-
const closingTrailers = (branch) => ({
|
|
32044
|
-
version: GTD_VERSION,
|
|
32045
|
-
...isSome(branch) ? { branch: branch.value } : {}
|
|
31867
|
+
message
|
|
32046
31868
|
});
|
|
32047
|
-
const
|
|
32048
|
-
const
|
|
32049
|
-
|
|
32050
|
-
|
|
32051
|
-
|
|
32052
|
-
}
|
|
32053
|
-
|
|
32054
|
-
|
|
32055
|
-
|
|
32056
|
-
|
|
32057
|
-
|
|
32058
|
-
|
|
32059
|
-
|
|
32060
|
-
|
|
32061
|
-
|
|
32062
|
-
const
|
|
31869
|
+
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
31870
|
+
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
31871
|
+
if (environmentOnly.length > 0) return yield* fail$8(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`));
|
|
31872
|
+
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
31873
|
+
if (undeclared.length > 0) return yield* fail$8(new GtdUsageError(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`));
|
|
31874
|
+
});
|
|
31875
|
+
/**
|
|
31876
|
+
* `gtd start <name>`: start a brand NEW process on a workflow, writing an
|
|
31877
|
+
* opening commit that names the workflow's first step and carries a
|
|
31878
|
+
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
31879
|
+
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
|
|
31880
|
+
* and a `Gtd-Config:` trailer per config file.
|
|
31881
|
+
* All validation is a REFUSAL, not an Effect failure.
|
|
31882
|
+
*/
|
|
31883
|
+
const planStart = (current, actor, start) => gen(function* () {
|
|
31884
|
+
const { workflow, first, commandLabel, vars: varOverrides } = start;
|
|
31885
|
+
const name = workflow.name;
|
|
31886
|
+
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before starting another`);
|
|
31887
|
+
if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`);
|
|
31888
|
+
const { firstStep } = first;
|
|
31889
|
+
yield* checkOverrides(commandLabel, varOverrides, Object.keys(workflow.vars), Object.keys(workflow.env));
|
|
31890
|
+
const baseOf = workflow.def.base;
|
|
31891
|
+
const flags = yield* StartFlags;
|
|
31892
|
+
const hostEnv = (yield* Host).env;
|
|
31893
|
+
const badFlag = ownFlagRefusal(workflow.vars, flags, hostEnv);
|
|
31894
|
+
if (badFlag !== void 0) return refusal(`${commandLabel}: ${badFlag}`);
|
|
31895
|
+
const vars = resolveStartVars(workflow.vars, flags, varOverrides, hostEnv);
|
|
31896
|
+
const multiline = multilineSetting(vars);
|
|
31897
|
+
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`);
|
|
31898
|
+
const template = yield* try_({
|
|
31899
|
+
try: () => baseOf?.(name, vars),
|
|
31900
|
+
catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
|
|
31901
|
+
});
|
|
31902
|
+
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
31903
|
+
if (typeof base === "object") return base;
|
|
31904
|
+
const subject = formatSubject(actor, firstStep);
|
|
32063
31905
|
return {
|
|
32064
|
-
kind: "
|
|
32065
|
-
|
|
32066
|
-
|
|
32067
|
-
|
|
32068
|
-
|
|
32069
|
-
|
|
32070
|
-
|
|
32071
|
-
|
|
32072
|
-
|
|
32073
|
-
|
|
32074
|
-
|
|
32075
|
-
|
|
32076
|
-
|
|
32077
|
-
|
|
31906
|
+
kind: "start",
|
|
31907
|
+
workflow: name,
|
|
31908
|
+
subject,
|
|
31909
|
+
steps: [{
|
|
31910
|
+
kind: "gitWrite",
|
|
31911
|
+
write: {
|
|
31912
|
+
kind: "commitAll",
|
|
31913
|
+
message: formatCommitMessage({
|
|
31914
|
+
actor,
|
|
31915
|
+
to: firstStep,
|
|
31916
|
+
workflow: name,
|
|
31917
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
31918
|
+
vars,
|
|
31919
|
+
config: start.config
|
|
31920
|
+
})
|
|
31921
|
+
}
|
|
31922
|
+
}, {
|
|
31923
|
+
kind: "outcome",
|
|
31924
|
+
outcome: {
|
|
31925
|
+
kind: "commit",
|
|
31926
|
+
subject
|
|
31927
|
+
}
|
|
31928
|
+
}]
|
|
32078
31929
|
};
|
|
32079
|
-
};
|
|
32080
|
-
const startTrailers = (rest, starts) => starts ? {
|
|
32081
|
-
vars: rest.vars,
|
|
32082
|
-
config: rest.config
|
|
32083
|
-
} : {};
|
|
32084
|
-
const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "ended" ? outcome.marks : [];
|
|
31930
|
+
});
|
|
32085
31931
|
/**
|
|
32086
|
-
*
|
|
32087
|
-
*
|
|
32088
|
-
*
|
|
31932
|
+
* The workflow's diff base: `git merge-base <base> HEAD`, where a blank base
|
|
31933
|
+
* means the default branch. The merge-base is what gets pinned, so a base on
|
|
31934
|
+
* another branch reviews exactly this branch's own commits.
|
|
32089
31935
|
*/
|
|
32090
|
-
const
|
|
32091
|
-
const
|
|
32092
|
-
|
|
32093
|
-
const
|
|
32094
|
-
|
|
32095
|
-
if (
|
|
32096
|
-
|
|
32097
|
-
|
|
32098
|
-
|
|
32099
|
-
|
|
31936
|
+
const resolveBase = (commandLabel, value) => gen(function* () {
|
|
31937
|
+
const git = yield* GitService;
|
|
31938
|
+
const rendered = value.trim();
|
|
31939
|
+
const target = rendered === "" ? yield* git.defaultBranch() : rendered;
|
|
31940
|
+
const resolvedBase = yield* result(git.resolveRef(target));
|
|
31941
|
+
if (resolvedBase._tag === "Failure") return refusal(`${commandLabel}: "${target}" does not resolve to a commit`);
|
|
31942
|
+
const mergeBase = yield* git.mergeBase(resolvedBase.success, "HEAD");
|
|
31943
|
+
if (isNone(mergeBase)) return refusal(`${commandLabel}: "${target}" shares no common ancestor with HEAD`);
|
|
31944
|
+
if (mergeBase.value === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: nothing to review: HEAD has no commits beyond ${target}`);
|
|
31945
|
+
return mergeBase.value;
|
|
31946
|
+
});
|
|
31947
|
+
//#endregion
|
|
31948
|
+
//#region src/step/strayPaths.ts
|
|
31949
|
+
/** Globs use the `access.write` dialect (`globMatches`). */
|
|
31950
|
+
const strayPaths = (touched, allow) => touched.filter((path) => !allow.some((glob) => globMatches(path, glob))).sort();
|
|
31951
|
+
//#endregion
|
|
31952
|
+
//#region src/edge/Edge.ts
|
|
31953
|
+
init_dist$1();
|
|
31954
|
+
init_steering();
|
|
31955
|
+
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
31956
|
+
const ACTORS = /* @__PURE__ */ new Set([
|
|
31957
|
+
"agent",
|
|
31958
|
+
"human",
|
|
31959
|
+
"check",
|
|
31960
|
+
"judge"
|
|
31961
|
+
]);
|
|
31962
|
+
const isOpening = ({ parsed, step }) => parsed !== void 0 && step === void 0 && parsed.from === void 0 && parsed.actor === "human";
|
|
31963
|
+
const boundsEpisode = (def, { parsed, pin }) => pin !== "add" && (parsed === void 0 || !ACTORS.has(parsed.actor) || parsed.to === def.initial);
|
|
31964
|
+
/**
|
|
31965
|
+
* Where the episode HEAD belongs to begins, read off subjects alone. Walking
|
|
31966
|
+
* back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
|
|
31967
|
+
* `gtd start` writes — no landing ever produces one — and opens an
|
|
31968
|
+
* episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
|
|
31969
|
+
* a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
|
|
31970
|
+
* entering the flow's first step (a finished episode), bounds the episode
|
|
31971
|
+
* from below.
|
|
31972
|
+
*/
|
|
31973
|
+
const locateEpisode = (def, history) => {
|
|
31974
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
31975
|
+
const message = parseCommitMessage(history[i].message);
|
|
31976
|
+
if (isOpening(message)) return {
|
|
31977
|
+
workflow: message.workflow,
|
|
31978
|
+
legacyOpening: message.workflow === void 0,
|
|
31979
|
+
baseIndex: i,
|
|
31980
|
+
processStart: i
|
|
31981
|
+
};
|
|
31982
|
+
if (boundsEpisode(def, message)) return {
|
|
31983
|
+
workflow: void 0,
|
|
31984
|
+
legacyOpening: false,
|
|
31985
|
+
baseIndex: i,
|
|
31986
|
+
processStart: i + 1
|
|
32100
31987
|
};
|
|
32101
31988
|
}
|
|
32102
|
-
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
32103
|
-
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
32104
|
-
...verdicts !== void 0 ? { verdicts } : {}
|
|
32105
|
-
}));
|
|
32106
|
-
const to = yield* landingTarget(outcome, rest);
|
|
32107
|
-
if (typeof to !== "string") return to;
|
|
32108
|
-
return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
32109
|
-
});
|
|
32110
|
-
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
32111
|
-
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$1(void 0)));
|
|
32112
|
-
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
32113
|
-
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
32114
|
-
const git = yield* GitService;
|
|
32115
|
-
const branch = yield* git.currentBranch();
|
|
32116
|
-
const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none();
|
|
32117
|
-
const landing = yield* decideLanding(rest, verdicts, branch, approver);
|
|
32118
31989
|
return {
|
|
32119
|
-
|
|
32120
|
-
|
|
32121
|
-
|
|
32122
|
-
|
|
32123
|
-
file: rest.hints.file,
|
|
32124
|
-
landing
|
|
31990
|
+
workflow: void 0,
|
|
31991
|
+
legacyOpening: false,
|
|
31992
|
+
baseIndex: -1,
|
|
31993
|
+
processStart: 0
|
|
32125
31994
|
};
|
|
32126
|
-
}
|
|
32127
|
-
const
|
|
32128
|
-
|
|
32129
|
-
|
|
32130
|
-
|
|
32131
|
-
|
|
32132
|
-
const entryCommit = run.trace[0].hash;
|
|
32133
|
-
const { total, byModel } = costTotals(run.costEntries);
|
|
31995
|
+
};
|
|
31996
|
+
const headTurnOf = (head) => {
|
|
31997
|
+
if (head === void 0) return void 0;
|
|
31998
|
+
const message = parseCommitMessage(head.message);
|
|
31999
|
+
const subject = message.parsed;
|
|
32000
|
+
if (subject === void 0 || !ACTORS.has(subject.actor) || message.pin !== void 0) return void 0;
|
|
32134
32001
|
return {
|
|
32135
|
-
|
|
32136
|
-
|
|
32137
|
-
|
|
32138
|
-
|
|
32139
|
-
processCost: total,
|
|
32140
|
-
processCostByModel: byModel,
|
|
32141
|
-
marks: run.marks,
|
|
32142
|
-
...settings
|
|
32002
|
+
state: subject.to,
|
|
32003
|
+
actor: subject.actor,
|
|
32004
|
+
empty: head.touched.length === 0,
|
|
32005
|
+
step: message.step !== void 0
|
|
32143
32006
|
};
|
|
32144
32007
|
};
|
|
32145
|
-
const
|
|
32146
|
-
|
|
32147
|
-
|
|
32148
|
-
|
|
32149
|
-
const summary = resolved.def.summary;
|
|
32150
|
-
if (summary === void 0 || run.trace.length === 0) return { prompt: void 0 };
|
|
32151
|
-
return { prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))) };
|
|
32008
|
+
const traceEntryOf = (hash, message) => ({
|
|
32009
|
+
state: message.parsed?.to ?? "",
|
|
32010
|
+
hash,
|
|
32011
|
+
actor: message.parsed?.actor ?? ""
|
|
32152
32012
|
});
|
|
32153
|
-
|
|
32154
|
-
|
|
32155
|
-
|
|
32156
|
-
|
|
32157
|
-
|
|
32013
|
+
const costEntriesOf = (hash, message) => message.cost.map((c) => ({
|
|
32014
|
+
cost: c.cost,
|
|
32015
|
+
model: c.model ?? "unspecified",
|
|
32016
|
+
hash
|
|
32017
|
+
}));
|
|
32018
|
+
const hashAt = (history, index) => history[index]?.hash;
|
|
32019
|
+
const historyFormatFault = (history, location) => formatFault(history.slice(location.baseIndex + 1).concat(history[location.baseIndex]?.message.startsWith("gtd") ? [history[location.baseIndex]] : []).map((c) => ({
|
|
32020
|
+
hash: c.hash,
|
|
32021
|
+
format: parseCommitMessage(c.message).format
|
|
32022
|
+
})));
|
|
32023
|
+
/** Fails on a history in a format this gtd does not read, before anything acts on it. */
|
|
32024
|
+
const runOf = (history, location, closingHash = void 0) => {
|
|
32025
|
+
const fault = historyFormatFault(history, location);
|
|
32026
|
+
return fault === void 0 ? succeed$1(buildRun(history, location, closingHash)) : fail$8(new Error(fault));
|
|
32158
32027
|
};
|
|
32159
|
-
const
|
|
32160
|
-
|
|
32161
|
-
|
|
32162
|
-
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
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");
|
|
32166
|
-
const historySteps = (run) => {
|
|
32167
|
-
const messages = new Map(run.episode.commits.map((c) => [c.hash, c.message]));
|
|
32168
|
-
return run.trace.map((entry) => {
|
|
32169
|
-
const message = parseCommitMessage(messages.get(entry.hash) ?? "");
|
|
32170
|
-
return {
|
|
32171
|
-
hash: entry.hash,
|
|
32172
|
-
subject: (messages.get(entry.hash) ?? "").split("\n")[0] ?? "",
|
|
32173
|
-
actor: entry.actor,
|
|
32174
|
-
from: message.parsed?.from,
|
|
32175
|
-
to: entry.state,
|
|
32176
|
-
verdicts: message.judge,
|
|
32177
|
-
...optional("approval", approvalEntryOf(entry.hash, message)[0])
|
|
32028
|
+
const pinnedConfigOf = (history) => {
|
|
32029
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
32030
|
+
const files = parseCommitMessage(history[i].message).config;
|
|
32031
|
+
if (Object.keys(files).length > 0) return {
|
|
32032
|
+
commit: history[i].hash,
|
|
32033
|
+
files
|
|
32178
32034
|
};
|
|
32179
|
-
}
|
|
32035
|
+
}
|
|
32180
32036
|
};
|
|
32181
|
-
const
|
|
32182
|
-
|
|
32183
|
-
|
|
32184
|
-
|
|
32185
|
-
|
|
32186
|
-
|
|
32187
|
-
const
|
|
32188
|
-
const
|
|
32189
|
-
const
|
|
32190
|
-
const
|
|
32191
|
-
const
|
|
32192
|
-
const
|
|
32193
|
-
|
|
32194
|
-
|
|
32195
|
-
});
|
|
32196
|
-
const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
|
|
32197
|
-
command: "gtd history",
|
|
32198
|
-
notEnded: (o) => notClosedError(hash, o)
|
|
32199
|
-
});
|
|
32200
|
-
return {
|
|
32201
|
-
tip: hash,
|
|
32202
|
-
steps: historySteps(run),
|
|
32203
|
-
ended: outcome.via,
|
|
32204
|
-
version: {
|
|
32205
|
-
recorded,
|
|
32206
|
-
running: GTD_VERSION
|
|
32207
|
-
},
|
|
32208
|
-
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
32209
|
-
};
|
|
32210
|
-
});
|
|
32211
|
-
/** Refuses on config drift before replaying, so a stale config never runs the flow. */
|
|
32212
|
-
const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
32213
|
-
const git = yield* GitService;
|
|
32214
|
-
const workspace = yield* Workspace;
|
|
32215
|
-
const host = yield* Host;
|
|
32216
|
-
const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
|
|
32217
|
-
const versionWarning = versionWarningFor(recorded);
|
|
32218
|
-
const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
|
|
32219
|
-
const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
|
|
32220
|
-
if (differing.length > 0) return yield* fail$8(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
|
|
32221
|
-
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
32222
|
-
const outcome = yield* promise(() => replayFor(setup));
|
|
32223
|
-
if (outcome.kind !== "ended") return yield* fail$8(withWarning(how.notEnded(outcome)));
|
|
32037
|
+
const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
|
|
32038
|
+
step: formatStepId(message.approved.step),
|
|
32039
|
+
name: message.approved.name,
|
|
32040
|
+
email: message.approved.email,
|
|
32041
|
+
commit: hash
|
|
32042
|
+
}];
|
|
32043
|
+
const buildRun = (history, location, closingHash) => {
|
|
32044
|
+
const allProcessCommits = history.slice(location.processStart);
|
|
32045
|
+
const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
|
|
32046
|
+
const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
|
|
32047
|
+
const parsed = allParsed.filter((m) => m.pin !== "add");
|
|
32048
|
+
const first = parsed[0];
|
|
32049
|
+
const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
|
|
32050
|
+
const head = history[history.length - 1];
|
|
32224
32051
|
return {
|
|
32225
|
-
|
|
32226
|
-
|
|
32227
|
-
|
|
32052
|
+
workflow: location.workflow,
|
|
32053
|
+
legacyOpening: location.legacyOpening,
|
|
32054
|
+
startHash: hashAt(history, location.processStart) ?? head?.hash ?? EMPTY_TREE$1,
|
|
32055
|
+
startParentHash,
|
|
32056
|
+
diffBase: first?.reviewBase ?? startParentHash,
|
|
32057
|
+
trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
|
|
32058
|
+
costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
|
|
32059
|
+
judgeVerdicts: parsed.flatMap((m) => m.judge),
|
|
32060
|
+
pinCommitted: allParsed.some((m) => m.pin === "add"),
|
|
32061
|
+
marks: parsed.flatMap((m) => m.marks),
|
|
32062
|
+
approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
|
|
32063
|
+
pinnedVars: { ...first?.vars },
|
|
32064
|
+
pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
|
|
32065
|
+
headTurn: headTurnOf(head),
|
|
32066
|
+
closingHash,
|
|
32067
|
+
episode: {
|
|
32068
|
+
base: hashAt(history, location.baseIndex),
|
|
32069
|
+
commits: history.slice(location.baseIndex + 1)
|
|
32070
|
+
}
|
|
32228
32071
|
};
|
|
32229
|
-
});
|
|
32230
|
-
//#endregion
|
|
32231
|
-
//#region src/emit/GitScript.ts
|
|
32232
|
-
const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
32233
|
-
/**
|
|
32234
|
-
* `--allow-empty` because gtd's workflow commits may be empty on purpose.
|
|
32235
|
-
* Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
|
|
32236
|
-
* "empty git commit" rejection message, discriminated from the captured
|
|
32237
|
-
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
32238
|
-
* error, a commit-msg hook) still fails.
|
|
32239
|
-
*/
|
|
32240
|
-
const commitAllowEmpty = (message) => {
|
|
32241
|
-
const m = shellQuote$1(message);
|
|
32242
|
-
return [
|
|
32243
|
-
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
32244
|
-
` case "$out" in`,
|
|
32245
|
-
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
|
|
32246
|
-
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
32247
|
-
` esac`,
|
|
32248
|
-
`fi`
|
|
32249
|
-
].join("\n");
|
|
32250
32072
|
};
|
|
32251
|
-
const
|
|
32252
|
-
|
|
32253
|
-
|
|
32254
|
-
|
|
32255
|
-
|
|
32256
|
-
|
|
32257
|
-
* hook runs and none can add to it. A HEAD that moved fails the guarded
|
|
32258
|
-
* `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
|
|
32259
|
-
*/
|
|
32260
|
-
const commitClean = (message) => [
|
|
32261
|
-
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
32262
|
-
`if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
|
|
32263
|
-
` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
|
|
32264
|
-
` exit 1`,
|
|
32265
|
-
`fi`,
|
|
32266
|
-
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
32267
|
-
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
32268
|
-
`gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
32269
|
-
`git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
|
|
32270
|
-
].join("\n");
|
|
32271
|
-
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
|
|
32272
|
-
const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
|
|
32073
|
+
const PAGE_TIERS = [
|
|
32074
|
+
32,
|
|
32075
|
+
128,
|
|
32076
|
+
512,
|
|
32077
|
+
Infinity
|
|
32078
|
+
];
|
|
32273
32079
|
/**
|
|
32274
|
-
*
|
|
32275
|
-
*
|
|
32276
|
-
*
|
|
32277
|
-
*
|
|
32278
|
-
*
|
|
32279
|
-
*
|
|
32280
|
-
*
|
|
32281
|
-
*
|
|
32282
|
-
*
|
|
32283
|
-
*
|
|
32080
|
+
* `locateEpisode`'s own boundary test, subject-only: a commit that isn't a
|
|
32081
|
+
* gtd step commit, or one landing the flow's initial state, bounds an
|
|
32082
|
+
* episode from below — EXCEPT a bare `gtd(human): <x>` subject (no `→`),
|
|
32083
|
+
* which is NEVER a boundary here, whatever `<x>` is. That bare shape is
|
|
32084
|
+
* `locateEpisode`'s own `opening` branch (a started episode's first commit),
|
|
32085
|
+
* which takes priority there over the initial-state check no matter what the
|
|
32086
|
+
* workflow's first step is named — including one named after `def.initial`
|
|
32087
|
+
* itself, where the two branches would otherwise disagree. A bare human commit can
|
|
32088
|
+
* also be a self-loop step landing rather than a real opening (this
|
|
32089
|
+
* subject-only pass can't read the `Gtd-Step` trailer that would tell them
|
|
32090
|
+
* apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
|
|
32091
|
+
* commits further back than strictly needed — never stop short of the
|
|
32092
|
+
* boundary `locateEpisode` would find once it has full commit bodies in
|
|
32093
|
+
* hand. That asymmetry is the only direction this approximation may err in;
|
|
32094
|
+
* it must never call a real boundary "not yet found".
|
|
32284
32095
|
*/
|
|
32285
|
-
const
|
|
32286
|
-
const
|
|
32287
|
-
return
|
|
32288
|
-
|
|
32289
|
-
|
|
32290
|
-
|
|
32291
|
-
|
|
32292
|
-
|
|
32293
|
-
|
|
32294
|
-
|
|
32295
|
-
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
|
|
32296
|
-
`if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
|
|
32297
|
-
` git reset -q && git update-ref -d ${keptRef}`,
|
|
32298
|
-
` exit 1`,
|
|
32299
|
-
`fi`,
|
|
32300
|
-
`git reset --hard HEAD`
|
|
32301
|
-
].join("\n");
|
|
32096
|
+
const subjectIsBoundary = (def, subject) => {
|
|
32097
|
+
const parsed = parseCommitMessage(subject).parsed;
|
|
32098
|
+
if (parsed === void 0 || !ACTORS.has(parsed.actor)) return true;
|
|
32099
|
+
if (parsed.actor === "human" && parsed.from === void 0) return false;
|
|
32100
|
+
if (parsed.actor === "check" && parsed.from === PIN_STATES.add) return false;
|
|
32101
|
+
return parsed.to === def.initial;
|
|
32102
|
+
};
|
|
32103
|
+
/** The last page index (walking newest→oldest, same direction as `locateEpisode`) whose subject is a boundary, or `undefined` if none in `page`. */
|
|
32104
|
+
const boundaryIndexIn = (def, page) => {
|
|
32105
|
+
for (let i = page.length - 1; i >= 0; i--) if (subjectIsBoundary(def, page[i].subject)) return i;
|
|
32302
32106
|
};
|
|
32303
|
-
const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
|
|
32304
|
-
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
32305
|
-
const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
|
|
32306
32107
|
/**
|
|
32307
|
-
*
|
|
32308
|
-
*
|
|
32108
|
+
* `findBoundaryBase`'s result: `base` is the hash to pass `commitHistory` so
|
|
32109
|
+
* a `git.commitHistory(base, head)` call reads exactly the episode ending at
|
|
32110
|
+
* `head`, plus whatever a subject-only read couldn't rule out — never the
|
|
32111
|
+
* whole repository. `base` is deliberately the boundary commit's PARENT, not
|
|
32112
|
+
* the boundary commit itself: `commitHistory`'s range excludes `base`, and
|
|
32113
|
+
* `runOf` needs the boundary commit present in the read history
|
|
32114
|
+
* (`episode.base`, `startParentHash`). `base: undefined` means "read from the
|
|
32115
|
+
* repository root" — an empty repository, an unresolvable `head`, or an
|
|
32116
|
+
* episode that reaches the root commit, none of which cost anything extra
|
|
32117
|
+
* since that IS the whole history in those cases.
|
|
32118
|
+
*
|
|
32119
|
+
* `head` is the CONCRETE hash the literal `head` argument resolved to on this
|
|
32120
|
+
* call's first read — `undefined` only when it never resolved (an empty
|
|
32121
|
+
* repository, or an unresolvable ref). The caller must hand this hash, not
|
|
32122
|
+
* the original `head` argument, to its own `commitHistory` call: resolving a
|
|
32123
|
+
* symbolic ref (literal `HEAD`, most callers' default) a second time risks
|
|
32124
|
+
* reading a DIFFERENT commit than this function paged through, if something
|
|
32125
|
+
* moves HEAD in between — pinning both reads to one resolved hash is what
|
|
32126
|
+
* keeps the two-read split from disagreeing with itself.
|
|
32309
32127
|
*/
|
|
32310
|
-
const
|
|
32311
|
-
|
|
32312
|
-
|
|
32313
|
-
|
|
32314
|
-
|
|
32315
|
-
|
|
32316
|
-
|
|
32317
|
-
|
|
32318
|
-
|
|
32319
|
-
|
|
32320
|
-
|
|
32321
|
-
|
|
32322
|
-
|
|
32323
|
-
|
|
32324
|
-
|
|
32325
|
-
|
|
32326
|
-
|
|
32327
|
-
|
|
32328
|
-
case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
|
|
32128
|
+
const findBoundaryBase = (git, def, head) => gen(function* () {
|
|
32129
|
+
let pinnedHead = head;
|
|
32130
|
+
for (const pageSize of PAGE_TIERS) {
|
|
32131
|
+
const page = yield* git.subjectHistory(pageSize, 0, pinnedHead);
|
|
32132
|
+
if (pinnedHead === void 0) pinnedHead = page[page.length - 1]?.hash;
|
|
32133
|
+
const wholeHistory = !Number.isFinite(pageSize) || page.length < pageSize;
|
|
32134
|
+
const boundaryIdx = boundaryIndexIn(def, page);
|
|
32135
|
+
if (boundaryIdx === void 0 || boundaryIdx === 0) {
|
|
32136
|
+
if (wholeHistory) return {
|
|
32137
|
+
base: void 0,
|
|
32138
|
+
head: pinnedHead
|
|
32139
|
+
};
|
|
32140
|
+
continue;
|
|
32141
|
+
}
|
|
32142
|
+
return {
|
|
32143
|
+
base: page[boundaryIdx - 1].hash,
|
|
32144
|
+
head: pinnedHead
|
|
32145
|
+
};
|
|
32329
32146
|
}
|
|
32147
|
+
return {
|
|
32148
|
+
base: void 0,
|
|
32149
|
+
head: pinnedHead
|
|
32150
|
+
};
|
|
32151
|
+
});
|
|
32152
|
+
const historyUpTo = (git, def, head) => flatMap(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
32153
|
+
const computeProcessRun = (git, def, head) => flatMap(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
32154
|
+
const changedSinceStart = (pinned, current, workspace) => {
|
|
32155
|
+
if (pinned === void 0) return [];
|
|
32156
|
+
return changedConfigFiles(pinned.files, current).filter((path) => {
|
|
32157
|
+
const committed = workspace.readCommittedSync(path, pinned.commit);
|
|
32158
|
+
return current[path] === void 0 || committed === void 0 || contentHash(committed) !== current[path];
|
|
32159
|
+
});
|
|
32330
32160
|
};
|
|
32331
|
-
const
|
|
32332
|
-
|
|
32333
|
-
|
|
32334
|
-
|
|
32335
|
-
|
|
32336
|
-
|
|
32161
|
+
const warnConfigChanges = (run, current, workspace) => gen(function* () {
|
|
32162
|
+
const changed = changedSinceStart(run.pinnedConfig, current, workspace);
|
|
32163
|
+
if (changed.length === 0) return;
|
|
32164
|
+
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`);
|
|
32165
|
+
});
|
|
32166
|
+
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
|
|
32167
|
+
const currentRun = gen(function* () {
|
|
32168
|
+
const git = yield* GitService;
|
|
32169
|
+
const config = yield* (yield* ConfigService).load;
|
|
32170
|
+
return yield* computeProcessRun(git, config.workflow);
|
|
32171
|
+
});
|
|
32172
|
+
const runAtTip = (git, def, tip) => gen(function* () {
|
|
32173
|
+
if (tip === void 0) {
|
|
32174
|
+
const history = yield* historyUpTo(git, def, void 0);
|
|
32175
|
+
return yield* runOf(history, locateEpisode(def, history));
|
|
32176
|
+
}
|
|
32177
|
+
if (!(parseCommitMessage(tip.subject).parsed?.to === def.initial)) {
|
|
32178
|
+
const history = yield* historyUpTo(git, def, tip.hash);
|
|
32179
|
+
return yield* runOf(history, locateEpisode(def, history));
|
|
32337
32180
|
}
|
|
32181
|
+
const parentRef = `${tip.hash}~1`;
|
|
32182
|
+
const before = yield* historyUpTo(git, def, parentRef);
|
|
32183
|
+
const location = locateEpisode(def, before);
|
|
32184
|
+
let closing = yield* git.commitHistory(parentRef, tip.hash);
|
|
32185
|
+
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
32186
|
+
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
32187
|
+
});
|
|
32188
|
+
const runAtHead = (git, def) => gen(function* () {
|
|
32189
|
+
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
|
|
32190
|
+
});
|
|
32191
|
+
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
32192
|
+
const summaryRun = gen(function* () {
|
|
32193
|
+
const git = yield* GitService;
|
|
32194
|
+
const def = (yield* (yield* ConfigService).load).workflow;
|
|
32195
|
+
return yield* runAtHead(git, def);
|
|
32196
|
+
});
|
|
32197
|
+
const commitTree = (workspace, hash) => {
|
|
32198
|
+
let entries;
|
|
32199
|
+
const list = () => entries ??= workspace.treeSync(hash);
|
|
32200
|
+
let sortedPaths;
|
|
32201
|
+
const contents = /* @__PURE__ */ new Map();
|
|
32202
|
+
return {
|
|
32203
|
+
paths: () => sortedPaths ??= [...list().keys()].sort(),
|
|
32204
|
+
read: (path) => {
|
|
32205
|
+
if (!list().has(path)) return void 0;
|
|
32206
|
+
if (!contents.has(path)) contents.set(path, workspace.readCommittedSync(path, hash));
|
|
32207
|
+
return contents.get(path);
|
|
32208
|
+
},
|
|
32209
|
+
id: (path) => list().get(path)
|
|
32210
|
+
};
|
|
32338
32211
|
};
|
|
32339
32212
|
/**
|
|
32340
|
-
*
|
|
32341
|
-
*
|
|
32342
|
-
*
|
|
32343
|
-
*
|
|
32213
|
+
* The tree a landing would commit: the working tree, with any rewrite the
|
|
32214
|
+
* landing script applies first. `entries` is `setup.pendingWorktree()` —
|
|
32215
|
+
* already memoized — never a fresh `workspace.worktreeSync()` call; see that
|
|
32216
|
+
* field's own comment for why the memo lives there and not on `Workspace`.
|
|
32344
32217
|
*/
|
|
32345
|
-
const
|
|
32346
|
-
|
|
32347
|
-
|
|
32348
|
-
|
|
32349
|
-
|
|
32350
|
-
|
|
32351
|
-
|
|
32352
|
-
|
|
32353
|
-
|
|
32354
|
-
|
|
32355
|
-
|
|
32356
|
-
|
|
32357
|
-
|
|
32358
|
-
|
|
32359
|
-
|
|
32360
|
-
|
|
32361
|
-
|
|
32218
|
+
const pendingTree = (workspace, entries, rewrite) => {
|
|
32219
|
+
const paths = [...entries.keys()];
|
|
32220
|
+
const readWorktree = (path) => {
|
|
32221
|
+
try {
|
|
32222
|
+
return workspace.readSync(path);
|
|
32223
|
+
} catch {
|
|
32224
|
+
return;
|
|
32225
|
+
}
|
|
32226
|
+
};
|
|
32227
|
+
return {
|
|
32228
|
+
paths: () => paths,
|
|
32229
|
+
read: (path) => {
|
|
32230
|
+
if (!entries.has(path)) return void 0;
|
|
32231
|
+
const content = readWorktree(path);
|
|
32232
|
+
return content === void 0 || rewrite?.path !== path ? content : rewrite.apply(content);
|
|
32233
|
+
},
|
|
32234
|
+
id: (path) => rewrite?.path === path ? void 0 : entries.get(path)
|
|
32235
|
+
};
|
|
32362
32236
|
};
|
|
32363
|
-
const ScriptSurface = { render: (steps) => {
|
|
32364
|
-
const rendered = steps.map(renderLandStep);
|
|
32365
|
-
return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
|
|
32366
|
-
} };
|
|
32367
|
-
//#endregion
|
|
32368
|
-
//#region src/emit/Emit.ts
|
|
32369
|
-
/** 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`. */
|
|
32370
|
-
const fileExistsGuard = (file) => `[ -f ${shellQuote$1(file)} ] || exit 0`;
|
|
32371
|
-
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
32372
|
-
const withFileVar$1 = (command, file) => `export GTD_FILE=${shellQuote$1(file)}\n${command}`;
|
|
32373
|
-
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
|
|
32374
|
-
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
|
|
32375
|
-
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
|
|
32376
|
-
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
32377
|
-
const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote$1(file));
|
|
32378
32237
|
/**
|
|
32379
|
-
* The
|
|
32380
|
-
*
|
|
32381
|
-
*
|
|
32382
|
-
*
|
|
32238
|
+
* The one resolver of both setting kinds. Process settings: the recorded
|
|
32239
|
+
* (pinned) value wins per name, anything unrecorded resolves live — the
|
|
32240
|
+
* fallback for a process started before pinning existed. Environment settings
|
|
32241
|
+
* are always live. `startOverrides` are `--var` values, passed only while a
|
|
32242
|
+
* process is being started.
|
|
32383
32243
|
*/
|
|
32384
|
-
const
|
|
32385
|
-
|
|
32386
|
-
|
|
32387
|
-
|
|
32388
|
-
|
|
32244
|
+
const settingsFor = (workflow, pinnedVars, hostEnv, startOverrides = {}, startFlags) => ({
|
|
32245
|
+
vars: {
|
|
32246
|
+
...resolveStartVars(workflow.vars, startFlags, startOverrides, hostEnv),
|
|
32247
|
+
...pinnedVars
|
|
32248
|
+
},
|
|
32249
|
+
env: resolveVars(workflow.env, {}, hostEnv)
|
|
32250
|
+
});
|
|
32251
|
+
/** The workflow a run replays on: its pinned one, else the default. */
|
|
32252
|
+
const workflowOf = (config, run) => {
|
|
32253
|
+
if (run.legacyOpening) return fail$8(/* @__PURE__ */ new Error("gtd: this process was opened by `gtd --entry`, which no longer exists — run `gtd abandon` to start over"));
|
|
32254
|
+
if (run.workflow === void 0) return succeed$1(config.defaultWorkflow);
|
|
32255
|
+
const named = config.workflowNamed(run.workflow);
|
|
32256
|
+
if (named !== void 0) return succeed$1(named);
|
|
32257
|
+
return fail$8(/* @__PURE__ */ new Error(`gtd: this process runs workflow "${run.workflow}", which gtd.config.ts no longer defines — run \`gtd abandon\` to start over`));
|
|
32389
32258
|
};
|
|
32390
32259
|
/**
|
|
32391
|
-
*
|
|
32392
|
-
*
|
|
32393
|
-
*
|
|
32260
|
+
* `judgeBudgetBytes` is the one var that refuses rather than disabling its
|
|
32261
|
+
* mechanism when blanked: without a bound the judge payload is rejected
|
|
32262
|
+
* anyway. Absent falls back to a conservative default.
|
|
32394
32263
|
*/
|
|
32395
|
-
const
|
|
32396
|
-
|
|
32397
|
-
|
|
32398
|
-
|
|
32399
|
-
|
|
32400
|
-
|
|
32401
|
-
|
|
32402
|
-
|
|
32264
|
+
const DEFAULT_JUDGE_BUDGET_BYTES = 32768;
|
|
32265
|
+
const judgeBudgetBytes = (vars) => {
|
|
32266
|
+
const raw = vars.judgeBudgetBytes;
|
|
32267
|
+
if (raw === void 0) return DEFAULT_JUDGE_BUDGET_BYTES;
|
|
32268
|
+
const n = Number(raw);
|
|
32269
|
+
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)`);
|
|
32270
|
+
return n;
|
|
32271
|
+
};
|
|
32272
|
+
const replayFor = (setup, pending) => replay({
|
|
32273
|
+
flow: setup.def.flow,
|
|
32274
|
+
episode: {
|
|
32275
|
+
base: setup.base,
|
|
32276
|
+
commits: setup.episode
|
|
32277
|
+
},
|
|
32278
|
+
vars: setup.vars,
|
|
32279
|
+
env: setup.env,
|
|
32280
|
+
start: setup.run.diffBase,
|
|
32281
|
+
startTree: setup.run.diffBase === EMPTY_TREE$1 ? treeFromRecord({}) : commitTree(setup.workspace, setup.run.diffBase),
|
|
32282
|
+
budgetBytes: setup.budget,
|
|
32283
|
+
skills: setup.skills,
|
|
32284
|
+
configuredSkills: setup.def.configuredSkills,
|
|
32285
|
+
access: setup.access,
|
|
32286
|
+
configuredAccess: setup.def.configuredAccess,
|
|
32287
|
+
...pending !== void 0 ? { pending } : {}
|
|
32288
|
+
});
|
|
32289
|
+
const replayError = (outcome) => {
|
|
32290
|
+
if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
|
|
32291
|
+
if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
|
|
32292
|
+
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");
|
|
32403
32293
|
};
|
|
32294
|
+
const judgeDocument = (step) => step.request.kind === "judge" ? judgeJson(step.request.evidence, step.request.questions) : void 0;
|
|
32404
32295
|
/**
|
|
32405
|
-
*
|
|
32406
|
-
*
|
|
32407
|
-
*
|
|
32296
|
+
* The fixed sentence appended to a judge gate's message when a bounded read
|
|
32297
|
+
* dropped bytes to fit `judgeBudgetBytes` — a constant, so no workflow can
|
|
32298
|
+
* reword or drop it.
|
|
32408
32299
|
*/
|
|
32409
|
-
const
|
|
32410
|
-
|
|
32411
|
-
|
|
32412
|
-
|
|
32413
|
-
|
|
32414
|
-
|
|
32415
|
-
|
|
32416
|
-
|
|
32417
|
-
|
|
32418
|
-
|
|
32419
|
-
|
|
32300
|
+
const TRUNCATION_NOTICE = "Note: some evidence above was truncated to fit the judge's payload budget.";
|
|
32301
|
+
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.";
|
|
32302
|
+
const optional = (key, value) => value === void 0 ? {} : { [key]: value };
|
|
32303
|
+
const commonOf = (step) => {
|
|
32304
|
+
const options = step.request.options;
|
|
32305
|
+
return {
|
|
32306
|
+
actor: step.actor,
|
|
32307
|
+
...optional("label", options.label),
|
|
32308
|
+
...optional("file", options.file),
|
|
32309
|
+
...optional("mode", options.mode)
|
|
32310
|
+
};
|
|
32420
32311
|
};
|
|
32421
|
-
const
|
|
32422
|
-
|
|
32423
|
-
|
|
32312
|
+
const promptDef = (common, request, skills, access) => ({
|
|
32313
|
+
...common,
|
|
32314
|
+
kind: "prompt",
|
|
32315
|
+
content: request.prompt,
|
|
32316
|
+
...optional("model", request.options.model),
|
|
32317
|
+
...optional("system", request.options.system),
|
|
32318
|
+
...optional("allowEmpty", request.options.allowEmpty),
|
|
32319
|
+
...optional("keep", request.options.keep),
|
|
32320
|
+
...optional("skills", skills),
|
|
32321
|
+
...optional("access", access)
|
|
32322
|
+
});
|
|
32323
|
+
const scriptDef = (common, request) => ({
|
|
32324
|
+
...common,
|
|
32325
|
+
kind: "script",
|
|
32326
|
+
content: request.body
|
|
32327
|
+
});
|
|
32328
|
+
const judgeDef = (common, request, step) => {
|
|
32329
|
+
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
32330
|
+
return {
|
|
32331
|
+
...common,
|
|
32332
|
+
kind: "message",
|
|
32333
|
+
content: step.truncated ? `${message}\n\n${TRUNCATION_NOTICE}` : message,
|
|
32334
|
+
...optional("judge", judgeDocument(step))
|
|
32335
|
+
};
|
|
32424
32336
|
};
|
|
32425
|
-
const
|
|
32426
|
-
|
|
32427
|
-
|
|
32428
|
-
|
|
32429
|
-
optional
|
|
32337
|
+
const messageDef = (common, request, step) => ({
|
|
32338
|
+
...common,
|
|
32339
|
+
kind: "message",
|
|
32340
|
+
content: request.options.message ?? request.options.label ?? step.name,
|
|
32341
|
+
...optional("acceptClean", request.options.approve === true || request.options.acceptClean),
|
|
32342
|
+
...optional("approve", request.options.approve)
|
|
32430
32343
|
});
|
|
32431
|
-
|
|
32432
|
-
const
|
|
32433
|
-
const
|
|
32434
|
-
|
|
32435
|
-
|
|
32436
|
-
|
|
32437
|
-
|
|
32438
|
-
|
|
32439
|
-
|
|
32440
|
-
*/
|
|
32441
|
-
const combinedScript = (required, optional) => {
|
|
32442
|
-
if (required.length === 0) return "";
|
|
32443
|
-
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
32444
|
-
return [
|
|
32445
|
-
DID_NOT_RUN_COMMENT,
|
|
32446
|
-
"",
|
|
32447
|
-
required,
|
|
32448
|
-
"",
|
|
32449
|
-
PRESENTATION_ONLY_COMMENT,
|
|
32450
|
-
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
32451
|
-
].join("\n");
|
|
32344
|
+
const stepDefOf = (step) => {
|
|
32345
|
+
const request = step.request;
|
|
32346
|
+
const common = commonOf(step);
|
|
32347
|
+
switch (request.kind) {
|
|
32348
|
+
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
32349
|
+
case "run": return scriptDef(common, request);
|
|
32350
|
+
case "judge": return judgeDef(common, request, step);
|
|
32351
|
+
case "human": return messageDef(common, request, step);
|
|
32352
|
+
}
|
|
32452
32353
|
};
|
|
32453
|
-
|
|
32454
|
-
//#region src/emit/OutcomeScript.ts
|
|
32455
|
-
/**
|
|
32456
|
-
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
32457
|
-
* tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
|
|
32458
|
-
* re-quoted here: a value reaches the script as a printf ARGUMENT, never
|
|
32459
|
-
* interpolated into the format itself, so a subject or state name containing
|
|
32460
|
-
* `%` can never be read as a conversion spec. Markers like `->` are arguments
|
|
32461
|
-
* too, keeping every format string free of a leading `-` that a shell's
|
|
32462
|
-
* `printf` could mistake for an option. A real newline in `fmt` is emitted as
|
|
32463
|
-
* the two-character `\n` escape `printf` itself expands, keeping every emitted
|
|
32464
|
-
* statement on one line.
|
|
32465
|
-
*/
|
|
32466
|
-
const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
32467
|
-
/**
|
|
32468
|
-
* Marks an emitted block as print-only. Load-bearing beyond documentation:
|
|
32469
|
-
* `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
|
|
32470
|
-
* this line, and a bare `printf` would be ambiguous with a workflow
|
|
32471
|
-
* `script:` command that happens to start the same way.
|
|
32472
|
-
*/
|
|
32473
|
-
const OUTCOME_MARKER = "# gtd: outcome (print-only)";
|
|
32474
|
-
const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
|
|
32475
|
-
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";
|
|
32476
|
-
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";
|
|
32477
|
-
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
32478
|
-
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
32479
|
-
/** One already-rendered plain line. */
|
|
32480
|
-
const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
|
|
32481
|
-
/**
|
|
32482
|
-
* Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
|
|
32483
|
-
* — this line runs AFTER the reset that made `head` the new HEAD, so neither
|
|
32484
|
-
* value is knowable when gtd generates it.
|
|
32485
|
-
*/
|
|
32486
|
-
const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
|
|
32487
|
-
shellQuote$1(from),
|
|
32488
|
-
`"$(git rev-parse --short ${shellQuote$1(head)})"`,
|
|
32489
|
-
`"$(git log -1 --format=%s ${shellQuote$1(head)})"`,
|
|
32490
|
-
shellQuote$1(state)
|
|
32491
|
-
]);
|
|
32492
|
-
/** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
|
|
32493
|
-
const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
|
|
32494
|
-
/** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
|
|
32495
|
-
const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
|
|
32496
|
-
`"$(git rev-parse --short ${shellQuote$1(to)})"`,
|
|
32497
|
-
`"$(git log -1 --format=%s ${shellQuote$1(to)})"`,
|
|
32498
|
-
shellQuote$1(state)
|
|
32499
|
-
]);
|
|
32500
|
-
//#endregion
|
|
32501
|
-
//#region src/emit/ModeContradiction.ts
|
|
32354
|
+
const ROOT_MEMORY_SCOPE_NAME = "root";
|
|
32502
32355
|
/**
|
|
32503
|
-
*
|
|
32504
|
-
*
|
|
32505
|
-
*
|
|
32506
|
-
*
|
|
32507
|
-
* `format:` command, to fix `gtd.config.ts` without reproducing the failure).
|
|
32356
|
+
* An agent rest's memory key, `<scope>#<hash7>`: the hash is the commit the
|
|
32357
|
+
* current unbroken run into the scope started FROM, so the same run always
|
|
32358
|
+
* re-derives the same key. `resumed` is whether an agent turn in that run
|
|
32359
|
+
* already landed.
|
|
32508
32360
|
*/
|
|
32509
|
-
const
|
|
32361
|
+
const memoryOf = (trace, run) => {
|
|
32362
|
+
const rest = trace[trace.length - 1];
|
|
32363
|
+
if (rest === void 0 || rest.kind !== "agent") return {
|
|
32364
|
+
key: void 0,
|
|
32365
|
+
resumed: false
|
|
32366
|
+
};
|
|
32367
|
+
const start = scopeRunStart(trace, rest.memoryScope);
|
|
32368
|
+
const token = start <= 0 ? run.startParentHash : trace[start - 1].enteredAt;
|
|
32369
|
+
const resumed = trace.slice(Math.max(start, 0), trace.length - 1).some((s) => s.kind === "agent" && s.memoryScope === rest.memoryScope);
|
|
32370
|
+
return {
|
|
32371
|
+
key: `${rest.memoryScope || ROOT_MEMORY_SCOPE_NAME}#${token.slice(0, 7)}`,
|
|
32372
|
+
resumed
|
|
32373
|
+
};
|
|
32374
|
+
};
|
|
32375
|
+
const hintsOf = (def) => ({
|
|
32376
|
+
...optional("model", def.model),
|
|
32377
|
+
...optional("label", def.label),
|
|
32378
|
+
...optional("file", def.file),
|
|
32379
|
+
...optional("judge", def.judge),
|
|
32380
|
+
...optional("system", def.system),
|
|
32381
|
+
...optional("mode", def.mode),
|
|
32382
|
+
...optional("skills", def.skills),
|
|
32383
|
+
...optional("access", def.access)
|
|
32384
|
+
});
|
|
32385
|
+
const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
|
|
32386
|
+
/** Per-model token totals, highest-cost first (ties broken by model name). */
|
|
32387
|
+
const costByModel = (entries) => {
|
|
32388
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
32389
|
+
for (const entry of entries) byModel.set(entry.model, (byModel.get(entry.model) ?? 0) + entry.cost);
|
|
32390
|
+
return [...byModel.entries()].map(([model, cost]) => ({
|
|
32391
|
+
model,
|
|
32392
|
+
cost
|
|
32393
|
+
})).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
32394
|
+
};
|
|
32395
|
+
const costTotals = (entries) => ({
|
|
32396
|
+
total: entries.reduce((sum, entry) => sum + entry.cost, 0),
|
|
32397
|
+
byModel: costByModel(entries)
|
|
32398
|
+
});
|
|
32399
|
+
const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(function* () {
|
|
32400
|
+
const resolved = yield* workflowOf(config, run);
|
|
32401
|
+
const def = resolved.def;
|
|
32402
|
+
const startFlags = run.trace.length === 0 ? yield* StartFlags : {};
|
|
32403
|
+
const { vars, env } = settingsFor(resolved, run.pinnedVars, host.env, startOverrides, startFlags);
|
|
32404
|
+
const skills = yield* resolveScopeSkills(def, vars);
|
|
32405
|
+
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
32406
|
+
const budget = yield* try_({
|
|
32407
|
+
try: () => judgeBudgetBytes(vars),
|
|
32408
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
32409
|
+
});
|
|
32410
|
+
yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
|
|
32411
|
+
const base = run.episode.base === void 0 ? {
|
|
32412
|
+
hash: EMPTY_TREE$1,
|
|
32413
|
+
tree: treeFromRecord({})
|
|
32414
|
+
} : {
|
|
32415
|
+
hash: run.episode.base,
|
|
32416
|
+
tree: commitTree(workspace, run.episode.base)
|
|
32417
|
+
};
|
|
32418
|
+
const git = yield* GitService;
|
|
32419
|
+
const episode = [];
|
|
32420
|
+
for (const c of run.episode.commits) {
|
|
32421
|
+
const tip = parseCommitMessage(c.message).kept;
|
|
32422
|
+
const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
|
|
32423
|
+
episode.push({
|
|
32424
|
+
hash: c.hash,
|
|
32425
|
+
message: c.message,
|
|
32426
|
+
tree: commitTree(workspace, c.hash),
|
|
32427
|
+
...exists ? { kept: {
|
|
32428
|
+
hash: tip,
|
|
32429
|
+
tree: commitTree(workspace, tip)
|
|
32430
|
+
} } : {}
|
|
32431
|
+
});
|
|
32432
|
+
}
|
|
32433
|
+
let pendingMemo;
|
|
32434
|
+
return {
|
|
32435
|
+
resolved,
|
|
32436
|
+
def,
|
|
32437
|
+
vars,
|
|
32438
|
+
env,
|
|
32439
|
+
setup: {
|
|
32440
|
+
def,
|
|
32441
|
+
run,
|
|
32442
|
+
vars,
|
|
32443
|
+
env,
|
|
32444
|
+
budget,
|
|
32445
|
+
skills,
|
|
32446
|
+
access,
|
|
32447
|
+
workspace,
|
|
32448
|
+
base,
|
|
32449
|
+
episode,
|
|
32450
|
+
pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
|
|
32451
|
+
}
|
|
32452
|
+
};
|
|
32453
|
+
});
|
|
32454
|
+
const pendingChanges = (git, withIndex) => gen(function* () {
|
|
32455
|
+
const worktree = yield* git.changedPaths();
|
|
32456
|
+
if (!withIndex) return worktree;
|
|
32457
|
+
const seen = new Set(worktree.map((e) => e.path));
|
|
32458
|
+
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
|
|
32459
|
+
});
|
|
32460
|
+
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
32461
|
+
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
32462
|
+
const git = yield* GitService;
|
|
32463
|
+
const config = yield* (yield* ConfigService).load;
|
|
32464
|
+
const workspace = yield* Workspace;
|
|
32465
|
+
const host = yield* Host;
|
|
32466
|
+
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
32467
|
+
const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
|
|
32468
|
+
const outcome = yield* promise(() => replayFor(setup));
|
|
32469
|
+
const pinned = headHasPin(workspace, run);
|
|
32470
|
+
const pin = pinBeatAt(outcome, run, def, pinned);
|
|
32471
|
+
const resting = restingOn(outcome, pin);
|
|
32472
|
+
if (resting === void 0) return yield* fail$8(replayError(outcome) ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
|
|
32473
|
+
const { reached, step } = resting;
|
|
32474
|
+
const { stepDef, state, memory } = restFacts(pin, step, reached.trace, run);
|
|
32475
|
+
yield* (yield* Narrator).narrate(`rest resolved: ${state} (awaits ${stepDef.actor})`);
|
|
32476
|
+
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$8(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
32477
|
+
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
32478
|
+
status: normalizeStatus(e.status),
|
|
32479
|
+
path: e.path
|
|
32480
|
+
})) : [];
|
|
32481
|
+
const configNow = config.fingerprint;
|
|
32482
|
+
if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
|
|
32483
|
+
return {
|
|
32484
|
+
def,
|
|
32485
|
+
workflowName: resolved.name,
|
|
32486
|
+
state,
|
|
32487
|
+
stepDef,
|
|
32488
|
+
actor: stepDef.actor,
|
|
32489
|
+
pin,
|
|
32490
|
+
pinned,
|
|
32491
|
+
step,
|
|
32492
|
+
trace: reached.trace,
|
|
32493
|
+
run,
|
|
32494
|
+
vars,
|
|
32495
|
+
env,
|
|
32496
|
+
config: configNow,
|
|
32497
|
+
changes,
|
|
32498
|
+
memory: memory.key,
|
|
32499
|
+
memoryResumed: memory.resumed,
|
|
32500
|
+
hints: hintsOf(stepDef),
|
|
32501
|
+
cost: costTotals(run.costEntries),
|
|
32502
|
+
setup
|
|
32503
|
+
};
|
|
32504
|
+
});
|
|
32505
|
+
const currentRest = restAt(void 0);
|
|
32506
|
+
/** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
|
|
32507
|
+
const restingOn = (outcome, pin) => {
|
|
32508
|
+
if (outcome.kind === "rest") return {
|
|
32509
|
+
reached: outcome,
|
|
32510
|
+
step: outcome.rest
|
|
32511
|
+
};
|
|
32512
|
+
if (outcome.kind !== "ended" || pin !== "remove") return void 0;
|
|
32513
|
+
const step = outcome.trace.at(-1);
|
|
32514
|
+
return step === void 0 ? void 0 : {
|
|
32515
|
+
reached: outcome,
|
|
32516
|
+
step
|
|
32517
|
+
};
|
|
32518
|
+
};
|
|
32519
|
+
/** A pin or unpin beat has its own script, no step's, and no agent memory. */
|
|
32520
|
+
const restFacts = (pin, step, trace, run) => pin === void 0 ? {
|
|
32521
|
+
stepDef: stepDefOf(step),
|
|
32522
|
+
state: step.name,
|
|
32523
|
+
memory: memoryOf(trace, run)
|
|
32524
|
+
} : {
|
|
32525
|
+
stepDef: pinStepDef(pin),
|
|
32526
|
+
state: PIN_STATES[pin],
|
|
32527
|
+
memory: {
|
|
32528
|
+
key: void 0,
|
|
32529
|
+
resumed: false
|
|
32530
|
+
}
|
|
32531
|
+
};
|
|
32532
|
+
const headHasPin = (workspace, run) => {
|
|
32533
|
+
const head = run.episode.commits.at(-1)?.hash ?? run.episode.base;
|
|
32534
|
+
return head !== void 0 && workspace.treeSync(head).has(".gtd/process.gtdpin");
|
|
32535
|
+
};
|
|
32536
|
+
const underwayAt = (run, step, def) => run.workflow !== void 0 || step.name !== def.initial || step.id.occurrence !== 1;
|
|
32510
32537
|
/**
|
|
32511
|
-
*
|
|
32512
|
-
*
|
|
32513
|
-
* mode whose `format:` contradicts its own `validate:` without ever touching
|
|
32514
|
-
* the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
|
|
32515
|
-
* heredoc, since no delimiter chosen ahead of time can collide with the
|
|
32516
|
-
* sample's own bytes. One bash fragment with no blank lines (so
|
|
32517
|
-
* `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
|
|
32518
|
-
* to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
|
|
32519
|
-
* still fires at a first-write beat where the real steering file doesn't
|
|
32520
|
-
* exist yet.
|
|
32538
|
+
* A started process pins first — again, should a turn have deleted the pin;
|
|
32539
|
+
* a pinned flow that ended unpins before the closing commit.
|
|
32521
32540
|
*/
|
|
32522
|
-
const
|
|
32523
|
-
|
|
32524
|
-
|
|
32525
|
-
|
|
32526
|
-
return [
|
|
32527
|
-
`printf '%s' ${shellQuote$1(sample)} > ${pathQ}`,
|
|
32528
|
-
formatCommand,
|
|
32529
|
-
`gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
|
|
32530
|
-
` printf '%s\\n' ${messageQ} >&2`,
|
|
32531
|
-
` cat ${pathQ} >&2`,
|
|
32532
|
-
` rm -f ${pathQ}`,
|
|
32533
|
-
` exit 1`,
|
|
32534
|
-
`}`,
|
|
32535
|
-
`rm -f ${pathQ}`
|
|
32536
|
-
].join("\n");
|
|
32541
|
+
const pinBeatAt = (outcome, run, def, pinned) => {
|
|
32542
|
+
if (flowEnded(outcome, run, def)) return pinned || run.pinCommitted ? "remove" : void 0;
|
|
32543
|
+
if (outcome.kind !== "rest" || pinned) return void 0;
|
|
32544
|
+
return underwayAt(run, outcome.rest, def) ? "add" : void 0;
|
|
32537
32545
|
};
|
|
32546
|
+
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);
|
|
32547
|
+
const pinStepDef = (pin) => ({
|
|
32548
|
+
actor: "check",
|
|
32549
|
+
kind: "script",
|
|
32550
|
+
content: pin === "add" ? pinScript(PIN_PATH) : unpinScript(PIN_PATH)
|
|
32551
|
+
});
|
|
32538
32552
|
/**
|
|
32539
|
-
*
|
|
32540
|
-
*
|
|
32541
|
-
*
|
|
32542
|
-
* as a clean bill of health.
|
|
32553
|
+
* Whether `gtd start <name>` can start a process here: the workflow must
|
|
32554
|
+
* reach a step from the tree the opening commit would capture. That step is
|
|
32555
|
+
* what the opening commit's subject names.
|
|
32543
32556
|
*/
|
|
32544
|
-
const
|
|
32545
|
-
|
|
32546
|
-
};
|
|
32547
|
-
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
|
|
32557
|
+
const startRefusal = (rest, resolved, startVars) => gen(function* () {
|
|
32558
|
+
const host = yield* Host;
|
|
32559
|
+
const { vars, env } = settingsFor(resolved, {}, host.env, startVars, yield* StartFlags);
|
|
32560
|
+
const skills = yield* resolveScopeSkills(resolved.def, vars);
|
|
32561
|
+
const access = yield* resolveScopeAccess(resolved.def, skills, vars);
|
|
32562
|
+
const workspace = rest.setup.workspace;
|
|
32563
|
+
const outcome = yield* promise(() => replay({
|
|
32564
|
+
flow: resolved.def.flow,
|
|
32565
|
+
episode: {
|
|
32566
|
+
base: {
|
|
32567
|
+
hash: "",
|
|
32568
|
+
tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
|
|
32569
|
+
},
|
|
32570
|
+
commits: []
|
|
32571
|
+
},
|
|
32572
|
+
vars,
|
|
32573
|
+
env,
|
|
32574
|
+
start: "",
|
|
32575
|
+
budgetBytes: rest.setup.budget,
|
|
32576
|
+
skills,
|
|
32577
|
+
configuredSkills: resolved.def.configuredSkills,
|
|
32578
|
+
access,
|
|
32579
|
+
configuredAccess: resolved.def.configuredAccess
|
|
32580
|
+
}));
|
|
32581
|
+
if (outcome.kind === "rest") return { firstStep: outcome.rest.name };
|
|
32582
|
+
if (outcome.kind === "refused") return { refusal: outcome.message };
|
|
32583
|
+
const why = outcome.kind === "ended" ? "the flow reaches no step" : outcome.message;
|
|
32584
|
+
return { refusal: `workflow "${resolved.name}" cannot start — ${why}` };
|
|
32585
|
+
});
|
|
32586
|
+
/** The review window's diff base at the rest. */
|
|
32587
|
+
const reviewBaseFor = (rest) => rest.step.request.options.base ?? rest.run.diffBase;
|
|
32588
|
+
const renderRest = (rest) => succeed$1({
|
|
32589
|
+
state: rest.state,
|
|
32590
|
+
actor: rest.actor,
|
|
32591
|
+
kind: rest.stepDef.kind,
|
|
32592
|
+
content: rest.stepDef.content,
|
|
32593
|
+
...rest.hints,
|
|
32594
|
+
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
32595
|
+
memoryResumed: rest.memoryResumed,
|
|
32596
|
+
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
32597
|
+
});
|
|
32551
32598
|
/**
|
|
32552
|
-
*
|
|
32553
|
-
*
|
|
32554
|
-
* that is exactly the format's own seeded string; `externalValidate` for any
|
|
32555
|
-
* other command. Each arm returns directly — a flat sequence, not nested
|
|
32556
|
-
* ternaries — so the four outcomes stay each their own case to read, not a
|
|
32557
|
-
* boolean expression to re-derive.
|
|
32599
|
+
* Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
|
|
32600
|
+
* attempt at this very agent rest, and another dispatch would repeat it.
|
|
32558
32601
|
*/
|
|
32559
|
-
const
|
|
32560
|
-
|
|
32561
|
-
|
|
32562
|
-
|
|
32563
|
-
|
|
32602
|
+
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;
|
|
32603
|
+
/** 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. */
|
|
32604
|
+
const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
|
|
32605
|
+
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
32606
|
+
const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
|
|
32607
|
+
const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
|
|
32608
|
+
const approvalRefusal = (rest, approver) => {
|
|
32609
|
+
if (rest.stepDef.approve !== true) return void 0;
|
|
32610
|
+
const id = rest.step.id;
|
|
32611
|
+
if (rest.changes.length > 0) return {
|
|
32612
|
+
kind: "refusal",
|
|
32613
|
+
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\``
|
|
32564
32614
|
};
|
|
32565
|
-
if (
|
|
32566
|
-
|
|
32567
|
-
|
|
32568
|
-
|
|
32615
|
+
if (isNone(approver)) return {
|
|
32616
|
+
kind: "refusal",
|
|
32617
|
+
message: `gtd land: approval-gate: ${formatStepId(id)} records who approved it, but git has no author identity — set user.name and user.email`
|
|
32618
|
+
};
|
|
32619
|
+
if (!isRecordableApproval({
|
|
32620
|
+
step: id,
|
|
32621
|
+
...approver.value
|
|
32622
|
+
})) return {
|
|
32623
|
+
kind: "refusal",
|
|
32624
|
+
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)`
|
|
32569
32625
|
};
|
|
32626
|
+
};
|
|
32627
|
+
/** The landing a clean tree decides before any replay, if it decides one. */
|
|
32628
|
+
const cleanLanding = (rest) => {
|
|
32629
|
+
if (rest.changes.length > 0) return void 0;
|
|
32630
|
+
const def = rest.stepDef;
|
|
32631
|
+
if (def.kind === "prompt" && def.allowEmpty !== true) return {
|
|
32632
|
+
kind: "attempt",
|
|
32633
|
+
subject: formatSubject(rest.actor, rest.state)
|
|
32634
|
+
};
|
|
32635
|
+
if (rest.actor === "human" && def.acceptClean !== true) return {
|
|
32636
|
+
kind: "noop",
|
|
32637
|
+
settled: false
|
|
32638
|
+
};
|
|
32639
|
+
};
|
|
32640
|
+
/** At the human review gate the pending tree lands with every tick cleared, as the landing script will. */
|
|
32641
|
+
const reviewGateRewrite = (def) => {
|
|
32642
|
+
const reviewFormat = steeringFormatFor("review");
|
|
32643
|
+
if (!isHumanReviewGate(def) || def.file === void 0 || reviewFormat === void 0) return;
|
|
32570
32644
|
return {
|
|
32571
|
-
|
|
32572
|
-
|
|
32645
|
+
path: def.file,
|
|
32646
|
+
apply: (content) => reviewFormat.clearTicks(content)
|
|
32647
|
+
};
|
|
32648
|
+
};
|
|
32649
|
+
const multilineRefusal = (vars) => {
|
|
32650
|
+
const multiline = multilineSetting(vars);
|
|
32651
|
+
return multiline === void 0 ? void 0 : {
|
|
32652
|
+
kind: "refusal",
|
|
32653
|
+
message: `gtd: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`
|
|
32573
32654
|
};
|
|
32574
32655
|
};
|
|
32656
|
+
const landingTarget = (outcome, rest) => {
|
|
32657
|
+
if (outcome.kind === "refused") return succeed$1({
|
|
32658
|
+
kind: "refusal",
|
|
32659
|
+
message: outcome.message
|
|
32660
|
+
});
|
|
32661
|
+
if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$8(new Error(outcome.message));
|
|
32662
|
+
if (outcome.kind !== "rest") return succeed$1(rest.def.initial);
|
|
32663
|
+
const reserved = initialStepRefusal(rest.run.workflow, outcome.rest.name, rest.def.initial);
|
|
32664
|
+
return succeed$1(reserved === void 0 ? outcome.rest.name : reserved);
|
|
32665
|
+
};
|
|
32575
32666
|
/**
|
|
32576
|
-
*
|
|
32577
|
-
*
|
|
32578
|
-
*
|
|
32579
|
-
* parser when the name is registered. `format` is set from the registry
|
|
32580
|
-
* ALONE, independent of which half of `validate` wins — a declared validator
|
|
32581
|
-
* overrides validation, not the format identity.
|
|
32582
|
-
*
|
|
32583
|
-
* `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
|
|
32584
|
-
* against the registry alone, with no workflow definition in hand (e.g. the
|
|
32585
|
-
* basename dispatch for a `REVIEW.md` no config maps).
|
|
32586
|
-
*
|
|
32587
|
-
* The `"unknown"` arm carries the whole refusal message, naming what IS known
|
|
32588
|
-
* when `def` is given — this is the one place that message is built, so it
|
|
32589
|
-
* can never drift from what actually resolved.
|
|
32667
|
+
* A step commit entering the default's first step is what ends an episode, so
|
|
32668
|
+
* a named workflow landing there would read back as finished. Its opening
|
|
32669
|
+
* commit may name that step: a bare opening is never read as a boundary.
|
|
32590
32670
|
*/
|
|
32591
|
-
const
|
|
32592
|
-
|
|
32593
|
-
|
|
32594
|
-
|
|
32595
|
-
|
|
32596
|
-
|
|
32671
|
+
const initialStepRefusal = (workflow, step, initial) => workflow !== void 0 && step === initial ? {
|
|
32672
|
+
kind: "refusal",
|
|
32673
|
+
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`
|
|
32674
|
+
} : void 0;
|
|
32675
|
+
const closingTrailers = (branch) => ({
|
|
32676
|
+
version: GTD_VERSION,
|
|
32677
|
+
...isSome(branch) ? { branch: branch.value } : {}
|
|
32678
|
+
});
|
|
32679
|
+
const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
|
|
32680
|
+
const keptTurn = (rest) => rest.stepDef.keep === true && rest.changes.length > 0 ? { keepTurn: keptRef(rest.run.startHash, rest.state) } : {};
|
|
32681
|
+
const approvedTrailer = (rest, approver) => isSome(approver) ? { approved: {
|
|
32682
|
+
step: rest.step.id,
|
|
32683
|
+
...approver.value
|
|
32684
|
+
} } : {};
|
|
32685
|
+
const landingTo = (rest, to, branch, approver, marks) => {
|
|
32686
|
+
if (isSettledNoop(rest, to)) return {
|
|
32687
|
+
kind: "noop",
|
|
32688
|
+
settled: true
|
|
32689
|
+
};
|
|
32690
|
+
const underway = !noProcessUnderway(rest);
|
|
32691
|
+
const starts = !underway && to !== rest.def.initial;
|
|
32692
|
+
const refused = starts ? multilineRefusal(rest.vars) : void 0;
|
|
32693
|
+
if (refused !== void 0) return refused;
|
|
32694
|
+
const { finishes, landsAt } = endingOf(rest, to, underway);
|
|
32695
|
+
return {
|
|
32696
|
+
kind: "commit",
|
|
32697
|
+
to: landsAt,
|
|
32698
|
+
spec: {
|
|
32699
|
+
actor: rest.actor,
|
|
32700
|
+
from: rest.state,
|
|
32701
|
+
to: landsAt,
|
|
32702
|
+
step: rest.step.id,
|
|
32703
|
+
...marks.length > 0 && { marks },
|
|
32704
|
+
...approvedTrailer(rest, approver),
|
|
32705
|
+
...startTrailers(rest, starts),
|
|
32706
|
+
...finishes ? closingTrailers(branch) : {}
|
|
32707
|
+
},
|
|
32708
|
+
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {},
|
|
32709
|
+
...keptTurn(rest)
|
|
32597
32710
|
};
|
|
32598
|
-
|
|
32599
|
-
|
|
32600
|
-
|
|
32601
|
-
} : format !== void 0 ? {
|
|
32602
|
-
kind: "builtin",
|
|
32603
|
-
format
|
|
32604
|
-
} : void 0;
|
|
32711
|
+
};
|
|
32712
|
+
const endingOf = (rest, to, underway) => {
|
|
32713
|
+
const ends = underway && to === rest.def.initial;
|
|
32605
32714
|
return {
|
|
32606
|
-
|
|
32607
|
-
|
|
32608
|
-
...format !== void 0 ? { format } : {},
|
|
32609
|
-
...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
|
|
32610
|
-
...validate !== void 0 ? { validate } : {},
|
|
32611
|
-
capabilities: capabilitiesFor(mode, format, validate)
|
|
32715
|
+
finishes: ends && !rest.pinned,
|
|
32716
|
+
landsAt: ends && rest.pinned ? PIN_STATES.remove : to
|
|
32612
32717
|
};
|
|
32613
32718
|
};
|
|
32614
|
-
/**
|
|
32615
|
-
const
|
|
32616
|
-
|
|
32617
|
-
|
|
32618
|
-
* is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
|
|
32619
|
-
* the script on its own `exit 127`, without touching `failurePromptWrapper`'s
|
|
32620
|
-
* captured-output path. Each command guards on its OWN leading word — a
|
|
32621
|
-
* guard naming the wrong binary is worse than none.
|
|
32622
|
-
*/
|
|
32623
|
-
const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
|
|
32624
|
-
const binary = extractLeadingBinary(command);
|
|
32625
|
-
if (binary !== void 0) steps.push({
|
|
32626
|
-
kind: "command",
|
|
32627
|
-
command: binaryGuard(binary, mode, key)
|
|
32628
|
-
});
|
|
32629
|
-
steps.push({
|
|
32630
|
-
kind: "command",
|
|
32631
|
-
command: withFileVar$1(command, file),
|
|
32632
|
-
...onFailure !== void 0 ? { onFailure } : {}
|
|
32633
|
-
});
|
|
32719
|
+
/** A pin or unpin commit carries the pin alone; the unpin commit closes the process. */
|
|
32720
|
+
const pinLanding = (rest, pin, branch) => {
|
|
32721
|
+
const change = rest.changes.find((c) => c.path === PIN_PATH)?.status;
|
|
32722
|
+
return pin === "add" ? pinCommit(rest, change) : unpinCommit(rest, change, branch);
|
|
32634
32723
|
};
|
|
32635
|
-
|
|
32636
|
-
|
|
32637
|
-
|
|
32638
|
-
|
|
32639
|
-
|
|
32640
|
-
|
|
32641
|
-
|
|
32642
|
-
|
|
32643
|
-
|
|
32644
|
-
|
|
32645
|
-
|
|
32646
|
-
|
|
32724
|
+
const pinCommit = (rest, change) => {
|
|
32725
|
+
if (change === void 0 || change === "D") return pinRefusal("add");
|
|
32726
|
+
const to = rest.step.name;
|
|
32727
|
+
return {
|
|
32728
|
+
kind: "commit",
|
|
32729
|
+
to,
|
|
32730
|
+
spec: {
|
|
32731
|
+
actor: rest.actor,
|
|
32732
|
+
from: rest.state,
|
|
32733
|
+
to,
|
|
32734
|
+
pin: "add"
|
|
32735
|
+
},
|
|
32736
|
+
only: PIN_PATH
|
|
32737
|
+
};
|
|
32647
32738
|
};
|
|
32648
|
-
|
|
32649
|
-
|
|
32739
|
+
const unpinCommit = (rest, change, branch) => {
|
|
32740
|
+
if (change !== "D" && rest.pinned) return pinRefusal("remove");
|
|
32741
|
+
const to = rest.def.initial;
|
|
32742
|
+
return {
|
|
32743
|
+
kind: "commit",
|
|
32744
|
+
to,
|
|
32745
|
+
spec: {
|
|
32746
|
+
actor: rest.actor,
|
|
32747
|
+
from: rest.state,
|
|
32748
|
+
to,
|
|
32749
|
+
pin: "remove",
|
|
32750
|
+
...closingTrailers(branch)
|
|
32751
|
+
},
|
|
32752
|
+
keepHistory: processHistoryRef(rest.run.startHash),
|
|
32753
|
+
...rest.pinned ? { only: PIN_PATH } : {}
|
|
32754
|
+
};
|
|
32755
|
+
};
|
|
32756
|
+
const pinRefusal = (pin) => ({
|
|
32757
|
+
kind: "refusal",
|
|
32758
|
+
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`
|
|
32759
|
+
});
|
|
32760
|
+
const startTrailers = (rest, starts) => starts ? {
|
|
32761
|
+
vars: rest.vars,
|
|
32762
|
+
config: rest.config
|
|
32763
|
+
} : {};
|
|
32764
|
+
const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "ended" ? outcome.marks : [];
|
|
32650
32765
|
/**
|
|
32651
|
-
*
|
|
32652
|
-
*
|
|
32653
|
-
*
|
|
32654
|
-
* sample; an external validator prints a one-line skip notice instead, since
|
|
32655
|
-
* silence there would read as a clean bill of health; a format-only mode with
|
|
32656
|
-
* neither has nothing to round-trip either.
|
|
32766
|
+
* What landing the pending turn at `rest` does — a pure decision from a
|
|
32767
|
+
* replay with the working tree as the pending turn. The target step is
|
|
32768
|
+
* computed by replaying, never matched.
|
|
32657
32769
|
*/
|
|
32658
|
-
const
|
|
32659
|
-
if (
|
|
32660
|
-
const
|
|
32661
|
-
if (
|
|
32662
|
-
|
|
32663
|
-
|
|
32664
|
-
|
|
32665
|
-
|
|
32666
|
-
|
|
32667
|
-
|
|
32668
|
-
|
|
32669
|
-
|
|
32670
|
-
sample: capabilities.format.sample,
|
|
32671
|
-
formatCommand
|
|
32672
|
-
})
|
|
32673
|
-
}];
|
|
32770
|
+
const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
32771
|
+
if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
|
|
32772
|
+
const unapproved = approvalRefusal(rest, approver);
|
|
32773
|
+
if (unapproved !== void 0) return unapproved;
|
|
32774
|
+
const early = cleanLanding(rest);
|
|
32775
|
+
if (early !== void 0) return early;
|
|
32776
|
+
if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
|
|
32777
|
+
const message = accessRefusal(rest.changes, rest.stepDef.access.write);
|
|
32778
|
+
if (message !== void 0) return {
|
|
32779
|
+
kind: "refusal",
|
|
32780
|
+
message
|
|
32781
|
+
};
|
|
32674
32782
|
}
|
|
32675
|
-
|
|
32676
|
-
|
|
32677
|
-
|
|
32678
|
-
}
|
|
32679
|
-
|
|
32783
|
+
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
32784
|
+
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
32785
|
+
...verdicts !== void 0 ? { verdicts } : {}
|
|
32786
|
+
}));
|
|
32787
|
+
const to = yield* landingTarget(outcome, rest);
|
|
32788
|
+
if (typeof to !== "string") return to;
|
|
32789
|
+
return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
32790
|
+
});
|
|
32791
|
+
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
32792
|
+
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$1(void 0)));
|
|
32793
|
+
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
32794
|
+
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
32795
|
+
const git = yield* GitService;
|
|
32796
|
+
const branch = yield* git.currentBranch();
|
|
32797
|
+
const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none();
|
|
32798
|
+
const landing = yield* decideLanding(rest, verdicts, branch, approver);
|
|
32799
|
+
return {
|
|
32800
|
+
stepDef: rest.stepDef,
|
|
32801
|
+
state: rest.state,
|
|
32802
|
+
actor: rest.actor,
|
|
32803
|
+
changes: rest.changes,
|
|
32804
|
+
file: rest.hints.file,
|
|
32805
|
+
landing
|
|
32806
|
+
};
|
|
32807
|
+
});
|
|
32808
|
+
const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
|
|
32809
|
+
hash: entry.hash,
|
|
32810
|
+
state: entry.state
|
|
32811
|
+
}));
|
|
32812
|
+
const summaryInput = (run, settings) => {
|
|
32813
|
+
const entryCommit = run.trace[0].hash;
|
|
32814
|
+
const { total, byModel } = costTotals(run.costEntries);
|
|
32815
|
+
return {
|
|
32816
|
+
entryCommit,
|
|
32817
|
+
processBase: run.startParentHash,
|
|
32818
|
+
processTip: run.trace[run.trace.length - 1].hash,
|
|
32819
|
+
humanCommits: humanCommitsOf(run.trace, entryCommit),
|
|
32820
|
+
processCost: total,
|
|
32821
|
+
processCostByModel: byModel,
|
|
32822
|
+
marks: run.marks,
|
|
32823
|
+
...settings
|
|
32824
|
+
};
|
|
32825
|
+
};
|
|
32826
|
+
const summaryFor = (run) => gen(function* () {
|
|
32827
|
+
const config = yield* (yield* ConfigService).load;
|
|
32828
|
+
const host = yield* Host;
|
|
32829
|
+
const resolved = yield* workflowOf(config, run);
|
|
32830
|
+
const summary = resolved.def.summary;
|
|
32831
|
+
if (summary === void 0 || run.trace.length === 0) return { prompt: void 0 };
|
|
32832
|
+
return { prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))) };
|
|
32833
|
+
});
|
|
32834
|
+
/** 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. */
|
|
32835
|
+
const inRepoConfigPaths = (root, files, recorded) => {
|
|
32836
|
+
const paths = new Set(Object.keys(recorded));
|
|
32837
|
+
for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
|
|
32838
|
+
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
|
|
32839
|
+
};
|
|
32840
|
+
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}`);
|
|
32841
|
+
const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
|
|
32842
|
+
const differing = [];
|
|
32843
|
+
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);
|
|
32844
|
+
return differing;
|
|
32680
32845
|
});
|
|
32846
|
+
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");
|
|
32847
|
+
const historySteps = (run) => {
|
|
32848
|
+
const messages = new Map(run.episode.commits.map((c) => [c.hash, c.message]));
|
|
32849
|
+
return run.trace.map((entry) => {
|
|
32850
|
+
const message = parseCommitMessage(messages.get(entry.hash) ?? "");
|
|
32851
|
+
return {
|
|
32852
|
+
hash: entry.hash,
|
|
32853
|
+
subject: (messages.get(entry.hash) ?? "").split("\n")[0] ?? "",
|
|
32854
|
+
actor: entry.actor,
|
|
32855
|
+
from: message.parsed?.from,
|
|
32856
|
+
to: entry.state,
|
|
32857
|
+
verdicts: message.judge,
|
|
32858
|
+
...optional("approval", approvalEntryOf(entry.hash, message)[0])
|
|
32859
|
+
};
|
|
32860
|
+
});
|
|
32861
|
+
};
|
|
32862
|
+
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`;
|
|
32681
32863
|
/**
|
|
32682
|
-
*
|
|
32683
|
-
*
|
|
32684
|
-
*
|
|
32685
|
-
* runs this script itself — `resolveMode`+`validateScriptFor` only render
|
|
32686
|
-
* text for a driver to execute (`gtd renders; the driver executes`).
|
|
32864
|
+
* Replays the closed run at `tip` under today's config. The workflow is not
|
|
32865
|
+
* fingerprinted for a version change: the engine under it moves with every
|
|
32866
|
+
* upgrade, and the version warning is the only signal.
|
|
32687
32867
|
*/
|
|
32688
|
-
const
|
|
32689
|
-
|
|
32690
|
-
|
|
32691
|
-
|
|
32692
|
-
|
|
32693
|
-
|
|
32868
|
+
const historyTrail = (tip) => gen(function* () {
|
|
32869
|
+
const git = yield* GitService;
|
|
32870
|
+
const config = yield* (yield* ConfigService).load;
|
|
32871
|
+
const hash = yield* git.resolveRef(tip);
|
|
32872
|
+
const subject = yield* git.lastCommitSubject(hash);
|
|
32873
|
+
const run = yield* runAtTip(git, config.workflow, {
|
|
32874
|
+
hash,
|
|
32875
|
+
subject
|
|
32876
|
+
});
|
|
32877
|
+
const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
|
|
32878
|
+
command: "gtd history",
|
|
32879
|
+
notEnded: (o) => notClosedError(hash, o)
|
|
32880
|
+
});
|
|
32881
|
+
return {
|
|
32882
|
+
tip: hash,
|
|
32883
|
+
steps: historySteps(run),
|
|
32884
|
+
ended: outcome.via,
|
|
32885
|
+
version: {
|
|
32886
|
+
recorded,
|
|
32887
|
+
running: GTD_VERSION
|
|
32694
32888
|
},
|
|
32695
|
-
...
|
|
32696
|
-
|
|
32889
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
32890
|
+
};
|
|
32891
|
+
});
|
|
32892
|
+
/** Refuses on config drift before replaying, so a stale config never runs the flow. */
|
|
32893
|
+
const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
32894
|
+
const git = yield* GitService;
|
|
32895
|
+
const workspace = yield* Workspace;
|
|
32896
|
+
const host = yield* Host;
|
|
32897
|
+
const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
|
|
32898
|
+
const versionWarning = versionWarningFor(recorded);
|
|
32899
|
+
const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
|
|
32900
|
+
const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
|
|
32901
|
+
if (differing.length > 0) return yield* fail$8(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
|
|
32902
|
+
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
32903
|
+
const outcome = yield* promise(() => replayFor(setup));
|
|
32904
|
+
if (outcome.kind !== "ended") return yield* fail$8(withWarning(how.notEnded(outcome)));
|
|
32905
|
+
return {
|
|
32906
|
+
outcome,
|
|
32907
|
+
recorded,
|
|
32908
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
32909
|
+
};
|
|
32697
32910
|
});
|
|
32698
32911
|
//#endregion
|
|
32699
32912
|
//#region src/cli/ToolCommands.ts
|
|
@@ -32753,7 +32966,7 @@ const runStraysCommand = (command, json, out) => gen(function* () {
|
|
|
32753
32966
|
const narrator = yield* Narrator;
|
|
32754
32967
|
const base = yield* resolveSide(git, command.base);
|
|
32755
32968
|
const tip = yield* resolveSide(git, command.tip);
|
|
32756
|
-
const touched = yield* git.diffPaths(base, tip);
|
|
32969
|
+
const touched = (yield* git.diffPaths(base, tip)).filter((path) => path !== PIN_PATH);
|
|
32757
32970
|
const strays = strayPaths(touched, command.globs);
|
|
32758
32971
|
const total = touched.length;
|
|
32759
32972
|
if (json.kind === "select") yield* writeSelection(out, strays, json.path).pipe(catchIf((error) => error instanceof SelectorUsageError && /^\d+$/.test(json.path), () => void_));
|
|
@@ -33768,6 +33981,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
33768
33981
|
kind: "gitWrite",
|
|
33769
33982
|
command: mixedResetTo(run.startParentHash)
|
|
33770
33983
|
},
|
|
33984
|
+
{
|
|
33985
|
+
kind: "gitWrite",
|
|
33986
|
+
command: unpinScript(PIN_PATH)
|
|
33987
|
+
},
|
|
33771
33988
|
{
|
|
33772
33989
|
kind: "outcome",
|
|
33773
33990
|
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
@@ -33925,7 +34142,7 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
|
|
|
33925
34142
|
if (rest.run.trace.length === 0) return;
|
|
33926
34143
|
const git = yield* GitService;
|
|
33927
34144
|
const workspace = yield* Workspace;
|
|
33928
|
-
const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D").flatMap(({ path }) => {
|
|
34145
|
+
const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D" && c.path !== ".gtd/process.gtdpin").flatMap(({ path }) => {
|
|
33929
34146
|
const { threads, findings } = parseCodeThreads(path, workspace.atPath(path) ?? "");
|
|
33930
34147
|
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}`)];
|
|
33931
34148
|
});
|