@keepgoingdev/mcp-server 0.7.2 → 0.8.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) {
@@ -706,22 +719,160 @@ function formatContinueOnPrompt(context, options) {
706
719
  return result;
707
720
  }
708
721
 
709
- // ../../packages/shared/src/storage.ts
710
- import fs2 from "fs";
711
- import path4 from "path";
712
- import { randomUUID as randomUUID2, createHash } from "crypto";
722
+ // ../../packages/shared/src/reader.ts
723
+ import fs4 from "fs";
724
+ import path6 from "path";
713
725
 
714
- // ../../packages/shared/src/registry.ts
726
+ // ../../packages/shared/src/license.ts
727
+ import crypto from "crypto";
715
728
  import fs from "fs";
716
729
  import os from "os";
717
730
  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");
731
+ var LICENSE_FILE = "license.json";
732
+ var DEVICE_ID_FILE = "device-id";
733
+ function getGlobalLicenseDir() {
734
+ return path3.join(os.homedir(), ".keepgoing");
735
+ }
736
+ function getGlobalLicensePath() {
737
+ return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
738
+ }
739
+ function getDeviceId() {
740
+ const dir = getGlobalLicenseDir();
741
+ const filePath = path3.join(dir, DEVICE_ID_FILE);
742
+ try {
743
+ const existing = fs.readFileSync(filePath, "utf-8").trim();
744
+ if (existing) return existing;
745
+ } catch {
746
+ }
747
+ const id = crypto.randomUUID();
748
+ if (!fs.existsSync(dir)) {
749
+ fs.mkdirSync(dir, { recursive: true });
750
+ }
751
+ fs.writeFileSync(filePath, id, "utf-8");
752
+ return id;
753
+ }
754
+ var DECISION_DETECTION_VARIANT_ID = 1361527;
755
+ var SESSION_AWARENESS_VARIANT_ID = 1366510;
756
+ var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
757
+ var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
758
+ var VARIANT_FEATURE_MAP = {
759
+ [DECISION_DETECTION_VARIANT_ID]: ["decisions"],
760
+ [SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
761
+ [TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
762
+ [TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
763
+ // Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
764
+ };
765
+ var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
766
+ function getVariantLabel(variantId) {
767
+ const features = VARIANT_FEATURE_MAP[variantId];
768
+ if (!features) return "Unknown Add-on";
769
+ if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
770
+ if (features.includes("decisions")) return "Decision Detection";
771
+ if (features.includes("session-awareness")) return "Session Awareness";
772
+ return "Pro Add-on";
773
+ }
774
+ var _cachedStore;
775
+ var _cacheTimestamp = 0;
776
+ var LICENSE_CACHE_TTL_MS = 2e3;
777
+ function readLicenseStore() {
778
+ const now = Date.now();
779
+ if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
780
+ return _cachedStore;
781
+ }
782
+ const licensePath = getGlobalLicensePath();
783
+ let store;
784
+ try {
785
+ if (!fs.existsSync(licensePath)) {
786
+ store = { version: 2, licenses: [] };
787
+ } else {
788
+ const raw = fs.readFileSync(licensePath, "utf-8");
789
+ const data = JSON.parse(raw);
790
+ if (data?.version === 2 && Array.isArray(data.licenses)) {
791
+ store = data;
792
+ } else {
793
+ store = { version: 2, licenses: [] };
794
+ }
795
+ }
796
+ } catch {
797
+ store = { version: 2, licenses: [] };
798
+ }
799
+ _cachedStore = store;
800
+ _cacheTimestamp = now;
801
+ return store;
802
+ }
803
+ function writeLicenseStore(store) {
804
+ const dirPath = getGlobalLicenseDir();
805
+ if (!fs.existsSync(dirPath)) {
806
+ fs.mkdirSync(dirPath, { recursive: true });
807
+ }
808
+ const licensePath = path3.join(dirPath, LICENSE_FILE);
809
+ fs.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
810
+ _cachedStore = store;
811
+ _cacheTimestamp = Date.now();
812
+ }
813
+ function addLicenseEntry(entry) {
814
+ const store = readLicenseStore();
815
+ const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
816
+ if (idx >= 0) {
817
+ store.licenses[idx] = entry;
818
+ } else {
819
+ store.licenses.push(entry);
820
+ }
821
+ writeLicenseStore(store);
822
+ }
823
+ function removeLicenseEntry(licenseKey) {
824
+ const store = readLicenseStore();
825
+ store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
826
+ writeLicenseStore(store);
827
+ }
828
+ function getActiveLicenses() {
829
+ return readLicenseStore().licenses.filter((l) => l.status === "active");
830
+ }
831
+ function getLicenseForFeature(feature) {
832
+ const active = getActiveLicenses();
833
+ return active.find((l) => {
834
+ const features = VARIANT_FEATURE_MAP[l.variantId];
835
+ return features?.includes(feature);
836
+ });
837
+ }
838
+ function getAllLicensesNeedingRevalidation() {
839
+ return getActiveLicenses().filter((l) => needsRevalidation(l));
840
+ }
841
+ var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
842
+ function needsRevalidation(entry) {
843
+ const lastValidated = new Date(entry.lastValidatedAt).getTime();
844
+ return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
845
+ }
846
+
847
+ // ../../packages/shared/src/storage.ts
848
+ import fs3 from "fs";
849
+ import path5 from "path";
850
+ import { randomUUID as randomUUID2, createHash } from "crypto";
851
+
852
+ // ../../packages/shared/src/registry.ts
853
+ import fs2 from "fs";
854
+ import os2 from "os";
855
+ import path4 from "path";
856
+ var KEEPGOING_DIR = path4.join(os2.homedir(), ".keepgoing");
857
+ var KNOWN_PROJECTS_FILE = path4.join(KEEPGOING_DIR, "known-projects.json");
858
+ var TRAY_CONFIG_FILE = path4.join(KEEPGOING_DIR, "tray-config.json");
720
859
  var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
860
+ function readTrayConfigProjects() {
861
+ try {
862
+ if (fs2.existsSync(TRAY_CONFIG_FILE)) {
863
+ const raw = JSON.parse(fs2.readFileSync(TRAY_CONFIG_FILE, "utf-8"));
864
+ if (raw && Array.isArray(raw.projects)) {
865
+ return raw.projects.filter((p) => typeof p === "string");
866
+ }
867
+ }
868
+ } catch {
869
+ }
870
+ return [];
871
+ }
721
872
  function readKnownProjects() {
722
873
  try {
723
- if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
724
- const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
874
+ if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
875
+ const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
725
876
  if (raw && Array.isArray(raw.projects)) {
726
877
  return raw;
727
878
  }
@@ -731,18 +882,18 @@ function readKnownProjects() {
731
882
  return { version: 1, projects: [] };
732
883
  }
733
884
  function writeKnownProjects(data) {
734
- if (!fs.existsSync(KEEPGOING_DIR)) {
735
- fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
885
+ if (!fs2.existsSync(KEEPGOING_DIR)) {
886
+ fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
736
887
  }
737
888
  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);
889
+ fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
890
+ fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
740
891
  }
741
892
  function registerProject(projectPath, projectName) {
742
893
  try {
743
894
  const data = readKnownProjects();
744
895
  const now = (/* @__PURE__ */ new Date()).toISOString();
745
- const name = projectName || path3.basename(projectPath);
896
+ const name = projectName || path4.basename(projectPath);
746
897
  const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
747
898
  if (existingIdx >= 0) {
748
899
  data.projects[existingIdx].lastSeen = now;
@@ -781,23 +932,23 @@ var KeepGoingWriter = class {
781
932
  currentTasksFilePath;
782
933
  constructor(workspacePath) {
783
934
  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);
935
+ this.storagePath = path5.join(mainRoot, STORAGE_DIR);
936
+ this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE);
937
+ this.stateFilePath = path5.join(this.storagePath, STATE_FILE);
938
+ this.metaFilePath = path5.join(this.storagePath, META_FILE);
939
+ this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE);
789
940
  }
790
941
  ensureDir() {
791
- if (!fs2.existsSync(this.storagePath)) {
792
- fs2.mkdirSync(this.storagePath, { recursive: true });
942
+ if (!fs3.existsSync(this.storagePath)) {
943
+ fs3.mkdirSync(this.storagePath, { recursive: true });
793
944
  }
794
945
  }
795
946
  saveCheckpoint(checkpoint, projectName) {
796
947
  this.ensureDir();
797
948
  let sessionsData;
798
949
  try {
799
- if (fs2.existsSync(this.sessionsFilePath)) {
800
- const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
950
+ if (fs3.existsSync(this.sessionsFilePath)) {
951
+ const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
801
952
  if (Array.isArray(raw)) {
802
953
  sessionsData = { version: 1, project: projectName, sessions: raw };
803
954
  } else {
@@ -815,13 +966,13 @@ var KeepGoingWriter = class {
815
966
  if (sessionsData.sessions.length > MAX_SESSIONS) {
816
967
  sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
817
968
  }
818
- fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
969
+ fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
819
970
  const state = {
820
971
  lastSessionId: checkpoint.id,
821
972
  lastKnownBranch: checkpoint.gitBranch,
822
973
  lastActivityAt: checkpoint.timestamp
823
974
  };
824
- fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
975
+ fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
825
976
  this.updateMeta(checkpoint.timestamp);
826
977
  const mainRoot = resolveStorageRoot(this.storagePath);
827
978
  registerProject(mainRoot, projectName);
@@ -829,8 +980,8 @@ var KeepGoingWriter = class {
829
980
  updateMeta(timestamp) {
830
981
  let meta;
831
982
  try {
832
- if (fs2.existsSync(this.metaFilePath)) {
833
- meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
983
+ if (fs3.existsSync(this.metaFilePath)) {
984
+ meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
834
985
  meta.lastUpdated = timestamp;
835
986
  } else {
836
987
  meta = {
@@ -846,7 +997,28 @@ var KeepGoingWriter = class {
846
997
  lastUpdated: timestamp
847
998
  };
848
999
  }
849
- fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
1000
+ fs3.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
1001
+ }
1002
+ // ---------------------------------------------------------------------------
1003
+ // Activity signals API
1004
+ // ---------------------------------------------------------------------------
1005
+ /**
1006
+ * Writes activity signals to state.json for momentum computation.
1007
+ * Performs a shallow merge: provided fields overwrite existing ones,
1008
+ * fields not provided are preserved.
1009
+ */
1010
+ writeActivitySignal(signal) {
1011
+ this.ensureDir();
1012
+ let state = {};
1013
+ try {
1014
+ if (fs3.existsSync(this.stateFilePath)) {
1015
+ state = JSON.parse(fs3.readFileSync(this.stateFilePath, "utf-8"));
1016
+ }
1017
+ } catch {
1018
+ state = {};
1019
+ }
1020
+ state.activitySignals = { ...state.activitySignals, ...signal };
1021
+ fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
850
1022
  }
851
1023
  // ---------------------------------------------------------------------------
852
1024
  // Multi-session API
@@ -854,8 +1026,8 @@ var KeepGoingWriter = class {
854
1026
  /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
855
1027
  readCurrentTasks() {
856
1028
  try {
857
- if (fs2.existsSync(this.currentTasksFilePath)) {
858
- const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
1029
+ if (fs3.existsSync(this.currentTasksFilePath)) {
1030
+ const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
859
1031
  const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
860
1032
  return this.pruneStale(tasks);
861
1033
  }
@@ -876,6 +1048,8 @@ var KeepGoingWriter = class {
876
1048
  /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
877
1049
  upsertSessionCore(update) {
878
1050
  this.ensureDir();
1051
+ if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
1052
+ if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
879
1053
  const sessionId = update.sessionId || generateSessionId(update);
880
1054
  const tasks = this.readAllTasksRaw();
881
1055
  const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
@@ -910,8 +1084,8 @@ var KeepGoingWriter = class {
910
1084
  // ---------------------------------------------------------------------------
911
1085
  readAllTasksRaw() {
912
1086
  try {
913
- if (fs2.existsSync(this.currentTasksFilePath)) {
914
- const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
1087
+ if (fs3.existsSync(this.currentTasksFilePath)) {
1088
+ const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
915
1089
  return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
916
1090
  }
917
1091
  } catch {
@@ -923,7 +1097,7 @@ var KeepGoingWriter = class {
923
1097
  }
924
1098
  writeTasksFile(tasks) {
925
1099
  const data = { version: 1, tasks };
926
- fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
1100
+ fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
927
1101
  }
928
1102
  };
929
1103
  function generateSessionId(context) {
@@ -939,57 +1113,337 @@ function generateSessionId(context) {
939
1113
  return `ses_${hash}`;
940
1114
  }
941
1115
 
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, []);
1116
+ // ../../packages/shared/src/reader.ts
1117
+ var STORAGE_DIR2 = ".keepgoing";
1118
+ var META_FILE2 = "meta.json";
1119
+ var SESSIONS_FILE2 = "sessions.json";
1120
+ var DECISIONS_FILE = "decisions.json";
1121
+ var STATE_FILE2 = "state.json";
1122
+ var CURRENT_TASKS_FILE2 = "current-tasks.json";
1123
+ var KeepGoingReader = class {
1124
+ workspacePath;
1125
+ storagePath;
1126
+ metaFilePath;
1127
+ sessionsFilePath;
1128
+ decisionsFilePath;
1129
+ stateFilePath;
1130
+ currentTasksFilePath;
1131
+ _isWorktree;
1132
+ _cachedBranch = null;
1133
+ // null = not yet resolved
1134
+ constructor(workspacePath) {
1135
+ this.workspacePath = workspacePath;
1136
+ const mainRoot = resolveStorageRoot(workspacePath);
1137
+ this._isWorktree = mainRoot !== workspacePath;
1138
+ this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1139
+ this.metaFilePath = path6.join(this.storagePath, META_FILE2);
1140
+ this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
1141
+ this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1142
+ this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
1143
+ this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
1144
+ }
1145
+ /** Check if .keepgoing/ directory exists. */
1146
+ exists() {
1147
+ return fs4.existsSync(this.storagePath);
1148
+ }
1149
+ /** Read state.json, returns undefined if missing or corrupt. */
1150
+ getState() {
1151
+ return this.readJsonFile(this.stateFilePath);
1152
+ }
1153
+ /** Read meta.json, returns undefined if missing or corrupt. */
1154
+ getMeta() {
1155
+ return this.readJsonFile(this.metaFilePath);
1156
+ }
1157
+ /**
1158
+ * Read sessions from sessions.json.
1159
+ * Handles both formats:
1160
+ * - Flat array: SessionCheckpoint[] (from ProjectStorage)
1161
+ * - Wrapper object: ProjectSessions (from SessionStorage)
1162
+ */
1163
+ getSessions() {
1164
+ return this.parseSessions().sessions;
1165
+ }
1166
+ /**
1167
+ * Get the most recent session checkpoint.
1168
+ * Uses state.lastSessionId if available, falls back to last in array.
1169
+ */
1170
+ getLastSession() {
1171
+ const { sessions, wrapperLastSessionId } = this.parseSessions();
1172
+ if (sessions.length === 0) {
1173
+ return void 0;
1174
+ }
1175
+ const state = this.getState();
1176
+ if (state?.lastSessionId) {
1177
+ const found = sessions.find((s) => s.id === state.lastSessionId);
1178
+ if (found) {
1179
+ return found;
976
1180
  }
977
- groups.get(prefix).push(body);
978
- } else {
979
- if (!groups.has("other")) {
980
- groups.set("other", []);
1181
+ }
1182
+ if (wrapperLastSessionId) {
1183
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
1184
+ if (found) {
1185
+ return found;
981
1186
  }
982
- groups.get("other").push(msg.trim());
983
1187
  }
1188
+ return sessions[sessions.length - 1];
984
1189
  }
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
- }
1190
+ /**
1191
+ * Returns the last N sessions, newest first.
1192
+ */
1193
+ getRecentSessions(count) {
1194
+ return getRecentSessions(this.getSessions(), count);
1195
+ }
1196
+ /** Read all decisions from decisions.json. */
1197
+ getDecisions() {
1198
+ return this.parseDecisions().decisions;
1199
+ }
1200
+ /** Returns the last N decisions, newest first. */
1201
+ getRecentDecisions(count) {
1202
+ const all = this.getDecisions();
1203
+ return all.slice(-count).reverse();
1204
+ }
1205
+ /** Read the multi-license store from `~/.keepgoing/license.json`. */
1206
+ getLicenseStore() {
1207
+ return readLicenseStore();
1208
+ }
1209
+ /**
1210
+ * Read all current tasks from current-tasks.json.
1211
+ * Automatically filters out stale finished sessions (> 2 hours).
1212
+ */
1213
+ getCurrentTasks() {
1214
+ const multiRaw = this.readJsonFile(this.currentTasksFilePath);
1215
+ if (multiRaw) {
1216
+ const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
1217
+ return this.pruneStale(tasks);
1218
+ }
1219
+ return [];
1220
+ }
1221
+ /** Get only active sessions (sessionActive=true and within stale threshold). */
1222
+ getActiveTasks() {
1223
+ return this.getCurrentTasks().filter((t) => t.sessionActive);
1224
+ }
1225
+ /** Get a specific session by ID. */
1226
+ getTaskBySessionId(sessionId) {
1227
+ return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
1228
+ }
1229
+ /**
1230
+ * Detect files being edited by multiple sessions simultaneously.
1231
+ * Returns pairs of session IDs and the conflicting file paths.
1232
+ */
1233
+ detectFileConflicts() {
1234
+ const activeTasks = this.getActiveTasks();
1235
+ if (activeTasks.length < 2) return [];
1236
+ const fileToSessions = /* @__PURE__ */ new Map();
1237
+ for (const task of activeTasks) {
1238
+ if (task.lastFileEdited && task.sessionId) {
1239
+ const existing = fileToSessions.get(task.lastFileEdited) ?? [];
1240
+ existing.push({
1241
+ sessionId: task.sessionId,
1242
+ agentLabel: task.agentLabel,
1243
+ branch: task.branch
1244
+ });
1245
+ fileToSessions.set(task.lastFileEdited, existing);
1246
+ }
1247
+ }
1248
+ const conflicts = [];
1249
+ for (const [file, sessions] of fileToSessions) {
1250
+ if (sessions.length > 1) {
1251
+ conflicts.push({ file, sessions });
1252
+ }
1253
+ }
1254
+ return conflicts;
1255
+ }
1256
+ /**
1257
+ * Detect sessions on the same branch (possible duplicate work).
1258
+ */
1259
+ detectBranchOverlap() {
1260
+ const activeTasks = this.getActiveTasks();
1261
+ if (activeTasks.length < 2) return [];
1262
+ const branchToSessions = /* @__PURE__ */ new Map();
1263
+ for (const task of activeTasks) {
1264
+ if (task.branch && task.sessionId) {
1265
+ const existing = branchToSessions.get(task.branch) ?? [];
1266
+ existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
1267
+ branchToSessions.set(task.branch, existing);
1268
+ }
1269
+ }
1270
+ const overlaps = [];
1271
+ for (const [branch, sessions] of branchToSessions) {
1272
+ if (sessions.length > 1) {
1273
+ overlaps.push({ branch, sessions });
1274
+ }
1275
+ }
1276
+ return overlaps;
1277
+ }
1278
+ pruneStale(tasks) {
1279
+ return pruneStaleTasks(tasks);
1280
+ }
1281
+ /** Get the last session checkpoint for a specific branch. */
1282
+ getLastSessionForBranch(branch) {
1283
+ const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
1284
+ return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1285
+ }
1286
+ /** Returns the last N sessions for a specific branch, newest first. */
1287
+ getRecentSessionsForBranch(branch, count) {
1288
+ const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1289
+ return filtered.slice(-count).reverse();
1290
+ }
1291
+ /** Returns the last N decisions for a specific branch, newest first. */
1292
+ getRecentDecisionsForBranch(branch, count) {
1293
+ const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1294
+ return filtered.slice(-count).reverse();
1295
+ }
1296
+ /** Whether the workspace is inside a git worktree. */
1297
+ get isWorktree() {
1298
+ return this._isWorktree;
1299
+ }
1300
+ /**
1301
+ * Returns the current git branch for this workspace.
1302
+ * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1303
+ */
1304
+ getCurrentBranch() {
1305
+ if (this._cachedBranch === null) {
1306
+ this._cachedBranch = getCurrentBranch(this.workspacePath);
1307
+ }
1308
+ return this._cachedBranch;
1309
+ }
1310
+ /**
1311
+ * Worktree-aware last session lookup.
1312
+ * In a worktree, scopes to the current branch with fallback to global.
1313
+ * Returns the session and whether it fell back to global.
1314
+ */
1315
+ getScopedLastSession() {
1316
+ const branch = this.getCurrentBranch();
1317
+ if (this._isWorktree && branch) {
1318
+ const scoped = this.getLastSessionForBranch(branch);
1319
+ if (scoped) return { session: scoped, isFallback: false };
1320
+ return { session: this.getLastSession(), isFallback: true };
1321
+ }
1322
+ return { session: this.getLastSession(), isFallback: false };
1323
+ }
1324
+ /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1325
+ getScopedRecentSessions(count) {
1326
+ const branch = this.getCurrentBranch();
1327
+ if (this._isWorktree && branch) {
1328
+ return this.getRecentSessionsForBranch(branch, count);
1329
+ }
1330
+ return this.getRecentSessions(count);
1331
+ }
1332
+ /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1333
+ getScopedRecentDecisions(count) {
1334
+ const branch = this.getCurrentBranch();
1335
+ if (this._isWorktree && branch) {
1336
+ return this.getRecentDecisionsForBranch(branch, count);
1337
+ }
1338
+ return this.getRecentDecisions(count);
1339
+ }
1340
+ /**
1341
+ * Resolves branch scope from an explicit `branch` parameter.
1342
+ * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1343
+ * - `"all"` returns no filter.
1344
+ * - An explicit branch name uses that.
1345
+ * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1346
+ */
1347
+ resolveBranchScope(branch) {
1348
+ if (branch === "all") {
1349
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1350
+ }
1351
+ if (branch) {
1352
+ return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1353
+ }
1354
+ const currentBranch = this.getCurrentBranch();
1355
+ if (this._isWorktree && currentBranch) {
1356
+ return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
1357
+ }
1358
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1359
+ }
1360
+ /**
1361
+ * Parses sessions.json once, returning both the session list
1362
+ * and the optional lastSessionId from a ProjectSessions wrapper.
1363
+ */
1364
+ parseSessions() {
1365
+ const raw = this.readJsonFile(
1366
+ this.sessionsFilePath
1367
+ );
1368
+ if (!raw) {
1369
+ return { sessions: [] };
1370
+ }
1371
+ if (Array.isArray(raw)) {
1372
+ return { sessions: raw };
1373
+ }
1374
+ return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
1375
+ }
1376
+ parseDecisions() {
1377
+ const raw = this.readJsonFile(this.decisionsFilePath);
1378
+ if (!raw) {
1379
+ return { decisions: [] };
1380
+ }
1381
+ return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
1382
+ }
1383
+ readJsonFile(filePath) {
1384
+ try {
1385
+ if (!fs4.existsSync(filePath)) {
1386
+ return void 0;
1387
+ }
1388
+ const raw = fs4.readFileSync(filePath, "utf-8");
1389
+ return JSON.parse(raw);
1390
+ } catch {
1391
+ return void 0;
1392
+ }
1393
+ }
1394
+ };
1395
+
1396
+ // ../../packages/shared/src/smartSummary.ts
1397
+ var PREFIX_VERBS = {
1398
+ feat: "Added",
1399
+ fix: "Fixed",
1400
+ refactor: "Refactored",
1401
+ docs: "Updated docs for",
1402
+ test: "Added tests for",
1403
+ chore: "Updated",
1404
+ style: "Styled",
1405
+ perf: "Optimized",
1406
+ ci: "Updated CI for",
1407
+ build: "Updated build for",
1408
+ revert: "Reverted"
1409
+ };
1410
+ var NOISE_PATTERNS = [
1411
+ "node_modules",
1412
+ "package-lock.json",
1413
+ "yarn.lock",
1414
+ "pnpm-lock.yaml",
1415
+ ".gitignore",
1416
+ ".DS_Store",
1417
+ "dist/",
1418
+ "out/",
1419
+ "build/"
1420
+ ];
1421
+ function categorizeCommits(messages) {
1422
+ const groups = /* @__PURE__ */ new Map();
1423
+ for (const msg of messages) {
1424
+ const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
1425
+ if (match) {
1426
+ const prefix = match[1].toLowerCase();
1427
+ const body = match[2].trim();
1428
+ if (!groups.has(prefix)) {
1429
+ groups.set(prefix, []);
1430
+ }
1431
+ groups.get(prefix).push(body);
1432
+ } else {
1433
+ if (!groups.has("other")) {
1434
+ groups.set("other", []);
1435
+ }
1436
+ groups.get("other").push(msg.trim());
1437
+ }
1438
+ }
1439
+ return groups;
1440
+ }
1441
+ function inferWorkAreas(files) {
1442
+ const areas = /* @__PURE__ */ new Map();
1443
+ for (const file of files) {
1444
+ if (NOISE_PATTERNS.some((p) => file.includes(p))) {
1445
+ continue;
1446
+ }
993
1447
  const parts = file.split("/").filter(Boolean);
994
1448
  let area;
995
1449
  if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
@@ -1101,130 +1555,221 @@ function capitalize(s) {
1101
1555
  return s.charAt(0).toUpperCase() + s.slice(1);
1102
1556
  }
1103
1557
 
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);
1558
+ // ../../packages/shared/src/contextSnapshot.ts
1559
+ import fs5 from "fs";
1560
+ import path7 from "path";
1561
+ function formatCompactRelativeTime(date) {
1562
+ const diffMs = Date.now() - date.getTime();
1563
+ if (isNaN(diffMs)) return "?";
1564
+ if (diffMs < 0) return "now";
1565
+ const seconds = Math.floor(diffMs / 1e3);
1566
+ const minutes = Math.floor(seconds / 60);
1567
+ const hours = Math.floor(minutes / 60);
1568
+ const days = Math.floor(hours / 24);
1569
+ const weeks = Math.floor(days / 7);
1570
+ if (seconds < 60) return "now";
1571
+ if (minutes < 60) return `${minutes}m ago`;
1572
+ if (hours < 24) return `${hours}h ago`;
1573
+ if (days < 7) return `${days}d ago`;
1574
+ return `${weeks}w ago`;
1120
1575
  }
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 {
1576
+ function computeMomentum(timestamp, signals) {
1577
+ if (signals) {
1578
+ if (signals.lastGitOpAt) {
1579
+ const gitOpDiffMs = Date.now() - new Date(signals.lastGitOpAt).getTime();
1580
+ if (!isNaN(gitOpDiffMs) && gitOpDiffMs >= 0 && gitOpDiffMs < 30 * 60 * 1e3) {
1581
+ return "hot";
1582
+ }
1583
+ }
1584
+ if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
1585
+ return "hot";
1586
+ }
1128
1587
  }
1129
- const id = crypto.randomUUID();
1130
- if (!fs3.existsSync(dir)) {
1131
- fs3.mkdirSync(dir, { recursive: true });
1588
+ const diffMs = Date.now() - new Date(timestamp).getTime();
1589
+ if (isNaN(diffMs) || diffMs < 0) return "hot";
1590
+ const minutes = diffMs / (1e3 * 60);
1591
+ if (minutes < 30) return "hot";
1592
+ if (minutes < 240) return "warm";
1593
+ return "cold";
1594
+ }
1595
+ function inferFocusFromBranch2(branch) {
1596
+ if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
1597
+ return void 0;
1132
1598
  }
1133
- fs3.writeFileSync(filePath, id, "utf-8");
1134
- return id;
1599
+ const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
1600
+ const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
1601
+ const stripped = branch.replace(prefixPattern, "");
1602
+ const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
1603
+ if (!cleaned) return void 0;
1604
+ return isFix ? `${cleaned} fix` : cleaned;
1135
1605
  }
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";
1606
+ function cleanCommitMessage(message, maxLen = 60) {
1607
+ if (!message) return "";
1608
+ const match = message.match(/^(?:\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
1609
+ const body = match ? match[1].trim() : message.trim();
1610
+ if (!body) return "";
1611
+ const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
1612
+ if (capitalized.length <= maxLen) return capitalized;
1613
+ return capitalized.slice(0, maxLen - 3) + "...";
1614
+ }
1615
+ function buildDoing(checkpoint, branch, recentCommitMessages) {
1616
+ if (checkpoint?.summary) return checkpoint.summary;
1617
+ const branchFocus = inferFocusFromBranch2(branch ?? checkpoint?.gitBranch);
1618
+ if (branchFocus) return branchFocus;
1619
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
1620
+ const cleaned = cleanCommitMessage(recentCommitMessages[0]);
1621
+ if (cleaned) return cleaned;
1622
+ }
1623
+ return "Unknown";
1155
1624
  }
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;
1625
+ function buildWhere(checkpoint, branch) {
1626
+ const effectiveBranch = branch ?? checkpoint?.gitBranch;
1627
+ const parts = [];
1628
+ if (effectiveBranch) {
1629
+ parts.push(effectiveBranch);
1163
1630
  }
1164
- const licensePath = getGlobalLicensePath();
1165
- let store;
1166
- try {
1167
- if (!fs3.existsSync(licensePath)) {
1168
- store = { version: 2, licenses: [] };
1631
+ if (checkpoint?.touchedFiles && checkpoint.touchedFiles.length > 0) {
1632
+ const fileNames = checkpoint.touchedFiles.slice(0, 2).map((f) => {
1633
+ const segments = f.replace(/\\/g, "/").split("/");
1634
+ return segments[segments.length - 1];
1635
+ });
1636
+ parts.push(fileNames.join(", "));
1637
+ }
1638
+ return parts.join(" \xB7 ") || "unknown";
1639
+ }
1640
+ function detectFocusArea(files) {
1641
+ if (!files || files.length === 0) return void 0;
1642
+ const areas = inferWorkAreas(files);
1643
+ if (areas.length === 0) return void 0;
1644
+ if (areas.length === 1) return areas[0];
1645
+ const topArea = areas[0];
1646
+ const areaCounts = /* @__PURE__ */ new Map();
1647
+ for (const file of files) {
1648
+ const parts = file.split("/").filter(Boolean);
1649
+ let area;
1650
+ if (parts.length <= 1) {
1651
+ area = "root";
1652
+ } else if (parts[0] === "apps" || parts[0] === "packages") {
1653
+ area = parts.length > 1 ? parts[1] : parts[0];
1654
+ } else if (parts[0] === "src") {
1655
+ area = parts.length > 1 ? parts[1] : "src";
1169
1656
  } 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
- }
1657
+ area = parts[0];
1177
1658
  }
1178
- } catch {
1179
- store = { version: 2, licenses: [] };
1659
+ areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
1180
1660
  }
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 });
1661
+ let topCount = 0;
1662
+ for (const [areaKey, count] of areaCounts) {
1663
+ if (topArea.toLowerCase().includes(areaKey.toLowerCase()) || areaKey.toLowerCase().includes(topArea.toLowerCase().split(" ")[0])) {
1664
+ topCount = Math.max(topCount, count);
1665
+ }
1189
1666
  }
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);
1667
+ if (topCount === 0) {
1668
+ topCount = Math.max(...areaCounts.values());
1202
1669
  }
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");
1670
+ const ratio = topCount / files.length;
1671
+ if (ratio >= 0.6) return topArea;
1672
+ return void 0;
1212
1673
  }
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);
1674
+ function generateContextSnapshot(projectRoot) {
1675
+ const reader = new KeepGoingReader(projectRoot);
1676
+ if (!reader.exists()) return null;
1677
+ const lastSession = reader.getLastSession();
1678
+ const state = reader.getState();
1679
+ if (!lastSession && !state) return null;
1680
+ const timestamp = state?.lastActivityAt ?? lastSession?.timestamp;
1681
+ if (!timestamp) return null;
1682
+ const branch = state?.lastKnownBranch ?? lastSession?.gitBranch;
1683
+ let activeAgents = 0;
1684
+ try {
1685
+ const activeTasks = reader.getActiveTasks();
1686
+ activeAgents = activeTasks.length;
1687
+ } catch {
1688
+ }
1689
+ const doing = buildDoing(lastSession, branch);
1690
+ const next = lastSession?.nextStep ?? "";
1691
+ const where = buildWhere(lastSession, branch);
1692
+ const when = formatCompactRelativeTime(new Date(timestamp));
1693
+ const momentum = computeMomentum(timestamp, state?.activitySignals);
1694
+ const blocker = lastSession?.blocker;
1695
+ const snapshot = {
1696
+ doing,
1697
+ next,
1698
+ where,
1699
+ when,
1700
+ momentum,
1701
+ lastActivityAt: timestamp
1702
+ };
1703
+ if (blocker) snapshot.blocker = blocker;
1704
+ if (activeAgents > 0) snapshot.activeAgents = activeAgents;
1705
+ const focusArea = detectFocusArea(lastSession?.touchedFiles ?? []);
1706
+ if (focusArea) snapshot.focusArea = focusArea;
1707
+ return snapshot;
1708
+ }
1709
+ function formatCrossProjectLine(entry) {
1710
+ const s = entry.snapshot;
1711
+ const icon = s.momentum === "hot" ? "\u26A1" : s.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
1712
+ const parts = [];
1713
+ parts.push(`${icon} ${entry.name}: ${s.doing}`);
1714
+ if (s.next) {
1715
+ parts.push(`\u2192 ${s.next}`);
1716
+ }
1717
+ let line = parts.join(" ");
1718
+ line += ` (${s.when})`;
1719
+ if (s.blocker) {
1720
+ line += ` \u26D4 ${s.blocker}`;
1721
+ }
1722
+ return line;
1723
+ }
1724
+ var MOMENTUM_RANK = { hot: 0, warm: 1, cold: 2 };
1725
+ function generateCrossProjectSummary() {
1726
+ const registry = readKnownProjects();
1727
+ const trayPaths = readTrayConfigProjects();
1728
+ const seenPaths = new Set(registry.projects.map((p) => p.path));
1729
+ const allEntries = [...registry.projects];
1730
+ for (const tp of trayPaths) {
1731
+ if (!seenPaths.has(tp)) {
1732
+ seenPaths.add(tp);
1733
+ allEntries.push({ path: tp, name: path7.basename(tp) });
1734
+ }
1735
+ }
1736
+ const projects = [];
1737
+ const seenRoots = /* @__PURE__ */ new Set();
1738
+ for (const entry of allEntries) {
1739
+ if (!fs5.existsSync(entry.path)) continue;
1740
+ const snapshot = generateContextSnapshot(entry.path);
1741
+ if (!snapshot) continue;
1742
+ let resolvedRoot;
1743
+ try {
1744
+ resolvedRoot = fs5.realpathSync(entry.path);
1745
+ } catch {
1746
+ resolvedRoot = entry.path;
1747
+ }
1748
+ if (seenRoots.has(resolvedRoot)) continue;
1749
+ seenRoots.add(resolvedRoot);
1750
+ projects.push({
1751
+ name: entry.name,
1752
+ path: entry.path,
1753
+ snapshot
1754
+ });
1755
+ }
1756
+ projects.sort((a, b) => {
1757
+ const rankA = MOMENTUM_RANK[a.snapshot.momentum ?? "cold"];
1758
+ const rankB = MOMENTUM_RANK[b.snapshot.momentum ?? "cold"];
1759
+ if (rankA !== rankB) return rankA - rankB;
1760
+ const timeA = a.snapshot.lastActivityAt ? new Date(a.snapshot.lastActivityAt).getTime() : 0;
1761
+ const timeB = b.snapshot.lastActivityAt ? new Date(b.snapshot.lastActivityAt).getTime() : 0;
1762
+ return timeB - timeA;
1218
1763
  });
1764
+ return {
1765
+ projects,
1766
+ generated: (/* @__PURE__ */ new Date()).toISOString()
1767
+ };
1219
1768
  }
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;
1227
- }
1769
+
1770
+ // ../../packages/shared/src/decisionStorage.ts
1771
+ import fs6 from "fs";
1772
+ import path8 from "path";
1228
1773
 
1229
1774
  // ../../packages/shared/src/featureGate.ts
1230
1775
  var DefaultFeatureGate = class {
@@ -1238,31 +1783,31 @@ function isDecisionsEnabled() {
1238
1783
  }
1239
1784
 
1240
1785
  // ../../packages/shared/src/decisionStorage.ts
1241
- var STORAGE_DIR2 = ".keepgoing";
1242
- var DECISIONS_FILE = "decisions.json";
1786
+ var STORAGE_DIR3 = ".keepgoing";
1787
+ var DECISIONS_FILE2 = "decisions.json";
1243
1788
  var MAX_DECISIONS = 100;
1244
1789
  var DecisionStorage = class {
1245
1790
  storagePath;
1246
1791
  decisionsFilePath;
1247
1792
  constructor(workspacePath) {
1248
1793
  const mainRoot = resolveStorageRoot(workspacePath);
1249
- this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1250
- this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1794
+ this.storagePath = path8.join(mainRoot, STORAGE_DIR3);
1795
+ this.decisionsFilePath = path8.join(this.storagePath, DECISIONS_FILE2);
1251
1796
  }
1252
1797
  ensureStorageDir() {
1253
- if (!fs4.existsSync(this.storagePath)) {
1254
- fs4.mkdirSync(this.storagePath, { recursive: true });
1798
+ if (!fs6.existsSync(this.storagePath)) {
1799
+ fs6.mkdirSync(this.storagePath, { recursive: true });
1255
1800
  }
1256
1801
  }
1257
1802
  getProjectName() {
1258
- return path6.basename(path6.dirname(this.storagePath));
1803
+ return path8.basename(path8.dirname(this.storagePath));
1259
1804
  }
1260
1805
  load() {
1261
1806
  try {
1262
- if (!fs4.existsSync(this.decisionsFilePath)) {
1807
+ if (!fs6.existsSync(this.decisionsFilePath)) {
1263
1808
  return createEmptyProjectDecisions(this.getProjectName());
1264
1809
  }
1265
- const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
1810
+ const raw = fs6.readFileSync(this.decisionsFilePath, "utf-8");
1266
1811
  const data = JSON.parse(raw);
1267
1812
  return data;
1268
1813
  } catch {
@@ -1272,7 +1817,7 @@ var DecisionStorage = class {
1272
1817
  save(decisions) {
1273
1818
  this.ensureStorageDir();
1274
1819
  const content = JSON.stringify(decisions, null, 2);
1275
- fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
1820
+ fs6.writeFileSync(this.decisionsFilePath, content, "utf-8");
1276
1821
  }
1277
1822
  /**
1278
1823
  * Save a decision record as a draft. Always persists regardless of Pro
@@ -1417,397 +1962,115 @@ function inferCategory(matchedKeywords, matchedTypes, matchedPaths) {
1417
1962
  if (/auth|oidc|oauth|redirect|origin|trusted/.test(all)) {
1418
1963
  return "auth";
1419
1964
  }
1420
- if (/migrat|postgres|db|supabase/.test(all)) {
1421
- return "migration";
1422
- }
1423
- if (/ci|workflow|build|deploy/.test(all)) {
1424
- return "deploy";
1425
- }
1426
- if (/fly|docker|k8s|terraform|infra|ops/.test(all)) {
1427
- return "infra";
1428
- }
1429
- return "unknown";
1430
- }
1431
- function classifyCommit(commit) {
1432
- const { message, filesChanged } = commit;
1433
- const messageLower = message.toLowerCase();
1434
- const parsed = parseConventionalCommit(message);
1435
- const type = commit.type ?? parsed.type;
1436
- const scope = commit.scope ?? parsed.scope;
1437
- const reasons = [];
1438
- let confidence = 0;
1439
- const matchedKeywords = MESSAGE_KEYWORDS.filter((kw) => kw.test(messageLower));
1440
- if (matchedKeywords.length > 0) {
1441
- confidence += 0.3;
1442
- const labels = matchedKeywords.slice(0, 3).map((kw) => kw.source.replace(/\\b/g, ""));
1443
- reasons.push(`commit message contains: ${labels.join(", ")}`);
1444
- }
1445
- const matchedTypes = [];
1446
- if (type && HIGH_SIGNAL_TYPES.has(type)) {
1447
- matchedTypes.push(`type:${type}`);
1448
- confidence += 0.35;
1449
- reasons.push(`conventional commit type '${type}' is high-signal`);
1450
- }
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);
1965
+ if (/migrat|postgres|db|supabase/.test(all)) {
1966
+ return "migration";
1691
1967
  }
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;
1968
+ if (/ci|workflow|build|deploy/.test(all)) {
1969
+ return "deploy";
1696
1970
  }
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();
1971
+ if (/fly|docker|k8s|terraform|infra|ops/.test(all)) {
1972
+ return "infra";
1701
1973
  }
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();
1974
+ return "unknown";
1975
+ }
1976
+ function classifyCommit(commit) {
1977
+ const { message, filesChanged } = commit;
1978
+ const messageLower = message.toLowerCase();
1979
+ const parsed = parseConventionalCommit(message);
1980
+ const type = commit.type ?? parsed.type;
1981
+ const scope = commit.scope ?? parsed.scope;
1982
+ const reasons = [];
1983
+ let confidence = 0;
1984
+ const matchedKeywords = MESSAGE_KEYWORDS.filter((kw) => kw.test(messageLower));
1985
+ if (matchedKeywords.length > 0) {
1986
+ confidence += 0.3;
1987
+ const labels = matchedKeywords.slice(0, 3).map((kw) => kw.source.replace(/\\b/g, ""));
1988
+ reasons.push(`commit message contains: ${labels.join(", ")}`);
1706
1989
  }
1707
- /** Whether the workspace is inside a git worktree. */
1708
- get isWorktree() {
1709
- return this._isWorktree;
1990
+ const matchedTypes = [];
1991
+ if (type && HIGH_SIGNAL_TYPES.has(type)) {
1992
+ matchedTypes.push(`type:${type}`);
1993
+ confidence += 0.35;
1994
+ reasons.push(`conventional commit type '${type}' is high-signal`);
1710
1995
  }
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;
1996
+ if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
1997
+ matchedTypes.push(`scope:${scope}`);
1998
+ confidence += 0.25;
1999
+ reasons.push(`conventional commit scope '${scope}' is high-signal`);
1720
2000
  }
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 };
2001
+ if (parsed.breaking) {
2002
+ confidence += 0.4;
2003
+ reasons.push("breaking change indicated by ! marker");
1734
2004
  }
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);
2005
+ const matchedPaths = [];
2006
+ let bestTier = null;
2007
+ for (const file of filesChanged) {
2008
+ const pm = matchHighSignalPath(file);
2009
+ if (pm && !matchedPaths.includes(pm.label)) {
2010
+ matchedPaths.push(pm.label);
2011
+ if (bestTier !== "infra") {
2012
+ bestTier = pm.tier;
2013
+ }
1740
2014
  }
1741
- return this.getRecentSessions(count);
1742
2015
  }
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);
2016
+ if (matchedPaths.length > 0) {
2017
+ confidence += bestTier === "infra" ? 0.4 : 0.2;
2018
+ reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
1750
2019
  }
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)` };
1768
- }
1769
- return { effectiveBranch: void 0, scopeLabel: "all branches" };
2020
+ if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
2021
+ confidence -= 0.5;
2022
+ reasons.push("all changed files are low-signal (lock files, generated code, dist)");
1770
2023
  }
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 };
2024
+ if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
2025
+ confidence -= 0.5;
2026
+ reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
1786
2027
  }
1787
- parseDecisions() {
1788
- const raw = this.readJsonFile(this.decisionsFilePath);
1789
- if (!raw) {
1790
- return { decisions: [] };
1791
- }
1792
- return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
2028
+ if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
2029
+ confidence -= 0.5;
2030
+ reasons.push("Tailwind generated CSS change (low signal)");
1793
2031
  }
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
- }
2032
+ const isDecisionCandidate = confidence >= 0.4;
2033
+ const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
2034
+ const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
2035
+ return {
2036
+ isDecisionCandidate,
2037
+ confidence: Math.max(0, Math.min(1, confidence)),
2038
+ reasons,
2039
+ category
2040
+ };
2041
+ }
2042
+ function tryDetectDecision(opts) {
2043
+ if (!isDecisionsEnabled()) {
2044
+ return void 0;
1804
2045
  }
1805
- };
2046
+ const classification = classifyCommit({
2047
+ message: opts.commitMessage,
2048
+ filesChanged: opts.filesChanged
2049
+ });
2050
+ if (!classification.isDecisionCandidate) {
2051
+ return void 0;
2052
+ }
2053
+ const decision = createDecisionRecord({
2054
+ checkpointId: opts.checkpointId,
2055
+ gitBranch: opts.gitBranch,
2056
+ commitHash: opts.commitHash,
2057
+ commitMessage: opts.commitMessage,
2058
+ filesChanged: opts.filesChanged,
2059
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2060
+ classification
2061
+ });
2062
+ const storage = new DecisionStorage(opts.workspacePath);
2063
+ storage.saveDecision(decision);
2064
+ return {
2065
+ category: classification.category,
2066
+ confidence: classification.confidence
2067
+ };
2068
+ }
1806
2069
 
1807
2070
  // ../../packages/shared/src/setup.ts
1808
- import fs6 from "fs";
2071
+ import fs7 from "fs";
1809
2072
  import os3 from "os";
1810
- import path8 from "path";
2073
+ import path9 from "path";
1811
2074
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1812
2075
  var SESSION_START_HOOK = {
1813
2076
  matcher: "",
@@ -1862,7 +2125,7 @@ function getRulesFileVersion(content) {
1862
2125
  }
1863
2126
  var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1864
2127
  function detectClaudeDir() {
1865
- return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
2128
+ return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
1866
2129
  }
1867
2130
  function hasKeepGoingHook(hookEntries) {
1868
2131
  return hookEntries.some(
@@ -1874,19 +2137,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
1874
2137
  const claudeDir2 = overrideClaudeDir || detectClaudeDir();
1875
2138
  return {
1876
2139
  claudeDir: claudeDir2,
1877
- settingsPath: path8.join(claudeDir2, "settings.json"),
1878
- claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1879
- rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
2140
+ settingsPath: path9.join(claudeDir2, "settings.json"),
2141
+ claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
2142
+ rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
1880
2143
  };
1881
2144
  }
1882
- const claudeDir = path8.join(workspacePath, ".claude");
1883
- const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1884
- const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
2145
+ const claudeDir = path9.join(workspacePath, ".claude");
2146
+ const dotClaudeMdPath = path9.join(workspacePath, ".claude", "CLAUDE.md");
2147
+ const rootClaudeMdPath = path9.join(workspacePath, "CLAUDE.md");
1885
2148
  return {
1886
2149
  claudeDir,
1887
- settingsPath: path8.join(claudeDir, "settings.json"),
1888
- claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1889
- rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
2150
+ settingsPath: path9.join(claudeDir, "settings.json"),
2151
+ claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
2152
+ rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
1890
2153
  };
1891
2154
  }
1892
2155
  function writeHooksToSettings(settings) {
@@ -1926,11 +2189,11 @@ function writeHooksToSettings(settings) {
1926
2189
  }
1927
2190
  function checkHookConflict(scope, workspacePath) {
1928
2191
  const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1929
- if (!fs6.existsSync(otherPaths.settingsPath)) {
2192
+ if (!fs7.existsSync(otherPaths.settingsPath)) {
1930
2193
  return null;
1931
2194
  }
1932
2195
  try {
1933
- const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
2196
+ const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
1934
2197
  const hooks = otherSettings?.hooks;
1935
2198
  if (!hooks) return null;
1936
2199
  const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
@@ -1959,10 +2222,10 @@ function setupProject(options) {
1959
2222
  workspacePath,
1960
2223
  claudeDirOverride
1961
2224
  );
1962
- const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
2225
+ const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
1963
2226
  let settings = {};
1964
- if (fs6.existsSync(settingsPath)) {
1965
- settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2227
+ if (fs7.existsSync(settingsPath)) {
2228
+ settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1966
2229
  }
1967
2230
  let settingsChanged = false;
1968
2231
  if (sessionHooks) {
@@ -1993,36 +2256,36 @@ function setupProject(options) {
1993
2256
  statusline?.cleanup?.();
1994
2257
  }
1995
2258
  if (settingsChanged) {
1996
- if (!fs6.existsSync(claudeDir)) {
1997
- fs6.mkdirSync(claudeDir, { recursive: true });
2259
+ if (!fs7.existsSync(claudeDir)) {
2260
+ fs7.mkdirSync(claudeDir, { recursive: true });
1998
2261
  }
1999
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2262
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2000
2263
  changed = true;
2001
2264
  }
2002
2265
  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");
2266
+ const rulesDir = path9.dirname(rulesPath);
2267
+ const rulesLabel = scope === "user" ? path9.join(path9.relative(os3.homedir(), path9.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
2268
+ if (fs7.existsSync(rulesPath)) {
2269
+ const existing = fs7.readFileSync(rulesPath, "utf-8");
2007
2270
  const existingVersion = getRulesFileVersion(existing);
2008
2271
  if (existingVersion === null) {
2009
2272
  messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
2010
2273
  } else if (existingVersion >= KEEPGOING_RULES_VERSION) {
2011
2274
  messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
2012
2275
  } else {
2013
- if (!fs6.existsSync(rulesDir)) {
2014
- fs6.mkdirSync(rulesDir, { recursive: true });
2276
+ if (!fs7.existsSync(rulesDir)) {
2277
+ fs7.mkdirSync(rulesDir, { recursive: true });
2015
2278
  }
2016
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2279
+ fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2017
2280
  changed = true;
2018
2281
  messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
2019
2282
  }
2020
2283
  } else {
2021
- const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
2022
- if (!fs6.existsSync(rulesDir)) {
2023
- fs6.mkdirSync(rulesDir, { recursive: true });
2284
+ const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
2285
+ if (!fs7.existsSync(rulesDir)) {
2286
+ fs7.mkdirSync(rulesDir, { recursive: true });
2024
2287
  }
2025
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2288
+ fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2026
2289
  changed = true;
2027
2290
  if (existingClaudeMd.includes("## KeepGoing")) {
2028
2291
  const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
@@ -2458,7 +2721,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
2458
2721
  }
2459
2722
 
2460
2723
  // src/tools/saveCheckpoint.ts
2461
- import path9 from "path";
2724
+ import path10 from "path";
2462
2725
  import { z as z4 } from "zod";
2463
2726
  function registerSaveCheckpoint(server, reader, workspacePath) {
2464
2727
  server.tool(
@@ -2470,11 +2733,14 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2470
2733
  blocker: z4.string().optional().describe("Any blocker preventing progress")
2471
2734
  },
2472
2735
  async ({ summary, nextStep, blocker }) => {
2736
+ summary = stripAgentTags(summary);
2737
+ nextStep = nextStep ? stripAgentTags(nextStep) : nextStep;
2738
+ blocker = blocker ? stripAgentTags(blocker) : blocker;
2473
2739
  const lastSession = reader.getLastSession();
2474
2740
  const gitBranch = getCurrentBranch(workspacePath);
2475
2741
  const touchedFiles = getTouchedFiles(workspacePath);
2476
2742
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2477
- const projectName = path9.basename(resolveStorageRoot(workspacePath));
2743
+ const projectName = path10.basename(resolveStorageRoot(workspacePath));
2478
2744
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2479
2745
  const checkpoint = createCheckpoint({
2480
2746
  summary,
@@ -2692,25 +2958,25 @@ function registerGetCurrentTask(server, reader) {
2692
2958
  import { z as z6 } from "zod";
2693
2959
 
2694
2960
  // src/cli/migrate.ts
2695
- import fs7 from "fs";
2961
+ import fs8 from "fs";
2696
2962
  import os4 from "os";
2697
- import path10 from "path";
2963
+ import path11 from "path";
2698
2964
  var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
2699
2965
  function isLegacyStatusline(command) {
2700
2966
  return !command.includes("--statusline") && command.includes("keepgoing-statusline");
2701
2967
  }
2702
2968
  function migrateStatusline(wsPath) {
2703
- const settingsPath = path10.join(wsPath, ".claude", "settings.json");
2704
- if (!fs7.existsSync(settingsPath)) return void 0;
2969
+ const settingsPath = path11.join(wsPath, ".claude", "settings.json");
2970
+ if (!fs8.existsSync(settingsPath)) return void 0;
2705
2971
  try {
2706
- const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
2972
+ const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
2707
2973
  const cmd = settings.statusLine?.command;
2708
2974
  if (!cmd || !isLegacyStatusline(cmd)) return void 0;
2709
2975
  settings.statusLine = {
2710
2976
  type: "command",
2711
2977
  command: STATUSLINE_CMD2
2712
2978
  };
2713
- fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2979
+ fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2714
2980
  cleanupLegacyScript();
2715
2981
  return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
2716
2982
  } catch {
@@ -2718,10 +2984,10 @@ function migrateStatusline(wsPath) {
2718
2984
  }
2719
2985
  }
2720
2986
  function cleanupLegacyScript() {
2721
- const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2722
- if (fs7.existsSync(legacyScript)) {
2987
+ const legacyScript = path11.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2988
+ if (fs8.existsSync(legacyScript)) {
2723
2989
  try {
2724
- fs7.unlinkSync(legacyScript);
2990
+ fs8.unlinkSync(legacyScript);
2725
2991
  } catch {
2726
2992
  }
2727
2993
  }
@@ -2954,6 +3220,87 @@ function registerContinueOn(server, reader, workspacePath) {
2954
3220
  );
2955
3221
  }
2956
3222
 
3223
+ // src/tools/getContextSnapshot.ts
3224
+ import { z as z10 } from "zod";
3225
+ function registerGetContextSnapshot(server, reader, workspacePath) {
3226
+ server.tool(
3227
+ "get_context_snapshot",
3228
+ "Get a compact context snapshot: what you were doing, what is next, and momentum. Use this for quick orientation without a full briefing.",
3229
+ {
3230
+ format: z10.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns a formatted single line. "json" returns the structured snapshot object.')
3231
+ },
3232
+ async ({ format }) => {
3233
+ const snapshot = generateContextSnapshot(workspacePath);
3234
+ if (!snapshot) {
3235
+ return {
3236
+ content: [
3237
+ {
3238
+ type: "text",
3239
+ text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
3240
+ }
3241
+ ]
3242
+ };
3243
+ }
3244
+ if (format === "json") {
3245
+ return {
3246
+ content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }]
3247
+ };
3248
+ }
3249
+ const icon = snapshot.momentum === "hot" ? "\u26A1" : snapshot.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
3250
+ const parts = [];
3251
+ parts.push(`${icon} ${snapshot.doing}`);
3252
+ if (snapshot.next) {
3253
+ parts.push(`\u2192 ${snapshot.next}`);
3254
+ }
3255
+ let line = parts.join(" ");
3256
+ line += ` (${snapshot.when})`;
3257
+ if (snapshot.blocker) {
3258
+ line += ` \u26D4 ${snapshot.blocker}`;
3259
+ }
3260
+ if (snapshot.activeAgents && snapshot.activeAgents > 0) {
3261
+ line += ` [${snapshot.activeAgents} active agent${snapshot.activeAgents > 1 ? "s" : ""}]`;
3262
+ }
3263
+ return {
3264
+ content: [{ type: "text", text: line }]
3265
+ };
3266
+ }
3267
+ );
3268
+ }
3269
+
3270
+ // src/tools/getWhatsHot.ts
3271
+ import { z as z11 } from "zod";
3272
+ function registerGetWhatsHot(server) {
3273
+ server.tool(
3274
+ "get_whats_hot",
3275
+ "Get a summary of activity across all registered projects, sorted by momentum. Shows what the developer is working on across their entire portfolio.",
3276
+ {
3277
+ format: z11.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns formatted lines. "json" returns the structured summary object.')
3278
+ },
3279
+ async ({ format }) => {
3280
+ const summary = generateCrossProjectSummary();
3281
+ if (summary.projects.length === 0) {
3282
+ return {
3283
+ content: [
3284
+ {
3285
+ type: "text",
3286
+ text: "No projects with activity found. Projects are registered automatically when checkpoints are saved."
3287
+ }
3288
+ ]
3289
+ };
3290
+ }
3291
+ if (format === "json") {
3292
+ return {
3293
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
3294
+ };
3295
+ }
3296
+ const lines = summary.projects.map((entry) => formatCrossProjectLine(entry));
3297
+ return {
3298
+ content: [{ type: "text", text: lines.join("\n") }]
3299
+ };
3300
+ }
3301
+ );
3302
+ }
3303
+
2957
3304
  // src/prompts/resume.ts
2958
3305
  function registerResumePrompt(server) {
2959
3306
  server.prompt(
@@ -3119,7 +3466,7 @@ async function handlePrintCurrent() {
3119
3466
  }
3120
3467
 
3121
3468
  // src/cli/saveCheckpoint.ts
3122
- import path11 from "path";
3469
+ import path12 from "path";
3123
3470
  async function handleSaveCheckpoint() {
3124
3471
  const wsPath = resolveWsPath();
3125
3472
  const reader = new KeepGoingReader(wsPath);
@@ -3147,9 +3494,9 @@ async function handleSaveCheckpoint() {
3147
3494
  sessionStartTime: lastSession?.timestamp ?? now,
3148
3495
  lastActivityTime: now
3149
3496
  });
3150
- const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
3497
+ const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path12.basename(f)).join(", ")}`;
3151
3498
  const nextStep = buildSmartNextStep(events);
3152
- const projectName = path11.basename(resolveStorageRoot(wsPath));
3499
+ const projectName = path12.basename(resolveStorageRoot(wsPath));
3153
3500
  const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
3154
3501
  const checkpoint = createCheckpoint({
3155
3502
  summary,
@@ -3194,7 +3541,7 @@ async function handleSaveCheckpoint() {
3194
3541
  }
3195
3542
 
3196
3543
  // src/cli/transcriptUtils.ts
3197
- import fs8 from "fs";
3544
+ import fs9 from "fs";
3198
3545
  var TAIL_READ_BYTES = 32768;
3199
3546
  var LATEST_LABEL_READ_BYTES = 65536;
3200
3547
  var TOOL_VERB_MAP = {
@@ -3251,9 +3598,9 @@ function isAssistantEntry(entry) {
3251
3598
  return entry.message?.role === "assistant";
3252
3599
  }
3253
3600
  function extractSessionLabel(transcriptPath) {
3254
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3601
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3255
3602
  try {
3256
- const raw = fs8.readFileSync(transcriptPath, "utf-8");
3603
+ const raw = fs9.readFileSync(transcriptPath, "utf-8");
3257
3604
  for (const line of raw.split("\n")) {
3258
3605
  const trimmed = line.trim();
3259
3606
  if (!trimmed) continue;
@@ -3266,7 +3613,9 @@ function extractSessionLabel(transcriptPath) {
3266
3613
  if (!isUserEntry(entry)) continue;
3267
3614
  let text = extractTextFromContent(entry.message?.content);
3268
3615
  if (!text) continue;
3269
- if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
3616
+ if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
3617
+ text = stripAgentTags(text);
3618
+ if (!text) continue;
3270
3619
  text = text.replace(/@[\w./\-]+/g, "").trim();
3271
3620
  text = text.replace(FILLER_PREFIX_RE, "").trim();
3272
3621
  text = text.replace(MARKDOWN_HEADING_RE, "").trim();
@@ -3282,19 +3631,19 @@ function extractSessionLabel(transcriptPath) {
3282
3631
  return null;
3283
3632
  }
3284
3633
  function extractLatestUserLabel(transcriptPath) {
3285
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3634
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3286
3635
  try {
3287
- const stat = fs8.statSync(transcriptPath);
3636
+ const stat = fs9.statSync(transcriptPath);
3288
3637
  const fileSize = stat.size;
3289
3638
  if (fileSize === 0) return null;
3290
3639
  const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
3291
3640
  const offset = fileSize - readSize;
3292
3641
  const buf = Buffer.alloc(readSize);
3293
- const fd = fs8.openSync(transcriptPath, "r");
3642
+ const fd = fs9.openSync(transcriptPath, "r");
3294
3643
  try {
3295
- fs8.readSync(fd, buf, 0, readSize, offset);
3644
+ fs9.readSync(fd, buf, 0, readSize, offset);
3296
3645
  } finally {
3297
- fs8.closeSync(fd);
3646
+ fs9.closeSync(fd);
3298
3647
  }
3299
3648
  const tail = buf.toString("utf-8");
3300
3649
  const lines = tail.split("\n").reverse();
@@ -3310,7 +3659,9 @@ function extractLatestUserLabel(transcriptPath) {
3310
3659
  if (!isUserEntry(entry)) continue;
3311
3660
  let text = extractTextFromContent(entry.message?.content);
3312
3661
  if (!text) continue;
3313
- if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
3662
+ if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
3663
+ text = stripAgentTags(text);
3664
+ if (!text) continue;
3314
3665
  text = text.replace(/@[\w./\-]+/g, "").trim();
3315
3666
  text = text.replace(FILLER_PREFIX_RE, "").trim();
3316
3667
  text = text.replace(MARKDOWN_HEADING_RE, "").trim();
@@ -3326,19 +3677,19 @@ function extractLatestUserLabel(transcriptPath) {
3326
3677
  return null;
3327
3678
  }
3328
3679
  function extractCurrentAction(transcriptPath) {
3329
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3680
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3330
3681
  try {
3331
- const stat = fs8.statSync(transcriptPath);
3682
+ const stat = fs9.statSync(transcriptPath);
3332
3683
  const fileSize = stat.size;
3333
3684
  if (fileSize === 0) return null;
3334
3685
  const readSize = Math.min(fileSize, TAIL_READ_BYTES);
3335
3686
  const offset = fileSize - readSize;
3336
3687
  const buf = Buffer.alloc(readSize);
3337
- const fd = fs8.openSync(transcriptPath, "r");
3688
+ const fd = fs9.openSync(transcriptPath, "r");
3338
3689
  try {
3339
- fs8.readSync(fd, buf, 0, readSize, offset);
3690
+ fs9.readSync(fd, buf, 0, readSize, offset);
3340
3691
  } finally {
3341
- fs8.closeSync(fd);
3692
+ fs9.closeSync(fd);
3342
3693
  }
3343
3694
  const tail = buf.toString("utf-8");
3344
3695
  const lines = tail.split("\n").reverse();
@@ -3373,6 +3724,8 @@ async function handleUpdateTask() {
3373
3724
  if (payloadStr) {
3374
3725
  try {
3375
3726
  const payload = JSON.parse(payloadStr);
3727
+ if (payload.taskSummary) payload.taskSummary = stripAgentTags(payload.taskSummary);
3728
+ if (payload.sessionLabel) payload.sessionLabel = stripAgentTags(payload.sessionLabel);
3376
3729
  const writer = new KeepGoingWriter(wsPath);
3377
3730
  const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
3378
3731
  const task = {
@@ -3440,8 +3793,8 @@ async function handleUpdateTaskFromHook() {
3440
3793
  }
3441
3794
 
3442
3795
  // src/cli/statusline.ts
3443
- import fs9 from "fs";
3444
- import path12 from "path";
3796
+ import fs10 from "fs";
3797
+ import path13 from "path";
3445
3798
  var STDIN_TIMEOUT_MS2 = 3e3;
3446
3799
  async function handleStatusline() {
3447
3800
  const chunks = [];
@@ -3471,9 +3824,9 @@ async function handleStatusline() {
3471
3824
  if (!label) {
3472
3825
  try {
3473
3826
  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"));
3827
+ const tasksFile = path13.join(gitRoot, ".keepgoing", "current-tasks.json");
3828
+ if (fs10.existsSync(tasksFile)) {
3829
+ const data = JSON.parse(fs10.readFileSync(tasksFile, "utf-8"));
3477
3830
  const tasks = pruneStaleTasks(data.tasks ?? []);
3478
3831
  const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
3479
3832
  if (match?.sessionLabel) {
@@ -3598,6 +3951,8 @@ if (flag) {
3598
3951
  registerGetCurrentTask(server, reader);
3599
3952
  registerSaveCheckpoint(server, reader, workspacePath);
3600
3953
  registerContinueOn(server, reader, workspacePath);
3954
+ registerGetContextSnapshot(server, reader, workspacePath);
3955
+ registerGetWhatsHot(server);
3601
3956
  registerSetupProject(server, workspacePath);
3602
3957
  registerActivateLicense(server);
3603
3958
  registerDeactivateLicense(server);