@kaydash9999/banker-plugins 0.20.1 → 0.20.5
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +195 -1
- package/PRIVACY.md +13 -3
- package/README.md +27 -4
- package/bin/banker.js +84 -40
- package/bin/lib/telemetry-config.mjs +31 -16
- package/codex/manifest.json +1 -1
- package/codex/transform-matrix.md +1 -1
- package/commands/setup.md +23 -0
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +2 -9
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/deep-research/SKILL.md +5 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lineage/SKILL.md +12 -12
- package/skills/lineage/lineage.py +399 -300
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +1 -1
- package/skills/obsidizer/obsidize.mjs +8 -1
- package/skills/omc-patch/SKILL.md +80 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +5 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +137 -30
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +13 -5
- package/skills/setup-omc-hud/assets/apply-hud.mjs +381 -27
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +4 -3
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +1 -0
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -34,7 +34,8 @@ if sys.version_info < (3, 7):
|
|
|
34
34
|
sys.exit(2)
|
|
35
35
|
|
|
36
36
|
SCHEMA_VERSION = 2 # cache dir schema (v2: redacted summaries only)
|
|
37
|
-
SUMMARIZER_VERSION =
|
|
37
|
+
SUMMARIZER_VERSION = 4 # bump when summarize_turn or the page redaction changes (B-3 cache key)
|
|
38
|
+
# 3: cut from redacted text (3.0.2); 4: split secrets joined first (3.0.8)
|
|
38
39
|
USER_FOLD = 400 # collapse user messages longer than this (E-1)
|
|
39
40
|
ECHO_ASK = 40 # echo-exchange: user question length ceiling (A-4)
|
|
40
41
|
ECHO_REPLY = 5 # echo-exchange: assistant reply length ceiling (A-4)
|
|
@@ -345,10 +346,9 @@ def _rejoin_split(text, patterns, entropy=False, trail=None):
|
|
|
345
346
|
return "".join(out) + text[at:]
|
|
346
347
|
|
|
347
348
|
|
|
348
|
-
def
|
|
349
|
-
"""
|
|
350
|
-
|
|
351
|
-
counts, out, sub = {}, text, _sub_for(trail)
|
|
349
|
+
def _hide_patterns(out, mode, sub, counts):
|
|
350
|
+
"""`out` with each value the page patterns and the entropy rule find replaced by its marker, or by a mask with
|
|
351
|
+
mode mask; `counts` adds them up by name."""
|
|
352
352
|
for name, pat in SECRET_PATTERNS:
|
|
353
353
|
def repl(m, n=name):
|
|
354
354
|
counts[n] = counts.get(n, 0) + 1
|
|
@@ -361,32 +361,43 @@ def redact(text: str, extra: "str | None" = None, mode: str = "full", trail=None
|
|
|
361
361
|
counts["entropy"] = counts.get("entropy", 0) + 1
|
|
362
362
|
return _mask(s) if mode == "mask" else "[REDACTED:entropy]"
|
|
363
363
|
return s
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
return sub(ENTROPY_LONG, ent_repl, out)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _scan_detected(held, sub, counts):
|
|
368
|
+
"""held[0] with each value detect-secrets finds in it replaced: raises ImportError when it is not installed.
|
|
369
|
+
held[0] keeps what was replaced before a failure, as one pass over a local did. The scratch file it scans goes
|
|
370
|
+
whatever happens, a write that fails too (a full disk, a character UTF-8 cannot hold): it holds the text before
|
|
371
|
+
detect-secrets hid anything."""
|
|
372
|
+
from detect_secrets import SecretsCollection
|
|
373
|
+
from detect_secrets.settings import default_settings
|
|
374
|
+
tf = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8")
|
|
375
|
+
tmp = tf.name
|
|
366
376
|
try:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
with
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
with tf:
|
|
378
|
+
tf.write(held[0])
|
|
379
|
+
col = SecretsCollection()
|
|
380
|
+
with default_settings():
|
|
381
|
+
col.scan_file(tmp)
|
|
382
|
+
for fname in col.files:
|
|
383
|
+
for s in col[fname]:
|
|
384
|
+
sv = s.secret_value
|
|
385
|
+
if sv and sv in held[0]:
|
|
386
|
+
held[0] = sub(re.compile(re.escape(sv)), lambda m: "[REDACTED:ds]", held[0])
|
|
387
|
+
counts["ds:detect-secrets"] = counts.get(
|
|
388
|
+
"ds:detect-secrets", 0) + 1
|
|
389
|
+
finally:
|
|
374
390
|
try:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
finally:
|
|
386
|
-
try:
|
|
387
|
-
os.unlink(tmp)
|
|
388
|
-
except OSError:
|
|
389
|
-
pass
|
|
391
|
+
os.unlink(tmp)
|
|
392
|
+
except OSError:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _hide_detected(out, sub, counts):
|
|
397
|
+
"""`out` after detect-secrets, when it is installed; a WARN once a run when it is not, or when it fails."""
|
|
398
|
+
held = [out]
|
|
399
|
+
try:
|
|
400
|
+
_scan_detected(held, sub, counts)
|
|
390
401
|
except ImportError:
|
|
391
402
|
if not getattr(redact, "_ds_warned", False):
|
|
392
403
|
print("[lineage] WARN: detect-secrets unavailable, using fallback "
|
|
@@ -397,16 +408,30 @@ def redact(text: str, extra: "str | None" = None, mode: str = "full", trail=None
|
|
|
397
408
|
print(f"[lineage] WARN: detect-secrets error ({e}), continuing "
|
|
398
409
|
"with fallback", file=sys.stderr)
|
|
399
410
|
redact._ds_err_warned = True
|
|
411
|
+
return held[0]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _hide_extra(out, extra, sub, counts):
|
|
415
|
+
"""`out` with each --redact-extra keyword replaced; a marker an earlier pass left stays whole: a keyword inside
|
|
416
|
+
its name is no secret."""
|
|
417
|
+
for kw in [k.strip() for k in extra.split(",") if k.strip()]:
|
|
418
|
+
pat = re.compile(r"\[REDACTED(?::[^\]\s]*)?\]|" + re.escape(kw), re.IGNORECASE)
|
|
419
|
+
hits = []
|
|
420
|
+
out = sub(pat, lambda m: m.group(0) if m.group(0).upper().startswith("[REDACTED")
|
|
421
|
+
else hits.append(1) or "[REDACTED]", out)
|
|
422
|
+
if hits:
|
|
423
|
+
counts[f"custom:{kw}"] = len(hits)
|
|
424
|
+
return out
|
|
400
425
|
|
|
426
|
+
|
|
427
|
+
def redact(text: str, extra: "str | None" = None, mode: str = "full", trail=None):
|
|
428
|
+
"""Multi-layer redaction; page_redact and review_redact join split secrets first (3.0.8). Returns (text, counts).
|
|
429
|
+
With `trail` (a _Trail), where each character of the result came from is followed too."""
|
|
430
|
+
counts, sub = {}, _sub_for(trail)
|
|
431
|
+
out = _hide_patterns(text, mode, sub, counts)
|
|
432
|
+
out = _hide_detected(out, sub, counts)
|
|
401
433
|
if extra:
|
|
402
|
-
|
|
403
|
-
# a marker an earlier pass left stays whole: a keyword inside its name is no secret
|
|
404
|
-
pat = re.compile(r"\[REDACTED(?::[^\]\s]*)?\]|" + re.escape(kw), re.IGNORECASE)
|
|
405
|
-
hits = []
|
|
406
|
-
out = sub(pat, lambda m: m.group(0) if m.group(0).upper().startswith("[REDACTED")
|
|
407
|
-
else hits.append(1) or "[REDACTED]", out)
|
|
408
|
-
if hits:
|
|
409
|
-
counts[f"custom:{kw}"] = len(hits)
|
|
434
|
+
out = _hide_extra(out, extra, sub, counts)
|
|
410
435
|
return out, counts
|
|
411
436
|
|
|
412
437
|
|
|
@@ -431,8 +456,8 @@ _CU_SKIP = (r"(?!" + _CU_LEAD + r"(?:\+[\"']?%[-_0^#]?[A-Za-z%]|(?:\d+:\d+|(?:"
|
|
|
431
456
|
+ r"|[^\s:=\"'`(]*\)):" + _CU_REF + r")(?![^\s\"'`;|&)\\^])"
|
|
432
457
|
r"|\$\{\w+:[-=?+]?[/~.][^\s}\"'`(:]*\}[^\s:]*(?!\S)))")
|
|
433
458
|
|
|
434
|
-
#
|
|
435
|
-
#
|
|
459
|
+
# Patterns every page (the reviewed flow and --rulebase) and every part file hides with before the
|
|
460
|
+
# page patterns above, while keys are whole (3.0.2).
|
|
436
461
|
# A value a redaction already replaced: a marker ([REDACTED...]) or a value that is only a mask
|
|
437
462
|
# (abcd****wxyz: up to 4 characters, a whole run of *, up to 4 characters, then the value ends; a
|
|
438
463
|
# closing `, |, * or ) and then . , ; : ! ? may stand between, as in **password: ****h12** or
|
|
@@ -1210,7 +1235,8 @@ def _read_text(path):
|
|
|
1210
1235
|
|
|
1211
1236
|
|
|
1212
1237
|
def _read_cached(p):
|
|
1213
|
-
"""The summary cached at p, else at the same name with .txt,
|
|
1238
|
+
"""The summary cached at p, else at the same name with .txt, the name 3.0.2 and earlier wrote (their digests
|
|
1239
|
+
carry summarizer version 2 or 3, so since version 4 no file of theirs is found).
|
|
1214
1240
|
None when neither holds one: missing, bytes that are not UTF-8 (a DRM encrypted them), or
|
|
1215
1241
|
nothing a person can see (a write that stopped early, or only blank-looking letters); the
|
|
1216
1242
|
turn is then summarized again (3.0.4, 3.0.5)."""
|
|
@@ -1300,11 +1326,13 @@ def _is_block_start(lines, i):
|
|
|
1300
1326
|
if (_FENCE_RE.match(line) or _HEAD_RE.match(line) or _HR_RE.match(line)
|
|
1301
1327
|
or _QUOTE_RE.match(line) or _LIST_RE.match(line)):
|
|
1302
1328
|
return True
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1329
|
+
return _is_table_start(lines, i)
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def _is_table_start(lines, i):
|
|
1333
|
+
"""Whether lines[i] starts a table: it has a pipe AND the next line is a delimiter row."""
|
|
1334
|
+
return bool("|" in lines[i] and i + 1 < len(lines)
|
|
1335
|
+
and "-" in lines[i + 1] and _TABLE_DELIM_RE.match(lines[i + 1]))
|
|
1308
1336
|
|
|
1309
1337
|
|
|
1310
1338
|
def _render_table(lines, i):
|
|
@@ -1323,85 +1351,90 @@ def _render_table(lines, i):
|
|
|
1323
1351
|
% (thead, body)), i
|
|
1324
1352
|
|
|
1325
1353
|
|
|
1354
|
+
def _md_fence(lines, i, fence):
|
|
1355
|
+
"""A code block from the fence at lines[i] to the first closing fence at least as long (or the end), and the
|
|
1356
|
+
index past it."""
|
|
1357
|
+
i += 1
|
|
1358
|
+
buf = []
|
|
1359
|
+
while i < len(lines):
|
|
1360
|
+
mc = _FENCE_CLOSE_RE.match(lines[i])
|
|
1361
|
+
if mc and len(mc.group(1)) >= len(fence):
|
|
1362
|
+
i += 1
|
|
1363
|
+
break
|
|
1364
|
+
buf.append(lines[i])
|
|
1365
|
+
i += 1
|
|
1366
|
+
return "<pre><code>%s</code></pre>" % html.escape("\n".join(buf)), i
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _md_quote(lines, i, depth):
|
|
1370
|
+
"""The blockquote of the run of `>` lines at lines[i], rendered one level deeper, and the index past it."""
|
|
1371
|
+
buf = []
|
|
1372
|
+
while i < len(lines) and _QUOTE_RE.match(lines[i]):
|
|
1373
|
+
buf.append(re.sub(r"^\s*>\s?", "", lines[i]))
|
|
1374
|
+
i += 1
|
|
1375
|
+
return "<blockquote>%s</blockquote>" % render_markdown("\n".join(buf), depth + 1), i
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def _md_list(lines, i):
|
|
1379
|
+
"""The list of the run of list lines at lines[i] (ordered when the first is numbered; nested ones flatten),
|
|
1380
|
+
and the index past it."""
|
|
1381
|
+
tag = "ol" if re.match(r"^\s*\d+[.)]", lines[i]) else "ul"
|
|
1382
|
+
items = []
|
|
1383
|
+
while i < len(lines):
|
|
1384
|
+
mli = _LIST_RE.match(lines[i])
|
|
1385
|
+
if not mli:
|
|
1386
|
+
break
|
|
1387
|
+
items.append(_md_inline(html.escape(mli.group(3))))
|
|
1388
|
+
i += 1
|
|
1389
|
+
return "<%s>%s</%s>" % (tag, "".join("<li>%s</li>" % it for it in items), tag), i
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def _md_paragraph(lines, i):
|
|
1393
|
+
"""The paragraph that starts at lines[i] (at least that line: C-2, no endless loop), its lines joined with <br>
|
|
1394
|
+
(N5: pre-wrap keeps leading spaces without a second break), and the index past it."""
|
|
1395
|
+
buf = [lines[i]]
|
|
1396
|
+
i += 1
|
|
1397
|
+
while i < len(lines) and lines[i].strip() and not _is_block_start(lines, i):
|
|
1398
|
+
buf.append(lines[i])
|
|
1399
|
+
i += 1
|
|
1400
|
+
return "<p>%s</p>" % "<br>".join(_md_inline(html.escape(b)) for b in buf), i
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def _md_block(lines, i, depth):
|
|
1404
|
+
"""The HTML of the block that starts at lines[i] (None for a blank line), and the index past it."""
|
|
1405
|
+
line = lines[i]
|
|
1406
|
+
mf = _FENCE_RE.match(line)
|
|
1407
|
+
if mf:
|
|
1408
|
+
return _md_fence(lines, i, mf.group(1))
|
|
1409
|
+
mh = _HEAD_RE.match(line)
|
|
1410
|
+
if mh:
|
|
1411
|
+
hl = max(3, min(6, len(mh.group(1)))) # h3..h6 (avoid page-header clash)
|
|
1412
|
+
return "<h%d>%s</h%d>" % (hl, _md_inline(html.escape(mh.group(2).strip())), hl), i + 1
|
|
1413
|
+
if _HR_RE.match(line):
|
|
1414
|
+
return "<hr>", i + 1
|
|
1415
|
+
if _QUOTE_RE.match(line):
|
|
1416
|
+
return _md_quote(lines, i, depth)
|
|
1417
|
+
if _is_table_start(lines, i):
|
|
1418
|
+
return _render_table(lines, i)
|
|
1419
|
+
if _LIST_RE.match(line):
|
|
1420
|
+
return _md_list(lines, i)
|
|
1421
|
+
if not line.strip():
|
|
1422
|
+
return None, i + 1
|
|
1423
|
+
return _md_paragraph(lines, i)
|
|
1424
|
+
|
|
1425
|
+
|
|
1326
1426
|
def render_markdown(text, depth=0):
|
|
1327
1427
|
"""Block-level markdown → HTML. External deps: none. Escapes first; no stage
|
|
1328
1428
|
re-runs over prior markup. Nested lists flatten; footnotes unsupported.
|
|
1329
1429
|
"""
|
|
1330
1430
|
if depth > _BLOCKQUOTE_MAX_DEPTH:
|
|
1331
1431
|
return "<p>%s</p>" % _md_inline(html.escape(text))
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
mf = _FENCE_RE.match(line)
|
|
1339
|
-
if mf:
|
|
1340
|
-
fence = mf.group(1)
|
|
1341
|
-
i += 1
|
|
1342
|
-
buf = []
|
|
1343
|
-
while i < n:
|
|
1344
|
-
mc = _FENCE_CLOSE_RE.match(lines[i])
|
|
1345
|
-
if mc and len(mc.group(1)) >= len(fence):
|
|
1346
|
-
i += 1
|
|
1347
|
-
break
|
|
1348
|
-
buf.append(lines[i])
|
|
1349
|
-
i += 1
|
|
1350
|
-
out.append("<pre><code>%s</code></pre>"
|
|
1351
|
-
% html.escape("\n".join(buf)))
|
|
1352
|
-
continue
|
|
1353
|
-
mh = _HEAD_RE.match(line)
|
|
1354
|
-
if mh:
|
|
1355
|
-
hl = max(3, min(6, len(mh.group(1)))) # h3..h6 (avoid page-header clash)
|
|
1356
|
-
out.append("<h%d>%s</h%d>"
|
|
1357
|
-
% (hl, _md_inline(html.escape(mh.group(2).strip())), hl))
|
|
1358
|
-
i += 1
|
|
1359
|
-
continue
|
|
1360
|
-
if _HR_RE.match(line):
|
|
1361
|
-
out.append("<hr>")
|
|
1362
|
-
i += 1
|
|
1363
|
-
continue
|
|
1364
|
-
if _QUOTE_RE.match(line):
|
|
1365
|
-
buf = []
|
|
1366
|
-
while i < n and _QUOTE_RE.match(lines[i]):
|
|
1367
|
-
buf.append(re.sub(r"^\s*>\s?", "", lines[i]))
|
|
1368
|
-
i += 1
|
|
1369
|
-
out.append("<blockquote>%s</blockquote>"
|
|
1370
|
-
% render_markdown("\n".join(buf), depth + 1))
|
|
1371
|
-
continue
|
|
1372
|
-
if ("|" in line and i + 1 < n and "-" in lines[i + 1]
|
|
1373
|
-
and _TABLE_DELIM_RE.match(lines[i + 1])):
|
|
1374
|
-
tbl, i = _render_table(lines, i)
|
|
1375
|
-
out.append(tbl)
|
|
1376
|
-
continue
|
|
1377
|
-
ml = _LIST_RE.match(line)
|
|
1378
|
-
if ml:
|
|
1379
|
-
ordered = bool(re.match(r"^\s*\d+[.)]", line))
|
|
1380
|
-
tag = "ol" if ordered else "ul"
|
|
1381
|
-
items = []
|
|
1382
|
-
while i < n:
|
|
1383
|
-
mli = _LIST_RE.match(lines[i])
|
|
1384
|
-
if not mli:
|
|
1385
|
-
break
|
|
1386
|
-
items.append(_md_inline(html.escape(mli.group(3))))
|
|
1387
|
-
i += 1
|
|
1388
|
-
out.append("<%s>%s</%s>"
|
|
1389
|
-
% (tag, "".join("<li>%s</li>" % it for it in items), tag))
|
|
1390
|
-
continue
|
|
1391
|
-
if not line.strip():
|
|
1392
|
-
i += 1
|
|
1393
|
-
continue
|
|
1394
|
-
# paragraph: force-consume at least this line (C-2: no infinite loop)
|
|
1395
|
-
buf = [line]
|
|
1396
|
-
i += 1
|
|
1397
|
-
while i < n and lines[i].strip() and not _is_block_start(lines, i):
|
|
1398
|
-
buf.append(lines[i])
|
|
1399
|
-
i += 1
|
|
1400
|
-
# source newlines within a paragraph become <br> (N5). pre-wrap on the
|
|
1401
|
-
# container preserves leading/aligned spaces without double-breaking,
|
|
1402
|
-
# because we join with <br> and emit no literal newline between lines.
|
|
1403
|
-
out.append("<p>%s</p>"
|
|
1404
|
-
% "<br>".join(_md_inline(html.escape(b)) for b in buf))
|
|
1432
|
+
lines = _CTRL_RE.sub("", text).split("\n")
|
|
1433
|
+
out, i = [], 0
|
|
1434
|
+
while i < len(lines):
|
|
1435
|
+
block, i = _md_block(lines, i, depth)
|
|
1436
|
+
if block is not None:
|
|
1437
|
+
out.append(block)
|
|
1405
1438
|
return "\n".join(out)
|
|
1406
1439
|
|
|
1407
1440
|
|
|
@@ -1614,102 +1647,110 @@ def _fmt_time(ts):
|
|
|
1614
1647
|
return ts[11:16] if ts and len(ts) >= 16 else ""
|
|
1615
1648
|
|
|
1616
1649
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
last_date = None
|
|
1621
|
-
last_session = None
|
|
1622
|
-
cache_hits = 0
|
|
1623
|
-
cache_total = 0
|
|
1624
|
-
redact_counts = {}
|
|
1625
|
-
open_attr = " open" if open_details else ""
|
|
1626
|
-
|
|
1627
|
-
def _red(text):
|
|
1628
|
-
r, c = page_redact(text, extra=redact_extra, mode=redact_mode)
|
|
1629
|
-
for k, v in c.items():
|
|
1630
|
-
redact_counts[k] = redact_counts.get(k, 0) + v
|
|
1631
|
-
return r
|
|
1650
|
+
class _Rows:
|
|
1651
|
+
"""What render_rows carries from turn to turn: the rows so far, the redaction settings and what they hid by kind,
|
|
1652
|
+
the open flag and markdown, the date and session the last pills showed, and the rule summaries' cache hits."""
|
|
1632
1653
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
rows.append('<div class="day" data-mark="%s">'
|
|
1640
|
-
'<span class="pill pill-session">%s</span></div>'
|
|
1641
|
-
% (name, name))
|
|
1642
|
-
last_date = None
|
|
1643
|
-
if ts:
|
|
1644
|
-
d = ts[:10]
|
|
1645
|
-
if d != last_date:
|
|
1646
|
-
rows.append('<div class="day" data-mark="%s">'
|
|
1647
|
-
'<span class="pill pill-date">%s</span></div>'
|
|
1648
|
-
% (html.escape(d), html.escape(d)))
|
|
1649
|
-
last_date = d
|
|
1650
|
-
time_hm = html.escape(_fmt_time(ts))
|
|
1651
|
-
role = t["role"]
|
|
1652
|
-
|
|
1653
|
-
if role == "mark":
|
|
1654
|
-
rows.append('<div class="day"><span class="pill pill-compact">'
|
|
1655
|
-
'⋯ 이전 대화 요약(compaction) ⋯</span></div>')
|
|
1656
|
-
continue
|
|
1654
|
+
def __init__(self, redact_extra, redact_mode, open_details, markdown):
|
|
1655
|
+
self.extra, self.mode, self.markdown = redact_extra, redact_mode, markdown
|
|
1656
|
+
self.open_attr = " open" if open_details else ""
|
|
1657
|
+
self.rows, self.counts = [], {}
|
|
1658
|
+
self.last_date = self.last_session = None
|
|
1659
|
+
self.hits = self.total = 0
|
|
1657
1660
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
body = render_body(red, markdown=markdown)
|
|
1664
|
-
rows.append(
|
|
1665
|
-
'<div class="row me"><span class="time">%s</span>'
|
|
1666
|
-
'<div class="bubble"><details%s>'
|
|
1667
|
-
'<summary><span class="sum">%s</span></summary>'
|
|
1668
|
-
'<div class="detail">%s</div></details></div></div>'
|
|
1669
|
-
% (time_hm, open_attr, summary, body))
|
|
1670
|
-
else:
|
|
1671
|
-
rows.append(
|
|
1672
|
-
'<div class="row me"><span class="time">%s</span>'
|
|
1673
|
-
'<div class="bubble"><div class="plain">%s</div></div></div>'
|
|
1674
|
-
% (time_hm, render_body(red, markdown=markdown)))
|
|
1675
|
-
continue
|
|
1661
|
+
def red(self, text):
|
|
1662
|
+
r, c = page_redact(text, extra=self.extra, mode=self.mode)
|
|
1663
|
+
for k, v in c.items():
|
|
1664
|
+
self.counts[k] = self.counts.get(k, 0) + v
|
|
1665
|
+
return r
|
|
1676
1666
|
|
|
1677
|
-
# assistant or agent bubble
|
|
1678
|
-
summary, hit = _page_summary(t, summaries, session_id, rebuild,
|
|
1679
|
-
redact_extra, redact_mode)
|
|
1680
|
-
if hit is not None:
|
|
1681
|
-
cache_total += 1
|
|
1682
|
-
cache_hits += bool(hit)
|
|
1683
|
-
esc_sum = html.escape(_red(summary))
|
|
1684
|
-
detail_body = render_body(_red(t["text"]), markdown=markdown)
|
|
1685
|
-
|
|
1686
|
-
tools_html = ""
|
|
1687
|
-
if t.get("tools"):
|
|
1688
|
-
ts_list = ", ".join(f"{_hide_keywords(k, redact_extra)}×{v}"
|
|
1689
|
-
for k, v in sorted(t["tools"].items()))
|
|
1690
|
-
total = sum(t["tools"].values())
|
|
1691
|
-
tools_html = ('<div class="tools">🔧 도구 %d건: %s</div>'
|
|
1692
|
-
% (total, html.escape(ts_list)))
|
|
1693
|
-
|
|
1694
|
-
if role == "agent":
|
|
1695
|
-
who = html.escape(_red(t.get("agent_from") or "agent"))
|
|
1696
|
-
avatar = "🤝"
|
|
1697
|
-
from_line = '<div class="from">%s</div>' % who
|
|
1698
|
-
row_cls = "row bot agent"
|
|
1699
|
-
else:
|
|
1700
|
-
avatar = "C"
|
|
1701
|
-
from_line = ""
|
|
1702
|
-
row_cls = "row bot"
|
|
1703
1667
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1668
|
+
def _pills(rs, t, all_sessions):
|
|
1669
|
+
"""The session pill (--all-sessions, when the session changes) and the date pill (when the day changes) before
|
|
1670
|
+
turn `t`."""
|
|
1671
|
+
if all_sessions and t.get("session") != rs.last_session:
|
|
1672
|
+
rs.last_session = t.get("session")
|
|
1673
|
+
name = html.escape(rs.red(t.get("session_name") or rs.last_session or "session"))
|
|
1674
|
+
rs.rows.append('<div class="day" data-mark="%s">'
|
|
1675
|
+
'<span class="pill pill-session">%s</span></div>'
|
|
1676
|
+
% (name, name))
|
|
1677
|
+
rs.last_date = None
|
|
1678
|
+
ts = t.get("ts") or ""
|
|
1679
|
+
if ts:
|
|
1680
|
+
d = ts[:10]
|
|
1681
|
+
if d != rs.last_date:
|
|
1682
|
+
rs.rows.append('<div class="day" data-mark="%s">'
|
|
1683
|
+
'<span class="pill pill-date">%s</span></div>'
|
|
1684
|
+
% (html.escape(d), html.escape(d)))
|
|
1685
|
+
rs.last_date = d
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
def _user_row(rs, t, summaries, time_hm):
|
|
1689
|
+
"""A user bubble: plain, or folded under a summary (the reviewer's, else cut from it) past USER_FOLD."""
|
|
1690
|
+
red = rs.red(t["text"])
|
|
1691
|
+
if len(red) > USER_FOLD:
|
|
1692
|
+
reviewed = _reviewed_fold(t, summaries)
|
|
1693
|
+
summary = html.escape(rs.red(reviewed) if reviewed else user_fold_summary(red))
|
|
1694
|
+
body = render_body(red, markdown=rs.markdown)
|
|
1695
|
+
return ('<div class="row me"><span class="time">%s</span>'
|
|
1696
|
+
'<div class="bubble"><details%s>'
|
|
1697
|
+
'<summary><span class="sum">%s</span></summary>'
|
|
1698
|
+
'<div class="detail">%s</div></details></div></div>'
|
|
1699
|
+
% (time_hm, rs.open_attr, summary, body))
|
|
1700
|
+
return ('<div class="row me"><span class="time">%s</span>'
|
|
1701
|
+
'<div class="bubble"><div class="plain">%s</div></div></div>'
|
|
1702
|
+
% (time_hm, render_body(red, markdown=rs.markdown)))
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def _tools_line(tools, redact_extra):
|
|
1706
|
+
"""The `🔧 도구 N건` line of a bot bubble ("" without tools), the --redact-extra keywords hidden in the names."""
|
|
1707
|
+
if not tools:
|
|
1708
|
+
return ""
|
|
1709
|
+
ts_list = ", ".join(f"{_hide_keywords(k, redact_extra)}×{v}"
|
|
1710
|
+
for k, v in sorted(tools.items()))
|
|
1711
|
+
return ('<div class="tools">🔧 도구 %d건: %s</div>'
|
|
1712
|
+
% (sum(tools.values()), html.escape(ts_list)))
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
def _bot_row(rs, t, summary, time_hm):
|
|
1716
|
+
"""An assistant or agent bubble: the summary, the details (body, tools) and, for an agent, who sent it."""
|
|
1717
|
+
esc_sum = html.escape(rs.red(summary))
|
|
1718
|
+
detail_body = render_body(rs.red(t["text"]), markdown=rs.markdown)
|
|
1719
|
+
tools_html = _tools_line(t.get("tools"), rs.extra)
|
|
1720
|
+
role = t["role"]
|
|
1721
|
+
if role == "agent":
|
|
1722
|
+
who = html.escape(rs.red(t.get("agent_from") or "agent"))
|
|
1723
|
+
avatar, from_line, row_cls = "🤝", '<div class="from">%s</div>' % who, "row bot agent"
|
|
1724
|
+
else:
|
|
1725
|
+
avatar, from_line, row_cls = "C", "", "row bot"
|
|
1726
|
+
return ('<div class="%s"><div class="avatar%s">%s</div>'
|
|
1706
1727
|
'<div class="bubble"><details%s>'
|
|
1707
1728
|
'<summary><span class="sum">%s</span></summary>'
|
|
1708
1729
|
'<div class="detail">%s%s%s</div></details></div>'
|
|
1709
1730
|
'<span class="time">%s</span></div>'
|
|
1710
1731
|
% (row_cls, " agent" if role == "agent" else "", avatar,
|
|
1711
|
-
open_attr, esc_sum, from_line, detail_body, tools_html, time_hm))
|
|
1712
|
-
|
|
1732
|
+
rs.open_attr, esc_sum, from_line, detail_body, tools_html, time_hm))
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def render_rows(turns, session_id, redact_extra, redact_mode, rebuild,
|
|
1736
|
+
open_details=False, markdown=True, all_sessions=False, summaries=None):
|
|
1737
|
+
rs = _Rows(redact_extra, redact_mode, open_details, markdown)
|
|
1738
|
+
for t in turns:
|
|
1739
|
+
_pills(rs, t, all_sessions)
|
|
1740
|
+
time_hm = html.escape(_fmt_time(t.get("ts") or ""))
|
|
1741
|
+
if t["role"] == "mark":
|
|
1742
|
+
rs.rows.append('<div class="day"><span class="pill pill-compact">'
|
|
1743
|
+
'⋯ 이전 대화 요약(compaction) ⋯</span></div>')
|
|
1744
|
+
elif t["role"] == "user":
|
|
1745
|
+
rs.rows.append(_user_row(rs, t, summaries, time_hm))
|
|
1746
|
+
else:
|
|
1747
|
+
summary, hit = _page_summary(t, summaries, session_id, rebuild,
|
|
1748
|
+
redact_extra, redact_mode)
|
|
1749
|
+
if hit is not None:
|
|
1750
|
+
rs.total += 1
|
|
1751
|
+
rs.hits += bool(hit)
|
|
1752
|
+
rs.rows.append(_bot_row(rs, t, summary, time_hm))
|
|
1753
|
+
return rs.rows, rs.counts, rs.hits, rs.total
|
|
1713
1754
|
|
|
1714
1755
|
|
|
1715
1756
|
_TAG_RE = re.compile(r"<[^>]*>")
|
|
@@ -2004,12 +2045,15 @@ def _review_view(t, red, args):
|
|
|
2004
2045
|
def _pack_turn(n, t, tid, why, session_id, args, counts):
|
|
2005
2046
|
"""One turn of the pack under its final id `tid` (the text as the page shows it, the rules'
|
|
2006
2047
|
decision, the reviewer's slot), and the reviewer's view of it. The cached decision keys on
|
|
2007
|
-
the text with the page's patterns only (`llm_key`), as 3.0.1 did
|
|
2048
|
+
the text with the page's patterns only (`llm_key`), as 3.0.1 did, its split secrets joined first: a key
|
|
2049
|
+
found only once joined keys anew, so no decision 3.0.8 or earlier cached for such a turn is read (it is
|
|
2050
|
+
reviewed once more)."""
|
|
2008
2051
|
red, found = page_redact(t["text"], extra=args.redact_extra, mode=args.redact_mode)
|
|
2009
2052
|
for k, v in found.items():
|
|
2010
2053
|
counts[k] = counts.get(k, 0) + v
|
|
2011
2054
|
hit = any(name in found for name, _ in REVIEW_SECRET_PATTERNS)
|
|
2012
|
-
key_text = redact(t["text"],
|
|
2055
|
+
key_text = (redact(_rejoin_secrets(t["text"], args.redact_extra), extra=args.redact_extra,
|
|
2056
|
+
mode=args.redact_mode)[0] if hit else red)
|
|
2013
2057
|
preview, clipped = _preview(review_redact(t["text"], args.redact_extra)[0])
|
|
2014
2058
|
session = t.get("session") or session_id
|
|
2015
2059
|
context = _decision_context(why, _keeps(args))
|
|
@@ -2025,6 +2069,8 @@ def _pack_turn(n, t, tid, why, session_id, args, counts):
|
|
|
2025
2069
|
"llm": dict(cached, cached=True) if cached else {"keep": None, "summary": None}}
|
|
2026
2070
|
if t.get("meta"):
|
|
2027
2071
|
turn["meta"] = True
|
|
2072
|
+
if t["role"] == "user" and _bang_output(t["text"]):
|
|
2073
|
+
turn["bang_output"] = True # for the dropped-turn WARN only: no part file carries it
|
|
2028
2074
|
for k in ("session", "session_name", "agent_from"):
|
|
2029
2075
|
if t.get(k):
|
|
2030
2076
|
turn[k] = t[k] if k == "session" else page_redact(t[k], args.redact_extra, args.redact_mode)[0]
|
|
@@ -2093,7 +2139,8 @@ def _sheet(k, of, part_path, group, args):
|
|
|
2093
2139
|
turns = []
|
|
2094
2140
|
for x, view in group:
|
|
2095
2141
|
y = {key: v for key, v in x.items()
|
|
2096
|
-
if key not in ("text", "session", "session_name", "agent_from", "redactions", "llm_key"
|
|
2142
|
+
if key not in ("text", "session", "session_name", "agent_from", "redactions", "llm_key",
|
|
2143
|
+
"bang_output")}
|
|
2097
2144
|
y["rule"] = {k: v for k, v in x["rule"].items() if k != "review_summary"}
|
|
2098
2145
|
y["rule"]["summary"] = view["summary"]
|
|
2099
2146
|
y["tools"] = {_hide_keywords(name, args.redact_extra): n for name, n in x["tools"].items()}
|
|
@@ -2348,7 +2395,7 @@ def _show(x, llm_summary, turns, summaries, stats, prior):
|
|
|
2348
2395
|
if isinstance(v, int):
|
|
2349
2396
|
prior[k] = prior.get(k, 0) + v
|
|
2350
2397
|
# what the gate's critic reads: the reviewer's summary, else the rules' cut from the
|
|
2351
|
-
# fully redacted text (the page's
|
|
2398
|
+
# fully redacted text (the page's may hold a --redact-mode mask: 4 + 4 characters of a value)
|
|
2352
2399
|
sample = (llm_summary if shows else None) or x["rule"].get("review_summary")
|
|
2353
2400
|
turns.append({"uuid": x["id"], "role": x["role"], "text": x["text"],
|
|
2354
2401
|
"ts": x.get("ts") or None, "tools": dict(x.get("tools") or {}),
|
|
@@ -2370,6 +2417,23 @@ def _kept_by_flag(pack, x):
|
|
|
2370
2417
|
return pack.get("keep_tool_only") is True and tool_only
|
|
2371
2418
|
|
|
2372
2419
|
|
|
2420
|
+
def _bang_output(text):
|
|
2421
|
+
"""Whether `text` is the record Claude Code writes after a `!` command and nothing else (2.1.304, no isMeta): a
|
|
2422
|
+
<bash-stdout> block then a <bash-stderr> block, or one <bash-stderr> block when the command failed. String checks
|
|
2423
|
+
only, linear in the text: a pattern repeating the blocks could take exponential time on many closing tags."""
|
|
2424
|
+
if text.startswith("<bash-stdout>"):
|
|
2425
|
+
return "</bash-stdout><bash-stderr>" in text and text.endswith("</bash-stderr>")
|
|
2426
|
+
return text.startswith("<bash-stderr>") and text.endswith("</bash-stderr>")
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
def _typed_by_user(x):
|
|
2430
|
+
"""Whether pack turn `x` holds words the user typed: a user turn the rules keep that is neither a body the
|
|
2431
|
+
harness injected (meta) nor what Claude Code writes after a `!` command (bang_output, judged on the text before
|
|
2432
|
+
redaction: a hidden value at the end of the output takes the closing tags with it)."""
|
|
2433
|
+
return (x["role"] == "user" and x["rule"]["keep"] and not x.get("meta")
|
|
2434
|
+
and x.get("bang_output") is not True)
|
|
2435
|
+
|
|
2436
|
+
|
|
2373
2437
|
def _review_result(pack, merged, args):
|
|
2374
2438
|
"""(turns to render, their summaries, cache writes, counts, prior redactions, the ids of
|
|
2375
2439
|
turns the user typed that the reviewers dropped) from the pack and the decisions: a
|
|
@@ -2384,7 +2448,7 @@ def _review_result(pack, merged, args):
|
|
|
2384
2448
|
held = llm_keep is False and _kept_by_flag(pack, x)
|
|
2385
2449
|
stats["flag-kept"] += held
|
|
2386
2450
|
keep = bool(_first(None if held else llm_keep, x["rule"]["keep"]))
|
|
2387
|
-
if not keep and x
|
|
2451
|
+
if not keep and _typed_by_user(x):
|
|
2388
2452
|
typed.append(x["id"]) # typed by the user: the rules keep these
|
|
2389
2453
|
llm_summary = _reviewer_summary(x, d, args, stats)
|
|
2390
2454
|
if x["id"] in merged: # reviewed, even if left to the rules
|
|
@@ -2613,114 +2677,149 @@ def _flag_clash(args):
|
|
|
2613
2677
|
return None
|
|
2614
2678
|
|
|
2615
2679
|
|
|
2616
|
-
def
|
|
2617
|
-
|
|
2618
|
-
|
|
2680
|
+
def _merge_env_extra(args):
|
|
2681
|
+
"""LINEAGE_REDACT_EXTRA's keywords after --redact-extra's."""
|
|
2619
2682
|
env_extra = os.environ.get("LINEAGE_REDACT_EXTRA", "").strip()
|
|
2620
2683
|
if env_extra:
|
|
2621
2684
|
args.redact_extra = ",".join(
|
|
2622
2685
|
p for p in (args.redact_extra, env_extra) if p)
|
|
2623
2686
|
|
|
2624
|
-
clash = _flag_clash(args)
|
|
2625
|
-
if clash:
|
|
2626
|
-
print(f"[lineage] ERROR: {clash}", file=sys.stderr)
|
|
2627
|
-
return 2
|
|
2628
2687
|
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2688
|
+
def _purge_cache():
|
|
2689
|
+
import shutil
|
|
2690
|
+
if CACHE_BASE.exists():
|
|
2691
|
+
shutil.rmtree(CACHE_BASE)
|
|
2692
|
+
print(f"[lineage] cache purged: {CACHE_BASE}", file=sys.stderr)
|
|
2693
|
+
else:
|
|
2694
|
+
print("[lineage] no cache to purge", file=sys.stderr)
|
|
2695
|
+
return 0
|
|
2637
2696
|
|
|
2638
|
-
if args.apply_review:
|
|
2639
|
-
if any((args.last, args.from_, args.to, args.turns, args.session,
|
|
2640
|
-
args.all_sessions, args.from_transcript)):
|
|
2641
|
-
print("[lineage] note: --apply-review renders the pack; source and range "
|
|
2642
|
-
"flags were applied at --emit-review", file=sys.stderr)
|
|
2643
|
-
return apply_review(args)
|
|
2644
2697
|
|
|
2645
|
-
|
|
2698
|
+
def _run_apply(args):
|
|
2699
|
+
if any((args.last, args.from_, args.to, args.turns, args.session,
|
|
2700
|
+
args.all_sessions, args.from_transcript)):
|
|
2701
|
+
print("[lineage] note: --apply-review renders the pack; source and range "
|
|
2702
|
+
"flags were applied at --emit-review", file=sys.stderr)
|
|
2703
|
+
return apply_review(args)
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
def _read_all_sessions(args):
|
|
2707
|
+
"""Every session of the project folder, merged record by record in time order."""
|
|
2708
|
+
files = project_jsonl_files()
|
|
2709
|
+
if not files:
|
|
2710
|
+
print("[lineage] --all-sessions: no jsonl found in project dir",
|
|
2711
|
+
file=sys.stderr)
|
|
2712
|
+
return 2, [], "session", None
|
|
2713
|
+
starts = {p: session_start(p) for p in files}
|
|
2646
2714
|
turns = []
|
|
2647
|
-
|
|
2648
|
-
|
|
2715
|
+
for p in files:
|
|
2716
|
+
name = discover_session_name(p) or p.stem[:8]
|
|
2717
|
+
part = _load_turns_from(p, args.unsafe_schema, p.stem, name)
|
|
2718
|
+
fill_missing_ts(part)
|
|
2719
|
+
for t in part:
|
|
2720
|
+
t["session_ts"] = starts[p] or "9999"
|
|
2721
|
+
turns.extend(part)
|
|
2722
|
+
# record-level chronological sort (stable → same-ts keeps file order)
|
|
2723
|
+
turns.sort(key=lambda t: t.get("ts") or t.get("session_ts") or "9999")
|
|
2724
|
+
print(f"[lineage] --all-sessions: merged {len(files)} sessions",
|
|
2725
|
+
file=sys.stderr)
|
|
2726
|
+
return None, turns, "all-sessions", None
|
|
2727
|
+
|
|
2728
|
+
|
|
2729
|
+
def _read_transcript(args):
|
|
2730
|
+
"""A transcript pasted on stdin ('-') or read from a file."""
|
|
2731
|
+
if args.from_transcript == "-":
|
|
2732
|
+
turns = list(parse_turns(sys.stdin, unsafe_schema=args.unsafe_schema,
|
|
2733
|
+
session_id="stdin"))
|
|
2734
|
+
return None, turns, "stdin", None
|
|
2735
|
+
jsonl_path = pathlib.Path(args.from_transcript)
|
|
2736
|
+
session_id = "file-" + jsonl_path.stem
|
|
2737
|
+
turns = _load_turns_from(jsonl_path, args.unsafe_schema, session_id, None)
|
|
2738
|
+
return None, turns, session_id, jsonl_path
|
|
2739
|
+
|
|
2740
|
+
|
|
2741
|
+
def _read_discovered(args):
|
|
2742
|
+
"""The newest session of the project folder."""
|
|
2743
|
+
f = auto_discover_jsonl()
|
|
2744
|
+
if not f:
|
|
2745
|
+
print("[lineage] No jsonl found. Use one of:",
|
|
2746
|
+
"\n --session FILE explicit jsonl path",
|
|
2747
|
+
"\n --all-sessions merge all project sessions",
|
|
2748
|
+
"\n --from-transcript - paste transcript via stdin",
|
|
2749
|
+
"\n --from-transcript FILE read transcript from file",
|
|
2750
|
+
"\n(non-ASCII cwd may not auto-encode — use --session)",
|
|
2751
|
+
file=sys.stderr)
|
|
2752
|
+
return 2, [], "session", None
|
|
2753
|
+
turns = _load_turns_from(f, args.unsafe_schema, f.stem, None)
|
|
2754
|
+
print(f"[lineage] auto-discovered session: {f.name}", file=sys.stderr)
|
|
2755
|
+
return None, turns, f.stem, f
|
|
2756
|
+
|
|
2757
|
+
|
|
2758
|
+
def _read_input(args):
|
|
2759
|
+
"""(exit code or None, raw turns, session id, jsonl path) from the source the flags name. The code is 2 when
|
|
2760
|
+
there is no source, or the code a schema marker stopped the read with."""
|
|
2649
2761
|
try:
|
|
2650
2762
|
if args.all_sessions:
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
return 2
|
|
2656
|
-
starts = {p: session_start(p) for p in files}
|
|
2657
|
-
for p in files:
|
|
2658
|
-
name = discover_session_name(p) or p.stem[:8]
|
|
2659
|
-
part = _load_turns_from(p, args.unsafe_schema, p.stem, name)
|
|
2660
|
-
fill_missing_ts(part)
|
|
2661
|
-
for t in part:
|
|
2662
|
-
t["session_ts"] = starts[p] or "9999"
|
|
2663
|
-
turns.extend(part)
|
|
2664
|
-
# record-level chronological sort (stable → same-ts keeps file order)
|
|
2665
|
-
turns.sort(key=lambda t: t.get("ts") or t.get("session_ts") or "9999")
|
|
2666
|
-
session_id = "all-sessions"
|
|
2667
|
-
print(f"[lineage] --all-sessions: merged {len(files)} sessions",
|
|
2668
|
-
file=sys.stderr)
|
|
2669
|
-
elif args.from_transcript:
|
|
2670
|
-
if args.from_transcript == "-":
|
|
2671
|
-
turns = list(parse_turns(sys.stdin, unsafe_schema=args.unsafe_schema,
|
|
2672
|
-
session_id="stdin"))
|
|
2673
|
-
session_id = "stdin"
|
|
2674
|
-
else:
|
|
2675
|
-
jsonl_path = pathlib.Path(args.from_transcript)
|
|
2676
|
-
session_id = "file-" + jsonl_path.stem
|
|
2677
|
-
turns = _load_turns_from(jsonl_path, args.unsafe_schema,
|
|
2678
|
-
session_id, None)
|
|
2679
|
-
elif args.session:
|
|
2763
|
+
return _read_all_sessions(args)
|
|
2764
|
+
if args.from_transcript:
|
|
2765
|
+
return _read_transcript(args)
|
|
2766
|
+
if args.session:
|
|
2680
2767
|
jsonl_path = pathlib.Path(args.session)
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
else:
|
|
2685
|
-
f = auto_discover_jsonl()
|
|
2686
|
-
if not f:
|
|
2687
|
-
print("[lineage] No jsonl found. Use one of:",
|
|
2688
|
-
"\n --session FILE explicit jsonl path",
|
|
2689
|
-
"\n --all-sessions merge all project sessions",
|
|
2690
|
-
"\n --from-transcript - paste transcript via stdin",
|
|
2691
|
-
"\n --from-transcript FILE read transcript from file",
|
|
2692
|
-
"\n(non-ASCII cwd may not auto-encode — use --session)",
|
|
2693
|
-
file=sys.stderr)
|
|
2694
|
-
return 2
|
|
2695
|
-
jsonl_path = f
|
|
2696
|
-
session_id = f.stem
|
|
2697
|
-
turns = _load_turns_from(f, args.unsafe_schema, session_id, None)
|
|
2698
|
-
print(f"[lineage] auto-discovered session: {f.name}", file=sys.stderr)
|
|
2768
|
+
turns = _load_turns_from(jsonl_path, args.unsafe_schema, jsonl_path.stem, None)
|
|
2769
|
+
return None, turns, jsonl_path.stem, jsonl_path
|
|
2770
|
+
return _read_discovered(args)
|
|
2699
2771
|
except SystemExit as e:
|
|
2700
|
-
return int(e.code) if isinstance(e.code, int) else 2
|
|
2772
|
+
return (int(e.code) if isinstance(e.code, int) else 2), [], "session", None
|
|
2701
2773
|
|
|
2702
|
-
# stdin → derive per-turn uuid from the content so the caches work (keyed: see _stdin_id_key)
|
|
2703
|
-
if args.from_transcript == "-":
|
|
2704
|
-
key = _stdin_id_key()
|
|
2705
|
-
for t in turns:
|
|
2706
|
-
t["uuid"] = hmac.new(key, str(t["text"]).encode(), hashlib.sha256).hexdigest()[:16]
|
|
2707
2774
|
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2775
|
+
def _stdin_ids(turns):
|
|
2776
|
+
"""stdin → derive per-turn uuid from the content so the caches work (keyed: see _stdin_id_key)."""
|
|
2777
|
+
key = _stdin_id_key()
|
|
2778
|
+
for t in turns:
|
|
2779
|
+
t["uuid"] = hmac.new(key, str(t["text"]).encode(), hashlib.sha256).hexdigest()[:16]
|
|
2780
|
+
|
|
2781
|
+
|
|
2782
|
+
def _run_emit(turns, session_id, jsonl_path, args):
|
|
2783
|
+
drops = heuristic_drops(turns, args)
|
|
2784
|
+
window = review_window(turns, drops, args)
|
|
2785
|
+
if window is None:
|
|
2786
|
+
return 2
|
|
2787
|
+
return emit_review(turns, drops, window, session_id,
|
|
2788
|
+
default_output(args, jsonl_path, session_id), args)
|
|
2789
|
+
|
|
2790
|
+
|
|
2791
|
+
def _rulebase_turns(turns, args):
|
|
2792
|
+
"""The turns a --rulebase page shows: echo exchanges and tool-only turns gone (unless kept), then the range.
|
|
2793
|
+
None on an invalid --turns."""
|
|
2718
2794
|
if args.drop_trivia:
|
|
2719
2795
|
turns = drop_echo_exchanges(turns)
|
|
2720
2796
|
if args.hide_tool_only:
|
|
2721
2797
|
turns = [t for t in turns
|
|
2722
2798
|
if not (t["role"] == "assistant" and not t["text"].strip())]
|
|
2723
|
-
|
|
2799
|
+
return select_range(turns, args)
|
|
2800
|
+
|
|
2801
|
+
|
|
2802
|
+
def main(argv=None) -> int:
|
|
2803
|
+
args = build_arg_parser().parse_args(argv)
|
|
2804
|
+
_merge_env_extra(args)
|
|
2805
|
+
clash = _flag_clash(args)
|
|
2806
|
+
if clash:
|
|
2807
|
+
print(f"[lineage] ERROR: {clash}", file=sys.stderr)
|
|
2808
|
+
return 2
|
|
2809
|
+
if args.purge_cache:
|
|
2810
|
+
return _purge_cache()
|
|
2811
|
+
if args.apply_review:
|
|
2812
|
+
return _run_apply(args)
|
|
2813
|
+
rc, turns, session_id, jsonl_path = _read_input(args)
|
|
2814
|
+
if rc is not None:
|
|
2815
|
+
return rc
|
|
2816
|
+
if args.from_transcript == "-":
|
|
2817
|
+
_stdin_ids(turns)
|
|
2818
|
+
# ---- Pipeline order (R6): classify → merge → echo → hide-tool-only → range ----
|
|
2819
|
+
turns = merge_assistant_runs(classify_turns(turns, drop_trivia=args.drop_trivia))
|
|
2820
|
+
if args.emit_review:
|
|
2821
|
+
return _run_emit(turns, session_id, jsonl_path, args)
|
|
2822
|
+
turns = _rulebase_turns(turns, args)
|
|
2724
2823
|
if turns is None:
|
|
2725
2824
|
return 2
|
|
2726
2825
|
if args.output is None:
|
|
@@ -2823,8 +2922,8 @@ def render_and_write(turns, session_id, args, all_sessions=False, markdown=True,
|
|
|
2823
2922
|
date_range = first if first == last else f"{first} ~ {last}"
|
|
2824
2923
|
|
|
2825
2924
|
html_doc = HTML_TEMPLATE
|
|
2826
|
-
html_doc = html_doc.replace("{{TITLE}}", html.escape(title), 1)
|
|
2827
|
-
html_doc = html_doc.replace("{{HEADER_TITLE}}", html.escape(title), 1)
|
|
2925
|
+
html_doc = html_doc.replace("{{TITLE}}", html.escape(_show_bidi_controls(title)), 1)
|
|
2926
|
+
html_doc = html_doc.replace("{{HEADER_TITLE}}", html.escape(_show_bidi_controls(title)), 1)
|
|
2828
2927
|
html_doc = html_doc.replace("{{DATE_RANGE}}", html.escape(_show_bidi_controls(date_range)), 1)
|
|
2829
2928
|
html_doc = html_doc.replace("{{TURNS}}", _show_bidi_controls("\n".join(rows)), 1)
|
|
2830
2929
|
|