@pmelab/gtd 35.0.0 → 37.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.
Files changed (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +45 -29
  3. package/claude/hooks/context.ts +66 -0
  4. package/claude/hooks/drive.ts +3 -0
  5. package/claude/hooks/register.tsx +69 -6
  6. package/dist/chunks/{reserved-C8Fm3s72.mjs → reserved-zAfoJtR7.mjs} +62 -24
  7. package/dist/chunks/{tail-oaYewBN_.mjs → tail-BRC4DGdV.mjs} +1 -1
  8. package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
  9. package/dist/flows.mjs +15 -10
  10. package/dist/gtd.bundle.mjs +511 -64
  11. package/dist/testkit.mjs +158 -26
  12. package/dist/wire.mjs +2 -2
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +138 -38
  15. package/dist/workflows/fix.mjs +2 -2
  16. package/dist/workflows/review.mjs +2 -2
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +208 -66
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.test.ts +10 -3
  22. package/src/cli/Cli.ts +76 -1
  23. package/src/cli/Program.test.ts +1 -0
  24. package/src/cli/Program.ts +28 -0
  25. package/src/cli/ToolCommands.ts +3 -0
  26. package/src/edge/Edge.ts +11 -0
  27. package/src/flows/runtime.ts +11 -0
  28. package/src/flows/scripts.ts +31 -9
  29. package/src/replay/Replay.ts +51 -2
  30. package/src/testkit/FlowScripts.test.ts +58 -0
  31. package/src/testkit/FlowScripts.ts +61 -24
  32. package/src/testkit/Layers.ts +1 -0
  33. package/src/wire/BeatDocument.ts +12 -2
  34. package/src/wire/BeatStatus.ts +3 -0
  35. package/src/wire/ContextLimit.fixture.ts +30 -0
  36. package/src/wire/ContextLimit.test.ts +96 -0
  37. package/src/wire/ContextLimit.ts +169 -0
  38. package/src/wire/index.ts +1 -0
  39. package/src/wire/types.ts +7 -0
  40. package/src/workflow/Workflow.ts +9 -1
  41. package/src/workflows/feature/acceptance-criteria.feature +5 -1
  42. package/src/workflows/feature/context-limit.feature +37 -0
  43. package/src/workflows/feature/coverage.feature +6 -3
  44. package/src/workflows/feature/coverage.ts +8 -2
  45. package/src/workflows/feature/fast-path.feature +11 -19
  46. package/src/workflows/feature/feature.test.ts +24 -4
  47. package/src/workflows/feature/feature.ts +14 -10
  48. package/src/workflows/feature/flag-mode.feature +17 -9
  49. package/src/workflows/feature/lens-models.feature +7 -1
  50. package/src/workflows/feature/naive-acceptance.feature +2 -2
  51. package/src/workflows/feature/one-step-land.feature +10 -12
  52. package/src/workflows/feature/quality-unresolved.feature +7 -1
  53. package/src/workflows/feature/refactor-absorbs.feature +12 -5
  54. package/src/workflows/feature/refactor-first.feature +11 -2
  55. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  56. package/src/workflows/feature/refactor-rebuild.feature +269 -0
  57. package/src/workflows/feature/refactor-review.feature +45 -1
  58. package/src/workflows/feature/refactor.ts +9 -3
  59. package/src/workflows/feature/risk-tier.feature +12 -4
  60. package/src/workflows/feature/seam-rejection.feature +4 -9
  61. package/src/workflows/feature/seam.feature +29 -26
  62. package/src/workflows/feature/seam.test.ts +117 -6
  63. package/src/workflows/feature/seam.ts +158 -39
  64. package/src/workflows/review/review-door.feature +12 -4
  65. package/src/workflows/shared/options.test.ts +11 -2
  66. package/src/workflows/shared/options.ts +18 -4
  67. package/src/workflows/shared/steps.ts +1 -0
  68. package/src/workflows/shared/text.test.ts +8 -0
  69. package/src/workflows/shared/text.ts +91 -22
@@ -116,6 +116,7 @@ const statusOf = (input) => {
116
116
  initial,
117
117
  model: rendered.model,
118
118
  system: rendered.system !== void 0 && rendered.system !== "" ? rendered.system : void 0,
119
+ context: rendered.context,
119
120
  log,
120
121
  state: rendered.state,
121
122
  actor: rendered.actor,
@@ -169,7 +170,7 @@ const dispatchFieldsOf = (demand) => {
169
170
  }
170
171
  };
171
172
  /**
172
- * Flatten a `Demand` plus its `BeatStatus` into the single 25-key document
173
+ * Flatten a `Demand` plus its `BeatStatus` into the single 26-key document
173
174
  * `gtd next --json` emits — the ONLY place the two are joined. `cost`/
174
175
  * `costByModel` are omitted together, exactly when no cost was recorded
175
176
  * (`cost <= 0`).
@@ -186,6 +187,7 @@ const beatDocument = (demand, status) => {
186
187
  session,
187
188
  model: status.model,
188
189
  system: status.system,
190
+ context: status.context,
189
191
  validate,
190
192
  log: status.log,
191
193
  state: status.state,
@@ -488,4 +490,117 @@ const slicesDocument = (slices) => ({
488
490
  });
489
491
  const renderSlicesJson = (document) => JSON.stringify(document) + "\n";
490
492
  //#endregion
491
- export { WIRE_SCHEMA as A, EXIT_RUNTIME_ERROR as C, EXIT_USAGE_ERROR as D, EXIT_STRAYS_FOUND as E, ACCESS_REFUSAL as O, EXIT_OK as S, EXIT_SIGTERM as T, BEAT_KINDS as _, selectPath as a, stallDiagnosis as b, landProseText as c, renderLandPlain as d, DEMAND_BRIEFING as f, statusOf as g, renderBeatJson as h, resolveSession as i, renderFormat as j, UNATTRIBUTED_MODEL as k, noopText as l, beatDocument as m, slicesDocument as n, judgeJson as o, renderBeatPlain as p, globMatches as r, landFields as s, renderSlicesJson as t, renderLandJson as u, beatKindOf as v, EXIT_SIGINT as w, EXIT_CODES as x, demandOf as y };
493
+ //#region src/wire/ContextLimit.ts
494
+ const DEFAULT_CONTEXT_LIMIT = {
495
+ soft: .6,
496
+ hard: .7
497
+ };
498
+ const DEFAULT_WINDOW = 2e5;
499
+ const LONG_WINDOW = 1e6;
500
+ /** Claude Code's `[1m]` suffix selects the long window; a transcript never names it, so the driver decides from the model it passed. */
501
+ const contextWindowFor = (model) => model !== void 0 && /\[1m\]/i.test(model) ? LONG_WINDOW : DEFAULT_WINDOW;
502
+ /** A positive whole number of tokens, or `undefined` for anything else. */
503
+ const parseContextWindow = (raw) => {
504
+ const n = Number(raw);
505
+ return raw !== void 0 && raw.trim() !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
506
+ };
507
+ /**
508
+ * Where a driver asks `gtd context-hook` to record a hard stop's `stopReason`:
509
+ * Claude Code prints it nowhere a driver reads, and `hook_stopped` alone would
510
+ * also name any other hook that stopped the turn.
511
+ */
512
+ const CONTEXT_STOP_FILE_ENV = "GTD_LOOP_CONTEXT_STOP_FILE";
513
+ /**
514
+ * Why one layer of a limit (a scope's, a step's) is malformed, ignoring how its
515
+ * two shares relate: layers merge key by key, so only the merged limit has to
516
+ * keep `soft` below `hard`. Config is evaluated untyped, so anything may arrive.
517
+ */
518
+ const contextShapeFault = (limit) => {
519
+ if (typeof limit !== "object" || limit === null || Array.isArray(limit)) return `context must be an object { soft, hard }, got ${JSON.stringify(limit)}`;
520
+ const unknown = Object.keys(limit).filter((key) => key !== "soft" && key !== "hard");
521
+ if (unknown.length > 0) return `unknown key(s) ${unknown.join(", ")} in context`;
522
+ const shares = limit;
523
+ for (const key of ["soft", "hard"]) {
524
+ const value = shares[key];
525
+ if (value === void 0) continue;
526
+ if (typeof value !== "number" || !(value > 0 && value <= 1)) return `context.${key} must be a share of the context window in (0, 1], got ${JSON.stringify(value)}`;
527
+ }
528
+ };
529
+ /** Why a whole limit is unusable, or `undefined` when it is fine. */
530
+ const contextLimitFault = (limit) => {
531
+ const shape = contextShapeFault(limit);
532
+ if (shape !== void 0) return shape;
533
+ const shares = limit;
534
+ const soft = shares.soft ?? DEFAULT_CONTEXT_LIMIT.soft;
535
+ const hard = shares.hard ?? DEFAULT_CONTEXT_LIMIT.hard;
536
+ return soft < hard ? void 0 : `context.soft (${soft}) must be below context.hard (${hard})`;
537
+ };
538
+ const record = (value) => typeof value === "object" && value !== null ? value : void 0;
539
+ const count = (value) => typeof value === "number" && value > 0 ? value : 0;
540
+ /** What a message's usage puts in the context window: its whole input, cached or not. */
541
+ const usageTokens = (usage) => {
542
+ const u = record(usage);
543
+ if (u === void 0) return void 0;
544
+ return count(u["input_tokens"]) + count(u["cache_read_input_tokens"]) + count(u["cache_creation_input_tokens"]);
545
+ };
546
+ /**
547
+ * The context size of the latest assistant message in a Claude Code transcript
548
+ * (one JSON entry per line). Usage is per message, not a running sum, so the
549
+ * last entry alone is the window's fill.
550
+ */
551
+ const transcriptTokens = (transcript) => {
552
+ const lines = transcript.split("\n");
553
+ for (let i = lines.length - 1; i >= 0; i--) {
554
+ const line = lines[i].trim();
555
+ if (line === "") continue;
556
+ let entry;
557
+ try {
558
+ entry = JSON.parse(line);
559
+ } catch {
560
+ continue;
561
+ }
562
+ if (record(entry)?.["type"] !== "assistant") continue;
563
+ const tokens = usageTokens(record(record(entry)?.["message"])?.["usage"]);
564
+ if (tokens !== void 0) return tokens;
565
+ }
566
+ };
567
+ /**
568
+ * A worker subagent's own transcript. A hook its tool call fires is handed the
569
+ * MAIN session's `transcript_path`; the worker's sits beside it.
570
+ */
571
+ const subagentTranscriptPath = (transcriptPath, agentId) => `${transcriptPath.replace(/\.jsonl$/, "")}/subagents/agent-${agentId}.jsonl`;
572
+ /** Every hard stop's `stopReason` starts with this: the driver's context-limit marker. */
573
+ const CONTEXT_LIMIT_MARKER = "gtd: context limit";
574
+ const percent = (share) => `${Math.round(share * 100)}%`;
575
+ /**
576
+ * What a hook answers, or `undefined` to let the run go on. Compaction about
577
+ * to happen means the limit was missed, so `PreCompact` is a hard stop whatever
578
+ * the count says.
579
+ */
580
+ const contextHook = (input) => {
581
+ const { event, tokens, window, limit } = input;
582
+ if (event === "PreCompact") return {
583
+ kind: "hard",
584
+ stopReason: `${CONTEXT_LIMIT_MARKER}: the context is about to be compacted — the run is too big for one turn`
585
+ };
586
+ if (tokens === void 0) return void 0;
587
+ const used = `${tokens} of ${window} tokens`;
588
+ if (tokens >= limit.hard * window) return {
589
+ kind: "hard",
590
+ stopReason: `${CONTEXT_LIMIT_MARKER}: ${used}, past the ${percent(limit.hard)} hard limit`
591
+ };
592
+ if (tokens >= limit.soft * window) return {
593
+ kind: "soft",
594
+ reason: `Context limit: ${used} used, past the ${percent(limit.soft)} soft limit. Stop working now. Do not start anything new; end your turn with a short handoff note as your answer: what you touched, and what was next.`
595
+ };
596
+ };
597
+ /** A decision as Claude Code's settings-hook JSON output. */
598
+ const hookOutput = (decision) => decision.kind === "soft" ? {
599
+ decision: "block",
600
+ reason: decision.reason
601
+ } : {
602
+ continue: false,
603
+ stopReason: decision.stopReason
604
+ };
605
+ //#endregion
606
+ export { beatKindOf as A, ACCESS_REFUSAL as B, renderLandPlain as C, renderBeatJson as D, beatDocument as E, EXIT_RUNTIME_ERROR as F, WIRE_SCHEMA as H, EXIT_SIGINT as I, EXIT_SIGTERM as L, stallDiagnosis as M, EXIT_CODES as N, statusOf as O, EXIT_OK as P, EXIT_STRAYS_FOUND as R, renderLandJson as S, renderBeatPlain as T, renderFormat as U, UNATTRIBUTED_MODEL as V, selectPath as _, contextLimitFault as a, landProseText as b, hookOutput as c, transcriptTokens as d, usageTokens as f, resolveSession as g, globMatches as h, contextHook as i, demandOf as j, BEAT_KINDS as k, parseContextWindow as l, slicesDocument as m, CONTEXT_STOP_FILE_ENV as n, contextShapeFault as o, renderSlicesJson as p, DEFAULT_CONTEXT_LIMIT as r, contextWindowFor as s, CONTEXT_LIMIT_MARKER as t, subagentTranscriptPath as u, judgeJson as v, DEMAND_BRIEFING as w, noopText as x, landFields as y, EXIT_USAGE_ERROR as z };
package/dist/flows.mjs CHANGED
@@ -226,25 +226,21 @@ const checkScript = (command, options) => {
226
226
  ""
227
227
  ].join("\n");
228
228
  };
