paseo-req-kit 1.12.0-beta.2 → 1.12.0-beta.4

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 (35) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/req.mjs +4 -0
  3. package/contracts/cli-json.md +3 -1
  4. package/contracts/notifier-event.md +17 -1
  5. package/docs/adr/0031-fewer-interventions.md +8 -0
  6. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
  7. 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
  8. package/engine/src/api.ts +42 -3
  9. package/engine/src/card-cmd.ts +13 -0
  10. package/engine/src/card.ts +29 -0
  11. package/engine/src/config-view.ts +289 -0
  12. package/engine/src/config.ts +42 -2
  13. package/engine/src/doctor.ts +14 -0
  14. package/engine/src/factory.ts +115 -4
  15. package/engine/src/messages.ts +22 -0
  16. package/engine/src/notifiers.ts +32 -1
  17. package/engine/src/notify-test.ts +8 -2
  18. package/engine/src/paseo-runtime.ts +9 -0
  19. package/engine/src/pipeline.ts +61 -4
  20. package/engine/src/reply.ts +59 -0
  21. package/engine/src/runtime.ts +2 -0
  22. package/engine/src/state.ts +11 -1
  23. package/notifiers/im-template.mjs +31 -5
  24. package/package.json +1 -1
  25. package/paseo-plugin/client/config.tsx +216 -0
  26. package/paseo-plugin/client/data.ts +7 -2
  27. package/paseo-plugin/client/format.ts +1 -0
  28. package/paseo-plugin/client/inbox.tsx +173 -0
  29. package/paseo-plugin/client/overview.tsx +7 -3
  30. package/paseo-plugin/index.client.tsx +10 -0
  31. package/paseo-plugin/index.server.ts +2 -1
  32. package/paseo-plugin/package.json +1 -1
  33. package/paseo-plugin/server/handlers.ts +8 -3
  34. package/paseo-plugin/shared/contracts.ts +41 -2
  35. 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
- }: 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,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,6 +1,6 @@
1
1
  {
2
2
  "name": "req-kit-paseo-plugin",
3
- "version": "1.12.0-beta.2",
3
+ "version": "1.12.0-beta.4",
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, 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
- 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()] : [];
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;
@@ -184,7 +223,7 @@ const profile = z.record(z.string(), z.unknown());
184
223
 
185
224
  /** Writes the agents to this machine's req-kit config (refused when the plugin is read-only). */
186
225
  export const setAgentsRpc = defineRpc({
187
- name: "req.setAgents",
226
+ name: "req.set-agents",
188
227
  input: z.object({ builder: profile, reviewer: profile, fallback: profile.nullable() }),
189
228
  output: z.object({ ok: z.boolean(), message: z.string() }),
190
229
  });
@@ -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. 需求方确认后启动开发