@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
@@ -4,6 +4,8 @@ import { cacheGet } from "../../cache/operations.js";
4
4
  import { ingestSingleForAccept as ingestSingleForAcceptTs } from "../../intake/issue-ingest.js";
5
5
  import { call } from "../../scm/call.js";
6
6
  import { ScmStubError } from "../../scm/errors.js";
7
+ import { findLifecycleArtifactsForIssue, findProposedArtifactsForIssue, promoteFromIssue, } from "../../scope/promote-from-issue.js";
8
+ import { promotePath } from "../../scope/promote-path.js";
7
9
  import { createCandidatesLog, findByIssue, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
8
10
  import { TriageError, UpstreamCloseError } from "./errors.js";
9
11
  import { parseResumeOn } from "./resume-on.js";
@@ -62,7 +64,8 @@ function defaultIssueIngest() {
62
64
  // Delegate to the native TS intake path (#2350). The legacy Python
63
65
  // `scripts/issue_ingest.py` shell-out was orphaned when #1933 removed the
64
66
  // Python surface, leaving `triage:accept` raising ModuleNotFoundError.
65
- ingestSingleForAcceptTs(issueNumber, repo, { projectRoot });
67
+ const [, path] = ingestSingleForAcceptTs(issueNumber, repo, { projectRoot });
68
+ return path;
66
69
  },
67
70
  };
68
71
  }
@@ -207,24 +210,81 @@ function ensureRejectedLabelApplied(deps, issueNumber, repo) {
207
210
  writeErr(`triage_actions: reject #${issueNumber} (${repo}) closed successfully but the '${REJECTED_LABEL}' label is missing and auto-create/re-add failed: ${healExc instanceof Error ? healExc.message : String(healExc)}`);
208
211
  }
209
212
  }
213
+ /**
214
+ * After a successful accept (or idempotent re-accept), promote proposed → pending (#1136).
215
+ * Surfaces WIP-cap refusals with the same messaging as scope:promote.
216
+ */
217
+ function runAutoPromote(issueNumber, repo, projectRoot, decisionId, ingestedPath, force) {
218
+ // Already pending for this issue → idempotent success (repeat accept --auto-promote).
219
+ const pending = findLifecycleArtifactsForIssue(projectRoot, issueNumber, {
220
+ folder: "pending",
221
+ repo,
222
+ });
223
+ if (pending.length >= 1) {
224
+ return;
225
+ }
226
+ let path = typeof ingestedPath === "string" && ingestedPath.length > 0 ? ingestedPath : null;
227
+ if (path === null) {
228
+ const matches = findProposedArtifactsForIssue(projectRoot, issueNumber, repo);
229
+ if (matches.length === 1) {
230
+ path = matches[0] ?? null;
231
+ }
232
+ else if (matches.length > 1) {
233
+ throw new TriageError(`accept #${issueNumber} (${repo}): --auto-promote found multiple proposed artifacts:\n` +
234
+ matches.map((p) => ` - ${p}`).join("\n") +
235
+ `\nPromote one with: task scope:promote -- <path>`);
236
+ }
237
+ }
238
+ if (path === null) {
239
+ // Fall back to full from-issue resolution (may still fail on missing artifact).
240
+ const fromIssue = promoteFromIssue({
241
+ issueNumber,
242
+ repo,
243
+ projectRoot,
244
+ force: force === true,
245
+ });
246
+ if (!fromIssue.ok) {
247
+ throw new TriageError(`accept #${issueNumber} (${repo}): accept recorded but --auto-promote failed: ${fromIssue.message}`);
248
+ }
249
+ return;
250
+ }
251
+ const result = promotePath(path, {
252
+ projectRoot,
253
+ force: force === true,
254
+ fromIssue: issueNumber,
255
+ cacheDecisionId: decisionId,
256
+ cacheStateAtPromote: "accept",
257
+ requireAudit: true,
258
+ });
259
+ if (!result.ok) {
260
+ throw new TriageError(`accept #${issueNumber} (${repo}): accept recorded but --auto-promote failed: ${result.message}`);
261
+ }
262
+ }
210
263
  /** Record an accept audit entry and delegate vBRIEF authoring to issue_ingest. */