229
- /**
230
- * Reverse-apply `commit`'s changes to the working tree. A failure — the
231
- * patch no longer applies — writes `failure` and git's error to `report`,
232
- * since a failed revert and one with nothing to undo both leave the tree clean.
233
- */
234
- const revertScript = (commit, report, failure) => {
229
+ const patchScript = (commit, report, failure, reverse, exclude = []) => {
230
+ const paths = exclude.length === 0 ? "" : ` . ${exclude.map((p) => quote(`:(exclude,literal)${p}`)).join(" ")}`;
235
231
  const range = `${quote(`${commit}^`)} ${quote(commit)}`;
236
232
  const error = quote(`${report}.output`);
237
233
  return [
238
234
  "#!/usr/bin/env sh",
239
235
  "set +e",
240
- `if ! git diff --quiet ${range} --; then`,
236
+ `if ! git diff --quiet ${range} --${paths}; then`,
241
237
  ` mkdir -p "$(dirname ${quote(report)})"`,
242
- ` git diff --binary ${range} -- | git apply -R 2> ${error}`,
238
+ ` git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${range} --${paths} | git apply --whitespace=nowarn${reverse ? " -R" : ""} 2> ${error}`,
243
239
  " code=$?",
244
240
  " if [ \"$code\" -ne 0 ]; then",
245
241
  " {",
246
242
  ` printf '%s\\n\\n' ${quote(failure)}`,
247
- ` if [ -s ${error} ]; then cat ${error}; else printf 'Reverting it exited %s and produced no output.\\n' "$code"; fi`,
243
+ ` if [ -s ${error} ]; then cat ${error}; else printf '${reverse ? "Reverting" : "Applying"} it exited %s and produced no output.\\n' "$code"; fi`,
248
244
  ` } > ${quote(report)}`,
249
245
  " fi",
250
246
  ` rm -f ${error}`,
@@ -253,6 +249,14 @@ const revertScript = (commit, report, failure) => {
253
249
  ].join("\n");
254
250
  };
255
251
  /**
252
+ * Reverse-apply `commit`'s changes to the working tree. A failure — the
253
+ * patch no longer applies — writes `failure` and git's error to `report`,
254
+ * since a failed revert and one with nothing to undo both leave the tree clean.
255
+ */
256
+ const revertScript = (commit, report, failure) => patchScript(commit, report, failure, true);
257
+ /** Apply `commit`'s changes outside `exclude` to the working tree; a failure is reported as `revertScript`'s is. */
258
+ const applyScript = (commit, report, failure, exclude = []) => patchScript(commit, report, failure, false, exclude);
259
+ /**
256
260
  * Undo the edits one commit made to `paths`: each goes back to its content
257
261
  * before `commit` — or away, when `commit` added it — but only while it is
258
262
  * still exactly as `commit` left it. A path changed since is left alone.
@@ -620,6 +624,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
620
624
  accessFor: () => accessFor,
621
625
  agent: () => agent,
622
626
  answered: () => answered,
627
+ applyScript: () => applyScript,
623
628
  changes: () => changes,
624
629
  changesSince: () => changesSince,
625
630
  check: () => check,
@@ -673,4 +678,4 @@ var flows_exports = /* @__PURE__ */ __exportAll({
673
678
  wrote: () => wrote
674
679
  });
675
680
  //#endregion
676
- export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, displayPath, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, mark, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, slice, start, flows_exports as t, threads, vars, wrote };
681
+ export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, applyScript, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, displayPath, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, mark, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, slice, start, flows_exports as t, threads, vars, wrote };