paseo-req-kit 1.14.0-beta.3 → 1.14.0-beta.5

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 (133) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/req.mjs +14 -0
  3. package/contracts/cli-json.md +1 -1
  4. package/contracts/notifier-event.md +1 -1
  5. package/contracts/spec.md +1 -0
  6. package/contracts/state.md +3 -1
  7. package/contracts/usage-record.md +16 -2
  8. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +11 -1
  9. 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 -1
  10. package/engine/prompts/00-shared-context.md +3 -2
  11. package/engine/prompts/06-code-review.md +12 -1
  12. package/engine/prompts/incremental.md +2 -0
  13. package/engine/src/activity.ts +21 -0
  14. package/engine/src/api.ts +44 -7
  15. package/engine/src/cli-args.ts +4 -1
  16. package/engine/src/code-review.ts +422 -0
  17. package/engine/src/codeowners.ts +87 -0
  18. package/engine/src/config-view.ts +10 -0
  19. package/engine/src/config.ts +27 -0
  20. package/engine/src/danger-guard.ts +180 -0
  21. package/engine/src/features.ts +1 -1
  22. package/engine/src/main.ts +13 -3
  23. package/engine/src/messages.ts +29 -0
  24. package/engine/src/outcomes.ts +145 -0
  25. package/engine/src/paseo-runtime.ts +9 -1
  26. package/engine/src/pipeline.ts +135 -6
  27. package/engine/src/preflight.ts +10 -1
  28. package/engine/src/records.ts +71 -0
  29. package/engine/src/reply.ts +30 -11
  30. package/engine/src/review-assets/code.css +93 -0
  31. package/engine/src/review-assets/code.html +43 -0
  32. package/engine/src/review-assets/code.js +402 -0
  33. package/engine/src/review.ts +98 -2
  34. package/engine/src/runtime.ts +2 -0
  35. package/engine/src/secret-scan.ts +177 -0
  36. package/engine/src/state.ts +30 -0
  37. package/engine/src/stats.ts +143 -9
  38. package/engine/src/watchdog.ts +38 -4
  39. package/package.json +1 -1
  40. package/paseo-plugin/client/data.ts +2 -2
  41. package/paseo-plugin/client/detail.tsx +7 -2
  42. package/paseo-plugin/client/nav.ts +5 -0
  43. package/paseo-plugin/client/review.tsx +16 -6
  44. package/paseo-plugin/client/stats.tsx +15 -0
  45. package/paseo-plugin/package.json +1 -1
  46. package/paseo-plugin/server/handlers.ts +1 -1
  47. package/paseo-plugin/shared/contracts.ts +11 -2
  48. package/site-dist/404.html +2 -2
  49. package/site-dist/assets/{app.BpC99f5a.js → app.TlOHPtfE.js} +1 -1
  50. package/site-dist/assets/changelog.md.DiGi8kHU.js +1 -0
  51. package/site-dist/assets/{changelog.md.m4cSU5e4.lean.js → changelog.md.DiGi8kHU.lean.js} +1 -1
  52. package/site-dist/assets/chunks/@localSearchIndexroot.Dk_-Yka5.js +1 -0
  53. package/site-dist/assets/chunks/{VPLocalSearchBox.CI43rhTA.js → VPLocalSearchBox.BUVNy7_o.js} +1 -1
  54. package/site-dist/assets/chunks/{theme.S6pNEF23.js → theme.BFeiG1US.js} +2 -2
  55. package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.js → guide_getting-started.md.UhkLCUoY.js} +1 -1
  56. package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.lean.js → guide_getting-started.md.UhkLCUoY.lean.js} +1 -1
  57. package/site-dist/assets/{guide_project.md.Csi5wiyt.js → guide_project.md.DIjDRoAq.js} +1 -1
  58. package/site-dist/assets/{guide_project.md.Csi5wiyt.lean.js → guide_project.md.DIjDRoAq.lean.js} +1 -1
  59. package/site-dist/assets/{guide_user.md.D9GpGJ7y.js → guide_user.md.UV0WxdJa.js} +1 -1
  60. package/site-dist/assets/{guide_user.md.D9GpGJ7y.lean.js → guide_user.md.UV0WxdJa.lean.js} +1 -1
  61. package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.js → reference_contracts_cli-json.md.CCvKLasM.js} +1 -1
  62. package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.lean.js → reference_contracts_cli-json.md.CCvKLasM.lean.js} +1 -1
  63. package/site-dist/assets/{reference_contracts_notifier-event.md.CCfrkm_A.js → reference_contracts_notifier-event.md.B9WqeZdO.js} +1 -1
  64. package/site-dist/assets/{reference_contracts_notifier-event.md.CCfrkm_A.lean.js → reference_contracts_notifier-event.md.B9WqeZdO.lean.js} +1 -1
  65. package/site-dist/assets/{reference_contracts_spec.md.BwwS0L_j.js → reference_contracts_spec.md.Dh5XczZL.js} +2 -2
  66. package/site-dist/assets/{reference_contracts_spec.md.BwwS0L_j.lean.js → reference_contracts_spec.md.Dh5XczZL.lean.js} +2 -2
  67. package/site-dist/assets/reference_contracts_state.md.STlqkPPS.js +1 -0
  68. package/site-dist/assets/{reference_contracts_usage-record.md.D5krNLW7.lean.js → reference_contracts_usage-record.md.MjVps4CA.js} +2 -2
  69. package/site-dist/assets/{reference_contracts_usage-record.md.D5krNLW7.js → reference_contracts_usage-record.md.MjVps4CA.lean.js} +2 -2
  70. package/site-dist/changelog.html +5 -5
  71. package/site-dist/guide/getting-started.html +5 -5
  72. package/site-dist/guide/how-it-works.html +4 -4
  73. package/site-dist/guide/index.html +4 -4
  74. package/site-dist/guide/install.html +4 -4
  75. package/site-dist/guide/maintainer.html +4 -4
  76. package/site-dist/guide/phases.html +4 -4
  77. package/site-dist/guide/project.html +6 -6
  78. package/site-dist/guide/user.html +5 -5
  79. package/site-dist/hashmap.json +1 -1
  80. package/site-dist/index.html +4 -4
  81. package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
  82. package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
  83. package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
  84. package/site-dist/reference/adr/0004-result-files.html +4 -4
  85. package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
  86. package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
  87. package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
  88. package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
  89. package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
  90. package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
  91. package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
  92. package/site-dist/reference/adr/0012-review-panel.html +4 -4
  93. package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
  94. package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
  95. package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
  96. package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
  97. package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
  98. package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
  99. package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
  100. package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
  101. package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
  102. package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
  103. package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
  104. package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
  105. package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
  106. package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
  107. package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
  108. package/site-dist/reference/adr/0028-external-integrations.html +4 -4
  109. package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
  110. package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
  111. package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
  112. package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
  113. package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
  114. package/site-dist/reference/adr/index.html +4 -4
  115. package/site-dist/reference/commands.html +4 -4
  116. package/site-dist/reference/config.html +4 -4
  117. package/site-dist/reference/contracts/cli-json.html +5 -5
  118. package/site-dist/reference/contracts/feature-files.html +4 -4
  119. package/site-dist/reference/contracts/index.html +4 -4
  120. package/site-dist/reference/contracts/notifier-event.html +6 -6
  121. package/site-dist/reference/contracts/result-file.html +4 -4
  122. package/site-dist/reference/contracts/spec.html +6 -6
  123. package/site-dist/reference/contracts/state.html +5 -5
  124. package/site-dist/reference/contracts/usage-record.html +6 -6
  125. package/site-dist/reference/contracts/verify.html +4 -4
  126. package/site-dist/reference/scope.html +4 -4
  127. package/skills/req/SKILL.md +2 -0
  128. package/skills/req/playbooks/incident.md +1 -1
  129. package/skills/spec-writing/SKILL.md +2 -1
  130. package/site-dist/assets/changelog.md.m4cSU5e4.js +0 -1
  131. package/site-dist/assets/chunks/@localSearchIndexroot.D6ywcTJ5.js +0 -1
  132. package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +0 -1
  133. /package/site-dist/assets/{reference_contracts_state.md.EEBd_l2P.lean.js → reference_contracts_state.md.STlqkPPS.lean.js} +0 -0