211
264
  export function accept(issueNumber, repo, deps, options = {}) {
212
265
  const projectRoot = options.projectRoot ?? process.cwd();
213
266
  const actor = resolveActor(options.actor);
214
267
  const prior = isIdempotentRepeat(deps, issueNumber, repo, "accept", projectRoot);
215
268
  if (prior !== null) {
269
+ if (options.autoPromote === true) {
270
+ runAutoPromote(issueNumber, repo, projectRoot, prior.decision_id, null, options.force);
271
+ }
216
272
  return prior.decision_id;
217
273
  }
218
274
  const entry = buildEntry(deps, "accept", issueNumber, repo, actor);
219
275
  const logPath = logPathFor(projectRoot);
220
276
  const decisionId = deps.candidatesLog.append(entry, { path: logPath });
277
+ let ingestedPath = null;
221
278
  try {
222
- deps.issueIngest.ingestSingleForAccept(issueNumber, repo, { projectRoot });
279
+ ingestedPath = deps.issueIngest.ingestSingleForAccept(issueNumber, repo, { projectRoot });
223
280
  }
224
281
  catch (exc) {
225
282
  rollbackAuditEntry(decisionId, projectRoot, logPath);
226
283
  throw new TriageError(`accept #${issueNumber} (${repo}): issue:ingest delegation failed; audit entry rolled back. Cause: ${exc instanceof Error ? exc.message : String(exc)}`);
227
284
  }
285
+ if (options.autoPromote === true) {
286
+ runAutoPromote(issueNumber, repo, projectRoot, decisionId, typeof ingestedPath === "string" ? ingestedPath : null, options.force);
287
+ }
228
288
  return decisionId;
229
289
  }
230
290
  /** Close upstream, best-effort label, record reject audit entry. */
@@ -21,9 +21,12 @@ export interface CandidatesLog {
21
21
  newDecisionId(): string;
22
22
  }
23
23
  export interface IssueIngest {
24
+ /**
25
+ * Ingest issue into proposed/; return absolute path when known (#1136 auto-promote).
26
+ */
24
27
  ingestSingleForAccept(issueNumber: number, repo: string, options?: {
25
28
  projectRoot?: string;
26
- }): void;
29
+ }): string | null | undefined;
27
30
  }
