@codyswann/lisa 2.85.2 → 2.88.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 (28) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/commands/automation-status.md +5 -0
  5. package/plugins/lisa/scripts/automation-status-expected-fleet.mjs +444 -0
  6. package/plugins/lisa/scripts/automation-status-report.mjs +170 -0
  7. package/plugins/lisa/skills/automation-status/SKILL.md +75 -0
  8. package/plugins/lisa/skills/automation-status/agents/openai.yaml +4 -0
  9. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  25. package/plugins/src/base/commands/automation-status.md +5 -0
  26. package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +444 -0
  27. package/plugins/src/base/scripts/automation-status-report.mjs +170 -0
  28. package/plugins/src/base/skills/automation-status/SKILL.md +75 -0
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: automation-status
3
+ description: "Read-only operator surface for the current project's Lisa automation fleet. Resolves the expected recurring jobs from the same setup-automations contract Lisa uses to create them, inspects the active runtime scheduler (Codex automations or Claude /schedule), compares live command/cadence/queue arguments against the expected contract, and reports grouped fleet health such as healthy, missing, unsupported, drifted, stale, or failing with remediation guidance."
4
+ allowed-tools: ["Skill", "Bash", "Read"]
5
+ ---
6
+
7
+ # Automation Status: $ARGUMENTS
8
+
9
+ `/lisa:automation-status` is the operator-facing inspection surface for Lisa's unattended job fleet. It answers, for the **current repo only**, whether the recurring Lisa automations that should exist actually exist, still match Lisa's setup contract, and appear healthy based on the runtime metadata available.
10
+
11
+ This command is **read-only** in v1. It does not create, update, resume, rerun, pause, or delete automations. It complements `/lisa:setup-automations`, `/lisa:tear-down-automations`, `/lisa:intake`, `/lisa:repair-intake`, `doctor`, and `monitor`; it does not replace them.
12
+
13
+ ## Confirmation policy
14
+
15
+ Do **not** ask for confirmation once invoked. This skill inspects scheduler state and reports what it finds. There are no write-side effects in the v1 surface.
16
+
17
+ ## Scope
18
+
19
+ Inspect only the Lisa automation fleet for the current project:
20
+
21
+ - `intake-repair`
22
+ - `intake-prd`
23
+ - `intake-tickets`
24
+ - `exploratory-bugs` when the current stack supports `exploratory-qa`
25
+ - `exploratory-prds`
26
+
27
+ Resolve the expected project identifier, fleet naming prefix, queue arguments, cadence, and stack-support rules from the same contract used by `setup-automations` and `tear-down-automations`. Do **not** invent a second source of truth for fleet naming or queue resolution.
28
+
29
+ ## Runtime inspection
30
+
31
+ Branch on the active runtime and prefer the runtime's native automation listing surface:
32
+
33
+ - **Codex**: inspect Codex automations metadata first. Use backing-store files only as a fallback or to enrich timestamps when the runtime surface cannot provide enough status detail directly.
34
+ - **Claude**: inspect the `/schedule` listing surface and any exposed recency or failure metadata available there.
35
+ - **Other runtimes**: if no native recurring-task inspection exists, report that automation-status is unsupported in this runtime rather than guessing.
36
+
37
+ The report must stay repo-scoped: inspect only automations whose names belong to the current repo's Lisa fleet prefix, and do not absorb unrelated automations into the result.
38
+
39
+ ## What to report
40
+
41
+ For each expected automation, report:
42
+
43
+ 1. Whether it exists.
44
+ 2. Whether it is expected or intentionally unsupported.
45
+ 3. The configured command and cadence Lisa expects.
46
+ 4. Any detected drift in name, cadence, command shape, or queue arguments.
47
+ 5. Any available recent-run health signal such as stale last-run timing or repeated failure status.
48
+ 6. A concise remediation hint when attention is needed.
49
+
50
+ Emit an overall grouped fleet verdict such as `HEALTHY`, `ATTENTION_NEEDED`, or `PARTIAL_SUPPORT`, plus the runtime surface inspected.
51
+
52
+ Render the report in grouped sections using the shared `scripts/automation-status-report.mjs` contract:
53
+
54
+ ```text
55
+ Overall verdict: <VERDICT>
56
+ Counts: <n HEALTHY>, <n MISSING>, <n UNSUPPORTED>, <n DRIFTED>, <n STALE>, <n FAILING>
57
+ Runtime inspected: <runtime surface>
58
+ Generated at: <ISO timestamp>
59
+
60
+ 1. <group title>
61
+ - <STATUS> <automation-id>: <summary>
62
+ Expected: <cadence> -> <command>
63
+ Observed: <what the runtime exposed>
64
+ Remediation: <next step when attention is needed>
65
+ ```
66
+
67
+ Keep observable runtime facts separate from remediation guidance so operators can distinguish drift, unsupported jobs, and actual failures quickly.
68
+
69
+ ## Rules
70
+
71
+ - Stay **read-only**. Never create, update, delete, enable, disable, or rerun automations from this skill.
72
+ - Reuse `setup-automations` contract logic for expected fleet resolution, cadence, queue arguments, naming, and stack-specific support checks.
73
+ - Distinguish **unsupported** from **missing**. An exploratory job omitted because the current stack lacks `exploratory-qa` is not a failure.
74
+ - If the runtime cannot expose a field such as last-run timestamp or failure state, say that explicitly instead of implying health.
75
+ - Keep the output operational and repo-scoped so operators can tell whether Lisa's unattended surfaces are present, current, and healthy right now.
@@ -0,0 +1,4 @@
1
+ display_name: "Automation Status"
2
+ short_description: "Read-only operator surface for the current project's Lisa automation fleet"
3
+ default_prompt:
4
+ - "Use $automation-status: Read-only operator surface for the current project's Lisa automation fleet."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.85.2",
3
+ "version": "2.88.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: "Inspect the current project's Lisa automation fleet and report whether the expected recurring jobs exist, match Lisa's setup contract, and show healthy recent activity. Read-only by default."
3
+ ---
4
+
5
+ Use the /lisa:automation-status skill to inspect the current project's expected Lisa automations, compare them with the runtime's scheduler metadata, and report fleet health, drift, staleness, unsupported jobs, and remediation hints. $ARGUMENTS
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared automation-status expected-fleet helpers.
4
+ *
5
+ * This module resolves the same naming, queue arguments, cadence, and
6
+ * exploratory support decisions documented by `/lisa:setup-automations`, so
7
+ * automation-status and later runtime adapters do not invent a second source of
8
+ * truth.
9
+ */
10
+
11
+ export const AUTOMATION_EXPECTED_CADENCES = {
12
+ "intake-repair": {
13
+ human: "every 60 minutes",
14
+ rrule: "FREQ=HOURLY;INTERVAL=1",
15
+ },
16
+ "intake-prd": {
17
+ human: "every 60 minutes",
18
+ rrule: "FREQ=HOURLY;INTERVAL=1",
19
+ },
20
+ "intake-tickets": {
21
+ human: "every 10 minutes",
22
+ rrule: "FREQ=MINUTELY;INTERVAL=10",
23
+ },
24
+ "exploratory-bugs": {
25
+ human: "once a day",
26
+ rrule: "FREQ=DAILY;INTERVAL=1",
27
+ },
28
+ "exploratory-prds": {
29
+ human: "once a day",
30
+ rrule: "FREQ=DAILY;INTERVAL=1",
31
+ },
32
+ };
33
+
34
+ export const EXPLORATORY_QA_STACK_PRIORITY = ["expo", "rails", "harper-fabric"];
35
+
36
+ const GITHUB_REMOTE_PATTERNS = [
37
+ /github\.com[:/](?<owner>[^/]+)\/(?<repo>[^/.]+?)(?:\.git)?$/,
38
+ /^git@github\.com:(?<owner>[^/]+)\/(?<repo>[^/.]+?)(?:\.git)?$/,
39
+ ];
40
+
41
+ /**
42
+ * @typedef {{
43
+ * readonly id: string
44
+ * readonly automationId: string
45
+ * readonly expectedCadence: string
46
+ * readonly expectedRRule: string
47
+ * readonly expectedCommand: string
48
+ * readonly group: "core" | "exploratory"
49
+ * }} ExpectedAutomationEntry
50
+ *
51
+ * @typedef {{
52
+ * readonly id: string
53
+ * readonly automationId: string
54
+ * readonly group: "core" | "exploratory"
55
+ * readonly reason: string
56
+ * readonly expectedCadence: string
57
+ * readonly expectedRRule: string
58
+ * }} UnsupportedAutomationEntry
59
+ */
60
+
61
+ /**
62
+ * Resolve the stable project identifier and automation prefix used by
63
+ * `/lisa:setup-automations`.
64
+ *
65
+ * @param {{
66
+ * readonly config?: Record<string, any>
67
+ * readonly gitRemoteUrl?: string
68
+ * }} input
69
+ * @returns {{ readonly owner: string, readonly repo: string, readonly project: string, readonly automationPrefix: string }}
70
+ */
71
+ export function resolveAutomationProjectIdentity(input = {}) {
72
+ const githubRef = resolveGithubRepoRef(
73
+ input.config ?? {},
74
+ input.gitRemoteUrl
75
+ );
76
+
77
+ if (!githubRef) {
78
+ throw new Error(
79
+ "Unable to resolve repo identity for automation naming. Configure github.org/github.repo or provide a GitHub origin remote."
80
+ );
81
+ }
82
+
83
+ const project = slugifyProjectToken(`${githubRef.owner}-${githubRef.repo}`);
84
+
85
+ return {
86
+ ...githubRef,
87
+ project,
88
+ automationPrefix: `lisa-auto-${project}-`,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Resolve the expected Lisa automation fleet for the current repo.
94
+ *
95
+ * @param {{
96
+ * readonly config?: Record<string, any>
97
+ * readonly gitRemoteUrl?: string
98
+ * readonly detectedTypes?: readonly string[]
99
+ * readonly autoStartPrds?: boolean | string
100
+ * readonly autoStartTickets?: boolean | string
101
+ * }} input
102
+ * @returns {{
103
+ * readonly owner: string
104
+ * readonly repo: string
105
+ * readonly project: string
106
+ * readonly automationPrefix: string
107
+ * readonly expected: readonly ExpectedAutomationEntry[]
108
+ * readonly unsupported: readonly UnsupportedAutomationEntry[]
109
+ * }}
110
+ */
111
+ export function resolveExpectedAutomationFleet(input = {}) {
112
+ const config = input.config ?? {};
113
+ const identity = resolveAutomationProjectIdentity(input);
114
+ const autoStartPrds = normalizeBooleanFlag(input.autoStartPrds);
115
+ const autoStartTickets = normalizeBooleanFlag(input.autoStartTickets);
116
+ const detectedTypes = input.detectedTypes ?? [];
117
+
118
+ const tracker = config.tracker;
119
+ const source = resolvePrdSource(config);
120
+ const prdQueue = resolvePrdQueueArgument(config, source);
121
+ const buildQueue = resolveBuildQueueArgument(config, tracker);
122
+ const repairQueue = resolveRepairQueueArgument(config, source, tracker);
123
+
124
+ const expected = [
125
+ createExpectedEntry(
126
+ identity,
127
+ "intake-repair",
128
+ `/lisa:repair-intake ${repairQueue}`,
129
+ "core"
130
+ ),
131
+ createExpectedEntry(
132
+ identity,
133
+ "intake-prd",
134
+ `/lisa:intake ${prdQueue}`,
135
+ "core"
136
+ ),
137
+ createExpectedEntry(
138
+ identity,
139
+ "intake-tickets",
140
+ `/lisa:intake ${buildQueue}`,
141
+ "core"
142
+ ),
143
+ createExpectedEntry(
144
+ identity,
145
+ "exploratory-prds",
146
+ `/lisa:project-ideation prd_ready=${String(autoStartPrds)}`,
147
+ "exploratory"
148
+ ),
149
+ ];
150
+
151
+ const exploratoryStack = resolveExploratoryQaStack(detectedTypes);
152
+ const unsupported = [];
153
+
154
+ if (exploratoryStack) {
155
+ expected.push(
156
+ createExpectedEntry(
157
+ identity,
158
+ "exploratory-bugs",
159
+ `/lisa-${exploratoryStack}:exploratory-qa ready=${String(autoStartTickets)}`,
160
+ "exploratory"
161
+ )
162
+ );
163
+ } else {
164
+ unsupported.push(
165
+ createUnsupportedEntry(
166
+ identity,
167
+ "exploratory-bugs",
168
+ "This repository does not ship an exploratory-qa command surface."
169
+ )
170
+ );
171
+ }
172
+
173
+ return {
174
+ ...identity,
175
+ expected,
176
+ unsupported,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Return the supported exploratory-qa surface for the detected host stacks.
182
+ *
183
+ * @param {readonly string[]} detectedTypes
184
+ * @returns {string | null}
185
+ */
186
+ export function resolveExploratoryQaStack(detectedTypes = []) {
187
+ for (const stack of EXPLORATORY_QA_STACK_PRIORITY) {
188
+ if (detectedTypes.includes(stack)) {
189
+ return stack;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * @param {{ readonly automationPrefix: string }} identity
197
+ * @param {string} id
198
+ * @param {string} expectedCommand
199
+ * @param {"core" | "exploratory"} group
200
+ * @returns {ExpectedAutomationEntry}
201
+ */
202
+ function createExpectedEntry(identity, id, expectedCommand, group) {
203
+ const cadence = AUTOMATION_EXPECTED_CADENCES[id];
204
+ return {
205
+ id,
206
+ automationId: `${identity.automationPrefix}${id}`,
207
+ expectedCadence: cadence.human,
208
+ expectedRRule: cadence.rrule,
209
+ expectedCommand,
210
+ group,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * @param {{ readonly automationPrefix: string }} identity
216
+ * @param {string} id
217
+ * @param {string} reason
218
+ * @returns {UnsupportedAutomationEntry}
219
+ */
220
+ function createUnsupportedEntry(identity, id, reason) {
221
+ const cadence = AUTOMATION_EXPECTED_CADENCES[id];
222
+ return {
223
+ id,
224
+ automationId: `${identity.automationPrefix}${id}`,
225
+ expectedCadence: cadence.human,
226
+ expectedRRule: cadence.rrule,
227
+ group: "exploratory",
228
+ reason,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * @param {Record<string, any>} config
234
+ * @param {string | undefined} source
235
+ * @returns {string}
236
+ */
237
+ function resolvePrdQueueArgument(config, source) {
238
+ switch (source) {
239
+ case "github":
240
+ requireGithubRepo(config);
241
+ return "github intake_mode=prd";
242
+ case "linear":
243
+ requireLinearWorkspace(config);
244
+ return "linear";
245
+ case "notion": {
246
+ const databaseId = config.notion?.prdDatabaseId;
247
+ if (!databaseId) {
248
+ throw new Error(
249
+ "Unable to resolve the PRD queue: notion.prdDatabaseId is required when source=notion."
250
+ );
251
+ }
252
+ return databaseId;
253
+ }
254
+ case "confluence": {
255
+ const parentPageId = config.confluence?.parentPageId;
256
+ const spaceKey = config.confluence?.spaceKey;
257
+ if (!parentPageId && !spaceKey) {
258
+ throw new Error(
259
+ "Unable to resolve the PRD queue: confluence.parentPageId or confluence.spaceKey is required when source=confluence."
260
+ );
261
+ }
262
+ return parentPageId ?? spaceKey;
263
+ }
264
+ case "jira": {
265
+ const project = config.jira?.project;
266
+ if (!project) {
267
+ throw new Error(
268
+ "Unable to resolve the PRD queue: jira.project is required when source=jira."
269
+ );
270
+ }
271
+ return project;
272
+ }
273
+ default:
274
+ throw new Error(
275
+ "Unable to resolve the PRD queue from config. Set source or use tracker=github self-host with github.org/github.repo."
276
+ );
277
+ }
278
+ }
279
+
280
+ /**
281
+ * @param {Record<string, any>} config
282
+ * @param {string | undefined} tracker
283
+ * @returns {string}
284
+ */
285
+ function resolveBuildQueueArgument(config, tracker) {
286
+ switch (tracker) {
287
+ case "github":
288
+ requireGithubRepo(config);
289
+ return "github intake_mode=build";
290
+ case "linear":
291
+ requireLinearWorkspace(config);
292
+ return "linear";
293
+ case "jira": {
294
+ const project = config.jira?.project;
295
+ if (!project) {
296
+ throw new Error(
297
+ "Unable to resolve the build queue: jira.project is required when tracker=jira."
298
+ );
299
+ }
300
+ return project;
301
+ }
302
+ default:
303
+ throw new Error(
304
+ "Unable to resolve the build queue from config. tracker must be github, linear, or jira."
305
+ );
306
+ }
307
+ }
308
+
309
+ /**
310
+ * @param {Record<string, any>} config
311
+ * @param {string | undefined} source
312
+ * @param {string | undefined} tracker
313
+ * @returns {string}
314
+ */
315
+ function resolveRepairQueueArgument(config, source, tracker) {
316
+ if (tracker === "github" && source === "github") {
317
+ requireGithubRepo(config);
318
+ return "github intake_mode=both";
319
+ }
320
+
321
+ if (tracker === "linear" && source === "linear") {
322
+ requireLinearWorkspace(config);
323
+ return "linear";
324
+ }
325
+
326
+ if (tracker === "jira" && source === "jira") {
327
+ const project = config.jira?.project;
328
+ if (!project) {
329
+ throw new Error(
330
+ "Unable to resolve the repair queue: jira.project is required when tracker=jira and source=jira."
331
+ );
332
+ }
333
+ return project;
334
+ }
335
+
336
+ if (tracker === "github" && source === undefined) {
337
+ requireGithubRepo(config);
338
+ return "github intake_mode=both";
339
+ }
340
+
341
+ throw new Error(
342
+ `Unable to resolve a single repair-intake queue for tracker=${String(tracker)} and source=${String(source)} without guessing.`
343
+ );
344
+ }
345
+
346
+ /**
347
+ * @param {Record<string, any>} config
348
+ * @returns {string | undefined}
349
+ */
350
+ function resolvePrdSource(config) {
351
+ if (typeof config.source === "string" && config.source.length > 0) {
352
+ return config.source;
353
+ }
354
+
355
+ if (
356
+ config.tracker === "github" &&
357
+ config.github?.org &&
358
+ config.github?.repo
359
+ ) {
360
+ return "github";
361
+ }
362
+
363
+ return undefined;
364
+ }
365
+
366
+ /**
367
+ * @param {Record<string, any>} config
368
+ * @param {string | undefined} gitRemoteUrl
369
+ * @returns {{ readonly owner: string, readonly repo: string } | null}
370
+ */
371
+ function resolveGithubRepoRef(config, gitRemoteUrl) {
372
+ const owner = config.github?.org;
373
+ const repo = config.github?.repo;
374
+
375
+ if (owner && repo) {
376
+ return { owner, repo };
377
+ }
378
+
379
+ if (!gitRemoteUrl) {
380
+ return null;
381
+ }
382
+
383
+ for (const pattern of GITHUB_REMOTE_PATTERNS) {
384
+ const match = gitRemoteUrl.match(pattern);
385
+ if (match?.groups?.owner && match.groups.repo) {
386
+ return {
387
+ owner: match.groups.owner,
388
+ repo: match.groups.repo,
389
+ };
390
+ }
391
+ }
392
+
393
+ return null;
394
+ }
395
+
396
+ /**
397
+ * @param {Record<string, any>} config
398
+ */
399
+ function requireGithubRepo(config) {
400
+ if (!config.github?.org || !config.github?.repo) {
401
+ throw new Error(
402
+ "Unable to resolve the GitHub queue: github.org and github.repo are required."
403
+ );
404
+ }
405
+ }
406
+
407
+ /**
408
+ * @param {Record<string, any>} config
409
+ */
410
+ function requireLinearWorkspace(config) {
411
+ if (!config.linear?.workspace) {
412
+ throw new Error(
413
+ "Unable to resolve the Linear queue: linear.workspace is required."
414
+ );
415
+ }
416
+ }
417
+
418
+ /**
419
+ * @param {boolean | string | undefined} value
420
+ * @returns {boolean}
421
+ */
422
+ function normalizeBooleanFlag(value) {
423
+ if (typeof value === "boolean") {
424
+ return value;
425
+ }
426
+
427
+ if (typeof value === "string") {
428
+ return value.toLowerCase() === "true";
429
+ }
430
+
431
+ return false;
432
+ }
433
+
434
+ /**
435
+ * @param {string} value
436
+ * @returns {string}
437
+ */
438
+ function slugifyProjectToken(value) {
439
+ return value
440
+ .trim()
441
+ .toLowerCase()
442
+ .replace(/[^a-z0-9]+/g, "-")
443
+ .replace(/^-+|-+$/g, "");
444
+ }