@deftai/directive-core 0.95.0 → 0.97.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 (116) hide show
  1. package/dist/cache/archive.d.ts +134 -0
  2. package/dist/cache/archive.js +636 -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/check/gate-lists.js +8 -0
  7. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  8. package/dist/consumer-check-contract/evaluate.js +699 -0
  9. package/dist/consumer-check-contract/index.d.ts +5 -0
  10. package/dist/consumer-check-contract/index.js +5 -0
  11. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  12. package/dist/content-contracts/skills/helpers.js +1 -0
  13. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  14. package/dist/delivery-attempt/disk-begin.js +68 -0
  15. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  16. package/dist/delivery-attempt/evaluate.js +443 -0
  17. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  18. package/dist/delivery-attempt/fingerprint.js +100 -0
  19. package/dist/delivery-attempt/handoff.d.ts +25 -0
  20. package/dist/delivery-attempt/handoff.js +102 -0
  21. package/dist/delivery-attempt/index.d.ts +17 -0
  22. package/dist/delivery-attempt/index.js +17 -0
  23. package/dist/delivery-attempt/ledger.d.ts +169 -0
  24. package/dist/delivery-attempt/ledger.js +758 -0
  25. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  26. package/dist/delivery-attempt/material-delta.js +126 -0
  27. package/dist/delivery-attempt/types.d.ts +210 -0
  28. package/dist/delivery-attempt/types.js +77 -0
  29. package/dist/doctor/index.d.ts +1 -0
  30. package/dist/doctor/index.js +1 -0
  31. package/dist/doctor/main.js +12 -0
  32. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  33. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  34. package/dist/hooks/dispatcher.d.ts +2 -1
  35. package/dist/hooks/dispatcher.js +63 -9
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +5 -0
  38. package/dist/init-deposit/gitignore.js +7 -0
  39. package/dist/init-deposit/hygiene.d.ts +1 -1
  40. package/dist/init-deposit/hygiene.js +16 -4
  41. package/dist/init-deposit/init-deposit.js +5 -0
  42. package/dist/init-deposit/refresh.js +3 -0
  43. package/dist/init-deposit/scaffold.js +6 -5
  44. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  45. package/dist/parent-turn-shape/evaluate.js +353 -0
  46. package/dist/parent-turn-shape/index.d.ts +8 -0
  47. package/dist/parent-turn-shape/index.js +8 -0
  48. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  49. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  50. package/dist/pr-merge-readiness/compute.js +10 -1
  51. package/dist/pr-merge-readiness/index.d.ts +2 -1
  52. package/dist/pr-merge-readiness/index.js +2 -1
  53. package/dist/pr-merge-readiness/output.js +14 -0
  54. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  55. package/dist/pr-merge-readiness/platform-status.js +49 -0
  56. package/dist/pr-watch/constants.d.ts +10 -0
  57. package/dist/pr-watch/constants.js +12 -1
  58. package/dist/pr-watch/main.js +16 -1
  59. package/dist/pr-watch/probe.js +13 -1
  60. package/dist/pr-watch/types.d.ts +3 -2
  61. package/dist/pr-watch/watch.js +14 -7
  62. package/dist/review-monitor/constants.js +3 -2
  63. package/dist/review-monitor/tier-detection.d.ts +7 -3
  64. package/dist/review-monitor/tier-detection.js +18 -1
  65. package/dist/review-monitor/verify.js +18 -0
  66. package/dist/scope/index.d.ts +2 -0
  67. package/dist/scope/index.js +2 -0
  68. package/dist/scope/main.d.ts +10 -0
  69. package/dist/scope/main.js +109 -24
  70. package/dist/scope/promote-from-issue.d.ts +49 -0
  71. package/dist/scope/promote-from-issue.js +367 -0
  72. package/dist/scope/promote-path.d.ts +39 -0
  73. package/dist/scope/promote-path.js +105 -0
  74. package/dist/scope-provenance/digest.d.ts +67 -0
  75. package/dist/scope-provenance/digest.js +188 -0
  76. package/dist/scope-provenance/evaluate.d.ts +82 -0
  77. package/dist/scope-provenance/evaluate.js +528 -0
  78. package/dist/scope-provenance/index.d.ts +6 -0
  79. package/dist/scope-provenance/index.js +6 -0
  80. package/dist/session/compact-ritual.d.ts +96 -0
  81. package/dist/session/compact-ritual.js +237 -0
  82. package/dist/session/compact-ritual.spec.d.ts +2 -0
  83. package/dist/session/compact-ritual.spec.js +21 -0
  84. package/dist/session/index.d.ts +2 -0
  85. package/dist/session/index.js +2 -0
  86. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  87. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  88. package/dist/swarm/routing.d.ts +4 -2
  89. package/dist/swarm/routing.js +26 -4
  90. package/dist/test-boundary/evaluate.d.ts +54 -0
  91. package/dist/test-boundary/evaluate.js +368 -0
  92. package/dist/test-boundary/index.d.ts +6 -0
  93. package/dist/test-boundary/index.js +6 -0
  94. package/dist/test-boundary/policy.d.ts +52 -0
  95. package/dist/test-boundary/policy.js +182 -0
  96. package/dist/triage/actions/index.js +62 -2
  97. package/dist/triage/actions/types.d.ts +8 -1
  98. package/dist/triage/author-filter.d.ts +51 -0
  99. package/dist/triage/author-filter.js +152 -0
  100. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  101. package/dist/triage/bootstrap/gitignore.js +15 -1
  102. package/dist/triage/classify/index.d.ts +2 -2
  103. package/dist/triage/classify/index.js +2 -2
  104. package/dist/triage/classify/label-mirror.d.ts +68 -5
  105. package/dist/triage/classify/label-mirror.js +261 -31
  106. package/dist/triage/help/registry-data.d.ts +49 -38
  107. package/dist/triage/help/registry-data.js +115 -40
  108. package/dist/triage/index.d.ts +1 -0
  109. package/dist/triage/index.js +1 -0
  110. package/dist/triage/queue/index.d.ts +1 -0
  111. package/dist/triage/queue/index.js +1 -0
  112. package/dist/triage/queue/render.d.ts +2 -0
  113. package/dist/triage/queue/render.js +6 -0
  114. package/dist/vbrief-activate/activate.js +22 -6
  115. package/dist/xbrief/styles.js +33 -17
  116. package/package.json +11 -3
