diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c7312d1..1753060 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Thinloop skills and continuity hooks for Issue-backed discovery, read-only project-status navigation, multi-Issue project DAG decomposition and execution, evidence-backed reengineering and delivery, and approved skill evolution.", - "version": "0.16.1" + "version": "0.17.0" }, "plugins": [ { "name": "thinloop", "source": ".", "description": "Discovery, Web UIUX, architecture, Next project-status navigation, Project DAG decomposition, Execute READY-wave orchestration, Reengineering gates, QuickDev delivery, maintenance, knowledge, and evolution skills with continuity checks; Project itself remains non-executing.", - "version": "0.16.1" + "version": "0.17.0" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3439d43..f65c00a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.16.1", + "version": "0.17.0", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/.codebuddy-plugin/marketplace.json b/.codebuddy-plugin/marketplace.json index c7312d1..1753060 100644 --- a/.codebuddy-plugin/marketplace.json +++ b/.codebuddy-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Thinloop skills and continuity hooks for Issue-backed discovery, read-only project-status navigation, multi-Issue project DAG decomposition and execution, evidence-backed reengineering and delivery, and approved skill evolution.", - "version": "0.16.1" + "version": "0.17.0" }, "plugins": [ { "name": "thinloop", "source": ".", "description": "Discovery, Web UIUX, architecture, Next project-status navigation, Project DAG decomposition, Execute READY-wave orchestration, Reengineering gates, QuickDev delivery, maintenance, knowledge, and evolution skills with continuity checks; Project itself remains non-executing.", - "version": "0.16.1" + "version": "0.17.0" } ] } diff --git a/.codebuddy-plugin/plugin.json b/.codebuddy-plugin/plugin.json index 70ed624..cb3bcf2 100644 --- a/.codebuddy-plugin/plugin.json +++ b/.codebuddy-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.16.1", + "version": "0.17.0", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8a4ff9f..e1e7ab3 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.16.1", + "version": "0.17.0", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index 8ae6d95..168c6bd 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.16.1", + "version": "0.17.0", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/EVALUATION.md b/EVALUATION.md index acfdb90..76defbd 100644 --- a/EVALUATION.md +++ b/EVALUATION.md @@ -367,3 +367,62 @@ node evals\score-runs.mjs ## 浏览器证据的实现后导入 当前页面评测先完成实现并保存代码快照,随后通过 `rescore.mjs --run <目录> --browser-evidence <文件>` 导入真实浏览器记录。记录绑定 runId、case、condition、代码快照及采集时间,产物必须存在且 SHA-256 匹配;离线重评分重新校验冻结记录与产物,不直接信任保存的通过标记。缺失证据保持 BLOCKED。具体格式及采集命令见 [当前版本评测说明](evals/thinloop/README.md#证据与重评分)。这项修复改善证据绑定与完整性,不代表已经重新测量模型整体收益。 + + +## 2026-09-05 真实仓库交付观察 + +本次 [Initiative #90](https://github.com/mindcarver/thinloop/issues/90) 已通过真实 GitHub 完成三个交付: +[评分证据 PR #95](https://github.com/mindcarver/thinloop/pull/95)、 +[三端更新 PR #96](https://github.com/mindcarver/thinloop/pull/96)、 +[完整交付评测 PR #97](https://github.com/mindcarver/thinloop/pull/97)。各自经历 Issue、PR、真实 CI、 +独立验收、合并及父 Execute 清理;验收证据分别见 +[#95 结论](https://github.com/mindcarver/thinloop/pull/95#issuecomment-5549049918)、 +[#96 结论](https://github.com/mindcarver/thinloop/pull/96#issuecomment-5549029298)、 +[#97 结论](https://github.com/mindcarver/thinloop/pull/97#issuecomment-5549163114)。 + +这是本次工程协作的观察性案例,区别于 delivery 套件的模拟 tracker;没有对照组,不能推出通用全链成功率或相对优势。QuickDev 精简的 #94 仍由自己的验收和关闭状态约束,不据上述三个交付提前声明完成。 + + +## 2026-09-05 QuickDev 精简成对实测 + +固定旧 QuickDev 提交 `b5067a8d8a61b316addeb7b18c345c8e164fa4be` 与候选冻结提交 +`193966b3a37d5481a3efce2b180d3cd29138331f`,其余11个 Skill固定为 +`370c3233f0c7f86900c90fe1c6a5f2f2f6c11e8a`。应用内 Codex CLI 0.153.0, +命令指定 `gpt-6-astra` / medium。清晰缺陷、CSV恢复、无关dirty保护各两次重复, +每次两臂,共12个真实subject;首pair预检通过后计入总数,没有剔除或补跑样本。 + +| 指标 | 旧版,6次 | 候选,6次 | +|---|---:|---:| +| 直接隐藏行为、原生测试和范围验收 | 6 PASS | 6 PASS | +| 完整入口实际读取证明 | 6/6 | 6/6 | +| input tokens合计 | 954203 | 679504 | +| output tokens合计 | 6092 | 6365 | +| cached input tokens合计 | 810624 | 586112 | +| cache-write input tokens合计(明确报告) | 0 | 0 | +| reasoning output tokens合计(CLI报告) | 89 | 61 | +| subject进程时长合计,ms | 874363 | 855040 | +| 已知command_execution事件 | 43 | 47 | +| 已知完成工具项 | 51 | 57 | +| description bytes / Unicode字符 | 934 / 412 | 346 / 164 | +| 入口 bytes / Unicode字符 | 18586 / 7936 | 6066 / 2854 | +| QuickDev总载荷 bytes / Unicode字符 | 40759 / 17884 | 32825 / 14736 | + +本组输入量差为 `954203 - 679504 = 274699`,占旧版合计 `28.79%`;输出量、 +命令和工具项反而增加,不能据此宣称全面提速或成本降低。未提供单价,成本为null。 +两臂各6次的用户输入计数与完成声明分类都保持unknown,不包装成零次干预或零次 +无证据声明;所有直接行为验收均有独立于最终措辞的测试与隐藏结果。 + +每对两臂并发,只交错启动顺序,不是串行A/B与B/A顺序平衡。12次均记录WebSocket +超时后回退HTTPS,因此时长含传输延迟、工具事件覆盖标为partial。全部入口通过 +当前识别器支持的完整stdout读取得到证明;识别器未覆盖所有分段sed/Python读法, +这轮未因此丢弃样本。协议显式调用并额外要求完整读取入口,不衡量自动路由或普通 +自动注入路径的Token开销;本地授权禁止外部Issue/PR/提交,未直接测紧凑Issue创建 +或模型自主GitHub全链收益。三个小任务各两次不能推出统计显著性或通用节省。 + +[逐样本指标与固定载荷hash](evals/compact/results/issue94.json) 和 +[运行方法](evals/compact/README.md) 可复核。完整脱敏轨迹、命令事件、安装文件hash、 +原生/隐藏结果、diff及报告保存在 +`/Users/carver/workspace/mindcarver/test/thinloop-compact-eval/20260905-issue94/`; +`evidence.sha256`覆盖168个文件,`replay.json`记录12/12离线直接重放PASS且代码快照未改变。 +临时认证已清理,秘密扫描0项;候选QuickDev载荷SHA-256为 +`3c5b8450c60c0c2e1d6134e2d20834242def5a4a2a3cd00de4d8ed97b6494e53`,最终Skill与该冻结载荷一致。 diff --git a/README.md b/README.md index 7b8c3c9..d02a5d0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