28
31
  export interface ScmRunner {
29
32
  call(source: string, verb: string, args: readonly string[], options?: {
@@ -44,6 +47,10 @@ export interface TriageActionsDeps {
44
47
  export interface AcceptOptions {
45
48
  actor?: string | null;
46
49
  projectRoot?: string;
50
+ /** After accept+ingest, promote proposed → pending (#1136). */
51
+ autoPromote?: boolean;
52
+ /** WIP-cap override for the auto-promote leg. */
53
+ force?: boolean;
47
54
  }
48
55
  export interface RejectOptions {
49
56
  actor?: string | null;
@@ -0,0 +1,51 @@
1
+ /** Injectable authenticated-login resolver (tests inject; production uses gh). */
2
+ export type ResolveAuthenticatedLogin = () => string | null;
3
+ export interface AuthorFilter {
4
+ /** Raw CLI value before resolution (e.g. `@me` or `alice,bob`). */
5
+ readonly raw: string;
6
+ /** Resolved allow-list logins (exact match; case-sensitive like bulk). */
7
+ readonly allowLogins: readonly string[];
8
+ /** True when any token was `@me` / `--author-mine`. */
9
+ readonly usedMe: boolean;
10
+ /** Header/digest display string. */
11
+ readonly display: string;
12
+ }
13
+ export interface AuthorFilterResolveResult {
14
+ readonly filter?: AuthorFilter;
15
+ readonly error?: string;
16
+ }
17
+ export interface AuthorPartitionResult<T> {
18
+ readonly matched: readonly T[];
19
+ readonly unknownCount: number;
20
+ readonly nonMatchingCount: number;
21
+ }
22
+ /** Split comma allow-list; trim; drop empties. */
23
+ export declare function parseAuthorTokens(raw: string): string[];
24
+ /**
25
+ * Resolve `@me` (and bare tokens) into an AuthorFilter.
26
+ * Returns error when raw is empty/whitespace-only or `@me` cannot be resolved.
27
+ */
28
+ export declare function resolveAuthorFilter(raw: string, resolveMe?: ResolveAuthenticatedLogin | null): AuthorFilterResolveResult;
29
+ /**
30
+ * Default `@me` resolution via live `gh` (not ghx — multi-arg api --jq; #2275 / #954).
31
+ */
32
+ export declare function defaultResolveAuthenticatedLogin(): string | null;
33
+ /** Login from a CachedIssue.author string (empty = unknown). */
34
+ export declare function normalizeAuthorLogin(login: string | null | undefined): string | null;
35
+ /**
36
+ * Login from a raw cache payload (author.login / user.login / string author).
37
+ * Empty / missing → null (unknown).
38
+ */
39
+ export declare function authorLoginFromRawIssue(issue: Record<string, unknown> | null | undefined): string | null;
40
+ /** Exact allow-list match (bulk parity). Unknown/missing never matches. */
41
+ export declare function matchesAuthorFilter(login: string | null | undefined, filter: AuthorFilter): boolean;
42
+ /**
43
+ * Partition items by author filter. Unknown (missing login) counted separately
44
+ * and excluded from matched — callers disclose unknownCount in headers.
45
+ */
46
+ export declare function partitionByAuthorFilter<T>(items: readonly T[], getLogin: (item: T) => string | null | undefined, filter: AuthorFilter): AuthorPartitionResult<T>;
47
+ /** Single-line header fragment for queue / classify digest. */
48
+ export declare function formatAuthorFilterLine(filter: AuthorFilter, options?: {
49
+ readonly unknownCount?: number;
50
+ }): string;
51
+ //# sourceMappingURL=author-filter.d.ts.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared `--author` filter for triage:queue and triage:classify (#3129 / #1318 Layer 1).
3
+ *
4
+ * Matches existing bulk/bootstrap semantics: exact login match on cached author.login
5
+ * (or user.login). `@me` resolves via authenticated `gh api user --jq .login`.
6
+ * Comma allow-lists are accepted. Missing author on cache rows is "unknown" — not a match,
7
+ * and callers should disclose the unknown count rather than silent-drop.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import { extractAuthor } from "./scope-drift/cache-walker.js";
11
+ /** Split comma allow-list; trim; drop empties. */
12
+ export function parseAuthorTokens(raw) {
13
+ return raw
14
+ .split(",")
15
+ .map((t) => t.trim())
16
+ .filter((t) => t.length > 0);
17
+ }
18
+ /**
19
+ * Resolve `@me` (and bare tokens) into an AuthorFilter.
20
+ * Returns error when raw is empty/whitespace-only or `@me` cannot be resolved.
21
+ */
22
+ export function resolveAuthorFilter(raw, resolveMe) {
23
+ const resolveAuthenticated = resolveMe ?? defaultResolveAuthenticatedLogin;
24
+ const tokens = parseAuthorTokens(raw);
25
+ if (tokens.length === 0) {
26
+ return { error: "argument --author: expected a non-empty login (or @me)" };
27
+ }
28
+ let usedMe = false;
29
+ let meLogin;
30
+ const allow = [];
31
+ const displayParts = [];
32
+ for (const token of tokens) {
33
+ if (token === "@me" || token.toLowerCase() === "@me") {
34
+ usedMe = true;
35
+ if (meLogin === undefined) {
36
+ meLogin = resolveAuthenticated();
37
+ }
38
+ if (meLogin === null || meLogin.length === 0) {
39
+ return {
40
+ error: "argument --author: @me could not be resolved (gh api user --jq .login failed; authenticate gh or pass an explicit login)",
41
+ };
42
+ }
43
+ allow.push(meLogin);
44
+ displayParts.push(`@me (resolved -> ${meLogin})`);
45
+ }
46
+ else {
47
+ allow.push(token);
48
+ displayParts.push(token);
49
+ }
50
+ }
51
+ // Dedup while preserving order
52
+ const seen = new Set();
53
+ const allowLogins = [];
54
+ for (const login of allow) {
55
+ if (!seen.has(login)) {
56
+ seen.add(login);
57
+ allowLogins.push(login);
58
+ }
59
+ }
60
+ return {
61
+ filter: {
62
+ raw,
63
+ allowLogins,
64
+ usedMe,
65
+ display: displayParts.join(", "),
66
+ },
67
+ };
68
+ }
69
+ /**
70
+ * Default `@me` resolution via live `gh` (not ghx — multi-arg api --jq; #2275 / #954).
71
+ */
72
+ export function defaultResolveAuthenticatedLogin() {
73
+ try {
74
+ const result = spawnSync("gh", ["api", "user", "--jq", ".login"], {
75
+ encoding: "utf8",
76
+ env: process.env,
77
+ windowsHide: true,
78
+ });
79
+ if (result.status !== 0) {
80
+ return null;
81
+ }
82
+ const text = String(result.stdout ?? "").trim();
83
+ if (text.length === 0) {
84
+ return null;
85
+ }
86
+ // jq may emit a JSON string with quotes; strip surrounding quotes when present
87
+ if ((text.startsWith('"') && text.endsWith('"')) ||
88
+ (text.startsWith("'") && text.endsWith("'"))) {
89
+ return text.slice(1, -1);
90
+ }
91
+ return text;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ /** Login from a CachedIssue.author string (empty = unknown). */
98
+ export function normalizeAuthorLogin(login) {
99
+ if (login === null || login === undefined) {
100
+ return null;
101
+ }
102
+ const trimmed = login.trim();
103
+ return trimmed.length > 0 ? trimmed : null;
104
+ }
105
+ /**
106
+ * Login from a raw cache payload (author.login / user.login / string author).
107
+ * Empty / missing → null (unknown).
108
+ */
109
+ export function authorLoginFromRawIssue(issue) {
110
+ if (issue === null || issue === undefined) {
111
+ return null;
112
+ }
113
+ return normalizeAuthorLogin(extractAuthor(issue));
114
+ }
115
+ /** Exact allow-list match (bulk parity). Unknown/missing never matches. */
116
+ export function matchesAuthorFilter(login, filter) {
117
+ const normalized = normalizeAuthorLogin(login);
118
+ if (normalized === null) {
119
+ return false;
120
+ }
121
+ return filter.allowLogins.includes(normalized);
122
+ }
123
+ /**
124
+ * Partition items by author filter. Unknown (missing login) counted separately
125
+ * and excluded from matched — callers disclose unknownCount in headers.
126
+ */
127
+ export function partitionByAuthorFilter(items, getLogin, filter) {
128
+ const matched = [];
129
+ let unknownCount = 0;
130
+ let nonMatchingCount = 0;
131
+ for (const item of items) {
132
+ const login = normalizeAuthorLogin(getLogin(item));
133
+ if (login === null) {
134
+ unknownCount += 1;
135
+ continue;
136
+ }
137
+ if (filter.allowLogins.includes(login)) {
138
+ matched.push(item);
139
+ }
140
+ else {
141
+ nonMatchingCount += 1;
142
+ }
143
+ }
144
+ return { matched, unknownCount, nonMatchingCount };
145
+ }
146
+ /** Single-line header fragment for queue / classify digest. */
147
+ export function formatAuthorFilterLine(filter, options = {}) {
148
+ const unknown = options.unknownCount ?? 0;
149
+ const unknownPart = unknown > 0 ? `; ${unknown} cached issue(s) missing author (unknown — excluded)` : "";
150
+ return `author filter: ${filter.display}${unknownPart}`;
151
+ }
152
+ //# sourceMappingURL=author-filter.js.map
@@ -8,7 +8,7 @@ export declare function gitignoreTriageCacheEntries(projectRoot: string): readon
8
8
  export declare function gitattributesTriageCacheGlob(projectRoot: string): string;
9
9
  export declare const GITATTRIBUTES_EVAL_RULE = "vbrief/.triage-cache/*.jsonl merge=union";
10
10
  export declare const FORBIDDEN_BLANKET_EVAL_LINES: readonly string[];
11
- export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, and `vbrief/.triage-cache/doctor-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
11
+ export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n| `staleness-tickler-state.json` | No | Per-clone upgrade-tickler throttle state. |\n| `release-availability-state.json` | No | Per-clone release-availability probe throttle state. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, `vbrief/.triage-cache/doctor-state.json`,\n`vbrief/.triage-cache/staleness-tickler-state.json`, and\n`vbrief/.triage-cache/release-availability-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
12
12
  /** Layout-aware triage-cache README body for the active lifecycle tree (#2344 / #2349). */
13
13
  export declare function generateTriageCacheReadmeBody(projectRoot: string): string;
14
14
  /** Strip an inline `# ...` comment from a gitignore line. */
@@ -20,6 +20,9 @@ export const GITIGNORE_EVAL_ENTRIES = [
20
20
  "xbrief/.triage-cache/scope-lifecycle.jsonl",
21
21
  "xbrief/.triage-cache/decompositions/",
22
22
  "xbrief/.triage-cache/doctor-state.json",
23
+ // Per-clone session state (#3146); selective only — hybrid policy preserved.
24
+ "xbrief/.triage-cache/staleness-tickler-state.json",
25
+ "xbrief/.triage-cache/release-availability-state.json",
23
26
  ];
24
27
  /** Layout-aware gitignore lines for triage working-set files (#1703). */
25
28
  export function gitignoreTriageCacheEntries(projectRoot) {
@@ -30,6 +33,9 @@ export function gitignoreTriageCacheEntries(projectRoot) {
30
33
  triageCacheRelPath(projectRoot, "scope-lifecycle.jsonl"),
31
34
  decomp.endsWith("/") ? decomp : `${decomp}/`,
32
35
  triageCacheRelPath(projectRoot, "doctor-state.json"),
36
+ // Per-clone session state (#3146); selective only — hybrid policy preserved.
37
+ triageCacheRelPath(projectRoot, "staleness-tickler-state.json"),
38
+ triageCacheRelPath(projectRoot, "release-availability-state.json"),
33
39
  ];
34
40
  }
35
41
  export function gitattributesTriageCacheGlob(projectRoot) {
@@ -76,6 +82,10 @@ const EVAL_ENTRIES_RATIONALE = "\n# vbrief/.triage-cache/ tracking governance (#
76
82
  "# throttle state gating the 24h/4h\n" +
77
83
  "# re-probe window; #1308 / #1464). Local\n" +
78
84
  "# to each clone; never committed.\n" +
85
+ "# - staleness-tickler-state.json -> gitignored (per-clone upgrade\n" +
86
+ "# tickler throttle; #2488 / #3146).\n" +
87
+ "# - release-availability-state.json -> gitignored (per-clone npm\n" +
88
+ "# release-availability throttle; #1692 / #3146).\n" +
79
89
  "# - slices.jsonl -> TRACKED (team-shared cohort records\n" +
80
90
  "# produced by slicing skills; see\n" +
81
91
  "# #1132 / D13).\n" +
@@ -104,12 +114,16 @@ committed.
104
114
  | \`scope-lifecycle.jsonl\` | No | Local audit trail for scope demotions (\`deft scope:demote\`). Each operator's stream stays on their machine. |
105
115
  | \`decompositions/\` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via \`deft scope:decompose\`. |
106
116
  | \`doctor-state.json\` | No | Per-clone throttle state for \`deft doctor\` re-probe timing. |
117
+ | \`staleness-tickler-state.json\` | No | Per-clone upgrade-tickler throttle state. |
118
+ | \`release-availability-state.json\` | No | Per-clone release-availability probe throttle state. |
107
119
 
108
120
  Paths listed as "No" above are added to \`.gitignore\` during bootstrap; anything
109
121
  not listed remains committable by default. The selective ignore entries live in
110
122
  the repo-root \`.gitignore\` (\`vbrief/.triage-cache/candidates.jsonl\`,
111
123
  \`vbrief/.triage-cache/summary-history.jsonl\`, \`vbrief/.triage-cache/scope-lifecycle.jsonl\`,
112
- \`vbrief/.triage-cache/decompositions/\`, and \`vbrief/.triage-cache/doctor-state.json\`).
124
+ \`vbrief/.triage-cache/decompositions/\`, \`vbrief/.triage-cache/doctor-state.json\`,
125
+ \`vbrief/.triage-cache/staleness-tickler-state.json\`, and
126
+ \`vbrief/.triage-cache/release-availability-state.json\`).
113
127
 
114
128
  ## Fresh clone
115
129
 
@@ -85,8 +85,8 @@ export declare function validateProject(projectRoot: string): {
85
85
  };
86
86
  /** Render --list output for a project root. */
87
87
  export declare function listProject(projectRoot: string): string;
88
- import { type ClassifyAction, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorEngine, type LabelMirrorItem, type LabelMirrorOptions as LabelMirrorOptionsCore, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule } from "./label-mirror.js";
89
- export { type ClassifyAction, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorEngine, type LabelMirrorItem, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
88
+ import { buildLabelMirrorDigest, type ClassifyAction, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorDigest, type LabelMirrorEngine, type LabelMirrorFilters, type LabelMirrorItem, type LabelMirrorOptions as LabelMirrorOptionsCore, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorSleepFn, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule } from "./label-mirror.js";
89
+ export { buildLabelMirrorDigest, type ClassifyAction, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorDigest, type LabelMirrorEngine, type LabelMirrorFilters, type LabelMirrorItem, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorSleepFn, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
90
90
  /** Public options: engine is optional (defaults to this module's classify API). */
91
91
  export type LabelMirrorOptions = Omit<LabelMirrorOptionsCore, "engine"> & {
92
92
  readonly engine?: LabelMirrorEngine;
@@ -751,8 +751,8 @@ export function listProject(projectRoot) {
751
751
  // label-mirror.ts does not import this module (avoids ESM/SLizard cycle).
752
752
  // Public mirrorLabels injects the classify engine into the pure implementation.
753
753
  // ---------------------------------------------------------------------------
754
- import { DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, mirrorLabels as mirrorLabelsCore, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule, } from "./label-mirror.js";
755
- export { DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
754
+ import { buildLabelMirrorDigest, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, mirrorLabels as mirrorLabelsCore, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule, } from "./label-mirror.js";
755
+ export { buildLabelMirrorDigest, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
756
756
  function defaultLabelMirrorEngine() {
757
757
  return {
758
758
  classifyIssue: (issue, options) => classifyIssue(issue, {
@@ -1,17 +1,28 @@
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
  */
11
15
  import type { LabelClient } from "../../vbrief-reconcile/types.js";
16
+ import { type AuthorFilter } from "../author-filter.js";
12
17
  export declare const DEFAULT_IDEMPOTENCY_LABEL = "triaged";
13
18
  export declare const CACHE_DIR_NAME = ".deft-cache";
14
19
  export declare const CACHE_SOURCE = "github-issue";
20
+ /** Default apply batch size for rate-limit awareness (#3125). */
21
+ export declare const DEFAULT_APPLY_BATCH_SIZE = 10;
22
+ /** Default delay between apply batches in ms (#3125). */
23
+ export declare const DEFAULT_APPLY_DELAY_MS = 1000;
24
+ /** Default sample count in human digest (#3125). */
25
+ export declare const DEFAULT_DIGEST_SAMPLE_LIMIT = 15;
15
26
  export type ClassifyAction = "defer" | "archive" | "escalate" | "accept";
16
27
  /** Minimal issue shape used by the mirror (matches classify GitHubIssue). */
17
28
  export interface MirrorGitHubIssue {
@@ -73,10 +84,12 @@ export interface ResolvedLabelMirrorPolicy {
73
84
  readonly alwaysLabels: readonly string[];
74
85
  readonly actionLabels: Readonly<Partial<Record<ClassifyAction, readonly string[]>>>;
75
86
  }
76
- export type LabelMirrorStatus = "planned" | "applied" | "unchanged" | "skipped_already_triaged" | "skipped_no_match" | "skipped_unreadable" | "skipped_disabled" | "error";
87
+ export type LabelMirrorStatus = "planned" | "applied" | "unchanged" | "skipped_already_triaged" | "skipped_no_match" | "skipped_unreadable" | "skipped_closed" | "skipped_author" | "skipped_disabled" | "error";
77
88
  export interface LabelMirrorItem {
78
89
  readonly repo: string;
79
90
  readonly issue_number: number;
91
+ /** Issue state from cache (open/closed/unknown). */
92
+ readonly state: string | null;
80
93
  readonly action: string | null;
81
94
  readonly reason: string | null;
82
95
  readonly ruleKind: string | null;
@@ -86,6 +99,24 @@ export interface LabelMirrorItem {
86
99
  readonly status: LabelMirrorStatus;
87
100
  readonly message?: string;
88
101
  }
102
+ /** Operator digest aggregates for bootstrap mass-triage (#3125 / #1423 Wave 2). */
103
+ export interface LabelMirrorDigest {
104
+ readonly by_state: Readonly<Record<string, number>>;
105
+ readonly by_rule: Readonly<Record<string, number>>;
106
+ readonly by_action: Readonly<Record<string, number>>;
107
+ readonly samples: readonly LabelMirrorItem[];
108
+ readonly sample_limit: number;
109
+ readonly sample_truncated: boolean;
110
+ }
111
+ export interface LabelMirrorFilters {
112
+ /** When false (default), closed issues are skipped before classify. */
113
+ readonly include_closed: boolean;
114
+ readonly repo: string | null;
115
+ /** Active author allow-list display (#3129); null when no author filter. */
116
+ readonly author: string | null;
117
+ /** Resolved author logins for machine consumers. */
118
+ readonly author_logins: readonly string[] | null;
119
+ }
89
120
  export interface LabelMirrorOutcome {
90
121
  readonly project_root: string;
91
122
  readonly dry_run: boolean;
@@ -96,10 +127,20 @@ export interface LabelMirrorOutcome {
96
127
  readonly skipped_already_triaged: number;
97
128
  readonly skipped_no_match: number;
98
129
  readonly skipped_unreadable: number;
130
+ /** Closed issues skipped by open-only default (#3125). */
131
+ readonly skipped_closed: number;
132
+ /** Issues skipped by --author filter (#3129). */
133
+ readonly skipped_author: number;
99
134
  readonly errors: number;
135
+ readonly filters: LabelMirrorFilters;
136
+ readonly digest: LabelMirrorDigest;
100
137
  readonly items: readonly LabelMirrorItem[];
101
138
  readonly policy: ResolvedLabelMirrorPolicy;
139
+ /** Apply path: successful writes in this run (same as applied). */
140
+ readonly batch_size?: number;
141
+ readonly delay_ms?: number;
102
142
  }
143
+ export type LabelMirrorSleepFn = (ms: number) => void;
103
144
  export interface LabelMirrorOptions {
104
145
  readonly dryRun?: boolean;
105
146
  readonly repo?: string | null;
@@ -110,6 +151,24 @@ export interface LabelMirrorOptions {
110
151
  /** Prefer live SCM labels when true (default: !dryRun). Cache labels used for dry-run. */
111
152
  readonly useLiveLabels?: boolean;
112
153
  readonly now?: Date;
154
+ /**
155
+ * Include closed issues in classify+mirror. Default false (open-only) for safe
156
+ * bootstrap mass-triage (#3125). Opt in with CLI `--include-closed`.
157
+ */
158
+ readonly includeClosed?: boolean;
159
+ /**
160
+ * Resolved author filter applied before plan/apply walk (#3129).
161
+ * Composes with open-only (AND). CLI resolves `@me` before passing this.
162
+ */
163
+ readonly authorFilter?: AuthorFilter | null;
164
+ /** Max planned/applied samples in human digest (default 15). */
165
+ readonly sampleLimit?: number;
166
+ /** SCM writes per batch before delay (default 10; apply path only). */
167
+ readonly batchSize?: number;
168
+ /** Delay in ms between apply batches (default 1000; apply path only). */
169
+ readonly delayMs?: number;
170
+ /** Injectable sleep for tests (receives ms). Default busy-wait when delayMs > 0. */
171
+ readonly sleepMs?: LabelMirrorSleepFn;
113
172
  /** Required: classify engine (provided by classify/index mirrorLabels wrapper). */
114
173
  readonly engine: LabelMirrorEngine;
115
174
  }
@@ -128,15 +187,19 @@ export declare function resolveLabelMirrorPolicy(options?: {
128
187
  }): ResolvedLabelMirrorPolicy;
129
188
  /** Labels to apply for a classified action (always + action-mapped). */
130
189
  export declare function desiredLabelsForClassification(action: string, policy: ResolvedLabelMirrorPolicy): string[];
190
+ /** Build digest aggregates + samples from mirror items (#3125). */
191
+ export declare function buildLabelMirrorDigest(items: readonly LabelMirrorItem[], sampleLimit?: number): LabelMirrorDigest;
131
192
  /**
132
- * Run Tier-1 label mirror over the github-issue cache.
193
+ * Run Tier-1 label mirror over the github-issue cache (bootstrap mass-triage surface).
133
194
  * Dry-run by default (no SCM writes). Pass dryRun: false to apply.
195
+ * Default state filter is open-only (#3125); pass includeClosed: true for archive stamps.
134
196
  * Requires options.engine (classify/index wrapper injects it).
197
+ * Never calls triage:accept / never writes proposed/ xBRIEFs.
135
198
  */
136
199
  export declare function mirrorLabels(projectRoot: string, options: LabelMirrorOptions): [number, LabelMirrorOutcome];
137
- /** Human-readable digest for dry-run / apply reports. */
200
+ /** Human-readable digest for dry-run / apply reports (bootstrap mass-triage UX). */
138
201
  export declare function renderLabelMirrorReport(outcome: LabelMirrorOutcome): string;
139
- /** JSON-serializable outcome (stable key order not required). */
202
+ /** JSON-serializable outcome including Wave 2 digest aggregates. */
140
203
  export declare function labelMirrorOutcomeToJson(outcome: LabelMirrorOutcome): Record<string, unknown>;
141
204
  /** Validate triageLabelMirror on a plan object (vbrief_validate hook). */
142
205
  export declare function validateTriageLabelMirrorOnPlan(plan: unknown, filepath: string): string[];