dsh-plugin-teamflow 0.1.9 → 0.2.0
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 +252 -185
- package/README.en.md +57 -62
- package/README.md +50 -76
- package/lib/client.js +207 -38
- package/lib/descriptors.mjs +5 -1
- package/lib/host.mjs +2277 -241
- package/lib/store.mjs +22 -0
- package/package.json +134 -134
package/lib/host.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { TEAMFLOW_DESCRIPTORS } from "./descriptors.mjs";
|
|
2
|
-
import { fileFor, journalFile, loadJournals, logsArchiveRoot, persistJournal, readJson, readJsonAny, runLogArchiveDir, runLogStagingDir, slugPath, teamflowRoot, writeJson } from "./store.mjs";
|
|
2
|
+
import { dshHome, fileFor, journalFile, loadJournals, logsArchiveRoot, persistJournal, readJson, readJsonAny, runLogArchiveDir, runLogStagingDir, slugPath, teamflowRoot, writeJson } from "./store.mjs";
|
|
3
3
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { parameterSchemaSpecToJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { fileAddressFor } from "@deepseek-ai/dsh-util-workspace-path";
|
|
9
|
-
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
10
12
|
//#region host/constants.ts
|
|
11
13
|
/**
|
|
12
14
|
* 单次 withRetry 调用的**新增** token 熔断预算(2026-09-11 口径修正)。
|
|
@@ -304,6 +306,7 @@ const PIPELINE_DICT = {
|
|
|
304
306
|
"dev.qaFixStage": "开发(QA 打回修复)",
|
|
305
307
|
"log.enterStage": "进入阶段:{phase}",
|
|
306
308
|
"log.triage": "自动分诊:{kind} → {mode}(source={source})",
|
|
309
|
+
"log.triageFallbackReason": "分诊退化兜底原因:{msg}(分诊子代理失败/超时——若为超时,考虑模型思考时长与 TRIAGE_TIMEOUT_MS)",
|
|
307
310
|
"log.triageFail": "自动分诊失败,按默认完整流程:{msg}",
|
|
308
311
|
"log.team": "团队「{name}」:阶段 {list}",
|
|
309
312
|
"log.stageSet": "档位阶段集:mode={mode} → {list}",
|
|
@@ -359,8 +362,21 @@ const PIPELINE_DICT = {
|
|
|
359
362
|
"log.commitDone": "统一收口提交完成(代码 + 任务夹产物,验收通过后一个 commit)",
|
|
360
363
|
"log.commitSkip": "统一收口提交:无待提交改动(跳过)",
|
|
361
364
|
"log.commitFail": "统一收口提交失败(忽略):{msg}",
|
|
365
|
+
"log.gitInitDone": "「改动存档」已开启(git init 完成;{baseline})",
|
|
366
|
+
"log.gitBaselineDone": "现有内容已记录为初始状态(基线提交)",
|
|
367
|
+
"log.gitBaselineSkip": "现有内容未记录为初始状态:{msg}",
|
|
368
|
+
"log.gitBaselineLarge": "该文件夹内容较多,未把现有内容记录为初始状态(不影响本次改动单独存档)",
|
|
369
|
+
"log.baselineExcludes": "基线提交按索引层排除了 {list}(不写入你的 .gitignore——\"该忽略什么\"由项目自己决定)",
|
|
370
|
+
"log.engine": "引擎:{provider}/{model}(run 起始模型路由;逐阶段真实路由见各阶段记录——子代理可被改道)",
|
|
371
|
+
"log.gitInitFail": "「改动存档」开启失败(git init 不可用):{path} → 本次按\"未开启存档\"继续",
|
|
372
|
+
"log.gitInitRefused": "「改动存档」被拒绝:该位置({path})太靠近磁盘根目录/系统目录,开启会波及大量无关文件 → 本次按\"未开启存档\"继续",
|
|
373
|
+
"log.noVcsByChoice": "本工作区未开启「改动存档」(用户选择):本次改动不会提交,也无法一键撤销;需要的话可让 agent 把该文件夹变成可存档的",
|
|
374
|
+
"log.noVcsDangerous": "该位置({path})不适合开启「改动存档」(盘根/系统目录):已跳过自动提交——请把项目放进一个单独的文件夹",
|
|
375
|
+
"commit.baseline": "chore: baseline before teamflow run",
|
|
376
|
+
"report.vcsArchived": "📦 本次改动**已存档**(1 次提交):不满意可整体撤销,也能随时查看这次改了什么。",
|
|
362
377
|
"log.logsUnstaged": "自有日志曾被扫进索引,已摘出 {n} 个(说明工作区 .gitignore 规则失效:被删掉或写不进去)——{list}",
|
|
363
378
|
"log.logsUnstageFail": "自有日志的索引兜底失败(提交面可能被污染):{msg}",
|
|
379
|
+
"log.devIdsBackfilled": "已为 {n} 个历史开发阶段补算任务编号(dt-N)——升级前的产物按标题回溯到任务,避免把它们当成\"没做过\"重新补跑",
|
|
364
380
|
"run.resumeBacklog": "断点续跑:复用 backlog(req={reqId}),从「{phase}」继续",
|
|
365
381
|
"run.resume": "断点续跑:从「{phase}」继续(已完成阶段复用产物)",
|
|
366
382
|
"run.resumeDev": "断点续跑开发:复用 {reused} 个已完成任务,补跑 {todo} 个失败任务",
|
|
@@ -404,8 +420,14 @@ const PIPELINE_DICT = {
|
|
|
404
420
|
"diag.degenerated": "{label} 进行中护栏中止(退化/推理复读),不再自动重试(污染会话内重试大概率复现且烧钱);可 teamflow_resume 以全新会话续跑",
|
|
405
421
|
"diag.stalled": "{label} 进行中护栏中止(挂死/空转),不再自动重试(会话已无有效产出);可 teamflow_resume 以全新会话续跑",
|
|
406
422
|
"diag.breaker": "{label} 累计新增 token {fresh}k(input+cacheWrite+output,不含缓存命中)超出预算 {budget}k,熔断,需人工介入",
|
|
423
|
+
"diag.breakerUncached": "{label} 累计新增 token {fresh}k 超出预算 {budget}k(**该 provider 无 prompt 缓存**:命中率 {ratio}%、{calls} 次调用——每轮调用都要重付 system+工具定义,预算已按 {budget}k 计),熔断,需人工介入",
|
|
424
|
+
"diag.docDelivered": "{label} 回复过短({length} 字符 < {min} 下限),但任务夹产物 {name} 已落盘({docLen} 字符)→ 判交付(doc 类阶段的产物是文件,回复只是摘要)",
|
|
407
425
|
"diag.retry": "{label} 第 {n} 次尝试未成功({outcome}),自动重试(重试 prompt 已附上一轮失败诊断)…",
|
|
408
426
|
"diag.retryExhausted": "{label} 连续 {n} 次尝试失败,超出重试阈值,需人工介入",
|
|
427
|
+
"diag.externalBackoff": "{label} 疑似外部供应商不可用(限流/额度/上游故障)→ 第 {n} 次退避 {sec}s 后重试(等待不计 token;命中原文:{detail})",
|
|
428
|
+
"diag.externalExhausted": "{label} 外部供应商持续不可用,退避 {n} 次仍失败 → 本条 run 落「可续跑中断态」(非交付缺陷;窗口恢复后 resume 只补跑这一段)",
|
|
429
|
+
"diag.triageCacheHit": "分诊结果命中缓存(同需求 + 同澄清答复,10 分钟内)——复用上次裁决、不重复起子代理(决策返回路径会重调 teamflow_start,实测会白跑一次分诊)",
|
|
430
|
+
"report.externalFailure": "⚠️ **本次失败疑似外部供应商不可用**(限流 / 无额度 / 上游故障 / 超时),**不是交付缺陷**:run 已停在可续跑的中断态,供应商窗口恢复后 `teamflow_resume` 只补跑未完成阶段(已完成阶段与产物全部复用)。",
|
|
409
431
|
"guard.observeTag": "[token 观测]",
|
|
410
432
|
"guard.repeatRead": "同一文件重复 read {n} 次:{file}(TOKEN_HYGIENE 上限 1 次,多余读取在为后续每一步付 cache 重放费)",
|
|
411
433
|
"guard.repeatScript": "验证脚本重复执行 {n} 次:{file}(批量修复纪律:一次修完所有失败再跑,≤3 轮)",
|
|
@@ -425,6 +447,11 @@ const PIPELINE_DICT = {
|
|
|
425
447
|
"guard.noticeSummary": "护栏轻提醒",
|
|
426
448
|
"guard.reminderRead": "你已整读 {file} 第 3 次(TOKEN_HYGIENE 上限 1 次)。停止整读:需要确认细节时用 grep 定位行号 + 限量片段读取。",
|
|
427
449
|
"guard.reminderScript": "验证脚本 {file} 已重复执行 3 次。遵守批量修复纪律:一次读完所有失败用例 → 一次全修 → 再跑一次;超出 3 轮请输出诊断摘要并停止。",
|
|
450
|
+
"guard.toolFail": "{label} 命令/工具以同一错误持续失败({tool},已 {n} 次):{detail}",
|
|
451
|
+
"guard.reasonToolFail": "环境不可用({tool} 连续 {n} 次以同一错误失败:{detail})",
|
|
452
|
+
"guard.reminderToolFail": "命令工具不可用:{tool} 已连续 {n} 次以同一错误失败(原文:{detail})。立即停止——不要重试、不要换命令绕过、不要用推理代替执行。请在回复里写明:工具名 + 原文错误 + 已完成/未完成,然后结束。",
|
|
453
|
+
"diag.envUnavailable": "{label} 环境不可用(命令工具持续以同一错误失败)→ 不再自动重试:这不是模型问题,请先修复工作区,再 teamflow_resume 续跑(已完成阶段与产物全部复用)",
|
|
454
|
+
"diag.envUnavailableStage": "{label} 环境不可用(命令工具持续以同一错误失败):{detail}",
|
|
428
455
|
"report.header": "【团队研发流水线汇报】runId={id}",
|
|
429
456
|
"report.statusLine": "状态:{status}{error}",
|
|
430
457
|
"report.error": "({error})",
|
|
@@ -441,13 +468,41 @@ const PIPELINE_DICT = {
|
|
|
441
468
|
"report.listSep": ",",
|
|
442
469
|
"report.product": "产品:{product}",
|
|
443
470
|
"report.mode": "模式:{mode}",
|
|
471
|
+
"report.engine": "引擎(模型路由):{engine}",
|
|
444
472
|
"report.backlog": "backlog:需求 {reqId}($DSH_HOME/teamflow/ 持久化)",
|
|
473
|
+
"report.installPending": "🧩 **安装待办(需主 agent 执行,子代理权限固定写不了 profile)**:profile 目录 `{dir}`——{how};装完重启宿主,确认插件被加载。安装前先记好回滚(`pnpm remove <包名>` 并确认它已从 `dsh.profile.bundles` 摘除)。",
|
|
474
|
+
"report.installHowCli": "执行 `dsh plugin --profile {profile} add \"<本插件路径或包名>\"`",
|
|
475
|
+
"report.installHowManual": "`dsh` 不在 PATH(源码运行)→ 在该目录执行 `pnpm add \"<本插件路径或包名>\"`,包声明了 `dsh.bundle.patch` 时会自动进 `dsh.profile.bundles`",
|
|
476
|
+
"report.installAskUser": "🧩 **安装待办**:未能探测到 profile 目录 → 请先**问用户**他的 profile 在哪,再给安装步骤(不要编路径)。子代理写不了 profile,这一步归主 agent。",
|
|
445
477
|
"report.mergeHint": "🧩 合回决策:验收已通过,当前在特性分支 {branch}(领先 main {ahead} 个提交)。请**询问用户**是否合回,并按用户选择调用 teamflow_merge:\n ① host 代为合回 → action=\"merge\"\n ② 给出命令用户自行合回 → action=\"command\"\n ③ 暂不合回 → action=\"keep\"\n(选项之外用户自定义输入亦可,如实转述)",
|
|
446
478
|
"report.needsHumanNoAcceptance": "⚠ **本轮未完成产品验收**(流程因需人工介入提前结束,验收阶段未运行)——请先处理人工介入项,不要据此合回 main。",
|
|
447
479
|
"report.knownIssuesNoMerge": "⚠ **本轮验收是「已知问题」只读模式**(QA 打回超限后补跑的交付级视图):ACCEPTANCE.md 与未闭环清单已产出,结论被**强制为需人工裁定**——先人工裁定未闭环阻断缺陷,**不要据此合回 main**;裁定后可 `teamflow_resume` 修复或另起需求。",
|
|
448
480
|
"report.stashHint": "🧩 启动前 stash 的改动仍在暂存区:流水线完成后请执行 git stash pop 恢复你的改动",
|
|
449
481
|
"report.tabHint": "用户可打开「🏭 团队工作台」tab 查看阶段泳道、拖拽看板与 token 明细。",
|
|
450
482
|
"report.next": "如需继续处理:可认领缺陷(teamflow_claim)、人工流转(teamflow_update)、断点重跑(teamflow_resume)。",
|
|
483
|
+
"report.nextCancelled": "本 run 已停在取消点,**不会自动续跑**(host 无自动续跑机制)。如需继续,请让**用户**在工作台点「↻ 从断点重跑」——取消是人的决定,不要自行调用 teamflow_resume。",
|
|
484
|
+
"report.cancelSource": "中断来源:{source}",
|
|
485
|
+
"report.assumptions": "⚠️ **本次基于以下假设启动**(agent 替你做的判断;如与预期不符,指出后可从 PRD 重跑):\n{list}",
|
|
486
|
+
"log.triageBlockersDropped": "分诊提了 {n} 条不合格「待澄清项」(缺互斥读法/影响面/返工面)→ 已丢弃(合格线见 host)",
|
|
487
|
+
"log.prdAssumptions": "PRD 的「假设 / 待澄清」段已记录({n} 行)——随完成汇报显式回给用户",
|
|
488
|
+
"log.prdAssumptionsMissing": "PRD 未给出「假设 / 待澄清」段(契约未兑现:无从判断这份 PRD 是否是用户想要的;已记录不中断)",
|
|
489
|
+
"log.modeUpgraded": "架构护栏:调用方自选档位「{from}」,但分诊判定为「{to}」→ 已升档(ADR-0006:架构型需求不得走轻档位)",
|
|
490
|
+
"log.triagePreflightFail": "预检分诊失败({msg})→ 已交给 pipeline 内部再判(不静默降级)",
|
|
491
|
+
"log.clarifyAbort": "需求澄清闸门(pipeline 兜底):意图「{intent}」/ must-know 缺口 {n} 条 → **未进入任何阶段**,等用户答复",
|
|
492
|
+
"log.clarifyBlocker": "待澄清:{q}(影响面:{changes};猜错的代价:{rework})",
|
|
493
|
+
"run.needsClarification": "需求尚未对齐(意图:{intent},must-know 缺口 {n} 条)——本 run 未进入任何阶段,可续跑。请主线程先向用户澄清,再把答复带(requirementSupplement)重新启动。",
|
|
494
|
+
"log.clarifyProceedWithAssumptions": "已澄清一轮但仍有 {n} 条 must-know 缺口 → 按假设开工(收敛规则:不再无限追问,假设写进 PRD「假设与待澄清」段并在汇报高亮)",
|
|
495
|
+
"log.triageResumed": "断点续跑:档位沿用「{mode}」(续跑不重跑分诊,避免白花调用与档位漂移)",
|
|
496
|
+
"log.artifactContract": "交付形态契约:判定形态「{kind}」→ 已向 PRD 下发 {n} 条必填 AC(形态契约,防\"看着完整却装不上\")",
|
|
497
|
+
"log.hostResearch": "目标宿主非本仓宿主(形态「{kind}」× 宿主「{host}」)→ **不下发本仓宿主契约**,改为要求 PRD 产出「宿主契约调研」段(硬门禁)",
|
|
498
|
+
"log.hostResearchOk": "宿主契约调研已产出({n} 条)→ 已落 journal.hostContract,后续阶段按该宿主自己的契约推进",
|
|
499
|
+
"log.hostResearchMissing": "PRD 缺少「宿主契约调研」段(本 run 目标是别的宿主框架,不先搞清它的加载契约就是在赌)→ 阶段失败,走重试",
|
|
500
|
+
"log.installEnv": "本机安装环境(运行时探测):profile「{profile}」· 目录 {dir} · dsh 在 PATH:{cli}(路径不写死,随用户环境变化)",
|
|
501
|
+
"log.installEnvUnknown": "本机安装环境**探测失败**(profile「{profile}」· 目录 {dir})→ PRD 将要求主 agent 先问用户 profile 位置,不编路径",
|
|
502
|
+
"cancelSource.ui": "工作台界面按钮(人工点击)",
|
|
503
|
+
"cancelSource.tool": "模型工具 teamflow_cancel",
|
|
504
|
+
"cancelSource.unknown": "未记录(历史 run)",
|
|
505
|
+
"log.cancelRequested": "收到中断请求(来源:{source})——在下一个阶段边界生效,随后收尾为「已取消」",
|
|
451
506
|
"report.relay": "若用户在场请简明转述以上要点;若无人值守仅记录即可,不必长篇回复。",
|
|
452
507
|
"report.notice": "团队研发流水线 {status}(runId={id})",
|
|
453
508
|
"report.status.completed": "✅ 已完成",
|
|
@@ -477,6 +532,7 @@ const PIPELINE_DICT = {
|
|
|
477
532
|
"state.acIndex": "- AC 索引({n} 条):{list}",
|
|
478
533
|
"state.modules": "- 模块({n}):{list}",
|
|
479
534
|
"state.verifyScripts": "- 验证脚本:{list}",
|
|
535
|
+
"state.artifactContracts": "- 交付形态契约(形态判定:{kind},共 {n} 条)——每条都是**可执行判据**,QA/验收必须逐条验证并把命令+结果写进产物;环境做不到的(如需重启宿主)必须写进人工补测清单,不得沉默:\n{list}",
|
|
480
536
|
"state.prdSummary": "- PRD 摘要:{summary}",
|
|
481
537
|
"state.techSummary": "- 技术方案摘要:{summary}",
|
|
482
538
|
"state.qaSummary": "- QA 摘要:{summary}",
|
|
@@ -542,6 +598,7 @@ const PIPELINE_DICT = {
|
|
|
542
598
|
"doc.archDeviationQ": "「deviates from blueprint / duplicated impl / should-have-extracted」",
|
|
543
599
|
"doc.engConstraints": "工程约束",
|
|
544
600
|
"doc.finalCommit": "统一收口提交",
|
|
601
|
+
"doc.assumptionsQ": "假设与待澄清",
|
|
545
602
|
"doc.noRevisionTable": "**No revision table, no version fields like「版本:vX.Y / 状态:进行中」**",
|
|
546
603
|
"qa.fixSummaryQ": "「truth judgment + fix」or「false-positive evidence」",
|
|
547
604
|
"doc.statusChain": "待办→开发中→待测试→测试中→待验收→已验收",
|
|
@@ -590,6 +647,7 @@ const PIPELINE_DICT = {
|
|
|
590
647
|
"dev.qaFixStage": "Dev (QA rework fix)",
|
|
591
648
|
"log.enterStage": "Entering stage: {phase}",
|
|
592
649
|
"log.triage": "Auto triage: {kind} → {mode} (source={source})",
|
|
650
|
+
"log.triageFallbackReason": "Triage fell back because: {msg} (triage subagent failed/timed out — if a timeout, consider model thinking time vs TRIAGE_TIMEOUT_MS)",
|
|
593
651
|
"log.triageFail": "Auto triage failed, falling back to the default full flow: {msg}",
|
|
594
652
|
"log.team": "Team \"{name}\": stages {list}",
|
|
595
653
|
"log.stageSet": "Stage set for mode: mode={mode} → {list}",
|
|
@@ -645,8 +703,21 @@ const PIPELINE_DICT = {
|
|
|
645
703
|
"log.commitDone": "Unified closing commit done (code + task-folder artifacts, one commit after acceptance)",
|
|
646
704
|
"log.commitSkip": "Unified closing commit: nothing to commit (skipped)",
|
|
647
705
|
"log.commitFail": "Unified closing commit failed (ignored): {msg}",
|
|
706
|
+
"log.gitInitDone": "\"Change archiving\" is now on (git init done; {baseline})",
|
|
707
|
+
"log.gitBaselineDone": "existing content recorded as the initial state (baseline commit)",
|
|
708
|
+
"log.gitBaselineSkip": "existing content NOT recorded as the initial state: {msg}",
|
|
709
|
+
"log.gitBaselineLarge": "this folder has many files, so existing content was not recorded as the initial state (this run's changes are still archived on their own)",
|
|
710
|
+
"log.baselineExcludes": "baseline commit excluded {list} at the index level only (your .gitignore is untouched — what to ignore is the project's own decision)",
|
|
711
|
+
"log.engine": "Engine: {provider}/{model} (model route at run start; each stage records its own effective route — subagents can be re-routed)",
|
|
712
|
+
"log.gitInitFail": "\"Change archiving\" could not be enabled (git init unavailable): {path} → continuing without archiving",
|
|
713
|
+
"log.gitInitRefused": "\"Change archiving\" refused: this location ({path}) is too close to the disk root / a system directory — enabling it would sweep in many unrelated files → continuing without archiving",
|
|
714
|
+
"log.noVcsByChoice": "This workspace has NO change archiving (user's choice): this run's changes will not be committed and cannot be undone in one step; ask the agent to make this folder archivable if wanted",
|
|
715
|
+
"log.noVcsDangerous": "This location ({path}) is not suitable for change archiving (disk root / system directory): the closing commit was skipped — put the project in its own folder instead",
|
|
716
|
+
"commit.baseline": "chore: baseline before teamflow run",
|
|
717
|
+
"report.vcsArchived": "📦 This run's changes are **archived** (1 commit): you can undo them as a whole and see exactly what changed at any time.",
|
|
648
718
|
"log.logsUnstaged": "Own log files had been staged and were unstaged: {n} ({list}) — the workspace .gitignore rule is not effective (removed or unwritable)",
|
|
649
719
|
"log.logsUnstageFail": "Unstaging own log files from the index failed (the commit surface may be polluted): {msg}",
|
|
720
|
+
"log.devIdsBackfilled": "Backfilled task ids (dt-N) for {n} historical dev stage(s) — pre-upgrade output is traced back to its task by title, so it is not treated as \"never done\" and re-run",
|
|
650
721
|
"run.resumeBacklog": "Resuming: reusing backlog (req={reqId}), continuing from \"{phase}\"",
|
|
651
722
|
"run.resume": "Resuming: continuing from \"{phase}\" (completed stages reuse their artifacts)",
|
|
652
723
|
"run.resumeDev": "Resuming development: reusing {reused} completed task(s), rerunning {todo} failed task(s)",
|
|
@@ -690,8 +761,14 @@ const PIPELINE_DICT = {
|
|
|
690
761
|
"diag.degenerated": "{label} aborted by the in-flight guard (degeneration/reasoning loop); no automatic retry (retrying in a polluted session usually reproduces and burns tokens); teamflow_resume can continue in a fresh session",
|
|
691
762
|
"diag.stalled": "{label} aborted by the in-flight guard (stall/spin); no automatic retry (the session produces nothing useful); teamflow_resume can continue in a fresh session",
|
|
692
763
|
"diag.breaker": "{label} cumulative fresh tokens {fresh}k (input+cacheWrite+output, excluding cache hits) exceed the {budget}k budget; circuit broken, human intervention required",
|
|
764
|
+
"diag.breakerUncached": "{label} cumulative fresh tokens {fresh}k exceed the {budget}k budget (**this provider has no prompt cache**: {ratio}% hit rate over {calls} calls — every call re-pays the system prompt + tool definitions, so the budget was scaled to {budget}k); circuit broken, human intervention required",
|
|
765
|
+
"diag.docDelivered": "{label} reply was too short ({length} chars < {min} minimum), but the task-folder artifact {name} is on disk ({docLen} chars) → judged delivered (for doc stages the artifact IS the file; the reply is only a summary)",
|
|
693
766
|
"diag.retry": "{label} attempt {n} did not succeed ({outcome}); retrying automatically (the retry prompt carries the previous failure diagnosis)…",
|
|
694
767
|
"diag.retryExhausted": "{label} failed {n} consecutive attempts, exceeding the retry threshold; human intervention required",
|
|
768
|
+
"diag.externalBackoff": "{label} looks like an external provider outage (rate limit / quota / upstream failure) → backoff #{n} for {sec}s before retrying (waiting costs no tokens; matched text: {detail})",
|
|
769
|
+
"diag.externalExhausted": "{label} external provider stayed unavailable after {n} backoffs → this run is parked as a resumable interruption (not a delivery defect; once the window recovers, resume re-runs only this segment)",
|
|
770
|
+
"diag.triageCacheHit": "triage result served from cache (same requirement + same clarification supplement, within 10 minutes) — reusing the previous verdict instead of spawning another subagent (the decision return path re-calls teamflow_start, which measurably wasted one triage run)",
|
|
771
|
+
"report.externalFailure": "⚠️ **This failure looks like an external provider outage** (rate limit / quota exhausted / upstream failure / timeout) — **not a delivery defect**: the run is parked at a resumable interruption; once the provider recovers, `teamflow_resume` re-runs only the unfinished stages (completed stages and artifacts are all reused).",
|
|
695
772
|
"guard.observeTag": "[token observation]",
|
|
696
773
|
"guard.repeatRead": "the same file was read {n} times: {file} (TOKEN_HYGIENE allows 1; extra reads pay cache replay on every later step)",
|
|
697
774
|
"guard.repeatScript": "verification script executed {n} times: {file} (batch-fix discipline: read all failures, fix once, run once; at most 3 rounds)",
|
|
@@ -711,6 +788,11 @@ const PIPELINE_DICT = {
|
|
|
711
788
|
"guard.noticeSummary": "Guard reminder",
|
|
712
789
|
"guard.reminderRead": "You have fully read {file} for the 3rd time (TOKEN_HYGIENE allows 1). Stop full reads: to confirm details, locate line numbers with grep and read a limited segment.",
|
|
713
790
|
"guard.reminderScript": "Verification script {file} has been executed 3 times. Follow the batch-fix discipline: read all failing cases once → fix them all in one pass → run once more; beyond 3 rounds, output a diagnostic summary and stop.",
|
|
791
|
+
"guard.toolFail": "{label} a command/tool keeps failing with the same error ({tool}, {n} times so far): {detail}",
|
|
792
|
+
"guard.reasonToolFail": "environment unavailable ({tool} failed {n} times with the same error: {detail})",
|
|
793
|
+
"guard.reminderToolFail": "The command tool is unavailable: {tool} has now failed {n} times with the same error (raw: {detail}). STOP now — do not retry, do not switch commands to work around it, do not replace execution with reasoning. Reply with the tool name, the raw error, what you finished and what is left undone, then end.",
|
|
794
|
+
"diag.envUnavailable": "{label} environment unavailable (a command tool kept failing with the same error) → no automatic retry: this is not a model problem; repair the workspace first, then teamflow_resume to continue (finished stages and artifacts are all reused)",
|
|
795
|
+
"diag.envUnavailableStage": "{label} environment unavailable (a command tool kept failing with the same error): {detail}",
|
|
714
796
|
"report.header": "[Team R&D pipeline report] runId={id}",
|
|
715
797
|
"report.statusLine": "Status: {status}{error}",
|
|
716
798
|
"report.error": " ({error})",
|
|
@@ -727,13 +809,41 @@ const PIPELINE_DICT = {
|
|
|
727
809
|
"report.listSep": ", ",
|
|
728
810
|
"report.product": "Product: {product}",
|
|
729
811
|
"report.mode": "Mode: {mode}",
|
|
812
|
+
"report.engine": "Engine (model route): {engine}",
|
|
730
813
|
"report.backlog": "backlog: requirement {reqId} (persisted under $DSH_HOME/teamflow/)",
|
|
814
|
+
"report.installPending": "🧩 **Install pending (for the MAIN agent — subagents cannot write the profile; their permission scope is fixed)**: profile dir `{dir}` — {how}; then restart the host and confirm the plugin loads. Note the rollback first (`pnpm remove <name>` and confirm it is gone from `dsh.profile.bundles`).",
|
|
815
|
+
"report.installHowCli": "run `dsh plugin --profile {profile} add \"<path-or-package>\"`",
|
|
816
|
+
"report.installHowManual": "`dsh` is not on PATH (source-run) → in that dir run `pnpm add \"<path-or-package>\"`; the package joins `dsh.profile.bundles` automatically when it declares `dsh.bundle.patch`",
|
|
817
|
+
"report.installAskUser": "🧩 **Install pending**: the profile dir could not be probed → **ASK THE USER** where their profile lives, then give the install steps (never invent a path). Subagents cannot write the profile; this step belongs to the main agent.",
|
|
731
818
|
"report.mergeHint": "🧩 Merge decision: acceptance passed and the current feature branch {branch} is {ahead} commit(s) ahead of main. **Ask the user** whether to merge and call teamflow_merge with their choice:\n ① host merges for them → action=\"merge\"\n ② print the command for them to merge manually → action=\"command\"\n ③ do not merge for now → action=\"keep\"\n(custom user input beyond these options is fine too — relay it faithfully)",
|
|
732
819
|
"report.needsHumanNoAcceptance": "⚠ **Product acceptance did NOT complete this run** (the pipeline stopped early for human intervention; the acceptance stage never ran) — resolve the human-intervention items first and do NOT merge main on this basis.",
|
|
733
820
|
"report.knownIssuesNoMerge": "⚠ **This run's acceptance was the read-only \"known issues\" mode** (a delivery-level view produced after the QA rework limit was hit): ACCEPTANCE.md and the open-item list exist, but the outcome is **forced to needs-human** — decide on the open blocking defects first and do **NOT** merge main on this basis; afterwards you can `teamflow_resume` to fix them or start a new requirement.",
|
|
734
821
|
"report.stashHint": "🧩 Changes stashed before the run are still in the stash: run git stash pop after the pipeline finishes to restore them",
|
|
735
822
|
"report.tabHint": "The user can open the \"🏭 Team workbench\" tab to see stage lanes, the drag-and-drop board and token details.",
|
|
736
823
|
"report.next": "To continue: claim defects (teamflow_claim), move items manually (teamflow_update), or resume from a checkpoint (teamflow_resume).",
|
|
824
|
+
"report.nextCancelled": "This run stopped at the cancellation point and will **not** auto-resume (the host has no auto-resume mechanism). To continue, ask the **user** to click \"↻ Resume from checkpoint\" in the workbench — stopping was a human decision, so do not call teamflow_resume on your own.",
|
|
825
|
+
"report.cancelSource": "Stop requested by: {source}",
|
|
826
|
+
"report.assumptions": "⚠️ **This run started on the following assumptions** (decisions the agent made for you; if any is wrong, say so and the PRD can be rerun from the checkpoint):\n{list}",
|
|
827
|
+
"log.triageBlockersDropped": "Triage proposed {n} unqualified \"open question(s)\" (missing competing readings / impact / rework) — dropped (the qualifying bar lives in the host)",
|
|
828
|
+
"log.prdAssumptions": "PRD assumptions/open-questions section recorded ({n} line(s)) — surfaced to the user in the completion report",
|
|
829
|
+
"log.prdAssumptionsMissing": "PRD did not provide an assumptions/open-questions section (contract not honored: nobody can tell whether this PRD is what the user wanted; logged, not blocking)",
|
|
830
|
+
"log.modeUpgraded": "Architecture guardrail: the caller picked tier \"{from}\" but triage judged \"{to}\" → upgraded (ADR-0006: architectural requirements must not run on a lightweight tier)",
|
|
831
|
+
"log.triagePreflightFail": "Pre-flight triage failed ({msg}) → handed to the pipeline to decide again (not silently degraded)",
|
|
832
|
+
"log.clarifyAbort": "Clarification gate (pipeline fallback): intent \"{intent}\" / {n} must-know gap(s) → **no stage was started**, waiting for the user",
|
|
833
|
+
"log.clarifyBlocker": "Needs clarification: {q} (changes: {changes}; cost if guessed wrong: {rework})",
|
|
834
|
+
"run.needsClarification": "The requirement is not settled yet (intent: {intent}, {n} must-know gap(s)) — this run started no stage and can be resumed. The main thread should clarify with the user first, then start again with the answers in requirementSupplement.",
|
|
835
|
+
"log.clarifyProceedWithAssumptions": "Already clarified once but {n} must-know gap(s) remain → starting anyway (convergence rule: stop re-asking; the assumptions go into the PRD \"Assumptions & open questions\" section and are highlighted in the report)",
|
|
836
|
+
"log.triageResumed": "Resumed from checkpoint: keeping tier \"{mode}\" (resume does not re-run triage — no wasted call, no tier drift)",
|
|
837
|
+
"log.artifactContract": "Deliverable-shape contracts: judged shape \"{kind}\" → {n} mandatory AC(s) handed to the PRD (shape contracts, to prevent \"looks complete but cannot be installed\")",
|
|
838
|
+
"log.hostResearch": "Target host is not this repo's host (shape \"{kind}\" × host \"{host}\") → this host's contracts are NOT applied; the PRD must instead carry a \"Host contract research\" section (hard gate)",
|
|
839
|
+
"log.hostResearchOk": "Host contract research produced ({n} item(s)) → stored in journal.hostContract; later stages follow that host's own contract",
|
|
840
|
+
"log.hostResearchMissing": "PRD is missing the \"Host contract research\" section (this run targets another host framework — not researching its loading contract first is a gamble) → stage failed, retrying",
|
|
841
|
+
"log.installEnv": "This machine's install environment (probed at run start): profile \"{profile}\" · dir {dir} · dsh on PATH: {cli} (no hardcoded paths — varies per user environment)",
|
|
842
|
+
"log.installEnvUnknown": "Install environment probe FAILED (profile \"{profile}\" · dir {dir}) → the PRD will require the main agent to ASK THE USER for the profile location; never invent a path",
|
|
843
|
+
"cancelSource.ui": "workbench button (human click)",
|
|
844
|
+
"cancelSource.tool": "model tool teamflow_cancel",
|
|
845
|
+
"cancelSource.unknown": "not recorded (legacy run)",
|
|
846
|
+
"log.cancelRequested": "Stop request received (source: {source}) — takes effect at the next stage boundary, then finalizes as cancelled",
|
|
737
847
|
"report.relay": "If the user is present, relay the key points above briefly; if unattended, just record them — no long reply needed.",
|
|
738
848
|
"report.notice": "Team R&D pipeline {status} (runId={id})",
|
|
739
849
|
"report.status.completed": "✅ Completed",
|
|
@@ -763,6 +873,7 @@ const PIPELINE_DICT = {
|
|
|
763
873
|
"state.acIndex": "- AC index ({n}): {list}",
|
|
764
874
|
"state.modules": "- Modules ({n}): {list}",
|
|
765
875
|
"state.verifyScripts": "- Verify scripts: {list}",
|
|
876
|
+
"state.artifactContracts": "- Deliverable-shape contracts (shape: {kind}, {n} item(s)) — each is an **executable criterion**; QA/acceptance must verify every one and record command+result in the artifact. Anything impossible in this environment (e.g. requires a host restart) MUST go into the manual-test checklist — never silently skip:\n{list}",
|
|
766
877
|
"state.prdSummary": "- PRD summary: {summary}",
|
|
767
878
|
"state.techSummary": "- Technical design summary: {summary}",
|
|
768
879
|
"state.qaSummary": "- QA summary: {summary}",
|
|
@@ -828,6 +939,7 @@ const PIPELINE_DICT = {
|
|
|
828
939
|
"doc.archDeviationQ": "\"deviates from blueprint / duplicated impl / should-have-extracted\"",
|
|
829
940
|
"doc.engConstraints": "Engineering constraints",
|
|
830
941
|
"doc.finalCommit": "one consolidated commit",
|
|
942
|
+
"doc.assumptionsQ": "Assumptions & open questions",
|
|
831
943
|
"doc.noRevisionTable": "**No revision table, no version fields like \"Version: vX.Y / Status: in progress\"**",
|
|
832
944
|
"qa.fixSummaryQ": "\"truth judgment + fix\" or \"false-positive evidence\"",
|
|
833
945
|
"doc.statusChain": "todo -> in progress -> testable -> testing -> pending acceptance -> accepted",
|
|
@@ -855,7 +967,14 @@ const TOOLS_DICT = {
|
|
|
855
967
|
zh: {
|
|
856
968
|
"tool.replyLang": "【回复语言】用中文回复用户(本会话的界面语言是中文)。",
|
|
857
969
|
"tool.start.decision": "【分支决策】{question}\n{options}\n(也可自定义输入)——请询问用户选择,确认后把所选选项的 value 作为 branchPolicy 重新调用 teamflow_start(如 'new'/'keep';脏工作区选项可拆为 branchPolicy + preAction 组合),自定义分支名则传 branchName。",
|
|
858
|
-
"tool.start.
|
|
970
|
+
"tool.start.gitDecision": "【改动存档决策】{question}\n{options}\n——**建议用 ask_user_question 呈现**(把选项转成点选项,另加\"其他\"兜底),问句用自然语言(开启=这次运行的改动会单独存档、可撤销、能看清改了什么;不开启=改动直接写入文件夹,之后无法一键撤销)。确认后把所选 value 重新调用 teamflow_start(init → branchPolicy=\"new\" + preAction=\"init\";keep → branchPolicy=\"keep\" + preAction=\"keep-nogit\")。",
|
|
971
|
+
"tool.start.gitDecisionNote": "{question}\n{options}\n{note}",
|
|
972
|
+
"tool.start.gitDecisionDanger": "【改动存档决策】{question}\n{options}\n——**建议用 ask_user_question 呈现**(点选项 + \"其他\"兜底)。请向用户说明原因(这个位置太靠根/太特殊,开启存档会波及大量无关文件,已被系统禁止),并建议把项目放进一个单独的文件夹后重新运行。确认后把所选 value(keep → branchPolicy=\"keep\" + preAction=\"keep-nogit\")重新调用 teamflow_start。",
|
|
973
|
+
"tool.start.needsConfirm": "【需求确认】{question}\n{note}——**建议用 ask_user_question 呈现**(点选项 + \"其他\"兜底),请按此询问用户后再决定。",
|
|
974
|
+
"tool.start.needsClarification": "【需求澄清】这条消息还不足以开工(意图:{intent})——**没有启动流水线**,也没有创建任何 run。请把每个 blocker 作为**一题 ask_user_question** 问用户(题干=question,选项=readings 里的互斥读法 + \"其他\"兜底),拿到全部答复后**保留原始 requirement 不变**、把答复放进 requirementSupplement 重新调用 teamflow_start。不要自行替用户假设后直接重调(这正是要拦的行为)。",
|
|
975
|
+
"tool.start.blockerReadings": "两种读法",
|
|
976
|
+
"tool.start.blockerChanges": "影响面",
|
|
977
|
+
"tool.start.blockerRework": "猜错的代价",
|
|
859
978
|
"tool.start.started": "团队研发流水线已启动(runId={runId},{status}),正在后台执行。【重要】你现在停手:不要自行读取/修改代码实现该需求,不要重复跑测试验证——实现、QA、汇报由流水线各阶段完成。你只需告知用户流水线已启动,等待流水线完成后的官方完成汇报,再向用户转述结果。可用 teamflow_status 查询进度/阶段 token;backlog 已持久化到 $DSH_HOME/teamflow。\n【回复语言】用中文回复用户。",
|
|
860
979
|
"tool.start.paused": "当前会话已暂停 teamflow。如需恢复,调用 teamflow_resume_session;或直接写代码。",
|
|
861
980
|
"tool.start.noTeam": "请先通过输入框旁的 🏭 按钮选择团队,再发送需求消息。未选团队时不走 teamflow。",
|
|
@@ -877,6 +996,13 @@ const TOOLS_DICT = {
|
|
|
877
996
|
"branch.opt.stashNewChild": "stash 现有改动后新建子分支开发",
|
|
878
997
|
"branch.opt.commitNewChild": "提交现有改动后新建子分支开发",
|
|
879
998
|
"branch.decisionNote": "选项之外可自定义输入(如指定分支名)。确认选择后,请以 teamflow_start 的 branchPolicy(回传所选选项 value,如 new/keep)与 branchName/preAction/commitMessage 参数重新调用本工具。",
|
|
999
|
+
"git.q.noRepo": "这个文件夹({path})还没有开启「改动存档」。开启后:本次运行的改动会单独存成一档,随时可以整体撤销,也能清楚看到这次改了什么;不开启:改动会直接写入文件夹,之后无法一键撤销。要开启吗?",
|
|
1000
|
+
"git.opt.init": "开启改动存档(推荐)——将初始化版本档案{baseline},然后开始运行",
|
|
1001
|
+
"git.opt.initBaseline": ",并把文件夹里现有的 {n} 个文件记录为初始状态",
|
|
1002
|
+
"git.opt.initNoBaseline": "(该文件夹内容较多,只开启存档、不记录现有内容为初始状态)",
|
|
1003
|
+
"git.opt.keep": "不开启,直接修改(本次运行的改动将无法一键撤销,也不会记录改了什么)",
|
|
1004
|
+
"git.q.danger": "这个位置({path})不适合开启「改动存档」——它太靠近磁盘根目录或系统目录,开启会波及大量与本项目无关的文件,已被系统禁止。",
|
|
1005
|
+
"git.opt.keepOnly": "不开启,直接修改(建议:把项目放进一个单独的文件夹后重新运行,即可开启存档)",
|
|
880
1006
|
"tool.status.reminder": "流水线仍在后台执行:不要自行改代码实现该需求或重复跑验证,等待完成汇报。",
|
|
881
1007
|
"tool.merge.command": "请用户在项目目录执行以下命令完成合回(合回后可 git branch -d {branch} 清理特性分支):\ngit checkout main && git merge --no-ff {branch}",
|
|
882
1008
|
"tool.merge.kept": "已标记暂不合回:特性分支 {branch} 保留,后续可随时调用 teamflow_merge 合回",
|
|
@@ -893,7 +1019,7 @@ const TOOLS_DICT = {
|
|
|
893
1019
|
"tool.cancel.fail": "取消失败",
|
|
894
1020
|
"tool.resume.ok": "流水线 {runId} 已从断点「{phase}」续跑",
|
|
895
1021
|
"tool.resume.fail": "续跑失败:{error}",
|
|
896
|
-
"tool.ctx.team": "[TeamFlow 上下文] 用户已选择「{icon} {name}」团队。只有收到明确的开发需求(新功能/迭代/重构/bug修复/代码改动请求)时才调用 teamflow_start 并指定 teamId=\"{teamId}\",requirement 参数忠实转写用户原话即可(不要自行扩写、不要臆造文件路径或技术细节)。收到反馈、讨论、闲聊、UI 意见等非开发请求时,不要调用 teamflow_start
|
|
1022
|
+
"tool.ctx.team": "[TeamFlow 上下文] 用户已选择「{icon} {name}」团队。只有收到明确的开发需求(新功能/迭代/重构/bug修复/代码改动请求)时才调用 teamflow_start 并指定 teamId=\"{teamId}\",requirement 参数忠实转写用户原话即可(不要自行扩写、不要臆造文件路径或技术细节)。收到反馈、讨论、闲聊、UI 意见等非开发请求时,不要调用 teamflow_start,直接正常回复。【需求不明确就先澄清,不要抢跑】需求只是探索/笼统时(如「我想做个 X」「帮我搞点 Y」),先跟用户对齐:给 2–4 个方向候选(每个一句话说明它做什么、什么场合用),只问**无法自查、答错就要返工**的关键点;能从代码/文档自己查到的不问。【澄清完必须回到流水线】对齐后**务必**调用 teamflow_start 开工:requirement 保留用户原话,澄清得到的补充说明放进 requirementSupplement——不要聊完就停在对话里不动手。若 teamflow_start 返回 needs-clarification,按它列出的 blockers 继续问用户,再把答复放进 requirementSupplement 重新调用;**不要自行替用户假设后直接重调**。调用 teamflow_start 之后:流水线在后台执行,你不要再自行读取/修改代码实现该需求,也不要重复跑测试验证——只需告知用户流水线已启动,等待流水线的完成汇报后再答复用户。【回复语言】用中文回复用户(本会话界面语言为中文)。",
|
|
897
1023
|
"err.tool.missingKindId": "缺少 kind/id",
|
|
898
1024
|
"err.tool.missingKindIdTo": "缺少 kind/id/to",
|
|
899
1025
|
"err.tool.missingAssign": "缺少 kind/id/role/assignee",
|
|
@@ -932,7 +1058,14 @@ const TOOLS_DICT = {
|
|
|
932
1058
|
en: {
|
|
933
1059
|
"tool.replyLang": "[Reply language] Reply to the user in English (this session is running with the English UI language).",
|
|
934
1060
|
"tool.start.decision": "[Branch decision] {question}\n{options}\n(custom input is allowed too) — ask the user to choose, then call teamflow_start again passing the chosen option value as branchPolicy (e.g. 'new'/'keep'; dirty-workspace options can be split into branchPolicy + preAction), or pass branchName for a custom branch name.",
|
|
935
|
-
"tool.start.
|
|
1061
|
+
"tool.start.gitDecision": "[Change-archiving decision] {question}\n{options}\n— **present via ask_user_question** (turn the options into clickable choices plus an \"Other\" fallback), phrased in plain words (enabling = this run's changes are archived separately, can be undone as a whole, and you can see exactly what changed; not enabling = changes are written straight into the folder and cannot be undone in one step later). Then RE-CALL teamflow_start with the chosen value (init → branchPolicy=\"new\" + preAction=\"init\"; keep → branchPolicy=\"keep\" + preAction=\"keep-nogit\").",
|
|
1062
|
+
"tool.start.gitDecisionNote": "{question}\n{options}\n{note}",
|
|
1063
|
+
"tool.start.gitDecisionDanger": "[Change-archiving decision] {question}\n{options}\n— **present via ask_user_question** (clickable choices + \"Other\" fallback). Explain the reason (this location is too close to the disk root / a special directory; archiving here would sweep in many unrelated files and has been disallowed) and suggest putting the project in its own folder. Then RE-CALL teamflow_start with the chosen value (keep → branchPolicy=\"keep\" + preAction=\"keep-nogit\").",
|
|
1064
|
+
"tool.start.needsConfirm": "[Requirement confirmation] {question}\n{note} — **present via ask_user_question** (clickable choices + \"Other\" fallback), then decide accordingly.",
|
|
1065
|
+
"tool.start.needsClarification": "[Requirement clarification] This message is not settled enough to start (intent: {intent}) — **no pipeline was started and no run was created**. Ask each blocker as **one ask_user_question** (stem = question, choices = the competing readings + an \"Other\" fallback); once all answers are in, RE-CALL teamflow_start keeping the original requirement unchanged and putting the user's answers into requirementSupplement. Do not just assume on the user's behalf and re-call — that is exactly what this gate blocks.",
|
|
1066
|
+
"tool.start.blockerReadings": "Competing readings",
|
|
1067
|
+
"tool.start.blockerChanges": "What it changes",
|
|
1068
|
+
"tool.start.blockerRework": "Cost if guessed wrong",
|
|
936
1069
|
"tool.start.started": "The team R&D pipeline has started (runId={runId}, {status}) and runs in the background. [IMPORTANT] Stop now: do not read or modify code to implement this requirement yourself, and do not rerun tests for verification — implementation, QA and reporting are handled by the pipeline stages. Just tell the user the pipeline has started, wait for the official completion report, and relay its result. Use teamflow_status to check progress/stage tokens; the backlog is persisted under $DSH_HOME/teamflow.\n[Reply language] Reply to the user in English.",
|
|
937
1070
|
"tool.start.paused": "teamflow is paused for the current session. Call teamflow_resume_session to resume, or just write the code directly.",
|
|
938
1071
|
"tool.start.noTeam": "Pick a team via the 🏭 button next to the input box before sending the requirement. Without a team, teamflow is not used.",
|
|
@@ -954,6 +1087,13 @@ const TOOLS_DICT = {
|
|
|
954
1087
|
"branch.opt.stashNewChild": "Stash the current changes and create a child branch",
|
|
955
1088
|
"branch.opt.commitNewChild": "Commit the current changes and create a child branch",
|
|
956
1089
|
"branch.decisionNote": "Custom input beyond these options is allowed (e.g. a specific branch name). After the user confirms, call this tool again with teamflow_start's branchPolicy (pass back the chosen option value, e.g. new/keep) plus branchName/preAction/commitMessage.",
|
|
1090
|
+
"git.q.noRepo": "This folder ({path}) does not have change archiving enabled yet. With it on: this run's changes are archived separately, can be undone as a whole at any time, and you can see exactly what changed. Without it: changes are written straight into the folder and cannot be undone in one step later. Enable it?",
|
|
1091
|
+
"git.opt.init": "Enable change archiving (recommended) — the version archive will be initialized{baseline}, then the run starts",
|
|
1092
|
+
"git.opt.initBaseline": ", with the {n} existing files recorded as the initial state",
|
|
1093
|
+
"git.opt.initNoBaseline": " (this folder has many files: archiving is enabled but existing content is NOT recorded as the initial state)",
|
|
1094
|
+
"git.opt.keep": "Do not enable — modify directly (this run's changes cannot be undone in one step and will not be recorded)",
|
|
1095
|
+
"git.q.danger": "This location ({path}) is not suitable for change archiving — it is too close to the disk root or a system directory; enabling it would sweep in many unrelated files and has been disallowed.",
|
|
1096
|
+
"git.opt.keepOnly": "Do not enable — modify directly (suggestion: put the project in its own folder and run again, then archiving can be enabled)",
|
|
957
1097
|
"tool.status.reminder": "The pipeline is still running in the background: do not modify code for this requirement or rerun verification yourself; wait for the completion report.",
|
|
958
1098
|
"tool.merge.command": "Ask the user to run the following command in the project directory to complete the merge (afterwards they can delete the feature branch with git branch -d {branch}):\ngit checkout main && git merge --no-ff {branch}",
|
|
959
1099
|
"tool.merge.kept": "Marked as not merged for now: feature branch {branch} is kept and teamflow_merge can be called later",
|
|
@@ -970,7 +1110,7 @@ const TOOLS_DICT = {
|
|
|
970
1110
|
"tool.cancel.fail": "Cancel failed",
|
|
971
1111
|
"tool.resume.ok": "Pipeline {runId} resumed from checkpoint \"{phase}\"",
|
|
972
1112
|
"tool.resume.fail": "Resume failed: {error}",
|
|
973
|
-
"tool.ctx.team": "[TeamFlow context] The user selected the \"{icon} {name}\" team. Call teamflow_start with teamId=\"{teamId}\" only for a clear development requirement (new feature / iteration / refactor / bug fix / code change request), and transcribe the user's own words faithfully into the requirement argument (do not expand it, do not invent file paths or technical details). For feedback, discussion, small talk or UI opinions, do not call teamflow_start — just reply normally. After calling teamflow_start the pipeline runs in the background: do not read or modify code for this requirement and do not rerun verification yourself — just tell the user the pipeline has started and answer them after the pipeline reports completion. [Reply language] Reply to the user in English (this session uses the English UI language).",
|
|
1113
|
+
"tool.ctx.team": "[TeamFlow context] The user selected the \"{icon} {name}\" team. Call teamflow_start with teamId=\"{teamId}\" only for a clear development requirement (new feature / iteration / refactor / bug fix / code change request), and transcribe the user's own words faithfully into the requirement argument (do not expand it, do not invent file paths or technical details). For feedback, discussion, small talk or UI opinions, do not call teamflow_start — just reply normally. [Clarify first, do not jump the gun] When the requirement is exploratory or vague (\"I want to build some kind of X\", \"help me make a Y\"), align with the user first: offer 2-4 concrete direction options (one line each on what it does and when it is used) and ask only the must-know points you cannot check yourself and whose wrong guess would cause rework; do not ask what you can find in the code or docs. [After clarifying, come back to the pipeline] Once aligned you MUST call teamflow_start to start the work: keep the user's own words in requirement and put the clarifications into requirementSupplement — do not stop at the conversation. If teamflow_start returns needs-clarification, keep asking the user about the blockers it lists and re-call with requirementSupplement; do not silently assume on the user's behalf. After calling teamflow_start the pipeline runs in the background: do not read or modify code for this requirement and do not rerun verification yourself — just tell the user the pipeline has started and answer them after the pipeline reports completion. [Reply language] Reply to the user in English (this session uses the English UI language).",
|
|
974
1114
|
"err.tool.missingKindId": "missing kind/id",
|
|
975
1115
|
"err.tool.missingKindIdTo": "missing kind/id/to",
|
|
976
1116
|
"err.tool.missingAssign": "missing kind/id/role/assignee",
|
|
@@ -1097,6 +1237,94 @@ function extractText(blocks) {
|
|
|
1097
1237
|
if (!Array.isArray(blocks)) return "";
|
|
1098
1238
|
return blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
1099
1239
|
}
|
|
1240
|
+
/** 从 `ctx.baseUrl`(宿主设为**当前 profile 目录**的 file:// URL)解析 profile。
|
|
1241
|
+
* 形态要求 `.../profiles/<name>/`(容忍尾斜杠与 cordis.yml 之类的文件名段);非该形态返回 null。
|
|
1242
|
+
* 纯函数:`file://` 解码走 `decodeURIComponent`(路径含空格/中文时 URL 是编码过的)。 */
|
|
1243
|
+
function profileDirFromBaseUrl(baseUrl) {
|
|
1244
|
+
const raw = String(baseUrl || "");
|
|
1245
|
+
if (!raw) return null;
|
|
1246
|
+
let p = raw;
|
|
1247
|
+
if (/^file:\/\//i.test(p)) {
|
|
1248
|
+
try {
|
|
1249
|
+
p = decodeURIComponent(new URL(p).pathname);
|
|
1250
|
+
} catch (e) {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
if (/^\/[a-zA-Z]:/.test(p)) p = p.slice(1);
|
|
1254
|
+
}
|
|
1255
|
+
const norm = p.replace(/\\/g, "/");
|
|
1256
|
+
const segs = norm.split("/").filter(Boolean);
|
|
1257
|
+
let i = segs.lastIndexOf("profiles");
|
|
1258
|
+
if (i < 0 || i + 1 >= segs.length) return null;
|
|
1259
|
+
let end = segs.length;
|
|
1260
|
+
if (end - i > 2 && /\.(ya?ml|json)$/i.test(segs[end - 1])) end -= 1;
|
|
1261
|
+
if (end !== i + 2) return null;
|
|
1262
|
+
const name = segs[i + 1];
|
|
1263
|
+
if (!name) return null;
|
|
1264
|
+
const isPosix = norm.startsWith("/");
|
|
1265
|
+
const sep = isPosix ? "/" : "\\";
|
|
1266
|
+
return {
|
|
1267
|
+
home: (isPosix ? "/" : "") + segs.slice(0, i).join(sep),
|
|
1268
|
+
profile: name,
|
|
1269
|
+
dir: (isPosix ? "/" : "") + segs.slice(0, i + 2).join(sep)
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
/** 从插件模块路径反推 `$DSH_HOME/profiles/<name>`(纯函数,门禁可测)。
|
|
1273
|
+
* 接受 `.../profiles/<name>/node_modules/<pkg>[/lib/host.mjs]` 形态;非该形态返回 null。
|
|
1274
|
+
* ⚠️ 次源:`link:`/junction 安装下 `import.meta.url` 会解析到真实目标(源码目录)→ 返回 null,
|
|
1275
|
+
* 此时靠 {@link profileDirFromBaseUrl}(宿主给的权威锚点)兜住。 */
|
|
1276
|
+
function profileFromModulePath(modulePath) {
|
|
1277
|
+
const raw = String(modulePath || "");
|
|
1278
|
+
if (!raw) return null;
|
|
1279
|
+
const norm = raw.replace(/\\/g, "/");
|
|
1280
|
+
const segs = norm.split("/").filter(Boolean);
|
|
1281
|
+
const i = segs.lastIndexOf("profiles");
|
|
1282
|
+
if (i < 0 || i + 2 >= segs.length) return null;
|
|
1283
|
+
const name = segs[i + 1];
|
|
1284
|
+
if (segs[i + 2] !== "node_modules" || !name) return null;
|
|
1285
|
+
const sep = norm.startsWith("/") ? "/" : "\\";
|
|
1286
|
+
const homeBody = segs.slice(0, i).join(sep);
|
|
1287
|
+
const home = (norm.startsWith("/") ? "/" : "") + homeBody;
|
|
1288
|
+
return {
|
|
1289
|
+
home,
|
|
1290
|
+
profile: name,
|
|
1291
|
+
dir: [
|
|
1292
|
+
home,
|
|
1293
|
+
"profiles",
|
|
1294
|
+
name
|
|
1295
|
+
].join(sep).replace(new RegExp(`\\${sep === "/" ? "/" : "\\\\"}+`, "g"), sep)
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
/** 探测本机安装环境。取值顺序(**主源是宿主权威锚点,不是环境变量**):
|
|
1299
|
+
* ① `baseUrl`(`ctx.baseUrl` = profile 目录)→ profile 名/目录/home 全齐,且不受 junction 影响;
|
|
1300
|
+
* ② `modulePath`(插件自身文件路径)反推 —— `link:` 安装下会落空,作为次源;
|
|
1301
|
+
* ③ `hintHome`(调用方给 `dshHome()`,**已有 `~/.dsh` 兜底**)只补 home;
|
|
1302
|
+
* `ok` = **profile 名已知 且 目录为绝对路径**。刻意**不要求 `DSH_HOME`**:它不是"装了 dsh 就自带"
|
|
1303
|
+
* (可选覆盖变量、`.env` 也设不了),默认安装下为 undefined,纳入判据会**误判失败**并把 PRD
|
|
1304
|
+
* 降级成"问用户"。要求绝对路径是因为相对目录照做不了(命令要在任意 cwd 下可执行)。
|
|
1305
|
+
* 纯函数(不碰进程/PATH;`hasCli` 由调用方探测)。 */
|
|
1306
|
+
function detectInstallEnv(opts) {
|
|
1307
|
+
const hintHome = String(opts.dshHome || "").replace(/[\\/]+$/, "");
|
|
1308
|
+
const fromUrl = profileDirFromBaseUrl(opts.baseUrl);
|
|
1309
|
+
const fromPath = profileFromModulePath(opts.modulePath || "");
|
|
1310
|
+
const src = fromUrl || fromPath;
|
|
1311
|
+
const profile = src?.profile || "";
|
|
1312
|
+
const home = src?.home || hintHome;
|
|
1313
|
+
const sep = home.includes("/") && !home.includes("\\") ? "/" : "\\";
|
|
1314
|
+
const profileDir = src?.dir || (profile && home ? `${home}${sep}profiles${sep}${profile}` : "");
|
|
1315
|
+
return {
|
|
1316
|
+
dshHome: home,
|
|
1317
|
+
profile,
|
|
1318
|
+
profileDir,
|
|
1319
|
+
cliOnPath: opts.hasCli === true,
|
|
1320
|
+
ok: !!profile && isAbsolutePath(profileDir)
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/** 绝对路径判据(跨平台,纯字符串):POSIX `/x`、Windows `C:\x`/`C:/x`、UNC `\\srv\share`。 */
|
|
1324
|
+
function isAbsolutePath(p) {
|
|
1325
|
+
const s = String(p || "");
|
|
1326
|
+
return /^\//.test(s) || /^[a-zA-Z]:[\\/]/.test(s) || /^\\\\/.test(s);
|
|
1327
|
+
}
|
|
1100
1328
|
/** 从 dev/qaFix 回复中提取「验证证据」块(`[Verification evidence]` 行起,到 state 块/结尾止)。
|
|
1101
1329
|
* dev 阶段无独立对抗校验(QA 有 QA-REPORT.md 结构化证据,dev 只有自述)——证据块是「可审计的
|
|
1102
1330
|
* 具体自述」:命令+退出码+断言计数+失败行引用,可对照 logs/teamflow/<runId>/ 命令输出日志核实;
|
|
@@ -1308,6 +1536,46 @@ function judgeDeliverable(phase, text) {
|
|
|
1308
1536
|
length
|
|
1309
1537
|
};
|
|
1310
1538
|
}
|
|
1539
|
+
const DOC_STAGE_FILES = {
|
|
1540
|
+
prd: ["PRD.md", "TECH-CHANGE.md"],
|
|
1541
|
+
design: ["DESIGN.md"],
|
|
1542
|
+
tech: ["TECHNICAL.md", "TECH-CHANGE.md"],
|
|
1543
|
+
qa: ["QA-REPORT.md"],
|
|
1544
|
+
acceptance: ["ACCEPTANCE.md"]
|
|
1545
|
+
};
|
|
1546
|
+
/** 任务夹产物读取(单轨契约:文件即产物——QA/验收 host 只读文件,回复仅摘要)。
|
|
1547
|
+
* 缺失/空/读取异常返回 null(调用方决定硬失败或 journal 兜底)。`pipeline` 与 `runner` 共用。 */
|
|
1548
|
+
function artifactText(journal, fileName) {
|
|
1549
|
+
const path = journal && journal.workspacePath && journal.runDocs ? `${journal.workspacePath}/${journal.runDocs}/${fileName}` : null;
|
|
1550
|
+
if (!path) return null;
|
|
1551
|
+
try {
|
|
1552
|
+
if (!existsSync(path)) return null;
|
|
1553
|
+
const t = readFileSync(path, "utf8").trim();
|
|
1554
|
+
return t ? t : null;
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* doc 类阶段的产物兜底:按 `DOC_STAGE_FILES` 取**最长**的一个候选文件(同一阶段可能有多种产物形态,
|
|
1561
|
+
* 如 tech 档 PRD 写 `TECH-CHANGE.md`);无候选/文件缺失/未达下限 → null(绝不抛)。
|
|
1562
|
+
*/
|
|
1563
|
+
function stageDocText(journal, phase) {
|
|
1564
|
+
const names = DOC_STAGE_FILES[phase];
|
|
1565
|
+
if (!names || !names.length) return null;
|
|
1566
|
+
let best = null;
|
|
1567
|
+
for (const name of names) {
|
|
1568
|
+
const text = artifactText(journal, name);
|
|
1569
|
+
if (!text) continue;
|
|
1570
|
+
const length = text.trim().length;
|
|
1571
|
+
if (!best || length > best.length) best = {
|
|
1572
|
+
name,
|
|
1573
|
+
text,
|
|
1574
|
+
length
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
return best;
|
|
1578
|
+
}
|
|
1311
1579
|
/** 归一化:去 markdown 装饰、压平空白、小写(仅用于身份比较,不改原值)。 */
|
|
1312
1580
|
function fpNorm(s) {
|
|
1313
1581
|
return String(s === null || s === void 0 ? "" : s).replace(/[`*]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
@@ -1400,6 +1668,70 @@ function refusalHit(text) {
|
|
|
1400
1668
|
context: s.slice(start, end).replace(/\s+/g, " ").trim()
|
|
1401
1669
|
};
|
|
1402
1670
|
}
|
|
1671
|
+
/**
|
|
1672
|
+
* 外部供应商不可用 vs 内容性失败(2026-09-17,实测驱动)。
|
|
1673
|
+
*
|
|
1674
|
+
* 背景:dddd 那条 run 在第一轮 01:37 连续 6 个 dev 阶段 `stopReason=error` → 快速重试两次 → 转人工;
|
|
1675
|
+
* 而**16 分钟后(01:53)同样的请求就成功了**——即那批失败是「供应商在一段时间窗内不可用」(限流/无额度/
|
|
1676
|
+
* 上游 5xx/超时),**与阶段内容无关**。旧行为把它当内容失败:快速失败、落 `failed + human`、让人误以为
|
|
1677
|
+
* 交付有问题。现在按错误文本分类(⚠️ **启发式**:宿主只给 `stopReason=error` + 错误文本,没有结构化错误码,
|
|
1678
|
+
* 故命中原文必须记日志以便日后核对):
|
|
1679
|
+
* - `external`:限流/额度/余额/上游不可用/超时/过载 → 走**长退避重试**;用尽仍失败 → 落可续跑中断态;
|
|
1680
|
+
* - `content`:上下文耗尽/护栏/产出无效等 → 维持现状(不自动重试或按既有策略);
|
|
1681
|
+
* - `unknown`:未命中任何词 → 维持现状(按内容类处置,宁严勿松)。
|
|
1682
|
+
* `hint` 为可选的 outcome/summary 文本,一并参与匹配(错误细节可能在 stage.summary 里)。
|
|
1683
|
+
*/
|
|
1684
|
+
function classifyExternalFailure(text, hint) {
|
|
1685
|
+
const s = `${String(text || "")} ${String(hint || "")}`;
|
|
1686
|
+
if (!s.trim()) return "unknown";
|
|
1687
|
+
if (/(?:\b429\b|\b402\b|rate[ _-]?limit|too many requests|insufficient[ _-]?(?:balance|quota|funds)|quota|exceeded[ _-]?(?:your[ _-]?)?(?:quota|limit|rate)|no[ _-]?(?:available[ _-]?)?(?:quota|balance|credit)|out of credit|billing|payment required|overload|temporarily unavailable|service unavailable|\b50[234]\b|upstream|gateway timeout|\b52[0-9]\b|timeout|timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|socket hang up|fetch failed|network error|限流|限速|频率限制|请求过于频繁|额度|配额|余额不足|欠费|无额度|暂时不可用|服务不可用|上游不可用|超时|网络错误|连接被重置|过载|供应商)/i.test(s)) return "external";
|
|
1688
|
+
if (/context[ _-]?(?:window|length)|too many tokens|maximum context|prompt is too long|上下文(?:长度|超限|耗尽)|护栏|degenerated|stalled|insufficient output|too short|产出过短/i.test(s)) return "content";
|
|
1689
|
+
return "unknown";
|
|
1690
|
+
}
|
|
1691
|
+
/** 外部故障退避序列(毫秒):30s → 60s → 120s → 240s(总等待上限 ≈ 7.5 分钟)。
|
|
1692
|
+
* 取值依据:dddd 实测窗口 ≈16 分钟(01:37 挂 → 01:53 恢复),故退避要能跨过几分钟的短窗口,
|
|
1693
|
+
* 又不至于让一条 run 无限期挂着(用尽后落**可续跑中断态**,窗口恢复后 resume 只补这一段)。 */
|
|
1694
|
+
const EXTERNAL_BACKOFF_MS = [
|
|
1695
|
+
3e4,
|
|
1696
|
+
6e4,
|
|
1697
|
+
12e4,
|
|
1698
|
+
24e4
|
|
1699
|
+
];
|
|
1700
|
+
/** 取第 n 次外部故障退避时长(n 从 1 起;超出序列 → null 表示退避用尽)。 */
|
|
1701
|
+
function externalBackoffMs(n) {
|
|
1702
|
+
return EXTERNAL_BACKOFF_MS[n - 1] ?? null;
|
|
1703
|
+
}
|
|
1704
|
+
/** 工具结果文本(拼接 message.content 的 text 块)——签名与日志共用。 */
|
|
1705
|
+
function toolResultText(data) {
|
|
1706
|
+
try {
|
|
1707
|
+
const content = data?.message?.content;
|
|
1708
|
+
if (!Array.isArray(content)) return "";
|
|
1709
|
+
return content.map((c) => c && typeof c.text === "string" ? c.text : "").join("");
|
|
1710
|
+
} catch (e) {
|
|
1711
|
+
return "";
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* 工具结果是否**失败**。首选结构化 `message.isError`(宿主 `tool/result` 自带,权威、不猜文本——
|
|
1716
|
+
* `packages/core/tools` 的 ToolExecutionResult);缺该字段(老宿主/裁剪事件)时按 `Error`/`failed`
|
|
1717
|
+
* 前缀兜底,宁严勿松:只认明确失败。
|
|
1718
|
+
*/
|
|
1719
|
+
function isToolErrorResult(data) {
|
|
1720
|
+
const msg = data?.message;
|
|
1721
|
+
if (msg && typeof msg.isError === "boolean") return msg.isError;
|
|
1722
|
+
return /^\s*(?:error|failed|fatal)\b/i.test(toolResultText(data));
|
|
1723
|
+
}
|
|
1724
|
+
/** 失败指纹:工具名 + 归一化错误前 160 字符。同一指纹反复出现 = 同一个坏环境(而非模型措辞波动)。 */
|
|
1725
|
+
function toolFailureSignature(tool, text) {
|
|
1726
|
+
return `${String(tool || "").trim().toLowerCase() || "tool"}::${String(text || "").replace(/\s+/g, " ").trim().slice(0, 160)}`;
|
|
1727
|
+
}
|
|
1728
|
+
/** 失败次数 → 护栏动作(纯函数,阈值可测):none = 继续观察;warn = 提醒模型停手;abort = 中止本阶段。 */
|
|
1729
|
+
function toolFailureAction(count, warnAt, abortAt) {
|
|
1730
|
+
const n = typeof count === "number" && Number.isFinite(count) ? count : 0;
|
|
1731
|
+
if (n >= abortAt) return "abort";
|
|
1732
|
+
if (n >= warnAt) return "warn";
|
|
1733
|
+
return "none";
|
|
1734
|
+
}
|
|
1403
1735
|
/** 重试诊断包:上一轮失败详情回灌进重试 prompt(盲试 → 带因重试)。
|
|
1404
1736
|
* 失败分类/详情/护栏原因取自 stage;产出尾部截断 1000 字符供自查修正。
|
|
1405
1737
|
* `locale` 为**尾参可选**(缺省/`zh` → 现状中文**逐字不变**;`en` → 新增英文文案,AC-3④)。
|
|
@@ -1431,16 +1763,25 @@ function buildRetryDiagnostic(attempt, stage, locale) {
|
|
|
1431
1763
|
* 反向护栏(漏报实锤 2026-09-03):模型写「❌ 不通过」但漏写「验收结论:」前缀 → accLine 为空 →
|
|
1432
1764
|
* 旧实现落回默认 accepted(最乐观默认值,质量门禁漏报=假交付)。现改为 **找不到结论行 → needs-human**
|
|
1433
1765
|
* (宁严勿松:误拦截=人工看一眼,误放行=假交付;📝 全文命中与架构红词仍优先于该默认)。
|
|
1766
|
+
* ⚠️ 结论行取值(2026-09-17 实测 bug,`tf-mu4bve7t-duux2k`):旧实现 `find(第一个含「验收结论」的行)` 会被
|
|
1767
|
+
* **章节标题**蒙住 —— 真实产物里 `## 1. 验收结论摘要`(含"验收结论"四字)排在真正的结论行
|
|
1768
|
+
* `验收结论:✅ 通过` 之前 → 抓到标题行 → 四档词全落空 → **明明通过却判 needs-human**。
|
|
1769
|
+
* 现规则:**优先取字面量模板行**(行首 `验收结论:` / `Acceptance verdict:` / `Overall verdict:`,
|
|
1770
|
+
* 允许 `## ` 前缀与列表符),且取**最后一个**(报告末尾的"结论"章才是终判);仍找不到时,回退到
|
|
1771
|
+
* **最后一个非标题、含结论词的行**(避免"只写 ❌ 不通过、漏写前缀"的漏报变体)。
|
|
1434
1772
|
* @param {unknown} text 验收报告全文
|
|
1435
1773
|
* @returns {'accepted'|'rework'|'reject'|'needs-human'}
|
|
1436
1774
|
*/
|
|
1437
1775
|
function parseAcceptanceVerdict(text) {
|
|
1438
1776
|
const acc = String(text || "");
|
|
1439
|
-
const
|
|
1777
|
+
const lines = acc.split("\n");
|
|
1778
|
+
const literal = /^\s*(?:#{1,6}\s*|[-*+]\s*)?(?:验收结论|整体结论|Acceptance verdict|Overall verdict)\s*[::]/i;
|
|
1779
|
+
const literalHits = lines.filter((l) => literal.test(l));
|
|
1780
|
+
const accLine = literalHits.length ? literalHits[literalHits.length - 1].replace(/\|.*/, "").trim() : "";
|
|
1440
1781
|
const hasArchRedFlag = /重复实现|重复适配|偏离蓝图|未按蓝图|该拆未拆|该抽象未抽象|破坏既有结构|结构性.*问题|架构(打回|需重构)|需.*返工|返工.*项.*(存在|仍)|仍.*(返工|重构)/.test(acc);
|
|
1441
1782
|
const archNegated = /无返工|无.*返工|不返工|无架构打回|无.*打回|非漂移|无.*重复|无.*偏离|无.*抽象.*问题|无.*蓝图.*问题|架构一致性.*(PASS|良好|达标|通过|无问题)|M3.*(PASS|通过|达标)|架构.*(达标|无问题|良好)/.test(acc);
|
|
1442
1783
|
if (hasArchRedFlag && !archNegated) return "rework";
|
|
1443
|
-
if (
|
|
1784
|
+
if (acc.split("\n").map((l) => String(l).replace(/^[\s#*\-+>|]+/, "").replace(/^(?:验收结论|整体结论|结论|判定|Acceptance verdict|Overall verdict|Conclusion|Verdict)\s*[::]\s*/i, "")).some((l) => /^📝\s*(?:需求不适用|Not applicable|N\/A)/i.test(l))) return "reject";
|
|
1444
1785
|
if (/不通过|需返工|未通过|\b(?:fail|failed|not pass|not passed)\b/i.test(accLine) && !/无\s*不通过|未发现不通过|未出现不通过|no failed|no failures/i.test(accLine)) {
|
|
1445
1786
|
if (/✅\s*通过|(?<!not\s)\bpass(?:ed)?\b|⚠️\s*conditional\s+pass/i.test(accLine)) return "accepted";
|
|
1446
1787
|
return "rework";
|
|
@@ -1523,6 +1864,302 @@ function extractBlueprint(text, locale) {
|
|
|
1523
1864
|
render: `${en ? "[Architecture blueprint (produced by the tech stage; implement on the existing architecture, do not rebuild)]" : "【架构蓝图(tech 阶段产出,dev 须在既有架构上实现,勿重建)】"}\n${parts.join("\n")}`
|
|
1524
1865
|
};
|
|
1525
1866
|
}
|
|
1867
|
+
/**
|
|
1868
|
+
* 并发池:按 max 个 worker 消费 items,返回**同序**结果。
|
|
1869
|
+
*
|
|
1870
|
+
* `shouldStop`(可选):取下一个任务**之前**判定,返回 true 即停止取新任务(已在飞的照常等它收尾),
|
|
1871
|
+
* 未取到的条目结果保持 `undefined`——**调用方必须容忍空位**(现有调用点一律 `r && …` 过滤)。
|
|
1872
|
+
*
|
|
1873
|
+
* 为什么要有它(2026-09-16 用户实测):取消后被 dispose 的任务返回 null,worker 拿到结果会立刻
|
|
1874
|
+
* `cursor++` 取下一个任务并启动新子代理——用户按了「中断」,界面上却又冒出一个「开发中」
|
|
1875
|
+
* (取消变成了队列补位)。放在 util(无宿主私有依赖)而非 runner,是为了让行为级测试能直接喂 items 断言。
|
|
1876
|
+
*/
|
|
1877
|
+
async function runPool(items, max, fn, shouldStop) {
|
|
1878
|
+
const results = new Array(items.length);
|
|
1879
|
+
let cursor = 0;
|
|
1880
|
+
const workers = Array.from({ length: Math.min(Math.max(1, max), items.length) }, async () => {
|
|
1881
|
+
while (cursor < items.length) {
|
|
1882
|
+
if (shouldStop && shouldStop()) return;
|
|
1883
|
+
const i = cursor++;
|
|
1884
|
+
results[i] = await fn(items[i], i);
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
await Promise.all(workers);
|
|
1888
|
+
return results;
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* 从产物文本里摘出「假设 / 待澄清」小节(2026-09-16 需求澄清闸门 Phase 1)。
|
|
1892
|
+
*
|
|
1893
|
+
* **行式解析而不是单条正则**——实测踩过两个坑(本批真 bug,实锤 run tf-mu34afd2-wcjaw1):
|
|
1894
|
+
* ① 产物标题常带编号/附录前缀(`## 9. 假设与待澄清`;本仓 PRD 惯例就是编号标题),
|
|
1895
|
+
* `^#{1,6}\s*(假设|…)` 这类写法直接匹配不到 → 明明写了段落却误报「契约未兑现」;
|
|
1896
|
+
* ② 正文用 `([\s\S]*?)(?=\n#{1,6}|\s*$)` 懒匹配时,`\s*$` 分支会在标题后立刻命中 → 摘出空串。
|
|
1897
|
+
* 规则:命中标题后一直取到下一个标题;`trim()` 后为空视为未记录(返回 null)。
|
|
1898
|
+
*/
|
|
1899
|
+
function extractAssumptionsSection(doc) {
|
|
1900
|
+
const text = String(doc || "");
|
|
1901
|
+
if (!text.trim()) return null;
|
|
1902
|
+
const lines = text.split(/\r?\n/);
|
|
1903
|
+
const head = /^#{1,6}\s*(?:[0-9]+[.、)]\s*)?(?:附录\s*[0-9A-Za-z]*[.、)]?\s*)?(假设|待澄清|开放问题|assumptions|open questions?)/i;
|
|
1904
|
+
const isHead = (l) => /^#{1,6}\s/.test(l);
|
|
1905
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1906
|
+
if (!head.test(lines[i])) continue;
|
|
1907
|
+
const out = [];
|
|
1908
|
+
for (let k = i + 1; k < lines.length; k++) {
|
|
1909
|
+
if (isHead(lines[k])) break;
|
|
1910
|
+
out.push(lines[k]);
|
|
1911
|
+
}
|
|
1912
|
+
return out.join("\n").trim() || null;
|
|
1913
|
+
}
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* 「宿主契约调研」小节的判定(2026-09-18 用户实锤新增,**硬门禁**)。
|
|
1918
|
+
*
|
|
1919
|
+
* 由来:teamflow 寄生在 dsh 上,我们对 dsh 的插件契约有权威(同仓样本可核实);但用户明确说过
|
|
1920
|
+
* 「用户拿我们去干活,不只是干开发 dsh 插件」——开发 openclaw / hermes / pi 插件时,**dsh 的契约
|
|
1921
|
+
* 一条都不适用**(那些框架有各自的加载/注册机制),套过去就是反向返工。而我们**对它们没有权威**,
|
|
1922
|
+
* 凭记忆写字段名正是 dddd 事故的成因。
|
|
1923
|
+
*
|
|
1924
|
+
* 故规则:`plugin-*` 且目标宿主不是 dsh → PRD **必须**含「宿主契约调研」段(写清目标宿主是哪个、
|
|
1925
|
+
* 从哪儿读到它的插件加载契约、核实到哪些具体要求)。用户原话:「不然你上下文都不知道你开发个啥出来
|
|
1926
|
+
* 都不知道」——所以这条**偏硬**:缺失 → PRD 阶段失败(走重试,附诊断),而不是只记 warn。
|
|
1927
|
+
*
|
|
1928
|
+
* 解析方式与 `extractAssumptionsSection` 同源(行式 + 容错编号标题):产物标题惯例带编号
|
|
1929
|
+
* (`## 7. 宿主契约调研`),单条正则匹配不到会误判"契约未兑现"(该坑已踩过一次)。
|
|
1930
|
+
*/
|
|
1931
|
+
function extractHostResearchSection(doc) {
|
|
1932
|
+
const text = String(doc || "");
|
|
1933
|
+
if (!text.trim()) return null;
|
|
1934
|
+
const lines = text.split(/\r?\n/);
|
|
1935
|
+
const head = /^#{1,6}\s*(?:[0-9]+[.、)]\s*)?(?:附录\s*[0-9A-Za-z]*[.、)]?\s*)?(宿主契约调研|宿主调研|host contract research|host research)/i;
|
|
1936
|
+
const isHead = (l) => /^#{1,6}\s/.test(l);
|
|
1937
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1938
|
+
if (!head.test(lines[i])) continue;
|
|
1939
|
+
const out = [];
|
|
1940
|
+
for (let k = i + 1; k < lines.length; k++) {
|
|
1941
|
+
if (isHead(lines[k])) break;
|
|
1942
|
+
out.push(lines[k]);
|
|
1943
|
+
}
|
|
1944
|
+
return out.join("\n").trim() || null;
|
|
1945
|
+
}
|
|
1946
|
+
return null;
|
|
1947
|
+
}
|
|
1948
|
+
/** dev 阶段 label 的「开发 · 」前缀(zh 存量兼容 + en)。 */
|
|
1949
|
+
const DEV_TITLE_PREFIX_LOCAL = /^(?:开发|Dev) · /;
|
|
1950
|
+
/** 重试/补跑后缀(zh + en:en 侧必须覆盖词典 `dev.taskRetry` 的实际产出 `(attempt N)`)。 */
|
|
1951
|
+
const RETRY_SUFFIX_LOCAL = /(?:((?:第 \d+ 次重试|补跑))| \((?:retry \d+|attempt \d+|follow-up run)\))$/;
|
|
1952
|
+
/**
|
|
1953
|
+
* 任务级聚合(journal 驱动):有 done stage = 该任务已成功(历史失败尝试不翻案)。
|
|
1954
|
+
*
|
|
1955
|
+
* **只认 `taskIds`,不认 title**(2026-09-18 二次修正):
|
|
1956
|
+
* 一个 stage 可能同时承载多个任务(合并执行),逐 id 记账 → 合并执行过的任务**各自**命中已做。
|
|
1957
|
+
*
|
|
1958
|
+
* 关于存量(升级前只写了 title 的 stage):**不在本函数里做 title 回退**——那会形成
|
|
1959
|
+
* 「id / title 两套命名空间」,实测必然全 Miss(补跑 8 个而非 2 个)。正确做法是调用方先跑
|
|
1960
|
+
* `backfillDevTaskIds` 给存量 stage 补算 id(用蓝图 title 结构化匹配),之后本函数只看到 id。
|
|
1961
|
+
* **判定逻辑因此始终只有一个键空间**,不做"双键匹配"、不切分 title。
|
|
1962
|
+
*/
|
|
1963
|
+
function devTaskStatuses(stages) {
|
|
1964
|
+
const m = /* @__PURE__ */ new Map();
|
|
1965
|
+
for (const s of stages || []) {
|
|
1966
|
+
const ids = Array.isArray(s.taskIds) ? s.taskIds.filter((x) => typeof x === "string" && !!x.trim()) : [];
|
|
1967
|
+
for (const key of ids) {
|
|
1968
|
+
const cur = m.get(key) || {
|
|
1969
|
+
done: false,
|
|
1970
|
+
lastStatus: null,
|
|
1971
|
+
lastSeq: -1
|
|
1972
|
+
};
|
|
1973
|
+
if ((s.seq || 0) > cur.lastSeq) {
|
|
1974
|
+
cur.lastSeq = s.seq || 0;
|
|
1975
|
+
cur.lastStatus = s.status || null;
|
|
1976
|
+
}
|
|
1977
|
+
if (s.status === "done") cur.done = true;
|
|
1978
|
+
m.set(key, cur);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return m;
|
|
1982
|
+
}
|
|
1983
|
+
/** dev 任务身份生成(host 侧,按定义顺序):同一份蓝图必然产生同一组 id,故 resume 可稳定对齐。
|
|
1984
|
+
* 蓝图落盘后不变 → id 稳定;title 会因合并/措辞变化 → 故不可作身份(见上方注释)。 */
|
|
1985
|
+
function devTaskIdAt(index) {
|
|
1986
|
+
return `dt-${index + 1}`;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* **存量 stage 的 id 补算**(2026-09-18 二次修正,勿回退)。
|
|
1990
|
+
*
|
|
1991
|
+
* 为什么必须有:升级前的 stage 只写了 `taskKey`(title),没有 `taskIds`。若判定只认 id,
|
|
1992
|
+
* 这些 stage 就成了"看不见的历史"——实测 probe-cache `tf-mu6tb281`:纯 title 判定补跑 2 个(正确),
|
|
1993
|
+
* 而"只认 id + 让存量回退 title"两头不靠 → **补跑 8 个**(灾难:T0/T1/T2 明明 done 却全重跑)。
|
|
1994
|
+
*
|
|
1995
|
+
* 做法:**用蓝图自己的 title 去匹配 stage 的 taskKey**,命中即该 stage 承载了这个任务。
|
|
1996
|
+
* 这是**结构化 → 文本**的比对(拿确定性数据去匹配),不是"按分隔符切分 title"那种反解——
|
|
1997
|
+
* 后者是拿文本长相当身份,同型错误已犯过两次(per-plugin 正则、固定 .gitignore 词表),明确禁止。
|
|
1998
|
+
*
|
|
1999
|
+
* 合并执行的 stage(`taskKey = "T0 … + T6 … + T7 …"`)会命中**多个**任务 title → 补算出多个 id,
|
|
2000
|
+
* 与"合并时 taskIds 数组累加"的新逻辑完全同构:这一类 stage 承载的几个任务**各自**记已做。
|
|
2001
|
+
*
|
|
2002
|
+
* 补算结果写回 stage(`taskIds`),下次直接读——**存量自愈一次**,不是每次都重算。
|
|
2003
|
+
*
|
|
2004
|
+
* @param stages 待补算的 dev stage(会被就地修改:补上 `taskIds`)
|
|
2005
|
+
* @param defs 任务定义(含 id + title,来自 `buildDevTaskDefs`,与本次 run 同源)
|
|
2006
|
+
* @returns 补算到的 stage 数(诊断/测试用)
|
|
2007
|
+
*/
|
|
2008
|
+
function backfillDevTaskIds(stages, defs) {
|
|
2009
|
+
if (!Array.isArray(stages) || !Array.isArray(defs) || !defs.length) return 0;
|
|
2010
|
+
let patched = 0;
|
|
2011
|
+
for (const s of stages) {
|
|
2012
|
+
if (Array.isArray(s.taskIds) && s.taskIds.length) continue;
|
|
2013
|
+
const key = String(s.taskKey || String(s.label || "").replace(DEV_TITLE_PREFIX_LOCAL, "").replace(RETRY_SUFFIX_LOCAL, "").trim());
|
|
2014
|
+
if (!key) continue;
|
|
2015
|
+
const ids = defs.filter((d) => String(d.title || "").trim() && key.includes(String(d.title).trim())).map((d) => d.id);
|
|
2016
|
+
if (!ids.length) continue;
|
|
2017
|
+
s.taskIds = ids;
|
|
2018
|
+
patched++;
|
|
2019
|
+
}
|
|
2020
|
+
return patched;
|
|
2021
|
+
}
|
|
2022
|
+
/** L1:硬拒绝初始化的路径(只做廉价字符串/路径判定,不扫盘)。 */
|
|
2023
|
+
function isDangerousVcsRoot(dir, home) {
|
|
2024
|
+
const p = String(dir || "").replace(/\//g, "\\").replace(/[\\/]+$/, "");
|
|
2025
|
+
if (!p) return true;
|
|
2026
|
+
if (/^[A-Za-z]:$/.test(p)) return true;
|
|
2027
|
+
const seg = (x) => String(x || "").replace(/\//g, "\\").replace(/[\\/]+$/, "").split("\\").filter(Boolean);
|
|
2028
|
+
const ps = seg(p);
|
|
2029
|
+
if (home) {
|
|
2030
|
+
const hs = seg(home);
|
|
2031
|
+
if (hs.length && ps.length <= hs.length && hs.slice(0, ps.length).join("\\").toLowerCase() === ps.join("\\").toLowerCase()) return true;
|
|
2032
|
+
}
|
|
2033
|
+
const sys = [
|
|
2034
|
+
"windows",
|
|
2035
|
+
"program files",
|
|
2036
|
+
"program files (x86)",
|
|
2037
|
+
"programdata",
|
|
2038
|
+
"system volume information",
|
|
2039
|
+
"$recycle.bin"
|
|
2040
|
+
];
|
|
2041
|
+
if (ps.some((s) => sys.indexOf(s.toLowerCase()) !== -1)) return true;
|
|
2042
|
+
if (ps[0] && /^[A-Za-z]:$/.test(ps[0]) && ps.length <= 2) return true;
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
/** L2:有界采样统计目录规模(防"基线提交扫全盘")。超过任一阈值即提前返回 true(不再继续数)。
|
|
2046
|
+
* `maxFiles`/`maxBytes` 阈值由调用方传(默认 1000 个文件 / 100 MB);`budgetMs` 限制采样时长(默认 1s)。 */
|
|
2047
|
+
function dirTooLargeForBaseline(dir, maxFiles = 1e3, maxBytes = 104857600, budgetMs = 1e3) {
|
|
2048
|
+
let files = 0, bytes = 0;
|
|
2049
|
+
const start = Date.now();
|
|
2050
|
+
const walk = (d, depth) => {
|
|
2051
|
+
if (depth > 12 || Date.now() - start > budgetMs) return files > maxFiles || bytes > maxBytes;
|
|
2052
|
+
let es;
|
|
2053
|
+
try {
|
|
2054
|
+
es = readdirSync(d, { withFileTypes: true });
|
|
2055
|
+
} catch (e) {
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
for (const e of es) {
|
|
2059
|
+
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
2060
|
+
const p = d + "\\" + e.name;
|
|
2061
|
+
if (e.isDirectory()) {
|
|
2062
|
+
if (walk(p, depth + 1)) return true;
|
|
2063
|
+
} else {
|
|
2064
|
+
files++;
|
|
2065
|
+
try {
|
|
2066
|
+
bytes += statSync(p).size;
|
|
2067
|
+
} catch (e2) {}
|
|
2068
|
+
if (files > maxFiles || bytes > maxBytes) return true;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return false;
|
|
2072
|
+
};
|
|
2073
|
+
try {
|
|
2074
|
+
if (!existsSync(dir)) return true;
|
|
2075
|
+
return walk(dir, 0);
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
return true;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
//#endregion
|
|
2081
|
+
//#region host/core/locale.ts
|
|
2082
|
+
/**
|
|
2083
|
+
* dsh-plugin-teamflow core — 运行期语言源(客户端推送槽 + 宿主 settings 只读端口)。
|
|
2084
|
+
*
|
|
2085
|
+
* 为什么单独一层:语言「从哪来」必须是**唯一读取点**,否则每个消费方各自读 settings /
|
|
2086
|
+
* 自建浏览器语言缓存,就会出现 PRD 明令禁止的并行机制与「同 run 语言漂移」。
|
|
2087
|
+
* 本文件只回答两个问题:
|
|
2088
|
+
* 1. 当前界面语言是什么(`ambientLocale()`)——`teamflow_*` 工具返回/Remote 错误用;
|
|
2089
|
+
* 2. 新 run 起跑时快照成什么(`newRunLocale()`)——写进 journal.locale,之后一律读快照。
|
|
2090
|
+
*
|
|
2091
|
+
* 三个来源(优先级同 AC-1):
|
|
2092
|
+
* - **客户端推送**(`noteClientLocale`):浏览器当前语言(含系统探测结果),经既有 Remote 面
|
|
2093
|
+
* `teamflow/setLocale` 推上来。用户没在宿主显式选过语言时只有这条通道能拿到真实语言
|
|
2094
|
+
* (宿主 `locale.preference` 仅在用户显式选择过时才有值)。
|
|
2095
|
+
* - **宿主 settings 只读端口**(`setSettingsPort`):`ctx.get('settings').get('locale').preference`,
|
|
2096
|
+
* 由 `@deepseek-ai/dsh-client-locale` 的 host face 注册。host 只读不写、未注册/服务缺失一律
|
|
2097
|
+
* null(全 try/catch,AC-10:解析链任一级缺失都不报错、不阻塞)。
|
|
2098
|
+
* - **`en`**:兜底(AC-1③)。
|
|
2099
|
+
*
|
|
2100
|
+
* 内存单槽、last-write-wins、不落盘:语言是界面态,重启后由客户端重新推送(持久化属宿主职责)。
|
|
2101
|
+
*/
|
|
2102
|
+
/** 客户端推送的语言(最近一次,内存单槽)。 */
|
|
2103
|
+
let clientPushed = null;
|
|
2104
|
+
/** 客户端推送语言:可解析即写入,非法即清空(非法值不保留旧值——避免「推了个 fr,host 还在用上次的 en」)。 */
|
|
2105
|
+
function noteClientLocale(v) {
|
|
2106
|
+
const parsed = parseLocale(v);
|
|
2107
|
+
clientPushed = parsed;
|
|
2108
|
+
return parsed !== null;
|
|
2109
|
+
}
|
|
2110
|
+
/** 宿主 settings 只读端口(由门面注入;缺省 null = 宿主未提供)。 */
|
|
2111
|
+
let settingsPort = null;
|
|
2112
|
+
/** 注入宿主 settings 只读端口(门面构造时调用;宿主缺 settings 服务时传 null)。 */
|
|
2113
|
+
function setSettingsPort(fn) {
|
|
2114
|
+
settingsPort = typeof fn === "function" ? fn : null;
|
|
2115
|
+
}
|
|
2116
|
+
/** 宿主用户显式选择的语言(读不到/服务缺失/异常 → null,永不抛出)。 */
|
|
2117
|
+
function hostPreference() {
|
|
2118
|
+
if (!settingsPort) return null;
|
|
2119
|
+
try {
|
|
2120
|
+
return parseLocale(settingsPort());
|
|
2121
|
+
} catch (e) {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
/** 当前界面语言(AC-1 解析链;工具返回/Remote 错误用)。 */
|
|
2126
|
+
function ambientLocale() {
|
|
2127
|
+
try {
|
|
2128
|
+
return resolveLocale({
|
|
2129
|
+
client: clientPushed,
|
|
2130
|
+
host: hostPreference()
|
|
2131
|
+
});
|
|
2132
|
+
} catch (e) {
|
|
2133
|
+
return "en";
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
/** 新 run 起跑时解析一次(写进 journal.locale 的快照值;AC-2)。 */
|
|
2137
|
+
function newRunLocale() {
|
|
2138
|
+
return ambientLocale();
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* 读 run 的语言快照(纯读、绝不重解析;AC-2)。
|
|
2142
|
+
* 历史 journal(升级前无 locale 字段)按 zh 处理——那些 run 的存量文案本就是中文,
|
|
2143
|
+
* 用 en 去渲染会与已落盘内容割裂(resumeRun 会补写该字段,见 core/pipeline.ts)。
|
|
2144
|
+
*/
|
|
2145
|
+
function runLocaleOf(journal) {
|
|
2146
|
+
const j = journal;
|
|
2147
|
+
return parseLocale(j && j.locale) || "zh";
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* 缺 locale 字段时的快照取值(QA-1:resume 绝不按当前界面语言重解析)。
|
|
2151
|
+
* - 新 run:环境语言(AC-1 解析链);
|
|
2152
|
+
* - resume/续跑:一律 `zh`——历史 journal 的存量文案本就是中文,用界面语言补写会造成同一 run 内中英混排
|
|
2153
|
+
* (PRD §4.1「已落库文本保留生成时语言」)。与 `runLocaleOf` 的缺省口径严格一致。
|
|
2154
|
+
*/
|
|
2155
|
+
function localeForMissingSnapshot(resume) {
|
|
2156
|
+
return resume ? "zh" : newRunLocale();
|
|
2157
|
+
}
|
|
2158
|
+
/** 读 state 注入块上下文里的语言(`state.__runCtx.locale`,由 pipeline 按快照写入)。 */
|
|
2159
|
+
function runCtxLocale(state) {
|
|
2160
|
+
const s = state;
|
|
2161
|
+
return parseLocale(s && s.__runCtx && s.__runCtx.locale) || "zh";
|
|
2162
|
+
}
|
|
1526
2163
|
//#endregion
|
|
1527
2164
|
//#region host/core/context.ts
|
|
1528
2165
|
/**
|
|
@@ -1548,14 +2185,70 @@ function setRuntime(agents, subagents, workspaceRegistry, agentDefaultModel, llm
|
|
|
1548
2185
|
function setSessionProjections(projections) {
|
|
1549
2186
|
runtime.sessionProjections = projections;
|
|
1550
2187
|
}
|
|
2188
|
+
/**
|
|
2189
|
+
* **宿主锚点**(2026-09-21):TeamflowService 构造时把 `ctx.baseUrl`(= 当前 profile 目录的
|
|
2190
|
+
* file:// URL,宿主在挂载插件树**之前**设好)与 `dshHomePath` 记下来,供 pipeline 起跑时探测本机
|
|
2191
|
+
* 安装环境。**为何不由 pipeline 自己拿**:pipeline 是纯编排模块,没有 ctx;且它链到宿主私有 peer
|
|
2192
|
+
* (report→@deepseek-ai/dsh-llm),Behavior 测试取不到 → 锚点经此单例搬运(与 setRuntime 同款,
|
|
2193
|
+
* 单向依赖不破)。
|
|
2194
|
+
* ⚠️ 不存 `process.env.DSH_HOME`:它**不是"装了 dsh 就自带"**(可选覆盖变量,装 dsh 不写它,
|
|
2195
|
+
* `.env` 也设不了 DSH_ 前缀),默认安装下为 undefined。baseUrl 才是权威事实。
|
|
2196
|
+
*/
|
|
2197
|
+
const installCtx = {};
|
|
2198
|
+
function setInstallCtx(ctx) {
|
|
2199
|
+
const c = ctx;
|
|
2200
|
+
const url = c && typeof c.baseUrl === "string" ? c.baseUrl : "";
|
|
2201
|
+
if (url) installCtx.baseUrl = url;
|
|
2202
|
+
}
|
|
1551
2203
|
/** 运行期 run 注册表(runId → Journal)。 */
|
|
1552
2204
|
const runs = /* @__PURE__ */ new Map();
|
|
1553
|
-
/** 进行中的 stage 注册表(runId →
|
|
2205
|
+
/** 进行中的 stage 注册表(runId → `Map<stage, run>`)。**同一 run 可有多路并发子代理**(dev 并发池),
|
|
2206
|
+
* 故每 run 存的是 stage→handle 的表而不是单个 handle:旧形状(每 run 一个 `{run, stage}`,后启动的覆盖前一个)
|
|
2207
|
+
* 让取消只能停掉最后一路,兄弟继续跑到自然结束(2026-09-16 用户实测:「开发的多 agent 中断不了」)。 */
|
|
1554
2208
|
const inFlight = /* @__PURE__ */ new Map();
|
|
2209
|
+
/** 登记一路在飞子代理(runAgent 拿到 handle 后调用)。 */
|
|
2210
|
+
function trackInFlight(runId, stage, run) {
|
|
2211
|
+
if (!runId || !stage) return;
|
|
2212
|
+
let m = inFlight.get(runId);
|
|
2213
|
+
if (!m) {
|
|
2214
|
+
m = /* @__PURE__ */ new Map();
|
|
2215
|
+
inFlight.set(runId, m);
|
|
2216
|
+
}
|
|
2217
|
+
m.set(stage, run);
|
|
2218
|
+
}
|
|
2219
|
+
/** 注销一路在飞子代理(该阶段结束时调用);空桶顺手清掉,避免 map 常驻膨胀。 */
|
|
2220
|
+
function untrackInFlight(runId, stage) {
|
|
2221
|
+
const m = inFlight.get(runId);
|
|
2222
|
+
if (!m) return;
|
|
2223
|
+
m.delete(stage);
|
|
2224
|
+
if (m.size === 0) inFlight.delete(runId);
|
|
2225
|
+
}
|
|
1555
2226
|
/** 产品级 backlog 缓存(product → BacklogStore)。 */
|
|
1556
2227
|
const stores = /* @__PURE__ */ new Map();
|
|
1557
2228
|
/** 产品级并发锁:product → 活跃 runId(同一产品同时只允许一条流水线)。 */
|
|
1558
2229
|
const activeProducts = /* @__PURE__ */ new Map();
|
|
2230
|
+
function cancelRun(runId, source = "unknown") {
|
|
2231
|
+
const j = runs.get(runId);
|
|
2232
|
+
if (!j || j.status !== "running") return false;
|
|
2233
|
+
j.cancelled = true;
|
|
2234
|
+
j.cancelSource = source;
|
|
2235
|
+
j.cancelRequestedAt = Date.now();
|
|
2236
|
+
try {
|
|
2237
|
+
const locale = runLocaleOf(j);
|
|
2238
|
+
j.logs.push({
|
|
2239
|
+
t: Date.now(),
|
|
2240
|
+
level: "warn",
|
|
2241
|
+
message: t(locale, "log.cancelRequested", { source: t(locale, `cancelSource.${source}`) })
|
|
2242
|
+
});
|
|
2243
|
+
} catch (e) {}
|
|
2244
|
+
const live = inFlight.get(runId);
|
|
2245
|
+
if (live) for (const run of live.values()) try {
|
|
2246
|
+
const p = run && typeof run.dispose === "function" ? run.dispose() : null;
|
|
2247
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
2248
|
+
} catch (e) {}
|
|
2249
|
+
persistJournal(j);
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
1559
2252
|
/** 可用的子代理 provider 名(优先 spawn)。 */
|
|
1560
2253
|
function providerName() {
|
|
1561
2254
|
const subagents = runtime.subagents;
|
|
@@ -1636,89 +2329,6 @@ function workspaceScopeOf(agent) {
|
|
|
1636
2329
|
};
|
|
1637
2330
|
}
|
|
1638
2331
|
//#endregion
|
|
1639
|
-
//#region host/core/locale.ts
|
|
1640
|
-
/**
|
|
1641
|
-
* dsh-plugin-teamflow core — 运行期语言源(客户端推送槽 + 宿主 settings 只读端口)。
|
|
1642
|
-
*
|
|
1643
|
-
* 为什么单独一层:语言「从哪来」必须是**唯一读取点**,否则每个消费方各自读 settings /
|
|
1644
|
-
* 自建浏览器语言缓存,就会出现 PRD 明令禁止的并行机制与「同 run 语言漂移」。
|
|
1645
|
-
* 本文件只回答两个问题:
|
|
1646
|
-
* 1. 当前界面语言是什么(`ambientLocale()`)——`teamflow_*` 工具返回/Remote 错误用;
|
|
1647
|
-
* 2. 新 run 起跑时快照成什么(`newRunLocale()`)——写进 journal.locale,之后一律读快照。
|
|
1648
|
-
*
|
|
1649
|
-
* 三个来源(优先级同 AC-1):
|
|
1650
|
-
* - **客户端推送**(`noteClientLocale`):浏览器当前语言(含系统探测结果),经既有 Remote 面
|
|
1651
|
-
* `teamflow/setLocale` 推上来。用户没在宿主显式选过语言时只有这条通道能拿到真实语言
|
|
1652
|
-
* (宿主 `locale.preference` 仅在用户显式选择过时才有值)。
|
|
1653
|
-
* - **宿主 settings 只读端口**(`setSettingsPort`):`ctx.get('settings').get('locale').preference`,
|
|
1654
|
-
* 由 `@deepseek-ai/dsh-client-locale` 的 host face 注册。host 只读不写、未注册/服务缺失一律
|
|
1655
|
-
* null(全 try/catch,AC-10:解析链任一级缺失都不报错、不阻塞)。
|
|
1656
|
-
* - **`en`**:兜底(AC-1③)。
|
|
1657
|
-
*
|
|
1658
|
-
* 内存单槽、last-write-wins、不落盘:语言是界面态,重启后由客户端重新推送(持久化属宿主职责)。
|
|
1659
|
-
*/
|
|
1660
|
-
/** 客户端推送的语言(最近一次,内存单槽)。 */
|
|
1661
|
-
let clientPushed = null;
|
|
1662
|
-
/** 客户端推送语言:可解析即写入,非法即清空(非法值不保留旧值——避免「推了个 fr,host 还在用上次的 en」)。 */
|
|
1663
|
-
function noteClientLocale(v) {
|
|
1664
|
-
const parsed = parseLocale(v);
|
|
1665
|
-
clientPushed = parsed;
|
|
1666
|
-
return parsed !== null;
|
|
1667
|
-
}
|
|
1668
|
-
/** 宿主 settings 只读端口(由门面注入;缺省 null = 宿主未提供)。 */
|
|
1669
|
-
let settingsPort = null;
|
|
1670
|
-
/** 注入宿主 settings 只读端口(门面构造时调用;宿主缺 settings 服务时传 null)。 */
|
|
1671
|
-
function setSettingsPort(fn) {
|
|
1672
|
-
settingsPort = typeof fn === "function" ? fn : null;
|
|
1673
|
-
}
|
|
1674
|
-
/** 宿主用户显式选择的语言(读不到/服务缺失/异常 → null,永不抛出)。 */
|
|
1675
|
-
function hostPreference() {
|
|
1676
|
-
if (!settingsPort) return null;
|
|
1677
|
-
try {
|
|
1678
|
-
return parseLocale(settingsPort());
|
|
1679
|
-
} catch (e) {
|
|
1680
|
-
return null;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
/** 当前界面语言(AC-1 解析链;工具返回/Remote 错误用)。 */
|
|
1684
|
-
function ambientLocale() {
|
|
1685
|
-
try {
|
|
1686
|
-
return resolveLocale({
|
|
1687
|
-
client: clientPushed,
|
|
1688
|
-
host: hostPreference()
|
|
1689
|
-
});
|
|
1690
|
-
} catch (e) {
|
|
1691
|
-
return "en";
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
/** 新 run 起跑时解析一次(写进 journal.locale 的快照值;AC-2)。 */
|
|
1695
|
-
function newRunLocale() {
|
|
1696
|
-
return ambientLocale();
|
|
1697
|
-
}
|
|
1698
|
-
/**
|
|
1699
|
-
* 读 run 的语言快照(纯读、绝不重解析;AC-2)。
|
|
1700
|
-
* 历史 journal(升级前无 locale 字段)按 zh 处理——那些 run 的存量文案本就是中文,
|
|
1701
|
-
* 用 en 去渲染会与已落盘内容割裂(resumeRun 会补写该字段,见 core/pipeline.ts)。
|
|
1702
|
-
*/
|
|
1703
|
-
function runLocaleOf(journal) {
|
|
1704
|
-
const j = journal;
|
|
1705
|
-
return parseLocale(j && j.locale) || "zh";
|
|
1706
|
-
}
|
|
1707
|
-
/**
|
|
1708
|
-
* 缺 locale 字段时的快照取值(QA-1:resume 绝不按当前界面语言重解析)。
|
|
1709
|
-
* - 新 run:环境语言(AC-1 解析链);
|
|
1710
|
-
* - resume/续跑:一律 `zh`——历史 journal 的存量文案本就是中文,用界面语言补写会造成同一 run 内中英混排
|
|
1711
|
-
* (PRD §4.1「已落库文本保留生成时语言」)。与 `runLocaleOf` 的缺省口径严格一致。
|
|
1712
|
-
*/
|
|
1713
|
-
function localeForMissingSnapshot(resume) {
|
|
1714
|
-
return resume ? "zh" : newRunLocale();
|
|
1715
|
-
}
|
|
1716
|
-
/** 读 state 注入块上下文里的语言(`state.__runCtx.locale`,由 pipeline 按快照写入)。 */
|
|
1717
|
-
function runCtxLocale(state) {
|
|
1718
|
-
const s = state;
|
|
1719
|
-
return parseLocale(s && s.__runCtx && s.__runCtx.locale) || "zh";
|
|
1720
|
-
}
|
|
1721
|
-
//#endregion
|
|
1722
2332
|
//#region host/core/backlog.ts
|
|
1723
2333
|
/**
|
|
1724
2334
|
* dsh-plugin-teamflow core — Backlog 数据层与状态机($DSH_HOME/teamflow/<product>/backlog/*)。
|
|
@@ -2135,12 +2745,13 @@ function noteTaskAssign(journal, role, assignee) {
|
|
|
2135
2745
|
persistJournal(journal);
|
|
2136
2746
|
}
|
|
2137
2747
|
/** 创建 dev 子卡:流水线 dev 阶段开始时,为每个 devTaskDef 建一张子卡。 */
|
|
2138
|
-
function createSubtask(journal, title, spec) {
|
|
2748
|
+
function createSubtask(journal, title, spec, dtId) {
|
|
2139
2749
|
const store = storeFor(journal.workspace || "default");
|
|
2140
2750
|
const mainTask = journal.taskId ? store.find("task", journal.taskId) : null;
|
|
2141
2751
|
if (!mainTask) return null;
|
|
2142
2752
|
const fullTitle = t(runLocaleOf(journal), "backlog.devTitle", { title });
|
|
2143
|
-
const
|
|
2753
|
+
const key = dtId ? String(dtId) : null;
|
|
2754
|
+
const existing = store.tasks.find((t) => t.reqId === journal.reqId && t.parentId === journal.taskId && (key ? t.dtId ? t.dtId === key : false : t.taskKey && t.taskKey === title || !t.taskKey && !t.dtId && t.title === fullTitle));
|
|
2144
2755
|
if (existing) {
|
|
2145
2756
|
existing.status = "pending";
|
|
2146
2757
|
existing.failed = false;
|
|
@@ -2148,6 +2759,7 @@ function createSubtask(journal, title, spec) {
|
|
|
2148
2759
|
existing.endedAt = null;
|
|
2149
2760
|
existing.retries = (existing.retries || 0) + 1;
|
|
2150
2761
|
existing.taskKey = existing.taskKey || title;
|
|
2762
|
+
if (key) existing.dtId = key;
|
|
2151
2763
|
existing.updatedAt = Date.now();
|
|
2152
2764
|
store.persist();
|
|
2153
2765
|
persistJournal(journal);
|
|
@@ -2162,6 +2774,7 @@ function createSubtask(journal, title, spec) {
|
|
|
2162
2774
|
type: "subtask",
|
|
2163
2775
|
title: fullTitle,
|
|
2164
2776
|
taskKey: title,
|
|
2777
|
+
dtId: key,
|
|
2165
2778
|
spec: spec || "",
|
|
2166
2779
|
status: "pending",
|
|
2167
2780
|
devAssign: mainTask && mainTask.devAssign || null,
|
|
@@ -2384,6 +2997,7 @@ function loadState(projectKey) {
|
|
|
2384
2997
|
base.acIndex = raw.acIndex || {};
|
|
2385
2998
|
base.stages = raw.stages || {};
|
|
2386
2999
|
base.lastRun = raw.lastRun ?? null;
|
|
3000
|
+
if (raw.gitMode === "repo" || raw.gitMode === "none") base.gitMode = raw.gitMode;
|
|
2387
3001
|
return base;
|
|
2388
3002
|
}
|
|
2389
3003
|
}
|
|
@@ -2482,6 +3096,14 @@ function stateSliceFor(state, role) {
|
|
|
2482
3096
|
list: Object.entries(state.modules).map(([f, c]) => `${f}${c ? "→" + c : ""}`).join(t(locale, "state.commaSep"))
|
|
2483
3097
|
}));
|
|
2484
3098
|
if (state.verifyScripts.length && (role === "qa" || role === "tech" || role === "dev")) lines.push(t(locale, "state.verifyScripts", { list: state.verifyScripts.join(t(locale, "state.commaSep")) }));
|
|
3099
|
+
if (state.__runCtx && Array.isArray(state.__runCtx.artifactContracts) && state.__runCtx.artifactContracts.length && (role === "qa" || role === "acceptance")) {
|
|
3100
|
+
const kind = state.__runCtx.artifact || "other";
|
|
3101
|
+
lines.push(t(locale, "state.artifactContracts", {
|
|
3102
|
+
kind,
|
|
3103
|
+
n: state.__runCtx.artifactContracts.length,
|
|
3104
|
+
list: state.__runCtx.artifactContracts.map((it, i) => `${i + 1}. ${it.requirement} — ${it.criteria}`).join("\n")
|
|
3105
|
+
}));
|
|
3106
|
+
}
|
|
2485
3107
|
if (role === "pm" || role === "acceptance") {
|
|
2486
3108
|
if (state.stages.prd) lines.push(t(locale, "state.prdSummary", { summary: state.stages.prd }));
|
|
2487
3109
|
if (state.stages.tech) lines.push(t(locale, "state.techSummary", { summary: state.stages.tech }));
|
|
@@ -2830,15 +3452,79 @@ function gitCmd(cwd, args, timeoutMs = 8e3) {
|
|
|
2830
3452
|
* `-c advice.addIgnoredFile=false` 也不行(那是 error 不是 advice)。
|
|
2831
3453
|
* 复现与候选方案矩阵见 `test/commit-path.test.js`(真 git 集成测试,锁死这个坑)。
|
|
2832
3454
|
*/
|
|
2833
|
-
function tfAddArgs() {
|
|
3455
|
+
function tfAddArgs(excludes = [], cwd) {
|
|
2834
3456
|
return [
|
|
2835
3457
|
"add",
|
|
2836
3458
|
"-A",
|
|
2837
3459
|
"--",
|
|
2838
|
-
"."
|
|
3460
|
+
".",
|
|
3461
|
+
...tfExcludePathspecs(excludes, cwd)
|
|
2839
3462
|
];
|
|
2840
3463
|
}
|
|
2841
3464
|
/**
|
|
3465
|
+
* 冷启动基线提交的**索引层噪音排除**(2026-09-18 方案 B 根治,勿回退为「写用户 .gitignore」)。
|
|
3466
|
+
*
|
|
3467
|
+
* 为什么存在:`git init` 后第一次 `add -A` 是**整树冷扫描**,pnpm 本地 store / node_modules
|
|
3468
|
+
* 在实测里是 549 文件 / 51 MB(probe-clock),默认 8s 超时被直接杀掉 → 基线提交失败。
|
|
3469
|
+
*
|
|
3470
|
+
* **为什么不再写进 `.gitignore`(用户实锤截图)**:旧实现 `pipeline.ensureCommonNoiseIgnores()`
|
|
3471
|
+
* 调 `mergeGitignore(before, ['.pnpm-store/','node_modules/'], 'zh')` —— 而 `mergeGitignore` 的注释
|
|
3472
|
+
* 参数是**整批一条**,于是 `.pnpm-store/` 顶着「TeamFlow 运行日志(插件自有产物…)」的文案写进了
|
|
3473
|
+
* **用户的 `.gitignore`**。两处错:① 注释张冠李戴;② **越界**——`.gitignore` 是用户的项目资产,
|
|
3474
|
+
* 「该忽略什么」属于 L2(PRD 阶段 PM 按技术栈规划)与用户本人,host 只该在**自己那一次 git 调用**上
|
|
3475
|
+
* 收敛范围,不该在用户文件里留下任何痕迹。
|
|
3476
|
+
*
|
|
3477
|
+
* 稳定性依据(temp 仓库实测,见 `test/commit-path.test.js`):`:(exclude)` 报错退出 1 的**唯一前提**
|
|
3478
|
+
* 是该路径**已被 `.gitignore` 忽略**(「显式点名 + 被忽略」)。本清单里的路径**从不写进 .gitignore**,
|
|
3479
|
+
* 故恒定 exit 0。运行时另有 `tfExcludePathspecs` 的自检双保险。
|
|
3480
|
+
*/
|
|
3481
|
+
const BASELINE_NOISE_EXCLUDES = [".pnpm-store", "node_modules"];
|
|
3482
|
+
/**
|
|
3483
|
+
* 把排除项转成 magic pathspec,并**用 git 自己判断**哪些项不该下发。
|
|
3484
|
+
*
|
|
3485
|
+
* **为什么必须用 `git check-ignore` 而不是自己读 `.gitignore`(2026-09-18 实测修正,勿回退)**:
|
|
3486
|
+
* 初版自检用 `existsSync('.gitignore')` + 正则近似——**相对路径读的是宿主进程的 cwd,不是目标仓库**,
|
|
3487
|
+
* 于是两个方向同时错:目标仓库没忽略的项被误拦(排除失效、噪音进索引),目标仓库忽略了的项被误放
|
|
3488
|
+
* (点名 + 被忽略 → **exit 1,正是 2026-09-11→09-15「4 天没有任何 run 提交过」那个坑原样复活**)。
|
|
3489
|
+
* `git check-ignore` 以**目标仓库为根**、且用 git 自己的规则引擎(通配/取反/嵌套 `.gitignore`/全局
|
|
3490
|
+
* excludesfile 全覆盖),是本判据唯一正确的实现。`-q` 只取退出码:0=被忽略,1=未忽略。
|
|
3491
|
+
*
|
|
3492
|
+
* 兜底方向(宁可不排、不可 exit 1):git 不可用/异常 → 一律**不下发**该排除项。
|
|
3493
|
+
* 代价只是"这次少排一点、add 慢一点",而失败的代价是**整条提交链路静默失效**——不对称,故从严。
|
|
3494
|
+
*/
|
|
3495
|
+
function tfExcludePathspecs(excludes, cwd) {
|
|
3496
|
+
const list = excludes.filter((p) => typeof p === "string" && !!p.trim());
|
|
3497
|
+
if (!list.length) return [];
|
|
3498
|
+
const out = [];
|
|
3499
|
+
for (const p of list) {
|
|
3500
|
+
const clean = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3501
|
+
if (!clean) continue;
|
|
3502
|
+
if (isIgnoredByGit(clean, cwd)) continue;
|
|
3503
|
+
out.push(`:(exclude)${clean}`);
|
|
3504
|
+
}
|
|
3505
|
+
return out;
|
|
3506
|
+
}
|
|
3507
|
+
/** `git check-ignore` 判定(0=被忽略/1=未忽略/其它=无法判定)。无法判定时按「被忽略」处理=不下发 pathspec。 */
|
|
3508
|
+
function isIgnoredByGit(rel, cwd) {
|
|
3509
|
+
try {
|
|
3510
|
+
execFileSync("git", [
|
|
3511
|
+
"check-ignore",
|
|
3512
|
+
"-q",
|
|
3513
|
+
"--",
|
|
3514
|
+
rel
|
|
3515
|
+
], {
|
|
3516
|
+
cwd: cwd || process.cwd(),
|
|
3517
|
+
encoding: "utf8",
|
|
3518
|
+
timeout: 4e3,
|
|
3519
|
+
windowsHide: true
|
|
3520
|
+
});
|
|
3521
|
+
return true;
|
|
3522
|
+
} catch (e) {
|
|
3523
|
+
if (e.status === 1) return false;
|
|
3524
|
+
return true;
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
2842
3528
|
* 索引兜底:把自有日志从索引里摘掉(**只动索引,不删工作区文件**)。
|
|
2843
3529
|
*
|
|
2844
3530
|
* 与 `.gitignore` 补写构成新的两道防线:
|
|
@@ -3096,6 +3782,29 @@ function freshTokensOf(usage) {
|
|
|
3096
3782
|
if (!usage) return 0;
|
|
3097
3783
|
return (usage.input || 0) + (usage.cacheWrite || 0) + (usage.output || 0);
|
|
3098
3784
|
}
|
|
3785
|
+
/** 官方口径缓存命中率 = cacheRead/(input+cacheRead)(无输入记 0,不做除零外推)。 */
|
|
3786
|
+
function cacheHitRatioOf(usage) {
|
|
3787
|
+
const input = usage && usage.input || 0;
|
|
3788
|
+
const cacheRead = usage && usage.cacheRead || 0;
|
|
3789
|
+
const total = input + cacheRead;
|
|
3790
|
+
return total > 0 ? cacheRead / total : 0;
|
|
3791
|
+
}
|
|
3792
|
+
/**
|
|
3793
|
+
* 该阶段**有效的新增 token 预算** + 判定依据(供熔断日志留痕)。
|
|
3794
|
+
* @returns budget = 基础预算(无缓存 ×`UNCACHED_BUDGET_FACTOR`);uncached = 是否放宽;
|
|
3795
|
+
* ratio/calls = 依据(命中率与累计调用数),未放宽时也为真实值。
|
|
3796
|
+
*/
|
|
3797
|
+
function effectiveFreshBudget(usage, base) {
|
|
3798
|
+
const calls = usage && usage.calls || 0;
|
|
3799
|
+
const ratio = cacheHitRatioOf(usage);
|
|
3800
|
+
const uncached = calls >= 5 && ratio < .5;
|
|
3801
|
+
return {
|
|
3802
|
+
budget: uncached ? base * 3 : base,
|
|
3803
|
+
uncached,
|
|
3804
|
+
ratio,
|
|
3805
|
+
calls
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3099
3808
|
//#endregion
|
|
3100
3809
|
//#region host/core/guard.ts
|
|
3101
3810
|
/**
|
|
@@ -3121,6 +3830,12 @@ function freshTokensOf(usage) {
|
|
|
3121
3830
|
* C. 空转检测:会话仍在产出事件,但连续 GUARD_NO_TOOL_MS 没有任何工具调用
|
|
3122
3831
|
* (纯推理打转/改写式循环;正常 agent 每分钟都在调工具)→ outcome='stalled'。
|
|
3123
3832
|
* 兜底关系:复读判定放宽后,edit 后陷入死循环的漏网场景由 C(长时间无工具调用)兜住。
|
|
3833
|
+
* D. **环境不可用检测(2026-09-23 probe-v4 实锤)**:同一工具**以完全相同错误**持续失败
|
|
3834
|
+
* (达 GUARD_TOOL_FAIL_WARN 次先注入提醒,达 GUARD_TOOL_FAIL_ABORT 次中止)→ outcome='env-unavailable'
|
|
3835
|
+
* (不自动重试,直接 needs-human 并点名环境)。实锤:`pwsh` 因 Windows 沙箱 ACL provision 失败
|
|
3836
|
+
* (`SetNamedSecurityInfoW failed (Win32 5)`)每次同样报错,架构师重试 7 次 + 90k 字符推理后撞
|
|
3837
|
+
* max-tokens 才停 —— 白烧 52.6k 输出,且汇报把真因误写成 `max-tokens`。重试/换命令/用推理代替执行
|
|
3838
|
+
* 都修不好环境,所以必须在烧钱之前停下并说清原因。
|
|
3124
3839
|
*
|
|
3125
3840
|
* 中止方式:run.dispose() → run.result 结算。outcome 命名刻意避开 isUnretryable 的
|
|
3126
3841
|
* /token|context|limit/ 正则;只有 'degenerated' 享受干净重试豁免(runner.withRetry)。
|
|
@@ -3219,8 +3934,7 @@ function injectReminder(run, text, locale) {
|
|
|
3219
3934
|
text
|
|
3220
3935
|
}],
|
|
3221
3936
|
source: {
|
|
3222
|
-
kind: "plugin",
|
|
3223
|
-
plugin: "dsh-plugin-teamflow",
|
|
3937
|
+
kind: "plugin:dsh-plugin-teamflow",
|
|
3224
3938
|
form: "notice",
|
|
3225
3939
|
summary: t(locale, "guard.noticeSummary")
|
|
3226
3940
|
}
|
|
@@ -3248,6 +3962,9 @@ function startStageGuard(opts) {
|
|
|
3248
3962
|
let lastMutationAt = 0;
|
|
3249
3963
|
let repeatWarned = false;
|
|
3250
3964
|
let busyWarned = false;
|
|
3965
|
+
const callNames = /* @__PURE__ */ new Map();
|
|
3966
|
+
const toolFailures = /* @__PURE__ */ new Map();
|
|
3967
|
+
const toolFailWarned = /* @__PURE__ */ new Set();
|
|
3251
3968
|
function warnOnce(key, set, message, hint) {
|
|
3252
3969
|
if (set.has(key)) return;
|
|
3253
3970
|
set.add(key);
|
|
@@ -3365,6 +4082,8 @@ function startStageGuard(opts) {
|
|
|
3365
4082
|
}
|
|
3366
4083
|
}
|
|
3367
4084
|
observeToolCalls(newEvents);
|
|
4085
|
+
observeToolFailures(newEvents);
|
|
4086
|
+
if (fired) return;
|
|
3368
4087
|
processed = events.length;
|
|
3369
4088
|
}
|
|
3370
4089
|
for (const ev of events.slice(-200)) {
|
|
@@ -3460,6 +4179,66 @@ function startStageGuard(opts) {
|
|
|
3460
4179
|
}
|
|
3461
4180
|
}
|
|
3462
4181
|
}
|
|
4182
|
+
/**
|
|
4183
|
+
* 环境不可用检测(2026-09-23 probe-v4 实锤,增量处理**失败**的 tool/result)。
|
|
4184
|
+
*
|
|
4185
|
+
* 判据:同一工具**以完全相同错误**反复失败 = 工作区坏了(实锤:Windows 沙箱 ACL provision 失败
|
|
4186
|
+
* `SetNamedSecurityInfoW failed (Win32 5): grantWrite(<workspace>)` → 该工作区所有命令全废)。
|
|
4187
|
+
* 这类失败不是模型问题——重试、换命令绕过、用推理代替执行都修不好它,只会继续烧钱
|
|
4188
|
+
* (那次架构师重试 7 次 + 90k 字符推理才撞 max-tokens 停下,白烧 52.6k 输出,且汇报把真因误写成 max-tokens)。
|
|
4189
|
+
* 失败判定用结构化 `message.isError`(不猜文本);指纹 = 工具名 + 归一化错误前 160 字符。
|
|
4190
|
+
* 动作:达 WARN → 记 warn + 注入提醒(请模型停手上报);达 ABORT → fire(outcome='env-unavailable')。
|
|
4191
|
+
*/
|
|
4192
|
+
function observeToolFailures(events) {
|
|
4193
|
+
for (const ev of events) {
|
|
4194
|
+
const e = ev;
|
|
4195
|
+
if (!e) continue;
|
|
4196
|
+
if (e.type === "tool/call" || e.type === "tool-call") {
|
|
4197
|
+
const d = e.data;
|
|
4198
|
+
if (d && typeof d.callId === "string" && typeof d.name === "string") callNames.set(d.callId, d.name);
|
|
4199
|
+
continue;
|
|
4200
|
+
}
|
|
4201
|
+
if (e.type !== "tool/result") continue;
|
|
4202
|
+
if (!isToolErrorResult(e.data)) continue;
|
|
4203
|
+
const callId = e.data?.message?.toolCallId;
|
|
4204
|
+
const tool = typeof callId === "string" && callNames.get(callId) || "command";
|
|
4205
|
+
const text = toolResultText(e.data);
|
|
4206
|
+
const sig = toolFailureSignature(tool, text);
|
|
4207
|
+
const n = (toolFailures.get(sig) || 0) + 1;
|
|
4208
|
+
toolFailures.set(sig, n);
|
|
4209
|
+
const detail = clip(String(text).replace(/\s+/g, " ").trim(), 160);
|
|
4210
|
+
const action = toolFailureAction(n, 2, 3);
|
|
4211
|
+
if (action === "warn" && !toolFailWarned.has(sig)) {
|
|
4212
|
+
toolFailWarned.add(sig);
|
|
4213
|
+
stage.envUnavailable = `${tool}: ${detail}`;
|
|
4214
|
+
try {
|
|
4215
|
+
journal.logs.push({
|
|
4216
|
+
t: Date.now(),
|
|
4217
|
+
level: "warn",
|
|
4218
|
+
message: t(locale, "guard.toolFail", {
|
|
4219
|
+
label,
|
|
4220
|
+
tool,
|
|
4221
|
+
n,
|
|
4222
|
+
detail
|
|
4223
|
+
})
|
|
4224
|
+
});
|
|
4225
|
+
} catch (err) {}
|
|
4226
|
+
injectReminder(run, `[ENV GUARD · reminder] ${t(locale, "guard.reminderToolFail", {
|
|
4227
|
+
tool,
|
|
4228
|
+
n,
|
|
4229
|
+
detail
|
|
4230
|
+
})}`, locale);
|
|
4231
|
+
} else if (action === "abort") {
|
|
4232
|
+
stage.envUnavailable = `${tool}: ${detail}`;
|
|
4233
|
+
fire(t(locale, "guard.reasonToolFail", {
|
|
4234
|
+
tool,
|
|
4235
|
+
n,
|
|
4236
|
+
detail
|
|
4237
|
+
}), "env-unavailable");
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
3463
4242
|
return () => {
|
|
3464
4243
|
fired = true;
|
|
3465
4244
|
clearInterval(timer);
|
|
@@ -3516,19 +4295,6 @@ async function resolveStageEffort(route, attempt, effortHint, locale = "zh") {
|
|
|
3516
4295
|
}) };
|
|
3517
4296
|
return { effort: wanted };
|
|
3518
4297
|
}
|
|
3519
|
-
/** 并发池:按 max 个 worker 消费 items,返回同序结果。 */
|
|
3520
|
-
async function runPool(items, max, fn) {
|
|
3521
|
-
const results = new Array(items.length);
|
|
3522
|
-
let cursor = 0;
|
|
3523
|
-
const workers = Array.from({ length: Math.min(Math.max(1, max), items.length) }, async () => {
|
|
3524
|
-
while (cursor < items.length) {
|
|
3525
|
-
const i = cursor++;
|
|
3526
|
-
results[i] = await fn(items[i], i);
|
|
3527
|
-
}
|
|
3528
|
-
});
|
|
3529
|
-
await Promise.all(workers);
|
|
3530
|
-
return results;
|
|
3531
|
-
}
|
|
3532
4298
|
/**
|
|
3533
4299
|
* 解析父 agent 当前生效的模型路由(provider/model)。
|
|
3534
4300
|
*
|
|
@@ -3573,8 +4339,10 @@ function resolveChildRoute(parent) {
|
|
|
3573
4339
|
}
|
|
3574
4340
|
return out;
|
|
3575
4341
|
}
|
|
3576
|
-
/** 运行单个阶段子代理:执行 + 产出实质校验 + token 双口径计量 + stage 状态流转。
|
|
3577
|
-
|
|
4342
|
+
/** 运行单个阶段子代理:执行 + 产出实质校验 + token 双口径计量 + stage 状态流转。
|
|
4343
|
+
* `taskIds`:本阶段承载的**开发任务身份**(host 生成的 `dt-N`;合并任务时是数组)。
|
|
4344
|
+
* 与 `taskKey`(人读标题)分家——判定只认 id,title 只作展示(见 pipeline.buildDevTaskDefs 注释)。 */
|
|
4345
|
+
async function runAgent(journal, parent, label, phase, prompt, signal, taskKey, attempt = 1, effortHint, taskIds) {
|
|
3578
4346
|
const maxSeq = journal.stages.length ? Math.max(...journal.stages.map((s) => s.seq)) : 0;
|
|
3579
4347
|
const locale = runLocaleOf(journal);
|
|
3580
4348
|
let stageText = null;
|
|
@@ -3585,6 +4353,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3585
4353
|
status: "running",
|
|
3586
4354
|
outcome: null,
|
|
3587
4355
|
taskKey: taskKey || null,
|
|
4356
|
+
taskIds: Array.isArray(taskIds) && taskIds.length ? [...taskIds] : null,
|
|
3588
4357
|
childId: null,
|
|
3589
4358
|
startedAt: Date.now(),
|
|
3590
4359
|
endedAt: null,
|
|
@@ -3599,6 +4368,8 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3599
4368
|
let cancelGuard = null;
|
|
3600
4369
|
try {
|
|
3601
4370
|
const route = resolveChildRoute(parent);
|
|
4371
|
+
stage.provider = route.provider || providerName() || null;
|
|
4372
|
+
stage.model = route.model || null;
|
|
3602
4373
|
const eff = await resolveStageEffort(route, attempt, effortHint, locale);
|
|
3603
4374
|
const effort = eff.effort;
|
|
3604
4375
|
const agentOptions = route.provider || route.model || effort ? {
|
|
@@ -3634,10 +4405,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3634
4405
|
signal: normalizeSignal(signal)
|
|
3635
4406
|
});
|
|
3636
4407
|
stage.childId = run.id;
|
|
3637
|
-
|
|
3638
|
-
run,
|
|
3639
|
-
stage
|
|
3640
|
-
});
|
|
4408
|
+
trackInFlight(journal.id, stage, run);
|
|
3641
4409
|
try {
|
|
3642
4410
|
if (parent && parent.session && typeof parent.session.append === "function") parent.session.append("tool-workflow/agent-start", {
|
|
3643
4411
|
runId: journal.id,
|
|
@@ -3658,15 +4426,49 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3658
4426
|
const text = extractText(result && result.output);
|
|
3659
4427
|
stageText = text;
|
|
3660
4428
|
const verdict = judgeDeliverable(phase, text);
|
|
4429
|
+
let docFallback = null;
|
|
4430
|
+
if (!verdict.ok && text && stop === "completed") {
|
|
4431
|
+
const doc = stageDocText(journal, phase);
|
|
4432
|
+
if (doc && doc.length >= verdict.min) docFallback = {
|
|
4433
|
+
name: doc.name,
|
|
4434
|
+
length: doc.length
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
3661
4437
|
if (journal.cancelled) {
|
|
3662
4438
|
stage.status = "cancelled";
|
|
3663
4439
|
stage.outcome = "cancelled";
|
|
3664
4440
|
return null;
|
|
3665
4441
|
}
|
|
3666
|
-
if (
|
|
4442
|
+
if (stage.envUnavailable) {
|
|
4443
|
+
stage.status = "failed";
|
|
4444
|
+
stage.outcome = "env-unavailable";
|
|
4445
|
+
stage.summary = t(locale, "diag.envUnavailableStage", {
|
|
4446
|
+
label,
|
|
4447
|
+
detail: stage.envUnavailable
|
|
4448
|
+
});
|
|
4449
|
+
if (text) stage.output = clip(text, 4e3);
|
|
4450
|
+
journal.logs.push({
|
|
4451
|
+
t: Date.now(),
|
|
4452
|
+
level: "error",
|
|
4453
|
+
message: stage.summary
|
|
4454
|
+
});
|
|
4455
|
+
return null;
|
|
4456
|
+
}
|
|
4457
|
+
if (stop === "completed" && text && (verdict.ok || docFallback)) {
|
|
3667
4458
|
stage.status = "done";
|
|
3668
4459
|
stage.outcome = "completed";
|
|
3669
4460
|
stage.output = clip(text, 5e4);
|
|
4461
|
+
if (docFallback) journal.logs.push({
|
|
4462
|
+
t: Date.now(),
|
|
4463
|
+
level: "warn",
|
|
4464
|
+
message: t(locale, "diag.docDelivered", {
|
|
4465
|
+
label,
|
|
4466
|
+
length: verdict.length,
|
|
4467
|
+
min: verdict.min,
|
|
4468
|
+
name: docFallback.name,
|
|
4469
|
+
docLen: docFallback.length
|
|
4470
|
+
})
|
|
4471
|
+
});
|
|
3670
4472
|
if (verdict.refusal) journal.logs.push({
|
|
3671
4473
|
t: Date.now(),
|
|
3672
4474
|
level: "warn",
|
|
@@ -3755,21 +4557,47 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3755
4557
|
stage.usage = accumulateSessionUsage(run);
|
|
3756
4558
|
stage.handoff = stageText ? handoffBrief(stageText) : null;
|
|
3757
4559
|
stage.endedAt = Date.now();
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
4560
|
+
untrackInFlight(journal.id, stage);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
/**
|
|
4564
|
+
* 该阶段失败是否「外部供应商不可用」(限流/无额度/上游 5xx/超时…)。
|
|
4565
|
+
* 判据 = `classifyExternalFailure(错误细节, stage.summary)`(纯函数,单测覆盖真值表)。
|
|
4566
|
+
* ⚠️ 启发式:宿主只给 `stopReason=error` + 错误文本,无结构化错误码;命中原文进日志便于日后核对。
|
|
4567
|
+
*/
|
|
4568
|
+
function isExternalFailure(stage) {
|
|
4569
|
+
if (stage.outcome === "insubstantial" || stage.outcome === "degenerated" || stage.outcome === "stalled" || stage.outcome === "aborted" || stage.outcome === "env-unavailable") return false;
|
|
4570
|
+
return classifyExternalFailure(String(stage.summary || ""), String(stage.output || "").slice(-500)) === "external";
|
|
4571
|
+
}
|
|
4572
|
+
/** 可取消等待:等待期间被取消/中断则立刻返回 false(不把 sleep 变成不可打断的挂起)。 */
|
|
4573
|
+
async function sleepUnlessCancelled(ms, isCancelled) {
|
|
4574
|
+
const step = 1e3;
|
|
4575
|
+
let waited = 0;
|
|
4576
|
+
while (waited < ms) {
|
|
4577
|
+
if (isCancelled()) return false;
|
|
4578
|
+
const slice = Math.min(step, ms - waited);
|
|
4579
|
+
await new Promise((r) => setTimeout(r, slice));
|
|
4580
|
+
waited += slice;
|
|
3762
4581
|
}
|
|
4582
|
+
return !isCancelled();
|
|
3763
4583
|
}
|
|
3764
4584
|
/**
|
|
3765
4585
|
* 单阶段重试 + token 熔断(**新增口径**:input+cacheWrite+output,排除 cacheRead——见
|
|
3766
4586
|
* `FRESH_TOKEN_BUDGET` 与 metering.freshTokensOf;汇报仍走官方 totalTokensOf 口径)。
|
|
3767
4587
|
* 顺序:不可重试/外部中止/护栏中止 → 预算门 → 自动重试(预算合理时重试优先于熔断,2026-09-11 修正)。
|
|
3768
4588
|
* `effortHint`:机械阶段的推理强度降档提示(第 1 次尝试生效,重试自动回升 high,见 resolveStageEffort)。 */
|
|
3769
|
-
async function withRetry(journal, parent, label, phase, prompt, signal, taskKey, effortHint) {
|
|
4589
|
+
async function withRetry(journal, parent, label, phase, prompt, signal, taskKey, effortHint, taskIds) {
|
|
3770
4590
|
let attempts = 0;
|
|
3771
4591
|
let freshTokens = 0;
|
|
4592
|
+
let usageAcc = {
|
|
4593
|
+
input: 0,
|
|
4594
|
+
cacheRead: 0,
|
|
4595
|
+
cacheWrite: 0,
|
|
4596
|
+
output: 0,
|
|
4597
|
+
calls: 0
|
|
4598
|
+
};
|
|
3772
4599
|
let lastStage = null;
|
|
4600
|
+
let externalAttempts = 0;
|
|
3773
4601
|
const locale = runLocaleOf(journal);
|
|
3774
4602
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
3775
4603
|
attempts = attempt;
|
|
@@ -3779,9 +4607,19 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3779
4607
|
}) : label;
|
|
3780
4608
|
const promptNow = attempt > 1 && lastStage ? prompt + buildRetryDiagnostic(attempt, lastStage, locale) : prompt;
|
|
3781
4609
|
const beforeLen = journal.stages.length;
|
|
3782
|
-
const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey, attempt, effortHint);
|
|
4610
|
+
const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey, attempt, effortHint, taskIds);
|
|
3783
4611
|
lastStage = journal.stages[beforeLen] || null;
|
|
3784
|
-
if (lastStage && lastStage.phase === phase)
|
|
4612
|
+
if (lastStage && lastStage.phase === phase) {
|
|
4613
|
+
freshTokens += freshTokensOf(lastStage.usage);
|
|
4614
|
+
const u = lastStage.usage || {};
|
|
4615
|
+
usageAcc = {
|
|
4616
|
+
input: usageAcc.input + (u.input || 0),
|
|
4617
|
+
cacheRead: usageAcc.cacheRead + (u.cacheRead || 0),
|
|
4618
|
+
cacheWrite: usageAcc.cacheWrite + (u.cacheWrite || 0),
|
|
4619
|
+
output: usageAcc.output + (u.output || 0),
|
|
4620
|
+
calls: usageAcc.calls + (u.calls || 0)
|
|
4621
|
+
};
|
|
4622
|
+
}
|
|
3785
4623
|
if (result) return {
|
|
3786
4624
|
text: result,
|
|
3787
4625
|
attempts,
|
|
@@ -3811,6 +4649,52 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3811
4649
|
stage: lastStage
|
|
3812
4650
|
};
|
|
3813
4651
|
}
|
|
4652
|
+
if (lastStage && isExternalFailure(lastStage)) {
|
|
4653
|
+
const wait = externalBackoffMs(externalAttempts + 1);
|
|
4654
|
+
if (wait !== null) {
|
|
4655
|
+
externalAttempts++;
|
|
4656
|
+
journal.logs.push({
|
|
4657
|
+
t: Date.now(),
|
|
4658
|
+
level: "warn",
|
|
4659
|
+
message: t(locale, "diag.externalBackoff", {
|
|
4660
|
+
label,
|
|
4661
|
+
n: externalAttempts,
|
|
4662
|
+
sec: Math.round(wait / 1e3),
|
|
4663
|
+
detail: clip(String(lastStage.summary || ""), 200)
|
|
4664
|
+
})
|
|
4665
|
+
});
|
|
4666
|
+
if (!await sleepUnlessCancelled(wait, () => journal.cancelled)) return {
|
|
4667
|
+
text: null,
|
|
4668
|
+
attempts,
|
|
4669
|
+
freshTokens,
|
|
4670
|
+
stage: lastStage
|
|
4671
|
+
};
|
|
4672
|
+
attempt--;
|
|
4673
|
+
continue;
|
|
4674
|
+
}
|
|
4675
|
+
journal.logs.push({
|
|
4676
|
+
t: Date.now(),
|
|
4677
|
+
level: "error",
|
|
4678
|
+
message: t(locale, "diag.externalExhausted", {
|
|
4679
|
+
label,
|
|
4680
|
+
n: externalAttempts
|
|
4681
|
+
})
|
|
4682
|
+
});
|
|
4683
|
+
if (lastStage) {
|
|
4684
|
+
lastStage.status = "interrupted";
|
|
4685
|
+
lastStage.outcome = "external";
|
|
4686
|
+
}
|
|
4687
|
+
journal.interrupted = true;
|
|
4688
|
+
journal.interruptedAt = Date.now();
|
|
4689
|
+
journal.externalFailure = true;
|
|
4690
|
+
journal.humanIntervention = true;
|
|
4691
|
+
return {
|
|
4692
|
+
text: null,
|
|
4693
|
+
attempts,
|
|
4694
|
+
freshTokens,
|
|
4695
|
+
stage: lastStage
|
|
4696
|
+
};
|
|
4697
|
+
}
|
|
3814
4698
|
if (lastStage && lastStage.outcome === "aborted") {
|
|
3815
4699
|
journal.logs.push({
|
|
3816
4700
|
t: Date.now(),
|
|
@@ -3825,6 +4709,20 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3825
4709
|
stage: lastStage
|
|
3826
4710
|
};
|
|
3827
4711
|
}
|
|
4712
|
+
if (lastStage && lastStage.outcome === "env-unavailable") {
|
|
4713
|
+
journal.logs.push({
|
|
4714
|
+
t: Date.now(),
|
|
4715
|
+
level: "error",
|
|
4716
|
+
message: t(locale, "diag.envUnavailable", { label })
|
|
4717
|
+
});
|
|
4718
|
+
journal.humanIntervention = true;
|
|
4719
|
+
return {
|
|
4720
|
+
text: null,
|
|
4721
|
+
attempts,
|
|
4722
|
+
freshTokens,
|
|
4723
|
+
stage: lastStage
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
3828
4726
|
if (lastStage && lastStage.outcome === "degenerated") {
|
|
3829
4727
|
journal.logs.push({
|
|
3830
4728
|
t: Date.now(),
|
|
@@ -3853,15 +4751,19 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
3853
4751
|
stage: lastStage
|
|
3854
4752
|
};
|
|
3855
4753
|
}
|
|
3856
|
-
|
|
4754
|
+
const eff = effectiveFreshBudget(usageAcc, FRESH_TOKEN_BUDGET);
|
|
4755
|
+
if (freshTokens >= eff.budget) {
|
|
4756
|
+
const params = {
|
|
4757
|
+
label,
|
|
4758
|
+
fresh: Math.round(freshTokens / 1e3),
|
|
4759
|
+
budget: Math.round(eff.budget / 1e3),
|
|
4760
|
+
calls: eff.calls,
|
|
4761
|
+
ratio: Math.round(eff.ratio * 100)
|
|
4762
|
+
};
|
|
3857
4763
|
journal.logs.push({
|
|
3858
4764
|
t: Date.now(),
|
|
3859
4765
|
level: "error",
|
|
3860
|
-
message: t(locale, "diag.breaker",
|
|
3861
|
-
label,
|
|
3862
|
-
fresh: Math.round(freshTokens / 1e3),
|
|
3863
|
-
budget: Math.round(FRESH_TOKEN_BUDGET / 1e3)
|
|
3864
|
-
})
|
|
4766
|
+
message: t(locale, eff.uncached ? "diag.breakerUncached" : "diag.breaker", params)
|
|
3865
4767
|
});
|
|
3866
4768
|
journal.humanIntervention = true;
|
|
3867
4769
|
return {
|
|
@@ -4154,6 +5056,7 @@ Before starting: read ${base}/AGENTS.md (team rules & doc index — read the sum
|
|
|
4154
5056
|
[AGENTS.md boundary · policy] AGENTS.md is team property (injected unconditionally — consensus/index/managed zone only): **do NOT append changelog-style sections during iterations (product memory / todos / change log)** — such data belongs in ${TF_DOCS}/memory.md and task folders; besides the <!-- teamflow:begin/end --> managed zone, no stage may rewrite, reorder, or overwrite any other part of AGENTS.md.
|
|
4155
5057
|
[Reply language · policy] Write your **final reply** (the summary handed back to the host, plus the state block) in ${replyName} — the artifact language above governs the FILES you write, this governs the TEXT you write back. Never mix languages inside one run: your reply becomes this stage's stored result and is shown in the workbench.
|
|
4156
5058
|
Backlog (req/task/bug) source of truth is the persisted mirror $DSH_HOME/teamflow/<workspace>/ under ${base}/backlog/: single rotating task card model (${t(locale, "doc.statusChain")}); devAssign/qaAssign live on the task card.
|
|
5059
|
+
[Env unavailable · policy] Command tools run inside the host's sandbox. If the SAME command keeps failing with the SAME error (sandbox/ACL denial, permission, not found, unavailable), **STOP at once**: do NOT retry it, do NOT switch to another command to work around it, and do NOT replace execution with reasoning or guesswork. Reply with the tool name, the RAW error text, what you finished and what is left undone — then end the turn. The host aborts a stage after repeated identical failures and names the environment as the cause; reporting it yourself is far cheaper and keeps the real reason visible.
|
|
4157
5060
|
`;
|
|
4158
5061
|
}
|
|
4159
5062
|
/** 头尾组合切片:保留头部(背景/基线) + 尾部(新增 AC/修订),预算不变但覆盖增量段。 */
|
|
@@ -4212,16 +5115,59 @@ const ONCE_DISCIPLINE = `[ONE-SHOT WRITE · policy] The most important efficienc
|
|
|
4212
5115
|
- Think it through once, then write. After writing, move on — do not polish retroactively.
|
|
4213
5116
|
- Always end output with a state block so the host can index and the next run needn't re-read.
|
|
4214
5117
|
`;
|
|
5118
|
+
/**
|
|
5119
|
+
* **本机安装环境注入块**(2026-09-21 用户实锤,勿写死路径)。
|
|
5120
|
+
*
|
|
5121
|
+
* 契约原来写死 `dsh plugin --profile web add`——用户是源码运行(`pnpm dsh`,`dsh` 不在 PATH),
|
|
5122
|
+
* 那条命令在他机器上跑不通;profile 名也可能不是 `web`。现在路径/命令**全部由 host 运行时探测**
|
|
5123
|
+
* (`$DSH_HOME` + 插件自身所在路径反推 profile + `dsh` 是否在 PATH),这里只把结论转述给 PM。
|
|
5124
|
+
* 探测失败 → 明确要求"问用户",**不许猜**。
|
|
5125
|
+
* 同时点明**执行者**:流水线子代理权限被钉死在工作区、**装不了**(会话原文:
|
|
5126
|
+
* `permission scope was fixed ... cannot be widened`);**主 agent 能**——它的 profile 写入被拒后
|
|
5127
|
+
* 宿主给出 `escalation available`,`approval/policy: ask` 下经用户批准即可。故 PRD 必须把「安装」
|
|
5128
|
+
* 写成一个**给主 agent 执行的步骤**,而不是"请用户手动测试"。
|
|
5129
|
+
*/
|
|
5130
|
+
function installBlock(en, rc) {
|
|
5131
|
+
const env = rc && rc.installEnv;
|
|
5132
|
+
if (!env) return "";
|
|
5133
|
+
const ok = env.ok === true;
|
|
5134
|
+
const p = env.profile || "?";
|
|
5135
|
+
const dir = env.profileDir || "?";
|
|
5136
|
+
const cli = env.cliOnPath === true;
|
|
5137
|
+
if (en) return ok ? `\n [THIS MACHINE · install environment, probed at run start — never assume a profile name or path] DSH_HOME=\`${env.dshHome}\`; profile=\`${p}\`; profile dir=\`${dir}\`; \`dsh\` on PATH: ${cli ? "yes (use the CLI)" : "NO (source-run — use the equivalent manual steps)"}.\n **Who installs**: pipeline subagents CANNOT write outside the workspace (their permission scope is fixed) — so the PRD must specify the install as a step **for the main agent to execute** (it can request a one-shot escalation, which the user approves), not as "please test it manually". Write the concrete command for THIS machine (CLI form, or \`pnpm add\` inside the profile dir + the \`dsh.profile.bundles\` entry, which is auto-derived from the package's \`dsh.bundle.patch\`), plus the exact rollback.` : `\n [THIS MACHINE · install environment] Could NOT be probed (DSH_HOME / profile name unavailable) → the PRD must instruct the main agent to **ASK THE USER** for the profile location; never invent a path. Subagents cannot install (their permission scope is fixed); the install step is for the main agent.`;
|
|
5138
|
+
return ok ? `\n 【本机环境 · 起跑时探测,禁止假设 profile 名或路径】DSH_HOME=\`${env.dshHome}\`;profile=\`${p}\`;profile 目录=\`${dir}\`;\`dsh\` 在 PATH:${cli ? "是(用 CLI)" : "**否**(源码运行 → 走等价手动步骤)"}。\n **谁执行安装**:流水线子代理**写不了**工作区之外(权限启动即固定)——所以 PRD 必须把安装写成**给主 agent 执行的步骤**(主 agent 可申请一次性升级授权,由用户批准),而不是"请用户手动测试"。请按**本机**实际情况给出可照做的命令(CLI 形式,或在 profile 目录内 \`pnpm add\` + \`dsh.profile.bundles\` 条目——后者由包的 \`dsh.bundle.patch\` 声明自动推导),并给出精确回滚。` : `\n 【本机环境】探测失败(DSH_HOME / profile 名不可得)→ PRD 必须指示主 agent **先问用户** profile 位置,**绝不许编路径**。子代理装不了(权限固定);安装步骤归主 agent。`;
|
|
5139
|
+
}
|
|
4215
5140
|
const prdPrompt = (requirement, root, runId, state) => {
|
|
4216
5141
|
const en = LOCALE(state) === "en";
|
|
4217
|
-
/** PRD 头部声明模板:zh 逐字不变;en 为新增英文契约(仅 en 分支出现,AC-4③)。 */
|
|
4218
5142
|
const hdrBaseline = en ? "`Baseline dependency: <prior task folder this requirement depends on> (its established behavior must not regress)`; write `Baseline dependency: none` if no dependency." : "`基线依赖:<prior task folder this requirement depends on>(its established behavior must not regress)`; write `基线依赖:无` if no dependency.";
|
|
4219
5143
|
const hdrSupersede = en ? "`Supersedes: <task folder>#<AC number>: <one sentence>` (only when this requirement explicitly changes existing behavior; omit otherwise)." : "`取代:<task folder>#<AC number>:<one sentence>` (only when this requirement explicitly changes existing behavior; omit otherwise).";
|
|
5144
|
+
const contract = (() => {
|
|
5145
|
+
try {
|
|
5146
|
+
const rc = state && state.__runCtx || {};
|
|
5147
|
+
const items = Array.isArray(rc.artifactContracts) ? rc.artifactContracts : [];
|
|
5148
|
+
if (!items.length) return "";
|
|
5149
|
+
const kind = String(rc.artifact || "other");
|
|
5150
|
+
const inst = rc.installable === true;
|
|
5151
|
+
const lines = items.map((it, i) => ` ${i + 1}. ${it.requirement} — ${it.criteria}`).join("\n");
|
|
5152
|
+
const repo = (ARTIFACT_REFERENCE_SAMPLES[kind] || []).join(en ? ", " : "、");
|
|
5153
|
+
const where = en ? `**first look at the dsh plugins already installed on THIS machine** — \`${LOCAL_PLUGIN_SAMPLES_HINT}\`: their \`package.json\` (\`dsh\` block) and \`cordis.patch.yml\` are the authoritative, version-current samples of how the declarations are really written${repo ? `; if the workspace happens to sit inside this repo you may also read \`${repo}\`` : ""}; if neither exists, read the host docs or ask the user — **never write them from memory**` : `**先读本机已安装的 dsh 插件**——\`${LOCAL_PLUGIN_SAMPLES_HINT}\`:它们的 \`package.json\`(\`dsh\` 块)与 \`cordis.patch.yml\` 就是"声明到底怎么写"的**权威且与宿主版本同步**的样本${repo ? `;若工作区恰好在本仓内,也可就近读 \`${repo}\`` : ""};两者都没有就去读宿主文档或问用户——**禁止凭记忆写**`;
|
|
5154
|
+
return en ? `\n[DELIVERABLE SHAPE · mandatory ACs] Triage judged this deliverable as \`${kind}\`${inst ? " and it must be **installable/loadable by its host**" : ""}. The following are **objective delivery contracts of that shape** — every item MUST become a testable AC in this PRD (not prose, not a "notes" section), because downstream QA/acceptance only verify what is in the AC table:\n${lines}\n Field names / file names vary with the host version, so ${where}.${installBlock(en, rc)}` : `\n[交付形态契约 · 必填 AC] 分诊判定本次交付物形态为 \`${kind}\`${inst ? ",且**必须可被宿主安装/加载**" : ""}。以下是该形态的**客观交付契约**——每一条都**必须落成 PRD 里可测的 AC**(不是正文说明、不是"备注"小节),因为下游 QA/验收只验 AC 表里的东西:\n${lines}\n 字段名/文件名随宿主版本演进,所以${where}。${installBlock(en, rc)}`;
|
|
5155
|
+
} catch (e) {
|
|
5156
|
+
return "";
|
|
5157
|
+
}
|
|
5158
|
+
})();
|
|
5159
|
+
const hostResearchBlock = (() => {
|
|
5160
|
+
const rc = state && state.__runCtx || {};
|
|
5161
|
+
if (rc.hostResearch !== true) return "";
|
|
5162
|
+
const host = String(rc.host || "unknown");
|
|
5163
|
+
const kind = String(rc.artifact || "other");
|
|
5164
|
+
return en ? `\n[HOST CONTRACT RESEARCH · mandatory section] Triage judged this deliverable as \`${kind}\` targeting a host that is **NOT this project's own host** (\`${host}\`). The hard contracts of this repo's host **do not apply** to it — do NOT carry over its profile entry declaration, bundle patch, client block or \`files\` whitelist. Instead, FIRST research the target host's own plugin/extension loading contract and write the findings into a dedicated section titled exactly "Host contract research" containing, at minimum: (a) which host framework and version this targets; (b) where its plugin/extension loading contract is documented (URL or file path you actually read); (c) the concrete requirements it imposes (entry declaration, packaging, install/load command, any safety or rollback requirement); (d) anything you could NOT verify and how the dev stage should resolve it. **Never invent field names from memory** — cite what you read. This section is a hard gate: a PRD without it fails the stage.` : `\n[宿主契约调研 · 必填段] 分诊判定本次交付物形态为 \`${kind}\`,其目标宿主**不是本项目自身的宿主**(\`${host}\`)。本仓宿主的那些硬契约(profile 层入口声明 / bundle patch / client 声明块 / files 白名单)**对它一条都不适用**,不得照搬。正确做法:**先去调研目标宿主自己的插件/扩展加载契约**,把结论写进一个标题恰为「宿主契约调研」的独立段,至少包含:(a) 目标是哪个宿主框架及其版本;(b) 它的插件/扩展加载契约**在哪儿有据可查**(你真读过的 URL 或文件路径);(c) 它实际要求什么(入口声明 / 打包 / 安装加载命令 / 有无安全与回滚要求);(d) 你**没能核实**的点,以及 dev 阶段该怎么解决。**禁止凭记忆写字段名**——写明你读过什么。这一段是硬门禁:PRD 缺它 → 阶段失败。`;
|
|
5165
|
+
})();
|
|
4220
5166
|
return `You are a senior Product Manager. The current workspace IS the target project (empty = project not yet created).
|
|
4221
5167
|
${productCtx(root, LOCALE(state))}${stateSliceFor(state, "pm")}
|
|
4222
5168
|
${ONCE_DISCIPLINE}[REQUIREMENT]
|
|
4223
5169
|
${requirement}
|
|
4224
|
-
[ARTIFACT LOCATION] ${RUN(state)}/PRD.md (write once; create dirs if missing)
|
|
5170
|
+
[ARTIFACT LOCATION] ${RUN(state)}/PRD.md (write once; create dirs if missing).${contract}${hostResearchBlock}
|
|
4225
5171
|
[REQUIREMENTS]
|
|
4226
5172
|
1. First look at the state index above and the AGENTS.md doc index to decide whether this is an iterative requirement and which prior task folders relate (folder names carry date+theme; reverse date order = evolution). Do not full-read historical docs.
|
|
4227
5173
|
2. [AC numbering] Number ACs from AC-1 within THIS folder only — ACs belong to this requirement, no global numbering.
|
|
@@ -4232,8 +5178,10 @@ ${requirement}
|
|
|
4232
5178
|
4. Output the full PRD (${langDirective(LOCALE(state))}): background & goals, user stories (each with testable acceptance criteria), scope & non-goals, interaction flow summary, priority (P0/P1/P2), dependencies & risks, milestone suggestions. ACs must be testable/quantifiable; prefer precision & brevity. ${L(state, "doc.noRevisionTable")} (the folder IS the archive; its name carries the identity).
|
|
4233
5179
|
5. [Memory write-back · convention changes ONLY] Update docs/teamflow/memory.md ONLY if this requirement introduces new team conventions / tech-stack decisions (replace the same-topic line, idempotent, no changelog-style appending); otherwise do not touch memory.
|
|
4234
5180
|
6. [Engineering actions carried verbatim] Engineering instructions in the raw requirement (create/switch branch, commit, tag...) MUST be preserved verbatim into the "${L(state, "doc.engConstraints")}" section of the PRD: specify the action, timing, and baseline (e.g. "branch from latest main, then implement"). If the workspace already has uncommitted changes, note how to handle them. Never silently drop or reword engineering instructions.
|
|
5181
|
+
6b. [Version-control hygiene · mandatory when the workspace is versioned] If the workspace has (or will get, per the host log "改动存档/git init") version control: audit the existing/potential \`.gitignore\` **for THIS project's stack** — build outputs, dependency/tool caches, IDE files, local env secrets — and put "complete/extend .gitignore" into the engineering-constraints section as a dev-stage action (file names must match this project's real tooling: a Python project needs __pycache__/.venv, a Rust one target/, a pnpm monorepo .pnpm-store — read the project, don't guess from a generic list). Rationale: the closing commit stages the whole tree; noise that slips into it becomes permanent history.
|
|
4235
5182
|
${ARTIFACT_DELIVERY(RUN(state))}
|
|
4236
|
-
7. [State] End with a state block (phase="prd"): summary covers the AC highlights + one-sentence product semantics; extra contains { "acIndex": {...}, "summary": "<product one-liner>", "techStack": "..." }.${STATE_BLOCK_INSTRUCTION}
|
|
5183
|
+
7. [State] End with a state block (phase="prd"): summary covers the AC highlights + one-sentence product semantics; extra contains { "acIndex": {...}, "summary": "<product one-liner>", "techStack": "...", "openQuestions": [{ "q": "...", "why": "...", "changes": "...", "default": "..." }] }.${STATE_BLOCK_INSTRUCTION}
|
|
5184
|
+
8. [Assumptions · mandatory] If ANY part of the requirement is under-specified, do NOT silently decide on the user's behalf: write a dedicated section titled exactly "${L(state, "doc.assumptionsQ")}" and list every assumption / open question as one bullet — what you assumed, why, and what would change if the user decides otherwise. If nothing is under-specified, still write the section with a single line stating there are no open questions. This section is what lets a human tell whether the PRD is what they actually wanted; the host surfaces it to the user verbatim.`;
|
|
4237
5185
|
};
|
|
4238
5186
|
const designPrompt = (prd, root, runId, state) => `You are a senior UI/UX designer. The current workspace IS the target project.
|
|
4239
5187
|
${productCtx(root, LOCALE(state))}${stateSliceFor(state, "design")}
|
|
@@ -4359,6 +5307,11 @@ ${clip(devSummary, 15e3)}
|
|
|
4359
5307
|
- If the injected blueprint JSON ("<!-- blueprint -->") is present, verify the implementation follows it (was the to-be-extracted module extracted? deps/assembly per blueprint? any deviations?).
|
|
4360
5308
|
- Check for **duplicated implementations** (e.g. multiple security wrappers/storage/adapter utilities drifting), **abstraction not extracted where it should be**, **obviously broken existing structure**.
|
|
4361
5309
|
- Report architecture findings in the defect table format (severity P1, module =${L(state, "doc.archModuleQ")}). This is part of the delivery quality gate, not just functional bugs.
|
|
5310
|
+
0b. [Deliverable-shape verification · mandatory when injected] If the state slice carries ${LOCALE(state) === "en" ? "\"Deliverable-shape contracts\"" : "「交付形态契约」"}, treat every item as a **required probe** (this is exactly the class of failure where a delivery "looks complete" but cannot be installed/loaded, or a stale build artifact crashes the host on startup):
|
|
5311
|
+
- Run each executable criterion and record command + exit code in QA-REPORT.md (sandbox-legal ones: file/field presence, dependency protocol scan, build-freshness, load-safety via \`node -e "require(...)"\`).
|
|
5312
|
+
- **Install rollback discipline**: before any profile/publish install, write down the exact uninstall command (e.g. remove the package from profile deps + bundles, or \`dsh plugin remove <name>\`); if a post-install verification fails, roll back FIRST, then report — never leave the host unbootable.
|
|
5313
|
+
- Environment-blocked items (e.g. needs a host restart to verify real loading) → list them in the ${L(state, "doc.manualChecklistQ")} with method + tool, for human review. Never silently skip.
|
|
5314
|
+
0c. [Commit-surface hygiene probe · when the workspace is versioned] If version control is in play (host log mentions 改动存档/git, or a .gitignore exists): before signing off, run \`git status --porcelain\` and verify it contains **no dependency dirs, build outputs, tool caches, IDE files or local secrets** (per this project's stack — e.g. node_modules/, .pnpm-store/, __pycache__/, target/, dist/, .idea/, .env). Missing/incorrect .gitignore coverage → file it as a P1 defect (module = 版本控制), because the closing commit stages the whole tree and noise becomes permanent history.
|
|
4362
5315
|
1. [Environment limits · dynamic by model capability]${VISUAL_POLICY(!!vision, LOCALE(state))}
|
|
4363
5316
|
- Always-available sandbox-legal paths: build/assembly checks, unit tests, DOM-level E2E (jsdom or equivalent), static audit, adversarial spot-checks.
|
|
4364
5317
|
2. [${t(LOCALE(state), "doc.manualChecklist")}] Items that cannot be auto-verified (audio output / real-device: 100dvh dynamic toolbar, safe-area, multi-touch / FPS performance / screen-reader): do NOT fail them — instead list each in the report's ${L(state, "doc.manualChecklistQ")} section (acceptance criteria + method + tool), note ${L(state, "doc.envLimitQ")}, for human review.
|
|
@@ -4478,9 +5431,14 @@ ${langNote}
|
|
|
4478
5431
|
3. hotfix/single-point/pure numeric/pure docs → patch; clear "add feature X" → pick lite/medium/full by size.
|
|
4479
5432
|
4. Focused change (even with tests/regression) → lite/tech by nature; not necessarily full.
|
|
4480
5433
|
5. [M1 ARCHITECTURE CRITERION (important)] **Architecture-level changes** — persistence/localStorage/database/standalone module/abstraction/cross-many-files without an existing reusable wrapper (like a localStorage wrapper, storage layer, state management) — even if they look like "small features", go **at least medium** (must pass the architecture stage and produce a blueprint, avoiding scattered local implementations by dev); such changes collapse under a light "micro feature" tier. Tech-driven rework (refactor/optimize/arch upgrade) is itself tech (tech also runs the lightweight blueprint now).${enExamples}
|
|
5434
|
+
6. [INTENT — decide before mode] \`intent\` = \`"requirement"\` **only** when this is a settled development ask. Use \`"exploration"\` for still-thinking-out-loud phrasing ("I've been wondering about adding X", "test this out", "I want to build some kind of plugin") and \`"feedback"\` for opinions/questions about existing behavior — **neither may start a pipeline**; the caller will ask the user first. When unsure between requirement and exploration, prefer \`"exploration"\` (a wasted prompt is cheaper than a wasted pipeline).
|
|
5435
|
+
7. [BLOCKERS — must-know gaps only] \`blockers\` = what you **cannot** settle yourself from the repo/state index **and** whose wrong guess causes rework. Each entry needs all five fields: \`settles\` (\`"installable"\` | \`"artifact"\` | \`"host"\` | \`"scope"\` | \`"ui"\` | \`"data"\` | \`"other"\` — **which verdict field the answer will decide**; use \`"other"\` when it decides no field), \`question\` (one sentence to ask the user), \`readings\` (≥2 concrete **competing** interpretations), \`changes\` (which artifact / AC / scope it changes), \`rework\` (what gets redone if guessed wrong). Anything you can self-check, or whose wrong guess costs nothing, or that has only one sensible reading → **do not list**. No such gap → \`[]\`. Never invent questions to look thorough: unqualified entries are dropped by the caller and counted against you. **Never file a gap your own verdict already answers** — the caller checks \`settles\` against your own fields and drops contradictions: with \`installable: true\` (or a requirement that already states the delivery form) the delivery-form blocker is self-contradictory and must not appear; with a settled \`host\` the "which host" blocker is likewise dropped. Real case: a requirement ending in "${en ? "install and actually work in my dsh web profile" : "装进我的 dsh web profile 里真实可用"}" still produced that blocker → one wasted clarification round (3 questions to the user) plus a second triage call for the same requirement.
|
|
5436
|
+
**One gap is must-ask whenever it applies**: a **new deliverable** (\`artifact\` ∈ plugin-host / plugin-client / plugin-full / cli / lib) whose **delivery form** the requirement does not state — must it actually be **installed/published** (真能被宿主装入 / 发 npm), or is "source in the repo" enough? These give different contract sets and different ACs, and guessing wrong means redoing the packaging work at the very end (real case: a plugin shipped without its host-load entry file and bundle declaration, because "can it be installed" was never settled). Ask it as ONE blocker (\`settles: "installable"\`) with the concrete readings (e.g. "installable into the profile & verified by a real load" vs "source-only, no packaging"), then set \`installable\` from the answer. **Do not** ask it when the requirement already states the form, or when the repo already settles it (existing conventions/scripts/docs), or when you have already set \`installable: true\`.
|
|
5437
|
+
8. [ARTIFACT — what kind of deliverable, and does it have to install] \`artifact\` describes the **deliverable's shape**, which decides which hard contracts the PRD must turn into ACs: \`"app"\` (end-user application) / \`"plugin-host"\` (host-side plugin: service/tools/events) / \`"plugin-client"\` (browser-side UI plugin) / \`"plugin-full"\` (both halves) / \`"cli"\` / \`"lib"\` (library/module) / \`"docs"\` / \`"data"\` / \`"other"\` (a change inside an existing product rather than a new deliverable). \`installable\` = does "done" mean the artifact must be **installable/loadable by its host** (e.g. a plugin that must actually load in a profile) rather than merely "source in a directory"? Judge from the requirement's own words ("做插件""能装上""发布") plus the repo's conventions — do NOT guess \`true\` for ordinary in-repo changes. Wrong shape is expensive: a plugin that "looks complete" but cannot be loaded fails at the very end (real case: a plugin shipped without its profile-load entry file and bundle declaration passed every functional AC because "can it be loaded" was never an AC).
|
|
5438
|
+
9. [HOST — which framework will load this deliverable] \`host\` says **whose plugin/extension mechanism this deliverable must satisfy**; it is a DIFFERENT axis from \`artifact\` and it decides whether this project's own hard contracts even apply: \`"dsh"\` = this DeepSeek-Harness host (the project this pipeline lives in — profile entry declaration, bundle patch, \`dsh.client\` block, \`files\` whitelist); \`"other"\` = a **different** host framework (openclaw / hermes-agent / pi-agent / any other agent framework with its own plugin or extension API); \`"unknown"\` = you cannot tell from the requirement and the repo. ${en ? "A plugin built for another host must NOT be given this host's contracts — those mechanisms do not exist there, and applying them causes rework in the wrong direction." : ""} For \`"other"\`/\`"unknown"\` with a plugin-shaped deliverable, the PRD is additionally required to carry a **host-contract research** section (which host, where its plugin/extension loading contract is documented, what it actually requires) — a run without it fails the PRD stage. If the host is a **new deliverable for an unspecified host** and getting it wrong would rework everything, file it as a blocker with \`settles: "host"\`.
|
|
4481
5439
|
|
|
4482
5440
|
[OUTPUT] JSON object ONLY — no commentary, no preface, no closing text. The FIRST character of your reply must be '{'. Do NOT say anything like "here is the JSON" or "Let me output the JSON" — output the object itself:
|
|
4483
|
-
{ "mode": "patch|lite|tech|medium|full", "slug": "<topic words> (3-24 lowercase letters/digits/hyphens, e.g. wallkick-toggle, 7bag-random; used to name the task folder)", "kind": "one-word nature", "needDesign": true|false, "complexity": "small|medium|large", "rationale": ["key argument 1","key argument 2"], "confidence": "high|medium|low" }`;
|
|
5441
|
+
{ "mode": "patch|lite|tech|medium|full", "slug": "<topic words> (3-24 lowercase letters/digits/hyphens, e.g. wallkick-toggle, 7bag-random; used to name the task folder)", "kind": "one-word nature", "needDesign": true|false, "complexity": "small|medium|large", "rationale": ["key argument 1","key argument 2"], "confidence": "high|medium|low", "intent": "requirement|exploration|feedback", "artifact": "app|plugin-host|plugin-client|plugin-full|cli|lib|docs|data|other", "installable": true|false, "host": "dsh|other|unknown", "blockers": [{ "settles": "installable|artifact|host|scope|ui|data|other", "question": "...", "readings": ["competing reading A","competing reading B"], "changes": "which artifact/AC/scope it changes", "rework": "what gets redone if guessed wrong" }] }`;
|
|
4484
5442
|
};
|
|
4485
5443
|
/** tech 档 PRD:技术变更单(无功能 AC,重范围/目标/改动面/回归)。 */
|
|
4486
5444
|
const techChangePrompt = (requirement, root, runId, state) => `You are the senior tech lead. The current workspace IS the target project. This is a **tech-driven rework** requirement (refactor/optimize/upgrade/architecture/dependencies/tech debt) — the product doesn't need a full feature PRD, but needs a ${L(state, "doc.techChangeB")} (tech change sheet) as the contract for dev/QA/acceptance and memory write-back.
|
|
@@ -4491,7 +5449,7 @@ ${requirement}
|
|
|
4491
5449
|
1. Produce the ${L(state, "doc.techChangeQ")} (Markdown), write to ${RUN(state)}/TECH-CHANGE.md — **do NOT rewrite any functional ACs in prior task-folder PRDs** (tech-driven rework adds no user-visible acceptance items in principle; if there IS a sliver of user-visible behavior change, state it explicitly in that section).
|
|
4492
5450
|
2. Change sheet content: background & goal (one sentence), impact scope (files/modules), tech approach (key points), behavior-compatibility impact (any user-visible change), regression & verification plan (which verify commands, regression floor), risks & rollback.
|
|
4493
5451
|
3. Sync the change's key points into docs/teamflow/memory.md (only when new conventions/todos change; same-topic line replace, idempotent); don't touch AGENTS.md beyond the teamflow managed zone.
|
|
4494
|
-
4. Tight (this is a contract for dev/QA, ≤120 lines), ${langDirective(LOCALE(state))}. [Boundary] only under ${TF_DOCS}/.
|
|
5452
|
+
4. Tight (this is a contract for dev/QA, ≤120 lines), ${langDirective(LOCALE(state))}. [Boundary] only under ${TF_DOCS}/. Also add a short "${L(state, "doc.assumptionsQ")}" section: every place the requirement was under-specified and you decided for the user, with what would change if they decide otherwise (single line if none).
|
|
4495
5453
|
5. [State] End with a state block (phase="tech"), extra = { "verifyScripts": [...], "scopedFiles": [...] }.${STATE_BLOCK_INSTRUCTION}`;
|
|
4496
5454
|
/** patch 档 PRD:单点修复快速确认(不产 PRD 文档)。 */
|
|
4497
5455
|
const patchConfirmPrompt = (requirement, root, runId, state) => `You are a senior engineer. The current workspace IS the target project. This is a **hotfix / single-point fix** requirement — no full PRD, just a short confirmation (≤40 lines).
|
|
@@ -4501,7 +5459,7 @@ ${requirement}
|
|
|
4501
5459
|
[REQUIREMENTS]
|
|
4502
5460
|
1. Judge whether it truly is a single-point/hotfix: yes → output the ${L(state, "doc.confirmSheetQ")} (confirmation sheet); no → explicitly say "suggest upgrading pipeline mode (e.g. tech/lite/full)", don't force it.
|
|
4503
5461
|
2. [Requirement vs reality] **First verify the requirement description matches the workspace reality**: matches → produce the sheet per the outline below; mismatches → explicitly note ${L(state, "doc.mismatchNoteQ")} in the sheet, **no fabricated changes**.
|
|
4504
|
-
3. Sheet content: fix point (file/location), change outline, regression impact (tiny / which verify commands to run), whether to bump version along the way.
|
|
5462
|
+
3. Sheet content: fix point (file/location), change outline, regression impact (tiny / which verify commands to run), whether to bump version along the way, plus a one-line "${L(state, "doc.assumptionsQ")}" note (what you had to assume; "none" if nothing).
|
|
4505
5463
|
4. Output the confirmation sheet text ONLY (${langDirective(LOCALE(state))}) — **do not touch any product docs** (no PRD this time; memory write-back belongs to acceptance stage).
|
|
4506
5464
|
5. [State] End with a state block (phase="patch"), summary = confirmation conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
4507
5465
|
//#endregion
|
|
@@ -4673,14 +5631,345 @@ function normalizeMode(v) {
|
|
|
4673
5631
|
if (PIPELINE_MODES.indexOf(v) !== -1) return v;
|
|
4674
5632
|
return null;
|
|
4675
5633
|
}
|
|
5634
|
+
const ARTIFACT_KINDS = [
|
|
5635
|
+
"app",
|
|
5636
|
+
"plugin-host",
|
|
5637
|
+
"plugin-client",
|
|
5638
|
+
"plugin-full",
|
|
5639
|
+
"cli",
|
|
5640
|
+
"lib",
|
|
5641
|
+
"docs",
|
|
5642
|
+
"data",
|
|
5643
|
+
"other"
|
|
5644
|
+
];
|
|
5645
|
+
/** 形态归一:非法/缺失一律 `other`(绝不因模型没给字段就套用某类契约)。 */
|
|
5646
|
+
const normalizeArtifact = (raw) => ARTIFACT_KINDS.indexOf(raw) !== -1 ? raw : "other";
|
|
5647
|
+
const ARTIFACT_HOSTS = [
|
|
5648
|
+
"dsh",
|
|
5649
|
+
"other",
|
|
5650
|
+
"unknown"
|
|
5651
|
+
];
|
|
5652
|
+
/** 宿主归一:非法/缺失一律 `unknown`(= 去调研,绝不默认成 dsh 而套错契约)。 */
|
|
5653
|
+
const normalizeHost = (raw) => ARTIFACT_HOSTS.indexOf(raw) !== -1 ? raw : "unknown";
|
|
5654
|
+
/**
|
|
5655
|
+
* dsh 宿主的**确定性标识词**(护栏用;不是"自然语言理解",是防模型漏判丢契约)。
|
|
5656
|
+
*
|
|
5657
|
+
* 只在模型判 `unknown` 时才升级为 `dsh`——**不覆盖 `other`**:模型明确判了别的宿主就尊重它,
|
|
5658
|
+
* 否则"开发一个 openclaw 插件,不要 dsh 那套"会被这条正则强行拖回 dsh(拿词表当身份,本项目已两次踩过)。
|
|
5659
|
+
*/
|
|
5660
|
+
const DSH_HOST_SIGNAL = /\bdsh\b|@deepseek-ai\/dsh|deepseek[- ]harness|cordis/i;
|
|
5661
|
+
/** 宿主护栏:模型没判出来(`unknown`)而需求里有 dsh 标识词 → 判 `dsh`(否则整条 dsh 契约静默丢失)。 */
|
|
5662
|
+
function forceHost(requirement, judged) {
|
|
5663
|
+
if (judged === "dsh") return "dsh";
|
|
5664
|
+
if (judged === "unknown" && DSH_HOST_SIGNAL.test(String(requirement || ""))) return "dsh";
|
|
5665
|
+
return judged;
|
|
5666
|
+
}
|
|
5667
|
+
/** `plugin-*` = **要被宿主加载的交付物**(只有这一类才谈"宿主契约";cli/lib/app 与宿主加载无关)。 */
|
|
5668
|
+
const PLUGIN_ARTIFACTS = [
|
|
5669
|
+
"plugin-host",
|
|
5670
|
+
"plugin-client",
|
|
5671
|
+
"plugin-full"
|
|
5672
|
+
];
|
|
5673
|
+
/** 该交付物是否必须走「宿主契约调研」(= 要被宿主加载、但宿主不是我们有权威的 dsh)。 */
|
|
5674
|
+
function hostResearchRequired(host, kind) {
|
|
5675
|
+
return host !== "dsh" && PLUGIN_ARTIFACTS.indexOf(kind) !== -1;
|
|
5676
|
+
}
|
|
5677
|
+
/**
|
|
5678
|
+
* 按(宿主 × 形态)取契约 —— **唯一的契约取用入口**(pipeline 调它,不再直接调 `artifactContractsFor`)。
|
|
5679
|
+
*
|
|
5680
|
+
* 规则:`plugin-*` 且 `host !== 'dsh'` → **不下发任何 dsh 契约**,改为要求「宿主契约调研」
|
|
5681
|
+
* (`hostResearch: true`,PRD 必须含对应段落,硬门禁在 pipeline)。其余情况照旧取形态契约
|
|
5682
|
+
* (`cli`/`lib` 的"有 bin / 有 main 且可 import"是通用判据,与宿主无关,不误伤)。
|
|
5683
|
+
*/
|
|
5684
|
+
function contractsForDeliverable(host, kind, installable) {
|
|
5685
|
+
if (hostResearchRequired(host, kind)) return {
|
|
5686
|
+
items: [],
|
|
5687
|
+
hostResearch: true
|
|
5688
|
+
};
|
|
5689
|
+
return {
|
|
5690
|
+
items: artifactContractsFor(kind, installable),
|
|
5691
|
+
hostResearch: false
|
|
5692
|
+
};
|
|
5693
|
+
}
|
|
5694
|
+
/**
|
|
5695
|
+
* **本机已安装的 dsh 插件** = 任何 dsh 插件开发者机器上**必然存在**的样本来源(首选项)。
|
|
5696
|
+
*
|
|
5697
|
+
* 由来(2026-09-21 用户实锤追问「指向的是不是都是我本地的目录,那如果用户没有的话会导致什么结果」):
|
|
5698
|
+
* 旧实现在 prompt 里写「去读**本仓**同类插件样本(`plugins/dsh-plugin-teamflow`)」——那是**本仓相对路径**:
|
|
5699
|
+
* - **只在工作区位于我们仓库的 `plugins/` 下时成立**(probe-v2 恰好如此,于是"有参考"是**测试被喂了答案**);
|
|
5700
|
+
* - 而 `npm pack` 实测发布包**只有 10 个文件**(`lib/*` + `package.json` + `cordis.patch.yml` + README),
|
|
5701
|
+
* **源码 `client/index.tsx` / `host/index.ts` 根本不发布** → npm 用户的机器上:① 没有
|
|
5702
|
+
* `plugins/dsh-plugin-teamflow` 这个路径(PM 读空或跳过 → "禁止凭记忆写"退化成空话);
|
|
5703
|
+
* ② 连我们自己的插件目录里也只有编译产物,**没有源码样本**可读。
|
|
5704
|
+
* 但用户在开发 dsh 插件 = 他机器上**一定装着 dsh 宿主与若干 dsh 插件**(就在 profile 的 `node_modules` 里),
|
|
5705
|
+
* 那些目录的 `package.json`(`dsh` 块:`bundle.patch` / `client`)与 `cordis.patch.yml` 正是
|
|
5706
|
+
* 「**声明怎么写**」的权威现场,且随宿主版本演进 —— 正是本契约要核实的东西。
|
|
5707
|
+
* 故样本首选项 = 本机已装插件;路径**运行时由 `$DSH_HOME` 表达,不写死任何绝对路径**。
|
|
5708
|
+
*/
|
|
5709
|
+
const LOCAL_PLUGIN_SAMPLES_HINT = "$DSH_HOME/profiles/*/node_modules/<任一 dsh 插件>/";
|
|
5710
|
+
/** 本仓样本(**次选**:仅当工作区恰好就在本仓 `plugins/` 下时可就近读;用户机器上不存在)。 */
|
|
5711
|
+
const ARTIFACT_REFERENCE_SAMPLES = {
|
|
5712
|
+
app: [],
|
|
5713
|
+
"plugin-host": ["plugins/dsh-plugin-teamflow"],
|
|
5714
|
+
"plugin-client": ["plugins/dsh-plugin-teamflow"],
|
|
5715
|
+
"plugin-full": ["plugins/dsh-plugin-teamflow"],
|
|
5716
|
+
cli: ["plugins/assetd"],
|
|
5717
|
+
lib: ["plugins/assetd"],
|
|
5718
|
+
docs: [],
|
|
5719
|
+
data: [],
|
|
5720
|
+
other: []
|
|
5721
|
+
};
|
|
5722
|
+
const ARTIFACT_CONTRACTS = {
|
|
5723
|
+
app: [],
|
|
5724
|
+
"plugin-host": [
|
|
5725
|
+
{
|
|
5726
|
+
requirement: "宿主半必须有 profile 层加载入口声明(参照同仓既有插件,不凭记忆写字段名)",
|
|
5727
|
+
criteria: "存在该声明文件/字段;**入口 `name` 必须用包根名**(写子路径会被 loader 判为\"无 client 声明\"而**静默跳过**——踩过)"
|
|
5728
|
+
},
|
|
5729
|
+
{
|
|
5730
|
+
requirement: "该入口声明文件必须进包分发白名单(`files` 等)",
|
|
5731
|
+
criteria: "读 package.json 的 files 数组,确认包含该声明文件(漏了 = 装进 profile 后不加载)"
|
|
5732
|
+
},
|
|
5733
|
+
{
|
|
5734
|
+
requirement: "依赖协议必须是 profile 可解析的形态(不得用 `workspace:` 等本地协议)",
|
|
5735
|
+
criteria: "`node -e \"const s=require('fs').readFileSync('package.json','utf8');process.exit(/workspace:/.test(s)?1:0)\"` 退出码 0"
|
|
5736
|
+
},
|
|
5737
|
+
{
|
|
5738
|
+
requirement: "宿主运行期依赖要按宿主模块表声明(peer/可选 peer,而非真实下载依赖)",
|
|
5739
|
+
criteria: "package.json 的 peerDependencies/peerDependenciesMeta 覆盖宿主提供的 @deepseek-ai/* 包"
|
|
5740
|
+
},
|
|
5741
|
+
{
|
|
5742
|
+
requirement: "**构建产物与源码同步**(实锤 dddd:`lib` 是旧产物 → 装上后宿主启动即炸)",
|
|
5743
|
+
criteria: "提交/安装前重新构建;产物里能找到只存在于当前源码的特征字符串(或产物 mtime 晚于全部 src 文件)"
|
|
5744
|
+
},
|
|
5745
|
+
{
|
|
5746
|
+
requirement: "**装载安全**:模块顶层不得抛错(实锤 dddd:顶层访问未注入的 `ctx.settings` → 宿主起不来)",
|
|
5747
|
+
criteria: "`node -e \"require('<构建产物入口>')\"` 退出码 0(能 require = 顶层无未捕获异常)"
|
|
5748
|
+
},
|
|
5749
|
+
{
|
|
5750
|
+
requirement: "能被 profile 真实装入(**以本机探测到的安装入口为准,不得写死 profile 名/路径/命令**)",
|
|
5751
|
+
criteria: "用 host 在 PRD 里注入的「本机环境」那段给出的命令(CLI 形式,或 `dsh` 不在 PATH 时的等价手动步骤:在探测到的 profile 目录 `pnpm add` + 该包进 `dsh.profile.bundles`);装后 profile 的 `dependencies` 出现该包、`bundles` 含包名。**探测失败时改为问用户 profile 位置,不许编路径**",
|
|
5752
|
+
onlyWhenInstallable: true
|
|
5753
|
+
},
|
|
5754
|
+
{
|
|
5755
|
+
requirement: "装载后宿主真实加载该插件(端到端判据)",
|
|
5756
|
+
criteria: "重启宿主后启动日志/插件列表出现该插件;仅有源码文件不算",
|
|
5757
|
+
onlyWhenInstallable: true
|
|
5758
|
+
},
|
|
5759
|
+
{
|
|
5760
|
+
requirement: "**安装必须带回滚**(实锤 dddd:装上后宿主起不来,只能另开 agent 手术卸载)",
|
|
5761
|
+
criteria: "安装前先写明卸载步骤(反向操作:`pnpm remove <包名>` / `dsh plugin remove <name>`,并确认该包已从 `dsh.profile.bundles` 摘除;包名从产物 package.json 读,不写死),装后验证失败 → 立即执行卸载恢复",
|
|
5762
|
+
onlyWhenInstallable: true
|
|
5763
|
+
}
|
|
5764
|
+
],
|
|
5765
|
+
"plugin-client": [
|
|
5766
|
+
{
|
|
5767
|
+
requirement: "client 半必须有 bundle 声明与被扫描的 id/name(参照同仓既有插件)",
|
|
5768
|
+
criteria: "package.json 的 client 声明块**字段集合与同仓样本逐字段一致**(少一个/名字错了 = loader 不扫);构建产出非空 client 产物,且产物里能找到注册用的包 id"
|
|
5769
|
+
},
|
|
5770
|
+
{
|
|
5771
|
+
requirement: "**UI 挂载点的 `name` 必须是 slot 名本身**(实锤:probe-v2 交付的表格对齐器把 `name` 写成插件自己的标识 `md-table-align.dock` → 与 slot 名对不上 → **静默不挂载**、工具条永不出现,而功能单测 31/31 全绿、AC 全过)",
|
|
5772
|
+
criteria: "读同仓样本核实两段式写法——`ctx.slots.inject('<slot>', () => ctx.slots.register({ name: '<同一个 slot 字符串>', id: '<本插件标识>', order: N }, Component))`:`inject` 的参数与 `register` 的 `name` **必须逐字相同**,插件自己的名字放进 `id`;端到端判据仍以「装进宿主后该 UI 真的出现」为准"
|
|
5773
|
+
},
|
|
5774
|
+
{
|
|
5775
|
+
requirement: "构建产物必须进包分发白名单",
|
|
5776
|
+
criteria: "读 files 数组确认包含构建产物目录/文件(漏了 = 装进 profile 后不加载)"
|
|
5777
|
+
},
|
|
5778
|
+
{
|
|
5779
|
+
requirement: "**构建产物与源码同步**(实锤 dddd:旧 `lib/client.js` 让浏览器端行为与源码不符)",
|
|
5780
|
+
criteria: "提交/安装前重新构建;产物含当前源码特征字符串或 mtime 晚于全部 src"
|
|
5781
|
+
},
|
|
5782
|
+
{
|
|
5783
|
+
requirement: "**安装由主 agent 执行(子代理装不了),PRD 必须给出本机可照做的步骤**",
|
|
5784
|
+
criteria: "流水线子代理权限被钉死在工作区(会话原文 `permission scope was fixed ... cannot be widened`)→ 写不了 profile;**主 agent 能**(profile 写入被拒时宿主给 `escalation available`,`approval/policy: ask` 下经用户批准即可)。故 PRD 要写成「给主 agent 的安装步骤」:本机命令(取自 host 注入的本机环境)+ 重启宿主 + GUI 可见性检查三步;**不得因为子代理跑不了就默认通过**",
|
|
5785
|
+
onlyWhenInstallable: true
|
|
5786
|
+
}
|
|
5787
|
+
],
|
|
5788
|
+
"plugin-full": [
|
|
5789
|
+
{
|
|
5790
|
+
requirement: "宿主半必须有 profile 层加载入口声明(参照同仓既有插件,不凭记忆写字段名)",
|
|
5791
|
+
criteria: "存在该声明文件/字段;**入口 `name` 必须用包根名**(子路径会被静默跳过——踩过)"
|
|
5792
|
+
},
|
|
5793
|
+
{
|
|
5794
|
+
requirement: "client 半必须有 bundle 声明与被扫描的 id/name",
|
|
5795
|
+
criteria: "client 声明块**字段集合与同仓样本逐字段一致**;构建产出非空 client 产物且含包 id"
|
|
5796
|
+
},
|
|
5797
|
+
{
|
|
5798
|
+
requirement: "**UI 挂载点的 `name` 必须是 slot 名本身**(实锤:probe-v2 交付的表格对齐器把 `name` 写成插件自己的标识 → 与 slot 名对不上 → **静默不挂载**、工具条永不出现,而功能单测全绿)",
|
|
5799
|
+
criteria: "读同仓样本核实两段式写法——`ctx.slots.inject('<slot>', () => ctx.slots.register({ name: '<同一个 slot 字符串>', id: '<本插件标识>', order: N }, Component))`:`inject` 的参数与 `register` 的 `name` **必须逐字相同**,插件自己的名字放进 `id`;端到端判据以「装进宿主后该 UI 真的出现」为准"
|
|
5800
|
+
},
|
|
5801
|
+
{
|
|
5802
|
+
requirement: "入口声明与构建产物都必须进包分发白名单",
|
|
5803
|
+
criteria: "读 package.json 的 files 数组确认包含二者(漏了 = 装进 profile 后不加载)"
|
|
5804
|
+
},
|
|
5805
|
+
{
|
|
5806
|
+
requirement: "依赖协议必须是 profile 可解析的形态(不得用 `workspace:` 等本地协议)",
|
|
5807
|
+
criteria: "读 package.json 全文不得命中 `workspace:`"
|
|
5808
|
+
},
|
|
5809
|
+
{
|
|
5810
|
+
requirement: "宿主运行期依赖按宿主模块表声明(peer/可选 peer)",
|
|
5811
|
+
criteria: "peerDependencies/peerDependenciesMeta 覆盖宿主提供的 @deepseek-ai/* 包"
|
|
5812
|
+
},
|
|
5813
|
+
{
|
|
5814
|
+
requirement: "**构建产物与源码同步**(实锤 dddd:旧 `lib/index.js` → 装上后宿主启动即炸)",
|
|
5815
|
+
criteria: "提交/安装前重新构建;宿主与 client 产物均含当前源码特征字符串(或 mtime 晚于全部 src)"
|
|
5816
|
+
},
|
|
5817
|
+
{
|
|
5818
|
+
requirement: "**装载安全**:两个产物的模块顶层都不得抛错(实锤 dddd:顶层访问未注入服务 → 宿主起不来,P0)",
|
|
5819
|
+
criteria: "`node -e \"require('<宿主产物入口>')\"` 与 client 产物同样检查,退出码 0"
|
|
5820
|
+
},
|
|
5821
|
+
{
|
|
5822
|
+
requirement: "能被 profile 真实装入且被宿主加载(端到端;**以本机探测到的安装入口为准,不得写死 profile 名/路径/命令**)",
|
|
5823
|
+
criteria: "用 host 注入的「本机环境」给出的命令(CLI 形式,或 `dsh` 不在 PATH 时的等价手动步骤:探测到的 profile 目录内 `pnpm add` + 该包进 `dsh.profile.bundles`)+ 重启宿主后启动日志出现该插件;探测失败时问用户,不许编路径",
|
|
5824
|
+
onlyWhenInstallable: true
|
|
5825
|
+
},
|
|
5826
|
+
{
|
|
5827
|
+
requirement: "**安装必须带回滚**(实锤 dddd:装上后宿主起不来,只能另开 agent 手术卸载才能救回宿主)",
|
|
5828
|
+
criteria: "安装前先写明卸载步骤(反向操作:`pnpm remove <包名>` / `dsh plugin remove <name>`,并确认该包已从 `dsh.profile.bundles` 摘除;包名从产物 package.json 读,不写死),装后验证失败 → 立即执行卸载恢复",
|
|
5829
|
+
onlyWhenInstallable: true
|
|
5830
|
+
},
|
|
5831
|
+
{
|
|
5832
|
+
requirement: "**安装由主 agent 执行(子代理装不了),PRD 必须给出本机可照做的步骤**",
|
|
5833
|
+
criteria: "流水线子代理权限被钉死在工作区(会话原文 `permission scope was fixed ... cannot be widened`)→ 写不了 profile;**主 agent 能**(被拒后宿主给 `escalation available`,`approval/policy: ask` 下经用户批准即可)。PRD 写成「给主 agent 的安装步骤」:本机命令 + 重启宿主 + GUI 可见性检查三步;**不得因为子代理跑不了就默认通过**(实锤:AC-7 被列为\"人工补测\"后无人真跑,用户装完才发现问题)",
|
|
5834
|
+
onlyWhenInstallable: true
|
|
5835
|
+
}
|
|
5836
|
+
],
|
|
5837
|
+
cli: [{
|
|
5838
|
+
requirement: "必须有可执行入口声明",
|
|
5839
|
+
criteria: "package.json 有 bin 字段且指向真实存在的文件"
|
|
5840
|
+
}, {
|
|
5841
|
+
requirement: "入口文件可被执行",
|
|
5842
|
+
criteria: "直接运行该入口(或 `--help`)退出码 0"
|
|
5843
|
+
}],
|
|
5844
|
+
lib: [{
|
|
5845
|
+
requirement: "必须声明模块入口且导出可用",
|
|
5846
|
+
criteria: "package.json 的 main/exports 指向真实文件,且能被 import 成功"
|
|
5847
|
+
}],
|
|
5848
|
+
docs: [],
|
|
5849
|
+
data: [],
|
|
5850
|
+
other: []
|
|
5851
|
+
};
|
|
5852
|
+
/** 取某形态的契约项(`installable=false` 时过滤掉安装类项)。 */
|
|
5853
|
+
function artifactContractsFor(kind, installable) {
|
|
5854
|
+
return (ARTIFACT_CONTRACTS[kind] || []).filter((it) => !it.onlyWhenInstallable || installable);
|
|
5855
|
+
}
|
|
5856
|
+
const TRIAGE_SETTLES = [
|
|
5857
|
+
"installable",
|
|
5858
|
+
"artifact",
|
|
5859
|
+
"host",
|
|
5860
|
+
"scope",
|
|
5861
|
+
"ui",
|
|
5862
|
+
"data",
|
|
5863
|
+
"other"
|
|
5864
|
+
];
|
|
5865
|
+
/** 归一:非法/缺失一律 `other`(= 不参与自洽门禁,绝不因字段缺失误丢一条真缺口)。 */
|
|
5866
|
+
const normalizeSettle = (raw) => TRIAGE_SETTLES.indexOf(raw) !== -1 ? raw : "other";
|
|
5867
|
+
const TRIAGE_INTENTS = [
|
|
5868
|
+
"requirement",
|
|
5869
|
+
"exploration",
|
|
5870
|
+
"feedback"
|
|
5871
|
+
];
|
|
5872
|
+
/**
|
|
5873
|
+
* 档位「轻重」序(**只用于架构护栏强升**)。tech 与 lite 同级——两者都跑轻量蓝图,差异在语义不在轻重;
|
|
5874
|
+
* patch 最轻(单 agent 直改)。
|
|
5875
|
+
*/
|
|
5876
|
+
const MODE_RANK = {
|
|
5877
|
+
patch: 0,
|
|
5878
|
+
lite: 1,
|
|
5879
|
+
tech: 1,
|
|
5880
|
+
medium: 2,
|
|
5881
|
+
full: 3
|
|
5882
|
+
};
|
|
5883
|
+
/**
|
|
5884
|
+
* **架构护栏强升**(ADR-0006 的护栏不得因「调用方显式指定档位」而失效)。
|
|
5885
|
+
*
|
|
5886
|
+
* 背景(2026-09-16 实测):`lite` 参数描述里写了 "(recommended) for small changes",模型逐字引用它自行传
|
|
5887
|
+
* `lite: true`——33 次启动里 14 次显式传档位(其中 0 次先跑 `teamflow_triage` 预览),于是 triage 的
|
|
5888
|
+
* 「架构信号 → 至少 medium」护栏与澄清闸门在 **42% 的启动**上被静默绕过。
|
|
5889
|
+
*
|
|
5890
|
+
* 规则:调用方**没给**档位 → 用分诊的;给了更轻的档位而分诊判 ≥medium → 升到分诊档位(架构型需求不得
|
|
5891
|
+
* 走轻档位);调用方给的是 medium/full(或已 ≥ 分诊档位)→ **保持调用方选择**(避免无谓 token 放大)。
|
|
5892
|
+
* 返回 null 表示不改动。
|
|
5893
|
+
*
|
|
5894
|
+
* **`needDesign` 档位下限**(2026-09-18 probe-v2 实锤新增):调用方**没给档位**却显式传了
|
|
5895
|
+
* `needDesign=true`(= 明确要求设计阶段),而分诊回了 `lite` —— 两件事语义冲突(`lite` 的档位定义就是
|
|
5896
|
+
* 「no UI design」,见 MODE 表 / `resolveStages`),旧实现让分诊的 lite 直接落地。当时 prompt 里
|
|
5897
|
+
* `needDesign=true → 强升 medium` 只是 regex **预筛提示**(模型可无视,实测就被无视了),这里把它变成
|
|
5898
|
+
* 宿主判定:**未给档位时显式 needDesign 拖到 ≥medium**。调用方显式给了 `mode`/`lite` 时依旧以调用方为准
|
|
5899
|
+
* (`lite` + `needDesign` 由 `resolveStages` 按 flag 追加设计阶段,不吞显式请求,也不改档位标签)。
|
|
5900
|
+
*/
|
|
5901
|
+
function guardrailUpgrade(explicit, lite, triaged, opts) {
|
|
5902
|
+
const want = MODE_RANK[triaged] || 0;
|
|
5903
|
+
if (!explicit && !lite) {
|
|
5904
|
+
if (opts && opts.needDesign === true && want < MODE_RANK.medium) return "medium";
|
|
5905
|
+
return triaged;
|
|
5906
|
+
}
|
|
5907
|
+
if ((explicit ? MODE_RANK[explicit] || 0 : MODE_RANK.lite) <= MODE_RANK.lite && want >= MODE_RANK.medium) return triaged;
|
|
5908
|
+
return null;
|
|
5909
|
+
}
|
|
5910
|
+
/** 意图归一:非法/缺失一律 `requirement`(**绝不因为模型没给字段就拦启动**)。 */
|
|
5911
|
+
const normalizeIntent = (raw) => TRIAGE_INTENTS.indexOf(raw) !== -1 ? raw : "requirement";
|
|
5912
|
+
/**
|
|
5913
|
+
* **host 侧合格线**(防仪式化:prompt 只是请求,这里才是判定)。
|
|
5914
|
+
* 三条证据必须齐全:① ≥2 个互斥读法 ② 改变哪个产物/AC/范围 ③ 猜错返工什么。
|
|
5915
|
+
* 缺一即丢弃并计数——模型几乎总能为任何需求凑出"问题",不合格线就会退化成每次都打断。
|
|
5916
|
+
* 上限 3 条(超过的部分按丢弃计)。
|
|
5917
|
+
*
|
|
5918
|
+
* **自洽门禁**(2026-09-18 probe-v2 实锤新增):`ctx.installable === true` 表示同一次裁决已经**明确判定**
|
|
5919
|
+
* 「必须可安装/可被宿主加载」;此时任何 `settles === 'installable'` 的 blocker 都是**自我矛盾**——它问的
|
|
5920
|
+
* 那件事已经被自己答了。实测代价:这条 blocker 凭空造出一轮澄清(用户答了 3 个问题)→ 输入变了 →
|
|
5921
|
+
* 缓存必然不命中 → **同一条需求被分诊两次**(lite 15.8k + medium 15.6k),用户看到两张「需求分诊」卡。
|
|
5922
|
+
* 注意只按 `true` 判:`false` 是"模型没给这个字段"的默认值,既可能是「不需要装」也可能是「还不知道」——
|
|
5923
|
+
* 后者正是 must-ask 缺口的合法形态,丢了它就会重演 dddd「插件装不进 profile」。
|
|
5924
|
+
*/
|
|
5925
|
+
function qualifyBlockers(raw, ctx) {
|
|
5926
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
5927
|
+
const good = [];
|
|
5928
|
+
let dropped = 0;
|
|
5929
|
+
for (const b of arr) {
|
|
5930
|
+
const o = b && typeof b === "object" ? b : null;
|
|
5931
|
+
const question = String(o && o.question || "").trim();
|
|
5932
|
+
const readings = Array.isArray(o && o.readings) ? o.readings.map((x) => String(x === null || x === void 0 ? "" : x).trim()).filter(Boolean) : [];
|
|
5933
|
+
const changes = String(o && o.changes || "").trim();
|
|
5934
|
+
const rework = String(o && o.rework || "").trim();
|
|
5935
|
+
if (question && readings.length >= 2 && changes && rework) {
|
|
5936
|
+
const settles = normalizeSettle(o && o.settles);
|
|
5937
|
+
if (settles === "installable" && ctx && ctx.installable === true) {
|
|
5938
|
+
dropped++;
|
|
5939
|
+
continue;
|
|
5940
|
+
}
|
|
5941
|
+
if (settles === "host" && ctx && ctx.host && ctx.host !== "unknown") {
|
|
5942
|
+
dropped++;
|
|
5943
|
+
continue;
|
|
5944
|
+
}
|
|
5945
|
+
good.push({
|
|
5946
|
+
settles,
|
|
5947
|
+
question: question.slice(0, 300),
|
|
5948
|
+
readings: readings.slice(0, 4).map((r) => r.slice(0, 200)),
|
|
5949
|
+
changes: changes.slice(0, 300),
|
|
5950
|
+
rework: rework.slice(0, 300)
|
|
5951
|
+
});
|
|
5952
|
+
} else dropped++;
|
|
5953
|
+
}
|
|
5954
|
+
const kept = good.slice(0, 3);
|
|
5955
|
+
return {
|
|
5956
|
+
blockers: kept,
|
|
5957
|
+
dropped: dropped + Math.max(0, good.length - kept.length)
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
4676
5960
|
/** 解析模型 JSON 输出(容错:定位首个 {...} 块;字段缺失/非法回退 null)。 */
|
|
4677
|
-
function parseVerdictText(text) {
|
|
5961
|
+
function parseVerdictText(text, requirement = "") {
|
|
4678
5962
|
const m = String(text || "").match(/\{[\s\S]*\}/);
|
|
4679
5963
|
if (!m) return null;
|
|
4680
5964
|
try {
|
|
4681
5965
|
const raw = JSON.parse(m[0]);
|
|
4682
5966
|
const mode = normalizeMode(raw.mode);
|
|
4683
5967
|
if (!mode) return null;
|
|
5968
|
+
const host = forceHost(requirement, normalizeHost(raw.host));
|
|
5969
|
+
const qb = qualifyBlockers(raw.blockers, {
|
|
5970
|
+
installable: raw.installable === true,
|
|
5971
|
+
host
|
|
5972
|
+
});
|
|
4684
5973
|
return {
|
|
4685
5974
|
mode,
|
|
4686
5975
|
kind: typeof raw.kind === "string" ? raw.kind : "",
|
|
@@ -4697,14 +5986,52 @@ function parseVerdictText(text) {
|
|
|
4697
5986
|
"low"
|
|
4698
5987
|
].indexOf(raw.confidence) !== -1 ? raw.confidence : "medium",
|
|
4699
5988
|
slug: /^[a-z0-9][a-z0-9-]{2,23}$/.test(String(raw.slug || "")) ? String(raw.slug) : "",
|
|
4700
|
-
source: "model"
|
|
5989
|
+
source: "model",
|
|
5990
|
+
intent: normalizeIntent(raw.intent),
|
|
5991
|
+
artifact: normalizeArtifact(raw.artifact),
|
|
5992
|
+
installable: raw.installable === true,
|
|
5993
|
+
host: forceHost(requirement, normalizeHost(raw.host)),
|
|
5994
|
+
blockers: qb.blockers,
|
|
5995
|
+
blockersDropped: qb.dropped
|
|
4701
5996
|
};
|
|
4702
5997
|
} catch (e) {
|
|
4703
5998
|
return null;
|
|
4704
5999
|
}
|
|
4705
6000
|
}
|
|
4706
|
-
/**
|
|
4707
|
-
|
|
6001
|
+
/**
|
|
6002
|
+
* `journal.triage` 的落盘记录(shadow 埋点 **+ 下游消费面**)。
|
|
6003
|
+
*
|
|
6004
|
+
* ⚠️ **这里是「白名单漏字段」的第五次现场(2026-09-18 实锤,勿回退)**:旧版只搬 9 个字段,
|
|
6005
|
+
* **漏了 `artifact` / `installable`** → `pipeline` 的形态契约注入读 `journal.triage.artifact`
|
|
6006
|
+
* 永远拿到 `undefined` → `normalizeArtifact(undefined)` = `'other'` → `artifactContractsFor('other')`
|
|
6007
|
+
* 恒为 `[]` → **`[交付形态契约]` 从未注入过任何 PRD**。实测:全部 **64/64** 个 run 的 `triage` 都
|
|
6008
|
+
* 不带 `artifact`,`log.artifactContract` **一次都没落过**。即 2026-09-17 为 `dddd`(插件"看着完整"
|
|
6009
|
+
* 却装不进 profile、功能 AC 全绿 → 验收照样通过)建的那道防线**一直没生效**。
|
|
6010
|
+
*
|
|
6011
|
+
* 教训:这个函数不是"挑几个有价值的字段记一记",而是**接口字段的完整搬运** —— 每个 `TriageVerdict`
|
|
6012
|
+
* 顶层键要么在这里被搬、要么有明确归属(见 `test/triage-gate.test.js` 的豁免表与结构化门禁,
|
|
6013
|
+
* 它会把「新加字段忘了搬」变成红灯)。
|
|
6014
|
+
*/
|
|
6015
|
+
function triageRecordOf(v) {
|
|
6016
|
+
return {
|
|
6017
|
+
mode: v.mode,
|
|
6018
|
+
kind: v.kind,
|
|
6019
|
+
complexity: v.complexity,
|
|
6020
|
+
confidence: v.confidence,
|
|
6021
|
+
source: v.source,
|
|
6022
|
+
intent: v.intent,
|
|
6023
|
+
blockers: v.blockers,
|
|
6024
|
+
blockersDropped: v.blockersDropped,
|
|
6025
|
+
artifact: normalizeArtifact(v.artifact),
|
|
6026
|
+
installable: v.installable === true,
|
|
6027
|
+
host: normalizeHost(v.host),
|
|
6028
|
+
upgradedFrom: v.__upgradedFrom || null
|
|
6029
|
+
};
|
|
6030
|
+
}
|
|
6031
|
+
/** 兜底:正则预筛 → fallback verdict(模型分诊不可用/超时/解析失败时)。`reason` = 退化原因(可见化:
|
|
6032
|
+
* 2026-09-18 probe-clock 实锤——分诊子代理**推理中被 90s 超时 dispose**(截图「已停止」),journal 只记
|
|
6033
|
+
* 一条 fallback info,没人知道为什么;现在把原因带回给调用方落 warn)。 */
|
|
6034
|
+
function fallbackVerdict(requirement, opts, locale = "zh", reason) {
|
|
4708
6035
|
const pre = suggestMode(requirement, opts, locale);
|
|
4709
6036
|
return {
|
|
4710
6037
|
mode: pre.mode,
|
|
@@ -4714,16 +6041,86 @@ function fallbackVerdict(requirement, opts, locale = "zh") {
|
|
|
4714
6041
|
rationale: [...pre.rationale, t(locale, "triage.fallback")],
|
|
4715
6042
|
confidence: pre.confidence,
|
|
4716
6043
|
slug: "",
|
|
4717
|
-
source: "fallback"
|
|
6044
|
+
source: "fallback",
|
|
6045
|
+
intent: "requirement",
|
|
6046
|
+
blockers: [],
|
|
6047
|
+
blockersDropped: 0,
|
|
6048
|
+
artifact: "other",
|
|
6049
|
+
installable: false,
|
|
6050
|
+
host: forceHost(requirement, "unknown"),
|
|
6051
|
+
...reason ? { fallbackReason: String(reason).slice(0, 300) } : {}
|
|
4718
6052
|
};
|
|
4719
6053
|
}
|
|
4720
6054
|
/** 模型驱动分诊:spawn「分诊分析师」子代理思考一轮;子代理不可用/超时/解析失败 → 正则兜底。
|
|
4721
6055
|
* 解析失败先带纠错提示重试一次(实锤 run tf-mtfo8exi:模型输出「Let me output the JSON.」开场白
|
|
4722
|
-
* 后无 JSON——首轮输出预算被思考耗尽/模型停早;重试提示直接输出 JSON 对象本身)。
|
|
6056
|
+
* 后无 JSON——首轮输出预算被思考耗尽/模型停早;重试提示直接输出 JSON 对象本身)。
|
|
6057
|
+
* **超时 240s**(2026-09-18 实锤修正,勿回退 90s):分诊职责今天已含 intent/blockers/artifact/installable
|
|
6058
|
+
* 判定(输出字段翻倍),深思考模型 90s 内经常答不完 → 被 dispose(UI 显示「已停止」)→ 两次尝试全废 →
|
|
6059
|
+
* fallback。probe-clock 截图实证:模型推理到 artifact 判别处被掐,推理质量很高、不是卡死。 */
|
|
6060
|
+
const TRIAGE_TIMEOUT_MS = 24e4;
|
|
6061
|
+
const triageCache = /* @__PURE__ */ new Map();
|
|
6062
|
+
/** 缓存键 = 需求 + 澄清答复(**不含任何决策字段**;见上方注释)。 */
|
|
6063
|
+
function triageCacheKey(requirement, supplement) {
|
|
6064
|
+
const sup = typeof supplement === "string" ? supplement.trim() : "";
|
|
6065
|
+
return `${String(requirement || "").trim()}\u0000${sup}`;
|
|
6066
|
+
}
|
|
6067
|
+
/** 读缓存。**待决策条目无视 TTL**(见上方注释);非待决策条目按 TTL 兜底回收。 */
|
|
6068
|
+
function triageCacheGet(key) {
|
|
6069
|
+
const hit = triageCache.get(key);
|
|
6070
|
+
if (!hit) return null;
|
|
6071
|
+
if (!hit.pendingDecision && Date.now() - hit.at > 72e5) {
|
|
6072
|
+
triageCache.delete(key);
|
|
6073
|
+
return null;
|
|
6074
|
+
}
|
|
6075
|
+
return hit.verdict;
|
|
6076
|
+
}
|
|
6077
|
+
/**
|
|
6078
|
+
* 写缓存(**仅 model 裁决**;容量超限淘汰最旧)。
|
|
6079
|
+
* @param pendingDecision 该裁决是否**仍未被消费**(= 本次调用会返回 needs-decision、不建 run)。
|
|
6080
|
+
* 工具侧两次调用共享同一 key,故:首次预检返回 needs-decision 时置 true(下次复用,隔多久都行);
|
|
6081
|
+
* 建 run 成功时置 false(这条需求的决策已落地,缓存降级为普通短期缓存)。
|
|
6082
|
+
*/
|
|
6083
|
+
function triageCachePut(key, verdict, pendingDecision = false) {
|
|
6084
|
+
if (!verdict || verdict.source !== "model") return;
|
|
6085
|
+
triageCache.delete(key);
|
|
6086
|
+
triageCache.set(key, {
|
|
6087
|
+
verdict,
|
|
6088
|
+
at: Date.now(),
|
|
6089
|
+
pendingDecision
|
|
6090
|
+
});
|
|
6091
|
+
while (triageCache.size > 32) {
|
|
6092
|
+
const oldest = triageCache.keys().next();
|
|
6093
|
+
if (oldest.done) break;
|
|
6094
|
+
triageCache.delete(oldest.value);
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
/**
|
|
6098
|
+
* 标记「仍在等用户决策」(工具返回 needs-decision / needs-clarification 时调用)。
|
|
6099
|
+
*
|
|
6100
|
+
* **为什么不重新 put 一次**:`put` 会被 `source !== 'model'` 拦掉,而这里要处理的情况是
|
|
6101
|
+
* 「缓存里**已有**裁决、现在要把它标成待决策」——只能就地改状态。若缓存里没有该键(例如裁决来自
|
|
6102
|
+
* fallback、根本没入缓存),这里是**无操作**(下次调用老老实实重跑分诊,符合预期)。
|
|
6103
|
+
*/
|
|
6104
|
+
function triageCacheMarkPending(key) {
|
|
6105
|
+
const hit = key ? triageCache.get(key) : void 0;
|
|
6106
|
+
if (hit) hit.pendingDecision = true;
|
|
6107
|
+
}
|
|
6108
|
+
/**
|
|
6109
|
+
* 决策已落地 → 清除「待决策」标记(**保留裁决本身**,降级为普通短期缓存)。
|
|
6110
|
+
* 调用点:`teamflow_start` 在建 run 成功之后(此时这条需求的启动决策已用完)。
|
|
6111
|
+
*/
|
|
6112
|
+
function triageCacheSettle(key) {
|
|
6113
|
+
const hit = triageCache.get(key);
|
|
6114
|
+
if (hit) {
|
|
6115
|
+
hit.pendingDecision = false;
|
|
6116
|
+
hit.at = Date.now();
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
4723
6119
|
async function runTriage(requirement, opts, parent, signal, locale = "zh") {
|
|
4724
6120
|
const subagents = runtime.subagents;
|
|
4725
|
-
if (!subagents || typeof subagents.start !== "function") return fallbackVerdict(requirement, opts, locale);
|
|
6121
|
+
if (!subagents || typeof subagents.start !== "function") return fallbackVerdict(requirement, opts, locale, "subagents service unavailable");
|
|
4726
6122
|
const pre = suggestMode(requirement, opts, locale);
|
|
6123
|
+
let lastReason = "";
|
|
4727
6124
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
4728
6125
|
let run = null;
|
|
4729
6126
|
try {
|
|
@@ -4737,10 +6134,12 @@ async function runTriage(requirement, opts, parent, signal, locale = "zh") {
|
|
|
4737
6134
|
parent,
|
|
4738
6135
|
signal
|
|
4739
6136
|
});
|
|
4740
|
-
const result = await Promise.race([run.result, new Promise((_, rej) => setTimeout(() => rej(new Error(t(locale, "err.triageTimeout"))),
|
|
4741
|
-
const parsed = parseVerdictText(extractText(result && result.output));
|
|
6137
|
+
const result = await Promise.race([run.result, new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error(t(locale, "err.triageTimeout") + ` (${Math.round(TRIAGE_TIMEOUT_MS / 1e3)}s)`)), TRIAGE_TIMEOUT_MS))]);
|
|
6138
|
+
const parsed = parseVerdictText(extractText(result && result.output), requirement);
|
|
4742
6139
|
if (parsed) return parsed;
|
|
6140
|
+
lastReason = `empty/unparseable verdict (stopReason=${result && result.stopReason || "unknown"})`;
|
|
4743
6141
|
} catch (e) {
|
|
6142
|
+
lastReason = String(e && e.message || e);
|
|
4744
6143
|
if (attempt === 2) {}
|
|
4745
6144
|
} finally {
|
|
4746
6145
|
if (run && run.dispose) try {
|
|
@@ -4748,7 +6147,7 @@ async function runTriage(requirement, opts, parent, signal, locale = "zh") {
|
|
|
4748
6147
|
} catch (e) {}
|
|
4749
6148
|
}
|
|
4750
6149
|
}
|
|
4751
|
-
return fallbackVerdict(requirement, opts, locale);
|
|
6150
|
+
return fallbackVerdict(requirement, opts, locale, lastReason);
|
|
4752
6151
|
}
|
|
4753
6152
|
//#endregion
|
|
4754
6153
|
//#region host/core/report.ts
|
|
@@ -4849,12 +6248,45 @@ function deliverCompletion(journal, parent) {
|
|
|
4849
6248
|
const needsHumanNotice = journal.humanIntervention && !acceptanceDone ? t(locale, "report.needsHumanNoAcceptance") : journal.knownIssuesAcceptance === true ? t(locale, "report.knownIssuesNoMerge") : "";
|
|
4850
6249
|
const noticeKey = `report.noticeStatus.${journal.status}`;
|
|
4851
6250
|
const noticeStatus = t(locale, noticeKey) === noticeKey ? journal.status : t(locale, noticeKey);
|
|
6251
|
+
const cancelSrcKey = `cancelSource.${journal.cancelSource || "unknown"}`;
|
|
6252
|
+
const cancelSourceLine = journal.status === "cancelled" || journal.cancelled === true ? t(locale, "report.cancelSource", { source: t(locale, cancelSrcKey) }) : "";
|
|
6253
|
+
const assumptionsLine = journal.assumptions && String(journal.assumptions).trim() ? t(locale, "report.assumptions", { list: clip(String(journal.assumptions), 900) }) : "";
|
|
6254
|
+
const externalLine = journal.externalFailure === true ? t(locale, "report.externalFailure") : "";
|
|
6255
|
+
const installLine = (() => {
|
|
6256
|
+
try {
|
|
6257
|
+
const env = journal.installEnv;
|
|
6258
|
+
if (!env) return "";
|
|
6259
|
+
const art = normalizeArtifact(journal.triage?.artifact);
|
|
6260
|
+
if (PLUGIN_ARTIFACTS.indexOf(art) === -1) return "";
|
|
6261
|
+
if (env.ok !== true) return t(locale, "report.installAskUser");
|
|
6262
|
+
return t(locale, "report.installPending", {
|
|
6263
|
+
dir: env.profileDir || "?",
|
|
6264
|
+
how: env.cliOnPath === true ? t(locale, "report.installHowCli", { profile: env.profile || "?" }) : t(locale, "report.installHowManual")
|
|
6265
|
+
});
|
|
6266
|
+
} catch (e) {
|
|
6267
|
+
return "";
|
|
6268
|
+
}
|
|
6269
|
+
})();
|
|
6270
|
+
const vcsLine = (() => {
|
|
6271
|
+
try {
|
|
6272
|
+
if (!journal.workspacePath) return "";
|
|
6273
|
+
if (loadState(journal.workspacePath).gitMode === "none") return t(locale, "log.noVcsByChoice");
|
|
6274
|
+
if (journal.status === "completed") return t(locale, "report.vcsArchived");
|
|
6275
|
+
return "";
|
|
6276
|
+
} catch (e) {
|
|
6277
|
+
return "";
|
|
6278
|
+
}
|
|
6279
|
+
})();
|
|
4852
6280
|
const text = [
|
|
4853
6281
|
t(locale, "report.header", { id: journal.id }),
|
|
4854
6282
|
t(locale, "report.statusLine", {
|
|
4855
6283
|
status: statusLine,
|
|
4856
6284
|
error: journal.error ? t(locale, "report.error", { error: clip(journal.error, 300) }) : ""
|
|
4857
6285
|
}),
|
|
6286
|
+
externalLine,
|
|
6287
|
+
vcsLine,
|
|
6288
|
+
cancelSourceLine,
|
|
6289
|
+
assumptionsLine,
|
|
4858
6290
|
t(locale, "report.stagesLine", { stages: stagesLine }),
|
|
4859
6291
|
t(locale, "report.agents", { n: journal.agentsStarted || 0 }),
|
|
4860
6292
|
tokenLine,
|
|
@@ -4864,11 +6296,13 @@ function deliverCompletion(journal, parent) {
|
|
|
4864
6296
|
const m = journal.options && journal.options.mode;
|
|
4865
6297
|
return typeof m === "string" && m !== "full" && m !== "medium" ? t(locale, "report.mode", { mode: modeLabel(locale, m, MODE_REGISTRY[m] ? MODE_REGISTRY[m].label : m) }) : "";
|
|
4866
6298
|
})(),
|
|
6299
|
+
journal.engine && (journal.engine.provider || journal.engine.model) ? t(locale, "report.engine", { engine: `${journal.engine.provider || "?"}/${journal.engine.model || "?"}` }) : "",
|
|
4867
6300
|
t(locale, "report.backlog", { reqId: journal.reqId || "—" }),
|
|
6301
|
+
installLine,
|
|
4868
6302
|
needsHumanNotice,
|
|
4869
6303
|
mergeHint,
|
|
4870
6304
|
t(locale, "report.tabHint"),
|
|
4871
|
-
t(locale, "report.next"),
|
|
6305
|
+
journal.status === "cancelled" || journal.cancelled === true ? t(locale, "report.nextCancelled") : t(locale, "report.next"),
|
|
4872
6306
|
t(locale, "report.relay")
|
|
4873
6307
|
].filter(Boolean).join("\n");
|
|
4874
6308
|
const message = createUserMessage({
|
|
@@ -4877,8 +6311,7 @@ function deliverCompletion(journal, parent) {
|
|
|
4877
6311
|
text
|
|
4878
6312
|
}],
|
|
4879
6313
|
source: {
|
|
4880
|
-
kind: "plugin",
|
|
4881
|
-
plugin: "dsh-plugin-teamflow",
|
|
6314
|
+
kind: "plugin:dsh-plugin-teamflow",
|
|
4882
6315
|
form: "notice",
|
|
4883
6316
|
summary: t(locale, "report.notice", {
|
|
4884
6317
|
status: noticeStatus,
|
|
@@ -5213,11 +6646,30 @@ function sweepWorkspaceLogs(journal, locale = "zh") {
|
|
|
5213
6646
|
* 【档位阶段集】按 mode(full/medium/lite/tech/patch)经 STAGE_POLICY(constants.ts)
|
|
5214
6647
|
* 展开实际执行阶段集(resolveStages),再与团队阶段取交集——见 ADR-0004。
|
|
5215
6648
|
*/
|
|
6649
|
+
/** 插件自身模块文件路径(用于反推当前 profile 目录;`import.meta.url` 在 ESM 产物里可用)。 */
|
|
6650
|
+
function selfModulePath() {
|
|
6651
|
+
try {
|
|
6652
|
+
return fileURLToPath(import.meta.url);
|
|
6653
|
+
} catch (e) {
|
|
6654
|
+
return "";
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
/** `dsh` 是否在 PATH(CLI 入口可用性)。探测失败一律按"不在"处理(走等价手动步骤,更保守)。 */
|
|
6658
|
+
function cliOnPath() {
|
|
6659
|
+
try {
|
|
6660
|
+
return spawnSync(process.platform === "win32" ? "where" : "which", ["dsh"], {
|
|
6661
|
+
stdio: "ignore",
|
|
6662
|
+
windowsHide: true,
|
|
6663
|
+
timeout: 4e3
|
|
6664
|
+
}).status === 0;
|
|
6665
|
+
} catch (e) {
|
|
6666
|
+
return false;
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
5216
6669
|
/** dev 子卡/阶段 label 的「开发 · 」前缀与重试后缀:zh 存量兼容 + en 新增(QA-2:既有解析契约只增不改)。
|
|
5217
6670
|
* en 侧必须覆盖词典 `dev.taskRetry` 的实际产出 `(attempt N)`(R3-2 实锤:只写 retry \d+ 时,
|
|
5218
6671
|
* 无 taskKey 的存量/异常数据走 label 兜底会漏剥离,任务标题归一失效)——**只增不改 zh 分支**。 */
|
|
5219
6672
|
const DEV_TITLE_PREFIX = /^(?:开发|Dev) · /;
|
|
5220
|
-
const RETRY_SUFFIX = /(?:((?:第 \d+ 次重试|补跑))| \((?:retry \d+|attempt \d+|follow-up run)\))$/;
|
|
5221
6673
|
/** 从 journal 已完成阶段重建断点续跑产物(prd/design/scaffold/tech/qa/acceptance/dev)。 */
|
|
5222
6674
|
function buildResumeProducts(journal) {
|
|
5223
6675
|
const products = {};
|
|
@@ -5302,18 +6754,139 @@ function ensureLogGitignore(cwd, journal, locale = "zh") {
|
|
|
5302
6754
|
return false;
|
|
5303
6755
|
}
|
|
5304
6756
|
}
|
|
5305
|
-
/**
|
|
5306
|
-
*
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
6757
|
+
/**
|
|
6758
|
+
* 基线提交前的**索引层排除**(2026-09-18 方案 B 根治,勿回退):
|
|
6759
|
+
* 冷启动 `git add -A` 可能被海量噪音拖过超时(.pnpm-store 数万硬链接,probe-clock 实测 549 文件/51MB),
|
|
6760
|
+
* 基线提交被杀。排除项走 `tfAddArgs(BASELINE_NOISE_EXCLUDES)` 的 **magic pathspec**——
|
|
6761
|
+
* **只影响这一次 git 调用,绝不写用户的 `.gitignore`**。
|
|
6762
|
+
*
|
|
6763
|
+
* **旧实现为什么必须删**(用户实锤截图):`ensureCommonNoiseIgnores()` 把 `.pnpm-store/` 写进用户
|
|
6764
|
+
* `.gitignore`,且因 `mergeGitignore` 的注释是**整批一条**,它顶着「TeamFlow 运行日志(插件自有产物…)」
|
|
6765
|
+
* 的文案落盘——注释张冠李戴只是表象,真问题是**越界**:`.gitignore` 是用户的项目资产,"该忽略什么"
|
|
6766
|
+
* 归 L2(PRD 阶段 PM 按技术栈规划,见 prdPrompt 必查项)与 L3(QA 收口探针)以及用户本人。
|
|
6767
|
+
*/
|
|
6768
|
+
/** 任务夹产物读取助手 `artifactText` 见 util.ts(pipeline/runner 共用:doc 类阶段的产物兜底要用它)。 */
|
|
6769
|
+
/**
|
|
6770
|
+
* tool 侧预检透传的分诊裁决(2026-09-16 需求澄清闸门):只接受形状正确的对象;
|
|
6771
|
+
* 形状不对 → 返回 null,走回「内部再跑一次分诊」的原路径(绝不因为透传字段坏掉就跳过路由)。
|
|
6772
|
+
*/
|
|
6773
|
+
function normalizeTriagePassthrough(raw) {
|
|
6774
|
+
const o = raw && typeof raw === "object" ? raw : null;
|
|
6775
|
+
if (!o) return null;
|
|
6776
|
+
const mode = normalizeMode(o.mode);
|
|
6777
|
+
if (!mode) return null;
|
|
6778
|
+
const qb = qualifyBlockers(o.blockers, {
|
|
6779
|
+
installable: o.installable === true,
|
|
6780
|
+
host: forceHost(o.requirement, normalizeHost(o.host))
|
|
6781
|
+
});
|
|
6782
|
+
return {
|
|
6783
|
+
mode,
|
|
6784
|
+
kind: typeof o.kind === "string" ? o.kind : "",
|
|
6785
|
+
needDesign: o.needDesign === true,
|
|
6786
|
+
complexity: [
|
|
6787
|
+
"small",
|
|
6788
|
+
"medium",
|
|
6789
|
+
"large"
|
|
6790
|
+
].indexOf(String(o.complexity)) !== -1 ? String(o.complexity) : "medium",
|
|
6791
|
+
rationale: Array.isArray(o.rationale) ? o.rationale.map((x) => String(x)).slice(0, 6) : [],
|
|
6792
|
+
confidence: [
|
|
6793
|
+
"high",
|
|
6794
|
+
"medium",
|
|
6795
|
+
"low"
|
|
6796
|
+
].indexOf(String(o.confidence)) !== -1 ? String(o.confidence) : "medium",
|
|
6797
|
+
slug: /^[a-z0-9][a-z0-9-]{2,23}$/.test(String(o.slug || "")) ? String(o.slug) : "",
|
|
6798
|
+
source: o.source === "fallback" ? "fallback" : "model",
|
|
6799
|
+
intent: normalizeIntent(o.intent),
|
|
6800
|
+
artifact: normalizeArtifact(o.artifact),
|
|
6801
|
+
installable: o.installable === true,
|
|
6802
|
+
host: forceHost(o.requirement, normalizeHost(o.host)),
|
|
6803
|
+
blockers: qb.blockers,
|
|
6804
|
+
blockersDropped: qb.dropped,
|
|
6805
|
+
upgradedFrom: normalizeMode(o.__upgradedFrom) || null
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
/**
|
|
6809
|
+
* PRD 收口:① 摘「假设 / 待澄清」段落 `journal.assumptions`;② **宿主契约调研硬门禁**。
|
|
6810
|
+
*
|
|
6811
|
+
* ① 的由来(2026-09-16 需求澄清闸门 Phase 1):实测 **12/12(另一次 39/39)份 PRD 都没记录过假设**——
|
|
6812
|
+
* agent 的替代决定完全不可见,验收人无从判断"这份 PRD 是不是我想要的"。缺失只记 warn(policy 级)。
|
|
6813
|
+
*
|
|
6814
|
+
* ② 的由来(2026-09-18 用户实锤,**偏硬**):交付物要被**非 dsh 宿主**加载(openclaw/hermes/pi…)时,
|
|
6815
|
+
* dsh 的契约一条都不适用,而我们对其没有权威 → PRD **必须含「宿主契约调研」段**(目标宿主是哪个、
|
|
6816
|
+
* 从哪儿读到它的插件加载契约、核实到哪些要求)。缺失 → 返回失败原因(由调用方抛阶段失败 → 走重试诊断),
|
|
6817
|
+
* 而不是只记 warn:用户原话「不然你上下文都不知道你开发个啥出来都不知道」。
|
|
6818
|
+
*
|
|
6819
|
+
* @returns 硬门禁失败原因(null = 通过或本 run 不需要该段)
|
|
6820
|
+
*/
|
|
6821
|
+
function notePrdAssumptions(journal, locale) {
|
|
6822
|
+
const doc = artifactText(journal, "PRD.md") || artifactText(journal, "TECH-CHANGE.md");
|
|
6823
|
+
if (!doc) return null;
|
|
6824
|
+
const body = extractAssumptionsSection(doc);
|
|
6825
|
+
if (body) {
|
|
6826
|
+
journal.assumptions = clip(body, 2e3);
|
|
6827
|
+
journal.logs.push({
|
|
6828
|
+
t: Date.now(),
|
|
6829
|
+
level: "info",
|
|
6830
|
+
message: t(locale, "log.prdAssumptions", { n: body.split(/\n+/).filter((l) => l.trim()).length })
|
|
6831
|
+
});
|
|
6832
|
+
} else journal.logs.push({
|
|
6833
|
+
t: Date.now(),
|
|
6834
|
+
level: "warn",
|
|
6835
|
+
message: t(locale, "log.prdAssumptionsMissing")
|
|
6836
|
+
});
|
|
6837
|
+
if (journal.hostResearch === true) {
|
|
6838
|
+
const hr = extractHostResearchSection(doc);
|
|
6839
|
+
if (!hr) {
|
|
6840
|
+
journal.logs.push({
|
|
6841
|
+
t: Date.now(),
|
|
6842
|
+
level: "error",
|
|
6843
|
+
message: t(locale, "log.hostResearchMissing")
|
|
6844
|
+
});
|
|
6845
|
+
return t(locale, "log.hostResearchMissing");
|
|
6846
|
+
}
|
|
6847
|
+
journal.hostContract = clip(hr, 2e3);
|
|
6848
|
+
journal.logs.push({
|
|
6849
|
+
t: Date.now(),
|
|
6850
|
+
level: "info",
|
|
6851
|
+
message: t(locale, "log.hostResearchOk", { n: hr.split(/\n+/).filter((l) => l.trim()).length })
|
|
6852
|
+
});
|
|
5316
6853
|
}
|
|
6854
|
+
return null;
|
|
6855
|
+
}
|
|
6856
|
+
/**
|
|
6857
|
+
* 需求澄清闸门 · pipeline 侧兜底(2026-09-16 实测补充):分诊判「还不是明确需求」或存在合格 must-know
|
|
6858
|
+
* 缺口 → **不开工**,把 run 落成**可续跑的中断态**(不建任何阶段),由完成汇报把问题交给主线程去问用户,
|
|
6859
|
+
* 用户答完带 `requirementSupplement` 重调(或 `teamflow_resume` 续跑)。
|
|
6860
|
+
*
|
|
6861
|
+
* 为什么兜底放在 pipeline 而不是只靠 tool 侧预检:预检在**工具调用内**跑,模型分诊可能失败并静默退回
|
|
6862
|
+
* 正则兜底(实测 `tf-mu35oza7-wmuckz` 漏传 signal → 0.4s fallback)→ 只靠预检会让闸门在那种情况下静默失效。
|
|
6863
|
+
* 代价:这一条罕见路径会留下一个零阶段 run(status=interrupted + humanIntervention),比"静默开跑"划算。
|
|
6864
|
+
*/
|
|
6865
|
+
function abortForClarification(journal, locale, verdict) {
|
|
6866
|
+
journal.interrupted = true;
|
|
6867
|
+
journal.interruptedAt = Date.now();
|
|
6868
|
+
journal.humanIntervention = true;
|
|
6869
|
+
journal.error = t(locale, "run.needsClarification", {
|
|
6870
|
+
intent: verdict.intent,
|
|
6871
|
+
n: verdict.blockers.length
|
|
6872
|
+
});
|
|
6873
|
+
journal.logs.push({
|
|
6874
|
+
t: Date.now(),
|
|
6875
|
+
level: "warn",
|
|
6876
|
+
message: t(locale, "log.clarifyAbort", {
|
|
6877
|
+
intent: verdict.intent,
|
|
6878
|
+
n: verdict.blockers.length
|
|
6879
|
+
})
|
|
6880
|
+
});
|
|
6881
|
+
for (const b of verdict.blockers) journal.logs.push({
|
|
6882
|
+
t: Date.now(),
|
|
6883
|
+
level: "warn",
|
|
6884
|
+
message: t(locale, "log.clarifyBlocker", {
|
|
6885
|
+
q: clip(b.question, 200),
|
|
6886
|
+
changes: clip(b.changes, 160),
|
|
6887
|
+
rework: clip(b.rework, 160)
|
|
6888
|
+
})
|
|
6889
|
+
});
|
|
5317
6890
|
}
|
|
5318
6891
|
/**
|
|
5319
6892
|
* 断点续跑起点:第一个「没有任意 done 尝试」的阶段。
|
|
@@ -5327,42 +6900,42 @@ function interruptedPhaseOf(journal) {
|
|
|
5327
6900
|
const phaseStages = (journal.stages || []).filter((s) => phaseKeyOf(s.phase) === phase);
|
|
5328
6901
|
if (phaseStages.length === 0) continue;
|
|
5329
6902
|
if (phase === "dev") {
|
|
6903
|
+
backfillDevTaskIds(phaseStages, buildDevTaskDefs(journal, [], runLocaleOf(journal)));
|
|
5330
6904
|
if ([...devTaskStatuses(phaseStages).values()].some((st) => !st.done)) return phase;
|
|
5331
6905
|
} else if (!phaseStages.some((s) => s.status === "done")) return phase;
|
|
5332
6906
|
}
|
|
5333
6907
|
return "acceptance";
|
|
5334
6908
|
}
|
|
5335
|
-
/**
|
|
5336
|
-
*
|
|
5337
|
-
* resume
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
6909
|
+
/** 任务级聚合见 `util.devTaskStatuses`(纯函数,放 util 以便行为级测试直接 import——
|
|
6910
|
+
* pipeline 链到宿主私有 peer `@deepseek-ai/dsh-llm`,测试取不到,同 `cancelRun` 的处置)。
|
|
6911
|
+
* 任务身份与 resume 判定的完整论证见该函数注释。 */
|
|
6912
|
+
/**
|
|
6913
|
+
* 读 journal 后**先补算存量 stage 的 id** 再判定(2026-09-18 二次修正,勿回退)。
|
|
6914
|
+
*
|
|
6915
|
+
* 为什么:升级前的 stage 只写了 `taskKey`(title)。若直接判定(只认 id),历史成果会被
|
|
6916
|
+
* 当成"没做过"——实测 probe-cache `tf-mu6tb281`:纯 title 判定补跑 2 个,而"只认 id +
|
|
6917
|
+
* 存量回退 title"两头不靠 → **补跑 8 个**。补算后**只有一个键空间**(id),存量自愈并写回 journal。
|
|
6918
|
+
* 补算用**蓝图 title 匹配**(结构化 → 文本),不切分 title;合并执行的 stage 会补出多个 id。
|
|
6919
|
+
*
|
|
6920
|
+
* @returns 含 id 的任务定义(供后续 filter/completed 判定用)
|
|
6921
|
+
*/
|
|
6922
|
+
function devTaskDefsWithBackfill(journal, tasks, locale) {
|
|
6923
|
+
const defs = buildDevTaskDefs(journal, tasks, locale);
|
|
6924
|
+
const patched = backfillDevTaskIds(journal.stages || [], defs);
|
|
6925
|
+
if (patched > 0) journal.logs.push({
|
|
6926
|
+
t: Date.now(),
|
|
6927
|
+
level: "info",
|
|
6928
|
+
message: t(locale, "log.devIdsBackfilled", { n: patched })
|
|
6929
|
+
});
|
|
6930
|
+
return defs;
|
|
5356
6931
|
}
|
|
5357
|
-
/** 开发任务定义(单一来源):架构蓝图自动拆 > 调用方显式 tasks > 整体开发兜底。
|
|
5358
|
-
* resume 补跑与正常执行共用(defByTitle 按 title 匹配失败子卡)。 */
|
|
5359
6932
|
function buildDevTaskDefs(journal, tasks, locale = "zh") {
|
|
5360
6933
|
const blueprintTasks = journal.blueprint && Array.isArray(journal.blueprint.tasks) && journal.blueprint.tasks.length ? journal.blueprint.tasks.map((t) => ({
|
|
5361
6934
|
title: t.title || t(locale, "dev.blueprintTask"),
|
|
5362
6935
|
files: Array.isArray(t.files) ? t.files : [],
|
|
5363
6936
|
spec: t.spec || ""
|
|
5364
6937
|
})) : [];
|
|
5365
|
-
return blueprintTasks.length ? blueprintTasks : tasks.length > 0 ? tasks.map((t) => ({
|
|
6938
|
+
return (blueprintTasks.length ? blueprintTasks : tasks.length > 0 ? tasks.map((t) => ({
|
|
5366
6939
|
title: t.title,
|
|
5367
6940
|
spec: t.spec,
|
|
5368
6941
|
files: []
|
|
@@ -5370,7 +6943,12 @@ function buildDevTaskDefs(journal, tasks, locale = "zh") {
|
|
|
5370
6943
|
title: t(locale, "dev.overall"),
|
|
5371
6944
|
spec: t(locale, "dev.overallSpec"),
|
|
5372
6945
|
files: []
|
|
5373
|
-
}]
|
|
6946
|
+
}]).map((d, i) => ({
|
|
6947
|
+
id: devTaskIdAt(i),
|
|
6948
|
+
title: d.title,
|
|
6949
|
+
spec: d.spec,
|
|
6950
|
+
files: d.files || []
|
|
6951
|
+
}));
|
|
5374
6952
|
}
|
|
5375
6953
|
/**
|
|
5376
6954
|
* 执行流水线。resume = null 全新运行;resume = { phase, products } 从断点续跑:
|
|
@@ -5380,9 +6958,26 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5380
6958
|
journal.status = "running";
|
|
5381
6959
|
if (!parseLocale(journal.locale)) journal.locale = localeForMissingSnapshot(!!resume);
|
|
5382
6960
|
const locale = runLocaleOf(journal);
|
|
6961
|
+
if (!journal.engine) try {
|
|
6962
|
+
const r = resolveChildRoute(parent);
|
|
6963
|
+
const engine = {
|
|
6964
|
+
provider: r.provider || providerName() || null,
|
|
6965
|
+
model: r.model || null
|
|
6966
|
+
};
|
|
6967
|
+
journal.engine = engine;
|
|
6968
|
+
journal.logs.push({
|
|
6969
|
+
t: Date.now(),
|
|
6970
|
+
level: "info",
|
|
6971
|
+
message: t(locale, "log.engine", {
|
|
6972
|
+
provider: engine.provider || "?",
|
|
6973
|
+
model: engine.model || "?"
|
|
6974
|
+
})
|
|
6975
|
+
});
|
|
6976
|
+
} catch (e) {}
|
|
5383
6977
|
if (!resume) journal.startedAt = Date.now();
|
|
5384
6978
|
const root = options.productRoot || null;
|
|
5385
6979
|
journal.product = root;
|
|
6980
|
+
journal.requirementSupplement = options.requirementSupplement ? String(options.requirementSupplement) : null;
|
|
5386
6981
|
const scopeKey = journal.workspace || root || "default";
|
|
5387
6982
|
if (activeProducts.has(scopeKey) && activeProducts.get(scopeKey) !== journal.id) {
|
|
5388
6983
|
journal.status = "failed";
|
|
@@ -5399,27 +6994,118 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5399
6994
|
sweepWorkspaceLogs(journal, locale);
|
|
5400
6995
|
} catch (e) {}
|
|
5401
6996
|
let triageSlug = "";
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
journal.options = Object.assign({}, options);
|
|
6997
|
+
const preTriage = normalizeTriagePassthrough(options.__triage);
|
|
6998
|
+
const preTriageError = String(options.__triageError || "");
|
|
6999
|
+
if (preTriage) {
|
|
7000
|
+
triageSlug = preTriage.slug || "";
|
|
7001
|
+
journal.triage = triageRecordOf(preTriage);
|
|
5408
7002
|
journal.logs.push({
|
|
5409
7003
|
t: Date.now(),
|
|
5410
7004
|
level: "info",
|
|
5411
7005
|
message: t(locale, "log.triage", {
|
|
5412
|
-
kind:
|
|
5413
|
-
mode:
|
|
5414
|
-
source:
|
|
7006
|
+
kind: preTriage.kind,
|
|
7007
|
+
mode: preTriage.mode,
|
|
7008
|
+
source: preTriage.source
|
|
5415
7009
|
})
|
|
5416
7010
|
});
|
|
5417
|
-
|
|
5418
|
-
journal.logs.push({
|
|
7011
|
+
const upFrom = preTriage.upgradedFrom;
|
|
7012
|
+
if (upFrom) journal.logs.push({
|
|
7013
|
+
t: Date.now(),
|
|
7014
|
+
level: "warn",
|
|
7015
|
+
message: t(locale, "log.modeUpgraded", {
|
|
7016
|
+
from: upFrom,
|
|
7017
|
+
to: preTriage.mode
|
|
7018
|
+
})
|
|
7019
|
+
});
|
|
7020
|
+
if (preTriage.blockersDropped > 0) journal.logs.push({
|
|
7021
|
+
t: Date.now(),
|
|
7022
|
+
level: "warn",
|
|
7023
|
+
message: t(locale, "log.triageBlockersDropped", { n: preTriage.blockersDropped })
|
|
7024
|
+
});
|
|
7025
|
+
} else if (resume) {
|
|
7026
|
+
if (!journal.triage) {
|
|
7027
|
+
journal.triage = {
|
|
7028
|
+
mode: options.mode || "full",
|
|
7029
|
+
kind: "resume",
|
|
7030
|
+
complexity: "medium",
|
|
7031
|
+
confidence: "medium",
|
|
7032
|
+
source: "resume",
|
|
7033
|
+
intent: "requirement",
|
|
7034
|
+
blockers: [],
|
|
7035
|
+
blockersDropped: 0,
|
|
7036
|
+
upgradedFrom: null
|
|
7037
|
+
};
|
|
7038
|
+
journal.logs.push({
|
|
7039
|
+
t: Date.now(),
|
|
7040
|
+
level: "info",
|
|
7041
|
+
message: t(locale, "log.triageResumed", { mode: options.mode || "full" })
|
|
7042
|
+
});
|
|
7043
|
+
}
|
|
7044
|
+
} else if (options.mode !== "patch") {
|
|
7045
|
+
if (preTriageError) journal.logs.push({
|
|
5419
7046
|
t: Date.now(),
|
|
5420
7047
|
level: "warn",
|
|
5421
|
-
message: t(locale, "log.
|
|
7048
|
+
message: t(locale, "log.triagePreflightFail", { msg: clip(preTriageError, 200) })
|
|
5422
7049
|
});
|
|
7050
|
+
try {
|
|
7051
|
+
const callerMode = options.mode;
|
|
7052
|
+
const verdict = await runTriage(journal.requirementSupplement ? `${requirement}\n\n[CLARIFIED — the user answered the open questions below during a clarification round; treat them as authoritative and do NOT re-ask]\n${String(journal.requirementSupplement)}` : requirement, { needDesign: options.needDesign }, parent, signal, locale);
|
|
7053
|
+
const up = guardrailUpgrade(callerMode, !!options.lite, verdict.mode, { needDesign: options.needDesign === true });
|
|
7054
|
+
if (up) {
|
|
7055
|
+
const fromMode = callerMode !== void 0 || options.lite ? callerMode || "lite" : verdict.mode;
|
|
7056
|
+
if (MODE_RANK[fromMode] < MODE_RANK[up]) verdict.__upgradedFrom = fromMode;
|
|
7057
|
+
options.mode = up;
|
|
7058
|
+
options.lite = up === "lite" || up === "tech" || up === "patch" ? !!options.lite : false;
|
|
7059
|
+
}
|
|
7060
|
+
if (verdict.needDesign && !options.needDesign) options.needDesign = true;
|
|
7061
|
+
triageSlug = verdict.slug || "";
|
|
7062
|
+
journal.options = Object.assign({}, options);
|
|
7063
|
+
journal.triage = triageRecordOf(verdict);
|
|
7064
|
+
journal.logs.push({
|
|
7065
|
+
t: Date.now(),
|
|
7066
|
+
level: "info",
|
|
7067
|
+
message: t(locale, "log.triage", {
|
|
7068
|
+
kind: verdict.kind,
|
|
7069
|
+
mode: verdict.mode,
|
|
7070
|
+
source: verdict.source
|
|
7071
|
+
})
|
|
7072
|
+
});
|
|
7073
|
+
const fbReason = verdict.fallbackReason;
|
|
7074
|
+
if (verdict.source === "fallback" && fbReason) journal.logs.push({
|
|
7075
|
+
t: Date.now(),
|
|
7076
|
+
level: "warn",
|
|
7077
|
+
message: t(locale, "log.triageFallbackReason", { msg: clip(fbReason, 200) })
|
|
7078
|
+
});
|
|
7079
|
+
const upFrom2 = verdict.upgradedFrom;
|
|
7080
|
+
if (upFrom2) journal.logs.push({
|
|
7081
|
+
t: Date.now(),
|
|
7082
|
+
level: "warn",
|
|
7083
|
+
message: t(locale, "log.modeUpgraded", {
|
|
7084
|
+
from: upFrom2,
|
|
7085
|
+
to: verdict.mode
|
|
7086
|
+
})
|
|
7087
|
+
});
|
|
7088
|
+
if (verdict.blockersDropped > 0) journal.logs.push({
|
|
7089
|
+
t: Date.now(),
|
|
7090
|
+
level: "warn",
|
|
7091
|
+
message: t(locale, "log.triageBlockersDropped", { n: verdict.blockersDropped })
|
|
7092
|
+
});
|
|
7093
|
+
const clarified = !!String(journal.requirementSupplement || "").trim();
|
|
7094
|
+
if (verdict.intent !== "requirement" || verdict.blockers.length > 0) {
|
|
7095
|
+
if (!clarified) return abortForClarification(journal, locale, verdict);
|
|
7096
|
+
journal.logs.push({
|
|
7097
|
+
t: Date.now(),
|
|
7098
|
+
level: "warn",
|
|
7099
|
+
message: t(locale, "log.clarifyProceedWithAssumptions", { n: verdict.blockers.length })
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
7102
|
+
} catch (e) {
|
|
7103
|
+
journal.logs.push({
|
|
7104
|
+
t: Date.now(),
|
|
7105
|
+
level: "warn",
|
|
7106
|
+
message: t(locale, "log.triageFail", { msg: String(e && e.message || e) })
|
|
7107
|
+
});
|
|
7108
|
+
}
|
|
5423
7109
|
}
|
|
5424
7110
|
const tasks = normalizeTasks(options.tasks);
|
|
5425
7111
|
const maxConcurrency = Number.isFinite(options.maxConcurrency) && options.maxConcurrency > 0 ? Math.min(options.maxConcurrency, 8) : 3;
|
|
@@ -5501,6 +7187,74 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5501
7187
|
n: maxConcurrency
|
|
5502
7188
|
})
|
|
5503
7189
|
});
|
|
7190
|
+
if (options.preAction === "keep-nogit") try {
|
|
7191
|
+
const st = loadState(scopeKey);
|
|
7192
|
+
st.gitMode = "none";
|
|
7193
|
+
saveState(scopeKey, st);
|
|
7194
|
+
} catch (e) {}
|
|
7195
|
+
if (options.preAction === "init" && journal.workspacePath) try {
|
|
7196
|
+
const st = loadState(scopeKey);
|
|
7197
|
+
if (runSanityCheck(journal.workspacePath, locale).inRepo) {
|
|
7198
|
+
st.gitMode = "repo";
|
|
7199
|
+
saveState(scopeKey, st);
|
|
7200
|
+
} else if (isDangerousVcsRoot(journal.workspacePath, homedir())) {
|
|
7201
|
+
journal.logs.push({
|
|
7202
|
+
t: Date.now(),
|
|
7203
|
+
level: "warn",
|
|
7204
|
+
message: t(locale, "log.gitInitRefused", { path: journal.workspacePath })
|
|
7205
|
+
});
|
|
7206
|
+
options.preAction = "keep-nogit";
|
|
7207
|
+
options.branchPolicy = "keep";
|
|
7208
|
+
st.gitMode = "none";
|
|
7209
|
+
saveState(scopeKey, st);
|
|
7210
|
+
} else {
|
|
7211
|
+
const iR = gitCmd(journal.workspacePath, ["init"]);
|
|
7212
|
+
const baselineSkip = dirTooLargeForBaseline(journal.workspacePath);
|
|
7213
|
+
if (iR !== null) {
|
|
7214
|
+
if (!baselineSkip) {
|
|
7215
|
+
ensureLogGitignore(journal.workspacePath, journal, locale);
|
|
7216
|
+
const aR = gitRun(journal.workspacePath, tfAddArgs(BASELINE_NOISE_EXCLUDES, journal.workspacePath), 12e4);
|
|
7217
|
+
journal.logs.push({
|
|
7218
|
+
t: Date.now(),
|
|
7219
|
+
level: "info",
|
|
7220
|
+
message: t(locale, "log.baselineExcludes", { list: BASELINE_NOISE_EXCLUDES.join(", ") })
|
|
7221
|
+
});
|
|
7222
|
+
const cR = gitRun(journal.workspacePath, [
|
|
7223
|
+
"commit",
|
|
7224
|
+
"-m",
|
|
7225
|
+
t(locale, "commit.baseline")
|
|
7226
|
+
], 12e4);
|
|
7227
|
+
journal.logs.push({
|
|
7228
|
+
t: Date.now(),
|
|
7229
|
+
level: "info",
|
|
7230
|
+
message: t(locale, "log.gitInitDone", { baseline: cR && cR.ok ? t(locale, "log.gitBaselineDone") : t(locale, "log.gitBaselineSkip", { msg: gitFailDetail(aR, cR) }) })
|
|
7231
|
+
});
|
|
7232
|
+
} else journal.logs.push({
|
|
7233
|
+
t: Date.now(),
|
|
7234
|
+
level: "info",
|
|
7235
|
+
message: t(locale, "log.gitInitDone", { baseline: t(locale, "log.gitBaselineLarge") })
|
|
7236
|
+
});
|
|
7237
|
+
st.gitMode = "repo";
|
|
7238
|
+
saveState(scopeKey, st);
|
|
7239
|
+
} else {
|
|
7240
|
+
journal.logs.push({
|
|
7241
|
+
t: Date.now(),
|
|
7242
|
+
level: "warn",
|
|
7243
|
+
message: t(locale, "log.gitInitFail", { path: journal.workspacePath })
|
|
7244
|
+
});
|
|
7245
|
+
options.preAction = "keep-nogit";
|
|
7246
|
+
options.branchPolicy = "keep";
|
|
7247
|
+
st.gitMode = "none";
|
|
7248
|
+
saveState(scopeKey, st);
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
} catch (e) {
|
|
7252
|
+
journal.logs.push({
|
|
7253
|
+
t: Date.now(),
|
|
7254
|
+
level: "warn",
|
|
7255
|
+
message: t(locale, "log.gitInitFail", { path: journal.workspacePath })
|
|
7256
|
+
});
|
|
7257
|
+
}
|
|
5504
7258
|
if (options.branchPolicy !== "keep" && journal.workspacePath) try {
|
|
5505
7259
|
const s = runSanityCheck(journal.workspacePath, locale);
|
|
5506
7260
|
if (s.ok && s.inRepo && !s.hasDirty) {
|
|
@@ -5632,6 +7386,66 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5632
7386
|
state.__runCtx = { ...state.__runCtx || {} };
|
|
5633
7387
|
if (journal.runDocs) state.__runCtx.runDocs = journal.runDocs;
|
|
5634
7388
|
state.__runCtx.locale = locale;
|
|
7389
|
+
try {
|
|
7390
|
+
const tj0 = journal.triage;
|
|
7391
|
+
const art0 = normalizeArtifact(tj0?.artifact);
|
|
7392
|
+
if (PLUGIN_ARTIFACTS.indexOf(art0) !== -1) {
|
|
7393
|
+
const env = detectInstallEnv({
|
|
7394
|
+
baseUrl: installCtx.baseUrl,
|
|
7395
|
+
modulePath: selfModulePath(),
|
|
7396
|
+
dshHome: dshHome(),
|
|
7397
|
+
hasCli: cliOnPath()
|
|
7398
|
+
});
|
|
7399
|
+
state.__runCtx.installEnv = env;
|
|
7400
|
+
journal.installEnv = env;
|
|
7401
|
+
journal.logs.push({
|
|
7402
|
+
t: Date.now(),
|
|
7403
|
+
level: env.ok ? "info" : "warn",
|
|
7404
|
+
message: t(locale, env.ok ? "log.installEnv" : "log.installEnvUnknown", {
|
|
7405
|
+
profile: env.profile || "?",
|
|
7406
|
+
dir: env.profileDir || "?",
|
|
7407
|
+
cli: env.cliOnPath ? "yes" : "no"
|
|
7408
|
+
})
|
|
7409
|
+
});
|
|
7410
|
+
}
|
|
7411
|
+
} catch (e) {}
|
|
7412
|
+
try {
|
|
7413
|
+
const tj = journal.triage;
|
|
7414
|
+
const art = normalizeArtifact(tj?.artifact);
|
|
7415
|
+
const inst = tj?.installable === true;
|
|
7416
|
+
const hst = normalizeHost(tj?.host);
|
|
7417
|
+
const { items, hostResearch } = contractsForDeliverable(hst, art, inst);
|
|
7418
|
+
if (items.length) {
|
|
7419
|
+
state.__runCtx.artifact = art;
|
|
7420
|
+
state.__runCtx.installable = inst;
|
|
7421
|
+
state.__runCtx.host = hst;
|
|
7422
|
+
state.__runCtx.artifactContracts = items.map((it) => ({
|
|
7423
|
+
requirement: it.requirement,
|
|
7424
|
+
criteria: it.criteria
|
|
7425
|
+
}));
|
|
7426
|
+
journal.logs.push({
|
|
7427
|
+
t: Date.now(),
|
|
7428
|
+
level: "info",
|
|
7429
|
+
message: t(locale, "log.artifactContract", {
|
|
7430
|
+
kind: art,
|
|
7431
|
+
n: items.length
|
|
7432
|
+
})
|
|
7433
|
+
});
|
|
7434
|
+
}
|
|
7435
|
+
if (hostResearch) {
|
|
7436
|
+
state.__runCtx.hostResearch = true;
|
|
7437
|
+
state.__runCtx.host = hst;
|
|
7438
|
+
journal.hostResearch = true;
|
|
7439
|
+
journal.logs.push({
|
|
7440
|
+
t: Date.now(),
|
|
7441
|
+
level: "warn",
|
|
7442
|
+
message: t(locale, "log.hostResearch", {
|
|
7443
|
+
kind: art,
|
|
7444
|
+
host: hst
|
|
7445
|
+
})
|
|
7446
|
+
});
|
|
7447
|
+
}
|
|
7448
|
+
} catch (e) {}
|
|
5635
7449
|
try {
|
|
5636
7450
|
const wsCwd = workspaceScopeOf(parent).path;
|
|
5637
7451
|
const sanity = runSanityCheck(wsCwd, locale);
|
|
@@ -5712,7 +7526,9 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5712
7526
|
label: t(locale, "dev.prdFull"),
|
|
5713
7527
|
fn: prdPrompt
|
|
5714
7528
|
};
|
|
5715
|
-
const
|
|
7529
|
+
const supplement = journal.requirementSupplement;
|
|
7530
|
+
const prdInput = supplement ? `${requirement}\n\n[CLARIFIED — the user answered the open questions below during a clarification round; treat them as authoritative and do NOT re-assume]\n${supplement}` : requirement;
|
|
7531
|
+
const prdR = await withRetry(journal, parent, pForm.label, "prd", pForm.fn(prdInput, root, journal.id, state), signal, void 0, options.mode === "patch" ? "low" : null);
|
|
5716
7532
|
if (!prdR.text) throw stageFailError("prd", prdR);
|
|
5717
7533
|
prd = prdR.text;
|
|
5718
7534
|
timeline.prd = prd;
|
|
@@ -5720,6 +7536,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5720
7536
|
noteTaskStageUsage(journal);
|
|
5721
7537
|
if (journal.cancelled) return;
|
|
5722
7538
|
}
|
|
7539
|
+
if (notePrdAssumptions(journal, locale)) throw stageFailError("prd", {});
|
|
5723
7540
|
let design = null;
|
|
5724
7541
|
if (enabled("design")) {
|
|
5725
7542
|
if (resumed("design")) {
|
|
@@ -5811,9 +7628,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5811
7628
|
let devResults = null;
|
|
5812
7629
|
if (resume) {
|
|
5813
7630
|
devResults = resume.products.dev || [];
|
|
7631
|
+
const devDefs = devTaskDefsWithBackfill(journal, tasks, locale);
|
|
5814
7632
|
const taskStatuses = devTaskStatuses(journal.stages || []);
|
|
5815
|
-
const todo =
|
|
5816
|
-
const st = taskStatuses.get(
|
|
7633
|
+
const todo = devDefs.filter((d) => {
|
|
7634
|
+
const st = taskStatuses.get(d.id);
|
|
5817
7635
|
return !st || !st.done;
|
|
5818
7636
|
});
|
|
5819
7637
|
if (todo.length === 0) {
|
|
@@ -5830,20 +7648,22 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5830
7648
|
})
|
|
5831
7649
|
});
|
|
5832
7650
|
const rerun = await runPool(todo, maxConcurrency, async (task) => {
|
|
5833
|
-
const prevStage = [...journal.stages].reverse().find((s) => phaseKeyOf(s.phase) === "dev" && s.status !== "done" && (s.taskKey && s.taskKey === String(task.title || "") || !s.taskKey && (s.label || "").includes(String(task.title || ""))));
|
|
7651
|
+
const prevStage = [...journal.stages].reverse().find((s) => phaseKeyOf(s.phase) === "dev" && s.status !== "done" && (Array.isArray(s.taskIds) && s.taskIds.length ? s.taskIds.includes(task.id) : s.taskKey && s.taskKey === String(task.title || "") || !s.taskKey && (s.label || "").includes(String(task.title || ""))));
|
|
5834
7652
|
const resumePrompt = devPrompt(task, tech, prd, root, journal.id, state) + (prevStage ? buildRetryDiagnostic(2, prevStage) : "");
|
|
5835
|
-
const devR = await withRetry(journal, parent, t(locale, "dev.taskRerun", { title: task.title }), "dev", resumePrompt, signal, task.title);
|
|
7653
|
+
const devR = await withRetry(journal, parent, t(locale, "dev.taskRerun", { title: task.title }), "dev", resumePrompt, signal, task.title, null, [task.id]);
|
|
5836
7654
|
const rerunText = stageTextOf(devR);
|
|
5837
7655
|
noteVerifyEvidence(devR.stage, rerunText);
|
|
5838
7656
|
const ok = !!devR.text;
|
|
5839
7657
|
return {
|
|
5840
7658
|
title: task.title,
|
|
7659
|
+
dtId: task.id,
|
|
5841
7660
|
failed: !ok,
|
|
5842
7661
|
output: rerunText || t(locale, "dev.failedPlaceholder")
|
|
5843
7662
|
};
|
|
5844
|
-
});
|
|
7663
|
+
}, () => journal.cancelled);
|
|
5845
7664
|
for (const t of rerun) {
|
|
5846
|
-
|
|
7665
|
+
if (!t) continue;
|
|
7666
|
+
const sub = createSubtask(journal, t.title, t.spec || "", t.dtId);
|
|
5847
7667
|
if (sub) completeSubtask(journal, sub.id, t.failed, t.output ? snippet(t.output, 1e3) : null, null);
|
|
5848
7668
|
}
|
|
5849
7669
|
devResults = [...reused, ...rerun];
|
|
@@ -5860,10 +7680,12 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5860
7680
|
for (const t of devTaskDefs) {
|
|
5861
7681
|
const hit = t.files && t.files.length ? mergedDefs.find((m) => m.files.some((f) => t.files.includes(f))) : void 0;
|
|
5862
7682
|
if (hit) {
|
|
7683
|
+
hit.ids.push(t.id);
|
|
5863
7684
|
hit.title = `${hit.title} + ${t.title}`;
|
|
5864
7685
|
hit.spec = `${hit.spec}${t.spec ? `;${t.spec}` : ""}`;
|
|
5865
7686
|
for (const f of t.files || []) if (!hit.files.includes(f)) hit.files.push(f);
|
|
5866
7687
|
} else mergedDefs.push({
|
|
7688
|
+
ids: [t.id],
|
|
5867
7689
|
title: t.title,
|
|
5868
7690
|
files: t.files || [],
|
|
5869
7691
|
spec: t.spec || ""
|
|
@@ -5879,7 +7701,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5879
7701
|
})
|
|
5880
7702
|
});
|
|
5881
7703
|
advanceTask(journal, "running", null, t(locale, "event.devStart"), { by: "dev" });
|
|
5882
|
-
const subCards = mergedDefs.map((dt) => createSubtask(journal, dt.title, dt.spec));
|
|
7704
|
+
const subCards = mergedDefs.map((dt) => createSubtask(journal, dt.title, dt.spec, dt.ids[0]));
|
|
5883
7705
|
devResults = await runPool(mergedDefs, maxConcurrency, async (task, idx) => {
|
|
5884
7706
|
const sub = subCards[idx];
|
|
5885
7707
|
if (sub) {
|
|
@@ -5893,7 +7715,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5893
7715
|
persistJournal(journal);
|
|
5894
7716
|
}
|
|
5895
7717
|
}
|
|
5896
|
-
const devR = await withRetry(journal, parent, t(locale, "dev.task", { title: task.title }), "dev", devPrompt(task, tech, prd, root, journal.id, state), signal, task.title);
|
|
7718
|
+
const devR = await withRetry(journal, parent, t(locale, "dev.task", { title: task.title }), "dev", devPrompt(task, tech, prd, root, journal.id, state), signal, task.title, null, task.ids);
|
|
5897
7719
|
const devText = stageTextOf(devR);
|
|
5898
7720
|
noteVerifyEvidence(devR.stage, devText);
|
|
5899
7721
|
const ok = !!devR.text;
|
|
@@ -5906,12 +7728,15 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5906
7728
|
failed: !ok,
|
|
5907
7729
|
output: devText || t(locale, "dev.failedPlaceholder")
|
|
5908
7730
|
};
|
|
5909
|
-
});
|
|
7731
|
+
}, () => journal.cancelled);
|
|
5910
7732
|
timeline.dev = devResults;
|
|
5911
7733
|
for (const r of devResults) if (r && r.output) mergeStageState("dev", r.output);
|
|
5912
7734
|
noteTaskStageUsage(journal);
|
|
5913
7735
|
noteTaskAssign(journal, "dev", journal.stages.filter((s) => phaseKeyOf(s.phase) === "dev").map((s) => (s.childId || "").slice(0, 8)).filter(Boolean).join(",") || t(locale, "role.devTeam"));
|
|
5914
|
-
|
|
7736
|
+
}
|
|
7737
|
+
if (journal.cancelled) return;
|
|
7738
|
+
{
|
|
7739
|
+
const failedCount = (devResults || []).filter((r) => r && r.failed).length;
|
|
5915
7740
|
if (failedCount > 0) {
|
|
5916
7741
|
advanceTask(journal, "needs-human", null, t(locale, "event.devFail"), { by: "dev" });
|
|
5917
7742
|
const req = storeFor(scopeKey).find("req", journal.reqId);
|
|
@@ -5924,12 +7749,12 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5924
7749
|
level: "error",
|
|
5925
7750
|
message: t(locale, "log.devFailGate", {
|
|
5926
7751
|
failed: failedCount,
|
|
5927
|
-
total: devResults.length
|
|
7752
|
+
total: (devResults || []).length
|
|
5928
7753
|
})
|
|
5929
7754
|
});
|
|
5930
7755
|
throw new Error(t(locale, "err.devFail", {
|
|
5931
7756
|
failed: failedCount,
|
|
5932
|
-
total: devResults.length
|
|
7757
|
+
total: (devResults || []).length
|
|
5933
7758
|
}));
|
|
5934
7759
|
} else {
|
|
5935
7760
|
advanceTask(journal, "testable", null, t(locale, "event.devTestable"), { by: "dev" });
|
|
@@ -5939,7 +7764,6 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
5939
7764
|
message: t(locale, "log.devDone")
|
|
5940
7765
|
});
|
|
5941
7766
|
}
|
|
5942
|
-
if (journal.cancelled) return;
|
|
5943
7767
|
}
|
|
5944
7768
|
persistJournal(journal);
|
|
5945
7769
|
let qa = null;
|
|
@@ -6228,6 +8052,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
6228
8052
|
req.humanIntervention = true;
|
|
6229
8053
|
store.pushEvent(req, req.status, "needs-human", t(locale, "event.reqMismatchHuman"));
|
|
6230
8054
|
}
|
|
8055
|
+
journal.humanIntervention = true;
|
|
6231
8056
|
journal.logs.push({
|
|
6232
8057
|
t: Date.now(),
|
|
6233
8058
|
level: "error",
|
|
@@ -6267,10 +8092,32 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
6267
8092
|
journal.error = String(e && e.message || e);
|
|
6268
8093
|
}
|
|
6269
8094
|
} finally {
|
|
8095
|
+
if (journal.cancelled && journal.status === "running") {
|
|
8096
|
+
journal.status = "cancelled";
|
|
8097
|
+
journal.error = journal.error || t(locale, "err.cancelled");
|
|
8098
|
+
}
|
|
6270
8099
|
journal.endedAt = Date.now();
|
|
6271
8100
|
inFlight.delete(journal.id);
|
|
6272
8101
|
activeProducts.delete(scopeKey);
|
|
6273
|
-
|
|
8102
|
+
const vcsState = (() => {
|
|
8103
|
+
try {
|
|
8104
|
+
return loadState(scopeKey).gitMode;
|
|
8105
|
+
} catch (e) {
|
|
8106
|
+
return;
|
|
8107
|
+
}
|
|
8108
|
+
})();
|
|
8109
|
+
const dangerousWs = journal.workspacePath && isDangerousVcsRoot(journal.workspacePath, homedir());
|
|
8110
|
+
if (journal.workspacePath && vcsState === "none") journal.logs.push({
|
|
8111
|
+
t: Date.now(),
|
|
8112
|
+
level: "warn",
|
|
8113
|
+
message: t(locale, "log.noVcsByChoice")
|
|
8114
|
+
});
|
|
8115
|
+
else if (journal.workspacePath && dangerousWs) journal.logs.push({
|
|
8116
|
+
t: Date.now(),
|
|
8117
|
+
level: "warn",
|
|
8118
|
+
message: t(locale, "log.noVcsDangerous", { path: journal.workspacePath })
|
|
8119
|
+
});
|
|
8120
|
+
else if (journal.workspacePath && journal.status === "completed" && !journal.humanIntervention) try {
|
|
6274
8121
|
const reqHead = String(journal.requirement || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
6275
8122
|
ensureLogGitignore(journal.workspacePath, journal, locale);
|
|
6276
8123
|
const docAdd = tfDocAddArgs([journal.runDocs, `${TF_DOCS_DIR}/memory.md`].filter((p) => existsSync(`${journal.workspacePath}/${p}`)));
|
|
@@ -6316,6 +8163,11 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
6316
8163
|
message: t(locale, "log.commitFail", { msg: String(e && e.message || e) })
|
|
6317
8164
|
});
|
|
6318
8165
|
}
|
|
8166
|
+
else if (journal.workspacePath && (vcsState === "none" || dangerousWs) && journal.status === "completed") journal.logs.push({
|
|
8167
|
+
t: Date.now(),
|
|
8168
|
+
level: "warn",
|
|
8169
|
+
message: t(locale, "run.notCommitted", { docs: journal.runDocs || "" })
|
|
8170
|
+
});
|
|
6319
8171
|
else if (journal.workspacePath && journal.runDocs && (journal.status === "failed" || journal.status === "cancelled" || journal.status === "interrupted")) journal.logs.push({
|
|
6320
8172
|
t: Date.now(),
|
|
6321
8173
|
level: "warn",
|
|
@@ -6432,21 +8284,18 @@ function startPipeline(agent, requirement, options, signal) {
|
|
|
6432
8284
|
if (firstKey !== void 0) runs.delete(firstKey);
|
|
6433
8285
|
}
|
|
6434
8286
|
persistJournal(journal);
|
|
6435
|
-
|
|
8287
|
+
const execOptions = Object.assign({}, journal.options, {
|
|
8288
|
+
requirementSupplement: options.requirementSupplement || null,
|
|
8289
|
+
__triage: options.__triage,
|
|
8290
|
+
__triageError: options.__triageError,
|
|
8291
|
+
branchPolicy: options.branchPolicy,
|
|
8292
|
+
branchName: options.branchName,
|
|
8293
|
+
preAction: options.preAction,
|
|
8294
|
+
commitMessage: options.commitMessage
|
|
8295
|
+
});
|
|
8296
|
+
executePipeline(journal, agent, journal.requirement, execOptions, signal);
|
|
6436
8297
|
return journal.id;
|
|
6437
8298
|
}
|
|
6438
|
-
/** 取消运行(置 cancelled + dispose 进行中的子代理)。 */
|
|
6439
|
-
function cancelRun(runId) {
|
|
6440
|
-
const j = runs.get(runId);
|
|
6441
|
-
if (!j) return false;
|
|
6442
|
-
j.cancelled = true;
|
|
6443
|
-
const entry = inFlight.get(runId);
|
|
6444
|
-
if (entry && entry.run) try {
|
|
6445
|
-
entry.run.dispose();
|
|
6446
|
-
} catch (e) {}
|
|
6447
|
-
persistJournal(j);
|
|
6448
|
-
return true;
|
|
6449
|
-
}
|
|
6450
8299
|
/** 从断点续跑:跳过已完成阶段,从第一个未完成阶段重跑(service 与工具共用)。 */
|
|
6451
8300
|
function resumeRun(runId, sessionId) {
|
|
6452
8301
|
const id = typeof runId === "string" ? runId : null;
|
|
@@ -6552,6 +8401,85 @@ function resumeRun(runId, sessionId) {
|
|
|
6552
8401
|
*
|
|
6553
8402
|
* 运行环境:宿主组合(web profile)的真实 Node 进程。
|
|
6554
8403
|
*/
|
|
8404
|
+
/** 有界计数(决策问句里的"现有 N 个文件";不精确——只采样,超阈值即停)。 */
|
|
8405
|
+
function countFilesBounded(dir, cap = 1e3) {
|
|
8406
|
+
let n = 0;
|
|
8407
|
+
const walk = (d, depth) => {
|
|
8408
|
+
if (depth > 12 || n > cap) return;
|
|
8409
|
+
let es = [];
|
|
8410
|
+
try {
|
|
8411
|
+
es = readdirSync(d, { withFileTypes: true });
|
|
8412
|
+
} catch (e) {
|
|
8413
|
+
return;
|
|
8414
|
+
}
|
|
8415
|
+
for (const e of es) {
|
|
8416
|
+
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
8417
|
+
if (e.isDirectory()) walk(d + "\\" + e.name, depth + 1);
|
|
8418
|
+
else {
|
|
8419
|
+
n++;
|
|
8420
|
+
if (n > cap) return;
|
|
8421
|
+
}
|
|
8422
|
+
}
|
|
8423
|
+
};
|
|
8424
|
+
walk(dir, 0);
|
|
8425
|
+
return n;
|
|
8426
|
+
}
|
|
8427
|
+
/**
|
|
8428
|
+
* 需求澄清闸门 · 启动前预检(2026-09-16 Phase 1,**快路径**)。
|
|
8429
|
+
*
|
|
8430
|
+
* 分诊本来就是一次模型调用——这里把它**前移到建 run 之前**,用同一份裁决判两件事:
|
|
8431
|
+
* ① `intent` 是否「明确需求」(`exploration` = 还在探讨、`feedback` = 对现状的反馈);② 有没有 **must-know**
|
|
8432
|
+
* 缺口(`blockers`,已过 host 合格线:≥2 互斥读法 + 改变哪个产物/AC + 猜错返工什么)。命中任一 → 返回
|
|
8433
|
+
* `needs-clarification`,**不建 run**——实锤:社区讨论 #6405 用户说「我想开发一个 dsh 插件」→ 直接跑完整条
|
|
8434
|
+
* 流水线(他本人:「我都不知道自己想要啥」)。
|
|
8435
|
+
*
|
|
8436
|
+
* ⚠️ **正确性不依赖本函数**(2026-09-16 实测教训,`tf-mu35oza7-wmuckz`):它在**工具调用内**跑,模型分诊
|
|
8437
|
+
* 一旦在此失败(那次 0.4s 返回 `source=fallback`,因为漏传 `signal`)就会静默降级成正则兜底 —— 所以
|
|
8438
|
+
* **权威判定在 pipeline 内**:pipeline 对「没有透传裁决」的启动一律自己再跑一次分诊,并据此做闸门与
|
|
8439
|
+
* 架构护栏。这里失败只把原因放进 `__triageError`,由 pipeline 记 warn(不静默)。
|
|
8440
|
+
*
|
|
8441
|
+
* 边界:**只有 `patch` 档豁免**——其余一律(含显式 `lite`/`mode`)都要过闸门与架构护栏;原因:实测模型
|
|
8442
|
+
* **系统性**自选档位(33 次启动 14 次显式传入、0 次先 `teamflow_triage` 预览;根因是旧 `lite` 描述里的
|
|
8443
|
+
* "(recommended)" 被逐字引用)→ 若继续豁免,闸门与 ADR-0006 护栏会在 42% 的启动上静默失效。
|
|
8444
|
+
*/
|
|
8445
|
+
async function clarificationPreflight(requirement, options, parent, signal, locale) {
|
|
8446
|
+
if (options.mode === "patch") return {
|
|
8447
|
+
verdict: null,
|
|
8448
|
+
cacheKey: ""
|
|
8449
|
+
};
|
|
8450
|
+
const triageInput = options.requirementSupplement ? `${requirement}\n\n[CLARIFIED — the user answered the open questions below during a clarification round; treat them as authoritative and do NOT re-ask]\n${String(options.requirementSupplement)}` : requirement;
|
|
8451
|
+
const cacheKey = triageCacheKey(requirement, options.requirementSupplement);
|
|
8452
|
+
let verdict = triageCacheGet(cacheKey);
|
|
8453
|
+
let error = "";
|
|
8454
|
+
if (verdict) try {
|
|
8455
|
+
console.error(`[teamflow] ${t(locale, "diag.triageCacheHit")}`);
|
|
8456
|
+
} catch (e) {}
|
|
8457
|
+
else try {
|
|
8458
|
+
verdict = await runTriage(triageInput, { needDesign: options.needDesign === true }, parent, signal, locale);
|
|
8459
|
+
triageCachePut(cacheKey, verdict);
|
|
8460
|
+
} catch (e) {
|
|
8461
|
+
error = String(e && e.message || e);
|
|
8462
|
+
verdict = null;
|
|
8463
|
+
}
|
|
8464
|
+
if (!verdict) return {
|
|
8465
|
+
verdict: null,
|
|
8466
|
+
error,
|
|
8467
|
+
cacheKey
|
|
8468
|
+
};
|
|
8469
|
+
const alreadyClarified = !!String(options.requirementSupplement || "").trim();
|
|
8470
|
+
if ((verdict.intent !== "requirement" || verdict.blockers.length > 0) && !alreadyClarified) return {
|
|
8471
|
+
needsClarification: {
|
|
8472
|
+
intent: verdict.intent,
|
|
8473
|
+
blockers: verdict.blockers
|
|
8474
|
+
},
|
|
8475
|
+
cacheKey
|
|
8476
|
+
};
|
|
8477
|
+
if (alreadyClarified && verdict.blockers.length > 0) verdict.__clarifyProceeded = verdict.blockers.length;
|
|
8478
|
+
return {
|
|
8479
|
+
verdict,
|
|
8480
|
+
cacheKey
|
|
8481
|
+
};
|
|
8482
|
+
}
|
|
6555
8483
|
/** 阶段顺序/key 映射见 constants.ts(PHASE_ORDER/PHASE_KEY_OF/PHASE_KEY_BY_NAME)。 */
|
|
6556
8484
|
/** 按工作区作用域过滤运行见 core/products.ts(runsFor;全局面板与远程面共用)。 */
|
|
6557
8485
|
/** 由 sessionId 推导会话所属 workspace(项目)作用域。 */
|
|
@@ -6576,6 +8504,8 @@ function snapshotOf(j) {
|
|
|
6576
8504
|
seq: s.seq,
|
|
6577
8505
|
label: s.label,
|
|
6578
8506
|
phase: s.phase,
|
|
8507
|
+
taskKey: s.taskKey || null,
|
|
8508
|
+
taskIds: Array.isArray(s.taskIds) && s.taskIds.length ? s.taskIds : null,
|
|
6579
8509
|
status: s.status,
|
|
6580
8510
|
outcome: s.outcome,
|
|
6581
8511
|
childId: s.childId,
|
|
@@ -6612,7 +8542,7 @@ function registerTools(ctx) {
|
|
|
6612
8542
|
};
|
|
6613
8543
|
T({
|
|
6614
8544
|
name: "teamflow_start",
|
|
6615
|
-
description: "Start the team R&D pipeline (background async): runs the stages per team config (PRD→design→tech→dev→QA→acceptance). Specify teamId (matches teams.json) or pick a team via the UI \"+\" button first so messages auto-match. Stage failures auto-retry; beyond threshold → rework/human intervention; per-stage token usage recorded. NOTE: after calling, the implementation work is done by pipeline subagents — the main thread MUST NOT write code or run verifications for it. requirement must be a faithful transcription of the user's words; do not invent file paths / tech claims without code verification (downstream stages build the PRD from it). Branch decision: when the return status is \"needs-decision\", ASK THE USER to pick one of the options (or take their custom input, e.g. a branch name), then RE-CALL this tool passing the CHOSEN OPTION VALUE as branchPolicy (\"new\" = confirmed create branch, \"keep\" = stay), optionally combined with preAction / branchName / commitMessage. Pass branchPolicy=\"keep\" when the user chooses to stay on the current branch.",
|
|
8545
|
+
description: "Start the team R&D pipeline (background async): runs the stages per team config (PRD→design→tech→dev→QA→acceptance). Specify teamId (matches teams.json) or pick a team via the UI \"+\" button first so messages auto-match. Stage failures auto-retry; beyond threshold → rework/human intervention; per-stage token usage recorded. NOTE: after calling, the implementation work is done by pipeline subagents — the main thread MUST NOT write code or run verifications for it. Routing: omit `mode`/`lite` and let auto-triage decide the tier (it applies the architecture guardrails); pass them only when the user explicitly asked for that tier. requirement must be a faithful transcription of the user's words; do not invent file paths / tech claims without code verification (downstream stages build the PRD from it). Clarification gate: if the return status is \"needs-clarification\", this is NOT a settled requirement yet (or it has must-know gaps) — do NOT retry blindly: discuss it with the user in your own words (the returned blockers list what is missing and why), then RE-CALL this tool with the original requirement plus requirementSupplement = the user's answers. Branch decision: when the return status is \"needs-decision\", ASK THE USER to pick one of the options (or take their custom input, e.g. a branch name), then RE-CALL this tool passing the CHOSEN OPTION VALUE as branchPolicy (\"new\" = confirmed create branch, \"keep\" = stay), optionally combined with preAction / branchName / commitMessage. Pass branchPolicy=\"keep\" when the user chooses to stay on the current branch.",
|
|
6616
8546
|
parameters: {
|
|
6617
8547
|
requirement: {
|
|
6618
8548
|
type: "string",
|
|
@@ -6633,11 +8563,11 @@ function registerTools(ctx) {
|
|
|
6633
8563
|
},
|
|
6634
8564
|
lite: {
|
|
6635
8565
|
type: "boolean",
|
|
6636
|
-
description: "Lightweight mode for small changes
|
|
8566
|
+
description: "Lightweight mode for genuinely small single-module changes. **Do NOT pick the tier yourself by default**: omit both `mode` and `lite` and let auto-triage decide — it applies the architecture guardrails (persistence/abstraction/cross-module → at least medium) that a hand-picked tier bypasses. Set `lite=true` only when the USER explicitly asked for a lightweight/fast run, or the change is a proven single-module micro change."
|
|
6637
8567
|
},
|
|
6638
8568
|
mode: {
|
|
6639
8569
|
type: "string",
|
|
6640
|
-
description: "Route mode: full / medium / lite / tech / patch
|
|
8570
|
+
description: "Route mode: full / medium / lite / tech / patch. **Do NOT pick the tier yourself by default** — omit it and let auto-triage decide (recommended), or preview with teamflow_triage and pass what it returns. A hand-picked lighter tier is subject to the architecture guardrail: if triage judges the requirement architectural it is upgraded (logged), so the guardrail can never be bypassed."
|
|
6641
8571
|
},
|
|
6642
8572
|
productRoot: {
|
|
6643
8573
|
type: "string",
|
|
@@ -6663,6 +8593,10 @@ function registerTools(ctx) {
|
|
|
6663
8593
|
type: "string",
|
|
6664
8594
|
description: "Custom commit message when preAction=commit"
|
|
6665
8595
|
},
|
|
8596
|
+
requirementSupplement: {
|
|
8597
|
+
type: "string",
|
|
8598
|
+
description: "Extra context the user gave during a clarification round (after a \"needs-clarification\" return). Kept separate from the original requirement (which stays a faithful transcription of the user's words) and handed to the PRD stage as authoritative input."
|
|
8599
|
+
},
|
|
6666
8600
|
tasks: {
|
|
6667
8601
|
type: "array",
|
|
6668
8602
|
description: "Optional splittable dev task list",
|
|
@@ -6691,14 +8625,26 @@ function registerTools(ctx) {
|
|
|
6691
8625
|
properties: {
|
|
6692
8626
|
runId: { type: "string" },
|
|
6693
8627
|
status: { type: "string" },
|
|
8628
|
+
kind: { type: "string" },
|
|
6694
8629
|
question: { type: "string" },
|
|
6695
8630
|
options: { type: "array" },
|
|
6696
|
-
note: { type: "string" }
|
|
8631
|
+
note: { type: "string" },
|
|
8632
|
+
intent: { type: "string" },
|
|
8633
|
+
blockers: { type: "array" },
|
|
8634
|
+
message: { type: "string" }
|
|
6697
8635
|
}
|
|
6698
8636
|
},
|
|
6699
8637
|
render: (args, value) => {
|
|
6700
8638
|
if (value && value.status === "needs-decision") {
|
|
6701
8639
|
const opts = Array.isArray(value.options) ? value.options.map((o, i) => `${i + 1}. ${o.label}`).join("\n") : "";
|
|
8640
|
+
if (value.kind === "git-init") return [{
|
|
8641
|
+
type: "text",
|
|
8642
|
+
text: t(ambientLocale(), "tool.start.gitDecisionNote", {
|
|
8643
|
+
question: value.question,
|
|
8644
|
+
options: opts,
|
|
8645
|
+
note: value.note || ""
|
|
8646
|
+
})
|
|
8647
|
+
}];
|
|
6702
8648
|
return [{
|
|
6703
8649
|
type: "text",
|
|
6704
8650
|
text: t(ambientLocale(), "tool.start.decision", {
|
|
@@ -6714,6 +8660,20 @@ function registerTools(ctx) {
|
|
|
6714
8660
|
note: value.note || ""
|
|
6715
8661
|
})
|
|
6716
8662
|
}];
|
|
8663
|
+
if (value && value.status === "needs-clarification") {
|
|
8664
|
+
const list = Array.isArray(value.blockers) ? value.blockers.map((b, i) => `${i + 1}. ${b.question}\n · ${t(ambientLocale(), "tool.start.blockerReadings")}: ${(b.readings || []).join(" / ")}\n · ${t(ambientLocale(), "tool.start.blockerChanges")}: ${b.changes}\n · ${t(ambientLocale(), "tool.start.blockerRework")}: ${b.rework}`).join("\n") : "";
|
|
8665
|
+
return [{
|
|
8666
|
+
type: "text",
|
|
8667
|
+
text: t(ambientLocale(), "tool.start.needsClarification", {
|
|
8668
|
+
intent: String(value.intent || "requirement"),
|
|
8669
|
+
blockers: list
|
|
8670
|
+
})
|
|
8671
|
+
}];
|
|
8672
|
+
}
|
|
8673
|
+
if (value && value.message && !value.runId) return [{
|
|
8674
|
+
type: "text",
|
|
8675
|
+
text: String(value.message)
|
|
8676
|
+
}];
|
|
6717
8677
|
return [{
|
|
6718
8678
|
type: "text",
|
|
6719
8679
|
text: t(ambientLocale(), "tool.start.started", {
|
|
@@ -6757,15 +8717,67 @@ function registerTools(ctx) {
|
|
|
6757
8717
|
maxConcurrency: args.maxConcurrency,
|
|
6758
8718
|
branchPolicy: args.branchPolicy === "keep" ? "keep" : "auto",
|
|
6759
8719
|
branchName: typeof args.branchName === "string" && args.branchName.trim() ? args.branchName.trim() : null,
|
|
6760
|
-
preAction: args.preAction === "stash" || args.preAction === "commit" ? args.preAction : null,
|
|
6761
|
-
commitMessage: typeof args.commitMessage === "string" && args.commitMessage.trim() ? args.commitMessage.trim() : null
|
|
8720
|
+
preAction: args.preAction === "stash" || args.preAction === "commit" || args.preAction === "init" || args.preAction === "keep-nogit" ? args.preAction : null,
|
|
8721
|
+
commitMessage: typeof args.commitMessage === "string" && args.commitMessage.trim() ? args.commitMessage.trim() : null,
|
|
8722
|
+
requirementSupplement: typeof args.requirementSupplement === "string" && args.requirementSupplement.trim() ? args.requirementSupplement.trim() : null
|
|
6762
8723
|
};
|
|
8724
|
+
const pre = await clarificationPreflight(requirement, options, parent, exec && exec.signal, ambientLocale());
|
|
8725
|
+
const triageKey = pre.cacheKey;
|
|
8726
|
+
if ("needsClarification" in pre) {
|
|
8727
|
+
triageCacheMarkPending(triageKey);
|
|
8728
|
+
return {
|
|
8729
|
+
status: "needs-clarification",
|
|
8730
|
+
intent: pre.needsClarification.intent,
|
|
8731
|
+
blockers: pre.needsClarification.blockers
|
|
8732
|
+
};
|
|
8733
|
+
}
|
|
8734
|
+
if (pre.verdict) {
|
|
8735
|
+
const explicit = options.mode;
|
|
8736
|
+
const up = guardrailUpgrade(explicit, !!options.lite, pre.verdict.mode, { needDesign: options.needDesign === true });
|
|
8737
|
+
if (up) {
|
|
8738
|
+
const fromMode = explicit !== void 0 || options.lite ? explicit || "lite" : pre.verdict.mode;
|
|
8739
|
+
if (MODE_RANK[fromMode] < MODE_RANK[up]) pre.verdict.__upgradedFrom = fromMode;
|
|
8740
|
+
options.mode = up;
|
|
8741
|
+
options.lite = up === "lite" || up === "tech" || up === "patch" ? !!options.lite : false;
|
|
8742
|
+
}
|
|
8743
|
+
if (pre.verdict.needDesign) options.needDesign = true;
|
|
8744
|
+
options.__triage = pre.verdict;
|
|
8745
|
+
} else if (pre.error) options.__triageError = pre.error;
|
|
6763
8746
|
const branchConfirmed = args.branchPolicy === "new";
|
|
6764
8747
|
if (options.branchPolicy === "auto" && !branchConfirmed && !options.branchName && !options.preAction && exec && exec.agent) {
|
|
6765
8748
|
const sc = workspaceScopeOf(exec.agent);
|
|
6766
8749
|
if (sc.path) try {
|
|
6767
8750
|
const s = runSanityCheck(sc.path);
|
|
6768
|
-
if (s.
|
|
8751
|
+
if (!s.inRepo) {
|
|
8752
|
+
if (loadState(sc.path).gitMode === "none" || options.preAction === "keep-nogit") {
|
|
8753
|
+
options.preAction = "keep-nogit";
|
|
8754
|
+
options.branchPolicy = "keep";
|
|
8755
|
+
} else {
|
|
8756
|
+
const home = homedir();
|
|
8757
|
+
const danger = isDangerousVcsRoot(sc.path, home);
|
|
8758
|
+
const tooLarge = danger ? false : dirTooLargeForBaseline(sc.path);
|
|
8759
|
+
const baseline = danger ? "" : t(ambientLocale(), tooLarge ? "git.opt.initNoBaseline" : "git.opt.initBaseline", { n: countFilesBounded(sc.path) });
|
|
8760
|
+
const q = danger ? t(ambientLocale(), "git.q.danger", { path: sc.path }) : t(ambientLocale(), "git.q.noRepo", { path: sc.path });
|
|
8761
|
+
const optsList = danger ? [{
|
|
8762
|
+
label: t(ambientLocale(), "git.opt.keepOnly"),
|
|
8763
|
+
value: "keep"
|
|
8764
|
+
}] : [{
|
|
8765
|
+
label: t(ambientLocale(), "git.opt.init", { baseline }),
|
|
8766
|
+
value: "init"
|
|
8767
|
+
}, {
|
|
8768
|
+
label: t(ambientLocale(), "git.opt.keep"),
|
|
8769
|
+
value: "keep"
|
|
8770
|
+
}];
|
|
8771
|
+
triageCacheMarkPending(triageKey);
|
|
8772
|
+
return {
|
|
8773
|
+
status: "needs-decision",
|
|
8774
|
+
kind: "git-init",
|
|
8775
|
+
question: q,
|
|
8776
|
+
options: optsList,
|
|
8777
|
+
note: t(ambientLocale(), danger ? "tool.start.gitDecisionDanger" : "tool.start.gitDecision")
|
|
8778
|
+
};
|
|
8779
|
+
}
|
|
8780
|
+
} else if (s.ok && s.inRepo) {
|
|
6769
8781
|
const onMain = !!s.branch && s.branch.trim().toLowerCase() === "main";
|
|
6770
8782
|
const dirty = s.hasDirty;
|
|
6771
8783
|
const dirtyN = s.dirty.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
@@ -6836,6 +8848,7 @@ function registerTools(ctx) {
|
|
|
6836
8848
|
}
|
|
6837
8849
|
];
|
|
6838
8850
|
}
|
|
8851
|
+
triageCacheMarkPending(triageKey);
|
|
6839
8852
|
return {
|
|
6840
8853
|
status: "needs-decision",
|
|
6841
8854
|
question,
|
|
@@ -6845,6 +8858,7 @@ function registerTools(ctx) {
|
|
|
6845
8858
|
}
|
|
6846
8859
|
} catch (e) {}
|
|
6847
8860
|
}
|
|
8861
|
+
triageCacheSettle(triageKey);
|
|
6848
8862
|
return {
|
|
6849
8863
|
runId: startPipeline(parent, requirement, options, exec && exec.signal),
|
|
6850
8864
|
status: "running"
|
|
@@ -7233,7 +9247,7 @@ function registerTools(ctx) {
|
|
|
7233
9247
|
},
|
|
7234
9248
|
async execute(args) {
|
|
7235
9249
|
const id = args && typeof args.runId === "string" ? args.runId : null;
|
|
7236
|
-
return { ok: id ? cancelRun(id) : false };
|
|
9250
|
+
return { ok: id ? cancelRun(id, "tool") : false };
|
|
7237
9251
|
}
|
|
7238
9252
|
});
|
|
7239
9253
|
T({
|
|
@@ -7329,8 +9343,7 @@ function tryFlushPendingInjections(sessionId) {
|
|
|
7329
9343
|
text: teamflowContextText(pending.teamIcon, pending.teamName, pending.teamId)
|
|
7330
9344
|
}],
|
|
7331
9345
|
source: {
|
|
7332
|
-
kind: "plugin",
|
|
7333
|
-
plugin: "dsh-plugin-teamflow",
|
|
9346
|
+
kind: "plugin:dsh-plugin-teamflow",
|
|
7334
9347
|
form: "instructions"
|
|
7335
9348
|
}
|
|
7336
9349
|
}));
|
|
@@ -7348,6 +9361,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
7348
9361
|
constructor(ctx) {
|
|
7349
9362
|
super(ctx, "teamflow");
|
|
7350
9363
|
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"), ctx.get("llm"));
|
|
9364
|
+
setInstallCtx(ctx);
|
|
7351
9365
|
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
7352
9366
|
setSessionProjections(projectionCtx.get("sessionProjections"));
|
|
7353
9367
|
});
|
|
@@ -7622,7 +9636,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
7622
9636
|
if (!key) return null;
|
|
7623
9637
|
return this.itemDetail(kind, id, sessionId, key);
|
|
7624
9638
|
}
|
|
7625
|
-
start(sessionId, requirement, options) {
|
|
9639
|
+
async start(sessionId, requirement, options) {
|
|
7626
9640
|
const sid = typeof sessionId === "string" ? sessionId : null;
|
|
7627
9641
|
const req = typeof requirement === "string" && requirement.trim() ? requirement.trim() : null;
|
|
7628
9642
|
if (!sid || !req) return {
|
|
@@ -7637,6 +9651,29 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
7637
9651
|
tryFlushPendingInjections(sid);
|
|
7638
9652
|
try {
|
|
7639
9653
|
const opts = options && typeof options === "object" ? options : {};
|
|
9654
|
+
const pre = await clarificationPreflight(req, opts, agent, void 0, ambientLocale());
|
|
9655
|
+
if ("needsClarification" in pre) {
|
|
9656
|
+
triageCacheMarkPending(pre.cacheKey);
|
|
9657
|
+
return {
|
|
9658
|
+
ok: false,
|
|
9659
|
+
status: "needs-clarification",
|
|
9660
|
+
intent: pre.needsClarification.intent,
|
|
9661
|
+
blockers: pre.needsClarification.blockers
|
|
9662
|
+
};
|
|
9663
|
+
}
|
|
9664
|
+
if (pre.verdict) {
|
|
9665
|
+
const explicit = opts.mode;
|
|
9666
|
+
const up = guardrailUpgrade(explicit, !!opts.lite, pre.verdict.mode, { needDesign: opts.needDesign === true });
|
|
9667
|
+
if (up) {
|
|
9668
|
+
const fromMode = explicit !== void 0 || opts.lite ? explicit || "lite" : pre.verdict.mode;
|
|
9669
|
+
if (MODE_RANK[fromMode] < MODE_RANK[up]) pre.verdict.__upgradedFrom = fromMode;
|
|
9670
|
+
opts.mode = up;
|
|
9671
|
+
if (up !== "lite" && up !== "tech" && up !== "patch") opts.lite = false;
|
|
9672
|
+
}
|
|
9673
|
+
if (pre.verdict.needDesign) opts.needDesign = true;
|
|
9674
|
+
opts.__triage = pre.verdict;
|
|
9675
|
+
} else if (pre.error) opts.__triageError = pre.error;
|
|
9676
|
+
triageCacheSettle(pre.cacheKey);
|
|
7640
9677
|
return {
|
|
7641
9678
|
ok: true,
|
|
7642
9679
|
runId: startPipeline(agent, req, opts, void 0),
|
|
@@ -7656,7 +9693,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
7656
9693
|
ok: false,
|
|
7657
9694
|
error: t(ambientLocale(), "err.tool.missingRunId")
|
|
7658
9695
|
};
|
|
7659
|
-
return { ok: cancelRun(id) };
|
|
9696
|
+
return { ok: cancelRun(id, "ui") };
|
|
7660
9697
|
}
|
|
7661
9698
|
/** 工作区级 backlog 视图(自动按当前会话 workspace 隔离)。productOverride:全局面板按产品线 key 寻址。 */
|
|
7662
9699
|
backlog(sessionId, productOverride) {
|
|
@@ -7750,8 +9787,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
7750
9787
|
text: teamflowContextText(team.icon, teamNameOf(ambientLocale(), team), tid)
|
|
7751
9788
|
}],
|
|
7752
9789
|
source: {
|
|
7753
|
-
kind: "plugin",
|
|
7754
|
-
plugin: "dsh-plugin-teamflow",
|
|
9790
|
+
kind: "plugin:dsh-plugin-teamflow",
|
|
7755
9791
|
form: "instructions"
|
|
7756
9792
|
}
|
|
7757
9793
|
});
|