paseo-req-kit 1.13.0-beta.5 → 1.14.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/contracts/cli-json.md +4 -4
- package/contracts/state.md +1 -1
- package/contracts/usage-record.md +5 -1
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +1 -1
- 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 +17 -2
- package/engine/prompts/00-shared-context.md +2 -0
- package/engine/src/activity.ts +23 -0
- package/engine/src/api.ts +106 -5
- package/engine/src/cli-args.ts +3 -0
- package/engine/src/config-view.ts +5 -0
- package/engine/src/config.ts +12 -0
- package/engine/src/control.ts +8 -1
- package/engine/src/doctor.ts +18 -0
- package/engine/src/features.ts +21 -1
- package/engine/src/launch.ts +25 -4
- package/engine/src/paseo-runtime.ts +14 -1
- package/engine/src/pipeline.ts +45 -1
- package/engine/src/preflight.ts +2 -0
- package/engine/src/records.ts +24 -2
- package/engine/src/review.ts +3 -1
- package/engine/src/runtime.ts +4 -0
- package/engine/src/scan-guard.ts +132 -0
- package/engine/src/state.ts +3 -1
- package/engine/src/stats.ts +1 -0
- package/engine/src/worktree.ts +180 -25
- package/package.json +1 -1
- package/paseo-plugin/client/format.ts +6 -2
- package/paseo-plugin/client/inbox.tsx +6 -1
- package/paseo-plugin/client/review.tsx +11 -1
- package/paseo-plugin/client/web.ts +17 -0
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/shared/contracts.ts +4 -2
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.BhERz0tz.js → app.ByJ4x-zl.js} +1 -1
- package/site-dist/assets/changelog.md.BUTM13q-.js +1 -0
- package/site-dist/assets/{changelog.md.Cu4SyOh_.lean.js → changelog.md.BUTM13q-.lean.js} +1 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.BJ01YY7h.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.BZONO_Xu.js → VPLocalSearchBox.LeFVc-_M.js} +1 -1
- package/site-dist/assets/chunks/{theme.Cdyvhz0n.js → theme.BMoBTK1B.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.CspCz67n.js → guide_getting-started.md.DDZak2vT.js} +1 -1
- package/site-dist/assets/{guide_getting-started.md.CspCz67n.lean.js → guide_getting-started.md.DDZak2vT.lean.js} +1 -1
- package/site-dist/assets/guide_maintainer.md.C1v7EpSY.js +1 -0
- package/site-dist/assets/guide_project.md.Csi5wiyt.js +77 -0
- package/site-dist/assets/guide_project.md.Csi5wiyt.lean.js +77 -0
- package/site-dist/assets/{guide_user.md.Cenm9vlN.js → guide_user.md.2v7b_fzd.js} +1 -1
- package/site-dist/assets/{guide_user.md.Cenm9vlN.lean.js → guide_user.md.2v7b_fzd.lean.js} +1 -1
- package/site-dist/assets/reference_contracts_cli-json.md.BR6gwq-e.js +1 -0
- package/site-dist/assets/reference_contracts_cli-json.md.BR6gwq-e.lean.js +1 -0
- package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +1 -0
- package/site-dist/assets/reference_contracts_usage-record.md.D5krNLW7.js +10 -0
- package/site-dist/assets/reference_contracts_usage-record.md.D5krNLW7.lean.js +10 -0
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +5 -5
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +7 -7
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +5 -5
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +4 -4
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +4 -4
- package/site-dist/reference/contracts/state.html +5 -5
- package/site-dist/reference/contracts/usage-record.html +6 -6
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/skills/req/SKILL.md +1 -0
- package/skills/req/playbooks/incident.md +3 -3
- package/skills/req/playbooks/new.md +1 -1
- package/skills/req/playbooks/small.md +1 -1
- package/skills/spec-writing/SKILL.md +3 -1
- package/site-dist/assets/changelog.md.Cu4SyOh_.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.C1vUA1BF.js +0 -1
- package/site-dist/assets/guide_maintainer.md.RZIzyCTV.js +0 -1
- package/site-dist/assets/guide_project.md.DnD2-Dsd.js +0 -77
- package/site-dist/assets/guide_project.md.DnD2-Dsd.lean.js +0 -77
- package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.js +0 -1
- package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.lean.js +0 -1
- package/site-dist/assets/reference_contracts_state.md.TRfzD5bc.js +0 -1
- package/site-dist/assets/reference_contracts_usage-record.md.DBMNY4OP.js +0 -10
- package/site-dist/assets/reference_contracts_usage-record.md.DBMNY4OP.lean.js +0 -10
- /package/site-dist/assets/{guide_maintainer.md.RZIzyCTV.lean.js → guide_maintainer.md.C1v7EpSY.lean.js} +0 -0
- /package/site-dist/assets/{reference_contracts_state.md.TRfzD5bc.lean.js → reference_contracts_state.md.EEBd_l2P.lean.js} +0 -0
package/engine/src/launch.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { loadConfig, PHASE_MODE_LABELS, type PipelineConfig, PROFILE_LABELS } fr
|
|
|
16
16
|
import { cardFieldsText } from "./integrations.js";
|
|
17
17
|
import { defaultConfigPath, ENGINE_DIR } from "./locations.js";
|
|
18
18
|
import { describeLiveActivity, describeStatus, stopPipeline } from "./control.js";
|
|
19
|
+
import { trackRequirement } from "./records.js";
|
|
19
20
|
import { describeFeatures, featuresWithRuns, findWorktreeRun, listFeatures, resolveFeatureArg } from "./features.js";
|
|
20
21
|
import { checkChatCwd, normalizeFeatureDir, preflight, type PreflightResult, specProfile, verifyPhaseProblem } from "./preflight.js";
|
|
21
22
|
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
@@ -92,6 +93,22 @@ if (args.cardFields) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
if (args.track) {
|
|
97
|
+
// 1.14: the requirement shows in the plugin from the start of the interview, with its chat.
|
|
98
|
+
try {
|
|
99
|
+
const repo = fs.realpathSync(args.repo);
|
|
100
|
+
const featureDir = normalizeFeatureDir(repo, args.featureDir);
|
|
101
|
+
const dir = path.join(repo, featureDir);
|
|
102
|
+
if (!fs.existsSync(dir)) throw new Error(`功能目录不存在:${featureDir}`);
|
|
103
|
+
trackRequirement(repo, featureDir, args.originAgentId);
|
|
104
|
+
console.log(`已登记需求:${featureDir}(Paseo 的 req-kit 插件里能看到它,状态"对齐中"${args.originAgentId ? ",关联这个对话" : ""})`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`没能登记需求:${(error as Error).message}`);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
if (args.list) {
|
|
96
113
|
console.log(describeFeatures(listFeatures(args.repo), args.repo));
|
|
97
114
|
process.exit(0);
|
|
@@ -344,7 +361,9 @@ if (args.worktree) {
|
|
|
344
361
|
try {
|
|
345
362
|
ownWorktree = await Promise.race([
|
|
346
363
|
create(),
|
|
347
|
-
|
|
364
|
+
// 1.14: big repositories (Windows especially) can take minutes to check out. Giving up early
|
|
365
|
+
// left a branch with only the spec on it; now it waits, and a later start continues there.
|
|
366
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("连接 Paseo 新建工作区超过 15 分钟没有完成。工作区可能稍后才建好:再执行一次启动命令,会在已建好的工作区里接着开始")), 15 * 60_000).unref()),
|
|
348
367
|
]);
|
|
349
368
|
} catch (error) {
|
|
350
369
|
fail(`没能为这个需求新建工作区:${(error as Error).message}`);
|
|
@@ -356,7 +375,7 @@ if (args.worktree) {
|
|
|
356
375
|
try {
|
|
357
376
|
checked = preflight(args, hashFile);
|
|
358
377
|
} catch (error) {
|
|
359
|
-
fail(`新工作区 ${ownWorktree.repo} 已建好,但无法在那里启动:${(error as Error).message}
|
|
378
|
+
fail(`新工作区 ${ownWorktree.repo} 已建好,但无法在那里启动:${(error as Error).message}\n问题解决后再执行一次启动命令,会在这个工作区里接着开始(不会再新建)。`);
|
|
360
379
|
}
|
|
361
380
|
}
|
|
362
381
|
|
|
@@ -403,8 +422,10 @@ console.log(
|
|
|
403
422
|
? "只做研发:写好提测说明和研发报告后结束,测试由人完成。"
|
|
404
423
|
: null,
|
|
405
424
|
ownWorktree
|
|
406
|
-
?
|
|
407
|
-
`需求文件已移到 ${path.join(ownWorktree.repo, checked.featureDir)}${ownWorktree.removedSource ? "" : "(这里原有的已提交版本保留不动)"},之后读写需求文件请用这个路径。`
|
|
425
|
+
? `${ownWorktree.reused ? "在上次已建好的工作区中开始" : "已为这个需求新建工作区"}:${ownWorktree.repo}(分支 ${ownWorktree.branch},在 Paseo 侧边栏中可以看到)。开发在那里进行,步骤仍挂在这个对话下;这个对话的工作目录不变。` +
|
|
426
|
+
`需求文件已移到 ${path.join(ownWorktree.repo, checked.featureDir)}${ownWorktree.removedSource ? "" : "(这里原有的已提交版本保留不动)"},之后读写需求文件请用这个路径。` +
|
|
427
|
+
(ownWorktree.carried.length ? `\n访谈中改过的 ${ownWorktree.carried.join("、")} 也一起带过去提交了;这里的未提交改动保留不动,确认无误后可以撤销。` : "") +
|
|
428
|
+
ownWorktree.warnings.map((w) => `\n注意:${w}`).join("")
|
|
408
429
|
: args.sourceRepo
|
|
409
430
|
? `这个需求在为它新建的工作区中开发:${checked.repo}`
|
|
410
431
|
: null,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PaseoAgentHandle, PaseoClient } from "@getpaseo/client";
|
|
2
|
-
import { summarizeActivity } from "./activity.js";
|
|
2
|
+
import { runningToolOf, summarizeActivity } from "./activity.js";
|
|
3
3
|
import { type AgentProfile, isResolved, type PipelineConfig } from "./config.js";
|
|
4
4
|
import { profilesFromDaemonConfig } from "./paseo-profiles.js";
|
|
5
5
|
import { cronAt, localTimeZone } from "./schedule-time.js";
|
|
@@ -288,6 +288,14 @@ export function createRuntimeWithClient(
|
|
|
288
288
|
return null;
|
|
289
289
|
}
|
|
290
290
|
},
|
|
291
|
+
async runningTool(agentId) {
|
|
292
|
+
try {
|
|
293
|
+
const page = await client.agents.ref(agentId).timeline.refetch({ direction: "tail", limit: 40 });
|
|
294
|
+
return runningToolOf(page.entries);
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
291
299
|
async createWorktree(request) {
|
|
292
300
|
if (!client.workspaces) throw new Error("这个 Paseo 版本不支持新建工作区");
|
|
293
301
|
const workspace = await client.workspaces.create({
|
|
@@ -299,6 +307,11 @@ export function createRuntimeWithClient(
|
|
|
299
307
|
if (!directory) throw new Error("Paseo 没有返回新工作区的目录");
|
|
300
308
|
return { workspaceId: workspace.id, directory };
|
|
301
309
|
},
|
|
310
|
+
async openWorkspace(directory) {
|
|
311
|
+
if (!client.workspaces) throw new Error("这个 Paseo 版本不支持接手已有的工作区目录");
|
|
312
|
+
const handle = await client.workspaces.open({ cwd: directory });
|
|
313
|
+
return { workspaceId: handle.id };
|
|
314
|
+
},
|
|
302
315
|
async waitForWorkspaceSetup(workspaceId, timeoutMs) {
|
|
303
316
|
const api = await getScripts();
|
|
304
317
|
if (!api) return { status: "none", error: null };
|
package/engine/src/pipeline.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { judge as judgeScan } from "./scan-guard.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -200,6 +201,13 @@ async function checkBudget(ctx: PipelineContext, stepTitle: string): Promise<voi
|
|
|
200
201
|
const budget = (ctx.state.budget ??= { allowance: limit, warnedAt: null });
|
|
201
202
|
const used = tokensUsed(ctx.state);
|
|
202
203
|
if (used >= budget.allowance) throw new BudgetReached(used, budget.allowance);
|
|
204
|
+
// 1.13: agents that report no usage make the budget do nothing; say so once instead of keeping quiet.
|
|
205
|
+
const finished = ctx.state.history.filter((h) => h.endedAt);
|
|
206
|
+
if (used === 0 && finished.length > 0 && !budget.noUsageNoticed && finished.every((h) => !h.usage || (h.usage.inputTokens === undefined && h.usage.outputTokens === undefined))) {
|
|
207
|
+
budget.noUsageNoticed = true;
|
|
208
|
+
save(ctx);
|
|
209
|
+
await notifyOrigin(ctx, noUsageMessage(shownFeatureDir(ctx), limit));
|
|
210
|
+
}
|
|
203
211
|
if (used >= (budget.allowance * ctx.config.budget.warnPercent) / 100 && budget.warnedAt !== budget.allowance) {
|
|
204
212
|
budget.warnedAt = budget.allowance;
|
|
205
213
|
save(ctx);
|
|
@@ -209,6 +217,10 @@ async function checkBudget(ctx: PipelineContext, stepTitle: string): Promise<voi
|
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
export function noUsageMessage(featureDir: string, limit: number): string {
|
|
221
|
+
return `【需求开发流水线通知】${featureDir} 设了每个需求 ${tokenText(limit)} tokens 的预算,但已完成的步骤里 agent 都没有报告 token 用量,所以预算不会起作用,token 统计和预计也没有数据。流水线照常继续。要让预算生效,请换用会报告用量的 agent(Paseo 里能看到用量的那种);不需要预算时可以在配置里把 budget.tokens 设为 null。这条消息只是告知,不需要处理。`;
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
class BusinessDecisionNeeded extends Error {
|
|
213
225
|
constructor(
|
|
214
226
|
public readonly questions: string[],
|
|
@@ -621,8 +633,37 @@ interface Deadline {
|
|
|
621
633
|
record?: StepRecord;
|
|
622
634
|
}
|
|
623
635
|
|
|
636
|
+
/** How often the scan guard looks at the running tool call. */
|
|
637
|
+
const SCAN_CHECK_SECONDS = 60;
|
|
638
|
+
/** At most this many interruptions per turn; after that the step's time limit takes over. */
|
|
639
|
+
const SCAN_MAX_INTERRUPTS = 5;
|
|
640
|
+
|
|
641
|
+
interface ScanGuardMemory {
|
|
642
|
+
interrupted: Set<string>;
|
|
643
|
+
count: number;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** 1.13: interrupts a search of a very large directory (or a search that runs too long), once per tool call. */
|
|
647
|
+
async function guardScan(ctx: PipelineContext, agent: StepAgent, def: StepDef, key: string, memory: ScanGuardMemory): Promise<void> {
|
|
648
|
+
if (!ctx.config.scanGuard.enabled || memory.count >= SCAN_MAX_INTERRUPTS) return;
|
|
649
|
+
const tool = await ctx.runtime.runningTool(agent.id).catch(() => null);
|
|
650
|
+
if (!tool || memory.interrupted.has(tool.callId)) return;
|
|
651
|
+
const verdict = judgeScan(tool, ctx.config.scanGuard);
|
|
652
|
+
if (!verdict) return;
|
|
653
|
+
memory.interrupted.add(tool.callId);
|
|
654
|
+
memory.count += 1;
|
|
655
|
+
try {
|
|
656
|
+
await ctx.runtime.interruptAgent(agent.id, verdict.message);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
ctx.log(`打断「${def.title}」的搜索失败:${(error as Error).message}`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
recover(ctx, key, "stop_scan", `「${def.title}」的命令${verdict.reason},已打断并告诉 agent 改用别的办法:${tool.text.replace(/\s+/g, " ").slice(0, 120)}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
624
664
|
async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, deadline: Deadline): Promise<AgentUsage | null> {
|
|
625
665
|
const notifiedPermissions = new Set<string>();
|
|
666
|
+
const guard: ScanGuardMemory = { interrupted: new Set(), count: 0 };
|
|
626
667
|
let consecutiveErrors = 0;
|
|
627
668
|
for (;;) {
|
|
628
669
|
let remaining = deadline.at - Date.now();
|
|
@@ -644,7 +685,9 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
644
685
|
}
|
|
645
686
|
let outcome;
|
|
646
687
|
try {
|
|
647
|
-
|
|
688
|
+
// 1.13: with the scan guard on, look at the running tool call about once a minute.
|
|
689
|
+
const chunk = ctx.config.scanGuard.enabled ? Math.min(ctx.config.waitChunkSeconds, SCAN_CHECK_SECONDS) : ctx.config.waitChunkSeconds;
|
|
690
|
+
outcome = await agent.wait(Math.min(chunk * 1000, remaining));
|
|
648
691
|
consecutiveErrors = 0;
|
|
649
692
|
} catch (error) {
|
|
650
693
|
consecutiveErrors += 1;
|
|
@@ -665,6 +708,7 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
665
708
|
case "idle":
|
|
666
709
|
return outcome.usage ?? null;
|
|
667
710
|
case "timeout":
|
|
711
|
+
await guardScan(ctx, agent, def, deadline.key, guard);
|
|
668
712
|
continue;
|
|
669
713
|
case "error":
|
|
670
714
|
throw new AgentErrored(def.title, `agent 报告错误:${outcome.error ?? "未知错误"}`);
|
package/engine/src/preflight.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface LaunchArgs {
|
|
|
31
31
|
list: boolean;
|
|
32
32
|
/** --card-fields: what /req confirms with the spec for the external systems (card.json). */
|
|
33
33
|
cardFields?: boolean;
|
|
34
|
+
/** 1.14 --track: register a requirement being aligned, before the pipeline runs (shown in the plugin). */
|
|
35
|
+
track?: boolean;
|
|
34
36
|
/** --spec-review: independent review of the draft spec, before confirmation. */
|
|
35
37
|
specReview: boolean;
|
|
36
38
|
/** --worktree: create a Paseo worktree workspace for this requirement and develop there. */
|
package/engine/src/records.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface IndexEntry {
|
|
|
47
47
|
/** Name of the project: the source checkout's directory name. */
|
|
48
48
|
project: string;
|
|
49
49
|
registeredAt: string;
|
|
50
|
+
/** 1.14: the chat the requirement is being aligned in (registered before the pipeline ran). */
|
|
51
|
+
chatAgentId?: string | null;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export function indexDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -59,7 +61,7 @@ export function requirementKey(sourceOrRepo: string, featureDir: string): string
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/** Records where this requirement lives, if it is not recorded that way already. Never throws. */
|
|
62
|
-
export function registerRequirement(state: Pick<PipelineState, "repo" | "featureDir" | "worktree">, env: NodeJS.ProcessEnv = process.env): void {
|
|
64
|
+
export function registerRequirement(state: Pick<PipelineState, "repo" | "featureDir" | "worktree">, env: NodeJS.ProcessEnv = process.env, chatAgentId: string | null = null): void {
|
|
63
65
|
if (recordingOff(env)) return;
|
|
64
66
|
try {
|
|
65
67
|
const source = state.worktree?.sourceRepo ?? null;
|
|
@@ -75,6 +77,7 @@ export function registerRequirement(state: Pick<PipelineState, "repo" | "feature
|
|
|
75
77
|
workspaceId: state.worktree?.workspaceId ?? null,
|
|
76
78
|
project: path.basename(real(source ?? state.repo)),
|
|
77
79
|
registeredAt: existing?.registeredAt ?? new Date().toISOString(),
|
|
80
|
+
...(chatAgentId ?? existing?.chatAgentId ? { chatAgentId: chatAgentId ?? existing?.chatAgentId ?? null } : {}),
|
|
78
81
|
};
|
|
79
82
|
if (existing && JSON.stringify(existing) === JSON.stringify(entry)) return;
|
|
80
83
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -86,6 +89,24 @@ export function registerRequirement(state: Pick<PipelineState, "repo" | "feature
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 1.14: a requirement whose interview has started: marked in its feature directory
|
|
94
|
+
* (.pipeline/tracked.json, not committed) and registered with its chat, so the plugin lists it
|
|
95
|
+
* before the pipeline ever runs.
|
|
96
|
+
*/
|
|
97
|
+
export function trackRequirement(repo: string, featureDir: string, chatAgentId: string | null, env: NodeJS.ProcessEnv = process.env): void {
|
|
98
|
+
const marker = path.join(repo, featureDir, ".pipeline", "tracked.json");
|
|
99
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
100
|
+
let since = new Date().toISOString();
|
|
101
|
+
try {
|
|
102
|
+
since = (JSON.parse(fs.readFileSync(marker, "utf8")) as { since?: string }).since ?? since;
|
|
103
|
+
} catch {
|
|
104
|
+
// First time.
|
|
105
|
+
}
|
|
106
|
+
fs.writeFileSync(marker, `${JSON.stringify({ since, chatAgentId }, null, 2)}\n`);
|
|
107
|
+
registerRequirement({ repo, featureDir, worktree: null }, env, chatAgentId);
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
function readEntry(file: string): IndexEntry | null {
|
|
90
111
|
try {
|
|
91
112
|
const raw = JSON.parse(fs.readFileSync(file, "utf8")) as IndexEntry;
|
|
@@ -104,7 +125,8 @@ export function readIndex(env: NodeJS.ProcessEnv = process.env): Array<IndexEntr
|
|
|
104
125
|
.filter((n) => n.endsWith(".json"))
|
|
105
126
|
.map((n) => readEntry(path.join(dir, n)))
|
|
106
127
|
.filter((e): e is IndexEntry => e !== null)
|
|
107
|
-
.
|
|
128
|
+
// 1.14: a requirement still in its interview has no state yet, only its notes.
|
|
129
|
+
.map((e) => ({ ...e, present: ["spec.md", "alignment.md", ".pipeline/state.json", ".pipeline/tracked.json"].some((f) => fs.existsSync(path.join(e.repo, e.featureDir, f))) }));
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
/** Removes the entries whose requirement is gone. Returns how many. */
|
package/engine/src/review.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
13
|
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
14
15
|
import http from "node:http";
|
|
15
16
|
import path from "node:path";
|
|
16
17
|
import { ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
|
|
@@ -510,7 +511,8 @@ async function serve(env: NodeJS.ProcessEnv): Promise<void> {
|
|
|
510
511
|
token,
|
|
511
512
|
readToken,
|
|
512
513
|
version: kitVersion(),
|
|
513
|
-
|
|
514
|
+
// Listening for other computers without a public address: this computer's name is the best guess.
|
|
515
|
+
publicUrl: env.REQ_KIT_REVIEW_PUBLIC_URL?.trim() || `http://${/^(127\.|localhost$|::1$)/.test(host) ? "127.0.0.1" : os.hostname()}:${port}`,
|
|
514
516
|
startedAt: new Date().toISOString(),
|
|
515
517
|
};
|
|
516
518
|
fs.mkdirSync(kitHome(env), { recursive: true });
|
package/engine/src/runtime.ts
CHANGED
|
@@ -135,12 +135,16 @@ export interface AgentRuntime {
|
|
|
135
135
|
stopServices(target: ServiceTarget, names: string[]): Promise<void>;
|
|
136
136
|
/** Creates a Paseo worktree workspace (paseo.json setup runs in the background). */
|
|
137
137
|
createWorktree(request: WorktreeRequest): Promise<{ workspaceId: string; directory: string }>;
|
|
138
|
+
/** 1.14: the Paseo workspace of an existing directory (opened as one if it is not yet). */
|
|
139
|
+
openWorkspace?(directory: string): Promise<{ workspaceId: string }>;
|
|
138
140
|
/** Waits for the workspace's paseo.json setup; "none" when there is nothing to wait for or it cannot be read. */
|
|
139
141
|
waitForWorkspaceSetup(workspaceId: string, timeoutMs: number): Promise<{ status: "completed" | "failed" | "none" | "timeout"; error: string | null }>;
|
|
140
142
|
/** Subscription windows of the given providers; null when Paseo has no usage data. */
|
|
141
143
|
providerQuota(providers: string[]): Promise<ProviderQuota[] | null>;
|
|
142
144
|
/** The agent's latest actions, oldest first; null when they cannot be read. */
|
|
143
145
|
recentActivity(agentId: string, limit: number): Promise<ActivityItem[] | null>;
|
|
146
|
+
/** 1.13: the tool call the agent is running now (the latest one still running), or null. */
|
|
147
|
+
runningTool(agentId: string): Promise<import("./scan-guard.js").RunningTool | null>;
|
|
144
148
|
/** Which Paseo tools step agents get (Settings → Agents); null when unknown. */
|
|
145
149
|
capabilities(): Promise<{ paseoTools: boolean; browser: boolean } | null>;
|
|
146
150
|
/** 1.12: whether each agent provider is installed and logged in ("ready"); null when the daemon does not say. */
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Stopping commands that search very large directories (1.13): a step agent that looks for a
|
|
2
|
+
// class or a package by searching the whole home directory, the Maven repository (~/.m2), the
|
|
3
|
+
// global node_modules or a drive can sit for many minutes and never finish the step. While a step
|
|
4
|
+
// runs, the pipeline looks at the agent's running tool call about once a minute; a search of such
|
|
5
|
+
// a directory (after a minute) or any search that runs too long is interrupted, and the agent is
|
|
6
|
+
// told how to find what it needs without the scan. A plain check of the command text, no model.
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
|
|
9
|
+
export interface RunningTool {
|
|
10
|
+
/** Paseo's tool kind: "shell", "search", … */
|
|
11
|
+
kind: string;
|
|
12
|
+
/** The command (shell) or the pattern (search). */
|
|
13
|
+
text: string;
|
|
14
|
+
/** When the call started (ISO time). */
|
|
15
|
+
startedAt: string;
|
|
16
|
+
/** The tool call's ID, so the same call is interrupted at most once. */
|
|
17
|
+
callId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ScanGuardSettings {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** A search that runs this long is interrupted wherever it searches. */
|
|
23
|
+
minutes: number;
|
|
24
|
+
/** More directories never to be searched as a whole (e.g. a shared drive). */
|
|
25
|
+
paths: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A search of a large directory gets this long before it is interrupted (most are quick to fail or finish). */
|
|
29
|
+
export const BIG_DIR_GRACE_MS = 60_000;
|
|
30
|
+
|
|
31
|
+
/** Commands that walk a directory tree. */
|
|
32
|
+
const SCAN_COMMAND =
|
|
33
|
+
/(^|[\s;&|("'`])(find|fd|locate|mlocate|tree|du|rg|ag|ack|grep\s+(-\w*[rR]\w*|--recursive)|egrep\s+-\w*[rR]|ls\s+(-\w*R\w*)|Get-ChildItem\b[^|;]*-r(ecurse)?\b|gci\b[^|;]*-r(ecurse)?\b|dir\b[^|;]*(\/s\b|-r(ecurse)?\b)|findstr\b[^|;]*\/s\b|where\s+\/r|Select-String\b[^|;]*-Path\b[^|;]*\*\*)(?=$|[\s;&|)"'`])/i;
|
|
34
|
+
|
|
35
|
+
/** Option values that keep a directory out of a search: not what it searches. */
|
|
36
|
+
const EXCLUSIONS = [
|
|
37
|
+
/--exclude(-dir)?(=|\s+)("[^"]*"|'[^']*'|\S+)/gi,
|
|
38
|
+
/(-g|--glob|--iglob)(=|\s+)("![^"]*"|'![^']*'|!\S+)/gi,
|
|
39
|
+
/(-not|!)\s+-(i?path|i?name|wholename)\s+("[^"]*"|'[^']*'|\S+)/gi,
|
|
40
|
+
/-(i?path|i?name|wholename)\s+("[^"]*"|'[^']*'|\S+)\s+-prune/gi,
|
|
41
|
+
/--(ignore-dir|ignore-directory|exclude-from)(=|\s+)("[^"]*"|'[^']*'|\S+)/gi,
|
|
42
|
+
/-Exclude\s+("[^"]*"|'[^']*'|\S+)/gi,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const END = String.raw`(?=$|[\s"'*;|&)\]` + "`" + `])`;
|
|
46
|
+
const START = String.raw`(?:^|[\s"'=(\[` + "`" + `])`;
|
|
47
|
+
|
|
48
|
+
const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
49
|
+
const norm = (s: string) => s.replace(/\\/g, "/");
|
|
50
|
+
|
|
51
|
+
interface BigDir {
|
|
52
|
+
re: RegExp;
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bigDirs(home: string, extra: string[]): BigDir[] {
|
|
57
|
+
const h = escape(norm(home).replace(/\/+$/, ""));
|
|
58
|
+
const homes = String.raw`(?:~|\$HOME|\$\{HOME\}|%USERPROFILE%|\$env:USERPROFILE|\$env:HOME${h ? `|${h}` : ""})`;
|
|
59
|
+
const sub = (p: string) => new RegExp(`${START}(?:${homes}/|[^\\s"']*/)?${p}/?${END}`, "i");
|
|
60
|
+
const list: BigDir[] = [
|
|
61
|
+
{ re: new RegExp(`${START}/${END}`), label: "整个磁盘(/)" },
|
|
62
|
+
{ re: new RegExp(`${START}[a-z]:/?${END}`, "i"), label: "整个磁盘" },
|
|
63
|
+
{ re: new RegExp(`${START}${homes}/?${END}`, "i"), label: "用户目录" },
|
|
64
|
+
{ re: sub(String.raw`\.m2(?:/repository)?`), label: "Maven 本地仓库(.m2)" },
|
|
65
|
+
{ re: sub(String.raw`\.gradle(?:/caches)?`), label: "Gradle 缓存(.gradle)" },
|
|
66
|
+
{ re: sub(String.raw`\.ivy2`), label: "Ivy 缓存(.ivy2)" },
|
|
67
|
+
{ re: sub(String.raw`(?:\.npm|\.pnpm-store|\.yarn|AppData/Local/(?:npm-cache|pnpm|Yarn))`), label: "npm/pnpm/yarn 缓存" },
|
|
68
|
+
{ re: sub(String.raw`(?:\.nvm|\.volta|\.fnm|AppData/Roaming/nvm)`), label: "Node.js 版本目录" },
|
|
69
|
+
{ re: sub(String.raw`\.cache`), label: "缓存目录(.cache)" },
|
|
70
|
+
{ re: sub(String.raw`(?:\.cargo|go/pkg/mod|\.rustup)`), label: "Rust/Go 依赖缓存" },
|
|
71
|
+
{ re: sub(String.raw`AppData(?:/Roaming|/Local)?`), label: "AppData" },
|
|
72
|
+
{ re: sub(String.raw`(?:usr/(?:local/)?lib/node_modules|AppData/Roaming/npm(?:/node_modules)?)`), label: "全局 node_modules" },
|
|
73
|
+
{ re: /npm\s+root\s+-g|pnpm\s+root\s+-g/i, label: "全局 node_modules" },
|
|
74
|
+
{ re: sub("node_modules"), label: "node_modules" },
|
|
75
|
+
{ re: new RegExp(`${START}(?:[a-z]:)?/(?:usr(?:/lib|/share|/local)?|opt|var|Library|Program Files(?: \\(x86\\))?|Windows)/?${END}`, "i"), label: "系统目录" },
|
|
76
|
+
{ re: sub(String.raw`site-packages`), label: "Python 依赖目录(site-packages)" },
|
|
77
|
+
];
|
|
78
|
+
for (const p of extra) {
|
|
79
|
+
const n = escape(norm(p).replace(/\/+$/, ""));
|
|
80
|
+
if (n) list.push({ re: new RegExp(`${START}${n}/?${END}`, "i"), label: p });
|
|
81
|
+
}
|
|
82
|
+
return list;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The large directory a command searches as a whole, or null. */
|
|
86
|
+
export function bigDirOf(command: string, options: { home?: string; paths?: string[] } = {}): string | null {
|
|
87
|
+
if (!SCAN_COMMAND.test(command)) return null;
|
|
88
|
+
let text = command;
|
|
89
|
+
for (const re of EXCLUSIONS) text = text.replace(re, " ");
|
|
90
|
+
text = norm(text);
|
|
91
|
+
for (const d of bigDirs(options.home ?? os.homedir(), options.paths ?? [])) if (d.re.test(text)) return d.label;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isScan(tool: Pick<RunningTool, "kind" | "text">): boolean {
|
|
96
|
+
return tool.kind === "search" || (tool.kind === "shell" && SCAN_COMMAND.test(tool.text));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ScanVerdict {
|
|
100
|
+
/** Why: the large directory, or how long the search has run. */
|
|
101
|
+
reason: string;
|
|
102
|
+
/** What the agent is told (the interrupt message). */
|
|
103
|
+
message: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const clip = (s: string, n = 160) => {
|
|
107
|
+
const one = s.replace(/\s+/g, " ").trim();
|
|
108
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Whether the running tool call should be interrupted, and what to tell the agent. */
|
|
112
|
+
export function judge(tool: RunningTool | null, settings: ScanGuardSettings, now = new Date(), home = os.homedir()): ScanVerdict | null {
|
|
113
|
+
if (!tool || !settings.enabled) return null;
|
|
114
|
+
const ran = now.getTime() - new Date(tool.startedAt).getTime();
|
|
115
|
+
if (!(ran >= 0)) return null;
|
|
116
|
+
const big = tool.kind === "shell" ? bigDirOf(tool.text, { home, paths: settings.paths }) : null;
|
|
117
|
+
let reason: string | null = null;
|
|
118
|
+
if (big && ran >= BIG_DIR_GRACE_MS) reason = `在搜索${big}`;
|
|
119
|
+
else if (isScan(tool) && ran >= settings.minutes * 60_000) reason = `搜索已经运行了 ${Math.round(ran / 60_000)} 分钟`;
|
|
120
|
+
if (!reason) return null;
|
|
121
|
+
return { reason, message: scanMessage(tool.text, reason) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function scanMessage(command: string, reason: string): string {
|
|
125
|
+
return [
|
|
126
|
+
`【流水线提示】已停止你刚才的${command ? `命令「${clip(command)}」` : "搜索"}:它${reason},这类搜索要很久,会让这一步一直做不完。请继续完成本步骤的任务,但不要再搜索整个磁盘、用户目录或依赖缓存,改用下面的办法:`,
|
|
127
|
+
"- 在仓库里找:用 `git grep -n <内容>` 或 `git ls-files | grep <文件名>`;用 rg 时只在仓库目录里搜,它会跳过 .gitignore 里的目录。",
|
|
128
|
+
"- 找某个 Maven/Gradle 依赖里的类:先在 pom.xml / build.gradle 里确定 groupId、artifactId 和版本,直接打开 `~/.m2/repository/<groupId 的点换成 />/<artifactId>/<版本>/` 这一个目录,用 `unzip -l <jar> | grep <类名>` 或 `jar tf` 看那一个 jar;要源码时用 `mvn dependency:sources -DincludeArtifactIds=<artifactId>`。不知道是哪个依赖时,`mvn dependency:tree` 比搜索快。",
|
|
129
|
+
"- 找某个 npm 包:直接看仓库里的 `node_modules/<包名>/package.json`,以及其中 main、types 指向的文件;不要搜索整个 node_modules 或全局 node_modules。",
|
|
130
|
+
"- 实在找不到就不要再找了:按项目代码和文档里能确认的信息继续,把不确定的地方写进本步骤产物的\"假设\"一节。",
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
package/engine/src/state.ts
CHANGED
|
@@ -50,7 +50,7 @@ export type StopKind =
|
|
|
50
50
|
export interface Recovery {
|
|
51
51
|
at: string;
|
|
52
52
|
key: string;
|
|
53
|
-
kind: "retry" | "reattach" | "reuse_result" | "extend_timeout" | "redo" | "fallback" | "auto_resume" | "quota_wait" | "self_answer";
|
|
53
|
+
kind: "retry" | "reattach" | "reuse_result" | "extend_timeout" | "redo" | "fallback" | "auto_resume" | "quota_wait" | "self_answer" | "stop_scan";
|
|
54
54
|
detail: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -89,6 +89,8 @@ export interface BudgetState {
|
|
|
89
89
|
allowance: number;
|
|
90
90
|
/** The allowance a warning was given for, so each level warns once. */
|
|
91
91
|
warnedAt: number | null;
|
|
92
|
+
/** 1.13: the person was told the agents report no token usage (so the budget cannot work). */
|
|
93
|
+
noUsageNoticed?: boolean;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export interface PipelineState {
|
package/engine/src/stats.ts
CHANGED