trekoon 0.1.6 → 0.1.8
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/.agents/skills/trekoon/SKILL.md +39 -15
- package/README.md +124 -10
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +13 -0
- package/src/commands/dep.ts +20 -1
- package/src/commands/epic.ts +72 -7
- package/src/commands/help.ts +255 -17
- package/src/commands/quickstart.ts +88 -24
- package/src/commands/skills.ts +177 -14
- package/src/commands/subtask.ts +76 -6
- package/src/commands/sync.ts +4 -0
- package/src/commands/task.ts +299 -7
- package/src/domain/tracker-domain.ts +113 -7
- package/src/domain/types.ts +7 -0
- package/src/runtime/cli-shell.ts +1 -2
- package/src/runtime/version.ts +20 -0
- package/src/storage/migrations.ts +48 -1
- package/src/sync/service.ts +67 -9
- package/src/sync/types.ts +9 -0
package/src/commands/skills.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
1
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
import { hasFlag, parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
|
|
@@ -9,10 +9,11 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
|
9
9
|
|
|
10
10
|
const SKILLS_USAGE = [
|
|
11
11
|
"Usage:",
|
|
12
|
-
" trekoon skills install [--link --editor opencode|claude|pi] [--to <path>]",
|
|
12
|
+
" trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
|
|
13
13
|
" trekoon skills update",
|
|
14
14
|
].join("\n");
|
|
15
15
|
const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
|
|
16
|
+
const ALLOW_OUTSIDE_REPO_FLAG = "allow-outside-repo";
|
|
16
17
|
|
|
17
18
|
type EditorName = (typeof EDITOR_NAMES)[number];
|
|
18
19
|
type LinkStateStatus = "missing" | "valid" | "conflict";
|
|
@@ -23,6 +24,12 @@ interface InstallOutcome {
|
|
|
23
24
|
readonly installedDir: string;
|
|
24
25
|
readonly linkPath: string | null;
|
|
25
26
|
readonly linkTarget: string | null;
|
|
27
|
+
readonly outsideRepoLink: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LinkTargetValidation {
|
|
31
|
+
readonly linkRoot: string;
|
|
32
|
+
readonly outsideRepoLink: boolean;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
interface LinkState {
|
|
@@ -96,6 +103,114 @@ function resolveLinkRoot(cwd: string, editor: EditorName, toOverride: string | u
|
|
|
96
103
|
return join(cwd, ".pi", "skills");
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
function isPathInsideRoot(pathValue: string, rootPath: string): boolean {
|
|
107
|
+
const normalizedPath: string = resolve(pathValue);
|
|
108
|
+
const normalizedRoot: string = resolve(rootPath);
|
|
109
|
+
const relativePath: string = relative(normalizedRoot, normalizedPath);
|
|
110
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function realpathNearestExistingAncestor(pathValue: string): string {
|
|
114
|
+
let cursor: string = resolve(pathValue);
|
|
115
|
+
|
|
116
|
+
while (!existsSync(cursor)) {
|
|
117
|
+
const parent: string = dirname(cursor);
|
|
118
|
+
if (parent === cursor) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
cursor = parent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return realpathSync(cursor);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateLinkRoot(
|
|
129
|
+
cwd: string,
|
|
130
|
+
editor: EditorName,
|
|
131
|
+
toOverride: string | undefined,
|
|
132
|
+
allowOutsideRepo: boolean,
|
|
133
|
+
): CliResult | LinkTargetValidation {
|
|
134
|
+
const linkRoot: string = resolveLinkRoot(cwd, editor, toOverride);
|
|
135
|
+
const repoRoot: string = realpathSync(cwd);
|
|
136
|
+
const effectiveTargetRoot: string = realpathNearestExistingAncestor(linkRoot);
|
|
137
|
+
const insideRepo: boolean = isPathInsideRoot(effectiveTargetRoot, repoRoot);
|
|
138
|
+
|
|
139
|
+
if (insideRepo) {
|
|
140
|
+
return {
|
|
141
|
+
linkRoot,
|
|
142
|
+
outsideRepoLink: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!allowOutsideRepo) {
|
|
147
|
+
return failResult({
|
|
148
|
+
command: "skills.install",
|
|
149
|
+
human: [
|
|
150
|
+
"Refusing to link skills outside repository root by default.",
|
|
151
|
+
`Requested link root: ${linkRoot}`,
|
|
152
|
+
`Resolved existing target ancestor: ${effectiveTargetRoot}`,
|
|
153
|
+
`Repository root: ${repoRoot}`,
|
|
154
|
+
`If intentional, re-run with --${ALLOW_OUTSIDE_REPO_FLAG} to override.`,
|
|
155
|
+
].join("\n"),
|
|
156
|
+
data: {
|
|
157
|
+
code: "outside_repo_target",
|
|
158
|
+
linkRoot,
|
|
159
|
+
effectiveTargetRoot,
|
|
160
|
+
repoRoot,
|
|
161
|
+
overrideFlag: `--${ALLOW_OUTSIDE_REPO_FLAG}`,
|
|
162
|
+
},
|
|
163
|
+
error: {
|
|
164
|
+
code: "outside_repo_target",
|
|
165
|
+
message: "Link target is outside repository root",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
linkRoot,
|
|
172
|
+
outsideRepoLink: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function revalidateLinkParentBoundary(
|
|
177
|
+
repoRoot: string,
|
|
178
|
+
linkPath: string,
|
|
179
|
+
allowOutsideRepo: boolean,
|
|
180
|
+
): CliResult | null {
|
|
181
|
+
if (allowOutsideRepo) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const linkParentRealpath: string = realpathSync(dirname(linkPath));
|
|
186
|
+
const insideRepo: boolean = isPathInsideRoot(linkParentRealpath, repoRoot);
|
|
187
|
+
if (insideRepo) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return failResult({
|
|
192
|
+
command: "skills.install",
|
|
193
|
+
human: [
|
|
194
|
+
"Refusing to link skills outside repository root by default.",
|
|
195
|
+
`Requested link root: ${dirname(linkPath)}`,
|
|
196
|
+
`Resolved existing target ancestor: ${linkParentRealpath}`,
|
|
197
|
+
`Repository root: ${repoRoot}`,
|
|
198
|
+
`If intentional, re-run with --${ALLOW_OUTSIDE_REPO_FLAG} to override.`,
|
|
199
|
+
].join("\n"),
|
|
200
|
+
data: {
|
|
201
|
+
code: "outside_repo_target",
|
|
202
|
+
linkRoot: dirname(linkPath),
|
|
203
|
+
effectiveTargetRoot: linkParentRealpath,
|
|
204
|
+
repoRoot,
|
|
205
|
+
overrideFlag: `--${ALLOW_OUTSIDE_REPO_FLAG}`,
|
|
206
|
+
},
|
|
207
|
+
error: {
|
|
208
|
+
code: "outside_repo_target",
|
|
209
|
+
message: "Link target is outside repository root",
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
99
214
|
function resolveDefaultLinkPath(cwd: string, editor: EditorName): string {
|
|
100
215
|
return join(resolveLinkRoot(cwd, editor, undefined), "trekoon");
|
|
101
216
|
}
|
|
@@ -146,9 +261,18 @@ function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; i
|
|
|
146
261
|
};
|
|
147
262
|
}
|
|
148
263
|
|
|
149
|
-
function replaceOrCreateSymlink(
|
|
264
|
+
function replaceOrCreateSymlink(
|
|
265
|
+
linkPath: string,
|
|
266
|
+
targetPath: string,
|
|
267
|
+
repoRoot: string,
|
|
268
|
+
allowOutsideRepo: boolean,
|
|
269
|
+
): CliResult | null {
|
|
150
270
|
if (!existsSync(linkPath)) {
|
|
151
271
|
mkdirSync(dirname(linkPath), { recursive: true });
|
|
272
|
+
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
273
|
+
if (boundaryFailure) {
|
|
274
|
+
return boundaryFailure;
|
|
275
|
+
}
|
|
152
276
|
symlinkSync(targetPath, linkPath, "dir");
|
|
153
277
|
return null;
|
|
154
278
|
}
|
|
@@ -191,6 +315,10 @@ function replaceOrCreateSymlink(linkPath: string, targetPath: string): CliResult
|
|
|
191
315
|
}
|
|
192
316
|
|
|
193
317
|
rmSync(linkPath, { force: true });
|
|
318
|
+
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
319
|
+
if (boundaryFailure) {
|
|
320
|
+
return boundaryFailure;
|
|
321
|
+
}
|
|
194
322
|
symlinkSync(targetPath, linkPath, "dir");
|
|
195
323
|
return null;
|
|
196
324
|
}
|
|
@@ -259,9 +387,16 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
259
387
|
}
|
|
260
388
|
|
|
261
389
|
const wantsLink: boolean = hasFlag(parsed.flags, "link");
|
|
390
|
+
const allowOutsideRepo: boolean = hasFlag(parsed.flags, ALLOW_OUTSIDE_REPO_FLAG);
|
|
262
391
|
const rawEditor: string | undefined = readOption(parsed.options, "editor");
|
|
263
392
|
const rawTo: string | undefined = readOption(parsed.options, "to");
|
|
264
393
|
|
|
394
|
+
if (allowOutsideRepo && !wantsLink) {
|
|
395
|
+
return invalidInput("skills.install", `--${ALLOW_OUTSIDE_REPO_FLAG} requires --link.`, {
|
|
396
|
+
allowOutsideRepo,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
265
400
|
if (!wantsLink && rawEditor !== undefined) {
|
|
266
401
|
return invalidInput("skills.install", "--editor requires --link.", {
|
|
267
402
|
editor: rawEditor,
|
|
@@ -299,22 +434,42 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
299
434
|
let linkTarget: string | null = null;
|
|
300
435
|
|
|
301
436
|
if (wantsLink && editor !== undefined) {
|
|
302
|
-
const
|
|
437
|
+
const validation = validateLinkRoot(context.cwd, editor, rawTo, allowOutsideRepo);
|
|
438
|
+
if ("ok" in validation) {
|
|
439
|
+
return validation;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const linkRoot: string = validation.linkRoot;
|
|
303
443
|
linkPath = join(linkRoot, "trekoon");
|
|
304
444
|
linkTarget = installResult.installedDir;
|
|
305
|
-
const linkFailure = replaceOrCreateSymlink(
|
|
445
|
+
const linkFailure = replaceOrCreateSymlink(
|
|
446
|
+
linkPath,
|
|
447
|
+
linkTarget,
|
|
448
|
+
realpathSync(context.cwd),
|
|
449
|
+
allowOutsideRepo,
|
|
450
|
+
);
|
|
306
451
|
if (linkFailure) {
|
|
307
452
|
return linkFailure;
|
|
308
453
|
}
|
|
309
|
-
}
|
|
310
454
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
455
|
+
outcome = {
|
|
456
|
+
sourcePath: installResult.sourcePath,
|
|
457
|
+
installedPath: installResult.installedPath,
|
|
458
|
+
installedDir: installResult.installedDir,
|
|
459
|
+
linkPath,
|
|
460
|
+
linkTarget,
|
|
461
|
+
outsideRepoLink: validation.outsideRepoLink,
|
|
462
|
+
};
|
|
463
|
+
} else {
|
|
464
|
+
outcome = {
|
|
465
|
+
sourcePath: installResult.sourcePath,
|
|
466
|
+
installedPath: installResult.installedPath,
|
|
467
|
+
installedDir: installResult.installedDir,
|
|
468
|
+
linkPath,
|
|
469
|
+
linkTarget,
|
|
470
|
+
outsideRepoLink: false,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
318
473
|
} catch (error: unknown) {
|
|
319
474
|
const message = error instanceof Error ? error.message : "Unknown skills install failure";
|
|
320
475
|
return failResult({
|
|
@@ -335,6 +490,11 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
335
490
|
command: "skills.install",
|
|
336
491
|
human: outcome.linkPath
|
|
337
492
|
? [
|
|
493
|
+
...(outcome.outsideRepoLink
|
|
494
|
+
? [
|
|
495
|
+
`WARNING: Linking outside repository root because --${ALLOW_OUTSIDE_REPO_FLAG} was provided.`,
|
|
496
|
+
]
|
|
497
|
+
: []),
|
|
338
498
|
"Installed Trekoon skill and linked editor path.",
|
|
339
499
|
`Source: ${outcome.sourcePath}`,
|
|
340
500
|
`Installed file: ${outcome.installedPath}`,
|
|
@@ -353,6 +513,9 @@ function runSkillsInstall(context: CliContext): CliResult {
|
|
|
353
513
|
linked: outcome.linkPath !== null,
|
|
354
514
|
linkPath: outcome.linkPath,
|
|
355
515
|
linkTarget: outcome.linkTarget,
|
|
516
|
+
outsideRepoLink: outcome.outsideRepoLink,
|
|
517
|
+
outsideRepoOverrideUsed: outcome.outsideRepoLink,
|
|
518
|
+
outsideRepoOverrideFlag: outcome.outsideRepoLink ? `--${ALLOW_OUTSIDE_REPO_FLAG}` : null,
|
|
356
519
|
},
|
|
357
520
|
});
|
|
358
521
|
}
|
package/src/commands/subtask.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasFlag,
|
|
3
|
+
parseArgs,
|
|
4
|
+
parseStrictNonNegativeInt,
|
|
5
|
+
parseStrictPositiveInt,
|
|
6
|
+
readEnumOption,
|
|
7
|
+
readMissingOptionValue,
|
|
8
|
+
readOption,
|
|
9
|
+
} from "./arg-parser";
|
|
2
10
|
|
|
3
11
|
import { MutationService } from "../domain/mutation-service";
|
|
4
12
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
@@ -54,16 +62,44 @@ function filterSortAndLimitSubtasks(
|
|
|
54
62
|
subtasks: readonly SubtaskRecord[],
|
|
55
63
|
statuses: readonly string[] | undefined,
|
|
56
64
|
limit: number | undefined,
|
|
57
|
-
|
|
65
|
+
cursor: number,
|
|
66
|
+
): { subtasks: SubtaskRecord[]; pagination: { hasMore: boolean; nextCursor: string | null } } {
|
|
58
67
|
const allowedStatuses = statuses === undefined ? undefined : new Set(statuses);
|
|
59
68
|
const filtered = allowedStatuses === undefined ? [...subtasks] : subtasks.filter((subtask) => allowedStatuses.has(subtask.status));
|
|
60
|
-
const sorted = [...filtered].sort((left, right) =>
|
|
69
|
+
const sorted = [...filtered].sort((left, right) => {
|
|
70
|
+
const byStatus = subtaskStatusPriority(left.status) - subtaskStatusPriority(right.status);
|
|
71
|
+
if (byStatus !== 0) {
|
|
72
|
+
return byStatus;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const byCreatedAt = left.createdAt - right.createdAt;
|
|
76
|
+
if (byCreatedAt !== 0) {
|
|
77
|
+
return byCreatedAt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return left.id.localeCompare(right.id);
|
|
81
|
+
});
|
|
61
82
|
|
|
62
83
|
if (limit === undefined) {
|
|
63
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
subtasks: sorted,
|
|
86
|
+
pagination: {
|
|
87
|
+
hasMore: false,
|
|
88
|
+
nextCursor: null,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
64
91
|
}
|
|
65
92
|
|
|
66
|
-
|
|
93
|
+
const pagedSubtasks = sorted.slice(cursor, cursor + limit);
|
|
94
|
+
const nextIndex = cursor + pagedSubtasks.length;
|
|
95
|
+
const hasMore = nextIndex < sorted.length;
|
|
96
|
+
return {
|
|
97
|
+
subtasks: pagedSubtasks,
|
|
98
|
+
pagination: {
|
|
99
|
+
hasMore,
|
|
100
|
+
nextCursor: hasMore ? `${nextIndex}` : null,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
67
103
|
}
|
|
68
104
|
|
|
69
105
|
function appendLine(existing: string, line: string): string {
|
|
@@ -161,6 +197,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
161
197
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
162
198
|
readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
|
|
163
199
|
readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
|
|
200
|
+
readMissingOptionValue(parsed.missingOptionValues, "cursor") ??
|
|
164
201
|
readMissingOptionValue(parsed.missingOptionValues, "task", "t");
|
|
165
202
|
if (missingListOption !== undefined) {
|
|
166
203
|
return failMissingOptionValue("subtask.list", missingListOption);
|
|
@@ -171,6 +208,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
171
208
|
const includeAll = hasFlag(parsed.flags, "all");
|
|
172
209
|
const rawStatuses = readOption(parsed.options, "status", "s");
|
|
173
210
|
const rawLimit = readOption(parsed.options, "limit", "l");
|
|
211
|
+
const rawCursor = readOption(parsed.options, "cursor");
|
|
174
212
|
|
|
175
213
|
if (rawView !== undefined && view === undefined) {
|
|
176
214
|
return failResult({
|
|
@@ -208,6 +246,18 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
208
246
|
});
|
|
209
247
|
}
|
|
210
248
|
|
|
249
|
+
if (includeAll && rawCursor !== undefined) {
|
|
250
|
+
return failResult({
|
|
251
|
+
command: "subtask.list",
|
|
252
|
+
human: "Use either --all or --cursor, not both.",
|
|
253
|
+
data: { code: "invalid_input", flags: ["all", "cursor"] },
|
|
254
|
+
error: {
|
|
255
|
+
code: "invalid_input",
|
|
256
|
+
message: "--all and --cursor are mutually exclusive",
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
211
261
|
const statuses = parseStatusCsv(rawStatuses);
|
|
212
262
|
if (rawStatuses !== undefined && statuses !== undefined && statuses.length === 0) {
|
|
213
263
|
return failResult({
|
|
@@ -234,6 +284,19 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
234
284
|
});
|
|
235
285
|
}
|
|
236
286
|
|
|
287
|
+
const parsedCursor = parseStrictNonNegativeInt(rawCursor);
|
|
288
|
+
if (Number.isNaN(parsedCursor)) {
|
|
289
|
+
return failResult({
|
|
290
|
+
command: "subtask.list",
|
|
291
|
+
human: "Invalid --cursor value. Use an integer >= 0.",
|
|
292
|
+
data: { code: "invalid_input", cursor: rawCursor },
|
|
293
|
+
error: {
|
|
294
|
+
code: "invalid_input",
|
|
295
|
+
message: "Invalid --cursor value",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
237
300
|
const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
|
|
238
301
|
const selectedStatuses = includeAll
|
|
239
302
|
? undefined
|
|
@@ -241,7 +304,13 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
241
304
|
const selectedLimit = includeAll
|
|
242
305
|
? undefined
|
|
243
306
|
: parsedLimit ?? DEFAULT_SUBTASK_LIST_LIMIT;
|
|
244
|
-
const
|
|
307
|
+
const listed = filterSortAndLimitSubtasks(
|
|
308
|
+
domain.listSubtasks(taskId),
|
|
309
|
+
selectedStatuses,
|
|
310
|
+
selectedLimit,
|
|
311
|
+
parsedCursor ?? 0,
|
|
312
|
+
);
|
|
313
|
+
const subtasks = listed.subtasks;
|
|
245
314
|
const listView = view ?? "table";
|
|
246
315
|
const human =
|
|
247
316
|
subtasks.length === 0
|
|
@@ -254,6 +323,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
254
323
|
command: "subtask.list",
|
|
255
324
|
human,
|
|
256
325
|
data: { subtasks },
|
|
326
|
+
...(context.mode === "human" ? {} : { meta: { pagination: listed.pagination } }),
|
|
257
327
|
});
|
|
258
328
|
}
|
|
259
329
|
case "update": {
|
package/src/commands/sync.ts
CHANGED
|
@@ -113,6 +113,10 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
113
113
|
`Scanned events: ${summary.scannedEvents}`,
|
|
114
114
|
`Applied events: ${summary.appliedEvents}`,
|
|
115
115
|
`Created conflicts: ${summary.createdConflicts}`,
|
|
116
|
+
`Malformed payloads: ${summary.diagnostics.malformedPayloadEvents}`,
|
|
117
|
+
`Quarantined events: ${summary.diagnostics.quarantinedEvents}`,
|
|
118
|
+
`Conflict events: ${summary.diagnostics.conflictEvents}`,
|
|
119
|
+
...summary.diagnostics.errorHints,
|
|
116
120
|
].join("\n"),
|
|
117
121
|
data: summary,
|
|
118
122
|
});
|