@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "20.0.0",
3
+ "version": "20.1.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
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.
@@ -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 rel = relative(root, isAbsolute(p) ? p : resolve(root, p))
18
- return rel === ".." || rel.startsWith("../") || isAbsolute(rel) ? undefined : rel
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[]) =>
@@ -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
- const subject = formatSubject(spec.actor, spec.to, spec.from);
47675
- return lines.length === 0 ? subject : `${subject}\n\n${lines.join("\n")}`;
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) => map$4(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
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.0.0",
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": "depcruise src tests",
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",