@deftai/directive-core 0.97.0 → 0.98.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/authz/classify.js +291 -0
  2. package/dist/check/cached-orchestrator.d.ts +5 -0
  3. package/dist/check/cached-orchestrator.js +18 -1
  4. package/dist/check/gate-lists.d.ts +20 -0
  5. package/dist/check/gate-lists.js +46 -9
  6. package/dist/check/index.d.ts +1 -1
  7. package/dist/check/index.js +1 -1
  8. package/dist/check/orchestrator.d.ts +4 -0
  9. package/dist/check/orchestrator.js +4 -0
  10. package/dist/doctor/checks.d.ts +7 -0
  11. package/dist/doctor/checks.js +83 -0
  12. package/dist/hooks/dispatcher.d.ts +5 -0
  13. package/dist/hooks/dispatcher.js +54 -4
  14. package/dist/init-deposit/hygiene.d.ts +70 -1
  15. package/dist/init-deposit/hygiene.js +582 -8
  16. package/dist/init-deposit/scaffold.js +277 -4
  17. package/dist/init-deposit/skill-discovery-deposit.js +15 -0
  18. package/dist/policy/check-resume.d.ts +72 -0
  19. package/dist/policy/check-resume.js +253 -0
  20. package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
  21. package/dist/policy/coverage-check-resume-presets.js +228 -0
  22. package/dist/policy/coverage-debt.d.ts +76 -0
  23. package/dist/policy/coverage-debt.js +262 -0
  24. package/dist/policy/index.d.ts +3 -0
  25. package/dist/policy/index.js +50 -21
  26. package/dist/release/auto-hatch.d.ts +114 -0
  27. package/dist/release/auto-hatch.js +301 -0
  28. package/dist/release/coverage-debt-ledger.d.ts +22 -0
  29. package/dist/release/coverage-debt-ledger.js +157 -0
  30. package/dist/release/index.d.ts +3 -0
  31. package/dist/release/index.js +3 -0
  32. package/dist/release/pipeline.js +164 -12
  33. package/dist/release/suite-stamp.d.ts +44 -0
  34. package/dist/release/suite-stamp.js +133 -0
  35. package/dist/release/types.d.ts +19 -0
  36. package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
  37. package/dist/session/coverage-check-resume-nudge.js +66 -0
  38. package/dist/session/index.d.ts +1 -0
  39. package/dist/session/index.js +1 -0
  40. package/dist/session/session-start.js +21 -0
  41. package/dist/triage/classify/label-mirror.d.ts +31 -1
  42. package/dist/triage/classify/label-mirror.js +78 -6
  43. package/dist/triage/help/registry-data.d.ts +6 -6
  44. package/dist/triage/help/registry-data.js +12 -3
  45. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  46. package/dist/vbrief-validate/plan-hooks.js +54 -0
  47. package/package.json +3 -3
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Session-start skippable nudge for coverageDebt + checkResume (#3189).
3
+ *
4
+ * Why / what + Strict / Hatch-aware / Later (+ Discuss / Back for #1470).
5
+ * Later does not mark decided; re-nag next interactive mutation ritual.
6
+ * Headless / CI / non-TTY fail-open (no block).
7
+ */
8
+ import { isCoverageCheckResumeUndecided } from "../policy/coverage-check-resume-presets.js";
9
+ import { policyColonInvocation } from "../policy/policy-invocation.js";
10
+ import { isHeadlessSession } from "../product-signal/headless.js";
11
+ /** Why block -- must appear in the nudge. */
12
+ export const COVERAGE_CHECK_RESUME_NUDGE_WHY = "Why: Long checks often fail late on small gates, or barely miss coverage. " +
13
+ "The project can fail closed, warn, or hatch with a tracked debt issue on THIS repo. " +
14
+ "Local machines may resume a suite that already passed at the same commit; " +
15
+ "CI must not trust a laptop stamp.";
16
+ /** What block -- bundled presets, not five micro-toggles. */
17
+ export const COVERAGE_CHECK_RESUME_NUDGE_WHAT = "What we need: one bundled project decision (not USER.md personal prefs; not npm publish; " +
18
+ "not turning off required CI):\n" +
19
+ " * Strict (recommended for most apps) -- coverageDebt.mode=off, checkResume.localStamp=off\n" +
20
+ " * Hatch-aware -- coverageDebt.mode=hatch (autoFile=false by default), localStamp=on for DX\n" +
21
+ " * Later -- skip this session; does NOT set status=decided; nag again next ritual\n" +
22
+ " * Discuss -- talk through the trade-offs\n" +
23
+ " * Back -- leave this prompt without choosing\n" +
24
+ "Apply path (writes PROJECT-DEFINITION):\n" +
25
+ " Strict -> `" +
26
+ policyColonInvocation("coverage-check-resume-preset", " -- --preset strict") +
27
+ "`\n" +
28
+ " Hatch-aware -> `" +
29
+ policyColonInvocation("coverage-check-resume-preset", " -- --preset hatch-aware") +
30
+ "`\n" +
31
+ " Later -> `" +
32
+ policyColonInvocation("coverage-check-resume-later") +
33
+ "` (no PD write)\n" +
34
+ " Dismiss-with-reason -> `" +
35
+ policyColonInvocation("coverage-check-resume-dismiss", ' -- --reason "…"') +
36
+ "`\n" +
37
+ "Stop nag only after Strict / Hatch-aware (status=decided) or dismiss-with-reason " +
38
+ `(visible via \`${policyColonInvocation("show", " --field=coverageDebt")}\` / doctor).`;
39
+ export const COVERAGE_CHECK_RESUME_NUDGE_BODY = `${COVERAGE_CHECK_RESUME_NUDGE_WHY}\n\n${COVERAGE_CHECK_RESUME_NUDGE_WHAT}`;
40
+ /**
41
+ * True when an interactive mutation session-start ritual should surface the
42
+ * coverage/check-resume decision nudge (#3189).
43
+ */
44
+ export function isCoverageCheckResumeNudgeEligible(options) {
45
+ if (isHeadlessSession(options)) {
46
+ return false;
47
+ }
48
+ return isCoverageCheckResumeUndecided(options.projectRoot);
49
+ }
50
+ /** Format the full operator-facing nudge. */
51
+ export function formatCoverageCheckResumeNudge() {
52
+ return `[deft policy] coverageDebt + checkResume undecided:\n${COVERAGE_CHECK_RESUME_NUDGE_BODY}\n`;
53
+ }
54
+ /**
55
+ * Emit the nudge when eligible; headless / decided callers get an empty string.
56
+ * Never blocks -- session-start always continues.
57
+ *
58
+ * Design note (#3189): session-start is non-interactive for agent hosts (same
59
+ * class as product-signal D17). Choice dispatch is via CLI verbs named in the
60
+ * nudge body (`policy:coverage-check-resume-preset|later|dismiss`), not a
61
+ * blocking TTY menu inside session:start.
62
+ */
63
+ export function maybeFormatCoverageCheckResumeNudge(options) {
64
+ return isCoverageCheckResumeNudgeEligible(options) ? formatCoverageCheckResumeNudge() : "";
65
+ }
66
+ //# sourceMappingURL=coverage-check-resume-nudge.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./compact-ritual.js";
2
+ export * from "./coverage-check-resume-nudge.js";
2
3
  export * from "./git.js";
3
4
  export * from "./json.js";
4
5
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./compact-ritual.js";
2
+ export * from "./coverage-check-resume-nudge.js";
2
3
  export * from "./git.js";
3
4
  export * from "./json.js";
4
5
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -18,6 +18,7 @@ import { runDefaultMode } from "../triage/welcome/default-mode.js";
18
18
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
19
19
  import { emitSessionValueReadback } from "../value/readback.js";
20
20
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
21
+ import { maybeFormatCoverageCheckResumeNudge } from "./coverage-check-resume-nudge.js";
21
22
  import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
22
23
  import { emitSessionStartProcessCost } from "./process-cost.js";
23
24
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
@@ -433,6 +434,16 @@ function runSessionRearm(projectRoot, options, instant, environment) {
433
434
  if (humanMergeLine !== null) {
434
435
  lines.push(humanMergeLine);
435
436
  }
437
+ // #3189: re-arm still surfaces undecided coverage/check-resume once per ritual.
438
+ try {
439
+ const coverageNudge = maybeFormatCoverageCheckResumeNudge({ projectRoot });
440
+ if (coverageNudge.length > 0) {
441
+ lines.push(coverageNudge.trimEnd());
442
+ }
443
+ }
444
+ catch {
445
+ // best-effort — re-arm must not abort
446
+ }
436
447
  const priorQuick = eligibility.state.quickSteps;
437
448
  const priorTriage = priorQuick.triage_welcome ?? ritualStep({ ok: true, ts: instant });
438
449
  const policyOk = policyResult.error === null || policyResult.source === "default-fail-closed";
@@ -863,6 +874,16 @@ export function runSessionStart(projectRoot, options = {}) {
863
874
  if (consentPrompt.length > 0) {
864
875
  lines.push(consentPrompt.trimEnd());
865
876
  }
877
+ // #3189: skippable coverageDebt/checkResume project-decision nudge (fail-open; never blocks).
878
+ try {
879
+ const coverageNudge = maybeFormatCoverageCheckResumeNudge({ projectRoot });
880
+ if (coverageNudge.length > 0) {
881
+ lines.push(coverageNudge.trimEnd());
882
+ }
883
+ }
884
+ catch {
885
+ // best-effort operator advisory — session start must not abort
886
+ }
866
887
  const writeStarted = performance.now();
867
888
  const coldSessionId = (options.newSessionId ?? randomUUID)();
868
889
  const payload = newRitualStatePayload({
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
2
+ * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap + #3197 re-enrich).
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
@@ -9,6 +9,10 @@
9
9
  * + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
10
10
  * `triage:classify -- --mirror` with these filters (not triage:accept).
11
11
  *
12
+ * #3197 re-enrich: default keeps one-shot skip on idempotencyLabel; opt-in
13
+ * `--re-enrich` re-classifies already-stamped issues and plans **additive**
14
+ * label deltas only (v1; no removals / no full reconcile).
15
+ *
12
16
  * Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
13
17
  * engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
14
18
  */
@@ -98,6 +102,12 @@ export interface LabelMirrorItem {
98
102
  readonly add: readonly string[];
99
103
  readonly status: LabelMirrorStatus;
100
104
  readonly message?: string;
105
+ /**
106
+ * True when this row re-planned an issue that already carried the idempotency
107
+ * label under opt-in re-enrich mode (#3197). Distinguishes first-time stamp
108
+ * rows from re-enrich additive backfill in dry-run digests.
109
+ */
110
+ readonly re_enrich?: boolean;
101
111
  }
102
112
  /** Operator digest aggregates for bootstrap mass-triage (#3125 / #1423 Wave 2). */
103
113
  export interface LabelMirrorDigest {
@@ -116,6 +126,8 @@ export interface LabelMirrorFilters {
116
126
  readonly author: string | null;
117
127
  /** Resolved author logins for machine consumers. */
118
128
  readonly author_logins: readonly string[] | null;
129
+ /** Whether this run used opt-in re-enrich mode (#3197). */
130
+ readonly re_enrich: boolean;
119
131
  }
120
132
  export interface LabelMirrorOutcome {
121
133
  readonly project_root: string;
@@ -132,6 +144,16 @@ export interface LabelMirrorOutcome {
132
144
  /** Issues skipped by --author filter (#3129). */
133
145
  readonly skipped_author: number;
134
146
  readonly errors: number;
147
+ /**
148
+ * Planned rows that re-enriched already-stamped issues (subset of planned; #3197).
149
+ * Zero when re_enrich mode is off.
150
+ */
151
+ readonly re_enrich_planned: number;
152
+ /**
153
+ * Applied rows that re-enriched already-stamped issues (subset of applied; #3197).
154
+ * Zero when re_enrich mode is off.
155
+ */
156
+ readonly re_enrich_applied: number;
135
157
  readonly filters: LabelMirrorFilters;
136
158
  readonly digest: LabelMirrorDigest;
137
159
  readonly items: readonly LabelMirrorItem[];
@@ -169,6 +191,14 @@ export interface LabelMirrorOptions {
169
191
  readonly delayMs?: number;
170
192
  /** Injectable sleep for tests (receives ms). Default busy-wait when delayMs > 0. */
171
193
  readonly sleepMs?: LabelMirrorSleepFn;
194
+ /**
195
+ * Opt-in re-enrich mode (#3197): re-classify issues that already carry the
196
+ * idempotency label and plan **additive** label deltas only (no removals).
197
+ * Default false preserves one-shot `skipped_already_triaged` behavior.
198
+ * Still dry-run by default; pair with dryRun:false / CLI `--apply` to write.
199
+ * Never triage:accept / never xBRIEF writes.
200
+ */
201
+ readonly reEnrich?: boolean;
172
202
  /** Required: classify engine (provided by classify/index mirrorLabels wrapper). */
173
203
  readonly engine: LabelMirrorEngine;
174
204
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
2
+ * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap + #3197 re-enrich).
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
@@ -9,6 +9,10 @@
9
9
  * + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
10
10
  * `triage:classify -- --mirror` with these filters (not triage:accept).
11
11
  *
12
+ * #3197 re-enrich: default keeps one-shot skip on idempotencyLabel; opt-in
13
+ * `--re-enrich` re-classifies already-stamped issues and plans **additive**
14
+ * label deltas only (v1; no removals / no full reconcile).
15
+ *
12
16
  * Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
13
17
  * engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
14
18
  */
@@ -399,6 +403,7 @@ export function mirrorLabels(projectRoot, options) {
399
403
  ? options.authorFilter
400
404
  : null;
401
405
  const sampleLimit = options.sampleLimit ?? DEFAULT_DIGEST_SAMPLE_LIMIT;
406
+ const reEnrich = options.reEnrich === true;
402
407
  const batchSize = options.batchSize !== undefined
403
408
  ? Math.max(1, Math.floor(options.batchSize))
404
409
  : DEFAULT_APPLY_BATCH_SIZE;
@@ -416,6 +421,7 @@ export function mirrorLabels(projectRoot, options) {
416
421
  repo: repoFilter,
417
422
  author: authorFilter !== null ? authorFilter.display : null,
418
423
  author_logins: authorFilter !== null ? authorFilter.allowLogins : null,
424
+ re_enrich: reEnrich,
419
425
  };
420
426
  const items = [];
421
427
  const outcomeBase = {
@@ -441,6 +447,8 @@ export function mirrorLabels(projectRoot, options) {
441
447
  skipped_unreadable: 0,
442
448
  skipped_closed: 0,
443
449
  skipped_author: 0,
450
+ re_enrich_planned: 0,
451
+ re_enrich_applied: 0,
444
452
  errors: 0,
445
453
  digest: emptyDigest,
446
454
  items: [
@@ -481,6 +489,8 @@ export function mirrorLabels(projectRoot, options) {
481
489
  let skippedUnreadable = 0;
482
490
  let skippedClosed = 0;
483
491
  let skippedAuthor = 0;
492
+ let reEnrichPlanned = 0;
493
+ let reEnrichApplied = 0;
484
494
  let errors = 0;
485
495
  let applyWritesSinceSleep = 0;
486
496
  for (const [repo, issueNumber] of pairs) {
@@ -573,7 +583,10 @@ export function mirrorLabels(projectRoot, options) {
573
583
  else {
574
584
  current = issueLabelNames(issue);
575
585
  }
576
- if (current.includes(policy.idempotencyLabel)) {
586
+ // Default one-shot: already stamped → skip. Opt-in --re-enrich continues past
587
+ // the stamp and plans additive deltas only (#3197; never removals in v1).
588
+ const alreadyStamped = policy.idempotencyLabel.length > 0 && current.includes(policy.idempotencyLabel);
589
+ if (alreadyStamped && !reEnrich) {
577
590
  skippedAlready += 1;
578
591
  items.push({
579
592
  repo,
@@ -590,6 +603,7 @@ export function mirrorLabels(projectRoot, options) {
590
603
  });
591
604
  continue;
592
605
  }
606
+ const isReEnrichRow = alreadyStamped && reEnrich;
593
607
  // Pass number set only when this exact repo+number is referenced from xBRIEF.
594
608
  const key = `${repo.toLowerCase()}\0${issueNumber}`;
595
609
  const vbriefReferenced = referencedKeys.has(key)
@@ -603,6 +617,26 @@ export function mirrorLabels(projectRoot, options) {
603
617
  now,
604
618
  });
605
619
  if (classification === null) {
620
+ // Re-enrich of a stamped issue with no current rule match: leave labels as-is
621
+ // (additive-only v1 never strips triaged / action chips). Count as unchanged.
622
+ if (isReEnrichRow) {
623
+ unchanged += 1;
624
+ items.push({
625
+ repo,
626
+ issue_number: issueNumber,
627
+ state,
628
+ action: null,
629
+ reason: null,
630
+ ruleKind: null,
631
+ current: [...current].sort(),
632
+ desired: [...current].sort(),
633
+ add: [],
634
+ status: "unchanged",
635
+ re_enrich: true,
636
+ message: `re-enrich: no classify match; left existing labels (incl. ${policy.idempotencyLabel})`,
637
+ });
638
+ continue;
639
+ }
606
640
  skippedNoMatch += 1;
607
641
  items.push({
608
642
  repo,
@@ -620,7 +654,9 @@ export function mirrorLabels(projectRoot, options) {
620
654
  }
621
655
  const desired = desiredLabelsForClassification(classification.action, policy);
622
656
  const currentSet = new Set(current);
657
+ // Additive-only: never plan removals even under re-enrich (#3197 v1).
623
658
  const add = desired.filter((label) => !currentSet.has(label)).sort();
659
+ const reEnrichFlag = isReEnrichRow ? { re_enrich: true } : {};
624
660
  if (add.length === 0) {
625
661
  unchanged += 1;
626
662
  items.push({
@@ -634,11 +670,15 @@ export function mirrorLabels(projectRoot, options) {
634
670
  desired,
635
671
  add: [],
636
672
  status: "unchanged",
673
+ ...reEnrichFlag,
637
674
  });
638
675
  continue;
639
676
  }
640
677
  if (dryRun) {
641
678
  planned += 1;
679
+ if (isReEnrichRow) {
680
+ reEnrichPlanned += 1;
681
+ }
642
682
  items.push({
643
683
  repo,
644
684
  issue_number: issueNumber,
@@ -650,6 +690,7 @@ export function mirrorLabels(projectRoot, options) {
650
690
  desired,
651
691
  add,
652
692
  status: "planned",
693
+ ...reEnrichFlag,
653
694
  });
654
695
  continue;
655
696
  }
@@ -673,6 +714,7 @@ export function mirrorLabels(projectRoot, options) {
673
714
  add,
674
715
  status: "error",
675
716
  message: mutateGate.reason ?? `refusing cross-repo mutation on ${repo}`,
717
+ ...reEnrichFlag,
676
718
  });
677
719
  continue;
678
720
  }
@@ -690,6 +732,7 @@ export function mirrorLabels(projectRoot, options) {
690
732
  add,
691
733
  status: "error",
692
734
  message: "no LabelClient available for apply",
735
+ ...reEnrichFlag,
693
736
  });
694
737
  continue;
695
738
  }
@@ -700,8 +743,12 @@ export function mirrorLabels(projectRoot, options) {
700
743
  sleepMs(delayMs);
701
744
  }
702
745
  applyWritesSinceSleep += 1;
746
+ // Additive-only: remove list is always empty (never strip labels; #3197).
703
747
  client.apply(repo, issueNumber, add, []);
704
748
  applied += 1;
749
+ if (isReEnrichRow) {
750
+ reEnrichApplied += 1;
751
+ }
705
752
  items.push({
706
753
  repo,
707
754
  issue_number: issueNumber,
@@ -713,6 +760,7 @@ export function mirrorLabels(projectRoot, options) {
713
760
  desired,
714
761
  add,
715
762
  status: "applied",
763
+ ...reEnrichFlag,
716
764
  });
717
765
  }
718
766
  catch (exc) {
@@ -730,6 +778,7 @@ export function mirrorLabels(projectRoot, options) {
730
778
  add,
731
779
  status: "error",
732
780
  message: formatMissingLabelHint(rawMsg, add),
781
+ ...reEnrichFlag,
733
782
  });
734
783
  // Partial failure: continue remaining issues (idempotent re-run skips applied).
735
784
  }
@@ -746,6 +795,8 @@ export function mirrorLabels(projectRoot, options) {
746
795
  skipped_unreadable: skippedUnreadable,
747
796
  skipped_closed: skippedClosed,
748
797
  skipped_author: skippedAuthor,
798
+ re_enrich_planned: reEnrichPlanned,
799
+ re_enrich_applied: reEnrichApplied,
749
800
  errors,
750
801
  digest,
751
802
  items,
@@ -763,16 +814,27 @@ function formatCountMap(map) {
763
814
  export function renderLabelMirrorReport(outcome) {
764
815
  const lines = [];
765
816
  const mode = outcome.dry_run ? "dry-run" : "apply";
766
- lines.push(`triage:classify --mirror (${mode}) bootstrap mass-triage (#1423 Wave 2)`);
817
+ const reEnrichOn = outcome.filters.re_enrich === true;
818
+ lines.push(reEnrichOn
819
+ ? `triage:classify --mirror --re-enrich (${mode}) — additive re-enrich (#3197 / #1423)`
820
+ : `triage:classify --mirror (${mode}) — bootstrap mass-triage (#1423 Wave 2)`);
767
821
  const stateFilter = outcome.filters.include_closed ? "all (include-closed)" : "open-only";
768
822
  const repoPart = outcome.filters.repo ?? "*";
769
823
  const authorPart = outcome.filters.author ?? "*";
770
- lines.push(`filters: state=${stateFilter} repo=${repoPart} author=${authorPart}`);
824
+ const reEnrichPart = reEnrichOn ? "on (additive-only)" : "off";
825
+ lines.push(`filters: state=${stateFilter} repo=${repoPart} author=${authorPart} re_enrich=${reEnrichPart}`);
826
+ const firstTimePlanned = Math.max(0, outcome.planned - outcome.re_enrich_planned);
827
+ const firstTimeApplied = Math.max(0, outcome.applied - outcome.re_enrich_applied);
771
828
  lines.push(`scanned=${outcome.scanned} planned=${outcome.planned} applied=${outcome.applied} ` +
772
829
  `unchanged=${outcome.unchanged} already_triaged=${outcome.skipped_already_triaged} ` +
773
830
  `no_match=${outcome.skipped_no_match} closed_skipped=${outcome.skipped_closed} ` +
774
831
  `author_skipped=${outcome.skipped_author} ` +
775
832
  `unreadable=${outcome.skipped_unreadable} errors=${outcome.errors}`);
833
+ // Distinguish first-time stamp rows vs re-enrich additive backfill (#3197 / #3124 re-run vs re-enrich).
834
+ lines.push(`planned_kind: first_time=${firstTimePlanned} re_enrich=${outcome.re_enrich_planned}` +
835
+ (outcome.dry_run
836
+ ? ""
837
+ : ` | applied_kind: first_time=${firstTimeApplied} re_enrich=${outcome.re_enrich_applied}`));
776
838
  lines.push(`idempotencyLabel=${outcome.policy.idempotencyLabel} alwaysLabels=${JSON.stringify(outcome.policy.alwaysLabels)}`);
777
839
  if (!outcome.dry_run) {
778
840
  lines.push(`apply: batch_size=${outcome.batch_size ?? DEFAULT_APPLY_BATCH_SIZE} delay_ms=${outcome.delay_ms ?? DEFAULT_APPLY_DELAY_MS}`);
@@ -797,8 +859,9 @@ export function renderLabelMirrorReport(outcome) {
797
859
  const actionPart = item.action !== null ? ` action=${item.action}` : "";
798
860
  const rulePart = item.ruleKind !== null ? ` rule=${item.ruleKind}` : "";
799
861
  const statePart = item.state !== null ? ` state=${item.state}` : "";
862
+ const kindPart = item.re_enrich === true ? " kind=re-enrich" : " kind=first-time";
800
863
  const addPart = sanitizeReportFragment(item.add.join(", +"));
801
- lines.push(`- ${item.repo}#${item.issue_number}:${statePart}${actionPart}${rulePart} +${addPart}`);
864
+ lines.push(`- ${item.repo}#${item.issue_number}:${statePart}${actionPart}${rulePart}${kindPart} +${addPart}`);
802
865
  }
803
866
  if (outcome.digest.sample_truncated) {
804
867
  const remaining = writeTotal - outcome.digest.samples.length;
@@ -817,6 +880,9 @@ export function renderLabelMirrorReport(outcome) {
817
880
  if (already.length > 0) {
818
881
  lines.push("");
819
882
  lines.push(`Skipped (already ${outcome.policy.idempotencyLabel}): ${already.length}`);
883
+ if (!reEnrichOn) {
884
+ lines.push(" Tip: after actionLabels / rule changes, use --mirror --re-enrich (dry-run) to plan additive chips on already-stamped issues (#3197; re-run vs re-enrich #3124).");
885
+ }
820
886
  }
821
887
  const noMatch = outcome.items.filter((i) => i.status === "skipped_no_match");
822
888
  if (noMatch.length > 0) {
@@ -833,7 +899,9 @@ export function renderLabelMirrorReport(outcome) {
833
899
  }
834
900
  if (outcome.dry_run && writeTotal > 0) {
835
901
  lines.push("");
836
- lines.push("Dry-run — re-run with --mirror --apply to write these labels via SCM (batched; never triage:accept).");
902
+ lines.push(reEnrichOn
903
+ ? "Dry-run — re-run with --mirror --re-enrich --apply to write additive labels via SCM (batched; never triage:accept; never removals)."
904
+ : "Dry-run — re-run with --mirror --apply to write these labels via SCM (batched; never triage:accept).");
837
905
  }
838
906
  return `${lines.join("\n")}\n`;
839
907
  }
@@ -851,12 +919,15 @@ export function labelMirrorOutcomeToJson(outcome) {
851
919
  skipped_unreadable: outcome.skipped_unreadable,
852
920
  skipped_closed: outcome.skipped_closed,
853
921
  skipped_author: outcome.skipped_author,
922
+ re_enrich_planned: outcome.re_enrich_planned,
923
+ re_enrich_applied: outcome.re_enrich_applied,
854
924
  errors: outcome.errors,
855
925
  filters: {
856
926
  include_closed: outcome.filters.include_closed,
857
927
  repo: outcome.filters.repo,
858
928
  author: outcome.filters.author,
859
929
  author_logins: outcome.filters.author_logins,
930
+ re_enrich: outcome.filters.re_enrich,
860
931
  },
861
932
  digest: {
862
933
  by_state: { ...outcome.digest.by_state },
@@ -890,6 +961,7 @@ function itemToJson(i) {
890
961
  add: [...i.add],
891
962
  status: i.status,
892
963
  ...(i.message !== undefined ? { message: i.message } : {}),
964
+ ...(i.re_enrich === true ? { re_enrich: true } : {}),
893
965
  };
894
966
  }
895
967
  /** Validate triageLabelMirror on a plan object (vbrief_validate hook). */
@@ -169,12 +169,12 @@ export declare const registryData: {
169
169
  readonly "task triage:classify": {
170
170
  readonly name: "task triage:classify";
171
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"];
172
+ readonly refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129, #3197)";
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). Default re-run skips already-stamped (idempotency) issues; after policy/rule changes use --re-enrich to re-classify already-triaged issues and plan additive label deltas only (#3197; re-run vs re-enrich discovery #3124). Dry-run digest shows totals + by state/rule/action + samples, first-time vs re-enrich planned rows, 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] [--re-enrich] [--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 ["--re-enrich", "(off)", "With --mirror: re-classify already-stamped issues; plan additive label deltas only (dry-run default; #3197 / #3124 re-run vs re-enrich)."], 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 --re-enrich --repo owner/name", "task triage:classify -- --mirror --re-enrich --apply --repo owner/name", "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", "#3124", "#3125", "#3129", "#3197"];
178
178
  readonly placeholder: false;
179
179
  };
180
180
  readonly "task triage:bootstrap": {
@@ -278,9 +278,9 @@ export const registryData = {
278
278
  "task triage:classify": {
279
279
  name: "task triage:classify",
280
280
  summary: "Inspect / validate auto-classification; bootstrap mass-triage label mirror",
281
- refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129)",
282
- 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.",
283
- 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]]",
281
+ refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129, #3197)",
282
+ 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). Default re-run skips already-stamped (idempotency) issues; after policy/rule changes use --re-enrich to re-classify already-triaged issues and plan additive label deltas only (#3197; re-run vs re-enrich discovery #3124). Dry-run digest shows totals + by state/rule/action + samples, first-time vs re-enrich planned rows, and surfaces the author filter; --apply batches writes with rate-limit delay. Never calls triage:accept / never writes proposed/ xBRIEFs.",
283
+ usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--re-enrich] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json] [--allow-cross-repo]]",
284
284
  flags: [
285
285
  ["--list", "(default)", "Print effective rules + hold markers."],
286
286
  ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."],
@@ -294,6 +294,11 @@ export const registryData = {
294
294
  "(off)",
295
295
  "With --mirror: write labels via SCM (batched; requires github SCM boundary).",
296
296
  ],
297
+ [
298
+ "--re-enrich",
299
+ "(off)",
300
+ "With --mirror: re-classify already-stamped issues; plan additive label deltas only (dry-run default; #3197 / #3124 re-run vs re-enrich).",
301
+ ],
297
302
  [
298
303
  "--include-closed",
299
304
  "(off)",
@@ -319,6 +324,8 @@ export const registryData = {
319
324
  "task triage:classify -- --mirror --author @me --repo owner/name",
320
325
  "task triage:classify -- --mirror --repo owner/name --json",
321
326
  "task triage:classify -- --mirror --apply --repo owner/name --batch-size 10 --delay-ms 1000",
327
+ "task triage:classify -- --mirror --re-enrich --repo owner/name",
328
+ "task triage:classify -- --mirror --re-enrich --apply --repo owner/name",
322
329
  "task triage:classify -- --mirror --include-closed --repo owner/name",
323
330
  ],
324
331
  see_also: [
@@ -327,8 +334,10 @@ export const registryData = {
327
334
  "task vbrief:reconcile:labels",
328
335
  "#1119 / D10",
329
336
  "#1423",
337
+ "#3124",
330
338
  "#3125",
331
339
  "#3129",
340
+ "#3197",
332
341
  ],
333
342
  placeholder: false,
334
343
  },
@@ -14,6 +14,10 @@ export declare function validateHostSlashCommandsOnPlan(plan: unknown, filepath:
14
14
  export declare function validateOpenClawProductCommandsOnPlan(plan: unknown, filepath: string): string[];
15
15
  /** vbrief_validate hook: validate ``plan.policy.stalenessTickler`` (#2489). */
16
16
  export declare function validateStalenessTicklerOnPlan(plan: unknown, filepath: string): string[];
17
+ /** vbrief_validate hook: validate ``plan.policy.coverageDebt`` (#3189). */
18
+ export declare function validateCoverageDebtOnPlan(plan: unknown, filepath: string): string[];
19
+ /** vbrief_validate hook: validate ``plan.policy.checkResume`` (#3189). */
20
+ export declare function validateCheckResumeOnPlan(plan: unknown, filepath: string): string[];
17
21
  /** Run all PROJECT-DEFINITION policy hooks (mirrors lazy-import block in Python). */
18
22
  export declare function runProjectDefinitionHooks(plan: unknown, filepath: string): string[];
19
23
  //# sourceMappingURL=plan-hooks.d.ts.map
@@ -1,3 +1,5 @@
1
+ import { validateCheckResume } from "../policy/check-resume.js";
2
+ import { validateCoverageDebt } from "../policy/coverage-debt.js";
1
3
  import { validateHostHooks } from "../policy/host-hooks.js";
2
4
  import { validateHostSlashCommands } from "../policy/host-slash-commands.js";
3
5
  import { readPlanPolicy } from "../policy/plan-extensions.js";
@@ -196,6 +198,51 @@ export function validateStalenessTicklerOnPlan(plan, filepath) {
196
198
  }
197
199
  return out;
198
200
  }
201
+ /** vbrief_validate hook: validate ``plan.policy.coverageDebt`` (#3189). */
202
+ export function validateCoverageDebtOnPlan(plan, filepath) {
203
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
204
+ return [];
205
+ }
206
+ const policy = readPlanPolicy(plan);
207
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
208
+ return [];
209
+ }
210
+ if (!("coverageDebt" in policy)) {
211
+ return [];
212
+ }
213
+ const raw = policy.coverageDebt;
214
+ // Explicit null is a typed key with invalid value — fail closed at validation time.
215
+ if (raw === null) {
216
+ return [`${filepath}: plan.policy.coverageDebt must be an object; got null (#3189)`];
217
+ }
218
+ const out = [];
219
+ for (const err of validateCoverageDebt(raw)) {
220
+ out.push(`${filepath}: ${err} (#3189)`);
221
+ }
222
+ return out;
223
+ }
224
+ /** vbrief_validate hook: validate ``plan.policy.checkResume`` (#3189). */
225
+ export function validateCheckResumeOnPlan(plan, filepath) {
226
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
227
+ return [];
228
+ }
229
+ const policy = readPlanPolicy(plan);
230
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
231
+ return [];
232
+ }
233
+ if (!("checkResume" in policy)) {
234
+ return [];
235
+ }
236
+ const raw = policy.checkResume;
237
+ if (raw === null) {
238
+ return [`${filepath}: plan.policy.checkResume must be an object; got null (#3189)`];
239
+ }
240
+ const out = [];
241
+ for (const err of validateCheckResume(raw)) {
242
+ out.push(`${filepath}: ${err} (#3189)`);
243
+ }
244
+ return out;
245
+ }
199
246
  /** Run all PROJECT-DEFINITION policy hooks (mirrors lazy-import block in Python). */
200
247
  export function runProjectDefinitionHooks(plan, filepath) {
201
248
  const errors = [];
@@ -266,6 +313,13 @@ export function runProjectDefinitionHooks(plan, filepath) {
266
313
  catch {
267
314
  /* hook must not break validation */
268
315
  }
316
+ try {
317
+ errors.push(...validateCoverageDebtOnPlan(plan, filepath));
318
+ errors.push(...validateCheckResumeOnPlan(plan, filepath));
319
+ }
320
+ catch {
321
+ /* hook must not break validation */
322
+ }
269
323
  return errors;
270
324
  }
271
325
  //# sourceMappingURL=plan-hooks.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.97.0",
3
+ "version": "0.98.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -354,8 +354,8 @@
354
354
  "provenance": true
355
355
  },
356
356
  "dependencies": {
357
- "@deftai/directive-content": "^0.97.0",
358
- "@deftai/directive-types": "^0.97.0",
357
+ "@deftai/directive-content": "^0.98.0",
358
+ "@deftai/directive-types": "^0.98.0",
359
359
  "archiver": "^8.0.0"
360
360
  },
361
361
  "scripts": {