paseo-req-kit 1.9.0-beta.2 → 1.9.1

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.
@@ -0,0 +1,219 @@
1
+ // A small Markdown renderer for the requirement documents (spec, design, report…): headings,
2
+ // paragraphs, lists, task lists, tables, code blocks, quotes, rules, and inline bold, code and
3
+ // links. React Native only, colors from the theme, so it reads the same on desktop, web and phones.
4
+ import type { PluginHostProps } from "@getpaseo/plugin/client";
5
+ import { ExternalLink } from "@getpaseo/plugin/client/ui";
6
+ import { type ReactNode, useMemo } from "react";
7
+ import { ScrollView, Text, View } from "react-native";
8
+
9
+ type Theme = PluginHostProps["theme"];
10
+
11
+ export type Block =
12
+ | { kind: "heading"; level: number; text: string }
13
+ | { kind: "paragraph"; text: string }
14
+ | { kind: "list"; ordered: boolean; items: Array<{ text: string; depth: number; checked: boolean | null }> }
15
+ | { kind: "code"; text: string }
16
+ | { kind: "quote"; text: string }
17
+ | { kind: "table"; header: string[]; rows: string[][] }
18
+ | { kind: "rule" };
19
+
20
+ const LIST = /^(\s*)([-*+]|\d+[.)])\s+(\[[ xX]\]\s+)?(.*)$/;
21
+
22
+ function cells(line: string): string[] {
23
+ return line
24
+ .trim()
25
+ .replace(/^\|/, "")
26
+ .replace(/\|$/, "")
27
+ .replace(/\\\|/g, "\u0000")
28
+ .split("|")
29
+ .map((c) => c.trim().replace(/\u0000/g, "|"));
30
+ }
31
+
32
+ /** A tab-separated file (decisions.tsv, lessons.tsv) as a Markdown table. */
33
+ export function tsvToMarkdown(tsv: string): string {
34
+ const rows = tsv.replace(/\r\n?/g, "\n").split("\n").filter((l) => l.trim());
35
+ if (rows.length === 0) return "";
36
+ const line = (r: string) => `| ${r.split("\t").map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
37
+ return [line(rows[0]), `|${rows[0].split("\t").map(() => " --- ").join("|")}|`, ...rows.slice(1).map(line)].join("\n");
38
+ }
39
+
40
+ /** Splits Markdown into blocks. Anything it does not recognise stays a paragraph. */
41
+ export function parseMarkdown(source: string): Block[] {
42
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
43
+ const blocks: Block[] = [];
44
+ let i = 0;
45
+ while (i < lines.length) {
46
+ const line = lines.at(i)!;
47
+ if (!line.trim()) {
48
+ i++;
49
+ continue;
50
+ }
51
+ const fence = /^\s*(```|~~~)/.exec(line);
52
+ if (fence) {
53
+ const body: string[] = [];
54
+ i++;
55
+ while (i < lines.length && !lines.at(i)!.trim().startsWith(fence[1])) body.push(lines.at(i++)!);
56
+ i++;
57
+ blocks.push({ kind: "code", text: body.join("\n") });
58
+ continue;
59
+ }
60
+ const heading = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
61
+ if (heading) {
62
+ blocks.push({ kind: "heading", level: heading[1].length, text: heading[2] });
63
+ i++;
64
+ continue;
65
+ }
66
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
67
+ blocks.push({ kind: "rule" });
68
+ i++;
69
+ continue;
70
+ }
71
+ if (line.trim().startsWith("|") && i + 1 < lines.length && /^\s*\|?\s*:?-{2,}/.test(lines.at(i + 1)!)) {
72
+ const header = cells(line);
73
+ const rows: string[][] = [];
74
+ i += 2;
75
+ while (i < lines.length && lines.at(i)!.trim().startsWith("|")) rows.push(cells(lines.at(i++)!));
76
+ blocks.push({ kind: "table", header, rows });
77
+ continue;
78
+ }
79
+ if (/^\s*>/.test(line)) {
80
+ const body: string[] = [];
81
+ while (i < lines.length && /^\s*>/.test(lines.at(i)!)) body.push(lines.at(i++)!.replace(/^\s*>\s?/, ""));
82
+ blocks.push({ kind: "quote", text: body.join("\n") });
83
+ continue;
84
+ }
85
+ if (LIST.test(line)) {
86
+ const ordered = /^\s*\d/.test(line);
87
+ const items: Array<{ text: string; depth: number; checked: boolean | null }> = [];
88
+ while (i < lines.length) {
89
+ const m = LIST.exec(lines.at(i)!);
90
+ if (m) {
91
+ items.push({ text: m[4], depth: Math.min(3, Math.floor(m[1].replace(/\t/g, " ").length / 2)), checked: m[3] ? /x/i.test(m[3]) : null });
92
+ i++;
93
+ } else if (lines.at(i)!.trim() && /^\s{2,}/.test(lines.at(i)!) && items.length > 0) {
94
+ items.at(-1)!.text += ` ${lines.at(i++)!.trim()}`;
95
+ } else break;
96
+ }
97
+ blocks.push({ kind: "list", ordered, items });
98
+ continue;
99
+ }
100
+ const body: string[] = [];
101
+ while (i < lines.length && lines.at(i)!.trim() && !/^(#{1,6}\s|\s*```|\s*~~~|\s*>|\s*\|)/.test(lines.at(i)!) && !LIST.test(lines.at(i)!)) body.push(lines.at(i++)!.trim());
102
+ blocks.push({ kind: "paragraph", text: body.join(" ") });
103
+ }
104
+ return blocks;
105
+ }
106
+
107
+ /** Inline **bold**, `code` and [links](https://…). */
108
+ export function inline(text: string, theme: Theme, key = "i"): ReactNode[] {
109
+ const out: ReactNode[] = [];
110
+ const pattern = /(\*\*([^*]+)\*\*|`([^`]+)`|\[([^\]]+)\]\(([^)\s]+)\))/g;
111
+ let last = 0;
112
+ let n = 0;
113
+ for (let m = pattern.exec(text); m; m = pattern.exec(text)) {
114
+ if (m.index > last) out.push(text.slice(last, m.index));
115
+ if (m[2] !== undefined) {
116
+ out.push(
117
+ <Text key={`${key}-${n++}`} style={{ fontWeight: "600" }}>
118
+ {m[2]}
119
+ </Text>,
120
+ );
121
+ } else if (m[3] !== undefined) {
122
+ out.push(
123
+ <Text key={`${key}-${n++}`} style={{ fontFamily: "monospace", backgroundColor: theme.colors.surface2 }}>
124
+ {m[3]}
125
+ </Text>,
126
+ );
127
+ } else if (m[5] && /^https?:\/\//.test(m[5])) {
128
+ out.push(
129
+ <ExternalLink key={`${key}-${n++}`} href={m[5]}>
130
+ {m[4]}
131
+ </ExternalLink>,
132
+ );
133
+ } else {
134
+ // A link to another file in the repository: show its text and where it points.
135
+ out.push(
136
+ <Text key={`${key}-${n++}`} style={{ color: theme.colors.accent }}>
137
+ {m[4]}
138
+ </Text>,
139
+ );
140
+ }
141
+ last = m.index + m[0].length;
142
+ }
143
+ if (last < text.length) out.push(text.slice(last));
144
+ return out;
145
+ }
146
+
147
+ export function Markdown({ theme, source }: { theme: Theme; source: string }) {
148
+ const blocks = useMemo(() => parseMarkdown(source), [source]);
149
+ const text = { color: theme.colors.foreground, fontSize: 14, lineHeight: 21 };
150
+ const muted = { color: theme.colors.foregroundMuted };
151
+ return (
152
+ <View style={{ gap: 10 }}>
153
+ {blocks.map((b, i) => {
154
+ switch (b.kind) {
155
+ case "heading":
156
+ return (
157
+ <Text key={i} selectable style={{ ...text, fontWeight: "600", fontSize: b.level === 1 ? 20 : b.level === 2 ? 17 : 15, marginTop: i === 0 ? 0 : 6 }}>
158
+ {inline(b.text, theme, `h${i}`)}
159
+ </Text>
160
+ );
161
+ case "paragraph":
162
+ return (
163
+ <Text key={i} selectable style={text}>
164
+ {inline(b.text, theme, `p${i}`)}
165
+ </Text>
166
+ );
167
+ case "list":
168
+ return (
169
+ <View key={i} style={{ gap: 4 }}>
170
+ {b.items.map((item, j) => (
171
+ <View key={j} style={{ flexDirection: "row", gap: 6, paddingLeft: item.depth * 16 }}>
172
+ <Text style={{ ...text, ...muted, minWidth: 16 }}>{item.checked === null ? (b.ordered ? `${j + 1}.` : "•") : item.checked ? "☑" : "☐"}</Text>
173
+ <Text selectable style={{ ...text, flex: 1 }}>
174
+ {inline(item.text, theme, `l${i}-${j}`)}
175
+ </Text>
176
+ </View>
177
+ ))}
178
+ </View>
179
+ );
180
+ case "code":
181
+ return (
182
+ <ScrollView key={i} horizontal style={{ backgroundColor: theme.colors.surface2, borderRadius: 8 }}>
183
+ <Text selectable style={{ ...text, fontFamily: "monospace", fontSize: 13, padding: 10 }}>
184
+ {b.text}
185
+ </Text>
186
+ </ScrollView>
187
+ );
188
+ case "quote":
189
+ return (
190
+ <View key={i} style={{ borderLeftWidth: 3, borderLeftColor: theme.colors.border, paddingLeft: 10 }}>
191
+ <Text selectable style={{ ...text, ...muted }}>
192
+ {inline(b.text, theme, `q${i}`)}
193
+ </Text>
194
+ </View>
195
+ );
196
+ case "rule":
197
+ return <View key={i} style={{ height: 1, backgroundColor: theme.colors.border }} />;
198
+ case "table":
199
+ return (
200
+ <ScrollView key={i} horizontal>
201
+ <View style={{ borderColor: theme.colors.border, borderWidth: 1, borderRadius: 6 }}>
202
+ {[b.header, ...b.rows].map((row, r) => (
203
+ <View key={r} style={{ flexDirection: "row", backgroundColor: r === 0 ? theme.colors.surface1 : undefined, borderTopWidth: r === 0 ? 0 : 1, borderTopColor: theme.colors.border }}>
204
+ {b.header.map((_, c) => (
205
+ <Text key={c} selectable style={{ ...text, fontSize: 13, fontWeight: r === 0 ? "600" : "400", width: 180, padding: 8 }}>
206
+ {inline(row.at(c) ?? "", theme, `t${i}-${r}-${c}`)}
207
+ </Text>
208
+ ))}
209
+ </View>
210
+ ))}
211
+ </View>
212
+ </ScrollView>
213
+ );
214
+ }
215
+ return null;
216
+ })}
217
+ </View>
218
+ );
219
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "req-kit-paseo-plugin",
3
- "version": "1.9.0-beta.2",
3
+ "version": "1.9.1",
4
4
  "private": true,
5
5
  "description": "The req-kit plugin for Paseo. Ships inside the paseo-req-kit package; installed with req plugin install.",
6
6
  "type": "module",
@@ -79,6 +79,8 @@ export interface RequirementDetail extends Requirement {
79
79
  steps: Step[];
80
80
  gate: { runs: GateRun[] } | null;
81
81
  joint: Array<{ name: string; repo: string; branch: string; workspaceId: string; gate: { runs: GateRun[] } | null }>;
82
+ /** Cards in external systems (req-kit 1.9.0+; absent before). */
83
+ external?: Array<{ name: string; id: string | null; url: string | null; status: string | null; syncedAt: string | null; pending: number; lastError: string | null }>;
82
84
  }
83
85
 
84
86
  export interface StatsSummary {
@@ -137,7 +139,7 @@ export const statsRpc = defineRpc({
137
139
  output: z.custom<StatsSummary>(),
138
140
  });
139
141
 
140
- export const ACTIONS = ["approve", "stop", "resume", "verify"] as const;
142
+ export const ACTIONS = ["approve", "stop", "resume", "verify", "sync"] as const;
141
143
  export type Action = (typeof ACTIONS)[number];
142
144
 
143
145
  export const runRpc = defineRpc({
@@ -65,6 +65,7 @@ metadata:
65
65
  | 做什么 | 命令 |
66
66
  | --- | --- |
67
67
  | 需求说明审阅 | `node "{{PIPELINE_LAUNCHER}}" --feature <功能目录> --spec-review` |
68
+ | 卡片字段 | `node "{{PIPELINE_LAUNCHER}}" --card-fields` |
68
69
  | 启动 | `node "{{PIPELINE_LAUNCHER}}" --feature <功能目录>` |
69
70
  | 启动(自动新建工作区) | `node "{{PIPELINE_LAUNCHER}}" --feature <功能目录> --worktree` |
70
71
  | 预约启动 | `node "{{PIPELINE_LAUNCHER}}" --feature <功能目录> --at <时间>`(时间写 `22:00`、`明天 8:30`、`2026-10-14 08:30` 或 `+2h`;可以和 `--worktree` 一起用) |
@@ -83,7 +83,9 @@
83
83
 
84
84
  ## 8. 请需求方确认
85
85
 
86
- 按 `spec-writing` 中"确认"一节:给出 10 行以内的摘要,确认后把状态改为"已确认"并提交。选了"自动新建工作区"的,只改状态、不提交(不在默认分支上提交),启动时流水线会在新工作区中提交。
86
+ 先执行"卡片字段"命令。输出"跳过"时不用管;列出了字段时(团队对接了需求管理等外部系统),按输出的说明从需求说明中填出这些字段,判断不了的问需求方,放进摘要里一起确认,确认后写入功能目录的 `card.json`。流水线开始时会用它在外部系统中建卡。
87
+
88
+ 按 `spec-writing` 中"确认"一节:给出 10 行以内的摘要,确认后把状态改为"已确认"并提交(有 `card.json` 的一起提交)。选了"自动新建工作区"的,只改状态、不提交(不在默认分支上提交),启动时流水线会在新工作区中提交。
87
89
 
88
90
  ## 9. 启动开发
89
91