diff --git a/.mux/actions/github/ensureIssueLabels.js b/.mux/actions/github/ensureIssueLabels.js deleted file mode 100644 index 5bd8cd5c..00000000 --- a/.mux/actions/github/ensureIssueLabels.js +++ /dev/null @@ -1,76 +0,0 @@ -const { - getIssueView, - inputObject, - normalizeIssue, - repositoryFromInput, - requiredIssueNumber, - stringList, -} = require('../../workflow-action-lib/github.cjs'); - -export const metadata = { - version: 1, - description: 'Idempotently add and remove GitHub issue labels', - effect: 'external', - inputSchema: mux.schema.object( - { - repository: mux.schema.optional(mux.schema.string()), - owner: mux.schema.optional(mux.schema.string()), - repo: mux.schema.optional(mux.schema.string()), - number: mux.schema.integer(), - addLabels: mux.schema.optional(mux.schema.array(mux.schema.string())), - removeLabels: mux.schema.optional(mux.schema.array(mux.schema.string())), - }, - { additionalProperties: false }, - ), - outputSchema: mux.schema.object( - { - changed: mux.schema.boolean(), - before: mux.schema.array(mux.schema.string()), - after: mux.schema.array(mux.schema.string()), - added: mux.schema.array(mux.schema.string()), - removed: mux.schema.array(mux.schema.string()), - }, - { additionalProperties: false }, - ), - permissions: [ - { kind: 'command', command: 'gh issue edit' }, - { kind: 'command', command: 'gh issue view' }, - ], - timeoutMs: 60000, -}; - -async function getLabelNames(ctx, repository, number) { - const issue = await getIssueView(ctx, repository, number, ['labels']); - return normalizeIssue(issue).labelNames; -} - -export async function execute(rawInput, ctx) { - const input = inputObject(rawInput); - const repository = repositoryFromInput(input); - const number = requiredIssueNumber(input.number); - const addLabels = stringList(input.addLabels); - const removeLabels = stringList(input.removeLabels); - const before = await getLabelNames(ctx, repository, number); - const missingAddLabels = addLabels.filter((label) => !before.includes(label)); - const presentRemoveLabels = removeLabels.filter((label) => - before.includes(label), - ); - if (missingAddLabels.length === 0 && presentRemoveLabels.length === 0) { - return { changed: false, before, after: before, added: [], removed: [] }; - } - const args = ['issue', 'edit', String(number)]; - if (repository) args.push('--repo', repository); - for (const label of missingAddLabels) args.push('--add-label', label); - for (const label of presentRemoveLabels) args.push('--remove-label', label); - await ctx.execChecked('gh', args); - const after = await getLabelNames(ctx, repository, number); - return { - changed: true, - before, - after, - added: missingAddLabels, - removed: presentRemoveLabels, - }; -} - -export const reconcile = execute; diff --git a/.mux/actions/github/findIssueComment.js b/.mux/actions/github/findIssueComment.js deleted file mode 100644 index b39e3136..00000000 --- a/.mux/actions/github/findIssueComment.js +++ /dev/null @@ -1,103 +0,0 @@ -const { - commentAuthorLogin, - currentUserLogin, - inputObject, - listComments, - optionalString, - requiredIssueNumber, - requiredRepository, - splitRepository, - stringList, -} = require('../../workflow-action-lib/github.cjs'); - -export const metadata = { - version: 1, - description: 'Find the latest GitHub issue comment containing expected text', - effect: 'read', - inputSchema: mux.schema.object( - { - repository: mux.schema.optional(mux.schema.string()), - owner: mux.schema.optional(mux.schema.string()), - repo: mux.schema.optional(mux.schema.string()), - number: mux.schema.integer(), - expectedAuthor: mux.schema.optional(mux.schema.string()), - requireAuthenticatedAuthor: mux.schema.optional(mux.schema.boolean()), - requiredBodyIncludes: mux.schema.array(mux.schema.string()), - }, - { additionalProperties: false }, - ), - outputSchema: mux.schema.object( - { - found: mux.schema.boolean(), - reason: mux.schema.string(), - url: mux.schema.nullable(mux.schema.string()), - commentId: mux.schema.nullable( - mux.schema.union([mux.schema.integer(), mux.schema.string()]), - ), - updatedAt: mux.schema.nullable(mux.schema.string()), - }, - { additionalProperties: false }, - ), - permissions: [{ kind: 'command', command: 'gh api' }], - timeoutMs: 60000, -}; - -async function expectedAuthor(input, ctx) { - const explicit = optionalString(input.expectedAuthor); - if (explicit) return explicit; - if (input.requireAuthenticatedAuthor === false) return ''; - return await currentUserLogin(ctx); -} - -export async function execute(rawInput, ctx) { - const input = inputObject(rawInput); - const repository = requiredRepository(input); - const parts = splitRepository(repository); - const number = requiredIssueNumber(input.number); - const includes = stringList(input.requiredBodyIncludes); - if (includes.length === 0) { - throw new Error('requiredBodyIncludes must include at least one string'); - } - - const author = await expectedAuthor(input, ctx); - if (input.requireAuthenticatedAuthor !== false && !author) { - return { - found: false, - reason: 'authenticated-author-unavailable', - url: null, - commentId: null, - updatedAt: null, - }; - } - - const comments = await listComments(ctx, parts.owner, parts.repo, number, { - limit: 'all', - }); - const match = comments - .slice() - .reverse() - .find( - (comment) => - (!author || commentAuthorLogin(comment) === author) && - typeof comment.body === 'string' && - includes.every((text) => comment.body.includes(text)), - ); - - if (!match) { - return { - found: false, - reason: 'not-found', - url: null, - commentId: null, - updatedAt: null, - }; - } - - return { - found: true, - reason: '', - url: match.html_url || null, - commentId: match.id || null, - updatedAt: match.updated_at || null, - }; -} diff --git a/.mux/actions/github/getIssueAutomationState.js b/.mux/actions/github/getIssueAutomationState.js deleted file mode 100644 index 11409143..00000000 --- a/.mux/actions/github/getIssueAutomationState.js +++ /dev/null @@ -1,120 +0,0 @@ -const { - getIssueView, - inputObject, - isMatchingMarker, - listComments, - markerStatus, - normalizeIssue, - optionalString, - requiredIssueNumber, - requiredRepository, - requiredString, - splitRepository, - stringList, -} = require('../../workflow-action-lib/github.cjs'); - -export const metadata = { - version: 1, - description: 'Read GitHub issue automation marker comments and done labels', - effect: 'read', - inputSchema: mux.schema.object( - { - repository: mux.schema.optional(mux.schema.string()), - owner: mux.schema.optional(mux.schema.string()), - repo: mux.schema.optional(mux.schema.string()), - number: mux.schema.integer(), - doneLabels: mux.schema.optional(mux.schema.array(mux.schema.string())), - ongoingLabels: mux.schema.optional(mux.schema.array(mux.schema.string())), - includeComments: mux.schema.optional(mux.schema.boolean()), - marker: mux.schema.optional(mux.schema.string()), - markerKey: mux.schema.optional(mux.schema.string()), - promptVersion: mux.schema.optional(mux.schema.string()), - }, - { additionalProperties: false }, - ), - outputSchema: mux.schema.object( - { - done: mux.schema.boolean(), - promptStarted: mux.schema.boolean(), - reportPosted: mux.schema.boolean(), - labelNames: mux.schema.array(mux.schema.string()), - markerComments: mux.schema.array( - mux.schema.object( - { - id: mux.schema.integer(), - url: mux.schema.nullable(mux.schema.string()), - status: mux.schema.string(), - }, - { additionalProperties: false }, - ), - ), - }, - { additionalProperties: false }, - ), - permissions: [ - { kind: 'command', command: 'gh api' }, - { kind: 'command', command: 'gh issue view' }, - ], - timeoutMs: 60000, -}; - -function labelsIncludeAll(labels, labelNames) { - return ( - labels.length > 0 && labels.every((label) => labelNames.includes(label)) - ); -} - -function labelsIncludeAny(labels, labelNames) { - return labels.some((label) => labelNames.includes(label)); -} - -export async function execute(rawInput, ctx) { - const input = inputObject(rawInput); - const repository = requiredRepository(input); - const number = requiredIssueNumber(input.number); - const doneLabels = stringList(input.doneLabels); - const ongoingLabels = stringList(input.ongoingLabels); - const includeComments = input.includeComments !== false; - const issuePromise = getIssueView(ctx, repository, number, ['labels']); - - if (!includeComments) { - const issue = await issuePromise; - const labelNames = normalizeIssue(issue).labelNames; - return { - done: labelsIncludeAll(doneLabels, labelNames), - promptStarted: labelsIncludeAny(ongoingLabels, labelNames), - reportPosted: false, - labelNames, - markerComments: [], - }; - } - - const parts = splitRepository(repository); - const marker = requiredString(input.marker, 'marker'); - const markerKey = requiredString(input.markerKey, 'markerKey'); - const promptVersion = optionalString(input.promptVersion) || 'v1'; - const [issue, comments] = await Promise.all([ - issuePromise, - listComments(ctx, parts.owner, parts.repo, number), - ]); - const labelNames = normalizeIssue(issue).labelNames; - const matching = comments.filter((comment) => - isMatchingMarker(comment.body, marker, markerKey, promptVersion), - ); - const statuses = matching - .map((comment) => markerStatus(comment.body)) - .filter(Boolean); - return { - done: labelsIncludeAll(doneLabels, labelNames), - promptStarted: - labelsIncludeAny(ongoingLabels, labelNames) || - statuses.includes('prompt-started'), - reportPosted: statuses.includes('report-posted'), - labelNames, - markerComments: matching.map((comment) => ({ - id: comment.id, - url: comment.html_url || null, - status: markerStatus(comment.body), - })), - }; -} diff --git a/.mux/actions/github/getIssueConversation.js b/.mux/actions/github/getIssueConversation.js deleted file mode 100644 index 0108dae2..00000000 --- a/.mux/actions/github/getIssueConversation.js +++ /dev/null @@ -1,186 +0,0 @@ -const { - boundedCharBudget, - boundedLimit, - commentAuthorLogin, - getIssueView, - inputObject, - listComments, - normalizeIssue, - repositoryFromInput, - requiredIssueNumber, - splitRepository, - truncateText, -} = require('../../workflow-action-lib/github.cjs'); - -export const metadata = { - version: 1, - description: 'Read a GitHub issue body and comments as markdown', - effect: 'read', - inputSchema: mux.schema.object( - { - repository: mux.schema.optional(mux.schema.string()), - owner: mux.schema.optional(mux.schema.string()), - repo: mux.schema.optional(mux.schema.string()), - number: mux.schema.integer(), - maxComments: mux.schema.optional(mux.schema.integer()), - issueBodyCharBudget: mux.schema.optional(mux.schema.integer()), - bodyCharBudget: mux.schema.optional(mux.schema.integer()), - conversationCharBudget: mux.schema.optional(mux.schema.integer()), - commentBodyCharBudget: mux.schema.optional(mux.schema.integer()), - }, - { additionalProperties: false }, - ), - outputSchema: mux.schema.object( - { - repository: mux.schema.nullable(mux.schema.string()), - number: mux.schema.integer(), - issue: mux.schema.object( - { - number: mux.schema.integer(), - safeId: mux.schema.string(), - title: mux.schema.string(), - url: mux.schema.string(), - state: mux.schema.string(), - body: mux.schema.string(), - author: mux.schema.nullable(mux.schema.string()), - createdAt: mux.schema.nullable(mux.schema.string()), - updatedAt: mux.schema.nullable(mux.schema.string()), - labelNames: mux.schema.array(mux.schema.string()), - }, - { additionalProperties: false }, - ), - conversationMarkdown: mux.schema.string(), - limits: mux.schema.object( - { - maxComments: mux.schema.integer(), - issueBodyBudget: mux.schema.integer(), - commentBodyBudget: mux.schema.integer(), - conversationBudget: mux.schema.integer(), - hasTruncatedConversation: mux.schema.boolean(), - hasOmittedComments: mux.schema.boolean(), - }, - { additionalProperties: false }, - ), - }, - { additionalProperties: false }, - ), - permissions: [ - { kind: 'command', command: 'gh issue view' }, - { kind: 'command', command: 'gh api' }, - ], - timeoutMs: 60000, -}; - -function repositoryPartsForComments(repository, issue) { - if (repository) return splitRepository(repository); - const match = - typeof issue.url === 'string' - ? issue.url.match( - /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/\d+$/, - ) - : null; - if (!match) - throw new Error( - 'repository or a GitHub issue URL is required to read comments', - ); - return { owner: match[1], repo: match[2] }; -} - -function formatConversation( - comments, - commentBodyBudget, - hasOmittedComments, - conversationBudget, -) { - const visibleComments = Array.isArray(comments) ? comments : []; - if (visibleComments.length === 0) - return { markdown: '(no issue comments)', truncated: false }; - const markdown = - visibleComments - .map( - (comment) => - '### Comment by ' + - (commentAuthorLogin(comment) || 'unknown') + - '\n\n' + - truncateText(comment.body || '', commentBodyBudget), - ) - .join('\n\n---\n\n') + - (hasOmittedComments ? '\n\n[omitted additional comments]' : ''); - return { - markdown: truncateText(markdown, conversationBudget), - truncated: markdown.length > conversationBudget, - }; -} - -export async function execute(rawInput, ctx) { - const input = inputObject(rawInput); - const repository = repositoryFromInput(input); - const number = requiredIssueNumber(input.number); - const maxComments = boundedLimit(input.maxComments, 100); - const issueBodyBudget = boundedCharBudget( - input.issueBodyCharBudget ?? input.bodyCharBudget, - 10000, - ); - const commentBodyBudget = boundedCharBudget( - input.commentBodyCharBudget, - 10000, - ); - const conversationBudget = boundedCharBudget( - input.conversationCharBudget, - 50000, - ); - const issueFields = [ - 'number', - 'title', - 'url', - 'state', - 'body', - 'author', - 'createdAt', - 'updatedAt', - 'labels', - ]; - let issue; - let comments; - if (repository) { - const parts = splitRepository(repository); - [issue, comments] = await Promise.all([ - getIssueView(ctx, repository, number, issueFields), - listComments(ctx, parts.owner, parts.repo, number, { - limit: maxComments + 1, - }), - ]); - } else { - issue = await getIssueView(ctx, repository, number, issueFields); - const parts = repositoryPartsForComments(repository, issue); - comments = await listComments(ctx, parts.owner, parts.repo, number, { - limit: maxComments + 1, - }); - } - const visibleComments = comments.slice(0, maxComments); - const hasOmittedComments = comments.length > visibleComments.length; - const normalizedIssue = normalizeIssue(issue); - const conversation = formatConversation( - visibleComments, - commentBodyBudget, - hasOmittedComments, - conversationBudget, - ); - return { - repository: repository || null, - number, - issue: { - ...normalizedIssue, - body: truncateText(normalizedIssue.body, issueBodyBudget), - }, - conversationMarkdown: conversation.markdown, - limits: { - maxComments, - issueBodyBudget, - commentBodyBudget, - conversationBudget, - hasTruncatedConversation: conversation.truncated, - hasOmittedComments: hasOmittedComments || conversation.truncated, - }, - }; -} diff --git a/.mux/actions/github/listIssues.js b/.mux/actions/github/listIssues.js deleted file mode 100644 index e15ce866..00000000 --- a/.mux/actions/github/listIssues.js +++ /dev/null @@ -1,126 +0,0 @@ -const { - boundedCharBudget, - boundedLimit, - inputObject, - normalizeIssue, - optionalString, - repositoryFromInput, - stringList, - truncateText, -} = require('../../workflow-action-lib/github.cjs'); - -export const metadata = { - version: 1, - description: 'List GitHub issues with reusable label/state filters', - effect: 'read', - inputSchema: mux.schema.object( - { - repository: mux.schema.optional(mux.schema.string()), - owner: mux.schema.optional(mux.schema.string()), - repo: mux.schema.optional(mux.schema.string()), - state: mux.schema.optional(mux.schema.string()), - includeLabels: mux.schema.optional(mux.schema.array(mux.schema.string())), - excludeLabels: mux.schema.optional(mux.schema.array(mux.schema.string())), - limit: mux.schema.optional(mux.schema.integer()), - includeBody: mux.schema.optional(mux.schema.boolean()), - bodyCharBudget: mux.schema.optional(mux.schema.integer()), - }, - { additionalProperties: false }, - ), - outputSchema: mux.schema.object( - { - repository: mux.schema.nullable(mux.schema.string()), - filters: mux.schema.object( - { - state: mux.schema.string(), - includeLabels: mux.schema.array(mux.schema.string()), - excludeLabels: mux.schema.array(mux.schema.string()), - limit: mux.schema.integer(), - includeBody: mux.schema.boolean(), - bodyCharBudget: mux.schema.integer(), - }, - { additionalProperties: false }, - ), - fetchedCount: mux.schema.integer(), - eligibleCount: mux.schema.integer(), - truncated: mux.schema.boolean(), - issues: mux.schema.array( - mux.schema.object( - { - number: mux.schema.integer(), - safeId: mux.schema.string(), - title: mux.schema.string(), - url: mux.schema.string(), - state: mux.schema.string(), - body: mux.schema.string(), - author: mux.schema.nullable(mux.schema.string()), - createdAt: mux.schema.nullable(mux.schema.string()), - updatedAt: mux.schema.nullable(mux.schema.string()), - labelNames: mux.schema.array(mux.schema.string()), - }, - { additionalProperties: false }, - ), - ), - }, - { additionalProperties: false }, - ), - permissions: [{ kind: 'command', command: 'gh issue list' }], - timeoutMs: 60000, -}; - -export async function execute(rawInput, ctx) { - const input = inputObject(rawInput); - const repository = repositoryFromInput(input); - const state = optionalString(input.state) || 'open'; - const includeLabels = stringList(input.includeLabels); - const excludeLabels = stringList(input.excludeLabels); - const limit = boundedLimit(input.limit, 1000); - const includeBody = input.includeBody === true; - const bodyCharBudget = boundedCharBudget(input.bodyCharBudget, 2000); - const jsonFields = - 'number,title,url,state,labels,author,createdAt,updatedAt' + - (includeBody ? ',body' : ''); - const fetchLimit = excludeLabels.length > 0 ? 1000 : limit; - const args = [ - 'issue', - 'list', - '--state', - state, - '--limit', - String(fetchLimit), - '--json', - jsonFields, - ]; - if (repository) args.push('--repo', repository); - for (const label of includeLabels) args.push('--label', label); - const fetchedIssues = (await ctx.execJson('gh', args)) - .map(normalizeIssue) - .map((issue) => ({ - ...issue, - body: includeBody ? truncateText(issue.body, bodyCharBudget) : '', - })); - const eligibleIssues = fetchedIssues - .filter((issue) => - includeLabels.every((label) => issue.labelNames.includes(label)), - ) - .filter((issue) => - excludeLabels.every((label) => !issue.labelNames.includes(label)), - ) - .sort((a, b) => a.number - b.number); - const issues = eligibleIssues.slice(0, limit); - return { - repository: repository || null, - filters: { - state, - includeLabels, - excludeLabels, - limit, - includeBody, - bodyCharBudget, - }, - fetchedCount: fetchedIssues.length, - eligibleCount: eligibleIssues.length, - truncated: fetchedIssues.length >= fetchLimit && excludeLabels.length > 0, - issues, - }; -} diff --git a/.mux/actions/github/verifyIssueCommentUrl.js b/.mux/actions/github/verifyIssueCommentUrl.js deleted file mode 100644 index 9dd45e30..00000000 --- a/.mux/actions/github/verifyIssueCommentUrl.js +++ /dev/null @@ -1,165 +0,0 @@ -const { - commentAuthorLogin, - currentUserLogin, - inputObject, - optionalString, - requiredIssueNumber, - requiredRepository, - requiredString, - splitRepository, - stringList, -} = require('../../workflow-action-lib/github.cjs'); - -export const metadata = { - version: 1, - description: - 'Verify that a GitHub issue comment URL belongs to an issue and contains expected text', - effect: 'read', - inputSchema: mux.schema.object( - { - repository: mux.schema.optional(mux.schema.string()), - owner: mux.schema.optional(mux.schema.string()), - repo: mux.schema.optional(mux.schema.string()), - number: mux.schema.integer(), - url: mux.schema.string(), - expectedAuthor: mux.schema.optional(mux.schema.string()), - requireAuthenticatedAuthor: mux.schema.optional(mux.schema.boolean()), - requiredBodyIncludes: mux.schema.optional( - mux.schema.array(mux.schema.string()), - ), - }, - { additionalProperties: false }, - ), - outputSchema: mux.schema.object( - { - verified: mux.schema.boolean(), - reason: mux.schema.string(), - missing: mux.schema.optional(mux.schema.array(mux.schema.string())), - url: mux.schema.optional(mux.schema.string()), - commentId: mux.schema.optional( - mux.schema.union([mux.schema.integer(), mux.schema.string()]), - ), - }, - { additionalProperties: false }, - ), - permissions: [{ kind: 'command', command: 'gh api' }], - timeoutMs: 60000, -}; - -function parseCommentUrl(url) { - const match = String(url || '').match( - /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)#issuecomment-(\d+)$/, - ); - return match - ? { - owner: match[1], - repo: match[2], - number: Number(match[3]), - commentId: match[4], - } - : null; -} - -function parseIssueApiUrl(url) { - const match = String(url || '').match( - /^https:\/\/api\.github\.com\/repos\/([^/]+)\/([^/]+)\/issues\/(\d+)$/, - ); - return match - ? { owner: match[1], repo: match[2], number: Number(match[3]) } - : null; -} - -function sameRepositoryPart(left, right) { - return String(left || '').toLowerCase() === String(right || '').toLowerCase(); -} - -function sameIssue(parts, number, parsed) { - return Boolean( - parsed && - sameRepositoryPart(parsed.owner, parts.owner) && - sameRepositoryPart(parsed.repo, parts.repo) && - parsed.number === number, - ); -} - -function commentBelongsToIssue(comment, parts, number) { - if (sameIssue(parts, number, parseIssueApiUrl(comment.issue_url))) - return true; - return sameIssue(parts, number, parseCommentUrl(comment.html_url)); -} - -async function expectedAuthor(input, ctx) { - const explicit = optionalString(input.expectedAuthor); - if (explicit) return explicit; - if (input.requireAuthenticatedAuthor === false) return ''; - return await currentUserLogin(ctx); -} - -export async function execute(rawInput, ctx) { - const input = inputObject(rawInput); - const repository = requiredRepository(input); - const parts = splitRepository(repository); - const number = requiredIssueNumber(input.number); - const parsed = parseCommentUrl(requiredString(input.url, 'url')); - if ( - !parsed || - !sameRepositoryPart(parsed.owner, parts.owner) || - !sameRepositoryPart(parsed.repo, parts.repo) || - parsed.number !== number - ) { - return { verified: false, reason: 'comment-url-does-not-match-issue' }; - } - const comment = await ctx.execJson('gh', [ - 'api', - 'repos/' + - parts.owner + - '/' + - parts.repo + - '/issues/comments/' + - parsed.commentId, - ]); - if (!commentBelongsToIssue(comment, parts, number)) { - return { - verified: false, - reason: 'fetched-comment-does-not-belong-to-issue', - url: comment.html_url || input.url, - commentId: comment.id || parsed.commentId, - }; - } - - const author = await expectedAuthor(input, ctx); - if (input.requireAuthenticatedAuthor !== false && !author) { - return { - verified: false, - reason: 'authenticated-author-unavailable', - url: comment.html_url || input.url, - commentId: comment.id || parsed.commentId, - }; - } - if (author && commentAuthorLogin(comment) !== author) { - return { - verified: false, - reason: 'comment-author-mismatch', - url: comment.html_url || input.url, - commentId: comment.id || parsed.commentId, - }; - } - - const includes = stringList(input.requiredBodyIncludes); - const missing = includes.filter( - (text) => typeof comment.body !== 'string' || !comment.body.includes(text), - ); - if (missing.length > 0) - return { - verified: false, - reason: 'missing-required-body-text', - missing, - url: comment.html_url || input.url, - }; - return { - verified: true, - reason: '', - url: comment.html_url || input.url, - commentId: comment.id || parsed.commentId, - }; -} diff --git a/.mux/actions/project/context.js b/.mux/actions/project/context.js deleted file mode 100644 index aca2ef83..00000000 --- a/.mux/actions/project/context.js +++ /dev/null @@ -1,132 +0,0 @@ -const path = require('path'); - -export const metadata = { - version: 1, - description: 'Resolve the current Mux project path and GitHub repository', - effect: 'read', - inputSchema: mux.schema.object({}, { additionalProperties: false }), - outputSchema: mux.schema.object( - { - cwd: mux.schema.nullable(mux.schema.string()), - gitRoot: mux.schema.nullable(mux.schema.string()), - projectPath: mux.schema.nullable(mux.schema.string()), - projectPathSource: mux.schema.string(), - repository: mux.schema.nullable(mux.schema.string()), - repositorySource: mux.schema.string(), - }, - { additionalProperties: false }, - ), - permissions: [ - { kind: 'command', command: 'pwd' }, - { kind: 'command', command: 'git rev-parse' }, - { kind: 'command', command: 'git remote get-url' }, - { kind: 'command', command: 'gh repo view' }, - ], - timeoutMs: 30000, -}; - -function cleanString(value) { - return typeof value === 'string' && value.trim().length > 0 - ? value.trim() - : null; -} - -async function execStdout(ctx, command, args) { - try { - const result = await ctx.execChecked(command, args); - return cleanString(result.stdout); - } catch { - return null; - } -} - -async function repoFromGh(ctx) { - try { - const result = await ctx.execJson('gh', [ - 'repo', - 'view', - '--json', - 'nameWithOwner', - ]); - return cleanString(result && result.nameWithOwner); - } catch { - return null; - } -} - -function repoFromRemoteUrl(url) { - const text = cleanString(url); - if (!text) return null; - const match = text.match(/(?:github\.com[:/])([^/]+)\/([^/?#]+)(?:\/?$)/); - if (!match) return null; - const repo = match[2].replace(/\.git$/, ''); - return match[1] && repo ? match[1] + '/' + repo : null; -} - -function projectPathFromGitCommonDir(commonDir, baseDir) { - const text = cleanString(commonDir); - if (!text) return null; - const absolute = path.isAbsolute(text) - ? text - : path.resolve(baseDir || '.', text); - return path.basename(absolute) === '.git' ? path.dirname(absolute) : null; -} - -function chooseProjectPath(envProjectPath, commonProjectPath, gitRoot, cwd) { - if (envProjectPath) { - return { - projectPath: envProjectPath, - projectPathSource: 'MUX_PROJECT_PATH', - }; - } - if (commonProjectPath) { - return { - projectPath: commonProjectPath, - projectPathSource: 'git-common-dir', - }; - } - if (gitRoot) return { projectPath: gitRoot, projectPathSource: 'git-root' }; - if (cwd) return { projectPath: cwd, projectPathSource: 'cwd' }; - return { projectPath: null, projectPathSource: 'unresolved' }; -} - -export async function execute(_input, ctx) { - const cwd = cleanString(ctx.cwd) || (await execStdout(ctx, 'pwd', [])); - const gitRoot = await execStdout(ctx, 'git', [ - 'rev-parse', - '--show-toplevel', - ]); - const gitCommonDir = await execStdout(ctx, 'git', [ - 'rev-parse', - '--git-common-dir', - ]); - const envProjectPath = cleanString(process.env.MUX_PROJECT_PATH); - const commonProjectPath = projectPathFromGitCommonDir( - gitCommonDir, - gitRoot || cwd, - ); - const { projectPath, projectPathSource } = chooseProjectPath( - envProjectPath, - commonProjectPath, - gitRoot, - cwd, - ); - const repositoryFromGh = await repoFromGh(ctx); - const remoteUrl = repositoryFromGh - ? null - : await execStdout(ctx, 'git', ['remote', 'get-url', 'origin']); - const repositoryFromGit = repoFromRemoteUrl(remoteUrl); - - return { - cwd, - gitRoot, - projectPath, - projectPathSource, - repository: repositoryFromGh || repositoryFromGit, - repositorySource: repositoryFromGh - ? 'gh-repo-view' - : repositoryFromGit - ? 'git-origin' - : 'unresolved', - }; -} diff --git a/.mux/workflow-action-lib/github.cjs b/.mux/workflow-action-lib/github.cjs deleted file mode 100644 index bbb8aa27..00000000 --- a/.mux/workflow-action-lib/github.cjs +++ /dev/null @@ -1,198 +0,0 @@ -function inputObject(input) { - return input != null && typeof input === 'object' && !Array.isArray(input) - ? input - : {}; -} - -function optionalString(value) { - return typeof value === 'string' && value.trim().length > 0 - ? value.trim() - : undefined; -} - -function requiredString(value, name) { - const text = optionalString(value); - if (!text) throw new Error(name + ' must be a non-empty string'); - return text; -} - -function stringList(value) { - if (!Array.isArray(value)) return []; - return value - .filter((item) => typeof item === 'string' && item.trim().length > 0) - .map((item) => item.trim()); -} - -function repositoryFromInput(input) { - const repository = optionalString(input.repository); - if (repository) return repository; - const owner = optionalString(input.owner); - const repo = optionalString(input.repo); - return owner && repo ? owner + '/' + repo : undefined; -} - -function requiredRepository(input) { - const repository = repositoryFromInput(input); - if (!repository) throw new Error('repository or owner/repo is required'); - return repository; -} - -function splitRepository(repository) { - const parts = repository.split('/'); - if (parts.length !== 2 || !parts[0] || !parts[1]) { - throw new Error('repository must use owner/repo format'); - } - return { owner: parts[0], repo: parts[1] }; -} - -function requiredIssueNumber(value) { - if (!Number.isInteger(value) || value <= 0) { - throw new Error('number must be a positive integer issue number'); - } - return value; -} - -function boundedLimit(value, fallback) { - if (!Number.isInteger(value)) return fallback; - return Math.max(1, Math.min(value, 1000)); -} - -function boundedCharBudget(value, fallback) { - if (!Number.isInteger(value)) return fallback; - return Math.max(0, Math.min(value, 100000)); -} - -function truncateText(value, budget) { - const text = typeof value === 'string' ? value : ''; - if (text.length <= budget) return text; - return ( - text.slice(0, budget) + - '\n\n[truncated ' + - (text.length - budget) + - ' chars]' - ); -} - -function normalizeIssue(issue) { - const labelNames = Array.isArray(issue.labels) - ? issue.labels - .map((label) => (typeof label === 'string' ? label : label.name)) - .filter((name) => typeof name === 'string' && name.length > 0) - : []; - return { - number: issue.number, - safeId: 'issue-' + issue.number, - title: issue.title || '', - url: issue.url || '', - state: issue.state || '', - body: issue.body || '', - author: issue.author && issue.author.login ? issue.author.login : null, - createdAt: issue.createdAt || null, - updatedAt: issue.updatedAt || null, - labelNames, - }; -} - -function markerCommentNeedle(marker, markerKey, promptVersion) { - return ( - '