@pmelab/gtd 20.0.0 → 20.1.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 +8 -1
- package/claude/hooks/access.ts +11 -4
- package/dist/gtd.bundle.mjs +30 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,6 +19,10 @@ to any machine that checks it out.
|
|
|
19
19
|
npm install -g @pmelab/gtd
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
That installs `latest`, which moves once a day to the newest release. Every
|
|
23
|
+
merge to `main` publishes to `next` first: `npm install -g @pmelab/gtd@next`
|
|
24
|
+
gets each one as it lands.
|
|
25
|
+
|
|
22
26
|
Or run without installing (prefix every `gtd` below with `npx`) — see
|
|
23
27
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md)
|
|
24
28
|
for the settings most projects tune. A process setting's value is committed to
|
|
@@ -63,7 +67,10 @@ for you. Ask your agent if you have any questions.
|
|
|
63
67
|
back next week: run the driver and it resumes exactly where it stopped.
|
|
64
68
|
- **Shareable.** Push the branch, and the workflow travels with it — the state
|
|
65
69
|
lives in the commits, so another machine (or another person) picks up exactly
|
|
66
|
-
where you left off.
|
|
70
|
+
where you left off. Each commit records the history format it was written in
|
|
71
|
+
(`Gtd-Format:`), so a gtd that cannot read a history says so instead of
|
|
72
|
+
misreading it — see
|
|
73
|
+
[History format](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#history-format).
|
|
67
74
|
- **Files, not chat.** Conversation lives in files like `REQUIREMENTS.md` or
|
|
68
75
|
`ARCHITECTURE.md`. Request changes by editing them, approve by leaving the
|
|
69
76
|
tree clean — all in your own editor. There is no chat UI that could flicker.
|
package/claude/hooks/access.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve } from "node:path"
|
|
2
|
-
|
|
3
1
|
import { globMatches } from "../../src/replay/Glob.js"
|
|
4
2
|
|
|
5
3
|
export type AccessDef = { read: string[] | null; write: string[] | null }
|
|
@@ -12,10 +10,19 @@ const WRITE: Record<string, string> = {
|
|
|
12
10
|
NotebookEdit: "notebook_path",
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
// Hand-rolled because a hooks module may not import `node:path`.
|
|
14
|
+
const segments = (p: string): string[] =>
|
|
15
|
+
p.split("/").reduce<string[]>((out, s) => {
|
|
16
|
+
if (s === "..") out.pop()
|
|
17
|
+
else if (s && s !== ".") out.push(s)
|
|
18
|
+
return out
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
15
21
|
// Repo-relative form of a path; undefined when it lies outside the repo.
|
|
16
22
|
const inRepo = (p: string, root: string): string | undefined => {
|
|
17
|
-
const
|
|
18
|
-
|
|
23
|
+
const base = segments(root)
|
|
24
|
+
const target = segments(p.startsWith("/") ? p : `${root}/${p}`)
|
|
25
|
+
return base.every((s, i) => target[i] === s) ? target.slice(base.length).join("/") : undefined
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
const deny = (verb: string, path: string, globs: string[]) =>
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -47564,6 +47564,12 @@ const diffTrees = (before, after) => {
|
|
|
47564
47564
|
};
|
|
47565
47565
|
};
|
|
47566
47566
|
const isEmptyDiff = (diff) => diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0;
|
|
47567
|
+
const short$1 = (hash) => hash.slice(0, 7);
|
|
47568
|
+
const formatFault = (commits) => {
|
|
47569
|
+
const foreign = commits.find((c) => c.format !== 1);
|
|
47570
|
+
if (foreign === void 0) return void 0;
|
|
47571
|
+
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads format 1 — run a gtd release that reads format ${foreign.format} to continue this process`;
|
|
47572
|
+
};
|
|
47567
47573
|
//#endregion
|
|
47568
47574
|
//#region src/replay/Trailers.ts
|
|
47569
47575
|
const TRANSITION_SEP = " → ";
|
|
@@ -47618,6 +47624,7 @@ const parseJson = (raw) => {
|
|
|
47618
47624
|
const TRAILER_RE = /^(Gtd-[A-Za-z-]+):[ \t]*(.*?)[ \t]*$/gm;
|
|
47619
47625
|
const COST_RE = /^([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+))?$/;
|
|
47620
47626
|
const VAR_RE = /^([^=\s]+)=(.*)$/;
|
|
47627
|
+
const FORMAT_RE = /^[1-9][0-9]*$/;
|
|
47621
47628
|
const TRAILER_READERS = {
|
|
47622
47629
|
"Gtd-Step": (value, into) => {
|
|
47623
47630
|
into.step ??= parseStepId(value);
|
|
@@ -47636,6 +47643,9 @@ const TRAILER_READERS = {
|
|
|
47636
47643
|
"Gtd-Review-Base": (value, into) => {
|
|
47637
47644
|
into.reviewBase ??= value.split(/\s/)[0];
|
|
47638
47645
|
},
|
|
47646
|
+
"Gtd-Format": (value, into) => {
|
|
47647
|
+
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
47648
|
+
},
|
|
47639
47649
|
"Gtd-Cost": (value, into) => {
|
|
47640
47650
|
const c = COST_RE.exec(value);
|
|
47641
47651
|
if (c !== null) into.cost.push({
|
|
@@ -47653,13 +47663,15 @@ const parseCommitMessage = (message) => {
|
|
|
47653
47663
|
reviewBase: void 0,
|
|
47654
47664
|
judge: [],
|
|
47655
47665
|
vars: {},
|
|
47656
|
-
cost: []
|
|
47666
|
+
cost: [],
|
|
47667
|
+
format: void 0
|
|
47657
47668
|
};
|
|
47658
47669
|
if (newline !== -1) for (const match of message.slice(newline).matchAll(TRAILER_RE)) TRAILER_READERS[match[1]]?.(match[2], trailers);
|
|
47659
47670
|
return {
|
|
47660
47671
|
subject,
|
|
47661
47672
|
parsed: parseSubject(subject),
|
|
47662
|
-
...trailers
|
|
47673
|
+
...trailers,
|
|
47674
|
+
format: trailers.format ?? 1
|
|
47663
47675
|
};
|
|
47664
47676
|
};
|
|
47665
47677
|
/** Trailer order is fixed so the same spec always yields the same bytes. */
|
|
@@ -47671,8 +47683,8 @@ const formatCommitMessage = (spec) => {
|
|
|
47671
47683
|
for (const [name, value] of Object.entries(spec.vars ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(`Gtd-Var: ${name}=${value}`);
|
|
47672
47684
|
if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
|
|
47673
47685
|
for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
|
|
47674
|
-
|
|
47675
|
-
return
|
|
47686
|
+
lines.push(`Gtd-Format: 1`);
|
|
47687
|
+
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
47676
47688
|
};
|
|
47677
47689
|
//#endregion
|
|
47678
47690
|
//#region src/replay/Replay.ts
|
|
@@ -64437,7 +64449,16 @@ const costEntriesOf = (message) => message.cost.map((c) => ({
|
|
|
64437
64449
|
model: c.model ?? "unspecified"
|
|
64438
64450
|
}));
|
|
64439
64451
|
const hashAt = (history, index) => history[index]?.hash;
|
|
64452
|
+
const historyFormatFault = (history, location) => formatFault(history.slice(location.baseIndex + 1).concat(history[location.baseIndex]?.message.startsWith("gtd") ? [history[location.baseIndex]] : []).map((c) => ({
|
|
64453
|
+
hash: c.hash,
|
|
64454
|
+
format: parseCommitMessage(c.message).format
|
|
64455
|
+
})));
|
|
64456
|
+
/** Fails on a history in a format this gtd does not read, before anything acts on it. */
|
|
64440
64457
|
const runOf = (history, location, closingHash = void 0) => {
|
|
64458
|
+
const fault = historyFormatFault(history, location);
|
|
64459
|
+
return fault === void 0 ? succeed$3(buildRun(history, location, closingHash)) : fail$7(new Error(fault));
|
|
64460
|
+
};
|
|
64461
|
+
const buildRun = (history, location, closingHash) => {
|
|
64441
64462
|
const processCommits = history.slice(location.processStart);
|
|
64442
64463
|
const parsed = processCommits.map((c) => parseCommitMessage(c.message));
|
|
64443
64464
|
const first = parsed[0];
|
|
@@ -64540,8 +64561,8 @@ const findBoundaryBase = (git, def, head) => gen(function* () {
|
|
|
64540
64561
|
};
|
|
64541
64562
|
});
|
|
64542
64563
|
const historyUpTo = (git, def, head) => flatMap$4(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
64543
|
-
const computeProcessRun = (git, def, head) =>
|
|
64544
|
-
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse. */
|
|
64564
|
+
const computeProcessRun = (git, def, head) => flatMap$4(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
64565
|
+
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
|
|
64545
64566
|
const currentRun = gen(function* () {
|
|
64546
64567
|
const git = yield* GitService;
|
|
64547
64568
|
const config = yield* (yield* ConfigService).load;
|
|
@@ -64554,18 +64575,18 @@ const summaryRun = gen(function* () {
|
|
|
64554
64575
|
const headEntry = (yield* git.subjectHistory(1, 0))[0];
|
|
64555
64576
|
if (headEntry === void 0) {
|
|
64556
64577
|
const history = yield* historyUpTo(git, def, void 0);
|
|
64557
|
-
return runOf(history, locateEpisode(def, history));
|
|
64578
|
+
return yield* runOf(history, locateEpisode(def, history));
|
|
64558
64579
|
}
|
|
64559
64580
|
if (!(parseCommitMessage(headEntry.subject).parsed?.to === def.initial)) {
|
|
64560
64581
|
const history = yield* historyUpTo(git, def, headEntry.hash);
|
|
64561
|
-
return runOf(history, locateEpisode(def, history));
|
|
64582
|
+
return yield* runOf(history, locateEpisode(def, history));
|
|
64562
64583
|
}
|
|
64563
64584
|
const parentRef = `${headEntry.hash}~1`;
|
|
64564
64585
|
const before = yield* historyUpTo(git, def, parentRef);
|
|
64565
64586
|
const location = locateEpisode(def, before);
|
|
64566
64587
|
let closing = yield* git.commitHistory(parentRef, headEntry.hash);
|
|
64567
64588
|
if (closing.length === 0) closing = yield* git.commitHistory(void 0, headEntry.hash);
|
|
64568
|
-
return runOf([...before, ...closing], { ...location }, headEntry.hash);
|
|
64589
|
+
return yield* runOf([...before, ...closing], { ...location }, headEntry.hash);
|
|
64569
64590
|
});
|
|
64570
64591
|
const commitTree = (workspace, hash) => {
|
|
64571
64592
|
let entries;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.json",
|
|
56
56
|
"lint": "oxlint .",
|
|
57
57
|
"lint:sh": "jiti scripts/generate-shell-corpus.ts --check && shellcheck -s sh tests/shell/corpus/*.sh",
|
|
58
|
-
"lint:boundaries": "
|
|
58
|
+
"lint:boundaries": "node scripts/lint-boundaries.mjs",
|
|
59
59
|
"format": "oxfmt --write .",
|
|
60
60
|
"format:check": "oxfmt --check .",
|
|
61
61
|
"test:mutation": "stryker run",
|