paseo-req-kit 0.0.0-stage → 1.8.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 +283 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/SCOPE.md +11 -0
- package/bin/locate.mjs +46 -0
- package/bin/req-doctor.mjs +21 -0
- package/bin/req.mjs +179 -0
- package/bin/start-pipeline +21 -0
- package/bin/start-pipeline.mjs +22 -0
- package/config.example.json +132 -0
- package/contracts/README.md +18 -0
- package/contracts/feature-files.md +117 -0
- package/contracts/notifier-event.md +72 -0
- package/contracts/result-file.md +21 -0
- package/contracts/spec.md +36 -0
- package/contracts/state.md +49 -0
- package/contracts/verify.md +41 -0
- package/docs/adr/0001-step-agents-under-origin-chat.md +15 -0
- package/docs/adr/0002-no-directory-switch-in-chat.md +15 -0
- package/docs/adr/0003-script-engine-not-hub.md +15 -0
- package/docs/adr/0004-result-files.md +15 -0
- package/docs/adr/0005-no-default-branch-local-commits-only.md +15 -0
- package/docs/adr/0006-skill-install-targets.md +15 -0
- package/docs/adr/0007-own-interview-skill.md +15 -0
- package/docs/adr/0008-entries-do-not-call-entries.md +15 -0
- package/docs/adr/0009-versioned-install-layout.md +15 -0
- package/docs/adr/0010-generated-feature-overview.md +15 -0
- package/docs/adr/0011-resume-after-completion.md +15 -0
- package/docs/adr/0012-review-panel.md +18 -0
- package/docs/adr/0013-no-release-tooling.md +15 -0
- package/docs/adr/0014-notifier-slot.md +19 -0
- package/docs/adr/0015-incremental-continue.md +17 -0
- package/docs/adr/0016-upgrade-from-tags.md +19 -0
- package/docs/adr/0017-use-paseo-capabilities.md +18 -0
- package/docs/adr/0018-own-worktree-option.md +21 -0
- package/docs/adr/0019-parallel-orchestration.md +21 -0
- package/docs/adr/0020-scheduling-through-the-chat.md +21 -0
- package/docs/adr/0021-npm-version-manager.md +19 -0
- package/docs/adr/0022-develop-and-verify-phases.md +32 -0
- package/docs/adr/0023-types-gate-slices.md +54 -0
- package/docs/adr/0024-confirm-early-review-axes.md +27 -0
- package/docs/adr/0025-joint-requirements.md +27 -0
- package/docs/adr/README.md +29 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +96 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +348 -0
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +85 -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 +356 -0
- package/engine/prompts/00-shared-context.md +104 -0
- package/engine/prompts/02-spec-review.md +46 -0
- package/engine/prompts/03-design.md +73 -0
- package/engine/prompts/03a-design-proposal.md +30 -0
- package/engine/prompts/04-design-review.md +73 -0
- package/engine/prompts/04b-design-revise.md +41 -0
- package/engine/prompts/05-implement.md +72 -0
- package/engine/prompts/06-code-review.md +70 -0
- package/engine/prompts/07-review-fix.md +41 -0
- package/engine/prompts/07b-handoff.md +45 -0
- package/engine/prompts/08-service-start.md +59 -0
- package/engine/prompts/09-verify.md +90 -0
- package/engine/prompts/10-defect-fix.md +41 -0
- package/engine/prompts/10b-reverify.md +43 -0
- package/engine/prompts/11-report.md +112 -0
- package/engine/prompts/browser-verify.md +13 -0
- package/engine/prompts/incremental.md +22 -0
- package/engine/prompts/joint.md +55 -0
- package/engine/prompts/panel-member.md +38 -0
- package/engine/prompts/paseo-services.md +9 -0
- package/engine/prompts/related-repos.md +7 -0
- package/engine/prompts/review-axis-spec.md +10 -0
- package/engine/prompts/review-axis-standards.md +11 -0
- package/engine/prompts/types/assessment.md +41 -0
- package/engine/prompts/types/defect.md +55 -0
- package/engine/prompts/types/refactor.md +39 -0
- package/engine/prompts/verify-deployed.md +14 -0
- package/engine/src/activity.ts +87 -0
- package/engine/src/cli-args.ts +217 -0
- package/engine/src/config.ts +619 -0
- package/engine/src/configure.ts +237 -0
- package/engine/src/control.ts +246 -0
- package/engine/src/daemon-check.ts +104 -0
- package/engine/src/daemon-version.ts +66 -0
- package/engine/src/digest.ts +142 -0
- package/engine/src/doctor.ts +462 -0
- package/engine/src/eval.ts +324 -0
- package/engine/src/features.ts +270 -0
- package/engine/src/gate.ts +157 -0
- package/engine/src/git.ts +30 -0
- package/engine/src/joint.ts +173 -0
- package/engine/src/launch.ts +405 -0
- package/engine/src/lessons.ts +72 -0
- package/engine/src/locations.ts +55 -0
- package/engine/src/main.ts +346 -0
- package/engine/src/messages.ts +394 -0
- package/engine/src/notifiers.ts +76 -0
- package/engine/src/notify-test.ts +101 -0
- package/engine/src/paseo-json.ts +73 -0
- package/engine/src/paseo-profiles.ts +133 -0
- package/engine/src/paseo-runtime.ts +437 -0
- package/engine/src/paths.ts +10 -0
- package/engine/src/pipeline.ts +1840 -0
- package/engine/src/playbooks.ts +46 -0
- package/engine/src/preflight.ts +337 -0
- package/engine/src/prompts.ts +72 -0
- package/engine/src/queue.ts +61 -0
- package/engine/src/runtime-loader.ts +14 -0
- package/engine/src/runtime.ts +155 -0
- package/engine/src/schedule-time.ts +51 -0
- package/engine/src/smoke-test.ts +202 -0
- package/engine/src/spec-review.ts +100 -0
- package/engine/src/state.ts +333 -0
- package/engine/src/steps.ts +266 -0
- package/engine/src/usage.ts +67 -0
- package/engine/src/worktree.ts +72 -0
- package/install.mjs +928 -0
- package/notifiers/im-template.mjs +60 -0
- package/package.json +67 -4
- package/principles/INDEX.md +11 -0
- package/skills/acceptance-criteria/SKILL.md +61 -0
- package/skills/principle-delete-before-add/SKILL.md +33 -0
- package/skills/principle-dont-block-on-humans/SKILL.md +35 -0
- package/skills/principle-fix-root-cause/SKILL.md +33 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-report-honestly/SKILL.md +33 -0
- package/skills/principle-speak-to-reader/SKILL.md +33 -0
- package/skills/principle-this-requirement-only/SKILL.md +33 -0
- package/skills/req/SKILL.md +101 -0
- package/skills/req/playbooks/accept.md +65 -0
- package/skills/req/playbooks/change.md +51 -0
- package/skills/req/playbooks/incident.md +55 -0
- package/skills/req/playbooks/new.md +94 -0
- package/skills/req/playbooks/small.md +48 -0
- package/skills/req/playbooks/status.md +41 -0
- package/skills/req/playbooks/takeover.md +51 -0
- package/skills/req/playbooks/verify.md +57 -0
- package/skills/req-doctor/SKILL.md +36 -0
- package/skills/req-retro/SKILL.md +81 -0
- package/skills/req-setup/SKILL.md +118 -0
- package/skills/req-setup/templates/paseo/copy-local-files.mjs +32 -0
- package/skills/req-setup/templates/paseo/with-port.mjs +36 -0
- package/skills/requirement-interview/SKILL.md +97 -0
- package/skills/requirement-kickoff/SKILL.md +17 -0
- package/skills/result-report/SKILL.md +34 -0
- package/skills/spec-writing/SKILL.md +55 -0
- package/skills/spec-writing/spec-template.md +109 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// The narrow surface the pipeline needs from Paseo. The real implementation is
|
|
2
|
+
// paseo-runtime.ts; tests swap in a fake through REQ_PIPELINE_FAKE_RUNTIME.
|
|
3
|
+
|
|
4
|
+
export type AgentRole = "builder" | "reviewer";
|
|
5
|
+
|
|
6
|
+
export interface PendingPermission {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WaitOutcome {
|
|
12
|
+
/** Same values as the SDK's waitForFinish(). */
|
|
13
|
+
status: "idle" | "error" | "permission" | "timeout";
|
|
14
|
+
error: string | null;
|
|
15
|
+
lastMessage: string | null;
|
|
16
|
+
pendingPermissions: PendingPermission[];
|
|
17
|
+
/** Usage the agent reports (its latest snapshot), when the provider reports it. */
|
|
18
|
+
usage?: import("./usage.js").AgentUsage | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StepAgent {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/** Waits for the current turn, at most timeoutMs. Returns early on permission requests. */
|
|
24
|
+
wait(timeoutMs: number): Promise<WaitOutcome>;
|
|
25
|
+
/** Starts a new turn in the same session. Call wait() afterwards. */
|
|
26
|
+
followUp(text: string): Promise<void>;
|
|
27
|
+
archive(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StartAgentInput {
|
|
31
|
+
role: AgentRole;
|
|
32
|
+
title: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
labels: Record<string, string>;
|
|
36
|
+
/** Attach the step agent under this chat agent (shows in its Subagents track). */
|
|
37
|
+
parentAgentId: string | null;
|
|
38
|
+
/** Agent settings to launch with (the role's, or a review panel member's), Paseo profiles resolved. */
|
|
39
|
+
profile?: import("./config.js").AgentProfile;
|
|
40
|
+
/** Place the agent in this Paseo workspace (a worktree created for the requirement); still a subagent of parentAgentId. */
|
|
41
|
+
workspaceId?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Where Paseo-managed services live: the chat's workspace, or the requirement's own worktree. */
|
|
45
|
+
export type ServiceTarget = { agentId: string } | { workspaceId: string };
|
|
46
|
+
|
|
47
|
+
export interface WorktreeRequest {
|
|
48
|
+
/** Checkout to cut the worktree from (the chat's directory). */
|
|
49
|
+
sourceRepo: string;
|
|
50
|
+
/** Branch or commit the new branch starts from. */
|
|
51
|
+
baseRef: string;
|
|
52
|
+
branchName: string;
|
|
53
|
+
title: string;
|
|
54
|
+
/** The chat; Paseo shows the workspace as created from it. */
|
|
55
|
+
callerAgentId: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A paseo.json service the pipeline runs through Paseo (supervised, own port per worktree). */
|
|
59
|
+
export interface ManagedService {
|
|
60
|
+
name: string;
|
|
61
|
+
/** Address to use: Paseo's proxy URL when there is one, otherwise http://127.0.0.1:<port>. */
|
|
62
|
+
url: string | null;
|
|
63
|
+
port: number | null;
|
|
64
|
+
running: boolean;
|
|
65
|
+
health: "healthy" | "unhealthy" | null;
|
|
66
|
+
exitCode: number | null;
|
|
67
|
+
/** Last lines of the service's terminal, when they could be read. */
|
|
68
|
+
output: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One subscription window of a provider (Paseo's provider usage), e.g. "5 小时" at 92%. */
|
|
72
|
+
export interface ProviderQuota {
|
|
73
|
+
provider: string;
|
|
74
|
+
displayName: string;
|
|
75
|
+
window: string;
|
|
76
|
+
usedPct: number | null;
|
|
77
|
+
resetsAt: string | null;
|
|
78
|
+
tone: "default" | "ok" | "warning" | "danger" | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A recent thing an agent did, in plain words, e.g. "运行命令:npm test". */
|
|
82
|
+
export interface ActivityItem {
|
|
83
|
+
at: string;
|
|
84
|
+
text: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ScheduledMessage {
|
|
88
|
+
agentId: string;
|
|
89
|
+
name: string;
|
|
90
|
+
prompt: string;
|
|
91
|
+
/** One run at this time; or `cron` for a recurring one. */
|
|
92
|
+
at?: Date;
|
|
93
|
+
cron?: string;
|
|
94
|
+
/** Recurring schedules stop by themselves at this time. */
|
|
95
|
+
expiresAt?: Date;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ScheduledAgent {
|
|
99
|
+
name: string;
|
|
100
|
+
prompt: string;
|
|
101
|
+
cron: string;
|
|
102
|
+
cwd: string;
|
|
103
|
+
profile: import("./config.js").AgentProfile;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AgentRuntime {
|
|
107
|
+
/**
|
|
108
|
+
* Checks that the chat which launched the pipeline exists and works in `cwd`.
|
|
109
|
+
* Step agents are created under that chat, and Paseo gives a subagent its parent's
|
|
110
|
+
* working directory, so the two must match. Throws with a user-facing reason otherwise.
|
|
111
|
+
*/
|
|
112
|
+
verifyOrigin(agentId: string, cwd: string): Promise<void>;
|
|
113
|
+
startAgent(input: StartAgentInput): Promise<StepAgent>;
|
|
114
|
+
/** Delivers a message into an existing agent's conversation without interrupting it. */
|
|
115
|
+
notifyAgent(agentId: string, text: string): Promise<void>;
|
|
116
|
+
/** Interrupts the agent's current turn and gives it `text` instead (used to stop a step). */
|
|
117
|
+
interruptAgent(agentId: string, text: string): Promise<void>;
|
|
118
|
+
/** The daemon's agent profiles (Paseo settings → Agent profiles); null when they cannot be read. */
|
|
119
|
+
listPaseoProfiles(): Promise<import("./paseo-profiles.js").PaseoProfile[] | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Names of the service scripts (paseo.json) of the workspace the chat works in. Null when
|
|
122
|
+
* Paseo cannot run them for us: no chat, an older daemon, or no services configured.
|
|
123
|
+
*/
|
|
124
|
+
listServices(target: ServiceTarget): Promise<string[] | null>;
|
|
125
|
+
/** (Re)starts the services so they run the current code, and waits until they are ready or failed. */
|
|
126
|
+
startServices(target: ServiceTarget, names: string[], readyTimeoutMs: number): Promise<ManagedService[]>;
|
|
127
|
+
stopServices(target: ServiceTarget, names: string[]): Promise<void>;
|
|
128
|
+
/** Creates a Paseo worktree workspace (paseo.json setup runs in the background). */
|
|
129
|
+
createWorktree(request: WorktreeRequest): Promise<{ workspaceId: string; directory: string }>;
|
|
130
|
+
/** Waits for the workspace's paseo.json setup; "none" when there is nothing to wait for or it cannot be read. */
|
|
131
|
+
waitForWorkspaceSetup(workspaceId: string, timeoutMs: number): Promise<{ status: "completed" | "failed" | "none" | "timeout"; error: string | null }>;
|
|
132
|
+
/** Subscription windows of the given providers; null when Paseo has no usage data. */
|
|
133
|
+
providerQuota(providers: string[]): Promise<ProviderQuota[] | null>;
|
|
134
|
+
/** The agent's latest actions, oldest first; null when they cannot be read. */
|
|
135
|
+
recentActivity(agentId: string, limit: number): Promise<ActivityItem[] | null>;
|
|
136
|
+
/** Which Paseo tools step agents get (Settings → Agents); null when unknown. */
|
|
137
|
+
capabilities(): Promise<{ paseoTools: boolean; browser: boolean } | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Paseo schedule that sends `prompt` into an existing agent's conversation (the chat):
|
|
140
|
+
* once at `at`, or on `cron` (5 fields, local time zone). Returns the schedule ID, or
|
|
141
|
+
* null when this Paseo cannot schedule.
|
|
142
|
+
*/
|
|
143
|
+
scheduleMessage(input: ScheduledMessage): Promise<string | null>;
|
|
144
|
+
/** Paseo schedule that starts a fresh agent on `cron` (e.g. to run a command weekly). */
|
|
145
|
+
scheduleNewAgent(input: ScheduledAgent): Promise<string | null>;
|
|
146
|
+
cancelSchedule(id: string): Promise<void>;
|
|
147
|
+
close(): Promise<void>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface RuntimeFactoryInput {
|
|
151
|
+
config: import("./config.js").PipelineConfig;
|
|
152
|
+
log: (message: string) => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type RuntimeFactory = (input: RuntimeFactoryInput) => Promise<AgentRuntime>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Times for Paseo schedules: parsing what people type ("22:00", "明天 8:30", "+2h")
|
|
2
|
+
// and turning a moment into a one-shot cron expression in the local time zone.
|
|
3
|
+
|
|
4
|
+
export function localTimeZone(): string {
|
|
5
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** "M H D Mon *" for one moment (the schedule is limited to one run). */
|
|
9
|
+
export function cronAt(at: Date): string {
|
|
10
|
+
return `${at.getMinutes()} ${at.getHours()} ${at.getDate()} ${at.getMonth() + 1} *`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Every N minutes, as cron: minutes under an hour, otherwise whole hours. */
|
|
14
|
+
export function cronEvery(minutes: number): string {
|
|
15
|
+
if (minutes < 60) return `*/${Math.max(5, Math.round(minutes))} * * * *`;
|
|
16
|
+
return `0 */${Math.min(23, Math.max(1, Math.round(minutes / 60)))} * * *`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Accepts "22:00" (today, or tomorrow when already past), "明天 8:30" / "tomorrow 8:30",
|
|
21
|
+
* "2026-10-14 08:30", "+90m", "+2h". Returns null when it cannot be read.
|
|
22
|
+
*/
|
|
23
|
+
export function parseStartTime(text: string, now = new Date()): Date | null {
|
|
24
|
+
const t = text.trim();
|
|
25
|
+
const rel = /^\+\s*(\d+(?:\.\d+)?)\s*(m|min|分钟|h|小时)$/i.exec(t);
|
|
26
|
+
if (rel) {
|
|
27
|
+
const n = Number(rel[1]);
|
|
28
|
+
const minutes = /^(m|min|分钟)$/i.test(rel[2]) ? n : n * 60;
|
|
29
|
+
return new Date(now.getTime() + Math.round(minutes * 60_000));
|
|
30
|
+
}
|
|
31
|
+
const full = /^(\d{4})-(\d{1,2})-(\d{1,2})[ T](\d{1,2}):(\d{2})$/.exec(t);
|
|
32
|
+
if (full) {
|
|
33
|
+
const d = new Date(Number(full[1]), Number(full[2]) - 1, Number(full[3]), Number(full[4]), Number(full[5]));
|
|
34
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
35
|
+
}
|
|
36
|
+
const clock = /^(今天|明天|today|tomorrow)?\s*(\d{1,2})[::](\d{2})$/i.exec(t);
|
|
37
|
+
if (clock) {
|
|
38
|
+
const h = Number(clock[2]);
|
|
39
|
+
const m = Number(clock[3]);
|
|
40
|
+
if (h > 23 || m > 59) return null;
|
|
41
|
+
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m);
|
|
42
|
+
const tomorrow = /^(明天|tomorrow)$/i.test(clock[1] ?? "");
|
|
43
|
+
if (tomorrow || (!clock[1] && d.getTime() <= now.getTime())) d.setDate(d.getDate() + 1);
|
|
44
|
+
return d;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function describeTime(at: Date): string {
|
|
50
|
+
return at.toLocaleString("zh-CN", { hour12: false, month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
51
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Checks the parts of the pipeline that depend on your real Paseo setup.
|
|
2
|
+
//
|
|
3
|
+
// npm run smoke -- check daemon, providers, models, modes; one tiny agent per role
|
|
4
|
+
// npm run smoke -- notify --origin-agent <id> message + subagent into an existing chat
|
|
5
|
+
// npm run smoke -- detach-start start a background heartbeat (run from a Paseo chat)
|
|
6
|
+
// npm run smoke -- detach-check is that heartbeat still alive?
|
|
7
|
+
//
|
|
8
|
+
// See docs/guide/安装与升级.md, "冒烟测试", for when to run each one.
|
|
9
|
+
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { parseArgs } from "node:util";
|
|
15
|
+
import { type AgentProfile, loadConfig, type PipelineConfig } from "./config.js";
|
|
16
|
+
import { checkProfile as checkProfileWith, type Client, connect, type Report, resolveWithDaemon } from "./daemon-check.js";
|
|
17
|
+
import { defaultConfigPath } from "./locations.js";
|
|
18
|
+
|
|
19
|
+
const HEARTBEAT = path.join(os.tmpdir(), "requirement-pipeline-heartbeat.json");
|
|
20
|
+
|
|
21
|
+
function line(ok: boolean, text: string): void {
|
|
22
|
+
console.log(`${ok ? "✓" : "✗"} ${text}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const report: Report = (level, text, fix) => {
|
|
26
|
+
line(level !== "fail", text);
|
|
27
|
+
if (fix) console.log(` ${fix}`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function tinyAgent(client: Client, role: string, profile: AgentProfile): Promise<boolean> {
|
|
31
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `requirement-pipeline-smoke-${role}-`));
|
|
32
|
+
const agent = await client.agents.create({
|
|
33
|
+
config: {
|
|
34
|
+
provider: profile.provider ?? "",
|
|
35
|
+
...(profile.modeId ? { modeId: profile.modeId } : {}),
|
|
36
|
+
...(profile.thinkingOptionId ? { thinkingOptionId: profile.thinkingOptionId } : {}),
|
|
37
|
+
...(profile.featureValues ? { featureValues: profile.featureValues } : {}),
|
|
38
|
+
},
|
|
39
|
+
cwd: dir,
|
|
40
|
+
title: `[冒烟测试] ${role}`,
|
|
41
|
+
labels: { "requirement-pipeline": "smoke-test" },
|
|
42
|
+
prompt: `在当前目录创建文件 smoke.txt,内容只有一行:ok-${role}。然后用 node 执行 \`node -e "require('fs').writeFileSync('shell.txt','ran')"\`。完成后直接结束,不要做其他事。`,
|
|
43
|
+
});
|
|
44
|
+
console.log(` 已创建 agent ${agent.id},等待它完成(最多 5 分钟)……`);
|
|
45
|
+
try {
|
|
46
|
+
const result = await agent.waitForFinish(5 * 60_000);
|
|
47
|
+
if (result.status === "permission") {
|
|
48
|
+
line(false, `${role}:agent 停下来等待权限批准。请把 agents.${role}.modeId 改成免确认的模式。`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (result.status !== "idle") {
|
|
52
|
+
line(false, `${role}:agent 结束状态为 ${result.status}:${result.error ?? ""}`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const wrote = fs.existsSync(path.join(dir, "smoke.txt"));
|
|
56
|
+
const ran = fs.existsSync(path.join(dir, "shell.txt"));
|
|
57
|
+
line(wrote && ran, `${role}:agent 能写文件${wrote ? "" : "(失败)"}、能执行命令${ran ? "" : "(失败)"}`);
|
|
58
|
+
return wrote && ran;
|
|
59
|
+
} finally {
|
|
60
|
+
await agent.archive().catch(() => undefined);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function check(config: PipelineConfig): Promise<number> {
|
|
65
|
+
let client: Client;
|
|
66
|
+
try {
|
|
67
|
+
client = await connect(config);
|
|
68
|
+
line(true, `已连接 daemon:${config.daemonUrl}`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
line(false, `无法连接 daemon ${config.daemonUrl}:${(error as Error).message}`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const snapshot = await client.providers.waitForReady({ timeoutMs: 60_000 });
|
|
75
|
+
console.log(" daemon 上可用的 provider:");
|
|
76
|
+
for (const entry of snapshot.entries) {
|
|
77
|
+
console.log(` - ${entry.provider}(${entry.status})`);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
config = await resolveWithDaemon(client, config);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
line(false, (error as Error).message);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
let ok = true;
|
|
86
|
+
for (const role of ["builder", "reviewer"] as const) {
|
|
87
|
+
ok = (await checkProfileWith(client, role, config.agents[role], report)) && ok;
|
|
88
|
+
}
|
|
89
|
+
if (!ok) {
|
|
90
|
+
console.log("\n请先修正 ~/.req-kit/config.json 中的 provider/model 和 modeId,再重新运行。");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
for (const role of ["builder", "reviewer"] as const) {
|
|
94
|
+
ok = (await tinyAgent(client, role, config.agents[role])) && ok;
|
|
95
|
+
}
|
|
96
|
+
console.log(ok ? "\n全部通过。" : "\n有检查没有通过,见上面的 ✗。");
|
|
97
|
+
return ok ? 0 : 1;
|
|
98
|
+
} finally {
|
|
99
|
+
await client.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function notify(config: PipelineConfig, origin: string): Promise<number> {
|
|
104
|
+
const client = await connect(config);
|
|
105
|
+
try {
|
|
106
|
+
await client.agents.ref(origin).send(
|
|
107
|
+
"【需求开发流水线通知】这是一条冒烟测试消息,不是用户发的。请回复用户:\"流水线的通知通道工作正常。\"",
|
|
108
|
+
{ activeTurnBehavior: "steer" },
|
|
109
|
+
);
|
|
110
|
+
line(true, `已向 agent ${origin} 发送测试通知。请到该对话中确认 agent 转告了"通知通道工作正常"。`);
|
|
111
|
+
|
|
112
|
+
config = await resolveWithDaemon(client, config);
|
|
113
|
+
const child = await client.agents.create({
|
|
114
|
+
config: {
|
|
115
|
+
provider: config.agents.builder.provider ?? "",
|
|
116
|
+
...(config.agents.builder.modeId ? { modeId: config.agents.builder.modeId } : {}),
|
|
117
|
+
},
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
parent: origin,
|
|
120
|
+
title: "[冒烟测试] 子 agent",
|
|
121
|
+
labels: { "requirement-pipeline": "smoke-test" },
|
|
122
|
+
prompt: "只回复\"ok\",然后结束。不要读写任何文件。",
|
|
123
|
+
});
|
|
124
|
+
const result = await child.waitForFinish(3 * 60_000);
|
|
125
|
+
line(result.status === "idle", `已在该对话下创建子 agent ${child.id}(结束状态 ${result.status})。请确认它出现在对话的子 agent 列表中。`);
|
|
126
|
+
await child.archive().catch(() => undefined);
|
|
127
|
+
return result.status === "idle" ? 0 : 1;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
line(false, (error as Error).message);
|
|
130
|
+
return 1;
|
|
131
|
+
} finally {
|
|
132
|
+
await client.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function detachStart(): number {
|
|
137
|
+
const script = `
|
|
138
|
+
const fs = require("fs");
|
|
139
|
+
const end = Date.now() + 10 * 60_000;
|
|
140
|
+
const beat = () => fs.writeFileSync(${JSON.stringify(HEARTBEAT)}, JSON.stringify({ pid: process.pid, at: Date.now(), startedAt }));
|
|
141
|
+
const startedAt = Date.now();
|
|
142
|
+
beat();
|
|
143
|
+
const timer = setInterval(() => { if (Date.now() > end) { clearInterval(timer); return; } beat(); }, 2000);
|
|
144
|
+
`;
|
|
145
|
+
// Same detach mechanism as launch.ts.
|
|
146
|
+
const child = spawn(process.execPath, ["-e", script], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
147
|
+
child.unref();
|
|
148
|
+
console.log(`已启动后台心跳进程 ${child.pid}(持续 10 分钟),心跳文件:${HEARTBEAT}`);
|
|
149
|
+
console.log("请结束这一轮对话,等待至少 1 分钟后,再让 agent 运行 detach-check。");
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function detachCheck(): number {
|
|
154
|
+
if (!fs.existsSync(HEARTBEAT)) {
|
|
155
|
+
line(false, "找不到心跳文件,请先运行 detach-start");
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
const beat = JSON.parse(fs.readFileSync(HEARTBEAT, "utf8")) as { pid: number; at: number; startedAt: number };
|
|
159
|
+
const age = Math.round((Date.now() - beat.at) / 1000);
|
|
160
|
+
const ranFor = Math.round((beat.at - beat.startedAt) / 1000);
|
|
161
|
+
const alive = age < 10;
|
|
162
|
+
line(
|
|
163
|
+
alive,
|
|
164
|
+
alive
|
|
165
|
+
? `后台进程 ${beat.pid} 仍在运行(已运行 ${ranFor} 秒)。聊天 agent 启动的后台流水线可以在它的回合结束后继续运行。`
|
|
166
|
+
: `后台进程 ${beat.pid} 在启动后约 ${ranFor} 秒停止(最后一次心跳在 ${age} 秒前)。` +
|
|
167
|
+
"聊天 agent 的回合结束时,它启动的后台进程被终止了;需要改用插件方式启动流水线。",
|
|
168
|
+
);
|
|
169
|
+
return alive ? 0 : 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function main(): Promise<number> {
|
|
173
|
+
const { values, positionals } = parseArgs({
|
|
174
|
+
args: process.argv.slice(2),
|
|
175
|
+
options: { "origin-agent": { type: "string" }, config: { type: "string" } },
|
|
176
|
+
allowPositionals: true,
|
|
177
|
+
});
|
|
178
|
+
const command = positionals[0];
|
|
179
|
+
if (command === "detach-start") return detachStart();
|
|
180
|
+
if (command === "detach-check") return detachCheck();
|
|
181
|
+
|
|
182
|
+
const config = loadConfig(values.config ? path.resolve(values.config) : defaultConfigPath()).config;
|
|
183
|
+
if (command === "check") return check(config);
|
|
184
|
+
if (command === "notify") {
|
|
185
|
+
const origin = values["origin-agent"] ?? process.env.PASEO_AGENT_ID;
|
|
186
|
+
if (!origin) {
|
|
187
|
+
console.error("需要 --origin-agent <agent ID>,或在 Paseo 的聊天中运行(自动读取 PASEO_AGENT_ID)");
|
|
188
|
+
return 2;
|
|
189
|
+
}
|
|
190
|
+
return notify(config, origin);
|
|
191
|
+
}
|
|
192
|
+
console.error("用法:npm run smoke -- check | notify --origin-agent <id> | detach-start | detach-check");
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main().then(
|
|
197
|
+
(code) => process.exit(code),
|
|
198
|
+
(error: Error) => {
|
|
199
|
+
console.error(error.message);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
},
|
|
202
|
+
);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Independent review of a draft spec, started from the chat before the requester
|
|
2
|
+
// confirms it (launch.ts --spec-review). Runs one "spec_review" agent under the chat,
|
|
3
|
+
// waits for it in the background, then posts the findings back into the chat.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { parseLaunchArgs } from "./cli-args.js";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { defaultConfigPath, KIT_ROOT, localPromptsDir, PRINCIPLES_INDEX, PROMPTS_DIR } from "./locations.js";
|
|
10
|
+
import { specReviewFailedMessage, specReviewMessage } from "./messages.js";
|
|
11
|
+
import { resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
12
|
+
import { runSingleStep } from "./pipeline.js";
|
|
13
|
+
import { normalizeFeatureDir, specProfile } from "./preflight.js";
|
|
14
|
+
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
15
|
+
import { ensurePipelineDir, hashFile, type PipelineState, pipelineDir, STATE_VERSION } from "./state.js";
|
|
16
|
+
|
|
17
|
+
function log(message: string): void {
|
|
18
|
+
console.log(`[${new Date().toISOString()}] ${message}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function main(): Promise<number> {
|
|
22
|
+
const args = parseLaunchArgs(process.argv.slice(2));
|
|
23
|
+
const repo = fs.realpathSync(args.repo);
|
|
24
|
+
const featureDir = normalizeFeatureDir(repo, args.featureDir);
|
|
25
|
+
const specPath = path.join(repo, featureDir, "spec.md");
|
|
26
|
+
let config = loadConfig(args.configPath ?? defaultConfigPath(), repo).config;
|
|
27
|
+
ensurePipelineDir(repo, featureDir);
|
|
28
|
+
const runtime = await (await loadRuntimeFactory())({ config, log });
|
|
29
|
+
const origin = args.originAgentId;
|
|
30
|
+
const notify = async (text: string) => {
|
|
31
|
+
log(`通知:\n${text}`);
|
|
32
|
+
if (origin) await runtime.notifyAgent(origin, text).catch((error: Error) => log(`通知发送失败:${error.message}`));
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
if (origin) await runtime.verifyOrigin(origin, repo);
|
|
36
|
+
if (usesPaseoProfiles(config)) config = resolveConfigProfiles(config, await runtime.listPaseoProfiles());
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
// A scratch state: the review is not part of a pipeline run and must not touch state.json.
|
|
39
|
+
const state: PipelineState = {
|
|
40
|
+
version: STATE_VERSION,
|
|
41
|
+
repo,
|
|
42
|
+
featureDir,
|
|
43
|
+
status: "running",
|
|
44
|
+
attempt: 1,
|
|
45
|
+
specHash: hashFile(specPath),
|
|
46
|
+
originAgentId: origin,
|
|
47
|
+
pid: process.pid,
|
|
48
|
+
startedAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
results: {},
|
|
51
|
+
history: [],
|
|
52
|
+
stopReason: null,
|
|
53
|
+
outcome: null,
|
|
54
|
+
profile: specProfile(fs.readFileSync(specPath, "utf8")) ?? config.defaultProfile,
|
|
55
|
+
branch: null,
|
|
56
|
+
milestones: [],
|
|
57
|
+
incremental: null,
|
|
58
|
+
};
|
|
59
|
+
const result = await runSingleStep(
|
|
60
|
+
{
|
|
61
|
+
repo,
|
|
62
|
+
featureDir,
|
|
63
|
+
config,
|
|
64
|
+
runtime,
|
|
65
|
+
state,
|
|
66
|
+
statePath: path.join(pipelineDir(repo, featureDir), "spec-review-state.json"),
|
|
67
|
+
prompts: { promptsDir: PROMPTS_DIR, localPromptsDir: localPromptsDir(), principlesFile: PRINCIPLES_INDEX },
|
|
68
|
+
logPath: path.join(pipelineDir(repo, featureDir), "spec-review.log"),
|
|
69
|
+
resumeCommand: "",
|
|
70
|
+
log,
|
|
71
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
72
|
+
},
|
|
73
|
+
"spec_review",
|
|
74
|
+
{ contextLines: [`需求说明的写法清单:${path.join(KIT_ROOT, "skills", "spec-writing", "SKILL.md")}`] },
|
|
75
|
+
);
|
|
76
|
+
await notify(
|
|
77
|
+
specReviewMessage({
|
|
78
|
+
featureDir,
|
|
79
|
+
verdict: String(result.verdict),
|
|
80
|
+
issues: (result.issues as string[] | undefined) ?? [],
|
|
81
|
+
reviewPath: `${featureDir}/spec-review.md`,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
return 0;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
log(`审阅没有完成:${(error as Error).message}`);
|
|
87
|
+
await notify(specReviewFailedMessage({ featureDir, reason: (error as Error).message }));
|
|
88
|
+
return 1;
|
|
89
|
+
} finally {
|
|
90
|
+
await runtime.close().catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().then(
|
|
95
|
+
(code) => process.exit(code),
|
|
96
|
+
(error: Error) => {
|
|
97
|
+
console.error(`[${new Date().toISOString()}] 需求说明审阅无法启动:${error.message}`);
|
|
98
|
+
process.exit(2);
|
|
99
|
+
},
|
|
100
|
+
);
|