paseo-req-kit 1.12.0-beta.2 → 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.
- package/CHANGELOG.md +19 -0
- package/bin/req.mjs +4 -0
- package/contracts/cli-json.md +3 -1
- package/contracts/notifier-event.md +17 -1
- package/docs/adr/0031-fewer-interventions.md +8 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
- 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 +37 -1
- package/engine/src/api.ts +42 -3
- package/engine/src/card-cmd.ts +13 -0
- package/engine/src/card.ts +29 -0
- package/engine/src/config-view.ts +289 -0
- package/engine/src/config.ts +42 -2
- package/engine/src/doctor.ts +14 -0
- package/engine/src/factory.ts +115 -4
- package/engine/src/messages.ts +22 -0
- package/engine/src/notifiers.ts +32 -1
- package/engine/src/notify-test.ts +8 -2
- package/engine/src/paseo-runtime.ts +9 -0
- package/engine/src/pipeline.ts +61 -4
- package/engine/src/reply.ts +59 -0
- package/engine/src/runtime.ts +2 -0
- package/engine/src/state.ts +11 -1
- package/notifiers/im-template.mjs +31 -5
- package/package.json +1 -1
- package/paseo-plugin/client/config.tsx +216 -0
- package/paseo-plugin/client/data.ts +7 -2
- package/paseo-plugin/client/format.ts +1 -0
- package/paseo-plugin/client/inbox.tsx +173 -0
- package/paseo-plugin/client/overview.tsx +7 -3
- package/paseo-plugin/index.client.tsx +10 -0
- package/paseo-plugin/index.server.ts +2 -1
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +8 -3
- package/paseo-plugin/shared/contracts.ts +40 -1
- package/skills/req/playbooks/unattended.md +1 -0
|
@@ -108,4 +108,5 @@ export const ACTION_TEXT = {
|
|
|
108
108
|
resume: { label: "继续", confirm: "从中断处继续?需求说明改过的话,只处理变化的部分。" },
|
|
109
109
|
verify: { label: "开始测试验证", confirm: "开始测试验证?选择在哪里验证。" },
|
|
110
110
|
sync: { label: "同步到外部系统", confirm: "把这个需求同步到对接的外部系统:补发没有成功的调用、补建卡片、更新状态。" },
|
|
111
|
+
reply: { label: "发送回复", confirm: "把回复转给这个需求的对话,由它更新需求说明并继续开发。" },
|
|
111
112
|
} as const;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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,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, modelsRpc, preferences, runRpc, setAgentsRpc, 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
|
|
|
@@ -36,6 +36,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
36
36
|
server.handle(statsRpc, (input) => handlers.stats(input));
|
|
37
37
|
server.handle(runRpc, (input) => handlers.run(input));
|
|
38
38
|
server.handle(modelsRpc, () => handlers.models());
|
|
39
|
+
server.handle(configRpc, (input) => handlers.config(input));
|
|
39
40
|
server.handle(setAgentsRpc, (input) => handlers.setAgents(input));
|
|
40
41
|
return () => {
|
|
41
42
|
unsubscribe();
|
|
@@ -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, ModelsData, Requirement, RequirementDetail, runRpc, setAgentsRpc, 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 {
|
|
@@ -12,6 +12,7 @@ export interface Handlers {
|
|
|
12
12
|
stats(input: RpcInput<typeof statsRpc>): Promise<StatsSummary>;
|
|
13
13
|
run(input: RpcInput<typeof runRpc>): Promise<{ ok: boolean; message: string }>;
|
|
14
14
|
models(): Promise<ModelsData>;
|
|
15
|
+
config(input: RpcInput<typeof configRpc>): Promise<ConfigViewData>;
|
|
15
16
|
setAgents(input: RpcInput<typeof setAgentsRpc>): Promise<{ ok: boolean; message: string }>;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -37,12 +38,16 @@ export function createHandlers(input: { req: ReqRunner; kitHome: () => string; r
|
|
|
37
38
|
else if (since) args.push("--since", since);
|
|
38
39
|
return (await req(args)) as StatsSummary;
|
|
39
40
|
},
|
|
40
|
-
async run({ action, repo, featureDir, verifyEnv }) {
|
|
41
|
+
async run({ action, repo, featureDir, verifyEnv, text }) {
|
|
41
42
|
if (await input.readOnly()) return { ok: false, message: "插件设置为只读:不能在这里操作需求。可以在插件设置中关闭只读,或在终端用 req 命令。" };
|
|
42
|
-
|
|
43
|
+
if (action === "reply" && !text?.trim()) return { ok: false, message: "回复是空的" };
|
|
44
|
+
const extra = action === "verify" && verifyEnv ? ["--verify-env", verifyEnv] : action === "reply" ? [text!.trim()] : [];
|
|
43
45
|
const result = (await req(["run", action, featureDir, "--repo", repo, ...extra], { timeoutMs: 200_000 })) as { ok: boolean; message: string };
|
|
44
46
|
return { ok: result.ok, message: result.message };
|
|
45
47
|
},
|
|
48
|
+
async config({ repo }) {
|
|
49
|
+
return (await req(repo ? ["config", "--repo", repo] : ["config", "--machine"])) as ConfigViewData;
|
|
50
|
+
},
|
|
46
51
|
async models() {
|
|
47
52
|
return (await req(["models"], { timeoutMs: 120_000 })) as ModelsData;
|
|
48
53
|
},
|
|
@@ -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,41 @@ 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
|
+
|
|
155
194
|
/** Agent settings, as req-kit writes them (1.12). */
|
|
156
195
|
export interface AgentProfileData {
|
|
157
196
|
provider?: string;
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
- **回答了问题、写了补充说明**:更新 `alignment.md` 和 `spec.md`,把已回答的问题从 card-ai.md 中删掉(问题编号不要重用,新问题接着往后编),有新问题就加上,更新摘要,再运行 `<publish>`。
|
|
82
82
|
- **需求方修改了卡片描述**:重新读 `card-source.md`,同上更新并发布。
|
|
83
83
|
- **"确认"一行写的不是同意**(例如"修改:…"):当作修改意见处理,更新后发布,请需求方再次确认。
|
|
84
|
+
- **按自动确认处理**(团队打开了 `autoConfirm`,小改动、没有待答问题的卡片到时没有异议):与"确认:同意"相同,进入第 6 步,第 6 步中 spec 顶部写 `- 状态:已确认(<日期>,到时没有异议,自动确认)`。
|
|
84
85
|
- **确认:同意**:进入第 6 步。确认时仍有没回答、而且会影响做法的问题:不要启动,在 card-ai.md 中说明还差哪些,发布后等待。
|
|
85
86
|
|
|
86
87
|
## 6. 需求方确认后启动开发
|