@deftai/directive-core 0.95.0 → 0.96.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.
Files changed (47) hide show
  1. package/dist/cache/archive.d.ts +134 -0
  2. package/dist/cache/archive.js +630 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/cache/main.js +298 -1
  6. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  7. package/dist/content-contracts/skills/helpers.js +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/init-deposit/hygiene.d.ts +1 -1
  11. package/dist/init-deposit/hygiene.js +16 -4
  12. package/dist/init-deposit/scaffold.js +6 -5
  13. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  14. package/dist/parent-turn-shape/evaluate.js +353 -0
  15. package/dist/parent-turn-shape/index.d.ts +8 -0
  16. package/dist/parent-turn-shape/index.js +8 -0
  17. package/dist/review-monitor/constants.js +3 -2
  18. package/dist/review-monitor/tier-detection.d.ts +7 -3
  19. package/dist/review-monitor/tier-detection.js +18 -1
  20. package/dist/review-monitor/verify.js +18 -0
  21. package/dist/scope/index.d.ts +2 -0
  22. package/dist/scope/index.js +2 -0
  23. package/dist/scope/main.d.ts +10 -0
  24. package/dist/scope/main.js +109 -24
  25. package/dist/scope/promote-from-issue.d.ts +49 -0
  26. package/dist/scope/promote-from-issue.js +367 -0
  27. package/dist/scope/promote-path.d.ts +39 -0
  28. package/dist/scope/promote-path.js +105 -0
  29. package/dist/swarm/routing.d.ts +4 -2
  30. package/dist/swarm/routing.js +26 -4
  31. package/dist/triage/actions/index.js +62 -2
  32. package/dist/triage/actions/types.d.ts +8 -1
  33. package/dist/triage/author-filter.d.ts +51 -0
  34. package/dist/triage/author-filter.js +152 -0
  35. package/dist/triage/classify/index.d.ts +2 -2
  36. package/dist/triage/classify/index.js +2 -2
  37. package/dist/triage/classify/label-mirror.d.ts +68 -5
  38. package/dist/triage/classify/label-mirror.js +261 -31
  39. package/dist/triage/help/registry-data.d.ts +49 -38
  40. package/dist/triage/help/registry-data.js +115 -40
  41. package/dist/triage/index.d.ts +1 -0
  42. package/dist/triage/index.js +1 -0
  43. package/dist/triage/queue/index.d.ts +1 -0
  44. package/dist/triage/queue/index.js +1 -0
  45. package/dist/triage/queue/render.d.ts +2 -0
  46. package/dist/triage/queue/render.js +6 -0
  47. package/package.json +7 -3
@@ -1,5 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { latestDecisions, readAuditLog } from "../triage/actions/candidates-log.js";
4
+ import { resolveCandidatesLogPath } from "../triage/cache-path.js";
5
+ import { archiveClosedEntries, DEFAULT_ARCHIVE_OLDER_THAN_DAYS, listArchivedEntries, restoreFromArchive, } from "./archive.js";
3
6
  import { ALLOWED_SOURCES, DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS, DEFAULT_PRUNE_OLDER_THAN_DAYS, } from "./constants.js";