- v0.16.1 + v0.17.0   ISSUE-DRIVEN   diff --git a/docs/installation.md b/docs/installation.md index d4c24ab..a5f54a5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -301,6 +301,11 @@ codebuddy plugin update thinloop@thinloop --scope user 输出未解决状态交接并结束自动纠正;DSH 对同一 Agent 的未改变错误状态不重复 steer, 状态改变或恢复后可重新纠正。更新后新建会话使新 Hook 生效,版本与验证范围见 [`docs/releases/v0.16.1.md`](./releases/v0.16.1.md)。WorkBuddy 维持 SKIP;ZCode 后续交付按三端规则核验。 +- 升级到 v0.17.0:QuickDev 按当前阶段读取 Issue、证据、页面和发布参考;清晰低风险 + 单交付使用五节紧凑 Issue,复杂 PRD/UI/迁移按需展开。独立验收、三本账、真实 + 页面证据、高风险确认、main 核验和精确清理门继续有效。评分器保留 unknown, + 完整交付协议与真实模型证据分开记录;每次交付核对 ZCode、Claude Code、Codex, + 同版本载荷漂移也需修复。范围和测量限制见 [`v0.17.0`](./releases/v0.17.0.md)。 - 若从 v0.6.x 升级,另确认旧 `scd-dev-loop` 已消失。 更新后可以在 Thinloop 源码仓库运行只读检查器: diff --git a/docs/releases/v0.17.0.md b/docs/releases/v0.17.0.md new file mode 100644 index 0000000..c257078 --- /dev/null +++ b/docs/releases/v0.17.0.md @@ -0,0 +1,18 @@ +# Thinloop v0.17.0 + +## 范围 + +- QuickDev 主入口与描述精简,按当前阶段加载参考;清晰低风险单交付使用五节 Issue,PRD、重要页面/跨层设计、迁移和复杂边界按需展开。中文输出、用户主动确认偏好、READY 范围、独立验收、三本账、真实页面门、高风险批准、main 验证与精确清理仍为契约。 +- 完成声明评分识别有限范围的明确声明,无法可靠判断时保持 unknown;用户输入事件按结构化轨迹计数,证据覆盖不全不推测为零。 +- 新增完整交付协议回归,以及独立实现/验收 Agent 的最小真实模型试验;真实 Git 与模拟 Issue/PR tracker 明确分开,不冒充全模型 GitHub 交付。 +- 每次交付核对已安装的 ZCode、Claude Code 和 Codex。ZCode 从 SKIP 改为原生 CLI 的版本、完整技能载荷和运行时 Hook 检查;WorkBuddy 继续 SKIP。更新器使用原生 CLI/API,Claude 同版本内容漂移可通过保留数据的原生重装修复。 + +## 验证边界 + +- 本版本组装后263/263仓库测试、版本11表面、路由/评测定义、dry-run、图表、严格插件验证及完整交付协议通过。 +- QuickDev三任务×两重复×两臂的12次真实subject均通过直接验收;入口完整读取12/12。样本输入Token合计954203→679504,输出6092→6365,已知工具项51→57;这是特定显式读取协议的有限观察,不是普遍成本或速度结论。 + + +完整工程检查、成对实测和本次真实仓库交付观察在 [EVALUATION.md](../../EVALUATION.md) 中分别记录。显式调用后的三个局部 fixture 比较不衡量自动路由召回,也不衡量完整模型自主 GitHub 交付成功率。没有单价依据,成本保持 null;有限样本不能推出统计显著性或整体节省。 + +合并后核验三端加载的版本与载荷;新任务/会话才能可靠获得新指令。本次不升级宿主、不改变认证、不部署生产,也不自动创建发布 Tag。 diff --git a/evals/compact/README.md b/evals/compact/README.md new file mode 100644 index 0000000..df8cd3f --- /dev/null +++ b/evals/compact/README.md @@ -0,0 +1,26 @@ +# QuickDev 精简成对评测 + +只比较显式调用后的局部行为,不测自然语言自动路由、不测模型自主操作完整 GitHub 交付链。 +冻结方案见 [Issue94](https://github.com/mindcarver/thinloop/issues/94):三类 fixture、两次重复、两种 QuickDev 载荷,共12个真实 subject。 + +- `definition.json` 固定旧 QuickDev 提交、其余11个 Skill的公共提交、CLI/model/reasoning、同一局部授权 prompt 和交错顺序。只替换 QuickDev 载荷。 +- 候选先提交,`--candidate` 必须是完整 SHA。全部文件逐项 SHA-256;入口与总载荷 bytes/chars 和真实 Token 分开记录。 +- 每对最多两个并发进程,只交错启动顺序;这不是串行 A/B 与 B/A 的顺序平衡。Subject的 HOME、仓库、轨迹目录只用随机ID,条件标签保留在外部 manifest/results,不写入模型上下文。 +- 隔离 HOME 只临时复制现有 Codex 认证,复用现有 runner 的 `--ignore-user-config`、禁工具网络/浏览器/多Agent和workspace-write;不改全局配置、安装或认证。临时认证在finally清理。 +- 两臂均明确授权本地fixture实施,禁止外部Issue/PR、分支/工作树、提交和其他Agent。这是用户批准的局部例外,不能把结果当成完整外部交付契约的成功率。 + +```sh +node evals/compact/run.mjs --mode dry --candidate +# PATH应选择已冻结的Codex CLI 0.153.0;不安装/升级CLI。 +node evals/compact/run.mjs --candidate --pairs 1 --output /absolute/evidence/directory +# 首pair验证格式、实际完整读取QuickDev、usage和直接行为结果,再继续同一归档: +node evals/compact/run.mjs --candidate --pairs 6 --output /same/evidence/directory +``` + +续跑只读取完整配对,不重跑已保存样本;若定义或载荷变化,原目录拒绝继续,必须保留原始失败记录并新建归档。出现基础设施BLOCKED,或baseline通过而candidate失败,停止后续配对,先诊断并修复/回退相关精简后重测。 + +生成汇总:`node evals/compact/report.mjs /absolute/evidence/directory`。正式归档的 `evidence.sha256` 可用 `shasum -a 256 -c evidence.sha256` 核对;重新生成报告后需要重新封存对应hash,不能把旧清单当新报告的证明。 + +`observations/`保存原生测试、隐藏行为、范围/脏改动、恢复状态和已修复评分器结果;`traces/`保存完整脱敏命令轨迹和最终回答、安装文件hash及diff;`results/`记录完整入口实际stdout读取证明、真实usage、已知工具项/command_execution事件次数和unknown覆盖(单条shell内多个子命令仍是一条事件)。可用`evidence.json`逐文件核对归档。 + +未报告的cache/reasoning字段保持null;known工具计数不把未知事件推算为零。完成声明评分只识别明确整体声明,其他情况为unknown。没有价格来源,成本始终null。三个小fixture各两次不能支持统计显著性、通用节省比例或完整交付增益。原生Git/模拟tracker交付回归另见 [delivery](../delivery/README.md)。 diff --git a/evals/compact/definition.json b/evals/compact/definition.json new file mode 100644 index 0000000..bf24a57 --- /dev/null +++ b/evals/compact/definition.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "id": "issue94-quickdev-paired-v1", + "baselineQuickdevRef": "b5067a8d8a61b316addeb7b18c345c8e164fa4be", + "sharedSkillsRef": "370c3233f0c7f86900c90fe1c6a5f2f2f6c11e8a", + "model": "gpt-6-astra", + "reasoning": "medium", + "cliVersion": "codex-cli 0.153.0", + "repeats": 2, + "concurrency": 2, + "caseIds": ["clear-local-bug", "resume-interrupted-export", "preserve-dirty-readme"], + "promptPrefix": "请显式使用 $scd-quickdev,先完整读取 $CODEX_HOME/skills/scd-quickdev/SKILL.md,再按当前阶段需要读取参考。当前是用户已明确批准的隔离本地基准任务:本消息与仓库已有状态共同界定交付范围,授权直接在当前工作树实施;不要创建外部 Issue/PR,不创建分支或工作树,不提交或合并,不启动其他 Agent。恢复记录里的 Issue 地址仅为已有任务标识,本次不要求联网复核或重建 Issue。该本地授权覆盖技能中的外部交付流程。请完成下面的局部行为并运行直接测试,最后准确说明结果与未验证边界。", + "scope": "explicit-skill local behavior only; no real GitHub delivery; shared delivery protocol is a separate regression", + "priceUsd": null +} diff --git a/evals/compact/report.mjs b/evals/compact/report.mjs new file mode 100644 index 0000000..b5ba119 --- /dev/null +++ b/evals/compact/report.mjs @@ -0,0 +1,63 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const sum = values => values.length && values.every(Number.isFinite) ? values.reduce((a, b) => a + b, 0) : null; +export function summarize(results, identities) { + return Object.fromEntries(["baseline", "candidate"].map(condition => { + const rows = results.filter(row => row.condition === condition); + const identity = identities[condition]; + const entry = identity.inventory.find(file => file.file === "skills/scd-quickdev/SKILL.md"); + const tokens = Object.fromEntries(["inputTokens", "outputTokens", "cachedInputTokens", "cacheWriteInputTokens", "reasoningOutputTokens"] + .map(key => [key, { total: sum(rows.map(row => row.evidence?.usage?.[key])), known: rows.filter(row => Number.isFinite(row.evidence?.usage?.[key])).length }])); + return [condition, { + samples: rows.length, + verdicts: Object.fromEntries(["PASS", "FAIL", "BLOCKED"].map(verdict => [verdict, rows.filter(row => row.scored.verdict === verdict).length])), + tokens, costUsd: null, + durationMs: sum(rows.map(row => row.durationMs)), + knownCompletedCommands: sum(rows.map(row => row.evidence?.knownCompletedCommands)), + knownCompletedToolItems: sum(rows.map(row => row.evidence?.knownCompletedToolItems)), + toolCoveragePartialSamples: rows.filter(row => row.evidence?.toolCountCoverage === "partial").length, + userInputUnknownSamples: rows.filter(row => row.scored.metrics.userInterruptRequests === null).length, + completionDeclarationUnknownSamples: rows.filter(row => row.scored.facts?.completionDeclaration?.state === "unknown").length, + entryFullReadSamples: rows.filter(row => row.evidence?.entryFullyObserved).length, + knownFullReadBytes: sum(rows.map(row => sum((row.evidence?.reads ?? []).filter(read => read.fullContentObserved).map(read => identity.inventory.find(file => file.file === read.file).bytes)))), + entryBytes: entry.bytes, entryChars: entry.chars, payloadBytes: identity.bytes, payloadChars: identity.chars, + }]; + })); +} + +export function markdown({ manifest, results }) { + const arms = summarize(results, manifest.frozen.identities); + const lines = [ + "# QuickDev 精简成对实测", "", + `候选冻结提交:\`${manifest.frozen.candidateRef}\`;旧 QuickDev:\`${manifest.frozen.definition.baselineQuickdevRef}\`。`, + `CLI:${manifest.cli};模型:${manifest.frozen.definition.model};推理:${manifest.frozen.definition.reasoning}。`, + "", + "| 条件 | 样本 PASS/FAIL/BLOCKED | input | output | cached input | 总时长 ms | 已知命令 | 已知工具项 | 入口 bytes/chars | 总载荷 bytes/chars |", + "|---|---|---:|---:|---:|---:|---:|---:|---|---|", + ]; + for (const [condition, arm] of Object.entries(arms)) lines.push(`| ${condition} | ${arm.samples}: ${arm.verdicts.PASS}/${arm.verdicts.FAIL}/${arm.verdicts.BLOCKED} | ${arm.tokens.inputTokens.total ?? "unknown"} | ${arm.tokens.outputTokens.total ?? "unknown"} | ${arm.tokens.cachedInputTokens.total ?? "unknown"} | ${arm.durationMs ?? "unknown"} | ${arm.knownCompletedCommands ?? "unknown"} | ${arm.knownCompletedToolItems ?? "unknown"} | ${arm.entryBytes}/${arm.entryChars} | ${arm.payloadBytes}/${arm.payloadChars} |`); + lines.push("", "| pair | task | repeat | baseline input/output | candidate input/output | baseline/candidate verdict |", "|---:|---|---:|---|---|---|"); + for (const pair of manifest.frozen.schedule) { + const base = results.find(row => row.pair === pair.pair && row.condition === "baseline"); + const candidate = results.find(row => row.pair === pair.pair && row.condition === "candidate"); + if (!base || !candidate) continue; + const token = row => `${row.evidence?.usage.inputTokens ?? "unknown"}/${row.evidence?.usage.outputTokens ?? "unknown"}`; + lines.push(`| ${pair.pair} | ${pair.caseId} | ${pair.repeat} | ${token(base)} | ${token(candidate)} | ${base.scored.verdict}/${candidate.scored.verdict} |`); + } + lines.push("", "## 可观察边界", "", + "显式调用且额外要求完整读取入口,两臂授权和环境一致;不衡量自动路由或普通自动注入路径的Token开销。每对两臂并发,只交错启动顺序,不是串行顺序平衡。局部fixture禁止外部Issue/PR和提交,完整交付协议另行回归。bytes/chars是静态文件规模,不等于真实用量。", + "总时长是subject进程时长之和,两两并发,不能当作端到端墙钟时长;查看轨迹中的传输回退等基础设施事件后再解释时延。成本null;不宣称统计显著性或普遍节省。", + "完整stdout读取证明只覆盖能从命令轨迹直接核对的文件;不计自动注入或部分读取。knownFullReadBytes每样本每文件只计一次,不是总上下文Token。已知命令次数指command_execution事件数,单条shell里的多个子命令仍算一个事件。未知事件不算成确定工具,用户输入/完成声明缺少可支持证据时保持unknown。", "", + "```json", JSON.stringify(arms, null, 2), "```", ""); + return lines.join("\n"); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const output = path.resolve(process.argv[2]); + const manifest = JSON.parse(fs.readFileSync(path.join(output, "manifest.json"))); + const { results } = JSON.parse(fs.readFileSync(path.join(output, "summary.json"))); + fs.writeFileSync(path.join(output, "report.md"), markdown({ manifest, results })); + fs.writeFileSync(path.join(output, "comparison.json"), JSON.stringify(summarize(results, manifest.frozen.identities), null, 2) + "\n"); +} diff --git a/evals/compact/results/issue94.json b/evals/compact/results/issue94.json new file mode 100644 index 0000000..1d5b634 --- /dev/null +++ b/evals/compact/results/issue94.json @@ -0,0 +1,422 @@ +{ + "schemaVersion": 1, + "issue": 94, + "model": "gpt-6-astra", + "reasoning": "medium", + "cli": "codex-cli 0.153.0", + "candidateRef": "193966b3a37d5481a3efce2b180d3cd29138331f", + "baselineQuickdevRef": "b5067a8d8a61b316addeb7b18c345c8e164fa4be", + "sharedSkillsRef": "370c3233f0c7f86900c90fe1c6a5f2f2f6c11e8a", + "archive": "/Users/carver/workspace/mindcarver/test/thinloop-compact-eval/20260905-issue94", + "measurementScope": "explicit-skill local behavior only; no real GitHub delivery; shared delivery protocol is a separate regression", + "concurrency": 2, + "ordering": "concurrent pairs, alternating launch order only", + "payloads": { + "baseline": { + "sha256": "7a23c3e1a37f50d4607bd22656c4baa6e4524a0b2b498c3c758c5dc6d7d29c29", + "entryBytes": 18586, + "entryChars": 7936, + "descriptionBytes": 934, + "descriptionChars": 412, + "bytes": 40759, + "chars": 17884 + }, + "candidate": { + "sha256": "3c5b8450c60c0c2e1d6134e2d20834242def5a4a2a3cd00de4d8ed97b6494e53", + "entryBytes": 6066, + "entryChars": 2854, + "descriptionBytes": 346, + "descriptionChars": 164, + "bytes": 32825, + "chars": 14736 + } + }, + "arms": { + "baseline": { + "samples": 6, + "verdicts": { + "PASS": 6, + "FAIL": 0, + "BLOCKED": 0 + }, + "tokens": { + "inputTokens": { + "total": 954203, + "known": 6 + }, + "outputTokens": { + "total": 6092, + "known": 6 + }, + "cachedInputTokens": { + "total": 810624, + "known": 6 + }, + "cacheWriteInputTokens": { + "total": 0, + "known": 6 + }, + "reasoningOutputTokens": { + "total": 89, + "known": 6 + } + }, + "costUsd": null, + "durationMs": 874363, + "knownCompletedCommands": 43, + "knownCompletedToolItems": 51, + "toolCoveragePartialSamples": 6, + "userInputUnknownSamples": 6, + "completionDeclarationUnknownSamples": 6, + "entryFullReadSamples": 6, + "knownFullReadBytes": 158322, + "entryBytes": 18586, + "entryChars": 7936, + "payloadBytes": 40759, + "payloadChars": 17884 + }, + "candidate": { + "samples": 6, + "verdicts": { + "PASS": 6, + "FAIL": 0, + "BLOCKED": 0 + }, + "tokens": { + "inputTokens": { + "total": 679504, + "known": 6 + }, + "outputTokens": { + "total": 6365, + "known": 6 + }, + "cachedInputTokens": { + "total": 586112, + "known": 6 + }, + "cacheWriteInputTokens": { + "total": 0, + "known": 6 + }, + "reasoningOutputTokens": { + "total": 61, + "known": 6 + } + }, + "costUsd": null, + "durationMs": 855040, + "knownCompletedCommands": 47, + "knownCompletedToolItems": 57, + "toolCoveragePartialSamples": 6, + "userInputUnknownSamples": 6, + "completionDeclarationUnknownSamples": 6, + "entryFullReadSamples": 6, + "knownFullReadBytes": 98224, + "entryBytes": 6066, + "entryChars": 2854, + "payloadBytes": 32825, + "payloadChars": 14736 + } + }, + "samples": [ + { + "id": "986eae0b-ac0c-4cb6-acf4-5f57e3a4ed4c", + "pair": 0, + "repeat": 1, + "caseId": "clear-local-bug", + "condition": "baseline", + "verdict": "PASS", + "usage": { + "inputTokens": 139280, + "outputTokens": 1121, + "cachedInputTokens": 119168, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 9 + }, + "durationMs": 147130, + "knownCommands": 6, + "knownToolItems": 7, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "669c3bfb-083c-440f-9e5a-5c5711276d42", + "pair": 0, + "repeat": 1, + "caseId": "clear-local-bug", + "condition": "candidate", + "verdict": "PASS", + "usage": { + "inputTokens": 110185, + "outputTokens": 1152, + "cachedInputTokens": 94464, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 6 + }, + "durationMs": 151940, + "knownCommands": 8, + "knownToolItems": 10, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "f1bfb969-6e08-4572-885d-ae2347e4a784", + "pair": 1, + "repeat": 1, + "caseId": "resume-interrupted-export", + "condition": "candidate", + "verdict": "PASS", + "usage": { + "inputTokens": 111282, + "outputTokens": 1160, + "cachedInputTokens": 90752, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 9 + }, + "durationMs": 148544, + "knownCommands": 8, + "knownToolItems": 10, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "4ff9aaa1-7a36-4f4c-ab55-4ad6e8c6dc7b", + "pair": 1, + "repeat": 1, + "caseId": "resume-interrupted-export", + "condition": "baseline", + "verdict": "PASS", + "usage": { + "inputTokens": 199911, + "outputTokens": 1124, + "cachedInputTokens": 179328, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 30 + }, + "durationMs": 156275, + "knownCommands": 7, + "knownToolItems": 9, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "f008ffec-01b4-4aca-bc6a-a53d50058fdd", + "pair": 2, + "repeat": 1, + "caseId": "preserve-dirty-readme", + "condition": "baseline", + "verdict": "PASS", + "usage": { + "inputTokens": 138283, + "outputTokens": 892, + "cachedInputTokens": 109184, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 17 + }, + "durationMs": 144130, + "knownCommands": 8, + "knownToolItems": 9, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "9cfff24c-e328-47ac-8ef1-35257541890e", + "pair": 2, + "repeat": 1, + "caseId": "preserve-dirty-readme", + "condition": "candidate", + "verdict": "PASS", + "usage": { + "inputTokens": 106383, + "outputTokens": 777, + "cachedInputTokens": 92800, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 10 + }, + "durationMs": 142848, + "knownCommands": 9, + "knownToolItems": 10, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "53a828cb-fc04-4711-b0c3-7e4ac6495b84", + "pair": 3, + "repeat": 2, + "caseId": "clear-local-bug", + "condition": "candidate", + "verdict": "PASS", + "usage": { + "inputTokens": 109314, + "outputTokens": 1161, + "cachedInputTokens": 93952, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 6 + }, + "durationMs": 150532, + "knownCommands": 7, + "knownToolItems": 9, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "7a28b705-b598-4a52-bcfd-025adba5e8b1", + "pair": 3, + "repeat": 2, + "caseId": "clear-local-bug", + "condition": "baseline", + "verdict": "PASS", + "usage": { + "inputTokens": 139086, + "outputTokens": 1117, + "cachedInputTokens": 118784, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 10 + }, + "durationMs": 153889, + "knownCommands": 8, + "knownToolItems": 9, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "33807270-36da-4de2-9dc5-7fa45034034d", + "pair": 4, + "repeat": 2, + "caseId": "resume-interrupted-export", + "condition": "baseline", + "verdict": "PASS", + "usage": { + "inputTokens": 170542, + "outputTokens": 1134, + "cachedInputTokens": 150144, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 14 + }, + "durationMs": 127486, + "knownCommands": 7, + "knownToolItems": 9, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "7b80754e-f335-432c-b328-530ec57ab9b8", + "pair": 4, + "repeat": 2, + "caseId": "resume-interrupted-export", + "condition": "candidate", + "verdict": "PASS", + "usage": { + "inputTokens": 111935, + "outputTokens": 1126, + "cachedInputTokens": 97536, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 11 + }, + "durationMs": 113877, + "knownCommands": 8, + "knownToolItems": 10, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "79412710-b468-4c0d-acff-429da90f8d5b", + "pair": 5, + "repeat": 2, + "caseId": "preserve-dirty-readme", + "condition": "candidate", + "verdict": "PASS", + "usage": { + "inputTokens": 130405, + "outputTokens": 989, + "cachedInputTokens": 116608, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 19 + }, + "durationMs": 147299, + "knownCommands": 7, + "knownToolItems": 8, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + }, + { + "id": "96ec20ab-d749-4a48-a00a-ccb057454b33", + "pair": 5, + "repeat": 2, + "caseId": "preserve-dirty-readme", + "condition": "baseline", + "verdict": "PASS", + "usage": { + "inputTokens": 167101, + "outputTokens": 704, + "cachedInputTokens": 134016, + "cacheWriteInputTokens": 0, + "reasoningOutputTokens": 9 + }, + "durationMs": 145453, + "knownCommands": 7, + "knownToolItems": 8, + "unknownItemTypes": [ + "error" + ], + "entryFullyObserved": true, + "completionDeclaration": "unknown", + "userInputRequests": null + } + ], + "costUsd": null, + "secretScanFindings": 0, + "replay": "12/12 direct hidden/native/scope checks PASS; snapshots unchanged", + "limitations": [ + "explicit invocation plus extra full entry read", + "only local behavior, no GitHub Issue creation or autonomous merge in subjects", + "all 12 subjects reported WebSocket to HTTPS fallback; duration includes transport delay", + "tool coverage partial; completion declaration and user input count unknown for all samples", + "three tasks twice, no statistical significance or general cost/speed claim" + ] +} diff --git a/evals/compact/run.mjs b/evals/compact/run.mjs new file mode 100644 index 0000000..7f7bee4 --- /dev/null +++ b/evals/compact/run.mjs @@ -0,0 +1,180 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { createIsolatedHomes, cleanupIsolatedHomes, codexLoginStatus, runSubjectTurn } from "../discovery/runner/codex.mjs"; +import { captureRepositoryEvidence } from "../discovery/runner/fixture.mjs"; +import { createRedactor, scanTree } from "../discovery/runner/redact.mjs"; +import { ensureDir, parseArgs, parseJsonLines, relativeFiles, sha256, writeJson } from "../discovery/runner/lib.mjs"; +import { prepareFixture } from "../thinloop/runner/fixture.mjs"; +import { observeRepository } from "../thinloop/runner/observe.mjs"; +import { scoreObservation } from "../thinloop/runner/scoring.mjs"; + +export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +export const definition = JSON.parse(fs.readFileSync(new URL("definition.json", import.meta.url))); +const currentCases = JSON.parse(fs.readFileSync(path.join(root, "evals/thinloop/manifest.json"))).cases; +export const cases = definition.caseIds.map(id => currentCases.find(testCase => testCase.id === id)); + +function git(args) { + const result = spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); + if (result.status !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr}`); + return result.stdout; +} + +export function snapshot(ref, prefix = "skills/") { + if (!/^[a-f0-9]{40}$/.test(ref)) throw new Error("payload must be frozen at a full commit SHA"); + const files = git(["ls-tree", "-r", "--name-only", ref, "--", prefix]).trim().split("\n").filter(Boolean); + if (!files.length) throw new Error(`empty skill snapshot: ${ref} ${prefix}`); + return Object.fromEntries(files.map(file => [file, git(["show", `${ref}:${file}`])])); +} + +export function payloadIdentity(files) { + const inventory = Object.keys(files).sort().map(file => ({ file, sha256: sha256(files[file]), bytes: Buffer.byteLength(files[file]), chars: [...files[file]].length })); + return { sha256: sha256(JSON.stringify(inventory)), bytes: inventory.reduce((n, f) => n + f.bytes, 0), chars: inventory.reduce((n, f) => n + f.chars, 0), inventory }; +} + +export function schedule() { + return Array.from({ length: definition.repeats }, (_, repeat) => cases.map((testCase, index) => ({ + pair: repeat * cases.length + index, repeat: repeat + 1, caseId: testCase.id, + order: (repeat * cases.length + index) % 2 ? ["candidate", "baseline"] : ["baseline", "candidate"], + }))).flat(); +} + +export function prompt(testCase) { + return `${definition.promptPrefix}\n\n${testCase.prompt}`; +} + +export function eventEvidence(events, quickdevFiles) { + const commands = events.filter(event => event.type === "item.completed" && event.item?.type === "command_execution").map(event => event.item); + const reads = Object.entries(quickdevFiles).map(([file, content]) => { + const suffix = file.replace("skills/scd-quickdev/", ""); + const observed = commands.filter(item => item.exit_code === 0 && /\b(cat|sed|head|readFile)/.test(item.command ?? "") && (item.command ?? "").includes(suffix)); + return { file, matchingCommands: observed.map(item => ({ id: item.id, command: item.command, outputSha256: sha256(item.aggregated_output ?? "") })), + fullContentObserved: observed.some(item => (item.aggregated_output ?? "").includes(content.trim())) }; + }); + const usageEvents = events.filter(event => event.type === "turn.completed").map(event => event.usage); + const sum = key => usageEvents.length && usageEvents.every(value => Number.isFinite(value?.[key])) ? usageEvents.reduce((n, value) => n + value[key], 0) : null; + const knownTools = new Set(["command_execution", "file_change", "web_search", "todo_list"]); + const other = new Set(["agent_message", "reasoning", "plan"]); + const items = events.filter(event => event.type === "item.completed" && event.item).map(event => event.item); + const unknownItemTypes = [...new Set(items.filter(item => !knownTools.has(item.type) && !other.has(item.type)).map(item => item.type))]; + return { + reads, + entryFullyObserved: reads.find(read => read.file === "skills/scd-quickdev/SKILL.md")?.fullContentObserved === true, + knownCompletedCommands: commands.length, + knownCompletedToolItems: items.filter(item => knownTools.has(item.type)).length, + unknownItemTypes, + toolCountCoverage: unknownItemTypes.length ? "partial" : "known-completed-items", + usage: { inputTokens: sum("input_tokens"), outputTokens: sum("output_tokens"), cachedInputTokens: sum("cached_input_tokens"), + cacheWriteInputTokens: sum("cache_write_input_tokens"), reasoningOutputTokens: sum("reasoning_output_tokens") }, + }; +} + +function persisted(output, files, redactor) { + for (const file of relativeFiles(output)) { + if (file.startsWith("repositories/") && file.includes("/.git/")) continue; + const absolute = path.join(output, file); + const bytes = fs.readFileSync(absolute); + if (!bytes.includes(0)) fs.writeFileSync(absolute, redactor(bytes.toString("utf8")).text); + } + const leaks = scanTree(output, redactor); + if (leaks.length) throw new Error(`secret scan failed: ${JSON.stringify(leaks)}`); + writeJson(path.join(output, "evidence.json"), { files: relativeFiles(output).filter(file => file !== "evidence.json" && !file.includes("/.git/")).map(file => ({ file, sha256: sha256(fs.readFileSync(path.join(output, file))) })), payloads: files }); +} + +async function runOne({ trial, condition, output, snapshots, authFile, redactor }) { + const id = randomUUID(); // Subject-visible paths never reveal its condition. + const testCase = cases.find(testCase => testCase.id === trial.caseId); + const fixture = await prepareFixture({ workspaceRoot: output, runKey: id, testCase }); + const homes = createIsolatedHomes({ authFile, prefix: "thinloop-eval-" }); + const quickdev = snapshots[condition]; + const raw = ensureDir(path.join(output, "traces", id)); + let subject, evidence, infrastructure = { blocked: false }, login; + try { + const installed = { ...snapshots.shared, ...quickdev }; + for (const [file, content] of Object.entries(installed)) { + const target = path.join(homes.subject.codexHome, file); + ensureDir(path.dirname(target)); fs.writeFileSync(target, content); + } + const installedIdentity = payloadIdentity(Object.fromEntries(Object.keys(installed).map(file => [file, fs.readFileSync(path.join(homes.subject.codexHome, file), "utf8")]))); + writeJson(path.join(raw, "installed-payload.json"), installedIdentity); + login = await codexLoginStatus({ home: homes.subject, redactor }); + if (login.code !== 0) throw new Error("isolated existing Codex authentication unavailable"); + subject = await runSubjectTurn({ home: homes.subject, cwd: fixture.repo, prompt: prompt(testCase), model: definition.model, + reasoning: definition.reasoning, outputDir: raw, turn: 1, redactor, timeoutMs: 900_000, + onProgress: message => process.stdout.write(`${new Date().toISOString()} ${id}: ${message}\n`) }); + const parsed = parseJsonLines(fs.readFileSync(path.join(raw, "turn-1.jsonl"), "utf8")); + evidence = eventEvidence(parsed.events, quickdev); + if (subject.code !== 0 || subject.timedOut || !subject.lastMessage || !evidence.entryFullyObserved || + evidence.usage.inputTokens === null || evidence.usage.outputTokens === null || parsed.invalid.length) { + infrastructure = { blocked: true, reason: "subject completion, usage, or complete QuickDev read proof unavailable" }; + } + } catch (error) { + infrastructure = { blocked: true, reason: redactor(error.message).text }; + } finally { cleanupIsolatedHomes(homes.root); } + await captureRepositoryEvidence({ repo: fixture.repo, outputDir: ensureDir(path.join(raw, "diff")), turn: 1, redactor }); + const final = await observeRepository({ testCase, condition, repo: fixture.repo, baseline: fixture.baseline, lastMessage: subject?.lastMessage ?? "" }); + const observation = { + schemaVersion: 3, runKey: id, caseId: testCase.id, category: testCase.category, condition, repeat: trial.repeat, + infrastructure, baseline: fixture.baseline, final, subject: subject ?? { metrics: {}, lastMessage: "" }, pricing: {}, + }; + const scored = scoreObservation(observation, testCase); + const result = { ...trial, id, condition, payloadSha256: payloadIdentity(quickdev).sha256, + evidence, scored, durationMs: subject?.durationMs ?? null, secretRedactions: (login?.secretRedactions ?? 0) + (subject?.secretRedactions ?? 0) }; + writeJson(path.join(output, "observations", `${id}.json`), JSON.parse(redactor(JSON.stringify(observation)).text)); + writeJson(path.join(output, "results", `${trial.pair}-${condition}.json`), JSON.parse(redactor(JSON.stringify(result)).text)); + return result; +} + +export async function run({ output, candidateRef, pairs = 6, dry = false }) { + if (!Number.isInteger(pairs) || pairs < 1 || pairs > 6) throw new Error("pairs must be 1..6"); + const candidate = snapshot(candidateRef, "skills/scd-quickdev/"); + const snapshots = { baseline: snapshot(definition.baselineQuickdevRef, "skills/scd-quickdev/"), candidate, + shared: Object.fromEntries(Object.entries(snapshot(definition.sharedSkillsRef)).filter(([file]) => !file.startsWith("skills/scd-quickdev/"))) }; + const identities = Object.fromEntries(Object.entries(snapshots).map(([name, files]) => [name, payloadIdentity(files)])); + const frozen = { definition, candidateRef, identities, cases, schedule: schedule() }; + if (dry) { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), "thinloop-compact-dry-")); + try { for (const testCase of cases) await prepareFixture({ workspaceRoot: temp, runKey: randomUUID(), testCase }); } + finally { fs.rmSync(temp, { recursive: true, force: true }); } + return { status: "DRY_PASS", frozen }; + } + const cli = spawnSync("codex", ["--version"], { encoding: "utf8" }); + if (cli.status !== 0 || cli.stdout.trim() !== definition.cliVersion) throw new Error(`expected ${definition.cliVersion}; observed ${cli.stdout.trim()}`); + ensureDir(output); + const manifestFile = path.join(output, "manifest.json"); + if (fs.existsSync(manifestFile)) { + if (JSON.stringify(JSON.parse(fs.readFileSync(manifestFile)).frozen) !== JSON.stringify(frozen)) throw new Error("frozen definition or payload changed; use a new run directory"); + } else writeJson(manifestFile, { frozen, cli: cli.stdout.trim(), createdAt: new Date().toISOString(), costUsd: null }); + const authFile = path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "auth.json"); + const auth = JSON.parse(fs.readFileSync(authFile, "utf8")); + const redactor = createRedactor({ auth, userProfile: os.homedir() }); + const completed = []; + for (const trial of schedule().slice(0, pairs)) { + const resultFiles = trial.order.map(condition => path.join(output, "results", `${trial.pair}-${condition}.json`)); + if (resultFiles.some(fs.existsSync) && !resultFiles.every(fs.existsSync)) throw new Error("partial pair exists; retain it and use a new run directory"); + const results = resultFiles.every(fs.existsSync) + ? resultFiles.map(file => JSON.parse(fs.readFileSync(file))) + : await Promise.all(trial.order.map(condition => runOne({ trial, condition, output, snapshots, authFile, redactor }))); + completed.push(...results); + writeJson(path.join(output, "summary.json"), { complete: completed.length === 12, completed: completed.length, costUsd: null, results: completed }); + process.stdout.write(`pair ${trial.pair}: ${results.map(result => `${result.condition}=${result.scored.verdict}, read=${result.evidence?.entryFullyObserved}`).join("; ")}\n`); + if (results.some(result => result.scored.verdict === "BLOCKED") || + results.find(result => result.condition === "baseline").scored.verdict === "PASS" && + results.find(result => result.condition === "candidate").scored.verdict !== "PASS") break; + } + persisted(output, identities, redactor); + return { complete: completed.length === 12, results: completed, costUsd: null }; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const args = parseArgs(process.argv.slice(2)); + try { + const result = await run({ output: path.resolve(args.output || "work/compact-eval"), candidateRef: args.candidate, + pairs: Number(args.pairs || 6), dry: args.mode === "dry" }); + process.stdout.write(`${JSON.stringify({ status: result.status, complete: result.complete, subjects: result.results?.length })}\n`); + if (result.results?.some(result => result.scored.verdict !== "PASS")) process.exitCode = 2; + } catch (error) { process.stderr.write(`${error.stack}\n`); process.exitCode = 1; } +} diff --git a/marketplace.json b/marketplace.json index 1c1602a..e51f171 100644 --- a/marketplace.json +++ b/marketplace.json @@ -6,7 +6,7 @@ "name": "thinloop", "source": ".", "description": "Discovery, Web UIUX, architecture, Next project-status navigation, Project DAG decomposition, Execute READY-wave orchestration, Reengineering gates, QuickDev delivery, maintenance, knowledge, and evolution skills with continuity checks; Project itself remains non-executing.", - "version": "0.16.1" + "version": "0.17.0" } ] } diff --git a/skills/scd-quickdev/SKILL.md b/skills/scd-quickdev/SKILL.md index edb4154..d827358 100644 --- a/skills/scd-quickdev/SKILL.md +++ b/skills/scd-quickdev/SKILL.md @@ -1,202 +1,52 @@ --- name: scd-quickdev -description: "编码 Agent 被要求修改仓库时,把一个交付 Issue 从请求推进到经过验证并合并的代码:修复缺陷、增加功能、重构代码、修改配置、执行迁移或恢复未完成实施。定义不足的产品工作先交给 scd-discovery,多交付项目交给 scd-project;只接受用户直接选择或 scd-execute 波次选择的一个已确认 READY Issue;以该 GitHub Delivery Issue 作为交付边界和验收事实来源;存在已确认全新产品 PRD 时按其约束产品范围;先诊断缺陷再修复;隔离实质工作;开发完成后审计整张 Issue;页面变更必须通过真实浏览器交互与视觉验收;通过独立验收子 Agent 验证观察行为;创建拉取请求;把符合条件的变更合并到 main;确认合并版本后才关闭 Issue。不适用于仅建议、解释、只读评审、Initiative Issues、PLANNED 占位节点或 BLOCKED 项目节点。" +description: "编码 Agent 被要求修改仓库时,把清晰的单交付仓库变更从中文 Issue 推进到实现、验证、独立验收、PR 合并与清理。适用于缺陷、功能、配置、迁移或恢复;只执行明确选中的 READY Delivery Issue。需求未定用 scd-discovery,多交付拆分用 scd-project,状态查询用 scd-next。" --- # SCD 快速开发 -让清晰的仓库工作沿最短安全路径从意图到合并代码。流程不产生决策价值时保持隐形,但始终保留交付边界和证据。 +一个中文 GitHub Issue 是选中交付边界与验收的事实来源。完成声明不得超过观察证据;独立验收、真实页面门、高风险确认和清理关闭门始终有效。 -维护以下契约: +## 确定边界 -- 请求的结果、边界和验收必须清晰; -- 一个中文 GitHub Issue 是选中交付边界与验收的事实来源; -- 已确认的全新产品 PRD 继续负责产品级原因、用户、问题、MVP 范围、`FR-*` 需求和成功指标; -- 实施与完成声明必须匹配观察证据; -- Issue 的验收行为、实施任务账目和交付状态必须分别闭合; -- 页面变更必须由真实浏览器路径证明交互和视觉结果; -- 合并和部署权限必须与风险匹配。 +先读适用 `AGENTS.md`、`CLAUDE.md`、实时 Issue、Git 分支/远程/工作树和有关代码。保留用户无关变更,恢复现有任务记录,复用附近模式;不得为流程额外创建 Wiki、永久实施计划或角色体系。 -## 从仓库事实出发 +- **直接实施:** 结果、边界和可观察验收清晰,创建或确认一个中文 Issue 后继续。 +- **单点澄清:** 只有一个影响结果的歧义时,检查后问该问题。 +- **项目规划:** 多个可独立验收交付使用 `scd-project`;它不授权直接实现全部节点。 +- **需求发现:** 新产品或多个依赖产品决策未定时使用 `scd-discovery`。实施期间不得生成或重新定义 PRD;清晰的孤立变更和缺陷继续只使用 Issue。 -1. 阅读适用的 `AGENTS.md`、`CLAUDE.md` 和附近仓库说明。 -2. 检查分支、工作树、远程仓库和工作区;保留用户无关变更。 -3. 复用现有文档、测试、命名和实施模式。 -4. 提议新抽象前,先搜索并阅读目标代码。 -5. 开始重叠工作前,先恢复现有 QuickDev 任务记录。 -6. 存在作为依据的 GitHub Issue 时找到它,并视为产品契约。 -7. 请求来自 UIUX 时,阅读相关 `.scd/ux/.md`,要求 `status: ready`;前端交付还要解析视觉交付清单,检查每个视觉 ID 的项目文件、视口、状态和所需确认,练习适用的非生产原型,并使用共享接口契约;不得把 UX 接口需求当成 API。 -8. 请求来自 Architecture 时,阅读相关 `.scd/architecture.md` 或 `.scd/designs/.md`,要求 `status: ready`,并使用与生产者相同的格式感知证据解析规范机器可读契约。 -9. 请求来自 Project 时,阅读 Initiative 和选中的 Delivery Issue,确认节点在当前图版本中是已确认的 `READY` Issue,并且只把该 Delivery Issue 用作实施契约。 -10. Issue 引用产品 PRD 时,从默认分支读取精确的已确认版本,确认每个具名 `FR-*` 标识存在;Issue 与产品契约矛盾或悄悄扩大范围时停止。 +只接受一个明确选中、已确认的 `READY` Delivery Issue,可由用户选择或来自当前图版本的已确认 `scd-execute` 波次。以 GitHub Delivery Issue 作为交付边界和验收事实来源,不吸收兄弟任务。存在 PRD、Project、UX 或 Architecture 交接时,按下表核对相应权威和就绪状态后才实施。 -已就绪 UX 契约是体验交接,不是产品确认或前端架构。它与 Issue、保留视觉产物或尚未统一的共享接口决策冲突时,实施前只返回该缺口。 +默认不询问用户是否要先确认完整的 Issue 草案、实施方案和任务清单。用户没有主动要求确认时,记录 `需要确认:否`、`状态:默认免确认` 并继续普通自主交付流程。只有用户主动要求先看或先确认时,先做只读仓库检查,再展示具体草案,等待明确确认;确认前不得创建或更新 Issue、修改仓库或开始实施。用户已经选择确认后,方案、任务、范围或验收实质改变时再次取得确认。这不是第二次产品确认。 -实施期间不得生成或重新定义 PRD。Discovery 生成已确认全新产品 PRD 时消费它;清晰的孤立变更和缺陷继续只使用 Issue。不得仅为满足本技能而生成项目 Wiki、永久实施计划、角色系统、命令套件、工作树、额外子 Agent 仪式或 TDD 仪式。下方要求的独立验收者是唯一默认子 Agent 角色。 +Issue 标题和正文都必须使用中文,代码标识、命令、路径、文件名、协议字段和机器状态令牌保持规范形式。 -## 选择最轻量且充分的路径 +## 实施与验收 -从请求和仓库派生结果、边界和可观察验收,然后在内部选择: +1. 创建/更新 Issue 与隔离任务时读取 Issue 契约;清晰低风险任务用紧凑模板。用户授权普通任务局部 Issue、分支、推送、PR、符合条件的合并和清理,不能创造缺失凭据或越过高风险边界。 +2. 缺陷先复现、检查因果根因、添加或找出会失败的回归测试,再作最小修复。原因进入框架或依赖时,在认定应用代码有错前查其官方 Issue 跟踪器;无法证实则保持 `Unconfirmed`。只改任务范围,证据使范围扩大时重新路由。 +3. 运行直接练习变更的可行最强检查与仓库要求的构建/相关测试,把每个验收项映射到已观察证据、`UNVERIFIED` 或具名阻塞。失败先区分当前回归与无关基线,不伪造完成,不无界重试。 +4. 实际差异影响页面、路由、组件交互、可见样式或响应式行为时,无论是否调用过 UIUX,都必须执行页面验收门:真实浏览器操作及视觉证据,缺失则 `BLOCKED`。触发时读取页面参考,不以测试/API绿色替代。 +5. 工程完成后读取发布契约,审计整个 Issue 的验收闭合、实施账目闭合和交付状态闭合。把最终验收交给独立的新上下文子 Agent;它必须亲读 Issue/差异并直接验证,不得只依赖实施 Agent 摘要。创建拉取请求并等待必需 CI;只有 `PASS` 才能合并到 `main`;`FAIL` 修复后重验,`BLOCKED` 保持开放。 +6. 合并前保留高风险人工门:身份验证/授权、支付、破坏性数据、密钥/隐私/合规、生产或不可逆动作;自动部署生产的合并也属生产操作。不得弱化仓库保护。合并后读取远端默认分支和合并提交,确认 `main` 包含的任务差异就是独立验证过的版本,完成精确资源清理,再重新读取 Issue,确认完成审计三本账仍闭合后显式关闭;PR 只用 `Refs`,不自动关闭 Issue。 -- **直接实施:** 结果、边界和可观察验收清晰。创建或确认 GitHub Issue,不追加产品问题,直接继续。 -- **单点澄清:** 一个答案即可使任务可执行。询问该实质性问题,更新 Issue,然后继续。 -- **项目规划:** 请求跨越多个可独立验证交付,每个都需要自己的 Issue 和交付通道,或存在跨 Issue 硬依赖。使用 `scd-project`;不得把 Initiative 或项目清单当成一个 QuickDev 任务。仅因实施规模而需要多个拉取请求,不会触发 Project。 -- **需求发现:** 多个依赖的产品决策或高成本产品边界仍未确定。使用 `scd-discovery`,取得明确确认,然后从其 Delivery Issue 或 `scd-project` 交接继续。 +其他技能委派实施时,以其更窄交付边界为准;`scd-evolve` 试验不授权提交、推送、拉取请求或合并。直接 QuickDev 清理自己创建的资源;父 Execute 创建的通道资源由父清理,通道返回精确资源清单,清理未闭合不得关闭 Issue。 -除非用户询问,否则不公布路径名称。新产品、应用、插件、服务或系统通常使用 Discovery;一个孤立歧义无需完整 Discovery。 +## 按阶段加载 -## 写入 Issue 前解决方案确认偏好 +只读取当前条件命中的参考;不因未来可能需要而一次读取全部。 -默认不询问用户是否要先确认完整的 Issue 草案、实施方案和任务清单。用户没有主动要求确认时,记录 `需要确认:否`、`状态:默认免确认`,创建或更新中文 Issue,然后继续普通自主交付流程。沉默、普通实施请求或“开始做”都不视为要求确认。 +| 当前行动/条件 | 读取 | +|---|---| +| 创建或更新 Issue、分支/工作树隔离 | [issue-delivery-contract.md](references/issue-delivery-contract.md) | +| 产品/范围歧义,或 PRD、Project、UX、Architecture 交接 | [scope-contract.md](references/scope-contract.md) | +| Issue 有 PRD、重要页面/跨层设计、迁移/兼容性复杂度 | [extended-issue.md](references/extended-issue.md),仅展开命中章节 | +| 选择验证或表述未验证边界 | [evidence-contract.md](references/evidence-contract.md) | +| 真实页面差异或 UX 视觉交付 | [page-acceptance.md](references/page-acceptance.md) | +| 工程完成,准备 PR/独立验收/合并/清理关闭 | [release-contract.md](references/release-contract.md) | +| 任务跨会话、暂停或恢复,需要创建/修改/删除后备状态 | [continuity-contract.md](references/continuity-contract.md) | -只有用户主动要求先看或先确认方案、Issue 或任务时,才启用方案确认门: +GitHub Issue 始终是持久事实来源;确需恢复才使用 [current-task.md](assets/current-task.md) 创建 `.scd/tasks/current.md`,只保存恢复增量和一个下一行动,停止/压缩前更新,不暂存提交;合并后按清理所有权删除。 -- 先完成让方案具体化所需的只读仓库检查;然后展示完整的中文 Issue 正文草案,其中必须包含实施方案和可验证任务,并等待明确确认。确认前不得创建或更新 Issue、修改仓库或开始实施。 -- 对现有 Issue,展示精确的拟修改内容、实施方案和任务。确认后先把已确认契约写回 Issue,再实施。 -- 用户已经选择确认时,后续证据实质改变已确认的方案、任务、范围或验收,必须展示差异并再次取得确认后才能继续。 - -这是用户主动选择后才生效的流程偏好,不是第二次产品确认,也不是创建 `plan.md` 的理由。Issue 继续作为持久方案与任务事实来源。 - -QuickDev 创建或更新的每个 Issue 标题和正文都必须使用中文,包括验收、实施任务、验证更新和状态说明。代码标识、命令、路径、文件名、协议字段和机器状态令牌在翻译会改变含义时保持原样。 - -## 执行中范围变化时重新选择路径 - -路径选择不是一次性门。诊断或实施一个交付期间,只要面前工作不再受选中 Issue 约束,就重新评估: - -- 反复修复 Issue 验收之外的相邻问题,例如不同根因、层或组件; -- 用户提供自己的设计或架构方案,或要求重构流程而不是修复报告缺陷; -- 证据表明真实变更跨越多个可独立验证交付,或依赖 Issue 未授权的产品决策。 - -出现任一情况时,停止扩大当前分支。明确说出新边界,把开放产品决策交给 `scd-discovery`,把有硬依赖的多交付工作交给 `scd-project`;让已选 Issue 完成或被明确取代。缺陷修复演变为重新设计时,应切换新路径,而不是把缺陷修复做大。 - -这项检查用于识别变化,不用于增加仪式。进行中的变更仍符合 Issue 验收时继续执行。 - -用户明确要求实施或使用 QuickDev,会授权指定仓库内普通的任务局部 Issue、分支、推送、拉取请求、符合条件的合并和清理步骤。它不授权高风险合并、生产部署、在线迁移、无关变更或仓库以外访问。 - -任务资源遵循创建者所有权:直接 QuickDev 创建的任务分支、工作树和连续性状态由 QuickDev 清理;父 `scd-execute` 创建的通道资源由父 Execute 清理。QuickDev 必须把精确资源清单和状态交回清理所有者,且清理证据完成前不得把 Issue 声明为可关闭。 - -其他 Thinloop 技能委派有边界的实施时,以其更窄交付边界为准。不得把局部试验或选中修复扩大成独立 QuickDev 的 Issue 到合并流程。特别是 `scd-evolve` 试验不授权提交、推送、拉取请求或合并,因为 Evolve 把这些操作保留在单独用户授权之后。 - -`scd-project` 不授权实施所有项目节点。只接受一个明确选中、已确认的 `READY` Delivery Issue。它可以由用户直接选择,也可以来自精确 Initiative 图版本仍有效的已确认 `scd-execute` 波次。Reengineering 波次通过额外的故障关闭门后,经 Execute 进入 QuickDev。拒绝 Initiative、PLANNED 占位节点、BLOCKED 节点、陈旧图版本或吸收兄弟 Issues 的请求,并在创建分支前返回精确项目缺口。 - -对 PRD 管理产品,还要拒绝缺失、草稿、未提交、已被取代或相互矛盾的 PRD 引用,以及具名 `FR-*` 标识不在已确认版本中的 Issue。把产品契约缺口返回 Discovery,不得猜测预期范围。 - -可能存在歧义或范围扩张时阅读 `references/scope-contract.md`。创建或更新 Issue、分支、工作树、拉取请求、合并或 UAT 交接前阅读 `references/issue-delivery-contract.md`。 - -## 先诊断缺陷,再改变行为 - -对于缺陷: - -1. 记录观察症状和预期行为; -2. 通过最窄且可靠的路径复现; -3. 形成竞争性假设并检查因果路径; -4. 因果路径进入框架或依赖时,在认定应用代码有缺陷前,搜索其官方 Issue 跟踪器中的匹配报告; -5. 找到因果根因,而不是修补可见表象; -6. 添加或找出能针对该缺陷失败的回归测试; -7. 实施最小且连贯的修复; -8. 重新运行同一路径和与风险匹配的周边检查。 - -无法复现时,根因保持 `Unconfirmed`,说明缺失证据,不得把相关性表述为因果。用户提供的原因仍是假设,直到仓库或运行时证据支持。 - -## 实施最小且连贯的变更 - -- 针对性检查代码后,把中文实施清单加入作为依据的 Issue;不得复制到本地规格。 -- 路径清晰时直接实施。 -- 只有有依赖顺序的工作确实获益时,才使用简短对话计划。 -- 遵循现有测试与架构,除非它们正是缺陷原因。 -- 无关清理留在分支和拉取请求之外。 -- 证据改变产品可见行为、范围、数据或隐私边界、权限、不可逆操作或验收时,先更新 Issue 再继续。只有受影响的产品决策需要再次确认。 - -只有证据显示真实风险时才提升流程。 - -## 验证工程验收 - -声明成功前,对变更行为运行可行的最强证据。优先顺序: - -1. 聚焦行为测试或回归测试; -2. 相关类型检查、代码检查、构建或更广测试; -3. 带可观察输出的运行时、API 或 UI 操作; -4. 只有执行不可用时才使用静态检查。 - -检查退出码和有意义的输出。把每个验收项映射到已观察证据、`UNVERIFIED` 或具名阻塞。不得用不相关的绿色检查当证据。 - -实际差异修改面向用户的页面、路由、组件交互、可见样式或响应式行为时,无论是否调用过 UIUX,都必须执行页面验收门。启动真实应用,通过用户可见的导航、按钮、表单和其他控件完成受影响关键旅程;直接调用 API、只打开首页、构建通过、组件存在或静态代码检查都不能替代页面行为证据。必需浏览器路径无法运行时记录确切阻塞,最终验收只能返回 `BLOCKED`。 - -页面验收必须记录页面或路由、已触发状态、代表性桌面和窄屏视口、旅程或验收标识、适用的视觉 ID、操作证据和结果。检查布局、裁切、遮挡、响应式重排、键盘与焦点、相关控制台错误和失败网络请求;只覆盖产品和差异真实触发的加载、空数据、成功、失败、权限、部分成功或恢复状态,不为填表制造状态。证据保存到仓库已有测试产物位置、CI artifact 或 Issue/PR 可访问附件,不要求把临时截图加入生产源码。 - -存在就绪 UX 交接时,同时依据三类权威实施和验证:UX 契约负责行为、状态与响应式规则,视觉 ID 对应的项目内视觉产物负责已标记视口的布局、层级、密度与外观,共享机器契约负责数据、操作、权限和错误语义。实现后把实际页面证据与视觉 ID 逐项比较,并记录允许差异。UX 契约标记“实现后最终视觉确认:需要”时,在独立验收完成前向用户展示设计与实际页面对照并取得明确确认;普通局部 UI 和标记“不需要”的页面仍由独立验收者自主决定,不增加人工门。 - -交付前,审查完整的 Issue 专属差异,检查正确性、安全、验收覆盖、意外文件和回归风险。修复范围内发现,并把验证记录到中文 Issue 和拉取请求。 - -## 审计整个 Issue 是否完成 - -实施和父 Agent 工程检查结束后,重新读取实时 Issue、精确任务差异和交付状态,把完成审计写回同一中文 Issue。先完成独立验收前可确定的字段;独立结论和合并版本明确标记为待完成,不得伪装为已闭合。最终关闭前必须分别闭合三本账: - -1. **验收闭合:** 每个验收标识都有直接观察的 `PASS` 证据;任何 `FAIL`、`BLOCKED`、`UNVERIFIED` 或缺失映射都阻止完成。 -2. **实施账目闭合:** 每个实施任务标记为 `DONE`、`SUPERSEDED` 或 `N/A`;`SUPERSEDED` 和 `N/A` 记录原因。任务状态说明实施路径,不替代行为验收。 -3. **交付状态闭合:** Issue 专属差异没有无关文件,必需检查已完成,没有未解决的阻塞性评审意见或未知项;页面任务还必须包含完整页面验收矩阵和适用的最终视觉确认。 - -任务资源清理属于交付状态。清理所有者必须分别证明 `main` 已同步、任务工作树不存在、本地任务分支不存在、远端任务分支不存在、任务连续性状态已删除或不适用。任何任务自有资源仍存在、状态未验证或脏工作树无法安全删除,都阻止关闭 Issue;不得把清理降级成自然交接中的剩余建议。 - -完成审计必须列出精确 Issue、分支或提交、拉取请求、验收结果、任务状态、页面验收适用性和剩余阻塞。独立验收前它是待复核的候选审计;独立验收写入结论,合并后再写入远端 `main` 提交并完成最终审计。不得因为复选框已勾选、提交存在、拉取请求可合并或测试总数为绿色就推断 Issue 完成。 - -实施和父 Agent 工程检查完成后,把最终验收委派给一个独立的新上下文子 Agent。验收者必须独立阅读作为依据的 Issue、验收项、仓库说明和真实 Issue 专属差异;必须运行直接练习变更行为的可行最强检查,包括适用时的浏览器、真实模型或生成产物验证;不得只依赖实施 Agent 摘要,也不得修改产品代码。 - -页面验收门由真实差异触发,不属于可选的“适用时”判断。验收者发现页面差异时,必须亲自复核页面验收矩阵,并通过浏览器练习关键用户路径;缺少浏览器、目标运行环境、必需状态或视觉证据时返回 `BLOCKED`,不得降级为静态检查。验收者还要核对完成审计三本账,不得把未闭合任务、未知项或交付状态提升为 `PASS`。 - -验收者只返回一个有证据支持的结果: - -- **PASS:** 每个验收项都有直接观察证据; -- **FAIL:** 变更行为违反验收项;把发现返回实施,修复后重复独立验收; -- **BLOCKED:** 必需验证无法运行;记录阻塞并保持 Issue 开放。 - -只有 `PASS` 授权交付和后续关闭 Issue。不相关测试套件通过、用静态提示词检查代替真实模型行为,或用假运行时验证真实环境验收项,都不能产生 `PASS`。 - -选择检查或报告不完整证据时,阅读 `references/evidence-contract.md`。 - -## 通过拉取请求交付 - -实施和工程验证通过后: - -1. 只提交 Issue 专属差异并推送任务分支; -2. 创建拉取请求,正文使用 `Refs #` 并提供验收证据; -3. 等待必需 CI 和仓库检查; -4. 解决范围内失败并重新运行受影响验证; -5. 完成审计和独立验收 `PASS` 后,交付契约允许时合并到 `main`; -6. 无论合并命令成功还是返回非零,都重新读取拉取请求状态、head 和合并提交;不得从命令退出码单独推断合并成败; -7. 同步本地 `main`,读取远端默认分支和合并提交,确认 `main` 包含的任务差异就是独立验证过的版本;合并、部署或环境状态可能改变结果时,在 `main` 上重新运行受影响验收路径; -8. 按 `references/issue-delivery-contract.md` 清理本次任务拥有的精确工作树、本地任务分支、远端任务分支和连续性状态;不得仅依赖 `gh pr merge --delete-branch`; -9. 验证任务工作树、本地任务分支和远端任务分支均不存在,且 `main` 与远端一致; -10. 重新读取 Issue,确认完成审计三本账仍闭合,并且清理账本闭合,附加合并提交和最终证据后关闭 Issue。 - -不得在合并时关闭 Issue,也不得使用拉取请求自动关闭语法。只有独立验收返回 `PASS` 后,才显式关闭。`FAIL` 时把观察结果记录到同一 Issue 并重新进入 QuickDev;`BLOCKED` 时记录缺失证据并保持 Issue 开放。 - -## 只在需要时保留连续性 - -只有任务可能跨会话或上下文压缩、存在多条独立验收路径、包含重要交接决策或已暂停时,才使用持久本地状态。 - -GitHub Issue 继续是权威来源。确实需要本地恢复状态时,从 `assets/current-task.md` 创建 `.scd/tasks/current.md`,引用 Issue,并只保存恢复所需增量。每个工作树最多保留一份后备记录。 - -停止或上下文压缩前,及时更新其状态、证据和一个下一行动。成功合并后删除后备记录,不要把它暂存到提交中。 - -创建、更新、恢复或删除后备状态前,阅读 `references/continuity-contract.md`。 - -## 自然交接 - -普通任务只报告: - -- 已合并结果和拉取请求; -- 重要变更位置; -- 已观察工程验证; -- 独立验收结果和 Issue 状态; -- 剩余风险或未验证工作。 - -除非发生流程升级或用户询问,否则不要打印契约名称、阶段标签或工作流回顾。 - -## 资源 - -- `references/scope-contract.md`:实质歧义与范围控制。 -- `references/issue-delivery-contract.md`:中文 Issue、隔离、PR、合并和独立验收规则。 -- `references/evidence-contract.md`:风险自适应验证与完成表述。 -- `references/continuity-contract.md`:后备状态结构与生命周期。 -- `assets/current-task.md`:后备恢复模板。 +自然交接只报告已合并结果/PR、直接验证、独立结论/Issue 状态及剩余未验证边界,不宣读流程。 diff --git a/skills/scd-quickdev/references/evidence-contract.md b/skills/scd-quickdev/references/evidence-contract.md index 08b1009..26de100 100644 --- a/skills/scd-quickdev/references/evidence-contract.md +++ b/skills/scd-quickdev/references/evidence-contract.md @@ -49,43 +49,6 @@ 验收者不得把 `UNVERIFIED` 或部分验证行为提升为 `PASS`。 -## 交付完成审计 - -开发完成不等于 Issue 完成。独立验收前,用实时 Issue、精确差异和交付状态建立三类闭合记录: - -| 账目 | 必须闭合的内容 | 阻塞完成的状态 | -|---|---|---| -| 验收 | 每个验收标识及其直接行为证据 | `FAIL`、`BLOCKED`、`UNVERIFIED`、缺失映射 | -| 实施 | 每个任务为 `DONE`、`SUPERSEDED` 或 `N/A` | 无状态任务;后两者缺少原因 | -| 交付 | 精确差异、必需检查、阻塞性评审、未知项、页面证据 | 无关差异、失败检查、未解决阻塞或缺失证据 | - -把审计结果写入 Issue 的 `## 完成审计`。独立验收前填入可确定的候选结果,把独立结论和合并版本保持为待完成;验收者写入结论,合并后再补远端 `main` 提交并执行最终闭合检查。任务复选框只记录计划执行情况;即使全部为 `DONE`,也不能替代验收项的直接观察证据。计划合理变化时使用 `SUPERSEDED` 或 `N/A` 并解释,不得伪造完成。 - -## 页面验收门 - -实际差异影响面向用户的页面、路由、组件交互、可见样式或响应式行为时,页面验收自动适用,不依赖 UIUX 契约是否存在。必须: - -1. 列出受影响页面或路由,以及共享壳层、导航、模态框或横跨路由的组件; -2. 启动真实应用,通过用户可见控件进入受影响路径并完成关键旅程;不得用 API、数据库写入或内部函数调用替代 UI 操作; -3. 覆盖差异真实触发的加载、空数据、成功、失败、权限、部分成功和恢复状态; -4. 在布局真实变化时覆盖代表性桌面和窄屏视口; -5. 存在 UX 视觉交付时,把实际结果逐项映射到视觉 ID 并记录允许差异;不存在时把视觉 ID 标为 `N/A`,仍需验证 Issue 定义的可见结果; -6. 检查布局、裁切、遮挡、溢出、响应式重排、键盘顺序、焦点与基本无障碍名称; -7. 检查与旅程相关的控制台错误和失败网络请求,并区分当前回归与已知无关基线; -8. 保存可复现的截图、录屏、浏览器追踪或等价证据引用。 - -在 Issue 中记录: - -```markdown -| 页面/路由 | 状态 | 视口 | 旅程/验收 | 视觉 ID | 操作与证据 | 结果 | -|---|---|---|---|---|---|---| -| /example | success | 1440×900 | A2 | UI-001 | <截图或追踪;操作摘要> | PASS | -``` - -只打开应用、只观察首页、只运行组件测试或只生成截图都不能证明交互旅程完成。必需浏览器、运行环境、状态或视觉证据不可用时,记录具体缺口并返回 `BLOCKED`。 - -UX 契约标记需要实现后最终视觉确认时,在独立验收前把设计视觉和实际页面按视觉 ID、状态和视口并排展示给用户。用户确认是该验收项的直接证据;普通页面任务不自动增加人工确认。 - ## 失败与基线 检查失败时: @@ -104,3 +67,5 @@ UX 契约标记需要实现后最终视觉确认时,在独立验收前把设 - **受阻:** 必需实施或验证步骤无法推进。 “完成”“已修复”或“可用”的范围不得超过证据允许范围。 + +工程完成审计与独立验收前读取 [release-contract.md](release-contract.md);真实页面差异必须读取 [page-acceptance.md](page-acceptance.md),不能以本表替代浏览器验收。 diff --git a/skills/scd-quickdev/references/extended-issue.md b/skills/scd-quickdev/references/extended-issue.md new file mode 100644 index 0000000..8243859 --- /dev/null +++ b/skills/scd-quickdev/references/extended-issue.md @@ -0,0 +1,44 @@ +# 复杂 Issue 的条件章节 + +只在 PRD、重要页面/跨层设计、迁移/兼容性或多个复杂边界实际出现时读取;保留基础模板的稳定验收/任务标识与三本账。只展开命中的部分,不把所有字段变成每次必填。 + +## 产品追溯 + +有已确认 `.scd/product/prd.md` 时补充: + +- PRD:精确位置;已确认版本:正整数。 +- 需求:`FR-001`、其余具名标识。 +- QuickDev 必须确认精确的已确认 PRD 版本可从默认分支访问,且每个具名 `FR-*` 标识存在。缺失、草稿、未提交、陈旧、矛盾或扩大范围的引用返回 Discovery,不能作为 READY 交付实施。 + +## 复杂边界与方案 + +需要独立说明多个边界或技术决定时,可将基础模板拆为: + +- `## 结果`、`## 用户问题`、`## 范围内`、`## 范围外`; +- `## 已确认决策`、`## 方案确认`(需要确认:是,或否;状态:已确认,或默认免确认); +- `## 失败与边界场景`、`## 验收条件`、`## 实施方案`、`## 实施任务`; +- `## 验证`、`## 完成审计`、`## 未知项`。 + +重要 UI:引用就绪 UX、视觉 ID 和共享机器契约,把浏览器证据矩阵写入验收;页面门见 [page-acceptance.md](page-acceptance.md)。 + +迁移/兼容性:记录保留行为、受影响数据/接口、失败恢复与回滚验证。触及身份验证或授权、支付、破坏性数据、密钥/隐私/合规、生产基础设施或不可逆操作时,保留明确人工确认,不能因为模板精简而省略。 + +## 交付阶段审计 + +准备独立验收时,按 [release-contract.md](release-contract.md) 展开必要证据: + +### 验收闭合 + +每个 A 标识的直接观察结果与证据。 + +### 实施账目 + +每个 T 标识的 DONE、SUPERSEDED 或 N/A;后两者说明原因。 + +### 交付状态 + +精确 Issue/head/PR、必需检查、独立结果、合并版本、未知项与适用页面证据。 + +### 清理状态 + +清理所有者、main 同步、精确任务工作树、本地任务分支、远端任务分支和连续性状态。只记录实际拥有的资源;不存在的资源可合并说明不适用,不削弱清理关闭门。 diff --git a/skills/scd-quickdev/references/issue-delivery-contract.md b/skills/scd-quickdev/references/issue-delivery-contract.md index d6504da..31b6c6d 100644 --- a/skills/scd-quickdev/references/issue-delivery-contract.md +++ b/skills/scd-quickdev/references/issue-delivery-contract.md @@ -1,6 +1,6 @@ -# Issue 交付契约 +# Issue 创建与任务隔离 -创建 GitHub Issue、隔离任务、创建拉取请求、决定合并和独立 Agent 验收时使用本参考。 +创建或更新 Issue、选择分支/工作树时读取。PR、独立验收、合并与清理时才读取 [release-contract.md](release-contract.md)。 ## 事实来源 @@ -24,111 +24,41 @@ QuickDev 创建或更新的全部 Issue 输出必须使用中文,包括标题、正文、验收项、实施方案、任务清单、验证更新、状态说明和 Issue 评论。代码标识、命令、路径、文件名、协议字段和机器状态令牌在翻译会改变行为或降低证据复现性时保持规范形式。 -## Issue 正文 +## 紧凑 Issue 正文 -功能或变更至少保留以下结构: +清晰、低风险、单交付且没有 PRD/UI/迁移复杂度时使用以下五节。未触发的产品追溯、页面、迁移字段不反复填写 N/A;这只简化呈现,不减少任何验收或关闭门。 ```markdown -## 结果 +## 结果与范围 -## 用户问题 +用户问题与可观察结果;范围内:……;范围外:…… -## 产品追溯 +## 方案与确认 -- PRD:`.scd/product/prd.md`,或不适用 -- 已确认版本:<正整数>,或不适用 -- 需求:`FR-001`、……,或不适用 +实施方案:最小技术方向与受影响边界。 +方案确认:需要确认:否;状态:默认免确认。 -## 范围内 +## 验收与任务 -## 范围外 - -## 已确认决策 - -## 方案确认 - -- 需要确认:是,或否 -- 状态:已确认,或默认免确认 - -## 失败与边界场景 - -## 验收条件 - -- [ ] A1:<可观察行为> - -## 实施方案 - -- <具体技术方向和受影响边界> - -## 实施任务 - -- [ ] T1:<小而可验证的任务> +- [ ] A1:可观察行为,包含本次失败/边界场景。 +- [ ] T1:小而可验证的实施任务。 ## 验证 -- A1:尚未执行 +- A1:尚未执行。 ## 完成审计 -### 验收闭合 - -- A1:尚未执行 - -### 实施账目 - -- T1:尚未执行 - -### 页面验收 - -- 页面变更:是,或否 -- 证据矩阵:尚未执行,或不适用 - -### 交付状态 - -- 精确差异:尚未检查 -- 必需检查:尚未完成 -- 独立验收:尚未执行 -- 合并版本:尚未确认 - -### 清理状态 - -- 清理所有者:QuickDev、Execute,或尚未确认 -- main 同步:尚未确认 -- 任务工作树:<精确路径>,状态尚未确认或不适用 -- 本地任务分支:<精确分支>,状态尚未确认 -- 远端任务分支:<精确分支>,状态尚未确认 -- 连续性状态:<精确路径>,状态尚未确认或不适用 - -## 未知项 - -- 无 +- 验收闭合:A1 待验证。 +- 实施账目:T1 待完成。 +- 交付状态:精确差异、检查、独立验收、合并与清理待完成。 ``` -Discovery 负责已确认产品章节。QuickDev 检查仓库后添加或更新实施任务和验证。不得保存隐藏推理、密钥或臆测性未来工作。 - -实施任务使用稳定 `T1`、`T2` 等标识,使完成审计可以逐项记录 `DONE`、`SUPERSEDED` 或 `N/A`。任务勾选表示计划动作已执行;验收行为仍由 `A1`、`A2` 等直接证据决定。 - -适用产品追溯时,QuickDev 必须确认精确的已确认 PRD 版本可从默认分支访问,且每个具名 `FR-*` 标识存在。缺失、陈旧、矛盾或扩大范围的引用返回 Discovery,不能作为 READY 交付实施。 - -缺陷还要记录: - -```markdown -## 已观察症状 - -## 预期行为 - -## 复现步骤 - -## 诊断 +完成审计到交付阶段再逐项补充事实,不在任务开始时复制所有待执行细目。稳定 A/T 标识不可省略:实施任务使用稳定 `T1`、`T2` 等标识,完成时逐项记录 `DONE`、`SUPERSEDED` 或 `N/A` 并说明原因;任务勾选不能替代验收证据。 -- 根因:`Unconfirmed` - -## 回归证据 - -- 尚未执行 -``` +缺陷在“方案与确认”中简洁记录已观察症状、预期行为、复现步骤、根因:`Unconfirmed` 和回归证据。只有代码或运行时证据支持因果路径才改为 `Confirmed`,不必为每一项新增标题。 -只有代码或运行时证据支持因果路径时,才能把 `Unconfirmed` 改为 `Confirmed`。 +只有出现已确认 PRD 引用、重要页面/跨层设计、迁移/兼容性或多项复杂边界时,才读取 [extended-issue.md](extended-issue.md) 并展开对应章节。普通缺陷不需要展开模板。 ## 分支与工作树隔离 @@ -150,65 +80,3 @@ Discovery 负责已确认产品章节。QuickDev 检查仓库后添加或更新 单个干净串行任务在当前工作树创建分支。编辑前验证分支来自预期 `main` 版本。 谁创建任务分支和工作树,谁就是清理所有者。直接 QuickDev 自己创建资源时由 QuickDev 清理;`scd-execute` 创建通道分支和工作树时,父 Execute 是清理所有者。QuickDev 通道必须返回精确分支、工作树和连续性路径,父 Execute 回填清理证据后才能关闭 Delivery Issue。不得出现双方都等待对方清理的无主资源。 - -## 拉取请求与工程门 - -Agent 负责工程验收: - -1. 把每个 Issue 验收项映射到直接证据、`UNVERIFIED` 或阻塞; -2. 评审完整 Issue 专属差异并排除无关文件; -3. 提交并推送 Issue 分支; -4. 创建拉取请求,正文使用 `Refs #`,并写入中文范围摘要、风险和验收证据; -5. 等待必需 CI、测试、构建和仓库检查; -6. 只修复范围内失败并重新运行受影响检查; -7. 完成独立验收前可确定的完成审计字段;独立结论和合并版本保持待完成; -8. 所有必需检查和独立验收通过且没有人工合并门时,合并到 `main`; -9. 合并后写入远端 `main` 提交并最终闭合三本账,才能关闭 Issue。 - -独立验收通过前不得使用 `Closes #`。 - -## 合并后任务资源清理 - -清理只针对当前 Issue 拥有的精确资源,不得扫描或删除历史残留、兄弟通道或其他用户工作。合并后的清理顺序如下: - -1. 无论合并命令退出码为何,都重新读取拉取请求的实时状态、head SHA 和合并提交。合并命令返回非零不代表拉取请求未合并;确认已经合并后不得重复合并。 -2. 同步本地默认分支并读取远端默认分支,确认它包含独立验收过的任务差异。squash 或 rebase merge 不要求任务 head 成为 `main` 祖先;使用已合并拉取请求、合并提交和 `main` 上的任务差异作为证据。 -3. 检查任务工作树的未提交状态。存在修改、暂存或未跟踪内容时不得强制删除;把精确状态记录为 `BLOCKED` 并保持 Issue 开放。 -4. 工作树干净时,先删除精确任务工作树,再修剪失效工作树元数据。不得先强删仍被工作树占用的分支。 -5. 删除本地任务分支。普通删除因 squash/rebase 祖先关系拒绝时,只有在拉取请求已合并、`main` 差异已验证、工作树已删除且分支 tip 与已验收 head 一致后,才可强制删除该精确本地分支。 -6. 显式删除远端任务分支。不得仅依赖 `gh pr merge --delete-branch`,因为远端合并可能成功而本地清理仍失败。 -7. 删除本次任务拥有的 `.scd/tasks/current.md` 或其他明确连续性状态;不存在时记录不适用。 -8. 重新验证:任务工作树不存在,本地任务分支不存在,远端任务分支不存在,本地 `main` 与远端默认分支一致。把命令和结果写入清理状态。 - -任何任务自有资源仍存在、终态未验证或清理失败,都阻止最终交付状态闭合和关闭 Issue。不要用“稍后清理”把它降级为非阻塞剩余工作。 - -## 人工合并门 - -以下变更合并前需要明确人工确认: - -- 身份验证或授权; -- 支付或计费; -- 破坏性数据或模式变更; -- 密钥、隐私、法律或合规边界; -- 生产基础设施或不可逆外部操作; -- 仓库规则要求的人工评审。 - -等待时可以创建拉取请求并展示证据。不得弱化分支保护或确认规则。 - -合并权限不等于部署权限。如果合并 `main` 会自动部署生产环境,把合并视为生产操作并取得所需明确人工确认。可用时优先使用 Preview 或 Staging 收集生产前证据。 - -## 独立验收 - -实施和工程检查后,启动一个独立的新上下文子 Agent 作为验收者。向它提供作为依据的 Issue、仓库位置和精确验收目标:已提交变更提供基准与目标引用,未提交变更提供提交前工作区状态。不要提供实施 Agent 的结论。验收者必须: - -1. 阅读 Issue 验收项和适用仓库说明; -2. 检查真实 Issue 专属差异; -3. 复核完成审计中验收、实施和交付三本账; -4. 运行直接相关检查和真实环境路径,包括变更依赖它们时的浏览器、真实模型或生成产物验证; -5. 真实差异包含页面变更时,必须通过浏览器复核关键旅程和页面验收矩阵,不得用 API 或静态检查代替; -6. 不修改产品代码; -7. 返回 `PASS`、`FAIL` 或 `BLOCKED`,提供可复现证据并映射每个验收项。 - -页面差异存在但浏览器、目标环境、必需状态或视觉证据不可用时只能返回 `BLOCKED`。`PASS` 授权符合条件的合并,以及确认远端 `main` 包含独立验收版本、重新读取 Issue 并确认三本账仍闭合后显式关闭 Issue。`FAIL` 把证据返回实施,保持 Issue 开放,并要求修复后再次独立验收。`BLOCKED` 记录缺失环境或依赖并保持 Issue 开放。合并、部署或环境状态可能改变观察结果时,关闭前在 `main` 上重新运行受影响验收路径。 - -父 Agent 负责交付编排,独立验收者负责验收结论。只有上述高风险边界或仓库强制人工门仍需人工确认。 diff --git a/skills/scd-quickdev/references/page-acceptance.md b/skills/scd-quickdev/references/page-acceptance.md new file mode 100644 index 0000000..6e33902 --- /dev/null +++ b/skills/scd-quickdev/references/page-acceptance.md @@ -0,0 +1,30 @@ +# QuickDev 页面与视觉验收 + +真实差异影响页面或收到 UX 视觉交付时读取;普通后端修复不加载。 + +## 页面验收门 + +实际差异影响面向用户的页面、路由、组件交互、可见样式或响应式行为时,页面验收自动适用,不依赖 UIUX 契约是否存在。必须: + +1. 列出受影响页面或路由,以及共享壳层、导航、模态框或横跨路由的组件; +2. 启动真实应用,通过用户可见控件进入受影响路径并完成关键旅程;不得用 API、数据库写入或内部函数调用替代 UI 操作; +3. 覆盖差异真实触发的加载、空数据、成功、失败、权限、部分成功和恢复状态; +4. 在布局真实变化时覆盖代表性桌面和窄屏视口; +5. 存在 UX 视觉交付时,把实际结果逐项映射到视觉 ID 并记录允许差异;不存在时把视觉 ID 标为 `N/A`,仍需验证 Issue 定义的可见结果; +6. 检查布局、裁切、遮挡、溢出、响应式重排、键盘顺序、焦点与基本无障碍名称; +7. 检查与旅程相关的控制台错误和失败网络请求,并区分当前回归与已知无关基线; +8. 保存可复现的截图、录屏、浏览器追踪或等价证据引用。 + +在 Issue 中记录: + +```markdown +| 页面/路由 | 状态 | 视口 | 旅程/验收 | 视觉 ID | 操作与证据 | 结果 | +|---|---|---|---|---|---|---| +| /example | success | 1440×900 | A2 | UI-001 | <截图或追踪;操作摘要> | PASS | +``` + +只打开应用、只观察首页、只运行组件测试或只生成截图都不能证明交互旅程完成。必需浏览器、运行环境、状态或视觉证据不可用时,记录具体缺口并返回 `BLOCKED`。 + +UX 契约标记需要实现后最终视觉确认时,在独立验收前把设计视觉和实际页面按视觉 ID、状态和视口并排展示给用户。用户确认是该验收项的直接证据;普通页面任务不自动增加人工确认。 + +存在就绪 UX 交接时,同时依据三类权威实施和验证:UX 契约负责行为、状态与响应式规则,视觉 ID 对应的项目内视觉产物负责已标记视口的布局、层级、密度与外观,共享机器契约负责数据、操作、权限和错误语义。实现后把实际页面证据与视觉 ID 逐项比较,并记录允许差异。页面验收门由真实差异触发,不属于可选的“适用时”判断。 diff --git a/skills/scd-quickdev/references/release-contract.md b/skills/scd-quickdev/references/release-contract.md new file mode 100644 index 0000000..edf55f3 --- /dev/null +++ b/skills/scd-quickdev/references/release-contract.md @@ -0,0 +1,77 @@ +# QuickDev 发布与关闭契约 + +工程实现和验证结束,准备创建 PR、独立验收、合并或清理时才读取。创建 Issue 阶段不需要读取本文件。 + +## 审计整个 Issue 是否完成 + +实施和父 Agent 工程检查结束后,重新读取实时 Issue、精确任务差异和交付状态,把完成审计写回同一中文 Issue。先完成独立验收前可确定的字段;独立结论和合并版本明确标记为待完成,不得伪装为已闭合。最终关闭前必须分别闭合三本账: + +1. **验收闭合:** 每个验收标识都有直接观察的 `PASS` 证据;任何 `FAIL`、`BLOCKED`、`UNVERIFIED` 或缺失映射都阻止完成。 +2. **实施账目闭合:** 每个实施任务标记为 `DONE`、`SUPERSEDED` 或 `N/A`;`SUPERSEDED` 和 `N/A` 记录原因。任务状态说明实施路径,不替代行为验收。 +3. **交付状态闭合:** Issue 专属差异没有无关文件,必需检查已完成,没有未解决的阻塞性评审意见或未知项;页面任务还必须包含完整页面验收矩阵和适用的最终视觉确认。 + +任务资源清理属于交付状态。清理所有者必须分别证明 `main` 已同步、任务工作树不存在、本地任务分支不存在、远端任务分支不存在、任务连续性状态已删除或不适用。任何任务自有资源仍存在、状态未验证或脏工作树无法安全删除,都阻止关闭 Issue;不得把清理降级成自然交接中的剩余建议。 + +完成审计必须列出精确 Issue、分支或提交、拉取请求、验收结果、任务状态、页面验收适用性和剩余阻塞。独立验收前它是待复核的候选审计;独立验收写入结论,合并后再写入远端 `main` 提交并完成最终审计。不得因为复选框已勾选、提交存在、拉取请求可合并或测试总数为绿色就推断 Issue 完成。 + +## 拉取请求与工程门 + +Agent 负责工程验收: + +1. 把每个 Issue 验收项映射到直接证据、`UNVERIFIED` 或阻塞; +2. 评审完整 Issue 专属差异并排除无关文件; +3. 提交并推送 Issue 分支; +4. 创建拉取请求,正文使用 `Refs #`,并写入中文范围摘要、风险和验收证据; +5. 等待必需 CI、测试、构建和仓库检查; +6. 只修复范围内失败并重新运行受影响检查; +7. 完成独立验收前可确定的完成审计字段;独立结论和合并版本保持待完成; +8. 所有必需检查和独立验收通过且没有人工合并门时,合并到 `main`; +9. 合并后写入远端 `main` 提交并最终闭合三本账,才能关闭 Issue。 + +独立验收通过前不得使用 `Closes #`。 + +## 合并后任务资源清理 + +清理只针对当前 Issue 拥有的精确资源,不得扫描或删除历史残留、兄弟通道或其他用户工作。合并后的清理顺序如下: + +1. 无论合并命令退出码为何,都重新读取拉取请求的实时状态、head SHA 和合并提交。合并命令返回非零不代表拉取请求未合并;确认已经合并后不得重复合并。 +2. 同步本地默认分支并读取远端默认分支,确认它包含独立验收过的任务差异。squash 或 rebase merge 不要求任务 head 成为 `main` 祖先;使用已合并拉取请求、合并提交和 `main` 上的任务差异作为证据。 +3. 检查任务工作树的未提交状态。存在修改、暂存或未跟踪内容时不得强制删除;把精确状态记录为 `BLOCKED` 并保持 Issue 开放。 +4. 工作树干净时,先删除精确任务工作树,再修剪失效工作树元数据。不得先强删仍被工作树占用的分支。 +5. 删除本地任务分支。普通删除因 squash/rebase 祖先关系拒绝时,只有在拉取请求已合并、`main` 差异已验证、工作树已删除且分支 tip 与已验收 head 一致后,才可强制删除该精确本地分支。 +6. 显式删除远端任务分支。不得仅依赖 `gh pr merge --delete-branch`,因为远端合并可能成功而本地清理仍失败。 +7. 删除本次任务拥有的 `.scd/tasks/current.md` 或其他明确连续性状态;不存在时记录不适用。 +8. 重新验证:任务工作树不存在,本地任务分支不存在,远端任务分支不存在,本地 `main` 与远端默认分支一致。把命令和结果写入清理状态。 + +任何任务自有资源仍存在、终态未验证或清理失败,都阻止最终交付状态闭合和关闭 Issue。不要用“稍后清理”把它降级为非阻塞剩余工作。 + +## 人工合并门 + +以下变更合并前需要明确人工确认: + +- 身份验证或授权; +- 支付或计费; +- 破坏性数据或模式变更; +- 密钥、隐私、法律或合规边界; +- 生产基础设施或不可逆外部操作; +- 仓库规则要求的人工评审。 + +等待时可以创建拉取请求并展示证据。不得弱化分支保护或确认规则。 + +合并权限不等于部署权限。如果合并 `main` 会自动部署生产环境,把合并视为生产操作并取得所需明确人工确认。可用时优先使用 Preview 或 Staging 收集生产前证据。 + +## 独立验收 + +实施和工程检查后,启动一个独立的新上下文子 Agent 作为验收者。向它提供作为依据的 Issue、仓库位置和精确验收目标:已提交变更提供基准与目标引用,未提交变更提供提交前工作区状态。不要提供实施 Agent 的结论。验收者必须: + +1. 阅读 Issue 验收项和适用仓库说明; +2. 检查真实 Issue 专属差异; +3. 复核完成审计中验收、实施和交付三本账; +4. 运行直接相关检查和真实环境路径,包括变更依赖它们时的浏览器、真实模型或生成产物验证; +5. 真实差异包含页面变更时,必须通过浏览器复核关键旅程和页面验收矩阵,不得用 API 或静态检查代替; +6. 不修改产品代码; +7. 返回 `PASS`、`FAIL` 或 `BLOCKED`,提供可复现证据并映射每个验收项。 + +页面差异存在但浏览器、目标环境、必需状态或视觉证据不可用时只能返回 `BLOCKED`。`PASS` 授权符合条件的合并,以及确认远端 `main` 包含独立验收版本、重新读取 Issue 并确认三本账仍闭合后显式关闭 Issue。`FAIL` 把证据返回实施,保持 Issue 开放,并要求修复后再次独立验收。`BLOCKED` 记录缺失环境或依赖并保持 Issue 开放。合并、部署或环境状态可能改变观察结果时,关闭前在 `main` 上重新运行受影响验收路径。 + +父 Agent 负责交付编排,独立验收者负责验收结论。只有上述高风险边界或仓库强制人工门仍需人工确认。 diff --git a/skills/scd-quickdev/references/scope-contract.md b/skills/scd-quickdev/references/scope-contract.md index b9af346..dcd9837 100644 --- a/skills/scd-quickdev/references/scope-contract.md +++ b/skills/scd-quickdev/references/scope-contract.md @@ -80,3 +80,13 @@ - 更新方案,不要机械执行原方案; - 只有新路径改变产品结果或权限边界时才更新 Issue 并请求决策; - 无关缺陷和清理作为观察报告,不得悄悄加入范围。 + +## 就绪设计与项目输入 + +请求来自 UIUX 时,阅读相关 `.scd/ux/.md`,要求 `status: ready`;解析视觉交付清单,检查每个视觉 ID 的项目文件、视口、状态及确认,练习适用的非生产原型,并使用共享接口契约;不得把 UX 接口需求当成 API。 + +请求来自 Architecture 时,阅读相关 `.scd/architecture.md` 或 `.scd/designs/.md`,要求 `status: ready`,并与生产者一样用格式感知证据解析规范机器可读契约。UX 体验交接不替代产品确认或前端架构;它与 Issue、视觉产物或尚未统一的共享接口决策冲突时,在实施前返回缺口。 + +收到 Project/Execute 交接时,读取实时 Initiative 和选中 Delivery Issue,确认当前图版本与 READY 依赖。Reengineering 波次通过额外的故障关闭门后,经 Execute 进入 QuickDev。拒绝 Initiative、PLANNED 占位节点、BLOCKED 节点、陈旧图版本或吸收兄弟 Issues 的请求;创建分支前返回精确缺口。 + +已确认的全新产品 PRD 继续负责产品级原因、用户、问题、MVP 范围、`FR-*` 需求和成功指标。Issue 引用 PRD 时,从默认分支读取精确已确认版本,确认每个具名 `FR-*` 标识存在;不接受缺失、草稿、未提交、已被取代或矛盾引用。 diff --git a/tests/architecture-contract.test.mjs b/tests/architecture-contract.test.mjs index 6b62473..41861a1 100644 --- a/tests/architecture-contract.test.mjs +++ b/tests/architecture-contract.test.mjs @@ -39,7 +39,8 @@ test("architecture and uiux reconcile one shared machine contract", () => { const skill = read("skills/scd-architecture/SKILL.md"); const discovery = read("skills/scd-discovery/SKILL.md"); const uiux = read("skills/scd-uiux/SKILL.md"); - const quickdev = read("skills/scd-quickdev/SKILL.md"); + const quickdev = read("skills/scd-quickdev/SKILL.md") + "\n" + + read("skills/scd-quickdev/references/scope-contract.md"); const contract = read( "skills/scd-architecture/references/interface-contract.md", ); diff --git a/tests/chinese-skill-payload.test.mjs b/tests/chinese-skill-payload.test.mjs index 54cacf2..1d82b71 100644 --- a/tests/chinese-skill-payload.test.mjs +++ b/tests/chinese-skill-payload.test.mjs @@ -32,7 +32,7 @@ test("全部 Thinloop Skill 文本载荷均包含中文", () => { .flatMap((entry) => collectPayloadFiles(path.join(skillsRoot, entry.name))) .sort(); - assert.equal(files.length, 60); + assert.equal(files.length, 63); // Three new conditional QuickDev references. for (const file of files) { assert.match(read(file), han, file); } diff --git a/tests/compact-eval.test.mjs b/tests/compact-eval.test.mjs new file mode 100644 index 0000000..c260c1f --- /dev/null +++ b/tests/compact-eval.test.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { cases, definition, eventEvidence, payloadIdentity, prompt, schedule, snapshot } from "../evals/compact/run.mjs"; + +test("compact comparison freezes three paired tasks, two repeats and interleaved order", () => { + const pairs = schedule(); + assert.equal(pairs.length, 6); + assert.equal(pairs.flatMap(pair => pair.order).length, 12); + for (const testCase of cases) assert.equal(pairs.filter(pair => pair.caseId === testCase.id).length, 2); + pairs.forEach((pair, index) => assert.deepEqual(pair.order, index % 2 ? ["candidate", "baseline"] : ["baseline", "candidate"])); + assert.equal(definition.concurrency, 2); + assert.equal(definition.model, "gpt-6-astra"); + assert.equal(definition.cliVersion, "codex-cli 0.153.0"); + for (const testCase of cases) { + assert.match(prompt(testCase), /显式使用 \$scd-quickdev/); + assert.match(prompt(testCase), /不创建分支或工作树,不提交或合并/); + assert.doesNotMatch(prompt(testCase), /baseline|candidate|评分|hiddenCheck/); + } +}); + +test("payload identity covers all frozen files and changes with bytes or names", () => { + const baseline = snapshot(definition.baselineQuickdevRef, "skills/scd-quickdev/"); + assert.ok(baseline["skills/scd-quickdev/SKILL.md"]); + assert.ok(baseline["skills/scd-quickdev/references/issue-delivery-contract.md"]); + const before = payloadIdentity(baseline); + assert.notEqual(payloadIdentity({ ...baseline, "skills/scd-quickdev/SKILL.md": "changed" }).sha256, before.sha256); + assert.notEqual(payloadIdentity({ ...baseline, "skills/scd-quickdev/new.md": "new" }).sha256, before.sha256); + assert.throws(() => snapshot("HEAD"), /full commit SHA/); +}); + +test("loading proof requires a successful full read and usage keeps absent counters unknown", () => { + const files = { "skills/scd-quickdev/SKILL.md": "# entry\ncomplete content\n" }; + const command = { type: "item.completed", item: { type: "command_execution", id: "read", command: "cat $CODEX_HOME/skills/scd-quickdev/SKILL.md", exit_code: 0, aggregated_output: files[Object.keys(files)[0]] } }; + const evidence = eventEvidence([command, { type: "turn.completed", usage: { input_tokens: 10, output_tokens: 2 } }], files); + assert.equal(evidence.entryFullyObserved, true); + assert.equal(evidence.knownCompletedCommands, 1); + assert.deepEqual(evidence.usage, { inputTokens: 10, outputTokens: 2, cachedInputTokens: null, cacheWriteInputTokens: null, reasoningOutputTokens: null }); + assert.equal(eventEvidence([{ ...command, item: { ...command.item, aggregated_output: "# entry\n[truncated]" } }], files).entryFullyObserved, false); + assert.equal(eventEvidence([{ ...command, item: { ...command.item, exit_code: 1 } }], files).entryFullyObserved, false); + const unknown = eventEvidence([{ type: "item.completed", item: { type: "new_tool_type" } }], files); + assert.equal(unknown.toolCountCoverage, "partial"); + assert.equal(unknown.usage.inputTokens, null); +}); + +test("paired summary preserves unknown usage rather than summing incomplete samples", async () => { + const { summarize } = await import("../evals/compact/report.mjs"); + const identity = { bytes: 20, chars: 10, inventory: [{ file: "skills/scd-quickdev/SKILL.md", bytes: 20, chars: 10 }] }; + const row = (condition, inputTokens) => ({ condition, durationMs: 5, + evidence: { usage: { inputTokens, outputTokens: 2 }, knownCompletedCommands: 1, knownCompletedToolItems: 1, + toolCountCoverage: "partial", entryFullyObserved: true, + reads: [{ file: "skills/scd-quickdev/SKILL.md", fullContentObserved: true }] }, + scored: { verdict: "PASS", metrics: { userInterruptRequests: null }, facts: { completionDeclaration: { state: "unknown" } } }, + }); + const summary = summarize([row("baseline", 100), row("baseline", 150), row("candidate", 50), row("candidate", null)], { baseline: identity, candidate: identity }); + assert.deepEqual(summary.baseline.tokens.inputTokens, { total: 250, known: 2 }); + assert.deepEqual(summary.candidate.tokens.inputTokens, { total: null, known: 1 }); + assert.equal(summary.candidate.tokens.cachedInputTokens.total, null); + assert.equal(summary.candidate.costUsd, null); + assert.equal(summary.candidate.userInputUnknownSamples, 2); + assert.equal(summary.candidate.completionDeclarationUnknownSamples, 2); + assert.equal(summary.candidate.toolCoveragePartialSamples, 2); + assert.equal(summary.candidate.entryFullReadSamples, 2); +}); diff --git a/tests/discovery-contract.test.mjs b/tests/discovery-contract.test.mjs index 493b652..d6a937e 100644 --- a/tests/discovery-contract.test.mjs +++ b/tests/discovery-contract.test.mjs @@ -76,7 +76,8 @@ test("discovery persists approved greenfield PRD without burdening clear changes }); test("downstream skills consume PRD authority and delivery evidence", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); + const skill = read("skills/scd-quickdev/SKILL.md") + "\n" + + read("skills/scd-quickdev/references/scope-contract.md"); const uiux = read("skills/scd-uiux/SKILL.md"); const architecture = read("skills/scd-architecture/SKILL.md"); const evidence = read( diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs index 30a3025..e12c63f 100644 --- a/tests/project-contract.test.mjs +++ b/tests/project-contract.test.mjs @@ -97,7 +97,8 @@ test("project uses rolling approval and deterministic graph validation", () => { test("discovery and quickdev route project work without widening implementation authority", () => { const discovery = read("skills/scd-discovery/SKILL.md"); const execute = read("skills/scd-execute/SKILL.md"); - const quickdev = read("skills/scd-quickdev/SKILL.md"); + const quickdev = read("skills/scd-quickdev/SKILL.md") + "\n" + + read("skills/scd-quickdev/references/scope-contract.md"); const workflow = read("docs/workflow-and-state.md"); assert.match(discovery, /交给 `scd-project` 拆解/); diff --git a/tests/quickdev-contract.test.mjs b/tests/quickdev-contract.test.mjs index e8b68d5..8bce159 100644 --- a/tests/quickdev-contract.test.mjs +++ b/tests/quickdev-contract.test.mjs @@ -10,8 +10,29 @@ function read(relativePath) { return fs.readFileSync(path.join(root, relativePath), "utf8"); } +// Rules may live in their stage-specific references. Verify the composed +// guidance without requiring every gate to be duplicated in the entry point. +function composeGuidance(relativePath) { + const files = [relativePath]; + if (relativePath.endsWith("SKILL.md")) files.push( + "skills/scd-quickdev/references/scope-contract.md", + "skills/scd-quickdev/references/issue-delivery-contract.md", + "skills/scd-quickdev/references/release-contract.md", + "skills/scd-quickdev/references/page-acceptance.md", + ); + if (relativePath.endsWith("issue-delivery-contract.md")) files.push( + "skills/scd-quickdev/references/extended-issue.md", + "skills/scd-quickdev/references/release-contract.md", + ); + if (relativePath.endsWith("evidence-contract.md")) files.push( + "skills/scd-quickdev/references/page-acceptance.md", + "skills/scd-quickdev/references/release-contract.md", + ); + return files.map(read).join("\n"); +} + test("quickdev selects direct, clarify, project, or discovery without forcing ceremony", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); assert.match(skill, /\*\*直接实施:\*\*/); assert.match(skill, /\*\*单点澄清:\*\*/); @@ -23,8 +44,8 @@ test("quickdev selects direct, clarify, project, or discovery without forcing ce }); test("quickdev separates product PRD authority from delivery Issue authority", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const issueContract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); @@ -49,8 +70,8 @@ test("quickdev separates product PRD authority from delivery Issue authority", ( }); test("quickdev defaults to autonomous delivery and only pauses when the user asks to confirm", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const issueContract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); const combined = `${skill}\n${issueContract}`; @@ -87,8 +108,8 @@ test("quickdev defaults to autonomous delivery and only pauses when the user ask }); test("quickdev writes Issue output in Chinese without translating machine identifiers", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const issueContract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); const combined = `${skill}\n${issueContract}`; @@ -108,8 +129,8 @@ test("quickdev writes Issue output in Chinese without translating machine identi }); test("quickdev diagnoses bugs and requires regression evidence", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const issueContract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); @@ -127,7 +148,7 @@ test("quickdev diagnoses bugs and requires regression evidence", () => { }); test("quickdev always isolates meaningful work on a branch and uses worktrees conditionally", () => { - const contract = read( + const contract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); @@ -140,11 +161,11 @@ test("quickdev always isolates meaningful work on a branch and uses worktrees co }); test("quickdev delegates acceptance to one independent verifier", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const contract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const contract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); - const evidence = read( + const evidence = composeGuidance( "skills/scd-quickdev/references/evidence-contract.md", ); const guidance = [ @@ -184,7 +205,7 @@ test("quickdev delegates acceptance to one independent verifier", () => { }); test("quickdev keeps high-risk merge and production deployment behind human approval", () => { - const contract = read( + const contract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); @@ -197,7 +218,7 @@ test("quickdev keeps high-risk merge and production deployment behind human appr }); test("quickdev respects a composing skill's narrower delivery authority", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); assert.match(skill, /以其更窄交付边界为准/); assert.match( @@ -207,8 +228,8 @@ test("quickdev respects a composing skill's narrower delivery authority", () => }); test("quickdev verifies frontend implementation against UX, visual, and interface contracts", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const evidence = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const evidence = composeGuidance( "skills/scd-quickdev/references/evidence-contract.md", ); const combined = `${skill}\n${evidence}`; @@ -223,11 +244,11 @@ test("quickdev verifies frontend implementation against UX, visual, and interfac }); test("quickdev audits acceptance, implementation, and delivery before declaring the Issue complete", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const evidence = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const evidence = composeGuidance( "skills/scd-quickdev/references/evidence-contract.md", ); - const issueContract = read( + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); const combined = `${skill}\n${evidence}\n${issueContract}`; @@ -250,11 +271,11 @@ test("quickdev audits acceptance, implementation, and delivery before declaring }); test("quickdev makes browser acceptance mandatory for every real page change", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const evidence = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const evidence = composeGuidance( "skills/scd-quickdev/references/evidence-contract.md", ); - const issueContract = read( + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); const combined = `${skill}\n${evidence}\n${issueContract}`; @@ -273,8 +294,8 @@ test("quickdev makes browser acceptance mandatory for every real page change", ( }); test("quickdev confirms the accepted merge on main before closing the Issue", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const issueContract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); const combined = `${skill}\n${issueContract}`; @@ -286,8 +307,8 @@ test("quickdev confirms the accepted merge on main before closing the Issue", () }); test("quickdev makes task-owned branch and worktree cleanup a closing gate", () => { - const skill = read("skills/scd-quickdev/SKILL.md"); - const issueContract = read( + const skill = composeGuidance("skills/scd-quickdev/SKILL.md"); + const issueContract = composeGuidance( "skills/scd-quickdev/references/issue-delivery-contract.md", ); const combined = `${skill}\n${issueContract}`; @@ -311,3 +332,25 @@ test("quickdev makes task-owned branch and worktree cleanup a closing gate", () assert.match(combined, /工作树.*不存在[\s\S]*本地任务分支.*不存在[\s\S]*远端任务分支.*不存在/); assert.match(combined, /不得扫描或删除.*历史|不得清理.*无关/); }); + + +test("quickdev loads short phase references conditionally and offers a compact low-risk template", () => { + const entry = read("skills/scd-quickdev/SKILL.md"); + const issue = read("skills/scd-quickdev/references/issue-delivery-contract.md"); + const release = read("skills/scd-quickdev/references/release-contract.md"); + assert.match(entry, /只读取当前条件命中的参考/); + assert.match(entry, /创建或更新 Issue、分支\/工作树隔离.*issue-delivery-contract/); + assert.match(entry, /工程完成,准备 PR\/独立验收\/合并\/清理关闭.*release-contract/); + assert.match(entry, /真实页面差异或 UX 视觉交付.*page-acceptance/); + assert.match(entry, /PRD、重要页面\/跨层设计、迁移\/兼容性复杂度.*extended-issue/); + assert.match(issue, /清晰、低风险、单交付/); + const compact = issue.match(/```markdown\n([\s\S]*?)```/)[1]; + assert.equal((compact.match(/^## /gm) || []).length, 5); + for (const marker of ["A1", "T1", "验收闭合", "实施账目", "交付状态", "范围内", "范围外", "状态:默认免确认"]) assert.ok(compact.includes(marker), marker); + assert.doesNotMatch(compact, /N\/A|不适用|产品追溯|页面验收/); + assert.match(release, /创建 Issue 阶段不需要读取/); + assert.ok(Buffer.byteLength(entry) < 8000, "entry remains a concise router plus essential gates"); + for (const file of ["scope-contract.md", "extended-issue.md", "evidence-contract.md", "page-acceptance.md", "release-contract.md", "continuity-contract.md"]) { + assert.ok(entry.includes(`references/${file}`), `${file} must remain reachable with its trigger`); + } +}); diff --git a/tests/readme-diagrams.test.mjs b/tests/readme-diagrams.test.mjs index c968359..bea5582 100644 --- a/tests/readme-diagrams.test.mjs +++ b/tests/readme-diagrams.test.mjs @@ -102,10 +102,10 @@ test("diagram stage summaries remain traceable to authoritative skill workflows" /证明项目完成/, ], quickdev: [ - /从仓库事实出发/, - /实施最小且连贯的变更/, - /验证工程验收/, - /审计整个 Issue 是否完成/, + /确定边界/, + /最小修复/, + /运行直接练习变更[\s\S]*构建[\s\S]*测试/, + /审计整个 Issue/, /独立的新上下文子 Agent/, ], knowledge: [ diff --git a/tests/reengineering-contract.test.mjs b/tests/reengineering-contract.test.mjs index 96f3761..a943c2a 100644 --- a/tests/reengineering-contract.test.mjs +++ b/tests/reengineering-contract.test.mjs @@ -54,7 +54,8 @@ test("reengineering composes existing Thinloop authority", () => { const skill = read("skills/scd-reengineering/SKILL.md"); const project = read("skills/scd-project/SKILL.md"); const execute = read("skills/scd-execute/SKILL.md"); - const quickdev = read("skills/scd-quickdev/SKILL.md"); + const quickdev = read("skills/scd-quickdev/SKILL.md") + "\n" + + read("skills/scd-quickdev/references/scope-contract.md"); assert.match(skill, /再工程前使用 `scd-discovery`/); assert.match(skill, /使用 `scd-architecture`/); diff --git a/tests/uiux-contract.test.mjs b/tests/uiux-contract.test.mjs index 8aa968e..6c3e101 100644 --- a/tests/uiux-contract.test.mjs +++ b/tests/uiux-contract.test.mjs @@ -145,7 +145,8 @@ test("uiux distinguishes design approval from post-implementation visual confirm }); test("quickdev consumes a ready UX handoff without confusing it for architecture", () => { - const quickdev = read("skills/scd-quickdev/SKILL.md"); + const quickdev = read("skills/scd-quickdev/SKILL.md") + "\n" + + read("skills/scd-quickdev/references/scope-contract.md"); assert.match(quickdev, /\.scd\/ux\/\.md/); assert.match(quickdev, /要求 `status: ready`/);