@gh-symphony/cli 0.1.4 → 0.2.2

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.
@@ -53,6 +53,7 @@ var DEFAULT_WORKFLOW_TRACKER = {
53
53
  terminalStates: DEFAULT_WORKFLOW_LIFECYCLE.terminalStates,
54
54
  projectId: null,
55
55
  stateFieldName: DEFAULT_WORKFLOW_LIFECYCLE.stateFieldName,
56
+ priority: null,
56
57
  priorityFieldName: null,
57
58
  blockerCheckStates: DEFAULT_WORKFLOW_LIFECYCLE.blockerCheckStates
58
59
  };
@@ -169,6 +170,7 @@ function parseWorkflowMarkdown(markdown, env = process.env, options = {}) {
169
170
  terminalStates,
170
171
  projectId: readOptionalString(tracker, "project_id", env),
171
172
  stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
173
+ priority: readPriorityConfig(tracker, env),
172
174
  priorityFieldName: readOptionalString(tracker, "priority_field", env),
173
175
  blockerCheckStates
174
176
  },
@@ -235,6 +237,52 @@ function validateTrackerConfig(tracker, trackerKind, env) {
235
237
  }
236
238
  }
237
239
  }
240
+ function readPriorityConfig(tracker, env) {
241
+ if (tracker.priority === void 0 || tracker.priority === null) {
242
+ return null;
243
+ }
244
+ const priority = readObject(tracker, "priority", "tracker.priority");
245
+ const source = readRequiredString(priority, "source", env);
246
+ const keys = new Set(Object.keys(priority));
247
+ if (source === "project-field") {
248
+ rejectPriorityKeys(keys, ["source", "field", "values"], source);
249
+ const field = readRequiredString(priority, "field", env);
250
+ const values = readNumberMap(priority, "values", "tracker.priority.values");
251
+ if (Object.keys(values).length === 0) {
252
+ throw new Error(
253
+ 'Workflow front matter field "tracker.priority.values" must be a non-empty object for tracker.priority.source "project-field".'
254
+ );
255
+ }
256
+ return { source, field, values };
257
+ }
258
+ if (source === "labels") {
259
+ rejectPriorityKeys(keys, ["source", "labels"], source);
260
+ const labels = readNumberMap(priority, "labels", "tracker.priority.labels");
261
+ if (Object.keys(labels).length === 0) {
262
+ throw new Error(
263
+ 'Workflow front matter field "tracker.priority.labels" must be a non-empty object for tracker.priority.source "labels".'
264
+ );
265
+ }
266
+ return { source, labels };
267
+ }
268
+ if (source === "disabled") {
269
+ rejectPriorityKeys(keys, ["source"], source);
270
+ return { source };
271
+ }
272
+ throw new Error(
273
+ `Unsupported workflow tracker.priority.source "${source}". Supported values: project-field, labels, disabled.`
274
+ );
275
+ }
276
+ function rejectPriorityKeys(keys, allowedKeys, source) {
277
+ const allowed = new Set(allowedKeys);
278
+ for (const key of keys) {
279
+ if (!allowed.has(key)) {
280
+ throw new Error(
281
+ `Workflow front matter field "tracker.priority.${key}" is not supported for tracker.priority.source "${source}".`
282
+ );
283
+ }
284
+ }
285
+ }
238
286
  function parseLegacyWorkflowMarkdown(markdown) {
239
287
  const promptGuidelines = matchOptionalSection(markdown, "Prompt Guidelines") ?? "";
240
288
  return {
@@ -262,6 +310,10 @@ function parseBlock(lines, startIndex, indent) {
262
310
  index += 1;
263
311
  continue;
264
312
  }
313
+ if (line.trim().startsWith("#")) {
314
+ index += 1;
315
+ continue;
316
+ }
265
317
  const lineIndent = countIndent(line);
266
318
  if (lineIndent < indent) {
267
319
  break;
@@ -306,11 +358,16 @@ function parseBlock(lines, startIndex, indent) {
306
358
  );
307
359
  }
308
360
  collectionType = "object";
309
- const separatorIndex = trimmed.indexOf(":");
361
+ const separatorIndex = findMappingSeparator(trimmed);
310
362
  if (separatorIndex < 0) {
311
363
  throw new Error(`Invalid workflow front matter line "${trimmed}".`);
312
364
  }
313
- const key = trimmed.slice(0, separatorIndex).trim();
365
+ const rawKey = trimmed.slice(0, separatorIndex).trim();
366
+ const parsedKey = parseScalar(rawKey);
367
+ if (typeof parsedKey !== "string") {
368
+ throw new Error(`Invalid workflow front matter key "${rawKey}".`);
369
+ }
370
+ const key = parsedKey;
314
371
  const remainder = trimmed.slice(separatorIndex + 1).trim();
315
372
  if (remainder === "|" || remainder === "|-") {
316
373
  const [multiline, nextIndex2] = parseMultilineScalar(
@@ -355,6 +412,30 @@ function parseMultilineScalar(lines, startIndex, indent) {
355
412
  function countIndent(line) {
356
413
  return line.match(/^ */)?.[0].length ?? 0;
357
414
  }
415
+ function findMappingSeparator(value) {
416
+ let quote = null;
417
+ for (let index = 0; index < value.length; index += 1) {
418
+ const char = value[index];
419
+ if (quote) {
420
+ if (char === "\\") {
421
+ index += 1;
422
+ continue;
423
+ }
424
+ if (char === quote) {
425
+ quote = null;
426
+ }
427
+ continue;
428
+ }
429
+ if (char === '"' || char === "'") {
430
+ quote = char;
431
+ continue;
432
+ }
433
+ if (char === ":") {
434
+ return index;
435
+ }
436
+ }
437
+ return -1;
438
+ }
358
439
  function parseScalar(value) {
359
440
  if (value === "null") return null;
360
441
  if (value === "true") return true;
@@ -363,8 +444,18 @@ function parseScalar(value) {
363
444
  return parseInlineArray(value);
364
445
  }
365
446
  if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
366
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
367
- return value.slice(1, -1);
447
+ if (value.startsWith('"') && value.endsWith('"')) {
448
+ try {
449
+ const parsed = JSON.parse(value);
450
+ if (typeof parsed === "string") {
451
+ return parsed;
452
+ }
453
+ } catch {
454
+ throw new Error(`Invalid quoted workflow front matter scalar "${value}".`);
455
+ }
456
+ }
457
+ if (value.startsWith("'") && value.endsWith("'")) {
458
+ return value.slice(1, -1).replace(/''/g, "'");
368
459
  }
369
460
  return value;
370
461
  }
@@ -559,13 +650,13 @@ function readOptionalIntegerLike(input, key) {
559
650
  }
560
651
  throw new Error(`Workflow front matter field "${key}" must be an integer.`);
561
652
  }
562
- function readNumberMap(input, key) {
653
+ function readNumberMap(input, key, path = key) {
563
654
  const value = input[key];
564
655
  if (value === void 0 || value === null) {
565
656
  return {};
566
657
  }
567
658
  if (typeof value !== "object" || Array.isArray(value)) {
568
- throw new Error(`Workflow front matter field "${key}" must be an object.`);
659
+ throw new Error(`Workflow front matter field "${path}" must be an object.`);
569
660
  }
570
661
  const result = {};
571
662
  for (const [entryKey, entryValue] of Object.entries(value)) {
@@ -573,12 +664,12 @@ function readNumberMap(input, key) {
573
664
  result[entryKey] = entryValue;
574
665
  continue;
575
666
  }
576
- if (typeof entryValue === "string" && /^\d+$/.test(entryValue)) {
667
+ if (typeof entryValue === "string" && /^-?\d+$/.test(entryValue)) {
577
668
  result[entryKey] = Number.parseInt(entryValue, 10);
578
669
  continue;
579
670
  }
580
671
  throw new Error(
581
- `Workflow front matter field "${key}.${entryKey}" must be an integer.`
672
+ `Workflow front matter field "${path}.${entryKey}" must be an integer.`
582
673
  );
583
674
  }
584
675
  return result;
@@ -1464,6 +1555,10 @@ function isFileMissing(error) {
1464
1555
  // ../core/src/observability/event-formatter.ts
1465
1556
  function formatEventMessage(event) {
1466
1557
  switch (event.event) {
1558
+ case "tracker.list":
1559
+ return `Tracker list saw ${event.issue.identifier}`;
1560
+ case "tracker.fetchByIds":
1561
+ return `Tracker fetch refreshed ${event.issue.identifier}`;
1467
1562
  case "run-dispatched":
1468
1563
  return event.issueState ? `Dispatched from ${event.issueState}` : "Dispatched";
1469
1564
  case "run-recovered":
@@ -1538,31 +1633,176 @@ var SENSITIVE_KEY_SUBSTRINGS = [
1538
1633
  "api_key"
1539
1634
  ];
1540
1635
  function redactObservabilitySecrets(value) {
1541
- return redactValue(value);
1636
+ return redactObservabilitySecretsWithStats(value).value;
1637
+ }
1638
+ function redactObservabilitySecretsWithStats(value) {
1639
+ const counts = createRedactionCounts();
1640
+ const redacted = redactValue(value, counts, {
1641
+ redactStringValues: false
1642
+ });
1643
+ return { value: redacted, redactions: summarizeRedactionCounts(counts) };
1644
+ }
1645
+ function redactObservabilityDiagnosticsWithStats(value) {
1646
+ const counts = createRedactionCounts();
1647
+ const redacted = redactValue(value, counts, {
1648
+ redactStringValues: true
1649
+ });
1650
+ return { value: redacted, redactions: summarizeRedactionCounts(counts) };
1542
1651
  }
1543
- function redactValue(value) {
1652
+ function redactObservabilityTextWithStats(text) {
1653
+ const counts = createRedactionCounts();
1654
+ return {
1655
+ value: redactTextValue(text, counts),
1656
+ redactions: summarizeRedactionCounts(counts)
1657
+ };
1658
+ }
1659
+ function redactValue(value, counts, options) {
1544
1660
  if (Array.isArray(value)) {
1545
- return value.map((item) => redactValue(item));
1661
+ return value.map((item) => redactValue(item, counts, options));
1662
+ }
1663
+ if (typeof value === "string" && options.redactStringValues) {
1664
+ return redactTextValue(value, counts);
1546
1665
  }
1547
1666
  if (!isRecord3(value)) {
1548
1667
  return value;
1549
1668
  }
1550
1669
  return Object.fromEntries(
1551
- Object.entries(value).map(([key, nested]) => [
1552
- key,
1553
- shouldRedactKey(key) ? REDACTED : redactValue(nested)
1554
- ])
1670
+ Object.entries(value).map(([key, nested]) => {
1671
+ const redactionClass = redactionClassForKey(key);
1672
+ if (redactionClass) {
1673
+ incrementRedaction(counts, redactionClass);
1674
+ return [key, REDACTED];
1675
+ }
1676
+ return [key, redactValue(nested, counts, options)];
1677
+ })
1555
1678
  );
1556
1679
  }
1557
- function shouldRedactKey(key) {
1680
+ function redactionClassForKey(key) {
1558
1681
  const normalizedKey = key.toLowerCase();
1559
- return normalizedKey === "token" || normalizedKey.endsWith("token") || SENSITIVE_KEY_SUBSTRINGS.some(
1682
+ if (normalizedKey.includes("authorization")) {
1683
+ return "authorization_header";
1684
+ }
1685
+ if (normalizedKey.includes("apikey") || normalizedKey.includes("api-key") || normalizedKey.includes("api_key")) {
1686
+ return "api_key";
1687
+ }
1688
+ if (normalizedKey.includes("secret")) {
1689
+ return "secret_key";
1690
+ }
1691
+ if (normalizedKey === "token" || normalizedKey.endsWith("token")) {
1692
+ return "env_token";
1693
+ }
1694
+ if (SENSITIVE_KEY_SUBSTRINGS.some(
1560
1695
  (pattern) => normalizedKey.includes(pattern.toLowerCase())
1561
- );
1696
+ )) {
1697
+ return "secret_key";
1698
+ }
1699
+ return null;
1562
1700
  }
1563
1701
  function isRecord3(value) {
1564
1702
  return value != null && typeof value === "object";
1565
1703
  }
1704
+ function redactTextValue(text, counts) {
1705
+ let redacted = replaceAndCount(
1706
+ text,
1707
+ /\b(Authorization\s*:\s*Bearer\s+)([^\s]+)/gi,
1708
+ "authorization_header",
1709
+ counts,
1710
+ "$1[REDACTED]"
1711
+ );
1712
+ redacted = replaceAndCount(
1713
+ redacted,
1714
+ /\b(X-API-Key\s*:\s*)([^\s]+)/gi,
1715
+ "api_key",
1716
+ counts,
1717
+ "$1[REDACTED]"
1718
+ );
1719
+ redacted = replaceAndCount(
1720
+ redacted,
1721
+ /^([A-Z0-9_]*(?:TOKEN)\w*\s*=\s*)([^\s]+)/gim,
1722
+ "env_token",
1723
+ counts,
1724
+ "$1[REDACTED]"
1725
+ );
1726
+ redacted = replaceAndCount(
1727
+ redacted,
1728
+ /^([A-Z0-9_]*(?:API_KEY)\w*\s*=\s*)([^\s]+)/gim,
1729
+ "api_key",
1730
+ counts,
1731
+ "$1[REDACTED]"
1732
+ );
1733
+ redacted = replaceAndCount(
1734
+ redacted,
1735
+ /^([A-Z0-9_]*(?:SECRET)\w*\s*=\s*)([^\s]+)/gim,
1736
+ "secret_key",
1737
+ counts,
1738
+ "$1[REDACTED]"
1739
+ );
1740
+ redacted = replaceAndCount(
1741
+ redacted,
1742
+ /((?:"token"|'token'|token)\s*:\s*)(?:"([^"]*)"|'([^']*)'|([^\s,}\]]+))/gi,
1743
+ "env_token",
1744
+ counts,
1745
+ '$1"[REDACTED]"'
1746
+ );
1747
+ redacted = replaceAndCount(
1748
+ redacted,
1749
+ /((?:"secret"|'secret'|secret)\s*:\s*)(?:"([^"]*)"|'([^']*)'|([^\s,}\]]+))/gi,
1750
+ "secret_key",
1751
+ counts,
1752
+ '$1"[REDACTED]"'
1753
+ );
1754
+ redacted = replaceAndCount(
1755
+ redacted,
1756
+ /((?:"apiKey"|'apiKey'|apiKey)\s*:\s*)(?:"([^"]*)"|'([^']*)'|([^\s,}\]]+))/g,
1757
+ "api_key",
1758
+ counts,
1759
+ '$1"[REDACTED]"'
1760
+ );
1761
+ redacted = replaceAndCount(
1762
+ redacted,
1763
+ /\bghp_[A-Za-z0-9_]+/g,
1764
+ "env_token",
1765
+ counts,
1766
+ "[REDACTED]"
1767
+ );
1768
+ redacted = replaceAndCount(
1769
+ redacted,
1770
+ /\blin_[A-Za-z0-9_]+/g,
1771
+ "api_key",
1772
+ counts,
1773
+ "[REDACTED]"
1774
+ );
1775
+ redacted = replaceAndCount(
1776
+ redacted,
1777
+ /\bsk-[A-Za-z0-9_-]+/g,
1778
+ "api_key",
1779
+ counts,
1780
+ "[REDACTED]"
1781
+ );
1782
+ return redacted;
1783
+ }
1784
+ function replaceAndCount(text, pattern, redactionClass, counts, replacement) {
1785
+ return text.replace(pattern, (...args) => {
1786
+ const matched = typeof args[0] === "string" ? args[0] : "";
1787
+ if (matched.includes(REDACTED)) {
1788
+ return matched;
1789
+ }
1790
+ incrementRedaction(counts, redactionClass);
1791
+ return replacement.replace(/\$(\d+)/g, (_placeholder, index) => {
1792
+ const group = args[Number.parseInt(index, 10)];
1793
+ return typeof group === "string" ? group : "";
1794
+ });
1795
+ });
1796
+ }
1797
+ function createRedactionCounts() {
1798
+ return /* @__PURE__ */ new Map();
1799
+ }
1800
+ function incrementRedaction(counts, redactionClass) {
1801
+ counts.set(redactionClass, (counts.get(redactionClass) ?? 0) + 1);
1802
+ }
1803
+ function summarizeRedactionCounts(counts) {
1804
+ return Array.from(counts.entries()).filter(([, count]) => count > 0).map(([redactionClass, count]) => ({ class: redactionClass, count })).sort((left, right) => left.class.localeCompare(right.class));
1805
+ }
1566
1806
 
1567
1807
  // ../core/src/observability/status-assembler.ts
1568
1808
  function isMatchingIssueRun(run, issueId, issueIdentifier) {
@@ -6304,6 +6544,8 @@ export {
6304
6544
  isFileMissing,
6305
6545
  parseRecentEvents,
6306
6546
  redactObservabilitySecrets,
6547
+ redactObservabilityDiagnosticsWithStats,
6548
+ redactObservabilityTextWithStats,
6307
6549
  isMatchingIssueRun,
6308
6550
  mapIssueOrchestrationStateToStatus,
6309
6551
  resolveGitHubGraphQLToken,
@@ -106,6 +106,7 @@ export {
106
106
  REPO_RUNTIME_DIR,
107
107
  resolveConfigDir,
108
108
  configFilePath,
109
+ projectConfigPath,
109
110
  daemonPidPath,
110
111
  orchestratorLogPath,
111
112
  httpStatusPath,
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveRepoRuntimeRoot
4
- } from "./chunk-6I753NYO.js";
4
+ } from "./chunk-RZ3WO7OV.js";
5
5
  import {
6
6
  parseWorkflowMarkdown
7
- } from "./chunk-EWTMSDCE.js";
7
+ } from "./chunk-3SKN5L3I.js";
8
8
  import {
9
9
  saveGlobalConfig,
10
10
  saveProjectConfig
11
- } from "./chunk-WOVNN5NW.js";
11
+ } from "./chunk-4ICDSQCJ.js";
12
12
 
13
13
  // src/repo-runtime.ts
14
14
  import { execFileSync } from "child_process";
@@ -58,6 +58,7 @@ async function initRepoRuntime(flags) {
58
58
  await migrateLegacyRuntime(runtimeRoot);
59
59
  const workflowPath = resolve(repoDir, flags.workflowFile ?? "WORKFLOW.md");
60
60
  const workflow = parseWorkflowMarkdown(await readFile(workflowPath, "utf8"));
61
+ validateRepoInitWorkflow(workflow);
61
62
  const repository = resolveRepository(repoDir);
62
63
  const trackerAdapter = workflow.tracker.kind ?? "github-project";
63
64
  const trackerBindingId = workflow.tracker.projectId ?? workflow.tracker.projectSlug ?? "";
@@ -70,6 +71,9 @@ async function initRepoRuntime(flags) {
70
71
  if (flags.assignedOnly) {
71
72
  trackerSettings.assignedOnly = true;
72
73
  }
74
+ if (workflow.tracker.priorityFieldName) {
75
+ trackerSettings.priorityFieldName = workflow.tracker.priorityFieldName;
76
+ }
73
77
  if (trackerAdapter === "file") {
74
78
  if (!process.env.GH_SYMPHONY_FILE_TRACKER_ISSUES_PATH) {
75
79
  throw new Error(
@@ -88,6 +92,7 @@ async function initRepoRuntime(flags) {
88
92
  adapter: trackerAdapter,
89
93
  bindingId: trackerBindingId,
90
94
  ...workflow.tracker.endpoint ? { apiUrl: workflow.tracker.endpoint } : {},
95
+ priority: workflow.tracker.priority,
91
96
  settings: trackerSettings
92
97
  }
93
98
  };
@@ -112,6 +117,16 @@ async function initRepoRuntime(flags) {
112
117
  repository
113
118
  };
114
119
  }
120
+ function validateRepoInitWorkflow(workflow) {
121
+ if (workflow.tracker.kind !== "linear") {
122
+ return;
123
+ }
124
+ if (!workflow.tracker.apiKey?.trim()) {
125
+ throw new Error(
126
+ 'Linear tracker repo init requires WORKFLOW.md field "tracker.api_key" to reference a resolvable environment variable such as "$LINEAR_API_KEY".'
127
+ );
128
+ }
129
+ }
115
130
  async function migrateLegacyRuntime(runtimeRoot) {
116
131
  const projectsDir = join(runtimeRoot, "projects");
117
132
  const projectIds = await readDirectoryNames(projectsDir);
@@ -32,6 +32,48 @@ function createClient(token, options) {
32
32
  fetchImpl: options?.fetchImpl ?? fetch
33
33
  };
34
34
  }
35
+ async function listRepositoryLabels(client, owner, name) {
36
+ const restUrl = client.apiUrl.replace("/graphql", "");
37
+ const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
38
+ const labels = [];
39
+ let page = 1;
40
+ while (true) {
41
+ const response = await client.fetchImpl(
42
+ `${baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/labels?per_page=100&page=${page}`,
43
+ {
44
+ headers: {
45
+ authorization: `Bearer ${client.token}`,
46
+ accept: "application/vnd.github+json"
47
+ }
48
+ }
49
+ );
50
+ if (!response.ok) {
51
+ const payload = await response.json().catch(() => null);
52
+ const message = payload?.message?.trim() || response.statusText;
53
+ throw new GitHubApiError(
54
+ `GitHub label lookup failed for ${owner}/${name}: ${response.status} ${message}`.trim(),
55
+ response.status
56
+ );
57
+ }
58
+ const pageLabels = await response.json();
59
+ labels.push(
60
+ ...pageLabels.flatMap(
61
+ (label) => typeof label.name === "string" ? [
62
+ {
63
+ name: label.name,
64
+ color: label.color ?? null,
65
+ description: label.description ?? null
66
+ }
67
+ ] : []
68
+ )
69
+ );
70
+ if (pageLabels.length < 100) {
71
+ break;
72
+ }
73
+ page += 1;
74
+ }
75
+ return labels;
76
+ }
35
77
  async function validateToken(client) {
36
78
  const restUrl = client.apiUrl.replace("/graphql", "");
37
79
  const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
@@ -739,6 +781,7 @@ export {
739
781
  GitHubApiError,
740
782
  GitHubScopeError,
741
783
  createClient,
784
+ listRepositoryLabels,
742
785
  validateToken,
743
786
  checkRequiredScopes,
744
787
  discoverUserProjects,
@@ -10,7 +10,7 @@ import {
10
10
  resolveGitHubGraphQLToken,
11
11
  shouldReuseAgentCredentialCache,
12
12
  writeAgentCredentialCache
13
- } from "./chunk-EWTMSDCE.js";
13
+ } from "./chunk-3SKN5L3I.js";
14
14
 
15
15
  // ../runtime-codex/src/runtime.ts
16
16
  import { spawn } from "child_process";