@keepgoingdev/mcp-server 0.7.1 → 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) {
@@ -78,7 +91,8 @@ function findGitRoot(startPath) {
78
91
  const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
79
92
  cwd: startPath,
80
93
  encoding: "utf-8",
81
- timeout: 5e3
94
+ timeout: 5e3,
95
+ stdio: ["pipe", "pipe", "pipe"]
82
96
  }).trim();
83
97
  return toplevel || startPath;
84
98
  } catch {
@@ -95,12 +109,14 @@ function resolveStorageRoot(startPath) {
95
109
  const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
96
110
  cwd: startPath,
97
111
  encoding: "utf-8",
98
- timeout: 5e3
112
+ timeout: 5e3,
113
+ stdio: ["pipe", "pipe", "pipe"]
99
114
  }).trim();
100
115
  const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
101
116
  cwd: startPath,
102
117
  encoding: "utf-8",
103
- timeout: 5e3
118
+ timeout: 5e3,
119
+ stdio: ["pipe", "pipe", "pipe"]
104
120
  }).trim();
105
121
  const absoluteCommonDir = path.resolve(toplevel, commonDir);
106
122
  const mainRoot = path.dirname(absoluteCommonDir);
@@ -703,22 +719,160 @@ function formatContinueOnPrompt(context, options) {
703
719
  return result;
704
720
  }
705
721
 
706
- // ../../packages/shared/src/storage.ts
707
- import fs2 from "fs";
708
- import path4 from "path";
709
- import { randomUUID as randomUUID2, createHash } from "crypto";
722
+ // ../../packages/shared/src/reader.ts
723
+ import fs4 from "fs";
724
+ import path6 from "path";
710
725
 
711
- // ../../packages/shared/src/registry.ts
726
+ // ../../packages/shared/src/license.ts
727
+ import crypto from "crypto";
712
728
  import fs from "fs";
713
729
  import os from "os";
714
730
  import path3 from "path";
