@deftai/directive 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.
@@ -70,6 +70,12 @@ export const SUBCOMMAND_ROUTES = {
70
70
  "cache:fetch-all": ["cache", "fetch-all"],
71
71
  "cache:prune": ["cache", "prune"],
72
72
  "cache:clear": ["cache", "clear"],
73
+ "cache:archive-closed": ["cache", "archive-closed"],
74
+ "cache:archive-list": ["cache", "archive-list"],
75
+ "cache:restore-from-archive": ["cache", "restore-from-archive"],
76
+ "triage:cache-archive": ["cache", "archive-closed"],
77
+ "triage:archive-list": ["cache", "archive-list"],
78
+ "triage:restore-from-archive": ["cache", "restore-from-archive"],
73
79
  "policy:show": ["policy", "show"],
74
80
  "policy:disable-directive": ["policy", "disable-directive"],
75
81
  "policy:enable-directive": ["policy", "enable-directive"],
@@ -9,6 +9,10 @@ interface ParsedArgs {
9
9
  comment?: string;
10
10
  ofN?: number;
11
11
  projectRoot: string;
12
+ /** After accept+ingest, promote to pending (#1136). */
13
+ autoPromote?: boolean;
14
+ /** WIP-cap override for auto-promote leg (#1136). */
15
+ force?: boolean;
12
16
  error?: string;
13
17
  }
14
18
  /** Parse triage-actions CLI argv mirroring ``triage_actions.py`` argparse. */
@@ -101,6 +101,12 @@ export function parseArgs(argv) {
101
101
  else if (arg?.startsWith("--project-root=")) {
102
102
  parsed.projectRoot = arg.slice("--project-root=".length);
103
103
  }
104
+ else if (arg === "--auto-promote") {
105
+ parsed.autoPromote = true;
106
+ }
107
+ else if (arg === "--force") {
108
+ parsed.force = true;
109
+ }
104
110
  else {
105
111
  return { ...parsed, error: `unrecognized argument: ${arg}` };
106
112
  }
@@ -144,8 +150,14 @@ export function run(argv) {
144
150
  const repo = args.repo;
145
151
  try {
146
152
  if (args.cmd === "accept") {
147
- const decisionId = accept(n, repo, deps, { actor: args.actor, projectRoot });
148
- process.stdout.write(`accept #${n} (${repo}) -> ${decisionId}\n`);
153
+ const decisionId = accept(n, repo, deps, {
154
+ actor: args.actor,
155
+ projectRoot,
156
+ autoPromote: args.autoPromote === true,
157
+ force: args.force === true,
158
+ });
159
+ const promoteNote = args.autoPromote === true ? " + auto-promote" : "";
160
+ process.stdout.write(`accept #${n} (${repo}) -> ${decisionId}${promoteNote}\n`);
149
161
  }
150
162
  else if (args.cmd === "reject") {
151
163
  const decisionId = reject(n, repo, args.reason ?? "", deps, {
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type ResolveAuthenticatedLogin } from "@deftai/directive-core/dist/triage/author-filter.js";
2
3
  import type { LabelClient } from "@deftai/directive-core/dist/vbrief-reconcile/types.js";
3
4
  export interface ParsedArgs {
4
5
  projectRoot: string;
@@ -9,13 +10,25 @@ export interface ParsedArgs {
9
10
  json: boolean;
10
11
  repo: string | null;
11
12
  allowCrossRepo: boolean;
13
+ /** Opt-in: include closed issues (default open-only, #3125). */
14
+ includeClosed: boolean;
15
+ /** Raw --author value (LOGIN, @me, comma allow-list); null = no filter (#3129). */
16
+ author: string | null;
17
+ /** Apply batch size (rate-limit awareness). */
18
+ batchSize: number | null;
19
+ /** Delay ms between apply batches. */
20
+ delayMs: number | null;
21
+ /** Max samples in human digest. */
22
+ sampleLimit: number | null;
12
23
  error?: string;
13
24
  }
14
- /** Parse triage-classify CLI args (#1129 + #1423 Wave 1 mirror flags). */
25
+ /** Parse triage-classify CLI args (#1129 + #1423 Wave 1/2 mirror flags). */
15
26
  export declare function parseArgs(argv: string[]): ParsedArgs;
16
27
  export interface RunOptions {
17
28
  /** Injected LabelClient for tests (apply path). */
18
29
  readonly labelClient?: LabelClient;
30
+ /** Override `@me` resolution for hermetic tests (#3129). */
31
+ readonly resolveAuthenticatedLogin?: ResolveAuthenticatedLogin;
19
32
  }
20
33
  /** Run the CLI and return the process exit code. */
21
34
  export declare function run(argv: string[], options?: RunOptions): number;
@@ -2,8 +2,29 @@
2
2
  import { statSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { resolveAuthorFilter, } from "@deftai/directive-core/dist/triage/author-filter.js";
5
6
  import { labelMirrorOutcomeToJson, listProject, mirrorLabels, renderLabelMirrorReport, validateProject, } from "@deftai/directive-core/dist/triage/classify/index.js";
6
- /** Parse triage-classify CLI args (#1129 + #1423 Wave 1 mirror flags). */
7
+ function parseNonNegInt(raw, flag) {
8
+ const n = Number.parseInt(raw, 10);
9
+ if (!Number.isFinite(n) || String(n) !== raw.trim() || n < 0) {
10
+ return { error: `argument ${flag}: expected a non-negative integer` };
11
+ }
12
+ return { value: n };
13
+ }
14
+ /** Batch size must be >= 1 (0 would silently fall back in core). */
15
+ function parseBatchSize(raw) {
16
+ const parsed = parseNonNegInt(raw, "--batch-size");
17
+ if (parsed.error !== undefined) {
18
+ return parsed;
19
+ }
20
+ if ((parsed.value ?? 0) < 1) {
21
+ return {
22
+ error: "argument --batch-size: expected an integer >= 1 (omit flag for default 10)",
23
+ };
24
+ }
25
+ return parsed;
26
+ }
27
+ /** Parse triage-classify CLI args (#1129 + #1423 Wave 1/2 mirror flags). */
7
28
  export function parseArgs(argv) {
8
29
  const parsed = {
9
30
  projectRoot: ".",
@@ -14,6 +35,11 @@ export function parseArgs(argv) {
14
35
  json: false,
15
36
  repo: null,
16
37
  allowCrossRepo: false,
38
+ includeClosed: false,
39
+ author: null,
40
+ batchSize: null,
41
+ delayMs: null,
42
+ sampleLimit: null,
17
43
  };
18
44
  for (let i = 0; i < argv.length; i += 1) {
19
45
  const arg = argv[i];
@@ -35,6 +61,38 @@ export function parseArgs(argv) {
35
61
  else if (arg === "--allow-cross-repo") {
36
62
  parsed.allowCrossRepo = true;
37
63
  }
64
+ else if (arg === "--include-closed") {
65
+ parsed.includeClosed = true;
66
+ }
67
+ else if (arg === "--author-mine") {
68
+ parsed.author = "@me";
69
+ }
70
+ else if (arg === "--author") {
71
+ const value = argv[i + 1];
72
+ if (value === undefined) {
73
+ return { ...parsed, error: "argument --author: expected one argument" };
74
+ }
75
+ // Reject adjacent flags (e.g. `--author --apply`) so they are not
76
+ // swallowed as logins (#3129 Greptile P1 adjacent-option).
77
+ if (value.startsWith("-")) {
78
+ return {
79
+ ...parsed,
80
+ error: `argument --author: expected a login (or @me), got flag token '${value}'`,
81
+ };
82
+ }
83
+ parsed.author = value;
84
+ i += 1;
85
+ }
86
+ else if (arg?.startsWith("--author=")) {
87
+ const value = arg.slice("--author=".length);
88
+ if (value.startsWith("-") && value.length > 1) {
89
+ return {
90
+ ...parsed,
91
+ error: `argument --author: expected a login (or @me), got flag token '${value}'`,
92
+ };
93
+ }
94
+ parsed.author = value;
95
+ }
38
96
  else if (arg === "--repo") {
39
97
  const value = argv[i + 1];
40
98
  if (value === undefined) {
@@ -46,6 +104,63 @@ export function parseArgs(argv) {
46
104
  else if (arg?.startsWith("--repo=")) {
47
105
  parsed.repo = arg.slice("--repo=".length);
48
106
  }
107
+ else if (arg === "--batch-size") {
108
+ const value = argv[i + 1];
109
+ if (value === undefined) {
110
+ return { ...parsed, error: "argument --batch-size: expected one argument" };
111
+ }
112
+ const parsedInt = parseBatchSize(value);
113
+ if (parsedInt.error !== undefined) {
114
+ return { ...parsed, error: parsedInt.error };
115
+ }
116
+ parsed.batchSize = parsedInt.value ?? null;
117
+ i += 1;
118
+ }
119
+ else if (arg?.startsWith("--batch-size=")) {
120
+ const parsedInt = parseBatchSize(arg.slice("--batch-size=".length));
121
+ if (parsedInt.error !== undefined) {
122
+ return { ...parsed, error: parsedInt.error };
123
+ }
124
+ parsed.batchSize = parsedInt.value ?? null;
125
+ }
126
+ else if (arg === "--delay-ms") {
127
+ const value = argv[i + 1];
128
+ if (value === undefined) {
129
+ return { ...parsed, error: "argument --delay-ms: expected one argument" };
130
+ }
131
+ const parsedInt = parseNonNegInt(value, "--delay-ms");
132
+ if (parsedInt.error !== undefined) {
133
+ return { ...parsed, error: parsedInt.error };
134
+ }
135
+ parsed.delayMs = parsedInt.value ?? null;
136
+ i += 1;
137
+ }
138
+ else if (arg?.startsWith("--delay-ms=")) {
139
+ const parsedInt = parseNonNegInt(arg.slice("--delay-ms=".length), "--delay-ms");
140
+ if (parsedInt.error !== undefined) {
141
+ return { ...parsed, error: parsedInt.error };
142
+ }
143
+ parsed.delayMs = parsedInt.value ?? null;
144
+ }
145
+ else if (arg === "--sample-limit") {
146
+ const value = argv[i + 1];
147
+ if (value === undefined) {
148
+ return { ...parsed, error: "argument --sample-limit: expected one argument" };
149
+ }
150
+ const parsedInt = parseNonNegInt(value, "--sample-limit");
151
+ if (parsedInt.error !== undefined) {
152
+ return { ...parsed, error: parsedInt.error };
153
+ }
154
+ parsed.sampleLimit = parsedInt.value ?? null;
155
+ i += 1;
156
+ }
157
+ else if (arg?.startsWith("--sample-limit=")) {
158
+ const parsedInt = parseNonNegInt(arg.slice("--sample-limit=".length), "--sample-limit");
159
+ if (parsedInt.error !== undefined) {
160
+ return { ...parsed, error: parsedInt.error };
161
+ }
162
+ parsed.sampleLimit = parsedInt.value ?? null;
163
+ }
49
164
  else if (arg === "--project-root") {
50
165
  const value = argv[i + 1];
51
166
  if (value === undefined) {
@@ -67,7 +182,18 @@ export function parseArgs(argv) {
67
182
  if (parsed.apply && !parsed.doMirror) {
68
183
  return {
69
184
  ...parsed,
70
- error: "--apply requires --mirror (Tier-1 label mirror, #1423)",
185
+ error: "--apply requires --mirror (Tier-1 label mirror / bootstrap mass-triage, #1423)",
186
+ };
187
+ }
188
+ if ((parsed.includeClosed ||
189
+ parsed.author !== null ||
190
+ parsed.batchSize !== null ||
191
+ parsed.delayMs !== null ||
192
+ parsed.sampleLimit !== null) &&
193
+ !parsed.doMirror) {
194
+ return {
195
+ ...parsed,
196
+ error: "--include-closed / --author / --batch-size / --delay-ms / --sample-limit require --mirror (#3125 / #3129)",
71
197
  };
72
198
  }
73
199
  return parsed;
@@ -102,10 +228,26 @@ export function run(argv, options = {}) {
102
228
  return result.code;
103
229
  }
104
230
  if (args.doMirror) {
231
+ let authorFilter = null;
232
+ // Flag present (including empty `--author=`) must resolve or fail closed —
233
+ // never silent no-op that would plan/apply the full open cache (#3129 Greptile P1).
234
+ if (args.author !== null) {
235
+ const resolved = resolveAuthorFilter(args.author, options.resolveAuthenticatedLogin);
236
+ if (resolved.error !== undefined || resolved.filter === undefined) {
237
+ process.stderr.write(`ERR: ${resolved.error ?? "argument --author: expected a non-empty login (or @me)"}\n`);
238
+ return 2;
239
+ }
240
+ authorFilter = resolved.filter;
241
+ }
105
242
  const mirrorOpts = {
106
243
  dryRun: !args.apply,
107
244
  repo: args.repo,
108
245
  allowCrossRepo: args.allowCrossRepo,
246
+ includeClosed: args.includeClosed,
247
+ ...(authorFilter !== null && authorFilter !== undefined ? { authorFilter } : {}),
248
+ ...(args.batchSize !== null ? { batchSize: args.batchSize } : {}),
249
+ ...(args.delayMs !== null ? { delayMs: args.delayMs } : {}),
250
+ ...(args.sampleLimit !== null ? { sampleLimit: args.sampleLimit } : {}),
109
251
  ...(options.labelClient !== undefined ? { client: options.labelClient } : {}),
110
252
  };
111
253
  const [code, outcome] = mirrorLabels(projectRoot, mirrorOpts);
@@ -10,6 +10,8 @@ export interface QueueFixtureIssue {
10
10
  readonly labels?: readonly string[];
11
11
  readonly updatedAt?: string;
12
12
  readonly createdAt?: string;
13
+ /** Cache author.login for --author filter tests (#3129). */
14
+ readonly author?: string;
13
15
  }
14
16
  export interface QueueAuditEntry {
15
17
  readonly issueNumber: number;
@@ -25,6 +25,7 @@ function writeCachedIssue(root, repo, issue) {
25
25
  labels: (issue.labels ?? []).map((label) => ({ name: label })),
26
26
  updated_at: issue.updatedAt ?? "2026-05-17T20:00:00Z",
27
27
  ...(issue.createdAt !== undefined ? { created_at: issue.createdAt } : {}),
28
+ ...(issue.author !== undefined ? { author: { login: issue.author } } : {}),
28
29
  };
29
30
  writeFileSync(join(dir, "raw.json"), `${JSON.stringify(raw)}\n`, { encoding: "utf8" });
30
31
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { type LiveOpenIssuesReader } from "@deftai/directive-core/dist/triage/queue/index.js";
2
+ import { type LiveOpenIssuesReader, type ResolveAuthenticatedLogin } from "@deftai/directive-core/dist/triage/queue/index.js";
3
3
  export type ShowFormat = "default" | "operator";
4
4
  interface CommonArgs {
5
5
  projectRoot: string;
@@ -14,6 +14,8 @@ interface QueueArgs extends CommonArgs {
14
14
  includeBlocked: boolean;
15
15
  reconcile: boolean;
16
16
  slicesLog: string | null;
17
+ /** Raw --author value (LOGIN, @me, or comma allow-list); null = no filter (#3129). */
18
+ author: string | null;
17
19
  }
18
20
  interface ShowArgs extends CommonArgs {
19
21
  cmd: "show";
@@ -24,9 +26,11 @@ interface ShowArgs extends CommonArgs {
24
26
  export declare function parseArgs(argv: string[]): QueueArgs;
25
27
  /** Parse show subcommand args. */
26
28
  export declare function parseShowArgs(argv: string[]): ShowArgs;
27
- /** Optional injection seam for `run` (tests supply a stub live-open reader). */
29
+ /** Optional injection seams for `run` (tests supply stubs). */
28
30
  export interface RunOptions {
29
31
  readonly liveOpenReader?: LiveOpenIssuesReader;
32
+ /** Override `@me` resolution for hermetic tests (#3129). */
33
+ readonly resolveAuthenticatedLogin?: ResolveAuthenticatedLogin;
30
34
  }
31
35
  /** Run triage:queue or triage:show and return process exit code. */
32
36
  export declare function run(argv: string[], options?: RunOptions): number;
@@ -13,7 +13,7 @@ import { fileURLToPath } from "node:url";
13
13
  import { ensureTriageCacheHydrated } from "@deftai/directive-core/dist/cache/empty-populate.js";
14
14
  import { findByIssue } from "@deftai/directive-core/dist/triage/actions/candidates-log.js";
15
15
  import { resolveTriageCachePath } from "@deftai/directive-core/dist/triage/cache-path.js";
16
- import { activeReferencedIssueNumbers, buildQueue, collectOrphanIssueNumbers, DEFAULT_QUEUE_LIMIT, loadCachedIssueDetail, loadCachedIssues, loadSliceRecords, readAuditEntries, reconcileLiveOpenState, renderOperatorBrief, renderQueue, renderShow, resolveRankingLabels, resolveRepo, } from "@deftai/directive-core/dist/triage/queue/index.js";
16
+ import { activeReferencedIssueNumbers, buildQueue, collectOrphanIssueNumbers, DEFAULT_QUEUE_LIMIT, formatAuthorFilterLine, loadCachedIssueDetail, loadCachedIssues, loadSliceRecords, partitionByAuthorFilter, readAuditEntries, reconcileLiveOpenState, renderOperatorBrief, renderQueue, renderShow, resolveAuthorFilter, resolveRankingLabels, resolveRepo, } from "@deftai/directive-core/dist/triage/queue/index.js";
17
17
  import { resolveScopeIgnores } from "@deftai/directive-core/dist/triage/scope-drift/index.js";
18
18
  function baseArgs() {
19
19
  return {
@@ -84,6 +84,7 @@ export function parseArgs(argv) {
84
84
  includeBlocked: false,
85
85
  reconcile: true,
86
86
  slicesLog: null,
87
+ author: null,
87
88
  };
88
89
  for (let i = 0; i < argv.length; i += 1) {
89
90
  const arg = argv[i];
@@ -98,6 +99,39 @@ export function parseArgs(argv) {
98
99
  parsed.reconcile = false;
99
100
  continue;
100
101
  }
102
+ if (arg === "--author-mine") {
103
+ // #1318 Layer 1 optional alias for --author @me
104
+ parsed.author = "@me";
105
+ continue;
106
+ }
107
+ if (arg === "--author") {
108
+ const value = argv[i + 1];
109
+ if (value === undefined) {
110
+ return { ...parsed, error: "argument --author: expected one argument" };
111
+ }
112
+ // Reject adjacent flags (e.g. `--author --limit 10`) so they are not
113
+ // swallowed as logins (#3129 Greptile P1 adjacent-option).
114
+ if (value.startsWith("-")) {
115
+ return {
116
+ ...parsed,
117
+ error: `argument --author: expected a login (or @me), got flag token '${value}'`,
118
+ };
119
+ }
120
+ parsed.author = value;
121
+ i += 1;
122
+ continue;
123
+ }
124
+ if (arg?.startsWith("--author=")) {
125
+ const value = arg.slice("--author=".length);
126
+ if (value.startsWith("-") && value.length > 1) {
127
+ return {
128
+ ...parsed,
129
+ error: `argument --author: expected a login (or @me), got flag token '${value}'`,
130
+ };
131
+ }
132
+ parsed.author = value;
133
+ continue;
134
+ }
101
135
  const commonHit = parseCommonFlag(arg, argv, i, parsed);
102
136
  if (commonHit !== null) {
103
137
  if (commonHit.error !== undefined) {
@@ -222,11 +256,30 @@ function runQueue(args, options = {}) {
222
256
  process.stderr.write("triage:queue: --repo OWNER/NAME (or $DEFT_TRIAGE_REPO) is required.\n");
223
257
  return 2;
224
258
  }
259
+ let authorFilterLine = null;
260
+ let authorAllow;
261
+ // Flag present (including empty `--author=`) must resolve or fail closed — never
262
+ // silent no-op that would show the full queue (#3129 Greptile P1).
263
+ if (args.author !== null) {
264
+ const resolved = resolveAuthorFilter(args.author, options.resolveAuthenticatedLogin ?? undefined);
265
+ if (resolved.error !== undefined || resolved.filter === undefined) {
266
+ process.stderr.write(`triage:queue: ${resolved.error ?? "argument --author: expected a non-empty login (or @me)"}\n`);
267
+ return 2;
268
+ }
269
+ authorAllow = resolved.filter;
270
+ }
225
271
  ensureTriageCacheHydrated(projectRoot, { repo });
226
272
  const cachedForQueue = loadCachedIssues(repo, { projectRoot });
227
- const issuesForQueue = args.reconcile
273
+ let issuesForQueue = args.reconcile
228
274
  ? reconcileLiveOpenState(cachedForQueue, repo, options.liveOpenReader)
229
275
  : cachedForQueue;
276
+ if (authorAllow !== undefined) {
277
+ const partition = partitionByAuthorFilter(issuesForQueue, (row) => row.author ?? null, authorAllow);
278
+ issuesForQueue = [...partition.matched];
279
+ authorFilterLine = formatAuthorFilterLine(authorAllow, {
280
+ unknownCount: partition.unknownCount,
281
+ });
282
+ }
230
283
  const issuesWithClosed = loadCachedIssues(repo, { projectRoot, includeClosed: true });
231
284
  const issuesByNumber = new Map(issuesWithClosed.map((row) => [row.number, row]));
232
285
  const auditEntries = readAuditEntries(repo, {
@@ -256,6 +309,7 @@ function runQueue(args, options = {}) {
256
309
  repo,
257
310
  limit,
258
311
  rankingLabels,
312
+ authorFilterLine,
259
313
  })}\n`);
260
314
  return 0;
261
315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
4
4
  "description": "Directive CLI — npm install path for the Deft Directive framework.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://deftai.github.io/directive/",
@@ -34,8 +34,8 @@
34
34
  "provenance": true
35
35
  },
36
36
  "dependencies": {
37
- "@deftai/directive-core": "^0.95.0",
38
- "@deftai/directive-content": "^0.95.0"
37
+ "@deftai/directive-core": "^0.96.0",
38
+ "@deftai/directive-content": "^0.96.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b"