@keepgoingdev/mcp-server 0.7.2 → 0.9.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/index.js CHANGED
@@ -6,6 +6,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
 
7
7
  // ../../packages/shared/src/session.ts
8
8
  import { randomUUID } from "crypto";
9
+
10
+ // ../../packages/shared/src/sanitize.ts
11
+ var AGENT_TAG_PATTERN = /<\/?(?:teammate-message|system-reminder|command-name|tool_use)\b[^>]*>/g;
12
+ function stripAgentTags(text) {
13
+ if (!text) return "";
14
+ const stripped = text.replace(AGENT_TAG_PATTERN, "");
15
+ return stripped.trim();
16
+ }
17
+
18
+ // ../../packages/shared/src/session.ts
9
19
  function generateCheckpointId() {
10
20
  return randomUUID();
11
21
  }
@@ -13,7 +23,10 @@ function createCheckpoint(fields) {
13
23
  return {
14
24
  id: generateCheckpointId(),
15
25
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16
- ...fields
26
+ ...fields,
27
+ summary: stripAgentTags(fields.summary),
28
+ nextStep: stripAgentTags(fields.nextStep),
29
+ blocker: fields.blocker ? stripAgentTags(fields.blocker) : fields.blocker
17
30
  };
18
31
  }
19
32
  function createDecisionRecord(fields) {
@@ -326,7 +339,8 @@ function generateEnrichedBriefing(opts) {
326
339
  timestamp: s.timestamp,
327
340
  summary: s.summary || "",
328
341
  nextStep: s.nextStep || "",
329
- branch: s.gitBranch
342
+ branch: s.gitBranch,
343
+ sessionPhase: s.sessionPhase
330
344
  }));
331
345
  }
