terminalhire 0.37.0 → 0.39.0
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/bin/claim-push-bg.js +170 -29
- package/dist/bin/job-status-store.js +29 -0
- package/dist/bin/jpi-bounties.js +729 -60
- package/dist/bin/jpi-chat-read.js +279 -54
- package/dist/bin/jpi-chat.js +298 -73
- package/dist/bin/jpi-claim.js +15205 -406
- package/dist/bin/jpi-contribute.js +240 -32
- package/dist/bin/jpi-devs.js +705 -60
- package/dist/bin/jpi-dispatch.js +40089 -19693
- package/dist/bin/jpi-hub.js +946 -125
- package/dist/bin/jpi-inbox.js +298 -73
- package/dist/bin/jpi-init.js +677 -57
- package/dist/bin/jpi-intro.js +77 -4
- package/dist/bin/jpi-jobs.js +710 -61
- package/dist/bin/jpi-learn.js +77 -4
- package/dist/bin/jpi-link.js +27 -2
- package/dist/bin/jpi-login.js +845 -94
- package/dist/bin/jpi-mcp-chat.js +6203 -283
- package/dist/bin/jpi-mcp.js +5826 -131
- package/dist/bin/jpi-profile.js +77 -4
- package/dist/bin/jpi-project.js +677 -57
- package/dist/bin/jpi-refresh.js +1001 -191
- package/dist/bin/jpi-repo.js +101 -4
- package/dist/bin/jpi-save.js +77 -4
- package/dist/bin/jpi-spinner.js +29 -0
- package/dist/bin/jpi-sync.js +77 -4
- package/dist/bin/jpi-trajectory.js +77 -4
- package/dist/bin/jpi.js +34 -6
- package/dist/bin/spinner-render.js +30 -1
- package/dist/bin/spinner-seen.js +5 -1
- package/dist/bin/spinner.js +30 -1
- package/dist/src/chat-client.js +234 -36
- package/dist/src/chat-keystore.js +229 -27
- package/dist/src/claims.js +24 -0
- package/dist/src/crypto-store.js +27 -2
- package/dist/src/github-auth.js +132 -13
- package/dist/src/intro.js +77 -4
- package/dist/src/link.js +26 -1
- package/dist/src/profile.js +56 -2
- package/dist/src/repo-experience.js +56 -2
- package/dist/src/repo-policy-semantic.js +13499 -0
- package/dist/src/repo-policy.js +81 -14
- package/dist/src/reputation/fetch.js +59 -9
- package/dist/src/signal.js +29 -0
- package/dist/src/state-dir.js +32 -1
- package/dist/src/test-race-barrier.js +45 -0
- package/dist/src/trajectory.js +77 -4
- package/dist/src/web-session.js +26 -1
- package/package.json +2 -1
|
@@ -50,7 +50,31 @@ function ensureStateDir(dir) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
function applyStateDirSecretPolicy(dir, status) {
|
|
54
|
+
if (status === STATE_DIR_SYMLINK) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`terminalhire: refusing to write key material into ${dir} \u2014 it is a symlink, not a directory.
|
|
57
|
+
A write through it would FOLLOW THE LINK and place key/token material wherever the symlink points, outside our control and outside the "owner-only" (0700) guarantee this directory is supposed to carry.
|
|
58
|
+
Fix: remove the symlink so terminalhire can recreate it as a real directory \u2014
|
|
59
|
+
rm ${dir}
|
|
60
|
+
then re-run the command. If the symlink is intentional, point TERMINALHIRE_DIR at a real directory instead of routing it through this one.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (status === STATE_DIR_UNVERIFIED && !warnedUnverifiedSecretWriteThisProcess) {
|
|
64
|
+
warnedUnverifiedSecretWriteThisProcess = true;
|
|
65
|
+
try {
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`terminalhire: could not verify ${dir}'s permissions (expected on Windows \u2014 POSIX mode bits do not apply there) \u2014 proceeding, but the "owner-only" guarantee on key/token storage is NOT enforced on this platform.
|
|
68
|
+
`
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function ensureStateDirForSecret(dir) {
|
|
75
|
+
applyStateDirSecretPolicy(dir, ensureStateDir(dir));
|
|
76
|
+
}
|
|
77
|
+
var STATE_DIR_MODE, STATE_DIR_OK, STATE_DIR_SYMLINK, STATE_DIR_UNVERIFIED, warnedDirs, warnedUnverifiedSecretWriteThisProcess;
|
|
54
78
|
var init_state_dir = __esm({
|
|
55
79
|
"src/state-dir.ts"() {
|
|
56
80
|
"use strict";
|
|
@@ -59,6 +83,7 @@ var init_state_dir = __esm({
|
|
|
59
83
|
STATE_DIR_SYMLINK = "symlink";
|
|
60
84
|
STATE_DIR_UNVERIFIED = "unverified";
|
|
61
85
|
warnedDirs = /* @__PURE__ */ new Set();
|
|
86
|
+
warnedUnverifiedSecretWriteThisProcess = false;
|
|
62
87
|
}
|
|
63
88
|
});
|
|
64
89
|
|
|
@@ -73,6 +98,8 @@ __export(claims_exports, {
|
|
|
73
98
|
readClaims: () => readClaims,
|
|
74
99
|
recordClaim: () => recordClaim,
|
|
75
100
|
removeClaim: () => removeClaim,
|
|
101
|
+
removeClaimIfStakeMatches: () => removeClaimIfStakeMatches,
|
|
102
|
+
reserveStake: () => reserveStake,
|
|
76
103
|
toPushedClaim: () => toPushedClaim,
|
|
77
104
|
updateClaim: () => updateClaim
|
|
78
105
|
});
|
|
@@ -81,12 +108,12 @@ import {
|
|
|
81
108
|
writeFileSync as writeFileSync2,
|
|
82
109
|
mkdirSync as mkdirSync2,
|
|
83
110
|
renameSync as renameSync2,
|
|
84
|
-
existsSync as
|
|
111
|
+
existsSync as existsSync3,
|
|
85
112
|
rmSync as rmSync2,
|
|
86
113
|
statSync
|
|
87
114
|
} from "fs";
|
|
88
115
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
89
|
-
import { join as
|
|
116
|
+
import { join as join3 } from "path";
|
|
90
117
|
import { homedir as homedir2 } from "os";
|
|
91
118
|
function sleepSync(ms) {
|
|
92
119
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -142,7 +169,7 @@ function normalizeClaim(c) {
|
|
|
142
169
|
}
|
|
143
170
|
function readClaims() {
|
|
144
171
|
try {
|
|
145
|
-
if (!
|
|
172
|
+
if (!existsSync3(CLAIMS_FILE)) return [];
|
|
146
173
|
const data = JSON.parse(readFileSync2(CLAIMS_FILE, "utf8"));
|
|
147
174
|
const claims = Array.isArray(data?.claims) ? data.claims : [];
|
|
148
175
|
return claims.map(normalizeClaim);
|
|
@@ -215,6 +242,17 @@ function updateClaim(id, patch) {
|
|
|
215
242
|
return claims[idx];
|
|
216
243
|
});
|
|
217
244
|
}
|
|
245
|
+
function reserveStake(id, stake) {
|
|
246
|
+
return withClaimsLock(() => {
|
|
247
|
+
const claims = readClaims();
|
|
248
|
+
const idx = claims.findIndex((c) => c.id === id);
|
|
249
|
+
if (idx === -1) return null;
|
|
250
|
+
if (claims[idx].stake) return null;
|
|
251
|
+
claims[idx] = { ...claims[idx], stake, updatedAt: nowISO() };
|
|
252
|
+
writeClaims(claims);
|
|
253
|
+
return claims[idx];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
218
256
|
function removeClaim(id) {
|
|
219
257
|
return withClaimsLock(() => {
|
|
220
258
|
const claims = readClaims();
|
|
@@ -224,6 +262,17 @@ function removeClaim(id) {
|
|
|
224
262
|
return true;
|
|
225
263
|
});
|
|
226
264
|
}
|
|
265
|
+
function removeClaimIfStakeMatches(id, expectedStakePostedAt) {
|
|
266
|
+
return withClaimsLock(() => {
|
|
267
|
+
const claims = readClaims();
|
|
268
|
+
const target = claims.find((c) => c.id === id);
|
|
269
|
+
if (!target) return false;
|
|
270
|
+
const actual = target.stake ? target.stake.postedAt : null;
|
|
271
|
+
if (actual !== expectedStakePostedAt) return false;
|
|
272
|
+
writeClaims(claims.filter((c) => c.id !== id));
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
227
276
|
function acceptedPRRate(claims = readClaims()) {
|
|
228
277
|
const total = claims.length;
|
|
229
278
|
const merged = claims.filter((c) => c.state === "merged").length;
|
|
@@ -234,8 +283,8 @@ var init_claims = __esm({
|
|
|
234
283
|
"src/claims.ts"() {
|
|
235
284
|
"use strict";
|
|
236
285
|
init_state_dir();
|
|
237
|
-
TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR ||
|
|
238
|
-
CLAIMS_FILE =
|
|
286
|
+
TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
|
|
287
|
+
CLAIMS_FILE = join3(TERMINALHIRE_DIR2, "claims.json");
|
|
239
288
|
LOCK_DIR = `${CLAIMS_FILE}.lock`;
|
|
240
289
|
LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
|
|
241
290
|
LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
|
|
@@ -274,30 +323,122 @@ var init_claims = __esm({
|
|
|
274
323
|
|
|
275
324
|
// bin/claim-push-bg.js
|
|
276
325
|
import { createHash } from "crypto";
|
|
277
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as
|
|
278
|
-
import { join as
|
|
326
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, rmSync as rmSync3 } from "fs";
|
|
327
|
+
import { join as join4 } from "path";
|
|
279
328
|
import { homedir as homedir3 } from "os";
|
|
280
329
|
|
|
281
330
|
// src/github-auth.ts
|
|
282
331
|
init_state_dir();
|
|
283
332
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
284
|
-
import {
|
|
285
|
-
|
|
333
|
+
import {
|
|
334
|
+
readFileSync,
|
|
335
|
+
writeFileSync,
|
|
336
|
+
existsSync as existsSync2,
|
|
337
|
+
rmSync,
|
|
338
|
+
renameSync,
|
|
339
|
+
linkSync,
|
|
340
|
+
unlinkSync
|
|
341
|
+
} from "fs";
|
|
342
|
+
import { join as join2 } from "path";
|
|
286
343
|
import { homedir } from "os";
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
344
|
+
|
|
345
|
+
// src/test-race-barrier.ts
|
|
346
|
+
import { closeSync as closeSync2, constants as constants2, existsSync, lstatSync, openSync as openSync2 } from "fs";
|
|
347
|
+
import { join } from "path";
|
|
348
|
+
var ENV_VAR = "TERMINALHIRE_TEST_RACE_BARRIER_DIR";
|
|
349
|
+
function syncSleepMs(ms) {
|
|
350
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
351
|
+
}
|
|
352
|
+
function waitForTestRaceBarrier(phase) {
|
|
353
|
+
const root = process.env[ENV_VAR];
|
|
354
|
+
if (!root) return;
|
|
355
|
+
const phaseDir = join(root, phase);
|
|
356
|
+
if (!existsSync(phaseDir)) return;
|
|
357
|
+
const readyFile = join(phaseDir, `ready-${process.pid}`);
|
|
358
|
+
const goFile = join(phaseDir, "go");
|
|
359
|
+
const noFollow = constants2.O_NOFOLLOW ?? 0;
|
|
360
|
+
if (lstatSync(readyFile, { throwIfNoEntry: false })) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`terminalhire: test race barrier "${phase}" found something already at its ready marker path ${readyFile} (regular file or symlink) \u2014 refusing rather than following or overwriting whatever is already there (this only fires under ${ENV_VAR}, never in production).`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
let readyFd;
|
|
366
|
+
try {
|
|
367
|
+
readyFd = openSync2(
|
|
368
|
+
readyFile,
|
|
369
|
+
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | noFollow
|
|
370
|
+
);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`terminalhire: test race barrier "${phase}" could not create its ready marker at ${readyFile} (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than blocking on or writing through whatever is already there (this only fires under ${ENV_VAR}, never in production).`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
closeSync2(readyFd);
|
|
377
|
+
const deadline = Date.now() + 3e4;
|
|
378
|
+
while (!existsSync(goFile)) {
|
|
379
|
+
if (Date.now() > deadline) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`terminalhire: test race barrier "${phase}" timed out waiting for ${goFile} (the test process never released it \u2014 this only fires under ${ENV_VAR}, never in production).`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
syncSleepMs(2);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/github-auth.ts
|
|
389
|
+
var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join2(homedir(), ".terminalhire");
|
|
390
|
+
var TOKEN_FILE = join2(TERMINALHIRE_DIR, "github-token.enc");
|
|
391
|
+
var KEY_FILE = join2(TERMINALHIRE_DIR, "key");
|
|
290
392
|
var ALGO = "aes-256-gcm";
|
|
291
393
|
var KEY_BYTES = 32;
|
|
292
394
|
var IV_BYTES = 12;
|
|
395
|
+
var KEY_HEX_RE = new RegExp(`^[0-9a-f]{${KEY_BYTES * 2}}$`);
|
|
396
|
+
function isValidKeyHex(value) {
|
|
397
|
+
return KEY_HEX_RE.test(value);
|
|
398
|
+
}
|
|
399
|
+
function readKeyFileOrThrow() {
|
|
400
|
+
const raw = readFileSync(KEY_FILE, "utf8").trim();
|
|
401
|
+
if (!isValidKeyHex(raw)) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`terminalhire: the shared encryption key at ${KEY_FILE} is not in the expected format (expected exactly ${KEY_BYTES * 2} lowercase-hex characters \u2014 a ${KEY_BYTES}-byte key).
|
|
404
|
+
This key decrypts the GitHub token, local profile, and chat identity stores under ~/.terminalhire \u2014 it should never be hand-edited.
|
|
405
|
+
Recovery: if you intend to reset it, delete the file yourself (this INVALIDATES every encrypted store under ~/.terminalhire, which will need to be re-created/re-authenticated):
|
|
406
|
+
rm ${KEY_FILE}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return Buffer.from(raw, "hex");
|
|
410
|
+
}
|
|
411
|
+
function publishKeyBlob(key) {
|
|
412
|
+
const tmpFile = `${KEY_FILE}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
413
|
+
try {
|
|
414
|
+
writeFileSync(tmpFile, key.toString("hex"), { encoding: "utf8", mode: 384, flag: "wx" });
|
|
415
|
+
try {
|
|
416
|
+
linkSync(tmpFile, KEY_FILE);
|
|
417
|
+
return true;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (err?.code === "EEXIST") {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
try {
|
|
426
|
+
unlinkSync(tmpFile);
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
293
431
|
async function loadKey() {
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
return
|
|
432
|
+
ensureStateDirForSecret(TERMINALHIRE_DIR);
|
|
433
|
+
if (existsSync2(KEY_FILE)) {
|
|
434
|
+
return readKeyFileOrThrow();
|
|
297
435
|
}
|
|
436
|
+
waitForTestRaceBarrier("key");
|
|
298
437
|
const key = randomBytes(KEY_BYTES);
|
|
299
|
-
|
|
300
|
-
|
|
438
|
+
if (publishKeyBlob(key)) {
|
|
439
|
+
return key;
|
|
440
|
+
}
|
|
441
|
+
return readKeyFileOrThrow();
|
|
301
442
|
}
|
|
302
443
|
function encrypt(plaintext, key) {
|
|
303
444
|
const iv = randomBytes(IV_BYTES);
|
|
@@ -318,21 +459,21 @@ function decrypt(blob, key) {
|
|
|
318
459
|
|
|
319
460
|
// bin/claim-push-bg.js
|
|
320
461
|
init_state_dir();
|
|
321
|
-
var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR ||
|
|
322
|
-
var CLAIM_PUSH_AUTO_MARKER =
|
|
323
|
-
var CLAIM_PUSH_TOKEN_FILE =
|
|
324
|
-
var CLAIM_PUSH_MANUAL_MARKER =
|
|
462
|
+
var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
|
|
463
|
+
var CLAIM_PUSH_AUTO_MARKER = join4(TERMINALHIRE_DIR3, "claim-push-auto.json");
|
|
464
|
+
var CLAIM_PUSH_TOKEN_FILE = join4(TERMINALHIRE_DIR3, "claim-push-token.enc");
|
|
465
|
+
var CLAIM_PUSH_MANUAL_MARKER = join4(TERMINALHIRE_DIR3, "claim-push.json");
|
|
325
466
|
var CLAIM_SYNC_BASE = "https://terminalhire.com";
|
|
326
467
|
var AUTO_CONSENT_VERSION = 2;
|
|
327
468
|
var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
328
469
|
async function writePushTokenEnc(rawToken) {
|
|
329
|
-
|
|
470
|
+
ensureStateDirForSecret(TERMINALHIRE_DIR3);
|
|
330
471
|
const key = await loadKey();
|
|
331
472
|
const blob = encrypt(rawToken, key);
|
|
332
473
|
writeFileSync3(CLAIM_PUSH_TOKEN_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
333
474
|
}
|
|
334
475
|
async function readPushTokenEnc() {
|
|
335
|
-
if (!
|
|
476
|
+
if (!existsSync4(CLAIM_PUSH_TOKEN_FILE)) return void 0;
|
|
336
477
|
try {
|
|
337
478
|
const key = await loadKey();
|
|
338
479
|
const blob = JSON.parse(readFileSync3(CLAIM_PUSH_TOKEN_FILE, "utf8"));
|
|
@@ -349,7 +490,7 @@ function clearPushTokenEnc() {
|
|
|
349
490
|
}
|
|
350
491
|
function readAutoMarker() {
|
|
351
492
|
try {
|
|
352
|
-
return
|
|
493
|
+
return existsSync4(CLAIM_PUSH_AUTO_MARKER) ? JSON.parse(readFileSync3(CLAIM_PUSH_AUTO_MARKER, "utf8")) : null;
|
|
353
494
|
} catch {
|
|
354
495
|
return null;
|
|
355
496
|
}
|
|
@@ -410,13 +551,13 @@ async function shouldNudgeUnpushed() {
|
|
|
410
551
|
const currentHash = computeSnapshotHash(pushed);
|
|
411
552
|
let manual = null;
|
|
412
553
|
try {
|
|
413
|
-
manual =
|
|
554
|
+
manual = existsSync4(CLAIM_PUSH_MANUAL_MARKER) ? JSON.parse(readFileSync3(CLAIM_PUSH_MANUAL_MARKER, "utf8")) : null;
|
|
414
555
|
} catch {
|
|
415
556
|
manual = null;
|
|
416
557
|
}
|
|
417
558
|
return unpushedNudgeGate({
|
|
418
|
-
autoMarkerExists:
|
|
419
|
-
tokenFileExists:
|
|
559
|
+
autoMarkerExists: existsSync4(CLAIM_PUSH_AUTO_MARKER),
|
|
560
|
+
tokenFileExists: existsSync4(CLAIM_PUSH_TOKEN_FILE),
|
|
420
561
|
manualMarkerExists: !!manual,
|
|
421
562
|
lastSnapshotHash: manual?.lastSnapshotHash ?? null,
|
|
422
563
|
currentHash,
|
|
@@ -428,7 +569,7 @@ async function shouldNudgeUnpushed() {
|
|
|
428
569
|
}
|
|
429
570
|
async function runBackgroundClaimPush({ now = Date.now() } = {}) {
|
|
430
571
|
try {
|
|
431
|
-
if (!
|
|
572
|
+
if (!existsSync4(CLAIM_PUSH_AUTO_MARKER) || !existsSync4(CLAIM_PUSH_TOKEN_FILE)) return;
|
|
432
573
|
const marker = readAutoMarker();
|
|
433
574
|
if (!marker || !marker.autoConsentedAt) return;
|
|
434
575
|
const { listClaims: listClaims2, toPushedClaim: toPushedClaim2, PUSHED_CLAIM_FIELDS: PUSHED_CLAIM_FIELDS2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
@@ -699,6 +699,35 @@ var VERIFY_CONTRACT = [
|
|
|
699
699
|
'{"supported":["c1","c3"]} \u2014 the ids of the claims that survive (omit all others; use',
|
|
700
700
|
'{"supported":[]} if none do). Any surviving id MUST be one you were given.'
|
|
701
701
|
].join("\n");
|
|
702
|
+
var STAGE4_CONTRACT = [
|
|
703
|
+
"You are the CLAIM VALIDITY STAGE \u2014 a second, independent, ADVERSARIAL verifier.",
|
|
704
|
+
"You have NOT seen how any claim below was generated, by whom, or why \u2014 only the",
|
|
705
|
+
"claims themselves and the source. Your only job is to try to INVALIDATE each claim",
|
|
706
|
+
"using the FULL, ORIGINAL, UNABRIDGED source object given below \u2014 not a narrow",
|
|
707
|
+
"excerpt of it, the whole thing.",
|
|
708
|
+
"",
|
|
709
|
+
"For each claim: does the FULL source UNEQUIVOCALLY support every assertion in its",
|
|
710
|
+
"text, with no benefit of the doubt, no inference, no outside knowledge? A claim can",
|
|
711
|
+
"cite a real, resolvable path and still be FALSE in context \u2014 for example if it",
|
|
712
|
+
"quotes an early or superseded value while the full record shows the opposite held",
|
|
713
|
+
"later, or generalizes one narrow fact into a broader claim the record does not",
|
|
714
|
+
"support. Treat either as UNSUPPORTED. When in doubt, mark it UNSUPPORTED",
|
|
715
|
+
"(default-to-drop).",
|
|
716
|
+
"",
|
|
717
|
+
"ANTICIPATION RULE: a claim asserting the contributor anticipated, or acted ahead",
|
|
718
|
+
"of, reviewer concerns is supported ONLY if the full record shows the relevant work",
|
|
719
|
+
"was delivered BEFORE any reviewer raised a related concern. If the full record",
|
|
720
|
+
"shows a reviewer raised it first, or the ordering cannot be established, that",
|
|
721
|
+
'specific "anticipated" framing is NOT supported \u2014 mark it UNSUPPORTED rather than',
|
|
722
|
+
"accept the claim's framing at face value.",
|
|
723
|
+
"",
|
|
724
|
+
"OUTPUT FORMAT \u2014 obey exactly: respond with ONLY the JSON object and NOTHING ELSE.",
|
|
725
|
+
"No preamble, no per-claim commentary, no reasoning prose, no markdown fence, no",
|
|
726
|
+
"text before or after:",
|
|
727
|
+
'{"results":[{"id":"c1","supported":true},{"id":"c2","supported":false}]}',
|
|
728
|
+
"Exactly one entry per claim you were given, each with EXACTLY the keys `id` and",
|
|
729
|
+
"`supported` and no others."
|
|
730
|
+
].join("\n");
|
|
702
731
|
|
|
703
732
|
// ../../packages/core/src/short-token.ts
|
|
704
733
|
import { createHash as createHash2 } from "crypto";
|