paseo-req-kit 1.11.0 → 1.12.0-beta.3

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 (54) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/bin/req.mjs +5 -0
  3. package/config.example.json +3 -1
  4. package/contracts/cli-json.md +5 -1
  5. package/contracts/notifier-event.md +17 -1
  6. package/contracts/state.md +3 -1
  7. package/contracts/usage-record.md +4 -1
  8. package/docs/adr/0031-fewer-interventions.md +41 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
  11. package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +3 -2
  12. package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +58 -4
  13. package/engine/prompts/04b-design-revise.md +15 -0
  14. package/engine/src/api.ts +74 -3
  15. package/engine/src/card-cmd.ts +21 -1
  16. package/engine/src/card.ts +38 -2
  17. package/engine/src/config-view.ts +289 -0
  18. package/engine/src/config.ts +65 -4
  19. package/engine/src/configure.ts +161 -36
  20. package/engine/src/control.ts +1 -0
  21. package/engine/src/doctor.ts +14 -0
  22. package/engine/src/factory.ts +162 -7
  23. package/engine/src/main.ts +1 -0
  24. package/engine/src/messages.ts +22 -0
  25. package/engine/src/notifiers.ts +32 -1
  26. package/engine/src/notify-test.ts +8 -2
  27. package/engine/src/paseo-profiles.ts +2 -0
  28. package/engine/src/paseo-runtime.ts +9 -0
  29. package/engine/src/pipeline.ts +222 -30
  30. package/engine/src/records.ts +8 -0
  31. package/engine/src/reply.ts +59 -0
  32. package/engine/src/runtime.ts +2 -0
  33. package/engine/src/state.ts +41 -1
  34. package/engine/src/stats.ts +90 -0
  35. package/engine/src/steps.ts +4 -1
  36. package/install.mjs +5 -1
  37. package/notifiers/im-template.mjs +31 -5
  38. package/package.json +2 -2
  39. package/paseo-plugin/OVERVIEW.md +2 -0
  40. package/paseo-plugin/client/config.tsx +216 -0
  41. package/paseo-plugin/client/data.ts +21 -2
  42. package/paseo-plugin/client/format.ts +1 -0
  43. package/paseo-plugin/client/inbox.tsx +173 -0
  44. package/paseo-plugin/client/models.tsx +117 -0
  45. package/paseo-plugin/client/overview.tsx +7 -3
  46. package/paseo-plugin/client/settings.tsx +3 -1
  47. package/paseo-plugin/index.client.tsx +10 -0
  48. package/paseo-plugin/index.server.ts +4 -1
  49. package/paseo-plugin/package.json +1 -1
  50. package/paseo-plugin/server/handlers.ts +21 -3
  51. package/paseo-plugin/shared/contracts.ts +77 -1
  52. package/skills/req/playbooks/new.md +14 -14
  53. package/skills/req/playbooks/small.md +3 -3
  54. package/skills/req/playbooks/unattended.md +5 -2
