issue-flow 0.8.0 → 0.10.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 (85) hide show
  1. package/README.md +16 -0
  2. package/dist/{analyze-RBNKXWL5.js → analyze-UZYJAWAP.js} +6 -6
  3. package/dist/{chunk-J4SICTAN.js → chunk-2LNQL4XH.js} +53 -9
  4. package/dist/chunk-2LNQL4XH.js.map +1 -0
  5. package/dist/{chunk-A4M6AKIK.js → chunk-2Z7OEZZT.js} +6 -6
  6. package/dist/{chunk-F52S6ZGF.js → chunk-652DJBCN.js} +17 -3
  7. package/dist/chunk-652DJBCN.js.map +1 -0
  8. package/dist/{chunk-TGP6WK65.js → chunk-67ZO5ROQ.js} +75 -11
  9. package/dist/chunk-67ZO5ROQ.js.map +1 -0
  10. package/dist/{chunk-VSF2CR3G.js → chunk-EWA3OGCM.js} +6 -6
  11. package/dist/{chunk-VPDWTZ3F.js → chunk-EZMOEBLU.js} +2 -2
  12. package/dist/{chunk-JWZKV3CL.js → chunk-MXTGCD5C.js} +2 -2
  13. package/dist/{chunk-QO2MWMAZ.js → chunk-PPLIXZI5.js} +33 -10
  14. package/dist/chunk-PPLIXZI5.js.map +1 -0
  15. package/dist/{chunk-HSGTZ3CT.js → chunk-S67Q4I2O.js} +219 -3
  16. package/dist/chunk-S67Q4I2O.js.map +1 -0
  17. package/dist/chunk-SP7DD4ID.js +265 -0
  18. package/dist/chunk-SP7DD4ID.js.map +1 -0
  19. package/dist/{chunk-36ECROTC.js → chunk-UH4KAJBJ.js} +7 -7
  20. package/dist/{chunk-RQW5W4XC.js → chunk-UUY4PCWW.js} +2 -2
  21. package/dist/{chunk-BH7DYADV.js → chunk-V5OHFSW4.js} +319 -181
  22. package/dist/chunk-V5OHFSW4.js.map +1 -0
  23. package/dist/{chunk-J3PJBZXJ.js → chunk-VOKNTBOO.js} +3 -3
  24. package/dist/{chunk-BASRRXAM.js → chunk-VYFKOUUR.js} +222 -6
  25. package/dist/chunk-VYFKOUUR.js.map +1 -0
  26. package/dist/cli.js +98 -29
  27. package/dist/cli.js.map +1 -1
  28. package/dist/{config-R7O5YC65.js → config-YCF3HIKR.js} +7 -3
  29. package/dist/execute-QYXA225W.js +14 -0
  30. package/dist/{generate-TGJQLGMV.js → generate-TW6QKSW5.js} +6 -6
  31. package/dist/init-G57UXKDG.js +9 -0
  32. package/dist/plan-YJLGBYBQ.js +16 -0
  33. package/dist/policy-NP4ZAVF6.js +1013 -0
  34. package/dist/policy-NP4ZAVF6.js.map +1 -0
  35. package/dist/pr-V5H65MRW.js +20 -0
  36. package/dist/pr-review-RCMXKXVP.js +16 -0
  37. package/dist/prd-3XR5KYJS.js +16 -0
  38. package/dist/review-ZZG3CSUO.js +14 -0
  39. package/dist/run-DIBC3YKX.js +1860 -0
  40. package/dist/run-DIBC3YKX.js.map +1 -0
  41. package/dist/{web-JVPK2PIF.js → web-O2FVOYZW.js} +4 -4
  42. package/package.json +1 -1
  43. package/prompts/execute.md +2 -2
  44. package/prompts/plan.md +10 -1
  45. package/prompts/pr.md +9 -4
  46. package/web/CLAUDE.md +22 -8
  47. package/web/public/app.css +178 -0
  48. package/web/public/app.js +336 -17
  49. package/web/public/index.html +24 -0
  50. package/dist/chunk-BASRRXAM.js.map +0 -1
  51. package/dist/chunk-BH7DYADV.js.map +0 -1
  52. package/dist/chunk-F52S6ZGF.js.map +0 -1
  53. package/dist/chunk-HSGTZ3CT.js.map +0 -1
  54. package/dist/chunk-J4SICTAN.js.map +0 -1
  55. package/dist/chunk-MK43N4X3.js +0 -123
  56. package/dist/chunk-MK43N4X3.js.map +0 -1
  57. package/dist/chunk-QO2MWMAZ.js.map +0 -1
  58. package/dist/chunk-TGP6WK65.js.map +0 -1
  59. package/dist/execute-LU3HWA54.js +0 -14
  60. package/dist/init-WP7Z2LFQ.js +0 -9
  61. package/dist/plan-2MWMCKRY.js +0 -16
  62. package/dist/pr-XVQDEQ7B.js +0 -16
  63. package/dist/pr-review-VO6ZXAFM.js +0 -16
  64. package/dist/prd-FPZAHQEO.js +0 -16
  65. package/dist/review-AHALLMXO.js +0 -14
  66. package/dist/run-2CXVR3VZ.js +0 -733
  67. package/dist/run-2CXVR3VZ.js.map +0 -1
  68. /package/dist/{analyze-RBNKXWL5.js.map → analyze-UZYJAWAP.js.map} +0 -0
  69. /package/dist/{chunk-A4M6AKIK.js.map → chunk-2Z7OEZZT.js.map} +0 -0
  70. /package/dist/{chunk-VSF2CR3G.js.map → chunk-EWA3OGCM.js.map} +0 -0
  71. /package/dist/{chunk-VPDWTZ3F.js.map → chunk-EZMOEBLU.js.map} +0 -0
  72. /package/dist/{chunk-JWZKV3CL.js.map → chunk-MXTGCD5C.js.map} +0 -0
  73. /package/dist/{chunk-36ECROTC.js.map → chunk-UH4KAJBJ.js.map} +0 -0
  74. /package/dist/{chunk-RQW5W4XC.js.map → chunk-UUY4PCWW.js.map} +0 -0
  75. /package/dist/{chunk-J3PJBZXJ.js.map → chunk-VOKNTBOO.js.map} +0 -0
  76. /package/dist/{config-R7O5YC65.js.map → config-YCF3HIKR.js.map} +0 -0
  77. /package/dist/{execute-LU3HWA54.js.map → execute-QYXA225W.js.map} +0 -0
  78. /package/dist/{generate-TGJQLGMV.js.map → generate-TW6QKSW5.js.map} +0 -0
  79. /package/dist/{init-WP7Z2LFQ.js.map → init-G57UXKDG.js.map} +0 -0
  80. /package/dist/{plan-2MWMCKRY.js.map → plan-YJLGBYBQ.js.map} +0 -0
  81. /package/dist/{pr-XVQDEQ7B.js.map → pr-V5H65MRW.js.map} +0 -0
  82. /package/dist/{pr-review-VO6ZXAFM.js.map → pr-review-RCMXKXVP.js.map} +0 -0
  83. /package/dist/{prd-FPZAHQEO.js.map → prd-3XR5KYJS.js.map} +0 -0
  84. /package/dist/{review-AHALLMXO.js.map → review-ZZG3CSUO.js.map} +0 -0
  85. /package/dist/{web-JVPK2PIF.js.map → web-O2FVOYZW.js.map} +0 -0
