diff --git a/.codebuddy-plugin/marketplace.json b/.codebuddy-plugin/marketplace.json new file mode 100644 index 0000000..3081f54 --- /dev/null +++ b/.codebuddy-plugin/marketplace.json @@ -0,0 +1,37 @@ +{ + "name": "better-harness", + "description": "Better Harness Review Team / Better Harness 评审专家团", + "description_en": "Better Harness Review Team for privacy-safe, evidence-grounded AI coding-agent delivery reviews.", + "owner": { + "name": "Qoder", + "email": "dev@qoder.com" + }, + "metadata": { + "version": "0.4.0" + }, + "plugins": [ + { + "name": "better-harness", + "description": "A four-role evidence review team for AI coding-agent delivery readiness.", + "source": "./", + "version": "0.4.0", + "categoryId": "10-ProjectQuality", + "expertType": "team", + "agents": [ + "./agents/better-harness-review-director.md", + "./agents/session-evidence-reviewer.md", + "./agents/project-harness-reviewer.md", + "./agents/agent-customize-reviewer.md" + ], + "skills": ["./skills/better-harness"], + "teamInfo": { + "leadAgent": "better-harness-review-director", + "memberAgents": [ + "session-evidence-reviewer", + "project-harness-reviewer", + "agent-customize-reviewer" + ] + } + } + ] +} diff --git a/.codebuddy-plugin/plugin.json b/.codebuddy-plugin/plugin.json new file mode 100644 index 0000000..a9a6678 --- /dev/null +++ b/.codebuddy-plugin/plugin.json @@ -0,0 +1,103 @@ +{ + "name": "better-harness", + "version": "0.4.0", + "description": "A read-only, evidence-grounded review team for AI coding-agent delivery readiness.", + "author": { + "name": "Qoder", + "email": "dev@qoder.com" + }, + "homepage": "https://github.com/QoderAI/better-harness", + "repository": "https://github.com/QoderAI/better-harness", + "license": "MIT", + "keywords": [ + "better-harness", + "agent-work-loop", + "evidence-review", + "harness-governance" + ], + "expertType": "team", + "agentName": "better-harness-review-director", + "agents": [ + "./agents/better-harness-review-director.md", + "./agents/session-evidence-reviewer.md", + "./agents/project-harness-reviewer.md", + "./agents/agent-customize-reviewer.md" + ], + "skills": ["./skills/better-harness"], + "teamInfo": { + "leadAgent": "better-harness-review-director", + "memberAgents": [ + "session-evidence-reviewer", + "project-harness-reviewer", + "agent-customize-reviewer" + ] + }, + "displayName": { + "en": "Better Harness Review Team", + "zh": "Better Harness 评审专家团" + }, + "profession": { + "en": "Evidence-led coding-agent delivery review", + "zh": "证据驱动的智能体交付评审" + }, + "displayDescription": { + "en": "A four-role review team that freezes one privacy-safe evidence bundle, independently reviews session, project, and agent lanes, then validates a single reconciled Markdown and HTML report.", + "zh": "四角色专家团一次冻结隐私安全证据包,独立评审会话、项目与智能体三路护栏,汇总并验证唯一 Markdown 与 HTML 报告。" + }, + "avatar": "avatars/team.svg", + "categoryId": "10-ProjectQuality", + "defaultInitPrompt": { + "en": "Review this workspace's AI coding-agent delivery readiness with Better Harness.", + "zh": "请用 Better Harness 评审这个工作区的 AI 编码智能体交付准备度。" + }, + "plugin": "better-harness", + "tags": [ + { "en": "Agent Work Loop", "zh": "Agent Work Loop" }, + { "en": "Evidence Review", "zh": "证据评审" }, + { "en": "Harness Governance", "zh": "Harness 治理" } + ], + "quickPrompts": [ + { + "en": "Review this workspace's AI coding-agent delivery readiness with Better Harness.", + "zh": "请用 Better Harness 评审这个工作区的 AI 编码智能体交付准备度。" + }, + { + "en": "Find the highest-confidence evidence gaps in this coding-agent workflow.", + "zh": "找出这个编码智能体工作流中证据最充分的能力缺口。" + }, + { + "en": "Validate the latest Better Harness findings and report artifacts.", + "zh": "验证最近一次 Better Harness 发现与报告产物。" + } + ], + "members": [ + { + "id": "better-harness-review-director", + "name": { "en": "Heng", "zh": "衡审" }, + "profession": { "en": "Harness Review Director", "zh": "评审总监" }, + "avatar": "avatars/better-harness-review-director.svg", + "role": "lead" + }, + { + "id": "session-evidence-reviewer", + "name": { "en": "Zheng", "zh": "证安" }, + "profession": { "en": "Session Evidence Analyst", "zh": "会话证据分析师" }, + "avatar": "avatars/session-evidence-reviewer.svg", + "role": "member" + }, + { + "id": "project-harness-reviewer", + "name": { "en": "Hu", "zh": "护程" }, + "profession": { "en": "Project Guardrail Analyst", "zh": "项目护栏分析师" }, + "avatar": "avatars/project-harness-reviewer.svg", + "role": "member" + }, + { + "id": "agent-customize-reviewer", + "name": { "en": "Zhi", "zh": "智配" }, + "profession": { "en": "Agent Asset Analyst", "zh": "智能体资产分析师" }, + "avatar": "avatars/agent-customize-reviewer.svg", + "role": "member" + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f9882e..86d3869 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ compatibility boundaries, or more than one canonical owner. ## Development Setup -Better Harness 0.4.0 supports Node.js `>=22.20.0 <25.0.0` and npm +Better Harness 0.4.0 supports Node.js `>=22.20.0 <26.0.0` and npm `>=10.9.3 <12.0.0`. The supported project targets are Windows, macOS, and Linux. ```bash diff --git a/README.md b/README.md index c5434d1..9892bc8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ Choose the host you already use to get its exact installation, verification, invocation, and report-output steps. Better Harness does not use one universal entrypoint across every host. +Pi and WorkBuddy also have native local plugin entrypoints documented below. +They remain outside the verified Quickstart list until a complete interactive +three-lane report smoke is observed on each host. + The canonical registry covers eight host adapters. Pi and WorkBuddy currently remain adapter-support entries rather than part of the six-host verified Quickstart; see the [public Host Adapter Matrix](docs/docs/hosts/adapter-matrix.md) @@ -355,9 +359,11 @@ Or try it for a single run without changing settings: pi -e git:github.com/QoderAI/better-harness ``` -Pi discovers the `better-harness` Skill and the `/better-harness` prompt -template through the `pi` manifest in `package.json`. Start a new Pi session -in the repository you want to analyze and run the report prompt: +Pi discovers the canonical `better-harness` Skill and native extension through +the `pi` manifest in `package.json`. The extension registers exactly one +`/better-harness` command, starts three isolated RPC lanes, and passes only +verified results to the current lead turn. Start a new Pi session in the +repository you want to analyze and run: ```text /better-harness analyze this project's AI coding workflow and generate an evidence-backed report @@ -369,9 +375,33 @@ session evidence is read from workspace-matching JSONL transcripts under `~/.pi/agent/sessions/`; missing evidence stays explicit rather than being inferred. +### WorkBuddy + +Install the repository as a WorkBuddy Team expert plugin from the checkout: + +```bash +codebuddy --plugin-dir /path/to/better-harness +``` + +The plugin card is **Better Harness Review Team / Better Harness 评审专家团**. +It discovers the canonical Skill, one review director, and three fixed members +for Session Evidence, Project Harness, and Agent Customize. The director runs +one `prepare-run`, one `TeamCreate`, three independent member returns, and one +verification/reconciliation pass before rendering `findings.json`, `report.md`, +and `report.html` under `.workbuddy/better-harness`. + +For an offline manifest/archive check from the source checkout, run: + +```bash +npm run workbuddy:verify +``` + +An authorized interactive WorkBuddy model smoke is a separate host-verification +gate; absence of the local CLI or model keeps that status explicit. + ## Develop and package from source -Development requires Node.js `>=22.20.0 <25.0.0` and npm +Development requires Node.js `>=22.20.0 <26.0.0` and npm `>=10.9.3 <12.0.0` on Windows, macOS, or Linux. ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 599ff2e..4e30652 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -33,6 +33,9 @@ 选择你正在使用的宿主,查看对应的安装、验证、调用和报告输出说明。 不同宿主的入口并不完全相同,请直接使用对应章节给出的命令。 +Pi 和 WorkBuddy 也提供原生本地插件入口,详见下方安装说明。在两个宿主均完成 +完整的交互式三路报告 smoke 之前,它们仍不进入“已验证快速开始”列表。 + 规范注册表当前包含八个宿主适配器。Pi 与 WorkBuddy 仍属于适配器支持入口, 没有进入包含六个宿主的已验证快速开始;具体边界见 [公开宿主适配矩阵](docs/docs/hosts/adapter-matrix.md)。 @@ -336,8 +339,9 @@ pi install https://github.com/QoderAI/better-harness pi -e git:github.com/QoderAI/better-harness ``` -Pi 通过 `package.json` 中的 `pi` manifest 发现 `better-harness` Skill 和 -`/better-harness` 提示模板。在需要分析的仓库中启动新的 Pi 会话,运行报告提示词: +Pi 通过 `package.json` 中的 `pi` manifest 发现规范 `better-harness` Skill 和原生扩展。 +扩展注册唯一的 `/better-harness` 命令,启动三路隔离 RPC,并将验证后的结果交给当前主理人回合。 +在需要分析的仓库中启动新的 Pi 会话,运行: ```text /better-harness 分析此项目的 AI 编码工作流并生成基于证据的报告 @@ -347,11 +351,34 @@ Pi 默认在仓库的 `.pi/better-harness` 报告根目录下生成自包含的 及配套的 `report.md` 和 `findings.json`。Pi 会话证据读自 `~/.pi/agent/sessions/` 下与工作区匹配的 JSONL 会话记录;缺失的证据会被明确标注而不会被推断。 +### WorkBuddy + +从源码检出目录以 WorkBuddy Team 专家插件启动: + +```bash +codebuddy --plugin-dir /path/to/better-harness +``` + +插件卡片名称为 **Better Harness Review Team / Better Harness 评审专家团**。 +它发现规范 Skill、一个评审总监以及 Session Evidence、Project Harness、Agent Customize +三个固定成员。总监只收集一次 `prepare-run`,只调用一次 `TeamCreate`,接收三次独立回传, +完成一次验证/汇总后,才将 `findings.json`、`report.md`、`report.html` 写入 +`.workbuddy/better-harness`。 + +在源码检出目录执行离线 manifest/归档检查: + +```bash +npm run workbuddy:verify +``` + +经授权的真实 WorkBuddy 模型 smoke 属于独立的宿主验证门禁;当前 CLI 或模型不可用时, +必须保留这一未验证状态。 + ## 从源码开发和打包 -开发环境需要 Node.js `>=22.20.0 <25.0.0` 和 npm +开发环境需要 Node.js `>=22.20.0 <26.0.0` 和 npm `>=10.9.3 <12.0.0`,支持 Windows、macOS 和 Linux。 ```bash diff --git a/agents/agent-customize-reviewer.md b/agents/agent-customize-reviewer.md new file mode 100644 index 0000000..6ff8fb3 --- /dev/null +++ b/agents/agent-customize-reviewer.md @@ -0,0 +1,44 @@ +--- +name: agent-customize-reviewer +description: Reviews only the Better Harness Agent Customize lane for Skills, agents, hooks, MCP, plugins, settings, and memory governance evidence. +displayName: + en: "Agent Asset Analyst" + zh: "智能体资产分析师" +profession: + en: "Agent Asset Analyst" + zh: "智能体资产分析师" +maxTurns: 40 +skills: + - better-harness +--- + +# 智能体资产分析师 - 智配 + +你是正式团队成员,只分析 `agentCustomize` lane,覆盖 Skills、agents、hooks、MCP、plugins、settings 和 Memory 的治理证据。你不能创建团队、委派 Agent、读取另两条 lane 或复制私有配置。 + +## 分析框架 + +1. 仅使用主理人传入的 envelope 和 input hash;不读取未授权的用户主目录、不打开凭证、不读取 Memory 正文。 +2. 识别 asset 的 `configured`、`enabled`、`observed`、`verified`、`unsupported`、`unavailable` 状态,区分“目录存在”和“真实运行使用”。 +3. 检查命名空间冲突、重复技能、孤立 hooks、缺少 owner、无验证命令和跨宿主路径漂移;未知 schema 必须产生 `partial`。 +4. 返回 bounded findings 和可执行的一个下一步,保持只读,不替主理人渲染报告。 + +## 结构化回传 + +完成后必须用 `SendMessage` 向主理人回传: + +```json +{ + "lane": "agentCustomize", + "contextId": "", + "status": "completed|partial|unavailable", + "inputHash": "", + "output": { + "coverage": [{"assetType": "skill|agent|hook|mcp|plugin|setting|memory", "state": "..."}], + "findings": [{"severity": "high|medium|low", "title": "...", "evidence": "bounded", "nextStep": "..."}], + "confidence": "normal|low" + } +} +``` + +严禁把原始路径、session ID、prompt、token、cookie 或完整资产正文放进回传;主理人完成 verify-run 后才允许进入最终报告。 diff --git a/agents/better-harness-review-director.md b/agents/better-harness-review-director.md new file mode 100644 index 0000000..9da51b8 --- /dev/null +++ b/agents/better-harness-review-director.md @@ -0,0 +1,50 @@ +--- +name: better-harness-review-director +description: Coordinates a privacy-safe Better Harness review, dispatches exactly three independent evidence lanes, verifies their structured returns, and renders one reconciled report. +displayName: + en: "Review Director" + zh: "评审总监" +profession: + en: "Harness Review Director" + zh: "评审总监" +maxTurns: 120 +skills: + - better-harness +--- + +# Better Harness 评审专家团 - 评审总监 + +你是 Better Harness 的主理人,负责把一次工作区评审编排成可审计的三路只读证据运行。你只负责收集一次 bundle、创建团队、汇编结构化回传、做一次 reconciliation 和调用现有 renderer;不要代替成员完成其 lane 的判断。 + +## 固定成员与路由 + +| Agent ID | 角色 | 只负责的 lane | +|---|---|---| +| `session-evidence-reviewer` | 会话证据分析师 | `sessionEvidence` | +| `project-harness-reviewer` | 项目护栏分析师 | `projectHarness` | +| `agent-customize-reviewer` | 智能体资产分析师 | `agentCustomize` | + +## 标准 SOP + +1. 确认用户语言、`quick`/`normal` 深度和当前工作区。优先使用 `CODEBUDDY_SESSION_ID` 绑定当前会话;兼容 `WORKBUDDY_SESSION_ID`,两者同时存在且不一致时立即阻断。 +2. 用 `node scripts/better-harness.mjs harness host-doctor --platform workbuddy --workspace --json` 做 doctor。缺 Node、资源、模型/身份、输出权限或 provider coverage 时必须显式报告。 +3. 只调用一次 `node scripts/better-harness.mjs harness prepare-run --platform workbuddy --workspace --depth --output --json`。run plan 只放在操作系统临时目录;禁止读取 `workbuddy.db`,禁止把私有 session id、home 路径或原始 prompt 传给成员。 +4. **必须且只能由你调用一次 `TeamCreate`**,并在同一并行阶段正式调度上述三个 Agent ID。禁止成员再次委派或创建团队。 +5. 三个成员分别只接收自己的 lane envelope 和 input hash;不得把一个成员的输入、原始 transcript 或另一 lane 的诊断传给其他成员。成员必须使用 `SendMessage` 回传一个结构化 JSON。 +6. 收齐三次独立回传后,运行 `verify-run`,确认恰好三个不同 Agent ID、三个不同 input hash、三个有效结果。normal 模式任何 `partial/unavailable` 都阻断;quick 模式保留缺口并降低 confidence。 +7. 只做一次 reconciliation:保留 `configured`、`enabled`、`observed`、`verified`、`unsupported`、`unavailable` 等 Evidence Boundary 状态,不把未知 schema 当作零能力。随后调用现有 `better-harness harness render` 和 `report-quality`,将最终产物写入 `.workbuddy/better-harness/findings.json`、`report.md`、`report.html`。 +8. 成功、取消、超时或验证失败后删除私有临时 run 文件。最终消息只报告 bounded counts、lane status、confidence、renderer status 和产物相对路径。 + +## Team 协作铁律 + +- 团队开始必须由主理人亲自执行且只执行一次 `TeamCreate`。 +- 只能调度固定的三个成员;成员不得再次委派,不得互相直连。 +- 专业结论必须来自对应成员的 `SendMessage`;你只做编排、冲突标记、汇总和渲染。 +- 未知 WorkBuddy record shape、cwd-less slug 冲突、当前会话身份冲突均按 `partial` 或阻断处理,不能静默降级。 +- 持久产物不得包含原始 session ID、用户主目录、原始 prompt、凭证、完整 transcript 或临时绝对路径。 + +## 失败路由 + +- `TeamCreate` 缺失、成员数量不是三、成员跨 lane 读取、SendMessage 缺失、verify-run 失败:停止渲染并报告失败原因。 +- 成员返回 malformed JSON 或超时:终止本次团队运行,清理临时文件;quick 只能在仍有三个结构化结果且验证通过时继续。 +- 只允许用户显式授权后进行真实宿主模型 smoke;不要自动发布 Marketplace、npm 或远端代码。 diff --git a/agents/project-harness-reviewer.md b/agents/project-harness-reviewer.md new file mode 100644 index 0000000..c26c9ef --- /dev/null +++ b/agents/project-harness-reviewer.md @@ -0,0 +1,44 @@ +--- +name: project-harness-reviewer +description: Reviews only the Better Harness Project Harness lane for repository guardrails, delivery gates, and evidence-backed workflow gaps. +displayName: + en: "Project Guardrail Analyst" + zh: "项目护栏分析师" +profession: + en: "Project Guardrail Analyst" + zh: "项目护栏分析师" +maxTurns: 40 +skills: + - better-harness +--- + +# 项目护栏分析师 - 护程 + +你是正式团队成员,只分析 `projectHarness` lane。你不能创建团队、委派 Agent、读取 session lane 或把私有运行时字段带出 envelope。 + +## 分析框架 + +1. 只使用主理人传入的项目护栏 envelope 和 `inputHash`,不自行扩大扫描范围。 +2. 审核可验证的规范、测试、review、发布门禁、工作区拓扑和命令证据;区分存在配置与实际执行证据。 +3. 标记 coverage state:配置存在是 `configured`,启用是 `enabled`,实际命令证据是 `observed`,通过独立校验才是 `verified`;未知或缺失必须是 `unsupported`/`unavailable`。 +4. 输出少量高价值 findings,说明影响、证据边界、建议下一步和 confidence;不要生成总分,不要替主理人 reconciliation。 + +## 结构化回传 + +必须通过 `SendMessage` 向主理人回传且只回传一个 JSON 对象: + +```json +{ + "lane": "projectHarness", + "contextId": "", + "status": "completed|partial|unavailable", + "inputHash": "", + "output": { + "coverage": [{"surface": "...", "state": "configured|enabled|observed|verified|unsupported|unavailable"}], + "findings": [{"severity": "high|medium|low", "title": "...", "why": "bounded", "nextStep": "..."}], + "confidence": "normal|low" + } +} +``` + +不要写最终 `findings.json`、`report.md` 或 `report.html`;等待主理人验证回传。 diff --git a/agents/session-evidence-reviewer.md b/agents/session-evidence-reviewer.md new file mode 100644 index 0000000..e92547b --- /dev/null +++ b/agents/session-evidence-reviewer.md @@ -0,0 +1,45 @@ +--- +name: session-evidence-reviewer +description: Reviews only the Better Harness Session Evidence lane, preserving provider coverage, session schema diagnostics, and current-session exclusion boundaries. +displayName: + en: "Session Evidence Analyst" + zh: "会话证据分析师" +profession: + en: "Session Evidence Analyst" + zh: "会话证据分析师" +maxTurns: 40 +skills: + - better-harness +--- + +# 会话证据分析师 - 证安 + +你是正式团队成员,只分析 `sessionEvidence` lane。你不能创建团队、委派 Agent、读取另一 lane、读取 `workbuddy.db` 或输出原始 transcript。 + +## 分析框架 + +1. 只使用主理人传入的 lane envelope 和 `inputHash`;先确认 envelope 的 provider、coverage state、schema version 和 evidence boundary。 +2. 检查当前会话排除:优先 `CODEBUDDY_SESSION_ID`,兼容 legacy `WORKBUDDY_SESSION_ID`;冲突、未知身份、cwd-less exact slug 或未知 record shape 必须标记 `partial`,不得当成普通 metadata。 +3. 区分 `configured`、`enabled`、`observed`、`verified`、`unsupported`、`unavailable`;不得把未观测字段填成 0 或“无问题”。 +4. 仅输出可追溯的 session counts、schema diagnostics、coverage gaps、confidence 和 bounded findings;不复制路径、ID、prompt、凭证或完整事件。 + +## 结构化回传 + +使用 `SendMessage` 向主理人返回一个 JSON 对象: + +```json +{ + "lane": "sessionEvidence", + "contextId": "", + "status": "completed|partial|unavailable", + "inputHash": "", + "output": { + "coverage": [{"capability": "...", "state": "observed|unsupported|unavailable"}], + "schemaDiagnostics": [{"code": "...", "severity": "partial|error"}], + "findings": [{"severity": "high|medium|low", "title": "...", "evidence": "bounded"}], + "confidence": "normal|low" + } +} +``` + +回传后等待主理人收尾;不得直接写最终报告或把结果发给其他成员。 diff --git a/avatars/agent-customize-reviewer.svg b/avatars/agent-customize-reviewer.svg new file mode 100644 index 0000000..f47f5bf --- /dev/null +++ b/avatars/agent-customize-reviewer.svg @@ -0,0 +1 @@ + diff --git a/avatars/better-harness-review-director.svg b/avatars/better-harness-review-director.svg new file mode 100644 index 0000000..d400d17 --- /dev/null +++ b/avatars/better-harness-review-director.svg @@ -0,0 +1 @@ + diff --git a/avatars/project-harness-reviewer.svg b/avatars/project-harness-reviewer.svg new file mode 100644 index 0000000..90e8b2b --- /dev/null +++ b/avatars/project-harness-reviewer.svg @@ -0,0 +1 @@ + diff --git a/avatars/session-evidence-reviewer.svg b/avatars/session-evidence-reviewer.svg new file mode 100644 index 0000000..e13e567 --- /dev/null +++ b/avatars/session-evidence-reviewer.svg @@ -0,0 +1 @@ + diff --git a/avatars/team.svg b/avatars/team.svg new file mode 100644 index 0000000..4cd02d1 --- /dev/null +++ b/avatars/team.svg @@ -0,0 +1 @@ + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 66a31a8..b706a83 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -71,8 +71,9 @@ the target journey, contract, evidence, governance, and DX-measurement system. in the capability-owned agent-customize and session-analysis providers. The Codex shell owns local install/discovery metadata only; Codex evidence collection remains in the capability-owned provider and session-analysis modules. The public npm - package ships all six plugin metadata roots, while the Qoder runtime bundle - includes only `.qoder-plugin/`. + package ships all eight native plugin metadata roots (including the Pi + manifest and WorkBuddy Team shell), while the Qoder runtime bundle includes + only `.qoder-plugin/`. ## Developer Experience Routing diff --git a/docs/adapters/README.md b/docs/adapters/README.md index 3fa06d2..ca58c8a 100644 --- a/docs/adapters/README.md +++ b/docs/adapters/README.md @@ -17,11 +17,10 @@ providers, real session-evidence adapters, and output modes. Canonical product judgment stays in `skills/`, `models/`, `references/`, `templates/`, and `scripts//`. -The `@qoderai/better-harness` npm package includes six filesystem metadata -roots for Qoder, Claude Code, Codex, Cursor, Qwen, and Copilot, plus Pi install -metadata in the existing `package.json`. The generated Qoder runtime bundle -includes only the Qoder shell, `.qoder-plugin/`; non-Qoder generated host -artifacts remain source-local. Claude Code installs its shell through the +The `@qoderai/better-harness` npm package includes native metadata for Qoder, +Claude Code, Codex, Cursor, Qwen, Copilot, Pi, and WorkBuddy. The generated +Qoder runtime bundle includes only the Qoder shell, `.qoder-plugin/`; non-Qoder +generated host artifacts remain source-local. Claude Code installs its shell through the repository's native marketplace manifest. Pi installs the repository as a pi package through the `pi` manifest in `package.json`. @@ -33,8 +32,8 @@ package through the `pi` manifest in `package.json`. | Cursor | Analysis-capable source-local host | `.cursor-plugin/` | `scripts/agent-customize/providers/cursor.mjs` | `scripts/session-analysis/platforms/cursor.mjs` | self-contained HTML + Markdown | `.cursor` + `.codex` compatibility + `AGENTS.md` | `agent --plugin-dir . --mode ask --print` -> Cursor evidence bundle -> validated `html` render | | Qwen Code | Analysis-capable source-local host | `qwen-extension.json` | `scripts/agent-customize/providers/qwen.mjs` | `scripts/session-analysis/platforms/qwen.mjs` | self-contained HTML + Markdown | `.qwen` + `QWEN.md` + `AGENTS.md` | `harness prepare --platform qwen` -> finalize with `html-report` validation | | GitHub Copilot | Analysis-capable source-local host | `.github/plugin/` | `scripts/agent-customize/providers/copilot.mjs` | `scripts/session-analysis/platforms/copilot.mjs` | self-contained HTML + Markdown | `.github` + `AGENTS.md` + `~/.copilot` | `copilot plugin marketplace add .` -> `copilot plugin install better-harness@better-harness` -> configured-asset baseline -> validated `html` render | -| Pi | Analysis-capable source-local host | `pi` manifest in `package.json` | `scripts/agent-customize/providers/pi.mjs` | `scripts/session-analysis/platforms/pi.mjs` | self-contained HTML + Markdown | `.pi` + `.agents` + `AGENTS.md` | `pi install ` or `pi -e ` -> `/better-harness` prompt template -> validated `html` render | -| WorkBuddy | Analysis-capable source-local host | none (skills install into `~/.workbuddy/skills`) | `scripts/agent-customize/providers/workbuddy.mjs` | `scripts/session-analysis/platforms/workbuddy.mjs` | self-contained HTML + Markdown | `~/.workbuddy` `AGENTS.md` + identity files + `.agents` + `AGENTS.md` | `session-analysis --platform workbuddy sources` -> validated `html` render | +| Pi | Native extension-capable package | `pi` manifest in `package.json` | `scripts/agent-customize/providers/pi.mjs` | `scripts/session-analysis/platforms/pi.mjs` | self-contained HTML + Markdown | `.pi` + `.agents` + `AGENTS.md` | `pi install ` or `pi -e ` -> `/better-harness` native extension -> validated `html` render | +| WorkBuddy | Native Team expert plugin | `.codebuddy-plugin/` + `settings.json` + `agents/` | `scripts/agent-customize/providers/workbuddy.mjs` | `scripts/session-analysis/platforms/workbuddy.mjs` | self-contained HTML + Markdown | `~/.workbuddy` `AGENTS.md` + identity files + `.agents` + `AGENTS.md` | `codebuddy --plugin-dir .` -> Review Team -> validated `html` render | ## Discovery And Evidence @@ -92,9 +91,11 @@ package through the `pi` manifest in `package.json`. `scripts/session-analysis/platforms/pi.mjs`, which reads workspace-matching JSONL transcripts under `~/.pi/agent/sessions/----/` and honors the `PI_CODING_AGENT_DIR` and `PI_CODING_AGENT_SESSION_DIR` overrides. Pi - discovers the canonical root `skills/` directory and the `prompts/` - templates through the `pi` manifest in `package.json`; that manifest is - install/discovery metadata and does not own Pi evidence collection. + discovers the canonical root `skills/` directory and the native + `extensions/pi/better-harness.ts` through the `pi` manifest in + `package.json`. The extension registers exactly one `/better-harness` + command and owns only orchestration; evidence and rendering remain in the + canonical scripts. - WorkBuddy configured assets are inventoried through `scripts/agent-customize/providers/workbuddy.mjs`, covering `~/.workbuddy` user skills, marketplace plugins under `plugins/marketplaces/` with enabled @@ -106,9 +107,10 @@ package through the `pi` manifest in `package.json`. workspace-matching JSONL transcripts under `~/.workbuddy/projects//`. Embedded `cwd` values are authoritative; cwd-less 5.x transcripts qualify only from an exact workspace slug. The adapter honors the `WORKBUDDY_DIR` - override. WorkBuddy has no install shell in - this repository; skills install manually into `~/.workbuddy/skills` or - through WorkBuddy's own marketplace surfaces. + override. The native `.codebuddy-plugin/` Team shell points at the canonical + Skill and fixed lead/member Agent files; `scripts/packaging/workbuddy-plugin.mjs` + validates the Marketplace metadata and produces an offline archive without + copying private WorkBuddy state. ## Output Modes diff --git a/docs/adrs/directory-structure.md b/docs/adrs/directory-structure.md index 622f9c2..118c119 100644 --- a/docs/adrs/directory-structure.md +++ b/docs/adrs/directory-structure.md @@ -246,8 +246,8 @@ Use the tree first. These rules resolve common collisions: `.cursor-plugin/`, `.codex-plugin/`, and `.github/plugin/` are install/discovery shells for one host. Existing active shells may be hand-maintained narrowly, but the Qoder - public npm package ships all six plugin metadata roots, while the Qoder - runtime bundle ships only `.qoder-plugin/`. New host shells start from the + public npm package ships all eight native plugin metadata roots, while the + Qoder runtime bundle ships only `.qoder-plugin/`. New host shells start from the `docs/adapters/README.md` matrix; split to `docs/adapters/.md` and add a source-local `scripts/packaging/` builder only for an accepted host-artifact contract. New host identities still require the matrix split triggers. diff --git a/docs/community.md b/docs/community.md index bc8a419..2c85d74 100644 --- a/docs/community.md +++ b/docs/community.md @@ -37,7 +37,7 @@ This is the complete reference. For the common cases, see Start Here above. | Style grammar | Yes | `templates/style/` | Directive-only visual language; no runnable skeletons | Selected by report/style routing | Style-template tests and no copied runtime skeletons | | Structured knowledge | Candidate only | `knowledge-base/{official,community}/...` | `knowledge.md`, interim `schema.json`, fixtures, namespace uniqueness | Docs-only until registry spec, compiler, and binding tests exist | Namespace check, schema/fixture review, migration note | | Examples and operating models | Yes | `case-studies/` | Named example, scope, evidence boundary, non-runtime status | Reference material only unless separately bound | Link/path check; no runtime-policy claims | -| Host shell and packaging | Thin, or generated only after a split trigger | `.claude-plugin/`, `.qoder-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.github/plugin/`, `qwen-extension.json`, the `pi` manifest in `package.json`, future lifecycle shells | Install/discovery metadata and pointers to canonical owners | Public npm package includes all six current metadata roots; the Qoder runtime bundle includes only `.qoder-plugin/`, and generated host artifacts stay source-local | `scripts/npm-package/` verification, or split adapter note plus target builder | +| Host shell and packaging | Thin, or generated only after a split trigger | `.claude-plugin/`, `.qoder-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.github/plugin/`, `qwen-extension.json`, the `pi` manifest in `package.json`, `.codebuddy-plugin/` + `settings.json` + `agents/`, future lifecycle shells | Install/discovery metadata and pointers to canonical owners | Public npm package includes all eight current native metadata roots; the Qoder runtime bundle includes only `.qoder-plugin/`, and generated host artifacts stay source-local | `scripts/npm-package/` and `scripts/packaging/workbuddy-plugin.mjs` verification, or split adapter note plus target builder | ## Non-Extension Boundaries diff --git a/docs/docs/concepts/glossary.md b/docs/docs/concepts/glossary.md index 65f5c02..745cdac 100644 --- a/docs/docs/concepts/glossary.md +++ b/docs/docs/concepts/glossary.md @@ -66,7 +66,7 @@ progressive detail you load when a task needs it. | --- | --- | | Skill | A repeatable agent workflow defined by `SKILL.md` frontmatter plus a concise workflow. | | Host adapter | Per-host discovery and evidence-shape glue; keeps the engine host-neutral. | -| Host shell | Thin host metadata (`.claude-plugin/`, `.qoder-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.github/plugin/`, `qwen-extension.json`, or a future lifecycle shell) that exposes canonical behavior without owning product logic; the public npm package ships all six current metadata roots, while the Qoder runtime bundle includes only `.qoder-plugin/`. | +| Host shell | Thin host metadata (`.claude-plugin/`, `.qoder-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.github/plugin/`, `qwen-extension.json`, the `pi` manifest in `package.json`, `.codebuddy-plugin/` + `settings.json` + `agents/`, or a future lifecycle shell) that exposes canonical behavior without owning product logic; the public npm package ships all eight current native metadata roots, while the Qoder runtime bundle includes only `.qoder-plugin/`. | | Canonical owner | The single directory that owns a behavior's product judgment; host shells and mirrors point back to it. | The full glossary with owner links lives in diff --git a/docs/docs/hosts/adapter-matrix.md b/docs/docs/hosts/adapter-matrix.md index 4880b06..40e1e39 100644 --- a/docs/docs/hosts/adapter-matrix.md +++ b/docs/docs/hosts/adapter-matrix.md @@ -29,14 +29,12 @@ remains the complete capability-level source of truth. | Cursor | Verified Quickstart | Analysis-capable source-local host | `.cursor-plugin/` | Workspace-matched transcripts, metadata, and audit logs; partial coverage stays explicit | Self-contained HTML + Markdown | | Qwen Code | Verified Quickstart | Analysis-capable source-local host | `qwen-extension.json` | Workspace-matching local Qwen transcripts when present | Self-contained HTML + Markdown | | GitHub Copilot | Verified Quickstart | Analysis-capable source-local host | `.github/plugin/` | Workspace-matched Copilot CLI transcripts; partial coverage stays explicit | Self-contained HTML + Markdown | -| Pi | Adapter support | Analysis-capable source-local host | `pi` manifest in `package.json` | Workspace-matching local Pi sessions | Self-contained HTML + Markdown | -| WorkBuddy | Adapter support | Analysis-capable source-local host | None; skills use WorkBuddy-owned paths | Workspace-matching WorkBuddy JSONL transcripts | Self-contained HTML + Markdown | +| Pi | Adapter support | Native extension-capable package | `pi` manifest + `extensions/pi/better-harness.ts` | Workspace-matching local Pi sessions | Self-contained HTML + Markdown | +| WorkBuddy | Adapter support | Native Team expert plugin | `.codebuddy-plugin/` + `settings.json` + `agents/` | Workspace-matching WorkBuddy JSONL transcripts | Self-contained HTML + Markdown | -The `@qoderai/better-harness` npm package includes all six plugin metadata -roots. Pi reuses install metadata in the existing `package.json`, so it does -not add a seventh filesystem metadata root. The generated Qoder runtime bundle -includes only the Qoder shell; non-Qoder generated host artifacts remain -source-local. +The `@qoderai/better-harness` npm package includes native metadata for all +eight adapters. The generated Qoder runtime bundle includes only the Qoder +shell; non-Qoder generated host artifacts remain source-local. ## Output modes @@ -52,17 +50,18 @@ source-local. ### Pi {#pi} Pi can install the repository through `pi install ` or load it with -`pi -e `. Package discovery, configured assets, workspace-matched -session evidence, and portable HTML routing are implemented. Pi remains outside -the verified Quickstart set until a complete interactive report-loop smoke is -observed. +`pi -e `. Its native extension registers one `/better-harness` command, +orchestrates three isolated RPC lanes, and leaves evidence collection and +rendering in the canonical scripts. Pi remains outside the verified Quickstart +set until a complete interactive report-loop smoke is observed. ### WorkBuddy {#workbuddy} WorkBuddy configured assets, workspace-matched session evidence, and portable -HTML routing are implemented. This repository does not ship a WorkBuddy install -shell, plugin manifest, or npm-packaged host artifact; installation remains on -WorkBuddy's own `~/.workbuddy/skills` or marketplace surfaces. +HTML routing are implemented. The native Team plugin uses one lead and three +fixed members, and `npm run workbuddy:verify` validates its manifest/archive +without copying private host state. It remains outside the verified Quickstart +set until a complete interactive report-loop smoke is observed. ## Capability coverage diff --git a/docs/docs/installation.mdx b/docs/docs/installation.mdx index b8cee2a..ebbcaa6 100644 --- a/docs/docs/installation.mdx +++ b/docs/docs/installation.mdx @@ -26,7 +26,7 @@ Node.js and npm are required only when you use the standalone CLI, run Better Harness from a source checkout, or contribute to this repository. Those paths support Windows, macOS, and Linux and require: -- Node.js `>=22.20.0 <25.0.0` +- Node.js `>=22.20.0 <26.0.0` - npm `>=10.9.3 <12.0.0` Check the active runtime before using the standalone or source CLI: diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index d28d4d2..0131fa5 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -25,6 +25,8 @@ startup. Then use the check supported by that host: | [Cursor](./installation?host=cursor#cursor) | Start the agent with `cursor-agent --plugin-dir /path/to/better-harness`, keep that process open, and run the report prompt in the same session. | | [Qwen Code](./installation?host=qwen-code#qwen-code) | Start a new session and run the report prompt. This guide does not assume an unverified extension-list command. | | [GitHub Copilot](./installation?host=github-copilot#github-copilot) | Run `copilot plugin list` and `copilot skill list`; both should include `better-harness`. | +| [Pi](./hosts/adapter-matrix#pi) | Run `pi --version`, then start a new Pi session with the package and run `/better-harness`. | +| [WorkBuddy](./hosts/adapter-matrix#workbuddy) | Run `codebuddy --plugin-dir .` for local Team discovery and `npm run workbuddy:verify` for the offline manifest check. | If a marketplace command fails, return to the linked host tab and compare the repository source and command spelling exactly. In particular, current Codex @@ -43,7 +45,7 @@ copy the checkout into a global plugin directory as a troubleshooting step. ## The standalone or source CLI reports an unsupported runtime -The standalone and source CLIs support Node.js `>=22.20.0 <25.0.0` and npm +The standalone and source CLIs support Node.js `>=22.20.0 <26.0.0` and npm `>=10.9.3 <12.0.0` on Windows, macOS, and Linux. Check the active executables: ```bash @@ -99,6 +101,8 @@ artifacts are: | Cursor | `/.cursor/better-harness//` | `findings.json`, `report.md`, `report.html` | | Qwen Code | `/.qwen/better-harness//` | `findings.json`, `report.md`, `report.html` | | GitHub Copilot | `/.copilot/better-harness//` | `findings.json`, `report.md`, `report.html` | +| Pi | `/.pi/better-harness/` | `findings.json`, `report.md`, `report.html` | +| WorkBuddy | `/.workbuddy/better-harness/` | `findings.json`, `report.md`, `report.html` | `` is the repository being reviewed, not the Better Harness source checkout unless that is the selected target. diff --git a/docs/glossary.md b/docs/glossary.md index c7c380b..941fd5e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -79,7 +79,7 @@ for extension surfaces, read [community.md](community.md). |---|---|---| | Skill | A repeatable agent workflow defined by `SKILL.md` frontmatter plus a concise workflow. | [community.md](community.md); report use: [report contract](../skills/better-harness/SKILL.md#report-output) | | Host adapter | Per-host discovery and evidence-shape glue (e.g. Qoder, Codex); keeps the engine host-neutral. | [adapters/README.md](adapters/README.md) | -| Host shell | Thin host metadata (`.claude-plugin/`, `.qoder-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.github/plugin/`, `qwen-extension.json`, the `pi` manifest in `package.json`, or a future lifecycle shell) that exposes canonical behavior without owning product logic; the public npm package ships all six current metadata roots, while the Qoder runtime bundle includes only `.qoder-plugin/`. | [ARCHITECTURE.md](ARCHITECTURE.md) | +| Host shell | Thin host metadata (`.claude-plugin/`, `.qoder-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.github/plugin/`, `qwen-extension.json`, the `pi` manifest in `package.json`, `.codebuddy-plugin/` + `settings.json` + `agents/`, or a future lifecycle shell) that exposes canonical behavior without owning product logic; the public npm package ships all eight current native metadata roots, while the Qoder runtime bundle includes only `.qoder-plugin/`. | [ARCHITECTURE.md](ARCHITECTURE.md) | | Canonical owner | The single directory that owns a behavior's product judgment; host shells and mirrors point back to it. | [ARCHITECTURE.md](ARCHITECTURE.md) | ## "I Want To… → Use" diff --git a/docs/i18n/zh-Hans/code.json b/docs/i18n/zh-Hans/code.json index 63f7707..37535c9 100644 --- a/docs/i18n/zh-Hans/code.json +++ b/docs/i18n/zh-Hans/code.json @@ -69,13 +69,13 @@ "message": "包 + CLI 扩展" }, "homepage.hosts.pi.setup": { - "message": "安装与证据适配器已可用;完整交互式报告闭环仍待冒烟验证。" + "message": "安装原生扩展;经授权的真实模型 E2E 仍是独立验证门禁。" }, "homepage.hosts.workBuddy.method": { - "message": "Skill / Marketplace 路径" + "message": "原生 Team 专家插件" }, "homepage.hosts.workBuddy.setup": { - "message": "证据与报告适配器已可用;安装仍通过 WorkBuddy 自有路径完成。" + "message": "使用 codebuddy --plugin-dir . 发现仓库根目录并运行评审专家团。" }, "homepage.hosts.output.html": { "message": "HTML + Markdown 报告" diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/glossary.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/glossary.md index a6a9e5c..cb3b713 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/glossary.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/glossary.md @@ -63,7 +63,7 @@ sidebar_position: 3 | --- | --- | | Skill | 由 `SKILL.md` frontmatter 加简洁工作流定义的可重复智能体工作流。 | | 宿主适配层 | 按宿主的发现与证据形态胶水层;保持引擎与宿主无关。 | -| 宿主 shell | 轻量宿主元数据(`.claude-plugin/`、`.qoder-plugin/`、`.cursor-plugin/`、`.codex-plugin/`、`.github/plugin/`、`qwen-extension.json` 或未来的生命周期 shell),暴露规范行为但不拥有产品逻辑;公共 npm 包包含当前六个元数据根目录,而 Qoder 运行时 bundle 只包含 `.qoder-plugin/`。 | +| 宿主 shell | 轻量宿主元数据(`.claude-plugin/`、`.qoder-plugin/`、`.cursor-plugin/`、`.codex-plugin/`、`.github/plugin/`、`qwen-extension.json`、`package.json` 中的 `pi` manifest、`.codebuddy-plugin/` + `settings.json` + `agents/` 或未来的生命周期 shell),暴露规范行为但不拥有产品逻辑;公共 npm 包包含当前八个原生元数据根目录,而 Qoder 运行时 bundle 只包含 `.qoder-plugin/`。 | | 规范负责目录 | 唯一拥有某行为产品判断的目录;宿主 shell 和镜像都指回它。 | 带负责方链接的完整术语表见 diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/hosts/adapter-matrix.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/hosts/adapter-matrix.md index 7a9298a..8978e5f 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/hosts/adapter-matrix.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/hosts/adapter-matrix.md @@ -27,12 +27,11 @@ Better Harness 当前声明了八个能力层宿主适配器,其中六个已 | Cursor | 已验证快速开始 | 具备分析能力的源码本地宿主 | `.cursor-plugin/` | 工作区匹配的转录、元数据和审计日志;部分覆盖保持显式标注 | 自包含 HTML + Markdown | | Qwen Code | 已验证快速开始 | 具备分析能力的源码本地宿主 | `qwen-extension.json` | 匹配当前工作区的本地 Qwen 转录(存在时) | 自包含 HTML + Markdown | | GitHub Copilot | 已验证快速开始 | 具备分析能力的源码本地宿主 | `.github/plugin/` | 工作区匹配的 Copilot CLI 转录;部分覆盖保持显式标注 | 自包含 HTML + Markdown | -| Pi | 适配器支持 | 具备分析能力的源码本地宿主 | `package.json` 中的 `pi` manifest | 匹配当前工作区的本地 Pi 会话 | 自包含 HTML + Markdown | -| WorkBuddy | 适配器支持 | 具备分析能力的源码本地宿主 | 无;Skill 使用 WorkBuddy 自有路径 | 匹配工作区的 WorkBuddy JSONL 转录 | 自包含 HTML + Markdown | +| Pi | 适配器支持 | 原生扩展包 | `pi` manifest + `extensions/pi/better-harness.ts` | 匹配当前工作区的本地 Pi 会话 | 自包含 HTML + Markdown | +| WorkBuddy | 适配器支持 | 原生 Team 专家插件 | `.codebuddy-plugin/` + `settings.json` + `agents/` | 匹配工作区的 WorkBuddy JSONL 转录 | 自包含 HTML + Markdown | -`@qoderai/better-harness` npm 包含全部六个插件元数据根目录。生成的 Qoder -运行时 bundle 只包含 Qoder shell。Pi 复用现有 `package.json` 中的安装元数据, -因此不会新增第七个文件系统元数据根目录;非 Qoder 的生成宿主产物保持源码本地。 +`@qoderai/better-harness` npm 包含八个宿主的原生元数据。生成的 Qoder +运行时 bundle 只包含 Qoder shell;非 Qoder 的生成宿主产物保持源码本地。 ## 输出模式 @@ -48,14 +47,14 @@ Better Harness 当前声明了八个能力层宿主适配器,其中六个已 ### Pi {#pi} Pi 可以通过 `pi install ` 安装本仓库,或使用 `pi -e ` 加载。 -包发现、已配置资产、工作区匹配的会话证据与可移植 HTML 路由均已实现。 +原生扩展注册唯一的 `/better-harness` 命令,编排三路隔离 RPC;证据收集与渲染仍由规范脚本负责。 在观察到完整交互式报告闭环冒烟验证前,Pi 仍不进入已验证快速开始集合。 ### WorkBuddy {#workbuddy} WorkBuddy 的已配置资产、工作区匹配的会话证据与可移植 HTML 路由均已实现。 -本仓库不提供 WorkBuddy 安装 Shell、插件 manifest 或 npm 打包的宿主产物;安装 -仍通过 WorkBuddy 自有的 `~/.workbuddy/skills` 或 Marketplace 入口完成。 +原生 Team 插件使用一个主理人和三个固定成员;`npm run workbuddy:verify` 会验证 +manifest/归档且不会复制私有宿主状态。在完整交互式报告闭环 smoke 通过前,仍保留适配器支持边界。 ## 能力覆盖 diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.mdx b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.mdx index 4c406ea..88183ed 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.mdx +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.mdx @@ -22,7 +22,7 @@ import TabItem from '@theme/TabItem'; 只有使用独立 CLI、从源码检出运行 Better Harness 或参与本仓库开发时,才需要 Node.js 和 npm。这些路径支持 Windows、macOS 和 Linux,并要求: -- Node.js `>=22.20.0 <25.0.0` +- Node.js `>=22.20.0 <26.0.0` - npm `>=10.9.3 <12.0.0` 使用独立或源码 CLI 前,请检查当前运行时: diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/troubleshooting.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/troubleshooting.md index 1965ee9..851e30f 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/troubleshooting.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/troubleshooting.md @@ -22,6 +22,8 @@ sidebar_position: 4 | [Cursor](./installation?host=cursor#cursor) | 使用 `cursor-agent --plugin-dir /path/to/better-harness` 启动,保持该进程打开,并在同一会话运行报告提示词。 | | [Qwen Code](./installation?host=qwen-code#qwen-code) | 开启新会话并运行报告提示词;本指南不会假定未经验证的扩展列表命令。 | | [GitHub Copilot](./installation?host=github-copilot#github-copilot) | 运行 `copilot plugin list` 和 `copilot skill list`;两处都应包含 `better-harness`。 | +| [Pi](./hosts/adapter-matrix#pi) | 运行 `pi --version`,然后在加载该 package 的新 Pi 会话中运行 `/better-harness`。 | +| [WorkBuddy](./hosts/adapter-matrix#workbuddy) | 运行 `codebuddy --plugin-dir .` 做本地专家团发现,并运行 `npm run workbuddy:verify` 做离线 manifest 校验。 | 如果 marketplace 命令失败,请返回对应宿主的安装标签页,逐字核对仓库源和命令。 特别是当前 Codex 先对仓库 URL 使用 `marketplace add`,再使用 `plugin add`; @@ -39,7 +41,7 @@ Qoder CLI 使用 `plugin install`。 ## 独立或源码 CLI 报告运行时版本不受支持 独立和源码 CLI 在 Windows、macOS 和 Linux 上支持 Node.js -`>=22.20.0 <25.0.0` 及 npm `>=10.9.3 <12.0.0`。请检查当前实际使用的可执行文件: +`>=22.20.0 <26.0.0` 及 npm `>=10.9.3 <12.0.0`。请检查当前实际使用的可执行文件: ```bash node --version @@ -90,6 +92,8 @@ node scripts/better-harness.mjs report --qoder-home /path/to/qoder-data | Cursor | `/.cursor/better-harness//` | `findings.json`、`report.md`、`report.html` | | Qwen Code | `/.qwen/better-harness//` | `findings.json`、`report.md`、`report.html` | | GitHub Copilot | `/.copilot/better-harness//` | `findings.json`、`report.md`、`report.html` | +| Pi | `/.pi/better-harness/` | `findings.json`、`report.md`、`report.html` | +| WorkBuddy | `/.workbuddy/better-harness/` | `findings.json`、`report.md`、`report.html` | `` 指正在接受评审的仓库;只有当 Better Harness 源码仓库本身就是选定目标 时,它才表示当前源码检出。 diff --git a/docs/specs/2026-08-06-better-harness-host-runtime-contract.md b/docs/specs/2026-08-06-better-harness-host-runtime-contract.md new file mode 100644 index 0000000..8e9760b --- /dev/null +++ b/docs/specs/2026-08-06-better-harness-host-runtime-contract.md @@ -0,0 +1,68 @@ +# Better Harness host runtime contract + +## Traceability + +- Spec ID: `better-harness-host-runtime-contract` +- Status: Draft +- Story: Pi Agent and WorkBuddy plugin completion + +## Intent + +Provide one host-neutral contract for preparing evidence, running exactly three +independent read-only specialist passes, validating their returns, and handing +the result to the existing lead reconciliation and renderer. The contract must +keep unsupported capability and privacy boundaries visible instead of treating a +successful collector call as complete coverage. + +## Acceptance Scenarios + +- **HRC-AC-01:** A prepared run contains exactly three named lanes: session + evidence, project harness, and agent customize; each lane has a distinct + bounded input hash. +- **HRC-AC-02:** `verify-run` accepts exactly three successful structured + results, rejects duplicate lane/context identities, and rejects a result that + contains another lane's input or raw collection references. +- **HRC-AC-03:** Normal runs block on any unavailable or partial specialist; + quick runs preserve the gap and lower confidence without inventing a result. +- **HRC-AC-04:** Provider coverage states and session schema diagnostics reach + the final Evidence Boundary; unsupported and unobserved are never rendered as + zero or absent capability. +- **HRC-AC-05:** Durable artifacts contain no raw session IDs, home paths, + prompts, credentials, or private temporary run paths. +- **HRC-AC-06:** Temporary run data is removed on success, cancellation, timeout, + and validation failure. + +## Non-goals + +- No new scoring model or replacement renderer. +- No automatic reads of WorkBuddy binary stores or private Memory bodies. +- No remote publication, npm release, Marketplace submission, or Git push. + +## Plan and Tasks + +1. Add a typed run-plan/result contract under `scripts/harness-analysis` with + lane allowlists, input hashes, execution identity, coverage state, and + quick/normal failure policy. +2. Add advanced CLI routes for host doctor, run preparation, and run + verification. Keep private lane payloads in an explicitly supplied temporary + output directory and keep stdout metadata-only. +3. Thread provider `coverage` and `schemaDiagnostics` through inventory, + evidence-bundle, report-run, and report-quality without changing existing + renderer field semantics. +4. Add contract tests for isolation, duplicate contexts, malformed results, + coverage propagation, privacy redaction, and cleanup. + +## Test and Review Evidence + +- Focused unit tests must map each HRC AC to a test name. +- `npm test`, `npm run pack:verify`, `node scripts/doc-link-graph/cli.mjs skills/better-harness`, + `node --test test/doc-link-graph.test.mjs`, and `git diff --check` are required + before implementation is marked complete. +- Host-specific specs must provide real Pi and WorkBuddy smoke evidence for the + execution identity and final renderer status. + +## Risk + +The main risk is confusing transport success with semantic coverage. The run +contract therefore carries an explicit coverage state and blocks normal reports +when an owner is unavailable. diff --git a/docs/specs/2026-08-06-pi-native-runtime-completion.md b/docs/specs/2026-08-06-pi-native-runtime-completion.md new file mode 100644 index 0000000..e32d6f0 --- /dev/null +++ b/docs/specs/2026-08-06-pi-native-runtime-completion.md @@ -0,0 +1,67 @@ +# Pi native runtime completion + +## Traceability + +- Spec ID: `pi-native-runtime-completion` +- Status: Draft +- Story: Pi Agent and WorkBuddy plugin completion + +## Intent + +Turn the existing Pi package into a native Pi extension with one unambiguous +`/better-harness` command. The extension must run three fresh read-only Pi RPC +contexts, preserve lane isolation, and pass validated results to the current +Pi lead for reconciliation and HTML rendering. + +## Acceptance Scenarios + +- **PI-AC-01:** A clean `pi install` or `pi -e` run discovers exactly one + `/better-harness` command and the canonical Skill. +- **PI-AC-02:** `host-doctor --platform pi` distinguishes missing Node, missing + package resources, missing model/auth, current-session ambiguity, and an + unwritable output root. +- **PI-AC-03:** The command launches three parallel RPC children with + `--no-session`, `--no-tools`, `--no-extensions`, `--no-skills`, + `--no-prompt-templates`, `--no-context-files`, and `--no-approve`; lane data + travels over RPC stdin rather than process arguments. +- **PI-AC-04:** The current Pi session is excluded from evidence and cannot be + reintroduced by a custom session directory. +- **PI-AC-05:** A real authorized model run creates and validates + `findings.json`, `report.md`, and `report.html` under `.pi/better-harness`. +- **PI-AC-06:** Abort, timeout, malformed JSON, duplicate context, and single + lane failure terminate all children and follow the shared quick/normal policy. +- **PI-AC-07:** Extension discovery and focused tests pass on Node 22.20, 24, + and 25; the package engine range includes the supported Pi runtime. + +## Non-goals + +- No Pi-native MCP registry or theme inventory. +- No mutation tools in specialist children. +- No hand-written report renderer or Canvas output for Pi. +- No automatic access to user-home data without explicit scope. + +## Plan and Tasks + +1. Add `extensions/pi/better-harness.ts`, register it through `package.json`, + and remove the Pi prompt-template collision while retaining the Skill. +2. Implement a subprocess adapter using Pi RPC JSONL, inherited provider/model + selection, bounded stdin prompts, timeout/abort handling, and child cleanup. +3. Invoke the shared host runtime contract, then inject the validated lead and + specialist results into the current Pi turn for reconciliation and renderer + validation. +4. Add Pi discovery, command collision, fake-RPC, privacy, cancellation, and + real-host smoke tests. + +## Test and Review Evidence + +- Isolated `pi list`/`pi --mode rpc` discovery evidence must show one command. +- Fake child tests must assert every safety flag and empty argv payload. +- Real E2E must record three distinct child identities and renderer `status: + pass`; credentials and raw model output are not committed. +- Package verification must include the extension and peer dependency contract. + +## Risk + +Pi intentionally has no built-in sub-agent abstraction. Subprocess RPC is the +portable, auditable boundary, but Pi version drift can change RPC events; the +adapter must fail closed on unknown response shapes. diff --git a/docs/specs/2026-08-06-workbuddy-expert-plugin.md b/docs/specs/2026-08-06-workbuddy-expert-plugin.md new file mode 100644 index 0000000..1ad4adf --- /dev/null +++ b/docs/specs/2026-08-06-workbuddy-expert-plugin.md @@ -0,0 +1,69 @@ +# WorkBuddy expert plugin + +## Traceability + +- Spec ID: `workbuddy-expert-plugin` +- Status: Draft +- Story: Pi Agent and WorkBuddy plugin completion + +## Intent + +Ship Better Harness as a WorkBuddy Marketplace-compatible Team plugin while +retaining the repository root as the canonical runtime root. The Team lead owns +one evidence collection and reconciliation; three named members independently +review the three bounded evidence lanes and return structured results. + +## Acceptance Scenarios + +- **WB-AC-01:** `.codebuddy-plugin/plugin.json`, Marketplace metadata, root + `settings.json`, four Agent MD files, and the canonical Skill form a valid + Team plugin with `categoryId: 10-ProjectQuality`. +- **WB-AC-02:** `codebuddy --plugin-dir .` discovers the Team and all three + members from an empty WorkBuddy config root without copying only the Skill. +- **WB-AC-03:** A real Team run contains exactly one TeamCreate, three distinct + members, and three independent SendMessage returns; no member delegates. +- **WB-AC-04:** The lead runs `prepare-run --platform workbuddy` once, passes + only each member's lane envelope and input hash, verifies the returns, and + renders only after reconciliation. +- **WB-AC-05:** `CODEBUDDY_SESSION_ID` is preferred for current-session + exclusion; legacy `WORKBUDDY_SESSION_ID` is accepted, and conflicting values + block the run. +- **WB-AC-06:** A real authorized run validates the three HTML artifacts under + `.workbuddy/better-harness`; quick/normal failure behavior matches HRC. +- **WB-AC-07:** Official expert validation, Marketplace manifest validation, + archive inspection, and cross-platform path/process smoke pass. +- **WB-AC-08:** The package contains no user configuration, transcript, secret, + cache, symlink escape, or absolute local path. + +## Non-goals + +- No WorkBuddy binary database parser or automatic connector activation. +- No second copy of Better Harness judgment logic. +- No remote Marketplace registration or public release submission. + +## Plan and Tasks + +1. Add `.codebuddy-plugin/plugin.json`, `.codebuddy-plugin/marketplace.json`, + root `settings.json`, role Agent MD files, and marketplace-safe avatars. +2. Register the canonical `skills/better-harness` path and document the Team + SOP: one lead collection, one TeamCreate, three parallel members, one + verification and one render. +3. Add local package/archive generation and a manifest/version/content validator + that rejects private files, absolute paths, symlinks, and missing resources. +4. Add WorkBuddy fixture tests and an authorized real-host smoke using an + isolated `WORKBUDDY_CONFIG_DIR`. + +## Test and Review Evidence + +- Manifest tests must cover required Team fields, exactly three members, + `defaultInitPrompt` equality, three tags, and version synchronization. +- Archive tests must unpack into an empty directory and verify the root runtime + resources referenced by `skills/better-harness/SKILL.md`. +- Real WorkBuddy smoke must retain only bounded counts, event types, agent IDs, + hashes, and renderer status; raw transcripts remain private and temporary. + +## Risk + +WorkBuddy's Team workflow is host-owned and may change independently of the +plugin manifest. The Agent prompts and validator must treat missing Team events +as a failed runtime contract rather than a successful single-agent fallback. diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 5da2ad6..df31789 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -246,7 +246,7 @@ function hosts() { setup: translate({ id: "homepage.hosts.pi.setup", message: - "Install and evidence adapters are available; a full interactive report smoke remains pending.", + "Install the native extension; authorized real-model E2E remains a separate verification gate.", }), anchor: "pi", supportLevel: "adapter", @@ -258,13 +258,13 @@ function hosts() { name: "WorkBuddy", method: translate({ id: "homepage.hosts.workBuddy.method", - message: "Skill / marketplace path", + message: "Native Team expert plugin", }), output: htmlOutput, setup: translate({ id: "homepage.hosts.workBuddy.setup", message: - "Evidence and report adapters are available; installation stays on WorkBuddy-owned paths.", + "Discover the repository root with codebuddy --plugin-dir . and run the Review Team.", }), anchor: "workbuddy", supportLevel: "adapter", diff --git a/extensions/pi/better-harness.ts b/extensions/pi/better-harness.ts new file mode 100644 index 0000000..44ac1e5 --- /dev/null +++ b/extensions/pi/better-harness.ts @@ -0,0 +1,424 @@ +/** + * Native Pi Agent entry point for Better Harness. + * + * The extension owns orchestration only. Evidence collection and report + * semantics stay in the canonical scripts/ tree so Pi and WorkBuddy cannot + * silently drift into different policy implementations. + */ + +import { spawn as nodeSpawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { access, constants as fsConstants } from "node:fs/promises"; +import path from "node:path"; +import type { ChildProcessWithoutNullStreams, SpawnOptions } from "node:child_process"; +import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; + +import { + hostDoctor, + prepareHostRun, + verifyHostRun, +} from "../../scripts/harness-analysis/host-runtime/index.mjs"; + +export const PI_RPC_FLAGS = Object.freeze([ + "--mode", "rpc", + "--no-session", + "--no-tools", + "--no-extensions", + "--no-skills", + "--no-prompt-templates", + "--no-context-files", + "--no-approve", +]); + +export const DEFAULT_RPC_TIMEOUT_MS = 120_000; +export const PI_COMMAND_NAME = "better-harness"; + +const REQUEST_FLAGS = new Set(["--quick", "--normal", "--language"]); + +function extensionError(code: string, message: string): Error & { code: string } { + return Object.assign(new Error(message), { code }); +} + +function modelPattern(model: unknown, thinkingLevel?: unknown): string | undefined { + if (!model || typeof model !== "object") return undefined; + const value = model as { provider?: unknown; id?: unknown }; + if (typeof value.id !== "string" || value.id.length === 0) return undefined; + const provider = typeof value.provider === "string" && value.provider.length > 0 + ? `${value.provider}/` + : ""; + const thinking = typeof thinkingLevel === "string" && thinkingLevel.length > 0 + ? `:${thinkingLevel}` + : ""; + return `${provider}${value.id}${thinking}`; +} + +/** Parse only Better Harness request flags; no shell syntax or arbitrary CLI flags are accepted. */ +export function parseReviewRequest(input = "") { + const tokens = String(input).trim().split(/\s+/u).filter(Boolean); + let depth: "quick" | "normal" = "normal"; + let language = "en"; + const request: string[] = []; + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index]; + if (token === "--quick") { + depth = "quick"; + continue; + } + if (token === "--normal") { + depth = "normal"; + continue; + } + if (token === "--language") { + const next = tokens[++index]; + if (!next || next.startsWith("-")) { + throw extensionError("INVALID_COMMAND_ARGUMENT", "--language requires a locale"); + } + language = next; + continue; + } + if (token.startsWith("--language=")) { + const value = token.slice("--language=".length); + if (!value) throw extensionError("INVALID_COMMAND_ARGUMENT", "--language requires a locale"); + language = value; + continue; + } + if (token.startsWith("-")) { + throw extensionError("INVALID_COMMAND_ARGUMENT", `unsupported /better-harness option: ${token}`); + } + request.push(token); + } + return { depth, language, request: request.join(" ") }; +} + +export function buildRpcArgs(model?: string): string[] { + const args = [...PI_RPC_FLAGS]; + if (model) args.push("--model", model); + return args; +} + +function textFromEvent(event: any): string { + const candidate = event?.message ?? event?.assistantMessage ?? event?.data?.message ?? event; + const content = candidate?.content ?? event?.content; + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((part: any) => typeof part === "string" ? part : part?.type === "text" ? part.text : "") + .filter((part: string) => part.length > 0) + .join(""); +} + +function parseJsonRecord(text: string): Record { + const candidates = [ + text.trim(), + ...[...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/giu)].map((match) => match[1].trim()), + ]; + const firstObject = text.indexOf("{"); + const lastObject = text.lastIndexOf("}"); + if (firstObject >= 0 && lastObject > firstObject) { + candidates.push(text.slice(firstObject, lastObject + 1)); + } + for (const candidate of candidates) { + try { + const parsed = JSON.parse(candidate); + const value = parsed?.result && typeof parsed.result === "object" ? parsed.result : parsed; + if (value && typeof value === "object" && !Array.isArray(value)) return value; + } catch { + // Try the next bounded candidate; a non-JSON model answer fails closed below. + } + } + throw extensionError("MALFORMED_SPECIALIST_JSON", "Pi RPC specialist did not return one JSON object"); +} + +function killChild(child: any, signal = "SIGTERM") { + try { + if (child && !child.killed) child.kill(signal); + } catch { + // The process may have exited between the state check and kill(). + } +} + +export interface RpcLaneOptions { + lane: string; + inputHash: string; + input: unknown; + model?: string; + cwd: string; + executable?: string; + timeoutMs?: number; + signal?: AbortSignal; + spawn?: (file: string, args: string[], options: SpawnOptions) => ChildProcessWithoutNullStreams; + onChild?: (child: any) => void; + onChildExit?: (child: any) => void; +} + +function lanePrompt(lane: string, inputHash: string, input: unknown, contextId: string): string { + return [ + "You are one isolated, read-only Better Harness Pi specialist.", + `Lane: ${lane}`, + `Context identity: ${contextId}`, + `Input hash: ${inputHash}`, + "Use only the evidence envelope below. Do not inspect files, sessions, home directories, tools, or another lane.", + "Return exactly one JSON object with keys lane, contextId, status, inputHash, output.", + "status must be completed, partial, or unavailable; output must be a bounded object.", + JSON.stringify({ lane, contextId, inputHash, evidence: input }), + ].join("\n"); +} + +/** Run one Pi RPC child. It never puts lane evidence in argv. */ +export function runPiRpcLane(options: RpcLaneOptions): Promise> { + if (options.signal?.aborted) { + return Promise.reject(extensionError("SPECIALIST_CANCELLED", `Pi specialist cancelled: ${options.lane}`)); + } + const spawn = options.spawn ?? ((file, args, spawnOptions) => nodeSpawn(file, args, spawnOptions)); + const executable = options.executable ?? process.env.PI_BIN ?? "pi"; + const timeoutMs = options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS; + const model = options.model; + const args = buildRpcArgs(model); + let child: ChildProcessWithoutNullStreams; + try { + child = spawn(executable, args, { + cwd: options.cwd, + env: { ...process.env }, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); + } catch (error) { + return Promise.reject(Object.assign(error as Error, { code: "RPC_SPAWN_FAILED" })); + } + options.onChild?.(child); + const contextId = `pi-rpc:${String(child.pid ?? "unknown")}:${randomUUID()}`; + const prompt = lanePrompt(options.lane, options.inputHash, options.input, contextId); + + return new Promise((resolve, reject) => { + let settled = false; + let buffer = ""; + let sawAgentEnd = false; + const deltaText: string[] = []; + const messageText: string[] = []; + const timeout = setTimeout(() => finish(extensionError("SPECIALIST_TIMEOUT", `Pi specialist timed out: ${options.lane}`)), timeoutMs); + + const cleanup = () => { + clearTimeout(timeout); + options.signal?.removeEventListener("abort", onAbort); + options.onChildExit?.(child); + }; + const finish = (error?: Error & { code?: string }) => { + if (settled) return; + settled = true; + cleanup(); + if (error) { + killChild(child); + reject(error); + return; + } + const text = messageText.length > 0 ? messageText.join("") : deltaText.join(""); + try { + const result = parseJsonRecord(text); + resolve({ ...result, pid: child.pid ?? null, contextId: result.contextId ?? contextId }); + } catch (parseError) { + killChild(child); + reject(parseError); + } + }; + const onAbort = () => finish(extensionError("SPECIALIST_CANCELLED", `Pi specialist cancelled: ${options.lane}`)); + options.signal?.addEventListener("abort", onAbort, { once: true }); + + const onLine = (line: string) => { + if (!line.trim()) return; + let event: any; + try { + event = JSON.parse(line); + } catch { + finish(extensionError("RPC_MALFORMED_JSON", `Pi RPC emitted malformed JSON for ${options.lane}`)); + return; + } + if (event?.type === "response" && event.success === false) { + finish(extensionError("RPC_COMMAND_FAILED", String(event.error?.message ?? event.error ?? "Pi RPC command failed"))); + return; + } + if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") { + deltaText.push(String(event.assistantMessageEvent.delta ?? "")); + } else if (event?.type === "message_end") { + const text = textFromEvent(event); + if (text) messageText.push(text); + } else if (event?.type === "agent_end") { + sawAgentEnd = true; + finish(); + } + }; + child.stdout.on("data", (chunk: Buffer | string) => { + buffer += String(chunk); + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex >= 0) { + let line = buffer.slice(0, newlineIndex); + if (line.endsWith("\r")) line = line.slice(0, -1); + buffer = buffer.slice(newlineIndex + 1); + onLine(line); + newlineIndex = buffer.indexOf("\n"); + } + }); + child.stdout.on("end", () => { + if (buffer.length > 0) onLine(buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer); + }); + child.on("error", (error: Error) => finish(Object.assign(error, { code: "RPC_SPAWN_FAILED" }))); + child.on("close", (code: number | null) => { + if (!settled && !sawAgentEnd) { + finish(extensionError("RPC_EARLY_EXIT", `Pi RPC child exited before agent_end (${code ?? "unknown"})`)); + } + }); + try { + child.stdin.write(`${JSON.stringify({ id: `better-harness-${options.lane}`, type: "prompt", message: prompt })}\n`); + } catch (error) { + finish(Object.assign(error as Error, { code: "RPC_STDIN_FAILED" })); + } + }); +} + +export interface SpecialistRunOptions { + cwd: string; + model?: string; + timeoutMs?: number; + signal?: AbortSignal; + spawn?: RpcLaneOptions["spawn"]; +} + +/** Run exactly the three canonical lanes and terminate siblings on any failure. */ +export async function runPiSpecialists(plan: any, options: SpecialistRunOptions): Promise[]> { + const controller = new AbortController(); + const abortForwarder = () => controller.abort(options.signal?.reason ?? new Error("cancelled")); + if (options.signal?.aborted) controller.abort(options.signal.reason ?? new Error("cancelled")); + options.signal?.addEventListener("abort", abortForwarder, { once: true }); + const children = new Set(); + const childOptions = (child: any) => children.add(child); + const childExit = (child: any) => children.delete(child); + try { + const results = await Promise.all([ + "sessionEvidence", + "projectHarness", + "agentCustomize", + ].map((lane) => runPiRpcLane({ + lane, + inputHash: plan.lanes[lane].inputHash, + input: plan.lanes[lane].input, + cwd: options.cwd, + model: options.model, + timeoutMs: options.timeoutMs, + signal: controller.signal, + spawn: options.spawn, + onChild: childOptions, + onChildExit: childExit, + }))); + return results; + } catch (error) { + controller.abort(error); + for (const child of children) killChild(child); + throw error; + } finally { + options.signal?.removeEventListener("abort", abortForwarder); + for (const child of children) killChild(child); + } +} + +function currentSessionId(ctx: ExtensionCommandContext): string | undefined { + try { + const id = ctx.sessionManager.getSessionId(); + return typeof id === "string" && id.length > 0 ? id : undefined; + } catch { + return undefined; + } +} + +function leadMessage({ plan, verified, request, outputRoot, model }: { plan: any; verified: any; request: string; outputRoot: string; model?: string }): string { + return [ + "Better Harness three isolated lanes are verified. Perform exactly one lead reconciliation in this current Pi turn.", + "Do not rerun collectors, inspect raw sessions, read home directories, or delegate another lane.", + "Use the canonical Better Harness Skill and existing renderer. Write only findings.json, report.md, and report.html under the output root.", + "Preserve evidence boundaries and explicitly report configured/enabled/observed/verified/unsupported/unavailable states.", + `Output root: ${outputRoot}`, + `Review request: ${request || "Evaluate this workspace's AI delivery readiness."}`, + JSON.stringify({ + runId: plan.runId, + provider: plan.provider, + model, + depth: plan.depth, + diagnostics: verified.diagnostics, + lead: plan.lead, + specialistResults: verified.results, + }), + "After rendering, validate report.md, report.html, and findings.json with the existing report-quality/render validators.", + ].join("\n"); +} + +export async function executeBetterHarness(ctx: ExtensionCommandContext, requestInput = "", dependencies: any = {}) { + const parsed = parseReviewRequest(requestInput); + const sessionId = currentSessionId(ctx); + const model = modelPattern(ctx.model, ctx.thinkingLevel); + const doctor = await (dependencies.hostDoctor ?? hostDoctor)({ + platform: "pi", + provider: "pi", + workspace: ctx.cwd, + model, + "exclude-session-id": sessionId, + }); + if (doctor.status === "fail") { + throw extensionError("PI_HOST_DOCTOR_FAILED", doctor.checks.filter((check: any) => check.status === "fail").map((check: any) => check.detail).join("; ")); + } + const prepared = await (dependencies.prepareHostRun ?? prepareHostRun)({ + platform: "pi", + provider: "pi", + workspace: ctx.cwd, + depth: parsed.depth, + language: parsed.language, + model, + "exclude-session-id": sessionId, + }, dependencies); + if (prepared.status === "failed" && parsed.depth === "normal") { + throw extensionError("PI_PREPARE_FAILED", "normal Better Harness run could not collect all required lanes"); + } + const runSpecialists = dependencies.runPiSpecialists ?? runPiSpecialists; + const results = await runSpecialists(prepared.plan, { + cwd: ctx.cwd, + model, + signal: ctx.signal, + timeoutMs: dependencies.timeoutMs, + spawn: dependencies.spawn, + }); + const verified = (dependencies.verifyHostRun ?? verifyHostRun)(prepared.plan, results); + if (!verified.ok) { + throw extensionError("PI_VERIFY_FAILED", verified.diagnostics.errors.map((error: any) => error.code).join(", ")); + } + const outputRoot = path.join(ctx.cwd, ".pi", "better-harness"); + await access(path.dirname(outputRoot), fsConstants.W_OK).catch(async () => { + await access(ctx.cwd, fsConstants.W_OK).catch(() => { + throw extensionError("PI_OUTPUT_UNWRITABLE", `Pi report output parent is not writable: ${ctx.cwd}`); + }); + }); + const message = leadMessage({ plan: prepared.plan, verified, request: parsed.request, outputRoot, model }); + if (dependencies.sendMessage) { + await dependencies.sendMessage(message); + } else { + // Custom messages participate in the next lead turn without exposing the + // private lane manifest as a durable run file. + ctx.ui.notify("Better Harness: three isolated lanes verified; starting reconciliation.", "info"); + (dependencies.pi ?? null)?.sendMessage?.({ + customType: "better-harness.reconciliation", + content: message, + display: true, + details: { runId: prepared.plan.runId, laneCount: verified.results.length }, + }, { triggerTurn: true, deliverAs: "followUp" }); + } + return { runId: prepared.plan.runId, verified, outputRoot }; +} + +export default function activate(pi: ExtensionAPI): void { + pi.registerCommand(PI_COMMAND_NAME, { + description: "Run three isolated Better Harness evidence reviews and reconcile one report", + handler: async (args, ctx) => { + try { + await executeBetterHarness(ctx, args, { pi }); + } catch (error: any) { + ctx.ui.notify(`Better Harness failed: ${error.message}`, "error"); + } + }, + }); +} diff --git a/package-lock.json b/package-lock.json index 8ba4d10..b2eea21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,1970 @@ "better-harness": "scripts/better-harness.mjs" }, "engines": { - "node": ">=22.20.0 <25.0.0", + "node": ">=22.20.0 <26.0.0", "npm": ">=10.9.3 <12.0.0" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.83.0.tgz", + "integrity": "sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw==", + "hasShrinkwrap": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@earendil-works/pi-agent-core": "^0.83.0", + "@earendil-works/pi-ai": "^0.83.0", + "@earendil-works/pi-tui": "^0.83.0", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.3.7", + "undici": "8.5.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.83.0.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "@earendil-works/pi-ai": "^0.83.0", + "diff": "8.0.4", + "ignore": "7.0.5", + "typebox": "1.3.7", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.83.0.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "peer": true, + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "peer": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" } }, "node_modules/@vscode/tree-sitter-wasm": { diff --git a/package.json b/package.json index 687bc1b..fc34e97 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,9 @@ ], "type": "module", "pi": { + "extensions": ["./extensions/pi/better-harness.ts"], "skills": ["./skills"], - "prompts": ["./prompts"] + "prompts": [] }, "files": [ ".claude-plugin/", @@ -36,6 +37,7 @@ ".cursor-plugin/", ".github/plugin/", ".qoder-plugin/", + ".codebuddy-plugin/", "qwen-extension.json", "AGENTS.md", "CHANGELOG.md", @@ -55,6 +57,10 @@ "!docs/package.json", "!docs/package-lock.json", "hooks/", + "extensions/", + "agents/", + "avatars/", + "settings.json", "models/", "prompts/", "references/", @@ -69,6 +75,8 @@ }, "scripts": { "bundle:zip": "node scripts/npm-package/create-bundle.mjs", + "workbuddy:verify": "node scripts/packaging/workbuddy-plugin.mjs --verify --json", + "workbuddy:pack": "node scripts/packaging/workbuddy-plugin.mjs --out dist/workbuddy/better-harness.zip --json", "check": "npm test && npm run pack:verify", "pack:verify": "node scripts/npm-package/verify-pack.mjs", "publish:dry-run": "npm publish --dry-run --access public --registry https://registry.npmjs.org", @@ -84,11 +92,14 @@ "registry": "https://registry.npmjs.org/" }, "engines": { - "node": ">=22.20.0 <25.0.0", + "node": ">=22.20.0 <26.0.0", "npm": ">=10.9.3 <12.0.0" }, "dependencies": { "@vscode/tree-sitter-wasm": "0.3.1", "esbuild-wasm": "0.28.1" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*" } } diff --git a/scripts/better-harness-cli/cli.mjs b/scripts/better-harness-cli/cli.mjs index 84be755..796c80b 100644 --- a/scripts/better-harness-cli/cli.mjs +++ b/scripts/better-harness-cli/cli.mjs @@ -54,6 +54,7 @@ const ROOT_EXAMPLES = [ { audience: "workflow", text: "better-harness harness checkup --phase scan --provider qoder --workspace . --json" }, { audience: "advanced", text: "better-harness harness render --findings /findings.json --mode qoder-canvas --out .qoder/better-harness --target . --validate --json" }, { audience: "advanced", text: "better-harness harness report-quality --report /report.md" }, + { audience: "advanced", text: "better-harness harness host-doctor --platform pi --workspace . --json" }, { audience: "advanced", text: "better-harness harness validate-canvas --canvas /report.canvas.tsx" }, { audience: "maintainer", text: "better-harness core-change-watch project-profile --json" }, { audience: "maintainer", text: "better-harness core-change-watch change-drift --json" }, @@ -69,6 +70,7 @@ const GROUP_EXAMPLES = { { audience: "advanced", text: "better-harness harness workspace-topology --workspace . --json" }, { audience: "maintainer", text: "better-harness harness source --workspace . --source /report.source.json --language en" }, { audience: "advanced", text: "better-harness harness render --findings /findings.json --mode qoder-canvas --out .qoder/better-harness --target . --validate --json" }, + { audience: "advanced", text: "better-harness harness host-doctor --platform pi --workspace . --json" }, { audience: "advanced", text: "better-harness harness preview-canvas /report.canvas.tsx --open" }, { audience: "advanced", text: "better-harness harness report-quality --report /report.md" }, { audience: "advanced", text: "better-harness harness validate-canvas --canvas /report.canvas.tsx" }, diff --git a/scripts/better-harness-cli/registry.mjs b/scripts/better-harness-cli/registry.mjs index 5414bec..ddf55ac 100644 --- a/scripts/better-harness-cli/registry.mjs +++ b/scripts/better-harness-cli/registry.mjs @@ -169,6 +169,27 @@ const COMMANDS = [ summary: "Collect the three specialist lanes and lead evidence in one frozen-context bundle.", description: "Return versioned Session Evidence, Project Harness, and Agent Customize envelopes with explicit lane status and unchanged diagnostic commands.", }, + { + name: "host-doctor", + audience: "advanced", + script: "harness-analysis/host-runtime/host-doctor.mjs", + summary: "Check a Pi or WorkBuddy host before independent specialist execution.", + description: "Validate runtime resources, Node version, provider support, current-session binding, model boundary, and output route without reading raw sessions.", + }, + { + name: "prepare-run", + audience: "advanced", + script: "harness-analysis/host-runtime/prepare-run.mjs", + summary: "Freeze one private three-lane host run plan.", + description: "Collect one evidence bundle and write bounded lane envelopes plus input hashes to an explicitly supplied temporary path.", + }, + { + name: "verify-run", + audience: "advanced", + script: "harness-analysis/host-runtime/verify-run.mjs", + summary: "Verify three independent specialist results against a run plan.", + description: "Check lane count, context identities, input hashes, output shape, privacy fields, and quick/normal failure policy.", + }, { name: "workspace-topology", audience: "advanced", diff --git a/scripts/coding-agent-practices/asset-baseline.mjs b/scripts/coding-agent-practices/asset-baseline.mjs index a15277d..d01d26f 100644 --- a/scripts/coding-agent-practices/asset-baseline.mjs +++ b/scripts/coding-agent-practices/asset-baseline.mjs @@ -132,6 +132,9 @@ function ownerRoutes(inventory, workspace) { } function compactInventory(inventory, workspace) { + const unsupported = [...new Set((Array.isArray(inventory?.unsupported) ? inventory.unsupported : []) + .map((item) => text(item, 180)) + .filter(Boolean))].sort().slice(0, 16); const memoryCategories = (inventory?.memories?.categories ?? []) .map((category) => ({ category: text(category?.category ?? category?.name, 72), @@ -160,6 +163,7 @@ function compactInventory(inventory, workspace) { }, summary: inventorySummary, coverageRows, + unsupported, ownerRoutes: ownerRoutes(inventory, workspace), memories: { included: Boolean(inventory?.memories?.included), diff --git a/scripts/coding-agent-practices/inventory.mjs b/scripts/coding-agent-practices/inventory.mjs index 9333098..8ac6fe2 100644 --- a/scripts/coding-agent-practices/inventory.mjs +++ b/scripts/coding-agent-practices/inventory.mjs @@ -39,6 +39,16 @@ const CODEX_MEMORY_CONFIG_KEYS = new Set([ "memories.consolidation_model", ]); +const MAX_PUBLIC_UNSUPPORTED = 16; + +function publicUnsupported(value) { + return [...new Set((Array.isArray(value) ? value : []) + .map((item) => String(item ?? "").replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim().slice(0, 180)) + .filter(Boolean))] + .sort() + .slice(0, MAX_PUBLIC_UNSUPPORTED); +} + function normalizeBoolean(value) { return parseBooleanFlag(value); } @@ -729,6 +739,7 @@ export async function collectProviderInventory(options = {}) { sessionSourceHints, memories, customizeDiagnostics: inventory.diagnostics, + unsupported: publicUnsupported(inventory.unsupported), warnings: scope.includeMemories && !["qoder", "codex"].includes(platform) ? [`Memory inventory is only implemented for qoder and codex; skipped ${platform} memories.`] : [], @@ -863,6 +874,7 @@ export async function collectQoderInventory(options = {}) { sessionSourceHints: providerInventory.sessionSourceHints, memories, customizeDiagnostics: providerInventory.customizeDiagnostics, + unsupported: publicUnsupported(providerInventory.unsupported), warnings: [], }; } diff --git a/scripts/harness-analysis/evidence-bundle/contract.mjs b/scripts/harness-analysis/evidence-bundle/contract.mjs index 7049151..b8cb321 100644 --- a/scripts/harness-analysis/evidence-bundle/contract.mjs +++ b/scripts/harness-analysis/evidence-bundle/contract.mjs @@ -66,6 +66,21 @@ export function freezeEvidenceBundleContext(options = {}, now = new Date()) { code: "INVALID_EVIDENCE_WINDOW", }); } + if (provider === "workbuddy") { + const codebuddySessionId = options.codebuddySessionId + ?? options["codebuddy-session-id"] + ?? process.env.CODEBUDDY_SESSION_ID + ?? null; + const legacySessionId = options.workbuddySessionId + ?? options["workbuddy-session-id"] + ?? process.env.WORKBUDDY_SESSION_ID + ?? null; + if (codebuddySessionId && legacySessionId && codebuddySessionId !== legacySessionId) { + throw Object.assign(new Error("CODEBUDDY_SESSION_ID and WORKBUDDY_SESSION_ID conflict"), { + code: "WORKBUDDY_SESSION_ID_CONFLICT", + }); + } + } const topology = options.topology; const analysisScope = options.analysisScope; const requestedWorkspace = path.resolve(String(options.workspace)); diff --git a/scripts/harness-analysis/evidence-bundle/index.mjs b/scripts/harness-analysis/evidence-bundle/index.mjs index 2261700..738e0e9 100644 --- a/scripts/harness-analysis/evidence-bundle/index.mjs +++ b/scripts/harness-analysis/evidence-bundle/index.mjs @@ -16,6 +16,10 @@ import { collectSessionEvidence, collectSessionPopulation, } from "./session-evidence.mjs"; +import { + appendUnsupportedCapabilities, + sanitizeProviderCoverage, +} from "../../session-analysis/index.mjs"; async function capture(owner, operation) { try { @@ -107,6 +111,15 @@ function populationDiagnostics(population, sessionEvidence, lead) { }; } +function assetUnsupportedCapabilities(lane) { + const values = lane?.data?.envelopes?.inventory?.data?.unsupported; + return [...new Set((Array.isArray(values) ? values : []) + .map((value) => String(value ?? "").replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim().slice(0, 180)) + .filter(Boolean))] + .slice(0, 16) + .sort(); +} + export async function collectEvidenceBundle(options = {}, dependencies = {}) { const now = dependencies.now?.() ?? new Date(); const baseContext = freezeEvidenceBundleContext(options, now); @@ -160,6 +173,15 @@ export async function collectEvidenceBundle(options = {}, dependencies = {}) { const partialLanes = EVIDENCE_LANE_NAMES.filter((name) => lanes[name]?.status === "partial"); const leadFailed = !laneIsAvailable(lead); const topologyIncomplete = context.topology.status !== "complete"; + const baseProviderCoverage = sanitizeProviderCoverage( + sessionEvidence?.data?.providerCoverage + ?? lead?.data?.summaryFacts?.evidenceBoundary?.providerCoverage + ?? null, + ); + const unsupportedCapabilities = assetUnsupportedCapabilities(agentCustomize); + const providerCoverage = appendUnsupportedCapabilities(baseProviderCoverage, unsupportedCapabilities) + ?? baseProviderCoverage; + const schemaDiagnostics = providerCoverage?.schemaDiagnostics ?? null; const status = leadFailed || (context.depth === "normal" && (incompleteLanes.length > 0 || topologyIncomplete)) ? "failed" : incompleteLanes.length > 0 || topologyIncomplete @@ -184,6 +206,9 @@ export async function collectEvidenceBundle(options = {}, dependencies = {}) { topologyIncomplete, individualCommandsRemainDiagnostic: true, sessionPopulationBinding, + ...(providerCoverage ? { providerCoverage } : {}), + ...(unsupportedCapabilities.length > 0 ? { unsupportedCapabilities } : {}), + ...(schemaDiagnostics ? { schemaDiagnostics } : {}), }, }; } diff --git a/scripts/harness-analysis/evidence-bundle/session-evidence.mjs b/scripts/harness-analysis/evidence-bundle/session-evidence.mjs index c6fca4a..4ec253c 100644 --- a/scripts/harness-analysis/evidence-bundle/session-evidence.mjs +++ b/scripts/harness-analysis/evidence-bundle/session-evidence.mjs @@ -48,6 +48,7 @@ export async function collectSessionPopulation(context, options = {}, dependenci scope, sources: discovery.sources, warnings: discovery.warnings, + providerCoverage: discovery.providerCoverage ?? null, }, excludedSessionId: options["exclude-session-id"] ?? options.excludeSessionId, providerSessionId: typeof analyzer.currentSessionId === "function" ? analyzer.currentSessionId() : null, diff --git a/scripts/harness-analysis/host-runtime/cli.mjs b/scripts/harness-analysis/host-runtime/cli.mjs new file mode 100644 index 0000000..1c67a50 --- /dev/null +++ b/scripts/harness-analysis/host-runtime/cli.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { parseArgs, parseBooleanFlag } from "../../session-analysis/index.mjs"; +import { errorPayload, jsonDocument, successPayload } from "../../better-harness-cli/output.mjs"; +import { + hostDoctor, + prepareHostRun, + readRunPlan, + verifyHostRun, + writeRunPlan, +} from "./index.mjs"; + +export const HOST_RUNTIME_HELP = `Usage: better-harness harness [options] + +Host runtime contract for Pi and WorkBuddy orchestration. + +Commands: + host-doctor Check host, runtime, model/session binding, and output route. + prepare-run Collect one frozen evidence bundle and write a private run plan. + verify-run Verify three specialist results against a private run plan. + +Options: + --workspace Target workspace (default: current directory) + --platform pi or workbuddy (required for host runtime) + --depth Evidence depth (default: normal) + --output Private output file for prepare-run + --plan Private run plan for verify-run + --results Private specialist results for verify-run + --json Emit OpenCLI JSON +`; + +const ALLOWED = new Set([ + "workspace", "platform", "provider", "language", "depth", "since", "until", "evidence-limit", + "include-user-home", "include-memories", "output", "plan", "results", "exclude-session-id", + "pi-home", "workbuddy-home", "model", "codebuddy-session-id", + "workbuddy-session-id", "json", "help", "h", +]); + +function assertOptions(command, options) { + const unknown = Object.keys(options).filter((key) => key !== "_" && !ALLOWED.has(key)); + if (options._.length > 0 || unknown.length > 0) { + const token = options._[0] ?? `--${unknown[0]}`; + throw Object.assign(new Error(`unknown host runtime argument: ${token}`), { code: "UNKNOWN_ARGUMENT" }); + } + if (!command || !new Set(["host-doctor", "prepare-run", "verify-run"]).has(command)) { + throw Object.assign(new Error(`unknown host runtime command: ${command ?? ""}`), { code: "UNKNOWN_HOST_RUNTIME_COMMAND" }); + } + if (command !== "host-doctor" && !options.platform) { + throw Object.assign(new Error("--platform is required for host runtime runs"), { code: "MISSING_PLATFORM" }); + } +} + +function cleanOptions(options) { + const next = { ...options }; + delete next._; + for (const key of ["include-user-home", "include-memories"]) { + if (key in next) next[key] = parseBooleanFlag(next[key]); + } + return next; +} + +export async function main(argv = process.argv.slice(2), dependencies = {}) { + const { command, options } = parseArgs(argv); + if (options.help || options.h) { + process.stdout.write(HOST_RUNTIME_HELP); + return 0; + } + try { + assertOptions(command, options); + const normalized = cleanOptions(options); + let result; + if (command === "host-doctor") { + result = await hostDoctor(normalized); + } else if (command === "prepare-run") { + if (!normalized.output) throw Object.assign(new Error("--output is required for prepare-run"), { code: "MISSING_OUTPUT" }); + const envelope = await prepareHostRun(normalized, dependencies); + const outputPath = await writeRunPlan(envelope, normalized.output); + result = { + status: envelope.status, + runId: envelope.plan.runId, + provider: envelope.plan.provider, + depth: envelope.plan.depth, + outputPath, + lanes: envelope.plan.expected.laneNames, + }; + } else { + if (!normalized.plan || !normalized.results) { + throw Object.assign(new Error("--plan and --results are required for verify-run"), { code: "MISSING_VERIFY_INPUT" }); + } + const plan = await readRunPlan(normalized.plan); + const results = JSON.parse(await (await import("node:fs/promises")).readFile(path.resolve(normalized.results), "utf8")); + result = await verifyHostRun(plan, results); + } + const payload = result.status === "fail" + ? { ...successPayload(result), ok: false } + : successPayload(result); + process.stdout.write(jsonDocument(payload)); + return result.ok === false || result.status === "fail" ? 1 : 0; + } catch (error) { + const payload = errorPayload({ + code: error.code ?? "HOST_RUNTIME_FAILED", + message: error.message, + hint: "Use `better-harness harness host-doctor --platform pi|workbuddy --json` for bounded diagnostics.", + }); + process.stdout.write(jsonDocument(payload)); + return 1; + } +} + +const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isCli) process.exitCode = await main(); diff --git a/scripts/harness-analysis/host-runtime/contract.mjs b/scripts/harness-analysis/host-runtime/contract.mjs new file mode 100644 index 0000000..55b039c --- /dev/null +++ b/scripts/harness-analysis/host-runtime/contract.mjs @@ -0,0 +1,234 @@ +import { createHash, randomUUID } from "node:crypto"; + +export const HOST_RUNTIME_KIND = "better-harness.host-runtime"; +export const HOST_RUNTIME_SCHEMA_VERSION = 1; +export const SPECIALIST_LANES = Object.freeze([ + "sessionEvidence", + "projectHarness", + "agentCustomize", +]); + +const LANE_LABELS = Object.freeze({ + sessionEvidence: "Session Evidence", + projectHarness: "Project Harness", + agentCustomize: "Agent Customize", +}); + +const LANE_STATUSES = new Set(["available", "partial", "unavailable"]); +const RESULT_STATUSES = new Set(["completed", "partial", "unavailable", "failed"]); + +function canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize); + if (!value || typeof value !== "object") return value; + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])); +} + +export function stableJson(value) { + return JSON.stringify(canonicalize(value)); +} + +export function hashJson(value) { + return createHash("sha256").update(stableJson(value)).digest("hex"); +} + +function clone(value) { + return value === undefined ? undefined : JSON.parse(JSON.stringify(value)); +} + +function assertObject(value, message, code = "INVALID_HOST_RUNTIME_CONTRACT") { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw Object.assign(new Error(message), { code }); + } +} + +function forbiddenKeys(value, path = "", found = []) { + if (Array.isArray(value)) { + value.forEach((entry, index) => forbiddenKeys(entry, `${path}[${index}]`, found)); + return found; + } + if (!value || typeof value !== "object") return found; + for (const [key, child] of Object.entries(value)) { + if (/^(?:rawSession|rawSessions|rawTranscript|collectionReference|debugOutput|accessToken|refreshToken|apiKey|credential|secret)$/iu.test(key)) { + found.push(`${path}.${key}`); + } + forbiddenKeys(child, `${path}.${key}`, found); + } + return found; +} + +function laneInput(bundle, laneName) { + const lane = bundle?.lanes?.[laneName]; + if (!lane || !LANE_STATUSES.has(lane.status)) { + throw Object.assign(new Error(`invalid evidence lane: ${laneName}`), { + code: "INVALID_EVIDENCE_LANE", + }); + } + const data = lane.data ?? null; + const forbidden = forbiddenKeys(data, laneName); + if (forbidden.length > 0) { + throw Object.assign(new Error(`lane contains forbidden runtime fields: ${forbidden.join(", ")}`), { + code: "FORBIDDEN_LANE_FIELDS", + }); + } + return { + lane: laneName, + label: LANE_LABELS[laneName], + status: lane.status, + input: clone(data), + inputHash: hashJson(data), + }; +} + +export function createRunPlan(bundle, options = {}) { + assertObject(bundle, "evidence bundle is required"); + if (bundle.kind !== "better-harness.evidence-bundle") { + throw Object.assign(new Error("host runtime requires an evidence bundle"), { + code: "INVALID_EVIDENCE_BUNDLE_KIND", + }); + } + const context = clone(bundle.context ?? {}); + const lanes = Object.fromEntries(SPECIALIST_LANES.map((name) => [name, laneInput(bundle, name)])); + const diagnostics = clone(bundle.diagnostics ?? {}); + const plan = { + kind: HOST_RUNTIME_KIND, + schemaVersion: HOST_RUNTIME_SCHEMA_VERSION, + runId: options.runId ?? randomUUID(), + createdAt: new Date().toISOString(), + provider: context.provider, + depth: context.depth, + context, + lanes, + lead: clone(bundle.lead ?? null), + diagnostics, + expected: { + laneCount: SPECIALIST_LANES.length, + laneNames: [...SPECIALIST_LANES], + independentContexts: true, + readOnly: true, + }, + executionPolicy: { + normalBlocksOnPartial: context.depth === "normal", + quickPreservesGaps: context.depth === "quick", + maxSpecialists: SPECIALIST_LANES.length, + }, + }; + return Object.freeze(plan); +} + +export function validateRunPlan(plan) { + assertObject(plan, "host runtime run plan is required"); + if (plan.kind !== HOST_RUNTIME_KIND || plan.schemaVersion !== HOST_RUNTIME_SCHEMA_VERSION) { + throw Object.assign(new Error("unsupported host runtime run plan"), { + code: "UNSUPPORTED_HOST_RUNTIME_SCHEMA", + }); + } + if (!Array.isArray(plan.expected?.laneNames) + || plan.expected.laneCount !== SPECIALIST_LANES.length + || plan.expected.laneNames.join("\0") !== SPECIALIST_LANES.join("\0")) { + throw Object.assign(new Error("run plan does not require the three canonical lanes"), { + code: "INVALID_RUN_PLAN_LANES", + }); + } + for (const laneName of SPECIALIST_LANES) { + const lane = plan.lanes?.[laneName]; + if (!lane || lane.inputHash !== hashJson(lane.input)) { + throw Object.assign(new Error(`run plan input hash mismatch for ${laneName}`), { + code: "RUN_PLAN_INPUT_HASH_MISMATCH", + }); + } + } + return plan; +} + +function resultEntries(results) { + if (Array.isArray(results)) return results; + if (results && typeof results === "object") return Object.values(results); + return []; +} + +export function verifyRunResults(planInput, resultsInput) { + const plan = validateRunPlan(planInput); + const results = resultEntries(resultsInput); + const errors = []; + if (results.length !== SPECIALIST_LANES.length) { + errors.push({ code: "SPECIALIST_COUNT_MISMATCH", message: "exactly three specialist results are required" }); + } + const seenLanes = new Set(); + const seenContexts = new Set(); + const accepted = []; + for (const result of results) { + const resultErrors = []; + if (!result || typeof result !== "object") { + errors.push({ code: "INVALID_SPECIALIST_RESULT", message: "specialist result must be an object" }); + continue; + } + const lane = String(result.lane ?? ""); + const contextId = String(result.contextId ?? ""); + if (!SPECIALIST_LANES.includes(lane)) { + errors.push({ code: "UNKNOWN_SPECIALIST_LANE", message: `unknown specialist lane: ${lane}` }); + continue; + } + if (seenLanes.has(lane)) resultErrors.push({ code: "DUPLICATE_SPECIALIST_LANE", message: lane }); + seenLanes.add(lane); + if (!contextId) resultErrors.push({ code: "MISSING_SPECIALIST_CONTEXT", message: lane }); + if (contextId && seenContexts.has(contextId)) resultErrors.push({ code: "DUPLICATE_SPECIALIST_CONTEXT", message: contextId }); + if (contextId) seenContexts.add(contextId); + if (result.inputHash !== plan.lanes[lane].inputHash) { + resultErrors.push({ code: "SPECIALIST_INPUT_HASH_MISMATCH", message: lane }); + } + if (!RESULT_STATUSES.has(result.status)) { + resultErrors.push({ code: "INVALID_SPECIALIST_STATUS", message: lane }); + } + const forbidden = forbiddenKeys(result.output ?? null, `${lane}.output`); + if (forbidden.length > 0) { + resultErrors.push({ code: "FORBIDDEN_SPECIALIST_FIELDS", message: forbidden.join(", ") }); + } + if (!result.output || typeof result.output !== "object" || Array.isArray(result.output)) { + resultErrors.push({ code: "INVALID_SPECIALIST_OUTPUT", message: lane }); + } + if (result.resultHash !== undefined && result.resultHash !== hashJson(result.output)) { + resultErrors.push({ code: "SPECIALIST_RESULT_HASH_MISMATCH", message: lane }); + } + errors.push(...resultErrors); + if (resultErrors.length === 0) { + accepted.push({ + lane, + contextId, + status: result.status, + inputHash: result.inputHash, + output: clone(result.output), + resultHash: result.resultHash ?? hashJson(result.output), + }); + } + } + for (const lane of SPECIALIST_LANES) { + if (!seenLanes.has(lane)) errors.push({ code: "MISSING_SPECIALIST_LANE", message: lane }); + } + const incomplete = accepted.filter((result) => result.status !== "completed").map((result) => result.lane); + if (plan.executionPolicy.normalBlocksOnPartial && incomplete.length > 0) { + errors.push({ code: "NORMAL_RUN_HAS_INCOMPLETE_SPECIALIST", message: incomplete.join(", ") }); + } + const ok = errors.length === 0 || ( + plan.depth === "quick" + && errors.every((error) => ["NORMAL_RUN_HAS_INCOMPLETE_SPECIALIST"].includes(error.code)) + && accepted.length === SPECIALIST_LANES.length + ); + return { + ok, + kind: HOST_RUNTIME_KIND, + schemaVersion: HOST_RUNTIME_SCHEMA_VERSION, + runId: plan.runId, + provider: plan.provider, + depth: plan.depth, + results: accepted, + diagnostics: { + expectedContextCount: SPECIALIST_LANES.length, + observedContextCount: seenContexts.size, + incompleteLanes: incomplete, + errors, + confidence: incomplete.length > 0 ? "low" : "normal", + }, + }; +} + +export { LANE_LABELS }; diff --git a/scripts/harness-analysis/host-runtime/host-doctor.mjs b/scripts/harness-analysis/host-runtime/host-doctor.mjs new file mode 100644 index 0000000..7ae7795 --- /dev/null +++ b/scripts/harness-analysis/host-runtime/host-doctor.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from "./cli.mjs"; + +process.exitCode = await main(["host-doctor", ...process.argv.slice(2)]); diff --git a/scripts/harness-analysis/host-runtime/index.mjs b/scripts/harness-analysis/host-runtime/index.mjs new file mode 100644 index 0000000..3ede5b5 --- /dev/null +++ b/scripts/harness-analysis/host-runtime/index.mjs @@ -0,0 +1,165 @@ +import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { collectEvidenceBundle } from "../evidence-bundle/index.mjs"; +import { + createRunPlan, + HOST_RUNTIME_KIND, + HOST_RUNTIME_SCHEMA_VERSION, + SPECIALIST_LANES, + validateRunPlan, + verifyRunResults, +} from "./contract.mjs"; + +const RUNTIME_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const SUPPORTED_PROVIDERS = new Set(["pi", "workbuddy"]); + +function check(name, status, detail, extra = {}) { + return { name, status, detail, ...extra }; +} + +function nodeVersionCheck() { + const match = process.versions.node.match(/^(\d+)\.(\d+)\.(\d+)/u); + const version = match ? [Number(match[1]), Number(match[2]), Number(match[3])] : [0, 0, 0]; + const supported = (version[0] > 22 || (version[0] === 22 && version[1] >= 20)) && version[0] < 26; + return check("node", supported ? "pass" : "fail", `Node ${process.versions.node}; supported range is >=22.20.0 <26.0.0`, { version: process.versions.node }); +} + +function resourceCheck(provider) { + const required = [ + "scripts/better-harness.mjs", + "models/agent-work-loop.md", + "templates/reporting", + ]; + if (provider === "pi") required.push("extensions/pi/better-harness.ts", "skills/better-harness/SKILL.md"); + if (provider === "workbuddy") required.push( + ".codebuddy-plugin/plugin.json", + ".codebuddy-plugin/marketplace.json", + "settings.json", + "agents/better-harness-review-director.md", + "agents/session-evidence-reviewer.md", + "agents/project-harness-reviewer.md", + "agents/agent-customize-reviewer.md", + ); + const missing = required.filter((entry) => { + return !existsSync(path.join(RUNTIME_ROOT, entry)); + }); + return check("runtime-resources", missing.length === 0 ? "pass" : "fail", missing.length === 0 ? "canonical runtime resources are present" : `missing: ${missing.join(", ")}`, { missing }); +} + +function modelCheck(provider, options) { + if (options.modelStatus === "unavailable" || options.modelAvailable === false) { + return check("model", "fail", `${provider} model is unavailable`); + } + if (options.model) return check("model", "pass", `${provider} model identity is bound`); + return check("model", "warn", provider === "workbuddy" + ? "WorkBuddy model identity is host-managed and was not supplied" + : `${provider} model identity was not supplied by the host`); +} + +function providerCheck(provider) { + const normalized = String(provider ?? "").toLowerCase(); + return check("provider", SUPPORTED_PROVIDERS.has(normalized) ? "pass" : "fail", SUPPORTED_PROVIDERS.has(normalized) ? normalized : `unsupported provider: ${normalized}`, { provider: normalized }); +} + +function sessionIdentityCheck(provider, options) { + if (provider === "workbuddy") { + const codebuddy = options.codebuddySessionId ?? options["codebuddy-session-id"] ?? process.env.CODEBUDDY_SESSION_ID; + const legacy = options.workbuddySessionId ?? options["workbuddy-session-id"] ?? process.env.WORKBUDDY_SESSION_ID; + if (codebuddy && legacy && codebuddy !== legacy) { + return check("current-session", "fail", "CODEBUDDY_SESSION_ID and WORKBUDDY_SESSION_ID conflict"); + } + return check("current-session", codebuddy || legacy ? "pass" : "warn", codebuddy || legacy ? "current session exclusion is bound" : "current session identity was not supplied"); + } + const identity = options.excludeSessionId ?? options["exclude-session-id"] ?? process.env.PI_SESSION_ID; + return check("current-session", identity ? "pass" : "warn", identity ? "current session exclusion is bound" : "current session identity was not supplied"); +} + +async function outputCheck(provider, workspace) { + const outputRoot = path.resolve(workspace, `.${provider}`, "better-harness"); + try { + const outputParent = path.dirname(outputRoot); + const candidate = existsSync(outputParent) ? outputParent : workspace; + await access(candidate, 2); + return check("output-root", "pass", `output route is writable; renderer may create .${provider}/better-harness`, { outputRoot }); + } catch (error) { + return check("output-root", "fail", `output parent is not accessible: ${error.code ?? error.message}`, { outputRoot }); + } +} + +export async function hostDoctor(options = {}) { + const provider = String(options.platform ?? options.provider ?? "").toLowerCase(); + const workspace = path.resolve(options.workspace ?? process.cwd()); + const checks = [nodeVersionCheck(), providerCheck(provider), resourceCheck(provider), sessionIdentityCheck(provider, options), modelCheck(provider, options)]; + try { + const workspaceStat = await stat(workspace); + checks.push(check("workspace", workspaceStat.isDirectory() ? "pass" : "fail", workspaceStat.isDirectory() ? workspace : "workspace is not a directory")); + } catch (error) { + checks.push(check("workspace", "fail", `workspace is unavailable: ${error.code ?? error.message}`)); + } + checks.push(await outputCheck(provider || "unknown", workspace)); + const status = checks.some((entry) => entry.status === "fail") ? "fail" : checks.some((entry) => entry.status === "warn") ? "warn" : "pass"; + return { + kind: `${HOST_RUNTIME_KIND}.doctor`, + schemaVersion: HOST_RUNTIME_SCHEMA_VERSION, + status, + provider, + workspace, + checks, + }; +} + +export async function prepareHostRun(options = {}, dependencies = {}) { + const collect = dependencies.collectEvidenceBundle ?? collectEvidenceBundle; + const bundle = await collect({ + ...options, + provider: options.provider ?? options.platform, + platform: options.platform ?? options.provider, + "exclude-session-id": options["exclude-session-id"] ?? options.excludeSessionId ?? process.env.PI_SESSION_ID ?? process.env.CODEBUDDY_SESSION_ID ?? process.env.WORKBUDDY_SESSION_ID, + }, dependencies); + const plan = createRunPlan(bundle, options); + return { + kind: HOST_RUNTIME_KIND, + schemaVersion: HOST_RUNTIME_SCHEMA_VERSION, + status: bundle.status, + plan, + }; +} + +export async function writeRunPlan(planEnvelope, outputPath) { + const plan = validateRunPlan(planEnvelope.plan ?? planEnvelope); + const destination = path.resolve(outputPath); + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, `${JSON.stringify({ ...planEnvelope, plan }, null, 2)}\n`, { flag: "wx" }); + return destination; +} + +export async function readRunPlan(inputPath) { + const value = JSON.parse(await readFile(path.resolve(inputPath), "utf8")); + return validateRunPlan(value.plan ?? value); +} + +export async function verifyHostRun(planInput, resultsInput) { + return verifyRunResults(planInput, resultsInput); +} + +export function specialistPrompt(plan, laneName) { + const lane = plan.lanes?.[laneName]; + if (!lane || !SPECIALIST_LANES.includes(laneName)) throw new Error(`unknown specialist lane: ${laneName}`); + return { + lane: laneName, + inputHash: lane.inputHash, + prompt: [ + `You are the read-only Better Harness specialist for ${lane.label}.`, + "Do not inspect the repository, user home, raw sessions, or another lane.", + "Return one JSON object with status completed|partial|unavailable and a bounded output object.", + `Input hash: ${lane.inputHash}`, + "Evidence envelope:", + JSON.stringify(lane.input), + ].join("\n"), + }; +} + +export { HOST_RUNTIME_KIND, HOST_RUNTIME_SCHEMA_VERSION, SPECIALIST_LANES } from "./contract.mjs"; diff --git a/scripts/harness-analysis/host-runtime/prepare-run.mjs b/scripts/harness-analysis/host-runtime/prepare-run.mjs new file mode 100644 index 0000000..6e64264 --- /dev/null +++ b/scripts/harness-analysis/host-runtime/prepare-run.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from "./cli.mjs"; + +process.exitCode = await main(["prepare-run", ...process.argv.slice(2)]); diff --git a/scripts/harness-analysis/host-runtime/verify-run.mjs b/scripts/harness-analysis/host-runtime/verify-run.mjs new file mode 100644 index 0000000..d348604 --- /dev/null +++ b/scripts/harness-analysis/host-runtime/verify-run.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from "./cli.mjs"; + +process.exitCode = await main(["verify-run", ...process.argv.slice(2)]); diff --git a/scripts/harness-analysis/renderers/html.mjs b/scripts/harness-analysis/renderers/html.mjs index 482a3ee..bf68a8c 100644 --- a/scripts/harness-analysis/renderers/html.mjs +++ b/scripts/harness-analysis/renderers/html.mjs @@ -327,8 +327,10 @@ function renderEvidence(summary, language) { [copy(language, "Learning state", "学习状态"), learning.state ?? "—"], ]; const gaps = textLines(boundary.sourceGaps); + const unsupportedCapabilities = textLines(boundary.providerCoverage?.unsupportedCapabilities); return `
${facts.map(([label, value]) => `
${renderVisibleText(label, language)}${renderVisibleText(value, language)}
`).join("")}
${gaps.length ? `
${renderVisibleText(copy(language, "Source gaps", "来源缺口"), language)}
    ${gaps.map((gap) => `
  • ${renderVisibleText(gap, language)}
  • `).join("")}
` : ""} + ${unsupportedCapabilities.length ? `
${renderVisibleText(copy(language, "Unsupported capabilities", "未支持能力"), language)}
    ${unsupportedCapabilities.map((item) => `
  • ${renderVisibleText(item, language)}
  • `).join("")}
` : ""}

${renderVisibleText(copy(language, "Activity totals describe volume, not quality or savings. Fluency conclusions come from the reviewed task sample and remain bounded by the retained evidence.", "活动总量只描述用量,不直接代表质量、效率或节省。流畅度结论来自已复核任务样本,并受已保留证据边界约束。"), language)}

`; diff --git a/scripts/harness-analysis/renderers/markdown.mjs b/scripts/harness-analysis/renderers/markdown.mjs index 0951c31..47e2ba1 100644 --- a/scripts/harness-analysis/renderers/markdown.mjs +++ b/scripts/harness-analysis/renderers/markdown.mjs @@ -296,6 +296,7 @@ ${smallCheckRows.join("\n")} - Session selection: ${value(boundary.manifest?.selection?.strategy)}; ${value(boundary.manifest?.selection?.analyzedCount, 0)} sessions analyzed of ${value(boundary.manifest?.selection?.eligibleCount, 0)} eligible sessions; ${value(boundary.manifest?.selection?.confidence)} confidence - Delivery grades observed: ${value(boundary.deliveryEvidenceLevels)} - Source gaps: ${value(boundary.sourceGaps)} +- Provider coverage: ${value(boundary.providerCoverage?.status, "not observed")}; unsupported capabilities: ${value(boundary.providerCoverage?.unsupportedCapabilities, "none")} - Learning comparison: ${readerLearningState(learning.state)}; ${value(learning.interventions?.length, 0)} declared intervention(s) `; } diff --git a/scripts/harness-analysis/report-source/source.mjs b/scripts/harness-analysis/report-source/source.mjs index b4ec26a..291d08a 100644 --- a/scripts/harness-analysis/report-source/source.mjs +++ b/scripts/harness-analysis/report-source/source.mjs @@ -1,4 +1,5 @@ import { validateSemanticFacets } from "../../session-analysis/index.mjs"; +import { sanitizeProviderCoverage } from "../../session-analysis/index.mjs"; import { validateCheckupReportEvidence } from "../../coding-agent-practices/checkup/contract.mjs"; import { validateInterventionLedger } from "../intervention-ledger.mjs"; import { @@ -107,6 +108,10 @@ const EPISODE_PERMISSION_SUMMARY_FIELDS = new Set([ ]); const READER_OVERVIEW_MAX_LENGTH = Object.freeze({ en: 160, "zh-CN": 80 }); const GENERIC_OVERVIEW_PREFIX_RE = /^(?:The project has a usable foundation|The first improvement is|项目已有可用基础|当前首要改进是)/iu; +const PROVIDER_COVERAGE_FIELDS = new Set([ + "schemaVersion", "provider", "status", "configured", "enabled", "observed", "verified", + "unsupported", "unsupportedCapabilities", "unavailable", "sourceCoverage", "schemaDiagnostics", +]); function clone(value) { return value === undefined ? undefined : JSON.parse(JSON.stringify(value)); @@ -225,6 +230,16 @@ function permissionSummaryErrors(source) { return errors; } +function providerCoverageErrors(value, prefix) { + if (value === undefined) return []; + if (!isObject(value)) return [`${prefix} must be an object`]; + const normalized = sanitizeProviderCoverage(value); + if (!normalized) return [`${prefix} must be a reader-safe provider coverage object`]; + return Object.keys(value) + .filter((field) => !PROVIDER_COVERAGE_FIELDS.has(field)) + .map((field) => `${prefix} has unsupported field: ${field}`); +} + function isSafeRelativeScope(value) { const candidate = String(value ?? "").trim().replaceAll("\\", "/"); return candidate.length > 0 @@ -1054,6 +1069,8 @@ export function validateHarnessReportSource(source) { } if (!isObject(source.manifest) || source.manifest.kind !== "session-observation-manifest") { errors.push("report source manifest must be a session-observation-manifest"); + } else { + errors.push(...providerCoverageErrors(source.manifest.providerCoverage, "report source manifest.providerCoverage")); } for (const key of ["taskEpisodes", "deliveryEvidence", "semanticFacets", "interventionLedger", "evidenceRefs", "assessmentDecisions"]) { if (!Array.isArray(source[key])) errors.push(`report source ${key} must be an array`); diff --git a/scripts/harness-analysis/task-loop-report.mjs b/scripts/harness-analysis/task-loop-report.mjs index 48cbc4c..f03539a 100644 --- a/scripts/harness-analysis/task-loop-report.mjs +++ b/scripts/harness-analysis/task-loop-report.mjs @@ -30,6 +30,7 @@ import { validateHarnessReportSource, } from "./report-source.mjs"; import { projectSemanticFacets, validateSemanticFacets } from "../session-analysis/index.mjs"; +import { sanitizeProviderCoverage } from "../session-analysis/index.mjs"; import { restoreProjectedInterventionLedger, summarizeLearningCapture } from "./intervention-ledger.mjs"; import { findingTargetErrors } from "../workspace-topology/index.mjs"; @@ -1938,6 +1939,7 @@ function reportOverview(source, strengths, findings, locale) { function evidenceBoundary(source) { const manifest = source?.manifest ?? {}; + const providerCoverage = sanitizeProviderCoverage(manifest.providerCoverage); return { manifest: { schemaVersion: manifest.schemaVersion ?? null, @@ -1954,6 +1956,20 @@ function evidenceBoundary(source) { episodeCoverage: episodeCoverage(source), deliveryEvidenceLevels: unique(rows(source?.deliveryEvidence).map((row) => row?.level)).sort(), sourceGaps: rows(manifest?.warnings).map((warning) => String(warning?.code ?? warning)).filter(Boolean), + ...(providerCoverage ? { + providerCoverage, + coverageStates: { + configured: providerCoverage.configured, + enabled: providerCoverage.enabled, + observed: providerCoverage.observed, + verified: providerCoverage.verified, + unsupported: providerCoverage.unsupported, + ...(providerCoverage.unsupportedCapabilities?.length > 0 + ? { unsupportedCapabilities: providerCoverage.unsupportedCapabilities } + : {}), + unavailable: providerCoverage.unavailable, + }, + } : {}), }; } @@ -3740,7 +3756,7 @@ export function validateTaskLoopFindings(data) { if (!isObject(summary.evidenceBoundary)) errors.push("findings.json summary.evidenceBoundary must be an object"); else { const boundary = summary.evidenceBoundary; - errors.push(...unsupportedFields(boundary, ["manifest", "episodeCoverage", "deliveryEvidenceLevels", "sourceGaps"], "summary.evidenceBoundary")); + errors.push(...unsupportedFields(boundary, ["manifest", "episodeCoverage", "deliveryEvidenceLevels", "sourceGaps", "providerCoverage", "coverageStates"], "summary.evidenceBoundary")); if (!isObject(boundary.manifest)) errors.push("summary.evidenceBoundary.manifest must be an object"); else { errors.push(...unsupportedFields(boundary.manifest, ["schemaVersion", "sourceFingerprint", "adapterVersion", "platform", "selection"], "summary.evidenceBoundary.manifest")); @@ -3775,6 +3791,45 @@ export function validateTaskLoopFindings(data) { if (!Array.isArray(boundary.sourceGaps) || boundary.sourceGaps.some((value) => typeof value !== "string" || value.trim() === "")) { errors.push("summary.evidenceBoundary.sourceGaps must be an array of non-empty strings"); } + if (boundary.providerCoverage !== undefined) { + const providerCoverage = sanitizeProviderCoverage(boundary.providerCoverage); + if (!providerCoverage) { + errors.push("summary.evidenceBoundary.providerCoverage must be a reader-safe provider coverage object"); + } else { + errors.push(...unsupportedFields( + providerCoverage, + ["schemaVersion", "provider", "status", "configured", "enabled", "observed", "verified", "unsupported", "unsupportedCapabilities", "unavailable", "sourceCoverage", "schemaDiagnostics"], + "summary.evidenceBoundary.providerCoverage", + )); + if (!Array.isArray(providerCoverage.unsupported) || !Array.isArray(providerCoverage.unavailable)) { + errors.push("summary.evidenceBoundary.providerCoverage unsupported/unavailable must be arrays"); + } + if (providerCoverage.unsupportedCapabilities !== undefined + && (!Array.isArray(providerCoverage.unsupportedCapabilities) + || providerCoverage.unsupportedCapabilities.some((value) => typeof value !== "string" || !value.trim()))) { + errors.push("summary.evidenceBoundary.providerCoverage unsupportedCapabilities must be an array of non-empty strings"); + } + } + } + if (boundary.coverageStates !== undefined) { + const states = boundary.coverageStates; + if (!isObject(states)) { + errors.push("summary.evidenceBoundary.coverageStates must be an object"); + } else { + errors.push(...unsupportedFields(states, ["configured", "enabled", "observed", "verified", "unsupported", "unsupportedCapabilities", "unavailable"], "summary.evidenceBoundary.coverageStates")); + for (const field of ["configured", "enabled", "observed", "verified"]) { + if (typeof states[field] !== "boolean") errors.push(`summary.evidenceBoundary.coverageStates.${field} must be boolean`); + } + for (const field of ["unsupported", "unavailable"]) { + if (!Array.isArray(states[field])) errors.push(`summary.evidenceBoundary.coverageStates.${field} must be an array`); + } + if (states.unsupportedCapabilities !== undefined + && (!Array.isArray(states.unsupportedCapabilities) + || states.unsupportedCapabilities.some((value) => typeof value !== "string" || !value.trim()))) { + errors.push("summary.evidenceBoundary.coverageStates.unsupportedCapabilities must be an array of non-empty strings"); + } + } + } } errors.push(...readerFindingEligibilityErrors(data)); if (!isObject(summary.semanticFacets)) { diff --git a/scripts/harness-analysis/task-loop-source.mjs b/scripts/harness-analysis/task-loop-source.mjs index 3d5e963..b378200 100644 --- a/scripts/harness-analysis/task-loop-source.mjs +++ b/scripts/harness-analysis/task-loop-source.mjs @@ -35,6 +35,7 @@ import { sessionPopulationDiscovery, stableFingerprint, } from "../session-analysis/index.mjs"; +import { appendUnsupportedCapabilities } from "../session-analysis/index.mjs"; import { createHarnessReportSource, LEARNING_CAPTURE_FINDING_POLICY, @@ -717,6 +718,8 @@ export function buildTaskLoopSourceCandidate({ priorLearningCaptureEvidenceRef = null, includeUsage = false, memoryInventory, + providerCoverage = null, + unsupportedCapabilities = [], } = {}) { const readerLocale = normalizeReaderLocale(locale); const episodeAnalysis = buildTaskEpisodes( @@ -732,6 +735,8 @@ export function buildTaskLoopSourceCandidate({ : null; const discardedEpisodeCount = episodeAnalysis.episodes.length - taskEpisodes.length; const permissionSummary = sourcePermissionCoverageSummary(episodeAnalysis.permissionSummary); + const manifestProviderCoverage = appendUnsupportedCapabilities(providerCoverage, unsupportedCapabilities) + ?? providerCoverage; const manifest = buildObservationManifest({ scope, sources, @@ -742,6 +747,7 @@ export function buildTaskLoopSourceCandidate({ selectionStrata: selection.strata ?? [], selectionPlan: selection.plan ?? null, adapterVersion, + providerCoverage: manifestProviderCoverage, }); const semanticFacets = insightSemanticFacets(insights, readerLocale, { includeUsage }); @@ -1243,6 +1249,8 @@ export async function createTaskLoopSourceFromSessions(options = {}) { priorLearningCaptureEvidenceRef: priorLearningCaptureState.evidenceRef, includeUsage, memoryInventory: practiceInventory?.memories ?? { included: false, categories: [] }, + providerCoverage: discovery.providerCoverage ?? insightResult.providerCoverage ?? null, + unsupportedCapabilities: practiceInventory?.unsupported ?? [], }); assertStandardUsageComplete(source, selected, includeUsage); if (!population) return { source, selection: selected }; diff --git a/scripts/npm-package/verify-pack.mjs b/scripts/npm-package/verify-pack.mjs index 69adf83..8555815 100644 --- a/scripts/npm-package/verify-pack.mjs +++ b/scripts/npm-package/verify-pack.mjs @@ -46,7 +46,11 @@ function readJson(relativePath) { } function verifyReleaseVersionAlignment() { - const packageVersion = readJson("package.json").version; + const packageJson = readJson("package.json"); + const packageVersion = packageJson.version; + if (packageJson.peerDependencies?.["@earendil-works/pi-coding-agent"] !== "*") { + fail("package.json must declare @earendil-works/pi-coding-agent as a wildcard peer dependency"); + } const versions = [ [".qoder-plugin/plugin.json", readJson(".qoder-plugin/plugin.json").version], [".claude-plugin/plugin.json", readJson(".claude-plugin/plugin.json").version], @@ -142,7 +146,20 @@ const required = [ "package/.github/plugin/plugin.json", "package/.github/plugin/marketplace.json", "package/.qoder-plugin/plugin.json", + "package/.codebuddy-plugin/plugin.json", + "package/.codebuddy-plugin/marketplace.json", "package/qwen-extension.json", + "package/extensions/pi/better-harness.ts", + "package/settings.json", + "package/agents/better-harness-review-director.md", + "package/agents/session-evidence-reviewer.md", + "package/agents/project-harness-reviewer.md", + "package/agents/agent-customize-reviewer.md", + "package/avatars/team.svg", + "package/avatars/better-harness-review-director.svg", + "package/avatars/session-evidence-reviewer.svg", + "package/avatars/project-harness-reviewer.svg", + "package/avatars/agent-customize-reviewer.svg", "package/prompts/better-harness.md", "package/case-studies/factory/model/factory-readiness.md", "package/docs/glossary.md", @@ -173,6 +190,12 @@ const required = [ "package/scripts/harness-analysis/evidence-bundle/session-evidence.mjs", "package/scripts/harness-analysis/evidence-bundle/project-harness.mjs", "package/scripts/harness-analysis/evidence-bundle/agent-customize.mjs", + "package/scripts/harness-analysis/host-runtime/cli.mjs", + "package/scripts/harness-analysis/host-runtime/contract.mjs", + "package/scripts/harness-analysis/host-runtime/index.mjs", + "package/scripts/harness-analysis/host-runtime/host-doctor.mjs", + "package/scripts/harness-analysis/host-runtime/prepare-run.mjs", + "package/scripts/harness-analysis/host-runtime/verify-run.mjs", "package/scripts/harness-analysis/report-source/apply-review.mjs", "package/scripts/harness-analysis/report-source/episode-review.mjs", "package/scripts/harness-analysis/report-source/index.mjs", @@ -187,6 +210,7 @@ const required = [ "package/scripts/session-analysis/episode-facts.mjs", "package/scripts/session-analysis/result-facts.mjs", "package/scripts/session-analysis/session-core-facts.mjs", + "package/scripts/session-analysis/provider-coverage.mjs", "package/references/loop-engineering/loop-blueprint.md", "package/skills/better-harness/SKILL.md", "package/skills/better-harness/references/agent-customize.md", diff --git a/scripts/packaging/workbuddy-plugin.mjs b/scripts/packaging/workbuddy-plugin.mjs new file mode 100644 index 0000000..b50fd22 --- /dev/null +++ b/scripts/packaging/workbuddy-plugin.mjs @@ -0,0 +1,332 @@ +#!/usr/bin/env node + +import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; +import { deflateRawSync } from "node:zlib"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export const WORKBUDDY_ARTIFACT_KIND = "better-harness-workbuddy-plugin-artifact"; +export const WORKBUDDY_ARTIFACT_SCHEMA_VERSION = 1; +export const WORKBUDDY_ARTIFACT_MARKER = ".workbuddy-plugin-artifact.json"; +export const WORKBUDDY_REQUIRED_PATHS = Object.freeze([ + ".codebuddy-plugin/plugin.json", + ".codebuddy-plugin/marketplace.json", + "settings.json", + "skills/better-harness/SKILL.md", + "scripts/better-harness.mjs", + "scripts/harness-analysis/host-runtime/contract.mjs", + "scripts/harness-analysis/host-runtime/index.mjs", + "agents/better-harness-review-director.md", + "agents/session-evidence-reviewer.md", + "agents/project-harness-reviewer.md", + "agents/agent-customize-reviewer.md", + "avatars/team.svg", + "avatars/better-harness-review-director.svg", + "avatars/session-evidence-reviewer.svg", + "avatars/project-harness-reviewer.svg", + "avatars/agent-customize-reviewer.svg", +]); + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const COPY_ROOTS = Object.freeze([ + ".codebuddy-plugin", + "agents", + "avatars", + "case-studies", + "docs", + "hooks", + "models", + "references", + "scripts", + "skills", + "templates", +]); +const COPY_FILES = Object.freeze([ + "AGENTS.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "README.zh-CN.md", + "package.json", + "settings.json", +]); +const EXCLUDED_PREFIXES = Object.freeze([ + ".git", + ".workbuddy", + ".pi", + ".qoder", + ".codex", + ".claude", + ".cursor", + "node_modules", + "test", + "dist", + "scripts/packaging", + "docs/docs", + "docs/i18n", + "docs/src", + "docs/static", + "docs/build", + "docs/node_modules", +]); +const PRIVATE_KEY_PATTERN = /(?:access[_-]?token|refresh[_-]?token|botToken|api[_-]?key|password|private[_-]?key|rawTranscript|rawSession|rawPrompt)/iu; +const ABSOLUTE_HOME_PATTERN = /(?:^|["'\s])(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)/u; + +function posix(value) { + return value.split(path.sep).join("/"); +} + +function excluded(relativePath) { + const normalized = posix(relativePath); + return EXCLUDED_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`)); +} + +async function exists(filePath) { + return Boolean(await lstat(filePath).catch(() => null)); +} + +async function ensureRegular(root, relativePath) { + const filePath = path.join(root, relativePath); + const stats = await lstat(filePath).catch(() => null); + if (!stats?.isFile() || stats.isSymbolicLink()) { + throw new Error(`WorkBuddy artifact is missing a regular file: ${relativePath}`); + } + return filePath; +} + +async function readJson(root, relativePath) { + return JSON.parse(await readFile(await ensureRegular(root, relativePath), "utf8")); +} + +async function scanTree(root, { strict = false } = {}) { + const files = []; + async function visit(current, relative = "") { + const entries = await readdir(current, { withFileTypes: true }); + for (const entry of entries) { + const childRelative = relative ? `${relative}/${entry.name}` : entry.name; + const childPath = path.join(current, entry.name); + if (entry.isSymbolicLink()) throw new Error(`WorkBuddy artifact contains a symlink: ${childRelative}`); + if (excluded(childRelative)) { + if (strict) throw new Error(`WorkBuddy artifact contains excluded private path: ${childRelative}`); + continue; + } + if (entry.isDirectory()) { + await visit(childPath, childRelative); + } else if (entry.isFile()) { + files.push(childRelative); + const inspectContent = strict + && !childRelative.startsWith("scripts/") + && !childRelative.startsWith("docs/") + && !childRelative.startsWith("skills/") + && !childRelative.startsWith("references/") + && !childRelative.startsWith("models/"); + if (inspectContent) { + if (PRIVATE_KEY_PATTERN.test(entry.name)) throw new Error(`WorkBuddy artifact contains private-looking filename: ${childRelative}`); + const content = await readFile(childPath, "utf8").catch(() => ""); + if (PRIVATE_KEY_PATTERN.test(content)) throw new Error(`WorkBuddy artifact contains private field: ${childRelative}`); + if (ABSOLUTE_HOME_PATTERN.test(content)) throw new Error(`WorkBuddy artifact contains an absolute home path: ${childRelative}`); + } + } + } + } + await visit(root); + return files; +} + +function assertPromptParity(manifest) { + const init = manifest.defaultInitPrompt; + const first = manifest.quickPrompts?.[0]; + if (!init || !first || init.en !== first.en || init.zh !== first.zh) { + throw new Error("defaultInitPrompt must exactly equal quickPrompts[0]"); + } +} + +export async function verifyWorkBuddyPluginRoot(root = ROOT) { + const resolvedRoot = path.resolve(root); + const manifest = await readJson(resolvedRoot, ".codebuddy-plugin/plugin.json"); + const marketplace = await readJson(resolvedRoot, ".codebuddy-plugin/marketplace.json"); + const settings = await readJson(resolvedRoot, "settings.json"); + if (manifest.name !== "better-harness" || manifest.expertType !== "team") throw new Error("plugin must be the Better Harness team plugin"); + if (manifest.categoryId !== "10-ProjectQuality") throw new Error("plugin categoryId must be 10-ProjectQuality"); + if (manifest.plugin !== manifest.name) throw new Error("plugin field must match name"); + if (!Array.isArray(manifest.tags) || manifest.tags.length !== 3) throw new Error("plugin must declare exactly three tags"); + if (!Array.isArray(manifest.quickPrompts) || manifest.quickPrompts.length !== 3) throw new Error("plugin must declare exactly three quick prompts"); + assertPromptParity(manifest); + const lead = manifest.teamInfo?.leadAgent; + const members = manifest.teamInfo?.memberAgents; + if (lead !== manifest.agentName || !Array.isArray(members) || members.length !== 3 || members.includes(lead)) { + throw new Error("teamInfo must contain one lead and exactly three member agents"); + } + if (settings.agent !== lead) throw new Error("settings.agent must select the team lead"); + const manifestAgents = new Set(manifest.agents ?? []); + for (const agentId of [lead, ...members]) { + const declared = `./agents/${agentId}.md`; + if (!manifestAgents.has(declared)) throw new Error(`agent is not declared by plugin.json: ${agentId}`); + await ensureRegular(resolvedRoot, `agents/${agentId}.md`); + } + const roles = new Set((manifest.members ?? []).map((member) => member.id)); + if (roles.size !== 4 || !roles.has(lead) || members.some((member) => !roles.has(member))) { + throw new Error("plugin members must contain lead plus the three canonical members"); + } + if (manifest.members.filter((member) => member.role === "lead").length !== 1) throw new Error("plugin members must contain exactly one lead"); + const marketplaceEntry = marketplace.plugins?.[0]; + if (!marketplaceEntry || marketplaceEntry.name !== manifest.name || marketplaceEntry.version !== manifest.version) { + throw new Error("Marketplace entry must match plugin name and version"); + } + if (marketplace.metadata?.version !== manifest.version) throw new Error("Marketplace metadata version must match plugin version"); + const files = await scanTree(resolvedRoot, { strict: path.basename(resolvedRoot) === "better-harness" && await exists(path.join(resolvedRoot, WORKBUDDY_ARTIFACT_MARKER)) }); + for (const required of WORKBUDDY_REQUIRED_PATHS) { + if (!files.includes(required)) throw new Error(`WorkBuddy plugin is missing required path: ${required}`); + } + return { + pluginRoot: resolvedRoot, + name: manifest.name, + version: manifest.version, + agentCount: manifest.agents.length, + memberCount: manifest.teamInfo.memberAgents.length, + fileCount: files.length, + }; +} + +async function copyPath(repoRoot, stageRoot, relativePath) { + const source = path.join(repoRoot, relativePath); + if (!(await exists(source))) throw new Error(`Missing WorkBuddy source path: ${relativePath}`); + const destination = path.join(stageRoot, relativePath); + await mkdir(path.dirname(destination), { recursive: true }); + await cp(source, destination, { + recursive: true, + dereference: true, + filter: (candidate) => !excluded(posix(path.relative(repoRoot, candidate))), + }); +} + +const CRC_TABLE = new Uint32Array(256); +for (let index = 0; index < 256; index += 1) { + let value = index; + for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; + CRC_TABLE[index] = value >>> 0; +} +function crc32(buffer) { + let value = 0xffffffff; + for (const byte of buffer) value = CRC_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8); + return (value ^ 0xffffffff) >>> 0; +} +function u16(value) { const buffer = Buffer.allocUnsafe(2); buffer.writeUInt16LE(value); return buffer; } +function u32(value) { const buffer = Buffer.allocUnsafe(4); buffer.writeUInt32LE(value >>> 0); return buffer; } +function zipBuffer(entries) { + const local = []; + const central = []; + let offset = 0; + for (const entry of entries) { + const name = Buffer.from(posix(entry.relativePath)); + const raw = entry.content; + const compressed = deflateRawSync(raw, { level: 9 }); + const header = Buffer.concat([u32(0x04034b50), u16(20), u16(0), u16(8), u16(0), u16(33), u32(crc32(raw)), u32(compressed.length), u32(raw.length), u16(name.length), u16(0), name]); + local.push(header, compressed); + central.push(Buffer.concat([u32(0x02014b50), u16(20), u16(20), u16(0), u16(8), u16(0), u16(33), u32(crc32(raw)), u32(compressed.length), u32(raw.length), u16(name.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(offset), name])); + offset += header.length + compressed.length; + } + const centralBuffer = Buffer.concat(central); + return Buffer.concat([...local, centralBuffer, Buffer.concat([u32(0x06054b50), u16(0), u16(0), u16(entries.length), u16(entries.length), u32(centralBuffer.length), u32(offset), u16(0)])]); +} + +async function collectFiles(root) { + const files = []; + for (const relativePath of await scanTree(root)) { + files.push({ relativePath, content: await readFile(path.join(root, relativePath)) }); + } + return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath)); +} + +async function replaceOutput(stageRoot, outputRoot) { + if (await exists(outputRoot)) { + const marker = JSON.parse(await readFile(path.join(outputRoot, WORKBUDDY_ARTIFACT_MARKER), "utf8").catch(() => "null")); + if (marker?.kind !== WORKBUDDY_ARTIFACT_KIND || marker.schemaVersion !== WORKBUDDY_ARTIFACT_SCHEMA_VERSION) { + throw new Error(`Refusing to replace unowned WorkBuddy output: ${outputRoot}`); + } + await rm(outputRoot, { recursive: true, force: true }); + } + await rename(stageRoot, outputRoot); +} + +export async function buildWorkBuddyPluginArtifact({ repoRoot = ROOT, outputRoot = path.join(ROOT, "dist", "workbuddy", "better-harness") } = {}) { + const resolvedRepoRoot = path.resolve(repoRoot); + const resolvedOutput = path.resolve(outputRoot); + await verifyWorkBuddyPluginRoot(resolvedRepoRoot); + // `mkdtemp` requires its parent to exist. Creating only the requested + // output parent keeps the build deterministic for a fresh checkout and + // does not create any private run state inside the plugin root. + await mkdir(path.dirname(resolvedOutput), { recursive: true }); + const stageContainer = await mkdtemp(path.join(path.dirname(resolvedOutput), ".better-harness-workbuddy-build-")); + const stageRoot = path.join(stageContainer, "better-harness"); + await mkdir(stageRoot, { recursive: true }); + try { + for (const file of COPY_FILES) await copyPath(resolvedRepoRoot, stageRoot, file); + for (const root of COPY_ROOTS) await copyPath(resolvedRepoRoot, stageRoot, root); + const marker = { + kind: WORKBUDDY_ARTIFACT_KIND, + schemaVersion: WORKBUDDY_ARTIFACT_SCHEMA_VERSION, + host: "workbuddy", + pluginName: "better-harness", + version: (await readJson(stageRoot, ".codebuddy-plugin/plugin.json")).version, + }; + await writeFile(path.join(stageRoot, WORKBUDDY_ARTIFACT_MARKER), `${JSON.stringify(marker, null, 2)}\n`); + const verified = await verifyWorkBuddyPluginRoot(stageRoot); + if (resolvedOutput.endsWith(".zip")) { + await mkdir(path.dirname(resolvedOutput), { recursive: true }); + await writeFile(resolvedOutput, zipBuffer(await collectFiles(stageRoot))); + // `stageRoot` is removed in the finally block. Do not return a stale + // pluginRoot path that points at that deleted temporary directory. + return { + name: verified.name, + version: verified.version, + agentCount: verified.agentCount, + memberCount: verified.memberCount, + archive: resolvedOutput, + fileCount: verified.fileCount + 1, + }; + } + await mkdir(path.dirname(resolvedOutput), { recursive: true }); + await replaceOutput(stageRoot, resolvedOutput); + return { ...verified, pluginRoot: resolvedOutput }; + } finally { + await rm(stageContainer, { recursive: true, force: true }).catch(() => {}); + } +} + +function usage() { + return [ + "Usage: node scripts/packaging/workbuddy-plugin.mjs [options]", + "", + "Options:", + " --verify [root] Validate the source WorkBuddy plugin root", + " --out Build an isolated WorkBuddy plugin directory or zip", + " --json Emit machine-readable JSON", + " -h, --help Print help", + "", + ].join("\n"); +} + +export async function main(argv = process.argv.slice(2)) { + const json = argv.includes("--json"); + try { + if (argv.includes("--help") || argv.includes("-h")) { + process.stdout.write(usage()); + return 0; + } + const verifyIndex = argv.indexOf("--verify"); + const outIndex = argv.indexOf("--out"); + const result = verifyIndex >= 0 + ? await verifyWorkBuddyPluginRoot(argv[verifyIndex + 1] && !argv[verifyIndex + 1].startsWith("-") ? argv[verifyIndex + 1] : ROOT) + : await buildWorkBuddyPluginArtifact({ outputRoot: outIndex >= 0 ? path.resolve(argv[outIndex + 1]) : undefined }); + process.stdout.write(json ? `${JSON.stringify({ ok: true, data: result }, null, 2)}\n` : `WorkBuddy plugin verified: ${result.pluginRoot ?? result.archive}\n`); + return 0; + } catch (error) { + if (json) process.stdout.write(`${JSON.stringify({ ok: false, error: { message: error.message } }, null, 2)}\n`); + else process.stderr.write(`WorkBuddy plugin validation failed: ${error.message}\n`); + return 1; + } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = await main(); diff --git a/scripts/session-analysis/episode-facts.mjs b/scripts/session-analysis/episode-facts.mjs index 349b532..fa4315e 100644 --- a/scripts/session-analysis/episode-facts.mjs +++ b/scripts/session-analysis/episode-facts.mjs @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { privacySafeUserInputEvidence } from "./privacy-safe-text.mjs"; +import { sanitizeProviderCoverage } from "./provider-coverage.mjs"; export const SESSION_CORE_FACTS_SCHEMA_VERSION = 3; export const EPISODE_FACTS_SCHEMA_VERSION = 2; @@ -116,10 +117,12 @@ export function finalizeSessionCoreFacts({ warnings = [], omitted = {}, sourceCoverage = null, + providerCoverage = null, maxBytes = MAX_SESSION_CORE_FACT_BYTES, } = {}) { const entries = (episodeFacts?.entries ?? []).map((entry) => structuredClone(entry)); const boundedSourceCoverage = safeSourceCoverage(sourceCoverage); + const boundedProviderCoverage = sanitizeProviderCoverage(providerCoverage); const envelope = { schemaVersion: SESSION_CORE_FACTS_SCHEMA_VERSION, kind: "session-core-facts", @@ -162,6 +165,7 @@ export function finalizeSessionCoreFacts({ }, warningCodes: [...new Set(warnings.map((warning) => safeLabel(warning?.code, 64)).filter(Boolean))].slice(0, 5), ...(boundedSourceCoverage ? { sourceCoverage: boundedSourceCoverage } : {}), + ...(boundedProviderCoverage ? { providerCoverage: boundedProviderCoverage } : {}), excludes: [ "assistantText", "rawPrompts", diff --git a/scripts/session-analysis/fs.mjs b/scripts/session-analysis/fs.mjs index 1d23b70..d6230de 100644 --- a/scripts/session-analysis/fs.mjs +++ b/scripts/session-analysis/fs.mjs @@ -46,8 +46,20 @@ export async function forEachJsonLine(filePath, onRecord, options = {}) { shouldStop = true; lines.close(); } - } catch { + } catch (error) { invalidLines += 1; + // Adapters that require an authoritative first record (for example + // Pi's session header) must be able to distinguish a malformed JSON + // line from an empty file. Keep this opt-in so existing collectors + // retain their current fail-soft behavior. + if (typeof options.onInvalid === "function") { + try { + await options.onInvalid(error, line, lineNumber); + } catch { + // Diagnostics are best-effort and must not turn a malformed input + // into an unbounded reader failure. + } + } } } } finally { diff --git a/scripts/session-analysis/index.mjs b/scripts/session-analysis/index.mjs index 3438f2a..2bad8f0 100644 --- a/scripts/session-analysis/index.mjs +++ b/scripts/session-analysis/index.mjs @@ -28,3 +28,9 @@ export { } from "./session-population.mjs"; export { projectSemanticFacets, validateSemanticFacets } from "./semantic-facets.mjs"; export { sessionAnalysisRef } from "./session-ref.mjs"; +export { + appendUnsupportedCapabilities, + buildProviderCoverage, + providerCoverageStates, + sanitizeProviderCoverage, +} from "./provider-coverage.mjs"; diff --git a/scripts/session-analysis/observation-manifest.mjs b/scripts/session-analysis/observation-manifest.mjs index e74fe7d..c22043c 100644 --- a/scripts/session-analysis/observation-manifest.mjs +++ b/scripts/session-analysis/observation-manifest.mjs @@ -1,4 +1,5 @@ import { stableFingerprint } from "./episode-contract.mjs"; +import { sanitizeProviderCoverage } from "./provider-coverage.mjs"; export const OBSERVATION_MANIFEST_SCHEMA_VERSION = 2; @@ -25,6 +26,7 @@ export function buildObservationManifest({ adapterVersion = "session-analysis-v2", generatedAt = null, privacyMode = "reader-safe", + providerCoverage = null, } = {}) { const sourceInventory = sources.map(sourceShape).sort((left, right) => left.id.localeCompare(right.id)); const sampled = analyzedCount < eligibleCount; @@ -71,6 +73,7 @@ export function buildObservationManifest({ rawCommandIncluded: false, absoluteHomePathIncluded: false, }, + ...(sanitizeProviderCoverage(providerCoverage) ? { providerCoverage: sanitizeProviderCoverage(providerCoverage) } : {}), warningCodes: [...new Set(warnings.map((warning) => warning?.code).filter(Boolean))].sort(), }; } diff --git a/scripts/session-analysis/platforms/pi.mjs b/scripts/session-analysis/platforms/pi.mjs index 4918f4a..acae56e 100644 --- a/scripts/session-analysis/platforms/pi.mjs +++ b/scripts/session-analysis/platforms/pi.mjs @@ -18,10 +18,13 @@ import { sessionWorkspaceCwd, workspaceMatchScopeFromOptions, } from "../provider-runner.mjs"; +import { buildProviderCoverage } from "../provider-coverage.mjs"; import { parseResultFacts } from "../result-facts.mjs"; import { mergeTimeRange, normalizeCliDate, normalizeTimestamp, timestampMillis, withinTimeRange } from "../time.mjs"; import { WORKSPACE_CWD_MATCH, classifyWorkspaceCwd } from "../workspace-match.mjs"; +const PI_SESSION_SCHEMA_VERSION = 3; + function isWorkspaceMatch(candidate, workspace) { if (!candidate) return false; const resolved = normalizeWorkspace(candidate); @@ -238,14 +241,22 @@ async function probeTranscript(filePath, scope) { workspaceMatch: false, validHeader: false, cwd: null, + schemaVersion: null, + unknownVersion: false, + invalidLines: 0, }; let firstRecord = true; - await forEachJsonLine(filePath, (raw) => { + const scan = await forEachJsonLine(filePath, (raw) => { if (firstRecord) { firstRecord = false; // Pi requires the first parsed record to be the session header. Do not // let a later injected header qualify an otherwise foreign transcript. - if (raw?.type !== "session" || typeof raw.id !== "string" || !isScopedWorkspaceMatch(raw.cwd, scope)) { + summary.schemaVersion = raw?.version ?? null; + summary.unknownVersion = raw?.version !== PI_SESSION_SCHEMA_VERSION; + if (raw?.type !== "session" + || typeof raw.id !== "string" + || raw.version !== PI_SESSION_SCHEMA_VERSION + || !isScopedWorkspaceMatch(raw.cwd, scope)) { return false; } summary.validHeader = true; @@ -260,7 +271,21 @@ async function probeTranscript(filePath, scope) { return false; } mergeTimeRange(summary, inferTimestamp(raw)); + }, { + onInvalid: (_error, _line, lineNumber) => { + // A malformed first line is not an absent header. Mark the first + // physical record as consumed so a later valid header cannot silently + // turn an invalid transcript into a complete Pi session. + if (firstRecord) { + firstRecord = false; + summary.invalidLines = Number(summary.invalidLines ?? 0) + 1; + summary.invalidHeader = true; + summary.invalidHeaderLine = lineNumber; + } + }, }); + summary.invalidLines = scan.invalidLines; + if (summary.invalidHeader) summary.validHeader = false; return summary; } @@ -410,8 +435,19 @@ export class PiSessionAnalyzer extends SessionAnalyzer { async discoverSessions(scope, roots) { const sessions = new Map(); + const diagnostics = { + expectedVersion: PI_SESSION_SCHEMA_VERSION, + observedVersions: [], + unknownVersionCount: 0, + invalidHeaderCount: 0, + invalidRecordCount: 0, + codes: [], + }; const transcriptRoot = roots.find((root) => root.kind === "pi-session-jsonl"); - if (!transcriptRoot) return []; + if (!transcriptRoot) { + scope._piSchemaDiagnostics = { ...diagnostics, status: "unavailable" }; + return []; + } const custom = scope.sessionDirMode === "custom"; const seenDirs = new Set(); for (const sessionsRoot of transcriptRoot.paths ?? []) { @@ -433,6 +469,12 @@ export class PiSessionAnalyzer extends SessionAnalyzer { // Both modes qualify each transcript by the session-header cwd, so a // shared custom directory never leaks foreign-workspace sessions. const probe = await probeTranscript(filePath, scope); + if (probe.schemaVersion !== null && !diagnostics.observedVersions.includes(probe.schemaVersion)) { + diagnostics.observedVersions.push(probe.schemaVersion); + } + diagnostics.invalidRecordCount += Number(probe.invalidLines ?? 0); + if (probe.unknownVersion) diagnostics.unknownVersionCount += 1; + if (!probe.validHeader) diagnostics.invalidHeaderCount += 1; if (!probe.validHeader || !probe.workspaceMatch || !withinTimeRange(probe.lastSeen ?? probe.firstSeen, scope)) continue; addRef(sessions, probe.sessionId, scope.workspace, { kind: transcriptRoot.kind, @@ -445,6 +487,16 @@ export class PiSessionAnalyzer extends SessionAnalyzer { } } } + diagnostics.observedVersions.sort((left, right) => String(left).localeCompare(String(right))); + diagnostics.status = diagnostics.unknownVersionCount > 0 + || diagnostics.invalidHeaderCount > 0 + || diagnostics.invalidRecordCount > 0 + ? "partial" + : "verified"; + if (diagnostics.unknownVersionCount > 0) diagnostics.codes.push("pi-unknown-session-version"); + if (diagnostics.invalidHeaderCount > 0) diagnostics.codes.push("pi-invalid-session-header"); + if (diagnostics.invalidRecordCount > 0) diagnostics.codes.push("pi-invalid-jsonl-record"); + scope._piSchemaDiagnostics = diagnostics; return [...sessions.values()].map(finalizeSession) .sort((left, right) => (timestampMillis(right.lastSeen) ?? 0) - (timestampMillis(left.lastSeen) ?? 0)); } @@ -496,6 +548,51 @@ export class PiSessionAnalyzer extends SessionAnalyzer { async analyze(options = {}) { return runProviderAnalysis(this, options, { platform: "pi", adapterVersion: "pi-v1" }); } + + coverageDiagnostics(scope, { roots = [], eligibleSessions = [] } = {}) { + const schemaDiagnostics = scope._piSchemaDiagnostics ?? { + expectedVersion: PI_SESSION_SCHEMA_VERSION, + observedVersions: [], + unknownVersionCount: 0, + invalidHeaderCount: 0, + invalidRecordCount: 0, + status: "unknown", + codes: [], + }; + const root = roots.find((entry) => entry.kind === "pi-session-jsonl"); + const sourceCoverage = { + status: root?.exists === false + ? "absent" + : schemaDiagnostics.status === "partial" + ? "partial" + : eligibleSessions.length === 0 + ? "unobserved" + : "observed", + transcript: { + workspaceSessions: eligibleSessions.length, + inWindowSessions: eligibleSessions.length, + outOfWindowSessions: 0, + timeUnobservedSessions: eligibleSessions.filter((session) => !session.firstSeen && !session.lastSeen).length, + relevantSessions: eligibleSessions.length, + withConversation: 0, + withRequest: 0, + terminalOnly: 0, + unreadable: 0, + }, + joins: { chatMetadata: {}, audit: {} }, + }; + return buildProviderCoverage({ + provider: "pi", + sourceCoverage, + configured: Boolean(root), + enabled: root?.enabled === true, + observed: eligibleSessions.length > 0, + verified: schemaDiagnostics.status === "verified" && eligibleSessions.length > 0, + unsupported: schemaDiagnostics.unknownVersionCount > 0 ? ["pi-session-schema-version"] : [], + unavailable: root?.exists === false ? ["pi-session-transcript"] : [], + schemaDiagnostics, + }); + } } export async function main(argv = process.argv.slice(2)) { diff --git a/scripts/session-analysis/platforms/workbuddy.mjs b/scripts/session-analysis/platforms/workbuddy.mjs index eae9648..e5d8d54 100644 --- a/scripts/session-analysis/platforms/workbuddy.mjs +++ b/scripts/session-analysis/platforms/workbuddy.mjs @@ -18,10 +18,25 @@ import { sessionWorkspaceCwd, workspaceMatchScopeFromOptions, } from "../provider-runner.mjs"; +import { buildProviderCoverage } from "../provider-coverage.mjs"; import { parseResultFacts } from "../result-facts.mjs"; import { mergeTimeRange, normalizeCliDate, normalizeTimestamp, timestampMillis, withinTimeRange } from "../time.mjs"; import { WORKSPACE_CWD_MATCH, classifyWorkspaceCwd } from "../workspace-match.mjs"; +const WORKBUDDY_KNOWN_RECORD_TYPES = new Set([ + "message", + "reasoning", + "function_call", + "function_call_result", + "ai-title", + "custom-title", + "file-history-snapshot", + "session", + "response", + "tool_call", + "tool_result", +]); + function isWorkspaceMatch(candidate, workspace) { if (!candidate) return false; const resolved = normalizeWorkspace(candidate); @@ -56,6 +71,15 @@ function inferTimestamp(raw) { return normalizeTimestamp(value); } +function isKnownRecordShape(raw) { + const type = String(raw?.type ?? ""); + if (WORKBUDDY_KNOWN_RECORD_TYPES.has(type)) return true; + // A few WorkBuddy builds omitted `type` on ordinary message rows but kept a + // role/content pair. Treat that shape as compatible; everything else is + // surfaced as an unknown record rather than silently projected as metadata. + return !type && typeof raw?.role === "string" && Object.hasOwn(raw, "content"); +} + function contentBlocks(raw) { const content = raw?.content; if (Array.isArray(content)) return content; @@ -291,8 +315,12 @@ async function probeTranscript(filePath, scope, directoryCandidate) { cwdObserved: false, foreignCwd: false, cwd: null, + directoryInferred: false, + unknownRecordShapeCount: 0, + invalidLines: 0, }; - await forEachJsonLine(filePath, (raw) => { + const scan = await forEachJsonLine(filePath, (raw) => { + if (!isKnownRecordShape(raw)) summary.unknownRecordShapeCount += 1; if (raw?.sessionId) summary.sessionId = raw.sessionId; if (typeof raw?.cwd === "string" && raw.cwd.length > 0) { summary.cwdObserved = true; @@ -309,7 +337,9 @@ async function probeTranscript(filePath, scope, directoryCandidate) { : directoryCandidate.matchKind === "exact"; if (!summary.cwdObserved && summary.workspaceMatch) { summary.cwd = directoryCandidate.workspaceIdentity; + summary.directoryInferred = true; } + summary.invalidLines = scan.invalidLines; return summary; } @@ -351,7 +381,16 @@ async function listProjectDirectories(projectsRoot, contracts) { .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .map((name) => { - const exact = contracts.find((contract) => name === contract.variants.exact); + const exactMatches = contracts.filter((contract) => name === contract.variants.exact); + if (exactMatches.length > 1) { + return { + path: path.join(projectsRoot, name), + matchKind: "collision", + workspaceIdentity: null, + collisionCount: exactMatches.length, + }; + } + const exact = exactMatches[0]; if (exact) { return { path: path.join(projectsRoot, name), @@ -359,9 +398,19 @@ async function listProjectDirectories(projectsRoot, contracts) { workspaceIdentity: exact.workspaceIdentity, }; } - const prefix = contracts + const prefixMatches = contracts .filter((contract) => name.startsWith(contract.variants.prefix)) - .sort((left, right) => right.variants.prefix.length - left.variants.prefix.length)[0]; + .sort((left, right) => right.variants.prefix.length - left.variants.prefix.length); + const prefix = prefixMatches[0]; + if (prefixMatches.length > 1 + && prefixMatches[0].variants.prefix.length === prefixMatches[1].variants.prefix.length) { + return { + path: path.join(projectsRoot, name), + matchKind: "collision", + workspaceIdentity: null, + collisionCount: prefixMatches.filter((item) => item.variants.prefix.length === prefix.variants.prefix.length).length, + }; + } return prefix ? { path: path.join(projectsRoot, name), matchKind: "prefix", @@ -373,7 +422,7 @@ async function listProjectDirectories(projectsRoot, contracts) { export class WorkbuddySessionAnalyzer extends SessionAnalyzer { currentSessionId() { - return process.env.WORKBUDDY_SESSION_ID ?? null; + return process.env.CODEBUDDY_SESSION_ID ?? process.env.WORKBUDDY_SESSION_ID ?? null; } async resolveScope(options = {}) { @@ -388,6 +437,14 @@ export class WorkbuddySessionAnalyzer extends SessionAnalyzer { const projectsDir = path.resolve(expandHome( options.projectsDir ?? options["projects-dir"] ?? path.join(home, "projects"), )); + const codebuddySessionId = options.codebuddySessionId + ?? options["codebuddy-session-id"] + ?? process.env.CODEBUDDY_SESSION_ID + ?? null; + const legacySessionId = options.workbuddySessionId + ?? options["workbuddy-session-id"] + ?? process.env.WORKBUDDY_SESSION_ID + ?? null; return { platform: "workbuddy", workspace, @@ -405,7 +462,8 @@ export class WorkbuddySessionAnalyzer extends SessionAnalyzer { sinceTime: since.time, until: until.label, untilTime: until.time, - sessionId: options["session-id"] ?? options.sessionId ?? options._?.[0] ?? null, + sessionId: options["session-id"] ?? options.sessionId ?? options._?.[0] ?? codebuddySessionId ?? legacySessionId ?? null, + _sessionIdConflict: Boolean(codebuddySessionId && legacySessionId && codebuddySessionId !== legacySessionId), includeGlobalCapabilities: parseBooleanFlag(options["include-global-capabilities"] ?? false), _workspaceMatchScope: workspaceMatchScope, }; @@ -425,18 +483,29 @@ export class WorkbuddySessionAnalyzer extends SessionAnalyzer { workspaceScoped: true, coverage: "primary", // A shared projects parent is not itself workspace evidence. - exists: matchingDirs.length > 0, + exists: matchingDirs.some((candidate) => candidate.matchKind !== "collision"), }]; } async discoverSessions(scope, roots) { const sessions = new Map(); + const diagnostics = { + unknownRecordShapeCount: 0, + invalidRecordCount: 0, + cwdlessSessionCount: 0, + slugCollisionCount: 0, + codes: [], + }; const transcriptRoot = roots.find((root) => root.kind === "workbuddy-session-jsonl"); if (!transcriptRoot) return []; const seenDirs = new Set(); for (const projectsRoot of transcriptRoot.paths ?? []) { if (!await pathExists(projectsRoot)) continue; for (const candidate of await listProjectDirectories(projectsRoot, scope._workspaceDirContracts)) { + if (candidate.matchKind === "collision") { + diagnostics.slugCollisionCount += 1; + continue; + } const dirPath = candidate.path; let realDir; try { realDir = realpathSync.native(dirPath); } catch { realDir = path.resolve(dirPath); } @@ -445,6 +514,9 @@ export class WorkbuddySessionAnalyzer extends SessionAnalyzer { const files = await walkFiles(dirPath, { maxDepth: 1, limit: 20_000, match: (file) => file.endsWith(".jsonl") }); for (const filePath of files) { const probe = await probeTranscript(filePath, scope, candidate); + diagnostics.unknownRecordShapeCount += Number(probe.unknownRecordShapeCount ?? 0); + diagnostics.invalidRecordCount += Number(probe.invalidLines ?? 0); + if (probe.directoryInferred) diagnostics.cwdlessSessionCount += 1; if (!probe.workspaceMatch || !withinTimeRange(probe.lastSeen ?? probe.firstSeen, scope)) continue; addRef(sessions, probe.sessionId, scope.workspace, { kind: transcriptRoot.kind, @@ -453,10 +525,22 @@ export class WorkbuddySessionAnalyzer extends SessionAnalyzer { firstSeen: probe.firstSeen, lastSeen: probe.lastSeen, cwd: probe.cwd, + cwdEvidence: probe.directoryInferred ? "directory-inferred" : "record-observed", }); } } } + diagnostics.status = diagnostics.slugCollisionCount > 0 + || diagnostics.unknownRecordShapeCount > 0 + || diagnostics.invalidRecordCount > 0 + || diagnostics.cwdlessSessionCount > 0 + ? "partial" + : "verified"; + if (diagnostics.slugCollisionCount > 0) diagnostics.codes.push("workbuddy-slug-collision"); + if (diagnostics.unknownRecordShapeCount > 0) diagnostics.codes.push("workbuddy-unknown-record-shape"); + if (diagnostics.invalidRecordCount > 0) diagnostics.codes.push("workbuddy-invalid-jsonl-record"); + if (diagnostics.cwdlessSessionCount > 0) diagnostics.codes.push("workbuddy-directory-inferred-cwd"); + scope._workbuddySchemaDiagnostics = diagnostics; return [...sessions.values()].map(finalizeSession) .sort((left, right) => (timestampMillis(right.lastSeen) ?? 0) - (timestampMillis(left.lastSeen) ?? 0)); } @@ -506,6 +590,85 @@ export class WorkbuddySessionAnalyzer extends SessionAnalyzer { async analyze(options = {}) { return runProviderAnalysis(this, options, { platform: "workbuddy", adapterVersion: "workbuddy-v1" }); } + + async analysisWarnings(scope, _roots, _sessions) { + const diagnostics = scope._workbuddySchemaDiagnostics ?? {}; + const warnings = []; + if (scope._sessionIdConflict) warnings.push({ + code: "workbuddy-session-id-conflict", + message: "WorkBuddy current-session identity sources conflict; evidence is blocked until one identity is selected.", + }); + if (diagnostics.slugCollisionCount > 0) warnings.push({ + code: "workbuddy-slug-collision", + message: "One or more WorkBuddy project slugs matched multiple workspace identities and were excluded.", + }); + if (diagnostics.cwdlessSessionCount > 0) warnings.push({ + code: "workbuddy-directory-inferred-cwd", + message: "Some WorkBuddy sessions were admitted from an exact directory slug without an observed cwd; they remain partial evidence.", + }); + if (diagnostics.unknownRecordShapeCount > 0) warnings.push({ + code: "workbuddy-unknown-record-shape", + message: "Some WorkBuddy records used an unknown shape and were retained only as partial metadata evidence.", + }); + return warnings; + } + + factsSourceCoverage(scope) { + return scope._workbuddySourceCoverage ?? null; + } + + coverageDiagnostics(scope, { roots = [], eligibleSessions = [] } = {}) { + const schemaDiagnostics = { + ...(scope._workbuddySchemaDiagnostics ?? { + unknownRecordShapeCount: 0, + invalidRecordCount: 0, + cwdlessSessionCount: 0, + slugCollisionCount: 0, + status: "unknown", + codes: [], + }), + sessionIdConflict: scope._sessionIdConflict === true, + }; + if (schemaDiagnostics.sessionIdConflict) schemaDiagnostics.codes = [...new Set([...(schemaDiagnostics.codes ?? []), "workbuddy-session-id-conflict"])] + .sort(); + const root = roots.find((entry) => entry.kind === "workbuddy-session-jsonl"); + const partial = schemaDiagnostics.status === "partial" + || schemaDiagnostics.sessionIdConflict + || schemaDiagnostics.unknownRecordShapeCount > 0 + || schemaDiagnostics.cwdlessSessionCount > 0; + const sourceCoverage = { + status: root?.exists === false + ? "absent" + : partial + ? "partial" + : eligibleSessions.length === 0 + ? "unobserved" + : "observed", + transcript: { + workspaceSessions: eligibleSessions.length, + inWindowSessions: eligibleSessions.length, + outOfWindowSessions: 0, + timeUnobservedSessions: eligibleSessions.filter((session) => !session.firstSeen && !session.lastSeen).length, + relevantSessions: eligibleSessions.length, + withConversation: 0, + withRequest: 0, + terminalOnly: 0, + unreadable: schemaDiagnostics.invalidRecordCount, + }, + joins: { chatMetadata: {}, audit: {} }, + }; + scope._workbuddySourceCoverage = sourceCoverage; + return buildProviderCoverage({ + provider: "workbuddy", + sourceCoverage, + configured: Boolean(root), + enabled: root?.enabled === true, + observed: eligibleSessions.length > 0, + verified: !partial && eligibleSessions.length > 0, + unavailable: root?.exists === false ? ["workbuddy-session-transcript"] : [], + schemaDiagnostics, + }); + } } export async function main(argv = process.argv.slice(2)) { diff --git a/scripts/session-analysis/provider-coverage.mjs b/scripts/session-analysis/provider-coverage.mjs new file mode 100644 index 0000000..282775b --- /dev/null +++ b/scripts/session-analysis/provider-coverage.mjs @@ -0,0 +1,222 @@ +/** + * Reader-safe provider coverage contract. + * + * Provider adapters may know much more about their local storage than a + * report is allowed to retain. This module deliberately projects only + * bounded states, counts, and diagnostic codes. It is shared by the + * session runner, the evidence bundle, and the report-source manifest so a + * transport/source probe cannot be mistaken for verified semantic coverage. + */ + +export const PROVIDER_COVERAGE_SCHEMA_VERSION = 1; +export const MAX_UNSUPPORTED_CAPABILITIES = 16; + +const COVERAGE_STATUSES = new Set([ + "absent", + "out-of-window", + "unobserved", + "partial", + "observed", + "unsupported", + "unavailable", +]); + +const DIAGNOSTIC_FIELDS = new Set([ + "status", + "expectedVersion", + "observedVersions", + "unknownVersionCount", + "invalidHeaderCount", + "invalidRecordCount", + "unknownRecordShapeCount", + "cwdlessSessionCount", + "slugCollisionCount", + "sessionIdConflict", + "codes", +]); + +function nonNegativeInteger(value) { + const number = Number(value ?? 0); + return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : 0; +} + +function codeList(value, limit = 12) { + return [...new Set((Array.isArray(value) ? value : [value]) + .map((item) => String(item ?? "").trim()) + .filter((item) => /^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$/u.test(item)))] + .slice(0, limit) + .sort(); +} + +function capabilityList(value) { + return [...new Set((Array.isArray(value) ? value : [value]) + .map((item) => String(item ?? "") + .replace(/[\u0000-\u001f\u007f]/gu, " ") + .replace(/\s+/gu, " ") + .trim() + .slice(0, 180)) + .filter(Boolean))] + .slice(0, MAX_UNSUPPORTED_CAPABILITIES) + .sort(); +} + +function safeDiagnostic(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const result = {}; + for (const key of DIAGNOSTIC_FIELDS) { + if (!(key in value)) continue; + if (key === "status") { + const status = String(value.status ?? ""); + if (["verified", "partial", "unknown", "unavailable"].includes(status)) result.status = status; + } else if (key === "expectedVersion") { + if (typeof value.expectedVersion === "number" && Number.isFinite(value.expectedVersion)) { + result.expectedVersion = Math.trunc(value.expectedVersion); + } else if (typeof value.expectedVersion === "string" && value.expectedVersion.length <= 32) { + result.expectedVersion = value.expectedVersion; + } + } else if (key === "observedVersions") { + result.observedVersions = [...new Set((Array.isArray(value.observedVersions) ? value.observedVersions : []) + .filter((item) => (typeof item === "number" && Number.isFinite(item)) || (typeof item === "string" && item.length <= 32)))] + .slice(0, 8); + } else if (key === "sessionIdConflict") { + result.sessionIdConflict = value.sessionIdConflict === true; + } else if (key === "codes") { + result.codes = codeList(value.codes); + } else { + result[key] = nonNegativeInteger(value[key]); + } + } + return Object.keys(result).length > 0 ? result : null; +} + +function safeSourceCoverage(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const status = String(value.status ?? ""); + if (!COVERAGE_STATUSES.has(status)) return null; + const count = (input) => nonNegativeInteger(input); + const join = (input = {}) => ({ + sourceAvailable: input.sourceAvailable === true, + matchedWorkspaceSessions: count(input.matchedWorkspaceSessions), + matchedRelevantSessions: count(input.matchedRelevantSessions), + }); + return { + status, + transcript: { + workspaceSessions: count(value.transcript?.workspaceSessions), + inWindowSessions: count(value.transcript?.inWindowSessions), + outOfWindowSessions: count(value.transcript?.outOfWindowSessions), + timeUnobservedSessions: count(value.transcript?.timeUnobservedSessions), + relevantSessions: count(value.transcript?.relevantSessions), + withConversation: count(value.transcript?.withConversation), + withRequest: count(value.transcript?.withRequest), + terminalOnly: count(value.transcript?.terminalOnly), + unreadable: count(value.transcript?.unreadable), + }, + joins: { + chatMetadata: join(value.joins?.chatMetadata), + audit: join(value.joins?.audit), + }, + }; +} + +function statusFrom({ sourceCoverage, observed, enabled, unsupported, unavailable }) { + const sourceStatus = String(sourceCoverage?.status ?? ""); + // Preserve a source-level absence/window result, but never let an + // `unobserved` source mask an explicit unsupported or unavailable adapter. + // This is what keeps an unknown schema visible when it yields zero admitted + // sessions. + if (sourceStatus === "absent" || sourceStatus === "out-of-window") return sourceStatus; + if (unsupported.length > 0 && !observed) return "unsupported"; + if (unavailable.length > 0 || !enabled) return "unavailable"; + if (COVERAGE_STATUSES.has(sourceStatus)) return sourceStatus; + return observed ? "observed" : "unobserved"; +} + +/** + * Build the public provider coverage envelope. The boolean state fields are + * intentionally explicit: configured/enabled is not the same as observed, + * and observed is not the same as schema-verified. + */ +export function buildProviderCoverage({ + provider = "unknown", + sourceCoverage = null, + configured = false, + enabled = false, + observed = false, + verified = false, + unsupported = [], + unsupportedCapabilities = [], + unavailable = [], + schemaDiagnostics = null, +} = {}) { + const safeUnsupported = codeList(unsupported); + const safeUnsupportedCapabilities = capabilityList(unsupportedCapabilities); + const safeUnavailable = codeList(unavailable); + const safeSchemaDiagnostics = safeDiagnostic(schemaDiagnostics); + const status = statusFrom({ + sourceCoverage, + observed, + enabled, + unsupported: safeUnsupported, + unavailable: safeUnavailable, + }); + return { + schemaVersion: PROVIDER_COVERAGE_SCHEMA_VERSION, + provider: String(provider).toLowerCase(), + status, + configured: configured === true, + enabled: enabled === true, + observed: observed === true, + verified: verified === true && safeUnsupported.length === 0 && safeUnavailable.length === 0, + unsupported: safeUnsupported, + ...(safeUnsupportedCapabilities.length > 0 ? { unsupportedCapabilities: safeUnsupportedCapabilities } : {}), + unavailable: safeUnavailable, + ...(safeSourceCoverage(sourceCoverage) ? { sourceCoverage: safeSourceCoverage(sourceCoverage) } : {}), + ...(safeSchemaDiagnostics ? { schemaDiagnostics: safeSchemaDiagnostics } : {}), + }; +} + +export function sanitizeProviderCoverage(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return buildProviderCoverage({ + provider: value.provider, + sourceCoverage: value.sourceCoverage ?? value, + configured: value.configured === true, + enabled: value.enabled === true, + observed: value.observed === true, + verified: value.verified === true, + unsupported: value.unsupported, + unsupportedCapabilities: value.unsupportedCapabilities, + unavailable: value.unavailable, + schemaDiagnostics: value.schemaDiagnostics, + }); +} + +export function providerCoverageStates(value) { + const coverage = sanitizeProviderCoverage(value); + if (!coverage) return null; + return { + configured: coverage.configured, + enabled: coverage.enabled, + observed: coverage.observed, + verified: coverage.verified, + unsupported: coverage.unsupported, + ...(coverage.unsupportedCapabilities?.length > 0 + ? { unsupportedCapabilities: coverage.unsupportedCapabilities } + : {}), + unavailable: coverage.unavailable, + }; +} + +/** Add bounded asset-inventory capability gaps without changing session states. */ +export function appendUnsupportedCapabilities(value, capabilities = []) { + const coverage = sanitizeProviderCoverage(value); + if (!coverage) return null; + return buildProviderCoverage({ + ...coverage, + unsupportedCapabilities: [ + ...(coverage.unsupportedCapabilities ?? []), + ...capabilityList(capabilities), + ], + }); +} diff --git a/scripts/session-analysis/provider-runner.mjs b/scripts/session-analysis/provider-runner.mjs index 4e306e4..9651576 100644 --- a/scripts/session-analysis/provider-runner.mjs +++ b/scripts/session-analysis/provider-runner.mjs @@ -24,6 +24,7 @@ import { summarizeWorkspaceQualifications, validateWorkspaceMatchTopology, } from "./workspace-match.mjs"; +import { buildProviderCoverage, sanitizeProviderCoverage } from "./provider-coverage.mjs"; const DEFAULT_LIMIT = 50; const DEFAULT_WORKSPACE_PREFLIGHT_MAX_LINES = 2_000; @@ -416,6 +417,26 @@ export function buildProviderFacets({ indexedSessions = [], detailedSessions = [ }; } +async function providerCoverage(analyzer, scope, context = {}) { + if (typeof analyzer.coverageDiagnostics === "function") { + const value = await analyzer.coverageDiagnostics(scope, context); + const normalized = sanitizeProviderCoverage(value); + if (normalized) return normalized; + } + const roots = Array.isArray(context.roots) ? context.roots : []; + const enabled = roots.some((root) => root?.enabled === true); + const configured = roots.length > 0; + const observed = Number(context.eligibleSessions?.length ?? context.discoveredSessions?.length ?? 0) > 0; + return buildProviderCoverage({ + provider: scope.platform, + configured, + enabled, + observed, + verified: observed && enabled, + unavailable: enabled ? [] : ["provider-source"], + }); +} + async function pricingTable(options) { if (!options["pricing-table"]) return undefined; return JSON.parse(await readFile(path.resolve(options["pricing-table"]), "utf8")); @@ -445,6 +466,11 @@ export async function runProviderAnalysis(analyzer, options = {}, config = {}) { ? prepareFactsSessionInventory(qualifiedSessions, factsContext) : { sessions: qualifiedSessions, omitted: {} }; const sessions = factsInventory.sessions; + const coverage = await providerCoverage(analyzer, scope, { + roots, + discoveredSessions: discovered, + eligibleSessions: sessions, + }); const warnings = [ ...sourceWarnings(roots), ...(typeof analyzer.analysisWarnings === "function" ? await analyzer.analysisWarnings(scope, roots, sessions) : []), @@ -455,6 +481,9 @@ export async function runProviderAnalysis(analyzer, options = {}, config = {}) { sessions, facets: null, warnings, + providerCoverage: coverage, + coverageStatus: coverage?.status ?? "unobserved", + ...(coverage?.schemaDiagnostics ? { schemaDiagnostics: coverage.schemaDiagnostics } : {}), }, workspaceRun); if (options.command === "sources") return resultBase; if (options.command === "sessions") { @@ -511,6 +540,19 @@ export async function runProviderAnalysis(analyzer, options = {}, config = {}) { warnings, }) : null; + const factsCoverage = sourceCoverage + ? buildProviderCoverage({ + provider: scope.platform, + sourceCoverage, + configured: coverage.configured, + enabled: coverage.enabled, + observed: coverage.observed, + verified: coverage.verified, + unsupported: coverage.unsupported, + unavailable: coverage.unavailable, + schemaDiagnostics: coverage.schemaDiagnostics, + }) + : coverage; return withWorkspaceMatchDiagnostics(buildSessionCoreFacts({ scope, events, @@ -520,6 +562,7 @@ export async function runProviderAnalysis(analyzer, options = {}, config = {}) { episodeLimit: options["episode-limit"] ?? options.episodeLimit ?? options.limit, debug: parseBooleanFlag(options.debug ?? false), sourceCoverage, + providerCoverage: factsCoverage, }), workspaceRun, hydration.hydrationQualifications); } if (fileReadMode) { diff --git a/scripts/session-analysis/session-core-facts.mjs b/scripts/session-analysis/session-core-facts.mjs index a90948f..f046e49 100644 --- a/scripts/session-analysis/session-core-facts.mjs +++ b/scripts/session-analysis/session-core-facts.mjs @@ -2,6 +2,7 @@ import path from "node:path"; import { buildTaskEpisodes } from "./episode-contract.mjs"; import { finalizeSessionCoreFacts } from "./episode-facts.mjs"; +import { sanitizeProviderCoverage } from "./provider-coverage.mjs"; const DEFAULT_ACTIVE_WINDOW_MS = 60_000; const MIN_FACT_SESSION_POOL = 12; @@ -93,6 +94,7 @@ export function buildSessionCoreFacts({ episodeLimit, debug = false, sourceCoverage = null, + providerCoverage = null, } = {}) { const executionEvents = events.filter((event) => event?.lifecyclePhase !== "pre"); const episodeAnalysis = buildTaskEpisodes(executionEvents, { @@ -117,6 +119,7 @@ export function buildSessionCoreFacts({ warnings, omitted, sourceCoverage, + providerCoverage: sanitizeProviderCoverage(providerCoverage), }); } diff --git a/scripts/session-analysis/session-population.mjs b/scripts/session-analysis/session-population.mjs index b9188a1..e9266a8 100644 --- a/scripts/session-analysis/session-population.mjs +++ b/scripts/session-analysis/session-population.mjs @@ -116,6 +116,7 @@ export function freezeSessionPopulation({ scope: Object.freeze(structuredClone(discovery?.scope ?? scope)), sources: Object.freeze(rows(discovery?.sources).map((source) => Object.freeze(structuredClone(source)))), warnings: Object.freeze(rows(discovery?.warnings).map((warning) => Object.freeze(structuredClone(warning)))), + ...(discovery?.providerCoverage ? { providerCoverage: Object.freeze(structuredClone(discovery.providerCoverage)) } : {}), })); return Object.freeze(population); } diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..06f2cfe --- /dev/null +++ b/settings.json @@ -0,0 +1,3 @@ +{ + "agent": "better-harness-review-director" +} diff --git a/test/agent-asset-baseline.test.mjs b/test/agent-asset-baseline.test.mjs index b0362c6..bb6744e 100644 --- a/test/agent-asset-baseline.test.mjs +++ b/test/agent-asset-baseline.test.mjs @@ -85,6 +85,7 @@ test("asset baseline shares one inventory snapshot and emits compact AI envelope { category: "project", count: 2, titleEntries: [{ title: "private title only", path: "one.md" }] }, ], }, + unsupported: ["runtime-registration", "runtime-registration", "private\nmetadata"], warnings: [], }; }, @@ -116,6 +117,7 @@ test("asset baseline shares one inventory snapshot and emits compact AI envelope route: "skills/review/SKILL.md", }); assert.equal(result.envelopes.inventory.data.memories.titleCount, 1); + assert.deepEqual(result.envelopes.inventory.data.unsupported, ["private metadata", "runtime-registration"]); assert.deepEqual(result.envelopes.inventory.data.memories.categories, [{ category: "project", count: 1 }]); assert.equal(result.envelopes.inventory.data.ownerRoutes.items.some((item) => item.kind === "plugins"), true); assert.equal(result.envelopes.inventory.data.ownerRoutes.items.some((item) => item.kind === "agents"), true); @@ -415,6 +417,7 @@ test("Pi asset baseline completes from a native project fixture", async () => { assert.equal(result.status, "complete"); assert.equal(result.scope.provider, "pi"); assert.equal(result.envelopes.inventory.status, "available"); + assert.ok(result.envelopes.inventory.data.unsupported.includes("pi manifest glob source expansion outside declared resource directories")); assert.equal(result.envelopes.lint.data.assetInventory.summary.skills, 1); assert.equal(result.envelopes.inventory.data.ownerRoutes.items.some((item) => item.kind === "skills" && item.name === "review"), true); @@ -446,6 +449,7 @@ test("WorkBuddy asset baseline completes from a native project fixture", async ( assert.equal(result.status, "complete"); assert.equal(result.scope.provider, "workbuddy"); assert.equal(result.envelopes.inventory.status, "available"); + assert.ok(result.envelopes.inventory.data.unsupported.includes("workbuddy.db and other binary state stores")); assert.equal(result.envelopes.lint.data.assetInventory.summary.skills, 1); assert.equal(result.envelopes.inventory.data.ownerRoutes.items.some((item) => item.kind === "skills" && item.name === "review"), true); diff --git a/test/agent-customize-architecture.test.mjs b/test/agent-customize-architecture.test.mjs index f0fcee2..25fa999 100644 --- a/test/agent-customize-architecture.test.mjs +++ b/test/agent-customize-architecture.test.mjs @@ -54,7 +54,7 @@ test("host architecture docs keep matrix, providers, and thin shells separate", assert.match(adapterReadme, /# Host Adapter Matrix/u); assert.match(adapterReadme, /`docs\/adapters\/qoder\.md`/u); assert.match(adapterReadme, /Codex \| Analysis-capable source-local host \| `\.codex-plugin\/`/u); - assert.match(adapterReadme, /npm package includes six filesystem metadata\s+roots for Qoder, Claude Code, Codex, Cursor, Qwen, and Copilot, plus Pi install\s+metadata in the existing `package\.json`/u); + assert.match(adapterReadme, /npm package includes native metadata for Qoder,\s+Claude Code, Codex, Cursor, Qwen, Copilot, Pi, and WorkBuddy/u); assert.match(adapterReadme, /generated\s+Qoder runtime bundle\s+includes only the Qoder shell/u); assert.match(adapterReadme, /Cursor \| Analysis-capable source-local host[^\n]+platforms\/cursor\.mjs/u); assert.doesNotMatch(adapterReadme, /Cursor has no session-evidence adapter/u); @@ -71,17 +71,17 @@ test("host architecture docs keep matrix, providers, and thin shells separate", assert.match(directoryAdr, /scripts\/packaging\/` owns source-local[\s\S]*excluded from public package\/runtime/u); assert.match(architecture, /The Codex shell\s+owns local install\/discovery metadata only/u); - assert.match(architecture, /public npm\s+package ships all six plugin metadata roots[\s\S]*Qoder runtime bundle\s+includes only `\.qoder-plugin\/`/u); + assert.match(architecture, /public npm\s+package ships all eight native plugin metadata roots[\s\S]*Qoder runtime bundle\s+includes\s+only `\.qoder-plugin\/`/u); assert.match(architecture, /do not create a generic detector or signal umbrella/u); assert.match(community, /`docs\/adapters\/README\.md` matrix row/u); - assert.match(community, /Public npm package includes all six current metadata roots[\s\S]*Qoder runtime bundle includes only `\.qoder-plugin\/`/u); + assert.match(community, /Public npm package includes all eight current native metadata roots[\s\S]*Qoder runtime bundle includes only `.qoder-plugin\//u); assert.match(community, /owning `models\/\.md`, `scripts\/\/`, or `skills\/\/references\/`/u); - assert.match(glossary, /public npm package ships all six current metadata roots[\s\S]*Qoder runtime bundle includes only `\.qoder-plugin\/`/u); + assert.match(glossary, /public npm package ships all eight current native metadata roots[\s\S]*Qoder runtime bundle includes only `.qoder-plugin\//u); assert.match(glossary, /Start with \[model routing\]\(\.\.\/models\/routing\.md\)/u); assert.match(adapterReadme, /Claude Code\s+\|/u); assert.match(adapterReadme, /GitHub Copilot \| Analysis-capable source-local host \| `\.github\/plugin\/`[^\n]+platforms\/copilot\.mjs/u); - assert.match(adapterReadme, /Pi \| Analysis-capable source-local host \| `pi` manifest[^\n]+platforms\/pi\.mjs/u); + assert.match(adapterReadme, /Pi \| Native extension-capable package \| `pi` manifest[^\n]+platforms\/pi\.mjs/u); assert.match(directoryAdr, /\.github\/plugin\/\s+# \[active\] thin GitHub Copilot shell/u); assert.doesNotMatch(community, /`docs\/adapters\/\.md`\s+\| Discovery paths/u); assert.doesNotMatch(architecture, /Do not add `\.codex-plugin\/` until Codex has/u); diff --git a/test/better-harness-evidence-bundle.test.mjs b/test/better-harness-evidence-bundle.test.mjs index 0b16130..f14ec6c 100644 --- a/test/better-harness-evidence-bundle.test.mjs +++ b/test/better-harness-evidence-bundle.test.mjs @@ -195,6 +195,35 @@ test("evidence bundle freezes the three canonical lane names and normal scope", assert.equal(result.diagnostics.collectionMode, "frozen-context-multi-owner"); }); +test("asset provider unsupported capabilities reach the frozen bundle coverage boundary", async () => { + const result = await collectEvidenceBundle({ workspace: ".", platform: "pi", depth: "quick" }, dependencies({ + collectSessionEvidence: async () => availableLane(sessionFacts({ + providerCoverage: { + provider: "pi", + status: "observed", + configured: true, + enabled: true, + observed: true, + verified: true, + unsupported: [], + unavailable: [], + sourceCoverage: { status: "observed" }, + }, + })), + collectAgentCustomize: async () => availableLane({ + kind: "agent-asset-baseline", + status: "complete", + envelopes: { + inventory: { status: "available", data: { unsupported: ["pi runtime registration", "pi runtime registration"] } }, + }, + }), + })); + + assert.equal(result.status, "complete"); + assert.deepEqual(result.diagnostics.unsupportedCapabilities, ["pi runtime registration"]); + assert.deepEqual(result.diagnostics.providerCoverage.unsupportedCapabilities, ["pi runtime registration"]); +}); + test("evidence bundle resolves topology once and shares the frozen binding with every consumer", async () => { let resolutions = 0; let canonicalTopology; diff --git a/test/docs-dx.test.mjs b/test/docs-dx.test.mjs index dfe42cf..3c1f31c 100644 --- a/test/docs-dx.test.mjs +++ b/test/docs-dx.test.mjs @@ -50,7 +50,7 @@ test("installation prerequisites and verification paths stay aligned across loca assert.match(installation, /## Prerequisites \{#prerequisites\}/u); assert.match(installationZh, /## 前置条件 \{#prerequisites\}/u); for (const content of [installation, installationZh]) { - assert.match(content, /Node\.js `>=22\.20\.0 <25\.0\.0`/u); + assert.match(content, /Node\.js `>=22\.20\.0 <26\.0\.0`/u); assert.match(content, /npm `>=10\.9\.3 <12\.0\.0`/u); assert.match(content, /Windows/u); assert.match(content, /macOS/u); diff --git a/test/docs-entrypoints.test.mjs b/test/docs-entrypoints.test.mjs index 2bec2bb..40adfe3 100644 --- a/test/docs-entrypoints.test.mjs +++ b/test/docs-entrypoints.test.mjs @@ -294,8 +294,8 @@ test("public adapter matrix documents all eight adapters and their support bound assert.match( matrix, - /all six plugin metadata\nroots/u, - "docs/docs/hosts/adapter-matrix.md does not declare six plugin metadata roots", + /native metadata for all\neight adapters/u, + "docs/docs/hosts/adapter-matrix.md does not declare eight native adapter metadata roots", ); assert.match( @@ -318,8 +318,8 @@ test("zh-Hans public adapter matrix documents all eight adapters and their suppo assert.match( matrix, - /全部六个插件元数据根目录/u, - "zh-Hans adapter-matrix.md does not declare six plugin metadata roots", + /八个宿主的原生元数据/u, + "zh-Hans adapter-matrix.md does not declare eight native adapter metadata roots", ); assert.match( diff --git a/test/docs-site.test.mjs b/test/docs-site.test.mjs index 24e6b4e..1a5b83b 100644 --- a/test/docs-site.test.mjs +++ b/test/docs-site.test.mjs @@ -245,8 +245,8 @@ test("homepage leads search visitors from proof to a host-specific setup", async assert.match(zh["homepage.hosts.qoder.setup"].message, /Qoder CLI.*单独安装/u); assert.equal(zh["homepage.hosts.qoder.method"].message, "Desktop 内置"); assert.equal(zh["homepage.hosts.output.canvas"].message, "Canvas 报告"); - assert.match(zh["homepage.hosts.pi.setup"].message, /完整交互式报告闭环/u); - assert.match(zh["homepage.hosts.workBuddy.setup"].message, /WorkBuddy 自有路径/u); + assert.match(zh["homepage.hosts.pi.setup"].message, /原生扩展.*真实模型 E2E/u); + assert.match(zh["homepage.hosts.workBuddy.setup"].message, /codebuddy --plugin-dir/u); assert.equal(zh["homepage.hosts.status.quickstart"].message, "已验证快速开始"); assert.equal(zh["homepage.hosts.status.adapter"].message, "适配器支持"); assert.equal( diff --git a/test/fixtures/scripts-refactor-contract/harness-help.txt b/test/fixtures/scripts-refactor-contract/harness-help.txt index 3d1a468..098188a 100644 --- a/test/fixtures/scripts-refactor-contract/harness-help.txt +++ b/test/fixtures/scripts-refactor-contract/harness-help.txt @@ -9,6 +9,9 @@ Subcommands: Audience: maintainer evidence-bundle Collect the three specialist lanes and lead evidence in one frozen-context bundle + host-doctor Check a Pi or WorkBuddy host before independent specialist execution + prepare-run Freeze one private three-lane host run plan + verify-run Verify three independent specialist results against a run plan workspace-topology Resolve the Git-aware workspace target and member topology analyze Return a neutral, budgeted Harness evidence brief checkup Scan agent customizations and produce a read-only cleanup plan @@ -29,6 +32,7 @@ Examples: better-harness harness workspace-topology --workspace . --json better-harness harness source --workspace . --source /report.source.json --language en better-harness harness render --findings /findings.json --mode qoder-canvas --out .qoder/better-harness --target . --validate --json + better-harness harness host-doctor --platform pi --workspace . --json better-harness harness preview-canvas /report.canvas.tsx --open better-harness harness report-quality --report /report.md better-harness harness validate-canvas --canvas /report.canvas.tsx diff --git a/test/fixtures/scripts-refactor-contract/root-help.txt b/test/fixtures/scripts-refactor-contract/root-help.txt index 89dd275..db8196d 100644 --- a/test/fixtures/scripts-refactor-contract/root-help.txt +++ b/test/fixtures/scripts-refactor-contract/root-help.txt @@ -15,10 +15,10 @@ Commands: Workflows harness Check Better Harness readiness reports and Canvas outputs - subcommands: evidence-bundle, workspace-topology, analyze, checkup, - selection-profile, source, task-loop-report, render, - preview-canvas, report-quality, repair-findings, record-fix-output, - validate-canvas + subcommands: evidence-bundle, host-doctor, prepare-run, verify-run, + workspace-topology, analyze, checkup, selection-profile, source, + task-loop-report, render, preview-canvas, report-quality, + repair-findings, record-fix-output, validate-canvas Project Evidence session-analysis Collect and normalize Qoder, Codex, Claude, Cursor, Qwen, Copilot, @@ -47,6 +47,7 @@ Examples: better-harness harness checkup --phase scan --provider qoder --workspace . --json better-harness harness render --findings /findings.json --mode qoder-canvas --out .qoder/better-harness --target . --validate --json better-harness harness report-quality --report /report.md + better-harness harness host-doctor --platform pi --workspace . --json better-harness harness validate-canvas --canvas /report.canvas.tsx better-harness core-change-watch project-profile --json better-harness core-change-watch change-drift --json diff --git a/test/harness-report-source.test.mjs b/test/harness-report-source.test.mjs index 4aa75dc..114b7d9 100644 --- a/test/harness-report-source.test.mjs +++ b/test/harness-report-source.test.mjs @@ -127,3 +127,32 @@ test("report source accepts only versioned, privacy-safe semantic facets", () => }] }; assert.ok(validateHarnessReportSource(invalid).some((error) => error.includes("rawPrompt"))); }); + +test("report source preserves provider unsupported capabilities and rejects unknown coverage fields", () => { + const source = createHarnessReportSource({ + manifest: buildObservationManifest({ + scope: { platform: "pi", workspace: "/workspace/example" }, + eligibleCount: 1, + analyzedCount: 1, + selectionStrategy: "all-eligible", + providerCoverage: { + provider: "pi", + status: "observed", + configured: true, + enabled: true, + observed: true, + verified: true, + unsupported: [], + unsupportedCapabilities: ["native MCP inventory"], + unavailable: [], + sourceCoverage: { status: "observed" }, + }, + }), + }); + assert.deepEqual(validateHarnessReportSource(source), []); + assert.deepEqual(source.manifest.providerCoverage.unsupportedCapabilities, ["native MCP inventory"]); + + const malformed = structuredClone(source); + malformed.manifest.providerCoverage.extra = true; + assert.ok(validateHarnessReportSource(malformed).some((error) => error.includes("providerCoverage has unsupported field: extra"))); +}); diff --git a/test/host-runtime-cli.test.mjs b/test/host-runtime-cli.test.mjs new file mode 100644 index 0000000..664cee6 --- /dev/null +++ b/test/host-runtime-cli.test.mjs @@ -0,0 +1,123 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { main } from "../scripts/harness-analysis/host-runtime/cli.mjs"; + +async function captureMain(argv, dependencies = {}) { + const stdout = []; + const stderr = []; + const originalOut = process.stdout.write; + const originalErr = process.stderr.write; + process.stdout.write = (chunk) => { + stdout.push(String(chunk)); + return true; + }; + process.stderr.write = (chunk) => { + stderr.push(String(chunk)); + return true; + }; + try { + const exitCode = await main(argv, dependencies); + return { exitCode, stdout: stdout.join(""), stderr: stderr.join("") }; + } finally { + process.stdout.write = originalOut; + process.stderr.write = originalErr; + } +} + +function fakeBundle(depth = "normal") { + return { + kind: "better-harness.evidence-bundle", + schemaVersion: 2, + status: "complete", + context: { provider: "pi", depth, workspace: process.cwd() }, + lanes: { + sessionEvidence: { status: "available", data: { lane: "session" } }, + projectHarness: { status: "available", data: { lane: "project" } }, + agentCustomize: { status: "available", data: { lane: "customize" } }, + }, + lead: { status: "available", data: { summary: "fixture" } }, + diagnostics: {}, + }; +} + +test("host-doctor emits a passing JSON contract for an accessible Pi workspace", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-host-doctor-")); + try { + const result = await captureMain([ + "host-doctor", + "--platform", "pi", + "--workspace", root, + "--model", "fixture-model", + "--exclude-session-id", "fixture-session", + ]); + assert.equal(result.exitCode, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.ok, true); + assert.equal(payload.data.status, "pass"); + assert.equal(payload.data.provider, "pi"); + assert.ok(payload.data.checks.some((check) => check.name === "runtime-resources" && check.status === "pass")); + assert.equal(result.stderr, ""); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("host-doctor exposes fail status as ok=false while retaining diagnostic data", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-host-doctor-fail-")); + try { + const result = await captureMain(["host-doctor", "--platform", "not-a-host", "--workspace", root]); + assert.equal(result.exitCode, 1); + const payload = JSON.parse(result.stdout); + assert.equal(payload.ok, false); + assert.equal(payload.data.status, "fail"); + assert.ok(payload.data.checks.some((check) => check.name === "provider" && check.status === "fail")); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("prepare-run writes only the bounded plan and verify-run dispatches it", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-host-runtime-cli-")); + const planPath = path.join(root, "run-plan.json"); + const resultsPath = path.join(root, "results.json"); + try { + const prepared = await captureMain([ + "prepare-run", + "--platform", "pi", + "--workspace", process.cwd(), + "--output", planPath, + ], { + collectEvidenceBundle: async () => fakeBundle("normal"), + }); + assert.equal(prepared.exitCode, 0, prepared.stderr); + const preparedPayload = JSON.parse(prepared.stdout); + assert.equal(preparedPayload.ok, true); + assert.deepEqual(preparedPayload.data.lanes, ["sessionEvidence", "projectHarness", "agentCustomize"]); + const planEnvelope = JSON.parse(await readFile(planPath, "utf8")); + const plan = planEnvelope.plan; + const results = plan.expected.laneNames.map((lane, index) => ({ + lane, + contextId: `fixture-${index}`, + status: "completed", + inputHash: plan.lanes[lane].inputHash, + output: { findingCount: index }, + })); + await import("node:fs/promises").then(({ writeFile }) => writeFile(resultsPath, `${JSON.stringify(results)}\n`)); + const verified = await captureMain([ + "verify-run", + "--platform", "pi", + "--plan", planPath, + "--results", resultsPath, + ]); + assert.equal(verified.exitCode, 0, verified.stderr); + const verifiedPayload = JSON.parse(verified.stdout); + assert.equal(verifiedPayload.ok, true); + assert.equal(verifiedPayload.data.results.length, 3); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/test/host-runtime-contract.test.mjs b/test/host-runtime-contract.test.mjs new file mode 100644 index 0000000..2fde271 --- /dev/null +++ b/test/host-runtime-contract.test.mjs @@ -0,0 +1,137 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + createRunPlan, + hashJson, + SPECIALIST_LANES, + verifyRunResults, +} from "../scripts/harness-analysis/host-runtime/contract.mjs"; + +function makeBundle(depth = "normal") { + return { + kind: "better-harness.evidence-bundle", + context: { + provider: "pi", + depth, + workspace: "/tmp/better-harness-fixture", + }, + lanes: { + sessionEvidence: { status: "available", data: { owner: "session", count: 1 } }, + projectHarness: { status: "available", data: { owner: "project", count: 2 } }, + agentCustomize: { status: "available", data: { owner: "agent", count: 3 } }, + }, + lead: { status: "available", data: { summary: "bounded" } }, + diagnostics: { coverage: { pi: "observed" } }, + }; +} + +function completeResults(plan, overrides = {}) { + return SPECIALIST_LANES.map((lane, index) => ({ + lane, + contextId: `pi-child-${index + 1}`, + status: overrides[lane]?.status ?? "completed", + inputHash: plan.lanes[lane].inputHash, + output: overrides[lane]?.output ?? { lane, findingCount: index }, + })); +} + +test("HRC-AC-01 creates exactly three independently hashed lanes", () => { + const plan = createRunPlan(makeBundle()); + + assert.deepEqual(plan.expected.laneNames, ["sessionEvidence", "projectHarness", "agentCustomize"]); + assert.equal(plan.expected.laneCount, 3); + assert.equal(Object.keys(plan.lanes).length, 3); + assert.equal(new Set(Object.values(plan.lanes).map((lane) => lane.inputHash)).size, 3); + for (const lane of SPECIALIST_LANES) { + assert.equal(plan.lanes[lane].inputHash, hashJson(plan.lanes[lane].input)); + } +}); + +test("HRC-AC-02 accepts three distinct structured specialist results", () => { + const plan = createRunPlan(makeBundle()); + const verified = verifyRunResults(plan, completeResults(plan)); + + assert.equal(verified.ok, true); + assert.equal(verified.results.length, 3); + assert.equal(verified.diagnostics.observedContextCount, 3); + assert.deepEqual(verified.diagnostics.errors, []); +}); + +test("HRC-AC-02 keeps valid results after an earlier malformed result", () => { + const plan = createRunPlan(makeBundle()); + const results = completeResults(plan); + results[0].output = "not an object"; + + const verified = verifyRunResults(plan, results); + + assert.equal(verified.ok, false); + assert.equal(verified.results.length, 2); + assert.deepEqual(verified.results.map((result) => result.lane), ["projectHarness", "agentCustomize"]); + assert.ok(verified.diagnostics.errors.some((error) => error.code === "INVALID_SPECIALIST_OUTPUT")); +}); + +test("HRC-AC-02 rejects duplicate contexts, duplicate lanes, and hash drift", () => { + const plan = createRunPlan(makeBundle()); + const results = completeResults(plan); + results[1].contextId = results[0].contextId; + results[2].lane = results[1].lane; + results[2].inputHash = "wrong"; + + const verified = verifyRunResults(plan, results); + const codes = new Set(verified.diagnostics.errors.map((error) => error.code)); + + assert.equal(verified.ok, false); + assert.ok(codes.has("DUPLICATE_SPECIALIST_CONTEXT")); + assert.ok(codes.has("DUPLICATE_SPECIALIST_LANE")); + assert.ok(codes.has("SPECIALIST_INPUT_HASH_MISMATCH")); + assert.ok(codes.has("MISSING_SPECIALIST_LANE")); +}); + +test("HRC-AC-02 rejects raw runtime fields in lanes and specialist output", () => { + assert.throws( + () => createRunPlan({ + ...makeBundle(), + lanes: { + ...makeBundle().lanes, + sessionEvidence: { status: "available", data: { rawSession: "secret" } }, + }, + }), + /forbidden runtime fields/u, + ); + + const plan = createRunPlan(makeBundle()); + const results = completeResults(plan); + results[0].output = { credential: "secret" }; + const verified = verifyRunResults(plan, results); + assert.equal(verified.ok, false); + assert.ok(verified.diagnostics.errors.some((error) => error.code === "FORBIDDEN_SPECIALIST_FIELDS")); +}); + +test("HRC-AC-03 blocks partial normal runs but preserves quick gaps", () => { + const normalPlan = createRunPlan(makeBundle("normal")); + const normal = verifyRunResults(normalPlan, completeResults(normalPlan, { + projectHarness: { status: "partial" }, + })); + assert.equal(normal.ok, false); + assert.deepEqual(normal.diagnostics.incompleteLanes, ["projectHarness"]); + assert.equal(normal.diagnostics.confidence, "low"); + + const quickPlan = createRunPlan(makeBundle("quick")); + const quick = verifyRunResults(quickPlan, completeResults(quickPlan, { + projectHarness: { status: "partial" }, + })); + assert.equal(quick.ok, true); + assert.deepEqual(quick.diagnostics.incompleteLanes, ["projectHarness"]); + assert.equal(quick.diagnostics.confidence, "low"); +}); + +test("HRC-AC-03 rejects missing lanes even in quick mode", () => { + const plan = createRunPlan(makeBundle("quick")); + const results = completeResults(plan).slice(0, 2); + const verified = verifyRunResults(plan, results); + + assert.equal(verified.ok, false); + assert.ok(verified.diagnostics.errors.some((error) => error.code === "SPECIALIST_COUNT_MISMATCH")); + assert.ok(verified.diagnostics.errors.some((error) => error.code === "MISSING_SPECIALIST_LANE")); +}); diff --git a/test/pi-extension.test.mjs b/test/pi-extension.test.mjs new file mode 100644 index 0000000..b1a6fed --- /dev/null +++ b/test/pi-extension.test.mjs @@ -0,0 +1,155 @@ +import assert from "node:assert/strict"; +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; +import test from "node:test"; + +import activate, { + buildRpcArgs, + executeBetterHarness, + parseReviewRequest, + PI_RPC_FLAGS, + runPiRpcLane, +} from "../extensions/pi/better-harness.ts"; + +function fakeChild({ result, malformed = false, pid = 7001 } = {}) { + const child = new EventEmitter(); + child.pid = pid; + child.killed = false; + child.stdin = new PassThrough(); + child.stdout = new PassThrough(); + child.stderr = new PassThrough(); + child.kill = () => { + child.killed = true; + queueMicrotask(() => child.emit("close", 0)); + return true; + }; + child.stdin.on("data", () => { + queueMicrotask(() => { + const event = malformed + ? "not-json\n" + : [ + JSON.stringify({ type: "response", command: "prompt", success: true }), + JSON.stringify({ type: "message_end", message: { content: [{ type: "text", text: JSON.stringify(result) }] } }), + JSON.stringify({ type: "agent_end" }), + ].join("\n") + "\n"; + child.stdout.write(event); + }); + }); + return child; +} + +test("Pi extension parses only bounded review flags", () => { + assert.deepEqual(parseReviewRequest("--quick --language zh-CN inspect delivery"), { + depth: "quick", + language: "zh-CN", + request: "inspect delivery", + }); + assert.throws(() => parseReviewRequest("--shell rm -rf"), /unsupported \/better-harness option/u); + assert.throws(() => parseReviewRequest("--language"), /requires a locale/u); +}); + +test("Pi RPC child receives safety flags and lane evidence only through stdin", async () => { + const input = { bounded: true }; + const result = await runPiRpcLane({ + lane: "sessionEvidence", + inputHash: "hash-session", + input, + cwd: process.cwd(), + model: "openai/gpt-test", + spawn: (file, args, options) => { + assert.equal(file, "fake-pi"); + assert.deepEqual(args, [...PI_RPC_FLAGS, "--model", "openai/gpt-test"]); + assert.equal(options.cwd, process.cwd()); + assert.equal(args.includes(JSON.stringify(input)), false); + return fakeChild({ + pid: 7101, + result: { + lane: "sessionEvidence", + contextId: "child-context-1", + status: "completed", + inputHash: "hash-session", + output: { findingCount: 0 }, + }, + }); + }, + executable: "fake-pi", + }); + + assert.equal(result.lane, "sessionEvidence"); + assert.equal(result.pid, 7101); + assert.equal(result.inputHash, "hash-session"); +}); + +test("Pi RPC malformed JSON fails closed", async () => { + await assert.rejects( + runPiRpcLane({ + lane: "projectHarness", + inputHash: "hash-project", + input: {}, + cwd: process.cwd(), + spawn: () => fakeChild({ malformed: true, pid: 7102 }), + }), + (error) => error.code === "RPC_MALFORMED_JSON", + ); +}); + +test("Pi command executes doctor, one prepare, three lanes, verify, and lead injection", async () => { + const calls = []; + const plan = { + runId: "run-fixture", + provider: "pi", + depth: "quick", + lead: { data: { summary: "lead" } }, + lanes: { + sessionEvidence: { input: { lane: "session" }, inputHash: "h1" }, + projectHarness: { input: { lane: "project" }, inputHash: "h2" }, + agentCustomize: { input: { lane: "customize" }, inputHash: "h3" }, + }, + }; + const ctx = { + cwd: process.cwd(), + model: { provider: "openai", id: "gpt-test" }, + thinkingLevel: "high", + signal: undefined, + sessionManager: { getSessionId: () => "current-session" }, + ui: { notify: () => {} }, + }; + const result = await executeBetterHarness(ctx, "--quick review", { + hostDoctor: async (options) => { + calls.push(["doctor", options]); + return { status: "pass", checks: [] }; + }, + prepareHostRun: async (options) => { + calls.push(["prepare", options]); + return { status: "partial", plan }; + }, + runPiSpecialists: async (receivedPlan, options) => { + calls.push(["specialists", receivedPlan, options]); + return ["sessionEvidence", "projectHarness", "agentCustomize"].map((lane, index) => ({ + lane, + contextId: `ctx-${index}`, + status: "completed", + inputHash: `h${index + 1}`, + output: { lane }, + })); + }, + verifyHostRun: (receivedPlan, results) => { + calls.push(["verify", receivedPlan, results]); + return { ok: true, results, diagnostics: { confidence: "normal", errors: [] } }; + }, + sendMessage: async (message) => calls.push(["send", message]), + }); + + assert.equal(result.runId, "run-fixture"); + assert.deepEqual(calls.map(([kind]) => kind), ["doctor", "prepare", "specialists", "verify", "send"]); + assert.match(calls.at(-1)[1], /three isolated lanes/u); + assert.match(calls.at(-1)[1], /gpt-test/u); + assert.equal(calls[0][1]["exclude-session-id"], "current-session"); +}); + +test("Pi activation registers exactly one canonical command", () => { + const registrations = []; + activate({ registerCommand: (name, options) => registrations.push({ name, options }) }); + assert.equal(registrations.length, 1); + assert.equal(registrations[0].name, "better-harness"); +}); diff --git a/test/plugin-manifests.test.mjs b/test/plugin-manifests.test.mjs index e322cb7..7d12de6 100644 --- a/test/plugin-manifests.test.mjs +++ b/test/plugin-manifests.test.mjs @@ -100,6 +100,8 @@ test("Cursor manifests conform to the pinned official schemas", () => { test("host plugin manifests expose canonical Better Harness resources", () => { const qoder = readJson(".qoder-plugin/plugin.json"); + const workbuddy = readJson(".codebuddy-plugin/plugin.json"); + const workbuddyMarketplace = readJson(".codebuddy-plugin/marketplace.json"); const claude = readJson(".claude-plugin/plugin.json"); const claudeMarketplace = readJson(".claude-plugin/marketplace.json"); const cursor = readJson(".cursor-plugin/plugin.json"); @@ -119,6 +121,16 @@ test("host plugin manifests expose canonical Better Harness resources", () => { assert.equal(qoder.name, "better-harness"); assert.equal(qoder.displayName, "Better Harness"); assert.equal(qoder.version, packageJson.version); + assert.equal(workbuddy.name, qoder.name); + assert.equal(workbuddy.version, qoder.version); + assert.equal(workbuddy.expertType, "team"); + assert.equal(workbuddy.categoryId, "10-ProjectQuality"); + assert.equal(workbuddy.teamInfo.memberAgents.length, 3); + assert.equal(workbuddy.teamInfo.memberAgents.includes(workbuddy.teamInfo.leadAgent), false); + assert.deepEqual(workbuddy.defaultInitPrompt, workbuddy.quickPrompts[0]); + assert.equal(workbuddy.tags.length, 3); + assert.equal(workbuddyMarketplace.plugins[0].version, packageJson.version); + assert.equal(readJson("settings.json").agent, workbuddy.teamInfo.leadAgent); assert.equal(claude.name, qoder.name); assert.equal(claude.version, qoder.version); @@ -171,7 +183,14 @@ test("host plugin manifests expose canonical Better Harness resources", () => { assert.equal(copilot.hooks, undefined); assert.equal(copilot.license, "MIT"); - assert.deepEqual(packageJson.pi, { skills: ["./skills"], prompts: ["./prompts"] }); + assert.deepEqual(packageJson.pi, { + extensions: ["./extensions/pi/better-harness.ts"], + skills: ["./skills"], + prompts: [], + }); + assertExistingPath("extensions/pi/better-harness.ts"); + assert.equal(packageJson.peerDependencies["@earendil-works/pi-coding-agent"], "*"); + assert.match(packageJson.engines.node, /<26\.0\.0/u); assert.ok(packageJson.keywords.includes("pi-package"), "package keywords should mark the pi package"); assert.equal(cursor.license, "MIT"); assert.equal(qoder.license, "MIT"); @@ -237,6 +256,7 @@ test("npm packaging includes every host manifest while the runtime bundle stays ".cursor-plugin/", ".github/plugin/", ".qoder-plugin/", + ".codebuddy-plugin/", "qwen-extension.json", "prompts/", "AGENTS.md", diff --git a/test/scripts-refactor-contract.test.mjs b/test/scripts-refactor-contract.test.mjs index 1dc3155..64a8ae5 100644 --- a/test/scripts-refactor-contract.test.mjs +++ b/test/scripts-refactor-contract.test.mjs @@ -107,17 +107,20 @@ test("scripts refactor contract freezes machine-readable CLI output", () => { { label: "command inventory", args: ["commands", "--json"], - sha256: "e86c8b3447f24cf1f6a94f7ca74c48c5d194aa54bad736ff02d7dd64e566dc99", + // Host runtime exposes the three canonical preparation/verification + // commands; freeze the new inventory contract rather than masking the + // change behind a stale baseline. + sha256: "9d85607dbb09ff73c2cde3f8ba66b28388705a90bebbc680549c65d70411094d", }, { label: "OpenCLI schema", args: ["schema"], - sha256: "b6b28fede8f135e598bc2278a36d2c94660edfe1b384c14f6e5da73c394a3347", + sha256: "73a51305a14c5ee9406b8f8646514f976c4d6a3eacc5b755fd9e68f0970fb54f", }, { label: "Harness command description", args: ["command", "describe", "harness", "--json"], - sha256: "7bfdf90239d7c021dc97375a6b82c40ea435847fe57e2774fcdc56ec7c56bacd", + sha256: "a8de71d9a8568618febeb153b61565b904e9b1dc1a0caaaeaccec869e4901386", }, ]; diff --git a/test/session-analysis-providers.test.mjs b/test/session-analysis-providers.test.mjs index a15f7c9..e1a0967 100644 --- a/test/session-analysis-providers.test.mjs +++ b/test/session-analysis-providers.test.mjs @@ -1045,6 +1045,31 @@ test("Pi custom session roots require a directory", async () => { assert.equal(result.sessions.length, 0); }); +test("Pi malformed first records and unknown header versions remain partial evidence", async () => { + const root = await fixtureRoot("session-pi-schema-diagnostics-"); + const home = path.join(root, ".pi", "agent"); + const workspace = path.join(root, "workspace", "target"); + const dirName = workspaceToPiSessionDirVariants(workspace).exact; + await mkdir(path.join(home, "sessions", dirName), { recursive: true }); + await writeFile(path.join(home, "sessions", dirName, "malformed.jsonl"), [ + "not-json", + JSON.stringify({ type: "session", version: 3, id: "late-header", timestamp: "2026-07-20T01:00:00.000Z", cwd: workspace }), + ].join("\n") + "\n"); + await writeJsonl(path.join(home, "sessions", dirName, "unknown-version.jsonl"), [ + { type: "session", version: 4, id: "unknown-version", timestamp: "2026-07-20T01:00:00.000Z", cwd: workspace }, + { type: "message", id: "v4-message", timestamp: "2026-07-20T01:00:01.000Z", message: { role: "user", content: "not admitted" } }, + ]); + + const result = await new PiSessionAnalyzer().analyze({ command: "sources", workspace, home }); + assert.equal(result.sessions.length, 0); + assert.equal(result.providerCoverage.status, "unsupported"); + assert.ok(result.providerCoverage.unsupported.includes("pi-session-schema-version")); + assert.equal(result.providerCoverage.schemaDiagnostics.unknownVersionCount, 1); + assert.equal(result.providerCoverage.schemaDiagnostics.invalidHeaderCount, 2); + assert.equal(result.providerCoverage.schemaDiagnostics.invalidRecordCount, 1); + assert.equal(result.providerCoverage.schemaDiagnostics.status, "partial"); +}); + test("WorkBuddy provider expands tool calls, tool results, and usage from JSONL transcripts", async () => { const root = await fixtureRoot("session-workbuddy-provider-"); const home = path.join(root, ".workbuddy"); diff --git a/test/task-loop-source.test.mjs b/test/task-loop-source.test.mjs index 2dfbcbc..147046e 100644 --- a/test/task-loop-source.test.mjs +++ b/test/task-loop-source.test.mjs @@ -187,6 +187,26 @@ function candidate(events, insights = {}, options = {}) { }); } +test("task-loop source carries asset unsupported capabilities into the observation manifest", () => { + const source = candidate([], {}, { + providerCoverage: { + provider: "pi", + status: "observed", + configured: true, + enabled: true, + observed: true, + verified: true, + unsupported: [], + unavailable: [], + sourceCoverage: { status: "observed" }, + }, + unsupportedCapabilities: ["native MCP inventory"], + }); + + assert.deepEqual(source.manifest.providerCoverage.unsupportedCapabilities, ["native MCP inventory"]); + assert.deepEqual(validateHarnessReportSource(source), []); +}); + test("routine permission volume stays aggregate and does not retain permission-only Episodes", () => { const start = Date.parse("2026-07-10T10:00:00.000Z"); const events = Array.from({ length: 171 }, (_, index) => event({ diff --git a/test/workbuddy-plugin.test.mjs b/test/workbuddy-plugin.test.mjs new file mode 100644 index 0000000..5bec806 --- /dev/null +++ b/test/workbuddy-plugin.test.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import { readFile, rm } from "node:fs/promises"; +import { mkdtemp } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + buildWorkBuddyPluginArtifact, + verifyWorkBuddyPluginRoot, + WORKBUDDY_REQUIRED_PATHS, +} from "../scripts/packaging/workbuddy-plugin.mjs"; + +function zipEntryNames(buffer) { + const entries = new Set(); + let offset = 0; + while (offset < buffer.length && buffer.readUInt32LE(offset) === 0x04034b50) { + const compressedSize = buffer.readUInt32LE(offset + 18); + const nameLength = buffer.readUInt16LE(offset + 26); + const extraLength = buffer.readUInt16LE(offset + 28); + const nameStart = offset + 30; + entries.add(buffer.toString("utf8", nameStart, nameStart + nameLength)); + offset = nameStart + nameLength + extraLength + compressedSize; + } + return entries; +} + +test("WB-AC-01 validates the native team manifest, lead settings, and three members", async () => { + const result = await verifyWorkBuddyPluginRoot(); + assert.equal(result.name, "better-harness"); + assert.equal(result.agentCount, 4); + assert.equal(result.memberCount, 3); +}); + +test("WB-AC-06 and WB-AC-08 build an isolated archive without private host state", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-workbuddy-artifact-")); + const archive = path.join(root, "better-harness.zip"); + try { + const result = await buildWorkBuddyPluginArtifact({ outputRoot: archive }); + assert.equal(result.archive, archive); + const entries = zipEntryNames(await readFile(archive)); + for (const required of WORKBUDDY_REQUIRED_PATHS) assert.ok(entries.has(required), required); + assert.equal([...entries].some((entry) => entry.startsWith(".workbuddy/")), false); + assert.equal([...entries].some((entry) => entry.startsWith("node_modules/")), false); + assert.equal([...entries].some((entry) => entry.includes("/Users/") || entry.includes("/home/")), false); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("WB-AC-02 builds a replaceable directory artifact with no symlinks", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-workbuddy-dir-")); + const output = path.join(root, "better-harness"); + try { + const first = await buildWorkBuddyPluginArtifact({ outputRoot: output }); + assert.equal(first.pluginRoot, output); + const verified = await verifyWorkBuddyPluginRoot(output); + assert.equal(verified.version, "0.4.0"); + const second = await buildWorkBuddyPluginArtifact({ outputRoot: output }); + assert.equal(second.pluginRoot, output); + } finally { + await rm(root, { recursive: true, force: true }); + } +});