@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.
- package/dist/cache/archive.d.ts +134 -0
- package/dist/cache/archive.js +630 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/cache/main.js +298 -1
- package/dist/content-contracts/skills/helpers.d.ts +1 -1
- package/dist/content-contracts/skills/helpers.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/hygiene.d.ts +1 -1
- package/dist/init-deposit/hygiene.js +16 -4
- package/dist/init-deposit/scaffold.js +6 -5
- package/dist/parent-turn-shape/evaluate.d.ts +84 -0
- package/dist/parent-turn-shape/evaluate.js +353 -0
- package/dist/parent-turn-shape/index.d.ts +8 -0
- package/dist/parent-turn-shape/index.js +8 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +7 -3
- package/dist/review-monitor/tier-detection.js +18 -1
- package/dist/review-monitor/verify.js +18 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +10 -0
- package/dist/scope/main.js +109 -24
- package/dist/scope/promote-from-issue.d.ts +49 -0
- package/dist/scope/promote-from-issue.js +367 -0
- package/dist/scope/promote-path.d.ts +39 -0
- package/dist/scope/promote-path.js +105 -0
- package/dist/swarm/routing.d.ts +4 -2
- package/dist/swarm/routing.js +26 -4
- package/dist/triage/actions/index.js +62 -2
- package/dist/triage/actions/types.d.ts +8 -1
- package/dist/triage/author-filter.d.ts +51 -0
- package/dist/triage/author-filter.js +152 -0
- package/dist/triage/classify/index.d.ts +2 -2
- package/dist/triage/classify/index.js +2 -2
- package/dist/triage/classify/label-mirror.d.ts +68 -5
- package/dist/triage/classify/label-mirror.js +261 -31
- package/dist/triage/help/registry-data.d.ts +49 -38
- package/dist/triage/help/registry-data.js +115 -40
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/render.d.ts +2 -0
- package/dist/triage/queue/render.js +6 -0
- 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
|
package/dist/swarm/routing.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/swarm/routing.js
CHANGED
|
@@ -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([
|
|
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
|
-
}):
|
|
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, {
|