patchrelay 0.68.5 → 0.68.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.68.5",
4
- "commit": "cafd8f502dee",
5
- "builtAt": "2026-05-16T20:40:41.007Z"
3
+ "version": "0.68.6",
4
+ "commit": "b47d248df282",
5
+ "builtAt": "2026-05-19T20:34:54.989Z"
6
6
  }
@@ -1,5 +1,13 @@
1
1
  import { buildInsertBindings, buildUpdateAssignments } from "./issue-upsert-columns.js";
2
2
  import { isoNow } from "./shared.js";
3
+ const CANCELED_OR_DUPLICATE_CHILD_PREDICATE = `
4
+ LOWER(TRIM(COALESCE(child.current_linear_state_type, ''))) NOT IN ('canceled', 'cancelled')
5
+ AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('duplicate', 'canceled', 'cancelled')
6
+ `;
7
+ const OPEN_CHILD_PREDICATE = `
8
+ LOWER(TRIM(COALESCE(child.current_linear_state_type, ''))) NOT IN ('completed', 'canceled', 'cancelled')
9
+ AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'completed', 'duplicate', 'canceled', 'cancelled')
10
+ `;
3
11
  export class IssueStore {
4
12
  connection;
5
13
  syncIssueSessionFromIssue;
@@ -292,6 +300,19 @@ export class IssueStore {
292
300
  `).all(projectId, parentLinearIssueId);
293
301
  return rows.map(mapIssueRow);
294
302
  }
303
+ listCanonicalChildIssues(projectId, parentLinearIssueId) {
304
+ const rows = this.connection.prepare(`
305
+ SELECT child.*
306
+ FROM issue_children edges
307
+ JOIN issues child
308
+ ON child.project_id = edges.project_id
309
+ AND child.linear_issue_id = edges.child_linear_issue_id
310
+ WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
311
+ AND ${CANCELED_OR_DUPLICATE_CHILD_PREDICATE}
312
+ ORDER BY COALESCE(child.issue_key, child.linear_issue_id) ASC
313
+ `).all(projectId, parentLinearIssueId);
314
+ return rows.map(mapIssueRow);
315
+ }
295
316
  countOpenChildIssues(projectId, parentLinearIssueId) {
296
317
  const row = this.connection.prepare(`
297
318
  SELECT COUNT(*) AS count
@@ -302,10 +323,7 @@ export class IssueStore {
302
323
  WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
303
324
  AND (
304
325
  child.linear_issue_id IS NULL
305
- OR (
306
- COALESCE(child.current_linear_state_type, '') NOT IN ('completed', 'canceled')
307
- AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'duplicate', 'canceled')
308
- )
326
+ OR (${OPEN_CHILD_PREDICATE})
309
327
  )
310
328
  `).get(projectId, parentLinearIssueId);
311
329
  return Number(row?.count ?? 0);
package/dist/db.js CHANGED
@@ -163,6 +163,9 @@ export class PatchRelayDatabase {
163
163
  listChildIssues(projectId, parentLinearIssueId) {
164
164
  return this.issues.listChildIssues(projectId, parentLinearIssueId);
165
165
  }
166
+ listCanonicalChildIssues(projectId, parentLinearIssueId) {
167
+ return this.issues.listCanonicalChildIssues(projectId, parentLinearIssueId);
168
+ }
166
169
  countOpenChildIssues(projectId, parentLinearIssueId) {
167
170
  return this.issues.countOpenChildIssues(projectId, parentLinearIssueId);
168
171
  }
@@ -1,3 +1,26 @@
1
+ function hasExplicitNoCodePlanningSplitIntent(issue) {
2
+ const text = [issue.title, issue.description].filter(Boolean).join("\n").toLowerCase();
3
+ if (!text.trim())
4
+ return false;
5
+ const noCodePlanning = [
6
+ /\bno code\b/,
7
+ /\bcode (?:is )?not (?:needed|required|part of this)\b/,
8
+ /\bplanning only\b/,
9
+ /\banalysis only\b/,
10
+ /код[^\n.]{0,80}не делаем/,
11
+ /без (?:изменени[яй]|правок) код[а]?/,
12
+ /только анализ/,
13
+ /только планирован/,
14
+ ].some((pattern) => pattern.test(text));
15
+ if (!noCodePlanning)
16
+ return false;
17
+ return [
18
+ /\b(?:create|open|file|add|split|decompose|break down)[^\n.]{0,100}\b(?:child issues|follow-?up issues|issues|tickets|tasks)\b/,
19
+ /\b(?:child issues|follow-?up issues|issues|tickets|tasks)[^\n.]{0,100}\b(?:create|open|file|add|split|decompose|break down)\b/,
20
+ /(?:поставь|создай|заведи|добавь|разбей)[^\n.]{0,100}(?:задач|тикет|issue)/,
21
+ /(?:задач|тикет|issue)[^\n.]{0,100}(?:поставь|создай|заведи|добавь|разбей)/,
22
+ ].some((pattern) => pattern.test(text));
23
+ }
1
24
  export function classifyIssue(params) {
2
25
  if (params.issue.parentLinearIssueId) {
3
26
  return { issueClass: "implementation", issueClassSource: "hierarchy" };
@@ -14,5 +37,8 @@ export function classifyIssue(params) {
14
37
  if (params.issue.issueClassSource === "triage" && params.issue.issueClass) {
15
38
  return { issueClass: params.issue.issueClass, issueClassSource: "triage" };
16
39
  }
40
+ if (hasExplicitNoCodePlanningSplitIntent(params.issue)) {
41
+ return { issueClass: "orchestration", issueClassSource: "heuristic" };
42
+ }
17
43
  return { issueClass: "implementation", issueClassSource: "heuristic" };
18
44
  }
@@ -6,7 +6,7 @@ export function computeOrchestrationSettleUntil(now = Date.now()) {
6
6
  function resolveOrchestrationIssueClass(db, issue) {
7
7
  return classifyIssue({
8
8
  issue,
9
- childIssueCount: db.issues.listChildIssues(issue.projectId, issue.linearIssueId).length,
9
+ childIssueCount: db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId).length,
10
10
  }).issueClass;
11
11
  }
12
12
  function unique(values) {
@@ -186,7 +186,7 @@ export class RunOrchestrator {
186
186
  ...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
187
187
  }));
188
188
  const childIssues = this.db.issues
189
- .listChildIssues(issue.projectId, issue.linearIssueId)
189
+ .listCanonicalChildIssues(issue.projectId, issue.linearIssueId)
190
190
  .map((entry) => ({
191
191
  linearIssueId: entry.linearIssueId,
192
192
  ...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
@@ -205,7 +205,7 @@ export class RunOrchestrator {
205
205
  };
206
206
  }
207
207
  async classifyTrackedIssue(issue) {
208
- const childIssues = this.db.issues.listChildIssues(issue.projectId, issue.linearIssueId);
208
+ const childIssues = this.db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId);
209
209
  const classification = classifyIssue({ issue, childIssueCount: childIssues.length });
210
210
  const triageHash = buildIssueTriageHash({ issue, childIssues });
211
211
  const triageCacheFresh = issue.issueClassSource === "triage" && issue.issueTriageHash === triageHash;
@@ -30,7 +30,7 @@ export class CommentWakeHandler {
30
30
  return;
31
31
  const issueClass = classifyIssue({
32
32
  issue,
33
- childIssueCount: this.db.issues.listChildIssues(project.id, normalized.issue.id).length,
33
+ childIssueCount: this.db.issues.listCanonicalChildIssues(project.id, normalized.issue.id).length,
34
34
  }).issueClass;
35
35
  const trimmedBody = normalized.comment.body.trim();
36
36
  const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
@@ -75,7 +75,7 @@ export class DesiredStageRecorder {
75
75
  terminal,
76
76
  currentState: existingIssue?.factoryState,
77
77
  });
78
- const childIssueCount = this.db.issues.listChildIssues(params.project.id, normalizedIssue.id).length;
78
+ const childIssueCount = this.db.issues.listCanonicalChildIssues(params.project.id, normalizedIssue.id).length;
79
79
  const classification = classifyIssue({
80
80
  issue: {
81
81
  issueClass: existingIssue?.issueClass,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.68.5",
3
+ "version": "0.68.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {