agent-trajectories 0.5.2 → 0.5.4
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/{chunk-4MACDCF4.js → chunk-W222QB6V.js} +346 -41
- package/dist/chunk-W222QB6V.js.map +1 -0
- package/dist/cli/index.js +1739 -195
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-Ce7r5yv0.d.ts → index-7tzw_CMS.d.ts} +122 -37
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +3 -1
- package/package.json +1 -1
- package/dist/chunk-4MACDCF4.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -49,18 +49,20 @@ var TrajectoryStatusSchema = z.enum([
|
|
|
49
49
|
"completed",
|
|
50
50
|
"abandoned"
|
|
51
51
|
]);
|
|
52
|
-
var TrajectoryEventTypeSchema = z.
|
|
53
|
-
"prompt",
|
|
54
|
-
"thinking",
|
|
55
|
-
"tool_call",
|
|
56
|
-
"tool_result",
|
|
57
|
-
"message_sent",
|
|
58
|
-
"message_received",
|
|
59
|
-
"decision",
|
|
60
|
-
"finding",
|
|
61
|
-
"reflection",
|
|
62
|
-
"note",
|
|
63
|
-
"error"
|
|
52
|
+
var TrajectoryEventTypeSchema = z.union([
|
|
53
|
+
z.literal("prompt"),
|
|
54
|
+
z.literal("thinking"),
|
|
55
|
+
z.literal("tool_call"),
|
|
56
|
+
z.literal("tool_result"),
|
|
57
|
+
z.literal("message_sent"),
|
|
58
|
+
z.literal("message_received"),
|
|
59
|
+
z.literal("decision"),
|
|
60
|
+
z.literal("finding"),
|
|
61
|
+
z.literal("reflection"),
|
|
62
|
+
z.literal("note"),
|
|
63
|
+
z.literal("error"),
|
|
64
|
+
z.string()
|
|
65
|
+
// Allow event types emitted by other tools (e.g. agent-relay's completion-evidence / completion-marker). Downstream code filters to known types.
|
|
64
66
|
]);
|
|
65
67
|
var EventSignificanceSchema = z.enum([
|
|
66
68
|
"low",
|
|
@@ -90,7 +92,7 @@ var DecisionSchema = z.object({
|
|
|
90
92
|
});
|
|
91
93
|
var AgentParticipationSchema = z.object({
|
|
92
94
|
name: z.string().min(1, "Agent name is required"),
|
|
93
|
-
role: z.
|
|
95
|
+
role: z.string().min(1, "Agent role is required"),
|
|
94
96
|
joinedAt: z.string().datetime(),
|
|
95
97
|
leftAt: z.string().datetime().optional()
|
|
96
98
|
});
|
|
@@ -150,7 +152,7 @@ var TrajectoryTraceRefSchema = z.object({
|
|
|
150
152
|
traceId: z.string().optional()
|
|
151
153
|
});
|
|
152
154
|
var TrajectorySchema = z.object({
|
|
153
|
-
id: z.string().regex(/^traj_[a-z0-
|
|
155
|
+
id: z.string().regex(/^traj_[a-z0-9_]+$/, "Invalid trajectory ID format"),
|
|
154
156
|
version: z.literal(1),
|
|
155
157
|
task: TaskReferenceSchema,
|
|
156
158
|
status: TrajectoryStatusSchema,
|
|
@@ -159,10 +161,11 @@ var TrajectorySchema = z.object({
|
|
|
159
161
|
agents: z.array(AgentParticipationSchema),
|
|
160
162
|
chapters: z.array(ChapterSchema),
|
|
161
163
|
retrospective: RetrospectiveSchema.optional(),
|
|
162
|
-
commits: z.array(z.string()),
|
|
163
|
-
filesChanged: z.array(z.string()),
|
|
164
|
-
projectId: z.string(),
|
|
165
|
-
|
|
164
|
+
commits: z.array(z.string()).default([]),
|
|
165
|
+
filesChanged: z.array(z.string()).default([]),
|
|
166
|
+
projectId: z.string().optional(),
|
|
167
|
+
workflowId: z.string().optional(),
|
|
168
|
+
tags: z.array(z.string()).default([]),
|
|
166
169
|
_trace: TrajectoryTraceRefSchema.optional()
|
|
167
170
|
});
|
|
168
171
|
var CreateTrajectoryInputSchema = z.object({
|
|
@@ -564,11 +567,11 @@ function extractDecisions(trajectory) {
|
|
|
564
567
|
}
|
|
565
568
|
|
|
566
569
|
// src/storage/file.ts
|
|
567
|
-
function expandPath(
|
|
568
|
-
if (
|
|
569
|
-
return join(process.env.HOME ?? "",
|
|
570
|
+
function expandPath(path2) {
|
|
571
|
+
if (path2.startsWith("~")) {
|
|
572
|
+
return join(process.env.HOME ?? "", path2.slice(1));
|
|
570
573
|
}
|
|
571
|
-
return
|
|
574
|
+
return path2;
|
|
572
575
|
}
|
|
573
576
|
function getSearchPaths() {
|
|
574
577
|
const searchPathsEnv = process.env.TRAJECTORIES_SEARCH_PATHS;
|
|
@@ -613,11 +616,129 @@ var FileStorage = class {
|
|
|
613
616
|
trajectories: {}
|
|
614
617
|
});
|
|
615
618
|
}
|
|
619
|
+
await this.reconcileIndex();
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Scan active/ and completed/ recursively and add any trajectory files
|
|
623
|
+
* missing from the index. Existing entries are preserved — reconcile
|
|
624
|
+
* only adds, never removes.
|
|
625
|
+
*
|
|
626
|
+
* Handles three on-disk layouts in completed/:
|
|
627
|
+
* - flat: completed/{id}.json (legacy workforce data)
|
|
628
|
+
* - monthly: completed/YYYY-MM/{id}.json (current save() writes)
|
|
629
|
+
* - nested: completed/.../{id}.json (defensive — any depth)
|
|
630
|
+
*
|
|
631
|
+
* Returns a ReconcileSummary so tests and CLI wrappers can observe
|
|
632
|
+
* outcomes without parsing logs. Only writes the index if anything was
|
|
633
|
+
* added.
|
|
634
|
+
*/
|
|
635
|
+
async reconcileIndex() {
|
|
636
|
+
const summary = {
|
|
637
|
+
scanned: 0,
|
|
638
|
+
added: 0,
|
|
639
|
+
alreadyIndexed: 0,
|
|
640
|
+
skippedMalformedJson: 0,
|
|
641
|
+
skippedSchemaViolation: 0,
|
|
642
|
+
skippedIoError: 0
|
|
643
|
+
};
|
|
644
|
+
const index = await this.loadIndex();
|
|
645
|
+
const before = Object.keys(index.trajectories).length;
|
|
646
|
+
const discovered = [];
|
|
647
|
+
try {
|
|
648
|
+
const activeFiles = await readdir(this.activeDir);
|
|
649
|
+
for (const file of activeFiles) {
|
|
650
|
+
if (!file.endsWith(".json")) continue;
|
|
651
|
+
discovered.push(join(this.activeDir, file));
|
|
652
|
+
}
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (error.code !== "ENOENT") throw error;
|
|
655
|
+
}
|
|
656
|
+
await this.walkJsonFilesInto(this.completedDir, discovered);
|
|
657
|
+
for (const filePath of discovered) {
|
|
658
|
+
summary.scanned += 1;
|
|
659
|
+
const result = await this.readTrajectoryFile(filePath);
|
|
660
|
+
if (!result.ok) {
|
|
661
|
+
if (result.reason === "malformed_json") {
|
|
662
|
+
summary.skippedMalformedJson += 1;
|
|
663
|
+
} else if (result.reason === "schema_violation") {
|
|
664
|
+
summary.skippedSchemaViolation += 1;
|
|
665
|
+
} else {
|
|
666
|
+
summary.skippedIoError += 1;
|
|
667
|
+
}
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const trajectory = result.trajectory;
|
|
671
|
+
if (index.trajectories[trajectory.id]) {
|
|
672
|
+
summary.alreadyIndexed += 1;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
index.trajectories[trajectory.id] = {
|
|
676
|
+
title: trajectory.task.title,
|
|
677
|
+
status: trajectory.status,
|
|
678
|
+
startedAt: trajectory.startedAt,
|
|
679
|
+
completedAt: trajectory.completedAt,
|
|
680
|
+
path: filePath
|
|
681
|
+
};
|
|
682
|
+
summary.added += 1;
|
|
683
|
+
}
|
|
684
|
+
if (Object.keys(index.trajectories).length !== before) {
|
|
685
|
+
await this.saveIndex(index);
|
|
686
|
+
}
|
|
687
|
+
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
688
|
+
if (summary.added > 0 || hadSkips) {
|
|
689
|
+
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
690
|
+
if (summary.skippedMalformedJson > 0) {
|
|
691
|
+
parts.push(`malformed: ${summary.skippedMalformedJson}`);
|
|
692
|
+
}
|
|
693
|
+
if (summary.skippedSchemaViolation > 0) {
|
|
694
|
+
parts.push(`invalid: ${summary.skippedSchemaViolation}`);
|
|
695
|
+
}
|
|
696
|
+
if (summary.skippedIoError > 0) {
|
|
697
|
+
parts.push(`io: ${summary.skippedIoError}`);
|
|
698
|
+
}
|
|
699
|
+
console.warn(`[trajectories] ${parts.join(", ")}`);
|
|
700
|
+
}
|
|
701
|
+
return summary;
|
|
616
702
|
}
|
|
617
703
|
/**
|
|
618
|
-
*
|
|
704
|
+
* Recursively collect all .json file paths under `dir` into `out`.
|
|
705
|
+
* Silently treats a missing directory as empty.
|
|
619
706
|
*/
|
|
620
|
-
async
|
|
707
|
+
async walkJsonFilesInto(dir, out) {
|
|
708
|
+
let entries;
|
|
709
|
+
try {
|
|
710
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (error.code === "ENOENT") return;
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
for (const entry of entries) {
|
|
716
|
+
const entryPath = join(dir, entry.name);
|
|
717
|
+
if (entry.isDirectory()) {
|
|
718
|
+
await this.walkJsonFilesInto(entryPath, out);
|
|
719
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
720
|
+
out.push(entryPath);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Save a trajectory.
|
|
726
|
+
*
|
|
727
|
+
* Validates the input against the trajectory schema before touching
|
|
728
|
+
* disk. Closes the historical read/write asymmetry where save() would
|
|
729
|
+
* happily write data that the reader then rejected, producing files
|
|
730
|
+
* that could never be loaded back.
|
|
731
|
+
*/
|
|
732
|
+
async save(input) {
|
|
733
|
+
const validation = validateTrajectory(input);
|
|
734
|
+
if (!validation.success) {
|
|
735
|
+
const issues = validation.errors?.issues.map((issue) => {
|
|
736
|
+
const path2 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
737
|
+
return `${path2}: ${issue.message}`;
|
|
738
|
+
}).join("; ") ?? "unknown validation error";
|
|
739
|
+
throw new Error(`Cannot save invalid trajectory: ${issues}`);
|
|
740
|
+
}
|
|
741
|
+
const trajectory = validation.data;
|
|
621
742
|
const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
|
|
622
743
|
let filePath;
|
|
623
744
|
if (isCompleted) {
|
|
@@ -647,19 +768,23 @@ var FileStorage = class {
|
|
|
647
768
|
async get(id) {
|
|
648
769
|
const activePath = join(this.activeDir, `${id}.json`);
|
|
649
770
|
if (existsSync(activePath)) {
|
|
650
|
-
return this.
|
|
771
|
+
return this.readTrajectoryOrNull(activePath);
|
|
651
772
|
}
|
|
652
773
|
const index = await this.loadIndex();
|
|
653
774
|
const entry = index.trajectories[id];
|
|
654
775
|
if (entry?.path && existsSync(entry.path)) {
|
|
655
|
-
return this.
|
|
776
|
+
return this.readTrajectoryOrNull(entry.path);
|
|
656
777
|
}
|
|
657
778
|
try {
|
|
779
|
+
const flatPath = join(this.completedDir, `${id}.json`);
|
|
780
|
+
if (existsSync(flatPath)) {
|
|
781
|
+
return this.readTrajectoryOrNull(flatPath);
|
|
782
|
+
}
|
|
658
783
|
const months = await readdir(this.completedDir);
|
|
659
784
|
for (const month of months) {
|
|
660
785
|
const filePath = join(this.completedDir, month, `${id}.json`);
|
|
661
786
|
if (existsSync(filePath)) {
|
|
662
|
-
return this.
|
|
787
|
+
return this.readTrajectoryOrNull(filePath);
|
|
663
788
|
}
|
|
664
789
|
}
|
|
665
790
|
} catch (error) {
|
|
@@ -682,7 +807,7 @@ var FileStorage = class {
|
|
|
682
807
|
let mostRecent = null;
|
|
683
808
|
let mostRecentTime = 0;
|
|
684
809
|
for (const file of jsonFiles) {
|
|
685
|
-
const trajectory = await this.
|
|
810
|
+
const trajectory = await this.readTrajectoryOrNull(
|
|
686
811
|
join(this.activeDir, file)
|
|
687
812
|
);
|
|
688
813
|
if (trajectory) {
|
|
@@ -810,20 +935,44 @@ var FileStorage = class {
|
|
|
810
935
|
async close() {
|
|
811
936
|
}
|
|
812
937
|
// Private helpers
|
|
813
|
-
|
|
938
|
+
/**
|
|
939
|
+
* Read a trajectory file and return a tagged result so callers can
|
|
940
|
+
* distinguish missing files, malformed JSON, and schema violations.
|
|
941
|
+
*
|
|
942
|
+
* Does NOT log. Callers choose whether to warn, swallow, or throw.
|
|
943
|
+
*/
|
|
944
|
+
async readTrajectoryFile(path2) {
|
|
945
|
+
let content;
|
|
814
946
|
try {
|
|
815
|
-
|
|
816
|
-
const data = JSON.parse(content);
|
|
817
|
-
const validation = validateTrajectory(data);
|
|
818
|
-
if (validation.success) {
|
|
819
|
-
return validation.data;
|
|
820
|
-
}
|
|
821
|
-
console.error(`Invalid trajectory at ${path}:`, validation.errors);
|
|
822
|
-
return null;
|
|
947
|
+
content = await readFile(path2, "utf-8");
|
|
823
948
|
} catch (error) {
|
|
824
|
-
|
|
825
|
-
return null;
|
|
949
|
+
return { ok: false, reason: "io_error", path: path2, error };
|
|
826
950
|
}
|
|
951
|
+
let data;
|
|
952
|
+
try {
|
|
953
|
+
data = JSON.parse(content);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
return { ok: false, reason: "malformed_json", path: path2, error };
|
|
956
|
+
}
|
|
957
|
+
const validation = validateTrajectory(data);
|
|
958
|
+
if (validation.success) {
|
|
959
|
+
return { ok: true, trajectory: validation.data };
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
reason: "schema_violation",
|
|
964
|
+
path: path2,
|
|
965
|
+
error: validation.errors
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Convenience wrapper for callers that only care whether they got a
|
|
970
|
+
* trajectory. Returns null for any failure and writes nothing to the
|
|
971
|
+
* console — so nothing leaks into test output or the CLI spinner.
|
|
972
|
+
*/
|
|
973
|
+
async readTrajectoryOrNull(path2) {
|
|
974
|
+
const result = await this.readTrajectoryFile(path2);
|
|
975
|
+
return result.ok ? result.trajectory : null;
|
|
827
976
|
}
|
|
828
977
|
async loadIndex() {
|
|
829
978
|
try {
|
|
@@ -880,9 +1029,1132 @@ function registerAbandonCommand(program2) {
|
|
|
880
1029
|
}
|
|
881
1030
|
|
|
882
1031
|
// src/cli/commands/compact.ts
|
|
883
|
-
import {
|
|
884
|
-
import { existsSync as
|
|
1032
|
+
import { execFileSync } from "child_process";
|
|
1033
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1034
|
+
import { dirname, join as join4 } from "path";
|
|
1035
|
+
|
|
1036
|
+
// src/compact/config.ts
|
|
1037
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
885
1038
|
import { join as join2 } from "path";
|
|
1039
|
+
var DEFAULT_CONFIG = {
|
|
1040
|
+
provider: "auto",
|
|
1041
|
+
model: void 0,
|
|
1042
|
+
maxInputTokens: 3e4,
|
|
1043
|
+
maxOutputTokens: 4e3,
|
|
1044
|
+
temperature: 0.3
|
|
1045
|
+
};
|
|
1046
|
+
function getCompactionConfig() {
|
|
1047
|
+
const fileConfig = loadFileConfig();
|
|
1048
|
+
return {
|
|
1049
|
+
provider: readStringEnv("TRAJECTORIES_LLM_PROVIDER") ?? readString(fileConfig.provider) ?? DEFAULT_CONFIG.provider,
|
|
1050
|
+
model: readStringEnv("TRAJECTORIES_LLM_MODEL") ?? readString(fileConfig.model) ?? DEFAULT_CONFIG.model,
|
|
1051
|
+
maxInputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_INPUT_TOKENS") ?? readNumber(fileConfig.maxInputTokens) ?? DEFAULT_CONFIG.maxInputTokens,
|
|
1052
|
+
maxOutputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_OUTPUT_TOKENS") ?? readNumber(fileConfig.maxOutputTokens) ?? DEFAULT_CONFIG.maxOutputTokens,
|
|
1053
|
+
temperature: readNumberEnv("TRAJECTORIES_LLM_TEMPERATURE") ?? readNumber(fileConfig.temperature) ?? DEFAULT_CONFIG.temperature
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function loadFileConfig() {
|
|
1057
|
+
const configPath = join2(getPrimaryConfigDir(), "config.json");
|
|
1058
|
+
if (!existsSync2(configPath)) {
|
|
1059
|
+
return {};
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1063
|
+
if (!isRecord(raw)) {
|
|
1064
|
+
return {};
|
|
1065
|
+
}
|
|
1066
|
+
const merged = {};
|
|
1067
|
+
for (const section of [raw, raw.compaction, raw.llm]) {
|
|
1068
|
+
if (!isRecord(section)) {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
for (const [key, value] of Object.entries(section)) {
|
|
1072
|
+
if (key === "compaction" || key === "llm") {
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
merged[key] = value;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
provider: readString(merged.provider),
|
|
1080
|
+
model: readString(merged.model),
|
|
1081
|
+
maxInputTokens: readNumber(merged.maxInputTokens),
|
|
1082
|
+
maxOutputTokens: readNumber(merged.maxOutputTokens),
|
|
1083
|
+
temperature: readNumber(merged.temperature)
|
|
1084
|
+
};
|
|
1085
|
+
} catch {
|
|
1086
|
+
return {};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function getPrimaryConfigDir() {
|
|
1090
|
+
const searchPaths = getSearchPaths();
|
|
1091
|
+
return searchPaths[0] ?? join2(process.cwd(), ".trajectories");
|
|
1092
|
+
}
|
|
1093
|
+
function readStringEnv(name) {
|
|
1094
|
+
return readString(process.env[name]);
|
|
1095
|
+
}
|
|
1096
|
+
function readNumberEnv(name) {
|
|
1097
|
+
return readNumber(process.env[name]);
|
|
1098
|
+
}
|
|
1099
|
+
function readString(value) {
|
|
1100
|
+
if (typeof value !== "string") {
|
|
1101
|
+
return void 0;
|
|
1102
|
+
}
|
|
1103
|
+
const trimmed = value.trim();
|
|
1104
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1105
|
+
}
|
|
1106
|
+
function readNumber(value) {
|
|
1107
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1108
|
+
return value;
|
|
1109
|
+
}
|
|
1110
|
+
if (typeof value !== "string") {
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
const trimmed = value.trim();
|
|
1114
|
+
if (trimmed.length === 0) {
|
|
1115
|
+
return void 0;
|
|
1116
|
+
}
|
|
1117
|
+
const parsed = Number(trimmed);
|
|
1118
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1119
|
+
}
|
|
1120
|
+
function isRecord(value) {
|
|
1121
|
+
return typeof value === "object" && value !== null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/compact/markdown.ts
|
|
1125
|
+
function generateCompactionMarkdown(compacted) {
|
|
1126
|
+
const dateRange = `${formatDate2(compacted.dateRange.start)} - ${formatDate2(compacted.dateRange.end)}`;
|
|
1127
|
+
const agents = compacted.summary.uniqueAgents.length > 0 ? compacted.summary.uniqueAgents.join(", ") : "None";
|
|
1128
|
+
const decisionRows = compacted.decisions.length > 0 ? compacted.decisions.map(
|
|
1129
|
+
(decision) => `| ${escapeTableCell(decision.question)} | ${escapeTableCell(decision.chosen)} | ${escapeTableCell(decision.impact)} |`
|
|
1130
|
+
).join("\n") : "| None identified | | |";
|
|
1131
|
+
const conventions = compacted.conventions.length > 0 ? compacted.conventions.map(
|
|
1132
|
+
(convention) => `- **${convention.pattern || "Unnamed pattern"}**: ${convention.rationale || "No rationale captured."} (scope: ${convention.scope || "unspecified"})`
|
|
1133
|
+
).join("\n") : "- None established.";
|
|
1134
|
+
const lessons = compacted.lessons.length > 0 ? compacted.lessons.map((lesson) => {
|
|
1135
|
+
const context = lesson.context ? ` (${lesson.context})` : "";
|
|
1136
|
+
const recommendation = lesson.recommendation ? ` - ${lesson.recommendation}` : "";
|
|
1137
|
+
return `- ${lesson.lesson}${context}${recommendation}`;
|
|
1138
|
+
}).join("\n") : "- None captured.";
|
|
1139
|
+
const openQuestions = compacted.openQuestions.length > 0 ? compacted.openQuestions.map((question) => `- ${question}`).join("\n") : "- None.";
|
|
1140
|
+
return [
|
|
1141
|
+
`# Trajectory Compaction: ${dateRange}`,
|
|
1142
|
+
"",
|
|
1143
|
+
"## Summary",
|
|
1144
|
+
compacted.narrative || "No narrative available.",
|
|
1145
|
+
"",
|
|
1146
|
+
`## Key Decisions (${compacted.decisions.length})`,
|
|
1147
|
+
"| Question | Decision | Impact |",
|
|
1148
|
+
"|----------|----------|--------|",
|
|
1149
|
+
decisionRows,
|
|
1150
|
+
"",
|
|
1151
|
+
"## Conventions Established",
|
|
1152
|
+
conventions,
|
|
1153
|
+
"",
|
|
1154
|
+
"## Lessons Learned",
|
|
1155
|
+
lessons,
|
|
1156
|
+
"",
|
|
1157
|
+
"## Open Questions",
|
|
1158
|
+
openQuestions,
|
|
1159
|
+
"",
|
|
1160
|
+
"## Stats",
|
|
1161
|
+
`- Sessions: ${compacted.sourceTrajectories.length}, Agents: ${agents}, Files: ${compacted.filesAffected.length}, Commits: ${compacted.commits.length}`,
|
|
1162
|
+
`- Date range: ${compacted.dateRange.start} - ${compacted.dateRange.end}`
|
|
1163
|
+
].join("\n");
|
|
1164
|
+
}
|
|
1165
|
+
function formatDate2(value) {
|
|
1166
|
+
const date = new Date(value);
|
|
1167
|
+
if (Number.isNaN(date.getTime())) {
|
|
1168
|
+
return value;
|
|
1169
|
+
}
|
|
1170
|
+
return date.toISOString().slice(0, 10);
|
|
1171
|
+
}
|
|
1172
|
+
function escapeTableCell(value) {
|
|
1173
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/compact/parser.ts
|
|
1177
|
+
function parseCompactionResponse(llmOutput) {
|
|
1178
|
+
const trimmed = llmOutput.trim();
|
|
1179
|
+
const parsedJson = parseJsonCandidate(trimmed) ?? parseJsonCandidate(extractFirstMarkdownJsonBlock(trimmed)) ?? parseJsonCandidate(extractBalancedJsonObject(trimmed));
|
|
1180
|
+
if (parsedJson) {
|
|
1181
|
+
return normalizeCompactionOutput(parsedJson, trimmed);
|
|
1182
|
+
}
|
|
1183
|
+
return normalizeCompactionOutput(extractFromProse(trimmed), trimmed);
|
|
1184
|
+
}
|
|
1185
|
+
function mergeCompactionWithMetadata(metadata, llmOutput) {
|
|
1186
|
+
return {
|
|
1187
|
+
...metadata,
|
|
1188
|
+
...llmOutput
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function parseJsonCandidate(candidate) {
|
|
1192
|
+
if (!candidate) {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
return JSON.parse(candidate);
|
|
1197
|
+
} catch {
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function extractFirstMarkdownJsonBlock(text) {
|
|
1202
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1203
|
+
return match ? match[1].trim() : null;
|
|
1204
|
+
}
|
|
1205
|
+
function extractBalancedJsonObject(text) {
|
|
1206
|
+
const start = text.indexOf("{");
|
|
1207
|
+
if (start === -1) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
let depth = 0;
|
|
1211
|
+
let inString = false;
|
|
1212
|
+
let escaped = false;
|
|
1213
|
+
for (let index = start; index < text.length; index += 1) {
|
|
1214
|
+
const char = text[index];
|
|
1215
|
+
if (escaped) {
|
|
1216
|
+
escaped = false;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (char === "\\") {
|
|
1220
|
+
escaped = true;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (char === '"') {
|
|
1224
|
+
inString = !inString;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (inString) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (char === "{") {
|
|
1231
|
+
depth += 1;
|
|
1232
|
+
} else if (char === "}") {
|
|
1233
|
+
depth -= 1;
|
|
1234
|
+
if (depth === 0) {
|
|
1235
|
+
return text.slice(start, index + 1);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
function extractFromProse(text) {
|
|
1242
|
+
const sections = splitSections(text);
|
|
1243
|
+
const narrativeSection = sections.narrative ?? sections.summary ?? leadingNarrative(text);
|
|
1244
|
+
return {
|
|
1245
|
+
narrative: normalizeText(narrativeSection),
|
|
1246
|
+
decisions: parseDecisionSection(
|
|
1247
|
+
sections["key decisions"] ?? sections.decisions ?? ""
|
|
1248
|
+
),
|
|
1249
|
+
conventions: parseConventionSection(
|
|
1250
|
+
sections["conventions established"] ?? sections.conventions ?? ""
|
|
1251
|
+
),
|
|
1252
|
+
lessons: parseLessonSection(
|
|
1253
|
+
sections["lessons learned"] ?? sections.lessons ?? ""
|
|
1254
|
+
),
|
|
1255
|
+
openQuestions: parseStringList(
|
|
1256
|
+
sections["open questions"] ?? sections.questions ?? ""
|
|
1257
|
+
)
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function splitSections(text) {
|
|
1261
|
+
const matches = [...text.matchAll(/^##+\s+(.+?)\s*$/gm)];
|
|
1262
|
+
const sections = {};
|
|
1263
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
1264
|
+
const current = matches[index];
|
|
1265
|
+
const next = matches[index + 1];
|
|
1266
|
+
const title = normalizeHeading(current[1]);
|
|
1267
|
+
const start = current.index === void 0 ? 0 : current.index + current[0].length;
|
|
1268
|
+
const end = next?.index ?? text.length;
|
|
1269
|
+
sections[title] = text.slice(start, end).trim();
|
|
1270
|
+
}
|
|
1271
|
+
return sections;
|
|
1272
|
+
}
|
|
1273
|
+
function normalizeHeading(value) {
|
|
1274
|
+
return value.toLowerCase().replace(/\(\d+\)/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
1275
|
+
}
|
|
1276
|
+
function leadingNarrative(text) {
|
|
1277
|
+
const beforeHeading = text.split(/^##+\s+/m, 1)[0] ?? "";
|
|
1278
|
+
const withoutCode = beforeHeading.replace(/```[\s\S]*?```/g, "").trim();
|
|
1279
|
+
return withoutCode;
|
|
1280
|
+
}
|
|
1281
|
+
function normalizeCompactionOutput(raw, fallbackNarrativeSource) {
|
|
1282
|
+
const candidate = isRecord2(raw) ? raw : {};
|
|
1283
|
+
const narrative = normalizeText(
|
|
1284
|
+
typeof candidate.narrative === "string" ? candidate.narrative : typeof candidate.summary === "string" ? candidate.summary : typeof candidate.overview === "string" ? candidate.overview : leadingNarrative(fallbackNarrativeSource)
|
|
1285
|
+
);
|
|
1286
|
+
return {
|
|
1287
|
+
narrative: narrative || normalizeText(fallbackNarrativeSource) || "No narrative provided.",
|
|
1288
|
+
decisions: normalizeDecisionArray(candidate.decisions),
|
|
1289
|
+
conventions: normalizeConventionArray(candidate.conventions),
|
|
1290
|
+
lessons: normalizeLessonArray(candidate.lessons),
|
|
1291
|
+
openQuestions: normalizeStringArray(
|
|
1292
|
+
candidate.openQuestions ?? candidate.open_questions ?? candidate.questions
|
|
1293
|
+
)
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function normalizeDecisionArray(value) {
|
|
1297
|
+
if (!Array.isArray(value)) {
|
|
1298
|
+
return [];
|
|
1299
|
+
}
|
|
1300
|
+
return value.map((entry) => {
|
|
1301
|
+
if (typeof entry === "string") {
|
|
1302
|
+
return {
|
|
1303
|
+
question: normalizeText(entry),
|
|
1304
|
+
chosen: "",
|
|
1305
|
+
reasoning: "",
|
|
1306
|
+
impact: ""
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
if (!isRecord2(entry)) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
question: readString2(entry, ["question", "prompt", "topic"]),
|
|
1314
|
+
chosen: readString2(entry, ["chosen", "decision", "answer"]),
|
|
1315
|
+
reasoning: readString2(entry, ["reasoning", "why", "rationale"]),
|
|
1316
|
+
impact: readString2(entry, ["impact", "result", "outcome"])
|
|
1317
|
+
};
|
|
1318
|
+
}).filter((entry) => {
|
|
1319
|
+
return entry !== null && hasContent(Object.values(entry));
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
function normalizeConventionArray(value) {
|
|
1323
|
+
if (!Array.isArray(value)) {
|
|
1324
|
+
return [];
|
|
1325
|
+
}
|
|
1326
|
+
return value.map((entry) => {
|
|
1327
|
+
if (typeof entry === "string") {
|
|
1328
|
+
return {
|
|
1329
|
+
pattern: normalizeText(entry),
|
|
1330
|
+
rationale: "",
|
|
1331
|
+
scope: ""
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
if (!isRecord2(entry)) {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
pattern: readString2(entry, ["pattern", "rule", "convention"]),
|
|
1339
|
+
rationale: readString2(entry, ["rationale", "reasoning", "why"]),
|
|
1340
|
+
scope: readString2(entry, ["scope", "appliesTo", "applies_to"])
|
|
1341
|
+
};
|
|
1342
|
+
}).filter((entry) => {
|
|
1343
|
+
return entry !== null && hasContent(Object.values(entry));
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
function normalizeLessonArray(value) {
|
|
1347
|
+
if (!Array.isArray(value)) {
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
return value.map((entry) => {
|
|
1351
|
+
if (typeof entry === "string") {
|
|
1352
|
+
return {
|
|
1353
|
+
lesson: normalizeText(entry),
|
|
1354
|
+
context: "",
|
|
1355
|
+
recommendation: ""
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
if (!isRecord2(entry)) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
return {
|
|
1362
|
+
lesson: readString2(entry, ["lesson", "learning", "takeaway"]),
|
|
1363
|
+
context: readString2(entry, ["context", "situation", "when"]),
|
|
1364
|
+
recommendation: readString2(entry, [
|
|
1365
|
+
"recommendation",
|
|
1366
|
+
"suggestion",
|
|
1367
|
+
"nextStep",
|
|
1368
|
+
"next_step"
|
|
1369
|
+
])
|
|
1370
|
+
};
|
|
1371
|
+
}).filter((entry) => {
|
|
1372
|
+
return entry !== null && hasContent(Object.values(entry));
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
function normalizeStringArray(value) {
|
|
1376
|
+
if (!Array.isArray(value)) {
|
|
1377
|
+
return [];
|
|
1378
|
+
}
|
|
1379
|
+
return value.map((entry) => typeof entry === "string" ? normalizeText(entry) : "").filter(Boolean);
|
|
1380
|
+
}
|
|
1381
|
+
function parseDecisionSection(section) {
|
|
1382
|
+
const tableDecisions = parseMarkdownTable(section).map((row) => ({
|
|
1383
|
+
question: row[0] ?? "",
|
|
1384
|
+
chosen: row[1] ?? "",
|
|
1385
|
+
reasoning: row[2] ?? "",
|
|
1386
|
+
impact: row[3] ?? row[2] ?? ""
|
|
1387
|
+
}));
|
|
1388
|
+
if (tableDecisions.length > 0) {
|
|
1389
|
+
return tableDecisions.filter((entry) => hasContent(Object.values(entry)));
|
|
1390
|
+
}
|
|
1391
|
+
return parseListItems(section).map((item) => {
|
|
1392
|
+
const fields = parseFieldMap(item);
|
|
1393
|
+
return {
|
|
1394
|
+
question: fields.question ?? fields.prompt ?? fields.topic ?? fields.title ?? item,
|
|
1395
|
+
chosen: fields.chosen ?? fields.decision ?? fields.answer ?? "",
|
|
1396
|
+
reasoning: fields.reasoning ?? fields.rationale ?? fields.why ?? "",
|
|
1397
|
+
impact: fields.impact ?? fields.outcome ?? fields.result ?? ""
|
|
1398
|
+
};
|
|
1399
|
+
}).filter((entry) => hasContent(Object.values(entry)));
|
|
1400
|
+
}
|
|
1401
|
+
function parseConventionSection(section) {
|
|
1402
|
+
return parseListItems(section).map((item) => {
|
|
1403
|
+
const emphasized = item.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
|
|
1404
|
+
const scopeMatch = item.match(/\((?:scope|applies to):\s*([^)]+)\)\s*$/i);
|
|
1405
|
+
const withoutScope = scopeMatch ? item.slice(0, scopeMatch.index).trim() : item;
|
|
1406
|
+
if (emphasized) {
|
|
1407
|
+
return {
|
|
1408
|
+
pattern: normalizeText(emphasized[1]),
|
|
1409
|
+
rationale: normalizeText(
|
|
1410
|
+
withoutScope.replace(/^\*\*(.+?)\*\*:\s*/, "")
|
|
1411
|
+
),
|
|
1412
|
+
scope: normalizeText(scopeMatch?.[1] ?? "")
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
const fields = parseFieldMap(item);
|
|
1416
|
+
return {
|
|
1417
|
+
pattern: fields.pattern ?? fields.convention ?? fields.rule ?? item,
|
|
1418
|
+
rationale: fields.rationale ?? fields.reasoning ?? fields.why ?? "",
|
|
1419
|
+
scope: fields.scope ?? fields.applies ?? ""
|
|
1420
|
+
};
|
|
1421
|
+
}).filter((entry) => hasContent(Object.values(entry)));
|
|
1422
|
+
}
|
|
1423
|
+
function parseLessonSection(section) {
|
|
1424
|
+
return parseListItems(section).map((item) => {
|
|
1425
|
+
const fields = parseFieldMap(item);
|
|
1426
|
+
const dashParts = item.split(/\s[—-]\s/, 2);
|
|
1427
|
+
return {
|
|
1428
|
+
lesson: fields.lesson ?? fields.learning ?? fields.takeaway ?? dashParts[0] ?? item,
|
|
1429
|
+
context: fields.context ?? "",
|
|
1430
|
+
recommendation: fields.recommendation ?? fields.suggestion ?? fields.nextstep ?? dashParts[1] ?? ""
|
|
1431
|
+
};
|
|
1432
|
+
}).filter((entry) => hasContent(Object.values(entry)));
|
|
1433
|
+
}
|
|
1434
|
+
function parseStringList(section) {
|
|
1435
|
+
return parseListItems(section).map(normalizeText).filter(Boolean);
|
|
1436
|
+
}
|
|
1437
|
+
function parseMarkdownTable(section) {
|
|
1438
|
+
const lines = section.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
1439
|
+
if (lines.length < 2) {
|
|
1440
|
+
return [];
|
|
1441
|
+
}
|
|
1442
|
+
return lines.slice(1).filter((line) => !/^\|?\s*:?-{3,}/.test(line.replace(/\|/g, ""))).map(
|
|
1443
|
+
(line) => line.split("|").slice(1, -1).map((cell) => normalizeText(cell))
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
function parseListItems(section) {
|
|
1447
|
+
return section.split("\n").map((line) => line.trim()).filter((line) => /^[-*] |\d+\.\s/.test(line)).map((line) => line.replace(/^[-*]\s+|\d+\.\s+/, "").trim()).filter(Boolean);
|
|
1448
|
+
}
|
|
1449
|
+
function parseFieldMap(item) {
|
|
1450
|
+
const normalized = item.replace(/\s+\|\s+/g, "; ");
|
|
1451
|
+
const segments = normalized.split(/;\s+/);
|
|
1452
|
+
const fields = {};
|
|
1453
|
+
for (const segment of segments) {
|
|
1454
|
+
const match = segment.match(/^([A-Za-z ]+):\s*(.+)$/);
|
|
1455
|
+
if (!match) {
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
const key = match[1].toLowerCase().replace(/\s+/g, "");
|
|
1459
|
+
fields[key] = normalizeText(match[2]);
|
|
1460
|
+
}
|
|
1461
|
+
return fields;
|
|
1462
|
+
}
|
|
1463
|
+
function readString2(record, keys) {
|
|
1464
|
+
for (const key of keys) {
|
|
1465
|
+
const value = record[key];
|
|
1466
|
+
if (typeof value === "string") {
|
|
1467
|
+
return normalizeText(value);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return "";
|
|
1471
|
+
}
|
|
1472
|
+
function isRecord2(value) {
|
|
1473
|
+
return typeof value === "object" && value !== null;
|
|
1474
|
+
}
|
|
1475
|
+
function normalizeText(value) {
|
|
1476
|
+
return value.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
1477
|
+
}
|
|
1478
|
+
function hasContent(values) {
|
|
1479
|
+
return values.some((value) => value.trim().length > 0);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/compact/prompts.ts
|
|
1483
|
+
var COMPACTION_SYSTEM_PROMPT = `You are a technical analyst reviewing agent work sessions (trajectories).
|
|
1484
|
+
Your job is to produce a concise, insightful summary that captures:
|
|
1485
|
+
- What was accomplished and how
|
|
1486
|
+
- Key decisions and their reasoning
|
|
1487
|
+
- Patterns/conventions established that should be followed in future work
|
|
1488
|
+
- Lessons learned from challenges and failures
|
|
1489
|
+
- Open questions or unresolved issues
|
|
1490
|
+
|
|
1491
|
+
Be specific. Reference actual file paths, function names, and technical details.
|
|
1492
|
+
Don't be generic - this summary replaces the raw data.`;
|
|
1493
|
+
var COMPACTED_OUTPUT_SCHEMA = `{
|
|
1494
|
+
"narrative": "string",
|
|
1495
|
+
"decisions": [
|
|
1496
|
+
{
|
|
1497
|
+
"question": "string",
|
|
1498
|
+
"chosen": "string",
|
|
1499
|
+
"reasoning": "string",
|
|
1500
|
+
"impact": "string"
|
|
1501
|
+
}
|
|
1502
|
+
],
|
|
1503
|
+
"conventions": [
|
|
1504
|
+
{
|
|
1505
|
+
"pattern": "string",
|
|
1506
|
+
"rationale": "string",
|
|
1507
|
+
"scope": "string"
|
|
1508
|
+
}
|
|
1509
|
+
],
|
|
1510
|
+
"lessons": [
|
|
1511
|
+
{
|
|
1512
|
+
"lesson": "string",
|
|
1513
|
+
"context": "string",
|
|
1514
|
+
"recommendation": "string"
|
|
1515
|
+
}
|
|
1516
|
+
],
|
|
1517
|
+
"openQuestions": ["string"]
|
|
1518
|
+
}`;
|
|
1519
|
+
function buildCompactionPrompt(serializedTrajectories, options = {}) {
|
|
1520
|
+
const focusAreas = options.focusAreas && options.focusAreas.length > 0 ? options.focusAreas.map((area) => `- ${area}`).join("\n") : [
|
|
1521
|
+
"- What work was attempted, completed, or abandoned",
|
|
1522
|
+
"- Why specific technical decisions were made",
|
|
1523
|
+
"- Which conventions should carry forward",
|
|
1524
|
+
"- What broke, what worked, and what should change next time"
|
|
1525
|
+
].join("\n");
|
|
1526
|
+
const maxOutputInstruction = options.maxOutputTokens ? `Keep the full response within approximately ${options.maxOutputTokens} tokens while preserving technical specificity.` : "Keep the response concise, dense with signal, and avoid filler.";
|
|
1527
|
+
const userPrompt = [
|
|
1528
|
+
"Review the following serialized agent trajectories and return a single JSON object.",
|
|
1529
|
+
"The JSON must match this schema exactly:",
|
|
1530
|
+
COMPACTED_OUTPUT_SCHEMA,
|
|
1531
|
+
"",
|
|
1532
|
+
"Requirements:",
|
|
1533
|
+
"- Output raw JSON only. Do not wrap it in markdown fences.",
|
|
1534
|
+
"- `narrative` should be 2-3 tight paragraphs.",
|
|
1535
|
+
"- `decisions`, `conventions`, and `lessons` must always be arrays, even if empty.",
|
|
1536
|
+
"- Prefer concrete file paths, symbols, commands, and implementation details over generic summaries.",
|
|
1537
|
+
maxOutputInstruction,
|
|
1538
|
+
"",
|
|
1539
|
+
"Focus areas:",
|
|
1540
|
+
focusAreas,
|
|
1541
|
+
"",
|
|
1542
|
+
"Serialized trajectories:",
|
|
1543
|
+
serializedTrajectories.trim()
|
|
1544
|
+
].join("\n");
|
|
1545
|
+
return [
|
|
1546
|
+
{
|
|
1547
|
+
role: "system",
|
|
1548
|
+
content: COMPACTION_SYSTEM_PROMPT
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
role: "user",
|
|
1552
|
+
content: userPrompt
|
|
1553
|
+
}
|
|
1554
|
+
];
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/compact/provider.ts
|
|
1558
|
+
import { execFile, spawn } from "child_process";
|
|
1559
|
+
import { constants, accessSync } from "fs";
|
|
1560
|
+
import { homedir } from "os";
|
|
1561
|
+
import { join as join3 } from "path";
|
|
1562
|
+
import { promisify } from "util";
|
|
1563
|
+
var execFileAsync = promisify(execFile);
|
|
1564
|
+
var DEFAULT_OPENAI_MODEL = "gpt-4o";
|
|
1565
|
+
var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
|
|
1566
|
+
var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
1567
|
+
var DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
1568
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
1569
|
+
var OpenAIProvider = class {
|
|
1570
|
+
apiKey;
|
|
1571
|
+
model;
|
|
1572
|
+
baseUrl;
|
|
1573
|
+
constructor(config = {}) {
|
|
1574
|
+
this.apiKey = config.apiKey?.trim() || process.env.OPENAI_API_KEY?.trim() || "";
|
|
1575
|
+
this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_OPENAI_MODEL;
|
|
1576
|
+
this.baseUrl = config.baseUrl ?? process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL;
|
|
1577
|
+
if (this.baseUrl !== DEFAULT_OPENAI_BASE_URL) {
|
|
1578
|
+
console.warn(
|
|
1579
|
+
`[trajectories] OpenAI base URL overridden to: ${this.baseUrl}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
if (!this.apiKey) {
|
|
1583
|
+
throw new Error("OPENAI_API_KEY is required for OpenAIProvider");
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
async complete(messages, options = {}) {
|
|
1587
|
+
const controller = new AbortController();
|
|
1588
|
+
const timeout = setTimeout(() => controller.abort(), 3e5);
|
|
1589
|
+
try {
|
|
1590
|
+
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
1591
|
+
method: "POST",
|
|
1592
|
+
headers: {
|
|
1593
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1594
|
+
"Content-Type": "application/json"
|
|
1595
|
+
},
|
|
1596
|
+
body: JSON.stringify({
|
|
1597
|
+
model: this.model,
|
|
1598
|
+
messages,
|
|
1599
|
+
max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
1600
|
+
temperature: options.temperature ?? 0.2,
|
|
1601
|
+
response_format: options.jsonMode ? { type: "json_object" } : void 0
|
|
1602
|
+
}),
|
|
1603
|
+
signal: controller.signal
|
|
1604
|
+
});
|
|
1605
|
+
const body = await parseJson(response);
|
|
1606
|
+
if (!response.ok) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
body.error?.message ?? `OpenAI request failed with status ${response.status}`
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
const content = body.choices?.[0]?.message?.content;
|
|
1612
|
+
if (!content) {
|
|
1613
|
+
throw new Error("OpenAI response did not include completion content");
|
|
1614
|
+
}
|
|
1615
|
+
return content;
|
|
1616
|
+
} finally {
|
|
1617
|
+
clearTimeout(timeout);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
var AnthropicProvider = class {
|
|
1622
|
+
apiKey;
|
|
1623
|
+
model;
|
|
1624
|
+
baseUrl;
|
|
1625
|
+
constructor(config = {}) {
|
|
1626
|
+
this.apiKey = config.apiKey?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || "";
|
|
1627
|
+
this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_ANTHROPIC_MODEL;
|
|
1628
|
+
this.baseUrl = config.baseUrl ?? process.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL;
|
|
1629
|
+
if (this.baseUrl !== DEFAULT_ANTHROPIC_BASE_URL) {
|
|
1630
|
+
console.warn(
|
|
1631
|
+
`[trajectories] Anthropic base URL overridden to: ${this.baseUrl}`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (!this.apiKey) {
|
|
1635
|
+
throw new Error("ANTHROPIC_API_KEY is required for AnthropicProvider");
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async complete(messages, options = {}) {
|
|
1639
|
+
const systemMessages = messages.filter((message) => message.role === "system").map((message) => message.content.trim()).filter(Boolean);
|
|
1640
|
+
const conversation = messages.filter((message) => message.role !== "system").map((message) => ({
|
|
1641
|
+
role: message.role,
|
|
1642
|
+
content: message.content
|
|
1643
|
+
}));
|
|
1644
|
+
if (conversation.length === 0) {
|
|
1645
|
+
throw new Error("AnthropicProvider requires at least one user message");
|
|
1646
|
+
}
|
|
1647
|
+
const controller = new AbortController();
|
|
1648
|
+
const timeout = setTimeout(() => controller.abort(), 3e5);
|
|
1649
|
+
try {
|
|
1650
|
+
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
headers: {
|
|
1653
|
+
"Content-Type": "application/json",
|
|
1654
|
+
"anthropic-version": "2024-10-22",
|
|
1655
|
+
"x-api-key": this.apiKey
|
|
1656
|
+
},
|
|
1657
|
+
body: JSON.stringify({
|
|
1658
|
+
model: this.model,
|
|
1659
|
+
system: systemMessages.length > 0 ? systemMessages.join("\n\n") : void 0,
|
|
1660
|
+
messages: conversation,
|
|
1661
|
+
max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
1662
|
+
temperature: options.temperature ?? 0.2
|
|
1663
|
+
}),
|
|
1664
|
+
signal: controller.signal
|
|
1665
|
+
});
|
|
1666
|
+
const body = await parseJson(response);
|
|
1667
|
+
if (!response.ok) {
|
|
1668
|
+
throw new Error(
|
|
1669
|
+
body.error?.message ?? `Anthropic request failed with status ${response.status}`
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
const textBlocks = (body.content ?? []).filter(
|
|
1673
|
+
(block) => block.type === "text" && typeof block.text === "string"
|
|
1674
|
+
);
|
|
1675
|
+
const content = textBlocks.map((block) => block.text).join("\n").trim();
|
|
1676
|
+
if (!content) {
|
|
1677
|
+
throw new Error("Anthropic response did not include text content");
|
|
1678
|
+
}
|
|
1679
|
+
return content;
|
|
1680
|
+
} finally {
|
|
1681
|
+
clearTimeout(timeout);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
var SUPPORTED_CLIS = ["claude", "codex", "gemini", "opencode"];
|
|
1686
|
+
var CLIProvider = class {
|
|
1687
|
+
cli;
|
|
1688
|
+
binaryPath;
|
|
1689
|
+
constructor(cli, binaryPath) {
|
|
1690
|
+
this.cli = cli;
|
|
1691
|
+
this.binaryPath = binaryPath;
|
|
1692
|
+
}
|
|
1693
|
+
get cliName() {
|
|
1694
|
+
return this.cli;
|
|
1695
|
+
}
|
|
1696
|
+
async complete(messages, _options = {}) {
|
|
1697
|
+
const prompt = messagesToPrompt(messages);
|
|
1698
|
+
const args = buildCliArgs(this.cli);
|
|
1699
|
+
const output = await spawnWithStdin(this.binaryPath, args, prompt);
|
|
1700
|
+
if (!output) {
|
|
1701
|
+
throw new Error(`${this.cli} CLI returned empty output`);
|
|
1702
|
+
}
|
|
1703
|
+
return output;
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
function messagesToPrompt(messages) {
|
|
1707
|
+
const systemParts = [];
|
|
1708
|
+
const conversationParts = [];
|
|
1709
|
+
for (const msg of messages) {
|
|
1710
|
+
if (msg.role === "system") {
|
|
1711
|
+
systemParts.push(msg.content.trim());
|
|
1712
|
+
} else {
|
|
1713
|
+
conversationParts.push(msg.content.trim());
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const parts = [];
|
|
1717
|
+
if (systemParts.length > 0) {
|
|
1718
|
+
parts.push(systemParts.join("\n\n"));
|
|
1719
|
+
}
|
|
1720
|
+
if (conversationParts.length > 0) {
|
|
1721
|
+
parts.push(conversationParts.join("\n\n"));
|
|
1722
|
+
}
|
|
1723
|
+
return parts.join("\n\n---\n\n");
|
|
1724
|
+
}
|
|
1725
|
+
function buildCliArgs(cli) {
|
|
1726
|
+
switch (cli) {
|
|
1727
|
+
case "claude":
|
|
1728
|
+
return ["-p", "--output-format", "text"];
|
|
1729
|
+
case "codex":
|
|
1730
|
+
return ["exec", "-q"];
|
|
1731
|
+
case "gemini":
|
|
1732
|
+
return ["-p"];
|
|
1733
|
+
case "opencode":
|
|
1734
|
+
return ["run", "--no-color"];
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function spawnWithStdin(command, args, input) {
|
|
1738
|
+
return new Promise((resolve, reject) => {
|
|
1739
|
+
const child = spawn(command, args, {
|
|
1740
|
+
timeout: 3e5,
|
|
1741
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1742
|
+
});
|
|
1743
|
+
const chunks = [];
|
|
1744
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
1745
|
+
let stderr = "";
|
|
1746
|
+
child.stderr.on("data", (chunk) => {
|
|
1747
|
+
stderr += chunk.toString();
|
|
1748
|
+
});
|
|
1749
|
+
child.on("error", reject);
|
|
1750
|
+
child.on("close", (code) => {
|
|
1751
|
+
if (code !== 0) {
|
|
1752
|
+
reject(
|
|
1753
|
+
new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
|
|
1754
|
+
);
|
|
1755
|
+
} else {
|
|
1756
|
+
resolve(Buffer.concat(chunks).toString().trim());
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
child.stdin.write(input);
|
|
1760
|
+
child.stdin.end();
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
async function resolveProvider(config = {}) {
|
|
1764
|
+
const explicitProvider = (config.provider ?? process.env.TRAJECTORIES_LLM_PROVIDER)?.toLowerCase();
|
|
1765
|
+
const model = normalizeModel(config.model);
|
|
1766
|
+
if (explicitProvider === "openai") {
|
|
1767
|
+
return process.env.OPENAI_API_KEY ? new OpenAIProvider({ model }) : null;
|
|
1768
|
+
}
|
|
1769
|
+
if (explicitProvider === "anthropic") {
|
|
1770
|
+
return process.env.ANTHROPIC_API_KEY ? new AnthropicProvider({ model }) : null;
|
|
1771
|
+
}
|
|
1772
|
+
if (explicitProvider === "cli") {
|
|
1773
|
+
return resolveCLIProvider();
|
|
1774
|
+
}
|
|
1775
|
+
if (explicitProvider && explicitProvider !== "auto") {
|
|
1776
|
+
return null;
|
|
1777
|
+
}
|
|
1778
|
+
const cliProvider = await resolveCLIProvider();
|
|
1779
|
+
if (cliProvider) {
|
|
1780
|
+
return cliProvider;
|
|
1781
|
+
}
|
|
1782
|
+
if (process.env.OPENAI_API_KEY) {
|
|
1783
|
+
return new OpenAIProvider({ model });
|
|
1784
|
+
}
|
|
1785
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
1786
|
+
return new AnthropicProvider({ model });
|
|
1787
|
+
}
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
var CLI_SEARCH_PATHS = [
|
|
1791
|
+
"~/.local/bin",
|
|
1792
|
+
"~/.claude/local",
|
|
1793
|
+
"/usr/local/bin",
|
|
1794
|
+
"/opt/homebrew/bin"
|
|
1795
|
+
];
|
|
1796
|
+
async function resolveCLIProvider() {
|
|
1797
|
+
const requestedCli = process.env.TRAJECTORIES_LLM_CLI?.trim().toLowerCase();
|
|
1798
|
+
const clisToTry = (() => {
|
|
1799
|
+
if (!requestedCli) {
|
|
1800
|
+
return SUPPORTED_CLIS;
|
|
1801
|
+
}
|
|
1802
|
+
if (SUPPORTED_CLIS.includes(requestedCli)) {
|
|
1803
|
+
return [requestedCli];
|
|
1804
|
+
}
|
|
1805
|
+
console.warn(
|
|
1806
|
+
`[trajectories] Unsupported TRAJECTORIES_LLM_CLI value "${requestedCli}", falling back to auto-detect`
|
|
1807
|
+
);
|
|
1808
|
+
return SUPPORTED_CLIS;
|
|
1809
|
+
})();
|
|
1810
|
+
for (const cli of clisToTry) {
|
|
1811
|
+
const path2 = await findBinary(cli);
|
|
1812
|
+
if (path2) {
|
|
1813
|
+
return new CLIProvider(cli, path2);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
async function findBinary(name) {
|
|
1819
|
+
try {
|
|
1820
|
+
const { stdout } = await execFileAsync("which", [name]);
|
|
1821
|
+
const path2 = stdout.trim();
|
|
1822
|
+
if (path2) return path2;
|
|
1823
|
+
} catch {
|
|
1824
|
+
}
|
|
1825
|
+
const home = homedir();
|
|
1826
|
+
for (const dir of CLI_SEARCH_PATHS) {
|
|
1827
|
+
const expanded = dir.startsWith("~/") ? join3(home, dir.slice(2)) : dir;
|
|
1828
|
+
const candidate = join3(expanded, name);
|
|
1829
|
+
try {
|
|
1830
|
+
accessSync(candidate, constants.X_OK);
|
|
1831
|
+
return candidate;
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return void 0;
|
|
1836
|
+
}
|
|
1837
|
+
function normalizeModel(value) {
|
|
1838
|
+
if (typeof value !== "string") {
|
|
1839
|
+
return void 0;
|
|
1840
|
+
}
|
|
1841
|
+
const trimmed = value.trim();
|
|
1842
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1843
|
+
}
|
|
1844
|
+
async function parseJson(response) {
|
|
1845
|
+
const text = await response.text();
|
|
1846
|
+
if (!text) {
|
|
1847
|
+
return {};
|
|
1848
|
+
}
|
|
1849
|
+
try {
|
|
1850
|
+
return JSON.parse(text);
|
|
1851
|
+
} catch {
|
|
1852
|
+
throw new Error(
|
|
1853
|
+
`Invalid JSON response (status ${response.status}, length ${text.length})`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/compact/serializer.ts
|
|
1859
|
+
var DEFAULT_MAX_TOKENS2 = 3e4;
|
|
1860
|
+
var CHARS_PER_TOKEN = 4;
|
|
1861
|
+
var INCLUDED_SIGNIFICANCE = /* @__PURE__ */ new Set([
|
|
1862
|
+
"medium",
|
|
1863
|
+
"high",
|
|
1864
|
+
"critical"
|
|
1865
|
+
]);
|
|
1866
|
+
function serializeForLLM(trajectories, maxTokens = DEFAULT_MAX_TOKENS2) {
|
|
1867
|
+
const maxChars = Math.max(0, maxTokens * CHARS_PER_TOKEN);
|
|
1868
|
+
const sessions = trajectories.map(renderSession);
|
|
1869
|
+
let document = joinSessions(sessions);
|
|
1870
|
+
if (document.length <= maxChars || sessions.length === 0) {
|
|
1871
|
+
return document;
|
|
1872
|
+
}
|
|
1873
|
+
const fixedChars = sessions.reduce(
|
|
1874
|
+
(total, session) => total + session.header.length + session.agents.length + session.decisions.length + session.findings.length + session.retrospective.length + session.filesAndCommits.length,
|
|
1875
|
+
0
|
|
1876
|
+
);
|
|
1877
|
+
const chapterChars = sessions.reduce(
|
|
1878
|
+
(total, session) => total + session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
|
|
1879
|
+
0
|
|
1880
|
+
);
|
|
1881
|
+
const remainingChapterChars = maxChars - fixedChars;
|
|
1882
|
+
if (remainingChapterChars <= 0 || chapterChars === 0) {
|
|
1883
|
+
return truncateText(document, maxChars);
|
|
1884
|
+
}
|
|
1885
|
+
const ratio = Math.min(1, remainingChapterChars / chapterChars);
|
|
1886
|
+
const truncatedSessions = sessions.map((session) => ({
|
|
1887
|
+
...session,
|
|
1888
|
+
chapters: truncateChapters(
|
|
1889
|
+
session.chapters,
|
|
1890
|
+
session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
|
|
1891
|
+
ratio
|
|
1892
|
+
)
|
|
1893
|
+
}));
|
|
1894
|
+
document = joinSessions(truncatedSessions);
|
|
1895
|
+
return document.length <= maxChars ? document : truncateText(document, maxChars);
|
|
1896
|
+
}
|
|
1897
|
+
function renderSession(trajectory) {
|
|
1898
|
+
const sessionTitle = trajectory.task.title.trim() || trajectory.id;
|
|
1899
|
+
const duration = formatDuration(trajectory.startedAt, trajectory.completedAt);
|
|
1900
|
+
const header = [
|
|
1901
|
+
`## Session: ${sessionTitle} (${trajectory.status}, ${duration})`,
|
|
1902
|
+
trajectory.task.description ? `Description: ${trajectory.task.description}` : "",
|
|
1903
|
+
`Started: ${trajectory.startedAt}`,
|
|
1904
|
+
trajectory.completedAt ? `Completed: ${trajectory.completedAt}` : ""
|
|
1905
|
+
].filter(Boolean).join("\n").concat("\n");
|
|
1906
|
+
const agents = trajectory.agents.length > 0 ? `Agents: ${trajectory.agents.map((agent) => `${agent.name} (${agent.role})`).join(", ")}
|
|
1907
|
+
` : "Agents: none recorded\n";
|
|
1908
|
+
const chapters = trajectory.chapters.map(renderChapter);
|
|
1909
|
+
const decisions = renderDecisions(trajectory);
|
|
1910
|
+
const findings = renderFindings(trajectory);
|
|
1911
|
+
const retrospective = renderRetrospective(trajectory.retrospective);
|
|
1912
|
+
const filesAndCommits = [
|
|
1913
|
+
`Files changed: ${formatList(trajectory.filesChanged)}`,
|
|
1914
|
+
`Commits: ${formatList(trajectory.commits)}`
|
|
1915
|
+
].join("\n").concat("\n");
|
|
1916
|
+
return {
|
|
1917
|
+
header,
|
|
1918
|
+
agents,
|
|
1919
|
+
chapters,
|
|
1920
|
+
decisions,
|
|
1921
|
+
findings,
|
|
1922
|
+
retrospective,
|
|
1923
|
+
filesAndCommits
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function renderChapter(chapter) {
|
|
1927
|
+
const lines = chapter.events.filter(shouldIncludeEvent).map((event) => formatEvent(event));
|
|
1928
|
+
const chapterBody = lines.length > 0 ? lines.map((line) => `- ${line}`).join("\n") : "- No medium/high/critical events captured";
|
|
1929
|
+
return [
|
|
1930
|
+
`### Chapter: ${chapter.title}`,
|
|
1931
|
+
`Agent: ${chapter.agentName}`,
|
|
1932
|
+
`Window: ${chapter.startedAt} -> ${chapter.endedAt ?? "ongoing"}`,
|
|
1933
|
+
chapterBody
|
|
1934
|
+
].join("\n").concat("\n");
|
|
1935
|
+
}
|
|
1936
|
+
function renderDecisions(trajectory) {
|
|
1937
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1938
|
+
const decisions = [];
|
|
1939
|
+
for (const chapter of trajectory.chapters) {
|
|
1940
|
+
for (const event of chapter.events) {
|
|
1941
|
+
if (event.type !== "decision") {
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
const decision = asDecision(event.raw);
|
|
1945
|
+
if (!decision) {
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
const key = `${decision.question}
|
|
1949
|
+
${decision.chosen}
|
|
1950
|
+
${decision.reasoning}`;
|
|
1951
|
+
if (!seen.has(key)) {
|
|
1952
|
+
seen.add(key);
|
|
1953
|
+
decisions.push(decision);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
for (const decision of trajectory.retrospective?.decisions ?? []) {
|
|
1958
|
+
const key = `${decision.question}
|
|
1959
|
+
${decision.chosen}
|
|
1960
|
+
${decision.reasoning}`;
|
|
1961
|
+
if (!seen.has(key)) {
|
|
1962
|
+
seen.add(key);
|
|
1963
|
+
decisions.push(decision);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (decisions.length === 0) {
|
|
1967
|
+
return "Decisions:\n- None recorded\n";
|
|
1968
|
+
}
|
|
1969
|
+
return [
|
|
1970
|
+
"Decisions:",
|
|
1971
|
+
...decisions.map(
|
|
1972
|
+
(decision) => [
|
|
1973
|
+
`- Question: ${decision.question}`,
|
|
1974
|
+
` Chosen: ${decision.chosen}`,
|
|
1975
|
+
` Reasoning: ${decision.reasoning}`
|
|
1976
|
+
].join("\n")
|
|
1977
|
+
)
|
|
1978
|
+
].join("\n").concat("\n");
|
|
1979
|
+
}
|
|
1980
|
+
function renderFindings(trajectory) {
|
|
1981
|
+
const findings = trajectory.chapters.flatMap(
|
|
1982
|
+
(chapter) => chapter.events.filter((event) => event.type === "finding").map((event) => asFinding(event.raw, event.content))
|
|
1983
|
+
);
|
|
1984
|
+
if (findings.length === 0) {
|
|
1985
|
+
return "Findings:\n- None recorded\n";
|
|
1986
|
+
}
|
|
1987
|
+
return [
|
|
1988
|
+
"Findings:",
|
|
1989
|
+
...findings.map(
|
|
1990
|
+
(finding) => [
|
|
1991
|
+
`- What: ${finding.what}`,
|
|
1992
|
+
` Where: ${finding.where}`,
|
|
1993
|
+
` Significance: ${finding.significance}`
|
|
1994
|
+
].join("\n")
|
|
1995
|
+
)
|
|
1996
|
+
].join("\n").concat("\n");
|
|
1997
|
+
}
|
|
1998
|
+
function renderRetrospective(retrospective) {
|
|
1999
|
+
if (!retrospective) {
|
|
2000
|
+
return "Retrospective:\n- None recorded\n";
|
|
2001
|
+
}
|
|
2002
|
+
const lines = [
|
|
2003
|
+
"Retrospective:",
|
|
2004
|
+
`- Summary: ${retrospective.summary}`,
|
|
2005
|
+
` Approach: ${retrospective.approach}`
|
|
2006
|
+
];
|
|
2007
|
+
if (retrospective.challenges && retrospective.challenges.length > 0) {
|
|
2008
|
+
lines.push(` Challenges: ${retrospective.challenges.join("; ")}`);
|
|
2009
|
+
}
|
|
2010
|
+
if (retrospective.learnings && retrospective.learnings.length > 0) {
|
|
2011
|
+
lines.push(` Learnings: ${retrospective.learnings.join("; ")}`);
|
|
2012
|
+
}
|
|
2013
|
+
if (retrospective.suggestions && retrospective.suggestions.length > 0) {
|
|
2014
|
+
lines.push(` Suggestions: ${retrospective.suggestions.join("; ")}`);
|
|
2015
|
+
}
|
|
2016
|
+
if (retrospective.timeSpent) {
|
|
2017
|
+
lines.push(` Time spent: ${retrospective.timeSpent}`);
|
|
2018
|
+
}
|
|
2019
|
+
return lines.join("\n").concat("\n");
|
|
2020
|
+
}
|
|
2021
|
+
function shouldIncludeEvent(event) {
|
|
2022
|
+
if (event.type === "tool_call" || event.type === "tool_result") {
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
return INCLUDED_SIGNIFICANCE.has(resolveSignificance(event));
|
|
2026
|
+
}
|
|
2027
|
+
function resolveSignificance(event) {
|
|
2028
|
+
if (event.significance) {
|
|
2029
|
+
return event.significance;
|
|
2030
|
+
}
|
|
2031
|
+
switch (event.type) {
|
|
2032
|
+
case "decision":
|
|
2033
|
+
case "finding":
|
|
2034
|
+
case "error":
|
|
2035
|
+
return "high";
|
|
2036
|
+
case "reflection":
|
|
2037
|
+
case "note":
|
|
2038
|
+
case "message_sent":
|
|
2039
|
+
case "message_received":
|
|
2040
|
+
return "medium";
|
|
2041
|
+
default:
|
|
2042
|
+
return "low";
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function formatEvent(event) {
|
|
2046
|
+
if (event.type === "decision") {
|
|
2047
|
+
const decision = asDecision(event.raw);
|
|
2048
|
+
if (decision) {
|
|
2049
|
+
return `[decision/${resolveSignificance(event)}] ${decision.question} -> ${decision.chosen}`;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (event.type === "finding") {
|
|
2053
|
+
const finding = asFinding(event.raw, event.content);
|
|
2054
|
+
return `[finding/${resolveSignificance(event)}] ${finding.what} @ ${finding.where}`;
|
|
2055
|
+
}
|
|
2056
|
+
return `[${event.type}/${resolveSignificance(event)}] ${event.content}`;
|
|
2057
|
+
}
|
|
2058
|
+
function asDecision(raw) {
|
|
2059
|
+
if (!raw || typeof raw !== "object") {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
const candidate = raw;
|
|
2063
|
+
if (typeof candidate.question !== "string" || typeof candidate.chosen !== "string" || typeof candidate.reasoning !== "string") {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
return {
|
|
2067
|
+
question: candidate.question,
|
|
2068
|
+
chosen: candidate.chosen,
|
|
2069
|
+
reasoning: candidate.reasoning,
|
|
2070
|
+
alternatives: Array.isArray(candidate.alternatives) ? candidate.alternatives : [],
|
|
2071
|
+
confidence: candidate.confidence
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
function asFinding(raw, fallbackContent) {
|
|
2075
|
+
if (!raw || typeof raw !== "object") {
|
|
2076
|
+
return {
|
|
2077
|
+
what: fallbackContent,
|
|
2078
|
+
where: "unknown",
|
|
2079
|
+
significance: "Not structured",
|
|
2080
|
+
category: "other"
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
const candidate = raw;
|
|
2084
|
+
return {
|
|
2085
|
+
what: typeof candidate.what === "string" && candidate.what.trim().length > 0 ? candidate.what : fallbackContent,
|
|
2086
|
+
where: typeof candidate.where === "string" && candidate.where.trim().length > 0 ? candidate.where : "unknown",
|
|
2087
|
+
significance: typeof candidate.significance === "string" && candidate.significance.trim().length > 0 ? candidate.significance : "Not structured",
|
|
2088
|
+
category: candidate.category ?? "other",
|
|
2089
|
+
suggestedAction: typeof candidate.suggestedAction === "string" ? candidate.suggestedAction : void 0,
|
|
2090
|
+
confidence: candidate.confidence
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function truncateChapters(chapters, totalChapterChars, ratio) {
|
|
2094
|
+
if (ratio >= 1 || totalChapterChars === 0) {
|
|
2095
|
+
return chapters;
|
|
2096
|
+
}
|
|
2097
|
+
let remaining = Math.floor(totalChapterChars * ratio);
|
|
2098
|
+
return chapters.map((chapter, index) => {
|
|
2099
|
+
if (remaining <= 0) {
|
|
2100
|
+
return "### Chapter: Truncated\n- Omitted due to token budget\n";
|
|
2101
|
+
}
|
|
2102
|
+
const proportionalTarget = index === chapters.length - 1 ? remaining : Math.floor(chapter.length * ratio);
|
|
2103
|
+
const allowance = Math.max(0, Math.min(chapter.length, proportionalTarget));
|
|
2104
|
+
remaining -= allowance;
|
|
2105
|
+
return truncateText(chapter, allowance);
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
function joinSessions(sessions) {
|
|
2109
|
+
return sessions.map(
|
|
2110
|
+
(session) => [
|
|
2111
|
+
session.header,
|
|
2112
|
+
session.agents,
|
|
2113
|
+
...session.chapters,
|
|
2114
|
+
session.decisions,
|
|
2115
|
+
session.findings,
|
|
2116
|
+
session.retrospective,
|
|
2117
|
+
session.filesAndCommits
|
|
2118
|
+
].filter(Boolean).join("\n").trim()
|
|
2119
|
+
).join("\n\n");
|
|
2120
|
+
}
|
|
2121
|
+
function truncateText(text, maxChars) {
|
|
2122
|
+
if (maxChars <= 0) {
|
|
2123
|
+
return "";
|
|
2124
|
+
}
|
|
2125
|
+
if (text.length <= maxChars) {
|
|
2126
|
+
return text;
|
|
2127
|
+
}
|
|
2128
|
+
if (maxChars <= 16) {
|
|
2129
|
+
return text.slice(0, maxChars);
|
|
2130
|
+
}
|
|
2131
|
+
return `${text.slice(0, maxChars - 16).trimEnd()}
|
|
2132
|
+
[truncated]
|
|
2133
|
+
`;
|
|
2134
|
+
}
|
|
2135
|
+
function formatList(values) {
|
|
2136
|
+
return values.length > 0 ? values.join(", ") : "none";
|
|
2137
|
+
}
|
|
2138
|
+
function formatDuration(startedAt, completedAt) {
|
|
2139
|
+
const start = new Date(startedAt).getTime();
|
|
2140
|
+
const end = new Date(completedAt ?? startedAt).getTime();
|
|
2141
|
+
const elapsedMs = Math.max(0, end - start);
|
|
2142
|
+
const minutes = Math.floor(elapsedMs / 6e4);
|
|
2143
|
+
const hours = Math.floor(minutes / 60);
|
|
2144
|
+
const remainingMinutes = minutes % 60;
|
|
2145
|
+
if (hours > 0 && remainingMinutes > 0) {
|
|
2146
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
2147
|
+
}
|
|
2148
|
+
if (hours > 0) {
|
|
2149
|
+
return `${hours}h`;
|
|
2150
|
+
}
|
|
2151
|
+
if (minutes > 0) {
|
|
2152
|
+
return `${minutes}m`;
|
|
2153
|
+
}
|
|
2154
|
+
return completedAt ? "0m" : "ongoing";
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/cli/commands/compact.ts
|
|
886
2158
|
function registerCompactCommand(program2) {
|
|
887
2159
|
program2.command("compact").description(
|
|
888
2160
|
"Compact trajectories into a summarized form (default: uncompacted only)"
|
|
@@ -892,16 +2164,22 @@ function registerCompactCommand(program2) {
|
|
|
892
2164
|
).option(
|
|
893
2165
|
"--until <date>",
|
|
894
2166
|
"Include trajectories until this date (ISO format)"
|
|
895
|
-
).option("--ids <ids>", "Comma-separated list of trajectory IDs to compact").option(
|
|
2167
|
+
).option("--ids <ids>", "Comma-separated list of trajectory IDs to compact").option(
|
|
2168
|
+
"--workflow <id>",
|
|
2169
|
+
"Compact trajectories with the specified workflow ID"
|
|
2170
|
+
).option("--pr <number>", "Compact trajectories associated with a PR number").option(
|
|
896
2171
|
"--branch <name>",
|
|
897
2172
|
"Compact trajectories with commits not in the specified branch (e.g., main)"
|
|
898
2173
|
).option(
|
|
899
2174
|
"--commits <shas>",
|
|
900
2175
|
"Comma-separated commit SHAs to match trajectories against"
|
|
901
|
-
).option("--all", "Include all trajectories, even previously compacted ones").option("--
|
|
2176
|
+
).option("--all", "Include all trajectories, even previously compacted ones").option("--llm", "Use LLM-based compaction when a provider is available").option("--no-llm", "Disable LLM-based compaction").option("--mechanical", "Force the original mechanical compaction flow").option(
|
|
2177
|
+
"--focus <areas>",
|
|
2178
|
+
"Comma-separated focus areas to emphasize in LLM compaction"
|
|
2179
|
+
).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
|
|
902
2180
|
const trajectories = await loadTrajectories(options);
|
|
903
2181
|
if (trajectories.length === 0) {
|
|
904
|
-
if (options.all || options.since || options.ids || options.pr || options.branch || options.commits) {
|
|
2182
|
+
if (options.all || options.since || options.ids || options.workflow || options.pr || options.branch || options.commits) {
|
|
905
2183
|
console.log("No trajectories found matching criteria");
|
|
906
2184
|
} else {
|
|
907
2185
|
console.log(
|
|
@@ -912,17 +2190,92 @@ function registerCompactCommand(program2) {
|
|
|
912
2190
|
}
|
|
913
2191
|
console.log(`Compacting ${trajectories.length} trajectories...
|
|
914
2192
|
`);
|
|
915
|
-
const
|
|
2193
|
+
const config = getCompactionConfig();
|
|
2194
|
+
const provider = await resolveProvider(config);
|
|
2195
|
+
const useLLM = shouldUseLLM(options, provider !== null);
|
|
2196
|
+
const markdownEnabled = options.markdown !== false;
|
|
2197
|
+
const mechanicalCompacted = compactTrajectories(
|
|
2198
|
+
trajectories,
|
|
2199
|
+
options.workflow
|
|
2200
|
+
);
|
|
2201
|
+
if (!useLLM || provider === null) {
|
|
2202
|
+
if (options.llm && provider === null && !options.mechanical) {
|
|
2203
|
+
console.log(
|
|
2204
|
+
"No LLM provider detected; falling back to mechanical compaction.\n"
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
if (options.dryRun) {
|
|
2208
|
+
console.log("=== DRY RUN - Preview ===\n");
|
|
2209
|
+
printCompactedSummary(mechanicalCompacted);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
const outputPath2 = options.output || getDefaultOutputPath(mechanicalCompacted, options.workflow);
|
|
2213
|
+
saveCompactionArtifacts(
|
|
2214
|
+
mechanicalCompacted,
|
|
2215
|
+
outputPath2,
|
|
2216
|
+
markdownEnabled
|
|
2217
|
+
);
|
|
2218
|
+
await markTrajectoriesAsCompacted(trajectories, mechanicalCompacted.id);
|
|
2219
|
+
console.log(`
|
|
2220
|
+
Compacted trajectory saved to: ${outputPath2}`);
|
|
2221
|
+
if (markdownEnabled) {
|
|
2222
|
+
console.log(
|
|
2223
|
+
`Markdown summary saved to: ${getMarkdownOutputPath(outputPath2)}`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
printCompactedSummary(mechanicalCompacted);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const llmPlan = buildLLMCompactionPlan(
|
|
2230
|
+
trajectories,
|
|
2231
|
+
parseFocusAreas(options.focus),
|
|
2232
|
+
config.maxInputTokens,
|
|
2233
|
+
config.maxOutputTokens
|
|
2234
|
+
);
|
|
2235
|
+
console.log(
|
|
2236
|
+
`Using ${getProviderLabel(provider)} compaction${config.model ? ` with model ${config.model}` : ""}.`
|
|
2237
|
+
);
|
|
2238
|
+
console.log(
|
|
2239
|
+
`Estimated: ~${llmPlan.estimatedInputTokens} input tokens, ~${llmPlan.estimatedOutputTokens} output tokens`
|
|
2240
|
+
);
|
|
916
2241
|
if (options.dryRun) {
|
|
917
|
-
|
|
918
|
-
printCompactedSummary(compacted);
|
|
2242
|
+
printLLMDryRun(llmPlan, config.model, options.workflow);
|
|
919
2243
|
return;
|
|
920
2244
|
}
|
|
921
|
-
const
|
|
922
|
-
|
|
2245
|
+
const llmOutput = await provider.complete(llmPlan.messages, {
|
|
2246
|
+
maxTokens: config.maxOutputTokens,
|
|
2247
|
+
temperature: config.temperature,
|
|
2248
|
+
jsonMode: provider instanceof OpenAIProvider
|
|
2249
|
+
});
|
|
2250
|
+
const llmCompacted = parseCompactionResponse(llmOutput);
|
|
2251
|
+
const mergedCompaction = mergeCompactionWithMetadata(
|
|
2252
|
+
{
|
|
2253
|
+
id: mechanicalCompacted.id,
|
|
2254
|
+
version: mechanicalCompacted.version,
|
|
2255
|
+
type: mechanicalCompacted.type,
|
|
2256
|
+
compactedAt: mechanicalCompacted.compactedAt,
|
|
2257
|
+
sourceTrajectories: mechanicalCompacted.sourceTrajectories,
|
|
2258
|
+
dateRange: mechanicalCompacted.dateRange,
|
|
2259
|
+
summary: mechanicalCompacted.summary,
|
|
2260
|
+
filesAffected: mechanicalCompacted.filesAffected,
|
|
2261
|
+
commits: mechanicalCompacted.commits
|
|
2262
|
+
},
|
|
2263
|
+
llmCompacted
|
|
2264
|
+
);
|
|
2265
|
+
const compacted = {
|
|
2266
|
+
...mechanicalCompacted,
|
|
2267
|
+
...mergedCompaction
|
|
2268
|
+
};
|
|
2269
|
+
const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
|
|
2270
|
+
saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
|
|
923
2271
|
await markTrajectoriesAsCompacted(trajectories, compacted.id);
|
|
924
2272
|
console.log(`
|
|
925
2273
|
Compacted trajectory saved to: ${outputPath}`);
|
|
2274
|
+
if (markdownEnabled) {
|
|
2275
|
+
console.log(
|
|
2276
|
+
`Markdown summary saved to: ${getMarkdownOutputPath(outputPath)}`
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
926
2279
|
printCompactedSummary(compacted);
|
|
927
2280
|
});
|
|
928
2281
|
}
|
|
@@ -943,55 +2296,55 @@ async function loadTrajectories(options) {
|
|
|
943
2296
|
const searchPaths = getSearchPaths();
|
|
944
2297
|
const seenIds = /* @__PURE__ */ new Set();
|
|
945
2298
|
for (const searchPath of searchPaths) {
|
|
946
|
-
if (!
|
|
2299
|
+
if (!existsSync3(searchPath)) continue;
|
|
947
2300
|
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
948
2301
|
process.env.TRAJECTORIES_DATA_DIR = searchPath;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const matchesPR = prPattern.test(trajectory.task.title) || prPattern.test(trajectory.task.description || "") || trajectory.commits.some((c) => prPattern.test(c));
|
|
973
|
-
if (!matchesPR) continue;
|
|
974
|
-
}
|
|
975
|
-
if (branchCommits) {
|
|
976
|
-
const hasMatchingCommit = trajectory.commits.some(
|
|
977
|
-
(c) => branchCommits.has(c.slice(0, 7)) || branchCommits.has(c)
|
|
978
|
-
);
|
|
979
|
-
if (!hasMatchingCommit && trajectory.commits.length > 0) continue;
|
|
980
|
-
}
|
|
981
|
-
if (targetCommits) {
|
|
982
|
-
const hasMatchingCommit = trajectory.commits.some(
|
|
983
|
-
(c) => targetCommits.has(c) || targetCommits.has(c.slice(0, 7))
|
|
984
|
-
);
|
|
985
|
-
if (!hasMatchingCommit) continue;
|
|
986
|
-
}
|
|
987
|
-
trajectories.push(trajectory);
|
|
2302
|
+
const storage = new FileStorage();
|
|
2303
|
+
if (originalDataDir !== void 0) {
|
|
2304
|
+
process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
|
|
2305
|
+
} else {
|
|
2306
|
+
delete process.env.TRAJECTORIES_DATA_DIR;
|
|
2307
|
+
}
|
|
2308
|
+
await storage.initialize();
|
|
2309
|
+
const summaries = await storage.list({
|
|
2310
|
+
status: "completed",
|
|
2311
|
+
limit: Number.MAX_SAFE_INTEGER
|
|
2312
|
+
});
|
|
2313
|
+
for (const summary of summaries) {
|
|
2314
|
+
if (seenIds.has(summary.id)) continue;
|
|
2315
|
+
if (compactedIds.has(summary.id)) continue;
|
|
2316
|
+
if (targetIds && !targetIds.includes(summary.id)) continue;
|
|
2317
|
+
const startDate = new Date(summary.startedAt);
|
|
2318
|
+
if (sinceDate && startDate < sinceDate) continue;
|
|
2319
|
+
if (untilDate && startDate > untilDate) continue;
|
|
2320
|
+
const trajectory = await storage.get(summary.id);
|
|
2321
|
+
if (trajectory) {
|
|
2322
|
+
seenIds.add(summary.id);
|
|
2323
|
+
if (options.workflow && trajectory.workflowId !== options.workflow) {
|
|
2324
|
+
continue;
|
|
988
2325
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
2326
|
+
if (options.pr) {
|
|
2327
|
+
const escaped = options.pr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2328
|
+
const prPattern = new RegExp(
|
|
2329
|
+
`#${escaped}\\b|\\bPR\\s*#?\\s*${escaped}\\b`,
|
|
2330
|
+
"i"
|
|
2331
|
+
);
|
|
2332
|
+
const matchesPR = prPattern.test(trajectory.task.title) || prPattern.test(trajectory.task.description || "") || trajectory.commits.some((c) => prPattern.test(c));
|
|
2333
|
+
if (!matchesPR) continue;
|
|
2334
|
+
}
|
|
2335
|
+
if (branchCommits) {
|
|
2336
|
+
const hasMatchingCommit = trajectory.commits.some(
|
|
2337
|
+
(c) => branchCommits.has(c.slice(0, 7)) || branchCommits.has(c)
|
|
2338
|
+
);
|
|
2339
|
+
if (!hasMatchingCommit && trajectory.commits.length > 0) continue;
|
|
2340
|
+
}
|
|
2341
|
+
if (targetCommits) {
|
|
2342
|
+
const hasMatchingCommit = trajectory.commits.some(
|
|
2343
|
+
(c) => targetCommits.has(c) || targetCommits.has(c.slice(0, 7))
|
|
2344
|
+
);
|
|
2345
|
+
if (!hasMatchingCommit) continue;
|
|
2346
|
+
}
|
|
2347
|
+
trajectories.push(trajectory);
|
|
995
2348
|
}
|
|
996
2349
|
}
|
|
997
2350
|
}
|
|
@@ -1000,8 +2353,9 @@ async function loadTrajectories(options) {
|
|
|
1000
2353
|
function getBranchCommits(targetBranch) {
|
|
1001
2354
|
const commits = /* @__PURE__ */ new Set();
|
|
1002
2355
|
try {
|
|
1003
|
-
const output =
|
|
1004
|
-
|
|
2356
|
+
const output = execFileSync(
|
|
2357
|
+
"git",
|
|
2358
|
+
["log", `${targetBranch}..HEAD`, "--format=%H"],
|
|
1005
2359
|
{
|
|
1006
2360
|
encoding: "utf-8",
|
|
1007
2361
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1024,10 +2378,10 @@ function getCompactedTrajectoryIds() {
|
|
|
1024
2378
|
const compacted = /* @__PURE__ */ new Set();
|
|
1025
2379
|
const searchPaths = getSearchPaths();
|
|
1026
2380
|
for (const searchPath of searchPaths) {
|
|
1027
|
-
const indexPath =
|
|
1028
|
-
if (!
|
|
2381
|
+
const indexPath = join4(searchPath, "index.json");
|
|
2382
|
+
if (!existsSync3(indexPath)) continue;
|
|
1029
2383
|
try {
|
|
1030
|
-
const indexContent =
|
|
2384
|
+
const indexContent = readFileSync2(indexPath, "utf-8");
|
|
1031
2385
|
const index = JSON.parse(indexContent);
|
|
1032
2386
|
for (const [id, entry] of Object.entries(index.trajectories || {})) {
|
|
1033
2387
|
if (entry.compactedInto) {
|
|
@@ -1042,10 +2396,10 @@ function getCompactedTrajectoryIds() {
|
|
|
1042
2396
|
async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
|
|
1043
2397
|
const searchPaths = getSearchPaths();
|
|
1044
2398
|
for (const searchPath of searchPaths) {
|
|
1045
|
-
const indexPath =
|
|
1046
|
-
if (!
|
|
2399
|
+
const indexPath = join4(searchPath, "index.json");
|
|
2400
|
+
if (!existsSync3(indexPath)) continue;
|
|
1047
2401
|
try {
|
|
1048
|
-
const indexContent =
|
|
2402
|
+
const indexContent = readFileSync2(indexPath, "utf-8");
|
|
1049
2403
|
const index = JSON.parse(indexContent);
|
|
1050
2404
|
let updated = false;
|
|
1051
2405
|
for (const traj of trajectories) {
|
|
@@ -1081,7 +2435,7 @@ function parseRelativeDate(input) {
|
|
|
1081
2435
|
}
|
|
1082
2436
|
return new Date(input);
|
|
1083
2437
|
}
|
|
1084
|
-
function compactTrajectories(trajectories) {
|
|
2438
|
+
function compactTrajectories(trajectories, workflowId) {
|
|
1085
2439
|
const allDecisions = [];
|
|
1086
2440
|
const allLearnings = [];
|
|
1087
2441
|
const allFindings = [];
|
|
@@ -1144,6 +2498,7 @@ function compactTrajectories(trajectories) {
|
|
|
1144
2498
|
version: 1,
|
|
1145
2499
|
type: "compacted",
|
|
1146
2500
|
compactedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2501
|
+
workflowId,
|
|
1147
2502
|
sourceTrajectories: trajectories.map((t) => t.id),
|
|
1148
2503
|
dateRange: {
|
|
1149
2504
|
start: minDate.toISOString(),
|
|
@@ -1214,56 +2569,206 @@ function groupDecisions(decisions) {
|
|
|
1214
2569
|
(a, b) => b.decisions.length - a.decisions.length
|
|
1215
2570
|
);
|
|
1216
2571
|
}
|
|
1217
|
-
function
|
|
2572
|
+
function shouldUseLLM(options, providerAvailable) {
|
|
2573
|
+
if (options.mechanical) {
|
|
2574
|
+
return false;
|
|
2575
|
+
}
|
|
2576
|
+
if (options.llm === false) {
|
|
2577
|
+
return false;
|
|
2578
|
+
}
|
|
2579
|
+
if (options.llm === true) {
|
|
2580
|
+
return providerAvailable;
|
|
2581
|
+
}
|
|
2582
|
+
return providerAvailable;
|
|
2583
|
+
}
|
|
2584
|
+
function buildLLMCompactionPlan(trajectories, focusAreas, maxInputTokens, maxOutputTokens) {
|
|
2585
|
+
const serialized = serializeForLLM(trajectories, maxInputTokens);
|
|
2586
|
+
const messages = buildCompactionPrompt(serialized, {
|
|
2587
|
+
focusAreas,
|
|
2588
|
+
maxOutputTokens
|
|
2589
|
+
});
|
|
2590
|
+
return {
|
|
2591
|
+
messages,
|
|
2592
|
+
estimatedInputTokens: estimateTokens(
|
|
2593
|
+
messages.map((message) => message.content).join("\n\n")
|
|
2594
|
+
),
|
|
2595
|
+
estimatedOutputTokens: maxOutputTokens,
|
|
2596
|
+
focusAreas
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
function parseFocusAreas(focus) {
|
|
2600
|
+
if (!focus) {
|
|
2601
|
+
return [];
|
|
2602
|
+
}
|
|
2603
|
+
return focus.split(",").map((area) => area.trim()).filter(Boolean);
|
|
2604
|
+
}
|
|
2605
|
+
function estimateTokens(text) {
|
|
2606
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
2607
|
+
}
|
|
2608
|
+
function printLLMDryRun(plan, model, workflowId) {
|
|
2609
|
+
console.log("=== DRY RUN - LLM Prompt Preview ===\n");
|
|
2610
|
+
console.log(
|
|
2611
|
+
`Estimated: ~${plan.estimatedInputTokens} input tokens, ~${plan.estimatedOutputTokens} output tokens`
|
|
2612
|
+
);
|
|
2613
|
+
if (model) {
|
|
2614
|
+
console.log(`Configured model: ${model}`);
|
|
2615
|
+
}
|
|
2616
|
+
if (workflowId) {
|
|
2617
|
+
console.log(`Workflow: ${workflowId}`);
|
|
2618
|
+
}
|
|
2619
|
+
if (plan.focusAreas.length > 0) {
|
|
2620
|
+
console.log(`Focus: ${plan.focusAreas.join(", ")}`);
|
|
2621
|
+
}
|
|
2622
|
+
console.log("");
|
|
2623
|
+
for (const message of plan.messages) {
|
|
2624
|
+
console.log(`[${message.role.toUpperCase()}]`);
|
|
2625
|
+
console.log(message.content);
|
|
2626
|
+
console.log("");
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
function getProviderLabel(provider) {
|
|
2630
|
+
if (provider instanceof OpenAIProvider) {
|
|
2631
|
+
return "OpenAI";
|
|
2632
|
+
}
|
|
2633
|
+
if (provider instanceof AnthropicProvider) {
|
|
2634
|
+
return "Anthropic";
|
|
2635
|
+
}
|
|
2636
|
+
if (provider instanceof CLIProvider) {
|
|
2637
|
+
return `CLI (${provider.cliName})`;
|
|
2638
|
+
}
|
|
2639
|
+
return "LLM";
|
|
2640
|
+
}
|
|
2641
|
+
function getDefaultOutputPath(compacted, workflowId) {
|
|
1218
2642
|
const trajDir = process.env.TRAJECTORIES_DATA_DIR || ".trajectories";
|
|
1219
|
-
const compactedDir =
|
|
1220
|
-
if (!
|
|
2643
|
+
const compactedDir = join4(trajDir, "compacted");
|
|
2644
|
+
if (!existsSync3(compactedDir)) {
|
|
1221
2645
|
mkdirSync(compactedDir, { recursive: true });
|
|
1222
2646
|
}
|
|
2647
|
+
if (workflowId) {
|
|
2648
|
+
return join4(compactedDir, `workflow-${workflowId}.json`);
|
|
2649
|
+
}
|
|
1223
2650
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1224
|
-
return
|
|
2651
|
+
return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
|
|
1225
2652
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const dir =
|
|
1228
|
-
if (!
|
|
2653
|
+
function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
|
|
2654
|
+
const dir = dirname(outputPath);
|
|
2655
|
+
if (!existsSync3(dir)) {
|
|
1229
2656
|
mkdirSync(dir, { recursive: true });
|
|
1230
2657
|
}
|
|
1231
2658
|
writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
|
|
2659
|
+
if (markdownEnabled) {
|
|
2660
|
+
writeFileSync(
|
|
2661
|
+
getMarkdownOutputPath(outputPath),
|
|
2662
|
+
renderCompactionMarkdown(compacted)
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
function getMarkdownOutputPath(outputPath) {
|
|
2667
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
|
|
2668
|
+
}
|
|
2669
|
+
function renderCompactionMarkdown(compacted) {
|
|
2670
|
+
if (compacted.narrative) {
|
|
2671
|
+
return generateCompactionMarkdown(
|
|
2672
|
+
compacted
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
const decisionGroups = compacted.decisionGroups.length > 0 ? compacted.decisionGroups.map((group) => {
|
|
2676
|
+
const decisions = group.decisions.length > 0 ? group.decisions.map(
|
|
2677
|
+
(decision) => `- ${decision.question} -> ${decision.chosen} (${decision.fromTrajectory})`
|
|
2678
|
+
).join("\n") : "- None";
|
|
2679
|
+
return `## ${capitalize(group.category)}
|
|
2680
|
+
${decisions}`;
|
|
2681
|
+
}).join("\n\n") : "## Decision Groups\n- None";
|
|
2682
|
+
const learnings = compacted.keyLearnings.length > 0 ? compacted.keyLearnings.map((learning) => `- ${learning}`).join("\n") : "- None";
|
|
2683
|
+
const findings = compacted.keyFindings.length > 0 ? compacted.keyFindings.map((finding) => `- ${finding}`).join("\n") : "- None";
|
|
2684
|
+
return [
|
|
2685
|
+
`# Trajectory Compaction: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`,
|
|
2686
|
+
"",
|
|
2687
|
+
"## Summary",
|
|
2688
|
+
`- Sessions: ${compacted.sourceTrajectories.length}`,
|
|
2689
|
+
...compacted.workflowId ? [`- Workflow: ${compacted.workflowId}`] : [],
|
|
2690
|
+
`- Decisions: ${compacted.summary.totalDecisions}`,
|
|
2691
|
+
`- Events: ${compacted.summary.totalEvents}`,
|
|
2692
|
+
`- Agents: ${compacted.summary.uniqueAgents.join(", ") || "None"}`,
|
|
2693
|
+
`- Files: ${compacted.filesAffected.length}`,
|
|
2694
|
+
`- Commits: ${compacted.commits.length}`,
|
|
2695
|
+
"",
|
|
2696
|
+
decisionGroups,
|
|
2697
|
+
"",
|
|
2698
|
+
"## Key Learnings",
|
|
2699
|
+
learnings,
|
|
2700
|
+
"",
|
|
2701
|
+
"## Key Findings",
|
|
2702
|
+
findings
|
|
2703
|
+
].join("\n");
|
|
1232
2704
|
}
|
|
1233
2705
|
function printCompactedSummary(compacted) {
|
|
1234
2706
|
console.log("=== Compacted Trajectory Summary ===\n");
|
|
1235
2707
|
console.log(`ID: ${compacted.id}`);
|
|
2708
|
+
if (compacted.workflowId) {
|
|
2709
|
+
console.log(`Workflow: ${compacted.workflowId}`);
|
|
2710
|
+
}
|
|
1236
2711
|
console.log(`Source trajectories: ${compacted.sourceTrajectories.length}`);
|
|
1237
2712
|
console.log(
|
|
1238
|
-
`Date range: ${
|
|
2713
|
+
`Date range: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`
|
|
1239
2714
|
);
|
|
1240
2715
|
console.log(`Total decisions: ${compacted.summary.totalDecisions}`);
|
|
1241
2716
|
console.log(`Total events: ${compacted.summary.totalEvents}`);
|
|
1242
2717
|
console.log(`Agents: ${compacted.summary.uniqueAgents.join(", ")}`);
|
|
1243
2718
|
console.log("");
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
console.log(
|
|
1247
|
-
|
|
1248
|
-
)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
2719
|
+
if (compacted.narrative) {
|
|
2720
|
+
console.log("=== Narrative ===\n");
|
|
2721
|
+
console.log(compacted.narrative);
|
|
2722
|
+
console.log("");
|
|
2723
|
+
if (compacted.decisions && compacted.decisions.length > 0) {
|
|
2724
|
+
console.log("=== Key Decisions ===\n");
|
|
2725
|
+
for (const decision of compacted.decisions.slice(0, 5)) {
|
|
2726
|
+
console.log(` - ${decision.question}`);
|
|
2727
|
+
console.log(` Chosen: ${decision.chosen}`);
|
|
2728
|
+
if (decision.impact) {
|
|
2729
|
+
console.log(` Impact: ${decision.impact}`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (compacted.decisions.length > 5) {
|
|
2733
|
+
console.log(` ... and ${compacted.decisions.length - 5} more`);
|
|
2734
|
+
}
|
|
2735
|
+
console.log("");
|
|
1252
2736
|
}
|
|
1253
|
-
if (
|
|
1254
|
-
console.log(
|
|
2737
|
+
if (compacted.openQuestions && compacted.openQuestions.length > 0) {
|
|
2738
|
+
console.log("=== Open Questions ===\n");
|
|
2739
|
+
for (const question of compacted.openQuestions.slice(0, 5)) {
|
|
2740
|
+
console.log(` - ${question}`);
|
|
2741
|
+
}
|
|
2742
|
+
if (compacted.openQuestions.length > 5) {
|
|
2743
|
+
console.log(` ... and ${compacted.openQuestions.length - 5} more`);
|
|
2744
|
+
}
|
|
2745
|
+
console.log("");
|
|
1255
2746
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2747
|
+
} else {
|
|
2748
|
+
console.log("=== Decision Groups ===\n");
|
|
2749
|
+
for (const group of compacted.decisionGroups) {
|
|
2750
|
+
console.log(
|
|
2751
|
+
`${capitalize(group.category)} (${group.decisions.length} decisions):`
|
|
2752
|
+
);
|
|
2753
|
+
for (const decision of group.decisions.slice(0, 3)) {
|
|
2754
|
+
console.log(` - ${decision.question}`);
|
|
2755
|
+
console.log(` Chose: ${decision.chosen}`);
|
|
2756
|
+
}
|
|
2757
|
+
if (group.decisions.length > 3) {
|
|
2758
|
+
console.log(` ... and ${group.decisions.length - 3} more`);
|
|
2759
|
+
}
|
|
2760
|
+
console.log("");
|
|
1262
2761
|
}
|
|
1263
|
-
if (compacted.keyLearnings.length >
|
|
1264
|
-
console.log(
|
|
2762
|
+
if (compacted.keyLearnings.length > 0) {
|
|
2763
|
+
console.log("=== Key Learnings ===\n");
|
|
2764
|
+
for (const learning of compacted.keyLearnings.slice(0, 5)) {
|
|
2765
|
+
console.log(` - ${learning}`);
|
|
2766
|
+
}
|
|
2767
|
+
if (compacted.keyLearnings.length > 5) {
|
|
2768
|
+
console.log(` ... and ${compacted.keyLearnings.length - 5} more`);
|
|
2769
|
+
}
|
|
2770
|
+
console.log("");
|
|
1265
2771
|
}
|
|
1266
|
-
console.log("");
|
|
1267
2772
|
}
|
|
1268
2773
|
if (compacted.filesAffected.length > 0) {
|
|
1269
2774
|
console.log(`Files affected: ${compacted.filesAffected.length}`);
|
|
@@ -1272,7 +2777,7 @@ function printCompactedSummary(compacted) {
|
|
|
1272
2777
|
console.log(`Commits: ${compacted.commits.length}`);
|
|
1273
2778
|
}
|
|
1274
2779
|
}
|
|
1275
|
-
function
|
|
2780
|
+
function formatDate3(isoString) {
|
|
1276
2781
|
return new Date(isoString).toLocaleDateString("en-US", {
|
|
1277
2782
|
month: "short",
|
|
1278
2783
|
day: "numeric",
|
|
@@ -1284,12 +2789,12 @@ function capitalize(str) {
|
|
|
1284
2789
|
}
|
|
1285
2790
|
|
|
1286
2791
|
// src/cli/commands/complete.ts
|
|
1287
|
-
import { existsSync as
|
|
2792
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1288
2793
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1289
|
-
import { join as
|
|
2794
|
+
import { join as join5 } from "path";
|
|
1290
2795
|
|
|
1291
2796
|
// src/core/trace.ts
|
|
1292
|
-
import { execSync
|
|
2797
|
+
import { execSync } from "child_process";
|
|
1293
2798
|
import { createHash } from "crypto";
|
|
1294
2799
|
function isValidGitRef(ref) {
|
|
1295
2800
|
const validRefPattern = /^[a-zA-Z0-9_\-./]+$/;
|
|
@@ -1309,7 +2814,7 @@ function isValidGitRef(ref) {
|
|
|
1309
2814
|
}
|
|
1310
2815
|
function isGitRepo() {
|
|
1311
2816
|
try {
|
|
1312
|
-
|
|
2817
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
1313
2818
|
encoding: "utf-8",
|
|
1314
2819
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1315
2820
|
});
|
|
@@ -1323,7 +2828,7 @@ function getGitHead() {
|
|
|
1323
2828
|
return null;
|
|
1324
2829
|
}
|
|
1325
2830
|
try {
|
|
1326
|
-
const head =
|
|
2831
|
+
const head = execSync("git rev-parse HEAD", {
|
|
1327
2832
|
encoding: "utf-8",
|
|
1328
2833
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1329
2834
|
}).trim();
|
|
@@ -1375,7 +2880,7 @@ function getChangedFiles(startRef, endRef = "HEAD") {
|
|
|
1375
2880
|
return [];
|
|
1376
2881
|
}
|
|
1377
2882
|
try {
|
|
1378
|
-
const diffOutput =
|
|
2883
|
+
const diffOutput = execSync(`git diff ${startRef}..${endRef}`, {
|
|
1379
2884
|
encoding: "utf-8",
|
|
1380
2885
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1381
2886
|
maxBuffer: 10 * 1024 * 1024
|
|
@@ -1416,8 +2921,8 @@ function generateTrace(trajectory, startRef) {
|
|
|
1416
2921
|
return null;
|
|
1417
2922
|
}
|
|
1418
2923
|
const model = detectModel();
|
|
1419
|
-
const traceFiles = changedFiles.map(({ path, ranges }) => ({
|
|
1420
|
-
path,
|
|
2924
|
+
const traceFiles = changedFiles.map(({ path: path2, ranges }) => ({
|
|
2925
|
+
path: path2,
|
|
1421
2926
|
conversations: [
|
|
1422
2927
|
{
|
|
1423
2928
|
contributor: {
|
|
@@ -1480,8 +2985,8 @@ function createTraceRef(startRef, traceId) {
|
|
|
1480
2985
|
}
|
|
1481
2986
|
|
|
1482
2987
|
// src/core/trailers.ts
|
|
1483
|
-
import { execSync as
|
|
1484
|
-
import { readFileSync as
|
|
2988
|
+
import { execSync as execSync2 } from "child_process";
|
|
2989
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1485
2990
|
function getCommitsBetween(startRef, endRef = "HEAD") {
|
|
1486
2991
|
if (!isGitRepo()) {
|
|
1487
2992
|
return [];
|
|
@@ -1490,7 +2995,7 @@ function getCommitsBetween(startRef, endRef = "HEAD") {
|
|
|
1490
2995
|
return [];
|
|
1491
2996
|
}
|
|
1492
2997
|
try {
|
|
1493
|
-
const output =
|
|
2998
|
+
const output = execSync2(
|
|
1494
2999
|
`git log --format=%H%n%h%n%s%n%an%n%aI%n--- ${startRef}..${endRef}`,
|
|
1495
3000
|
{
|
|
1496
3001
|
encoding: "utf-8",
|
|
@@ -1527,7 +3032,7 @@ function getFilesChangedBetween(startRef, endRef = "HEAD") {
|
|
|
1527
3032
|
return [];
|
|
1528
3033
|
}
|
|
1529
3034
|
try {
|
|
1530
|
-
const output =
|
|
3035
|
+
const output = execSync2(`git diff --name-only ${startRef}..${endRef}`, {
|
|
1531
3036
|
encoding: "utf-8",
|
|
1532
3037
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1533
3038
|
});
|
|
@@ -1564,8 +3069,11 @@ if [ -z "$ACTIVE_FILE" ]; then
|
|
|
1564
3069
|
exit 0
|
|
1565
3070
|
fi
|
|
1566
3071
|
|
|
1567
|
-
# Extract trajectory ID (grep for the "id" field)
|
|
1568
|
-
|
|
3072
|
+
# Extract trajectory ID (grep for the "id" field). Character class must
|
|
3073
|
+
# include underscore to match legacy traj_<timestamp>_<hex> ids -- without
|
|
3074
|
+
# it, grep -o silently truncates at the first internal underscore and
|
|
3075
|
+
# emits a wrong (shorter) id into the commit trailer.
|
|
3076
|
+
TRAJ_ID=$(grep -o '"id"[[:space:]]*:[[:space:]]*"traj_[a-z0-9_]*"' "$ACTIVE_FILE" | head -1 | grep -o 'traj_[a-z0-9_]*')
|
|
1569
3077
|
if [ -z "$TRAJ_ID" ]; then
|
|
1570
3078
|
exit 0
|
|
1571
3079
|
fi
|
|
@@ -1585,13 +3093,13 @@ function detectExistingHook() {
|
|
|
1585
3093
|
return "none";
|
|
1586
3094
|
}
|
|
1587
3095
|
try {
|
|
1588
|
-
const hooksDir =
|
|
3096
|
+
const hooksDir = execSync2("git rev-parse --git-dir", {
|
|
1589
3097
|
encoding: "utf-8",
|
|
1590
3098
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1591
3099
|
}).trim();
|
|
1592
3100
|
const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
|
|
1593
3101
|
try {
|
|
1594
|
-
const content =
|
|
3102
|
+
const content = readFileSync3(hookPath, "utf-8");
|
|
1595
3103
|
if (content.includes("agent-trajectories")) {
|
|
1596
3104
|
return "ours";
|
|
1597
3105
|
}
|
|
@@ -1607,17 +3115,17 @@ function detectExistingHook() {
|
|
|
1607
3115
|
// src/cli/commands/complete.ts
|
|
1608
3116
|
async function saveTraceFile(trajectory, trace) {
|
|
1609
3117
|
const dataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
1610
|
-
const baseDir = dataDir ? dataDir :
|
|
1611
|
-
const completedDir =
|
|
3118
|
+
const baseDir = dataDir ? dataDir : join5(process.cwd(), ".trajectories");
|
|
3119
|
+
const completedDir = join5(baseDir, "completed");
|
|
1612
3120
|
const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
|
|
1613
|
-
const monthDir =
|
|
3121
|
+
const monthDir = join5(
|
|
1614
3122
|
completedDir,
|
|
1615
3123
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
1616
3124
|
);
|
|
1617
|
-
if (!
|
|
3125
|
+
if (!existsSync4(monthDir)) {
|
|
1618
3126
|
await mkdir2(monthDir, { recursive: true });
|
|
1619
3127
|
}
|
|
1620
|
-
const tracePath =
|
|
3128
|
+
const tracePath = join5(monthDir, `${trajectory.id}.trace.json`);
|
|
1621
3129
|
await writeFile2(tracePath, JSON.stringify(trace, null, 2), "utf-8");
|
|
1622
3130
|
}
|
|
1623
3131
|
function registerCompleteCommand(program2) {
|
|
@@ -1730,17 +3238,17 @@ function registerDecisionCommand(program2) {
|
|
|
1730
3238
|
}
|
|
1731
3239
|
|
|
1732
3240
|
// src/cli/commands/enable.ts
|
|
1733
|
-
import { execSync as
|
|
1734
|
-
import { existsSync as
|
|
3241
|
+
import { execSync as execSync3 } from "child_process";
|
|
3242
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1735
3243
|
import { chmod, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1736
|
-
import { join as
|
|
3244
|
+
import { join as join6 } from "path";
|
|
1737
3245
|
function getHooksDir() {
|
|
1738
3246
|
try {
|
|
1739
|
-
const gitDir =
|
|
3247
|
+
const gitDir = execSync3("git rev-parse --git-dir", {
|
|
1740
3248
|
encoding: "utf-8",
|
|
1741
3249
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1742
3250
|
}).trim();
|
|
1743
|
-
return
|
|
3251
|
+
return join6(gitDir, "hooks");
|
|
1744
3252
|
} catch {
|
|
1745
3253
|
return null;
|
|
1746
3254
|
}
|
|
@@ -1758,7 +3266,7 @@ function registerEnableCommand(program2) {
|
|
|
1758
3266
|
console.error("Error: Could not determine git hooks directory");
|
|
1759
3267
|
throw new Error("Cannot find hooks directory");
|
|
1760
3268
|
}
|
|
1761
|
-
const hookPath =
|
|
3269
|
+
const hookPath = join6(hooksDir, "prepare-commit-msg");
|
|
1762
3270
|
const existing = detectExistingHook();
|
|
1763
3271
|
if (existing === "other" && !options.force) {
|
|
1764
3272
|
console.error("Error: A prepare-commit-msg hook already exists");
|
|
@@ -1771,7 +3279,7 @@ function registerEnableCommand(program2) {
|
|
|
1771
3279
|
console.log("Trajectory hook is already installed");
|
|
1772
3280
|
return;
|
|
1773
3281
|
}
|
|
1774
|
-
if (!
|
|
3282
|
+
if (!existsSync5(hooksDir)) {
|
|
1775
3283
|
await mkdir3(hooksDir, { recursive: true });
|
|
1776
3284
|
}
|
|
1777
3285
|
const hookContent = generateHookScript();
|
|
@@ -1793,7 +3301,7 @@ function registerEnableCommand(program2) {
|
|
|
1793
3301
|
console.error("Error: Could not determine git hooks directory");
|
|
1794
3302
|
throw new Error("Cannot find hooks directory");
|
|
1795
3303
|
}
|
|
1796
|
-
const hookPath =
|
|
3304
|
+
const hookPath = join6(hooksDir, "prepare-commit-msg");
|
|
1797
3305
|
const existing = detectExistingHook();
|
|
1798
3306
|
if (existing === "none") {
|
|
1799
3307
|
console.log("No trajectory hook installed");
|
|
@@ -1815,7 +3323,7 @@ function registerEnableCommand(program2) {
|
|
|
1815
3323
|
// src/cli/commands/export.ts
|
|
1816
3324
|
import { exec } from "child_process";
|
|
1817
3325
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1818
|
-
import { join as
|
|
3326
|
+
import { join as join7 } from "path";
|
|
1819
3327
|
|
|
1820
3328
|
// src/export/json.ts
|
|
1821
3329
|
function exportToJSON(trajectory, options) {
|
|
@@ -2229,7 +3737,7 @@ h2 {
|
|
|
2229
3737
|
function escapeHtml(text) {
|
|
2230
3738
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2231
3739
|
}
|
|
2232
|
-
function
|
|
3740
|
+
function formatDate4(isoDate) {
|
|
2233
3741
|
const date = new Date(isoDate);
|
|
2234
3742
|
return date.toLocaleDateString("en-US", {
|
|
2235
3743
|
month: "short",
|
|
@@ -2239,7 +3747,7 @@ function formatDate3(isoDate) {
|
|
|
2239
3747
|
minute: "2-digit"
|
|
2240
3748
|
});
|
|
2241
3749
|
}
|
|
2242
|
-
function
|
|
3750
|
+
function formatDuration2(startDate, endDate) {
|
|
2243
3751
|
const start = new Date(startDate).getTime();
|
|
2244
3752
|
const end = endDate ? new Date(endDate).getTime() : Date.now();
|
|
2245
3753
|
const ms = end - start;
|
|
@@ -2278,7 +3786,7 @@ function renderDecision(decision) {
|
|
|
2278
3786
|
`;
|
|
2279
3787
|
}
|
|
2280
3788
|
function renderEvent(event) {
|
|
2281
|
-
const time =
|
|
3789
|
+
const time = formatDate4(new Date(event.ts).toISOString());
|
|
2282
3790
|
let content = "";
|
|
2283
3791
|
let typeClass = "";
|
|
2284
3792
|
const rawData = event.raw;
|
|
@@ -2324,7 +3832,7 @@ function renderEvent(event) {
|
|
|
2324
3832
|
</div>
|
|
2325
3833
|
`;
|
|
2326
3834
|
}
|
|
2327
|
-
function
|
|
3835
|
+
function renderChapter2(chapter, index) {
|
|
2328
3836
|
const events = chapter.events.map(renderEvent).join("");
|
|
2329
3837
|
return `
|
|
2330
3838
|
<div class="chapter">
|
|
@@ -2341,7 +3849,7 @@ function renderChapter(chapter, index) {
|
|
|
2341
3849
|
</div>
|
|
2342
3850
|
`;
|
|
2343
3851
|
}
|
|
2344
|
-
function
|
|
3852
|
+
function renderRetrospective2(trajectory) {
|
|
2345
3853
|
if (!trajectory.retrospective) {
|
|
2346
3854
|
return "";
|
|
2347
3855
|
}
|
|
@@ -2373,7 +3881,7 @@ function renderRetrospective(trajectory) {
|
|
|
2373
3881
|
}
|
|
2374
3882
|
function generateTrajectoryHtml(trajectory) {
|
|
2375
3883
|
const statusClass = getStatusClass(trajectory.status);
|
|
2376
|
-
const duration =
|
|
3884
|
+
const duration = formatDuration2(trajectory.startedAt, trajectory.completedAt);
|
|
2377
3885
|
const decisions = trajectory.chapters.flatMap(
|
|
2378
3886
|
(ch) => ch.events.filter((e) => e.type === "decision" && e.raw).map((e) => e.raw).filter(
|
|
2379
3887
|
(d) => d !== void 0 && typeof d.question === "string"
|
|
@@ -2392,7 +3900,7 @@ function generateTrajectoryHtml(trajectory) {
|
|
|
2392
3900
|
Chapters (${trajectory.chapters.length})
|
|
2393
3901
|
</h2>
|
|
2394
3902
|
<div class="collapsible-content">
|
|
2395
|
-
${trajectory.chapters.map(
|
|
3903
|
+
${trajectory.chapters.map(renderChapter2).join("")}
|
|
2396
3904
|
</div>
|
|
2397
3905
|
` : "";
|
|
2398
3906
|
const filesHtml = trajectory.filesChanged.length ? `
|
|
@@ -2433,7 +3941,7 @@ function generateTrajectoryHtml(trajectory) {
|
|
|
2433
3941
|
</div>
|
|
2434
3942
|
<div class="meta-item">
|
|
2435
3943
|
<span class="meta-label">Started</span>
|
|
2436
|
-
<span class="meta-value">${
|
|
3944
|
+
<span class="meta-value">${formatDate4(trajectory.startedAt)}</span>
|
|
2437
3945
|
</div>
|
|
2438
3946
|
${trajectory.task.source ? `
|
|
2439
3947
|
<div class="meta-item">
|
|
@@ -2448,7 +3956,7 @@ function generateTrajectoryHtml(trajectory) {
|
|
|
2448
3956
|
</div>
|
|
2449
3957
|
</div>
|
|
2450
3958
|
|
|
2451
|
-
${
|
|
3959
|
+
${renderRetrospective2(trajectory)}
|
|
2452
3960
|
${decisionsHtml}
|
|
2453
3961
|
${chaptersHtml}
|
|
2454
3962
|
${filesHtml}
|
|
@@ -2512,9 +4020,9 @@ function registerExportCommand(program2) {
|
|
|
2512
4020
|
openInBrowser(options.output);
|
|
2513
4021
|
}
|
|
2514
4022
|
} else if (options.open && options.format === "html") {
|
|
2515
|
-
const outputDir =
|
|
4023
|
+
const outputDir = join7(process.cwd(), ".trajectories", "html");
|
|
2516
4024
|
await mkdir4(outputDir, { recursive: true });
|
|
2517
|
-
const filePath =
|
|
4025
|
+
const filePath = join7(outputDir, `${trajectory.id}.html`);
|
|
2518
4026
|
await writeFile4(filePath, output, "utf-8");
|
|
2519
4027
|
console.log(`\u2713 Generated: ${filePath}`);
|
|
2520
4028
|
openInBrowser(filePath);
|
|
@@ -2523,25 +4031,25 @@ function registerExportCommand(program2) {
|
|
|
2523
4031
|
}
|
|
2524
4032
|
});
|
|
2525
4033
|
}
|
|
2526
|
-
function openInBrowser(
|
|
4034
|
+
function openInBrowser(path2) {
|
|
2527
4035
|
const platform = process.platform;
|
|
2528
4036
|
let command;
|
|
2529
4037
|
if (platform === "darwin") {
|
|
2530
|
-
command = `open "${
|
|
4038
|
+
command = `open "${path2}"`;
|
|
2531
4039
|
} else if (platform === "win32") {
|
|
2532
|
-
command = `start "" "${
|
|
4040
|
+
command = `start "" "${path2}"`;
|
|
2533
4041
|
} else {
|
|
2534
|
-
command = `xdg-open "${
|
|
4042
|
+
command = `xdg-open "${path2}"`;
|
|
2535
4043
|
}
|
|
2536
4044
|
exec(command, (error) => {
|
|
2537
4045
|
if (error) {
|
|
2538
|
-
console.log(`Open manually: file://${
|
|
4046
|
+
console.log(`Open manually: file://${path2}`);
|
|
2539
4047
|
}
|
|
2540
4048
|
});
|
|
2541
4049
|
}
|
|
2542
4050
|
|
|
2543
4051
|
// src/cli/commands/list.ts
|
|
2544
|
-
import { existsSync as
|
|
4052
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2545
4053
|
function registerListCommand(program2) {
|
|
2546
4054
|
program2.command("list").description("List and search trajectories").option(
|
|
2547
4055
|
"-s, --status <status>",
|
|
@@ -2551,7 +4059,7 @@ function registerListCommand(program2) {
|
|
|
2551
4059
|
let allTrajectories = [];
|
|
2552
4060
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2553
4061
|
for (const searchPath of searchPaths) {
|
|
2554
|
-
if (!
|
|
4062
|
+
if (!existsSync6(searchPath)) {
|
|
2555
4063
|
continue;
|
|
2556
4064
|
}
|
|
2557
4065
|
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -2610,9 +4118,9 @@ function registerListCommand(program2) {
|
|
|
2610
4118
|
const confidence = traj.confidence ? ` (${Math.round(traj.confidence * 100)}%)` : "";
|
|
2611
4119
|
console.log(`${statusIcon} ${traj.id}`);
|
|
2612
4120
|
console.log(` ${traj.title}${confidence}`);
|
|
2613
|
-
console.log(` Started: ${
|
|
4121
|
+
console.log(` Started: ${formatDate5(traj.startedAt)}`);
|
|
2614
4122
|
if (traj.completedAt) {
|
|
2615
|
-
console.log(` Completed: ${
|
|
4123
|
+
console.log(` Completed: ${formatDate5(traj.completedAt)}`);
|
|
2616
4124
|
}
|
|
2617
4125
|
console.log("");
|
|
2618
4126
|
}
|
|
@@ -2630,7 +4138,7 @@ function getStatusIcon(status) {
|
|
|
2630
4138
|
return "\u2022";
|
|
2631
4139
|
}
|
|
2632
4140
|
}
|
|
2633
|
-
function
|
|
4141
|
+
function formatDate5(isoString) {
|
|
2634
4142
|
return new Date(isoString).toLocaleDateString("en-US", {
|
|
2635
4143
|
month: "short",
|
|
2636
4144
|
day: "numeric",
|
|
@@ -2700,13 +4208,13 @@ function registerReflectCommand(program2) {
|
|
|
2700
4208
|
}
|
|
2701
4209
|
|
|
2702
4210
|
// src/cli/commands/show.ts
|
|
2703
|
-
import { existsSync as
|
|
4211
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2704
4212
|
import { readFile as readFile2 } from "fs/promises";
|
|
2705
|
-
import { join as
|
|
4213
|
+
import { join as join8 } from "path";
|
|
2706
4214
|
async function findTrajectory(id) {
|
|
2707
4215
|
const searchPaths = getSearchPaths();
|
|
2708
4216
|
for (const searchPath of searchPaths) {
|
|
2709
|
-
if (!
|
|
4217
|
+
if (!existsSync7(searchPath)) {
|
|
2710
4218
|
continue;
|
|
2711
4219
|
}
|
|
2712
4220
|
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -2731,19 +4239,19 @@ async function findTrajectory(id) {
|
|
|
2731
4239
|
async function findTraceFile(id) {
|
|
2732
4240
|
const searchPaths = getSearchPaths();
|
|
2733
4241
|
for (const searchPath of searchPaths) {
|
|
2734
|
-
if (!
|
|
4242
|
+
if (!existsSync7(searchPath)) {
|
|
2735
4243
|
continue;
|
|
2736
4244
|
}
|
|
2737
|
-
const completedDir =
|
|
2738
|
-
if (!
|
|
4245
|
+
const completedDir = join8(searchPath, "completed");
|
|
4246
|
+
if (!existsSync7(completedDir)) {
|
|
2739
4247
|
continue;
|
|
2740
4248
|
}
|
|
2741
4249
|
try {
|
|
2742
4250
|
const { readdirSync } = await import("fs");
|
|
2743
4251
|
const months = readdirSync(completedDir);
|
|
2744
4252
|
for (const month of months) {
|
|
2745
|
-
const tracePath =
|
|
2746
|
-
if (
|
|
4253
|
+
const tracePath = join8(completedDir, month, `${id}.trace.json`);
|
|
4254
|
+
if (existsSync7(tracePath)) {
|
|
2747
4255
|
let record;
|
|
2748
4256
|
let migrated;
|
|
2749
4257
|
try {
|
|
@@ -2894,7 +4402,10 @@ function registerStartCommand(program2) {
|
|
|
2894
4402
|
program2.command("start <title>").description("Start a new trajectory").option("-t, --task <id>", "External task ID").option(
|
|
2895
4403
|
"-s, --source <system>",
|
|
2896
4404
|
"Task system (github, linear, jira, beads)"
|
|
2897
|
-
).option("--url <url>", "URL to external task").option("-a, --agent <name>", "Agent name (or set TRAJECTORIES_AGENT)").option("-p, --project <id>", "Project ID (or set TRAJECTORIES_PROJECT)").option(
|
|
4405
|
+
).option("--url <url>", "URL to external task").option("-a, --agent <name>", "Agent name (or set TRAJECTORIES_AGENT)").option("-p, --project <id>", "Project ID (or set TRAJECTORIES_PROJECT)").option(
|
|
4406
|
+
"-w, --workflow <id>",
|
|
4407
|
+
"Workflow run id (or set TRAJECTORIES_WORKFLOW_ID). Stamped onto the trajectory so `trail compact --workflow <id>` can collate a run."
|
|
4408
|
+
).option("-q, --quiet", "Only output trajectory ID (for scripting)").action(async (title, options) => {
|
|
2898
4409
|
const storage = new FileStorage();
|
|
2899
4410
|
await storage.initialize();
|
|
2900
4411
|
const active = await storage.getActive();
|
|
@@ -2917,12 +4428,16 @@ function registerStartCommand(program2) {
|
|
|
2917
4428
|
}
|
|
2918
4429
|
const agentName = options.agent ?? process.env.TRAJECTORIES_AGENT ?? void 0;
|
|
2919
4430
|
const projectId = options.project ?? process.env.TRAJECTORIES_PROJECT ?? void 0;
|
|
4431
|
+
const workflowId = typeof options.workflow === "string" && options.workflow.trim() || typeof process.env.TRAJECTORIES_WORKFLOW_ID === "string" && process.env.TRAJECTORIES_WORKFLOW_ID.trim() || void 0;
|
|
2920
4432
|
const startRef = captureGitState();
|
|
2921
4433
|
let trajectory = createTrajectory({
|
|
2922
4434
|
title,
|
|
2923
4435
|
source,
|
|
2924
4436
|
projectId
|
|
2925
4437
|
});
|
|
4438
|
+
if (workflowId) {
|
|
4439
|
+
trajectory = { ...trajectory, workflowId };
|
|
4440
|
+
}
|
|
2926
4441
|
if (startRef) {
|
|
2927
4442
|
trajectory = {
|
|
2928
4443
|
...trajectory,
|
|
@@ -2965,7 +4480,7 @@ function registerStatusCommand(program2) {
|
|
|
2965
4480
|
console.log('Start one with: trail start "Task description"');
|
|
2966
4481
|
return;
|
|
2967
4482
|
}
|
|
2968
|
-
const duration =
|
|
4483
|
+
const duration = formatDuration3(
|
|
2969
4484
|
(/* @__PURE__ */ new Date()).getTime() - new Date(active.startedAt).getTime()
|
|
2970
4485
|
);
|
|
2971
4486
|
const eventCount = active.chapters.reduce(
|
|
@@ -3007,7 +4522,7 @@ Current Chapter: ${currentChapter.title}`);
|
|
|
3007
4522
|
}
|
|
3008
4523
|
});
|
|
3009
4524
|
}
|
|
3010
|
-
function
|
|
4525
|
+
function formatDuration3(ms) {
|
|
3011
4526
|
const seconds = Math.floor(ms / 1e3);
|
|
3012
4527
|
const minutes = Math.floor(seconds / 60);
|
|
3013
4528
|
const hours = Math.floor(minutes / 60);
|
|
@@ -3033,8 +4548,37 @@ function registerCommands(program2) {
|
|
|
3033
4548
|
registerCompactCommand(program2);
|
|
3034
4549
|
}
|
|
3035
4550
|
|
|
4551
|
+
// src/cli/version.ts
|
|
4552
|
+
import fs from "fs";
|
|
4553
|
+
import path from "path";
|
|
4554
|
+
import { fileURLToPath } from "url";
|
|
4555
|
+
function findPackageJson(startDir) {
|
|
4556
|
+
let dir = startDir;
|
|
4557
|
+
while (dir !== path.dirname(dir)) {
|
|
4558
|
+
const candidate = path.join(dir, "package.json");
|
|
4559
|
+
if (fs.existsSync(candidate)) {
|
|
4560
|
+
return candidate;
|
|
4561
|
+
}
|
|
4562
|
+
dir = path.dirname(dir);
|
|
4563
|
+
}
|
|
4564
|
+
throw new Error("Could not find package.json");
|
|
4565
|
+
}
|
|
4566
|
+
function resolveCliVersion() {
|
|
4567
|
+
try {
|
|
4568
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
4569
|
+
const packageJsonPath = findPackageJson(here);
|
|
4570
|
+
const packageJson = JSON.parse(
|
|
4571
|
+
fs.readFileSync(packageJsonPath, "utf-8")
|
|
4572
|
+
);
|
|
4573
|
+
return packageJson.version ?? "unknown";
|
|
4574
|
+
} catch {
|
|
4575
|
+
return "unknown";
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
var VERSION = resolveCliVersion();
|
|
4579
|
+
|
|
3036
4580
|
// src/cli/index.ts
|
|
3037
|
-
program.name("trail").description("Leave a trail of your work for others to follow").version(
|
|
4581
|
+
program.name("trail").description("Leave a trail of your work for others to follow").version(VERSION).option(
|
|
3038
4582
|
"--data-dir <path>",
|
|
3039
4583
|
"Override trajectory storage directory (or set TRAJECTORIES_DATA_DIR)"
|
|
3040
4584
|
).hook("preAction", (thisCommand) => {
|