viveworker 0.5.5 → 0.6.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 +178 -27
- package/package.json +7 -2
- package/scripts/a2a-cli.mjs +11 -6
- package/scripts/moltbook-api.mjs +319 -72
- package/scripts/share-cli.mjs +576 -0
- package/scripts/viveworker-bridge.mjs +61 -14
- package/scripts/viveworker.mjs +771 -130
- package/web/i18n.js +164 -6
package/scripts/moltbook-api.mjs
CHANGED
|
@@ -312,43 +312,114 @@ export async function listPendingDrafts(dir = DEFAULT_DRAFTS_DIR) {
|
|
|
312
312
|
//
|
|
313
313
|
// Shared by the CLI (for manual `reply` flow) and the bridge (for fire-and-
|
|
314
314
|
// forget draft posting on approval).
|
|
315
|
+
//
|
|
316
|
+
// Moltbook's verification puzzles are obfuscated word problems — e.g.
|
|
317
|
+
// `lOoB-stErR ClAw FoRcE iS tHiRtY fIvE NoOtOnS aNd iT s OtHeR ClAw Is tWeNtY
|
|
318
|
+
// tHrEe NooToNs, tOtAl/ FoRcE?` → 35 + 23 = 58.00. The solver strips
|
|
319
|
+
// non-letter noise, identifies number words (including compounds like
|
|
320
|
+
// "one hundred forty two" = 142), identifies operator words around each
|
|
321
|
+
// number, and evaluates left-to-right. See `scripts/test-puzzle-solver.mjs`
|
|
322
|
+
// for the regression corpus; add new cases there whenever we observe a
|
|
323
|
+
// failure in the wild via the `~/.viveworker/moltbook-verify-history.jsonl`
|
|
324
|
+
// log.
|
|
325
|
+
|
|
326
|
+
// Operator encoding: 0=add, 1=sub, 2=mul, 3=div.
|
|
327
|
+
const OP_ADD = 0;
|
|
328
|
+
const OP_SUB = 1;
|
|
329
|
+
const OP_MUL = 2;
|
|
330
|
+
const OP_DIV = 3;
|
|
331
|
+
|
|
332
|
+
const NUMBER_WORDS = Object.freeze({
|
|
333
|
+
zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9,
|
|
334
|
+
ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16,
|
|
335
|
+
seventeen: 17, eighteen: 18, nineteen: 19,
|
|
336
|
+
twenty: 20, thirty: 30, forty: 40, fifty: 50,
|
|
337
|
+
sixty: 60, seventy: 70, eighty: 80, ninety: 90,
|
|
338
|
+
hundred: 100, thousand: 1000,
|
|
339
|
+
// Cultural shortcuts that occasionally appear in the generator.
|
|
340
|
+
dozen: 12, score: 20,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Operator words. Keep contextual/framing words (force, velocity, total, etc.)
|
|
344
|
+
// mapped to OP_ADD so their presence doesn't crash the parser — the binary-
|
|
345
|
+
// sequence builder drops ops that have no number to operate against.
|
|
346
|
+
const OP_WORDS = Object.freeze({
|
|
347
|
+
// Addition / neutral-context.
|
|
348
|
+
add: OP_ADD, added: OP_ADD, adds: OP_ADD, plus: OP_ADD, and: OP_ADD,
|
|
349
|
+
sum: OP_ADD, summed: OP_ADD, combined: OP_ADD, combine: OP_ADD, together: OP_ADD,
|
|
350
|
+
total: OP_ADD, totaled: OP_ADD, totals: OP_ADD, both: OP_ADD,
|
|
351
|
+
gain: OP_ADD, gains: OP_ADD, gained: OP_ADD,
|
|
352
|
+
increase: OP_ADD, increased: OP_ADD, increases: OP_ADD,
|
|
353
|
+
boost: OP_ADD, boosted: OP_ADD, boosts: OP_ADD,
|
|
354
|
+
faster: OP_ADD, accelerates: OP_ADD, accelerated: OP_ADD, more: OP_ADD,
|
|
355
|
+
force: OP_ADD, velocity: OP_ADD, speed: OP_ADD, exerts: OP_ADD, new: OP_ADD,
|
|
356
|
+
// Subtraction.
|
|
357
|
+
minus: OP_SUB, less: OP_SUB, difference: OP_SUB, subtract: OP_SUB,
|
|
358
|
+
subtracts: OP_SUB, subtracted: OP_SUB,
|
|
359
|
+
decrease: OP_SUB, decreased: OP_SUB, decreases: OP_SUB,
|
|
360
|
+
loses: OP_SUB, lost: OP_SUB, lose: OP_SUB,
|
|
361
|
+
reduce: OP_SUB, reduced: OP_SUB, reduces: OP_SUB, reducing: OP_SUB, reduction: OP_SUB,
|
|
362
|
+
drop: OP_SUB, drops: OP_SUB, dropped: OP_SUB,
|
|
363
|
+
slower: OP_SUB, slows: OP_SUB, slowed: OP_SUB,
|
|
364
|
+
remove: OP_SUB, removes: OP_SUB, removed: OP_SUB,
|
|
365
|
+
// Multiplication.
|
|
366
|
+
multiply: OP_MUL, multiplied: OP_MUL, multiplies: OP_MUL,
|
|
367
|
+
times: OP_MUL, product: OP_MUL,
|
|
368
|
+
twice: OP_MUL, double: OP_MUL, doubled: OP_MUL,
|
|
369
|
+
triple: OP_MUL, tripled: OP_MUL,
|
|
370
|
+
squared: OP_MUL, square: OP_MUL,
|
|
371
|
+
// Division.
|
|
372
|
+
divide: OP_DIV, divided: OP_DIV, divides: OP_DIV, division: OP_DIV,
|
|
373
|
+
ratio: OP_DIV,
|
|
374
|
+
// NOTE: "per" intentionally NOT mapped here — "per second" / "per hour"
|
|
375
|
+
// are units, not division operators. When a puzzle really means division
|
|
376
|
+
// it uses "divided by" or "ratio of" or "split".
|
|
377
|
+
half: OP_DIV, halved: OP_DIV, halves: OP_DIV,
|
|
378
|
+
split: OP_DIV,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Collapse runs of the same letter: the generator often doubles consonants
|
|
382
|
+
// (e.g. "NoOtOnS" → "nootons"; "lOoB-stErR" → "loobsterr"). Collapsing means
|
|
383
|
+
// known-word detection still fires on the de-doubled form.
|
|
384
|
+
const collapseRuns = (w) => w.replace(/([a-z])\1+/g, "$1");
|
|
385
|
+
|
|
386
|
+
function lookupWord(word) {
|
|
387
|
+
if (NUMBER_WORDS[word] != null) return { type: "num", value: NUMBER_WORDS[word] };
|
|
388
|
+
if (OP_WORDS[word] != null) return { type: "op", value: OP_WORDS[word] };
|
|
389
|
+
const collapsed = collapseRuns(word);
|
|
390
|
+
if (collapsed !== word) {
|
|
391
|
+
if (NUMBER_WORDS[collapsed] != null) return { type: "num", value: NUMBER_WORDS[collapsed] };
|
|
392
|
+
if (OP_WORDS[collapsed] != null) return { type: "op", value: OP_WORDS[collapsed] };
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
315
396
|
|
|
316
|
-
// Naive verification-puzzle solver.
|
|
317
|
-
//
|
|
318
|
-
// `null` if it can't confidently solve — caller falls back to LLM or manual.
|
|
397
|
+
// Naive verification-puzzle solver. Returns answer as "XX.XX" or null if we
|
|
398
|
+
// can't confidently recover two numbers. Caller falls back to LLM.
|
|
319
399
|
export function solveVerificationPuzzle(challengeText) {
|
|
320
400
|
if (!challengeText) return null;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
seventeen: 17, eighteen: 18, nineteen: 19, twenty: 20, thirty: 30, forty: 40, fifty: 50,
|
|
328
|
-
sixty: 60, seventy: 70, eighty: 80, ninety: 90, hundred: 100,
|
|
329
|
-
};
|
|
330
|
-
const collapseRuns = (w) => w.replace(/([a-z])\1+/g, "$1");
|
|
331
|
-
const rawTokens = cleaned
|
|
401
|
+
|
|
402
|
+
// Strip everything that isn't a letter or digit. Symbols (/, *, ^, ~, [, ])
|
|
403
|
+
// are deliberately NOT operators — the generator sprinkles them as pure
|
|
404
|
+
// noise. All real operators are spelled out in words.
|
|
405
|
+
const rawTokens = String(challengeText)
|
|
406
|
+
.toLowerCase()
|
|
332
407
|
.replace(/[^a-z0-9\s]/g, " ")
|
|
333
408
|
.split(/\s+/)
|
|
334
409
|
.filter(Boolean);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"divide", "divided", "ratio",
|
|
340
|
-
"how", "much", "what", "exerts", "new",
|
|
341
|
-
]);
|
|
342
|
-
const isKnown = (w) => numberWords[w] != null || numberWords[collapseRuns(w)] != null || operationWords.has(w) || operationWords.has(collapseRuns(w));
|
|
410
|
+
|
|
411
|
+
// The generator sometimes splits a known word into pieces separated by
|
|
412
|
+
// stripped punctuation (e.g. "tHiR-tY" → "thir" "ty"). Try to re-merge
|
|
413
|
+
// adjacent tokens if their concatenation is a known number or operator.
|
|
343
414
|
const merged = [];
|
|
344
415
|
let ti = 0;
|
|
345
416
|
while (ti < rawTokens.length) {
|
|
346
|
-
let best = rawTokens[ti];
|
|
347
417
|
let bestLen = 1;
|
|
418
|
+
let best = rawTokens[ti];
|
|
348
419
|
let candidate = rawTokens[ti];
|
|
349
420
|
for (let span = 2; span <= Math.min(4, rawTokens.length - ti); span++) {
|
|
350
421
|
candidate += rawTokens[ti + span - 1];
|
|
351
|
-
if (
|
|
422
|
+
if (lookupWord(candidate)) {
|
|
352
423
|
best = candidate;
|
|
353
424
|
bestLen = span;
|
|
354
425
|
}
|
|
@@ -356,57 +427,199 @@ export function solveVerificationPuzzle(challengeText) {
|
|
|
356
427
|
merged.push(best);
|
|
357
428
|
ti += bestLen;
|
|
358
429
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
while (i < words.length) {
|
|
369
|
-
const w = words[i];
|
|
430
|
+
|
|
431
|
+
// Convert the token stream into a sequence of {type:"num"|"op", value}.
|
|
432
|
+
// Compound numbers ("one hundred forty two" = 142) are collected greedily.
|
|
433
|
+
const tokens = [];
|
|
434
|
+
let j = 0;
|
|
435
|
+
while (j < merged.length) {
|
|
436
|
+
const w = merged[j];
|
|
437
|
+
|
|
438
|
+
// Bare digit.
|
|
370
439
|
if (/^\d+$/.test(w)) {
|
|
371
|
-
|
|
372
|
-
|
|
440
|
+
tokens.push({ type: "num", value: Number(w) });
|
|
441
|
+
j += 1;
|
|
373
442
|
continue;
|
|
374
443
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
444
|
+
|
|
445
|
+
const lookup = lookupWord(w);
|
|
446
|
+
if (!lookup) { j += 1; continue; }
|
|
447
|
+
|
|
448
|
+
if (lookup.type === "num") {
|
|
449
|
+
// Greedy compound-number consumption. Examples:
|
|
450
|
+
// "thirty five" = 35 (chunk += 30 then chunk += 5)
|
|
451
|
+
// "one hundred forty two" = 142 (chunk=1 → *100 → +40 → +2)
|
|
452
|
+
// "three thousand four hundred fifty" = 3450
|
|
453
|
+
let sum = 0;
|
|
454
|
+
let chunk = 0;
|
|
455
|
+
let consumed = false;
|
|
456
|
+
while (j < merged.length) {
|
|
457
|
+
const cur = merged[j];
|
|
458
|
+
if (/^\d+$/.test(cur)) {
|
|
459
|
+
if (consumed) break; // mixing word-number + digit-number, stop
|
|
460
|
+
sum = Number(cur);
|
|
461
|
+
consumed = true;
|
|
462
|
+
j += 1;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
const inner = lookupWord(cur);
|
|
466
|
+
if (!inner || inner.type !== "num") break;
|
|
467
|
+
const n = inner.value;
|
|
468
|
+
if (n === 1000) {
|
|
469
|
+
sum += (chunk || 1) * 1000;
|
|
470
|
+
chunk = 0;
|
|
471
|
+
} else if (n === 100) {
|
|
472
|
+
chunk = (chunk || 1) * 100;
|
|
473
|
+
} else {
|
|
474
|
+
// n < 100: tens + ones accumulate into the current chunk.
|
|
475
|
+
chunk += n;
|
|
476
|
+
}
|
|
477
|
+
consumed = true;
|
|
478
|
+
j += 1;
|
|
384
479
|
}
|
|
385
|
-
|
|
480
|
+
if (consumed) tokens.push({ type: "num", value: sum + chunk });
|
|
386
481
|
continue;
|
|
387
482
|
}
|
|
388
|
-
|
|
483
|
+
|
|
484
|
+
// Operator.
|
|
485
|
+
tokens.push({ type: "op", value: lookup.value });
|
|
486
|
+
j += 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Capture the first non-default op that appears BEFORE the first number.
|
|
490
|
+
// Phrases like "product of X and Y", "difference between X and Y", and
|
|
491
|
+
// "ratio of X to Y" put the real operation at the front and leave only a
|
|
492
|
+
// weak connector ("and", "to", "of") between the numbers. Without this,
|
|
493
|
+
// the solver would drop the leading op and default to addition.
|
|
494
|
+
let leadingStrongOp = null;
|
|
495
|
+
for (const t of tokens) {
|
|
496
|
+
if (t.type === "num") break;
|
|
497
|
+
if (t.type === "op" && t.value !== OP_ADD) { leadingStrongOp = t.value; break; }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Normalise into a strict alternation [num, op, num, op, num, ...].
|
|
501
|
+
// - Drop leading ops (framing words like "total force is ...").
|
|
502
|
+
// - If two ops appear back-to-back, prefer the STRONGER (non-ADD) one
|
|
503
|
+
// over the default ADD. This is because contextual framing verbs
|
|
504
|
+
// (force / velocity / speed / exerts) are mapped to OP_ADD so their
|
|
505
|
+
// presence doesn't crash the parser, but they should NOT clobber a
|
|
506
|
+
// real operator (OP_SUB / OP_MUL / OP_DIV) that appears right next
|
|
507
|
+
// to them. Example: "reducing speed by four" → tokens end up as
|
|
508
|
+
// [OP_SUB(reducing), OP_ADD(speed)] — SUB must survive.
|
|
509
|
+
// When both ops are the same strength, the latter wins (closest to
|
|
510
|
+
// the following number).
|
|
511
|
+
// - If two nums appear with no op between them, insert a default add.
|
|
512
|
+
// - Drop trailing ops.
|
|
513
|
+
const seq = [];
|
|
514
|
+
for (const t of tokens) {
|
|
515
|
+
if (seq.length === 0) {
|
|
516
|
+
if (t.type === "num") seq.push(t);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const last = seq[seq.length - 1];
|
|
520
|
+
if (t.type === "num") {
|
|
521
|
+
if (last.type === "num") {
|
|
522
|
+
seq.push({ type: "op", value: OP_ADD });
|
|
523
|
+
}
|
|
524
|
+
seq.push(t);
|
|
525
|
+
} else {
|
|
526
|
+
if (last.type === "op") {
|
|
527
|
+
// Prefer strong over default (ADD). Otherwise latter wins.
|
|
528
|
+
if (last.value === OP_ADD && t.value !== OP_ADD) {
|
|
529
|
+
seq[seq.length - 1] = t;
|
|
530
|
+
} else if (last.value !== OP_ADD && t.value === OP_ADD) {
|
|
531
|
+
// Keep last — strong op survives over a trailing framing verb.
|
|
532
|
+
} else {
|
|
533
|
+
seq[seq.length - 1] = t;
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
seq.push(t);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
389
539
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
540
|
+
while (seq.length > 0 && seq[seq.length - 1].type === "op") seq.pop();
|
|
541
|
+
|
|
542
|
+
if (seq.length < 3) return null;
|
|
543
|
+
if (seq[0].type !== "num") return null;
|
|
544
|
+
|
|
545
|
+
// Promote the leading op if no strong op appeared between the numbers. We
|
|
546
|
+
// only do this when every internal op is the default OP_ADD — otherwise
|
|
547
|
+
// the internal ops were the real signal (e.g. "a+b-c" should stay mixed,
|
|
548
|
+
// even if the sentence started with "total").
|
|
549
|
+
if (leadingStrongOp != null) {
|
|
550
|
+
const hasStrongInternal = seq.some((t) => t.type === "op" && t.value !== OP_ADD);
|
|
551
|
+
if (!hasStrongInternal) {
|
|
552
|
+
for (const t of seq) {
|
|
553
|
+
if (t.type === "op") t.value = leadingStrongOp;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Evaluate strictly left-to-right (no precedence — Moltbook puzzles are
|
|
559
|
+
// binary in practice, so this only matters for the rare 3-number case).
|
|
560
|
+
let result = seq[0].value;
|
|
561
|
+
for (let k = 1; k < seq.length; k += 2) {
|
|
562
|
+
const op = seq[k];
|
|
563
|
+
const rhs = seq[k + 1];
|
|
564
|
+
if (!op || !rhs || op.type !== "op" || rhs.type !== "num") break;
|
|
565
|
+
switch (op.value) {
|
|
566
|
+
case OP_ADD: result += rhs.value; break;
|
|
567
|
+
case OP_SUB: result -= rhs.value; break;
|
|
568
|
+
case OP_MUL: result *= rhs.value; break;
|
|
569
|
+
case OP_DIV: result = rhs.value !== 0 ? result / rhs.value : result; break;
|
|
570
|
+
default: break;
|
|
571
|
+
}
|
|
404
572
|
}
|
|
573
|
+
|
|
574
|
+
if (!Number.isFinite(result)) return null;
|
|
575
|
+
// Refuse answers the puzzle generator shouldn't produce. A negative answer
|
|
576
|
+
// usually means we mis-identified the operator direction; returning null
|
|
577
|
+
// lets the LLM take a clean shot rather than submitting a definitely-wrong
|
|
578
|
+
// value.
|
|
579
|
+
if (result < 0) return null;
|
|
580
|
+
|
|
405
581
|
return result.toFixed(2);
|
|
406
582
|
}
|
|
407
583
|
|
|
408
584
|
// LLM-based verification puzzle solver. Shells out to claude or codex CLI.
|
|
409
585
|
// Returns the answer as "XX.XX" string, or null if unavailable.
|
|
586
|
+
//
|
|
587
|
+
// The naive solver misses ~13% of Moltbook's puzzles (mostly 3-number or
|
|
588
|
+
// unusual-vocabulary cases). The LLM fallback has been the same ~13% blind
|
|
589
|
+
// spot because parsing was too strict — it only accepted "XX.XX" on its own
|
|
590
|
+
// or a pure-integer line, and many CLI wrappers prepend banners or reason
|
|
591
|
+
// step-by-step before emitting the final number. `extractPuzzleAnswerFromText`
|
|
592
|
+
// is now shared with `solvePuzzleWithLLM` and is much more forgiving:
|
|
593
|
+
// prefer explicit decimals, fall back to the last standalone integer, then
|
|
594
|
+
// the last integer anywhere in the response. Exported for tests.
|
|
595
|
+
export function extractPuzzleAnswerFromText(text) {
|
|
596
|
+
if (!text) return null;
|
|
597
|
+
const trimmed = String(text).trim();
|
|
598
|
+
if (!trimmed) return null;
|
|
599
|
+
|
|
600
|
+
// 1. Prefer an exact "NN.NN" (two decimals) token. This is what the prompt
|
|
601
|
+
// explicitly asks for and what every happy-path response contains.
|
|
602
|
+
const twoDecimal = trimmed.match(/(?:^|[^\d])(\d+\.\d{2})(?:[^\d]|$)/);
|
|
603
|
+
if (twoDecimal) return twoDecimal[1];
|
|
604
|
+
|
|
605
|
+
// 2. Accept a one-decimal form and pad — e.g. "58.5" → "58.50".
|
|
606
|
+
const oneDecimal = trimmed.match(/(?:^|[^\d])(\d+\.\d)(?:[^\d]|$)/);
|
|
607
|
+
if (oneDecimal) return `${oneDecimal[1]}0`;
|
|
608
|
+
|
|
609
|
+
// 3. If the response contains any integer at all, use the LAST one. The LLM
|
|
610
|
+
// often reasons step-by-step ("thirty-five = 35, twenty-three = 23,
|
|
611
|
+
// sum is 58"), and the final integer is the answer. This is less
|
|
612
|
+
// precise than step (1) but beats returning null on otherwise-correct
|
|
613
|
+
// reasoning.
|
|
614
|
+
const ints = [...trimmed.matchAll(/\b(\d+)\b/g)].map((m) => m[1]);
|
|
615
|
+
if (ints.length > 0) {
|
|
616
|
+
const pick = Number(ints[ints.length - 1]);
|
|
617
|
+
if (Number.isFinite(pick) && pick >= 0) return `${pick}.00`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
410
623
|
export async function solvePuzzleWithLLM(challengeText) {
|
|
411
624
|
if (!challengeText) return null;
|
|
412
625
|
const prompt =
|
|
@@ -414,13 +627,16 @@ export async function solvePuzzleWithLLM(challengeText) {
|
|
|
414
627
|
`The text has random capitalization, doubled letters, and stray punctuation — ignore all of that. ` +
|
|
415
628
|
`CRITICAL: ALL symbols (/, *, ^, ~, [, ], etc.) are NOISE, NOT arithmetic operators. ` +
|
|
416
629
|
`The operation is ALWAYS expressed in natural language words only. ` +
|
|
417
|
-
`Extract the numbers (written as words like "thirty five" = 35
|
|
418
|
-
`
|
|
419
|
-
`
|
|
420
|
-
`
|
|
421
|
-
`
|
|
630
|
+
`Extract the numbers (written as words like "thirty five" = 35, or "one hundred forty" = 140), ` +
|
|
631
|
+
`determine the operation from WORDS ONLY ` +
|
|
632
|
+
`(addition: "total", "combined", "and", "plus", "gains", "sum", "together", "increased", "new velocity"; ` +
|
|
633
|
+
`subtraction: "difference", "minus", "less", "loses", "decreased", "reduced"; ` +
|
|
634
|
+
`multiplication: "times", "product", "multiplied", "doubled", "tripled", "squared"; ` +
|
|
635
|
+
`division: "divided by", "ratio", "per", "halved"). ` +
|
|
636
|
+
`If two or more operation words appear, apply them left-to-right in order. ` +
|
|
422
637
|
`If no operation word is found, default to addition. ` +
|
|
423
|
-
`Compute the result and output ONLY the number with exactly 2 decimal places (e.g. "58.00").
|
|
638
|
+
`Compute the result and output ONLY the number with exactly 2 decimal places (e.g. "58.00"). ` +
|
|
639
|
+
`No reasoning, no other text — JUST the number.\n\n` +
|
|
424
640
|
`Puzzle: ${challengeText}`;
|
|
425
641
|
for (const cmd of ["claude", "codex"]) {
|
|
426
642
|
let bin;
|
|
@@ -443,13 +659,44 @@ export async function solvePuzzleWithLLM(challengeText) {
|
|
|
443
659
|
p.on("exit", (code) => (code === 0 ? resolve(out.trim()) : reject(new Error(`exit ${code}`))));
|
|
444
660
|
p.on("error", reject);
|
|
445
661
|
});
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
const intMatch = result.match(/^(\d+)$/m);
|
|
449
|
-
if (intMatch) return `${intMatch[1]}.00`;
|
|
662
|
+
const extracted = extractPuzzleAnswerFromText(result);
|
|
663
|
+
if (extracted) return extracted;
|
|
450
664
|
} catch {
|
|
451
665
|
// try next
|
|
452
666
|
}
|
|
453
667
|
}
|
|
454
668
|
return null;
|
|
455
669
|
}
|
|
670
|
+
|
|
671
|
+
// ---------- Verification history (for offline analysis / retraining) ----------
|
|
672
|
+
//
|
|
673
|
+
// Every verify attempt — whether it succeeded, was rejected by the server,
|
|
674
|
+
// or couldn't be solved at all — gets a single JSONL line appended to
|
|
675
|
+
// `~/.viveworker/moltbook-verify-history.jsonl`. The file is the only way to
|
|
676
|
+
// reconstruct a failing `challenge_text` after the fact, because the puzzle
|
|
677
|
+
// is ephemeral on the Moltbook side (returned once in the POST /posts
|
|
678
|
+
// response and never exposed again).
|
|
679
|
+
//
|
|
680
|
+
// Having the corpus lets us:
|
|
681
|
+
// 1. Diagnose concrete failures with `scripts/test-puzzle-solver.mjs`.
|
|
682
|
+
// 2. Add new regression cases to the test harness when a new template
|
|
683
|
+
// starts appearing.
|
|
684
|
+
// 3. Measure solver vs LLM hit rate over time.
|
|
685
|
+
export const DEFAULT_VERIFY_HISTORY_FILE = path.join(
|
|
686
|
+
os.homedir(), ".viveworker", "moltbook-verify-history.jsonl"
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
export async function recordPuzzleAttempt(entry, file = DEFAULT_VERIFY_HISTORY_FILE) {
|
|
690
|
+
try {
|
|
691
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
692
|
+
const record = {
|
|
693
|
+
ts: new Date().toISOString(),
|
|
694
|
+
...entry,
|
|
695
|
+
};
|
|
696
|
+
await fs.appendFile(file, JSON.stringify(record) + "\n", "utf8");
|
|
697
|
+
} catch (err) {
|
|
698
|
+
// Best-effort logging — never fail a post just because the history file
|
|
699
|
+
// is unwritable.
|
|
700
|
+
try { console.error(`[moltbook-verify-history] Failed to record: ${err.message}`); } catch { /* ignore */ }
|
|
701
|
+
}
|
|
702
|
+
}
|