@skitterbyte/skitterspec-linear 8.0.1 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/core/SETUP.md
CHANGED
|
@@ -168,3 +168,9 @@ With a linked spec, confirm push end-to-end:
|
|
|
168
168
|
(`Backlog / Planned / In Progress / Completed / Canceled`).
|
|
169
169
|
- **Reconnecting doesn't switch workspace** — Linear ties the OAuth session to one
|
|
170
170
|
workspace. Remove and re-add the server to authenticate against another.
|
|
171
|
+
- **Bold around an inline code span renders oddly in Linear** — Linear moves the
|
|
172
|
+
closing `**` before an inline code span on save (`**no unresolved `` `X` ``**`
|
|
173
|
+
→ `**no unresolved** `` `X` ``). This is a Linear rendering quirk in the mirror
|
|
174
|
+
only; it never touches your repo (one-way sync never reads content back), so
|
|
175
|
+
it's cosmetic. Avoid wrapping a whole phrase that ends in code in bold if the
|
|
176
|
+
mirror's rendering matters to you.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec-linear",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.2",
|
|
4
4
|
"description": "Spec-driven development for Claude Code, with one-way Linear sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
Binary file
|
|
@@ -433,6 +433,19 @@ function remoteWorkflowState(project, config) {
|
|
|
433
433
|
return name != null ? bucketForState(name, config) : null
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// Trim a trailing parenthetical whose opener has no matching close — so a cut
|
|
437
|
+
// title never ends on a dangling `(`/`[`.
|
|
438
|
+
function dropUnclosedBracket(t) {
|
|
439
|
+
for (const [open, close] of [
|
|
440
|
+
['(', ')'],
|
|
441
|
+
['[', ']'],
|
|
442
|
+
]) {
|
|
443
|
+
const o = t.lastIndexOf(open)
|
|
444
|
+
if (o !== -1 && t.indexOf(close, o) === -1) t = t.slice(0, o)
|
|
445
|
+
}
|
|
446
|
+
return t
|
|
447
|
+
}
|
|
448
|
+
|
|
436
449
|
// Derive a short Linear issue title from a task's full text: the first sentence,
|
|
437
450
|
// falling back to the first `max` chars at a word boundary. A paragraph-length
|
|
438
451
|
// task keeps its full text as the issue *description* (see the projection); this
|
|
@@ -457,8 +470,23 @@ function titleFromText(text, max = 100) {
|
|
|
457
470
|
title = title.trim()
|
|
458
471
|
if (title.length > max) {
|
|
459
472
|
const cut = title.slice(0, max)
|
|
460
|
-
|
|
461
|
-
|
|
473
|
+
// Prefer the last clause boundary before the limit — a terminator/separator
|
|
474
|
+
// to cut AFTER (`.`/`;`/`:`…) or a dash to cut BEFORE — over a bare word break.
|
|
475
|
+
const minCut = Math.floor(max * 0.4)
|
|
476
|
+
let boundary = -1
|
|
477
|
+
let mm
|
|
478
|
+
const after = /[.!?;:](?=\s|$)/g
|
|
479
|
+
while ((mm = after.exec(cut)) !== null) boundary = Math.max(boundary, mm.index + 1)
|
|
480
|
+
const before = /\s[—–]/g
|
|
481
|
+
while ((mm = before.exec(cut)) !== null) boundary = Math.max(boundary, mm.index)
|
|
482
|
+
let t
|
|
483
|
+
if (boundary > minCut) {
|
|
484
|
+
t = cut.slice(0, boundary)
|
|
485
|
+
} else {
|
|
486
|
+
const sp = cut.lastIndexOf(' ')
|
|
487
|
+
t = sp > 40 ? cut.slice(0, sp) : cut
|
|
488
|
+
}
|
|
489
|
+
title = dropUnclosedBracket(t).replace(/[\s.,:;—–([]+$/, '').trim()
|
|
462
490
|
}
|
|
463
491
|
return title
|
|
464
492
|
}
|
|
@@ -36,6 +36,29 @@ function cleanLogicalLine(text) {
|
|
|
36
36
|
const BULLET_RE = /^(\s*)((?:[-*+]|\d+\.)\s+(?:\[[ xX]\]\s+)?)(.*)$/
|
|
37
37
|
// Structural blocks we never reflow.
|
|
38
38
|
const PASSTHROUGH_RE = /^\s*(#{1,6}\s|>|\||[-*_]{3,}\s*$|<)/
|
|
39
|
+
// A line that is its own structural block — a blockquote (`>`) or a table/pipe
|
|
40
|
+
// row (`|`). A hard boundary a join must never reach across (else the markers get
|
|
41
|
+
// absorbed into the preceding paragraph as literal text); copied verbatim.
|
|
42
|
+
const STRUCT_LINE_RE = /^[ \t]*[>|]/
|
|
43
|
+
// A fenced-code delimiter.
|
|
44
|
+
const FENCE_RE = /^[ \t]*```/
|
|
45
|
+
|
|
46
|
+
// A structural fingerprint of a document: the count of block-marker lines that a
|
|
47
|
+
// safe reflow must leave untouched. Used as a self-check — if sanitising changes
|
|
48
|
+
// any of these, the reflow corrupted structure (a blockquote absorbed, a fence
|
|
49
|
+
// eaten, a bullet collapsed) and the write is refused. Cheap insurance against the
|
|
50
|
+
// next corruption class, not just the ones we special-case.
|
|
51
|
+
function structuralSignature(text) {
|
|
52
|
+
let struct = 0 // blockquote or pipe-row lines
|
|
53
|
+
let fence = 0
|
|
54
|
+
let bullets = 0
|
|
55
|
+
for (const l of String(text).split('\n')) {
|
|
56
|
+
if (STRUCT_LINE_RE.test(l)) struct++
|
|
57
|
+
if (FENCE_RE.test(l)) fence++
|
|
58
|
+
if (BULLET_RE.test(l)) bullets++
|
|
59
|
+
}
|
|
60
|
+
return `${struct}|${fence}|${bullets}`
|
|
61
|
+
}
|
|
39
62
|
// A GFM table separator row (only pipes/colons/dashes/spaces, with a pipe AND a
|
|
40
63
|
// dash). Detecting a table by this — not by any stray `|` — so a pipe inside an
|
|
41
64
|
// inline `code|span` doesn't make us treat a whole list as an untouchable table.
|
|
@@ -99,21 +122,28 @@ function sanitizeSpecMarkdown(text, { width } = {}) {
|
|
|
99
122
|
while (i < lines.length) {
|
|
100
123
|
const line = lines[i]
|
|
101
124
|
// Fenced code — copy verbatim through the closing fence.
|
|
102
|
-
if (
|
|
125
|
+
if (FENCE_RE.test(line)) {
|
|
103
126
|
out.push(line)
|
|
104
127
|
i++
|
|
105
|
-
while (i < lines.length &&
|
|
128
|
+
while (i < lines.length && !FENCE_RE.test(lines[i])) out.push(lines[i++])
|
|
106
129
|
if (i < lines.length) out.push(lines[i++])
|
|
107
130
|
continue
|
|
108
131
|
}
|
|
132
|
+
// Blockquote / table row — copy the whole run verbatim. A hard boundary: a
|
|
133
|
+
// join must never reach across it, so it also terminates any preceding block.
|
|
134
|
+
if (STRUCT_LINE_RE.test(line)) {
|
|
135
|
+
while (i < lines.length && STRUCT_LINE_RE.test(lines[i])) out.push(lines[i++])
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
109
138
|
if (!line.trim()) {
|
|
110
139
|
out.push(line)
|
|
111
140
|
i++
|
|
112
141
|
continue
|
|
113
142
|
}
|
|
114
|
-
// Gather a block of consecutive non-blank,
|
|
143
|
+
// Gather a block of consecutive non-blank lines, stopping at any structural
|
|
144
|
+
// boundary (fence, blockquote, table row) so a reflow can't absorb one.
|
|
115
145
|
let j = i
|
|
116
|
-
while (j < lines.length && lines[j].trim() &&
|
|
146
|
+
while (j < lines.length && lines[j].trim() && !FENCE_RE.test(lines[j]) && !STRUCT_LINE_RE.test(lines[j])) j++
|
|
117
147
|
const block = lines.slice(i, j)
|
|
118
148
|
i = j
|
|
119
149
|
|
|
@@ -137,7 +167,13 @@ function sanitizeSpecMarkdown(text, { width } = {}) {
|
|
|
137
167
|
}
|
|
138
168
|
}
|
|
139
169
|
const result = out.join('\n')
|
|
170
|
+
// Self-check: a safe reflow never changes the count of structural markers. If it
|
|
171
|
+
// did, we corrupted something (a blockquote, fence, or bullet) — refuse the write
|
|
172
|
+
// and hand back the original untouched, flagged for the caller to report.
|
|
173
|
+
if (result !== src && structuralSignature(result) !== structuralSignature(src)) {
|
|
174
|
+
return { text: src, changed: false, fixes: 0, refused: true }
|
|
175
|
+
}
|
|
140
176
|
return { text: result, changed: result !== src, fixes }
|
|
141
177
|
}
|
|
142
178
|
|
|
143
|
-
module.exports = { sanitizeSpecMarkdown, hasStraddle }
|
|
179
|
+
module.exports = { sanitizeSpecMarkdown, hasStraddle, structuralSignature }
|