openplanr 1.2.8 → 1.3.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 (145) hide show
  1. package/README.md +7 -3
  2. package/dist/agents/task-parser.d.ts.map +1 -1
  3. package/dist/agents/task-parser.js +8 -34
  4. package/dist/agents/task-parser.js.map +1 -1
  5. package/dist/ai/prompts/system-prompts.d.ts +2 -2
  6. package/dist/ai/prompts/system-prompts.d.ts.map +1 -1
  7. package/dist/ai/prompts/system-prompts.js +7 -7
  8. package/dist/ai/schemas/ai-response-schemas.js +1 -1
  9. package/dist/ai/schemas/ai-response-schemas.js.map +1 -1
  10. package/dist/ai/types.d.ts.map +1 -1
  11. package/dist/ai/types.js +2 -0
  12. package/dist/ai/types.js.map +1 -1
  13. package/dist/cli/commands/backlog.d.ts +12 -0
  14. package/dist/cli/commands/backlog.d.ts.map +1 -1
  15. package/dist/cli/commands/backlog.js +88 -2
  16. package/dist/cli/commands/backlog.js.map +1 -1
  17. package/dist/cli/commands/config.d.ts.map +1 -1
  18. package/dist/cli/commands/config.js +8 -2
  19. package/dist/cli/commands/config.js.map +1 -1
  20. package/dist/cli/commands/linear.d.ts +8 -0
  21. package/dist/cli/commands/linear.d.ts.map +1 -0
  22. package/dist/cli/commands/linear.js +550 -0
  23. package/dist/cli/commands/linear.js.map +1 -0
  24. package/dist/cli/commands/quick.d.ts +17 -0
  25. package/dist/cli/commands/quick.d.ts.map +1 -1
  26. package/dist/cli/commands/quick.js +31 -15
  27. package/dist/cli/commands/quick.js.map +1 -1
  28. package/dist/cli/commands/revise.d.ts +9 -8
  29. package/dist/cli/commands/revise.d.ts.map +1 -1
  30. package/dist/cli/commands/revise.js +93 -25
  31. package/dist/cli/commands/revise.js.map +1 -1
  32. package/dist/cli/index.js +2 -0
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/models/schema.d.ts +43 -0
  35. package/dist/models/schema.d.ts.map +1 -1
  36. package/dist/models/schema.js +49 -0
  37. package/dist/models/schema.js.map +1 -1
  38. package/dist/models/types.d.ts +179 -3
  39. package/dist/models/types.d.ts.map +1 -1
  40. package/dist/services/artifact-gathering.d.ts +4 -0
  41. package/dist/services/artifact-gathering.d.ts.map +1 -1
  42. package/dist/services/artifact-gathering.js +1 -1
  43. package/dist/services/artifact-gathering.js.map +1 -1
  44. package/dist/services/artifact-service.d.ts +12 -1
  45. package/dist/services/artifact-service.d.ts.map +1 -1
  46. package/dist/services/artifact-service.js +49 -6
  47. package/dist/services/artifact-service.js.map +1 -1
  48. package/dist/services/atomic-write-service.d.ts +2 -2
  49. package/dist/services/atomic-write-service.js +2 -2
  50. package/dist/services/audit-log-service.d.ts +3 -6
  51. package/dist/services/audit-log-service.d.ts.map +1 -1
  52. package/dist/services/audit-log-service.js +4 -7
  53. package/dist/services/audit-log-service.js.map +1 -1
  54. package/dist/services/cascade-service.d.ts +2 -2
  55. package/dist/services/cascade-service.js +3 -3
  56. package/dist/services/cascade-service.js.map +1 -1
  57. package/dist/services/credentials-service.js +2 -2
  58. package/dist/services/credentials-service.js.map +1 -1
  59. package/dist/services/diff-service.d.ts +1 -1
  60. package/dist/services/diff-service.js +1 -1
  61. package/dist/services/evidence-verifier.d.ts +1 -1
  62. package/dist/services/evidence-verifier.d.ts.map +1 -1
  63. package/dist/services/evidence-verifier.js +5 -2
  64. package/dist/services/evidence-verifier.js.map +1 -1
  65. package/dist/services/git-service.d.ts +4 -4
  66. package/dist/services/git-service.js +4 -4
  67. package/dist/services/graph-integrity.d.ts +2 -3
  68. package/dist/services/graph-integrity.d.ts.map +1 -1
  69. package/dist/services/graph-integrity.js +2 -3
  70. package/dist/services/graph-integrity.js.map +1 -1
  71. package/dist/services/linear/body-formatters.d.ts +69 -0
  72. package/dist/services/linear/body-formatters.d.ts.map +1 -0
  73. package/dist/services/linear/body-formatters.js +183 -0
  74. package/dist/services/linear/body-formatters.js.map +1 -0
  75. package/dist/services/linear/constants.d.ts +61 -0
  76. package/dist/services/linear/constants.d.ts.map +1 -0
  77. package/dist/services/linear/constants.js +84 -0
  78. package/dist/services/linear/constants.js.map +1 -0
  79. package/dist/services/linear/errors.d.ts +14 -0
  80. package/dist/services/linear/errors.d.ts.map +1 -0
  81. package/dist/services/linear/errors.js +106 -0
  82. package/dist/services/linear/errors.js.map +1 -0
  83. package/dist/services/linear/estimate-resolver.d.ts +50 -0
  84. package/dist/services/linear/estimate-resolver.d.ts.map +1 -0
  85. package/dist/services/linear/estimate-resolver.js +82 -0
  86. package/dist/services/linear/estimate-resolver.js.map +1 -0
  87. package/dist/services/linear/plan-builders.d.ts +64 -0
  88. package/dist/services/linear/plan-builders.d.ts.map +1 -0
  89. package/dist/services/linear/plan-builders.js +237 -0
  90. package/dist/services/linear/plan-builders.js.map +1 -0
  91. package/dist/services/linear/scope-loaders.d.ts +79 -0
  92. package/dist/services/linear/scope-loaders.d.ts.map +1 -0
  93. package/dist/services/linear/scope-loaders.js +227 -0
  94. package/dist/services/linear/scope-loaders.js.map +1 -0
  95. package/dist/services/linear/strategy-context.d.ts +66 -0
  96. package/dist/services/linear/strategy-context.d.ts.map +1 -0
  97. package/dist/services/linear/strategy-context.js +121 -0
  98. package/dist/services/linear/strategy-context.js.map +1 -0
  99. package/dist/services/linear-mapping-service.d.ts +11 -0
  100. package/dist/services/linear-mapping-service.d.ts.map +1 -0
  101. package/dist/services/linear-mapping-service.js +220 -0
  102. package/dist/services/linear-mapping-service.js.map +1 -0
  103. package/dist/services/linear-pull-service.d.ts +137 -0
  104. package/dist/services/linear-pull-service.d.ts.map +1 -0
  105. package/dist/services/linear-pull-service.js +720 -0
  106. package/dist/services/linear-pull-service.js.map +1 -0
  107. package/dist/services/linear-push-service.d.ts +86 -0
  108. package/dist/services/linear-push-service.d.ts.map +1 -0
  109. package/dist/services/linear-push-service.js +956 -0
  110. package/dist/services/linear-push-service.js.map +1 -0
  111. package/dist/services/linear-service.d.ts +122 -0
  112. package/dist/services/linear-service.d.ts.map +1 -0
  113. package/dist/services/linear-service.js +361 -0
  114. package/dist/services/linear-service.js.map +1 -0
  115. package/dist/services/prompt-service.d.ts +19 -0
  116. package/dist/services/prompt-service.d.ts.map +1 -1
  117. package/dist/services/prompt-service.js +64 -0
  118. package/dist/services/prompt-service.js.map +1 -1
  119. package/dist/services/revise-apply-service.d.ts +55 -0
  120. package/dist/services/revise-apply-service.d.ts.map +1 -0
  121. package/dist/services/revise-apply-service.js +255 -0
  122. package/dist/services/revise-apply-service.js.map +1 -0
  123. package/dist/services/revise-cache-service.d.ts +1 -1
  124. package/dist/services/revise-cache-service.js +1 -1
  125. package/dist/services/revise-plan-service.d.ts +38 -0
  126. package/dist/services/revise-plan-service.d.ts.map +1 -0
  127. package/dist/services/revise-plan-service.js +151 -0
  128. package/dist/services/revise-plan-service.js.map +1 -0
  129. package/dist/services/revise-service.d.ts +18 -11
  130. package/dist/services/revise-service.d.ts.map +1 -1
  131. package/dist/services/revise-service.js +57 -12
  132. package/dist/services/revise-service.js.map +1 -1
  133. package/dist/services/template-sections.d.ts +1 -1
  134. package/dist/services/template-sections.js +1 -1
  135. package/dist/templates/backlog/backlog-item.md.hbs +3 -0
  136. package/dist/templates/quick/quick-task.md.hbs +6 -0
  137. package/dist/utils/diff.d.ts +22 -1
  138. package/dist/utils/diff.d.ts.map +1 -1
  139. package/dist/utils/diff.js +136 -1
  140. package/dist/utils/diff.js.map +1 -1
  141. package/dist/utils/markdown.d.ts +23 -0
  142. package/dist/utils/markdown.d.ts.map +1 -1
  143. package/dist/utils/markdown.js +79 -0
  144. package/dist/utils/markdown.js.map +1 -1
  145. package/package.json +3 -2
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Pure markdown body/description formatters for Linear issues and projects.
3
+ *
4
+ * Every function here takes local OpenPlanr artifact data and returns the
5
+ * string that goes into the Linear entity's `description` / `title` field.
6
+ * Stateless + side-effect free except for `buildMergedTaskListBody`, which
7
+ * reads task files via the artifact-service to assemble a feature's
8
+ * aggregated checkbox body.
9
+ */
10
+ import { parseTaskMarkdown } from '../../agents/task-parser.js';
11
+ import { readArtifact, readArtifactRaw } from '../artifact-service.js';
12
+ /** Convert an unknown frontmatter value to an optional string at the type boundary. */
13
+ export function toOptionalString(v) {
14
+ return typeof v === 'string' ? v : undefined;
15
+ }
16
+ /** Convert an unknown frontmatter value to an optional array of strings. */
17
+ export function toOptionalStringArray(v) {
18
+ if (!Array.isArray(v))
19
+ return undefined;
20
+ const out = v.filter((item) => typeof item === 'string');
21
+ return out.length > 0 ? out : undefined;
22
+ }
23
+ /** Epic → Linear Project `description` (markdown). Skips whitespace-only sections. */
24
+ export function buildEpicProjectDescription(epic) {
25
+ const lines = [];
26
+ const section = (label, value) => {
27
+ const trimmed = value?.trim();
28
+ if (trimmed)
29
+ lines.push(`**${label}**\n\n${trimmed}`);
30
+ };
31
+ section('Business value', epic.businessValue);
32
+ section('Problem', epic.problemStatement);
33
+ section('Solution', epic.solutionOverview);
34
+ section('Success criteria', epic.successCriteria);
35
+ section('Target users', epic.targetUsers);
36
+ section('Risks', epic.risks);
37
+ section('Dependencies', epic.dependencies);
38
+ return lines.join('\n\n');
39
+ }
40
+ /** Feature → Linear issue body (overview + functional requirements bullets). */
41
+ export function buildFeatureIssueBody(feature) {
42
+ const lines = [feature.overview?.trim() || ''];
43
+ if (feature.functionalRequirements?.length) {
44
+ lines.push('**Functional requirements**');
45
+ for (const r of feature.functionalRequirements) {
46
+ lines.push(`- ${r}`);
47
+ }
48
+ }
49
+ return lines.filter(Boolean).join('\n\n');
50
+ }
51
+ /**
52
+ * User story → Linear sub-issue body.
53
+ *
54
+ * Composes, in order:
55
+ * 1. The "As a / I want / So that" sentence — only when all three fields
56
+ * are present (otherwise rendering with blanks produces visible
57
+ * garbage in Linear).
58
+ * 2. The frontmatter `acceptanceCriteria` prose — if set.
59
+ * 3. The Gherkin scenarios from `<storyId>-gherkin.feature` — if the
60
+ * caller provides them. Stories in the OpenPlanr convention store
61
+ * their real acceptance criteria as Gherkin in a sibling `.feature`
62
+ * file; without this, the Linear issue was empty for every story
63
+ * that followed the convention.
64
+ */
65
+ export function buildStoryIssueBody(story, gherkinContent) {
66
+ const role = story.role?.trim();
67
+ const goal = story.goal?.trim();
68
+ const benefit = story.benefit?.trim();
69
+ const ac = story.acceptanceCriteria?.trim();
70
+ const gherkin = gherkinContent?.trim();
71
+ const hasFullUserStoryLine = Boolean(role && goal && benefit);
72
+ const sections = [];
73
+ if (hasFullUserStoryLine) {
74
+ sections.push(`As a **${role}**, I want **${goal}** so that **${benefit}**.`);
75
+ }
76
+ if (ac)
77
+ sections.push(`**Acceptance criteria**\n\n${ac}`);
78
+ if (gherkin)
79
+ sections.push(`**Gherkin scenarios**\n\n\`\`\`gherkin\n${gherkin}\n\`\`\``);
80
+ return sections.join('\n\n');
81
+ }
82
+ /** Render parsed task lines to markdown checkboxes (Linear description). */
83
+ export function formatTaskCheckboxBody(parsed) {
84
+ if (parsed.length === 0)
85
+ return '';
86
+ return parsed
87
+ .map((p) => {
88
+ const mark = p.done ? 'x' : ' ';
89
+ if (p.depth === 0) {
90
+ return `- [${mark}] **${p.id}** ${p.title}`;
91
+ }
92
+ return ` - [${mark}] ${p.id} ${p.title}`;
93
+ })
94
+ .join('\n');
95
+ }
96
+ /**
97
+ * Build a merged task-list body for a feature — concatenates every task
98
+ * artifact whose `featureId` matches, parses its checkboxes, renders them,
99
+ * and (when multiple files exist) prefixes each section with its task id
100
+ * as an `## h2`. Returns `''` when there's nothing to sync.
101
+ */
102
+ export async function buildMergedTaskListBody(projectDir, config, featureId, taskFiles) {
103
+ const sections = [];
104
+ const sorted = [...taskFiles].sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
105
+ for (const tf of sorted) {
106
+ const raw = await readArtifactRaw(projectDir, config, 'task', tf.id);
107
+ if (!raw)
108
+ continue;
109
+ const data = (await readArtifact(projectDir, config, 'task', tf.id))?.data;
110
+ const fId = toOptionalString(data?.featureId);
111
+ if (fId !== featureId)
112
+ continue;
113
+ const parsed = parseTaskMarkdown(raw);
114
+ if (parsed.length === 0)
115
+ continue;
116
+ const body = formatTaskCheckboxBody(parsed);
117
+ if (taskFiles.length > 1) {
118
+ sections.push(`## ${tf.id}\n\n${body}`);
119
+ }
120
+ else {
121
+ sections.push(body);
122
+ }
123
+ }
124
+ return sections.join('\n\n');
125
+ }
126
+ /**
127
+ * Extract the markdown body of a standalone artifact (QT / BL) for pushing
128
+ * to Linear as an issue description.
129
+ *
130
+ * Strips:
131
+ * - the frontmatter block (YAML between the `---` markers)
132
+ * - the top-level `# <ID>: <title>` heading (Linear shows the title
133
+ * separately, so repeating it in the description is noise)
134
+ *
135
+ * Everything else — prose, sub-headings, checkbox lists — is preserved
136
+ * verbatim. Linear renders standard markdown, so checkboxes stay checkboxes,
137
+ * `## sections` stay sections, links stay clickable.
138
+ */
139
+ export function buildStandaloneArtifactBody(raw, id) {
140
+ // Strip frontmatter if present.
141
+ let body = raw;
142
+ const fmMatch = /^---[^\S\r\n]*\r?\n[\s\S]*?\r?\n---[^\S\r\n]*\r?\n?/.exec(raw);
143
+ if (fmMatch) {
144
+ body = raw.slice(fmMatch[0].length);
145
+ }
146
+ // Drop leading blank lines — markdown files typically have a blank line
147
+ // between the frontmatter's closing `---` and the first `#` heading.
148
+ body = body.replace(/^\s*\r?\n/, '').trimStart();
149
+ // Strip a single top-level `# <ID>:...` or `# <anything>` heading at the
150
+ // start of the body, plus the blank line that typically follows it.
151
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
152
+ const titleHeadingRegex = new RegExp(`^#\\s+(?:${escapedId}:\\s*)?.*\\r?\\n(?:\\r?\\n)?`);
153
+ body = body.replace(titleHeadingRegex, '');
154
+ return body.trimEnd();
155
+ }
156
+ /**
157
+ * Backlog item → Linear issue body. Priority + tags + description + optional
158
+ * acceptance criteria + notes. Accepts the generic frontmatter record shape
159
+ * because backlog items aren't currently loaded via a typed interface.
160
+ */
161
+ export function buildBacklogItemBody(bl) {
162
+ const fm = bl.frontmatter;
163
+ const lines = [];
164
+ const priority = toOptionalString(fm.priority);
165
+ if (priority)
166
+ lines.push(`**Priority:** ${priority}`);
167
+ if (Array.isArray(fm.tags) && fm.tags.length > 0) {
168
+ const tags = fm.tags.filter((t) => typeof t === 'string');
169
+ if (tags.length)
170
+ lines.push(`**Tags:** ${tags.join(', ')}`);
171
+ }
172
+ const description = toOptionalString(fm.description);
173
+ if (description)
174
+ lines.push(description.trim());
175
+ const ac = toOptionalString(fm.acceptanceCriteria);
176
+ if (ac)
177
+ lines.push(`**Acceptance criteria**\n\n${ac.trim()}`);
178
+ const notes = toOptionalString(fm.notes);
179
+ if (notes)
180
+ lines.push(`**Notes**\n\n${notes.trim()}`);
181
+ return lines.join('\n\n');
182
+ }
183
+ //# sourceMappingURL=body-formatters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-formatters.js","sourceRoot":"","sources":["../../../src/services/linear/body-formatters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Linear integration constants + id-shape validators + input-safety helpers.
3
+ *
4
+ * All field-limit values here are enforced at the SDK-wrapper layer in
5
+ * `src/services/linear-service.ts` so that every caller gets the guard for
6
+ * free. Shape validators (`isLikelyLinear*Id`) fence off stale frontmatter
7
+ * before it reaches the Linear API.
8
+ */
9
+ /** Credential-service provider key under which the Linear PAT is stored. */
10
+ export declare const LINEAR_CREDENTIAL_KEY: "linear";
11
+ /**
12
+ * Linear's backend enforces per-field length limits on every create/update
13
+ * mutation. Defend at the SDK-wrapper layer so callers don't have to think
14
+ * about them. Names / titles: confirmed or best-known caps; descriptions:
15
+ * conservative floors well under Linear's real ceilings (markdown + HTML
16
+ * are both accepted; real limits are in the tens of thousands).
17
+ */
18
+ export declare const LINEAR_FIELD_LIMITS: {
19
+ /** ProjectMilestone.name — confirmed 80 by `Argument Validation Error`. */
20
+ readonly milestoneName: 80;
21
+ /** IssueLabel.name — Linear team labels cap ~64 chars. */
22
+ readonly labelName: 64;
23
+ /** Project.name — generous cap. */
24
+ readonly projectName: 256;
25
+ /** Issue.title — Linear issue title cap ~255. */
26
+ readonly issueTitle: 255;
27
+ /** Project.description — conservative floor; real Linear ceiling is higher. */
28
+ readonly projectDescription: 50000;
29
+ /** ProjectMilestone.description. */
30
+ readonly milestoneDescription: 50000;
31
+ /** IssueLabel.description — labels rarely need long descriptions. */
32
+ readonly labelDescription: 500;
33
+ /** Issue.description (markdown body). */
34
+ readonly issueDescription: 65000;
35
+ };
36
+ /**
37
+ * Truncate a string to Linear's character limit for a given field. Logs a
38
+ * warning on truncation so the operator can spot it in the push output.
39
+ */
40
+ export declare function truncateForLinear(value: string, maxLen: number, fieldLabel: string): string;
41
+ /**
42
+ * Non-empty guard for required Linear name/title fields. Fails fast with an
43
+ * actionable message before the API would reject the call.
44
+ */
45
+ export declare function requireNonEmpty(value: string | null | undefined, fieldLabel: string): string;
46
+ /**
47
+ * Heuristic: Linear workflow state id (uuid) vs human-readable state name.
48
+ * The `/i` flag is intentional — Linear's API canonicalizes UUIDs to
49
+ * lowercase, but defensive acceptance of uppercase hex matches RFC 4122
50
+ * and protects against tools that normalize differently.
51
+ */
52
+ export declare function isLikelyLinearWorkflowStateId(s: string): boolean;
53
+ /**
54
+ * Validate that a value plausibly identifies a Linear issue. Two valid shapes:
55
+ * 1. UUIDv4 (e.g. `9b2f4c3e-...`) — canonical API form
56
+ * 2. Linear identifier (e.g. `ENG-42`) — human-readable, also accepted by `client.issue()`
57
+ * Anything else is treated as stale/corrupted frontmatter and skipped before
58
+ * hitting the API.
59
+ */
60
+ export declare function isLikelyLinearIssueId(s: string): boolean;
61
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/services/linear/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,EAAG,QAAiB,CAAC;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;IAC9B,2EAA2E;;IAE3E,0DAA0D;;IAE1D,mCAAmC;;IAEnC,iDAAiD;;IAEjD,+EAA+E;;IAE/E,oCAAoC;;IAEpC,qEAAqE;;IAErE,yCAAyC;;CAEjC,CAAC;AAEX;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ3F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ5F;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAIhE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAKxD"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Linear integration constants + id-shape validators + input-safety helpers.
3
+ *
4
+ * All field-limit values here are enforced at the SDK-wrapper layer in
5
+ * `src/services/linear-service.ts` so that every caller gets the guard for
6
+ * free. Shape validators (`isLikelyLinear*Id`) fence off stale frontmatter
7
+ * before it reaches the Linear API.
8
+ */
9
+ import { logger } from '../../utils/logger.js';
10
+ /** Credential-service provider key under which the Linear PAT is stored. */
11
+ export const LINEAR_CREDENTIAL_KEY = 'linear';
12
+ /**
13
+ * Linear's backend enforces per-field length limits on every create/update
14
+ * mutation. Defend at the SDK-wrapper layer so callers don't have to think
15
+ * about them. Names / titles: confirmed or best-known caps; descriptions:
16
+ * conservative floors well under Linear's real ceilings (markdown + HTML
17
+ * are both accepted; real limits are in the tens of thousands).
18
+ */
19
+ export const LINEAR_FIELD_LIMITS = {
20
+ /** ProjectMilestone.name — confirmed 80 by `Argument Validation Error`. */
21
+ milestoneName: 80,
22
+ /** IssueLabel.name — Linear team labels cap ~64 chars. */
23
+ labelName: 64,
24
+ /** Project.name — generous cap. */
25
+ projectName: 256,
26
+ /** Issue.title — Linear issue title cap ~255. */
27
+ issueTitle: 255,
28
+ /** Project.description — conservative floor; real Linear ceiling is higher. */
29
+ projectDescription: 50_000,
30
+ /** ProjectMilestone.description. */
31
+ milestoneDescription: 50_000,
32
+ /** IssueLabel.description — labels rarely need long descriptions. */
33
+ labelDescription: 500,
34
+ /** Issue.description (markdown body). */
35
+ issueDescription: 65_000,
36
+ };
37
+ /**
38
+ * Truncate a string to Linear's character limit for a given field. Logs a
39
+ * warning on truncation so the operator can spot it in the push output.
40
+ */
41
+ export function truncateForLinear(value, maxLen, fieldLabel) {
42
+ const trimmed = value.trim();
43
+ if (trimmed.length <= maxLen)
44
+ return trimmed;
45
+ const truncated = trimmed.slice(0, maxLen);
46
+ logger.warn(`${fieldLabel} truncated from ${trimmed.length} → ${maxLen} chars to satisfy Linear's limit.`);
47
+ return truncated;
48
+ }
49
+ /**
50
+ * Non-empty guard for required Linear name/title fields. Fails fast with an
51
+ * actionable message before the API would reject the call.
52
+ */
53
+ export function requireNonEmpty(value, fieldLabel) {
54
+ const trimmed = typeof value === 'string' ? value.trim() : '';
55
+ if (!trimmed) {
56
+ throw new Error(`${fieldLabel} is empty — Linear requires a non-empty value. Add a title to the OpenPlanr artifact and re-run.`);
57
+ }
58
+ return trimmed;
59
+ }
60
+ /**
61
+ * Heuristic: Linear workflow state id (uuid) vs human-readable state name.
62
+ * The `/i` flag is intentional — Linear's API canonicalizes UUIDs to
63
+ * lowercase, but defensive acceptance of uppercase hex matches RFC 4122
64
+ * and protects against tools that normalize differently.
65
+ */
66
+ export function isLikelyLinearWorkflowStateId(s) {
67
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s.trim());
68
+ }
69
+ /**
70
+ * Validate that a value plausibly identifies a Linear issue. Two valid shapes:
71
+ * 1. UUIDv4 (e.g. `9b2f4c3e-...`) — canonical API form
72
+ * 2. Linear identifier (e.g. `ENG-42`) — human-readable, also accepted by `client.issue()`
73
+ * Anything else is treated as stale/corrupted frontmatter and skipped before
74
+ * hitting the API.
75
+ */
76
+ export function isLikelyLinearIssueId(s) {
77
+ const trimmed = s.trim();
78
+ if (trimmed.length === 0)
79
+ return false;
80
+ if (isLikelyLinearWorkflowStateId(trimmed))
81
+ return true;
82
+ return /^[A-Z]{2,}-\d+$/.test(trimmed);
83
+ }
84
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/services/linear/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/C,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,QAAiB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,2EAA2E;IAC3E,aAAa,EAAE,EAAE;IACjB,0DAA0D;IAC1D,SAAS,EAAE,EAAE;IACb,mCAAmC;IACnC,WAAW,EAAE,GAAG;IAChB,iDAAiD;IACjD,UAAU,EAAE,GAAG;IACf,+EAA+E;IAC/E,kBAAkB,EAAE,MAAM;IAC1B,oCAAoC;IACpC,oBAAoB,EAAE,MAAM;IAC5B,qEAAqE;IACrE,gBAAgB,EAAE,GAAG;IACrB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM;CAChB,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAc,EAAE,UAAkB;IACjF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,MAAM,IAAI,MAAM;QAAE,OAAO,OAAO,CAAC;IAC7C,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3C,MAAM,CAAC,IAAI,CACT,GAAG,UAAU,mBAAmB,OAAO,CAAC,MAAM,MAAM,MAAM,mCAAmC,CAC9F,CAAC;IACF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAgC,EAAE,UAAkB;IAClF,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,GAAG,UAAU,kGAAkG,CAChH,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAAC,CAAS;IACrD,OAAO,4EAA4E,CAAC,IAAI,CACtF,CAAC,CAAC,IAAI,EAAE,CACT,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,CAAS;IAC7C,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,6BAA6B,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACxD,OAAO,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Linear error classification, user-facing mapping, and retry wrapper.
3
+ *
4
+ * Two responsibilities:
5
+ * 1. Map raw SDK errors to user-friendly messages (branded guidance for
6
+ * auth / network / rate-limit; surfaced friendly SDK message for other
7
+ * classified types; sanitized fallback for unclassified errors).
8
+ * 2. Wrap Linear calls with small exponential backoff that honours
9
+ * `RatelimitedLinearError.retryAfter`.
10
+ */
11
+ /** Wraps a Linear call with small exponential backoff on rate limit / network errors. */
12
+ export declare function withLinearRetry<T>(op: string, fn: () => Promise<T>, retries?: number): Promise<T>;
13
+ export declare function mapLinearError(err: unknown, context: string): Error;
14
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/services/linear/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgCH,yFAAyF;AACzF,wBAAsB,eAAe,CAAC,CAAC,EACrC,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE,MAAwB,GAChC,OAAO,CAAC,CAAC,CAAC,CA4BZ;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CA6CnE"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Linear error classification, user-facing mapping, and retry wrapper.
3
+ *
4
+ * Two responsibilities:
5
+ * 1. Map raw SDK errors to user-friendly messages (branded guidance for
6
+ * auth / network / rate-limit; surfaced friendly SDK message for other
7
+ * classified types; sanitized fallback for unclassified errors).
8
+ * 2. Wrap Linear calls with small exponential backoff that honours
9
+ * `RatelimitedLinearError.retryAfter`.
10
+ */
11
+ import { LinearError, LinearErrorType, RatelimitedLinearError } from '@linear/sdk';
12
+ import { logger } from '../../utils/logger.js';
13
+ const DEFAULT_RETRIES = 3;
14
+ /**
15
+ * Extract the first user-facing message from a LinearError. The SDK exposes
16
+ * per-GraphQL-error `.message` strings on `errors[]` — these are backend-
17
+ * authored user descriptions (e.g., "Milestone already exists with this
18
+ * name"). Falls back to `err.message` and finally to the error `type`.
19
+ * Safe to surface — this is not the raw query/variables.
20
+ */
21
+ function extractLinearFriendlyMessage(err) {
22
+ const first = err.errors?.[0]?.message?.trim();
23
+ if (first)
24
+ return first;
25
+ const top = err.message?.trim();
26
+ if (top)
27
+ return top;
28
+ const type = err.type;
29
+ return type || undefined;
30
+ }
31
+ function isRetriableLinearError(err) {
32
+ if (err instanceof LinearError) {
33
+ const t = err.type ?? LinearErrorType.Unknown;
34
+ return t === LinearErrorType.Ratelimited || t === LinearErrorType.NetworkError;
35
+ }
36
+ if (err instanceof Error && err.name === 'AbortError')
37
+ return true;
38
+ return false;
39
+ }
40
+ /** Wraps a Linear call with small exponential backoff on rate limit / network errors. */
41
+ export async function withLinearRetry(op, fn, retries = DEFAULT_RETRIES) {
42
+ let last;
43
+ for (let attempt = 0; attempt <= retries; attempt++) {
44
+ try {
45
+ return await fn();
46
+ }
47
+ catch (err) {
48
+ last = err;
49
+ if (attempt < retries && isRetriableLinearError(err)) {
50
+ // Prefer Linear's own `Retry-After` when the error is a rate-limit
51
+ // (surfaced on the `RatelimitedLinearError` subclass as a seconds
52
+ // value). Fall back to exponential backoff for network errors and
53
+ // when the server didn't advertise a retry hint. Use `Math.max` so
54
+ // we respect both: never retry sooner than Linear asked, never
55
+ // faster than our own backoff schedule.
56
+ const retryAfterMs = err instanceof RatelimitedLinearError && typeof err.retryAfter === 'number'
57
+ ? Math.max(0, err.retryAfter) * 1000
58
+ : 0;
59
+ const backoffMs = Math.min(30_000, 500 * 2 ** attempt);
60
+ const waitMs = Math.max(retryAfterMs, backoffMs);
61
+ logger.dim(`Linear ${op}: retrying in ${waitMs}ms (attempt ${attempt + 1}/${retries})...`);
62
+ await new Promise((r) => setTimeout(r, waitMs));
63
+ continue;
64
+ }
65
+ throw mapLinearError(err, op);
66
+ }
67
+ }
68
+ throw mapLinearError(last, op);
69
+ }
70
+ export function mapLinearError(err, context) {
71
+ if (err instanceof Error && err.name === 'AbortError') {
72
+ return new Error(`Network error while ${context}: request was cancelled or timed out.`);
73
+ }
74
+ if (err instanceof LinearError) {
75
+ const t = err.type ?? LinearErrorType.Unknown;
76
+ if (t === LinearErrorType.AuthenticationError) {
77
+ return new Error(`Linear rejected this token while ${context}. Create a new PAT at https://linear.app/settings/account/security (app, read, write as needed) and run \`planr linear init\` again.`);
78
+ }
79
+ if (t === LinearErrorType.NetworkError) {
80
+ return new Error(`Cannot reach Linear while ${context}. Check your network connection, try again, and see https://status.linear.app for outages.`);
81
+ }
82
+ if (t === LinearErrorType.Ratelimited) {
83
+ return new Error('Linear rate limit reached. Wait about 1–2 minutes (longer if you are polling heavily), then retry. See https://status.linear.app if issues persist.');
84
+ }
85
+ // For other classified LinearError types (Forbidden, InvalidInput, UserError,
86
+ // FeatureNotAccessible, Internal, LockTimeout, UsageLimitExceeded, Graphql,
87
+ // Other, Bootstrap, Unknown) surface the SDK's friendly .message string —
88
+ // backend-authored user descriptions like "Milestone name already exists
89
+ // in project" or "Input invalid: projectId". Also log the full object at
90
+ // debug level for --verbose diagnostics.
91
+ logger.debug(`Linear error (${context}, type=${t})`, err);
92
+ const friendly = extractLinearFriendlyMessage(err);
93
+ const suffix = friendly ? `: ${friendly}` : '';
94
+ if (t === LinearErrorType.Forbidden) {
95
+ return new Error(`Permission denied while ${context}${suffix}. Your token may be missing the required scope, or your user cannot access this resource.`);
96
+ }
97
+ return new Error(`Linear error while ${context} (${t})${suffix}`);
98
+ }
99
+ // Unknown / unclassified error class: log the full object at debug level so
100
+ // operators can inspect it with `--verbose`, but do NOT surface the raw
101
+ // message to end users — could contain arbitrary response bodies.
102
+ logger.debug(`Linear error (${context})`, err);
103
+ const klass = err instanceof Error ? err.constructor.name : 'Unknown';
104
+ return new Error(`Linear error while ${context} (${klass}). Re-run with --verbose for diagnostic details.`);
105
+ }
106
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/services/linear/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Scale-aware snapping from OpenPlanr `storyPoints` → Linear `estimate` field.
3
+ *
4
+ * Linear's estimate field is numeric and must match the team's configured
5
+ * scale (`fibonacci`, `linear`, `exponential`, or `tShirt`). A value that
6
+ * isn't on the scale is rejected by the API, so we snap to the nearest
7
+ * allowed value before sending. Callers use the returned snap event to emit
8
+ * a debug log once per transformation.
9
+ *
10
+ * Scales mirror Linear's own SDK values — see `LinearIssueEstimationType`
11
+ * in `src/models/types.ts`.
12
+ */
13
+ import type { LinearIssueEstimationType } from '../../models/types.js';
14
+ /**
15
+ * Result of resolving a local estimate for a push. Exactly one of
16
+ * `estimate` (mapped value, ready to send to Linear) or `reason` (why the
17
+ * field is being omitted) is populated.
18
+ */
19
+ export type EstimateResolution = {
20
+ kind: 'mapped';
21
+ estimate: number;
22
+ originalValue: number;
23
+ snapped: boolean;
24
+ } | {
25
+ kind: 'skip-not-used';
26
+ } | {
27
+ kind: 'skip-t-shirt';
28
+ } | {
29
+ kind: 'skip-no-local-value';
30
+ } | {
31
+ kind: 'skip-invalid-value';
32
+ rawValue: unknown;
33
+ };
34
+ /**
35
+ * Resolve a local `storyPoints` (or `estimatedPoints`) value to a Linear
36
+ * `estimate` value given the team's scale.
37
+ *
38
+ * Precedence for the raw local value:
39
+ * 1. `frontmatter.estimatedPoints` — canonical name written by
40
+ * `planr estimate --save` (see `src/cli/commands/estimate.ts`).
41
+ * 2. `frontmatter.storyPoints` — alias accepted for hand-edited files or
42
+ * direct AI-response copies that used the schema field name verbatim.
43
+ *
44
+ * Returns one of:
45
+ * - `mapped` — include `estimate: <value>` in the push input
46
+ * - `skip-*` — omit the `estimate` field; `kind` carries the reason for
47
+ * logging / dry-run display
48
+ */
49
+ export declare function resolveEstimateForPush(frontmatter: Record<string, unknown>, scale: LinearIssueEstimationType | string | undefined): EstimateResolution;
50
+ //# sourceMappingURL=estimate-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate-resolver.d.ts","sourceRoot":"","sources":["../../../src/services/linear/estimate-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAqBvE;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,GACzB;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,qBAAqB,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,KAAK,EAAE,yBAAyB,GAAG,MAAM,GAAG,SAAS,GACpD,kBAAkB,CAyCpB"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Scale-aware snapping from OpenPlanr `storyPoints` → Linear `estimate` field.
3
+ *
4
+ * Linear's estimate field is numeric and must match the team's configured
5
+ * scale (`fibonacci`, `linear`, `exponential`, or `tShirt`). A value that
6
+ * isn't on the scale is rejected by the API, so we snap to the nearest
7
+ * allowed value before sending. Callers use the returned snap event to emit
8
+ * a debug log once per transformation.
9
+ *
10
+ * Scales mirror Linear's own SDK values — see `LinearIssueEstimationType`
11
+ * in `src/models/types.ts`.
12
+ */
13
+ const FIBONACCI_SCALE = [0, 1, 2, 3, 5, 8, 13, 21];
14
+ const LINEAR_SCALE = [0, 1, 2, 3, 4, 5];
15
+ const EXPONENTIAL_SCALE = [0, 1, 2, 4, 8, 16];
16
+ function snapToNearest(value, scale) {
17
+ let best = scale[0];
18
+ let bestDistance = Math.abs(value - best);
19
+ for (const candidate of scale) {
20
+ const distance = Math.abs(value - candidate);
21
+ // Break ties toward the larger value — under-estimating is a common
22
+ // planning bias; snapping up ("4 is really a 5") leans against it.
23
+ if (distance < bestDistance || (distance === bestDistance && candidate > best)) {
24
+ best = candidate;
25
+ bestDistance = distance;
26
+ }
27
+ }
28
+ return best;
29
+ }
30
+ /**
31
+ * Resolve a local `storyPoints` (or `estimatedPoints`) value to a Linear
32
+ * `estimate` value given the team's scale.
33
+ *
34
+ * Precedence for the raw local value:
35
+ * 1. `frontmatter.estimatedPoints` — canonical name written by
36
+ * `planr estimate --save` (see `src/cli/commands/estimate.ts`).
37
+ * 2. `frontmatter.storyPoints` — alias accepted for hand-edited files or
38
+ * direct AI-response copies that used the schema field name verbatim.
39
+ *
40
+ * Returns one of:
41
+ * - `mapped` — include `estimate: <value>` in the push input
42
+ * - `skip-*` — omit the `estimate` field; `kind` carries the reason for
43
+ * logging / dry-run display
44
+ */
45
+ export function resolveEstimateForPush(frontmatter, scale) {
46
+ if (scale === 'notUsed' || !scale) {
47
+ return { kind: 'skip-not-used' };
48
+ }
49
+ if (scale === 'tShirt') {
50
+ // No safe numeric → XS/S/M/L/XL mapping without user config. Skip.
51
+ return { kind: 'skip-t-shirt' };
52
+ }
53
+ const rawLocal = frontmatter.estimatedPoints !== undefined
54
+ ? frontmatter.estimatedPoints
55
+ : frontmatter.storyPoints;
56
+ if (rawLocal === undefined || rawLocal === null || rawLocal === '') {
57
+ return { kind: 'skip-no-local-value' };
58
+ }
59
+ const parsed = typeof rawLocal === 'number' ? rawLocal : Number(rawLocal);
60
+ if (!Number.isFinite(parsed) || parsed < 0) {
61
+ return { kind: 'skip-invalid-value', rawValue: rawLocal };
62
+ }
63
+ const allowed = scale === 'fibonacci'
64
+ ? FIBONACCI_SCALE
65
+ : scale === 'linear'
66
+ ? LINEAR_SCALE
67
+ : scale === 'exponential'
68
+ ? EXPONENTIAL_SCALE
69
+ : undefined;
70
+ if (!allowed) {
71
+ // Unknown scale value returned by Linear — skip rather than guess.
72
+ return { kind: 'skip-not-used' };
73
+ }
74
+ const snapped = snapToNearest(parsed, allowed);
75
+ return {
76
+ kind: 'mapped',
77
+ estimate: snapped,
78
+ originalValue: parsed,
79
+ snapped: snapped !== parsed,
80
+ };
81
+ }
82
+ //# sourceMappingURL=estimate-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate-resolver.js","sourceRoot":"","sources":["../../../src/services/linear/estimate-resolver.ts"],"names":[],"mappings":"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"}