Merge origin/master into feat/tui-master-port
This commit is contained in:
33
scripts/client-tsconfig.spec.ts
Normal file
33
scripts/client-tsconfig.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/** Regression coverage for source declarations owned by the client test aggregate. */
|
||||
|
||||
import { existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
function clientCssDeclarations(): string[] {
|
||||
const clientRoot = resolve(root, 'packages/client')
|
||||
return readdirSync(clientRoot, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => resolve(clientRoot, entry.name, 'src/css-modules.d.ts'))
|
||||
.filter(existsSync)
|
||||
.sort()
|
||||
}
|
||||
|
||||
describe('client TypeScript aggregate', () => {
|
||||
it('loads package CSS declarations without relying on workspace-link realpaths', () => {
|
||||
const configPath = resolve(root, 'tsconfig.client.json')
|
||||
const read = ts.readConfigFile(configPath, file => ts.sys.readFile(file))
|
||||
if (read.error !== undefined) {
|
||||
throw new Error(ts.flattenDiagnosticMessageText(read.error.messageText, '\n'))
|
||||
}
|
||||
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root)
|
||||
const loaded = parsed.fileNames
|
||||
.filter(file => file.endsWith('/src/css-modules.d.ts'))
|
||||
.sort()
|
||||
expect(loaded).toEqual(clientCssDeclarations())
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AGENTS.md": 1680,
|
||||
"AGENTS.md": 1705,
|
||||
"docs/AGENTS.md": 1150,
|
||||
"docs/architecture.md": 1800,
|
||||
"docs/cordis-primer.md": 600,
|
||||
|
||||
@@ -32,13 +32,16 @@ function quote(value: string): string {
|
||||
/**
|
||||
* Reduce an exported class to its type shape: drop method/constructor bodies
|
||||
* and property initializers so the catalog serves member signatures, not
|
||||
* implementation.
|
||||
* implementation. An abstract class (e.g. `Agent`) is a public type consumers
|
||||
* program against, so it belongs in the type closure alongside interfaces.
|
||||
*/
|
||||
function classShape(node: ts.ClassDeclaration): ts.ClassDeclaration {
|
||||
const isNonPublic = (member: ts.ClassElement): boolean =>
|
||||
(ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined)?.some(m =>
|
||||
m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword) ?? false
|
||||
const members = node.members.flatMap((member): ts.ClassElement[] => {
|
||||
// A model-facing type shape carries only the public surface — drop private,
|
||||
// protected, and #private members, and strip every kept member's body.
|
||||
if (isNonPublic(member) || (ts.isPropertyDeclaration(member) && ts.isPrivateIdentifier(member.name))) return []
|
||||
if (ts.isMethodDeclaration(member)) {
|
||||
return [ts.factory.updateMethodDeclaration(
|
||||
@@ -67,8 +70,9 @@ function classShape(node: ts.ClassDeclaration): ts.ClassDeclaration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect exported interface, type-alias, and body-stripped class shapes; omit
|
||||
* names declared in multiple packages rather than serve the wrong shape.
|
||||
* Collect exported interface, type-alias, and (body-stripped) class shapes;
|
||||
* omit names declared in multiple packages rather than risk serving the wrong
|
||||
* package's shape.
|
||||
*/
|
||||
function collectTypeDecls(scanRoot: string = root): Map<string, string> {
|
||||
const printer = ts.createPrinter({ removeComments: true })
|
||||
|
||||
@@ -35,9 +35,11 @@ export const LINK_MAP: Record<string, string> = {
|
||||
ContinuationDecision: 'core.md',
|
||||
ContinuationStop: 'core.md',
|
||||
GenerateOptions: 'core.md',
|
||||
InboxPlacement: 'core.md',
|
||||
AgentMessage: 'core.md',
|
||||
AgentMessageId: 'core.md',
|
||||
HookContext: 'core.md',
|
||||
SettleReason: 'core.md',
|
||||
LlmCallConfig: 'core.md',
|
||||
LlmModelContext: 'core.md',
|
||||
LlmModelReasoningInfo: 'core.md',
|
||||
@@ -48,8 +50,8 @@ export const LINK_MAP: Record<string, string> = {
|
||||
Message: 'core.md',
|
||||
MessageSource: 'core.md',
|
||||
PromptDecision: 'core.md',
|
||||
RequestErrorAction: 'core.md',
|
||||
RequestError: 'core.md',
|
||||
RequestErrorDecision: 'core.md',
|
||||
PreparedReferencedMessage: 'session-reference.md',
|
||||
SessionReferenceCandidate: 'session-reference.md',
|
||||
SessionReferenceInput: 'session-reference.md',
|
||||
@@ -66,7 +68,12 @@ export const LINK_MAP: Record<string, string> = {
|
||||
BashExecSpec: 'bash.md',
|
||||
BashProcess: 'bash.md',
|
||||
BashRunResult: 'bash.md',
|
||||
DshEnvironment: 'bash.md',
|
||||
DshEnvironment: 'subprocess.md',
|
||||
SubprocessHandle: 'subprocess.md',
|
||||
SubprocessOutcome: 'subprocess.md',
|
||||
SubprocessOutputRead: 'subprocess.md',
|
||||
SubprocessOutputReader: 'subprocess.md',
|
||||
SubprocessSpawnSpec: 'subprocess.md',
|
||||
CodeRunRequest: 'code-runtime.md',
|
||||
CodeRunResult: 'code-runtime.md',
|
||||
CompactionResult: 'compaction.md',
|
||||
@@ -242,6 +249,7 @@ const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
|
||||
SubagentRunEndInfo: 'event-local snapshot is owned by packages/subagent/subagent/src/index.ts',
|
||||
SubagentRunInfo: 'event-local snapshot is owned by packages/subagent/subagent/src/index.ts',
|
||||
TelemetryRecord: 'seam-local record contract is owned by packages/telemetry/session-telemetry/src/index.ts',
|
||||
WorkflowAgentEndInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
|
||||
WorkflowAgentInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
|
||||
WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
|
||||
|
||||
@@ -59,6 +59,7 @@ const GROUP_ORDER = [
|
||||
'llm',
|
||||
'core',
|
||||
'goal',
|
||||
'process',
|
||||
'bash',
|
||||
'pty',
|
||||
'sandbox',
|
||||
@@ -77,6 +78,7 @@ const GROUP_ORDER = [
|
||||
'session-persistence',
|
||||
'session-query',
|
||||
'session-title',
|
||||
'telemetry',
|
||||
'storage',
|
||||
'workspace',
|
||||
'support',
|
||||
@@ -135,6 +137,15 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite'],
|
||||
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
|
||||
},
|
||||
{
|
||||
key: 'telemetry',
|
||||
pkg: 'session-telemetry',
|
||||
title: 'Session telemetry seam',
|
||||
mode: 'seam',
|
||||
implementations: ['session-telemetry-otel'],
|
||||
consumers: [],
|
||||
note: 'The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process.',
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
pkg: 'storage',
|
||||
@@ -264,6 +275,15 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
mode: 'core',
|
||||
note: 'Folds revisioned objective state from the session log and keeps live continuation activation process-local.',
|
||||
},
|
||||
{
|
||||
key: 'subprocess',
|
||||
pkg: 'subprocess',
|
||||
title: 'Subprocess seam',
|
||||
mode: 'seam',
|
||||
implementations: ['subprocess-local'],
|
||||
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp'],
|
||||
note: 'The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
|
||||
},
|
||||
{
|
||||
key: 'bash',
|
||||
pkg: 'bash',
|
||||
@@ -689,22 +709,31 @@ class EventRelationCollector {
|
||||
/** Walk one package source file and classify event API calls by receiver type. */
|
||||
private visitSource(source: PackageSource): void {
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
||||
const receiverKind = this.receiverKind(node.expression.expression)
|
||||
const method = node.expression.name.text
|
||||
if (receiverKind === 'events-service' && method === 'dispatch') {
|
||||
const argumentList = node.arguments[1]
|
||||
if (argumentList) {
|
||||
for (const event of this.eventNamesFromArgumentList(argumentList, new Set())) {
|
||||
this.addDispatcher(event, source.pkg, 'events.dispatch')
|
||||
if (ts.isCallExpression(node)) {
|
||||
if (this.isAgentEventEmitter(node.expression)) {
|
||||
const event = node.arguments[2]
|
||||
if (event) {
|
||||
for (const name of this.finiteStringValues(event) ?? []) {
|
||||
this.addDispatcher(name, source.pkg, 'emitAgentEvent')
|
||||
}
|
||||
}
|
||||
} else if (receiverKind === 'context' || receiverKind === 'agent-dispatch') {
|
||||
const eventNames = this.eventNamesFromCall(node, receiverKind)
|
||||
if (method === 'on' || method === 'once') {
|
||||
for (const event of eventNames) this.ensure(event).listeners.add(source.pkg)
|
||||
} else if (method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall') {
|
||||
for (const event of eventNames) this.addDispatcher(event, source.pkg, method)
|
||||
} else if (ts.isPropertyAccessExpression(node.expression)) {
|
||||
const receiverKind = this.receiverKind(node.expression.expression)
|
||||
const method = node.expression.name.text
|
||||
if (receiverKind === 'events-service' && method === 'dispatch') {
|
||||
const argumentList = node.arguments[1]
|
||||
if (argumentList) {
|
||||
for (const event of this.eventNamesFromArgumentList(argumentList, new Set())) {
|
||||
this.addDispatcher(event, source.pkg, 'events.dispatch')
|
||||
}
|
||||
}
|
||||
} else if (receiverKind === 'context' || receiverKind === 'agent-dispatch') {
|
||||
const eventNames = this.eventNamesFromCall(node, receiverKind)
|
||||
if (method === 'on' || method === 'once') {
|
||||
for (const event of eventNames) this.ensure(event).listeners.add(source.pkg)
|
||||
} else if (method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall') {
|
||||
for (const event of eventNames) this.addDispatcher(event, source.pkg, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,6 +742,22 @@ class EventRelationCollector {
|
||||
visit(source.sourceFile)
|
||||
}
|
||||
|
||||
/** Match the exported contained-notification helper by declaration identity. */
|
||||
private isAgentEventEmitter(expression: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(expression)) return false
|
||||
const local = this.project.checker.getSymbolAtLocation(expression)
|
||||
if (!local) return false
|
||||
const symbol = local.flags & ts.SymbolFlags.Alias
|
||||
? this.project.checker.getAliasedSymbol(local)
|
||||
: local
|
||||
const declarations = symbol.declarations ?? []
|
||||
return declarations.some((declaration) => {
|
||||
return ts.isFunctionDeclaration(declaration)
|
||||
&& declaration.name?.text === 'emitAgentEvent'
|
||||
&& this.project.relativePath(declaration.getSourceFile()) === 'packages/core/agent/src/dispatch.ts'
|
||||
})
|
||||
}
|
||||
|
||||
/** Classify a receiver using assignability to the repository's actual event API types. */
|
||||
private receiverKind(receiver: ts.Expression): EventReceiverKind | undefined {
|
||||
const type = this.project.checker.getTypeAtLocation(receiver)
|
||||
@@ -964,18 +1009,21 @@ function renderLifecycle(): string {
|
||||
' participant LLM as ctx.llm',
|
||||
' participant Tools as ctx.tools',
|
||||
' participant Session',
|
||||
' participant Persistence',
|
||||
' participant SDK as UI or SDK listener',
|
||||
' User->>Agent: followup(content)',
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/enqueue')}`,
|
||||
' Agent->>Driver: queued work wakes driver',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} running`,
|
||||
` Driver->>Session: ${mermaidCode('turn/start')}`,
|
||||
' Note over Agent,Driver: next-step acceptance window opens',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/prompt-submit')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative allow, block, or add context',
|
||||
` Driver->>Session: ${mermaidCode('user/message')} or rejected ${mermaidCode('turn/end')}`,
|
||||
' alt prompt blocked or admission failed',
|
||||
' Driver-->>Driver: append context-only batch or keep steering boundary pending',
|
||||
' else prompt allowed',
|
||||
` Driver->>Session: ${mermaidCode('turn/start')}`,
|
||||
` Driver->>Session: ${mermaidCode('user/message')}`,
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
|
||||
` Driver-->>Driver: ${mermaidCode('agent/pre-step')} serial checkpoint`,
|
||||
` Driver-->>Driver: ${mermaidCode('agent/step')} serial checkpoint`,
|
||||
` Driver->>Session: ${mermaidCode('step/start')}`,
|
||||
` Driver->>LLM: ${mermaidCode('agent/request')} waterfall, then ${mermaidCode('llm/stream')} waterfall`,
|
||||
' LLM-->>Driver: StreamChunk*',
|
||||
@@ -984,9 +1032,8 @@ function renderLifecycle(): string {
|
||||
' alt final adapter or terminal in-band request failure',
|
||||
` Driver->>Session: ${mermaidCode('step/end')}`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/request-error')} waterfall`,
|
||||
' Hooks-->>Driver: retry in a new step or preserve the original error',
|
||||
' Hooks-->>Driver: return retry action or preserve the original error',
|
||||
' else model request succeeded',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/step-result')} waterfall`,
|
||||
` Driver->>Session: ${mermaidCode('assistant/message')}`,
|
||||
' Driver->>Tools: classify pending call by executionMode',
|
||||
' loop barriers and bounded rolling pool, reclassify before start',
|
||||
@@ -1001,19 +1048,18 @@ function renderLifecycle(): string {
|
||||
' end',
|
||||
' end',
|
||||
' Driver->>Session: post-tool context and steering (no prompt-submit)',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/post-step')} serial checkpoint`,
|
||||
` Driver->>Session: ${mermaidCode('step/end')}`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-continuation')} waterfall`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stop')} serial terminal checkpoint`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
|
||||
' end',
|
||||
' Note over Agent,Driver: next-step acceptance window closes',
|
||||
` Driver->>Session: ${mermaidCode('turn/end')}`,
|
||||
` Driver->>Persistence: ${mermaidCode('session/flush')} parallel checkpoint`,
|
||||
' end',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
|
||||
'```',
|
||||
'',
|
||||
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
|
||||
'',
|
||||
'`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and a fresh retry step, and returns retry only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'`dsh-compact-basic` uses `agent/step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'',
|
||||
'The returned `agent/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.',
|
||||
'',
|
||||
|
||||
@@ -19,6 +19,7 @@ import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools
|
||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
|
||||
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
@@ -197,6 +198,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.tasks at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(LocalBashExecutor)
|
||||
await ctx.plugin(ToolBash)
|
||||
},
|
||||
|
||||
@@ -736,8 +736,6 @@ def scrub_snapshot_header(value: dict[object, object]) -> None:
|
||||
tool.get("name") if isinstance(tool, dict) else "{{tools}}"
|
||||
for tool in tools
|
||||
]
|
||||
if isinstance(header.get("messagePrefix"), list):
|
||||
header["messagePrefix"] = ["{{messagePrefix}}" for _ in header["messagePrefix"]]
|
||||
|
||||
|
||||
def render_jsonl(records: list[object]) -> str:
|
||||
|
||||
@@ -86,21 +86,21 @@
|
||||
"symbol": "SessionEvent",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SendTarget",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxPlacement",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SendOptions",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InjectOptions",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ResolvedAgentInput",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "AgentMessageId",
|
||||
@@ -126,11 +126,6 @@
|
||||
"symbol": "Agent",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "HookContext",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "PromptDecision",
|
||||
@@ -138,7 +133,7 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContinuationDecision",
|
||||
"symbol": "RequestErrorAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
@@ -146,16 +141,6 @@
|
||||
"symbol": "RequestError",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "RequestErrorDecision",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContinuationStop",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SessionStartSource",
|
||||
@@ -335,7 +320,7 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/session.md",
|
||||
"symbol": "PromptMessageData",
|
||||
"symbol": "UserMessageData",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
@@ -759,16 +744,6 @@
|
||||
"symbol": "ApprovalRequest",
|
||||
"source": "packages/ui/user-approval/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/bash.md",
|
||||
"symbol": "DshEnvironmentKey",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/bash.md",
|
||||
"symbol": "DshEnvironment",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/bash.md",
|
||||
"symbol": "BashExecRequest",
|
||||
@@ -789,11 +764,6 @@
|
||||
"symbol": "BashSandboxInfo",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/bash.md",
|
||||
"symbol": "CollectedOutput",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/bash.md",
|
||||
"symbol": "BashProcess",
|
||||
@@ -1278,6 +1248,71 @@
|
||||
"doc": "docs/core-data-structures/session-query.md",
|
||||
"symbol": "SessionSearchHit",
|
||||
"source": "packages/session-query/session-query/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessSpawnSpec",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessHandle",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessOutputReader",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessOutputRead",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessOutcome",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "DshEnvironmentKey",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "DshEnvironment",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "CollectedOutput",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessStdinMode",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessCollect",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessOutputMode",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessStdio",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/subprocess.md",
|
||||
"symbol": "SubprocessCollectedOutputs",
|
||||
"source": "packages/subprocess/subprocess/src/types.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
|
||||
'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
|
||||
'packages/lsp/lsp-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-lsp.' },
|
||||
'packages/subprocess/subprocess': { kind: 'indirect', reason: 'The seam delegates all model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
|
||||
'packages/sandbox/sandbox-policy': { kind: 'indirect', reason: 'The policy service holds the mode dsh-tool-bash and dsh-tool-fs render in their denial markers.' },
|
||||
'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
|
||||
@@ -85,12 +87,13 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/sdk/telemetry': { kind: 'none', reason: 'The launcher-side reporter sends developer-cycle telemetry and registers no live agent or model surface.' },
|
||||
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
|
||||
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers no model surface.' },
|
||||
'packages/telemetry/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
|
||||
'packages/telemetry/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' },
|
||||
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },
|
||||
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
|
||||
'packages/subagent/subagent': { kind: 'indirect', reason: 'The provider registry delegates parent-model rendering to dsh-tool-subagent.' },
|
||||
'packages/subagent/subagent-subprocess': { kind: 'indirect', reason: 'Only process-based subagent backends compose a child model request.' },
|
||||
'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
|
||||
'packages/support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
|
||||
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
|
||||
|
||||
223
scripts/wine-windows-gates.sh
Executable file
223
scripts/wine-windows-gates.sh
Executable file
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the blocking Windows gates (workspace build, production site) with real
|
||||
# win-x64 Node.js under Wine — the same script the pull-request `windows` job
|
||||
# in ci.yml executes and the optional local gate `pnpm run check:windows-wine`
|
||||
# wraps. Owning rationale, fidelity limits, and measured timings:
|
||||
# .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
|
||||
#
|
||||
# The working tree is never mutated: tracked plus untracked-unignored files
|
||||
# are snapshotted into a scratch directory, the Wine-specific pnpm overrides
|
||||
# (hoisted layout, win32-x64 platform packages) are appended to the SNAPSHOT's
|
||||
# pnpm-workspace.yaml, and the install and gates run there against the shared
|
||||
# pnpm store. The Wine prefix and the checksum-verified Windows Node zip
|
||||
# persist in .cache/wine-windows/ so reruns skip provisioning.
|
||||
#
|
||||
# Environment: DSH_WINE_NODE_MAJOR (default $PRIMARY_NODE_VERSION, then 24)
|
||||
# picks the Windows Node line; DSH_WINE_GATE_CACHE_DIR relocates the cache;
|
||||
# DSH_WINE_GATE_KEEP=1 preserves the scratch tree for inspection.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
node_major="${DSH_WINE_NODE_MAJOR:-${PRIMARY_NODE_VERSION:-24}}"
|
||||
cache_dir="${DSH_WINE_GATE_CACHE_DIR:-$repo_root/.cache/wine-windows}"
|
||||
|
||||
export WINEDEBUG='-all'
|
||||
export WINEARCH=win64
|
||||
# Skip Wine Mono / Gecko installers: Node needs neither.
|
||||
export WINEDLLOVERRIDES='mscoree,mshtml='
|
||||
export WINEPREFIX="$cache_dir/prefix"
|
||||
|
||||
# ---- preflight: fail loud before any expensive work --------------------
|
||||
wine_bin=''
|
||||
for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do
|
||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then wine_bin="$candidate"; break; fi
|
||||
done
|
||||
# GNU coreutils sha256sum on Linux; perl shasum ships with macOS. Both
|
||||
# accept the same "<hash> <file>" --check input.
|
||||
checksum_tool=''
|
||||
if command -v sha256sum > /dev/null; then
|
||||
checksum_tool='sha256sum'
|
||||
elif command -v shasum > /dev/null; then
|
||||
checksum_tool='shasum'
|
||||
fi
|
||||
missing=()
|
||||
[ -n "$wine_bin" ] || missing+=('wine (apt: wine | brew: wine-stable)')
|
||||
command -v curl > /dev/null || missing+=('curl')
|
||||
command -v unzip > /dev/null || missing+=('unzip')
|
||||
[ -n "$checksum_tool" ] || missing+=('sha256sum or shasum (apt: coreutils | macOS ships shasum)')
|
||||
if ! command -v pnpm > /dev/null; then corepack enable > /dev/null 2>&1 || true; fi
|
||||
command -v pnpm > /dev/null || missing+=('pnpm (corepack enable)')
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
printf 'wine-windows-gates: missing required tool: %s\n' "${missing[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify file $2 against SHA-256 hex $1 with whichever tool preflight found.
|
||||
verify_sha256() {
|
||||
case "$checksum_tool" in
|
||||
sha256sum) printf '%s %s\n' "$1" "$2" | sha256sum --check - > /dev/null ;;
|
||||
shasum) printf '%s %s\n' "$1" "$2" | shasum -a 256 --check - > /dev/null ;;
|
||||
esac
|
||||
}
|
||||
|
||||
scratch="$(mktemp -d "${TMPDIR:-/tmp}/dsh-wine-gates.XXXXXX")"
|
||||
cleanup() {
|
||||
wineserver -k > /dev/null 2>&1 || true
|
||||
if [ "${DSH_WINE_GATE_KEEP:-0}" = '1' ]; then
|
||||
echo "wine-windows-gates: scratch tree kept at $scratch"
|
||||
else
|
||||
rm -rf "$scratch"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$cache_dir" "$scratch/logs"
|
||||
|
||||
# ---- provision Windows Node, boot Wine, snapshot + install concurrently ----
|
||||
provision_node() {
|
||||
# Latest release of the primary line, checksum-verified against the same
|
||||
# dist directory. Offline runs fall back to the newest cached zip, loudly.
|
||||
local version zip
|
||||
version="$(curl -fsSL --max-time 30 https://nodejs.org/dist/index.json 2> /dev/null \
|
||||
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const v=JSON.parse(d).find(r=>r.version.startsWith('v$node_major.'));if(v)console.log(v.version)})" \
|
||||
|| true)"
|
||||
if [ -n "$version" ]; then
|
||||
zip="$cache_dir/node-$version-win-x64.zip"
|
||||
if [ ! -f "$zip" ]; then
|
||||
curl -fsSL -o "$zip.tmp" "https://nodejs.org/dist/$version/node-$version-win-x64.zip"
|
||||
local expected
|
||||
expected="$(curl -fsSL "https://nodejs.org/dist/$version/SHASUMS256.txt" \
|
||||
| awk -v a="node-$version-win-x64.zip" '$2 == a { print $1; exit }')"
|
||||
[ -n "$expected" ] || { echo "wine-windows-gates: no SHASUMS256 entry for node-$version-win-x64.zip" >&2; exit 1; }
|
||||
verify_sha256 "$expected" "$zip.tmp"
|
||||
mv "$zip.tmp" "$zip"
|
||||
fi
|
||||
else
|
||||
zip="$(ls -t "$cache_dir"/node-v"$node_major".*-win-x64.zip 2> /dev/null | head -1 || true)"
|
||||
[ -n "$zip" ] || { echo "wine-windows-gates: nodejs.org unreachable and no cached Windows Node v$node_major zip in $cache_dir" >&2; exit 1; }
|
||||
echo "wine-windows-gates: nodejs.org unreachable; using cached $(basename "$zip")" >&2
|
||||
fi
|
||||
unzip -q -o "$zip" -d "$scratch/node-win"
|
||||
echo "$scratch/node-win/$(basename "$zip" .zip)/node.exe" > "$scratch/node-win-path"
|
||||
}
|
||||
|
||||
boot_wine() {
|
||||
"$wine_bin" wineboot --init > /dev/null 2>&1 || true
|
||||
wineserver -w || true
|
||||
}
|
||||
|
||||
snapshot_and_install() {
|
||||
# Tracked + untracked-unignored files, minus agent-session litter; the
|
||||
# existence filter drops paths staged as deleted. Then the Wine-specific
|
||||
# install-time overrides go on the SNAPSHOT only: hoisted because Windows
|
||||
# Node under Wine does not realpath pnpm's isolated-layout symlinks, and
|
||||
# win32-x64 so the Windows esbuild/rolldown/rollup binaries materialize.
|
||||
# Neither is recorded in the lockfile, so --frozen-lockfile stays valid;
|
||||
# --ignore-scripts skips host lifecycle scripts no gate loads.
|
||||
git -C "$repo_root" ls-files -z --cached --others --exclude-standard -- . ':!:.claude' ':!:.codex' \
|
||||
| while IFS= read -r -d '' file; do [ -e "$repo_root/$file" ] && printf '%s\0' "$file"; done \
|
||||
| tar -C "$repo_root" --null --files-from=- -cf - \
|
||||
| tar -C "$scratch/tree" -xf -
|
||||
cat >> "$scratch/tree/pnpm-workspace.yaml" << 'EOF'
|
||||
|
||||
nodeLinker: hoisted
|
||||
supportedArchitectures:
|
||||
os: [current, win32]
|
||||
cpu: [current, x64]
|
||||
EOF
|
||||
(cd "$scratch/tree" && pnpm install --frozen-lockfile --ignore-scripts > "$scratch/logs/install.log" 2>&1) \
|
||||
|| { tail -40 "$scratch/logs/install.log" >&2; return 1; }
|
||||
}
|
||||
|
||||
mkdir "$scratch/tree"
|
||||
start=$SECONDS
|
||||
provision_node & node_pid=$!
|
||||
boot_wine & wine_pid=$!
|
||||
snapshot_and_install & install_pid=$!
|
||||
# Wait for EVERY child before judging any: a bare `wait` under set -e would
|
||||
# exit on the first failure and let the EXIT trap delete $scratch while the
|
||||
# other children still run inside it. Named statuses also make the report
|
||||
# point at the root cause instead of a downstream symptom.
|
||||
node_status=0; wait "$node_pid" || node_status=$?
|
||||
wine_status=0; wait "$wine_pid" || wine_status=$?
|
||||
install_status=0; wait "$install_pid" || install_status=$?
|
||||
provision_failed=0
|
||||
report_provision() {
|
||||
if (( $2 != 0 )); then
|
||||
echo "wine-windows-gates: FAILED $1 (exit $2)" >&2
|
||||
provision_failed=$2
|
||||
fi
|
||||
}
|
||||
report_provision 'Windows Node provisioning' "$node_status"
|
||||
report_provision 'wineboot' "$wine_status"
|
||||
report_provision 'workspace snapshot + pnpm install' "$install_status"
|
||||
if (( provision_failed != 0 )); then exit "$provision_failed"; fi
|
||||
node_win="$(cat "$scratch/node-win-path")"
|
||||
echo "wine-windows-gates: provisioned in $((SECONDS - start))s (wine $("$wine_bin" --version 2> /dev/null), node $(basename "$(dirname "$node_win")"))"
|
||||
|
||||
# ---- resolve entrypoints, lay the vue link, smoke ------------------------
|
||||
# Node under Wine cannot attach stdio to pipes the caller owns (Socket open
|
||||
# EBADF at bootstrap), so every invocation routes stdio through a file.
|
||||
wine_node() {
|
||||
local log="$1"
|
||||
shift
|
||||
local status=0
|
||||
"$wine_bin" "$node_win" "$@" < /dev/null > "$log" 2>&1 || status=$?
|
||||
return "$status"
|
||||
}
|
||||
|
||||
cd "$scratch/tree"
|
||||
tsc_js='node_modules/typescript/bin/tsc'
|
||||
tsdown_js='node_modules/tsdown/dist/run.mjs'
|
||||
vitepress_js='node_modules/vitepress/bin/vitepress.js'
|
||||
[ -f "$vitepress_js" ] || vitepress_js='website/node_modules/vitepress/bin/vitepress.js'
|
||||
for entry in "$tsc_js" "$tsdown_js" "$vitepress_js"; do
|
||||
[ -f "$entry" ] || { echo "wine-windows-gates: expected entrypoint missing after hoisted install: $entry" >&2; exit 1; }
|
||||
done
|
||||
# VitePress links vue into the site's node_modules at build time; Wine cannot
|
||||
# CREATE Windows symlinks (ENOTSUP) but follows pre-existing Unix ones.
|
||||
if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then
|
||||
mkdir -p website/node_modules
|
||||
ln -s ../../node_modules/vue website/node_modules/vue
|
||||
fi
|
||||
|
||||
wine_node "$scratch/logs/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
|
||||
cat "$scratch/logs/smoke.log"
|
||||
grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; }
|
||||
|
||||
# ---- the two blocking surfaces, concurrently ------------------------------
|
||||
# The same shape run-gates gives ci-windows-blocking on native Windows:
|
||||
# `build` = tsc -b then tsdown, `production site` = the VitePress build. Both
|
||||
# statuses are captured so one failure cannot hide the other's result.
|
||||
build_gate() {
|
||||
wine_node "$scratch/logs/tsc.log" "$tsc_js" -b --pretty false || return $?
|
||||
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
|
||||
}
|
||||
site_gate() {
|
||||
cd website
|
||||
wine_node "$scratch/logs/site.log" "../$vitepress_js" build .
|
||||
}
|
||||
|
||||
start=$SECONDS
|
||||
build_gate & build_pid=$!
|
||||
site_gate & site_pid=$!
|
||||
build_status=0
|
||||
wait "$build_pid" || build_status=$?
|
||||
site_status=0
|
||||
wait "$site_pid" || site_status=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
|
||||
report() {
|
||||
local label="$1" status="$2"
|
||||
shift 2
|
||||
if (( status == 0 )); then
|
||||
echo "wine-windows-gates: PASS $label (${elapsed}s window)"
|
||||
else
|
||||
echo "== FAILED $label (exit $status) ==" >&2
|
||||
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
||||
fi
|
||||
}
|
||||
report 'build (tsc -b, tsdown)' "$build_status" "$scratch/logs/tsc.log" "$scratch/logs/tsdown.log"
|
||||
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
|
||||
if (( build_status != 0 )); then exit "$build_status"; fi
|
||||
exit "$site_status"
|
||||
Reference in New Issue
Block a user