715
- var KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
716
- 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");
717
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
+ }
718
872
  function readKnownProjects() {
719
873
  try {
720
- if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
721
- 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"));
722
876
  if (raw && Array.isArray(raw.projects)) {
723
877
  return raw;
724
878
  }
@@ -728,18 +882,18 @@ function readKnownProjects() {
728
882
  return { version: 1, projects: [] };
729
883
  }
730
884
  function writeKnownProjects(data) {
731
- if (!fs.existsSync(KEEPGOING_DIR)) {
732
- fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
885
+ if (!fs2.existsSync(KEEPGOING_DIR)) {
886
+ fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
733
887
  }
734
888
  const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
735
- fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
736
- 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);
737
891
  }
738
892
  function registerProject(projectPath, projectName) {
739
893
  try {
740
894
  const data = readKnownProjects();
741
895
  const now = (/* @__PURE__ */ new Date()).toISOString();
742
- const name = projectName || path3.basename(projectPath);
896
+ const name = projectName || path4.basename(projectPath);
743
897
  const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
744
898
  if (existingIdx >= 0) {
745
899
  data.projects[existingIdx].lastSeen = now;
@@ -778,23 +932,23 @@ var KeepGoingWriter = class {
778
932
  currentTasksFilePath;
779
933
  constructor(workspacePath) {
780
934
  const mainRoot = resolveStorageRoot(workspacePath);
781
- this.storagePath = path4.join(mainRoot, STORAGE_DIR);
782
- this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
783
- this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
784
- this.metaFilePath = path4.join(this.storagePath, META_FILE);
785
- 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);
786
940
  }
787
941
  ensureDir() {
788
- if (!fs2.existsSync(this.storagePath)) {
789
- fs2.mkdirSync(this.storagePath, { recursive: true });
942
+ if (!fs3.existsSync(this.storagePath)) {
943
+ fs3.mkdirSync(this.storagePath, { recursive: true });
790
944
  }
791
945
  }
792
946
  saveCheckpoint(checkpoint, projectName) {
793
947
  this.ensureDir();
794
948
  let sessionsData;
795
949
  try {
796
- if (fs2.existsSync(this.sessionsFilePath)) {
797
- 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"));
798
952
  if (Array.isArray(raw)) {
799
953
  sessionsData = { version: 1, project: projectName, sessions: raw };
800
954
  } else {
@@ -812,13 +966,13 @@ var KeepGoingWriter = class {
812
966
  if (sessionsData.sessions.length > MAX_SESSIONS) {
813
967
  sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
814
968
  }
815
- fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
969
+ fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
816
970
  const state = {
817
971
  lastSessionId: checkpoint.id,
818
972
  lastKnownBranch: checkpoint.gitBranch,
819
973
  lastActivityAt: checkpoint.timestamp
820
974
  };
821
- fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
975
+ fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
822
976
  this.updateMeta(checkpoint.timestamp);
823
977
  const mainRoot = resolveStorageRoot(this.storagePath);
824
978
  registerProject(mainRoot, projectName);
@@ -826,8 +980,8 @@ var KeepGoingWriter = class {
826
980
  updateMeta(timestamp) {
827
981
  let meta;
828
982
  try {
829
- if (fs2.existsSync(this.metaFilePath)) {
830
- 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"));
831
985
  meta.lastUpdated = timestamp;
832
986
  } else {
833
987
  meta = {
@@ -843,7 +997,28 @@ var KeepGoingWriter = class {
843
997
  lastUpdated: timestamp
844
998
  };
845
999
  }
846
- 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");
847
1022
  }
848
1023
  // ---------------------------------------------------------------------------
849
1024
  // Multi-session API
@@ -851,8 +1026,8 @@ var KeepGoingWriter = class {
851
1026
  /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
852
1027
  readCurrentTasks() {
853
1028
  try {
854
- if (fs2.existsSync(this.currentTasksFilePath)) {
855
- 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"));
856
1031
  const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
857
1032
  return this.pruneStale(tasks);
858
1033
  }
@@ -873,6 +1048,8 @@ var KeepGoingWriter = class {
873
1048
  /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
874
1049
  upsertSessionCore(update) {
875
1050
  this.ensureDir();
1051
+ if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
1052
+ if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
876
1053
  const sessionId = update.sessionId || generateSessionId(update);
877
1054
  const tasks = this.readAllTasksRaw();
878
1055
  const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
@@ -907,8 +1084,8 @@ var KeepGoingWriter = class {
907
1084
  // ---------------------------------------------------------------------------
908
1085
  readAllTasksRaw() {
909
1086
  try {
910
- if (fs2.existsSync(this.currentTasksFilePath)) {
911
- 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"));
912
1089
  return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
913
1090
  }
914
1091
  } catch {
@@ -920,7 +1097,7 @@ var KeepGoingWriter = class {
920
1097
  }
921
1098
  writeTasksFile(tasks) {
922
1099
  const data = { version: 1, tasks };
923
- fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
1100
+ fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
924
1101
  }
925
1102
  };
926
1103
  function generateSessionId(context) {
@@ -936,54 +1113,334 @@ function generateSessionId(context) {
936
1113
  return `ses_${hash}`;
937
1114
  }
938
1115
 
939
- // ../../packages/shared/src/smartSummary.ts
940
- var PREFIX_VERBS = {
941
- feat: "Added",
942
- fix: "Fixed",
943
- refactor: "Refactored",
944
- docs: "Updated docs for",
945
- test: "Added tests for",
946
- chore: "Updated",
947
- style: "Styled",
948
- perf: "Optimized",
949
- ci: "Updated CI for",
950
- build: "Updated build for",
951
- revert: "Reverted"
952
- };
953
- var NOISE_PATTERNS = [
954
- "node_modules",
955
- "package-lock.json",
956
- "yarn.lock",
957
- "pnpm-lock.yaml",
958
- ".gitignore",
959
- ".DS_Store",
960
- "dist/",
961
- "out/",
962
- "build/"
963
- ];
964
- function categorizeCommits(messages) {
965
- const groups = /* @__PURE__ */ new Map();
966
- for (const msg of messages) {
967
- const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
968
- if (match) {
969
- const prefix = match[1].toLowerCase();
970
- const body = match[2].trim();
971
- if (!groups.has(prefix)) {
972
- 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;
973
1180
  }
974
- groups.get(prefix).push(body);
975
- } else {
976
- if (!groups.has("other")) {
977
- groups.set("other", []);
1181
+ }
1182
+ if (wrapperLastSessionId) {
1183
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
1184
+ if (found) {
1185
+ return found;
978
1186
  }
979
- groups.get("other").push(msg.trim());
980
1187
  }
1188
+ return sessions[sessions.length - 1];
981
1189
  }
982
- return groups;
983
- }
984
- function inferWorkAreas(files) {
985
- const areas = /* @__PURE__ */ new Map();
986
- for (const file of files) {
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) {
987
1444
  if (NOISE_PATTERNS.some((p) => file.includes(p))) {
988
1445
  continue;
989
1446
  }
@@ -1098,131 +1555,222 @@ function capitalize(s) {
1098
1555
  return s.charAt(0).toUpperCase() + s.slice(1);
1099
1556
  }
1100
1557
 
1101
- // ../../packages/shared/src/decisionStorage.ts
1102
- import fs4 from "fs";
1103
- import path6 from "path";
1104
-
1105
- // ../../packages/shared/src/license.ts
1106
- import crypto from "crypto";
1107
- import fs3 from "fs";
1108
- import os2 from "os";
1109
- import path5 from "path";
1110
- var LICENSE_FILE = "license.json";
1111
- var DEVICE_ID_FILE = "device-id";
1112
- function getGlobalLicenseDir() {
1113
- return path5.join(os2.homedir(), ".keepgoing");
1114
- }
1115
- function getGlobalLicensePath() {
1116
- 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`;
1117
1575
  }
1118
- function getDeviceId() {
1119
- const dir = getGlobalLicenseDir();
1120
- const filePath = path5.join(dir, DEVICE_ID_FILE);
1121
- try {
1122
- const existing = fs3.readFileSync(filePath, "utf-8").trim();
1123
- if (existing) return existing;
1124
- } 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
+ }
1125
1587
  }
1126
- const id = crypto.randomUUID();
1127
- if (!fs3.existsSync(dir)) {
1128
- 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;
1129
1598
  }
1130
- fs3.writeFileSync(filePath, id, "utf-8");
1131
- 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;
1132
1605
  }
1133
- var DECISION_DETECTION_VARIANT_ID = 1361527;
1134
- var SESSION_AWARENESS_VARIANT_ID = 1366510;
1135
- var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
1136
- var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
1137
- var VARIANT_FEATURE_MAP = {
1138
- [DECISION_DETECTION_VARIANT_ID]: ["decisions"],
1139
- [SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
1140
- [TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
1141
- [TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
1142
- // Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
1143
- };
1144
- var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
1145
- function getVariantLabel(variantId) {
1146
- const features = VARIANT_FEATURE_MAP[variantId];
1147
- if (!features) return "Unknown Add-on";
1148
- if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
1149
- if (features.includes("decisions")) return "Decision Detection";
1150
- if (features.includes("session-awareness")) return "Session Awareness";
1151
- 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";
1152
1624
  }
1153
- var _cachedStore;
1154
- var _cacheTimestamp = 0;
1155
- var LICENSE_CACHE_TTL_MS = 2e3;
1156
- function readLicenseStore() {
1157
- const now = Date.now();
1158
- if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
1159
- return _cachedStore;
1625
+ function buildWhere(checkpoint, branch) {
1626
+ const effectiveBranch = branch ?? checkpoint?.gitBranch;
1627
+ const parts = [];
1628
+ if (effectiveBranch) {
1629
+ parts.push(effectiveBranch);
1160
1630
  }
1161
- const licensePath = getGlobalLicensePath();
1162
- let store;
1163
- try {
1164
- if (!fs3.existsSync(licensePath)) {
1165
- 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";
1166
1656
  } else {
1167
- const raw = fs3.readFileSync(licensePath, "utf-8");
1168
- const data = JSON.parse(raw);
1169
- if (data?.version === 2 && Array.isArray(data.licenses)) {
1170
- store = data;
1171
- } else {
1172
- store = { version: 2, licenses: [] };
1173
- }
1657
+ area = parts[0];
1174
1658
  }
1175
- } catch {
1176
- store = { version: 2, licenses: [] };
1659
+ areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
1177
1660
  }
1178
- _cachedStore = store;
1179
- _cacheTimestamp = now;
1180
- return store;
1181
- }
1182
- function writeLicenseStore(store) {
1183
- const dirPath = getGlobalLicenseDir();
1184
- if (!fs3.existsSync(dirPath)) {
1185
- 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
+ }
1186
1666
  }
1187
- const licensePath = path5.join(dirPath, LICENSE_FILE);
1188
- fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
1189
- _cachedStore = store;
1190
- _cacheTimestamp = Date.now();
1191
- }
1192
- function addLicenseEntry(entry) {
1193
- const store = readLicenseStore();
1194
- const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
1195
- if (idx >= 0) {
1196
- store.licenses[idx] = entry;
1197
- } else {
1198
- store.licenses.push(entry);
1667
+ if (topCount === 0) {
1668
+ topCount = Math.max(...areaCounts.values());
1199
1669
  }
1200
- writeLicenseStore(store);
1201
- }
1202
- function removeLicenseEntry(licenseKey) {
1203
- const store = readLicenseStore();
1204
- store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
1205
- writeLicenseStore(store);
1206
- }
1207
- function getActiveLicenses() {
1208
- 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;
1209
1673
  }
1210
- function getLicenseForFeature(feature) {
1211
- const active = getActiveLicenses();
1212
- return active.find((l) => {
1213
- const features = VARIANT_FEATURE_MAP[l.variantId];
1214
- 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;
1215
1763
  });
1216
- }
1217
- function getAllLicensesNeedingRevalidation() {
1218
- return getActiveLicenses().filter((l) => needsRevalidation(l));
1219
- }
1220
- var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1221
- function needsRevalidation(entry) {
1222
- const lastValidated = new Date(entry.lastValidatedAt).getTime();
1223
- return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
1764
+ return {
1765
+ projects,
1766
+ generated: (/* @__PURE__ */ new Date()).toISOString()
1767
+ };
1224
1768
  }
1225
1769
 
1770
+ // ../../packages/shared/src/decisionStorage.ts
1771
+ import fs6 from "fs";
1772
+ import path8 from "path";
1773
+
1226
1774
  // ../../packages/shared/src/featureGate.ts
1227
1775
  var DefaultFeatureGate = class {
1228
1776
  isEnabled(_feature) {
@@ -1235,31 +1783,31 @@ function isDecisionsEnabled() {
1235
1783
  }
1236
1784
 
1237
1785
  // ../../packages/shared/src/decisionStorage.ts
1238
- var STORAGE_DIR2 = ".keepgoing";
1239
- var DECISIONS_FILE = "decisions.json";
1786
+ var STORAGE_DIR3 = ".keepgoing";
1787
+ var DECISIONS_FILE2 = "decisions.json";
1240
1788
  var MAX_DECISIONS = 100;
1241
1789
  var DecisionStorage = class {
1242
1790
  storagePath;
1243
1791
  decisionsFilePath;
1244
1792
  constructor(workspacePath) {
1245
1793
  const mainRoot = resolveStorageRoot(workspacePath);
1246
- this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1247
- 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);
1248
1796
  }
1249
1797
  ensureStorageDir() {
1250
- if (!fs4.existsSync(this.storagePath)) {
1251
- fs4.mkdirSync(this.storagePath, { recursive: true });
1798
+ if (!fs6.existsSync(this.storagePath)) {
1799
+ fs6.mkdirSync(this.storagePath, { recursive: true });
1252
1800
  }
1253
1801
  }
1254
1802
  getProjectName() {
1255
- return path6.basename(path6.dirname(this.storagePath));
1803
+ return path8.basename(path8.dirname(this.storagePath));
1256
1804
  }
1257
1805
  load() {
1258
1806
  try {
1259
- if (!fs4.existsSync(this.decisionsFilePath)) {
1807
+ if (!fs6.existsSync(this.decisionsFilePath)) {
1260
1808
  return createEmptyProjectDecisions(this.getProjectName());
1261
1809
  }
1262
- const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
1810
+ const raw = fs6.readFileSync(this.decisionsFilePath, "utf-8");
1263
1811
  const data = JSON.parse(raw);
1264
1812
  return data;
1265
1813
  } catch {
@@ -1269,7 +1817,7 @@ var DecisionStorage = class {
1269
1817
  save(decisions) {
1270
1818
  this.ensureStorageDir();
1271
1819
  const content = JSON.stringify(decisions, null, 2);
1272
- fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
1820
+ fs6.writeFileSync(this.decisionsFilePath, content, "utf-8");
1273
1821
  }
1274
1822
  /**
1275
1823
  * Save a decision record as a draft. Always persists regardless of Pro
@@ -1442,369 +1990,87 @@ function classifyCommit(commit) {
1442
1990
  const matchedTypes = [];
1443
1991
  if (type && HIGH_SIGNAL_TYPES.has(type)) {
1444
1992
  matchedTypes.push(`type:${type}`);
1445
- confidence += 0.35;
1446
- reasons.push(`conventional commit type '${type}' is high-signal`);
1447
- }
1448
- if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
1449
- matchedTypes.push(`scope:${scope}`);
1450
- confidence += 0.25;
1451
- reasons.push(`conventional commit scope '${scope}' is high-signal`);
1452
- }
1453
- if (parsed.breaking) {
1454
- confidence += 0.4;
1455
- reasons.push("breaking change indicated by ! marker");
1456
- }
1457
- const matchedPaths = [];
1458
- let bestTier = null;
1459
- for (const file of filesChanged) {
1460
- const pm = matchHighSignalPath(file);
1461
- if (pm && !matchedPaths.includes(pm.label)) {
1462
- matchedPaths.push(pm.label);
1463
- if (bestTier !== "infra") {
1464
- bestTier = pm.tier;
1465
- }
1466
- }
1467
- }
1468
- if (matchedPaths.length > 0) {
1469
- confidence += bestTier === "infra" ? 0.4 : 0.2;
1470
- reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
1471
- }
1472
- if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
1473
- confidence -= 0.5;
1474
- reasons.push("all changed files are low-signal (lock files, generated code, dist)");
1475
- }
1476
- if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
1477
- confidence -= 0.5;
1478
- reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
1479
- }
1480
- if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
1481
- confidence -= 0.5;
1482
- reasons.push("Tailwind generated CSS change (low signal)");
1483
- }
1484
- const isDecisionCandidate = confidence >= 0.4;
1485
- const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
1486
- const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
1487
- return {
1488
- isDecisionCandidate,
1489
- confidence: Math.max(0, Math.min(1, confidence)),
1490
- reasons,
1491
- category
1492
- };
1493
- }
1494
- function tryDetectDecision(opts) {
1495
- if (!isDecisionsEnabled()) {
1496
- return void 0;
1497
- }
1498
- const classification = classifyCommit({
1499
- message: opts.commitMessage,
1500
- filesChanged: opts.filesChanged
1501
- });
1502
- if (!classification.isDecisionCandidate) {
1503
- return void 0;
1504
- }
1505
- const decision = createDecisionRecord({
1506
- checkpointId: opts.checkpointId,
1507
- gitBranch: opts.gitBranch,
1508
- commitHash: opts.commitHash,
1509
- commitMessage: opts.commitMessage,
1510
- filesChanged: opts.filesChanged,
1511
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1512
- classification
1513
- });
1514
- const storage = new DecisionStorage(opts.workspacePath);
1515
- storage.saveDecision(decision);
1516
- return {
1517
- category: classification.category,
1518
- confidence: classification.confidence
1519
- };
1520
- }
1521
-
1522
- // ../../packages/shared/src/reader.ts
1523
- import fs5 from "fs";
1524
- import path7 from "path";
1525
- var STORAGE_DIR3 = ".keepgoing";
1526
- var META_FILE2 = "meta.json";
1527
- var SESSIONS_FILE2 = "sessions.json";
1528
- var DECISIONS_FILE2 = "decisions.json";
1529
- var STATE_FILE2 = "state.json";
1530
- var CURRENT_TASKS_FILE2 = "current-tasks.json";
1531
- var KeepGoingReader = class {
1532
- workspacePath;
1533
- storagePath;
1534
- metaFilePath;
1535
- sessionsFilePath;
1536
- decisionsFilePath;
1537
- stateFilePath;
1538
- currentTasksFilePath;
1539
- _isWorktree;
1540
- _cachedBranch = null;
1541
- // null = not yet resolved
1542
- constructor(workspacePath) {
1543
- this.workspacePath = workspacePath;
1544
- const mainRoot = resolveStorageRoot(workspacePath);
1545
- this._isWorktree = mainRoot !== workspacePath;
1546
- this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
1547
- this.metaFilePath = path7.join(this.storagePath, META_FILE2);
1548
- this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
1549
- this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
1550
- this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
1551
- this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
1552
- }
1553
- /** Check if .keepgoing/ directory exists. */
1554
- exists() {
1555
- return fs5.existsSync(this.storagePath);
1556
- }
1557
- /** Read state.json, returns undefined if missing or corrupt. */
1558
- getState() {
1559
- return this.readJsonFile(this.stateFilePath);
1560
- }
1561
- /** Read meta.json, returns undefined if missing or corrupt. */
1562
- getMeta() {
1563
- return this.readJsonFile(this.metaFilePath);
1564
- }
1565
- /**
1566
- * Read sessions from sessions.json.
1567
- * Handles both formats:
1568
- * - Flat array: SessionCheckpoint[] (from ProjectStorage)
1569
- * - Wrapper object: ProjectSessions (from SessionStorage)
1570
- */
1571
- getSessions() {
1572
- return this.parseSessions().sessions;
1573
- }
1574
- /**
1575
- * Get the most recent session checkpoint.
1576
- * Uses state.lastSessionId if available, falls back to last in array.
1577
- */
1578
- getLastSession() {
1579
- const { sessions, wrapperLastSessionId } = this.parseSessions();
1580
- if (sessions.length === 0) {
1581
- return void 0;
1582
- }
1583
- const state = this.getState();
1584
- if (state?.lastSessionId) {
1585
- const found = sessions.find((s) => s.id === state.lastSessionId);
1586
- if (found) {
1587
- return found;
1588
- }
1589
- }
1590
- if (wrapperLastSessionId) {
1591
- const found = sessions.find((s) => s.id === wrapperLastSessionId);
1592
- if (found) {
1593
- return found;
1594
- }
1595
- }
1596
- return sessions[sessions.length - 1];
1597
- }
1598
- /**
1599
- * Returns the last N sessions, newest first.
1600
- */
1601
- getRecentSessions(count) {
1602
- return getRecentSessions(this.getSessions(), count);
1603
- }
1604
- /** Read all decisions from decisions.json. */
1605
- getDecisions() {
1606
- return this.parseDecisions().decisions;
1607
- }
1608
- /** Returns the last N decisions, newest first. */
1609
- getRecentDecisions(count) {
1610
- const all = this.getDecisions();
1611
- return all.slice(-count).reverse();
1612
- }
1613
- /** Read the multi-license store from `~/.keepgoing/license.json`. */
1614
- getLicenseStore() {
1615
- return readLicenseStore();
1616
- }
1617
- /**
1618
- * Read all current tasks from current-tasks.json.
1619
- * Automatically filters out stale finished sessions (> 2 hours).
1620
- */
1621
- getCurrentTasks() {
1622
- const multiRaw = this.readJsonFile(this.currentTasksFilePath);
1623
- if (multiRaw) {
1624
- const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
1625
- return this.pruneStale(tasks);
1626
- }
1627
- return [];
1628
- }
1629
- /** Get only active sessions (sessionActive=true and within stale threshold). */
1630
- getActiveTasks() {
1631
- return this.getCurrentTasks().filter((t) => t.sessionActive);
1632
- }
1633
- /** Get a specific session by ID. */
1634
- getTaskBySessionId(sessionId) {
1635
- return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
1636
- }
1637
- /**
1638
- * Detect files being edited by multiple sessions simultaneously.
1639
- * Returns pairs of session IDs and the conflicting file paths.
1640
- */
1641
- detectFileConflicts() {
1642
- const activeTasks = this.getActiveTasks();
1643
- if (activeTasks.length < 2) return [];
1644
- const fileToSessions = /* @__PURE__ */ new Map();
1645
- for (const task of activeTasks) {
1646
- if (task.lastFileEdited && task.sessionId) {
1647
- const existing = fileToSessions.get(task.lastFileEdited) ?? [];
1648
- existing.push({
1649
- sessionId: task.sessionId,
1650
- agentLabel: task.agentLabel,
1651
- branch: task.branch
1652
- });
1653
- fileToSessions.set(task.lastFileEdited, existing);
1654
- }
1655
- }
1656
- const conflicts = [];
1657
- for (const [file, sessions] of fileToSessions) {
1658
- if (sessions.length > 1) {
1659
- conflicts.push({ file, sessions });
1660
- }
1661
- }
1662
- return conflicts;
1663
- }
1664
- /**
1665
- * Detect sessions on the same branch (possible duplicate work).
1666
- */
1667
- detectBranchOverlap() {
1668
- const activeTasks = this.getActiveTasks();
1669
- if (activeTasks.length < 2) return [];
1670
- const branchToSessions = /* @__PURE__ */ new Map();
1671
- for (const task of activeTasks) {
1672
- if (task.branch && task.sessionId) {
1673
- const existing = branchToSessions.get(task.branch) ?? [];
1674
- existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
1675
- branchToSessions.set(task.branch, existing);
1676
- }
1677
- }
1678
- const overlaps = [];
1679
- for (const [branch, sessions] of branchToSessions) {
1680
- if (sessions.length > 1) {
1681
- overlaps.push({ branch, sessions });
1682
- }
1683
- }
1684
- return overlaps;
1685
- }
1686
- pruneStale(tasks) {
1687
- return pruneStaleTasks(tasks);
1688
- }
1689
- /** Get the last session checkpoint for a specific branch. */
1690
- getLastSessionForBranch(branch) {
1691
- const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
1692
- return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1693
- }
1694
- /** Returns the last N sessions for a specific branch, newest first. */
1695
- getRecentSessionsForBranch(branch, count) {
1696
- const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1697
- return filtered.slice(-count).reverse();
1698
- }
1699
- /** Returns the last N decisions for a specific branch, newest first. */
1700
- getRecentDecisionsForBranch(branch, count) {
1701
- const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1702
- return filtered.slice(-count).reverse();
1703
- }
1704
- /** Whether the workspace is inside a git worktree. */
1705
- get isWorktree() {
1706
- return this._isWorktree;
1707
- }
1708
- /**
1709
- * Returns the current git branch for this workspace.
1710
- * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1711
- */
1712
- getCurrentBranch() {
1713
- if (this._cachedBranch === null) {
1714
- this._cachedBranch = getCurrentBranch(this.workspacePath);
1715
- }
1716
- return this._cachedBranch;
1993
+ confidence += 0.35;
1994
+ reasons.push(`conventional commit type '${type}' is high-signal`);
1717
1995
  }
1718
- /**
1719
- * Worktree-aware last session lookup.
1720
- * In a worktree, scopes to the current branch with fallback to global.
1721
- * Returns the session and whether it fell back to global.
1722
- */
1723
- getScopedLastSession() {
1724
- const branch = this.getCurrentBranch();
1725
- if (this._isWorktree && branch) {
1726
- const scoped = this.getLastSessionForBranch(branch);
1727
- if (scoped) return { session: scoped, isFallback: false };
1728
- return { session: this.getLastSession(), isFallback: true };
1729
- }
1730
- return { session: this.getLastSession(), isFallback: false };
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`);
1731
2000
  }
1732
- /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1733
- getScopedRecentSessions(count) {
1734
- const branch = this.getCurrentBranch();
1735
- if (this._isWorktree && branch) {
1736
- return this.getRecentSessionsForBranch(branch, count);
1737
- }
1738
- return this.getRecentSessions(count);
2001
+ if (parsed.breaking) {
2002
+ confidence += 0.4;
2003
+ reasons.push("breaking change indicated by ! marker");
1739
2004
  }
1740
- /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1741
- getScopedRecentDecisions(count) {
1742
- const branch = this.getCurrentBranch();
1743
- if (this._isWorktree && branch) {
1744
- return this.getRecentDecisionsForBranch(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
+ }
1745
2014
  }
1746
- return this.getRecentDecisions(count);
1747
2015
  }
1748
- /**
1749
- * Resolves branch scope from an explicit `branch` parameter.
1750
- * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1751
- * - `"all"` returns no filter.
1752
- * - An explicit branch name uses that.
1753
- * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1754
- */
1755
- resolveBranchScope(branch) {
1756
- if (branch === "all") {
1757
- return { effectiveBranch: void 0, scopeLabel: "all branches" };
1758
- }
1759
- if (branch) {
1760
- return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1761
- }
1762
- const currentBranch = this.getCurrentBranch();
1763
- if (this._isWorktree && currentBranch) {
1764
- return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
1765
- }
1766
- return { effectiveBranch: void 0, scopeLabel: "all branches" };
2016
+ if (matchedPaths.length > 0) {
2017
+ confidence += bestTier === "infra" ? 0.4 : 0.2;
2018
+ reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
1767
2019
  }
1768
- /**
1769
- * Parses sessions.json once, returning both the session list
1770
- * and the optional lastSessionId from a ProjectSessions wrapper.
1771
- */
1772
- parseSessions() {
1773
- const raw = this.readJsonFile(
1774
- this.sessionsFilePath
1775
- );
1776
- if (!raw) {
1777
- return { sessions: [] };
1778
- }
1779
- if (Array.isArray(raw)) {
1780
- return { sessions: raw };
1781
- }
1782
- return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
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)");
1783
2023
  }
1784
- parseDecisions() {
1785
- const raw = this.readJsonFile(this.decisionsFilePath);
1786
- if (!raw) {
1787
- return { decisions: [] };
1788
- }
1789
- return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
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)");
1790
2027
  }
1791
- readJsonFile(filePath) {
1792
- try {
1793
- if (!fs5.existsSync(filePath)) {
1794
- return void 0;
1795
- }
1796
- const raw = fs5.readFileSync(filePath, "utf-8");
1797
- return JSON.parse(raw);
1798
- } catch {
1799
- return void 0;
1800
- }
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)");
1801
2031
  }
1802
- };
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;
2045
+ }
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
+ }
1803
2069
 
1804
2070
  // ../../packages/shared/src/setup.ts
1805
- import fs6 from "fs";
2071
+ import fs7 from "fs";
1806
2072
  import os3 from "os";
1807
- import path8 from "path";
2073
+ import path9 from "path";
1808
2074
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1809
2075
  var SESSION_START_HOOK = {
1810
2076
  matcher: "",
@@ -1842,12 +2108,14 @@ var SESSION_END_HOOK = {
1842
2108
  }
1843
2109
  ]
1844
2110
  };
1845
- var KEEPGOING_RULES_VERSION = 1;
2111
+ var KEEPGOING_RULES_VERSION = 2;
1846
2112
  var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
1847
2113
  ## KeepGoing
1848
2114
 
2115
+ When you see KeepGoing momentum data in your session context (from a SessionStart hook), share a brief welcome with the user that includes: what was last worked on, the suggested next step, and any blockers.
2116
+
1849
2117
  After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
1850
- - \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
2118
+ - \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
1851
2119
  - \`nextStep\`: What to do next
1852
2120
  - \`blocker\`: Any blocker (if applicable)
1853
2121
  `;
@@ -1857,7 +2125,7 @@ function getRulesFileVersion(content) {
1857
2125
  }
1858
2126
  var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1859
2127
  function detectClaudeDir() {
1860
- return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
2128
+ return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
1861
2129
  }
1862
2130
  function hasKeepGoingHook(hookEntries) {
1863
2131
  return hookEntries.some(
@@ -1869,19 +2137,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
1869
2137
  const claudeDir2 = overrideClaudeDir || detectClaudeDir();
1870
2138
  return {
1871
2139
  claudeDir: claudeDir2,
1872
- settingsPath: path8.join(claudeDir2, "settings.json"),
1873
- claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1874
- 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")
1875
2143
  };
1876
2144
  }
1877
- const claudeDir = path8.join(workspacePath, ".claude");
1878
- const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1879
- 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");
1880
2148
  return {
1881
2149
  claudeDir,
1882
- settingsPath: path8.join(claudeDir, "settings.json"),
1883
- claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1884
- 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")
1885
2153
  };
1886
2154
  }
1887
2155
  function writeHooksToSettings(settings) {
@@ -1921,11 +2189,11 @@ function writeHooksToSettings(settings) {
1921
2189
  }
1922
2190
  function checkHookConflict(scope, workspacePath) {
1923
2191
  const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1924
- if (!fs6.existsSync(otherPaths.settingsPath)) {
2192
+ if (!fs7.existsSync(otherPaths.settingsPath)) {
1925
2193
  return null;
1926
2194
  }
1927
2195
  try {
1928
- const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
2196
+ const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
1929
2197
  const hooks = otherSettings?.hooks;
1930
2198
  if (!hooks) return null;
1931
2199
  const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
@@ -1954,10 +2222,10 @@ function setupProject(options) {
1954
2222
  workspacePath,
1955
2223
  claudeDirOverride
1956
2224
  );
1957
- 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";
1958
2226
  let settings = {};
1959
- if (fs6.existsSync(settingsPath)) {
1960
- settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2227
+ if (fs7.existsSync(settingsPath)) {
2228
+ settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1961
2229
  }
1962
2230
  let settingsChanged = false;
1963
2231
  if (sessionHooks) {
@@ -1973,7 +2241,7 @@ function setupProject(options) {
1973
2241
  messages.push(`Warning: ${conflict}`);
1974
2242
  }
1975
2243
  }
1976
- if (scope === "project") {
2244
+ {
1977
2245
  const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
1978
2246
  if (!settings.statusLine || needsUpdate) {
1979
2247
  settings.statusLine = {
@@ -1981,43 +2249,43 @@ function setupProject(options) {
1981
2249
  command: STATUSLINE_CMD
1982
2250
  };
1983
2251
  settingsChanged = true;
1984
- messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : "Statusline: Added to .claude/settings.json");
2252
+ messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : `Statusline: Added to ${scopeLabel}`);
1985
2253
  } else {
1986
2254
  messages.push("Statusline: Already configured in settings, skipped");
1987
2255
  }
1988
2256
  statusline?.cleanup?.();
1989
2257
  }
1990
2258
  if (settingsChanged) {
1991
- if (!fs6.existsSync(claudeDir)) {
1992
- fs6.mkdirSync(claudeDir, { recursive: true });
2259
+ if (!fs7.existsSync(claudeDir)) {
2260
+ fs7.mkdirSync(claudeDir, { recursive: true });
1993
2261
  }
1994
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2262
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1995
2263
  changed = true;
1996
2264
  }
1997
2265
  if (claudeMd) {
1998
- const rulesDir = path8.dirname(rulesPath);
1999
- const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
2000
- if (fs6.existsSync(rulesPath)) {
2001
- 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");
2002
2270
  const existingVersion = getRulesFileVersion(existing);
2003
2271
  if (existingVersion === null) {
2004
2272
  messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
2005
2273
  } else if (existingVersion >= KEEPGOING_RULES_VERSION) {
2006
2274
  messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
2007
2275
  } else {
2008
- if (!fs6.existsSync(rulesDir)) {
2009
- fs6.mkdirSync(rulesDir, { recursive: true });
2276
+ if (!fs7.existsSync(rulesDir)) {
2277
+ fs7.mkdirSync(rulesDir, { recursive: true });
2010
2278
  }
2011
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2279
+ fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2012
2280
  changed = true;
2013
2281
  messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
2014
2282
  }
2015
2283
  } else {
2016
- const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
2017
- if (!fs6.existsSync(rulesDir)) {
2018
- 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 });
2019
2287
  }
2020
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2288
+ fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2021
2289
  changed = true;
2022
2290
  if (existingClaudeMd.includes("## KeepGoing")) {
2023
2291
  const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
@@ -2453,7 +2721,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
2453
2721
  }
2454
2722
 
2455
2723
  // src/tools/saveCheckpoint.ts
2456
- import path9 from "path";
2724
+ import path10 from "path";
2457
2725
  import { z as z4 } from "zod";
2458
2726
  function registerSaveCheckpoint(server, reader, workspacePath) {
2459
2727
  server.tool(
@@ -2465,11 +2733,14 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2465
2733
  blocker: z4.string().optional().describe("Any blocker preventing progress")
2466
2734
  },
2467
2735
  async ({ summary, nextStep, blocker }) => {
2736
+ summary = stripAgentTags(summary);
2737
+ nextStep = nextStep ? stripAgentTags(nextStep) : nextStep;
2738
+ blocker = blocker ? stripAgentTags(blocker) : blocker;
2468
2739
  const lastSession = reader.getLastSession();
2469
2740
  const gitBranch = getCurrentBranch(workspacePath);
2470
2741
  const touchedFiles = getTouchedFiles(workspacePath);
2471
2742
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2472
- const projectName = path9.basename(resolveStorageRoot(workspacePath));
2743
+ const projectName = path10.basename(resolveStorageRoot(workspacePath));
2473
2744
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2474
2745
  const checkpoint = createCheckpoint({
2475
2746
  summary,
@@ -2687,25 +2958,25 @@ function registerGetCurrentTask(server, reader) {
2687
2958
  import { z as z6 } from "zod";
2688
2959
 
2689
2960
  // src/cli/migrate.ts
2690
- import fs7 from "fs";
2961
+ import fs8 from "fs";
2691
2962
  import os4 from "os";
2692
- import path10 from "path";
2963
+ import path11 from "path";
2693
2964
  var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
2694
2965
  function isLegacyStatusline(command) {
2695
2966
  return !command.includes("--statusline") && command.includes("keepgoing-statusline");
2696
2967
  }
2697
2968
  function migrateStatusline(wsPath) {
2698
- const settingsPath = path10.join(wsPath, ".claude", "settings.json");
2699
- if (!fs7.existsSync(settingsPath)) return void 0;
2969
+ const settingsPath = path11.join(wsPath, ".claude", "settings.json");
2970
+ if (!fs8.existsSync(settingsPath)) return void 0;
2700
2971
  try {
2701
- const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
2972
+ const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
2702
2973
  const cmd = settings.statusLine?.command;
2703
2974
  if (!cmd || !isLegacyStatusline(cmd)) return void 0;
2704
2975
  settings.statusLine = {
2705
2976
  type: "command",
2706
2977
  command: STATUSLINE_CMD2
2707
2978
  };
2708
- fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2979
+ fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2709
2980
  cleanupLegacyScript();
2710
2981
  return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
2711
2982
  } catch {
@@ -2713,10 +2984,10 @@ function migrateStatusline(wsPath) {
2713
2984
  }
2714
2985
  }
2715
2986
  function cleanupLegacyScript() {
2716
- const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2717
- if (fs7.existsSync(legacyScript)) {
2987
+ const legacyScript = path11.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2988
+ if (fs8.existsSync(legacyScript)) {
2718
2989
  try {
2719
- fs7.unlinkSync(legacyScript);
2990
+ fs8.unlinkSync(legacyScript);
2720
2991
  } catch {
2721
2992
  }
2722
2993
  }
@@ -2949,6 +3220,87 @@ function registerContinueOn(server, reader, workspacePath) {
2949
3220
  );
2950
3221
  }
2951
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
+
2952
3304
  // src/prompts/resume.ts
2953
3305
  function registerResumePrompt(server) {
2954
3306
  server.prompt(
@@ -3114,7 +3466,7 @@ async function handlePrintCurrent() {
3114
3466
  }
3115
3467
 
3116
3468
  // src/cli/saveCheckpoint.ts
3117
- import path11 from "path";
3469
+ import path12 from "path";
3118
3470
  async function handleSaveCheckpoint() {
3119
3471
  const wsPath = resolveWsPath();
3120
3472
  const reader = new KeepGoingReader(wsPath);
@@ -3142,9 +3494,9 @@ async function handleSaveCheckpoint() {
3142
3494
  sessionStartTime: lastSession?.timestamp ?? now,
3143
3495
  lastActivityTime: now
3144
3496
  });
3145
- 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(", ")}`;
3146
3498
  const nextStep = buildSmartNextStep(events);
3147
- const projectName = path11.basename(resolveStorageRoot(wsPath));
3499
+ const projectName = path12.basename(resolveStorageRoot(wsPath));
3148
3500
  const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
3149
3501
  const checkpoint = createCheckpoint({
3150
3502
  summary,
@@ -3189,8 +3541,9 @@ async function handleSaveCheckpoint() {
3189
3541
  }
3190
3542
 
3191
3543
  // src/cli/transcriptUtils.ts
3192
- import fs8 from "fs";
3193
- var TAIL_READ_BYTES = 8192;
3544
+ import fs9 from "fs";
3545
+ var TAIL_READ_BYTES = 32768;
3546
+ var LATEST_LABEL_READ_BYTES = 65536;
3194
3547
  var TOOL_VERB_MAP = {
3195
3548
  Edit: "editing",
3196
3549
  MultiEdit: "editing",
@@ -3202,7 +3555,12 @@ var TOOL_VERB_MAP = {
3202
3555
  Agent: "delegating",
3203
3556
  WebFetch: "browsing",
3204
3557
  WebSearch: "browsing",
3205
- TodoWrite: "planning"
3558
+ TodoWrite: "planning",
3559
+ AskUserQuestion: "discussing",
3560
+ EnterPlanMode: "planning",
3561
+ ExitPlanMode: "planning",
3562
+ TaskCreate: "planning",
3563
+ TaskUpdate: "planning"
3206
3564
  };
3207
3565
  function truncateAtWord(text, max) {
3208
3566
  if (text.length <= max) return text;
@@ -3240,9 +3598,9 @@ function isAssistantEntry(entry) {
3240
3598
  return entry.message?.role === "assistant";
3241
3599
  }
3242
3600
  function extractSessionLabel(transcriptPath) {
3243
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3601
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3244
3602
  try {
3245
- const raw = fs8.readFileSync(transcriptPath, "utf-8");
3603
+ const raw = fs9.readFileSync(transcriptPath, "utf-8");
3246
3604
  for (const line of raw.split("\n")) {
3247
3605
  const trimmed = line.trim();
3248
3606
  if (!trimmed) continue;
@@ -3255,7 +3613,55 @@ function extractSessionLabel(transcriptPath) {
3255
3613
  if (!isUserEntry(entry)) continue;
3256
3614
  let text = extractTextFromContent(entry.message?.content);
3257
3615
  if (!text) continue;
3258
- 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;
3619
+ text = text.replace(/@[\w./\-]+/g, "").trim();
3620
+ text = text.replace(FILLER_PREFIX_RE, "").trim();
3621
+ text = text.replace(MARKDOWN_HEADING_RE, "").trim();
3622
+ text = text.replace(/\s+/g, " ").trim();
3623
+ if (text.length < 20) continue;
3624
+ if (text.length > 80) {
3625
+ text = text.slice(0, 80);
3626
+ }
3627
+ return text;
3628
+ }
3629
+ } catch {
3630
+ }
3631
+ return null;
3632
+ }
3633
+ function extractLatestUserLabel(transcriptPath) {
3634
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3635
+ try {
3636
+ const stat = fs9.statSync(transcriptPath);
3637
+ const fileSize = stat.size;
3638
+ if (fileSize === 0) return null;
3639
+ const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
3640
+ const offset = fileSize - readSize;
3641
+ const buf = Buffer.alloc(readSize);
3642
+ const fd = fs9.openSync(transcriptPath, "r");
3643
+ try {
3644
+ fs9.readSync(fd, buf, 0, readSize, offset);
3645
+ } finally {
3646
+ fs9.closeSync(fd);
3647
+ }
3648
+ const tail = buf.toString("utf-8");
3649
+ const lines = tail.split("\n").reverse();
3650
+ for (const line of lines) {
3651
+ const trimmed = line.trim();
3652
+ if (!trimmed) continue;
3653
+ let entry;
3654
+ try {
3655
+ entry = JSON.parse(trimmed);
3656
+ } catch {
3657
+ continue;
3658
+ }
3659
+ if (!isUserEntry(entry)) continue;
3660
+ let text = extractTextFromContent(entry.message?.content);
3661
+ if (!text) continue;
3662
+ if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
3663
+ text = stripAgentTags(text);
3664
+ if (!text) continue;
3259
3665
  text = text.replace(/@[\w./\-]+/g, "").trim();
3260
3666
  text = text.replace(FILLER_PREFIX_RE, "").trim();
3261
3667
  text = text.replace(MARKDOWN_HEADING_RE, "").trim();
@@ -3271,19 +3677,19 @@ function extractSessionLabel(transcriptPath) {
3271
3677
  return null;
3272
3678
  }
3273
3679
  function extractCurrentAction(transcriptPath) {
3274
- if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3680
+ if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
3275
3681
  try {
3276
- const stat = fs8.statSync(transcriptPath);
3682
+ const stat = fs9.statSync(transcriptPath);
3277
3683
  const fileSize = stat.size;
3278
3684
  if (fileSize === 0) return null;
3279
3685
  const readSize = Math.min(fileSize, TAIL_READ_BYTES);
3280
3686
  const offset = fileSize - readSize;
3281
3687
  const buf = Buffer.alloc(readSize);
3282
- const fd = fs8.openSync(transcriptPath, "r");
3688
+ const fd = fs9.openSync(transcriptPath, "r");
3283
3689
  try {
3284
- fs8.readSync(fd, buf, 0, readSize, offset);
3690
+ fs9.readSync(fd, buf, 0, readSize, offset);
3285
3691
  } finally {
3286
- fs8.closeSync(fd);
3692
+ fs9.closeSync(fd);
3287
3693
  }
3288
3694
  const tail = buf.toString("utf-8");
3289
3695
  const lines = tail.split("\n").reverse();
@@ -3318,6 +3724,8 @@ async function handleUpdateTask() {
3318
3724
  if (payloadStr) {
3319
3725
  try {
3320
3726
  const payload = JSON.parse(payloadStr);
3727
+ if (payload.taskSummary) payload.taskSummary = stripAgentTags(payload.taskSummary);
3728
+ if (payload.sessionLabel) payload.sessionLabel = stripAgentTags(payload.sessionLabel);
3321
3729
  const writer = new KeepGoingWriter(wsPath);
3322
3730
  const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
3323
3731
  const task = {
@@ -3385,8 +3793,8 @@ async function handleUpdateTaskFromHook() {
3385
3793
  }
3386
3794
 
3387
3795
  // src/cli/statusline.ts
3388
- import fs9 from "fs";
3389
- import path12 from "path";
3796
+ import fs10 from "fs";
3797
+ import path13 from "path";
3390
3798
  var STDIN_TIMEOUT_MS2 = 3e3;
3391
3799
  async function handleStatusline() {
3392
3800
  const chunks = [];
@@ -3410,12 +3818,15 @@ async function handleStatusline() {
3410
3818
  if (input.agent?.name) {
3411
3819
  label = input.agent.name;
3412
3820
  }
3821
+ if (!label && transcriptPath) {
3822
+ label = extractLatestUserLabel(transcriptPath);
3823
+ }
3413
3824
  if (!label) {
3414
3825
  try {
3415
3826
  const gitRoot = findGitRoot(dir);
3416
- const tasksFile = path12.join(gitRoot, ".keepgoing", "current-tasks.json");
3417
- if (fs9.existsSync(tasksFile)) {
3418
- 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"));
3419
3830
  const tasks = pruneStaleTasks(data.tasks ?? []);
3420
3831
  const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
3421
3832
  if (match?.sessionLabel) {
@@ -3428,7 +3839,28 @@ async function handleStatusline() {
3428
3839
  if (!label && transcriptPath) {
3429
3840
  label = extractSessionLabel(transcriptPath);
3430
3841
  }
3431
- if (!label) process.exit(0);
3842
+ if (!label) {
3843
+ try {
3844
+ const gitRoot = findGitRoot(dir);
3845
+ const reader = new KeepGoingReader(gitRoot);
3846
+ if (reader.exists()) {
3847
+ const recent = reader.getScopedRecentSessions(10);
3848
+ const last = recent.find((s) => s.source !== "auto") ?? recent[0];
3849
+ if (last) {
3850
+ const ago = formatRelativeTime(last.timestamp);
3851
+ const summary = last.summary ? truncateAtWord(last.summary, 40) : null;
3852
+ const next = last.nextStep ? truncateAtWord(last.nextStep, 30) : null;
3853
+ const parts = [`[KG] ${ago}`];
3854
+ if (summary) parts.push(summary);
3855
+ if (next) parts.push(`\u2192 ${next}`);
3856
+ process.stdout.write(`${parts.join(" \xB7 ")}
3857
+ `);
3858
+ }
3859
+ }
3860
+ } catch {
3861
+ }
3862
+ process.exit(0);
3863
+ }
3432
3864
  const action = transcriptPath ? extractCurrentAction(transcriptPath) : null;
3433
3865
  const budget = action ? 40 : 55;
3434
3866
  const displayLabel = truncateAtWord(label, budget);
@@ -3519,6 +3951,8 @@ if (flag) {
3519
3951
  registerGetCurrentTask(server, reader);
3520
3952
  registerSaveCheckpoint(server, reader, workspacePath);
3521
3953
  registerContinueOn(server, reader, workspacePath);
3954
+ registerGetContextSnapshot(server, reader, workspacePath);
3955
+ registerGetWhatsHot(server);
3522
3956
  registerSetupProject(server, workspacePath);
3523
3957
  registerActivateLicense(server);
3524
3958
  registerDeactivateLicense(server);