@@ -1,10 +1,14 @@
1
1
  /**
2
- * Tier-1 deterministic SCM label mirror (#1423 Wave 1).
2
+ * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
3
3
  *
4
4
  * Classifies cached issues with the existing #1129 engine, then mirrors the
5
5
  * outcome as SCM labels (dry-run default, --apply to write). Never accepts into
6
6
  * the xBRIEF lifecycle and never writes proposed/ scopes.
7
7
  *
8
+ * Wave 2 (#3125): open-only default, operator digest (totals + by state/rule/action
9
+ * + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
10
+ * `triage:classify -- --mirror` with these filters (not triage:accept).
11
+ *
8
12
  * Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
9
13
  * engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
10
14
  */
@@ -16,11 +20,18 @@ import { readPlanPolicy } from "../../policy/plan-extensions.js";
16
20
  import { ScmLabelClient } from "../../vbrief-reconcile/labels.js";
17
21
  import { isRepoMutationAllowed } from "../../vbrief-reconcile/repo-guard.js";
18
22
  import { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
23
+ import { authorLoginFromRawIssue, matchesAuthorFilter, } from "../author-filter.js";
19
24
  import { resolveCandidatesLogPath } from "../cache-path.js";
20
25
  import { iterCachedIssues } from "../summary/index.js";
21
26
  export const DEFAULT_IDEMPOTENCY_LABEL = "triaged";
22
27
  export const CACHE_DIR_NAME = ".deft-cache";
23
28
  export const CACHE_SOURCE = "github-issue";
29
+ /** Default apply batch size for rate-limit awareness (#3125). */
30
+ export const DEFAULT_APPLY_BATCH_SIZE = 10;
31
+ /** Default delay between apply batches in ms (#3125). */
32
+ export const DEFAULT_APPLY_DELAY_MS = 1000;
33
+ /** Default sample count in human digest (#3125). */
34
+ export const DEFAULT_DIGEST_SAMPLE_LIMIT = 15;
24
35
  const VALID_ACTIONS = new Set(["defer", "archive", "escalate", "accept"]);
25
36
  /**
26
37
  * Repo-qualified xBRIEF github-issue keys (`owner/name\\0number`).
@@ -308,10 +319,72 @@ function loadLatestDecisionMap(projectRoot) {
308
319
  function decisionMapHas(map, repo, issueNumber) {
309
320
  return map.has(`${repo}\0${issueNumber}`);
310
321
  }
322
+ function defaultSleepMs(ms) {
323
+ if (ms <= 0) {
324
+ return;
325
+ }
326
+ // Sync sleep: LabelClient.apply is sync; keep mirrorLabels non-async (#3125).
327
+ const sab = new SharedArrayBuffer(4);
328
+ const ia = new Int32Array(sab);
329
+ Atomics.wait(ia, 0, 0, ms);
330
+ }
331
+ function normalizeIssueState(raw) {
332
+ if (typeof raw !== "string" || raw.trim().length === 0) {
333
+ return null;
334
+ }
335
+ return raw.trim().toLowerCase();
336
+ }
337
+ function isClosedState(state) {
338
+ return state === "closed";
339
+ }
340
+ /** Build digest aggregates + samples from mirror items (#3125). */
341
+ export function buildLabelMirrorDigest(items, sampleLimit = DEFAULT_DIGEST_SAMPLE_LIMIT) {
342
+ const limit = Number.isFinite(sampleLimit) && sampleLimit >= 0
343
+ ? Math.floor(sampleLimit)
344
+ : DEFAULT_DIGEST_SAMPLE_LIMIT;
345
+ const byState = {};
346
+ const byRule = {};
347
+ const byAction = {};
348
+ const writeItems = items.filter((i) => i.status === "planned" || i.status === "applied");
349
+ for (const item of writeItems) {
350
+ const st = item.state ?? "unknown";
351
+ byState[st] = (byState[st] ?? 0) + 1;
352
+ const rule = item.ruleKind ?? "(none)";
353
+ byRule[rule] = (byRule[rule] ?? 0) + 1;
354
+ const action = item.action ?? "(none)";
355
+ byAction[action] = (byAction[action] ?? 0) + 1;
356
+ }
357
+ const samples = writeItems.slice(0, limit);
358
+ return {
359
+ by_state: byState,
360
+ by_rule: byRule,
361
+ by_action: byAction,
362
+ samples,
363
+ sample_limit: limit,
364
+ sample_truncated: writeItems.length > limit,
365
+ };
366
+ }
367
+ function formatMissingLabelHint(message, labels) {
368
+ const lower = message.toLowerCase();
369
+ const looksMissing = lower.includes("not found") ||
370
+ lower.includes("could not add label") ||
371
+ lower.includes("invalid label") ||
372
+ lower.includes("unknown label") ||
373
+ (lower.includes("label") && (lower.includes("404") || lower.includes("does not exist")));
374
+ if (!looksMissing) {
375
+ return message;
376
+ }
377
+ const want = labels.length > 0 ? labels.join(", ") : "triaged";
378
+ return (`${message} — ensure label(s) exist on the repo before --apply ` +
379
+ `(create missing labels e.g. \`gh label create "${want.split(",")[0]?.trim() ?? "triaged"}"\`; ` +
380
+ `idempotency + actionLabels must exist or apply fails closed per issue).`);
381
+ }
311
382
  /**
312
- * Run Tier-1 label mirror over the github-issue cache.
383
+ * Run Tier-1 label mirror over the github-issue cache (bootstrap mass-triage surface).
313
384
  * Dry-run by default (no SCM writes). Pass dryRun: false to apply.
385
+ * Default state filter is open-only (#3125); pass includeClosed: true for archive stamps.
314
386
  * Requires options.engine (classify/index wrapper injects it).
387
+ * Never calls triage:accept / never writes proposed/ xBRIEFs.
315
388
  */
316
389
  export function mirrorLabels(projectRoot, options) {
317
390
  const root = resolve(projectRoot);
@@ -321,12 +394,39 @@ export function mirrorLabels(projectRoot, options) {
321
394
  const useLiveLabels = options.useLiveLabels ?? !dryRun;
322
395
  const client = options.client ?? (useLiveLabels || !dryRun ? new ScmLabelClient() : undefined);
323
396
  const engine = options.engine;
397
+ const includeClosed = options.includeClosed === true;
398
+ const authorFilter = options.authorFilter !== undefined && options.authorFilter !== null
399
+ ? options.authorFilter
400
+ : null;
401
+ const sampleLimit = options.sampleLimit ?? DEFAULT_DIGEST_SAMPLE_LIMIT;
402
+ const batchSize = options.batchSize !== undefined
403
+ ? Math.max(1, Math.floor(options.batchSize))
404
+ : DEFAULT_APPLY_BATCH_SIZE;
405
+ const delayMs = options.delayMs !== undefined && options.delayMs >= 0
406
+ ? Math.floor(options.delayMs)
407
+ : dryRun
408
+ ? 0
409
+ : DEFAULT_APPLY_DELAY_MS;
410
+ const sleepMs = options.sleepMs ?? defaultSleepMs;
411
+ const repoFilter = options.repo !== undefined && options.repo !== null && options.repo.trim().length > 0
412
+ ? options.repo.trim()
413
+ : null;
414
+ const filters = {
415
+ include_closed: includeClosed,
416
+ repo: repoFilter,
417
+ author: authorFilter !== null ? authorFilter.display : null,
418
+ author_logins: authorFilter !== null ? authorFilter.allowLogins : null,
419
+ };
324
420
  const items = [];
325
421
  const outcomeBase = {
326
422
  project_root: root,
327
423
  dry_run: dryRun,
328
424
  policy,
425
+ filters,
426
+ batch_size: dryRun ? undefined : batchSize,
427
+ delay_ms: dryRun ? undefined : delayMs,
329
428
  };
429
+ const emptyDigest = buildLabelMirrorDigest([], sampleLimit);
330
430
  if (!policy.enabled) {
331
431
  return [
332
432
  0,
@@ -339,11 +439,15 @@ export function mirrorLabels(projectRoot, options) {
339
439
  skipped_already_triaged: 0,
340
440
  skipped_no_match: 0,
341
441
  skipped_unreadable: 0,
442
+ skipped_closed: 0,
443
+ skipped_author: 0,
342
444
  errors: 0,
445
+ digest: emptyDigest,
343
446
  items: [
344
447
  {
345
448
  repo: "",
346
449
  issue_number: 0,
450
+ state: null,
347
451
  action: null,
348
452
  reason: null,
349
453
  ruleKind: null,
@@ -365,8 +469,8 @@ export function mirrorLabels(projectRoot, options) {
365
469
  const decisions = loadLatestDecisionMap(root);
366
470
  const now = options.now ?? new Date();
367
471
  let pairs = iterCachedIssues(cacheRoot);
368
- if (options.repo !== undefined && options.repo !== null && options.repo.trim().length > 0) {
369
- const want = options.repo.trim().toLowerCase();
472
+ if (repoFilter !== null) {
473
+ const want = repoFilter.toLowerCase();
370
474
  pairs = pairs.filter(([repo]) => repo.toLowerCase() === want);
371
475
  }
372
476
  let planned = 0;
@@ -375,7 +479,10 @@ export function mirrorLabels(projectRoot, options) {
375
479
  let skippedAlready = 0;
376
480
  let skippedNoMatch = 0;
377
481
  let skippedUnreadable = 0;
482
+ let skippedClosed = 0;
483
+ let skippedAuthor = 0;
378
484
  let errors = 0;
485
+ let applyWritesSinceSleep = 0;
379
486
  for (const [repo, issueNumber] of pairs) {
380
487
  const issue = readCachedRawIssue(cacheRoot, repo, issueNumber);
381
488
  if (issue === null) {
@@ -383,6 +490,7 @@ export function mirrorLabels(projectRoot, options) {
383
490
  items.push({
384
491
  repo,
385
492
  issue_number: issueNumber,
493
+ state: null,
386
494
  action: null,
387
495
  reason: null,
388
496
  ruleKind: null,
@@ -394,6 +502,48 @@ export function mirrorLabels(projectRoot, options) {
394
502
  });
395
503
  continue;
396
504
  }
505
+ const state = normalizeIssueState(issue.state);
506
+ // Open-only default: skip closed before classify (avoids mass-stamping archive).
507
+ if (!includeClosed && isClosedState(state)) {
508
+ skippedClosed += 1;
509
+ items.push({
510
+ repo,
511
+ issue_number: issueNumber,
512
+ state,
513
+ action: null,
514
+ reason: null,
515
+ ruleKind: null,
516
+ current: issueLabelNames(issue).sort(),
517
+ desired: [],
518
+ add: [],
519
+ status: "skipped_closed",
520
+ message: "open-only default; pass includeClosed / --include-closed to mirror closed issues",
521
+ });
522
+ continue;
523
+ }
524
+ // Author filter (#3129): AND with open-only / other filters; missing author = non-match.
525
+ if (authorFilter !== null) {
526
+ const login = authorLoginFromRawIssue(issue);
527
+ if (!matchesAuthorFilter(login, authorFilter)) {
528
+ skippedAuthor += 1;
529
+ items.push({
530
+ repo,
531
+ issue_number: issueNumber,
532
+ state,
533
+ action: null,
534
+ reason: null,
535
+ ruleKind: null,
536
+ current: issueLabelNames(issue).sort(),
537
+ desired: [],
538
+ add: [],
539
+ status: "skipped_author",
540
+ message: login === null
541
+ ? `author filter ${authorFilter.display}: missing author on cache row (unknown — excluded)`
542
+ : `author filter ${authorFilter.display}: author.login=${login} does not match`,
543
+ });
544
+ continue;
545
+ }
546
+ }
397
547
  let current;
398
548
  if (useLiveLabels && client !== undefined) {
399
549
  try {
@@ -406,6 +556,7 @@ export function mirrorLabels(projectRoot, options) {
406
556
  items.push({
407
557
  repo,
408
558
  issue_number: issueNumber,
559
+ state,
409
560
  action: null,
410
561
  reason: null,
411
562
  ruleKind: null,
@@ -427,6 +578,7 @@ export function mirrorLabels(projectRoot, options) {
427
578
  items.push({
428
579
  repo,
429
580
  issue_number: issueNumber,
581
+ state,
430
582
  action: null,
431
583
  reason: null,
432
584
  ruleKind: null,
@@ -455,6 +607,7 @@ export function mirrorLabels(projectRoot, options) {
455
607
  items.push({
456
608
  repo,
457
609
  issue_number: issueNumber,
610
+ state,
458
611
  action: null,
459
612
  reason: null,
460
613
  ruleKind: null,
@@ -473,6 +626,7 @@ export function mirrorLabels(projectRoot, options) {
473
626
  items.push({
474
627
  repo,
475
628
  issue_number: issueNumber,
629
+ state,
476
630
  action: classification.action,
477
631
  reason: classification.reason,
478
632
  ruleKind: classification.ruleKind,
@@ -488,6 +642,7 @@ export function mirrorLabels(projectRoot, options) {
488
642
  items.push({
489
643
  repo,
490
644
  issue_number: issueNumber,
645
+ state,
491
646
  action: classification.action,
492
647
  reason: classification.reason,
493
648
  ruleKind: classification.ruleKind,
@@ -498,7 +653,7 @@ export function mirrorLabels(projectRoot, options) {
498
653
  });
499
654
  continue;
500
655
  }
501
- // --apply path: SCM boundary + write
656
+ // --apply path: SCM boundary + write (batched + delay for rate-limit awareness)
502
657
  const mutateGate = isRepoMutationAllowed(repo, root, {
503
658
  allowCrossRepo: options.allowCrossRepo,
504
659
  allowlist: options.repoAllowlist,
@@ -509,6 +664,7 @@ export function mirrorLabels(projectRoot, options) {
509
664
  items.push({
510
665
  repo,
511
666
  issue_number: issueNumber,
667
+ state,
512
668
  action: classification.action,
513
669
  reason: classification.reason,
514
670
  ruleKind: classification.ruleKind,
@@ -525,6 +681,7 @@ export function mirrorLabels(projectRoot, options) {
525
681
  items.push({
526
682
  repo,
527
683
  issue_number: issueNumber,
684
+ state,
528
685
  action: classification.action,
529
686
  reason: classification.reason,
530
687
  ruleKind: classification.ruleKind,
@@ -537,11 +694,18 @@ export function mirrorLabels(projectRoot, options) {
537
694
  continue;
538
695
  }
539
696
  try {
697
+ // Count every SCM write *attempt* toward the batch (including failures) so
698
+ // rate-limit delay still applies under partial failure storms (#3125 Greptile P1).
699
+ if (applyWritesSinceSleep > 0 && applyWritesSinceSleep % batchSize === 0 && delayMs > 0) {
700
+ sleepMs(delayMs);
701
+ }
702
+ applyWritesSinceSleep += 1;
540
703
  client.apply(repo, issueNumber, add, []);
541
704
  applied += 1;
542
705
  items.push({
543
706
  repo,
544
707
  issue_number: issueNumber,
708
+ state,
545
709
  action: classification.action,
546
710
  reason: classification.reason,
547
711
  ruleKind: classification.ruleKind,
@@ -553,9 +717,11 @@ export function mirrorLabels(projectRoot, options) {
553
717
  }
554
718
  catch (exc) {
555
719
  errors += 1;
720
+ const rawMsg = exc instanceof Error ? exc.message : String(exc);
556
721
  items.push({
557
722
  repo,
558
723
  issue_number: issueNumber,
724
+ state,
559
725
  action: classification.action,
560
726
  reason: classification.reason,
561
727
  ruleKind: classification.ruleKind,
@@ -563,10 +729,12 @@ export function mirrorLabels(projectRoot, options) {
563
729
  desired,
564
730
  add,
565
731
  status: "error",
566
- message: exc instanceof Error ? exc.message : String(exc),
732
+ message: formatMissingLabelHint(rawMsg, add),
567
733
  });
734
+ // Partial failure: continue remaining issues (idempotent re-run skips applied).
568
735
  }
569
736
  }
737
+ const digest = buildLabelMirrorDigest(items, sampleLimit);
570
738
  const outcome = {
571
739
  ...outcomeBase,
572
740
  scanned: pairs.length,
@@ -576,35 +744,75 @@ export function mirrorLabels(projectRoot, options) {
576
744
  skipped_already_triaged: skippedAlready,
577
745
  skipped_no_match: skippedNoMatch,
578
746
  skipped_unreadable: skippedUnreadable,
747
+ skipped_closed: skippedClosed,
748
+ skipped_author: skippedAuthor,
579
749
  errors,
750
+ digest,
580
751
  items,
581
752
  };
582
753
  return [errors > 0 ? 1 : 0, outcome];
583
754
  }
584
- /** Human-readable digest for dry-run / apply reports. */
755
+ function formatCountMap(map) {
756
+ const keys = Object.keys(map).sort();
757
+ if (keys.length === 0) {
758
+ return [" (none)"];
759
+ }
760
+ return keys.map((k) => ` ${k}: ${map[k]}`);
761
+ }
762
+ /** Human-readable digest for dry-run / apply reports (bootstrap mass-triage UX). */
585
763
  export function renderLabelMirrorReport(outcome) {
586
764
  const lines = [];
587
765
  const mode = outcome.dry_run ? "dry-run" : "apply";
588
- lines.push(`triage:classify --mirror (${mode})`);
766
+ lines.push(`triage:classify --mirror (${mode}) — bootstrap mass-triage (#1423 Wave 2)`);
767
+ const stateFilter = outcome.filters.include_closed ? "all (include-closed)" : "open-only";
768
+ const repoPart = outcome.filters.repo ?? "*";
769
+ const authorPart = outcome.filters.author ?? "*";
770
+ lines.push(`filters: state=${stateFilter} repo=${repoPart} author=${authorPart}`);
589
771
  lines.push(`scanned=${outcome.scanned} planned=${outcome.planned} applied=${outcome.applied} ` +
590
772
  `unchanged=${outcome.unchanged} already_triaged=${outcome.skipped_already_triaged} ` +
591
- `no_match=${outcome.skipped_no_match} unreadable=${outcome.skipped_unreadable} ` +
592
- `errors=${outcome.errors}`);
773
+ `no_match=${outcome.skipped_no_match} closed_skipped=${outcome.skipped_closed} ` +
774
+ `author_skipped=${outcome.skipped_author} ` +
775
+ `unreadable=${outcome.skipped_unreadable} errors=${outcome.errors}`);
593
776
  lines.push(`idempotencyLabel=${outcome.policy.idempotencyLabel} alwaysLabels=${JSON.stringify(outcome.policy.alwaysLabels)}`);
777
+ if (!outcome.dry_run) {
778
+ lines.push(`apply: batch_size=${outcome.batch_size ?? DEFAULT_APPLY_BATCH_SIZE} delay_ms=${outcome.delay_ms ?? DEFAULT_APPLY_DELAY_MS}`);
779
+ }
594
780
  lines.push("");
595
- const plannedOrApplied = outcome.items.filter((i) => i.status === "planned" || i.status === "applied");
596
- lines.push(outcome.dry_run ? "Would add labels:" : "Added labels:");
597
- if (plannedOrApplied.length === 0) {
781
+ lines.push("By state (planned/applied):");
782
+ lines.push(...formatCountMap(outcome.digest.by_state));
783
+ lines.push("By rule (planned/applied):");
784
+ lines.push(...formatCountMap(outcome.digest.by_rule));
785
+ lines.push("By action (planned/applied):");
786
+ lines.push(...formatCountMap(outcome.digest.by_action));
787
+ lines.push("");
788
+ const writeTotal = outcome.planned + outcome.applied;
789
+ lines.push(outcome.dry_run
790
+ ? `Samples (up to ${outcome.digest.sample_limit} of ${writeTotal} planned):`
791
+ : `Samples (up to ${outcome.digest.sample_limit} of ${writeTotal} applied/planned):`);
792
+ if (outcome.digest.samples.length === 0) {
598
793
  lines.push("- none");
599
794
  }
600
795
  else {
601
- for (const item of plannedOrApplied) {
796
+ for (const item of outcome.digest.samples) {
602
797
  const actionPart = item.action !== null ? ` action=${item.action}` : "";
603
798
  const rulePart = item.ruleKind !== null ? ` rule=${item.ruleKind}` : "";
799
+ const statePart = item.state !== null ? ` state=${item.state}` : "";
604
800
  const addPart = sanitizeReportFragment(item.add.join(", +"));
605
- lines.push(`- ${item.repo}#${item.issue_number}:${actionPart}${rulePart} +${addPart}`);
801
+ lines.push(`- ${item.repo}#${item.issue_number}:${statePart}${actionPart}${rulePart} +${addPart}`);
802
+ }
803
+ if (outcome.digest.sample_truncated) {
804
+ const remaining = writeTotal - outcome.digest.samples.length;
805
+ lines.push(`… and ${remaining} more (use --json for full items list)`);
606
806
  }
607
807
  }
808
+ if (outcome.skipped_closed > 0) {
809
+ lines.push("");
810
+ lines.push(`Skipped closed (open-only default): ${outcome.skipped_closed} — re-run with --include-closed to include archive`);
811
+ }
812
+ if (outcome.skipped_author > 0) {
813
+ lines.push("");
814
+ lines.push(`Skipped (author filter ${outcome.filters.author ?? "?"}): ${outcome.skipped_author}`);
815
+ }
608
816
  const already = outcome.items.filter((i) => i.status === "skipped_already_triaged");
609
817
  if (already.length > 0) {
610
818
  lines.push("");
@@ -617,19 +825,19 @@ export function renderLabelMirrorReport(outcome) {
617
825
  const errs = outcome.items.filter((i) => i.status === "error");
618
826
  if (errs.length > 0) {
619
827
  lines.push("");
620
- lines.push("Errors:");
828
+ lines.push(`Errors (partial failure report; ${errs.length} of ${outcome.scanned}):`);
621
829
  for (const item of errs) {
622
830
  const msg = sanitizeReportFragment(item.message ?? "unknown error");
623
831
  lines.push(`- ${item.repo}#${item.issue_number}: ${msg}`);
624
832
  }
625
833
  }
626
- if (outcome.dry_run && plannedOrApplied.length > 0) {
834
+ if (outcome.dry_run && writeTotal > 0) {
627
835
  lines.push("");
628
- lines.push("Dry-run -- re-run with --mirror --apply to write these labels via SCM.");
836
+ lines.push("Dry-run re-run with --mirror --apply to write these labels via SCM (batched; never triage:accept).");
629
837
  }
630
838
  return `${lines.join("\n")}\n`;
631
839
  }
632
- /** JSON-serializable outcome (stable key order not required). */
840
+ /** JSON-serializable outcome including Wave 2 digest aggregates. */
633
841
  export function labelMirrorOutcomeToJson(outcome) {
634
842
  return {
635
843
  project_root: outcome.project_root,
@@ -641,25 +849,47 @@ export function labelMirrorOutcomeToJson(outcome) {
641
849
  skipped_already_triaged: outcome.skipped_already_triaged,
642
850
  skipped_no_match: outcome.skipped_no_match,
643
851
  skipped_unreadable: outcome.skipped_unreadable,
852
+ skipped_closed: outcome.skipped_closed,
853
+ skipped_author: outcome.skipped_author,
644
854
  errors: outcome.errors,
855
+ filters: {
856
+ include_closed: outcome.filters.include_closed,
857
+ repo: outcome.filters.repo,
858
+ author: outcome.filters.author,
859
+ author_logins: outcome.filters.author_logins,
860
+ },
861
+ digest: {
862
+ by_state: { ...outcome.digest.by_state },
863
+ by_rule: { ...outcome.digest.by_rule },
864
+ by_action: { ...outcome.digest.by_action },
865
+ sample_limit: outcome.digest.sample_limit,
866
+ sample_truncated: outcome.digest.sample_truncated,
867
+ samples: outcome.digest.samples.map((i) => itemToJson(i)),
868
+ },
869
+ ...(outcome.batch_size !== undefined ? { batch_size: outcome.batch_size } : {}),
870
+ ...(outcome.delay_ms !== undefined ? { delay_ms: outcome.delay_ms } : {}),
645
871
  policy: {
646
872
  enabled: outcome.policy.enabled,
647
873
  idempotencyLabel: outcome.policy.idempotencyLabel,
648
874
  alwaysLabels: [...outcome.policy.alwaysLabels],
649
875
  actionLabels: Object.fromEntries(Object.entries(outcome.policy.actionLabels).map(([k, v]) => [k, [...(v ?? [])]])),
650
876
  },
651
- items: outcome.items.map((i) => ({
652
- repo: i.repo,
653
- issue_number: i.issue_number,
654
- action: i.action,
655
- reason: i.reason,
656
- ruleKind: i.ruleKind,
657
- current: [...i.current],
658
- desired: [...i.desired],
659
- add: [...i.add],
660
- status: i.status,
661
- ...(i.message !== undefined ? { message: i.message } : {}),
662
- })),
877
+ items: outcome.items.map((i) => itemToJson(i)),
878
+ };
879
+ }
880
+ function itemToJson(i) {
881
+ return {
882
+ repo: i.repo,
883
+ issue_number: i.issue_number,
884
+ state: i.state,
885
+ action: i.action,
886
+ reason: i.reason,
887
+ ruleKind: i.ruleKind,
888
+ current: [...i.current],
889
+ desired: [...i.desired],
890
+ add: [...i.add],
891
+ status: i.status,
892
+ ...(i.message !== undefined ? { message: i.message } : {}),
663
893
  };
664
894
  }
665
895
  /** Validate triageLabelMirror on a plan object (vbrief_validate hook). */
@@ -114,12 +114,12 @@ export declare const registryData: {
114
114
  readonly "task triage:queue": {
115
115
  readonly name: "task triage:queue";
116
116
  readonly summary: "Ranked candidate list";
117
- readonly refs: "(D11 / #1128)";
118
- readonly description: "Print the ranked triage queue from the local cache. Groups (display order): [ORPHAN] -> [RESUME] -> [URGENT] -> untriaged -> other -> [BLOCKED]. Within-group default = updated_at desc; consumer plan.policy.triageRankingLabels[] re-orders within-group by matched-label declared order. Items whose linked vBRIEF is blocked (status:blocked / unresolved depends_on) are demoted into [BLOCKED] unless --include-blocked is passed (#1286).";
119
- readonly usage: "task triage:queue [-- --limit=N] [--include-blocked] [--repo=owner/name]";
120
- readonly flags: readonly [readonly ["--limit N", "10", "Max rows to print."], readonly ["--include-blocked", "(off)", "Re-surface blocked items into their natural group (#1286)."], readonly ["--repo owner/name", "(git remote)", "Explicit repo override."]];
121
- readonly examples: readonly ["task triage:queue", "task triage:queue -- --limit=20 --state=accept", "task triage:queue -- --format=json | jq '.[] | select(.score > 5)'"];
122
- readonly see_also: readonly ["task triage:show", "task triage:audit", "#1119 / D11"];
117
+ readonly refs: "(D11 / #1128, #3129 / #1318 Layer 1)";
118
+ readonly description: "Print the ranked triage queue from the local cache. Groups (display order): [ORPHAN] -> [RESUME] -> [URGENT] -> untriaged -> other -> [BLOCKED]. Within-group default = updated_at desc; consumer plan.policy.triageRankingLabels[] re-orders within-group by matched-label declared order. Items whose linked vBRIEF is blocked (status:blocked / unresolved depends_on) are demoted into [BLOCKED] unless --include-blocked is passed (#1286). Optional --author LOGIN filters to cache author.login (exact; @me resolves to authenticated login; comma allow-list; missing author counted as unknown and disclosed in the header) (#3129 / #1318 Layer 1).";
119
+ readonly usage: "task triage:queue [-- --limit=N] [--include-blocked] [--repo=owner/name] [--author LOGIN|@me] [--author-mine]";
120
+ readonly flags: readonly [readonly ["--limit N", "10", "Max rows to print."], readonly ["--include-blocked", "(off)", "Re-surface blocked items into their natural group (#1286)."], readonly ["--repo owner/name", "(git remote)", "Explicit repo override."], readonly ["--author LOGIN|@me", "(none)", "Only issues whose cache author.login matches (exact; @me = gh user; comma allow-list) (#3129)."], readonly ["--author-mine", "(off)", "Alias for --author @me (#1318 / #3129)."]];
121
+ readonly examples: readonly ["task triage:queue", "task triage:queue -- --limit=20", "task triage:queue -- --author @me", "task triage:queue -- --author alice,bob --limit=20"];
122
+ readonly see_also: readonly ["task triage:show", "task triage:audit", "task triage:classify", "#1119 / D11", "#3129"];
123
123
  readonly placeholder: false;
124
124
  };
125
125
  readonly "task triage:audit": {
@@ -168,13 +168,13 @@ export declare const registryData: {
168
168
  };
169
169
  readonly "task triage:classify": {
170
170
  readonly name: "task triage:classify";
171
- readonly summary: "Inspect / validate auto-classification surface; Tier-1 SCM label mirror";
172
- readonly refs: "(D10 / #1129, #1423 Wave 1)";
173
- readonly description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror runs the Tier-1 deterministic SCM label mirror over the github-issue cache (dry-run default; --apply writes via SCM boundary). Never calls triage:accept / never writes proposed/ xBRIEFs.";
174
- readonly usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--repo owner/name] [--json] [--allow-cross-repo]]";
175
- readonly flags: readonly [readonly ["--list", "(default)", "Print effective rules + hold markers."], readonly ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."], readonly ["--mirror", "(off)", "Tier-1 label mirror: classify cache → planned/applied labels (dry-run default)."], readonly ["--apply", "(off)", "With --mirror: write labels via SCM (requires github SCM boundary)."], readonly ["--repo owner/name", "(all cached)", "Limit mirror to one repo."], readonly ["--json", "(off)", "With --mirror: structured JSON outcome."], readonly ["--allow-cross-repo", "(off)", "With --mirror --apply: allow non-project repos (#2601)."]];
176
- readonly examples: readonly ["task triage:classify -- --list", "task triage:classify -- --validate", "task triage:classify -- --mirror", "task triage:classify -- --mirror --apply --repo owner/name"];
177
- readonly see_also: readonly ["task triage:bootstrap", "task triage:queue", "task vbrief:reconcile:labels", "#1119 / D10", "#1423"];
171
+ readonly summary: "Inspect / validate auto-classification; bootstrap mass-triage label mirror";
172
+ readonly refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129)";
173
+ readonly description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror is the bootstrap mass-triage entrypoint (#3125): classifies the github-issue cache and mirrors outcomes as SCM labels. Open-only by default (opt-in --include-closed); optional --author LOGIN scopes plan/apply to matching author.login (AND with open-only; #3129); dry-run digest shows totals + by state/rule/action + samples and surfaces the author filter; --apply batches writes with rate-limit delay. Never calls triage:accept / never writes proposed/ xBRIEFs.";
174
+ readonly usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json] [--allow-cross-repo]]";
175
+ readonly flags: readonly [readonly ["--list", "(default)", "Print effective rules + hold markers."], readonly ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."], readonly ["--mirror", "(off)", "Bootstrap mass-triage label mirror: classify cache → planned/applied labels (dry-run default, open-only)."], readonly ["--apply", "(off)", "With --mirror: write labels via SCM (batched; requires github SCM boundary)."], readonly ["--include-closed", "(off)", "With --mirror: include closed issues (default open-only avoids archive mass-stamp)."], readonly ["--author LOGIN|@me", "(none)", "With --mirror: only plan/apply issues whose cache author.login matches (exact; @me; comma allow-list) (#3129)."], readonly ["--author-mine", "(off)", "With --mirror: alias for --author @me (#3129)."], readonly ["--repo owner/name", "(all cached)", "Limit mirror to one repo."], readonly ["--batch-size N", "10", "With --mirror --apply: writes per batch before delay."], readonly ["--delay-ms N", "1000", "With --mirror --apply: ms sleep between batches (rate-limit)."], readonly ["--sample-limit N", "15", "With --mirror: max samples in human digest."], readonly ["--json", "(off)", "With --mirror: structured JSON outcome including digest aggregates."], readonly ["--allow-cross-repo", "(off)", "With --mirror --apply: allow non-project repos (#2601)."]];
176
+ readonly examples: readonly ["task triage:classify -- --list", "task triage:classify -- --validate", "task triage:classify -- --mirror", "task triage:classify -- --mirror --author @me --repo owner/name", "task triage:classify -- --mirror --repo owner/name --json", "task triage:classify -- --mirror --apply --repo owner/name --batch-size 10 --delay-ms 1000", "task triage:classify -- --mirror --include-closed --repo owner/name"];
177
+ readonly see_also: readonly ["task triage:bootstrap", "task triage:queue", "task vbrief:reconcile:labels", "#1119 / D10", "#1423", "#3125", "#3129"];
178
178
  readonly placeholder: false;
179
179
  };
180
180
  readonly "task triage:bootstrap": {
@@ -298,37 +298,48 @@ export declare const registryData: {
298
298
  readonly see_also: readonly ["task triage:subscribe", "task triage:scope", "#1119 / D14"];
299
299
  readonly placeholder: false;
300
300
  };
301
- readonly "task triage:audit:prune": {
302
- readonly name: "task triage:audit:prune";
303
- readonly summary: "Operator-invoked archive of closed-terminal entries";
304
- readonly refs: "(D19, coming)";
305
- readonly description: "Move closed-terminal audit entries (accept/reject/mark-duplicate after the upstream issue is closed) into the archive sidecar so the live cache stays focused on open work.";
306
- readonly usage: "task triage:audit:prune [-- --dry-run] [--older-than-days N]";
307
- readonly flags: readonly [readonly ["--dry-run", "(off)", "Preview eligible entries without writing."], readonly ["--older-than-days N", "30", "Age threshold."]];
308
- readonly examples: readonly ["task triage:audit:prune -- --dry-run"];
309
- readonly see_also: readonly ["task triage:archive-list", "task triage:restore-from-archive", "#1119 / D19"];
310
- readonly placeholder: true;
301
+ readonly "task triage:cache-archive": {
302
+ readonly name: "task triage:cache-archive";
303
+ readonly summary: "Reversible archive of closed github-issue cache entries";
304
+ readonly refs: "(#1137)";
305
+ readonly description: "Move closed-and-aged live `.deft-cache/github-issue/` entries under `.deft-cache/archived/github-issue/...` with archive-meta.json. Operator-only; never auto on check/session/sync. Distinct from task cache:prune (TTL hard-delete). Skips issues referenced in xbrief/{proposed,pending,active}. Alias: cache:archive-closed.";
306
+ readonly usage: "task triage:cache-archive -- [--dry-run] [--older-than-days 30] [--repo owner/name] [--json] [--terminal-decision-only]";
307
+ readonly flags: readonly [readonly ["--dry-run", "(off)", "Report eligible entries without moving."], readonly ["--older-than-days N", "30", "Minimum age of closed_at (or fetched_at/mtime fallback)."], readonly ["--repo owner/name", "(all)", "Limit to one repo."], readonly ["--json", "(off)", "Structured summary."], readonly ["--terminal-decision-only", "(off)", "Only archive when latest candidates decision is reject or mark-duplicate."]];
308
+ readonly examples: readonly ["task triage:cache-archive -- --dry-run", "task triage:cache-archive -- --older-than-days 30 --repo deftai/directive"];
309
+ readonly see_also: readonly ["task triage:archive-list", "task triage:restore-from-archive", "task cache:prune", "#1137"];
310
+ readonly placeholder: false;
311
311
  };
312
312
  readonly "task triage:archive-list": {
313
313
  readonly name: "task triage:archive-list";
314
- readonly summary: "List archived entries";
315
- readonly refs: "(D19, coming)";
316
- readonly description: "Read-only listing of archived audit entries from the archive sidecar. Useful before restoring an entry.";
317
- readonly usage: "task triage:archive-list [-- --since=WINDOW]";
318
- readonly flags: readonly [readonly ["--since WINDOW", "(all)", "Time window (e.g. '30d', '6mo')."]];
319
- readonly examples: readonly ["task triage:archive-list"];
320
- readonly see_also: readonly ["task triage:audit:prune", "#1119 / D19"];
321
- readonly placeholder: true;
314
+ readonly summary: "List archived github-issue cache entries";
315
+ readonly refs: "(#1137)";
316
+ readonly description: "List `.deft-cache/archived/github-issue/...` entries newest archived_at first. Alias: cache:archive-list.";
317
+ readonly usage: "task triage:archive-list -- [--repo owner/name] [--format=json] [--since ISO] [--limit N]";
318
+ readonly flags: readonly [readonly ["--repo owner/name", "(all)", "Limit to one repo."], readonly ["--format=json", "text", "JSON listing."], readonly ["--since ISO", "(none)", "Only entries archived at/after this timestamp."], readonly ["--limit N", "(none)", "Cap result count."]];
319
+ readonly examples: readonly ["task triage:archive-list -- --format=json"];
320
+ readonly see_also: readonly ["task triage:cache-archive", "task triage:restore-from-archive", "#1137"];
321
+ readonly placeholder: false;
322
322
  };
323
323
  readonly "task triage:restore-from-archive": {
324
324
  readonly name: "task triage:restore-from-archive";
325
- readonly summary: "Restore archived entry to live cache";
326
- readonly refs: "(D19, coming)";
327
- readonly description: "Move an archived entry back to the live audit log so it becomes visible to triage:queue / triage:audit again.";
328
- readonly usage: "task triage:restore-from-archive -- <N>";
329
- readonly flags: readonly [readonly ["<N>", "(required)", "Archived entry number to restore."]];
330
- readonly examples: readonly ["task triage:restore-from-archive -- 42"];
331
- readonly see_also: readonly ["task triage:archive-list", "#1119 / D19"];
325
+ readonly summary: "Restore archived issue entry to live cache";
326
+ readonly refs: "(#1137)";
327
+ readonly description: "Move an archived github-issue entry back to the live key path. Idempotent if already live with matching content; refuse live conflict unless --force. Alias: cache:restore-from-archive.";
328
+ readonly usage: "task triage:restore-from-archive -- --issue N --repo owner/name [--force]";
329
+ readonly flags: readonly [readonly ["--issue N", "(required unless --key)", "Issue number."], readonly ["--repo owner/name", "(required with --issue)", "Upstream repo."], readonly ["--key owner/repo/N", "(alt)", "Full cache key."], readonly ["--force", "(off)", "Replace live path when content differs."]];
330
+ readonly examples: readonly ["task triage:restore-from-archive -- --issue 1234 --repo deftai/directive"];
331
+ readonly see_also: readonly ["task triage:cache-archive", "task triage:show", "#1137"];
332
+ readonly placeholder: false;
333
+ };
334
+ readonly "task triage:audit:prune": {
335
+ readonly name: "task triage:audit:prune";
336
+ readonly summary: "Legacy name — use triage:cache-archive";
337
+ readonly refs: "(#1137 supersedes D19 name)";
338
+ readonly description: "Historical D19 placeholder name. Implemented surface is task triage:cache-archive (reversible closed github-issue cache archive). Do not confuse with task cache:prune (TTL hard-delete).";
339
+ readonly usage: "task triage:cache-archive -- [--dry-run] [--older-than-days N]";
340
+ readonly flags: readonly [readonly ["--dry-run", "(off)", "Preview eligible entries without writing."], readonly ["--older-than-days N", "30", "Age threshold."]];
341
+ readonly examples: readonly ["task triage:cache-archive -- --dry-run"];
342
+ readonly see_also: readonly ["task triage:cache-archive", "task triage:archive-list", "#1137"];
332
343
  readonly placeholder: true;
333
344
  };
334
345
  readonly "task triage:audit-log:rotate": {