vibe-coding-master 0.2.0 → 0.2.2
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 +22 -12
- package/dist/backend/adapters/claude-adapter.js +0 -3
- package/dist/backend/adapters/filesystem.js +14 -0
- package/dist/backend/api/round-routes.js +1 -10
- package/dist/backend/api/translation-routes.js +8 -0
- package/dist/backend/runtime/node-pty-runtime.js +21 -1
- package/dist/backend/server.js +9 -9
- package/dist/backend/services/app-settings-service.js +4 -1
- package/dist/backend/services/claude-hook-service.js +36 -0
- package/dist/backend/services/harness-service.js +12 -8
- package/dist/backend/services/round-service.js +309 -57
- package/dist/backend/services/session-service.js +14 -2
- package/dist/backend/services/translation-service.js +280 -13
- package/dist/backend/templates/harness/reviewer-agent.js +2 -0
- package/dist/shared/types/translation.js +1 -0
- package/dist-frontend/assets/index-CfH8XNnx.js +90 -0
- package/dist-frontend/assets/index-CfqduxVB.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/full-harness-baseline.md +8 -4
- package/docs/product-design.md +36 -17
- package/docs/v0.2-implementation-plan.md +4 -4
- package/docs/vcm-cc-best-practices.md +4 -4
- package/package.json +1 -1
- package/scripts/install-vcm-harness.mjs +94 -9
- package/dist-frontend/assets/index-CrY5Ryps.js +0 -90
- package/dist-frontend/assets/index-CvvtrrCN.css +0 -32
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { TRANSLATION_PROMPT_KEYS } from "../../shared/types/translation.js";
|
|
2
|
+
import { TRANSLATION_ENTRY_RETENTION_LIMIT, TRANSLATION_PROMPT_KEYS } from "../../shared/types/translation.js";
|
|
3
3
|
import { TranslationProviderError } from "../adapters/translation-provider.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -17,11 +17,11 @@ const DEFAULT_SETTINGS = {
|
|
|
17
17
|
inputMode: "review-before-send",
|
|
18
18
|
translateOutput: true,
|
|
19
19
|
translateUserInput: true,
|
|
20
|
-
contextEnabled:
|
|
20
|
+
contextEnabled: false,
|
|
21
21
|
preserveTechnicalTokens: true,
|
|
22
22
|
skipCjkText: true,
|
|
23
23
|
redactSecrets: true,
|
|
24
|
-
requestTimeoutMs:
|
|
24
|
+
requestTimeoutMs: 120000,
|
|
25
25
|
temperature: 0.1
|
|
26
26
|
};
|
|
27
27
|
const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
|
|
@@ -62,6 +62,7 @@ export function createTranslationService(deps) {
|
|
|
62
62
|
listeners: new Set(),
|
|
63
63
|
seenTranscriptIds: new Set(),
|
|
64
64
|
entries: [],
|
|
65
|
+
failures: new Map(),
|
|
65
66
|
status: "ready",
|
|
66
67
|
events: [],
|
|
67
68
|
nextSeq: 1
|
|
@@ -92,6 +93,11 @@ export function createTranslationService(deps) {
|
|
|
92
93
|
appendEvent(sessionId, { type: "error", id, message });
|
|
93
94
|
emit(sessionId, { type: "translation-error", id, message });
|
|
94
95
|
}
|
|
96
|
+
function publishFailures(sessionId) {
|
|
97
|
+
const failures = getFailureItems(getState(sessionId));
|
|
98
|
+
appendEvent(sessionId, { type: "failures", failures });
|
|
99
|
+
emit(sessionId, { type: "translation-failures", failures });
|
|
100
|
+
}
|
|
95
101
|
function appendEvent(sessionId, input) {
|
|
96
102
|
const state = getState(sessionId);
|
|
97
103
|
const event = {
|
|
@@ -117,6 +123,7 @@ export function createTranslationService(deps) {
|
|
|
117
123
|
if (!state.cacheLoaded) {
|
|
118
124
|
await loadCachedEvents(state);
|
|
119
125
|
state.cacheLoaded = true;
|
|
126
|
+
pruneTranslationEntries(input.sessionId);
|
|
120
127
|
}
|
|
121
128
|
await deps.fs.ensureDir(path.dirname(cachePath));
|
|
122
129
|
return state;
|
|
@@ -150,6 +157,9 @@ export function createTranslationService(deps) {
|
|
|
150
157
|
else if (event.type === "error") {
|
|
151
158
|
state.status = "failed";
|
|
152
159
|
}
|
|
160
|
+
else if (event.type === "failures") {
|
|
161
|
+
state.failures = new Map(event.failures.map((failure) => [failure.translationId, failure]));
|
|
162
|
+
}
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
async function persistEvents(state) {
|
|
@@ -216,14 +226,20 @@ export function createTranslationService(deps) {
|
|
|
216
226
|
}
|
|
217
227
|
}
|
|
218
228
|
function processClaudeOutputText(sessionId, rawText, config, entryId) {
|
|
229
|
+
return startClaudeOutputTranslation(sessionId, rawText, config, {
|
|
230
|
+
entryId,
|
|
231
|
+
replaceExisting: false
|
|
232
|
+
}) !== undefined;
|
|
233
|
+
}
|
|
234
|
+
function startClaudeOutputTranslation(sessionId, rawText, config, options) {
|
|
219
235
|
const session = deps.runtime.getSession(sessionId);
|
|
220
236
|
const roleSession = deps.sessionRegistry.get(sessionId);
|
|
221
237
|
if (!session && !roleSession) {
|
|
222
|
-
return
|
|
238
|
+
return undefined;
|
|
223
239
|
}
|
|
224
240
|
const { settings, secrets } = config;
|
|
225
241
|
if (!rawText.trim()) {
|
|
226
|
-
return
|
|
242
|
+
return undefined;
|
|
227
243
|
}
|
|
228
244
|
const text = rawText;
|
|
229
245
|
const baseEntry = {
|
|
@@ -236,11 +252,16 @@ export function createTranslationService(deps) {
|
|
|
236
252
|
settings,
|
|
237
253
|
status: "translating",
|
|
238
254
|
contextUsed: false,
|
|
239
|
-
id: entryId
|
|
255
|
+
id: options.entryId
|
|
240
256
|
}),
|
|
241
257
|
translationStartedAt: now()
|
|
242
258
|
};
|
|
243
|
-
|
|
259
|
+
if (options.replaceExisting) {
|
|
260
|
+
replaceEntry(sessionId, baseEntry);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
pushEntry(sessionId, baseEntry);
|
|
264
|
+
}
|
|
244
265
|
const queue = queues.getQueue(sessionId);
|
|
245
266
|
void queue.enqueue(async () => {
|
|
246
267
|
publishStatus(sessionId, "translating");
|
|
@@ -265,6 +286,7 @@ export function createTranslationService(deps) {
|
|
|
265
286
|
tokenUsage: result.tokenUsage
|
|
266
287
|
};
|
|
267
288
|
replaceEntry(sessionId, completed);
|
|
289
|
+
clearFailure(sessionId, completed.id);
|
|
268
290
|
getState(sessionId).lastAssistantText = text;
|
|
269
291
|
publishStatus(sessionId, "ready");
|
|
270
292
|
}
|
|
@@ -276,15 +298,23 @@ export function createTranslationService(deps) {
|
|
|
276
298
|
completedAt: now()
|
|
277
299
|
};
|
|
278
300
|
replaceEntry(sessionId, failed);
|
|
301
|
+
recordFailure(sessionId, failed);
|
|
279
302
|
publishStatus(sessionId, "failed");
|
|
280
303
|
}
|
|
281
304
|
}).catch((error) => {
|
|
282
305
|
publishError(sessionId, error instanceof Error ? error.message : "Translation failed.");
|
|
283
306
|
});
|
|
284
|
-
return
|
|
307
|
+
return baseEntry;
|
|
285
308
|
}
|
|
286
309
|
function pushEntry(sessionId, entry) {
|
|
287
310
|
getState(sessionId).entries.push(entry);
|
|
311
|
+
pruneTranslationEntries(sessionId, new Set([entry.id]));
|
|
312
|
+
publishEntry(sessionId, entry);
|
|
313
|
+
}
|
|
314
|
+
function upsertAndPublishEntry(sessionId, entry) {
|
|
315
|
+
const state = getState(sessionId);
|
|
316
|
+
state.entries = upsertEntry(state.entries, entry);
|
|
317
|
+
pruneTranslationEntries(sessionId, new Set([entry.id]));
|
|
288
318
|
publishEntry(sessionId, entry);
|
|
289
319
|
}
|
|
290
320
|
function pushPreservedTranscriptEntry(sessionId, entryId, sourceText, settings) {
|
|
@@ -311,9 +341,100 @@ export function createTranslationService(deps) {
|
|
|
311
341
|
}
|
|
312
342
|
function replaceEntry(sessionId, entry) {
|
|
313
343
|
const state = getState(sessionId);
|
|
314
|
-
state.entries = state.entries
|
|
344
|
+
state.entries = upsertEntry(state.entries, entry);
|
|
345
|
+
pruneTranslationEntries(sessionId, new Set([entry.id]));
|
|
315
346
|
publishEntry(sessionId, entry);
|
|
316
347
|
}
|
|
348
|
+
function recordFailure(sessionId, entry) {
|
|
349
|
+
if (!isRetryableFailedEntry(entry)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const state = getState(sessionId);
|
|
353
|
+
const existing = state.failures.get(entry.id);
|
|
354
|
+
state.failures.set(entry.id, {
|
|
355
|
+
translationId: entry.id,
|
|
356
|
+
sessionId,
|
|
357
|
+
taskSlug: entry.taskSlug,
|
|
358
|
+
role: entry.role,
|
|
359
|
+
sourceText: entry.sourceText,
|
|
360
|
+
error: entry.error ?? "Translation failed.",
|
|
361
|
+
failedAt: entry.completedAt ?? now(),
|
|
362
|
+
retryCount: existing?.retryCount ?? 0,
|
|
363
|
+
lastRetryAt: existing?.lastRetryAt
|
|
364
|
+
});
|
|
365
|
+
publishFailures(sessionId);
|
|
366
|
+
}
|
|
367
|
+
function clearFailure(sessionId, translationId) {
|
|
368
|
+
const state = getState(sessionId);
|
|
369
|
+
if (state.failures.delete(translationId)) {
|
|
370
|
+
publishFailures(sessionId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function pruneTranslationEntries(sessionId, protectedIds = new Set()) {
|
|
374
|
+
const state = getState(sessionId);
|
|
375
|
+
const overflow = state.entries.length - TRANSLATION_ENTRY_RETENTION_LIMIT;
|
|
376
|
+
if (overflow <= 0) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const removedIds = new Set();
|
|
380
|
+
for (const entry of state.entries) {
|
|
381
|
+
if (removedIds.size >= overflow) {
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
if (protectedIds.has(entry.id) || isActiveTranslationEntry(entry)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
removedIds.add(entry.id);
|
|
388
|
+
}
|
|
389
|
+
if (removedIds.size === 0) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
state.entries = state.entries.filter((entry) => !removedIds.has(entry.id));
|
|
393
|
+
state.events = pruneTranslationEntryEvents(state.events, removedIds);
|
|
394
|
+
void persistEvents(state);
|
|
395
|
+
let failuresChanged = false;
|
|
396
|
+
for (const entryId of removedIds) {
|
|
397
|
+
failuresChanged = state.failures.delete(entryId) || failuresChanged;
|
|
398
|
+
}
|
|
399
|
+
if (failuresChanged) {
|
|
400
|
+
publishFailures(sessionId);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function markFailureRetrying(sessionId, failure) {
|
|
404
|
+
const retrying = {
|
|
405
|
+
...failure,
|
|
406
|
+
retryCount: failure.retryCount + 1,
|
|
407
|
+
lastRetryAt: now()
|
|
408
|
+
};
|
|
409
|
+
getState(sessionId).failures.set(failure.translationId, retrying);
|
|
410
|
+
return retrying;
|
|
411
|
+
}
|
|
412
|
+
function retryOneTranslation(sessionId, original, config) {
|
|
413
|
+
const state = getState(sessionId);
|
|
414
|
+
const existingFailure = state.failures.get(original.id) ?? {
|
|
415
|
+
translationId: original.id,
|
|
416
|
+
sessionId,
|
|
417
|
+
taskSlug: original.taskSlug,
|
|
418
|
+
role: original.role,
|
|
419
|
+
sourceText: original.sourceText,
|
|
420
|
+
error: original.error ?? "Translation failed.",
|
|
421
|
+
failedAt: original.completedAt ?? now(),
|
|
422
|
+
retryCount: 0
|
|
423
|
+
};
|
|
424
|
+
markFailureRetrying(sessionId, existingFailure);
|
|
425
|
+
const retrying = startClaudeOutputTranslation(sessionId, original.sourceText, config, {
|
|
426
|
+
entryId: original.id,
|
|
427
|
+
replaceExisting: true
|
|
428
|
+
});
|
|
429
|
+
if (!retrying) {
|
|
430
|
+
throw new VcmError({
|
|
431
|
+
code: "TRANSLATION_RETRY_UNSUPPORTED",
|
|
432
|
+
message: "Translation entry cannot be retried.",
|
|
433
|
+
statusCode: 400
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return retrying;
|
|
437
|
+
}
|
|
317
438
|
function createEntry(input) {
|
|
318
439
|
return {
|
|
319
440
|
id: input.id ?? id(),
|
|
@@ -327,6 +448,9 @@ export function createTranslationService(deps) {
|
|
|
327
448
|
translatedText: input.translatedText ?? "",
|
|
328
449
|
status: input.status,
|
|
329
450
|
contextUsed: input.contextUsed,
|
|
451
|
+
boundaryKind: input.boundaryKind,
|
|
452
|
+
conversationTurn: input.conversationTurn,
|
|
453
|
+
occurredAt: input.occurredAt,
|
|
330
454
|
createdAt: now(),
|
|
331
455
|
completedAt: input.completedAt,
|
|
332
456
|
provider: input.settings.providerType,
|
|
@@ -413,6 +537,44 @@ export function createTranslationService(deps) {
|
|
|
413
537
|
events
|
|
414
538
|
};
|
|
415
539
|
},
|
|
540
|
+
async recordConversationBoundary(input) {
|
|
541
|
+
const { settings } = await loadConfig();
|
|
542
|
+
const state = await prepareCache({
|
|
543
|
+
repoRoot: input.taskRepoRoot ?? input.repoRoot,
|
|
544
|
+
taskSlug: input.taskSlug,
|
|
545
|
+
role: input.role,
|
|
546
|
+
sessionId: input.sessionId
|
|
547
|
+
});
|
|
548
|
+
const conversationTurn = resolveConversationBoundaryTurn(state, input.boundaryKind);
|
|
549
|
+
if (conversationTurn === undefined) {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
const entryId = `boundary:${input.sessionId}:${conversationTurn}:${input.boundaryKind}`;
|
|
553
|
+
const existing = state.entries.find((entry) => entry.id === entryId);
|
|
554
|
+
if (existing) {
|
|
555
|
+
return existing;
|
|
556
|
+
}
|
|
557
|
+
const occurredAt = input.occurredAt ?? now();
|
|
558
|
+
const sourceText = formatConversationBoundaryText(input.boundaryKind, conversationTurn, occurredAt);
|
|
559
|
+
const entry = createEntry({
|
|
560
|
+
taskSlug: input.taskSlug,
|
|
561
|
+
role: input.role,
|
|
562
|
+
direction: "cc-output-to-user",
|
|
563
|
+
sourceKind: "conversation-boundary",
|
|
564
|
+
sourceText,
|
|
565
|
+
settings,
|
|
566
|
+
status: "preserved",
|
|
567
|
+
contextUsed: false,
|
|
568
|
+
id: entryId,
|
|
569
|
+
translatedText: sourceText,
|
|
570
|
+
completedAt: occurredAt,
|
|
571
|
+
boundaryKind: input.boundaryKind,
|
|
572
|
+
conversationTurn,
|
|
573
|
+
occurredAt
|
|
574
|
+
});
|
|
575
|
+
upsertAndPublishEntry(input.sessionId, entry);
|
|
576
|
+
return entry;
|
|
577
|
+
},
|
|
416
578
|
async translateUserInput(input) {
|
|
417
579
|
const { settings, secrets } = await loadConfig();
|
|
418
580
|
if (!input.text.trim()) {
|
|
@@ -524,6 +686,7 @@ export function createTranslationService(deps) {
|
|
|
524
686
|
for (const entry of state.entries) {
|
|
525
687
|
listener({ type: "translation-entry", entry });
|
|
526
688
|
}
|
|
689
|
+
listener({ type: "translation-failures", failures: getFailureItems(state) });
|
|
527
690
|
if (!state.unsubscribeTranscript) {
|
|
528
691
|
if (!roleSession) {
|
|
529
692
|
listener({
|
|
@@ -545,6 +708,7 @@ export function createTranslationService(deps) {
|
|
|
545
708
|
async clearSession(sessionId) {
|
|
546
709
|
const state = getState(sessionId);
|
|
547
710
|
state.entries = [];
|
|
711
|
+
state.failures.clear();
|
|
548
712
|
state.events = [];
|
|
549
713
|
state.nextSeq = 1;
|
|
550
714
|
queues.clearQueue(sessionId);
|
|
@@ -577,16 +741,45 @@ export function createTranslationService(deps) {
|
|
|
577
741
|
statusCode: 404
|
|
578
742
|
});
|
|
579
743
|
}
|
|
580
|
-
if (original
|
|
744
|
+
if (!isRetryableFailedEntry(original)) {
|
|
581
745
|
throw new VcmError({
|
|
582
746
|
code: "TRANSLATION_RETRY_UNSUPPORTED",
|
|
583
|
-
message: "Only Claude Code output translation entries can be retried.",
|
|
747
|
+
message: "Only failed Claude Code output prose translation entries can be retried.",
|
|
584
748
|
statusCode: 400
|
|
585
749
|
});
|
|
586
750
|
}
|
|
587
751
|
const config = await loadConfig();
|
|
588
|
-
|
|
589
|
-
|
|
752
|
+
const retrying = retryOneTranslation(sessionId, original, config);
|
|
753
|
+
publishFailures(sessionId);
|
|
754
|
+
return retrying;
|
|
755
|
+
},
|
|
756
|
+
async retryFailedTranslations(sessionId) {
|
|
757
|
+
const state = getState(sessionId);
|
|
758
|
+
const failures = getFailureItems(state);
|
|
759
|
+
const config = await loadConfig();
|
|
760
|
+
let changed = false;
|
|
761
|
+
for (const failure of failures) {
|
|
762
|
+
const original = state.entries.find((entry) => entry.id === failure.translationId);
|
|
763
|
+
if (!original || !isRetryableFailedEntry(original)) {
|
|
764
|
+
state.failures.delete(failure.translationId);
|
|
765
|
+
changed = true;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
retryOneTranslation(sessionId, original, config);
|
|
769
|
+
changed = true;
|
|
770
|
+
}
|
|
771
|
+
if (changed) {
|
|
772
|
+
publishFailures(sessionId);
|
|
773
|
+
}
|
|
774
|
+
return { failures: getFailureItems(state) };
|
|
775
|
+
},
|
|
776
|
+
async ignoreTranslationFailures(sessionId) {
|
|
777
|
+
const state = getState(sessionId);
|
|
778
|
+
if (state.failures.size > 0) {
|
|
779
|
+
state.failures.clear();
|
|
780
|
+
publishFailures(sessionId);
|
|
781
|
+
}
|
|
782
|
+
return { failures: [] };
|
|
590
783
|
}
|
|
591
784
|
};
|
|
592
785
|
async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
|
|
@@ -658,6 +851,80 @@ function formatUnknown(value) {
|
|
|
658
851
|
return String(value);
|
|
659
852
|
}
|
|
660
853
|
}
|
|
854
|
+
function resolveConversationBoundaryTurn(state, boundaryKind) {
|
|
855
|
+
const openTurn = getOpenConversationTurn(state);
|
|
856
|
+
if (boundaryKind === "start") {
|
|
857
|
+
return openTurn ?? getMaxConversationTurn(state) + 1;
|
|
858
|
+
}
|
|
859
|
+
return openTurn;
|
|
860
|
+
}
|
|
861
|
+
function getOpenConversationTurn(state) {
|
|
862
|
+
const startTurns = getBoundaryEntries(state)
|
|
863
|
+
.filter((entry) => entry.boundaryKind === "start")
|
|
864
|
+
.map((entry) => entry.conversationTurn)
|
|
865
|
+
.filter((turn) => typeof turn === "number" && Number.isFinite(turn))
|
|
866
|
+
.sort((left, right) => right - left);
|
|
867
|
+
return startTurns.find((turn) => !hasBoundaryEntry(state, turn, "end"));
|
|
868
|
+
}
|
|
869
|
+
function getMaxConversationTurn(state) {
|
|
870
|
+
return Math.max(0, ...getBoundaryEntries(state)
|
|
871
|
+
.map((entry) => entry.conversationTurn)
|
|
872
|
+
.filter((turn) => typeof turn === "number" && Number.isFinite(turn)));
|
|
873
|
+
}
|
|
874
|
+
function hasBoundaryEntry(state, conversationTurn, boundaryKind) {
|
|
875
|
+
return getBoundaryEntries(state).some((entry) => entry.boundaryKind === boundaryKind &&
|
|
876
|
+
entry.conversationTurn === conversationTurn);
|
|
877
|
+
}
|
|
878
|
+
function getBoundaryEntries(state) {
|
|
879
|
+
return state.entries.filter((entry) => entry.sourceKind === "conversation-boundary");
|
|
880
|
+
}
|
|
881
|
+
function getFailureItems(state) {
|
|
882
|
+
return Array.from(state.failures.values());
|
|
883
|
+
}
|
|
884
|
+
function isActiveTranslationEntry(entry) {
|
|
885
|
+
return entry.status === "queued" || entry.status === "translating";
|
|
886
|
+
}
|
|
887
|
+
function isRetryableFailedEntry(entry) {
|
|
888
|
+
return entry.status === "failed"
|
|
889
|
+
&& entry.direction === "cc-output-to-user"
|
|
890
|
+
&& entry.sourceKind === "prose";
|
|
891
|
+
}
|
|
892
|
+
function pruneTranslationEntryEvents(events, removedIds) {
|
|
893
|
+
const pruned = [];
|
|
894
|
+
for (const event of events) {
|
|
895
|
+
if (event.type === "entry") {
|
|
896
|
+
if (!removedIds.has(event.entry.id)) {
|
|
897
|
+
pruned.push(event);
|
|
898
|
+
}
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (event.type === "failures") {
|
|
902
|
+
pruned.push({
|
|
903
|
+
...event,
|
|
904
|
+
failures: event.failures.filter((failure) => !removedIds.has(failure.translationId))
|
|
905
|
+
});
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
pruned.push(event);
|
|
909
|
+
}
|
|
910
|
+
return pruned;
|
|
911
|
+
}
|
|
912
|
+
function formatConversationBoundaryText(boundaryKind, conversationTurn, occurredAt) {
|
|
913
|
+
const label = boundaryKind === "start" ? "开始" : "结束";
|
|
914
|
+
return `-------${label}---第 ${conversationTurn} 轮----${formatBoundaryTime(occurredAt)}---------------`;
|
|
915
|
+
}
|
|
916
|
+
function formatBoundaryTime(value) {
|
|
917
|
+
const date = new Date(value);
|
|
918
|
+
if (Number.isNaN(date.getTime())) {
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
922
|
+
hour: "2-digit",
|
|
923
|
+
minute: "2-digit",
|
|
924
|
+
second: "2-digit",
|
|
925
|
+
hour12: false
|
|
926
|
+
}).format(date);
|
|
927
|
+
}
|
|
661
928
|
function upsertEntry(entries, entry) {
|
|
662
929
|
const index = entries.findIndex((current) => current.id === entry.id);
|
|
663
930
|
if (index === -1) {
|
|
@@ -23,6 +23,8 @@ export function renderReviewerHarnessRules() {
|
|
|
23
23
|
|
|
24
24
|
- Independently design the test coverage needed to prove the implemented behavior, including missing cases beyond coder's baseline tests.
|
|
25
25
|
- Decide whether stronger L1/L2/L3 validation is needed for final confidence.
|
|
26
|
+
- Before final validation, perform a full cache cleanup, then rerun validation from a clean state.
|
|
27
|
+
- Do not use validation results produced before full cache cleanup as final acceptance evidence.
|
|
26
28
|
- Add or modify tests needed for test adequacy.
|
|
27
29
|
- Do not skip smoke, integration, or E2E checks only because coder did not run them.
|
|
28
30
|
|