@pmelab/gtd 23.0.0 → 23.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": "23.0.0",
3
+ "version": "23.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
@@ -60,9 +60,11 @@ enforces it at the OS level. See
60
60
 
61
61
  Run `gtd install` at your repository's root. It writes a `gtd.config.ts` that
62
62
  runs gtd's shipped workflows — commit it; gtd refuses to run without one — and
63
- prints a briefing. Pipe that briefing into your coding agent and answer its
64
- questions. It should build you a script that will autonomously run the process
65
- for you. Ask your agent if you have any questions.
63
+ prints a briefing. The repository need not be a JavaScript project: gtd resolves
64
+ the config's `@pmelab/gtd/*` imports itself, so a Python, Rust or notes
65
+ repository needs no `package.json` and no install. Pipe that briefing into your
66
+ coding agent and answer its questions. It should build you a script that will
67
+ autonomously run the process for you. Ask your agent if you have any questions.
66
68
 
67
69
  ## Why
68
70
 
@@ -49222,6 +49222,13 @@ const parseJson$1 = (raw) => {
49222
49222
  const TRAILER_RE = /^(Gtd-[A-Za-z-]+):[ \t]*(.*?)[ \t]*$/gm;
49223
49223
  const COST_RE = /^([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+))?$/;
49224
49224
  const VAR_RE = /^([^=\s]+)=(.*)$/;
49225
+ const CONFIG_RE = /^([0-9a-f]{16}) (.+)$/;
49226
+ const CONTROL_CHARACTER$1 = /[\x00-\x1f\x7f-\x9f]/;
49227
+ const encodePath = (path) => path.trim() !== path || path.startsWith("\"") || CONTROL_CHARACTER$1.test(path) ? JSON.stringify(path) : path;
49228
+ const decodePath = (raw) => {
49229
+ const path = raw.startsWith("\"") ? parseJson$1(raw) : raw;
49230
+ return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
49231
+ };
49225
49232
  const FORMAT_RE = /^[1-9][0-9]*$/;
49226
49233
  const TRAILER_READERS = {
49227
49234
  "Gtd-Step": (value, into) => {
@@ -49238,6 +49245,11 @@ const TRAILER_READERS = {
49238
49245
  const kv = VAR_RE.exec(value);
49239
49246
  if (kv !== null) into.vars[kv[1]] = kv[2];
49240
49247
  },
49248
+ "Gtd-Config": (value, into) => {
49249
+ const entry = CONFIG_RE.exec(value);
49250
+ const path = entry === null ? void 0 : decodePath(entry[2]);
49251
+ if (path !== void 0) into.config[path] = entry[1];
49252
+ },
49241
49253
  "Gtd-Review-Base": (value, into) => {
49242
49254
  into.reviewBase ??= value.split(/\s/)[0];
49243
49255
  },
@@ -49261,6 +49273,7 @@ const parseCommitMessage = (message) => {
49261
49273
  reviewBase: void 0,
49262
49274
  judge: [],
49263
49275
  vars: {},
49276
+ config: {},
49264
49277
  cost: [],
49265
49278
  format: void 0
49266
49279
  };
@@ -49272,13 +49285,15 @@ const parseCommitMessage = (message) => {
49272
49285
  format: trailers.format ?? 1
49273
49286
  };
49274
49287
  };
49288
+ const sortedEntries = (record = {}) => Object.entries(record).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
49275
49289
  /** Trailer order is fixed so the same spec always yields the same bytes. */
49276
49290
  const formatCommitMessage = (spec) => {
49277
49291
  const lines = [];
49278
49292
  if (spec.step !== void 0) lines.push(`Gtd-Step: ${formatStepId(spec.step)}`);
49279
49293
  if (spec.workflow !== void 0) lines.push(`Gtd-Workflow: ${spec.workflow}`);
49280
49294
  if (spec.reviewBase !== void 0) lines.push(`Gtd-Review-Base: ${spec.reviewBase}`);
49281
- 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}`);
49295
+ for (const [name, value] of sortedEntries(spec.vars)) lines.push(`Gtd-Var: ${name}=${value}`);
49296
+ for (const [path, hash] of sortedEntries(spec.config)) lines.push(`Gtd-Config: ${hash} ${encodePath(path)}`);
49282
49297
  if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
49283
49298
  for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
49284
49299
  lines.push(`Gtd-Format: 1`);
@@ -50438,6 +50453,35 @@ const resolveScopeAccess = (def, skills, vars) => gen(function* () {
50438
50453
  //#region src/workflow/Workflow.ts
50439
50454
  const knownModes = (def) => Object.keys(def.modes);
50440
50455
  //#endregion
50456
+ //#region src/workflow/fingerprint.ts
50457
+ const HASH_LENGTH = 16;
50458
+ const CONTROL_CHARACTER = /[\x00-\x1f\x7f-\x9f]/;
50459
+ const contentHash = (content) => createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH);
50460
+ const realOr = (path) => {
50461
+ try {
50462
+ return realpathSync(path);
50463
+ } catch {
50464
+ return path;
50465
+ }
50466
+ };
50467
+ const configFingerprint = (files, root) => {
50468
+ const fingerprint = {};
50469
+ const realRoot = realOr(root);
50470
+ for (const file of files) {
50471
+ const path = relative(realRoot, realOr(file)).split(sep).join("/");
50472
+ if (CONTROL_CHARACTER.test(path)) continue;
50473
+ let content;
50474
+ try {
50475
+ content = readFileSync(file);
50476
+ } catch {
50477
+ continue;
50478
+ }
50479
+ fingerprint[path] = contentHash(content);
50480
+ }
50481
+ return fingerprint;
50482
+ };
50483
+ const changedConfigFiles = (recorded, current) => [.../* @__PURE__ */ new Set([...Object.keys(recorded), ...Object.keys(current)])].filter((path) => recorded[path] !== current[path]).sort();
50484
+ //#endregion
50441
50485
  //#region src/judges/providers/fixed.ts
50442
50486
  const fail$3 = (message) => {
50443
50487
  throw new Error(`gtd judge run: ${message}`);
@@ -56082,7 +56126,8 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
56082
56126
  * `gtd --workflow <name>`: start a brand NEW process on a workflow, writing an
56083
56127
  * opening commit that names the workflow's first step and carries a
56084
56128
  * `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
56085
- * a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base.
56129
+ * a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
56130
+ * and a `Gtd-Config:` trailer per config file.
56086
56131
  * All validation is a REFUSAL, not an Effect failure.
56087
56132
  */
56088
56133
  const planStart = (current, actor, start) => gen(function* () {
@@ -56117,7 +56162,8 @@ const planStart = (current, actor, start) => gen(function* () {
56117
56162
  to: firstStep,
56118
56163
  workflow: name,
56119
56164
  ...base !== void 0 ? { reviewBase: base } : {},
56120
- vars
56165
+ vars,
56166
+ config: start.config
56121
56167
  })
56122
56168
  }
56123
56169
  }, {
@@ -56218,6 +56264,15 @@ const runOf = (history, location, closingHash = void 0) => {
56218
56264
  const fault = historyFormatFault(history, location);
56219
56265
  return fault === void 0 ? succeed$3(buildRun(history, location, closingHash)) : fail$7(new Error(fault));
56220
56266
  };
56267
+ const pinnedConfigOf = (history) => {
56268
+ for (let i = history.length - 1; i >= 0; i--) {
56269
+ const files = parseCommitMessage(history[i].message).config;
56270
+ if (Object.keys(files).length > 0) return {
56271
+ commit: history[i].hash,
56272
+ files
56273
+ };
56274
+ }
56275
+ };
56221
56276
  const buildRun = (history, location, closingHash) => {
56222
56277
  const processCommits = history.slice(location.processStart);
56223
56278
  const parsed = processCommits.map((c) => parseCommitMessage(c.message));
@@ -56234,6 +56289,7 @@ const buildRun = (history, location, closingHash) => {
56234
56289
  costEntries: parsed.flatMap(costEntriesOf),
56235
56290
  judgeVerdicts: parsed.flatMap((m) => m.judge),
56236
56291
  pinnedVars: { ...first?.vars },
56292
+ pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
56237
56293
  headTurn: headTurnOf(head),
56238
56294
  closingHash,
56239
56295
  episode: {
@@ -56322,6 +56378,18 @@ const findBoundaryBase = (git, def, head) => gen(function* () {
56322
56378
  });
56323
56379
  const historyUpTo = (git, def, head) => flatMap$4(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
56324
56380
  const computeProcessRun = (git, def, head) => flatMap$4(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
56381
+ const changedSinceStart = (pinned, current, workspace) => {
56382
+ if (pinned === void 0) return [];
56383
+ return changedConfigFiles(pinned.files, current).filter((path) => {
56384
+ const committed = workspace.readCommittedSync(path, pinned.commit);
56385
+ return current[path] === void 0 || committed === void 0 || contentHash(committed) !== current[path];
56386
+ });
56387
+ };
56388
+ const warnConfigChanges = (run, current, workspace) => gen(function* () {
56389
+ const changed = changedSinceStart(run.pinnedConfig, current, workspace);
56390
+ if (changed.length === 0) return;
56391
+ 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`);
56392
+ });
56325
56393
  /** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
56326
56394
  const currentRun = gen(function* () {
56327
56395
  const git = yield* GitService;
@@ -56621,6 +56689,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
56621
56689
  path: e.path
56622
56690
  })) : [];
56623
56691
  const memory = memoryOf(outcome.trace, run);
56692
+ const configNow = configFingerprint(config.workflowFiles, host.root);
56693
+ if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
56624
56694
  return {
56625
56695
  def,
56626
56696
  workflowName: resolved.name,
@@ -56632,6 +56702,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
56632
56702
  run,
56633
56703
  vars,
56634
56704
  env,
56705
+ config: configNow,
56635
56706
  changes,
56636
56707
  memory: memory.key,
56637
56708
  memoryResumed: memory.resumed,
@@ -56763,7 +56834,10 @@ const landingTo = (rest, to) => {
56763
56834
  from: rest.state,
56764
56835
  to,
56765
56836
  step: rest.step.id,
56766
- ...starts ? { vars: rest.vars } : {}
56837
+ ...starts ? {
56838
+ vars: rest.vars,
56839
+ config: rest.config
56840
+ } : {}
56767
56841
  }
56768
56842
  };
56769
56843
  };
@@ -69222,8 +69296,8 @@ const normalizeScriptNewline = (script) => script.length === 0 ? script : `${scr
69222
69296
  const landingScript = (required) => combinedScript(required, "");
69223
69297
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
69224
69298
  /** Decides `gtd land`'s one transition without performing it. Refusals fail the Effect; a no-op returns `subject: null` and empty scripts. */
69225
- const planLanding = (opts = {}) => gen(function* () {
69226
- const rest = yield* currentRest;
69299
+ const planLanding = (opts = {}, current) => gen(function* () {
69300
+ const rest = current ?? (yield* currentRest);
69227
69301
  const outcome = planStep(yield* snapshotFromRest(rest, opts.judge), opts);
69228
69302
  if (outcome.kind === "refusal") return yield* fail$7(new Error(outcome.message));
69229
69303
  if (outcome.kind === "noop") {
@@ -69358,7 +69432,7 @@ const runJudgeAnswerCommand = (json, out) => gen(function* () {
69358
69432
  catch: () => new SelectorUsageError("gtd judge answer: stdin is not valid JSON")
69359
69433
  });
69360
69434
  const verdict = yield* decodeUnknown(verdictSchemaFor(ids))(parsed).pipe(mapError$1((e) => new SelectorUsageError(`gtd judge answer: the verdict on stdin does not match the pending questions — ${e.message}`)));
69361
- const built = landFields(yield* planLanding({ judge: verdict }));
69435
+ const built = landFields(yield* planLanding({ judge: verdict }, rest));
69362
69436
  if (json.kind === "document") out.write(renderLandJson(built));
69363
69437
  else if (json.kind === "select") yield* writeSelection(out, built, json.path);
69364
69438
  else out.write(renderLandPlain(built, "judge answer"));
@@ -69398,7 +69472,8 @@ const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => ge
69398
69472
  workflow: resolved,
69399
69473
  first: started,
69400
69474
  commandLabel,
69401
- vars: varOverrides
69475
+ vars: varOverrides,
69476
+ config: rest.config
69402
69477
  });
69403
69478
  if (plan.kind === "refusal") return yield* fail$7(new Error(plan.message));
69404
69479
  out.write(landingScript(ScriptSurface.render(plan.steps)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "23.0.0",
3
+ "version": "23.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": {
@@ -186,9 +186,13 @@ certainly a bug.
186
186
  ## Rules for flow code
187
187
 
188
188
  Flow code is replayed on every command, so it must reach the same steps every
189
- time it sees the same history. `run()` bodies and module top-level code are
190
- exempt. gtd does not read the source ahead of time: breaking a rule shows up
191
- when replay runs, as an error or, for nondeterminism, as a divergence later.
189
+ time it sees the same history. Only `run()` bodies are exempt: module top-level
190
+ code and every local import are evaluated on every command too, so they read no
191
+ `process.env`, clock, network or files either (a per-machine value is an
192
+ `envDefaults` setting). A process records the config's hashes when it starts and
193
+ warns whenever it differs. gtd does not read the source ahead of time: breaking
194
+ a rule shows up when replay runs, as an error or, for nondeterminism, as a
195
+ divergence later.
192
196
 
193
197
  - No IO or nondeterminism in flow code — no clock, randomness, environment,
194
198
  network or filesystem. Read the tree through the helpers; do IO inside a