${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