@pmelab/gtd 3.4.0 → 3.5.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/dist/gtd.bundle.mjs +177 -38
- package/docs/cli.md +44 -12
- package/docs/configuration.md +60 -11
- package/docs/examples/advanced-workflow.md +17 -1
- package/docs/loop.md +4 -1
- package/package.json +1 -1
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -191766,6 +191766,40 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
|
|
|
191766
191766
|
*/
|
|
191767
191767
|
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
191768
191768
|
const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
|
|
191769
|
+
const COST_TRAILER_PREFIX = "Gtd-Cost: ";
|
|
191770
|
+
const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
|
|
191771
|
+
/** The bucket a cost with no `--model` tag is grouped under, kept distinct so a mixed history still totals correctly. */
|
|
191772
|
+
const UNATTRIBUTED_MODEL = "unspecified";
|
|
191773
|
+
/**
|
|
191774
|
+
* Append a `Gtd-Cost: <cost>[ <model>]` trailer (after a blank line) to a
|
|
191775
|
+
* commit `subject`, or return the subject unchanged when no cost was supplied.
|
|
191776
|
+
* The subject (first line) is never touched, so `parseStateSubject`/
|
|
191777
|
+
* `resolveState` still read `gtd(<actor>): <state>` back exactly as before.
|
|
191778
|
+
*/
|
|
191779
|
+
const withCostTrailer = (subject, cost, model) => cost === void 0 ? subject : `${subject}\n\n${COST_TRAILER_PREFIX}${cost}${model !== void 0 ? ` ${model}` : ""}`;
|
|
191780
|
+
/** Every `Gtd-Cost:` entry found across the given commit messages (each entry's model defaults to `UNATTRIBUTED_MODEL`). */
|
|
191781
|
+
const parseCostTrailers = (messages) => {
|
|
191782
|
+
const entries = [];
|
|
191783
|
+
for (const message of messages) for (const match of message.matchAll(COST_TRAILER_RE)) {
|
|
191784
|
+
const model = match[2]?.trim();
|
|
191785
|
+
entries.push({
|
|
191786
|
+
cost: Number(match[1]),
|
|
191787
|
+
model: model !== void 0 && model !== "" ? model : UNATTRIBUTED_MODEL
|
|
191788
|
+
});
|
|
191789
|
+
}
|
|
191790
|
+
return entries;
|
|
191791
|
+
};
|
|
191792
|
+
/** The total token cost across the given entries (`0` when none). */
|
|
191793
|
+
const totalCostOf = (entries) => entries.reduce((sum, entry) => sum + entry.cost, 0);
|
|
191794
|
+
/** Per-model token totals, highest-cost first (ties broken by model name for a stable order). */
|
|
191795
|
+
const costByModel = (entries) => {
|
|
191796
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
191797
|
+
for (const entry of entries) byModel.set(entry.model, (byModel.get(entry.model) ?? 0) + entry.cost);
|
|
191798
|
+
return [...byModel.entries()].map(([model, cost]) => ({
|
|
191799
|
+
model,
|
|
191800
|
+
cost
|
|
191801
|
+
})).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
191802
|
+
};
|
|
191769
191803
|
/** Collapse an arbitrary git status letter to the pattern grammar's closed `A|M|D` set (mirrors the plan's decision 5 — only those three are meaningful statuses). */
|
|
191770
191804
|
const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
|
|
191771
191805
|
/** Resolve HEAD's subject against the active workflow definition (the bundled default, or a compiled `.gtdrc` `workflow:` key). */
|
|
@@ -191810,7 +191844,8 @@ const computeProcessRun = (git, def) => gen(function* () {
|
|
|
191810
191844
|
if (!(yield* git.hasCommits())) return {
|
|
191811
191845
|
startHash: "",
|
|
191812
191846
|
startParentHash: EMPTY_TREE$1,
|
|
191813
|
-
trace: []
|
|
191847
|
+
trace: [],
|
|
191848
|
+
costEntries: []
|
|
191814
191849
|
};
|
|
191815
191850
|
const initialState = initialStateOf(def);
|
|
191816
191851
|
const history = yield* git.commitHistory();
|
|
@@ -191821,12 +191856,15 @@ const computeProcessRun = (git, def) => gen(function* () {
|
|
|
191821
191856
|
i--;
|
|
191822
191857
|
}
|
|
191823
191858
|
const startIdx = i + 1;
|
|
191824
|
-
const
|
|
191859
|
+
const processCommits = history.slice(startIdx);
|
|
191860
|
+
const trace = processCommits.map((h) => parseStateSubject(subjectOf$1(h.message)).state);
|
|
191861
|
+
const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
|
|
191825
191862
|
const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
|
|
191826
191863
|
return {
|
|
191827
191864
|
startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
|
|
191828
191865
|
startParentHash,
|
|
191829
|
-
trace
|
|
191866
|
+
trace,
|
|
191867
|
+
costEntries
|
|
191830
191868
|
};
|
|
191831
191869
|
});
|
|
191832
191870
|
/** The `GTD_VAR_`-prefix stripped, exact-case, from every matching entry — the highest-precedence `it.vars` layer. A `value === undefined` entry (a name declared-but-unset in the environment) is skipped, never coerced to the string `"undefined"`. */
|
|
@@ -191863,13 +191901,27 @@ const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe
|
|
|
191863
191901
|
pattern,
|
|
191864
191902
|
target
|
|
191865
191903
|
});
|
|
191866
|
-
/**
|
|
191867
|
-
|
|
191904
|
+
/**
|
|
191905
|
+
* Build the `PatternTemplates.TemplateContext` for rendering `state`'s content
|
|
191906
|
+
* at the resolved rest. `vars` is the already-merged three-layer map (see
|
|
191907
|
+
* `resolveVars`); `edges` is the resting state's own `on` edges (see
|
|
191908
|
+
* `toTemplateEdges`). `currentCost`/`currentModel` are the in-flight step's own
|
|
191909
|
+
* `--cost`/`--model` (folded into the process's committed cost entries so a
|
|
191910
|
+
* `commit:` squash template sees the whole-process total AND per-model
|
|
191911
|
+
* breakdown including the squashing step) — `0`/absent for the pure emitters
|
|
191912
|
+
* (`gtd next`/`gtd status`), where no step is being performed.
|
|
191913
|
+
*/
|
|
191914
|
+
const buildTemplateContext = (git, read, state, actor, run, vars, edges, currentCost = 0, currentModel) => gen(function* () {
|
|
191868
191915
|
const hasCommits = yield* git.hasCommits();
|
|
191869
191916
|
const currentCommit = hasCommits ? yield* git.resolveRef("HEAD") : "";
|
|
191870
191917
|
const previousCommit = hasCommits ? yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash))) : "";
|
|
191871
191918
|
const processDiff = [yield* git.diffRef(run.startParentHash).pipe(catchAll(() => succeed$3(""))), yield* git.diffHead().pipe(catchAll(() => succeed$3("")))].filter((d) => d.trim().length > 0).join("\n\n");
|
|
191872
191919
|
const lastDiff = run.trace.length > 0 ? yield* git.commitDiff(currentCommit).pipe(catchAll(() => succeed$3(""))) : "";
|
|
191920
|
+
const stepEntry = currentCost > 0 || currentModel !== void 0 ? [{
|
|
191921
|
+
cost: currentCost,
|
|
191922
|
+
model: currentModel ?? "unspecified"
|
|
191923
|
+
}] : [];
|
|
191924
|
+
const allCostEntries = [...run.costEntries, ...stepEntry];
|
|
191873
191925
|
return {
|
|
191874
191926
|
startCommit: run.startHash,
|
|
191875
191927
|
currentCommit,
|
|
@@ -191878,6 +191930,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(
|
|
|
191878
191930
|
actor,
|
|
191879
191931
|
processDiff,
|
|
191880
191932
|
lastDiff,
|
|
191933
|
+
processCost: totalCostOf(allCostEntries),
|
|
191934
|
+
processCostByModel: costByModel(allCostEntries),
|
|
191881
191935
|
read,
|
|
191882
191936
|
vars,
|
|
191883
191937
|
edges: toTemplateEdges(edges)
|
|
@@ -191945,18 +191999,22 @@ const renderRest = (rest, context) => gen(function* () {
|
|
|
191945
191999
|
});
|
|
191946
192000
|
/**
|
|
191947
192001
|
* Execute a `PatternMachine.step` decision: a `"commit"` decision stages and
|
|
191948
|
-
* commits everything pending under the decided subject
|
|
191949
|
-
*
|
|
191950
|
-
*
|
|
191951
|
-
* the
|
|
191952
|
-
*
|
|
191953
|
-
*
|
|
191954
|
-
*
|
|
191955
|
-
|
|
191956
|
-
|
|
192002
|
+
* commits everything pending under the decided subject, with an optional
|
|
192003
|
+
* `Gtd-Cost: <cost>[ <model>]` trailer (the invocation's `--cost`/`--model`,
|
|
192004
|
+
* persisted in the git log); a `"squash"` decision renders the commit-state
|
|
192005
|
+
* template against the PENDING tree — a render failure REFUSES the step,
|
|
192006
|
+
* touching nothing — then soft-resets to the process's start parent, writes
|
|
192007
|
+
* ONE commit with the rendered message (via `commitAsIs`, so the
|
|
192008
|
+
* still-uncommitted template file is excluded), and discards everything left
|
|
192009
|
+
* pending (the template file included). A squash records no trailer of its own
|
|
192010
|
+
* — the whole-process total and per-model breakdown reach the message through
|
|
192011
|
+
* `it.processCost`/`it.processCostByModel` in the rendered template. A
|
|
192012
|
+
* `"noop"` performs no IO.
|
|
192013
|
+
*/
|
|
192014
|
+
const executeDecision = (git, run, decision, context, cost, model) => gen(function* () {
|
|
191957
192015
|
switch (decision.kind) {
|
|
191958
192016
|
case "commit":
|
|
191959
|
-
yield* git.commitAllWithPrefix(decision.subject);
|
|
192017
|
+
yield* git.commitAllWithPrefix(withCostTrailer(decision.subject, cost, model));
|
|
191960
192018
|
return {
|
|
191961
192019
|
kind: "commit",
|
|
191962
192020
|
subject: decision.subject
|
|
@@ -364727,6 +364785,8 @@ const buildFileModeMap = (def, vars, root) => {
|
|
|
364727
364785
|
actor: "",
|
|
364728
364786
|
processDiff: "",
|
|
364729
364787
|
lastDiff: "",
|
|
364788
|
+
processCost: 0,
|
|
364789
|
+
processCostByModel: [],
|
|
364730
364790
|
read: (path) => {
|
|
364731
364791
|
throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
|
|
364732
364792
|
},
|
|
@@ -364978,7 +365038,10 @@ const HELP_TEXT = `Usage: gtd [command] [options]
|
|
|
364978
365038
|
Commands:
|
|
364979
365039
|
step <actor> Authenticate as <actor>, match the resolved rest's
|
|
364980
365040
|
declared patterns against the pending changes, and commit
|
|
364981
|
-
(or squash) the one resulting transition
|
|
365041
|
+
(or squash) the one resulting transition. Pass
|
|
365042
|
+
--cost=<n> (optionally --model=<name>) to record the
|
|
365043
|
+
just-finished invocation's token cost and model on the
|
|
365044
|
+
turn commit (summed into it.processCost/processCostByModel)
|
|
364982
365045
|
next Print the resolved rest's rendered script/prompt/message
|
|
364983
365046
|
(no mutation)
|
|
364984
365047
|
run Execute the resolved rest's emitted script, then step its
|
|
@@ -364992,6 +365055,8 @@ Commands:
|
|
|
364992
365055
|
|
|
364993
365056
|
Options:
|
|
364994
365057
|
--json Output structured JSON instead of plain text
|
|
365058
|
+
--cost=<n> (gtd step only) record the invocation's token cost
|
|
365059
|
+
--model=<name> (gtd step only, with --cost) tag that cost's model
|
|
364995
365060
|
--version, -v Print version and exit
|
|
364996
365061
|
--help, -h Print this help and exit
|
|
364997
365062
|
`;
|
|
@@ -365007,6 +365072,58 @@ const markEnveloped = (error) => Object.assign(error, { [ENVELOPED]: true });
|
|
|
365007
365072
|
const isEnveloped = (error) => typeof error === "object" && error !== null && error[ENVELOPED] === true;
|
|
365008
365073
|
/** Non-flag positional arguments past the subcommand name (argv[3..]). */
|
|
365009
365074
|
const commandArgs = (argv) => argv.slice(3).filter((a) => a.length > 0 && !a.startsWith("--"));
|
|
365075
|
+
const COST_FLAG = "--cost=";
|
|
365076
|
+
/**
|
|
365077
|
+
* Parse the optional `--cost=<n>` flag (only `gtd step` accepts it — see
|
|
365078
|
+
* `makeProgram`). `<n>` is the token cost of the invocation that produced the
|
|
365079
|
+
* pending changes, recorded as a `Gtd-Cost:` trailer on the turn commit. Must
|
|
365080
|
+
* be a non-negative finite number; a bare `--cost` (no `=`) or a non-numeric/
|
|
365081
|
+
* negative value is a usage error. Returns `undefined` when the flag is absent.
|
|
365082
|
+
*/
|
|
365083
|
+
const parseCostFlag = (argv) => {
|
|
365084
|
+
if (argv.slice(2).includes("--cost")) return fail$2(/* @__PURE__ */ new Error("gtd: --cost requires a value — use --cost=<number>"));
|
|
365085
|
+
const flag = argv.slice(2).find((a) => a.startsWith(COST_FLAG));
|
|
365086
|
+
if (flag === void 0) return succeed$3(void 0);
|
|
365087
|
+
const raw = flag.slice(7);
|
|
365088
|
+
const n = Number(raw);
|
|
365089
|
+
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) return fail$2(/* @__PURE__ */ new Error(`gtd: --cost must be a non-negative number — got "${raw}"`));
|
|
365090
|
+
return succeed$3(n);
|
|
365091
|
+
};
|
|
365092
|
+
const MODEL_FLAG = "--model=";
|
|
365093
|
+
/**
|
|
365094
|
+
* Parse the optional `--model=<name>` flag (only `gtd step`, and only
|
|
365095
|
+
* alongside `--cost` — see `makeProgram`). `<name>` tags the recorded cost
|
|
365096
|
+
* with the model the invocation ran on, appended to the `Gtd-Cost:` trailer
|
|
365097
|
+
* and grouped in `it.processCostByModel`. Must be non-empty and single-line
|
|
365098
|
+
* (it rides on one trailer line); a bare `--model` (no `=`) or an empty/
|
|
365099
|
+
* multiline value is a usage error. Returns `undefined` when the flag is absent.
|
|
365100
|
+
*/
|
|
365101
|
+
const parseModelFlag = (argv) => {
|
|
365102
|
+
if (argv.slice(2).includes("--model")) return fail$2(/* @__PURE__ */ new Error("gtd: --model requires a value — use --model=<name>"));
|
|
365103
|
+
const flag = argv.slice(2).find((a) => a.startsWith(MODEL_FLAG));
|
|
365104
|
+
if (flag === void 0) return succeed$3(void 0);
|
|
365105
|
+
const raw = flag.slice(8);
|
|
365106
|
+
if (raw.trim() === "" || /[\r\n]/.test(raw)) return fail$2(/* @__PURE__ */ new Error("gtd: --model must be a non-empty, single-line value"));
|
|
365107
|
+
return succeed$3(raw);
|
|
365108
|
+
};
|
|
365109
|
+
/**
|
|
365110
|
+
* Parse and validate the `--cost`/`--model` step flags together. Both are
|
|
365111
|
+
* orthogonal to `--json` but only meaningful to `gtd step` — rejected on any
|
|
365112
|
+
* other command rather than silently ignored (same discipline as `--json` on
|
|
365113
|
+
* `format`) — and `--model` requires `--cost` (a model tag with no token count
|
|
365114
|
+
* records nothing to sum).
|
|
365115
|
+
*/
|
|
365116
|
+
const parseStepFlags = (argv, positional) => gen(function* () {
|
|
365117
|
+
const cost = yield* parseCostFlag(argv);
|
|
365118
|
+
const model = yield* parseModelFlag(argv);
|
|
365119
|
+
if (cost !== void 0 && positional !== "step") return yield* fail$2(/* @__PURE__ */ new Error("gtd: --cost is only valid for `gtd step`"));
|
|
365120
|
+
if (model !== void 0 && positional !== "step") return yield* fail$2(/* @__PURE__ */ new Error("gtd: --model is only valid for `gtd step`"));
|
|
365121
|
+
if (model !== void 0 && cost === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd: --model requires --cost — it tags the recorded cost with the model that ran"));
|
|
365122
|
+
return {
|
|
365123
|
+
cost,
|
|
365124
|
+
model
|
|
365125
|
+
};
|
|
365126
|
+
});
|
|
365010
365127
|
/** Rejects extra positional arguments for a subcommand that takes none (`status`, `run`). */
|
|
365011
365128
|
const rejectExtraArgs = (command, argv) => {
|
|
365012
365129
|
const args = commandArgs(argv);
|
|
@@ -365055,6 +365172,8 @@ const resolveRestContext = (git) => gen(function* () {
|
|
|
365055
365172
|
context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on)
|
|
365056
365173
|
};
|
|
365057
365174
|
});
|
|
365175
|
+
/** The user-facing message for a `step` refusal — out-of-turn names the awaited actor, no-match names every declared pattern. */
|
|
365176
|
+
const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
|
|
365058
365177
|
/**
|
|
365059
365178
|
* Authenticate `invoker` against the resolved rest and perform the one
|
|
365060
365179
|
* resulting transition (commit or squash), shared by `gtd step` and
|
|
@@ -365063,7 +365182,7 @@ const resolveRestContext = (git) => gen(function* () {
|
|
|
365063
365182
|
* `subject: null` rather than failing (exit zero, per the plan's "clean
|
|
365064
365183
|
* no-op exits zero").
|
|
365065
365184
|
*/
|
|
365066
|
-
const stepAsActor = (invoker) => gen(function* () {
|
|
365185
|
+
const stepAsActor = (invoker, cost, model) => gen(function* () {
|
|
365067
365186
|
const git = yield* GitService;
|
|
365068
365187
|
const config = yield* ConfigService;
|
|
365069
365188
|
const worktree = yield* WorktreeReader;
|
|
@@ -365075,37 +365194,40 @@ const stepAsActor = (invoker) => gen(function* () {
|
|
|
365075
365194
|
changes,
|
|
365076
365195
|
processTrace: run.trace
|
|
365077
365196
|
});
|
|
365078
|
-
if (decision.kind === "refusal")
|
|
365079
|
-
const message = decision.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${decision.state}" awaits ${decision.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${decision.state}" — declared patterns: ${decision.patterns.length > 0 ? decision.patterns.join(", ") : "(none)"}`;
|
|
365080
|
-
return yield* fail$2(new Error(message));
|
|
365081
|
-
}
|
|
365197
|
+
if (decision.kind === "refusal") return yield* fail$2(new Error(formatStepRefusal(invoker, decision)));
|
|
365082
365198
|
if (decision.kind === "noop") return {
|
|
365083
365199
|
state: decision.state,
|
|
365084
|
-
subject: null
|
|
365200
|
+
subject: null,
|
|
365201
|
+
cost: null,
|
|
365202
|
+
model: null
|
|
365085
365203
|
};
|
|
365086
365204
|
const executable = decision;
|
|
365087
365205
|
const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
|
|
365088
365206
|
const renderedState = decision.kind === "squash" ? decision.state : rest.state;
|
|
365089
|
-
const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on));
|
|
365207
|
+
const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on, cost ?? 0, model), cost, model);
|
|
365090
365208
|
return {
|
|
365091
365209
|
state: rest.state,
|
|
365092
|
-
subject: outcome.kind === "noop" ? null : outcome.subject
|
|
365210
|
+
subject: outcome.kind === "noop" ? null : outcome.subject,
|
|
365211
|
+
cost: cost ?? null,
|
|
365212
|
+
model: model ?? null
|
|
365093
365213
|
};
|
|
365094
365214
|
});
|
|
365095
365215
|
/** Renders `stepAsActor`'s result the same way for both `gtd step` and `gtd run`. */
|
|
365096
365216
|
const reportStepResult = (result, json, write) => {
|
|
365097
365217
|
if (json) write(JSON.stringify({
|
|
365098
365218
|
state: result.state,
|
|
365099
|
-
subject: result.subject
|
|
365219
|
+
subject: result.subject,
|
|
365220
|
+
...result.cost !== null ? { cost: result.cost } : {},
|
|
365221
|
+
...result.model !== null ? { model: result.model } : {}
|
|
365100
365222
|
}) + "\n");
|
|
365101
365223
|
else write(result.subject !== null ? `committed: ${result.subject}\n` : `nothing to do at "${result.state}"\n`);
|
|
365102
365224
|
};
|
|
365103
|
-
/** `gtd step <actor
|
|
365104
|
-
const runStepCommand = (argv, json, write) => gen(function* () {
|
|
365225
|
+
/** `gtd step <actor> [--cost=<n>] [--model=<name>]`: authenticate as `<actor>` and perform the one resulting transition, recording `--cost`/`--model` as a `Gtd-Cost:` trailer. */
|
|
365226
|
+
const runStepCommand = (argv, json, write, cost, model) => gen(function* () {
|
|
365105
365227
|
const args = commandArgs(argv);
|
|
365106
365228
|
if (args.length === 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd step: missing actor argument"));
|
|
365107
365229
|
if (args.length > 1) return yield* fail$2(/* @__PURE__ */ new Error(`gtd step: too many arguments — expected one actor, got: ${args.join(", ")}`));
|
|
365108
|
-
const result = yield* stepAsActor(args[0]);
|
|
365230
|
+
const result = yield* stepAsActor(args[0], cost, model);
|
|
365109
365231
|
reportStepResult(result, json, write);
|
|
365110
365232
|
});
|
|
365111
365233
|
/** `gtd next [--json]`: pure emitter of the resolved rest's rendered content (no mutation). */
|
|
@@ -365164,8 +365286,21 @@ const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
|
|
|
365164
365286
|
pattern: matchedRow?.[0] ?? null
|
|
365165
365287
|
};
|
|
365166
365288
|
});
|
|
365167
|
-
/**
|
|
365168
|
-
|
|
365289
|
+
/**
|
|
365290
|
+
* True when the per-model breakdown adds information beyond the `Cost:` total:
|
|
365291
|
+
* more than one model, or a single model that carries an actual `--model` tag
|
|
365292
|
+
* (a lone `unspecified` bucket just restates the total, so it's suppressed).
|
|
365293
|
+
*/
|
|
365294
|
+
const breakdownIsInformative = (byModel) => byModel.length > 1 || byModel.length === 1 && byModel[0].model !== "unspecified";
|
|
365295
|
+
/** The plain-text `Cost:` line(s) for `gtd status` — empty when nothing recorded, plus an indented per-model split when informative. */
|
|
365296
|
+
const costStatusLines = (cost, byModel) => {
|
|
365297
|
+
if (cost <= 0) return [];
|
|
365298
|
+
const lines = [`Cost: ${cost}`];
|
|
365299
|
+
if (breakdownIsInformative(byModel)) for (const m of byModel) lines.push(` ${m.model}: ${m.cost}`);
|
|
365300
|
+
return lines;
|
|
365301
|
+
};
|
|
365302
|
+
/** `gtd status --json`'s emission — `{state, actor, changes, model?, memory?, file?, mode?, cost?, costByModel?, edges?}`. */
|
|
365303
|
+
const writeStatusJson = (write, rest, statusChanges, model, memory, file, cost, costByModel) => {
|
|
365169
365304
|
const edges = toTemplateEdges(rest.stateDef.on);
|
|
365170
365305
|
write(JSON.stringify({
|
|
365171
365306
|
state: rest.state,
|
|
@@ -365175,16 +365310,19 @@ const writeStatusJson = (write, rest, statusChanges, model, memory, file) => {
|
|
|
365175
365310
|
...memory !== void 0 ? { memory } : {},
|
|
365176
365311
|
...file !== void 0 ? { file } : {},
|
|
365177
365312
|
...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
|
|
365313
|
+
...cost > 0 ? { cost } : {},
|
|
365314
|
+
...cost > 0 ? { costByModel } : {},
|
|
365178
365315
|
...edges.length > 0 ? { edges } : {}
|
|
365179
365316
|
}) + "\n");
|
|
365180
365317
|
};
|
|
365181
|
-
/** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`Memory:`/`File:`/`Mode:`/`Pending:` lines. */
|
|
365182
|
-
const writeStatusPlain = (write, rest, statusChanges, model, memory, file) => {
|
|
365318
|
+
/** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`/`Pending:` lines. */
|
|
365319
|
+
const writeStatusPlain = (write, rest, statusChanges, model, memory, file, cost, costByModel) => {
|
|
365183
365320
|
const lines = [`State: ${rest.state}`, `Awaits: ${rest.actor}`];
|
|
365184
365321
|
if (model !== void 0) lines.push(`Model: ${model}`);
|
|
365185
365322
|
if (memory !== void 0) lines.push(`Memory: ${memory}`);
|
|
365186
365323
|
if (file !== void 0) lines.push(`File: ${file}`);
|
|
365187
365324
|
if (rest.stateDef.mode !== void 0) lines.push(`Mode: ${rest.stateDef.mode}`);
|
|
365325
|
+
lines.push(...costStatusLines(cost, costByModel));
|
|
365188
365326
|
if (statusChanges.length === 0) lines.push("Pending: (clean)");
|
|
365189
365327
|
else {
|
|
365190
365328
|
lines.push("Pending:");
|
|
@@ -365202,8 +365340,8 @@ const runStatusCommand = (argv, json, write) => gen(function* () {
|
|
|
365202
365340
|
const memory = yield* renderMemory(rest.stateDef, context);
|
|
365203
365341
|
const file = yield* renderFile(rest.stateDef, context);
|
|
365204
365342
|
const statusChanges = computeStatusChanges(rest.stateDef.on ?? [], changes);
|
|
365205
|
-
if (json) writeStatusJson(write, rest, statusChanges, model, memory, file);
|
|
365206
|
-
else writeStatusPlain(write, rest, statusChanges, model, memory, file);
|
|
365343
|
+
if (json) writeStatusJson(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
|
|
365344
|
+
else writeStatusPlain(write, rest, statusChanges, model, memory, file, context.processCost, context.processCostByModel);
|
|
365207
365345
|
});
|
|
365208
365346
|
/**
|
|
365209
365347
|
* `gtd mermaid`: pure emitter of the active workflow's SHAPE (not the
|
|
@@ -365269,9 +365407,9 @@ const assertRunningFromRepoRoot = (git, fs) => gen(function* () {
|
|
|
365269
365407
|
if ((yield* fs.realPath(topLevel)) !== (yield* fs.realPath(process.cwd()))) return yield* fail$2(/* @__PURE__ */ new Error(`gtd must be run from the repository root (${topLevel}); the current directory is ${process.cwd()}`));
|
|
365270
365408
|
});
|
|
365271
365409
|
/** Dispatches to the named `run*Command` handler for every known subcommand. */
|
|
365272
|
-
const dispatchKnownSubcommand = (sub, argv, json, write) => {
|
|
365410
|
+
const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
|
|
365273
365411
|
switch (sub) {
|
|
365274
|
-
case "step": return runStepCommand(argv, json, write);
|
|
365412
|
+
case "step": return runStepCommand(argv, json, write, cost, model);
|
|
365275
365413
|
case "next": return runNextCommand(json, write);
|
|
365276
365414
|
case "run": return runRunCommand(argv, json, write);
|
|
365277
365415
|
case "status": return runStatusCommand(argv, json, write);
|
|
@@ -365299,8 +365437,9 @@ function makeProgram(opts = {}) {
|
|
|
365299
365437
|
const positional = argv.slice(2).find((a) => !a.startsWith("--"));
|
|
365300
365438
|
return gen(function* () {
|
|
365301
365439
|
if (runVersionOrHelp(argv, write)) return;
|
|
365302
|
-
const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json");
|
|
365440
|
+
const unknownOption = argv.slice(2).find((a) => a.startsWith("--") && a !== "--json" && a !== "--cost" && a !== "--model" && !a.startsWith(COST_FLAG) && !a.startsWith(MODEL_FLAG));
|
|
365303
365441
|
if (unknownOption !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd: unknown option '${unknownOption}' — see \`gtd --help\``));
|
|
365442
|
+
const { cost, model } = yield* parseStepFlags(argv, positional);
|
|
365304
365443
|
if (positional === "format") return yield* runFormatCommand(argv, json);
|
|
365305
365444
|
if (positional === "lsp") return yield* runLspCommand(argv, json);
|
|
365306
365445
|
const sub = yield* requireKnownSubcommand(positional, json, write);
|
|
@@ -365309,7 +365448,7 @@ function makeProgram(opts = {}) {
|
|
|
365309
365448
|
yield* assertRunningFromRepoRoot(git, fs);
|
|
365310
365449
|
yield* closeReviewWindow;
|
|
365311
365450
|
yield* (yield* ConfigInit).ensure;
|
|
365312
|
-
const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write));
|
|
365451
|
+
const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write, cost, model));
|
|
365313
365452
|
const rearm = yield* either(openReviewWindow);
|
|
365314
365453
|
if (isLeft(outcome)) return yield* fail$2(outcome.left);
|
|
365315
365454
|
if (isLeft(rearm)) return yield* fail$2(rearm.left);
|
package/docs/cli.md
CHANGED
|
@@ -6,7 +6,10 @@ Usage: gtd [command] [options]
|
|
|
6
6
|
Commands:
|
|
7
7
|
step <actor> Authenticate as <actor>, match the resolved rest's
|
|
8
8
|
declared patterns against the pending changes, and commit
|
|
9
|
-
(or squash) the one resulting transition
|
|
9
|
+
(or squash) the one resulting transition. Pass
|
|
10
|
+
--cost=<n> (optionally --model=<name>) to record the
|
|
11
|
+
just-finished invocation's token cost and model on the
|
|
12
|
+
turn commit (summed into it.processCost/processCostByModel)
|
|
10
13
|
next Print the resolved rest's rendered script/prompt/message
|
|
11
14
|
(no mutation)
|
|
12
15
|
run Execute the resolved rest's emitted script, then step its
|
|
@@ -20,6 +23,8 @@ Commands:
|
|
|
20
23
|
|
|
21
24
|
Options:
|
|
22
25
|
--json Output structured JSON instead of plain text
|
|
26
|
+
--cost=<n> (gtd step only) record the invocation's token cost
|
|
27
|
+
--model=<name> (gtd step only, with --cost) tag that cost's model
|
|
23
28
|
--version, -v Print version and exit
|
|
24
29
|
--help, -h Print this help and exit
|
|
25
30
|
```
|
|
@@ -32,11 +37,15 @@ without touching the repository. Every other command must be run from the
|
|
|
32
37
|
history relative to cwd, so it refuses with a clear error if invoked from a
|
|
33
38
|
subdirectory.
|
|
34
39
|
|
|
35
|
-
`--json
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
`--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
|
|
41
|
+
`gtd step`) are the only long options. Any other `--` option (including a typo
|
|
42
|
+
like `--jsn`) is rejected with a usage error rather than silently ignored, so a
|
|
43
|
+
mistyped flag can never degrade a JSON caller to plain-text mode. A bare
|
|
44
|
+
`--cost`/`--model` with no value, a non-numeric or negative `--cost`, an empty
|
|
45
|
+
`--model`, `--model` without `--cost`, or either flag on any command other than
|
|
46
|
+
`gtd step` are all usage errors.
|
|
38
47
|
|
|
39
|
-
## `gtd step <actor
|
|
48
|
+
## `gtd step <actor> [--cost=<n>] [--model=<name>]`
|
|
40
49
|
|
|
41
50
|
Authenticates `<actor>` against the resolved rest and performs the ONE resulting
|
|
42
51
|
transition. Unlike v2, there is no fixpoint chain to drive: the pattern
|
|
@@ -45,6 +54,16 @@ authors at most one commit (a normal turn) or performs one squash
|
|
|
45
54
|
(§[STATES.md](../STATES.md#8-the-squash-lifecycle)) — never both, never a chain
|
|
46
55
|
of several. A caller that wants several transitions issues several invocations.
|
|
47
56
|
|
|
57
|
+
`--cost=<n>` records the token cost of the invocation that produced the pending
|
|
58
|
+
changes (a non-negative number), and the optional `--model=<name>` tags it with
|
|
59
|
+
the model that ran, as a `Gtd-Cost: <n> <model>` trailer on the turn commit —
|
|
60
|
+
persisted in the git log, summed across the process into `it.processCost` and
|
|
61
|
+
grouped by model into `it.processCostByModel` (see
|
|
62
|
+
[Configuration: Token cost](configuration.md#token-cost)). `--model` requires
|
|
63
|
+
`--cost`. On a squash they are folded into the process total/breakdown the
|
|
64
|
+
`commit:` template renders rather than written as a trailer. Both are orthogonal
|
|
65
|
+
to `--json`.
|
|
66
|
+
|
|
48
67
|
- **Out-of-turn refusal** — `<actor>` isn't the resolved state's declared actor:
|
|
49
68
|
exit non-zero, zero commits.
|
|
50
69
|
`gtd step <actor>: out of turn — "<state>" awaits <awaited-actor>`
|
|
@@ -69,10 +88,17 @@ or, at a no-op:
|
|
|
69
88
|
nothing to do at "idle"
|
|
70
89
|
```
|
|
71
90
|
|
|
72
|
-
`--json` emits `{state, subject}` — `subject` is `null` at a no-op
|
|
91
|
+
`--json` emits `{state, subject}` — `subject` is `null` at a no-op — plus
|
|
92
|
+
`cost`/`model` keys echoing what `--cost`/`--model` recorded (each omitted when
|
|
93
|
+
not passed):
|
|
73
94
|
|
|
74
95
|
```json
|
|
75
|
-
{
|
|
96
|
+
{
|
|
97
|
+
"state": "grilling",
|
|
98
|
+
"subject": "gtd(human): grilling",
|
|
99
|
+
"cost": 1450,
|
|
100
|
+
"model": "claude-opus-4-8"
|
|
101
|
+
}
|
|
76
102
|
```
|
|
77
103
|
|
|
78
104
|
## `gtd next [--json]`
|
|
@@ -171,15 +197,21 @@ Pending:
|
|
|
171
197
|
or, on a clean tree: `Pending: (clean)`. A `Model: <value>` line appears right
|
|
172
198
|
after `Awaits:` when the resolved state declares a `model:` hint, and
|
|
173
199
|
`Memory: <value>`/`File: <value>`/`Mode: <value>` lines appear after that (in
|
|
174
|
-
that order) when declared — each independently, only when set.
|
|
200
|
+
that order) when declared — each independently, only when set. A `Cost: <n>`
|
|
201
|
+
line appears after those when the current process has accumulated any
|
|
202
|
+
`Gtd-Cost:` (see [`gtd step --cost`](#gtd-step-actor---costn-model-name)),
|
|
203
|
+
omitted when the running total is `0`; when the breakdown adds information (more
|
|
204
|
+
than one model, or a single tagged model), indented ` <model>: <cost>` lines
|
|
205
|
+
follow it.
|
|
175
206
|
|
|
176
207
|
`--json` emits
|
|
177
|
-
`{state, actor, changes: [{status, path, pattern}], model?, memory?, file?, mode?, edges?}`
|
|
208
|
+
`{state, actor, changes: [{status, path, pattern}], model?, memory?, file?, mode?, cost?, costByModel?, edges?}`
|
|
178
209
|
— `pattern` is `null` when no declared row matches that change;
|
|
179
210
|
`model`/`memory`/`file`/`mode` are present only when the resolved state declares
|
|
180
|
-
them (
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
them; `cost`/`costByModel` (the `[{model, cost}]` breakdown) only when the
|
|
212
|
+
running process total is above `0`; and `edges` is the resting state's `on`
|
|
213
|
+
edges as `[{ pattern, target, describe? }]` (same as `gtd next --json`), omitted
|
|
214
|
+
when the state has no `on` (all omitted entirely, never `null`, otherwise):
|
|
183
215
|
|
|
184
216
|
```json
|
|
185
217
|
{
|
package/docs/configuration.md
CHANGED
|
@@ -322,17 +322,66 @@ head is preserved under `refs/gtd/review-head` (the base under
|
|
|
322
322
|
Every `script`/`prompt`/`message`/`commit`/`model`/`memory`/`file` template is
|
|
323
323
|
rendered as an Eta template (`it.<name>`) with:
|
|
324
324
|
|
|
325
|
-
| Variable
|
|
326
|
-
|
|
|
327
|
-
| `startCommit`
|
|
328
|
-
| `currentCommit`
|
|
329
|
-
| `previousCommit`
|
|
330
|
-
| `state`
|
|
331
|
-
| `actor`
|
|
332
|
-
| `processDiff`
|
|
333
|
-
| `lastDiff`
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
325
|
+
| Variable | Meaning |
|
|
326
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
327
|
+
| `startCommit` | The hash the current process started from (before its first turn). |
|
|
328
|
+
| `currentCommit` | HEAD's hash at render time. |
|
|
329
|
+
| `previousCommit` | The hash before the last transition (HEAD's parent, in-process). |
|
|
330
|
+
| `state` | The state whose content is being rendered. |
|
|
331
|
+
| `actor` | The actor this render is for. |
|
|
332
|
+
| `processDiff` | `startCommit..HEAD` plus the pending working-tree diff. |
|
|
333
|
+
| `lastDiff` | The diff of the last transition alone. |
|
|
334
|
+
| `processCost` | The accumulated token cost of the current process — the sum of every `Gtd-Cost:` trailer recorded by `gtd step <actor> --cost=<n>` across its turn commits, plus the in-flight step's own `--cost` (so a `commit:` squash template sees the whole-process total). A number, `0` when none recorded. See ["Token cost"](#token-cost). |
|
|
335
|
+
| `processCostByModel` | The same accumulated cost broken down per model: an array of `{ model, cost }`, highest-cost first, one entry per distinct `--model` tag (costs recorded with no `--model` group under `"unspecified"`). A squash template iterates it to itemize the feature's cost per model. Empty when none recorded. See ["Token cost"](#token-cost). |
|
|
336
|
+
| `read(path)` | Reads a working-tree file (pending contents, not HEAD's) by repo-relative path. Throws for a missing/unreadable path — for a `commit:` template, that throw refuses the step (see [STATES.md §8](../STATES.md#8-the-squash-lifecycle)). |
|
|
337
|
+
| `vars` | The merged three-layer variable map, always a flat `Record<string, string>` — see ["Variables"](#variables) below. |
|
|
338
|
+
|
|
339
|
+
### Token cost
|
|
340
|
+
|
|
341
|
+
A loop driver that knows how many tokens the invocation it just drove cost — and
|
|
342
|
+
on which model — can record both with
|
|
343
|
+
`gtd step <actor> --cost=<n> [--model=<name>]` (`<n>` a non-negative number).
|
|
344
|
+
gtd appends them to that turn's commit as a `Gtd-Cost: <n> <model>` trailer — a
|
|
345
|
+
blank line then the trailer, below the untouched `gtd(<actor>): <state>` subject
|
|
346
|
+
— so the per-turn cost and model are persisted in the git log:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
gtd(agent): reviewing
|
|
350
|
+
|
|
351
|
+
Gtd-Cost: 1450 claude-opus-4-8
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`--model` is optional and requires `--cost` (a model tag with no token count
|
|
355
|
+
records nothing to sum); a cost recorded without a model is grouped under
|
|
356
|
+
`"unspecified"`. `gtd status` shows the process's running total and, when the
|
|
357
|
+
breakdown adds information, a per-model split (a `Cost:` line with indented
|
|
358
|
+
`<model>: <cost>` lines / a `cost` number and a `costByModel` array, omitted
|
|
359
|
+
when nothing has been recorded). `gtd step --json` echoes what it recorded
|
|
360
|
+
(`{ "state": …, "subject": …, "cost": 1450, "model": "claude-opus-4-8" }`).
|
|
361
|
+
|
|
362
|
+
Every template sees the running total as `it.processCost` and the per-model
|
|
363
|
+
breakdown as `it.processCostByModel` (the table above): the current process's
|
|
364
|
+
turn-commit entries, plus the in-flight step's own `--cost`/`--model`. Their
|
|
365
|
+
intended use is a `commit:` squash template, which renders against the pending
|
|
366
|
+
tree as the process collapses — so the whole feature's total AND its per-model
|
|
367
|
+
itemization (including the squashing step itself) land in the squash message
|
|
368
|
+
even though the intermediate per-turn trailers are discarded with the turns they
|
|
369
|
+
rode on:
|
|
370
|
+
|
|
371
|
+
```yaml
|
|
372
|
+
done:
|
|
373
|
+
commit: |
|
|
374
|
+
feat: ship it
|
|
375
|
+
|
|
376
|
+
Total token cost: <%= it.processCost %>
|
|
377
|
+
<% it.processCostByModel.forEach(function(m){ %>
|
|
378
|
+
- <%= m.model %>: <%= m.cost %>
|
|
379
|
+
<% }) %>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`--cost`/`--model` are accepted only by `gtd step` (a usage error on any other
|
|
383
|
+
command), and gtd never interprets the number's unit (tokens, cents, whatever
|
|
384
|
+
the driver records) or the model name — it only records, sums, and groups them.
|
|
336
385
|
|
|
337
386
|
## Variables
|
|
338
387
|
|
|
@@ -405,9 +405,25 @@ workflow:
|
|
|
405
405
|
"M .gtd/COMMIT_MSG.md": done
|
|
406
406
|
|
|
407
407
|
done:
|
|
408
|
-
commit:
|
|
408
|
+
commit: |
|
|
409
|
+
<%~ it.read(".gtd/COMMIT_MSG.md") %>
|
|
410
|
+
|
|
411
|
+
Token cost: <%= it.processCost %>
|
|
412
|
+
<% it.processCostByModel.forEach(function(m){ %>
|
|
413
|
+
- <%= m.model %>: <%= m.cost %>
|
|
414
|
+
<% }) %>
|
|
409
415
|
````
|
|
410
416
|
|
|
417
|
+
The `done` commit appends a token-cost summary to the squashed message: if the
|
|
418
|
+
loop driving this workflow passes `gtd step <actor> --cost=<n> --model=<name>`
|
|
419
|
+
after each agent turn, `it.processCost` is the summed cost of the whole cycle
|
|
420
|
+
(including the `squashing` step itself) and `it.processCostByModel` itemizes it
|
|
421
|
+
per model — so the single squash commit records the feature's complete cost,
|
|
422
|
+
broken down by which model spent what, even though the per-turn trailers are
|
|
423
|
+
discarded with the turns they collapsed. Drop these lines if your driver doesn't
|
|
424
|
+
track cost — `it.processCost` is `0` and `it.processCostByModel` empty then. See
|
|
425
|
+
[Configuration: Token cost](../configuration.md#token-cost).
|
|
426
|
+
|
|
411
427
|
A clean `gtd step human` at `grilling-answer`/`architecting-answer` (the `C`
|
|
412
428
|
event) accepts the current phase's draft and moves on; any other pending change
|
|
413
429
|
loops back for another round. `picking` is a `script` state whose only job is to
|
package/docs/loop.md
CHANGED
|
@@ -5,7 +5,10 @@ gtd (v3, the pattern machine) exposes three commands a loop driver combines:
|
|
|
5
5
|
- **`gtd step <actor>`** — authenticate as `<actor>`, match the resolved rest's
|
|
6
6
|
declared patterns against the pending changes, and commit (or squash) the one
|
|
7
7
|
resulting transition. Performs AT MOST one transition — there is no fixpoint
|
|
8
|
-
chain to drive.
|
|
8
|
+
chain to drive. Pass `--cost=<n>` (optionally `--model=<name>`) to record the
|
|
9
|
+
just-finished invocation's token cost and model on the turn commit; gtd sums
|
|
10
|
+
them across the process into `it.processCost`/`it.processCostByModel` (see
|
|
11
|
+
[Configuration: Token cost](configuration.md#token-cost)).
|
|
9
12
|
- **`gtd next`** — print the resolved rest's rendered script/prompt/message,
|
|
10
13
|
without mutating anything.
|
|
11
14
|
- **`gtd run`** — execute the resolved rest's emitted script (only for a
|