332
346
  if (opts.recentCommits && opts.recentCommits.length > 0) {
@@ -359,10 +373,13 @@ function buildCurrentFocus(lastSession, projectState, gitBranch) {
359
373
  function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
360
374
  const parts = [];
361
375
  const sessionCount = recentSessions.length;
376
+ const planCount = recentSessions.filter((s) => s.sessionPhase === "planning").length;
362
377
  if (sessionCount > 1) {
363
- parts.push(`${sessionCount} recent sessions`);
378
+ const planSuffix = planCount > 0 ? ` (${planCount} plan-only)` : "";
379
+ parts.push(`${sessionCount} recent sessions${planSuffix}`);
364
380
  } else if (sessionCount === 1) {
365
- parts.push("1 recent session");
381
+ const planSuffix = planCount === 1 ? " (plan-only)" : "";
382
+ parts.push(`1 recent session${planSuffix}`);
366
383
  }
367
384
  if (lastSession.summary) {
368
385
  const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
@@ -561,7 +578,8 @@ function formatEnrichedBriefing(briefing) {
561
578
  for (const s of briefing.sessionHistory) {
562
579
  const relTime = formatRelativeTime(s.timestamp);
563
580
  const branch = s.branch ? ` (${s.branch})` : "";
564
- lines.push(`- **${relTime}${branch}:** ${s.summary || "No summary"}. Next: ${s.nextStep || "Not specified"}`);
581
+ const planTag = s.sessionPhase === "planning" ? " (plan)" : "";
582
+ lines.push(`- **${relTime}${branch}${planTag}:** ${s.summary || "No summary"}. Next: ${s.nextStep || "Not specified"}`);
565
583
  }
566
584
  }
567
585
  if (briefing.recentCommits && briefing.recentCommits.length > 0) {
@@ -706,22 +724,160 @@ function formatContinueOnPrompt(context, options) {
706
724
  return result;
707
725
  }
708
726
 
709
- // ../../packages/shared/src/storage.ts
710
- import fs2 from "fs";
711
- import path4 from "path";
712
- import { randomUUID as randomUUID2, createHash } from "crypto";
727
+ // ../../packages/shared/src/reader.ts
728
+ import fs4 from "fs";
729
+ import path6 from "path";
713
730
 
714
- // ../../packages/shared/src/registry.ts
731
+ // ../../packages/shared/src/license.ts
732
+ import crypto from "crypto";
715
733
  import fs from "fs";
716
734
  import os from "os";
717
735
  import path3 from "path";
718
- var KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
719
- var KNOWN_PROJECTS_FILE = path3.join(KEEPGOING_DIR, "known-projects.json");
736
+ var LICENSE_FILE = "license.json";
737
+ var DEVICE_ID_FILE = "device-id";
738
+ function getGlobalLicenseDir() {
739
+ return path3.join(os.homedir(), ".keepgoing");
740
+ }
741
+ function getGlobalLicensePath() {
742
+ return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
743
+ }
744
+ function getDeviceId() {
745
+ const dir = getGlobalLicenseDir();
746
+ const filePath = path3.join(dir, DEVICE_ID_FILE);
747
+ try {
748
+ const existing = fs.readFileSync(filePath, "utf-8").trim();
749
+ if (existing) return existing;
750
+ } catch {
751
+ }
752
+ const id = crypto.randomUUID();
753
+ if (!fs.existsSync(dir)) {
754
+ fs.mkdirSync(dir, { recursive: true });
755
+ }
756
+ fs.writeFileSync(filePath, id, "utf-8");
757
+ return id;
758
+ }
759
+ var DECISION_DETECTION_VARIANT_ID = 1361527;
760
+ var SESSION_AWARENESS_VARIANT_ID = 1366510;
761
+ var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
762
+ var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
763
+ var VARIANT_FEATURE_MAP = {
764
+ [DECISION_DETECTION_VARIANT_ID]: ["decisions"],
765
+ [SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
766
+ [TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
767
+ [TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
768
+ // Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
769
+ };
770
+ var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
771
+ function getVariantLabel(variantId) {
772
+ const features = VARIANT_FEATURE_MAP[variantId];
773
+ if (!features) return "Unknown Add-on";
774
+ if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
775
+ if (features.includes("decisions")) return "Decision Detection";
776
+ if (features.includes("session-awareness")) return "Session Awareness";
777
+ return "Pro Add-on";
778
+ }
779
+ var _cachedStore;
780
+ var _cacheTimestamp = 0;
781
+ var LICENSE_CACHE_TTL_MS = 2e3;
782
+ function readLicenseStore() {
783
+ const now = Date.now();
784
+ if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
785
+ return _cachedStore;
786
+ }
787
+ const licensePath = getGlobalLicensePath();
788
+ let store;
789
+ try {
790
+ if (!fs.existsSync(licensePath)) {
791
+ store = { version: 2, licenses: [] };
792
+ } else {
793
+ const raw = fs.readFileSync(licensePath, "utf-8");
794
+ const data = JSON.parse(raw);
795
+ if (data?.version === 2 && Array.isArray(data.licenses)) {
796
+ store = data;
797
+ } else {
798
+ store = { version: 2, licenses: [] };
799
+ }
800
+ }
801
+ } catch {
802
+ store = { version: 2, licenses: [] };
803
+ }
804
+ _cachedStore = store;
805
+ _cacheTimestamp = now;
806
+ return store;
807
+ }
808
+ function writeLicenseStore(store) {
809
+ const dirPath = getGlobalLicenseDir();
810
+ if (!fs.existsSync(dirPath)) {
811
+ fs.mkdirSync(dirPath, { recursive: true });
812
+ }
813
+ const licensePath = path3.join(dirPath, LICENSE_FILE);
814
+ fs.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
815
+ _cachedStore = store;
816
+ _cacheTimestamp = Date.now();
817
+ }
818
+ function addLicenseEntry(entry) {
819
+ const store = readLicenseStore();
820
+ const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
821
+ if (idx >= 0) {
822
+ store.licenses[idx] = entry;
823
+ } else {
824
+ store.licenses.push(entry);
825
+ }
826
+ writeLicenseStore(store);
827
+ }
828
+ function removeLicenseEntry(licenseKey) {
829
+ const store = readLicenseStore();
830
+ store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
831
+ writeLicenseStore(store);
832
+ }
833
+ function getActiveLicenses() {
834
+ return readLicenseStore().licenses.filter((l) => l.status === "active");
835
+ }
836
+ function getLicenseForFeature(feature) {
837
+ const active = getActiveLicenses();
838
+ return active.find((l) => {
839
+ const features = VARIANT_FEATURE_MAP[l.variantId];
840
+ return features?.includes(feature);
841
+ });
842
+ }
843
+ function getAllLicensesNeedingRevalidation() {
844
+ return getActiveLicenses().filter((l) => needsRevalidation(l));
845
+ }
846
+ var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
847
+ function needsRevalidation(entry) {
848
+ const lastValidated = new Date(entry.lastValidatedAt).getTime();
849
+ return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
850
+ }
851
+
852
+ // ../../packages/shared/src/storage.ts
853
+ import fs3 from "fs";
854
+ import path5 from "path";
855
+ import { randomUUID as randomUUID2, createHash } from "crypto";
856
+
857
+ // ../../packages/shared/src/registry.ts
858
+ import fs2 from "fs";
859
+ import os2 from "os";
860
+ import path4 from "path";
861
+ var KEEPGOING_DIR = path4.join(os2.homedir(), ".keepgoing");
862
+ var KNOWN_PROJECTS_FILE = path4.join(KEEPGOING_DIR, "known-projects.json");
863
+ var TRAY_CONFIG_FILE = path4.join(KEEPGOING_DIR, "tray-config.json");
720
864
  var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
865
+ function readTrayConfigProjects() {
866
+ try {
867
+ if (fs2.existsSync(TRAY_CONFIG_FILE)) {
868
+ const raw = JSON.parse(fs2.readFileSync(TRAY_CONFIG_FILE, "utf-8"));
869
+ if (raw && Array.isArray(raw.projects)) {
870
+ return raw.projects.filter((p) => typeof p === "string");
871
+ }
872
+ }
873
+ } catch {
874
+ }
875
+ return [];
876
+ }
721
877
  function readKnownProjects() {
722
878
  try {
723
- if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
724
- const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
879
+ if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
880
+ const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
725
881
  if (raw && Array.isArray(raw.projects)) {
726
882
  return raw;
727
883
  }
@@ -731,18 +887,18 @@ function readKnownProjects() {
731
887
  return { version: 1, projects: [] };
732
888
  }
733
889
  function writeKnownProjects(data) {
734
- if (!fs.existsSync(KEEPGOING_DIR)) {
735
- fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
890
+ if (!fs2.existsSync(KEEPGOING_DIR)) {
891
+ fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
736
892
  }
737
893
  const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
738
- fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
739
- fs.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
894
+ fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
895
+ fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
740
896
  }
741
897
  function registerProject(projectPath, projectName) {
742
898
  try {
743
899
  const data = readKnownProjects();
744
900
  const now = (/* @__PURE__ */ new Date()).toISOString();
745
- const name = projectName || path3.basename(projectPath);
901
+ const name = projectName || path4.basename(projectPath);
746
902
  const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
747
903
  if (existingIdx >= 0) {
748
904
  data.projects[existingIdx].lastSeen = now;
@@ -781,23 +937,23 @@ var KeepGoingWriter = class {
781
937
  currentTasksFilePath;
782
938
  constructor(workspacePath) {
783
939
  const mainRoot = resolveStorageRoot(workspacePath);
784
- this.storagePath = path4.join(mainRoot, STORAGE_DIR);
785
- this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
786
- this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
787
- this.metaFilePath = path4.join(this.storagePath, META_FILE);
788
- this.currentTasksFilePath = path4.join(this.storagePath, CURRENT_TASKS_FILE);
940
+ this.storagePath = path5.join(mainRoot, STORAGE_DIR);
941
+ this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE);
942
+ this.stateFilePath = path5.join(this.storagePath, STATE_FILE);
943
+ this.metaFilePath = path5.join(this.storagePath, META_FILE);
944
+ this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE);
789
945
  }
790
946
  ensureDir() {
791
- if (!fs2.existsSync(this.storagePath)) {
792
- fs2.mkdirSync(this.storagePath, { recursive: true });
947
+ if (!fs3.existsSync(this.storagePath)) {
948
+ fs3.mkdirSync(this.storagePath, { recursive: true });
793
949
  }
794
950
  }
795
951
  saveCheckpoint(checkpoint, projectName) {
796
952
  this.ensureDir();
797
953
  let sessionsData;
798
954
  try {
799
- if (fs2.existsSync(this.sessionsFilePath)) {
800
- const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
955
+ if (fs3.existsSync(this.sessionsFilePath)) {
956
+ const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
801
957
  if (Array.isArray(raw)) {
802
958
  sessionsData = { version: 1, project: projectName, sessions: raw };
803
959
  } else {
@@ -815,13 +971,13 @@ var KeepGoingWriter = class {
815
971
  if (sessionsData.sessions.length > MAX_SESSIONS) {
816
972
  sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
817
973
  }
818
- fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
974
+ fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
819
975
  const state = {
820
976
  lastSessionId: checkpoint.id,
821
977
  lastKnownBranch: checkpoint.gitBranch,
822
978
  lastActivityAt: checkpoint.timestamp
823
979
  };
824
- fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
980
+ fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
825
981
  this.updateMeta(checkpoint.timestamp);
826
982
  const mainRoot = resolveStorageRoot(this.storagePath);
827
983
  registerProject(mainRoot, projectName);
@@ -829,8 +985,8 @@ var KeepGoingWriter = class {
829
985
  updateMeta(timestamp) {
830
986
  let meta;
831
987
  try {
832
- if (fs2.existsSync(this.metaFilePath)) {
833
- meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
988
+ if (fs3.existsSync(this.metaFilePath)) {
989
+ meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
834
990
  meta.lastUpdated = timestamp;
835
991
  } else {
836
992
  meta = {
@@ -846,7 +1002,28 @@ var KeepGoingWriter = class {
846
1002
  lastUpdated: timestamp
847
1003
  };
848
1004
  }
849
- fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
1005
+ fs3.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
1006
+ }
1007
+ // ---------------------------------------------------------------------------
1008
+ // Activity signals API
1009
+ // ---------------------------------------------------------------------------
1010
+ /**
1011
+ * Writes activity signals to state.json for momentum computation.
1012
+ * Performs a shallow merge: provided fields overwrite existing ones,
1013
+ * fields not provided are preserved.
1014
+ */
1015
+ writeActivitySignal(signal) {
1016
+ this.ensureDir();
1017
+ let state = {};
1018
+ try {
1019
+ if (fs3.existsSync(this.stateFilePath)) {
1020
+ state = JSON.parse(fs3.readFileSync(this.stateFilePath, "utf-8"));
1021
+ }
1022
+ } catch {
1023
+ state = {};
1024
+ }
1025
+ state.activitySignals = { ...state.activitySignals, ...signal };
1026
+ fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
850
1027
  }
851
1028
  // ---------------------------------------------------------------------------
852
1029
  // Multi-session API
@@ -854,8 +1031,8 @@ var KeepGoingWriter = class {
854
1031
  /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
855
1032
  readCurrentTasks() {
856
1033
  try {
857
- if (fs2.existsSync(this.currentTasksFilePath)) {
858
- const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
1034
+ if (fs3.existsSync(this.currentTasksFilePath)) {
1035
+ const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
859
1036
  const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
860
1037
  return this.pruneStale(tasks);
861
1038
  }
@@ -876,6 +1053,8 @@ var KeepGoingWriter = class {
876
1053
  /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
877
1054
  upsertSessionCore(update) {
878
1055
  this.ensureDir();
1056
+ if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
1057
+ if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
879
1058
  const sessionId = update.sessionId || generateSessionId(update);
880
1059
  const tasks = this.readAllTasksRaw();
881
1060
  const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
@@ -910,8 +1089,8 @@ var KeepGoingWriter = class {
910
1089
  // ---------------------------------------------------------------------------
911
1090
  readAllTasksRaw() {
912
1091
  try {
913
- if (fs2.existsSync(this.currentTasksFilePath)) {
914
- const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
1092
+ if (fs3.existsSync(this.currentTasksFilePath)) {
1093
+ const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
915
1094
  return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
916
1095
  }
917
1096
  } catch {
@@ -923,7 +1102,7 @@ var KeepGoingWriter = class {
923
1102
  }
924
1103
  writeTasksFile(tasks) {
925
1104
  const data = { version: 1, tasks };
926
- fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
1105
+ fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
927
1106
  }
928
1107
  };
929
1108
  function generateSessionId(context) {
@@ -939,57 +1118,337 @@ function generateSessionId(context) {
939
1118
  return `ses_${hash}`;
940
1119
  }
941
1120
 
942
- // ../../packages/shared/src/smartSummary.ts
943
- var PREFIX_VERBS = {
944
- feat: "Added",
945
- fix: "Fixed",
946
- refactor: "Refactored",
947
- docs: "Updated docs for",
948
- test: "Added tests for",
949
- chore: "Updated",
950
- style: "Styled",
951
- perf: "Optimized",
952
- ci: "Updated CI for",
953
- build: "Updated build for",
954
- revert: "Reverted"
955
- };
956
- var NOISE_PATTERNS = [
957
- "node_modules",
958
- "package-lock.json",
959
- "yarn.lock",
960
- "pnpm-lock.yaml",
961
- ".gitignore",
962
- ".DS_Store",
963
- "dist/",
964
- "out/",
965
- "build/"
966
- ];
967
- function categorizeCommits(messages) {
968
- const groups = /* @__PURE__ */ new Map();
969
- for (const msg of messages) {
970
- const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
971
- if (match) {
972
- const prefix = match[1].toLowerCase();
973
- const body = match[2].trim();
974
- if (!groups.has(prefix)) {
975
- groups.set(prefix, []);
1121
+ // ../../packages/shared/src/reader.ts
1122
+ var STORAGE_DIR2 = ".keepgoing";
1123
+ var META_FILE2 = "meta.json";
1124
+ var SESSIONS_FILE2 = "sessions.json";
1125
+ var DECISIONS_FILE = "decisions.json";
1126
+ var STATE_FILE2 = "state.json";
1127
+ var CURRENT_TASKS_FILE2 = "current-tasks.json";
1128
+ var KeepGoingReader = class {
1129
+ workspacePath;
1130
+ storagePath;
1131
+ metaFilePath;
1132
+ sessionsFilePath;
1133
+ decisionsFilePath;
1134
+ stateFilePath;
1135
+ currentTasksFilePath;
1136
+ _isWorktree;
1137
+ _cachedBranch = null;
1138
+ // null = not yet resolved
1139
+ constructor(workspacePath) {
1140
+ this.workspacePath = workspacePath;
1141
+ const mainRoot = resolveStorageRoot(workspacePath);
1142
+ this._isWorktree = mainRoot !== workspacePath;
1143
+ this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1144
+ this.metaFilePath = path6.join(this.storagePath, META_FILE2);
1145
+ this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
1146
+ this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1147
+ this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
1148
+ this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
1149
+ }
1150
+ /** Check if .keepgoing/ directory exists. */
1151
+ exists() {
1152
+ return fs4.existsSync(this.storagePath);
1153
+ }
1154
+ /** Read state.json, returns undefined if missing or corrupt. */
1155
+ getState() {
1156
+ return this.readJsonFile(this.stateFilePath);
1157
+ }
1158
+ /** Read meta.json, returns undefined if missing or corrupt. */
1159
+ getMeta() {
1160
+ return this.readJsonFile(this.metaFilePath);
1161
+ }
1162
+ /**
1163
+ * Read sessions from sessions.json.
1164
+ * Handles both formats:
1165
+ * - Flat array: SessionCheckpoint[] (from ProjectStorage)
1166
+ * - Wrapper object: ProjectSessions (from SessionStorage)
1167
+ */
1168
+ getSessions() {
1169
+ return this.parseSessions().sessions;
1170
+ }
1171
+ /**
1172
+ * Get the most recent session checkpoint.
1173
+ * Uses state.lastSessionId if available, falls back to last in array.
1174
+ */
1175
+ getLastSession() {
1176
+ const { sessions, wrapperLastSessionId } = this.parseSessions();
1177
+ if (sessions.length === 0) {
1178
+ return void 0;
1179
+ }
1180
+ const state = this.getState();
1181
+ if (state?.lastSessionId) {
1182
+ const found = sessions.find((s) => s.id === state.lastSessionId);
1183
+ if (found) {
1184
+ return found;
976
1185
  }
977
- groups.get(prefix).push(body);
978
- } else {
979
- if (!groups.has("other")) {
980
- groups.set("other", []);
1186
+ }
1187
+ if (wrapperLastSessionId) {
1188
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
1189
+ if (found) {
1190
+ return found;
981
1191
  }
982
- groups.get("other").push(msg.trim());
983
1192
  }
1193
+ return sessions[sessions.length - 1];
984
1194
  }
985
- return groups;
986
- }
987
- function inferWorkAreas(files) {
988
- const areas = /* @__PURE__ */ new Map();
989
- for (const file of files) {
990
- if (NOISE_PATTERNS.some((p) => file.includes(p))) {
991
- continue;
992
- }
1195
+ /**
1196
+ * Returns the last N sessions, newest first.
1197
+ */
1198
+ getRecentSessions(count) {
1199
+ return getRecentSessions(this.getSessions(), count);
1200
+ }
1201
+ /** Read all decisions from decisions.json. */
1202
+ getDecisions() {
1203
+ return this.parseDecisions().decisions;
1204
+ }
1205
+ /** Returns the last N decisions, newest first. */
1206
+ getRecentDecisions(count) {
1207
+ const all = this.getDecisions();
1208
+ return all.slice(-count).reverse();
1209
+ }
1210
+ /** Read the multi-license store from `~/.keepgoing/license.json`. */
1211
+ getLicenseStore() {
1212
+ return readLicenseStore();
1213
+ }
1214
+ /**
1215
+ * Read all current tasks from current-tasks.json.
1216
+ * Automatically filters out stale finished sessions (> 2 hours).
1217
+ */
1218
+ getCurrentTasks() {
1219
+ const multiRaw = this.readJsonFile(this.currentTasksFilePath);
1220
+ if (multiRaw) {
1221
+ const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
1222
+ return this.pruneStale(tasks);
1223
+ }
1224
+ return [];
1225
+ }
1226
+ /** Get only active sessions (sessionActive=true and within stale threshold). */
1227
+ getActiveTasks() {
1228
+ return this.getCurrentTasks().filter((t) => t.sessionActive);
1229
+ }
1230
+ /** Get a specific session by ID. */
1231
+ getTaskBySessionId(sessionId) {
1232
+ return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
1233
+ }
1234
+ /**
1235
+ * Detect files being edited by multiple sessions simultaneously.
1236
+ * Returns pairs of session IDs and the conflicting file paths.
1237
+ */
1238
+ detectFileConflicts() {
1239
+ const activeTasks = this.getActiveTasks();
1240
+ if (activeTasks.length < 2) return [];
1241
+ const fileToSessions = /* @__PURE__ */ new Map();
1242
+ for (const task of activeTasks) {
1243
+ if (task.lastFileEdited && task.sessionId) {
1244
+ const existing = fileToSessions.get(task.lastFileEdited) ?? [];
1245
+ existing.push({
1246
+ sessionId: task.sessionId,
1247
+ agentLabel: task.agentLabel,
1248
+ branch: task.branch
1249
+ });
1250
+ fileToSessions.set(task.lastFileEdited, existing);
1251
+ }
1252
+ }
1253
+ const conflicts = [];
1254
+ for (const [file, sessions] of fileToSessions) {
1255
+ if (sessions.length > 1) {
1256
+ conflicts.push({ file, sessions });
1257
+ }
1258
+ }
1259
+ return conflicts;
1260
+ }
1261
+ /**
1262
+ * Detect sessions on the same branch (possible duplicate work).
1263
+ */
1264
+ detectBranchOverlap() {
1265
+ const activeTasks = this.getActiveTasks();
1266
+ if (activeTasks.length < 2) return [];
1267
+ const branchToSessions = /* @__PURE__ */ new Map();
1268
+ for (const task of activeTasks) {
1269
+ if (task.branch && task.sessionId) {
1270
+ const existing = branchToSessions.get(task.branch) ?? [];
1271
+ existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
1272
+ branchToSessions.set(task.branch, existing);
1273
+ }
1274
+ }
1275
+ const overlaps = [];
1276
+ for (const [branch, sessions] of branchToSessions) {
1277
+ if (sessions.length > 1) {
1278
+ overlaps.push({ branch, sessions });
1279
+ }
1280
+ }
1281
+ return overlaps;
1282
+ }
1283
+ pruneStale(tasks) {
1284
+ return pruneStaleTasks(tasks);
1285
+ }
1286
+ /** Get the last session checkpoint for a specific branch. */
1287
+ getLastSessionForBranch(branch) {
1288
+ const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
1289
+ return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1290
+ }
1291
+ /** Returns the last N sessions for a specific branch, newest first. */
1292
+ getRecentSessionsForBranch(branch, count) {
1293
+ const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1294
+ return filtered.slice(-count).reverse();
1295
+ }
1296
+ /** Returns the last N decisions for a specific branch, newest first. */
1297
+ getRecentDecisionsForBranch(branch, count) {
1298
+ const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1299
+ return filtered.slice(-count).reverse();
1300
+ }
1301
+ /** Whether the workspace is inside a git worktree. */
1302
+ get isWorktree() {
1303
+ return this._isWorktree;
1304
+ }
1305
+ /**
1306
+ * Returns the current git branch for this workspace.
1307
+ * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1308
+ */
1309
+ getCurrentBranch() {
1310
+ if (this._cachedBranch === null) {
1311
+ this._cachedBranch = getCurrentBranch(this.workspacePath);
1312
+ }
1313
+ return this._cachedBranch;
1314
+ }
1315
+ /**
1316
+ * Worktree-aware last session lookup.
1317
+ * In a worktree, scopes to the current branch with fallback to global.
1318
+ * Returns the session and whether it fell back to global.
1319
+ */
1320
+ getScopedLastSession() {
1321
+ const branch = this.getCurrentBranch();
1322
+ if (this._isWorktree && branch) {
1323
+ const scoped = this.getLastSessionForBranch(branch);
1324
+ if (scoped) return { session: scoped, isFallback: false };
1325
+ return { session: this.getLastSession(), isFallback: true };
1326
+ }
1327
+ return { session: this.getLastSession(), isFallback: false };
1328
+ }
1329
+ /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1330
+ getScopedRecentSessions(count) {
1331
+ const branch = this.getCurrentBranch();
1332
+ if (this._isWorktree && branch) {
1333
+ return this.getRecentSessionsForBranch(branch, count);
1334
+ }
1335
+ return this.getRecentSessions(count);
1336
+ }
1337
+ /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1338
+ getScopedRecentDecisions(count) {
1339
+ const branch = this.getCurrentBranch();
1340
+ if (this._isWorktree && branch) {
1341
+ return this.getRecentDecisionsForBranch(branch, count);
1342
+ }
1343
+ return this.getRecentDecisions(count);
1344
+ }
1345
+ /**
1346
+ * Resolves branch scope from an explicit `branch` parameter.
1347
+ * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1348
+ * - `"all"` returns no filter.
1349
+ * - An explicit branch name uses that.
1350
+ * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1351
+ */
1352
+ resolveBranchScope(branch) {
1353
+ if (branch === "all") {
1354
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1355
+ }
1356
+ if (branch) {
1357
+ return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1358
+ }
1359
+ const currentBranch = this.getCurrentBranch();
1360
+ if (this._isWorktree && currentBranch) {
1361
+ return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
1362
+ }
1363
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1364
+ }
1365
+ /**
1366
+ * Parses sessions.json once, returning both the session list
1367
+ * and the optional lastSessionId from a ProjectSessions wrapper.
1368
+ */
1369
+ parseSessions() {
1370
+ const raw = this.readJsonFile(
1371
+ this.sessionsFilePath
1372
+ );
1373
+ if (!raw) {
1374
+ return { sessions: [] };
1375
+ }
1376
+ if (Array.isArray(raw)) {
1377
+ return { sessions: raw };
1378
+ }
1379
+ return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
1380
+ }
1381
+ parseDecisions() {
1382
+ const raw = this.readJsonFile(this.decisionsFilePath);
1383
+ if (!raw) {
1384
+ return { decisions: [] };
1385
+ }
1386
+ return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
1387
+ }
1388
+ readJsonFile(filePath) {
1389
+ try {
1390
+ if (!fs4.existsSync(filePath)) {
1391
+ return void 0;
1392
+ }
1393
+ const raw = fs4.readFileSync(filePath, "utf-8");
1394
+ return JSON.parse(raw);
1395
+ } catch {
1396
+ return void 0;
1397
+ }
1398
+ }
1399
+ };
1400
+
1401
+ // ../../packages/shared/src/smartSummary.ts
1402
+ var PREFIX_VERBS = {
1403
+ feat: "Added",
1404
+ fix: "Fixed",
1405
+ refactor: "Refactored",
1406
+ docs: "Updated docs for",
1407
+ test: "Added tests for",
1408
+ chore: "Updated",
1409
+ style: "Styled",
1410
+ perf: "Optimized",
1411
+ ci: "Updated CI for",
1412
+ build: "Updated build for",
1413
+ revert: "Reverted"
1414
+ };
1415
+ var NOISE_PATTERNS = [
1416
+ "node_modules",
1417
+ "package-lock.json",
1418
+ "yarn.lock",
1419
+ "pnpm-lock.yaml",
1420
+ ".gitignore",
1421
+ ".DS_Store",
1422
+ "dist/",
1423
+ "out/",
1424
+ "build/"
1425
+ ];
1426
+ function categorizeCommits(messages) {
1427
+ const groups = /* @__PURE__ */ new Map();
1428
+ for (const msg of messages) {
1429
+ const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
1430
+ if (match) {
1431
+ const prefix = match[1].toLowerCase();
1432
+ const body = match[2].trim();
1433
+ if (!groups.has(prefix)) {
1434
+ groups.set(prefix, []);
1435
+ }
1436
+ groups.get(prefix).push(body);
1437
+ } else {
1438
+ if (!groups.has("other")) {
1439
+ groups.set("other", []);
1440
+ }
1441
+ groups.get("other").push(msg.trim());
1442
+ }
1443
+ }
1444
+ return groups;
1445
+ }
1446
+ function inferWorkAreas(files) {
1447
+ const areas = /* @__PURE__ */ new Map();
1448
+ for (const file of files) {
1449
+ if (NOISE_PATTERNS.some((p) => file.includes(p))) {
1450
+ continue;
1451
+ }
993
1452
  const parts = file.split("/").filter(Boolean);
994
1453
  let area;
995
1454
  if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
@@ -1101,131 +1560,222 @@ function capitalize(s) {
1101
1560
  return s.charAt(0).toUpperCase() + s.slice(1);
1102
1561
  }
1103
1562
 
1104
- // ../../packages/shared/src/decisionStorage.ts
1105
- import fs4 from "fs";
1106
- import path6 from "path";
1107
-
1108
- // ../../packages/shared/src/license.ts
1109
- import crypto from "crypto";
1110
- import fs3 from "fs";
1111
- import os2 from "os";
1112
- import path5 from "path";
1113
- var LICENSE_FILE = "license.json";
1114
- var DEVICE_ID_FILE = "device-id";
1115
- function getGlobalLicenseDir() {
1116
- return path5.join(os2.homedir(), ".keepgoing");
1117
- }
1118
- function getGlobalLicensePath() {
1119
- return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
1563
+ // ../../packages/shared/src/contextSnapshot.ts
1564
+ import fs5 from "fs";
1565
+ import path7 from "path";
1566
+ function formatCompactRelativeTime(date) {
1567
+ const diffMs = Date.now() - date.getTime();
1568
+ if (isNaN(diffMs)) return "?";
1569
+ if (diffMs < 0) return "now";
1570
+ const seconds = Math.floor(diffMs / 1e3);
1571
+ const minutes = Math.floor(seconds / 60);
1572
+ const hours = Math.floor(minutes / 60);
1573
+ const days = Math.floor(hours / 24);
1574
+ const weeks = Math.floor(days / 7);
1575
+ if (seconds < 60) return "now";
1576
+ if (minutes < 60) return `${minutes}m ago`;
1577
+ if (hours < 24) return `${hours}h ago`;
1578
+ if (days < 7) return `${days}d ago`;
1579
+ return `${weeks}w ago`;
1120
1580
  }
1121
- function getDeviceId() {
1122
- const dir = getGlobalLicenseDir();
1123
- const filePath = path5.join(dir, DEVICE_ID_FILE);
1124
- try {
1125
- const existing = fs3.readFileSync(filePath, "utf-8").trim();
1126
- if (existing) return existing;
1127
- } catch {
1581
+ function computeMomentum(timestamp, signals) {
1582
+ if (signals) {
1583
+ if (signals.lastGitOpAt) {
1584
+ const gitOpDiffMs = Date.now() - new Date(signals.lastGitOpAt).getTime();
1585
+ if (!isNaN(gitOpDiffMs) && gitOpDiffMs >= 0 && gitOpDiffMs < 30 * 60 * 1e3) {
1586
+ return "hot";
1587
+ }
1588
+ }
1589
+ if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
1590
+ return "hot";
1591
+ }
1128
1592
  }
1129
- const id = crypto.randomUUID();
1130
- if (!fs3.existsSync(dir)) {
1131
- fs3.mkdirSync(dir, { recursive: true });
1593
+ const diffMs = Date.now() - new Date(timestamp).getTime();
1594
+ if (isNaN(diffMs) || diffMs < 0) return "hot";
1595
+ const minutes = diffMs / (1e3 * 60);
1596
+ if (minutes < 30) return "hot";
1597
+ if (minutes < 240) return "warm";
1598
+ return "cold";
1599
+ }
1600
+ function inferFocusFromBranch2(branch) {
1601
+ if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
1602
+ return void 0;
1132
1603
  }
1133
- fs3.writeFileSync(filePath, id, "utf-8");
1134
- return id;
1604
+ const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
1605
+ const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
1606
+ const stripped = branch.replace(prefixPattern, "");
1607
+ const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
1608
+ if (!cleaned) return void 0;
1609
+ return isFix ? `${cleaned} fix` : cleaned;
1135
1610
  }
1136
- var DECISION_DETECTION_VARIANT_ID = 1361527;
1137
- var SESSION_AWARENESS_VARIANT_ID = 1366510;
1138
- var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
1139
- var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
1140
- var VARIANT_FEATURE_MAP = {
1141
- [DECISION_DETECTION_VARIANT_ID]: ["decisions"],
1142
- [SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
1143
- [TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
1144
- [TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
1145
- // Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
1146
- };
1147
- var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
1148
- function getVariantLabel(variantId) {
1149
- const features = VARIANT_FEATURE_MAP[variantId];
1150
- if (!features) return "Unknown Add-on";
1151
- if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
1152
- if (features.includes("decisions")) return "Decision Detection";
1153
- if (features.includes("session-awareness")) return "Session Awareness";
1154
- return "Pro Add-on";
1611
+ function cleanCommitMessage(message, maxLen = 60) {
1612
+ if (!message) return "";
1613
+ const match = message.match(/^(?:\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
1614
+ const body = match ? match[1].trim() : message.trim();
1615
+ if (!body) return "";
1616
+ const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
1617
+ if (capitalized.length <= maxLen) return capitalized;
1618
+ return capitalized.slice(0, maxLen - 3) + "...";
1619
+ }
1620
+ function buildDoing(checkpoint, branch, recentCommitMessages) {
1621
+ if (checkpoint?.summary) return checkpoint.summary;
1622
+ const branchFocus = inferFocusFromBranch2(branch ?? checkpoint?.gitBranch);
1623
+ if (branchFocus) return branchFocus;
1624
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
1625
+ const cleaned = cleanCommitMessage(recentCommitMessages[0]);
1626
+ if (cleaned) return cleaned;
1627
+ }
1628
+ return "Unknown";
1155
1629
  }
1156
- var _cachedStore;
1157
- var _cacheTimestamp = 0;
1158
- var LICENSE_CACHE_TTL_MS = 2e3;
1159
- function readLicenseStore() {
1160
- const now = Date.now();
1161
- if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
1162
- return _cachedStore;
1630
+ function buildWhere(checkpoint, branch) {
1631
+ const effectiveBranch = branch ?? checkpoint?.gitBranch;
1632
+ const parts = [];
1633
+ if (effectiveBranch) {
1634
+ parts.push(effectiveBranch);
1163
1635
  }
1164
- const licensePath = getGlobalLicensePath();
1165
- let store;
1166
- try {
1167
- if (!fs3.existsSync(licensePath)) {
1168
- store = { version: 2, licenses: [] };
1636
+ if (checkpoint?.touchedFiles && checkpoint.touchedFiles.length > 0) {
1637
+ const fileNames = checkpoint.touchedFiles.slice(0, 2).map((f) => {
1638
+ const segments = f.replace(/\\/g, "/").split("/");
1639
+ return segments[segments.length - 1];
1640
+ });
1641
+ parts.push(fileNames.join(", "));
1642
+ }
1643
+ return parts.join(" \xB7 ") || "unknown";
1644
+ }
1645
+ function detectFocusArea(files) {
1646
+ if (!files || files.length === 0) return void 0;
1647
+ const areas = inferWorkAreas(files);
1648
+ if (areas.length === 0) return void 0;
1649
+ if (areas.length === 1) return areas[0];
1650
+ const topArea = areas[0];
1651
+ const areaCounts = /* @__PURE__ */ new Map();
1652
+ for (const file of files) {
1653
+ const parts = file.split("/").filter(Boolean);
1654
+ let area;
1655
+ if (parts.length <= 1) {
1656
+ area = "root";
1657
+ } else if (parts[0] === "apps" || parts[0] === "packages") {
1658
+ area = parts.length > 1 ? parts[1] : parts[0];
1659
+ } else if (parts[0] === "src") {
1660
+ area = parts.length > 1 ? parts[1] : "src";
1169
1661
  } else {
1170
- const raw = fs3.readFileSync(licensePath, "utf-8");
1171
- const data = JSON.parse(raw);
1172
- if (data?.version === 2 && Array.isArray(data.licenses)) {
1173
- store = data;
1174
- } else {
1175
- store = { version: 2, licenses: [] };
1176
- }
1662
+ area = parts[0];
1177
1663
  }
1178
- } catch {
1179
- store = { version: 2, licenses: [] };
1664
+ areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
1180
1665
  }
1181
- _cachedStore = store;
1182
- _cacheTimestamp = now;
1183
- return store;
1184
- }
1185
- function writeLicenseStore(store) {
1186
- const dirPath = getGlobalLicenseDir();
1187
- if (!fs3.existsSync(dirPath)) {
1188
- fs3.mkdirSync(dirPath, { recursive: true });
1666
+ let topCount = 0;
1667
+ for (const [areaKey, count] of areaCounts) {
1668
+ if (topArea.toLowerCase().includes(areaKey.toLowerCase()) || areaKey.toLowerCase().includes(topArea.toLowerCase().split(" ")[0])) {
1669
+ topCount = Math.max(topCount, count);
1670
+ }
1189
1671
  }
1190
- const licensePath = path5.join(dirPath, LICENSE_FILE);
1191
- fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
1192
- _cachedStore = store;
1193
- _cacheTimestamp = Date.now();
1194
- }
1195
- function addLicenseEntry(entry) {
1196
- const store = readLicenseStore();
1197
- const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
1198
- if (idx >= 0) {
1199
- store.licenses[idx] = entry;
1200
- } else {
1201
- store.licenses.push(entry);
1672
+ if (topCount === 0) {
1673
+ topCount = Math.max(...areaCounts.values());
1202
1674
  }
1203
- writeLicenseStore(store);
1204
- }
1205
- function removeLicenseEntry(licenseKey) {
1206
- const store = readLicenseStore();
1207
- store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
1208
- writeLicenseStore(store);
1209
- }
1210
- function getActiveLicenses() {
1211
- return readLicenseStore().licenses.filter((l) => l.status === "active");
1675
+ const ratio = topCount / files.length;
1676
+ if (ratio >= 0.6) return topArea;
1677
+ return void 0;
1212
1678
  }
1213
- function getLicenseForFeature(feature) {
1214
- const active = getActiveLicenses();
1215
- return active.find((l) => {
1216
- const features = VARIANT_FEATURE_MAP[l.variantId];
1217
- return features?.includes(feature);
1679
+ function generateContextSnapshot(projectRoot) {
1680
+ const reader = new KeepGoingReader(projectRoot);
1681
+ if (!reader.exists()) return null;
1682
+ const lastSession = reader.getLastSession();
1683
+ const state = reader.getState();
1684
+ if (!lastSession && !state) return null;
1685
+ const timestamp = state?.lastActivityAt ?? lastSession?.timestamp;
1686
+ if (!timestamp) return null;
1687
+ const branch = state?.lastKnownBranch ?? lastSession?.gitBranch;
1688
+ let activeAgents = 0;
1689
+ try {
1690
+ const activeTasks = reader.getActiveTasks();
1691
+ activeAgents = activeTasks.length;
1692
+ } catch {
1693
+ }
1694
+ const doing = buildDoing(lastSession, branch);
1695
+ const next = lastSession?.nextStep ?? "";
1696
+ const where = buildWhere(lastSession, branch);
1697
+ const when = formatCompactRelativeTime(new Date(timestamp));
1698
+ const momentum = computeMomentum(timestamp, state?.activitySignals);
1699
+ const blocker = lastSession?.blocker;
1700
+ const snapshot = {
1701
+ doing,
1702
+ next,
1703
+ where,
1704
+ when,
1705
+ momentum,
1706
+ lastActivityAt: timestamp
1707
+ };
1708
+ if (blocker) snapshot.blocker = blocker;
1709
+ if (activeAgents > 0) snapshot.activeAgents = activeAgents;
1710
+ const focusArea = detectFocusArea(lastSession?.touchedFiles ?? []);
1711
+ if (focusArea) snapshot.focusArea = focusArea;
1712
+ return snapshot;
1713
+ }
1714
+ function formatCrossProjectLine(entry) {
1715
+ const s = entry.snapshot;
1716
+ const icon = s.momentum === "hot" ? "\u26A1" : s.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
1717
+ const parts = [];
1718
+ parts.push(`${icon} ${entry.name}: ${s.doing}`);
1719
+ if (s.next) {
1720
+ parts.push(`\u2192 ${s.next}`);
1721
+ }
1722
+ let line = parts.join(" ");
1723
+ line += ` (${s.when})`;
1724
+ if (s.blocker) {
1725
+ line += ` \u26D4 ${s.blocker}`;
1726
+ }
1727
+ return line;
1728
+ }
1729
+ var MOMENTUM_RANK = { hot: 0, warm: 1, cold: 2 };
1730
+ function generateCrossProjectSummary() {
1731
+ const registry = readKnownProjects();
1732
+ const trayPaths = readTrayConfigProjects();
1733
+ const seenPaths = new Set(registry.projects.map((p) => p.path));
1734
+ const allEntries = [...registry.projects];
1735
+ for (const tp of trayPaths) {
1736
+ if (!seenPaths.has(tp)) {
1737
+ seenPaths.add(tp);
1738
+ allEntries.push({ path: tp, name: path7.basename(tp) });
1739
+ }
1740
+ }
1741
+ const projects = [];
1742
+ const seenRoots = /* @__PURE__ */ new Set();
1743
+ for (const entry of allEntries) {
1744
+ if (!fs5.existsSync(entry.path)) continue;
1745
+ const snapshot = generateContextSnapshot(entry.path);
1746
+ if (!snapshot) continue;
1747
+ let resolvedRoot;
1748
+ try {
1749
+ resolvedRoot = fs5.realpathSync(entry.path);
1750
+ } catch {
1751
+ resolvedRoot = entry.path;
1752
+ }
1753
+ if (seenRoots.has(resolvedRoot)) continue;
1754
+ seenRoots.add(resolvedRoot);
1755
+ projects.push({
1756
+ name: entry.name,
1757
+ path: entry.path,
1758
+ snapshot
1759
+ });
1760
+ }
1761
+ projects.sort((a, b) => {
1762
+ const rankA = MOMENTUM_RANK[a.snapshot.momentum ?? "cold"];
1763
+ const rankB = MOMENTUM_RANK[b.snapshot.momentum ?? "cold"];
1764
+ if (rankA !== rankB) return rankA - rankB;
1765
+ const timeA = a.snapshot.lastActivityAt ? new Date(a.snapshot.lastActivityAt).getTime() : 0;
1766
+ const timeB = b.snapshot.lastActivityAt ? new Date(b.snapshot.lastActivityAt).getTime() : 0;
1767
+ return timeB - timeA;
1218
1768
  });
1219
- }
1220
- function getAllLicensesNeedingRevalidation() {
1221
- return getActiveLicenses().filter((l) => needsRevalidation(l));
1222
- }
1223
- var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1224
- function needsRevalidation(entry) {
1225
- const lastValidated = new Date(entry.lastValidatedAt).getTime();
1226
- return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
1769
+ return {
1770
+ projects,
1771
+ generated: (/* @__PURE__ */ new Date()).toISOString()
1772
+ };
1227
1773
  }
1228
1774
 
1775
+ // ../../packages/shared/src/decisionStorage.ts
1776
+ import fs6 from "fs";
1777
+ import path8 from "path";
1778
+
1229
1779
  // ../../packages/shared/src/featureGate.ts
1230
1780
  var DefaultFeatureGate = class {
1231
1781
  isEnabled(_feature) {
@@ -1238,31 +1788,31 @@ function isDecisionsEnabled() {
1238
1788
  }
1239
1789
 
1240
1790
  // ../../packages/shared/src/decisionStorage.ts
1241
- var STORAGE_DIR2 = ".keepgoing";
1242
- var DECISIONS_FILE = "decisions.json";
1791
+ var STORAGE_DIR3 = ".keepgoing";
1792
+ var DECISIONS_FILE2 = "decisions.json";
1243
1793
  var MAX_DECISIONS = 100;
1244
1794
  var DecisionStorage = class {
1245
1795
  storagePath;
1246
1796
  decisionsFilePath;
1247
1797
  constructor(workspacePath) {
1248
1798
  const mainRoot = resolveStorageRoot(workspacePath);
1249
- this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1250
- this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1799
+ this.storagePath = path8.join(mainRoot, STORAGE_DIR3);
1800
+ this.decisionsFilePath = path8.join(this.storagePath, DECISIONS_FILE2);
1251
1801
  }
1252
1802
  ensureStorageDir() {
1253
- if (!fs4.existsSync(this.storagePath)) {
1254
- fs4.mkdirSync(this.storagePath, { recursive: true });
1803
+ if (!fs6.existsSync(this.storagePath)) {
1804
+ fs6.mkdirSync(this.storagePath, { recursive: true });
1255
1805
  }
1256
1806
  }
1257
1807
  getProjectName() {
1258
- return path6.basename(path6.dirname(this.storagePath));
1808
+ return path8.basename(path8.dirname(this.storagePath));
1259
1809
  }
1260
1810
  load() {
1261
1811
  try {
1262
- if (!fs4.existsSync(this.decisionsFilePath)) {
1812
+ if (!fs6.existsSync(this.decisionsFilePath)) {
1263
1813
  return createEmptyProjectDecisions(this.getProjectName());
1264
1814
  }
1265
- const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
1815
+ const raw = fs6.readFileSync(this.decisionsFilePath, "utf-8");
1266
1816
  const data = JSON.parse(raw);
1267
1817
  return data;
1268
1818
  } catch {
@@ -1272,7 +1822,7 @@ var DecisionStorage = class {
1272
1822
  save(decisions) {
1273
1823
  this.ensureStorageDir();
1274
1824
  const content = JSON.stringify(decisions, null, 2);
1275
- fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
1825
+ fs6.writeFileSync(this.decisionsFilePath, content, "utf-8");
1276
1826
  }
1277
1827
  /**
1278
1828
  * Save a decision record as a draft. Always persists regardless of Pro
@@ -1448,366 +1998,84 @@ function classifyCommit(commit) {
1448
1998
  confidence += 0.35;
1449
1999
  reasons.push(`conventional commit type '${type}' is high-signal`);
1450
2000
  }
1451
- if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
1452
- matchedTypes.push(`scope:${scope}`);
1453
- confidence += 0.25;
1454
- reasons.push(`conventional commit scope '${scope}' is high-signal`);
1455
- }
1456
- if (parsed.breaking) {
1457
- confidence += 0.4;
1458
- reasons.push("breaking change indicated by ! marker");
1459
- }
1460
- const matchedPaths = [];
1461
- let bestTier = null;
1462
- for (const file of filesChanged) {
1463
- const pm = matchHighSignalPath(file);
1464
- if (pm && !matchedPaths.includes(pm.label)) {
1465
- matchedPaths.push(pm.label);
1466
- if (bestTier !== "infra") {
1467
- bestTier = pm.tier;
1468
- }
1469
- }
1470
- }
1471
- if (matchedPaths.length > 0) {
1472
- confidence += bestTier === "infra" ? 0.4 : 0.2;
1473
- reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
1474
- }
1475
- if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
1476
- confidence -= 0.5;
1477
- reasons.push("all changed files are low-signal (lock files, generated code, dist)");
1478
- }
1479
- if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
1480
- confidence -= 0.5;
1481
- reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
1482
- }
1483
- if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
1484
- confidence -= 0.5;
1485
- reasons.push("Tailwind generated CSS change (low signal)");
1486
- }
1487
- const isDecisionCandidate = confidence >= 0.4;
1488
- const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
1489
- const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
1490
- return {
1491
- isDecisionCandidate,
1492
- confidence: Math.max(0, Math.min(1, confidence)),
1493
- reasons,
1494
- category
1495
- };
1496
- }
1497
- function tryDetectDecision(opts) {
1498
- if (!isDecisionsEnabled()) {
1499
- return void 0;
1500
- }
1501
- const classification = classifyCommit({
1502
- message: opts.commitMessage,
1503
- filesChanged: opts.filesChanged
1504
- });
1505
- if (!classification.isDecisionCandidate) {
1506
- return void 0;
1507
- }
1508
- const decision = createDecisionRecord({
1509
- checkpointId: opts.checkpointId,
1510
- gitBranch: opts.gitBranch,
1511
- commitHash: opts.commitHash,
1512
- commitMessage: opts.commitMessage,
1513
- filesChanged: opts.filesChanged,
1514
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1515
- classification
1516
- });
1517
- const storage = new DecisionStorage(opts.workspacePath);
1518
- storage.saveDecision(decision);
1519
- return {
1520
- category: classification.category,
1521
- confidence: classification.confidence
1522
- };
1523
- }
1524
-
1525
- // ../../packages/shared/src/reader.ts
1526
- import fs5 from "fs";
1527
- import path7 from "path";
1528
- var STORAGE_DIR3 = ".keepgoing";
1529
- var META_FILE2 = "meta.json";
1530
- var SESSIONS_FILE2 = "sessions.json";
1531
- var DECISIONS_FILE2 = "decisions.json";
1532
- var STATE_FILE2 = "state.json";
1533
- var CURRENT_TASKS_FILE2 = "current-tasks.json";
1534
- var KeepGoingReader = class {
1535
- workspacePath;
1536
- storagePath;
1537
- metaFilePath;
1538
- sessionsFilePath;
1539
- decisionsFilePath;
1540
- stateFilePath;
1541
- currentTasksFilePath;
1542
- _isWorktree;
1543
- _cachedBranch = null;
1544
- // null = not yet resolved
1545
- constructor(workspacePath) {
1546
- this.workspacePath = workspacePath;
1547
- const mainRoot = resolveStorageRoot(workspacePath);
1548
- this._isWorktree = mainRoot !== workspacePath;
1549
- this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
1550
- this.metaFilePath = path7.join(this.storagePath, META_FILE2);
1551
- this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
1552
- this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
1553
- this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
1554
- this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
1555
- }
1556
- /** Check if .keepgoing/ directory exists. */
1557
- exists() {
1558
- return fs5.existsSync(this.storagePath);
1559
- }
1560
- /** Read state.json, returns undefined if missing or corrupt. */
1561
- getState() {
1562
- return this.readJsonFile(this.stateFilePath);
1563
- }
1564
- /** Read meta.json, returns undefined if missing or corrupt. */
1565
- getMeta() {
1566
- return this.readJsonFile(this.metaFilePath);
1567
- }
1568
- /**
1569
- * Read sessions from sessions.json.
1570
- * Handles both formats:
1571
- * - Flat array: SessionCheckpoint[] (from ProjectStorage)
1572
- * - Wrapper object: ProjectSessions (from SessionStorage)
1573
- */
1574
- getSessions() {
1575
- return this.parseSessions().sessions;
1576
- }
1577
- /**
1578
- * Get the most recent session checkpoint.
1579
- * Uses state.lastSessionId if available, falls back to last in array.
1580
- */
1581
- getLastSession() {
1582
- const { sessions, wrapperLastSessionId } = this.parseSessions();
1583
- if (sessions.length === 0) {
1584
- return void 0;
1585
- }
1586
- const state = this.getState();
1587
- if (state?.lastSessionId) {
1588
- const found = sessions.find((s) => s.id === state.lastSessionId);
1589
- if (found) {
1590
- return found;
1591
- }
1592
- }
1593
- if (wrapperLastSessionId) {
1594
- const found = sessions.find((s) => s.id === wrapperLastSessionId);
1595
- if (found) {
1596
- return found;
1597
- }
1598
- }
1599
- return sessions[sessions.length - 1];
1600
- }
1601
- /**
1602
- * Returns the last N sessions, newest first.
1603
- */
1604
- getRecentSessions(count) {
1605
- return getRecentSessions(this.getSessions(), count);
1606
- }
1607
- /** Read all decisions from decisions.json. */
1608
- getDecisions() {
1609
- return this.parseDecisions().decisions;
1610
- }
1611
- /** Returns the last N decisions, newest first. */
1612
- getRecentDecisions(count) {
1613
- const all = this.getDecisions();
1614
- return all.slice(-count).reverse();
1615
- }
1616
- /** Read the multi-license store from `~/.keepgoing/license.json`. */
1617
- getLicenseStore() {
1618
- return readLicenseStore();
1619
- }
1620
- /**
1621
- * Read all current tasks from current-tasks.json.
1622
- * Automatically filters out stale finished sessions (> 2 hours).
1623
- */
1624
- getCurrentTasks() {
1625
- const multiRaw = this.readJsonFile(this.currentTasksFilePath);
1626
- if (multiRaw) {
1627
- const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
1628
- return this.pruneStale(tasks);
1629
- }
1630
- return [];
1631
- }
1632
- /** Get only active sessions (sessionActive=true and within stale threshold). */
1633
- getActiveTasks() {
1634
- return this.getCurrentTasks().filter((t) => t.sessionActive);
1635
- }
1636
- /** Get a specific session by ID. */
1637
- getTaskBySessionId(sessionId) {
1638
- return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
1639
- }
1640
- /**
1641
- * Detect files being edited by multiple sessions simultaneously.
1642
- * Returns pairs of session IDs and the conflicting file paths.
1643
- */
1644
- detectFileConflicts() {
1645
- const activeTasks = this.getActiveTasks();
1646
- if (activeTasks.length < 2) return [];
1647
- const fileToSessions = /* @__PURE__ */ new Map();
1648
- for (const task of activeTasks) {
1649
- if (task.lastFileEdited && task.sessionId) {
1650
- const existing = fileToSessions.get(task.lastFileEdited) ?? [];
1651
- existing.push({
1652
- sessionId: task.sessionId,
1653
- agentLabel: task.agentLabel,
1654
- branch: task.branch
1655
- });
1656
- fileToSessions.set(task.lastFileEdited, existing);
1657
- }
1658
- }
1659
- const conflicts = [];
1660
- for (const [file, sessions] of fileToSessions) {
1661
- if (sessions.length > 1) {
1662
- conflicts.push({ file, sessions });
1663
- }
1664
- }
1665
- return conflicts;
1666
- }
1667
- /**
1668
- * Detect sessions on the same branch (possible duplicate work).
1669
- */
1670
- detectBranchOverlap() {
1671
- const activeTasks = this.getActiveTasks();
1672
- if (activeTasks.length < 2) return [];
1673
- const branchToSessions = /* @__PURE__ */ new Map();
1674
- for (const task of activeTasks) {
1675
- if (task.branch && task.sessionId) {
1676
- const existing = branchToSessions.get(task.branch) ?? [];
1677
- existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
1678
- branchToSessions.set(task.branch, existing);
1679
- }
1680
- }
1681
- const overlaps = [];
1682
- for (const [branch, sessions] of branchToSessions) {
1683
- if (sessions.length > 1) {
1684
- overlaps.push({ branch, sessions });
1685
- }
1686
- }
1687
- return overlaps;
1688
- }
1689
- pruneStale(tasks) {
1690
- return pruneStaleTasks(tasks);
1691
- }
1692
- /** Get the last session checkpoint for a specific branch. */
1693
- getLastSessionForBranch(branch) {
1694
- const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
1695
- return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1696
- }
1697
- /** Returns the last N sessions for a specific branch, newest first. */
1698
- getRecentSessionsForBranch(branch, count) {
1699
- const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1700
- return filtered.slice(-count).reverse();
1701
- }
1702
- /** Returns the last N decisions for a specific branch, newest first. */
1703
- getRecentDecisionsForBranch(branch, count) {
1704
- const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1705
- return filtered.slice(-count).reverse();
1706
- }
1707
- /** Whether the workspace is inside a git worktree. */
1708
- get isWorktree() {
1709
- return this._isWorktree;
1710
- }
1711
- /**
1712
- * Returns the current git branch for this workspace.
1713
- * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1714
- */
1715
- getCurrentBranch() {
1716
- if (this._cachedBranch === null) {
1717
- this._cachedBranch = getCurrentBranch(this.workspacePath);
1718
- }
1719
- return this._cachedBranch;
1720
- }
1721
- /**
1722
- * Worktree-aware last session lookup.
1723
- * In a worktree, scopes to the current branch with fallback to global.
1724
- * Returns the session and whether it fell back to global.
1725
- */
1726
- getScopedLastSession() {
1727
- const branch = this.getCurrentBranch();
1728
- if (this._isWorktree && branch) {
1729
- const scoped = this.getLastSessionForBranch(branch);
1730
- if (scoped) return { session: scoped, isFallback: false };
1731
- return { session: this.getLastSession(), isFallback: true };
1732
- }
1733
- return { session: this.getLastSession(), isFallback: false };
1734
- }
1735
- /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1736
- getScopedRecentSessions(count) {
1737
- const branch = this.getCurrentBranch();
1738
- if (this._isWorktree && branch) {
1739
- return this.getRecentSessionsForBranch(branch, count);
1740
- }
1741
- return this.getRecentSessions(count);
2001
+ if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
2002
+ matchedTypes.push(`scope:${scope}`);
2003
+ confidence += 0.25;
2004
+ reasons.push(`conventional commit scope '${scope}' is high-signal`);
1742
2005
  }
1743
- /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1744
- getScopedRecentDecisions(count) {
1745
- const branch = this.getCurrentBranch();
1746
- if (this._isWorktree && branch) {
1747
- return this.getRecentDecisionsForBranch(branch, count);
1748
- }
1749
- return this.getRecentDecisions(count);
2006
+ if (parsed.breaking) {
2007
+ confidence += 0.4;
2008
+ reasons.push("breaking change indicated by ! marker");
1750
2009
  }
1751
- /**
1752
- * Resolves branch scope from an explicit `branch` parameter.
1753
- * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1754
- * - `"all"` returns no filter.
1755
- * - An explicit branch name uses that.
1756
- * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1757
- */
1758
- resolveBranchScope(branch) {
1759
- if (branch === "all") {
1760
- return { effectiveBranch: void 0, scopeLabel: "all branches" };
1761
- }
1762
- if (branch) {
1763
- return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1764
- }
1765
- const currentBranch = this.getCurrentBranch();
1766
- if (this._isWorktree && currentBranch) {
1767
- return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
2010
+ const matchedPaths = [];
2011
+ let bestTier = null;
2012
+ for (const file of filesChanged) {
2013
+ const pm = matchHighSignalPath(file);
2014
+ if (pm && !matchedPaths.includes(pm.label)) {
2015
+ matchedPaths.push(pm.label);
2016
+ if (bestTier !== "infra") {
2017
+ bestTier = pm.tier;
2018
+ }
1768
2019
  }
1769
- return { effectiveBranch: void 0, scopeLabel: "all branches" };
1770
2020
  }
1771
- /**
1772
- * Parses sessions.json once, returning both the session list
1773
- * and the optional lastSessionId from a ProjectSessions wrapper.
1774
- */
1775
- parseSessions() {
1776
- const raw = this.readJsonFile(
1777
- this.sessionsFilePath
1778
- );
1779
- if (!raw) {
1780
- return { sessions: [] };
1781
- }
1782
- if (Array.isArray(raw)) {
1783
- return { sessions: raw };
1784
- }
1785
- return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
2021
+ if (matchedPaths.length > 0) {
2022
+ confidence += bestTier === "infra" ? 0.4 : 0.2;
2023
+ reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
1786
2024
  }
1787
- parseDecisions() {
1788
- const raw = this.readJsonFile(this.decisionsFilePath);
1789
- if (!raw) {
1790
- return { decisions: [] };
1791
- }
1792
- return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
2025
+ if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
2026
+ confidence -= 0.5;
2027
+ reasons.push("all changed files are low-signal (lock files, generated code, dist)");
1793
2028
  }
1794
- readJsonFile(filePath) {
1795
- try {
1796
- if (!fs5.existsSync(filePath)) {
1797
- return void 0;
1798
- }
1799
- const raw = fs5.readFileSync(filePath, "utf-8");
1800
- return JSON.parse(raw);
1801
- } catch {
1802
- return void 0;
1803
- }
2029
+ if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
2030
+ confidence -= 0.5;
2031
+ reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
1804
2032
  }
1805
- };
2033
+ if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
2034
+ confidence -= 0.5;
2035
+ reasons.push("Tailwind generated CSS change (low signal)");
2036
+ }
2037
+ const isDecisionCandidate = confidence >= 0.4;
2038
+ const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
2039
+ const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
2040
+ return {
2041
+ isDecisionCandidate,
2042
+ confidence: Math.max(0, Math.min(1, confidence)),
2043
+ reasons,
2044
+ category
2045
+ };
2046
+ }
2047
+ function tryDetectDecision(opts) {
2048
+ if (!isDecisionsEnabled()) {
2049
+ return void 0;
2050
+ }
2051
+ const classification = classifyCommit({
2052
+ message: opts.commitMessage,
2053
+ filesChanged: opts.filesChanged
2054
+ });
2055
+ if (!classification.isDecisionCandidate) {
2056
+ return void 0;
2057
+ }
2058
+ const decision = createDecisionRecord({
2059
+ checkpointId: opts.checkpointId,
2060
+ gitBranch: opts.gitBranch,
2061
+ commitHash: opts.commitHash,
2062
+ commitMessage: opts.commitMessage,
2063
+ filesChanged: opts.filesChanged,
2064
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2065
+ classification
2066
+ });
2067
+ const storage = new DecisionStorage(opts.workspacePath);
2068
+ storage.saveDecision(decision);
2069
+ return {
2070
+ category: classification.category,
2071
+ confidence: classification.confidence
2072
+ };
2073
+ }
1806
2074
 
1807
2075
  // ../../packages/shared/src/setup.ts
1808
- import fs6 from "fs";
2076
+ import fs7 from "fs";
1809
2077
  import os3 from "os";
1810
- import path8 from "path";
2078
+ import path9 from "path";
1811
2079
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1812
2080
  var SESSION_START_HOOK = {
1813
2081
  matcher: "",
@@ -1836,6 +2104,15 @@ var POST_TOOL_USE_HOOK = {
1836
2104
  }
1837
2105
  ]
1838
2106
  };
2107
+ var PLAN_MODE_HOOK = {
2108
+ matcher: "Read|Grep|Glob|Bash|WebSearch",
2109
+ hooks: [
2110
+ {
2111
+ type: "command",
2112
+ command: "npx -y @keepgoingdev/mcp-server --heartbeat"
2113
+ }
2114
+ ]
2115
+ };
1839
2116
  var SESSION_END_HOOK = {
1840
2117
  matcher: "",
1841
2118
  hooks: [
@@ -1845,7 +2122,7 @@ var SESSION_END_HOOK = {
1845
2122
  }
1846
2123
  ]
1847
2124
  };
1848
- var KEEPGOING_RULES_VERSION = 2;
2125
+ var KEEPGOING_RULES_VERSION = 3;
1849
2126
  var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
1850
2127
  ## KeepGoing
1851
2128
 
@@ -1855,6 +2132,8 @@ After completing a task or meaningful piece of work, call the \`save_checkpoint\
1855
2132
  - \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
1856
2133
  - \`nextStep\`: What to do next
1857
2134
  - \`blocker\`: Any blocker (if applicable)
2135
+
2136
+ When working in plan mode (investigating, designing, iterating on an approach before any edits), call \`save_checkpoint\` when you reach a significant milestone or conclusion. Use the summary to capture what was investigated and decided. This preserves planning context for future sessions.
1858
2137
  `;
1859
2138
  function getRulesFileVersion(content) {
1860
2139
  const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
@@ -1862,7 +2141,7 @@ function getRulesFileVersion(content) {
1862
2141
  }
1863
2142
  var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1864
2143
  function detectClaudeDir() {
1865
- return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
2144
+ return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
1866
2145
  }
1867
2146
  function hasKeepGoingHook(hookEntries) {
1868
2147
  return hookEntries.some(
@@ -1874,19 +2153,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
1874
2153
  const claudeDir2 = overrideClaudeDir || detectClaudeDir();
1875
2154
  return {
1876
2155
  claudeDir: claudeDir2,
1877
- settingsPath: path8.join(claudeDir2, "settings.json"),
1878
- claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1879
- rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
2156
+ settingsPath: path9.join(claudeDir2, "settings.json"),
2157
+ claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
2158
+ rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
1880
2159
  };
1881
2160
  }
1882
- const claudeDir = path8.join(workspacePath, ".claude");
1883
- const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1884
- const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
2161
+ const claudeDir = path9.join(workspacePath, ".claude");
2162
+ const dotClaudeMdPath = path9.join(workspacePath, ".claude", "CLAUDE.md");
2163
+ const rootClaudeMdPath = path9.join(workspacePath, "CLAUDE.md");
1885
2164
  return {
1886
2165
  claudeDir,
1887
- settingsPath: path8.join(claudeDir, "settings.json"),
1888
- claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1889
- rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
2166
+ settingsPath: path9.join(claudeDir, "settings.json"),
2167
+ claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
2168
+ rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
1890
2169
  };
1891
2170
  }
1892
2171
  function writeHooksToSettings(settings) {
@@ -1915,6 +2194,13 @@ function writeHooksToSettings(settings) {
1915
2194
  settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1916
2195
  changed = true;
1917
2196
  }
2197
+ const hasHeartbeat = settings.hooks.PostToolUse.some(
2198
+ (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes("--heartbeat"))
2199
+ );
2200
+ if (!hasHeartbeat) {
2201
+ settings.hooks.PostToolUse.push(PLAN_MODE_HOOK);
2202
+ changed = true;
2203
+ }
1918
2204
  if (!Array.isArray(settings.hooks.SessionEnd)) {
1919
2205
  settings.hooks.SessionEnd = [];
1920
2206
  }
@@ -1926,11 +2212,11 @@ function writeHooksToSettings(settings) {
1926
2212
  }
1927
2213
  function checkHookConflict(scope, workspacePath) {
1928
2214
  const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1929
- if (!fs6.existsSync(otherPaths.settingsPath)) {
2215
+ if (!fs7.existsSync(otherPaths.settingsPath)) {
1930
2216
  return null;
1931
2217
  }
1932
2218
  try {
1933
- const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
2219
+ const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
1934
2220
  const hooks = otherSettings?.hooks;
1935
2221
  if (!hooks) return null;
1936
2222
  const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
@@ -1959,10 +2245,10 @@ function setupProject(options) {
1959
2245
  workspacePath,
1960
2246
  claudeDirOverride
1961
2247
  );
1962
- const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
2248
+ const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
1963
2249
  let settings = {};
1964
- if (fs6.existsSync(settingsPath)) {
1965
- settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2250
+ if (fs7.existsSync(settingsPath)) {
2251
+ settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1966
2252
  }
1967
2253
  let settingsChanged = false;
1968
2254
  if (sessionHooks) {
@@ -1993,36 +2279,36 @@ function setupProject(options) {
1993
2279
  statusline?.cleanup?.();
1994
2280
  }
1995
2281
  if (settingsChanged) {
1996
- if (!fs6.existsSync(claudeDir)) {
1997
- fs6.mkdirSync(claudeDir, { recursive: true });
2282
+ if (!fs7.existsSync(claudeDir)) {
2283
+ fs7.mkdirSync(claudeDir, { recursive: true });
1998
2284
  }
1999
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2285
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2000
2286
  changed = true;
2001
2287
  }
2002
2288
  if (claudeMd) {
2003
- const rulesDir = path8.dirname(rulesPath);
2004
- const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
2005
- if (fs6.existsSync(rulesPath)) {
2006
- const existing = fs6.readFileSync(rulesPath, "utf-8");
2289
+ const rulesDir = path9.dirname(rulesPath);
2290
+ const rulesLabel = scope === "user" ? path9.join(path9.relative(os3.homedir(), path9.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
2291
+ if (fs7.existsSync(rulesPath)) {
2292
+ const existing = fs7.readFileSync(rulesPath, "utf-8");
2007
2293
  const existingVersion = getRulesFileVersion(existing);
2008
2294
  if (existingVersion === null) {
2009
2295
  messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
2010
2296
  } else if (existingVersion >= KEEPGOING_RULES_VERSION) {
2011
2297
  messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
2012
2298
  } else {
2013
- if (!fs6.existsSync(rulesDir)) {
2014
- fs6.mkdirSync(rulesDir, { recursive: true });
2299
+ if (!fs7.existsSync(rulesDir)) {
2300
+ fs7.mkdirSync(rulesDir, { recursive: true });
2015
2301
  }
2016
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2302
+ fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2017
2303
  changed = true;
2018
2304
  messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
2019
2305
  }
2020
2306
  } else {
2021
- const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
2022
- if (!fs6.existsSync(rulesDir)) {
2023
- fs6.mkdirSync(rulesDir, { recursive: true });
2307
+ const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
2308
+ if (!fs7.existsSync(rulesDir)) {
2309
+ fs7.mkdirSync(rulesDir, { recursive: true });
2024
2310
  }
2025
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2311
+ fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2026
2312
  changed = true;
2027
2313
  if (existingClaudeMd.includes("## KeepGoing")) {
2028
2314
  const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
@@ -2458,7 +2744,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
2458
2744
  }
2459
2745
 
2460
2746
  // src/tools/saveCheckpoint.ts
2461
- import path9 from "path";
2747
+ import path10 from "path";
2462
2748
  import { z as z4 } from "zod";
2463
2749
  function registerSaveCheckpoint(server, reader, workspacePath) {
2464
2750
  server.tool(
@@ -2470,12 +2756,19 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2470
2756
  blocker: z4.string().optional().describe("Any blocker preventing progress")
2471
2757
  },
2472
2758
  async ({ summary, nextStep, blocker }) => {
2759
+ summary = stripAgentTags(summary);
2760
+ nextStep = nextStep ? stripAgentTags(nextStep) : nextStep;
2761
+ blocker = blocker ? stripAgentTags(blocker) : blocker;
2473
2762
  const lastSession = reader.getLastSession();
2474
2763
  const gitBranch = getCurrentBranch(workspacePath);
2475
2764
  const touchedFiles = getTouchedFiles(workspacePath);
2476
2765
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2477
- const projectName = path9.basename(resolveStorageRoot(workspacePath));
2766
+ const projectName = path10.basename(resolveStorageRoot(workspacePath));
2767
+ const writer = new KeepGoingWriter(workspacePath);
2478
2768
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2769
+ const existingTasks = writer.readCurrentTasks();
2770
+ const existingSession = existingTasks.find((t) => t.sessionId === sessionId);
2771
+ const sessionPhase = existingSession?.sessionPhase;
2479
2772
  const checkpoint = createCheckpoint({
2480
2773
  summary,
2481
2774
  nextStep: nextStep || "",
@@ -2485,10 +2778,19 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2485
2778
  commitHashes,
2486
2779
  workspaceRoot: workspacePath,
2487
2780
  source: "manual",
2488
- sessionId
2781
+ sessionId,
2782
+ ...sessionPhase ? { sessionPhase } : {},
2783
+ ...sessionPhase === "planning" ? { tags: ["plan"] } : {}
2489
2784
  });
2490
- const writer = new KeepGoingWriter(workspacePath);
2491
2785
  writer.saveCheckpoint(checkpoint, projectName);
2786
+ writer.upsertSession({
2787
+ sessionId,
2788
+ sessionActive: true,
2789
+ branch: gitBranch ?? void 0,
2790
+ updatedAt: checkpoint.timestamp,
2791
+ taskSummary: summary,
2792
+ nextStep: nextStep || void 0
2793
+ });
2492
2794
  const lines = [
2493
2795
  `Checkpoint saved.`,
2494
2796
  `- **ID:** ${checkpoint.id}`,
@@ -2641,8 +2943,9 @@ function registerGetCurrentTask(server, reader) {
2641
2943
  lines.push("");
2642
2944
  }
2643
2945
  for (const task of [...activeTasks, ...finishedTasks]) {
2946
+ const phaseLabel = task.sessionPhase === "planning" ? "Planning" : void 0;
2644
2947
  const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
2645
- const statusLabel = task.sessionActive ? "Active" : "Finished";
2948
+ const statusLabel = task.sessionActive ? phaseLabel || "Active" : "Finished";
2646
2949
  const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
2647
2950
  lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
2648
2951
  lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
@@ -2692,25 +2995,25 @@ function registerGetCurrentTask(server, reader) {
2692
2995
  import { z as z6 } from "zod";
2693
2996
 
2694
2997
  // src/cli/migrate.ts
2695
- import fs7 from "fs";
2998
+ import fs8 from "fs";
2696
2999
  import os4 from "os";
2697
- import path10 from "path";
3000
+ import path11 from "path";
2698
3001
  var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
2699
3002
  function isLegacyStatusline(command) {
2700
3003
  return !command.includes("--statusline") && command.includes("keepgoing-statusline");
2701
3004
  }
2702
3005
  function migrateStatusline(wsPath) {
2703
- const settingsPath = path10.join(wsPath, ".claude", "settings.json");
2704
- if (!fs7.existsSync(settingsPath)) return void 0;
3006
+ const settingsPath = path11.join(wsPath, ".claude", "settings.json");
3007
+ if (!fs8.existsSync(settingsPath)) return void 0;
2705
3008
  try {
2706
- const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
3009
+ const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
2707
3010
  const cmd = settings.statusLine?.command;
2708
3011
  if (!cmd || !isLegacyStatusline(cmd)) return void 0;
2709
3012
  settings.statusLine = {
2710
3013
  type: "command",
2711
3014
  command: STATUSLINE_CMD2
2712
3015
  };
2713
- fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3016
+ fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2714
3017
  cleanupLegacyScript();
2715
3018
  return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
2716
3019
  } catch {
@@ -2718,10 +3021,10 @@ function migrateStatusline(wsPath) {
2718
3021
  }
2719
3022
  }
2720
3023
  function cleanupLegacyScript() {
2721
- const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2722
- if (fs7.existsSync(legacyScript)) {
3024
+ const legacyScript = path11.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
3025
+ if (fs8.existsSync(legacyScript)) {
2723
3026
  try {
2724
- fs7.unlinkSync(legacyScript);
3027
+ fs8.unlinkSync(legacyScript);
2725
3028
  } catch {
2726
3029
  }
2727
3030
  }
@@ -2954,6 +3257,87 @@ function registerContinueOn(server, reader, workspacePath) {
2954
3257
  );
2955
3258
  }
2956
3259
 
3260
+ // src/tools/getContextSnapshot.ts
3261
+ import { z as z10 } from "zod";
3262
+ function registerGetContextSnapshot(server, reader, workspacePath) {
3263
+ server.tool(
3264
+ "get_context_snapshot",
3265
+ "Get a compact context snapshot: what you were doing, what is next, and momentum. Use this for quick orientation without a full briefing.",
3266
+ {
3267
+ format: z10.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns a formatted single line. "json" returns the structured snapshot object.')
3268
+ },
3269
+ async ({ format }) => {
3270
+ const snapshot = generateContextSnapshot(workspacePath);
3271
+ if (!snapshot) {
3272
+ return {
3273
+ content: [
3274
+ {
3275
+ type: "text",
3276
+ text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
3277
+ }
3278
+ ]
3279
+ };
3280
+ }
3281
+ if (format === "json") {
3282
+ return {
3283
+ content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }]
3284
+ };
3285
+ }
3286
+ const icon = snapshot.momentum === "hot" ? "\u26A1" : snapshot.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
3287
+ const parts = [];
3288
+ parts.push(`${icon} ${snapshot.doing}`);
3289
+ if (snapshot.next) {
3290
+ parts.push(`\u2192 ${snapshot.next}`);
3291
+ }
3292
+ let line = parts.join(" ");
3293
+ line += ` (${snapshot.when})`;
3294
+ if (snapshot.blocker) {
3295
+ line += ` \u26D4 ${snapshot.blocker}`;
3296
+ }
3297
+ if (snapshot.activeAgents && snapshot.activeAgents > 0) {
3298
+ line += ` [${snapshot.activeAgents} active agent${snapshot.activeAgents > 1 ? "s" : ""}]`;
3299
+ }
3300
+ return {
3301
+ content: [{ type: "text", text: line }]
3302
+ };
3303
+ }
3304
+ );
3305
+ }
3306
+
3307
+ // src/tools/getWhatsHot.ts
3308
+ import { z as z11 } from "zod";
3309
+ function registerGetWhatsHot(server) {
3310
+ server.tool(
3311
+ "get_whats_hot",
3312
+ "Get a summary of activity across all registered projects, sorted by momentum. Shows what the developer is working on across their entire portfolio.",
3313
+ {
3314
+ format: z11.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns formatted lines. "json" returns the structured summary object.')
3315
+ },
3316
+ async ({ format }) => {
3317
+ const summary = generateCrossProjectSummary();
3318
+ if (summary.projects.length === 0) {
3319
+ return {
3320
+ content: [
3321
+ {
3322
+ type: "text",
3323
+ text: "No projects with activity found. Projects are registered automatically when checkpoints are saved."
3324
+ }
3325
+ ]
3326
+ };
3327
+ }
3328
+ if (format === "json") {
3329
+ return {
3330
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
3331
+ };
3332
+ }
3333
+ const lines = summary.projects.map((entry) => formatCrossProjectLine(entry));
3334
+ return {
3335
+ content: [{ type: "text", text: lines.join("\n") }]
3336
+ };
3337
+ }
3338
+ );
3339
+ }
3340
+
2957
3341
  // src/prompts/resume.ts
2958
3342
  function registerResumePrompt(server) {
2959
3343
  server.prompt(
@@ -3119,10 +3503,11 @@ async function handlePrintCurrent() {
3119
3503
  }
3120
3504
 
3121
3505
  // src/cli/saveCheckpoint.ts
3122
- import path11 from "path";
3506
+ import path12 from "path";
3123
3507
  async function handleSaveCheckpoint() {
3124
3508
  const wsPath = resolveWsPath();
3125
3509
  const reader = new KeepGoingReader(wsPath);
3510
+ const writer = new KeepGoingWriter(wsPath);
3126
3511
  const { session: lastSession } = reader.getScopedLastSession();
3127
3512
  if (lastSession?.timestamp) {
3128
3513
  const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
@@ -3132,10 +3517,40 @@ async function handleSaveCheckpoint() {
3132
3517
  }
3133
3518
  const touchedFiles = getTouchedFiles(wsPath);
3134
3519
  const commitHashes = getCommitsSince(wsPath, lastSession?.timestamp);
3135
- if (touchedFiles.length === 0 && commitHashes.length === 0) {
3520
+ const gitBranch = getCurrentBranch(wsPath);
3521
+ const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
3522
+ const existingTasks = writer.readCurrentTasks();
3523
+ const existingSession = existingTasks.find((t) => t.sessionId === sessionId);
3524
+ const isPlanning = existingSession?.sessionPhase === "planning";
3525
+ if (touchedFiles.length === 0 && commitHashes.length === 0 && !isPlanning) {
3526
+ process.exit(0);
3527
+ }
3528
+ const projectName = path12.basename(resolveStorageRoot(wsPath));
3529
+ if (touchedFiles.length === 0 && commitHashes.length === 0 && isPlanning) {
3530
+ const summary2 = existingSession?.sessionLabel || existingSession?.taskSummary || "Planning session";
3531
+ const checkpoint2 = createCheckpoint({
3532
+ summary: summary2,
3533
+ nextStep: existingSession?.nextStep || "",
3534
+ gitBranch,
3535
+ touchedFiles: [],
3536
+ commitHashes: [],
3537
+ workspaceRoot: wsPath,
3538
+ source: "auto",
3539
+ sessionId,
3540
+ sessionPhase: "planning",
3541
+ tags: ["plan"]
3542
+ });
3543
+ writer.saveCheckpoint(checkpoint2, projectName);
3544
+ writer.upsertSession({
3545
+ sessionId,
3546
+ sessionActive: false,
3547
+ nextStep: checkpoint2.nextStep || void 0,
3548
+ branch: gitBranch ?? void 0,
3549
+ updatedAt: checkpoint2.timestamp
3550
+ });
3551
+ console.log(`[KeepGoing] Plan checkpoint saved: ${summary2}`);
3136
3552
  process.exit(0);
3137
3553
  }
3138
- const gitBranch = getCurrentBranch(wsPath);
3139
3554
  const commitMessages = getCommitMessagesSince(wsPath, lastSession?.timestamp);
3140
3555
  const now = (/* @__PURE__ */ new Date()).toISOString();
3141
3556
  const events = buildSessionEvents({
@@ -3147,10 +3562,9 @@ async function handleSaveCheckpoint() {
3147
3562
  sessionStartTime: lastSession?.timestamp ?? now,
3148
3563
  lastActivityTime: now
3149
3564
  });
3150
- const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
3565
+ const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path12.basename(f)).join(", ")}`;
3151
3566
  const nextStep = buildSmartNextStep(events);
3152
- const projectName = path11.basename(resolveStorageRoot(wsPath));
3153
- const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
3567
+ const sessionPhase = existingSession?.sessionPhase;
3154
3568
  const checkpoint = createCheckpoint({
3155
3569
  summary,
3156
3570
  nextStep,
@@ -3159,9 +3573,10 @@ async function handleSaveCheckpoint() {
3159
3573
  commitHashes,
3160
3574
  workspaceRoot: wsPath,
3161
3575
  source: "auto",
3162
- sessionId
3576
+ sessionId,
3577
+ ...sessionPhase ? { sessionPhase } : {},
3578
+ ...sessionPhase === "planning" ? { tags: ["plan"] } : {}
3163
3579
  });
3164
- const writer = new KeepGoingWriter(wsPath);
3165
3580
  writer.saveCheckpoint(checkpoint, projectName);
3166
3581
  writer.upsertSession({
3167
3582
  sessionId,
@@ -3194,7 +3609,7 @@ async function handleSaveCheckpoint() {
3194
3609
  }
3195
3610
 
3196
3611
  // src/cli/transcriptUtils.ts
3197
- import fs8 from "fs";
3612
+ import fs9 from "fs";
3198
3613
  var TAIL_READ_BYTES = 32768;
3199
3614
  var LATEST_LABEL_READ_BYTES = 65536;
3200
3615
  var TOOL_VERB_MAP = {
@@ -3251,9 +3666,9 @@ function isAssistantEntry(entry) {
3251
3666
  return entry.message?.role === "assistant";
3252
3667
  }
3253
3668
  function extractSessionLabel(transcriptPath) {
3254
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3669
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3255
3670
  try {
3256
- const raw = fs8.readFileSync(transcriptPath, "utf-8");
3671
+ const raw = fs9.readFileSync(transcriptPath, "utf-8");
3257
3672
  for (const line of raw.split("\n")) {
3258
3673
  const trimmed = line.trim();
3259
3674
  if (!trimmed) continue;
@@ -3266,7 +3681,9 @@ function extractSessionLabel(transcriptPath) {
3266
3681
  if (!isUserEntry(entry)) continue;
3267
3682
  let text = extractTextFromContent(entry.message?.content);
3268
3683
  if (!text) continue;
3269
- if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
3684
+ if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
3685
+ text = stripAgentTags(text);
3686
+ if (!text) continue;
3270
3687
  text = text.replace(/@[\w./\-]+/g, "").trim();
3271
3688
  text = text.replace(FILLER_PREFIX_RE, "").trim();
3272
3689
  text = text.replace(MARKDOWN_HEADING_RE, "").trim();
@@ -3282,19 +3699,19 @@ function extractSessionLabel(transcriptPath) {
3282
3699
  return null;
3283
3700
  }
3284
3701
  function extractLatestUserLabel(transcriptPath) {
3285
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3702
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3286
3703
  try {
3287
- const stat = fs8.statSync(transcriptPath);
3704
+ const stat = fs9.statSync(transcriptPath);
3288
3705
  const fileSize = stat.size;
3289
3706
  if (fileSize === 0) return null;
3290
3707
  const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
3291
3708
  const offset = fileSize - readSize;
3292
3709
  const buf = Buffer.alloc(readSize);
3293
- const fd = fs8.openSync(transcriptPath, "r");
3710
+ const fd = fs9.openSync(transcriptPath, "r");
3294
3711
  try {
3295
- fs8.readSync(fd, buf, 0, readSize, offset);
3712
+ fs9.readSync(fd, buf, 0, readSize, offset);
3296
3713
  } finally {
3297
- fs8.closeSync(fd);
3714
+ fs9.closeSync(fd);
3298
3715
  }
3299
3716
  const tail = buf.toString("utf-8");
3300
3717
  const lines = tail.split("\n").reverse();
@@ -3310,7 +3727,9 @@ function extractLatestUserLabel(transcriptPath) {
3310
3727
  if (!isUserEntry(entry)) continue;
3311
3728
  let text = extractTextFromContent(entry.message?.content);
3312
3729
  if (!text) continue;
3313
- if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
3730
+ if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
3731
+ text = stripAgentTags(text);
3732
+ if (!text) continue;
3314
3733
  text = text.replace(/@[\w./\-]+/g, "").trim();
3315
3734
  text = text.replace(FILLER_PREFIX_RE, "").trim();
3316
3735
  text = text.replace(MARKDOWN_HEADING_RE, "").trim();
@@ -3326,19 +3745,19 @@ function extractLatestUserLabel(transcriptPath) {
3326
3745
  return null;
3327
3746
  }
3328
3747
  function extractCurrentAction(transcriptPath) {
3329
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3748
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3330
3749
  try {
3331
- const stat = fs8.statSync(transcriptPath);
3750
+ const stat = fs9.statSync(transcriptPath);
3332
3751
  const fileSize = stat.size;
3333
3752
  if (fileSize === 0) return null;
3334
3753
  const readSize = Math.min(fileSize, TAIL_READ_BYTES);
3335
3754
  const offset = fileSize - readSize;
3336
3755
  const buf = Buffer.alloc(readSize);
3337
- const fd = fs8.openSync(transcriptPath, "r");
3756
+ const fd = fs9.openSync(transcriptPath, "r");
3338
3757
  try {
3339
- fs8.readSync(fd, buf, 0, readSize, offset);
3758
+ fs9.readSync(fd, buf, 0, readSize, offset);
3340
3759
  } finally {
3341
- fs8.closeSync(fd);
3760
+ fs9.closeSync(fd);
3342
3761
  }
3343
3762
  const tail = buf.toString("utf-8");
3344
3763
  const lines = tail.split("\n").reverse();
@@ -3373,6 +3792,8 @@ async function handleUpdateTask() {
3373
3792
  if (payloadStr) {
3374
3793
  try {
3375
3794
  const payload = JSON.parse(payloadStr);
3795
+ if (payload.taskSummary) payload.taskSummary = stripAgentTags(payload.taskSummary);
3796
+ if (payload.sessionLabel) payload.sessionLabel = stripAgentTags(payload.sessionLabel);
3376
3797
  const writer = new KeepGoingWriter(wsPath);
3377
3798
  const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
3378
3799
  const task = {
@@ -3423,7 +3844,8 @@ async function handleUpdateTaskFromHook() {
3423
3844
  branch,
3424
3845
  worktreePath: wsPath,
3425
3846
  sessionActive: true,
3426
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3847
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3848
+ sessionPhase: "active"
3427
3849
  };
3428
3850
  const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
3429
3851
  task.sessionId = sessionId;
@@ -3440,8 +3862,8 @@ async function handleUpdateTaskFromHook() {
3440
3862
  }
3441
3863
 
3442
3864
  // src/cli/statusline.ts
3443
- import fs9 from "fs";
3444
- import path12 from "path";
3865
+ import fs10 from "fs";
3866
+ import path13 from "path";
3445
3867
  var STDIN_TIMEOUT_MS2 = 3e3;
3446
3868
  async function handleStatusline() {
3447
3869
  const chunks = [];
@@ -3471,9 +3893,9 @@ async function handleStatusline() {
3471
3893
  if (!label) {
3472
3894
  try {
3473
3895
  const gitRoot = findGitRoot(dir);
3474
- const tasksFile = path12.join(gitRoot, ".keepgoing", "current-tasks.json");
3475
- if (fs9.existsSync(tasksFile)) {
3476
- const data = JSON.parse(fs9.readFileSync(tasksFile, "utf-8"));
3896
+ const tasksFile = path13.join(gitRoot, ".keepgoing", "current-tasks.json");
3897
+ if (fs10.existsSync(tasksFile)) {
3898
+ const data = JSON.parse(fs10.readFileSync(tasksFile, "utf-8"));
3477
3899
  const tasks = pruneStaleTasks(data.tasks ?? []);
3478
3900
  const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
3479
3901
  if (match?.sessionLabel) {
@@ -3570,6 +3992,59 @@ async function handleDetectDecisions() {
3570
3992
  process.exit(0);
3571
3993
  }
3572
3994
 
3995
+ // src/cli/heartbeat.ts
3996
+ var STDIN_TIMEOUT_MS3 = 3e3;
3997
+ var THROTTLE_MS = 3e4;
3998
+ async function handleHeartbeat() {
3999
+ const wsPath = resolveWsPath();
4000
+ const chunks = [];
4001
+ const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT_MS3);
4002
+ process.stdin.on("error", () => {
4003
+ clearTimeout(timeout);
4004
+ process.exit(0);
4005
+ });
4006
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
4007
+ process.stdin.on("end", () => {
4008
+ clearTimeout(timeout);
4009
+ try {
4010
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
4011
+ if (!raw) {
4012
+ process.exit(0);
4013
+ }
4014
+ const hookData = JSON.parse(raw);
4015
+ const writer = new KeepGoingWriter(wsPath);
4016
+ const existing = writer.readCurrentTasks();
4017
+ const sessionIdFromHook = hookData.session_id;
4018
+ const sessionId = sessionIdFromHook || generateSessionId({ workspaceRoot: wsPath, worktreePath: wsPath, branch: getCurrentBranch(wsPath) ?? void 0 });
4019
+ const existingSession = existing.find((t) => t.sessionId === sessionId);
4020
+ if (existingSession?.updatedAt) {
4021
+ const ageMs = Date.now() - new Date(existingSession.updatedAt).getTime();
4022
+ if (ageMs < THROTTLE_MS) {
4023
+ process.exit(0);
4024
+ }
4025
+ }
4026
+ const sessionPhase = existingSession?.sessionPhase === "active" ? "active" : "planning";
4027
+ const branch = existingSession?.branch ?? getCurrentBranch(wsPath) ?? void 0;
4028
+ const task = {
4029
+ sessionId,
4030
+ sessionActive: true,
4031
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4032
+ sessionPhase,
4033
+ worktreePath: wsPath,
4034
+ branch
4035
+ };
4036
+ if (!existingSession?.sessionLabel && hookData.transcript_path) {
4037
+ const label = extractSessionLabel(hookData.transcript_path);
4038
+ if (label) task.sessionLabel = label;
4039
+ }
4040
+ writer.upsertSession(task);
4041
+ } catch {
4042
+ }
4043
+ process.exit(0);
4044
+ });
4045
+ process.stdin.resume();
4046
+ }
4047
+
3573
4048
  // src/index.ts
3574
4049
  var CLI_HANDLERS = {
3575
4050
  "--print-momentum": handlePrintMomentum,
@@ -3579,7 +4054,8 @@ var CLI_HANDLERS = {
3579
4054
  "--print-current": handlePrintCurrent,
3580
4055
  "--statusline": handleStatusline,
3581
4056
  "--continue-on": handleContinueOn,
3582
- "--detect-decisions": handleDetectDecisions
4057
+ "--detect-decisions": handleDetectDecisions,
4058
+ "--heartbeat": handleHeartbeat
3583
4059
  };
3584
4060
  var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
3585
4061
  if (flag) {
@@ -3598,6 +4074,8 @@ if (flag) {
3598
4074
  registerGetCurrentTask(server, reader);
3599
4075
  registerSaveCheckpoint(server, reader, workspacePath);
3600
4076
  registerContinueOn(server, reader, workspacePath);
4077
+ registerGetContextSnapshot(server, reader, workspacePath);
4078
+ registerGetWhatsHot(server);
3601
4079
  registerSetupProject(server, workspacePath);
3602
4080
  registerActivateLicense(server);
3603
4081
  registerDeactivateLicense(server);