bertrand 0.22.2 → 0.23.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/bertrand.js +514 -287
- package/dist/dashboard/assets/{index-CCHVGoKS.js → index-DW_dgQQt.js} +97 -92
- package/dist/dashboard/index.html +1 -1
- package/dist/migrations/0007_awesome_justin_hammer.sql +2 -0
- package/dist/migrations/meta/0007_snapshot.json +693 -0
- package/dist/migrations/meta/_journal.json +7 -0
- package/dist/run-screen.js +33 -28
- package/package.json +1 -1
package/dist/bertrand.js
CHANGED
|
@@ -600,6 +600,7 @@ var init_router = __esm(() => {
|
|
|
600
600
|
HOOK_COMMANDS = new Set([
|
|
601
601
|
"update",
|
|
602
602
|
"assistant-message",
|
|
603
|
+
"contract",
|
|
603
604
|
"notify",
|
|
604
605
|
"badge"
|
|
605
606
|
]);
|
|
@@ -654,6 +655,8 @@ var init_schema = __esm(() => {
|
|
|
654
655
|
pid: integer("pid"),
|
|
655
656
|
startedAt: text("started_at").notNull().default(sql`(datetime('now'))`),
|
|
656
657
|
endedAt: text("ended_at"),
|
|
658
|
+
worktreePath: text("worktree_path"),
|
|
659
|
+
worktreeBranch: text("worktree_branch"),
|
|
657
660
|
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
658
661
|
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
659
662
|
}, (t) => [
|
|
@@ -782,28 +785,122 @@ function createId(size = 12) {
|
|
|
782
785
|
}
|
|
783
786
|
var init_id = () => {};
|
|
784
787
|
|
|
788
|
+
// src/db/queries/categories.ts
|
|
789
|
+
import { eq, like, or, isNull } from "drizzle-orm";
|
|
790
|
+
function createCategory(opts) {
|
|
791
|
+
const db = getDb();
|
|
792
|
+
const id = createId();
|
|
793
|
+
let path = opts.slug;
|
|
794
|
+
let depth = 0;
|
|
795
|
+
if (opts.parentId) {
|
|
796
|
+
const parent = db.select().from(categories).where(eq(categories.id, opts.parentId)).get();
|
|
797
|
+
if (!parent)
|
|
798
|
+
throw new Error(`Parent category ${opts.parentId} not found`);
|
|
799
|
+
path = `${parent.path}/${opts.slug}`;
|
|
800
|
+
depth = parent.depth + 1;
|
|
801
|
+
}
|
|
802
|
+
return db.insert(categories).values({ id, slug: opts.slug, name: opts.name, parentId: opts.parentId, path, depth }).returning().get();
|
|
803
|
+
}
|
|
804
|
+
function getCategory(id) {
|
|
805
|
+
return getDb().select().from(categories).where(eq(categories.id, id)).get();
|
|
806
|
+
}
|
|
807
|
+
function getCategoryByPath(path) {
|
|
808
|
+
return getDb().select().from(categories).where(eq(categories.path, path)).get();
|
|
809
|
+
}
|
|
810
|
+
function getOrCreateCategoryPath(path) {
|
|
811
|
+
const existing = getCategoryByPath(path);
|
|
812
|
+
if (existing)
|
|
813
|
+
return existing.id;
|
|
814
|
+
const segments = path.split("/");
|
|
815
|
+
let parentId;
|
|
816
|
+
for (let i = 0;i < segments.length; i++) {
|
|
817
|
+
const partialPath = segments.slice(0, i + 1).join("/");
|
|
818
|
+
const category = getCategoryByPath(partialPath);
|
|
819
|
+
if (category) {
|
|
820
|
+
parentId = category.id;
|
|
821
|
+
} else {
|
|
822
|
+
const slug = segments[i];
|
|
823
|
+
const created = createCategory({ slug, name: slug, parentId });
|
|
824
|
+
parentId = created.id;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return parentId;
|
|
828
|
+
}
|
|
829
|
+
var init_categories = __esm(() => {
|
|
830
|
+
init_client();
|
|
831
|
+
init_schema();
|
|
832
|
+
init_id();
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// src/lib/parse-session-name.ts
|
|
836
|
+
function parseSessionName(input) {
|
|
837
|
+
const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
|
|
838
|
+
if (!trimmed) {
|
|
839
|
+
throw new Error("Session name cannot be empty");
|
|
840
|
+
}
|
|
841
|
+
const segments = trimmed.split("/").filter(Boolean);
|
|
842
|
+
if (segments.length < 2) {
|
|
843
|
+
throw new Error(`Session name must include at least one category: "category/session" (got "${trimmed}")`);
|
|
844
|
+
}
|
|
845
|
+
for (const segment of segments) {
|
|
846
|
+
if (!SEGMENT_PATTERN.test(segment)) {
|
|
847
|
+
throw new Error(`Invalid segment "${segment}": must start with alphanumeric and contain only letters, digits, dots, underscores, or dashes`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const categoryPath = segments[0];
|
|
851
|
+
const slug = segments.slice(1).join("/");
|
|
852
|
+
return { categoryPath, slug };
|
|
853
|
+
}
|
|
854
|
+
var SEGMENT_PATTERN;
|
|
855
|
+
var init_parse_session_name = __esm(() => {
|
|
856
|
+
SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
857
|
+
});
|
|
858
|
+
|
|
785
859
|
// src/db/queries/sessions.ts
|
|
786
|
-
import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
|
|
860
|
+
import { eq as eq2, and, inArray, sql as sql2 } from "drizzle-orm";
|
|
861
|
+
function resolveSessionByName(name) {
|
|
862
|
+
const flat = parseSessionName(name);
|
|
863
|
+
const flatCategory = getCategoryByPath(flat.categoryPath);
|
|
864
|
+
if (flatCategory) {
|
|
865
|
+
const session = getSessionByCategorySlug(flatCategory.id, flat.slug);
|
|
866
|
+
if (session) {
|
|
867
|
+
return { session, categoryPath: flat.categoryPath, slug: flat.slug };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const segments = name.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
871
|
+
if (segments.length >= 3) {
|
|
872
|
+
const legacyCategoryPath = segments.slice(0, -1).join("/");
|
|
873
|
+
const legacySlug = segments[segments.length - 1];
|
|
874
|
+
const legacyCategory = getCategoryByPath(legacyCategoryPath);
|
|
875
|
+
if (legacyCategory) {
|
|
876
|
+
const session = getSessionByCategorySlug(legacyCategory.id, legacySlug);
|
|
877
|
+
if (session) {
|
|
878
|
+
return { session, categoryPath: legacyCategoryPath, slug: legacySlug };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
787
884
|
function createSession(opts) {
|
|
788
885
|
const db = getDb();
|
|
789
886
|
const id = createId();
|
|
790
887
|
return db.insert(sessions).values({ id, ...opts }).returning().get();
|
|
791
888
|
}
|
|
792
889
|
function getSession(id) {
|
|
793
|
-
return getDb().select().from(sessions).where(
|
|
890
|
+
return getDb().select().from(sessions).where(eq2(sessions.id, id)).get();
|
|
794
891
|
}
|
|
795
892
|
function getSessionByCategorySlug(categoryId, slug) {
|
|
796
|
-
return getDb().select().from(sessions).where(and(
|
|
893
|
+
return getDb().select().from(sessions).where(and(eq2(sessions.categoryId, categoryId), eq2(sessions.slug, slug))).get();
|
|
797
894
|
}
|
|
798
895
|
function getSessionsByCategory(categoryId) {
|
|
799
|
-
return getDb().select().from(sessions).where(
|
|
896
|
+
return getDb().select().from(sessions).where(eq2(sessions.categoryId, categoryId)).all();
|
|
800
897
|
}
|
|
801
898
|
function getActiveSessions() {
|
|
802
|
-
return getDb().select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories,
|
|
899
|
+
return getDb().select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories, eq2(sessions.categoryId, categories.id)).where(inArray(sessions.status, ["active", "waiting"])).all();
|
|
803
900
|
}
|
|
804
901
|
function getAllSessions(opts) {
|
|
805
902
|
const db = getDb();
|
|
806
|
-
const query = db.select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories,
|
|
903
|
+
const query = db.select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories, eq2(sessions.categoryId, categories.id));
|
|
807
904
|
if (opts?.excludeArchived) {
|
|
808
905
|
return query.where(inArray(sessions.status, [
|
|
809
906
|
"active",
|
|
@@ -814,33 +911,35 @@ function getAllSessions(opts) {
|
|
|
814
911
|
return query.all();
|
|
815
912
|
}
|
|
816
913
|
function updateSessionStatus(id, status) {
|
|
817
|
-
return getDb().update(sessions).set({ status, updatedAt: sql2`(datetime('now'))` }).where(
|
|
914
|
+
return getDb().update(sessions).set({ status, updatedAt: sql2`(datetime('now'))` }).where(eq2(sessions.id, id)).returning().get();
|
|
818
915
|
}
|
|
819
916
|
function updateSession(id, data) {
|
|
820
|
-
return getDb().update(sessions).set({ ...data, updatedAt: sql2`(datetime('now'))` }).where(
|
|
917
|
+
return getDb().update(sessions).set({ ...data, updatedAt: sql2`(datetime('now'))` }).where(eq2(sessions.id, id)).returning().get();
|
|
821
918
|
}
|
|
822
919
|
function deleteSession(id) {
|
|
823
|
-
return getDb().delete(sessions).where(
|
|
920
|
+
return getDb().delete(sessions).where(eq2(sessions.id, id)).run();
|
|
824
921
|
}
|
|
825
922
|
var init_sessions = __esm(() => {
|
|
826
923
|
init_client();
|
|
827
924
|
init_schema();
|
|
828
925
|
init_id();
|
|
926
|
+
init_categories();
|
|
927
|
+
init_parse_session_name();
|
|
829
928
|
});
|
|
830
929
|
|
|
831
930
|
// src/db/queries/conversations.ts
|
|
832
|
-
import { eq as
|
|
931
|
+
import { eq as eq3, and as and2, desc, sql as sql3 } from "drizzle-orm";
|
|
833
932
|
function createConversation(opts) {
|
|
834
933
|
return getDb().insert(conversations).values(opts).returning().get();
|
|
835
934
|
}
|
|
836
935
|
function getConversation(id) {
|
|
837
|
-
return getDb().select().from(conversations).where(
|
|
936
|
+
return getDb().select().from(conversations).where(eq3(conversations.id, id)).get();
|
|
838
937
|
}
|
|
839
938
|
function getConversationsBySession(sessionId) {
|
|
840
|
-
return getDb().select().from(conversations).where(and2(
|
|
939
|
+
return getDb().select().from(conversations).where(and2(eq3(conversations.sessionId, sessionId), eq3(conversations.discarded, false))).orderBy(desc(conversations.startedAt)).all();
|
|
841
940
|
}
|
|
842
941
|
function endConversation(id) {
|
|
843
|
-
return getDb().update(conversations).set({ endedAt: sql3`(datetime('now'))` }).where(
|
|
942
|
+
return getDb().update(conversations).set({ endedAt: sql3`(datetime('now'))` }).where(eq3(conversations.id, id)).returning().get();
|
|
844
943
|
}
|
|
845
944
|
var init_conversations = __esm(() => {
|
|
846
945
|
init_client();
|
|
@@ -896,7 +995,7 @@ function normalizeAnsweredMeta(meta) {
|
|
|
896
995
|
}
|
|
897
996
|
|
|
898
997
|
// src/db/queries/events.ts
|
|
899
|
-
import { eq as
|
|
998
|
+
import { eq as eq4, and as and3, desc as desc2 } from "drizzle-orm";
|
|
900
999
|
function insertEvent(opts) {
|
|
901
1000
|
return getDb().insert(events).values({
|
|
902
1001
|
sessionId: opts.sessionId,
|
|
@@ -907,18 +1006,18 @@ function insertEvent(opts) {
|
|
|
907
1006
|
}).returning().get();
|
|
908
1007
|
}
|
|
909
1008
|
function getEventsBySession(sessionId) {
|
|
910
|
-
return getDb().select().from(events).where(
|
|
1009
|
+
return getDb().select().from(events).where(eq4(events.sessionId, sessionId)).orderBy(events.createdAt, events.id).all();
|
|
911
1010
|
}
|
|
912
1011
|
function getEventsByType(sessionId, eventType) {
|
|
913
|
-
return getDb().select().from(events).where(and3(
|
|
1012
|
+
return getDb().select().from(events).where(and3(eq4(events.sessionId, sessionId), eq4(events.event, eventType))).orderBy(events.createdAt).all();
|
|
914
1013
|
}
|
|
915
1014
|
function getLatestEventOfType(sessionId, eventType, conversationId) {
|
|
916
1015
|
const conditions = [
|
|
917
|
-
|
|
918
|
-
|
|
1016
|
+
eq4(events.sessionId, sessionId),
|
|
1017
|
+
eq4(events.event, eventType)
|
|
919
1018
|
];
|
|
920
1019
|
if (conversationId) {
|
|
921
|
-
conditions.push(
|
|
1020
|
+
conditions.push(eq4(events.conversationId, conversationId));
|
|
922
1021
|
}
|
|
923
1022
|
return getDb().select().from(events).where(and3(...conditions)).orderBy(desc2(events.createdAt)).limit(1).get();
|
|
924
1023
|
}
|
|
@@ -927,7 +1026,7 @@ function getLatestRecaps() {
|
|
|
927
1026
|
sessionId: events.sessionId,
|
|
928
1027
|
meta: events.meta,
|
|
929
1028
|
createdAt: events.createdAt
|
|
930
|
-
}).from(events).where(
|
|
1029
|
+
}).from(events).where(eq4(events.event, "session.recap")).orderBy(desc2(events.createdAt)).all();
|
|
931
1030
|
const result = {};
|
|
932
1031
|
for (const row of rows) {
|
|
933
1032
|
if (result[row.sessionId])
|
|
@@ -1079,6 +1178,24 @@ function emitAssistantRecap(args) {
|
|
|
1079
1178
|
meta: { recap: args.recap, claude_id: args.conversationId }
|
|
1080
1179
|
});
|
|
1081
1180
|
}
|
|
1181
|
+
function emitWorktreeEntered(args) {
|
|
1182
|
+
return insertEvent({
|
|
1183
|
+
sessionId: args.sessionId,
|
|
1184
|
+
conversationId: args.conversationId,
|
|
1185
|
+
event: "worktree.entered",
|
|
1186
|
+
summary: args.branch ? `entered worktree ${args.branch}` : "entered worktree",
|
|
1187
|
+
meta: { path: args.path, branch: args.branch, claude_id: args.conversationId }
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function emitWorktreeExited(args) {
|
|
1191
|
+
return insertEvent({
|
|
1192
|
+
sessionId: args.sessionId,
|
|
1193
|
+
conversationId: args.conversationId,
|
|
1194
|
+
event: "worktree.exited",
|
|
1195
|
+
summary: args.branch ? `exited worktree ${args.branch}` : "exited worktree",
|
|
1196
|
+
meta: { path: args.path, branch: args.branch, claude_id: args.conversationId }
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1082
1199
|
var init_emit = __esm(() => {
|
|
1083
1200
|
init_events();
|
|
1084
1201
|
});
|
|
@@ -1086,7 +1203,8 @@ var init_emit = __esm(() => {
|
|
|
1086
1203
|
// src/cli/commands/update.ts
|
|
1087
1204
|
var exports_update = {};
|
|
1088
1205
|
__export(exports_update, {
|
|
1089
|
-
shouldIgnoreStatusFlip: () => shouldIgnoreStatusFlip
|
|
1206
|
+
shouldIgnoreStatusFlip: () => shouldIgnoreStatusFlip,
|
|
1207
|
+
dispatchHookEvent: () => dispatchHookEvent
|
|
1090
1208
|
});
|
|
1091
1209
|
function shouldIgnoreStatusFlip(newStatus, sessionPid) {
|
|
1092
1210
|
if (!newStatus)
|
|
@@ -1145,6 +1263,22 @@ function dispatchHookEvent(event, ctx) {
|
|
|
1145
1263
|
outcome: meta.outcome === "approved" ? "approved" : "auto"
|
|
1146
1264
|
});
|
|
1147
1265
|
return true;
|
|
1266
|
+
case "worktree.entered": {
|
|
1267
|
+
const path = String(meta.path ?? "");
|
|
1268
|
+
const branch = meta.branch ? String(meta.branch) : undefined;
|
|
1269
|
+
emitWorktreeEntered({ sessionId, conversationId, path, branch });
|
|
1270
|
+
updateSession(sessionId, {
|
|
1271
|
+
worktreePath: path || null,
|
|
1272
|
+
worktreeBranch: branch ?? null
|
|
1273
|
+
});
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
case "worktree.exited": {
|
|
1277
|
+
const path = meta.path ? String(meta.path) : undefined;
|
|
1278
|
+
emitWorktreeExited({ sessionId, conversationId, path });
|
|
1279
|
+
updateSession(sessionId, { worktreePath: null, worktreeBranch: null });
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1148
1282
|
default:
|
|
1149
1283
|
return false;
|
|
1150
1284
|
}
|
|
@@ -1374,6 +1508,156 @@ var init_assistant_message = __esm(() => {
|
|
|
1374
1508
|
});
|
|
1375
1509
|
});
|
|
1376
1510
|
|
|
1511
|
+
// src/contract/template.md
|
|
1512
|
+
var template_default = `You are running inside bertrand, session: {sessionName}. Follow these rules strictly:
|
|
1513
|
+
|
|
1514
|
+
After every response, you MUST call AskUserQuestion. This is a continuous loop \u2014 every turn ends with AskUserQuestion. Always include an option labeled exactly \`Done for now\` (this exact wording is required \u2014 the session-exit hook greps for it). The description for "Done for now" must be a 1-2 sentence summary of what was accomplished so far. Describe outcomes (what was built, fixed, decided), not process.
|
|
1515
|
+
|
|
1516
|
+
If the user's most recent answer to AskUserQuestion was "Done for now" (or contains it), this turn is the FINAL turn. Respond briefly to acknowledge and do NOT call AskUserQuestion again \u2014 the loop is over.
|
|
1517
|
+
|
|
1518
|
+
Before each AskUserQuestion call, emit a \`<recap>...</recap>\` block in your text output. Use markdown \u2014 a short bullet list is usually the most scannable shape; a single short paragraph is fine when the turn was one cohesive thing. Keep it concise. The recap covers what happened since the previous AskUserQuestion (or session start) \u2014 what you found, decided, or did. Write the gist for someone reading the session timeline, not a process log. The dashboard renders these between AskUserQuestion events; do not use this tag for any other purpose.
|
|
1519
|
+
`;
|
|
1520
|
+
var init_template = () => {};
|
|
1521
|
+
|
|
1522
|
+
// src/contract/template.ts
|
|
1523
|
+
function buildContract(sessionName, ...contextLayers) {
|
|
1524
|
+
const base = template_default.replace("{sessionName}", sessionName);
|
|
1525
|
+
const layers = contextLayers.map((c) => c.trim()).filter((c) => c.length > 0);
|
|
1526
|
+
if (layers.length === 0)
|
|
1527
|
+
return base;
|
|
1528
|
+
return base + `
|
|
1529
|
+
|
|
1530
|
+
` + layers.join(`
|
|
1531
|
+
|
|
1532
|
+
`);
|
|
1533
|
+
}
|
|
1534
|
+
var init_template2 = __esm(() => {
|
|
1535
|
+
init_template();
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
// src/lib/format.ts
|
|
1539
|
+
function formatDuration(ms) {
|
|
1540
|
+
if (ms < MINUTE)
|
|
1541
|
+
return `${Math.round(ms / SECOND)}s`;
|
|
1542
|
+
const days = Math.floor(ms / DAY);
|
|
1543
|
+
const hours = Math.floor(ms % DAY / HOUR);
|
|
1544
|
+
const minutes = Math.floor(ms % HOUR / MINUTE);
|
|
1545
|
+
if (days > 0)
|
|
1546
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
1547
|
+
if (hours > 0)
|
|
1548
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
1549
|
+
return `${minutes}m`;
|
|
1550
|
+
}
|
|
1551
|
+
function formatAgo(isoOrDate) {
|
|
1552
|
+
const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
|
|
1553
|
+
const ms = Date.now() - date.getTime();
|
|
1554
|
+
if (ms < MINUTE)
|
|
1555
|
+
return "just now";
|
|
1556
|
+
if (ms < HOUR)
|
|
1557
|
+
return `${Math.floor(ms / MINUTE)}m ago`;
|
|
1558
|
+
if (ms < DAY)
|
|
1559
|
+
return `${Math.floor(ms / HOUR)}h ago`;
|
|
1560
|
+
if (ms < 2 * DAY)
|
|
1561
|
+
return "yesterday";
|
|
1562
|
+
if (ms < 7 * DAY)
|
|
1563
|
+
return `${Math.floor(ms / DAY)}d ago`;
|
|
1564
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1565
|
+
}
|
|
1566
|
+
function truncate(text2, maxLen) {
|
|
1567
|
+
if (text2.length <= maxLen)
|
|
1568
|
+
return text2;
|
|
1569
|
+
return text2.slice(0, maxLen - 1) + "\u2026";
|
|
1570
|
+
}
|
|
1571
|
+
function formatTime(iso, includeDate = false) {
|
|
1572
|
+
const date = new Date(iso);
|
|
1573
|
+
const time = date.toLocaleTimeString("en-US", {
|
|
1574
|
+
hour: "numeric",
|
|
1575
|
+
minute: "2-digit"
|
|
1576
|
+
});
|
|
1577
|
+
if (!includeDate)
|
|
1578
|
+
return time;
|
|
1579
|
+
const day = date.toLocaleDateString("en-US", {
|
|
1580
|
+
month: "short",
|
|
1581
|
+
day: "numeric"
|
|
1582
|
+
});
|
|
1583
|
+
return `${day} ${time}`;
|
|
1584
|
+
}
|
|
1585
|
+
var SECOND = 1000, MINUTE, HOUR, DAY;
|
|
1586
|
+
var init_format = __esm(() => {
|
|
1587
|
+
MINUTE = 60 * SECOND;
|
|
1588
|
+
HOUR = 60 * MINUTE;
|
|
1589
|
+
DAY = 24 * HOUR;
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
// src/contract/context.ts
|
|
1593
|
+
function buildSiblingContext(categoryId, categoryPath, currentSessionId) {
|
|
1594
|
+
const siblings = getSessionsByCategory(categoryId).filter((s) => s.id !== currentSessionId);
|
|
1595
|
+
if (siblings.length === 0)
|
|
1596
|
+
return "";
|
|
1597
|
+
const lines = siblings.map((s) => {
|
|
1598
|
+
const ago = s.updatedAt ? formatAgo(s.updatedAt) : "unknown";
|
|
1599
|
+
const summary = s.summary ? ` \u2014 "${s.summary}"` : "";
|
|
1600
|
+
const worktree = s.worktreeBranch ? ` [worktree: ${s.worktreeBranch}]` : "";
|
|
1601
|
+
return `- ${categoryPath}/${s.slug}: ${s.status}${worktree}${summary} (${ago})`;
|
|
1602
|
+
});
|
|
1603
|
+
const guidance = [
|
|
1604
|
+
"",
|
|
1605
|
+
"To inspect any sibling session's full record, run:",
|
|
1606
|
+
" bertrand log <category>/<slug> --json",
|
|
1607
|
+
"Returns session metadata, stats, conversations, and the full event timeline.",
|
|
1608
|
+
"Reach for this when the user references work done in another session, or you need to verify what was decided or tried elsewhere."
|
|
1609
|
+
].join(`
|
|
1610
|
+
`);
|
|
1611
|
+
return `## Sibling Sessions
|
|
1612
|
+
${lines.join(`
|
|
1613
|
+
`)}
|
|
1614
|
+
${guidance}`;
|
|
1615
|
+
}
|
|
1616
|
+
var init_context = __esm(() => {
|
|
1617
|
+
init_sessions();
|
|
1618
|
+
init_format();
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
// src/cli/commands/contract.ts
|
|
1622
|
+
var exports_contract = {};
|
|
1623
|
+
var init_contract = __esm(() => {
|
|
1624
|
+
init_router();
|
|
1625
|
+
init_sessions();
|
|
1626
|
+
init_categories();
|
|
1627
|
+
init_template2();
|
|
1628
|
+
init_context();
|
|
1629
|
+
register("contract", async (args) => {
|
|
1630
|
+
let sessionId = "";
|
|
1631
|
+
let short = false;
|
|
1632
|
+
for (let i = 0;i < args.length; i++) {
|
|
1633
|
+
const arg = args[i];
|
|
1634
|
+
const next = args[i + 1];
|
|
1635
|
+
if (arg === "--session-id" && next) {
|
|
1636
|
+
sessionId = next;
|
|
1637
|
+
i++;
|
|
1638
|
+
} else if (arg === "--short") {
|
|
1639
|
+
short = true;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
if (!sessionId) {
|
|
1643
|
+
console.error("Usage: bertrand contract --session-id <id> [--short]");
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
const session = getSession(sessionId);
|
|
1647
|
+
if (!session)
|
|
1648
|
+
return;
|
|
1649
|
+
const category = getCategory(session.categoryId);
|
|
1650
|
+
const categoryPath = category?.path ?? "";
|
|
1651
|
+
const sessionName = categoryPath ? `${categoryPath}/${session.slug}` : session.slug;
|
|
1652
|
+
if (short) {
|
|
1653
|
+
process.stdout.write(`Reminder \u2014 you are in bertrand session ${sessionName}: end this turn with an AskUserQuestion call (multiSelect:true on every question, plus a "Done for now" option) preceded by a <recap> block.`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const siblingContext = buildSiblingContext(session.categoryId, categoryPath, session.id);
|
|
1657
|
+
process.stdout.write(buildContract(sessionName, siblingContext));
|
|
1658
|
+
});
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1377
1661
|
// src/terminal/wave.ts
|
|
1378
1662
|
import { execSync } from "child_process";
|
|
1379
1663
|
|
|
@@ -1491,9 +1775,9 @@ var init_notify = __esm(() => {
|
|
|
1491
1775
|
});
|
|
1492
1776
|
|
|
1493
1777
|
// src/db/queries/stats.ts
|
|
1494
|
-
import { eq as
|
|
1778
|
+
import { eq as eq5, sql as sql4 } from "drizzle-orm";
|
|
1495
1779
|
function getSessionStats(sessionId) {
|
|
1496
|
-
return getDb().select().from(sessionStats).where(
|
|
1780
|
+
return getDb().select().from(sessionStats).where(eq5(sessionStats.sessionId, sessionId)).get();
|
|
1497
1781
|
}
|
|
1498
1782
|
function upsertSessionStats(sessionId, data) {
|
|
1499
1783
|
return getDb().insert(sessionStats).values({
|
|
@@ -1666,7 +1950,7 @@ var init_timing = __esm(() => {
|
|
|
1666
1950
|
});
|
|
1667
1951
|
|
|
1668
1952
|
// src/lib/engagement_stats.ts
|
|
1669
|
-
import { eq as
|
|
1953
|
+
import { eq as eq6, sql as sql5 } from "drizzle-orm";
|
|
1670
1954
|
function aggregateToolUsage(sessionId) {
|
|
1671
1955
|
const counts = {};
|
|
1672
1956
|
const applied = getEventsByType(sessionId, "tool.applied");
|
|
@@ -1693,7 +1977,7 @@ function discardRate(sessionId) {
|
|
|
1693
1977
|
const row = getDb().select({
|
|
1694
1978
|
total: sql5`count(*)`,
|
|
1695
1979
|
discarded: sql5`sum(case when ${conversations.discarded} then 1 else 0 end)`
|
|
1696
|
-
}).from(conversations).where(
|
|
1980
|
+
}).from(conversations).where(eq6(conversations.sessionId, sessionId)).get();
|
|
1697
1981
|
return {
|
|
1698
1982
|
total: row?.total ?? 0,
|
|
1699
1983
|
discarded: row?.discarded ?? 0
|
|
@@ -1945,7 +2229,7 @@ var PORT, listSessions = (_params, url) => {
|
|
|
1945
2229
|
}, getActiveProjectMeta = () => {
|
|
1946
2230
|
const active = resolveActiveProject();
|
|
1947
2231
|
return { slug: active.slug, name: active.name };
|
|
1948
|
-
}, routes, ARCHIVE_ERROR, DASHBOARD_DIR;
|
|
2232
|
+
}, listWorktreeSessions = () => getAllSessions({ excludeArchived: true }).filter(({ session }) => session.worktreePath != null), routes, ARCHIVE_ERROR, DASHBOARD_DIR;
|
|
1949
2233
|
var init_server = __esm(() => {
|
|
1950
2234
|
init_sessions();
|
|
1951
2235
|
init_events();
|
|
@@ -1960,6 +2244,7 @@ var init_server = __esm(() => {
|
|
|
1960
2244
|
routes = [
|
|
1961
2245
|
[/^\/api\/sessions$/, listSessions],
|
|
1962
2246
|
[/^\/api\/sessions\/(?<id>[^/]+)$/, getSessionById],
|
|
2247
|
+
[/^\/api\/worktrees$/, listWorktreeSessions],
|
|
1963
2248
|
[/^\/api\/events\/(?<sessionId>[^/]+)$/, listEvents],
|
|
1964
2249
|
[/^\/api\/stats$/, listAllStats],
|
|
1965
2250
|
[/^\/api\/stats\/(?<sessionId>[^/]+)$/, getStatsBySession],
|
|
@@ -2375,60 +2660,6 @@ var init_bootstrap = __esm(() => {
|
|
|
2375
2660
|
init_config2();
|
|
2376
2661
|
});
|
|
2377
2662
|
|
|
2378
|
-
// src/lib/format.ts
|
|
2379
|
-
function formatDuration(ms) {
|
|
2380
|
-
if (ms < MINUTE)
|
|
2381
|
-
return `${Math.round(ms / SECOND)}s`;
|
|
2382
|
-
const days = Math.floor(ms / DAY);
|
|
2383
|
-
const hours = Math.floor(ms % DAY / HOUR);
|
|
2384
|
-
const minutes = Math.floor(ms % HOUR / MINUTE);
|
|
2385
|
-
if (days > 0)
|
|
2386
|
-
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
2387
|
-
if (hours > 0)
|
|
2388
|
-
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
2389
|
-
return `${minutes}m`;
|
|
2390
|
-
}
|
|
2391
|
-
function formatAgo(isoOrDate) {
|
|
2392
|
-
const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
|
|
2393
|
-
const ms = Date.now() - date.getTime();
|
|
2394
|
-
if (ms < MINUTE)
|
|
2395
|
-
return "just now";
|
|
2396
|
-
if (ms < HOUR)
|
|
2397
|
-
return `${Math.floor(ms / MINUTE)}m ago`;
|
|
2398
|
-
if (ms < DAY)
|
|
2399
|
-
return `${Math.floor(ms / HOUR)}h ago`;
|
|
2400
|
-
if (ms < 2 * DAY)
|
|
2401
|
-
return "yesterday";
|
|
2402
|
-
if (ms < 7 * DAY)
|
|
2403
|
-
return `${Math.floor(ms / DAY)}d ago`;
|
|
2404
|
-
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
2405
|
-
}
|
|
2406
|
-
function truncate(text2, maxLen) {
|
|
2407
|
-
if (text2.length <= maxLen)
|
|
2408
|
-
return text2;
|
|
2409
|
-
return text2.slice(0, maxLen - 1) + "\u2026";
|
|
2410
|
-
}
|
|
2411
|
-
function formatTime(iso, includeDate = false) {
|
|
2412
|
-
const date = new Date(iso);
|
|
2413
|
-
const time = date.toLocaleTimeString("en-US", {
|
|
2414
|
-
hour: "numeric",
|
|
2415
|
-
minute: "2-digit"
|
|
2416
|
-
});
|
|
2417
|
-
if (!includeDate)
|
|
2418
|
-
return time;
|
|
2419
|
-
const day = date.toLocaleDateString("en-US", {
|
|
2420
|
-
month: "short",
|
|
2421
|
-
day: "numeric"
|
|
2422
|
-
});
|
|
2423
|
-
return `${day} ${time}`;
|
|
2424
|
-
}
|
|
2425
|
-
var SECOND = 1000, MINUTE, HOUR, DAY;
|
|
2426
|
-
var init_format = __esm(() => {
|
|
2427
|
-
MINUTE = 60 * SECOND;
|
|
2428
|
-
HOUR = 60 * MINUTE;
|
|
2429
|
-
DAY = 24 * HOUR;
|
|
2430
|
-
});
|
|
2431
|
-
|
|
2432
2663
|
// src/cli/commands/sync.ts
|
|
2433
2664
|
var exports_sync = {};
|
|
2434
2665
|
import { hostname as hostname2 } from "os";
|
|
@@ -2753,53 +2984,6 @@ var init_sync = __esm(() => {
|
|
|
2753
2984
|
});
|
|
2754
2985
|
});
|
|
2755
2986
|
|
|
2756
|
-
// src/db/queries/categories.ts
|
|
2757
|
-
import { eq as eq6, like, or, isNull } from "drizzle-orm";
|
|
2758
|
-
function createCategory(opts) {
|
|
2759
|
-
const db = getDb();
|
|
2760
|
-
const id = createId();
|
|
2761
|
-
let path = opts.slug;
|
|
2762
|
-
let depth = 0;
|
|
2763
|
-
if (opts.parentId) {
|
|
2764
|
-
const parent = db.select().from(categories).where(eq6(categories.id, opts.parentId)).get();
|
|
2765
|
-
if (!parent)
|
|
2766
|
-
throw new Error(`Parent category ${opts.parentId} not found`);
|
|
2767
|
-
path = `${parent.path}/${opts.slug}`;
|
|
2768
|
-
depth = parent.depth + 1;
|
|
2769
|
-
}
|
|
2770
|
-
return db.insert(categories).values({ id, slug: opts.slug, name: opts.name, parentId: opts.parentId, path, depth }).returning().get();
|
|
2771
|
-
}
|
|
2772
|
-
function getCategory(id) {
|
|
2773
|
-
return getDb().select().from(categories).where(eq6(categories.id, id)).get();
|
|
2774
|
-
}
|
|
2775
|
-
function getCategoryByPath(path) {
|
|
2776
|
-
return getDb().select().from(categories).where(eq6(categories.path, path)).get();
|
|
2777
|
-
}
|
|
2778
|
-
function getOrCreateCategoryPath(path) {
|
|
2779
|
-
const existing = getCategoryByPath(path);
|
|
2780
|
-
if (existing)
|
|
2781
|
-
return existing.id;
|
|
2782
|
-
const segments = path.split("/");
|
|
2783
|
-
let parentId;
|
|
2784
|
-
for (let i = 0;i < segments.length; i++) {
|
|
2785
|
-
const partialPath = segments.slice(0, i + 1).join("/");
|
|
2786
|
-
const category = getCategoryByPath(partialPath);
|
|
2787
|
-
if (category) {
|
|
2788
|
-
parentId = category.id;
|
|
2789
|
-
} else {
|
|
2790
|
-
const slug = segments[i];
|
|
2791
|
-
const created = createCategory({ slug, name: slug, parentId });
|
|
2792
|
-
parentId = created.id;
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
return parentId;
|
|
2796
|
-
}
|
|
2797
|
-
var init_categories = __esm(() => {
|
|
2798
|
-
init_client();
|
|
2799
|
-
init_schema();
|
|
2800
|
-
init_id();
|
|
2801
|
-
});
|
|
2802
|
-
|
|
2803
2987
|
// src/db/queries/labels.ts
|
|
2804
2988
|
import { eq as eq7, and as and4 } from "drizzle-orm";
|
|
2805
2989
|
function createLabel(opts) {
|
|
@@ -2823,61 +3007,6 @@ var init_labels = __esm(() => {
|
|
|
2823
3007
|
init_id();
|
|
2824
3008
|
});
|
|
2825
3009
|
|
|
2826
|
-
// src/contract/template.md
|
|
2827
|
-
var template_default = `You are running inside bertrand, session: {sessionName}. Follow these rules strictly:
|
|
2828
|
-
|
|
2829
|
-
After every response, you MUST call AskUserQuestion. This is a continuous loop \u2014 every turn ends with AskUserQuestion. Always include an option labeled exactly \`Done for now\` (this exact wording is required \u2014 the session-exit hook greps for it). The description for "Done for now" must be a 1-2 sentence summary of what was accomplished so far. Describe outcomes (what was built, fixed, decided), not process.
|
|
2830
|
-
|
|
2831
|
-
If the user's most recent answer to AskUserQuestion was "Done for now" (or contains it), this turn is the FINAL turn. Respond briefly to acknowledge and do NOT call AskUserQuestion again \u2014 the loop is over.
|
|
2832
|
-
|
|
2833
|
-
Before each AskUserQuestion call, emit a \`<recap>...</recap>\` block in your text output. Use markdown \u2014 a short bullet list is usually the most scannable shape; a single short paragraph is fine when the turn was one cohesive thing. Keep it concise. The recap covers what happened since the previous AskUserQuestion (or session start) \u2014 what you found, decided, or did. Write the gist for someone reading the session timeline, not a process log. The dashboard renders these between AskUserQuestion events; do not use this tag for any other purpose.
|
|
2834
|
-
`;
|
|
2835
|
-
var init_template = () => {};
|
|
2836
|
-
|
|
2837
|
-
// src/contract/template.ts
|
|
2838
|
-
function buildContract(sessionName, ...contextLayers) {
|
|
2839
|
-
const base = template_default.replace("{sessionName}", sessionName);
|
|
2840
|
-
const layers = contextLayers.map((c) => c.trim()).filter((c) => c.length > 0);
|
|
2841
|
-
if (layers.length === 0)
|
|
2842
|
-
return base;
|
|
2843
|
-
return base + `
|
|
2844
|
-
|
|
2845
|
-
` + layers.join(`
|
|
2846
|
-
|
|
2847
|
-
`);
|
|
2848
|
-
}
|
|
2849
|
-
var init_template2 = __esm(() => {
|
|
2850
|
-
init_template();
|
|
2851
|
-
});
|
|
2852
|
-
|
|
2853
|
-
// src/contract/context.ts
|
|
2854
|
-
function buildSiblingContext(categoryId, categoryPath, currentSessionId) {
|
|
2855
|
-
const siblings = getSessionsByCategory(categoryId).filter((s) => s.id !== currentSessionId);
|
|
2856
|
-
if (siblings.length === 0)
|
|
2857
|
-
return "";
|
|
2858
|
-
const lines = siblings.map((s) => {
|
|
2859
|
-
const ago = s.updatedAt ? formatAgo(s.updatedAt) : "unknown";
|
|
2860
|
-
const summary = s.summary ? ` \u2014 "${s.summary}"` : "";
|
|
2861
|
-
return `- ${categoryPath}/${s.slug}: ${s.status}${summary} (${ago})`;
|
|
2862
|
-
});
|
|
2863
|
-
const guidance = [
|
|
2864
|
-
"",
|
|
2865
|
-
"To inspect any sibling session's full record, run:",
|
|
2866
|
-
" bertrand log <category>/<slug> --json",
|
|
2867
|
-
"Returns session metadata, stats, conversations, and the full event timeline.",
|
|
2868
|
-
"Reach for this when the user references work done in another session, or you need to verify what was decided or tried elsewhere."
|
|
2869
|
-
].join(`
|
|
2870
|
-
`);
|
|
2871
|
-
return `## Sibling Sessions
|
|
2872
|
-
${lines.join(`
|
|
2873
|
-
`)}
|
|
2874
|
-
${guidance}`;
|
|
2875
|
-
}
|
|
2876
|
-
var init_context = __esm(() => {
|
|
2877
|
-
init_sessions();
|
|
2878
|
-
init_format();
|
|
2879
|
-
});
|
|
2880
|
-
|
|
2881
3010
|
// src/engine/process.ts
|
|
2882
3011
|
import { spawn as spawn2 } from "child_process";
|
|
2883
3012
|
function launchClaude(opts) {
|
|
@@ -3021,6 +3150,44 @@ var init_server_lifecycle = __esm(() => {
|
|
|
3021
3150
|
deps = defaultDeps;
|
|
3022
3151
|
});
|
|
3023
3152
|
|
|
3153
|
+
// src/hooks/runtime.ts
|
|
3154
|
+
import { readdirSync as readdirSync2, rmSync, statSync as statSync4 } from "fs";
|
|
3155
|
+
import { join as join10 } from "path";
|
|
3156
|
+
function rmMarker(name) {
|
|
3157
|
+
rmSync(join10(runtimeDir, name), { force: true });
|
|
3158
|
+
}
|
|
3159
|
+
function pruneSessionMarkers(sessionId, conversationId) {
|
|
3160
|
+
rmMarker(`done-${sessionId}`);
|
|
3161
|
+
rmMarker(`auq-nudge-${sessionId}`);
|
|
3162
|
+
rmMarker(`working-${sessionId}`);
|
|
3163
|
+
rmMarker(`worktree-${sessionId}`);
|
|
3164
|
+
if (conversationId)
|
|
3165
|
+
rmMarker(`${CONTRACT_MARKER_PREFIX}${conversationId}`);
|
|
3166
|
+
}
|
|
3167
|
+
function pruneStaleContractMarkers(maxAgeMs = STALE_MS) {
|
|
3168
|
+
let entries;
|
|
3169
|
+
try {
|
|
3170
|
+
entries = readdirSync2(runtimeDir);
|
|
3171
|
+
} catch {
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
3175
|
+
for (const name of entries) {
|
|
3176
|
+
if (!name.startsWith(CONTRACT_MARKER_PREFIX))
|
|
3177
|
+
continue;
|
|
3178
|
+
try {
|
|
3179
|
+
if (statSync4(join10(runtimeDir, name)).mtimeMs < cutoff)
|
|
3180
|
+
rmMarker(name);
|
|
3181
|
+
} catch {}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
var CONTRACT_MARKER_PREFIX = "contract-sent-", STALE_MS, runtimeDir;
|
|
3185
|
+
var init_runtime = __esm(() => {
|
|
3186
|
+
init_paths();
|
|
3187
|
+
STALE_MS = 24 * 60 * 60 * 1000;
|
|
3188
|
+
runtimeDir = paths.runtime;
|
|
3189
|
+
});
|
|
3190
|
+
|
|
3024
3191
|
// src/engine/session.ts
|
|
3025
3192
|
import { randomUUID } from "crypto";
|
|
3026
3193
|
function forceFinalizeLive() {
|
|
@@ -3057,6 +3224,7 @@ function installExitHandlers() {
|
|
|
3057
3224
|
process.on("SIGHUP", onSignal);
|
|
3058
3225
|
}
|
|
3059
3226
|
async function launch(opts) {
|
|
3227
|
+
pruneStaleContractMarkers();
|
|
3060
3228
|
const existingCategory = getCategoryByPath(opts.categoryPath);
|
|
3061
3229
|
if (existingCategory) {
|
|
3062
3230
|
const existing = getSessionByCategorySlug(existingCategory.id, opts.slug);
|
|
@@ -3102,6 +3270,7 @@ async function launch(opts) {
|
|
|
3102
3270
|
return session.id;
|
|
3103
3271
|
}
|
|
3104
3272
|
async function resume(opts) {
|
|
3273
|
+
pruneStaleContractMarkers();
|
|
3105
3274
|
const session = getSession(opts.sessionId);
|
|
3106
3275
|
if (!session)
|
|
3107
3276
|
throw new Error(`Session not found: ${opts.sessionId}`);
|
|
@@ -3153,6 +3322,7 @@ function finalizeSession(sessionId, conversationId, exitCode) {
|
|
|
3153
3322
|
});
|
|
3154
3323
|
if (liveSession?.sessionId === sessionId)
|
|
3155
3324
|
liveSession = null;
|
|
3325
|
+
pruneSessionMarkers(sessionId, safeConversationId);
|
|
3156
3326
|
computeAndPersist(sessionId);
|
|
3157
3327
|
stopServerIfIdle();
|
|
3158
3328
|
triggerBackgroundPush();
|
|
@@ -3171,16 +3341,17 @@ var init_session = __esm(() => {
|
|
|
3171
3341
|
init_server_lifecycle();
|
|
3172
3342
|
init_trigger();
|
|
3173
3343
|
init_transcript();
|
|
3344
|
+
init_runtime();
|
|
3174
3345
|
});
|
|
3175
3346
|
|
|
3176
3347
|
// src/tui/app.tsx
|
|
3177
3348
|
import { spawn as spawn4 } from "child_process";
|
|
3178
3349
|
import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
3179
3350
|
import { tmpdir } from "os";
|
|
3180
|
-
import { join as
|
|
3351
|
+
import { join as join11 } from "path";
|
|
3181
3352
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3182
3353
|
async function runScreen(screen, ...args) {
|
|
3183
|
-
const tmpFile =
|
|
3354
|
+
const tmpFile = join11(tmpdir(), `bertrand-tui-${randomUUID2()}.json`);
|
|
3184
3355
|
if (process.env.BERTRAND_DEBUG_TUI) {
|
|
3185
3356
|
try {
|
|
3186
3357
|
const { appendFileSync } = await import("fs");
|
|
@@ -3190,7 +3361,7 @@ async function runScreen(screen, ...args) {
|
|
|
3190
3361
|
}
|
|
3191
3362
|
const child = spawn4("bun", ["run", SCREEN_ENTRY, screen, tmpFile, ...args], {
|
|
3192
3363
|
stdio: "inherit",
|
|
3193
|
-
env: process.env
|
|
3364
|
+
env: { ...process.env, BERTRAND_PROJECT: resolveActiveProject().slug }
|
|
3194
3365
|
});
|
|
3195
3366
|
const noopSignal = () => {};
|
|
3196
3367
|
process.on("SIGINT", noopSignal);
|
|
@@ -3337,35 +3508,11 @@ var init_app = __esm(() => {
|
|
|
3337
3508
|
init_create();
|
|
3338
3509
|
init_resolve();
|
|
3339
3510
|
SCREEN_ENTRY = (() => {
|
|
3340
|
-
const built =
|
|
3341
|
-
return existsSync8(built) ? built :
|
|
3511
|
+
const built = join11(import.meta.dir, "run-screen.js");
|
|
3512
|
+
return existsSync8(built) ? built : join11(import.meta.dir, "run-screen.tsx");
|
|
3342
3513
|
})();
|
|
3343
3514
|
});
|
|
3344
3515
|
|
|
3345
|
-
// src/lib/parse-session-name.ts
|
|
3346
|
-
function parseSessionName(input) {
|
|
3347
|
-
const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
|
|
3348
|
-
if (!trimmed) {
|
|
3349
|
-
throw new Error("Session name cannot be empty");
|
|
3350
|
-
}
|
|
3351
|
-
const segments = trimmed.split("/").filter(Boolean);
|
|
3352
|
-
if (segments.length < 2) {
|
|
3353
|
-
throw new Error(`Session name must include at least one category: "category/session" (got "${trimmed}")`);
|
|
3354
|
-
}
|
|
3355
|
-
for (const segment of segments) {
|
|
3356
|
-
if (!SEGMENT_PATTERN.test(segment)) {
|
|
3357
|
-
throw new Error(`Invalid segment "${segment}": must start with alphanumeric and contain only letters, digits, dots, underscores, or dashes`);
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
const categoryPath = segments[0];
|
|
3361
|
-
const slug = segments.slice(1).join("/");
|
|
3362
|
-
return { categoryPath, slug };
|
|
3363
|
-
}
|
|
3364
|
-
var SEGMENT_PATTERN;
|
|
3365
|
-
var init_parse_session_name = __esm(() => {
|
|
3366
|
-
SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
3367
|
-
});
|
|
3368
|
-
|
|
3369
3516
|
// src/engine/recovery.ts
|
|
3370
3517
|
function isProcessAlive2(pid) {
|
|
3371
3518
|
try {
|
|
@@ -3430,7 +3577,7 @@ var init_launch = __esm(() => {
|
|
|
3430
3577
|
function quietHelper(bin) {
|
|
3431
3578
|
return `bq() { ${bin} "$@" 2>/dev/null || true; }`;
|
|
3432
3579
|
}
|
|
3433
|
-
function waitingScript(bin,
|
|
3580
|
+
function waitingScript(bin, runtimeDir2) {
|
|
3434
3581
|
return `#!/usr/bin/env bash
|
|
3435
3582
|
# Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
|
|
3436
3583
|
${quietHelper(bin)}
|
|
@@ -3455,7 +3602,7 @@ question="$(printf '%s' "$input" | grep -o '"question":"[^"]*"' | head -1 | cut
|
|
|
3455
3602
|
[ -z "$question" ] && question="Waiting for input"
|
|
3456
3603
|
|
|
3457
3604
|
# Clear working debounce marker so next resume\u2192working transition fires
|
|
3458
|
-
rm -f "${
|
|
3605
|
+
rm -f "${runtimeDir2}/working-$sid"
|
|
3459
3606
|
|
|
3460
3607
|
bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
|
|
3461
3608
|
|
|
@@ -3473,7 +3620,7 @@ bq notify bertrand "$question" &
|
|
|
3473
3620
|
wait
|
|
3474
3621
|
`;
|
|
3475
3622
|
}
|
|
3476
|
-
function answeredScript(bin,
|
|
3623
|
+
function answeredScript(bin, runtimeDir2) {
|
|
3477
3624
|
return `#!/usr/bin/env bash
|
|
3478
3625
|
# Hook: PostToolUse AskUserQuestion \u2192 mark session as active
|
|
3479
3626
|
#
|
|
@@ -3507,9 +3654,18 @@ bq update --session-id "$sid" --event session.answered --meta "$meta"
|
|
|
3507
3654
|
|
|
3508
3655
|
bq badge --clear &
|
|
3509
3656
|
|
|
3657
|
+
# The loop is healthy \u2014 the agent ended its turn on AskUserQuestion and the
|
|
3658
|
+
# user answered. Reset the Stop-hook nudge counter so its cap applies per
|
|
3659
|
+
# run of consecutive contract violations, not cumulatively across the session.
|
|
3660
|
+
rm -f "${runtimeDir2}/auq-nudge-$sid"
|
|
3661
|
+
|
|
3510
3662
|
# Halt the agent loop if the user signaled Done for now. The Stop hook
|
|
3511
3663
|
# (on-done.sh) will fire afterwards and mark the session as paused.
|
|
3512
3664
|
if printf '%s' "$done_check" | grep -q "Done for now"; then
|
|
3665
|
+
# Tell on-done.sh this Stop is a legitimate exit, not a dropped AUQ call \u2014
|
|
3666
|
+
# so it pauses normally instead of forcing the loop to continue.
|
|
3667
|
+
touch "${runtimeDir2}/done-$sid"
|
|
3668
|
+
|
|
3513
3669
|
# Promote the picked Done-for-now option's description into a session.recap
|
|
3514
3670
|
# event so the timeline has a dedicated end-of-session summary row. Bertrand
|
|
3515
3671
|
# forces session exit before Claude can write a closing message, so this
|
|
@@ -3527,7 +3683,7 @@ fi
|
|
|
3527
3683
|
wait
|
|
3528
3684
|
`;
|
|
3529
3685
|
}
|
|
3530
|
-
function permissionWaitScript(bin,
|
|
3686
|
+
function permissionWaitScript(bin, runtimeDir2) {
|
|
3531
3687
|
return `#!/usr/bin/env bash
|
|
3532
3688
|
# Hook: PermissionRequest \u2192 mark pending, badge + notify
|
|
3533
3689
|
${quietHelper(bin)}
|
|
@@ -3541,7 +3697,7 @@ ${EXTRACT_TOOL}
|
|
|
3541
3697
|
# Marker tells the PostToolUse hook to emit tool.used with outcome:approved
|
|
3542
3698
|
# instead of outcome:auto. Without it, every prompted-then-approved tool call
|
|
3543
3699
|
# would look identical to an auto-approved one.
|
|
3544
|
-
touch "${
|
|
3700
|
+
touch "${runtimeDir2}/perm-pending-$sid"
|
|
3545
3701
|
|
|
3546
3702
|
# Badge + notify in background
|
|
3547
3703
|
bq badge bell-exclamation --color '#ff6b35' --priority 25 --beep &
|
|
@@ -3549,7 +3705,7 @@ bq notify bertrand "Needs permission: $tool" &
|
|
|
3549
3705
|
wait
|
|
3550
3706
|
`;
|
|
3551
3707
|
}
|
|
3552
|
-
function permissionDoneScript(bin,
|
|
3708
|
+
function permissionDoneScript(bin, runtimeDir2) {
|
|
3553
3709
|
return `#!/usr/bin/env bash
|
|
3554
3710
|
# Hook: PostToolUse (catch-all)
|
|
3555
3711
|
#
|
|
@@ -3569,10 +3725,11 @@ sid="\${BERTRAND_SESSION:-}"
|
|
|
3569
3725
|
input="$(cat)"
|
|
3570
3726
|
${EXTRACT_TOOL}
|
|
3571
3727
|
|
|
3572
|
-
# Don't double-log: AskUserQuestion has its own waiting/answered events
|
|
3573
|
-
|
|
3728
|
+
# Don't double-log: AskUserQuestion has its own waiting/answered events, and
|
|
3729
|
+
# EnterWorktree/ExitWorktree have their own worktree.entered/exited hooks.
|
|
3730
|
+
case "$tool" in AskUserQuestion|EnterWorktree|ExitWorktree) exit 0 ;; esac
|
|
3574
3731
|
|
|
3575
|
-
marker="${
|
|
3732
|
+
marker="${runtimeDir2}/perm-pending-$sid"
|
|
3576
3733
|
had_marker=0
|
|
3577
3734
|
if [ -f "$marker" ]; then
|
|
3578
3735
|
had_marker=1
|
|
@@ -3626,9 +3783,17 @@ bq update --session-id "$sid" --event tool.used --meta "$(jq -n --arg t "$tool"
|
|
|
3626
3783
|
wait
|
|
3627
3784
|
`;
|
|
3628
3785
|
}
|
|
3629
|
-
function userPromptScript(bin,
|
|
3786
|
+
function userPromptScript(bin, runtimeDir2) {
|
|
3630
3787
|
return `#!/usr/bin/env bash
|
|
3631
|
-
# Hook: UserPromptSubmit \u2192 record user
|
|
3788
|
+
# Hook: UserPromptSubmit \u2192 record user prompt + re-inject the session contract.
|
|
3789
|
+
#
|
|
3790
|
+
# The contract normally arrives via --append-system-prompt on bertrand's own
|
|
3791
|
+
# claude spawn, which reaches only that one process. Sessions that inherit the
|
|
3792
|
+
# BERTRAND_* env vars without going through launchClaude (background jobs,
|
|
3793
|
+
# nested \`claude\`, the Warp plugin's own launcher) never receive it. Re-
|
|
3794
|
+
# injecting here \u2014 through the durable env/hook channel \u2014 closes that gap.
|
|
3795
|
+
# Full contract on the first prompt of each conversation, a one-line reminder
|
|
3796
|
+
# thereafter, to keep the per-turn token cost low.
|
|
3632
3797
|
${quietHelper(bin)}
|
|
3633
3798
|
sid="\${BERTRAND_SESSION:-}"
|
|
3634
3799
|
[ -z "$sid" ] && exit 0
|
|
@@ -3636,34 +3801,102 @@ sid="\${BERTRAND_SESSION:-}"
|
|
|
3636
3801
|
input="$(cat)"
|
|
3637
3802
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3638
3803
|
|
|
3804
|
+
# Record the prompt event. Stdout muted so only the context JSON below reaches
|
|
3805
|
+
# the hook's stdout (UserPromptSubmit parses stdout as a hook decision).
|
|
3639
3806
|
meta="$(printf '%s' "$input" | jq --arg cid "$cid" '{prompt: (.prompt // ""), claude_id: $cid}')"
|
|
3640
|
-
[ -
|
|
3807
|
+
[ -n "$meta" ] && bq update --session-id "$sid" --event user.prompt --meta "$meta" >/dev/null
|
|
3641
3808
|
|
|
3642
|
-
|
|
3809
|
+
# Re-deliver the contract as additional context.
|
|
3810
|
+
marker="${runtimeDir2}/contract-sent-\${cid:-$sid}"
|
|
3811
|
+
if [ -f "$marker" ]; then
|
|
3812
|
+
contract="$(bq contract --session-id "$sid" --short)"
|
|
3813
|
+
else
|
|
3814
|
+
contract="$(bq contract --session-id "$sid")"
|
|
3815
|
+
: > "$marker"
|
|
3816
|
+
fi
|
|
3817
|
+
|
|
3818
|
+
[ -n "$contract" ] && jq -n --arg c "$contract" '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $c}}'
|
|
3643
3819
|
`;
|
|
3644
3820
|
}
|
|
3645
|
-
function doneScript(bin,
|
|
3821
|
+
function doneScript(bin, runtimeDir2) {
|
|
3646
3822
|
return `#!/usr/bin/env bash
|
|
3647
|
-
# Hook: Stop \u2192 flip session status to paused
|
|
3648
|
-
# The status flip is driven by EVENT_STATUS_MAP[session.paused]; dispatchHookEvent
|
|
3649
|
-
# no longer has a session.paused case, so update.ts flips status without inserting.
|
|
3823
|
+
# Hook: Stop \u2192 enforce AUQ loop, else flip session status to paused.
|
|
3650
3824
|
${quietHelper(bin)}
|
|
3651
3825
|
sid="\${BERTRAND_SESSION:-}"
|
|
3652
3826
|
[ -z "$sid" ] && exit 0
|
|
3653
3827
|
|
|
3654
3828
|
input="$(cat)"
|
|
3655
3829
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3656
|
-
bq update --session-id "$sid" --event session.paused
|
|
3657
3830
|
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3831
|
+
done_marker="${runtimeDir2}/done-$sid"
|
|
3832
|
+
nudge_marker="${runtimeDir2}/auq-nudge-$sid"
|
|
3833
|
+
|
|
3834
|
+
# Capture the assistant turn either way. Stdout is muted so it can never corrupt
|
|
3835
|
+
# a decision-JSON payload. Dedups against the most-recent AskUQ-time capture, so
|
|
3836
|
+
# a Done-for-now exit lands zero new events; a dropped-AUQ Stop records the
|
|
3837
|
+
# stray turn; intermediate Stops with fresh output land normally.
|
|
3661
3838
|
tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
|
|
3662
3839
|
if [ -n "$tpath" ]; then
|
|
3663
|
-
bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3840
|
+
bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" >/dev/null &
|
|
3664
3841
|
fi
|
|
3665
3842
|
|
|
3843
|
+
if [ ! -f "$done_marker" ]; then
|
|
3844
|
+
# Not a Done-for-now exit \u2192 the turn ended without AskUserQuestion. Force the
|
|
3845
|
+
# loop to continue, up to a small cap so a context where AUQ is genuinely
|
|
3846
|
+
# unavailable can't wedge the session in an endless block/stop cycle. The
|
|
3847
|
+
# counter is reset on every answered AUQ (on-answered.sh), so the cap bounds
|
|
3848
|
+
# consecutive violations, not the whole session.
|
|
3849
|
+
count="$(cat "$nudge_marker" 2>/dev/null)"
|
|
3850
|
+
case "$count" in ''|*[!0-9]*) count=0 ;; esac
|
|
3851
|
+
if [ "$count" -lt 3 ]; then
|
|
3852
|
+
printf '%s' "$((count + 1))" > "$nudge_marker"
|
|
3853
|
+
reason='This is a bertrand session: every turn must end with an AskUserQuestion call (multiSelect:true on every question) that includes a "Done for now" option, preceded by a <recap> block. You ended a turn without calling AskUserQuestion. Call it now to continue the loop, or \u2014 if the work is finished \u2014 present it so the user can pick "Done for now" to end the session.'
|
|
3854
|
+
wait
|
|
3855
|
+
jq -n --arg r "$reason" '{decision:"block", reason:$r}'
|
|
3856
|
+
exit 0
|
|
3857
|
+
fi
|
|
3858
|
+
# Cap reached \u2014 stop nudging and let the session pause normally.
|
|
3859
|
+
fi
|
|
3860
|
+
|
|
3861
|
+
# Terminal path: legitimate Done-for-now exit, or nudge cap exhausted.
|
|
3862
|
+
rm -f "$done_marker" "$nudge_marker"
|
|
3863
|
+
bq update --session-id "$sid" --event session.paused
|
|
3666
3864
|
bq badge check --color '#58c142' --priority 10
|
|
3865
|
+
wait
|
|
3866
|
+
`;
|
|
3867
|
+
}
|
|
3868
|
+
function enterWorktreeScript(bin, runtimeDir2) {
|
|
3869
|
+
return `#!/usr/bin/env bash
|
|
3870
|
+
# Hook: PostToolUse EnterWorktree \u2192 record worktree path + branch on the session.
|
|
3871
|
+
${quietHelper(bin)}
|
|
3872
|
+
sid="\${BERTRAND_SESSION:-}"
|
|
3873
|
+
[ -z "$sid" ] && exit 0
|
|
3874
|
+
|
|
3875
|
+
input="$(cat)"
|
|
3876
|
+
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3877
|
+
|
|
3878
|
+
path="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
|
|
3879
|
+
[ -z "$path" ] && exit 0
|
|
3880
|
+
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
|
3881
|
+
|
|
3882
|
+
printf '%s' "$path" > "${runtimeDir2}/worktree-$sid"
|
|
3883
|
+
|
|
3884
|
+
bq update --session-id "$sid" --event worktree.entered --meta "$(jq -n --arg p "$path" --arg b "$branch" --arg cid "$cid" '{path:$p, branch:$b, claude_id:$cid}')"
|
|
3885
|
+
`;
|
|
3886
|
+
}
|
|
3887
|
+
function exitWorktreeScript(bin, runtimeDir2) {
|
|
3888
|
+
return `#!/usr/bin/env bash
|
|
3889
|
+
# Hook: PostToolUse ExitWorktree \u2192 clear the session's worktree state.
|
|
3890
|
+
${quietHelper(bin)}
|
|
3891
|
+
sid="\${BERTRAND_SESSION:-}"
|
|
3892
|
+
[ -z "$sid" ] && exit 0
|
|
3893
|
+
|
|
3894
|
+
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3895
|
+
marker="${runtimeDir2}/worktree-$sid"
|
|
3896
|
+
path="$(cat "$marker" 2>/dev/null)"
|
|
3897
|
+
rm -f "$marker"
|
|
3898
|
+
|
|
3899
|
+
bq update --session-id "$sid" --event worktree.exited --meta "$(jq -n --arg p "$path" --arg cid "$cid" '{path:$p, claude_id:$cid}')"
|
|
3667
3900
|
`;
|
|
3668
3901
|
}
|
|
3669
3902
|
var EXTRACT_TOOL = `tool="$(printf '%s' "$input" | grep -o '"tool_name":"[^"]*"' | cut -d'"' -f4)"`, HOOK_SCRIPTS;
|
|
@@ -3674,18 +3907,20 @@ var init_scripts = __esm(() => {
|
|
|
3674
3907
|
"on-permission-wait.sh": permissionWaitScript,
|
|
3675
3908
|
"on-permission-done.sh": permissionDoneScript,
|
|
3676
3909
|
"on-user-prompt.sh": userPromptScript,
|
|
3677
|
-
"on-done.sh": doneScript
|
|
3910
|
+
"on-done.sh": doneScript,
|
|
3911
|
+
"on-enter-worktree.sh": enterWorktreeScript,
|
|
3912
|
+
"on-exit-worktree.sh": exitWorktreeScript
|
|
3678
3913
|
};
|
|
3679
3914
|
});
|
|
3680
3915
|
|
|
3681
3916
|
// src/hooks/install.ts
|
|
3682
3917
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5, chmodSync as chmodSync2 } from "fs";
|
|
3683
|
-
import { join as
|
|
3918
|
+
import { join as join12 } from "path";
|
|
3684
3919
|
function installHookScripts(bin) {
|
|
3685
3920
|
mkdirSync7(paths.hooks, { recursive: true });
|
|
3686
3921
|
mkdirSync7(paths.runtime, { recursive: true });
|
|
3687
3922
|
for (const [filename, scriptFn] of Object.entries(HOOK_SCRIPTS)) {
|
|
3688
|
-
const filePath =
|
|
3923
|
+
const filePath = join12(paths.hooks, filename);
|
|
3689
3924
|
writeFileSync5(filePath, scriptFn(bin, paths.runtime));
|
|
3690
3925
|
chmodSync2(filePath, 493);
|
|
3691
3926
|
}
|
|
@@ -3698,7 +3933,7 @@ var init_install = __esm(() => {
|
|
|
3698
3933
|
|
|
3699
3934
|
// src/hooks/settings.ts
|
|
3700
3935
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "fs";
|
|
3701
|
-
import { join as
|
|
3936
|
+
import { join as join13, dirname as dirname4 } from "path";
|
|
3702
3937
|
import { homedir as homedir3 } from "os";
|
|
3703
3938
|
function isBertrandGroup(group) {
|
|
3704
3939
|
return group.hooks?.some((h) => h.command?.includes(".bertrand/hooks/")) ?? false;
|
|
@@ -3723,7 +3958,7 @@ function installHookSettings() {
|
|
|
3723
3958
|
var SETTINGS_PATH, BERTRAND_HOOKS;
|
|
3724
3959
|
var init_settings = __esm(() => {
|
|
3725
3960
|
init_paths();
|
|
3726
|
-
SETTINGS_PATH =
|
|
3961
|
+
SETTINGS_PATH = join13(homedir3(), ".claude", "settings.json");
|
|
3727
3962
|
BERTRAND_HOOKS = {
|
|
3728
3963
|
PreToolUse: [
|
|
3729
3964
|
{
|
|
@@ -3736,6 +3971,14 @@ var init_settings = __esm(() => {
|
|
|
3736
3971
|
matcher: "AskUserQuestion",
|
|
3737
3972
|
hooks: [{ type: "command", command: `${paths.hooks}/on-answered.sh` }]
|
|
3738
3973
|
},
|
|
3974
|
+
{
|
|
3975
|
+
matcher: "EnterWorktree",
|
|
3976
|
+
hooks: [{ type: "command", command: `${paths.hooks}/on-enter-worktree.sh` }]
|
|
3977
|
+
},
|
|
3978
|
+
{
|
|
3979
|
+
matcher: "ExitWorktree",
|
|
3980
|
+
hooks: [{ type: "command", command: `${paths.hooks}/on-exit-worktree.sh` }]
|
|
3981
|
+
},
|
|
3739
3982
|
{
|
|
3740
3983
|
matcher: "",
|
|
3741
3984
|
hooks: [{ type: "command", command: `${paths.hooks}/on-permission-done.sh` }]
|
|
@@ -3764,7 +4007,7 @@ var init_settings = __esm(() => {
|
|
|
3764
4007
|
|
|
3765
4008
|
// src/lib/completions.ts
|
|
3766
4009
|
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
|
|
3767
|
-
import { join as
|
|
4010
|
+
import { join as join14 } from "path";
|
|
3768
4011
|
function bashCompletion() {
|
|
3769
4012
|
return `# bertrand bash completion
|
|
3770
4013
|
_bertrand() {
|
|
@@ -3794,11 +4037,11 @@ function fishCompletion() {
|
|
|
3794
4037
|
`;
|
|
3795
4038
|
}
|
|
3796
4039
|
function generateCompletions() {
|
|
3797
|
-
const dir =
|
|
4040
|
+
const dir = join14(paths.root, "completions");
|
|
3798
4041
|
mkdirSync9(dir, { recursive: true });
|
|
3799
|
-
writeFileSync7(
|
|
3800
|
-
writeFileSync7(
|
|
3801
|
-
writeFileSync7(
|
|
4042
|
+
writeFileSync7(join14(dir, "bertrand.bash"), bashCompletion());
|
|
4043
|
+
writeFileSync7(join14(dir, "_bertrand"), zshCompletion());
|
|
4044
|
+
writeFileSync7(join14(dir, "bertrand.fish"), fishCompletion());
|
|
3802
4045
|
console.log(`Shell completions written to ${dir}`);
|
|
3803
4046
|
console.log(" Add to your shell config:");
|
|
3804
4047
|
console.log(` bash: source ${dir}/bertrand.bash`);
|
|
@@ -3829,7 +4072,7 @@ var init_completions = __esm(() => {
|
|
|
3829
4072
|
var exports_init = {};
|
|
3830
4073
|
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, chmodSync as chmodSync3 } from "fs";
|
|
3831
4074
|
import { execSync as execSync2 } from "child_process";
|
|
3832
|
-
import { join as
|
|
4075
|
+
import { join as join15 } from "path";
|
|
3833
4076
|
function detectTerminal() {
|
|
3834
4077
|
try {
|
|
3835
4078
|
execSync2("which wsh", { stdio: "ignore" });
|
|
@@ -3844,8 +4087,8 @@ function resolveBin() {
|
|
|
3844
4087
|
return onPath;
|
|
3845
4088
|
const entry = process.argv[1];
|
|
3846
4089
|
if (entry && SOURCE_ENTRY.test(entry)) {
|
|
3847
|
-
const launcherDir =
|
|
3848
|
-
const launcher =
|
|
4090
|
+
const launcherDir = join15(paths.root, "bin");
|
|
4091
|
+
const launcher = join15(launcherDir, "bertrand-dev");
|
|
3849
4092
|
mkdirSync10(launcherDir, { recursive: true });
|
|
3850
4093
|
writeFileSync8(launcher, `#!/usr/bin/env bash
|
|
3851
4094
|
exec ${process.execPath} ${JSON.stringify(entry)} "$@"
|
|
@@ -4056,6 +4299,9 @@ function extractSummary(row) {
|
|
|
4056
4299
|
return meta.prompt ?? "";
|
|
4057
4300
|
case "session.recap":
|
|
4058
4301
|
return meta.recap ?? "";
|
|
4302
|
+
case "worktree.entered":
|
|
4303
|
+
case "worktree.exited":
|
|
4304
|
+
return meta.branch ?? meta.path ?? "";
|
|
4059
4305
|
default:
|
|
4060
4306
|
return "";
|
|
4061
4307
|
}
|
|
@@ -4093,7 +4339,9 @@ var init_catalog = __esm(() => {
|
|
|
4093
4339
|
"tool.work": { label: "tool work", category: "work", color: 214, detailColor: 245, skip: false },
|
|
4094
4340
|
"session.recap": { label: "session recap", category: "lifecycle", color: 33, detailColor: 245, skip: false },
|
|
4095
4341
|
"assistant.message": { label: "claude", category: "interaction", color: 39, detailColor: 245, skip: false },
|
|
4096
|
-
"assistant.recap": { label: "recap", category: "interaction", color: 39, detailColor: 245, skip: false }
|
|
4342
|
+
"assistant.recap": { label: "recap", category: "interaction", color: 39, detailColor: 245, skip: false },
|
|
4343
|
+
"worktree.entered": { label: "worktree entered", category: "lifecycle", color: 78, detailColor: 245, skip: false },
|
|
4344
|
+
"worktree.exited": { label: "worktree exited", category: "lifecycle", color: 245, detailColor: 245, skip: false }
|
|
4097
4345
|
};
|
|
4098
4346
|
DEFAULT_INFO = {
|
|
4099
4347
|
label: "unknown",
|
|
@@ -4400,13 +4648,11 @@ var init_log = __esm(() => {
|
|
|
4400
4648
|
init_router();
|
|
4401
4649
|
init_sessions();
|
|
4402
4650
|
init_events();
|
|
4403
|
-
init_categories();
|
|
4404
4651
|
init_stats();
|
|
4405
4652
|
init_conversations();
|
|
4406
4653
|
init_catalog();
|
|
4407
4654
|
init_compact();
|
|
4408
4655
|
init_timing();
|
|
4409
|
-
init_parse_session_name();
|
|
4410
4656
|
init_format();
|
|
4411
4657
|
init_resolve();
|
|
4412
4658
|
init_cli_flag();
|
|
@@ -4426,18 +4672,12 @@ var init_log = __esm(() => {
|
|
|
4426
4672
|
showAllSessions();
|
|
4427
4673
|
return;
|
|
4428
4674
|
}
|
|
4429
|
-
const
|
|
4430
|
-
|
|
4431
|
-
if (!category) {
|
|
4432
|
-
console.error(`Category not found: ${categoryPath}`);
|
|
4433
|
-
process.exit(1);
|
|
4434
|
-
}
|
|
4435
|
-
const session = getSessionByCategorySlug(category.id, slug);
|
|
4436
|
-
if (!session) {
|
|
4675
|
+
const resolved = resolveSessionByName(target);
|
|
4676
|
+
if (!resolved) {
|
|
4437
4677
|
console.error(`Session not found: ${target}`);
|
|
4438
4678
|
process.exit(1);
|
|
4439
4679
|
}
|
|
4440
|
-
showSessionLog(session, `${categoryPath}/${slug}`, isJson);
|
|
4680
|
+
showSessionLog(resolved.session, `${resolved.categoryPath}/${resolved.slug}`, isJson);
|
|
4441
4681
|
});
|
|
4442
4682
|
});
|
|
4443
4683
|
|
|
@@ -4557,7 +4797,6 @@ var init_stats2 = __esm(() => {
|
|
|
4557
4797
|
init_events();
|
|
4558
4798
|
init_timing();
|
|
4559
4799
|
init_format();
|
|
4560
|
-
init_parse_session_name();
|
|
4561
4800
|
ACTIVE_STATUSES2 = ["active", "waiting"];
|
|
4562
4801
|
register("stats", async (args) => {
|
|
4563
4802
|
const isJson = args.includes("--json");
|
|
@@ -4570,29 +4809,23 @@ var init_stats2 = __esm(() => {
|
|
|
4570
4809
|
return;
|
|
4571
4810
|
}
|
|
4572
4811
|
if (target.endsWith("/")) {
|
|
4573
|
-
const
|
|
4574
|
-
const
|
|
4575
|
-
if (!
|
|
4576
|
-
console.error(`Category not found: ${
|
|
4812
|
+
const categoryPath = target.replace(/\/+$/, "");
|
|
4813
|
+
const category = getCategoryByPath(categoryPath);
|
|
4814
|
+
if (!category) {
|
|
4815
|
+
console.error(`Category not found: ${categoryPath}`);
|
|
4577
4816
|
process.exit(1);
|
|
4578
4817
|
}
|
|
4579
|
-
const categorySessions = getSessionsByCategory(
|
|
4818
|
+
const categorySessions = getSessionsByCategory(category.id);
|
|
4580
4819
|
const metrics = categorySessions.map((s) => getMetrics(s.id, s.slug, s.status));
|
|
4581
|
-
renderCategory(metrics,
|
|
4820
|
+
renderCategory(metrics, categoryPath, isJson);
|
|
4582
4821
|
return;
|
|
4583
4822
|
}
|
|
4584
|
-
const
|
|
4585
|
-
|
|
4586
|
-
if (!category) {
|
|
4587
|
-
console.error(`Category not found: ${categoryPath}`);
|
|
4588
|
-
process.exit(1);
|
|
4589
|
-
}
|
|
4590
|
-
const session = getSessionByCategorySlug(category.id, slug);
|
|
4591
|
-
if (!session) {
|
|
4823
|
+
const resolved = resolveSessionByName(target);
|
|
4824
|
+
if (!resolved) {
|
|
4592
4825
|
console.error(`Session not found: ${target}`);
|
|
4593
4826
|
process.exit(1);
|
|
4594
4827
|
}
|
|
4595
|
-
const m = getMetrics(session.id, `${categoryPath}/${slug}`, session.status);
|
|
4828
|
+
const m = getMetrics(resolved.session.id, `${resolved.categoryPath}/${resolved.slug}`, resolved.session.status);
|
|
4596
4829
|
renderSession(m, isJson);
|
|
4597
4830
|
});
|
|
4598
4831
|
});
|
|
@@ -4618,24 +4851,16 @@ var init_backfill_stats = __esm(() => {
|
|
|
4618
4851
|
// src/cli/commands/archive.ts
|
|
4619
4852
|
var exports_archive = {};
|
|
4620
4853
|
function resolveSession(name) {
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
if (!category) {
|
|
4624
|
-
console.error(`Category not found: ${categoryPath}`);
|
|
4625
|
-
process.exit(1);
|
|
4626
|
-
}
|
|
4627
|
-
const session = getSessionByCategorySlug(category.id, slug);
|
|
4628
|
-
if (!session) {
|
|
4854
|
+
const resolved = resolveSessionByName(name);
|
|
4855
|
+
if (!resolved) {
|
|
4629
4856
|
console.error(`Session not found: ${name}`);
|
|
4630
4857
|
process.exit(1);
|
|
4631
4858
|
}
|
|
4632
|
-
return { session, categoryPath };
|
|
4859
|
+
return { session: resolved.session, categoryPath: resolved.categoryPath };
|
|
4633
4860
|
}
|
|
4634
4861
|
var init_archive = __esm(() => {
|
|
4635
4862
|
init_router();
|
|
4636
4863
|
init_sessions();
|
|
4637
|
-
init_categories();
|
|
4638
|
-
init_parse_session_name();
|
|
4639
4864
|
init_session_archive();
|
|
4640
4865
|
register("archive", async (args) => {
|
|
4641
4866
|
const isUndo = args.includes("--undo");
|
|
@@ -4707,7 +4932,7 @@ __export(exports_project, {
|
|
|
4707
4932
|
createSubcommand: () => createSubcommand,
|
|
4708
4933
|
_UsageError: () => _UsageError
|
|
4709
4934
|
});
|
|
4710
|
-
import { existsSync as existsSync9, rmSync } from "fs";
|
|
4935
|
+
import { existsSync as existsSync9, rmSync as rmSync2 } from "fs";
|
|
4711
4936
|
function countSessions(slug) {
|
|
4712
4937
|
const dbFile = projectPaths(slug).db;
|
|
4713
4938
|
if (!existsSync9(dbFile))
|
|
@@ -4887,7 +5112,7 @@ function removeSubcommand(args) {
|
|
|
4887
5112
|
removeProject(slug);
|
|
4888
5113
|
invalidateDbCache(slug);
|
|
4889
5114
|
if (purge) {
|
|
4890
|
-
|
|
5115
|
+
rmSync2(projectPaths(slug).root, { recursive: true, force: true });
|
|
4891
5116
|
}
|
|
4892
5117
|
console.log(`Removed project "${slug}"${purge ? " (directory purged)" : " (directory left on disk; pass --purge to delete)"}.`);
|
|
4893
5118
|
}
|
|
@@ -4997,6 +5222,7 @@ var command = process.argv[2];
|
|
|
4997
5222
|
var hotPath = {
|
|
4998
5223
|
update: () => Promise.resolve().then(() => (init_update(), exports_update)),
|
|
4999
5224
|
"assistant-message": () => Promise.resolve().then(() => (init_assistant_message(), exports_assistant_message)),
|
|
5225
|
+
contract: () => Promise.resolve().then(() => (init_contract(), exports_contract)),
|
|
5000
5226
|
badge: () => Promise.resolve().then(() => (init_badge(), exports_badge)),
|
|
5001
5227
|
notify: () => Promise.resolve().then(() => (init_notify(), exports_notify)),
|
|
5002
5228
|
serve: () => Promise.resolve().then(() => (init_serve(), exports_serve)),
|
|
@@ -5015,6 +5241,7 @@ if (command && command in hotPath) {
|
|
|
5015
5241
|
Promise.resolve().then(() => (init_archive(), exports_archive)),
|
|
5016
5242
|
Promise.resolve().then(() => (init_update(), exports_update)),
|
|
5017
5243
|
Promise.resolve().then(() => (init_assistant_message(), exports_assistant_message)),
|
|
5244
|
+
Promise.resolve().then(() => (init_contract(), exports_contract)),
|
|
5018
5245
|
Promise.resolve().then(() => (init_serve(), exports_serve)),
|
|
5019
5246
|
Promise.resolve().then(() => (init_badge(), exports_badge)),
|
|
5020
5247
|
Promise.resolve().then(() => (init_notify(), exports_notify)),
|