4
7
  import { CacheCapBreachedError, CacheError, CacheFetchError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
5
8
  import { cacheFetchAll, cacheRefreshClosed } from "./fetch.js";
@@ -8,7 +11,7 @@ import { cacheGet, cacheInvalidate, cachePrune, cachePruneToCap, cachePut } from
8
11
  import { resolveCaps } from "./quota.js";
9
12
  import { clearTaskCache } from "./task-cache/store.js";
10
13
  function usage() {
11
- process.stderr.write("usage: cache [-h] {put,get,invalidate,fetch-all,prune,clear} ...\n");
14
+ process.stderr.write("usage: cache [-h] {put,get,invalidate,fetch-all,prune,clear,archive-closed,archive-list,restore-from-archive} ...\n");
12
15
  }
13
16
  function normaliseLabelFilter(raw) {
14
17
  if (!raw || raw.length === 0)
@@ -309,6 +312,293 @@ function cmdPrune(args) {
309
312
  process.stdout.write(`${pythonJsonPretty(payload)}\n`);
310
313
  return 0;
311
314
  }
315
+ /**
316
+ * Reversible closed-entry archive (#1137). Operator-only; not TTL prune.
317
+ * Distinct from `cache prune` (hard-delete by expires_at).
318
+ */
319
+ function cmdArchiveClosed(args) {
320
+ let olderThanDays = DEFAULT_ARCHIVE_OLDER_THAN_DAYS;
321
+ let source = "github-issue";
322
+ let repo;
323
+ let dryRun = false;
324
+ let json = false;
325
+ let terminalDecisionOnly = false;
326
+ let projectRoot;
327
+ let cacheRoot;
328
+ let reason;
329
+ for (let i = 0; i < args.length; i += 1) {
330
+ const arg = args[i];
331
+ if (arg === "--older-than-days") {
332
+ olderThanDays = Number.parseInt(args[i + 1] ?? "", 10);
333
+ i += 1;
334
+ }
335
+ else if (arg === "--source") {
336
+ source = args[i + 1] ?? "github-issue";
337
+ i += 1;
338
+ }
339
+ else if (arg === "--repo") {
340
+ repo = args[i + 1];
341
+ i += 1;
342
+ }
343
+ else if (arg === "--dry-run") {
344
+ dryRun = true;
345
+ }
346
+ else if (arg === "--json") {
347
+ json = true;
348
+ }
349
+ else if (arg === "--terminal-decision-only") {
350
+ terminalDecisionOnly = true;
351
+ }
352
+ else if (arg === "--project-root") {
353
+ projectRoot = args[i + 1];
354
+ i += 1;
355
+ }
356
+ else if (arg === "--cache-root") {
357
+ cacheRoot = args[i + 1];
358
+ i += 1;
359
+ }
360
+ else if (arg === "--reason") {
361
+ reason = args[i + 1];
362
+ i += 1;
363
+ }
364
+ else if (arg === "-h" || arg === "--help") {
365
+ process.stdout.write("usage: cache archive-closed [--dry-run] [--older-than-days 30] [--repo OWNER/NAME]\n" +
366
+ " [--source github-issue] [--json] [--terminal-decision-only]\n" +
367
+ " [--project-root PATH] [--cache-root PATH] [--reason TEXT]\n" +
368
+ "\n" +
369
+ "Reversible archive of closed github-issue cache entries (not TTL cache:prune).\n" +
370
+ "Skips issues still referenced by xbrief/{proposed,pending,active}.\n" +
371
+ "Also available as: task triage:cache-archive\n");
372
+ return 0;
373
+ }
374
+ else {
375
+ throw new CacheError(`unexpected argument: ${arg}`);
376
+ }
377
+ }
378
+ const resolvedProjectRoot = projectRoot ?? process.cwd();
379
+ const resolvedCacheRoot = cacheRoot ?? (projectRoot !== undefined ? resolve(projectRoot, ".deft-cache") : undefined);
380
+ let latestDecisionMap = null;
381
+ if (terminalDecisionOnly) {
382
+ try {
383
+ const logPath = resolveCandidatesLogPath(resolvedProjectRoot);
384
+ latestDecisionMap = latestDecisions(readAuditLog(logPath, repo ?? null));
385
+ }
386
+ catch {
387
+ latestDecisionMap = new Map();
388
+ }
389
+ }
390
+ const result = archiveClosedEntries({
391
+ olderThanDays,
392
+ source,
393
+ repo: repo ?? null,
394
+ dryRun,
395
+ terminalDecisionOnly,
396
+ latestDecisions: latestDecisionMap,
397
+ projectRoot: resolvedProjectRoot,
398
+ cacheRoot: resolvedCacheRoot,
399
+ reason,
400
+ });
401
+ const payload = {
402
+ mode: "archive-closed",
403
+ note: "Reversible closed-entry archive; distinct from cache:prune (TTL hard-delete).",
404
+ dry_run: result.dryRun,
405
+ older_than_days: result.olderThanDays,
406
+ source: result.source,
407
+ repo: result.repo,
408
+ terminal_decision_only: terminalDecisionOnly,
409
+ archived_count: result.archivedCount,
410
+ skipped_count: result.skippedCount,
411
+ archived: result.archived.map((c) => ({
412
+ key: c.key,
413
+ live_dir: c.liveDir,
414
+ archive_dir: c.archiveDir,
415
+ age_basis: c.ageBasis,
416
+ closed_at: c.closedAt,
417
+ pre_archive_decision: c.preArchiveDecision,
418
+ })),
419
+ skipped: result.skipped.map((s) => ({
420
+ key: s.key,
421
+ reason: s.reason,
422
+ detail: s.detail ?? null,
423
+ })),
424
+ };
425
+ if (json) {
426
+ process.stdout.write(`${pythonJsonPretty(payload)}\n`);
427
+ }
428
+ else {
429
+ process.stdout.write(`cache:archive-closed dry_run=${pythonBool(result.dryRun)} archived=${result.archivedCount} skipped=${result.skippedCount} older_than_days=${result.olderThanDays}\n`);
430
+ for (const c of result.archived) {
431
+ process.stdout.write(` archived ${c.key} -> ${c.archiveDir}\n`);
432
+ }
433
+ for (const s of result.skipped) {
434
+ process.stdout.write(` skipped ${s.key} (${s.reason}${s.detail ? `: ${s.detail}` : ""})\n`);
435
+ }
436
+ }
437
+ return 0;
438
+ }
439
+ function cmdArchiveList(args) {
440
+ let source = "github-issue";
441
+ let repo;
442
+ let since;
443
+ let limit;
444
+ let format = "text";
445
+ let cacheRoot;
446
+ let projectRoot;
447
+ for (let i = 0; i < args.length; i += 1) {
448
+ const arg = args[i];
449
+ if (arg === "--source") {
450
+ source = args[i + 1] ?? "github-issue";
451
+ i += 1;
452
+ }
453
+ else if (arg === "--repo") {
454
+ repo = args[i + 1];
455
+ i += 1;
456
+ }
457
+ else if (arg === "--since") {
458
+ since = args[i + 1];
459
+ i += 1;
460
+ }
461
+ else if (arg === "--limit") {
462
+ limit = Number.parseInt(args[i + 1] ?? "", 10);
463
+ i += 1;
464
+ }
465
+ else if (arg === "--format") {
466
+ format = args[i + 1] ?? "text";
467
+ i += 1;
468
+ }
469
+ else if (arg === "--format=json" || arg === "--json") {
470
+ format = "json";
471
+ }
472
+ else if (arg?.startsWith("--format=")) {
473
+ format = arg.slice("--format=".length);
474
+ }
475
+ else if (arg === "--cache-root") {
476
+ cacheRoot = args[i + 1];
477
+ i += 1;
478
+ }
479
+ else if (arg === "--project-root") {
480
+ projectRoot = args[i + 1];
481
+ i += 1;
482
+ }
483
+ else if (arg === "-h" || arg === "--help") {
484
+ process.stdout.write("usage: cache archive-list [--repo OWNER/NAME] [--since ISO] [--limit N]\n" +
485
+ " [--format=json|text] [--source github-issue] [--cache-root PATH]\n" +
486
+ "Also available as: task triage:archive-list\n");
487
+ return 0;
488
+ }
489
+ else {
490
+ throw new CacheError(`unexpected argument: ${arg}`);
491
+ }
492
+ }
493
+ const resolvedCacheRoot = cacheRoot ?? (projectRoot !== undefined ? resolve(projectRoot, ".deft-cache") : undefined);
494
+ const result = listArchivedEntries({
495
+ source,
496
+ repo: repo ?? null,
497
+ since: since ?? null,
498
+ limit: limit ?? null,
499
+ cacheRoot: resolvedCacheRoot,
500
+ });
501
+ if (format === "json") {
502
+ process.stdout.write(`${pythonJsonPretty({
503
+ source: result.source,
504
+ repo: result.repo,
505
+ count: result.count,
506
+ entries: result.entries.map((e) => ({
507
+ key: e.key,
508
+ archived_at: e.archivedAt,
509
+ archive_dir: e.archiveDir,
510
+ meta: e.meta,
511
+ })),
512
+ })}\n`);
513
+ }
514
+ else {
515
+ process.stdout.write(`cache:archive-list count=${result.count}\n`);
516
+ for (const e of result.entries) {
517
+ process.stdout.write(` ${e.key} archived_at=${e.archivedAt}\n`);
518
+ }
519
+ }
520
+ return 0;
521
+ }
522
+ function cmdRestoreFromArchive(args) {
523
+ let source = "github-issue";
524
+ let key;
525
+ let issue;
526
+ let repo;
527
+ let force = false;
528
+ let cacheRoot;
529
+ let projectRoot;
530
+ let json = false;
531
+ for (let i = 0; i < args.length; i += 1) {
532
+ const arg = args[i];
533
+ if (arg === "--source") {
534
+ source = args[i + 1] ?? "github-issue";
535
+ i += 1;
536
+ }
537
+ else if (arg === "--key") {
538
+ key = args[i + 1];
539
+ i += 1;
540
+ }
541
+ else if (arg === "--issue") {
542
+ const rawIssue = args[i + 1] ?? "";
543
+ if (!/^[1-9]\d*$/.test(rawIssue)) {
544
+ throw new CacheError(`--issue must be a positive integer (got ${JSON.stringify(rawIssue)})`);
545
+ }
546
+ issue = Number.parseInt(rawIssue, 10);
547
+ i += 1;
548
+ }
549
+ else if (arg === "--repo") {
550
+ repo = args[i + 1];
551
+ i += 1;
552
+ }
553
+ else if (arg === "--force") {
554
+ force = true;
555
+ }
556
+ else if (arg === "--json") {
557
+ json = true;
558
+ }
559
+ else if (arg === "--cache-root") {
560
+ cacheRoot = args[i + 1];
561
+ i += 1;
562
+ }
563
+ else if (arg === "--project-root") {
564
+ projectRoot = args[i + 1];
565
+ i += 1;
566
+ }
567
+ else if (arg === "-h" || arg === "--help") {
568
+ process.stdout.write("usage: cache restore-from-archive -- --issue N --repo OWNER/NAME [--force]\n" +
569
+ " [--key owner/repo/N] [--source github-issue] [--json]\n" +
570
+ "Also available as: task triage:restore-from-archive\n");
571
+ return 0;
572
+ }
573
+ else if (!key && arg && !arg.startsWith("-") && arg.includes("/")) {
574
+ key = arg;
575
+ }
576
+ else {
577
+ throw new CacheError(`unexpected argument: ${arg}`);
578
+ }
579
+ }
580
+ const resolvedCacheRoot = cacheRoot ?? (projectRoot !== undefined ? resolve(projectRoot, ".deft-cache") : undefined);
581
+ const result = restoreFromArchive({
582
+ source,
583
+ key,
584
+ issue,
585
+ repo: repo ?? null,
586
+ force,
587
+ cacheRoot: resolvedCacheRoot,
588
+ });
589
+ if (json) {
590
+ process.stdout.write(`${pythonJsonPretty(result)}\n`);
591
+ }
592
+ else {
593
+ process.stdout.write(`cache:restore-from-archive status=${result.status} key=${result.key} live=${result.liveDir}\n`);
594
+ if (result.detail) {
595
+ process.stdout.write(` detail: ${result.detail}\n`);
596
+ }
597
+ }
598
+ if (result.status === "missing" || result.status === "conflict")
599
+ return 1;
600
+ return 0;
601
+ }
312
602
  /** CLI entry point (mirrors `scripts/cache.py::main`). */
313
603
  export function main(argv) {
314
604
  if (argv.length === 0) {
@@ -332,6 +622,13 @@ export function main(argv) {
332
622
  return cmdPrune(rest);
333
623
  case "clear":
334
624
  return cmdClear(rest);
625
+ case "archive-closed":
626
+ case "cache-archive":
627
+ return cmdArchiveClosed(rest);
628
+ case "archive-list":
629
+ return cmdArchiveList(rest);
630
+ case "restore-from-archive":
631
+ return cmdRestoreFromArchive(rest);
335
632
  default:
336
633
  usage();
337
634
  process.stderr.write(`cache: error: argument cmd: invalid choice: '${cmd}'\n`);
@@ -21,7 +21,7 @@ export declare function readSkill(relPath: string): string;
21
21
  */
22
22
  export declare const SWARM_SKILL_REL = "skills/deft-directive-swarm/SKILL.md";
23
23
  /** Stable load order: core phases, then host launch adapters, then ops. */
24
- export declare const SWARM_REFERENCE_ORDER: readonly ["core-phase-0.md", "core-phase-1-2.md", "core-phase-3.md", "host-warp.md", "host-generic.md", "host-grok-build.md", "host-cursor.md", "host-openclaw.md", "core-phase-4.md", "core-phase-5-6.md", "core-ops.md"];
24
+ export declare const SWARM_REFERENCE_ORDER: readonly ["core-phase-0.md", "core-phase-1-2.md", "core-phase-3.md", "host-warp.md", "host-generic.md", "host-grok-build.md", "host-cursor.md", "host-claude-code.md", "host-openclaw.md", "core-phase-4.md", "core-phase-5-6.md", "core-ops.md"];
25
25
  export declare function readSwarmSkillSurface(): string;
26
26
  export declare function readAgentsMd(): string;
27
27
  /** Slice the first `## Returning Sessions` section body out of AGENTS.md. */
@@ -48,6 +48,7 @@ export const SWARM_REFERENCE_ORDER = [
48
48
  "host-generic.md",
49
49
  "host-grok-build.md",
50
50
  "host-cursor.md",
51
+ "host-claude-code.md",
51
52
  "host-openclaw.md",
52
53
  "core-phase-4.md",
53
54
  "core-phase-5-6.md",
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export * as lifecycle from "./lifecycle/index.js";
35
35
  export * as metrics from "./metrics/index.js";
36
36
  export * as orchestration from "./orchestration/index.js";
37
37
  export * as packs from "./packs/index.js";
38
+ export * as parentTurnShape from "./parent-turn-shape/index.js";
38
39
  export * as platform from "./platform/index.js";
39
40
  export * as policy from "./policy/index.js";
40
41
  export * as prClosingKeywords from "./pr-closing-keywords/index.js";
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ export * as lifecycle from "./lifecycle/index.js";
36
36
  export * as metrics from "./metrics/index.js";
37
37
  export * as orchestration from "./orchestration/index.js";
38
38
  export * as packs from "./packs/index.js";
39
+ export * as parentTurnShape from "./parent-turn-shape/index.js";
39
40
  export * as platform from "./platform/index.js";
40
41
  export * as policy from "./policy/index.js";
41
42
  export * as prClosingKeywords from "./pr-closing-keywords/index.js";
@@ -8,7 +8,7 @@
8
8
  * and scope briefs are never installer-managed; if they reappear in
9
9
  * `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
10
10
  *
11
- * Refs #1576, #1453, #1430, #3029, #3030.
11
+ * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
12
12
  */
13
13
  import { type InitDepositIo } from "./constants.js";
14
14
  export declare const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
@@ -8,7 +8,7 @@
8
8
  * and scope briefs are never installer-managed; if they reappear in
9
9
  * `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
10
10
  *
11
- * Refs #1576, #1453, #1430, #3029, #3030.
11
+ * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
12
12
  */
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { existsSync, readdirSync, rmSync } from "node:fs";
@@ -82,6 +82,16 @@ export function installerManagedMatchers() {
82
82
  { exact: CODEQL_CONFIG_REL },
83
83
  { exact: CORE_GUARD_WORKFLOW_REL },
84
84
  { exact: "Taskfile.yml" },
85
+ // Upgrade co-travel unit (#3127): npm pin + lock follow-through and the
86
+ // freshness live stamp (#3117) are part of a normal framework upgrade, not
87
+ // product feature work. Path-level allow for v1 (stricter “only
88
+ // @deftai/directive version changed” is a follow-on if needed).
89
+ // Mixing .deft/core/** with true app/product paths still fails (#1430).
90
+ { exact: "package.json" },
91
+ { exact: "package-lock.json" },
92
+ { exact: "pnpm-lock.yaml" },
93
+ { exact: "yarn.lock" },
94
+ { exact: ".deft/GENERATION.json" },
85
95
  // Legacy vbrief/ tree -- retained for not-yet-migrated consumers.
86
96
  { exact: "vbrief/.deft-version" },
87
97
  { exact: "vbrief/vbrief.md" },
@@ -476,9 +486,11 @@ export function printCommitGuidance(io, paths, staged) {
476
486
  if (paths.length === 0)
477
487
  return;
478
488
  const addCmd = `git add ${paths.join(" ")}`;
479
- io.printf("\nCommit hygiene (#1453, #1671): keep the framework deposit in its OWN branch/PR.\n");
480
- io.printf("Do NOT use `git add -A` -- mixing the payload with your own files trips the\n");
489
+ io.printf("\nCommit hygiene (#1453, #1671, #3127): keep the framework upgrade in its OWN branch/PR.\n");
490
+ io.printf("Do NOT use `git add -A` -- mixing the payload with product/app files trips the\n");
481
491
  io.printf("deft-core-guard CI check.\n");
492
+ io.printf("One upgrade PR MAY co-travel: .deft/core/** + installer-managed deposits + package.json\n");
493
+ io.printf("pin/lock + .deft/GENERATION.json. True app/product paths still require a separate PR.\n");
482
494
  if (staged) {
483
495
  io.printf("The installer already staged ONLY these framework + installer-managed paths:\n");
484
496
  io.printf(` ${addCmd}\n`);
@@ -488,7 +500,7 @@ export function printCommitGuidance(io, paths, staged) {
488
500
  io.printf(` ${addCmd}\n`);
489
501
  }
490
502
  io.printf("Then take the framework deposit through the full PR lifecycle so deft-core-guard\n");
491
- io.printf("evaluates a clean, standalone PR:\n");
503
+ io.printf("evaluates a clean, standalone upgrade PR:\n");
492
504
  io.printf(` 1. Branch: git switch -c ${COMMIT_HYGIENE_BRANCH_NAME}\n`);
493
505
  io.printf(' 2. Commit: git commit -m "chore(deft): update framework payload"\n');
494
506
  io.printf(` 3. Push: git push -u origin ${COMMIT_HYGIENE_BRANCH_NAME}\n`);
@@ -507,11 +507,12 @@ function coreGuardWorkflowContent() {
507
507
  const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
508
508
  const headSha = githubActionsExpr("github.event.pull_request.head.sha");
509
509
  return ("name: deft-core-guard\n\n" +
510
- "# Deft framework guard (#1430): a single PR should not mix changes to the\n" +
511
- "# vendored framework payload (.deft/core/**) with changes to your own project\n" +
512
- "# files. Framework updates come from `deft-install` / upgrade and should\n" +
513
- "# land in their own PR so reviewers (and bot reviewers) can treat them as\n" +
514
- "# packaged, machine-managed assets. Delete this file if you do not want the guard.\n" +
510
+ "# Deft framework guard (#1430 / #3127): a single PR should not mix changes to the\n" +
511
+ "# vendored framework payload (.deft/core/**) with true application/product files.\n" +
512
+ "# One upgrade PR MAY include deposit + installer-managed paths + package.json pin/lock\n" +
513
+ "# + .deft/GENERATION.json. Framework updates come from `deft update` / install and should\n" +
514
+ "# land without product feature work so reviewers treat the payload as machine-managed.\n" +
515
+ "# Delete this file if you do not want the guard.\n" +
515
516
  "on:\n" +
516
517
  " pull_request:\n\n" +
517
518
  "permissions:\n" +
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Parent turn-shape hard-stop for text-repetition hang (FC14 / #3131).
3
+ *
4
+ * Soft skill prose (#2943) alone is insufficient: parents still burn the full
5
+ * output budget on identical progress sentences with zero tool_use after leaf
6
+ * announce. This pure library is the Directive-side machine-checkable gate.
7
+ *
8
+ * Hosts (OpenClaw, swarm parents, review monitors) SHOULD evaluate the parent
9
+ * turn stream mid-generation and hard-stop when `ok` is false.
10
+ *
11
+ * Illegal shape (MUST NOT):
12
+ * N > maxIdenticalWithoutTool (default 2) near-identical assistant sentences
13
+ * (or streaming text chunks) in one turn with no tool_use and no yield.
14
+ *
15
+ * Legal post-announce shapes:
16
+ * 1. Tool batch (ground-truth) then one consolidate
17
+ * 2. sessions_yield / wait without filler
18
+ * 3. One short user answer that is NOT a repeated progress line
19
+ */
20
+ /** Failure class codes for operator / host recovery surfaces. */
21
+ export type ParentTurnFailClass = "none" | "FC14" | "text-repetition-hang" | "progress-only-no-tool";
22
+ /** Ordered events observed within a single parent turn. */
23
+ export type ParentTurnEvent = {
24
+ kind: "assistant_text";
25
+ text: string;
26
+ } | {
27
+ kind: "tool_use";
28
+ name?: string;
29
+ } | {
30
+ kind: "yield";
31
+ };
32
+ export interface ParentTurnShapeInput {
33
+ /** Ordered events from one parent turn (streaming deltas may be separate). */
34
+ readonly events: readonly ParentTurnEvent[];
35
+ /**
36
+ * When true, multi-sentence progress-only text with zero tools/yield is also
37
+ * illegal even without exact N>2 identity (post-`subagent_announce` policy).
38
+ */
39
+ readonly afterSubagentAnnounce?: boolean;
40
+ /**
41
+ * Max near-identical consecutive (or within-turn) text units allowed without
42
+ * tool_use / yield. Issue contract: MUST NOT emit N>2 → default max = 2.
43
+ */
44
+ readonly maxIdenticalWithoutTool?: number;
45
+ }
46
+ export interface ParentTurnShapeResult {
47
+ readonly ok: boolean;
48
+ /** Primary fail class (`FC14` aliases `text-repetition-hang`). */
49
+ readonly failClass: ParentTurnFailClass;
50
+ readonly reasons: readonly string[];
51
+ /** Highest count of near-identical text units observed without tool/yield. */
52
+ readonly maxIdenticalCount: number;
53
+ readonly hasToolUse: boolean;
54
+ readonly hasYield: boolean;
55
+ }
56
+ /** Canonical fail class for the hang (#3131 / soft FC14 recurrence of #2943). */
57
+ export declare const PARENT_TURN_FAIL_FC14: "FC14";
58
+ /** Default max identical text units without tool/yield (N>2 is illegal). */
59
+ export declare const DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL = 2;
60
+ /**
61
+ * Normalize assistant text for identity comparison.
62
+ * Collapses whitespace, lowercases, strips common trailing punctuation.
63
+ */
64
+ export declare function normalizeTurnText(text: string): string;
65
+ /** Split a blob into sentence-like units (periods, newlines, ellipsis). */
66
+ export declare function splitTextUnits(text: string): string[];
67
+ /**
68
+ * True when two normalized strings are near-identical (exact, containment with
69
+ * high length ratio, or high word Jaccard). Short units never match.
70
+ */
71
+ export declare function isNearIdentical(a: string, b: string): boolean;
72
+ /**
73
+ * Detect when a single blob itself embeds the same sentence many times
74
+ * (model streams one giant assistant message of repeated lines).
75
+ */
76
+ export declare function countRepeatedUnitsInBlob(text: string): number;
77
+ /**
78
+ * Evaluate whether a parent turn shape is legal under the FC14 hard-stop.
79
+ * Pure / side-effect free — safe for mid-stream host gates and unit tests.
80
+ */
81
+ export declare function evaluateParentTurnShape(input: ParentTurnShapeInput): ParentTurnShapeResult;
82
+ /** Convenience: true when the turn is an illegal text-repetition hang. */
83
+ export declare function isTextRepetitionHang(input: ParentTurnShapeInput): boolean;
84
+ //# sourceMappingURL=evaluate.d.ts.map