@@ -0,0 +1,173 @@
1
+ // "等你处理" (1.12): everything that is waiting for a person, at the top, each with what to do and
2
+ // how long it has waited. Answers and plan confirmations can be given right here: an answer is
3
+ // passed to the requirement's chat (req reply), the same as typing it there.
4
+ import type { PluginHostProps } from "@getpaseo/plugin/client";
5
+ import { TextInput, useToast } from "@getpaseo/plugin/client/react-native";
6
+ import { useState } from "react";
7
+ import { Text, View } from "react-native";
8
+ import type { Attention, Requirement } from "../shared/contracts";
9
+ import { useAction, usePreferences } from "./data";
10
+ import { facts } from "./format";
11
+ import { Badge, Button, useStyles } from "./ui";
12
+
13
+ type Theme = PluginHostProps["theme"];
14
+ type Layout = PluginHostProps["layout"];
15
+
16
+ const KIND: Record<Exclude<Attention, null>, { label: string; tone: "warning" | "danger" | "muted"; todo: string }> = {
17
+ approval: { label: "确认方案", tone: "warning", todo: "方案已通过审核。看一下“做完后会看到什么”,没问题就确认,开始写代码;要改就写下来。" },
18
+ business: { label: "回答问题", tone: "warning", todo: "方案审核有问题只有你能决定,回答后开发会接着进行。" },
19
+ interrupted: { label: "已中断", tone: "danger", todo: "开发中断了,自动处理没能解决。看一下原因,能继续就继续。" },
20
+ not_completed: { label: "没有完成", tone: "muted", todo: "流水线结束了,但有部分没有做完。完成报告里写了原因和建议。" },
21
+ };
22
+
23
+ /** Plain waiting time: "等了 3 小时". */
24
+ export function waited(since: string, now = Date.now()): string {
25
+ const m = (now - Date.parse(since)) / 60_000;
26
+ if (!Number.isFinite(m) || m < 1) return "刚刚";
27
+ if (m < 60) return `等了 ${Math.round(m)} 分钟`;
28
+ if (m < 48 * 60) return `等了 ${Math.round(m / 60)} 小时`;
29
+ return `等了 ${Math.round(m / 60 / 24)} 天`;
30
+ }
31
+
32
+ /** Longest-waiting first: the oldest decision is the one holding things up most. */
33
+ export function inboxItems(items: Requirement[]): Requirement[] {
34
+ return items
35
+ .filter((r) => r.attention)
36
+ .sort((a, b) => (a.waiting?.since ?? a.updatedAt).localeCompare(b.waiting?.since ?? b.updatedAt));
37
+ }
38
+
39
+ function InboxCard({ theme, layout, item, open, readOnly }: { theme: Theme; layout: Layout; item: Requirement; open(): void; readOnly: boolean }) {
40
+ const s = useStyles(theme, layout.compact);
41
+ const run = useAction();
42
+ const toast = useToast();
43
+ const [writing, setWriting] = useState(item.attention === "business");
44
+ const [text, setText] = useState("");
45
+ const kind = KIND[item.attention!];
46
+ const w = item.waiting;
47
+ const repo = item.sourceRepo ?? item.repo;
48
+
49
+ const act = async (action: "approve" | "resume" | "reply") => {
50
+ try {
51
+ const result = await run.mutateAsync({ action, repo, featureDir: item.featureDir, ...(action === "reply" ? { text } : {}) });
52
+ const first = result.message.split("\n").find((l) => l.trim()) ?? "";
53
+ if (result.ok) {
54
+ toast.show(first || "已完成", { variant: "success", durationMs: 4000 });
55
+ setText("");
56
+ } else toast.error(first || "没有完成");
57
+ } catch (error) {
58
+ toast.error((error as Error).message);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <View style={[s.card, { gap: 10, borderLeftWidth: 3, borderLeftColor: kind.tone === "danger" ? theme.colors.statusDanger : kind.tone === "warning" ? theme.colors.statusWarning : theme.colors.border }]}>
64
+ <View style={[s.row, { justifyContent: "space-between" }]}>
65
+ <View style={[s.row, { flexShrink: 1 }]}>
66
+ <Badge theme={theme} tone={kind.tone}>
67
+ {kind.label}
68
+ </Badge>
69
+ <Text style={[s.text, { fontWeight: "600", flexShrink: 1 }]}>{item.title}</Text>
70
+ </View>
71
+ <Text style={s.muted}>{waited(w?.since ?? item.updatedAt)}</Text>
72
+ </View>
73
+ <Text style={s.muted}>
74
+ {facts(item)}
75
+ {w?.card ? ` · 卡片 ${w.card.system} ${w.card.id}${w.card.requester ? `(${w.card.requester})` : ""}` : ""}
76
+ </Text>
77
+ <Text style={s.text}>{kind.todo}</Text>
78
+
79
+ {item.attention === "business" && w && w.questions.length > 0 ? (
80
+ <View style={{ gap: 4 }}>
81
+ {w.questions.map((q, i) => (
82
+ <Text key={i} style={s.text}>
83
+ {i + 1}. {q}
84
+ </Text>
85
+ ))}
86
+ </View>
87
+ ) : null}
88
+ {item.attention === "interrupted" && w?.reason ? (
89
+ <Text style={s.danger} numberOfLines={4}>
90
+ {w.reason}
91
+ </Text>
92
+ ) : null}
93
+
94
+ {readOnly ? (
95
+ <View style={s.row}>
96
+ <Button theme={theme} label="查看详情" onPress={open} />
97
+ <Text style={s.muted}>插件设置为只读,请在对话中处理</Text>
98
+ </View>
99
+ ) : (
100
+ <>
101
+ {writing && w?.canReply ? (
102
+ <View style={{ gap: 8 }}>
103
+ <TextInput
104
+ value={text}
105
+ onChangeText={setText}
106
+ multiline
107
+ placeholder={item.attention === "approval" ? "写下要改的地方,例如:按钮放在右上角" : w.questions.length > 1 ? "按编号回答,例如:1. 只有新订单显示 2. 不需要" : "写下你的回答"}
108
+ placeholderTextColor={theme.colors.foregroundMuted}
109
+ style={{
110
+ minHeight: 72,
111
+ padding: 10,
112
+ borderRadius: 8,
113
+ borderWidth: 1,
114
+ borderColor: theme.colors.border,
115
+ backgroundColor: theme.colors.surface0,
116
+ color: theme.colors.foreground,
117
+ fontSize: 14,
118
+ textAlignVertical: "top",
119
+ }}
120
+ />
121
+ {w.card ? <Text style={s.muted}>也可以在卡片的“需求方回复”区回答,效果一样。</Text> : null}
122
+ </View>
123
+ ) : null}
124
+ <View style={s.row}>
125
+ {item.attention === "approval" ? (
126
+ <>
127
+ <Button theme={theme} primary label={run.isPending ? "正在执行…" : "确认方案"} disabled={run.isPending} onPress={() => void act("approve")} />
128
+ {w?.canReply ? (
129
+ writing ? (
130
+ <Button theme={theme} label="发送修改意见" disabled={run.isPending || !text.trim()} onPress={() => void act("reply")} />
131
+ ) : (
132
+ <Button theme={theme} label="提修改意见" onPress={() => setWriting(true)} />
133
+ )
134
+ ) : null}
135
+ </>
136
+ ) : null}
137
+ {item.attention === "business" && w?.canReply ? (
138
+ <Button theme={theme} primary label={run.isPending ? "正在发送…" : "发送回答"} disabled={run.isPending || !text.trim()} onPress={() => void act("reply")} />
139
+ ) : null}
140
+ {item.attention === "interrupted" ? (
141
+ <Button theme={theme} primary label={run.isPending ? "正在执行…" : "继续"} disabled={run.isPending} onPress={() => void act("resume")} />
142
+ ) : null}
143
+ <Button theme={theme} label={item.attention === "approval" ? "看方案和确认预览" : item.attention === "not_completed" ? "看完成报告" : "查看详情"} onPress={open} />
144
+ </View>
145
+ </>
146
+ )}
147
+ </View>
148
+ );
149
+ }
150
+
151
+ export function Inbox({ theme, layout, items, open }: { theme: Theme; layout: Layout; items: Requirement[]; open(item: Requirement): void }) {
152
+ const s = useStyles(theme, layout.compact);
153
+ const { readOnly } = usePreferences();
154
+ const waiting = inboxItems(items);
155
+ return (
156
+ <View style={{ gap: 10 }}>
157
+ <View style={[s.row, { justifyContent: "space-between" }]}>
158
+ <Text style={[s.title, { fontSize: 16 }]}>等你处理{waiting.length > 0 ? `(${waiting.length})` : ""}</Text>
159
+ {waiting.length > 0 ? <Text style={s.muted}>等得最久的在前</Text> : null}
160
+ </View>
161
+ {waiting.length === 0 ? (
162
+ <View style={[s.card, { flexDirection: "row", alignItems: "center", gap: 8 }]}>
163
+ <Badge theme={theme} tone="success">
164
+ 没有
165
+ </Badge>
166
+ <Text style={s.muted}>现在没有需要你处理的事。需要你确认或回答时,会出现在这里。</Text>
167
+ </View>
168
+ ) : (
169
+ waiting.map((item) => <InboxCard key={item.key} theme={theme} layout={layout} item={item} open={() => open(item)} readOnly={readOnly} />)
170
+ )}
171
+ </View>
172
+ );
173
+ }
@@ -0,0 +1,117 @@
1
+ // Settings → Plugins → req-kit → 模型 (1.12): pick builder, reviewer and the fallback model from what
2
+ // this daemon offers, and write them to req-kit's machine config — the same as `req configure`.
3
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
4
+ import { SettingsAction, SettingsCard, SettingsRow, SettingsSection, SettingsSelect } from "@getpaseo/plugin/client/ui";
5
+ import { useEffect, useState } from "react";
6
+ import { Text } from "react-native";
7
+ import type { AgentChoiceData, AgentProfileData } from "../shared/contracts";
8
+ import { useModels, useSetAgents } from "./data";
9
+
10
+ const NONE = "none";
11
+
12
+ /** The choice a saved profile corresponds to; a profile Paseo no longer offers keeps its own entry. */
13
+ function idOf(profile: AgentProfileData | null | undefined, choices: AgentChoiceData[]): string {
14
+ if (!profile) return NONE;
15
+ if (profile.paseoProfile) return `paseo:${profile.paseoProfile}`;
16
+ const found = choices.find((c) => !c.profile.paseoProfile && c.profile.provider === profile.provider);
17
+ return found ? found.id : `saved:${profile.provider ?? ""}`;
18
+ }
19
+
20
+ type Role = "builder" | "reviewer" | "fallback";
21
+
22
+ const ROLES: Array<{ role: Role; label: string; hint: string }> = [
23
+ { role: "builder", label: "builder", hint: "方案设计、写代码、修复、写报告" },
24
+ { role: "reviewer", label: "reviewer", hint: "方案审核、代码评审、功能验证。和 builder 用不同厂商的模型,更容易发现问题" },
25
+ { role: "fallback", label: "备用模型", hint: "主模型一直限流、额度用完或报错时,换它把这一步做完,而不是停下等人" },
26
+ ];
27
+
28
+ export function ModelSettings({ theme, readOnly }: { theme: PluginSurfaceProps["theme"]; readOnly: boolean }) {
29
+ const models = useModels();
30
+ const save = useSetAgents();
31
+ const data = models.data;
32
+ const choices = data?.choices ?? [];
33
+ const [picked, setPicked] = useState<Record<Role, string>>({ builder: NONE, reviewer: NONE, fallback: NONE });
34
+ const [message, setMessage] = useState<string | null>(null);
35
+
36
+ useEffect(() => {
37
+ if (!data) return;
38
+ setPicked({
39
+ builder: idOf(data.current?.builder, choices),
40
+ reviewer: idOf(data.current?.reviewer, choices),
41
+ fallback: idOf(data.current?.fallback ?? null, choices),
42
+ });
43
+ }, [data]);
44
+
45
+ if (!data) {
46
+ return (
47
+ <SettingsSection title="模型">
48
+ <SettingsCard>
49
+ <SettingsRow label="可用的 agent" error={models.error?.message ?? undefined} hint={models.isPending ? "正在向 Paseo 读取已登录的 agent 和模型…" : undefined} />
50
+ </SettingsCard>
51
+ </SettingsSection>
52
+ );
53
+ }
54
+
55
+ const saved = (role: Role): AgentProfileData | null => (role === "fallback" ? (data.current?.fallback ?? null) : (data.current?.[role] ?? null));
56
+ const profileOf = (role: Role, id: string): AgentProfileData | null => {
57
+ if (id === NONE) return null;
58
+ if (id.startsWith("saved:")) return saved(role);
59
+ return choices.find((c) => c.id === id)?.profile ?? null;
60
+ };
61
+ const optionsFor = (role: Role) => {
62
+ const list = choices.map((c) => ({ label: c.label, value: c.id }));
63
+ const current = picked[role];
64
+ if (current.startsWith("saved:")) list.unshift({ label: `现在的设置:${current.slice(6)}(Paseo 中没有找到)`, value: current });
65
+ if (role === "fallback" || current === NONE) list.unshift({ label: role === "fallback" ? "不用备用模型" : "(未设置)", value: NONE });
66
+ return list;
67
+ };
68
+ const builder = profileOf("builder", picked.builder);
69
+ const reviewer = profileOf("reviewer", picked.reviewer);
70
+ const changed = ROLES.some(({ role }) => picked[role] !== idOf(role === "fallback" ? (data.current?.fallback ?? null) : data.current?.[role], choices));
71
+
72
+ const useRecommended = () => {
73
+ const r = data.recommended;
74
+ if (!r) return;
75
+ setPicked({ builder: idOf(r.builder, choices), reviewer: idOf(r.reviewer, choices), fallback: idOf(r.fallback ?? null, choices) });
76
+ };
77
+
78
+ return (
79
+ <SettingsSection title="模型">
80
+ <SettingsCard>
81
+ {choices.length === 0 ? (
82
+ <SettingsRow label="可用的 agent" hint="Paseo 中还没有登录任何能无人值守运行的 agent。在 Paseo 中登录 Claude Code、Codex 等之后再打开这一页。" />
83
+ ) : null}
84
+ {ROLES.map(({ role, label, hint }) => (
85
+ <SettingsSelect
86
+ key={role}
87
+ label={label}
88
+ hint={hint}
89
+ value={picked[role]}
90
+ options={optionsFor(role)}
91
+ disabled={readOnly || save.isPending}
92
+ onValueChange={(v) => {
93
+ setMessage(null);
94
+ setPicked({ ...picked, [role]: v });
95
+ }}
96
+ />
97
+ ))}
98
+ {data.recommended ? (
99
+ <SettingsAction label="按推荐选择" hint="不同厂商的模型分别做 builder 和 reviewer,再选一个做备用" actionLabel="填入推荐" disabled={readOnly || save.isPending} onPress={useRecommended} />
100
+ ) : null}
101
+ <SettingsAction
102
+ label="保存到本机配置"
103
+ hint={readOnly ? "插件设置为只读,不能在这里修改" : "写入 req-kit 的本机配置(原文件会备份);之后开始的需求使用新的设置,进行中的不变"}
104
+ actionLabel={save.isPending ? "保存中…" : "保存"}
105
+ disabled={readOnly || save.isPending || !changed || !builder || !reviewer}
106
+ onPress={() =>
107
+ void save.mutateAsync({ builder: builder!, reviewer: reviewer!, fallback: profileOf("fallback", picked.fallback) }).then(
108
+ (r) => setMessage(r.message),
109
+ (e: Error) => setMessage(e.message),
110
+ )
111
+ }
112
+ />
113
+ {message ? <Text style={{ color: theme.colors.foregroundMuted }}>{message}</Text> : null}
114
+ </SettingsCard>
115
+ </SettingsSection>
116
+ );
117
+ }
@@ -6,6 +6,7 @@ import { Text, View } from "react-native";
6
6
  import type { Requirement } from "../shared/contracts";
7
7
  import { useRequirements } from "./data";
8
8
  import { type Group, GROUP_LABELS, groupOf } from "./format";
9
+ import { Inbox } from "./inbox";
9
10
  import { RequirementRow } from "./row";
10
11
  import { Button, Chip, Status, useStyles } from "./ui";
11
12
 
@@ -19,7 +20,8 @@ export function RequirementGroups({
19
20
  layout,
20
21
  items,
21
22
  open,
22
- }: Pick<PluginScreenProps, "theme" | "layout"> & { items: Requirement[]; open: OpenRequirement }) {
23
+ skip = [],
24
+ }: Pick<PluginScreenProps, "theme" | "layout"> & { items: Requirement[]; open: OpenRequirement; skip?: Group[] }) {
23
25
  const s = useStyles(theme, layout.compact);
24
26
  const [showOther, setShowOther] = useState(false);
25
27
  const groups = useMemo(() => {
@@ -29,7 +31,7 @@ export function RequirementGroups({
29
31
  }, [items]);
30
32
  return (
31
33
  <View style={{ gap: 20 }}>
32
- {ORDER.filter((g) => groups.has(g)).map((g) => (
34
+ {ORDER.filter((g) => groups.has(g) && !skip.includes(g)).map((g) => (
33
35
  <View key={g} style={{ gap: 8 }}>
34
36
  <View style={s.row}>
35
37
  <Text style={s.heading}>
@@ -67,6 +69,7 @@ export function OverviewScreen(props: PluginScreenProps & { openScreen: ScreenOp
67
69
  <ScrollView contentContainerStyle={s.body}>
68
70
  <View style={s.row}>
69
71
  <Button theme={theme} label="使用统计" onPress={() => props.openScreen({ screenId: "stats" })} />
72
+ <Button theme={theme} label="配置" onPress={() => props.openScreen({ screenId: "config" })} />
70
73
  </View>
71
74
  {projects.length > 1 ? (
72
75
  <View style={s.row}>
@@ -82,7 +85,8 @@ export function OverviewScreen(props: PluginScreenProps & { openScreen: ScreenOp
82
85
  error={query.error}
83
86
  empty={query.data && items.length === 0 ? "这台机器上还没有记录任何需求。在 Paseo 对话中用 /req 开始一个;1.9 之前运行过的需求,在项目目录运行 req index --rebuild。" : null}
84
87
  />
85
- <RequirementGroups theme={theme} layout={layout} items={shown} open={open} />
88
+ {query.data && items.length > 0 ? <Inbox theme={theme} layout={layout} items={shown} open={open} /> : null}
89
+ <RequirementGroups theme={theme} layout={layout} items={shown} open={open} skip={["attention"]} />
86
90
  </ScrollView>
87
91
  </View>
88
92
  );
@@ -1,10 +1,11 @@
1
- // Settings → Plugins → req-kit: what the plugin found, read-only mode, refresh interval.
1
+ // Settings → Plugins → req-kit: what the plugin found, the models (1.12), read-only mode, refresh interval.
2
2
  import { type PluginSurfaceProps, type SettingsState, useSettings } from "@getpaseo/plugin/client";
3
3
  import { SettingsAction, SettingsCard, SettingsInput, SettingsRow, SettingsSection, SettingsSelect, SettingsSwitch } from "@getpaseo/plugin/client/ui";
4
4
  import { useState } from "react";
5
5
  import { Text } from "react-native";
6
6
  import { preferences } from "../shared/contracts";
7
7
  import { useKitInfo } from "./data";
8
+ import { ModelSettings } from "./models";
8
9
 
9
10
  type Ready = Extract<SettingsState<typeof preferences.schema>, { status: "ready" }>;
10
11
 
@@ -51,6 +52,7 @@ function Controls({ settings, theme }: { settings: Ready; theme: PluginSurfacePr
51
52
  />
52
53
  </SettingsCard>
53
54
  </SettingsSection>
55
+ <ModelSettings theme={theme} readOnly={settings.values.readOnly} />
54
56
  <SettingsSection title="显示与操作">
55
57
  <SettingsCard>
56
58
  <SettingsSwitch
@@ -1,4 +1,5 @@
1
1
  import type { PluginClientContext, PluginScreenProps } from "@getpaseo/plugin/client";
2
+ import { ConfigScreen } from "./client/config";
2
3
  import { RequirementScreen } from "./client/detail";
3
4
  import { contributeLive } from "./client/live";
4
5
  import { OverviewScreen } from "./client/overview";
@@ -14,6 +15,7 @@ export default function contribute(client: PluginClientContext) {
14
15
  client.addScreen({ id: "overview", title: "需求", Component: Overview });
15
16
  client.addScreen({ id: "requirement", title: (params) => params.title || params.name || "需求", Component: RequirementScreen });
16
17
  client.addScreen({ id: "stats", title: "需求统计", Component: StatsScreen });
18
+ client.addScreen({ id: "config", title: "req-kit 配置", Component: ConfigScreen });
17
19
  client.addSidebarHeaderItem({ id: "requirements", title: "需求(req-kit)", Component: RequirementsItem });
18
20
  client.addWorkspacePanel({
19
21
  id: "requirements",
@@ -40,6 +42,14 @@ export default function contribute(client: PluginClientContext) {
40
42
  context: "global",
41
43
  onSelect: ({ openScreen: open }) => open({ screenId: "stats" }),
42
44
  });
45
+ client.addCommandCenterItem({
46
+ id: "open-config",
47
+ title: "req-kit 配置",
48
+ icon: "Settings",
49
+ keywords: ["req", "config", "配置", "设置"],
50
+ context: "global",
51
+ onSelect: ({ openScreen: open }) => open({ screenId: "config" }),
52
+ });
43
53
  client.addCommandCenterItem({
44
54
  id: "open-panel",
45
55
  title: "这个工作区的需求(req-kit)",
@@ -1,5 +1,5 @@
1
1
  import type { PluginServerContext } from "@getpaseo/plugin/server";
2
- import { fileRpc, listRpc, preferences, runRpc, statsRpc, statusRpc, versionRpc } from "./shared/contracts";
2
+ import { configRpc, fileRpc, listRpc, modelsRpc, preferences, runRpc, setAgentsRpc, statsRpc, statusRpc, versionRpc } from "./shared/contracts";
3
3
  import { createHandlers } from "./server/handlers";
4
4
  import { kitHomeOf, reqApi } from "./server/req";
5
5
 
@@ -35,6 +35,9 @@ export default function contribute(server: PluginServerContext) {
35
35
  server.handle(fileRpc, (input) => handlers.file(input));
36
36
  server.handle(statsRpc, (input) => handlers.stats(input));
37
37
  server.handle(runRpc, (input) => handlers.run(input));
38
+ server.handle(modelsRpc, () => handlers.models());
39
+ server.handle(configRpc, (input) => handlers.config(input));
40
+ server.handle(setAgentsRpc, (input) => handlers.setAgents(input));
38
41
  return () => {
39
42
  unsubscribe();
40
43
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "req-kit-paseo-plugin",
3
- "version": "1.11.0",
3
+ "version": "1.12.0-beta.3",
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",
@@ -1,7 +1,7 @@
1
1
  // The daemon side: each RPC is one `req api` call. Management actions are refused here too
2
2
  // when the plugin is set to read-only, not only hidden in the app.
3
3
  import type { RpcInput } from "@getpaseo/plugin";
4
- import type { fileRpc, KitInfo, Requirement, RequirementDetail, runRpc, statsRpc, StatsSummary, statusRpc } from "../shared/contracts";
4
+ import type { configRpc, ConfigViewData, fileRpc, KitInfo, ModelsData, Requirement, RequirementDetail, runRpc, setAgentsRpc, statsRpc, StatsSummary, statusRpc } from "../shared/contracts";
5
5
  import { coalesce, type ReqRunner } from "./req";
6
6
 
7
7
  export interface Handlers {
@@ -11,6 +11,9 @@ export interface Handlers {
11
11
  file(input: RpcInput<typeof fileRpc>): Promise<{ file: string; size: number; truncated: boolean; content: string }>;
12
12
  stats(input: RpcInput<typeof statsRpc>): Promise<StatsSummary>;
13
13
  run(input: RpcInput<typeof runRpc>): Promise<{ ok: boolean; message: string }>;
14
+ models(): Promise<ModelsData>;
15
+ config(input: RpcInput<typeof configRpc>): Promise<ConfigViewData>;
16
+ setAgents(input: RpcInput<typeof setAgentsRpc>): Promise<{ ok: boolean; message: string }>;
14
17
  }
15
18
 
16
19
  export function createHandlers(input: { req: ReqRunner; kitHome: () => string; readOnly: () => Promise<boolean> }): Handlers {
@@ -35,11 +38,26 @@ export function createHandlers(input: { req: ReqRunner; kitHome: () => string; r
35
38
  else if (since) args.push("--since", since);
36
39
  return (await req(args)) as StatsSummary;
37
40
  },
38
- async run({ action, repo, featureDir, verifyEnv }) {
41
+ async run({ action, repo, featureDir, verifyEnv, text }) {
39
42
  if (await input.readOnly()) return { ok: false, message: "插件设置为只读:不能在这里操作需求。可以在插件设置中关闭只读,或在终端用 req 命令。" };
40
- const extra = action === "verify" && verifyEnv ? ["--verify-env", verifyEnv] : [];
43
+ if (action === "reply" && !text?.trim()) return { ok: false, message: "回复是空的" };
44
+ const extra = action === "verify" && verifyEnv ? ["--verify-env", verifyEnv] : action === "reply" ? [text!.trim()] : [];
41
45
  const result = (await req(["run", action, featureDir, "--repo", repo, ...extra], { timeoutMs: 200_000 })) as { ok: boolean; message: string };
42
46
  return { ok: result.ok, message: result.message };
43
47
  },
48
+ async config({ repo }) {
49
+ return (await req(repo ? ["config", "--repo", repo] : ["config", "--machine"])) as ConfigViewData;
50
+ },
51
+ async models() {
52
+ return (await req(["models"], { timeoutMs: 120_000 })) as ModelsData;
53
+ },
54
+ async setAgents(agents) {
55
+ if (await input.readOnly()) return { ok: false, message: "插件设置为只读:不能在这里修改。可以关闭只读,或在终端运行 req configure。" };
56
+ try {
57
+ return (await req(["set-agents", "--json", JSON.stringify(agents)])) as { ok: boolean; message: string };
58
+ } catch (error) {
59
+ return { ok: false, message: (error as Error).message };
60
+ }
61
+ },
44
62
  };
45
63
  }
@@ -34,6 +34,14 @@ export interface Requirement {
34
34
  updatedAt: string;
35
35
  usage: Usage | null;
36
36
  present: boolean;
37
+ /** req-kit 1.12+: what a requirement that needs a person is waiting for; absent before. */
38
+ waiting?: {
39
+ since: string;
40
+ questions: string[];
41
+ reason: string | null;
42
+ card: { system: string; id: string; url: string | null; requester: string | null } | null;
43
+ canReply: boolean;
44
+ } | null;
37
45
  }
38
46
 
39
47
  export interface Step {
@@ -139,7 +147,7 @@ export const statsRpc = defineRpc({
139
147
  output: z.custom<StatsSummary>(),
140
148
  });
141
149
 
142
- export const ACTIONS = ["approve", "stop", "resume", "verify", "sync"] as const;
150
+ export const ACTIONS = ["approve", "stop", "resume", "verify", "sync", "reply"] as const;
143
151
  export type Action = (typeof ACTIONS)[number];
144
152
 
145
153
  export const runRpc = defineRpc({
@@ -148,10 +156,78 @@ export const runRpc = defineRpc({
148
156
  action: z.enum(ACTIONS),
149
157
  /** For verify: local | deployed. */
150
158
  verifyEnv: z.enum(["local", "deployed"]).optional(),
159
+ /** For reply (1.12): the answer, passed to the requirement's chat. */
160
+ text: z.string().max(8000).optional(),
151
161
  }),
152
162
  output: z.object({ ok: z.boolean(), message: z.string() }),
153
163
  });
154
164
 
165
+ /** req api config (1.12): every setting, its value in words and which file set it. */
166
+ export type ConfigLayer = "machine" | "team" | "project";
167
+
168
+ export interface ConfigItemData {
169
+ key: string;
170
+ label: string;
171
+ description: string;
172
+ display: string;
173
+ value: unknown;
174
+ setBy: ConfigLayer[];
175
+ scope: "any" | "machine" | "machine_team";
176
+ changed: boolean;
177
+ }
178
+
179
+ export interface ConfigViewData {
180
+ sources: Array<{ layer: ConfigLayer; path: string }>;
181
+ ignored: string[];
182
+ error: string | null;
183
+ repo: string | null;
184
+ groups: Array<{ id: string; title: string; description: string; items: ConfigItemData[] }>;
185
+ }
186
+
187
+ /** The configuration in a project (repo), or this machine's without a project. */
188
+ export const configRpc = defineRpc({
189
+ name: "req.config",
190
+ input: z.object({ repo: z.string().optional() }),
191
+ output: z.custom<ConfigViewData>(),
192
+ });
193
+
194
+ /** Agent settings, as req-kit writes them (1.12). */
195
+ export interface AgentProfileData {
196
+ provider?: string;
197
+ paseoProfile?: string;
198
+ modeId?: string;
199
+ [key: string]: unknown;
200
+ }
201
+
202
+ export interface AgentChoiceData {
203
+ id: string;
204
+ label: string;
205
+ provider: string;
206
+ profile: AgentProfileData;
207
+ }
208
+
209
+ export interface ModelsData {
210
+ current: { builder?: AgentProfileData; reviewer?: AgentProfileData; fallback?: AgentProfileData | null } | null;
211
+ choices: AgentChoiceData[];
212
+ recommended: { builder?: AgentProfileData; reviewer?: AgentProfileData; fallback?: AgentProfileData | null } | null;
213
+ }
214
+
215
+ /** What Paseo offers for builder, reviewer and the fallback model, and what is set now. */
216
+ export const modelsRpc = defineRpc({
217
+ name: "req.models",
218
+ input: z.object({}),
219
+ output: z.custom<ModelsData>(),
220
+ });
221
+
222
+ const profile = z.record(z.string(), z.unknown());
223
+
224
+ /** Writes the agents to this machine's req-kit config (refused when the plugin is read-only). */
225
+ export const setAgentsRpc = defineRpc({
226
+ name: "req.setAgents",
227
+ input: z.object({ builder: profile, reviewer: profile, fallback: profile.nullable() }),
228
+ output: z.object({ ok: z.boolean(), message: z.string() }),
229
+ });
230
+
155
231
  export const preferences = defineSettings({
156
232
  id: "preferences",
157
233
  scope: "host",
@@ -18,10 +18,9 @@
18
18
 
19
19
  运行 `git rev-parse --git-dir --git-common-dir` 和 `git branch --show-current`:
20
20
 
21
- - **两个路径相同(项目主目录)**:说明上面的好处,用一个选择题请用户选:
22
- 1. **开发时自动新建工作区**(推荐,不熟悉 worktree 也没关系):就在这个对话里谈需求,需求说明先写在这里、不提交;启动开发时流水线会为这个需求新建一个 worktree 工作区,把需求说明移过去提交,在那里开发。这个对话不用动,之后的进度、暂停、继续、验收都在这里说。记下这个选择,第 9 步用"启动(自动新建工作区)"命令。
23
- 2. **自己新建工作区**:在 Paseo 中 **New workspace** → 选择这个项目 → **Isolation** 选 **New worktree** → 起点选主分支(如 `origin/main`),然后在新工作区的对话里重新输入 `/req`。
24
- 3. **就在这里开发**:到第 3 步之后新建功能分支(见下)。开发期间这个目录会被占用几个小时。
21
+ - **两个路径相同(项目主目录)**:不用问,默认**开发时自动新建工作区**:就在这个对话里谈需求,需求说明先写在这里、不提交;启动开发时流水线为这个需求新建一个 worktree 工作区,把需求说明移过去提交,在那里开发。这个对话不用动,之后的进度、暂停、继续、验收都在这里说。第 8 步的确认摘要里写一句"将在新的工作区中开发",第 9 步用"启动(自动新建工作区)"命令。
22
+ - 用户主动说要"就在这里开发"时:到第 3 步之后新建功能分支(见下),开发期间这个目录会被占用几个小时;第 9 步用普通的"启动"命令。
23
+ - 用户想自己建工作区时:在 Paseo 中 **New workspace** → 选择这个项目 → **Isolation** 选 **New worktree** → 起点选主分支(如 `origin/main`),然后在新工作区的对话里重新输入 `/req`。
25
24
  - **两个路径不同(已在 worktree 中)**:直接继续,使用当前分支,不要再新建分支。
26
25
  - **不是 git 仓库**:直接继续,跳过所有分支相关的操作。
27
26
 
@@ -35,7 +34,7 @@
35
34
 
36
35
  然后确定功能目录:`docs/features/<YYYYMMDD>-<英文短名>/`,例如 `docs/features/20261012-sms-login/`,英文短名用小写字母和连字符。创建该目录。
37
36
 
38
- 只有用户在第 1 步选了"就在这里开发"、且当前在默认分支(如 `main`、`master`)上时,才新建并切换到分支 `feature/<功能目录名>`。选了"自动新建工作区"的,不建分支、不提交,留在当前分支上即可。不要动工作区里已有的、与本需求无关的改动。流水线不会在默认分支上提交代码。
37
+ 只有用户在第 1 步说了要"就在这里开发"、且当前在默认分支(如 `main`、`master`)上时,才新建并切换到分支 `feature/<功能目录名>`。自动新建工作区的,不建分支、不提交,留在当前分支上即可。不要动工作区里已有的、与本需求无关的改动。流水线不会在默认分支上提交代码。
39
38
 
40
39
  ## 4. 判断类型和规模,确定档位和执行方式
41
40
 
@@ -46,20 +45,19 @@
46
45
  - **重构**:使用者看到的行为不变,只改进代码(整理结构、升级依赖、提高可维护性)。流水线会先用测试锁住现有行为再小步改动。访谈重点问清**改进目标**(怎么检查达成了)和**哪些行为必须保持不变**。
47
46
  - **评估**:只想知道能不能做、怎么做、影响多大、大概要多少工作量,先不写代码。流水线只调研、写评估报告,由另一个模型审阅。访谈重点问清**要回答的问题**和评估结论用来做什么决定。评估用标准档、连续执行,下面关于档位和执行方式的问题不用问。
48
47
 
49
- 再判断**规模**,向用户推荐一个档位,说明理由,请用户确认:
48
+ 再判断**规模**,选定一个档位,不用单独问:在第 8 步的确认摘要里写明档位和一句理由,用户想改再改。
50
49
 
51
50
  - **快速档**:小改动,影响面窄,不涉及数据结构、权限、计费或对外接口。例如改文案或样式、加一个配置项、调整一个字段的显示。之后按 `small.md` 手册继续(从它的"访谈"一步开始)。
52
51
  - **标准档**:其他情况,继续本手册。
53
52
 
54
53
  拿不准时选标准档。访谈中发现需求比预想的复杂(涉及数据、权限、多个角色或流程),改为标准档并告诉用户。
55
54
 
56
- 标准档再确定**执行方式**。项目根目录的 `.requirement-pipeline.json` 中已经写了 `phases.mode` 的,按它来,不问。否则用一个选择题请用户选:
55
+ 标准档再确定**执行方式**,同样不单独问:项目根目录的 `.requirement-pipeline.json` 中写了 `phases.mode` 的按它来,否则用**连续**(研发完成后流水线接着做测试验证,一次跑完)。确认摘要里写明执行方式。用户在谈需求时说了要先提测、由测试同事测,或者只要研发的,改用:
57
56
 
58
- - **连续**(推荐,默认):研发完成后流水线接着做测试验证,一次跑完。适合本地就能验证的项目。
59
- - **分阶段**:研发完成、写好提测说明后停下来通知你,由你或测试同事决定什么时候开始测试验证(可以先部署到测试环境、准备测试数据,也可以预约时间)。适合有专门测试、需要测试环境的项目。
57
+ - **分阶段**:研发完成、写好提测说明后停下来通知,由用户或测试同事决定什么时候开始测试验证(可以先部署到测试环境、准备测试数据,也可以预约时间)。
60
58
  - **只做研发**:只做到提测说明和研发报告,测试由人完成。
61
59
 
62
- 选了"分阶段"或"只做研发"的,第 6 步在需求说明头部写上 `- 执行方式:分阶段` 或 `- 执行方式:只做研发`。快速档一律连续,不问。
60
+ 用了"分阶段"或"只做研发"的,第 6 步在需求说明头部写上 `- 执行方式:分阶段` 或 `- 执行方式:只做研发`。快速档一律连续。
63
61
 
64
62
  ## 5. 访谈
65
63
 
@@ -85,12 +83,14 @@
85
83
 
86
84
  先执行"卡片字段"命令。输出"跳过"时不用管;列出了字段时(团队对接了需求管理等外部系统),按输出的说明从需求说明中填出这些字段,判断不了的问需求方,放进摘要里一起确认,确认后写入功能目录的 `card.json`。流水线开始时会用它在外部系统中建卡。
87
85
 
88
- 按 `spec-writing` 中"确认"一节:给出 10 行以内的摘要,确认后把状态改为"已确认"并提交(有 `card.json` 的一起提交)。选了"自动新建工作区"的,只改状态、不提交(不在默认分支上提交),启动时流水线会在新工作区中提交。
86
+ 按 `spec-writing` 中"确认"一节:给出 10 行以内的摘要。摘要最后一行写明怎么开发,例如"确认后立即开始:新的工作区、标准档(涉及订单数据)、连续执行",并说明想改其中哪一项直接说。确认后把状态改为"已确认"并提交(有 `card.json` 的一起提交)。自动新建工作区的,只改状态、不提交(不在默认分支上提交),启动时流水线会在新工作区中提交。
89
87
 
90
88
  ## 9. 启动开发
91
89
 
92
- 告诉用户需求说明保存在哪里,然后问是现在开始开发,还是预约一个时间开始(例如晚上或明天早上,机器空闲、额度充足的时候)。说明三点:开发在后台进行,通常需要几个小时;过程中不需要盯着;有进展、需要他们决定或完成时,会在这个对话里通知。
90
+ 用户确认需求说明就是同意开始,不再另外问"现在开始还是预约":立即执行 `/req` 中的"启动"命令(自动新建工作区的执行"启动(自动新建工作区)"命令),把输出中的新工作区位置告诉用户(在 Paseo 侧边栏中可以看到它)。然后告诉用户需求说明保存在哪里,并说明三点:开发在后台进行,通常需要几个小时;过程中不需要盯着;有进展、需要他们决定或完成时,会在这个对话里通知。
93
91
 
94
- 执行方式是"分阶段"的,顺便问用户是否要在研发完成后的某个时间自动开始测试验证;要的话在启动命令后加 `--verify-at <时间>`。
92
+ 例外:
95
93
 
96
- 用户想预约时,执行"预约启动"命令(选了"自动新建工作区"的加上 `--worktree`),把预约的时间告诉用户,并说明到时会在这个对话里自动开始、想取消就说"先停一下"。用户同意现在开始后,执行 `/req` 中的"启动"命令;第 1 步选了"自动新建工作区"的,执行"启动(自动新建工作区)"命令,并把输出中的新工作区位置告诉用户(在 Paseo 侧边栏中可以看到它)。用户没有同意时,告诉他们之后随时说"开始开发"即可。
94
+ - 用户在确认时说了要晚一点开始("今晚再跑""明天早上开始"):执行"预约启动"命令(自动新建工作区的加上 `--worktree`),把预约的时间告诉用户,并说明到时会在这个对话里自动开始、想取消就说"先停一下"。
95
+ - 流水线开始后发来额度提醒、并给出了恢复时间时:转告用户,问是否暂停、在恢复时间预约继续;用户没回答也不影响,流水线遇到限流会自己等到额度恢复后继续。
96
+ - 执行方式是"分阶段"、用户说了希望研发完成后在某个时间自动开始测试验证的:在启动命令后加 `--verify-at <时间>`。