diff --git a/CHANGELOG.md b/CHANGELOG.md
index 312686d..152dc3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ changes.
## [Unreleased]
+No unreleased changes.
+
+## [0.2.0] - 2026-10-09
+
### Added
- ACP v1 subprocess Provider through the official SDK, with correlated human permissions,
@@ -33,6 +37,8 @@ changes.
cleanup errors and timeouts included in the report.
- Updated Hono to 4.13.7 to clear four production dependency advisories, including
[GHSA-hxh3-vqpv-xpqv](https://github.com/honojs/hono/security/advisories/GHSA-hxh3-vqpv-xpqv).
+- Subprocess integration tests run serially with bounded startup allowances instead of assuming
+ idle-machine latency. Synthetic ACP traces use atomic replacement to avoid partial JSON reads.
### Documentation
@@ -44,6 +50,18 @@ changes.
- Added focused guides for one complete execution, provider ownership, and production deployment.
- Reorganized the documentation map around evaluation, integration, provider development, and
operations paths.
+- Corrected both lifecycle diagrams to include failure while awaiting input or approval, with a
+ deterministic generator and release-version consistency tests.
+
+### Migration From 0.1.0
+
+- DSH launch options must be set by the trusted Provider constructor; per-execution launch
+ overrides are rejected. Install exactly `@deepseek-ai/dsh-sdk-client@0.1.0-rc.7` and explicitly
+ declare only extensions verified in your deployment.
+- Catch `HarnessRuntimeStreamInterruptedError` when an SSE stream ends before the execution is
+ terminal. Reconnect using the event cursor or query execution state before retrying work.
+- The portable protocol remains `2026-09-02`. Reference storage remains in-memory; ACP is
+ experimental, and workspace packages are not published to npm.
## [0.1.0] - 2026-09-02
diff --git a/README.md b/README.md
index 38ba9fd..d72d8bd 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
English | 简体中文
-> **Experimental:** runtime `0.1.0`, protocol `2026-09-02`. The reference implementation is
+> **Experimental:** runtime `0.2.0`, protocol `2026-09-02`. The reference implementation is
> in-memory, unauthenticated, and intended for development or one-process embedding.

diff --git a/README.zh-CN.md b/README.zh-CN.md
index dfbde17..13d378f 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -8,7 +8,7 @@
English | 简体中文
-> **实验阶段:** Runtime `0.1.0`,协议 `2026-09-02`。当前参考实现使用内存状态、没有身份认证,
+> **实验阶段:** Runtime `0.2.0`,协议 `2026-09-02`。当前参考实现使用内存状态、没有身份认证,
> 适合本地开发、测试或单进程嵌入,不是可直接暴露到公网的多租户平台。

diff --git a/RELEASING.md b/RELEASING.md
index e403308..6f15feb 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -29,6 +29,17 @@ pnpm audit --prod --audit-level high
## GitHub Release
+The first experimental release uses a source-only archive; it does not bundle dependencies,
+credentials, local logs, or prebuilt binaries. From a clean reviewed commit, run:
+
+```bash
+node scripts/package-release.mjs /tmp/harness-runtime-release
+```
+
+The output contains a Git archive, `release-manifest.json` bound to the exact commit, and
+`SHA256SUMS`. Verify the archive in an isolated checkout using the frozen lockfile and documented
+checks. Publish as a prerelease while the adapters and reference implementation are experimental.
+
After the exact tag and commit SHA are approved:
1. create an annotated `vX.Y.Z` tag at the reviewed commit;
diff --git a/assets/README.md b/assets/README.md
index 7d73111..ee8f16f 100644
--- a/assets/README.md
+++ b/assets/README.md
@@ -23,3 +23,9 @@ image and normative text disagree, schemas and specification documents win.
| `capability-preflight-zh.svg` | Chinese | T0 deterministic | Exact capability decision flow |
AnyCap-generated PNGs retain a neighboring `.prompt.md` production and validation record.
+
+The lifecycle SVGs are generated by `node scripts/generate-lifecycle.mjs`. Use `--check` to verify
+reproducibility without writing files. On 2026-10-09, both languages were inspected in Chrome at
+1600 x 900 and 1024 x 576: nine states, twelve transitions, readable labels, and no visible overlap.
+Failure paths cover running and both human-wait states; the normative runtime model remains the
+source of truth. Regression tests check these paths and generator synchronization.
diff --git a/assets/execution-lifecycle-en.svg b/assets/execution-lifecycle-en.svg
index 18c6ddd..75db9e9 100644
--- a/assets/execution-lifecycle-en.svg
+++ b/assets/execution-lifecycle-en.svg
@@ -1,66 +1,50 @@
diff --git a/assets/execution-lifecycle-zh.svg b/assets/execution-lifecycle-zh.svg
index e267bb7..4e49c1e 100644
--- a/assets/execution-lifecycle-zh.svg
+++ b/assets/execution-lifecycle-zh.svg
@@ -1,66 +1,50 @@
diff --git a/docs/development/delivery-plan.md b/docs/development/delivery-plan.md
index fca4035..0dee88b 100644
--- a/docs/development/delivery-plan.md
+++ b/docs/development/delivery-plan.md
@@ -17,10 +17,10 @@ delivery does not claim those features exist in the reference server.
malformed inputs, rather than only in-process request helpers.
- [x] Runnable examples cover application integration and provider implementation; a fresh checkout
can follow documented commands without private services or model credentials.
-- [ ] Protocol, OpenAPI, bilingual README, provider guides, and release materials agree with code.
-- [ ] The final tree, public history, dependency inventory, and publishable artifacts pass privacy
+- [x] Protocol, OpenAPI, bilingual README, provider guides, and release materials agree with code.
+- [x] The final tree, public history, dependency inventory, and publishable artifacts pass privacy
and security review; findings and exclusions are stated explicitly.
-- [ ] Local checks and the reviewed GitHub commit's CI pass; the worktree is clean and synchronized.
+- [x] Local checks and the reviewed GitHub commit's CI pass; the worktree is clean and synchronized.
- [ ] The exact release candidate, version, packaging decision, and any required publication
authorization are recorded before claiming final delivery.
@@ -46,14 +46,41 @@ delivery does not claim those features exist in the reference server.
## Remaining Work
-1. Finish documentation/visual consistency review for the complete delivery, including failure
- while awaiting a human Action.
-2. Finish source/history/media/artifact privacy review and release packaging/version decisions.
-3. Run final local and GitHub checks for the complete delivery, verify fresh-checkout instructions,
- then prepare the exact candidate required by the release authorization policy.
+1. Record the final merged commit for `v0.2.0` and obtain separate immutable-tag/GitHub prerelease
+ publication approval. Source-only packaging is prepared; npm publication is not part of delivery.
No completion claim has been made for the unchecked acceptance items.
+## 0.2.0 Candidate
+
+- [PR 12](https://github.com/capa-cloud/harness-runtime-api/pull/12) prepares runtime/workspace
+ version `0.2.0`, retaining portable protocol `2026-09-02` and adding migration notes.
+- Bilingual lifecycle diagrams now show failure during both human waits. Chrome inspections at
+ original and reading size passed; automated checks cover nine states, twelve transitions, and
+ deterministic regeneration.
+- Source packaging requires a clean commit and an output directory outside the repository,
+ including through symbolic links. Archives exclude Git metadata and include commit-bound
+ metadata and SHA-256 checksums. Packages remain unpublished.
+- Seven added version/diagram/packaging regression tests passed. Candidate
+ `c97c45d` passed [Node 22/24 CI](https://github.com/capa-cloud/harness-runtime-api/actions/runs/37911321061)
+ with 62 unit/contract tests and 35 integration tests per environment.
+- A complete local reachable-history scan found no credentials or private fingerprints; the
+ current-tree audit covered 137 files and metadata of eleven PNGs. Final artifacts are rechecked
+ after the reviewed commit is frozen; scans do not prove an absence of every possible secret.
+- Initial local source/archive reruns exposed overly tight subprocess deadlines and a partial
+ synthetic trace write under load. The tests now run serially with bounded startup allowances;
+ trace writes use atomic replacement. The assertions and production provider defaults are unchanged.
+ The final code revision `c8aa77e` passed both full local and isolated-archive reruns: each had
+ 62 unit/contract tests and 35 integration tests, format/lint/type/build checks, and public scanning.
+ The exact revision also passed [Node 22/24 CI](https://github.com/capa-cloud/harness-runtime-api/actions/runs/37912523565).
+ ACP fixture initialization and cleanup allowances are 10 seconds and one second; failure
+ diagnostics are explicit, and production defaults remain unchanged.
+- The final code audit covered 137 files, eleven PNGs, and 29 reachable commits, with zero private
+ fingerprint or credential findings. Source archive and manifest SHA-256 verification passed.
+ Source bundles include the frozen dependency inventory; production dependency auditing reported
+ no known vulnerabilities. Native model quality, operating-system isolation, and production load
+ testing remain outside this credential-free delivery validation.
+
## ACP and Schema Verification
- ACP v1 implementation and boundary hardening were developed in
diff --git a/docs/explanations/execution-lifecycle.md b/docs/explanations/execution-lifecycle.md
index 10dc40b..05ca3f9 100644
--- a/docs/explanations/execution-lifecycle.md
+++ b/docs/explanations/execution-lifecycle.md
@@ -16,6 +16,10 @@ Exactly one of three terminal outcomes is retained:
- `failed` after an unrecovered provider error;
- `cancelled` after cancellation finalization.
+Provider errors may occur in `running`, `awaiting_input`, or `awaiting_approval`. All three can
+transition directly to `failed`; pending Actions are rejected rather than left orphaned. Returning
+final output with an unresolved Action is also a failure, not a successful execution.
+
A cancellation request is accepted from any non-terminal state. Runtime moves through `cancelling`,
aborts the provider context, rejects pending actions, invokes optional provider cleanup, and appends
one `run.cancelled` terminal event. Terminal executions never return to active states.
diff --git a/package.json b/package.json
index 2a74718..9d6ab62 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "harness-runtime-api",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "A provider-neutral runtime contract for AI agent harnesses.",
"license": "Apache-2.0",
diff --git a/packages/conformance/package.json b/packages/conformance/package.json
index 6b7fa2c..6700875 100644
--- a/packages/conformance/package.json
+++ b/packages/conformance/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/conformance",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Provider conformance runner for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/core/package.json b/packages/core/package.json
index a1875b6..55a6156 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/core",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Provider SPI and reference runtime for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts
index d178ae3..4fd1bb6 100644
--- a/packages/core/src/runtime.ts
+++ b/packages/core/src/runtime.ts
@@ -74,7 +74,7 @@ export class InMemoryHarnessRuntime {
constructor(providers: HarnessProvider[] = [], options: InMemoryRuntimeOptions = {}) {
this.name = options.name ?? 'harness-runtime-reference'
- this.version = options.version ?? '0.1.0'
+ this.version = options.version ?? '0.2.0'
for (const provider of providers) this.registerProvider(provider)
}
diff --git a/packages/protocol/package.json b/packages/protocol/package.json
index b87675a..098eb59 100644
--- a/packages/protocol/package.json
+++ b/packages/protocol/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/protocol",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Schemas and types for the Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/provider-acp/package.json b/packages/provider-acp/package.json
index 21b1e9f..93711a2 100644
--- a/packages/provider-acp/package.json
+++ b/packages/provider-acp/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/provider-acp",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "ACP v1 subprocess adapter for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/provider-acp/src/index.ts b/packages/provider-acp/src/index.ts
index 8b4e132..e158279 100644
--- a/packages/provider-acp/src/index.ts
+++ b/packages/provider-acp/src/index.ts
@@ -49,7 +49,7 @@ export class AcpProvider implements HarnessProvider {
this.manifest = ProviderManifestSchema.parse({
id: this.options.id,
name: 'ACP v1 Agent Adapter',
- version: '0.1.0',
+ version: '0.2.0',
protocolVersion: PROTOCOL_VERSION,
topologies: ['subprocess'],
capabilities: {
diff --git a/packages/provider-dsh/README.md b/packages/provider-dsh/README.md
index 5b7929e..bd55055 100644
--- a/packages/provider-dsh/README.md
+++ b/packages/provider-dsh/README.md
@@ -1,6 +1,6 @@
# DeepSeek Harness Provider
-Optional subprocess adapter for `@deepseek-ai/dsh-sdk-client` `0.1.0-rc.7` or a compatible release.
+Optional subprocess adapter for exactly `@deepseek-ai/dsh-sdk-client` `0.1.0-rc.7`.
The dependency is loaded only when the provider starts an execution. Unit tests use an injected
structural client and never invoke a model or require a credential.
diff --git a/packages/provider-dsh/package.json b/packages/provider-dsh/package.json
index 4218fa0..88918ce 100644
--- a/packages/provider-dsh/package.json
+++ b/packages/provider-dsh/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/provider-dsh",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Optional DeepSeek Harness adapter for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/provider-dsh/src/index.ts b/packages/provider-dsh/src/index.ts
index c9b1688..7c68858 100644
--- a/packages/provider-dsh/src/index.ts
+++ b/packages/provider-dsh/src/index.ts
@@ -77,7 +77,7 @@ interface ActiveDshClient {
const manifest: ProviderManifest = {
id: 'dsh',
name: 'DeepSeek Harness Adapter',
- version: '0.1.0',
+ version: '0.2.0',
protocolVersion: PROTOCOL_VERSION,
topologies: ['subprocess'],
capabilities: {
diff --git a/packages/provider-mock/package.json b/packages/provider-mock/package.json
index d130eb0..51cf2b4 100644
--- a/packages/provider-mock/package.json
+++ b/packages/provider-mock/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/provider-mock",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Deterministic mock provider for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/provider-mock/src/index.ts b/packages/provider-mock/src/index.ts
index c2f7ab6..d445b72 100644
--- a/packages/provider-mock/src/index.ts
+++ b/packages/provider-mock/src/index.ts
@@ -8,7 +8,7 @@ import {
const manifest: ProviderManifest = {
id: 'mock',
name: 'Deterministic Mock Provider',
- version: '0.1.0',
+ version: '0.2.0',
protocolVersion: PROTOCOL_VERSION,
topologies: ['embedded'],
capabilities: {
diff --git a/packages/sdk-typescript/package.json b/packages/sdk-typescript/package.json
index 6543244..8f61253 100644
--- a/packages/sdk-typescript/package.json
+++ b/packages/sdk-typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/sdk-typescript",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "TypeScript HTTP client for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/packages/server/package.json b/packages/server/package.json
index f628edf..5009015 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@harness-runtime/server",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "HTTP and SSE binding for Harness Runtime API.",
"license": "Apache-2.0",
diff --git a/scripts/generate-lifecycle.mjs b/scripts/generate-lifecycle.mjs
new file mode 100644
index 0000000..4d69706
--- /dev/null
+++ b/scripts/generate-lifecycle.mjs
@@ -0,0 +1,73 @@
+import { readFile, writeFile } from 'node:fs/promises'
+
+const nodes = [
+ ['queued', 60, 140, 180, 80, 'active', '排队'],
+ ['starting', 300, 140, 180, 80, 'active', '启动'],
+ ['running', 540, 140, 200, 80, 'active', '运行'],
+ ['succeeded', 1260, 140, 240, 80, '', '成功'],
+ ['awaiting_input', 350, 360, 250, 80, 'wait', '等待输入'],
+ ['awaiting_approval', 690, 360, 290, 80, 'wait', '等待审批'],
+ ['failed', 1260, 360, 240, 80, '', '失败'],
+ ['cancelling', 670, 650, 240, 90, 'wait', '正在取消'],
+ ['cancelled', 1260, 650, 240, 90, '', '已取消'],
+]
+const edges = [
+ ['queued', 'starting', 'M240 180 H300'],
+ ['starting', 'running', 'M480 180 H540'],
+ ['running', 'succeeded', 'M740 180 H1260'],
+ ['running', 'awaiting_input', 'M600 220 V300 H475 V360'],
+ ['awaiting_input', 'running', 'M430 360 V260 H560 V220'],
+ ['running', 'awaiting_approval', 'M680 220 V315 H835 V360'],
+ ['awaiting_approval', 'running', 'M930 360 V275 H720 V220'],
+ ['running', 'failed', 'M740 205 H1110 V400 H1260'],
+ ['awaiting_input', 'failed', 'M600 400 H640 V505 H1110 V400 H1260'],
+ ['awaiting_approval', 'failed', 'M980 400 H1260'],
+ ['any_nonterminal', 'cancelling', 'M560 695 H670'],
+ ['cancelling', 'cancelled', 'M910 695 H1260'],
+]
+const text = (x, y, value, style = 'small') =>
+ `${value}`
+
+for (const language of ['en', 'zh']) {
+ const zh = language === 'zh'
+ const title = zh ? '执行生命周期' : 'Portable Execution Lifecycle'
+ const svg = `\n`
+ const target = new URL(`../assets/execution-lifecycle-${language}.svg`, import.meta.url)
+ if (process.argv.includes('--check')) {
+ if ((await readFile(target, 'utf8')) !== svg)
+ throw new Error(`Stale lifecycle diagram: ${language}`)
+ } else {
+ await writeFile(target, svg)
+ }
+}
+console.log('Bilingual lifecycle SVGs verified with 12 explicit transitions')
diff --git a/scripts/generate-openapi.mjs b/scripts/generate-openapi.mjs
index 5746080..d7e9383 100644
--- a/scripts/generate-openapi.mjs
+++ b/scripts/generate-openapi.mjs
@@ -4,6 +4,9 @@ import { runtimeSchemas } from './openapi-schemas.mjs'
const file = new URL('../spec/openapi.yaml', import.meta.url)
const document = parse(await readFile(file, 'utf8'))
+document.info.version = JSON.parse(
+ await readFile(new URL('../package.json', import.meta.url), 'utf8'),
+).version
document.components.schemas = runtimeSchemas()
document.paths['/v1/conversations'].post.requestBody.content['application/json'].schema = {
$ref: '#/components/schemas/CreateConversationRequest',
diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs
new file mode 100644
index 0000000..19d6c8f
--- /dev/null
+++ b/scripts/package-release.mjs
@@ -0,0 +1,69 @@
+import { execFileSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises'
+import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+async function canonicalPath(path) {
+ const suffix = []
+ for (;;) {
+ try {
+ return resolve(await realpath(path), ...suffix.reverse())
+ } catch (error) {
+ if (error.code !== 'ENOENT') throw error
+ suffix.push(basename(path))
+ path = dirname(path)
+ }
+ }
+}
+
+const root = await realpath(fileURLToPath(new URL('..', import.meta.url)))
+const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim()
+const outputArgument = process.argv[2]
+if (!outputArgument) throw new Error('Usage: node scripts/package-release.mjs ')
+const output = await canonicalPath(resolve(outputArgument))
+const location = relative(root, output)
+if (!isAbsolute(location) && location !== '..' && !location.startsWith(`..${sep}`)) {
+ throw new Error('Release output must be outside the repository')
+}
+if (git('status', '--porcelain'))
+ throw new Error('Release packaging requires a clean committed tree')
+const commit = git('rev-parse', 'HEAD')
+const manifest = JSON.parse(git('show', 'HEAD:package.json'))
+if (!/^\d+\.\d+\.\d+$/.test(manifest.version))
+ throw new Error('Expected a stable SemVer base version')
+const name = `harness-runtime-api-${manifest.version}`
+const archive = `${name}-source.tar.gz`
+await mkdir(output, { recursive: true })
+execFileSync(
+ 'git',
+ ['archive', '--format=tar.gz', `--prefix=${name}/`, '-o', resolve(output, archive), commit],
+ { cwd: root },
+)
+const sha256 = createHash('sha256')
+ .update(await readFile(resolve(output, archive)))
+ .digest('hex')
+await writeFile(
+ resolve(output, 'release-manifest.json'),
+ `${JSON.stringify(
+ {
+ version: manifest.version,
+ commit,
+ format: 'source-only',
+ install: 'pnpm install --frozen-lockfile',
+ verification: 'pnpm check && pnpm sanitize',
+ packageRegistryPublished: false,
+ assets: [{ name: archive, sha256 }],
+ },
+ null,
+ 2,
+ )}\n`,
+)
+const metadataHash = createHash('sha256')
+ .update(await readFile(resolve(output, 'release-manifest.json')))
+ .digest('hex')
+await writeFile(
+ resolve(output, 'SHA256SUMS'),
+ `${sha256} ${archive}\n${metadataHash} release-manifest.json\n`,
+)
+console.log(JSON.stringify({ version: manifest.version, commit, archive, sha256 }))
diff --git a/spec/openapi.yaml b/spec/openapi.yaml
index 20f0b65..d4a053c 100644
--- a/spec/openapi.yaml
+++ b/spec/openapi.yaml
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: Harness Runtime API
- version: 0.1.0
+ version: 0.2.0
description: Provider-neutral control and event protocol for AI agent harness runtimes.
servers:
- url: http://127.0.0.1:4310
diff --git a/spec/runtime-model.md b/spec/runtime-model.md
index aff3383..db3be8d 100644
--- a/spec/runtime-model.md
+++ b/spec/runtime-model.md
@@ -55,12 +55,17 @@ queued -> starting -> running
|-> succeeded
|-> failed
any non-terminal state -> cancelling -> cancelled
+running or awaiting_* -> failed on provider failure
```
Terminal states are `succeeded`, `failed`, and `cancelled`. A terminal execution never returns to a
non-terminal state. Cancellation can begin from any non-terminal state; the provider-specific cancel
hook and abort signal determine how native work is stopped.
+Provider failures can also terminate an execution while it awaits input or approval. Returning
+success with an unresolved Action is a provider failure; abandoned Action promises are settled.
+Once cancellation begins, its cleanup path owns the terminal cancellation outcome.
+
## Authority
- The caller owns identity, authorization, business policy, and credentials.
diff --git a/tests/fixtures/acp-agent.mjs b/tests/fixtures/acp-agent.mjs
index 5668d66..72d01bc 100644
--- a/tests/fixtures/acp-agent.mjs
+++ b/tests/fixtures/acp-agent.mjs
@@ -1,4 +1,4 @@
-import { writeFileSync } from 'node:fs'
+import { renameSync, writeFileSync } from 'node:fs'
import { isAbsolute } from 'node:path'
import { Readable, Writable } from 'node:stream'
import { createInterface } from 'node:readline'
@@ -18,8 +18,11 @@ const state = {
parseErrors: 0,
}
const record = () => {
- if (process.env.FIXTURE_TRACE_PATH)
- writeFileSync(process.env.FIXTURE_TRACE_PATH, JSON.stringify(state))
+ const path = process.env.FIXTURE_TRACE_PATH
+ if (path) {
+ writeFileSync(`${path}.tmp`, JSON.stringify(state))
+ renameSync(`${path}.tmp`, path)
+ }
}
record()
const inputObserver = createInterface({ input: process.stdin })
diff --git a/tests/integration/acp-provider.integration.ts b/tests/integration/acp-provider.integration.ts
index 5c8cce6..ce9c8cb 100644
--- a/tests/integration/acp-provider.integration.ts
+++ b/tests/integration/acp-provider.integration.ts
@@ -32,9 +32,9 @@ async function setup(mode: string, maxMessageBytes?: number, maxPendingPermissio
command: process.execPath,
args: [fixture],
env: { FIXTURE_MODE: mode, FIXTURE_TRACE_PATH: tracePath, FIXTURE_DENIED_TARGET: deniedTarget },
- initializationTimeoutMs: 1_000,
- runTimeoutMs: 3_000,
- shutdownGraceMs: 100,
+ initializationTimeoutMs: 10_000,
+ runTimeoutMs: 10_000,
+ shutdownGraceMs: 1_000,
...(maxMessageBytes === undefined ? {} : { maxMessageBytes }),
...(maxPendingPermissions === undefined ? {} : { maxPendingPermissions }),
})
@@ -242,6 +242,7 @@ describe('ACP v1 adapter through the official SDK and owned subprocess', () => {
const task = await setup('bad-json')
try {
await drain(task.runtime, task.execution.id)
+ expect(task.runtime.getExecution(task.execution.id).error).toBeUndefined()
expect(task.runtime.getExecution(task.execution.id).state).toBe('succeeded')
expect((await task.trace()).parseErrors).toBe(1)
expect(diagnostics).not.toHaveBeenCalled()
@@ -256,6 +257,7 @@ describe('ACP v1 adapter through the official SDK and owned subprocess', () => {
const task = await setup('forbidden-client')
try {
await drain(task.runtime, task.execution.id)
+ expect(task.runtime.getExecution(task.execution.id).error).toBeUndefined()
expect(task.runtime.getExecution(task.execution.id).state).toBe('succeeded')
expect((await task.trace()).forbiddenErrors).toEqual([-32601, -32601, -32601])
expect(await task.protectedText()).toBe('synthetic protected text')
@@ -271,9 +273,9 @@ describe('ACP v1 adapter through the official SDK and owned subprocess', () => {
const report = await runProviderConformance(task.provider, {
requiredCapabilities: ['action.approval'],
actionResponse: { approved: true },
- timeoutMs: 5_000,
+ timeoutMs: 15_000,
})
- expect(report.passed).toBe(true)
+ expect(report.passed, JSON.stringify(report)).toBe(true)
} finally {
await task.cleanup()
}
diff --git a/tests/integration/dsh-sdk.integration.ts b/tests/integration/dsh-sdk.integration.ts
index e123a55..7b1bec1 100644
--- a/tests/integration/dsh-sdk.integration.ts
+++ b/tests/integration/dsh-sdk.integration.ts
@@ -16,7 +16,7 @@ function providerFor(mode: string) {
command: process.execPath,
args: [fixture],
env: { FIXTURE_MODE: mode },
- requestTimeoutMs: 1_000,
+ requestTimeoutMs: 5_000,
shutdownTimeoutMs: 100,
disposeEofGraceMs: 200,
disposeGraceMs: 200,
@@ -107,7 +107,7 @@ describe('Pinned public DSH SDK with a real synthetic subprocess', () => {
})
it('passes the reusable conformance probe through the actual SDK', async () => {
- const report = await runProviderConformance(providerFor('success'), { timeoutMs: 5_000 })
- expect(report.passed).toBe(true)
+ const report = await runProviderConformance(providerFor('success'), { timeoutMs: 10_000 })
+ expect(report.passed, JSON.stringify(report)).toBe(true)
})
})
diff --git a/tests/integration/http-server.integration.ts b/tests/integration/http-server.integration.ts
index 1376a7c..8962041 100644
--- a/tests/integration/http-server.integration.ts
+++ b/tests/integration/http-server.integration.ts
@@ -40,7 +40,10 @@ beforeAll(async () => {
const lines = createInterface({ input: server.stdout })
try {
baseUrl = await new Promise((resolve, reject) => {
- const timer = setTimeout(() => reject(new Error('Reference server startup timed out')), 3_000)
+ const timer = setTimeout(
+ () => reject(new Error('Reference server startup timed out')),
+ 10_000,
+ )
const cleanup = () => {
clearTimeout(timer)
server.removeListener('exit', failed)
diff --git a/tests/release-consistency.test.ts b/tests/release-consistency.test.ts
new file mode 100644
index 0000000..daa3944
--- /dev/null
+++ b/tests/release-consistency.test.ts
@@ -0,0 +1,58 @@
+import { execFileSync } from 'node:child_process'
+import { readFile } from 'node:fs/promises'
+import { describe, expect, it } from 'vitest'
+import { parse } from 'yaml'
+import { InMemoryHarnessRuntime } from '../packages/core/src/index.js'
+import { MockProvider } from '../packages/provider-mock/src/index.js'
+import { DshProvider } from '../packages/provider-dsh/src/index.js'
+import { AcpProvider } from '../packages/provider-acp/src/index.js'
+
+const root = new URL('..', import.meta.url)
+const read = (path: string) => readFile(new URL(path, root), 'utf8')
+
+describe('release metadata and exact diagrams', () => {
+ it('keeps workspace, runtime, provider, and OpenAPI versions aligned', async () => {
+ const { version } = JSON.parse(await read('package.json'))
+ for (const name of [
+ 'protocol',
+ 'core',
+ 'provider-mock',
+ 'provider-dsh',
+ 'provider-acp',
+ 'conformance',
+ 'server',
+ 'sdk-typescript',
+ ]) {
+ expect(JSON.parse(await read(`packages/${name}/package.json`)).version).toBe(version)
+ }
+ expect(new InMemoryHarnessRuntime().describe().version).toBe(version)
+ expect(new MockProvider().manifest.version).toBe(version)
+ expect(new DshProvider({ launch: { command: 'synthetic-agent' } }).manifest.version).toBe(
+ version,
+ )
+ expect(new AcpProvider({ command: 'synthetic-agent' }).manifest.version).toBe(version)
+ expect(parse(await read('spec/openapi.yaml')).info.version).toBe(version)
+ })
+
+ it('keeps both lifecycle diagrams synchronized with the deterministic generator', () => {
+ expect(() =>
+ execFileSync(
+ process.execPath,
+ [new URL('scripts/generate-lifecycle.mjs', root).pathname, '--check'],
+ { stdio: 'pipe' },
+ ),
+ ).not.toThrow()
+ })
+
+ it.each(['en', 'zh'])(
+ 'shows failure during both human waits in the %s diagram',
+ async (language) => {
+ const diagram = await read(`assets/execution-lifecycle-${language}.svg`)
+ expect(diagram.match(/data-state=/g)).toHaveLength(9)
+ expect(diagram.match(/data-from=/g)).toHaveLength(12)
+ for (const state of ['running', 'awaiting_input', 'awaiting_approval']) {
+ expect(diagram).toContain(`data-from="${state}" data-to="failed"`)
+ }
+ },
+ )
+})
diff --git a/tests/release-package.test.ts b/tests/release-package.test.ts
new file mode 100644
index 0000000..57935c8
--- /dev/null
+++ b/tests/release-package.test.ts
@@ -0,0 +1,80 @@
+import { execFileSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import { copyFile, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+
+const directories: string[] = []
+afterEach(async () => {
+ await Promise.all(
+ directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })),
+ )
+})
+
+async function fixture() {
+ const directory = await mkdtemp(join(tmpdir(), 'harness-release-test-'))
+ directories.push(directory)
+ const repo = join(directory, 'repo')
+ const output = join(directory, 'output')
+ await mkdir(join(repo, 'scripts'), { recursive: true })
+ await copyFile(
+ new URL('../scripts/package-release.mjs', import.meta.url),
+ join(repo, 'scripts/package-release.mjs'),
+ )
+ await writeFile(join(repo, 'package.json'), JSON.stringify({ version: '0.2.0', private: true }))
+ const git = (...args: string[]) =>
+ execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: 'pipe' }).trim()
+ git('init', '--quiet')
+ git('add', '.')
+ git(
+ '-c',
+ 'user.name=Synthetic Test',
+ '-c',
+ 'user.email=test@example.invalid',
+ '-c',
+ 'commit.gpgsign=false',
+ 'commit',
+ '--quiet',
+ '-m',
+ 'Synthetic fixture',
+ )
+ const run = (destination = output) =>
+ execFileSync(process.execPath, [join(repo, 'scripts/package-release.mjs'), destination], {
+ cwd: repo,
+ encoding: 'utf8',
+ stdio: 'pipe',
+ })
+ return { repo, output, git, run }
+}
+
+describe('source-only release packaging', () => {
+ it('binds archive and checksums to the exact clean commit without Git metadata', async () => {
+ const { output, git, run } = await fixture()
+ const result = JSON.parse(run())
+ expect(result.commit).toBe(git('rev-parse', 'HEAD'))
+ const archive = await readFile(join(output, result.archive))
+ expect(result.sha256).toBe(createHash('sha256').update(archive).digest('hex'))
+ const metadata = await readFile(join(output, 'release-manifest.json'))
+ expect(JSON.parse(metadata.toString()).packageRegistryPublished).toBe(false)
+ expect(await readFile(join(output, 'SHA256SUMS'), 'utf8')).toContain(
+ `${createHash('sha256').update(metadata).digest('hex')} release-manifest.json`,
+ )
+ const files = execFileSync('tar', ['-tzf', join(output, result.archive)], { encoding: 'utf8' })
+ .trim()
+ .split('\n')
+ expect(files.every((file) => file.startsWith('harness-runtime-api-0.2.0/'))).toBe(true)
+ expect(files.some((file) => file.includes('/.git/'))).toBe(false)
+ })
+
+ it('rejects uncommitted source', async () => {
+ const { repo, run } = await fixture()
+ await writeFile(join(repo, 'unreviewed.txt'), 'synthetic')
+ expect(run).toThrow()
+ })
+
+ it('rejects output inside the source repository', async () => {
+ const { repo, run } = await fixture()
+ expect(() => run(join(repo, 'release'))).toThrow()
+ })
+})
diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts
index 2666f76..f61125f 100644
--- a/vitest.integration.config.ts
+++ b/vitest.integration.config.ts
@@ -3,6 +3,8 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['tests/integration/**/*.integration.ts'],
- testTimeout: 15_000,
+ fileParallelism: false,
+ testTimeout: 30_000,
+ hookTimeout: 15_000,
},
})