@@ -1,4 +1,6 @@
1
1
  import { judge as judgeScan } from "./scan-guard.js";
2
+ import { dangerMessage, dangerOf } from "./danger-guard.js";
3
+ import { describeFinding, scanChange, secretFollowUp } from "./secret-scan.js";
2
4
  import { execFileSync } from "node:child_process";
3
5
  import fs from "node:fs";
4
6
  import path from "node:path";
@@ -29,6 +31,7 @@ import {
29
31
  verifyStartPrompt,
30
32
  milestoneMessage,
31
33
  permissionMessage,
34
+ dangerNotice,
32
35
  resultFollowUp,
33
36
  } from "./messages.js";
34
37
  import { KIT_ROOT, kitVersion } from "./locations.js";
@@ -38,7 +41,7 @@ import { describeExternal, type IntegrationEvent, renderArgs, resolveCommand, ty
38
41
  import { cardSlug, planAppendix, publishCard, readSource } from "./card.js";
39
42
  import { principlesSection, projectAdditions, type PromptLocations, readToolPrompt } from "./prompts.js";
40
43
  import type { AgentRuntime, ManagedService, ServiceTarget, StepAgent, WaitOutcome } from "./runtime.js";
41
- import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type Recovery, type RunPhases, type StepRecord, type StopKind, writeState } from "./state.js";
44
+ import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type Recovery, type SecretScanRun, type RunPhases, type StepRecord, type StopKind, writeState } from "./state.js";
42
45
  import { type PanelStep, STEPS, type StepDef, type StepId, stepKey, validateResult, VERIFY_PHASE_STEPS } from "./steps.js";
43
46
 
