@deftai/directive-core 0.95.0 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cache/archive.d.ts +134 -0
  2. package/dist/cache/archive.js +630 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/cache/main.js +298 -1
  6. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  7. package/dist/content-contracts/skills/helpers.js +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/init-deposit/hygiene.d.ts +1 -1
  11. package/dist/init-deposit/hygiene.js +16 -4
  12. package/dist/init-deposit/scaffold.js +6 -5
  13. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  14. package/dist/parent-turn-shape/evaluate.js +353 -0
  15. package/dist/parent-turn-shape/index.d.ts +8 -0
  16. package/dist/parent-turn-shape/index.js +8 -0
  17. package/dist/review-monitor/constants.js +3 -2
  18. package/dist/review-monitor/tier-detection.d.ts +7 -3
  19. package/dist/review-monitor/tier-detection.js +18 -1
  20. package/dist/review-monitor/verify.js +18 -0
  21. package/dist/scope/index.d.ts +2 -0
  22. package/dist/scope/index.js +2 -0
  23. package/dist/scope/main.d.ts +10 -0
  24. package/dist/scope/main.js +109 -24
  25. package/dist/scope/promote-from-issue.d.ts +49 -0
  26. package/dist/scope/promote-from-issue.js +367 -0
  27. package/dist/scope/promote-path.d.ts +39 -0
  28. package/dist/scope/promote-path.js +105 -0
  29. package/dist/swarm/routing.d.ts +4 -2
  30. package/dist/swarm/routing.js +26 -4
  31. package/dist/triage/actions/index.js +62 -2
  32. package/dist/triage/actions/types.d.ts +8 -1
  33. package/dist/triage/author-filter.d.ts +51 -0
  34. package/dist/triage/author-filter.js +152 -0
  35. package/dist/triage/classify/index.d.ts +2 -2
  36. package/dist/triage/classify/index.js +2 -2
  37. package/dist/triage/classify/label-mirror.d.ts +68 -5
  38. package/dist/triage/classify/label-mirror.js +261 -31
  39. package/dist/triage/help/registry-data.d.ts +49 -38
  40. package/dist/triage/help/registry-data.js +115 -40
  41. package/dist/triage/index.d.ts +1 -0
  42. package/dist/triage/index.js +1 -0
  43. package/dist/triage/queue/index.d.ts +1 -0
  44. package/dist/triage/queue/index.js +1 -0
  45. package/dist/triage/queue/render.d.ts +2 -0
  46. package/dist/triage/queue/render.js +6 -0
  47. package/package.json +7 -3
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared single-path promote (proposed/ → pending/) with optional triage audit linkage (#1136).
3
+ */
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
7
+ import { resolveProjectRoot } from "./project-context.js";
8
+ import { recordWipCapOverride, runTransition } from "./transition.js";
9
+ import { utcNowIso } from "./vbrief-json.js";
10
+ import { canonicalRelpath } from "./vbrief-ref.js";
11
+ import { checkWipCap, formatWipCapRefusal } from "./wip-cap-check.js";
12
+ /**
13
+ * Promote a single proposed-scope path to pending/, enforcing WIP and
14
+ * optionally recording from_issue / cache_decision_id on the scope audit log.
15
+ */
16
+ export function promotePath(filePath, options = {}) {
17
+ const root = resolveProjectRoot(options.projectRoot);
18
+ if (root === null) {
19
+ return {
20
+ ok: false,
21
+ message: "Cannot determine project root. Pass --project-root PATH, set $DEFT_PROJECT_ROOT, or run from inside a directory tree that contains vbrief/ or .git/ (#535).",
22
+ exitCode: 2,
23
+ };
24
+ }
25
+ const resolved = resolve(filePath);
26
+ if (!existsSync(resolved)) {
27
+ return { ok: false, message: `File not found: ${resolved}`, exitCode: 2 };
28
+ }
29
+ const capCheck = checkWipCap(root, options.force === true);
30
+ if (!capCheck.allowed) {
31
+ return {
32
+ ok: false,
33
+ message: formatWipCapRefusal(capCheck),
34
+ exitCode: 1,
35
+ };
36
+ }
37
+ const now = options.now ?? new Date();
38
+ const result = runTransition("promote", resolved, now);
39
+ if (!result.ok) {
40
+ return { ok: false, message: result.message, exitCode: 1 };
41
+ }
42
+ const basename = resolved.split(/[/\\]/).pop() ?? "";
43
+ // Destination after promote is sibling pending/ under the same lifecycle root.
44
+ const lifecycleRoot = dirname(dirname(resolved));
45
+ const destPath = join(lifecycleRoot, "pending", basename);
46
+ let auditEntry = null;
47
+ const shouldAudit = options.alwaysAudit === true ||
48
+ options.fromIssue !== undefined ||
49
+ options.forceNoCache === true ||
50
+ options.cacheDecisionId !== undefined ||
51
+ options.cacheStateAtPromote !== undefined;
52
+ if (shouldAudit) {
53
+ try {
54
+ const entry = {
55
+ decision_id: newDecisionId(),
56
+ timestamp: utcNowIso(now),
57
+ action: "promote",
58
+ vbrief_path: canonicalRelpath(destPath, root),
59
+ from_status: "proposed",
60
+ to_status: "pending",
61
+ actor: options.actor ?? "operator",
62
+ };
63
+ if (options.fromIssue !== undefined) {
64
+ entry.from_issue = options.fromIssue;
65
+ }
66
+ if (options.cacheDecisionId !== undefined) {
67
+ entry.cache_decision_id = options.cacheDecisionId;
68
+ }
69
+ if (options.cacheStateAtPromote !== undefined) {
70
+ entry.cache_state_at_promote = options.cacheStateAtPromote;
71
+ }
72
+ if (options.forceNoCache === true) {
73
+ entry.force_no_cache = true;
74
+ }
75
+ append(entry, canonicalLogPath(root));
76
+ auditEntry = entry;
77
+ }
78
+ catch (err) {
79
+ if (options.requireAudit === true) {
80
+ return {
81
+ ok: false,
82
+ message: `Promoted ${basename} to pending/ but required scope audit failed: ${String(err)}. ` +
83
+ `Artifact is in pending/; re-run promote will no-op once audit is writable.`,
84
+ exitCode: 1,
85
+ destPath,
86
+ auditEntry: null,
87
+ wipCapOverride: capCheck.forceOverride,
88
+ };
89
+ }
90
+ /* best-effort audit for plain path promote */
91
+ }
92
+ }
93
+ if (capCheck.forceOverride) {
94
+ recordWipCapOverride(destPath, root, capCheck, now);
95
+ }
96
+ return {
97
+ ok: true,
98
+ message: result.message,
99
+ exitCode: 0,
100
+ destPath,
101
+ auditEntry,
102
+ wipCapOverride: capCheck.forceOverride,
103
+ };
104
+ }
105
+ //# sourceMappingURL=promote-path.js.map
@@ -9,7 +9,7 @@ export declare const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
9
9
  export declare const ROUTING_FILENAME = "routing.local.json";
10
10
  /** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
11
11
  export declare const HARNESS_BOUND_PROVIDERS: Set<string>;
12
- /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877 / #2875). */
12
+ /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877 / #2875 / #3134). */
13
13
  export declare const ROUTING_GATED_DISPATCH_PROVIDERS: Set<string>;
14
14
  export interface RouteDecision {
15
15
  model: string | null;
@@ -55,7 +55,9 @@ export declare function dispatchProviderFromRuntime(runtimeMode: string): string
55
55
  * Separate from `runtime_mode` (#1557): Cursor sessions may carry
56
56
  * `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
57
57
  * `cursor` for model selection (#1877). OpenClaw routes under `openclaw` when
58
- * `sessions_spawn` / OPENCLAW signals are present (#2875).
58
+ * `sessions_spawn` / OPENCLAW signals are present (#2875). Claude Code routes
59
+ * under `claude` when Claude-unique signals are present (#3134) — never via bare
60
+ * Task (that would misclassify as cursor).
59
61
  */
60
62
  export declare function resolveDispatchProvider(environ?: NodeJS.ProcessEnv): string;
61
63
  /**
@@ -33,8 +33,13 @@ export const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
33
33
  export const ROUTING_FILENAME = "routing.local.json";
34
34
  /** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
35
35
  export const HARNESS_BOUND_PROVIDERS = new Set(["grok"]);
36
- /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877 / #2875). */
37
- export const ROUTING_GATED_DISPATCH_PROVIDERS = new Set(["cursor", "grok", "openclaw"]);
36
+ /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877 / #2875 / #3134). */
37
+ export const ROUTING_GATED_DISPATCH_PROVIDERS = new Set([
38
+ "cursor",
39
+ "grok",
40
+ "openclaw",
41
+ "claude",
42
+ ]);
38
43
  const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
39
44
  function envTruthy(environ, name) {
40
45
  return TRUTHY_ENV.has((environ[name] ?? "").trim().toLowerCase());
@@ -153,6 +158,9 @@ export function dispatchProviderFromRuntime(runtimeMode) {
153
158
  if (normalized.includes("openclaw")) {
154
159
  return "openclaw";
155
160
  }
161
+ if (normalized.includes("claude")) {
162
+ return "claude";
163
+ }
156
164
  if (normalized.includes("grok")) {
157
165
  return "grok";
158
166
  }
@@ -166,13 +174,24 @@ export function dispatchProviderFromRuntime(runtimeMode) {
166
174
  * Separate from `runtime_mode` (#1557): Cursor sessions may carry
167
175
  * `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
168
176
  * `cursor` for model selection (#1877). OpenClaw routes under `openclaw` when
169
- * `sessions_spawn` / OPENCLAW signals are present (#2875).
177
+ * `sessions_spawn` / OPENCLAW signals are present (#2875). Claude Code routes
178
+ * under `claude` when Claude-unique signals are present (#3134) — never via bare
179
+ * Task (that would misclassify as cursor).
170
180
  */
171
181
  export function resolveDispatchProvider(environ = process.env) {
172
182
  if (envTruthy(environ, "CURSOR_COMPOSER") || envTruthy(environ, "CURSOR_AGENT")) {
173
183
  return "cursor";
174
184
  }
175
185
  const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
186
+ // Claude Code before OpenClaw/CI so CLAUDECODE / DEFT_PROBE_CLAUDE_CODE win (#3134).
187
+ if (envTruthy(environ, "DEFT_PROBE_CLAUDE_CODE") ||
188
+ envTruthy(environ, "DEFT_HAS_CLAUDE_AGENT") ||
189
+ envTruthy(environ, "CLAUDECODE") ||
190
+ envTruthy(environ, "CLAUDE_CODE") ||
191
+ runtime === "claude-code" ||
192
+ runtime === "claude") {
193
+ return "claude";
194
+ }
176
195
  if (envTruthy(environ, "OPENCLAW") ||
177
196
  envTruthy(environ, "DEFT_HAS_SESSIONS_SPAWN") ||
178
197
  envTruthy(environ, "DEFT_PROBE_SESSIONS_SPAWN") ||
@@ -191,7 +210,10 @@ export function resolveDispatchProvider(environ = process.env) {
191
210
  !envTruthy(environ, "CURSOR_COMPOSER") &&
192
211
  !envTruthy(environ, "CURSOR_AGENT") &&
193
212
  !envTruthy(environ, "OPENCLAW") &&
194
- !envTruthy(environ, "DEFT_HAS_SESSIONS_SPAWN"))) {
213
+ !envTruthy(environ, "DEFT_HAS_SESSIONS_SPAWN") &&
214
+ !envTruthy(environ, "CLAUDECODE") &&
215
+ !envTruthy(environ, "CLAUDE_CODE") &&
216
+ !envTruthy(environ, "DEFT_PROBE_CLAUDE_CODE"))) {
195
217
  return "cloud-headless";
196
218
  }
197
219
  return "unknown";
@@ -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
@@ -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, {