agent-trajectories 0.2.3 → 0.3.1
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/README.md +33 -0
- package/dist/{chunk-2NBMLFIW.js → chunk-DX4OPGH7.js} +51 -9
- package/dist/chunk-DX4OPGH7.js.map +1 -0
- package/dist/cli/index.js +300 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +214 -45
- package/dist/index.js +1 -1
- package/package.json +5 -1
- package/dist/chunk-2NBMLFIW.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
exportToJSON,
|
|
10
10
|
exportToMarkdown,
|
|
11
11
|
exportToTimeline,
|
|
12
|
+
generateRandomId,
|
|
12
13
|
getSearchPaths
|
|
13
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-DX4OPGH7.js";
|
|
14
15
|
|
|
15
16
|
// src/cli/index.ts
|
|
16
17
|
import { program } from "commander";
|
|
@@ -35,6 +36,184 @@ function registerAbandonCommand(program2) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// src/cli/commands/complete.ts
|
|
39
|
+
import { existsSync } from "fs";
|
|
40
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
41
|
+
import { join } from "path";
|
|
42
|
+
|
|
43
|
+
// src/core/trace.ts
|
|
44
|
+
import { execSync } from "child_process";
|
|
45
|
+
import { createHash } from "crypto";
|
|
46
|
+
function isValidGitRef(ref) {
|
|
47
|
+
const validRefPattern = /^[a-zA-Z0-9_\-./]+$/;
|
|
48
|
+
const commitHashPattern = /^[a-fA-F0-9]{7,40}$/;
|
|
49
|
+
if (ref === "HEAD" || ref === "working") {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (commitHashPattern.test(ref)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (validRefPattern.test(ref) && ref.length <= 255) {
|
|
56
|
+
if (!ref.includes("..") || ref.split("..").every((p) => p.length > 0)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function isGitRepo() {
|
|
63
|
+
try {
|
|
64
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
65
|
+
encoding: "utf-8",
|
|
66
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function getGitHead() {
|
|
74
|
+
if (!isGitRepo()) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const head = execSync("git rev-parse HEAD", {
|
|
79
|
+
encoding: "utf-8",
|
|
80
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
81
|
+
}).trim();
|
|
82
|
+
return head;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function captureGitState() {
|
|
88
|
+
return getGitHead();
|
|
89
|
+
}
|
|
90
|
+
function parseDiffOutput(diffOutput) {
|
|
91
|
+
const files = [];
|
|
92
|
+
const lines = diffOutput.split("\n");
|
|
93
|
+
let currentFile = null;
|
|
94
|
+
let currentRanges = [];
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const diffHeaderMatch = line.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
97
|
+
if (diffHeaderMatch) {
|
|
98
|
+
if (currentFile) {
|
|
99
|
+
files.push({ path: currentFile, ranges: currentRanges });
|
|
100
|
+
}
|
|
101
|
+
currentFile = diffHeaderMatch[1];
|
|
102
|
+
currentRanges = [];
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
106
|
+
if (hunkMatch && currentFile) {
|
|
107
|
+
const startLine = Number.parseInt(hunkMatch[1], 10);
|
|
108
|
+
const lineCount = hunkMatch[2] ? Number.parseInt(hunkMatch[2], 10) : 1;
|
|
109
|
+
if (lineCount > 0) {
|
|
110
|
+
currentRanges.push({
|
|
111
|
+
start_line: startLine,
|
|
112
|
+
end_line: startLine + lineCount - 1
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (currentFile) {
|
|
118
|
+
files.push({ path: currentFile, ranges: currentRanges });
|
|
119
|
+
}
|
|
120
|
+
return files;
|
|
121
|
+
}
|
|
122
|
+
function getChangedFiles(startRef, endRef = "HEAD") {
|
|
123
|
+
if (!isGitRepo()) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
if (!isValidGitRef(startRef) || !isValidGitRef(endRef)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const diffOutput = execSync(`git diff ${startRef}..${endRef}`, {
|
|
131
|
+
encoding: "utf-8",
|
|
132
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
133
|
+
maxBuffer: 10 * 1024 * 1024
|
|
134
|
+
// 10MB buffer for large diffs
|
|
135
|
+
});
|
|
136
|
+
return parseDiffOutput(diffOutput);
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function detectModel() {
|
|
142
|
+
if (process.env.TRAIL_TRACE_MODEL) {
|
|
143
|
+
return process.env.TRAIL_TRACE_MODEL;
|
|
144
|
+
}
|
|
145
|
+
if (process.env.ANTHROPIC_MODEL) {
|
|
146
|
+
return process.env.ANTHROPIC_MODEL;
|
|
147
|
+
}
|
|
148
|
+
if (process.env.OPENAI_MODEL) {
|
|
149
|
+
return process.env.OPENAI_MODEL;
|
|
150
|
+
}
|
|
151
|
+
return "unknown";
|
|
152
|
+
}
|
|
153
|
+
function generateTraceId() {
|
|
154
|
+
return `trace_${generateRandomId()}`;
|
|
155
|
+
}
|
|
156
|
+
function generateTrace(trajectory, startRef) {
|
|
157
|
+
if (!isGitRepo()) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const endRef = getGitHead();
|
|
161
|
+
if (!endRef) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const changedFiles = getChangedFiles(startRef, endRef);
|
|
165
|
+
if (changedFiles.length === 0) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const model = detectModel();
|
|
169
|
+
const traceFiles = changedFiles.map(({ path, ranges }) => ({
|
|
170
|
+
path,
|
|
171
|
+
conversations: [
|
|
172
|
+
{
|
|
173
|
+
contributor: {
|
|
174
|
+
type: "agent",
|
|
175
|
+
model
|
|
176
|
+
},
|
|
177
|
+
ranges: ranges.map((range) => ({
|
|
178
|
+
...range,
|
|
179
|
+
revision: endRef
|
|
180
|
+
}))
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}));
|
|
184
|
+
return {
|
|
185
|
+
version: 1,
|
|
186
|
+
id: generateTraceId(),
|
|
187
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
188
|
+
trajectory: trajectory.id,
|
|
189
|
+
files: traceFiles
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function createTraceRef(startRef, traceId) {
|
|
193
|
+
const endRef = getGitHead();
|
|
194
|
+
return {
|
|
195
|
+
startRef,
|
|
196
|
+
endRef: endRef ?? void 0,
|
|
197
|
+
traceId
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/cli/commands/complete.ts
|
|
202
|
+
async function saveTraceFile(trajectory, trace) {
|
|
203
|
+
const dataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
204
|
+
const baseDir = dataDir ? dataDir : join(process.cwd(), ".trajectories");
|
|
205
|
+
const completedDir = join(baseDir, "completed");
|
|
206
|
+
const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
|
|
207
|
+
const monthDir = join(
|
|
208
|
+
completedDir,
|
|
209
|
+
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
210
|
+
);
|
|
211
|
+
if (!existsSync(monthDir)) {
|
|
212
|
+
await mkdir(monthDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
const tracePath = join(monthDir, `${trajectory.id}.trace.json`);
|
|
215
|
+
await writeFile(tracePath, JSON.stringify(trace, null, 2), "utf-8");
|
|
216
|
+
}
|
|
38
217
|
function registerCompleteCommand(program2) {
|
|
39
218
|
program2.command("complete").description("Complete the active trajectory with retrospective").option("--summary <text>", "Summary of what was accomplished").option("--approach <text>", "How the work was approached").option("--confidence <number>", "Confidence level 0-1", Number.parseFloat).action(async (options) => {
|
|
40
219
|
const storage = new FileStorage();
|
|
@@ -54,15 +233,37 @@ function registerCompleteCommand(program2) {
|
|
|
54
233
|
console.error("Error: --confidence must be between 0 and 1");
|
|
55
234
|
throw new Error("Invalid confidence");
|
|
56
235
|
}
|
|
57
|
-
|
|
236
|
+
let completed = completeTrajectory(active, {
|
|
58
237
|
summary: options.summary,
|
|
59
238
|
approach: options.approach || "Standard approach",
|
|
60
239
|
confidence
|
|
61
240
|
});
|
|
241
|
+
let trace = null;
|
|
242
|
+
if (active._trace?.startRef) {
|
|
243
|
+
trace = generateTrace(completed, active._trace.startRef);
|
|
244
|
+
if (trace) {
|
|
245
|
+
const endRef = getGitHead();
|
|
246
|
+
completed = {
|
|
247
|
+
...completed,
|
|
248
|
+
_trace: {
|
|
249
|
+
...completed._trace,
|
|
250
|
+
startRef: active._trace.startRef,
|
|
251
|
+
endRef: endRef ?? void 0,
|
|
252
|
+
traceId: trace.id
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
62
257
|
await storage.save(completed);
|
|
258
|
+
if (trace) {
|
|
259
|
+
await saveTraceFile(completed, trace);
|
|
260
|
+
}
|
|
63
261
|
console.log(`\u2713 Trajectory completed: ${completed.id}`);
|
|
64
262
|
console.log(` Summary: ${options.summary}`);
|
|
65
263
|
console.log(` Confidence: ${Math.round(confidence * 100)}%`);
|
|
264
|
+
if (trace) {
|
|
265
|
+
console.log(` Trace: ${trace.id} (${trace.files.length} files)`);
|
|
266
|
+
}
|
|
66
267
|
});
|
|
67
268
|
}
|
|
68
269
|
|
|
@@ -83,7 +284,7 @@ function registerDecisionCommand(program2) {
|
|
|
83
284
|
console.error('Start one with: trail start "Task description"');
|
|
84
285
|
throw new Error("No active trajectory");
|
|
85
286
|
}
|
|
86
|
-
const alternatives = options.alternatives ? options.alternatives.split(",").map((s) => s.trim()) : [];
|
|
287
|
+
const alternatives = options.alternatives ? options.alternatives.split(",").map((s) => ({ option: s.trim(), reason: "" })) : [];
|
|
87
288
|
const reasoning = options.reasoning || "";
|
|
88
289
|
const updated = addDecision(active, {
|
|
89
290
|
question: choice,
|
|
@@ -97,15 +298,18 @@ function registerDecisionCommand(program2) {
|
|
|
97
298
|
console.log(` Reasoning: ${reasoning}`);
|
|
98
299
|
}
|
|
99
300
|
if (alternatives.length > 0) {
|
|
100
|
-
|
|
301
|
+
const altStrings = alternatives.map(
|
|
302
|
+
(a) => a.option
|
|
303
|
+
);
|
|
304
|
+
console.log(` Alternatives: ${altStrings.join(", ")}`);
|
|
101
305
|
}
|
|
102
306
|
});
|
|
103
307
|
}
|
|
104
308
|
|
|
105
309
|
// src/cli/commands/export.ts
|
|
106
310
|
import { exec } from "child_process";
|
|
107
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
108
|
-
import { join } from "path";
|
|
311
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
312
|
+
import { join as join2 } from "path";
|
|
109
313
|
|
|
110
314
|
// src/web/styles.ts
|
|
111
315
|
var styles = `
|
|
@@ -502,7 +706,7 @@ function getStatusClass(status) {
|
|
|
502
706
|
function renderDecision(decision) {
|
|
503
707
|
const alternatives = decision.alternatives?.length ? `<div class="alternatives">
|
|
504
708
|
<span class="alternatives-label">Considered:</span>
|
|
505
|
-
${decision.alternatives.map((a) => escapeHtml(a)).join(", ")}
|
|
709
|
+
${decision.alternatives.map((a) => escapeHtml(typeof a === "string" ? a : a.option)).join(", ")}
|
|
506
710
|
</div>` : "";
|
|
507
711
|
return `
|
|
508
712
|
<div class="decision">
|
|
@@ -741,16 +945,16 @@ function registerExportCommand(program2) {
|
|
|
741
945
|
break;
|
|
742
946
|
}
|
|
743
947
|
if (options.output) {
|
|
744
|
-
await
|
|
948
|
+
await writeFile2(options.output, output, "utf-8");
|
|
745
949
|
console.log(`\u2713 Exported to ${options.output}`);
|
|
746
950
|
if (options.open && options.format === "html") {
|
|
747
951
|
openInBrowser(options.output);
|
|
748
952
|
}
|
|
749
953
|
} else if (options.open && options.format === "html") {
|
|
750
|
-
const outputDir =
|
|
751
|
-
await
|
|
752
|
-
const filePath =
|
|
753
|
-
await
|
|
954
|
+
const outputDir = join2(process.cwd(), ".trajectories", "html");
|
|
955
|
+
await mkdir2(outputDir, { recursive: true });
|
|
956
|
+
const filePath = join2(outputDir, `${trajectory.id}.html`);
|
|
957
|
+
await writeFile2(filePath, output, "utf-8");
|
|
754
958
|
console.log(`\u2713 Generated: ${filePath}`);
|
|
755
959
|
openInBrowser(filePath);
|
|
756
960
|
} else {
|
|
@@ -776,7 +980,7 @@ function openInBrowser(path) {
|
|
|
776
980
|
}
|
|
777
981
|
|
|
778
982
|
// src/cli/commands/list.ts
|
|
779
|
-
import { existsSync } from "fs";
|
|
983
|
+
import { existsSync as existsSync2 } from "fs";
|
|
780
984
|
function registerListCommand(program2) {
|
|
781
985
|
program2.command("list").description("List and search trajectories").option(
|
|
782
986
|
"-s, --status <status>",
|
|
@@ -786,7 +990,7 @@ function registerListCommand(program2) {
|
|
|
786
990
|
let allTrajectories = [];
|
|
787
991
|
const seenIds = /* @__PURE__ */ new Set();
|
|
788
992
|
for (const searchPath of searchPaths) {
|
|
789
|
-
if (!
|
|
993
|
+
if (!existsSync2(searchPath)) {
|
|
790
994
|
continue;
|
|
791
995
|
}
|
|
792
996
|
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -874,11 +1078,13 @@ function formatDate2(isoString) {
|
|
|
874
1078
|
}
|
|
875
1079
|
|
|
876
1080
|
// src/cli/commands/show.ts
|
|
877
|
-
import { existsSync as
|
|
1081
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1082
|
+
import { readFile } from "fs/promises";
|
|
1083
|
+
import { join as join3 } from "path";
|
|
878
1084
|
async function findTrajectory(id) {
|
|
879
1085
|
const searchPaths = getSearchPaths();
|
|
880
1086
|
for (const searchPath of searchPaths) {
|
|
881
|
-
if (!
|
|
1087
|
+
if (!existsSync3(searchPath)) {
|
|
882
1088
|
continue;
|
|
883
1089
|
}
|
|
884
1090
|
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -900,13 +1106,79 @@ async function findTrajectory(id) {
|
|
|
900
1106
|
}
|
|
901
1107
|
return null;
|
|
902
1108
|
}
|
|
1109
|
+
async function findTraceFile(id) {
|
|
1110
|
+
const searchPaths = getSearchPaths();
|
|
1111
|
+
for (const searchPath of searchPaths) {
|
|
1112
|
+
if (!existsSync3(searchPath)) {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const completedDir = join3(searchPath, "completed");
|
|
1116
|
+
if (!existsSync3(completedDir)) {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const { readdirSync } = await import("fs");
|
|
1121
|
+
const months = readdirSync(completedDir);
|
|
1122
|
+
for (const month of months) {
|
|
1123
|
+
const tracePath = join3(completedDir, month, `${id}.trace.json`);
|
|
1124
|
+
if (existsSync3(tracePath)) {
|
|
1125
|
+
const content = await readFile(tracePath, "utf-8");
|
|
1126
|
+
return JSON.parse(content);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
903
1134
|
function registerShowCommand(program2) {
|
|
904
|
-
program2.command("show <id>").description("Show trajectory details").option("-d, --decisions", "Show decisions only").action(async (id, options) => {
|
|
1135
|
+
program2.command("show <id>").description("Show trajectory details").option("-d, --decisions", "Show decisions only").option("-t, --trace", "Show trace information").action(async (id, options) => {
|
|
905
1136
|
const trajectory = await findTrajectory(id);
|
|
906
1137
|
if (!trajectory) {
|
|
907
1138
|
console.error(`Error: Trajectory not found: ${id}`);
|
|
908
1139
|
throw new Error("Trajectory not found");
|
|
909
1140
|
}
|
|
1141
|
+
if (options.trace) {
|
|
1142
|
+
console.log(`Trace for ${trajectory.task.title}:
|
|
1143
|
+
`);
|
|
1144
|
+
if (trajectory._trace) {
|
|
1145
|
+
console.log("Trace Reference:");
|
|
1146
|
+
console.log(` Start Ref: ${trajectory._trace.startRef}`);
|
|
1147
|
+
if (trajectory._trace.endRef) {
|
|
1148
|
+
console.log(` End Ref: ${trajectory._trace.endRef}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (trajectory._trace.traceId) {
|
|
1151
|
+
console.log(` Trace ID: ${trajectory._trace.traceId}`);
|
|
1152
|
+
}
|
|
1153
|
+
console.log("");
|
|
1154
|
+
}
|
|
1155
|
+
const trace = await findTraceFile(id);
|
|
1156
|
+
if (trace) {
|
|
1157
|
+
console.log("Trace Details:");
|
|
1158
|
+
console.log(` ID: ${trace.id}`);
|
|
1159
|
+
console.log(` Timestamp: ${trace.timestamp}`);
|
|
1160
|
+
console.log(` Files: ${trace.files.length}`);
|
|
1161
|
+
console.log("");
|
|
1162
|
+
if (trace.files.length > 0) {
|
|
1163
|
+
console.log("Modified Files:");
|
|
1164
|
+
for (const file of trace.files) {
|
|
1165
|
+
const rangeCount = file.conversations.reduce(
|
|
1166
|
+
(sum, conv) => sum + conv.ranges.length,
|
|
1167
|
+
0
|
|
1168
|
+
);
|
|
1169
|
+
const model = file.conversations[0]?.contributor.model ?? "unknown";
|
|
1170
|
+
console.log(` \u2022 ${file.path}`);
|
|
1171
|
+
console.log(` Ranges: ${rangeCount}, Model: ${model}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} else if (!trajectory._trace) {
|
|
1175
|
+
console.log("No trace information available");
|
|
1176
|
+
console.log(
|
|
1177
|
+
"Trace is captured when starting a trajectory in a git repo"
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
910
1182
|
if (options.decisions) {
|
|
911
1183
|
const decisions = extractDecisions(trajectory);
|
|
912
1184
|
if (decisions.length === 0) {
|
|
@@ -920,7 +1192,10 @@ function registerShowCommand(program2) {
|
|
|
920
1192
|
console.log(` Chose: ${decision.chosen}`);
|
|
921
1193
|
console.log(` Reasoning: ${decision.reasoning}`);
|
|
922
1194
|
if (decision.alternatives.length > 0) {
|
|
923
|
-
|
|
1195
|
+
const altStrings = decision.alternatives.map(
|
|
1196
|
+
(a) => typeof a === "string" ? a : a.option
|
|
1197
|
+
);
|
|
1198
|
+
console.log(` Alternatives: ${altStrings.join(", ")}`);
|
|
924
1199
|
}
|
|
925
1200
|
console.log("");
|
|
926
1201
|
}
|
|
@@ -1002,11 +1277,18 @@ function registerStartCommand(program2) {
|
|
|
1002
1277
|
}
|
|
1003
1278
|
const agentName = options.agent ?? process.env.TRAJECTORIES_AGENT ?? void 0;
|
|
1004
1279
|
const projectId = options.project ?? process.env.TRAJECTORIES_PROJECT ?? void 0;
|
|
1280
|
+
const startRef = captureGitState();
|
|
1005
1281
|
let trajectory = createTrajectory({
|
|
1006
1282
|
title,
|
|
1007
1283
|
source,
|
|
1008
1284
|
projectId
|
|
1009
1285
|
});
|
|
1286
|
+
if (startRef) {
|
|
1287
|
+
trajectory = {
|
|
1288
|
+
...trajectory,
|
|
1289
|
+
_trace: createTraceRef(startRef)
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1010
1292
|
if (agentName) {
|
|
1011
1293
|
trajectory = addChapter(trajectory, {
|
|
1012
1294
|
title: "Initial work",
|