44
47
  export interface PipelineContext {
@@ -518,6 +521,15 @@ function pushBranch(ctx: PipelineContext): void {
518
521
  const at = new Date().toISOString();
519
522
  for (const repo of repos) {
520
523
  let error: string | null = null;
524
+ // 1.14: nothing with a secret in it leaves the machine.
525
+ const target = (ctx.state.joint ?? []).map((j) => ({ joint: j, repo: j.repo })).find((t) => t.repo === repo);
526
+ const secrets = ctx.config.secretScan.enabled ? scanChange(repo, secretBase(ctx, target as StepTarget | undefined), ctx.config.secretScan) : [];
527
+ if (secrets && secrets.length > 0) {
528
+ error = `发现 ${secrets.length} 处疑似密钥,没有推送(例如 ${describeFinding(secrets[0])});去掉后继续开发会再推送`;
529
+ ctx.log(`推送 ${repo} 的分支:${error}`);
530
+ ctx.state.pushed = [...(ctx.state.pushed ?? []).filter((p) => p.repo !== repo), { repo, remote: push.remote, branch, at, error, mergeRequest: ctx.state.pushed?.find((p) => p.repo === repo)?.mergeRequest ?? null }];
531
+ continue;
532
+ }
521
533
  try {
522
534
  execFileSync("git", ["push", push.remote, `HEAD:refs/heads/${branch}`], { cwd: repo, stdio: ["ignore", "pipe", "pipe"], timeout: 180_000, windowsHide: true });
523
535
  } catch (e) {
@@ -661,9 +673,57 @@ async function guardScan(ctx: PipelineContext, agent: StepAgent, def: StepDef, k
661
673
  recover(ctx, key, "stop_scan", `「${def.title}」的命令${verdict.reason},已打断并告诉 agent 改用别的办法:${tool.text.replace(/\s+/g, " ").slice(0, 120)}`);
662
674
  }
663
675
 
676
+ /** How often the danger guard looks at the agent's commands (it waits in chunks this long). */
677
+ const DANGER_CHECK_SECONDS = 15;
678
+ /** At most this many danger interruptions per turn. */
679
+ const DANGER_MAX_INTERRUPTS = 5;
680
+
681
+ /**
682
+ * 1.14: looks at the commands the step agent ran or is running; a dangerous one is recorded, the
683
+ * person is told, and the agent is interrupted (while its turn runs) with what to do instead.
684
+ * Each tool call is handled once, also across turns and pipeline restarts.
685
+ */
686
+ async function guardDanger(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, key: string, turnRunning: boolean, memory: { count: number }): Promise<void> {
687
+ const settings = ctx.config.dangerGuard;
688
+ if (!settings.enabled || !ctx.runtime.toolCalls) return;
689
+ const calls = await ctx.runtime.toolCalls(agent.id).catch(() => null);
690
+ if (!calls) return;
691
+ const seen = new Set((ctx.state.dangers ?? []).filter((d) => d.agentId === agent.id).map((d) => d.callId));
692
+ for (const call of calls) {
693
+ if (seen.has(call.callId) || !["shell", "fetch", "unknown"].includes(call.kind)) continue;
694
+ const danger = dangerOf(call.text, settings, { repo: agentRepo(ctx, agent) });
695
+ if (!danger) continue;
696
+ const ran = call.status !== "running";
697
+ seen.add(call.callId);
698
+ ctx.state.dangers = [...(ctx.state.dangers ?? []), { at: new Date().toISOString(), key, stepTitle: def.title, command: call.text.slice(0, 500), label: danger.label, ran, agentId: agent.id, callId: call.callId }].slice(-50);
699
+ save(ctx);
700
+ ctx.log(`危险命令(${key}):${danger.label},${ran ? "已运行完" : "正在运行,打断"}:${call.text.replace(/\s+/g, " ").slice(0, 160)}`);
701
+ if (turnRunning && memory.count < DANGER_MAX_INTERRUPTS) {
702
+ memory.count += 1;
703
+ await ctx.runtime.interruptAgent(agent.id, dangerMessage(call.text, danger, ran)).catch((error: Error) => ctx.log(`打断「${def.title}」失败:${error.message}`));
704
+ }
705
+ await notifyOrigin(ctx, dangerNotice({ stepTitle: def.title, agentTitle, command: call.text, label: danger.label, ran }));
706
+ await notifyExternal(ctx, "alert", `「${def.title}」步骤的 agent ${ran ? "运行了" : "开始运行"}不允许的命令(${danger.label})${ran ? ",请检查有没有造成影响" : ",已打断"}。`, {
707
+ kind: "danger",
708
+ stepTitle: def.title,
709
+ command: call.text.slice(0, 500),
710
+ label: danger.label,
711
+ ran,
712
+ });
713
+ }
714
+ }
715
+
716
+ /** The directory the agent works in: the joint repository's worktree when it is one of theirs. */
717
+ function agentRepo(ctx: PipelineContext, agent: StepAgent): string {
718
+ const record = ctx.state.history.find((h) => h.agentId === agent.id);
719
+ const slug = record?.key.includes("@") ? record.key.split("@").pop() : null;
720
+ return (slug && ctx.state.joint?.find((j) => j.slug === slug)?.repo) || ctx.repo;
721
+ }
722
+
664
723
  async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, deadline: Deadline): Promise<AgentUsage | null> {
665
724
  const notifiedPermissions = new Set<string>();
666
725
  const guard: ScanGuardMemory = { interrupted: new Set(), count: 0 };
726
+ const danger = { count: 0 };
667
727
  let consecutiveErrors = 0;
668
728
  for (;;) {
669
729
  let remaining = deadline.at - Date.now();
@@ -686,7 +746,9 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
686
746
  let outcome;
687
747
  try {
688
748
  // 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;
749
+ // 1.14: with the danger guard on, look at its commands every 15 seconds.
750
+ const guardSeconds = ctx.config.dangerGuard.enabled ? DANGER_CHECK_SECONDS : ctx.config.scanGuard.enabled ? SCAN_CHECK_SECONDS : Infinity;
751
+ const chunk = Math.min(ctx.config.waitChunkSeconds, guardSeconds);
690
752
  outcome = await agent.wait(Math.min(chunk * 1000, remaining));
691
753
  consecutiveErrors = 0;
692
754
  } catch (error) {
@@ -706,8 +768,10 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
706
768
  }
707
769
  switch (outcome.status) {
708
770
  case "idle":
771
+ await guardDanger(ctx, agent, def, agentTitle, deadline.key, false, danger);
709
772
  return outcome.usage ?? null;
710
773
  case "timeout":
774
+ await guardDanger(ctx, agent, def, agentTitle, deadline.key, true, danger);
711
775
  await guardScan(ctx, agent, def, deadline.key, guard);
712
776
  continue;
713
777
  case "error":
@@ -924,12 +988,14 @@ async function runStepOnce(
924
988
 
925
989
  if (extras.gate) {
926
990
  // The same agent fixes what its own change broke: it still has the context.
927
- await qualityGate(ctx, key, target, async (text) => {
991
+ const fix = async (text: string) => {
928
992
  await agent.followUp(text);
929
993
  record.usage = (await waitForTurn(ctx, agent, def, agentTitle, deadline)) ?? record.usage ?? null;
930
994
  const again = readResultFile(resultAbs, def);
931
995
  if (again.ok) validation = again;
932
- });
996
+ };
997
+ await qualityGate(ctx, key, target, fix);
998
+ await secretGate(ctx, key, target, fix);
933
999
  }
934
1000
 
935
1001
  record.outcome = "completed";
@@ -1703,6 +1769,69 @@ async function qualityGate(ctx: PipelineContext, key: string, target: StepTarget
1703
1769
  }
1704
1770
  }
1705
1771
 
1772
+ /** The commit the secret scan starts from: the attempt's code base, else where the branch left the main line. */
1773
+ function secretBase(ctx: PipelineContext, target?: StepTarget): string | null {
1774
+ const repo = target?.repo ?? ctx.repo;
1775
+ const recorded = codeBaseCommit(ctx, target);
1776
+ if (recorded) return recorded;
1777
+ for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main", "master"]) {
1778
+ try {
1779
+ const mb = execFileSync("git", ["merge-base", "HEAD", ref], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
1780
+ if (mb && mb !== headCommit(repo)) return mb;
1781
+ } catch {
1782
+ /* try the next */
1783
+ }
1784
+ }
1785
+ return null;
1786
+ }
1787
+
1788
+ /**
1789
+ * 1.14: after a step that changed code, looks for secrets the requirement added (secret-scan.ts);
1790
+ * what it finds goes back to the step's agent, like a failing check, up to qualityGate.fixAttempts
1791
+ * times. The outcome is kept for the review, the report and the push.
1792
+ */
1793
+ async function secretGate(ctx: PipelineContext, key: string, target: StepTarget | undefined, fix: (text: string) => Promise<void>): Promise<void> {
1794
+ if (!ctx.config.secretScan.enabled) return;
1795
+ const repo = target?.repo ?? ctx.repo;
1796
+ const base = secretBase(ctx, target);
1797
+ const max = ctx.config.qualityGate.fixAttempts;
1798
+ for (let fixRounds = 0; ; fixRounds += 1) {
1799
+ const findings = scanChange(repo, base, ctx.config.secretScan);
1800
+ if (findings === null) {
1801
+ ctx.log(`密钥扫描(${key} 之后):找不到改动的起点,没有扫描`);
1802
+ return;
1803
+ }
1804
+ if (findings.length === 0 || fixRounds >= max) {
1805
+ const run: SecretScanRun = { key, attempt: ctx.state.attempt, at: new Date().toISOString(), findings, fixRounds, passed: findings.length === 0 };
1806
+ ctx.state.secretScans = [...(ctx.state.secretScans ?? []).filter((r) => !(r.key === key && r.attempt === run.attempt)), run].slice(-30);
1807
+ save(ctx);
1808
+ ctx.log(`密钥扫描(${key} 之后):${run.passed ? (fixRounds > 0 ? `要求处理 ${fixRounds} 次后没有发现` : "没有发现") : `要求处理 ${fixRounds} 次后仍有 ${findings.length} 处`}`);
1809
+ if (!run.passed) {
1810
+ await notifyOrigin(ctx, milestoneMessage(`「${key}」之后的密钥扫描仍发现 ${findings.length} 处疑似密钥(例如 ${describeFinding(findings[0])})。这些提交不会被推送;请用户看一下是不是真实的密钥。`));
1811
+ }
1812
+ return;
1813
+ }
1814
+ ctx.log(`密钥扫描(${key} 之后):发现 ${findings.length} 处疑似密钥,第 ${fixRounds + 1} 次要求处理`);
1815
+ await fix(secretFollowUp({ findings, base: base!, round: fixRounds + 1, max }));
1816
+ }
1817
+ }
1818
+
1819
+ /** What the secret scan found in this attempt, for the code review and the report. */
1820
+ function secretLines(ctx: PipelineContext): string[] {
1821
+ const runs = (ctx.state.secretScans ?? []).filter((r) => r.attempt === ctx.state.attempt);
1822
+ const last = runs.at(-1);
1823
+ if (!last) return [];
1824
+ if (last.passed) return [`密钥扫描(${last.key} 之后,流水线运行):没有发现疑似密钥${last.fixRounds > 0 ? `(要求处理 ${last.fixRounds} 次后)` : ""}`];
1825
+ return [`密钥扫描(${last.key} 之后,流水线运行):仍有 ${last.findings.length} 处疑似密钥,分支不会被推送:`, ...last.findings.slice(0, 10).map((f) => `- ${describeFinding(f)}`)];
1826
+ }
1827
+
1828
+ /** Dangerous commands step agents ran in this run, for the report. */
1829
+ function dangerLines(ctx: PipelineContext): string[] {
1830
+ const list = ctx.state.dangers ?? [];
1831
+ if (list.length === 0) return [];
1832
+ return [`流水线拦下的危险命令(${list.length} 次,已通知用户):`, ...list.slice(-10).map((d) => `- 「${d.stepTitle}」${d.ran ? "运行了" : "开始运行(已打断)"}:${d.label}`)];
1833
+ }
1834
+
1706
1835
  function describeGateRun(run: GateRun): string {
1707
1836
  const ran = run.checks.filter((c) => c.result !== "skipped");
1708
1837
  const skipped = run.checks.length - ran.length;
@@ -1963,7 +2092,7 @@ async function developPhase(ctx: PipelineContext): Promise<Record<string, unknow
1963
2092
 
1964
2093
  const reviewLoop = async (target?: StepTarget) => {
1965
2094
  for (let r = 1; r <= rounds.codeReview; r++) {
1966
- const lines = [...gateLines(ctx, target), ...(target ? [] : scopeLines(ctx)), ...acceptanceLines(ctx, target)];
2095
+ const lines = [...gateLines(ctx, target), ...(target ? [] : [...secretLines(ctx), ...scopeLines(ctx)]), ...acceptanceLines(ctx, target)];
1967
2096
  const review = await runReview(ctx, "code_review", r, rounds.codeReview, lines, target);
1968
2097
  if (review.verdict === "pass") break;
1969
2098
  await runStep(ctx, "review_fix", r, rounds.codeReview, { gate: true, target });
@@ -2142,7 +2271,7 @@ function externalLines(ctx: PipelineContext): string[] {
2142
2271
  }
2143
2272
 
2144
2273
  function reportLines(ctx: PipelineContext): string[] {
2145
- const lines = [...usageLines(ctx), ...gateLines(ctx), ...scopeLines(ctx), ...acceptanceLines(ctx), ...jointLines(ctx), ...externalLines(ctx)];
2274
+ const lines = [...usageLines(ctx), ...gateLines(ctx), ...secretLines(ctx), ...dangerLines(ctx), ...scopeLines(ctx), ...acceptanceLines(ctx), ...jointLines(ctx), ...externalLines(ctx)];
2146
2275
  if (ctx.config.checks.length === 0 && ctx.state.type !== "assessment") {
2147
2276
  lines.push("质量门:项目没有配置检查(checks),流水线没有自己运行检查,以各步骤记录的检查结果为准");
2148
2277
  }
@@ -1,3 +1,4 @@
1
+ import { pendingHumanReview } from "./code-review.js";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -14,7 +15,7 @@ import {
14
15
  } from "./config.js";
15
16
  import { gitInfo } from "./git.js";
16
17
  import { samePath } from "./paths.js";
17
- import { currentLockHolder, type PipelineState, readState, statePath } from "./state.js";
18
+ import { currentLockHolder, isAlive, type PipelineState, readState, statePath } from "./state.js";
18
19
 
19
20
  export interface LaunchArgs {
20
21
  repo: string;
@@ -33,6 +34,8 @@ export interface LaunchArgs {
33
34
  cardFields?: boolean;
34
35
  /** 1.14 --track: register a requirement being aligned, before the pipeline runs (shown in the plugin). */
35
36
  track?: boolean;
37
+ /** 1.14 --review-fix: a person's code review asked for changes; a new attempt handles them. */
38
+ reviewFix?: boolean;
36
39
  /** --spec-review: independent review of the draft spec, before confirmation. */
37
40
  specReview: boolean;
38
41
  /** --worktree: create a Paseo worktree workspace for this requirement and develop there. */
@@ -254,6 +257,12 @@ export function preflight(
254
257
  ? "这个需求的流水线已经完成。如需重新完整运行,请加 --restart。"
255
258
  : `这个需求已有运行记录(状态:${previous.status})。从中断处继续请加 --resume,从头开始请加 --restart。`;
256
259
  throw new PreflightError(hint);
260
+ } else if (args.reviewFix) {
261
+ // 1.14: the person's code review (plugin) asked for changes; the spec itself did not change.
262
+ if (previous.status === "running" && isAlive(previous.pid)) throw new PreflightError("流水线正在运行,等它结束后再处理人工评审意见。");
263
+ const pending = pendingHumanReview(path.join(repo, featureDir));
264
+ if (!pending) throw new PreflightError("没有待处理的人工代码评审意见(在插件的代码审查页上提交\"退回修改\"后才有)。");
265
+ mode = { kind: "resume-from-design", previous, reason: `人工代码评审提出了 ${pending.items.length} 条修改意见(见 code-review.md 的"人工评审"一节和 changes.md),需求说明不变,从方案设计开始只处理这些意见` };
257
266
  } else if (args.approve && previous.status !== "waiting_for_approval") {
258
267
  throw new PreflightError(`这个需求没有等待确认的方案(状态:${previous.status}),不需要 --approve。继续开发请用 --resume。`);
259
268
  } else if (previous.status === "waiting_for_approval" && currentSpecHash(specPath) !== previous.specHash) {
@@ -6,6 +6,8 @@
6
6
  // on the machine. The overview itself is still read live (ADR 0010).
7
7
  // usage/runs.jsonl one line per pipeline run (a process from start to stop): steps,
8
8
  // time, usage, gate and review rounds. What `req stats` reports on.
9
+ // 1.14: also one line per event after the run: a person's code review,
10
+ // the acceptance, the merge into the main line, a problem found after it.
9
11
  //
10
12
  // Recording never affects a run: every write is best effort. REQ_KIT_NO_RECORD=1 turns both
11
13
  // off (evaluation runs are not real requirements).
@@ -321,3 +323,72 @@ export function readRuns(file: string): RunRecord[] {
321
323
  }
322
324
  return out;
323
325
  }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Events after the run (1.14)
329
+
330
+ export type EventKind = "review" | "acceptance" | "merge" | "issue";
331
+
332
+ /** Something that happened to a requirement outside its runs (contracts/usage-record.md). */
333
+ export interface EventRecord {
334
+ schema: 1;
335
+ kind: EventKind;
336
+ machine: string;
337
+ kitVersion: string;
338
+ at: string;
339
+ requirement: { key: string; project: string; name: string; title: string };
340
+ /** A person's code review in the plugin. */
341
+ review?: { verdict: "approved" | "changes_requested"; reviewer: string; items: number; head: string | null };
342
+ /** The requester's acceptance on the checklist page. */
343
+ acceptance?: { verdict: "通过" | "不通过"; reviewer: string; total: number; passed: number; failed: number; unchecked: number };
344
+ /** The requirement's change reached the main line. how: found its files there ("files"), or a person said so ("manual"). */
345
+ merge?: { into: string; commit: string | null; mergedAt: string | null; how: "files" | "manual" };
346
+ /** A problem found after the merge: from a defect requirement that names this one, or a person's note. */
347
+ issue?: { text: string; by: string | null; id: string };
348
+ }
349
+
350
+ /** The requirement as events name it: the index entry when there is one (same key as its runs). */
351
+ export function requirementRef(repo: string, featureDir: string, env: NodeJS.ProcessEnv = process.env): EventRecord["requirement"] {
352
+ const fd = featureDir.split(path.sep).join("/");
353
+ const r = real(repo);
354
+ const entry = readIndex(env).find((e) => e.featureDir === fd && (real(e.repo) === r || (e.sourceRepo && real(e.sourceRepo) === r)));
355
+ const source = entry?.sourceRepo ?? entry?.repo ?? repo;
356
+ let title = path.basename(fd);
357
+ try {
358
+ const m = /^#\s+(.+)$/m.exec(fs.readFileSync(path.join(entry?.repo ?? repo, fd, "spec.md"), "utf8"));
359
+ if (m) title = m[1].trim();
360
+ } catch {
361
+ /* no spec: the directory name */
362
+ }
363
+ return { key: entry?.key ?? requirementKey(source, fd), project: entry?.project ?? path.basename(real(source)), name: path.basename(fd), title };
364
+ }
365
+
366
+ /** Appends an event to the usage records. Never throws. */
367
+ export function appendEvent(input: Omit<EventRecord, "schema" | "machine" | "kitVersion" | "at"> & { at?: string }, env: NodeJS.ProcessEnv = process.env): void {
368
+ if (recordingOff(env)) return;
369
+ const record: EventRecord = { schema: 1, machine: os.hostname(), kitVersion: kitVersion(), at: input.at ?? new Date().toISOString(), ...input } as EventRecord;
370
+ try {
371
+ fs.mkdirSync(usageDir(env), { recursive: true });
372
+ fs.appendFileSync(runsFile(env), `${JSON.stringify(record)}\n`);
373
+ } catch {
374
+ // Usage records are a convenience.
375
+ }
376
+ }
377
+
378
+ const EVENT_KINDS = new Set<string>(["review", "acceptance", "merge", "issue"]);
379
+
380
+ /** Event records from a JSONL file (this machine's, or an export). */
381
+ export function readEvents(file: string): EventRecord[] {
382
+ if (!fs.existsSync(file)) return [];
383
+ const out: EventRecord[] = [];
384
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
385
+ if (!line.trim()) continue;
386
+ try {
387
+ const r = JSON.parse(line) as EventRecord;
388
+ if (r && EVENT_KINDS.has(r.kind) && r.schema === RECORD_SCHEMA && r.requirement && typeof r.at === "string") out.push(r);
389
+ } catch {
390
+ // A torn line.
391
+ }
392
+ }
393
+ return out;
394
+ }
@@ -6,12 +6,15 @@ import { DEFAULT_CONFIG, loadConfig, type PipelineConfig } from "./config.js";
6
6
  import { defaultConfigPath } from "./locations.js";
7
7
  import { pluginReplyMessage } from "./messages.js";
8
8
  import { loadRuntimeFactory } from "./runtime-loader.js";
9
+ import { readIndex } from "./records.js";
9
10
  import { readState, statePath } from "./state.js";
10
11
 
11
12
  async function main(argvIn: string[]): Promise<number> {
12
13
  // 1.14 --acceptance: the checklist page found problems; the text is the whole notice.
14
+ // 1.14 --notice: a notice of the background check (no run needed: the chat it was aligned in).
13
15
  const acceptance = argvIn.includes("--acceptance");
14
- const argv = argvIn.filter((a) => a !== "--acceptance");
16
+ const notice = argvIn.includes("--notice");
17
+ const argv = argvIn.filter((a) => a !== "--acceptance" && a !== "--notice");
15
18
  const [feature, ...words] = argv;
16
19
  const text = words.join(" ").trim();
17
20
  if (!feature || !text) {
@@ -20,6 +23,14 @@ async function main(argvIn: string[]): Promise<number> {
20
23
  }
21
24
  const { repo, featureDir } = locate(process.cwd(), feature);
22
25
  const state = readState(statePath(repo, featureDir));
26
+ if (notice) {
27
+ const chat = state?.originAgentId ?? readIndex().find((e) => e.featureDir === featureDir && [e.repo, e.sourceRepo].includes(repo))?.chatAgentId ?? null;
28
+ if (!chat) {
29
+ console.error("这个需求没有登记过对话,没有可以转达的对话。");
30
+ return 1;
31
+ }
32
+ return deliver(repo, chat, text.slice(0, 8000), "已转给这个需求的对话。");
33
+ }
23
34
  if (!state || (!acceptance && state.status !== "waiting_for_business" && state.status !== "waiting_for_approval")) {
24
35
  console.error(`${featureDir} 现在没有在等待回答或确认,不用回复。`);
25
36
  return 1;
@@ -28,6 +39,22 @@ async function main(argvIn: string[]): Promise<number> {
28
39
  console.error("这个需求不是从 Paseo 对话中开始的,没有可以转达的对话。请在终端或卡片中处理。");
29
40
  return 1;
30
41
  }
42
+ return deliver(
43
+ repo,
44
+ state.originAgentId,
45
+ acceptance
46
+ ? text.slice(0, 8000)
47
+ : pluginReplyMessage({
48
+ featureDir,
49
+ waitingFor: state.status === "waiting_for_approval" ? "approval" : "business",
50
+ text: text.slice(0, 8000),
51
+ questions: state.pendingQuestions ?? [],
52
+ }),
53
+ acceptance ? "已转给这个需求的对话。" : "已转给这个需求的对话,它会按回复更新需求说明并继续开发。",
54
+ );
55
+ }
56
+
57
+ async function deliver(repo: string, chat: string, message: string, done: string): Promise<number> {
31
58
  let config: PipelineConfig;
32
59
  try {
33
60
  config = loadConfig(defaultConfigPath(), repo).config;
@@ -37,19 +64,11 @@ async function main(argvIn: string[]): Promise<number> {
37
64
  }
38
65
  const runtime = await (await loadRuntimeFactory())({ config, log: () => undefined });
39
66
  try {
40
- await runtime.notifyAgent(
41
- state.originAgentId,
42
- acceptance ? text.slice(0, 8000) : pluginReplyMessage({
43
- featureDir,
44
- waitingFor: state.status === "waiting_for_approval" ? "approval" : "business",
45
- text: text.slice(0, 8000),
46
- questions: state.pendingQuestions ?? [],
47
- }),
48
- );
67
+ await runtime.notifyAgent(chat, message);
49
68
  } finally {
50
69
  await runtime.close().catch(() => undefined);
51
70
  }
52
- console.log(acceptance ? "已转给这个需求的对话。" : "已转给这个需求的对话,它会按回复更新需求说明并继续开发。");
71
+ console.log(done);
53
72
  return 0;
54
73
  }
55
74
 
@@ -0,0 +1,93 @@
1
+ :root {
2
+ --bg: #f6f7f9; --surface: #ffffff; --surface-2: #f1f3f6; --text: #1c2430; --muted: #5d6b7c; --border: #dfe3e9;
3
+ --accent: #2457d6; --accent-text: #ffffff; --ok: #15803d; --ok-soft: #e7f6ec; --danger: #b91c1c; --danger-soft: #fdecec; --warn: #b45309; --warn-soft: #fdf3e2;
4
+ --add: #e6f6ea; --del: #fdecec; --flash: #fff4c2;
5
+ color-scheme: light;
6
+ }
7
+ @media (prefers-color-scheme: dark) {
8
+ :root {
9
+ --bg: #12161c; --surface: #1a2029; --surface-2: #222a35; --text: #e6eaf0; --muted: #9aa7b8; --border: #2e3846;
10
+ --accent: #6f9bff; --accent-text: #0d1420; --ok: #4ade80; --ok-soft: #173322; --danger: #f87171; --danger-soft: #3a1c1c; --warn: #fbbf24; --warn-soft: #3a2c12;
11
+ --add: #15301f; --del: #3a1c1c; --flash: #4a3f12;
12
+ color-scheme: dark;
13
+ }
14
+ }
15
+ * { box-sizing: border-box; }
16
+ [hidden] { display: none !important; }
17
+ body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.6 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; padding-bottom: 110px; }
18
+ .top { padding: 20px 24px 8px; }
19
+ .eyebrow { font-size: 13px; color: var(--muted); }
20
+ h1 { margin: 2px 0 4px; font-size: 22px; }
21
+ h2 { font-size: 15px; margin: 0 0 6px; }
22
+ .muted { color: var(--muted); }
23
+ .small { font-size: 13px; }
24
+ .pad { padding: 8px 12px; }
25
+ .banner { margin: 8px 24px; padding: 10px 14px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--border); }
26
+ .banner.ok { background: var(--ok-soft); border-color: var(--ok); }
27
+ .banner.warn { background: var(--warn-soft); border-color: var(--warn); }
28
+ .layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 16px; padding: 0 24px; }
29
+ .files { position: sticky; top: 8px; align-self: start; max-height: calc(100vh - 140px); overflow: auto; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 10px; font-size: 13px; }
30
+ .nav-title { font-weight: 600; margin-bottom: 6px; }
31
+ .files a { display: flex; justify-content: space-between; gap: 6px; padding: 4px 6px; border-radius: 6px; color: var(--text); text-decoration: none; word-break: break-all; }
32
+ .files a:hover { background: var(--surface-2); }
33
+ .files a.doc { color: var(--muted); }
34
+ main { min-width: 0; }
35
+ .intro { margin: 8px 0 12px; }
36
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; margin: 0 0 12px; }
37
+ .badge { font-size: 12px; padding: 1px 8px; border-radius: 999px; background: var(--surface-2); color: var(--muted); white-space: nowrap; }
38
+ .badge.danger { background: var(--danger-soft); color: var(--danger); }
39
+ .badge.warn { background: var(--warn-soft); color: var(--warn); }
40
+ .badge.ok { background: var(--ok-soft); color: var(--ok); }
41
+ .file { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; margin-bottom: 12px; overflow: hidden; }
42
+ .file-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 8px 12px; background: var(--surface-2); cursor: pointer; border-bottom: 1px solid var(--border); }
43
+ .file.folded .file-body { display: none; }
44
+ .file.folded .file-head { border-bottom: 0; }
45
+ .path { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; font-weight: 600; word-break: break-all; }
46
+ .plus { color: var(--ok); font-size: 13px; }
47
+ .minus { color: var(--danger); font-size: 13px; }
48
+ .file-body { overflow-x: auto; }
49
+ table.diff { border-collapse: collapse; width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; line-height: 1.5; }
50
+ .diff td { padding: 0 8px; vertical-align: top; }
51
+ .diff .num { width: 1%; min-width: 44px; text-align: right; color: var(--muted); user-select: none; }
52
+ .diff .code { white-space: pre; }
53
+ .diff .hunk td { color: var(--muted); background: var(--surface-2); padding: 2px 8px; }
54
+ .diff .line { cursor: pointer; }
55
+ .diff .line:hover td { outline: 1px solid var(--border); }
56
+ .diff .add td { background: var(--add); }
57
+ .diff .del td { background: var(--del); }
58
+ .diff .flash td, .flash { background: var(--flash) !important; }
59
+ .finding { border: 1px solid var(--border); border-left: 4px solid var(--border); border-radius: 8px; padding: 8px 10px; margin: 6px 0; background: var(--surface); font-family: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 14px; white-space: normal; }
60
+ .finding.danger { border-left-color: var(--danger); }
61
+ .finding.warn { border-left-color: var(--warn); }
62
+ .finding.decided-ok { opacity: 0.6; }
63
+ .finding-row td, .comment-row td { padding: 4px 12px 8px; }
64
+ .finding-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
65
+ .finding-text { margin: 4px 0; }
66
+ .where { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; color: var(--accent); }
67
+ .marks { display: flex; gap: 8px; margin-top: 6px; }
68
+ .comment { border: 1px solid var(--accent); border-radius: 8px; padding: 8px 10px; background: var(--surface); font-family: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
69
+ textarea, input { font: inherit; color: inherit; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; width: 100%; }
70
+ .note-input { margin-top: 6px; }
71
+ .btn { font: inherit; border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 8px; padding: 6px 14px; cursor: pointer; }
72
+ .btn.small { padding: 2px 10px; font-size: 13px; }
73
+ .btn.on-fix { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }
74
+ .btn.on-ok { background: var(--ok-soft); border-color: var(--ok); color: var(--ok); }
75
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
76
+ .btn.danger { border-color: var(--danger); color: var(--danger); }
77
+ .btn:disabled { opacity: 0.5; cursor: default; }
78
+ .bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; padding: 10px 24px; background: var(--surface); border-top: 1px solid var(--border); }
79
+ .bar .summary { flex: 1 1 240px; font-size: 13px; }
80
+ .field { display: flex; align-items: center; gap: 6px; font-size: 13px; white-space: nowrap; }
81
+ .field input { width: 200px; }
82
+ .actions { display: flex; gap: 8px; }
83
+ .toast { position: fixed; left: 50%; bottom: 90px; transform: translateX(-50%); max-width: min(640px, 92vw); background: var(--text); color: var(--bg); padding: 10px 14px; border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,.2); white-space: pre-line; }
84
+ @media (max-width: 860px) {
85
+ .layout { grid-template-columns: minmax(0, 1fr); padding: 0 12px; }
86
+ .files { position: static; max-height: 220px; }
87
+ .top { padding: 16px 12px 6px; }
88
+ .banner { margin: 8px 12px; }
89
+ .bar { padding: 10px 12px; }
90
+ .field { flex: 1 1 100%; }
91
+ .field input { width: 100%; }
92
+ body { padding-bottom: 220px; }
93
+ }
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>代码审查</title>
7
+ <link rel="stylesheet" href="code.css">
8
+ </head>
9
+ <body>
10
+ <header class="top">
11
+ <div>
12
+ <div class="eyebrow" id="project"></div>
13
+ <h1 id="title">代码审查</h1>
14
+ <div class="muted" id="sub"></div>
15
+ </div>
16
+ </header>
17
+ <section id="status" class="banner" hidden></section>
18
+ <div class="layout">
19
+ <nav class="files" id="files" aria-label="改动的文件"></nav>
20
+ <main>
21
+ <section id="summary" class="card" hidden></section>
22
+ <section id="ai" class="card" hidden></section>
23
+ <p class="muted intro">点代码行写意见(只对这一行);AI 的意见可以标<b>要改</b>或<b>不用改</b>。看完在下面选择<b>退回修改</b>(流水线会按意见改,改完再评审、再验证)或<b>评审通过</b>。进度会自动保存。</p>
24
+ <div id="diffs"></div>
25
+ <section class="card">
26
+ <h2>总体意见(可不填)</h2>
27
+ <textarea id="note" rows="3" placeholder="例如:整体可以,但导出接口要加权限校验"></textarea>
28
+ </section>
29
+ </main>
30
+ </div>
31
+ <footer class="bar">
32
+ <div class="summary" id="count"></div>
33
+ <label class="field">审核人 <input id="reviewers" placeholder="@用户名,用逗号分开"></label>
34
+ <label class="field">评审人 <input id="reviewer" placeholder="你的名字" maxlength="60"></label>
35
+ <div class="actions">
36
+ <button id="changes" class="btn danger" disabled>退回修改</button>
37
+ <button id="approve" class="btn primary" disabled>评审通过</button>
38
+ </div>
39
+ </footer>
40
+ <div id="toast" class="toast" hidden></div>
41
+ <script src="code.js"></script>
42
+ </body>
43
+ </html>