@@ -0,0 +1,1013 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getProjectRoot,
4
+ loadPolicyConfig,
5
+ mergeConfigLayers,
6
+ run
7
+ } from "./chunk-VYFKOUUR.js";
8
+ import {
9
+ printError,
10
+ printInfo,
11
+ printWarning
12
+ } from "./chunk-V5OHFSW4.js";
13
+
14
+ // src/policy/discovery.ts
15
+ import { readdir, readFile, stat } from "fs/promises";
16
+ import { join, relative, sep } from "path";
17
+
18
+ // src/policy/parsers/docs.ts
19
+ import { posix } from "path";
20
+ function classifyDocument(fileName) {
21
+ switch (fileName.toLowerCase()) {
22
+ case "agents.md":
23
+ return "agents";
24
+ case "claude.md":
25
+ return "claude";
26
+ case "contributing.md":
27
+ return "contributing";
28
+ case "code_of_conduct.md":
29
+ case "code-of-conduct.md":
30
+ return "code-of-conduct";
31
+ default:
32
+ return null;
33
+ }
34
+ }
35
+ var MAX_REFERENCED_DOCUMENTS = 20;
36
+ var MARKDOWN_LINK = /\[[^\]\n]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
37
+ function extractReferencedDocuments(content, fromPath) {
38
+ const fromDir = posix.dirname(fromPath.replace(/\\/g, "/"));
39
+ const found = [];
40
+ for (const match of content.matchAll(MARKDOWN_LINK)) {
41
+ const raw = match[1];
42
+ if (raw === void 0) continue;
43
+ const target = raw.split("#")[0]?.split("?")[0]?.trim() ?? "";
44
+ if (target === "") continue;
45
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target) || target.startsWith("//")) continue;
46
+ if (!/\.(md|markdown)$/i.test(target)) continue;
47
+ const resolved = posix.normalize(
48
+ target.startsWith("/") ? target.slice(1) : fromDir === "." ? target : posix.join(fromDir, target)
49
+ );
50
+ if (resolved.startsWith("..") || resolved === "" || resolved === ".") continue;
51
+ if (resolved === fromPath || found.includes(resolved)) continue;
52
+ found.push(resolved);
53
+ if (found.length >= MAX_REFERENCED_DOCUMENTS) break;
54
+ }
55
+ return found;
56
+ }
57
+
58
+ // src/policy/parsers/labels.ts
59
+ function parseLabels(payload) {
60
+ if (!Array.isArray(payload)) return [];
61
+ const labels = [];
62
+ for (const entry of payload) {
63
+ if (entry === null || typeof entry !== "object") continue;
64
+ const record = entry;
65
+ const name = typeof record.name === "string" ? record.name.trim() : "";
66
+ if (name === "") continue;
67
+ const description = typeof record.description === "string" ? record.description.trim() : "";
68
+ const color = typeof record.color === "string" ? record.color.trim() : "";
69
+ labels.push({
70
+ name,
71
+ description: description === "" ? null : description,
72
+ color: color === "" ? null : color
73
+ });
74
+ }
75
+ return labels;
76
+ }
77
+ function parseIssueTypes(payload) {
78
+ const entries = Array.isArray(payload) ? payload : Array.isArray(payload?.issue_types) ? payload.issue_types : [];
79
+ const types = [];
80
+ for (const entry of entries) {
81
+ const name = typeof entry === "string" ? entry : entry !== null && typeof entry === "object" ? entry.name : void 0;
82
+ if (typeof name !== "string") continue;
83
+ const trimmed = name.trim();
84
+ if (trimmed !== "" && !types.includes(trimmed)) {
85
+ types.push(trimmed);
86
+ }
87
+ }
88
+ return types;
89
+ }
90
+
91
+ // src/policy/parsers/templates.ts
92
+ import { basename, extname } from "path";
93
+
94
+ // src/policy/parsers/issue-forms.ts
95
+ function emptyTemplateMetadata() {
96
+ return { name: null, about: null, title: null, labels: [], type: null, assignees: [] };
97
+ }
98
+ var TOP_LEVEL_KEY = /^([A-Za-z_][A-Za-z0-9_-]*):(?:\s+(.*))?$/;
99
+ function topLevelRegions(source) {
100
+ const regions = /* @__PURE__ */ new Map();
101
+ const lines = source.split(/\r?\n/);
102
+ let current = null;
103
+ for (const line of lines) {
104
+ const match = /^[^\s#-]/.test(line) ? TOP_LEVEL_KEY.exec(line) : null;
105
+ if (match?.[1] !== void 0) {
106
+ current = { key: match[1], inline: (match[2] ?? "").trim(), block: [] };
107
+ if (!regions.has(current.key)) {
108
+ regions.set(current.key, current);
109
+ }
110
+ continue;
111
+ }
112
+ current?.block.push(line);
113
+ }
114
+ return regions;
115
+ }
116
+ function stripComment(value) {
117
+ let quote = null;
118
+ for (let i = 0; i < value.length; i++) {
119
+ const char = value[i];
120
+ if (quote !== null) {
121
+ if (char === quote) quote = null;
122
+ continue;
123
+ }
124
+ if (char === '"' || char === "'") {
125
+ quote = char;
126
+ continue;
127
+ }
128
+ if (char === "#" && (i === 0 || /\s/.test(value[i - 1] ?? ""))) {
129
+ return value.slice(0, i);
130
+ }
131
+ }
132
+ return value;
133
+ }
134
+ function unquote(value) {
135
+ const trimmed = value.trim();
136
+ if (trimmed.length >= 2) {
137
+ const first = trimmed[0];
138
+ const last = trimmed[trimmed.length - 1];
139
+ if (first === '"' && last === '"') {
140
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
141
+ }
142
+ if (first === "'" && last === "'") {
143
+ return trimmed.slice(1, -1).replace(/''/g, "'");
144
+ }
145
+ }
146
+ return trimmed;
147
+ }
148
+ function splitFlow(inner) {
149
+ const parts = [];
150
+ let buffer = "";
151
+ let quote = null;
152
+ for (const char of inner) {
153
+ if (quote !== null) {
154
+ if (char === quote) quote = null;
155
+ buffer += char;
156
+ continue;
157
+ }
158
+ if (char === '"' || char === "'") {
159
+ quote = char;
160
+ buffer += char;
161
+ continue;
162
+ }
163
+ if (char === ",") {
164
+ parts.push(buffer);
165
+ buffer = "";
166
+ continue;
167
+ }
168
+ buffer += char;
169
+ }
170
+ parts.push(buffer);
171
+ return parts.map(unquote).filter((part) => part !== "");
172
+ }
173
+ function readScalar(region) {
174
+ if (region === void 0) return null;
175
+ const inline = stripComment(region.inline).trim();
176
+ if (inline !== "" && inline !== "|" && inline !== ">" && inline !== "|-" && inline !== ">-") {
177
+ const value = unquote(inline);
178
+ return value === "" ? null : value;
179
+ }
180
+ const collected = region.block.filter((line) => line.trim() !== "").map((line) => line.trim()).join(inline.startsWith(">") ? " " : "\n").trim();
181
+ return collected === "" ? null : collected;
182
+ }
183
+ function readSequence(region) {
184
+ if (region === void 0) return [];
185
+ const inline = stripComment(region.inline).trim();
186
+ if (inline.startsWith("[")) {
187
+ const closing = inline.lastIndexOf("]");
188
+ return splitFlow(inline.slice(1, closing === -1 ? void 0 : closing));
189
+ }
190
+ if (inline !== "") {
191
+ const single = unquote(inline);
192
+ return single === "" ? [] : [single];
193
+ }
194
+ const items = [];
195
+ for (const line of region.block) {
196
+ const trimmed = line.trim();
197
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
198
+ if (!trimmed.startsWith("- ") && trimmed !== "-") break;
199
+ const value = unquote(stripComment(trimmed.slice(1)).trim());
200
+ if (value === "" || /^[A-Za-z_][A-Za-z0-9_-]*:\s/.test(value)) continue;
201
+ items.push(value);
202
+ }
203
+ return items;
204
+ }
205
+ function parseTemplateMetadata(source) {
206
+ const regions = topLevelRegions(source);
207
+ return {
208
+ name: readScalar(regions.get("name")),
209
+ // Issue Forms spell it `description`, legacy markdown templates `about`.
210
+ about: readScalar(regions.get("about")) ?? readScalar(regions.get("description")),
211
+ title: readScalar(regions.get("title")),
212
+ labels: readSequence(regions.get("labels")),
213
+ type: readScalar(regions.get("type")),
214
+ assignees: readSequence(regions.get("assignees"))
215
+ };
216
+ }
217
+ function extractFrontMatter(source) {
218
+ const normalized = source.replace(/^\uFEFF/, "");
219
+ if (!/^---[ \t]*\r?\n/.test(normalized)) return null;
220
+ const rest = normalized.slice(normalized.indexOf("\n") + 1);
221
+ const closing = /^---[ \t]*$/m.exec(rest);
222
+ if (closing?.index === void 0) return null;
223
+ return rest.slice(0, closing.index);
224
+ }
225
+
226
+ // src/policy/parsers/templates.ts
227
+ function parseIssueTemplateFile(relPath, content) {
228
+ const extension = extname(relPath).toLowerCase();
229
+ const isForm = extension === ".yml" || extension === ".yaml";
230
+ const metadataSource = isForm ? content : extractFrontMatter(content);
231
+ const metadata = metadataSource === null ? emptyTemplateMetadata() : parseTemplateMetadata(metadataSource);
232
+ return {
233
+ path: relPath,
234
+ format: isForm ? "form" : "markdown",
235
+ origin: "filesystem",
236
+ // A template with no `name:` is still addressable by its file name, which
237
+ // is what GitHub falls back to for legacy markdown templates.
238
+ name: metadata.name ?? basename(relPath, extension),
239
+ about: metadata.about,
240
+ title: metadata.title,
241
+ labels: metadata.labels,
242
+ type: metadata.type,
243
+ assignees: metadata.assignees,
244
+ content
245
+ };
246
+ }
247
+ function asString(value) {
248
+ return typeof value === "string" && value.trim() !== "" ? value : null;
249
+ }
250
+ function isRecord(value) {
251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
252
+ }
253
+ function asNameList(value, key) {
254
+ const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.nodes) ? value.nodes : [];
255
+ return entries.map(
256
+ (entry) => typeof entry === "string" ? entry : asString(isRecord(entry) ? entry[key] : null)
257
+ ).filter((entry) => entry !== null && entry !== "");
258
+ }
259
+ function parseOrganizationTemplates(payload) {
260
+ const repository = isRecord(payload) && isRecord(payload.data) ? payload.data.repository : null;
261
+ const entries = isRecord(repository) ? repository.issueTemplates : null;
262
+ if (!Array.isArray(entries)) return [];
263
+ const templates = [];
264
+ for (const entry of entries) {
265
+ if (!isRecord(entry)) continue;
266
+ const name = asString(entry.name);
267
+ if (name === null) continue;
268
+ templates.push({
269
+ // The filename is the stable identity; the display name is not unique.
270
+ path: asString(entry.filename) ?? name,
271
+ format: "markdown",
272
+ origin: "organization",
273
+ name,
274
+ about: asString(entry.about),
275
+ title: asString(entry.title),
276
+ labels: asNameList(entry.labels, "name"),
277
+ // GraphQL exposes no Issue Type on a template, so it stays unknown here
278
+ // rather than being guessed from the name.
279
+ type: null,
280
+ assignees: asNameList(entry.assignees, "login"),
281
+ content: asString(entry.body) ?? ""
282
+ });
283
+ }
284
+ return templates;
285
+ }
286
+
287
+ // src/policy/types.ts
288
+ var POLICY_SCHEMA_VERSION = 1;
289
+ var MAX_POLICY_DOCUMENT_BYTES = 64 * 1024;
290
+
291
+ // src/policy/discovery.ts
292
+ var DISCOVERY_TIMEOUT_MS = 1e4;
293
+ var POLICY_DIRECTORIES = [".github", "", "docs"];
294
+ var defaultExec = (command, args, options) => run(command, args, { cwd: options.cwd, timeout: options.timeout });
295
+ function toRelativePath(root, absolute) {
296
+ return relative(root, absolute).split(sep).join("/");
297
+ }
298
+ async function listDirectory(dir) {
299
+ try {
300
+ const entries = await readdir(dir, { withFileTypes: true });
301
+ return entries.filter((entry) => !entry.isDirectory()).map((entry) => entry.name);
302
+ } catch {
303
+ return [];
304
+ }
305
+ }
306
+ async function isDirectory(path) {
307
+ try {
308
+ return (await stat(path)).isDirectory();
309
+ } catch {
310
+ return false;
311
+ }
312
+ }
313
+ async function readCapped(path) {
314
+ let raw;
315
+ try {
316
+ raw = await readFile(path, "utf-8");
317
+ } catch {
318
+ return null;
319
+ }
320
+ if (Buffer.byteLength(raw, "utf-8") <= MAX_POLICY_DOCUMENT_BYTES) {
321
+ return { content: raw, truncated: false };
322
+ }
323
+ const clipped = Buffer.from(raw, "utf-8").subarray(0, MAX_POLICY_DOCUMENT_BYTES).toString("utf-8");
324
+ return { content: clipped, truncated: true };
325
+ }
326
+ async function findFile(dir, names) {
327
+ const entries = await listDirectory(dir);
328
+ const wanted = names.map((name) => name.toLowerCase());
329
+ for (const name of wanted) {
330
+ const hit = entries.find((entry) => entry.toLowerCase() === name);
331
+ if (hit !== void 0) return hit;
332
+ }
333
+ return null;
334
+ }
335
+ var TEMPLATE_EXTENSIONS = [".yml", ".yaml", ".md", ".markdown"];
336
+ function isTemplateFile(name) {
337
+ const lower = name.toLowerCase();
338
+ if (lower === "config.yml" || lower === "config.yaml") return false;
339
+ return TEMPLATE_EXTENSIONS.some((extension) => lower.endsWith(extension));
340
+ }
341
+ async function discoverIssueTemplates(root) {
342
+ const templates = [];
343
+ const sources = [];
344
+ for (const base of POLICY_DIRECTORIES) {
345
+ const baseDir = base === "" ? root : join(root, base);
346
+ const templateDir = await findDirectory(baseDir, "ISSUE_TEMPLATE") ?? null;
347
+ if (templateDir !== null) {
348
+ const names = (await listDirectory(templateDir)).filter(isTemplateFile).sort();
349
+ for (const name of names) {
350
+ const filePath = join(templateDir, name);
351
+ const file = await readCapped(filePath);
352
+ if (file === null) continue;
353
+ const relPath = toRelativePath(root, filePath);
354
+ templates.push(parseIssueTemplateFile(relPath, file.content));
355
+ sources.push({
356
+ kind: "issue-templates",
357
+ origin: "filesystem",
358
+ path: relPath,
359
+ status: "found",
360
+ detail: file.truncated ? "content truncated" : null
361
+ });
362
+ }
363
+ }
364
+ const singleFile = await findFile(baseDir, [
365
+ "ISSUE_TEMPLATE.md",
366
+ "ISSUE_TEMPLATE.yml",
367
+ "ISSUE_TEMPLATE.yaml",
368
+ "ISSUE_TEMPLATE.markdown"
369
+ ]);
370
+ if (singleFile !== null) {
371
+ const filePath = join(baseDir, singleFile);
372
+ const file = await readCapped(filePath);
373
+ if (file !== null) {
374
+ const relPath = toRelativePath(root, filePath);
375
+ templates.push(parseIssueTemplateFile(relPath, file.content));
376
+ sources.push({
377
+ kind: "issue-templates",
378
+ origin: "filesystem",
379
+ path: relPath,
380
+ status: "found",
381
+ detail: file.truncated ? "content truncated" : null
382
+ });
383
+ }
384
+ }
385
+ }
386
+ return { templates, sources };
387
+ }
388
+ async function findDirectory(parent, name) {
389
+ let entries;
390
+ try {
391
+ entries = (await readdir(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
392
+ } catch {
393
+ return null;
394
+ }
395
+ const hit = entries.find((entry) => entry.toLowerCase() === name.toLowerCase());
396
+ return hit === void 0 ? null : join(parent, hit);
397
+ }
398
+ var PR_TEMPLATE_FILES = [
399
+ "PULL_REQUEST_TEMPLATE.md",
400
+ "PULL_REQUEST_TEMPLATE.markdown",
401
+ "PULL_REQUEST_TEMPLATE.txt",
402
+ "PULL_REQUEST_TEMPLATE"
403
+ ];
404
+ async function discoverPullRequestTemplates(root) {
405
+ const templates = [];
406
+ const sources = [];
407
+ const collect = async (filePath) => {
408
+ const file = await readCapped(filePath);
409
+ if (file === null) return;
410
+ const relPath = toRelativePath(root, filePath);
411
+ templates.push({
412
+ path: relPath,
413
+ name: relPath.split("/").pop() ?? relPath,
414
+ content: file.content
415
+ });
416
+ sources.push({
417
+ kind: "pull-request-template",
418
+ origin: "filesystem",
419
+ path: relPath,
420
+ status: "found",
421
+ detail: file.truncated ? "content truncated" : null
422
+ });
423
+ };
424
+ for (const base of POLICY_DIRECTORIES) {
425
+ const baseDir = base === "" ? root : join(root, base);
426
+ const single = await findFile(baseDir, PR_TEMPLATE_FILES);
427
+ if (single !== null && !await isDirectory(join(baseDir, single))) {
428
+ await collect(join(baseDir, single));
429
+ }
430
+ const templateDir = await findDirectory(baseDir, "PULL_REQUEST_TEMPLATE");
431
+ if (templateDir !== null) {
432
+ for (const name of (await listDirectory(templateDir)).sort()) {
433
+ if (!/\.(md|markdown|txt)$/i.test(name)) continue;
434
+ await collect(join(templateDir, name));
435
+ }
436
+ }
437
+ }
438
+ return { templates, sources };
439
+ }
440
+ async function discoverCodeowners(root) {
441
+ for (const base of POLICY_DIRECTORIES) {
442
+ const baseDir = base === "" ? root : join(root, base);
443
+ const name = await findFile(baseDir, ["CODEOWNERS"]);
444
+ if (name === null) continue;
445
+ const file = await readCapped(join(baseDir, name));
446
+ if (file === null) continue;
447
+ const relPath = toRelativePath(root, join(baseDir, name));
448
+ return {
449
+ content: file.content,
450
+ sources: [
451
+ {
452
+ kind: "codeowners",
453
+ origin: "filesystem",
454
+ path: relPath,
455
+ status: "found",
456
+ detail: file.truncated ? "content truncated" : null
457
+ }
458
+ ]
459
+ };
460
+ }
461
+ return { content: null, sources: [] };
462
+ }
463
+ var ROOT_DOCUMENT_FILES = ["CONTRIBUTING.md", "CODE_OF_CONDUCT.md"];
464
+ var SCOPED_DOCUMENT_FILES = ["AGENTS.md", "CLAUDE.md"];
465
+ function scopeLadder(scope) {
466
+ if (scope === null || scope === "") return [""];
467
+ const segments = scope.split("/").filter((segment) => segment !== "");
468
+ const ladder = [""];
469
+ let current = "";
470
+ for (const segment of segments) {
471
+ current = current === "" ? segment : `${current}/${segment}`;
472
+ ladder.push(current);
473
+ }
474
+ return ladder;
475
+ }
476
+ async function discoverDocuments(root, scope) {
477
+ const documents = [];
478
+ const sources = [];
479
+ const seen = /* @__PURE__ */ new Set();
480
+ const collect = async (filePath, kind, docScope, referencedFrom) => {
481
+ const relPath = toRelativePath(root, filePath);
482
+ if (seen.has(relPath)) return null;
483
+ const file = await readCapped(filePath);
484
+ if (file === null) return null;
485
+ seen.add(relPath);
486
+ const document = {
487
+ path: relPath,
488
+ kind,
489
+ scope: docScope,
490
+ referencedFrom,
491
+ content: file.content
492
+ };
493
+ documents.push(document);
494
+ sources.push({
495
+ kind: "docs",
496
+ origin: "filesystem",
497
+ path: relPath,
498
+ status: "found",
499
+ detail: file.truncated ? "content truncated" : null
500
+ });
501
+ return document;
502
+ };
503
+ for (const base of POLICY_DIRECTORIES) {
504
+ const baseDir = base === "" ? root : join(root, base);
505
+ for (const wanted of ROOT_DOCUMENT_FILES) {
506
+ const name = await findFile(baseDir, [wanted]);
507
+ if (name === null) continue;
508
+ const kind = classifyDocument(name);
509
+ if (kind === null) continue;
510
+ await collect(join(baseDir, name), kind, "", null);
511
+ }
512
+ }
513
+ const indexes = [];
514
+ for (const level of scopeLadder(scope)) {
515
+ const levelDir = level === "" ? root : join(root, level);
516
+ for (const wanted of SCOPED_DOCUMENT_FILES) {
517
+ const name = await findFile(levelDir, [wanted]);
518
+ if (name === null) continue;
519
+ const kind = classifyDocument(name);
520
+ if (kind === null) continue;
521
+ const document = await collect(join(levelDir, name), kind, level, null);
522
+ if (document !== null && kind === "agents") {
523
+ indexes.push(document);
524
+ }
525
+ }
526
+ }
527
+ for (const index of indexes) {
528
+ for (const target of extractReferencedDocuments(index.content, index.path)) {
529
+ await collect(join(root, target), "referenced", index.scope, index.path);
530
+ }
531
+ }
532
+ return { documents, sources };
533
+ }
534
+ async function discoverBaseBranch(root, exec = defaultExec) {
535
+ const options = { cwd: root, timeout: DISCOVERY_TIMEOUT_MS };
536
+ try {
537
+ const remoteHead = await exec(
538
+ "git",
539
+ ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
540
+ options
541
+ );
542
+ if (remoteHead.exitCode === 0) {
543
+ const name = remoteHead.stdout.trim().replace(/^origin\//, "");
544
+ if (name !== "") {
545
+ return {
546
+ baseBranch: name,
547
+ sources: [
548
+ {
549
+ kind: "base-branch",
550
+ origin: "git",
551
+ path: null,
552
+ status: "found",
553
+ detail: "origin/HEAD"
554
+ }
555
+ ]
556
+ };
557
+ }
558
+ }
559
+ for (const candidate of ["main", "master"]) {
560
+ const check = await exec(
561
+ "git",
562
+ ["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`],
563
+ options
564
+ );
565
+ if (check.exitCode === 0) {
566
+ return {
567
+ baseBranch: candidate,
568
+ sources: [
569
+ {
570
+ kind: "base-branch",
571
+ origin: "git",
572
+ path: null,
573
+ status: "found",
574
+ detail: `refs/heads/${candidate}`
575
+ }
576
+ ]
577
+ };
578
+ }
579
+ }
580
+ } catch {
581
+ return {
582
+ baseBranch: null,
583
+ sources: [
584
+ {
585
+ kind: "base-branch",
586
+ origin: "git",
587
+ path: null,
588
+ status: "unavailable",
589
+ detail: "git could not be executed"
590
+ }
591
+ ]
592
+ };
593
+ }
594
+ return { baseBranch: null, sources: [] };
595
+ }
596
+ async function discoverGitHubSlug(root, exec = defaultExec) {
597
+ let url = "";
598
+ try {
599
+ const result = await exec("git", ["remote", "get-url", "origin"], {
600
+ cwd: root,
601
+ timeout: DISCOVERY_TIMEOUT_MS
602
+ });
603
+ if (result.exitCode !== 0) return null;
604
+ url = result.stdout.trim();
605
+ } catch {
606
+ return null;
607
+ }
608
+ if (url === "") return null;
609
+ const match = /[/:]([^/:\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url);
610
+ if (match?.[1] === void 0 || match[2] === void 0) return null;
611
+ return { owner: match[1], repo: match[2] };
612
+ }
613
+ async function ghJson(root, args, exec) {
614
+ let result;
615
+ try {
616
+ result = await exec("gh", args, { cwd: root, timeout: DISCOVERY_TIMEOUT_MS });
617
+ } catch (err) {
618
+ return { error: err instanceof Error ? err.message : String(err) };
619
+ }
620
+ if (result.exitCode !== 0) {
621
+ const stderr = result.stderr.trim().split("\n")[0] ?? "";
622
+ return { error: stderr === "" ? `gh exited with ${result.exitCode}` : stderr };
623
+ }
624
+ try {
625
+ return { value: JSON.parse(result.stdout) };
626
+ } catch {
627
+ return { error: "gh returned unparseable JSON" };
628
+ }
629
+ }
630
+ var LABEL_PAGE_SIZE = 200;
631
+ async function discoverLabels(root, exec = defaultExec) {
632
+ const outcome = await ghJson(
633
+ root,
634
+ ["label", "list", "--json", "name,description,color", "--limit", String(LABEL_PAGE_SIZE)],
635
+ exec
636
+ );
637
+ if ("error" in outcome) {
638
+ return {
639
+ labels: [],
640
+ sources: [
641
+ { kind: "labels", origin: "gh", path: null, status: "unavailable", detail: outcome.error }
642
+ ]
643
+ };
644
+ }
645
+ const labels = parseLabels(outcome.value);
646
+ return {
647
+ labels,
648
+ sources: [
649
+ {
650
+ kind: "labels",
651
+ origin: "gh",
652
+ path: null,
653
+ status: "found",
654
+ detail: `${labels.length} label(s)`
655
+ }
656
+ ]
657
+ };
658
+ }
659
+ async function discoverIssueTypes(root, owner, exec = defaultExec) {
660
+ if (owner === null || owner === "") {
661
+ return { types: [], sources: [] };
662
+ }
663
+ const outcome = await ghJson(root, ["api", `orgs/${owner}/issue-types`], exec);
664
+ if ("error" in outcome) {
665
+ return {
666
+ types: [],
667
+ sources: [
668
+ {
669
+ kind: "issue-types",
670
+ origin: "gh",
671
+ path: null,
672
+ // A personal account (404) and a plan without Issue Types (403) are
673
+ // the common answers here, so the detail carries what gh said.
674
+ status: "unavailable",
675
+ detail: outcome.error
676
+ }
677
+ ]
678
+ };
679
+ }
680
+ const types = parseIssueTypes(outcome.value);
681
+ return {
682
+ types,
683
+ sources: [
684
+ {
685
+ kind: "issue-types",
686
+ origin: "gh",
687
+ path: null,
688
+ status: "found",
689
+ detail: `${types.length} type(s)`
690
+ }
691
+ ]
692
+ };
693
+ }
694
+ var ORGANIZATION_TEMPLATES_QUERY = `query($owner:String!,$name:String!){
695
+ repository(owner:$owner,name:$name){
696
+ issueTemplates{
697
+ name
698
+ title
699
+ about
700
+ filename
701
+ body
702
+ assignees(first:10){nodes{login}}
703
+ labels(first:20){nodes{name}}
704
+ }
705
+ }
706
+ }`;
707
+ async function discoverOrganizationTemplates(root, slug, exec = defaultExec) {
708
+ if (slug === null) {
709
+ return { templates: [], sources: [] };
710
+ }
711
+ const outcome = await ghJson(
712
+ root,
713
+ [
714
+ "api",
715
+ "graphql",
716
+ "-f",
717
+ `query=${ORGANIZATION_TEMPLATES_QUERY}`,
718
+ "-f",
719
+ `owner=${slug.owner}`,
720
+ "-f",
721
+ `name=${slug.repo}`
722
+ ],
723
+ exec
724
+ );
725
+ if ("error" in outcome) {
726
+ return {
727
+ templates: [],
728
+ sources: [
729
+ {
730
+ kind: "issue-templates",
731
+ origin: "gh",
732
+ path: null,
733
+ status: "unavailable",
734
+ detail: outcome.error
735
+ }
736
+ ]
737
+ };
738
+ }
739
+ const templates = parseOrganizationTemplates(outcome.value);
740
+ if (templates.length === 0) {
741
+ return { templates, sources: [] };
742
+ }
743
+ return {
744
+ templates,
745
+ sources: templates.map((template) => ({
746
+ kind: "issue-templates",
747
+ origin: "gh",
748
+ path: null,
749
+ status: "found",
750
+ detail: `organization default "${template.name}"`
751
+ }))
752
+ };
753
+ }
754
+
755
+ // src/policy/resolve.ts
756
+ import { isAbsolute, normalize, sep as sep2 } from "path";
757
+ function normalizeScope(scope) {
758
+ if (scope === null || scope === void 0) return null;
759
+ const trimmed = scope.trim();
760
+ if (trimmed === "" || trimmed === "." || isAbsolute(trimmed)) return null;
761
+ const normalized = normalize(trimmed).split(sep2).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
762
+ if (normalized === "" || normalized === "." || normalized.startsWith("..")) return null;
763
+ return normalized;
764
+ }
765
+ function emptyPolicy(root, scope, enabled, sources) {
766
+ return {
767
+ schemaVersion: POLICY_SCHEMA_VERSION,
768
+ root,
769
+ scope,
770
+ enabled,
771
+ issues: { templates: [], types: [], labels: [], titleConvention: null },
772
+ pullRequests: { template: null, templates: [], baseBranch: null, titleConvention: null },
773
+ git: { branchConvention: null, commitConvention: null },
774
+ docs: [],
775
+ codeowners: null,
776
+ sources
777
+ };
778
+ }
779
+ function disabledSource(kind) {
780
+ return {
781
+ kind,
782
+ origin: "config",
783
+ path: null,
784
+ status: "disabled",
785
+ detail: "disabled by configuration"
786
+ };
787
+ }
788
+ var DECLARATION_KINDS = {
789
+ issues: "issue-templates",
790
+ pullRequests: "pull-request-template",
791
+ git: "base-branch"
792
+ };
793
+ async function resolvePolicy(root, scope, options) {
794
+ const warn = options.warn ?? printWarning;
795
+ const config = await loadPolicyConfig({
796
+ projectRoot: root,
797
+ env: options.env,
798
+ cli: options.cli,
799
+ warn
800
+ });
801
+ if (!config.enabled) {
802
+ return emptyPolicy(root, scope, false, [
803
+ { kind: "docs", origin: "config", path: null, status: "disabled", detail: "policy.enabled" }
804
+ ]);
805
+ }
806
+ const toggles = config.discovery;
807
+ const [templateDiscovery, prTemplates, documents, codeowners] = await Promise.all([
808
+ toggles.issueTemplates ? discoverIssueTemplates(root) : Promise.resolve({ templates: [], sources: [disabledSource("issue-templates")] }),
809
+ toggles.pullRequestTemplate ? discoverPullRequestTemplates(root) : Promise.resolve({ templates: [], sources: [disabledSource("pull-request-template")] }),
810
+ toggles.docs ? discoverDocuments(root, scope) : Promise.resolve({ documents: [], sources: [disabledSource("docs")] }),
811
+ toggles.codeowners ? discoverCodeowners(root) : Promise.resolve({ content: null, sources: [disabledSource("codeowners")] })
812
+ ]);
813
+ const wantsOrgTemplates = toggles.issueTemplates && templateDiscovery.templates.length === 0;
814
+ const [baseBranch, slug] = await Promise.all([
815
+ discoverBaseBranch(root, options.exec),
816
+ toggles.issueTypes || wantsOrgTemplates ? discoverGitHubSlug(root, options.exec) : Promise.resolve(null)
817
+ ]);
818
+ const [labels, issueTypes, orgTemplates] = await Promise.all([
819
+ toggles.labels ? discoverLabels(root, options.exec) : Promise.resolve({ labels: [], sources: [disabledSource("labels")] }),
820
+ toggles.issueTypes ? discoverIssueTypes(root, slug?.owner ?? null, options.exec) : Promise.resolve({ types: [], sources: [disabledSource("issue-types")] }),
821
+ wantsOrgTemplates ? discoverOrganizationTemplates(root, slug, options.exec) : Promise.resolve({ templates: [], sources: [] })
822
+ ]);
823
+ const sources = [
824
+ ...templateDiscovery.sources,
825
+ ...orgTemplates.sources,
826
+ ...issueTypes.sources,
827
+ ...labels.sources,
828
+ ...prTemplates.sources,
829
+ ...baseBranch.sources,
830
+ ...documents.sources,
831
+ ...codeowners.sources
832
+ ];
833
+ const discoveredPullRequests = {};
834
+ if (baseBranch.baseBranch !== null) {
835
+ discoveredPullRequests.baseBranch = baseBranch.baseBranch;
836
+ }
837
+ const issues = mergeConfigLayers({
838
+ defaults: { titleConvention: null },
839
+ project: config.issues
840
+ });
841
+ const pullRequests = mergeConfigLayers({
842
+ defaults: { baseBranch: null, titleConvention: null },
843
+ discovered: discoveredPullRequests,
844
+ project: config.pullRequests
845
+ });
846
+ const git = mergeConfigLayers({
847
+ defaults: { branchConvention: null, commitConvention: null },
848
+ project: config.git
849
+ });
850
+ for (const group of ["issues", "pullRequests", "git"]) {
851
+ for (const key of Object.keys(config[group])) {
852
+ sources.push({
853
+ kind: DECLARATION_KINDS[group],
854
+ origin: "config",
855
+ path: null,
856
+ status: "found",
857
+ detail: `${group}.${key} declared`
858
+ });
859
+ }
860
+ }
861
+ return {
862
+ schemaVersion: POLICY_SCHEMA_VERSION,
863
+ root,
864
+ scope,
865
+ enabled: true,
866
+ issues: {
867
+ templates: [...templateDiscovery.templates, ...orgTemplates.templates],
868
+ types: issueTypes.types,
869
+ labels: labels.labels,
870
+ titleConvention: issues.titleConvention ?? null
871
+ },
872
+ pullRequests: {
873
+ template: prTemplates.templates[0]?.content ?? null,
874
+ templates: prTemplates.templates,
875
+ baseBranch: pullRequests.baseBranch ?? null,
876
+ titleConvention: pullRequests.titleConvention ?? null
877
+ },
878
+ git: {
879
+ branchConvention: git.branchConvention ?? null,
880
+ commitConvention: git.commitConvention ?? null
881
+ },
882
+ docs: documents.documents,
883
+ codeowners: codeowners.content,
884
+ sources
885
+ };
886
+ }
887
+ var cache = /* @__PURE__ */ new Map();
888
+ async function loadRepositoryPolicy(options = {}) {
889
+ const root = options.root ?? await getProjectRoot();
890
+ const scope = normalizeScope(options.scope);
891
+ if (options.cache === false) {
892
+ return resolvePolicy(root, scope, options);
893
+ }
894
+ const key = `${root}\0${scope ?? ""}`;
895
+ const cached = cache.get(key);
896
+ if (cached !== void 0) {
897
+ return cached;
898
+ }
899
+ const pending = resolvePolicy(root, scope, options);
900
+ cache.set(key, pending);
901
+ try {
902
+ return await pending;
903
+ } catch (err) {
904
+ cache.delete(key);
905
+ throw err;
906
+ }
907
+ }
908
+
909
+ // src/commands/policy.ts
910
+ function groupSources(sources) {
911
+ const grouped = /* @__PURE__ */ new Map();
912
+ for (const source of sources) {
913
+ const bucket = grouped.get(source.kind);
914
+ if (bucket === void 0) {
915
+ grouped.set(source.kind, [source]);
916
+ } else {
917
+ bucket.push(source);
918
+ }
919
+ }
920
+ return grouped;
921
+ }
922
+ function describeSource(source) {
923
+ const where = source.path ?? source.origin;
924
+ const detail = source.detail === null ? "" : ` \u2014 ${source.detail}`;
925
+ return `${source.status === "found" ? "" : `[${source.status}] `}${where}${detail}`;
926
+ }
927
+ function render(policy) {
928
+ const lines = [];
929
+ const push = (line = "") => {
930
+ lines.push(line);
931
+ };
932
+ push(`Repository: ${policy.root}`);
933
+ push(`Scope: ${policy.scope ?? "(root)"}`);
934
+ if (!policy.enabled) {
935
+ push();
936
+ push("Policy discovery is disabled (policy.enabled = false).");
937
+ console.log(lines.join("\n"));
938
+ return;
939
+ }
940
+ push();
941
+ push("Issues");
942
+ push(` templates: ${policy.issues.templates.length}`);
943
+ for (const template of policy.issues.templates) {
944
+ const labels = template.labels.length > 0 ? ` labels=[${template.labels.join(", ")}]` : "";
945
+ const type = template.type === null ? "" : ` type=${template.type}`;
946
+ push(` - ${template.path} (${template.format}, ${template.origin})${type}${labels}`);
947
+ }
948
+ push(` types: ${policy.issues.types.length > 0 ? policy.issues.types.join(", ") : "(none)"}`);
949
+ push(` labels: ${policy.issues.labels.length}`);
950
+ for (const label of policy.issues.labels) {
951
+ push(` - ${label.name}${label.description === null ? "" : ` \u2014 ${label.description}`}`);
952
+ }
953
+ push(` titleConvention: ${policy.issues.titleConvention ?? "(none)"}`);
954
+ push();
955
+ push("Pull Requests");
956
+ push(` template: ${policy.pullRequests.templates[0]?.path ?? "(none)"}`);
957
+ for (const template of policy.pullRequests.templates.slice(1)) {
958
+ push(` - ${template.path}`);
959
+ }
960
+ push(` baseBranch: ${policy.pullRequests.baseBranch ?? "(none)"}`);
961
+ push(` titleConvention: ${policy.pullRequests.titleConvention ?? "(none)"}`);
962
+ push();
963
+ push("Git");
964
+ push(` branchConvention: ${policy.git.branchConvention ?? "(none)"}`);
965
+ push(` commitConvention: ${policy.git.commitConvention ?? "(none)"}`);
966
+ push();
967
+ push(`Documents: ${policy.docs.length}`);
968
+ for (const doc of policy.docs) {
969
+ const scope = doc.scope === "" ? "root" : doc.scope;
970
+ const from = doc.referencedFrom === null ? "" : ` \u2190 ${doc.referencedFrom}`;
971
+ push(` - ${doc.path} (${doc.kind}, scope=${scope})${from}`);
972
+ }
973
+ push();
974
+ push(`CODEOWNERS: ${policy.codeowners === null ? "(none)" : "found"}`);
975
+ push();
976
+ push("Sources");
977
+ for (const [kind, entries] of groupSources(policy.sources)) {
978
+ push(` ${kind}`);
979
+ for (const entry of entries) {
980
+ push(` - ${describeSource(entry)}`);
981
+ }
982
+ }
983
+ console.log(lines.join("\n"));
984
+ }
985
+ async function runPolicy(options = {}) {
986
+ let policy;
987
+ try {
988
+ policy = await loadRepositoryPolicy({ scope: options.scope ?? null });
989
+ } catch (err) {
990
+ printError(err instanceof Error ? err.message : String(err));
991
+ return 1;
992
+ }
993
+ if (options.json === true) {
994
+ console.log(JSON.stringify(policy, null, 2));
995
+ return 0;
996
+ }
997
+ render(policy);
998
+ const unavailable = policy.sources.filter((source) => source.status === "unavailable");
999
+ if (unavailable.length > 0) {
1000
+ console.log("");
1001
+ printWarning(
1002
+ `${unavailable.length} source(s) could not be consulted; the policy above is incomplete.`
1003
+ );
1004
+ } else if (policy.sources.length === 0) {
1005
+ console.log("");
1006
+ printInfo("This repository declares no policy sources; Issue Flow defaults apply.");
1007
+ }
1008
+ return 0;
1009
+ }
1010
+ export {
1011
+ runPolicy
1012
+ };
1013
+ //# sourceMappingURL=policy-NP4ZAVF6.js.map